diff --git a/.travis.yml b/.travis.yml
index 8aff8b68..0534ebdb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,7 +8,7 @@ language: python # Can use any language here, but if it's not 'python'
python: "3.6"
env:
- - LUA_VERSION=5.3 BUILD_VERSION=`cat data/version.txt` ENABLE_RENDER=1
+ - LUA_VERSION=5.3 BUILD_VERSION=`cat data/version.txt` CARTRIDGES=`cat data/cartridges.txt` ENABLE_RENDER=1
cache:
directories:
@@ -69,14 +69,16 @@ script:
# we *do* want to ignore WIP tests flagged #mute
# test: all utests
- ITEST_CARTRIDGE_SUFFIX=ignore ./test.sh
- # test: all headless itests for titlemenu (ENABLE_RENDER set in env)
- - ITEST_CARTRIDGE_SUFFIX=titlemenu ./test.sh -f headless_itests
- # test: all headless itests for stage_intro (ENABLE_RENDER set in env)
- - ITEST_CARTRIDGE_SUFFIX=stage_intro ./test.sh -f headless_itests
- # test: all headless itests for stage (ENABLE_RENDER set in env)
- - ITEST_CARTRIDGE_SUFFIX=ingame ./test.sh -f headless_itests
- # test: all headless itests for stage_clear (ENABLE_RENDER set in env)
- - ITEST_CARTRIDGE_SUFFIX=stage_clear ./test.sh -f headless_itests
+ # test: all headless itests for each cartridge (ENABLE_RENDER set in env)
+ - |
+ for cartridge in $CARTRIDGES; do
+ ITEST_CARTRIDGE_SUFFIX="$cartridge" ./test.sh -f headless_itests
+ if [[ $? -ne 0 ]]; then
+ echo ""
+ echo "Headless itests failed for cartridge "$cartridge", STOP."
+ exit 1
+ fi
+ done
# coverage
- bash <(curl -s https://codecov.io/bash)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27b00d45..6697be9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [6.0] - 2021-08-17
+### Added
+- Character physics: spin dash (includes crouching)
+- Character physics: allow late jump up to 6 frames after leaving ground, to mimic modern platformer physics (can be disabled in pause menu)
+- Animation: crouch and spin dash animations, using new dynamic sprite reloading system to allow even more sprites on a single spritesheet (unfortunately smoke PFX was cut from Release as it took too many characters, although still present in code)
+- Camera: spin dash lag
+- Stage visual: added animated waterfalls at the beginning of the level (they actually use color palette swapping as in the original game)
+- Attract mode: added Attract mode when player waits for end of intro BGM on the title screen. This is played inside a new cartridge that is mostly a stripped version of the ingame cartridge + a puppet sequence to make Sonic move by himself
+
+### Changed
+- Application: upgraded to PICO-8 0.2.2c with full binary patching and upgraded custom web template to integrate latest improvements
+- UI: press O on title menu before menu appears to make it appear immediately
+- Credits: add mention of SAGE and itch.io URL
+- Camera: use small vertical window even on ground to avoid moving when character just moves by 1px up and down (due to new bumps)
+- Stage physics: fixed last descending slope tile connecting slope and loop having no collision
+- Stage physics/visual: reworked rock sprites to be smaller
+- Stage physics/visual: offset last emerald (orange) by 5px to the right
+- Stage physics/visual: replaced very low slopes with 1px bumps that are still considered flat ground to avoid slowing down character when running on them, while keeping the funny up-and-down motion
+- Stage visual: hide emerald behind leaves to make harder to find
+- Stage visual: improved forest hole lightshaft in background (now sprite instead of procedurally generated)
+- Stage visual: fixed one-way platform grass appearing in front of character
+- Stage visual: fixed background parallax to only move when camera moves by an integer pixel, not pixel fractions
+- Character physics: fixed detecting flat ground when running down slopes where some columns of the collision mask are empty
+- Character physics & Optimization: big overhaul with switch to "big steps" method instead of the expensive "pixel step" method. This applies to both grounded and airborne motion. First move by the full motion you'd expect on a single frame, ignoring obstacles. Then detect wall, ground and ceiling, if meaningful considering the current speed (with an extra final wall check if grounded). This effectively reduced complexity from O(speed) to O(1) and allows the game to run at 60 FPS consistently (with only a few 30 FPS drops when reloading memory e.g. to change region)
+- Optimization: optimized the sprite rotation method to use efficient code specific for 90-degree rotations, instead of general trigonometry with backward approach (particularly slow due to iterating on all pixels inside the bounding box containing a given disc).
+- Compressed characters: various refactoring and replacement of every constant with hardcoded strings/values (as pre-build step) to reduce compressed characters size and allow exporting cartridge again despite adding new features
+- Debug: fix and improve debug rays (development only)
+- Export: merged audio data with built-in ingame data cartridge to avoid going over the limit of 16 cartridges per export (attract mode cartridge). Also offset BGM tracks by 8 tracks to allow custom instruments to be used ingame. This was required for the new spin dash SFX.
+
## [5.4] - 2021-04-17
### Added
- Audio: added "got all emeralds" jingle with delay
@@ -247,7 +276,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Game: in-game: debug character flies X/Y on directional input, go back to title menu on reach goal
- Test: all busted unit tests in separator folder tests
-[Unreleased]: https://github.com/hsandt/sonic-pico8/compare/v5.3...HEAD
+[Unreleased]: https://github.com/hsandt/sonic-pico8/compare/v6.0...HEAD
+[6.0]: https://github.com/hsandt/sonic-pico8/compare/v5.3...v6.0
[5.3]: https://github.com/hsandt/sonic-pico8/compare/v5.2...v5.3
[5.2]: https://github.com/hsandt/sonic-pico8/compare/v5.1...v5.2
[5.1]: https://github.com/hsandt/sonic-pico8/compare/v5.0...v5.1
diff --git a/README.md b/README.md
index 86a847ae..df615c56 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ develop
![The 8 Pico Emeralds displayed in circle, each color corresponding to a color on the PICO-8 logo](doc/all_emeralds.png?raw=true)
-**pico sonic** is a partial demake of Sonic the Hedgehog 3 made with PICO-8. It features a simplified version of Angel Island Act 1 with some tweaks. Various classic Sonic games were used as reference, including the 8-bit games (Game Gear and Master System) which have sprites closer to what PICO-8's resolution and color palette, and the GBA titles which have more clear-cut graphics.
+**pico sonic** is a partial demake of Sonic the Hedgehog 3 made with [PICO-8](https://www.lexaloffle.com/pico-8.php). It features a simplified version of Angel Island Act 1 with some tweaks. Various classic Sonic games were used as reference, including the 8-bit games (Game Gear and Master System), which have sprites closer to PICO-8's resolution and color palette, and the GBA titles, which have more clear-cut graphics.
The project was started as a personal challenge and was meant to be a fully-fledged fan game, but I eventually dropped many features to focus on Sonic's main movements and the exploration of the stage. Consider it a technical demo with some exploration challenge.
@@ -28,7 +28,7 @@ pico sonic is a fan game distributed for free and is not endorsed by Sega. Sega
## Compatibility
-Works with PICO-8 0.2.0i ~ 0.2.1b.
+Works with PICO-8 0.2.2.
## Features
@@ -76,6 +76,26 @@ Version: 5.3
* Spin dash
* When you collect all emeralds...
+### Notable physics differences
+
+* Preservation of velocity when landing on slopes is more organic and uses vector projection, while the [SPG](https://info.sonicretro.org/SPG:Slope_Physics#Reacquisition_Of_The_Ground) denotes different formulas based on the slope angle and the relationship between horizontal and vertical speed. This is very perceptible when jumping on the first two slopes.
+* It is possible to control horizontal acceleration after jumping out of a roll. This was considered to be a better user experience, and actually recommended by the Sonic Physics Guide despite being unlike the original games.
+* Late jump: as in modern platforms, the character can jump up to 6 frames after falling off ground, for more permissive jumps from a platform ledge. This can be disabled in the Pause menu for a more "classic" experience.
+* Pixel step-by-step approach: currently, character motion is computed pixel by pixel. This is very precise and avoids relying on ground escape, but is also very CPU expensive (the main reason behind FPS dropping to 30) and can get the character stuck if one calculation is wrong. It will probably be replaced with bigger steps and ground escape in the future.
+
+### Notable camera differences
+
+Because PICO-8 has a square view of 128x128 pixels, and the game is more about exploration than moving toward the right, camera was adjusted to make navgiation a little easier.
+
+* Camera is fundamentally centered on X, but moves toward the direction Sonic is facing. When Sonic is running, camera moves even more forward to show what is ahead
+* Spin dash lag is implemented by freezing then releasing the camera, instead of the more complex recording and playing of character positions during the start of the spin dash
+
+### Notable sprite differences
+
+* Sonic uses the "jump fall" sprite from Sonic CD/Mania as spring jump sprite (although it's not technically correct since it shouldn't be used for upward motion)
+
+* I reversed the order the Brake sprites so it made more sense visually. Now, Sonic just plays a short 2-sprite brake animation when you start moving in the opposite direction of running. If you keep moving in the opposite direction, it shows the "reverse brake" sprite, which gives more the impression than Sonic is doing a complete turn and sprinting in the opposite direction.
+
## Content
There is a single demo stage which covers the first part of Angel Island Act 1. Scale is close to 1:1, but Sonic is slightly smaller (relatively to the environment) than in the original game.
@@ -108,6 +128,7 @@ If you gamepad mapping is not correct, you can customize it with [SDL2 Gamepad T
In the pause menu (toggled with Enter/Start), if you are in-game, you can select the following options:
+* Late jump: press left/right to toggle the Late jump feature ON/OFF (default: ON)
* Warp to start: restart stage from beginning keeping collected emeralds
* Retry from zero: restart stage losing emeralds collected so far
* Back to title: go back to title menu
diff --git a/build_all_cartridges.sh b/build_all_cartridges.sh
index a9d0fe2a..b9a035f0 100755
--- a/build_all_cartridges.sh
+++ b/build_all_cartridges.sh
@@ -5,6 +5,7 @@
# Configuration: paths
game_scripts_path="$(dirname "$0")"
+data_path="$(dirname "$0")/data"
help() {
echo "Build a PICO-8 cartridge with the passed config."
@@ -57,7 +58,10 @@ if [[ ${#positional_args[@]} -ge 1 ]]; then
config="${positional_args[0]}"
fi
-cartridge_list="titlemenu stage_intro ingame stage_clear"
+# cartridges.txt lists cartridge names, one line per cartridge
+# newlines act like separators for iteration just like spaces,
+# so this is equivalent to `cartridge_list="titlemenu stage_intro ..."`
+cartridge_list=`cat "$data_path/cartridges.txt"`
for cartridge in $cartridge_list; do
"$game_scripts_path/build_single_cartridge.sh" "$cartridge" "$config"
diff --git a/build_and_install_all_cartridges.sh b/build_and_install_all_cartridges.sh
index 9aa6ec86..16ac83c9 100755
--- a/build_and_install_all_cartridges.sh
+++ b/build_and_install_all_cartridges.sh
@@ -13,12 +13,14 @@ help() {
}
usage() {
- echo "Usage: build_and_install_all_cartridges.sh [CONFIG]
+ echo "Usage: build_and_install_all_cartridges.sh [CONFIG] [OPTIONS]
ARGUMENTS
CONFIG Build config. Determines defined preprocess symbols.
(default: 'debug')
+ -i, --itest Pass this option to build an itest instead of a normal game cartridge.
+
-h, --help Show this help message
"
}
@@ -30,6 +32,10 @@ config='debug'
# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash
while [[ $# -gt 0 ]]; do
case $1 in
+ -i | --itest )
+ itest=true
+ shift # past argument
+ ;;
-h | --help )
help
exit 0
@@ -57,5 +63,13 @@ if [[ ${#positional_args[@]} -ge 1 ]]; then
config="${positional_args[0]}"
fi
-"$game_scripts_path/build_all_cartridges.sh" "$config"
-"$game_scripts_path/install_all_cartridges.sh" "$config"
+if [[ "$itest" == true ]]; then
+ # itest cartridges enforce special config 'itest' and ignore passed config
+ config='itest'
+ options='--itest'
+else
+ options=''
+fi
+
+"$game_scripts_path/build_all_cartridges.sh" "$config" $options
+"$game_scripts_path/install_all_cartridges.sh" "$config" $options
diff --git a/build_and_install_single_cartridge_with_data.sh b/build_and_install_single_cartridge_with_data.sh
index 9a886706..cd26191a 100755
--- a/build_and_install_single_cartridge_with_data.sh
+++ b/build_and_install_single_cartridge_with_data.sh
@@ -18,11 +18,13 @@ usage() {
ARGUMENTS
CARTRIDGE_SUFFIX Cartridge to build for the multi-cartridge game
- 'titlemenu', 'stage_intro', ingame' or 'stage_clear'
+ See data/cartridges.txt for the list of cartridge names
CONFIG Build config. Determines defined preprocess symbols.
(default: 'debug')
+ -i, --itest Pass this option to build an itest instead of a normal game cartridge.
+
-h, --help Show this help message
"
}
@@ -34,6 +36,10 @@ config='debug'
# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash
while [[ $# -gt 0 ]]; do
case $1 in
+ -i | --itest )
+ itest=true
+ shift # past argument
+ ;;
-h | --help )
help
exit 0
@@ -65,8 +71,16 @@ if [[ ${#positional_args[@]} -ge 2 ]]; then
config="${positional_args[1]}"
fi
+if [[ "$itest" == true ]]; then
+ # itest cartridges enforce special config 'itest' and ignore passed config
+ config='itest'
+ options='--itest'
+else
+ options=''
+fi
+
# Immediately export to carts to allow multi-cartridge loading
-"$game_scripts_path/build_single_cartridge.sh" "$cartridge_suffix" "$config"
+"$game_scripts_path/build_single_cartridge.sh" "$cartridge_suffix" "$config" $options
if [[ $? -ne 0 ]]; then
echo ""
@@ -74,4 +88,4 @@ if [[ $? -ne 0 ]]; then
exit 1
fi
-"$game_scripts_path/install_single_cartridge_with_data.sh" "$cartridge_suffix" "$config"
+"$game_scripts_path/install_single_cartridge_with_data.sh" "$cartridge_suffix" "$config" $options
diff --git a/build_itest.sh b/build_itest.sh
index bce294e9..09312549 100755
--- a/build_itest.sh
+++ b/build_itest.sh
@@ -4,7 +4,7 @@
# This is essentially a proxy script for pico-boots/scripts/build_cartridge.sh with the right parameters.
# Usage: build_itest.sh cartridge_suffix
-# cartridge_suffix 'titlemenu', 'stage_intro', 'ingame' or 'stage_clear'
+# See data/cartridges.txt for the list of cartridge names
# Configuration: paths
picoboots_scripts_path="$(dirname "$0")/pico-boots/scripts"
diff --git a/build_single_cartridge.sh b/build_single_cartridge.sh
index 8f0d8166..53a43a68 100755
--- a/build_single_cartridge.sh
+++ b/build_single_cartridge.sh
@@ -6,6 +6,7 @@
# Configuration: paths
picoboots_scripts_path="$(dirname "$0")/pico-boots/scripts"
+game_prebuild_path="$(dirname "$0")/prebuild"
game_src_path="$(dirname "$0")/src"
data_path="$(dirname "$0")/data"
build_dir_path="$(dirname "$0")/build"
@@ -22,28 +23,35 @@ help() {
}
usage() {
- echo "Usage: build_single_cartridge.sh CARTRIDGE_SUFFIX [CONFIG]
+ echo "Usage: build_single_cartridge.sh CARTRIDGE_SUFFIX [CONFIG] [OPTIONS]
ARGUMENTS
CARTRIDGE_SUFFIX Cartridge to build for the multi-cartridge game
- 'titlemenu', 'stage_intro', 'ingame' or 'stage_clear'
+ See data/cartridges.txt for the list of cartridge names
A symbol equal to the cartridge suffix is always added
to the config symbols.
CONFIG Build config. Determines defined preprocess symbols.
(default: 'debug')
+ -i, --itest Pass this option to build an itest instead of a normal game cartridge.
+
-h, --help Show this help message
"
}
# Default parameters
config='debug'
+itest=false
# Read arguments
# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash
while [[ $# -gt 0 ]]; do
case $1 in
+ -i | --itest )
+ itest=true
+ shift # past argument
+ ;;
-h | --help )
help
exit 0
@@ -75,6 +83,11 @@ if [[ ${#positional_args[@]} -ge 2 ]]; then
config="${positional_args[1]}"
fi
+# itest cartridges enforce special config 'itest' and ignore passed config
+if [[ "$itest" == true ]]; then
+ config='itest'
+fi
+
# Define build output folder from config
# (to simplify cartridge loading, cartridge files are always named the same,
# so we can only distinguish builds by their folder names)
@@ -86,12 +99,15 @@ symbols=''
if [[ $config == 'debug' ]]; then
# symbols='assert,deprecated,log,visual_logger,tuner,profiler,mouse,cheat,sandbox'
# lighter config (to remain under 65536 chars)
- symbols='assert,tostring,dump,log,debug_menu,debug_character'
+ # symbols='assert,tostring,dump,log,debug_menu,debug_character'
+ # symbols='tostring,dump,log,debug_menu,debug_character,cheat'
+ # symbols='debug_menu,debug_character,cheat'
+ symbols='tostring,dump,debug_character,debug_menu,debug_collision_mask,cheat,pfx'
elif [[ $config == 'debug-ultrafast' ]]; then
symbols='assert,tostring,dump,log,cheat,ultrafast'
elif [[ $config == 'cheat' ]]; then
# symbols='cheat,tostring,dump,log,debug_menu'
- symbols='assert,cheat,debug_menu'
+ symbols='cheat,tostring,dump,debug_menu'
elif [[ $config == 'tuner' ]]; then
symbols='tuner,mouse'
elif [[ $config == 'ultrafast' ]]; then
@@ -99,11 +115,28 @@ elif [[ $config == 'ultrafast' ]]; then
elif [[ $config == 'cheat-ultrafast' ]]; then
symbols='cheat,ultrafast,debug_menu'
elif [[ $config == 'sandbox' ]]; then
- symbols='assert,deprecated,sandbox'
+ # symbols='assert,deprecated,sandbox'
+ symbols='sandbox,assert,tuner,mouse'
elif [[ $config == 'assert' ]]; then
- symbols='assert,tostring,dump'
+ # symbols='assert,tostring,dump'
+ symbols='assert,tostring,debug_collision_mask'
elif [[ $config == 'profiler' ]]; then
- symbols='profiler,cheat'
+ # symbols='profiler,debug_menu'
+ # profiler is too heavy right now, cannot build, so use lightweight version
+ symbols='profiler_lightweight,cheat'
+elif [[ $config == 'recorder' ]]; then
+ symbols='recorder,tostring,log'
+elif [[ $config == 'itest' ]]; then
+ # cheat needed to set debug motion mode; remove if not testing and you need to spare chars
+ # symbols='itest,proto,tostring,cheat'
+ symbols='itest,proto,tostring'
+elif [[ $config == 'release' ]]; then
+ # usually release has no symbols except those that help making the code more compact
+ # in this game project we define 'release' as a special symbol for that
+ # most fo the time, we could replace `#if release` with
+ # `#if debug_option1 || debug_option2 || debug_option3 ` but the problem is that
+ # 2+ OR statements syntax is not supported by preprocess.py yet
+ symbols='release'
fi
# we always add a symbol for the cartridge suffix in case
@@ -115,23 +148,70 @@ if [[ -n "$symbols" ]]; then
fi
symbols+="$cartridge_suffix"
+# Define builtin data to use (in most cases it's just the cartridge suffix)
+if [[ $cartridge_suffix == 'sandbox' ]]; then
+ # for now we just need to test Sonic sprites in sandbox (e.g. rotation)
+ # data_filebasename="data_stage_sonic"
+ data_filebasename="data_stage1_ingame"
+else
+ if [[ $cartridge_suffix == 'attract_mode' ]]; then
+ # attract mode reuses same data as ingame, so no need for dedicated data cartridge
+ builtin_data_suffix="ingame"
+ # we must also define the ingame symbols to have access to all ingame code
+ # (as opposed to stage_intro / stage_clear code)
+ symbols+=",ingame"
+ else
+ if [[ $cartridge_suffix == 'ingame' ]]; then
+ # add symbol #normal_mode to distinguish playable ingame from attract_mode,
+ # as both define #ingame
+ symbols+=",normal_mode"
+ fi
+ builtin_data_suffix="$cartridge_suffix"
+ fi
+
+ if [[ "$itest" == true ]]; then
+ main_prefix='itest_'
+ required_relative_dirpath="itests/${cartridge_suffix}"
+ cartridge_extra_suffix='itest_all_'
+ else
+ main_prefix=''
+ required_relative_dirpath=''
+ cartridge_extra_suffix=''
+ fi
+ data_filebasename="builtin_data_${builtin_data_suffix}"
+fi
+
+# Define list of data module paths, separated by space (Python argparse nargs='*')
+game_constant_module_paths_string="${game_src_path}/data/camera_data.lua \
+${game_src_path}/data/playercharacter_numerical_data.lua \
+${game_src_path}/data/stage_clear_data.lua \
+${game_src_path}/data/stage_common_data.lua \
+${game_src_path}/resources/audio.lua \
+${game_src_path}/resources/visual_ingame_numerical_data.lua"
+
# Build cartridges without version nor config appended to name
# so we can use PICO-8 load() with a cartridge file name
# independent from the version and config
-# Build cartridge ('titlemenu', 'stage_intro', 'ingame' or 'stage_clear')
+# Build cartridge
+# See data/cartridges.txt for the list of cartridge names
# metadata really counts for the entry cartridge (titlemenu)
-"$picoboots_scripts_path/build_cartridge.sh" \
- "$game_src_path" main_${cartridge_suffix}.lua \
- -d "$data_path/builtin_data_${cartridge_suffix}.p8" \
- -M "$data_path/metadata.p8" \
- -a "$author" -t "$title ($cartridge_suffix)" \
- -p "$build_output_path" \
- -o "${cartridge_stem}_${cartridge_suffix}" \
- -c "$config" \
- --no-append-config \
- -s "$symbols" \
- --minify-level 3 \
+"$picoboots_scripts_path/build_cartridge.sh" \
+ "$game_src_path" \
+ ${main_prefix}main_${cartridge_suffix}.lua \
+ ${required_relative_dirpath} \
+ -d "${data_path}/${data_filebasename}.p8" \
+ -M "$data_path/metadata.p8" \
+ -a "$author" -t "$title (${cartridge_extra_suffix}${cartridge_suffix})" \
+ -p "$build_output_path" \
+ -o "${cartridge_stem}_${cartridge_extra_suffix}${cartridge_suffix}" \
+ -c "$config" \
+ --no-append-config \
+ -s "$symbols" \
+ -g "$game_constant_module_paths_string" \
+ -r "$game_prebuild_path" \
+ -v version="$version" \
+ --minify-level 3 \
--unify "_${cartridge_suffix}"
if [[ $? -ne 0 ]]; then
diff --git a/data/builtin_data_ingame.p8 b/data/builtin_data_ingame.p8
index c666d82a..e8150834 100644
--- a/data/builtin_data_ingame.p8
+++ b/data/builtin_data_ingame.p8
@@ -1,11 +1,16 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic builtin data: ingame
-- by leyn
-- this section will be overwritten during build
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+-- Note that we integrate the collision masks as built-in spritesheet just to parse them and store collision data
+-- We will load the stage spritesheet at runtime
+import "spritesheet_stage_collision_masks.png"
+
-- the collision masks at the top will be overwritten by runtime sprites
-- via reload
__gfx__
@@ -17,129 +22,129 @@ __gfx__
00700700777777777777777777777777777777777777000000000000000000000000000000007777777777777777777777777777770000000000007777777777
00000000777777777777777777777777777777777777777777777777000000000000000077777777777777777777777777777777777700000000777777777777
00000000777777777777777777777777777777777777777777777777777700000000777777777777777777777777777777777777777777000077777777777777
-70000000700000000000000000000000000000070000000700000000777777777777777700000000000000000000000000007777777777770077777777777700
-77000000770000000000000000000000000000770000007700000007077777777777777070000000000000000000000000007777777777770077777777777700
-77700000777700000000000000000000000077770000077700000007077777777777777070000000000077777777777700007777777777770077777777777700
-77770000777770000000000000000000000777770000777700000077007777777777770077000000000077777777777700007777777777770077777777777700
-77777000777777700000000000000000077777770007777700000077000777777777700077000000000077777777777700007777777777770077777777777700
-77777700777777777000000000000007777777770077777700000077000777777777700077000000000077777777777700007777777777770077777777777700
-77777770777777777770000000000777777777770777777700000777000077777777000077700000000077777777777700007777777777770077777777777700
-77777777777777777777700000077777777777777777777700000777000077777777000077700000000077777777777700007777777777770077777777777700
-77777777777777777777700000077777777777777777777700007777000007777770000077770000000000000000007700000000000000000000000000000000
-77777770777777777770000000000777777777770777777700007777000007777770000077770000000000000000777700000000000000000000000000000000
-77777700777777777000000000000007777777770077777700077777000000777700000077777000000000000077777700000000000000000000000000000000
-77777000777777700000000000000000077777770007777700077777000000777700000077777000000000777777777700000000000000000000000000000000
-77770000777770000000000000000000000777770000777700777777000000777700000077777700000077777777777700007777000000000000000000000000
-77700000777700000000000000000000000077770000077707777777000000077000000077777770007777777777777700007777000000000000000000000000
-77000000770000000000000000000000000000770000007707777777000000077000000077777770777777777777777700007777000000000000000000000000
-70000000700000000000000000000000000000070000000777777777000000077000000077777777777777777777777700007777000000000000000000000000
-44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
-44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
-44949444bbbbbbbbabbbbbbababbeaaeebeaeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaaebbabbbbbbb3bbbbbbaeeeeeeeeeeeeeeeeeeaeabbbbb
-44499444b3babbb3bbbbabbababbb3ababbababaeaeeeeaeeeeeaeeeeeeeeeeeeeeeeeeeeaeeeeaeba3bbbabbb3bab3bb3bababbeaeeeeeeeeeeeeaeabbab3bb
-44949944b3bb3bb3bb3ba3bbb3bbb3b3b3bbbbbababaebaeeeaebeeaeeeeeeeeeeeeeeeeeabeababbb3bbbbbbb3bbbbbb3babbbbbaeaeeeeeeeeaeabbbbab3bb
-44949944bbbb3bbbbb3bb3b3bbb3b3bbb3bb3bb3bbbabbabababbabbbaeeaeeeeeeaeeabbabbabbbbb3bbbbbbbbbbb3bb3bbb3bbbbbaeeaeeaeeabbbb3bbbb3b
-44449444bb3bbbbb3bbbb3bbbbb3bbbbbbbb3bb3bb3abbbbbb3bbbbbbabaaeaeeaeaababbbbbabbbbbbb3b3bbbbbbbbbbbbbbbbb3ababbaeeabbaba3b3b3bb3b
-44949444bbbbbbbbbbbbbbbbbbbb3bbbb3bbbbbbbbbbbb3bbbbbbb3bbababbabbabbababb3bbbbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-44999444bb0bb3b3bb3bb0bbbbb303bbb3b3bb3bb3bb3b3bb3b3bbbbb3bbbbabbabbbbbbbbbb3b3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-449944443003b030b30b30033b300033bbbbbb3bb3bbbbbbbbb3bb3bb3bbbbbbbbbb3b3bb3bb3bbbeeee999aaa7777a9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-49494944004030003003040003004000bbbb0bbbbbb0bbbb3bbbb33bbbbb3b3bbbbbbb3bb33bbbb3eeee444999aa7aa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-44499444044400440000044440044440b03b03bbbb30b30b033b303bbbbbbb3bbb3bbbbbb303b330eeee44999aa777a9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
-44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
-444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+70000000700000000000000000000000000000070000000700000000777777777777777700000000000000000000000000000000777777770077777777777700
+77000000770000000000000000000000000000770000007700000007077777777777777070000000000000000000000000000007777777770077777777777700
+77700000777700000000000000000000000077770000077700000007077777777777777070000000000077770000000000000077777777770077777777777700
+77770000777770000000000000000000000777770000777700000077007777777777770077000000000077770000000000000077777777770077777777777700
+77777000777777700000000000000000077777770007777700000077000777777777700077000000000077770000000000000777777777770077777777777700
+77777700777777777000000000000007777777770077777700000077000777777777700077000000000077770000000000000777777777770077777777777700
+77777770777777777770000000000777777777770777777700000777000077777777000077700000000077770000000000007777777777770077777777777700
+77777777777777777777700000077777777777777777777700000777000077777777000077700000000077770000000000007777777777770077777777777700
+77777777777777777777700000077777777777777777777700007777000007777770000077770000000000000000007700077777000000000000000000000000
+77777770777777777770000000000777777777770777777700007777000007777770000077770000000000000000777700077777000000000000000000000000
+77777700777777777000000000000007777777770077777700077777000000777700000077777000000000000077777700777777000000000000000000000000
+77777000777777700000000000000000077777770007777700077777000000777700000077777000000000777777777700777777000000000000000000000000
+77770000777770000000000000000000000777770000777700777777000000777700000077777700000077777777777707777777000000000000000000000000
+77700000777700000000000000000000000077770000077707777777000000077000000077777770007777777777777707777777000000000000000000000000
+77000000770000000000000000000000000000770000007707777777000000077000000077777770777777777777777777777777000000000000000000000000
+70000000700000000000000000000000000000070000000777777777000000077000000077777777777777777777777777777777777777770000000000000000
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141414141414141434341414141414141410000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000414141414141414141414141414101014141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
000000000000000000006e6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e49466e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f497e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f000000000000000000007c7d7c7d7c7d7c7d1e1f1e1f1e1f1e1f5e5f5e5f5f
000000000000000000006e6f6e6f6e6f6e6f6f585945434443454244455554307e7f7e7f7e7f7e7f7e7f7e7f7e2e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f6f6e6f6e6f6e6f6e6f6e6f44417000000000000000000000000000000000000000000000000000000000000000000000000000007c7d5e5f5e5f5e5f6e6f6e6f6f
@@ -176,108 +181,102 @@ __map__
__sfx__
011000003005032050300503205030050320503005032050300503205030050320503005032050300503205030050320503005032050300503205030050320503005032050300503205000000000000000000000
010c00001835018350183501835018350183501835018350183501835018350183501835018350183501835018350183501835018350183501835018350183501835018350183501835018350183501835018350
+000400001e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3401e2401e3401e2401e3301e2301e3301e2301e3301e2201e3201e2201e3201e2101e3101e2101e310
+00040000396503d6503f6503f6503d6503b6503965035650326403064030640306403063030630306303063030620306203062030620306203061030610306103061030610306103061030610306103061030610
+001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+010400001e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e3501e2501e350
+010700001f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c130211301d130211301d130211301d130211301d130
+010700000c1300c1300c1300c13518130181350c0000c00016130161301613016135151301513516130161350c0000c000151301513516130161350c0000c00016130161350c0000c00018130181301813018135
+01070000221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130211301d130211301d130211301d130211301d130
+010700000c1300c1300c1300c13518130181350c1000c10016130161301613016135151301513516130161350c1000c1000a1300a1350c1000c1000a1300a1350c1000c1000a1300a1350b1300b1300b1300b135
+010700002113022130211301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f130
+0107000024130241352413024135301303013524130241352e1302e13524130241352e1302e13530130301351d1301d1352213022135291302913528130281302813028130281302813028130281350000000000
+0107000030140301453014030145301403014530140301450c0000c0000c0000c0000c0000c0000c0000c0000c0000c0000c0000c0001d1301d1301d1301d1352213022130221302213529130291302913029135
+010700001813018135181301813518130181351813018135110001100011000110001100011000110001100011000110001100011000110001100011000110001100011000110001100011000110001100011000
+010700001c1301c1301c1301c13500000000001813018130181350000000000000001613016130161301613016130161301613016130161301613016130161301613016130161301613016130161301613500000
+010700000c1300c1300c1300c1300c1300c1300c1300c1350c1300c1300c1300c1350c1000c1050c1300c1300c1300c1300c1300c135071300713007130071350c1300c1300c1300c13507130071300713007135
+01070000000000000000000000002d1302d1352e1302e13500000000002e130000002e130000002e130000002e1302e13500000000002d1302d1302d1302d1302d1302d1302d1302d13500000000000000000000
+010700001c1301c1301c1301c13500000000001813018130181350000000000000001a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a13500000
+010700000000000000000000000029130291352b1302b13500000000002b130000002b130000002b130000002b1302b135000000000011130111301113011135161301613016130161351d1301d1301d1301d135
+01070000000000000000000000002d1302d1352e1302e13500000000002e130000002e130000002e130000002e1302e135000000000030130301352e1302e1302e1302e1302e1302e13500000000000000000000
+01070000000000000000000000002d1302d1352e1302e13500000000002e130000002e130000002e130000002e1302e135000000000030130301352e1302e1302e1302e1302e1302e13528130281352913029135
+010700002b1302b1302b1302b13500000000002413024130241350000000000000002e1302e1302e1302e1302e1302e1352e130000002e1302e13500000000002e1302e1302e1302e1352b1302b1302b1302b135
+010700002d1302d1302d1302d13500000000002913029130291350000000000000002413024130241302413024130241302413024130241302413024130241302413024130241302413528130281352913029135
+010700003c1203c1203c1203c12539120391253a1203a1203a1203a125371203712037120371253c1203c1203c1203c12539120391253a1203a1203a1203a1253712037120371203712500100001000010000105
+010700002b1302b1302b1302b13500000000002413024130241350000000000000002e1302e1302e1302e1302e1302e1302e1302e1352e1302e1302e1302e1352e1302e1302e1302e13530130301303013030135
+010700002d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d13500000000000000000000000000000000000000000000000000000000000028130281352913029135
+010700002d1302d1302d1302d1302d1302d1352e1302e1302e1302e1302e1302e1353013030130301303013030130301303013030130301303013030130301350000000000000000000000000000000000000000
+010700003c1203c1203c1203c12539120391253a1203a1203a1203a12537120371203712500000000000000000000000002113000000221300000024130000002713000000261300000022130000002413000000
+01070000000000000000000000001f1301f1301f1301f135241302413024130241352b1302b1302b1302b13529130291302913029135000000000028130281302813500000000000000024130241302413024135
+0107000032130000002f130000002b130000002813000000261300000023130000001f130000001c1300000030130000002f130000002b130000002813000000241300000023130000001f130000001c13000000
+010700002113021130211302113021130211302113021130211202112021120211202413024130241302413523130231302313023135000000000024130241302413500000000000000026130261302613026135
+0107000030130000002d1300000029130000002613000000241300000021130000001d130000001a130000002f130000002d1300000029130000002613000000231300000021130000001d130000001a13000000
+010700002813028130281302813028130281302813028130281302813028130281302812028120281202812021130211302113021130211302113021130211302113021130211302113021130211302113021135
+0107000010130101301013010130101301013010130101351013010130101301013010130101350c1300c1300c1300c1300c1300c1350c1300c1300c1300c135101301013010130101350c1300c1300c1300c135
+01070000291302913029130291350000000000281302813028135000000000000000261302613026130261352313023130231302313500000000002113021130211350000000000000001f1301f1301f1301f135
+010700000e1300e1300e1300e1300e1300e1300e1300e1350e1300e1300e1300e1300e1300e1350c1300c1300c1300c135131300c000131301313013130131351313013130131301313511130111301113011135
+010700002b1302b1302b1302b1302b1302b1302b1302b1302b1302b1302b1302b1302b1202b1202b1202b12028130281302813028130281302813028130281302813028130281302813021130211302113021135
+010700000e1300e1300e1300e1300e1300e1300e1300e1350e1300e1300e1300e1300e1300e1350c1300c1300c1300c135131300c000131301313013130131351313013130131301313512130121301213012135
+010700002813028130281302813500000000002813000000000000000000000000001d1301d1351f1301f13521130211302113021130211302113523130231302313023130231302313524130241302413024135
+0107000011130111301113011130111301113011130111351113011130111301113011130111350c1300c1300c1300c1300c1300c1350c1300c1300c1300c1351113011130111301113511130111301113011135
+010700000e1300e1300e1300e1300e1300e1300e1300e135000000000000000000001d130000001f130000002113021130211302113021130211351f1301f1301f1301f1301f135000001d1301d1301d1301d135
+01070000111301113515130151351a1301a1351d1301d1351a1301a1351d1301d135211302113526130261352113021135261302613529130291352d1302d13529130291352d1302d13532130321353513035135
+010700001513015130151301513015130151301513015130151301513015130151301513015130151301513511130111301113011130111301113011130111301113011130111301113011130111301113011135
+010700001813018130181301813018130181301813018135171301713017130171301713017130171301713518130181301813018130181301813018130181351a1301a1301a1301a1301a1301a1301a1301a135
+010700001513015130151301513015130151301513015135131301313013130131301313013130131301313515130151301513015130151301513015130151351713017130171301713017130171301713017135
+0107000030100301053010030105301003010530100301050c0000c0000c0000c0000c0000c0000c0000c0000c0000c0000c0000c0001d1301d1301d1301d1352213022130221302213529130291302913029135
+010700001862532605256053260527605266051f62526605186052660500605006051e3230060500605006051862500605006050060500605006051f625006052b6050060500605006051e323286052660500605
+010700001862518605256053260527605266051f62526605246052660500605006051e3230060526605006052b60500605006050060500605006051f6250060526605006051e625006051f323006052660500605
+01070000303230060530323256053032325605303230060524625006052a6252b6052a6252b6052b6252b60524625006052b625006052b6252b6252b62500605006052b6252b605006052b6252b6050060500605
+0107000018625326052560532605276052660526605266051862526605006050060526605006052660500605196350060519635006051960500605196351f6052660500605196350060519635006051f60500605
+0107000018625326052560532605276051f6051f63526605186052660500605006051e6351e6051e60500605196350060519635006051963500605196051f6051963500605196350060519635006051f60500605
+010700001f6351f6351f63500000000001f6051f6350000000000000001e635000001963500000186350000018635000000000000000186350000000000000001863500000000000000018635186351963500000
001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0110000024755287552b75530755307002d7550000032755327000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-010900003f5733f5633f5433f5233f5133f5233f5433f5633f5733f5633f5433f5233f5133f503025030250302503025030250301503025030250302503025030050300503005030050300503005030050300503
010300001354013540145401555016550185501b5601e560235712a56100501005010050100501005010050100501005010050100501005010050100501005010050100501005010050100000000000000000000
01030000144721447215462164621a4621d4522144222432234222342224422244222442225412254122541225412254121640216402164021640216402164021640216402164021640216402164021640216402
01070000285512c5512d5512d5512d5512d5412d5412d5312d5212d5212d5112d5112d5112d5112d5112d5002a5012a5012a5012a5012a5012c5012c5012c5012c5012d5012d5012d5011e5011e5011e5011e501
010400002433024330243002433024330243302433024330243302433024330243302433022300223000830008300083000830008300083000830008300083000830008300083000830008300083000830008300
-011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+0110000024755287552b75530755307002d7550000032755327000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+010900003f5733f5633f5433f5233f5133f5233f5433f5633f5733f5633f5433f5233f5133f503025030250302503025030250301503025030250302503025030050300503005030050300503005030050300503
+0004000027a702ba702da702ea702fa7030a7032a7035a7036a7036a7037a7037a7037a6037a5037a4037a4037a3037a3037a3037a2037a2037a2037a2037a1037a1037a1037a1037a1037a1037a1037a1037a10
+0004000034b6538b753ab753bb753bb753bb7532b7529b6526b6522b651eb551bb5519b5516b5515b5513b5511b4510b450fb450eb450db450cb350bb350bb350ab350ab350ab3509b3508b1508b1508b1508b15
+0104000022f702af702ef7031f7033f7036f7036f7037f7037f7037f7037f7037f7037f6037f5037f4037f4037f3037f3037f3037f2037f2037f2037f2037f1037f1037f1037f1037f1037f1037f1037f1037f10
__music__
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-00 41424344
-04 39777879
-
+01 08093040
+00 0a0b3140
+00 0c093040
+00 0d0b3140
+00 08093040
+00 0a0b3140
+00 0c093040
+00 0e0f3240
+00 10113040
+00 2f123140
+00 13113040
+00 14153140
+00 10113040
+00 2f123140
+00 13113040
+00 16153340
+00 17113040
+00 18193140
+00 1a113040
+00 1b193140
+00 17113040
+00 18193140
+00 1a113040
+00 1c1d3440
+00 1e1f3040
+00 20213140
+00 22233040
+00 24253140
+00 1e1f3040
+00 20213140
+00 26233040
+00 24273140
+00 28293040
+00 282a3140
+00 2b2c3040
+02 2d2e3540
diff --git a/data/builtin_data_stage_clear.p8 b/data/builtin_data_stage_clear.p8
index 11ef5df9..ed323dc5 100644
--- a/data/builtin_data_stage_clear.p8
+++ b/data/builtin_data_stage_clear.p8
@@ -1,11 +1,16 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic builtin data: stage_clear
-- by leyn
-- this section will be overwritten during build
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+-- Note that we don't need the collision masks for the stage clear sequence as Sonic is not visible,
+-- so we just build with the stage clear spritesheet directly.
+import "spritesheet_stage_clear.png"
+
-- the collision masks at the top will be overwritten by runtime sprites
-- via reload
__gfx__
@@ -17,22 +22,22 @@ __gfx__
00700700ebb7bb3bb3b7beee56d4aaa047aa4a7aa047a0655699ccccccccccc95759756557755eeeeedd78049087ddeeee2e272eee287778e272e2ee877782ee
00000000ebb77bbbb777bbee564aa0047aaaa700a04aa065569ccccc77cccc7c57757565575eeeeeeeedde8008eddeeeeeeee2eeeeee272eee2eeeeee272eeee
00000000eebb777777bbbbee564a0a04a004aa0d704aa06556ccccc7077cc07c95757565e5eeeeeeeeeeeee88eeeeeeeeeeeeeeeeeeee8eeeeeeeeeeee8eeeee
-eeed6deeeb773777777bbeee564a0a70a0d4aaaa707a0d655cccccc70777707c95777565e00000eeeeeeccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeccccceeeee
-eee777eebb77bb37777eeeee564a00a0a447aa44ad7a006556999cc7077ff07c995775650eeeee0eeeeeeccccceeceeeeeeeccccceeeeeeeeeeeeeeccccccece
-ee66666eeebbeebb777bb7bb560aaaa0aaaa0a00a0aaa0655699ccffffffff00557577550eeeee0eeeeeeecffcccceeeeeeeeccccceeceeeeeeeeeeecffcccfe
-eed666deeeeeee77b777b7b756d0a0a00aa00a0da00000655699cccfffffff9957757755e0eee0eeeeeecccfcc7ccceeeeeeeecffcccceeeeeeeeecccfcc7ccc
-eed666deeeba77777777777756dd00a0d00d0a0da0dddd65569cccc9fffff99957566655ee0e0eeeeeecccccc770cceeeeeecccfcc7ccceeeeeeecccccc770cc
-eed666deebba77777777b77b56dddd0dddddd0dd00dddd65569cc9999999999995666655eee0eeee77eeecccc770ceeeeeecccccc770cceeeeeeeeecccc7707c
-eed666deebbb77777777bb775d77777777777777777777d55d7c777777777777775555d5eeeeeeeee77eeeccff77f0ee77eeecccc770ceeeeeeeeeffccff77f0
-eed666debbbba7777777bbbbe0000000000000000000000ee0000000000000000000000eeeeeeeee777ffccccfffeeeee77eeeccff77f0eeeee77fcccccfffee
-eed666debbbb7a7777777777bbbb3bbbbbbbbbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeefccfeeeeeeeee7ffccccfffeeeeee7707eccffce67e
-eed666de77b77777777777bbbbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee1e1cffeeeeeeee7e1efcffeeeeeeeeee77cccccf11607
-eed666deebba777777777ebb3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee11cf1eeeeeeeeeee11cff11eeeeeeeeccceee611ee06
-eed666deebbba7777777eeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeecc11ee8800eeeeeecccc1ee880ee77eeee227eeeee
-eed666dee7b7777777777bbe4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeecc1688800eeeeeeeeec6e8880e882eeee2887eeee
-eed666debbbb77777b3377bb4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeec7872000eeeeeeeeec77820008772eeee268eeee
-ee06660eebbe77b777bbbbeb4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee788200eeeeeeeeee788820e07882eeee288eeee
-eee000eeeebee77bebebeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee22000eeeeeeeeeee88200ee0888eeeee8eeeee
+eeed6deeeb773777777bbeee564a0a70a0d4aaaa707a0d655cccccc70777707c95777565e00000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eee777eebb77bb37777eeeee564a00a0a447aa44ad7a006556999cc7077ff07c995775650eeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3
+ee66666eeebbeebb777bb7bb560aaaa0aaaa0a00a0aaa0655699ccffffffff00557577550eeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3b
+eed666deeeeeee77b777b7b756d0a0a00aa00a0da00000655699cccfffffff9957757755e0eee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33bb
+eed666deeeba77777777777756dd00a0d00d0a0da0dddd65569cccc9fffff99957566655ee0e0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee337bba
+eed666deebba77777777b77b56dddd0dddddd0dd00dddd65569cc9999999999995666655eee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee337abba7
+eed666deebbb77777777bb775d77777777777777777777d55d7c777777777777775555d5eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33abbbb7b3
+eed666debbbba7777777bbbbe0000000000000000000000ee0000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee337bbbb3b33e
+eed666debbbb7a7777777777bbbb3bbbbbbbbbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee333babbbb333eee
+eed666de77b77777777777bbbbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee333b7bbbb3b33eeee
+eed666debbba777777777ebb3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33337bb3bb3333eeeee
+eed666de7bbba7777777eeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee333b33ab3bb3b33eeeeee
+eed666dea7b7777777777bbe4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33ab37bb33b3333eeeeeee
+eed666debbbb77777b3377bb4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33333333bbb3b33eeeeeeee
+ee06660eebbe77b777bbbbeb4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33ab33a3b33333eeeeeeeee
+eee000eeeebee77bebebeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33333333333eeeeeeeeeee
44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
@@ -49,78 +54,78 @@ eee000eeeebee77bebebeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
@@ -138,8 +143,8 @@ eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656d
eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0000000000000000000000000000000040000000000000000000000000000000400000434300000000000000000000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414141414141414141414141414141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
000000000000000000006e6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e49466e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f497e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f000000000000000000007c7d7c7d7c7d7c7d1e1f1e1f1e1f1e1f5e5f5e5f5f
000000000000000000006e6f6e6f6e6f6e6f6f585945434443454244455554307e7f7e7f7e7f7e7f7e7f7e7f7e2e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f6f6e6f6e6f6e6f6e6f6e6f44417000000000000000000000000000000000000000000000000000000000000000000000000000007c7d5e5f5e5f5e5f6e6f6e6f6f
diff --git a/data/builtin_data_stage_intro.p8 b/data/builtin_data_stage_intro.p8
index 1ba50ca5..708a7af2 100644
--- a/data/builtin_data_stage_intro.p8
+++ b/data/builtin_data_stage_intro.p8
@@ -1,11 +1,16 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic builtin data: stage_intro
-- by leyn
-- this section will be overwritten during build
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+-- Note that we integrate the collision masks as built-in spritesheet like builtin_data_ingame.p8,
+-- because we still need Sonic to land on the ground during the intro.
+import "spritesheet_stage_collision_masks.png"
+
-- the collision masks at the top will be overwritten by runtime sprites
-- via reload
__gfx__
@@ -17,129 +22,129 @@ __gfx__
00700700777777777777777777777777777777777777000000000000000000000000000000007777777777777777777777777777770000000000007777777777
00000000777777777777777777777777777777777777777777777777000000000000000077777777777777777777777777777777777700000000777777777777
00000000777777777777777777777777777777777777777777777777777700000000777777777777777777777777777777777777777777000077777777777777
-70000000700000000000000000000000000000070000000700000007777777777777777770000000000000000000000000007777777777770000000000000000
-77000000770000000000000000000000000000770000007700000007077777777777777070000000000000000000000000007777777777770000000000000000
-77700000777700000000000000000000000077770000077700000007077777777777777070000000000077777777777700007777777777770000000000000000
-77770000777770000000000000000000000777770000777700000077007777777777770077000000000077777777777700007777777777770000000000000000
-77777000777777700000000000000000077777770007777700000077000777777777700077000000000077777777777700007777777777770000000000000000
-77777700777777777000000000000007777777770077777700000077000777777777700077000000000077777777777700007777777777770000000000000000
-77777770777777777770000000000777777777770777777700000777000077777777000077700000000077777777777700007777777777770000000000000000
-77777777777777777777700000077777777777777777777700000777000077777777000077700000000077777777777700007777777777770000000000000000
-77777777777777777777700000077777777777777777777700007777000007777770000077770000000000000000007700000000000000000000000000000000
-77777770777777777770000000000777777777770777777700007777000007777770000077770000000000000000777700000000000000000000000000000000
-77777700777777777000000000000007777777770077777700077777000000777700000077777000000000000077777700000000000000000000000000000000
-77777000777777700000000000000000077777770007777700077777000000777700000077777000000000777777777700000000000000000000000000000000
-77770000777770000000000000000000000777770000777700777777000000777700000077777700000077777777777700007777000000000000000000000000
-77700000777700000000000000000000000077770000077707777777000000077000000077777770007777777777777700007777000000000000000000000000
-77000000770000000000000000000000000000770000007707777777000000077000000077777770777777777777777700007777000000000000000000000000
-70000000700000000000000000000000000000070000000777777777000000077000000077777777777777777777777700007777000000000000000000000000
-44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
-44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
-44949444bbbbbbbbabbbbbbababbeaaeebeaeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaaebbabbbbbbb3bbbbbbaeeeeeeeeeeeeeeeeeeaeabbbbb
-44499444b3babbb3bbbbabbababbb3ababbababaeaeeeeaeeeeeaeeeeeeeeeeeeeeeeeeeeaeeeeaeba3bbbabbb3bab3bb3bababbeaeeeeeeeeeeeeaeabbab3bb
-44949944b3bb3bb3bb3ba3bbb3bbb3b3b3bbbbbababaebaeeeaebeeaeeeeeeeeeeeeeeeeeabeababbb3bbbbbbb3bbbbbb3babbbbbaeaeeeeeeeeaeabbbbab3bb
-44949944bbbb3bbbbb3bb3b3bbb3b3bbb3bb3bb3bbbabbabababbabbbaeeaeeeeeeaeeabbabbabbbbb3bbbbbbbbbbb3bb3bbb3bbbbbaeeaeeaeeabbbb3bbbb3b
-44449444bb3bbbbb3bbbb3bbbbb3bbbbbbbb3bb3bb3abbbbbb3bbbbbbabaaeaeeaeaababbbbbabbbbbbb3b3bbbbbbbbbbbbbbbbb3ababbaeeabbaba3b3b3bb3b
-44949444bbbbbbbbbbbbbbbbbbbb3bbbb3bbbbbbbbbbbb3bbbbbbb3bbababbabbabbababb3bbbbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-44999444bb0bb3b3bb3bb0bbbbb303bbb3b3bb3bb3bb3b3bb3b3bbbbb3bbbbabbabbbbbbbbbb3b3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-449944443003b030b30b30033b300033bbbbbb3bb3bbbbbbbbb3bb3bb3bbbbbbbbbb3b3bb3bb3bbbeeee999aaa7777a9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-49494944004030003003040003004000bbbb0bbbbbb0bbbb3bbbb33bbbbb3b3bbbbbbb3bb33bbbb3eeee444999aa7aa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-44499444044400440000044440044440b03b03bbbb30b30b033b303bbbbbbb3bbb3bbbbbb303b330eeee44999aa777a9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
-44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
-444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+70000000700000000000000000000000000000070000000700000000777777777777777700000000000000000000000000000000777777770077777777777700
+77000000770000000000000000000000000000770000007700000007077777777777777070000000000000000000000000000007777777770077777777777700
+77700000777700000000000000000000000077770000077700000007077777777777777070000000000077770000000000000077777777770077777777777700
+77770000777770000000000000000000000777770000777700000077007777777777770077000000000077770000000000000077777777770077777777777700
+77777000777777700000000000000000077777770007777700000077000777777777700077000000000077770000000000000777777777770077777777777700
+77777700777777777000000000000007777777770077777700000077000777777777700077000000000077770000000000000777777777770077777777777700
+77777770777777777770000000000777777777770777777700000777000077777777000077700000000077770000000000007777777777770077777777777700
+77777777777777777777700000077777777777777777777700000777000077777777000077700000000077770000000000007777777777770077777777777700
+77777777777777777777700000077777777777777777777700007777000007777770000077770000000000000000007700077777000000000000000000000000
+77777770777777777770000000000777777777770777777700007777000007777770000077770000000000000000777700077777000000000000000000000000
+77777700777777777000000000000007777777770077777700077777000000777700000077777000000000000077777700777777000000000000000000000000
+77777000777777700000000000000000077777770007777700077777000000777700000077777000000000777777777700777777000000000000000000000000
+77770000777770000000000000000000000777770000777700777777000000777700000077777700000077777777777707777777000000000000000000000000
+77700000777700000000000000000000000077770000077707777777000000077000000077777770007777777777777707777777000000000000000000000000
+77000000770000000000000000000000000000770000007707777777000000077000000077777770777777777777777777777777000000000000000000000000
+70000000700000000000000000000000000000070000000777777777000000077000000077777777777777777777777777777777777777770000000000000000
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141410000414141434341414141414141410000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414545454343434343454541414141414145455149434343434545414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
000000000000000000006e6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e49466e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f497e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f000000000000000000007c7d7c7d7c7d7c7d1e1f1e1f1e1f1e1f5e5f5e5f5f
000000000000000000006e6f6e6f6e6f6e6f6f585945434443454244455554307e7f7e7f7e7f7e7f7e7f7e7f7e2e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f6f6e6f6e6f6e6f6e6f6e6f44417000000000000000000000000000000000000000000000000000000000000000000000000000007c7d5e5f5e5f5e5f6e6f6e6f6f
diff --git a/data/builtin_data_titlemenu.p8 b/data/builtin_data_titlemenu.p8
index 63e54488..20c51346 100644
--- a/data/builtin_data_titlemenu.p8
+++ b/data/builtin_data_titlemenu.p8
@@ -4,6 +4,9 @@ __lua__
-- picosonic builtin data: titlemenu
-- by leyn
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_titlemenu.png"
+
-- this section will be overwritten during build
__gfx__
00000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee5eeeeeee77778eeeeeeeeeeee8eeeeeeeeeeeeeeeeee8eeeeeeeeee
diff --git a/data/cartridges.txt b/data/cartridges.txt
new file mode 100644
index 00000000..439b7e12
--- /dev/null
+++ b/data/cartridges.txt
@@ -0,0 +1,5 @@
+titlemenu
+stage_intro
+ingame
+stage_clear
+attract_mode
diff --git a/data/data_bgm1.p8 b/data/data_bgm1.p8
deleted file mode 100644
index 92661e90..00000000
--- a/data/data_bgm1.p8
+++ /dev/null
@@ -1,90 +0,0 @@
-pico-8 cartridge // http://www.pico-8.com
-version 29
-__lua__
--- picosonic data: pico island
--- bgm by leyn
-
-__sfx__
-010700001f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c130211301d130211301d130211301d130211301d130
-010700000c1300c1300c1300c13518130181350c0000c00016130161301613016135151301513516130161350c0000c000151301513516130161350c0000c00016130161350c0000c00018130181301813018135
-01070000221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130221301f130211301d130211301d130211301d130211301d130
-010700000c1300c1300c1300c13518130181350c1000c10016130161301613016135151301513516130161350c1000c1000a1300a1350c1000c1000a1300a1350c1000c1000a1300a1350b1300b1300b1300b135
-010700002113022130211301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f1301c1301f130
-0107000024130241352413024135301303013524130241352e1302e13524130241352e1302e13530130301351d1301d1352213022135291302913528130281302813028130281302813028130281350000000000
-0107000030140301453014030145301403014530140301450c0000c0000c0000c0000c0000c0000c0000c0000c0000c0000c0000c0001d1301d1301d1301d1352213022130221302213529130291302913029135
-010700001813018135181301813518130181351813018135110001100011000110001100011000110001100011000110001100011000110001100011000110001100011000110001100011000110001100011000
-010700001c1301c1301c1301c13500000000001813018130181350000000000000001613016130161301613016130161301613016130161301613016130161301613016130161301613016130161301613500000
-010700000c1300c1300c1300c1300c1300c1300c1300c1350c1300c1300c1300c1350c1000c1050c1300c1300c1300c1300c1300c135071300713007130071350c1300c1300c1300c13507130071300713007135
-01070000000000000000000000002d1302d1352e1302e13500000000002e130000002e130000002e130000002e1302e13500000000002d1302d1302d1302d1302d1302d1302d1302d13500000000000000000000
-010700001c1301c1301c1301c13500000000001813018130181350000000000000001a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a1301a13500000
-010700000000000000000000000029130291352b1302b13500000000002b130000002b130000002b130000002b1302b135000000000011130111301113011135161301613016130161351d1301d1301d1301d135
-01070000000000000000000000002d1302d1352e1302e13500000000002e130000002e130000002e130000002e1302e135000000000030130301352e1302e1302e1302e1302e1302e13500000000000000000000
-01070000000000000000000000002d1302d1352e1302e13500000000002e130000002e130000002e130000002e1302e135000000000030130301352e1302e1302e1302e1302e1302e13528130281352913029135
-010700002b1302b1302b1302b13500000000002413024130241350000000000000002e1302e1302e1302e1302e1302e1352e130000002e1302e13500000000002e1302e1302e1302e1352b1302b1302b1302b135
-010700002d1302d1302d1302d13500000000002913029130291350000000000000002413024130241302413024130241302413024130241302413024130241302413024130241302413528130281352913029135
-010700003c1203c1203c1203c12539120391253a1203a1203a1203a125371203712037120371253c1203c1203c1203c12539120391253a1203a1203a1203a1253712037120371203712500100001000010000105
-010700002b1302b1302b1302b13500000000002413024130241350000000000000002e1302e1302e1302e1302e1302e1302e1302e1352e1302e1302e1302e1352e1302e1302e1302e13530130301303013030135
-010700002d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d1302d13500000000000000000000000000000000000000000000000000000000000028130281352913029135
-010700002d1302d1302d1302d1302d1302d1352e1302e1302e1302e1302e1302e1353013030130301303013030130301303013030130301303013030130301350000000000000000000000000000000000000000
-010700003c1203c1203c1203c12539120391253a1203a1203a1203a12537120371203712500000000000000000000000002113000000221300000024130000002713000000261300000022130000002413000000
-01070000000000000000000000001f1301f1301f1301f135241302413024130241352b1302b1302b1302b13529130291302913029135000000000028130281302813500000000000000024130241302413024135
-0107000032130000002f130000002b130000002813000000261300000023130000001f130000001c1300000030130000002f130000002b130000002813000000241300000023130000001f130000001c13000000
-010700002113021130211302113021130211302113021130211202112021120211202413024130241302413523130231302313023135000000000024130241302413500000000000000026130261302613026135
-0107000030130000002d1300000029130000002613000000241300000021130000001d130000001a130000002f130000002d1300000029130000002613000000231300000021130000001d130000001a13000000
-010700002813028130281302813028130281302813028130281302813028130281302812028120281202812021130211302113021130211302113021130211302113021130211302113021130211302113021135
-0107000010130101301013010130101301013010130101351013010130101301013010130101350c1300c1300c1300c1300c1300c1350c1300c1300c1300c135101301013010130101350c1300c1300c1300c135
-01070000291302913029130291350000000000281302813028135000000000000000261302613026130261352313023130231302313500000000002113021130211350000000000000001f1301f1301f1301f135
-010700000e1300e1300e1300e1300e1300e1300e1300e1350e1300e1300e1300e1300e1300e1350c1300c1300c1300c135131300c000131301313013130131351313013130131301313511130111301113011135
-010700002b1302b1302b1302b1302b1302b1302b1302b1302b1302b1302b1302b1302b1202b1202b1202b12028130281302813028130281302813028130281302813028130281302813021130211302113021135
-010700000e1300e1300e1300e1300e1300e1300e1300e1350e1300e1300e1300e1300e1300e1350c1300c1300c1300c135131300c000131301313013130131351313013130131301313512130121301213012135
-010700002813028130281302813500000000002813000000000000000000000000001d1301d1351f1301f13521130211302113021130211302113523130231302313023130231302313524130241302413024135
-0107000011130111301113011130111301113011130111351113011130111301113011130111350c1300c1300c1300c1300c1300c1350c1300c1300c1300c1351113011130111301113511130111301113011135
-010700000e1300e1300e1300e1300e1300e1300e1300e135000000000000000000001d130000001f130000002113021130211302113021130211351f1301f1301f1301f1301f135000001d1301d1301d1301d135
-01070000111301113515130151351a1301a1351d1301d1351a1301a1351d1301d135211302113526130261352113021135261302613529130291352d1302d13529130291352d1302d13532130321353513035135
-010700001513015130151301513015130151301513015130151301513015130151301513015130151301513511130111301113011130111301113011130111301113011130111301113011130111301113011135
-010700001813018130181301813018130181301813018135171301713017130171301713017130171301713518130181301813018130181301813018130181351a1301a1301a1301a1301a1301a1301a1301a135
-010700001513015130151301513015130151301513015135131301313013130131301313013130131301313515130151301513015130151301513015130151351713017130171301713017130171301713017135
-0107000030100301053010030105301003010530100301050c0000c0000c0000c0000c0000c0000c0000c0000c0000c0000c0000c0001d1301d1301d1301d1352213022130221302213529130291302913029135
-010700001862532605256053260527605266051f62526605186052660500605006051e3230060500605006051862500605006050060500605006051f625006052b6050060500605006051e323286052660500605
-010700001862518605256053260527605266051f62526605246052660500605006051e3230060526605006052b60500605006050060500605006051f6250060526605006051e625006051f323006052660500605
-01070000303230060530323256053032325605303230060524625006052a6252b6052a6252b6052b6252b60524625006052b625006052b6252b6252b62500605006052b6252b605006052b6252b6050060500605
-0107000018625326052560532605276052660526605266051862526605006050060526605006052660500605196350060519635006051960500605196351f6052660500605196350060519635006051f60500605
-0107000018625326052560532605276051f6051f63526605186052660500605006051e6351e6051e60500605196350060519635006051963500605196051f6051963500605196350060519635006051f60500605
-010700001f6351f6351f63500000000001f6051f6350000000000000001e635000001963500000186350000018635000000000000000186350000000000000001863500000000000000018635186351963500000
-__music__
-01 00012840
-00 02032940
-00 04012840
-00 05032940
-00 00012840
-00 02032940
-00 04012840
-00 06072a40
-00 08092840
-00 270a2940
-00 0b092840
-00 0c0d2940
-00 08092840
-00 270a2940
-00 0b092840
-00 0e0d2b40
-00 0f092840
-00 10112940
-00 12092840
-00 13112940
-00 0f092840
-00 10112940
-00 12092840
-00 14152c40
-00 16172840
-00 18192940
-00 1a1b2840
-00 1c1d2940
-00 16172840
-00 18192940
-00 1e1b2840
-00 1c1f2940
-00 20212840
-00 20222940
-00 23242840
-02 25262d40
diff --git a/data/data_stage1_00.p8 b/data/data_stage1_00.p8
index a7bb7deb..e799e8e0 100644
--- a/data/data_stage1_00.p8
+++ b/data/data_stage1_00.p8
@@ -1,9 +1,12 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic data: pico island
-- region (0, 0) by leyn
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_edition.png"
+
-- we only need __map__, but we kept __gfx__ only to visualize tiles when editing
-- (and __gff__ because it's not too big)
__gfx__
@@ -28,9 +31,9 @@ eeeeeeeeeeeeeeeeeeeeeeeebbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
@@ -47,127 +50,127 @@ eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeeeeeeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141410000414141434341414141414141410000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414141414141414141414141414141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6e6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6e6e6e6e
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f6f6f6e6e6e6e6e6e6e6e6e6e6e6e6e6f6e6e6e6e6e6e6e
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6e6e6f000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f4d4efa4c4e00383636374c4e4c4e4c000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f48453132323b494645474844424155520000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f58eb575857585354eb5758ebeb7c7d000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f7e7f7e7f7e7f7e7f7e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f535453545354dadb5354eb7c7d0000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f0000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f5e5f5e5f7d00000000000000000000004c4d
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f0000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f0000000000000000000000512324
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e55596f0000000000000000007f57466e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f49466e6f6e6f6e7f000000000000000000000000fbfc
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6eebeb6f000000000000000000007c7d57586e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7e7f7e7f59557deb59466e6f7e000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e53546f0000000000000000000000007c7d6e6f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f000000007c7d00007c7d7e7f00000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e5e5f6f00000000000000000000000000007e7f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f4c4c00fa0000383636374d4c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f49453132323b49464547484552000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c4e4d4e
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f5354575857585354eb5758eb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005145444945
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6fdadb53545354dadb53545354000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000053545354
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f5e5f5e5f5e5f5e5f5e5f5e5f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dadbdadb
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f004d4c4d4c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dadbdadb
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e7f5123242324520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dadbdadb
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a86e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6e6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6e6e6e6e
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f6f6f6e6e6e6e6e6e6e6e6e6e6e6e6e6f6e6e6e6e6e6e6e
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6e6e6f000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6fa0a1fa4ca0a1a0a1a14e4c4e4c4e4c000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f424647484945494645474844424155520000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5354575853545354eb5758ebeb7c7d000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f7e7f7e7f7e7f7e7f7e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6fdadb5354dadbdadb5354eb7c7d0000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f0000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f5e5f5e5f7d00000000000000000000002525
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f0000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f0000000000000000000000512324
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e55596f0000000000000000007f57466e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f49466e6f6e6f6e7f000000000000000000000000fbfc
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6eebeb6f000000000000000000007c7d57586e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7e7f7e7f59557deb59466e6f7e000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e53546f0000000000000000000000007c7d6e6f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f000000007c7d00007c7d7e7f00000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e5e5f6f00000000000000000000000000007e7f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6fa0a14efaa0a14e4ca0a14d4c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f49464748494647484546474852000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c4e4d4e
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f535457585354575853545758000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005145444946
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6fdadb5354dadb535453545354000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000053545354
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f5e5f5e5f5e5f5e5f5e5f5e5f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dadbdadb
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f0025252525000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dadbdadb
+a7a8a7a8a7a80000a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a80000a7a8a7a8a7a8000000000000000000000000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e7f5123242324520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dadbdadb
diff --git a/data/data_stage1_01.p8 b/data/data_stage1_01.p8
index f2625e14..b73b76dd 100644
--- a/data/data_stage1_01.p8
+++ b/data/data_stage1_01.p8
@@ -1,9 +1,12 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic data: pico island
-- region (0, 1) by leyn
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_edition.png"
+
-- we only need __map__, but we kept __gfx__ only to visualize tiles when editing
-- (and __gff__ because it's not too big)
__gfx__
@@ -28,9 +31,9 @@ eeeeeeeeeeeeeeeeeeeeeeeebbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
@@ -47,116 +50,116 @@ eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeeeeeeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141410000414141434341414141414141410000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414141414141414141414141414141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e7f0000fbfcfbfc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007cdbdadb
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007c7d7c
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f6e6f6e6f6e6f6e6f6e6f495758466e7f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a4b0000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f7e7f7e7f7e7f7e7f7e7f7c7d7c7d7e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec0000000000000000000000
-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaabac000000004d4c4d000000
-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a4b00000000000000000000000000000000000000000000000000000000000000bb0000000051232424520000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb0000000000fbfcfc000000
-000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec00000000000000000000000000000000000000000000000000000000000000bb0000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaabac000000000000000000000000000000000000000000000000000000000000bb0000000000000000000000
-000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000bb0000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000bb0000000000000038393a3b
-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c4d4e4d4c4d0000000000000000000000000000000000aaabac0000000000000000000000000000bb000000000000000000000000000000000000004e4c4d4e4c4d4c4e4d4e4c4dcb4e4c4d38393a3b48484955
-0000000000000000000000000000000000000000000000000000000000000000000000000000000038393a3b4849434243463c3d00000000000000000000000000000000bb000000000000000000000000000000bb00000000000000000000000000000000000051424342414346464748494541434344464848495557585354
-00000000000000000000000000000000000000000000000000000000000000000000000038393a3b4849434258535453543059463c3d0000000000000000000000000000bb000000000000000000000000000000bb0000000000000000000000000000004e4e4eca535453545354eb5758eb5354535453545758535453545e5f
-4d3836363636374e4d3836363636374e4d3836363636374e4d3836363636374e38393a3b484943425853545354dadbdadb40535459463c3d000000000000000000000000bb0000000000000000004e383636374ccb4e4e4a4b4e383636374c4e4d4c4d51434241435e5f5e5f5e5f535453545e5f5e5f5e5f53545e5f5e5f6e6f
-4847494546464847484749454646484748474945464648474847494546464847484943425853545354dadbdadbdadbdadb40dadb535459463c3d00000000000000000000bb00000000000000006659494546464646454647484649454646474849444545535453546e6f6e6f6e6f5e5f5e5f6e6f6e6f6e6f5e5f6e6f6e6f6e6f
-58585354535457585858535453545758585853545354575858585354535457565853545354dadbdadbdadbdadbdadbdadb40dadbdadb535459463c3d0000000000000000bb0000000000000000765354535453545354eb5758eb535453545758535453545e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f
+a7a8a7a8a7a8c5c6a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8c5c6a7a8a7a8a7a8000000000000000000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e7f0000fbfcfbfc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007cdbdadb
+a7a8a7a8a7a8c5c6a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8c5c6a7a8a7a8a7a8000000000000000000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007c7d7c
+a7a8a7a8a7a8c5c6a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8c5c6a7a8a7a8a7a8000000000000000000000000000000006f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+a7a8a7a8a7a8c5c6a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8c5c6a7a8a7a8a7a8000000000000000000000000000000006f6e6f6e6f6e6f6e6f6e6f495758466e7f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a4b0000000000000000000000
+a7a8a7a8a7a8c5c6a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8c5c6a7a8a7a8a7a8000000000000000000000000000000007f7e7f7e7f7e7f7e7f7e7f7c7d7c7d7e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec0000000000000000000000
+a7a8a7a8a7a8c5c6a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8c5c6a7a8a7a8a7a80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaabac00000000252525000000
+a7a8a7a8a7a8c5c6a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8c5c6a7a8a7a8a7a80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a4b00000000000000000000000000000000000000000000000000000000000000bb0000000051232424520000
+a7a8a7a9a7a8c5c6a7a8a7a8a7a8a7a8a7a8a7a8a7a8a7a8c5c6a7a8a7a8a7a8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb0000000000fbfcfc000000
+a7a8a7b9a7a8c5c6a7a8a7a8a7a9a7a8a7a8a7a8a7a8a7a8c5c6a7a8a7a9a7a800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec00000000000000000000000000000000000000000000000000000000000000bb0000000000000000000000
+a7a8a7c9a7a8c5c6a7a8a7a8a7b9a7a8a7a8a7a8a7a8a7a9c5c6a7a8a7b9a7a8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaabac000000000000000000000000000000000000000000000000000000000000bb0000000000000000000000
+a7a8a7d9a7a8c5c6a7a8a7a8a7c9a7a8a7a8a7a8a7a8a7b9c5c6a7a8a7c9a7a800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000bb0000000000000000000000
+a7a8a7d9a7a8c5c6a7a8a7a8a7d9a7a8a7a8a7a8a7a8a7c9c5c6a7a8a7d9a7a8000000000000000000000000000000000000000000000000000000000000000000000000ec000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000bb0000000000000038393a3b
+a7a8a7d9a7a8c5c6a7a8a7a8a7d9b7b8a7a8a7a8a7a8a7d9c5c6a7a8a7a8b7b80000000000000000000000004c4d4e4d4c4d0000000000000000000000000000000000aaabac0000000000000000000000000000bb000000000000000000000000000000000000004e4c4d4e4c4d4c4e4d4e4c4dcb4e4c4d38393a3b48484955
+a7a8a7a8b7b8c5c6b7b8a7a8b7b8c7c8b7b8a7a8b7b8a7a8c5c6a7a8b7b8c7c8000000000000000038393a3b4849434243463c3d00000000000000000000000000000000bb000000000000000000000000000000bb00000000000000000000000000000000000051424342414346464748494541434344464848495557585354
+b7b8b7b8c7c8c5c6b7b8b7b8c7c8c7c8b7b8b7b8c7c8b7b8c5c6b7b8c7c8c7c80000000038393a3b4849434258535453543059463c3d0000000000000000000000000000bb000000000000000000000000000000bb0000000000000000000000000000004e4e4eca535453545354eb5758eb5354535453545758535453545e5f
+d7d8d5d6d5d6c5c6d7d8d5d6d5d6d7d8d7d8d5d6d5d6d7d8c5c6d5d6d5d6d7d838393a3b484943425853545354dadbdadb40535459463c3d000000000000000000000000bb0000000000000000004e4ca0a14d4ccb4e4e4a4b4e4ca0a14d4c4e4d4c4d51434241435e5f5e5f5e5f535453545e5f5e5f5e5f53545e5f5e5f6e6f
+4847494546464847484749454646484748474945464648474847494546464847484943425853545354dadbdadbdadbdadb40dadb535459463c3d00000000000000000000bb0000000000000000a659494546464646454647484649454646474849444545535453546e6f6e6f6e6f5e5f5e5f6e6f6e6f6e6f5e5f6e6f6e6f6e6f
+58585354535457585858535453545758585853545354575858585354535457565853545354dadbdadbdadbdadbdadbdadb40dadbdadb535459463c3d0000000000000000bb0000000000000000b65354535453545354eb5758eb535453545758535453545e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f
5354dadbdadb53545354dadbdadb53545354dadbdadb53545354dadbdadb535430dadbdadbdadbdadbdadbdadbdadbdadb40dadbdadbdadb535459463c3d000000000000bb0000000000000065eb53545e5f5e5f5e5f535453545e5f5e5f53545e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f
5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f505e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f505e5f5e5f5e5f5e5f535459463c3d4e4c4d4ccb4d4e4d4c4e6364535453546e6f6e6f6e6f5e5f5e5f6e6f6e6f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f
6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f5e5f535459464544454549494545464158ebdadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f
diff --git a/data/data_stage1_10.p8 b/data/data_stage1_10.p8
index 808b35f7..3d5e11eb 100644
--- a/data/data_stage1_10.p8
+++ b/data/data_stage1_10.p8
@@ -1,9 +1,12 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic data: pico island
-- region (1, 0) by leyn
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_edition.png"
+
-- we only need __map__, but we kept __gfx__ only to visualize tiles when editing
-- (and __gff__ because it's not too big)
__gfx__
@@ -28,9 +31,9 @@ eeeeeeeeeeeeeeeeeeeeeeeebbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
@@ -47,97 +50,97 @@ eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeeeeeeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141410000414141434341414141414141410000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414141414141414141414141414141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e
6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e
@@ -155,19 +158,19 @@ __map__
000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaabac00000000000000007e7f7e7f7e0000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e
000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e
4246474847484748474847484748476f494344435556425852000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e6f6e6f6e6f6e
-ebeb59466e6f6e6f6e6f6e6f6f6e6f587c7d7c7d7c7d7c7d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000575857466e6f6e
-7c7debeb59466e6e6f6e6f6e6f6e6f7d000000000000000000000000000000000000000000000000000000000000000000000000000000000000a85c5d000000000000bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007c7d7ceb575857
-00007c7debeb59466f6e6f6e6f6e7e000000000000000000000000000000000000000000000000000000000000000000000000004c4d4d4c4e4db86c6d4e4e4d4c4e4ecb4c4d4e00000000000000a85c5d000000a85c5d000000000000000000000000000000000000000000000000000000000000000000000000007c7d7d00
-000000007c7d7c7d7e7f7e7f7e7f000000000000000000000000000000000000000000000000000000000000000000000000005141444441434346444644434649444143464546313335374d4d4cb86c6d4e4e4eb86c6d4d4c4e4e4e4c0000000000000000000000000000000000000000000000000000000000000000000000
-000000000000000000000000000000000000000000000000000000a85c5d0000000000000000000000000000000000000000000053545354535453545354535453545354535430594948484444494346464748494345414445424346463133353700000000000000000000000000000000000000000000000000000000000000
-000000000000000000000000000000000000000000000000000000b86c6d0000000000004c4e4c4d4e4d00000000000000000000dadbdadbdadbdadbdadbdadbdadbdadbdadb40535459585354535453545758535453545354535453545741424152000000000000000000000000000000000000000000000000000000000000
-00000000000000000000000000000000000000004e383636374d4ea9bebf4c4e4c393a3b4945424346453c3d0000000000000000dadbdadbdadbdadb5e5f5e5f5e5f5e5f5e5f40dadb5354dadbdadbdadb5354dadbdadbdadbdadb7c7d7c7d7c7d00000000000000000000000000000000000000000000000000000000000000
+ebeb59466e6f6e6f6e6f6e6f6f6e6f587c7d7c7d7c7d7c7d0000000000000000000000000000000000000000000000000000000000000000000000b0b1000000000000bb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000575857466e6f6e
+7c7debeb59466e6e6f6e6f6e6f6e6f7d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0c1000000000000bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007c7d7ceb575857
+00007c7debeb59466f6e6f6e6f6e7e000000000000000000000000000000000000000000000000000000000000000000000000004c4d4d4c4e4d4ed0d14e4e4d4c4e4ecb4c4d4e0000000000000000b0b100000000b0b1000000000000000000000000000000000000000000000000000000000000000000000000007c7d7c7d
+000000007c7d7c7d7e7f7e7f7e7f000000000000000000000000000000000000000000000000000000000000000000000000005141444441434346444644434649444143464546313335374d4d4c4dc0c14e4c4e4ec0c14d4c4e4e4e4c0000000000000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000b0b1000000000000000000000000000000000000000000000053545354535453545354535453545354535430594948484444494346464748494345414445424346463133353700000000000000000000000000000000000000000000000000000000000000
+000000000000000000000000000000000000000000000000000000c0c1000000000000004c4e4c4d4e4d00000000000000000000dadbdadbdadbdadbdadbdadbdadbdadbdadb40535459585354535453545758535453545354535453545741424152000000000000000000000000000000000000000000000000000000000000
+00000000000000000000000000000000000000004e4ca0a14d4d4ed0d14e4c4e4c393a3b4945424346453c3d0000000000000000dadbdadbdadbdadb5e5f5e5f5e5f5e5f5e5f40dadb5354dadbdadbdadb5354dadbdadbdadbdadb7c7d7c7d7c7d00000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000514647494648484642434445444749455553545354535457463c3d000000000000dadbdadbdadb5e5f6e6f6e6f6e6f6e6f6e6f505e5f5e5f5e5f5e5fdadbdadbdadbdadbdadbdadb00000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000004e4d3839eb565354595553545354535455eb5354dadbdadbdadbdadb59463c3d4d4c4e4ddadbdadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6fdadbdadbdadbdadbdadbdadb00000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000004c393a3b454547485354dadb5354dadbdadbdadb5354dadbdadbdadbdadbdadbdadb574646424643dadb5e5f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6fdadbdadbdadbdadbdadbdadb00000000000000000000000000000000000000000000000000000000000000000000000000
-4d4e000000004c4e4c393a3b4749455553545758dadb5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5fdadbdadbdadb5e5f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6fdadbdadb7c7ddadbdadb7c7d00000000000000000000000000000000000000000000000000000000000000000000000000
-44463132323b49464748455555eb5354dadb53545e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f7c7d7c7d0000dadb7c7d000000000000000000000000000000000000000000000000000000000000000000000000000000
-5354575857585354595553545354dadbdadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f0000000000007c7d0000000000000000000000000000000000000000000000000000000000000000000000000000000000
-dadb53545354dadb5354dadbdadbdadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f7e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+4d4ea0a1a0a14c4e4c393a3b4749455553545758dadb5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5fdadbdadbdadb5e5f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6fdadbdadb7c7ddadbdadb7c7d00000000000000000000000000000000000000000000000000000000000000000000000000
+47484944454649464748455555eb5354dadb53545e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f7c7d7c7d0000dadb7c7d000000000000000000000000000000000000000000000000000000000000000000000000000000
+5758535453545354595553545354dadbdadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f0000000000007c7d0000000000000000000000000000000000000000000000000000000000000000000000000000000000
+5354dadbdadbdadb5354dadbdadbdadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f7e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6e6f6e6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e7e7f7e7f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
diff --git a/data/data_stage1_11.p8 b/data/data_stage1_11.p8
index 59124d9a..73dceab1 100644
--- a/data/data_stage1_11.p8
+++ b/data/data_stage1_11.p8
@@ -1,9 +1,12 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic data: pico island
-- region (1, 1) by leyn
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_edition.png"
+
-- we only need __map__, but we kept __gfx__ only to visualize tiles when editing
-- (and __gff__ because it's not too big)
__gfx__
@@ -28,9 +31,9 @@ eeeeeeeeeeeeeeeeeeeeeeeebbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
@@ -47,127 +50,127 @@ eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeeeeeeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141410000414141434341414141414141410000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414141414141414141414141414141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7f7e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
6e6f49466e6f6e6f6e6f49466e6f6e6f6e6f6e6f6e6f6e6f6e6f7e7f7e7f7e7f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-49557c7d59466e6f6e6f7c7d59466e6f6e6f6e7f7e7f7e7f7e7f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a85c5d00000000000000000000000000000000000000000000000000000000000000
-000000007c7d7e7f7e7f00007c7d7e7f7e7f7e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b86c6d0000004d4e4c4e00000000000000000000000000000000000000000000a85c
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a85c5d0000a9bebf0000514344424631333537000000000000000000a85c5d000000000000b86c
-000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a85c5d0000b86c6d0000b9cecf0000005354535457444446313335374e4c4e4e4db86c6d4c4d4e4d4e4ca9be
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e4d4db86c6d4c4ca9bebf4c4cba7a7b4d4e4d5e5f5e5f535453545744444747494643454346464245454246494549
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000004c4d4e4d4e0000000000000000000000000000000000000000004c4d4e4d4e0000000000000051424346494346464346474847484748474748486e6f6e6f5e5f5e5f5354eb5958eb5354535453545354535453545354
-000000000000a85c5d000000000000a85c5d00000000000000000000000000004c4d4e00000000000051232423242352000000000000004c4d4e4d4e0000000000000051232423242352000000000000007c7debeb7c7d7c7d5357466e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f535453545e5f5e5f5e5f5e5f5e5f5e5f5e5f
-4c4e4e4d4e4cb86c6d4e4c4d4e4e4cb86c6d4c4c4c4e0000000000000000005123242352000000000000fbfcfbfcfc000000000000005123242324235200000000000000fbfcfbfcfc000000000000000000007c7d000000007c7d7d59466e6f6e6f6e6f6e6f6e6f6e6f6e7f6e6f5e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f
-4748484748484748474848474847484748494243454631333537000000000000fbfcfc0000000000000000000000000000000000000000fbfcfbfcfc00000000000000000000000000000000000000000000000000000000000000007c7d595556574246464246466e6f6e007e7f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e
-434344555643464455564346494241465853545354305743464252000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006740404040404040404059557e0000007e7f7e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e
+49557c7d59466e6f6e6f7c7d59466e6f6e6f6e7f7e7f7e7f7e7f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+000000007c7d7e7f7e7f00007c7d7e7f7e7f7e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2a30000004d4e4c4e0000000000000000000000000000000000000000000000b0
+00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b0b10000b2b3000051434442463133353700000000000000000000b0b100000000000000c0
+000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b0b10000c0c10000c2c30000005354535457444446313335374ea0a14e4d4ec0c14c4da0a14e4c4cd0
+0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e4d4d4e4c4dc0c14e4dd0d14c4dd2d34d4e4d5e5f5e5f535453545744444747494643454346464245454246494549
+000000000000000000000000000000000000000000000000000000000000000000000000000000000000252525252500000000000000000000000000000000000000000025252525250000000000000051424346494346464346474847484748474748486e6f6e6f5e5f5e5f5354eb5958eb5354535453545354535453545354
+00000000000000b0b100000000000000b0b10000000000000000000000000000252525000000000000512324232423520000000000000025252525250000000000000051232423242352000000000000007c7debeb7c7d7c7d5357466e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f535453545e5f5e5f5e5f5e5f5e5f5e5f5e5f
+4ca14e4ea04c4ec0c14e4c4d4e4e4c4cc0c14ca0a14e0000000000000000005123242352000000000000fbfcfbfcfc000000000000005123242324235200000000000000fbfcfbfcfc000000000000000000007c7d000000007c7d7d59466e6f6e6f6e6f6e6f6e6f6e6f6e7f6e6f5e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f
+4744474846484748474848474847484748494243454631333537000000000000fbfcfc0000000000000000000000000000000000000000fbfcfbfcfc00000000000000000000000000000000000000000000000000000000000000007c7d595556574246464246466e6f6e007e7f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e
+55eb5758eb59464455564346494241465853545354305743464252000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006740404040404040404059557e0000007e7f7e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e
5354535453545353545354535453545354dadbdadb407c7d7c7d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000774040407172000073744040690000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e
5e5f5e5f5e5f5e5e5f5e5f5e5f5e5f5e5f5e5f5e5f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004040700000000000007540790000000000006e6f6e6f6e6f6e6f6e6f6e6f6e6f6e
6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004068000000000000000067400000000000007f7e7f7e6e6f6e6f6e6f6e6f6e6f6e
6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7e7f7e7f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000407800000000000000007740000000000000000000007e7f7e7f6e6f6e6f6e7e7f
6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e7f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000004000000000000000000000000000006e6f6e6f6e0000
-6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e000000000000000000000000000000000000004a4b000000004c4d4e00000000000000000000000000004d4c4d4c4e4c4c4e4c4d4d4c4d4d4d4c4e4d4c4e4c4c000000000000000000000000000040000000000000000000004000000000000000000000000000007e7f7e7f7e0000
-6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7e000000000000000000000000000000000000000000000000512324235200000000000000000000000051434455434445494546444549424146444444464646463133353700000000000000000000406900000000000000006668000000000000000000000000000000000000000000
-6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e7f000000000000000000000000000000000000000000ec00000000fbfcfc0000000000000000000000000000535453545354535453545354535453545354535453545744444731333537000000000000757900000000000000007678000000000000000000000000000000000000000000
-6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e000000000000000000000000000000000000000000aaabac00000000000000000000000000000000000000005e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5354eb595744444731333537000000673c3d0000000000656800000000000000000000000000000000000000000000
-6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e00000000000000000000000000000000000000000000bb000000000000000000000000004d4e4e4d4c4e4dca6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f53545354eb59574444473c3d4d7759463c3d4d6364407800000000000000000000000000000000000000000000
+6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e000000000000000000000000000000000000004a4b0000000025252500000000000000000000000000004d4ca0a14e4ea0a14e4da0a14e4ea0a14e4da0a14e4c000000000000000000000000000040000000000000000000004000000000000000000000000000007e7f7e7f7e0000
+6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f7e000000000000000000000000000000000000000000000000512324235200000000000000000000000051434445464748454647484546474845464748454647483133353700000000000000000000406900000000000000006668000000000000000000000000000000000000000000
+6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e7f000000000000000000000000000000000000000000ec00000000fbfcfc0000000000000000000000000000535453545758535457585354575853545758535457585744444731333537000000000000757900000000000000007678000000000000000000000000000000000000000000
+6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e000000000000000000000000000000000000000000aaabac0000000000000000000000000000000000000000dadbdadb5354dadb5354dadb5354dadb5354dadb53545354eb595744444731333537000000673c3d0000000000656800000000000000000000000000000000000000000000
+6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e00000000000000000000000000000000000000000000bb000000000000000000000000004d4e4e4d4c4e4dca5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f53545354eb59574444473ca44d7759463c3d4d6364407800000000000000000000000000000000000000000000
7e7feaeaeaeaeaeaeaeaeaeaeaeaea00000000000000000000000000000000000000000000bb0000000000000000000000005143444547484855566e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f53545354eb595744474955594241465853543c3d000000000000000000000000000000000000000000
0000eaeaeaeaeaeaeaeaeaeaeaeaea00000000000000000000000000000000000000000000bb0000000000000000004e4e4eca53545357585853546e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f5354535458eb53545354dadbdadb59483c3d00000000000000000000000000000000000000
0000eaeaeaeaeaeaeaeaeaeaeaeaea00000000000000000000000000000000000000000000bb000000000000003e3f47494149dadbdadb5354dadb6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5fdadb5354dadbdadbdadbdadbeb5744593c3d0000000000000000000000000000000000
-4c4eeaeaeaeaeaeaeaeaeaeaeaeaea4c4e4c4d4e4d4c4d0000000000000000000000000000bb00000000003e3f4749585354305e5f5e5f5e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f5e5f5e5f5e5f5354535459483c3d00000000000000000000e0e1000000
+a0a1eaeaeaeaeaeaeaeaeaeaeaeaea4c4e4ca0a14d4c4d0000000000000000000000000000bb00000000003e3f4749585354305e5f5e5f5e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f5e5f5e5f5e5f5354535459483c3d00000000000000000000e0e1000000
46454541444946454344494545494546454344454341423133353700000000000000000000bb0000003e3f48455553545e5f506e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5fdadbeb5744463c3d00000000000000e3e4e5000000
53545354535453545354535453545354535453545354eb57434642313335374c4e4e4d4e4dcb4c3e3f47495853545e5f6e6f6e6f6e6e6f6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5354535457463c3d4d4ef0f1f2f3f4f5000000
5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f53545354eb57434646474848474848474848455553545e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5edbdadb535457424358535453545200000000
-6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f535453543059434455564346555853545e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e5f5e5fdadb5354dadbdadbdadb4c4e4d4d4c
+6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f535453543059434455564346555853545e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e5f5e5fdadb5354dadbdadbdadb4c4e4da0a1
6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f505354535453545354305e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f5e5f5e5f5944415946
diff --git a/data/data_stage1_20.p8 b/data/data_stage1_20.p8
index a5539ca5..3e46daf1 100644
--- a/data/data_stage1_20.p8
+++ b/data/data_stage1_20.p8
@@ -1,9 +1,12 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic data: pico island
-- region (2, 0) by leyn
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_edition.png"
+
-- we only need __map__, but we kept __gfx__ only to visualize tiles when editing
-- (and __gff__ because it's not too big)
__gfx__
@@ -28,9 +31,9 @@ eeeeeeeeeeeeeeeeeeeeeeeebbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
@@ -47,97 +50,97 @@ eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee99000000eeeeeeeed6d949eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee99aa9000499eeeeeeed6d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeeeeeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeeeeeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040eeeeeeeeeeeeeeeeeeeeeeeeeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeeeeeeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004eeeeeeeeeeeeeeeeeeeeeeeeeeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440eeeeeeeeeeeeeeeeeeeeeeeeeeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141410000414141434341414141414141410000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414141414141414141414141414141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
6e6f000000000000000000000000000000000000000000006e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f
6e6f000000000000000000000000000000000000000000006e6f6e6f6e6f6e6e6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f
@@ -161,7 +164,7 @@ __map__
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000514244474847484748474849435200000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c4c4e4c4d4c4e4c4d4c4d0000000000000000000000000000000000006730594342424646425530300000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005144454648474847484748475200000000000000000000000000000000007740403071720000737440406900000000000000000000000000000000000000000000000000000000000000000000
-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000535453596e6f6e6f6e6f6e00000000000000000000000000000000000000403070000000000000754079000000004c4e4d4e0000000000000000000000000000000000000000000000000000
+00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000535453596e6f6e6f6e6f6e0000000000000000000000000000000000000040307000000000000075407900000000252525250000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dadbdadb466f6e6f6e6f6e0000000000000000000000000000000000000030680000000000000000674000000051232423245200000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007cebdadbda596e6f7e7e7f0000000000000000000000000000000000000030780000000000000000774000000000fbfcfbfc000000000000000000eaeaea0000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007c7d7d7d7d6e7f0000000000000000000000000000000000000000000040000000000000000000004000000000000000000000000000eaeaeaeaeafaeaeaeaea0000000000000000000000
@@ -169,5 +172,5 @@ __map__
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040690000000000000000666800000000000000000000514244474847484748474849435200000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec00000000000000000000000000000000000000000000004a4b4c4e4e4d4e4e00000000000075790000000000000000767800000000000000000000006730594342424646425530300000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaabac00000000000000000000000000000000000000000051414243464246434631333537000000673c3d000000000065680000000000000000000000007740403071720000737440406900000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000005354535453545354574444473c3d4d7743443c3d4d636440780000000000000000000000000040307000000000000075407900000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000005354535453545354574444473ca44c7743443c3d4d636440780000000000000000000000000040307000000000000075407900000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb0000000000000000000000000000000000000000000000dadbdadbdadb53db5354eb574649444243434458565853543c3d00000000000000000000000030680000000000000000674000000000000000000000
diff --git a/data/data_stage1_21.p8 b/data/data_stage1_21.p8
index 3f7a31a5..51993c91 100644
--- a/data/data_stage1_21.p8
+++ b/data/data_stage1_21.p8
@@ -1,9 +1,12 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic data: pico island
-- region (2, 1) by leyn
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_edition.png"
+
-- we only need __map__, but we kept __gfx__ only to visualize tiles when editing
-- (and __gff__ because it's not too big)
__gfx__
@@ -28,9 +31,9 @@ eeeeeeeeeeeeeeeeeeeeeeeebbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
@@ -47,107 +50,107 @@ eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeeeeeeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141410000414141434341414141414141410000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414141414141414141414141414141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000004e4d393a3335374c4d4e4c4e5e5f5e5f5e5f5e5f5e5f535453545354535453545354535457463c3d0000000000000000000040780000000000000000774000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb0000000000000000003e3f4544444445454244444246446e6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f5e5f5e5f5e5f5e5fdadb59463c3d000000000000000040000000000000000000004000000000000000000000
-00000000000000000000000000004c4d4e000000000000000000000000000000000000000000000000000000bb000000000000003e3f45585354535453545354535453546e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f535457463c3d00000000000040690000000000000000666800000000000000000000
+0000000000000000000000000000252525000000000000000000000000000000000000000000000000000000bb000000000000003e3f45585354535453545354535453546e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f535457463c3d00000000000040690000000000000000666800000000000000000000
0000000000000000000000000051232423520000000000000000000000000000000000000000000000000000bb00000000003e3f49585354dadb5e5f5e5f5e5f5e5f5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6e5e5f535457463c3d0000000075790000000000000000767800000000000000000000
-5d00000000000000000000000000fbfcfc0000000000000000004d4c4d4d4c4d4e4d4e4a4b4c4e4d4c4e4c4dcb4e4d4e3e3f45585354dadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6f6e6f5e5f535457443c3d000000673c3d000000000065680000000000000000000000
-6d00000000000000000000000000000000000000000038393a3b4945434542444943424246494945424145434649494649585354dadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6f6e6f6e6f5e5f535457463c3d4e7759463c3d4d636440780000000000000000000000
-bf4e4e4c4e4c0000000000000000000000000000005141424358ebeb53545354535453545354535453545354535453545354dadb5e5f6e6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6f6e6f6e6f6e6f5e5f5354574446494543425559585354000000000000000000000000
+b100000000000000000000000000fbfcfc0000000000000000004da0a14d4ca0a14d4e4a4b4c4ea0a14e4c4dcb4e4d4e3e3f45585354dadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6f6e6f5e5f535457443c3d000000673c3d000000000065680000000000000000000000
+c100000000000000000000000000000000000000000038393a3b4945434542444943424246494945424145434649494649585354dadb5e5f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6f6e6f6e6f5e5f535457463ca44e7759463c3d4d636440780000000000000000000000
+d14e4ea0a14c0000000000000000000000000000005141424358ebeb53545354535453545354535453545354535453545354dadb5e5f6e6e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6f6e6f6e6f6e6f5e5f5354574446494543425559585354000000000000000000000000
454243424346313335370000000000000000000000007c7d7c7d7c7ddadb5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f6e6f6e6f6e6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6f6e6f6e6f6e6f6e6f5e5f5354535453545354dadbdadbeaea3c3d0000000000000000
-535453545354574243425200000000000000000000000000000000007ceb6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6f6e6f6e6f6e6f6e6f6e6f5e5fdadbdadbdadbdadbdadbeaea44483c3d4c4d4c4e4e4d
+535453545354574243425200000000000000000000000000000000007ceb6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6e6f6e6f6e6f6e6f6e6f6e6f5e5fdadbdadbdadbdadbdadbeaea44483c3d4ca0a14e4e4d
5e5fdadbdadb53545354000000000000000000000000000000000000007c6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5fdadbdadbdadbdadbeaeaeb575944444245464242
6e6f5e5f7c7d7c7d7c7d00000000000000000000000000000000000000007e7f7e6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6e6f6e6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f5e5feaea53545354535453545354
6e6f6e6f00000000000000000000000000000000000000000000000000000000007e7f7e7f6e6f6e6f6e6f6e6f6e49466f6e6f6e6f6e49467e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f7e7f6e6f7e7f6e6f6e6f6f6e49466e6f6e6f6f6e49466e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6feaea5e5f5e5f5e5f5e5f5e5f
@@ -156,10 +159,10 @@ bf4e4e4c4e4c0000000000000000000000000000005141424358ebeb535453545354535453545354
6e6f6e550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea6e6f6e6f6e6f6e6f6e6f
6e55587d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea6e6f6e6f6e6f6e6f6e6f
6e7c7d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea00006f6e6e6f6e6f6e6f6e6f
-7f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec0000000000000000000000000000000000000000a85c5d0000000000000000000000000000000000000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea0000666f6e6e6f6e6f6e6f6e6f
-000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaabac00000000000000000000000000004e4d4c4e4cb86c6d00004d4c4d4d4c4d000000000000000000000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea0000000000766f6e6e6f6e6f6e6f6e6f
+7f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec000000000000000000000000000000000000000000b0b10000000000000000000000000000000000000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea0000666f6e6e6f6e6f6e6f6e6f
+000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaabac00000000000000000000000000004e4d4c4e4c4ec0c100004d4c4d4d4c4d000000000000000000000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea0000000000766f6e6e6f6e6f6e6f6e6f
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000005142414342444545585251594642444442520000000000000000000000eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea0000000065306f6e6e6f6e6f6e6f6e6f
-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000053545354535453540000535453545354ad4c4e4d4c4e4c4d4d4e4d4eeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea4d4c6364eb406f6e6e6f6e6f6e6f6e6f
+00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000053545354535453540000535453545354ad4c4e4da0a14c4da0a14d4eeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea4d4c6364eb406f6e6e6f6e6f6e6f6e6f
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb000000000000000000000000000000dadbdadbdadb5e5f00005e5fdadbdadb46494945424145434649494649434542494546494345454145454642434455eb30406f6e6e6f6e6f6e6f6e6f
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb000000000000000000000000000000dadbdadbdadb6e6f00006e6f5e5fdadb535453545354535453545354535453545354535453545354535453545354535440406e6f6e6f6e6f6e6f6e6f
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb000000000000000000000000000000dadbdadb5e5f6e6f00006e6f6e6f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f50406e6f6e6f6e6f6e6f6e6f
diff --git a/data/data_stage1_30.p8 b/data/data_stage1_30.p8
index e68c9e12..e15fda82 100644
--- a/data/data_stage1_30.p8
+++ b/data/data_stage1_30.p8
@@ -1,9 +1,12 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic data: pico island
-- region (3, 0) by leyn
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_edition.png"
+
-- we only need __map__, but we kept __gfx__ only to visualize tiles when editing
-- (and __gff__ because it's not too big)
__gfx__
@@ -28,9 +31,9 @@ eeeeeeeeeeeeeeeeeeeeeeeebbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
@@ -47,97 +50,97 @@ eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeeeeeeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141410000414141434341414141414141410000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414141414141414141414141414141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
diff --git a/data/data_stage1_31.p8 b/data/data_stage1_31.p8
index ce36a50a..0f5e58b7 100644
--- a/data/data_stage1_31.p8
+++ b/data/data_stage1_31.p8
@@ -1,9 +1,12 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic data: pico island
-- region (3, 1) by leyn
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_edition.png"
+
-- we only need __map__, but we kept __gfx__ only to visualize tiles when editing
-- (and __gff__ because it's not too big)
__gfx__
@@ -28,9 +31,9 @@ eeeeeeeeeeeeeeeeeeeeeeeebbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeee4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
@@ -47,97 +50,97 @@ eeeeeeeeeeeeeeeeeeeeeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
-49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeed7dd11ddd71eeeee4444449999494444
-44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeee777dd1ddd711eeee4444949999444444
-49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeee7777dd1d7d111eee4344999994494444
-44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeedd777dd0dd1111ee4b34394999494444
-44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9ddd7dd101111111e3bb4b94999994444
-49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9dd1dd1101ddd111ebbbb3999999944b3
-b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a97ddd1111dd77ddd1b33bb939993b43bb
-bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deee77ddd0ddd7777dd1bb33b3b3393b3bbb
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeee404444444444444444eeeeeeeeeeeeed67dddeeee77ddd0ddddd7ddd1bb3b3bbbbb3bbb3b
-40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeee777dd1ddddddd111b3bb33bbb333bb3b
-444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeee77dd11ddddd1111103b3bbbbb3b3b3b0
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeedd11111ddd011111b03bbb333bbbbbb0
-44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6dee1111111ddd011111b333bbbbb33b30bb
-494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deee1111111dd1011111b3bbb3bb30033bbb
-4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeee11111011d10111103bb330bbbb3b33bb
-444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeee111000011000000ebbbbb0bbb33bb3bb
-444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeee777ddd11111111114444444444444444e3bbbbb3bbbb3bbe
-4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeedddddd11111111110449444449444444eeba3b3e3bb3e3b3
-449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeddddd111110111110449494449444940e3b3e3ee3bb3eebb
-44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeedddd1111101111114449494444444940ebb3eeeeeabbeea3
-4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eedddd1111001111110444494444440944ebbeeeeee3b3ee3e
-440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440e1dd11110011111100044444400000440eabbeeeeee3eeeee
-44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444e11111100111111000000040000e00000ee3beeeeeeeeeeee
-0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440000000000000000ee00000000ee00000eeebbeeeeeeeeeee
-eeeeeeccceeeeeeeeeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeecccceeeee
-eeeeeccccceeceeeeeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeeeeccccceee
-eeeeeecffcccceeeeeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeeeeccccccee
-eeeeeccfcc7ccceeeeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeee77eeccffccccce
-eeeeccccc770cceeeeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeee7777cccfc7ccc77
-eeecccccc770ceeeeecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeee7ffccc707ccee
-eeeeeeccff77f0eeeeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeee7eecfccf07c67e
-eeeeeccccfffeeeeeeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeecccccffff0ee
-eeeeccc99ccfeeeeeeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeeecf9effeeee
-eeeeeeef77ff66eeeeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeccffeeeeeee
-eeeeeecc777f56eeee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeececcceeeeeee
-eeeeeece0711eeeeee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeeecceeeeeee
-eeeeeeeeece1eeeeeee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeee6ceeeeeee
-eeeeeeeee7e7eeeeeeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeee2ceeeeeee
-eeeeeeee0878022eeeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeee06c7eeeeee
-eeeeeeee07888022eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeee0277eeeeee
-eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeeeeedeee11111eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
-eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeeeedddeee11dddeeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
-eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeeeedddeedddd77eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
-eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeeeeee1ddedddddd7eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
-eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeeeeee111edddddddeeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
-eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeee11dedddddd1eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
-eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeee1ddeddd1d11eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
-eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeeeeddd7e1d11111eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
-eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeeedd77ee1111111ddddddd49aaaa94eeeeeeeeeeeeeeee1110110111dd1111
-ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeedddd7ee111117e1dddddd3bb99bb3eeeeeeeeeeeeeeeedd1111011ddddd11
-e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeddddddeee11d77e011dddd4b9aa9b4eeeeeeeeeeeeeeee7dd11101dddd77d1
-087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2edddddddeeeed777ee1111dd4b9aa9b4eeeeeeeeeeeeeeeedd1110011ddddd11
-0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2edddddddeeedd777eee1111d49aaaa94eeeeeeeeeeeeebbb1111101111dd1110
-08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222edddddddeedd7777eeee11113bb99bb3eeeeeeeeeeeebabb1111101101111000
-e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeddddd11eddd7777eeeee0114b9aa9b4eeeeeeeeeebbbbbe111100110110000e
-ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeed111111eddddd77eeeeee004b9aa9b4eeeeeeeeebbabbbe11100010000000ee
-eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeee7a7d6d49aaaa94beeeeeeebaabbeee177011111ddd11ee
-eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeee777d6d3bb99bb3bbeeeeebbbbbbbee7777d11dd777ddde
-eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeee7a7d7d4b9449b4bbbeeebbabbbbbee77777ddd77777ddd
-eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeee7aad7d49b44b94bbbbebbbbbbbbeee777777ddd77777dd
-ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeea9ad7d04bbbb40babbbbbbbb3eeeee7777777dddd7ddd1
-ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeeeeea99d7d049bb940bbabb3bbbbbbeeee777777771dddd111
-cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeeeea99d6d00433400bbbbb33bebbeeeee7777777d11111101
-cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeeeee949d6d0a4334a0bbbabbbbbee33e3377777dd111110011
-ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeee4444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
-ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeee4444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
-eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeee444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
-eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeee44444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
-eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
-eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
-eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
-eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
-eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
-eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
-eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
-eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeeeeeeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeb3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
__gff__
-0041414141414141414141414141414141414141414141414141414141410000414141434341414141414141410000004141414141414141414141414141414141414141414141414141616180808000414040414141414141414040414141414141414141414141414141414141414141414141414141414141414141418080
-0000000000000000000000000000000000000000000000000000000000000000000000000000000041418080800100000000000000000000414141800080414100000000000000000000018080804141000000000000000000004141808080804040404043430000000080418080800041414141434300000000004040008000
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
__map__
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
@@ -147,27 +150,27 @@ __map__
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-4d00000000004e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-46313231323b4952000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-545945444258eb4c4e00000000004e0000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-5f5e5f5e5f5e5f4446313231323b495200000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6e6f53545945444258eb00000000004e4e00000000003836374c4e4d374d4e384e4de24c4d4c4e4d374d4e0000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6e6f5e5f5e5f5e5f5e5f313231323b4944313231323b4946444944464146494945454645434944464142494141414141414141000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6e6f6e6f6e6f6e6f6e6f5945444258ebeb5945444258535453535453545354da54535453545354535453540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f535453545354535453545354dadbdadadbdadbdadbdadbdadbdadbdadbdadbdadb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6f6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+4d4c4d4e4da0a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+4647484748454952000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+54594544585354a0a14e4d4d4c4d4e0000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+5f53545354dadb44464748474847495200000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f5e5f5e5f5e5f53545945444258eba0a14e4ea0a14e4ea0a14e4ea0a14e4ea0a14e4ea0a14ee2a0a14e4ea0a14e4ea00000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e5e5f5354535453544446474844464748444647484446474844464748444647484446474844464748444141414141414141000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6e6f6e6f5e5f5e5f5e5f5354575853545758535457585354575853545758535457585354575853545700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6e6f6f6e6e6f6e6f6f6edadb5354dadb5354dadb5354dadb5354dadb5354dadb5354dadb5354dadb5300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6e6f6e6f6f6e6f6e6f6f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f6e6f6f6e6f6e6f6e6f6f6e6f6e6f6f6e6f6f6e6f6e6f6f6e6f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
diff --git a/data/data_stage1_ingame.p8 b/data/data_stage1_ingame.p8
new file mode 100644
index 00000000..9219a3ba
--- /dev/null
+++ b/data/data_stage1_ingame.p8
@@ -0,0 +1,141 @@
+pico-8 cartridge // http://www.pico-8.com
+version 32
+__lua__
+-- picosonic data:
+-- runtime spritesheet by leyn
+
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_ingame.png"
+
+__gfx__
+00000000eeeeebbbeeeeeeeeeeeeeeeeeeed65eeeeeeeeeeeeeeeccccced65eeeeeeeeeee5eeeeeeeeeeeeeeeeeeeeeeee8eeeeeeeeeeeeeeeeee8eeeeeeeeee
+00000000eeeebbbeeeeeeeeeed66666666666666666666deed66cccccccc6666666666de575eeeeeeeeeeeeeeeeeeeeee272eeeeee2eeeeeeeee272eeeeee2ee
+00700700eeebbbb7eeebeeee56000000000000000000006556000cc4ccccc0c0000000655775eeeeeeeeeeeeeeeeeeee877782eee272e2eeee287778ee2e272e
+00077000eeeeeb77eebbbeee56ddd440dddddd4a0dd40d65569999cf4cccccc99999996557775eeeeeeeeeeeeeeeeeeee272eeeeee2ee8eeeeee272eee8ee2ee
+00077000eeebb3b7e777eeee56dd47a0d44404aaa047a06556999ccfccccccc995995965577775eeeeeeeeeeeeeeeeeeee8ee2eeeeee272eee2ee8eee272eeee
+00700700ebb7bb3bb3b7beee56d4aaa047aa4a7aa047a0655699ccccccccccc95759756557755eeeeeeeeeeeeeeeeeeeee2e272eee287778e272e2ee877782ee
+00000000ebb77bbbb777bbee564aa0047aaaa700a04aa065569ccccc77cccc7c57757565575eeeeeeeeeeeeeeeeeeeeeeeeee2eeeeee272eee2eeeeee272eeee
+00000000eebb777777bbbbee564a0a04a004aa0d704aa06556ccccc7077cc07c95757565e5eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee8eeeeeeeeeeee8eeeee
+eeed6deeeb773777777bbeee564a0a70a0d4aaaa707a0d655cccccc70777707c95777565e00000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eee777eebb77bb37777eeeee564a00a0a447aa44ad7a006556999cc7077ff07c995775650eeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3
+ee66666eeebbeebb777bb7bb560aaaa0aaaa0a00a0aaa0655699ccffffffff00557577550eeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3b
+eed666deeeeeee77b777b7b756d0a0a00aa00a0da00000655699cccfffffff9957757755e0eee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33bb
+eed666deeeba77777777777756dd00a0d00d0a0da0dddd65569cccc9fffff99957566655ee0e0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee337bba
+eed666deebba77777777b77b56dddd0dddddd0dd00dddd65569cc9999999999995666655eee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee337abba7
+eed666deebbb77777777bb775d77777777777777777777d55d7c777777777777775555d5eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33abbbb7b3
+eed666debbbba7777777bbbbe0000000000000000000000ee0000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee337bbbb3b33e
+eed666debbbb7a7777777777bbbb3bbbbbbbbbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee333babbbb333eee
+eed666de77b77777777777bbbbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee333b7bbbb3b33eeee
+eed666debbba777777777ebb3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33337bb3bb3333eeeee
+eed666de7bbba7777777eeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee333b33ab3bb3b33eeeeee
+eed666dea7b7777777777bbe4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33ab37bb33b3333eeeeeee
+eed666debbbb77777b3377bb4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33333333bbb3b33eeeeeeee
+ee06660eebbe77b777bbbbeb4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33ab33a3b33333eeeeeeeee
+eee000eeeebee77bebebeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33333333333eeeeeeeeeee
+44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
+44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
+44949444bbbbbbbbabbbbbbababbeaaeebeaeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaaebbabbbbbbb3bbbbbbaeeeeeeeeeeeeeeeeeeaeabbbbb
+44499444b3babbb3bbbbabbababbb3ababbababaeaeeeeaeeeeeaeeeeeeeeeeeeeeeeeeeeaeeeeaeba3bbbabbb3bab3bb3bababbeaeeeeeeeeeeeeaeabbab3bb
+44949944b3bb3bb3bb3ba3bbb3bbb3b3b3bbbbbababaebaeeeaebeeaeeeeeeeeeeeeeeeeeabeababbb3bbbbbbb3bbbbbb3babbbbbaeaeeeeeeeeaeabbbbab3bb
+44949944bbbb3bbbbb3bb3b3bbb3b3bbb3bb3bb3bbbabbabababbabbbaeeaeeeeeeaeeabbabbabbbbb3bbbbbbbbbbb3bb3bbb3bbbbbaeeaeeaeeabbbb3bbbb3b
+44449444bb3bbbbb3bbbb3bbbbb3bbbbbbbb3bb3bb3abbbbbb3bbbbbbabaaeaeeaeaababbbbbabbbbbbb3b3bbbbbbbbbbbbbbbbb3ababbaeeabbaba3b3b3bb3b
+44949444bbbbbbbbbbbbbbbbbbbb3bbbb3bbbbbbbbbbbb3bbbbbbb3bbababbabbabbababb3bbbbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+44999444bb0bb3b3bb3bb0bbbbb303bbb3b3bb3bb3bb3b3bb3b3bbbbb3bbbbabbabbbbbbbbbb3b3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+449944443003b030b30b30033b300033bbbbbb3bb3bbbbbbbbb3bb3bb3bbbbbbbbbb3b3bb3bb3bbbeeee999aaa7777a9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+49494944004030003003040003004000bbbb0bbbbbb0bbbb3bbbb33bbbbb3b3bbbbbbb3bb33bbbb3eeee444999aa7aa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+44499444044400440000044440044440b03b03bbbb30b30b033b303bbbbbbb3bbb3bbbbbb303b330eeee44999aa777a9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
+44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
+444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+__gff__
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
diff --git a/data/data_stage1_intro.p8 b/data/data_stage1_intro.p8
new file mode 100644
index 00000000..69d64562
--- /dev/null
+++ b/data/data_stage1_intro.p8
@@ -0,0 +1,141 @@
+pico-8 cartridge // http://www.pico-8.com
+version 32
+__lua__
+-- picosonic data:
+-- runtime spritesheet by leyn
+
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_intro.png"
+
+__gfx__
+00000000eeeeebbbeeeeeeeeeeeeeeeeeeed65eeeeeeeeeeeeeeeccccced65eeeeeeeeeee5eeeeeeeeeeeeeeeeeeeeeeee8eeeeeeeeeeeeeeeeee8eeeeeeeeee
+00000000eeeebbbeeeeeeeeeed66666666666666666666deed66cccccccc6666666666de575eeeeeeeeeeeeeeeeeeeeee272eeeeee2eeeeeeeee272eeeeee2ee
+00700700eeebbbb7eeebeeee56000000000000000000006556000cc4ccccc0c0000000655775eeeeeeeeeeeeeeeeeeee877782eee272e2eeee287778ee2e272e
+00077000eeeeeb77eebbbeee56ddd440dddddd4a0dd40d65569999cf4cccccc99999996557775eeeeeeeeeeeeeeeeeeee272eeeeee2ee8eeeeee272eee8ee2ee
+00077000eeebb3b7e777eeee56dd47a0d44404aaa047a06556999ccfccccccc995995965577775eeeeeeeeeeeeeeeeeeee8ee2eeeeee272eee2ee8eee272eeee
+00700700ebb7bb3bb3b7beee56d4aaa047aa4a7aa047a0655699ccccccccccc95759756557755eeeeeeeeeeeeeeeeeeeee2e272eee287778e272e2ee877782ee
+00000000ebb77bbbb777bbee564aa0047aaaa700a04aa065569ccccc77cccc7c57757565575eeeeeeeeeeeeeeeeeeeeeeeeee2eeeeee272eee2eeeeee272eeee
+00000000eebb777777bbbbee564a0a04a004aa0d704aa06556ccccc7077cc07c95757565e5eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee8eeeeeeeeeeee8eeeee
+eeed6deeeb773777777bbeee564a0a70a0d4aaaa707a0d655cccccc70777707c95777565e00000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eee777eebb77bb37777eeeee564a00a0a447aa44ad7a006556999cc7077ff07c995775650eeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3
+ee66666eeebbeebb777bb7bb560aaaa0aaaa0a00a0aaa0655699ccffffffff00557577550eeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3b
+eed666deeeeeee77b777b7b756d0a0a00aa00a0da00000655699cccfffffff9957757755e0eee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33bb
+eed666deeeba77777777777756dd00a0d00d0a0da0dddd65569cccc9fffff99957566655ee0e0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee337bba
+eed666deebba77777777b77b56dddd0dddddd0dd00dddd65569cc9999999999995666655eee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee337abba7
+eed666deebbb77777777bb775d77777777777777777777d55d7c777777777777775555d5eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33abbbb7b3
+eed666debbbba7777777bbbbe0000000000000000000000ee0000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee337bbbb3b33e
+eed666debbbb7a7777777777bbbb3bbbbbbbbbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee333babbbb333eee
+eed666de77b77777777777bbbbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee333b7bbbb3b33eeee
+eed666debbba777777777ebb3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33337bb3bb3333eeeee
+eed666de7bbba7777777eeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee333b33ab3bb3b33eeeeee
+eed666dea7b7777777777bbe4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33ab37bb33b3333eeeeeee
+eed666debbbb77777b3377bb4444444444004494eeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33333333bbb3b33eeeeeeee
+ee06660eebbe77b777bbbbeb4449444444044494eaeaeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33ab33a3b33333eeeeeeeee
+eee000eeeebee77bebebeeee4449444444444444baebeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee33333333333eeeeeeeeeee
+44444444eaeaeeeeeeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaebeeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+44449444babaeabeeaeebeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeebaeabbbabbeeeeeeeeeeeeeeeeeeeeeeeeeaeab
+44444494bbbababbbababbbbeebeeaeeeeeeeebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaeebbabab3babbbeaeeeeeeeeeeeeeeeeeeaeeebbab
+44949444bbbbbbbbabbbbbbababbeaaeebeaeebeeaeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaeeaaebbabbbbbbb3bbbbbbaeeeeeeeeeeeeeeeeeeaeabbbbb
+44499444b3babbb3bbbbabbababbb3ababbababaeaeeeeaeeeeeaeeeeeeeeeeeeeeeeeeeeaeeeeaeba3bbbabbb3bab3bb3bababbeaeeeeeeeeeeeeaeabbab3bb
+44949944b3bb3bb3bb3ba3bbb3bbb3b3b3bbbbbababaebaeeeaebeeaeeeeeeeeeeeeeeeeeabeababbb3bbbbbbb3bbbbbb3babbbbbaeaeeeeeeeeaeabbbbab3bb
+44949944bbbb3bbbbb3bb3b3bbb3b3bbb3bb3bb3bbbabbabababbabbbaeeaeeeeeeaeeabbabbabbbbb3bbbbbbbbbbb3bb3bbb3bbbbbaeeaeeaeeabbbb3bbbb3b
+44449444bb3bbbbb3bbbb3bbbbb3bbbbbbbb3bb3bb3abbbbbb3bbbbbbabaaeaeeaeaababbbbbabbbbbbb3b3bbbbbbbbbbbbbbbbb3ababbaeeabbaba3b3b3bb3b
+44949444bbbbbbbbbbbbbbbbbbbb3bbbb3bbbbbbbbbbbb3bbbbbbb3bbababbabbabbababb3bbbbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+44999444bb0bb3b3bb3bb0bbbbb303bbb3b3bb3bb3bb3b3bb3b3bbbbb3bbbbabbabbbbbbbbbb3b3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+449944443003b030b30b30033b300033bbbbbb3bb3bbbbbbbbb3bb3bb3bbbbbbbbbb3b3bb3bb3bbbeeee999aaa7777a9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+49494944004030003003040003004000bbbb0bbbbbb0bbbb3bbbb33bbbbb3b3bbbbbbb3bb33bbbb3eeee444999aa7aa9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+44499444044400440000044440044440b03b03bbbb30b30b033b303bbbbbbb3bbb3bbbbbb303b330eeee44999aa777a9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+44999444444444444400449444444444000b00b03b00b00003033003bbbbbbbbbb3bbbbb30033030eeeeeeddddddddeeeeeeeeeeeeeaeeeeeeeeeaeeeeeeeeee
+44994944444944444404449444494444040300300300304040003040b3b3bbbbbbbb3b3b30400000eeeeed66777766deeaeeeeaeeaeaeeaeeeaeaaeeeeeeeeee
+444949444444444444444444444944444440040000440444404004403bb3bb3bb3bb3bb304400404eeeeeeddddddddeeebeebeaebaebeebeeabeabebeeeeeeee
+49499444eeeeeeebbeeeeeee4444444494444444bb3bbbb33bbbbbbb0b30b30bb3bb03b03bbbb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444449999494444
+44949444eeeeeeabbaeeeeee4449444494944444bbbb3b300bb3b3b30030b00bb03b030003b3bbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444949999444444
+49949944eeeeeeabbaeeeeee4444494444444444b3bb3b0403b333b040003003b003000440b3bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4344999994494444
+44949494eeeeeeb33beeeeee4444494994444444b33bbb0440303030440400403000404440bbb33beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4b34394999494444
+44999944eeeeeebeebeeeeee44444449949444443003b304400000004444444004044444403b3003eeee999aaa7777a9eeeeeeeeeeeeeeee3bb4b94999994444
+49994934eeeeeebeebeeeeee44449499994444440403300444040404444494440444444440033040eeee444999aa7aa9eeeeeeeeeeeeeeeebbbb3999999944b3
+b39933b3eeeeeeeeeeeeeeee44449499494944444440304444444444444494444449444444030444eeee44999aa777a9eeeeeeeeeeeeeeeeb33bb939993b43bb
+bb93bbbbeeeeeeeeeeeeeeee44449449494944444444004444444444444444444444444444004444eeeeeeedd766deeeeeeeeeeeeeeeeeeebb33b3b3393b3bbb
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee0eeeeeeee0444444444444444eeeeeeeeeeeeeed67dddeeeeeeeeeeeeeeeeeeeebb3b3bbbbb3bbb3b
+40eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04eeeeeee4e04944444444944e4eeeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeeeb3bb33bbb333bb3b
+444eeeee4440eeeeeeeeeeeeeeeeeeeeeeee0044eeeee444eeeeeee4e04944444944440e4eeeeeeeeeeeee55dddeeeeeeeeeeeeeeeeeeeee03b3bbbbb3b3b3b0
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04444eeee0494eeeeee04ee044944449440ee40eeeeeeeeeeeee55ddddeeeeeeeeeeeeeeeeeeeb03bbb333bbbbbb0
+44440eee4944400eeeeeeeeeeeeeeeaee0444944eee04494eeeeee04eee0444444940eee40eeeeeeeeeeeeeeeedd6deeeeeeeeeeeeeeeeeeb333bbbbb33b30bb
+494944ee449444400eeeeeeeeeeeaea004444994ee449444eeeeee44eee0449444440eee40eeeeeeeeeeeeeeed77deeeeeeeeeeeeeeeeeeeb3bbb3bb30033bbb
+4449440e44944944440eeeeeeeee303b44944494e0444494eeeee044eeee44944944eeee440eeeeeeeeeeeed77ddeeeeeeeeeeeeeeeeeeee3bb330bbbb3b33bb
+444444404444444444440eeeebebbb3b4444444404444444eeeee044eeee04444440eeee444eeeeeeeeeeed6ddeeeeeeeeeeeeeeeeeeeeeebbbbb0bbb33bb3bb
+444444404444444444440eeeeee044444444444404444444eeee0444eeeee494440eeeee4440eeeeeeeeeeeeeeeeeeee4444444444444444e3bbbbb3bbbb3bbe
+4494440e44444444440eeeeeeeeee00494494444e0449494eeee4444eeeee044440eeeee4944eeeeeeeeeeeeeeeeeeee0449444449444444eeba3b3e3bb3e3b3
+449940ee499449400eeeeeeeeeeeeee044444944ee444494eee04494eeeeee4444eeeeee49440eeeeeeeeeeeeeeeeeee0449494449444940e3b3e3ee3bb3eebb
+44444eee4494440eeeeeeeeeeeeeeeeee0044994eee04444eee04494eeeeee0444eeeeee44440eeeeeeeeeeeeeeeeeee4449494444444940ebb3eeeeeabbeea3
+4940eeee44440eeeeeeeeeeeeeeeeeeeeee04494eeee0494ee044444eeeeee0440eeeeee494440eeeeeeeeeeeeeeeeee0444494444440944ebbeeeeee3b3ee3e
+440eeeee4400eeeeeeeeeeeeeeeeeeeeeeee0444eeeee444e0444494eeeeeee44eeeeeee4449440eeeeeeeeeeeeeeeee0044444400000440eabbeeeeee3eeeee
+44eeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee04eeeeee04e4449444eeeeeee44eeeeeee4949444eeeeeeeeeeeeeeeee0000040000e00000ee3beeeeeeeeeeee
+0eeeeeee0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0eeeeeee004444444eeeeeee44eeeeeee44444440eeeeeeeeeeeeeeeee00000000ee00000eeebbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee77dded777deeeeeeeeeeeeeeeeeeeeeeeeeee444444499949444494494444eeeeeeee99000000eeeeeeeed6d949eeeeeeee0288eeeeee
+eeeeeeeeeeeeeeeeeedd77d1dd777ddeeeeeeeeeeeeeeeeeeeeeeee0444449499444444494444444eeeeee99aa9000499eeeeeeed6d99aeeeeeeeee0870eeeee
+eeeeeeeeeeeeeeeee7d1111dddd77d1eeeeeeeeeeeeeeeeeeeeeee00444449499444444499494444eeeee9aa9a94049aa9eeeeeed7d99aeeeeeeeeee780eeeee
+eeeeeeeeeeeeeeeeed11111dddd7d111eeeeeeeeeeeeeeeeeeeeee00444444499949444499444944eeee9aa9aa94049aaa9eeeeed7da9aeeeeeeeeee080eeeee
+eeeeeeeeeeeeeeeee111117ddddd1111eaeeeeeeeeeeeeeeeeeee040444444499944444444444444eeee9a9aaa94049aaa9eeeeed7daa7eeeeeeeeeee0eeeeee
+eeeeeeaeeaeeeeeee111d777dddddd11baeaeeeeeeeeeeeeeeeee004444494949994444444004444eeee9a9aa994004999eeeeeed7d7a7eeeeeeeeeeeeeeeeee
+eeaeaebeebeaeaeee11ddd77710d1111bbbaeeeeeeeeeeeeeeee0440444494949949444440000044eeeee99999403344eeeeeeeed6d777eeeeeeeeeeeeeeeeee
+ababbbbbbbbbbabae0ddddd7111111113abbbaeeeeeeeeeeeeee0444444494999949444400000004eeeeeeee449aa934eeeeeeeed6d7a7eeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee7dddddd1110110eeeeeeeeeeeeeeeeeeee04444444444999949444404411444eeeeeeee49aaaa94eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1dd1dd107777ddeeeeeeeeeeeeeeeeeeee049444404949999444044044dd444eeeeeeee3bb99bb3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee1d1ddd0dd7777deeeeeeeeeeeeeeeeeee0444444404999900094444444cd444eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeee111d77dddd7777eeeeeeeeeeeeeeeeeee0444444444494044494044494cc494eeeeeeee4b9aa9b4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeedd7d11dd7eeeee11d777777d7777deeeeeeeeeeeeeeeee044949400044940444900004447c494eeeeeeee49aaaa94eeeeeeeeeeeeebbbeeeeeeeeeeeeeeee
+eedd777ddd7d11eeeed77777777d7dddeeeeeeeeeeeeeeeee0449444494049444440449440077444eeeeeeee3bb99bb3eeeeeeeeeeeebabbeeeeeeeeeeeeeeee
+ee1ddd77d0dd111eed77777777d1d111eeeeeeeeeeeeeeee04944444444044444440449444477044eeeeeeee4b9aa9b4eeeeeeeeeebbbbbeeeeeeeeeeeeeeeee
+ee11dd7dd011111ed777777777111111eeeeeeeeeeeeeeee04444444444044444444444494417000eeeeeeee4b9aa9b4eeeeeeeeebbabbbeeeeeeeeeeeeeeeee
+ee17dddd101dd11edd7777777d111111eeeeeeee17ccdc70dd1c7c714440444400004444cd104444ee7a7d6d49aaaa94beeeeeeebaabbeeeeeeeeeeeeeeeeeee
+edd777dd0dd77dd1dddddddddd111111eeeeeeee177cc773ccd77711000444444440404411044949ee777d6d3bb99bb3bbeeeeebbbbbbbeeeeeeeeeeeeeeeeee
+ddd7777d1ddddd11ddd1dddddd111111eeeeeeee1717c7107cc7171d440440404440444411444444ee7a7d7d4b9449b4bbbeeebbabbbbbeeeeeeeeeeeeeeeeee
+dddd77d11ddd1111ddd7dddddd111110eeeeeeeed11171131771111d4400000049404040d0499444ee7aad7d49b44b94bbbbebbbbbbbbeeeeeeeeeeeeeeeeeee
+dddddd111ddd1111ddd7dddddd111110eeeeeeeed1d171d31171d1dc4404444044440000d0494444eea9ad7d04bbbb40babbbbbbbb3eeeeeeeeeeeeeeeeeeeee
+ddddd1111ddd1111ddddddddd111111eeeeeeeeecddd1dc0111ddddc0004444000004440c0444444eea99d7d049bb940bbabb3bbbbbbeeeeeeeeeeeeeeeeeeee
+ddddd11111d11110ddddddddd111101eeeeeeeeeccddddc01d1dcdd74440400440404490c0449494eea99d6d00433400bbbbb33bebbeeeeeeeeeeeeeeeeeeeee
+111d11110110110eddddddddd111101eeeeeeeee7ccddc70dd1dccd74440444444444444c0449444ee949d6d0a4334a0bbbabbbbbee33e33eeeeeeeeeeeeeeee
+e1111dd10dd777dedddddddd1111101eeeeeeeee44404444404444404440444440444440704449444444444999494444bbbbbbbbe333333333eebbbbbbeeeeee
+ee11d77ddd77777ddddddddd11111011eeeeeeee04044444444040400404444444404040704944444444494994444444bbbbabbb33333bbbbbbbbbbaabbbeeee
+ee1d7777ddd7777d1ddddddd11111111eeeeeeee0000440444400000000044044440000070494444444449499444444433bbabbbbb3bbabbabbbbebbbbbbbeee
+eedd777d10dddd1101dddddd11111111eeeeeeee040044444040000004004444404000007444444444444449994944443bbbb3bbbb3bbabbbbbbbbebbbbbbbee
+edddd7dd10ddd111e111dddd11110111eeeeeeee00000404000000000000040400000000100004444444444999444444bb3bbbbbbabbbbbbbbebbbbbbbebbbbe
+dddddd1110ddd111e111ddd111100111eeeeeeee000000a00a40000000000000004a0000d04044444444949499944444bb3bbb3bbbbbbbebbbbebbeeeeeeeeee
+ddddd11110dd1111eee0111111011110eeeeeeee00a0a4b00b0a0a000a0004a00a0a00a0c09444444444949499494444abbbbb33bbbbbbbebbbbeeeeeeeeeeee
+dd1111110011011eeeee00000000000eeeeeeeeeababbbbbbbbbbaba0b00b0a0ba0b00b0c94944444444949999494444bbbbbbbbbbbbb33eeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeee5d5eeeeeeeeeeaaaaaaaaaaaaaaa9eeeeee56deeeeeeeeeeeeeed65eeeeeee30bbebe44444444ababb3bbbbb33bbbbeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeeeeaaaaaaaaaaaaaaa94ed6566666666666ee6666666666656de3bebeeb349444444bba33bbbbabbb3333bbeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeeee9a9a9a9a9a9aaa9940d656000000000005500000000000656dbeb33b3e44444494bbbbbbbbbbbabb33333bbeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeed65eeeeeee9a9a9a9a9a9aaa94400d656d944dc49d91551d944dc49d9656deb3e33eb44494444b3abba3bbbbbabb333333beeeeeeeeee
+eeeea994eeeeeeeeeeed65eeeeee999999999999aa944400d65694cdc0ca9a155194cdc0ca9a656de03bb03b44494444b00bb00bbbbbbbbbe33333beeeeeeeee
+eeeaaaa94eeeeeeeeeed65eeeee99999999999aa99444000d656dcacac75471551dcacac7547656d3beb3ebe44444494b00bb00bbbbbbbab3333b33beeeeeeee
+eeaaaaaaa999eeeeeeed65eeee9999999999aa9449444400d6569ac7ca7a7a15519ac7ca7a7a656db3b33eb3449444443b0330b33b3bbbbbb33bee3beeeeeeee
+eaaaaaaaaaaa999eeeed65eee4949494999a944444444000d6564c000c094715514c000c0947656deebeebee444444440b0330b0eeebbebbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeee949494449a99494444444440ed656c0ca7a77701551c0ca7a7770656de77778ee4444444444444444eeeeebbbbbeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeee99444449a9944444444444440eed65649a04f4977155149a04f4977656d7777788e0449449949944444eeeeeebbbbaeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeee44449a994444444444994440eeeed6569afaf00a7a15519afaf00a7a656d2288822e0449444994444000eeeeeeeebbbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeeee4499994444494444944494444eeeeed656dcafaf05071551dcafaf0507656de28882ee4444444999444044eeeeeeeeebbbeeeeeeeeeeee
+eeeeeeeeeeeeeeeea9994494499449444494444440eeeeeed65690cdf09a5d155190cdf09a5d656dee282eee0444499444444444eeeeeeeeebbbbeeeeeeeeeee
+eeeeeeeeeeee99994494444444944444449444444eeeeeeed656dc09d9d9d61551dc09d9d9d6656deee2eeee0444444444444440eeeeeeeeeebbaeeeeeeeeeee
+eeeeeeee44994494449444444494444444444400eeeeeeeed60d777777777775577777777777d06deeeeeeee004499440e004440eeeeeeeeeeebaeeeeeeeeeee
+eeee44994444444444444444444444444440eeeeeeeeeeeeed6000000000000ee0000000000006deeeeeeeeee0049eeeeeee000eeeeeeeeeeeeeeeeeeeeeeeee
+__gff__
+0000000000000000000000000000000000000000000000000000000000000000000000434340000000000000000000004141414141414141414141414141414141414141414141414141212180808000414040414141414141410000000041414141414141414141414101010000414141414141414141414141000041418080
+0000000000000000000000000000000000000000000000000000000000000000414141414500414040408080800100004141414100004140401000800080101041414141001010404010018080801010414141410041414040104141808080804040404043430000000080418080800041414141434300000000004040008000
diff --git a/data/data_stage1_runtime.p8 b/data/data_stage_sonic.p8
similarity index 55%
rename from data/data_stage1_runtime.p8
rename to data/data_stage_sonic.p8
index 23bebb90..0f10bc93 100644
--- a/data/data_stage1_runtime.p8
+++ b/data/data_stage_sonic.p8
@@ -1,61 +1,109 @@
pico-8 cartridge // http://www.pico-8.com
-version 29
+version 32
__lua__
-- picosonic data:
-- runtime spritesheet by leyn
+
+-- Import latest spritesheet. Open data with pico8 -run for it to run automatically on launch.
+import "spritesheet_stage_sonic.png"
+
__gfx__
-00000000eeeeebbbeeeeeeeeeeeeeeeeeeed65eeeeeeeeeeeeeeeccccced65eeeeeeeeeee5eeeeeeeeeeeeeeeeeeeeeeee8eeeeeeeeeeeeeeeeee8eeeeeeeeee
-00000000eeeebbbeeeeeeeeeed66666666666666666666deed66cccccccc6666666666de575eeeeeee7eeeeeeeeee7eee272eeeeee2eeeeeeeee272eeeeee2ee
-00700700eeebbbb7eeebeeee56000000000000000000006556000cc4ccccc0c0000000655775eeeeeed7eee87eee7dee877782eee272e2eeee287778ee2e272e
-00077000eeeeeb77eebbbeee56ddd440dddddd4a0dd40d65569999cf4cccccc99999996557775eeeeee7ee8008ee7eeee272eeeeee2ee8eeeeee272eee8ee2ee
-00077000eeebb3b7e777eeee56dd47a0d44404aaa047a06556999ccfccccccc995995965577775eeedd7780970877ddeee8ee2eeeeee272eee2ee8eee272eeee
-00700700ebb7bb3bb3b7beee56d4aaa047aa4a7aa047a0655699ccccccccccc95759756557755eeeeedd78049087ddeeee2e272eee287778e272e2ee877782ee
-00000000ebb77bbbb777bbee564aa0047aaaa700a04aa065569ccccc77cccc7c57757565575eeeeeeeedde8008eddeeeeeeee2eeeeee272eee2eeeeee272eeee
-00000000eebb777777bbbbee564a0a04a004aa0d704aa06556ccccc7077cc07c95757565e5eeeeeeeeeeeee88eeeeeeeeeeeeeeeeeeee8eeeeeeeeeeee8eeeee
-eeed6deeeb773777777bbeee564a0a70a0d4aaaa707a0d655cccccc70777707c95777565e00000eeeeeeccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeccccceeeee
-eee777eebb77bb37777eeeee564a00a0a447aa44ad7a006556999cc7077ff07c995775650eeeee0eeeeeeccccceeceeeeeeeccccceeeeeeeeeeeeeeccccccece
-ee66666eeebbeebb777bb7bb560aaaa0aaaa0a00a0aaa0655699ccffffffff00557577550eeeee0eeeeeeecffcccceeeeeeeeccccceeceeeeeeeeeeecffcccfe
-eed666deeeeeee77b777b7b756d0a0a00aa00a0da00000655699cccfffffff9957757755e0eee0eeeeeecccfcc7ccceeeeeeeecffcccceeeeeeeeecccfcc7ccc
-eed666deeeba77777777777756dd00a0d00d0a0da0dddd65569cccc9fffff99957566655ee0e0eeeeeecccccc770cceeeeeecccfcc7ccceeeeeeecccccc770cc
-eed666deebba77777777b77b56dddd0dddddd0dd00dddd65569cc9999999999995666655eee0eeee77eeecccc770ceeeeeecccccc770cceeeeeeeeecccc7707c
-eed666deebbb77777777bb775d77777777777777777777d55d7c777777777777775555d5eeeeeeeee77eeeccff77f0ee77eeecccc770ceeeeeeeeeffccff77f0
-eed666debbbba7777777bbbbe0000000000000000000000ee0000000000000000000000eeeeeeeee777ffccccfffeeeee77eeeccff77f0eeeee77fcccccfffee
-eed666debbbb7a7777777777bbbb3bbbbbbbbbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeefccfeeeeeeeee7ffccccfffeeeeee7707eccffce67e
-eed666de77b77777777777bbbbb303bbbb3bb0bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee1e1cffeeeeeeee7e1efcffeeeeeeeeee77cccccf11607
-eed666deebba777777777ebb3b300033b30b3003eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee11cf1eeeeeeeeeee11cff11eeeeeeeeccceee611ee06
-eed666deebbba7777777eeee0300400030030400eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeecc11ee8800eeeeeecccc1ee880ee77eeee227eeeee
-eed666dee7b7777777777bbe4004444000000444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeecc1688800eeeeeeeeec6e8880e882eeee2887eeee
-eed666debbbb77777b3377bb4444444444004494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeec7872000eeeeeeeeec77820008772eeee268eeee
-ee06660eebbe77b777bbbbeb4449444444044494eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee788200eeeeeeeeee788820e07882eeee288eeee
-eee000eeeebee77bebebeeee4449444444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee22000eeeeeeeeeee88200ee0888eeeee8eeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+00000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+00000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+00700700eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+00077000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+00077000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+00700700eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+00000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+00000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeccceeeeeeeeeeeeeccceeeeeeeeeeeeeeccceeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeccceeeeeeeeeeeeeecccceeeee
+eeeeccccceeceeeeeeeeccccceeceeeeeeeeeccccceeceeeeeecccccceceeeeeeeecccccceceeeeeeeecccccceceeeeeeeeeeccccceeceeeeeeeeeeeccccceee
+eeeeecffcccceeeeeeeeecffcccceeeeeeeeeecffcccceeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeccfccceeeeeeeeeeecffcccceeeeeeeeeeeccccccee
+eeeeccfcc7ccceeeeeeeccfcc7ccceeeeeeeeccfcc7ccceeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeccccfc7cceeeeeeeeeccfcc7ccceeee77eeccffccccce
+eeeccccc770cceeeeeeccccc770cceeeeeeeccccc770cceeeeccccccc71ceeeeeeccccccc71ceeeeeeccccccc71ceeeeeeeeccccc770cceee7777cccfc7ccc77
+eecccccc770ceeeeeecccccc770ceeeeeeecccccc770ceeeecccccccc70ceeeeecccccccc70ceeeeecccccccc70ceeeeeeecccccc770ceeeeee7ffccc707ccee
+eeeeeccff77f0eeeeeeeeccff77f0eeeeeeeeeccff77f0eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeeeeccff77f0eeee7eecfccf07c67e
+eeeeccccfffeeeeeeeeeccccfffeeeeeeeeeeccccfffeeeeeeecccc7fffeeeeeeeecccccff7eeeeeeeecccccfffeeeeeeeeeeccccfffeeeeeeeecccccffff0ee
+eeecffcff6eeeeeeeeecffccfee56eeeeee777fccfeeeeeeeeccc9777eeeeeeeeeccccffe777eeeeeeccccffee7eeeeeeeeeccc99ccfeeeeeeeeeecf9effeeee
+eeefeecffeeeeeeee77feccff9966ee8ee7770ccff66eeeeeeee9f07eeeeeeeee6ceccfff770eeeeeeceecffe777eeeeeeeeeeef77ff66eeeeeeeccffeeeeeee
+ee770ccc7feeeeee707ecccffeeeee88eee7ececff56eeeeeeeecccf111eeeeee66ccccf1eeee20eee07ccccf770eeeeeeeeeecc777f56eeeeeececcceeeeeee
+ee777ec887eeeeeee7eececcccc78780e002661cccee80eeeeeececce61eeeeeeeececeee172700eee087eee1eeeeeeeeeeeeece0711eeeeeeeeeeecceeeeeee
+eee7e260887eeeeeee02611eeec7870e0262611ec78780eeee87ccce0262eeeee86cceeee77200eeee088eeee16eeeeeeeeeeeeeece1eeeeeeeeeee6ceeeeeee
+eeee022000788eeee02261eeeee8880e022eeeee78780eeee088eeeee0222eee6886eeeeee200eeeeee078eee626220eeeeeeeeee7e7eeeeeeeeeee2ceeeeeee
+eeee0022eee00eee062eeeeeeeee00eeeeeeeeee8870eeeee0877eeeeee00eee860eeeeeee00eeeeeeee00eee02600eeeeeeeeee0878022eeeeeee06c7eeeeee
+eeeee00600eeeeee22eeeeeeeeeeeeeeeeeeeeee000eeeeeee08888eeeeeeeee00eeeeeeeeeeeeeeeeeeeeeeee00eeeeeeeeeeee07888022eeeeee0277eeeeee
+eeeeeceeeeeeeeeeeeeeeceeeeeeeeeeeeeeeeceeeeeeeeeeeeeceeeeeeeeeeeeeeeceeeeeeeeeeeeeeeceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eecccccceeeeeeeeeecccccceeeeeeeeeeecccccceeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeeeeeccceeeeeeeeeeeeeeeeeeeeeee
+ecccccccceeeeeeeecccccccceeeeeeeeecccccccceeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeeeeeeecccceeeeeeeeeeeeeeeeeeeee
+ccccc70cc0eeeeeeccccc70cc0e56ee0eccccc70cc0eeeeeccccc70cc0eeeeeeccccc70cc0eeeee0ccccc70cc0eeeeeeeeeeeccccccceceeeeeeeeeecceeecee
+ccffc7d07feeeeeeccffc7d07fe66e88eccffc7d07feeeeeccffc7d07feeeeeeccffc7d07fe77e62ccffc7d07feeeeeeeeeeccccffcccceeeeeeeeeeecccecce
+cccfc777ffeeeeeecccfc777ff9eee77ecccfc777ff6eeeecccfcc77ffeeeeeecccfcc77ff777e26cccfcc77ffeeeeeeeeeeeeccfc7cceeeeeeeecccecccccce
+ceccccfff66eeeeececcccfff9eee788ececcccfff65eeeececcccff7eeeeeeececcccfffe777622ceccccfffeeeeeeeeeeeecccc770cceeeeeeccccc7ccccce
+ecccccccff6eeeeeecccccccffccc780eecccccccffeeeeeecccccc7770eeeeeecccccccfff11620ecccccccfe771eeeeeeeccccc770cceeeeeeeecc7777cc77
+ecceccfccfeeeeeeecceccfccfceeeeeeecceccfccfeee82ecceccf9711eeeeeeccecccff1eeeeeeeccecccfc777e20eeeeeec9cff77f0eeeeeecccc776cccc7
+eceecfcc7eee88eeeceecfeccceeeeeeeeceec7ecccce782eceecec9ee1eeeeeeceec9cccceeeeeeeceec66ff777620eeeeeccfcc7fff7eeeeecccfc76ccccc6
+eeeefcc7787880eeeeeeefece1eeeeeeeeeee770c1ec7872eeeeeeccee77222eeeee66cceceeeeeeeeeee60c1cc6260eeeeeceff777c777eeeeeecffcccfccce
+eeeef77088700eeeeeee77eee1eeeeeeeeeee777e1e7882eeeeeeecece2270eeeeee66ceeceeeeeeeeeeeee1ee6220eeeeeeeecc76eee76eeeeccccccccffccc
+eeee7777080eeeeeeeee707e66eeeeeeeeeeee7611ee00eeeeeeeee7c708eeeeeeeeecee77eeeeeeeeeeee11eee00eeeeeeeece1e1eeeeeeeeeeeeccc7ccccce
+eeeee772662622eeeeeee7e222eeeeeeeeeeee226eeeeeeeeeeeeee777880eeeeeeeeee888eeeeeeeeee0717eeeeeeeeeeeeee7e7eeeeeeeeeeeee7c77711cee
+eeeeeee000060eeeeeeeeee066eeeeeeeeeeee66eeeeeeeeeeeeeee87880eeeeeeeeeee277eeeeeeeeee02787eeeeeeeeeeee0878022eeeeeeeee0870011eeee
+eeeeeeee2220eeeeeeeeeeee022eeeeeeeeeee02eeeeeeeeeeeeeee0220eeeeeeeeeeeee288eeeeeeeeee02878eeeeeeeeeee07888022eeeeeeee07888022eee
+eeeeecccceeeeeeeeeeeeecccceeeeeeeeeeecccceeeeeeeeeeeeecccceeeeeeeeeecccccceeeeeeeeeccccccceeeeeeeeeecccccceeeeeeeeeccccccceeeeee
+eeeeeecffcceceeeeeeeecccccceceeeeeeeeecffcceceeeeeeeecccccceceeeeeecccc70cc0eeeeecccccc70cc0eeeeeeecccc70cc0eeeeecccccc70cc0eeee
+eeeeeeecffccceeeeeeeeecffcccceeeeeeeeeecffccceeeeeeeeecffcccceeeeeccffc7d07feeeeccccffc7d07feeeeeeccffc7d07feeeeccccffc7d07feeee
+eeeeccccccc7cceeeeeeeccfccc7cceeeeeeccccccc7cceeeeeeeccfccc7cceeecccfcc777ffeeeeeeccfcc777ffeeeeecccfcc777ffeeeeeeccfcc777ffeeee
+eeeccccccc770ceeeeeecccccc770ceeeeeccccccc770ceeeeeecccccc770ceeececccccfff220eeeeecccccfffeeeeeececccccff22e20eeeecccccfffeee2e
+eeeeeccccc770ceeeeeccccccc770ceeeeeeeccccc770ceeeeeccccccc770ceeeeeccccfffee260eeecccccfffeeee2eeeeccccff22ee800eecccccfffeeeee2
+eeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeeeeecccff77f0eeeccccccc66e222eeccccc8cc66e2e2eeeccccccc66ee780ecccccccc66ee2e2
+eeec9cc70cfffeeeeeee9c888cfffeeeeeec9cc702f2feeeeeee9cc70cfffeeeeecceccc766e6200ccccc88c766ee22eeecceccc766ee870cccccccc766eee22
+eee9ff77756e2eeeeee9f888856eeeeeeee9ff77752e22eeeee9ff777e6eeeeeeeceeec777f11680cceee88777fee22eeeceecc777fee787cceeecc777feee22
+ee7ecc77766220eeee228887766eeeeeeeee11777662222eee221177766eeeeeeeeeecf77fce6880eeee88877fcee2eeeeeeecf77fccc788eeeeccf7fcceee22
+e777cccff1e260eee22888cffeeeeee2ee87e11ffeee280ee228e11ffeeeeeeeeeeeefccceeee88eeeee88cc11ee22eeeeeeef11cceeee88eeeecf11cceee22e
+087cccece16220ee222ccee11eeee2e2088111ecccee780e22211eecceeeeee2eeeeee7cceeeeeeeeee288cee1e222eeeeeeeee1eeeeeeeeeeee211eec7ee88e
+0888eeeee6820eee22eeeeee1eeee2220887eeeeeec7870e22eeeeeceeeee2e2eeeee777eeeee2eeeee22cce71772eeeeeeee711eeee2eeeeee2281ee778780e
+08878eeee8880eee2eeee871eeee222e08822eeeee7880ee2eeeee77eeee2222eeee087872e22eeeeeee2cee8722eeeeeeee287e2ee22eeeeeee221ee08870ee
+e07782eeee80eeeee2ee8877222222eee02222eeee8880eee2eee0887822222eeeee08888722e2eeeeee2eee822eeeeeeeee88822222eeeeeeee22eeee080eee
+ee0888222eeeeeeeeeee82227222eeeeee0222222ee80eeeeeeee08788822eeeeeeee00870822eeeeeeee2ee22eeeeeeeeee0888222eeeeeeeeee222eeeeeeee
+eeeecccceeeeeeeeeeeecccceeeeeeeeeeeeccecceeeeeeeeeee2222eeeeeeeeeeeeeee8eeeeeeeeeeeeccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeccccceeeee
+eecccccccceeeeeeeeccccccceeeeeeeeeceeccecceeeeeeee278227cceeeeeeeeeeee0e82eeeeeeeeeeeccccceeceeeeeeeccccceeeeeeeeeeeeeeccccccece
+eccccccc7cceeeeeeccccfccceeeeeeeececccccccceeeeee28777ccceceeeeeeeecccfee82eeeeeeeeeeecffcccceeeeeeeeccccceeceeeeeeeeeeecffcccfe
+eccccc777cceeeeecccccfc7cceeeeeeecccccccccceeeeee8ee777fcceeeeeeeccc007fe77eeeeeeeeecccfcc7ccceeeeeeeecffcccceeeeeeeeecccfcc7ccc
+ccccccc777cceeeececcccc70ceeeeee27cfcccccccceeee8eefffcccceceeeeccc7777f7782eeeeeeecccccc770cceeeeeecccfcc7ccceeeeeeecccccc770cc
+ccccccc7cccceeeeecccccc70ceeeeee22c7cfccffcceeeee0f77ffccccceeeeccccccff7722eeee77eeecccc770ceeeeeecccccc770cceeeeeeeeecccc7707c
+cccccccccccceeeecccccff77f0eeeee2277ffcccccceeeeeec07cccccceeeeeccffccfc7c22eeeee77eeeccff77f0ee77eeecccc770ceeeeeeeeeffccff77f0
+cccccccccccceeeececcccfffee8eeee2877f7777ccceeeeeec07ccccceceeeeccccccccfc72eeee777ffccccfffeeeee77eeeccff77f0eeeee77fcccccfffee
+ecccccccccceeeeeeeccf777ee8eeeeee77ef700ccceeeeeeecc7cfccccceeeeecccccccccceeeeeeeeeefccfeeeeeeeee7ffccccfffeeeeee7707eccffce67e
+ecccccccccceeeeeececcc77782eeeeee28eefccceeeeeeeeeecccfcccceeeeeecccccccceceeeeeeee1e1cffeeeeeeee7e1efcffeeeeeeeeee77cccccf11607
+eecccccccceeeeeeeecc722872eeeeeeee28e0eeeeeeeeeeeeeccccccceeeeeeeeccecceeceeeeeeeeee11cf1eeeeeeeeeee11cff11eeeeeeeeccceee611ee06
+eeeecccceeeeeeeeeeee2222eeeeeeeeeeee8eeeeeeeeeeeeeeecccceeeeeeeeeeeccecceeeeeeeeeeeeeecc11ee8800eeeeeecccc1ee880ee77eeee227eeeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeecc1688800eeeeeeeeec6e8880e882eeee2887eeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeec7872000eeeeeeeeec77820008772eeee268eeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee788200eeeeeeeeee788820e07882eeee288eeee
+eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee22000eeeeeeeeeee88200ee0888eeeee8eeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeeecceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeeeeccccceeeeeeeeeccccccceeeeeeeeec777ccceeeeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeeecccccccceeeeeeeccccc7ccceeeeeeecccc77ccceeeeeeeccccc7ccceeeeeeeccccc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eeccccc77ccceeeeeecccc777ccceeeeeecccc777ccceeeeeecccc777cc7eeeeeecccc777ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+ecccccc77ccceeeeec77ccc777cceeeeeccccc7777cceeeeecccccc77777eeeeecccccc777cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eccccccccccceeeee7777cc7cccceeeeeccccc77cccceeeeecccccc7c777eeeeecccccc7cccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+ccccccccccceeeee77ccc7ccccceeeeeccccc7ccccceeeeeccccc7c7777eeeeeccccc7ccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+ccccccccccceeeee7cccccccccceeeeeccccccccccceeeeeccccccccccceeeeeccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+cccccccccceeeeeecccccccccceeeeeecccccccccceeeeeecccccccccceeeeeeccccc7cc7ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+cccccccceeeeeeeeccccccccceeeeeeeccccccccceeeeeeeccccccccceeeeeeeccccc777ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eccccceeeeeeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeccccc77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
+eecceeeeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
@@ -68,54 +116,10 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeceeeeeeeeeeeeeeeceeeeeeeeeeeeeeeeceeeeeeeeeeeeeceeeeeeeeeeeeeeeceeeeeeeeeeeeeeeceeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeecccccceeeeeeeeeecccccceeeeeeeeeeecccccceeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeecccccccceeeeeeeecccccccceeeeeeeeecccccccceeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeccccc70cc0eeeeeeccccc70cc0e56ee0eccccc70cc0eeeeeccccc70cc0eeeeeeccccc70cc0eeeee0ccccc70cc0eeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeccffc7d07feeeeeeccffc7d07fe66e88eccffc7d07feeeeeccffc7d07feeeeeeccffc7d07fe77e62ccffc7d07feeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeecccfc777ffeeeeeecccfc777ff9eee77ecccfc777ff6eeeecccfcc77ffeeeeeecccfcc77ff777e26cccfcc77ffeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeececcccfff66eeeeececcccfff9eee788ececcccfff65eeeececcccff7eeeeeeececcccfffe777622ceccccfffeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeecccccccff6eeeeeecccccccffccc780eecccccccffeeeeeecccccc7770eeeeeecccccccfff11620ecccccccfe771eeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeecceccfccfeeeeeeecceccfccfceeeeeeecceccfccfeee82ecceccf9711eeeeeeccecccff1eeeeeeeccecccfc777e20eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeceecfcc7eee88eeeceecfeccceeeeeeeeceec7ecccce782eceecec9ee1eeeeeeceec9cccceeeeeeeceec66ff777620eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeefcc7787880eeeeeeefece1eeeeeeeeeee770c1ec7872eeeeeeccee77222eeeee66cceceeeeeeeeeee60c1cc6260eeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeef77088700eeeeeee77eee1eeeeeeeeeee777e1e7882eeeeeeecece2270eeeeee66ceeceeeeeeeeeeeee1ee6220eeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeee7777080eeeeeeeee707e66eeeeeeeeeeee7611ee00eeeeeeeee7c708eeeeeeeeecee77eeeeeeeeeeee11eee00eeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeee772662622eeeeeee7e222eeeeeeeeeeee226eeeeeeeeeeeeee777880eeeeeeeeee888eeeeeeeeee0717eeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeee000060eeeeeeeeee066eeeeeeeeeeee66eeeeeeeeeeeeeee87880eeeeeeeeeee277eeeeeeeeee02787eeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeeeeeeeeeeeeeeeeee2220eeeeeeeeeeee022eeeeeeeeeee02eeeeeeeeeeeeeee0220eeeeeeeeeeeee288eeeeeeeeee02878eeeeeeeeeeeeeeeeeeeeee
-eeeecccccceeeeeeeeeccccccceeeeeeeeeecccccceeeeeeeeeccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeecccc70cc0eeeeecccccc70cc0eeeeeeecccc70cc0eeeeecccccc70cc0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeccffc7d07feeeeccccffc7d07feeeeeeccffc7d07feeeeccccffc7d07feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-ecccfcc777ffeeeeeeccfcc777ffeeeeecccfcc777ffeeeeeeccfcc777ffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-ececccccfff220eeeeecccccfffeeeeeececccccff22e20eeeecccccfffeee2eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeccccfffee260eeecccccfffeeee2eeeeccccff22ee800eecccccfffeeeee2eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeccccccc66e222eeccccc8cc66e2e2eeeccccccc66ee780ecccccccc66ee2e2eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eecceccc766e6200ccccc88c766ee22eeecceccc766ee870cccccccc766eee22eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeceeec777f11680cceee88777fee22eeeceecc777fee787cceeecc777feee22eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeecf77fce6880eeee88877fcee2eeeeeeecf77fccc788eeeeccf7fcceee22eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeefccceeee88eeeee88cc11ee22eeeeeeef11cceeee88eeeecf11cceee22eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeee7cceeeeeeeeee288cee1e222eeeeeeeee1eeeeeeeeeeee211eec7ee88eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeee777eeeee2eeeee22cce71772eeeeeeee711eeee2eeeeee2281ee778780eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeee087872e22eeeeeee2cee8722eeeeeeee287e2ee22eeeeeee221ee08870eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeee08888722e2eeeeee2eee822eeeeeeeee88822222eeeeeeee22eeee080eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeee00870822eeeeeeee2ee22eeeeeeeeee0888222eeeeeeeeee222eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeeecceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeeeeccccceeeeeeeeeccccccceeeeeeeeec777ccceeeeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeeecccccccceeeeeeeccccc7ccceeeeeeecccc77ccceeeeeeeccccc7ccceeeeeeeccccc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eeccccc77ccceeeeeecccc777ccceeeeeecccc777ccceeeeeecccc777cc7eeeeeecccc777ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-ecccccc77ccceeeeec77ccc777cceeeeeccccc7777cceeeeecccccc77777eeeeecccccc777cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eccccccccccceeeee7777cc7cccceeeeeccccc77cccceeeeecccccc7c777eeeeecccccc7cccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-ccccccccccceeeee77ccc7ccccceeeeeccccc7ccccceeeeeccccc7c7777eeeeeccccc7ccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-ccccccccccceeeee7cccccccccceeeeeccccccccccceeeeeccccccccccceeeeeccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-cccccccccceeeeeecccccccccceeeeeecccccccccceeeeeecccccccccceeeeeeccccc7cc7ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-cccccccceeeeeeeeccccccccceeeeeeeccccccccceeeeeeeccccccccceeeeeeeccccc777ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eccccceeeeeeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeccccccceeeeeeeeeccccc77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-eecceeeeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeecccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
@@ -132,6 +136,3 @@ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-__gff__
-0000000000000000000000000000000000000000000000000000000000000000000000030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
diff --git a/data/pico8_version.txt b/data/pico8_version.txt
new file mode 100644
index 00000000..c1c1f168
--- /dev/null
+++ b/data/pico8_version.txt
@@ -0,0 +1 @@
+0.2.2c
diff --git a/data/version.txt b/data/version.txt
index 37c2d996..e0ea36fe 100644
--- a/data/version.txt
+++ b/data/version.txt
@@ -1 +1 @@
-5.4
+6.0
diff --git a/export_and_patch_cartridge_release.sh b/export_and_patch_cartridge_release.sh
index 75ab1613..cf517b92 100755
--- a/export_and_patch_cartridge_release.sh
+++ b/export_and_patch_cartridge_release.sh
@@ -9,14 +9,15 @@
# Configuration: paths
picoboots_scripts_path="$(dirname "$0")/pico-boots/scripts"
+picoboots_plates_path="$(dirname "$0")/pico-boots/plates"
game_scripts_path="$(dirname "$0")"
data_path="$(dirname "$0")/data"
-local_plates_path="$(dirname "$0")/plates"
# Linux only
carts_dirpath="$HOME/.lexaloffle/pico-8/carts"
config_plates_dirpath="$HOME/.lexaloffle/pico-8/plates"
# Configuration: cartridge
+pico8_version=`cat "$data_path/pico8_version.txt"`
version=`cat "$data_path/version.txt"`
export_folder="$carts_dirpath/picosonic/v${version}_release"
cartridge_basename="picosonic_v${version}_release"
@@ -48,8 +49,9 @@ rm -rf "${p8_folder}/"*
# continuing to show old folder in system bin. Make sure to place blob * outside ""
rm -rf "${png_folder}/"*
-# Cleanup bin folder as a bug in PICO-8 makes it accumulate files in .zip for each export (even homonymous files!)
-# and we want to remove any extraneous files too
+# Cleanup bin folder as we want to remove any extraneous files too
+# Note: PICO-8 used to accumulate files in .zip for each export (even homonymous files!),
+# making this mandatory, but this was fixed in 0.2.2. We still decided to keep the line to be clean.
rm -rf "${bin_folder}/"*
# p8 cartridges can be distributed as such, so just copy them to the folder to zip later
@@ -67,9 +69,9 @@ if [[ $? -ne 0 ]]; then
exit 1
fi
-# Copy custom template to PICO-8 config plates folder as "picosonic_template.html"
+# Copy custom template to PICO-8 config plates folder as "${cartridge_basename}_template.html"
# (just to avoid conflicts with other games)
-cp "${local_plates_path}/custom_template.html" "${config_plates_dirpath}/picosonic_template.html"
+cp "${picoboots_plates_path}/custom_template.html" "${config_plates_dirpath}/${cartridge_basename}_template.html"
# Export via PICO-8 editor: PNG cartridges, binaries, HTML
pico8 -x "$game_scripts_path/export_game_release.p8"
@@ -94,7 +96,7 @@ if [[ ! -d "$bin_folder" || ! $(ls -A "$bin_folder") ]]; then
fi
# Patch the runtime binaries in-place with 4x_token, fast_reload, fast_load (experimental) if available
-patch_bin_cmd="\"$picoboots_scripts_path/patch_pico8_runtime.sh\" --inplace \"$bin_folder\" \"$cartridge_basename\""
+patch_bin_cmd="\"$picoboots_scripts_path/patch_pico8_runtime.sh\" --inplace \"$pico8_version\" \"$bin_folder\" \"$cartridge_basename\""
echo "> $patch_bin_cmd"
bash -c "$patch_bin_cmd"
diff --git a/export_game_release.p8 b/export_game_release.p8
index b7b28413..f1d6fea7 100644
--- a/export_game_release.p8
+++ b/export_game_release.p8
@@ -11,7 +11,7 @@ __lua__
-- #version
-- PICO-8 cannot read data/version.txt, so exceptionally set the version manually here
-local version = "5.4"
+local version = "6.0"
local export_folder = "picosonic/v"..version.."_release"
local game_basename = "picosonic_v"..version.."_release"
local rel_png_folder = game_basename.."_png_cartridges"
@@ -20,7 +20,10 @@ cd(export_folder)
local entry_cartridge = "picosonic_titlemenu.p8"
+ -- #cartridge (tagged to easily find what code to change when adding a new cartridge,
+ -- and because this script cannot access external files like cartridges.txt)
local additional_main_cartridges_list = {
+ "picosonic_attract_mode.p8",
"picosonic_stage_intro.p8",
"picosonic_ingame.p8",
"picosonic_stage_clear.p8",
@@ -33,10 +36,10 @@ cd(export_folder)
end
local data_cartridges_list = {
- "data_bgm1.p8",
"data_stage1_00.p8", "data_stage1_10.p8", "data_stage1_20.p8", "data_stage1_30.p8",
"data_stage1_01.p8", "data_stage1_11.p8", "data_stage1_21.p8", "data_stage1_31.p8",
- "data_stage1_runtime.p8"
+ "data_stage1_intro.p8", "data_stage1_ingame.p8",
+ "data_stage_sonic.p8"
}
-- PNG
diff --git a/install_all_cartridges.sh b/install_all_cartridges.sh
index d4086de3..8698544e 100755
--- a/install_all_cartridges.sh
+++ b/install_all_cartridges.sh
@@ -4,49 +4,56 @@
# This is required if you need to play with multiple carts,
# as other carts will only be loaded in PICO-8 carts location
-# Usage: install_all_cartridges.sh config [png]
+# Usage: install_all_cartridges.sh config [--itest]
# config build config (e.g. 'debug' or 'release')
-# png if passed, the .png cartridges are installed
+# -i, --itest pass this option to build an itest instead of a normal game cartridge
# Currently only supported on Linux
-# png option is legacy for p8tool. It works in theory but in practice,
-# since p8tool fails to build .p8.png properly, png will be directly
-# saved from PICO-8 with export_game_release.p8 into PICO-8 carts folder
-
# Configuration: paths
game_scripts_path="$(dirname "$0")"
data_path="$(dirname "$0")/data"
# check that source and output paths have been provided
-if ! [[ $# -ge 1 && $# -le 2 ]] ; then
- echo "build.sh takes 1 or 2 params, provided $#:
+if ! [[ $# -ge 1 && $# -le 3 ]] ; then
+ echo "install_all_cartridges.sh takes 1 or 2 params + option value, provided $#:
\$1: config ('debug', 'release', etc.)
- \$2: optional suffix ('png' for .png cartridge install)"
+ -i, --itest: Pass this option to build an itest instead of a normal game cartridge."
exit 1
fi
# Configuration: cartridge
+cartridge_stem="picosonic"
version=`cat "$data_path/version.txt"`
config="$1"; shift
+# ! This is a short version for the usual while-case syntax, but in counterpart
+# ! it doesn't support reordering (--itest must be after config)
+if [[ $1 == '-i' || $1 == '--itest' ]]; then
+ itest=true
+ shift
+fi
-# option "png" will export the png cartridge
-if [[ $1 = "png" ]] ; then
- suffix=".png"
+if [[ "$itest" == true ]]; then
+ # itest cartridges enforce special config 'itest' and ignore passed config
+ config='itest'
+ options='--itest'
else
- suffix=""
+ options=''
fi
-cartridge_list="titlemenu stage_intro ingame stage_clear"
+# cartridges.txt lists cartridge names, one line per cartridge
+# newlines act like separators for iteration just like spaces,
+# so this is equivalent to `cartridge_list="titlemenu stage_intro ..."`
+cartridge_list=`cat "$data_path/cartridges.txt"`
for cartridge in $cartridge_list; do
- "$game_scripts_path/install_single_cartridge.sh" "$cartridge" "$config" "$suffix"
+ "$game_scripts_path/install_single_cartridge.sh" "$cartridge" "$config" $options
done
# recompute same install dirpath as used in install_single_cartridge.sh
# (no need to mkdir -p "${install_dirpath}", it must have been created in said script)
carts_dirpath="$HOME/.lexaloffle/pico-8/carts"
-install_dirpath="${carts_dirpath}/picosonic/v${version}_${config}"
+install_dirpath="${carts_dirpath}/${cartridge_stem}/v${version}_${config}"
# Also copy data cartridges
echo "Copying data cartridges data/data_*.p8 in ${install_dirpath} ..."
diff --git a/install_single_cartridge.sh b/install_single_cartridge.sh
index 34b41e96..9d47ccd7 100755
--- a/install_single_cartridge.sh
+++ b/install_single_cartridge.sh
@@ -7,26 +7,22 @@
# make sure to use install_single_cartridge_with_data.sh or
# to manually copy data cartridges after this.
-# Usage: install_single_cartridge.sh cartridge_suffix config [png]
-# cartridge_suffix 'titlemenu', 'stage_intro', 'ingame' or 'stage_clear'
+# Usage: install_single_cartridge.sh cartridge_suffix config [--itest]
+# cartridge_suffix see data/cartridges.txt for the list of cartridge names
# config build config (e.g. 'debug' or 'release'. Default: 'debug')
-# png if passed, the .png cartridge is installed
+# -i, --itest pass this option to build an itest instead of a normal game cartridge
# Currently only supported on Linux
-# png option is legacy for p8tool. It works in theory but in practice,
-# since p8tool fails to build .p8.png properly, png will be directly
-# saved from PICO-8 with export_game_release.p8 into PICO-8 carts folder
-
# Configuration: paths
data_path="$(dirname "$0")/data"
# check that source and output paths have been provided
-if ! [[ $# -ge 1 && $# -le 3 ]] ; then
- echo "build.sh takes 1 to 2 params, provided $#:
- \$1: cartridge_suffix ('titlemenu', 'stage_intro', 'ingame' or 'stage_clear')
+if ! [[ $# -ge 1 && $# -le 4 ]] ; then
+ echo "install_single_cartridge.sh takes 1 to 3 params + option value, provided $#:
+ \$1: cartridge_suffix (see data/cartridges.txt for the list of cartridge names)
\$2: config ('debug', 'release', etc. Default: 'debug')
- \$3: optional suffix ('png' for .png cartridge install)"
+ -i, --itest: Pass this option to build an itest instead of a normal game cartridge."
exit 1
fi
@@ -35,16 +31,23 @@ cartridge_stem="picosonic"
version=`cat "$data_path/version.txt"`
cartridge_suffix="$1"; shift
config="$1"; shift
+# ! This is a short version for the usual while-case syntax, but in counterpart
+# ! it doesn't support reordering (--itest must be after config)
+if [[ $1 == '-i' || $1 == '--itest' ]]; then
+ itest=true
+ shift
+fi
-# option "png" will export the png cartridge
-if [[ $1 = "png" ]] ; then
- suffix=".png"
+if [[ "$itest" == true ]]; then
+ # itest cartridges enforce special config 'itest' and ignore passed config
+ config='itest'
+ cartridge_extra_suffix='itest_all_'
else
- suffix=""
+ cartridge_extra_suffix=''
fi
output_path="build/v${version}_${config}"
-cartridge_filepath="${output_path}/${cartridge_stem}_${cartridge_suffix}.p8${suffix}"
+cartridge_filepath="${output_path}/${cartridge_stem}_${cartridge_extra_suffix}${cartridge_suffix}.p8${suffix}"
# Linux only
carts_dirpath="$HOME/.lexaloffle/pico-8/carts"
install_dirpath="${carts_dirpath}/${cartridge_stem}/v${version}_${config}"
diff --git a/install_single_cartridge_with_data.sh b/install_single_cartridge_with_data.sh
index 7e716d22..da6ff3e7 100755
--- a/install_single_cartridge_with_data.sh
+++ b/install_single_cartridge_with_data.sh
@@ -4,27 +4,24 @@
# This is required if you changed data or build for the first time for a given config,
# as install_single_cartridge.sh will not copy data along and is not reliable alone.
-# Usage: install_single_cartridge_with_data.sh config [png]
-# cartridge_suffix 'titlemenu', 'stage_intro', 'ingame' or 'stage_clear'
+# Usage: install_single_cartridge_with_data.sh config [--itest]
+# cartridge_suffix see data/cartridges.txt for the list of cartridge names
# config build config (e.g. 'debug' or 'release'. Default: 'debug')
-# png if passed, the .png cartridge is installed
+# -i, --itest pass this option to build an itest instead of a normal game cartridge
# Currently only supported on Linux
-# png option is legacy for p8tool. It works in theory but in practice,
-# since p8tool fails to build .p8.png properly, png will be directly
-# saved from PICO-8 with export_game_release.p8 into PICO-8 carts folder
-
# Configuration: paths
game_scripts_path="$(dirname "$0")"
data_path="$(dirname "$0")/data"
# check that source and output paths have been provided
if ! [[ $# -ge 1 && $# -le 3 ]] ; then
- echo "build.sh takes 1 or 2 params, provided $#:
- \$1: cartridge_suffix ('titlemenu', 'stage_intro', 'ingame' or 'stage_clear')
+ echo "install_single_cartridge_with_data.sh takes 1 or 3 params + option value, provided $#:
+ \$1: cartridge_suffix (see data/cartridges.txt for the list of cartridge names)
\$2: config ('debug', 'release', etc. Default: 'debug')
- \$3: optional suffix ('png' for .png cartridge install)"
+ -i, --itest: Pass this option to build an itest instead of a normal game cartridge."
+
exit 1
fi
@@ -32,18 +29,25 @@ fi
version=`cat "$data_path/version.txt"`
cartridge_suffix="$1"; shift
config="$1"; shift
+# ! This is a short version for the usual while-case syntax, but in counterpart
+# ! it doesn't support reordering (--itest must be after config)
+if [[ $1 == '-i' || $1 == '--itest' ]]; then
+ itest=true
+ shift
+fi
-# option "png" will export the png cartridge
-if [[ $1 = "png" ]] ; then
- suffix=".png"
+if [[ "$itest" == true ]]; then
+ # itest cartridges enforce special config 'itest' and ignore passed config
+ config='itest'
+ options='--itest'
else
- suffix=""
+ options=''
fi
# note that we don't add the data/data_stage*.p8 cartridges because
# install_single_cartridge.sh for ingame will install all data cartridges anyway
# (and said script is really meant for built cartridges as it refers to build path)
-"$game_scripts_path/install_single_cartridge.sh" "$cartridge_suffix" "$config" "$suffix"
+"$game_scripts_path/install_single_cartridge.sh" "$cartridge_suffix" "$config" $options
# recompute same install dirpath as used in install_single_cartridge.sh
# (no need to mkdir -p "${install_dirpath}", it must have been created in said script)
diff --git a/pico-boots b/pico-boots
index 748926bd..2d33cb11 160000
--- a/pico-boots
+++ b/pico-boots
@@ -1 +1 @@
-Subproject commit 748926bde24bb64c2cd4be1a363524db36ac2062
+Subproject commit 2d33cb111c3c7908bf0852709f7fb59f447d0145
diff --git a/plates/custom_template.html b/plates/custom_template.html
deleted file mode 100644
index dfd9caa4..00000000
--- a/plates/custom_template.html
+++ /dev/null
@@ -1,848 +0,0 @@
-
-PICO-8 Cartridge
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-  
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/prebuild/game_substitute_table.py b/prebuild/game_substitute_table.py
new file mode 100644
index 00000000..901e57dc
--- /dev/null
+++ b/prebuild/game_substitute_table.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3.6
+# -*- coding: utf-8 -*-
+
+# table of symbol substitutes specific to the game
+# complements the generic engine symbol substitute table in replace_strings.py
+# format: { namespace1: {name1: substitute1, name2: substitute2, ...}, ... }
+GAME_SYMBOL_SUBSTITUTE_TABLE = {
+
+ # data
+
+ 'sprite_flags': {
+ 'collision': 0,
+ 'oneway': 1,
+ 'ignore_loop_layer': 2,
+ # 'unused3': 3,
+ 'waterfall': 4,
+ 'spring': 5,
+ 'midground': 6,
+ 'foreground': 7,
+ },
+
+ 'sprite_masks': {
+ 'collision': 1,
+ 'oneway': 2,
+ 'ignore_loop_layer': 4,
+ # 'unused3': 8,
+ 'waterfall': 16,
+ 'spring': 32,
+ 'midground': 64,
+ 'foreground': 128,
+ },
+
+ # playercharacter
+
+ 'control_modes': {
+ 'human': 1,
+ 'puppet': 2,
+ 'ai': 3,
+ },
+
+ 'motion_modes': {
+ 'platformer': 1,
+ 'debug': 2,
+ },
+
+ 'motion_states': {
+ 'standing': 1,
+ 'falling': 2,
+ 'air_spin': 3,
+ 'rolling': 4,
+ 'crouching': 5,
+ 'spin_dashing': 6,
+ },
+
+ # itest_dsl
+
+ 'parsable_types': {
+ 'none': 1,
+ 'number': 2,
+ 'vector': 3,
+ 'horizontal_dir': 4,
+ 'control_mode': 5,
+ 'motion_mode': 6,
+ 'motion_state': 7,
+ 'button_id': 8,
+ 'gp_value': 9,
+ },
+
+ 'command_types': {
+ 'warp': 1,
+ 'set': 2,
+ 'set_control_mode': 3,
+ 'set_motion_mode': 4,
+ 'move': 5,
+ 'stop': 6,
+ 'jump': 7,
+ 'stop_jump': 8,
+ 'crouch': 9,
+ 'stop_crouch': 10,
+ 'press': 11,
+ 'release': 12,
+ 'wait': 13,
+ 'expect': 14,
+ },
+
+ 'gp_value_types': {
+ 'pc_bottom_pos': 1,
+ 'pc_velocity': 2,
+ 'pc_velocity_y': 3,
+ 'pc_ground_spd': 4,
+ 'pc_motion_state': 5,
+ 'pc_slope': 6,
+ },
+
+ # memory
+
+ 'memory': {
+ # interpreted as 24063 by Python, can reduce compressed chars slightly when passed
+ # as number instead of string
+ 'picked_emerald_address': 0x5dff,
+ }
+}
+
+
+# table of constants specific to the game
+# complements the generic engine arg substitute table in replace_strings.py
+# format: {name1: value1, name2: value2, ...}
+GAME_CONSTANT_SUBSTITUTE_TABLE = {
+ # note that we are confident we'll ONLY using first as a priority expression,
+ # never in higher priority operations like (contrived example) `true ^ screen_width / 2`
+ 'screen_width / 2': 64,
+ 'screen_height / 2': 64,
+}
diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py
deleted file mode 100755
index 79d56c98..00000000
--- a/prebuild/replace_strings.py
+++ /dev/null
@@ -1,304 +0,0 @@
-#!/usr/bin/env python3.6
-# -*- coding: utf-8 -*-
-
-# LEGACY: use preprocess.py in pico-boots now
-
-import argparse
-import logging
-import os
-import re
-
-# This script replace glyph identifiers, some functions and symbols in general, and arg substitutes ($arg)
-# with the corresponding unicode characters and substitute symbol names.
-# Set the glyphs and symbols to replace in GLYPH_TABLE and SYMBOL_SUBSTITUTE_TABLE.
-
-# input glyphs
-# (when using input functions (btn, btnp), prefer enum input.button_ids)
-GLYPH_UP = '⬆️'
-GLYPH_DOWN = '⬇️'
-GLYPH_LEFT = '⬅️'
-GLYPH_RIGHT = '➡️'
-GLYPH_X = '❎'
-GLYPH_O = '🅾️'
-
-# prefix of all glyph identifiers
-GLYPH_PREFIX = '##'
-
-# dict mapping an ascii glyph identifier suffix with a unicode glyph
-GLYPH_TABLE = {
- 'u': GLYPH_UP,
- 'd': GLYPH_DOWN,
- 'l': GLYPH_LEFT,
- 'r': GLYPH_RIGHT,
- 'x': GLYPH_X,
- 'o': GLYPH_O,
-}
-
-# functions and enum constants to substitute
-# enums are only substituted for token/char limit reasons
-# format: { namespace1: {name1: substitute1, name 2: substitute2, ...}, ... }
-SYMBOL_SUBSTITUTE_TABLE = {
- # Functions
-
- # api.print is useful for tests using native print, but in runtime, just use print
- 'api': {
- 'print': 'print'
- },
-
- # Enums
-
- # for every enum added here, surround enum definition with --#ifn pico8
- # to strip it from the build, unless you need to map the enum string
- # to its value dynamically with enum_values[dynamic_string]
- # remember to update the values of any preprocessed enum modified
-
- # TODO: to avoid issues like enum values mismatch, parse the values
- # directly from the .lua instead of writing them manually
-
- # color
- 'colors': {
- 'black': 0,
- 'dark_blue': 1,
- 'dark_purple': 2,
- 'dark_green': 3,
- 'brown': 4,
- 'dark_gray': 5,
- 'light_gray': 6,
- 'white': 7,
- 'red': 8,
- 'orange': 9,
- 'yellow': 10,
- 'green': 11,
- 'blue': 12,
- 'indigo': 13,
- 'pink': 14,
- 'peach': 15,
- },
-
- # math
- 'directions': {
- 'left': 0,
- 'up': 1,
- 'right': 2,
- 'down': 3,
- },
-
- 'horizontal_dirs': {
- 'left': 1,
- 'right': 2,
- },
-
- # input
- 'button_ids': {
- 'left': 0,
- 'right': 1,
- 'up': 2,
- 'down': 3,
- 'o': 4,
- 'x': 5,
- },
-
- 'btn_states': {
- 'released': 0,
- 'just_pressed': 1,
- 'pressed': 2,
- 'just_released': 3,
- },
-
- 'input_modes': {
- 'native': 0,
- 'simulated': 1,
- },
-
- # playercharacter
- 'control_modes': {
- 'human': 1,
- 'ai': 2,
- 'puppet': 3,
- },
-
- 'motion_modes': {
- 'platformer': 1,
- 'debug': 2,
- },
-
- 'motion_states': {
- 'grounded': 1,
- 'airborne': 2,
- },
-
- # itest_dsl
- 'parsable_types': {
- 'none': 1,
- 'number': 2,
- 'vector': 3,
- 'horizontal_dir': 4,
- 'control_mode': 5,
- 'motion_mode': 6,
- 'motion_state': 7,
- 'button_id': 8,
- 'gp_value': 9,
- },
-
- 'command_types': {
- 'warp': 1,
- 'set': 2,
- 'set_control_mode': 3,
- 'set_motion_mode': 4,
- 'move': 5,
- 'stop': 6,
- 'jump': 7,
- 'stop_jump': 8,
- 'press': 9,
- 'release': 10,
- 'wait': 11,
- 'expect': 12,
- },
-
- 'gp_value_types': {
- 'pc_bottom_pos': 1,
- 'pc_velocity': 2,
- 'pc_ground_spd': 3,
- 'pc_motion_state': 4,
- 'pc_slope': 5,
- },
-}
-
-# prefix of all arg identifiers
-ARG_PREFIX = '$'
-
-# default arg substitutes
-DEFAULT_ARG_SUBSTITUTES = {
- "titlemenu_ver": "",
- "credits_ver": "",
- "stage_ver": "",
-}
-
-def replace_all_strings_in_dir(dirpath, arg_substitutes_table):
- """
- Replace all the glyph identifiers, symbols and arg substitutes in all source files in a given directory
-
- """
- for root, dirs, files in os.walk(dirpath):
- for file in files:
- if file.endswith(".lua"):
- replace_all_strings_in_file(os.path.join(root, file), arg_substitutes_table)
-
-
-def replace_all_strings_in_file(filepath, arg_substitutes_table):
- """
- Replace all the glyph identifiers, symbols and arg substitutes in a given file
-
- test.txt:
- require('itest_$itest')
- ##d or ##u
- and ##x
- api.print("press ##x")
-
- >>> replace_all_glyphs_in_file('test.txt', {'itest': 'character'})
-
- test.txt:
- require('itest_character')
- ⬇️ or ⬆️
- and ❎
- print("press ❎")
-
- """
- # make sure to open files as utf-8 so we can handle glyphs on any platform
- # (when locale.getpreferredencoding() and sys.getfilesystemencoding() are not "UTF-8" and "utf-8")
- # you can also set PYTHONIOENCODING="UTF-8" to visualize glyphs when debugging if needed
- with open(filepath, 'r+', encoding='utf-8') as f:
- data = f.read()
- data = replace_all_glyphs_in_string(data)
- data = replace_all_symbols_in_string(data)
- data = replace_all_args_in_string(data, arg_substitutes_table)
- # replace file content (truncate as the new content may be shorter)
- f.seek(0)
- f.truncate()
- f.write(data)
-
-def replace_all_glyphs_in_string(text):
- """
- Replace the glyph identifiers of a certain type with the corresponding glyph
-
- >>> replace_all_glyphs_in_string("##d and ##x ##d")
- '⬇️ and ❎ ⬇️'
-
- """
- for identifier_char, glyph in GLYPH_TABLE.items():
- text = text.replace(GLYPH_PREFIX + identifier_char, glyph)
- return text
-
-def generate_get_substitute_from_dict(substitutes):
- def get_substitute(match):
- member = match.group(1) # "{member}"
- if member in substitutes:
- return str(substitutes[member]) # enums are substituted with integers, so convert
- else:
- original_symbol = match.group(0) # "{namespace}.{member}"
- # in general, we should substitute all members of a namespace, especially enums
- logging.error(f'no substitute defined for {original_symbol}, but the namespace (first part) is present in SYMBOL_SUBSTITUTE_TABLE')
- # return something easy to debug in PICO-8, in case the user missed the error message
- # note that we should normally escape quotes in original_symbol, but we rely on the fact that
- # symbols should not contain quotes
- return f'assert(false, "UNSUBSTITUTED {original_symbol}")'
- return get_substitute
-
-def replace_all_symbols_in_string(text):
- """
- Replace symbols "namespace.member" defined in SYMBOL_SUBSTITUTE_TABLE
- with the corresponding substitutes
- Convert integer to string for replacement to support enum constants
-
- >>> replace_all_symbols_in_string("api.print(\"hello\")")
- 'print("hello")'
-
- """
- for namespace, substitutes in SYMBOL_SUBSTITUTE_TABLE.items():
- SYMBOL_PATTERN = re.compile(rf"{namespace}\.(\w+)")
- text = SYMBOL_PATTERN.sub(generate_get_substitute_from_dict(substitutes), text)
- return text
-
-
-def replace_all_args_in_string(text, arg_substitutes_table):
- """
- Replace args with the corresponding substitutes.
- Use DEFAULT_ARG_SUBSTITUTES if arg_substitutes_table is not overriding a key.
-
- >>> replace_all_args_in_string("require('itest_$itest')", {"itest": "character"})
- 'require("itest_character")'
-
- """
- full_arg_substitutes_table = {**DEFAULT_ARG_SUBSTITUTES, **arg_substitutes_table}
- for arg, substitute in full_arg_substitutes_table.items():
- text = text.replace(ARG_PREFIX + arg, substitute)
- return text
-
-
-def parse_arg_substitutes(arg_substitutes):
- """Parse a list of arg substitutes in the format 'arg1=substitute1 arg2=substitute2 ...' into a dictionary of {arg: substitute}"""
- arg_substitutes_table = {}
- for arg_definition in arg_substitutes:
- # arg_definition should have format 'arg1=substitute1'
- members = arg_definition.split("=")
- if len(members) == 2:
- arg, substitute = arg_definition.split("=")
- # we do not support surrounding quotes which would be integrated in the names, so don't use names with spaces
- arg_substitutes_table[arg] = substitute
- else:
- raise ValueError(f"arg_substitutes contain definition with not exactly 2 '=' signs: {arg_definition.split}")
- return arg_substitutes_table
-
-if __name__ == '__main__':
- import sys
- parser = argparse.ArgumentParser(description='Replace predetermined strings in all source files in a directory.')
- parser.add_argument('dirpath', type=str, help='path containing source files where strings should be replaced')
- parser.add_argument('--substitutes', type=str, nargs='*', default=[],
- help='extra substitutes table in the format "arg1=substitute1 arg2=substitute2 ...". \
- Does not support spaces in names because surrounding quotes would be part of the names')
- args = parser.parse_args()
-
- logging.basicConfig(level=logging.INFO)
- arg_substitutes_table = parse_arg_substitutes(args.substitutes)
- replace_all_strings_in_dir(args.dirpath, arg_substitutes_table)
- print(f"Replaced all strings in all files in {args.dirpath} with substitutes: {arg_substitutes_table}.")
diff --git a/prebuild/test_replace_strings.py b/prebuild/test_replace_strings.py
deleted file mode 100644
index ffb5edd2..00000000
--- a/prebuild/test_replace_strings.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# LEGACY: use test_preprocess.py to test preprocess.py in pico-boots now
-
-import unittest
-from . import replace_strings
-
-import logging
-from os import path
-import shutil, tempfile
-
-
-class TestParsing(unittest.TestCase):
-
- def test_parse_arg_substitutes(self):
- test_arg_substitutes = ['itest=character', 'optimization=3']
- self.assertEqual(replace_strings.parse_arg_substitutes(test_arg_substitutes), {'itest': 'character', 'optimization': '3'})
-
- def test_parse_arg_substitutes_parsing_error(self):
- test_arg_substitutes = ['itest character']
- self.assertRaises(ValueError, replace_strings.parse_arg_substitutes, test_arg_substitutes)
-
-
-class TestReplaceStrings(unittest.TestCase):
-
- def test_replace_all_glyphs_in_string(self):
- test_string = '##d and ##x ##d'
- self.assertEqual(replace_strings.replace_all_glyphs_in_string(test_string), '⬇️ and ❎ ⬇️')
-
- def test_replace_all_symbols_in_string_function(self):
- test_string = 'api.print("hello")'
- self.assertEqual(replace_strings.replace_all_symbols_in_string(test_string), 'print("hello")')
-
- def test_replace_all_symbols_in_string_enum(self):
- test_string = 'local c = colors.dark_purple'
- self.assertEqual(replace_strings.replace_all_symbols_in_string(test_string), 'local c = 2')
-
- def test_replace_all_symbols_in_string_missing_member(self):
- test_string = 'local c = colors.unknown'
- # this will trigger a warning, hide by setting logging level to ERROR in main
- self.assertEqual(replace_strings.replace_all_symbols_in_string(test_string), 'local c = assert(false, "UNSUBSTITUTED colors.unknown")')
-
- def test_replace_all_args_in_string(self):
- test_string = 'require("itest_$itest")'
- self.assertEqual(replace_strings.replace_all_args_in_string(test_string, {'itest': 'character'}), 'require("itest_character")')
-
- def test_replace_default_args_in_string(self):
- test_string = 'require("titlemenu$titlemenu_ver")'
- self.assertEqual(replace_strings.replace_all_args_in_string(test_string, {}), 'require("titlemenu")')
-
- def test_replace_default_args_overridden_in_string(self):
- test_string = 'require("titlemenu$titlemenu_ver")'
- self.assertEqual(replace_strings.replace_all_args_in_string(test_string, {'titlemenu_ver': '_dummy'}), 'require("titlemenu_dummy")')
-
-
-class TestReplaceStringsInFile(unittest.TestCase):
-
- def setUp(self):
- # Create a temporary directory
- self.test_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- # Remove the directory after the test
- shutil.rmtree(self.test_dir)
-
- def test_replace_strings(self):
- """^ Test replacing strings in a whole file, with substitutes being shorter or longer than original symbol to test if file is truncated"""
- test_filepath = path.join(self.test_dir, 'test.lua')
- with open(test_filepath, 'w', encoding='utf-8') as f:
- f.write('require("itest_$itest")\nrequire("$symbol_is_much_longer")\n##d or ##u\nand ##x\napi.print("press ##x")')
- replace_strings.replace_all_strings_in_file(test_filepath, {'itest': 'character', 'symbol_is_much_longer': 'short'})
- with open(test_filepath, 'r', encoding='utf-8') as f:
- self.assertEqual(f.read(), 'require("itest_character")\nrequire("short")\n⬇️ or ⬆️\nand ❎\nprint("press ❎")')
-
-class TestReplaceStringsInDir(unittest.TestCase):
-
- def setUp(self):
- # Create a temporary directory
- self.test_dir = tempfile.mkdtemp()
-
- def tearDown(self):
- # Remove the directory after the test
- shutil.rmtree(self.test_dir)
-
- def test_replace_all_strings_in_dir(self):
- test_filepath1 = path.join(self.test_dir, 'test1.lua')
- with open(test_filepath1, 'w', encoding='utf-8') as f:
- f.write('require("itest_$itest")\n##d or ##u\nand ##x\napi.print("press ##x")')
- test_filepath2 = path.join(self.test_dir, 'test2.lua')
- with open(test_filepath2, 'w', encoding='utf-8') as f:
- f.write('require("itest_$itest")\n##l or ##r\nand ##o\napi.print("press ##o")')
- replace_strings.replace_all_strings_in_dir(self.test_dir, {'itest': 'character'})
- with open(test_filepath1, 'r', encoding='utf-8') as f:
- self.assertEqual(f.read(), 'require("itest_character")\n⬇️ or ⬆️\nand ❎\nprint("press ❎")')
- with open(test_filepath2, 'r', encoding='utf-8') as f:
- self.assertEqual(f.read(), 'require("itest_character")\n⬅️ or ➡️\nand 🅾️\nprint("press 🅾️")')
-
-if __name__ == '__main__':
- logging.basicConfig(level=logging.CRITICAL)
- unittest.main()
diff --git a/run_cartridge.sh b/run_cartridge.sh
index 14d03216..d603d706 100755
--- a/run_cartridge.sh
+++ b/run_cartridge.sh
@@ -3,7 +3,7 @@
# Run game cartridge located in PICO-8 carts install folder with PICO-8 executable
# Must be called after build and install script for that cartridge suffix.
# Usage: run_game.sh cartridge_suffix config [extra]
-# cartridge_suffix 'titlemenu', 'stage_intro', 'ingame' or 'stage_clear'
+# cartridge_suffix see data/cartridges.txt for the list of cartridge names
# config build config (e.g. 'debug' or 'release')
# Any extra arguments are passed to pico8
@@ -19,6 +19,20 @@ version=`cat "$data_path/version.txt"`
# shift allows to pass extra arguments as $@
cartridge_suffix="$1"; shift
config="$1"; shift
+# ! This is a short version for the usual while-case syntax, but in counterpart
+# ! it doesn't support reordering (--itest must be after config)
+if [[ $1 == '-i' || $1 == '--itest' ]]; then
+ itest=true
+ shift
+fi
+
+if [[ "$itest" == true ]]; then
+ # itest cartridges enforce special config 'itest' and ignore passed config
+ config='itest'
+ cartridge_extra_suffix='itest_all_'
+else
+ cartridge_extra_suffix=''
+fi
carts_dirpath="$HOME/.lexaloffle/pico-8/carts"
install_dirpath="${carts_dirpath}/picosonic/v${version}_${config}"
@@ -27,7 +41,7 @@ install_dirpath="${carts_dirpath}/picosonic/v${version}_${config}"
# because load() paths may be relative (in our case, inside picosonic/vX.Y)
# and first cartridge path is only cd-ed into if somewhere inside carts/
# this means you must install the built cartridge before running
-run_cmd="pico8 -run ${install_dirpath}/${cartridge_stem}_${cartridge_suffix}.p8 -screenshot_scale 4 -gif_scale 4 -gif_len 60 $@"
+run_cmd="pico8 -run ${install_dirpath}/${cartridge_stem}_${cartridge_extra_suffix}${cartridge_suffix}.p8 -screenshot_scale 4 -gif_scale 4 -gif_len 60 $@"
# Support UNIX platforms without gnome-terminal by checking if the command exists
# If you `reload.sh` the game, the separate terminal allows you to keep watching the program output,
diff --git a/run_itest.sh b/run_itest.sh
index 4a4d8193..fa5c7486 100755
--- a/run_itest.sh
+++ b/run_itest.sh
@@ -3,7 +3,7 @@
# Run itest with PICO-8 executable
# Usage: build_itest.sh cartridge_suffix
-# cartridge_suffix 'titlemenu', 'stage_intro', 'ingame' or 'stage_clear'
+# cartridge_suffix see data/cartridges.txt for the list of cartridge names
# Any extra arguments are passed to pico8
diff --git a/run_legacy.sh b/run_legacy.sh
index 51c13a57..6247d062 100755
--- a/run_legacy.sh
+++ b/run_legacy.sh
@@ -5,7 +5,7 @@
# check that source and output paths have been provided
if [[ $# -lt 2 ]] ; then
- echo "build.sh takes 2 params, provided $#:
+ echo "run.sh takes 2 params, provided $#:
\$1: representative file base name (used to deduce the main source file and the output path)
\$2: config ('debug' or 'release')"
exit 1
diff --git a/sonic-2d-tech-demo.sublime-project b/sonic-2d-tech-demo.sublime-project
index ee9bc989..f8cb25de 100644
--- a/sonic-2d-tech-demo.sublime-project
+++ b/sonic-2d-tech-demo.sublime-project
@@ -43,37 +43,43 @@
{
"name": "Game: edit built-in data: titlemenu",
"shell": true,
- "cmd": ["pico8 data/builtin_data_titlemenu.p8"],
+ "cmd": ["pico8 -run data/builtin_data_titlemenu.p8"],
"working_dir": "${project_path}"
},
{
"name": "Game: edit built-in data: ingame",
"shell": true,
- "cmd": ["pico8 data/builtin_data_ingame.p8"],
+ "cmd": ["pico8 -run data/builtin_data_ingame.p8"],
"working_dir": "${project_path}"
},
{
"name": "Game: edit built-in data: stage_intro",
"shell": true,
- "cmd": ["pico8 data/builtin_data_stage_intro.p8"],
+ "cmd": ["pico8 -run data/builtin_data_stage_intro.p8"],
"working_dir": "${project_path}"
},
{
"name": "Game: edit built-in data: stage_clear",
"shell": true,
- "cmd": ["pico8 data/builtin_data_stage_clear.p8"],
+ "cmd": ["pico8 -run data/builtin_data_stage_clear.p8"],
"working_dir": "${project_path}"
},
{
- "name": "Game: edit data: bgm",
+ "name": "Game: edit data: stage1 intro",
"shell": true,
- "cmd": ["pico8 data/data_bgm1.p8"],
+ "cmd": ["pico8 -run data/data_stage1_intro.p8"],
"working_dir": "${project_path}"
},
{
- "name": "Game: edit data: stage1 runtime",
+ "name": "Game: edit data: stage1 ingame",
"shell": true,
- "cmd": ["pico8 data/data_stage1_runtime.p8"],
+ "cmd": ["pico8 -run data/data_stage1_ingame.p8"],
+ "working_dir": "${project_path}"
+ },
+ {
+ "name": "Game: edit data: stage sonic",
+ "shell": true,
+ "cmd": ["pico8 -run data/data_stage_sonic.p8"],
"working_dir": "${project_path}"
},
{
@@ -84,35 +90,35 @@
[
{
"name": "00",
- "cmd": ["pico8 data/data_stage1_00.p8"],
+ "cmd": ["pico8 -run data/data_stage1_00.p8"],
},
{
"name": "01",
- "cmd": ["pico8 data/data_stage1_01.p8"],
+ "cmd": ["pico8 -run data/data_stage1_01.p8"],
},
{
"name": "10",
- "cmd": ["pico8 data/data_stage1_10.p8"],
+ "cmd": ["pico8 -run data/data_stage1_10.p8"],
},
{
"name": "11",
- "cmd": ["pico8 data/data_stage1_11.p8"],
+ "cmd": ["pico8 -run data/data_stage1_11.p8"],
},
{
"name": "20",
- "cmd": ["pico8 data/data_stage1_20.p8"],
+ "cmd": ["pico8 -run data/data_stage1_20.p8"],
},
{
"name": "21",
- "cmd": ["pico8 data/data_stage1_21.p8"],
+ "cmd": ["pico8 -run data/data_stage1_21.p8"],
},
{
"name": "30",
- "cmd": ["pico8 data/data_stage1_30.p8"],
+ "cmd": ["pico8 -run data/data_stage1_30.p8"],
},
{
"name": "31",
- "cmd": ["pico8 data/data_stage1_31.p8"],
+ "cmd": ["pico8 -run data/data_stage1_31.p8"],
},
]
},
@@ -141,7 +147,7 @@
},
{
"name": "release",
- "cmd": ["./build_and_install_single_cartridge_with_data.sh titlemenu release"]
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh titlemenu release"],
}
]
},
@@ -178,10 +184,6 @@
"name": "cheat-ultrafast",
"cmd": ["./build_and_install_single_cartridge_with_data.sh ingame cheat-ultrafast"]
},
- {
- "name": "sandbox",
- "cmd": ["./build_and_install_single_cartridge_with_data.sh ingame sandbox"]
- },
{
"name": "assert",
"cmd": ["./build_and_install_single_cartridge_with_data.sh ingame assert"]
@@ -196,6 +198,41 @@
}
]
},
+ {
+ "name": "Game: build cartridge attract_mode",
+ "working_dir": "${project_path}",
+ "shell": true,
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode debug"],
+ "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
+ "syntax": "Packages/Python/Python.sublime-syntax",
+ "variants":
+ [
+ {
+ "name": "debug",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode debug"]
+ },
+ {
+ "name": "tuner",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode tuner"]
+ },
+ {
+ "name": "assert",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode assert"]
+ },
+ {
+ "name": "profiler",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode profiler"]
+ },
+ {
+ "name": "recorder",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode recorder"]
+ },
+ {
+ "name": "release",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode release"]
+ }
+ ]
+ },
{
"name": "Game: build cartridge stage_intro",
"working_dir": "${project_path}",
@@ -242,6 +279,14 @@
}
]
},
+ {
+ "name": "Game: build cartridge sandbox",
+ "working_dir": "${project_path}",
+ "shell": true,
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh sandbox sandbox"],
+ "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
+ "syntax": "Packages/Python/Python.sublime-syntax"
+ },
{
"name": "Game: build full game",
"working_dir": "${project_path}",
@@ -269,7 +314,7 @@
"name": "Game: build itests titlemenu",
"working_dir": "${project_path}",
"shell": true,
- "cmd": ["./build_itest.sh titlemenu"],
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh titlemenu itest --itest"],
"file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
"syntax": "Packages/Python/Python.sublime-syntax",
},
@@ -277,7 +322,7 @@
"name": "Game: build itests ingame",
"working_dir": "${project_path}",
"shell": true,
- "cmd": ["./build_itest.sh ingame"],
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh ingame itest --itest"],
"file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
"syntax": "Packages/Python/Python.sublime-syntax",
},
@@ -285,7 +330,7 @@
"name": "Game: build itests stage_intro",
"working_dir": "${project_path}",
"shell": true,
- "cmd": ["./build_itest.sh stage_intro"],
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh stage_intro itest --itest"],
"file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
"syntax": "Packages/Python/Python.sublime-syntax",
},
@@ -293,7 +338,7 @@
"name": "Game: build itests stage_clear",
"working_dir": "${project_path}",
"shell": true,
- "cmd": ["./build_itest.sh stage_clear"],
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh stage_clear itest --itest"],
"file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
"syntax": "Packages/Python/Python.sublime-syntax",
},
@@ -365,10 +410,6 @@
"name": "cheat-ultrafast",
"cmd": ["./build_and_install_single_cartridge_with_data.sh ingame cheat-ultrafast && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh ingame cheat-ultrafast)"]
},
- {
- "name": "sandbox",
- "cmd": ["./build_and_install_single_cartridge_with_data.sh ingame sandbox && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh ingame sandbox)"]
- },
{
"name": "assert",
"cmd": ["./build_and_install_single_cartridge_with_data.sh ingame assert && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh ingame assert)"]
@@ -383,6 +424,41 @@
},
]
},
+ {
+ "name": "Game: build and run attract_mode",
+ "working_dir": "${project_path}",
+ "shell": true,
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode debug && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh attract_mode debug)"],
+ "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
+ "syntax": "Packages/Python/Python.sublime-syntax",
+ "variants":
+ [
+ {
+ "name": "debug",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode debug && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh attract_mode debug)"]
+ },
+ {
+ "name": "tuner",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode tuner && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh attract_mode tuner)"]
+ },
+ {
+ "name": "assert",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode assert && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh attract_mode assert)"]
+ },
+ {
+ "name": "profiler",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode profiler && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh attract_mode profiler)"]
+ },
+ {
+ "name": "recorder",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode recorder && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh attract_mode recorder)"]
+ },
+ {
+ "name": "release",
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh attract_mode release && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh attract_mode release)"]
+ },
+ ]
+ },
{
"name": "Game: build and run stage_intro",
"working_dir": "${project_path}",
@@ -437,6 +513,14 @@
},
]
},
+ {
+ "name": "Game: build and run sandbox",
+ "working_dir": "${project_path}",
+ "shell": true,
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh sandbox sandbox && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh sandbox sandbox)"],
+ "file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
+ "syntax": "Packages/Python/Python.sublime-syntax"
+ },
{
"name": "Game: build and run full game",
"working_dir": "${project_path}",
@@ -468,7 +552,7 @@
"name": "Game: build and run itests titlemenu",
"working_dir": "${project_path}",
"shell": true,
- "cmd": ["./build_itest.sh titlemenu && (./pico-boots/scripts/reload.sh || ./run_itest.sh titlemenu)"],
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh titlemenu itest --itest && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh titlemenu itest --itest)"],
"file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
"syntax": "Packages/Python/Python.sublime-syntax",
},
@@ -476,7 +560,7 @@
"name": "Game: build and run itests ingame",
"working_dir": "${project_path}",
"shell": true,
- "cmd": ["./build_itest.sh ingame && (./pico-boots/scripts/reload.sh || ./run_itest.sh ingame)"],
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh ingame itest --itest && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh ingame itest --itest)"],
"file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
"syntax": "Packages/Python/Python.sublime-syntax",
},
@@ -484,7 +568,7 @@
"name": "Game: build and run itests stage_intro",
"working_dir": "${project_path}",
"shell": true,
- "cmd": ["./build_itest.sh stage_intro && (./pico-boots/scripts/reload.sh || ./run_itest.sh stage_intro)"],
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh stage_intro itest --itest && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh stage_intro itest --itest)"],
"file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
"syntax": "Packages/Python/Python.sublime-syntax",
},
@@ -492,7 +576,7 @@
"name": "Game: build and run itests stage_clear",
"working_dir": "${project_path}",
"shell": true,
- "cmd": ["./build_itest.sh stage_clear && (./pico-boots/scripts/reload.sh || ./run_itest.sh stage_clear)"],
+ "cmd": ["./build_and_install_single_cartridge_with_data.sh stage_clear itest --itest && (./pico-boots/scripts/reload.sh || ./run_cartridge.sh stage_clear itest --itest)"],
"file_regex": "(.*\\.sh)(?:\\: line )([\\d]+)(?:\\:)([\\d]+)? (.*)",
"syntax": "Packages/Python/Python.sublime-syntax",
},
@@ -536,10 +620,6 @@
"name": "ingame cheat-ultrafast",
"cmd": ["./run_cartridge.sh ingame cheat-ultrafast"]
},
- {
- "name": "ingame sandbox",
- "cmd": ["./run_cartridge.sh ingame sandbox"]
- },
{
"name": "ingame assert",
"cmd": ["./run_cartridge.sh ingame assert"]
@@ -552,6 +632,30 @@
"name": "ingame release",
"cmd": ["./run_cartridge.sh ingame release"]
},
+ {
+ "name": "attract_mode debug",
+ "cmd": ["./run_cartridge.sh attract_mode debug"]
+ },
+ {
+ "name": "attract_mode tuner",
+ "cmd": ["./run_cartridge.sh attract_mode tuner"]
+ },
+ {
+ "name": "attract_mode assert",
+ "cmd": ["./run_cartridge.sh attract_mode assert"]
+ },
+ {
+ "name": "attract_mode profiler",
+ "cmd": ["./run_cartridge.sh attract_mode profiler"]
+ },
+ {
+ "name": "attract_mode recorder",
+ "cmd": ["./run_cartridge.sh attract_mode recorder"]
+ },
+ {
+ "name": "attract_mode release",
+ "cmd": ["./run_cartridge.sh attract_mode release"]
+ },
{
"name": "titlemenu debug",
"cmd": ["./run_cartridge.sh titlemenu debug"]
@@ -598,11 +702,11 @@
},
{
"name": "itests titlemenu",
- "cmd": ["./run_itest.sh titlemenu"],
+ "cmd": ["./run_cartridge.sh titlemenu itest --itest"],
},
{
"name": "itests ingame",
- "cmd": ["./run_itest.sh ingame"],
+ "cmd": ["./run_cartridge.sh ingame itest --itest"],
},
{
"name": "pico8 utests",
@@ -698,6 +802,40 @@
}
]
},
+ {
+ "name": "Game: install cartridge attract_mode (with data)",
+ "working_dir": "${project_path}",
+ "shell": true,
+ "cmd": ["./install_single_cartridge_with_data.sh attract_mode debug"],
+ "syntax": "Packages/Python/Python.sublime-syntax",
+ "variants":
+ [
+ {
+ "name": "debug",
+ "cmd": ["./install_single_cartridge_with_data.sh attract_mode debug"]
+ },
+ {
+ "name": "tuner",
+ "cmd": ["./install_single_cartridge_with_data.sh attract_mode tuner"]
+ },
+ {
+ "name": "assert",
+ "cmd": ["./install_single_cartridge_with_data.sh attract_mode assert"]
+ },
+ {
+ "name": "profiler",
+ "cmd": ["./install_single_cartridge_with_data.sh attract_mode profiler"]
+ },
+ {
+ "name": "recorder",
+ "cmd": ["./install_single_cartridge_with_data.sh attract_mode recorder"]
+ },
+ {
+ "name": "release",
+ "cmd": ["./install_single_cartridge_with_data.sh attract_mode release"]
+ }
+ ]
+ },
{
"name": "Game: install cartridge stage_intro (with data)",
"working_dir": "${project_path}",
@@ -758,6 +896,13 @@
}
]
},
+ {
+ "name": "Game: install cartridge sandbox (with data)",
+ "working_dir": "${project_path}",
+ "shell": true,
+ "cmd": ["./install_single_cartridge_with_data.sh sandbox sandbox"],
+ "syntax": "Packages/Python/Python.sublime-syntax"
+ },
{
"name": "Game: install all cartridges (with data)",
"working_dir": "${project_path}",
@@ -982,6 +1127,51 @@
"ENABLE_RENDER": "1"
}
},
+ {
+ "name": "headless_itests attract_mode",
+ "cmd": ["./test.sh -f headless_itests"],
+ "env": {
+ "ITEST_CARTRIDGE_SUFFIX": "attract_mode"
+ }
+ },
+ {
+ "name": "headless_itests attract_mode (coverage)",
+ "cmd": ["./test.sh -C -f headless_itests"],
+ "env": {
+ "ITEST_CARTRIDGE_SUFFIX": "attract_mode"
+ }
+ },
+ {
+ "name": "headless_itests attract_mode (render)",
+ "cmd": ["./test.sh -f headless_itests"],
+ "env": {
+ "ITEST_CARTRIDGE_SUFFIX": "attract_mode",
+ "ENABLE_RENDER": "1"
+ }
+ },
+ {
+ "name": "headless_itests attract_mode (render + coverage)",
+ "cmd": ["./test.sh -C -f headless_itests"],
+ "env": {
+ "ITEST_CARTRIDGE_SUFFIX": "attract_mode",
+ "ENABLE_RENDER": "1"
+ }
+ },
+ {
+ "name": "headless_itests attract_mode - solo",
+ "cmd": ["./test.sh -f headless_itests -m solo"],
+ "env": {
+ "ITEST_CARTRIDGE_SUFFIX": "attract_mode"
+ }
+ },
+ {
+ "name": "headless_itests attract_mode - solo (render)",
+ "cmd": ["./test.sh -f headless_itests -m solo"],
+ "env": {
+ "ITEST_CARTRIDGE_SUFFIX": "attract_mode",
+ "ENABLE_RENDER": "1"
+ }
+ },
{
"name": "headless_itests stage_intro",
"cmd": ["./test.sh -f headless_itests"],
@@ -1127,34 +1317,34 @@
]
},
{
- "name": "Engine: Postbuild: test add_metadata",
+ "name": "Engine: Prebuild: test preprocess",
"working_dir": "${project_path}/pico-boots",
"shell": true,
- "cmd": ["python3 -m scripts.test_add_metadata"],
+ "cmd": ["python3 -m scripts.test_preprocess"],
"file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
"syntax": "Packages/Python/Python.sublime-syntax"
},
{
- "name": "Engine: Postbuild: test add_require",
+ "name": "Engine: Prebuild: test replace_strings",
"working_dir": "${project_path}/pico-boots",
"shell": true,
- "cmd": ["python3 -m scripts.test_add_require"],
+ "cmd": ["python3 -m scripts.test_replace_strings"],
"file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
"syntax": "Packages/Python/Python.sublime-syntax"
},
{
- "name": "Engine: Postbuild: test analyze",
+ "name": "Engine: Prebuild: test generate_ordered_require_file",
"working_dir": "${project_path}/pico-boots",
"shell": true,
- "cmd": ["python3 -m scripts.test_analyze"],
+ "cmd": ["python3 -m scripts.test_generate_ordered_require_file"],
"file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
"syntax": "Packages/Python/Python.sublime-syntax"
},
{
- "name": "Engine: Postbuild: test preprocess",
+ "name": "Engine: Prebuild: test add_require",
"working_dir": "${project_path}/pico-boots",
"shell": true,
- "cmd": ["python3 -m scripts.test_preprocess"],
+ "cmd": ["python3 -m scripts.test_add_require"],
"file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
"syntax": "Packages/Python/Python.sublime-syntax"
},
@@ -1175,43 +1365,51 @@
"syntax": "Packages/Python/Python.sublime-syntax"
},
{
- "name": "Engine: Postbuild: test generate_dependency_graph",
+ "name": "Engine: Postbuild: test add_metadata",
"working_dir": "${project_path}/pico-boots",
"shell": true,
- "cmd": ["python3 -m scripts.test_generate_dependency_graph"],
+ "cmd": ["python3 -m scripts.test_add_metadata"],
"file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
"syntax": "Packages/Python/Python.sublime-syntax"
},
{
- "name": "Engine: Postbuild: test generate_ordered_require_file",
+ "name": "Engine: Export: test adapt_for_png",
"working_dir": "${project_path}/pico-boots",
"shell": true,
- "cmd": ["python3 -m scripts.test_generate_ordered_require_file"],
+ "cmd": ["python3 -m scripts.test_adapt_for_png"],
"file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
"syntax": "Packages/Python/Python.sublime-syntax"
},
{
- "name": "Engine: Postbuild: test adapt_for_png",
+ "name": "Engine: Export: test patch_pico8_js",
"working_dir": "${project_path}/pico-boots",
"shell": true,
- "cmd": ["python3 -m scripts.test_adapt_for_png"],
+ "cmd": ["python3 -m scripts.test_patch_pico8_js"],
"file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
"syntax": "Packages/Python/Python.sublime-syntax"
},
{
- "name": "Engine: Postbuild: test patch_pico8_js",
+ "name": "Engine: Tool: test offset bgm tracks",
"working_dir": "${project_path}/pico-boots",
"shell": true,
- "cmd": ["python3 -m scripts.test_patch_pico8_js"],
+ "cmd": ["python3 -m scripts.test_offset_bgm_tracks"],
"file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
"syntax": "Packages/Python/Python.sublime-syntax"
},
{
- "name": "Python (legacy): test replace_strings",
- "working_dir": "${project_path}",
+ "name": "Engine: Doc: test generate_dependency_graph",
+ "working_dir": "${project_path}/pico-boots",
"shell": true,
- "cmd": ["python3 -m prebuild.test_replace_strings"],
- "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?, (.*)",
+ "cmd": ["python3 -m scripts.test_generate_dependency_graph"],
+ "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
+ "syntax": "Packages/Python/Python.sublime-syntax"
+ },
+ {
+ "name": "Engine: Doc: test analyze",
+ "working_dir": "${project_path}/pico-boots",
+ "shell": true,
+ "cmd": ["python3 -m scripts.test_analyze"],
+ "file_regex": "\"(.*\\.py)\"(?:, line )([\\d]+)(?:\\:)?([\\d]+)?(.*)",
"syntax": "Packages/Python/Python.sublime-syntax"
},
{
diff --git a/src/application/picosonic_app_attract_mode.lua b/src/application/picosonic_app_attract_mode.lua
new file mode 100644
index 00000000..d9974141
--- /dev/null
+++ b/src/application/picosonic_app_attract_mode.lua
@@ -0,0 +1,67 @@
+-- game application for state: stage
+-- used by main and itest_main
+
+-- this really only defines used gamestates
+-- and wouldn't be necessary if we injected gamestates from main scripts
+
+local picosonic_app_base = require("application/picosonic_app_base")
+
+local input = require("engine/input/input")
+local postprocess = require("engine/render/postprocess")
+
+local stage_state = require("ingame/stage_state")
+
+local picosonic_app_attract_mode = derived_class(picosonic_app_base)
+
+function picosonic_app_attract_mode:instantiate_gamestates() -- override (mandatory)
+ return {stage_state()}
+end
+
+function picosonic_app_attract_mode:on_post_start() -- override (optional)
+ picosonic_app_base.on_post_start(self)
+
+ -- postprocess for fade-out
+ -- usually this is done in gamestate, but attract mode uses the same stage_state
+ -- as ingame, so we'd need #if attract_mode to distinguish behavior, and it's simpler to
+ -- put dedicated behavior here
+ self.postproc = postprocess()
+ -- self.is_exiting = false -- commented out to spare characters, as nil is equivalent
+
+ menuitem(5, "back to title", function()
+ -- prefer passing basename for compatibility with .p8.png
+ load('picosonic_titlemenu')
+ end)
+end
+
+function picosonic_app_attract_mode:on_update() -- override (optional)
+ -- handle input: press any button to leave attract mode (avoids entering pause menu)
+ -- usually inputs are handled in gamestate, but for same reason as self.postproc we do it here
+
+ if input:is_just_pressed(button_ids.o) or input:is_just_pressed(button_ids.x) then
+ self:exit_attract_mode()
+ end
+end
+
+function picosonic_app_attract_mode:on_render() -- override (render)
+ self.postproc:apply()
+end
+
+function picosonic_app_attract_mode:exit_attract_mode()
+ if not self.is_exiting then
+ self.is_exiting = true
+ self:start_coroutine(self.exit_attract_mode_async, self)
+ end
+end
+
+function picosonic_app_attract_mode:exit_attract_mode_async()
+ -- fade out
+ for i = 0, 5 do
+ self.postproc.darkness = i
+ yield_delay_frames(6)
+ end
+
+ -- prefer passing basename for compatibility with .p8.png
+ load('picosonic_titlemenu')
+end
+
+return picosonic_app_attract_mode
diff --git a/src/application/picosonic_app_base.lua b/src/application/picosonic_app_base.lua
index 955e50cf..21804cc7 100644
--- a/src/application/picosonic_app_base.lua
+++ b/src/application/picosonic_app_base.lua
@@ -20,6 +20,10 @@ local vlogger = require("engine/debug/visual_logger")
local mouse = require("engine/ui/mouse")
--#endif
+--#if profiler_lightweight
+local outline = require("engine/ui/outline")
+--#endif
+
local visual = require("resources/visual_common")
local picosonic_app_base = derived_class(gameapp)
@@ -29,6 +33,8 @@ function picosonic_app_base:init()
end
function picosonic_app_base:on_post_start() -- override
+ extcmd("set_title","Pico Sonic")
+
-- disable input auto-repeat (this is to be cleaner, as input module barely uses btnp anyway,
-- and simply detects state changes using btn; if too many compressed chars, strip that first)
poke(0x5f5c, -1)
@@ -40,12 +46,19 @@ function picosonic_app_base:on_post_start() -- override
--#endif
end
+--#if itest
function picosonic_app_base:on_reset() -- override
--#if mouse
mouse:set_cursor_sprite_data(nil)
--#endif
end
+--#endif
+-- Note that if you add support for 2+ OR statements in preprocess.py, it will be more correct
+-- to use `--#if profiler || visual_logger || tuner` so non-release builds that don't use those
+-- don't define on_update either; but the most important is to strip code from release anyway.
+-- Same remark for on_render below
+--#ifn release
function picosonic_app_base:on_update() -- override
--#if profiler
profiler.window:update()
@@ -59,7 +72,9 @@ function picosonic_app_base:on_update() -- override
codetuner:update_window()
--#endif
end
+--#endif
+--#ifn release
function picosonic_app_base:on_render() -- override
--#if profiler
profiler.window:render()
@@ -74,9 +89,17 @@ function picosonic_app_base:on_render() -- override
--#endif
--#if mouse
- -- always draw cursor on top
+ -- always draw cursor on top of the rest (except for profiling)
mouse:render()
--#endif
+
+--#if profiler_lightweight
+ -- when profiler is too heavy due to the whole UI module it uses, use this
+ -- it is drawn after the rest so it can take mouse render into account if used in real game
+ -- print total CPU
+ outline.print_with_outline("cpu: "..stat(1), 2, 10, colors.orange, colors. black)
+--#endif
end
+--#endif
return picosonic_app_base
diff --git a/src/application/picosonic_app_base_utest.lua b/src/application/picosonic_app_base_utest.lua
index baa60a95..21d2c82d 100644
--- a/src/application/picosonic_app_base_utest.lua
+++ b/src/application/picosonic_app_base_utest.lua
@@ -20,21 +20,31 @@ describe('picosonic_app_base', function ()
describe('on_post_start', function ()
setup(function ()
+ stub(_G, "extcmd")
stub(input, "toggle_mouse")
stub(mouse, "set_cursor_sprite_data")
end)
teardown(function ()
+ extcmd:revert()
input.toggle_mouse:revert()
mouse.set_cursor_sprite_data:revert()
end)
after_each(function ()
clear_table(pico8.poked_addresses)
+
+ extcmd:clear()
input.toggle_mouse:clear()
mouse.set_cursor_sprite_data:clear()
end)
+ it('should set title to "Pico Sonic" using extcmd', function ()
+ app:on_post_start()
+ assert.spy(extcmd).was_called(1)
+ assert.spy(extcmd).was_called_with("set_title", "Pico Sonic")
+ end)
+
it('should disable input auto-repeat by poking 0x5f5c = 255 (-1)', function ()
app:on_post_start()
assert.are_equal(-1, pico8.poked_addresses[0x5f5c])
@@ -42,16 +52,14 @@ describe('picosonic_app_base', function ()
it('should toggle mouse cursor', function ()
app:on_post_start()
- local s = assert.spy(input.toggle_mouse)
- s.was_called(1)
- s.was_called_with(match.ref(input), true)
+ assert.spy(input.toggle_mouse).was_called(1)
+ assert.spy(input.toggle_mouse).was_called_with(match.ref(input), true)
end)
it('should set the mouse cursor sprite data', function ()
app:on_post_start()
- local s = assert.spy(mouse.set_cursor_sprite_data)
- s.was_called(1)
- s.was_called_with(match.ref(mouse), match.ref(visual.sprite_data_t.cursor))
+ assert.spy(mouse.set_cursor_sprite_data).was_called(1)
+ assert.spy(mouse.set_cursor_sprite_data).was_called_with(match.ref(mouse), match.ref(visual.sprite_data_t.cursor))
end)
end)
diff --git a/src/application/picosonic_app_ingame.lua b/src/application/picosonic_app_ingame.lua
index 589fada3..e7e59c24 100644
--- a/src/application/picosonic_app_ingame.lua
+++ b/src/application/picosonic_app_ingame.lua
@@ -14,9 +14,56 @@ function picosonic_app_ingame:instantiate_gamestates() -- override (mandatory)
return {stage_state()}
end
+-- made local (equivalent of file static in C++) so the static menuitem callback late_jump_feature_callback,
+-- which doesn't take a self parameter, can access it without accessing singleton flow.curr_state.app...
+-- this is OK because there is only one picosonic app (and seriously this could be a singleton anyway)
+-- in counterpart, you need to provide a getter to it
+local enable_late_jump_feature
+
+function picosonic_app_ingame.get_enable_late_jump_feature()
+ return enable_late_jump_feature
+end
+
+--#ifn debug_menu
+
+-- to allow circular referencing, we must declare the second function before defining it
+local create_late_jump_feature_menuitem
+
+local function late_jump_feature_callback(b)
+ -- normally we should check bitmask with band/&, but to spare characters we exploit the undocumented
+ -- fact that only the last button press mask is used
+ if b == 1 or b == 2 then
+ -- pressing left or right -> toggle feature
+ enable_late_jump_feature = not enable_late_jump_feature
+
+ -- update menuitem label (we have no choice but to recreate the whole menuitem)
+ create_late_jump_feature_menuitem()
+
+ -- don't close pause menu after that
+ return true
+ end
+end
+
+create_late_jump_feature_menuitem = function()
+ -- create/replace menuitem to update the label, as PICO-8 doesn't have a native way to show a variable
+ menuitem(1, "late jump: <"..(enable_late_jump_feature and " on" or "off")..">", late_jump_feature_callback)
+end
+
+--#endif
+
function picosonic_app_ingame:on_post_start() -- override (optional)
picosonic_app_base.on_post_start(self)
+ -- Original feature: late jump
+ -- Enable by default (see playercharacter_data > late_jump_max_delay)
+ -- Note that it's not currently stored as player preference, so it resets each time you restart ingame
+ enable_late_jump_feature = true
+
+--#ifn debug_menu
+ -- default debug menu adds "debug pause" as item 1, so don't overwrite it if present
+ create_late_jump_feature_menuitem()
+--#endif
+
menuitem(3, "warp to start", function()
assert(flow.curr_state.type == ':stage')
flow.curr_state:store_picked_emerald_data()
diff --git a/src/application/picosonic_app_ingame_utest.lua b/src/application/picosonic_app_ingame_utest.lua
index 1f6c32f4..82c309c7 100644
--- a/src/application/picosonic_app_ingame_utest.lua
+++ b/src/application/picosonic_app_ingame_utest.lua
@@ -43,9 +43,14 @@ describe('picosonic_app_ingame', function ()
assert.spy(picosonic_app_base.on_post_start).was_called_with(match.ref(app))
end)
- it('should load cartridge: picosonic_titlemenu', function ()
+ it('should initialize late_jump_max_delay to default', function ()
app:on_post_start()
- assert.spy(menuitem).was_called(3)
+ assert.is_true(app.get_enable_late_jump_feature())
+ end)
+
+ it('should create 4 menu items', function ()
+ app:on_post_start()
+ assert.spy(menuitem).was_called(4)
-- no reference to lambda passed to menuitem, so don't test was_called_with
end)
diff --git a/src/common_attract_mode.lua b/src/common_attract_mode.lua
new file mode 100644
index 00000000..16c4bb10
--- /dev/null
+++ b/src/common_attract_mode.lua
@@ -0,0 +1,34 @@
+-- See common_ingame.lua for explanations, since attract_mode is very close to ingame
+
+-- Usage: add require("common_attract_mode") at the top of each of your attract_mode main scripts
+-- (along with "engine/common") and in bustedhelper_attract_mode
+
+require("engine/core/angle")
+require("engine/core/vector_ext_angle")
+require("engine/core/table_helper")
+
+
+--#if minify_level3
+
+-- in this particular project, this happens to be defined early anyway,
+-- but to be safe
+require("engine/render/animated_sprite_data_enums")
+
+--#endif
+
+require("data/sprite_flags")
+require("ingame/playercharacter_enums")
+
+--#if recorder
+-- exceptionally a global non-constant variable to easily access and print for action recording
+total_frames = 0
+--#endif
+
+--[[#pico8
+--#if unity
+
+-- see explanations in common_ingame.lua
+require("ordered_require_attract_mode")
+
+--#endif
+--#pico8]]
diff --git a/src/common_ingame.lua b/src/common_ingame.lua
index 4aae1151..fd3ce412 100644
--- a/src/common_ingame.lua
+++ b/src/common_ingame.lua
@@ -8,7 +8,6 @@ require("engine/core/angle") -- used by playercharacter, so technically not nee
require("engine/core/vector_ext_angle")
require("engine/core/table_helper")
-
--#if minify_level3
--#if itest
@@ -27,6 +26,13 @@ require("engine/render/animated_sprite_data_enums")
require("data/sprite_flags")
require("ingame/playercharacter_enums")
+--#if recorder
+-- exceptionally a global non-constant variable to easily access and print for action recording
+-- (we don't build stage ingame with #recorder, only attract_mode, but we could; and headless itests
+-- do run the game as if all symbols were active)
+total_frames = 0
+--#endif
+
--[[#pico8
--#if unity
-- When doing a unity build, all modules must be concatenated in dependency, with modules relied upon
diff --git a/src/common_sandbox.lua b/src/common_sandbox.lua
new file mode 100644
index 00000000..3ac5a36b
--- /dev/null
+++ b/src/common_sandbox.lua
@@ -0,0 +1,25 @@
+-- Require all common sandbox modules (used across various scripts in game project)
+-- that define globals and don't return a module table
+-- Equivalent to engine/common.lua but for sandbox cartridge.
+-- Usage: add require("common_sandbox") at the top of each of your sandbox main scripts
+-- (along with "engine/common") and in bustedhelper_sandbox
+
+require("engine/core/angle") -- used by playercharacter, so technically not needed for stage_clear
+require("engine/core/vector_ext_angle")
+require("engine/core/table_helper")
+
+--#if minify_level3
+require("engine/render/animated_sprite_data_enums")
+--#endif
+
+require("data/sprite_flags")
+require("ingame/playercharacter_enums")
+
+--[[#pico8
+--#if unity
+
+-- see explanations in common_ingame.lua
+require("ordered_require_sandbox")
+
+--#endif
+--#pico8]]
diff --git a/src/data/camera_data.lua b/src/data/camera_data.lua
index a344b0e2..19746d8c 100644
--- a/src/data/camera_data.lua
+++ b/src/data/camera_data.lua
@@ -1,3 +1,7 @@
+--#if game_constants
+--(when using replace_strings with --game-constant-module-path [this_data.lua], all namespaced constants
+-- below are replaced with their values (as strings), so this file can be skipped)
+
-- camera parameters
local camera_data = {
@@ -24,6 +28,9 @@ local camera_data = {
-- ((128 - 64) / 2) / 2 (PICO-8 scaling)
window_half_height = 16,
+ -- catchup speed on X
+ catchup_speed_x = 8,
+
-- ground speed from which fast catchup speed is used (when grounded only)
fast_catchup_min_ground_speed = 4,
@@ -35,6 +42,7 @@ local camera_data = {
-- fast_catchup_min_ground_speed or more (e.g. when rolling fast)
fast_catchup_speed_y = 8,
+
-- Forward offset system
-- Base: When character is looking forward a horizontal direction for a certain time
@@ -70,8 +78,33 @@ local camera_data = {
-- at speeds between forward_ext_min_speed_x and max_forward_ext_speed_x, a ratio is applied
forward_ext_max_distance = 32,
- -- catchup speed on X to reach maximum forward extension (px/frame)
- forward_ext_catchup_speed_x = 0.5,
+ -- catchup speed on X to reach maximum forward offset (base + extension) (px/frame)
+ forward_offset_catchup_speed_x = 0.5,
+
+
+ -- Look down
+
+ -- number of frames since starting crouch (and holding it) before camera starts moving down
+ -- (to avoid annoying look down when preparing spin dash). 120 frames = 2s
+ frames_before_look_down = 120,
+
+ -- speed of camera moving down during look down, but also moving up to neutral state (px/frame)
+ look_down_speed = 1,
+
+ -- maximum vertical distance of camera moving down during look down (px)
+ max_look_down_distance = 44,
+
+
+ -- Spin dash lag
+
+ -- duration of spin dash lag in frames
+ -- Note that we use the simple method recommended at the end of http://info.sonicretro.org/SPG:Camera#Spindash_Lag
+ -- therefore we just set timer to 16 frames and decrement each frame instead of 32 frames and catching up 2 frames
+ -- each frame
+ spin_dash_lag_duration = 16
}
+--(game_constants)
+--#endif
+
return camera_data
diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua
index 4d807263..14004451 100644
--- a/src/data/collision_data.lua
+++ b/src/data/collision_data.lua
@@ -12,6 +12,9 @@ local collision_data = {}
-- depending on how we interpretr pixel ladders, we prefer setting them manually to get the slope factor we want
local mask_tile_angles = transform(
{
+ -- TODO OPTIMIZE CHARS: consider removing key as long as the first entries start at [1] and there is no hole
+ -- unfortunately this requires reordering the keys in a non-semantic manner, so check how many chars you really gain
+
-- low slope descending every 4px with flat ground at every step
[1] = {8, 2},
[2] = {8, 0}, -- flat tile 6px high
@@ -68,28 +71,64 @@ local mask_tile_angles = transform(
[25] = { 3, 8},
[41] = { 4, 8},
- -- 6px-high rectangles (angle doesn't matter)
- [26] = {8, 0}, -- 4x6 used for spring left part (collider only)
- [27] = {8, 0}, -- 8x6 used for spring right part (collider only) -- TODO: reuse 2, it's the same!
-
- -- 8px-high rectangles (angle doesn't matter)
- [28] = {8, 0}, -- 4x8 used for rock left part
- [29] = {8, 0}, -- 8x8 used for rock right part and any full ground
- [30] = {8, 0}, -- 6x8 used for spring oriented left (ground part only, object is separate)
- [31] = {8, 0}, -- 6x8 used for spring oriented right (ground part only, object is separate)
+ -- ascending slope variant for first slope of pico-island
+ [44] = {4, -8}, -- bottom of regular 1:2 ascending slope
+ [28] = {4, -8}, -- top of regular 1:2 ascending slope, except at bottom where 1px was removed to allow easy fall-off
+
+ -- 6px-high rectangles (angle still matters for non-8x8 rectangles for tile_collision_data.slope_angle_to_interiors)
+ -- we used a hack here because [26] is a partial rectangle with interior horizontal: right and interior vertical: down
+ -- and we need both to be found correctly by tile_collision_data.slope_angle_to_interiors so the collision reverse
+ -- check in world.compute_qcolumn_height_at is done correctly (if interior horizontal is incorrect, Sonic gets
+ -- blocked before touching left part as in former issue with spring right; if interior vertical is incorrect, Sonic
+ -- floats above spring up left part and cannot trigger it there)
+ -- the actual slope angle doesn't matter as we can never land on a spring, but to make sense we pick something
+ -- almost horizontal, but with coords just enough to get x > 0 and y < 0 => interior is down-right
+ [26] = {8, -0.0625}, -- 4x6 used for spring oriented up - left part (collider only)
+ -- [27] = {8, 0}, -- 8x6 used for spring oriented up - right part (collider only) => same as [2], so removed to spare characters
+
+ -- 8px-high rectangles (angle still matters for non-8x8 rectangles for tile_collision_data.slope_angle_to_interiors)
+ [29] = {8, 0}, -- 8x8 used for full ground
+ [30] = {0, -8}, -- 6x8 used for spring oriented left (ground part only, object is separate)
+ [31] = {0, 8}, -- 6x8 used for spring oriented right (ground part only, object is separate)
-- test only, no corresponding visual tiles
[42] = {8, -4}, -- mid slope ascending but starts 2px high unlike 15 (which starts 4px high)
[43] = {8, -4}, -- mid slope ascending but starts 5px high unlike 15
- [44] = {8, 0}, -- 4x4 block in bottom-right corner, useful for small mask testing (angle doesn't matter)
- -- [45] = {45, {8, 0}}, -- empty tile (can be reused for visual sprite if needed)
+ -- 0 slope bump (ground with uniform height 1px)
+ [45] = {8, 0},
+
+ -- [46], [47]: empty
},
function (dx_dy)
return atan2(dx_dy[1], dx_dy[2])
end
)
+-- set of mask tile ids for which land_on_empty_qcolumn = true
+-- those flags are important to prevent character from detecting the ground below empty q-columns,
+-- and instead consider empty q-columns like actual ground at q-height 0 with the same slope angle as the other q-columns
+-- it's particularly important to set on regular slope tiles that are repeated periodically to avoid slope factor resetting to 0
+-- each time the ground sensor detects flat ground below an empty column
+local mask_tile_land_on_empty_qcolumn_flags = {
+ -- low slope descending every 4px with flat ground at every step
+ [7] = true, -- the 4 columns on the right are empty, but physically you should be able to walk on them
+ -- low slope ascending every 4px
+ [8] = true, -- the 4 columns on the left are empty
+ -- mid slope descending every 2px,
+ [13] = true, -- the 2 columns on the right are empty
+ -- mid slope ascending every 2px
+ [14] = true,-- the 2 columns on the left are empty
+ -- loop parts: bottom (from left to right)
+ [18] = true,-- the 2 columns on the right are empty
+ [19] = true,-- the 2 columns on the left are empty
+ -- loop parts: top (from left to right)
+ [34] = true,-- the 2 columns on the right are empty
+ [35] = true,-- the 2 columns on the left are empty
+ -- [22]/[28] and [25] vertical slopes don't really need this, we removed the top pixel to make fall-off easier,
+ -- but we don't need to stick the the left/right wall for longer
+}
+
-- table of tile collision mask ids indexed by tile id
local mask_tile_ids = {
@@ -159,7 +198,7 @@ local mask_tile_ids = {
-- 6px-high rectangles (angle doesn't matter)
[26] = 26,
- [27] = 27,
+ [27] = 2, -- 2 had same height mask as 27, so we're using this now (also removed from spritesheet)
-- 8px-high rectangles (angle doesn't matter)
[28] = 28,
@@ -172,7 +211,8 @@ local mask_tile_ids = {
[43] = 43,
[44] = 44,
--- [45] = 45, -- empty tile
+-- 0 slope bump (ground with uniform height 1px)
+ [45] = 45,
--(proto)
--#endif
@@ -192,6 +232,8 @@ local mask_tile_ids = {
-- full tiles
-- wood
+ -- ! 167 and 168 look like 218 and 219 but they are reversed to background tiles,
+ -- ! so those have no collisions!
[218] = 29, -- wood (specular middle left)
[219] = 29, -- wood (specular middle right)
[235] = 29, -- wood (generic)
@@ -201,6 +243,10 @@ local mask_tile_ids = {
[83] = 29, -- wood (specular top left)
[84] = 29, -- wood (specular top right)
+-- wood slope variant for first slope
+ [182] = 44, -- wood (bottom of regular 1:2 ascending slope)
+ [166] = 28, -- wood (top of regular 1:2 ascending slope)
+
-- floating platform bottom (left and right)
[124] = 29,
[125] = 29,
@@ -236,6 +282,17 @@ local mask_tile_ids = {
[88] = 29,
[89] = 29,
+ -- 0 slope bumps (grass ground with uniform height 1px)
+ [160] = 45,
+ [161] = 45,
+
+ -- 0 slope bumps (grass ground with uniform height 1px) with wood wall behind
+ [213] = 45,
+ [214] = 45,
+
+ -- loop variant of last mid slope descending every 2px
+ [164] = 13,
+
-- leaves
[94] = 29, -- wood (specular bottom left) with first leaves
[95] = 29, -- wood (specular bottom right) with first leaves
@@ -245,35 +302,35 @@ local mask_tile_ids = {
-- other shapes
-- spring
- [74] = 26, -- normal: left part
- [75] = 27, -- normal: right part
- [106] = 29, -- extended: bottom-left part
- [107] = 29, -- extended: bottom-right part
--- extended higher parts (no collisions)
+ [74] = 26, -- spring oriented up - normal: left part
+ [75] = 2, -- spring oriented up - normal: right part (2 had same height mask as 27, so we're using this now (also removed from spritesheet))
+-- extended parts (kept for reference, but not the spring sprite is fully rendered via spring:render in both states,
+-- and the collision doesn't change, so the tilemap is never modified so we keep using the 2 tiles above for collision)
--[[
- [90] = 0, -- spring extended: top-left part (we only collide with bottom)
- [91] = 0, -- spring extended: top-right part (we only collide with bottom)
+ [106] = 26, -- spring oriented up - extended: bottom-left part
+ [107] = 2, -- spring oriented up - extended: bottom-right part
+ [90] = 0, -- spring oriented up - extended: top-left part (we only collide with bottom)
+ [91] = 0, -- spring oriented up - extended: top-right part (we only collide with bottom)
--]]
[202] = 30, -- spring oriented left representative tile (still collides to avoid "falling inside")
[173] = 31, -- spring oriented right representative tile (still collides to avoid "falling inside")
-- rock
-- (only left parts have partial colliders)
- [168] = 28, -- rock (top-left part)
- [92] = 29, -- rock (top-middle part)
- [93] = 29, -- rock (top-right part)
- [184] = 28, -- rock (small rock bottom-left part, can be connected to medium rock extension)
- [108] = 29, -- rock (small rock bottom-middle part, can be connected to medium rock extension)
- [109] = 29, -- rock (small rock bottom-right part, can be connected to medium rock extension)
- [169] = 28, -- rock (medium rock bottom-left part, can be connected to big rock extension)
- [190] = 29, -- rock (medium rock bottom-middle part, can be connected to big rock extension)
- [191] = 29, -- rock (medium rock bottom-right part, can be connected to big rock extension)
- [185] = 28, -- rock (big rock extension top-left part)
- [206] = 29, -- rock (big rock extension top-middle part)
- [207] = 29, -- rock (big rock extension top-right part)
- [186] = 28, -- rock (big rock bottom-left part)
- [122] = 29, -- rock (big rock bottom-middle part)
- [123] = 29, -- rock (big rock bottom-right part)
+ [176] = 4, -- rock (small and medium top-left, 8x4 rect)
+ [177] = 4, -- rock (small and medium top-right, 8x4 rect)
+ [192] = 29, -- rock (small bottom-left = medium mid-left)
+ [193] = 29, -- rock (small bottom-right = medium mid-right)
+ [208] = 29, -- rock (medium bottom-left)
+ [209] = 29, -- rock (medium bottom-right)
+ [162] = 29, -- rock (big rock top-left)
+ [163] = 29, -- rock (big rock top-right)
+ [178] = 29, -- rock (big rock mid-left 1)
+ [179] = 29, -- rock (big rock mid-right 1)
+ [194] = 29, -- rock (big rock mid-left 2)
+ [195] = 29, -- rock (big rock mid-right 2)
+ [210] = 29, -- rock (big rock bottom-left)
+ [211] = 29, -- rock (big rock bottom-right)
-- loop (collider only)
@@ -345,13 +402,14 @@ local mask_tile_ids = {
-- (via picosonic_app_ingame > stage_state > player_char > world)
-- so this will be initialized on game start, which is perfect for us as the initial
-- spritesheet is loaded at that point, and it contains all the collision masks
+-- (in v3, it actually contains *only* collision masks)
-- doing this later, after background data cartridge reload (in stage on_enter)
-- would fail, as the collision mask sprites would be overwritten by the runtime background
-- sprites (only meant to be drawn programmatically)
-- could probably be done via transform too
local tiles_collision_data = {}
for sprite_id, mask_tile_id in pairs(mask_tile_ids) do
- tiles_collision_data[sprite_id] = tile_collision_data.from_raw_tile_collision_data(mask_tile_id, mask_tile_angles[mask_tile_id])
+ tiles_collision_data[sprite_id] = tile_collision_data.from_raw_tile_collision_data(mask_tile_id, mask_tile_angles[mask_tile_id], mask_tile_land_on_empty_qcolumn_flags[mask_tile_id])
end
-- proxy getter is only here to make stubbing possible in tile_test_data
diff --git a/src/data/playercharacter_data.lua b/src/data/playercharacter_numerical_data.lua
similarity index 74%
rename from src/data/playercharacter_data.lua
rename to src/data/playercharacter_numerical_data.lua
index ae6bb920..3375dbb7 100644
--- a/src/data/playercharacter_data.lua
+++ b/src/data/playercharacter_numerical_data.lua
@@ -1,6 +1,14 @@
-local sprite_data = require("engine/render/sprite_data")
-local animated_sprite_data = require("engine/render/animated_sprite_data")
+--#if game_constants
+--(when using replace_strings with --game-constant-module-path [this_data.lua], all namespaced constants
+-- below are replaced with their values (as strings), so this file can be skipped)
+-- note that we extracted sprite data in a separate file, playercharacter_sprite_data.lua,
+-- so we could parse constants here without any ambiguity, and avoid getting "UNSUBSTITUTED pc_data."
+
+-- remember that the name of the local here matters more than the file name for replace_strings,
+-- so we will effectively replace `pc_data.member`, not `playercharacter_numerical_data.member`
+-- we could have named it pc_numerical_data but there were many more usages than sprite data,
+-- so it was easier to just keep the original name
local pc_data = {
-- platformer motion
@@ -74,6 +82,8 @@ local pc_data = {
-- note that combined with air_accel_x_frame2, we can deduce the actual
-- max air speed x: air_accel_x_frame2 / (1/air_drag_factor_per_frame - 1)
-- = 1.453125 px/frame
+ -- value comes from 1 - 0.125*256, as SPG mentions value is subtracted by ((previous_value div 0.125) / 256)
+ -- but we don't mind about the Euclidian division and just keep the remainder, effectively dividing by 0.125*256
air_drag_factor_per_frame = 0.96875, -- 62/64
-- min absolute velocity x for which air drag is applied (px/frame)
@@ -129,6 +139,14 @@ local pc_data = {
-- which will give a value of 1.890625. for a hop, the initial speed will remain 2.
jump_interrupt_speed_frame = 2,
+ -- Original feature: late jump
+ -- Late jump max delay: number of frames after falling off ground while walking during which the character
+ -- can still jump.
+ -- It comes from modern platformers but was not present in Classic Sonic
+ -- I found it useful for tricky jumps required to get some emeralds,
+ -- but purists may want to disable it, so I added a menuitem for that in picosonic_app_ingame:on_post_start.
+ late_jump_max_delay = 6,
+
-- absolute vertical speed given by spring bounce (px/frame)
-- from this and gravity we can deduce the max jump height: 116.71875
-- (measurement with debug step: 112) ~ 14+ tiles at frame 45
@@ -151,7 +169,8 @@ local pc_data = {
launch_ramp_speed_max_launch_speed = 9.7,
-- launch angle of ramp (PICO-8 angle)
- launch_ramp_velocity_angle = atan2(8, -5),
+ -- note that PICO-8 accepts 0x
+ launch_ramp_velocity_angle = 0x0.16c2, -- atan2(8, -5)
-- duration to ignore launch ramp after trigger to avoid hitting it and landing again
-- (frames)
@@ -188,15 +207,20 @@ local pc_data = {
-- max vertical distance allowed to escape from inside ground (must be < tile_size as
-- (px)
- -- _compute_closest_ground_query_info uses it as upper_limit tile_size)
+ -- compute_closest_ground_query_info uses it as upper_limit tile_size)
-- also the max step up of the character in ground motion
- max_ground_escape_height = 4,
+ -- this used to be 4, which was enough with pixel step physics, but since we use big steps now,
+ -- we had to increase it to 7 since it's much easier to step into the ground, esp. in a loop
+ -- 5 is enough for running, but 7 required for fast spin dash
+ max_ground_escape_height = 7,
-- max vertical distance allowed to snap to a lower ground while running (on step or curve)
-- a.k.a. max step down
-- (px)
max_ground_snap_height = 4,
+
+--#if cheat
-- debug motion
-- motion speed in debug mode (px/frame)
@@ -210,8 +234,39 @@ local pc_data = {
-- friction aka passive deceleration speed in debug mode (px/frame^2)
debug_move_friction = 1,
+--#endif
+
+
+ -- spin dash
+
+ -- rev increase every time player pressed jump button
+ -- note that this is an abstract value, so we don't divide it by 2 like speed values
+ -- (no unit)
+ spin_dash_rev_increase_step = 2,
+
+ -- when not revving (charging spin dash) this frame, apply this factor to reduce rev slightly
+ -- same value as air_drag_factor_per_frame (SPG remarks that)
+ -- (no unit)
+ spin_dash_drag_factor_per_frame = 0.96875, -- 62/64
+
+ -- maximum rev value (abstract value, so no division by 2 for PICO-8)
+ -- (no unit)
+ spin_dash_rev_max = 8,
+
+ -- base launch speed on spin dash release (SPG value / 2) (px/frame)
+ spin_dash_base_speed = 4,
+
+ -- factor applied to floor part of spin dash rev to contribute to spin dash launch speed (px/frame)
+ -- SPG divides rev by 2, so in PICO-8 units we must divide by 4, so multiply by 0.25
+ -- from the parameters above we can deduce the maximum spin dash launch speed
+ -- (although very hard to even get close to by a human):
+ -- spin_dash_base_speed + spin_dash_rev_max * spin_dash_rev_increase_factor
+ -- = 4 + 8 * 0.25
+ -- = 6
+ spin_dash_rev_increase_factor = 0.25,
- -- sprite
+
+ -- animation
-- speed at which the character sprite angle falls back toward 0 (upward)
-- when character is airborne (after falling from ceiling or running up and off an ascending slope) (pico8 angle/frame)
@@ -219,42 +274,6 @@ local pc_data = {
-- deduced duration to rotate from upside down to upward: 0.5/(1/128) = 64 frames = 1s + 4 frames
sprite_angle_airborne_reset_speed_frame = 1/128,
- -- stand right
- -- colors.pink: 14
- sonic_sprite_data_table = transform(
- -- anim_name below is not protected since accessed via minified member to define animations more below
- --anim_name = sprite_data(
- -- id_loc, span, pivot, transparent_color (14: pink))
- {
- idle = {{0, 8}, {2, 2}, {10, 8}, 14},
- walk1 = {{2, 8}, {2, 2}, { 8, 8}, 14},
- walk2 = {{4, 8}, {2, 2}, { 8, 8}, 14},
- walk3 = {{6, 8}, {2, 2}, { 9, 8}, 14},
- walk4 = {{8, 8}, {2, 2}, { 8, 8}, 14},
- walk5 = {{10, 8}, {2, 2}, { 8, 8}, 14},
- walk6 = {{12, 8}, {2, 2}, { 8, 8}, 14},
- brake1 = {{10, 1}, {2, 2}, { 9, 8}, 14},
- brake2 = {{12, 1}, {2, 2}, { 9, 8}, 14},
- brake3 = {{14, 1}, {2, 2}, {11, 8}, 14},
- spring_jump = {{14, 8}, {2, 3}, {9, 8}, 14},
- run1 = {{0, 10}, {2, 2}, { 8, 8}, 14},
- run2 = {{2, 10}, {2, 2}, { 8, 8}, 14},
- run3 = {{4, 10}, {2, 2}, { 8, 8}, 14},
- run4 = {{6, 10}, {2, 2}, { 8, 8}, 14},
- spin_full_ball = {{0, 12}, {2, 2}, { 6, 6}, 14},
- spin1 = {{2, 12}, {2, 2}, { 6, 6}, 14},
- spin2 = {{4, 12}, {2, 2}, { 6, 6}, 14},
- spin3 = {{6, 12}, {2, 2}, { 6, 6}, 14},
- spin4 = {{8, 12}, {2, 2}, { 6, 6}, 14},
- }, function (raw_data)
- return sprite_data(
- sprite_id_location(raw_data[1][1], raw_data[1][2]), -- id_loc
- tile_vector(raw_data[2][1], raw_data[2][2]), -- span
- vector(raw_data[3][1], raw_data[3][2]), -- pivot
- raw_data[4] -- transparent_color
- )
- end),
-
-- minimum playback speed for "walk" animation, to avoid very slow animation
-- 10/16=5/8: the 10 counters the 10 duration frames of ["walk"] below, 1/8 to represent max duration 8 in SPG:Animations
-- and an extra 1/2 factor because for some reason, SPG values make animations look too fast (as if durations were for 30FPS)
@@ -271,35 +290,33 @@ local pc_data = {
-- speed from which the brake anim is played when decelerating (px/frame)
brake_anim_min_speed_frame = 2,
+
+
+ -- pfx
+
+ -- spin dash dust particle spawn period (frames)
+ spin_dash_dust_spawn_period_frames = 3.1,
+
+ -- spin dash dust particle spawn count every period
+ spin_dash_dust_spawn_count = 4,
+
+ -- spin dash dust particle spawn period (frames)
+ spin_dash_dust_lifetime_frames = 34,
+
+ -- spin dash dust particle initial velocity (px/frames)
+ -- before we switch to perfect numerical substitution:
+ -- spin_dash_dust_base_init_velocity = vector(-0.43, -0.17),
+ spin_dash_dust_base_init_velocity_x = -0.43,
+ spin_dash_dust_base_init_velocity_y = -0.17,
+
+ -- spin dash dust particle spawn period (frames)
+ spin_dash_dust_max_deviation = 0.04,
+
+ -- spin dash dust particle spawn period (frames)
+ spin_dash_dust_base_max_size = 4.1,
}
-local sdt = pc_data.sonic_sprite_data_table
-
--- define animated sprite data in a second step, as it needs sprite data to be defined first
--- note that we do not split spin_slow and spin_fast as distinguished by SPG anymore
--- in addition, while spin_slow was defined to have 1 spin_full_ball frame and
--- spin_fast had 2, our spin has 4, once every other frame, to match Sonic 3 more closely
-pc_data.sonic_animated_sprite_data_table = transform(
- -- access sprite data by non-protected member to allow minification
- -- see animated_sprite_data.lua for anim_loop_modes values
- --[anim_name] = animated_sprite_data.create(pc_data.sonic_sprite_data_table,
- -- sprite_keys, step_frames, loop_mode as int)
- {
- ["idle"] = {{sdt.idle}, 10, 2},
- ["walk"] = {{sdt.walk1, sdt.walk2, sdt.walk3, sdt.walk4, sdt.walk5, sdt.walk6},
- 10, 4},
- ["brake_start"] = {{sdt.brake1, sdt.brake2},
- 10, 2},
- ["brake_reverse"] = {{sdt.brake3},
- 15, 2},
- ["run"] = {{sdt.run1, sdt.run2, sdt.run3, sdt.run4},
- 5, 4},
- ["spin"] = {{sdt.spin_full_ball, sdt.spin1, sdt.spin_full_ball, sdt.spin2, sdt.spin_full_ball,
- sdt.spin3, sdt.spin_full_ball, sdt.spin4},
- 5, 4},
- ["spring_jump"] = {{sdt.spring_jump}, 10, 2}
-}, function (raw_data)
- return animated_sprite_data(raw_data[1], raw_data[2], raw_data[3])
-end)
+--(game_constants)
+--#endif
return pc_data
diff --git a/src/data/playercharacter_sprite_data.lua b/src/data/playercharacter_sprite_data.lua
new file mode 100644
index 00000000..9f216a35
--- /dev/null
+++ b/src/data/playercharacter_sprite_data.lua
@@ -0,0 +1,84 @@
+local sprite_data = require("engine/render/sprite_data")
+local animated_sprite_data = require("engine/render/animated_sprite_data")
+
+-- sdt: sprite_data_table
+local sdt = transform(
+ -- anim_name below is not protected since accessed via minified member to define animations more below
+ --anim_name = sprite_data(
+ -- id_loc, span, pivot, transparent_color (14: pink))
+ {
+ idle = {{12, 8}, {2, 2}, {10, 8}, 14},
+ walk1 = {{0, 8}, {2, 2}, { 8, 8}, 14},
+ walk2 = {{2, 8}, {2, 2}, { 8, 8}, 14},
+ walk3 = {{4, 8}, {2, 2}, { 9, 8}, 14},
+ walk4 = {{6, 8}, {2, 2}, { 8, 8}, 14},
+ walk5 = {{8, 8}, {2, 2}, { 8, 8}, 14},
+ walk6 = {{10, 8}, {2, 2}, { 8, 8}, 14},
+ brake1 = {{10, 8}, {2, 2}, { 9, 8}, 14},
+ brake2 = {{12, 8}, {2, 2}, { 9, 8}, 14},
+ brake3 = {{14, 8}, {2, 2}, {11, 8}, 14},
+ spring_jump = {{14, 8}, {2, 3}, { 9, 8}, 14},
+ run1 = {{0, 8}, {2, 2}, { 8, 8}, 14},
+ run2 = {{2, 8}, {2, 2}, { 8, 8}, 14},
+ run3 = {{4, 8}, {2, 2}, { 8, 8}, 14},
+ run4 = {{6, 8}, {2, 2}, { 8, 8}, 14},
+ spin_full_ball = {{0, 8}, {2, 2}, { 6, 6}, 14},
+ spin1 = {{2, 8}, {2, 2}, { 6, 6}, 14},
+ spin2 = {{4, 8}, {2, 2}, { 6, 6}, 14},
+ spin3 = {{6, 8}, {2, 2}, { 6, 6}, 14},
+ spin4 = {{8, 8}, {2, 2}, { 6, 6}, 14},
+ crouch1 = {{12, 8}, {2, 2}, { 7,10}, 14},
+ crouch2 = {{14, 8}, {2, 2}, { 7,10}, 14},
+ spin_dash_shrink = {{0, 8}, {2, 2}, { 3,10}, 14},
+ spin_dash1 = {{2, 8}, {2, 2}, { 3,10}, 14},
+ spin_dash2 = {{4, 8}, {2, 2}, { 3,10}, 14},
+ spin_dash3 = {{6, 8}, {2, 2}, { 3,10}, 14},
+ spin_dash4 = {{8, 8}, {2, 2}, { 3,10}, 14},
+ }, function (raw_data)
+ return sprite_data(
+ sprite_id_location(raw_data[1][1], raw_data[1][2]), -- id_loc
+ tile_vector(raw_data[2][1], raw_data[2][2]), -- span
+ vector(raw_data[3][1], raw_data[3][2]), -- pivot
+ raw_data[4] -- transparent_color
+ )
+end)
+
+-- define animated sprite data in a second step, as it needs sprite data to be defined first
+-- note that we do not split spin_slow and spin_fast as distinguished by SPG anymore
+-- in addition, while spin_slow was defined to have 1 spin_full_ball frame and
+-- spin_fast had 2, our spin has 4, once every other frame, to match Sonic 3 more closely
+-- asdt: animated_sprite_data_table
+local asdt = transform(
+ -- access sprite data by non-protected member to allow minification
+ -- see animated_sprite_data.lua for anim_loop_modes values
+ --[anim_name] = animated_sprite_data.create(sdt,
+ -- sprite_keys, step_frames, loop_mode as int)
+ {
+ ["idle"] = {{sdt.idle}, 1, 2},
+ ["walk"] = {{sdt.walk1, sdt.walk2, sdt.walk3, sdt.walk4, sdt.walk5, sdt.walk6},
+ 10, 4},
+ ["brake_start"] = {{sdt.brake1, sdt.brake2},
+ 10, 2},
+ ["brake_reverse"] = {{sdt.brake3},
+ 15, 2},
+ ["run"] = {{sdt.run1, sdt.run2, sdt.run3, sdt.run4},
+ 5, 4},
+ ["spin"] = {{sdt.spin_full_ball, sdt.spin1, sdt.spin_full_ball, sdt.spin2, sdt.spin_full_ball,
+ sdt.spin3, sdt.spin_full_ball, sdt.spin4},
+ 5, 4},
+ ["crouch"] = {{sdt.crouch1, sdt.crouch2},
+ 6, 2},
+ ["spring_jump"] = {{sdt.spring_jump}, 1, 2},
+ ["spin_dash"] = {{sdt.spin_dash_shrink, sdt.spin_dash1, sdt.spin_dash_shrink, sdt.spin_dash2, sdt.spin_dash_shrink,
+ sdt.spin_dash3, sdt.spin_dash_shrink, sdt.spin_dash4},
+ 1, 4},
+}, function (raw_data)
+ return animated_sprite_data(raw_data[1], raw_data[2], raw_data[3])
+end)
+
+local pc_sprite_data = {
+ sonic_sprite_data_table = sdt,
+ sonic_animated_sprite_data_table = asdt
+}
+
+return pc_sprite_data
diff --git a/src/data/sprite_flags.lua b/src/data/sprite_flags.lua
index e13d7f2b..8eb1f7a9 100644
--- a/src/data/sprite_flags.lua
+++ b/src/data/sprite_flags.lua
@@ -1,21 +1,23 @@
sprite_flags = {
- collision = 0, -- collision flag set on VISUAL sprite (and MASK sprite for testing with proto tiles)
- oneway = 1, -- one-way collision flag set on VISUAL sprite
- -- unused2 = 2,
+ collision = 0, -- collision flag set on VISUAL sprite (and MASK sprite for testing with proto tiles)
+ oneway = 1, -- one-way collision flag set on VISUAL sprite
+ ignore_loop_layer = 2, -- never ignore collision due to being in loop entrance/exit
+ -- (added to avoid entering ground near entrance)
-- unused3 = 3,
- -- unused4 = 4,
- spring = 5, -- spring
- midground = 6, -- midground sprite (should be drawn after programmatical background)
- foreground = 7, -- foreground sprite (should be drawn last)
+ waterfall = 4, -- any tile normally on midground but containing animated waterfall parts via color swapping
+ -- when setting this flag, do *not* also set the midground flag so we can render them separately
+ spring = 5, -- spring
+ midground = 6, -- midground sprite (should be drawn after programmatical background, and includes tilemap BG)
+ foreground = 7, -- foreground sprite (should be drawn last)
}
sprite_masks = {
- collision = 1, -- 1 << 0
- oneway = 2, -- 1 << 1
- -- unused2 = 4, -- 1 << 2
- -- unused3 = 8, -- 1 << 3
- -- unused4 = 16, -- 1 << 4
- spring = 32, -- 1 << 5
- midground = 64, -- 1 << 6
- foreground = 128, -- 1 << 7
+ collision = 1, -- 1 << 0
+ oneway = 2, -- 1 << 1
+ ignore_loop_layer = 4, -- 1 << 2
+ -- unused3 = 8, -- 1 << 3
+ waterfall = 16, -- 1 << 4
+ spring = 32, -- 1 << 5
+ midground = 64, -- 1 << 6
+ foreground = 128, -- 1 << 7
}
diff --git a/src/data/stage_clear_data.lua b/src/data/stage_clear_data.lua
index 18f672b8..a3698c01 100644
--- a/src/data/stage_clear_data.lua
+++ b/src/data/stage_clear_data.lua
@@ -1,3 +1,7 @@
+--#if game_constants
+--(when using replace_strings with --game-constant-module-path [this_data.lua], all namespaced constants
+-- below are replaced with their values (as strings), so this file can be skipped)
+
local stage_clear_data = {
-- stage clear sequence timing
@@ -22,4 +26,7 @@ local stage_clear_data = {
delay_after_zigzag_fadeout = 1.0,
}
+--(game_constants)
+--#endif
+
return stage_clear_data
diff --git a/src/data/stage_common_data.lua b/src/data/stage_common_data.lua
new file mode 100644
index 00000000..a1413323
--- /dev/null
+++ b/src/data/stage_common_data.lua
@@ -0,0 +1,37 @@
+--#if game_constants
+--(when using replace_strings with --game-constant-module-path [this_data.lua], all namespaced constants
+-- below are replaced with their values (as strings), so this file can be skipped)
+
+local stage_common_data = {
+
+ -- Gameplay
+
+ emerald_pick_radius = 8,
+
+
+ -- Visual
+
+ -- duration of goal plate rotating before stage clear (results sub-state) starts (frames)
+ goal_rotating_anim_duration = 120,
+
+ -- spring extension duration (tiles use custom animation via async instead of animated_sprite)
+ spring_extend_duration = 0.15,
+
+
+ -- Audio
+
+ -- bgm id
+ -- with the new dynamic bgm cartridge reload system,
+ -- we have separate cartridges containing the bgm
+ -- and it always starts at 0, covering not more than patterns 0-49
+ -- (to guarantee space for SFX)
+ bgm_id = 0,
+
+ -- duration of bgm fade out after reaching goal (s)
+ bgm_fade_out_duration = 1.0,
+}
+
+--(game_constants)
+--#endif
+
+return stage_common_data
diff --git a/src/data/stage_data.lua b/src/data/stage_data.lua
index 0e162892..1310bb95 100644
--- a/src/data/stage_data.lua
+++ b/src/data/stage_data.lua
@@ -1,117 +1,91 @@
local location_rect = require("engine/core/location_rect")
local stage_data = {
-
- -- Common data
-
- -- Gameplay
-
- emerald_pick_radius = 8,
-
-
- -- Visual
-
- -- duration of goal plate rotating before stage clear (results sub-state) starts (frames)
- goal_rotating_anim_duration = 120,
-
- -- spring extension duration (tiles use custom animation via async instead of animated_sprite)
- spring_extend_duration = 0.15,
-
-
- -- Audio
-
- -- duration of bgm fade out after reaching goal (s)
- bgm_fade_out_duration = 1.0,
-
-
-- Stage-specific data, per id
-
- for_stage = {
-
- [1] = {
- -- stage title
- title = "pico island",
-
- -- dimensions in tiles (128 * number of chained maps per row, 32 * number of chained maps per column,
- -- extra tiles on width for goal area)
- -- they will be divided by 128 or 32 and ceiled to deduce the extended map grid to load
- tile_width = 128 * 3 + 48,
- tile_height = 32 * 2,
-
- -- where the player character spawns on stage start (region (0, 1))
- -- ! structs are still accessed by ref in Lua, OK but make sure to always copy
- -- or apply a conversion method, never assign them directly to a variable that may change
- spawn_location = location(7, 32+15),
-
- -- the x to reach to finish the stage
- -- remove it with new goal soon
- goal_x = (3*128 + 48)*8, -- after 3 regions of 128 tiles, in the middle of the partial final region
-
- -- bgm id
- -- with the new dynamic bgm cartridge reload system,
- -- we have separate cartridges containing the bgm
- -- and it always starts at 0, covering not more than patterns 0-49
- -- (to guarantee space for SFX)
- bgm_id = 0,
-
- -- camera data
-
- -- camera bottom limit margin piecewise constant curve keypoints
- -- it is made of horizontal segments defined by key points at their *end*,
- -- and mostly useful because we don't want the camera to show too much of the bottom at some places,
- -- so the player can see more of the top and feel that they have reached the (local) bottom of the stage
- -- keypoint X represents the tile i coordinate that camera X must reach (must * tile_size)
- -- because it is simpler to count tiles from the bottom, we define keypoint Y as the number of tiles
- -- from the real stage bottom, that are hidden and from where the camera will be clamped
- -- (note that they are only hidden in the middle of the segments they belong to; crouching, if implemented, just on the left/right
- -- of a keypoint would reveal suddenly more or less tiles, but we don't mind because the sudden changes
- -- are done when the ground is at a high level, so they can't be experienced when running normally)
- -- keypoints must be defined in order, from left to right, at each segment end
- -- a keypoint = vector(camera_x, camera_bottom_limit_offset)
- -- they have been deduced from playing Sonic 3 (& K) and crouching to see how far I can look down
- camera_bottom_limit_margin_keypoints = {
- vector(47, 11),
- vector(104, 8),
- vector(384, 0), -- normal stage bottom limit from tile 104 to tile (the original game limit there matches our true stage bottom)
- -- 128 * 3 = 384 which is the start of region 31 (it's an approx of the original game since we cut the spiral tree)
- vector(513, 15)
- -- 128 * 4 + 1 = 513 which covers the whole region 31 (overkill as we don't use it completely, but safe)
- -- 15 has been chosen to see 4 tiles below the ground level
- -- usually it's 3 in Sonic 3, but because PICO-8 has a slightly bigger relative screen height,
- -- only 3 would show a sudden 4px clamp when passing tile 384
- -- of course we imitate Sonic 3 but adding tiles descending progressively from much higher than the bottom limit
- -- to hide the sudden change of limit, but while Sonic 3 is fine with 7 tiles, PICO-8 can see up to 8 tiles below (without crouching)
- -- so we need 8 tiles of margin from the highest platform in region 31, and that corresponds to 4 tiles under the lowest ground
-
- -- there is actually yet another level in the original game, vector(188, -8) but it is to reveal the water area
- -- which is below our stage bottom (hence negative -8),
- -- so, since it was cut bottom for our adaptationate limit for us
- },
-
- -- layer data
- -- all tile locations are global
-
- loop_exit_areas = {
- -- lower loop (read in region (1, 1))
- location_rect(128 + 94, 32 + 12, 128 + 100, 32 + 22),
- -- upper loop 1 (read in region (2, 0))
- location_rect(256 + 81, 20, 256 + 87, 30),
- -- upper loop 2 (read in region (2, 0) and (2, 1))
- location_rect(256 + 105, 28, 256 + 111, 32 + 5),
- },
-
- loop_entrance_areas = {
- -- small loop (read in region (1, 1))
- location_rect(128 + 101, 32 + 12, 128 + 106, 32 + 22),
- -- upper loop 1 (read in region (2, 0))
- location_rect(256 + 88, 20, 256 + 93, 30),
- -- upper loop 2 (read in region (2, 0) and (2, 1))
- location_rect(256 + 112, 28, 256 + 117, 32 + 5),
- }
+ [1] = {
+ -- stage title
+ title = "pico island",
+
+ -- dimensions in tiles (128 * number of chained maps per row, 32 * number of chained maps per column,
+ -- extra tiles on width for goal area)
+ -- they will be divided by 128 or 32 and ceiled to deduce the extended map grid to load
+ -- note that tile_width does not take into account the extra tiles added off-camera after the goal,
+ -- to prevent Sonic from falling into the void
+ tile_width = 430, -- 128 * 3 + 46,
+ tile_height = 64, -- 32 * 2,
+
+ -- where the player character spawns on stage start (region (0, 1))
+ -- ! structs are still accessed by ref in Lua, OK but make sure to always copy
+ -- or apply a conversion method, never assign them directly to a variable that may change
+ -- note that spawn location -> topleft position so this tile is actually the ground where Sonic will stand
+ spawn_location = location(5, 47), -- location(5, 32+15),
+
+ -- the x to reach to finish the stage
+ -- remove it with new goal soon
+ goal_x = 3456, -- (3*128 + 48)*8, -- after 3 regions of 128 tiles, in the middle of the partial final region
+
+
+ -- camera data
+
+ -- camera bottom limit margin piecewise constant curve keypoints
+ -- it is made of horizontal segments defined by key points at their *end*,
+ -- and mostly useful because we don't want the camera to show too much of the bottom at some places,
+ -- so the player can see more of the top and feel that they have reached the (local) bottom of the stage
+ -- keypoint X represents the tile i coordinate that camera X must reach (must * tile_size)
+ -- because it is simpler to count tiles from the bottom, we define keypoint Y as the number of tiles
+ -- from the real stage bottom, that are hidden and from where the camera will be clamped
+ -- (note that they are only hidden in the middle of the segments they belong to; crouching, if implemented, just on the left/right
+ -- of a keypoint would reveal suddenly more or less tiles, but we don't mind because the sudden changes
+ -- are done when the ground is at a high level, so they can't be experienced when running normally)
+ -- keypoints must be defined in order, from left to right, at each segment end
+ -- a keypoint = vector(camera_x, camera_bottom_limit_offset)
+ -- they have been deduced from playing Sonic 3 (& K) and crouching to see how far I can look down
+ camera_bottom_limit_margin_keypoints = {
+ vector(47, 11),
+ vector(104, 8),
+ vector(384, 0), -- normal stage bottom limit from tile 104 to tile (the original game limit there matches our true stage bottom)
+ -- 128 * 3 = 384 which is the start of region 31 (it's an approx of the original game since we cut the spiral tree)
+ vector(513, 15)
+ -- 128 * 4 + 1 = 513 which covers the whole region 31 (overkill as we don't use it completely, but safe)
+ -- 15 has been chosen to see 4 tiles below the ground level
+ -- usually it's 3 in Sonic 3, but because PICO-8 has a slightly bigger relative screen height,
+ -- only 3 would show a sudden 4px clamp when passing tile 384
+ -- of course we imitate Sonic 3 but adding tiles descending progressively from much higher than the bottom limit
+ -- to hide the sudden change of limit, but while Sonic 3 is fine with 7 tiles, PICO-8 can see up to 8 tiles below (without crouching)
+ -- so we need 8 tiles of margin from the highest platform in region 31, and that corresponds to 4 tiles under the lowest ground
+
+ -- there is actually yet another level in the original game, vector(188, -8) but it is to reveal the water area
+ -- which is below our stage bottom (hence negative -8),
+ -- so, since it was cut bottom for our adaptationate limit for us
+ },
+
+ -- layer data
+ -- all tile locations are global
+
+ loop_exit_areas = {
+ -- lower loop (read in region (1, 1))
+ -- location_rect(128 + 94, 32 + 12, 128 + 100, 32 + 22),
+ location_rect(222, 44, 228, 54),
+ -- upper loop 1 (read in region (2, 0))
+ -- location_rect(256 + 81, 20, 256 + 87, 30),
+ location_rect(337, 20, 343, 30),
+ -- upper loop 2 (read in region (2, 0) and (2, 1))
+ -- location_rect(256 + 105, 28, 256 + 111, 32 + 5),
+ location_rect(361, 28, 367, 37),
+ },
+
+ loop_entrance_areas = {
+ -- small loop (read in region (1, 1))
+ -- location_rect(128 + 101, 32 + 12, 128 + 106, 32 + 22),
+ location_rect(229, 44, 234, 54),
+ -- upper loop 1 (read in region (2, 0))
+ -- location_rect(256 + 88, 20, 256 + 93, 30),
+ location_rect(344, 20, 349, 30),
+ -- upper loop 2 (read in region (2, 0) and (2, 1))
+ -- location_rect(256 + 112, 28, 256 + 117, 32 + 5),
+ location_rect(368, 28, 373, 37),
}
-
}
-
}
return stage_data
diff --git a/src/data/tile_collision_data.lua b/src/data/tile_collision_data.lua
index 9025cc1f..926e1a78 100644
--- a/src/data/tile_collision_data.lua
+++ b/src/data/tile_collision_data.lua
@@ -7,35 +7,41 @@ local tile = {}
-- tile_collision_data makes it possible to check for collision with ground very easily
local tile_collision_data = new_struct()
--- mask_tile_id_loc sprite_id_location sprite location of the collision mask for this tile on the spritesheet
--- taken directly from raw_tile_collision_data
--- height_array [int] sequence of column heights of a tile collision mask per column index,
--- counting index from the left
--- if tile vertical interior is down, a column rises from the bottom (floor)
--- if tile vertical interior is up, a column falls from the top (ceiling)
--- width_array [int] sequence of row widths of a tile collision mask per row index,
--- counting index from the top
--- if tile horizontal interior is left, a row is filled from the left (left wall or desc slope)
--- if tile horizontal interior is up, a row is filled from the right (right wall or asc slope)
--- note: knowing height_array and knowing width_array is equivalent (reciprocity)
--- we simply store both for faster access
--- slope_angle float slope angle in turn ratio (0.0 to 1.0 excluded, positive clockwise)
--- it also determines the interior:
--- 0 to 0.25: horizontal interior right, vertical interior down (flat or asc slope)
--- 0.25 to 0.5: horizontal interior right, vertical interior up (ceiling right corner or flat)
--- 0.5 to 0.75: horizontal interior left, vertical interior up (ceiling flat or left corner)
--- 0.75 to 1: horizontal interior left, vertical interior down (desc slope or flat)
--- interior_v vertical_dirs vertical direction of the tile's interior
--- (up for ceiling, down for floor)
--- interior_h horizontal_dirs horizontal direction of the tile's interior
--- (left for desc slope or left ceiling, asc slope or right ceiling)
-function tile_collision_data:init(mask_tile_id_loc, height_array, width_array, slope_angle, interior_v, interior_h)
+-- mask_tile_id_loc sprite_id_location sprite location of the collision mask for this tile on the spritesheet
+-- taken directly from raw_tile_collision_data
+-- height_array [int] sequence of column heights of a tile collision mask per column index,
+-- counting index from the left
+-- if tile vertical interior is down, a column rises from the bottom (floor)
+-- if tile vertical interior is up, a column falls from the top (ceiling)
+-- width_array [int] sequence of row widths of a tile collision mask per row index,
+-- counting index from the top
+-- if tile horizontal interior is left, a row is filled from the left (left wall or desc slope)
+-- if tile horizontal interior is up, a row is filled from the right (right wall or asc slope)
+-- note: knowing height_array and knowing width_array is equivalent (reciprocity)
+-- we simply store both for faster access
+-- slope_angle float slope angle in turn ratio (0.0 to 1.0 excluded, positive clockwise)
+-- it also determines the interior:
+-- 0 to 0.25: horizontal interior right, vertical interior down (flat or asc slope)
+-- 0.25 to 0.5: horizontal interior right, vertical interior up (ceiling right corner or flat)
+-- 0.5 to 0.75: horizontal interior left, vertical interior up (ceiling flat or left corner)
+-- 0.75 to 1: horizontal interior left, vertical interior down (desc slope or flat)
+-- interior_v vertical_dirs vertical direction of the tile's interior
+-- (up for ceiling, down for floor)
+-- interior_h horizontal_dirs horizontal direction of the tile's interior
+-- (left for desc slope or left ceiling, asc slope or right ceiling)
+-- land_on_empty_qcolumn bool when true, character will land on an empty q-column with height 0 and same slope angle as other columns
+-- (unless falling in reverse direction i.e. from the interior)
+-- note that this only applies to the q-columns in the main quadrant direction (the q-down direction
+-- defined by the slope angle via world.angle_to_quadrant)
+-- default: nil (acts like false)
+function tile_collision_data:init(mask_tile_id_loc, height_array, width_array, slope_angle, interior_v, interior_h, land_on_empty_qcolumn)
self.mask_tile_id_loc = mask_tile_id_loc
self.height_array = height_array
self.width_array = width_array
self.slope_angle = slope_angle
self.interior_v = interior_v
self.interior_h = interior_h
+ self.land_on_empty_qcolumn = land_on_empty_qcolumn -- `or false` cut to spare characters, as nil behaves like false in bool tests
end
-- return the height for a column index starting at 0, from left to right
@@ -53,7 +59,7 @@ local function is_full_or_empty(array)
-- check if all values in array are 8 / any value is not 8
-- (there are no any/all helper functions yet, only contains with is any + ==)
- -- check columns first
+ -- check all columns/rows
for v in all(array) do
if v ~= 0 and v ~= 8 then
return false
@@ -79,17 +85,68 @@ function tile_collision_data:is_full_horizontal_rectangle()
return is_full_or_empty(self.width_array)
end
+-- return true iff tile is only made of empty columns, and columns with all the same non-zero height
+-- this is equivalent to having only empty rows, and rows with all the same non-zero width
+-- in practice, those columns/rows should be contiguous (else the row widths/column heights cannot be defined)
+-- and the tile is a rectangle (partial or full in any direction; also if empty, but that's a degenerated case)
+function tile_collision_data:is_rectangle()
+ -- if empty columns are considerer 0-height ground, then (reasonably) assuming we have at least one
+ -- non-empty column, we have technically 2 columns with different heights, so we're not a rectangle
+ -- this will allow very low slopes made of 1 row of pixel to be considered like rectangles,
+ -- preventing the usage of their manually defined slope angle
+ if self.land_on_empty_qcolumn then
+ return false
+ end
+
+ local non_empty_column_height_found
+
+ -- check columns, will be enough
+ for v in all(self.height_array) do
+ if v ~= 0 then
+ -- non-empty, we need to check if height matches all heights found so far
+ if non_empty_column_height_found then
+ -- it's not the first column found, let's compare with previous height found
+ if v ~= non_empty_column_height_found then
+ -- height mismatch, this cannot be a rectangle
+ return false
+ end
+ else
+ -- first column found, remember height for later
+ non_empty_column_height_found = v
+ end
+ end
+ end
+
+ return true
+end
+
+--#if debug_collision_mask
+function tile_collision_data:debug_render(global_tile_location)
+ -- note: this only works if we reloaded the collision mask sprites at the top of the spritesheet,
+ -- which means runtime sprites like emerald pick up FX will be messed up!
+ spr(self.mask_tile_id_loc:to_sprite_id(), tile_size * global_tile_location.i, tile_size * global_tile_location.j)
+end
+--#endif
+
-- return tuple (interior_v, interior_h) for a slope angle
+-- ! This function makes the slope angle actually important even for rectangular tiles
+-- ! unless it's a full square 8x8, you need to set the most meaningful angle so the interior
+-- ! corresponds to the side where the collision mask is filled (or the longest, if dealing
+-- ! with a partial rectangle on both sides), so the reverse + all-or-nothing check makes sense
+-- ! in world.compute_qcolumn_height_at (this means partial rectangles won't detect reverse in both
+-- ! directions, so avoid costruction a geometry where the character can touch the shortest side
+-- ! from the reverse direction).
+-- ! This really happened with vertical rectangle spring right mask, and was fixed with slope angle.
function tile_collision_data.slope_angle_to_interiors(slope_angle)
assert(slope_angle % 1 == slope_angle)
- -- in edge cases (square angles), interior direction is arbitrary
+ -- in edge cases (square angles 0, 0.25, 0.5, 0.75), one of the interior directions is arbitrary
local is_slope_angle_down = slope_angle < 0.25 or slope_angle >= 0.75
local interior_v = is_slope_angle_down and vertical_dirs.down or vertical_dirs.up
local interior_h = slope_angle < 0.5 and horizontal_dirs.right or horizontal_dirs.left
return interior_v, interior_h
end
-function tile_collision_data.from_raw_tile_collision_data(mask_tile_id, slope_angle)
+function tile_collision_data.from_raw_tile_collision_data(mask_tile_id, slope_angle, land_on_empty_qcolumn)
assert(slope_angle >= 0 and slope_angle < 1, "tile_collision_data.from_raw_tile_collision_data: slope_angle is "..slope_angle..", please apply `% 1` before passing")
-- we don't mind edge cases (slope angle at 0, 0.25, 0.5 or 0.75 exactly)
-- and assume the code will handle any arbitrary decision on interior_h/v
@@ -103,7 +160,8 @@ function tile_collision_data.from_raw_tile_collision_data(mask_tile_id, slope_an
tile_collision_data.read_width_array(mask_tile_id_loc, interior_h),
slope_angle,
interior_v,
- interior_h
+ interior_h,
+ land_on_empty_qcolumn
)
end
diff --git a/src/data/tile_collision_data_utest.lua b/src/data/tile_collision_data_utest.lua
index f1e37222..80ce457f 100644
--- a/src/data/tile_collision_data_utest.lua
+++ b/src/data/tile_collision_data_utest.lua
@@ -43,8 +43,8 @@ describe('tile_collision_data', function ()
describe('init', function ()
it('should create a tile_collision_data with reciprocal arrays and slope angle', function ()
- local tcd = tile_collision_data(10, {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4), horizontal_dirs.right, vertical_dirs.down)
- assert.are_same({10, {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4)}, {tcd.mask_tile_id_loc, tcd.height_array, tcd.width_array, tcd.slope_angle})
+ local tcd = tile_collision_data(sprite_id_location(1, 0), {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4), horizontal_dirs.right, vertical_dirs.down, true)
+ assert.are_same({sprite_id_location(1, 0), {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4), true}, {tcd.mask_tile_id_loc, tcd.height_array, tcd.width_array, tcd.slope_angle, tcd.land_on_empty_qcolumn})
end)
end)
@@ -52,7 +52,7 @@ describe('tile_collision_data', function ()
describe('get_height', function ()
it('should return the height at the given column index', function ()
- local tcd = tile_collision_data(10, {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4))
+ local tcd = tile_collision_data(sprite_id_location(1, 0), {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4))
assert.are_equal(2, tcd:get_height(2))
end)
@@ -61,7 +61,7 @@ describe('tile_collision_data', function ()
describe('get_width', function ()
it('should return the width at the given column index', function ()
- local tcd = tile_collision_data(10, {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4))
+ local tcd = tile_collision_data(sprite_id_location(1, 0), {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4))
assert.are_equal(2, tcd:get_width(4))
end)
@@ -74,42 +74,101 @@ describe('tile_collision_data', function ()
-- check the utests for is_full_horizontal_rectangle, which
-- define the same tiles!
- it('should return true when empty', function ()
- local tcd = tile_collision_data(20, {0, 0, 0, 0, 0, 0, 0, 0}, {}, 0)
+ it('should return true when empty (abnormal case though)', function ()
+ local tcd = tile_collision_data(sprite_id_location(2, 0), {0, 0, 0, 0, 0, 0, 0, 0}, "unused", 0)
assert.is_true(tcd:is_full_vertical_rectangle())
end)
it('should return true when made of empty/full columns', function ()
- local tcd = tile_collision_data(30, {8, 8, 8, 0, 0, 0, 0, 0}, {}, 0.75)
+ local tcd = tile_collision_data(sprite_id_location(3, 0), {8, 8, 8, 0, 0, 0, 0, 0}, "unused", 0.75)
assert.is_true(tcd:is_full_vertical_rectangle())
end)
it('should return false when not made of empty/full columns only', function ()
- local tcd = tile_collision_data(40, {4, 4, 4, 4, 3, 3, 3, 3}, {}, atan2(8, -1))
+ local tcd = tile_collision_data(sprite_id_location(4, 0), {4, 4, 4, 4, 3, 3, 3, 3}, "unused", atan2(8, -1))
assert.is_false(tcd:is_full_vertical_rectangle())
end)
end)
- describe('is_full_horizontal_rectangle', function ()
+ describe('is_full_horizontal_rectangle (abnormal case though)', function ()
+
+ -- same, no need to define columns
it('should return true when empty', function ()
- local tcd = tile_collision_data(50, {}, {0, 0, 0, 0, 0, 0, 0, 0}, 0)
+ local tcd = tile_collision_data(sprite_id_location(5, 0), "unused", {0, 0, 0, 0, 0, 0, 0, 0}, 0)
assert.is_true(tcd:is_full_horizontal_rectangle())
end)
it('should return true when made of empty/full rows', function ()
- local tcd = tile_collision_data(60, {}, {0, 0, 0, 0, 0, 8, 8, 8}, 0)
+ local tcd = tile_collision_data(sprite_id_location(6, 0), "unused", {0, 0, 0, 0, 0, 8, 8, 8}, 0)
assert.is_true(tcd:is_full_horizontal_rectangle())
end)
it('should return false when not made of empty/full rows only', function ()
- local tcd = tile_collision_data(70, {}, {0, 0, 0, 0, 4, 8, 8, 8}, atan2(8, -1))
+ local tcd = tile_collision_data(sprite_id_location(7, 0), "unused", {0, 0, 0, 0, 4, 8, 8, 8}, atan2(8, -1))
assert.is_false(tcd:is_full_horizontal_rectangle())
end)
end)
+ describe('is_rectangle', function ()
+
+ -- we're only testing columns, so to simplify we don't define rows
+
+ it('should return true when empty (abnormal case though)', function ()
+ local tcd = tile_collision_data(sprite_id_location(5, 0), {0, 0, 0, 0, 0, 0, 0, 0}, "unused", 0, vertical_dirs.down, horizontal_dirs.right)
+ assert.is_true(tcd:is_rectangle())
+ end)
+
+ it('should return true when made of empty/full columns', function ()
+ local tcd = tile_collision_data(sprite_id_location(6, 0), {0, 0, 0, 0, 0, 8, 8, 8}, "unused", 0, vertical_dirs.down, horizontal_dirs.right)
+ assert.is_true(tcd:is_rectangle())
+ end)
+
+ it('should return true when made of empty/partial columns, but all of same height', function ()
+ local tcd = tile_collision_data(sprite_id_location(6, 0), {5, 5, 5, 5, 5, 0, 0, 0}, "unused", 0, vertical_dirs.down, horizontal_dirs.right)
+ assert.is_true(tcd:is_rectangle())
+ end)
+
+ it('should return false when made of at least two columns of different heights', function ()
+ local tcd = tile_collision_data(sprite_id_location(7, 0), {0, 0, 0, 0, 4, 7, 7, 7}, "unused", atan2(8, -1), vertical_dirs.down, horizontal_dirs.right)
+ assert.is_false(tcd:is_rectangle())
+ end)
+
+ it('should return true when made of empty/partial columns all of same height, but land_on_empty_qcolumn is true', function ()
+ local tcd = tile_collision_data(sprite_id_location(6, 0), {1, 1, 1, 1, 1, 1, 0, 0}, "unused", 0, true, vertical_dirs.down, horizontal_dirs.right, true)
+ assert.is_false(tcd:is_rectangle())
+ end)
+
+ end)
+
+ describe('(#debug_collision_mask) debug_render', function ()
+
+ setup(function ()
+ stub(_G, "spr")
+ end)
+
+ teardown(function ()
+ spr:revert()
+ end)
+
+ after_each(function ()
+ spr:clear()
+ end)
+
+ it('should call spr with mask_tile_id', function ()
+ -- sprite id = 16 + 1 = 17, args after the 1st one don't matter
+ local tcd = tile_collision_data(sprite_id_location(1, 1), "unused", "unused", 0, vertical_dirs.down, horizontal_dirs.right)
+
+ tcd:debug_render(location(2, 4))
+
+ assert.spy(spr).was_called(1)
+ assert.spy(spr).was_called_with(17, 8 * 2, 8 * 4)
+ end)
+
+ end)
+
describe('slope_angle_to_interiors', function ()
-- cardinals
@@ -179,20 +238,20 @@ describe('tile_collision_data', function ()
tile_collision_data.read_width_array:revert()
end)
- it('should return a tile_collision_data containing (mock tile 1) height/width array, slope angle, derived interior directions', function ()
+ it('should return a tile_collision_data containing (mock tile 1) height/width array, slope angle, derived interior directions, land_on_empty_qcolumn (default: nil)', function ()
local tcd = tile_collision_data.from_raw_tile_collision_data(1, atan2(8, -4))
-- struct equality with are_equal would work, we just use are_same to benefit from diff asterisk provided by luassert
- assert.are_same(tile_collision_data(sprite_id_location(1, 0), {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4), vertical_dirs.down, horizontal_dirs.right), tcd)
+ assert.are_same(tile_collision_data(sprite_id_location(1, 0), {0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4), vertical_dirs.down, horizontal_dirs.right, nil), tcd)
end)
- it('should return a tile_collision_data containing (mock tile 2) height/width array, slope angle, derived interior directions', function ()
+ it('should return a tile_collision_data containing (mock tile 2) height/width array, slope angle, derived interior directions, land_on_empty_qcolumn (default: nil)', function ()
local tcd = tile_collision_data.from_raw_tile_collision_data(2, atan2(-8, 8))
- assert.are_same(tile_collision_data(sprite_id_location(2, 0), {8, 6, 4, 3, 2, 2, 1, 1}, {8, 6, 4, 3, 2, 2, 1, 1}, atan2(-8, 8), vertical_dirs.up, horizontal_dirs.left), tcd)
+ assert.are_same(tile_collision_data(sprite_id_location(2, 0), {8, 6, 4, 3, 2, 2, 1, 1}, {8, 6, 4, 3, 2, 2, 1, 1}, atan2(-8, 8), vertical_dirs.up, horizontal_dirs.left, nil), tcd)
end)
- it('should return a tile_collision_data containing (mock tile 3) height/width array, slope angle, derived interior directions', function ()
- local tcd = tile_collision_data.from_raw_tile_collision_data(2, atan2(-8, -8))
- assert.are_same(tile_collision_data(sprite_id_location(2, 0), {8, 6, 4, 3, 2, 2, 1, 1}, {8, 6, 4, 3, 2, 2, 1, 1}, atan2(-8, -8), vertical_dirs.up, horizontal_dirs.right), tcd)
+ it('should return a tile_collision_data containing (mock tile 3) height/width array, slope angle, derived interior directions, land_on_empty_qcolumn', function ()
+ local tcd = tile_collision_data.from_raw_tile_collision_data(2, atan2(-8, -8), true)
+ assert.are_same(tile_collision_data(sprite_id_location(2, 0), {8, 6, 4, 3, 2, 2, 1, 1}, {8, 6, 4, 3, 2, 2, 1, 1}, atan2(-8, -8), vertical_dirs.up, horizontal_dirs.right, true), tcd)
end)
end)
diff --git a/src/ingame/base_stage_state.lua b/src/ingame/base_stage_state.lua
index 31a3440c..0ca40c20 100644
--- a/src/ingame/base_stage_state.lua
+++ b/src/ingame/base_stage_state.lua
@@ -1,6 +1,7 @@
local gamestate = require("engine/application/gamestate")
local camera_class = require("ingame/camera")
+local player_char = require("ingame/playercharacter")
local visual = require("resources/visual_common")
-- abstract base class for stage_state, stage_intro_state and stage_clear_state
@@ -20,6 +21,10 @@ function base_stage_state:init()
-- used to draw the palm tree extension sprites on foreground
self.palm_tree_leaves_core_global_locations = {}
--#endif
+
+-- don't initialize loaded region coords (we don't know in which region player character will spawn),
+-- each child class on_enter will set them in on_enter
+-- self.loaded_map_region_coords = nil
end
@@ -28,7 +33,12 @@ end
-- return map filename for current stage and given region coordinates (u: int, v: int)
-- do not try this with transitional regions, instead we'll patch them from individual regions
function base_stage_state:get_map_region_filename(u, v)
- return "data_stage"..self.curr_stage_id.."_"..u..v..cartridge_ext
+ return "data_stage"..self.curr_stage_id.."_"..u..v..".p8"
+end
+
+-- this one is used by #stage_clear
+function base_stage_state:region_to_global_location(region_loc)
+ return region_loc + self:get_region_topleft_location()
end
@@ -40,10 +50,20 @@ function base_stage_state:global_to_region_location(global_loc)
return global_loc - self:get_region_topleft_location()
end
-function base_stage_state:region_to_global_location(region_loc)
- return region_loc + self:get_region_topleft_location()
+
+-- spawn
+
+-- spawn the player character at the stage spawn location
+function base_stage_state:spawn_player_char()
+ -- note we switched from center to topleft position because it gave better initial positions
+ -- (with ground bumps, center was higher in the air or too deep inside ground, while topleft
+ -- was just 1px from the surface, allowing immediate escape from ground)
+ local spawn_position = self.curr_stage_data.spawn_location:to_topleft_position()
+ self.player_char = player_char()
+ self.player_char:spawn_at(spawn_position)
end
+
-- queries
-- return true iff global_tile_loc: location is in any of the areas: {location_rect}
@@ -127,33 +147,96 @@ end
-- render
+--#ifn itest
+
+local waterfall_color_cycle = {
+ -- original colors : dark_blue, indigo, blue, white
+ {colors.dark_blue, colors.blue, colors.blue, colors.white},
+ {colors.white, colors.dark_blue, colors.blue, colors.blue},
+ {colors.blue, colors.white, colors.dark_blue, colors.blue},
+ {colors.blue, colors.blue, colors.white, colors.dark_blue},
+}
+
+function base_stage_state:set_color_palette_for_waterfall_animation()
+ local period = 0.5
+ local ratio = (t() % period) / period
+ local step_count = #waterfall_color_cycle
+ local step = min(flr(ratio * step_count) + 1, step_count)
+ local new_colors = waterfall_color_cycle[step]
+ swap_colors({colors.dark_blue, colors.indigo, colors.blue, colors.white}, new_colors)
+end
+
+--#endif
+
-- render the stage environment (tiles)
function base_stage_state:render_environment_midground()
+ self:set_camera_with_region_origin()
+ self:render_environment_midground_static()
+ self:render_environment_midground_waterfall()
+end
+
+-- render the stage environment (tiles)
+function base_stage_state:render_environment_midground_static()
+ set_unique_transparency(colors.pink)
+
+ -- only draw midground tiles that don't need waterfall color swapping animation
+ -- note that we are drawing loop entrance tiles even though they will be (they'll be drawn on foreground later)
-- possible optimize: don't draw the whole stage offset by camera,
-- instead just draw the portion of the level of interest
-- (and either keep camera offset or offset manually and subtract from camera offset)
-- that said, I didn't notice a performance drop by drawing the full tilemap
-- so I guess map is already optimized to only draw what's on camera
- set_unique_transparency(colors.pink)
-
- -- only draw midground tiles
- -- note that we are drawing loop entrance tiles even though they will be (they'll be drawn on foreground later)
- self:set_camera_with_region_origin()
map(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.midground)
end
+-- render the stage environment (tiles)
+function base_stage_state:render_environment_midground_waterfall()
+--#ifn itest
+ -- waterfall sprites are now placed as tiles of the tilemap, so we apply the waterfall color swap animation
+ -- directly on them
+ self:set_color_palette_for_waterfall_animation()
+--#endif
+
+ -- only draw midground tiles that need waterfall color swapping animation
+ map(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.waterfall)
+
+--#ifn itest
+ -- clear palette swap, or Sonic (and rocks, etc.) will inherit from the waterfall blue color swapping!
+ pal()
+--#endif
+end
+
function base_stage_state:render_environment_foreground()
+--#ifn itest
set_unique_transparency(colors.pink)
-- draw tiles always on foreground first
self:set_camera_with_region_origin()
map(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.foreground)
- local region_topleft_loc = self:get_region_topleft_location()
+ -- CARTRIDGE NOTE: currently objects are not scanned in stage_intro, and there are no
+ -- loops nor palm trees at stage start anyway. Stage clear doesn't have them at stage end either.
+ -- stage_clear will error on nil self.curr_stage_data anyway, so just skip the whole operation
+ -- if stage intro or stage clear.
+ -- Headless itests will use #busted + state type check, while PICO-8 will rely on #ingame.
+ -- We used to test for self.curr_stage_data being not nil directly to pass utests,
+ -- then removed it as we removed utests on base methods, then revived the #busted check
+ -- for headless itests with render but we prefer checking state type now, as it really matches
+ -- the #ingame check below.
+
+--#if busted
+ if self.type ~= ':stage' then
+ return
+ end
+--#endif
+
+--#if ingame
-- draw loop entrances on the foreground (it was already drawn on the midground, so we redraw on top of it;
-- it's ultimately more performant to draw twice than to cherry-pick, in case loop entrance tiles
-- are reused in loop exit or other possibly disabled layers so we cannot just tag them all foreground)
+ local region_topleft_loc = self:get_region_topleft_location()
+
self:set_camera_with_origin()
for area in all(self.curr_stage_data.loop_entrance_areas) do
-- draw map subset just for the loop entrance
@@ -164,9 +247,6 @@ function base_stage_state:render_environment_foreground()
sprite_masks.midground)
end
- -- CARTRIDGE NOTE: currently objects are not scanned in stage_intro, and there are no
- -- palm trees at stage start anyway. Stage clear doesn't have them at stage end either.
---#if ingame
-- draw palm tree extension sprites on the foreground, so they can hide the character and items at the top
for global_loc in all(self.palm_tree_leaves_core_global_locations) do
-- top has pivot at its bottom-left = the top-left of the core
@@ -177,6 +257,11 @@ function base_stage_state:render_environment_foreground()
-- left is mirrored from right, so its pivot is at its bottom-right = the top-left of the core
visual.sprite_data_t.palm_tree_leaves_right:render(global_loc:to_topleft_position(), --[[flip_x:]] true)
end
+
+--(ingame)
+--#endif
+
+--(!itest)
--#endif
end
diff --git a/src/ingame/base_stage_state_utest.lua b/src/ingame/base_stage_state_utest.lua
index 6fffe7cf..beb2db32 100644
--- a/src/ingame/base_stage_state_utest.lua
+++ b/src/ingame/base_stage_state_utest.lua
@@ -11,6 +11,7 @@ local sprite_data = require("engine/render/sprite_data")
-- same remark as for bustedhelper, we just pick picosonic_app_ingame for convenience
local picosonic_app = require("application/picosonic_app_ingame")
local camera_class = require("ingame/camera")
+local player_char = require("ingame/playercharacter")
local visual = require("resources/visual_common")
local tile_repr = require("test_data/tile_representation")
local tile_test_data = require("test_data/tile_test_data")
@@ -49,13 +50,13 @@ describe('base_base_stage_state', function ()
describe('set_camera_with_origin', function ()
it('should set the pico8 camera so that it is centered on the camera position, with origin (0, 0) by default', function ()
- state.camera.position = vector(24, 13)
+ state.camera:init_position(vector(24, 13))
state:set_camera_with_origin()
assert.are_same(vector(24 - 128 / 2, 13 - 128 / 2), vector(pico8.camera_x, pico8.camera_y))
end)
it('should set the pico8 camera so that it is centered on the camera position, with custom origin subtracted', function ()
- state.camera.position = vector(24, 13)
+ state.camera:init_position(vector(24, 13))
state:set_camera_with_origin(vector(10, 20))
assert.are_same(vector(24 - 128 / 2 - 10, 13 - 128 / 2 - 20), vector(pico8.camera_x, pico8.camera_y))
end)
@@ -88,17 +89,51 @@ describe('base_base_stage_state', function ()
end)
+ describe('spawn_player_char', function ()
+
+ setup(function ()
+ stub(player_char, "spawn_at")
+ end)
+
+ teardown(function ()
+ player_char.spawn_at:revert()
+ end)
+
+ before_each(function ()
+ -- clear count before test as entering stage will auto-spawn character once
+ player_char.spawn_at:clear()
+
+ -- dummy data for test to work
+ state.curr_stage_data = {
+ spawn_location = location(5, 47)
+ }
+ end)
+
+ it('should spawn the player character at the stage spawn location', function ()
+ state:spawn_player_char()
+
+ local player_char = state.player_char
+ assert.is_not_nil(player_char)
+
+ local spawn_position = state.curr_stage_data.spawn_location:to_topleft_position()
+
+ assert.spy(player_char.spawn_at).was_called(1)
+ assert.spy(player_char.spawn_at).was_called_with(match.ref(state.player_char), spawn_position)
+ end)
+
+ end)
+
describe('is_tile_in_area', function ()
it('should return true for tile in one of the entrance areas', function ()
- -- this depends on stage_data.for_stage[1].loop_entrance_areas content and
+ -- this depends on stage_data[1].loop_entrance_areas content and
-- location_rect:contains correctness
assert.is_true(state:is_tile_in_area(location(4, 4),
{location_rect(0, 0, 2, 2), location_rect(4, 4, 6, 6)}))
end)
it('should return false for tile not in any of the entrance areas', function ()
- -- this depends on stage_data.for_stage[1].loop_entrance_areas content and
+ -- this depends on stage_data[1].loop_entrance_areas content and
-- location_rect:contains correctness
assert.is_true(state:is_tile_in_area(location(5, 5),
{location_rect(0, 0, 2, 2), location_rect(4, 4, 6, 6)}))
@@ -205,26 +240,68 @@ describe('base_base_stage_state', function ()
-- render
- describe('(with tile_test_data)', function ()
+
+ describe('render_environment_midground', function ()
setup(function ()
tile_test_data.setup()
- stub(base_stage_state, "set_camera_with_origin")
stub(base_stage_state, "set_camera_with_region_origin")
+ stub(base_stage_state, "render_environment_midground_static")
+ stub(base_stage_state, "render_environment_midground_waterfall")
+ end)
+
+ teardown(function ()
+ base_stage_state.set_camera_with_region_origin:revert()
+ base_stage_state.render_environment_midground_static:revert()
+ base_stage_state.render_environment_midground_waterfall:revert()
+ end)
+
+ after_each(function ()
+ pico8:clear_map()
+
+ base_stage_state.set_camera_with_region_origin:clear()
+ base_stage_state.render_environment_midground_static:clear()
+ base_stage_state.render_environment_midground_waterfall:clear()
+ end)
+
+ it('should call set_camera_with_region_origin, render_environment_midground_static, render_environment_midground_waterfall', function ()
+ state:render_environment_midground()
+
+ assert.spy(base_stage_state.set_camera_with_region_origin).was_called(1)
+ assert.spy(base_stage_state.set_camera_with_region_origin).was_called_with(match.ref(state))
+
+ assert.spy(base_stage_state.render_environment_midground_static).was_called(1)
+ assert.spy(base_stage_state.render_environment_midground_static).was_called_with(match.ref(state))
+
+ assert.spy(base_stage_state.render_environment_midground_waterfall).was_called(1)
+ assert.spy(base_stage_state.render_environment_midground_waterfall).was_called_with(match.ref(state))
+ end)
+
+ end)
+
+ describe('(with tile_test_data)', function ()
+
+ setup(function ()
+ stub(base_stage_state, "set_color_palette_for_waterfall_animation")
+ stub(base_stage_state, "set_camera_with_region_origin")
+ stub(base_stage_state, "set_camera_with_origin")
stub(sprite_data, "render")
stub(_G, "spr")
stub(_G, "map")
+ stub(_G, "set_unique_transparency")
end)
teardown(function ()
tile_test_data.teardown()
- base_stage_state.set_camera_with_origin:revert()
+ base_stage_state.set_color_palette_for_waterfall_animation:revert()
base_stage_state.set_camera_with_region_origin:revert()
+ base_stage_state.set_camera_with_origin:revert()
sprite_data.render:revert()
spr:revert()
map:revert()
+ set_unique_transparency:revert()
end)
before_each(function ()
@@ -253,31 +330,51 @@ describe('base_base_stage_state', function ()
after_each(function ()
pico8:clear_map()
- base_stage_state.set_camera_with_origin:clear()
+ base_stage_state.set_color_palette_for_waterfall_animation:clear()
base_stage_state.set_camera_with_region_origin:clear()
+ base_stage_state.set_camera_with_origin:clear()
sprite_data.render:clear()
spr:clear()
map:clear()
+ set_unique_transparency:clear()
end)
- it('render_environment_midground should call map for all midground sprites', function ()
+ it('render_environment_midground_static should call set_unique_transparency and map for all midground sprites', function ()
-- note that we reverted to using map for performance, so this test doesn't need to be
-- in the tile test data setup context anymore
- state.camera.position = vector(0, 0)
+ state.camera:init_position(vector(0, 0))
state.loaded_map_region_coords = vector(0, 0)
- state:render_environment_midground()
+ state:render_environment_midground_static()
- assert.spy(base_stage_state.set_camera_with_region_origin).was_called(1)
- assert.spy(base_stage_state.set_camera_with_region_origin).was_called_with(match.ref(state))
+ assert.spy(set_unique_transparency).was_called(1)
+ assert.spy(set_unique_transparency).was_called_with(colors.pink)
assert.spy(map).was_called(1)
assert.spy(map).was_called_with(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.midground)
end)
- it('render_environment_foreground should call spr on tiles present on screen', function ()
+ it('render_environment_midground_waterfall should call set_color_palette_for_waterfall_animation and map for all waterfall sprites', function ()
+ -- note that we reverted to using map for performance, so this test doesn't need to be
+ -- in the tile test data setup context anymore
+ state.camera:init_position(vector(0, 0))
+ state.loaded_map_region_coords = vector(0, 0)
+
+ state:render_environment_midground_waterfall()
+
+ assert.spy(base_stage_state.set_color_palette_for_waterfall_animation).was_called(1)
+ assert.spy(base_stage_state.set_color_palette_for_waterfall_animation).was_called_with(match.ref(state))
+
+ assert.spy(map).was_called(1)
+ assert.spy(map).was_called_with(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.waterfall)
+ end)
+
+ it('(ingame state) render_environment_foreground should call spr on tiles present on screen', function ()
+ -- simulate an ingame state
+ state.type = ':stage'
+
-- this test was copy-pasted from render_environment_midground
- state.camera.position = vector(0, 0)
+ state.camera:init_position(vector(0, 0))
state.loaded_map_region_coords = vector(2, 1)
state:render_environment_foreground()
@@ -309,6 +406,25 @@ describe('base_base_stage_state', function ()
assert.spy(sprite_data.render).was_called_with(match.ref(visual.sprite_data_t.palm_tree_leaves_right), vector(8 * 10, 8 * 2), true)
end)
+ it('(non-ingame state) render_environment_foreground should call spr on tiles present on screen', function ()
+ -- simulate a non-ingame state
+ state.type = ':stage_clear'
+
+ -- this test was copy-pasted from render_environment_midground
+ state.camera:init_position(vector(0, 0))
+ state.loaded_map_region_coords = vector(2, 1)
+
+ state:render_environment_foreground()
+
+ -- we can't check call order, but set camera methods should be called consistently with map!
+ assert.spy(base_stage_state.set_camera_with_region_origin).was_called(1)
+ assert.spy(base_stage_state.set_camera_with_region_origin).was_called_with(match.ref(state))
+
+ assert.spy(map).was_called(1)
+
+ assert.spy(map).was_called_with(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.foreground)
+ end)
+
end) -- (with tile_test_data)
end) -- (with instance)
diff --git a/src/ingame/camera.lua b/src/ingame/camera.lua
index 5b7e5125..3fb4de64 100644
--- a/src/ingame/camera.lua
+++ b/src/ingame/camera.lua
@@ -1,8 +1,6 @@
local camera_data = require("data/camera_data")
---#if cheat
-local player_char = require("ingame/playercharacter")
---#endif
+local pc_data = require("data/playercharacter_numerical_data")
local camera_class = new_class()
@@ -17,7 +15,7 @@ function camera_class:init()
-- camera forward offset (px, signed)
-- this intermediate value needs to be stored because it follows its own catchup over time
- self.forward_offset = 0
+ self.forward_signed_offset = 0
-- store last grounded orientation of character to keep using it while it is airborne
self.last_grounded_orientation = horizontal_dirs.right
@@ -29,44 +27,98 @@ function camera_class:init()
-- it is confirmed and can be applied to forward base offset
self.confirmed_orientation = horizontal_dirs.right
---#if busted
- -- intermediate values that are much easier to test when isolated, but don't need to be stored
- -- at runtime (only set in update, not in setup)
- self.base_position_x = 0
---#endif
+ -- time since crouching, incremented until we reach frames_before_look_down,
+ -- and reset when leaving crouching (frames)
+ self.frames_since_crouching = 0
+
+ -- current offset of look down (positive when camera moves down) (px)
+ self.look_down_offset = 0
+
+ -- base position: must be stored to track current position without offset
+ -- which moves smoothly at catch up speeds on X and Y toward windowed positions,
+ -- before we apply the offsets (which have their own smoothing and catchup speeds)
+ self.base_position = vector.zero()
+
+ -- time during which camera targets the old spin dash start position instead of Sonic's actual position
+ self.spin_dash_lag_timer = 0
+
+ -- registered position where character was spin dashing (initializing is useful to allow copy_assign)
+ self.spin_dashing_position = vector.zero()
+
+ -- registered orientation when character was spin dashing (commented out to spare characters, make sure
+ -- to assign it at the same time as spin_dashing_position)
+ -- self.spin_dashing_orientation = nil
end
-- setup camera for stage data
function camera_class:setup_for_stage(data)
-- store ref for later
- self.stage_data = data
+ self.curr_stage_data = data
-- warp the camera to spawn location (anywhere in the starting region will be enough
-- so the tilemap region is loaded properly for collision detection; but centering it
-- on the character first makes sense, since with the window system several positions are possible)
- self.position = data.spawn_location:to_center_position()
+ -- topleft position is passed to match behavior of stage_state:spawn_player_char
+ self:init_position(data.spawn_location:to_topleft_position())
-- prepare forward base offset set for future character (we assume it will be facing right)
-- just so camera doesn't move just on start (this is quick, would probably
-- be covered by stage splash screen eventually)
-- note that we don't need to also add this offset (as a vector) to self.position,
- -- since it will be updated with forward_offset next frame (right now, position is really
+ -- since it will be updated with forward_signed_offset next frame (right now, position is really
-- set just for the initial region loading, so only the approx. location matters)
- self.forward_offset = camera_data.forward_distance
+ self.forward_signed_offset = camera_data.forward_distance
+end
+
+-- initialize camera position
+-- only used by setup_for_stage at runtime, it was useful to extract for the various utests
+-- so they could start with a base position y matching the initial position
+function camera_class:init_position(initial_position)
+ -- copy by value to avoid changing initial_position in case it was not just a temporary value
+ -- we used to copy by reference, and this triggered the infamous bug of debug mode -> exit debug mode
+ -- inside ground -> character moves in a linear fashion due to camera position being the same as character
+ -- position!
+ self.position:copy_assign(initial_position)
+ -- immediately sync base position x/y, which needs a starting point
+ -- note that this is not the final position, as x will need to receive the base forward offset
+ -- for current character orientation; but currently this happens immediately on stage start, so not perceptible
+ -- in utests though, you may need to set self.base_position.x to pure base position on X
+ -- (subtract signed base forward offset) depending on what you are testing
+ self.base_position.x = initial_position.x
+ self.base_position.y = initial_position.y
end
+-- --#if busted
+-- -- helper for utests, as it's sometimes easier to work directly with camera on final position
+-- -- so we should subtract the base forward offset from the initial position on X
+-- -- but we need target_pc which we don't have at first during stage setup, so not used in runtime
+-- function camera_class:init_position_including_base_forward_offset(initial_position)
+-- assert(self.target_pc)
+-- self.position = initial_position
+-- -- immediately sync base position x/y, which needs a starting point, to the expected
+-- -- static position after waiting an infinite amount of time
+-- -- x needs to receive the base forward offset for current character orientation
+-- self.base_position.x = initial_position.x - camera_data.forward_distance * horizontal_dir_signs[self.target_pc.orientation]
+-- self.base_position.y = initial_position.y
+-- end
+-- --#endif
+
-- update camera position based on player character position
function camera_class:update()
--#if cheat
if self.target_pc.motion_mode == motion_modes.debug then
-- in debug motion, just track the character (otherwise he may move too fast vertically
-- and lost the camera)
- self.position = self.target_pc.position
+ -- init_position seems weird here, but it's just to update base_position.y
+ -- so the camera doesn't start from a different Y again when leaving debug motion mode
+ self:init_position(self.target_pc.position)
return
end
-- else: self.motion_mode == motion_modes.platformer
--#endif
+ -- this part is done before spin dash lag fictive target creation, because we really care about
+ -- the actual character state for ground orientation tracking
local should_reset_ground_orientation_timer = false
if self.target_pc:is_grounded() then
if self.last_grounded_orientation ~= self.target_pc.orientation then
@@ -84,30 +136,102 @@ function camera_class:update()
end
end
+ -- update frames_since_crouching (increment or reset)
+ if self.target_pc.motion_state == motion_states.crouching then
+ if self.frames_since_crouching >= camera_data.frames_before_look_down then
+ -- move camera down at given speed until limit
+ self.look_down_offset = min(self.look_down_offset + camera_data.look_down_speed, camera_data.max_look_down_distance)
+ else
+ -- we haven't crouched for long enough, increment frame counter
+ -- note that we are not increasing look down offset this frame, so when frames_before_look_down is 1,
+ -- we effectively wait 1 frame before starting looking down
+ self.frames_since_crouching = self.frames_since_crouching + 1
+ end
+ else
+ -- reset frame counter immediately in case it was positive
+ self.frames_since_crouching = 0
+
+ -- move camera back up at same speed as when moving down, until neutral position
+ -- note that due to level bottom limit clamping, when crouching near the bottom limit,
+ -- we won't see the camera move back up before a small delay
+ self.look_down_offset = max(0, self.look_down_offset - camera_data.look_down_speed)
+ end
+
+ -- check for spin dash lag
+ if self.target_pc.motion_state == motion_states.spin_dashing then
+ -- setup timer in case it wasn't already
+ self.spin_dash_lag_timer = camera_data.spin_dash_lag_duration
+ -- remember position and orientation of spin dash start
+ self.spin_dashing_position:copy_assign(self.target_pc.position)
+ self.spin_dashing_orientation = self.target_pc.orientation
+ elseif self.spin_dash_lag_timer > 0 then
+ -- we are not preparing spin dash anymore (now rolling, or even in another state after launching)
+ -- decrement timer
+ self.spin_dash_lag_timer = self.spin_dash_lag_timer - 1
+ end
+
+ local target_pos
+ local target_velocity_x
+ local is_target_grounded
+ local target_orientation
+
+ if self.spin_dash_lag_timer > 0 then
+ -- spin dash lag is active, set fictive grounded target at previous spin dash position and orientation,
+ -- velocity 0
+ -- note that we're a variant of the simplified version of spin dash lag suggested at the end of
+ -- http://info.sonicretro.org/SPG:Camera#Spindash_Lag
+ -- so we just target the old spin dash position after launch (almost like freezing camera, except if camera
+ -- had some motion to catch up from before spin dash start, we allow it to do so), and wait 16 frames
+ -- before tracking actual player character again (instead of recording 32 frames of motion and catching them
+ -- up 2 by 2)
+ -- also, we currently don't subtract spin dash rev (actually rev/2 since we start timer at 16 instead of 32)
+ -- from the initial timer; do it if you feel that lag is too strong when rev is high
+ -- copy reference is safe here, we won't be modifying target_pos
+ target_pos = self.spin_dashing_position
+ target_velocity_x = 0
+ is_target_grounded = true
+ -- overwrite target_orientation temporarily but *preserve* self.confirmed_orientation
+ -- we actually updated self.confirmed_orientation at the top, checking actual self.target_pc:is_grounded()
+ -- rather than is_target_grounded on purpose, so we keep track of the real character orientation
+ -- in the background -> when spin dash lag is over, we can immediately switch to the correct
+ -- Base Forward offset (e.g. spin dashing right against a wall, then immediately turning left,
+ -- will still give a reactive camera)
+ target_orientation = self.spin_dashing_orientation
+ else
+ -- use actual character position
+ -- copy reference is safe here, we won't be modifying target_pos
+ target_pos = self.target_pc.position
+ target_velocity_x = self.target_pc.velocity.x
+ is_target_grounded = self.target_pc:is_grounded()
+ target_orientation = self.confirmed_orientation
+ end
+
-- Window system: most of the time, only move camera when character
-- is leaving the central window
-- X tracking
-- Window system
- -- clamp to required window
- -- Be sure to use the non-forward-offset camera position X by subtracting the old
- -- self.forward_offset
- -- (if you subtract self.forward_offset after its update below,
- -- result will change slightly)
- local windowed_camera_x = mid(self.position.x - self.forward_offset,
- self.target_pc.position.x - camera_data.window_half_width,
- self.target_pc.position.x + camera_data.window_half_width)
-
---#if busted
- self.base_position_x = windowed_camera_x
---#endif
+ -- Target base X is clamped to required window, but we move there smoothly with max catchup speed
+ -- to avoid warping the camera
+ local target_base_x = mid(self.base_position.x,
+ target_pos.x - camera_data.window_half_width,
+ target_pos.x + camera_data.window_half_width)
+
+ local dx = target_base_x - self.base_position.x
+
+ -- clamp abs dx with catchup speed
+ dx = sgn(dx) * min(abs(dx), camera_data.catchup_speed_x)
+
+ -- apply move to base x
+ self.base_position.x = self.base_position.x + dx
+
-- Forward offset system
-- # Base
- local forward_base_offset = camera_data.forward_distance * horizontal_dir_signs[self.confirmed_orientation]
+ local forward_base_offset = camera_data.forward_distance * horizontal_dir_signs[target_orientation]
-- # Extension
@@ -127,70 +251,98 @@ function camera_class:update()
-- at which they activate forward extension)
-- (the original game uses ground speed not velocity X so it doesn't have this issue)
local range = camera_data.max_forward_ext_speed_x - camera_data.forward_ext_min_speed_x
- local ratio = mid(0, 1, (abs(self.target_pc.velocity.x) - camera_data.forward_ext_min_speed_x) / range)
+ local ratio = mid(0, 1, (abs(target_velocity_x) - camera_data.forward_ext_min_speed_x) / range)
-- remember that our offset is signed to allow left/right transitions
- local forward_ext_offset = sgn(self.target_pc.velocity.x) * ratio * camera_data.forward_ext_max_distance
+ local forward_ext_offset = sgn(target_velocity_x) * ratio * camera_data.forward_ext_max_distance
-- Combine both
- local target_forward_offset = forward_base_offset + forward_ext_offset
+ local target_forward_signed_offset = forward_base_offset + forward_ext_offset
-- compute delta to target
- local forward_dx = target_forward_offset - self.forward_offset
+ local forward_dx = target_forward_signed_offset - self.forward_signed_offset
-- clamp abs forward_dx with catchup speed
- forward_dx = sgn(forward_dx) * min(abs(forward_dx), camera_data.forward_ext_catchup_speed_x)
+ forward_dx = sgn(forward_dx) * min(abs(forward_dx), camera_data.forward_offset_catchup_speed_x)
-- apply delta
- self.forward_offset = self.forward_offset + forward_dx
+ self.forward_signed_offset = self.forward_signed_offset + forward_dx
-- combine Window and Forward extension
- self.position.x = windowed_camera_x + self.forward_offset
+ self.position.x = self.base_position.x + self.forward_signed_offset
+
-- Y tracking
+
+ -- Always track fictive center of character as if standing, for camera stability on Y
+ -- In particular, when crouching, character center moves down (compact height is lower), which would cause
+ -- the camera to move down if it was tracking the real center. When standing, adjusted_target_pc_pos_y == target_pos.y
+ -- SPG confirms this even when airborne, see GIF and comment about "5px offset while sonic is curled"
+ -- on http://info.sonicretro.org/SPG:Camera#In_the_Air
+ local adjusted_target_pc_pos_y = target_pos.y + self.target_pc:get_center_height() - pc_data.center_height_standing
+ local dy
+
-- unlike original game we simply use the current center position even when compact (curled)
-- instead of the ghost standing center position
if self.target_pc:is_grounded() then
-- on the ground, stick to y as much as possible
- local target_y = self.target_pc.position.y - camera_data.window_center_offset_y
- local dy = target_y - self.position.y
+ -- we recently added a mini-window of -1/+1 around target y though, to avoid vertical camera jitter
+ -- when character is running on ground with small bumps of 1px
+ -- (Sonic 3 actually has the jitter, but 1px on Genesis is much less remarkable, on PICO-8 with its
+ -- half resolution it would be like a 2x jitter on Genesis)
+ local target_base_y = mid(self.base_position.y,
+ adjusted_target_pc_pos_y - camera_data.window_center_offset_y - 1,
+ adjusted_target_pc_pos_y - camera_data.window_center_offset_y + 1)
+ dy = target_base_y - self.base_position.y
-- clamp abs dy with catchup speed (which depends on ground speed)
local catchup_speed_y = abs(self.target_pc.ground_speed) < camera_data.fast_catchup_min_ground_speed and
camera_data.slow_catchup_speed_y or camera_data.fast_catchup_speed_y
dy = sgn(dy) * min(abs(dy), catchup_speed_y)
-
- -- apply move
- self.position.y = self.position.y + dy
else
-- in the air apply vertical window (stick to top and bottom edges)
- local target_y = mid(self.position.y,
- self.target_pc.position.y - camera_data.window_center_offset_y - camera_data.window_half_height,
- self.target_pc.position.y - camera_data.window_center_offset_y + camera_data.window_half_height)
- local dy = target_y - self.position.y
+ local target_base_y = mid(self.base_position.y,
+ adjusted_target_pc_pos_y - camera_data.window_center_offset_y - camera_data.window_half_height,
+ adjusted_target_pc_pos_y - camera_data.window_center_offset_y + camera_data.window_half_height)
+ dy = target_base_y - self.base_position.y
-- clamp abs dy with fast catchup speed
dy = sgn(dy) * min(abs(dy), camera_data.fast_catchup_speed_y)
-
- -- apply move
- self.position.y = self.position.y + dy
end
+ -- apply move to base y
+ self.base_position.y = self.base_position.y + dy
+
+ -- apply look down offset
+ self.position.y = self.base_position.y + self.look_down_offset
+
+ -- compressed chars note: we now replace constants and even game constant expressions
+ -- like 'screen_width / 2' => 64,
+ -- so don't worry about characters if you want to write values semantically,
+ -- but make sure to write the expressions with spaces exactly as in GAME_CONSTANT_SUBSTITUTE_TABLE
+
-- clamp on level edges
-- we are handling the center so we need to offset by screen_width/height
- self.position.x = mid(screen_width / 2, self.position.x, self.stage_data.tile_width * tile_size - screen_width / 2)
+ self.position.x = mid(screen_width / 2, self.position.x, self.curr_stage_data.tile_width * tile_size - screen_width / 2)
-- Y has dynamic clamping so compute it from camera_bottom_limit_margin_keypoints
local dynamic_bottom_limit = self:get_bottom_limit_at_x(self.position.x)
self.position.y = mid(screen_height / 2, self.position.y, dynamic_bottom_limit - screen_height / 2)
end
+-- return position with floored coordinates
+-- use this when passing position to a function that doesn't automatically floor
+-- like pico8's camera(), and where pixel fractions may cause unintended effects
+function camera_class:get_floored_position()
+ return vector(flr(self.position.x), flr(self.position.y))
+end
+
function camera_class:get_bottom_limit_at_x(x)
local bottom_limit_tile_margin = 0
-- first, evaluate piecewise constant curve, considering each keypoint is placed
-- at the *end* of a constant region
-- iterate from left to right
- for keypoint in all(self.stage_data.camera_bottom_limit_margin_keypoints) do
+ for keypoint in all(self.curr_stage_data.camera_bottom_limit_margin_keypoints) do
-- check if X is before next keypoint X since it indicates the end
if x < keypoint.x * tile_size then
-- we are in the right region since we iterated from left to right
@@ -202,7 +354,7 @@ function camera_class:get_bottom_limit_at_x(x)
-- whether we reached the end and kept margin 0 or found a specific margin,
-- return the complemented value at pixel scale for the bottom limit as Y
- return (self.stage_data.tile_height - bottom_limit_tile_margin) * tile_size
+ return (self.curr_stage_data.tile_height - bottom_limit_tile_margin) * tile_size
end
-- return true if a rectangle with diagonal corners at (topleft, bottomright)
@@ -214,18 +366,21 @@ end
-- the last visible bottom-right pixel of the sprite to test visibility of
-- this makes it easier to compute bounds (e.g. for a 8x8 sprite, bottomright = topleft + (8, 8))
-- we assume integer coordinates
-function camera_class:is_rect_visible(topleft, bottomright)
+function camera_class:is_rect_visible(topleft, exclusive_bottomright)
-- AABB intersection: are camera view rectangle and object rectangle intersecting?
-- compute camera view bounds
+ -- ! we should probably floor camera and passed coordinates, and use >= (+1) instead of >
+ -- however in our current usages, at least one of them is integer so it still works
+ -- but consider flooring if you have enough compressed characters left, for robustness
local left_edge = self.position.x - screen_width / 2
local right_edge = self.position.x + screen_width / 2
local top_edge = self.position.y - screen_height / 2
local bottom_edge = self.position.y + screen_height / 2
-- compare edge positions
- return left_edge < bottomright.x and right_edge > topleft.x and
- top_edge < bottomright.y and bottom_edge > topleft.y
+ return left_edge < exclusive_bottomright.x and right_edge > topleft.x and
+ top_edge < exclusive_bottomright.y and bottom_edge > topleft.y
end
return camera_class
diff --git a/src/ingame/camera_utest.lua b/src/ingame/camera_utest.lua
index 87c05c0a..dd311ab5 100644
--- a/src/ingame/camera_utest.lua
+++ b/src/ingame/camera_utest.lua
@@ -3,6 +3,7 @@ local camera_class = require("ingame/camera")
local camera_data = require("data/camera_data")
local player_char = require("ingame/playercharacter")
+local pc_data = require("data/playercharacter_numerical_data")
describe('camera_class', function ()
@@ -10,8 +11,34 @@ describe('camera_class', function ()
it('should init members to defaults', function ()
local cam = camera_class()
- assert.are_same({nil, vector.zero(), 0, horizontal_dirs.right, 0},
- {cam.target_pc, cam.position, cam.forward_offset, cam.last_grounded_orientation, cam.frames_since_grounded_orientation_change})
+ assert.are_same({
+ nil,
+ vector.zero(),
+ 0,
+ horizontal_dirs.right,
+ 0,
+ horizontal_dirs.right,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ vector.zero(),
+ },
+ {
+ cam.target_pc,
+ cam.position,
+ cam.forward_signed_offset,
+ cam.last_grounded_orientation,
+ cam.frames_since_grounded_orientation_change,
+ cam.confirmed_orientation,
+ cam.frames_since_crouching,
+ cam.look_down_offset,
+ cam.base_position.x,
+ cam.base_position.y,
+ cam.spin_dash_lag_timer,
+ cam.spin_dashing_position,
+ })
end)
end)
@@ -26,11 +53,15 @@ describe('camera_class', function ()
local cam = camera_class()
cam:setup_for_stage(mock_curr_stage_data)
- local spawn_position = mock_curr_stage_data.spawn_location:to_center_position()
+ -- normally we should stub init_position and verify we called it passing the spawn topleft position,
+ -- but this test was written before init_position was extracted, so we kept checking the final result
+ -- although it's a bit redundant with the actual init_position utest below
+ local spawn_position = mock_curr_stage_data.spawn_location:to_topleft_position()
assert.are_same(spawn_position, cam.position)
+ assert.are_equal(spawn_position.y, cam.base_position.y) -- base position y sync
end)
- it('should initialize forward_offset to + camera_data.forward_distance ', function ()
+ it('should initialize forward_signed_offset to + camera_data.forward_distance ', function ()
local mock_curr_stage_data = {
-- doesn't matter for this test
spawn_location = location(1, 2)
@@ -39,12 +70,36 @@ describe('camera_class', function ()
local cam = camera_class()
cam:setup_for_stage(mock_curr_stage_data)
- assert.are_same(camera_data.forward_distance, cam.forward_offset)
+ assert.are_same(camera_data.forward_distance, cam.forward_signed_offset)
end)
end)
- describe('update_camera', function ()
+ describe('init_position', function ()
+
+ it('should initialize camera at *copy* of passed position', function ()
+ local original_position = vector(10, 2)
+
+ local cam = camera_class()
+ cam:init_position(original_position)
+
+ assert.are_same(vector(10, 2), cam.position)
+ assert.are_equal(10, cam.base_position.x) -- base position x sync
+ assert.are_equal(2, cam.base_position.y) -- base position y sync
+
+ -- extra test to verify that we copied by value; we must not modify target position!
+ cam.position.x = 99
+ assert.are_same(vector(10, 2), original_position)
+ end)
+
+ end)
+
+ describe('update', function ()
+
+ -- add this to pos Y when Sonic is compact so you can place its actual center,
+ -- while the camera will still target a few pixels above, i.e. the center as if Sonic was standing
+ -- see comment on adjusted_target_pc_pos_y in source
+ local compact_height_adjustment = pc_data.center_height_standing - pc_data.center_height_compact
local cam
local pc
@@ -79,17 +134,20 @@ describe('camera_class', function ()
pc = player_char()
cam = camera_class()
- cam.stage_data = mock_curr_stage_data
+ cam.curr_stage_data = mock_curr_stage_data
cam.target_pc = pc
end)
it('(debug motion) should track the player 1:1', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
+
cam.target_pc.motion_mode = motion_modes.debug
cam.target_pc.position = vector(140, 100)
cam:update()
+ -- normally we should stub init_position and check call with cam.target_pc.position,
+ -- but it was faster to just check the result
assert.are_same(vector(140, 100), cam.position)
end)
@@ -177,50 +235,193 @@ describe('camera_class', function ()
assert.are_same({horizontal_dirs.right, 1}, {cam.last_grounded_orientation, cam.frames_since_grounded_orientation_change})
end)
- -- below, make sure to test base_position_x instead of cam.position.x if you want to ignore the forward offset,
+ -- look down: frames_since_crouching update
+
+ it('(pc not crouching) should reset frames_since_crouching', function ()
+ cam.target_pc.motion_state = motion_states.standing
+ cam.frames_since_crouching = 60
+
+ cam:update()
+
+ assert.are_equal(0, cam.frames_since_crouching)
+ end)
+
+ it('(pc not crouching) should decrease look_down_offset by look_down_speed', function ()
+ cam.target_pc.motion_state = motion_states.standing
+ cam.look_down_offset = 10
+
+ cam:update()
+
+ assert.are_equal(10 - camera_data.look_down_speed, cam.look_down_offset)
+ end)
+
+ it('(pc not crouching) should decrease look_down_offset by look_down_speed clamped to 0', function ()
+ cam.target_pc.motion_state = motion_states.standing
+ cam.look_down_offset = camera_data.look_down_speed / 2
+
+ cam:update()
+
+ assert.are_equal(0, cam.look_down_offset)
+ end)
+
+ it('(pc crouching) should increment frames_since_crouching', function ()
+ cam.target_pc.motion_state = motion_states.crouching
+ cam.frames_since_crouching = 60
+
+ cam:update()
+
+ assert.are_equal(61, cam.frames_since_crouching)
+ end)
+
+ it('(pc crouching just reaching enough time this frame) should still not increase look_down_offset', function ()
+ cam.target_pc.motion_state = motion_states.crouching
+ cam.frames_since_crouching = camera_data.frames_before_look_down - 1
+
+ cam:update()
+
+ assert.are_equal(0, cam.look_down_offset)
+ end)
+
+ it('(pc crouching already reached frames_before_look_down last frame) should increase look_down_offset', function ()
+ cam.target_pc.motion_state = motion_states.crouching
+ cam.frames_since_crouching = camera_data.frames_before_look_down
+
+ cam:update()
+
+ assert.are_equal(camera_data.look_down_speed, cam.look_down_offset)
+ end)
+
+ it('(pc crouching already reached frames_before_look_down last frame) should increase look_down_offset clamped to camera_data.max_look_down_distance', function ()
+ cam.target_pc.motion_state = motion_states.crouching
+ cam.frames_since_crouching = camera_data.frames_before_look_down
+ cam.look_down_offset = camera_data.max_look_down_distance - camera_data.look_down_speed / 2
+
+ cam:update()
+
+ assert.are_equal(camera_data.max_look_down_distance, cam.look_down_offset)
+ end)
+
+ -- spin dash lag
+
+ it('(pc spin dashing) should set spin dash lag timer to max duration and store spin dashing position', function ()
+ cam.target_pc.motion_state = motion_states.spin_dashing
+ cam.target_pc.position = vector(10, 2)
+
+ cam:update()
+
+ assert.are_equal(camera_data.spin_dash_lag_duration, cam.spin_dash_lag_timer)
+ assert.are_equal(vector(10, 2), cam.spin_dashing_position)
+ end)
+
+ it('(pc stopped spin dashing but spin dash timer still > 0) should decrement spin dash lag timer', function ()
+ cam.target_pc.motion_state = motion_states.rolling
+ cam.spin_dash_lag_timer = 1
+
+ cam:update()
+
+ assert.are_equal(0, cam.spin_dash_lag_timer)
+ end)
+
+ it('(pc stopped spin dashing but spin dash timer still > 1) should target fictive character at spin dash position, orientation, grounded, velocity 0', function ()
+ -- pick the farthest state possible just to check we are ignoring this
+ cam.target_pc.motion_state = motion_states.air_spin
+ cam.target_pc.position = vector(60, 40)
+ cam.target_pc.orientation = horizontal_dirs.right
+
+ -- spin dash lag with stored spin dash parameters
+ cam.spin_dash_lag_timer = 2
+ cam.spin_dashing_position = vector(100, 80)
+ cam.spin_dashing_orientation = horizontal_dirs.left
+
+ -- fictive target is looking left, so negative sign
+ -- to simplify we don't test camera motion on top of that, so we pick a static state
+ -- where camera has already reached max extent = base forward distance toward left
+ -- in counter part we don't see the subtlety or simulating target being grounded
+ -- (as airborne mostly affects handling of Y), so mutating the test by commenting out
+ -- `is_target_grounded = true` still passes, but it's OK
+ cam:init_position(vector(100 - camera_data.forward_distance, 80))
+ cam.base_position.x = 100 -- must be set to start from correct position
+ cam.forward_signed_offset = - camera_data.forward_distance
+
+ cam:update()
+
+ assert.are_equal(1, cam.spin_dash_lag_timer)
+
+ assert.are_equal(100 - camera_data.forward_distance, cam.position.x)
+ assert.are_equal(80, cam.position.y)
+ end)
+
+ it('(pc not spin dashing and spin dash timer == 0) should not change spin dash lag timer', function ()
+ cam.target_pc.motion_state = motion_states.rolling
+ cam.spin_dash_lag_timer = 0
+
+ cam:update()
+
+ assert.are_equal(0, cam.spin_dash_lag_timer)
+ end)
+
+ -- below, make sure to test base_position.x instead of cam.position.x if you want to ignore the forward offset,
-- and in particular the base forward offset which is always present due to orientation
- it('should move the camera X so player X is on left edge if he goes beyond left edge', function ()
- cam.position = vector(120, 80)
+ it('should move the camera X so player X is on left edge if he goes beyond left edge, but close enough (1px out)', function ()
+ cam:init_position(vector(120, 80))
cam.target_pc.position = vector(120 - camera_data.window_half_width - 1, 80)
cam:update()
- assert.are_equal(120 - 1, cam.base_position_x)
+ assert.are_equal(120 - 1, cam.base_position.x)
+ end)
+
+ it('should move the camera X toward a position so player X is on left edge if he goes beyond left edge, but dx is abs clamped by camera_data.catchup_speed_x', function ()
+ cam:init_position(vector(120, 80))
+ cam.target_pc.position = vector(120 - camera_data.window_half_width - camera_data.catchup_speed_x - 1, 80)
+
+ cam:update()
+
+ assert.are_equal(120 - camera_data.catchup_speed_x, cam.base_position.x)
end)
it('should not move the camera on X if player X remains in window X (left edge)', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.position = vector(120 - camera_data.window_half_width, 80)
cam:update()
- assert.are_equal(120, cam.base_position_x)
+ assert.are_equal(120, cam.base_position.x)
end)
it('should not move the camera on X if player X remains in window X (right edge)', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.position = vector(120 + camera_data.window_half_width, 80)
cam:update()
- assert.are_equal(120, cam.base_position_x)
+ assert.are_equal(120, cam.base_position.x)
end)
- it('should move the camera X so player X is on right edge if he goes beyond right edge', function ()
- cam.position = vector(120, 80)
+ it('should move the camera X so player X is on right edge if he goes beyond right edge, but close enough (1px out)', function ()
+ cam:init_position(vector(120, 80))
cam.target_pc.position = vector(120 + camera_data.window_half_width + 1, 80)
cam:update()
- assert.are_equal(120 + 1, cam.base_position_x)
+ assert.are_equal(120 + 1, cam.base_position.x)
+ end)
+
+ it('should move the camera X toward a position so player X is on right edge if he goes beyond right edge, but dx is abs clamped by camera_data.catchup_speed_x', function ()
+ cam:init_position(vector(120, 80))
+ cam.target_pc.position = vector(120 + camera_data.window_half_width + camera_data.catchup_speed_x + 1, 80)
+
+ cam:update()
+
+ assert.are_equal(120 + camera_data.catchup_speed_x, cam.base_position.x)
end)
-- forward base, positive X
it('forward base: should increase forward offset toward + camera_data.forward_distance by catch up speed (not clamped yet) when character faces right (but not moving fast)', function ()
- cam.position = vector(120, 80)
- cam.forward_offset = 0
+ cam:init_position(vector(120, 80))
+ cam.forward_signed_offset = 0
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector.zero()
-- we try to set state to falling to make sure we don't update
@@ -232,13 +433,14 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(camera_data.forward_ext_catchup_speed_x, cam.forward_offset)
- assert.are_equal(120 + camera_data.forward_ext_catchup_speed_x, cam.position.x)
+ assert.are_equal(camera_data.forward_offset_catchup_speed_x, cam.forward_signed_offset)
+ assert.are_equal(120 + camera_data.forward_offset_catchup_speed_x, cam.position.x)
end)
it('forward base: should increase forward offset toward + camera_data.forward_distance by catch up speed (clamped) when character faces right (but not moving fast)', function ()
- cam.position = vector(120 + camera_data.forward_distance, 80)
- cam.forward_offset = camera_data.forward_distance
+ cam:init_position(vector(120 + camera_data.forward_distance, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
+ cam.forward_signed_offset = camera_data.forward_distance
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector.zero()
cam.confirmed_orientation = horizontal_dirs.right
@@ -246,15 +448,15 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(camera_data.forward_distance, cam.forward_offset)
+ assert.are_equal(camera_data.forward_distance, cam.forward_signed_offset)
assert.are_equal(120 + camera_data.forward_distance, cam.position.x)
end)
-- forward base, negative X
it('forward base: should increase forward offset toward - camera_data.forward_distance by catch up speed (not clamped yet) when character faces left (but not moving fast)', function ()
- cam.position = vector(120, 80)
- cam.forward_offset = 0
+ cam:init_position(vector(120, 80))
+ cam.forward_signed_offset = 0
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector.zero()
cam.confirmed_orientation = horizontal_dirs.left
@@ -262,13 +464,14 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(- camera_data.forward_ext_catchup_speed_x, cam.forward_offset)
- assert.are_equal(120 - camera_data.forward_ext_catchup_speed_x, cam.position.x)
+ assert.are_equal(- camera_data.forward_offset_catchup_speed_x, cam.forward_signed_offset)
+ assert.are_equal(120 - camera_data.forward_offset_catchup_speed_x, cam.position.x)
end)
it('forward base: should increase forward offset toward - camera_data.forward_distance by catch up speed (clamped) when character faces left (but not moving fast)', function ()
- cam.position = vector(120 - camera_data.forward_distance, 80)
- cam.forward_offset = - camera_data.forward_distance
+ cam:init_position(vector(120 - camera_data.forward_distance, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
+ cam.forward_signed_offset = - camera_data.forward_distance
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector.zero()
cam.confirmed_orientation = horizontal_dirs.left
@@ -276,7 +479,7 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(- camera_data.forward_distance, cam.forward_offset)
+ assert.are_equal(- camera_data.forward_distance, cam.forward_signed_offset)
assert.are_equal(120 - camera_data.forward_distance, cam.position.x)
end)
@@ -286,7 +489,7 @@ describe('camera_class', function ()
it('forward extension: should increase forward extension by catch up speed when character reaches (forward_ext_min_speed_x + max_forward_ext_speed_x) / 2', function ()
-- we start from offset 0 so the forward base offset doesn't have an impact here
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector((camera_data.forward_ext_min_speed_x + camera_data.max_forward_ext_speed_x) / 2, 0)
cam.confirmed_orientation = horizontal_dirs.right
@@ -294,14 +497,14 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(camera_data.forward_ext_catchup_speed_x, cam.forward_offset)
- assert.are_equal(120 + camera_data.forward_ext_catchup_speed_x, cam.position.x)
+ assert.are_equal(camera_data.forward_offset_catchup_speed_x, cam.forward_signed_offset)
+ assert.are_equal(120 + camera_data.forward_offset_catchup_speed_x, cam.position.x)
end)
it('forward extension: should increase forward extension toward max by catch up speed when character reaches max_forward_ext_speed_x', function ()
-- we start from offset 0 so the forward base offset doesn't have an impact here
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector(camera_data.max_forward_ext_speed_x, 0)
cam.confirmed_orientation = horizontal_dirs.right
@@ -309,8 +512,8 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(camera_data.forward_ext_catchup_speed_x, cam.forward_offset)
- assert.are_equal(120 + camera_data.forward_ext_catchup_speed_x, cam.position.x)
+ assert.are_equal(camera_data.forward_offset_catchup_speed_x, cam.forward_signed_offset)
+ assert.are_equal(120 + camera_data.forward_offset_catchup_speed_x, cam.position.x)
end)
it('forward extension: should increase forward extension by catch up speed until half max when character stays at (forward_ext_min_speed_x + max_forward_ext_speed_x) / 2 for long', function ()
@@ -318,10 +521,11 @@ describe('camera_class', function ()
-- everywhere
-- simulate a camera that has already been moving toward half max offset and close to reaching it
- cam.forward_offset = camera_data.forward_distance + camera_data.forward_ext_max_distance / 2 - 0.1 -- just subtract something lower than camera_data.forward_ext_max_distance
+ cam.forward_signed_offset = camera_data.forward_distance + camera_data.forward_ext_max_distance / 2 - 0.1 -- just subtract something lower than camera_data.forward_ext_max_distance
-- to reproduce the fast that the camera is more forward that it should be with window only,
-- we must add the forward ext offset (else utest won't pass as camera will lag behind)
- cam.position = vector(120 + cam.forward_offset, 80)
+ cam:init_position(vector(120 + cam.forward_signed_offset, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector((camera_data.forward_ext_min_speed_x + camera_data.max_forward_ext_speed_x) / 2, 0)
cam.confirmed_orientation = horizontal_dirs.right
@@ -329,7 +533,7 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(camera_data.forward_distance + camera_data.forward_ext_max_distance / 2, cam.forward_offset)
+ assert.are_equal(camera_data.forward_distance + camera_data.forward_ext_max_distance / 2, cam.forward_signed_offset)
assert.are_equal(120 + camera_data.forward_distance + camera_data.forward_ext_max_distance / 2, cam.position.x)
end)
@@ -338,10 +542,11 @@ describe('camera_class', function ()
-- everywhere
-- simulate a camera that has already been moving toward max offset and close to reaching it
- cam.forward_offset = camera_data.forward_distance + camera_data.forward_ext_max_distance - 0.1 -- just subtract something lower than camera_data.forward_ext_max_distance
+ cam.forward_signed_offset = camera_data.forward_distance + camera_data.forward_ext_max_distance - 0.1 -- just subtract something lower than camera_data.forward_ext_max_distance
-- to reproduce the fast that the camera is more forward that it should be with window only,
-- we must add the forward ext offset (else utest won't pass as camera will lag behind)
- cam.position = vector(120 + cam.forward_offset, 80)
+ cam:init_position(vector(120 + cam.forward_signed_offset, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector(camera_data.max_forward_ext_speed_x, 0)
cam.confirmed_orientation = horizontal_dirs.right
@@ -349,7 +554,7 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(camera_data.forward_distance + camera_data.forward_ext_max_distance, cam.forward_offset)
+ assert.are_equal(camera_data.forward_distance + camera_data.forward_ext_max_distance, cam.forward_signed_offset)
assert.are_equal(120 + camera_data.forward_distance + camera_data.forward_ext_max_distance, cam.position.x)
end)
@@ -357,8 +562,9 @@ describe('camera_class', function ()
-- here we are reaching the max, so base forward offset contribution is felt, add it
-- everywhere
- cam.forward_offset = camera_data.forward_distance + camera_data.forward_ext_max_distance - 0.1 -- just subtract something lower than camera_data.forward_ext_max_distance
- cam.position = vector(120 + cam.forward_offset, 80)
+ cam.forward_signed_offset = camera_data.forward_distance + camera_data.forward_ext_max_distance - 0.1 -- just subtract something lower than camera_data.forward_ext_max_distance
+ cam:init_position(vector(120 + cam.forward_signed_offset, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector(camera_data.max_forward_ext_speed_x + 1, 0)
cam.confirmed_orientation = horizontal_dirs.right
@@ -366,7 +572,7 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(camera_data.forward_distance + camera_data.forward_ext_max_distance, cam.forward_offset)
+ assert.are_equal(camera_data.forward_distance + camera_data.forward_ext_max_distance, cam.forward_signed_offset)
assert.are_equal(120 + camera_data.forward_distance + camera_data.forward_ext_max_distance, cam.position.x)
end)
@@ -374,8 +580,9 @@ describe('camera_class', function ()
-- here we are reaching the max, so base forward offset contribution is felt, add it
-- everywhere
- cam.forward_offset = camera_data.forward_distance + camera_data.forward_ext_max_distance / 2 + 0.1 -- just add something lower than camera_data.forward_ext_max_distance
- cam.position = vector(120 + cam.forward_offset, 80)
+ cam.forward_signed_offset = camera_data.forward_distance + camera_data.forward_ext_max_distance / 2 + 0.1 -- just add something lower than camera_data.forward_ext_max_distance
+ cam:init_position(vector(120 + cam.forward_signed_offset, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector((camera_data.forward_ext_min_speed_x + camera_data.max_forward_ext_speed_x) / 2, 0)
cam.confirmed_orientation = horizontal_dirs.right
@@ -383,7 +590,7 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(camera_data.forward_distance + camera_data.forward_ext_max_distance / 2, cam.forward_offset)
+ assert.are_equal(camera_data.forward_distance + camera_data.forward_ext_max_distance / 2, cam.forward_signed_offset)
assert.are_equal(120 + camera_data.forward_distance + camera_data.forward_ext_max_distance / 2, cam.position.x)
end)
@@ -391,8 +598,9 @@ describe('camera_class', function ()
-- here we decrease back toward the opposite sign from the max,
-- so the forward base offset doesn't have an impact
- cam.forward_offset = camera_data.forward_ext_max_distance
- cam.position = vector(120 + cam.forward_offset, 80)
+ cam.forward_signed_offset = camera_data.forward_ext_max_distance
+ cam:init_position(vector(120 + cam.forward_signed_offset, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector((camera_data.forward_ext_min_speed_x + camera_data.max_forward_ext_speed_x) / 2, 0)
cam.confirmed_orientation = horizontal_dirs.right
@@ -400,16 +608,17 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(camera_data.forward_ext_max_distance - camera_data.forward_ext_catchup_speed_x, cam.forward_offset)
- assert.are_equal(120 + camera_data.forward_ext_max_distance - camera_data.forward_ext_catchup_speed_x, cam.position.x)
+ assert.are_equal(camera_data.forward_ext_max_distance - camera_data.forward_offset_catchup_speed_x, cam.forward_signed_offset)
+ assert.are_equal(120 + camera_data.forward_ext_max_distance - camera_data.forward_offset_catchup_speed_x, cam.position.x)
end)
it('forward extension: should decrease forward extension back to 0 when character goes below forward_ext_min_speed_x for long', function ()
-- here we are reaching the new target 0, so base forward offset contribution is felt, add it
-- everywhere
- cam.forward_offset = camera_data.forward_distance + 0.1 -- just something lower than camera_data.forward_ext_max_distance
- cam.position = vector(120 + cam.forward_offset, 80)
+ cam.forward_signed_offset = camera_data.forward_distance + 0.1 -- just something lower than camera_data.forward_ext_max_distance
+ cam:init_position(vector(120 + cam.forward_signed_offset, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector(camera_data.forward_ext_min_speed_x - 1, 0)
cam.confirmed_orientation = horizontal_dirs.right
@@ -417,7 +626,7 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(0 + camera_data.forward_distance, cam.forward_offset)
+ assert.are_equal(0 + camera_data.forward_distance, cam.forward_signed_offset)
assert.are_equal(120 + camera_data.forward_distance, cam.position.x)
end)
@@ -428,7 +637,7 @@ describe('camera_class', function ()
it('forward extension: should increase forward extension toward NEGATIVE by catch up speed when character reaches -max_forward_ext_speed_x', function ()
-- we start from offset 0 so the forward base offset doesn't have an impact here
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector(-camera_data.max_forward_ext_speed_x, 0)
cam.confirmed_orientation = horizontal_dirs.right
@@ -436,24 +645,25 @@ describe('camera_class', function ()
cam:update()
- assert.are_equal(-camera_data.forward_ext_catchup_speed_x, cam.forward_offset)
- assert.are_equal(120 - camera_data.forward_ext_catchup_speed_x, cam.position.x)
+ assert.are_equal(-camera_data.forward_offset_catchup_speed_x, cam.forward_signed_offset)
+ assert.are_equal(120 - camera_data.forward_offset_catchup_speed_x, cam.position.x)
end)
it('forward extension: should increase forward extension toward NEGATIVE by catch up speed until max when character stays above -max_forward_ext_speed_x for long', function ()
-- here we are reaching the negative max, so base forward offset contribution is felt, add it
-- everywhere
- cam.forward_offset = camera_data.forward_distance - (camera_data.forward_ext_max_distance - 0.1) -- just subtract something lower than camera_data.forward_ext_max_distance
+ cam.forward_signed_offset = camera_data.forward_distance - (camera_data.forward_ext_max_distance - 0.1) -- just subtract something lower than camera_data.forward_ext_max_distance
-- to reproduce the fast that the camera is more forward that it should be with window only,
-- we must add the forward ext offset (else utest won't pass as camera will lag behind)
- cam.position = vector(120 + cam.forward_offset, 80)
+ cam:init_position(vector(120 + cam.forward_signed_offset, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector(-camera_data.max_forward_ext_speed_x, 0)
cam:update()
- assert.are_equal(camera_data.forward_distance - camera_data.forward_ext_max_distance, cam.forward_offset)
+ assert.are_equal(camera_data.forward_distance - camera_data.forward_ext_max_distance, cam.forward_signed_offset)
assert.are_equal(120 + camera_data.forward_distance - camera_data.forward_ext_max_distance, cam.position.x)
end)
@@ -461,43 +671,46 @@ describe('camera_class', function ()
-- here we are reaching the negative max, so base forward offset contribution is felt, add it
-- everywhere
- cam.forward_offset = camera_data.forward_distance - camera_data.forward_ext_max_distance
- cam.position = vector(120 + cam.forward_offset, 80)
+ cam.forward_signed_offset = camera_data.forward_distance - camera_data.forward_ext_max_distance
+ cam:init_position(vector(120 + cam.forward_signed_offset, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector(-(camera_data.max_forward_ext_speed_x - 1), 0)
cam:update()
- assert.are_equal(camera_data.forward_distance - (camera_data.forward_ext_max_distance - camera_data.forward_ext_catchup_speed_x), cam.forward_offset)
- assert.are_equal(120 + camera_data.forward_distance - (camera_data.forward_ext_max_distance - camera_data.forward_ext_catchup_speed_x), cam.position.x)
+ assert.are_equal(camera_data.forward_distance - (camera_data.forward_ext_max_distance - camera_data.forward_offset_catchup_speed_x), cam.forward_signed_offset)
+ assert.are_equal(120 + camera_data.forward_distance - (camera_data.forward_ext_max_distance - camera_data.forward_offset_catchup_speed_x), cam.position.x)
end)
it('forward extension: should decrease forward extension (in abs) back to 0 when character goes below max_forward_ext_speed_x (in abs) for long', function ()
-- here we are reaching the new target 0, so base forward offset contribution is felt, add it
-- everywhere
- cam.forward_offset = camera_data.forward_distance - 0.1 -- just something lower (in abs) than camera_data.forward_ext_max_distance
- cam.position = vector(120 + cam.forward_offset, 80)
+ cam.forward_signed_offset = camera_data.forward_distance - 0.1 -- just something lower (in abs) than camera_data.forward_ext_max_distance
+ cam:init_position(vector(120 + cam.forward_signed_offset, 80))
+ cam.base_position.x = 120 -- must be set to start from correct position
cam.target_pc.position = vector(120, 80)
cam.target_pc.velocity = vector(-(camera_data.max_forward_ext_speed_x - 1), 0)
cam:update()
- assert.are_equal(0 + camera_data.forward_distance, cam.forward_offset)
+ assert.are_equal(0 + camera_data.forward_distance, cam.forward_signed_offset)
assert.are_equal(120 + camera_data.forward_distance, cam.position.x)
end)
-- Y
it('(standing, low ground speed) should move the camera Y toward player position (so it matches reference Y) using slow catchup speed if he goes beyond top edge', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
-- alternative +/- ground speed to check abs logic
cam.target_pc.ground_speed = -(camera_data.fast_catchup_min_ground_speed - 0.5)
-- it's hard to find realistic values for such a motion, where you're move slowly on a slope but still
-- fast vertically... but it should be possible on a very high slope. Here we imagine a wall where we move
-- at ground speed 3.5, 100% vertically!
- cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y - (camera_data.slow_catchup_speed_y + 0.5))
+ -- we recently changed the test with -0.5 since adding the new vertical mini-window of -1/+1
+ cam.target_pc.position = vector(120, 80 - 0.5 + camera_data.window_center_offset_y - (camera_data.slow_catchup_speed_y + 0.5))
cam:update()
@@ -506,11 +719,12 @@ describe('camera_class', function ()
end)
it('(standing, high ground speed) should move the camera Y toward player position (so it matches reference Y) using slow catchup speed if he goes beyond top edge', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
cam.target_pc.ground_speed = camera_data.fast_catchup_min_ground_speed
-- unrealistic, we have ground speed 4 but still move by more than 8, impossible even on vertical wall... but good for testing
- cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y - (camera_data.fast_catchup_speed_y + 0.5))
+ -- we recently changed the test with -0.5 since adding the new vertical mini-window of -1/+1
+ cam.target_pc.position = vector(120, 80 - 0.5 + camera_data.window_center_offset_y - (camera_data.fast_catchup_speed_y + 0.5))
cam:update()
@@ -519,10 +733,11 @@ describe('camera_class', function ()
end)
it('(standing, low ground speed) should move the camera Y to match player Y if he goes beyond top edge slower than low catchup speed', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
cam.target_pc.ground_speed = camera_data.fast_catchup_min_ground_speed - 0.5
- cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y - (camera_data.slow_catchup_speed_y - 0.5))
+ -- we recently changed the test with -1 since adding the new vertical mini-window of -1/+1
+ cam.target_pc.position = vector(120, 80 - 1 + camera_data.window_center_offset_y - (camera_data.slow_catchup_speed_y - 0.5))
cam:update()
@@ -530,10 +745,11 @@ describe('camera_class', function ()
end)
it('(standing, high ground speed) should move the camera Y to match player Y if he goes beyond top edge slower than fast catchup speed', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
cam.target_pc.ground_speed = -camera_data.fast_catchup_min_ground_speed
- cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y - (camera_data.fast_catchup_speed_y - 0.5))
+ -- we recently changed the test with -1 since adding the new vertical mini-window of -1/+1
+ cam.target_pc.position = vector(120, 80 - 1 + camera_data.window_center_offset_y - (camera_data.fast_catchup_speed_y - 0.5))
cam:update()
@@ -541,7 +757,7 @@ describe('camera_class', function ()
end)
it('(standing) should not move the camera Y if player Y remains in window Y (top edge)', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
cam.target_pc.ground_speed = -(camera_data.fast_catchup_min_ground_speed - 0.5)
cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y)
@@ -552,7 +768,7 @@ describe('camera_class', function ()
end)
it('(standing) should not move the camera Y if player Y remains in window Y (bottom edge)', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
cam.target_pc.ground_speed = camera_data.fast_catchup_min_ground_speed
cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y)
@@ -563,10 +779,11 @@ describe('camera_class', function ()
end)
it('(standing, low ground speed) should move the camera Y to match player Y if he goes beyond bottom edge slower than low catchup speed', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
cam.target_pc.ground_speed = camera_data.fast_catchup_min_ground_speed - 0.5
- cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y + (camera_data.slow_catchup_speed_y - 0.5))
+ -- we recently changed the test with +1 since adding the new vertical mini-window of -1/+1
+ cam.target_pc.position = vector(120, 80 + 1 + camera_data.window_center_offset_y + (camera_data.slow_catchup_speed_y - 0.5))
cam:update()
@@ -574,10 +791,11 @@ describe('camera_class', function ()
end)
it('(standing, high ground speed) should move the camera Y to match player Y if he goes beyond bottom edge slower than low catchup speed', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
cam.target_pc.ground_speed = -camera_data.fast_catchup_min_ground_speed
- cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y + (camera_data.fast_catchup_speed_y - 0.5))
+ -- we recently changed the test with +1 since adding the new vertical mini-window of -1/+1
+ cam.target_pc.position = vector(120, 80 + 1 + camera_data.window_center_offset_y + (camera_data.fast_catchup_speed_y - 0.5))
cam:update()
@@ -585,13 +803,14 @@ describe('camera_class', function ()
end)
it('(standing, low ground speed) should move the camera Y toward player position (so it matches reference Y) using slow catchup speed if he goes beyond bottom edge', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
cam.target_pc.ground_speed = -(camera_data.fast_catchup_min_ground_speed - 0.5)
-- it's hard to find realistic values for such a motion, where you're move slowly on a slope but still
-- fast vertically... but it should be possible on a very high slope. Here we imagine a wall where we move
-- at ground speed 3.5, 100% vertically!
- cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y + (camera_data.slow_catchup_speed_y + 0.5))
+ -- we recently changed the test with +0.5 since adding the new vertical mini-window of -1/+1
+ cam.target_pc.position = vector(120, 80 + 0.5 + camera_data.window_center_offset_y + (camera_data.slow_catchup_speed_y + 0.5))
cam:update()
@@ -600,11 +819,12 @@ describe('camera_class', function ()
end)
it('(standing, high ground speed) should move the camera Y toward player position (so it matches reference Y) using slow catchup speed if he goes beyond bottom edge', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.standing
cam.target_pc.ground_speed = camera_data.fast_catchup_min_ground_speed
-- unrealistic, we have ground speed 4 but still move by more than 8, impossible even on vertical wall... but good for testing
- cam.target_pc.position = vector(120, 80 + camera_data.window_center_offset_y + (camera_data.fast_catchup_speed_y + 0.5))
+ -- we recently changed the test with +0.5 since adding the new vertical mini-window of -1/+1
+ cam.target_pc.position = vector(120, 80 + 0.5 + camera_data.window_center_offset_y + (camera_data.fast_catchup_speed_y + 0.5))
cam:update()
@@ -613,9 +833,9 @@ describe('camera_class', function ()
end)
it('(airborne) should move the camera Y toward player Y with fast catchup speed (so that it gets closer to top edge) if player Y goes beyond top edge faster than fast_catchup_speed_y', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.air_spin
- cam.target_pc.position = vector(120 , 80 + camera_data.window_center_offset_y - camera_data.window_half_height - (camera_data.fast_catchup_speed_y + 5))
+ cam.target_pc.position = vector(120 , 80 + compact_height_adjustment + camera_data.window_center_offset_y - camera_data.window_half_height - (camera_data.fast_catchup_speed_y + 5))
cam:update()
@@ -624,9 +844,9 @@ describe('camera_class', function ()
end)
it('(airborne) should move the camera Y so player Y is on top edge if he goes beyond top edge', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.air_spin
- cam.target_pc.position = vector(120 , 80 + camera_data.window_center_offset_y - camera_data.window_half_height - 1)
+ cam.target_pc.position = vector(120 , 80 + compact_height_adjustment + camera_data.window_center_offset_y - camera_data.window_half_height - 1)
cam:update()
@@ -634,9 +854,9 @@ describe('camera_class', function ()
end)
it('(airborne) should not move the camera on Y if player Y remains in window Y (top edge)', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.air_spin
- cam.target_pc.position = vector(120 , 80 + camera_data.window_center_offset_y - camera_data.window_half_height)
+ cam.target_pc.position = vector(120 , 80 + compact_height_adjustment + camera_data.window_center_offset_y - camera_data.window_half_height)
cam:update()
@@ -644,9 +864,9 @@ describe('camera_class', function ()
end)
it('(airborne) should not move the camera on X if player X remains in window X (bottom edge)', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.air_spin
- cam.target_pc.position = vector(120 , 80 + camera_data.window_center_offset_y + camera_data.window_half_height)
+ cam.target_pc.position = vector(120 , 80 + compact_height_adjustment + camera_data.window_center_offset_y + camera_data.window_half_height)
cam:update()
@@ -654,9 +874,9 @@ describe('camera_class', function ()
end)
it('(airborne) should move the camera X so player X is on bottom edge if he goes beyond bottom edge', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.air_spin
- cam.target_pc.position = vector(120 , 80 + camera_data.window_center_offset_y + camera_data.window_half_height + 1)
+ cam.target_pc.position = vector(120 , 80 + compact_height_adjustment + camera_data.window_center_offset_y + camera_data.window_half_height + 1)
cam:update()
@@ -664,9 +884,9 @@ describe('camera_class', function ()
end)
it('(airborne) should move the camera Y toward player Y with fast catchup speed (so that it gets closer to bottom edge) if player Y goes beyond bottom edge faster than fast_catchup_speed_y', function ()
- cam.position = vector(120, 80)
+ cam:init_position(vector(120, 80))
cam.target_pc.motion_state = motion_states.air_spin
- cam.target_pc.position = vector(120 , 80 + camera_data.window_center_offset_y + camera_data.window_half_height + (camera_data.fast_catchup_speed_y + 5))
+ cam.target_pc.position = vector(120 , 80 + compact_height_adjustment + camera_data.window_center_offset_y + camera_data.window_half_height + (camera_data.fast_catchup_speed_y + 5))
cam:update()
@@ -674,34 +894,74 @@ describe('camera_class', function ()
assert.are_equal(80 + camera_data.fast_catchup_speed_y, cam.position.y)
end)
+ it('(compact) should target center for standing height even if character is compact for camera stability on Y', function ()
+ cam:init_position(vector(120, 80))
+ cam.look_down_offset = 0
+
+ cam.target_pc.motion_state = motion_states.rolling
+ cam.target_pc.position = vector(120, 80 + compact_height_adjustment + camera_data.window_center_offset_y)
+
+ cam:update()
+
+ assert.are_equal(80, cam.position.y)
+ end)
+
+ it('(crouching with look_down_offset) should apply look_down_offset', function ()
+ cam:init_position(vector(120, 80))
+ cam.look_down_offset = 10
+
+ cam.target_pc.motion_state = motion_states.crouching
+ cam.target_pc.position = vector(120, 80 + compact_height_adjustment + camera_data.window_center_offset_y)
+
+ cam:update()
+
+ assert.are_equal(80 + 10, cam.position.y)
+ end)
+
+ it('(crouching) should apply look_down_offset then clamp on Y (bottom limit offset 0 at that X)', function ()
+ cam:init_position(vector(800-64, 220-64))
+ cam.look_down_offset = 30
+
+ cam.target_pc.motion_state = motion_states.crouching
+ cam.target_pc.position = vector(800-64, 220-64 + compact_height_adjustment + camera_data.window_center_offset_y)
+
+ cam:update()
+
+ -- 220 + 30 = 250 but clamped so 240 (64 for screen half height)
+ assert.are_equal(240-64, cam.position.y)
+ end)
+
it('should move the camera to player position, clamped (top-left)', function ()
- cam.target_pc.ground_speed = camera_data.fast_catchup_min_ground_speed
-- start near/at the edge already, if you're too far the camera won't have
-- time to reach the edge in one update due to smooth motion (in y)
-- pick offsets of camera_data.slow_catchup_speed_y or lower to be safe
- cam.position = vector(64 + 2, 64 + 2)
+ cam:init_position(vector(64 + 2, 64 + 2))
+
cam.target_pc.position = vector(12, 24)
+ cam.target_pc.ground_speed = camera_data.fast_catchup_min_ground_speed
cam:update()
assert.are_same(vector(64, 64), cam.position)
end)
- it('should move the camera to player position, clamped (bottom-right)', function ()
+ it('should move the camera to player position, clamped (top-right)', function ()
-- start near/at the edge already, if you're too far the camera won't have
-- time to reach the edge in one update due to smooth motion (in y)
- cam.position = vector(800-64, 240-64)
- cam.target_pc.position = vector(2000, 1000)
+ cam:init_position(vector(800-64, 64 + 2))
+
+ cam.target_pc.position = vector(2000, 0)
cam:update()
- assert.are_same(vector(800-64, 240-64), cam.position)
+ assert.are_same(vector(800-64, 64), cam.position)
end)
it('should move the camera to player position, clamped (bottom-right, bottom limit offset 0)', function ()
-- start near/at the edge already, if you're too far the camera won't have
-- time to reach the edge in one update due to smooth motion (in y)
- cam.position = vector(800-64, 240-64)
+ cam:init_position(vector(800-64, 240-64))
+
cam.target_pc.position = vector(2000, 1000)
cam:update()
@@ -712,7 +972,8 @@ describe('camera_class', function ()
it('should move the camera to player position, clamped (bottom-left, bottom limit offset 2)', function ()
-- start near/at the edge already, if you're too far the camera won't have
-- time to reach the edge in one update due to smooth motion (in y)
- cam.position = vector(64, 224-64)
+ cam:init_position(vector(64, 224-64))
+
cam.target_pc.position = vector(0, 1000)
cam:update()
@@ -722,6 +983,16 @@ describe('camera_class', function ()
end)
+ describe('get_floored_position', function ()
+
+ it('should return current position with floored coordinates', function ()
+ local cam = camera_class()
+ cam:init_position(vector(5.9, -5.1))
+ assert.are_same(vector(5, -6), cam:get_floored_position())
+ end)
+
+ end)
+
describe('get_bottom_limit_at_x', function ()
local cam
@@ -738,7 +1009,7 @@ describe('camera_class', function ()
}
cam = camera_class()
- cam.stage_data = mock_curr_stage_data
+ cam.curr_stage_data = mock_curr_stage_data
end)
it('should return complement of margin 50 for pixel scale for x = 9 * tile_size + .9', function ()
@@ -763,9 +1034,12 @@ describe('camera_class', function ()
local cam
+ -- note: if you add flooring to camera position and passed coordinates,
+ -- you should add 0.9 everywhere for more precise utests
+
before_each(function ()
cam = camera_class()
- cam.position = vector(64, 64) -- so top-left is (0, 0) and bottom-right is (128, 128)
+ cam:init_position(vector(64, 64)) -- so top-left is (0, 0) and bottom-right is (128, 128)
end)
it('should return false for square just touching on the left, but outside camera view', function ()
diff --git a/src/ingame/emerald.lua b/src/ingame/emerald.lua
index 584d62d7..2cec8246 100644
--- a/src/ingame/emerald.lua
+++ b/src/ingame/emerald.lua
@@ -19,16 +19,20 @@ function emerald.set_color_palette(number, brightness)
-- so we must replace the last two with our custom colors
local light_color, dark_color = unpack(visual.emerald_colors[number])
- if brightness == 0 then
- pal(colors.red, light_color)
- pal(colors.dark_purple, dark_color)
- elseif brightness == 1 then
- pal(colors.red, colors.white)
- pal(colors.dark_purple, light_color)
- else -- brightness == 2
- pal(colors.red, colors.white)
- pal(colors.dark_purple, colors.white)
- end
+ local brightness_color_swap = {
+ -- original colors : red, dark_purple
+ {light_color, dark_color},
+ {colors.white, light_color},
+ {colors.white, colors.white},
+ }
+
+ -- brightness starts at 0, index starts at 1, so add 1
+ assert(0 <= brightness and brightness <= 2, "invalid brightness: "..brightness)
+ -- local new_colors = brightness_color_swap[brightness + 1]
+ swap_colors({colors.red, colors.dark_purple}, brightness_color_swap[brightness + 1])
+
+ -- pal(colors.red, brightness_color_swap[brightness + 1][1])
+ -- pal(colors.dark_purple, brightness_color_swap[brightness + 1][2])
end
-- static (as used by render_hud even without a proper emerald object)
@@ -65,7 +69,15 @@ end
--#endif
function emerald:get_center()
- return self.location:to_center_position()
+ local center_position = self.location:to_center_position()
+
+ -- visual trick: last emerald (above spring) is offset compared to spring sprite,
+ -- so adjust position to place it above spring center (this also affects picking collision)
+ if self.number == 8 then
+ center_position:add_inplace(vector(5, 0))
+ end
+
+ return center_position
end
function emerald:get_render_bounding_corners()
diff --git a/src/ingame/emerald_utest.lua b/src/ingame/emerald_utest.lua
index d571af95..8dbc292d 100644
--- a/src/ingame/emerald_utest.lua
+++ b/src/ingame/emerald_utest.lua
@@ -31,11 +31,16 @@ describe('emerald', function ()
describe('get_center', function ()
- it('emerald(..., location(2, 1)) => vector(20, 12)', function ()
+ it('emerald(1~7, location(2, 1)) => vector(20, 12)', function ()
local em = emerald(7, location(2, 1))
assert.are_same(vector(20, 12), em:get_center())
end)
+ it('emerald(8, location(2, 1)) => vector(25, 12)', function ()
+ local em = emerald(8, location(2, 1))
+ assert.are_same(vector(25, 12), em:get_center())
+ end)
+
end)
describe('get_render_bounding_corners', function ()
@@ -49,6 +54,9 @@ describe('emerald', function ()
describe('set_color_palette (static)', function ()
+ -- now all is done via swap_colors, but this utest was written before so we kept the test
+ -- checking actual pal() calls inside
+
setup(function ()
stub(_G, "pal")
end)
diff --git a/src/ingame/particle.lua b/src/ingame/particle.lua
new file mode 100644
index 00000000..c6f0ab72
--- /dev/null
+++ b/src/ingame/particle.lua
@@ -0,0 +1,68 @@
+local particle = new_class()
+
+-- single particle class
+
+-- parameters
+-- frame_lifetime float total lifetime (frames)
+-- frame_accel vector velocity difference applied every frame (px/frame^2)
+
+-- state
+-- elapsed_frames vector elapsed frames since spawn
+-- position vector current position
+-- initial_frame_velocity vector current velocity (applied every frame, so divide second-based velocity by FPS)
+-- base_size float base size (px)
+-- size_ratio_over_lifetime ratio -> float function returning factor of base size over lifetime ratio
+function particle:init(frame_lifetime, initial_position, initial_frame_velocity, frame_accel, base_size, size_ratio_over_lifetime)
+ -- parameters
+ self.frame_lifetime = frame_lifetime
+ self.frame_accel = frame_accel or vector.zero()
+ self.base_size = base_size
+ self.size_ratio_over_lifetime = size_ratio_over_lifetime
+
+ -- state
+ self.elapsed_frames = 0
+ self.position = initial_position
+ self.frame_velocity = initial_frame_velocity
+ self.size = 0 -- will be set on first update
+end
+
+-- update particle and return true iff particle is still alive this frame
+function particle:update_and_check_alive()
+ -- increment elapsed frames
+ self.elapsed_frames = self.elapsed_frames + 1
+
+ -- check lifetime (we don't update on spawn frame, so we should really destroy particle
+ -- when elapsed_frames reaches frame_lifetime, hence >= not >)
+ if self.elapsed_frames >= self.frame_lifetime then
+ -- no need to update state, this particle is gonna disappear this frame
+ return -- false, commented out to spare characters but [nil] will work the same
+ end
+
+ self:update()
+ return true
+end
+
+-- update particle state
+function particle:update()
+ self.position = self.position + self.frame_velocity
+ self.frame_velocity = self.frame_velocity + self.frame_accel
+ self.size = self.base_size * self.size_ratio_over_lifetime(self.elapsed_frames / self.frame_lifetime)
+end
+
+-- render particle at its current location
+function particle:render()
+ -- PICO-8 shapes evolve suddenly with radius, so we mix and match shapes to get the gradual size we want
+ -- size = 0..2 -> circfill radius 0 -> dot
+ -- size = 2..3 -> rectfill width and height 2 -> 2x2 square (center at topleft, as even size forces us to offset)
+ -- size = 3..4 -> circfill radius 1 (size/2) -> 3x3 cross
+ -- size = 4..5 -> circfill radius 2 (size/2) -> 4x4 disc
+ -- size = 5+ -> circfill radius size/2
+ -- since circfill auto-floors radius, we can just pass size/2 for size >= 3, and even for size = 1
+ if 2 <= self.size and self.size < 3 then
+ rectfill(self.position.x, self.position.y, self.position.x + 1, self.position.y + 1, colors.white)
+ else
+ circfill(self.position.x, self.position.y, self.size / 2, colors.white)
+ end
+end
+
+return particle
diff --git a/src/ingame/particle_utest.lua b/src/ingame/particle_utest.lua
new file mode 100644
index 00000000..fe2160c0
--- /dev/null
+++ b/src/ingame/particle_utest.lua
@@ -0,0 +1,147 @@
+require("test/bustedhelper_ingame")
+local particle = require("ingame/particle")
+
+describe('particle', function ()
+
+ local function dummy_size_ratio_over_lifetime(life_ratio)
+ return life_ratio
+ end
+
+ describe('init', function ()
+
+ it('should create an particle with frame_lifetime, initial_position, initial_frame_velocity, frame_accel, base_size, size_ratio_over_lifetime', function ()
+ local particle1 = particle(60, vector(10, 20), vector(2, 3), vector(0, 0), 5.5, dummy_size_ratio_over_lifetime)
+ assert.are_same( {60, vector(10, 20), vector(2, 3), vector(0, 0), 5.5, dummy_size_ratio_over_lifetime, 0, 0},
+ {particle1.frame_lifetime, particle1.position, particle1.frame_velocity, particle1.frame_accel,
+ particle1.base_size, particle1.size_ratio_over_lifetime, particle1.elapsed_frames, particle1.size})
+ end)
+
+ end)
+
+ describe('update_and_check_alive', function ()
+
+ setup(function ()
+ stub(particle, "update")
+ end)
+
+ teardown(function ()
+ particle.update:revert()
+ end)
+
+ after_each(function ()
+ particle.update:clear()
+ end)
+
+ it('should increment elapsed_frames', function ()
+ local particle1 = particle(10, vector(10, 20), vector(2, 3), vector(0, 0), 5.5, dummy_size_ratio_over_lifetime)
+ particle1.elapsed_frames = 5
+
+ particle1:update_and_check_alive()
+
+ assert.are_same(6, particle1.elapsed_frames)
+ end)
+
+ it('(elapsed_frames is just before frame_lifetime) should increment elapsed_frames', function ()
+ local particle1 = particle(10, vector(10, 20), vector(2, 3), vector(0, 0), 5.5, dummy_size_ratio_over_lifetime)
+ particle1.elapsed_frames = 9 -- 10 - 1
+
+ local result = particle1:update_and_check_alive()
+
+ -- semantically false, but for character optimization reasons, we return nil
+ assert.is_nil(result)
+ end)
+
+ it('(elapsed_frames not just before frame_lifetime) should call update and return true', function ()
+ local particle1 = particle(10, vector(10, 20), vector(2, 3), vector(0, 0), 5.5, dummy_size_ratio_over_lifetime)
+ particle1.elapsed_frames = 5
+
+ local result = particle1:update_and_check_alive()
+
+ assert.spy(particle.update).was_called(1)
+ assert.spy(particle.update).was_called_with(match.ref(particle1))
+
+ assert.is_true(result)
+ end)
+
+ it('(elapsed_frames is just before frame_lifetime) should not call update as we returned earlier', function ()
+ local particle1 = particle(10, vector(10, 20), vector(2, 3), vector(0, 0), 5.5, dummy_size_ratio_over_lifetime)
+ particle1.elapsed_frames = 9 -- 10 - 1
+
+ particle1:update_and_check_alive()
+
+ assert.spy(particle.update).was_not_called()
+ end)
+
+ end)
+
+ describe('update', function ()
+
+ it('should update position by adding frame_velocity', function ()
+ local particle1 = particle(10, vector(10, 20), vector(2, 3), vector(0, 0), 5.5, dummy_size_ratio_over_lifetime)
+
+ particle1:update()
+
+ assert.are_same(vector(12, 23), particle1.position)
+ end)
+
+ it('should update velocity by adding frame_accel', function ()
+ local particle1 = particle(10, vector(10, 20), vector(-2, 3), vector(-2, 2), 5.5, dummy_size_ratio_over_lifetime)
+
+ particle1:update()
+
+ assert.are_same(vector(-4, 5), particle1.frame_velocity)
+ end)
+
+ it('should update size', function ()
+ local particle1 = particle(10, vector(10, 20), vector(-2, 3), vector(-2, 2), 3, dummy_size_ratio_over_lifetime)
+ -- dummy_size_ratio_over_lifetime is linear from 0 to 1, so by picking half the lifetime (5/10 = 0.5)
+ -- we know the particle size will be half of base size 3, so 1.5
+ particle1.elapsed_frames = 5
+
+ particle1:update()
+
+ assert.are_equal(1.5, particle1.size)
+ end)
+
+ end)
+
+ describe('render', function ()
+
+ setup(function ()
+ stub(_G, "rectfill")
+ stub(_G, "circfill")
+ end)
+
+ teardown(function ()
+ rectfill:revert()
+ circfill:revert()
+ end)
+
+ after_each(function ()
+ rectfill:clear()
+ circfill:clear()
+ end)
+
+ it('(size = 2..3) should call circfill at particle position, with size, white (hardcoded)', function ()
+ local particle1 = particle(10, vector(12, 2), vector(-2, 3), vector(-2, 2), 5, dummy_size_ratio_over_lifetime)
+ particle1.size = 2.5
+
+ particle1:render()
+
+ assert.spy(rectfill).was_called(1)
+ assert.spy(rectfill).was_called_with(12, 2, 13, 3, colors.white)
+ end)
+
+ it('(size not in 2..3) should call circfill at particle position, with size, white (hardcoded)', function ()
+ local particle1 = particle(10, vector(12, 2), vector(-2, 3), vector(-2, 2), 10, dummy_size_ratio_over_lifetime)
+ particle1.size = 5
+
+ particle1:render()
+
+ assert.spy(circfill).was_called(1)
+ assert.spy(circfill).was_called_with(12, 2, 2.5, colors.white)
+ end)
+
+ end)
+
+end)
diff --git a/src/ingame/pfx.lua b/src/ingame/pfx.lua
new file mode 100644
index 00000000..25321cae
--- /dev/null
+++ b/src/ingame/pfx.lua
@@ -0,0 +1,106 @@
+--#if pfx
+--(you should surround any require pfx with #if pfx yourself, but in case you didn't we also
+-- strip the file's content)
+
+local pfx = new_class()
+
+local particle = require("ingame/particle")
+
+-- particle effect class
+-- it is a game script, and currently only used for spin dash smoke
+-- therefore, some behaviors and parameters are hard-coded
+-- in addition, some interfaces are not what you'd commonly find in other engines,
+-- e.g. we pass position to start() (ok because we don't move during spin dash...)
+
+-- parameters
+-- spawn_period_frames number particle spawning period (frames, fractions ok)
+-- spawn_count int number of particles emitted every spawn period
+-- base_lifetime_frames number base lifetime for spawned particles (frames, fractions ok)
+-- base_velocity vector base velocity for spawned particles (px/frame)
+-- max_deviation float max factor of base_velocity magnitude used for orthogonal
+-- acceleration, randomized per particle (ratio)
+-- base_size float base max size for spawned particles (px, fraction ok)
+-- size_ratio_over_lifetime ratio -> float function returning factor of base size over lifetime ratio
+
+-- state
+-- particles {particle} sequence of particles to update and render
+-- is_emitting bool is the particle effect playing, i.e. spawning particles periodically?
+-- frame_time float current time since started playing, modulo spawn_period_frames
+-- position vector current position, used as a base to determine where to spawn new particles
+-- mirror_x bool if true, mirror particle velocity on X
+
+function pfx:init(spawn_period_frames, spawn_count, base_lifetime_frames, base_velocity, max_deviation, base_size, size_ratio_over_lifetime)
+ -- parameters
+ self.spawn_period_frames = spawn_period_frames
+ self.spawn_count = spawn_count
+ self.base_lifetime_frames = base_lifetime_frames
+ self.base_velocity = base_velocity
+ self.max_deviation = max_deviation
+ self.base_size = base_size
+ self.size_ratio_over_lifetime = size_ratio_over_lifetime
+
+ -- state
+ self.particles = {}
+ self.is_emitting = false
+ -- more correct to setup, but commented out to spare characters (start() will set it anyway,
+ -- and frame_time/position are only accessed if is_emitting, which only start() can set to true)
+ -- self.frame_time = 0
+ -- self.position = vector.zero()
+end
+
+function pfx:start(position, mirror_x)
+ self.is_emitting = true
+ self.frame_time = 0
+ self.position = position
+ self.mirror_x = mirror_x -- "or false" stripped to spare a few characters, as nil has same behavior as false
+end
+
+function pfx:stop()
+ self.is_emitting = false
+end
+
+function pfx:spawn_particle()
+ local initial_frame_velocity = self.base_velocity:copy()
+ if self.mirror_x then
+ initial_frame_velocity.x = -initial_frame_velocity.x
+ end
+
+ -- apply random orthogonal velocity variation to cause motion deviation over time
+ local frame_accel = initial_frame_velocity:rotated_90_cw() * (rnd(2 * self.max_deviation) - self.max_deviation)
+ add(self.particles, particle(self.base_lifetime_frames, self.position, initial_frame_velocity, frame_accel,
+ self.base_size, self.size_ratio_over_lifetime))
+end
+
+-- update each pfx
+function pfx:update()
+ -- in a reverse loop, delete particles that have reached end of lifetime
+ -- reverse iteration to avoid messing up with the loop when removing entries
+ for i = #self.particles, 1, -1 do
+ local should_stay_alive = self.particles[i]:update_and_check_alive()
+ if not should_stay_alive then
+ deli(self.particles, i)
+ end
+ end
+
+ if self.is_emitting then
+ -- update time and check spawn_period_frames to see if we should spawn new particles
+ self.frame_time = self.frame_time + 1
+ if self.frame_time >= self.spawn_period_frames then
+ self.frame_time = 0
+ for i = 1, self.spawn_count do
+ self:spawn_particle()
+ end
+ end
+ end
+end
+
+-- render each pfx at its current location
+function pfx:render()
+ -- render existing particles
+ -- particles can live after pfx stopped emitting, so don't check for self.is_emitting here
+ foreach(self.particles, particle.render)
+end
+
+--#endif
+
+return pfx
diff --git a/src/ingame/pfx_utest.lua b/src/ingame/pfx_utest.lua
new file mode 100644
index 00000000..ba9826c1
--- /dev/null
+++ b/src/ingame/pfx_utest.lua
@@ -0,0 +1,179 @@
+require("test/bustedhelper_ingame")
+local pfx = require("ingame/pfx")
+
+local particle = require("ingame/particle")
+
+describe('pfx', function ()
+
+ local function dummy_size_ratio_over_lifetime(life_ratio)
+ return life_ratio
+ end
+
+ describe('init', function ()
+
+ it('should create an pfx with passed frame period, and an empty sequence of particles', function ()
+ local pfx1 = pfx(10, 4, 60, vector(2, 3), 0.5, 5.5, dummy_size_ratio_over_lifetime)
+ assert.are_same({10, 4, 60, vector(2, 3), 0.5, 5.5, dummy_size_ratio_over_lifetime, {}, false},
+ {pfx1.spawn_period_frames, pfx1.spawn_count, pfx1.base_lifetime_frames,
+ pfx1.base_velocity, pfx1.max_deviation, pfx1.base_size, pfx1.size_ratio_over_lifetime,
+ pfx1.particles, pfx1.is_emitting})
+ end)
+
+ end)
+
+ describe('start', function ()
+
+ it('should set is_emitting to true, reset frame_time to 0, set position and mirror_x', function ()
+ local pfx1 = pfx(10, 4, 60, vector(2, 3), 0.5, 5.5, dummy_size_ratio_over_lifetime)
+ pfx1:start(vector(2, 4), true)
+ assert.are_same({true, 0, vector(2, 4), true}, {pfx1.is_emitting, pfx1.frame_time, pfx1.position, pfx1.mirror_x})
+ end)
+
+ end)
+
+ describe('stop', function ()
+
+ it('should set is_emitting to false', function ()
+ local pfx1 = pfx(10, 4, 60, vector(2, 3), 0.5, 5.5, dummy_size_ratio_over_lifetime)
+ pfx1:stop()
+ assert.is_false(pfx1.is_emitting)
+ end)
+
+ end)
+
+ describe('spawn_particle', function ()
+
+ it('should add a new particle to the sequence', function ()
+ -- max deviation 0 to avoid trickiness of testing random
+ local pfx1 = pfx(10, 4, 60, vector(2, 3), 0, 5.5, dummy_size_ratio_over_lifetime)
+ pfx1.position = vector(10, 20)
+
+ pfx1:spawn_particle()
+
+ assert.are_same({particle(60, vector(10, 20), vector(2, 3), vector(0, 0), 5.5, dummy_size_ratio_over_lifetime)}, pfx1.particles)
+ end)
+
+ it('(mirror_x: true) should add a new particle with velocity mirrored od X', function ()
+ local pfx1 = pfx(10, 5, 60, vector(10, 5), 0, 3, dummy_size_ratio_over_lifetime)
+ pfx1.position = vector(10, 20)
+ pfx1.mirror_x = true
+
+ pfx1:spawn_particle()
+
+ -- vector(10, 5) -> vector(-10, 5)
+ assert.are_same({particle(60, vector(10, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime)}, pfx1.particles)
+ end)
+
+ end)
+
+ describe('update', function ()
+
+ setup(function ()
+ -- we don't want to rewrite the behavior of incrementing elapsed_frames and checking lifetime,
+ -- so spy.on instead of stubbing
+ spy.on(particle, "update_and_check_alive")
+ stub(pfx, "spawn_particle")
+ end)
+
+ teardown(function ()
+ particle.update_and_check_alive:revert()
+ pfx.spawn_particle:revert()
+ end)
+
+ after_each(function ()
+ particle.update_and_check_alive:clear()
+ pfx.spawn_particle:clear()
+ end)
+
+ it('should call update_and_check_alive on each particle', function ()
+ local pfx1 = pfx(10, 4, 60, vector(2, 3), 0, 5.5, dummy_size_ratio_over_lifetime)
+ add(pfx1.particles, particle(60, vector(10, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+ add(pfx1.particles, particle(60, vector(20, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+ add(pfx1.particles, particle(60, vector(30, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+
+ pfx1:update()
+
+ assert.spy(particle.update_and_check_alive).was_called(3)
+ assert.spy(particle.update_and_check_alive).was_called_with(match.ref(pfx1.particles[1]))
+ assert.spy(particle.update_and_check_alive).was_called_with(match.ref(pfx1.particles[2]))
+ assert.spy(particle.update_and_check_alive).was_called_with(match.ref(pfx1.particles[3]))
+ end)
+
+ it('should delete particles that reached end of lifetime from the particles sequence', function ()
+ local pfx1 = pfx(10, 4, 60, vector(2, 3), 0, 5.5, dummy_size_ratio_over_lifetime)
+ add(pfx1.particles, particle(2, vector(10, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+ -- no velocity to simplify update check
+ add(pfx1.particles, particle(2, vector(20, 20), vector( 0, 0), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+ add(pfx1.particles, particle(2, vector(30, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+ pfx1.particles[1].elapsed_frames = 1
+ pfx1.particles[3].elapsed_frames = 1
+
+ pfx1:update()
+
+ assert.are_equal(1, #pfx1.particles)
+ -- any test to identify the 2nd particle is fine (no velocity so position hasn't changed)
+ assert.are_equal(20, pfx1.particles[1].position.x)
+ end)
+
+ it('(not emitting) should not spawn particle', function ()
+ local pfx1 = pfx(10, 4, 60, vector(2, 3), 0, 5.5, dummy_size_ratio_over_lifetime)
+ add(pfx1.particles, particle(2, vector(10, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+ add(pfx1.particles, particle(2, vector(20, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+
+ pfx1:update()
+
+ assert.spy(pfx.spawn_particle).was_not_called()
+ end)
+
+ it('(emitting, more than one frame before period end) should not spawn particle', function ()
+ local pfx1 = pfx(10, 4, 60, vector(2, 3), 0, 5.5, dummy_size_ratio_over_lifetime)
+ pfx1.is_emitting = true
+ pfx1.frame_time = 8
+
+ pfx1:update()
+
+ assert.spy(pfx.spawn_particle).was_not_called()
+ end)
+
+ it('(emitting, one frame before period end) should spawn new particle', function ()
+ local pfx1 = pfx(10, --[[spawn_count:]] 3, 60, vector(2, 3), 0, 5.5, dummy_size_ratio_over_lifetime)
+ pfx1.is_emitting = true
+ pfx1.frame_time = 9
+
+ pfx1:update()
+
+ assert.spy(pfx.spawn_particle).was_called(3)
+ assert.spy(pfx.spawn_particle).was_called_with(match.ref(pfx1))
+ end)
+
+ end)
+
+ describe('render', function ()
+
+ setup(function ()
+ stub(particle, "render")
+ end)
+
+ teardown(function ()
+ particle.render:revert()
+ end)
+
+ after_each(function ()
+ particle.render:clear()
+ end)
+
+ it('should call render on each particle"', function ()
+ local pfx1 = pfx()
+ add(pfx1.particles, particle(2, vector(10, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+ add(pfx1.particles, particle(2, vector(10, 20), vector(-10, 5), vector(0, 0), 3, dummy_size_ratio_over_lifetime))
+
+ pfx1:render()
+
+ assert.spy(particle.render).was_called(2)
+ assert.spy(particle.render).was_called_with(match.ref(pfx1.particles[1]))
+ assert.spy(particle.render).was_called_with(match.ref(pfx1.particles[2]))
+ end)
+
+ end)
+
+end)
diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua
index 44a70e48..8be1ec50 100644
--- a/src/ingame/playercharacter.lua
+++ b/src/ingame/playercharacter.lua
@@ -3,15 +3,39 @@ local input = require("engine/input/input")
local animated_sprite = require("engine/render/animated_sprite")
local collision_data = require("data/collision_data")
-local pc_data = require("data/playercharacter_data")
+local pc_data = require("data/playercharacter_numerical_data")
+local pc_sprite_data = require("data/playercharacter_sprite_data")
+--#if pfx
+local pfx = require("ingame/pfx")
+--#endif
local motion = require("platformer/motion")
local world = require("platformer/world")
local audio = require("resources/audio")
local visual = require("resources/visual_common")
-- we should require ingameadd-on in main, as early as possible
+--#if debug_character
+local outline = require("engine/ui/outline")
+--#endif
+
local player_char = new_class()
+--#if pfx
+-- helper for spin dash dust
+function player_char.pfx_size_ratio_over_lifetime(life_ratio)
+ -- make size grow quickly at start of lifetime, but shrink again around 1/3 of lifetime
+ -- (to avoid big particles hiding character bottom too much)
+ -- negative size will draw nothing, no need to clamp
+ local junction = 0.36
+ if life_ratio < junction then
+ -- linear piece, start at size 0.4 at 0, ends at 1 at junction
+ return 0.4 * (1 - life_ratio / junction) + life_ratio / junction
+ end
+ -- linear piece start at 1 at junction, ends at 0 at 1
+ return 1 - (life_ratio - junction) / (1 - junction)
+end
+--#endif
+
-- parameters cached from PC data
-- debug_move_max_speed (#cheat) float move max speed in debug mode
@@ -42,25 +66,38 @@ local player_char = new_class()
-- velocity vector current velocity in platformer mode (px/frame)
-- debug_velocity (#cheat) vector current velocity in debug mode (m/s)
-- slope_angle float slope angle of the current ground (clockwise turn ratio)
+-- late_jump_slope_angle float (late jump feature only) slope angle of the last ground
-- ascending_slope_time float time before applying full slope factor, when ascending a slope (s)
-- (#original_slope_features)
+-- spin_dash_rev float spin dash charge (aka revving) value (float to allow drag over time)
-- move_intention vector current move intention (binary cardinal)
--- jump_intention bool current intention to start jump (consumed on jump)
+-- jump_intention bool current intention to start jump or spin dash (consumed on jump or spin dash)
-- hold_jump_intention bool current intention to hold jump (always true when jump_intention is true)
-- should_jump bool should the character jump when next frame is entered? used to delay variable jump/hop by 1 frame
-- has_jumped_this_frame bool has the character started a jump/hop this frame?
-- can_interrupt_jump bool can the character interrupted his jump once?
+-- time_left_for_late_jump int (late jump feature only) number of frames left to do a late jump after falling. Initialized on fall, decrement each frame.
-- anim_spr animated_sprite animated sprite component
-- anim_run_speed float Walk/Run animation playback speed. Reflects ground_speed, but preserves value even when falling.
-- continuous_sprite_angle float Sprite angle with high precision used internally. Reflects slope_angle when standing, but gradually moves toward 0 (upward) when airborne.
-- To avoid ugly sprite rotations, only a few angle steps are actually used on render.
--- should_play_spring_jump bool Set to true when sent upward in the air thanks to spring, and not falling down yet
+-- is_sprite_diagonal bool Derived from continuous_sprite_angle. True iff continuous angle is closer to diagonal (45-degree multiple).
+-- sprite_angle float Derived from continuous_sprite_angle. Sprite angle actually used for rendering. Rounded to multiple of 0.25. Takes 45-degree sprite variant into account.
+-- To avoid ugly sprite rotations, only a few angle steps are actually used on render.
+-- last_copied_double_row float Last sprite double row index copied to spritesheet memory, tracked to avoid copying it every frame
+-- should_play_spring_jump bool Set to true when sent upward in the air thanks to spring, and not falling down yet
-- brake_anim_phase int 0: no braking anim. 1: brake start. 2: brake reverse.
+-- smoke_pfx (#pfx) pfx particle system used to render smoke during spin dash charge
+
-- last_emerald_warp_nb (cheat) int number of last emerald character warped to
--- debug_rays (debug_character) {...} rays to draw for debug render this frame
+-- debug_rays (#debug_character) {start = vector, direction_vector = vector, distance = number, hit = bool}
+-- rays to draw for debug render this frame
+-- debug_mask_global_tile_locations (#debug_collision_mask)
+-- {tile_location}
+-- tile locations on which we should debug render collision mask
function player_char:init()
--#if cheat
self.debug_move_max_speed = pc_data.debug_move_max_speed
@@ -69,7 +106,17 @@ function player_char:init()
self.debug_move_friction = pc_data.debug_move_friction
--#endif
- self.anim_spr = animated_sprite(pc_data.sonic_animated_sprite_data_table)
+ self.anim_spr = animated_sprite(pc_sprite_data.sonic_animated_sprite_data_table)
+
+--#if pfx
+ self.smoke_pfx = pfx(pc_data.spin_dash_dust_spawn_period_frames,
+ pc_data.spin_dash_dust_spawn_count,
+ pc_data.spin_dash_dust_lifetime_frames,
+ vector(pc_data.spin_dash_dust_base_init_velocity_x, pc_data.spin_dash_dust_base_init_velocity_y),
+ pc_data.spin_dash_dust_max_deviation,
+ pc_data.spin_dash_dust_base_max_size,
+ player_char.pfx_size_ratio_over_lifetime)
+--#endif
--#if cheat
-- exceptionally not in setup, because this member but be persistent persist after warping
@@ -90,9 +137,9 @@ function player_char:setup()
self.active_loop_layer = 1
self.ignore_launch_ramp_timer = 0
- -- impossible value makes sure that first set_ground_tile_location
- -- will trigger change event
- self.ground_tile_location = location(-1, -1)
+ -- no ground -> nil
+ -- self.ground_tile_location = nil -- commented out to spare characters
+ -- undefined position convention
self.position = vector(-1, -1)
self.ground_speed = 0
self.horizontal_control_lock_timer = 0
@@ -104,9 +151,11 @@ function player_char:setup()
-- slope_angle starts at 0 instead of nil to match standing state above
-- (if spawning in the air, fine, next update will reset angle to nil)
self.slope_angle = 0
+ self.late_jump_slope_angle = 0
--#if original_slope_features
self.ascending_slope_time = 0
--#endif
+ self.spin_dash_rev = 0
self.move_intention = vector.zero()
self.jump_intention = false
@@ -114,26 +163,42 @@ function player_char:setup()
self.should_jump = false
self.has_jumped_this_frame = false
self.can_interrupt_jump = false
+ self.time_left_for_late_jump = 0
+
+ self:set_continuous_sprite_angle(0)
+ -- equivalent to:
+ -- self.continuous_sprite_angle = 0
+ -- self.is_sprite_diagonal = false
+ -- self.sprite_angle = 0
+
+ -- no need to setup that, update_sprite_row_and_play_sprite_animation will set it to match idle sprite below
+ -- nil is not equal to any number, so the first call will always copy a row and initialize last_copied_double_row
+ -- self.last_copied_double_row = nil
- self.anim_spr:play("idle")
+ -- must be called after setting angle, as it checks if we need diagonal sprites
+ self:update_sprite_row_and_play_sprite_animation("idle")
self.anim_run_speed = 0
- self.continuous_sprite_angle = 0
+
self.should_play_spring_jump = false
self.brake_anim_phase = 0
--#if debug_character
self.debug_rays = {}
--#endif
+
+--#if debug_collision_mask
+ self.debug_mask_global_tile_locations = {}
+--#endif
end
-- return true iff character is grounded
function player_char:is_grounded()
- return self.motion_state == motion_states.standing or self.motion_state == motion_states.rolling
+ return contains({motion_states.standing, motion_states.rolling, motion_states.crouching, motion_states.spin_dashing}, self.motion_state)
end
-- return true iff character is curled
function player_char:is_compact()
- return self.motion_state == motion_states.air_spin or self.motion_state == motion_states.rolling
+ return contains({motion_states.air_spin, motion_states.rolling, motion_states.crouching, motion_states.spin_dashing}, self.motion_state)
end
function player_char:get_center_height()
@@ -144,9 +209,13 @@ function player_char:get_full_height()
return self:is_compact() and pc_data.full_height_compact or pc_data.full_height_standing
end
--- return quadrant tangent right (forward) unit vector
-function player_char:get_quadrant_right()
- return dir_vectors[(self.quadrant - 1) % 4]
+-- return horizontal direction relative to quadrant in world direction, depending on horizontal_dir
+-- equivalent to returning quadrant down, rotated by 90 degrees cw if horizontal_dir is left,
+-- and by 90 degrees ccw if horizontal_dir is right
+function player_char:get_horizontal(horizontal_dir)
+ -- See formula of rotate_dir_90_cw in direction_ext.lua (not included for minimal chars usage)
+ -- => we want + 1 for CW, so when dir is left, so we must oppose the horizontal sign, hence `-`
+ return (self.quadrant - horizontal_dir_signs[horizontal_dir]) % 4
end
-- return quadrant normal down (interior) unit vector
@@ -189,8 +258,14 @@ end
function player_char:warp_to(position)
self.position = position
- -- character is initialized standing, but let him fall if he is spawned in the air
- -- if grounded, also allows to set ground tile properly
+ -- start falling, then call check_escape_from_ground
+ -- if no ground is found, character will just fall
+ -- otherwise (even if just touching ground), state will be set to standing
+ -- note that unlike running, we never snap down
+ -- we could also not set motion state at all, as the next update will detect no ground
+ -- and start character fall if needed (but if late jump feature is enabled, it may allow
+ -- player to oddly jump in the air just after warping)
+ self:enter_motion_state(motion_states.falling)
self:check_escape_from_ground()
end
@@ -288,11 +363,53 @@ function player_char:set_ground_tile_location(global_tile_loc)
end
end
+function player_char:set_continuous_sprite_angle(angle)
+ self.continuous_sprite_angle = angle
+ self:update_sprite_angle_parameters()
+end
+
+function player_char:update_sprite_angle_parameters()
+ local sprite_angle = 0
+ local is_sprite_diagonal = false
+
+ if self.anim_spr.current_anim_key == "idle" then
+ -- snap render angle to a few set of values (90 degrees steps)
+ -- originally we always used angle = 0 as Sonic cannot normally be idle on a wall or ceiling,
+ -- but in edge cases (speed reaches 0 for 1 frame on a slope, Sonic get stuck inside wall and
+ -- we want to debug quadrant) it can happen and then it's more useful to show at least
+ -- the correct 90-degree rotation (as we don't have 45-deg sprite variants)
+ -- 90 degrees is 0.25 = 1/4, so by multiplying by 4, each integer represent a 90-degree step
+ -- we just need to add 0.5 before flooring to effectively round to the closest step, then go back
+ sprite_angle = flr(4 * self.continuous_sprite_angle + 0.5) / 4
+ elseif contains({"walk", "run"}, self.anim_spr.current_anim_key) then
+ -- snap render angle to a few set of values (45 degrees steps), classic style
+ -- (unlike Freedom Planet and Sonic Mania)
+ -- 45 degrees is 0.125 = 1/8, so by multiplying by 8, each integer represent a 45-degree step
+ -- we just need to add 0.5 before flooring to effectively round to the closest step, then go back
+ sprite_angle = flr(8 * self.continuous_sprite_angle + 0.5) / 8
+
+ -- a computed rotation of 45 degrees would result in an ugly sprite
+ -- so we only use rotations multiple of 90 degrees, using handmade 45-degree
+ -- sprites when we want a better angle resolution
+ if sprite_angle % 0.25 ~= 0 then
+ is_sprite_diagonal = true
+
+ -- rotated sprite embeds a rotation of 45 degrees, so if not flipped, rotate by angle - 45 degrees
+ -- if flipped, the sprite is 45 degrees *behind* the horizontal left, so we must add 45 degrees instead
+ local flip_x = self.orientation == horizontal_dirs.left
+ sprite_angle = sprite_angle + (flip_x and 1 or -1) * 0.125
+ end
+ end
+
+ self.sprite_angle = sprite_angle % 1
+ self.is_sprite_diagonal = is_sprite_diagonal
+end
+
-- set slope angle and update quadrant
-- if force_upward_sprite is true, set sprite angle to 0
-- else, set sprite angle to angle (if not nil)
function player_char:set_slope_angle_with_quadrant(angle, force_upward_sprite)
- assert(angle == nil or 0 <= angle and angle <= 1, "player_char:set_slope_angle_with_quadrant: angle is "..tostr(angle)..", should be nil or between 0 and 1 (apply % 1 is needed)")
+ assert(angle == nil or 0 <= angle and angle < 1, "player_char:set_slope_angle_with_quadrant: angle is "..tostr(angle)..", should be nil or between 0 and 1 (excluded), please apply % 1 if needed")
self.slope_angle = angle
@@ -300,19 +417,34 @@ function player_char:set_slope_angle_with_quadrant(angle, force_upward_sprite)
-- this is to prevent character sprite from switching straight upward immediately
-- on fall
if force_upward_sprite then
- self.continuous_sprite_angle = 0
+ self:set_continuous_sprite_angle(0)
elseif angle then
- self.continuous_sprite_angle = angle
+ self:set_continuous_sprite_angle(angle)
end
self.quadrant = world.angle_to_quadrant(angle)
end
function player_char:update()
+
+--#if debug_collision_mask
+ clear_table(self.debug_mask_global_tile_locations)
+--#endif
+
+--#if debug_character
+ -- clear the debug rays to start anew for this frame (don't clear them after rendering
+ -- so you can continue seeing them during debug pause)
+ -- OPTIMIZE: pool the rays instead (you can also make them proper structs)
+ clear_table(self.debug_rays)
+--#endif
+
-- in stage_intro cartridge, we want Sonic to stay idle, so no input
-- but update physics and render as usual
--#if ingame
+-- input is used by normal (non-attract) mode and attract-mode in recorder sub-mode only
+--#if normal_mode || recorder
+
--#if busted
if flow.curr_state.type == ':stage' then
--#endif
@@ -321,15 +453,25 @@ function player_char:update()
end
--#endif
+--(not attract_mode)
+--#endif
+
--(ingame)
--#endif
self:update_motion()
self:update_anim()
self.anim_spr:update()
+
+--#if pfx
+ self.smoke_pfx:update()
+--#endif
end
--#if ingame
+-- input is used by normal (non-attract) mode and attract-mode in recorder sub-mode only
+--#if normal_mode || recorder
+
-- update intention based on current input
function player_char:handle_input()
if self.control_mode == control_modes.human then
@@ -341,6 +483,11 @@ function player_char:handle_input()
if not self:is_grounded() or self.horizontal_control_lock_timer <= 0 then
-- horizontal input
+ -- note: in the original game, pressing left + right at the same time makes the game think
+ -- Sonic is moving toward left at speed 0, then braking to the right, making him walk toward right
+ -- at a very low speed. This is useful for a quick startup in TAS and a quick controlled break
+ -- when going to the right; but not useful otherwise and not feasible on some gamepads; so we don't
+ -- emulate this behavior (see Start faster on http://tasvideos.org/GameResources/Genesis/SonicTheHedgehog.html)
if input:is_down(button_ids.left) then
player_move_intention:add_inplace(vector(-1, 0))
elseif input:is_down(button_ids.right) then
@@ -360,7 +507,7 @@ function player_char:handle_input()
if self.horizontal_control_lock_timer > 0 then
-- decrement control lock frame timer
-- normally it's better to update non-intention state vars
- -- in a normal update method not _handle_input, but since we know
+ -- in a normal update method not handle_input, but since we know
-- that both are updated at 60FPS, it shouldn't be a problem here
self.horizontal_control_lock_timer = self.horizontal_control_lock_timer - 1
end
@@ -372,12 +519,89 @@ function player_char:handle_input()
player_move_intention:add_inplace(vector(0, 1))
end
+--#if recorder
+ -- No unit test for this code, it is only meant for temporary usage to record intention changes and find good async delays
+ -- in attract_mode_scenario_async. #recorder symbol should be dfined together with #tostring to make meaningful logs.
+
+ -- detect move intention direction change
+ if self.move_intention ~= player_move_intention then
+ -- print usable Lua directly to the log (we'll just have to remove [recorder] at the start)
+ -- ex:
+ -- yield_delay_frames(10)
+ -- pc.move_intention = vector(1, 0)
+ if total_frames > 0 then
+ log("yield_delay_frames("..total_frames..")", "recorder")
+
+ -- reset total frames as we want relative delays since last record
+ total_frames = 0
+ end
+ log("pc.move_intention = "..player_move_intention, "recorder")
+
+ -- reset total frames as we want relative delays since last record
+ total_frames = 0
+ end
+--#endif
+
self.move_intention = player_move_intention
-- jump
local is_jump_input_down = input:is_down(button_ids.o) -- convenient var for optional pre-check
- -- set jump intention each frame, don't set it to true for later consumption to avoid sticky input
+ -- set jump intention *each frame*, don't set it to true for later consumption to avoid sticky input
-- without needing a reset later during update
+
+--#if recorder
+ -- No unit test for this code, it is only meant for temporary usage to record intention changes and find good async delays
+ -- in attract_mode_scenario_async. #recorder symbol should be defined together with #tostring and #log in some 'recorder' config.
+
+ local has_jump_intention_this_frame = is_jump_input_down and input:is_just_pressed(button_ids.o)
+
+ -- safety code to detect a jump intention that was not consumed (and player doesn't keep trying
+ -- to jump, rare as they'd need to repeat pressing the button every frame)
+ -- this allows to clear a jump intention recorded by player pressing jump button
+ -- while not able to jump (e.g. in the air) as it would be sticky and cause an unwanted
+ -- chained jump as soon as able to jump again (e.g. when landing)
+ if self.jump_intention and not has_jump_intention_this_frame then
+ -- usable Lua ex:
+ -- yield_delay_frames(10)
+ -- pc.jump_intention = false
+ if total_frames > 0 then
+ log("yield_delay_frames("..total_frames..")", "recorder")
+
+ -- reset total frames as we want relative delays since last record
+ total_frames = 0
+ end
+ log("pc.jump_intention = false", "recorder")
+ end
+
+ -- detect start jump
+ if not self.jump_intention and has_jump_intention_this_frame then
+ -- usable Lua ex:
+ -- yield_delay_frames(10)
+ -- pc.jump_intention = true
+ if total_frames > 0 then
+ log("yield_delay_frames("..total_frames..")", "recorder")
+
+ -- reset total frames as we want relative delays since last record
+ total_frames = 0
+ end
+ log("pc.jump_intention = true", "recorder")
+ end
+
+ -- detect start and stop holding jump intention
+ if self.hold_jump_intention ~= is_jump_input_down then
+ -- usable Lua ex:
+ -- yield_delay_frames(10)
+ -- pc.hold_jump_intention = true
+ if total_frames > 0 then
+ log("yield_delay_frames("..total_frames..")", "recorder")
+
+ -- reset total frames as we want relative delays since last record
+ total_frames = 0
+ end
+ log("pc.hold_jump_intention = "..tostr(is_jump_input_down), "recorder")
+ end
+--#endif
+
self.jump_intention = is_jump_input_down and input:is_just_pressed(button_ids.o)
self.hold_jump_intention = is_jump_input_down
@@ -398,6 +622,8 @@ function player_char:handle_input()
end
end
+--(not attract_mode)
+--#endif
function player_char:force_move_right()
-- force player to move to the right
@@ -536,12 +762,13 @@ end
-- return the position of the ground sensor in quadrant_horizontal_dir when the character center is at center_position
-- subpixels are ignored
function player_char:get_ground_sensor_position_from(center_position, quadrant_horizontal_dir)
-
- -- ignore subpixels from center position in qx (collision checks use Sonic's integer position,
- -- but we keep exact qy coordinate to get the exact ground sensor qy, and thus exact distance to ground)
local x = center_position.x
local y = center_position.y
+ -- ignore subpixels from center position in qx (collision checks use Sonic's integer position,
+ -- but we keep exact qy coordinate to get the exact ground sensor qy, and thus exact distance to ground)
+ -- this is important to avoid assert in iterate_over_collision_tiles as the qx value will be used
+ -- as (integer) index to get qcolumn height
-- vertical: up (1) and down (3)
if self.quadrant % 2 == 1 then
x = flr(x)
@@ -555,13 +782,53 @@ function player_char:get_ground_sensor_position_from(center_position, quadrant_h
-- rotate proper vector (initially horizontal) for quadrant compatibility, but make sure to apply coord flooring
-- *afterward* so it applies to the final coord and we don't rotate a +2.5 -> +2 into a -2 instead of having -3
local offset_qx_vector = self:quadrant_rotated(pc_data.ground_sensor_extent_x * horizontal_dir_vectors[quadrant_horizontal_dir])
- -- brutal way to floor coordinates are rotation, without having to extract qx, recreating (qx, 0) vector and rotating again
+ -- brutal way to floor coordinates after rotation, without having to extract qx, recreating (qx, 0) vector and rotating again
offset_qx_vector = vector(flr(offset_qx_vector.x), flr(offset_qx_vector.y))
return qx_floored_bottom_center + offset_qx_vector
end
--- helper method for compute_closest_ground_query_info and is_blocked_by_ceiling_at
+-- Return the position of the wall sensor position when checking wall in quadrant_horizontal_dir
+-- (relatively to current quadrant) and character center is at center_position.
+-- Note that in the future, we may merge this method with get_ground_sensor_position_from and
+-- also move the wall sensor position to the front of Sonic; if so, re-add parameter quadrant_horizontal_dir,
+function player_char:get_wall_sensor_position_from(center_position)
+ local x = center_position.x
+ local y = center_position.y
+
+ -- ignore subpixels from center position in qx (see get_ground_sensor_position_from)
+ -- however, since it's a wall, the test is reversed: up and down means walls are checked with horizontal
+ -- raycast where qx is actually y, which must be floored
+ if self.quadrant % 2 == 0 then
+ x = flr(x)
+ else
+ y = flr(y)
+ end
+
+ local qx_floored_center_position = vector(x, y)
+
+ -- http://info.sonicretro.org/SPG:Solid_Tiles#Wall_Sensors_.28E_and_F.29
+ -- On flat ground, lower the wall sensor position so Sonic can detect low steps as in Marble Zone
+ -- (in pico island that will just be vertical springs placed on the ground)
+ -- Note that compute_closest_wall_query_info already has a parameter sensor_offset_qy
+ -- for that, so consider using it instead
+ if self.slope_angle == 0 then
+ -- normally we should use a clean constant, but at this point we just hardcode the offset
+ -- more exactly, unlike the original games we don't set the wall sensor qy lower when compact,
+ -- because it made the wall sensor very low when rolling, and combined with spin dash it was easy
+ -- to hit a very low slope (esp. in a loop) and get blocked for no reason
+ -- instead, we always place the wall sensor 6px above the ground
+ -- (ideal to detect vertical spring of height 6px while not detecting low ground in loops at high speed)
+ return qx_floored_center_position + (self:get_center_height() - 6) * self:get_quadrant_down()
+ else
+ -- return copy of vector for safety
+ -- in practice, it's not modified in-place, but we prefer satefy to saving compressed chars
+ -- unless we're really tight on budget
+ return qx_floored_center_position:copy()
+ end
+end
+
+-- helper method for compute_closest_ground_query_info, compute_closest_ceiling_query_info, compute_closest_wall_query_info
-- for given player character pc, it iterates over tiles from start to last (defined via offset from sensor position), providing distance from sensor_position_base + sensor_offset_qy along q-down (foot or head)
-- to q-column q-top (with reverse tile support) to custom callbacks which should return ground query info to closest ground/ceiling in quadrant direction
-- pass it a quadrant of interest (direction used to check collisions), iteration start and last tile locations
@@ -579,7 +846,7 @@ local function iterate_over_collision_tiles(pc, collision_check_quadrant, start_
-- apply sensor offset along check quadrant down (only used for ceiling, so actually upward to get head top position)
local sensor_position = sensor_position_base + sensor_offset_qy * collision_check_quadrant_down
- assert(world.get_quadrant_x_coord(sensor_position, collision_check_quadrant) % 1 == 0, "iterate_over_collision_tiles: sensor_position qx must be floored, found "..sensor_position)
+ assert(world.get_quadrant_x_coord(sensor_position, collision_check_quadrant) % 1 == 0, "iterate_over_collision_tiles: sensor_position qx for collision_check_quadrant: "..collision_check_quadrant.." must be floored, found "..sensor_position)
-- deduce start and last tile from offset from the sensor position
-- always oriented with check quadrant (by convention we check from q-top to q-bottom)
@@ -628,6 +895,7 @@ local function iterate_over_collision_tiles(pc, collision_check_quadrant, start_
local tile_region_loc = curr_stage_state:global_to_region_location(curr_global_tile_loc)
local visual_tile_id = mget(tile_region_loc.i, tile_region_loc.j)
local is_oneway = fget(visual_tile_id, sprite_flags.oneway)
+ local ignore_loop_layer = fget(visual_tile_id, sprite_flags.ignore_loop_layer)
--#if ingame
@@ -638,9 +906,10 @@ local function iterate_over_collision_tiles(pc, collision_check_quadrant, start_
-- a. ramps just after launching
-- b. loops on inactive layer from PC's point-of-view
-- c. one-way platforms unless we check collision downward
- if pc.ignore_launch_ramp_timer > 0 and visual_tile_id == visual.launch_ramp_last_tile_id or
- pc.active_loop_layer == 1 and curr_stage_state:is_tile_in_loop_exit(curr_global_tile_loc) or
- pc.active_loop_layer == 2 and curr_stage_state:is_tile_in_loop_entrance(curr_global_tile_loc) or
+ if pc.ignore_launch_ramp_timer > 0 and visual_tile_id == visual_ingame_data.launch_ramp_last_tile_id or
+ not ignore_loop_layer and
+ (pc.active_loop_layer == 1 and curr_stage_state:is_tile_in_loop_exit(curr_global_tile_loc) or
+ pc.active_loop_layer == 2 and curr_stage_state:is_tile_in_loop_entrance(curr_global_tile_loc)) or
is_oneway and collision_check_quadrant ~= directions.down then
ignore_tile = true
end
@@ -671,14 +940,26 @@ local function iterate_over_collision_tiles(pc, collision_check_quadrant, start_
-- make sure to convert the global tile location into region coordinates
qcolumn_height, slope_angle = world.compute_qcolumn_height_at(curr_global_tile_loc - region_topleft_loc,
qcolumn_index0, collision_check_quadrant, ignore_reverse)
+
+--#if debug_collision_mask
+ -- add global tile location (to avoid unstability on frames where we change regions)
+ -- to debug render, if there was a collision tile
+ -- (remember that with land_on_empty_qcolumn feature, qcolumn_height may be 0 yet there is a collision tile,
+ -- so testing slope_angle is more reliable)
+ if slope_angle then
+ add(pc.debug_mask_global_tile_locations, curr_global_tile_loc)
+ end
+--#endif
end
- -- a q-column height of 0 doesn't mean that there is ground just below relative offset qy = 0,
- -- but that the q-column is empty and we don't know what is more below
+ -- if ground is found, including ground of height 0 thx to land_on_empty_qcolumn, slope_angle is never nil
+ -- so check that, it's more reliable than the ground height
+ -- if no ground is found (ground height is 0 and slope angle is nil),
+ -- we still don't know whether there is something below
-- so don't do anything yet but check for the tile one level lower
-- (unless we've reached end of iteration with the last tile, in which case
-- the next tile would be too far to snap down anyway)
- if qcolumn_height > 0 then
+ if slope_angle then
-- get q-bottom of tile to compare heights
-- when iterating q-upward (ceiling check) this is actually a q-top from character's perspective
local current_tile_qbottom = world.get_tile_qbottom(curr_global_tile_loc, collision_check_quadrant)
@@ -687,14 +968,17 @@ local function iterate_over_collision_tiles(pc, collision_check_quadrant, start_
-- PICO-8 Y sign is positive up, so to get the current relative height of the sensor
-- in the current tile, you need the opposite of (quadrant-signed) (sensor_position.qy - current_tile_qbottom)
-- then subtract qcolumn_height and you get the signed distance to the current ground q-column
+ -- SYMMETRY NOTE: we decided to *not* subtract 1 when collision_check_quadrants is "positive" ie right or down,
+ -- although that would be the thing to do if we take the convention that sensor position covers a full pixel,
+ -- and we want the distance to that full pixel (actually its edge in the collision_check_quadrant direction)
+ -- Instead, we picked the convention that the sensor position is a CROSS between 4 pixels, so we don't need to do that
+ -- and results are symmetrical. When sensor position is on the top pixel of a ground column, distance is 0.
+ -- For wall detection, sensor position does not need the +/-0.5 with flooring hack to be at visually symmetrical pixels:
+ -- since it's on a cross, it's already placed symmetrically. Therefore the +/-0.5 hack is only needed to place
+ -- *ground* sensors on *qx*, i.e. the direction orthogonal to the raycast direction aka collision_check_quadrant.
+ -- Test along collision_check_quadrant itself never needs it.
local signed_distance_to_closest_collider = world.sub_qy(current_tile_qbottom, world.get_quadrant_y_coord(sensor_position, collision_check_quadrant), collision_check_quadrant) - qcolumn_height
- -- even when checking downward, we cannot detect one-way platforms from below their surface (signed distance < 0)
- -- this way, we don't step up or get blocked by them as ceiling inadvertently, but can still just land on them
- if is_oneway and signed_distance_to_closest_collider < -1 then
- signed_distance_to_closest_collider = pc_data.max_ground_snap_height + 1
- end
-
-- callback returns ground query info, let it decide how to handle presence of collider
local result = collider_distance_callback(curr_global_tile_loc, signed_distance_to_closest_collider, slope_angle)
@@ -702,11 +986,8 @@ local function iterate_over_collision_tiles(pc, collision_check_quadrant, start_
-- if so, we return from the caller
if result then
--#if debug_character
- -- ceiling only returns true, and we are only interested in debugging ground sensor rays anyway,
- -- so only consider ground sensor result which should be a proper ground_query_info
- if type(result) == "table" then
- add(pc.debug_rays, {start = sensor_position, direction = collision_check_quadrant_down, distance = result.signed_distance, hit = result.tile_location ~= nil})
- end
+ -- store debug ray for hit or no-hit (we may have found ground/ceiling which happens to be too far)
+ add(pc.debug_rays, {start = sensor_position:copy(), direction_vector = collision_check_quadrant_down:copy(), distance = result.signed_distance, hit = result.tile_location ~= nil})
--#endif
return result
end
@@ -720,7 +1001,7 @@ local function iterate_over_collision_tiles(pc, collision_check_quadrant, start_
assert(curr_global_tile_loc == last_global_tile_loc or is_oneway)
end
- -- check fo end of iteration (reached last tile)
+ -- check for end of iteration (reached last tile)
-- we do a simple check in PICO-8 release:
--[[#pico8
--#ifn assert
@@ -745,10 +1026,8 @@ local function iterate_over_collision_tiles(pc, collision_check_quadrant, start_
local result = no_collider_callback()
--#if debug_character
- -- see similar code above with collider_distance_callback call
- if type(result) == "table" then
- add(pc.debug_rays, {start = sensor_position, direction = collision_check_quadrant_down, distance = result.signed_distance, hit = result.tile_location ~= nil})
- end
+ -- store debug ray for no-hit (result.signed_distance will just be some max detection distance + 1)
+ add(pc.debug_rays, {start = sensor_position:copy(), direction_vector = collision_check_quadrant_down:copy(), distance = result.signed_distance, hit = false})
--#endif
-- this is the final check so return the result whatever it is
@@ -759,6 +1038,16 @@ local function iterate_over_collision_tiles(pc, collision_check_quadrant, start_
end
end
+-- actual body of compute_closest_ground_query_info passed to iterate_over_collision_tiles
+-- as no_collider_callback
+-- defined before ground_check_collider_distance_callback to make it callable from there
+local function ground_check_no_collider_callback()
+ -- end of iteration, and no ground found or too far below to snap q-down
+ -- return edge case for ground considered too far below
+ -- (pc_data.max_ground_snap_height + 1, nil)
+ return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+end
+
-- actual body of compute_closest_ground_query_info passed to iterate_over_collision_tiles
-- as collider_distance_callback
-- return nil if no clear result and we must continue to iterate (until the last tile)
@@ -768,10 +1057,10 @@ local function ground_check_collider_distance_callback(tile_location, signed_dis
-- return edge case (nil, -pc_data.max_ground_escape_height - 1, 0)
-- the slope angle 0 allows to still have character stand straight (world) up visually,
-- but he's probably stuck inside the ground...
- -- by convention, we will set ground tile location to nil
- -- the reason is that we don't need to pass tile_location since when character is inside ground
- -- we don't expect tile surface effect like loop layer trigger or spike damage to happen
- return motion.ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0)
+ -- convention v3 is to ignore ground completely is too deep inside
+ -- to avoid walking with head on ceiling or inside one-way platform when a little too low
+ -- to spare characters just reuse no collider callback directly
+ return ground_check_no_collider_callback()
elseif signed_distance_to_closest_ground <= pc_data.max_ground_snap_height then
-- ground found, and close enough to snap up/down, return ground query info
-- to allow snapping + set slope angle
@@ -779,15 +1068,6 @@ local function ground_check_collider_distance_callback(tile_location, signed_dis
end
end
--- actual body of compute_closest_ground_query_info passed to iterate_over_collision_tiles
--- as no_collider_callback
-local function ground_check_no_collider_callback()
- -- end of iteration, and no ground found or too far below to snap q-down
- -- return edge case for ground considered too far below
- -- (pc_data.max_ground_snap_height + 1, nil)
- return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
-end
-
-- return ground_query_info(tile_location, signed_distance, slope_angle) where:
-- - tile_location is the location where we found the first colliding tile, or nil if no collision
-- - signed distance to closest ground from sensor_position,
@@ -814,44 +1094,179 @@ function player_char:compute_closest_ground_query_info(sensor_position)
return iterate_over_collision_tiles(self, self.quadrant, - (pc_data.max_ground_escape_height + 1), pc_data.max_ground_snap_height, sensor_position, 0, ground_check_collider_distance_callback, ground_check_no_collider_callback)
end
+-- actual body of compute_closest_ceiling_query_info passed to iterate_over_collision_tiles
+-- as no_collider_callback
+-- defined before ceiling_check_collider_distance_callback to make it callable from there
+local function ceiling_check_no_collider_callback()
+ -- end of iteration, and no ceiling found
+ return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+end
+
+-- actual body of compute_closest_ceiling_query_info passed to iterate_over_collision_tiles
+-- as collider_distance_callback
+-- return "ground query info" although it's ceiling, because depending on the angle, character may actually adhere, making it
+-- a q-up ground
+-- return nil if no clear result and we must continue to iterate (until the last tile)
+local function ceiling_check_collider_distance_callback(curr_tile_loc, signed_distance_to_closest_ceiling, slope_angle)
+ -- previous calculations already reversed sign of distance to match convention (> 0 when not touching, < 0 when inside)
+ if signed_distance_to_closest_ceiling <= 0 then
+ -- head (or body) just touching or inside ceiling
+ return motion.ground_query_info(curr_tile_loc, signed_distance_to_closest_ceiling, slope_angle)
+ else
+ -- head far touching ceiling or has some gap from ceiling
+ -- unlike ground, we never "step q-down" onto ceiling, the ceiling check only results in collision with movement interruption
+ -- or ceiling adherence, but then character started going inside ceiling (distance <= 0), therefore distance is never > 0
+ -- unless we reached ceiling_check_no_collider_callback and then it's the max + 1
+ -- TODO OPTIMIZE CPU: I'm pretty sure we can stop the search here since we found a ceiling, it's just too far,
+ -- and there's no chance we'll find a ceiling *closer* from here one.
+ -- So we can probably, like ceiling_check_no_collider_callback, do:
+ -- return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+ -- but I'll wait for the rest of new physics to work before trying that.
+ -- when you're ready, just uncomment this:
+ -- return ceiling_check_no_collider_callback()
+ return nil
+ end
+end
+
+-- similar to compute_closest_ground_query_info, but for ceiling
+-- return ground_query_info(tile_location, signed_distance, slope_angle) (see compute_closest_ground_query_info for more info)
+-- note that we return a query info with negative sign (inside ceiling) even if the detected obstacle is lower than one step up's height,
+-- because we assume that if the character could step this up, it would have and the passed
+-- sensor_position would be the resulting position, so only higher tiles will be considered
+-- so the step up itself will be ignored (e.g. when moving from a flat ground to an ascending slope)
+function player_char:compute_closest_ceiling_query_info(sensor_position)
+ assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:compute_closest_ceiling_query_info: sensor_position qx must be floored")
+
+ -- oppose_dir since we check ceiling by detecting tiles q-above, and their q-column height matters
+ -- when measured from the q-top (e.g. if there's a top half-tile maybe character head is not hitting it
+ -- depending on the exact distance; if q-bottom based, it's considered reverse so full q-height and character
+ -- head will hit it as soon as it enters the tile)
+
+ -- top must be q-above bottom or we will get stuck in infinite loop
+ -- (because to reduce tokens we compare locations directly instead of sub_qy(curr_tile_qj, last_tile_qy, quadrant_opp) >= 0
+ -- which would ensure loop end)
+
+ -- we must at least start checking ceiling 1 px above foot sensor (because when foot is just on top of tile,
+ -- the current sensor tile is actually the tile *below* the character, which is often a full tile and will bypass
+ -- ignore_reverse (see world.compute_qcolumn_height_at); in practice +4/+8 is a good offset, we pick max_ground_escape_height + 1 = 5
+ -- because it allows us to effectively check the q-higher pixels not already checked in compute_closest_ground_query_info)
+
+ -- finally, we check actual collision at head top position, so we pass an offset of self:get_full_height() (argument 5)
+ -- from here, we need:
+ -- - (max_ground_escape_height + 1 - full_height) offset for first tile according to explanation above + the fact that we consider this offset from sensor_position base + offset (full_height)
+ -- - no offset for last tile since we end checking at head top exactly, so argument 3 is 0
+ local full_height = self:get_full_height()
+ -- the pc_data constant will be replaced in-line by replace_strings if game constant info is passed correctly
+ -- ("text "..number.." text" will fail to pass luamin although correct, and adding tostr() is a waste)
+ assert(pc_data.max_ground_escape_height + 1 - full_height <= 0, "max_ground_escape_height: pc_data.max_ground_escape_height is too high, risk of infinite loop (only ends thx to number wrapping), consider clamping start_tile_offset_qy")
+ return iterate_over_collision_tiles(self, oppose_dir(self.quadrant), pc_data.max_ground_escape_height + 1 - full_height, 0, sensor_position, full_height, ceiling_check_collider_distance_callback, ceiling_check_no_collider_callback, --[[ignore_reverse_on_start_tile:]] true)
+end
+
+-- actual body of compute_closest_wall_query_info passed to iterate_over_collision_tiles
+-- as no_collider_callback
+-- defined before wall_check_no_collider_callback to make it callable from there
+local function wall_check_no_collider_callback()
+ -- end of iteration, and no wall found
+ -- by convention pass a distance bigger than the raycast length (ceil(pc_data.ground_sensor_extent_x))
+ return motion.ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil)
+end
+
+-- actual body of compute_closest_wall_query_info passed to iterate_over_collision_tiles
+-- as collider_distance_callback
+local function wall_check_collider_distance_callback(curr_tile_loc, signed_distance_to_closest_wall, slope_angle)
+ -- note that we want to block character and floor its position (at least on qx) when just *entering* wall,
+ -- not if just touching wall (makes left and right symmetrical), hence <
+ if signed_distance_to_closest_wall < ceil(pc_data.ground_sensor_extent_x) then
+ -- touching or inside wall
+ return motion.ground_query_info(curr_tile_loc, signed_distance_to_closest_wall, slope_angle)
+ else
+ -- we noted in the ceiling version that maybe we should return like the check_no_collider_callback
+ -- but didn't want to change the implementation on existing code yet
+ -- wall detection is new though, so let's return the same as wall_check_no_collider_callback,
+ -- running the risk of stopping the tile iteration early, as it may benefit CPU
+ -- to spare characters, reuse no collider callback
+ return wall_check_no_collider_callback()
+ end
+end
+
+-- "raycast" from wall sensor_position: vector toward q-left or q-right
+-- (based on quadrant_hdir: horizontal_dirs, itself based on movement direction relative to q-ground)
+-- and return ground_query_info(tile_location, signed_distance, slope_angle)
+-- - tile_location is the location of the detected tile (or nil), but it is not used for walls
+-- - signed_distance is positive when not touching wall, negative when inside wall (and needs escape)
+-- if nothing is detected, by convention we return ceil(pc_data.ground_sensor_extent_x) + 1
+-- (1 above raycast length)
+-- - slope_angle is the angle of the detected tile (or nil), but it is not used for walls
+function player_char:compute_closest_wall_query_info(sensor_position, quadrant_hdir)
+ -- collision_check_quadrant:
+ -- if going q-left, we must detect collision in quadrant rotated 90 clockwise from q-down, else 90 counter-clockwise
+ -- => self:get_horizontal(quadrant_hdir)
+
+ -- start_tile_offset_qy: 0 since we already "raycast" from character center which should be far enough from the wall surface
+ -- if character didn't "enter" wall at a speed too high (max ground speed is 3 though, so it may happen, be careful)
+
+ -- last_tile_offset_qy: ceil(ground_sensor_extent_x) as we want to detect walls as close as the ground sensors
+ -- can detect, as those would be blocking us (no need to +1 unless we absolutely want to detect walls on the
+ -- absolute LEFT when we are just touching them; this particularity is due to dissymmetry of pixels,
+ -- where a pixel position is considered to contain the bottom/right position but not the top/left one)
+
+ -- sensor_position_base: the passed sensor_position
+ -- note that we must simulate a motion big step to get the future next position, then check wall
+ -- from that predicted position (and if we're blocked the real next position will be adjusted to touch wall)
+
+ -- sensor_offset_qy: 0 since no offset required
+ -- (there is an offset on qx though, which is already embedded in wall sensor position)
+
+ return iterate_over_collision_tiles(self,
+ --[[collision_check_quadrant]] self:get_horizontal(quadrant_hdir),
+ --[[start_tile_offset_qy]] 0,
+ --[[last_tile_offset_qy]] ceil(pc_data.ground_sensor_extent_x),
+ --[[sensor_position_base]] sensor_position,
+ --[[sensor_offset_qy]] 0,
+ --[[collider_distance_callback]] wall_check_collider_distance_callback,
+ --[[no_collider_callback]] wall_check_no_collider_callback--[[,]]
+ --[[ignore_reverse_on_start_tile: false]])
+end
+
-- verifies if character is inside ground, and push him upward outside if inside but not too deep inside
-- if ground is detected and the character can escape, update the slope angle with the angle of the new ground
--- if the character cannot escape or is in the air, still reset all values to be safe
--- (e.g. on initial warp it allows us to set ground_tile_location to a proper value instead of default location(0, 0))
--- finally, enter standing state if the character was either touching the ground or inside it (even too deep),
--- else enter falling state
+-- if the character is too deep in ground and cannot escape, set tile to nil and angle to 0 by convention
+-- if ground is detected but character was airborne, enter standing state
+-- if no ground is detected, do nothing. Do not even enter airborne state. Either the caller must enter it
+-- by default before calling this method, or they should count on the next frame update to start character fall.
+-- note that unlike other escape methods, exceptionally we do care about the "touch" case
+-- this is because the method is called after warp, where we'd like an instant landing (but that's optional,
+-- character would fall and land in 1 frame without this anyway)
function player_char:check_escape_from_ground()
local query_info = self:compute_ground_sensors_query_info(self.position)
local signed_distance_to_closest_ground, next_slope_angle = query_info.signed_distance, query_info.slope_angle
- if signed_distance_to_closest_ground <= 0 then
+ if - pc_data.max_ground_escape_height <= signed_distance_to_closest_ground and signed_distance_to_closest_ground <= 0 then
-- character is either just touching ground (signed_distance_to_closest_ground == 0)
- -- or inside ground, so check how deep he is inside ground
- if - signed_distance_to_closest_ground <= pc_data.max_ground_escape_height then
- -- close to surface enough to escape
- -- snap character q-upward to ground q-top (it does nothing if already touching ground)
- -- (we currently only check_escape_from_ground after a warp where quadrant is down,
- -- but this can prove useful if using this for ceiling adherence later; currently
- -- we just use a manual offset when landing on a non-down quadrant to fix
- -- #129 BUG MOTION curve_run_up_fall_in_wall as check_escape_from_ground even with quadrant
- -- gives a different result, pushing out too much and ending quadrant down again)
- local vector_to_closest_ground = signed_distance_to_closest_ground * self:get_quadrant_down()
- self.position:add_inplace(vector_to_closest_ground)
- -- register ground tile for later
- self:set_ground_tile_location(query_info.tile_location)
- -- set slope angle to new ground
- self:set_slope_angle_with_quadrant(next_slope_angle)
- else
- -- too deep to escape, stay there
- -- by convention, set ground tile location to nil (see ground_check_collider_distance_callback)
- -- by slope angle to 0 to stand upward
- self.ground_tile_location = nil
- self:set_slope_angle_with_quadrant(0)
+ -- or inside ground and enough close to surface to escape
+ -- snap character q-upward to ground q-top (it does nothing if already touching ground)
+ -- (we currently only check_escape_from_ground after a warp where quadrant is down,
+ -- but this can prove useful if using this for ceiling adherence later; currently
+ -- we just use a manual offset when landing on a non-down quadrant to fix
+ -- #129 BUG MOTION curve_run_up_fall_in_wall as check_escape_from_ground even with quadrant
+ -- gives a different result, pushing out too much and ending quadrant down again)
+ local vector_to_closest_ground = signed_distance_to_closest_ground * self:get_quadrant_down()
+ self.position:add_inplace(vector_to_closest_ground)
+ -- register ground tile for later
+ self:set_ground_tile_location(query_info.tile_location)
+ -- set slope angle to new ground
+ self:set_slope_angle_with_quadrant(next_slope_angle)
+
+ -- if airborne, simulate landing
+ -- if already grounded, don't change state in case we were rolling etc.
+ -- (it never happends with pixel step motion, but it can with big step / frame by frame motion)
+ if not self:is_grounded() then
+ self:enter_motion_state(motion_states.standing)
end
- self:enter_motion_state(motion_states.standing)
- else
- -- character in the air, reset
- self:enter_motion_state(motion_states.falling)
end
+ -- note: if inside ground but too deep to escape:
+ -- convention v3 is to ignore ground completely is too deep inside
+ -- to avoid walking with head on ceiling or inside one-way platform when a little too low
+ -- since in this context we must not do anything when no ground is detected, do nothing
end
-- enter motion state, reset state vars appropriately
@@ -902,23 +1317,35 @@ function player_char:enter_motion_state(next_motion_state)
self.ground_speed = 0
self.should_jump = false
self.should_play_spring_jump = false
- self.brake_anim_phase = 0
elseif next_motion_state == motion_states.standing then
if not was_grounded then
-- Momentum: transfer part of airborne velocity tangential to slope to ground speed (self.slope_angle must have been set previously)
+ -- using a projection on the ground
-- do not clamp ground speed! this allows us to spin dash, fall a bit, land and run at high speed!
-- SPG (https://info.sonicretro.org/SPG:Slope_Physics#Reacquisition_Of_The_Ground) says original calculation either preserves vx or
-- uses vy * sin * some factor depending on angle range (possibly to reduce CPU)
-- but for now we keep this as it's physically logical and feels good enough
+ -- The difference is very perceptible when jumping on the first two slopes of pico island
+ -- - When landing on the first, very low slope with vx = 0 Sonic will retain momentum and descend left,
+ -- while in Sonic 3 he would stop moving at once
+ -- - When landing on the second slope, resulting velocity will smoothly change with vx, going through 0,
+ -- while in Sonic 3, the behavior completely changes when |vx| crosses |vy|: when going to the left fast enough,
+ -- Sonic will only preserve vx and keep going to the left; when going not fast enough, vy * sin is used
+ -- and Sonic goes down to the right; he can never just stop.
self.ground_speed = self.velocity:dot(vector.unit_from_angle(self.slope_angle))
+ -- immediately update velocity to avoid keeping old air velocity while grounded,
+ -- which would result in chained jump being very low, or even going downward, due to
+ -- downward velocity remaining when cumulated with jump impulse
+ self.velocity:copy_assign(self:compute_velocity_from_ground_speed())
+
-- we have just reached the ground (and possibly escaped),
-- reset values airborne vars
self.has_jumped_this_frame = false -- optional since consumed immediately in update_platformer_motion_airborne
self.can_interrupt_jump = false
self.should_play_spring_jump = false
end
- else -- next_motion_state == motion_states.rolling
+ elseif next_motion_state == motion_states.rolling then
-- we don't have code to preserve airborne tangential velocity here because we cannot really land and immediately roll
-- without going through the standing state (even Sonic 3 shows Sonic in standing sprite for 1 frame);
-- and Sonic Mania's Drop Dash would probably ignore previous velocity anyway
@@ -928,9 +1355,19 @@ function player_char:enter_motion_state(next_motion_state)
self.has_jumped_this_frame = false -- optional since consumed immediately in update_platformer_motion_airborne
self.can_interrupt_jump = false
self.should_play_spring_jump = false
- self.brake_anim_phase = 0
end
end
+
+ -- reset brake anim unless standing (actually walking) or falling (which exceptionally allows brake anim)
+ -- the most common case is to brake, then immediately try to crouch to roll -> we should show roll animation
+ if next_motion_state ~= motion_states.standing and next_motion_state ~= motion_states.falling then
+ self.brake_anim_phase = 0
+ end
+
+ -- reset late jump timer if not falling any more
+ if next_motion_state ~= motion_states.falling then
+ self.time_left_for_late_jump = 0
+ end
end
function player_char:update_collision_timer()
@@ -941,24 +1378,40 @@ end
-- update velocity, position and state based on current motion state
function player_char:update_platformer_motion()
---#if debug_character
- -- clear the debug rays to start anew for this frame (don't clear them after rendering
- -- so you can continue seeing them during debug pause)
- -- OPTIMIZE: pool the rays instead (you can also make them proper structs)
- clear_table(self.debug_rays)
---#endif
+ -- SPG note: http://info.sonicretro.org/SPG:Main_Game_Loop
+ -- I started working on this before this page appeared though, so the order may not exactly be the same
+ -- Nevertheless, it's working quite well.
-- check for jump before apply motion, so character can jump at the beginning of the motion
-- (as in classic Sonic), but also apply an initial impulse if character starts idle and
-- left/right is pressed just when jumping (to fix classic Sonic missing a directional input frame there)
- if self:is_grounded() then
+ -- In the original game, pressing down and jump at the same time gives priority to jump.
+ -- Releasing down and pressing jump during crouch gives also priority to spin dash.
+ -- So checking jump before crouching is the correct order (you need 2 frames to crouch, then spin dash)
+ if self:is_grounded() or self.time_left_for_late_jump > 0 then
+ if self.time_left_for_late_jump > 0 then
+ end
self:check_jump() -- this may change the motion state to air_spin and affect branching below
+ self:check_spin_dash() -- this is exclusive with jumping, so there is no order conflict
+ end
+
+ -- decrement late jump timer if positive
+ -- make sure to do this *after* checking it above and *before* update_platformer_motion_grounded
+ -- because if we decrement it before checking it above, on the last frame allowed for late jump (time == 1),
+ -- we will call check_jump_intention (see below) but we need an extra frame to confirm the jump
+ -- and on the next update_platformer_motion, time == 0 and we won't enter the block above
+ -- if we decrement it after update_platformer_motion_grounded (before check_jump_intention),
+ -- since it can set time_left_for_late_jump, that would immediately decrement the initial value
+ -- so we'd need to add +1 to optional_jump_delay_after_fall (and we want initial value 1 to work already)
+ if self.time_left_for_late_jump > 0 then
+ self.time_left_for_late_jump = self.time_left_for_late_jump - 1
end
-- do not move check below inside the is_grounded() check above,
-- to clearly show that the state may have changed and we check it properly again
- if self.motion_state == motion_states.standing then
- self:check_roll_start()
+ -- (even though ultimately, the current checks are all about grounded states)
+ if contains({motion_states.standing, motion_states.crouching}, self.motion_state) then
+ self:check_crouch_and_roll_start()
elseif self.motion_state == motion_states.rolling then
self:check_roll_end()
end
@@ -969,6 +1422,14 @@ function player_char:update_platformer_motion()
self:update_platformer_motion_airborne()
end
+ -- only allow jump preparation for next frame if still grounded,
+ -- or started falling recently with late jump feature enabled
+ if self:is_grounded() or self.time_left_for_late_jump > 0 then
+ if self.time_left_for_late_jump > 0 then
+ end
+ self:check_jump_intention()
+ end
+
--#if ingame
--#if busted
@@ -986,16 +1447,27 @@ function player_char:update_platformer_motion()
--#endif
end
--- check if character is fast enough to roll and wants to roll
--- if so, start rolling
--- we assume character is standing on ground
-function player_char:check_roll_start()
- -- if character is walking fast enough and pressing down (and no horizontal direction), he will roll
- if abs(self.ground_speed) >= pc_data.roll_min_ground_speed and self.move_intention.x == 0 and self.move_intention.y > 0 then
- -- currently enter_motion_state from standing to rolling will do nothing more than set the state
- -- but we call it so we have a centralized place to add other side effects or cleanup if needed
- self:enter_motion_state(motion_states.rolling)
- self:play_low_priority_sfx(audio.sfx_ids.roll)
+-- Check if character wants to crouch (move pure down) or stop crouching (release down or move horizontally).
+-- If crouching and moving fast enough, he will roll.
+-- We assume character is standing on ground or crouching.
+function player_char:check_crouch_and_roll_start()
+ -- Check move intention down (as in the original down, no horizontal direction must be pressed)
+ if self:wants_to_crouch() then
+ -- if character is walking fast enough, he will roll; else, he will crouch
+ -- if character is already crouching and starts sliding at high speed because of a slope,
+ -- rolling also starts; else, do nothing, character just keeps crouching (can slide at low speed)
+ if abs(self.ground_speed) >= pc_data.roll_min_ground_speed then
+ -- currently enter_motion_state from standing to rolling will do nothing more than set the state
+ -- but we call it so we have a centralized place to add other side effects or cleanup if needed
+ self:enter_motion_state(motion_states.rolling)
+ self:play_low_priority_sfx(audio.sfx_ids.roll)
+ elseif self.motion_state ~= motion_states.crouching then
+ -- same remark as above, no side effect as crouch is really like standing state except
+ -- it shrinks the hitbox and allows spin dash
+ self:enter_motion_state(motion_states.crouching)
+ end
+ elseif self.motion_state ~= motion_states.standing then
+ self:enter_motion_state(motion_states.standing)
end
end
@@ -1012,6 +1484,10 @@ end
-- update motion following platformer grounded motion rules
function player_char:update_platformer_motion_grounded()
+ -- SPG note: http://info.sonicretro.org/SPG:Main_Game_Loop
+ -- I started working on this before this page appeared though, so the order may not exactly be the same
+ -- Nevertheless, it's working quite well.
+
self:update_ground_speed()
local ground_motion_result = self:compute_ground_motion_result()
@@ -1059,16 +1535,18 @@ function player_char:update_platformer_motion_grounded()
-- update velocity based on new ground speed and old slope angle (positive clockwise and top-left origin, so +cos, -sin)
-- we must use the old slope because if character is leaving ground (falling)
-- this frame, new slope angle will be nil
- self.velocity = self.ground_speed * vector.unit_from_angle(self.slope_angle)
+ self.velocity:copy_assign(self:compute_velocity_from_ground_speed())
-- update position
- self.position = ground_motion_result.position
+ self.position:copy_assign(ground_motion_result.position)
-- character falls by default if finds no ground to stick to
local should_fall = ground_motion_result.is_falling
-- SPG: Falling and Sliding Off Of Walls And Ceilings
- if self.quadrant ~= directions.down and abs(self.ground_speed) < pc_data.ceiling_adherence_min_ground_speed then
+ -- if we are already falling due to lack of ground, do not check this
+ -- this means we won't trigger horizontal control lock even if going under adherence speed if falling naturally this frame
+ if not should_fall and self.quadrant ~= directions.down and abs(self.ground_speed) < pc_data.ceiling_adherence_min_ground_speed then
-- Only falling when on straight wall, wall-ceiling or ceiling
-- Note that at this point, we haven't set slope angle and we were grounded so it should not be nil
if self.slope_angle >= 0.25 and self.slope_angle <= 0.75 then
@@ -1079,9 +1557,37 @@ function player_char:update_platformer_motion_grounded()
if should_fall then
local new_state
- if self.motion_state == motion_states.standing then
+
+ -- if enabling late jump, track frames after falling naturally from ground (no spring jump, etc. which is
+ -- done elsewhere in code). This also applies to rolling -> falling with air_spin.
+ -- note that it's the only place where we check for the feature. In other places, we keep decrementing the timer
+ -- and applying late jump. This is simpler and avoids having a frozen timer that is resumed later in bad places.
+--#if normal_mode
+ -- picosonic_app_attract_mode doesn't have get_enable_late_jump_feature, and we want
+ -- the attract mode recording the always play the same way anyway, so just skip the test altogether
+ if flow.curr_state.app.get_enable_late_jump_feature() then
+--#endif
+ self.time_left_for_late_jump = pc_data.late_jump_max_delay
+
+ -- track slope angle of current ground before we clear it due to fall/jump
+ -- so we can do the late jump with the correct angle (otherwise, running off a rising curve + late jumping
+ -- sends character to tremendous heights)
+ -- this must be called before enter_motion_state so slope_angle is still set!
+ -- note that we don't clear it even when time_left_for_late_jump reaches 0 to spare characters,
+ -- as we won't be using when not doing late jump
+ self.late_jump_slope_angle = self.slope_angle
+--#if normal_mode
+ end
+--#endif
+
+ -- in the original game, Sonic keeps crouching and spin dash during fall (possible using crouch slide
+ -- or spin dashing on crumbling ground), but you cannot release spin dash during the fall...
+ -- this is very rare, and we don't want to handle the case of air crouching to prevent spin dashing,
+ -- so we just switch to normal fall in this case (and air_spin for spin_dashing, but there is
+ -- no crumbling ground in the level and spin dash freezes velocity X, so this cannot even happen)
+ if contains({motion_states.standing, motion_states.crouching}, self.motion_state) then
new_state = motion_states.falling
- else -- self.motion_state == motion_states.rolling
+ else -- self.motion_state == motion_states.rolling or self.motion_state == motion_states.spin_dashing
-- roll fall is like an air_spin without can_interrupt_jump (nor double jump in Sonic 3)
new_state = motion_states.air_spin
end
@@ -1095,8 +1601,8 @@ function player_char:update_platformer_motion_grounded()
-- update slope angle (if needed)
self:set_slope_angle_with_quadrant(ground_motion_result.slope_angle)
- -- only allow jump preparation for next frame if not already falling
- self:check_jump_intention()
+ -- we moved self:check_jump_intention() to after calling this method
+ -- because of the new time_left_for_late_jump
end
log("self.position: "..self.position, "trace")
@@ -1113,18 +1619,23 @@ function player_char:update_ground_speed()
-- We apply slope factor *before* move intention because it gives
-- better results when not moving on a low slope (friction will stop you completely).
-- Another side effect is that the ground speed *after* slope factor application
- -- will be considered for the move intention effect, such as decelerating
+ -- will be considered for the move intention effect, such as using ground active deceleration
-- when moving forward on an ascending slope if it started make you move down.
- -- Also, if ground speed is 0 and we start trying to ascend slope,
+ -- Disabled Original Slope Feature Note:
+ -- Also, if ground speed is 0 and we start trying to ascend slope,
-- Progressive Ascending Steep Slope Factor feature won't be applied the first frame.
-- But it should be OK overall.
-- Note that this order is supported by the SPG (http://info.sonicretro.org/SPG:Solid_Tiles)
- if self.motion_state == motion_states.standing then
+ if contains({motion_states.standing, motion_states.crouching}, self.motion_state) then
local previous_ground_speed = self.ground_speed
self:update_ground_speed_by_slope()
+ -- the two below are not needed for crouching:
+ -- - there should be no move intention x with crouch
+ -- - high speed will turn crouch into roll, so crouch only has low speed
+ -- but it takes fewer characters not to add an extra check... and it shouldn't take much extra CPU
self:update_ground_run_speed_by_intention()
self:clamp_ground_speed(previous_ground_speed)
- else
+ elseif self.motion_state == motion_states.rolling then
self:update_ground_speed_by_slope()
self:update_ground_roll_speed_by_intention()
-- There is some particular clamping based on speed X in the original game,
@@ -1132,6 +1643,9 @@ function player_char:update_ground_speed()
-- or not at all. Right now there's no place in the level where you'd go overspeed,
-- actually you'd rather try to get as much speed as possible to get past the loops,
-- so we are not clamping roll speed at all. Otherwise we'd probably just clamp ground speed to 8.
+ -- else -- self.motion_state == motion_states.spin_dashing
+ -- do nothing so ground speed is frozen, as in the original game (crouch and spin dash just before
+ -- falling down after trying to climb up a slope with not enough momentum to reproduce)
end
end
@@ -1347,367 +1861,251 @@ function player_char:clamp_ground_speed(previous_ground_speed)
end
end
+-- return velocity when grounded based on slope
+-- self.ground_speed and self.slope_angle must be set
+function player_char:compute_velocity_from_ground_speed()
+ return self.ground_speed * vector.unit_from_angle(self.slope_angle)
+end
+
-- return {next_position: vector, is_blocked: bool, is_falling: bool} where
-- - next_position is the position of the character next frame considering his current ground speed
-- - is_blocked is true iff the character encounters a wall during this motion
-- - is_falling is true iff the character leaves the ground just by running during this motion
function player_char:compute_ground_motion_result()
- -- if character is not moving, he is not blocked nor falling (we assume the environment is static)
+ assert(self.ground_tile_location, "compute_ground_motion_result: self.ground_tile_location not set")
+
+ -- if character is not moving, immediately return result with same position,
+ -- ground location, slope angle, not blocked nor falling (we assume the environment is static)
if self.ground_speed == 0 then
return motion.ground_motion_result(
- self.ground_tile_location,
- self.position,
+ self.ground_tile_location:copy(),
+ self.position:copy(),
self.slope_angle,
false,
false
)
-
end
- -- from here we will be considering positions, velocities, angles relatively
- -- to the current quadrant to allow Sonic to walk on walls and ceilings
- -- when quadrant is rotated by 0.25 (90 degrees CCW), the following transformations occur:
- -- - ground move intention x <-> y (+x -> -y, -x -> +y, +y -> +x, -y -> -x)
- -- ("intention" matters because we apply a forward rotation as Sonic will try to run on walls and ceilings
- -- this is different from transposing an *existing* vector to another frame, which would have the backward (reverse)
- -- transformation such as +x -> +y)
- -- because the sign of x/y changes, the way we add values also matter, so in some cases
- -- x + dx would become y - dy and a simple transposition is not enough
- -- therefore, it is more reliable to add rotated vectors, even if only one component is non-zero,
- -- and then extract x/y from this vector
- -- we then call these coordinates "quadrant x" and "quadrant y", but note that they still
- -- follow the positive axis sense of PICO-8 (only ground_speed and ground_based_signed_distance_qx are
- -- based on ground orientation, CCW positive)
- -- - existing slope angle -> slope angle - 0.25
- -- when quadrant is rotated by 0.5 (e.g. floor to ceiling), x <-> -x and y <-> -y
- -- and slope angle -> slope angle - 0.5 (these ops are reflective so we don't need to care about reverse transformation as above)
- -- a few examples of quadrant variables:
- -- - quadrant horizontal direction: is it left or right from Sonic's point of view?
- -- (on a left wall, moving up is "left" and moving down is "right"
- -- on the ceiling, moving left is "right" and moving right is "left")
- -- - quadrant horizontal axis: horizontal for quadrants up and down, vertical for quadrants left and right
- -- (we also define the forward as the counter-clockwise direction in any case, e.g. right on quadrant down
- -- and down on quadrant left)
- -- - quadrant vertical axis: orthogonal to quadrant horizontal axis
- -- (we also define "up" as the direction pointing outside the quadrant interior)
- -- - quadrant height: the collision mask column height, in the quadrant's own frame
- -- (when quadrant is left or right, this is effectively a row width, where the row extends from left/right resp.)
- -- - quadrant slope angle: the slope angle subtracted by the quadrant's angle (quadrant down having angle 0, then steps of 0.25 counter-clockwise)
- -- - quadrant columns are rows on walls
- -- we prefix values with "q" or "q-" for "quadrant", e.g. "qx" and "qy"
- -- we even name floors, walls and ceilings "q-wall" to express the fact they are blocking Sonic's motion
- -- relatively to his current quadrant, acting as walls, but may be any solid tile
-
- -- initialise result with floored coords, it's not to easily visualize
- -- pixel by pixel motion at integer coordinates (we will reinject subpixels
- -- if character didn't touch a wall)
- -- we do this on both coordinates to simplify, but note that Sonic always snaps
- -- to the ground quadrant height, so the quadrant vertical coordinate (qy) is already integer,
- -- so it really matters for qx (but to reduce tokens we don't add a condition based on quadrant)
- -- note that quadrant left and right motion is not completely symmetrical
- -- since flr is asymmetrical so there may be up to a 1px difference in how we hit stuff on
- -- the left or right (Classic Sonic has a collider with odd width, it may be actually symmetrical
- -- on collision checks)
- local floored_x = flr(self.position.x)
- local floored_y = flr(self.position.y)
- local motion_result = motion.ground_motion_result(
- self.ground_tile_location,
- vector(floored_x, floored_y),
- self.slope_angle,
- false,
- false
- )
-
- local quadrant = self.quadrant
+ -- SPG note: http://info.sonicretro.org/SPG:Main_Game_Loop
+ -- We're following the indicated order by checking ground BEFORE wall (and updating quadrant for wall check)
+ -- because it prevented stepping up on tiles of height 6 (wall sensor height) to 7 (max ground escape height)
+ -- like the spring oriented up.
+ -- But we added an EXTRA wall check AFTER ground check, using the new quadrant (and position),
+ -- to avoid wall raycasting into what was previously a wall, but is now a ground, when moving at fast speed
+ -- (esp. with spin dash) inside a loop.
+ -- I also tried checking wall AFTER ground check but NOT BEFORE. It still required me to increase the
+ -- max escape distance to 8, and character was stepping up springs (unless moving by less than 1 pixel into them).
+ -- In addition, we're not checking roll here, we're doing this earlier, at the same time as checking spin dash
+
+ -- Big step method:
+ -- 1. apply full velocity to get hypothetical position next frame in the absence of collisions
+ -- 2. check for wall collisions (only recognized if started entering wall)
+ -- 3. check for ground collisions (with new position from 2) to snap to / escape from ground,
+ -- or fall if no ground / angle difference too big
+ -- 4. if found wall and changed quadrant, check for wall collisions again (with new position and quadrant from 3) and cancel wall detection
+ -- if not touching wall (this time, we recognize touching wall as collision since 2 made us escape)
+
+ local is_falling = false
+ local previous_quadrant = self.quadrant
local quadrant_horizontal_dir = signed_speed_to_dir(self.ground_speed)
- local qx = world.get_quadrant_x_coord(self.position, quadrant)
-
- -- only full pixels matter for collisions, but subpixels (of last position + delta motion)
- -- may sum up to a full pixel,
- -- so first estimate how many full pixel columns the character may actually explore this frame
- local ground_based_signed_distance_qx = self.ground_speed * cos(self.slope_angle - world.quadrant_to_right_angle(quadrant))
- -- but ground_based_signed_distance_qx is positive when walking a right wall up or ceiling left,
- -- which is opposite of the x/y sign convention; project on quadrant right unit vector to get vector
- -- with x/y with the correct sign for addition to x/y position later
- local ground_velocity_projected_on_quadrant_right = ground_based_signed_distance_qx * self:get_quadrant_right()
- -- equivalent to dot expression below, but more compact than it:
- -- local ground_velocity_projected_on_quadrant_right = quadrant_right:dot(self.ground_speed * vector.unit_from_angle(self.slope_angle)) * quadrant_right
- local projected_velocity_qx = world.get_quadrant_x_coord(ground_velocity_projected_on_quadrant_right, quadrant)
-
- -- max_distance_qx is always integer
- local max_distance_qx = player_char.compute_max_pixel_distance(qx, projected_velocity_qx)
-
- -- iterate pixel by pixel on the qx direction until max possible distance is reached
- -- only stopping if the character is blocked by a q-wall (not if falling, since we want
- -- him to continue moving in the air as far as possible; in edge cases, he may even
- -- touch the ground again some pixels farther)
- local qhorizontal_distance_before_step = 0
- while qhorizontal_distance_before_step < max_distance_qx and not motion_result.is_blocked do
- self:next_ground_step(quadrant_horizontal_dir, motion_result)
- qhorizontal_distance_before_step = qhorizontal_distance_before_step + 1
- end
-
- -- check if we need to add or cut subpixels
- if not motion_result.is_blocked then
- -- since subpixels are always counted to the right/down, the subpixel test below is asymmetrical
- -- but this is correct, we will simply move backward a bit when moving left/up
- local are_subpixels_left = qx + projected_velocity_qx > world.get_quadrant_x_coord(motion_result.position, quadrant)
-
- if are_subpixels_left then
- -- character has not been blocked and has some subpixels left to go
- -- unlike Classic Sonic, and *only* when moving right/down, we decide to check if those
- -- subpixels would leak to hitting a q-wall on the right/down, and cut them if so,
- -- blocking the character on the spot (we just reuse the result of the extra step,
- -- since is_falling doesn't change if is_blocked is true)
- -- when moving left/up, the subpixels are a small "backward" motion to the right/down and should
- -- never hit a wall back
- local is_blocked_by_extra_step = false
- if projected_velocity_qx > 0 then
- local extra_step_motion_result = motion_result:copy()
- self:next_ground_step(quadrant_horizontal_dir, extra_step_motion_result)
- if extra_step_motion_result.is_blocked then
- motion_result = extra_step_motion_result
- is_blocked_by_extra_step = true
- end
- end
-
- -- unless moving right/down and hitting a q-wall due to subpixels, apply the remaining subpixels
- -- as they cannot affect collision anymore. when moving left/up, they go a little backward
- if not is_blocked_by_extra_step then
- -- character has not touched a q-wall at all, so add the remaining subpixels
- -- (it's simpler to just recompute the full motion in qx; don't touch qy though,
- -- as it depends on the shape of the ground - we floored it earlier but it should
- -- have been integer from the start so it shouldn't have changed anything)
- -- do not apply other changes (like slope) since technically we have not reached
- -- the next tile yet, only advanced of some subpixels
- world.set_position_quadrant_x(motion_result.position, qx + projected_velocity_qx, quadrant)
- end
- end
- end
- return motion_result
-end
+ -- Step 1: future position prediction
--- return the number of new pixel q-columns explored when moving from initial_position_coord (x or y)
--- over velocity_coord (x or y) * 1 frame. consider full pixel motion starting at floored coord,
--- even when moving in the negative direction
--- this is either flr(velocity_coord)
--- or flr(velocity_coord) + 1 (if subpixels from initial position coord and speed sum up to 1.0 or more)
--- note that for negative motion, we must go a bit beyond the next integer to count a full pixel motion,
--- and that is intended
-function player_char.compute_max_pixel_distance(initial_position_coord, velocity_coord)
- return abs(flr(initial_position_coord + velocity_coord) - flr(initial_position_coord))
-end
+ -- Compute next position after velocity is applied, if there were no obstacles
+ -- We have *not* updated self.velocity yet at this point (compute_velocity_from_ground_speed will
+ -- do it after checking if blocked so it knows if it should force set velocity to 0),
+ -- but we can still compute the expected velocity in the absence of collisions in advance,
+ -- using compute_velocity_from_ground_speed
+ local next_position = self.position + self:compute_velocity_from_ground_speed()
--- update ref_motion_result: motion.ground_motion_result for a character trying to move
--- by 1 pixel step in quadrant_horizontal_dir, taking obstacles into account
--- if character is blocked, it doesn't update the position and flag is_blocked
--- if character is falling, it updates the position and flag is_falling
--- ground_motion_result.position's qx should be floored for these steps
--- (some functions assert when giving subpixel coordinates)
-function player_char:next_ground_step(quadrant_horizontal_dir, ref_motion_result)
- log(" next_ground_step: "..joinstr(", ", quadrant_horizontal_dir, ref_motion_result), "trace2")
+ -- Step 2: 1st wall check (always)
- -- compute candidate position on next step. only flat slopes supported
- local step_vec = self:quadrant_rotated(horizontal_dir_vectors[quadrant_horizontal_dir])
- local next_position_candidate = ref_motion_result.position + step_vec
+ -- do a wall raycast in the q-direction of ground speed
+ local is_blocked = self:check_escape_wall_and_update_next_position(next_position, quadrant_horizontal_dir)
- log("step_vec: "..step_vec, "trace2")
- log("next_position_candidate: "..next_position_candidate, "trace2")
+ -- Step 2: ground check
-- check if next position is inside/above ground
- local query_info = self:compute_ground_sensors_query_info(next_position_candidate)
+ local query_info = self:compute_ground_sensors_query_info(next_position)
local signed_distance_to_closest_ground = query_info.signed_distance
-
log("signed_distance_to_closest_ground: "..signed_distance_to_closest_ground, "trace2")
-- signed distance is useful, but for quadrant vector ops we need actual vectors
- -- to get the right signs (e.g. on floor, signed distance > 0 <=> offset dy < 0 from ground,
+ -- to get the right escape motions (e.g. on floor, signed distance > 0 <=> offset dy < 0 from ground,
-- but on left wall, signed distance > 0 <=> offset dx > 0)
-- signed distance is from character to ground, so get unit vector for quadrant down
local vector_to_closest_ground = signed_distance_to_closest_ground * self:get_quadrant_down()
- -- merge < 0 and == 0 cases together to spare tokens
- -- when 0, next_position_candidate.y will simply not change
if signed_distance_to_closest_ground < 0 then
- -- position is inside ground, check if we can step up during this step
- -- (note that we kept the name max_ground_escape_height but in quadrant left and right,
- -- the escape is done on the X axis so technically we escape row width)
- -- refactor: code is similar to check_escape_from_ground and above all next_air_step
+ -- Next position is inside ground, but are we close to surface enough?
if - signed_distance_to_closest_ground <= pc_data.max_ground_escape_height then
- -- step up or step flat
- next_position_candidate:add_inplace(vector_to_closest_ground)
- -- if we left the ground during a previous step, cancel that
- -- (fall, then touch ground or step up to land, very rare)
- ref_motion_result.is_falling = false
- else
- -- step blocked: step up is too high, character is blocked
- -- if character left the ground during a previous step, let it this way;
- -- character will simply hit the wall, then fall
- ref_motion_result.is_blocked = true
+ -- Close enough to surface => Step up
+ next_position:add_inplace(vector_to_closest_ground)
end
elseif signed_distance_to_closest_ground >= 0 then
- -- position is above ground, check if we can step down during this step
- -- (step down is during ground motion only)
+ -- Next position is above or just touching ground, should we leave ground or step down?
if signed_distance_to_closest_ground <= pc_data.max_ground_snap_height then
- -- if character has fallen during previous step, prevent step down AND no need to check for angle take-off
- -- note he can still re-land, but only by entering the ground i.e. signed distance to ground < 0, as in block above
- -- otherwise, character is still grounded, so check for angle take-off, and if not taking off, step down
- if not ref_motion_result.is_falling then
- -- Original slope feature: Take-Off Angle Difference
- -- When character falls when running from to ground, he could normally step down,
- -- but the new ground is a descending slope too steep compared to previous slope angle.
- -- Exceptionally not inside --#if original_slope_features because it really fixes glitches
- -- when character moves at low speed from flat ground to steep descending slope
- -- In the original, Sonic just runs on the steep descending slope as if nothing, and also exceptionally
- -- preserves his sprite angle, but that would have required extra code.
- -- Make sure to check if we are not already falling so slope angle exists (alternatively check that ref_motion_result.slope_angle is not nil)
- -- When running toward the left, angle diff has opposite sign, so multiply by horizontal sign to counter this
- -- Note that character is not falling, so grounded (during step), so ref_motion_result.slope_angle is not nil
- local signed_angle_delta = compute_signed_angle_between(query_info.slope_angle, ref_motion_result.slope_angle)
- if horizontal_dir_signs[quadrant_horizontal_dir] * signed_angle_delta > pc_data.take_off_angle_difference then
- -- step fall due to angle difference aka angle-based Take-Off
- ref_motion_result.is_falling = true
- else
- -- step down
- next_position_candidate:add_inplace(vector_to_closest_ground)
- end
+ -- Close enough to step down, but first check angle difference
+
+ -- Original slope feature: Take-Off Angle Difference
+ -- When character could normally step down, but the new ground has an angle too low
+ -- compared to the previous ground, character still falls off.
+ -- Exceptionally not inside --#if original_slope_features because it fixes character NOT falling off
+ -- the first curved slope (before the 1st spring) when running or even spin dashing to the left
+ -- (no ugly glitch, but Sonic literally sticks to the ground unless the speed is so high that
+ -- the big step goes too far to detect the slope's first tile on the first frame leaving the ground)
+ -- In the original, Sonic just runs on the steep descending slope as if nothing, and also exceptionally
+ -- preserves his sprite angle, but that would have required extra code. Besides, when running fast and spin dashing
+ -- he's actually falling off.
+ -- When running toward the left, angle diff has opposite sign, so multiply by horizontal sign to counter this
+ -- Note that character was grounded last frame since we're calling compute_ground_motion_result,
+ -- so self.slope_angle is not nil
+ local signed_angle_delta = compute_signed_angle_between(query_info.slope_angle, self.slope_angle)
+
+ -- note the `>` comparison: if difference is just at threshold, we don't fall-off
+ if horizontal_dir_signs[quadrant_horizontal_dir] * signed_angle_delta > pc_data.take_off_angle_difference then
+ -- Step fall due to angle difference aka angle-based Take-Off
+ is_falling = true
+ else
+ -- Step down
+ next_position:add_inplace(vector_to_closest_ground)
end
else
- -- step fall: step down is too low, character will fall
- -- in some rare instances, character may find ground again farther, so don't stop the outside loop yet
- -- (but he'll need to really enter the ground i.e. signed distance to ground < 0)
- -- caution: we are not updating qy at all, which means the character starts
- -- "walking horizontally in the air". in sonic games, we would expect
- -- momentum to take over and send the character along qy, preserving
- -- velocity qvy from last frame (e.g. when running off a slope)
- -- consider reusing the last delta qy (e.g. vector_to_closest_ground qy)
- -- and applying it this frame
- -- but we tested and since we lose a single frame of step max, it's not perceptible:
- -- on the next airborne frames, the velocity and full air motion is more important and works
- -- as expected when running off a slope
- ref_motion_result.is_falling = true
+ -- Ground is too far to step down, fall
+ is_falling = true
end
end
- if not ref_motion_result.is_blocked then
- -- character is not blocked by a steep q-step up/q-wall, but we need to check if it is
- -- blocked by a q-ceiling too low; in the extreme case, a diagonal tile pattern
- -- ->X
- -- X
- -- is also considered a ceiling and ignoring it will let Sonic go through and fall
- -- (unlike Classic Sonic, we do check for ceilings even when Sonic is grounded;
- -- this case rarely happens in normally constructed levels though; and q-ceilings
- -- even more rare)
- ref_motion_result.is_blocked = self:is_blocked_by_ceiling_at(next_position_candidate)
-
- -- only advance if character is still not blocked (else, preserve previous position,
- -- which should be floored)
- -- this only works because the q-wall sensors are 1px farther from the character center
- -- than the ground sensors; if there were even farther, we'd even need to
- -- move the position backward by hypothetical wall_sensor_extent_x - ground_sensor_extent_x - 1
- -- when ref_motion_result.is_blocked (and adapt y)
- -- in addition, because a step is no more than 1px, if we were blocked this step
- -- we have not moved at all and therefore there is no need to update slope angle
- if not ref_motion_result.is_blocked then
- ref_motion_result.position = next_position_candidate
- if ref_motion_result.is_falling then
- ref_motion_result.tile_location = nil
- ref_motion_result.slope_angle = nil
- else
- ref_motion_result.tile_location = query_info.tile_location
- ref_motion_result.slope_angle = query_info.slope_angle
- end
- end
- end
-end
+ -- don't reverse that ternary! `and nil or ...` won't work!
+ local next_ground_tile_location = not is_falling and query_info.tile_location or nil
--- return true iff the character cannot stand in his full height (based on ground_sensor_extent_x)
--- at position because of the ceiling (or a full tile if standing at the top of a tile)
-function player_char:is_blocked_by_ceiling_at(center_position)
+ -- Step 4: 2nd wall check (only if found wall in step 2, but found ground with different quadrant in step 3)
- -- note: we could use compute_ceiling_sensors_query_info and check for negative distance since it finds
- -- the closest ceiling, but it's slightly more optimal to stop as soon as first true collision is found
- -- if we lack characters in cartridge space, it's worth trying the other way though
+ -- At this point self.quadrant has not been changed YET, but we can still
+ -- predict it from the slope angle to compare previous and new quadrant
+ -- Note that even if we don't enter this block and update quadrant now, it will be updated
+ -- by the caller of this method afterward
+ if is_blocked and previous_quadrant ~= world.angle_to_quadrant(query_info.slope_angle) then
+ -- Since we want to apply wall check with new quadrant, to make it meaningful we must update the ground tile location
+ -- so the raycasts are more likely to be oriented toward the new character forward and not hit unwanted ground as wall
+ self:set_ground_tile_location(next_ground_tile_location)
- -- check ceiling from both ground sensors. if any finds one, return true
- for i in all({horizontal_dirs.left, horizontal_dirs.right}) do
+ -- In principle we should also update the slope angle to be consistent, but check_escape_wall_and_update_next_position
+ -- doesn't use it anyway, so we commented out to spare characters, but kept for understanding
+ -- if is_falling then
+ -- self:set_slope_angle_with_quadrant(nil)
+ -- else
+ -- self:set_slope_angle_with_quadrant(query_info.slope_angle)
+ -- end
- -- check if ground sensor #i has ceiling closer than a character's height
- local sensor_position = self:get_ground_sensor_position_from(center_position, i)
- local ceiling_query_info = self:compute_closest_ceiling_query_info(sensor_position)
- -- distance to ceiling is always negative or 0 as we never "step q-down" onto ceiling
- -- but we must still exclude the case of distance == 0 is case we are just touching ceiling, not blocked
- if ceiling_query_info.signed_distance < 0 then
- return true
- end
+ is_blocked = self:check_escape_wall_and_update_next_position(next_position, quadrant_horizontal_dir)
+ -- what's interesting is that we noticed that not doing all of this
+ -- (set_ground_tile_location + check_escape_wall_and_update_next_position)
+ -- and just clearing flag: is_blocked = false
+ -- works too, thx to the quadrant check: we don't change quadrant often, and if we do,
+ -- we can tolerate ignoring walls and going through for 1 frame; the next frame is unlikely to
+ -- change quadrant *again*, and therefore will detect any wall normally
+ -- OPTIMIZE CHARS: so consider this alternative if you need to release more characters,
+ -- as it showed no issues in the game (and it even passes our utest to fix #265, as long as you only
+ -- check the final result and not count spy calls!)
end
- return false
-end
+ -- make sure to only pass tile location if not falling, to avoid assert on invalid result construction
+ local motion_result = motion.ground_motion_result(
+ next_ground_tile_location,
+ next_position,
+ query_info.slope_angle,
+ is_blocked,
+ is_falling
+ )
+ return motion_result
+end
--- actual body of compute_closest_ceiling_query_info passed to iterate_over_collision_tiles
--- as collider_distance_callback
--- return "ground query info" although it's ceiling, because depending on the angle, character may actually adhere, making it
--- a q-up ground
--- return nil if no clear result and we must continue to iterate (until the last tile)
-local function ceiling_check_collider_distance_callback(curr_tile_loc, signed_distance_to_closest_ceiling, slope_angle)
- -- previous calculations already reversed sign of distance to match convention (> 0 when not touching, < 0 when inside)
- if signed_distance_to_closest_ceiling <= 0 then
- -- head (or body) just touching or inside ceiling
- return motion.ground_query_info(curr_tile_loc, signed_distance_to_closest_ceiling, slope_angle)
- else
- -- head far touching ceiling or has some gap from ceiling
- -- unlike ground, we never "step q-down" onto ceiling, the ceiling check only results in collision with movement interruption
- -- or ceiling adherence, but then character started going inside ceiling (distance <= 0), therefore distance is never > 0
- -- unless we reached ceiling_check_no_collider_callback and then it's the max + 1
- return nil
+-- check for wall in relative quadrant_horizontal_dir when character center is at next position
+-- update next position in-place to escape any wall entered
+-- return true if escaping wall this way, else falsy value
+function player_char:check_escape_wall_and_update_next_position(next_position, quadrant_horizontal_dir)
+ local sensor_position_base = self:get_wall_sensor_position_from(next_position)
+ local wall_query_info = self:compute_closest_wall_query_info(sensor_position_base, quadrant_horizontal_dir)
+
+ if wall_query_info.tile_location then
+ -- We detected a wall, but it doesn't mean we should stop:
+ -- a. future position may be just touching a wall on the right (when raycasting over ceil(pc_data.ground_sensor_extent_x)
+ -- we can only detect touched walls on the right due to pixel asymmetry)
+ -- we ignore his case by checking `< ceil(pc_data.ground_sensor_extent_x)` (although stopping right at that distance is no big deal)
+ -- OPTIMIZE CHARS: you can remove this condition if too tight on budget
+ -- b. future position may be "too much inside wall to escape". Actually, we should try to escape anyway,
+ -- but in this case we shouldn't because of a quirk in the detection: we detect the tile in the start position
+ -- of the raycast, so if an object collision mask partially occupies a tile, like the left part of the spring oriented up,
+ -- and we have our back turned to that object, we are actually still detecting it BEHIND us with a big negative distance
+ -- like -7.5 as if we were inside. Therefore, trying to escape this would drag the character backward into the spring.
+ -- To fix this, we either need to
+ -- (i) add some threshold on negative distance (here, -6), or
+ -- (ii) pass ignore_reverse_on_start_tile: true in the call to iterate_over_collision_tiles in compute_closest_wall_query_info,
+ -- exactly as we're doing in compute_closest_ceiling_query_info. Originally, I didn't choose (ii) because I was afraid I would
+ -- miss legit tile reverse detection in some places. I tried it though just to see, and in practice I could not
+ -- find any places where behavior was unexpected or different from (i). But (i) still makes sense in terms of
+ -- escape range, so I kept it.
+ -- OPTIMIZE CHARS: you can remove this condition and replace it with [[ignore_reverse_on_start_tile:]] true in compute_closest_wall_query_info
+ -- if too tight on budget
+ -- If you remove both conditions, you can remove the if entirely!
+
+ -- To simplify, we're just passing 6 hardcoded and not as pc_data, for once.
+ -- Remember that we're raycasting from the character center, so 6 is actually super deep already, it means the character's front
+ -- is around 9 pixels inside the wall already, which is bigger than tile_size = 8! So enough to cover even the fastest spin dash
+ -- (max spin dash launch speed is 6).
+ -- OPTIMIZE CHARS: consider storing ceil(pc_data.ground_sensor_extent_x) in a variable in pc_data (either precomputed or hardcoded)
+ -- that you'd reuse *everywhere*
+ if -6 <= wall_query_info.signed_distance and wall_query_info.signed_distance < ceil(pc_data.ground_sensor_extent_x) then
+ -- we're in good range to escape wall
+
+ -- remember we raycast from character center, but to get the escape vector we need to know the actual
+ -- distance from character *front* to wall, so subtract the distance from center to front
+ -- we really want to *subtract* in that direction: result is negative, but wall quadrant is an *interior* normal
+ -- so the escape vector will be in the sense of the *exterior* normal so we can escape
+ -- considering the test above, signed_distance_to_closest_wall must be < 0
+ local signed_distance_to_closest_wall = wall_query_info.signed_distance - ceil(pc_data.ground_sensor_extent_x)
+ log("signed_distance_to_closest_wall: "..signed_distance_to_closest_wall, "trace2")
+
+ local wall_quadrant = dir_vectors[self:get_horizontal(quadrant_horizontal_dir)]
+ local vector_to_closest_wall = signed_distance_to_closest_wall * wall_quadrant
+
+ -- Escape wall
+
+ -- Note that sensor position preserves pixel fraction on qy (expected wall normal axis:
+ -- we consider the wall quadrant to define qx/qy here, not character quadrant)
+ -- so we can get the exact distance to closest wall.
+ -- This means that even when we enter wall by a fraction of pixel, we'll escape from it perfectly.
+ -- In addition, we don't need to floor the result's qy, similarly to how we perfectly snap
+ -- to ground thanks to ground sensor position preserving qy fraction.
+ next_position:add_inplace(vector_to_closest_wall)
+
+ -- Return true so caller can remember character was blocked and reset ground speed
+ return true
+ end
end
-end
--- actual body of _compute_signed_distance_to_closest_ceiling passed to iterate_over_collision_tiles
--- as no_collider_callback
-local function ceiling_check_no_collider_callback()
- -- end of iteration, and no ceiling found
- return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+ -- not commented out for now to maintain utests validity,
+ -- but if you need to spare characters, comment this out and change utests to check for nil / no value
+ return false
end
--- similar to compute_closest_ground_query_info, but for ceiling
--- return ground_query_info(tile_location, signed_distance, slope_angle) (see compute_closest_ground_query_info for more info)
--- note that we return a query info with negative sign (inside ceiling) even if the detected obstacle is lower than one step up's height,
--- because we assume that if the character could step this up, it would have and the passed
--- sensor_position would be the resulting position, so only higher tiles will be considered
--- so the step up itself will be ignored (e.g. when moving from a flat ground to an ascending slope)
-function player_char:compute_closest_ceiling_query_info(sensor_position)
- assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:compute_closest_ceiling_query_info: sensor_position qx must be floored")
-
- -- oppose_dir since we check ceiling by detecting tiles q-above, and their q-column height matters
- -- when measured from the q-top (e.g. if there's a top half-tile maybe character head is not hitting it
- -- depending on the exact distance; if q-bottom based, it's considered reverse so full q-height and character
- -- head will hit it as soon as it enters the tile)
-
- -- top must be q-above bottom or we will get stuck in infinite loop
- -- (because to reduce tokens we compare locations directly instead of sub_qy(curr_tile_qj, last_tile_qy, quadrant_opp) >= 0
- -- which would ensure loop end)
-
- -- we must at least start checking ceiling 1 px above foot sensor (because when foot is just on top of tile,
- -- the current sensor tile is actually the tile *below* the character, which is often a full tile and will bypass
- -- ignore_reverse (see world.compute_qcolumn_height_at); in practice +4/+8 is a good offset, we pick max_ground_escape_height + 1 = 5
- -- because it allows us to effectively check the q-higher pixels not already checked in compute_closest_ground_query_info)
-
- -- finally, we check actual collision at head top position, so we pass an offset of self:get_full_height() (argument 5)
- -- from here, we need:
- -- - (max_ground_escape_height + 1 - full_height) offset for first tile according to explanation above + the fact that we consider this offset from sensor_position base + offset (full_height)
- -- - no offset for last tile since we end checking at head top exactly, so argument 3 is 0
- local full_height = self:get_full_height()
- return iterate_over_collision_tiles(self, oppose_dir(self.quadrant), pc_data.max_ground_escape_height + 1 - full_height, 0, sensor_position, full_height, ceiling_check_collider_distance_callback, ceiling_check_no_collider_callback, --[[ignore_reverse_on_start_tile:]] true)
+-- return true iff move intention is down, without horizontal component
+function player_char:wants_to_crouch()
+ return self.move_intention.x == 0 and self.move_intention.y > 0
end
-- if character intends to jump, prepare jump for next frame
-- this extra frame allows us to detect if the player wants a variable jump or a hop
-- depending whether input is hold or not
+-- TODO/CHAR OPTIMIZATION: now hop is no more than a variable jump immediately interrupted, so there is no need
+-- to delay jump detection like this. We shouldn't be losing a frame even now as jump is checked
+-- at the beginning of the next frame, however since ground accel is weaker than air accel,
+-- character will tend to be slower when running and jumping.
+-- We can probably merge check_jump_intention and check_jump now, and do it before update_platformer_motion_grounded.
+-- Make sure to also trigger spin dash at this point, since it's also using self.jump_intention.
function player_char:check_jump_intention()
if self.jump_intention then
-- consume intention so puppet control mode (which is sticky) also works
@@ -1719,6 +2117,7 @@ end
-- if character intends to jump, apply jump velocity from current ground
-- and enter the air_spin state
-- return true iff jump was applied (return value is currently unused)
+-- CHAR OPTIMIZATION: no need for return value anymore (update utest too)
function player_char:check_jump()
if self.should_jump then
self.should_jump = false
@@ -1726,12 +2125,13 @@ function player_char:check_jump()
-- apply initial jump speed for variable jump
-- note: if the player is doing a hop, the vertical speed will be reset
-- to the interrupt speed during the same frame in update_platformer_motion_airborne
- -- via _check_hold_jump (we don't do it here so we centralize the check and
+ -- via check_hold_jump (we don't do it here so we centralize the check and
-- don't apply gravity during such a frame)
-- to support slopes, we use the ground normal (rotate right tangent ccw)
- -- we don't have double jumps yet so we assume we are grounded here and
- -- self.slope_angle is not nil
- local jump_impulse = pc_data.initial_var_jump_speed_frame * vector.unit_from_angle(self.slope_angle):rotated_90_ccw()
+ -- either we are grounded and jumping along ground normal, or we are doing late jump
+ -- and jumping along last ground normal (defined via late_jump_slope_angle)
+ local jump_angle = self.time_left_for_late_jump > 0 and self.late_jump_slope_angle or self.slope_angle
+ local jump_impulse = pc_data.initial_var_jump_speed_frame * vector.unit_from_angle(jump_angle):rotated_90_ccw()
self.velocity:add_inplace(jump_impulse)
self:enter_motion_state(motion_states.air_spin)
self.has_jumped_this_frame = true
@@ -1744,8 +2144,94 @@ function player_char:check_jump()
return false
end
+-- check if player should start spin dash, or charge spin dash further
+function player_char:check_spin_dash()
+ if contains({motion_states.crouching, motion_states.spin_dashing}, self.motion_state) then
+ if self.motion_state == motion_states.spin_dashing and self.move_intention.y <= 0 then
+ -- player released down button, release spin dash!
+ -- edge case: if during crouch, player releases down and pressed JUMP at the same time,
+ -- character still rev once, only to release spin dash next frame
+ -- (in theory player could press down again to hold the spin dash charge... in the original
+ -- game, it doesn't seem possible and spin dash releases anyway, although I couldn't test
+ -- with TAS so not sure at; but it doesn't matter, we don't reproduce this edge behavior at 100%)
+ self:release_spin_dash()
+ elseif self.jump_intention then
+ -- player is charging spin dash (this includes the initial charge)
+ -- consume intention so puppet control mode (which is sticky) also works
+ self.jump_intention = false
+
+ -- enter spin dashing state the first time, after that it will just be rev
+ if self.motion_state == motion_states.crouching then
+ self:enter_motion_state(motion_states.spin_dashing)
+
+ -- reset ground speed (it effectively freezes it, as update won't apply slope factor
+ -- during spin dash charge)
+ self.ground_speed = 0
+
+ -- reset spin dash rev (it's important to do because we do not reset it on release)
+ self.spin_dash_rev = 0
+ end
+
+ -- revvin' up!
+ -- this includes the first rev
+
+ -- fill spin dash rev formula from SPG
+ self.spin_dash_rev = min(self.spin_dash_rev + pc_data.spin_dash_rev_increase_step, pc_data.spin_dash_rev_max)
+
+ -- visual
+
+ -- exceptionally play anim from here instead of player_char:check_play_anim,
+ -- because we must replay animation from start on every rev
+ self:update_sprite_row_and_play_sprite_animation("spin_dash", --[[from_start:]] true)
+
+--#if pfx
+ -- hardcoded values as unlikely to change once set, and to spare characters
+ self.smoke_pfx:start(self.position + vector(0, 5), self.orientation == horizontal_dirs.left)
+--#endif
+
+ -- audio
+ self:play_low_priority_sfx(audio.sfx_ids.spin_dash_rev)
+ else
+ if self.motion_state == motion_states.spin_dashing then
+ -- only apply friction when not charging this frame (gives a change to reach maximum speed,
+ -- although needs perfect timing)
+ self.spin_dash_rev = self.spin_dash_rev * pc_data.spin_dash_drag_factor_per_frame
+ end
+ end
+ end
+end
+
+-- release spin dash and launch character rolling at charged speed
+-- we assume character is spin dashing
+function player_char:release_spin_dash()
+ -- set ground speed and let velocity be updated next frame (we're not losing a frame)
+ local dir_sign = horizontal_dir_signs[self.orientation]
+ self:enter_motion_state(motion_states.rolling)
+
+ -- set ground speed using base launch speed and rev contribution
+ self.ground_speed = dir_sign * (pc_data.spin_dash_base_speed + flr(self.spin_dash_rev) * pc_data.spin_dash_rev_increase_factor)
+
+--#if pfx
+ -- visual
+ self.smoke_pfx:stop()
+--#endif
+
+ -- audio
+ self:play_low_priority_sfx(audio.sfx_ids.spin_dash_release)
+end
+
-- update motion following platformer airborne motion rules
function player_char:update_platformer_motion_airborne()
+ -- SPG note: http://info.sonicretro.org/SPG:Main_Game_Loop
+ -- I started working on this before this page appeared though, so the order may not exactly be the same
+ -- For instance, I update gravity before air drag, so air drag actually checks the new velocity y,
+ -- although the difference is not perceptible.
+ -- There are also some flow differences, e.g. we call check_update_sprite_angle in update_anim
+ -- then check for falling state inside, instead of making it part of the airborne update
+ -- Nevertheless, it's working quite well.
+
+ -- TODO: follow Main_Game_Loop and update gravity AFTER applying velocity
+ -- this will remove the need for has_jumped_this_frame member while preserving behavior
if self.has_jumped_this_frame then
-- do not apply gravity on first frame of jump, and consume has_jumped_this_frame
self.has_jumped_this_frame = false
@@ -1778,9 +2264,13 @@ function player_char:update_platformer_motion_airborne()
self.velocity.y = pc_data.max_air_velocity_y
end
- -- apply air motion
+ -- apply air motion without caring about obstacles to start with (step 5 in SPG Main Loop)
+ self.position:add_inplace(self.velocity)
- local air_motion_result = self:compute_air_motion_result()
+ -- we're supposed to apply gravity here
+
+ -- check for air collisions (wall, ceiling, ground) and update position in-place
+ local air_motion_result = self:check_air_collisions()
-- FIX to top-left corner enter during jump lies here, or when is_blocked_by_wall is set...
-- since motion is not considered up, we are only blocked by wall...
@@ -1794,8 +2284,8 @@ function player_char:update_platformer_motion_airborne()
end
-- check for stage left edge soft block
- -- see _update_platformer_motion_grounded
- if flr(air_motion_result.position.x) < pc_data.ground_sensor_extent_x then
+ -- see update_platformer_motion_grounded
+ if flr(self.position.x) < pc_data.ground_sensor_extent_x then
-- clamp position to stage left edge and clamp velocity x to 0
-- note that in theory we should update the air motion result
-- tile location and slope angle to match the new position,
@@ -1803,18 +2293,19 @@ function player_char:update_platformer_motion_airborne()
-- nothing on the left of the stage so basically we already have
-- the ground info we need, worst case character will fall 1 extra frame
-- then land
- air_motion_result.position.x = ceil(pc_data.ground_sensor_extent_x)
+ self.position.x = ceil(pc_data.ground_sensor_extent_x)
self.velocity.x = max(0, self.velocity.x)
end
- self.position = air_motion_result.position
-
if air_motion_result.is_landing then
-- register new ground tile, update slope angle and enter standing state
self:set_ground_tile_location(air_motion_result.tile_location)
self:set_slope_angle_with_quadrant(air_motion_result.slope_angle)
-- always stand on ground, if we want to roll we'll switch to rolling on next frame
self:enter_motion_state(motion_states.standing)
+
+ -- with the new big step method and reliable escape, the second call to
+ -- check_escape_from_ground here is now unneeded
end
log("self.position: "..self.position, "trace")
@@ -1868,343 +2359,131 @@ function player_char:clamp_air_velocity_x(previous_velocity_x)
end
end
--- return {next_position: vector, is_blocked_by_ceiling: bool, is_blocked_by_wall: bool, is_landing: bool} where
+-- check for air collisions with wall and ground, and apply escape vector in-place on self.position
+-- return {next_position: vector, is_blocked_by_wall: bool, is_blocked_by_ceiling: bool, is_landing: bool} where
-- - next_position is the position of the character next frame considering his current (air) velocity
-- - is_blocked_by_ceiling is true iff the character encounters a ceiling during this motion
-- - is_blocked_by_wall is true iff the character encounters a wall during this motion
-- - is_landing is true iff the character touches a ground from above during this motion
-function player_char:compute_air_motion_result()
+function player_char:check_air_collisions()
-- if character is not moving, he is not blocked nor landing (we assume the environment is static)
- -- this is pretty rare in the air, but could happen when being pushed upward by fans
+ -- this is pretty rare in the air, but could happen at the apogee
if self.velocity:is_zero() then
return motion.air_motion_result(
- nil, -- start in air, so no ground tile
- self.position,
- false,
- false,
- false,
- nil
+ nil, -- start in air, so no ground tile
+ false, -- is_blocked_by_wall
+ false, -- is_blocked_by_ceiling
+ false, -- is_landing
+ nil -- slope_angle
)
end
- -- initialize air motion result (do not floor coordinates, _advance_in_air_along will do it)
- local motion_result = motion.air_motion_result(
- nil, -- start in air, so no ground tile
- vector(self.position.x, self.position.y),
- false,
- false,
- false,
- nil
- )
-
- -- from here, unlike ground motion, there are 3 ways to iterate:
- -- a. describe a Bresenham's line, stepping on x and y, for the best precision
- -- b. step on x until you reach the max distance x, then step on y (may hit wall you wouldn't have with a. or c.)
- -- c. step on y until you reach the max distance y, then step on x (may hit ceiling you wouldn't have with a. or b.)
- -- and 1 way without iteration:
- -- d. compute final position of air motion at the end of the frame, and escape from x and y if needed
-
- -- We choose b. which is precise enough while always finishing with a potential landing
- -- Initially we used c., but Sonic tended to fly above descending slopes as the X motion was applied
- -- after Y motion, including snapping, causing a ladder-shaped motion above the slope where the final position
- -- was always above the ground.
- -- Note, however, that this is a temporary fix: where we add quadrants, X and Y will have more symmetrical roles
- -- and we can expect similar issues when trying to land with high speed adherence on a 90-deg wall.
- -- Ultimately, I think it will work better with either d. or an Unreal-style multi-mode step approach
- -- (i.e. if landing in the middle of the Y move, finish the remaining part of motion as standing,
- -- following the ground as usual).
- self:advance_in_air_along(motion_result, self.velocity, "x")
- log("=> "..motion_result, "trace2")
- self:advance_in_air_along(motion_result, self.velocity, "y")
- log("=> "..motion_result, "trace2")
+ -- SPG note: http://info.sonicretro.org/SPG:Main_Game_Loop
+ -- We're following the indicated order by checking wall before ground
- return motion_result
-end
+ -- Big step method:
+ -- 1. escape from any wall and remember being blocked by it
+ -- 2a. (going down) escape from any ground and remember landing
+ -- 2b. (going up) escape from any ceiling and remember being blocked by it
--- TODO: factorize with _compute_ground_motion_result
--- modifies ref_motion_result in-place, setting it to the result of an air motion from ref_motion_result.position
--- over velocity:get(coord) px, where coord is "x" or "y"
-function player_char:advance_in_air_along(ref_motion_result, velocity, coord)
- log("_advance_in_air_along: "..joinstr(", ", ref_motion_result, velocity, coord), "trace2")
+ -- Note that there is no extra wall check after that unlike compute_ground_motion_result
+ -- because even if we landed and changed quadrant (from air quadrant which is always down) and happen to stop
+ -- touching a wall with new quadrant, the effect of blocking would just be to lose velocity X,
+ -- but velocity Y would still contribute to landing ground speed, so we would not brutally stop motion as on ground.
- if velocity:get(coord) == 0 then return end
+ -- There is no position prediction step in this case, because the caller must already
+ -- have updated the position to next position (assuming no obstacles at first)
- -- only full pixels matter for collisions, but subpixels may sum up to a full pixel
- -- so first estimate how many full pixel columns the character may actually explore this frame
- local initial_position_coord = ref_motion_result.position:get(coord)
- local max_pixel_distance = player_char.compute_max_pixel_distance(initial_position_coord, velocity:get(coord))
+ local ground_tile_location-- = nil
+ local is_blocked_by_wall = false
+ local is_blocked_by_ceiling = false
+ local is_landing = false
+ local slope_angle-- = nil
- -- floor coordinate to simplify step by step pixel detection (mostly useful along x to avoid
- -- flooring every time we query column heights)
- -- since initial_position_coord is storing the original position with subpixels, we are losing information
- ref_motion_result.position:set(coord, flr(ref_motion_result.position:get(coord)))
+ -- Step 1: wall check
- -- iterate pixel by pixel on the x direction until max possible distance is reached
- -- only stopping if the character is blocked by a wall (not if falling, since we want
- -- him to continue moving in the air as far as possible; in edge cases, he may even
- -- touch the ground again some pixels farther)
- local direction
- if coord == "x" then
- direction = directions.right
- else
- direction = directions.down
- end
- if velocity:get(coord) < 0 then
- direction = oppose_dir(direction)
- end
-
- local pixel_distance_before_step = 0
- while pixel_distance_before_step < max_pixel_distance and not ref_motion_result:is_blocked_along(direction) do
- self:next_air_step(direction, ref_motion_result)
- log(" => "..ref_motion_result, "trace2")
- pixel_distance_before_step = pixel_distance_before_step + 1
- end
-
- -- check if we need to add or cut subpixels
- if not ref_motion_result:is_blocked_along(direction) then
- -- since subpixels are always counted to the right, the subpixel test below is asymmetrical
- -- but this is correct, we will simply move backward a bit when moving left
- local are_subpixels_left = initial_position_coord + velocity:get(coord) > ref_motion_result.position:get(coord)
- -- local are_subpixels_left = initial_position_coord + max_pixel_distance > ref_motion_result.position:get(coord)
- if are_subpixels_left then
- -- character has not been blocked and has some subpixels left to go
- -- *only* when moving in the positive sense (right/up),
- -- as a way to clean the subpixels unlike classic sonic,
- -- we check if character is theoretically colliding a wall with those subpixels
- -- (we need an extra step to "ceil" the subpixels)
- -- when moving in the negative sense, the subpixels are a small "backward" motion
- -- to the positive sense and should
- -- never hit a wall back
- local is_blocked_by_extra_step = false
- if velocity:get(coord) > 0 then
- local extra_step_motion_result = ref_motion_result:copy()
- self:next_air_step(direction, extra_step_motion_result)
- log(" => "..ref_motion_result, "trace2")
- if extra_step_motion_result:is_blocked_along(direction) then
- -- character has just reached a wall, plus a few subpixels
- -- unlike classic sonic, we decide to cut the subpixels and block the character
- -- on the spot (we just reuse the result of the extra step, since is_falling doesn't change if is_blocked is true)
- -- it's very important to keep the reference and assign member values instead
- ref_motion_result:copy_assign(extra_step_motion_result)
- is_blocked_by_extra_step = true
- end
- end
-
- if not is_blocked_by_extra_step then
- -- character has not touched a wall at all, so add the remaining subpixels
- -- (it's simpler to just recompute the full motion in x; don't touch y tough,
- -- as it depends on the shape of the ground)
- -- do not apply other changes (like slope) since technically we have not reached
- -- the next tile yet, only advanced of some subpixels
- -- note that this calculation equivalent to adding to ref_motion_result.position:get(coord)
- -- sign(velocity:get(coord)) * (max_distance - distance_to_floored_coord)
- ref_motion_result.position:set(coord, initial_position_coord + velocity:get(coord))
- log(" => (after adding remaining subpx) "..ref_motion_result, "trace2")
- end
- end
+ -- if moving horizontally, do a wall raycast in the direction of velocity X, from current position
+ if self.velocity.x ~= 0 then
+ local quadrant_horizontal_dir = signed_speed_to_dir(self.velocity.x)
+ is_blocked_by_wall = self:check_escape_wall_and_update_next_position(self.position, quadrant_horizontal_dir)
end
-end
-
--- update ref_motion_result: motion.air_motion_result for a character trying to move
--- by 1 pixel step in direction in the air, taking obstacles into account
--- if character is blocked by wall, ceiling or landing when moving toward left/right, up or down resp.,
--- it doesn't update the position and the corresponding flag is set
--- air_motion_result.position.x/y should be floored for these steps
-function player_char:next_air_step(direction, ref_motion_result)
- log(" next_air_step: "..joinstr(", ", direction, ref_motion_result), "trace2")
-
- local step_vec = dir_vectors[direction]
- local next_position_candidate = ref_motion_result.position + step_vec
- log("direction: "..direction, "trace2")
- log("step_vec: "..step_vec, "trace2")
- log("next_position_candidate: "..next_position_candidate, "trace2")
+ if self.velocity.y > 0 then
+ -- Step 2a: ground check
+ -- Note that we just check going down to simplify while original game may accept going slightly up at sheer angle
- -- we can only hit walls or the ground when stepping left, right or down
- -- (horizontal step of diagonal upward motion is OK)
- if direction ~= directions.up then
- -- query ground to check for obstacles (we only care about distance, not slope angle)
- -- note that we reuse the ground sensors for air motion, because they are good at finding
- -- collisions around the bottom left/right corners
- local query_info = self:compute_ground_sensors_query_info(next_position_candidate)
- local signed_distance_to_closest_ground = query_info.signed_distance
+ -- Check if next position is inside/above ground
+ -- (same as compute_ground_motion_result)
+ -- REFACTOR: this really is check_escape_from_ground, consider reusing this method here,
+ -- although we're setting vars more than actually changing state (except for self.position)
+ -- so this may need to readjust method to return something like vector_to_closest_ground or nil
+ local ground_query_info = self:compute_ground_sensors_query_info(self.position)
+ local signed_distance_to_closest_ground = ground_query_info.signed_distance
log("signed_distance_to_closest_ground: "..signed_distance_to_closest_ground, "trace2")
- -- Check if the character has hit a ground or a wall
- -- First, following SPG (http://info.sonicretro.org/SPG:Solid_Tiles#Ceiling_Sensors_.28C_and_D.29),
- -- allow jump from an ascending sheer angle directly onto a platform. This includes moving horizontally.
- -- This must be combined with a step up (snap to ground top, but directly from the air) to really work
- if self.velocity.y > 0 or abs(self.velocity.x) > abs(self.velocity.y) then
- -- check if we are entering ground
- -- NOTE: for solid ground we could also consider *touching* as landing, by checking <= 0,
- -- then we'd need to move signed_distance_to_closest_ground definition outside direction ~= directions.up block
- -- and in the bottom block of this method, check if not ref_motion_result:is_blocked_along(direction) or
- -- signed_distance_to_closest_ground == 0, instead of just is_blocked_along, since when landing
- -- we are technically blocked along q-down, but must still update position to avoid getting stuck above ground
- if signed_distance_to_closest_ground < 0 then
- -- Just like during ground step, check the step height: if too high, we hit a wall and stay airborne
- -- else, we land
- -- This step up check is really important, even for low slopes:
- -- if not done, when Sonic lands on an ascending slope, it will consider the few pixels up
- -- to be a wall!
- -- I used to check direction == directions.down only, and indeed if you step 1px down,
- -- the penetration distance will be no more than 1 and you will always snap to ground.
- -- But this didn't work when direction left/right hit the slope.
- -- refactor: code is similar to check_escape_from_ground and above all next_ground_step
- if - signed_distance_to_closest_ground <= pc_data.max_ground_escape_height then
- next_position_candidate.y = next_position_candidate.y + signed_distance_to_closest_ground
- -- landing: the character has just set foot on ground, flag it and initialize slope angle
- -- note that we only consider the character to touch ground when it is about to enter it
- -- below deprecated if we <= 0 check
- -- therefore, if he exactly reaches signed_distance_to_closest_ground == 0 this frame,
- -- it is still technically considered in the air
- -- if this step is blocked by landing, there is no extra motion,
- -- but character will enter standing state
- ref_motion_result.is_landing, ref_motion_result.slope_angle = true, query_info.slope_angle
- -- WALL LANDING ADJUSTMENT OFFSET
- -- as part of the bigger adherence system, but for now very simplified
- -- to fix #129 BUG MOTION curve_run_up_fall_in_wall:
- -- if the quadrant changed (from the air default, down), we must adjust the character
- -- center position to stabilize his bottom position, so that his feet are just touching the new ground
- -- instead of entering it
- -- world.angle_to_quadrant will be called later as part of player_char:set_slope_angle_with_quadrant
- -- on the final air motion result, but we prefer adjusting the position now
- local new_quadrant = world.angle_to_quadrant(ref_motion_result.slope_angle)
- -- we only care about left and right wall, as character center is *centered* in the collision rectangle,
- -- if character becomes upside down (e.g. with ceiling adherence system) his feet will be placed
- -- where his head was when it hit the ceiling, so they will also touch the ceiling
- -- (currently character can only adhere to bottom-left or bottom-right slopes anyway, but if very steep
- -- they are turn the quadrant to the side)
- if new_quadrant % 2 == 0 then -- equivalent to (new_quadrant == directions.left or new_quadrant == directions.right)
- -- unfortunaly, check_escape_from_ground even with the new quadrant code proved either unable to push
- -- the character out (already too deep in ground), or pushing the character out too much (when increasing escape threshold just there)
- -- when called after taking final air motion result into account and detecting landing
- -- so instead we manually adjust the position here, without being sure of how close to the ground
- -- we are due to the complex case of rotating near a slope, but as an estimation we consider that
- -- Sonic being a rectangle (almost a square when compact due to ground sensor 2.5 ~= compact center height 4 though,
- -- being the reason for bug #129 only showing when falling standing), by adding the difference ground/wall sensor
- -- vs center height we can somewhat escape from the ground and let next updates do the final adjustments
- -- (leaving ground again and falling again on flatter ground, or escaping ground on next full pixel ground motion
- -- so this time Sonic really steps exactly on the ground)
- local new_quadrant_down = dir_vectors[new_quadrant]
- local qupward_offset = - ceil(self:get_center_height() - pc_data.ground_sensor_extent_x) * new_quadrant_down
- ref_motion_result.position:add_inplace(qupward_offset)
- end
- -- to simplify we keep the tile location, even though in theory we should readjust it to the adjusted position,
- -- as we consider the position close enough, and if it sent us airborne then we'll just reland in a few frames anyway
- -- note that at this point, it would be good to return some signal to the caller (advance_in_air_along)
- -- to tell them to stop iterating because moving on XY after landing is not consistent,
- -- plus we may have started adjusting the position above (if quadrant is left or right)
- -- causing a weird result if we go on; but there are currently no visible issues in game
- ref_motion_result.tile_location = query_info.tile_location
- log("is landing at adjusted y: "..next_position_candidate.y..", setting slope angle to "..query_info.slope_angle, "trace2")
- else
- ref_motion_result.is_blocked_by_wall = true
- log("is blocked by wall", "trace2")
- end
- else
- -- in the air: the most common case, in general requires nothing to do
- -- in rare cases, the character has landed on a previous step, and we must cancel that now
- ref_motion_result.is_landing, ref_motion_result.slope_angle = false--, nil
- ref_motion_result.tile_location = nil
- end
- end
- end
+ -- to spare characters, instead of checking if we detected a tile, then check distance,
+ -- we just check if distance is negative - that should only be true when a tile was found
+ -- (new convention makes sure to always set tile position even if too deep inside ground)
+ -- convention v3 is to ignore ground completely if too deep inside
+ -- to avoid walking with head on ceiling or inside one-way platform when a little too low,
+ -- so check for ideal range here
+ if - pc_data.max_ground_escape_height <= signed_distance_to_closest_ground and signed_distance_to_closest_ground < 0 then
+ assert(ground_query_info.tile_location, "signed_distance_to_closest_ground < 0 yet ground_query_info.tile_location is not set")
+
+ -- Next position is inside ground, close enough to surface => Step up
+ -- (same as compute_ground_motion_result)
+ -- Note that enter_motion_state contains its own code to adjust center position based on becoming (un)compact
+ local vector_to_closest_ground = signed_distance_to_closest_ground * self:get_quadrant_down()
+ self.position:add_inplace(vector_to_closest_ground)
- -- Ceiling check
- -- It is necessary during horizontal motion to complement
- -- ground sensors, the edge case being when the bottom of the character matches
- -- the bottom of a collision tile, ground sensors could only detect the tile below
- -- if we have already found a blocker above and are still in left/right direction
- -- then there is no need to check further, though.
- -- But, as in the case of a jump into a ceiling corner, we may have an old is_blocked_by_wall flag from a previous
- -- x motion step and now doing a y motion (in particular upward) that is unrelated.
- -- We definitely want to detect the wall on the side AND the ceiling, so if direction is up, ALSO check
- -- the ceiling even if wall was found in previous step.
-
- -- The SPG (http://info.sonicretro.org/SPG:Solid_Tiles#Ceiling_Sensors_.28C_and_D.29)
- -- remarks that ceiling detection is done when moving upward or when moving faster horizontally than vertically
- -- (this includes moving horizontally)
- -- Since it's just for this extra test, we check self.velocity directly instead of passing it as argument
- -- Note that we don't check the exact step direction, if we happen to hit the ceiling during
- -- the X motion, that's fine.
- -- In practice, when approaching a ceiling from a descending direction with a sheer horizontal angle,
- -- we will hit the block as a wall first; but that's because we consider blocks as wall and ceilings at the same time.
- -- If we wanted to be symmetrical with floor check above, we would need to call some check_escape_from_ceiling
- -- to snap Sonic slightly down when only hitting the wall by a few pixels, so character can continue moving horizontally
- -- under the ceiling, touching it at the beginning. But it doesn't seem to happen in Classic Sonic so we don't implement
- -- it unless our stage has ceilings where this often happens and it annoys the player.
- -- UPDATE: it doesn't seem to reliable as itest platformer slope ceiling block right
- -- would fail by considering character blocked by ascending slope above nothing
- -- I'm not sure why that itest used to work, but if having issues with this,
- -- add an extra check on ground step if no pixel is found (and exactly at a tile bottom)
- -- to see if there is not a collision pixel 1px above (should be on another tile above)
- -- and from here compute the actual ground distance... of course, always add supporting ground
- -- tile under a ground tile when possible
- -- UPDATE after adding landing on ceiling: the condition should still work with ceiling adherence catch,
- -- although the SPG doesn't mention it again in Slope Physics
- if not ref_motion_result.is_blocked_by_wall and
- (self.velocity.y < 0 or abs(self.velocity.x) > abs(self.velocity.y)) or direction == directions.up then
- -- TODO: use new compute_ceiling_sensors_query_info to retrieve complete info
- -- if signed distance is negative, then we're hitting the ceiling. But better, we can check slope for adherence
- -- https://info.sonicretro.org/SPG:Slope_Physics#When_Going_Upward
- local ceiling_query_info = self:compute_ceiling_sensors_query_info(next_position_candidate)
-
- -- if there is touch/collision with ceiling, tile_location is set
- if ceiling_query_info.tile_location then
- -- note that angles inclusive/exclusive are not exactly like SPG says, because the comparisons were asymmetrical,
- -- which must have made sense in terms of coding at the time, but we prefer symmetrical angles. Besides, we actually
- -- have ceiling slopes at 45 degrees which we'd like to adhere onto
---#if assert
- assert(ceiling_query_info.signed_distance <= 0, "player_char:next_air_step: touch/collision detected with ceiling "..
- "but signed distance is positive: "..ceiling_query_info.signed_distance)
+ is_landing = true
+ ground_tile_location = ground_query_info.tile_location -- no need to :copy(), we won't reuse ground_query_info
+ slope_angle = ground_query_info.slope_angle
+ end
+ -- unlike compute_ground_motion_result, we don't care about the else case, where we hover over ground
+ -- or just touch it, as we don't want to step down, and we only consider we are landing when
+ -- entering ground by at least a fraction of pixel
+ elseif self.velocity.y < 0 then
+ -- Step 2a: ceiling check
+ -- Note that we just check going up to simplify while original game may accept going slightly down at sheer angle
+
+ local ceiling_query_info = self:compute_ceiling_sensors_query_info(self.position)
+ local signed_distance_to_closest_ceiling = ceiling_query_info.signed_distance
+
+ if signed_distance_to_closest_ceiling < 0 then
+ assert(ceiling_query_info.tile_location, "signed_distance_to_closest_ground < 0 yet ceiling_query_info.tile_location is not set")
assert(ceiling_query_info.slope_angle > 0.25 and ceiling_query_info.slope_angle < 0.75,
- "player_char:next_air_step: touch/collision detected with ceiling and quadrant is always down when airborne, yet "..
- "ceiling_query_info.slope_angle is not between 0.25 and 0.75, it is: "..ceiling_query_info.slope_angle)
---#endif
+ "detected ceiling with slope angle expected between 0.25 and 0.75 excluded, got: "..ceiling_query_info.slope_angle..
+ " (we don't check for those bounds in ceiling adherence check so Sonic may adhere to unwanted walls)")
+ -- Hit ceiling
+ -- Whether we can land or not, we must escape (there is no max ceiling escape distance, so just do it)
+ -- Remember to oppose direction of quadrant down to get quadrant up used for ceiling raycast (hence `-`)
+ local vector_to_closest_ceiling = - signed_distance_to_closest_ceiling * self:get_quadrant_down()
+ self.position:add_inplace(vector_to_closest_ceiling)
+
if ceiling_query_info.slope_angle <= 0.25 + pc_data.ceiling_adherence_catch_range_from_vertical or
ceiling_query_info.slope_angle >= 0.75 - pc_data.ceiling_adherence_catch_range_from_vertical then
- -- character lands on ceiling aka ceiling adherence catch (touching is enough, and no extra condition on velocity)
- ref_motion_result.tile_location = ceiling_query_info.tile_location
- -- no need to set position, we are not blocked by wall and should not be blocked along direction
- -- (mostly up for ceiling, and rarely left/right when entering this block with the sheer angle condition)
- -- so we'll enter the final block at the bottom which sets ref_motion_result.position to next_position_candidate
- ref_motion_result.is_landing = true
- ref_motion_result.slope_angle = ceiling_query_info.slope_angle
- elseif ceiling_query_info.signed_distance < 0 then
- -- character hit the hard (almost horizontal) ceiling and cannot adhere: just blocked by ceiling,
- -- or, if moving to the side, blocked by wall
- -- note that above we check for going inside ceiling to be exact, since just touching it should not block you,
- -- while landing on ceiling can happen just when touching ceiling (but difference is hard to see in game,
- -- as you rarely jump and just touch the ceiling anyway)
- if direction == directions.up then
- ref_motion_result.is_blocked_by_ceiling = true
- log("is blocked by ceiling", "trace2")
- else
- -- we would be blocked by ceiling on the next position, but since we can't even go there,
- -- we are actually blocked by the wall preventing the horizontal move
- -- 4-quadrant note: if moving diagonally downward, this will actually correspond to the SPG case
- -- mentioned above where ysp >= 0 but abs(xsp) > abs(ysp)
- -- in this case, we are really detecting the *ceiling*, but Sonic can also start running on it
- -- we should actually test the penetration distance is a symmetrical way to ground, not just the direction
- ref_motion_result.is_blocked_by_wall = true
- log("is blocked by ceiling as wall", "trace2")
- end
+ -- Character lands on ceiling aka ceiling adherence catch (we changed convention to match ground
+ -- and only land when entering ceiling by at least a fraction of pixel; otherwise, no extra condition on velocity)
+ -- Note that enter_motion_state contains its own code to adjust center position based on becoming (un)compact
+ ground_tile_location = ceiling_query_info.tile_location
+ is_landing = true
+ slope_angle = ceiling_query_info.slope_angle
+ else
+ -- Cannot land, just get blocked by ceiling (will reset velocity Y)
+ is_blocked_by_ceiling = true
end
end
end
- -- only advance if character is still not blocked (else, preserve previous position,
- -- which should be floored)
- if not ref_motion_result:is_blocked_along(direction) then
- -- this only works because the wall sensors are 1px farther from the character center
- -- than the ground sensors; if there were even farther, we'd even need to
- -- move the position backward by hypothetical wall_sensor_extent_x - ground_sensor_extent_x - 1
- -- when ref_motion_result:is_blocked_along() (and adapt y)
- ref_motion_result.position = next_position_candidate
- log("not blocked, setting motion result position to next candidate: "..next_position_candidate, "trace2")
- end
+ return motion.air_motion_result(
+ ground_tile_location,
+ is_blocked_by_wall,
+ is_blocked_by_ceiling,
+ is_landing,
+ slope_angle
+ )
end
-- item and trigger checks
@@ -2234,6 +2513,9 @@ function player_char:trigger_spring(spring_obj)
-- set orientation to match spring, even in the air
-- small trick to convert cardinal direction to horizontal direction
-- cardinal left = 0 -> horizontal left 1, cardinal right = 2 -> horizontal right = 2
+ -- unlike the original game, it will make Sonic look in the spring's direction before actually moving
+ -- there, since render will be done before next velocity update
+ -- to fix that, you can just check for spring collision at the beginning of the update rather than the end
self.orientation = spring_obj.direction / 2 + 1
-- set horizontal control lock to prevent character from immediately braking (when grounded)
@@ -2242,7 +2524,7 @@ function player_char:trigger_spring(spring_obj)
local horizontal_dir_sign = horizontal_dir_signs[self.orientation]
if self:is_grounded() then
-- we assume the spring on ground (ceiling would reverse ground speed sign)
- -- set the ground speed and let velocity be updated next frame
+ -- set the ground speed and let velocity be updated next frame (we're not losing a frame)
self.ground_speed = horizontal_dir_sign * pc_data.spring_jump_speed_frame
else
-- in the air, only velocity makes sense
@@ -2271,7 +2553,7 @@ function player_char:check_launch_ramp()
local ground_tile_region_loc = curr_stage_state:global_to_region_location(self.ground_tile_location)
local ground_visual_tile_id = mget(ground_tile_region_loc.i, ground_tile_region_loc.j)
- if ground_visual_tile_id == visual.launch_ramp_last_tile_id then
+ if ground_visual_tile_id == visual_ingame_data.launch_ramp_last_tile_id then
self:trigger_launch_ramp_effect()
end
end
@@ -2283,7 +2565,7 @@ function player_char:trigger_launch_ramp_effect()
local new_speed = min(pc_data.launch_ramp_speed_max_launch_speed, self.ground_speed * pc_data.launch_ramp_speed_multiplier)
- self.velocity = new_speed * vector.unit_from_angle(pc_data.launch_ramp_velocity_angle)
+ self.velocity:copy_assign(new_speed * vector.unit_from_angle(pc_data.launch_ramp_velocity_angle))
self:enter_motion_state(motion_states.falling)
-- just reuse spring jump animation since in Sonic 3, launch ramp also uses 3D animation
@@ -2406,7 +2688,7 @@ function player_char:check_play_anim()
-- brake anims can be played during standing but also falling, so make a global check
-- giving priority to them
if self.brake_anim_phase == 1 then
- self.anim_spr:play("brake_start")
+ self:update_sprite_row_and_play_sprite_animation("brake_start")
-- unlike Sonic 3:
-- as long as brake anim has started, it gets priority over standing and falling
@@ -2420,7 +2702,7 @@ function player_char:check_play_anim()
-- no input and anim is over
return
elseif self.brake_anim_phase == 2 then
- self.anim_spr:play("brake_reverse")
+ self:update_sprite_row_and_play_sprite_animation("brake_reverse")
-- as long as brake anim is playing, it gets priority over standing and falling
-- brake anim ends with freeze_last, just to give us an extra frame to check
@@ -2437,15 +2719,15 @@ function player_char:check_play_anim()
if self.motion_state == motion_states.standing then
-- update ground animation based on speed
if self.ground_speed == 0 then
- self.anim_spr:play("idle")
+ self:update_sprite_row_and_play_sprite_animation("idle")
else
-- standing and moving: play walk cycle at low speed, run cycle at high speed
-- we have access to self.ground_speed but self.anim_run_speed is shorter than
-- abs(self.ground_speed), and the values are the same for normal to high speeds
if self.anim_run_speed < pc_data.run_cycle_min_speed_frame then
- self.anim_spr:play("walk", false, max(pc_data.walk_anim_min_play_speed, self.anim_run_speed))
+ self:update_sprite_row_and_play_sprite_animation("walk", false, max(pc_data.walk_anim_min_play_speed, self.anim_run_speed))
else
- self.anim_spr:play("run", false, self.anim_run_speed)
+ self:update_sprite_row_and_play_sprite_animation("run", false, self.anim_run_speed)
end
end
elseif self.motion_state == motion_states.falling then
@@ -2455,7 +2737,7 @@ function player_char:check_play_anim()
end
if self.should_play_spring_jump then
- self.anim_spr:play("spring_jump")
+ self:update_sprite_row_and_play_sprite_animation("spring_jump")
else
-- normal fall -> run in the air (even if not working, just to avoid having Sonic falling idle
-- e.g. when crumbling floor breaks beneath his feet; what Classic Sonic does, but we don't mind)
@@ -2463,20 +2745,123 @@ function player_char:check_play_anim()
-- but we can use the stored anim_run_speed, which is the same except for very low speed
-- (and we don't mind them as we are checking run cycle for high speeds)
if self.anim_run_speed < pc_data.run_cycle_min_speed_frame then
- self.anim_spr:play("walk", false, max(pc_data.walk_anim_min_play_speed, self.anim_run_speed))
+ self:update_sprite_row_and_play_sprite_animation("walk", false, max(pc_data.walk_anim_min_play_speed, self.anim_run_speed))
else
-- run_cycle_min_speed_frame > walk_anim_min_play_speed so no need to clamp here
- self.anim_spr:play("run", false, self.anim_run_speed)
+ self:update_sprite_row_and_play_sprite_animation("run", false, self.anim_run_speed)
end
end
+ elseif self.motion_state == motion_states.crouching then
+ -- we don't mind about speed here, character can totally slide at low speed due to momentum or slope
+ self:update_sprite_row_and_play_sprite_animation("crouch")
+ elseif self.motion_state == motion_states.spin_dashing then
+ -- exceptionally we don't need to self:update_sprite_row_and_play_sprite_animation("spin_dash"), it's already done on every rev
+ -- so we can also pass from_start: true
else -- self.motion_state == motion_states.rolling and self.motion_state == motion_states.air_spin
local min_play_speed = self.motion_state == motion_states.rolling and
pc_data.rolling_spin_anim_min_play_speed or pc_data.air_spin_anim_min_play_speed
- self.anim_spr:play("spin", false, max(min_play_speed, self.anim_run_speed))
+ self:update_sprite_row_and_play_sprite_animation("spin", false, max(min_play_speed, self.anim_run_speed))
+ end
+end
+
+-- table associating sprite animation name to double row index (starting at 0) in Sonic spritesheet
+-- memory stored in general memory
+-- animations with diagonal (45-degree) sprite variants are indicated by a suffix "45"
+-- they are not new animations per se, since they use exactly the same sequence of sprites at the same positions,
+-- but we must reload the appropriate sprites
+-- use ["key"] syntax to protect member names against minification
+local sprite_anim_name_to_double_row_index_table = {
+ ["idle"] = 0,
+ ["walk"] = 0,
+ ["walk45"] = 1,
+ ["brake_start"] = 3,
+ ["brake_reverse"] = 3,
+ ["run"] = 2,
+ -- encode the fact that the sprites start halfway on the first line of run sprites
+ -- remember we use double sprite rows => 16 lines, so 1 line = 1/16 of a double sprite row memory,
+ -- and half a line = 1/32 of that, hence + 1/32 (we could use manually add +0x20 if not relying on a pure factor)
+ ["run45"] = 2 + 1/32,
+ ["spin"] = 3,
+ ["crouch"] = 1,
+ ["spring_jump"] = 0,
+ ["spin_dash"] = 4,
+}
+
+-- helper to copy needed sprite (double) row in memory and play animation
+function player_char:update_sprite_row_and_play_sprite_animation(anim_key, from_start, speed)
+ -- play anim (important to still call it even if already played, to update speed or restart it)
+ self.anim_spr:play(anim_key, from_start, speed)
+
+ -- now update sprite angle parameters
+ -- this must be done *after* playing animation as it checks the current anim key
+ -- and *before* copying the correct double row of sprites below, as this requires
+ -- is_sprite_diagonal updated for the latest anim
+ self:update_sprite_angle_parameters()
+
+ -- Copy the first 8 rows = 4 double rows at once
+ -- Main Sonic sprites have been copied to general memory in stage_state:reload_runtime_data
+ -- We're copying them back, except we only copy the row (or partial row) we are interested in
+ -- Source addresses are the Dest addresses from reload_runtime_data + some offset if needed
+ -- Dest address is always row index 8 as we always play Sonic sprite animations on double row 8-9
+ -- (with spring_jump sprite exceptionally overlapping row 10)
+ -- 1 row = 0x200 so row index 8 starts at 0x1000 (middle of spritesheet)
+ -- Note that in the table below, double_row_index starts at 0 although the first double row of Sonic sprites
+ -- starts at row index 2 (it was just to preserve the cross sprite 0, although it doesn't really matter as we copy the sprites
+ -- elsewhere in runtime memory)
+
+ -- double_row_index Dest Source Size Content
+ -- 0 0x1000 0x4b00 0x400 First double row of Sonic sprites (walk cycle, idle, spring jump top)
+ -- 1 0x1000 0x4f00 0x400 Second double row of Sonic sprites (walk cycle 45 degrees, crouch 2 sprites)
+ -- 2 0x1000 0x5300 0x400 Third double row of Sonic sprites (run cycle 0 and 45 degrees)
+ -- 3 0x1000 0x5700 0x400 Fourth double row of Sonic sprites (air spin, brake 3 sprites)
+ -- 4 0x1000 0x5b00 0x1400 Last 5 Sonic sprites = 10x2 cells located on rows of indices 10-11 (spin dash sprites)
+
+ local anim_name_with_optional_suffix
+
+ -- is_sprite_diagonal already checks for "walk" and "run" so the suffixed name should be a valid entry
+ -- in the table
+ if self.is_sprite_diagonal then
+ -- those two animations have a 45-degree variant
+ -- indicate it with suffix "45"
+ anim_name_with_optional_suffix = anim_key.."45"
+ else
+ anim_name_with_optional_suffix = anim_key
+ end
+
+ -- find which double row (or half double row) to copy, and remember that for next time
+ local double_row_index = sprite_anim_name_to_double_row_index_table[anim_name_with_optional_suffix]
+ assert(double_row_index, "sprite_anim_name_to_double_row_index_table has no entry for key: "..anim_name_with_optional_suffix)
+
+ -- only copy row if not already done to preserve CPU every frame
+ if self.last_copied_double_row ~= double_row_index then
+ self.last_copied_double_row = double_row_index
+
+ local start_address = 0x4b00 + double_row_index * 0x400
+
+ if double_row_index < 4 then
+ -- if anim is anything but spin_dash, we can copy 2 full rows at once, back from general memory
+ -- note that we do this even for run45 which is a set of 4 sprites located on the right half of the spritesheet
+ -- this is because we already computed the correct start address thx to the fractional double_row_index,
+ -- and offsetting the whole run sprite set by half a line (+0x20 -> 0x5320) effectively moves the run45 sprites
+ -- to the left half of the spritesheet, losing the 1st (half) line of non-rotated run sprites, and ending
+ -- with an extra half line of irrelevant air+spin sprites copied from the line just after the last run45 sprites line
+ -- we could also copy the 16 half lines manually but it's simpler to just copy unused contiguous memory
+ -- as lons as we have a single operation
+ memcpy(0x1000, start_address, 0x400)
+ else
+ -- special case for spin_dash sprites which, for compactness, are only copied by half lines in general memory,
+ -- se we must copy 16 half lines back to runtime spritesheet memory
+ -- spin_dash sprites span over 10 cell lines = 10 * 4 bytes = 40 bytes = 0x28 bytes
+ -- reversing the logic from reload_runtime_data, source addresses are chained (+0x28)
+ -- while dest addresses leave a gap and skip a full line each time (+0x40)
+ for i = 0, 15 do
+ -- for spin_dash, start_address = 0x5b00
+ memcpy(0x1000 + i * 0x40, start_address + i * 0x28, 0x28)
+ end
+ end
end
end
--- update sprite angle (falling only)
function player_char:check_update_sprite_angle()
local angle = self.continuous_sprite_angle
assert(0 <= angle and angle < 1, "player_char:update_sprite_angle: expecting modulo angle, got: "..angle)
@@ -2484,7 +2869,7 @@ function player_char:check_update_sprite_angle()
if self.motion_state == motion_states.falling and angle ~= 0 then
if angle < 0.5 then
-- just apply friction calculation as usual
- self.continuous_sprite_angle = max(0, abs(angle) - pc_data.sprite_angle_airborne_reset_speed_frame)
+ self:set_continuous_sprite_angle(max(0, abs(angle) - pc_data.sprite_angle_airborne_reset_speed_frame))
else
-- problem is we must rotate counter-clockwise toward 1 which is actually 0 modulo 1
-- so we increase angle, clamp to 1 and % 1 so if we reached 1, we now have 0 instead
@@ -2493,59 +2878,16 @@ function player_char:check_update_sprite_angle()
end
end
--- replace all Sonic sprites that have a 45-degree rotation variant
--- with either the non-rotated or the 45-degree rotation variant
--- requirement: stage_state:reload_runtime_data must have been called
-function player_char:reload_rotated_sprites(rotated_by_45)
- -- see stage_state:reload_runtime_data for address explanation
- -- basically we are copying sprites general memory (with the correct
- -- address offset if rotated), back into the current spritesheet memory
- -- consider extracting a common helper function from both methods
- local addr_offset = rotated_by_45 and 0x500 or 0
-
- for i = 0, 15 do
- -- 6 walk cycle sprites
- memcpy(0x1008 + i * 0x40, 0x5300 + addr_offset + i * 0x30, 0x30)
-
- -- 4 run cycle sprites
- memcpy(0x1400 + i * 0x40, 0x5600 + addr_offset + i * 0x20, 0x20)
- end
-end
-
-- render the player character sprite at its current position
function player_char:render()
-- floor position to avoid jittering when running on ceiling due to
-- partial pixel position being sometimes one more pixel on the right due after 180-deg rotation
local floored_position = vector(flr(self.position.x), flr(self.position.y))
local flip_x = self.orientation == horizontal_dirs.left
- local sprite_angle = 0
-
- -- only walk and run can use rotated sprite
- if contains({"walk", "run"}, self.anim_spr.current_anim_key) then
- -- snap render angle to a few set of values (45 degrees steps), classic style
- -- (unlike Freedom Planet and Sonic Mania)
- -- 45 degrees is 0.125 = 1/8, so by multiplying by 8, each integer represent a 45-degree step
- -- we just need to add 0.5 before flooring to effectively round to the closest step, then go back
- sprite_angle = flr(8 * self.continuous_sprite_angle + 0.5) / 8
-
- -- an computed rotation of 45 degrees would result in an ugly sprite
- -- so we only use rotations multiple of 90 degrees, using handmade 45-degree
- -- sprites when we want a better angle resolution
- if sprite_angle % 0.25 == 0 then
- -- closest 45-degree angle is already cardinal, we can safely rotate
- -- still make sure we use non-rotated sprites in case we changed them earlier
- self:reload_rotated_sprites(--[[rotated_by_45: nil]])
- else
- -- closest 45-degree angle is diagonal, reload 45-degree sprite variants
- self:reload_rotated_sprites(--[[rotated_by_45: ]] true)
-
- -- rotated sprite embeds a rotation of 45 degrees, so if not flipped, rotate by angle - 45 degrees
- -- if flipped, the sprite is 45 degrees *behind* the horizontal left, so we must add 45 degrees instead
- sprite_angle = sprite_angle + (flip_x and 1 or -1) * 0.125
- end
- end
-
- self.anim_spr:render(floored_position, flip_x, false, sprite_angle)
+ self.anim_spr:render(floored_position, flip_x, false, self.sprite_angle)
+--#if pfx
+ self.smoke_pfx:render()
+--#endif
end
-- play sfx on channel 3, only if a jingle is not already playing there
@@ -2560,31 +2902,102 @@ function player_char:play_low_priority_sfx(n)
end
--#if debug_character
+local debug_ray_colors_hit = {
+ colors.green, -- first wall check
+ colors.pink, -- ground check (left sensor)
+ colors.pink, -- ground check (right sensor)
+ colors.blue, -- second wall check (optional)
+}
+
+local debug_ray_colors_no_hit = {
+ colors.yellow, -- first wall check
+ colors.white, -- ground check (left sensor)
+ colors.white, -- ground check (right sensor)
+ colors.peach, -- second wall check (optional)
+}
+
function player_char:debug_draw_rays()
-- debug "raycasts"
+ local i = 0
for debug_ray in all(self.debug_rays) do
- local start = debug_ray.start
- local end_pos = debug_ray.start + debug_ray.distance * debug_ray.direction
- if debug_ray.distance <= 0 then
- -- inside ground, ray will be all read up to surface
- line(start.x, start.y, end_pos.x, end_pos.y, colors.red)
+ i = i + 1
+ local start_pos = debug_ray.start:copy()
+ local end_pos = debug_ray.start + debug_ray.distance * debug_ray.direction_vector
+
+ -- if direction is left or up and the start / end position qx (x for left, y for up)
+ -- is at integer coordinate, then we must draw the corresponding pixel one pixel to the left/up
+ -- this is because start / end position is actually at a crosspoint (for qx; qy is always floored
+ -- and corresponds to an exact q-column index already), therefore when doing a symmetrical test
+ -- like wall left/right, the source is exactly at the same position, but at an integer qx = 1,
+ -- it should only cover qx = 0- when going left, but qx = 1+ when going right
+ -- same for the end position
+ -- this should fix the raycast stopping right before a left wall when blocked by left wall
+ -- (same for ceiling)
+ if debug_ray.direction_vector.x == -1 then
+ -- left
+ start_pos.x = ceil(start_pos.x) - 1
+ end_pos.x = ceil(end_pos.x) - 1
+ elseif debug_ray.direction_vector.y == -1 then
+ -- up
+ start_pos.y = ceil(start_pos.y) - 1
+ end_pos.y = ceil(end_pos.y) - 1
+ end
+
+ if debug_ray.hit then
+ -- hit, q-above ground (if distance > 0) or from inside ground (if distance <= 0),
+ -- ray will be pink except the last pixel which will be red
+ -- (subtract direction which is a cardinal unit vector to get the penultimate pixel)
+ line(start_pos.x, start_pos.y, end_pos.x, end_pos.y, debug_ray_colors_hit[i])
+ pset(end_pos.x, end_pos.y, colors.red)
else
- -- q-above ground, ray will be blue except the last pixel (subtract direction which is
- -- a cardinal unit vector to get the penultimate pixel)
- local before_end_pos = end_pos - debug_ray.direction
- line(start.x, start.y, before_end_pos.x, before_end_pos.y, colors.pink)
- mset(end_pos.x, end_pos.y, colors.red)
+ -- no-hit, draw full ray in white to distinguish from hit case
+ -- I tried different colors from distance <= 0 vs > 0, but unfortunately
+ -- they were hard to distinguish from the midground
+ -- so you'll have to guess the distance sign by looking at the environment
+ line(start_pos.x, start_pos.y, end_pos.x, end_pos.y, debug_ray_colors_no_hit[i])
end
end
end
function player_char:debug_print_info()
-- debug info
- api.print("state: "..self.motion_state, 8, 94, colors.white)
- api.print("quadrant: "..tostr(self.quadrant), 8, 100, colors.white)
- api.print("slope: "..tostr(self.slope_angle), 8, 106, colors.white)
- api.print("x: "..self.position.x, 8, 112, colors.white)
- api.print("y: "..self.position.y, 8, 118, colors.white)
+ outline.print_with_outline("state: "..self.motion_state, 8, 94, colors.white, colors.black)
+ outline.print_with_outline("quadrant: "..tostr(self.quadrant), 8, 100, colors.white, colors.black)
+ outline.print_with_outline("slope: "..tostr(self.slope_angle), 8, 106, colors.white, colors.black)
+ outline.print_with_outline("tile: "..(self.ground_tile_location and self.ground_tile_location.i..", "..self.ground_tile_location.j or "[nil]"),
+ 68, 106, colors.white, colors.black)
+ outline.print_with_outline("x: "..self.position.x, 8, 112, colors.white, colors.black)
+ outline.print_with_outline("y: "..self.position.y, 8, 118, colors.white, colors.black)
+ outline.print_with_outline("vx: "..self.velocity.x, 68, 112, colors.white, colors.black)
+ outline.print_with_outline("vy: "..self.velocity.y, 68, 118, colors.white, colors.black)
+end
+--#endif
+
+--#if debug_collision_mask
+function player_char:debug_draw_tile_collision_masks()
+ local curr_stage_state = flow.curr_state
+ assert(curr_stage_state.type == ':stage')
+
+ local region_topleft_loc = curr_stage_state:get_region_topleft_location()
+
+ -- debug "raycasts"
+ for debug_mask_global_tile_location in all(self.debug_mask_global_tile_locations) do
+ local debug_mask_region_tile_location = debug_mask_global_tile_location - region_topleft_loc
+ local tile_id = mget(debug_mask_region_tile_location.i, debug_mask_region_tile_location.j)
+ local tile_collision_flag = fget(tile_id, sprite_flags.collision)
+ if tile_collision_flag then
+ -- get the tile collision mask
+ local tcd = collision_data.get_tile_collision_data(tile_id)
+ assert(tcd, "collision_data.tiles_collision_data does not contain entry for sprite id: "..tile_id..", yet it has the collision flag set")
+
+ if tcd then
+ tcd:debug_render(debug_mask_global_tile_location)
+ end
+ else
+ assert(false, "region tile location "..debug_mask_region_tile_location.." with tile id "..tile_id.." was added to debug_mask_region_tile_locations "..
+ "but it has no collision flag")
+ end
+ end
end
--#endif
diff --git a/src/ingame/playercharacter_enums.lua b/src/ingame/playercharacter_enums.lua
index e8fa8c57..fcb99e16 100644
--- a/src/ingame/playercharacter_enums.lua
+++ b/src/ingame/playercharacter_enums.lua
@@ -24,8 +24,10 @@ motion_modes = {
-- enum for character motion state in platformer mode
motion_states = {
- standing = 1, -- character is idle or running on the ground
- falling = 2, -- character is falling in the air, but not spinning
- air_spin = 3, -- character is in the air after a jump
- rolling = 4, -- character is rolling on the ground
+ standing = 1, -- character is idle or running on the ground
+ falling = 2, -- character is falling in the air, but not spinning
+ air_spin = 3, -- character is in the air after a jump
+ rolling = 4, -- character is rolling on the ground
+ crouching = 5, -- character is crouching on the ground
+ spin_dashing = 6, -- character is charging spin dash
}
diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua
index 1952ba49..95cd7f56 100644
--- a/src/ingame/playercharacter_utest.lua
+++ b/src/ingame/playercharacter_utest.lua
@@ -8,14 +8,17 @@ local location_rect = require("engine/core/location_rect")
local input = require("engine/input/input")
local animated_sprite = require("engine/render/animated_sprite")
-local pc_data = require("data/playercharacter_data")
+local pc_data = require("data/playercharacter_numerical_data")
+local pc_sprite_data = require("data/playercharacter_sprite_data")
local emerald = require("ingame/emerald")
+local pfx = require("ingame/pfx")
local stage_state = require("ingame/stage_state")
local motion = require("platformer/motion")
local ground_query_info = motion.ground_query_info
local world = require("platformer/world")
local audio = require("resources/audio")
local visual = require("resources/visual_common")
+local visual_ingame_data = require("resources/visual_ingame_numerical_data")
local tile_repr = require("test_data/tile_representation")
local tile_test_data = require("test_data/tile_test_data")
@@ -23,63 +26,22 @@ describe('player_char', function ()
-- static methods
- describe('compute_max_pixel_distance', function ()
+ describe('pfx_size_ratio_over_lifetime', function ()
- it('(2, 0) => 0', function ()
- assert.are_equal(0, player_char.compute_max_pixel_distance(2, 0))
+ it('0 => 0.4', function ()
+ assert.are_equal(0.4, player_char.pfx_size_ratio_over_lifetime(0))
end)
- it('(2, 1.5) => 1', function ()
- assert.are_equal(1, player_char.compute_max_pixel_distance(2, 1.5))
+ it('0.36 => 1', function ()
+ assert.are_equal(1, player_char.pfx_size_ratio_over_lifetime(0.36))
end)
- it('(2, 3) => 3', function ()
- assert.are_equal(3, player_char.compute_max_pixel_distance(2, 3))
- end)
-
- it('(2.2, 1.7) => 1', function ()
- assert.are_equal(1, player_char.compute_max_pixel_distance(2.2, 1.7))
- end)
-
- it('(2.2, 1.8) => 2', function ()
- assert.are_equal(2, player_char.compute_max_pixel_distance(2.2, 1.8))
- end)
-
- -- bugfix history:
- -- / I completely forgot the left case, which is important to test flooring asymmetry
- -- I thought it was hiding bugs, but I realize my asymmetrical design was actually fine
-
- it('(2, -0.1) => 1', function ()
- assert.are_equal(1, player_char.compute_max_pixel_distance(2, -0.1))
- end)
-
- it('(2, -1) => 1', function ()
- assert.are_equal(1, player_char.compute_max_pixel_distance(2, -1))
- end)
-
- it('(2, -1.1) => 2', function ()
- assert.are_equal(2, player_char.compute_max_pixel_distance(2, -1.1))
- end)
-
- it('(2.2, -0.2) => 0', function ()
- assert.are_equal(0, player_char.compute_max_pixel_distance(2.2, -0.2))
- end)
-
- it('(2.2, -0.3) => 1', function ()
- assert.are_equal(1, player_char.compute_max_pixel_distance(2.2, -0.3))
- end)
-
- it('(2.2, -1.2) => 1', function ()
- assert.are_equal(1, player_char.compute_max_pixel_distance(2.2, -1.2))
- end)
-
- it('(2.2, -1.3) => 2', function ()
- assert.are_equal(2, player_char.compute_max_pixel_distance(2.2, -1.3))
+ it('1 => 0', function ()
+ assert.are_equal(0, player_char.pfx_size_ratio_over_lifetime(1))
end)
end)
-
-- methods
describe('init', function ()
@@ -116,7 +78,14 @@ describe('player_char', function ()
pc_data.debug_move_friction,
-- setup will modify anim_spr state, but we stubbed it so it's still
-- has the value on init now
- animated_sprite(pc_data.sonic_animated_sprite_data_table),
+ animated_sprite(pc_sprite_data.sonic_animated_sprite_data_table),
+ pfx(pc_data.spin_dash_dust_spawn_period_frames,
+ pc_data.spin_dash_dust_spawn_count,
+ pc_data.spin_dash_dust_lifetime_frames,
+ vector(pc_data.spin_dash_dust_base_init_velocity_x, pc_data.spin_dash_dust_base_init_velocity_y),
+ pc_data.spin_dash_dust_max_deviation,
+ pc_data.spin_dash_dust_base_max_size,
+ player_char.pfx_size_ratio_over_lifetime),
0, -- cheat
},
{
@@ -125,6 +94,7 @@ describe('player_char', function ()
pc.debug_move_decel,
pc.debug_move_friction,
pc.anim_spr,
+ pc.smoke_pfx,
pc.last_emerald_warp_nb, -- cheat
}
)
@@ -134,11 +104,11 @@ describe('player_char', function ()
describe('setup', function ()
setup(function ()
- spy.on(animated_sprite, "play")
+ spy.on(player_char, "update_sprite_row_and_play_sprite_animation")
end)
teardown(function ()
- animated_sprite.play:revert()
+ player_char.update_sprite_row_and_play_sprite_animation:revert()
end)
it('should reset the character state vars', function ()
@@ -154,7 +124,7 @@ describe('player_char', function ()
1,
0,
- location(-1, -1),
+ nil,
vector(-1, -1),
0,
0,
@@ -162,6 +132,8 @@ describe('player_char', function ()
vector.zero(),
0,
0,
+ 0,
+ 0,
vector.zero(),
false,
@@ -169,13 +141,17 @@ describe('player_char', function ()
false,
false,
false,
+ 0,
0,
0,
false,
0,
+ false,
+ 0,
{}, -- debug_character
+ {}, -- debug_collision_mask
},
{
pc.control_mode,
@@ -193,7 +169,9 @@ describe('player_char', function ()
pc.velocity,
pc.debug_velocity,
pc.slope_angle,
+ pc.late_jump_slope_angle,
pc.ascending_slope_time,
+ pc.spin_dash_rev,
pc.move_intention,
pc.jump_intention,
@@ -201,17 +179,22 @@ describe('player_char', function ()
pc.should_jump,
pc.has_jumped_this_frame,
pc.can_interrupt_jump,
+ pc.time_left_for_late_jump,
pc.anim_run_speed,
+ -- to simplify we test the actual result of set_continuous_sprite_angle, not that we called it
pc.continuous_sprite_angle,
+ pc.is_sprite_diagonal,
+ pc.sprite_angle,
pc.should_play_spring_jump,
pc.brake_anim_phase,
pc.debug_rays, -- debug_character
+ pc.debug_mask_global_tile_locations, -- debug_collision_mask
}
)
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "idle")
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "idle")
end)
end)
@@ -227,6 +210,9 @@ describe('player_char', function ()
curr_stage_state.loaded_map_region_coords = vector(0, 0)
flow.curr_state = curr_stage_state
+ -- create dummy app just for get_enable_late_jump_feature
+ flow.curr_state.app = { get_enable_late_jump_feature = function () return true end }
+
-- recreate player character for each test (setup spies will need to refer to player_char,
-- not the instance)
pc = player_char()
@@ -244,6 +230,16 @@ describe('player_char', function ()
assert.is_true(pc:is_grounded())
end)
+ it('should return true when character is crouching on the ground', function ()
+ pc.motion_state = motion_states.crouching
+ assert.is_true(pc:is_grounded())
+ end)
+
+ it('should return true when character is spin dashing on the ground', function ()
+ pc.motion_state = motion_states.spin_dashing
+ assert.is_true(pc:is_grounded())
+ end)
+
it('should return false when character is falling', function ()
pc.motion_state = motion_states.falling
assert.is_false(pc:is_grounded())
@@ -268,6 +264,16 @@ describe('player_char', function ()
assert.is_true(pc:is_compact())
end)
+ it('should return true when character is crouching on the ground', function ()
+ pc.motion_state = motion_states.crouching
+ assert.is_true(pc:is_compact())
+ end)
+
+ it('should return true when character is spin dashing on the ground', function ()
+ pc.motion_state = motion_states.spin_dashing
+ assert.is_true(pc:is_compact())
+ end)
+
it('should return false when character is falling', function ()
pc.motion_state = motion_states.falling
assert.is_false(pc:is_compact())
@@ -306,26 +312,46 @@ describe('player_char', function ()
end)
- describe('get_quadrant_right', function ()
+ describe('get_horizontal', function ()
+
+ it('should return directions.right when quadrant is down, passing right', function ()
+ pc.quadrant = directions.down
+ assert.are_same(directions.right, pc:get_horizontal(horizontal_dirs.right))
+ end)
- it('should return vector(1, 0) when quadrant is down', function ()
+ it('should return directions.left when quadrant is down, passing left', function ()
pc.quadrant = directions.down
- assert.are_same(vector(1, 0), pc:get_quadrant_right())
+ assert.are_same(directions.left, pc:get_horizontal(horizontal_dirs.left))
+ end)
+
+ it('should return directions.left when quadrant is up, passing right', function ()
+ pc.quadrant = directions.up
+ assert.are_same(directions.left, pc:get_horizontal(horizontal_dirs.right))
end)
- it('should return vector(-1, 0) when quadrant is up', function ()
+ it('should return directions.right when quadrant is down, passing left', function ()
pc.quadrant = directions.up
- assert.are_same(vector(-1, 0), pc:get_quadrant_right())
+ assert.are_same(directions.right, pc:get_horizontal(horizontal_dirs.left))
+ end)
+
+ it('should return directions.up when quadrant is right, , passing right', function ()
+ pc.quadrant = directions.right
+ assert.are_same(directions.up, pc:get_horizontal(horizontal_dirs.right))
end)
- it('should return vector(0, -1) when quadrant is right', function ()
+ it('should return directions.down when quadrant is down, passing left', function ()
pc.quadrant = directions.right
- assert.are_same(vector(0, -1), pc:get_quadrant_right())
+ assert.are_same(directions.down, pc:get_horizontal(horizontal_dirs.left))
end)
- it('should return vector(0, 1) when quadrant is left', function ()
+ it('should return directions.down when quadrant is left, passing right', function ()
pc.quadrant = directions.left
- assert.are_same(vector(0, 1), pc:get_quadrant_right())
+ assert.are_same(directions.down, pc:get_horizontal(horizontal_dirs.right))
+ end)
+
+ it('should return directions.up when quadrant is down, passing left', function ()
+ pc.quadrant = directions.left
+ assert.are_same(directions.up, pc:get_horizontal(horizontal_dirs.left))
end)
end)
@@ -415,14 +441,17 @@ describe('player_char', function ()
setup(function ()
stub(player_char, "enter_motion_state")
+ stub(player_char, "check_escape_from_ground")
end)
teardown(function ()
player_char.enter_motion_state:revert()
+ player_char.check_escape_from_ground:revert()
end)
after_each(function ()
player_char.enter_motion_state:clear()
+ player_char.check_escape_from_ground:clear()
end)
it('should set the character\'s position', function ()
@@ -430,52 +459,20 @@ describe('player_char', function ()
assert.are_same(vector(56, 12), pc.position)
end)
- describe('(check_escape_from_ground returns false)', function ()
-
- local check_escape_from_ground_mock
-
- setup(function ()
- check_escape_from_ground_mock = stub(player_char, "check_escape_from_ground", function (self)
- return false
- end)
- end)
-
- teardown(function ()
- check_escape_from_ground_mock:revert()
- end)
-
- it('should call check_escape_from_ground and enter_motion_state(motion_states.falling)', function ()
- pc:spawn_at(vector(56, 12))
-
- -- implementation
- assert.spy(check_escape_from_ground_mock).was_called(1)
- assert.spy(check_escape_from_ground_mock).was_called_with(match.ref(pc))
- end)
+ it('should call enter_motion_state(motion_states.falling)', function ()
+ pc:spawn_at(vector(56, 12))
+ -- implementation
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.falling)
end)
- describe('(check_escape_from_ground returns true)', function ()
-
- local check_escape_from_ground_mock
-
- setup(function ()
- check_escape_from_ground_mock = stub(player_char, "check_escape_from_ground", function (self)
- return true
- end)
- end)
-
- teardown(function ()
- check_escape_from_ground_mock:revert()
- end)
-
- it('should call check_escape_from_ground', function ()
- pc:spawn_at(vector(56, 12))
-
- -- implementation
- assert.spy(check_escape_from_ground_mock).was_called(1)
- assert.spy(check_escape_from_ground_mock).was_called_with(match.ref(pc))
- end)
+ it('should call check_escape_from_ground', function ()
+ pc:spawn_at(vector(56, 12))
+ -- implementation
+ assert.spy(player_char.check_escape_from_ground).was_called(1)
+ assert.spy(player_char.check_escape_from_ground).was_called_with(match.ref(pc))
end)
end)
@@ -633,8 +630,119 @@ describe('player_char', function ()
end)
+ describe('set_continuous_sprite_angle', function ()
+
+ setup(function ()
+ stub(player_char, "update_sprite_angle_parameters")
+ end)
+
+ teardown(function ()
+ player_char.update_sprite_angle_parameters:revert()
+ end)
+
+ -- may be called during init(), so prefer before_each
+ before_each(function ()
+ player_char.update_sprite_angle_parameters:clear()
+ end)
+
+ it('should set continuous sprite angle', function ()
+ pc.continuous_sprite_angle = 0
+
+ pc:set_continuous_sprite_angle(0.25)
+
+ assert.are_equal(0.25, pc.continuous_sprite_angle)
+ end)
+
+ it('should call update_sprite_angle_parameters', function ()
+ pc:set_continuous_sprite_angle(0.25)
+
+ assert.spy(pc.update_sprite_angle_parameters).was_called(1)
+ assert.spy(pc.update_sprite_angle_parameters).was_called_with(match.ref(pc))
+ end)
+
+ end)
+
+ describe('update_sprite_angle_parameters', function ()
+
+ it('(pc brake_start, facing right) should always set is_sprite_diagonal to false and sprite_angle to 0 even if angle is closer to diagonal direction', function ()
+ pc.anim_spr.current_anim_key = "brake_start"
+ pc.continuous_sprite_angle = 0.875 -- diagonal
+ pc.orientation = horizontal_dirs.right
+
+ pc:update_sprite_angle_parameters()
+
+ -- braking, so reset all angles
+ assert.is_false(pc.is_sprite_diagonal)
+ assert.are_equal(0, pc.sprite_angle)
+ end)
+
+ it('(pc idle, facing right) should round to closest cardinal direction and therefore not diagonal even if angle is closer to diagonal direction', function ()
+ pc.anim_spr.current_anim_key = "idle"
+ pc.continuous_sprite_angle = 0.125 -- exact diagonal to demonstrate (note that we round up -> 0.25 not 0)
+ pc.orientation = horizontal_dirs.right
+
+ pc:update_sprite_angle_parameters()
+
+ assert.is_false(pc.is_sprite_diagonal)
+ assert.are_equal(0.25, pc.sprite_angle)
+ end)
+
+ it('(pc walking, facing right) should set is_sprite_diagonal to false if angle is closer to cardinal direction, and sprite_angle to this cardinal angle', function ()
+ pc.anim_spr.current_anim_key = "walk"
+ pc.continuous_sprite_angle = 0.25 + 0.0624 -- closer to 0.5 than 0.375
+ pc.orientation = horizontal_dirs.right
+
+ pc:update_sprite_angle_parameters()
+
+ assert.is_false(pc.is_sprite_diagonal)
+ assert.are_equal(0.25, pc.sprite_angle)
+ end)
+
+ it('(pc walking, facing right) should set is_sprite_diagonal to true if angle is closer to diagonal direction, and sprite_angle to this diagonal angle MINUS 45 deg (0.125 pico8 angle unit)', function ()
+ pc.anim_spr.current_anim_key = "walk"
+ pc.continuous_sprite_angle = 0.875 + 0.0624 -- closer to 0.875 than 1 (0 modulo 1)
+ pc.orientation = horizontal_dirs.right
+
+ pc:update_sprite_angle_parameters()
+
+ assert.is_true(pc.is_sprite_diagonal)
+ -- sprite is already rotated by 45 (in pico8 unit, 0.125), so the additional angle is only 0.875 - 0.125 = 0.75
+ assert.are_equal(0.75, pc.sprite_angle)
+ end)
+
+ it('(pc running, facing left) should set is_sprite_diagonal to true if angle is closer to diagonal direction, and sprite_angle to this diagonal angle PLUS 45 deg (0.125 pico8 angle unit) = 1 -> 0 with modulo', function ()
+ pc.anim_spr.current_anim_key = "run"
+ pc.continuous_sprite_angle = 0.875 + 0.0624 -- closer to 0.875 than 1 (0 modulo 1)
+ pc.orientation = horizontal_dirs.left
+
+ pc:update_sprite_angle_parameters()
+
+ assert.is_true(pc.is_sprite_diagonal)
+ -- sprite is already rotated by -45 due to flip x (in pico8 unit, -0.125), so the additional angle is only 0.875 - (- 0.125) = 1
+ -- since using spr_r90 we decided to modulo 1 early, so although calculation gave 1, stored member is 0
+ assert.are_equal(0, pc.sprite_angle)
+ end)
+
+ end)
+
describe('set_slope_angle_with_quadrant', function ()
+ setup(function ()
+ -- stub is fine too, but since it's a low-level method
+ -- that obviously sets a member, and some utests may still want to test
+ -- the final result, we prefer just spying
+ spy.on(player_char, "set_continuous_sprite_angle")
+ end)
+
+ teardown(function ()
+ player_char.set_continuous_sprite_angle:revert()
+ end)
+
+ -- called on init, so make sure to clear *before* each test
+ before_each(function ()
+ player_char.set_continuous_sprite_angle:clear()
+ end)
+
-- slope angle
it('should set slope_angle to passed angle even if nil', function ()
@@ -651,22 +759,27 @@ describe('player_char', function ()
-- sprite angle
- it('should not set sprite_angle if passed angle is nil', function ()
+ it('should not call set_continuous_sprite_angle if passed angle is nil', function ()
pc.continuous_sprite_angle = 0.25
pc:set_slope_angle_with_quadrant(nil)
- assert.are_equal(0.25, pc.continuous_sprite_angle)
+
+ assert.spy(pc.set_continuous_sprite_angle).was_not_called()
end)
- it('should set sprite_angle to angle if not nil', function ()
+ it('should call set_continuous_sprite_angle with angle if not nil', function ()
pc.continuous_sprite_angle = 0.25
pc:set_slope_angle_with_quadrant(0.75)
- assert.are_equal(0.75, pc.continuous_sprite_angle)
+
+ assert.spy(pc.set_continuous_sprite_angle).was_called(1)
+ assert.spy(pc.set_continuous_sprite_angle).was_called_with(match.ref(pc), 0.75)
end)
- it('should set sprite_angle to 0 when passing force_upward_sprite: true', function ()
+ it('should call set_continuous_sprite_angle with 0 when passing force_upward_sprite: true', function ()
pc.continuous_sprite_angle = 0.25
pc:set_slope_angle_with_quadrant(0.75, true)
- assert.are_equal(0, pc.continuous_sprite_angle)
+
+ assert.spy(pc.set_continuous_sprite_angle).was_called(1)
+ assert.spy(pc.set_continuous_sprite_angle).was_called_with(match.ref(pc), 0)
end)
-- below also tests world.angle_to_quadrant implementation,
@@ -741,6 +854,7 @@ describe('player_char', function ()
stub(player_char, "update_motion")
stub(player_char, "update_anim")
stub(animated_sprite, "update")
+ stub(pfx, "update")
end)
teardown(function ()
@@ -748,6 +862,7 @@ describe('player_char', function ()
player_char.update_motion:revert()
player_char.update_anim:revert()
animated_sprite.update:revert()
+ pfx.update:revert()
end)
after_each(function ()
@@ -755,9 +870,26 @@ describe('player_char', function ()
player_char.update_motion:clear()
player_char.update_anim:clear()
animated_sprite.update:clear()
+ pfx.update:clear()
+ end)
+
+ it('(#debug_collision_mask) should clear debug tile locations from previous frame', function ()
+ pc.debug_mask_global_tile_locations = {"dummy"}
+
+ pc:update()
+
+ assert.are_same({}, pc.debug_mask_global_tile_locations)
+ end)
+
+ it('(#debug_character) should clear debug rays from previous frame', function ()
+ pc.debug_rays = {"dummy"}
+
+ pc:update()
+
+ assert.are_same({}, pc.debug_rays)
end)
- it('should call _handle_input, _update_motion, _update_anim and update animated sprite', function ()
+ it('should call handle_input, update_motion, update_anim, update animated sprite, update smoke pfx', function ()
pc:update()
-- implementation
@@ -769,6 +901,8 @@ describe('player_char', function ()
assert.spy(pc.update_anim).was_called_with(match.ref(pc))
assert.spy(animated_sprite.update).was_called(1)
assert.spy(animated_sprite.update).was_called_with(match.ref(pc.anim_spr))
+ assert.spy(pfx.update).was_called(1)
+ assert.spy(pfx.update).was_called_with(match.ref(pc.smoke_pfx))
end)
end)
@@ -1040,7 +1174,7 @@ describe('player_char', function ()
describe('(when motion mode is platformer)', function ()
- it('should call _update_platformer_motion', function ()
+ it('should call update_platformer_motion', function ()
pc:update_motion()
assert.spy(player_char.update_platformer_motion).was_called(1)
assert.spy(player_char.update_platformer_motion).was_called_with(match.ref(pc))
@@ -1057,8 +1191,8 @@ describe('player_char', function ()
-- bugfix history
-- .
- -- * the test revealed a missing return, as _update_platformer_motion was called but shouldn't
- it('should call _update_debug', function ()
+ -- * the test revealed a missing return, as update_platformer_motion was called but shouldn't
+ it('should call update_debug', function ()
pc:update_motion()
assert.spy(player_char.update_platformer_motion).was_not_called()
assert.spy(player_char.update_debug).was_called(1)
@@ -1351,6 +1485,42 @@ describe('player_char', function ()
end)
+ describe('get_wall_sensor_position_from', function ()
+
+ it('should return the position 2px below center, y floored', function ()
+ -- floor only y
+ -- lower raycast by 8 - 6 = 2
+ assert.are_same(vector(10.9, 12), pc:get_wall_sensor_position_from(vector(10.9, 10.9)))
+ end)
+
+ it('(right wall) should return the position 2px to the right of center (q-down of center), x floored', function ()
+ pc.quadrant = directions.right
+ assert.are_same(vector(12, 10.9), pc:get_wall_sensor_position_from(vector(10.9, 10.9)))
+ end)
+
+ it('(ceiling) should return the position 2px to the up of center (q-down of center), y floored', function ()
+ pc.quadrant = directions.up
+ assert.are_same(vector(10.9, 8), pc:get_wall_sensor_position_from(vector(10.9, 10.9)))
+ end)
+
+ it('(left wall) should return the position 2px to the left of center (q-down of center), x floored', function ()
+ pc.quadrant = directions.left
+ assert.are_same(vector(8, 10.9), pc:get_wall_sensor_position_from(vector(10.9, 10.9)))
+ end)
+
+ it('(non-0 slope) should return the center position, y floored or right', function ()
+ pc.slope_angle = 0.25
+ assert.are_same(vector(10.9, 10), pc:get_wall_sensor_position_from(vector(10.9, 10.9)))
+ end)
+
+ it('(left wall, non-0 slope) should return the center position, x floored', function ()
+ pc.quadrant = directions.left
+ pc.slope_angle = 0.25
+ assert.are_same(vector(10, 10.9), pc:get_wall_sensor_position_from(vector(10.9, 10.9)))
+ end)
+
+ end)
+
describe('compute_closest_ground_query_info', function ()
describe('with full flat tile', function ()
@@ -1418,19 +1588,22 @@ describe('player_char', function ()
assert.are_same(ground_query_info(location(1, 1), -pc_data.max_ground_escape_height, 0), pc:compute_closest_ground_query_info(vector(15, 8 + pc_data.max_ground_escape_height)))
end)
- it('should return ground_query_info(nil, -max_ground_escape_height - 1, 0) if max_ground_escape_height + 2 inside', function ()
- assert.are_same(ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(15, 8 + pc_data.max_ground_escape_height + 2)))
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 inside', function ()
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(15, 8 + pc_data.max_ground_escape_height + 1)))
end)
-- beyond the tile, still detecting it until step up is reached, including the +1 up to detect a wall (step up too high)
- it('should return ground_query_info(nil, - max_ground_escape_height - 1, 0) if max_ground_escape_height below the bottom', function ()
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if max_ground_escape_height below the bottom', function ()
-- we really check 1 extra px above max_ground_escape_height, so even that far from the ground above we still see it as a step too high, not ceiling
- assert.are_same(ground_query_info(nil, - pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height)))
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height)))
end)
- it('should return ground_query_info(nil, -max_ground_escape_height - 1, 0) (clamped) if max_ground_escape_height - 1 below the bottom', function ()
- assert.are_same(ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height - 1)))
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) (clamped) if max_ground_escape_height - 1 below the bottom', function ()
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height - 1)))
end)
-- step up distance reached, character considered in the air
@@ -1455,16 +1628,17 @@ describe('player_char', function ()
assert.are_same(ground_query_info(location(1, 1), 2, 0.25), pc:compute_closest_ground_query_info(vector(6, 12)))
end)
- it('(right wall) should return ground_query_info(location(1, 1), -2, 0.25) if 2 pixels inside the wall', function ()
+ it('(right wall) should return ground_query_info(location(1, 1), -max_ground_escape_height, 0.25) if max_ground_escape_height pixels inside the wall', function ()
pc.quadrant = directions.right
- assert.are_same(ground_query_info(location(1, 1), -2, 0.25), pc:compute_closest_ground_query_info(vector(10, 12)))
+ assert.are_same(ground_query_info(location(1, 1), -pc_data.max_ground_escape_height, 0.25), pc:compute_closest_ground_query_info(vector(8 + pc_data.max_ground_escape_height, 12)))
end)
- it('(right wall) should return ground_query_info(nil, -max_ground_escape_height - 1, 0) if too far inside the wall', function ()
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ it('(right wall) should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if too far inside the wall', function ()
pc.quadrant = directions.right
- assert.are_same(ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(14, 12)))
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(8 + pc_data.max_ground_escape_height + 1, 12)))
end)
-- ceiling
@@ -1481,16 +1655,17 @@ describe('player_char', function ()
assert.are_same(ground_query_info(location(1, 1), 2, 0.5), pc:compute_closest_ground_query_info(vector(12, 18)))
end)
- it('(ceiling) should return ground_query_info(location(1, 1), -2, 0.5) if 2 pixels inside the wall', function ()
+ it('(ceiling) should return ground_query_info(location(1, 1), -max_ground_escape_height, 0.5) if max_ground_escape_height pixels inside the wall', function ()
pc.quadrant = directions.up
- assert.are_same(ground_query_info(location(1, 1), -2, 0.5), pc:compute_closest_ground_query_info(vector(12, 14)))
+ assert.are_same(ground_query_info(location(1, 1), -pc_data.max_ground_escape_height, 0.5), pc:compute_closest_ground_query_info(vector(12, 16 - pc_data.max_ground_escape_height)))
end)
- it('(ceiling) should return ground_query_info(nil, -max_ground_escape_height - 1, 0) if too far inside the wall', function ()
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ it('(ceiling) should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if too far inside the wall', function ()
pc.quadrant = directions.up
- assert.are_same(ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(12, 8)))
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(12, 16 - pc_data.max_ground_escape_height - 1)))
end)
-- left wall
@@ -1507,16 +1682,35 @@ describe('player_char', function ()
assert.are_same(ground_query_info(location(1, 1), 2, 0.75), pc:compute_closest_ground_query_info(vector(18, 12)))
end)
- it('(left wall) should return ground_query_info(location(1, 1), -2, 0.75) if 2 pixels inside the wall', function ()
+ it('(left wall) should return ground_query_info(location(1, 1), -max_ground_escape_height, 0.75) if max_ground_escape_height pixels inside the wall', function ()
pc.quadrant = directions.left
- assert.are_same(ground_query_info(location(1, 1), -2, 0.75), pc:compute_closest_ground_query_info(vector(14, 12)))
+ assert.are_same(ground_query_info(location(1, 1), -pc_data.max_ground_escape_height, 0.75), pc:compute_closest_ground_query_info(vector(16 - pc_data.max_ground_escape_height, 12)))
end)
- it('(left wall) should return ground_query_info(nil, -max_ground_escape_height - 1, 0) if too far inside the wall', function ()
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ it('(left wall) should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if too far inside the wall', function ()
pc.quadrant = directions.left
- assert.are_same(ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(10, 12)))
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(16 - pc_data.max_ground_escape_height - 1, 12)))
+ end)
+
+ -- #debug_collision_mask below
+
+ it('(#debug_collision_mask) should not add debug tile location if no tile found)', function ()
+ -- raycast down on the left of the tile, so miss it
+ pc.quadrant = directions.down
+ pc:compute_closest_ground_query_info(vector(7, 8))
+
+ assert.are_same({}, pc.debug_mask_global_tile_locations)
+ end)
+
+ it('(#debug_collision_mask) should add debug tile location if tile found)', function ()
+ -- raycast down just on the left of the tile, so hit it
+ pc.quadrant = directions.down
+ pc:compute_closest_ground_query_info(vector(8, 8))
+
+ assert.are_same({location(1, 1)}, pc.debug_mask_global_tile_locations)
end)
-- #debug_character below
@@ -1527,8 +1721,8 @@ describe('player_char', function ()
assert.are_same({{
start = vector(12, 8 - (pc_data.max_ground_snap_height + 2)),
- direction = vector(0, 1), -- unit down
- distance = pc_data.max_ground_escape_height + 1,
+ direction_vector = vector(0, 1), -- unit down
+ distance = pc_data.max_ground_snap_height + 1,
hit = false
}}, pc.debug_rays)
end)
@@ -1539,7 +1733,7 @@ describe('player_char', function ()
assert.are_same({{
start = vector(6, 12),
- direction = vector(1, 0), -- unit right
+ direction_vector = vector(1, 0), -- unit right
distance = 2,
hit = true
}}, pc.debug_rays)
@@ -1567,7 +1761,7 @@ describe('player_char', function ()
assert.are_same(ground_query_info(location(1, 1), 0, 0), pc:compute_closest_ground_query_info(vector(12, 8)))
end)
- -- just below the top by up to 1px (still recognize ground to allow step up on low one-way slopes)
+ -- just below the top by up to 1px
it('should return ground_query_info(location(1, 1), -0.0625, 0) if 0.0625 inside the top-left pixel', function ()
assert.are_same(ground_query_info(location(1, 1), -0.0625, 0), pc:compute_closest_ground_query_info(vector(8, 8 + 0.0625)))
@@ -1577,17 +1771,18 @@ describe('player_char', function ()
assert.are_same(ground_query_info(location(1, 1), -1, 0), pc:compute_closest_ground_query_info(vector(12, 8 + 1)))
end)
- -- below the top by more than 1px (ignoring it)
+ -- below the top by more than 1px (new convention is to still detect one-way to allow big step fall in airborne physics)
- it('should return ground_query_info(nil, max_ground_snap_height + 1, nil) if 1.1 (<= max_ground_escape_height) inside vertically', function ()
- assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(12, 8 + 1.1)))
+ it('should return ground_query_info(nil, max_ground_snap_height + 1, nil) if 1.25 (<= max_ground_escape_height) inside vertically', function ()
+ -- used to be -1.1, but -1.25 avoids floating imprecisions
+ assert.are_same(ground_query_info(location(1, 1), -1.25, 0), pc:compute_closest_ground_query_info(vector(12, 8 + 1.25)))
end)
it('should return ground_query_info(nil, max_ground_snap_height + 1, nil) if 3 (<= max_ground_escape_height) inside vertically', function ()
- assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(12, 8 + 3)))
+ assert.are_same(ground_query_info(location(1, 1), -3, 0), pc:compute_closest_ground_query_info(vector(12, 8 + 3)))
end)
- -- below by more than step up distance (still ignoring it)
+ -- below by more than step up distance (ignoring it like normal ground)
it('should return ground_query_info(nil, max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 below the bottom', function ()
assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height + 1)))
@@ -1631,8 +1826,9 @@ describe('player_char', function ()
-- the current tile because current column is just at max_ground_escape_height,
-- only to land inside the tile above
- it('should return ground_query_info(nil, -max_ground_escape_height - 1, 0) if max_ground_escape_height + 1 inside, including max_ground_escape_height in current tile', function ()
- assert.are_same(ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(4, 8 + pc_data.max_ground_escape_height)))
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 inside, including max_ground_escape_height in current tile', function ()
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(4, 8 + pc_data.max_ground_escape_height)))
end)
end)
@@ -1711,18 +1907,21 @@ describe('player_char', function ()
-- beyond the tile, still detecting it until step up is reached, including the +1 up to detect a wall (step up too high)
- it('should return ground_query_info(nil, -max_ground_escape_height - 1, 0) if max_ground_escape_height - 1 below the bottom', function ()
- assert.are_same(ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height - 1)))
+ -- new convention is returning tile when deep inside ground
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if max_ground_escape_height - 1 below the bottom', function ()
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height - 1)))
end)
- it('should return ground_query_info(nil, -max_ground_escape_height - 1, 0) if max_ground_escape_height below the bottom', function ()
- assert.are_same(ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height)))
+ -- new convention is returning tile when deep inside ground
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if max_ground_escape_height below the bottom', function ()
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height)))
end)
-- step up distance reached, character considered in the air
- it('should return ground_query_info(nil, max_ground_snap_height + 1, nil) if max_ground_snap_height + 1 below the bottom', function ()
- assert.are_same(ground_query_info(nil, pc_data.max_ground_escape_height + 1, nil), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height + 1)))
+ -- new convention is returning tile when deep inside ground
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 below the bottom', function ()
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(15, 16 + pc_data.max_ground_escape_height + 1)))
end)
end)
@@ -1802,8 +2001,9 @@ describe('player_char', function ()
-- step up distance reached, character considered in the air
- it('should return ground_query_info(nil, -max_ground_escape_height - 1, 0) if max_ground_escape_height + 1 below the top of column 0 but only max_ground_snap_height below the bottom of column 0 (of the tile)', function ()
- assert.are_same(ground_query_info(nil, -pc_data.max_ground_escape_height - 1, 0), pc:compute_closest_ground_query_info(vector(8, 15 + pc_data.max_ground_escape_height + 1)))
+ -- new convention is returning tile when deep inside ground
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 below the top of column 0 but only max_ground_snap_height below the bottom of column 0 (of the tile)', function ()
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(8, 15 + pc_data.max_ground_escape_height + 1)))
end)
end)
@@ -1960,48 +2160,59 @@ describe('player_char', function ()
end)
- describe('with bottom/side loop tile', function ()
+ describe('with bottom/side loop tile + unrelated tile that ignores active_loop_layer', function ()
before_each(function ()
-- place loop tiles, but remember the loop areas give them meaning
- mock_mset(0, 0, tile_repr.visual_loop_bottomleft)
- mock_mset(1, 0, tile_repr.visual_loop_bottomright)
+ mock_mset(1, 0, tile_repr.visual_loop_bottomleft)
+ mock_mset(2, 0, tile_repr.visual_loop_bottomright)
+
+ -- real case where I had a slope just at the left limit of the exit area,
+ -- but inside, and collision on it was ignored until I added ignore_loop_layer flag
+ mock_mset(0, 0, tile_repr.desc_slope_4px_last_id_loop_variant)
-- customize loop areas locally. We are redefining a table so that won't affect
-- the original data table in stage_data.lua. To simplify we don't redefine everything,
-- but if we need to for the tests we'll just add the missing members
flow.curr_state.curr_stage_data = {
-- a bit tight honestly because I placed to corners too close to each other, but
- -- can get away with narrow rectangles; as long as the trigger corners are not at
+ -- can get away with narrow rectangles; as long as the trigger (top) corners are not at
-- the bottom
- loop_exit_areas = {location_rect(0, -3, 0, 0)},
- loop_entrance_areas = {location_rect(1, -3, 1, 0)}
+ loop_exit_areas = {location_rect(0, -3, 1, 0)},
+ loop_entrance_areas = {location_rect(2, -3, 2, 0)}
}
end)
it('(entrance active) position on exit should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) as if there were nothing', function ()
pc.active_loop_layer = 1
-- interface
- assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(4, 4)))
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(8 + 4, 4)))
end)
it('(entrance active) position on entrance should return actual ground_query_info() as entrance is solid', function ()
pc.active_loop_layer = 1
-- interface
- assert.are_same(ground_query_info(location(1, 0), -2, atan2(8, -5)), pc:compute_closest_ground_query_info(vector(12, 4)))
+ assert.are_same(ground_query_info(location(2, 0), -2, atan2(8, -5)), pc:compute_closest_ground_query_info(vector(16 + 4, 4)))
end)
it('(exit active) position on exit should return actual ground_query_info() as exit is solid', function ()
pc.active_loop_layer = 2
-- interface
-- slight dissymetry due to pixel coord being considered at the top left... so we are 2px inside the step at 3, not 4
- assert.are_same(ground_query_info(location(0, 0), -2, atan2(8, 5)), pc:compute_closest_ground_query_info(vector(3, 4)))
+ assert.are_same(ground_query_info(location(1, 0), -2, atan2(8, 5)), pc:compute_closest_ground_query_info(vector(8 + 3, 4)))
end)
it('(exit active) position on entrance should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) as if there were nothing', function ()
pc.active_loop_layer = 2
-- interface
- assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(12, 4)))
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(16 + 4, 4)))
+ end)
+
+ it('(even with entrance active ie exit inactive) position on ignore_loop_layer tile inside exit area should return actual ground_query_info()', function ()
+ pc.active_loop_layer = 1
+ -- interface
+ -- at x = 3, column is detected, which has height 2, and y = 4 @ tile (-1, 0), so dy = 8 - 4 - 2 = 2
+ assert.are_same(ground_query_info(location(0, 0), 2, atan2(8, 4)), pc:compute_closest_ground_query_info(vector(3, 4)))
end)
end)
@@ -2009,7 +2220,7 @@ describe('player_char', function ()
describe('with ramp tile', function ()
before_each(function ()
- mock_mset(0, 0, visual.launch_ramp_last_tile_id)
+ mock_mset(0, 0, visual_ingame_data.launch_ramp_last_tile_id)
end)
it('(not ignoring ramp) position on ramp should return actual ground_query_info() as it would be detected', function ()
@@ -2018,10 +2229,10 @@ describe('player_char', function ()
assert.are_same(ground_query_info(location(0, 0), 0, atan2(8, -5)), pc:compute_closest_ground_query_info(vector(4, 2)))
end)
- it('(ignoring ramp) position below ramp by more than 1px should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) because it is one-way', function ()
+ it('(ignoring ramp) position below ramp by more than 1px should STILL return ground_query_info() with non-nil tile location because we changed the one-way convention to allow snap up as usual', function ()
pc.ignore_launch_ramp_timer = 0
-- same shape as tile_repr.visual_loop_bottomright, so expect same signed distance
- assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ground_query_info(vector(4, 4)))
+ assert.are_same(ground_query_info(location(0, 0), -2, atan2(8, -5)), pc:compute_closest_ground_query_info(vector(4, 4)))
end)
it('(ignoring ramp) position on entrance should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) as if there were nothing', function ()
@@ -2031,4020 +2242,3326 @@ describe('player_char', function ()
end)
+ describe('with last part of descending slope every 4px (to test land_on_empty_qcolumn)', function ()
+
+ before_each(function ()
+ -- create a descending slope from (0, 7) to (3, 7) px then 4 ghost pixels of
+ -- ground that we can land on thx to land_on_empty_qcolumn
+ mock_mset(0, 0, tile_repr.desc_slope_2px_last_id)
+ end)
+
+ it('should return ground_query_info(location(0, 0), 0, atan2(8, 2)) if right sensors are 0 above empty column 4', function ()
+ assert.are_same(ground_query_info(location(0, 0), 0, atan2(8, 2)), pc:compute_closest_ground_query_info(vector(4, 8)))
+ end)
+
+ end)
+
end)
- describe('check_escape_from_ground', function ()
+ describe('compute_closest_ceiling_query_info', function ()
setup(function ()
- spy.on(player_char, "set_slope_angle_with_quadrant") -- spy not stub in case the resulting slope_angle/quadrant matters
- -- trigger check inside set_ground_tile_location will fail as it needs context
- -- (tile_test_data + mset), so we prefer stubbing as we don't check ground_tile_location directly
- spy.on(player_char, "set_ground_tile_location")
- spy.on(player_char, "enter_motion_state")
+ stub(player_char, "get_full_height", function ()
+ return 16
+ end)
end)
teardown(function ()
- player_char.set_slope_angle_with_quadrant:revert()
- player_char.set_ground_tile_location:revert()
- player_char.enter_motion_state:revert()
+ player_char.get_full_height:revert()
end)
- after_each(function ()
- player_char.set_slope_angle_with_quadrant:clear()
- player_char.set_ground_tile_location:clear()
- player_char.enter_motion_state:clear()
+ describe('no tiles)', function ()
+
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) everywhere', function ()
+ assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 5)))
+ end)
+
end)
- describe('with full flat tile', function ()
+ describe('(1 full tile)', function ()
before_each(function ()
- -- create a full tile at (1, 1), i.e. (8, 8) to (15, 15) px
- mock_mset(1, 1, tile_repr.full_tile_id)
+ -- .#
+ mock_mset(1, 0, tile_repr.full_tile_id) -- full tile (act like a full ceiling if position is at bottom)
end)
- it('should reset state vars to airborne convention when character is not touching ground at all, and enter state falling', function ()
- pc:set_bottom_center(vector(12, 6))
- pc:check_escape_from_ground()
+ it('should return ground_query_info(location(1, 0), - character height - 0.1, 0.5) for sensor position just above the bottom-center of the tile', function ()
+ -- max_ground_escape_height is quite big now so we start checking ceiling quite high (8px above ground sensor, which is reused for ceiling check) so make sure to parameterize this test
+ -- currently, 0 - (pc_data.max_ground_escape_height + 1 - pc_data.full_height_standing) = 8
+ -- with 7.999 or less we miss the tile
+ -- -16 should also be parameterized but it's a bit cumbersome, and easy to arrange to match actual result
+ -- (unlike the method params which completely change the result), so we kept it this way
+ -- remember that we are detection ceiling so quadrant is up, and angle is 0.5 (180 deg)
+ assert.are_same(ground_query_info(location(1, 0), -16, 0.5), pc:compute_closest_ceiling_query_info(vector(12, 0 - (pc_data.max_ground_escape_height + 1 - pc_data.full_height_standing))))
+ end)
- -- interface
- assert.are_same({vector(12, 6), nil}, {pc:get_bottom_center(), pc.ground_tile_location})
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position on the left of the tile', function ()
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(7, 8)))
+ end)
- assert.spy(player_char.enter_motion_state).was_called(1)
- assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.falling)
+ -- bugfix history:
+ -- ? i thought that by design, function should return true but realized it was not consistent
+ -- ? actually I was right, since if the character moves inside the 2nd of a diagonal tile pattern,
+ -- it *must* be blocked. when character has a foot on the lower tile, it is considered to be
+ -- in this lower tile
+ it('should return ground_query_info(location(1, 0), -character height, 0.5) for sensor position at the bottom-left of the tile', function ()
+ assert.is_same(ground_query_info(location(1, 0), -16, 0.5), pc:compute_closest_ceiling_query_info(vector(8, 8)))
end)
- it('should do nothing when character is just on top of the ground, update slope to 0 and enter state standing', function ()
- pc:set_bottom_center(vector(12, 8))
- pc:check_escape_from_ground()
+ it('should return ground_query_info(location(1, 0), -character height, 0.5) for sensor position on the bottom-right of the tile', function ()
+ assert.is_same(ground_query_info(location(1, 0), -16, 0.5), pc:compute_closest_ceiling_query_info(vector(15, 8)))
+ end)
- -- interface
- assert.are_same(vector(12, 8), pc:get_bottom_center())
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position on the right of the tile', function ()
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(16, 8)))
+ end)
- assert.spy(player_char.set_ground_tile_location).was_called(1)
- assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0)
+ it('should return ground_query_info(location(1, 0), -1, 0.5) for sensor position below the tile, at character height - 1px', function ()
+ assert.is_same(ground_query_info(location(1, 0), -1, 0.5), pc:compute_closest_ceiling_query_info(vector(12, 8 + 16 - 1)))
+ end)
- assert.spy(player_char.enter_motion_state).was_called(1)
- assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
+ -- bugfix history:
+ -- < i realized that values of full_height_standing < 8 would fail the test
+ -- so i moved the height_distance >= pc_data.full_height_standing check above
+ -- the ground_array_height check (computing height_distance from tile bottom instead of top)
+ -- to pass it in this case too
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position below the tile, at character height', function ()
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(12, 8 + 16)))
end)
- it('should move the character upward just enough to escape ground if character is inside ground, update slope to 0 and enter state standing', function ()
- pc:set_bottom_center(vector(12, 9))
- pc:check_escape_from_ground()
+ end)
- -- interface
- assert.are_same(vector(12, 8), pc:get_bottom_center())
+ describe('(1 half-tile)', function ()
- assert.spy(player_char.set_ground_tile_location).was_called(1)
- assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0)
+ before_each(function ()
+ -- =
+ mock_mset(0, 0, tile_repr.half_tile_id)
+ end)
- assert.spy(player_char.enter_motion_state).was_called(1)
- assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position in the middle of the tile', function ()
+ -- we now start checking ceiling a few pixels q-above character feet
+ -- and ignore reverse full height on same tile as sensor, so slope not detected as ceiling
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 6)))
end)
- it('should move the character q-upward (to the left on right wall) just enough to escape ground if character is inside q-ground, update slope to 0 and enter state standing', function ()
- pc.quadrant = directions.right
- pc:set_bottom_center(vector(9, 12))
- pc:check_escape_from_ground()
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position at the bottom of the tile', function ()
+ -- here we don't detect a ceiling because y = 8 is considered belonging to
+ -- tile j = 1, but we define ignore_reverse = start_tile_loc == curr_tile_loc
+ -- not ignore_reverse = curr_tile_loc == curr_tile_loc
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 8)))
+ end)
- -- interface
- assert.are_same(vector(8, 12), pc:get_bottom_center())
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position 2 px below tile (so that 4px above is inside tile)', function ()
+ -- this test makes sure that we ignore reverse full height for start tile
+ -- *not* sensor tile, which is different when sensor is less than 4px of the neighboring tile
+ -- in iteration direction
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 10)))
+ end)
- assert.spy(player_char.set_ground_tile_location).was_called(1)
- assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0.25)
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for quadrant left, offset sensor position (head) 1 px q-outside tile', function ()
+ pc.quadrant = directions.left
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(-17, 4)))
+ end)
- assert.spy(player_char.enter_motion_state).was_called(1)
- assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
+ it('should return ground_query_info(location(0, 0), 0, 0.25) for quadrant left, offset sensor position (head) just touching left of tile', function ()
+ pc.quadrant = directions.left
+ assert.is_same(ground_query_info(location(0, 0), 0, 0.25), pc:compute_closest_ceiling_query_info(vector(-16, 4)))
end)
- it('should reset state vars to too deep convention when character is too deep inside the ground and enter state falling', function ()
- pc:set_bottom_center(vector(12, 13))
- pc:check_escape_from_ground()
+ it('should return ground_query_info(location(0, 0), - 1, 0.25) for quadrant left, offset sensor position (head) 1 px reverse-q(right)-inside tile', function ()
+ pc.quadrant = directions.left
+ assert.is_same(ground_query_info(location(0, 0), -1, 0.25), pc:compute_closest_ceiling_query_info(vector(-15, 4)))
+ end)
- -- interface
- assert.are_same({vector(12, 13), nil}, {pc:get_bottom_center(), pc.ground_tile_location})
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for quadrant right, when 4 px to the left is outside tile', function ()
+ pc.quadrant = directions.right
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 4)))
+ end)
- -- when nil, we don't use the set callback for ground tile location
- assert.spy(player_char.set_ground_tile_location).was_not_called()
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0)
+ it('should return ground_query_info(location(0, 0), -character height, 0.5) for quadrant right, offset sensor position (head) at the right limit so it still detects the tile', function ()
+ -- this test makes sure that we do *not* ignore reverse full height for initial tile if
+ -- that are full horizontal rectangle (see world.compute_qcolumn_height_at)
+ -- since slope_angle_to_interiors has a bias 0 -> right so onceiling check,
+ -- we check on left which is reverse of tile interior_h
+ -- (if bias was for left, then the test above would check this instead)
+ pc.quadrant = directions.right
+ -- max_ground_escape_height is quite big now so we start checking ceiling quite high (8px above ground sensor, which is reused for ceiling check) so make sure to parameterize this test
+ -- currently, 0 - (pc_data.max_ground_escape_height + 1 - pc_data.full_height_standing) = 8
+ -- with 7.999 or less we miss the tile
+ -- -16 should also be parameterized but it's a bit cumbersome, and easy to arrange to match actual result
+ -- (unlike the method params which completely change the result), so we kept it this way
+ assert.is_same(ground_query_info(location(0, 0), -16, 0.75), pc:compute_closest_ceiling_query_info(vector(0 - (pc_data.max_ground_escape_height + 1 - pc_data.full_height_standing), 4)))
+ end)
- assert.spy(player_char.enter_motion_state).was_called(1)
- assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
+ end)
+
+ describe('(1 ascending slope 45)', function ()
+
+ before_each(function ()
+ -- /
+ mock_mset(0, 0, tile_repr.asc_slope_45_id)
+ end)
+
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position on the left of the tile', function ()
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(0, 7)))
+ end)
+
+ it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position at the bottom-left of the tile', function ()
+ -- we now start checking ceiling a few pixels q-above character feet, so slope not detected as ceiling
+ assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(0, 8)))
end)
end)
- -- note that 45 deg slope is considered quadrant down by world.angle_to_quadrant
- -- therefore our tests will work as on flat ground
- -- otherwise we'd need to adjust the expected get_bottom_center which is affected by quadrant
+ end) -- compute_closest_ceiling_query_info
- describe('with descending slope 45', function ()
+ describe('compute_closest_wall_query_info', function ()
+
+ describe('with full flat tile', function ()
before_each(function ()
- -- create a descending slope at (1, 1), i.e. (8, 8) to (15, 15) px
- mock_mset(1, 1, tile_repr.desc_slope_45_id)
+ -- create a full tile at (1, 1), i.e. (8, 8) to (15, 15) px
+ mock_mset(1, 1, tile_repr.full_tile_id)
end)
- it('should reset state vars to airborne convention when character is not touching ground at all, and return false', function ()
- pc:set_bottom_center(vector(15, 10))
- pc:check_escape_from_ground()
+ -- QUADRANT DOWN (testing walls on left and right)
- -- interface
- assert.are_same(vector(15, 10), pc:get_bottom_center())
+ -- missing tile as too high or too low
- assert.spy(player_char.set_ground_tile_location).was_not_called()
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), nil)
+ it('should return ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil) if just at wall height but slightly up', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(8, 7), horizontal_dirs.right))
end)
- it('should do nothing when character is just on top of the ground, update slope to 1-45/360 and return true', function ()
- pc:set_bottom_center(vector(15, 12))
- pc:check_escape_from_ground()
+ it('should return ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil) if just at wall height but slightly down', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(8, 16), horizontal_dirs.right))
+ end)
- -- interface
- assert.are_same(vector(15, 12), pc:get_bottom_center())
+ -- detect right wall
- assert.spy(player_char.set_ground_tile_location).was_called(1)
- assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 1-45/360)
+ -- far
+
+ -- new convention is to ignore touching walls until we really enter them, on the right too (so symmetrical with left)
+ it('should return ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil) if distant from wall by ceil(pc_data.ground_sensor_extent_x)', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(8 - ceil(pc_data.ground_sensor_extent_x), 12), horizontal_dirs.right))
end)
- it('should move the character upward just enough to escape ground if character is inside ground, update slope to 1-45/360 and return true', function ()
- pc:set_bottom_center(vector(15, 13))
- pc:check_escape_from_ground()
+ -- just within reach
- -- interface
- assert.are_same(vector(15, 12), pc:get_bottom_center())
+ it('should return ground_query_info(location(1, 1), ceil(pc_data.ground_sensor_extent_x) + 0.5, 0.25) if distant from wall by ceil(pc_data.ground_sensor_extent_x) - 0.5', function ()
+ assert.are_same(ground_query_info(location(1, 1), ceil(pc_data.ground_sensor_extent_x) - 0.5, 0.25), pc:compute_closest_wall_query_info(vector(8 - ceil(pc_data.ground_sensor_extent_x) + 0.5, 12), horizontal_dirs.right))
+ end)
- assert.spy(player_char.set_ground_tile_location).was_called(1)
- assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 1-45/360)
+ -- inside, just before other side
+
+ -- when going inside tile, the only limit to detection is going to the other side of the tile (since start_tile_offset_qy = 0)
+ -- ie position reached 8 + 8 = 16. So we're just gonna test hardcoded positions just before and just after
+
+ it('should return ground_query_info(location(1, 1), -7.9, 0.25) if inside tile, just before the end of the tile', function ()
+ assert.are_same(ground_query_info(location(1, 1), -7.9, 0.25), pc:compute_closest_wall_query_info(vector(15.9, 12), horizontal_dirs.right))
end)
- it('should reset state vars to too deep convention when character is too deep inside the ground, and return true', function ()
- pc:set_bottom_center(vector(11, 13))
- pc:check_escape_from_ground()
+ -- other side
- -- interface
- assert.are_same(vector(11, 13), pc:get_bottom_center())
+ it('should return ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil) if just on the other side of the tile', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(16, 12), horizontal_dirs.right))
+ end)
- assert.spy(player_char.set_ground_tile_location).was_not_called()
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0)
+ -- detect left wall
+
+ -- far
+
+ -- with new convention, we're ignoring wall touch (ceil(pc_data.ground_sensor_extent_x) away) on right side too, so now symmetrical
+ it('(checking left wall) should return ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil) if distant from wall by ceil(pc_data.ground_sensor_extent_x))', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(16 + ceil(pc_data.ground_sensor_extent_x), 12), horizontal_dirs.left))
end)
- end)
+ -- just within reach
- end) -- check_escape_from_ground
+ -- related to the note on the test above, to actually detect left wall we need to go slightly closer to it, hence -0.5
+ -- (-0.1 is enough, but it causes float imprecisions making test fail)
+ it('(checking left wall) should return ground_query_info(location(1, 1), ceil(pc_data.ground_sensor_extent_x) - 0.5, 0.75) if distant from wall by ceil(pc_data.ground_sensor_extent_x) - 0.5', function ()
+ assert.are_same(ground_query_info(location(1, 1), ceil(pc_data.ground_sensor_extent_x) - 0.5, 0.75), pc:compute_closest_wall_query_info(vector(16 + ceil(pc_data.ground_sensor_extent_x) - 0.5, 12), horizontal_dirs.left))
+ end)
- describe('enter_motion_state', function ()
+ -- inside, just before other side
- setup(function ()
- spy.on(player_char, "set_slope_angle_with_quadrant") -- spy not stub in case the resulting slope_angle/quadrant matters
- end)
+ -- when going inside tile, the only limit to detection is going to the other side of the tile (since start_tile_offset_qy = 0)
+ -- ie position reached 8 + 8 = 16. So we're just gonna test hardcoded positions just before and just after
- teardown(function ()
- player_char.set_slope_angle_with_quadrant:revert()
- end)
+ -- however, for the same dissymmetry reason as above, we still detect the tile at 8 since it's on the right side of it
+ -- so 8 is considered inside, while 7.9 is considered outside
- after_each(function ()
- player_char.set_slope_angle_with_quadrant:clear()
- end)
+ it('(checking left wall) should return ground_query_info(location(1, 1), -9, 0.75) if inside tile, just before the end of the tile', function ()
+ assert.are_same(ground_query_info(location(1, 1), -8, 0.75), pc:compute_closest_wall_query_info(vector(8, 12), horizontal_dirs.left))
+ end)
- it('should enter passed state: falling, reset ground-specific state vars, no animation change', function ()
- -- character starts standing
- pc:enter_motion_state(motion_states.falling)
+ -- other side
- assert.are_same({
- motion_states.falling,
- 0,
- false
- },
- {
- pc.motion_state,
- pc.ground_speed,
- pc.should_jump
- })
- end)
+ it('(checking left wall) should return ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil) if just on the other side of the tile', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(7.9, 12), horizontal_dirs.left))
+ end)
- it('(standing -> falling) should set enter_motion_state to nil', function ()
- pc.ground_tile_location = location(0, 1)
- pc:enter_motion_state(motion_states.falling)
- assert.is_nil(pc.ground_tile_location)
- end)
- it('(standing -> falling) should call set_slope_angle_with_quadrant(nil)', function ()
- -- character starts standing
- pc:enter_motion_state(motion_states.falling)
+ -- other quadrants (just one test to be safe, since the utests on other compute_closest_..._query_info methods
+ -- have showed that the quadrant rotation system was good already; not testing left and right directions every time either)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), nil)
- end)
+ -- QUADRANT RIGHT
- it('should enter passed state: air_spin, reset ground-specific state vars, play spin animation', function ()
- pc.ground_speed = 10
- pc.should_jump = true
- pc.should_play_spring_jump = true
- pc.brake_anim_phase = 1
+ it('(quadrant right, check q-left, so raycast down) should return ground_query_info(location(1, 1), 2, 0) if 2px from the wall below', function ()
+ pc.quadrant = directions.right
- -- character starts standing
- pc:enter_motion_state(motion_states.air_spin)
+ assert.are_same(ground_query_info(location(1, 1), 2, 0), pc:compute_closest_wall_query_info(vector(12, 8 - 2), horizontal_dirs.left))
+ end)
- assert.are_same({
- motion_states.air_spin,
- 0,
- false,
- false,
- 0,
- },
- {
- pc.motion_state,
- pc.ground_speed,
- pc.should_jump,
- pc.should_play_spring_jump,
- pc.brake_anim_phase,
- })
- end)
+ -- CEILING
- it('(standing -> air_spin) should set enter_motion_state to nil', function ()
- pc.ground_tile_location = location(0, 1)
- pc:enter_motion_state(motion_states.falling)
- assert.is_nil(pc.ground_tile_location)
- end)
+ it('(ceiling, check q-right, so raycast left) should return ground_query_info(location(1, 1), 1, 0.75) if 1px from the wall on the left', function ()
+ pc.quadrant = directions.up
- it('(standing -> air_spin) should call set_slope_angle_with_quadrant(nil)', function ()
- -- character starts standing
- pc:enter_motion_state(motion_states.air_spin)
+ assert.are_same(ground_query_info(location(1, 1), 1, 0.75), pc:compute_closest_wall_query_info(vector(16 + 1, 12), horizontal_dirs.right))
+ end)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), nil, true)
- end)
+ -- QUADRANT LEFT
- -- bugfix history: .
- it('should enter passed state: standing, reset has_jumped_this_frame, can_interrupt_jump and should_play_spring_jump', function ()
- pc.ground_speed = 10
- pc.should_jump = true
- pc.should_play_spring_jump = true
+ it('(left wall, checking q-left, so raycast up) should return ground_query_info(location(1, 1), -2, 0.5) if 2 px inside the wall above', function ()
+ pc.quadrant = directions.left
- pc.motion_state = motion_states.falling
+ assert.are_same(ground_query_info(location(1, 1), -2, 0.5), pc:compute_closest_wall_query_info(vector(12, 16 - 2), horizontal_dirs.left))
+ end)
- pc:enter_motion_state(motion_states.standing)
+ -- no need to test debug methods again
- assert.are_same({
- motion_states.standing,
- false,
- false,
- false
- },
- {
- pc.motion_state,
- pc.has_jumped_this_frame,
- pc.can_interrupt_jump,
- pc.should_play_spring_jump
- })
end)
- it('should enter passed state: rolling, reset has_jumped_this_frame, can_interrupt_jump and should_play_spring_jump', function ()
- pc.ground_speed = 10
- pc.should_jump = true
- pc.should_play_spring_jump = true
- pc.brake_anim_phase = 1
+ -- added to cover wall_check_collider_distance_callback else case
+ -- (detect wall but too far)
+ describe('with partial rectangle vertically', function ()
- pc.motion_state = motion_states.falling
+ before_each(function ()
+ -- this tile is the collision mash for the spring up (left part), which is a partial rectangle in both dirs,
+ -- but we're particularly interested in it not covering the whole width
+ mock_mset(1, 1, tile_repr.flat_high_tile_left_id)
+ end)
- pc:enter_motion_state(motion_states.rolling)
+ it('should return ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil) if tile is detected but q-column is too far (via wall_check_collider_distance_callback)', function ()
+ -- any x is good at long as the front of the wall raycast enters location(1, 1), ie from 5 to 8.9
+ -- below 5 it will still work but not going through wall_check_collider_distance_callback, rather wall_check_no_collider_callback
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(8.9, 12), horizontal_dirs.right))
+ end)
- assert.are_same({
- motion_states.rolling,
- false,
- false,
- false,
- 0,
- },
- {
- pc.motion_state,
- pc.has_jumped_this_frame,
- pc.can_interrupt_jump,
- pc.should_play_spring_jump,
- pc.brake_anim_phase,
- })
end)
- it('(falling -> standing, velocity X = 0 on flat ground) should set ground speed to 0', function ()
- pc.motion_state = motion_states.falling
- pc.velocity.x = 0
- pc.velocity.y = 5
+ -- after that, just check one-way to verify we DON'T detect it as a wall
- pc:enter_motion_state(motion_states.standing)
+ describe('with one-way tile', function ()
- assert.are_equal(0, pc.ground_speed)
- end)
+ before_each(function ()
+ mock_mset(1, 1, tile_repr.oneway_platform_left)
+ end)
- it('(falling -> standing, velocity X = 2 on flat ground) should transfer velocity X completely to ground speed', function ()
- pc.motion_state = motion_states.falling
- pc.velocity.x = 2
- pc.velocity.y = 5
+ -- QUADRANT DOWN
- pc:enter_motion_state(motion_states.standing)
+ -- detect right wall
- assert.are_equal(2, pc.ground_speed)
- end)
+ -- far
- it('(falling -> standing, velocity X = 5 (over max) on flat ground) should transfer velocity X *unclamped* to ground speed', function ()
- pc.motion_state = motion_states.falling
- pc.velocity.x = pc_data.max_running_ground_speed + 2
- pc.velocity.y = 5
+ it('should return ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil) if distant from wall by ceil(pc_data.ground_sensor_extent_x) + 1)', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(8 - ceil(pc_data.ground_sensor_extent_x) - 1, 12), horizontal_dirs.right))
+ end)
- pc:enter_motion_state(motion_states.standing)
+ -- just within reach
- assert.are_equal(pc_data.max_running_ground_speed + 2, pc.ground_speed)
- end)
+ it('should return NO WALL INFO even if distant from wall by ceil(pc_data.ground_sensor_extent_x)', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(8 - ceil(pc_data.ground_sensor_extent_x), 12), horizontal_dirs.right))
+ end)
- it('(falling -> standing, velocity (sqrt(3)/2, 0.5) tangent to slope 30 deg desc) should transfer velocity norm (1) completely to ground speed', function ()
- pc.motion_state = motion_states.falling
- pc.velocity.x = sqrt(3)/2
- pc.velocity.y = 0.5
- pc.slope_angle = 1-1/12 -- 30 deg/360 deg
+ -- inside, just before other side
- pc:enter_motion_state(motion_states.standing)
+ it('should return NO WALL INFO even if inside tile, just before the end of the tile', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(15.9, 12), horizontal_dirs.right))
+ end)
- -- should be OK in PICO-8, but with floating precision we need almost
- -- (angle of -1/12 was fine, but 1-1/12 offsets a little)
- assert.is_true(almost_eq_with_message(1, pc.ground_speed))
- end)
+ -- other side
- it('(falling -> standing, velocity (-4, 4) orthogonally to slope 45 deg desc) should set ground speed to 0', function ()
- pc.motion_state = motion_states.falling
- pc.velocity.x = -4
- pc.velocity.y = 4
- pc.slope_angle = 1-0.125 -- 45 deg/360 deg
+ it('should return ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil) if just on the other side of the tile', function ()
+ assert.are_same(ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil), pc:compute_closest_wall_query_info(vector(16, 12), horizontal_dirs.right))
+ end)
- pc:enter_motion_state(motion_states.standing)
+ -- no need to test left wall
- assert.is_true(almost_eq_with_message(0, pc.ground_speed))
- end)
+ -- QUADRANT RIGHT
- it('(falling -> standing, velocity (-4, 5) on slope 45 deg desc) should transfer just the tangent velocity (1/sqrt(2)) to ground speed', function ()
- pc.motion_state = motion_states.falling
- pc.velocity.x = -4
- pc.velocity.y = 5
- pc.slope_angle = 1-0.125 -- -45 deg/360 deg
+ -- this is the only other interesting test with one-way tile: it shows that if we are running on a wall and hitting
+ -- a one-way platform in the absolute down direction, we can indeed land on it (although it doesn't really happen in pico island, it's logical)
- pc:enter_motion_state(motion_states.standing)
+ it('(quadrant right, check q-left, so raycast down) should actually detect the one-way platform when 2px from the one-way platform below', function ()
+ pc.quadrant = directions.right
+
+ assert.are_same(ground_query_info(location(1, 1), 2, 0), pc:compute_closest_wall_query_info(vector(12, 8 - 2), horizontal_dirs.left))
+ end)
- assert.is_true(almost_eq_with_message(1/sqrt(2), pc.ground_speed))
end)
- it('should adjust center position down when becoming compact', function ()
- pc.position = vector(10, 20)
+ -- wall is based on ground check, so no need to check all the other cases like 2 full flat tiles
+ -- although in theory we *should* since loop slope tiles ended up being the main cause of issues
+ -- with wall check, forcing us to raise the wall sensor position, it was much easier to catch
+ -- during real ingame playtesting
- -- character starts standing
- pc:enter_motion_state(motion_states.air_spin)
+ end)
- assert.are_equal(20 + pc_data.center_height_standing - pc_data.center_height_compact, pc.position.y)
- end)
+ describe('check_escape_from_ground', function ()
- it('should adjust center position up when standing up', function ()
- pc.motion_state = motion_states.air_spin
- pc.position = vector(10, 20)
+ setup(function ()
+ spy.on(player_char, "set_slope_angle_with_quadrant") -- spy not stub in case the resulting slope_angle/quadrant matters
+ -- trigger check inside set_ground_tile_location will fail as it needs context
+ -- (tile_test_data + mset), so we prefer stubbing as we don't check ground_tile_location directly
+ spy.on(player_char, "set_ground_tile_location")
+ spy.on(player_char, "enter_motion_state")
+ end)
- -- character starts standing
- pc:enter_motion_state(motion_states.standing)
+ teardown(function ()
+ player_char.set_slope_angle_with_quadrant:revert()
+ player_char.set_ground_tile_location:revert()
+ player_char.enter_motion_state:revert()
+ end)
- assert.are_equal(20 - pc_data.center_height_standing + pc_data.center_height_compact, pc.position.y)
+ after_each(function ()
+ player_char.set_slope_angle_with_quadrant:clear()
+ player_char.set_ground_tile_location:clear()
+ player_char.enter_motion_state:clear()
end)
- it('should adjust center position qdown = left when becoming compact on left wall', function ()
- pc.position = vector(10, 20)
- pc.quadrant = directions.left
+ describe('with full flat tile', function ()
- -- character starts standing
- pc:enter_motion_state(motion_states.air_spin)
+ before_each(function ()
+ -- create a full tile at (1, 1), i.e. (8, 8) to (15, 15) px
+ mock_mset(1, 1, tile_repr.full_tile_id)
+ end)
- assert.are_same(vector(10 - pc_data.center_height_standing + pc_data.center_height_compact, 20), pc.position)
- end)
+ it('should reset do nothing when no ground is detected', function ()
+ pc:set_bottom_center(vector(12, 6))
+ pc:check_escape_from_ground()
- it('should adjust center position qup = up when landing and standing up on floor', function ()
- pc.motion_state = motion_states.air_spin
- pc.position = vector(10, 20)
+ -- interface
+ assert.are_same({vector(12, 6), nil}, {pc:get_bottom_center(), pc.ground_tile_location})
+ end)
- -- character starts standing
- pc:enter_motion_state(motion_states.standing)
+ it('(when character is just on top of the ground, already grounded) keep position, set ground tile location, slope angle to 0', function ()
+ pc:set_bottom_center(vector(12, 8))
+ pc:check_escape_from_ground()
- assert.are_same(vector(10, 20 - pc_data.center_height_standing + pc_data.center_height_compact), pc.position)
- end)
+ -- interface
+ assert.are_same(vector(12, 8), pc:get_bottom_center())
- end)
+ assert.spy(player_char.set_ground_tile_location).was_called(1)
+ assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0)
+ end)
- describe('update_collision_timer', function ()
+ it('should move the character upward just enough to escape ground if character is inside ground, update slope to 0', function ()
+ pc:set_bottom_center(vector(12, 9))
+ pc:check_escape_from_ground()
- it('should do nothing when timer is 0 (or negative)', function ()
- pc.ignore_launch_ramp_timer = 0
+ -- interface
+ assert.are_same(vector(12, 8), pc:get_bottom_center())
- pc:update_collision_timer()
+ assert.spy(player_char.set_ground_tile_location).was_called(1)
+ assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0)
+ end)
- assert.are_equal(0, pc.ignore_launch_ramp_timer)
- end)
+ it('should move the character q-upward (to the left on right wall) just enough to escape ground if character is inside q-ground, update slope to 0', function ()
+ pc.quadrant = directions.right
+ pc:set_bottom_center(vector(9, 12))
+ pc:check_escape_from_ground()
- it('should decrease timer by 1/60 s when timer is positive', function ()
- pc.ignore_launch_ramp_timer = 1
+ -- interface
+ assert.are_same(vector(8, 12), pc:get_bottom_center())
- pc:update_collision_timer()
+ assert.spy(player_char.set_ground_tile_location).was_called(1)
+ assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0.25)
+ end)
- assert.are_equal(0, pc.ignore_launch_ramp_timer)
- end)
+ it('should do nothing when character is too deep inside the ground', function ()
+ pc:set_bottom_center(vector(12, 8 + pc_data.max_ground_escape_height + 1))
+ pc:check_escape_from_ground()
- end)
+ -- interface
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ assert.are_same({vector(12, 8 + pc_data.max_ground_escape_height + 1), nil}, {pc:get_bottom_center(), pc.ground_tile_location})
- describe('update_platformer_motion', function ()
+ -- convention v3 is doing nothing to escape when deep inside ground
+ assert.spy(player_char.set_ground_tile_location).was_not_called()
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_not_called()
+ end)
- setup(function ()
- stub(player_char, "check_roll_start")
- stub(player_char, "check_roll_end")
- stub(player_char, "check_spring")
- stub(player_char, "check_launch_ramp")
- stub(player_char, "check_emerald")
- stub(player_char, "check_loop_external_triggers")
- end)
+ it('(when character detects ground, already grounded) not enter state', function ()
+ pc.motion_state = motion_states.rolling
+ pc:set_bottom_center(vector(12, 8)) -- touch ground
- teardown(function ()
- player_char.check_roll_start:revert()
- player_char.check_roll_end:revert()
- player_char.check_spring:revert()
- player_char.check_launch_ramp:revert()
- player_char.check_emerald:revert()
- player_char.check_loop_external_triggers:revert()
- end)
+ pc:check_escape_from_ground()
- after_each(function ()
- player_char.check_roll_start:clear()
- player_char.check_roll_end:clear()
- player_char.check_spring:clear()
- player_char.check_launch_ramp:clear()
- player_char.check_emerald:clear()
- player_char.check_loop_external_triggers:clear()
- end)
+ assert.spy(player_char.enter_motion_state).was_not_called()
+ end)
+
+ it('(when character detects ground, not grounded) enter state standing', function ()
+ pc.motion_state = motion_states.falling
+ pc:set_bottom_center(vector(12, 8)) -- touch ground
- it('(#debug_character) should clear debug rays from previous frame', function ()
- pc.debug_rays = {"dummy"}
+ pc:check_escape_from_ground()
- pc:update_platformer_motion()
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
+ end)
- assert.are_same({}, pc.debug_rays)
end)
- describe('(check_jump stubbed)', function ()
+ -- note that 45 deg slope is considered quadrant down by world.angle_to_quadrant
+ -- therefore our tests will work as on flat ground
+ -- otherwise we'd need to adjust the expected get_bottom_center which is affected by quadrant
- setup(function ()
- stub(player_char, "check_jump")
- end)
+ describe('with descending slope 45', function ()
- teardown(function ()
- player_char.check_jump:revert()
+ before_each(function ()
+ -- create a descending slope at (1, 1), i.e. (8, 8) to (15, 15) px
+ mock_mset(1, 1, tile_repr.desc_slope_45_id)
end)
- after_each(function ()
- player_char.check_jump:clear()
- end)
+ it('should not call set_ground_tile_location nor set_slope_angle_with_quadrant', function ()
+ pc:set_bottom_center(vector(15, 10))
+ pc:check_escape_from_ground()
- it('(when motion state is standing on ground) should call check_jump', function ()
- pc.motion_state = motion_states.standing
- pc:update_platformer_motion()
- assert.spy(player_char.check_jump).was_called(1)
- assert.spy(player_char.check_jump).was_called_with(match.ref(pc))
- end)
+ -- interface
+ assert.are_same(vector(15, 10), pc:get_bottom_center())
- it('(when motion state is rolling on ground) should call check_jump', function ()
- pc.motion_state = motion_states.rolling
- pc:update_platformer_motion()
- assert.spy(player_char.check_jump).was_called(1)
- assert.spy(player_char.check_jump).was_called_with(match.ref(pc))
+ assert.spy(player_char.set_ground_tile_location).was_not_called()
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_not_called()
end)
- it('(when motion state is airborne) should call check_jump', function ()
- pc.motion_state = motion_states.falling -- or any airborne state
- pc:update_platformer_motion()
- assert.spy(player_char.check_jump).was_not_called()
- end)
+ it('should do nothing when character is just on top of the ground, update slope to 1-45/360', function ()
+ pc:set_bottom_center(vector(15, 12))
+ pc:check_escape_from_ground()
- it('should call check_spring (after motion)', function ()
- pc.motion_state = motion_states.falling -- or any airborne state
- pc:update_platformer_motion()
- assert.spy(player_char.check_spring).was_called()
- assert.spy(player_char.check_spring).was_called_with(match.ref(pc))
- end)
+ -- interface
+ assert.are_same(vector(15, 12), pc:get_bottom_center())
- it('should call check_launch_ramp (after motion)', function ()
- pc.motion_state = motion_states.falling -- or any airborne state
- pc:update_platformer_motion()
- assert.spy(player_char.check_launch_ramp).was_called()
- assert.spy(player_char.check_launch_ramp).was_called_with(match.ref(pc))
+ assert.spy(player_char.set_ground_tile_location).was_called(1)
+ assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 1-45/360)
end)
- it('should call check_emerald (after motion)', function ()
- pc.motion_state = motion_states.falling -- or any airborne state
- pc:update_platformer_motion()
- assert.spy(player_char.check_emerald).was_called()
- assert.spy(player_char.check_emerald).was_called_with(match.ref(pc))
- end)
-
- it('should call check_loop_external_triggers (after motion)', function ()
- pc.motion_state = motion_states.falling -- or any airborne state
- pc:update_platformer_motion()
- assert.spy(player_char.check_loop_external_triggers).was_called()
- assert.spy(player_char.check_loop_external_triggers).was_called_with(match.ref(pc))
- end)
+ it('should move the character upward just enough to escape ground if character is inside ground, update slope to 1-45/360', function ()
+ pc:set_bottom_center(vector(15, 13))
+ pc:check_escape_from_ground()
- end)
+ -- interface
+ assert.are_same(vector(15, 12), pc:get_bottom_center())
- describe('(update_platformer_motion_grounded sets motion state to air_spin)', function ()
+ assert.spy(player_char.set_ground_tile_location).was_called(1)
+ assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(1, 1))
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 1-45/360)
+ end)
- local update_platformer_motion_grounded_mock
- local update_platformer_motion_airborne_stub
+ it('do nothing when character is too deep inside the ground', function ()
+ pc:set_bottom_center(vector(15, 12 + pc_data.max_ground_escape_height + 1))
+ pc:check_escape_from_ground()
- setup(function ()
- -- mock the worst case possible for update_platformer_motion_grounded,
- -- changing the state to air_spin to make sure the airborne branch is not entered afterward (else instead of 2 if blocks)
- update_platformer_motion_grounded_mock = stub(player_char, "update_platformer_motion_grounded", function (self)
- self.motion_state = motion_states.air_spin
- end)
- update_platformer_motion_airborne_stub = stub(player_char, "update_platformer_motion_airborne")
- end)
+ -- interface
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ assert.are_same(vector(15, 12 + pc_data.max_ground_escape_height + 1), pc:get_bottom_center())
- teardown(function ()
- update_platformer_motion_grounded_mock:revert()
- update_platformer_motion_airborne_stub:revert()
+ -- convention v3 is returning nothing/too far to snap when deep inside ground
+ assert.spy(player_char.set_ground_tile_location).was_not_called()
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_not_called()
end)
- after_each(function ()
- update_platformer_motion_grounded_mock:clear()
- update_platformer_motion_airborne_stub:clear()
- end)
+ end)
- describe('(check_jump does nothing)', function ()
+ end) -- check_escape_from_ground
- local check_jump_stub
+ describe('enter_motion_state', function ()
- setup(function ()
- check_jump_stub = stub(player_char, "check_jump")
- end)
+ setup(function ()
+ spy.on(player_char, "set_slope_angle_with_quadrant") -- spy not stub in case the resulting slope_angle/quadrant matters
+ end)
- teardown(function ()
- check_jump_stub:revert()
- end)
+ teardown(function ()
+ player_char.set_slope_angle_with_quadrant:revert()
+ end)
- after_each(function ()
- check_jump_stub:clear()
- end)
+ after_each(function ()
+ player_char.set_slope_angle_with_quadrant:clear()
+ end)
- describe('(when character is standing)', function ()
+ it('should enter passed state: falling, reset ground-specific state vars, no animation change', function ()
+ -- character starts standing
+ pc:enter_motion_state(motion_states.falling)
- it('should call check_roll_start', function ()
- pc.motion_state = motion_states.standing
+ assert.are_same({
+ motion_states.falling,
+ 0,
+ false
+ },
+ {
+ pc.motion_state,
+ pc.ground_speed,
+ pc.should_jump
+ })
+ end)
- pc:update_platformer_motion()
+ it('(standing -> falling) should set enter_motion_state to nil', function ()
+ pc.ground_tile_location = location(0, 1)
+ pc:enter_motion_state(motion_states.falling)
+ assert.is_nil(pc.ground_tile_location)
+ end)
- assert.spy(player_char.check_roll_start).was_called(1)
- assert.spy(player_char.check_roll_start).was_called_with(match.ref(pc))
- end)
+ it('(standing -> falling) should call set_slope_angle_with_quadrant(nil)', function ()
+ -- character starts standing
+ pc:enter_motion_state(motion_states.falling)
- it('should call update_platformer_motion_grounded', function ()
- pc.motion_state = motion_states.standing
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), nil)
+ end)
- pc:update_platformer_motion()
+ it('should enter passed state: air_spin, reset ground-specific state vars, play spin animation', function ()
+ pc.ground_speed = 10
+ pc.should_jump = true
+ pc.should_play_spring_jump = true
- assert.spy(update_platformer_motion_grounded_mock).was_called(1)
- assert.spy(update_platformer_motion_grounded_mock).was_called_with(match.ref(pc))
- assert.spy(update_platformer_motion_airborne_stub).was_not_called()
- end)
+ -- character starts standing
+ pc:enter_motion_state(motion_states.air_spin)
- end)
+ assert.are_same({
+ motion_states.air_spin,
+ 0,
+ false,
+ false,
+ },
+ {
+ pc.motion_state,
+ pc.ground_speed,
+ pc.should_jump,
+ pc.should_play_spring_jump,
+ })
+ end)
- describe('(when character is rolling)', function ()
+ it('(standing -> air_spin) should set enter_motion_state to nil', function ()
+ pc.ground_tile_location = location(0, 1)
+ pc:enter_motion_state(motion_states.falling)
+ assert.is_nil(pc.ground_tile_location)
+ end)
- it('should call check_roll_start', function ()
- pc.motion_state = motion_states.rolling
+ it('(standing -> air_spin) should call set_slope_angle_with_quadrant(nil)', function ()
+ -- character starts standing
+ pc:enter_motion_state(motion_states.air_spin)
- pc:update_platformer_motion()
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), nil, true)
+ end)
- assert.spy(player_char.check_roll_end).was_called(1)
- assert.spy(player_char.check_roll_end).was_called_with(match.ref(pc))
- end)
+ -- bugfix history: .
+ it('should enter passed state: standing, reset has_jumped_this_frame, can_interrupt_jump and should_play_spring_jump', function ()
+ pc.ground_speed = 10
+ pc.should_jump = true
+ pc.should_play_spring_jump = true
- it('should call update_platformer_motion_grounded', function ()
- pc.motion_state = motion_states.rolling
+ pc.motion_state = motion_states.falling
- pc:update_platformer_motion()
+ pc:enter_motion_state(motion_states.standing)
- assert.spy(update_platformer_motion_grounded_mock).was_called(1)
- assert.spy(update_platformer_motion_grounded_mock).was_called_with(match.ref(pc))
- assert.spy(update_platformer_motion_airborne_stub).was_not_called()
- end)
+ assert.are_same({
+ motion_states.standing,
+ false,
+ false,
+ false
+ },
+ {
+ pc.motion_state,
+ pc.has_jumped_this_frame,
+ pc.can_interrupt_jump,
+ pc.should_play_spring_jump
+ })
+ end)
- end)
+ it('(not grounded, pass rolling) should enter passed state: rolling, reset has_jumped_this_frame, can_interrupt_jump and should_play_spring_jump', function ()
+ pc.ground_speed = 10
+ pc.should_jump = true
+ pc.should_play_spring_jump = true
- describe('(when character is in air_spin)', function ()
+ pc.motion_state = motion_states.falling
- it('should call update_platformer_motion_airborne', function ()
- pc.motion_state = motion_states.air_spin
+ pc:enter_motion_state(motion_states.rolling)
- pc:update_platformer_motion()
+ assert.are_same({
+ motion_states.rolling,
+ false,
+ false,
+ false,
+ },
+ {
+ pc.motion_state,
+ pc.has_jumped_this_frame,
+ pc.can_interrupt_jump,
+ pc.should_play_spring_jump,
+ })
+ end)
- assert.spy(update_platformer_motion_airborne_stub).was_called(1)
- assert.spy(update_platformer_motion_airborne_stub).was_called_with(match.ref(pc))
- assert.spy(update_platformer_motion_grounded_mock).was_not_called()
- end)
+ it('(falling -> standing, velocity X = 0 on flat ground) should set ground speed to 0', function ()
+ pc.motion_state = motion_states.falling
+ pc.velocity.x = 0
+ pc.velocity.y = 5
- end)
+ pc:enter_motion_state(motion_states.standing)
- end)
+ assert.are_equal(0, pc.ground_speed)
+ end)
- describe('(check_jump enters air_spin motion state)', function ()
+ -- note: the tests below check velocity transfer, which used to be computed directly in the method
+ -- since, it has been extracted to compute_velocity_from_ground_speed, but it was easier to keep
+ -- the general checks than doing weird stubs on a simple math function, although it means those utests
+ -- are overlapping the utests for compute_velocity_from_ground_speed
- local check_jump_mock
+ it('(falling -> standing, velocity X = 2 on flat ground) should transfer velocity X completely to ground speed', function ()
+ pc.motion_state = motion_states.falling
+ pc.velocity.x = 2
+ pc.velocity.y = 5
- setup(function ()
- check_jump_mock = stub(player_char, "check_jump", function ()
- pc.motion_state = motion_states.air_spin
- end)
- end)
+ pc:enter_motion_state(motion_states.standing)
- teardown(function ()
- check_jump_mock:revert()
- end)
+ assert.are_equal(2, pc.ground_speed)
+ end)
- after_each(function ()
- check_jump_mock:clear()
- end)
+ it('(falling -> standing, velocity X = 5 (over max) on flat ground) should transfer velocity X *unclamped* to ground speed', function ()
+ pc.motion_state = motion_states.falling
+ pc.velocity.x = pc_data.max_running_ground_speed + 2
+ pc.velocity.y = 5
- describe('(when character is standing first)', function ()
+ pc:enter_motion_state(motion_states.standing)
- it('should not call check_roll_start since check_jump will enter air_spin first', function ()
- pc.motion_state = motion_states.standing
+ assert.are_equal(pc_data.max_running_ground_speed + 2, pc.ground_speed)
+ end)
- pc:update_platformer_motion()
+ it('(falling -> standing, velocity (sqrt(3)/2, 0.5) tangent to slope 30 deg desc) should transfer velocity norm (1) completely to ground speed', function ()
+ pc.motion_state = motion_states.falling
+ pc.velocity.x = sqrt(3)/2
+ pc.velocity.y = 0.5
+ pc.slope_angle = 1-1/12 -- 30 deg/360 deg
- assert.spy(player_char.check_roll_start).was_not_called()
- end)
+ pc:enter_motion_state(motion_states.standing)
- it('should call update_platformer_motion_airborne since check_jump will enter air_spin first', function ()
- pc.motion_state = motion_states.standing
+ -- should be OK in PICO-8, but with floating precision we need almost
+ -- (angle of -1/12 was fine, but 1-1/12 offsets a little)
+ assert.is_true(almost_eq_with_message(1, pc.ground_speed))
+ end)
- pc:update_platformer_motion()
+ it('(falling -> standing, velocity (-4, 4) orthogonally to slope 45 deg desc) should set ground speed to 0', function ()
+ pc.motion_state = motion_states.falling
+ pc.velocity.x = -4
+ pc.velocity.y = 4
+ pc.slope_angle = 1-0.125 -- 45 deg/360 deg
- assert.spy(update_platformer_motion_airborne_stub).was_called(1)
- assert.spy(update_platformer_motion_airborne_stub).was_called_with(match.ref(pc))
- assert.spy(update_platformer_motion_grounded_mock).was_not_called()
- end)
-
- end)
-
- describe('(when character is rolling first)', function ()
-
- it('should not call check_roll_end since check_jump will enter air_spin first', function ()
- pc.motion_state = motion_states.rolling
-
- pc:update_platformer_motion()
-
- assert.spy(player_char.check_roll_end).was_not_called()
- end)
-
- it('should call update_platformer_motion_airborne since check_jump will enter air_spin first', function ()
- pc.motion_state = motion_states.rolling
-
- pc:update_platformer_motion()
-
- assert.spy(update_platformer_motion_airborne_stub).was_called(1)
- assert.spy(update_platformer_motion_airborne_stub).was_called_with(match.ref(pc))
- assert.spy(update_platformer_motion_grounded_mock).was_not_called()
- end)
-
- end)
-
- -- we don't need to test (when character is airborne) since in this context check_jump
- -- always trigger a jump, which is impossible from the air (as double jump is not implemented)
-
- end)
+ pc:enter_motion_state(motion_states.standing)
+ assert.is_true(almost_eq_with_message(0, pc.ground_speed))
end)
- end) -- _update_platformer_motion
+ it('(falling -> standing, velocity (-4, 5) on slope 45 deg desc) should transfer just the tangent velocity (1/sqrt(2)) to ground speed', function ()
+ pc.motion_state = motion_states.falling
+ pc.velocity.x = -4
+ pc.velocity.y = 5
+ pc.slope_angle = 1-0.125 -- -45 deg/360 deg
- describe('check_roll_start', function ()
+ pc:enter_motion_state(motion_states.standing)
- setup(function ()
- stub(player_char, "enter_motion_state")
- stub(player_char, "play_low_priority_sfx")
+ assert.is_true(almost_eq_with_message(1/sqrt(2), pc.ground_speed))
end)
- teardown(function ()
- player_char.enter_motion_state:revert()
- player_char.play_low_priority_sfx:revert()
- end)
+ it('should adjust center position down when becoming compact', function ()
+ pc.position = vector(10, 20)
- after_each(function ()
- player_char.enter_motion_state:clear()
- player_char.play_low_priority_sfx:clear()
- end)
+ -- character starts standing
+ pc:enter_motion_state(motion_states.air_spin)
- before_each(function ()
- -- assumption
- pc.motion_state = motion_states.standing
+ assert.are_equal(20 + pc_data.center_height_standing - pc_data.center_height_compact, pc.position.y)
end)
- it('should not start rolling if input down is pressed but abs ground speed (positive) is not enough', function ()
- pc.ground_speed = pc_data.roll_min_ground_speed - 0.01
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc.move_intention.y = 1
+ it('should adjust center position up when standing up', function ()
+ pc.motion_state = motion_states.air_spin
+ pc.position = vector(10, 20)
- pc:check_roll_start()
+ -- character starts standing
+ pc:enter_motion_state(motion_states.standing)
- assert.spy(player_char.enter_motion_state).was_not_called()
+ assert.are_equal(20 - pc_data.center_height_standing + pc_data.center_height_compact, pc.position.y)
end)
- it('should not start rolling if input down is pressed but abs ground speed (negative) is not enough', function ()
- pc.ground_speed = -pc_data.roll_min_ground_speed + 0.01
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc.move_intention.y = 1
+ it('should adjust center position qdown = left when becoming compact on left wall', function ()
+ pc.position = vector(10, 20)
+ pc.quadrant = directions.left
- pc:check_roll_start()
+ -- character starts standing
+ pc:enter_motion_state(motion_states.air_spin)
- assert.spy(player_char.enter_motion_state).was_not_called()
+ assert.are_same(vector(10 - pc_data.center_height_standing + pc_data.center_height_compact, 20), pc.position)
end)
- it('should not start rolling if input down is pressed and abs ground speed (positive) is enough, but input x is also pressed', function ()
- pc.ground_speed = pc_data.roll_min_ground_speed
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc.move_intention = vector(-1, 1)
+ it('should adjust center position qup = up when landing and standing up on floor', function ()
+ pc.motion_state = motion_states.air_spin
+ pc.position = vector(10, 20)
- pc:check_roll_start()
+ -- character starts standing
+ pc:enter_motion_state(motion_states.standing)
- assert.spy(player_char.enter_motion_state).was_not_called()
+ assert.are_same(vector(10, 20 - pc_data.center_height_standing + pc_data.center_height_compact), pc.position)
end)
- it('should not start rolling if abs ground speed (positive) is high enough but input down is not pressed', function ()
- pc.ground_speed = pc_data.roll_min_ground_speed
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc.move_intention.y = 0
+ it('(entering any state other than standing and falling) should reset brake_anim_phase', function ()
+ -- simulate character braking
+ pc.motion_state = motion_states.standing
+ pc.brake_anim_phase = 1
- pc:check_roll_start()
+ -- simulate character rolling (possible to chain during brake)
+ pc:enter_motion_state(motion_states.rolling)
- assert.spy(player_char.enter_motion_state).was_not_called()
+ assert.are_equal(0, pc.brake_anim_phase)
end)
- it('should not start rolling if abs ground speed (negative) is high enough but input down is not pressed', function ()
- pc.ground_speed = -pc_data.roll_min_ground_speed
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc.move_intention.y = 0
+ it('(entering any state other than falling) should reset time_left_for_late_jump', function ()
+ pc.motion_state = motion_states.falling
+ pc.time_left_for_late_jump = 5
- pc:check_roll_start()
+ -- simulate character confirming a late jump
+ pc:enter_motion_state(motion_states.air_spin)
- assert.spy(player_char.enter_motion_state).was_not_called()
+ assert.are_equal(0, pc.time_left_for_late_jump)
end)
- it('should start rolling if input down is pressed and abs ground speed (positive) is enough', function ()
- pc.ground_speed = pc_data.roll_min_ground_speed
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc.move_intention.y = 1
-
- pc:check_roll_start()
+ end)
- assert.spy(player_char.enter_motion_state).was_called(1)
- assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.rolling)
- end)
+ describe('update_collision_timer', function ()
- it('should start rolling if input down is pressed and abs ground speed (negative) is enough', function ()
- pc.ground_speed = -pc_data.roll_min_ground_speed
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc.move_intention.y = 1
+ it('should do nothing when timer is 0 (or negative)', function ()
+ pc.ignore_launch_ramp_timer = 0
- pc:check_roll_start()
+ pc:update_collision_timer()
- assert.spy(player_char.enter_motion_state).was_called(1)
- assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.rolling)
+ assert.are_equal(0, pc.ignore_launch_ramp_timer)
end)
- it('should play low priority sfx when conditions to start rolling are met', function ()
- pc.ground_speed = pc_data.roll_min_ground_speed
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc.move_intention.y = 1
+ it('should decrease timer by 1/60 s when timer is positive', function ()
+ pc.ignore_launch_ramp_timer = 1
- pc:check_roll_start()
+ pc:update_collision_timer()
- assert.spy(player_char.play_low_priority_sfx).was_called(1)
- assert.spy(player_char.play_low_priority_sfx).was_called_with(match.ref(pc), audio.sfx_ids.roll)
+ assert.are_equal(0, pc.ignore_launch_ramp_timer)
end)
end)
- describe('check_roll_end', function ()
+ describe('update_platformer_motion', function ()
setup(function ()
- stub(player_char, "enter_motion_state")
+ -- we're not interested in the 2 update methods, but stubbing them
+ -- allows us to test the rest without having to worry about setting up all vars
+ -- to avoid assert/errors (e.g. ground tile location must be set or update_platformer_motion_grounded will fail)
+ stub(player_char, "update_platformer_motion_grounded")
+ stub(player_char, "update_platformer_motion_airborne")
+ stub(player_char, "check_crouch_and_roll_start")
+ stub(player_char, "check_roll_end")
+ stub(player_char, "check_spring")
+ stub(player_char, "check_launch_ramp")
+ stub(player_char, "check_emerald")
+ stub(player_char, "check_loop_external_triggers")
+ stub(player_char, "check_jump_intention")
end)
teardown(function ()
- player_char.enter_motion_state:revert()
+ player_char.update_platformer_motion_grounded:revert()
+ player_char.update_platformer_motion_airborne:revert()
+ player_char.check_crouch_and_roll_start:revert()
+ player_char.check_roll_end:revert()
+ player_char.check_spring:revert()
+ player_char.check_launch_ramp:revert()
+ player_char.check_emerald:revert()
+ player_char.check_loop_external_triggers:revert()
+ player_char.check_jump_intention:revert()
end)
after_each(function ()
- player_char.enter_motion_state:clear()
- end)
-
- before_each(function ()
- -- assumption
- pc.motion_state = motion_states.rolling
+ player_char.update_platformer_motion_grounded:clear()
+ player_char.update_platformer_motion_airborne:clear()
+ player_char.check_crouch_and_roll_start:clear()
+ player_char.check_roll_end:clear()
+ player_char.check_spring:clear()
+ player_char.check_launch_ramp:clear()
+ player_char.check_emerald:clear()
+ player_char.check_loop_external_triggers:clear()
+ player_char.check_jump_intention:clear()
end)
- it('should not end rolling if abs ground speed (positive) is high enough', function ()
- pc.ground_speed = pc_data.continue_roll_min_ground_speed
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ describe('(check_jump and check_spin_dash stubbed)', function ()
- pc:check_roll_end()
+ setup(function ()
+ stub(player_char, "check_jump")
+ stub(player_char, "check_spin_dash")
+ end)
- -- interface
- assert.spy(player_char.enter_motion_state).was_not_called()
- end)
+ teardown(function ()
+ player_char.check_jump:revert()
+ player_char.check_spin_dash:revert()
+ end)
- it('should not end rolling if abs ground speed (negative) is high enough', function ()
- pc.ground_speed = -pc_data.continue_roll_min_ground_speed
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ after_each(function ()
+ player_char.check_jump:clear()
+ player_char.check_spin_dash:clear()
+ end)
- pc:check_roll_end()
+ it('(when motion state is standing on ground) should call check_jump', function ()
+ pc.motion_state = motion_states.standing
+ pc:update_platformer_motion()
+ assert.spy(player_char.check_jump).was_called(1)
+ assert.spy(player_char.check_jump).was_called_with(match.ref(pc))
+ end)
- -- interface
- assert.spy(player_char.enter_motion_state).was_not_called()
- end)
+ it('(when motion state is rolling on ground) should call check_jump', function ()
+ pc.motion_state = motion_states.rolling
+ pc:update_platformer_motion()
+ assert.spy(player_char.check_jump).was_called(1)
+ assert.spy(player_char.check_jump).was_called_with(match.ref(pc))
+ end)
- it('should end rolling if abs ground speed (positive) is not enough', function ()
- pc.ground_speed = pc_data.continue_roll_min_ground_speed - 0.01
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ it('(when motion state is airborne, time_left_for_late_jump == 0) should not call check_jump', function ()
+ pc.motion_state = motion_states.falling -- or any airborne state
+ pc.time_left_for_late_jump = 0
- pc:check_roll_end()
+ pc:update_platformer_motion()
- -- interface
- assert.spy(player_char.enter_motion_state).was_called(1)
- assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
- end)
+ assert.spy(player_char.check_jump).was_not_called()
+ assert.are_equal(0, pc.time_left_for_late_jump)
+ end)
- it('should end rolling if abs ground speed (negative) is not enough', function ()
- pc.ground_speed = -pc_data.continue_roll_min_ground_speed + 0.01
- -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ it('(when motion state is airborne, time_left_for_late_jump > 0) should still call check_jump', function ()
+ pc.motion_state = motion_states.falling -- or any airborne state
+ pc.time_left_for_late_jump = 2
- pc:check_roll_end()
+ pc:update_platformer_motion()
- -- interface
- assert.spy(player_char.enter_motion_state).was_called(1)
- assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
- end)
+ assert.spy(player_char.check_jump).was_called(1)
+ assert.spy(player_char.check_jump).was_called_with(match.ref(pc))
+ assert.are_equal(1, pc.time_left_for_late_jump)
+ end)
- end)
+ it('should call check_spring (after motion)', function ()
+ pc.motion_state = motion_states.falling -- or any airborne state
+ pc:update_platformer_motion()
+ assert.spy(player_char.check_spring).was_called()
+ assert.spy(player_char.check_spring).was_called_with(match.ref(pc))
+ end)
- -- bugfix history:
- -- ^ use fractional speed to check that fractional moves are supported
- describe('update_platformer_motion_grounded (when _update_velocity sets ground_speed to 2.5)', function ()
+ it('should call check_launch_ramp (after motion)', function ()
+ pc.motion_state = motion_states.falling -- or any airborne state
+ pc:update_platformer_motion()
+ assert.spy(player_char.check_launch_ramp).was_called()
+ assert.spy(player_char.check_launch_ramp).was_called_with(match.ref(pc))
+ end)
- local update_ground_speed_mock
- local enter_motion_state_stub
- local check_jump_intention_stub
- local compute_ground_motion_result_mock
+ it('should call check_emerald (after motion)', function ()
+ pc.motion_state = motion_states.falling -- or any airborne state
+ pc:update_platformer_motion()
+ assert.spy(player_char.check_emerald).was_called()
+ assert.spy(player_char.check_emerald).was_called_with(match.ref(pc))
+ end)
- -- allows to modify the mock _update_ground_speed without restubbing it for every test section
- local new_ground_speed = -2.5 -- use fractional speed to check that fractions are preserved
+ it('should call check_loop_external_triggers (after motion)', function ()
+ pc.motion_state = motion_states.falling -- or any airborne state
+ pc:update_platformer_motion()
+ assert.spy(player_char.check_loop_external_triggers).was_called()
+ assert.spy(player_char.check_loop_external_triggers).was_called_with(match.ref(pc))
+ end)
- setup(function ()
- -- trigger check inside set_ground_tile_location will fail as it needs context
- -- (tile_test_data + mset), so we prefer stubbing as we don't check ground_tile_location directly
- stub(player_char, "set_ground_tile_location")
- spy.on(player_char, "set_slope_angle_with_quadrant") -- spy not stub in case the resulting slope_angle/quadrant matters
+ it('(when motion state is grounded) should call check_spin_dash', function ()
+ pc.motion_state = motion_states.standing
+ pc:update_platformer_motion()
+ assert.spy(player_char.check_spin_dash).was_called(1)
+ assert.spy(player_char.check_spin_dash).was_called_with(match.ref(pc))
+ end)
- update_ground_speed_mock = stub(player_char, "update_ground_speed", function (self)
- self.ground_speed = new_ground_speed
+ it('(when motion state is airborne) should call check_spin_dash', function ()
+ pc.motion_state = motion_states.falling
+ pc:update_platformer_motion()
+ assert.spy(player_char.check_spin_dash).was_not_called()
end)
- enter_motion_state_stub = stub(player_char, "enter_motion_state")
- check_jump_intention_stub = stub(player_char, "check_jump_intention")
+
end)
- teardown(function ()
- player_char.set_slope_angle_with_quadrant:revert()
-
- update_ground_speed_mock:revert()
- enter_motion_state_stub:revert()
- player_char.set_ground_tile_location:revert()
- check_jump_intention_stub:revert()
- end)
-
- after_each(function ()
- player_char.set_slope_angle_with_quadrant:clear()
-
- update_ground_speed_mock:clear()
- enter_motion_state_stub:clear()
- player_char.set_ground_tile_location:clear()
- check_jump_intention_stub:clear()
- end)
-
- it('should call _update_ground_speed', function ()
- pc:update_platformer_motion_grounded()
-
- -- implementation
- assert.spy(update_ground_speed_mock).was_called(1)
- assert.spy(update_ground_speed_mock).was_called_with(match.ref(pc))
- end)
+ describe('(update_platformer_motion_grounded sets motion state to air_spin, update_platformer_motion_airborne stubbed)', function ()
- describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: 0.25, is_blocked: false, is_falling: false)', function ()
+ local update_platformer_motion_grounded_mock
+ local update_platformer_motion_airborne_stub
setup(function ()
- compute_ground_motion_result_mock = stub(player_char, "compute_ground_motion_result", function (self)
- return motion.ground_motion_result(
- location(0, 1),
- vector(3, 4),
- 0.25,
- false,
- false
- )
+ -- mock the worst case possible for update_platformer_motion_grounded,
+ -- changing the state to air_spin to make sure the airborne branch is not entered afterward (else instead of 2 if blocks)
+ update_platformer_motion_grounded_mock = stub(player_char, "update_platformer_motion_grounded", function (self)
+ self.motion_state = motion_states.air_spin
end)
+ update_platformer_motion_airborne_stub = stub(player_char, "update_platformer_motion_airborne")
end)
teardown(function ()
- compute_ground_motion_result_mock:revert()
+ update_platformer_motion_grounded_mock:revert()
+ update_platformer_motion_airborne_stub:revert()
end)
after_each(function ()
- compute_ground_motion_result_mock:clear()
- end)
-
- it('should keep updated ground speed and set velocity frame according to ground speed (not blocked)', function ()
- pc:update_platformer_motion_grounded()
- -- interface: relying on _update_ground_speed implementation
- assert.are_same({-2.5, vector(-2.5, 0)}, {pc.ground_speed, pc.velocity})
+ update_platformer_motion_grounded_mock:clear()
+ update_platformer_motion_airborne_stub:clear()
end)
- it('should keep updated ground speed and set velocity frame according to ground speed and slope if not flat (not blocked)', function ()
- pc.slope_angle = 1/6 -- cos = 1/2, sin = -sqrt(3)/2, but use the formula directly to support floating errors
- pc:update_platformer_motion_grounded()
- -- interface: relying on _update_ground_speed implementation
- assert.are_same({-2.5, vector(-2.5*cos(1/6), 2.5*sqrt(3)/2)}, {pc.ground_speed, pc.velocity})
- end)
+ describe('(check_jump does nothing)', function ()
- it('should set the position to vector(3, 4)', function ()
- pc:update_platformer_motion_grounded()
- assert.are_same(vector(3, 4), pc.position)
- end)
+ local check_jump_stub
- it('should call player_char.set_ground_tile_location with location(0, 1)', function ()
- pc:update_platformer_motion_grounded()
- assert.spy(player_char.set_ground_tile_location).was_called(1)
- assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(0, 1))
- end)
+ setup(function ()
+ check_jump_stub = stub(player_char, "check_jump")
+ end)
- it('should call set_slope_angle_with_quadrant with 0.25', function ()
- pc.slope_angle = 1-0.25
- pc:update_platformer_motion_grounded()
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0.25)
- end)
+ teardown(function ()
+ check_jump_stub:revert()
+ end)
- it('should call check_jump_intention, not enter_motion_state (not falling)', function ()
- pc:update_platformer_motion_grounded()
+ after_each(function ()
+ check_jump_stub:clear()
+ end)
- -- implementation
- assert.spy(check_jump_intention_stub).was_called(1)
- assert.spy(check_jump_intention_stub).was_called_with(match.ref(pc))
- assert.spy(enter_motion_state_stub).was_not_called()
- end)
+ describe('(when character is standing)', function ()
- it('should set the run animation playback speed to abs(ground speed) (non-zero)', function ()
- -- mock is setting ground speed to -2.5
- pc:update_platformer_motion_grounded()
+ it('should call check_crouch_and_roll_start', function ()
+ pc.motion_state = motion_states.standing
- assert.are_equal(2.5, pc.anim_run_speed)
- end)
+ pc:update_platformer_motion()
- describe('(walking on ceiling or wall-ceiling)', function ()
+ assert.spy(player_char.check_crouch_and_roll_start).was_called(1)
+ assert.spy(player_char.check_crouch_and_roll_start).was_called_with(match.ref(pc))
+ end)
- before_each(function ()
- -- must be > 0.25 and < 0.75
- -- for full testing we should test 0.25, 0.75 and 0.74 too,
- -- but that will be enough
- -- the normal tests being done on ground where slope angle is 0 or very low (1-1/6)
- pc.slope_angle = 0.26
- pc.quadrant = directions.right
- end)
+ it('should call update_platformer_motion_grounded', function ()
+ pc.motion_state = motion_states.standing
- describe('(_update_ground_speed sets ground speed to -pc_data.ceiling_adherence_min_ground_speed / 2)', function ()
+ pc:update_platformer_motion()
- setup(function ()
- -- something lower than pc_data.ceiling_adherence_min_ground_speed in abs value
- new_ground_speed = -pc_data.ceiling_adherence_min_ground_speed / 2
+ assert.spy(update_platformer_motion_grounded_mock).was_called(1)
+ assert.spy(update_platformer_motion_grounded_mock).was_called_with(match.ref(pc))
+ assert.spy(update_platformer_motion_airborne_stub).was_not_called()
end)
- teardown(function ()
- -- pretty hacky way to restore the original stub of _update_ground_speed for further tests below
- new_ground_speed = -2.5
- end)
+ it('(time_left_for_late_jump == 0) should not call check_jump_intention for late jump', function ()
+ pc.motion_state = motion_states.standing
+ pc.time_left_for_late_jump = 0
- it('should enter falling state thanks to Falling and Sliding Off condition', function ()
- pc:update_platformer_motion_grounded()
+ pc:update_platformer_motion()
- assert.spy(enter_motion_state_stub).was_called(1)
- assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling)
+ assert.spy(player_char.check_jump_intention).was_not_called()
end)
- it('(when rolling) should enter air_spin state thanks to Falling and Sliding Off condition', function ()
- pc.motion_state = motion_states.rolling
+ it('(time_left_for_late_jump > 0) should call check_jump_intention for late jump', function ()
+ pc.motion_state = motion_states.standing
+ -- normally we should stub update_platformer_motion_grounded since the only way to get late jump timer
+ -- from a grounded state is that this method just initialized time_left_for_late_jump
+ -- but it's already stubbed so we'd need to set some local variable target_time_left_for_late_jump used
+ -- in the stub definition... to simplify, just set it here
+ -- but because the timer is decremented once, we need at least 2 not 1 for it to work (we set 6 which would correspond
+ -- to an initial value of 5 after decrement)
+ -- it's not very clean but simpler than doing the exact stub
+ pc.time_left_for_late_jump = 6
- pc:update_platformer_motion_grounded()
+ pc:update_platformer_motion()
- assert.spy(enter_motion_state_stub).was_called(1)
- assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.air_spin)
+ assert.spy(player_char.check_jump_intention).was_called(1)
+ assert.spy(player_char.check_jump_intention).was_called_with(match.ref(pc))
end)
end)
- describe('(_update_ground_speed sets ground speed to -pc_data.ceiling_adherence_min_ground_speed)', function ()
+ describe('(when character is crouching)', function ()
- setup(function ()
- -- exactly pc_data.ceiling_adherence_min_ground_speed in abs value to test exact comparison
- new_ground_speed = -pc_data.ceiling_adherence_min_ground_speed
- end)
+ it('should call check_crouch_and_roll_start', function ()
+ pc.motion_state = motion_states.crouching
+
+ pc:update_platformer_motion()
- teardown(function ()
- -- pretty hacky way to restore the original stub of _update_ground_speed for further tests below
- new_ground_speed = -2.5
+ assert.spy(player_char.check_crouch_and_roll_start).was_called(1)
+ assert.spy(player_char.check_crouch_and_roll_start).was_called_with(match.ref(pc))
end)
- it('should not enter falling (nor air_spin) state, escaping Falling and Sliding Off condition', function ()
- pc:update_platformer_motion_grounded()
+ it('should call update_platformer_motion_grounded', function ()
+ pc.motion_state = motion_states.crouching
- assert.spy(enter_motion_state_stub).was_not_called()
+ pc:update_platformer_motion()
+
+ assert.spy(update_platformer_motion_grounded_mock).was_called(1)
+ assert.spy(update_platformer_motion_grounded_mock).was_called_with(match.ref(pc))
+ assert.spy(update_platformer_motion_airborne_stub).was_not_called()
end)
end)
- end)
+ describe('(when character is rolling)', function ()
- end)
+ it('should call check_crouch_and_roll_start', function ()
+ pc.motion_state = motion_states.rolling
- describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: 0.5, is_blocked: true, is_falling: false)', function ()
+ pc:update_platformer_motion()
- local compute_ground_motion_result_mock
+ assert.spy(player_char.check_roll_end).was_called(1)
+ assert.spy(player_char.check_roll_end).was_called_with(match.ref(pc))
+ end)
- setup(function ()
- compute_ground_motion_result_mock = stub(player_char, "compute_ground_motion_result", function (self)
- return motion.ground_motion_result(
- location(0, 1),
- vector(3, 4),
- 0.5,
- true,
- false
- )
- end)
- end)
+ it('should call update_platformer_motion_grounded', function ()
+ pc.motion_state = motion_states.rolling
- teardown(function ()
- compute_ground_motion_result_mock:revert()
- end)
+ pc:update_platformer_motion()
- after_each(function ()
- compute_ground_motion_result_mock:clear()
- end)
+ assert.spy(update_platformer_motion_grounded_mock).was_called(1)
+ assert.spy(update_platformer_motion_grounded_mock).was_called_with(match.ref(pc))
+ assert.spy(update_platformer_motion_airborne_stub).was_not_called()
+ end)
- it('should reset ground speed and velocity frame to zero (blocked)', function ()
- pc:update_platformer_motion_grounded()
- assert.are_same({0, vector.zero()}, {pc.ground_speed, pc.velocity})
- end)
+ end)
- it('should call check_jump_intention, not enter_motion_state (not falling)', function ()
- pc:update_platformer_motion_grounded()
+ describe('(when character is in air_spin)', function ()
- -- implementation
- assert.spy(check_jump_intention_stub).was_called(1)
- assert.spy(check_jump_intention_stub).was_called_with(match.ref(pc))
- assert.spy(enter_motion_state_stub).was_not_called()
- end)
+ it('should call update_platformer_motion_airborne', function ()
+ pc.motion_state = motion_states.air_spin
- it('should set the position to vector(3, 4)', function ()
- pc:update_platformer_motion_grounded()
- assert.are_same(vector(3, 4), pc.position)
- end)
+ pc:update_platformer_motion()
- it('should call player_char.set_ground_tile_location with location(0, 1)', function ()
- pc:update_platformer_motion_grounded()
- assert.spy(player_char.set_ground_tile_location).was_called(1)
- assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(0, 1))
- end)
+ assert.spy(update_platformer_motion_airborne_stub).was_called(1)
+ assert.spy(update_platformer_motion_airborne_stub).was_called_with(match.ref(pc))
+ assert.spy(update_platformer_motion_grounded_mock).was_not_called()
+ end)
- it('should call set_slope_angle_with_quadrant with 0.5', function ()
- pc.slope_angle = 1-0.24
- pc.quadrant = directions.left
- pc:update_platformer_motion_grounded()
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0.5)
- end)
+ it('(time_left_for_late_jump == 0) should not call check_jump_intention for late jump', function ()
+ pc.motion_state = motion_states.air_spin
+ pc.time_left_for_late_jump = 0
- it('should set the run animation playback speed to abs(ground speed) = 0', function ()
- pc:update_platformer_motion_grounded()
+ pc:update_platformer_motion()
- assert.are_equal(0, pc.anim_run_speed)
- end)
+ assert.spy(player_char.check_jump_intention).was_not_called()
+ end)
- it('(on ceiling/wall-ceiling) should enter falling state and set horizontal control lock timer thanks to Falling and Sliding Off condition combined with block setting ground speed to 0', function ()
- pc.slope_angle = 0.25
- pc.quadrant = directions.right
+ it('(time_left_for_late_jump > 0) should call check_jump_intention for late jump', function ()
+ pc.motion_state = motion_states.air_spin
+ -- counter decrement so need at least 2 to remain positive afterwards
+ -- we're supposed to have set jump intention *last* frame
+ -- note that it's possible to be in air_spin with late jump, but we must have fallen from rolling
+ pc.time_left_for_late_jump = 2
- pc:update_platformer_motion_grounded()
+ pc:update_platformer_motion()
- assert.spy(enter_motion_state_stub).was_called(1)
- assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling)
+ assert.spy(player_char.check_jump_intention).was_called(1)
+ assert.spy(player_char.check_jump_intention).was_called_with(match.ref(pc))
+ end)
+
+ end)
- assert.are_equal(pc_data.fall_off_horizontal_control_lock_duration, pc.horizontal_control_lock_timer)
end)
- it('(rolling on ceiling/wall-ceiling) should enter air_spin state and set horizontal control lock timer thanks to Falling and Sliding Off condition combined with block setting ground speed to 0', function ()
- pc.motion_state = motion_states.rolling
- pc.slope_angle = 0.25
- pc.quadrant = directions.right
+ describe('(check_jump enters air_spin motion state)', function ()
- pc:update_platformer_motion_grounded()
+ local check_jump_mock
- assert.spy(enter_motion_state_stub).was_called(1)
- assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.air_spin)
+ setup(function ()
+ check_jump_mock = stub(player_char, "check_jump", function ()
+ pc.motion_state = motion_states.air_spin
+ pc.time_left_for_late_jump = 0 -- in reality should also reset this timer
+ end)
+ end)
- assert.are_equal(pc_data.fall_off_horizontal_control_lock_duration, pc.horizontal_control_lock_timer)
- end)
+ teardown(function ()
+ check_jump_mock:revert()
+ end)
- it('(on slope less than 90 degrees) should not enter falling state but still set horizontal control lock timer', function ()
- pc.slope_angle = 1-0.24
- pc.quadrant = directions.right
+ after_each(function ()
+ check_jump_mock:clear()
+ end)
- pc:update_platformer_motion_grounded()
+ describe('(when character is standing first)', function ()
- assert.spy(enter_motion_state_stub).was_not_called()
+ it('should not call check_crouch_and_roll_start since check_jump will enter air_spin first', function ()
+ pc.motion_state = motion_states.standing
- assert.are_equal(pc_data.fall_off_horizontal_control_lock_duration, pc.horizontal_control_lock_timer)
- end)
+ pc:update_platformer_motion()
- end)
+ assert.spy(player_char.check_crouch_and_roll_start).was_not_called()
+ end)
- describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: nil, is_blocked: false, is_falling: true)', function ()
+ it('should call update_platformer_motion_airborne since check_jump will enter air_spin first', function ()
+ pc.motion_state = motion_states.standing
- local compute_ground_motion_result_mock
+ pc:update_platformer_motion()
- setup(function ()
- compute_ground_motion_result_mock = stub(player_char, "compute_ground_motion_result", function (self)
- return motion.ground_motion_result(
- nil,
- vector(3, 4),
- nil,
- false,
- true
- )
- end)
- end)
+ assert.spy(update_platformer_motion_airborne_stub).was_called(1)
+ assert.spy(update_platformer_motion_airborne_stub).was_called_with(match.ref(pc))
+ assert.spy(update_platformer_motion_grounded_mock).was_not_called()
+ end)
- teardown(function ()
- compute_ground_motion_result_mock:revert()
- end)
+ it('(time_left_for_late_jump > 0) should not call check_jump_intention anyway as character is jumping now', function ()
+ pc.motion_state = motion_states.air_spin
+ -- just to show that this timer will be reset as we're confirming jump (it must be a late jump since timer was positive)
+ -- and so we don't check for jump intention again
+ pc.time_left_for_late_jump = 2
- after_each(function ()
- compute_ground_motion_result_mock:clear()
- end)
+ pc:update_platformer_motion()
- it('should keep updated ground speed and set velocity frame according to ground speed (not blocked)', function ()
- pc:update_platformer_motion_grounded()
- -- interface: relying on _update_ground_speed implementation
- assert.are_same({-2.5, vector(-2.5, 0)}, {pc.ground_speed, pc.velocity})
- end)
+ assert.spy(player_char.check_jump_intention).was_not_called()
+ end)
- it('should keep updated ground speed and set velocity frame according to ground speed and slope if not flat (not blocked)', function ()
- pc.slope_angle = 1/6 -- cos = 1/2, sin = -sqrt(3)/2, but use the formula directly to support floating errors
- pc:update_platformer_motion_grounded()
- -- interface: relying on _update_ground_speed implementation
- assert.are_same({-2.5, vector(-2.5*cos(1/6), 2.5*sqrt(3)/2)}, {pc.ground_speed, pc.velocity})
- end)
+ end)
- it('should call enter_motion_state with falling state, not call check_jump_intention (falling)', function ()
- pc:update_platformer_motion_grounded()
+ describe('(when character is rolling first)', function ()
- -- implementation
- assert.spy(enter_motion_state_stub).was_called(1)
- assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling)
- assert.spy(check_jump_intention_stub).was_not_called()
- end)
+ it('should not call check_roll_end since check_jump will enter air_spin first', function ()
+ pc.motion_state = motion_states.rolling
- it('(when rolling) should call enter_motion_state with air_spin state, not call check_jump_intention (falling)', function ()
- pc.motion_state = motion_states.rolling
+ pc:update_platformer_motion()
- pc:update_platformer_motion_grounded()
+ assert.spy(player_char.check_roll_end).was_not_called()
+ end)
- -- implementation
- assert.spy(enter_motion_state_stub).was_called(1)
- assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.air_spin)
- assert.spy(check_jump_intention_stub).was_not_called()
- end)
+ it('should call update_platformer_motion_airborne since check_jump will enter air_spin first', function ()
+ pc.motion_state = motion_states.rolling
- it('should set the position to vector(3, 4)', function ()
- pc:update_platformer_motion_grounded()
- assert.are_same(vector(3, 4), pc.position)
- end)
+ pc:update_platformer_motion()
- -- we don't test that ground_tile_location is set to nil
- -- because we stubbed enter_motion_state which should do it,
- -- but if it was spied we could test it
+ assert.spy(update_platformer_motion_airborne_stub).was_called(1)
+ assert.spy(update_platformer_motion_airborne_stub).was_called_with(match.ref(pc))
+ assert.spy(update_platformer_motion_grounded_mock).was_not_called()
+ end)
+
+ end)
+
+ -- we don't need to test (when character is airborne) since in this context check_jump
+ -- always trigger a jump, which is impossible from the air (as double jump is not implemented)
- it('should not call set_slope_angle_with_quadrant (actually called inside enter_motion_state)', function ()
- pc.slope_angle = 0
- pc:update_platformer_motion_grounded()
- -- this only works because enter_motion_state is stubbed
- -- if it was spied, it would still call set_slope_angle_with_quadrant inside
- assert.spy(player_char.set_slope_angle_with_quadrant).was_not_called()
end)
end)
- describe('(when _compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: nil, is_blocked: true, is_falling: true)', function ()
-
- local compute_ground_motion_result_mock
+ end) -- update_platformer_motion
- setup(function ()
- compute_ground_motion_result_mock = stub(player_char, "compute_ground_motion_result", function (self)
- return motion.ground_motion_result(
- nil,
- vector(3, 4),
- nil,
- true,
- true
- )
- end)
- end)
+ describe('check_crouch_and_roll_start', function ()
- teardown(function ()
- compute_ground_motion_result_mock:revert()
- end)
+ setup(function ()
+ stub(player_char, "enter_motion_state")
+ stub(player_char, "play_low_priority_sfx")
+ end)
- after_each(function ()
- compute_ground_motion_result_mock:clear()
- end)
+ teardown(function ()
+ player_char.enter_motion_state:revert()
+ player_char.play_low_priority_sfx:revert()
+ end)
- it('should reset ground speed and velocity frame to zero (blocked)', function ()
- pc:update_platformer_motion_grounded()
- assert.are_same({0, vector.zero()}, {pc.ground_speed, pc.velocity})
- end)
+ after_each(function ()
+ player_char.enter_motion_state:clear()
+ player_char.play_low_priority_sfx:clear()
+ end)
- it('should call enter_motion_state with falling state, not call check_jump_intention (falling)', function ()
- pc:update_platformer_motion_grounded()
+ before_each(function ()
+ -- assumption
+ pc.motion_state = motion_states.standing
+ end)
- -- implementation
- assert.spy(enter_motion_state_stub).was_called(1)
- assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling)
- assert.spy(check_jump_intention_stub).was_not_called()
- end)
+ -- stubbing note: normally we should stub wants_to_crouch, but most of the tests below have been written
+ -- before wants_to_crouch was extracted, and it's still simpler to set move_intention that changing
+ -- some local var returned by wants_to_crouch's stub function, so we kept them this way
+ -- in counterpart, do not bother testing all move_intention cases, wants_to_crouch utests will do that
- it('(when rolling) should call enter_motion_state with air_spin state, not call check_jump_intention (falling)', function ()
- pc.motion_state = motion_states.rolling
+ it('(standing) should crouch if input down is pressed but abs ground speed (positive) is not enough', function ()
+ pc.ground_speed = pc_data.roll_min_ground_speed - 0.01
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 1
- pc:update_platformer_motion_grounded()
+ pc:check_crouch_and_roll_start()
- -- implementation
- assert.spy(enter_motion_state_stub).was_called(1)
- assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.air_spin)
- assert.spy(check_jump_intention_stub).was_not_called()
- end)
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.crouching)
+ end)
- it('should set the position to vector(3, 4)', function ()
- pc:update_platformer_motion_grounded()
- assert.are_same(vector(3, 4), pc.position)
- end)
+ it('(standing) should crouch if input down is pressed but abs ground speed (negative) is not enough', function ()
+ pc.ground_speed = -pc_data.roll_min_ground_speed + 0.01
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 1
- it('should not call set_slope_angle_with_quadrant (actually called inside enter_motion_state)', function ()
- pc.slope_angle = 0
- pc:update_platformer_motion_grounded()
- -- this only works because enter_motion_state is stubbed
- -- if it was spied, it would still call set_slope_angle_with_quadrant inside
- assert.spy(player_char.set_slope_angle_with_quadrant).was_not_called()
- end)
+ pc:check_crouch_and_roll_start()
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.crouching)
end)
- describe('(when _compute_ground_motion_result returns a motion result with position vector(*2.5*, 4), slope_angle: 0, is_blocked: false, is_falling: false)', function ()
+ it('(standing) should not crouch nor start rolling if input down is pressed and abs ground speed (positive) is enough, but input x is also pressed', function ()
+ pc.ground_speed = pc_data.roll_min_ground_speed
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention = vector(-1, 1)
- local compute_ground_motion_result_mock
+ pc:check_crouch_and_roll_start()
- setup(function ()
- stub(player_char, "compute_ground_motion_result", function (self)
- return motion.ground_motion_result(
- location(-1, 0),
- vector(2.5, 4), -- flr(2.5) must be < pc_data.ground_sensor_extent_x
- 0,
- false,
- false
- )
- end)
- end)
+ assert.spy(player_char.enter_motion_state).was_not_called()
+ end)
- teardown(function ()
- player_char.compute_ground_motion_result:revert()
- end)
+ it('(standing) should not crouch nor start rolling if abs ground speed (positive) is high enough but input down is not pressed', function ()
+ pc.ground_speed = pc_data.roll_min_ground_speed
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 0
- after_each(function ()
- player_char.compute_ground_motion_result:clear()
- end)
+ pc:check_crouch_and_roll_start()
- it('should clamp character position X to stage left boundary (including half-width offset)', function ()
- pc:update_platformer_motion_grounded()
+ assert.spy(player_char.enter_motion_state).was_not_called()
+ end)
- -- in practice, clamped to 3
- assert.are_same(ceil(pc_data.ground_sensor_extent_x), pc.position.x)
- end)
+ it('(standing) should not crouch nor start rolling if abs ground speed (negative) is high enough but input down is not pressed', function ()
+ pc.ground_speed = -pc_data.roll_min_ground_speed
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 0
- it('should clamp the ground speed to -0.1', function ()
- -- note that we didn't set move intention
- -- so character will decel to -2.5 this frame, but enough to test clamping
- pc.ground_speed = -3
+ pc:check_crouch_and_roll_start()
- pc:update_platformer_motion_grounded()
+ assert.spy(player_char.enter_motion_state).was_not_called()
+ end)
- assert.are_equal(-0.1, pc.ground_speed)
- end)
+ it('(standing) should start rolling if input down is pressed and abs ground speed (positive) is enough', function ()
+ pc.ground_speed = pc_data.roll_min_ground_speed
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 1
+
+ pc:check_crouch_and_roll_start()
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.rolling)
end)
- end) -- update_platformer_motion_grounded
+ it('(standing) should start rolling if input down is pressed and abs ground speed (negative) is enough', function ()
+ pc.ground_speed = -pc_data.roll_min_ground_speed
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 1
- describe('update_ground_speed', function ()
+ pc:check_crouch_and_roll_start()
- setup(function ()
- -- the only reason we spy and not stub is to test the interface in the first test below
- spy.on(player_char, "update_ground_speed_by_slope")
- spy.on(player_char, "update_ground_run_speed_by_intention")
- spy.on(player_char, "update_ground_roll_speed_by_intention")
- spy.on(player_char, "clamp_ground_speed")
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.rolling)
end)
- teardown(function ()
- player_char.update_ground_speed_by_slope:revert()
- player_char.update_ground_run_speed_by_intention:revert()
- player_char.update_ground_roll_speed_by_intention:revert()
- player_char.clamp_ground_speed:revert()
+ it('(standing) should play low priority sfx when conditions to start rolling are met', function ()
+ pc.ground_speed = pc_data.roll_min_ground_speed
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 1
+
+ pc:check_crouch_and_roll_start()
+
+ assert.spy(player_char.play_low_priority_sfx).was_called(1)
+ assert.spy(player_char.play_low_priority_sfx).was_called_with(match.ref(pc), audio.sfx_ids.roll)
end)
- after_each(function ()
- player_char.update_ground_speed_by_slope:clear()
- player_char.update_ground_run_speed_by_intention:clear()
- player_char.update_ground_roll_speed_by_intention:clear()
- player_char.clamp_ground_speed:clear()
+ it('(crouching) should start rolling if input down is held and abs ground speed (negative) becomes enough (due to auto-slide on slope)', function ()
+ pc.motion_state = motion_states.crouching
+ pc.ground_speed = -pc_data.roll_min_ground_speed
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 1
+
+ pc:check_crouch_and_roll_start()
+
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.rolling)
end)
- -- usually we'd only test the interface (calls)
- -- but since we cannot easily test the call order with spies,
- -- we do a mini itest to check the resulting velocity,
- -- which will prove that slope factor is applied before intention
+ it('(crouching) should not try to crouch again if input down is held at low abs ground speed', function ()
+ pc.motion_state = motion_states.crouching
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 1
- it('(standing) should apply descending slope factor, then oppose it with strong decel when moving in the ascending direction of 45-degree slope from ground speed 0', function ()
- -- interface: check overall behavior (mini integration test)
- pc.ground_speed = 0
- pc.slope_angle = 1/8 -- 45 deg ascending
+ pc:check_crouch_and_roll_start()
- pc.move_intention.x = 1
- pc:update_ground_speed()
- -- Note that we have fixed the classic Sonic exploit of decelerating faster when accelerating backward from ground speed 0,
- -- so the speed will still be clamped to ground accel on this frame, and not become
- -- - pc_data.slope_accel_factor_frame2 * sin(-1/8) + pc_data.ground_decel_frame2
- assert.are_equal(pc_data.ground_accel_frame2, pc.ground_speed)
+ assert.spy(player_char.enter_motion_state).was_not_called()
end)
- it('(standing) should update ground speed based on slope, then intention', function ()
- pc.ground_speed = 2.5
+ it('(crouching) should stand up if input down is released', function ()
+ pc.motion_state = motion_states.crouching
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
+ pc.move_intention.y = 0
- pc:update_ground_speed()
+ pc:check_crouch_and_roll_start()
- assert.spy(player_char.update_ground_speed_by_slope).was_called(1)
- assert.spy(player_char.update_ground_speed_by_slope).was_called_with(match.ref(pc))
- assert.spy(player_char.update_ground_run_speed_by_intention).was_called(1)
- assert.spy(player_char.update_ground_run_speed_by_intention).was_called_with(match.ref(pc))
- assert.spy(player_char.clamp_ground_speed).was_called(1)
- assert.spy(player_char.clamp_ground_speed).was_called_with(match.ref(pc), 2.5)
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
end)
- it('(rolling) should call update_ground_roll_speed_by_intention (instead of _run_)', function ()
- pc.motion_state = motion_states.rolling
+ end)
- pc:update_ground_speed()
+ describe('check_roll_end', function ()
- assert.spy(player_char.update_ground_speed_by_slope).was_called(1)
- assert.spy(player_char.update_ground_speed_by_slope).was_called_with(match.ref(pc))
- assert.spy(player_char.update_ground_roll_speed_by_intention).was_called(1)
- assert.spy(player_char.update_ground_roll_speed_by_intention).was_called_with(match.ref(pc))
- assert.spy(player_char.clamp_ground_speed).was_not_called()
+ setup(function ()
+ stub(player_char, "enter_motion_state")
end)
- end) -- _update_ground_speed
-
- describe('update_ground_speed_by_slope', function ()
-
- it('should preserve ground speed on flat ground', function ()
- pc.ground_speed = 2
- pc.slope_angle = 0
- pc.ascending_slope_time = 77
-
- pc:update_ground_speed_by_slope(1.8)
-
- assert.are_equal(2, pc.ground_speed)
+ teardown(function ()
+ player_char.enter_motion_state:revert()
+ end)
- assert.are_same({
- 2,
- 0
- },
- {
- pc.ground_speed,
- pc.ascending_slope_time
- })
+ after_each(function ()
+ player_char.enter_motion_state:clear()
end)
- -- Original feature (not in SPG): Progressive Ascending Steep Slope Factor
+ before_each(function ()
+ -- assumption
+ pc.motion_state = motion_states.rolling
+ end)
- it('should accelerate toward left on a steep ascending slope, with very reduced slope factor at the beginning of the climb, and increase ascending slope time', function ()
- pc.ground_speed = 2
- pc.slope_angle = 0.125 -- sin(0.125) = -sqrt(2)/2
- pc.ascending_slope_time = 0
+ it('should not end rolling if abs ground speed (positive) is high enough', function ()
+ pc.ground_speed = pc_data.continue_roll_min_ground_speed
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc:update_ground_speed_by_slope(1.8)
+ pc:check_roll_end()
- assert.are_same({
- 2 - delta_time60 / pc_data.progressive_ascending_slope_duration * pc_data.slope_accel_factor_frame2 * sqrt(2)/2,
- delta_time60
- },
- {
- pc.ground_speed,
- pc.ascending_slope_time
- })
+ -- interface
+ assert.spy(player_char.enter_motion_state).was_not_called()
end)
- it('should accelerate toward left on a steep ascending slope, with reduced slope factor before ascending slope duration, and increase ascending slope time', function ()
- pc.ground_speed = 2
- pc.slope_angle = 0.125 -- sin(0.125) = -sqrt(2)/2
- pc.ascending_slope_time = 0.1
+ it('should not end rolling if abs ground speed (negative) is high enough', function ()
+ pc.ground_speed = -pc_data.continue_roll_min_ground_speed
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc:update_ground_speed_by_slope(1.8)
+ pc:check_roll_end()
- assert.are_same({
- 2 - (0.1 + delta_time60) / pc_data.progressive_ascending_slope_duration * pc_data.slope_accel_factor_frame2 * sqrt(2)/2,
- 0.1 + delta_time60
- },
- {
- pc.ground_speed,
- pc.ascending_slope_time
- })
+ -- interface
+ assert.spy(player_char.enter_motion_state).was_not_called()
end)
- it('should accelerate toward left on a steep ascending slope, with full slope factor after ascending slope duration, and clamp time to that duration', function ()
- pc.ground_speed = 2
- pc.slope_angle = 0.125 -- sin(0.125) = -sqrt(2)/2
- pc.ascending_slope_time = pc_data.progressive_ascending_slope_duration
+ it('should end rolling if abs ground speed (positive) is not enough', function ()
+ pc.ground_speed = pc_data.continue_roll_min_ground_speed - 0.01
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc:update_ground_speed_by_slope(1.8)
+ pc:check_roll_end()
- assert.are_same({
- 2 - pc_data.slope_accel_factor_frame2 * sqrt(2)/2,
- pc_data.progressive_ascending_slope_duration
- },
- {
- pc.ground_speed,
- pc.ascending_slope_time
- })
+ -- interface
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
end)
- it('should accelerate toward right on a non-steep ascending slope, and reset any ascending slope time', function ()
- pc.ground_speed = 2
- pc.slope_angle = 0.0625
- pc.ascending_slope_time = 77
+ it('should end rolling if abs ground speed (negative) is not enough', function ()
+ pc.ground_speed = -pc_data.continue_roll_min_ground_speed + 0.01
+ -- we don't set velocity, but on flat ground it would be vector(pc.ground_speed, 0)
- pc:update_ground_speed_by_slope(1.8)
+ pc:check_roll_end()
- assert.are_same({
- 2 - pc_data.slope_accel_factor_frame2 * sin(-0.0625), -- note that the sin is positive
- 0
- },
- {
- pc.ground_speed,
- pc.ascending_slope_time
- })
+ -- interface
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
end)
- it('should accelerate toward right on an descending slope, with full slope factor, and reset any ascending slope time', function ()
- pc.ground_speed = 2
- pc.slope_angle = 1-0.125 -- sin(-0.125) = sqrt(2)/2
- pc.ascending_slope_time = 77
-
- pc:update_ground_speed_by_slope(1.8)
+ end)
- assert.are_same({
- 2 + pc_data.slope_accel_factor_frame2 * sqrt(2)/2,
- 0
- },
- {
- pc.ground_speed,
- pc.ascending_slope_time
- })
- end)
+ describe('update_platformer_motion_grounded (when update_velocity sets ground_speed to 2.5)', function ()
- end) -- _update_ground_speed_by_slope
+ local update_ground_speed_mock
+ local enter_motion_state_stub
+ local compute_ground_motion_result_mock
- describe('update_ground_run_speed_by_intention', function ()
+ -- allows to modify the mock update_ground_speed without restubbing it for every test section
+ local new_ground_speed = -2.5 -- use fractional speed to check that fractions are preserved
setup(function ()
- stub(player_char, "play_low_priority_sfx")
+ -- trigger check inside set_ground_tile_location will fail as it needs context
+ -- (tile_test_data + mset), so we prefer stubbing as we don't check ground_tile_location directly
+ stub(player_char, "set_ground_tile_location")
+ spy.on(player_char, "set_slope_angle_with_quadrant") -- spy not stub in case the resulting slope_angle/quadrant matters
+
+ update_ground_speed_mock = stub(player_char, "update_ground_speed", function (self)
+ self.ground_speed = new_ground_speed
+ end)
+ enter_motion_state_stub = stub(player_char, "enter_motion_state")
end)
teardown(function ()
- player_char.play_low_priority_sfx:revert()
- end)
+ player_char.set_slope_angle_with_quadrant:revert()
- after_each(function ()
- player_char.play_low_priority_sfx:clear()
+ update_ground_speed_mock:revert()
+ enter_motion_state_stub:revert()
+ player_char.set_ground_tile_location:revert()
end)
- it('should accelerate and set direction based on new speed when character is facing left, has ground speed 0 and move intention x > 0', function ()
- pc.orientation = horizontal_dirs.left
- pc.move_intention.x = 1
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, pc_data.ground_accel_frame2},
- {pc.orientation, pc.ground_speed})
- end)
+ after_each(function ()
+ -- hacky way to restore the original stub behavior of update_ground_speed after every test
+ -- that could modify it
+ new_ground_speed = -2.5
- it('should accelerate and set orientation + reset brake_anim_phase when character is facing left, has ground speed > 0 and move intention x > 0', function ()
- pc.orientation = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward e.g. after landing on a steep ascending slope and walking backward
- pc.brake_anim_phase = 1
- pc.ground_speed = 1.5
- pc.move_intention.x = 1
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 0, 1.5 + pc_data.ground_accel_frame2},
- {pc.orientation, pc.brake_anim_phase, pc.ground_speed})
- end)
+ player_char.set_slope_angle_with_quadrant:clear()
- it('should accelerate and preserve direction when character is facing left, has ground speed < 0 and move intention x < 0', function ()
- pc.orientation = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward e.g. after hitting a spring after landing on a steep ascending slope and walking backward
- pc.ground_speed = -1.5
- pc.move_intention.x = -1
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.left, -1.5 - pc_data.ground_accel_frame2},
- {pc.orientation, pc.ground_speed})
+ update_ground_speed_mock:clear()
+ enter_motion_state_stub:clear()
+ player_char.set_ground_tile_location:clear()
end)
- it('should decelerate keeping same sign and direction when character is facing right, has high ground speed > ground accel * 1 frame and move intention x < 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 1.5
- pc.move_intention.x = -1
- pc:update_ground_run_speed_by_intention()
- -- ground_decel_frame2 = 0.25, subtract it from ground_speed
- assert.are_same({horizontal_dirs.right, 1.25},
- {pc.orientation, pc.ground_speed})
- end)
+ describe('(stub compute_ground_motion_result)', function ()
- -- Original feature (not in SPG): Reduced Deceleration on Steep Descending Slope
+ setup(function ()
+ stub(player_char, "compute_ground_motion_result", function (self)
+ -- return anything with correct type, this stub is just to avoid crashes due to incomplete setup
+ return motion.ground_motion_result(
+ location(0, 0),
+ vector(0, 0),
+ 0,
+ false,
+ false
+ )
+ end)
+ end)
- it('should decelerate with decel descending slope factor, keeping same sign and direction when character is on steep descending slope facing right, has high ground speed > ground accel * 1 frame and move intention x < 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 1.5
- pc.move_intention.x = -1
- pc.slope_angle = 1-0.125
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_decel_descending_slope_factor * pc_data.ground_decel_frame2},
- {pc.orientation, pc.ground_speed})
- end)
+ teardown(function ()
+ player_char.compute_ground_motion_result:revert()
+ end)
- it('should decelerate without decel descending slope factor, keeping same sign and direction when character is on non-steep descending slope facing right, has high ground speed > ground accel * 1 frame and move intention x < 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 1.5
- pc.move_intention.x = -1
- pc.slope_angle = 1-0.0625
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_decel_frame2},
- {pc.orientation, pc.ground_speed})
- end)
+ it('should call update_ground_speed', function ()
+ pc:update_platformer_motion_grounded()
- -- End Original feature
+ -- implementation
+ assert.spy(update_ground_speed_mock).was_called(1)
+ assert.spy(update_ground_speed_mock).was_called_with(match.ref(pc))
+ end)
- it('should decelerate and stop exactly at speed 0, when character has ground speed = ground decel * 1 frame and move intention x < 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = pc_data.ground_decel_frame2
- pc.move_intention.x = -1
- pc:update_ground_run_speed_by_intention()
- assert.are_equal(0, pc.ground_speed)
end)
- -- test orientation and brake anim phase together as they are related to visuals
-
- it('should set orientation to move intention dir (here, *change orientation*) and preserve brake_anim_phase when character decelerates exactly to 0 but no brake anim started', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = pc_data.ground_decel_frame2
- pc.move_intention.x = -1
- pc.brake_anim_phase = 0
-
- pc:update_ground_run_speed_by_intention()
-
- assert.are_same({horizontal_dirs.left, 0}, {pc.orientation, pc.brake_anim_phase})
- end)
+ describe('(when compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: 0.25, is_blocked: false, is_falling: false)', function ()
- it('should set orientation to move intention dir (here, *change orientation*) and advance brake_anim_phase to 2 when character decelerates exactly to 0 but and brake_start is playing', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = pc_data.ground_decel_frame2
- pc.move_intention.x = -1
- pc.brake_anim_phase = 1
+ setup(function ()
+ compute_ground_motion_result_mock = stub(player_char, "compute_ground_motion_result", function (self)
+ return motion.ground_motion_result(
+ location(0, 1),
+ vector(3, 4),
+ 0.25,
+ false,
+ false
+ )
+ end)
+ end)
- pc:update_ground_run_speed_by_intention()
+ teardown(function ()
+ compute_ground_motion_result_mock:revert()
+ end)
- assert.are_same({horizontal_dirs.left, 2}, {pc.orientation, pc.brake_anim_phase})
- end)
+ after_each(function ()
+ compute_ground_motion_result_mock:clear()
+ end)
- it('should preserve orientation and brake anim phase when quadrant down and abs ground speed is too low', function ()
- pc.quadrant = directions.down
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = pc_data.brake_anim_min_speed_frame - 0.01
- pc.move_intention.x = -1
- pc.brake_anim_phase = 0
+ -- note: these tests test in particular the behavior of compute_velocity_from_ground_speed,
+ -- but they were written before it was extracted, so just kept like this
- pc:update_ground_run_speed_by_intention()
+ it('should keep updated ground speed and set velocity frame according to ground speed (not blocked)', function ()
+ pc:update_platformer_motion_grounded()
+ -- interface: relying on update_ground_speed implementation
+ assert.are_same({-2.5, vector(-2.5, 0)}, {pc.ground_speed, pc.velocity})
+ end)
- assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
- end)
+ it('should keep updated ground speed and set velocity frame according to ground speed and slope if not flat (not blocked)', function ()
+ pc.slope_angle = 1/6 -- cos = 1/2, sin = -sqrt(3)/2, but use the formula directly to support floating errors
+ pc:update_platformer_motion_grounded()
+ -- interface: relying on update_ground_speed implementation
+ assert.are_same({-2.5, vector(-2.5*cos(1/6), 2.5*sqrt(3)/2)}, {pc.ground_speed, pc.velocity})
+ end)
- it('should preserve orientation and brake anim phase when quadrant right and abs ground speed is high enough', function ()
- pc.quadrant = directions.right
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = pc_data.brake_anim_min_speed_frame
- pc.move_intention.x = -1
- pc.brake_anim_phase = 0
+ it('should set the position to vector(3, 4)', function ()
+ pc:update_platformer_motion_grounded()
+ assert.are_same(vector(3, 4), pc.position)
+ end)
- pc:update_ground_run_speed_by_intention()
+ it('should call player_char.set_ground_tile_location with location(0, 1)', function ()
+ pc:update_platformer_motion_grounded()
+ assert.spy(player_char.set_ground_tile_location).was_called(1)
+ assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(0, 1))
+ end)
- assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
- end)
+ it('should call set_slope_angle_with_quadrant with 0.25', function ()
+ pc.slope_angle = 1-0.25
+ pc:update_platformer_motion_grounded()
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0.25)
+ end)
- it('should set orientation to ground speed dir (here, no change) and brake anim phase to 1 then play brake low priority sfx when quadrant down and abs ground speed is high enough', function ()
- pc.quadrant = directions.down
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = pc_data.brake_anim_min_speed_frame
- pc.move_intention.x = -1
- pc.brake_anim_phase = 0
+ it('should not call enter_motion_state (not falling)', function ()
+ pc:update_platformer_motion_grounded()
- pc:update_ground_run_speed_by_intention()
+ -- implementation
+ assert.spy(enter_motion_state_stub).was_not_called()
+ end)
- assert.are_same({horizontal_dirs.right, 1}, {pc.orientation, pc.brake_anim_phase})
+ it('should set the run animation playback speed to abs(ground speed) (non-zero)', function ()
+ -- mock is setting ground speed to -2.5
+ pc:update_platformer_motion_grounded()
- assert.spy(player_char.play_low_priority_sfx).was_called(1)
- assert.spy(player_char.play_low_priority_sfx).was_called_with(match.ref(pc), audio.sfx_ids.brake)
- end)
+ assert.are_equal(2.5, pc.anim_run_speed)
+ end)
- -- bugfix history:
- -- _ missing tests that check the change of sign of ground speed
- it('should decelerate and start moving to the left when character is facing right, '..
- 'has low ground speed > 0 but < ground accel * 1 frame and move intention x < 0 '..
- 'but the ground speed is high enough so that the new speed wouldn\'t be over the max ground speed', function ()
- pc.orientation = horizontal_dirs.right
- pc.brake_anim_phase = true
- -- start with speed >= -ground_accel_frame2 + ground_decel_frame2 but still < ground_decel_frame2
- pc.ground_speed = 0.24
- pc.move_intention.x = -1
- pc:update_ground_run_speed_by_intention()
- assert.is_true(almost_eq_with_message(-0.01, pc.ground_speed, 1e-16))
- end)
+ describe('(walking on ceiling or wall-ceiling)', function ()
- it('should decelerate and start moving to the left, and clamp to the max ground speed in the opposite sign '..
- 'when character is facing right, has low ground speed > 0 and move intention x < 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.should_play_brake_start_anim = true
- -- start with speed < -ground_accel_frame2 + ground_decel_frame2
- pc.ground_speed = 0.12
- pc.move_intention.x = -1
- pc:update_ground_run_speed_by_intention()
- assert.are_equal(-pc_data.ground_accel_frame2, pc.ground_speed)
- end)
+ before_each(function ()
+ -- must be > 0.25 and < 0.75
+ -- for full testing we should test 0.25, 0.75 and 0.74 too,
+ -- but that will be enough
+ -- the normal tests being done on ground where slope angle is 0 or very low (1-1/6)
+ pc.slope_angle = 0.26
+ pc.quadrant = directions.right
+ end)
- it('should should set orientation to move intention dir (here, change orientation) and preserve brake_anim_phase when character decelerates to opposite sign but no brake anim started', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = pc_data.ground_decel_frame2 - pc_data.ground_accel_frame2
- pc.move_intention.x = -1
- pc.brake_anim_phase = 0
+ describe('(update_ground_speed sets ground speed to -pc_data.ceiling_adherence_min_ground_speed / 2)', function ()
- pc:update_ground_run_speed_by_intention()
+ -- rely on after_each above to reset new_ground_speed
+ before_each(function ()
+ -- something lower than pc_data.ceiling_adherence_min_ground_speed in abs value
+ new_ground_speed = -pc_data.ceiling_adherence_min_ground_speed / 2
+ end)
- assert.are_same({horizontal_dirs.left, 0}, {pc.orientation, pc.brake_anim_phase})
- assert.are_equal(0, pc.brake_anim_phase)
- end)
+ it('should enter falling state thanks to Falling and Sliding Off condition', function ()
+ pc:update_platformer_motion_grounded()
- it('should should set orientation to move intention dir (here, change orientation) and advance brake_anim_phase to 2 when character decelerates to opposite sign but and brake_start is playing', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = pc_data.ground_decel_frame2 - pc_data.ground_accel_frame2
- pc.move_intention.x = -1
- pc.brake_anim_phase = 1
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling)
+ end)
- pc:update_ground_run_speed_by_intention()
+ -- rare, but possible with crouch sliding
+ it('(when crouching) should enter air_spin state thanks to Falling and Sliding Off condition', function ()
+ pc.motion_state = motion_states.crouching
- assert.are_equal(2, pc.brake_anim_phase)
- end)
+ pc:update_platformer_motion_grounded()
- -- tests below seem symmetrical, but as a twist we have the character running backward (e.g. after a reverse jump)
- -- so he's facing the opposite direction of the run, so we can test direction update
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling)
+ end)
- -- in addition, character faces ground speed dir again when brake_start anim is played,
- -- which can only be tested when running backward
+ it('(when rolling) should enter air_spin state thanks to Falling and Sliding Off condition', function ()
+ pc.motion_state = motion_states.rolling
- it('should decelerate keeping same sign when character is facing right, has mid ground speed < 0 but not abs higher than brake_anim_min_speed_frame and move intention x > 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -1.5
- pc.move_intention.x = 1
- pc:update_ground_run_speed_by_intention()
- assert.are_equal(-1.25, pc.ground_speed)
- end)
+ pc:update_platformer_motion_grounded()
- it('should when character has ground speed = ground decel * 1 frame and move intention x < 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -pc_data.ground_decel_frame2
- pc.move_intention.x = 1
- pc:update_ground_run_speed_by_intention()
- assert.are_equal(0, pc.ground_speed)
- end)
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.air_spin)
+ end)
- it('should decelerate and stop exactly at speed 0 when character has ground speed = ground decel * 1 frame and move intention x < 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -pc_data.ground_decel_frame2
- pc.move_intention.x = 1
- pc:update_ground_run_speed_by_intention()
- assert.are_equal(0, pc.ground_speed)
- end)
+ -- hypothetical case, only possible with crumbling floor or some moving wall pushing you to fall during spin dash
+ it('(when spin dashing) should enter air_spin state thanks to Falling and Sliding Off condition', function ()
+ pc.motion_state = motion_states.spin_dashing
- it('should set orientation to move intention dir (here, no change) and preserve brake_anim_phase when character decelerates to 0 but no brake anim started', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -pc_data.ground_decel_frame2
- pc.move_intention.x = 1
- pc.brake_anim_phase = 0
+ pc:update_platformer_motion_grounded()
- pc:update_ground_run_speed_by_intention()
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.air_spin)
+ end)
- assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
- assert.are_equal(0, pc.brake_anim_phase)
- end)
+ it('(when rolling, for instance) should set time_left_for_late_jump to delay and late_jump_slope_angle to current slope angle', function ()
+ pc.motion_state = motion_states.rolling
+ pc.slope_angle = 0.25
- it('should set orientation to move intention dir (here, no change) and advance brake_anim_phase to 2 when character decelerates to 0 and brake_start is playing', function ()
- -- in practice, this case doesn't happen, because if you were running backward and started brake anim
- -- by decelerating in the orientation dir, you must have changed dir to the ground speed dir when the brake anim
- -- started so the brake sprite could make sense, so we should be oriented left at this point
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -pc_data.ground_decel_frame2
- pc.move_intention.x = 1
- pc.brake_anim_phase = 1
+ pc:update_platformer_motion_grounded()
- pc:update_ground_run_speed_by_intention()
+ assert.are_equal(pc_data.late_jump_max_delay, pc.time_left_for_late_jump)
+ assert.are_equal(0.25, pc.late_jump_slope_angle)
+ end)
- assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
- end)
+ it('(when rolling, for instance, but late jump feature is disabled) should *not* set time_left_for_late_jump nor late_jump_slope_angle', function ()
+ pc.motion_state = motion_states.rolling
+ pc.slope_angle = 0.25
- it('should decelerate and change sign when character has low ground speed < 0 and move intention x > 0 '..
- 'but the ground speed is high enough so that the new speed wouldn\'t be over the max ground speed', function ()
- pc.orientation = horizontal_dirs.right
- -- start with speed <= ground_accel_frame2 - ground_decel_frame2
- pc.ground_speed = -0.24
- pc.move_intention.x = 1
- pc:update_ground_run_speed_by_intention()
- assert.are_equal(horizontal_dirs.right, pc.orientation)
- assert.is_true(almost_eq_with_message(0.01, pc.ground_speed, 1e-16))
- end)
+ -- dummy app will be recreated on before_each, so it's OK to change this member
+ flow.curr_state.app.get_enable_late_jump_feature = function () return false end
- it('should decelerate and clamp to the max ground speed in the opposite sign '..
- 'when character has low ground speed < 0 and move intention x > 0', function ()
- pc.orientation = horizontal_dirs.right
- -- start with speed > ground_accel_frame2 - ground_decel_frame2
- pc.ground_speed = -0.12
- pc.move_intention.x = 1
- pc:update_ground_run_speed_by_intention()
- assert.are_equal(pc_data.ground_accel_frame2, pc.ground_speed)
- end)
+ pc:update_platformer_motion_grounded()
- it('should set orientation to move intention dir (here, no change) and preserve brake_anim_phase when character decelerates to opposite sign but no brake anim started', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -pc_data.ground_decel_frame2 + pc_data.ground_accel_frame2
- pc.move_intention.x = 1
- pc.brake_anim_phase = 0
+ assert.are_equal(0, pc.time_left_for_late_jump)
+ assert.are_equal(0, pc.late_jump_slope_angle)
+ end)
- pc:update_ground_run_speed_by_intention()
+ end)
- assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
- assert.are_equal(0, pc.brake_anim_phase)
- end)
+ describe('(update_ground_speed sets ground speed to -pc_data.ceiling_adherence_min_ground_speed)', function ()
- it('should set orientation to move intention dir (here, no change) and advance brake_anim_phase to 2 when character decelerates to opposite sign and brake_start is playing', function ()
- -- in practice, this case doesn't happen, because if you were running backward and started brake anim
- -- by decelerating in the orientation dir, you must have changed dir to the ground speed dir when the brake anim
- -- started so the brake sprite could make sense, so we should be oriented left at this point
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -pc_data.ground_decel_frame2 + pc_data.ground_accel_frame2
- pc.move_intention.x = 1
- pc.brake_anim_phase = 1
+ -- rely on after_each above to reset new_ground_speed
+ before_each(function ()
+ -- exactly pc_data.ceiling_adherence_min_ground_speed in abs value to test exact comparison
+ new_ground_speed = -pc_data.ceiling_adherence_min_ground_speed
+ end)
- pc:update_ground_run_speed_by_intention()
+ it('should not enter falling (nor air_spin) state, escaping Falling and Sliding Off condition', function ()
+ pc:update_platformer_motion_grounded()
- assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
- end)
+ assert.spy(enter_motion_state_stub).was_not_called()
+ end)
- it('should preserve orientation and brake anim phase when quadrant down and abs ground speed is too low', function ()
- pc.quadrant = directions.down
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -pc_data.brake_anim_min_speed_frame + 0.01
- pc.move_intention.x = 1
- pc.brake_anim_phase = 0
+ end)
- pc:update_ground_run_speed_by_intention()
+ end)
- assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
end)
- it('should preserve orientation and brake anim phase when quadrant right and abs ground speed is high enough', function ()
- pc.quadrant = directions.right
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -pc_data.brake_anim_min_speed_frame
- pc.move_intention.x = 1
- pc.brake_anim_phase = 0
-
- pc:update_ground_run_speed_by_intention()
+ describe('(when compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: 0.5, is_blocked: true, is_falling: false)', function ()
- assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
- end)
+ local compute_ground_motion_result_mock
- it('should set orientation to ground speed dir (here, change direction) and brake anim phase to 1 then play brake sfx when quadrant down and abs ground speed is high enough', function ()
- pc.quadrant = directions.down
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -pc_data.brake_anim_min_speed_frame
- pc.move_intention.x = 1
- pc.brake_anim_phase = 0
+ setup(function ()
+ compute_ground_motion_result_mock = stub(player_char, "compute_ground_motion_result", function (self)
+ return motion.ground_motion_result(
+ location(0, 1),
+ vector(3, 4),
+ 0.5,
+ true,
+ false
+ )
+ end)
+ end)
- pc:update_ground_run_speed_by_intention()
+ teardown(function ()
+ compute_ground_motion_result_mock:revert()
+ end)
- assert.are_same({horizontal_dirs.left, 1}, {pc.orientation, pc.brake_anim_phase})
- end)
+ after_each(function ()
+ compute_ground_motion_result_mock:clear()
+ end)
- it('should apply friction and preserve direction when character has ground speed > 0 and move intention x is 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 1.5
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2},
- {pc.orientation, pc.ground_speed})
- end)
+ it('should reset ground speed and velocity frame to zero (blocked)', function ()
+ pc:update_platformer_motion_grounded()
+ assert.are_same({0, vector.zero()}, {pc.ground_speed, pc.velocity})
+ end)
- it('should apply friction when character has ground speed > 0, move intention x is 0 and character is descending a low slope', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 1.5
- pc.slope_angle = 0.0625
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2},
- {pc.orientation, pc.ground_speed})
- end)
+ it('should not call enter_motion_state (not falling)', function ()
+ pc:update_platformer_motion_grounded()
- it('should apply friction when character has ground speed > 0, move intention x is 0 and character is ascending a steep slope', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 1.5
- pc.slope_angle = 0.125
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2},
- {pc.orientation, pc.ground_speed})
- end)
+ -- implementation
+ assert.spy(enter_motion_state_stub).was_not_called()
+ end)
- -- Original feature (not in SPG): No Friction on Steep Descending Slope
+ it('should set the position to vector(3, 4)', function ()
+ pc:update_platformer_motion_grounded()
+ assert.are_same(vector(3, 4), pc.position)
+ end)
- it('should not apply friction when character has ground speed > 0, move intention x is 0 and character is descending a steep slope', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 1.5
- pc.slope_angle = 1-0.125
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 1.5},
- {pc.orientation, pc.ground_speed})
- end)
+ it('should call player_char.set_ground_tile_location with location(0, 1)', function ()
+ pc:update_platformer_motion_grounded()
+ assert.spy(player_char.set_ground_tile_location).was_called(1)
+ assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(0, 1))
+ end)
- -- End Original feature
+ it('should call set_slope_angle_with_quadrant with 0.5', function ()
+ pc.slope_angle = 1-0.24
+ pc.quadrant = directions.left
+ pc:update_platformer_motion_grounded()
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0.5)
+ end)
- -- bugfix history: missing tests that check the change of sign of ground speed
- it('should apply friction and preserve direction but stop at 0 without changing ground speed sign when character has low ground speed > 0 and move intention x is 0', function ()
- pc.orientation = horizontal_dirs.right
- -- must be < friction
- pc.ground_speed = 0.01
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 0},
- {pc.orientation, pc.ground_speed})
- end)
+ it('should set the run animation playback speed to abs(ground speed) = 0', function ()
+ pc:update_platformer_motion_grounded()
- it('should reset brake_anim_phase from 1 to 0 when character has ground speed > 0, move intention x is 0 and animation has finished', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 0.01
- pc.brake_anim_phase = 1
- pc.anim_spr.playing = false
+ assert.are_equal(0, pc.anim_run_speed)
+ end)
- pc:update_ground_run_speed_by_intention()
+ it('(on ceiling/wall-ceiling) should enter falling state and set horizontal control lock timer thanks to Falling and Sliding Off condition combined with block setting ground speed to 0', function ()
+ pc.slope_angle = 0.25
+ pc.quadrant = directions.right
- assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
- end)
+ pc:update_platformer_motion_grounded()
- it('should *not* reset brake_anim_phase from 1 to 0 when character has ground speed > 0 and move intention x is 0, but animation is still playing', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 0.01
- pc.brake_anim_phase = 2
- pc.anim_spr.playing = true
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling)
- pc:update_ground_run_speed_by_intention()
+ assert.are_equal(pc_data.fall_off_horizontal_control_lock_duration, pc.horizontal_control_lock_timer)
+ end)
- assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
- end)
+ it('(rolling on ceiling/wall-ceiling) should enter air_spin state and set horizontal control lock timer thanks to Falling and Sliding Off condition combined with block setting ground speed to 0', function ()
+ pc.motion_state = motion_states.rolling
+ pc.slope_angle = 0.25
+ pc.quadrant = directions.right
- it('should *not* reset brake_anim_phase from 2 to 0 when character has ground speed > 0 and move intention x is 0, even if animation has finished', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 0.01
- pc.brake_anim_phase = 2
- pc.anim_spr.playing = false
+ pc:update_platformer_motion_grounded()
- pc:update_ground_run_speed_by_intention()
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.air_spin)
- assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
- end)
+ assert.are_equal(pc_data.fall_off_horizontal_control_lock_duration, pc.horizontal_control_lock_timer)
+ end)
- -- tests below seem symmetrical, but the character is actually running backward
+ it('(on slope less than 90 degrees) should not enter falling state but still set horizontal control lock timer', function ()
+ pc.slope_angle = 1-0.24
+ pc.quadrant = directions.right
- it('should apply friction and preserve direction when character has ground speed < 0 and move intention x is 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -1.5
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, -1.5 + pc_data.ground_friction_frame2},
- {pc.orientation, pc.ground_speed})
- end)
+ pc:update_platformer_motion_grounded()
- -- bugfix history: missing tests that check the change of sign of ground speed
- it(' should apply friction but stop at 0 without changing ground speed sign when character has low ground speed < 0 and move intention x is 0', function ()
- pc.orientation = horizontal_dirs.right
- -- must be < friction in abs
- pc.ground_speed = -0.01
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 0},
- {pc.orientation, pc.ground_speed})
- end)
+ assert.spy(enter_motion_state_stub).was_not_called()
- -- in principle we should also check brake anim phases backward running + friction
- -- but there's not much extra change, even orientation simply doesn't change on friction
+ assert.are_equal(pc_data.fall_off_horizontal_control_lock_duration, pc.horizontal_control_lock_timer)
+ end)
- it('should not change ground speed nor direction when ground speed is 0 and move intention x is 0', function ()
- pc.orientation = horizontal_dirs.left
- pc:update_ground_run_speed_by_intention()
- assert.are_same({horizontal_dirs.left, 0},
- {pc.orientation, pc.ground_speed})
end)
- it('should preserve orientation and reset brake_anim_phase from 1 to 0 when character has ground speed 0, move intention x is 0 and animation has finished', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 0
- pc.brake_anim_phase = 1
- pc.anim_spr.playing = false
+ describe('(when compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: nil, is_blocked: false, is_falling: true)', function ()
- pc:update_ground_run_speed_by_intention()
+ local compute_ground_motion_result_mock
- assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
- end)
+ setup(function ()
+ compute_ground_motion_result_mock = stub(player_char, "compute_ground_motion_result", function (self)
+ return motion.ground_motion_result(
+ nil,
+ vector(3, 4),
+ nil,
+ false,
+ true
+ )
+ end)
+ end)
- it('should preserve orientation and *not* reset brake_anim_phase from 1 to 0 when character has ground speed 0 and move intention x is 0, but animation is still playing', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 0
- pc.brake_anim_phase = 2
- pc.anim_spr.playing = true
+ teardown(function ()
+ compute_ground_motion_result_mock:revert()
+ end)
- pc:update_ground_run_speed_by_intention()
+ after_each(function ()
+ compute_ground_motion_result_mock:clear()
+ end)
- assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
- end)
+ -- note: these tests test in particular the behavior of compute_velocity_from_ground_speed,
+ -- but they were written before it was extracted, so just kept like this
- it('should preserve orientation and *not* reset brake_anim_phase from 2 to 0 when character has ground speed 0 and move intention x is 0, even if animation has finished', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 0
- pc.brake_anim_phase = 2
- pc.anim_spr.playing = false
+ it('should keep updated ground speed and set velocity frame according to ground speed (not blocked)', function ()
+ pc:update_platformer_motion_grounded()
+ -- interface: relying on update_ground_speed implementation
+ assert.are_same({-2.5, vector(-2.5, 0)}, {pc.ground_speed, pc.velocity})
+ end)
- pc:update_ground_run_speed_by_intention()
+ it('should keep updated ground speed and set velocity frame according to ground speed and slope if not flat (not blocked)', function ()
+ pc.slope_angle = 1/6 -- cos = 1/2, sin = -sqrt(3)/2, but use the formula directly to support floating errors
+ pc:update_platformer_motion_grounded()
+ -- interface: relying on update_ground_speed implementation
+ assert.are_same({-2.5, vector(-2.5*cos(1/6), 2.5*sqrt(3)/2)}, {pc.ground_speed, pc.velocity})
+ end)
- assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
- end)
+ it('should call enter_motion_state with falling state (falling)', function ()
+ pc:update_platformer_motion_grounded()
- end) -- update_ground_run_speed_by_intention
+ -- implementation
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling)
+ end)
- describe('update_ground_roll_speed_by_intention', function ()
+ it('(when rolling) should call enter_motion_state with air_spin state (falling)', function ()
+ pc.motion_state = motion_states.rolling
- -- really, rolling applies friction at anytime, active deceleration or not
- -- so our tests are really split between two cases: just friction and decel + friction
+ pc:update_platformer_motion_grounded()
- it('should apply friction only when ground speed > 0 and move intention x is 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 1.5
- pc.move_intention.x = 0
- pc:update_ground_roll_speed_by_intention()
- assert.are_equal(1.5 - pc_data.ground_roll_friction_frame2, pc.ground_speed)
- end)
-
- it('should apply friction only and *not* acceleration when ground speed > 0 and move intention x > 0', function ()
- pc.orientation = horizontal_dirs.left
- pc.ground_speed = -1.5
- pc.move_intention.x = -1
- pc:update_ground_roll_speed_by_intention()
- assert.are_equal(-1.5 + pc_data.ground_roll_friction_frame2, pc.ground_speed)
- end)
-
- it('should set orientation forward when ground speed > 0 and move intention x > 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -1.5
- pc.move_intention.x = -1
- pc:update_ground_roll_speed_by_intention()
- assert.are_equal(horizontal_dirs.left, pc.orientation)
- end)
-
- -- in general we do not need to check what happens when applying so much friction/deceleration that we are going to change sign,
- -- simply because when going below continue_roll_min_ground_speed Sonic will stand up at the end of the update
- -- (not here yet though), so it's unlikely he manages to change speed sign while still rolling by decelerating
- -- since he would have to lose 0.25 px/frame in a single frame, while roll decel is 0.0625
- -- however, because update_ground_speed_by_slope is called before, it's possibly in theory with a strong gravity and steep slope...
- -- so we just check that the safety check that blocks the speed at 0 is working
- -- in practice, it simply won't happen because even on a straight wall where gravity is applied at 100%, it's still lower than 0.25
-
- it('should decelerate and stop exactly at speed 0, preserving direction, when character has ground speed < friction in abs move intention x has opposite sign', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = pc_data.ground_roll_friction_frame2 / 2
- pc.move_intention.x = 0
- pc:update_ground_roll_speed_by_intention()
- assert.are_equal(0, pc.ground_speed)
- end)
-
- it('should decelerate *with friction added* keeping orientation when ground speed > 0 and move intention x < 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = 1.5
- pc.move_intention.x = -1
- pc:update_ground_roll_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_roll_decel_frame2 - pc_data.ground_roll_friction_frame2},
- {pc.orientation, pc.ground_speed})
- end)
-
- it('should decelerate *with friction added* keeping orientation when ground speed < 0 and move intention x > 0', function ()
- pc.orientation = horizontal_dirs.left
- pc.ground_speed = -1.5
- pc.move_intention.x = 1
- pc:update_ground_roll_speed_by_intention()
- assert.are_same({horizontal_dirs.left, -1.5 + pc_data.ground_roll_decel_frame2 + pc_data.ground_roll_friction_frame2},
- {pc.orientation, pc.ground_speed})
- end)
-
- -- same remark as above, check clamping just for safety
-
- it('should decelerate and stop exactly at speed 0, preserving direction, when character has ground speed < (roll decel + friction) in abs and move intention x has opposite sign', function ()
- pc.orientation = horizontal_dirs.left
- pc.ground_speed = - (pc_data.ground_roll_friction_frame2 + pc_data.ground_roll_decel_frame2) / 2
- pc.move_intention.x = 1
- pc:update_ground_roll_speed_by_intention()
- assert.are_equal(0, pc.ground_speed)
- end)
-
- -- we do not check what happens when friction is applied so much that we are going to change sign,
- -- for the same reason as above for decel
+ -- implementation
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.air_spin)
+ end)
- -- tests below seem symmetrical, but the character is actually running backward
+ it('should set the position to vector(3, 4)', function ()
+ pc:update_platformer_motion_grounded()
+ assert.are_same(vector(3, 4), pc.position)
+ end)
- it('should apply friction and preserve direction when character has ground speed < 0 and move intention x is 0', function ()
- pc.orientation = horizontal_dirs.right
- pc.ground_speed = -1.5
- pc:update_ground_roll_speed_by_intention()
- assert.are_same({horizontal_dirs.right, -1.5 + pc_data.ground_roll_friction_frame2},
- {pc.orientation, pc.ground_speed})
- end)
+ -- we don't test that ground_tile_location is set to nil
+ -- because we stubbed enter_motion_state which should do it,
+ -- but if it was spied we could test it
- it(' should apply friction but stop at 0 without changing ground speed sign when character has low ground speed < 0 and move intention x is 0', function ()
- pc.orientation = horizontal_dirs.right
- -- must be < ground_roll_friction_frame2 in abs
- pc.ground_speed = -0.01
- pc:update_ground_roll_speed_by_intention()
- assert.are_same({horizontal_dirs.right, 0},
- {pc.orientation, pc.ground_speed})
- end)
+ it('should not call set_slope_angle_with_quadrant (actually called inside enter_motion_state)', function ()
+ pc.slope_angle = 0
+ pc:update_platformer_motion_grounded()
+ -- this only works because enter_motion_state is stubbed
+ -- if it was spied, it would still call set_slope_angle_with_quadrant inside
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_not_called()
+ end)
- end) -- update_ground_roll_speed_by_intention
+ describe('(ground speed is set to 0)', function ()
- describe('clamp_ground_speed', function ()
+ -- rely on after_each above to reset new_ground_speed
+ before_each(function ()
+ new_ground_speed = 0
+ end)
- it('should preserve ground speed when it is not over max running speed in absolute value', function ()
- pc.ground_speed = pc_data.max_running_ground_speed - 0.1
- pc:clamp_ground_speed(0)
- assert.are_equal(pc_data.max_running_ground_speed - 0.1, pc.ground_speed)
- end)
+ it('(on ceiling/wall-ceiling) should NOT set horizontal control lock timer even when falling due to low speed on non-down quadrant because already falling due to no ground detected', function ()
+ pc.ground_speed = 0
+ pc.slope_angle = 0.25
+ pc.quadrant = directions.right
- it('should clamp ground speed to signed max speed if over max running speed in absolute value, and previous speed was 0', function ()
- pc.ground_speed = pc_data.max_running_ground_speed + 1
- pc:clamp_ground_speed(0)
- assert.are_equal(pc_data.max_running_ground_speed, pc.ground_speed)
- end)
+ pc:update_platformer_motion_grounded()
- it('should clamp ground speed to signed max speed if over max running speed in absolute value, and previous speed was also max running speed', function ()
- pc.ground_speed = pc_data.max_running_ground_speed + 1
- pc:clamp_ground_speed(pc_data.max_running_ground_speed)
- assert.are_equal(pc_data.max_running_ground_speed, pc.ground_speed)
- end)
+ -- note that enter_motion_state is still called, but due to normal fall
+ -- this test passes thanks to the extra `not should_fall` condition just before setting horizontal_control_lock_timer
+ assert.are_equal(0, pc.horizontal_control_lock_timer)
+ end)
- it('should clamp ground speed to previous speed in absolute value if previous speed was higher than max running speed in abs', function ()
- pc.ground_speed = pc_data.max_running_ground_speed + 2
- pc:clamp_ground_speed(pc_data.max_running_ground_speed + 1)
- assert.are_equal(pc_data.max_running_ground_speed + 1, pc.ground_speed)
- end)
+ end)
- it('should allow decreasing ground speed in absolute value if previous speed was higher than max running speed in abs', function ()
- pc.ground_speed = pc_data.max_running_ground_speed + 5
- pc:clamp_ground_speed(pc_data.max_running_ground_speed + 10)
- assert.are_equal(pc_data.max_running_ground_speed + 5, pc.ground_speed)
end)
- end)
-
- describe('compute_ground_motion_result', function ()
-
- describe('(when ground_speed is 0)', function ()
+ describe('(when compute_ground_motion_result returns a motion result with position vector(3, 4), slope_angle: nil, is_blocked: true, is_falling: true)', function ()
- -- bugfix history:
- -- + method was returning a tuple instead of a table
- it('should return the current ground tile location, position and slope, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 1)
- pc.position = vector(3, 4)
- pc.slope_angle = 0.125
+ local compute_ground_motion_result_mock
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
+ setup(function ()
+ compute_ground_motion_result_mock = stub(player_char, "compute_ground_motion_result", function (self)
+ return motion.ground_motion_result(
+ nil,
vector(3, 4),
- 0.125,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
+ nil,
+ true,
+ true
+ )
+ end)
end)
- it('should preserve position subpixels if any', function ()
- pc.ground_tile_location = location(0, 1)
- pc.position = vector(3.5, 4)
- pc.slope_angle = 0.125
+ teardown(function ()
+ compute_ground_motion_result_mock:revert()
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(3.5, 4),
- 0.125,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
+ after_each(function ()
+ compute_ground_motion_result_mock:clear()
end)
- it('(wall right) should return the current position and slope, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 1)
- pc.position = vector(3, 4.5)
- pc.quadrant = directions.right
- pc.slope_angle = 0.25
+ it('should reset ground speed and velocity frame to zero (blocked)', function ()
+ pc:update_platformer_motion_grounded()
+ assert.are_same({0, vector.zero()}, {pc.ground_speed, pc.velocity})
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(3, 4.5),
- 0.25,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
+ it('should call enter_motion_state with falling state (falling)', function ()
+ pc:update_platformer_motion_grounded()
+
+ -- implementation
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.falling)
end)
- it('(ceiling) should return the current position and slope, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 1)
- pc.position = vector(3, 4.5)
- pc.quadrant = directions.up
- pc.slope_angle = 0.5
+ it('(when rolling) should call enter_motion_state with air_spin state (falling)', function ()
+ pc.motion_state = motion_states.rolling
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(3, 4.5),
- 0.5,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
+ pc:update_platformer_motion_grounded()
+
+ -- implementation
+ assert.spy(enter_motion_state_stub).was_called(1)
+ assert.spy(enter_motion_state_stub).was_called_with(match.ref(pc), motion_states.air_spin)
end)
- it('(wall left) should return the current position and slope, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 1)
- pc.position = vector(3, 4.5)
- pc.quadrant = directions.left
- pc.slope_angle = 0.75
+ it('should set the position to vector(3, 4)', function ()
+ pc:update_platformer_motion_grounded()
+ assert.are_same(vector(3, 4), pc.position)
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(3, 4.5),
- 0.75,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
+ it('should not call set_slope_angle_with_quadrant (actually called inside enter_motion_state)', function ()
+ pc.slope_angle = 0
+ pc:update_platformer_motion_grounded()
+ -- this only works because enter_motion_state is stubbed
+ -- if it was spied, it would still call set_slope_angle_with_quadrant inside
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_not_called()
end)
end)
- describe('(when _next_ground_step moves motion_result.position by 1px in the quadrant_horizontal_dir without blocking nor falling)', function ()
+ describe('(when compute_ground_motion_result returns a motion result with position vector(*2.5*, 4), slope_angle: 0, is_blocked: false, is_falling: false)', function ()
- local next_ground_step_mock
+ local compute_ground_motion_result_mock
setup(function ()
- next_ground_step_mock = stub(player_char, "next_ground_step", function (self, quadrant_horizontal_dir, motion_result)
- local step_vec = self:quadrant_rotated(horizontal_dir_vectors[quadrant_horizontal_dir])
- motion_result.position = motion_result.position + step_vec
- -- to simplify, we say the new tile location is where the new position is
- -- to be exact, it should be at the location of sensor closest to ground,
- -- which may be in a different location that center position, but easier for testing
- motion_result.tile_location = motion_result.position:to_location()
- motion_result.slope_angle = (world.quadrant_to_right_angle(self.quadrant) - 0.01) % 1
+ stub(player_char, "compute_ground_motion_result", function (self)
+ return motion.ground_motion_result(
+ location(-1, 0),
+ vector(2.5, 4), -- flr(2.5) must be < pc_data.ground_sensor_extent_x
+ 0,
+ false,
+ false
+ )
end)
end)
teardown(function ()
- next_ground_step_mock:revert()
+ player_char.compute_ground_motion_result:revert()
end)
- -- bugfix history:
- -- + failed because case where we add subpixels without reaching next full pixel didn't set slope_angle
- -- ?? failed I tried to fix it (see above), but actually subpixels should not be taken into account for ground slope detection
- it('(vector(3, 4) at speed 0.5) should return vector(3.5, 4), slope: 0, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.ground_speed = 0.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 0
- -- but as there is no blocking, the remaining subpixels will still be added
-
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(3.5, 4),
- 0, -- character has not moved by a full pixel, so visible position and slope remains the same
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
+ after_each(function ()
+ player_char.compute_ground_motion_result:clear()
end)
- -- bugfix history:
- -- ?? same reason as test above
- it('(vector(3, 4) at speed 1 on slope cos 0.5) should return vector(3.5, 4), is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = 1 -- * slope cos = 0.5
+ it('should clamp character position X to stage left boundary (including half-width offset)', function ()
+ pc:update_platformer_motion_grounded()
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(3.5, 4),
- 1-1/6, -- character has not moved by a full pixel, so visible position and slope remains the same
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
+ -- in practice, clamped to 3
+ assert.are_same(ceil(pc_data.ground_sensor_extent_x), pc.position.x)
end)
- it('(vector(3.5, 4) at speed 0.5) should return vector(0.5, 4), is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3.5, 4)
- pc.ground_speed = 0.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 1
-
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(4, 4),
- 1-0.01,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ it('should clamp the ground speed to -0.1', function ()
+ -- note that we didn't set move intention
+ -- so character will decel to -2.5 this frame, but enough to test clamping
+ pc.ground_speed = -3
- it('(vector(3, 4) at speed -2.5) should return vector(0.5, 4), is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.ground_speed = -2.5
+ pc:update_platformer_motion_grounded()
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(0.5, 4),
- 1-0.01,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
+ assert.are_equal(-0.1, pc.ground_speed)
end)
- it('(right wall, vector(3, 4) at speed 2 (going up) on slope cos 0.5) should return vector(3, 3), is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.quadrant = directions.right
- pc.slope_angle = 0.25-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = 2 -- * slope cos = 1
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(3, 3),
- 0.25-0.01, -- character has not moved by a full pixel, so visible position and slope remains the same
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ end) -- update_platformer_motion_grounded
- it('(ceiling, vector(3, 4) at speed 2 (going up) on slope cos 0.5) should return vector(2, 4), is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.quadrant = directions.up
- pc.slope_angle = 0.5-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = 2 -- * slope cos = 1
+ describe('update_ground_speed', function ()
- -- unfortunately native Lua has small calculation errors
- -- so we must check for almost equal on result position x
- local result = pc:compute_ground_motion_result()
- assert.is_true(almost_eq_with_message(2, result.position.x))
+ setup(function ()
+ -- the only reason we spy and not stub is to test the interface in the first test below
+ spy.on(player_char, "update_ground_speed_by_slope")
+ spy.on(player_char, "update_ground_run_speed_by_intention")
+ spy.on(player_char, "update_ground_roll_speed_by_intention")
+ spy.on(player_char, "clamp_ground_speed")
+ end)
- -- then set that position to expected value and check the rest
- -- with an are_equal to cover all members
- result.position.x = 2
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(2, 4),
- 0.5-0.01,
- false,
- false
- ),
- result
- )
- end)
+ teardown(function ()
+ player_char.update_ground_speed_by_slope:revert()
+ player_char.update_ground_run_speed_by_intention:revert()
+ player_char.update_ground_roll_speed_by_intention:revert()
+ player_char.clamp_ground_speed:revert()
+ end)
- it('(left wall, vector(3, 4) at speed 2 (going down) on slope cos 0.5) should return vector(3, 5), is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.quadrant = directions.left
- pc.slope_angle = 0.75-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = 2 -- * slope cos = 1
+ after_each(function ()
+ player_char.update_ground_speed_by_slope:clear()
+ player_char.update_ground_run_speed_by_intention:clear()
+ player_char.update_ground_roll_speed_by_intention:clear()
+ player_char.clamp_ground_speed:clear()
+ end)
- -- unfortunately native Lua has small calculation errors
- -- so we must check for almost equal on result position y
- local result = pc:compute_ground_motion_result()
- assert.is_true(almost_eq_with_message(5, result.position.y))
+ -- usually we'd only test the interface (calls)
+ -- but since we cannot easily test the call order with spies,
+ -- we do a mini itest to check the resulting velocity,
+ -- which will prove that slope factor is applied before intention
- -- then set that position to expected value and check the rest
- -- with an are_equal to cover all members
- result.position.y = 5
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(3, 5),
- 0.75-0.01,
- false,
- false
- ),
- result
- )
- end)
+ it('(standing) should apply descending slope factor, then oppose it with strong decel when moving in the ascending direction of 45-degree slope from ground speed 0', function ()
+ -- interface: check overall behavior (mini integration test)
+ pc.ground_speed = 0
+ pc.slope_angle = 1/8 -- 45 deg ascending
+ pc.move_intention.x = 1
+ pc:update_ground_speed()
+ -- Note that we have fixed the classic Sonic exploit of decelerating faster when accelerating backward from ground speed 0,
+ -- so the speed will still be clamped to ground accel on this frame, and not become
+ -- - pc_data.slope_accel_factor_frame2 * sin(-1/8) + pc_data.ground_decel_frame2
+ assert.are_equal(pc_data.ground_accel_frame2, pc.ground_speed)
end)
- describe('(when _next_ground_step moves motion_result.position by 1px in the quadrant_quadrant_horizontal_dir, but blocks when motion_result.position.x < -4 (moving left) or x >= 5 (moving right) or y < -4 (moving up) or y >= 5 (moving down))', function ()
+ it('(standing) should update ground speed based on slope, then intention', function ()
+ pc.ground_speed = 2.5
- local next_ground_step_mock
+ pc:update_ground_speed()
- setup(function ()
- next_ground_step_mock = stub(player_char, "next_ground_step", function (self, quadrant_horizontal_dir, motion_result)
-
- local step_vec = self:quadrant_rotated(horizontal_dir_vectors[quadrant_horizontal_dir])
- -- x/y < -4 <=> x/y <= -5 for an integer as passed to step functions,
- -- but we want to make clear that flooring is asymmetrical
- -- and that for floating coordinates, -4.01 is already hitting the left wall
- if motion_result.position.x < -4 and step_vec.x < 0 or motion_result.position.x >= 5 and step_vec.x > 0 or
- motion_result.position.y < -4 and step_vec.y < 0 or motion_result.position.y >= 5 and step_vec.y > 0 then
- motion_result.is_blocked = true
- else
- motion_result.position = motion_result.position + step_vec
- -- to simplify, we say the new tile location is where the new position is
- motion_result.tile_location = motion_result.position:to_location()
- motion_result.slope_angle = (world.quadrant_to_right_angle(self.quadrant) + 0.01) % 1
- end
- end)
- end)
+ assert.spy(player_char.update_ground_speed_by_slope).was_called(1)
+ assert.spy(player_char.update_ground_speed_by_slope).was_called_with(match.ref(pc))
+ assert.spy(player_char.update_ground_run_speed_by_intention).was_called(1)
+ assert.spy(player_char.update_ground_run_speed_by_intention).was_called_with(match.ref(pc))
+ assert.spy(player_char.clamp_ground_speed).was_called(1)
+ assert.spy(player_char.clamp_ground_speed).was_called_with(match.ref(pc), 2.5)
+ end)
- teardown(function ()
- next_ground_step_mock:revert()
- end)
+ it('(crouching) should update ground speed based on slope, like standing (we check intention but should do nothing)', function ()
+ pc.ground_speed = 2.5
- it('(vector(3.5, 4) at speed 1.5) should return vector(5, 4), slope before blocked, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3.5, 4)
- pc.ground_speed = 1.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 2
+ pc:update_ground_speed()
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(5, 4),
- 0.01,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ assert.spy(player_char.update_ground_speed_by_slope).was_called(1)
+ assert.spy(player_char.update_ground_speed_by_slope).was_called_with(match.ref(pc))
+ -- move intention x should be 0, so this is called but should do nothing
+ assert.spy(player_char.update_ground_run_speed_by_intention).was_called(1)
+ assert.spy(player_char.update_ground_run_speed_by_intention).was_called_with(match.ref(pc))
+ assert.spy(player_char.clamp_ground_speed).was_called(1)
+ assert.spy(player_char.clamp_ground_speed).was_called_with(match.ref(pc), 2.5)
+ end)
- it('(vector(-3.5, 4) at speed -1.5) should return vector(-5, 4), slope before blocked, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(-1, 0)
- pc.position = vector(-3.5, 4)
- pc.ground_speed = -1.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 2
+ it('(rolling) should call update_ground_roll_speed_by_intention (instead of _run_)', function ()
+ pc.motion_state = motion_states.rolling
- assert.are_same(motion.ground_motion_result(
- location(-1, 0),
- vector(-5, 4),
- 0.01,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_speed()
- -- bugfix history: + the test revealed that is_blocked should be false when just touching a wall on arrival
- -- so I added a check to only check a wall on an extra column farther if there are subpixels left in motion
- it('(vector(4.5, 4) at speed 0.5) should return vector(5, 4), slope before blocked, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(4.5, 4)
- pc.ground_speed = 0.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 1
+ assert.spy(player_char.update_ground_speed_by_slope).was_called(1)
+ assert.spy(player_char.update_ground_speed_by_slope).was_called_with(match.ref(pc))
+ assert.spy(player_char.update_ground_roll_speed_by_intention).was_called(1)
+ assert.spy(player_char.update_ground_roll_speed_by_intention).was_called_with(match.ref(pc))
+ assert.spy(player_char.clamp_ground_speed).was_not_called()
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(5, 4),
- 0.01,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ it('(spin dashing) should not do anything', function ()
+ pc.motion_state = motion_states.spin_dashing
- -- the negative motion equivalent is not symmetrical due to flooring
- it('(vector(-4, 4) at speed -0.1) should return vector(-5, 4), slope before blocked, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(-1, 0)
- pc.position = vector(-4, 4)
- pc.ground_speed = -1
- -- we assume _compute_max_pixel_distance is correct, so it should return 1
+ pc:update_ground_speed()
- assert.are_same(motion.ground_motion_result(
- location(-1, 0),
- vector(-5, 4),
- 0.01,
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ assert.spy(player_char.update_ground_speed_by_slope).was_not_called()
+ assert.spy(player_char.update_ground_run_speed_by_intention).was_not_called()
+ assert.spy(player_char.update_ground_roll_speed_by_intention).was_not_called()
+ assert.spy(player_char.clamp_ground_speed).was_not_called()
+ end)
- -- bugfix history: < replaced self.ground_speed with distance_x in are_subpixels_left evaluation
- it('(vector(4.5, 4) at speed 1 on slope cos 0.5) should return vector(5, 4), is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- -- this is the same as the test above (we just reach the wall edge without being blocked),
- -- but we make sure that are_subpixels_left check takes the slope factor into account
- pc.position = vector(4.5, 4)
- pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = 1 -- * slope cos = -0.5
+ end) -- update_ground_speed
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(5, 4),
- 0.01, -- new slope angle, no relation with initial one
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ describe('update_ground_speed_by_slope', function ()
- -- the negative motion equivalent is not symmetrical due to flooring
- -- in particular, to update the slope angle, we need to change of full pixel
- it('(vector(-4, 4) at speed -2 on slope cos 0.5) should return vector(-5, 4), is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(-1, 0)
- pc.position = vector(-4, 4)
- pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = -2 -- * slope cos = -1
+ it('should preserve ground speed on flat ground', function ()
+ pc.ground_speed = 2
+ pc.slope_angle = 0
+ pc.ascending_slope_time = 77
- assert.are_same(motion.ground_motion_result(
- location(-1, 0),
- vector(-5, 4),
- 0.01, -- new slope angle, no relation with initial one
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_speed_by_slope(1.8)
- it('(vector(4, 4) at speed 1.5) should return vector(5, 4), slope before blocked, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(4, 4)
- pc.ground_speed = 1.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 1
- -- the character will just touch the wall but because it has some extra subpixels
- -- going "into" the wall, we floor them and consider character as blocked
- -- (unlike Classic Sonic that would simply ignore subpixels)
+ assert.are_equal(2, pc.ground_speed)
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(5, 4),
- 0.01,
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ assert.are_same({
+ 2,
+ 0
+ },
+ {
+ pc.ground_speed,
+ pc.ascending_slope_time
+ })
+ end)
- it('(vector(-4, 4) at speed -1.5) should return vector(-5, 4), slope before blocked, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(-1, 0)
- pc.position = vector(-4, 4)
- pc.ground_speed = -1.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 1
- -- the character will just touch the wall but because it has some extra subpixels
- -- going "into" the wall, we floor them and consider character as blocked
- -- (unlike Classic Sonic that would simply ignore subpixels)
+ -- Original feature (not in SPG): Progressive Ascending Steep Slope Factor
- assert.are_same(motion.ground_motion_result(
- location(-1, 0),
- vector(-5, 4),
- 0.01,
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ it('should accelerate toward left on a steep ascending slope, with very reduced slope factor at the beginning of the climb, and increase ascending slope time', function ()
+ pc.ground_speed = 2
+ pc.slope_angle = 0.125 -- sin(0.125) = -sqrt(2)/2
+ pc.ascending_slope_time = 0
- -- bugfix history:
- -- ?? same reason as test far above where "character has not moved by a full pixel" so slope should not change
- it('(vector(4, 4) at speed 1.5 on slope cos 0.5) should return vector(4.75, 4), slope before blocked, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(4, 4)
- pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = 1.5 -- * slope cos = 0.75
- -- this time, due to the slope cos, charaacter doesn't reach the wall and is not blocked
+ pc:update_ground_speed_by_slope(1.8)
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(4.75, 4),
- 1-1/6, -- character has not moved by a full pixel, so visible position and slope remains the same
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
-
- it('(vector(-4.1, 4) at speed -1.5 on slope cos 0.5) should return vector(-4.85, 4), slope before blocked, is_blocked: false, is_falling: false', function ()
- -- start under -4 so we don't change full pixel and preserve slope angle
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(-4.1, 4)
- pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = -1.5 -- * slope cos = -0.75
-
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(-4.85, 4),
- 1-1/6, -- character has not moved by a full pixel, so visible position and slope remains the same
- false,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
-
- it('(vector(4, 4) at speed 3 on slope cos 0.5) should return vector(5, 4), slope before blocked, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(4, 4)
- pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = 3 -- * slope cos = 1.5
- -- but here, even with the slope cos, charaacter will hit wall
-
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(5, 4),
- 0.01,
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
-
- it('(vector(-4, 4) at speed 3 on slope cos 0.5) should return vector(-5, 4), slope before blocked, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(-1, 0)
- pc.position = vector(-4, 4)
- pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2
- pc.ground_speed = -3 -- * slope cos = -1.5
-
- assert.are_same(motion.ground_motion_result(
- location(-1, 0),
- vector(-5, 4),
- 0.01,
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
-
- -- bugfix history:
- -- + it failed until I added the subpixels check at the end of the method
- -- (also fixed in v1: subpixel cut when max_column_distance is 0 and blocked on next column)
- it('(vector(5, 4) at speed 0.5) should return vector(5, 4), slope before moving, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(5, 4)
- pc.ground_speed = 0.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 0
- -- the character is already touching the wall, so any motion, even of just a few subpixels,
- -- is considered blocked
-
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(5, 4),
- 0, -- character couldn't move at all, so we preserved the initial slope angle
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
-
- it('(vector(-5, 4) at speed 0.5) should return vector(-5, 4), slope before moving, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(-5, 4)
- pc.ground_speed = -0.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 0
- -- the character is already touching the wall, so any motion, even of just a few subpixels,
- -- is considered blocked
-
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(-5, 4),
- 0, -- character couldn't move at all, so we preserved the initial slope angle
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ assert.are_same({
+ 2 - delta_time60 / pc_data.progressive_ascending_slope_duration * pc_data.slope_accel_factor_frame2 * sqrt(2)/2,
+ delta_time60
+ },
+ {
+ pc.ground_speed,
+ pc.ascending_slope_time
+ })
+ end)
- it('(vector(5.5, 4) at speed 0.5) should return vector(5, 4), slope before moving, is_blocked: true, is_falling: false', function ()
- -- this is possible e.g. if character walked along 1.5 from x=4
- -- to reduce computation we didn't check an extra column for a wall
- -- at that time, but starting next frame we will, effectively clamping
- -- the character to x=5
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(5.5, 4)
- pc.ground_speed = 0.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 1
- -- but we will be blocked by the wall anyway
+ it('should accelerate toward left on a steep ascending slope, with reduced slope factor before ascending slope duration, and increase ascending slope time', function ()
+ pc.ground_speed = 2
+ pc.slope_angle = 0.125 -- sin(0.125) = -sqrt(2)/2
+ pc.ascending_slope_time = 0.1
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(5, 4), -- this works on the *right* thanks to subpixel cut working "inside" a wall
- 0, -- character couldn't move and went back, so we preserved the initial slope angle
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_speed_by_slope(1.8)
- it('(vector(-5.5, 4) at speed -0.5) should return vector(-6, 4), slope before moving, is_blocked: false, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(-5.5, 4)
- pc.ground_speed = -0.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 1
- -- but we will be blocked by the wall anyway
+ assert.are_same({
+ 2 - (0.1 + delta_time60) / pc_data.progressive_ascending_slope_duration * pc_data.slope_accel_factor_frame2 * sqrt(2)/2,
+ 0.1 + delta_time60
+ },
+ {
+ pc.ground_speed,
+ pc.ascending_slope_time
+ })
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(-6, 4), -- we are already inside the wall, floored to -6
- 0, -- character only snap to floored x, so we preserved the slope angle
- false, -- no wall detected from inside!
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ it('should accelerate toward left on a steep ascending slope, with full slope factor after ascending slope duration, and clamp time to that duration', function ()
+ pc.ground_speed = 2
+ pc.slope_angle = 0.125 -- sin(0.125) = -sqrt(2)/2
+ pc.ascending_slope_time = pc_data.progressive_ascending_slope_duration
- it('(vector(-5.5, 4) at speed -1) should return vector(-6, 4), slope before moving, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(-5.5, 4)
- pc.ground_speed = -1
- -- we assume _compute_max_pixel_distance is correct, so it should return 1
- -- but we will be blocked by the wall anyway
+ pc:update_ground_speed_by_slope(1.8)
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(-6, 4), -- we are already inside the wall, floored to -6
- 0, -- character only snap to floored x, so we preserved the slope angle
- true, -- wall detected from inside if moving 1 full pixel toward the next column on the left
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ assert.are_same({
+ 2 - pc_data.slope_accel_factor_frame2 * sqrt(2)/2,
+ pc_data.progressive_ascending_slope_duration
+ },
+ {
+ pc.ground_speed,
+ pc.ascending_slope_time
+ })
+ end)
- it('(vector(3, 4) at speed 3) should return vector(5, 4), slope before blocked, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.ground_speed = 3.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- but because of the blocking, we stop at x=5 instead of 6.5
+ it('should accelerate toward right on a non-steep ascending slope, and reset any ascending slope time', function ()
+ pc.ground_speed = 2
+ pc.slope_angle = 0.0625
+ pc.ascending_slope_time = 77
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(5, 4),
- 0.01,
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_speed_by_slope(1.8)
- it('(vector(-3, 4) at speed -3) should return vector(-5, 4), slope before blocked, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(-1, 0)
- pc.position = vector(-3, 4)
- pc.ground_speed = -3.5
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- but because of the blocking, we stop at x=-5 instead of -6.5
+ assert.are_same({
+ 2 - pc_data.slope_accel_factor_frame2 * sin(-0.0625), -- note that the sin is positive
+ 0
+ },
+ {
+ pc.ground_speed,
+ pc.ascending_slope_time
+ })
+ end)
- assert.are_same(motion.ground_motion_result(
- location(-1, 0),
- vector(-5, 4),
- 0.01,
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ it('should accelerate toward right on an descending slope, with full slope factor, and reset any ascending slope time', function ()
+ pc.ground_speed = 2
+ pc.slope_angle = 1-0.125 -- sin(-0.125) = sqrt(2)/2
+ pc.ascending_slope_time = 77
- it('(right wall, vector(3, -3) at speed 3 (moving up)) should return vector(3, -5), slope before blocked, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(0, -1)
- pc.position = vector(3, -3)
- pc.ground_speed = 3.5
- pc.quadrant = directions.right
- pc.slope_angle = 0.25
+ pc:update_ground_speed_by_slope(1.8)
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- but because of the blocking, we stop at y=-5 instead of -6.5
+ assert.are_same({
+ 2 + pc_data.slope_accel_factor_frame2 * sqrt(2)/2,
+ 0
+ },
+ {
+ pc.ground_speed,
+ pc.ascending_slope_time
+ })
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, -1),
- vector(3, -5),
- 0.25 + 0.01,
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ end) -- update_ground_speed_by_slope
- it('(ceiling, vector(-3, 3) at speed 3 (moving left)) should return vector(-5, 3), slope before blocked, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(-1, 0)
- pc.position = vector(-3, 3)
- pc.ground_speed = 3.5
- pc.quadrant = directions.up
- pc.slope_angle = 0.5
+ describe('update_ground_run_speed_by_intention', function ()
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- but because of the blocking, we stop at x=-5 instead of -6.5
+ setup(function ()
+ stub(player_char, "play_low_priority_sfx")
+ end)
- assert.are_same(motion.ground_motion_result(
- location(-1, 0),
- vector(-5, 3),
- 0.5 + 0.01,
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ teardown(function ()
+ player_char.play_low_priority_sfx:revert()
+ end)
- it('(left wall, vector(3, 3) at speed 3 (moving down)) should return vector(3, 5), slope before blocked, is_blocked: true, is_falling: false', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 3)
- pc.ground_speed = 3.5
- pc.quadrant = directions.left
- pc.slope_angle = 0.75
+ after_each(function ()
+ player_char.play_low_priority_sfx:clear()
+ end)
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- but because of the blocking, we stop at y=5 instead of 6.5
+ it('should accelerate and set direction based on new speed when character is facing left, has ground speed 0 and move intention x > 0', function ()
+ pc.orientation = horizontal_dirs.left
+ pc.move_intention.x = 1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, pc_data.ground_accel_frame2},
+ {pc.orientation, pc.ground_speed})
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 0),
- vector(3, 5),
- 0.75 + 0.01,
- true,
- false
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ it('should accelerate and set orientation + reset brake_anim_phase when character is facing left, has ground speed > 0 and move intention x > 0', function ()
+ pc.orientation = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward e.g. after landing on a steep ascending slope and walking backward
+ pc.brake_anim_phase = 1
+ pc.ground_speed = 1.5
+ pc.move_intention.x = 1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 0, 1.5 + pc_data.ground_accel_frame2},
+ {pc.orientation, pc.brake_anim_phase, pc.ground_speed})
+ end)
+ it('should accelerate and preserve direction when character is facing left, has ground speed < 0 and move intention x < 0', function ()
+ pc.orientation = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward e.g. after hitting a spring after landing on a steep ascending slope and walking backward
+ pc.ground_speed = -1.5
+ pc.move_intention.x = -1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.left, -1.5 - pc_data.ground_accel_frame2},
+ {pc.orientation, pc.ground_speed})
end)
- -- bugfix history: the mock was wrong (was using updated position instead of original_position)
- describe('. (when _next_ground_step moves motion_result.position by 1px in the quadrant_horizontal_dir on x/y < 7, falls on 5 <= x/y < 7 and blocks on x/y >= 7 with x/y matching step direction)', function ()
+ it('should decelerate keeping same sign and direction when character is facing right, has high ground speed > ground accel * 1 frame and move intention x < 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 1.5
+ pc.move_intention.x = -1
+ pc:update_ground_run_speed_by_intention()
+ -- ground_decel_frame2 = 0.25, subtract it from ground_speed
+ assert.are_same({horizontal_dirs.right, 1.25},
+ {pc.orientation, pc.ground_speed})
+ end)
- local next_ground_step_mock
+ -- Original feature (not in SPG): Reduced Deceleration on Steep Descending Slope
- setup(function ()
- next_ground_step_mock = stub(player_char, "next_ground_step", function (self, quadrant_horizontal_dir, motion_result)
- local step_vec = self:quadrant_rotated(horizontal_dir_vectors[quadrant_horizontal_dir])
- local original_position = motion_result.position
- -- quadrant_rotated busted implementation has perfect precision, so don't worry about checking ~= 0
- if step_vec.x ~= 0 then
- if original_position.x < 7 then
- motion_result.position = original_position + step_vec
- motion_result.slope_angle = 0.25
- end
- if original_position.x >= 5 then
- if original_position.x < 7 then
- motion_result.is_falling = true
- motion_result.slope_angle = nil -- mimic actual implementation
- else
- motion_result.is_blocked = true
- end
- end
- else -- moving on y (quadrant is left or right)
- if original_position.y < 7 then
- motion_result.position = original_position + step_vec
- motion_result.slope_angle = 0.25
- end
- if original_position.y >= 5 then
- if original_position.y < 7 then
- motion_result.is_falling = true
- motion_result.slope_angle = nil -- mimic actual implementation
- else
- motion_result.is_blocked = true
- end
- end
- end
+ it('should decelerate with decel descending slope factor, keeping same sign and direction when character is on steep descending slope facing right, has high ground speed > ground accel * 1 frame and move intention x < 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 1.5
+ pc.move_intention.x = -1
+ pc.slope_angle = 1-0.125
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_decel_descending_slope_factor * pc_data.ground_decel_frame2},
+ {pc.orientation, pc.ground_speed})
+ end)
- if motion_result.is_falling then
- -- falling, no tile should be set (or we'll assert in ground_motion_result:init!)
- motion_result.tile_location = nil
- else
- -- to simplify, we say the new tile location is where the new position is
- -- normally we only *modify* tile_location when there is actually some motion
- -- but result is the same as long as the initial tile location matched the position
- motion_result.tile_location = motion_result.position:to_location()
- end
- end)
- end)
+ it('should decelerate without decel descending slope factor, keeping same sign and direction when character is on non-steep descending slope facing right, has high ground speed > ground accel * 1 frame and move intention x < 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 1.5
+ pc.move_intention.x = -1
+ pc.slope_angle = 1-0.0625
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_decel_frame2},
+ {pc.orientation, pc.ground_speed})
+ end)
- teardown(function ()
- next_ground_step_mock:revert()
- end)
+ -- End Original feature
- it('(vector(3, 4) at speed 3) should return nil, vector(6, 4), slope_angle: nil, is_blocked: false, is_falling: true', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.ground_speed = 3
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- we are falling but not blocked, so we continue running in the air until x=6
+ it('should decelerate and stop exactly at speed 0, when character has ground speed = ground decel * 1 frame and move intention x < 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = pc_data.ground_decel_frame2
+ pc.move_intention.x = -1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_equal(0, pc.ground_speed)
+ end)
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(6, 4),
- nil,
- false,
- true
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ -- test orientation and brake anim phase together as they are related to visuals
- it('(vector(3, 4) at speed 5) should return nil, vector(7, 4), slope_angle: nil, is_blocked: true, is_falling: true', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.ground_speed = 5
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- we are falling then blocked on 7
+ it('should set orientation to move intention dir (here, *change orientation*) and preserve brake_anim_phase when character decelerates exactly to 0 but no brake anim started', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = pc_data.ground_decel_frame2
+ pc.move_intention.x = -1
+ pc.brake_anim_phase = 0
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(7, 4),
- nil,
- true,
- true
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_run_speed_by_intention()
- it('(right wall, vector(4, 3) at speed -3 (moving down)) should return nil, vector(4, 6), slope_angle: nil, is_blocked: false, is_falling: true', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(4, 3)
- pc.ground_speed = -3
- pc.quadrant = directions.right
- pc.slope_angle = 0.25
+ assert.are_same({horizontal_dirs.left, 0}, {pc.orientation, pc.brake_anim_phase})
+ end)
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- we are falling but not blocked, so we continue running in the air until y=6
+ it('should set orientation to move intention dir (here, *change orientation*) and advance brake_anim_phase to 2 when character decelerates exactly to 0 but and brake_start is playing', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = pc_data.ground_decel_frame2
+ pc.move_intention.x = -1
+ pc.brake_anim_phase = 1
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(4, 6),
- nil,
- false,
- true
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_run_speed_by_intention()
- it('(right wall, vector(4, 3) at speed -5 (moving down)) should return nil, vector(7, 4), slope_angle: nil, is_blocked: true, is_falling: true', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(4, 3)
- pc.ground_speed = -5
- pc.quadrant = directions.right
- pc.slope_angle = 0.25
+ assert.are_same({horizontal_dirs.left, 2}, {pc.orientation, pc.brake_anim_phase})
+ end)
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- we are falling then blocked on 7
+ it('should preserve orientation and brake anim phase when quadrant down and abs ground speed is too low', function ()
+ pc.quadrant = directions.down
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = pc_data.brake_anim_min_speed_frame - 0.01
+ pc.move_intention.x = -1
+ pc.brake_anim_phase = 0
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(4, 7),
- nil,
- true,
- true
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_run_speed_by_intention()
- it('(ceiling, vector(3, 4) at speed -3 (moving right)) should return nil, vector(4, 6), slope_angle: nil, is_blocked: false, is_falling: true', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.ground_speed = -3
- pc.quadrant = directions.up
- pc.slope_angle = 0.5
+ assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
+ end)
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- we are falling but not blocked, so we continue running in the air until x=6
+ it('should preserve orientation and brake anim phase when quadrant right and abs ground speed is high enough', function ()
+ pc.quadrant = directions.right
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = pc_data.brake_anim_min_speed_frame
+ pc.move_intention.x = -1
+ pc.brake_anim_phase = 0
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(6, 4),
- nil,
- false,
- true
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_run_speed_by_intention()
- it('(ceiling, vector(3, 4) at speed -5 (moving right)) should return nil, vector(7, 4), slope_angle: nil, is_blocked: true, is_falling: true', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(3, 4)
- pc.ground_speed = -5
- pc.quadrant = directions.up
- pc.slope_angle = 0.5
+ assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
+ end)
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- we are falling then blocked on 7
+ it('should set orientation to ground speed dir (here, no change) and brake anim phase to 1 then play brake low priority sfx when quadrant down and abs ground speed is high enough', function ()
+ pc.quadrant = directions.down
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = pc_data.brake_anim_min_speed_frame
+ pc.move_intention.x = -1
+ pc.brake_anim_phase = 0
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(7, 4),
- nil,
- true,
- true
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_run_speed_by_intention()
- it('(left wall, vector(4, 3) at speed 3 (moving down)) should return nil, vector(4, 6), slope_angle: nil, is_blocked: false, is_falling: true', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(4, 3)
- pc.ground_speed = 3
- pc.quadrant = directions.left
- pc.slope_angle = 0.75
+ assert.are_same({horizontal_dirs.right, 1}, {pc.orientation, pc.brake_anim_phase})
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- we are falling but not blocked, so we continue running in the air until y=6
+ assert.spy(player_char.play_low_priority_sfx).was_called(1)
+ assert.spy(player_char.play_low_priority_sfx).was_called_with(match.ref(pc), audio.sfx_ids.brake)
+ end)
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(4, 6),
- nil,
- false,
- true
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ -- bugfix history:
+ -- _ missing tests that check the change of sign of ground speed
+ it('should decelerate and start moving to the left when character is facing right, '..
+ 'has low ground speed > 0 but < ground accel * 1 frame and move intention x < 0 '..
+ 'but the ground speed is high enough so that the new speed wouldn\'t be over the max ground speed', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.brake_anim_phase = true
+ -- start with speed >= -ground_accel_frame2 + ground_decel_frame2 but still < ground_decel_frame2
+ pc.ground_speed = 0.24
+ pc.move_intention.x = -1
+ pc:update_ground_run_speed_by_intention()
+ assert.is_true(almost_eq_with_message(-0.01, pc.ground_speed, 1e-16))
+ end)
- it('(left wall, vector(4, 3) at speed 5 (moving down)) should return nil, vector(7, 4), slope_angle: nil, is_blocked: true, is_falling: true', function ()
- pc.ground_tile_location = location(0, 0)
- pc.position = vector(4, 3)
- pc.ground_speed = 5
- pc.quadrant = directions.left
- pc.slope_angle = 0.75
+ it('should decelerate and start moving to the left, and clamp to the max ground speed in the opposite sign '..
+ 'when character is facing right, has low ground speed > 0 and move intention x < 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.should_play_brake_start_anim = true
+ -- start with speed < -ground_accel_frame2 + ground_decel_frame2
+ pc.ground_speed = 0.12
+ pc.move_intention.x = -1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_equal(-pc_data.ground_accel_frame2, pc.ground_speed)
+ end)
- -- we assume _compute_max_pixel_distance is correct, so it should return 3
- -- we are falling then blocked on 7
+ it('should should set orientation to move intention dir (here, change orientation) and preserve brake_anim_phase when character decelerates to opposite sign but no brake anim started', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = pc_data.ground_decel_frame2 - pc_data.ground_accel_frame2
+ pc.move_intention.x = -1
+ pc.brake_anim_phase = 0
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(4, 7),
- nil,
- true,
- true
- ),
- pc:compute_ground_motion_result()
- )
- end)
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.left, 0}, {pc.orientation, pc.brake_anim_phase})
+ assert.are_equal(0, pc.brake_anim_phase)
end)
- end) -- _compute_ground_motion_result
-
- describe('next_ground_step', function ()
+ it('should should set orientation to move intention dir (here, change orientation) and advance brake_anim_phase to 2 when character decelerates to opposite sign but and brake_start is playing', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = pc_data.ground_decel_frame2 - pc_data.ground_accel_frame2
+ pc.move_intention.x = -1
+ pc.brake_anim_phase = 1
- -- for these utests, we assume that _compute_ground_sensors_query_info and
- -- _is_blocked_by_ceiling are correct,
- -- so rather than mocking them, so we setup simple tiles to walk on
+ pc:update_ground_run_speed_by_intention()
- describe('(with flat ground)', function ()
+ assert.are_equal(2, pc.brake_anim_phase)
+ end)
- before_each(function ()
- -- .
- -- #
- mock_mset(0, 1, tile_repr.full_tile_id) -- full tile
- end)
+ -- tests below seem symmetrical, but as a twist we have the character running backward (e.g. after a reverse jump)
+ -- so he's facing the opposite direction of the run, so we can test direction update
- -- in the tests below, we can use pc_data.center_height_standing directly instead
- -- of pc:get_center_height()
- -- because the character is not compact (e.g. no air spin)
+ -- in addition, character faces ground speed dir again when brake_start anim is played,
+ -- which can only be tested when running backward
- it('when stepping left with the right sensor still on the ground, decrement x', function ()
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(-1, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ it('should decelerate keeping same sign when character is facing right, has mid ground speed < 0 but not abs higher than brake_anim_min_speed_frame and move intention x > 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -1.5
+ pc.move_intention.x = 1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_equal(-1.25, pc.ground_speed)
+ end)
- -- step flat
- pc:next_ground_step(horizontal_dirs.left, motion_result)
+ it('should when character has ground speed = ground decel * 1 frame and move intention x < 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -pc_data.ground_decel_frame2
+ pc.move_intention.x = 1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_equal(0, pc.ground_speed)
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(-2, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- ),
- motion_result
- )
- end)
+ it('should decelerate and stop exactly at speed 0 when character has ground speed = ground decel * 1 frame and move intention x < 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -pc_data.ground_decel_frame2
+ pc.move_intention.x = 1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_equal(0, pc.ground_speed)
+ end)
- it('when stepping right with the left sensor still on the ground, increment x', function ()
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(9, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ it('should set orientation to move intention dir (here, no change) and preserve brake_anim_phase when character decelerates to 0 but no brake anim started', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -pc_data.ground_decel_frame2
+ pc.move_intention.x = 1
+ pc.brake_anim_phase = 0
- -- step flat
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ pc:update_ground_run_speed_by_intention()
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(10, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- ),
- motion_result
- )
- end)
+ assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
+ assert.are_equal(0, pc.brake_anim_phase)
+ end)
- it('when stepping left leaving the ground, decrement x and fall', function ()
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(-2, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ it('should set orientation to move intention dir (here, no change) and advance brake_anim_phase to 2 when character decelerates to 0 and brake_start is playing', function ()
+ -- in practice, this case doesn't happen, because if you were running backward and started brake anim
+ -- by decelerating in the orientation dir, you must have changed dir to the ground speed dir when the brake anim
+ -- started so the brake sprite could make sense, so we should be oriented left at this point
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -pc_data.ground_decel_frame2
+ pc.move_intention.x = 1
+ pc.brake_anim_phase = 1
- -- step fall
- pc:next_ground_step(horizontal_dirs.left, motion_result)
+ pc:update_ground_run_speed_by_intention()
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(-3, 8 - pc_data.center_height_standing),
- nil,
- false,
- true
- ),
- motion_result
- )
- end)
+ assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
+ end)
- it('when stepping right leaving the ground, increment x and fall', function ()
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(10, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ it('should decelerate and change sign when character has low ground speed < 0 and move intention x > 0 '..
+ 'but the ground speed is high enough so that the new speed wouldn\'t be over the max ground speed', function ()
+ pc.orientation = horizontal_dirs.right
+ -- start with speed <= ground_accel_frame2 - ground_decel_frame2
+ pc.ground_speed = -0.24
+ pc.move_intention.x = 1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_equal(horizontal_dirs.right, pc.orientation)
+ assert.is_true(almost_eq_with_message(0.01, pc.ground_speed, 1e-16))
+ end)
- -- step fall
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ it('should decelerate and clamp to the max ground speed in the opposite sign '..
+ 'when character has low ground speed < 0 and move intention x > 0', function ()
+ pc.orientation = horizontal_dirs.right
+ -- start with speed > ground_accel_frame2 - ground_decel_frame2
+ pc.ground_speed = -0.12
+ pc.move_intention.x = 1
+ pc:update_ground_run_speed_by_intention()
+ assert.are_equal(pc_data.ground_accel_frame2, pc.ground_speed)
+ end)
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(11, 8 - pc_data.center_height_standing),
- nil,
- false,
- true
- ),
- motion_result
- )
- end)
+ it('should set orientation to move intention dir (here, no change) and preserve brake_anim_phase when character decelerates to opposite sign but no brake anim started', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -pc_data.ground_decel_frame2 + pc_data.ground_accel_frame2
+ pc.move_intention.x = 1
+ pc.brake_anim_phase = 0
- -- this behaviour changed with #132, we don't step down after falling when just touching ground
- it('when stepping right after fall but just touching ground, increment x but do not cancel fall', function ()
- local motion_result = motion.ground_motion_result(
- nil,
- vector(-3, 8 - pc_data.center_height_standing),
- nil,
- false,
- true
- )
+ pc:update_ground_run_speed_by_intention()
- -- step land (very rare)
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
+ assert.are_equal(0, pc.brake_anim_phase)
+ end)
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(-2, 8 - pc_data.center_height_standing),
- nil,
- false,
- true -- still falling
- ),
- motion_result
- )
- end)
+ it('should set orientation to move intention dir (here, no change) and advance brake_anim_phase to 2 when character decelerates to opposite sign and brake_start is playing', function ()
+ -- in practice, this case doesn't happen, because if you were running backward and started brake anim
+ -- by decelerating in the orientation dir, you must have changed dir to the ground speed dir when the brake anim
+ -- started so the brake sprite could make sense, so we should be oriented left at this point
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -pc_data.ground_decel_frame2 + pc_data.ground_accel_frame2
+ pc.move_intention.x = 1
+ pc.brake_anim_phase = 1
- -- we still wanted to test the case where character lands back on ground (what happened
- -- above before #132 fix), but we need to place another tile just high enough so step
- -- gets at least 1px inside ground... a bit cumbersome, so we cheat and assume character
- -- is 1px lower from the start (although impossible as it should have left ground),
- -- so character steps up
- it('when stepping right back on the ground, increment x and cancel fall', function ()
- local motion_result = motion.ground_motion_result(
- nil,
- vector(-3, 9 - pc_data.center_height_standing),
- nil,
- false,
- true
- )
+ pc:update_ground_run_speed_by_intention()
- -- step land (very rare)
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(-2, 8 - pc_data.center_height_standing), -- go up 1px to step up ground again
- 0,
- false,
- false -- landed back after a step falling
- ),
- motion_result
- )
- end)
+ it('should preserve orientation and brake anim phase when quadrant down and abs ground speed is too low', function ()
+ pc.quadrant = directions.down
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -pc_data.brake_anim_min_speed_frame + 0.01
+ pc.move_intention.x = 1
+ pc.brake_anim_phase = 0
- -- for other quadrants we only test the most common cases
+ pc:update_ground_run_speed_by_intention()
- it('(right wall) when stepping q-right (up) with the q-left sensor still on the ground, DEcrement y', function ()
- pc.quadrant = directions.right
- pc.slope_angle = 0.25
+ assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
+ end)
- -- remember to place the character on the left of the tile at (0, 1) as if walking on its left side
- -- this means the center offset should be subtracted from X this time
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(0 - pc_data.center_height_standing, 12),
- 0,
- false,
- false
- )
+ it('should preserve orientation and brake anim phase when quadrant right and abs ground speed is high enough', function ()
+ pc.quadrant = directions.right
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -pc_data.brake_anim_min_speed_frame
+ pc.move_intention.x = 1
+ pc.brake_anim_phase = 0
- -- step flat
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ pc:update_ground_run_speed_by_intention()
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(0 - pc_data.center_height_standing, 11),
- 0.25,
- false,
- false
- ),
- motion_result
- )
- end)
+ assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
+ end)
- it('(right wall) when stepping q-right (up) with the q-left sensor leaving the ground, DEcrement y and fall', function ()
- pc.quadrant = directions.right
- pc.slope_angle = 0.25
+ it('should set orientation to ground speed dir (here, change direction) and brake anim phase to 1 then play brake sfx when quadrant down and abs ground speed is high enough', function ()
+ pc.quadrant = directions.down
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -pc_data.brake_anim_min_speed_frame
+ pc.move_intention.x = 1
+ pc.brake_anim_phase = 0
- -- remember to place the character on the left of the tile at (0, 1) as if walking on its left side
- -- this means the center offset should be subtracted from X this time
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(0 - pc_data.center_height_standing, 6),
- 0,
- false,
- false
- )
+ pc:update_ground_run_speed_by_intention()
- -- step fall
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ assert.are_same({horizontal_dirs.left, 1}, {pc.orientation, pc.brake_anim_phase})
+ end)
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(0 - pc_data.center_height_standing, 5),
- nil,
- false,
- true
- ),
- motion_result
- )
- end)
+ it('should apply friction and preserve direction when character has ground speed > 0 and move intention x is 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 1.5
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2},
+ {pc.orientation, pc.ground_speed})
+ end)
- it('(ceiling) when stepping q-right (left) with the q-left sensor still on the ground, DEcrement x', function ()
- pc.quadrant = directions.up
- pc.slope_angle = 0.25
+ it('should apply friction when character has ground speed > 0, move intention x is 0 and character is descending a low slope', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 1.5
+ pc.slope_angle = 0.0625
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2},
+ {pc.orientation, pc.ground_speed})
+ end)
- -- remember to place the character on the left of the tile at (0, 1) as if walking on its left side
- -- this means the center offset should be subtracted from X this time
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(-1, 16 + pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ it('should apply friction when character has ground speed > 0, move intention x is 0 and character is ascending a steep slope', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 1.5
+ pc.slope_angle = 0.125
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2},
+ {pc.orientation, pc.ground_speed})
+ end)
- -- step flat
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ -- Original feature (not in SPG): No Friction on Steep Descending Slope
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(-2, 16 + pc_data.center_height_standing),
- 0.5,
- false,
- false
- ),
- motion_result
- )
- end)
+ it('should not apply friction when character has ground speed > 0, move intention x is 0 and character is descending a steep slope', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 1.5
+ pc.slope_angle = 1-0.125
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 1.5},
+ {pc.orientation, pc.ground_speed})
+ end)
- it('(ceiling) when stepping q-right (left) with the q-left sensor leaving the ground, DEcrement x and fall', function ()
- pc.quadrant = directions.up
- pc.slope_angle = 0.25
+ -- End Original feature
- -- remember to place the character on the left of the tile at (0, 1) as if walking on its left side
- -- this means the center offset should be subtracted from X this time
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(-2, 16 + pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ -- bugfix history: missing tests that check the change of sign of ground speed
+ it('should apply friction and preserve direction but stop at 0 without changing ground speed sign when character has low ground speed > 0 and move intention x is 0', function ()
+ pc.orientation = horizontal_dirs.right
+ -- must be < friction
+ pc.ground_speed = 0.01
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 0},
+ {pc.orientation, pc.ground_speed})
+ end)
- -- step fall
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ it('should reset brake_anim_phase from 1 to 0 when character has ground speed > 0, move intention x is 0 and animation has finished', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 0.01
+ pc.brake_anim_phase = 1
+ pc.anim_spr.playing = false
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(-3, 16 + pc_data.center_height_standing),
- nil,
- false,
- true
- ),
- motion_result
- )
- end)
+ pc:update_ground_run_speed_by_intention()
- it('(left wall) when stepping q-right (down) with the q-left sensor still on the ground, INcrement y', function ()
- pc.quadrant = directions.left
- pc.slope_angle = 0.75
+ assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
+ end)
- -- remember to place the character on the left of the tile at (0, 1) as if walking on its left side
- -- this means the center offset should be subtracted from X this time
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(8 + pc_data.center_height_standing, 15),
- 0.75,
- false,
- false
- )
+ it('should *not* reset brake_anim_phase from 1 to 0 when character has ground speed > 0 and move intention x is 0, but animation is still playing', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 0.01
+ pc.brake_anim_phase = 2
+ pc.anim_spr.playing = true
- -- step flat
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ pc:update_ground_run_speed_by_intention()
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(8 + pc_data.center_height_standing, 16),
- 0.75,
- false,
- false
- ),
- motion_result
- )
- end)
+ assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
+ end)
- it('(left wall) when stepping q-right (down) with the q-left sensor leaving the ground, INcrement y and fall', function ()
- pc.quadrant = directions.left
- pc.slope_angle = 0.75
+ it('should *not* reset brake_anim_phase from 2 to 0 when character has ground speed > 0 and move intention x is 0, even if animation has finished', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 0.01
+ pc.brake_anim_phase = 2
+ pc.anim_spr.playing = false
- -- remember to place the character on the left of the tile at (0, 1) as if walking on its left side
- -- this means the center offset should be subtracted from X this time
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(0 - pc_data.center_height_standing, 16),
- 0,
- false,
- false
- )
+ pc:update_ground_run_speed_by_intention()
- -- step fall
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
+ end)
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(0 - pc_data.center_height_standing, 17),
- nil,
- false,
- true
- ),
- motion_result
- )
- end)
+ -- tests below seem symmetrical, but the character is actually running backward
+ it('should apply friction and preserve direction when character has ground speed < 0 and move intention x is 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -1.5
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, -1.5 + pc_data.ground_friction_frame2},
+ {pc.orientation, pc.ground_speed})
end)
- describe('(with walls)', function ()
+ -- bugfix history: missing tests that check the change of sign of ground speed
+ it(' should apply friction but stop at 0 without changing ground speed sign when character has low ground speed < 0 and move intention x is 0', function ()
+ pc.orientation = horizontal_dirs.right
+ -- must be < friction in abs
+ pc.ground_speed = -0.01
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 0},
+ {pc.orientation, pc.ground_speed})
+ end)
- before_each(function ()
- -- # #
- -- ###
- mock_mset(0, 0, tile_repr.full_tile_id) -- full tile (left wall)
- mock_mset(0, 1, tile_repr.full_tile_id) -- full tile
- mock_mset(1, 1, tile_repr.full_tile_id) -- full tile
- mock_mset(2, 0, tile_repr.full_tile_id) -- full tile
- mock_mset(2, 1, tile_repr.full_tile_id) -- full tile (right wall)
- end)
-
- it('when stepping left and hitting the wall, preserve x and block', function ()
- local motion_result = motion.ground_motion_result(
- location(1, 1),
- vector(11, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ -- in principle we should also check brake anim phases backward running + friction
+ -- but there's not much extra change, even orientation simply doesn't change on friction
- -- step block
- pc:next_ground_step(horizontal_dirs.left, motion_result)
+ it('should not change ground speed nor direction when ground speed is 0 and move intention x is 0', function ()
+ pc.orientation = horizontal_dirs.left
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.left, 0},
+ {pc.orientation, pc.ground_speed})
+ end)
- assert.are_same(motion.ground_motion_result(
- location(1, 1),
- vector(11, 8 - pc_data.center_height_standing),
- 0,
- true,
- false
- ),
- motion_result
- )
- end)
+ it('should preserve orientation and reset brake_anim_phase from 1 to 0 when character has ground speed 0, move intention x is 0 and animation has finished', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 0
+ pc.brake_anim_phase = 1
+ pc.anim_spr.playing = false
- it('when stepping right and hitting the wall, preserve x and block', function ()
- local motion_result = motion.ground_motion_result(
- location(1, 1),
- vector(13, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ pc:update_ground_run_speed_by_intention()
+
+ assert.are_same({horizontal_dirs.right, 0}, {pc.orientation, pc.brake_anim_phase})
+ end)
- -- step block
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ it('should preserve orientation and *not* reset brake_anim_phase from 1 to 0 when character has ground speed 0 and move intention x is 0, but animation is still playing', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 0
+ pc.brake_anim_phase = 2
+ pc.anim_spr.playing = true
- assert.are_same(motion.ground_motion_result(
- location(1, 1),
- vector(13, 8 - pc_data.center_height_standing),
- 0,
- true,
- false
- ),
- motion_result
- )
- end)
+ pc:update_ground_run_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
end)
- describe('(with wall without ground below)', function ()
+ it('should preserve orientation and *not* reset brake_anim_phase from 2 to 0 when character has ground speed 0 and move intention x is 0, even if animation has finished', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 0
+ pc.brake_anim_phase = 2
+ pc.anim_spr.playing = false
- before_each(function ()
- -- #
- -- #
- mock_mset(0, 1, tile_repr.full_tile_id) -- full tile (ground)
- mock_mset(1, 0, tile_repr.full_tile_id) -- full tile (wall without ground below)
- end)
-
- -- it will fail until compute_closest_ground_query_info
- -- detects upper-level tiles as suggested in the note
- it('when stepping right on the ground and hitting the non-supported wall, preserve x and block', function ()
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(5, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ pc:update_ground_run_speed_by_intention()
- -- step block
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ assert.are_same({horizontal_dirs.right, 2}, {pc.orientation, pc.brake_anim_phase})
+ end)
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(5, 8 - pc_data.center_height_standing),
- 0,
- true,
- false
- ),
- motion_result
- )
- end)
+ end) -- update_ground_run_speed_by_intention
- end)
+ describe('update_ground_roll_speed_by_intention', function ()
- describe('(with head wall)', function ()
+ -- really, rolling applies friction at anytime, active deceleration or not
+ -- so our tests are really split between two cases: just friction and decel + friction
- before_each(function ()
- -- #
- -- =
- mock_mset(0, 1, tile_repr.half_tile_id) -- bottom half-tile
- mock_mset(1, 0, tile_repr.full_tile_id) -- full tile (head wall)
- end)
+ it('should apply friction only when ground speed > 0 and move intention x is 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 1.5
+ pc.move_intention.x = 0
+ pc:update_ground_roll_speed_by_intention()
+ assert.are_equal(1.5 - pc_data.ground_roll_friction_frame2, pc.ground_speed)
+ end)
- -- it will fail until compute_closest_ground_query_info
- -- detects upper-level tiles as suggested in the note
- it('when stepping right on the half-tile and hitting the head wall, preserve x and block', function ()
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(5, 12 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ it('should apply friction only and *not* acceleration when ground speed > 0 and move intention x > 0', function ()
+ pc.orientation = horizontal_dirs.left
+ pc.ground_speed = -1.5
+ pc.move_intention.x = -1
+ pc:update_ground_roll_speed_by_intention()
+ assert.are_equal(-1.5 + pc_data.ground_roll_friction_frame2, pc.ground_speed)
+ end)
+
+ it('should set orientation forward when ground speed > 0 and move intention x > 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -1.5
+ pc.move_intention.x = -1
+ pc:update_ground_roll_speed_by_intention()
+ assert.are_equal(horizontal_dirs.left, pc.orientation)
+ end)
- -- step block
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ -- in general we do not need to check what happens when applying so much friction/deceleration that we are going to change sign,
+ -- simply because when going below continue_roll_min_ground_speed Sonic will stand up at the end of the update
+ -- (not here yet though), so it's unlikely he manages to change speed sign while still rolling by decelerating
+ -- since he would have to lose 0.25 px/frame in a single frame, while roll decel is 0.0625
+ -- however, because update_ground_speed_by_slope is called before, it's possibly in theory with a strong gravity and steep slope...
+ -- so we just check that the safety check that blocks the speed at 0 is working
+ -- in practice, it simply won't happen because even on a straight wall where gravity is applied at 100%, it's still lower than 0.25
- assert.are_same(motion.ground_motion_result(
- location(0, 1),
- vector(5, 12 - pc_data.center_height_standing),
- 0,
- true,
- false
- ),
- motion_result
- )
- end)
+ it('should decelerate and stop exactly at speed 0, preserving direction, when character has ground speed < friction in abs move intention x has opposite sign', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = pc_data.ground_roll_friction_frame2 / 2
+ pc.move_intention.x = 0
+ pc:update_ground_roll_speed_by_intention()
+ assert.are_equal(0, pc.ground_speed)
+ end)
+ it('should decelerate *with friction added* keeping orientation when ground speed > 0 and move intention x < 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = 1.5
+ pc.move_intention.x = -1
+ pc:update_ground_roll_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_roll_decel_frame2 - pc_data.ground_roll_friction_frame2},
+ {pc.orientation, pc.ground_speed})
end)
- -- added to test fix #132 BUG MOTION/VISUAL running from flat to steep descending slope causes glitch
- describe('(with steepest curve then flat ground)', function ()
+ it('should decelerate *with friction added* keeping orientation when ground speed < 0 and move intention x > 0', function ()
+ pc.orientation = horizontal_dirs.left
+ pc.ground_speed = -1.5
+ pc.move_intention.x = 1
+ pc:update_ground_roll_speed_by_intention()
+ assert.are_same({horizontal_dirs.left, -1.5 + pc_data.ground_roll_decel_frame2 + pc_data.ground_roll_friction_frame2},
+ {pc.orientation, pc.ground_speed})
+ end)
- before_each(function ()
- -- ..
- -- i#
- mock_mset(0, 1, tile_repr.visual_loop_bottomright_steepest)
- mock_mset(1, 1, tile_repr.full_tile_id)
- end)
+ -- same remark as above, check clamping just for safety
- -- case: step fall due to angle
- it('when stepping from flat ground onto very steep descending ground, angle diff is enough to still fall', function ()
- local motion_result = motion.ground_motion_result(
- location(1, 1),
- vector(6, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ it('should decelerate and stop exactly at speed 0, preserving direction, when character has ground speed < (roll decel + friction) in abs and move intention x has opposite sign', function ()
+ pc.orientation = horizontal_dirs.left
+ pc.ground_speed = - (pc_data.ground_roll_friction_frame2 + pc_data.ground_roll_decel_frame2) / 2
+ pc.move_intention.x = 1
+ pc:update_ground_roll_speed_by_intention()
+ assert.are_equal(0, pc.ground_speed)
+ end)
- -- step to left "onto" curve (actually above)
- pc:next_ground_step(horizontal_dirs.left, motion_result)
+ -- we do not check what happens when friction is applied so much that we are going to change sign,
+ -- for the same reason as above for decel
- assert.are_same(motion.ground_motion_result(
- nil,
- vector(5, 8 - pc_data.center_height_standing),
- nil,
- false,
- true -- now falling
- ),
- motion_result
- )
- end)
+ -- tests below seem symmetrical, but the character is actually running backward
+ it('should apply friction and preserve direction when character has ground speed < 0 and move intention x is 0', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.ground_speed = -1.5
+ pc:update_ground_roll_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, -1.5 + pc_data.ground_roll_friction_frame2},
+ {pc.orientation, pc.ground_speed})
end)
- -- added after fixing #132 as I noticed my angle comparison was incorrect when one angle was above 0, and the other just below 0 (~0.9)
- -- it was fixed by using the new compute_signed_angle_between
- describe('(with steepest curve then flat ground)', function ()
+ it(' should apply friction but stop at 0 without changing ground speed sign when character has low ground speed < 0 and move intention x is 0', function ()
+ pc.orientation = horizontal_dirs.right
+ -- must be < ground_roll_friction_frame2 in abs
+ pc.ground_speed = -0.01
+ pc:update_ground_roll_speed_by_intention()
+ assert.are_same({horizontal_dirs.right, 0},
+ {pc.orientation, pc.ground_speed})
+ end)
- before_each(function ()
- -- >-
- mock_mset(0, 0, tile_repr.desc_slope_2px_id)
- mock_mset(1, 0, tile_repr.flat_high_tile_id)
- end)
-
- -- case: no step fall as angle difference is small
- it('when stepping from low descending ground onto flat ground, angle diff is not enough to fall', function ()
- local motion_result = motion.ground_motion_result(
- location(0, 0),
- -- > tile last column has height 6, so gap of 2
- vector(10, 2 - pc_data.center_height_standing),
- atan2(8, 2),
- false,
- false
- )
+ end) -- update_ground_roll_speed_by_intention
- -- step to right onto flat high tile
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ describe('clamp_ground_speed', function ()
- assert.are_same(motion.ground_motion_result(
- location(1, 0),
- vector(11, 2 - pc_data.center_height_standing),
- 0,
- false,
- false -- still grounded
- ),
- motion_result
- )
- end)
+ it('should preserve ground speed when it is not over max running speed in absolute value', function ()
+ pc.ground_speed = pc_data.max_running_ground_speed - 0.1
+ pc:clamp_ground_speed(0)
+ assert.are_equal(pc_data.max_running_ground_speed - 0.1, pc.ground_speed)
+ end)
+ it('should clamp ground speed to signed max speed if over max running speed in absolute value, and previous speed was 0', function ()
+ pc.ground_speed = pc_data.max_running_ground_speed + 1
+ pc:clamp_ground_speed(0)
+ assert.are_equal(pc_data.max_running_ground_speed, pc.ground_speed)
end)
- -- bugfix history:
- -- = itest of player running on flat ground when ascending a slope showed that when removing supporting ground,
- -- character would be blocked at the bottom of the slope, so I isolated just that part into a utest
- describe('(with non-supported ascending slope)', function ()
+ it('should clamp ground speed to signed max speed if over max running speed in absolute value, and previous speed was also max running speed', function ()
+ pc.ground_speed = pc_data.max_running_ground_speed + 1
+ pc:clamp_ground_speed(pc_data.max_running_ground_speed)
+ assert.are_equal(pc_data.max_running_ground_speed, pc.ground_speed)
+ end)
- before_each(function ()
- -- /
- -- #
- mock_mset(0, 1, tile_repr.full_tile_id) -- full tile (ground)
- mock_mset(1, 0, tile_repr.asc_slope_45_id) -- ascending slope 45
- end)
+ it('should clamp ground speed to previous speed in absolute value if previous speed was higher than max running speed in abs', function ()
+ pc.ground_speed = pc_data.max_running_ground_speed + 2
+ pc:clamp_ground_speed(pc_data.max_running_ground_speed + 1)
+ assert.are_equal(pc_data.max_running_ground_speed + 1, pc.ground_speed)
+ end)
- it('when stepping right from the bottom of the ascending slope, increment x and adjust y', function ()
- local motion_result = motion.ground_motion_result(
- location(0, 1),
- vector(5, 8 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ it('should allow decreasing ground speed in absolute value if previous speed was higher than max running speed in abs', function ()
+ pc.ground_speed = pc_data.max_running_ground_speed + 5
+ pc:clamp_ground_speed(pc_data.max_running_ground_speed + 10)
+ assert.are_equal(pc_data.max_running_ground_speed + 5, pc.ground_speed)
+ end)
- -- step down
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ end)
- assert.are_same(motion.ground_motion_result(
- location(1, 0),
- vector(6, 7 - pc_data.center_height_standing),
- 45/360,
- false,
- false
- ),
- motion_result
- )
- end)
+ describe('compute_velocity_from_ground_speed', function ()
+ it('should return a vector of magnitude |self.ground_speed|, direction along self.slope_angle, following ground speed sign as sense', function ()
+ pc.ground_speed = 3
+ pc.slope_angle = 0.25
+ assert.is_true(almost_eq_with_message(vector(0, -3), pc:compute_velocity_from_ground_speed()))
end)
- describe('(with ascending slope and wall)', function ()
+ -- no need to test other orientations, since the method relies on vector.unit_from_angle,
+ -- so the utests for the latter are already doing the job
- before_each(function ()
- -- # #
- -- #/#
- mock_mset(0, 0, tile_repr.full_tile_id) -- full tile (high wall, needed to block motion to the left as right sensor makes the character quite high on the slope)
- mock_mset(0, 1, tile_repr.full_tile_id) -- full tile (wall)
- mock_mset(1, 1, tile_repr.asc_slope_45_id) -- ascending slope 45
- mock_mset(2, 0, tile_repr.full_tile_id) -- full tile (wall)
- end)
-
- it('when stepping left on the ascending slope without leaving the ground, decrement x and adjust y', function ()
- local motion_result = motion.ground_motion_result(
- location(1, 1),
- vector(12, 9 - pc_data.center_height_standing),
- 45/360,
- false,
- false
- )
+ end)
+
+ describe('compute_ground_motion_result', function ()
+
+ describe('(when ground_speed is 0)', function ()
- -- step down
- pc:next_ground_step(horizontal_dirs.left, motion_result)
+ it('should return the current ground tile location, position and slope, is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(0, 1)
+ pc.position = vector(3, 4)
+ pc.slope_angle = 0.125
assert.are_same(motion.ground_motion_result(
- location(1, 1),
- vector(11, 10 - pc_data.center_height_standing),
- 45/360,
+ location(0, 1),
+ vector(3, 4),
+ 0.125,
false,
false
),
- motion_result
+ pc:compute_ground_motion_result()
)
end)
- -- case: after step fall, we are close to ground again but not inside yet
- -- This variant was added after testing case #132: initial utest passed, but during
- -- actual gameplay, the character sometimes relanded immediately on the slope after taking off,
- -- defeating the feature.
- -- But for the unit test we don't need such a complex scenario, any ground will do.
- it('when already falling from previous step, do not step down', function ()
- local motion_result = motion.ground_motion_result(
- nil, -- no ground
- vector(12, 9 - pc_data.center_height_standing),
- nil, -- no ground, so no angle
- false,
- true -- previously falling
- )
-
- -- step down
- pc:next_ground_step(horizontal_dirs.left, motion_result)
+ it('should preserve position subpixels if any', function ()
+ pc.ground_tile_location = location(0, 1)
+ pc.position = vector(3.5, 4)
+ pc.slope_angle = 0.125
assert.are_same(motion.ground_motion_result(
- nil,
- vector(11, 9 - pc_data.center_height_standing), -- don't step down, so keep Y
- nil,
+ location(0, 1),
+ vector(3.5, 4),
+ 0.125,
false,
- true -- still falling
+ false
),
- motion_result
+ pc:compute_ground_motion_result()
)
end)
- it('when stepping right on the ascending slope without leaving the ground, decrement x and adjust y', function ()
- local motion_result = motion.ground_motion_result(
- location(1, 1),
- vector(12, 9 - pc_data.center_height_standing),
- 45/360,
- false,
- false
- )
-
- -- step up
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ it('(wall right) should return the current position and slope, is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(0, 1)
+ pc.position = vector(3, 4.5)
+ pc.quadrant = directions.right
+ pc.slope_angle = 0.25
assert.are_same(motion.ground_motion_result(
- location(1, 1),
- vector(13, 8 - pc_data.center_height_standing),
- 45/360,
+ location(0, 1),
+ vector(3, 4.5),
+ 0.25,
false,
false
),
- motion_result
+ pc:compute_ground_motion_result()
)
end)
- it('when stepping right on the ascending slope and hitting the right wall, preserve x and y and block', function ()
- local motion_result = motion.ground_motion_result(
- location(1, 1),
- vector(13, 10 - pc_data.center_height_standing),
- -45/360,
- false,
- false
- )
-
- -- step up blocked
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ it('(ceiling) should return the current position and slope, is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(0, 1)
+ pc.position = vector(3, 4.5)
+ pc.quadrant = directions.up
+ pc.slope_angle = 0.5
assert.are_same(motion.ground_motion_result(
- location(1, 1),
- vector(13, 10 - pc_data.center_height_standing),
- -45/360,
- true,
+ location(0, 1),
+ vector(3, 4.5),
+ 0.5,
+ false,
false
),
- motion_result
+ pc:compute_ground_motion_result()
)
end)
- it('when stepping left on the ascending slope and hitting the left wall, preserve x and y and block', function ()
- local motion_result = motion.ground_motion_result(
- location(1, 1),
- vector(11, 10 - pc_data.center_height_standing),
- -45/360,
- false,
- false
- )
-
- -- step down blocked
- pc:next_ground_step(horizontal_dirs.left, motion_result)
+ it('(wall left) should return the current position and slope, is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(0, 1)
+ pc.position = vector(3, 4.5)
+ pc.quadrant = directions.left
+ pc.slope_angle = 0.75
assert.are_same(motion.ground_motion_result(
- location(1, 1),
- vector(11, 10 - pc_data.center_height_standing),
- -45/360,
- true,
+ location(0, 1),
+ vector(3, 4.5),
+ 0.75,
+ false,
false
),
- motion_result
+ pc:compute_ground_motion_result()
)
end)
end)
- describe('(with two tiles in a row)', function ()
+ describe('(when compute_closest_wall_query_info finds no wall in any horizontal direction)', function ()
- before_each(function ()
- -- ##
- mock_mset(0, 0, tile_repr.full_tile_id)
- mock_mset(1, 0, tile_repr.full_tile_id)
+ setup(function ()
+ stub(player_char, "compute_closest_wall_query_info", function (self, next_position, quadrant_horizontal_dir)
+ return motion.ground_query_info(nil, ceil(pc_data.ground_sensor_extent_x) + 1, nil)
+ end)
end)
- it('when stepping right on a new tile, increment x and update tile location to new tile', function ()
- local motion_result = motion.ground_motion_result(
- location(0, 0),
- vector(5, 0 - pc_data.center_height_standing),
- 0,
- false,
- false
- )
+ teardown(function ()
+ player_char.compute_closest_wall_query_info:revert()
+ end)
- pc:next_ground_step(horizontal_dirs.right, motion_result)
+ describe('(when compute_ground_sensors_query_info finds ground at signed distance 0, slope angle 0, at next position)', function ()
+
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, next_position)
+ -- simplified stub that always return location matching position
+ -- in reality, ground sensors are offset on qx so we may detect the tile a little farther than where center is
+ return motion.ground_query_info(next_position:to_location(), 0, 0)
+ end)
+ end)
+
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
+ end)
+
+ -- run on flat ground (same tile)
+ it('(vector(3, 4) at speed 0.5) should return vector(3, 4), slope: 0, is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(0, 0)
+ pc.position = vector(3, 4)
+ pc.ground_speed = 0.5
+
+ assert.are_same(motion.ground_motion_result(
+ location(0, 0),
+ vector(3.5, 4),
+ 0, -- character has not moved by a full pixel, so visible position and slope remains the same
+ false,
+ false
+ ),
+ pc:compute_ground_motion_result()
+ )
+ end)
- assert.are_same(motion.ground_motion_result(
- location(1, 0),
- vector(6, 0 - pc_data.center_height_standing),
- 0,
- false,
- false
- ),
- motion_result
- )
end)
- end)
+ describe('(when compute_ground_sensors_query_info finds ground at signed distance 1, slope angle atan2(8, 4), at next position)', function ()
- end) -- _next_ground_step
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, next_position)
+ return motion.ground_query_info(next_position:to_location(), 1, atan2(8, 4))
+ end)
+ end)
- describe('is_blocked_by_ceiling_at', function ()
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
+ end)
- local get_ground_sensor_position_from_mock
- local compute_closest_ceiling_query_info_mock
+ -- run from flat ground and find new descending slope on next tile
+ it('(vector(7, 4) at speed 3 on slope (8, 4)) should return location(1, 0), vector(7 + 3, 4 + 1), slope: atan2(8, 4), is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(0, 0)
+ pc.position = vector(7, 4)
+ pc.slope_angle = 0
+ pc.ground_speed = 3
+
+ assert.are_same(motion.ground_motion_result(
+ location(1, 0),
+ vector(7 + 3, 4 + 1), -- advance by 3 to the right, then snap down by 1
+ atan2(8, 4),
+ false,
+ false
+ ),
+ pc:compute_ground_motion_result()
+ )
+ end)
- setup(function ()
- get_ground_sensor_position_from_mock = stub(player_char, "get_ground_sensor_position_from", function (self, center_position, i)
- return i == horizontal_dirs.left and vector(-1, center_position.y) or vector(1, center_position.y)
- end)
-
- compute_closest_ceiling_query_info_mock = stub(player_char, "compute_closest_ceiling_query_info", function (self, sensor_position)
- -- simulate ceiling detection by encoding information in x and y
- -- no particular realism in the returned values
- -- remember that 0 <=> touching <=> not blocked
- local signed_distance
- if sensor_position.y == 1 then
- signed_distance = pc_data.max_ground_snap_height + 1 -- to test no collider found, not even touch
- elseif sensor_position.y == 2 then
- signed_distance = sensor_position.x < 0 and -1 or 0 -- left sensor detects inside ceiling, right only touch
- elseif sensor_position.y == 3 then
- signed_distance = sensor_position.x < 0 and 0 or -1 -- right sensor detects inside ceiling, left only touch
- else
- signed_distance = sensor_position.x < 0 and -1 or -1 -- both sensors detect inside ceiling
- end
- if signed_distance <= 0 then
- return ground_query_info(location(0, 0), signed_distance, 0.5)
- else
- return ground_query_info(nil, signed_distance, nil)
- end
end)
- end)
- teardown(function ()
- get_ground_sensor_position_from_mock:revert()
- compute_closest_ceiling_query_info_mock:revert()
- end)
+ describe('(when compute_ground_sensors_query_info finds ground at signed distance 1 - sin(atan2(8, 4)) * 3, slope: 0, at next position)', function ()
- it('should return false when both sensors detect no near ceiling', function ()
- assert.is_false(pc:is_blocked_by_ceiling_at(vector(0, 1)))
- end)
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, next_position)
+ -- signed distance is just the right amount to escape and snap up to an integer y (tile mask column)
+ return motion.ground_query_info(next_position:to_location(), 1 - sin(atan2(8, 4)) * 3, atan2(8, 4))
+ end)
+ end)
- it('should return true when left sensor detects near ceiling', function ()
- assert.is_true(pc:is_blocked_by_ceiling_at(vector(0, 2)))
- end)
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
+ end)
- it('should return true when right sensor detects no near ceiling', function ()
- assert.is_true(pc:is_blocked_by_ceiling_at(vector(0, 3)))
- end)
+ -- run on descending slope and snap up on same tile (1, 1)
+ it('(vector(8, 4) at speed 3 on slope (8, 4)) should return location(1, 0), vector(8 + 3 * cos(atan2(8, 4)), 4 + 1), slope: atan2(8, 4), is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(1, 0)
+ pc.position = vector(8, 4)
+ pc.slope_angle = atan2(8, 4)
+ pc.ground_speed = 3
+
+ assert.are_same(motion.ground_motion_result(
+ location(1, 0),
+ vector(8 + 3 * cos(atan2(8, 4)), 4 + 1), -- we went down by 1 + fraction and snapped up to clear the fraction
+ atan2(8, 4),
+ false,
+ false
+ ),
+ pc:compute_ground_motion_result()
+ )
+ end)
- it('should return true when both sensors detect near ceiling', function ()
- assert.is_true(pc:is_blocked_by_ceiling_at(vector(0, 4)))
- end)
+ end)
- end) -- _is_blocked_by_ceiling_at
+ describe('(when compute_ground_sensors_query_info finds ground at signed distance sin(atan2(8, 4)) * 3, slope: 0, at next position)', function ()
- describe('compute_closest_ceiling_query_info', function ()
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, next_position)
+ -- signed distance = 1 + fraction, it is the right amount to snap down by 1px + just enough to reach flat ground integer y (4)
+ return motion.ground_query_info(next_position:to_location(), sin(atan2(8, 4)) * 3, 0)
+ end)
+ end)
+
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
+ end)
+
+ -- run left, leaving descending slope and snapping down to flat ground
+ it('(vector(8, 4) at speed -3) should return location(1, 0), vector(8 - 3 * cos(atan2(8, 4)), 4), is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(1, 0)
+ pc.position = vector(8, 4)
+ pc.slope_angle = atan2(8, 4)
+ pc.ground_speed = -3
+
+ assert.are_same(motion.ground_motion_result(
+ location(0, 0),
+ vector(8 - 3 * cos(atan2(8, 4)), 4), -- we went down by sin(atan2(8, 4)) * 3 = 1 + fraction
+ 0,
+ false,
+ false
+ ),
+ pc:compute_ground_motion_result()
+ )
+ end)
- setup(function ()
- stub(player_char, "get_full_height", function ()
- return 16
end)
- end)
- teardown(function ()
- player_char.get_full_height:revert()
- end)
+ describe('(when compute_ground_sensors_query_info finds ground at signed distance sin(atan2(8, 4)) * 3, slope: 0, at next position)', function ()
- describe('no tiles)', function ()
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, next_position)
+ -- signed distance = + fraction, it is the right amount to snap down
+ return motion.ground_query_info(next_position:to_location(), 1 - 2 * cos(atan2(4, -8)), atan2(4, -8))
+ end)
+ end)
+
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
+ end)
+
+ -- run on right wall absolute steep ascending slope / low q-descending slope, snap down to px column
+ it('(right wall, vector(3, 4) at speed 2 (going up) on slope (4, -8)) should return location(0, 0), vector(3 + 1, 4 + 2 * sin(atan2(4, -8))), is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(0, 0)
+ pc.position = vector(3, 4)
+ pc.quadrant = directions.right
+ pc.slope_angle = atan2(4, -8)
+ pc.ground_speed = 2
+
+ assert.are_same(motion.ground_motion_result(
+ location(0, 0),
+ vector(3 + 1, 4 + 2 * sin(atan2(4, -8))),
+ atan2(4, -8),
+ false,
+ false
+ ),
+ pc:compute_ground_motion_result()
+ )
+ end)
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) everywhere', function ()
- assert.are_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 5)))
end)
- end)
+ -- we'll skip left wall and ceiling utests, it seems robust enough
- describe('(1 full tile)', function ()
+ -- FALLING due to lack of ground
- before_each(function ()
- -- .#
- mock_mset(1, 0, tile_repr.full_tile_id) -- full tile (act like a full ceiling if position is at bottom)
- end)
+ describe('(when compute_ground_sensors_query_info finds no ground at next position)', function ()
- it('should return ground_query_info(location(1, 0), - character height - 0.1, 0.5) for sensor position just above the bottom-center of the tile', function ()
- -- with new implementation, we check tile even at foot level
- -- remember that we are detection ceiling so quadrant is up, and angle is 0.5 (180 deg)
- assert.are_same(ground_query_info(location(1, 0), -16.1, 0.5), pc:compute_closest_ceiling_query_info(vector(12, 7.9)))
- end)
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, next_position)
+ return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+ end)
+ end)
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position on the left of the tile', function ()
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(7, 8)))
- end)
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
+ end)
- -- bugfix history:
- -- ? i thought that by design, function should return true but realized it was not consistent
- -- ? actually I was right, since if the character moves inside the 2nd of a diagonal tile pattern,
- -- it *must* be blocked. when character has a foot on the lower tile, it is considered to be
- -- in this lower tile
- it('should return ground_query_info(location(1, 0), -character height, 0.5) for sensor position at the bottom-left of the tile', function ()
- assert.is_same(ground_query_info(location(1, 0), -16, 0.5), pc:compute_closest_ceiling_query_info(vector(8, 8)))
- end)
+ -- run from flat ground to void
+ it('(vector(7, 4) at speed 3) should return no location, vector(7, 4), slope: 0, is_blocked: false, is_falling: true', function ()
+ pc.ground_tile_location = location(0, 0)
+ pc.position = vector(7, 4)
+ pc.ground_speed = 3
+
+ assert.are_same(motion.ground_motion_result(
+ nil,
+ vector(7 + 3, 4),
+ nil,
+ false,
+ true
+ ),
+ pc:compute_ground_motion_result()
+ )
+ end)
- it('should return ground_query_info(location(1, 0), -character height, 0.5) for sensor position on the bottom-right of the tile', function ()
- assert.is_same(ground_query_info(location(1, 0), -16, 0.5), pc:compute_closest_ceiling_query_info(vector(15, 8)))
end)
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position on the right of the tile', function ()
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(16, 8)))
- end)
+ -- FALLING OFF due to difference of angle
- it('should return ground_query_info(location(1, 0), -1, 0.5) for sensor position below the tile, at character height - 1px', function ()
- assert.is_same(ground_query_info(location(1, 0), -1, 0.5), pc:compute_closest_ceiling_query_info(vector(12, 8 + 16 - 1)))
- end)
+ describe('(when compute_ground_sensors_query_info finds ground close to step down normally, but angle difference is above fall-off threshold)', function ()
+
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, next_position)
+ -- `- 0.1` to verify < comparison
+ return motion.ground_query_info(location(1, 0), 2, - pc_data.take_off_angle_difference - 0.1)
+ end)
+ end)
+
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
+ end)
+
+ -- run from flat ground to void
+ it('(vector(7, 4) at speed 3) should return no location, vector(7 + 3, 4), slope: - pc_data.take_off_angle_difference, is_blocked: false, is_falling: true', function ()
+ pc.ground_tile_location = location(0, 0)
+ pc.position = vector(7, 4)
+ pc.ground_speed = 3
+
+ assert.are_same(motion.ground_motion_result(
+ nil,
+ vector(7 + 3, 4),
+ - pc_data.take_off_angle_difference - 0.1,
+ false,
+ true
+ ),
+ pc:compute_ground_motion_result()
+ )
+ end)
- -- bugfix history:
- -- < i realized that values of full_height_standing < 8 would fail the test
- -- so i moved the height_distance >= pc_data.full_height_standing check above
- -- the ground_array_height check (computing height_distance from tile bottom instead of top)
- -- to pass it in this case too
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position below the tile, at character height', function ()
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(12, 8 + 16)))
end)
end)
- describe('(1 half-tile)', function ()
+ -- this time we'll focus on wall, so stub the ground check first to return a standard "on ground"
+ -- (we are not too interested in combined cases like hitting wall + falling since rare)
+ describe('(when compute_ground_sensors_query_info finds ground at signed distance 0, slope angle 0, at next position)', function ()
- before_each(function ()
- -- =
- mock_mset(0, 0, tile_repr.half_tile_id)
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, next_position)
+ return motion.ground_query_info(location(1, 0), 0, 0)
+ end)
end)
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position in the middle of the tile', function ()
- -- we now start checking ceiling a few pixels q-above character feet
- -- and ignore reverse full height on same tile as sensor, so slope not detected as ceiling
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 6)))
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
end)
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position at the bottom of the tile', function ()
- -- here we don't detect a ceiling because y = 8 is considered belonging to
- -- tile j = 1, but we define ignore_reverse = start_tile_loc == curr_tile_loc
- -- not ignore_reverse = curr_tile_loc == curr_tile_loc
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 8)))
+ -- note that tests below stub compute_closest_wall_query_info, but since the utests were written,
+ -- we've extracted the block calling it into check_escape_wall_and_update_next_position
+ -- but now that the utests exist and cover all cases, we don't need to rewrite tests to stub
+ -- check_escape_wall_and_update_next_position and make proper utests for check_escape_wall_and_update_next_position
+ -- itself, so we keep testing compute_ground_motion_result
+
+ describe('(when compute_closest_wall_query_info detects just touching wall)', function ()
+
+ setup(function ()
+ -- note that compute_closest_wall_query_info may now be called TWICE
+ -- but we are not changing quadrant here so it won't happen, and anyway the result would be the same the 2nd time
+ stub(player_char, "compute_closest_wall_query_info", function (self, next_position, quadrant_horizontal_dir)
+ -- we simulate a tilemap like this:
+ -- .#
+ -- ##
+ -- where pc starts at (2, 4) on tile (0, 0), and moves by 2px to the right, with front just touching wall on (1, 0)
+ -- we're currently raycasting from center and using convention that signed distance is from center, not front
+ -- so we must offset all signed distances by ceil(pc_data.ground_sensor_extent_x)
+ -- note that since we raycast up to ceil(pc_data.ground_sensor_extent_x) (not +1)
+ -- only the right raycast can find touching wall, left raycast will ignore it due to pixel dissymmetry
+ -- in both cases, we ignore the result
+ -- wall should have slope 0.25 on the right... but it doesn't matter here, we don't use it
+ return motion.ground_query_info(location(1, 1), ceil(pc_data.ground_sensor_extent_x) + 1, 0.25)
+ end)
+ end)
+
+ teardown(function ()
+ player_char.compute_closest_wall_query_info:revert()
+ end)
+
+ -- run on flat ground and just touch wall => NOT blocking yet
+ it('(vector(2, 4) at speed 2) should return vector(2 + 2, 4), slope: 0, is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(0, 0)
+ pc.position = vector(2, 4)
+ pc.ground_speed = 2
+
+ assert.are_same(motion.ground_motion_result(
+ location(1, 0),
+ vector(2 + 2, 4),
+ 0,
+ false,
+ false
+ ),
+ pc:compute_ground_motion_result()
+ )
+ end)
+
end)
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position 2 px below tile (so that 4px above is inside tile)', function ()
- -- this test makes sure that we ignore reverse full height for start tile
- -- *not* sensor tile, which is different when sensor is less than 4px of the neighboring tile
- -- in iteration direction
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 10)))
+ describe('(when compute_closest_wall_query_info finds ground at signed distance -0.2, slope angle 0, at next position)', function ()
+
+ setup(function ()
+ stub(player_char, "compute_closest_wall_query_info", function (self, next_position)
+ -- we simulate a tilemap like this:
+ -- .#
+ -- ##
+ -- where pc starts at (2, 4) on tile (0, 0), and moves by 2.2px to the right, just entering wall on (1, 0)
+ -- 0.1 works too, but causes floating imprecisions in busted
+ return motion.ground_query_info(location(1, 0), ceil(pc_data.ground_sensor_extent_x) - 0.2, 0.25)
+ end)
+ end)
+
+ teardown(function ()
+ player_char.compute_closest_wall_query_info:revert()
+ end)
+
+ -- run on flat ground and entering wall by fraction of pixel => BLOCK
+ it('(vector(2, 4) at speed 2) should return vector(2 + 2, 4), slope: 0, is_blocked: false, is_falling: false', function ()
+ pc.ground_tile_location = location(0, 0)
+ pc.position = vector(2, 4)
+ pc.ground_speed = 2.2 -- extra 0.1 to enter wall, so we can check escape wall
+
+ assert.are_same(motion.ground_motion_result(
+ location(1, 0),
+ vector(2 + 2, 4),
+ 0,
+ true,
+ false
+ ),
+ pc:compute_ground_motion_result()
+ )
+ end)
+
end)
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for quadrant left, offset sensor position (head) 1 px q-outside tile', function ()
- pc.quadrant = directions.left
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(-17, 4)))
+ end) -- stub compute_ground_sensors_query_info
+
+ -- this time don't stub, go with mock map testing, just because we need to test
+ describe('with 2 full flat tiles', function ()
+
+ setup(function ()
+ -- don't stub but still spy.on to verify it's called twice
+ spy.on(player_char, "check_escape_wall_and_update_next_position")
end)
- it('should return ground_query_info(location(0, 0), 0, 0.25) for quadrant left, offset sensor position (head) just touching left of tile', function ()
- pc.quadrant = directions.left
- assert.is_same(ground_query_info(location(0, 0), 0, 0.25), pc:compute_closest_ceiling_query_info(vector(-16, 4)))
+ teardown(function ()
+ player_char.check_escape_wall_and_update_next_position:revert()
end)
- it('should return ground_query_info(location(0, 0), - 1, 0.25) for quadrant left, offset sensor position (head) 1 px reverse-q(right)-inside tile', function ()
+ before_each(function ()
+ -- let's prepare map to test character entering ground diagonally and escaping
+ -- ..
+ -- .\
+ -- Normally at (0, 0) you'd have a steep slope (desc_slope_45_id \ is not enough as 45 degree
+ -- is still considered quadrant down), but I couldn't find a test tile to use.
+ -- Fortunately it doesn't matter, compute_ground_motion_result only cares about the next frame position
+ -- so we only need to set the next tile at (1, 1)
+ -- mock_mset(0, 1, tile_repr.full_tile_id)
+ -- mock_mset(1, 1, tile_repr.full_tile_id)
+ mock_mset(1, 1, tile_repr.visual_loop_bottomleft)
+ -- mock_mset(1, 1, tile_repr.desc_slope_45_id)
+ end)
+
+ -- added to fix #265 BUG PHYSICS character still blocked in loop after powerful spin dash
+ -- character is rolling down the slope
+ -- => center at (10, 1))
+ -- it will enter the flat ground and must escape properly WITHOUT being blockd by wall thx to 2nd wall check block cancel
+ -- this reproduces a real situation encountered at the end of a loop when launched at high speed by spin dash
+ it('(vector(10, 1) at speed 6) should return vector(7 + sqrt(2) * 6, 2), slope: 0, is_blocked: false, is_falling: false', function ()
+ -- simulate a steep tile here as a little before the end of a loop
+ -- quadrant is the most important
+ pc.ground_tile_location = location(0, 0)
pc.quadrant = directions.left
- assert.is_same(ground_query_info(location(0, 0), -1, 0.25), pc:compute_closest_ceiling_query_info(vector(-15, 4)))
- end)
+ pc.slope_angle = atan2(7, 8) -- very much to the right, almost 45 degrees, but still quadrant left
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for quadrant right, when 4 px to the left is outside tile', function ()
- pc.quadrant = directions.right
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(4, 4)))
- end)
+ pc.motion_state = motion_states.rolling
+ pc.position = vector(8, 5) -- no precise calculation, I adjusted until I got the result I wanted...
+ pc.ground_speed = 6
- it('should return ground_query_info(location(0, 0), -character height - 2, 0.5) for quadrant right, offset sensor position (head) 2 px reverse-q(left)-inside tile', function ()
- -- this test makes sure that we do *not* ignore reverse full height for initial tile if
- -- that are full horizontal rectangle (see world.compute_qcolumn_height_at)
- -- since slope_angle_to_interiors has a bias 0 -> right so onceiling check,
- -- we check on left which is reverse of tile interior_h
- -- (if bias was for left, then the test above would check this instead)
- pc.quadrant = directions.right
- -- note that we also detect ceiling on (5, 4) although it is symmetrical to the (3, 4)
- -- test for quadrant left, due to the fact that pixel x = 0 is considered still in tile i = 0
- -- we can fix the disymmetry with some .5 pixel extent in qy in both ground distance and ceiling check
- -- (as in the qx direction with ground sensor extent) but we don't mind since Classic Sonic itself
- -- has an odd size collider in reality
- assert.is_same(ground_query_info(location(0, 0), -18, 0.75), pc:compute_closest_ceiling_query_info(vector(6, 4)))
+ -- result check
+ assert.are_same(motion.ground_motion_result(
+ location(1, 1),
+ vector(16, 7), -- didn't calculate, just watched result and reinjected... wall snap floored y, ground snap (quadrant left) floored x
+ atan2(8, 5),
+ false,
+ false
+ ),
+ pc:compute_ground_motion_result()
+ )
+
+ -- call check
+ -- just to check we used check_escape_wall_and_update_next_position twice indeed
+ assert.spy(player_char.check_escape_wall_and_update_next_position).was_called(2)
+ -- first call with predicted position without obstacles
+ assert.spy(player_char.check_escape_wall_and_update_next_position).was_called_with(match.ref(pc), vector(8 + 6 * cos(atan2(7, 8)), 5 + 6 * sin(atan2(7, 8))), horizontal_dirs.right)
+ -- second call with position after escaping ground
+ assert.spy(player_char.check_escape_wall_and_update_next_position).was_called_with(match.ref(pc), vector(16, 7), horizontal_dirs.right)
end)
end)
- describe('(1 ascending slope 45)', function ()
+ end) -- compute_ground_motion_result
- before_each(function ()
- -- /
- mock_mset(0, 0, tile_repr.asc_slope_45_id)
- end)
+ describe('wants_to_crouch', function ()
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position on the left of the tile', function ()
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(0, 7)))
- end)
+ it('should return false if no move intention', function ()
+ pc:wants_to_crouch()
+ assert.is_false(pc:wants_to_crouch())
+ end)
- it('should return ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil) for sensor position at the bottom-left of the tile', function ()
- -- we now start checking ceiling a few pixels q-above character feet, so slope not detected as ceiling
- assert.is_same(ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil), pc:compute_closest_ceiling_query_info(vector(0, 8)))
- end)
+ it('should return false if move intention is left-down', function ()
+ pc.move_intention.x = -1
+ pc.move_intention.y = 1
+ assert.is_false(pc:wants_to_crouch())
+ end)
+ it('should return true if move intention is pure down', function ()
+ pc.move_intention.x = 0
+ pc.move_intention.y = 1
+ assert.is_true(pc:wants_to_crouch())
end)
- end) -- _compute_closest_ceiling_query_info
+ end)
describe('check_jump_intention', function ()
@@ -6065,14 +5582,17 @@ describe('player_char', function ()
setup(function ()
stub(player_char, "play_low_priority_sfx")
+ stub(player_char, "enter_motion_state") -- we used to check motion state directly, but now we can stub
end)
teardown(function ()
player_char.play_low_priority_sfx:revert()
+ player_char.enter_motion_state:revert()
end)
after_each(function ()
player_char.play_low_priority_sfx:clear()
+ player_char.enter_motion_state:clear()
end)
it('should not set jump members and return false when should_jump is false', function ()
@@ -6080,16 +5600,31 @@ describe('player_char', function ()
local result = pc:check_jump()
-- interface
- assert.are_same({false, vector(4.1, -1), motion_states.standing, false, false}, {result, pc.velocity, pc.motion_state, pc.has_jumped_this_frame, pc.can_interrupt_jump})
+ assert.are_same({false, vector(4.1, -1), false, false}, {result, pc.velocity, pc.has_jumped_this_frame, pc.can_interrupt_jump})
+ end)
+
+ it('should not enter another motion state when should_jump is false', function ()
+ pc.should_jump = false
+ pc:check_jump()
+
+ assert.spy(player_char.enter_motion_state).was_not_called()
end)
- it('should consume should_jump, add initial var jump velocity, update motion state, set has_jumped_this_frame amd can_interrupt_jump flags and return true when should_jump is true', function ()
+ it('should consume should_jump, add initial var jump velocity, set has_jumped_this_frame amd can_interrupt_jump flags and return true when should_jump is true', function ()
pc.velocity = vector(4.1, -1)
pc.should_jump = true
local result = pc:check_jump()
-- interface
- assert.are_same({true, vector(4.1, -4.25), motion_states.air_spin, true, true}, {result, pc.velocity, pc.motion_state, pc.has_jumped_this_frame, pc.can_interrupt_jump})
+ assert.are_same({true, vector(4.1, -4.25), true, true}, {result, pc.velocity, pc.has_jumped_this_frame, pc.can_interrupt_jump})
+ end)
+
+ it('should enter motion state: air_spin', function ()
+ pc.should_jump = true
+ pc:check_jump()
+
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.air_spin)
end)
it('should add impulse along ground normal when slope_angle is not 0 (and we should jump)', function ()
@@ -6103,1726 +5638,1297 @@ describe('player_char', function ()
assert.is_true(almost_eq_with_message(-2 - pc_data.initial_var_jump_speed_frame / sqrt(2), pc.velocity.y))
end)
- it('should play jump low priority sfx when character should jump', function ()
+ it('(should do late jump) should add impulse along previous ground normal using late_jump_slope_angle', function ()
+ pc.velocity = vector(2, -2)
pc.should_jump = true
+ pc.time_left_for_late_jump = 1
+ pc.slope_angle = nil -- unused, and actually nil in the air
+ pc.late_jump_slope_angle = 0.125 -- same angle as above so we can copy the assertions of the utest above
pc:check_jump()
- assert.spy(player_char.play_low_priority_sfx).was_called(1)
- assert.spy(player_char.play_low_priority_sfx).was_called_with(match.ref(pc), audio.sfx_ids.jump)
+ assert.is_true(almost_eq_with_message(2 - pc_data.initial_var_jump_speed_frame / sqrt(2), pc.velocity.x))
+ assert.is_true(almost_eq_with_message(-2 - pc_data.initial_var_jump_speed_frame / sqrt(2), pc.velocity.y))
end)
- end)
-
- describe('update_platformer_motion_airborne', function ()
-
- setup(function ()
- spy.on(player_char, "enter_motion_state")
- spy.on(player_char, "check_hold_jump")
- -- trigger check inside set_ground_tile_location will fail as it needs context
- -- (tile_test_data + mset), so we prefer stubbing as we don't check ground_tile_location directly
- stub(player_char, "set_ground_tile_location")
- spy.on(player_char, "set_slope_angle_with_quadrant")
- end)
+ it('should play jump sfx (low priority) when character should jump', function ()
+ pc.should_jump = true
- teardown(function ()
- player_char.enter_motion_state:revert()
- player_char.check_hold_jump:revert()
- player_char.set_ground_tile_location:revert()
- player_char.set_slope_angle_with_quadrant:revert()
- end)
+ pc:check_jump()
- before_each(function ()
- -- optional, just to enter an airborne state and be in a meaningful state in this context
- pc:enter_motion_state(motion_states.falling)
- -- clear spy just after this instead of after_each to avoid messing the call count
- player_char.enter_motion_state:clear()
- player_char.check_hold_jump:clear()
- player_char.set_ground_tile_location:clear()
- player_char.set_slope_angle_with_quadrant:clear()
+ assert.spy(player_char.play_low_priority_sfx).was_called(1)
+ assert.spy(player_char.play_low_priority_sfx).was_called_with(match.ref(pc), audio.sfx_ids.jump)
end)
- describe('(when _compute_air_motion_result returns a motion result with position vector(2, 8), is_blocked_by_ceiling: false, is_blocked_by_wall: false, is_landing: false)', function ()
-
- setup(function ()
- compute_air_motion_result_mock = stub(player_char, "compute_air_motion_result", function (self)
- return motion.air_motion_result(
- nil,
- vector(4, 8), -- make sure it's far enough from stage left edge to avoid soft clamping
- false,
- false,
- false,
- nil
- )
- end)
- end)
-
- teardown(function ()
- compute_air_motion_result_mock:revert()
- end)
-
- after_each(function ()
- compute_air_motion_result_mock:clear()
- end)
-
- it('should set velocity y to -jump_interrupt_speed_frame on first frame of hop if velocity.y is not already greater, and clear has_jumped_this_frame flag', function ()
- pc.motion_state = motion_states.air_spin
- pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2)
- pc.has_jumped_this_frame = true
- pc.can_interrupt_jump = true
- pc.hold_jump_intention = false
-
- pc:update_platformer_motion_airborne()
-
- -- call check
- assert.spy(player_char.check_hold_jump).was_called(1)
- assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
-
- -- result check
- assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame})
- end)
-
- it('should preserve velocity y completely on first frame of hop if velocity.y is already greater, and clear has_jumped_this_frame flag', function ()
- -- this can happen when character is running down a steep slope, and hops with a normal close to horizontal
- pc.motion_state = motion_states.air_spin
- pc.velocity.y = -1 -- must be >= -pc_data.jump_interrupt_speed_frame (-2)
- pc.has_jumped_this_frame = true
- pc.can_interrupt_jump = true
- pc.hold_jump_intention = false
-
- pc:update_platformer_motion_airborne()
-
- -- call check (but will do nothing)
- assert.spy(player_char.check_hold_jump).was_called(1)
- assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
-
- -- result check
- assert.are_same({-1, false}, {pc.velocity.y, pc.has_jumped_this_frame})
- end)
-
- it('should preserve (supposedly initial jump) velocity y on first frame of jump (not hop) and clear has_jumped_this_frame flag', function ()
- pc.motion_state = motion_states.air_spin
- pc.velocity.y = -3
- pc.has_jumped_this_frame = true
- pc.can_interrupt_jump = true
- pc.hold_jump_intention = true
-
- pc:update_platformer_motion_airborne()
-
- -- call check (but will do nothing)
- assert.spy(player_char.check_hold_jump).was_called(1)
- assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
-
- -- result check
- assert.are_same({-3, false}, {pc.velocity.y, pc.has_jumped_this_frame})
- end)
-
- it('should apply gravity to velocity y when not on first frame of jump and not interrupting jump', function ()
- pc.motion_state = motion_states.air_spin
- pc.velocity.y = -1
- pc.has_jumped_this_frame = false
- pc.can_interrupt_jump = true
- pc.hold_jump_intention = true
-
- pc:update_platformer_motion_airborne()
-
- -- call check (but will do nothing)
- assert.spy(player_char.check_hold_jump).was_called(1)
- assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
-
- -- result check
- assert.are_same({-1 + pc_data.gravity_frame2, false}, {pc.velocity.y, pc.has_jumped_this_frame})
- end)
-
- it('should set to speed y to interrupt speed (no gravity added) when interrupting actual jump', function ()
- pc.motion_state = motion_states.air_spin
- pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2)
- pc.has_jumped_this_frame = false
- pc.can_interrupt_jump = true
- pc.hold_jump_intention = false
-
- pc:update_platformer_motion_airborne()
-
- -- call check
- assert.spy(player_char.check_hold_jump).was_called(1)
- assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
-
- -- result check
- -- note that gravity is applied *before* interrupt jump, so we don't see it in the final velocity.y
- assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame})
- end)
-
- it('should NOT check for speed interrupt at all when running falling (not air_spin)', function ()
- pc.motion_state = motion_states.falling
- pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2)
- pc.has_jumped_this_frame = false
- pc.can_interrupt_jump = true
- pc.hold_jump_intention = false
-
- pc:update_platformer_motion_airborne()
-
- -- call check
- assert.spy(player_char.check_hold_jump).was_not_called()
-
- -- result check
- assert.are_same({-3 + pc_data.gravity_frame2, false}, {pc.velocity.y, pc.has_jumped_this_frame})
- end)
-
- -- unfortunately it's hard to stub clamp_air_velocity_x properly
- -- so we test the content of clamp_air_velocity_x below, which is redundant with its
- -- own utests
- -- it is *possible* to stub clamp_air_velocity_x completely and test that pure air accel x is applied,
- -- and that clamp_air_velocity_x is called with previous velocity x,
- -- although semantically a bit weird as the latter affects velocity.x
-
- it('should apply air accel x', function ()
- pc.velocity.x = 2
- pc.move_intention.x = -1
-
- pc:update_platformer_motion_airborne()
-
- assert.are_equal(2 - pc_data.air_accel_x_frame2, pc.velocity.x)
- end)
-
- it('should apply air accel x but clamp at max air velocity x in abs if not already beyond', function ()
- pc.velocity.x = -pc_data.max_air_velocity_x
- pc.move_intention.x = -1
-
- pc:update_platformer_motion_airborne()
+ end)
- assert.are_equal(- pc_data.max_air_velocity_x, pc.velocity.x)
- end)
+ describe('check_spin_dash', function ()
- it('should apply air accel x but clamp at previous air velocity x in abs if already beyond', function ()
- pc.velocity.x = -pc_data.max_air_velocity_x - 1
- pc.move_intention.x = -1
+ setup(function ()
+ stub(pfx, "start")
+ stub(player_char, "play_low_priority_sfx")
+ stub(player_char, "release_spin_dash")
+ stub(player_char, "enter_motion_state")
+ stub(player_char, "update_sprite_row_and_play_sprite_animation")
+ end)
- pc:update_platformer_motion_airborne()
+ teardown(function ()
+ pfx.start:revert()
+ player_char.play_low_priority_sfx:revert()
+ player_char.release_spin_dash:revert()
+ player_char.enter_motion_state:revert()
+ player_char.update_sprite_row_and_play_sprite_animation:revert()
+ end)
- assert.are_equal(- pc_data.max_air_velocity_x - 1, pc.velocity.x)
- end)
+ -- since pc is init in before_each and init calls setup
+ -- which calls pc.anim_spr:play("idle"), we must clear call count just after that
+ before_each(function ()
+ player_char.update_sprite_row_and_play_sprite_animation:clear()
+ end)
- it('should apply air accel x and allow decreasing air velocity x in abs if already beyond', function ()
- pc.velocity.x = -pc_data.max_air_velocity_x - 10
- pc.move_intention.x = 1
+ after_each(function ()
+ pfx.start:clear()
+ player_char.play_low_priority_sfx:clear()
+ player_char.release_spin_dash:clear()
+ player_char.enter_motion_state:clear()
+ end)
- pc:update_platformer_motion_airborne()
+ it('(standing) should not do anything', function ()
+ pc.motion_state = motion_states.standing
- assert.are_equal(- pc_data.max_air_velocity_x - 10 + pc_data.air_accel_x_frame2, pc.velocity.x)
- end)
+ pc:check_spin_dash()
- it('should set horizontal direction to intended motion direction: left', function ()
- pc.orientation = horizontal_dirs.right
- pc.velocity.x = 4
- pc.move_intention.x = -1
+ assert.spy(player_char.enter_motion_state).was_not_called()
+ assert.spy(player_char.release_spin_dash).was_not_called()
+ end)
- pc:update_platformer_motion_airborne()
+ it('(crouching, release down) should not do anything (standing up is not processed here)', function ()
+ pc.motion_state = motion_states.crouching
- assert.are_equal(horizontal_dirs.left, pc.orientation)
- end)
+ pc:check_spin_dash()
- it('should set horizontal direction to intended motion direction: right', function ()
- pc.orientation = horizontal_dirs.left
- pc.velocity.x = 4
- pc.move_intention.x = 1
+ assert.spy(player_char.enter_motion_state).was_not_called()
+ assert.spy(player_char.release_spin_dash).was_not_called()
+ end)
- pc:update_platformer_motion_airborne()
+ it('(spin dashing, release down) should call release_spin_dash', function ()
+ pc.motion_state = motion_states.spin_dashing
- assert.are_equal(horizontal_dirs.right, pc.orientation)
- end)
+ pc:check_spin_dash()
- it('should clamp velocity Y if beyond limit (positive)', function ()
- pc.velocity.y = 1000
+ assert.spy(player_char.release_spin_dash).was_called(1)
+ assert.spy(player_char.release_spin_dash).was_called_with(match.ref(pc))
+ end)
- pc:update_platformer_motion_airborne()
+ it('(facing right, crouching, keep down with jump intention) should enter spin dashing state, set ground speed to 0 (frozen from here), start smoke pfx', function ()
+ pc.motion_state = motion_states.crouching
+ pc.move_intention.y = 1
+ pc.jump_intention = true
- assert.are_equal(pc_data.max_air_velocity_y, pc.velocity.y)
- end)
+ pc:check_spin_dash()
- -- bugfix history:
- -- .
- it('should update position with air motion result position', function ()
- pc.position = vector(0, 0) -- doesn't matter, since we mock _compute_air_motion_result
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.spin_dashing)
- pc:update_platformer_motion_airborne()
+ assert.are_equal(0, pc.ground_speed)
- assert.are_same(vector(4, 8), pc.position)
- end)
+ assert.spy(pfx.start).was_called(1)
+ assert.spy(pfx.start).was_called_with(match.ref(pc.smoke_pfx), pc.position + vector(0, 5), false)
+ end)
- it('should preserve velocity.y', function ()
- -- set those flags to true to make computations more simple:
- -- velocity.y will not affected by gravity nor interrupt jump
- pc.has_jumped_this_frame = true
- pc.hold_jump_intention = true
- pc.velocity = vector(10, -10)
- pc:update_platformer_motion_airborne()
+ it('(facing right, crouching, keep down with jump intention) should enter spin dashing state, set ground speed to 0 (frozen from here), start smoke pfx (mirrored)', function ()
+ pc.orientation = horizontal_dirs.left
+ pc.motion_state = motion_states.crouching
+ pc.move_intention.y = 1
+ pc.jump_intention = true
- assert.are_equal(-10, pc.velocity.y)
- end)
+ pc:check_spin_dash()
- end) -- compute_air_motion_result_mock (vector(2, 8), false, false, false)
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.spin_dashing)
- describe('(when _compute_air_motion_result returns a motion result with is_blocked_by_wall: false, is_blocked_by_ceiling: true) '..
- '(when apply_air_drag multiplies velocity x by 0.9 no matter what)', function ()
+ assert.are_equal(0, pc.ground_speed)
- setup(function ()
- stub(player_char, "compute_air_motion_result", function (self)
- return motion.air_motion_result(
- nil,
- vector(4, 8),
- false, -- not the focus, but verified
- true, -- focus in this test
- false,
- nil
- )
- end)
- stub(player_char, "apply_air_drag", function (self)
- self.velocity.x = 0.9 * self.velocity.x
- end)
- end)
+ assert.spy(pfx.start).was_called(1)
+ assert.spy(pfx.start).was_called_with(match.ref(pc.smoke_pfx), pc.position + vector(0, 5), true)
+ end)
- teardown(function ()
- player_char.compute_air_motion_result:revert()
- player_char.apply_air_drag:revert()
- end)
+ it('(crouching, keep down with jump intention) should consume jump intention', function ()
+ pc.motion_state = motion_states.crouching
+ pc.move_intention.y = 1
+ pc.jump_intention = true
- after_each(function ()
- player_char.compute_air_motion_result:clear()
- player_char.apply_air_drag:clear()
- end)
+ pc:check_spin_dash()
- it('should set velocity.y to 0', function ()
- -- set those flags to true to make computations more simple:
- -- velocity.y will not affected by gravity nor interrupt jump
- pc.has_jumped_this_frame = true
- pc.hold_jump_intention = true
- pc.velocity = vector(10, -10)
+ assert.is_false(pc.jump_intention)
+ end)
- pc:update_platformer_motion_airborne()
+ it('(crouching, keep down with jump intention) should reset spin dash rev to first increase step EVEN if last spin dash left old value (no friction this frame)', function ()
+ pc.motion_state = motion_states.crouching
+ pc.move_intention.y = 1
+ pc.jump_intention = true
+ pc.spin_dash_rev = 4
- assert.are_equal(0, pc.velocity.y)
- end)
+ pc:check_spin_dash()
- it('should apply air drag, then preserve velocity.x on hit ceiling', function ()
- pc.velocity = vector(10, -10)
+ assert.are_equal(pc_data.spin_dash_rev_increase_step, pc.spin_dash_rev)
+ end)
- pc:update_platformer_motion_airborne()
- -- spy test (should always be called anyway, but only this test really demonstrates X velocity)
- assert.spy(player_char.apply_air_drag).was_called(1)
- assert.spy(player_char.apply_air_drag).was_called_with(match.ref(pc))
+ it('(crouching, release down with jump intention) edge case: should still do first rev (and release only next frame)', function ()
+ pc.motion_state = motion_states.crouching
+ pc.move_intention.y = 0
+ pc.jump_intention = true
- -- value test
- assert.are_equal(9, pc.velocity.x)
- end)
+ pc:check_spin_dash()
- end) -- compute_air_motion_result_mock (is_blocked_by_ceiling: true)
+ assert.is_false(pc.jump_intention)
+ end)
- describe('(when _compute_air_motion_result returns a motion result with is_blocked_by_wall: true, is_blocked_by_ceiling: false)', function ()
+ it('(spin dashing, keep down with jump intention) should increase spin dash by rev increase step (no friction this frame)', function ()
+ pc.motion_state = motion_states.spin_dashing
+ pc.move_intention.y = 1
+ pc.jump_intention = true
+ pc.spin_dash_rev = 4
- setup(function ()
- compute_air_motion_result_mock = stub(player_char, "compute_air_motion_result", function (self)
- return motion.air_motion_result(
- nil,
- vector(4, 8),
- true, -- focus in this test
- false, -- not the focus, but verified
- false,
- nil
- )
- end)
- end)
+ pc:check_spin_dash()
- teardown(function ()
- compute_air_motion_result_mock:revert()
- end)
+ assert.are_equal(4 + pc_data.spin_dash_rev_increase_step, pc.spin_dash_rev)
+ end)
- after_each(function ()
- compute_air_motion_result_mock:clear()
- end)
+ it('(spin dashing, keep down with jump intention) should increase spin dash by rev but clamped to max (no friction this frame)', function ()
+ pc.motion_state = motion_states.spin_dashing
+ pc.move_intention.y = 1
+ pc.jump_intention = true
+ pc.spin_dash_rev = 7.5
- it('should preserve velocity.y', function ()
- -- set those flags to true to make computations more simple:
- -- velocity.y will not affected by gravity nor interrupt jump
- pc.has_jumped_this_frame = true
- pc.hold_jump_intention = true
- pc.velocity = vector(10, -10)
+ pc:check_spin_dash()
- pc:update_platformer_motion_airborne()
+ assert.are_equal(pc_data.spin_dash_rev_max, pc.spin_dash_rev)
+ end)
- assert.are_equal(-10, pc.velocity.y)
- end)
+ it('(crouching or spin dashing, keep down with jump intention) should play spin_dash anim *from start* when spin dashing', function ()
+ pc.motion_state = motion_states.spin_dashing
+ pc.move_intention.y = 1
+ pc.jump_intention = true
- it('should set velocity.x to 0', function ()
- pc.velocity = vector(10, -10)
+ pc:check_spin_dash()
- pc:update_platformer_motion_airborne()
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "spin_dash", true)
+ end)
- assert.are_equal(0, pc.velocity.x)
- end)
+ it('(crouching or spin dashing, keep down with jump intention) should play spin dash rev sfx (low priority)', function ()
+ pc.motion_state = motion_states.spin_dashing
+ pc.move_intention.y = 1
+ pc.jump_intention = true
+
+ pc:check_spin_dash()
+ assert.spy(player_char.play_low_priority_sfx).was_called(1)
+ assert.spy(player_char.play_low_priority_sfx).was_called_with(match.ref(pc), audio.sfx_ids.spin_dash_rev)
end)
- describe('(when _compute_air_motion_result returns a motion result with is_landing: true, slope_angle: 0.5)', function ()
+ it('(spin dashing, keep down without jump intention) should apply friction to spin dash rev', function ()
+ pc.motion_state = motion_states.spin_dashing
+ pc.move_intention.y = 1
+ pc.jump_intention = false
+ pc.spin_dash_rev = 2
- setup(function ()
- compute_air_motion_result_mock = stub(player_char, "compute_air_motion_result", function (self)
- return motion.air_motion_result(
- location(0, 1),
- vector(4, 8),
- false,
- false,
- true, -- focus in this test
- 0.5
- )
- end)
- end)
+ pc:check_spin_dash()
- teardown(function ()
- compute_air_motion_result_mock:revert()
- end)
+ assert.are_equal(2 * pc_data.spin_dash_drag_factor_per_frame, pc.spin_dash_rev)
+ end)
- after_each(function ()
- compute_air_motion_result_mock:clear()
- end)
+ end)
- it('should call player_char.set_ground_tile_location with location(0, 1)', function ()
- pc:update_platformer_motion_airborne()
- assert.spy(player_char.set_ground_tile_location).was_called(1)
- assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(0, 1))
- end)
+ describe('release_spin_dash', function ()
- it('should enter standing state and set_slope_angle_with_quadrant: 0.5', function ()
- pc.slope_angle = 0
+ setup(function ()
+ stub(player_char, "enter_motion_state")
+ stub(pfx, "stop")
+ stub(player_char, "play_low_priority_sfx")
+ end)
- pc:update_platformer_motion_airborne()
+ teardown(function ()
+ player_char.enter_motion_state:revert()
+ pfx.stop:revert()
+ player_char.play_low_priority_sfx:revert()
+ end)
- -- implementation
- assert.spy(pc.enter_motion_state).was_called(1)
- assert.spy(pc.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
+ after_each(function ()
+ player_char.enter_motion_state:clear()
+ pfx.stop:clear()
+ player_char.play_low_priority_sfx:clear()
+ end)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
- assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0.5)
- end)
+ it('should enter rolling state', function ()
+ pc.motion_state = motion_states.spin_dashing -- optional
- end) -- compute_air_motion_result_mock (is_blocked_by_wall: true)
+ pc:release_spin_dash()
- describe('(when _compute_ground_motion_result returns a motion result with position vector(*2.5*, 4), slope_angle: 0, is_blocked: false, is_falling: false)', function ()
+ assert.spy(player_char.enter_motion_state).was_called(1)
+ assert.spy(player_char.enter_motion_state).was_called_with(match.ref(pc), motion_states.rolling)
+ end)
- local compute_ground_motion_result_mock
+ it('should set ground speed based on spin dash rev: less than 1, so floored to 0 -> base speed only', function ()
+ pc.spin_dash_rev = 0.5
- setup(function ()
- stub(player_char, "compute_air_motion_result", function (self)
- return motion.air_motion_result(
- nil,
- vector(2.5, 0), -- flr(2.5) must be < pc_data.ground_sensor_extent_x
- false,
- false,
- false,
- 0.5
- )
- end)
- end)
+ pc:release_spin_dash()
- teardown(function ()
- player_char.compute_air_motion_result:revert()
- end)
+ assert.are_equal(pc_data.spin_dash_base_speed, pc.ground_speed)
+ end)
- after_each(function ()
- player_char.compute_air_motion_result:clear()
- end)
+ it('should set ground speed based on spin dash rev: less than 1, so floored to 0 -> base speed only', function ()
+ pc.spin_dash_rev = 2.5
- it('should clamp character position X to stage left boundary (including half-width offset)', function ()
- pc:update_platformer_motion_airborne()
+ pc:release_spin_dash()
- -- in practice, clamped to 3
- assert.are_equal(ceil(pc_data.ground_sensor_extent_x), pc.position.x)
- end)
+ assert.are_equal(pc_data.spin_dash_base_speed + 2 * pc_data.spin_dash_rev_increase_factor, pc.ground_speed)
+ end)
- it('should clamp the ground speed to -0.1', function ()
- pc.velocity.x = -10
+ it('should stop smoke pfx', function ()
+ pc:release_spin_dash()
- pc:update_platformer_motion_airborne()
+ assert.spy(pfx.stop).was_called(1)
+ assert.spy(pfx.stop).was_called_with(match.ref(pc.smoke_pfx))
+ end)
- assert.are_equal(0, pc.velocity.x)
- end)
+ it('should play spin dash release sfx (low priority)', function ()
+ pc:release_spin_dash()
+ assert.spy(player_char.play_low_priority_sfx).was_called(1)
+ assert.spy(player_char.play_low_priority_sfx).was_called_with(match.ref(pc), audio.sfx_ids.spin_dash_release)
end)
- end) -- update_platformer_motion_airborne
+ end)
- describe('check_spring', function ()
+ describe('update_platformer_motion_airborne', function ()
setup(function ()
- stub(player_char, "trigger_spring")
+ spy.on(player_char, "enter_motion_state")
+ spy.on(player_char, "check_hold_jump")
+ -- trigger check inside set_ground_tile_location will fail as it needs context
+ -- (tile_test_data + mset), so we prefer stubbing as we don't check ground_tile_location directly
+ stub(player_char, "set_ground_tile_location")
+ spy.on(player_char, "set_slope_angle_with_quadrant")
end)
teardown(function ()
- player_char.trigger_spring:revert()
+ player_char.enter_motion_state:revert()
+ player_char.check_hold_jump:revert()
+ player_char.set_ground_tile_location:revert()
+ player_char.set_slope_angle_with_quadrant:revert()
end)
- after_each(function ()
- player_char.trigger_spring:clear()
+ before_each(function ()
+ -- optional, just to enter an airborne state and be in a meaningful state in this context
+ pc:enter_motion_state(motion_states.falling)
+ -- clear spy just after this instead of after_each to avoid messing the call count
+ player_char.enter_motion_state:clear()
+ player_char.check_hold_jump:clear()
+ player_char.set_ground_tile_location:clear()
+ player_char.set_slope_angle_with_quadrant:clear()
end)
- describe('(check_player_char_in_spring_trigger_area finds no spring)', function ()
+ describe('(when check_air_collisions sets position and returns a motion result with position vector(2, 8), is_blocked_by_ceiling: false, is_blocked_by_wall: false, is_landing: false)', function ()
setup(function ()
- stub(stage_state, "check_player_char_in_spring_trigger_area", function (self)
- return nil
+ stub(player_char, "check_air_collisions", function (self)
+ -- check_air_collisions must update position in-place
+ self.position = vector(4, 8) -- make sure it's far enough from stage left edge to avoid soft clamping
+
+ return motion.air_motion_result(
+ nil,
+ false,
+ false,
+ false,
+ nil
+ )
end)
- stub(player_char, "trigger_spring")
end)
teardown(function ()
- stage_state.check_player_char_in_spring_trigger_area:revert()
- player_char.trigger_spring:revert()
+ player_char.check_air_collisions:revert()
end)
after_each(function ()
- stage_state.check_player_char_in_spring_trigger_area:clear()
- player_char.trigger_spring:clear()
+ player_char.check_air_collisions:clear()
end)
- it('should call trigger_spring when ground tile location points to a spring tile (left)', function ()
- pc:check_spring()
- assert.spy(player_char.trigger_spring).was_not_called()
+ it('should set velocity y to -jump_interrupt_speed_frame on first frame of hop if velocity.y is not already greater, and clear has_jumped_this_frame flag', function ()
+ pc.motion_state = motion_states.air_spin
+ pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2)
+ pc.has_jumped_this_frame = true
+ pc.can_interrupt_jump = true
+ pc.hold_jump_intention = false
+
+ pc:update_platformer_motion_airborne()
+
+ -- call check
+ assert.spy(player_char.check_hold_jump).was_called(1)
+ assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
+
+ -- result check
+ assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame})
end)
- end)
+ it('should preserve velocity y completely on first frame of hop if velocity.y is already greater, and clear has_jumped_this_frame flag', function ()
+ -- this can happen when character is running down a steep slope, and hops with a normal close to horizontal
+ pc.motion_state = motion_states.air_spin
+ pc.velocity.y = -1 -- must be >= -pc_data.jump_interrupt_speed_frame (-2)
+ pc.has_jumped_this_frame = true
+ pc.can_interrupt_jump = true
+ pc.hold_jump_intention = false
- describe('(check_player_char_in_spring_trigger_area finds spring)', function ()
+ pc:update_platformer_motion_airborne()
- local mock_spring = {"mock spring"}
+ -- call check (but will do nothing)
+ assert.spy(player_char.check_hold_jump).was_called(1)
+ assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
- setup(function ()
- stub(stage_state, "check_player_char_in_spring_trigger_area", function (self)
- return mock_spring
- end)
- stub(player_char, "trigger_spring")
+ -- result check
+ assert.are_same({-1, false}, {pc.velocity.y, pc.has_jumped_this_frame})
end)
- teardown(function ()
- stage_state.check_player_char_in_spring_trigger_area:revert()
- player_char.trigger_spring:revert()
- end)
+ it('should preserve (supposedly initial jump) velocity y on first frame of jump (not hop) and clear has_jumped_this_frame flag', function ()
+ pc.motion_state = motion_states.air_spin
+ pc.velocity.y = -3
+ pc.has_jumped_this_frame = true
+ pc.can_interrupt_jump = true
+ pc.hold_jump_intention = true
- after_each(function ()
- stage_state.check_player_char_in_spring_trigger_area:clear()
- player_char.trigger_spring:clear()
- end)
+ pc:update_platformer_motion_airborne()
- it('should call trigger_spring when ground tile location points to a spring tile (left)', function ()
- pc:check_spring()
- assert.spy(player_char.trigger_spring).was_called(1)
- assert.spy(player_char.trigger_spring).was_called_with(match.ref(pc), match.ref(mock_spring))
+ -- call check (but will do nothing)
+ assert.spy(player_char.check_hold_jump).was_called(1)
+ assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
+
+ -- result check
+ assert.are_same({-3, false}, {pc.velocity.y, pc.has_jumped_this_frame})
end)
- end)
+ it('should apply gravity to velocity y when not on first frame of jump and not interrupting jump', function ()
+ pc.motion_state = motion_states.air_spin
+ pc.velocity.y = -1
+ pc.has_jumped_this_frame = false
+ pc.can_interrupt_jump = true
+ pc.hold_jump_intention = true
- end)
+ pc:update_platformer_motion_airborne()
- describe('check_launch_ramp', function ()
+ -- call check (but will do nothing)
+ assert.spy(player_char.check_hold_jump).was_called(1)
+ assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
- setup(function ()
- stub(player_char, "trigger_launch_ramp_effect")
- end)
+ -- result check
+ assert.are_same({-1 + pc_data.gravity_frame2, false}, {pc.velocity.y, pc.has_jumped_this_frame})
+ end)
- teardown(function ()
- player_char.trigger_launch_ramp_effect:revert()
- end)
+ it('should set to speed y to interrupt speed (no gravity added) when interrupting actual jump', function ()
+ pc.motion_state = motion_states.air_spin
+ pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2)
+ pc.has_jumped_this_frame = false
+ pc.can_interrupt_jump = true
+ pc.hold_jump_intention = false
- before_each(function ()
- mock_mset(2, 0, visual.launch_ramp_last_tile_id)
- end)
+ pc:update_platformer_motion_airborne()
- it('should not call trigger_launch_ramp_effect when ground tile location points to a launch_ramp tile but ground speed is too low', function ()
- pc.ground_tile_location = location(2, 0)
- pc.ground_speed = pc_data.launch_ramp_min_ground_speed - 0.1
+ -- call check
+ assert.spy(player_char.check_hold_jump).was_called(1)
+ assert.spy(player_char.check_hold_jump).was_called_with(match.ref(pc))
- pc:check_launch_ramp()
+ -- result check
+ -- note that gravity is applied *before* interrupt jump, so we don't see it in the final velocity.y
+ assert.are_same({-pc_data.jump_interrupt_speed_frame, false}, {pc.velocity.y, pc.has_jumped_this_frame})
+ end)
- assert.spy(player_char.trigger_launch_ramp_effect).was_not_called()
- end)
+ it('should NOT check for speed interrupt at all when running falling (not air_spin)', function ()
+ pc.motion_state = motion_states.falling
+ pc.velocity.y = -3 -- must be < -pc_data.jump_interrupt_speed_frame (-2)
+ pc.has_jumped_this_frame = false
+ pc.can_interrupt_jump = true
+ pc.hold_jump_intention = false
- it('should call trigger_launch_ramp_effect when ground tile location points to a launch_ramp tile and ground speed is high enough', function ()
- pc.ground_tile_location = location(2, 0)
- pc.ground_speed = pc_data.launch_ramp_min_ground_speed
+ pc:update_platformer_motion_airborne()
- pc:check_launch_ramp()
+ -- call check
+ assert.spy(player_char.check_hold_jump).was_not_called()
- assert.spy(player_char.trigger_launch_ramp_effect).was_called(1)
- assert.spy(player_char.trigger_launch_ramp_effect).was_called_with(match.ref(pc))
- end)
+ -- result check
+ assert.are_same({-3 + pc_data.gravity_frame2, false}, {pc.velocity.y, pc.has_jumped_this_frame})
+ end)
- end)
+ -- unfortunately it's hard to stub clamp_air_velocity_x properly
+ -- so we test the content of clamp_air_velocity_x below, which is redundant with its
+ -- own utests
+ -- it is *possible* to stub clamp_air_velocity_x completely and test that pure air accel x is applied,
+ -- and that clamp_air_velocity_x is called with previous velocity x,
+ -- although semantically a bit weird as the latter affects velocity.x
- describe('check_emerald', function ()
+ it('should apply air accel x', function ()
+ pc.velocity.x = 2
+ pc.move_intention.x = -1
- local mock_is_in_emerald_pick_area
- local mock_emerald = emerald(3, location(1, 2))
+ pc:update_platformer_motion_airborne()
- setup(function ()
- stub(stage_state, "character_pick_emerald")
- stub(stage_state, "check_emerald_pick_area", function (self, _pos)
- return mock_is_in_emerald_pick_area and mock_emerald or nil
+ assert.are_equal(2 - pc_data.air_accel_x_frame2, pc.velocity.x)
end)
- end)
- teardown(function ()
- stage_state.character_pick_emerald:revert()
- stage_state.check_emerald_pick_area:revert()
- end)
+ it('should apply air accel x but clamp at max air velocity x in abs if not already beyond', function ()
+ pc.velocity.x = -pc_data.max_air_velocity_x
+ pc.move_intention.x = -1
- before_each(function ()
- mock_mset(2, 0, visual.sprite_data_t.emerald.id_loc:to_sprite_id())
- end)
+ pc:update_platformer_motion_airborne()
- after_each(function ()
- mock_is_in_emerald_pick_area = nil
+ assert.are_equal(- pc_data.max_air_velocity_x, pc.velocity.x)
+ end)
- stage_state.character_pick_emerald:clear()
- end)
+ it('should apply air accel x but clamp at previous air velocity x in abs if already beyond', function ()
+ pc.velocity.x = -pc_data.max_air_velocity_x - 1
+ pc.move_intention.x = -1
- describe('(not in emerald pick area)', function ()
+ pc:update_platformer_motion_airborne()
- setup(function ()
- mock_is_in_emerald_pick_area = false
+ assert.are_equal(- pc_data.max_air_velocity_x - 1, pc.velocity.x)
end)
- teardown(function ()
- mock_is_in_emerald_pick_area = nil
+ it('should apply air accel x and allow decreasing air velocity x in abs if already beyond', function ()
+ pc.velocity.x = -pc_data.max_air_velocity_x - 10
+ pc.move_intention.x = 1
+
+ pc:update_platformer_motion_airborne()
+
+ assert.are_equal(- pc_data.max_air_velocity_x - 10 + pc_data.air_accel_x_frame2, pc.velocity.x)
end)
- it('should not call pick_emerald when check_emerald_pick_area returns false on current position', function ()
- mock_is_in_emerald_pick_area = false
- pc:check_emerald()
- assert.spy(stage_state.character_pick_emerald).was_not_called()
+ it('should set horizontal direction to intended motion direction: left', function ()
+ pc.orientation = horizontal_dirs.right
+ pc.velocity.x = 4
+ pc.move_intention.x = -1
+
+ pc:update_platformer_motion_airborne()
+
+ assert.are_equal(horizontal_dirs.left, pc.orientation)
end)
- it('should call pick_emerald when check_emerald_pick_area returns true on current position', function ()
- mock_is_in_emerald_pick_area = true
- pc:check_emerald()
- assert.spy(stage_state.character_pick_emerald).was_called(1)
- assert.spy(stage_state.character_pick_emerald).was_called_with(match.ref(flow.curr_state), match.ref(mock_emerald))
+ it('should set horizontal direction to intended motion direction: right', function ()
+ pc.orientation = horizontal_dirs.left
+ pc.velocity.x = 4
+ pc.move_intention.x = 1
+
+ pc:update_platformer_motion_airborne()
+
+ assert.are_equal(horizontal_dirs.right, pc.orientation)
end)
- end)
+ it('should clamp velocity Y if beyond limit (positive)', function ()
+ pc.velocity.y = 1000
- end)
+ pc:update_platformer_motion_airborne()
- describe('check_loop_external_triggers', function ()
+ assert.are_equal(pc_data.max_air_velocity_y, pc.velocity.y)
+ end)
- setup(function ()
- stub(stage_state, "check_loop_external_triggers", function (self, pos, _previous_active_layer)
- -- simulate some very broad triggers
- -- don't care about previous_active_layer, we are already doing proper checks for that
- -- in stage_state:check_loop_external_triggers utests
- if pos.y > 10 then
- return nil
- end
- if pos.x < 0 then
- return 1
- elseif pos.x > 5 then
- return 2
- end
+ -- bugfix history:
+ -- .
+ it('should update position with air motion result position', function ()
+ pc.position = vector(0, 0) -- doesn't matter, since we stub check_air_collisions
+
+ pc:update_platformer_motion_airborne()
+
+ assert.are_same(vector(4, 8), pc.position)
end)
- end)
- teardown(function ()
- stage_state.check_loop_external_triggers:revert()
- end)
+ it('should preserve velocity.y', function ()
+ -- set those flags to true to make computations more simple:
+ -- velocity.y will not affected by gravity nor interrupt jump
+ pc.has_jumped_this_frame = true
+ pc.hold_jump_intention = true
+ pc.velocity = vector(10, -10)
- it('should set active loop layer to 1 when detecting external entrance trigger', function ()
- pc.active_loop_layer = -1
- pc.position = vector(-1, 0)
- pc:check_loop_external_triggers()
- assert.are_equal(1, pc.active_loop_layer)
- end)
+ pc:update_platformer_motion_airborne()
- it('should set active loop layer to 2 when detecting external exit trigger', function ()
- pc.active_loop_layer = -1
- pc.position = vector(6, 0)
- pc:check_loop_external_triggers()
- assert.are_equal(2, pc.active_loop_layer)
- end)
+ assert.are_equal(-10, pc.velocity.y)
+ end)
- it('should not set active loop layer when not detecting any external loop trigger', function ()
- pc.active_loop_layer = -1
- pc.position = vector(0, 15)
- pc:check_loop_external_triggers()
- -- invalid value of course, just to show that nothing was set
- assert.are_equal(-1, pc.active_loop_layer)
- end)
+ end) -- stub check_air_collisions (vector(2, 8), false, false, false)
- end)
+ describe('(when check_air_collisions sets position and returns a motion result with is_blocked_by_wall: false, is_blocked_by_ceiling: true) '..
+ '(when apply_air_drag multiplies velocity x by 0.9 no matter what)', function ()
- end) -- (with mock tiles data setup)
+ setup(function ()
+ stub(player_char, "check_air_collisions", function (self)
+ self.position = vector(4, 8)
- describe('check_hold_jump', function ()
+ return motion.air_motion_result(
+ nil,
+ false, -- not the focus, but verified
+ true, -- focus in this test
+ false,
+ nil
+ )
+ end)
+ stub(player_char, "apply_air_drag", function (self)
+ self.velocity.x = 0.9 * self.velocity.x
+ end)
+ end)
- before_each(function ()
- -- optional, just to enter air_spin state and be in a meaningful state in this context
- pc:enter_motion_state(motion_states.air_spin)
- end)
+ teardown(function ()
+ player_char.check_air_collisions:revert()
+ player_char.apply_air_drag:revert()
+ end)
- it('should interrupt the jump when still possible and hold_jump_intention is false', function ()
- pc.velocity.y = -3
- pc.can_interrupt_jump = true
+ after_each(function ()
+ player_char.check_air_collisions:clear()
+ player_char.apply_air_drag:clear()
+ end)
- pc:check_hold_jump()
+ it('should set velocity.y to 0', function ()
+ -- set those flags to true to make computations more simple:
+ -- velocity.y will not affected by gravity nor interrupt jump
+ pc.has_jumped_this_frame = true
+ pc.hold_jump_intention = true
+ pc.velocity = vector(10, -10)
- assert.are_same({false, -pc_data.jump_interrupt_speed_frame}, {pc.can_interrupt_jump, pc.velocity.y})
- end)
+ pc:update_platformer_motion_airborne()
- it('should not change velocity but still set the interrupt flag when it\'s too late to interrupt jump and hold_jump_intention is false', function ()
- pc.velocity.y = -1
- pc.can_interrupt_jump = true
+ assert.are_equal(0, pc.velocity.y)
+ end)
- pc:check_hold_jump()
+ it('should apply air drag, then preserve velocity.x on hit ceiling', function ()
+ pc.velocity = vector(10, -10)
- assert.are_same({false, -1}, {pc.can_interrupt_jump, pc.velocity.y})
- end)
+ pc:update_platformer_motion_airborne()
- it('should not try to interrupt jump if already done', function ()
- pc.velocity.y = -3
- pc.can_interrupt_jump = false
+ -- spy test (should always be called anyway, but only this test really demonstrates X velocity)
+ assert.spy(player_char.apply_air_drag).was_called(1)
+ assert.spy(player_char.apply_air_drag).was_called_with(match.ref(pc))
- pc:check_hold_jump()
+ -- value test
+ assert.are_equal(9, pc.velocity.x)
+ end)
- assert.are_same({false, -3}, {pc.can_interrupt_jump, pc.velocity.y})
- end)
+ end) -- stub check_air_collisions (is_blocked_by_ceiling: true)
- it('should not try to interrupt jump if still holding jump input', function ()
- pc.velocity.y = -3
- pc.can_interrupt_jump = true
- pc.hold_jump_intention = true
+ describe('(when check_air_collisions sets position and returns a motion result with is_blocked_by_wall: true, is_blocked_by_ceiling: false)', function ()
- pc:check_hold_jump()
+ setup(function ()
+ stub(player_char, "check_air_collisions", function (self)
+ self.position = vector(4, 8)
- assert.are_same({true, -3}, {pc.can_interrupt_jump, pc.velocity.y})
- end)
+ return motion.air_motion_result(
+ nil,
+ true, -- focus in this test
+ false, -- not the focus, but verified
+ false,
+ nil
+ )
+ end)
+ end)
- end)
+ teardown(function ()
+ player_char.check_air_collisions:revert()
+ end)
- describe('apply_air_drag', function ()
+ after_each(function ()
+ player_char.check_air_collisions:clear()
+ end)
- it('(when velocity is 0.25 0) should do nothing', function ()
- -- abs(vel.x) >= pc_data.air_drag_min_velocity_x but vel.y >= 0
- pc.velocity = vector(0.25, 0)
+ it('should preserve velocity.y', function ()
+ -- set those flags to true to make computations more simple:
+ -- velocity.y will not affected by gravity nor interrupt jump
+ pc.has_jumped_this_frame = true
+ pc.hold_jump_intention = true
+ pc.velocity = vector(10, -10)
- pc:apply_air_drag()
+ pc:update_platformer_motion_airborne()
- assert.are_same(vector(0.25, 0), pc.velocity)
- end)
+ assert.are_equal(-10, pc.velocity.y)
+ end)
- it('(when velocity is 0.25 7) should do nothing', function ()
- -- abs(vel.x) >= pc_data.air_drag_min_velocity_x but vel.y >= 0
- pc.velocity = vector(0.25, 7)
+ it('should set velocity.x to 0', function ()
+ pc.velocity = vector(10, -10)
- pc:apply_air_drag()
+ pc:update_platformer_motion_airborne()
- assert.are_same(vector(0.25, 7), pc.velocity)
- end)
+ assert.are_equal(0, pc.velocity.x)
+ end)
- it('(when velocity is 0.1 -7) should do nothing', function ()
- -- vel.y is OK but abs(vel.x) < pc_data.air_drag_min_velocity_x
- pc.velocity = vector(0.1, -7)
+ end)
- pc:apply_air_drag()
+ describe('(when check_air_collisions sets position and returns a motion result with is_landing: true, slope_angle: 0.5)', function ()
- assert.are_same(vector(0.1, -7), pc.velocity)
- end)
+ setup(function ()
+ stub(player_char, "check_air_collisions", function (self)
+ self.position = vector(4, 8)
+ return motion.air_motion_result(
+ location(0, 1),
+ false,
+ false,
+ true, -- focus in this test
+ 0.5
+ )
+ end)
+ end)
- it('(when velocity is 0.25 -7) should do nothing', function ()
- -- both velocity coords match the conditions, apply drag factor
- pc.velocity = vector(0.25, -7)
+ teardown(function ()
+ player_char.check_air_collisions:revert()
+ end)
- pc:apply_air_drag()
+ after_each(function ()
+ player_char.check_air_collisions:clear()
+ end)
- -- velocity x should be = 0.2421875
- assert.are_same(vector(0.25 * pc_data.air_drag_factor_per_frame, -7), pc.velocity)
- end)
+ it('should call player_char.set_ground_tile_location with location(0, 1)', function ()
+ pc:update_platformer_motion_airborne()
+ assert.spy(player_char.set_ground_tile_location).was_called(1)
+ assert.spy(player_char.set_ground_tile_location).was_called_with(match.ref(pc), location(0, 1))
+ end)
- it('(when velocity is 0.25 -8) should do nothing', function ()
- -- abs(vel.x) >= pc_data.air_drag_min_velocity_x but vel.y <= - pc_data.air_drag_max_abs_velocity_y
- pc.velocity = vector(0.25, -8)
+ it('should enter standing state and set_slope_angle_with_quadrant: 0.5', function ()
+ pc.slope_angle = 0
- pc:apply_air_drag()
+ pc:update_platformer_motion_airborne()
- assert.are_same(vector(0.25, -8), pc.velocity)
- end)
+ -- implementation
+ assert.spy(pc.enter_motion_state).was_called(1)
+ assert.spy(pc.enter_motion_state).was_called_with(match.ref(pc), motion_states.standing)
- end)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called(1)
+ assert.spy(player_char.set_slope_angle_with_quadrant).was_called_with(match.ref(pc), 0.5)
+ end)
- describe('clamp_air_velocity_x', function ()
+ end) -- stub check_air_collisions (is_blocked_by_wall: true)
- it('should preserve velocity x when it is not over max speed in absolute value (positive)', function ()
- pc.motion_state = motion_states.falling -- to avoid assert
- pc.velocity.x = pc_data.max_air_velocity_x - 0.01
+ describe('(when check_air_collisions sets position and returns a motion result with position vector(*2.5*, 4), slope_angle: 0, is_blocked: false, is_falling: false)', function ()
- pc:clamp_air_velocity_x(0)
+ setup(function ()
- assert.are_equal(pc_data.max_air_velocity_x - 0.01, pc.velocity.x)
- end)
+ stub(player_char, "check_air_collisions", function (self)
+ self.position = vector(2.5, 0) -- flr(2.5) must be < pc_data.ground_sensor_extent_x
- it('should clamp at previous air velocity x in abs if already beyond', function ()
- pc.motion_state = motion_states.air_spin -- to avoid assert
- pc.velocity.x = -pc_data.max_air_velocity_x - 1
+ return motion.air_motion_result(
+ nil,
+ false,
+ false,
+ false,
+ 0.5
+ )
+ end)
+ end)
- pc:clamp_air_velocity_x(-pc_data.max_air_velocity_x - 1)
+ teardown(function ()
+ player_char.check_air_collisions:revert()
+ end)
- assert.are_equal(- pc_data.max_air_velocity_x - 1, pc.velocity.x)
- end)
+ after_each(function ()
+ player_char.check_air_collisions:clear()
+ end)
- it('should allow decreasing air velocity x in abs if already beyond', function ()
- pc.motion_state = motion_states.air_spin -- to avoid assert
- pc.velocity.x = -pc_data.max_air_velocity_x - 10
+ it('should clamp character position X to stage left boundary (including half-width offset)', function ()
+ pc:update_platformer_motion_airborne()
- pc:clamp_air_velocity_x(-pc_data.max_air_velocity_x - 9)
+ -- in practice, clamped to 3
+ assert.are_equal(ceil(pc_data.ground_sensor_extent_x), pc.position.x)
+ end)
- assert.are_equal(-pc_data.max_air_velocity_x - 9, pc.velocity.x)
- end)
+ it('should clamp the ground speed to -0.1', function ()
+ pc.velocity.x = -10
- end)
+ pc:update_platformer_motion_airborne()
+
+ assert.are_equal(0, pc.velocity.x)
+ end)
- describe('compute_air_motion_result', function ()
+ end)
- it('(when velocity is zero) should return air_motion_result with initial position and no hits', function ()
- pc.position = vector(4, 8)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(4, 8),
- false,
- false,
- false,
- nil
- ), pc:compute_air_motion_result())
- end)
+ end) -- update_platformer_motion_airborne
- describe('(when _advance_in_air_along returns an air_motion_result with full motion done along x, half motion done with hit ceiling along y)', function ()
+ describe('check_spring', function ()
- setup(function ()
- advance_in_air_along_mock = stub(player_char, "advance_in_air_along", function (self, ref_motion_result, velocity, coord)
- if coord == "x" then
- local motion = vector(velocity.x, 0)
- ref_motion_result.position = ref_motion_result.position + motion
- else -- coord == "y"
- -- to make sure we are calling _advance_in_air_along on x before y, we add a check here:
- -- if we have already moved from initial pos.y = 8 (see test below), block any motion along y
- if ref_motion_result.position.y == 8 then
- local motion = vector(0, velocity.y / 2)
- ref_motion_result.position = ref_motion_result.position + motion
- end
- ref_motion_result.is_blocked_by_ceiling = true
- end
- end)
+ setup(function ()
+ stub(player_char, "trigger_spring")
end)
teardown(function ()
- advance_in_air_along_mock:revert()
+ player_char.trigger_spring:revert()
end)
after_each(function ()
- advance_in_air_along_mock:clear()
- end)
-
- it('(when velocity is zero) should return air_motion_result with initial position and no hits', function ()
- pc.position = vector(4.5, 8)
- pc.velocity = vector(5, -12)
-
- -- character should advance of (5, -6) resulting in pos (9.5, 2)
-
- -- interface: check that the final result is correct
- assert.are_same(motion.air_motion_result(
- nil,
- vector(9.5, 2),
- false,
- true, -- hit ceiling
- false,
- nil
- ), pc:compute_air_motion_result())
+ player_char.trigger_spring:clear()
end)
- end)
-
- end)
+ describe('(check_player_char_in_spring_trigger_area finds no spring)', function ()
- describe('advance_in_air_along', function ()
+ setup(function ()
+ stub(stage_state, "check_player_char_in_spring_trigger_area", function (self)
+ return nil
+ end)
+ stub(player_char, "trigger_spring")
+ end)
- describe('(when next_air_step moves motion_result.position.x/y by 1px in the given direction, ' ..
- 'unless moving along x from x >= 5, where it is blocking by wall)', function ()
+ teardown(function ()
+ stage_state.check_player_char_in_spring_trigger_area:revert()
+ player_char.trigger_spring:revert()
+ end)
- local next_air_step_mock
+ after_each(function ()
+ stage_state.check_player_char_in_spring_trigger_area:clear()
+ player_char.trigger_spring:clear()
+ end)
- setup(function ()
- next_air_step_mock = stub(player_char, "next_air_step", function (self, direction, motion_result)
- if coord == "y" or motion_result.position.x < 5 then
- local step_vec = dir_vectors[direction]
- motion_result.position = motion_result.position + step_vec
- else
- motion_result.is_blocked_by_wall = true
- end
+ it('should call trigger_spring when ground tile location points to a spring tile (left)', function ()
+ pc:check_spring()
+ assert.spy(player_char.trigger_spring).was_not_called()
end)
- end)
- teardown(function ()
- next_air_step_mock:revert()
end)
- after_each(function ()
- next_air_step_mock:clear()
- end)
+ describe('(check_player_char_in_spring_trigger_area finds spring)', function ()
- -- bugfix history:
- -- = the itest 'platformer air wall block' showed that the subpixel check
- -- was using the integer max_pixel_distance instead of the float velocity:get()
- -- and this revealed a bug of no motion on x at all when velocity.x is < 1 and x starts integer
- it('(vector(0, 10) at speed 0.5 along x) should move to vector(0.7, 10) without being blocked', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(0, 10),
- false,
- false,
- false,
- nil
- )
+ local mock_spring = {"mock spring"}
- -- we assume _compute_max_pixel_distance is correct
- pc:advance_in_air_along(motion_result, vector(0.5, 99), "x")
+ setup(function ()
+ stub(stage_state, "check_player_char_in_spring_trigger_area", function (self)
+ return mock_spring
+ end)
+ stub(player_char, "trigger_spring")
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(0.5, 10),
- false,
- false,
- false,
- nil
- ), motion_result
- )
- end)
+ teardown(function ()
+ stage_state.check_player_char_in_spring_trigger_area:revert()
+ player_char.trigger_spring:revert()
+ end)
- it('(vector(0.2, 10) at speed 0.5 along x) should move to vector(0.7, 10) without being blocked', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(0.2, 10),
- false,
- false,
- false,
- nil
- )
+ after_each(function ()
+ stage_state.check_player_char_in_spring_trigger_area:clear()
+ player_char.trigger_spring:clear()
+ end)
- -- we assume _compute_max_pixel_distance is correct
- pc:advance_in_air_along(motion_result, vector(0.5, 99), "x")
+ it('should call trigger_spring when ground tile location points to a spring tile (left)', function ()
+ pc:check_spring()
+ assert.spy(player_char.trigger_spring).was_called(1)
+ assert.spy(player_char.trigger_spring).was_called_with(match.ref(pc), match.ref(mock_spring))
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(0.7, 10),
- false,
- false,
- false,
- nil
- ), motion_result
- )
end)
- it('(vector(0.5, 10) at speed 0.5 along x) should move to vector(1, 10) without being blocked', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(0.5, 10),
- false,
- false,
- false,
- nil
- )
+ end)
- -- we assume _compute_max_pixel_distance is correct
- pc:advance_in_air_along(motion_result, vector(0.5, 99), "x")
+ describe('check_launch_ramp', function ()
- assert.are_same(motion.air_motion_result(
- nil,
- vector(1, 10),
- false,
- false,
- false,
- nil
- ), motion_result
- )
+ setup(function ()
+ stub(player_char, "trigger_launch_ramp_effect")
end)
- it('(vector(0.4, 10) at speed 2.7 along x) should move to vector(3.1, 10)', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(0.4, 10),
- false,
- false,
- false,
- nil
- )
-
- -- we assume _compute_max_pixel_distance is correct
- pc:advance_in_air_along(motion_result, vector(2.7, 99), "x")
+ teardown(function ()
+ player_char.trigger_launch_ramp_effect:revert()
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(3.1, 10),
- false,
- false,
- false,
- nil
- ), motion_result
- )
+ before_each(function ()
+ mock_mset(2, 0, visual_ingame_data.launch_ramp_last_tile_id)
end)
- it('(vector(2.5, 10) at speed 2.7 along x) should move to vector(5, 10) and blocked by wall', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(2.5, 10),
- false,
- false,
- false,
- nil
- )
+ it('should not call trigger_launch_ramp_effect when ground tile location points to a launch_ramp tile but ground speed is too low', function ()
+ pc.ground_tile_location = location(2, 0)
+ pc.ground_speed = pc_data.launch_ramp_min_ground_speed - 0.1
- -- we assume _compute_max_pixel_distance is correct
- pc:advance_in_air_along(motion_result, vector(2.7, 99), "x")
+ pc:check_launch_ramp()
- assert.are_same(motion.air_motion_result(
- nil,
- vector(5, 10),
- true,
- false,
- false,
- nil
- ), motion_result
- )
+ assert.spy(player_char.trigger_launch_ramp_effect).was_not_called()
end)
- it('(vector(2.5, 7.3) at speed -4.4 along y) should move to vector(2.5, 2.9) without being blocked', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(2.5, 7.3),
- false,
- false,
- false,
- nil
- )
+ it('should call trigger_launch_ramp_effect when ground tile location points to a launch_ramp tile and ground speed is high enough', function ()
+ pc.ground_tile_location = location(2, 0)
+ pc.ground_speed = pc_data.launch_ramp_min_ground_speed
- -- we assume _compute_max_pixel_distance is correct
- pc:advance_in_air_along(motion_result, vector(99, -4.4), "y")
+ pc:check_launch_ramp()
- assert.is_true(almost_eq_with_message(vector(2.5, 2.9), motion_result.position))
- assert.are_same({
- false,
- false,
- false
- }, {
- motion_result.is_blocked_by_wall,
- motion_result.is_blocked_by_ceiling,
- motion_result.is_landing
- })
+ assert.spy(player_char.trigger_launch_ramp_effect).was_called(1)
+ assert.spy(player_char.trigger_launch_ramp_effect).was_called_with(match.ref(pc))
end)
end)
- end)
+ describe('check_emerald', function ()
- describe('next_air_step', function ()
- it('(in the air) direction up should move 1px up without being blocked', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(2, 7),
- false,
- false,
- false,
- nil
- )
+ local mock_is_in_emerald_pick_area
+ local mock_emerald = emerald(3, location(1, 2))
- pc:next_air_step(directions.up, motion_result)
+ setup(function ()
+ stub(stage_state, "character_pick_emerald")
+ stub(stage_state, "check_emerald_pick_area", function (self, _pos)
+ return mock_is_in_emerald_pick_area and mock_emerald or nil
+ end)
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(2, 6),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
- end)
+ teardown(function ()
+ stage_state.character_pick_emerald:revert()
+ stage_state.check_emerald_pick_area:revert()
+ end)
- it('(in the air) direction down should move 1px down without being blocked', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(2, 7),
- false,
- false,
- false,
- nil
- )
+ before_each(function ()
+ mock_mset(2, 0, visual.sprite_data_t.emerald.id_loc:to_sprite_id())
+ end)
- pc:next_air_step(directions.down, motion_result)
+ after_each(function ()
+ mock_is_in_emerald_pick_area = nil
- assert.are_same(motion.air_motion_result(
- nil,
- vector(2, 8),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
- end)
+ stage_state.character_pick_emerald:clear()
+ end)
- it('(in the air) direction left should move 1px left without being blocked', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(2, 7),
- false,
- false,
- false,
- nil
- )
+ describe('(not in emerald pick area)', function ()
- pc:next_air_step(directions.left, motion_result)
+ setup(function ()
+ mock_is_in_emerald_pick_area = false
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(1, 7),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
- end)
+ teardown(function ()
+ mock_is_in_emerald_pick_area = nil
+ end)
- it('(in the air) direction right should move 1px right without being blocked', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(2, 7),
- false,
- false,
- false,
- nil
- )
+ it('should not call pick_emerald when check_emerald_pick_area returns false on current position', function ()
+ mock_is_in_emerald_pick_area = false
+ pc:check_emerald()
+ assert.spy(stage_state.character_pick_emerald).was_not_called()
+ end)
- pc:next_air_step(directions.right, motion_result)
+ it('should call pick_emerald when check_emerald_pick_area returns true on current position', function ()
+ mock_is_in_emerald_pick_area = true
+ pc:check_emerald()
+ assert.spy(stage_state.character_pick_emerald).was_called(1)
+ assert.spy(stage_state.character_pick_emerald).was_called_with(match.ref(flow.curr_state), match.ref(mock_emerald))
+ end)
+
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(3, 7),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
end)
- describe('(with mock tiles data setup)', function ()
+ describe('check_loop_external_triggers', function ()
setup(function ()
- tile_test_data.setup()
+ stub(stage_state, "check_loop_external_triggers", function (self, pos, _previous_active_layer)
+ -- simulate some very broad triggers
+ -- don't care about previous_active_layer, we are already doing proper checks for that
+ -- in stage_state:check_loop_external_triggers utests
+ if pos.y > 10 then
+ return nil
+ end
+ if pos.x < 0 then
+ return 1
+ elseif pos.x > 5 then
+ return 2
+ end
+ end)
end)
teardown(function ()
- tile_test_data.teardown()
+ stage_state.check_loop_external_triggers:revert()
end)
- after_each(function ()
- pico8:clear_map()
+ it('should set active loop layer to 1 when detecting external entrance trigger', function ()
+ pc.active_loop_layer = -1
+ pc.position = vector(-1, 0)
+ pc:check_loop_external_triggers()
+ assert.are_equal(1, pc.active_loop_layer)
+ end)
+
+ it('should set active loop layer to 2 when detecting external exit trigger', function ()
+ pc.active_loop_layer = -1
+ pc.position = vector(6, 0)
+ pc:check_loop_external_triggers()
+ assert.are_equal(2, pc.active_loop_layer)
+ end)
+
+ it('should not set active loop layer when not detecting any external loop trigger', function ()
+ pc.active_loop_layer = -1
+ pc.position = vector(0, 15)
+ pc:check_loop_external_triggers()
+ -- invalid value of course, just to show that nothing was set
+ assert.are_equal(-1, pc.active_loop_layer)
end)
- -- for these utests, we assume that _compute_ground_sensors_query_info and
- -- _is_blocked_by_ceiling are correct,
- -- so rather than mocking them, so we setup simple tiles to walk on
+ end)
+
+ end) -- (with mock tiles data setup)
+
+ describe('check_hold_jump', function ()
+
+ before_each(function ()
+ -- optional, just to enter air_spin state and be in a meaningful state in this context
+ pc:enter_motion_state(motion_states.air_spin)
+ end)
+
+ it('should interrupt the jump when still possible and hold_jump_intention is false', function ()
+ pc.velocity.y = -3
+ pc.can_interrupt_jump = true
- describe('(with flat ground)', function ()
+ pc:check_hold_jump()
- before_each(function ()
- -- #
- mock_mset(0, 0, tile_repr.full_tile_id) -- full tile
- end)
+ assert.are_same({false, -pc_data.jump_interrupt_speed_frame}, {pc.can_interrupt_jump, pc.velocity.y})
+ end)
- -- in the tests below, we can use pc_data.full/center_height_standing directly instead
- -- of pc:get_full/center_height()
- -- because the character is not compact (e.g. no air spin)
+ it('should not change velocity but still set the interrupt flag when it\'s too late to interrupt jump and hold_jump_intention is false', function ()
+ pc.velocity.y = -1
+ pc.can_interrupt_jump = true
- it('direction up into ceiling should not move, and flag is_blocked_by_ceiling', function ()
- -- we need an upward velocity for ceiling check if not faster on x than y
- pc.velocity.x = 0
- pc.velocity.y = -3
+ pc:check_hold_jump()
- local motion_result = motion.air_motion_result(
- nil,
- vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ assert.are_same({false, -1}, {pc.can_interrupt_jump, pc.velocity.y})
+ end)
- pc:next_air_step(directions.up, motion_result)
+ it('should not try to interrupt jump if already done', function ()
+ pc.velocity.y = -3
+ pc.can_interrupt_jump = false
- assert.are_same(motion.air_motion_result(
- nil,
- vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing),
- false,
- true,
- false,
- nil
- ),
- motion_result
- )
- end)
+ pc:check_hold_jump()
- -- added to identify #122 BUG MOTION jump-through-ceiling-diagonal
- -- trying to reduce itest "platformer air ceiling corner block" to a utest
- -- fixed by re-adding condition direction == directions.up which I removed
- -- when I switched to the sheer velocity check (which in the end is much more rare)
- it('direction up into ceiling should not move, and flag is_blocked_by_ceiling, even if already is_blocked_by_wall', function ()
- -- we need an upward velocity for ceiling check if not faster on x than y
- pc.velocity.x = 0
- pc.velocity.y = -3
+ assert.are_same({false, -3}, {pc.can_interrupt_jump, pc.velocity.y})
+ end)
- local motion_result = motion.air_motion_result(
- nil,
- vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing),
- true, -- is_blocked_by_wall
- false,
- false,
- nil
- )
+ it('should not try to interrupt jump if still holding jump input', function ()
+ pc.velocity.y = -3
+ pc.can_interrupt_jump = true
+ pc.hold_jump_intention = true
- pc:next_air_step(directions.up, motion_result)
+ pc:check_hold_jump()
- assert.are_same(motion.air_motion_result(
- nil,
- vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing),
- true, -- is_blocked_by_wall
- true,
- false,
- nil
- ),
- motion_result
- )
- end)
+ assert.are_same({true, -3}, {pc.can_interrupt_jump, pc.velocity.y})
+ end)
- it('direction down into ground should not move, and flag is_landing with slope_angle 0', function ()
- pc.velocity.x = 0
- pc.velocity.y = 3
+ end)
- local motion_result = motion.air_motion_result(
- nil,
- vector(4, 0 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ describe('apply_air_drag', function ()
- pc:next_air_step(directions.down, motion_result)
+ it('(when velocity is 0.25 0) should do nothing', function ()
+ -- abs(vel.x) >= pc_data.air_drag_min_velocity_x but vel.y >= 0
+ pc.velocity = vector(0.25, 0)
- assert.are_same(motion.air_motion_result(
- location(0, 0),
- vector(4, 0 - pc_data.center_height_standing),
- false,
- false,
- true,
- 0
- ),
- motion_result
- )
- end)
+ pc:apply_air_drag()
- it('direction left exactly onto ground should step left, but flag NOT is_landing with slop_angle nil', function ()
- -- we wait next frame to actually land, else character will stay 1 px above ground
+ assert.are_same(vector(0.25, 0), pc.velocity)
+ end)
- pc.velocity.x = -3
- pc.velocity.y = 0
+ it('(when velocity is 0.25 7) should do nothing', function ()
+ -- abs(vel.x) >= pc_data.air_drag_min_velocity_x but vel.y >= 0
+ pc.velocity = vector(0.25, 7)
- local motion_result = motion.air_motion_result(
- nil,
- vector(11, 0 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ pc:apply_air_drag()
- pc:next_air_step(directions.left, motion_result)
+ assert.are_same(vector(0.25, 7), pc.velocity)
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(10, 0 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
- end)
+ it('(when velocity is 0.1 -7) should do nothing', function ()
+ -- vel.y is OK but abs(vel.x) < pc_data.air_drag_min_velocity_x
+ pc.velocity = vector(0.1, -7)
- it('direction right exactly onto ground should step right, and flag NOT is_landing with slop_angle nil', function ()
- -- we wait next frame to actually land, else character will stay 1 px above ground
+ pc:apply_air_drag()
- pc.velocity.x = 3
- pc.velocity.y = 0
+ assert.are_same(vector(0.1, -7), pc.velocity)
+ end)
- local motion_result = motion.air_motion_result(
- nil,
- vector(-3, 0 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ it('(when velocity is 0.25 -7) should do nothing', function ()
+ -- both velocity coords match the conditions, apply drag factor
+ pc.velocity = vector(0.25, -7)
- pc:next_air_step(directions.right, motion_result)
+ pc:apply_air_drag()
- assert.are_same(motion.air_motion_result(
- nil,
- vector(-2, 0 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
- end)
+ -- velocity x should be = 0.2421875
+ assert.are_same(vector(0.25 * pc_data.air_drag_factor_per_frame, -7), pc.velocity)
+ end)
- it('direction left into ground not deeper than max_ground_escape_height should step left and up, and flag is_landing with slop_angle 0', function ()
- pc.velocity.x = -3
- pc.velocity.y = 0
+ it('(when velocity is 0.25 -8) should do nothing', function ()
+ -- abs(vel.x) >= pc_data.air_drag_min_velocity_x but vel.y <= - pc_data.air_drag_max_abs_velocity_y
+ pc.velocity = vector(0.25, -8)
- local motion_result = motion.air_motion_result(
- nil,
- vector(11, 1 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ pc:apply_air_drag()
- pc:next_air_step(directions.left, motion_result)
+ assert.are_same(vector(0.25, -8), pc.velocity)
+ end)
- assert.are_same(motion.air_motion_result(
- location(0, 0),
- vector(10, 0 - pc_data.center_height_standing),
- false,
- false,
- true,
- 0
- ),
- motion_result
- )
- end)
+ end)
- it('direction right into ground not deeper than max_ground_escape_height should step right and up, and flag is_landing with slop_angle 0', function ()
- pc.velocity.x = 3
- pc.velocity.y = 0
+ describe('clamp_air_velocity_x', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(-3, 1 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ it('should preserve velocity x when it is not over max speed in absolute value (positive)', function ()
+ pc.motion_state = motion_states.falling -- to avoid assert
+ pc.velocity.x = pc_data.max_air_velocity_x - 0.01
- pc:next_air_step(directions.right, motion_result)
+ pc:clamp_air_velocity_x(0)
- assert.are_same(motion.air_motion_result(
- location(0, 0),
- vector(-2, 0 - pc_data.center_height_standing),
- false,
- false,
- true,
- 0
- ),
- motion_result
- )
- end)
+ assert.are_equal(pc_data.max_air_velocity_x - 0.01, pc.velocity.x)
+ end)
- -- extra tests for sheer horizontally velocity check
+ it('should clamp at previous air velocity x in abs if already beyond', function ()
+ pc.motion_state = motion_states.air_spin -- to avoid assert
+ pc.velocity.x = -pc_data.max_air_velocity_x - 1
- it('(at upward velocity, sheer angle) direction right into ground not deeper than max_ground_escape_height should step right and up, and flag is_landing with slop_angle 0', function ()
- pc.velocity.x = 3
- pc.velocity.y = -1
+ pc:clamp_air_velocity_x(-pc_data.max_air_velocity_x - 1)
- local motion_result = motion.air_motion_result(
- nil,
- vector(-3, 1 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ assert.are_equal(- pc_data.max_air_velocity_x - 1, pc.velocity.x)
+ end)
- pc:next_air_step(directions.right, motion_result)
+ it('should allow decreasing air velocity x in abs if already beyond', function ()
+ pc.motion_state = motion_states.air_spin -- to avoid assert
+ pc.velocity.x = -pc_data.max_air_velocity_x - 10
- assert.are_same(motion.air_motion_result(
- location(0, 0),
- vector(-2, 0 - pc_data.center_height_standing),
- false,
- false,
- true,
- 0
- ),
- motion_result
- )
- end)
+ pc:clamp_air_velocity_x(-pc_data.max_air_velocity_x - 9)
- it('(at upward velocity, high angle) direction right into ground not deeper than max_ground_escape_height should ignore the floor completely (even during right step)', function ()
- pc.velocity.x = 3
- pc.velocity.y = -3
+ assert.are_equal(-pc_data.max_air_velocity_x - 9, pc.velocity.x)
+ end)
- local motion_result = motion.air_motion_result(
- nil,
- vector(-3, 1 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ end)
- pc:next_air_step(directions.right, motion_result)
+ describe('check_air_collisions', function ()
- assert.are_same(motion.air_motion_result(
- nil,
- vector(-2, 1 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
- end)
+ it('(when velocity is zero) should not change position and eturn air_motion_result with current position and no hits', function ()
+ pc.position = vector(4, 4)
- it('direction left into wall deeper than max_ground_escape_height should not move, and flag is_blocked_by_wall', function ()
- pc.velocity.x = -3
- pc.velocity.y = 0
+ local result = pc:check_air_collisions()
- local motion_result = motion.air_motion_result(
- nil,
- vector(11, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ assert.are_same(vector(4, 4), pc.position)
+ assert.are_same(motion.air_motion_result(
+ nil,
+ false,
+ false,
+ false,
+ nil
+ ), result)
+ end)
- pc:next_air_step(directions.left, motion_result)
+ -- for compute_ground_motion_result we stubbed compute_closest_wall_query_info,
+ -- but since we extracted check_escape_wall_and_update_next_position which is even higher level,
+ -- so for these utests we will directly stub check_escape_wall_and_update_next_position
+ describe('(when check_escape_wall_and_update_next_position returns false)', function ()
- assert.are_same(motion.air_motion_result(
- nil,
- vector(11, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing),
- true,
- false,
- false,
- nil
- ),
- motion_result
- )
+ setup(function ()
+ stub(player_char, "check_escape_wall_and_update_next_position", function (self, next_position, quadrant_horizontal_dir)
+ return false
end)
+ end)
- it('direction right into wall deeper than max_ground_escape_height should not move, and flag is_blocked_by_wall', function ()
- pc.velocity.x = 3
- pc.velocity.y = 0
+ teardown(function ()
+ player_char.check_escape_wall_and_update_next_position:revert()
+ end)
- local motion_result = motion.air_motion_result(
- nil,
- vector(-3, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ describe('(when compute_ground_sensors_query_info finds no ground at position)', function ()
- pc:next_air_step(directions.right, motion_result)
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, center_position)
+ return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+ end)
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(-3, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing),
- true,
- false,
- false,
- nil
- ),
- motion_result
- )
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
end)
- -- ceiling tests below also try sheer vs high angle (but no separate test with velocity.y 0 and not 0)
+ describe('(when compute_ceiling_sensors_query_info finds no ceiling at position)', function ()
- it('direction left into wall via ceiling downward and faster on x than y should not move, and flag is_blocked_by_wall', function ()
- -- important
- pc.velocity.x = -3
- pc.velocity.y = 2
+ setup(function ()
+ stub(player_char, "compute_ceiling_sensors_query_info", function (self, center_position)
+ return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+ end)
+ end)
- local motion_result = motion.air_motion_result(
- nil,
- vector(11, 7 + pc_data.full_height_standing - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ teardown(function ()
+ player_char.compute_ceiling_sensors_query_info:revert()
+ end)
- pc:next_air_step(directions.left, motion_result)
+ it('(velocity diagonal) should not change position and return air_motion_result with current position and no hits', function ()
+ pc.position = vector(4, 4)
+ -- non-zero velocity avoid early return and actually run collision checks,
+ -- but remember that check_air_collisions only tries to escape colliders,
+ -- the motion must have already been applied by called, i.e. (4, 8) is the final position already!
+ pc.velocity = vector(5, -12)
+
+ local result = pc:check_air_collisions()
+
+ assert.are_same(vector(4, 4), pc.position)
+ assert.are_same(motion.air_motion_result(
+ nil,
+ false,
+ false,
+ false,
+ nil
+ ), result)
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(11, 7 + pc_data.full_height_standing - pc_data.center_height_standing),
- true,
- false,
- false,
- nil
- ),
- motion_result
- )
end)
- it('direction left into wall via ceiling downward and slower on x than y should 1px left without being blocked', function ()
- -- important
- pc.velocity.x = -2
- pc.velocity.y = 3
+ describe('(when compute_ceiling_sensors_query_info finds horizontal ceiling at position)', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(11, 7 + pc_data.full_height_standing - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ setup(function ()
+ stub(player_char, "compute_ceiling_sensors_query_info", function (self, center_position)
+ return motion.ground_query_info(location(0, 0), -3, 0.5)
+ end)
+ end)
+
+ teardown(function ()
+ player_char.compute_ceiling_sensors_query_info:revert()
+ end)
- pc:next_air_step(directions.left, motion_result)
+ it('(velocity upward) should snap position to ceiling and return air_motion_result with new position and hit ceiling', function ()
+ pc.position = vector(4, 4)
+ -- remember that sheer angle allow ceiling adherence landing, so to avoid this case we use orthogonal angle
+ pc.velocity = vector(0, -8)
+
+ local result = pc:check_air_collisions()
+
+ -- y = 4, escape by -3 in quadrant up ie 3 downward => new y = 4 + 3 = 7
+ assert.are_same(vector(4, 7), pc.position)
+ assert.are_same(motion.air_motion_result(
+ nil, -- this is ground tile location, so not set if hitting ceiling (with no adherence)
+ false,
+ true, -- hit ceiling
+ false,
+ nil
+ ), result)
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(10, 7 + pc_data.full_height_standing - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
end)
- it('direction right into wall via ceiling downward and faster on x than y should not move, and flag is_blocked_by_wall', function ()
- -- important
- pc.velocity.x = 3
- pc.velocity.y = 2
+ describe('(when compute_ceiling_sensors_query_info finds ceiling at angle ceiling adherence limit (included) at position)', function ()
- local motion_result = motion.air_motion_result(
- nil,
- vector(-3, 7 + pc_data.full_height_standing - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ setup(function ()
+ stub(player_char, "compute_ceiling_sensors_query_info", function (self, center_position)
+ return motion.ground_query_info(location(0, 0), -3, 0.25 + pc_data.ceiling_adherence_catch_range_from_vertical)
+ end)
+ end)
+
+ teardown(function ()
+ player_char.compute_ceiling_sensors_query_info:revert()
+ end)
- pc:next_air_step(directions.right, motion_result)
+ it('(velocity upward) should snap position to ceiling and return air_motion_result with new position and hit ceiling', function ()
+ pc.position = vector(4, 4)
+ pc.velocity = vector(0, -8)
+
+ local result = pc:check_air_collisions()
+
+ -- y = 4, escape by -3 in quadrant up ie 3 downward => new y = 4 + 3 = 7
+ assert.are_same(vector(4, 7), pc.position)
+ assert.are_same(motion.air_motion_result(
+ location(0, 0), -- ground tile location is set when landing on ceiling
+ false,
+ false, -- NO hit ceiling when landing on ceiling
+ true, -- is landing (on ceiling!)
+ 0.25 + pc_data.ceiling_adherence_catch_range_from_vertical
+ ), result)
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(-3, 7 + pc_data.full_height_standing - pc_data.center_height_standing),
- true,
- false,
- false,
- nil
- ),
- motion_result
- )
end)
- it('direction right into wall via ceiling downward and slower on x than y should 1px right without being blocked', function ()
- -- important
- pc.velocity.x = 2
- pc.velocity.y = 3
+ end)
- local motion_result = motion.air_motion_result(
- nil,
- vector(-3, 7 + pc_data.full_height_standing - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ describe('(when compute_ground_sensors_query_info finds ground at position, character inside but not too deep)', function ()
- pc:next_air_step(directions.right, motion_result)
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, center_position)
+ return motion.ground_query_info(location(0, 0), -pc_data.max_ground_escape_height, atan2(8, 4))
+ end)
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(-2, 7 + pc_data.full_height_standing - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
end)
- it('(after landing in previous step) direction right onto new ground should move, set flag to NOT landing and update slope_angle to nil', function ()
- -- we wait next frame to actually land, else character will stay 1 px above ground
- -- this test specifically, however, is to check that is_landing: true and slope_angle: 0.5
- -- are reset when arrive just above ground, as it's not considered landing
- -- (if you change signed_distance_to_closest_ground >= 0 to ... > 0)
- -- in next_air_step it won't pass
+ describe('(when compute_ceiling_sensors_query_info finds no ceiling at position)', function ()
- pc.velocity.x = 1
- pc.velocity.y = 0
+ setup(function ()
+ stub(player_char, "compute_ceiling_sensors_query_info", function (self, center_position)
+ return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+ end)
+ end)
- local motion_result = motion.air_motion_result(
- location(0, 0),
- vector(-3, 0 - pc_data.center_height_standing),
- false,
- false,
- true,
- 0.5
- )
+ teardown(function ()
+ player_char.compute_ceiling_sensors_query_info:revert()
+ end)
- pc:next_air_step(directions.right, motion_result)
+ it('(velocity downward) should snap position to ground and return air_motion_result with new position and landing', function ()
+ pc.position = vector(4, 8)
+ pc.velocity = vector(0, 1)
+
+ local result = pc:check_air_collisions()
+
+ assert.are_same(vector(4, 8 - pc_data.max_ground_escape_height), pc.position)
+ assert.are_same(motion.air_motion_result(
+ location(0, 0),
+ false,
+ false,
+ true,
+ atan2(8, 4)
+ ), result)
+ end)
- assert.are_same(motion.air_motion_result(
- nil,
- vector(-2, 0 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
end)
- it('(after landing in previous step) direction left into the air should move and unset is_landing', function ()
- pc.velocity.x = -1
- pc.velocity.y = 0
+ end)
- local motion_result = motion.air_motion_result(
- location(0, 0),
- vector(-2, 0 - pc_data.center_height_standing),
- false,
- false,
- true,
- 0
- )
+ end)
- pc:next_air_step(directions.left, motion_result)
+ describe('(when check_escape_wall_and_update_next_position snaps character to left and return true)', function ()
- assert.are_same(motion.air_motion_result(
- nil,
- vector(-3, 0 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- ),
- motion_result
- )
+ setup(function ()
+ stub(player_char, "check_escape_wall_and_update_next_position", function (self, next_position, quadrant_horizontal_dir)
+ self.position.x = 2
+ return true
end)
+ end)
- end) -- (with flat ground)
-
- describe('(with steep curve top)', function ()
+ teardown(function ()
+ player_char.check_escape_wall_and_update_next_position:revert()
+ end)
- before_each(function ()
- -- i
- mock_mset(0, 0, tile_repr.visual_loop_bottomright_steepest)
- end)
-
- -- added to identify #129 BUG MOTION curve_run_up_fall_in_wall
- -- and accompany itest "fall on curve top"
- -- it was fixed by WALL LANDING ADJUSTMENT OFFSET
- it('direction down into steep curve should move, flag is_landing with slope_angle atan2(3, -8) but above all adjust position X to the left so feet just stand on the slope', function ()
- pc.velocity.x = 0
- pc.velocity.y = 3
-
- local motion_result = motion.air_motion_result(
- nil,
- -- used to be 0 -, now it's 1 - since we removed the top pixel of the steepest slope
- -- when fixing #132 (see corresponding utest)
- vector(5, 1 - pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ describe('(when compute_ground_sensors_query_info finds no ground at position)', function ()
- pc:next_air_step(directions.down, motion_result)
+ setup(function ()
+ stub(player_char, "compute_ground_sensors_query_info", function (self, center_position)
+ return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+ end)
+ end)
- assert.are_same(motion.air_motion_result(
- location(0, 0),
- -- kinda arbitrary offset of 6, but based on character data
- vector(-1, 1 - pc_data.center_height_standing),
- false,
- false,
- true,
- atan2(3, -8)
- ),
- motion_result
- )
+ teardown(function ()
+ player_char.compute_ground_sensors_query_info:revert()
end)
- end)
+ describe('(when compute_ceiling_sensors_query_info finds no ceiling at position)', function ()
- -- testing landing on ceiling aka ceiling adherence catch
- describe('(with ceiling top-left and top-right 45-deg corners)', function ()
+ setup(function ()
+ stub(player_char, "compute_ceiling_sensors_query_info", function (self, center_position)
+ return motion.ground_query_info(nil, pc_data.max_ground_snap_height + 1, nil)
+ end)
+ end)
- before_each(function ()
- -- 45
- mock_mset(0, 0, tile_repr.visual_topleft_45)
- mock_mset(1, 0, tile_repr.visual_topright_45)
- end)
+ teardown(function ()
+ player_char.compute_ceiling_sensors_query_info:revert()
+ end)
- it('direction up into top-left corner should land on (adhere to) ceiling', function ()
- pc.velocity.x = 0
- pc.velocity.y = -3
+ it('(velocity right) should snap position to left and return air_motion_result with new position and hit wall', function ()
+ pc.position = vector(4, 4)
+ pc.velocity = vector(1, 0)
- local motion_result = motion.air_motion_result(
- nil,
- -- column 4 in topleft tile should have downward column of height 6
- vector(4, 6 + pc_data.center_height_standing),
- false,
- false,
- false,
- nil
- )
+ local result = pc:check_air_collisions()
- pc:next_air_step(directions.down, motion_result)
+ assert.are_same(vector(2, 4), pc.position)
+ assert.are_same(motion.air_motion_result(
+ nil,
+ true, -- hit wall
+ false,
+ false,
+ nil
+ ), result)
+ end)
- assert.are_same(motion.air_motion_result(
- location(0, 0),
- vector(4, 6 + pc_data.center_height_standing),
- false,
- false,
- true, -- is_landing
- atan2(-8, 8)
- ),
- motion_result
- )
end)
end)
- end) -- (with mock tiles data setup)
+ end)
- end) -- next_air_step
+ end)
describe('trigger_spring', function ()
@@ -7846,6 +6952,7 @@ describe('player_char', function ()
stage_state.extend_spring:clear()
player_char.enter_motion_state:clear()
player_char.play_low_priority_sfx:clear()
+
mock_spring_up.extend:clear()
mock_spring_left.extend:clear()
mock_spring_right.extend:clear()
@@ -8076,7 +7183,7 @@ describe('player_char', function ()
update_velocity_component_debug_stub:revert()
end)
- it('should call _update_velocity_component_debug on each component', function ()
+ it('should call update_velocity_component_debug on each component', function ()
pc:update_velocity_debug()
assert.spy(update_velocity_component_debug_stub).was_called(2)
assert.spy(update_velocity_component_debug_stub).was_called_with(match.ref(pc), "x")
@@ -8226,17 +7333,17 @@ describe('player_char', function ()
setup(function ()
-- spy.on would help testing more deeply, but we prefer utests independent
-- from other modules nor animation data
- stub(animated_sprite, "play")
+ stub(player_char, "update_sprite_row_and_play_sprite_animation")
end)
teardown(function ()
- animated_sprite.play:revert()
+ player_char.update_sprite_row_and_play_sprite_animation:revert()
end)
- -- since pc is init in before_each and init calls _setup
- -- which calls pc.anim_spr:play, we must clear call count just after that
+ -- since pc is init in before_each and init calls setup
+ -- which calls pc.anim_spr:play("idle"), we must clear call count just after that
before_each(function ()
- animated_sprite.play:clear()
+ player_char.update_sprite_row_and_play_sprite_animation:clear()
end)
it('should play brake start animation (and preserve brake_anim_phase) when brake_anim_phase: 1 and return immediately', function ()
@@ -8247,8 +7354,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "brake_start")
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "brake_start")
assert.are_equal(1, pc.brake_anim_phase)
end)
@@ -8266,8 +7373,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "brake_reverse")
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "brake_reverse")
assert.are_equal(2, pc.brake_anim_phase)
end)
@@ -8285,11 +7392,11 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(2)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(2)
-- tentative play -> not playing anymore
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "brake_reverse")
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "brake_reverse")
-- fallback based on motion_state
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "idle")
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "idle")
assert.are_equal(0, pc.brake_anim_phase)
end)
@@ -8300,8 +7407,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "idle")
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "idle")
end)
it('should play walk anim with walk_anim_min_play_speed when standing and ground speed is lower than anim_run_speed in abs (clamping)', function ()
@@ -8311,8 +7418,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "walk", false, pc_data.walk_anim_min_play_speed)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "walk", false, pc_data.walk_anim_min_play_speed)
end)
it('should play walk anim with last anim_run_speed when standing and ground speed is low', function ()
@@ -8322,8 +7429,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "walk", false, 2.9)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "walk", false, 2.9)
end)
it('should play run anim with last anim_run_speed when standing and ground speed is high', function ()
@@ -8333,8 +7440,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "run", false, 3.0)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "run", false, 3.0)
end)
it('should play spring_jump when "falling upward" with should_play_spring_jump: true', function ()
@@ -8343,8 +7450,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "spring_jump")
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "spring_jump")
end)
it('(low anim speed) should stop spring_jump anim and play walk anim at walk_anim_min_play_speed when falling with should_play_spring_jump: true but velocity.y > 0 (falling down again) and anim run speed is lower than anim_run_speed in abs (clamping)', function ()
@@ -8355,8 +7462,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "walk", false, pc_data.walk_anim_min_play_speed)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "walk", false, pc_data.walk_anim_min_play_speed)
end)
it('(low anim speed) should stop spring_jump anim and play walk anim when falling with should_play_spring_jump: true but velocity.y > 0 (falling down again)', function ()
@@ -8367,8 +7474,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "walk", false, 2.9)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "walk", false, 2.9)
end)
it('(high anim speed) should stop spring_jump anim and play run anim when falling with should_play_spring_jump: true but velocity.y > 0 (falling down again)', function ()
@@ -8379,8 +7486,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "run", false, 3.0)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "run", false, 3.0)
end)
it('(low anim speed) should play walk anim with last anim_run_speed when falling and should_play_spring_jump is false', function ()
@@ -8389,8 +7496,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "walk", false, 2.9)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "walk", false, 2.9)
end)
it('(high anim speed)should play run anim with last anim_run_speed when falling and should_play_spring_jump is false', function ()
@@ -8399,8 +7506,27 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "run", false, 3.0)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "run", false, 3.0)
+ end)
+
+ it('should play crouch anim when crouching (even when sliding)', function ()
+ pc.motion_state = motion_states.crouching
+ pc.ground_speed = 0
+
+ pc:check_play_anim()
+
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "crouch")
+ end)
+
+ it('should *not* play spin_dash anim when spin dashing', function ()
+ pc.motion_state = motion_states.spin_dashing
+
+ pc:check_play_anim()
+
+ -- exceptionally not playing anim from here, see comment in method
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_not_called()
end)
it('(air spin with anim_run_speed below air_spin_anim_min_play_speed) should play spin anim at air_spin_anim_min_play_speed', function ()
@@ -8409,8 +7535,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "spin", false, pc_data.air_spin_anim_min_play_speed)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "spin", false, pc_data.air_spin_anim_min_play_speed)
end)
it('(air spin with anim_run_speed above air_spin_anim_min_play_speed) should play spin_fast anim at anim_run_speed', function ()
@@ -8419,8 +7545,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "spin", false, pc_data.air_spin_anim_min_play_speed + 1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "spin", false, pc_data.air_spin_anim_min_play_speed + 1)
end)
-- rolling uses the same animation as air_spin but with a different minimum, so we check this threshold instead
@@ -8431,8 +7557,8 @@ describe('player_char', function ()
pc:check_play_anim()
- assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "spin", false, pc_data.rolling_spin_anim_min_play_speed)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "spin", false, pc_data.rolling_spin_anim_min_play_speed)
end)
it('(rolling with anim_run_speed above rolling_spin_anim_min_play_speed) should play spin_fast anim at rolling_spin_anim_min_play_speed', function ()
@@ -8441,8 +7567,89 @@ describe('player_char', function ()
pc:check_play_anim()
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called(1)
+ assert.spy(player_char.update_sprite_row_and_play_sprite_animation).was_called_with(match.ref(pc), "spin", false, pc_data.rolling_spin_anim_min_play_speed + 1)
+ end)
+
+ end)
+
+ describe('update_sprite_row_and_play_sprite_animation', function ()
+
+ setup(function ()
+ stub(_G, "memcpy")
+ stub(animated_sprite, "play")
+ stub(player_char, "update_sprite_angle_parameters")
+ end)
+
+ teardown(function ()
+ memcpy:revert()
+ animated_sprite.play:revert()
+ player_char.update_sprite_angle_parameters:revert()
+ end)
+
+ -- since pc is init in before_each and init calls setup
+ -- which calls update_sprite_row_and_play_sprite_animation itself,
+ -- we must clear spies before_each
+ before_each(function ()
+ memcpy:clear()
+ animated_sprite.play:clear()
+ player_char.update_sprite_angle_parameters:clear()
+ end)
+
+ it('should play animation on anim_spr passing the same arguments', function ()
+ pc:update_sprite_row_and_play_sprite_animation("spin_dash", true, 2)
+
assert.spy(animated_sprite.play).was_called(1)
- assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "spin", false, pc_data.rolling_spin_anim_min_play_speed + 1)
+ assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "spin_dash", true, 2)
+ end)
+
+ it('should call update_sprite_angle_parameters', function ()
+ pc:update_sprite_row_and_play_sprite_animation("spin_dash", true, 2)
+
+ assert.spy(player_char.update_sprite_angle_parameters).was_called(1)
+ assert.spy(player_char.update_sprite_angle_parameters).was_called_with(match.ref(pc))
+ end)
+
+ it('(run) should cardinal row containing run sprites from general memory to spritesheet', function ()
+ pc:update_sprite_row_and_play_sprite_animation("run", true, 2)
+
+ assert.spy(memcpy).was_called(1)
+ -- too many calls to check them all, but test at least the first ones of each
+ -- to verify addr_offset is correct
+ assert.spy(memcpy).was_called_with(0x1000, 0x5300, 0x400)
+ end)
+
+ it('(run, is_sprite_diagonal: true) should still copy diagonal run sprites as part of 2 full rows to simplify, from general memory to spritesheet', function ()
+ pc.is_sprite_diagonal = true
+
+ pc:update_sprite_row_and_play_sprite_animation("run", true, 2)
+
+ assert.spy(memcpy).was_called(1)
+ assert.spy(memcpy).was_called_with(0x1000, 0x5320, 0x400)
+ end)
+
+ it('(spin_dash) should copy partial lines of spin dash sprites from general to spritesheet', function ()
+ pc:update_sprite_row_and_play_sprite_animation("spin_dash", true, 2)
+
+ assert.spy(memcpy).was_called(16)
+ -- too many calls to check them all, but test at least the first ones of each
+ -- to verify addr_offset is correct
+ assert.spy(memcpy).was_called_with(0x1000, 0x5b00, 0x28)
+ assert.spy(memcpy).was_called_with(0x1040, 0x5b28, 0x28)
+ end)
+
+ it('(spin_dash) should set last_copied_double_row to match anim', function ()
+ pc:update_sprite_row_and_play_sprite_animation("spin_dash")
+
+ assert.are_equal(pc.last_copied_double_row, 4)
+ end)
+
+ it('(spin_dash, already copied row) should not memcpy again at all', function ()
+ pc.last_copied_double_row = 4
+
+ pc:update_sprite_row_and_play_sprite_animation("spin_dash")
+
+ assert.spy(memcpy).was_not_called()
end)
end)
@@ -8516,164 +7723,54 @@ describe('player_char', function ()
end)
- describe('reload_rotated_sprites', function ()
-
- setup(function ()
- stub(_G, "memcpy")
- end)
-
- teardown(function ()
- memcpy:revert()
- end)
-
- after_each(function ()
- memcpy:clear()
- end)
-
- it('should copy non-rotated sprites in general memory back to spritesheet', function ()
- pc:reload_rotated_sprites()
-
- assert.spy(memcpy).was_called(32)
- -- too many calls to check them all, but test at least the first ones of each
- -- to verify addr_offset is correct
- assert.spy(memcpy).was_called_with(0x1008, 0x5300, 0x30)
- assert.spy(memcpy).was_called_with(0x1048, 0x5330, 0x30)
- assert.spy(memcpy).was_called_with(0x1400, 0x5600, 0x20)
- assert.spy(memcpy).was_called_with(0x1440, 0x5620, 0x20)
- end)
-
- it('should copy rotated sprites in general memory back to spritesheet', function ()
- pc:reload_rotated_sprites(true)
-
- assert.spy(memcpy).was_called(32)
- -- too many calls to check them all, but test at least the first ones of each
- -- to verify addr_offset is correct
- assert.spy(memcpy).was_called_with(0x1008, 0x5800, 0x30)
- assert.spy(memcpy).was_called_with(0x1048, 0x5830, 0x30)
- assert.spy(memcpy).was_called_with(0x1400, 0x5b00, 0x20)
- assert.spy(memcpy).was_called_with(0x1440, 0x5b20, 0x20)
- end)
-
- end)
-
describe('render', function ()
setup(function ()
stub(animated_sprite, "render")
- stub(player_char, "reload_rotated_sprites")
+ stub(pfx, "render")
end)
teardown(function ()
animated_sprite.render:revert()
- player_char.reload_rotated_sprites:revert()
+ pfx.render:revert()
end)
after_each(function ()
animated_sprite.render:clear()
- player_char.reload_rotated_sprites:clear()
+ pfx.render:clear()
end)
- describe('(brake_start)', function ()
-
- before_each(function ()
- pc.anim_spr.current_anim_key = "brake_start"
- end)
-
- it('(when character is facing left, any angle) should only call render on sonic sprite data: idle with the character\'s position floored, flipped x, angle 0', function ()
- pc.position = vector(12.5, 8.2)
- pc.orientation = horizontal_dirs.left
- pc.continuous_sprite_angle = 0.25
-
- pc:render()
-
- assert.spy(player_char.reload_rotated_sprites).was_not_called()
-
- assert.spy(animated_sprite.render).was_called(1)
- -- sprite is already rotated by 45, so the additional angle is 0
- assert.spy(animated_sprite.render).was_called_with(match.ref(pc.anim_spr), vector(12, 8), true, false, 0)
- end)
-
- it('(when character is facing right, any angle) should only call render on sonic sprite data: idle with the character\'s position floored, not flipped x, angle 0', function ()
- pc.position = vector(12.5, 8.2)
- pc.orientation = horizontal_dirs.right
- pc.continuous_sprite_angle = 0.75
-
- pc:render()
+ -- but it doesn't really matter, since render is not responsible for calculating sprite angle any more,
+ -- so no need to test various states any more to check for angle reset, etc. just set sprite_angle
+ -- to some multiple of 0.25, and test flip_x: true and false
- assert.spy(player_char.reload_rotated_sprites).was_not_called()
+ it('(when character is facing left, closer to cardinal angle) should call render on sonic sprite data: idle with the character\'s position floored, flipped x, current slope angle rounded to closest 45-degree step', function ()
+ pc.position = vector(12.5, 8.2)
+ pc.orientation = horizontal_dirs.left
+ -- optional, sprite_angle is what matters now
+ -- pc.continuous_sprite_angle = 0.25 - 0.0624 -- closer to 0.25 than 0.125
+ -- pc.is_sprite_diagonal = false
+ pc.sprite_angle = 0.25
- assert.spy(animated_sprite.render).was_called(1)
- -- note that we don't apply modulo and count on render to detect that 1 == 0 [1], so we effectively pass 1
- assert.spy(animated_sprite.render).was_called_with(match.ref(pc.anim_spr), vector(12, 8), false, false, 0)
- end)
+ pc:render()
+ assert.spy(animated_sprite.render).was_called(1)
+ assert.spy(animated_sprite.render).was_called_with(match.ref(pc.anim_spr), vector(12, 8), true, false, 0.25)
end)
- describe('(walk)', function ()
-
- before_each(function ()
- pc.anim_spr.current_anim_key = "walk"
- end)
-
- it('(when character is facing left, closer to cardinal angle) should reload non-rotated sprites and call render on sonic sprite data: idle with the character\'s position floored, flipped x, current slope angle rounded to closest 45-degree step', function ()
- pc.position = vector(12.5, 8.2)
- pc.orientation = horizontal_dirs.left
- pc.continuous_sprite_angle = 0.25 - 0.0624 -- closer to 0.25 than 0.125
-
- pc:render()
-
- assert.spy(player_char.reload_rotated_sprites).was_called(1)
- assert.spy(player_char.reload_rotated_sprites).was_called_with(match.ref(pc))
-
- assert.spy(animated_sprite.render).was_called(1)
- assert.spy(animated_sprite.render).was_called_with(match.ref(pc.anim_spr), vector(12, 8), true, false, 0.25)
- end)
-
- it('(when character is facing left, closer to diagonal angle) should reload 45-degree rotated sprites and call render on sonic sprite data: idle with the character\'s position floored, flipped x, current slope angle rounded to closest 45-degree step MINUS 45 deg', function ()
- pc.position = vector(12.5, 8.2)
- pc.orientation = horizontal_dirs.left
- pc.continuous_sprite_angle = 0.0626 -- closer to 0.125 than 0
-
- pc:render()
-
- assert.spy(player_char.reload_rotated_sprites).was_called(1)
- assert.spy(player_char.reload_rotated_sprites).was_called_with(match.ref(pc), true)
-
- assert.spy(animated_sprite.render).was_called(1)
- -- sprite is already rotated by 45, so the additional angle is 0
- assert.spy(animated_sprite.render).was_called_with(match.ref(pc.anim_spr), vector(12, 8), true, false, 0.25)
- end)
-
- it('(when character is facing right, closer to cardinal angle) should reload non-rotated sprites and call render on sonic sprite data: idle with the character\'s position floored, not flipped x, current slope angle rounded to closest 45-degree step', function ()
- pc.position = vector(12.5, 8.2)
- pc.orientation = horizontal_dirs.right
- pc.continuous_sprite_angle = 1 - 0.0624 -- closer to 1 (i.e. 0 modulo 1) than 0.875
-
- pc:render()
-
- assert.spy(player_char.reload_rotated_sprites).was_called(1)
- assert.spy(player_char.reload_rotated_sprites).was_called_with(match.ref(pc))
-
- assert.spy(animated_sprite.render).was_called(1)
- -- note that we don't apply modulo and count on render to detect that 1 == 0 [1], so we effectively pass 1
- assert.spy(animated_sprite.render).was_called_with(match.ref(pc.anim_spr), vector(12, 8), false, false, 1)
- end)
-
- it('(when character is facing right, closer to diagonal angle) should reload 45-degree rotated sprites and call render on sonic sprite data: idle with the character\'s position floored, not flipped x, current slope angle rounded to closest 45-degree step MINUS 45 deg', function ()
- pc.position = vector(12.5, 8.2)
- pc.orientation = horizontal_dirs.right
- pc.continuous_sprite_angle = 0.875 + 0.0624 -- closer to 0.875 than 1 (0 modulo 1)
-
- pc:render()
-
- assert.spy(player_char.reload_rotated_sprites).was_called(1)
- assert.spy(player_char.reload_rotated_sprites).was_called_with(match.ref(pc), true)
+ it('(when character is facing right, closer to diagonal angle) should call render on sonic sprite data: idle with the character\'s position floored, not flipped x, current slope angle rounded to closest 45-degree step MINUS 45 deg', function ()
+ pc.position = vector(12.5, 8.2)
+ pc.orientation = horizontal_dirs.right
+ -- optional, sprite_angle is what matters now
+ -- pc.continuous_sprite_angle = 0.875 + 0.0624 -- closer to 0.875 than 1 (0 modulo 1)
+ -- pc.is_sprite_diagonal = true
+ -- sprite is already rotated by 45 = 0.125 pico8 angle unit, so the additional angle is 0.875 - 0.125 = 0.75
+ pc.sprite_angle = 0.75
- assert.spy(animated_sprite.render).was_called(1)
- -- sprite is already rotated by 45, so the additional angle is only 0.75
- assert.spy(animated_sprite.render).was_called_with(match.ref(pc.anim_spr), vector(12, 8), false, false, 0.75)
- end)
+ pc:render()
+ assert.spy(animated_sprite.render).was_called(1)
+ assert.spy(animated_sprite.render).was_called_with(match.ref(pc.anim_spr), vector(12, 8), false, false, 0.75)
end)
end)
diff --git a/src/ingame/spring.lua b/src/ingame/spring.lua
index 89a860c5..fd2827f3 100644
--- a/src/ingame/spring.lua
+++ b/src/ingame/spring.lua
@@ -1,4 +1,4 @@
-local stage_data = require("data/stage_data")
+local stage_common_data = require("data/stage_common_data")
local world = require("platformer/world")
-- visual requires ingame add-on to have access to spring sprite data
local visual = require("resources/visual_common")
@@ -24,7 +24,7 @@ end
function spring:extend()
-- if spring was already extended, simply reset the timer
-- collision mask doesn't change anyway
- self.extended_timer = stage_data.spring_extend_duration
+ self.extended_timer = stage_common_data.spring_extend_duration
end
function spring:update()
@@ -80,15 +80,6 @@ function spring:render()
-- however, flip is applied *before* rotation so we need to flip on Y to actually flip on X
flip_y = true
angle = 0.25
-
- -- unfortunately using flip Y has the side effect of messing up visual pivot,
- -- so we must offset adjusted pivot (which is still correct for physics trigger check)
- -- depending on whether sprite is extended or not
- if self.extended_timer > 0 then
- adjusted_pivot.x = adjusted_pivot.x + 4
- else
- adjusted_pivot.x = adjusted_pivot.x - 4 -- 6-4 = 2 so we now got the same adjusted pivot as left
- end
end
if self.extended_timer > 0 then
diff --git a/src/ingame/spring_utest.lua b/src/ingame/spring_utest.lua
index 38765d2f..8026de90 100644
--- a/src/ingame/spring_utest.lua
+++ b/src/ingame/spring_utest.lua
@@ -4,7 +4,7 @@ local spring = require("ingame/spring")
local sprite_data = require("engine/render/sprite_data")
-local stage_data = require("data/stage_data")
+local stage_common_data = require("data/stage_common_data")
local visual = require("resources/visual_common")
describe('spring', function ()
@@ -33,7 +33,7 @@ describe('spring', function ()
it('should reset extended_timer to spring extend duration"', function ()
local spring_obj = spring(directions.up, location(2, 1))
spring_obj:extend()
- assert.are_equal(stage_data.spring_extend_duration, spring_obj.extended_timer)
+ assert.are_equal(stage_common_data.spring_extend_duration, spring_obj.extended_timer)
end)
end)
@@ -149,7 +149,7 @@ describe('spring', function ()
spring_obj:render()
assert.spy(sprite_data.render).was_called(1)
- assert.spy(sprite_data.render).was_called_with(match.ref(visual.sprite_data_t.spring), vector(18, 10), false, true, 0.25)
+ assert.spy(sprite_data.render).was_called_with(match.ref(visual.sprite_data_t.spring), vector(22, 10), false, true, 0.25)
end)
it('(right, extended_timer > 0) should draw spring extended sprite data from top-left location, flipped y and rotated by 90 with offset adjustment', function ()
@@ -159,7 +159,7 @@ describe('spring', function ()
spring_obj:render()
assert.spy(sprite_data.render).was_called(1)
- assert.spy(sprite_data.render).was_called_with(match.ref(visual.sprite_data_t.spring_extended), vector(26, 10), false, true, 0.25)
+ assert.spy(sprite_data.render).was_called_with(match.ref(visual.sprite_data_t.spring_extended), vector(22, 10), false, true, 0.25)
end)
end)
diff --git a/src/ingame/stage_state.lua b/src/ingame/stage_state.lua
index ac53dd9a..d32dec0d 100644
--- a/src/ingame/stage_state.lua
+++ b/src/ingame/stage_state.lua
@@ -4,11 +4,13 @@ local base_stage_state = require("ingame/base_stage_state")
local emerald = require("ingame/emerald")
local emerald_fx = require("ingame/emerald_fx")
local goal_plate = require("ingame/goal_plate")
-local player_char = require("ingame/playercharacter")
local spring = require("ingame/spring")
+local stage_common_data = require("data/stage_common_data")
local stage_data = require("data/stage_data")
local audio = require("resources/audio")
+local memory = require("resources/memory")
local visual = require("resources/visual_common") -- we should require ingameadd-on in main
+local visual_ingame_data = require("resources/visual_ingame_numerical_data")
local visual_stage = require("resources/visual_stage")
local stage_state = derived_class(base_stage_state)
@@ -22,7 +24,7 @@ function stage_state:init()
self.curr_stage_id = 1
-- reference to current stage data (derived from curr_stage_id)
- self.curr_stage_data = stage_data.for_stage[self.curr_stage_id]
+ self.curr_stage_data = stage_data[self.curr_stage_id]
-- player character
-- self.player_char = nil -- commented out to spare characters
@@ -39,6 +41,13 @@ function stage_state:init()
-- list of emerald pick fxs playing (currently no pooling, just add and delete)
self.emerald_pick_fxs = {}
+ -- overlap tiles: tiles that are overlapping another tile in the tilemap and cannot be defined directly
+ -- in tilemap data, but can be stored in advance and mset on every region reload
+ -- they can be midground or foreground, it's the sprite flag that decides how they are rendered
+ -- since they are rendered as part of the tilemap
+ -- format: {{global tile location, sprite_id}, ...}
+ self.overlap_tiles = {}
+
-- spring objects
self.springs = {}
@@ -55,10 +64,6 @@ end
--#endif
function stage_state:on_enter()
- -- don't initialize loaded region coords to force first
- -- (we don't know in which region player character will spawn)
- -- self.loaded_map_region_coords = nil
-
-- to avoid scanning object tiles to spawn new objects every time a new region is loaded,
-- we preload all map regions on stage start and spawn
@@ -76,8 +81,14 @@ function stage_state:on_enter()
--[[#pico8
--#ifn itest
self:spawn_objects_in_all_map_regions()
+
+--#if normal_mode
+--(attract mode doesn't care about remembering picked emeralds)
self:restore_picked_emerald_data()
--#endif
+
+--(ifn itest)
+--#endif
--#pico8]]
-- make sure to reload map region where player character will be before spawning player character,
@@ -89,118 +100,151 @@ function stage_state:on_enter()
self.camera:setup_for_stage(self.curr_stage_data)
self:check_reload_map_region()
+ -- must be done before spawn_player_char so the player character can access
+ -- the initial anim spritesheet in its init > update_sprite_row_and_play_sprite_animation
+ self:reload_runtime_data()
+
self:spawn_player_char()
self.camera.target_pc = self.player_char
self.has_player_char_reached_goal = false
- -- reload bgm only once, then we can play bgm whenever we want for this stage
- self:reload_bgm()
-- initial play bgm
self:play_bgm()
-
- self:reload_runtime_data()
end
-- reload background, HUD and character sprites from runtime data
-- also store non-rotated and rotated sprites into general memory for swapping later
function stage_state:reload_runtime_data()
- -- reload runtime background+HUD sprites by copying spritesheet top from background data
- -- cartridge to the top of the current spritesheet, just to overwrite
- -- spritesheet starts at 0x0 in memory
- -- we need to copy 3 rows of 16 sprites, 32 = 0x20 bytes per sprite,
- -- so 512 = 0x200 bytes per row,
- -- so 1536 = 0x600 bytes
+ -- in v3, the builtin contains *only* collision masks so we must reload the *full* spritesheet
+ -- for stage ingame, hence reload memory length 0x2000
-- NOTE: we are *not* reloading sprite flags (could do by copying 0x100 bytes from 0x3000-0x30ff)
- -- which means our builtin spritesheet *must* contain any new flags brought by runtime extra tiles
- -- (located in the 3 top rows of the spritesheet). Those are rare (only one-way platform tiles)
- -- but without the flags, they won't behave properly. This means you must (unintuitively) place flags
- -- on the mask tiles in the built-in spritesheet. To make it easier, you may edit the flags on a cartridge
- -- containing all the sprites of interest, then copy-paste the __gff__ lines into the builtin .p8 cartridge
- -- Later, you can move all mask tiles to another spritesheet to reload
- -- on start instead.
- local runtime_data_path = "data_stage"..self.curr_stage_id.."_runtime"..cartridge_ext
- reload(0x0, 0x0, 0x600, runtime_data_path)
-
- -- the runtime spritesheet also contains 45-degree rotated sprite variants
- -- for Sonic walk and run cycle, meant to replace the non-rotated equivalents
- -- in the built-in spritesheet
- -- we don't want to reload data files (neither the built-in nor the runtime)
- -- every time Sonic changes 1/8 rotation, so we copy both the non-rotated
- -- and rotated variants now into in general memory, so we can swap them later
-
- -- 1st argument of memcpy is destination, and is always general memory address
- -- (0x4300) + some offset
- -- first, we need to spare 0x1000 for reload_..._map_region methods
- -- which use the general memory as temporary storage for patching operations
- -- second, we prefer keeping memory compact by copying the sprites one after
- -- the other in general memory, rather than following the spritesheet layout
- -- (even if there is a hole after a rotated sprite because it made sprite
- -- alignment look better, we don't have a similar hole in general memory)
- -- so, we start at 0x5300 and then simply add the length of the previous memcpy
- -- operation to get the next dest address
-
- -- the non-rotated variants are already in current memory, so just copy them
-
- -- the walk cycle sprites start at row 8, column 2 (counting from 0)
- -- sprites are stored in memory line by line, 1 cell being 8x8 px, and
- -- 8px are represented by 4 bytes, so top-left address is at:
- -- 8 * 0x200 + 2 * 0x4 = 0x1008
- -- there are 6 walk sprites of span 2x2, which don't cover the whole row of 16 cells of 8x8
- -- and need to copy 8 * 2 = 16 partial lines over 6 * 2 = 12 cells of 8x8, which makes
- -- 12 * 4 = 48 = 0x30 bytes per line
- -- to go to the next line, advance px by px over 16 cells (number of cells in a row)
- -- of 8x8, which makes 16 * 4 = 64 = 0x40 (it's also 0x200 / 8 since 0x200 advances
- -- by 1 full row of sprites, each sprite containing 8 lines)
-
- -- we don't have to keep the same structure in general memory, as we don't debug it
- -- visually, which means we can just paste partial lines of 0x30 one after the other
- -- for the most compact memory (but represented with line breaks every 0x40 like
- -- the spritesheet, it would look garbage due to the offset of 0x10 every line)
- -- we could also keep the structure aligned by pasting every 0x40, then to fill the holes
- -- (2 sprites of span 2x2 left at the end), we could place 2 run cycle sprites
- -- but we'd still need to place the remaining 2 run cycle sprites 2 rows below,
- -- occupying 2 partial lines, and if we want to use general memory for non-sprite
- -- data later then we don't have the most compact memory as we'd have holes for each
- -- partial line but the last one
-
- -- the run cycle sprites follow the same logic, but they start at row 10, column 0,
- -- and there are 4 sprites of span 2x2, so their topleft address is at:
- -- 10 * 0x200 = 0x1400
- -- and we need 8 * 2 = 16 partial lines over 4 * 2 = 8 cells of 8x8,
- -- so 8 * 4 = 32 = 0x20 bytes per line
-
- -- to make code shorter, let's copy in parallel the 16 lines for the walk and run sprites
- -- each time, we advance dest address by the same value as copied length, so 0x30 and 0x20
- -- resp.
+ -- which means our builtin spritesheet *must* contain all the sprite flags.
+ -- in v3, this is terrible since the built-in data only shows collision masks, but works because we already
+ -- defined all the sprite flags during v2
+ -- if we start adding/moving tiles around and changing sprite flags, then I'd strongly recommend
+ -- setting the flags on the spritesheets actually showing the tiles, and copy the flags from them
+ -- with the addresses mentioned above
+ -- OR if you really need to spare code characters, copy-paste the __gff__ lines into the builtin .p8 cartridge
+ -- manually.
+ local runtime_data_path = "data_stage"..self.curr_stage_id.."_ingame.p8"
+ reload(0x0, 0x0, 0x2000, runtime_data_path)
+
+ -- Sonic spritesheet
+
+ -- The Sonic spritesheet contains all the sprites for Sonic, including 45-degree rotated variants.
+ -- There are all meant to be copied into rows of index 8-9 in runtime memory spritesheet,
+ -- at runtime when Sonic changes state and needs to play certain sprite animations.
+ -- However, reading memory directly from a cartridge data with reload() is a bit slow.
+ -- (with the fast-reload patch, it's fast enough to be called from times to times as during
+ -- stage region transitions, but Sonic can change state much faster than that; experience seems
+ -- to indicate that reloading data from the same cartridge multiple times in a row is faster
+ -- than reloading data from different cartridges, but we prefer not relying on that at the moment;
+ -- this property is useful to make the repeated reload below faster though)
+ -- Therefore we copy the content of the whole Sonic spritesheet in general memory for super-fast
+ -- copy from internal memory to internal memory at runtime.
+
+ -- Address start/size calculation
+ --
+ -- Sprites are stored in memory line by line.
+ -- 1 pixel = 4 bits (as we use 16 colors)
+ -- 2 pixels = 8 bits = 1 byte
+ -- 8 pixels = 4 bytes
+ -- 1 cell occupies 8x8 pixels, and needs 16 bytes.
+ -- A 2x2-cell sprite = 16x16 pixels = 256 pixels = 128 bytes = 0x80 bytes
+ -- This is useful to count the total memory required by a sprite, and works when dealing with a row fully occupied by sprites
+ -- to copy, as we don't care in which order pixels were copied.
+ --
+ -- However, when copying partial lines, it is imperative to think in terms of lines, partial or complete,
+ -- to determine start addresses and copy lengths:
+ -- 1 cell line = 8 pixels = 4 bytes
+ -- 1 line = 8*16 pixels = 128 pixels = 64 bytes = 0x40 bytes
+ -- 1 row = 8 lines = 0x200 bytes
+ -- 1 double row = 2 rows (what we actually use since our sprites are 2x2) needs 0x400 bytes
+ -- The first Sonic sprites are located on the left of row index 2, so at address offset 0x400
+ -- Spritesheet memory starts at 0x0, therefore the first address to copy from really is 0x400
+
+ -- Address mapping
+ --
+ -- General memory starts at 0x4300, but we use the first blocks of memory for temporary operations.
+ -- Then we start copying Sonic sprites. The first 4 double rows (8 rows) are easy to copy, as rows are fully occupied.
+ -- For the last double row (2 rows), there are only partially filled with Sonic sprites, so to avoid wasting memory copying
+ -- holes (which would result in stopping just before address 0x5f00, at 0x5eff, which is beyond the end of general memory
+ -- 0x5dff), we only copy partial lines of just what we need (spanning over 10 cells / 5 spin dash sprites each time).
+ -- Note that we will also need to copy the partial lines back one by one to reconstruct the sprites properly in runtime
+ -- spritesheet memory.
+ --
+ -- Below, Dest is the address to copy sprites to in general memory (for later usage).
+ -- Source is the address to copy from data_stage_sonic.p8 cartridge, which contains the Sonic spritesheet.
+ -- As explained above in `Address start/size calculation`, it starts on row index 2, therefore at 0x400.
+ -- The first entry is just a reminder that temporary memory is used at the start of general memory.
+ -- We don't copy sprites there.
+
+ -- Dest Source Size Content
+ -- 0x4300 0x800 Temporary memory for stage region patching (see reload_..._map_region methods)
+ -- 0x4b00 0x400 0x1000 First 4 double rows of Sonic sprites = first 8 rows of Sonic sprites (sprites occupy 2x2 cells)
+ -- 0x5b00 0x1400 0x280 Last 5 Sonic sprites = 10x2 cells located on rows of indices 10-11 (spin dash sprites)
+ -- 0x5d80 0x1680 Free from here, use it if you need to copy more things that need to be available quickly
+
+ -- Total size for sprites: 0x1280
+
+-- Comment for RELOAD
+
+-- Below, Dest is the address to copy with on runtime memory. Spritesheet memory starts at 0x0, but we always copy
+-- Sonic sprites on row indices 8-9 (because we put a hole there in the stage spritesheets, and even kept the foot
+-- of Sonic jump sprite sticking out on row index 10 to complement!), so we start at 8*0x200 = 0x1000 (right in the middle
+-- of spritesheet memory).
+-- Of course we don't copy the temporary memory for stage region patching there, so Dest is not defined in the first entry.
+
+-- END
+
+ -- Copy the first 8 rows = 4 double rows at once
+ reload(0x4b00, 0x400, 0x1000, "data_stage_sonic.p8")
+
+ -- Starting from 0x1400 (see above):
+ -- Copy 16 partial lines covering 10 cells on X to make sure we get the 5 2x2-cell spin dash sprites
+ -- starting on row index 10
+ -- Each partial line covers 10 cell lines, so according to `Address start/size calculation` it takes
+ -- 10 * 4 bytes = 40 bytes = 0x28 bytes
+ -- We need to skip a full row to get the next partial line, so each iteration advances by +0x40 on src address
+ -- However, we don't want to waste space in general memory (it's the whole point of copying partial lines),
+ -- so we only advance by the length we copy on dest address, i.e. 0x28 bytes each iteration
+ -- Performance note: don't worry about repeating reloads from cartridge because:
+ -- 1. this only happens once on stage setup
+ -- 2. reloading from same cartridge seems to keep it in some cache, making further reloads faster
+ -- 3. ideally we'd copy the whole spritesheet memory into general memory, then operate on it to move partial lines
+ -- where we want; but that's more code, so unless you notice a particular lag on start, don't mind it
for i = 0, 15 do
- -- 6 walk cycle sprites
- memcpy(0x5300 + i * 0x30, 0x1008 + i * 0x40, 0x30)
-
- -- 4 run cycle sprites
- memcpy(0x5600 + i * 0x20, 0x1400 + i * 0x40, 0x20)
+ reload(0x5b00 + i * 0x28, 0x1400 + i * 0x40, 0x28, "data_stage_sonic.p8")
end
- -- the rotated sprite variants are in the runtime data, so reload them from that cartridge
- -- the dest addresses start just after the last memcpy above, at 0x5800
- -- from here, the same principle applies, but the dest addresses are offset by 0x300
- -- (the src addresses are the same because the runtime spritesheet's rotated sprite variants
- -- are located at the same location as their non-rotated equivalents in the built-in spritesheet)
-
- for i = 0, 15 do
- -- 6 walk cycle sprites (rotated)
- reload(0x5800 + i * 0x30, 0x1008 + i * 0x40, 0x30, runtime_data_path)
+ -- Total memory used by Sonic sprites: 0x1280
- -- 4 run cycle sprites (rotated)
- reload(0x5b00 + i * 0x20, 0x1400 + i * 0x40, 0x20, runtime_data_path)
- end
+ -- Memory range left: 0x5d80-0x5dff
+ -- We just have enough memory left for one 2x2 sprite!
+ -- However, we can still get four 1x1 sprites, useful for e.g. Ring animation.
- -- we check that we arrive at 0x5d00, and the general memory ends at 0x5dff,
- -- so we just have a little margin!
-- PICO-8 0.2.2 note: 0x5600-0x5dff is now used for custom font.
-- of course we can keep using it for general memory, but if we start using custom font,
- -- since the first bytes are used for default parameters, I'll have to stop using addresses
- -- before 0x5600
+ -- since the first bytes are used for default parameters, I would have to stop using addresses
+ -- before 0x5600, and I don't have this margin unless I accept to lose performance by reloading
+ -- Sonic sprites directly from data_stage_sonic.p8 cartridge...
+
+-- put in pico8 only to avoid polluting unit test counting reload calls
+--[[#pico8
+
+--#if debug_collision_mask
+ -- exceptionally overwrite the top of the spritesheet with tile collision mask sprites again,
+ -- so we can debug them with tile_collision_data:debug_render
+ -- the collision masks are located in the built-in data of ingame cartridge (also stage_intro),
+ -- so just reload data from the current cartridge (pass no filename)
+ -- spritesheet is located at 0x0
+ -- there are 3 lines of collision masks, so we need to copy 3 * 0x200 = 0x600 bytes
+ -- !! this will mess up runtime sprites like the emerald pick up FX !!
+ reload(0x0, 0x0, 0x600)
+--#endif
+
+--#pico8]]
end
-- never called, we directly load stage_clear cartridge
@@ -234,33 +278,34 @@ function stage_state:update()
self.player_char:update()
+--#if normal_mode
+--(attract mode never reaches goal)
self:check_reached_goal()
if self.goal_plate then
self.goal_plate:update()
end
+--#endif
self.camera:update()
self:check_reload_map_region()
end
function stage_state:render()
- visual_stage.render_background(self.camera.position)
+ -- background parallax layers use precise calculation and will sometimes move parallax
+ -- during sub-pixel motion, causing visual instability => so floor camera position
+ visual_stage.render_background(self.camera:get_floored_position())
self:render_stage_elements()
self:render_fx()
+--#ifn itest
self:render_hud()
+--(!itest)
+--#endif
end
-- setup
--- spawn the player character at the stage spawn location
-function stage_state:spawn_player_char()
- local spawn_position = self.curr_stage_data.spawn_location:to_center_position()
- self.player_char = player_char()
- self.player_char:spawn_at(spawn_position)
-end
-
function stage_state:spawn_emerald_at(global_loc)
-- no need to mset(i, j, 0) because emerald sprites don't have the midground/foreground flag
-- and won't be drawn at all
@@ -286,6 +331,17 @@ function stage_state:spawn_emerald_at(global_loc)
-- for given emerald locations, their colors are deterministic
add(self.emeralds, emerald(#self.spawned_emerald_locations, global_loc))
+ -- if emerald is surrounded by hiding leaves (we only check if there's one on the right)
+ -- we must draw an extra hiding leaves sprite on top of the emerald
+ -- but to make it cheaper, we mset it directly onto the tilemap
+ -- except tilemap is reloaded from file on region reload, so we cannot mset now,
+ -- and must store that info for later (to mset during every region reload)
+ local region_loc = self:global_to_region_location(global_loc)
+ local s = mget(region_loc.i, region_loc.j)
+ if mget(region_loc.i + 1, region_loc.j) == visual_ingame_data.hiding_leaves_id then
+ add(self.overlap_tiles, {global_loc, visual_ingame_data.hiding_leaves_id})
+ end
+
log("added emerald #"..#self.emeralds, "emerald")
end
@@ -314,12 +370,14 @@ stage_state.spawn_spring_right_at = generate_spawn_spring_dir_at_callback(direct
-- register spawn object callbacks by tile id to find them easily in scan_current_region_to_spawn_objects
stage_state.spawn_object_callbacks_by_tile_id = {
+ -- emerald sprite id is computed via sprite location so not replaced numerical constants like the rest
+ -- (but once we're settled, could be added to visual_ingame_numerical_data.lua for more compact code)
[visual.emerald_repr_sprite_id] = stage_state.spawn_emerald_at,
- [visual.palm_tree_leaves_core_id] = stage_state.spawn_palm_tree_leaves_at,
- [visual.goal_plate_base_id] = stage_state.spawn_goal_plate_at,
- [visual.spring_up_repr_tile_id] = stage_state.spawn_spring_up_at,
- [visual.spring_left_repr_tile_id] = stage_state.spawn_spring_left_at,
- [visual.spring_right_repr_tile_id] = stage_state.spawn_spring_right_at,
+ [visual_ingame_data.palm_tree_leaves_core_id] = stage_state.spawn_palm_tree_leaves_at,
+ [visual_ingame_data.goal_plate_base_id] = stage_state.spawn_goal_plate_at,
+ [visual_ingame_data.spring_up_repr_tile_id] = stage_state.spawn_spring_up_at,
+ [visual_ingame_data.spring_left_repr_tile_id] = stage_state.spawn_spring_left_at,
+ [visual_ingame_data.spring_right_repr_tile_id] = stage_state.spawn_spring_right_at,
}
-- proxy for table above, mostly to ease testing
@@ -435,10 +493,6 @@ function stage_state:reload_horizontal_half_of_map_region(dest_hdir, filename)
-- in order to copy the horizontal half of a tilemap (64x32 tiles), we need to copy
-- 32 lines of 64 tiles, one by one
- -- reloading from an external file 32 times is too slow, so we store the whole
- -- external tilemap first into general memory 0x4300, to process it later
- reload(0x4300, 0x2000, 0x1000, filename)
-
-- depending on whether we copy from their left half to our right half, or reversely,
-- we set the source and destination addresses differently
-- a line contains 128 = 0x80 tiles so:
@@ -453,10 +507,34 @@ function stage_state:reload_horizontal_half_of_map_region(dest_hdir, filename)
temp_source_addr0 = temp_source_addr0 + 0x40
end
- -- copy 32 lines of length 64 = 0x40
- for j = 0, 31 do
+ -- reloading from an external file 32 times is too slow, so we store parts of
+ -- the external tilemap in general memory, then copy half-lines from it one by one
+ -- with local memory copy operations only
+ -- we used to copy the full tilemap, but to avoid using too much of general memory
+ -- as temporary memory for one-time operations, we prefer splitting the load of 0x1000 bytes
+ -- in 2: first copy the upper part, second copy the lower part into the same location,
+ -- 0x800 bytes each (so it uses as much temp memory as reload_quarter_of_map_region)
+ -- with fast reload, reload is relatively fast and therefore 2 reloads at runtime are OK,
+ -- there is only a slight CPU increment but this will make sense to optimize (e.g. slicing the op
+ -- over 2 frames) when we reached 60 FPS
+ -- (but we couldn't do 32 reloads, as even with fast reload, a half-second freeze would be perceived)
+ -- for now we just put everything at the start of general memory 0x4300
+ reload(0x4300, 0x2000, 0x800, filename)
+
+ -- copy first 16 lines of length 64 = 0x40
+ for j = 0, 15 do
memcpy(dest_addr0 + j * 0x80, temp_source_addr0 + j * 0x80, 0x40)
end
+
+ -- same as before, but add 0x800 to source addresses to access the lower part of the map
+ reload(0x4300, 0x2800, 0x800, filename)
+
+ -- copy last 16 lines of length 64 = 0x40
+ -- note that we reuse the same temporary address, so make sure to restart index at 0
+ -- just for the temporary address offset
+ for j = 16, 31 do
+ memcpy(dest_addr0 + j * 0x80, temp_source_addr0 + (j-16) * 0x80, 0x40)
+ end
end
-- reload vertical half (upper or lower) of map in filename
@@ -596,6 +674,18 @@ function stage_state:check_reload_map_region()
if self.loaded_map_region_coords ~= new_map_region_coords then
-- current map region changed, must reload
self:reload_map_region(new_map_region_coords)
+
+ for overlap_tile_info in all(self.overlap_tiles) do
+ local global_loc, sprite_id = unpack(overlap_tile_info)
+ local region_loc = self:global_to_region_location(global_loc)
+ -- OPTIMIZE CHARS: region coords range check is to be cleaner,
+ -- but PICO-8 can handle an mset outside the 128x32 tiles, just do nothing
+ -- So you can remove this check if it really costs too many characters
+ if region_loc.i >= 0 and region_loc.i < map_region_tile_width and
+ region_loc.j >= 0 and region_loc.j < map_region_tile_height then
+ mset(region_loc.i, region_loc.j, sprite_id)
+ end
+ end
end
end
@@ -658,7 +748,7 @@ function stage_state:check_emerald_pick_area(position)
-- max xy distance check <=> inside square area (simplified version of AABB)
local delta = position - em:get_center()
local max_distance = max(abs(delta.x), abs(delta.y))
- if max_distance < stage_data.emerald_pick_radius then
+ if max_distance < stage_common_data.emerald_pick_radius then
return em
end
end
@@ -669,7 +759,7 @@ function stage_state:character_pick_emerald(em)
self.picked_emerald_numbers_set[em.number] = true
-- add emerald pick FX at emerald position and play it immediately
- local pfx = emerald_fx(em.number, em.location:to_center_position())
+ local pfx = emerald_fx(em.number, em:get_center())
add(self.emerald_pick_fxs, pfx)
-- remove emerald from sequence (use del to make sure
@@ -683,8 +773,8 @@ end
function stage_state:play_pick_emerald_jingle_async()
-- reduce bgm volume by half (notes have volume from 0 to 4, so decrement all sound volumes by 2)
-- to make the pick emerald jingle stand out
- -- the music sfx take maximum 50 entries (out of 64), so cover all tracks from 0 to 49
- volume.decrease_volume_for_track_range(0, 49, 2)
+ -- the music sfx take maximum 46 entries (out of 64), so cover all tracks from 8 to 53
+ volume.decrease_volume_for_track_range(8, 53, 2)
-- start jingle with an SFX since the usic still occupies the 3 channels, at lower volume
-- this has high priority so we don't use sound.play_low_priority_sfx unlike PC SFX,
@@ -701,16 +791,16 @@ function stage_state:play_pick_emerald_jingle_async()
-- won't hear the step-by-step volume transition too much during the end of the jingle),
-- so only wait 48 frames for now, then the remaining 16 frames after we incremented bgm
-- volume once
- yield_delay(48)
+ yield_delay_frames(48)
- -- unfortunately we cannot reincrement volume as some values were clamped to 0 durng decrease
+ -- unfortunately we cannot reincrement volume as some values were clamped to 0 during decrease
-- so we completely reload the bgm sfx, and redecrement them a little from the original volumes,
-- then reset without decrementing to retrieve the original volume
self:reload_bgm_tracks()
- volume.decrease_volume_for_track_range(0, 49, 1)
+ volume.decrease_volume_for_track_range(8, 53, 1)
-- wait the remaining 16 frames, the jingle should have ended just after that
- yield_delay(16)
+ yield_delay_frames(16)
self:reload_bgm_tracks()
end
@@ -771,6 +861,9 @@ function stage_state:check_loop_external_triggers(position, previous_active_laye
end
end
+--#if normal_mode
+--(attract mode never reaches goal nor remembers picked emeralds)
+
function stage_state:check_reached_goal()
if not self.has_player_char_reached_goal and self.goal_plate and
--[[#pico8
@@ -795,11 +888,11 @@ function stage_state:on_reached_goal_async()
-- play goal plate animation and wait for it to end
self:feedback_reached_goal()
- yield_delay(stage_data.goal_rotating_anim_duration)
+ yield_delay_frames(stage_common_data.goal_rotating_anim_duration)
self.goal_plate.anim_spr:play("sonic")
- self:stop_bgm(stage_data.bgm_fade_out_duration)
- self.app:yield_delay_s(stage_data.bgm_fade_out_duration)
+ self:stop_bgm(stage_common_data.bgm_fade_out_duration)
+ self.app:yield_delay_s(stage_common_data.bgm_fade_out_duration)
-- take advantage of the dead time to load the stage_clear cartridge,
-- which is a super-stripped version of ingame that doesn't know about
@@ -824,11 +917,11 @@ function stage_state:restore_picked_emerald_data()
-- Similar to stage_clear_state:restore_picked_emerald_data, but we also
-- remove emerald objects from the stage with a "silent pick"
-- (so this method must be called after object spawning)
- -- It is stored in 0x5d00, see store_picked_emerald_data below
- local picked_emerald_byte = peek(0x5d00)
+ -- It is stored in picked_emerald_address (0x5dff), see store_picked_emerald_data below
+ local picked_emerald_byte = peek(memory.picked_emerald_address)
-- consume emerald immediately to avoid sticky emeralds on hard ingame reload (ctrl+R)
- poke(0x5d00, 0)
+ poke(memory.picked_emerald_address, 0)
-- read bitset low-endian, from highest bit (emerald 8) to lowest bit (emerald 1)
-- the only reason we iterate from the end is because del() will remove elements
@@ -848,8 +941,13 @@ end
function stage_state:store_picked_emerald_data()
-- General memory is persistent during a single session, so a good fit to store data
-- across cartridges, although this behavior is undocumented.
- -- However, 0x4300-0x52ff is occupied by runtime regions, and 0x5300-0x5cff
- -- is occupied non-rotated/rotated walk/run sprite variants, so store 1 byte at 0x5d00.
+ -- We only need to store 1 byte = 8 bits, 1 bit per emerald, so we just poke one byte.
+ -- However, 0x4300-0x4aff is occupied by runtime regions, and 0x4b00-0x56ff
+ -- is occupied non-rotated/rotated walk/run sprite variants... but it was annoying to offset
+ -- picked emerald byte address every time I added a runtime sprite, so I decided to use the
+ -- *last* byte (picked_emerald_address = 0x5dff) so it will always be free (as long as runtime sprites don't occupy all the memory
+ -- left). When saving data in persistent memory (so player can continue emerald hunting later),
+ -- it won't even be a problem since we will use a very different address in the persistent block.
-- We could also use persistent memory, considering we may save emeralds collected by player
-- on next run (but for now we don't, so player always starts game from zero)
--
@@ -862,7 +960,7 @@ function stage_state:store_picked_emerald_data()
picked_emerald_bytes = picked_emerald_bytes + shl(1, i - 1)
end
end
- poke(0x5d00, picked_emerald_bytes)
+ poke(memory.picked_emerald_address, picked_emerald_bytes)
end
function stage_state:feedback_reached_goal()
@@ -872,6 +970,9 @@ function stage_state:feedback_reached_goal()
sfx(audio.sfx_ids.goal_reached)
end
+--(normal_mode)
+--#endif
+
-- fx
function stage_state:update_fx()
@@ -885,6 +986,9 @@ function stage_state:update_fx()
end
end
+ -- normally we should deactivate pfx and reuse it for pooling,
+ -- but deleting them was simpler (fewer characters) and single-time operation
+ --- so CPU cost is OK
for pfx in all(to_delete) do
del(self.emerald_pick_fxs, pfx)
end
@@ -914,18 +1018,14 @@ function stage_state:render_stage_elements()
--#if debug_trigger
self:debug_render_trigger()
--#endif
+--#if debug_collision_mask
+ self.player_char:debug_draw_tile_collision_masks()
+--#endif
--#if debug_character
self.player_char:debug_draw_rays()
--#endif
end
--- same kind of helper as base_stage_state:global_to_region_location and region_to_global_location,
--- but for mset
-function stage_state:mset_global_to_region(global_loc_i, global_loc_j, sprite_id)
- local region_loc = location(global_loc_i, global_loc_j) - self:get_region_topleft_location()
- mset(region_loc.i, region_loc.j, sprite_id)
-end
-
-- render the player character at its current position
function stage_state:render_player_char()
self:set_camera_with_origin()
@@ -979,6 +1079,8 @@ function stage_state:render_goal_plate()
end
end
+--#ifn itest
+
-- render the hud:
-- - emeralds obtained
-- - character debug info (#debug_character only)
@@ -1003,29 +1105,41 @@ function stage_state:render_hud()
--#endif
end
+--(!itest)
+--#endif
-- audio
-function stage_state:reload_bgm()
- -- reload music patterns from bgm cartridge memory
- -- we guarantee that the bgm will take maximum 40 patterns (out of 64)
- -- => 40 * 4 = 160 = 0xa0 bytes
- -- the bgm should start at pattern 0 on both source and
- -- current cartridge, so use copy memory from the start of music section
- reload(0x3100, 0x3100, 0xa0, "data_bgm"..self.curr_stage_id..cartridge_ext)
-
- -- we also need the music sfx referenced by the patterns
- self:reload_bgm_tracks()
-end
-
function stage_state:reload_bgm_tracks()
- -- reload sfx from bgm cartridge memory
- -- we guarantee that the music sfx will take maximum 50 entries (out of 64),
- -- potentially 0-7 for custom instruments and 8-49 for music tracks
- -- => 50 * 68 = 3400 = 0xd48 bytes
- -- the bgm sfx should start at index 0 on both source and
- -- current cartridge, so use copy memory from the start of sfx section
- reload(0x3200, 0x3200, 0xd48, "data_bgm"..self.curr_stage_id..cartridge_ext)
+ -- Note: bgm is now integrated in builtin data as we've reached the max cartridge limit of 16
+ -- We still kept this method, as besides loading music tracks on stage start (which is not needed
+ -- anymore), we were also using it to restore volume during the pick emerald jingle
+ -- (see play_pick_emerald_jingle_async), so it's still useful, but now needs a mere
+ -- We cannot use memcpy since memory has been modified in-place, so still reload.
+
+ -- !! PICO-8 PATCH vs COMPRESSED CHARS
+ -- Normally, we should call reload without filename argument so it gets memory directly from the
+ -- current original cartridge file.
+ -- But because this method is called during play_pick_emerald_jingle_async which happens
+ -- mid-game, and due to a quirk, our fast-reload patch only works consistently with reload()
+ -- taking filename argument, we still pass the ingame cartridge filename just to get fast reload
+ -- and not interrupt the game flow! (when not passing filename, game may freeze or not on reload
+ -- depending on the last cartridge reloaded)
+ -- (note that builtin_data_ingame.p8 doesn't exist in distribution, since it has been integrated
+ -- inside picosonic_ingame cartridge, so we really load the ingame cartridge)
+ -- Ideally, we'd improve the fast reload patch to cover reload from current cartridge file
+ -- (and possibly make load fast too), but for now this is the easiest approach,
+ -- at the cost of a few extra compressed characters
+
+ -- Reload sfx from builtin data ingame cartridge memory (must be current one)
+ -- we guarantee that the music sfx will take maximum 46 entries (out of 64),
+ -- skip 0-7 (custom instruments reserved to normal SFX) use 8-53 for music tracks
+ -- https://pico-8.fandom.com/wiki/Memory says 1 sfx = 68 bytes, so we must copy:
+ -- => 46 * 68 = 3400 = 0xc38 bytes
+ -- the bgm sfx should start at index 8 (after custom instruments) on both source and
+ -- current cartridge, so use copy memory from 8 * 68 = 544 = +0x220 after start of sfx section,
+ -- i.e. 0x3200 + 0x220 = 0x3420
+ reload(0x3420, 0x3420, 0xc38, "picosonic_ingame.p8")
end
function stage_state:play_bgm()
@@ -1033,7 +1147,7 @@ function stage_state:play_bgm()
-- Angel Island BGM currently uses only 3 channels so it's pretty safe
-- as there is always a channel left for SFX, but in case we add a 4th one
-- (or we try to play 2 SFX at once), protect the 3 channels by passing priority mask
- music(self.curr_stage_data.bgm_id, 0, shl(1, 0) + shl(1, 1) + shl(1, 2))
+ music(stage_common_data.bgm_id, 0, shl(1, 0) + shl(1, 1) + shl(1, 2))
end
function stage_state:stop_bgm(fade_duration)
diff --git a/src/ingame/stage_state_utest.lua b/src/ingame/stage_state_utest.lua
index e141c4fb..d6ebffee 100644
--- a/src/ingame/stage_state_utest.lua
+++ b/src/ingame/stage_state_utest.lua
@@ -14,6 +14,7 @@ local animated_sprite = require("engine/render/animated_sprite")
local picosonic_app = require("application/picosonic_app_ingame")
local camera_data = require("data/camera_data")
+local stage_common_data = require("data/stage_common_data")
local stage_data = require("data/stage_data")
local base_stage_state = require("ingame/base_stage_state")
local camera_class = require("ingame/camera")
@@ -24,6 +25,7 @@ local player_char = require("ingame/playercharacter")
local spring = require("ingame/spring")
local audio = require("resources/audio")
local visual = require("resources/visual_common")
+local visual_ingame_data = require("resources/visual_ingame_numerical_data")
local visual_stage = require("resources/visual_stage")
describe('stage_state', function ()
@@ -72,7 +74,7 @@ describe('stage_state', function ()
assert.are_same({
':stage',
1,
- stage_data.for_stage[1],
+ stage_data[1],
nil,
false,
{},
@@ -80,6 +82,7 @@ describe('stage_state', function ()
{},
{},
{},
+ {},
nil,
-- itest only
true,
@@ -94,6 +97,7 @@ describe('stage_state', function ()
state.emeralds,
state.picked_emerald_numbers_set,
state.emerald_pick_fxs,
+ state.overlap_tiles,
state.springs,
state.loaded_map_region_coords,
-- itest only
@@ -114,36 +118,33 @@ describe('stage_state', function ()
describe('on_enter', function ()
setup(function ()
+ stub(stage_state, "reload_runtime_data")
stub(stage_state, "spawn_player_char")
stub(stage_state, "play_bgm")
- stub(stage_state, "reload_bgm")
stub(stage_state, "spawn_objects_in_all_map_regions")
stub(stage_state, "restore_picked_emerald_data")
stub(camera_class, "setup_for_stage")
stub(stage_state, "check_reload_map_region")
- stub(stage_state, "reload_runtime_data")
end)
teardown(function ()
+ stage_state.reload_runtime_data:revert()
stage_state.spawn_player_char:revert()
stage_state.play_bgm:revert()
- stage_state.reload_bgm:revert()
stage_state.spawn_objects_in_all_map_regions:revert()
stage_state.restore_picked_emerald_data:revert()
camera_class.setup_for_stage:revert()
stage_state.check_reload_map_region:revert()
- stage_state.reload_runtime_data:revert()
end)
after_each(function ()
+ stage_state.reload_runtime_data:clear()
stage_state.spawn_player_char:clear()
stage_state.play_bgm:clear()
- stage_state.reload_bgm:clear()
stage_state.spawn_objects_in_all_map_regions:clear()
stage_state.restore_picked_emerald_data:clear()
camera_class.setup_for_stage:clear()
stage_state.check_reload_map_region:clear()
- stage_state.reload_runtime_data:clear()
end)
before_each(function ()
@@ -170,6 +171,11 @@ describe('stage_state', function ()
assert.spy(state.check_reload_map_region).was_called_with(match.ref(state))
end)
+ it('should call reload_runtime_data', function ()
+ assert.spy(state.reload_runtime_data).was_called(1)
+ assert.spy(state.reload_runtime_data).was_called_with(match.ref(state))
+ end)
+
it('should call spawn_player_char', function ()
assert.spy(stage_state.spawn_player_char).was_called(1)
assert.spy(stage_state.spawn_player_char).was_called_with(match.ref(state))
@@ -183,63 +189,44 @@ describe('stage_state', function ()
assert.is_false(state.has_player_char_reached_goal)
end)
- it('should call reload_bgm', function ()
- assert.spy(state.reload_bgm).was_called(1)
- assert.spy(state.reload_bgm).was_called_with(match.ref(state))
- end)
-
it('should call play_bgm', function ()
assert.spy(state.play_bgm).was_called(1)
assert.spy(state.play_bgm).was_called_with(match.ref(state))
end)
- it('should call reload_runtime_data', function ()
- assert.spy(state.reload_runtime_data).was_called(1)
- assert.spy(state.reload_runtime_data).was_called_with(match.ref(state))
- end)
-
end)
describe('reload_runtime_data', function ()
setup(function ()
stub(_G, "reload")
- stub(_G, "memcpy")
end)
teardown(function ()
reload:revert()
- memcpy:revert()
end)
after_each(function ()
reload:clear()
- memcpy:clear()
end)
- it('should reload stage runtime data into spritesheet top, and rotated sprite variants into general memory', function ()
+ it('should all stage runtime data copy Sonic sprite variants into general memory for quick runtime reload', function ()
state:reload_runtime_data()
- assert.spy(reload).was_called(33)
- assert.spy(reload).was_called_with(0x0, 0x0, 0x600, "data_stage1_runtime.p8")
- assert.spy(reload).was_called_with(0x5800, 0x1008, 0x30, "data_stage1_runtime.p8")
- assert.spy(reload).was_called_with(0x5830, 0x1048, 0x30, "data_stage1_runtime.p8")
- assert.spy(reload).was_called_with(0x5b00, 0x1400, 0x20, "data_stage1_runtime.p8")
- assert.spy(reload).was_called_with(0x5b20, 0x1440, 0x20, "data_stage1_runtime.p8")
- -- this has become too long since we copy line by line, so we stopped checking
- -- individual calls, except the first ones
- end)
+ -- note that debug_collision_mask adds an extra reload for collision masks,
+ -- but we stripped it from busted by surrounding it with #pico8
+ assert.spy(reload).was_called(18)
- it('should copy non-rotated sprite variants into general memory', function ()
- state:reload_runtime_data()
+ -- general runtime data
+ assert.spy(reload).was_called_with(0x0, 0x0, 0x2000, "data_stage1_ingame.p8")
+
+ -- sprites occupying full rows
+ assert.spy(reload).was_called_with(0x4b00, 0x400, 0x1000, "data_stage_sonic.p8")
- assert.spy(memcpy).was_called(32)
- -- this has become too long since we copy line by line, so we stopped checking
- -- individual calls, except the first ones
- assert.spy(memcpy).was_called_with(0x5300, 0x1008, 0x30)
- assert.spy(memcpy).was_called_with(0x5330, 0x1048, 0x30)
- assert.spy(memcpy).was_called_with(0x5600, 0x1400, 0x20)
- assert.spy(memcpy).was_called_with(0x5620, 0x1440, 0x20)
+ -- spin dash sprites
+ -- just test the first iterations...
+ assert.spy(reload).was_called_with(0x5b00, 0x1400, 0x28, "data_stage_sonic.p8")
+ assert.spy(reload).was_called_with(0x5b28, 0x1440, 0x28, "data_stage_sonic.p8")
end)
end)
@@ -247,21 +234,46 @@ describe('stage_state', function ()
describe('spawn_emerald_at', function ()
it('should store emerald global location', function ()
- state:spawn_emerald_at(location(1, 33))
+ state.loaded_map_region_coords = vector(1, 0.5)
+
+ state:spawn_emerald_at(location(128 + 5, 16 + 17))
assert.are_same({
- location(1, 33),
+ location(128 + 5, 16 + 17),
}, state.spawned_emerald_locations)
end)
it('should spawn and store emerald objects for each emerald tile', function ()
- state:spawn_emerald_at(location(1, 33))
+ state.loaded_map_region_coords = vector(1, 0.5)
+
+ state:spawn_emerald_at(location(128 + 5, 16 + 17))
assert.are_same({
- emerald(1, location(1, 33)),
+ emerald(1, location(128 + 5, 16 + 17)),
}, state.emeralds)
end)
+ it('(no hiding leaves on the right) should not add overlap tile hiding leaves on emerald location', function ()
+ state.loaded_map_region_coords = vector(1, 0.5)
+
+ state:spawn_emerald_at(location(128 + 5, 16 + 17))
+
+ assert.are_same({}, state.overlap_tiles)
+ end)
+
+ it('(hiding leaves on the right) should add overlap tile hiding leaves on emerald location', function ()
+ state.loaded_map_region_coords = vector(1, 0.5)
+ -- region coords: place hiding leaves just on right of emerald
+ mset(5 + 1, 17, visual_ingame_data.hiding_leaves_id)
+ state.overlap_tiles = {"dummy"}
+
+ -- '128 +' and '16 +' because region u = 1 and v = 0.5 (we could also write 33 = 32 + 1, but
+ -- 16 is the real reference = topleft.j for v = 0.5)
+ state:spawn_emerald_at(location(128 + 5, 16 + 17))
+
+ assert.are_same({"dummy", {location(128 + 5, 16 + 17), visual_ingame_data.hiding_leaves_id}}, state.overlap_tiles)
+ end)
+
end)
describe('spawn_palm_tree_leaves', function ()
@@ -668,6 +680,14 @@ describe('stage_state', function ()
end)
+ -- there are currently no utests for:
+ -- - reload_horizontal_half_of_map_region
+ -- - reload_vertical_half_of_map_region
+ -- - reload_quarter_of_map_region
+ -- we could add them, but experience showed that it was easy to mess up addresses
+ -- and that utests would not help a lot with that, so testing in real game is probably best for those
+ -- however utests can still be useful for syntax and trivial error checking
+
describe('reload_map_region', function ()
setup(function ()
@@ -747,12 +767,17 @@ describe('stage_state', function ()
end
return vector(0, 0)
end)
- stub(stage_state, "reload_map_region")
+ stub(stage_state, "reload_map_region", function (self, new_map_region_coords)
+ -- minimal stub just to change member that must be used by statements below
+ self.loaded_map_region_coords = new_map_region_coords
+ end)
+ stub(_G, "mset")
end)
teardown(function ()
stage_state.get_map_region_coords:revert()
stage_state.reload_map_region:revert()
+ mset:revert()
end)
before_each(function ()
@@ -760,12 +785,13 @@ describe('stage_state', function ()
state.player_char = {position = vector(0, 0)}
-- at least set some camera position used in get_map_region_coords stub
-- so we can verify we are passing it correctly
- state.camera.position = vector(200, 64)
+ state.camera:init_position(vector(200, 64))
end)
after_each(function ()
stage_state.get_map_region_coords:clear()
stage_state.reload_map_region:clear()
+ mset:clear()
end)
it('should call reload_map_region with (1, 0.5)', function ()
@@ -784,6 +810,28 @@ describe('stage_state', function ()
assert.spy(stage_state.reload_map_region).was_not_called()
end)
+ it('should mset overlap tiles at region coordinates inside current region range', function ()
+ -- note that check_reload_map_region will *move* to region (1, 0.5)
+ state.loaded_map_region_coords = vector(0, 0)
+ state.overlap_tiles = {{location(128 + 5, 16 + 17), 24}}
+
+ state:check_reload_map_region()
+
+ assert.spy(mset).was_called(1)
+ assert.spy(mset).was_called_with(5, 17, 24)
+ end)
+
+ it('should *not* mset overlap tiles at region coordinates outside current region range', function ()
+ -- note that check_reload_map_region will *move* to region (1, 0.5)
+ state.loaded_map_region_coords = vector(0, 0)
+ -- too much on the left! region coords would be (-5, 17) which are outside current map!
+ state.overlap_tiles = {{location(128 - 5, 16 + 17), 24}}
+
+ state:check_reload_map_region()
+
+ assert.spy(mset).was_not_called()
+ end)
+
end)
describe('get_spawn_object_callback', function ()
@@ -792,12 +840,12 @@ describe('stage_state', function ()
assert.are_equal(stage_state.spawn_emerald_at, state:get_spawn_object_callback(visual.emerald_repr_sprite_id))
end)
- it('should return stage_state.spawn_palm_tree_leaves_at for visual.palm_tree_leaves_core_id', function ()
- assert.are_equal(stage_state.spawn_palm_tree_leaves_at, state:get_spawn_object_callback(visual.palm_tree_leaves_core_id))
+ it('should return stage_state.spawn_palm_tree_leaves_at for visual_ingame_data.palm_tree_leaves_core_id', function ()
+ assert.are_equal(stage_state.spawn_palm_tree_leaves_at, state:get_spawn_object_callback(visual_ingame_data.palm_tree_leaves_core_id))
end)
- it('should return stage_state.spawn_goal_plate_at for visual.goal_plate_base_id', function ()
- assert.are_equal(stage_state.spawn_goal_plate_at, state:get_spawn_object_callback(visual.goal_plate_base_id))
+ it('should return stage_state.spawn_goal_plate_at for visual_ingame_data.goal_plate_base_id', function ()
+ assert.are_equal(stage_state.spawn_goal_plate_at, state:get_spawn_object_callback(visual_ingame_data.goal_plate_base_id))
end)
end)
@@ -847,11 +895,11 @@ describe('stage_state', function ()
before_each(function ()
-- 0b01001001 -> 73 (low-endian, so lowest bit is for emerald 1)
- poke(0x5d00, 73)
+ poke(0x5dff, 73)
end)
after_each(function ()
- poke(0x5d00, 0)
+ poke(0x5dff, 0)
end)
it('should read 1 byte in general memory representing picked emeralds bitset', function ()
@@ -875,7 +923,7 @@ describe('stage_state', function ()
it('should clear picked emerald transitional memory', function ()
state:restore_picked_emerald_data()
- assert.are_equal(0, peek(0x5d00))
+ assert.are_equal(0, peek(0x5dff))
end)
end)
@@ -918,39 +966,6 @@ describe('stage_state', function ()
flow:change_state(state)
end)
- describe('spawn_player_char', function ()
-
- setup(function ()
- spy.on(player_char, "spawn_at")
- end)
-
- teardown(function ()
- player_char.spawn_at:revert()
- end)
-
- before_each(function ()
- -- clear count before test as entering stage will auto-spawn character once
- player_char.spawn_at:clear()
- end)
-
- it('should spawn the player character at the stage spawn location', function ()
- state:spawn_player_char()
- local player_char = state.player_char
- assert.is_not_nil(player_char)
- local spawn_position = state.curr_stage_data.spawn_location:to_center_position()
-
- -- interface
- assert.are_equal(spawn_position, player_char.position)
- -- we haven't initialized any map in busted, so the character is falling in the air and spawn_at detected this
- assert.are_equal(motion_states.falling, player_char.motion_state)
-
- -- implementation
- assert.spy(player_char.spawn_at).was_called(1)
- assert.spy(player_char.spawn_at).was_called_with(match.ref(state.player_char), spawn_position)
- end)
-
- end)
-
describe('update_fx', function ()
setup(function ()
@@ -1158,9 +1173,13 @@ describe('stage_state', function ()
end)
it('should call render_background, render_stage_elements, render_fx, render_hud', function ()
+ -- to test camera position flooring
+ state.camera:init_position(vector(60.1, 380.9))
+
state:render()
+
assert.spy(visual_stage.render_background).was_called(1)
- assert.spy(visual_stage.render_background).was_called_with(state.camera.position)
+ assert.spy(visual_stage.render_background).was_called_with(vector(60, 380)) -- floored coords
assert.spy(stage_state.render_stage_elements).was_called(1)
assert.spy(stage_state.render_stage_elements).was_called_with(match.ref(state))
assert.spy(stage_state.render_fx).was_called(1)
@@ -1291,7 +1310,7 @@ describe('stage_state', function ()
state.emeralds = {
emerald(1, location(0, 0)),
emerald(2, location(1, 0)),
- emerald(3, location(0, 1)),
+ emerald(8, location(0, 1)),
}
end)
@@ -1299,11 +1318,6 @@ describe('stage_state', function ()
state.picked_emerald_numbers_set = {
[4] = true
}
- state.emeralds = {
- emerald(1, location(0, 0)),
- emerald(2, location(1, 0)),
- emerald(3, location(0, 1)),
- }
state:character_pick_emerald(state.emeralds[2])
assert.are_same({[2] = true, [4] = true}, state.picked_emerald_numbers_set)
end)
@@ -1313,25 +1327,20 @@ describe('stage_state', function ()
emerald_fx(1, vector(0, 0))
}
- state:character_pick_emerald(state.emeralds[2])
+ state:character_pick_emerald(state.emeralds[3])
- -- emerald 2 was at location (1, 0),
- -- so its center was at (12, 4)
+ -- emerald 2 was at location (0, 1),
+ -- so its center was at (4, 12)
assert.are_same({
emerald_fx(1, vector(0, 0)),
- emerald_fx(2, vector(12, 4))
+ emerald_fx(8, vector(4 + 5, 12)) -- pfx also follow emerald 8 center adjustment of X+5
},
state.emerald_pick_fxs)
end)
it('should remove an emerald from the sequence', function ()
- state.emeralds = {
- emerald(1, location(0, 0)),
- emerald(2, location(1, 0)),
- emerald(3, location(0, 1)),
- }
state:character_pick_emerald(state.emeralds[2])
- assert.are_same({emerald(1, location(0, 0)), emerald(3, location(0, 1))}, state.emeralds)
+ assert.are_same({emerald(1, location(0, 0)), emerald(8, location(0, 1))}, state.emeralds)
end)
it('should play character_pick_emerald sfx', function ()
@@ -1475,7 +1484,17 @@ describe('stage_state', function ()
local corunner
+ setup(function ()
+ stub(_G, "load")
+ end)
+
+ teardown(function ()
+ _G.load:revert()
+ end)
+
before_each(function ()
+ _G.load:clear()
+
state.goal_plate = goal_plate(location(100, 0))
state.spawned_emerald_locations = {1, 2, 3, 4, 5, 6, 7, 8}
@@ -1513,7 +1532,7 @@ describe('stage_state', function ()
}
-- 0b01001001 -> 73 (low-endian, so lowest bit is for emerald 1)
state:store_picked_emerald_data()
- assert.are_equal(73, peek(0x5d00))
+ assert.are_equal(73, peek(0x5dff))
end)
end)
@@ -1566,6 +1585,7 @@ describe('stage_state', function ()
stub(stage_state, "render_player_char")
stub(stage_state, "render_environment_foreground")
stub(stage_state, "debug_render_trigger")
+ stub(player_char, "debug_draw_tile_collision_masks")
stub(player_char, "debug_draw_rays")
end)
@@ -1577,6 +1597,7 @@ describe('stage_state', function ()
stage_state.render_player_char:revert()
stage_state.render_environment_foreground:revert()
stage_state.debug_render_trigger:revert()
+ player_char.debug_draw_tile_collision_masks:revert()
player_char.debug_draw_rays:revert()
end)
@@ -1588,6 +1609,7 @@ describe('stage_state', function ()
stage_state.render_player_char:clear()
stage_state.render_environment_foreground:clear()
stage_state.debug_render_trigger:clear()
+ player_char.debug_draw_tile_collision_masks:clear()
player_char.debug_draw_rays:clear()
end)
@@ -1609,6 +1631,10 @@ describe('stage_state', function ()
assert.spy(state.debug_render_trigger).was_called(1)
assert.spy(state.debug_render_trigger).was_called_with(match.ref(state))
-- #debug_trigger only end
+ -- #debug_collision_mask only
+ assert.spy(player_char.debug_draw_tile_collision_masks).was_called(1)
+ assert.spy(player_char.debug_draw_tile_collision_masks).was_called_with(match.ref(state.player_char))
+ -- #debug_collision_mask only end
-- #debug_character only
assert.spy(player_char.debug_draw_rays).was_called(1)
assert.spy(player_char.debug_draw_rays).was_called_with(match.ref(state.player_char))
@@ -1674,7 +1700,7 @@ describe('stage_state', function ()
emerald(2, location(2, 0)),
emerald(3, location(8, 0)), -- on right edge of screen, not visible
}
- state.camera.position = vector(0, 0)
+ state.camera:init_position(vector(0, 0))
state:render_emeralds()
@@ -1711,7 +1737,7 @@ describe('stage_state', function ()
spring(directions.left, location(2, 0)),
spring(directions.right, location(8, 0)), -- on right edge of screen, not visible
}
- state.camera.position = vector(0, 0)
+ state.camera:init_position(vector(0, 0))
state:render_springs()
@@ -1825,42 +1851,17 @@ describe('stage_state', function ()
pico8.current_music = nil
end)
- describe('state audio methods', function ()
-
- setup(function ()
- stub(stage_state, "reload_bgm_tracks")
- end)
-
- teardown(function ()
- stage_state.reload_bgm_tracks:revert()
- end)
-
- before_each(function ()
- stage_state.reload_bgm_tracks:clear()
- end)
-
- it('reload_bgm should reload music memory from bgm cartridge and call reload_bgm_tracks', function ()
- state:reload_bgm()
-
- assert.spy(reload).was_called(1)
- assert.spy(reload).was_called_with(0x3100, 0x3100, 0xa0, "data_bgm1.p8")
- assert.spy(stage_state.reload_bgm_tracks).was_called(1)
- assert.spy(stage_state.reload_bgm_tracks).was_called_with(match.ref(state))
- end)
-
- end)
-
it('reload_bgm_tracks should reload sfx from bgm cartridge', function ()
state:reload_bgm_tracks()
assert.spy(reload).was_called(1)
- assert.spy(reload).was_called_with(0x3200, 0x3200, 0xd48, "data_bgm1.p8")
+ assert.spy(reload).was_called_with(0x3420, 0x3420, 0xc38, "picosonic_ingame.p8")
end)
it('play_bgm should start level bgm', function ()
state:play_bgm()
- assert.are_same({music=state.curr_stage_data.bgm_id, fadems=0, channel_mask=(1 << 0) + (1 << 1) + (1 << 2)}, pico8.current_music)
+ assert.are_same({music=stage_common_data.bgm_id, fadems=0, channel_mask=(1 << 0) + (1 << 1) + (1 << 2)}, pico8.current_music)
end)
it('stop_bgm should stop level bgm if started, else do nothing', function ()
diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua
index d70c8c7b..b502b74a 100644
--- a/src/itest/itest_dsl.lua
+++ b/src/itest/itest_dsl.lua
@@ -37,7 +37,6 @@ local flow = require("engine/application/flow")
local input = require("engine/input/input")
local player_char = require("ingame/playercharacter")
-local pc_data = require("data/playercharacter_data")
local tile_repr = require("test_data/tile_representation")
--#if busted
@@ -80,10 +79,9 @@ function gameplay_value_data:init(name, parsable_type, eval)
end
--- type of variables that can be parsed
--- those names are *not* parsed at runtime for DSL, so we can minify them
--- to allow this, we do *not* use enum {} and define the table manually
--- it also allows us to access the types without the ["key"] syntax
+-- Type of variables that can be parsed
+-- We use enum so names are protected and we can support runtime parsing of
+-- parsable types strings.
parsable_types = enum {
"none",
"number",
@@ -97,6 +95,7 @@ parsable_types = enum {
}
-- Protected enums: map hardcoded strings to members, to support runtime parsing even when member names are minified on the original enums
+-- We could also use the enum {} helper as with parsable_types
horizontal_dirs_protected = {
["left"] = 1,
["right"] = 2
@@ -114,9 +113,12 @@ motion_modes_protected = {
}
motion_states_protected = {
- ["grounded"] = 1, -- character is idle or running on the ground
- ["falling"] = 2, -- character is falling in the air, but not spinning
- ["air_spin"] = 3 -- character is in the air after a jump
+ ["standing"] = 1, -- character is idle or running on the ground
+ ["falling"] = 2, -- character is falling in the air, but not spinning
+ ["air_spin"] = 3, -- character is in the air after a jump
+ ["rolling"] = 4, -- character is rolling on the ground
+ ["crouching"] = 5, -- character is crouching on the ground
+ ["spin_dashing"] = 6, -- character is charging spin dash
}
button_ids_protected = {
@@ -153,7 +155,9 @@ command_types = enum {
"stop", -- stop moving horizontally args: {}
"jump", -- start and hold jump args: {}
"stop_jump", -- stop any jump intention args: {}
- -- todo: crouch, spin_dash
+ "crouch", -- set sticky pc move intention y to 1
+ -- args: {}
+ "stop_crouch", -- clear move intention y args: {}
"press", -- press and hold button args: {button_id_str: button_ids key}
"release", -- release button args: {button_id_str: button_ids key}
"wait", -- wait some frames args: {frames: int}
@@ -174,6 +178,8 @@ command_arg_types = {
--[[stop]] parsable_types["none"],
--[[jump]] parsable_types["none"],
--[[stop_jump]] parsable_types["none"],
+ --[[crouch]] parsable_types["none"],
+ --[[stop_crouch]] parsable_types["none"],
--[[press]] parsable_types["button_id"],
--[[release]] parsable_types["button_id"],
--[[wait]] parsable_types["number"],
@@ -302,8 +308,9 @@ function itest_dsl.execute_warp(args)
-- and reload map region there
-- this trick is needed for itests that last only 1 frame (like spring bounce),
-- so the colliders in the warp region are correctly set and ground reactions work
- current_stage_state.camera.position.x = mid(screen_width / 2, current_stage_state.player_char.position.x, current_stage_state.curr_stage_data.tile_width * tile_size - screen_width / 2)
- current_stage_state.camera.position.y = mid(screen_height / 2, current_stage_state.player_char.position.y, current_stage_state.curr_stage_data.tile_height * tile_size - screen_height / 2)
+ local new_cam_pos_x = mid(screen_width / 2, current_stage_state.player_char.position.x, current_stage_state.curr_stage_data.tile_width * tile_size - screen_width / 2)
+ local new_cam_pos_y = mid(screen_height / 2, current_stage_state.player_char.position.y, current_stage_state.curr_stage_data.tile_height * tile_size - screen_height / 2)
+ current_stage_state.camera:init_position(vector(new_cam_pos_x, new_cam_pos_y))
current_stage_state:check_reload_map_region()
end
@@ -348,6 +355,16 @@ function itest_dsl.execute_stop_jump(args)
current_stage_state.player_char.hold_jump_intention = false
end
+function itest_dsl.execute_crouch(args)
+ local current_stage_state = get_current_state_as_stage()
+ current_stage_state.player_char.move_intention.y = 1
+end
+
+function itest_dsl.execute_stop_crouch(args)
+ local current_stage_state = get_current_state_as_stage()
+ current_stage_state.player_char.move_intention.y = 0
+end
+
function itest_dsl.execute_press(args)
-- simulate sticky press for player 0
input.simulated_buttons_down[0][args[1]] = true
@@ -372,6 +389,8 @@ executors = {
itest_dsl.execute_stop,
itest_dsl.execute_jump,
itest_dsl.execute_stop_jump,
+ itest_dsl.execute_crouch,
+ itest_dsl.execute_stop_crouch,
itest_dsl.execute_press,
itest_dsl.execute_release
}
diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua
index fc0f737e..2d2d3b4e 100644
--- a/src/itest/itest_dsl_utest.lua
+++ b/src/itest/itest_dsl_utest.lua
@@ -23,7 +23,7 @@ local stage_state = require("ingame/stage_state")
local picosonic_app_ingame = require("application/picosonic_app_ingame")
local player_char = require("ingame/playercharacter")
-local pc_data = require("data/playercharacter_data")
+local pc_data = require("data/playercharacter_numerical_data")
local tile_repr = require("test_data/tile_representation")
local tile_test_data = require("test_data/tile_test_data")
@@ -887,16 +887,18 @@ expect
assert.are_same(vector(-1, 0), state.player_char.move_intention)
-- we have not passed time so the character cannot have reached expected position
- -- note we are testing as busted, so we get the almost_eq messages
+ -- OLD note: we are testing as busted, so we get the almost_eq messages
-- since we added quadrants, even integer coordinates receive float transformation,
-- hence the .0 on passed position
+ -- (since we removed .0 and . in front of numbers as much as possible to reduce compressed chars count,
+ -- and so float operation was detected, in this particular case we won't have the float points anymore)
-- local expected_message = "\nFor gameplay value 'player character bottom position':\nExpected objects to be almost equal with eps: 0.015625.\n"..
-- "Passed in:\nvector(12.0, 45.0)\nExpected:\nvector(10, 45)\n"..
-- "\nFor gameplay value 'player character velocity':\nExpected objects to be almost equal with eps: 0.015625.\n"..
-- "Passed in:\nvector(0, 0)\nExpected:\nvector(2, -3.5)\n"
-- shorter version in assertions.lua
local expected_message = "\nFor gameplay value 'player character bottom position':\nExpected ~~ with eps: 0.015625.\n"..
- "Passed in:\nvector(12.0, 45.0)\nExpected:\nvector(10, 45)\n"..
+ "Passed in:\nvector(12, 45)\nExpected:\nvector(10, 45)\n"..
"\nFor gameplay value 'player character velocity':\nExpected ~~ with eps: 0.015625.\n"..
"Passed in:\nvector(0, 0)\nExpected:\nvector(2, -3.5)\n"
assert.are_same({false, expected_message}, {test.final_assertion()})
diff --git a/src/itests/ingame/itestplayercharacter.lua b/src/itests/ingame/itestplayercharacter.lua
index ade6b72a..d748eb43 100644
--- a/src/itests/ingame/itestplayercharacter.lua
+++ b/src/itests/ingame/itestplayercharacter.lua
@@ -1,9 +1,6 @@
-- gamestates: stage
local itest_dsl = require("itest/itest_dsl")
local itest_dsl_parser = itest_dsl.itest_dsl_parser
-local input = require("engine/input/input")
-local flow = require("engine/application/flow")
-local pc_data = require("data/playercharacter_data")
local itest
@@ -59,7 +56,7 @@ warp 4 8
wait 10
expect pc_bottom_pos 4 12
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_ground_spd 0
expect pc_velocity 0 0
]])
@@ -81,7 +78,7 @@ move right
wait 30
expect pc_bottom_pos 14.8984375 8
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_ground_spd 0.703125
expect pc_velocity 0.703125 0
]])
@@ -99,7 +96,7 @@ move left
wait 10
expect pc_bottom_pos 14.7109375 8
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_ground_spd -0.1875
expect pc_velocity -0.1875 0
]])
@@ -117,7 +114,7 @@ stop
wait 30
expect pc_bottom_pos 25.09375 8
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_ground_spd 0
expect pc_velocity 0 0
]])
@@ -162,14 +159,14 @@ move right
wait 14
expect pc_bottom_pos 6.36378532203461338585 15
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_slope -0.125
expect pc_ground_spd 0.3266448974609375
expect pc_velocity 0.23097282203461338585 -0.23097282203461338585
]])
-- expect pc_bottom_pos 0x0006.8509 15
--- expect pc_motion_state grounded
+-- expect pc_motion_state standing
-- expect pc_slope -0.125
-- expect pc_ground_spd 0.26318359375
-- expect pc_velocity 0x0000.2fa4 -0x0000.2fa5
@@ -210,7 +207,10 @@ Frame Ground Speed Velocity Bottom Pos
-- on this slope, divide ground speed in *sqrt(2) on x and y, hence velocity
-- y snaps to integer floor so it's just deduced from x as 15
+--]=]
+
+--#if busted
-- bugfix history:
-- + revealed that spawn_at was not resetting state vars, so added _setup method
itest_dsl_parser.register(
@@ -224,17 +224,89 @@ move right
wait 28
expect pc_bottom_pos 13 8
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_ground_spd 0
expect pc_velocity 0 0
]])
+--#endif
+
+-- trying to reproduce crash in vector.__add due to adding scalar 0 to vector
+-- fixed! forgot to get dir_vectors
+-- now still useful like utest above to test wall blocking movement, including subpixel cut, if any
+itest_dsl_parser.register(
+ '#mute jump into wall left', [[
+@stage #
+#.
+#.
+#.
+#.
+##
+
+warp 12 32
+move left
+jump
+wait 60
+
+expect pc_bottom_pos 11 32
+expect pc_motion_state standing
+expect pc_ground_spd 0
+expect pc_velocity 0 0
+]])
+
+--#if busted
+-- trying to bug when switching to big step for air motion:
+-- Sonic enters wall too much by 2px or so, but only when jumping into LEFT wall!
+itest_dsl_parser.register(
+ 'run into wall right', [[
+@stage #
+...#
+####
+
+warp 4 8
+move right
+wait 60
+
+expect pc_bottom_pos 21 8
+expect pc_motion_state standing
+expect pc_ground_spd 0
+expect pc_velocity 0 0
+]])
+--#endif
+
+--#if busted
+-- note: start position matters to test tunnel effect
+-- if we start at 4 we touch the wall (it may be a bug, that should happen at 5)
+-- at 3 we enter wall by 3px (!) so good to test deep wall escape
+itest_dsl_parser.register(
+ 'spin dash into wall right', [[
+@stage #
+.#
+##
+
+warp 3 8
+wait 1
+crouch
+wait 1
+jump
+wait 1
+stop_crouch
+wait 60
+
+expect pc_bottom_pos 5 8
+expect pc_motion_state standing
+expect pc_ground_spd 0
+expect pc_velocity 0 0
+]])
+--#endif
+
+--[=[
-- calculation notes
-- wait 28 frames and stop
-- character will be blocked when right wall sensor is at x = 16.5, so when center will be at x = 13
--- at frame 1: pos (4 + 0.0234375, 8), velocity (0.0234375, 0), grounded
+-- at frame 1: pos (4 + 0.0234375, 8), velocity (0.0234375, 0), standing
-- at frame 27: pos (12.8359375, 8), velocity (0.6328125, 0), about to meet wall
-- at frame 28: pos (13, 8), velocity (0, 0), hit wall
@@ -252,7 +324,7 @@ move right
wait 4
expect pc_bottom_pos 13 11
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_ground_spd 0
expect pc_velocity 0 0
]])
@@ -267,10 +339,10 @@ expect pc_velocity 0 0
-- character will be blocked when right wall sensor is at x = 16.5, so when center will be at x = 13
-- if move intention is applied after slope factor (or both are applied, then ground speed is clamped as we should):
--- at frame 1: pos (7, 14), velocity (3, 0), grounded
--- at frame 2: pos (7 + 0x0002.c589 = 9.771621704102, 13), velocity (3, 0), grounded
--- at frame 3: pos (7 + 2 * 0x0002.c589 = 12.543243408204, 11), velocity (3, 0), grounded
--- at frame 4: pos (13, 11), velocity (3, 0), grounded
+-- at frame 1: pos (7, 14), velocity (3, 0), standing
+-- at frame 2: pos (7 + 0x0002.c589 = 9.771621704102, 13), velocity (3, 0), standing
+-- at frame 3: pos (7 + 2 * 0x0002.c589 = 12.543243408204, 11), velocity (3, 0), standing
+-- at frame 4: pos (13, 11), velocity (3, 0), standing
-- in practice, slope after is applied after intention, causing a slight decel:
@@ -299,11 +371,13 @@ warp 4 0
wait 21
expect pc_bottom_pos 4 24
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_ground_spd 0
expect pc_velocity 0 0
]])
+--]=]
+
-- bugfix history:
-- ! identified bug in _update_platformer_motion where absence of elseif
@@ -311,9 +385,10 @@ expect pc_velocity 0 0
-- * revealed that new system always flooring pixel position x caused leaving cliff
-- frame later, adding a grounded frame with friction
+--#if busted
-- this test is now failing, I suspect air friction to mess up X...
itest_dsl_parser.register(
- 'platformer fall cliff', [[
+ '#mute platformer fall cliff', [[
@stage #
..
##
@@ -329,24 +404,22 @@ expect pc_motion_state falling
expect pc_ground_spd 0
expect pc_velocity 0.84375 2.625
]])
+--#endif
-- calculation notes:
--- at frame 1: pos (17.9453125, 8), velocity (0.796875, 0), grounded
--- at frame 34: pos (17.9453125, 8), velocity (0.796875, 0), grounded
--- at frame 35: pos (18.765625, 8), velocity (0.8203125, 0), grounded (do not apply ground sensor extent: -2.5 directly, floor to full px first)
+-- at frame 1: pos (17.9453125, 8), velocity (0.796875, 0), standing
+-- at frame 34: pos (17.9453125, 8), velocity (0.796875, 0), standing
+-- at frame 35: pos (18.765625, 8), velocity (0.8203125, 0), standing (do not apply ground sensor extent: -2.5 directly, floor to full px first)
-- at frame 36: pos (19.609375, 8), velocity (0.84375, 0), falling (flr_x=19) -> stop accel
-- wait 24 frames and stop
-- gravity during 24 frames: accel = 0.109375 * (24 * 25 / 2), velocity = 0.109375 * 24 = 2.625
-- at frame 60: pos (39.859375, 8 + 32.8125), velocity (0.84375, 2.625), falling
---]=]
-
---[=[
--- this test is now failing by 1 frame because jump is interrupted on frame 3 only...
--- not sure why, otherwise hoping works fine in real game
+--#if busted
+-- this test is working again
itest_dsl_parser.register(
- 'platformer hop flat', [[
+ 'platformer hop flat apogee', [[
@stage #
.
#
@@ -362,23 +435,89 @@ expect pc_ground_spd 0
expect pc_velocity 0 -0.03125
]])
+itest_dsl_parser.register(
+ 'platformer hop flat landing', [[
+@stage #
+.
+#
+
+warp 4 8
+jump
+stop_jump
+wait 39
+
+expect pc_bottom_pos 4 8
+expect pc_motion_state standing
+expect pc_ground_spd 0
+expect pc_velocity 0 0
+]])
+--#endif
+
-- calculation notes
-- wait for apogee (frame 20) and stop
--- at frame 1: bpos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump)
+-- at frame 1: bpos (4, 8), velocity (0, 0), standing (waits 1 frame before confirming hop/jump)
-- at frame 2: bpos (4, 8 - 2), velocity (0, -2), air_spin (hop confirmed, no gravity applied this frame)
-- at frame 3: bpos (4, 8 - 3.890625), velocity (0, -1.890625), air_spin
-- at frame 19: pos (4, 8 - 19.265625), velocity (0, -0.140625), air_spin -> before apogee
-- at frame 20: pos (4, 8 - 19.296875), velocity (0, -0.03125), air_spin -> reached apogee
-- at frame 21: pos (4, 8 - 19.21875), velocity (0, 0.078125), air_spin -> starts going down
-- at frame 38: pos (4, 8 - 1.15625), velocity (0, 1.9375), air_spin -> about to land
--- at frame 39: pos (4, 8), velocity (0, 0), grounded -> has landed
+-- at frame 39: pos (4, 8), velocity (0, 0), standing -> has landed
+-- (note: last air velocity was 2.046875 but immediately set to projection on ground on landing, which is 0 here)
-- => apogee at y = 8 - 19.296875 = -11.296875
---]=]
+-- This test was added to identify a bug when jumping exactly on landing frame, where old landing air velocity
+-- was preserved and summed with jump impulse, causing a very low jump when chaining 2 hops, or imperceptible jump
+-- (vy = -0.0625) when chaining 2 full jumps
+-- We would fail with:
+--[[
+For gameplay value 'player character bottom position':
+Expected ~~ with eps: 0.015625.
+Passed in:
+vector(4, 6.796875)
+Expected:
+vector(4, 6)
+
+For gameplay value 'player character velocity':
+Expected ~~ with eps: 0.015625.
+Passed in:
+vector(0.0, -1.203125)
+Expected:
+vector(0, -2)
+--]]
+-- but after setting velocity to match projected ground speed in enter_motion_state(standing),
+-- the test passed.
+--#if busted
+itest_dsl_parser.register(
+ 'platformer hop chain', [[
+@stage #
+.
+#
---[=[
+warp 4 8
+jump
+stop_jump
+wait 38
+jump
+stop_jump
+wait 2
+
+expect pc_bottom_pos 4 6
+expect pc_motion_state air_spin
+expect pc_ground_spd 0
+expect pc_velocity 0 -2
+]])
+--#endif
+
+-- timing notes:
+-- must set jump intention after 38 frames so it's ready for the check at the end of frame #39 (wait 1),
+-- so at the start of frame #40, check_jump confirms the 2nd hop
+-- we must then wait yet another frame to get the actual jump and velocity update until end of frame #40,
+-- hence wait 2
+
+--#if busted
itest_dsl_parser.register(
'platformer jump start flat', [[
@@ -418,7 +557,7 @@ expect pc_velocity 0 -2
-- calculation notes
-- interrupt variable jump at the end of frame 2
--- at frame 1: bpos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump)
+-- at frame 1: bpos (4, 8), velocity (0, 0), standing (waits 1 frame before confirming hop/jump)
-- at frame 2: bpos (4, 8 - 3.25), velocity (0, -3.25), air_spin (jump confirmed)
-- at frame 3: bpos (4, 8 - 6.390625), velocity (0, -3.140625), air_spin
-- at frame 4: bpos (4, 8 - 9.421875), velocity (0, -3.03125), air_spin
@@ -474,13 +613,13 @@ expect pc_velocity 0 -0.078125
-- calculation notes
-- wait for the apogee (frame 31) and stop
--- at frame 1: bpos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump)
--- at frame 2: bpos (4, 8 - 3.25), velocity (0, -3.25), air_spin (do not apply gravity on first frame of jump since we were grounded)
+-- at frame 1: bpos (4, 8), velocity (0, 0), standing (waits 1 frame before confirming hop/jump)
+-- at frame 2: bpos (4, 8 - 3.25), velocity (0, -3.25), air_spin (do not apply gravity on first frame of jump since we were standing)
-- at frame 30: bpos (4, 8 - 49.84375), velocity (0, -0.1875), air_spin -> before apogee
-- at frame 31: bpos (4, 8 - 49.921875), velocity (0, -0.078125), air_spin -> reached apogee (100px in 16-bit, matches SPG on Jumping)
-- at frame 32: bpos (4, 8 - 49.890625), velocity (0, 0.03125), air_spin -> starts going down
-- at frame 61: bpos (4, 8 - 1.40625), velocity (0, 3.203125), air_spin -> about to land
--- at frame 62: bpos (4, 8), velocity (0, 0), grounded -> has landed
+-- at frame 62: bpos (4, 8), velocity (0, 0), standing -> has landed
itest_dsl_parser.register(
@@ -498,10 +637,11 @@ press o
wait 20
expect pc_bottom_pos 4 8
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_ground_spd 0
expect pc_velocity 0 0
]])
+--#endif
-- if the player presses the jump button in mid-air, the character should not
-- jump again when he lands on the ground (later, it will trigger a special action)
@@ -516,19 +656,19 @@ expect pc_velocity 0 0
-- calculation notes:
-- wait for apogee (frame 20) and stop
--- at frame 1: bpos (4, 8), velocity (0, 0), grounded (waits 1 frame before confirming hop/jump)
+-- at frame 1: bpos (4, 8), velocity (0, 0), standing (waits 1 frame before confirming hop/jump)
-- at frame 2: bpos (4, 8 - 2), velocity (0, -2), air_spin (hop confirmed)
-- at frame 3: bpos (4, 8 - 3.890625), velocity (0, -1.890625), air_spin (hop confirmed)
-- at frame 19: bpos (4, 8 - 19.265625), velocity (0, -0.140625), air_spin -> before apogee
-- at frame 20: bpos (4, 8 - 19.296875), velocity (0, -0.03125), air_spin -> reached apogee
-- at frame 21: bpos (4, 8 - 19.21875), velocity (0, 0.078125), air_spin -> starts going down
-- at frame 38: bpos (4, 8 - 1.15625), velocity (0, 1.9375), air_spin -> about to land
--- at frame 39: bpos (4, 8), velocity (0, 0), grounded -> has landed
+-- at frame 39: bpos (4, 8), velocity (0, 0), standing -> has landed
-- and wait an extra frame to see if Sonic will jump due to holding jump input,
-- so stop at frame 40
-
+--[=[
itest_dsl_parser.register(
'platformer jump air accel', [[
@stage #
@@ -546,6 +686,9 @@ expect pc_motion_state air_spin
expect pc_ground_spd 0
expect pc_velocity 1.359375 -0.078125
]])
+--]=]
+
+--#if busted
itest_dsl_parser.register(
'platformer air right wall block', [[
@@ -570,7 +713,7 @@ expect pc_velocity 0 -1.125
-- calculation notes:
-- start jump input
--- at frame 1: bpos (4, 24), velocity (0, 0), grounded
+-- at frame 1: bpos (4, 24), velocity (0, 0), standing
-- wait 1 frame to confirm hop, and start moving right, then wait 9 frames
-- at frame 2: bpos (4 + .046875, 24 - 2), velocity (3/64, -2), air_spin (hop)
-- at frame 3: bpos (4.140625, 24 - 3.890625), velocity (6/64, -1 - 57/64), air_spin
@@ -600,7 +743,7 @@ expect pc_velocity 0 -1.125
-- to preserve the original expectations we duplicated the itest above,
-- except we added another block above the existing one to really block Sonic in the air
itest_dsl_parser.register(
- 'platformer air right wall block then just above', [[
+ '#mute platformer air right wall block then just above', [[
@stage #
.#
..
@@ -620,7 +763,7 @@ expect pc_velocity 0.09375 -1.125
]])
itest_dsl_parser.register(
- 'platformer air left wall block', [[
+ '#mute platformer air left wall block', [[
@stage #
#.
#.
@@ -641,7 +784,7 @@ expect pc_velocity 0 -1.125
]])
itest_dsl_parser.register(
- 'platformer air left wall block', [[
+ '#mute platformer air left wall block 2', [[
@stage #
#.
..
@@ -660,6 +803,10 @@ expect pc_ground_spd 0
expect pc_velocity -0.09375 -1.125
]])
+--#endif
+
+--[=[
+
itest_dsl_parser.register(
'platformer air ceiling block', [[
@stage #
@@ -687,7 +834,7 @@ expect pc_velocity 0 0
-- wait for the apogee (frame 31) and stop
-- frame bottom pos velocity state event
--- 1 (4, 32) (0, 0) grounded
+-- 1 (4, 32) (0, 0) standing
-- 2 (4, 32 - 3 - 16/64) (0, -3 - 16/64) air_spin confirm jump (no gravity on first frame)
-- 3 (4, 32 - 6 - 25/64) (0, -3 - 9/64) air_spin
-- 4 (4, 32 - 8) (0, 0) air_spin
@@ -792,7 +939,7 @@ expect pc_velocity -0x000.9aba -1.609375
-- calculation notes
-- wait ceiling hit (frame 31) and stop
--- at frame 1: bpos (19.9765625, 56), velocity (-0.0234375, 0), grounded (apply ground accel, waits 1 frame before confirming jump)
+-- at frame 1: bpos (19.9765625, 56), velocity (-0.0234375, 0), standing (apply ground accel, waits 1 frame before confirming jump)
-- at frame 2: bpos (19.90625, 56 - 3.25), velocity (-0.0703125, -3.25), air_spin (apply air accel, do not apply gravity on first frame of jump since we were grounded)
-- at frame 3: bpos (19.7890625, 56 - 6.390625), velocity (-0.1171875, -3.140625), air_spin (start applying gravity)
-- ...
@@ -844,11 +991,13 @@ warp 13 12
wait 9
expect pc_bottom_pos 15 8
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_velocity 0 0.984375
]])
--#endif
+--[=[
+
-- variant after discovering:
-- #189 BUG MOTION walking up loop stopping midway falls in wall right without safety offset
-- WIP
@@ -867,10 +1016,12 @@ move right
wait 60
expect pc_bottom_pos 15 8
-expect pc_motion_state grounded
+expect pc_motion_state standing
expect pc_velocity 0 0.984375
]])
+--]=]
+
--[=[
itest_dsl_parser.register(
@@ -908,11 +1059,8 @@ expect pc_motion_state falling
expect pc_velocity 0 -5
]])
---]=]
-
---#if busted
itest_dsl_parser.register(
- '#solo stand on one-way', [[
+ 'stand on one-way', [[
@stage #
.
o
@@ -921,7 +1069,42 @@ warp 4 7
wait 10
expect pc_bottom_pos 4 8
-expect pc_motion_state grounded
+expect pc_motion_state standing
+expect pc_velocity 0 0
+]])
+
+--]=]
+
+itest_dsl_parser.register(
+ '#solo land on one-way at high speed', [[
+@stage #
+.
+o
+
+warp 4 -20
+wait 25
+
+expect pc_bottom_pos 4 8
+expect pc_motion_state standing
+expect pc_velocity 0 0
+]])
+
+--#if busted
+itest_dsl_parser.register(
+ 'crouch and spin dash', [[
+@stage #
+.
+#
+
+warp 4 8
+wait 2
+crouch
+wait 1
+jump
+wait 60
+
+expect pc_bottom_pos 4 8
+expect pc_motion_state spin_dashing
expect pc_velocity 0 0
]])
--#endif
diff --git a/src/itests/stage_clear/iteststage_clear.lua b/src/itests/stage_clear/iteststage_clear.lua
index 1d526716..d568ca94 100644
--- a/src/itests/stage_clear/iteststage_clear.lua
+++ b/src/itests/stage_clear/iteststage_clear.lua
@@ -2,12 +2,12 @@
local itest_manager = require("engine/test/itest_manager")
local input = require("engine/input/input")
local flow = require("engine/application/flow")
-local visual = require("resources/visual_common") -- we should require ingameadd-on in main
+local visual_ingame_data = require("resources/visual_ingame_numerical_data") -- we should require ingameadd-on in main
-- testing credits is easier than entering stage
-- because stage in on another cartridge (ingame),
-- and itest builds are done separately (so we'd need to stub load)
-itest_manager:register_itest('player waits',
+itest_manager:register_itest('(stage clear) player waits',
{':stage_clear'}, function ()
-- enter stage clear state, simulate data from ingame cartridge and goal plate in tilemap
@@ -17,7 +17,7 @@ itest_manager:register_itest('player waits',
poke(0x4300, 73)
-- simulate goal plate in level (which starts empty) so render doesn't fail
- mset(64, 16, visual.goal_plate_base_id)
+ mset(64, 16, visual_ingame_data.goal_plate_base_id)
flow:change_gamestate_by_type(':stage_clear')
end)
diff --git a/src/itests/stage_intro/iteststage_intro.lua b/src/itests/stage_intro/iteststage_intro.lua
index 04bd026e..99aef85f 100644
--- a/src/itests/stage_intro/iteststage_intro.lua
+++ b/src/itests/stage_intro/iteststage_intro.lua
@@ -7,7 +7,7 @@ local visual = require("resources/visual_common") -- we should require ingamead
-- testing credits is easier than entering stage
-- because stage in on another cartridge (ingame),
-- and itest builds are done separately (so we'd need to stub load)
-itest_manager:register_itest('player waits',
+itest_manager:register_itest('(intro) player waits',
{':stage_intro'}, function ()
-- enter stage intro state
diff --git a/src/itests/titlemenu/itesttitlemenu.lua b/src/itests/titlemenu/itesttitlemenu.lua
index 3bb69b47..ba20194a 100644
--- a/src/itests/titlemenu/itesttitlemenu.lua
+++ b/src/itests/titlemenu/itesttitlemenu.lua
@@ -31,3 +31,26 @@ itest_manager:register_itest('player select credits, confirm',
end)
end)
+
+-- testing entering attract mode after a long time
+itest_manager:register_itest('attract mode starts after opening jingle',
+ {':titlemenu'}, function ()
+
+ -- enter title menu
+ setup_callback(function (app)
+ flow:change_gamestate_by_type(':titlemenu')
+ end)
+
+ -- opening jingle except fade out
+ wait(864 / 60)
+
+ -- fade out + small delay
+ wait(108 / 60)
+
+ -- we cannot really test load(), so just return true
+ -- a human can still verify that "load cartridge: picosonic_attract_mode" was printed
+ final_assert(function ()
+ return true
+ end)
+
+end)
diff --git a/src/main_attract_mode.lua b/src/main_attract_mode.lua
new file mode 100644
index 00000000..be079c41
--- /dev/null
+++ b/src/main_attract_mode.lua
@@ -0,0 +1,252 @@
+-- main entry file for the attract_mode cartridge
+-- game states: stage
+
+-- must require at main top, to be used in any required modules from here
+require("engine/pico8/api")
+require("engine/common")
+require("common_attract_mode")
+
+-- require visual add-on for ingame, so any require visual_common
+-- in this cartridge will get both common data and ingame data
+require("resources/visual_ingame_addon")
+
+-- we also require codetuner so any file can used tuned()
+-- if tuner symbol is defined, then we also initialize it in init
+local codetuner = require("engine/debug/codetuner")
+
+--#if log
+local logging = require("engine/debug/logging")
+--#endif
+
+--#if visual_logger
+local vlogger = require("engine/debug/visual_logger")
+--#endif
+
+--#if profiler
+local profiler = require("engine/debug/profiler")
+--#endif
+
+local flow = require("engine/application/flow")
+local coroutine_runner = require("engine/application/coroutine_runner")
+
+local picosonic_app_attract_mode = require("application/picosonic_app_attract_mode")
+
+local app = picosonic_app_attract_mode()
+
+local attract_mode_coroutine_runner
+
+-- define everything Sonic must do during the attract mode here
+-- it's not 100% correct to do it in main because this relies on flow having entered stage state
+-- but since it's the initial state, it should be entered on frame 1 anyway
+local function attract_mode_scenario_async()
+ -- wait for 1 frame so flow finishes loading the initial state: stage
+ yield()
+
+ assert(flow.curr_state, "flow has no current state yet")
+ assert(flow.curr_state.type == ':stage')
+
+ local pc = flow.curr_state.player_char
+ assert(pc)
+
+ pc.control_mode = control_modes.puppet
+
+ -- normally we should set pc.control_mode to control_modes.puppet
+ -- but since we're already stripping player_char:handle_input from #ifn attract_mode,
+ -- we don't need to do anything
+
+ -- now do the demonstration!
+ -- this is similar to our itest DSL, except the DSL being too expensive in a cartridge
+ -- (when combined with ingame code which includes background rendering unlike our itests)
+ -- so we just manually set character intention and wait between actions with yield_delay
+
+ yield_delay_frames(42)
+ pc.move_intention = vector(1, 0)
+ yield_delay_frames(318)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(4)
+ pc.hold_jump_intention = false
+ yield_delay_frames(10)
+ pc.move_intention = vector(0, 0)
+ yield_delay_frames(16)
+ pc.move_intention = vector(1, 0)
+ yield_delay_frames(26)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(14)
+ pc.hold_jump_intention = false
+ yield_delay_frames(90)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(6)
+ pc.move_intention = vector(0, 0)
+ pc.hold_jump_intention = false
+ yield_delay_frames(6)
+ pc.move_intention = vector(-1, 0)
+ yield_delay_frames(4)
+ pc.move_intention = vector(0, 0)
+ yield_delay_frames(36)
+ pc.move_intention = vector(1, 0)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(6)
+ pc.hold_jump_intention = false
+ yield_delay_frames(6)
+ pc.move_intention = vector(0, 0)
+ yield_delay_frames(8)
+ pc.move_intention = vector(1, 0)
+ yield_delay_frames(12)
+ pc.move_intention = vector(0, 0)
+ yield_delay_frames(16)
+ pc.move_intention = vector(1, 0)
+ yield_delay_frames(10)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(10)
+ pc.move_intention = vector(0, 0)
+ pc.hold_jump_intention = false
+ yield_delay_frames(21)
+ pc.move_intention = vector(-1, 0)
+ yield_delay_frames(14)
+ pc.move_intention = vector(0, 0)
+ yield_delay_frames(16)
+ pc.move_intention = vector(1, 0)
+ yield_delay_frames(4)
+ pc.move_intention = vector(0, 0)
+ yield_delay_frames(14)
+ pc.move_intention = vector(0, 1)
+ yield_delay_frames(14)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(5)
+ pc.hold_jump_intention = false
+ yield_delay_frames(3)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(5)
+ pc.hold_jump_intention = false
+ yield_delay_frames(2)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(5)
+ pc.hold_jump_intention = false
+ yield_delay_frames(2)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(5)
+ pc.hold_jump_intention = false
+ yield_delay_frames(2)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(5)
+ pc.hold_jump_intention = false
+ yield_delay_frames(3)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(4)
+ pc.hold_jump_intention = false
+ yield_delay_frames(4)
+ pc.jump_intention = true
+ pc.hold_jump_intention = true
+ yield_delay_frames(4)
+ pc.hold_jump_intention = false
+ yield_delay_frames(2)
+ pc.move_intention = vector(0, 0)
+ yield_delay_frames(21)
+ pc.move_intention = vector(1, 0)
+ yield_delay_frames(94)
+ pc.move_intention = vector(0, 1)
+ yield_delay_frames(4)
+ pc.move_intention = vector(0, 0)
+ yield_delay_frames(24)
+
+ -- if you want to record a demo yourself:
+ -- 0. comment/remove any previous input order you don't need in the list above
+ -- 1. uncomment the block of code below
+ -- 2. build and run 'attract_mode' cartridge with 'recorder' config
+ -- 3. it will automatically play the section above, then give control to human
+ -- 4. from here, play what you want to demonstrate in attract mode
+ -- 5. close the game and open (on Linux) .lexaloffle/pico-8/carts/picosonic/v[version]_recorder/picosonic_attract_mode_log.p8l
+ -- 6. remove first line with "START RECORDING HUMAN INPUT" then all line prefixes "[recorder] "
+ -- 7. copy-paste the resulting lines just above this comment block
+
+ -- pc.control_mode = control_modes.human
+ -- log(total_frames..": START RECORDING HUMAN INPUT", "recorder")
+ -- total_frames = 0 -- reset total frames as we want relative delays since last record
+
+ -- end demo, exit attract mode with fade out (if not already due to input)
+ app:exit_attract_mode()
+end
+
+function _init()
+--#if log
+ -- start logging before app in case we need to read logs about app start itself
+ logging.logger:register_stream(logging.console_log_stream)
+ logging.logger:register_stream(logging.file_log_stream)
+--#if visual_logger
+ logging.logger:register_stream(vlogger.vlog_stream)
+--#endif
+
+ logging.file_log_stream.file_prefix = "picosonic_attract_mode"
+
+ -- clear log file on new game session (or to preserve the previous log,
+ -- you could add a newline and some "[SESSION START]" tag instead)
+ logging.file_log_stream:clear()
+
+ logging.logger.active_categories = {
+ -- engine
+ -- ['default'] = true,
+ -- ['codetuner'] = true,
+ -- ['flow'] = true,
+ -- ['itest'] = true,
+ -- ['log'] = true,
+ -- ['ui'] = true,
+ -- ['reload'] = true,
+ -- ['trace'] = true,
+ -- ['trace2'] = true,
+ -- ['frame'] = true,
+
+ -- game
+ ['recorder'] = true,
+ -- ['...'] = true,
+ }
+--#endif
+
+--#if visual_logger
+ -- uncomment to enable visual logger
+ -- vlogger.window:show()
+--#endif
+
+--#if profiler
+ -- uncomment to enable profiler
+ profiler.window:show(colors.orange)
+--#endif
+
+--#if tuner
+ codetuner:show()
+ codetuner.active = true
+--#endif
+
+ app.initial_gamestate = ':stage'
+ app:start()
+
+ -- create coroutine runner and start attract mode scenario
+ attract_mode_coroutine_runner = coroutine_runner()
+ attract_mode_coroutine_runner:start_coroutine(attract_mode_scenario_async)
+end
+
+function _update60()
+--#if recorder
+ -- increment total frames for timed recording
+ total_frames = total_frames + 1
+--#endif
+
+ -- update coroutine so player character receives puppet mode instructions
+ attract_mode_coroutine_runner:update_coroutines()
+
+ app:update()
+end
+
+function _draw()
+ app:draw()
+end
diff --git a/src/main_sandbox.lua b/src/main_sandbox.lua
new file mode 100644
index 00000000..d88b7c60
--- /dev/null
+++ b/src/main_sandbox.lua
@@ -0,0 +1,124 @@
+-- main entry file for the sandbox cartridge
+-- game states: none, it's a custom cartridge just for testing
+
+-- must require at main top, to be used in any required modules from here
+require("engine/pico8/api")
+require("engine/common")
+require("common_sandbox")
+
+require("engine/render/sprite")
+
+require("resources/visual_ingame_addon")
+
+local input = require("engine/input/input")
+local visual = require("resources/visual_common")
+
+local codetuner = require("engine/debug/codetuner")
+
+--#if mouse
+local mouse = require("engine/ui/mouse")
+--#endif
+
+-- caveats
+
+-- (note: must require math for this to work)
+-- syntax error: malformed number near 27..d
+-- this error will block the output stream, getting picotest stuck!
+-- printh(27..vector(11, 45)) -- incorrect
+-- correct:
+-- printh("27"..vector(11, 45))
+-- or
+-- printh(tostr(27)..vector(11, 45))
+
+-- s = [[
+-- 1
+
+-- 2]]
+
+-- lines = strspl(s, "\n")
+
+-- COMMENT
+--[[BLOCk
+COMMENT]]
+-- for line in all(lines) do
+-- print("line: "..line)
+-- end
+
+--[[
+--]]
+
+-- test spr_r90
+-- require("engine/render/sprite_rotate90")
+local pc_sprite_data = require("data/playercharacter_sprite_data")
+
+local function draw_sprite()
+ spr_r90(12, 8, 64, 64, 2, 2, false, false, 0, 0, 0, 2)
+end
+
+local draw_mode = 0
+local angle = 0
+
+function _init()
+--#if tuner
+ codetuner:show()
+ codetuner.active = true
+--#endif
+
+--#if mouse
+ -- enable mouse devkit
+ printh("codetuner.active: "..nice_dump(codetuner.active))
+ input:toggle_mouse(true)
+ mouse:set_cursor_sprite_data(visual.sprite_data_t.cursor)
+--#endif
+end
+
+function _update60()
+ input:toggle_mouse(true)
+
+ if btnp(button_ids.o) then
+ draw_mode = (draw_mode + 1) % 5
+ end
+
+ if btnp(button_ids.left) then
+ angle = (angle + 0.25) % 1
+ end
+
+ if btnp(button_ids.right) then
+ angle = (angle - 0.25) % 1
+ end
+
+--#if tuner
+ codetuner:update_window()
+--#endif
+end
+
+function _draw()
+ cls()
+
+ if draw_mode == 0 then
+ visual.sprite_data_t.spring:render(vector(64, 64), false, false, angle)
+ elseif draw_mode == 1 then
+ -- spring up (stage1 spritesheet)
+ spr_r90(10, 4, 64, 64, 2, 1, false, false, tuned("px", 10), tuned("py", 2), angle, 2)
+ elseif draw_mode == 2 then
+ palt(2)
+ spr(4*16 + 10, 64 - tuned("px", 10), 64 - tuned("py", 2), 2, 1, false, false)
+ palt()
+ elseif draw_mode == 3 then
+ spr_r(10, 4, 64, 64, 2, 1, false, false, tuned("px", 10), tuned("py", 2), angle, 2)
+ elseif draw_mode == 4 then
+ spr_r90(10, 4, 64 + tuned("dx", 0), 64 + tuned("dy", 0), 2, 1, false, false, tuned("px", 10), tuned("py", 2), angle, 2)
+ end
+
+ api.print("draw_mode: "..draw_mode, 80, 80, colors.orange)
+ api.print("angle: "..angle, 80, 90, colors.orange)
+
+--#if tuner
+ codetuner:render_window()
+--#endif
+
+--#if mouse
+ -- always draw cursor on top of the rest (except for profiling)
+ mouse:render()
+--#endif
+end
diff --git a/src/menu/credits.lua b/src/menu/credits.lua
index d4194b85..e6c8aa00 100644
--- a/src/menu/credits.lua
+++ b/src/menu/credits.lua
@@ -20,7 +20,7 @@ local menu_item_params = {
end}
}
-local copyright_text = text_helper.wwrap("this is a fan game distributed for free and is not endorsed by sega games co. ltd, which owns the sonic the hedgehog trademark and copyrights.", 31)
+local copyright_text = text_helper.wwrap("this is a fan game distributed for free on itch.io and sfghq (sage 2021) and is not endorsed by sega games co. ltd, which owns the sonic the hedgehog trademark and copyrights.", 31)
function credits:init()
-- sequence of menu items to display, with their target states
@@ -46,7 +46,7 @@ end
function credits:render()
self:draw_credits_text()
- self.menu:draw(screen_width / 2, 120)
+ self.menu:draw(screen_width / 2, 121)
end
function credits:draw_credits_text()
@@ -55,7 +55,7 @@ function credits:draw_credits_text()
local text_color = colors.white
local margin_x = 2
local line_dy = character_height
- local paragraph_margin = 4
+ local paragraph_margin = 3
-- top
local y = 2
@@ -68,9 +68,7 @@ function credits:draw_credits_text()
y = y + line_dy + paragraph_margin
api.print("leyn", margin_x, y, text_color)
- text_helper.print_aligned("programming", 127 - margin_x, y, alignments.right, text_color)
- y = y + line_dy
- text_helper.print_aligned("sprites and sfx", 127 - margin_x, y, alignments.right, text_color)
+ text_helper.print_aligned("code, sprites, sfx", 127 - margin_x, y, alignments.right, text_color)
y = y + line_dy
text_helper.print_aligned("bgm adjustments", 127 - margin_x, y, alignments.right, text_color)
y = y + line_dy + paragraph_margin
@@ -82,6 +80,9 @@ function credits:draw_credits_text()
y = y + 3 * line_dy + paragraph_margin
api.print(copyright_text, margin_x, y, text_color)
+ y = y + 6 * line_dy + paragraph_margin
+
+ api.print("komehara.itch.io/pico-sonic", margin_x, y, text_color)
end
-- export
diff --git a/src/menu/menu_utest.lua b/src/menu/menu_utest.lua
index 15b3f5da..66c51a51 100644
--- a/src/menu/menu_utest.lua
+++ b/src/menu/menu_utest.lua
@@ -170,9 +170,8 @@ describe('menu', function ()
end)
after_each(function ()
- input.players_btn_states[0][button_ids.up] = btn_states.released
- input.players_btn_states[0][button_ids.down] = btn_states.released
- input.players_btn_states[0][button_ids.x] = btn_states.released
+ -- reset all inputs
+ input:init()
menu.select_previous:clear()
menu.select_next:clear()
diff --git a/src/menu/titlemenu.lua b/src/menu/titlemenu.lua
index 4f979b4f..3eb85d74 100644
--- a/src/menu/titlemenu.lua
+++ b/src/menu/titlemenu.lua
@@ -26,8 +26,13 @@ local menu_item_params = {
end},
}
--- attributes:
--- menu menu title menu showing items (only created when it must be shown)
+-- parameters:
+-- items {menu_item} sequence of menu items that the menu should display
+
+-- state:
+-- frames_before_showing_menu int number of frames before showing menu. Ignored if 0.
+-- should_start_attract_mode bool should we enter attract mode now?
+-- menu menu title menu showing items (only created when it must be shown)
function titlemenu:init()
-- sequence of menu items to display, with their target states
@@ -35,21 +40,28 @@ function titlemenu:init()
-- outer scope definition, so we don't need to declare local menu_item
-- at source top for unity build
self.items = transform(menu_item_params, unpacking(menu_item))
-end
+ -- self.menu = nil -- commented out to spare characters
-function titlemenu:on_enter()
- self.app:start_coroutine(self.opening_sequence_async, self)
+ -- defined in on_enter anyway, but we still define it to allow utests to handle that
+ -- without simulating on_enter (and titlemenu cartridge has enough space)
+ self.frames_before_showing_menu = 0
+ self.should_start_attract_mode = false
end
-function titlemenu:opening_sequence_async()
- -- start title BGM
- music(audio.music_ids.title)
+function titlemenu:on_enter()
+ self.app:start_coroutine(self.play_opening_music_async, self)
-- show menu after short intro of 2 columns
+ -- we assume play_opening_music_async was started at the same time
-- title bgm is at SPD 12 so that makes
-- 12 SPD * 4 frames/SPD/column * 2 columns = 96 frames
- yield_delay(96)
- self:show_menu()
+ self.frames_before_showing_menu = 96
+ self.should_start_attract_mode = false
+end
+
+function titlemenu:play_opening_music_async()
+ -- start title BGM
+ music(audio.music_ids.title)
-- fade out current bgm during the last half-measure (we have a decreasing volume
-- in the music itself but there is still a gap between volume 1 and 0 in PICO-8
@@ -60,9 +72,14 @@ function titlemenu:opening_sequence_async()
-- 12 SPD * 4 frames/SPD/column * (4 patterns * 4 columns + 2 columns) = 864 frames
-- and lasts:
-- 12 SPD * 4 frames/SPD/column * 1 column = 48 frames = 48 * 1000 / 60 = 800 ms
- -- we've already waited 96 frames so only wait 864 - 96 = 768 frames now
- yield_delay(768)
+ yield_delay_frames(864)
music(-1, 800)
+
+ -- wait for music fade out to finish (48 frames), then wait a little more before
+ -- starting attract mode (1s = 60 frames), similarly to Sonic 3
+ yield_delay_frames(108)
+
+ self.should_start_attract_mode = true
end
function titlemenu:show_menu()
@@ -70,11 +87,15 @@ function titlemenu:show_menu()
self.menu:show_items(self.items)
end
+-- this is called when entering credits
function titlemenu:on_exit()
+ -- a priori not needed, because we can only enter credits once the opening is over
+ self.is_playing_opening = false
+
-- clear menu completely (will call GC, but fine)
self.menu = nil
- -- stop all coroutines, this is important to prevent opening_sequence_async from continuing in the background
+ -- stop all coroutines, this is important to prevent play_opening_music_async from continuing in the background
-- while reading credits, and fading out music earlier than expected after coming back to title
self.app:stop_all_coroutines()
end
@@ -82,9 +103,32 @@ end
function titlemenu:update()
if self.menu then
self.menu:update()
+
+ -- attract mode countdown
+ if self.should_start_attract_mode then
+ self:start_attract_mode()
+ end
+ else
+ -- menu not shown yet, check for immediate show input vs normal countdown
+
+ if input:is_just_pressed(button_ids.o) then
+ -- show menu immediately
+ self.frames_before_showing_menu = 0
+ else
+ -- decrement countdown
+ self.frames_before_showing_menu = self.frames_before_showing_menu - 1
+ end
+
+ if self.frames_before_showing_menu <= 0 then
+ self:show_menu()
+ end
end
end
+function titlemenu:start_attract_mode()
+ load('picosonic_attract_mode')
+end
+
function titlemenu:render()
self:draw_background()
self:draw_title()
@@ -105,8 +149,7 @@ function titlemenu:draw_background()
-- just in case, max to step_count)
local step = min(flr(ratio * step_count) + 1, step_count)
local new_colors = visual.water_shimmer_color_cycle[step]
- pal(colors.red, new_colors[1])
- pal(colors.yellow, new_colors[2])
+ swap_colors({colors.red, colors.yellow}, new_colors)
visual.sprite_data_t.angel_island_bg:render(vector(0, 88))
pal()
end
@@ -118,11 +161,9 @@ function titlemenu:draw_title()
end
function titlemenu:draw_version()
- -- #version
- -- PICO-8 cannot access data/version.txt and we don't want to preprocess substitute some $version
- -- tag in build script just for this, so we exceptionally hardcode version number
- -- coords correspond to top-right corner with a small margin
- text_helper.print_aligned("V5.4", 126, 2, alignments.right, colors.white, colors.black)
+ -- preprocess can now replace $variables so build_single_cartridge.sh
+ -- will just pass the version string to the builder so it replaces $version here
+ text_helper.print_aligned("V$version", 126, 2, alignments.right, colors.white, colors.black)
end
return titlemenu
diff --git a/src/menu/titlemenu_utest.lua b/src/menu/titlemenu_utest.lua
index b9766384..c4922a53 100644
--- a/src/menu/titlemenu_utest.lua
+++ b/src/menu/titlemenu_utest.lua
@@ -3,6 +3,7 @@ require("resources/visual_titlemenu_addon")
local titlemenu = require("menu/titlemenu")
+local input = require("engine/input/input")
local text_helper = require("engine/ui/text_helper")
local picosonic_app = require("application/picosonic_app_titlemenu")
@@ -11,6 +12,19 @@ local visual = require("resources/visual_common")
describe('titlemenu', function ()
+ describe('init', function ()
+
+ it('should initialize members', function ()
+ local tm = titlemenu()
+
+ -- a bit complicated to test the generated items, so just test length for items
+ assert.are_equal(2, #tm.items)
+ assert.are_equal(0, tm.frames_before_showing_menu)
+ assert.is_false(tm.should_start_attract_mode)
+ end)
+
+ end)
+
describe('(with instance)', function ()
local tm
@@ -48,11 +62,18 @@ describe('titlemenu', function ()
picosonic_app.start_coroutine:clear()
end)
- it('should call start_coroutine_method on opening_sequence_async', function ()
+ it('should call start_coroutine_method on play_opening_music_async', function ()
tm:on_enter()
assert.spy(picosonic_app.start_coroutine).was_called(1)
- assert.spy(picosonic_app.start_coroutine).was_called_with(match.ref(tm.app), titlemenu.opening_sequence_async, match.ref(tm))
+ assert.spy(picosonic_app.start_coroutine).was_called_with(match.ref(tm.app), titlemenu.play_opening_music_async, match.ref(tm))
+ end)
+
+ it('should initialize frames_before_showing_menu for countdown and reset frames_before_showing_attract_mode', function ()
+ tm:on_enter()
+
+ assert.are_equal(96, tm.frames_before_showing_menu)
+ assert.is_false(tm.should_start_attract_mode)
end)
end)
@@ -113,13 +134,70 @@ describe('titlemenu', function ()
setup(function ()
stub(menu, "update")
+ stub(titlemenu, "show_menu")
+ stub(titlemenu, "start_attract_mode")
end)
teardown(function ()
menu.update:revert()
+ titlemenu.show_menu:revert()
+ titlemenu.start_attract_mode:revert()
end)
- it('should not try to update menu if nil', function ()
+ after_each(function ()
+ menu.update:clear()
+ titlemenu.show_menu:clear()
+ titlemenu.start_attract_mode:clear()
+ end)
+
+ it('(no menu) should not try to update menu', function ()
+ tm:update()
+
+ assert.spy(menu.update).was_not_called()
+ end)
+
+ describe('(simulating input)', function ()
+
+ after_each(function ()
+ -- reset all inputs
+ input:init()
+ end)
+
+ it('(button O pressed) should set frames_before_showing_menu to 0 and immediately show menu', function ()
+ tm.frames_before_showing_menu = 99
+ input.players_btn_states[0][button_ids.o] = btn_states.just_pressed
+
+ tm:update()
+
+ assert.are_equal(0, tm.frames_before_showing_menu)
+
+ assert.spy(titlemenu.show_menu).was_called(1)
+ assert.spy(titlemenu.show_menu).was_called_with(match.ref(tm))
+ end)
+
+ end)
+
+ it('(button O not pressed, frames_before_showing_menu > 1) should decrement frames_before_showing_menu, but not show menu yet', function ()
+ tm.frames_before_showing_menu = 2
+
+ tm:update()
+
+ assert.are_equal(1, tm.frames_before_showing_menu)
+ assert.spy(titlemenu.show_menu).was_not_called(1)
+ end)
+
+ it('(button O not pressed, frames_before_showing_menu <= 1) should decrement frames_before_showing_menu to <=0 and show menu', function ()
+ tm.frames_before_showing_menu = 1
+
+ tm:update()
+
+ assert.are_equal(0, tm.frames_before_showing_menu)
+
+ assert.spy(titlemenu.show_menu).was_called(1)
+ assert.spy(titlemenu.show_menu).was_called_with(match.ref(tm))
+ end)
+
+ it('(no menu) should not try to update menu', function ()
tm:update()
assert.spy(menu.update).was_not_called()
@@ -128,7 +206,8 @@ describe('titlemenu', function ()
describe('(with menu shown)', function ()
before_each(function ()
- tm:show_menu()
+ -- dummy menu
+ tm.menu = menu(tm.app--[[, 2]], alignments.left, 3, colors.white--[[skip prev_page_arrow_offset]], visual.sprite_data_t.menu_cursor_shoe, 7)
end)
it('should update menu', function ()
@@ -138,6 +217,46 @@ describe('titlemenu', function ()
assert.spy(menu.update).was_called_with(match.ref(tm.menu))
end)
+ it('(should_start_attract_mode: false) should not start attract mode', function ()
+ tm.should_start_attract_mode = false
+
+ tm:update()
+
+ assert.spy(titlemenu.start_attract_mode).was_not_called()
+ end)
+
+ it('(should_start_attract_mode: true) should start attract mode', function ()
+ tm.should_start_attract_mode = true
+
+ tm:update()
+
+ assert.spy(titlemenu.start_attract_mode).was_called(1)
+ assert.spy(titlemenu.start_attract_mode).was_called_with(match.ref(tm))
+ end)
+
+ end)
+
+ end)
+
+ describe('start_attract_mode', function ()
+
+ setup(function ()
+ stub(_G, "load")
+ end)
+
+ teardown(function ()
+ load:revert()
+ end)
+
+ after_each(function ()
+ load:clear()
+ end)
+
+ it('should load attract mode cartridge', function ()
+ tm:start_attract_mode()
+
+ assert.spy(load).was_called(1)
+ assert.spy(load).was_called_with('picosonic_attract_mode')
end)
end)
diff --git a/src/platformer/motion.lua b/src/platformer/motion.lua
index 14ed0534..1d0f3ee3 100644
--- a/src/platformer/motion.lua
+++ b/src/platformer/motion.lua
@@ -41,6 +41,7 @@ function ground_motion_result:init(tile_location, position, slope_angle, is_bloc
-- it's possible to have no ground tile location and not is_falling
-- when _check_escape_from_ground found character too deep inside ground,
-- so it can have the grounded animation with slope 0 but no specific tile to walk on
+ assert((tile_location == nil) == is_falling, "tile location is "..stringify(tile_location).." but is_falling is "..tostr(is_falling))
assert(type(slope_angle) == "number" or slope_angle == nil)
self.tile_location = tile_location
self.position = position
@@ -62,16 +63,17 @@ local air_motion_result = new_struct()
motion.air_motion_result = air_motion_result
-- tile_location location|nil location of ground tile at the end of motion (nil if no ground i.e. not is_landing)
--- position vector position at the end of motion
-- is_blocked_by_wall bool was the character blocked by a left/right wall during motion?
-- is_blocked_by_ceiling bool was the character blocked by a ceiling during motion?
-- is_landing bool has the character landed at the end of this motion?
-- slope_angle float|nil slope angle of the final position (nil unless is_landing is true)
-function air_motion_result:init(tile_location, position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing, slope_angle)
+
+-- note: we removed member position since air_motion_result is now only returned by check_air_collisions
+-- which is done in-place on character current position
+function air_motion_result:init(tile_location, is_blocked_by_wall, is_blocked_by_ceiling, is_landing, slope_angle)
assert((tile_location ~= nil) == is_landing, "tile location is "..stringify(tile_location).." but is_landing is "..tostr(is_landing))
assert(type(is_blocked_by_wall) == "boolean")
self.tile_location = tile_location
- self.position = position
self.is_blocked_by_wall = is_blocked_by_wall
self.is_blocked_by_ceiling = is_blocked_by_ceiling
self.is_landing = is_landing
@@ -92,7 +94,7 @@ end
--#if tostring
function air_motion_result:_tostring()
return "air_motion_result("..joinstr(", ",
- self.tile_location, self.position, self.is_blocked_by_wall, self.is_blocked_by_ceiling, self.is_landing, self.slope_angle)..")"
+ self.tile_location, self.is_blocked_by_wall, self.is_blocked_by_ceiling, self.is_landing, self.slope_angle)..")"
end
--#endif
diff --git a/src/platformer/motion_utest.lua b/src/platformer/motion_utest.lua
index 7c0aa243..16c312cc 100644
--- a/src/platformer/motion_utest.lua
+++ b/src/platformer/motion_utest.lua
@@ -60,8 +60,8 @@ describe('motion', function ()
describe('init', function ()
it('should create a air_motion_result with position, is_blocked_by_wall, is_blocked_by_ceiling, is_landing, slope_angle', function ()
- local gmr = air_motion_result(location(2, 1), vector(2, 3), true, false, true, -0.25)
- assert.are_same({location(2, 1), vector(2, 3), true, false, true, -0.25}, {gmr.tile_location, gmr.position, gmr.is_blocked_by_wall, gmr.is_blocked_by_ceiling, gmr.is_landing, gmr.slope_angle})
+ local amr = air_motion_result(location(2, 1), true, false, true, -0.25)
+ assert.are_same({location(2, 1), true, false, true, -0.25}, {amr.tile_location, amr.is_blocked_by_wall, amr.is_blocked_by_ceiling, amr.is_landing, amr.slope_angle})
end)
end)
@@ -69,52 +69,52 @@ describe('motion', function ()
describe('is_blocked_along', function ()
it('return false if direction is left and is_blocked_by_wall is false', function ()
- local gmr = air_motion_result(nil, vector(2, 3), false, false, false)
- assert.is_false(gmr:is_blocked_along(directions.left))
+ local amr = air_motion_result(nil, false, false, false)
+ assert.is_false(amr:is_blocked_along(directions.left))
end)
it('return true if direction is left and is_blocked_by_wall is true', function ()
- local gmr = air_motion_result(nil, vector(2, 3), true, false, false)
- assert.is_true(gmr:is_blocked_along(directions.left))
+ local amr = air_motion_result(nil, true, false, false)
+ assert.is_true(amr:is_blocked_along(directions.left))
end)
it('return false if direction is right and is_blocked_by_wall is false', function ()
- local gmr = air_motion_result(nil, vector(2, 3), false, false, false)
- assert.is_false(gmr:is_blocked_along(directions.right))
+ local amr = air_motion_result(nil, false, false, false)
+ assert.is_false(amr:is_blocked_along(directions.right))
end)
it('return true if direction is right and is_blocked_by_wall is true', function ()
- local gmr = air_motion_result(nil, vector(2, 3), true, false, false)
- assert.is_true(gmr:is_blocked_along(directions.right))
+ local amr = air_motion_result(nil, true, false, false)
+ assert.is_true(amr:is_blocked_along(directions.right))
end)
it('return false if direction is up and is_blocked_by_ceiling is false', function ()
- local gmr = air_motion_result(nil, vector(2, 3), false, false, false)
- assert.is_false(gmr:is_blocked_along(directions.up))
+ local amr = air_motion_result(nil, false, false, false)
+ assert.is_false(amr:is_blocked_along(directions.up))
end)
it('return true if direction is up and is_blocked_by_ceiling is true', function ()
- local gmr = air_motion_result(nil, vector(2, 3), false, true, false)
- assert.is_true(gmr:is_blocked_along(directions.up))
+ local amr = air_motion_result(nil, false, true, false)
+ assert.is_true(amr:is_blocked_along(directions.up))
end)
it('return false if direction is down and is_landing is false', function ()
- local gmr = air_motion_result(nil, vector(2, 3), false, false, false)
- assert.is_false(gmr:is_blocked_along(directions.down))
+ local amr = air_motion_result(nil, false, false, false)
+ assert.is_false(amr:is_blocked_along(directions.down))
end)
it('return true if direction is down and is_landing is true', function ()
- local gmr = air_motion_result(location(2, 1), vector(2, 3), false, false, true)
- assert.is_true(gmr:is_blocked_along(directions.down))
+ local amr = air_motion_result(location(2, 1), false, false, true)
+ assert.is_true(amr:is_blocked_along(directions.down))
end)
end)
describe('_tostring', function ()
- it('should return "air_motion_result(vector(2, 3), true, false, true, -0.25)"', function ()
- local gmr = air_motion_result(location(2, 1), vector(2, 3), true, false, true, -0.25)
- assert.are_equal("air_motion_result(location(2, 1), vector(2, 3), true, false, true, -0.25)", gmr:_tostring())
+ it('should return "air_motion_result(location(2, 1), true, false, true, -0.25)"', function ()
+ local amr = air_motion_result(location(2, 1), true, false, true, -0.25)
+ assert.are_equal("air_motion_result(location(2, 1), true, false, true, -0.25)", amr:_tostring())
end)
end)
diff --git a/src/platformer/world.lua b/src/platformer/world.lua
index a1d5c423..7ca7fd74 100644
--- a/src/platformer/world.lua
+++ b/src/platformer/world.lua
@@ -6,9 +6,7 @@ local world = {}
-- return quadrant in which angle is contained (non-injective)
function world.angle_to_quadrant(angle)
-- priority to vertical quadrants at the boundaries like Classic Sonic
- -- (so 45-deg slope is recognized as up/down)
- -- note that in those edge cases, the tiles should always be a rectangle to avoid confusion
- -- of which side the columns/rows are defined from
+ -- (so 45-deg slope is recognized as up/down, it's important if you're defining 45-deg slopes)
-- nil angle (airborne) defaults to down so Sonic will try to "stand up" in the air
if not angle or angle >= 0.875 or angle <= 0.125 then
return directions.down
@@ -161,7 +159,7 @@ function world.compute_qcolumn_height_at(region_tile_location, qcolumn_index0, q
local is_full_vertical_rectangle = tcd:is_full_vertical_rectangle()
local is_full_horizontal_rectangle = tcd:is_full_horizontal_rectangle()
- local is_full_rectangle = is_full_vertical_rectangle or is_full_horizontal_rectangle
+ local is_rectangle = tcd:is_rectangle() -- remember that land_on_empty_qcolumn => false
if quadrant % 2 == 1 then
-- floor/ceiling (quadrant down/up)
@@ -176,15 +174,29 @@ function world.compute_qcolumn_height_at(region_tile_location, qcolumn_index0, q
if ignore_reverse and not is_full_vertical_rectangle then
return 0--, nil
end
- -- return all-or-nothing, always with angle
- -- (not nil even if nothing, let ground motion set slope angle appropriately when falling)
- return height > 0 and tile_size or 0, world.quadrant_to_right_angle(quadrant)
- elseif is_full_rectangle then
+ -- return all-or-nothing
+ -- if all, return (height, slope angle)
+ -- if nothing, return (0, nil). This is important since we now support land_on_empty_qcolumn
+ -- and the only way to distinguish landing on a q-column of height 0 and finding no ground at all
+ -- if that the slope of no-ground is nil (we could also return height = nil by convention,
+ -- but we go this way for safety as slope angle wasn't used when falling anyway)
+ if height > 0 then
+ return tile_size, world.quadrant_to_right_angle(quadrant)
+ end
+ elseif is_rectangle then
-- flat side of rectangle (or empty region near flat side)
- return height, world.quadrant_to_right_angle(quadrant)
+ if height > 0 then
+ return height, world.quadrant_to_right_angle(quadrant)
+ end
+ else
+ -- normal side
+ -- return height and angle if height is positive,
+ -- or if height is 0, but we can land on an empty qcolumn to avoid detecting tile q-below
+ -- (the only way to distinguish no-ground from ground at height 0 is to pass a non-nil slope angle)
+ if height > 0 or tcd.land_on_empty_qcolumn and world.angle_to_quadrant(tcd.slope_angle) == quadrant then
+ return height, tcd.slope_angle
+ end
end
- -- normal side
- return height, tcd.slope_angle
else
-- right wall/left wall (quadrant right/left)
local width = tcd:get_width(qcolumn_index0)
@@ -193,13 +205,26 @@ function world.compute_qcolumn_height_at(region_tile_location, qcolumn_index0, q
if ignore_reverse and not is_full_horizontal_rectangle then
return 0--, nil
end
- return width > 0 and tile_size or 0, world.quadrant_to_right_angle(quadrant)
- elseif is_full_rectangle then
+ -- return all-or-nothing
+ -- if all, return (width, slope angle)
+ -- if nothing, return (0, nil). This is important since we now support land_on_empty_qcolumn
+ if width > 0 then
+ return tile_size, world.quadrant_to_right_angle(quadrant)
+ end
+ elseif is_rectangle then
-- flat side of rectangle (or empty region near flat side)
- return width, world.quadrant_to_right_angle(quadrant)
+ if width > 0 then
+ return width, world.quadrant_to_right_angle(quadrant)
+ end
+ else
+ -- normal side
+ -- return width and angle if width is positive,
+ -- or if width is 0, but we can land on an empty qcolumn to avoid detecting tile q-below
+ -- (same thing as above with height)
+ if width > 0 or tcd.land_on_empty_qcolumn and world.angle_to_quadrant(tcd.slope_angle) == quadrant then
+ return width, tcd.slope_angle
+ end
end
- -- normal side
- return width, tcd.slope_angle
end
end
diff --git a/src/platformer/world_utest.lua b/src/platformer/world_utest.lua
index 4fdb8573..f504e690 100644
--- a/src/platformer/world_utest.lua
+++ b/src/platformer/world_utest.lua
@@ -225,7 +225,7 @@ describe('world (with mock tiles data setup)', function ()
end)
- describe('_compute_qcolumn_height_at', function ()
+ describe('compute_qcolumn_height_at', function ()
it('should return (0, nil) if tile location is outside map area except on the left (any quadrant)', function ()
assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(128, 2), 0, directions.down)})
@@ -261,8 +261,9 @@ describe('world (with mock tiles data setup)', function ()
end)
- -- this unrealistic tile is useful to check all-or-nothing in both horizontal and vertical dirs
- -- more realistically, you could have an ascending slope that only occupies the bottom-right corner of the tile
+ -- we kept bottom_right_quarter_tile_id for testing,
+ -- but we actually have a bottom-right tile in real game now,
+ -- flat_high_tile_left_id used for spring right, just taller
describe('with tile_repr.bottom_right_quarter_tile_id offset by 2', function ()
before_each(function ()
@@ -270,51 +271,54 @@ describe('world (with mock tiles data setup)', function ()
mock_mset(1, 1, tile_repr.bottom_right_quarter_tile_id)
end)
- it('should return 0 on column 3 (quadrant down)', function ()
- assert.are_same({0, 0}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.down)})
+ it('should return (0, nil) on column 3 (quadrant down)', function ()
+ assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.down)})
end)
- it('should return 4 on column 4 (quadrant down)', function ()
+ -- this one is kind of a bad example... because a quarter tile is not a full rectangle,
+ -- we cannot apply the quadrant formula, and must use the tcd angle, which happens to be 0
+ -- which works but only because we're on quadrant down!
+ it('should return (4, slope angle) on column 4 (quadrant down)', function ()
assert.are_same({4, 0}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.down)})
end)
- it('should return 0 (reverse: nothing) on column 3 (quadrant up)', function ()
- assert.are_same({0, 0.5}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.up)})
+ it('should return (0, nil) (reverse: nothing) on column 3 (quadrant up)', function ()
+ assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.up)})
end)
- it('should return 8 (reverse: all) on column 4 (quadrant up)', function ()
+ it('should return (8, right angle) (reverse: all) on column 4 (quadrant up)', function ()
assert.are_same({8, 0.5}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.up)})
end)
- it('should return 0 (ignore reverse) on column 3 (quadrant up)', function ()
+ it('should return (0, nil) (ignore reverse) on column 3 (quadrant up)', function ()
assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.up, true)})
end)
- it('should return 0 (ignore reverse) on column 4 (quadrant up)', function ()
+ it('should return (0, nil) (ignore reverse) on column 4 (quadrant up)', function ()
assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.up, true)})
end)
- it('should return 0 on row 3 (quadrant right)', function ()
- assert.are_same({0, 0}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.right)})
+ it('should return (0, nil) on row 3 (quadrant right)', function ()
+ assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.right)})
end)
- it('should return 4 on row 3 (quadrant right)', function ()
- assert.are_same({4, 0}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.right)})
+ it('should return (4, right angle) (partial rectangle) on row 3 (quadrant right)', function ()
+ assert.are_same({4, 0.25}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.right)})
end)
- it('should return 0 (reverse: nothing) on row 3 (quadrant left)', function ()
- assert.are_same({0, 0.75}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.left)})
+ it('should return (0, nil) (reverse: nothing) on row 3 (quadrant left)', function ()
+ assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.left)})
end)
- it('should return 8 (reverse: all) on row 4 (quadrant left)', function ()
+ it('should return (8, right angle) (reverse: all) on row 4 (quadrant left)', function ()
assert.are_same({8, 0.75}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.left)})
end)
- it('should return 0 (ignore reverse) on row 3 (quadrant left)', function ()
+ it('should return (0, nil) (ignore reverse) on row 3 (quadrant left)', function ()
assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.left, true)})
end)
- it('should return 0 (ignore reverse) on row 4 (quadrant left)', function ()
+ it('should return (0, nil) (ignore reverse) on row 4 (quadrant left)', function ()
assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.left, true)})
end)
@@ -326,24 +330,50 @@ describe('world (with mock tiles data setup)', function ()
mock_mset(1, 1, tile_repr.visual_loop_topleft)
end)
- it('should return 8 on column 6 (quadrant down)', function ()
+ it('should return (8, right angle) (reverse all) on column 6 (quadrant down)', function ()
assert.are_same({8, 0}, {world.compute_qcolumn_height_at(location(1, 1), 6, directions.down)})
end)
- it('should return 4 on column 6 (quadrant up)', function ()
+ it('should return (4, slope angle) on column 6 (quadrant up)', function ()
assert.are_same({4, atan2(-8, 5)}, {world.compute_qcolumn_height_at(location(1, 1), 6, directions.up)})
end)
- it('should return 8 on row 6 (quadrant right)', function ()
+ it('should return (8, right angle) (reverse all) on row 6 (quadrant right)', function ()
assert.are_same({8, 0.25}, {world.compute_qcolumn_height_at(location(1, 1), 6, directions.right)})
end)
- it('should return 2 on row 6 (quadrant left)', function ()
+ it('should return (2, slope angle) on row 6 (quadrant left)', function ()
assert.are_same({2, atan2(-8, 5)}, {world.compute_qcolumn_height_at(location(1, 1), 6, directions.left)})
end)
end)
+ describe('with full tile', function ()
+
+ before_each(function ()
+ mock_mset(1, 1, tile_repr.full_tile_id)
+ end)
+
+ -- full_tile_id allows us to test is_full_rectangle case
+
+ it('should return (8, right angle) (full rectangle) on any column (quadrant down)', function ()
+ assert.are_same({8, 0}, {world.compute_qcolumn_height_at(location(1, 1), 6, directions.down)})
+ end)
+
+ it('should return (8, right angle) (full rectangle) on any column (quadrant up)', function ()
+ assert.are_same({8, 0.5}, {world.compute_qcolumn_height_at(location(1, 1), 2, directions.up)})
+ end)
+
+ it('should return (8, right angle) (full rectangle) on any column (quadrant left)', function ()
+ assert.are_same({8, 0.75}, {world.compute_qcolumn_height_at(location(1, 1), 7, directions.left)})
+ end)
+
+ it('should return (8, right angle) (full rectangle) on any column (quadrant right)', function ()
+ assert.are_same({8, 0.25}, {world.compute_qcolumn_height_at(location(1, 1), 0, directions.right)})
+ end)
+
+ end)
+
describe('with half-tile', function ()
before_each(function ()
@@ -355,36 +385,96 @@ describe('world (with mock tiles data setup)', function ()
-- because we are only testing the quadrant left/right + is_rectangle case
-- and not up/down... but that would only be a hypothetical tile, we don't have such a thing
-- right now in the game
- it('should return 4 (rectangle) on column 6 (quadrant down)', function ()
+ it('should return (4, right angle) (rectangle) on column 6 (quadrant down)', function ()
assert.are_same({4, 0}, {world.compute_qcolumn_height_at(location(1, 1), 6, directions.down)})
end)
- it('should return 8 (reverse all) on column 6 (quadrant up)', function ()
+ it('should return (8, right angle) (reverse all) on column 6 (quadrant up)', function ()
assert.are_same({8, 0.5}, {world.compute_qcolumn_height_at(location(1, 1), 6, directions.up)})
end)
- it('should return 0 (ignore reverse) on column 6 (quadrant up)', function ()
+ it('should return (0, nil) (ignore reverse) on column 6 (quadrant up)', function ()
assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 6, directions.up, true)})
end)
- it('should return 0 (rectangle) on row 3 (quadrant right)', function ()
- assert.are_same({0, 0.25}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.right)})
+ it('should return (0, nil) (empty part besides rectangle) on row 3 (quadrant right)', function ()
+ assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.right)})
end)
- it('should return 0 (rectangle) on row 4 (quadrant right)', function ()
+ it('should return (8, right angle) (rectangle) on row 4 (quadrant right)', function ()
assert.are_same({8, 0.25}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.right)})
end)
- it('should return 0 (rectangle) on row 3 (quadrant left)', function ()
- assert.are_same({0, 0.75}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.left)})
+ it('should return (0, nil) (empty part besides rectangle) on row 3 (quadrant left)', function ()
+ assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.left)})
end)
- it('should return 8 (rectangle) on row 4 (quadrant left)', function ()
+ it('should return (8, right angle) (rectangle) on row 4 (quadrant left)', function ()
assert.are_same({8, 0.75}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.left)})
end)
end)
+ describe('with vertical rectangle filled at 3/4 on the left (aka spring right mask or rect 6x8 on the left)', function ()
+
+ before_each(function ()
+ mock_mset(1, 1, tile_repr.spring_right_mask_repr_tile_id)
+ end)
+
+ it('should return (8, right angle) (rectangle) on column 5 (quadrant down)', function ()
+ assert.are_same({8, 0}, {world.compute_qcolumn_height_at(location(1, 1), 5, directions.down)})
+ end)
+
+ it('should return (0, nil) (empty part besides rectangle) on column 6 (quadrant down)', function ()
+ assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 6, directions.down, true)})
+ end)
+
+ it('should return (8, right angle) (reverse all) on row 4 (quadrant right)', function ()
+ assert.are_same({8, 0.25}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.right)})
+ end)
+
+ -- the very test that helped us fix the 2px offset (away) during collision with spring right
+ -- normally we should even add a symmetrical test with rectangles with interior right and up,
+ -- but now that we've understood that the slope angle matters for non-8x8 rectangles to define
+ -- the interior, it's OK
+ it('should return (6, right angle) (rectangle) on row 4 (quadrant left)', function ()
+ assert.are_same({6, 0.75}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.left)})
+ end)
+
+ end)
+
+ describe('with last part of descending slope every 4px (to test land_on_empty_qcolumn)', function ()
+
+ before_each(function ()
+ mock_mset(1, 1, tile_repr.desc_slope_2px_last_id)
+ end)
+
+ it('should return (1, slope angle) (left part is not empty) on column 3 (quadrant down)', function ()
+ assert.are_same({1, atan2(8, 2)}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.down)})
+ end)
+
+ it('should return (0, *slope angle*) (right part empty but land_on_empty_qcolumn allows landing) on column 4 (quadrant down)', function ()
+ assert.are_same({0, atan2(8, 2)}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.down)})
+ end)
+
+ it('should return (8, right angle) (reverse non-empty column is flat ground) on column 3 (quadrant up)', function ()
+ assert.are_same({8, 0.5}, {world.compute_qcolumn_height_at(location(1, 1), 3, directions.up)})
+ end)
+
+ it('should return (0, nil) (empty column found on reverse check *ignores* land_on_empty_qcolumn) on column 4 (quadrant up)', function ()
+ assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 4, directions.up)})
+ end)
+
+ it('should return (0, nil) (non-main quadrant check *ignores* land_on_empty_qcolumn) on row 0 (quadrant left)', function ()
+ assert.are_same({0, nil}, {world.compute_qcolumn_height_at(location(1, 1), 0, directions.left)})
+ end)
+
+ it('should return (4, slope) (kinda rare but testing the row of 4px from the right) on row 7 (quadrant left)', function ()
+ assert.are_same({4, atan2(8, 2)}, {world.compute_qcolumn_height_at(location(1, 1), 7, directions.left)})
+ end)
+
+ end)
+
end)
end)
diff --git a/src/resources/audio.lua b/src/resources/audio.lua
index e99e9744..704a4057 100644
--- a/src/resources/audio.lua
+++ b/src/resources/audio.lua
@@ -1,3 +1,7 @@
+--#if game_constants
+--(when using replace_strings with --game-constant-module-path [this_data.lua], all namespaced constants
+-- below are replaced with their values (as strings), so this file can be skipped)
+
local audio = {}
audio.sfx_ids = {
@@ -10,24 +14,29 @@ audio.sfx_ids = {
got_all_emeralds = 56,
-- builtin_data_ingame only
+ jump = 55,
+ spring_jump = 56,
+ roll = 57,
+ brake = 58,
-- because it plays on 4th channel over low-volume bgm,
-- pick emerald jingle is considered an sfx
- pick_emerald = 57,
- goal_reached = 58,
- jump = 59,
- spring_jump = 60,
- roll = 61,
- brake = 62,
+ pick_emerald = 59,
+ goal_reached = 60,
+ spin_dash_rev = 61,
+ spin_dash_release = 62
}
audio.jingle_ids = {
- -- builtin_data_stage_clear only (overlaps stage bgm in data_bgm1.p8)
+ -- builtin_data_stage_clear only (overlaps stage bgm in builtin_data_ingame.p8)
stage_clear = 0,
}
audio.music_ids = {
- -- builtin_data_titlemenu only (overlaps stage bgm in data_bgm1.p8)
+ -- builtin_data_titlemenu only (overlaps stage bgm in builtin_data_ingame.p8)
title = 0,
}
+--(game_constants)
+--#endif
+
return audio
diff --git a/src/resources/memory.lua b/src/resources/memory.lua
new file mode 100644
index 00000000..82187f74
--- /dev/null
+++ b/src/resources/memory.lua
@@ -0,0 +1,12 @@
+--#if game_constants
+--(when using replace_strings with --game-constant-module-path [this_data.lua], all namespaced constants
+-- below are replaced with their values (as strings), so this file can be skipped)
+
+local memory = {
+ picked_emerald_address = 0x5dff
+}
+
+--(game_constants)
+--#endif
+
+return memory
diff --git a/src/resources/visual_common.lua b/src/resources/visual_common.lua
index 7a97d341..cf75ff7a 100644
--- a/src/resources/visual_common.lua
+++ b/src/resources/visual_common.lua
@@ -33,18 +33,27 @@ local visual = {
}
}
-local sprite_data_t = {
- -- COMMON INITIAL SPRITES
+local sprite_data_t = transform(
+ {
+ -- sprite_data(id_loc: sprite_id_location([1], [2]), span: nil (currently all sprites below are 1x1),
+ -- pivot: vector([3], [4]), transparent_color_arg: colors.pink),
+ -- parameters: {id_loc(2), pivot(2)}
+
+ -- COMMON INITIAL SPRITES
--#if mouse
- cursor = sprite_data(sprite_id_location(9, 0), nil, nil, colors.pink),
+ cursor = { 9, 0, 0, 0},
--#endif
- -- ANIMATION SPRITES
- emerald_pick_fx1 = sprite_data(sprite_id_location(12, 0), nil, vector(4, 4), colors.pink),
- emerald_pick_fx2 = sprite_data(sprite_id_location(13, 0), nil, vector(4, 4), colors.pink),
- emerald_pick_fx3 = sprite_data(sprite_id_location(14, 0), nil, vector(4, 4), colors.pink),
- emerald_pick_fx4 = sprite_data(sprite_id_location(15, 0), nil, vector(4, 4), colors.pink),
-}
+ -- ANIMATION SPRITES
+ emerald_pick_fx1 = { 12, 0, 4, 4},
+ emerald_pick_fx2 = { 13, 0, 4, 4},
+ emerald_pick_fx3 = { 14, 0, 4, 4},
+ emerald_pick_fx4 = { 15, 0, 4, 4},
+ },
+ function (params)
+ return sprite_data(sprite_id_location(params[1], params[2]), nil, vector(params[3], params[4]), colors.pink)
+ end
+)
visual.sprite_data_t = sprite_data_t
diff --git a/src/resources/visual_ingame_addon.lua b/src/resources/visual_ingame_addon.lua
index c119f09d..8d4b385c 100644
--- a/src/resources/visual_ingame_addon.lua
+++ b/src/resources/visual_ingame_addon.lua
@@ -3,64 +3,49 @@ local visual = require("resources/visual_common")
local animated_sprite_data = require("engine/render/animated_sprite_data")
local sprite_data = require("engine/render/sprite_data")
--- visual for in-game only
--- it uses the add-on system, which means you only need to require it along with visual_common,
--- but only get the return value of visual_common named `visual` here
--- it will automatically add extra information to `visual`
-local ingame_visual = {
- spring_up_repr_tile_id = 74, -- add 1 to get right part, must match value in tile_representation
- -- and location in ingame_sprite_data_t.spring
- spring_left_repr_tile_id = 202, -- just representing spring oriented to left on tilemap,
- -- we use the generic sprite rotated for rendering
- spring_right_repr_tile_id = 173, -- just representing spring oriented to right on tilemap,
- -- we use the generic sprite rotated for rendering
+local ingame_sprite_data_t = transform(
+ {
+ -- sprite_data(id_loc: sprite_id_location([1], [2]), span: tile_vector([3], [4]), pivot: vector([5], [6]), transparent_color_arg: colors.pink),
+ -- parameters: {id_loc(2), span(2), pivot(2)}
- -- palm tree top representative tile is drawn via tilemap, so id is enough
- -- for extension sprites drawn around it, see ingame_sprite_data_t.palm_tree_leaves*
- palm_tree_leaves_core_id = 236,
+ -- palm tree extension sprites
+ -- top pivot is located at top-left of core
+ palm_tree_leaves_top = { 12, 12, 1, 2, 0, 16},
+ -- right side pivot is located at top-right of core
+ -- left side is a mirror of right side, and must be placed just on the left of the core
+ -- no multicolor transparency right now, but if you add sprites on the top-left of the palm tree leaves for compactness on the spritesheet,
+ -- you can, but they must have no common colors with the leaves, and you must add all of their unique colors to the transparency list {color1, color2, ...}
+ palm_tree_leaves_right = { 13, 12, 3, 4, 0, 16},
- -- launch ramp last tile
- launch_ramp_last_tile_id = 229,
+ -- RUNTIME SPRITES (stage-specific and common runtime)
+ -- below need runtime sprites to be reloaded, overwriting collision masks
+ background_forest_bottom_hole = { 1, 0, 2, 3, 0, 0},
+ background_forest_bottom_lightshaft = { 13, 1, 3, 2, 0, 0},
+ emerald_silhouette = { 9, 1, 1, 1, 3, 2},
+ goal_plate_goal = { 3, 0, 3, 2, 12, 16},
+ goal_plate_sonic = { 6, 0, 3, 2, 12, 16},
+ goal_plate_rotating_90 = { 0, 1, 1, 2, 4, 16},
- -- goal plate base id (representative tile used to generate animated sprite)
- goal_plate_base_id = 226,
+ -- rotating goal plates at 45 degrees are exceptions and placed in the common area despite only being used at runtime
+ -- this is simply because there was no space left for sprites 2-tile high in the runtime area; hence the high location j
+ goal_plate_rotating_45_ccw = { 6, 14, 2, 2, 7, 16},
+ goal_plate_rotating_45_cw = { 8, 14, 2, 2, 8, 16},
- -- emerald_repr_sprite_id will be derived from sprite data, see below
-}
-
-local ingame_sprite_data_t = {
- -- palm tree extension sprites
- -- top pivot is located at top-left of core
- palm_tree_leaves_top = sprite_data(sprite_id_location(12, 12), tile_vector(1, 2), vector(0, 16), colors.pink),
- -- right side pivot is located at top-right of core
- -- left side is a mirror of right side, and must be placed just on the left of the core
- -- we need multicolor transparency to exclude rock sprite located in the same rectangle for compactness
- palm_tree_leaves_right = sprite_data(sprite_id_location(13, 12), tile_vector(3, 4), vector(0, 16), {colors.pink, colors.black, colors.dark_blue, colors.indigo, colors.white}),
-
- -- RUNTIME SPRITES (stage-specific and common runtime)
- -- below need runtime sprites to be reloaded, overwriting collision masks
- background_forest_bottom_hole = sprite_data(sprite_id_location(1, 0), tile_vector(2, 3), vector(0, 0), colors.pink),
- emerald_silhouette = sprite_data(sprite_id_location(9, 1), nil, vector(3, 2), colors.pink),
- goal_plate_goal = sprite_data(sprite_id_location(3, 0), tile_vector(3, 2), vector(12, 16), colors.pink),
- goal_plate_sonic = sprite_data(sprite_id_location(6, 0), tile_vector(3, 2), vector(12, 16), colors.pink),
- goal_plate_rotating_90 = sprite_data(sprite_id_location(0, 1), tile_vector(1, 2), vector(4, 16), colors.pink),
+ -- emerald representation tile (left part) and object sprite (both parts)
+ emerald = { 10, 15, 1, 1, 3, 2},
- -- rotating goal plates at 45 degrees are exceptions and placed in the common area despite only being used at runtime
- -- this is simply because there was no space left for sprites 2-tile high in the runtime area; hence the high location j
- goal_plate_rotating_45_ccw = sprite_data(sprite_id_location(6, 14), tile_vector(2, 2), vector(7, 16), colors.pink),
- goal_plate_rotating_45_cw = sprite_data(sprite_id_location(8, 14), tile_vector(2, 2), vector(8, 16), colors.pink),
-
- -- emerald representation tile (left part) and object sprite (both parts)
- emerald = sprite_data(sprite_id_location(10, 15), nil, vector(3, 2), colors.pink),
-
- -- spring (pivot at bottom center on both sprites so it extends correctly)
- spring = sprite_data(sprite_id_location(10, 4), tile_vector(2, 1), vector(10, 2), colors.pink),
- spring_extended = sprite_data(sprite_id_location(10, 5), tile_vector(2, 2), vector(10, 10), colors.pink)
-}
+ -- spring (pivot at bottom center on both sprites so it extends correctly)
+ spring = { 10, 4, 2, 1, 10, 2},
+ spring_extended = { 10, 5, 2, 2, 10, 10}
+ },
+ function (params)
+ return sprite_data(sprite_id_location(params[1], params[2]), tile_vector(params[3], params[4]), vector(params[5], params[6]), colors.pink)
+ end
+)
-- derived data: the representative sprite of an emerald (the one placed on the tilemap)
-- in the left part of the sprite, so convert id location (which is at top-left) to sprite ID
-ingame_visual.emerald_repr_sprite_id = ingame_sprite_data_t.emerald.id_loc:to_sprite_id()
+visual.emerald_repr_sprite_id = ingame_sprite_data_t.emerald.id_loc:to_sprite_id()
local ingame_animated_sprite_data_t = {
goal_plate = {
@@ -85,6 +70,5 @@ local ingame_animated_sprite_data_t = {
}
}
-merge(visual, ingame_visual)
merge(visual.sprite_data_t, ingame_sprite_data_t)
merge(visual.animated_sprite_data_t, ingame_animated_sprite_data_t)
diff --git a/src/resources/visual_ingame_numerical_data.lua b/src/resources/visual_ingame_numerical_data.lua
new file mode 100644
index 00000000..692307d2
--- /dev/null
+++ b/src/resources/visual_ingame_numerical_data.lua
@@ -0,0 +1,38 @@
+--#if game_constants
+--(when using replace_strings with --game-constant-module-path [this_data.lua], all namespaced constants
+-- below are replaced with their values (as strings), so this file can be skipped)
+
+-- visual numerical data for in-game only
+-- it uses the add-on system, which means you only need to require it along with visual_common,
+-- but only get the return value of visual_common named `visual` here
+-- it will automatically add extra information to `visual`
+local visual_ingame_data = {
+ spring_up_repr_tile_id = 74, -- add 1 to get right part, must match value in tile_representation
+ -- and location in ingame_sprite_data_t.spring (in visual_ingame_addon.lua)
+ spring_left_repr_tile_id = 202, -- just representing spring oriented to left on tilemap,
+ -- we use the generic sprite rotated for rendering
+ spring_right_repr_tile_id = 173, -- just representing spring oriented to right on tilemap,
+ -- we use the generic sprite rotated for rendering
+
+ -- hiding leaves, must be known to detect emerald surrounded by them and render an extra hiding leaf
+ -- on top of the emerald itself (via tilemap)
+ hiding_leaves_id = 234,
+
+ -- palm tree top representative tile is drawn via tilemap, so id is enough
+ -- for extension sprites drawn around it, see ingame_sprite_data_t.palm_tree_leaves*
+ -- (in visual_ingame_addon.lua)
+ palm_tree_leaves_core_id = 236,
+
+ -- launch ramp last tile
+ launch_ramp_last_tile_id = 229,
+
+ -- goal plate base id (representative tile used to generate animated sprite)
+ goal_plate_base_id = 226,
+
+ -- emerald_repr_sprite_id will be derived from sprite data, see visual_ingame_addon.lua
+}
+
+--(game_constants)
+--#endif
+
+return visual_ingame_data
diff --git a/src/resources/visual_stage.lua b/src/resources/visual_stage.lua
index ec4b79c9..4375fbd8 100644
--- a/src/resources/visual_stage.lua
+++ b/src/resources/visual_stage.lua
@@ -196,17 +196,8 @@ function visual_stage.draw_background_forest_bottom(camera_pos, horizon_line_dy)
local hole_y0 = y0 + tile_offset_j_cycle[i + 1] * tile_size
visual.sprite_data_t.background_forest_bottom_hole:render(vector(x0, hole_y0))
-- light shaft
- local light_shaft_start = vector(x0 - 1, hole_y0 + 2 * tile_size + 4)
- line(light_shaft_start.x, light_shaft_start.y, light_shaft_start.x - 15, light_shaft_start.y + 7, colors.dark_green)
- line(light_shaft_start.x + 1, light_shaft_start.y, light_shaft_start.x - 15, light_shaft_start.y + 8, colors.green)
- -- add bits of yellow to make the ray shinier
- for k = 0, 2 do
- pset(light_shaft_start.x - 3 - 6 * k, light_shaft_start.y + 2 + 3 * k, colors.yellow)
- end
- line(light_shaft_start.x, light_shaft_start.y + 1, light_shaft_start.x - 14, light_shaft_start.y + 9, colors.green)
- line(light_shaft_start.x, light_shaft_start.y + 2, light_shaft_start.x - 13, light_shaft_start.y + 9, colors.green)
- line(light_shaft_start.x + 1, light_shaft_start.y + 2, light_shaft_start.x - 11, light_shaft_start.y + 9, colors.green)
- line(light_shaft_start.x, light_shaft_start.y + 3, light_shaft_start.x - 10, light_shaft_start.y + 9, colors.dark_green)
+ -- located 3 tiles to the left, 2 tiles down of hole -> (-3*8, 2*8) = (-24, 16)
+ visual.sprite_data_t.background_forest_bottom_lightshaft:render(vector(x0 - 24, hole_y0 + 16))
end
end
@@ -254,28 +245,25 @@ function visual_stage.draw_cloud(x, y, dy_list, base_radius, speed)
end
end
+-- TODO VISUAL IMPROVEMENT: improve colors by reusing water_shimmer_color_cycle from visual_titlemenu_addon
+local water_reflection_color_cycle = {
+ {colors.dark_blue, colors.blue},
+ {colors.white, colors.blue},
+ {colors.blue, colors.dark_blue},
+ {colors.blue, colors.white},
+ {colors.dark_blue, colors.blue}
+}
+
function visual_stage.draw_water_reflections(parallax_offset, x, y, period)
-- animate reflections by switching colors over time
local ratio = (t() % period) / period
- local c1, c2
- if ratio < 0.2 then
- c1 = colors.dark_blue
- c2 = colors.blue
- elseif ratio < 0.4 then
- c1 = colors.white
- c2 = colors.blue
- elseif ratio < 0.6 then
- c1 = colors.blue
- c2 = colors.dark_blue
- elseif ratio < 0.8 then
- c1 = colors.blue
- c2 = colors.white
- else
- c1 = colors.dark_blue
- c2 = colors.blue
- end
- pset((x - parallax_offset) % screen_width, y, c1)
- pset((x - parallax_offset + 1) % screen_width, y, c2)
+ local step_count = #water_reflection_color_cycle
+ -- compute step from ratio (normally ratio should be < 1
+ -- just in case, max to step_count)
+ local step = min(flr(ratio * step_count) + 1, step_count)
+ local draw_colors = water_reflection_color_cycle[step]
+ pset((x - parallax_offset) % screen_width, y, draw_colors[1])
+ pset((x - parallax_offset + 1) % screen_width, y, draw_colors[2])
end
function visual_stage.draw_tree_row(parallax_offset, y, base_height, row_index0, color)
diff --git a/src/sandbox.lua b/src/sandbox.lua
deleted file mode 100644
index afa08bdd..00000000
--- a/src/sandbox.lua
+++ /dev/null
@@ -1,26 +0,0 @@
--- caveats
-
--- syntax error: malformed number near 27..d
--- this error will block the output stream, getting picotest stuck!
--- printh(27..vector(11, 45)) -- incorrect
--- correct:
-printh("27"..vector(11, 45))
--- or
--- printh(tostr(27)..vector(11, 45))
-
-s = [[
-1
-
-2]]
-
-lines = strspl(s, "\n")
-
--- COMMENT
---[[BLOCk
-COMMENT]]
-for line in all(lines) do
- print("line: "..line)
-end
-
---[[
---]]
diff --git a/src/stage_clear/stage_clear_state.lua b/src/stage_clear/stage_clear_state.lua
index 521965ff..5b1d471f 100644
--- a/src/stage_clear/stage_clear_state.lua
+++ b/src/stage_clear/stage_clear_state.lua
@@ -12,7 +12,9 @@ local menu_item = require("menu/menu_item")
local menu = require("menu/menu_with_sfx")
local audio = require("resources/audio")
local ui_animation = require("ui/ui_animation")
+local memory = require("resources/memory")
local visual = require("resources/visual_common") -- we should require ingameadd-on in main
+local visual_ingame_data = require("resources/visual_ingame_numerical_data")
local visual_stage = require("resources/visual_stage")
local stage_clear_state = derived_class(base_stage_state)
@@ -44,13 +46,13 @@ end
function stage_clear_state.retry_from_zero_async()
-- clear picked emeralds data (see stage_state:store_picked_emerald_data) in general memory
- poke(0x5d00, 0)
+ poke(memory.picked_emerald_address, 0)
stage_clear_state.retry_stage_async()
end
function stage_clear_state.back_to_titlemenu_async()
-- remember to clear picked emerald data, so if we start again from titlemenu we'll also restart from zero
- poke(0x5d00, 0)
+ poke(memory.picked_emerald_address, 0)
-- zigzag fadeout will also give time to player to hear confirm SFX
flow.curr_state:zigzag_fade_out_async()
@@ -86,9 +88,8 @@ end
function stage_clear_state:on_enter()
-- simplified compared to stage_state
-- we don't even need to reload runtime spritesheet since the stage_clear builtin spritesheet
- -- now integrates the runtime spritesheet top rows from the start (and later, ingame may do the same,
- -- since ultimately we only need the tile masks in the top rows for initial collision data loading,
- -- and we could quick reload on stage start just for that)
+ -- now integrates the runtime spritesheet top rows from the start (as there is no physics during stage clear
+ -- so we don't need the collision masks as builtin data)
-- we need the runtime sprites for goal plate and menu cursor in particular
-- first, restore picked emerald data set in ingame, just before loading this cartridge
@@ -99,8 +100,7 @@ function stage_clear_state:on_enter()
-- Instead we just set the position directly to values observed at the end
-- of ingame cartridge, just before loading stage_clear. This allows us to use base_stage_state methods
-- relying on the camera position.
- self.camera.position.x = 3392
- self.camera.position.y = 328
+ self.camera:init_position(vector(3376, 328))
-- Hardcoded: we know that goal is in region (3, 1)
reload(0x2000, 0x2000, 0x1000, self:get_map_region_filename(3, 1))
@@ -121,7 +121,7 @@ function stage_clear_state:play_stage_clear_sequence_async()
-- stop BGM and play stage clear jingle
music(audio.jingle_ids.stage_clear)
- yield_delay(stage_clear_data.stage_clear_duration)
+ yield_delay_frames(stage_clear_data.stage_clear_duration)
-- play result UI "calculation" (we don't have score so it's just checking
-- if we have all the emeralds)
@@ -166,7 +166,7 @@ function stage_clear_state:render()
-- phase 0: stage result
-- see set_camera_with_origin for value explanation (we must pass camera position)
- visual_stage.render_background(vector(3392, 328))
+ visual_stage.render_background(vector(3376, 328))
self:render_stage_elements()
else
-- phase 1: retry menu
@@ -211,7 +211,7 @@ function stage_clear_state:scan_current_region_to_spawn_objects()
-- there is only one object type we are interested in, the goal plate,
-- so check it manually instead of using a table of spawn callbacks as in stage_state
- if tile_sprite_id == visual.goal_plate_base_id then
+ if tile_sprite_id == visual_ingame_data.goal_plate_base_id then
-- tile has been recognized as a representative tile for object spawning
-- apply callback now
@@ -226,14 +226,6 @@ function stage_clear_state:scan_current_region_to_spawn_objects()
end
--- camera methods, also simplified versions of stage_stage equivalent
-
--- same as stage_state:region_to_global_location but short enough to copy
-function stage_clear_state:region_to_global_location(region_loc)
- return region_loc + self:get_region_topleft_location()
-end
-
-
-- actual stage clear sequence functions
function stage_clear_state:restore_picked_emerald_data()
@@ -241,7 +233,7 @@ function stage_clear_state:restore_picked_emerald_data()
-- cartridge was loaded
-- similar to stage_state:restore_picked_emerald_data, but we don't remove emerald objects
-- and cache the picked count for assessment
- local picked_emerald_byte = peek(0x5d00)
+ local picked_emerald_byte = peek(memory.picked_emerald_address)
-- read bitset low-endian, from lowest bit (emerald 1) to highest bit (emerald 8)
for i = 1, 8 do
@@ -285,15 +277,15 @@ function stage_clear_state:assess_result_async()
local light_color, dark_color = unpack(visual.emerald_colors[num])
-- brightness level is: step 1 => 2, step 2 => 1, step 3 => 0 (or nil)
self.result_emerald_brightness_levels[num] = 3 - step
- yield_delay(10) -- duration of a step
+ yield_delay_frames(9) -- duration of a step
end
end
-- clear table will reset brightness level to nil, interpreted as 0
clear_table(self.result_emerald_brightness_levels)
- yield_delay(10) -- pause between emeralds
+ yield_delay_frames(9) -- pause between emeralds
end
- yield_delay(30)
+ yield_delay_frames(30)
-- retrieve labels from overlay (as we didn't store references as state members)
local sonic_label = self.result_overlay.drawables_map["sonic"]
@@ -311,7 +303,7 @@ function stage_clear_state:assess_result_async()
self.result_overlay:remove_drawable("through")
self.result_overlay:remove_drawable("stage")
- yield_delay(30)
+ yield_delay_frames(30)
local got_all_emeralds = self.picked_emerald_count >= 8
@@ -423,7 +415,7 @@ function stage_clear_state:show_retry_screen_async()
-- fade in (we start from everything black so skip max darkness 5)
for i = 4, 0, -1 do
self.postproc.darkness = i
- yield_delay(5)
+ yield_delay_frames(4)
end
end
@@ -439,29 +431,6 @@ function stage_clear_state:render_stage_elements()
self:render_environment_foreground()
end
--- render the stage environment (tiles)
-function stage_clear_state:render_environment_midground()
- -- possible optimize: don't draw the whole stage offset by camera,
- -- instead just draw the portion of the level of interest
- -- (and either keep camera offset or offset manually and subtract from camera offset)
- -- that said, I didn't notice a performance drop by drawing the full tilemap
- -- so I guess map is already optimized to only draw what's on camera
- set_unique_transparency(colors.pink)
-
- -- only draw midground tiles
- -- note that we are drawing loop entrance tiles even though they will be (they'll be drawn on foreground later)
- self:set_camera_with_region_origin()
- map(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.midground)
-end
-
-function stage_clear_state:render_environment_foreground()
- set_unique_transparency(colors.pink)
-
- -- draw tiles always on foreground
- self:set_camera_with_region_origin()
- map(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.foreground)
-end
-
-- render the goal plate upper body (similar to stage_state equivalent)
function stage_clear_state:render_goal_plate()
assert(self.goal_plate, "stage_clear_state:render_goal_plate: no goal plate spawned in stage")
diff --git a/src/stage_clear/stage_clear_state_utest.lua b/src/stage_clear/stage_clear_state_utest.lua
index e94bd417..3fedd681 100644
--- a/src/stage_clear/stage_clear_state_utest.lua
+++ b/src/stage_clear/stage_clear_state_utest.lua
@@ -21,6 +21,7 @@ local base_stage_state = require("ingame/base_stage_state")
local goal_plate = require("ingame/goal_plate")
local titlemenu = require("menu/titlemenu")
local visual = require("resources/visual_common")
+local visual_ingame_data = require("resources/visual_ingame_numerical_data")
local visual_stage = require("resources/visual_stage")
local tile_repr = require("test_data/tile_representation")
local tile_test_data = require("test_data/tile_test_data")
@@ -65,7 +66,7 @@ describe('stage_clear_state', function ()
base_stage_state.init:clear()
end)
- it('#solo should call base constructor', function ()
+ it('should call base constructor', function ()
assert.spy(base_stage_state.init).was_called(1)
assert.spy(base_stage_state.init).was_called_with(match.ref(state))
end)
@@ -130,7 +131,9 @@ describe('stage_clear_state', function ()
end)
it('should hardcode set camera position', function ()
- assert.are_equal(vector(3392, 328), state.camera.position)
+ -- uses init_position under the hood, but check result directly to simplify
+ assert.are_equal(vector(3376, 328), state.camera.position)
+ assert.are_equal(328, state.camera.base_position.y)
end)
it('should hardcode set loaded_map_region_coords', function ()
@@ -206,9 +209,9 @@ describe('stage_clear_state', function ()
-- we're not using tile_test_data.setup here
-- (since objects are checked directly by id, not using collision data)
-- so don't use mock_mset
- mset(1, 1, visual.goal_plate_base_id)
- mset(2, 2, visual.goal_plate_base_id)
- mset(3, 3, visual.goal_plate_base_id)
+ mset(1, 1, visual_ingame_data.goal_plate_base_id)
+ mset(2, 2, visual_ingame_data.goal_plate_base_id)
+ mset(3, 3, visual_ingame_data.goal_plate_base_id)
-- mock stage dimensions, not too big to avoid test too long
-- (just 2 regions so we can check that location conversion works)
@@ -326,13 +329,13 @@ describe('stage_clear_state', function ()
stage_clear_state.render_emeralds:clear()
end)
- it('#solo (phase 0) should call render_background, render_stage_elements, render_overlay, render_emeralds', function ()
+ it('(phase 0) should call render_background, render_stage_elements, render_overlay, render_emeralds', function ()
state.phase = 0
state:render()
assert.spy(visual_stage.render_background).was_called(1)
- assert.spy(visual_stage.render_background).was_called_with(vector(3392, 328))
+ assert.spy(visual_stage.render_background).was_called_with(vector(3376, 328))
assert.spy(stage_clear_state.render_stage_elements).was_called(1)
assert.spy(stage_clear_state.render_stage_elements).was_called_with(match.ref(state))
assert.spy(stage_clear_state.render_emeralds).was_called(1)
@@ -392,57 +395,6 @@ describe('stage_clear_state', function ()
end)
- describe('set_camera_with_origin', function ()
-
- it('should set the pico8 camera to hardcoded position around goal', function ()
- state:set_camera_with_origin()
- assert.are_same(vector(3328, 264), vector(pico8.camera_x, pico8.camera_y))
- end)
-
- it('should set the pico8 camera to hardcoded position around goal, with custom origin subtracted', function ()
- state:set_camera_with_origin(vector(10, 20))
- assert.are_same(vector(3328 - 10, 264 - 20), vector(pico8.camera_x, pico8.camera_y))
- end)
-
- end)
-
- describe('set_camera_with_region_origin', function ()
-
- setup(function ()
- stub(stage_clear_state, "set_camera_with_origin")
- end)
-
- teardown(function ()
- stage_clear_state.set_camera_with_origin:revert()
- end)
-
- after_each(function ()
- stage_clear_state.set_camera_with_origin:clear()
- end)
-
- it('should call set_camera_with_origin with current region topleft xy', function ()
- state:set_camera_with_region_origin()
-
- assert.spy(state.set_camera_with_origin).was_called(1)
- assert.spy(state.set_camera_with_origin).was_called_with(match.ref(state), vector(tile_size * map_region_tile_width * 3, tile_size * map_region_tile_height * 1))
- end)
-
- end)
-
- describe('region_to_global_location', function ()
- it('region loc (0, 1) => (0 + map_region_tile_width * 3, 1 + map_region_tile_height * 1)', function ()
- assert.are_equal(location(0 + map_region_tile_width * 3, 1 + map_region_tile_height * 1), state:region_to_global_location(location(0, 1)))
- end)
- end)
-
- describe('get_region_topleft_location', function ()
-
- it('should return hardcoded location(map_region_tile_width * 3, map_region_tile_height * 1)', function ()
- assert.are_same(location(map_region_tile_width * 3, map_region_tile_height * 1), state:get_region_topleft_location())
- end)
-
- end)
-
describe('render_stage_elements', function ()
setup(function ()
@@ -576,28 +528,6 @@ describe('stage_clear_state', function ()
map:clear()
end)
- it('render_environment_midground should call map for all midground sprites', function ()
- state:render_environment_midground()
-
- assert.spy(stage_clear_state.set_camera_with_region_origin).was_called(1)
- assert.spy(stage_clear_state.set_camera_with_region_origin).was_called_with(match.ref(state))
-
- assert.spy(map).was_called(1)
- assert.spy(map).was_called_with(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.midground)
- end)
-
- it('render_environment_foreground should call spr on tiles present on screen', function ()
- state:render_environment_foreground()
-
- -- we can't check call order, but set camera methods should be called consistently with map!
- assert.spy(stage_clear_state.set_camera_with_region_origin).was_called(1)
- assert.spy(stage_clear_state.set_camera_with_region_origin).was_called_with(match.ref(state))
-
- assert.spy(map).was_called(1)
-
- assert.spy(map).was_called_with(0, 0, 0, 0, map_region_tile_width, map_region_tile_height, sprite_masks.foreground)
- end)
-
end) -- (with tile_test_data)
describe('extra render methods (no-crash only)', function ()
diff --git a/src/stage_intro/stage_intro_state.lua b/src/stage_intro/stage_intro_state.lua
index cd4dfdc9..28b98f68 100644
--- a/src/stage_intro/stage_intro_state.lua
+++ b/src/stage_intro/stage_intro_state.lua
@@ -22,7 +22,7 @@ function stage_intro_state:init()
self.curr_stage_id = 1
-- data
- self.curr_stage_data = stage_data.for_stage[1]
+ self.curr_stage_data = stage_data[1]
-- render
@@ -35,16 +35,18 @@ function stage_intro_state:init()
end
function stage_intro_state:on_enter()
- -- like the original stage_state, we need to have collision masks in builtin spritesheet,
- -- then load runtime spritesheet top portion
- -- of course, if we manage to isolate collision masks in their own spritesheet we could reload
- -- them (and reload the original builtin back) in some collision data method dedicated to this
- -- whole process (instead of relying on collision_data calling
- -- tile_collision_data.from_raw_tile_collision_data in outer scope, so early enough to be before
- -- loading runtime spritesheet...). Or we could have a custom intro cinematics that doesn't use physics
- -- at all and so no tile collision data is needed.
- local runtime_data_path = "data_stage"..self.curr_stage_id.."_runtime"..cartridge_ext
- reload(0x0, 0x0, 0x600, runtime_data_path)
+ -- like the original stage_state, we need to have collision masks in builtin spritesheet
+ -- in v3, the builtin contains *only* collision masks so we must reload the *full* spritesheet
+ -- for stage intro, hence reload memory length 0x2000
+ -- alternatively, like stage clear, we could have a custom intro cinematics that doesn't use physics
+ -- at all, and so no tile collision data is needed and we can just set intro spritesheet as built-in data
+ local runtime_data_path = "data_stage"..self.curr_stage_id.."_intro.p8"
+ reload(0x0, 0x0, 0x2000, runtime_data_path)
+
+ -- Reduced version of Sonic sprite copy, copied from stage_state:reload_runtime_data
+ -- We know Sonic doesn't spin dash during the intro, so storing the main sprites in general memory will be enough
+ -- Copy the first 8 rows = 4 double rows at once
+ reload(0x4b00, 0x400, 0x1000, "data_stage_sonic.p8")
self.camera:setup_for_stage(self.curr_stage_data)
@@ -76,14 +78,6 @@ end
-- setup
--- spawn the player character at the stage spawn location
--- (no unit test yet, will probably change for custom intro scene later)
-function stage_intro_state:spawn_player_char()
- local spawn_position = self.curr_stage_data.spawn_location:to_center_position()
- self.player_char = player_char()
- self.player_char:spawn_at(spawn_position)
-end
-
function stage_intro_state:update()
self.player_char:update()
self.camera:update()
@@ -91,6 +85,7 @@ function stage_intro_state:update()
end
function stage_intro_state:render()
+ -- no need to floor camera position like stage_state, since we don't really move on X
visual_stage.render_background(self.camera.position)
self:render_stage_elements()
self:render_overlay()
@@ -123,7 +118,7 @@ function stage_intro_state:show_stage_splash_async()
-- fade in
for i = 5, 0, -1 do
self.postproc.darkness = i
- yield_delay(7)
+ yield_delay_frames(6)
end
self.app:yield_delay_s(stage_intro_data.show_stage_splash_delay)
@@ -149,7 +144,7 @@ function stage_intro_state:show_stage_splash_async()
ui_animation.move_drawables_on_coord_async("x", {zone_rectangle, zone_label}, {0, 1}, 128, 41, 14)
-- keep zone displayed for a moment
- yield_delay(102)
+ yield_delay_frames(101)
-- make banner exit to the top
ui_animation.move_drawables_on_coord_async("y", {banner, banner_text}, {0, 89}, 0, -106, 8)
diff --git a/src/stage_intro/stage_intro_state_utest.lua b/src/stage_intro/stage_intro_state_utest.lua
index d29b1c05..d0ac6b46 100644
--- a/src/stage_intro/stage_intro_state_utest.lua
+++ b/src/stage_intro/stage_intro_state_utest.lua
@@ -59,7 +59,7 @@ describe('stage_intro_state', function ()
it('should initialize members', function ()
assert.are_same({
':stage_intro',
- stage_data.for_stage[1],
+ stage_data[1],
camera_class(),
overlay(),
postprocess(),
@@ -111,11 +111,12 @@ describe('stage_intro_state', function ()
assert.are_equal(vector(0, 1), state.loaded_map_region_coords)
end)
- it('should call reload for runtime data and stage1, map 01 (hardcoded)', function ()
+ it('should call reload for stage tiles, Sonic main sprites (general memory storage) and stage1, map 01 (hardcoded)', function ()
state:on_enter()
- assert.spy(reload).was_called(2)
- assert.spy(reload).was_called_with(0x0, 0x0, 0x600, "data_stage1_runtime.p8")
+ assert.spy(reload).was_called(3)
+ assert.spy(reload).was_called_with(0x0, 0x0, 0x2000, "data_stage1_intro.p8")
+ assert.spy(reload).was_called_with(0x4b00, 0x400, 0x1000, "data_stage_sonic.p8")
assert.spy(reload).was_called_with(0x2000, 0x2000, 0x1000, "data_stage1_01.p8")
end)
diff --git a/src/test/bustedhelper_attract_mode.lua b/src/test/bustedhelper_attract_mode.lua
new file mode 100644
index 00000000..360ede1b
--- /dev/null
+++ b/src/test/bustedhelper_attract_mode.lua
@@ -0,0 +1,10 @@
+-- engine bustedhelper equivalent for game project
+-- see bustedhelper_ingame.lua for explanations
+-- attract_mode being the closest to ingame cartridge, we have the same content
+-- except we require common_attract_mode and removed uncommented log activation code
+-- Usage:
+-- in your game utests, always require("test/bustedhelper_attract_mode") at the top
+-- instead of "engine/test/bustedhelper"
+require("engine/test/bustedhelper")
+require("common_attract_mode")
+require("resources/visual_ingame_addon")
diff --git a/src/test_data/tile_representation.lua b/src/test_data/tile_representation.lua
index 354bcaeb..c4b0c0de 100644
--- a/src/test_data/tile_representation.lua
+++ b/src/test_data/tile_representation.lua
@@ -7,7 +7,7 @@ local tile_repr = {
no_tile_id = 0,
full_tile_id = 29,
flat_high_tile_left_id = 26,
- flat_high_tile_id = 27, -- TODO: use 2, it's the same
+ flat_high_tile_id = 2,
half_tile_id = 4,
flat_low_tile_id = 6,
bottom_right_quarter_tile_id = 44, -- test only
@@ -15,7 +15,9 @@ local tile_repr = {
asc_slope_22_upper_level_id = 43, -- test only
asc_slope_45_id = 21,
desc_slope_45_id = 16,
- desc_slope_2px_id = 1, -- low slope descending every 4px, from height 7 to 6, 2px total on connection
+ desc_slope_2px_id = 1, -- low slope descending every 4px, from height 7 to 6, 2px total on connection
+ desc_slope_2px_last_id = 7, -- last part of low slope descending every 4px, from height 1 to 0, the last columns being empty
+ desc_slope_4px_last_id_loop_variant = 164, -- (loop variant) last part of medium slope descending every 2px, from height 3 to 0, the last columns being empty
-- because of the new convention of placing special sprite flags on visual tiles,
-- for meaningful tests we separate both tiles and check that flags are verified
-- on the right sprites. tilemap testing loop functionality should place the visual
@@ -39,7 +41,10 @@ local tile_repr = {
visual_loop_bottomright = 100,
mask_loop_bottomright = 20,
visual_loop_bottomright_steepest = 102,
- spring_up_repr_tile_id = 74, -- add 1 to get right, must match value in visual
+ spring_up_repr_tile_id = 74, -- add 1 to get right, must match value in visual
+ spring_right_mask_repr_tile_id = 31, -- added to get collision 2px away from spring right
+ -- we don't need to test the spring functionality, hence just mask
+ -- (actual spring right has id 173 in game)
grass_top_decoration1 = 76, -- no collider, just to test foreground
oneway_platform_left = 35, -- left side of one-way platform top part
}
@@ -68,6 +73,7 @@ tile_repr.tile_symbol_to_ids = {
['i'] = tile_repr.visual_loop_bottomright_steepest,
['s'] = tile_repr.spring_up_repr_tile_id,
['S'] = tile_repr.spring_up_repr_tile_id + 1,
+ [']'] = tile_repr.spring_right_mask_repr_tile_id,
['o'] = tile_repr.oneway_platform_left,
}
diff --git a/src/test_data/tile_test_data.lua b/src/test_data/tile_test_data.lua
index e7bfa1ac..933ce5c3 100644
--- a/src/test_data/tile_test_data.lua
+++ b/src/test_data/tile_test_data.lua
@@ -8,9 +8,10 @@ local tile_collision_data = require("data/tile_collision_data")
local stub = require("luassert.stub")
local tile_repr = require("test_data/tile_representation")
--- some tiles are defined in visual_ingame_addon for use in real game, but they are not in tile_representation.lua
+-- some tiles are defined in visual_ingame_numerical_data for use in real game, but they are not in tile_representation.lua
-- to avoid redundancy or because we didn't need them in itests yet
-local visual = require("resources/visual_common")
+visual_ingame_data = require("resources/visual_ingame_numerical_data")
+
-- we should require ingameadd-on in main
local mock_raw_tile_collision_data = {
@@ -27,12 +28,15 @@ local mock_raw_tile_collision_data = {
[tile_repr.flat_high_tile_id] = {tile_repr.flat_high_tile_id, {6, 6, 6, 6, 6, 6, 6, 6}, {0, 0, 8, 8, 8, 8, 8, 8}, atan2(8, 0)},
[tile_repr.half_tile_id] = {tile_repr.half_tile_id, {4, 4, 4, 4, 4, 4, 4, 4}, {0, 0, 0, 0, 8, 8, 8, 8}, atan2(8, 0)},
[tile_repr.flat_low_tile_id] = {tile_repr.flat_low_tile_id, {2, 2, 2, 2, 2, 2, 2, 2}, {0, 0, 0, 0, 0, 0, 8, 8}, atan2(8, 0)},
+ -- kept for testing; there is no *longer* edge so by convention we pick an angle so the interior is down... it won't detect reverse collisions coming horizontally
[tile_repr.bottom_right_quarter_tile_id] = {tile_repr.bottom_right_quarter_tile_id, {0, 0, 0, 0, 4, 4, 4, 4}, {0, 0, 0, 0, 4, 4, 4, 4}, atan2(8, 0)},
[tile_repr.asc_slope_22_id] = {tile_repr.asc_slope_22_id, {2, 2, 3, 3, 4, 4, 5, 5}, {0, 0, 0, 2, 4, 6, 8, 8}, 0.0625},
[tile_repr.asc_slope_22_upper_level_id] = {tile_repr.asc_slope_22_upper_level_id, {5, 5, 6, 6, 7, 7, 8, 8}, {2, 4, 6, 8, 8, 8, 8, 8}, atan2(8, -4)},
[tile_repr.asc_slope_45_id] = {tile_repr.asc_slope_45_id, {1, 2, 3, 4, 5, 6, 7, 8}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, -8)},
[tile_repr.desc_slope_45_id] = {tile_repr.desc_slope_45_id, {8, 7, 6, 5, 4, 3, 2, 1}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, 8)},
[tile_repr.desc_slope_2px_id] = {tile_repr.desc_slope_2px_id, {7, 7, 7, 7, 6, 6, 6, 6}, {0, 4, 8, 8, 8, 8, 8, 8}, atan2(8, 2)},
+ [tile_repr.desc_slope_2px_last_id] = {tile_repr.desc_slope_2px_last_id, {1, 1, 1, 1, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 4}, atan2(8, 2), true},
+ [tile_repr.desc_slope_4px_last_id_loop_variant] = {tile_repr.desc_slope_4px_last_id_loop_variant, {3, 3, 2, 2, 1, 1, 0, 0}, {0, 0, 0, 0, 0, 2, 4, 6}, atan2(8, 4), true},
[tile_repr.visual_topleft_45] = {tile_repr.mask_topleft_45, {8, 7, 6, 5, 4, 3, 2, 1}, {8, 7, 6, 5, 4, 3, 2, 1}, atan2(-8, 8)},
[tile_repr.visual_loop_topleft] = {tile_repr.mask_loop_topleft, {8, 7, 6, 6, 5, 4, 4, 3}, {8, 8, 8, 7, 5, 4, 2, 1}, atan2(-8, 5)},
[tile_repr.visual_loop_toptopleft] = {tile_repr.mask_loop_toptopleft, {3, 2, 2, 1, 1, 0, 0, 0}, {5, 3, 1, 0, 0, 0, 0, 0}, atan2(-8, 3)},
@@ -47,7 +51,8 @@ local mock_raw_tile_collision_data = {
-- after this table definition, just define mock_raw_tile_collision_data[mask_X] = mock_raw_tile_collision_data[visual_X] for X: loop tile locations
[tile_repr.spring_up_repr_tile_id] = {tile_repr.flat_high_tile_left_id, {0, 0, 0, 0, 6, 6, 6, 6}, {0, 0, 4, 4, 4, 4, 4, 4}, atan2(8, 0)}, -- copied from flat_high_tile_left_id
[tile_repr.spring_up_repr_tile_id + 1] = {tile_repr.flat_high_tile_id, {6, 6, 6, 6, 6, 6, 6, 6}, {0, 0, 8, 8, 8, 8, 8, 8}, atan2(8, 0)}, -- copied from flat_high_tile_id
- [visual.launch_ramp_last_tile_id] = {tile_repr.mask_loop_bottomright, {3, 4, 4, 5, 6, 6, 7, 8}, {1, 2, 4, 5, 7, 8, 8, 8}, atan2(8, -5)}, -- copied from visual_loop_bottomright
+ [tile_repr.spring_right_mask_repr_tile_id] = {tile_repr.spring_right_mask_repr_tile_id, {8, 8, 8, 8, 8, 8, 0, 0}, {6, 6, 6, 6, 6, 6, 6, 6}, atan2(0, 8)},
+ [visual_ingame_data.launch_ramp_last_tile_id] = {tile_repr.mask_loop_bottomright, {3, 4, 4, 5, 6, 6, 7, 8}, {1, 2, 4, 5, 7, 8, 8, 8}, atan2(8, -5)}, -- copied from visual_loop_bottomright
[tile_repr.oneway_platform_left] = {tile_repr.oneway_platform_left, {8, 8, 8, 8, 8, 8, 8, 8}, {8, 8, 8, 8, 8, 8, 8, 8}, atan2(8, 0)},
}
@@ -63,7 +68,8 @@ local mock_tile_collision_data = transform(mock_raw_tile_collision_data, functio
raw_data[3],
slope_angle,
interior_v,
- interior_h
+ interior_h,
+ raw_data[5]
)
end)
@@ -85,6 +91,8 @@ function tile_test_data.setup()
fset(tile_repr.asc_slope_45_id, sprite_masks.collision + sprite_masks.midground) -- ascending slope 45
fset(tile_repr.desc_slope_45_id, sprite_masks.collision + sprite_masks.midground) -- descending slope 45
fset(tile_repr.desc_slope_2px_id, sprite_masks.collision + sprite_masks.midground) -- descending slope every 4px, from height 7 to 6
+ fset(tile_repr.desc_slope_2px_last_id, sprite_masks.collision + sprite_masks.midground) -- descending slope every 4px, from height 1 to 0
+ fset(tile_repr.desc_slope_4px_last_id_loop_variant, sprite_masks.collision + sprite_masks.ignore_loop_layer + sprite_masks.midground) -- descending slope every 2px, from height 3 to 0
-- masks also have collision flag, but only useful to test
-- a non-loop proto curve tile with the same shape (as loop require visual tiles anyway)
@@ -118,9 +126,11 @@ function tile_test_data.setup()
-- spring
fset(tile_repr.spring_up_repr_tile_id, sprite_masks.collision + sprite_masks.spring + sprite_masks.midground)
fset(tile_repr.spring_up_repr_tile_id + 1, sprite_masks.collision + sprite_masks.spring + sprite_masks.midground)
+ -- spring mask
+ fset(tile_repr.spring_right_mask_repr_tile_id, sprite_masks.collision + sprite_masks.midground)
-- ramp (last tile is one-way)
- fset(visual.launch_ramp_last_tile_id, sprite_masks.collision + sprite_masks.oneway + sprite_masks.midground)
+ fset(visual_ingame_data.launch_ramp_last_tile_id, sprite_masks.collision + sprite_masks.oneway + sprite_masks.midground)
-- one-way platform
fset(tile_repr.oneway_platform_left, sprite_masks.collision + sprite_masks.oneway + sprite_masks.midground)
diff --git a/src/tests/headless_itests_utest.lua b/src/tests/headless_itests_utest.lua
index c4a3f39c..51d66df7 100644
--- a/src/tests/headless_itests_utest.lua
+++ b/src/tests/headless_itests_utest.lua
@@ -14,6 +14,7 @@ if cartridge_suffix == 'ignore' then
return
end
+-- Make sure to create a bustedhelper_[cartridge] for each cartridge
require("test/bustedhelper_"..cartridge_suffix)
require("engine/test/headless_itest")
@@ -28,11 +29,15 @@ local picosonic_app = require("application/picosonic_app_"..cartridge_suffix)
local app = picosonic_app()
-- define initial gamestate based on cartridge
+-- #cartridge
+-- (no itests for attract_mode)
local initial_gamestate
if cartridge_suffix == 'titlemenu' then
initial_gamestate = ':titlemenu'
elseif cartridge_suffix == 'ingame' then
initial_gamestate = ':stage'
+elseif cartridge_suffix == 'attract_mode' then
+ initial_gamestate = ':stage'
elseif cartridge_suffix == 'stage_clear' then
initial_gamestate = ':stage_clear'
elseif cartridge_suffix == 'stage_intro' then
@@ -59,7 +64,7 @@ logging.logger.active_categories = {
['itest'] = true,
-- ['log'] = true,
-- ['ui'] = true,
- ['frame'] = true,
+ -- ['frame'] = true,
-- ['trace'] = true,
-- ['trace2'] = true,
['spring'] = true,
diff --git a/src/utests/utestdata.lua b/src/utests/utestdata.lua
index b9127d19..711879a0 100644
--- a/src/utests/utestdata.lua
+++ b/src/utests/utestdata.lua
@@ -1,4 +1,5 @@
require("engine/test/p8utest")
+local pc_sprite_data = require("data/playercharacter_sprite_data")
local tile_collision_data = require("data/tile_collision_data")
-- data to test
@@ -6,7 +7,7 @@ local collision_data = require("data/collision_data")
-- this one is not checked although we could verify that sprites are not empty or something
-- but it's useful to check definition sanity (e.g. animation with 0 sprites, in particular
-- after minification if keys are not protected with ["key"] syntax)
-local pc_data = require("data/playercharacter_data")
+local pc_numerical_data = require("data/playercharacter_numerical_data")
check('sprite_id_location(1, 2) (loop top-left) should have collision flag set', function (utest_name)
local sprite_id = sprite_id_location(1, 2):to_sprite_id()
@@ -38,5 +39,5 @@ check('tile_collision_data.read_height_array on sprite_id_location(4, 0) return
end)
check('sonic_animated_sprite_data_table preserved key "idle"', function (utest_name)
- assert_log(utest_name, pc_data.sonic_animated_sprite_data_table["idle"] ~= nil, 'Expected pc_data.sonic_animated_sprite_data_table["idle"] to not be nil')
+ assert_log(utest_name, pc_sprite_data.sonic_animated_sprite_data_table["idle"] ~= nil, 'Expected pc_sprite_data.sonic_animated_sprite_data_table["idle"] to not be nil')
end)