From 335968e14475834bad4382f8504b5600e4cec73f Mon Sep 17 00:00:00 2001 From: huulong Date: Tue, 14 Jul 2020 17:20:03 +0100 Subject: [PATCH 001/112] [SUBMODULE] Update pico-boots (better script errors) Marked replace_strings as LEGACY --- pico-boots | 2 +- prebuild/replace_strings.py | 3 +++ prebuild/test_replace_strings.py | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pico-boots b/pico-boots index d93d71ca..82bc5c87 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit d93d71ca3b9d827e22c424406c0b484d911e5dc7 +Subproject commit 82bc5c8794bb0712a4693baed7adc8eeac06d60e diff --git a/prebuild/replace_strings.py b/prebuild/replace_strings.py index ae5596c4..79d56c98 100755 --- a/prebuild/replace_strings.py +++ b/prebuild/replace_strings.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3.6 # -*- coding: utf-8 -*- + +# LEGACY: use preprocess.py in pico-boots now + import argparse import logging import os diff --git a/prebuild/test_replace_strings.py b/prebuild/test_replace_strings.py index 3ed835cf..ffb5edd2 100644 --- a/prebuild/test_replace_strings.py +++ b/prebuild/test_replace_strings.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +# LEGACY: use test_preprocess.py to test preprocess.py in pico-boots now + import unittest from . import replace_strings @@ -94,5 +96,5 @@ def test_replace_all_strings_in_dir(self): self.assertEqual(f.read(), 'require("itest_character")\n⬅️ or ➡️\nand 🅾️\nprint("press 🅾️")') if __name__ == '__main__': - logging.basicConfig(level=logging.ERROR) + logging.basicConfig(level=logging.CRITICAL) unittest.main() From 2d823c5da7f889e4e5093cebb28953d8a8f50aa2 Mon Sep 17 00:00:00 2001 From: huulong Date: Tue, 14 Jul 2020 17:23:21 +0100 Subject: [PATCH 002/112] [SCRIPTS] chmod +x export_cartridge_release.sh and install_cartridge_linux.sh --- export_cartridge_release.sh | 0 install_cartridge_linux.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 export_cartridge_release.sh mode change 100644 => 100755 install_cartridge_linux.sh diff --git a/export_cartridge_release.sh b/export_cartridge_release.sh old mode 100644 new mode 100755 diff --git a/install_cartridge_linux.sh b/install_cartridge_linux.sh old mode 100644 new mode 100755 From 013930bc7a41e1f36051e800b4598eb4fd246fbc Mon Sep 17 00:00:00 2001 From: huulong Date: Tue, 14 Jul 2020 18:40:37 +0100 Subject: [PATCH 003/112] [README] Added PICO-8 compatibility note (v0.2.0i and v0.2.1b) --- README.md | 4 ++++ pico-boots | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 24a8b4ec..475d4e91 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ This is a fan game distributed for free and is not endorsed by Sega Games Co., L It is currently under development. +## Compatibility + +Works with PICO-8 0.2.0i and 0.2.1b. + ## Features Version: 3.0 diff --git a/pico-boots b/pico-boots index 82bc5c87..c84f006b 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 82bc5c8794bb0712a4693baed7adc8eeac06d60e +Subproject commit c84f006b96f09b450b123e9331d520f240639dde From e0375e079db2c750fac5e7316e28b39f20976e35 Mon Sep 17 00:00:00 2001 From: huulong Date: Tue, 14 Jul 2020 18:40:49 +0100 Subject: [PATCH 004/112] [CHANGELOG] Added CHANGELOG so far (until v3.0) --- CHANGELOG.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..1667e754 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,93 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [3.0] - 2020-07-11 +### Added +- Game: split airborne state into falling and air_spin to only play Spin animation on jump +- Game: when going airborne/grounded, adapt height and center position so Sonic hits the ceiling when we expect by looking at the sprite +- Test: convert itests to new DSL system + +### Changed +- Project: extracted engine as submodule pico-boots, adapted build pipeline +- Game: set pink as transparent color for Sonic sprites to match new… +- Game: preserve last animation (including playback speed) when falling +- Game: only allow ceiling detection during descending motion if abs(vel.x) > abs(vel.y) +- Test: improved itests + +## [2.3-sprite-anim] - 2019-05-16 +### Added +- Engine: sprite animation system +- Game: press X to toggle debug motion +- Game: added air motion block with wall, ceiling and landing +- Game: character running animation +- Game: character can run on slopes +- Test: added itest for running up a slope +- Test: convert itests to new DSL system + +### Changed +- Project: split engine and game folders properly +- Engine: misc logging fixes +- Game: clamp ground speed on acceleration +- Game: fixed sticky jump input during jump +- Game: fixed air motion pixel flooring system + +## [2.2] - 2018-10-31 +### Added +- Game: added Air Spin sprite used when airborne +- Test: completed 100% coverage on player character + +### Changed +- Game: fixed ground speed accumulating during wall block +- Game: character is blocked by ceiling and diagonal tiles +- Game: cleaner wall block by cutting subpixels + +## [2.1] - 2018-10-27 +### Added +- Game: character snaps to lower ground when walking +- Game: character falls when walking off a cliff +- Game: character can jump with air control (X axis motion) +- Game: apply artificial gravity after jump interrupt, allow hop on 1-frame jumps + +## [2.0-flat-ground] - 2018-09-09 +### Added +- Game: character can walk on flat ground +- Test: added support for #solo for headless itests + +## [2.0-land] - 2018-09-08 +### Added +- Game: blue sky background +- Game: added gravity, character can fall and land on ground +- Test: integration (simulation) test system aka "itests" + +### Changed +- Engine: improved logging, classes, math + +## [1.0] - 2018-07-06 +### Added +- Engine: application: constants, flow with gamestates +- Engine: core modules: class, coroutine, helper, math +- Engine: debug tools: codetuner, logging, profiler using WTK +- Engine: input: mouse input and button IDs +- Engine: render: color constants and sprite render +- Engine: UI: render mouse and overlay system +- Engine: build: basic build pipeline with data.p8 and post-build replace strings script +- Game: gamestates title menu, in-game, credits (empty) +- Game: menu: simple titlemenu to start the debug stage +- Game: in-game: stage shows title on start, plays BGM, has goal +- 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/v3.0...HEAD +[3.0]: https://github.com/hsandt/sonic-pico8/compare/v2.3-sprite-anim...v3.0 +[2.3-sprite-anim]: https://github.com/hsandt/sonic-pico8/compare/v2.2...v2.3-sprite-anim +[2.2]: https://github.com/hsandt/sonic-pico8/compare/v2.1...v2.2 +[2.1]: https://github.com/hsandt/sonic-pico8/compare/v2.0-flat-ground...v2.1 +[2.0-flat-ground]: https://github.com/hsandt/sonic-pico8/compare/v2.0-land...v2.0-flat-ground +[2.0-land]: https://github.com/hsandt/sonic-pico8/compare/v1.0-framework...v2.0-land +[1.0-framework]: https://github.com/hsandt/sonic-pico8/releases/tag/v1.0-framework From a2618381aaa7be18dd25140580ce5efabade7562 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Thu, 16 Jul 2020 15:05:55 +0200 Subject: [PATCH 005/112] [PLAYER CHARACTER] Fixed orientation changing based on ground speed only instead of intention (causing character to glitch when falling passively from a slope and trying to walk back up) Now, character simply changes orientation when accelerating toward a new direction, and when turning around at the end of a deceleration --- src/ingame/playercharacter.lua | 33 ++++++----- src/ingame/playercharacter_utest.lua | 88 ++++++++++++++-------------- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 0edd926a..48f2dc9c 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -59,7 +59,7 @@ local player_char = new_class() -- control_mode control_modes control mode: human (default) or ai -- motion_mode (cheat) motion_modes motion mode: platformer (under gravity) or debug (fly around) -- motion_state motion_states motion state (platformer mode only) --- horizontal_dir horizontal_dirs direction faced by character +-- orientation horizontal_dirs direction faced by character -- position vector current position (character center "between" pixels) -- ground_speed float current speed along the ground (~px/frame) -- velocity vector current velocity in platformer mode (px/frame) @@ -89,7 +89,7 @@ function player_char:_setup() self.motion_mode = motion_modes.platformer --#endif self.motion_state = motion_states.grounded - self.horizontal_dir = horizontal_dirs.right + self.orientation = horizontal_dirs.right self.position = vector.zero() self.ground_speed = 0. @@ -288,7 +288,7 @@ function player_char:_get_prioritized_dir() return signed_speed_to_dir(self.velocity.x) end end - return self.horizontal_dir + return self.orientation end -- return the position of the ground sensor in horizontal_dir when the character center is at center_position @@ -492,22 +492,23 @@ function player_char:_update_ground_speed_by_intention() if self.ground_speed == 0 or sgn(self.ground_speed) == sgn(self.move_intention.x) then -- accelerate self.ground_speed = self.ground_speed + self.move_intention.x * pc_data.ground_accel_frame2 + -- face move direction if not already + self.orientation = signed_speed_to_dir(self.move_intention.x) else -- decelerate self.ground_speed = self.ground_speed + self.move_intention.x * pc_data.ground_decel_frame2 - -- if speed must switch sign this frame, clamp it by ground accel in absolute value to prevent exploit of - -- moving back 1 frame then forward to gain an initial speed boost (mentioned in Sonic Physics Guide as a bug) + -- check if speed has switched sign this frame, i.e. character has turned around local has_changed_sign = self.ground_speed ~= 0 and sgn(self.ground_speed) == sgn(self.move_intention.x) - if has_changed_sign and abs(self.ground_speed) > pc_data.ground_accel_frame2 then - self.ground_speed = sgn(self.ground_speed) * pc_data.ground_accel_frame2 - end - end - if self.ground_speed ~= 0 then - -- always update direction when player tries to move and the character is moving after update - -- this is useful even when move intention x has same sign as ground speed, - -- as the character may be running backward after failing to run a steep slope up - self.horizontal_dir = signed_speed_to_dir(self.ground_speed) + if has_changed_sign then + -- clamp speed after turn around by ground accel in absolute value to prevent exploit of + -- moving back 1 frame then forward to gain an initial speed boost (mentioned in Sonic Physics Guide as a bug) + if abs(self.ground_speed) > pc_data.ground_accel_frame2 then + self.ground_speed = sgn(self.ground_speed) * pc_data.ground_accel_frame2 + end + -- turn around + self.orientation = signed_speed_to_dir(self.move_intention.x) + end end elseif self.ground_speed ~= 0 then @@ -823,7 +824,7 @@ function player_char:_update_platformer_motion_airborne() self.velocity.x = self.velocity.x + self.move_intention.x * pc_data.air_accel_x_frame2 -- in the air, apply intended motion to direction immediately - self.horizontal_dir = signed_speed_to_dir(self.move_intention.x) + self.orientation = signed_speed_to_dir(self.move_intention.x) end -- apply air motion @@ -1134,7 +1135,7 @@ end -- render the player character sprite at its current position function player_char:render() - local flip_x = self.horizontal_dir == horizontal_dirs.left + local flip_x = self.orientation == horizontal_dirs.left self.anim_spr:render(self.position, flip_x) end diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 529137fa..506c0b87 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -150,7 +150,7 @@ describe('player_char', function () pc.control_mode, pc.motion_mode, pc.motion_state, - pc.horizontal_dir, + pc.orientation, pc.position, pc.ground_speed, @@ -833,12 +833,12 @@ describe('player_char', function () end) it('should return left when character is not moving and facing left', function () - pc.horizontal_dir = horizontal_dirs.left + pc.orientation = horizontal_dirs.left assert.are_equal(horizontal_dirs.left, pc:_get_prioritized_dir()) end) it('should return right when character is not moving and facing right', function () - pc.horizontal_dir = horizontal_dirs.right + pc.orientation = horizontal_dirs.right assert.are_equal(horizontal_dirs.right, pc:_get_prioritized_dir()) end) @@ -1930,153 +1930,153 @@ describe('player_char', function () describe('_update_ground_speed_by_intention', function () 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.horizontal_dir = horizontal_dirs.left + pc.orientation = horizontal_dirs.left pc.move_intention.x = 1 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.right, pc_data.ground_accel_frame2}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) end) it('should accelerate and set direction when character is facing left, has ground speed > 0 and move intention x > 0', function () - pc.horizontal_dir = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward + pc.orientation = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward e.g. after hitting a spring pc.ground_speed = 1.5 pc.move_intention.x = 1 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.right, 1.5 + pc_data.ground_accel_frame2}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, 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.horizontal_dir = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward + pc.orientation = horizontal_dirs.left -- rare to oppose ground speed sense, but possible when running backward e.g. after hitting a spring pc.ground_speed = -1.5 pc.move_intention.x = -1 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.left, -1.5 - pc_data.ground_accel_frame2}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) 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.horizontal_dir = horizontal_dirs.right + pc.orientation = horizontal_dirs.right pc.ground_speed = 1.5 pc.move_intention.x = -1 pc:_update_ground_speed_by_intention() -- ground_decel_frame2 = 0.25, subtract it from ground_speed assert.are_same({horizontal_dirs.right, 1.25}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) end) it('should decelerate and stop exactly at speed 0, preserving direction, when character has ground speed = ground accel * 1 frame and move intention x < 0', function () - pc.horizontal_dir = horizontal_dirs.right + pc.orientation = horizontal_dirs.right pc.ground_speed = 0.25 pc.move_intention.x = -1 pc:_update_ground_speed_by_intention() -- ground_decel_frame2 = 0.25, subtract it from ground_speed assert.are_same({horizontal_dirs.right, 0}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) end) -- bugfix history: -- _ missing tests that check the change of sign of ground speed - it('should decelerate and change sign and direction when character is facing right, '.. + it('should decelerate, turn 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.horizontal_dir = horizontal_dirs.right + 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_speed_by_intention() - assert.are_equal(horizontal_dirs.left, pc.horizontal_dir) + assert.are_equal(horizontal_dirs.left, pc.orientation) assert.is_true(almost_eq_with_message(-0.01, pc.ground_speed, 1e-16)) end) - it('should change direction, decelerate and clamp to the max ground speed in the opposite sign '.. + it('should decelerate, turn 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.horizontal_dir = horizontal_dirs.right + 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_speed_by_intention() assert.are_same({horizontal_dirs.left, -pc_data.ground_accel_frame2}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) end) - -- tests below seem symmetrical, but as a twist we have the character running backward + -- tests below seem symmetrical, but as a twist we have the character running backward (e.g. after being hit by a horizontal spring) -- so he's facing the opposite direction of the run, so we can test direction update - it('should decelerate keeping same sign when character has high ground speed < 0 and move intention x > 0', function () - pc.horizontal_dir = horizontal_dirs.right + it('should decelerate keeping same sign and orientation when character is facing right, has high 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_speed_by_intention() - assert.are_same({horizontal_dirs.left, -1.25}, - {pc.horizontal_dir, pc.ground_speed}) + assert.are_same({horizontal_dirs.right, -1.25}, + {pc.orientation, pc.ground_speed}) end) 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.horizontal_dir = horizontal_dirs.right + 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_speed_by_intention() - assert.are_equal(horizontal_dirs.right, pc.horizontal_dir) + assert.are_equal(horizontal_dirs.right, pc.orientation) assert.is_true(almost_eq_with_message(0.01, pc.ground_speed, 1e-16)) 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.horizontal_dir = horizontal_dirs.right + 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_speed_by_intention() assert.are_same({horizontal_dirs.right, pc_data.ground_accel_frame2}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) end) it('should apply friction and preserve direction when character has ground speed > 0 and move intention x is 0', function () - pc.horizontal_dir = horizontal_dirs.right + pc.orientation = horizontal_dirs.right pc.ground_speed = 1.5 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) 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.horizontal_dir = horizontal_dirs.right + pc.orientation = horizontal_dirs.right -- must be < friction pc.ground_speed = 0.01 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.right, 0}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) end) -- tests below seem symmetrical, but the character is actually running backward it('should apply friction and preserive direction when character has ground speed < 0 and move intention x is 0', function () - pc.horizontal_dir = horizontal_dirs.right + pc.orientation = horizontal_dirs.right pc.ground_speed = -1.5 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.right, -1.5 + pc_data.ground_friction_frame2}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) end) -- 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.horizontal_dir = horizontal_dirs.right + pc.orientation = horizontal_dirs.right -- must be < friction in abs pc.ground_speed = -0.01 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.right, 0}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) end) it('should not change ground speed nor direction when ground speed is 0 and move intention x is 0', function () - pc.horizontal_dir = horizontal_dirs.left + pc.orientation = horizontal_dirs.left pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.left, 0}, - {pc.horizontal_dir, pc.ground_speed}) + {pc.orientation, pc.ground_speed}) end) end) -- _update_ground_speed_by_intention @@ -3297,23 +3297,23 @@ describe('player_char', function () end) it('should set horizontal direction to intended motion direction: left', function () - pc.horizontal_dir = horizontal_dirs.right + 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.horizontal_dir) + assert.are_equal(horizontal_dirs.left, pc.orientation) end) it('should set horizontal direction to intended motion direction: right', function () - pc.horizontal_dir = horizontal_dirs.left + 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.horizontal_dir) + assert.are_equal(horizontal_dirs.right, pc.orientation) end) -- bugfix history: @@ -4250,7 +4250,7 @@ describe('player_char', function () it('(when character is facing left) should call render on sonic sprite data: idle with the character\'s position, flipped x', function () pc.position = vector(12, 8) - pc.horizontal_dir = horizontal_dirs.left + pc.orientation = horizontal_dirs.left pc:render() @@ -4260,7 +4260,7 @@ describe('player_char', function () it('(when character is facing right) should call render on sonic sprite data: idle with the character\'s position, not flipped x', function () pc.position = vector(12, 8) - pc.horizontal_dir = horizontal_dirs.right + pc.orientation = horizontal_dirs.right pc:render() From 945d976a617b77624937adc208daf6a6fa6c6df2 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Thu, 16 Jul 2020 16:17:36 +0200 Subject: [PATCH 006/112] [VISUAL] Fixed cursor pink background in debug by adding transparent color --- pico-boots | 2 +- src/resources/visual.lua | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pico-boots b/pico-boots index c84f006b..5933662a 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit c84f006b96f09b450b123e9331d520f240639dde +Subproject commit 5933662a26892c769ecbbb6e1d7b1a27e65f4b81 diff --git a/src/resources/visual.lua b/src/resources/visual.lua index b8ad0526..fd6c957c 100644 --- a/src/resources/visual.lua +++ b/src/resources/visual.lua @@ -1,10 +1,11 @@ +require("engine/render/color") local sprite_data = require("engine/render/sprite_data") local visual = {} local sprite_data_t = { --#if mouse - cursor = sprite_data(sprite_id_location(1, 0)) + cursor = sprite_data(sprite_id_location(1, 0), nil, nil, colors.pink) --#endif } From 90d393a40a5344b6ce7ade61b01f24355a878298 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Thu, 16 Jul 2020 23:04:00 +0200 Subject: [PATCH 007/112] [PLAYER CHARACTER] Added 3 original features to improve motion on slope - Reduced Deceleration on Descending Slope - No Friction on Steep Descending Slope - Progressive Ascending Slope Factor Animation: set minimum playback speed for Running animation --- pico-boots | 2 +- src/data/playercharacter_data.lua | 20 ++++ src/ingame/playercharacter.lua | 82 +++++++++++++--- src/ingame/playercharacter_utest.lua | 140 ++++++++++++++++++++++++--- src/main.lua | 3 +- 5 files changed, 219 insertions(+), 28 deletions(-) diff --git a/pico-boots b/pico-boots index 5933662a..fe3d86d7 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 5933662a26892c769ecbbb6e1d7b1a27e65f4b81 +Subproject commit fe3d86d7db4b32deb4a00e9c8ae92e899d80a413 diff --git a/src/data/playercharacter_data.lua b/src/data/playercharacter_data.lua index 3389e09d..f8fee95d 100644 --- a/src/data/playercharacter_data.lua +++ b/src/data/playercharacter_data.lua @@ -12,15 +12,30 @@ local playercharacter_data = { -- ground active deceleration (brake) (px/frame^2) ground_decel_frame2 = 0.25, -- 16/64 + -- Original feature (not in SPG): Reduced Deceleration on Descending Slope + -- ground active deceleration factor on descending slope ([0-1]) + ground_decel_descending_slope_factor = 0.5, + -- ground friction (passive deceleration) (px/frame^2) ground_friction_frame2 = 0.0234375, -- 1.5/64 + -- Original feature (not in SPG): No Friction on Steep Descending Slope + -- max slope angle on which friction is applied ([0-0.25], but we recommend more than 22.5 degrees i.e. 0.0625) + ground_friction_max_slope_angle = 0.075, -- 27/360 + -- gravity acceleration (px/frame^2) gravity_frame2 = 0.109375, -- 7/64 -- slope accel acceleration factor (px/frame^2), to multiply by sin(angle) slope_accel_factor_frame2 = 0.0625, -- 7/64 + -- derived data: the slope angle for which ground friction is exactly opposed to slope factor + -- is 22.02 degrees ~ 0.061 angle/360 ratio (PICO-8 unit) + + -- Original feature (not in SPG): Progressive Ascending Slope Factor + -- time needed when ascending a slope before full slope factor is applied (s) + progressive_ascending_slope_duration = 0.5, + -- air acceleration on x axis (px/frames^2) air_accel_x_frame2 = 0.046875, -- 3/64 @@ -96,6 +111,11 @@ local playercharacter_data = { ["spin"] = sprite_data(sprite_id_location(0, 12), tile_vector(2, 2), vector(5, 5), colors.pink), }, + -- minimum playback speed for "run" animation, to avoid very slow animation + -- 5/16: the 5 counters the 5 duration frames of ["run"] below, 1/8 to represent max duration 8 in SPG:Animations + -- and an extra 1/2 because for some reason, SPG values make animations look too fast (as if durations were for 30FPS) + run_anim_min_play_speed = 0.3125 + } -- define animated sprite data in a second step, as it needs sprite data to be defined first diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 48f2dc9c..4e2b811b 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -65,6 +65,7 @@ local player_char = new_class() -- velocity vector current velocity in platformer mode (px/frame) -- debug_velocity vector current velocity in debug mode (m/s) -- slope_angle float slope angle of the current ground (clockwise turn ratio) +-- ascending_slope_time float time before applying full slope factor, when ascending a slope (s) -- move_intention vector current move intention (normalized) -- jump_intention bool current intention to start jump (consumed on jump) -- hold_jump_intention bool current intention to hold jump (always true when jump_intention is true) @@ -97,6 +98,7 @@ function player_char:_setup() self.debug_velocity = vector.zero() -- slope_angle starts at 0 instead of nil to match grounded state above (first spawn will set this anyway) self.slope_angle = 0. + self.ascending_slope_time = 0. self.move_intention = vector.zero() self.jump_intention = false @@ -450,7 +452,10 @@ function player_char:_update_platformer_motion_grounded() if self.ground_speed == 0 then self.anim_spr:play("idle") else - self.anim_spr:play("run", false, abs(self.ground_speed)) + -- for the run playback speed, we don't follow the SPG which uses flr(max(0, 8-abs(self.ground_speed))) + -- instead, we prefer the more organic approach of continuous playback speed + -- however, to simulat the max duration clamping, we use min playback speed clamping + self.anim_spr:play("run", false, max(pc_data.run_anim_min_play_speed, abs(self.ground_speed))) end end @@ -465,23 +470,49 @@ end -- update ground speed function player_char:_update_ground_speed() - -- we need to update ground speed by intention first so accel/decel is handled correctly - -- otherwise, when starting at speed 0 on an ascending slope, gravity changes speed to < 0 - -- and a right move intention would be handled as a decel, which is fast enough to climb up - -- even the highest slopes - -- FIXME: but that broke low slopes since we apply friction for nothing, - -- then the slope factor... - -- Instead, we need to synthetize by applying the slope factor in the background, - -- then the move intention but not based on the intermediate speed but the old speed (or better, the horizontal dir) + -- In order to process move intention and slope effect as independently as possible, + -- we process intention first, then compute slope contribution based on the ground speed + -- *before* intention was processed. + -- We could do the opposite, but since _update_ground_speed_by_intention relies on clamping, + -- it would be a bit harder (we'd need both previous_ground_speed and some + -- fictive_future_ground_speed = previous_ground_speed + ground_speed_delta, then compute + -- some clamped_ground_speed_delta and reapply it to the actual self.ground_speed) + local previous_ground_speed = self.ground_speed self:_update_ground_speed_by_intention() - self:_update_ground_speed_by_slope() + self:_update_ground_speed_by_slope(previous_ground_speed) self:_clamp_ground_speed() end -- update ground speed based on current slope -function player_char:_update_ground_speed_by_slope() +function player_char:_update_ground_speed_by_slope(previous_ground_speed) + local is_ascending_slope = false + if self.slope_angle ~= 0 then - self.ground_speed = self.ground_speed - pc_data.slope_accel_factor_frame2 * sin(self.slope_angle) + -- Original feature (not in SPG): Progressive Ascending Slope Factor + -- If character is ascending a slope, do not apply the full slope factor immediately. + -- Instead, linearly increase the applied slope factor from 0 to full over a given duration. + -- We use the ground speed before applying intention to avoid exploid of spamming + -- the left/right (ascending) input to restart the timer thanks to the ground speed + -- being slightly increased by the intention, but actually countered by slope accel in the same frame. + -- Effect: the character can completely cross steep but short slopes + -- Resolves: character was suddenly stopped by longer slopes when starting ascension with low momentum, + -- falling back to the flat ground behind, and repeating, causing a glitch-like oscillation + local ascending_slope_factor = 1 + if previous_ground_speed ~= 0 and sgn(previous_ground_speed) ~= sgn(self.slope_angle) then + is_ascending_slope = true + local ascending_slope_duration = pc_data.progressive_ascending_slope_duration + local progressive_ascending_slope_factor = 1 + -- increase tracking time every frame + self.ascending_slope_time = min(self.ascending_slope_time + delta_time60, ascending_slope_duration) + ascending_slope_factor = self.ascending_slope_time / ascending_slope_duration + end + + self.ground_speed = self.ground_speed - ascending_slope_factor * pc_data.slope_accel_factor_frame2 * sin(self.slope_angle) + end + + if not is_ascending_slope then + -- reset ascending slope time + self.ascending_slope_time = 0 end end @@ -495,8 +526,19 @@ function player_char:_update_ground_speed_by_intention() -- face move direction if not already self.orientation = signed_speed_to_dir(self.move_intention.x) else + -- Original feature (not in SPG): Reduced Deceleration on Descending Slope + -- Apply a fixed factor + -- Effect: a character descending a steep slope will take more time to brake than if + -- considering slope factor alone + -- Resolves: character descending a steep slope was braking and turning back too suddenly + local ground_decel_factor = 1 + if self.slope_angle ~= 0 and sgn(self.ground_speed) == sgn(self.slope_angle) then + -- character is trying to brake on a descending slope + ground_decel_factor = pc_data.ground_decel_descending_slope_factor + end + -- decelerate - self.ground_speed = self.ground_speed + self.move_intention.x * pc_data.ground_decel_frame2 + self.ground_speed = self.ground_speed + self.move_intention.x * ground_decel_factor * pc_data.ground_decel_frame2 -- check if speed has switched sign this frame, i.e. character has turned around local has_changed_sign = self.ground_speed ~= 0 and sgn(self.ground_speed) == sgn(self.move_intention.x) @@ -512,9 +554,19 @@ function player_char:_update_ground_speed_by_intention() end elseif self.ground_speed ~= 0 then - -- friction - self.ground_speed = sgn(self.ground_speed) * max(0, abs(self.ground_speed) - pc_data.ground_friction_frame2) + -- no move intention, character is passive + + -- Original feature (not in SPG): No Friction on Steep Descending Slope + -- Do not apply friction when character is descending a steep slope passively; + -- In other words, apply it only on flat ground, low slope and only steep slopes if ascending + -- Effect: the character will automatically run down a steep slope and accumulate acceleration downward + -- without friction + -- Resolves: the character was moving down a steep slope very slowly because of friction + if abs(self.slope_angle) <= pc_data.ground_friction_max_slope_angle or sgn(self.ground_speed) ~= sgn(self.slope_angle) then + self.ground_speed = sgn(self.ground_speed) * max(0, abs(self.ground_speed) - pc_data.ground_friction_frame2) + end end + end -- clamp ground speed to max diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 506c0b87..2e373400 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -134,11 +134,14 @@ describe('player_char', function () motion_modes.platformer, motion_states.grounded, horizontal_dirs.right, + vector.zero(), 0, vector.zero(), vector.zero(), 0, + 0, + vector.zero(), false, false, @@ -157,6 +160,7 @@ describe('player_char', function () pc.velocity, pc.debug_velocity, pc.slope_angle, + pc.ascending_slope_time, pc.move_intention, pc.jump_intention, @@ -1586,11 +1590,14 @@ describe('player_char', function () local check_jump_intention_stub local compute_ground_motion_result_mock + -- 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 () spy.on(animated_sprite, "play") update_ground_speed_mock = stub(player_char, "_update_ground_speed", function (self) - self.ground_speed = -2.5 -- use fractional speed to check that fractions are preserved + self.ground_speed = new_ground_speed end) enter_motion_state_stub = stub(player_char, "_enter_motion_state") check_jump_intention_stub = stub(player_char, "_check_jump_intention") @@ -1687,6 +1694,28 @@ describe('player_char', function () assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "run", false, 2.5) end) + describe('(_update_ground_speed sets ground speed to -pc_data.run_anim_min_play_speed / 2)', function () + + setup(function () + -- something lower than pc_data.run_anim_min_play_speed in abs value to test max + new_ground_speed = -pc_data.run_anim_min_play_speed / 2 + 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('should play the run animation at playback speed = run_anim_min_play_speed, if not 0 but lower than run_anim_min_play_speed', function () + pc:_update_platformer_motion_grounded() + + -- implementation + assert.spy(animated_sprite.play).was_called(1) + assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "run", false, pc_data.run_anim_min_play_speed) + end) + + end) + end) 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 () @@ -1860,6 +1889,7 @@ describe('player_char', function () describe('_update_ground_speed', function () 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_speed_by_intention") spy.on(player_char, "_clamp_ground_speed") @@ -1880,7 +1910,7 @@ describe('player_char', function () it('should counter the ground speed in the opposite direction of motion when moving upward a 45-degree slope', function () pc:_update_ground_speed() - -- interface + -- interface: check overall behavior (mini integration test) pc.ground_speed = 0 pc.slope_angle = -1/8 -- 45 deg ascending pc.move_intention.x = 1 @@ -1889,11 +1919,13 @@ describe('player_char', function () end) it('should update ground speed based on slope, then intention', function () + pc.ground_speed = 2.5 + pc:_update_ground_speed() -- implementation 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_speed_by_slope).was_called_with(match.ref(pc), 2.5) assert.spy(player_char._update_ground_speed_by_intention).was_called(1) assert.spy(player_char._update_ground_speed_by_intention).was_called_with(match.ref(pc)) assert.spy(player_char._clamp_ground_speed).was_called(1) @@ -1907,22 +1939,71 @@ describe('player_char', function () it('should preserve ground speed on flat ground', function () pc.ground_speed = 2 pc.slope_angle = 0 - pc:_update_ground_speed_by_slope() + pc.ascending_slope_time = 77 + + pc:_update_ground_speed_by_slope(1.8) + assert.are_equal(2, pc.ground_speed) + + assert.are_same({ + 2, + 0 + }, + { + pc.ground_speed, + pc.ascending_slope_time + }) + end) + + it('should accelerate toward left on an 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 + + pc:_update_ground_speed_by_slope(1.8) + + 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('should accelerate toward left on an ascending slope', function () + it('should accelerate toward left on an 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:_update_ground_speed_by_slope() - assert.are_equal(2 - pc_data.slope_accel_factor_frame2 * sqrt(2)/2, pc.ground_speed) + pc.ascending_slope_time = pc_data.progressive_ascending_slope_duration + + pc:_update_ground_speed_by_slope(1.8) + + 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('should accelerate toward right on an descending slope', function () + 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 = 0.125 -- sin(0.125) = sqrt(2)/2 - pc:_update_ground_speed_by_slope() - assert.are_equal(2 + pc_data.slope_accel_factor_frame2 * sqrt(2)/2, pc.ground_speed) + pc.ascending_slope_time = 77 + + pc:_update_ground_speed_by_slope(1.8) + + assert.are_same({ + 2 + pc_data.slope_accel_factor_frame2 * sqrt(2)/2, + 0 + }, + { + pc.ground_speed, + pc.ascending_slope_time + }) end) end) -- _update_ground_speed_by_slope @@ -1965,6 +2046,16 @@ describe('player_char', function () {pc.orientation, pc.ground_speed}) end) + it('should decelerate with decel descending slope factor, keeping same sign and direction when character is on 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 = 0.125 + pc:_update_ground_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) + it('should decelerate and stop exactly at speed 0, preserving direction, when character has ground speed = ground accel * 1 frame and move intention x < 0', function () pc.orientation = horizontal_dirs.right pc.ground_speed = 0.25 @@ -2042,6 +2133,33 @@ describe('player_char', function () {pc.orientation, pc.ground_speed}) 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_speed_by_intention() + assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2}, + {pc.orientation, pc.ground_speed}) + end) + + 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_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 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 = 0.125 + pc:_update_ground_speed_by_intention() + assert.are_same({horizontal_dirs.right, 1.5}, + {pc.orientation, pc.ground_speed}) + 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 @@ -2054,7 +2172,7 @@ describe('player_char', function () -- tests below seem symmetrical, but the character is actually running backward - it('should apply friction and preserive direction when character has ground speed < 0 and move intention x is 0', function () + 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_speed_by_intention() diff --git a/src/main.lua b/src/main.lua index 251e92ee..b99c905d 100644 --- a/src/main.lua +++ b/src/main.lua @@ -45,7 +45,8 @@ function _init() ['itest'] = true, ['log'] = true, ['ui'] = true, - -- ['frame'] = nil, + ['trace'] = true, + -- ['frame'] = true, -- game -- ['...'] = true, From d958a7f32d5672434252f058cb19a752305c2f1f Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Thu, 16 Jul 2020 23:27:02 +0200 Subject: [PATCH 008/112] [PLAYER CHARACTER] All new original features now consider steep slopes only - Reduced Deceleration on *Steep* Descending Slope - Progressive Ascending *Steep* Slope Factor --- src/data/playercharacter_data.lua | 11 +++++---- src/ingame/playercharacter.lua | 10 ++++---- src/ingame/playercharacter_utest.lua | 35 ++++++++++++++++++++++++---- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/data/playercharacter_data.lua b/src/data/playercharacter_data.lua index f8fee95d..937cd5e0 100644 --- a/src/data/playercharacter_data.lua +++ b/src/data/playercharacter_data.lua @@ -19,16 +19,19 @@ local playercharacter_data = { -- ground friction (passive deceleration) (px/frame^2) ground_friction_frame2 = 0.0234375, -- 1.5/64 - -- Original feature (not in SPG): No Friction on Steep Descending Slope - -- max slope angle on which friction is applied ([0-0.25], but we recommend more than 22.5 degrees i.e. 0.0625) - ground_friction_max_slope_angle = 0.075, -- 27/360 - -- gravity acceleration (px/frame^2) gravity_frame2 = 0.109375, -- 7/64 -- slope accel acceleration factor (px/frame^2), to multiply by sin(angle) slope_accel_factor_frame2 = 0.0625, -- 7/64 + -- Used by 3 original features (not in SPG): + -- - Reduced Deceleration on Steep Descending Slope + -- - No Friction on Steep Descending Slope + -- - Progressive Ascending Steep Slope Factor + -- max slope angle on which friction is applied (]0-0.25[, but we recommend more than 22.5 degrees i.e. 0.0625) + steep_slope_min_angle = 0.075, -- 27/360 + -- derived data: the slope angle for which ground friction is exactly opposed to slope factor -- is 22.02 degrees ~ 0.061 angle/360 ratio (PICO-8 unit) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 4e2b811b..5709d350 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -488,7 +488,7 @@ function player_char:_update_ground_speed_by_slope(previous_ground_speed) local is_ascending_slope = false if self.slope_angle ~= 0 then - -- Original feature (not in SPG): Progressive Ascending Slope Factor + -- Original feature (not in SPG): Progressive Ascending Steep Slope Factor -- If character is ascending a slope, do not apply the full slope factor immediately. -- Instead, linearly increase the applied slope factor from 0 to full over a given duration. -- We use the ground speed before applying intention to avoid exploid of spamming @@ -498,7 +498,7 @@ function player_char:_update_ground_speed_by_slope(previous_ground_speed) -- Resolves: character was suddenly stopped by longer slopes when starting ascension with low momentum, -- falling back to the flat ground behind, and repeating, causing a glitch-like oscillation local ascending_slope_factor = 1 - if previous_ground_speed ~= 0 and sgn(previous_ground_speed) ~= sgn(self.slope_angle) then + if previous_ground_speed ~= 0 and abs(self.slope_angle) >= pc_data.steep_slope_min_angle and sgn(previous_ground_speed) ~= sgn(self.slope_angle) then is_ascending_slope = true local ascending_slope_duration = pc_data.progressive_ascending_slope_duration local progressive_ascending_slope_factor = 1 @@ -526,13 +526,13 @@ function player_char:_update_ground_speed_by_intention() -- face move direction if not already self.orientation = signed_speed_to_dir(self.move_intention.x) else - -- Original feature (not in SPG): Reduced Deceleration on Descending Slope + -- Original feature (not in SPG): Reduced Deceleration on Steep Descending Slope -- Apply a fixed factor -- Effect: a character descending a steep slope will take more time to brake than if -- considering slope factor alone -- Resolves: character descending a steep slope was braking and turning back too suddenly local ground_decel_factor = 1 - if self.slope_angle ~= 0 and sgn(self.ground_speed) == sgn(self.slope_angle) then + if abs(self.slope_angle) >= pc_data.steep_slope_min_angle and sgn(self.ground_speed) == sgn(self.slope_angle) then -- character is trying to brake on a descending slope ground_decel_factor = pc_data.ground_decel_descending_slope_factor end @@ -562,7 +562,7 @@ function player_char:_update_ground_speed_by_intention() -- Effect: the character will automatically run down a steep slope and accumulate acceleration downward -- without friction -- Resolves: the character was moving down a steep slope very slowly because of friction - if abs(self.slope_angle) <= pc_data.ground_friction_max_slope_angle or sgn(self.ground_speed) ~= sgn(self.slope_angle) then + if abs(self.slope_angle) <= pc_data.steep_slope_min_angle or sgn(self.ground_speed) ~= sgn(self.slope_angle) then self.ground_speed = sgn(self.ground_speed) * max(0, abs(self.ground_speed) - pc_data.ground_friction_frame2) end end diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 2e373400..984e5057 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1955,7 +1955,7 @@ describe('player_char', function () }) end) - it('should accelerate toward left on an ascending slope, with reduced slope factor before ascending slope duration, and increase ascending slope time', function () + 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 @@ -1972,7 +1972,7 @@ describe('player_char', function () }) end) - it('should accelerate toward left on an ascending slope, with full slope factor after ascending slope duration, and clamp time to that duration', function () + 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 @@ -1989,9 +1989,26 @@ describe('player_char', function () }) 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 + + pc:_update_ground_speed_by_slope(1.8) + + 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) + 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 = 0.125 -- sin(0.125) = sqrt(2)/2 + pc.slope_angle = 0.125 -- sin(0.125) = - sqrt(2)/2 pc.ascending_slope_time = 77 pc:_update_ground_speed_by_slope(1.8) @@ -2046,7 +2063,7 @@ describe('player_char', function () {pc.orientation, pc.ground_speed}) end) - it('should decelerate with decel descending slope factor, keeping same sign and direction when character is on descending slope facing right, has high ground speed > ground accel * 1 frame and move intention x < 0', function () + 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 @@ -2056,6 +2073,16 @@ describe('player_char', function () {pc.orientation, pc.ground_speed}) 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 = 0.0625 + pc:_update_ground_speed_by_intention() + assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_decel_frame2}, + {pc.orientation, pc.ground_speed}) + end) + it('should decelerate and stop exactly at speed 0, preserving direction, when character has ground speed = ground accel * 1 frame and move intention x < 0', function () pc.orientation = horizontal_dirs.right pc.ground_speed = 0.25 From eb4029e92bcf04d9a9f3a0549897bed716eab5f7 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 17 Jul 2020 15:32:53 +0200 Subject: [PATCH 009/112] [GIT] Use absolute path for pico-boots submodule to enable hyperlink on GitHub (and forking) --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 04f46fbb..07ac0c8e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "pico-boots"] path = pico-boots - url = ../pico-boots.git + url = https://github.com/hsandt/pico-boots.git From f7aed7b01adf61cb1911d9ebbffb2413e6c13462 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Thu, 23 Jul 2020 16:15:01 +0200 Subject: [PATCH 010/112] [TEST] Fixed itest 'platformer ascending slope right' affected by Original feature: Progressive Ascending Steep Slope Factor --- src/ingame/playercharacter_utest.lua | 25 +++++++++++++++++++- src/itests/itestplayercharacter.lua | 34 ++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 984e5057..1c4dba2b 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1955,7 +1955,9 @@ describe('player_char', function () }) 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 () + -- Original feature (not in SPG): Progressive Ascending Steep Slope Factor + + 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 @@ -1972,6 +1974,23 @@ describe('player_char', function () }) 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 + + pc:_update_ground_speed_by_slope(1.8) + + 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) + 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 @@ -2063,6 +2082,8 @@ describe('player_char', function () {pc.orientation, pc.ground_speed}) end) + -- Original feature (not in SPG): Reduced Deceleration on Steep Descending Slope + 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 @@ -2178,6 +2199,8 @@ describe('player_char', function () {pc.orientation, pc.ground_speed}) end) + -- Original feature (not in SPG): No Friction on Steep Descending Slope + 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 diff --git a/src/itests/itestplayercharacter.lua b/src/itests/itestplayercharacter.lua index 8c777ada..4ecce1d3 100644 --- a/src/itests/itestplayercharacter.lua +++ b/src/itests/itestplayercharacter.lua @@ -103,12 +103,15 @@ expect pc_velocity 0 0 -- expected position: vector(4 + 2 * 10.8984375 - 0.703125, 80.) = vector(25.09375, 80) -- otherwise, character has stopped so expected speed is 0 - -- bugfix history: -- . forgot to add a solid ground below the slope to confirm ground -- ! identified bug in _compute_ground_motion_result where slope angle was set on extra step, -- despite being only a subpixel extra move -- . was expecting positive speed but slope was ascending +-- note that I reduced frame count from 15 tp 14 as I didn't want to check slope factor reduction too much +-- eventually it's more a utest than an itest, but fine +-- I will stop doing those super-precise checks anyway, since I may add for Original Features not matching +-- Sonic behavior exactly itest_dsl_parser.register( 'platformer ascending slope right', [[ @stage # @@ -118,15 +121,27 @@ itest_dsl_parser.register( warp 4 16 move right -wait 15 +wait 14 -expect pc_bottom_pos 0x0006.8509 15 +expect pc_bottom_pos 6.36378532203461338585 15 expect pc_motion_state grounded expect pc_slope -0.125 -expect pc_ground_spd 0.26318359375 -expect pc_velocity 0x0000.2fa4 -0x0000.2fa5 +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_slope -0.125 +-- expect pc_ground_spd 0.26318359375 +-- expect pc_velocity 0x0000.2fa4 -0x0000.2fa5 +-- ]]) + +--[[ +Frame Ground Speed Velocity Bottom Pos + 1 +--]] + -- precision note on expected pc_bottom_pos: -- 6.5196685791015625, 15 (0x0006.8509, 0x000f.0000) in PICO-8 fixed point precision -- 6.5196741403377, 15 in Lua floating point precision @@ -149,8 +164,13 @@ expect pc_velocity 0x0000.2fa4 -0x0000.2fa5 -- at frame 11: bpos (5.546875, 16), velocity (0.2578125, 0), ground_speed(0.2578125) -- at frame 12: bpos (5.828125, 16), velocity (0.28125, 0), ground_speed(0.28125) -- at frame 13: bpos (6.1328125, 15), velocity (0.3046875, 0), ground_speed(0.3046875), first step on slope and at higher level than flat ground, acknowledge slope as current ground --- at frame 14: bpos (6.333572387695, 15), velocity (0.2007598876953125, -0.2007598876953125), ground_speed(0.283935546875), because slope was current ground at frame start, slope factor was applied with 0.0625*sin(45) = -0.044189453125 (in PICO-8 16.16 fixed point precision) --- at frame 15: bpos (6.519668501758, 15), velocity (0.1860961140625, -0.1860961140625), ground_speed(0.26318359375), still under slope factor effect and velocity following slope tangent + +-- from here, we apply Original feature (not in SPG): Progressive Ascending Steep Slope Factor +-- without Original Feature: at frame 14: bpos (6.333572387695, 15), velocity (0.2007598876953125, -0.2007598876953125), ground_speed(0.283935546875), because slope was current ground at frame start, slope factor was applied with 0.0625*sin(45) = -0.044189453125 (in PICO-8 16.16 fixed point precision) +-- with Original Feature: at frame 14: bpos (6.333572387695, 15), velocity (0.23097282203461338585, -0.23097282203461338585), ground_speed(0.32665186087253), because slope was current ground at frame start, slope factor was applied with 1/60/0.5*0.0625*sin(45) = -0.001472982 ~ 0xffff.ffd0 (in PICO-8 16.16 fixed point precision) +-- but still applying normal accel 0.0234375 +-- 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 -- bugfix history: From d2a78eb4bfbddcc11634f1eba7893fb81bd44783 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 00:57:27 +0200 Subject: [PATCH 011/112] [TILES] Replaced tiles with new Angel Island ground tiles with matching tile data ! Breaks itests as they were relying on certain slopes (see tile_data) ! Changed proto zone to demonstrate low slopes Known bug: friction on low slope is applied too early, Sonic falls slowly --- data/data.p8 | 292 ++++++++++++++++++------------------ pico-boots | 2 +- src/data/collision_data.lua | 47 ++++-- src/data/stage_data.lua | 2 +- 4 files changed, 184 insertions(+), 159 deletions(-) diff --git a/data/data.p8 b/data/data.p8 index ec094ee3..da6b291d 100644 --- a/data/data.p8 +++ b/data/data.p8 @@ -1,72 +1,72 @@ pico-8 cartridge // http://www.pico-8.com -version 27 +version 29 __lua__ __gfx__ -eeeeeeeee5eeeeeeeeeecccccceeceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeeeeeee575eeeeeeeeceeccccccceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -ee7ee7ee5775eeeeeeeeccffccccceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eee77eee57775eeeeeecccfcc7ccceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eee77eee577775eeeecccccc770cceee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -ee7ee7ee57755eeeeeceeecc770ceeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeeeeeee575eeeeeeeeeccccf77f0eee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeeeeeeee5eeeeeeeee77ffcfffeeeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -8888888888888888ee7777ccfeee77ee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -8888888888888888eee77ecccfcf77ee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -8888888888888888eeeeeecccc7ceeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -8888888888888888eeeee7cee788eeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -8888888888888888eee0877ee8878eee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -8888888888888888eee0888eee788eee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -8888888888888888eeee8778eee08eee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -8888888888888888eeee0888eeeeeeee888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -99994444eeeeeee49eeeeeeeeeeeeeee99eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -99994444eeeeee4499eeeeeeeeeeeeee999ee4e4eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -99994444eeeee444999eeeeeeeeeeeee99994444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -99994444eeee44449999eeeeeeeeee4499994444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -44449999eee4999944449eeeeeee999944449999eeeeeeee44449999eeee9999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -44449999ee449999444499eeee44999944449999eeeeeeee44449999eeee9999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -44449999e44499994444999e4444999944449999eeeeeeee44449999eeee999944449999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -4444999944449999444499994444999944449999eeeeeeee44449999eeee999944449999eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -77777777000000077000000000000000770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -77777777000000777700000000000000777007070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -77777777000007777770000000000000777777770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -77777777000077777777000000000077777777770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -77777777000777777777700000007777777777770000000077777777000077770000000000000000000000000000000000000000000000000000000000000000 -77777777007777777777770000777777777777770000000077777777000077770000000000000000000000000000000000000000000000000000000000000000 -77777777077777777777777077777777777777770000000077777777000077777777777700000000000000000000000000000000000000000000000000000000 -77777777777777777777777777777777777777770000000077777777000077777777777700000000000000000000000000000000000000000000000000000000 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeeeeeee5eeeeeeeeeecccccceeceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee575eeeeeeeeceeccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +ee7ee7ee5775eeeeeeeeccffccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eee77eee57775eeeeeecccfcc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eee77eee577775eeeecccccc770cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +ee7ee7ee57755eeeeeceeecc770ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee575eeeeeeeeeccccf77f0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeee5eeeeeeeee77ffcfffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +8888888888888888ee7777ccfeee77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +8888888888888888eee77ecccfcf77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +8888888888888888eeeeeecccc7ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +8888888888888888eeeee7cee788eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +8888888888888888eee0877ee8878eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +8888888888888888eee0888eee788eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +8888888888888888eeee8778eee08eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +8888888888888888eeee0888eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +7777777700000000eeeeeeeeeeeeeeeeeeeeeeee0000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +7777777700007777eeeeeeeeeeeeeeeeeeeeeeee7777000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +7777777777777777eeeeeeeeeeeeeeeeeeeeeeee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +7777777777777777eeeeeeeeeeeeeeeeeeeeeeee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +7777777777777777eeeeeeeeeeeeeeeeeeeeeeee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +7777777777777777eeeeeeeeeeebeeeeeeeee9ee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +7777777777777777eeeeeeeeebeb9e9eee9e99ee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +7777777777777777e9eeee9ebb39b9b9e9be9beb7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeee9ee9eb9bb9b9bb9b9b9b9b9b39b9be9ee9eeeeeeee9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee9ee9bb9bb9bb9bbbb9bbb9bbb9bbbb9bb9bb9ee9ee9ee9e9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeb9bbbbb9bb9bbbbbbbbbbbbbbbbbbbbbbb9bbbbb9bb9bb9b9beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeee9bbbb93bbbbbb39b3bbbbbbbbbbbbbbbbbbbb39bbbbbbbb9bbb9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeee9bbbbb03bbbb30b30bbbbb0bbbbbb3bbbbbb30bbbbbbbbbbbbb9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeb3b3bb03b30b30b00bb03b03bbbbb030b0bb30bb3bbbbb3bbb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeebeb0b3003003003003000b00b033b00030b3003b0bb3b30b33ebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeebe00000000000400400403003003004000b000000003000303ebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee04440444000440400440440044404440eeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000eeeeeeee +eeeeeeee04444444404444440444444044444440eeeeeeeeeeeeeeee0000000000000000000000000000777777770000000000000000000000000000eeeeeeee +eeeeeeee04444444444444444444444444444440eeeeeeeeeeeeeeee0000000000000000000000007777777777777777000000000000000000000000eeeeeeee +eeeeeeee04444494444449499444444449444440eeeeeeeeeeeeeeee0000000000000000000077777777777777777777777700000000000000000000eeeeeeee +eeeeeeee44444499444444499494444499444444eeeeeeeeeeeeeeee0000000000007777777777777777777777777777777777777777000000000000eeeeeeee +eeeeeeee04444499444494999944444499444440eeeeeeeeeeeeeeee0000000077777777777777777777777777777777777777777777777700000000eeeeeeee +eeeeeeee44444494444494994949444449444444eeeeeeeeeeeeeeee0000777777777777777777777777777777777777777777777777777777770000eeeeeeee +eeeeeeee04444494444494494949444449444440eeeeeeeeeeeeeeee7777777777777777777777777777777777777777777777777777777777777777eeeeeeee +eeeeeeee04444444444444499949444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9ebeebe9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee04444494444449499444444449444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eeeb9e9bbbbbb9e9beee9eeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeee04444444444449499444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eee9ee9eebb9b9bbbbbb9b9bbee9ee9eee9eeeeeeeeeeeeeeeeeeeeee +eeeeeeee04444494444444499949444449444440eeeeeeeeeeeeeeeeeeeeeeeee9eeee9ee99ebb9bbbbbbbbbbbbbbbbbb9bbe99ee9eeee9eeeeeeeeeeeeeeeee +eeeeeeee44444499444444499944444499444444eeeeeeeeeeeeeeeeeeeeeeeee9be9b9bb93bbb9bbbbb9bbbbbb9bbbbb9bbb39bb9b9eb9eeeeeeeeeeeeeeeee +eeeeeeee04444499444494949994444499444440eeeeeeeeeeeeeeeeeee9ee9bb9bb9bbbbb3bbbbbb3bbbbb33bbbbb3bbbbbb3bbbbb9bb9bb9ee9eeeeeeeeeee +eeeeeeee44444494444494949949444449444444eeeeeeeeeeeeeeeee9e99b9bbbbb93b9bb3bbbbbb33b3b3003b3b33bbbbbb3bb9b39bbbbb9b99e9eeeeeeeee +eeeeeeee04444494444494999949444449444440eeeeeeeeeeeeeeee99b99bb3bbbbb3b9bbbb3b3b3303030000303033b3b3bbbb9b3bbbbb3bb99b99eeeeeeee +eeeeeeee04444494444444999949444449444440eeeeeeeeeeeeeeeb9bbb9bbbbbbbbbb3bbbbbbb300000004400000003bbbbbbb3bbbbbbbbbb9bbb9beeeeeee +eeeeeeee04444494444494999944444449444440eeeeeeeeeeeeee9bbbbbbbbbbb0bb3b3bb0bb3b304404004400404403b3bb0bb3b3bb0bbbbbbbbbbb9eeeeee +eeeeeeee04444444434499999449444444444440eeeeeeeeeeeeee9bbbbb300b3003b0303003b0304444440440444444030b3003030b3003b003bbbbb9eeeeee +eeeeeeee044444944b3439499949444449444440eeeeeeeeeeeeeeb3b3bb04030040300400403004444444444444444440030400400304003040bb3b3beeeeee +eeeeeeeeb44444993bb4b949999944449944444beeeeeeeeeeeeeebeb0bb44404444004444440044444444444444444444004444440044440444bb0bebeeeeee +eeeeeeeebb4444bbbbbb3999999944b3bb4444bbeeeeeeeeeeeeeebe30b3444044444044444440449444444444444449440444444404444404443b03ebeeeeee +eeeeeeee4b344bbbb33bb939993b43bbbbb443b4eeeeeeeeeeeeeeee00b4444444444444444444499449444444449449944444444444444444444b00eeeeeeee +eeeeeeeebbb3443bbb33b3b3393b3bbbb3443bbbeeeeeeeeeeeeeeee4034444494494444444494499944444444444499944944444444944944444304eeeeeeee +eeeeeeeeeeeeeeeebb3b3bbbbb3bbb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeb3bb33bbb333bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeee03b3bbbbb3b3b3b0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeb03bbb333bbbbbb0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeb333bbbbb33b30bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeeb3bbb3bb30033bbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeee3bb330bbbb3b33bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeeeeeeeeeebbbbb0bbb33bb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeccccccee1eeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceee eeeceeccccccceeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceee eeeeccffccccceeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceee @@ -83,90 +83,90 @@ eeeeeeeececeeeeeeeee7ccee888eeeeee87ccceeec7870ee07887cceecceeeeeee087ccceeeeeee eeeeeeee7e7eeeeeeee877eeee877eeee787eeeeee7880eee8787cceeee778eeee077eee7eeeeeeeeeeeeec087eeeeeeeee0888eee788eeeee087eeeeee00eee eeeeeee0872088eeeee88878eee88eeee87eeeeeeee80eeeeeeeeeeeeee8878eee08eeee8788eeeeeeeee870788eeeeeeeee8778eee08eeeee888eeeeeeeeeee eeeeeee27888028eeeee87888eeeeeeee88eeeeeeeeeeeeeeeeeeeeeeeee8788ee88eeee07088eeeeeeee8880888eeeeeeee0888eeeeeeeeee0888eeeeeeeeee -eeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceeeeeeeccccceeeeeee8888888888888888888888888888888888888888888888888888888888888888 -eeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceeeeeececccccceeeee8888888888888888888888888888888888888888888888888888888888888888 -eeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffcccceeee8888888888888888888888888888888888888888888888888888888888888888 -eeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceee8888888888888888888888888888888888888888888888888888888888888888 -eecccccc771cceeeeecccccc771cceeeeecccccc771cceeeeeccccccc70cceee8888888888888888888888888888888888888888888888888888888888888888 -eeceeecc771ceeeeeeceeecc771ceeeeeeceeecc771ceeeeeeceecccc70ceeee8888888888888888888888888888888888888888888888888888888888888888 -eeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf7ff0eee8888888888888888888888888888888888888888888888888888888888888888 -eeeecefccfeeeeeeeeeecefccfeeeeeeeeeecffccffeeeeeeeeceecccffeeeee8888888888888888888888888888888888888888888888888888888888888888 -eeeeefccffe77eeeeeeeefcccfeeeeeeeeeefcccfeeeeeeeeeeeeccffeeeeeee8888888888888888888888888888888888888888888888888888888888888888 -eeeefecccff77eeeeeeee77ccf77eeeeeee77eecfeeeeeeeeeeee77cfe77eeee8888888888888888888888888888888888888888888888888888888888888888 -eee77eeccceeeeeeeeeee77ccc77eeeeeee77eeccf77eeeeeeeee77ccf77eeee8888888888888888888888888888888888888888888888888888888888888888 -eee77ecceccee88eee000eececceeeeeeeeeeeecce77eeeeeeeeeeececeeeeee8888888888888888888888888888888888888888888888888888888888888888 -ee80ccceeec7870ee07887cceecceeeeeee087ccceeeeeeeeeeeeeec07eeeeee8888888888888888888888888888888888888888888888888888888888888888 -e787eeeeee7880eee8787cceeee778eeee077eee7eeeeeeeeeeeee7c88eeeeee8888888888888888888888888888888888888888888888888888888888888888 -e87eeeeeeee80eeeeeeeeeeeeee8878eee08eeee8788eeeeeeeee08700eeeeee8888888888888888888888888888888888888888888888888888888888888888 -e88eeeeeeeeeeeeeeeeeeeeeeeee8788ee88eeee07088eeeeeeee078880eeeee8888888888888888888888888888888888888888888888888888888888888888 -eeecccceeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -ecccccccceeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eccccc7cceeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -ccccc777cceeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -cccccc7ccceeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -cccccccccceeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -cccccccccceeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -ecccccccceeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -ecccccccceeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeecccceeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -eeeeeeeeeeeeeeee8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 -88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888 +eeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceeeeeeeccccceeeeeee9999999999999999999999999999999999999999999999999999999999999999 +eeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceeeeeececccccceeeee9999999999999999999999999999999999999999999999999999999999999999 +eeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffcccceeee9999999999999999999999999999999999999999999999999999999999999999 +eeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceeeeeecccfcc7ccceee9999999999999999999999999999999999999999999999999999999999999999 +eecccccc771cceeeeecccccc771cceeeeecccccc771cceeeeeccccccc70cceee9999999999999999999999999999999999999999999999999999999999999999 +eeceeecc771ceeeeeeceeecc771ceeeeeeceeecc771ceeeeeeceecccc70ceeee9999999999999999999999999999999999999999999999999999999999999999 +eeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf77f0eeeeeeeccccf7ff0eee9999999999999999999999999999999999999999999999999999999999999999 +eeeecefccfeeeeeeeeeecefccfeeeeeeeeeecffccffeeeeeeeeceecccffeeeee9999999999999999999999999999999999999999999999999999999999999999 +eeeeefccffe77eeeeeeeefcccfeeeeeeeeeefcccfeeeeeeeeeeeeccffeeeeeee9999999999999999999999999999999999999999999999999999999999999999 +eeeefecccff77eeeeeeee77ccf77eeeeeee77eecfeeeeeeeeeeee77cfe77eeee9999999999999999999999999999999999999999999999999999999999999999 +eee77eeccceeeeeeeeeee77ccc77eeeeeee77eeccf77eeeeeeeee77ccf77eeee9999999999999999999999999999999999999999999999999999999999999999 +eee77ecceccee88eee000eececceeeeeeeeeeeecce77eeeeeeeeeeececeeeeee9999999999999999999999999999999999999999999999999999999999999999 +ee80ccceeec7870ee07887cceecceeeeeee087ccceeeeeeeeeeeeeec07eeeeee9999999999999999999999999999999999999999999999999999999999999999 +e787eeeeee7880eee8787cceeee778eeee077eee7eeeeeeeeeeeee7c88eeeeee9999999999999999999999999999999999999999999999999999999999999999 +e87eeeeeeee80eeeeeeeeeeeeee8878eee08eeee8788eeeeeeeee08700eeeeee9999999999999999999999999999999999999999999999999999999999999999 +e88eeeeeeeeeeeeeeeeeeeeeeeee8788ee88eeee07088eeeeeeee078880eeeee9999999999999999999999999999999999999999999999999999999999999999 +eeecccceeeeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +ecccccccceeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +eccccc7cceeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +ccccc777cceeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +cccccc7ccceeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +cccccccccceeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +cccccccccceeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +ecccccccceeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +ecccccccceeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +eeecccceeeeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +eeeeeeeeeeeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +eeeeeeeeeeeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +eeeeeeeeeeeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +eeeeeeeeeeeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +eeeeeeeeeeeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +eeeeeeeeeeeeeeee9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 __gff__ -0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010001010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010100000000000000000000010101010000000000000000000000000101010100000101010101010101000001010101000001010101010101010000000101000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 __map__ -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454540404040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454540404045454545454545454545454540404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545454545454545454545454545454545454545454545454545454545454545454545454545404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545454545454545454545454545454545454545454545454545454545454545454545454545404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545454545454545454545454545454545454545454545454545454040404040454545454545404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545454545454545454646464646454545454545454545454545454545454545454545454546454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545454545454545454545454545454545454545454545454545454545454545454541404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545454545454545454545454545454545454545454545454545454545454545454540404040454545454545454040404045454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545464646464645454545454545454545454545454545454545454545454545454545454545454545454545454040404045454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545454545454545454545454545454545454545454545454545454545454545454545454545454540404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4045454545454545454545454545454545454545454545454545414040404040454545454545454545454545454540404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4040404040404040424545454545454545454545454545454541404040404040454545454545454140404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4040404040404040404245454545454545454545454545454140404040404040454545454545414040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4040404040404040404042454545454545454545454545414040404040404040404040404040404040404040404545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4040404040404040404040424545454545454545454541404040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454040404245454545454545454140404040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454540404040404040404040404040404040404040404040454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 -4545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545454545 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101010101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101010102223241010101010102422232222245758595a5b5c5d5e5758595a6768696a6b6c6d6e5b5c5d5e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101030313233343536363135313234323334326768696a6b6c6d6e6768696a52535253525352536b6c6d6e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101041424342434243424342434243424342525352535253525352535253525352535253525352535253101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101051525352535253525352535253525352525352535253525352535253525352535253525352535253101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101051525352535253525352535253525352525352535253525352535253525352535253525352535253101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101061626362636263626362636263626362626362636263626362636263626362636263626362636263101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101072737273737273737273737273737273737372737372737273737273737273737273737273737273101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101073737272737272737272737272737272737272727373727373737272737272727372727273737372101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 __sfx__ 010c00002d340293402d340293402f3402b3402f3402b340303402d340303402d340323402f340323402f34023340233403062500000306250000021340213403062500000306250000023340233403062500000 010c000030625000000934009345153401534509340093450a3400a34516340163450a3400a34517340173450c3400c3450c3400c3450c3400c3450c3400c3450c3400c3450c3400c3450c3400c3450c3400c345 diff --git a/pico-boots b/pico-boots index fe3d86d7..b1d08e33 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit fe3d86d7db4b32deb4a00e9c8ae92e899d80a413 +Subproject commit b1d08e3348312ce45c8d936d4a99859e07820f68 diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua index 9d6f55f8..30ed2ca7 100644 --- a/src/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -20,17 +20,42 @@ return { -- converter so we can edit visually, but also generate data code reusable -- for headless tests tiles_data = { - [64] = tile_data(sprite_id_location(0, 5), 0), -- 64 @ (0, 4) - [65] = tile_data(sprite_id_location(1, 5), -0.125), -- 65 @ (1, 4) - [66] = tile_data(sprite_id_location(2, 5), 0.125), -- 66 @ (2, 4) - [67] = tile_data(sprite_id_location(3, 5), -0.0625), -- 67 @ (3, 4) - [68] = tile_data(sprite_id_location(4, 5), 0), -- 68 @ (4, 4) - [69] = tile_data(sprite_id_location(5, 5), 0), -- 69 @ (5, 4) - [70] = tile_data(sprite_id_location(6, 5), 0), -- 70 @ (6, 4) - [71] = tile_data(sprite_id_location(7, 5), 0), -- 71 @ (7, 4) - [72] = tile_data(sprite_id_location(8, 5), 0), -- 72 @ (8, 4) - [73] = tile_data(sprite_id_location(9, 5), 0), -- 73 @ (9, 4) - [74] = tile_data(sprite_id_location(10, 5), 0), -- 74 @ (10, 4) + [49] = tile_data(sprite_id_location(1, 2), atan2(8, 2)), -- 49 @ (1, 3) + [50] = tile_data(sprite_id_location(0, 2), 0), -- 50 @ (2, 3) + [51] = tile_data(sprite_id_location(0, 2), 0), -- 51 @ (3, 3) + [52] = tile_data(sprite_id_location(0, 2), 0), -- 52 @ (4, 3) + [53] = tile_data(sprite_id_location(5, 2), atan2(8, -2)), -- 53 @ (5, 3) + [54] = tile_data(sprite_id_location(6, 2), 0), -- 54 @ (6, 3) + [65] = tile_data(sprite_id_location(0, 2), 0), -- 65 @ (1, 4) + [66] = tile_data(sprite_id_location(0, 2), 0), -- 66 @ (2, 4) + [67] = tile_data(sprite_id_location(0, 2), 0), -- 67 @ (3, 4) + [68] = tile_data(sprite_id_location(0, 2), 0), -- 68 @ (4, 4) + [81] = tile_data(sprite_id_location(0, 2), 0), -- 81 @ (1, 5) + [82] = tile_data(sprite_id_location(0, 2), 0), -- 82 @ (2, 5) + [83] = tile_data(sprite_id_location(0, 2), 0), -- 83 @ (3, 5) + [84] = tile_data(sprite_id_location(0, 2), 0), -- 84 @ (4, 5) + [97] = tile_data(sprite_id_location(0, 2), 0), -- 97 @ (1, 6) + [98] = tile_data(sprite_id_location(0, 2), 0), -- 98 @ (2, 6) + [99] = tile_data(sprite_id_location(0, 2), 0), -- 99 @ (3, 6) + [100]= tile_data(sprite_id_location(0, 2), 0), --100 @ (4, 6) + [114]= tile_data(sprite_id_location(0, 2), 0), --114 @ (2, 7) + [115]= tile_data(sprite_id_location(0, 2), 0), --115 @ (3, 7) + [87] = tile_data(sprite_id_location(7, 4), atan2(8, 2)), -- 87 @ (7, 5) + [88] = tile_data(sprite_id_location(8, 4), atan2(8, 1)), -- 88 @ (8, 5) + [89] = tile_data(sprite_id_location(9, 4), atan2(8, 2)), -- 89 @ (9, 5) + [90] = tile_data(sprite_id_location(10, 4), atan2(8, 2)), -- 90 @ (10, 5) + [91] = tile_data(sprite_id_location(11, 4), atan2(8, -2)), -- 91 @ (11, 5) + [92] = tile_data(sprite_id_location(12, 4), atan2(8, -2)), -- 92 @ (12, 5) + [93] = tile_data(sprite_id_location(13, 4), atan2(8, -1)), -- 93 @ (13, 5) + [94] = tile_data(sprite_id_location(14, 4), atan2(8, -2)), -- 94 @ (14, 5) + [103]= tile_data(sprite_id_location(0, 2), 0), -- 103 @ (7, 6) + [104]= tile_data(sprite_id_location(0, 2), 0), -- 104 @ (8, 6) + [105]= tile_data(sprite_id_location(0, 2), 0), -- 105 @ (9, 6) + [106]= tile_data(sprite_id_location(0, 2), 0), -- 106 @ (10, 6) + [107]= tile_data(sprite_id_location(0, 2), 0), -- 107 @ (11, 6) + [108]= tile_data(sprite_id_location(0, 2), 0), -- 108 @ (12, 6) + [109]= tile_data(sprite_id_location(0, 2), 0), -- 109 @ (13, 6) + [110]= tile_data(sprite_id_location(0, 2), 0), -- 110 @ (14, 6) } } diff --git a/src/data/stage_data.lua b/src/data/stage_data.lua index 979f8010..ed1ad8f9 100644 --- a/src/data/stage_data.lua +++ b/src/data/stage_data.lua @@ -23,7 +23,7 @@ return { height = 32, -- where the player character spawns on stage start - spawn_location = location(2, 10), + spawn_location = location(5, 20), -- the x to reach to finish the stage goal_x = 100 * 8, From 49fd70dc2840e58138dc8c0b13d90fa335f95c2c Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 01:13:03 +0200 Subject: [PATCH 012/112] [PLAYER CHARACTER] Update ground speed by intention after slope so friction is applied on slope factor itself and low slopes don't make Sonic move at all Added parameter previous_ground_speed just in case, currently unused Tests have not been updated yet (but not much will change since we cannot test callback order) --- src/ingame/playercharacter.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 5709d350..58f16055 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -478,8 +478,9 @@ function player_char:_update_ground_speed() -- fictive_future_ground_speed = previous_ground_speed + ground_speed_delta, then compute -- some clamped_ground_speed_delta and reapply it to the actual self.ground_speed) local previous_ground_speed = self.ground_speed - self:_update_ground_speed_by_intention() + -- testing different order so friction applies and stops low slope self:_update_ground_speed_by_slope(previous_ground_speed) + self:_update_ground_speed_by_intention(previous_ground_speed) self:_clamp_ground_speed() end @@ -517,7 +518,7 @@ function player_char:_update_ground_speed_by_slope(previous_ground_speed) end -- update ground speed based on current move intention -function player_char:_update_ground_speed_by_intention() +function player_char:_update_ground_speed_by_intention(previous_ground_speed) if self.move_intention.x ~= 0 then if self.ground_speed == 0 or sgn(self.ground_speed) == sgn(self.move_intention.x) then From 75abcfb98b69e40215eafb9dd099fdbf4f0ca832 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 21:04:12 +0200 Subject: [PATCH 013/112] [ITEST] Fixed PICO-8 itest build with aggressive minification command_types and gp_value_types are always accessed via ["key"] Optimize: parsable_types members can be minified --- src/itest/itest_dsl.lua | 59 ++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index 1917e999..766cee0e 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -94,16 +94,19 @@ itest_dsl.generate_function_table = generate_function_table --#endif -- type of variables that can be parsed -parsable_types = enum { - "none", - "number", - "vector", - "horizontal_dir", - "control_mode", - "motion_mode", - "motion_state", - "button_id", - "gp_value", -- meta-type compounded of [gp_value_type, gp_value_args...] where gp_value_args depend on gp_value_type +-- 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 +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, -- meta-type compounded of [gp_value_type, gp_value_args...] where gp_value_args depend on gp_value_type } --#if assert @@ -112,6 +115,8 @@ parsable_type_strings = invert_table(parsable_types) -- type of commands available +-- those names are parsed at runtime for DSL, so we don't want to minify them +-- and using enum {} is fine command_types = enum { "warp", -- warp player character bottom args: {bottom_position: vector} "set", -- set gameplay value args: {gp_value_type_str: string, new_value_args...: matching gp value parsable type} @@ -134,18 +139,18 @@ command_type_strings = invert_table(command_types) -- argument types expected after those commands command_arg_types = { - [command_types.warp] = parsable_types.vector, - [command_types.set] = parsable_types.gp_value, - [command_types.set_control_mode] = parsable_types.control_mode, - [command_types.set_motion_mode] = parsable_types.motion_mode, - [command_types.move] = parsable_types.horizontal_dir, - [command_types.stop] = parsable_types.none, - [command_types.jump] = parsable_types.none, - [command_types.stop_jump] = parsable_types.none, - [command_types.press] = parsable_types.button_id, - [command_types.release] = parsable_types.button_id, - [command_types.wait] = parsable_types.number, - [command_types.expect] = parsable_types.gp_value, + [command_types["warp"]] = parsable_types.vector, + [command_types["set"]] = parsable_types.gp_value, + [command_types["set_control_mode"]] = parsable_types.control_mode, + [command_types["set_motion_mode"]] = parsable_types.motion_mode, + [command_types["move"]] = parsable_types.horizontal_dir, + [command_types["stop"]] = parsable_types.none, + [command_types["jump"]] = parsable_types.none, + [command_types["stop_jump"]] = parsable_types.none, + [command_types["press"]] = parsable_types.button_id, + [command_types["release"]] = parsable_types.button_id, + [command_types["wait"]] = parsable_types.number, + [command_types["expect"]] = parsable_types.gp_value, } @@ -161,11 +166,11 @@ gp_value_types = enum { -- data for each gameplay value type local gp_value_data_t = { - [gp_value_types.pc_bottom_pos] = gameplay_value_data("player character bottom position", parsable_types.vector), - [gp_value_types.pc_velocity] = gameplay_value_data("player character velocity", parsable_types.vector), - [gp_value_types.pc_ground_spd] = gameplay_value_data("player character ground speed", parsable_types.number), - [gp_value_types.pc_motion_state] = gameplay_value_data("player character motion state", parsable_types.motion_state), - [gp_value_types.pc_slope] = gameplay_value_data("player character slope", parsable_types.number), + [gp_value_types["pc_bottom_pos"]] = gameplay_value_data("player character bottom position", parsable_types.vector), + [gp_value_types["pc_velocity"]] = gameplay_value_data("player character velocity", parsable_types.vector), + [gp_value_types["pc_ground_spd"]] = gameplay_value_data("player character ground speed", parsable_types.number), + [gp_value_types["pc_motion_state"]] = gameplay_value_data("player character motion state", parsable_types.motion_state), + [gp_value_types["pc_slope"]] = gameplay_value_data("player character slope", parsable_types.number), } From 4880b61b4fb7e7947096651f640040f15f65baea Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 21:07:54 +0200 Subject: [PATCH 014/112] [ITEST] Set config to itest instead of debug for separate intermediate folder --- build_itest.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_itest.sh b/build_itest.sh index 4cb2a8bb..c3a3fd33 100755 --- a/build_itest.sh +++ b/build_itest.sh @@ -17,7 +17,7 @@ author="hsandt" title="pico-sonic itests (all)" cartridge_stem="picosonic_itest_all" version="3.0" -config='debug' +config='itest' # symbols='assert,log,visual_logger,tuner,profiler,mouse,itest' # for now, we don't set extra symbols like cheat to make it lighter, but it's still possible # to test cheats in headless itests as busted preserves all (non-#pico8) code From f93e6997a757f625e9f5776b7eb0b5f761fa3162 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 21:08:31 +0200 Subject: [PATCH 015/112] [ITEST] Fix init_game_and_start_next_itest being called as a global function by prepending itest_manager: --- src/itest_main.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/itest_main.lua b/src/itest_main.lua index 778a1f3c..6941022f 100644 --- a/src/itest_main.lua +++ b/src/itest_main.lua @@ -47,7 +47,7 @@ function _init() picosonic_app.initial_gamestate = ':titlemenu' -- start first itest - init_game_and_start_next_itest() + itest_manager:init_game_and_start_next_itest() end function _update60() @@ -69,7 +69,7 @@ function handle_input() return elseif btnp(button_ids.right) then -- skip current itest - init_game_and_start_next_itest() + itest_manager:init_game_and_start_next_itest() return elseif btnp(button_ids.up) then -- go back 10 itests @@ -86,7 +86,7 @@ function handle_input() itest_runner.current_state == test_states.timeout then -- previous itest has finished, wait for x press to continue to next itest if btnp(button_ids.x) then - init_game_and_start_next_itest() + itest_manager:init_game_and_start_next_itest() end end end From 783c43719219bd005e254f84b3fe3747a5dccaf5 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 21:09:06 +0200 Subject: [PATCH 016/112] [PICO-BOOTS] Updated engine to fix extra lines added in itest DSL strings --- pico-boots | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pico-boots b/pico-boots index b1d08e33..7e28d4b8 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit b1d08e3348312ce45c8d936d4a99859e07820f68 +Subproject commit 7e28d4b86de1fbfa141c99d946b38c9f1d49b69c From fc09dbdd6bb28e3f1d5677a956964efd046733a2 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 23:02:13 +0200 Subject: [PATCH 017/112] [RUN] Fix run_itest.sh to match new itest build name --- run_itest.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run_itest.sh b/run_itest.sh index bd3036e9..1264ae99 100755 --- a/run_itest.sh +++ b/run_itest.sh @@ -1,13 +1,13 @@ #!/bin/bash -# Run itest with PICO-8 executable (itests only work in debug config) +# Run itest with PICO-8 executable # Pass any extra arguments to pico8 # Configuration: cartridge cartridge_stem="picosonic_itest_all" version="3.0" -run_cmd="pico8 -run build/${cartridge_stem}_v${version}_debug.p8 -screenshot_scale 4 -gif_scale 4 $@" +run_cmd="pico8 -run build/${cartridge_stem}_v${version}_itest.p8 -screenshot_scale 4 -gif_scale 4 $@" # 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, From 4b9bd7ffe01643f341a679984c9775111866a3d5 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 23:03:33 +0200 Subject: [PATCH 018/112] [ITEST] Fix aggressive minification with "_parse_", protected enum versions Restored parsable_types as enum with fixed strings --- src/itest/itest_dsl.lua | 147 +++++++++++++++++++++------------- src/itest/itest_dsl_utest.lua | 92 ++++++++++----------- 2 files changed, 136 insertions(+), 103 deletions(-) diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index 766cee0e..38bb5e51 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -97,18 +97,51 @@ itest_dsl.generate_function_table = generate_function_table -- 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 -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, -- meta-type compounded of [gp_value_type, gp_value_args...] where gp_value_args depend on gp_value_type +parsable_types = enum { + "none", + "number", + "vector", + "horizontal_dir", + "control_mode", + "motion_mode", + "motion_state", + "button_id", + "gp_value", -- meta-type compounded of [gp_value_type, gp_value_args...] where gp_value_args depend on gp_value_type } +-- Protected enums: map hardcoded strings to members, to support runtime parsing even when member names are minified on the original enums +horizontal_dirs_protected = { + ["left"] = 1, + ["right"] = 2 +} + +control_modes_protected = { + ["human"] = 1, -- player controls character + ["ai"] = 2, -- ai controls character + ["puppet"] = 3 -- itest script controls character +} + +motion_modes_protected = { + ["platformer"] = 1, -- normal in-game + ["debug"] = 2 -- debug "fly" mode +} + +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 +} + +button_ids_protected = { + ["left"] = 0, + ["right"] = 1, + ["up"] = 2, + ["down"] = 3, + ["o"] = 4, + ["x"] = 5 +} + + --#if assert parsable_type_strings = invert_table(parsable_types) --#endif @@ -139,18 +172,18 @@ command_type_strings = invert_table(command_types) -- argument types expected after those commands command_arg_types = { - [command_types["warp"]] = parsable_types.vector, - [command_types["set"]] = parsable_types.gp_value, - [command_types["set_control_mode"]] = parsable_types.control_mode, - [command_types["set_motion_mode"]] = parsable_types.motion_mode, - [command_types["move"]] = parsable_types.horizontal_dir, - [command_types["stop"]] = parsable_types.none, - [command_types["jump"]] = parsable_types.none, - [command_types["stop_jump"]] = parsable_types.none, - [command_types["press"]] = parsable_types.button_id, - [command_types["release"]] = parsable_types.button_id, - [command_types["wait"]] = parsable_types.number, - [command_types["expect"]] = parsable_types.gp_value, + [command_types["warp"]] = parsable_types["vector"], + [command_types["set"]] = parsable_types["gp_value"], + [command_types["set_control_mode"]] = parsable_types["control_mode"], + [command_types["set_motion_mode"]] = parsable_types["motion_mode"], + [command_types["move"]] = parsable_types["horizontal_dir"], + [command_types["stop"]] = parsable_types["none"], + [command_types["jump"]] = parsable_types["none"], + [command_types["stop_jump"]] = parsable_types["none"], + [command_types["press"]] = parsable_types["button_id"], + [command_types["release"]] = parsable_types["button_id"], + [command_types["wait"]] = parsable_types["number"], + [command_types["expect"]] = parsable_types["gp_value"], } @@ -166,68 +199,68 @@ gp_value_types = enum { -- data for each gameplay value type local gp_value_data_t = { - [gp_value_types["pc_bottom_pos"]] = gameplay_value_data("player character bottom position", parsable_types.vector), - [gp_value_types["pc_velocity"]] = gameplay_value_data("player character velocity", parsable_types.vector), - [gp_value_types["pc_ground_spd"]] = gameplay_value_data("player character ground speed", parsable_types.number), - [gp_value_types["pc_motion_state"]] = gameplay_value_data("player character motion state", parsable_types.motion_state), - [gp_value_types["pc_slope"]] = gameplay_value_data("player character slope", parsable_types.number), + [gp_value_types["pc_bottom_pos"]] = gameplay_value_data("player character bottom position", parsable_types["vector"]), + [gp_value_types["pc_velocity"]] = gameplay_value_data("player character velocity", parsable_types["vector"]), + [gp_value_types["pc_ground_spd"]] = gameplay_value_data("player character ground speed", parsable_types["number"]), + [gp_value_types["pc_motion_state"]] = gameplay_value_data("player character motion state", parsable_types["motion_state"]), + [gp_value_types["pc_slope"]] = gameplay_value_data("player character slope", parsable_types["number"]), } --- parsing functions +-- parsing functions (start with _ to protect against member name minification) -function itest_dsl.parse_none(arg_strings) - assert(#arg_strings == 0, "parse_none: got "..#arg_strings.." args, expected 0") +function itest_dsl._parse_none(arg_strings) + assert(#arg_strings == 0, "_parse_none: got "..#arg_strings.." args, expected 0") return nil end -function itest_dsl.parse_number(arg_strings) - assert(#arg_strings == 1, "parse_number: got "..#arg_strings.." args, expected 1") +function itest_dsl._parse_number(arg_strings) + assert(#arg_strings == 1, "_parse_number: got "..#arg_strings.." args, expected 1") return string_tonum(arg_strings[1]) end -function itest_dsl.parse_vector(arg_strings) - assert(#arg_strings == 2, "parse_vector: got "..#arg_strings.." args, expected 2") +function itest_dsl._parse_vector(arg_strings) + assert(#arg_strings == 2, "_parse_vector: got "..#arg_strings.." args, expected 2") return vector(string_tonum(arg_strings[1]), string_tonum(arg_strings[2])) end -function itest_dsl.parse_horizontal_dir(arg_strings) - assert(#arg_strings == 1, "parse_horizontal_dir: got "..#arg_strings.." args, expected 1") - local horizontal_dir = horizontal_dirs[arg_strings[1]] - assert(horizontal_dir, "horizontal_dirs["..arg_strings[1].."] is not defined") +function itest_dsl._parse_horizontal_dir(arg_strings) + assert(#arg_strings == 1, "_parse_horizontal_dir: got "..#arg_strings.." args, expected 1") + local horizontal_dir = horizontal_dirs_protected[arg_strings[1]] + assert(horizontal_dir, "horizontal_dirs_protected["..arg_strings[1].."] is not defined") return horizontal_dir end -function itest_dsl.parse_control_mode(arg_strings) - assert(#arg_strings == 1, "parse_control_mode: got "..#arg_strings.." args, expected 1") - local control_mode = control_modes[arg_strings[1]] - assert(control_mode, "control_modes["..arg_strings[1].."] is not defined") +function itest_dsl._parse_control_mode(arg_strings) + assert(#arg_strings == 1, "_parse_control_mode: got "..#arg_strings.." args, expected 1") + local control_mode = control_modes_protected[arg_strings[1]] + assert(control_mode, "control_modes_protected["..arg_strings[1].."] is not defined") return control_mode end -function itest_dsl.parse_motion_mode(arg_strings) - assert(#arg_strings == 1, "parse_motion_mode: got "..#arg_strings.." args, expected 1") - local motion_mode = motion_modes[arg_strings[1]] - assert(motion_mode, "motion_modes["..arg_strings[1].."] is not defined") +function itest_dsl._parse_motion_mode(arg_strings) + assert(#arg_strings == 1, "_parse_motion_mode: got "..#arg_strings.." args, expected 1") + local motion_mode = motion_modes_protected[arg_strings[1]] + assert(motion_mode, "motion_modes_protected["..arg_strings[1].."] is not defined") return motion_mode end -function itest_dsl.parse_motion_state(arg_strings) - assert(#arg_strings == 1, "parse_motion_state: got "..#arg_strings.." args, expected 1") - local motion_state = motion_states[arg_strings[1]] - assert(motion_state, "motion_states["..arg_strings[1].."] is not defined") +function itest_dsl._parse_motion_state(arg_strings) + assert(#arg_strings == 1, "_parse_motion_state: got "..#arg_strings.." args, expected 1") + local motion_state = motion_states_protected[arg_strings[1]] + assert(motion_state, "motion_states_protected["..arg_strings[1].."] is not defined") return motion_states[arg_strings[1]] end -function itest_dsl.parse_button_id(arg_strings) - assert(#arg_strings == 1, "parse_button_id: got "..#arg_strings.." args, expected 1") - local button_id = button_ids[arg_strings[1]] - assert(button_id, "button_ids["..arg_strings[1].."] is not defined") +function itest_dsl._parse_button_id(arg_strings) + assert(#arg_strings == 1, "_parse_button_id: got "..#arg_strings.." args, expected 1") + local button_id = button_ids_protected[arg_strings[1]] + assert(button_id, "button_ids_protected["..arg_strings[1].."] is not defined") return button_ids[arg_strings[1]] end -function itest_dsl.parse_gp_value(arg_strings) - assert(#arg_strings > 1, "parse_gp_value: got "..#arg_strings.." args, expected at least 2") +function itest_dsl._parse_gp_value(arg_strings) + assert(#arg_strings > 1, "_parse_gp_value: got "..#arg_strings.." args, expected at least 2") -- same principle as itest_dsl_parser.parse, the type of the first arg -- determines how we parse the rest of the args, named "value components" local gp_value_type_str = arg_strings[1] @@ -249,7 +282,7 @@ function itest_dsl.parse_gp_value(arg_strings) end -- table of parsers for command args and gameplay values, indexed by parsed type -value_parsers = generate_function_table(itest_dsl, parsable_types, "parse_") +value_parsers = generate_function_table(itest_dsl, parsable_types, "_parse_") itest_dsl.value_parsers = value_parsers diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua index 82199490..ce89e11d 100644 --- a/src/itest/itest_dsl_utest.lua +++ b/src/itest/itest_dsl_utest.lua @@ -63,131 +63,131 @@ describe('itest_dsl', function () end) - describe('parse_', function () + describe('_parse_', function () - describe('parse_none', function () + describe('_parse_none', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_none({"too many"}) - end, "parse_none: got 1 args, expected 0") + itest_dsl._parse_none({"too many"}) + end, "_parse_none: got 1 args, expected 0") end) it('should return nil', function () - assert.is_nil(itest_dsl.parse_none({})) + assert.is_nil(itest_dsl._parse_none({})) end) end) - describe('parse_number', function () + describe('_parse_number', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_number({"too", "many"}) - end, "parse_number: got 2 args, expected 1") + itest_dsl._parse_number({"too", "many"}) + end, "_parse_number: got 2 args, expected 1") end) it('should return the single string argument as number', function () - assert.are_equal(5, itest_dsl.parse_number({"5"})) + assert.are_equal(5, itest_dsl._parse_number({"5"})) end) end) - describe('parse_vector', function () + describe('_parse_vector', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_vector({"too few"}) - end, "parse_vector: got 1 args, expected 2") + itest_dsl._parse_vector({"too few"}) + end, "_parse_vector: got 1 args, expected 2") end) it('should return the 2 coordinate string arguments as vector', function () - assert.are_equal(vector(2, -3.5), itest_dsl.parse_vector({"2", "-3.5"})) + assert.are_equal(vector(2, -3.5), itest_dsl._parse_vector({"2", "-3.5"})) end) end) - describe('parse_horizontal_dir', function () + describe('_parse_horizontal_dir', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_horizontal_dir({"too", "many"}) - end, "parse_horizontal_dir: got 2 args, expected 1") + itest_dsl._parse_horizontal_dir({"too", "many"}) + end, "_parse_horizontal_dir: got 2 args, expected 1") end) it('should return the single argument as horizontal direction', function () - assert.are_equal(horizontal_dirs.right, itest_dsl.parse_horizontal_dir({"right"})) + assert.are_equal(horizontal_dirs.right, itest_dsl._parse_horizontal_dir({"right"})) end) end) - describe('parse_control_mode', function () + describe('_parse_control_mode', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_control_mode({"too", "many"}) - end, "parse_control_mode: got 2 args, expected 1") + itest_dsl._parse_control_mode({"too", "many"}) + end, "_parse_control_mode: got 2 args, expected 1") end) it('should return the single argument as control mode', function () - assert.are_equal(control_modes.ai, itest_dsl.parse_control_mode({"ai"})) + assert.are_equal(control_modes.ai, itest_dsl._parse_control_mode({"ai"})) end) end) - describe('parse_motion_mode', function () + describe('_parse_motion_mode', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_motion_mode({"too", "many"}) - end, "parse_motion_mode: got 2 args, expected 1") + itest_dsl._parse_motion_mode({"too", "many"}) + end, "_parse_motion_mode: got 2 args, expected 1") end) it('should return the single argument as motion mode', function () - assert.are_equal(motion_modes.debug, itest_dsl.parse_motion_mode({"debug"})) + assert.are_equal(motion_modes.debug, itest_dsl._parse_motion_mode({"debug"})) end) end) - describe('parse_button_id', function () + describe('_parse_button_id', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_button_id({"too", "many"}) - end, "parse_button_id: got 2 args, expected 1") + itest_dsl._parse_button_id({"too", "many"}) + end, "_parse_button_id: got 2 args, expected 1") end) it('should return the single argument as motion mode', function () - assert.are_equal(button_ids.o, itest_dsl.parse_button_id({"o"})) + assert.are_equal(button_ids.o, itest_dsl._parse_button_id({"o"})) end) end) - describe('parse_motion_state', function () + describe('_parse_motion_state', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_motion_state({"too", "many"}) - end, "parse_motion_state: got 2 args, expected 1") + itest_dsl._parse_motion_state({"too", "many"}) + end, "_parse_motion_state: got 2 args, expected 1") end) it('should return the single argument as motion state', function () - assert.are_equal(motion_states.falling, itest_dsl.parse_motion_state({"falling"})) + assert.are_equal(motion_states.falling, itest_dsl._parse_motion_state({"falling"})) end) end) - describe('parse_gp_value', function () + describe('_parse_gp_value', function () it('should assert when the number of arguments is wrong', function () assert.has_error(function () - itest_dsl.parse_gp_value({"too few"}) - end, "parse_gp_value: got 1 args, expected at least 2") + itest_dsl._parse_gp_value({"too few"}) + end, "_parse_gp_value: got 1 args, expected at least 2") end) it('should return the gameplay value type string and the expected value, itself recursively parsed', function () assert.are_same({"pc_bottom_pos", vector(1, 3)}, - {itest_dsl.parse_gp_value({"pc_bottom_pos", "1", "3"})}) + {itest_dsl._parse_gp_value({"pc_bottom_pos", "1", "3"})}) end) end) @@ -535,7 +535,7 @@ describe('itest_dsl', function () local dsli_source = [[ @stage # -64 +32 warp expect @@ -551,8 +551,8 @@ expect '@stage', '#', tilemap({ - { 0, 64}, - {64, 0} + { 0, 32}, + {32, 0} }), { command(command_types.warp, { vector(1, 2) } ), @@ -618,8 +618,8 @@ expect setup(function () stub(itest_dsl_parser, "parse_tilemap", function () return tilemap({ - {70, 64}, - {64, 70} + {70, 32}, + {32, 70} }), 5 end) end) @@ -643,8 +643,8 @@ expect ':stage', '#', tilemap({ - {70, 64}, - {64, 70} + {70, 32}, + {32, 70} }), 5 }, @@ -696,8 +696,8 @@ expect { tilemap({ { 0, 0, 0, 0}, - {64, 64, 0, 0}, - { 0, 0, 64, 64} + {32, 32, 0, 0}, + { 0, 0, 32, 32} }), 6 }, From 87a327e898cf950e2272665310543728f0150d50 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 23:07:45 +0200 Subject: [PATCH 019/112] [ITEST] Do not use get_members outside utest, since broken by aggressive minification --- src/itest/itest_dsl.lua | 3 +-- src/itests/itestplayercharacter.lua | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index 38bb5e51..c0447970 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -29,8 +29,7 @@ expect gp_value_type expect a gameplay value to be equal to (...) require("engine/core/helper") require("engine/test/assertions") local integrationtest = require("engine/test/integrationtest") -local itest_manager, integration_test = get_members(integrationtest, - "itest_manager", "integration_test") +local itest_manager, integration_test = integrationtest.itest_manager, integrationtest.integration_test local tile_data = require("data/tile_data") local tilemap = require("engine/data/tilemap") diff --git a/src/itests/itestplayercharacter.lua b/src/itests/itestplayercharacter.lua index 4ecce1d3..90988296 100644 --- a/src/itests/itestplayercharacter.lua +++ b/src/itests/itestplayercharacter.lua @@ -1,9 +1,8 @@ -- gamestates: stage local integrationtest = require("engine/test/integrationtest") +local itest_manager, integration_test, time_trigger = integrationtest.itest_manager, integrationtest.integration_test, integrationtest.time_trigger local itest_dsl = require("itest/itest_dsl") local itest_dsl_parser = itest_dsl.itest_dsl_parser -local itest_manager, integration_test, time_trigger = get_members(integrationtest, - "itest_manager", "integration_test", "time_trigger") local input = require("engine/input/input") local flow = require("engine/application/flow") local pc_data = require("data/playercharacter_data") @@ -94,6 +93,7 @@ expect pc_motion_state grounded expect pc_ground_spd 0 expect pc_velocity 0 0 ]]) +--[=[ -- calculation notes: -- to compute position, use the fact that friction == accel, so our speed describes a pyramid over where each value is mirrored @@ -653,3 +653,5 @@ warp 4 8 move right wait 60 ]]) + +--]=] From 486f769e3969159471ac417b9f3150e3fe9af2d6 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 23:16:34 +0200 Subject: [PATCH 020/112] [ITEST] Access itest_manager (now local) via module --- pico-boots | 2 +- src/itest_main.lua | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pico-boots b/pico-boots index 7e28d4b8..7396dcff 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 7e28d4b86de1fbfa141c99d946b38c9f1d49b69c +Subproject commit 7396dcffa67edb94cecd09ca82f5bd3ba0fae001 diff --git a/src/itest_main.lua b/src/itest_main.lua index 6941022f..a26d8745 100644 --- a/src/itest_main.lua +++ b/src/itest_main.lua @@ -3,7 +3,8 @@ -- must require at main top, to be used in any required modules from here require("engine/pico8/api") -require("engine/test/integrationtest") +local integrationtest = require("engine/test/integrationtest") +local itest_manager = integrationtest.itest_manager --#if log local logging = require("engine/debug/logging") From b93b2fcc5b0a064889d6c84bbd198600b4280a0c Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Fri, 24 Jul 2020 23:38:37 +0200 Subject: [PATCH 021/112] [ITEST] Final fix to make itests run (still fail due to tile changes) - protect executors and evaluators with "_" - protect command_types ["wait"] and ["execute"] - added assert for executor access --- src/itest/itest_dsl.lua | 51 +++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index c0447970..aad546a0 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -84,6 +84,8 @@ local function generate_function_table(module, enum_types, prefix) local t = {} for type_name, enum_type in pairs(enum_types) do t[enum_type] = module[prefix..type_name] + printh("t[enum_type] = module[prefix..type_name] => t["..enum_type.."] = module["..prefix..".."..type_name.."]") + printh("t[enum_type]: "..dump(t[enum_type])) end return t end @@ -146,6 +148,13 @@ parsable_type_strings = invert_table(parsable_types) --#endif +-- Note: enums below have protected string keys to support aggressive minification +-- When done everywhere though, it makes more sense to just use strings +-- Keeping the enums around allows to track the list of supported values, +-- but consider using direct strings and listing possible values in comment. +-- The only benefit is to index other tables with enum values (so integers) rather than strings, +-- but if we have a table with string keys we do some hashing at some point anyway. + -- type of commands available -- those names are parsed at runtime for DSL, so we don't want to minify them -- and using enum {} is fine @@ -288,12 +297,12 @@ itest_dsl.value_parsers = value_parsers -- functions to execute dsl commands. they take the dsl parser as 1st parameter -- so they can update its state if needed -function itest_dsl.execute_warp(args) +function itest_dsl._execute_warp(args) local current_stage_state = get_current_state_as_stage() current_stage_state.player_char:warp_bottom_to(args[1]) end -function itest_dsl.execute_set(args) +function itest_dsl._execute_set(args) local gp_value_type_str, new_gp_value = unpack(args) local setter = itest_dsl["set_"..gp_value_type_str] @@ -301,43 +310,43 @@ function itest_dsl.execute_set(args) setter(new_gp_value) end -function itest_dsl.execute_set_control_mode(args) +function itest_dsl._execute_set_control_mode(args) local current_stage_state = get_current_state_as_stage() current_stage_state.player_char.control_mode = args[1] end -function itest_dsl.execute_set_motion_mode(args) +function itest_dsl._execute_set_motion_mode(args) local current_stage_state = get_current_state_as_stage() current_stage_state.player_char.motion_mode = args[1] end -function itest_dsl.execute_move(args) +function itest_dsl._execute_move(args) local current_stage_state = get_current_state_as_stage() current_stage_state.player_char.move_intention = horizontal_dir_vectors[args[1]] end -function itest_dsl.execute_stop(args) +function itest_dsl._execute_stop(args) local current_stage_state = get_current_state_as_stage() current_stage_state.player_char.move_intention = vector.zero() end -function itest_dsl.execute_jump(args) +function itest_dsl._execute_jump(args) local current_stage_state = get_current_state_as_stage() current_stage_state.player_char.jump_intention = true -- will be consumed current_stage_state.player_char.hold_jump_intention = true end -function itest_dsl.execute_stop_jump(args) +function itest_dsl._execute_stop_jump(args) local current_stage_state = get_current_state_as_stage() current_stage_state.player_char.hold_jump_intention = false end -function itest_dsl.execute_press(args) +function itest_dsl._execute_press(args) -- simulate sticky press for player 0 input.simulated_buttons_down[0][args[1]] = true end -function itest_dsl.execute_release(args) +function itest_dsl._execute_release(args) -- simulate release for player 0 input.simulated_buttons_down[0][args[1]] = false end @@ -345,39 +354,39 @@ end -- wait and expect are not timed actions and will be handled as special cases -- table of functions to call when applying a command with args, indexed by command type -executors = generate_function_table(itest_dsl, command_types, "execute_") +executors = generate_function_table(itest_dsl, command_types, "_execute_") itest_dsl.executors = executors -- gameplay value evaluation functions -function itest_dsl.eval_pc_bottom_pos() +function itest_dsl._eval_pc_bottom_pos() local current_stage_state = get_current_state_as_stage() return current_stage_state.player_char:get_bottom_center() end -function itest_dsl.eval_pc_velocity() +function itest_dsl._eval_pc_velocity() local current_stage_state = get_current_state_as_stage() return current_stage_state.player_char.velocity end -function itest_dsl.eval_pc_ground_spd() +function itest_dsl._eval_pc_ground_spd() local current_stage_state = get_current_state_as_stage() return current_stage_state.player_char.ground_speed end -function itest_dsl.eval_pc_motion_state() +function itest_dsl._eval_pc_motion_state() local current_stage_state = get_current_state_as_stage() return current_stage_state.player_char.motion_state end -function itest_dsl.eval_pc_slope() +function itest_dsl._eval_pc_slope() local current_stage_state = get_current_state_as_stage() return current_stage_state.player_char.slope_angle end -- table of functions used to evaluate and returns the gameplay value in current game state -evaluators = generate_function_table(itest_dsl, gp_value_types, "eval_") +evaluators = generate_function_table(itest_dsl, gp_value_types, "_eval_") itest_dsl.evaluators = evaluators @@ -622,10 +631,10 @@ function itest_dsl_parser.create_itest(name, dsli) end for cmd in all(dsli.commands) do - if cmd.type == command_types.wait then + if cmd.type == command_types["wait"] then itest_dsl_parser:_wait(cmd.args[1]) - elseif cmd.type == command_types.expect then + elseif cmd.type == command_types["expect"] then -- we currently don't support live assertions, but we support multiple -- final expectations add(itest_dsl_parser._final_expectations, expectation(cmd.args[1], cmd.args[2])) @@ -633,7 +642,9 @@ function itest_dsl_parser.create_itest(name, dsli) else -- common action, store callback for execution during itest_dsl_parser:_act(function () - executors[cmd.type](cmd.args) + local executor = executors[cmd.type] + assert(executor, "executors["..cmd.type.."] (for '"..command_type_strings[cmd.type].."') is not defined") + executor(cmd.args) end) end end From 92bc7e8540e99f56fb9d993ab11b0b434d4eb0f6 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 25 Jul 2020 00:59:06 +0200 Subject: [PATCH 022/112] [TEST] Restored test tile data for utests and itests with new IDs matching new spritesheet. Merged tile_data into tile_test_data since it's only used for tests --- data/data.p8 | 64 ++++++++++++------------ src/data/collision_data.lua | 12 +++++ src/data/tile_data.lua | 29 ----------- src/ingame/playercharacter_utest.lua | 60 +++++++++++----------- src/itest/itest_dsl.lua | 15 ++++-- src/itest/itest_dsl_utest.lua | 47 ++++++++--------- src/itests/itestplayercharacter.lua | 4 -- src/platformer/world_utest.lua | 6 +-- src/test_data/tile_data_utest.lua | 38 -------------- src/test_data/tile_test_data.lua | 75 +++++++++++++++++++--------- 10 files changed, 164 insertions(+), 186 deletions(-) delete mode 100644 src/data/tile_data.lua delete mode 100644 src/test_data/tile_data_utest.lua diff --git a/data/data.p8 b/data/data.p8 index da6b291d..c5dd7004 100644 --- a/data/data.p8 +++ b/data/data.p8 @@ -35,38 +35,38 @@ eeeeee9bbbbb03bbbb30b30bbbbb0bbbbbb3bbbbbb30bbbbbbbbbbbbb9eeeeeeeeeeeeeeeeeeeeee eeeeeeb3b3bb03b30b30b00bb03b03bbbbb030b0bb30bb3bbbbb3bbb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeebeb0b3003003003003000b00b033b00030b3003b0bb3b30b33ebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeeebe00000000000400400403003003004000b000000003000303ebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee04440444000440400440440044404440eeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000eeeeeeee -eeeeeeee04444444404444440444444044444440eeeeeeeeeeeeeeee0000000000000000000000000000777777770000000000000000000000000000eeeeeeee -eeeeeeee04444444444444444444444444444440eeeeeeeeeeeeeeee0000000000000000000000007777777777777777000000000000000000000000eeeeeeee -eeeeeeee04444494444449499444444449444440eeeeeeeeeeeeeeee0000000000000000000077777777777777777777777700000000000000000000eeeeeeee -eeeeeeee44444499444444499494444499444444eeeeeeeeeeeeeeee0000000000007777777777777777777777777777777777777777000000000000eeeeeeee -eeeeeeee04444499444494999944444499444440eeeeeeeeeeeeeeee0000000077777777777777777777777777777777777777777777777700000000eeeeeeee -eeeeeeee44444494444494994949444449444444eeeeeeeeeeeeeeee0000777777777777777777777777777777777777777777777777777777770000eeeeeeee -eeeeeeee04444494444494494949444449444440eeeeeeeeeeeeeeee7777777777777777777777777777777777777777777777777777777777777777eeeeeeee -eeeeeeee04444444444444499949444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9ebeebe9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee04444494444449499444444449444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eeeb9e9bbbbbb9e9beee9eeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee04444444444449499444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eee9ee9eebb9b9bbbbbb9b9bbee9ee9eee9eeeeeeeeeeeeeeeeeeeeee -eeeeeeee04444494444444499949444449444440eeeeeeeeeeeeeeeeeeeeeeeee9eeee9ee99ebb9bbbbbbbbbbbbbbbbbb9bbe99ee9eeee9eeeeeeeeeeeeeeeee -eeeeeeee44444499444444499944444499444444eeeeeeeeeeeeeeeeeeeeeeeee9be9b9bb93bbb9bbbbb9bbbbbb9bbbbb9bbb39bb9b9eb9eeeeeeeeeeeeeeeee -eeeeeeee04444499444494949994444499444440eeeeeeeeeeeeeeeeeee9ee9bb9bb9bbbbb3bbbbbb3bbbbb33bbbbb3bbbbbb3bbbbb9bb9bb9ee9eeeeeeeeeee -eeeeeeee44444494444494949949444449444444eeeeeeeeeeeeeeeee9e99b9bbbbb93b9bb3bbbbbb33b3b3003b3b33bbbbbb3bb9b39bbbbb9b99e9eeeeeeeee -eeeeeeee04444494444494999949444449444440eeeeeeeeeeeeeeee99b99bb3bbbbb3b9bbbb3b3b3303030000303033b3b3bbbb9b3bbbbb3bb99b99eeeeeeee -eeeeeeee04444494444444999949444449444440eeeeeeeeeeeeeeeb9bbb9bbbbbbbbbb3bbbbbbb300000004400000003bbbbbbb3bbbbbbbbbb9bbb9beeeeeee -eeeeeeee04444494444494999944444449444440eeeeeeeeeeeeee9bbbbbbbbbbb0bb3b3bb0bb3b304404004400404403b3bb0bb3b3bb0bbbbbbbbbbb9eeeeee -eeeeeeee04444444434499999449444444444440eeeeeeeeeeeeee9bbbbb300b3003b0303003b0304444440440444444030b3003030b3003b003bbbbb9eeeeee -eeeeeeee044444944b3439499949444449444440eeeeeeeeeeeeeeb3b3bb04030040300400403004444444444444444440030400400304003040bb3b3beeeeee -eeeeeeeeb44444993bb4b949999944449944444beeeeeeeeeeeeeebeb0bb44404444004444440044444444444444444444004444440044440444bb0bebeeeeee -eeeeeeeebb4444bbbbbb3999999944b3bb4444bbeeeeeeeeeeeeeebe30b3444044444044444440449444444444444449440444444404444404443b03ebeeeeee -eeeeeeee4b344bbbb33bb939993b43bbbbb443b4eeeeeeeeeeeeeeee00b4444444444444444444499449444444449449944444444444444444444b00eeeeeeee -eeeeeeeebbb3443bbb33b3b3393b3bbbb3443bbbeeeeeeeeeeeeeeee4034444494494444444494499944444444444499944944444444944944444304eeeeeeee -eeeeeeeeeeeeeeeebb3b3bbbbb3bbb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeb3bb33bbb333bb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeee03b3bbbbb3b3b3b0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeb03bbb333bbbbbb0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeb333bbbbb33b30bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeeb3bbb3bb30033bbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeee3bb330bbbb3b33bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeeeeeeeebbbbb0bbb33bb3bbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000004440444000440400440440044404440eeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000eeeeeeee +0000000004444444404444440444444044444440eeeeeeeeeeeeeeee0000000000000000000000000000777777770000000000000000000000000000eeeeeeee +0000000004444444444444444444444444444440eeeeeeeeeeeeeeee0000000000000000000000007777777777777777000000000000000000000000eeeeeeee +0000000004444494444449499444444449444440eeeeeeeeeeeeeeee0000000000000000000077777777777777777777777700000000000000000000eeeeeeee +0000777744444499444444499494444499444444eeeeeeeeeeeeeeee0000000000007777777777777777777777777777777777777777000000000000eeeeeeee +0000777704444499444494999944444499444440eeeeeeeeeeeeeeee0000000077777777777777777777777777777777777777777777777700000000eeeeeeee +0000777744444494444494994949444449444444eeeeeeeeeeeeeeee0000777777777777777777777777777777777777777777777777777777770000eeeeeeee +0000777704444494444494494949444449444440eeeeeeeeeeeeeeee7777777777777777777777777777777777777777777777777777777777777777eeeeeeee +0000000004444444444444499949444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9ebeebe9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000004444494444449499444444449444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eeeb9e9bbbbbb9e9beee9eeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000004444444444449499444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eee9ee9eebb9b9bbbbbb9b9bbee9ee9eee9eeeeeeeeeeeeeeeeeeeeee +0000000004444494444444499949444449444440eeeeeeeeeeeeeeeeeeeeeeeee9eeee9ee99ebb9bbbbbbbbbbbbbbbbbb9bbe99ee9eeee9eeeeeeeeeeeeeeeee +7777777744444499444444499944444499444444eeeeeeeeeeeeeeeeeeeeeeeee9be9b9bb93bbb9bbbbb9bbbbbb9bbbbb9bbb39bb9b9eb9eeeeeeeeeeeeeeeee +7777777704444499444494949994444499444440eeeeeeeeeeeeeeeeeee9ee9bb9bb9bbbbb3bbbbbb3bbbbb33bbbbb3bbbbbb3bbbbb9bb9bb9ee9eeeeeeeeeee +7777777744444494444494949949444449444444eeeeeeeeeeeeeeeee9e99b9bbbbb93b9bb3bbbbbb33b3b3003b3b33bbbbbb3bb9b39bbbbb9b99e9eeeeeeeee +7777777704444494444494999949444449444440eeeeeeeeeeeeeeee99b99bb3bbbbb3b9bbbb3b3b3303030000303033b3b3bbbb9b3bbbbb3bb99b99eeeeeeee +0000000004444494444444999949444449444440eeeeeeeeeeeeeeeb9bbb9bbbbbbbbbb3bbbbbbb300000004400000003bbbbbbb3bbbbbbbbbb9bbb9beeeeeee +0000000004444494444494999944444449444440eeeeeeeeeeeeee9bbbbbbbbbbb0bb3b3bb0bb3b304404004400404403b3bb0bb3b3bb0bbbbbbbbbbb9eeeeee +0000000004444444434499999449444444444440eeeeeeeeeeeeee9bbbbb300b3003b0303003b0304444440440444444030b3003030b3003b003bbbbb9eeeeee +00000000044444944b3439499949444449444440eeeeeeeeeeeeeeb3b3bb04030040300400403004444444444444444440030400400304003040bb3b3beeeeee +00000000b44444993bb4b949999944449944444beeeeeeeeeeeeeebeb0bb44404444004444440044444444444444444444004444440044440444bb0bebeeeeee +00000000bb4444bbbbbb3999999944b3bb4444bbeeeeeeeeeeeeeebe30b3444044444044444440449444444444444449440444444404444404443b03ebeeeeee +777777774b344bbbb33bb939993b43bbbbb443b4eeeeeeeeeeeeeeee00b4444444444444444444499449444444449449944444444444444444444b00eeeeeeee +77777777bbb3443bbb33b3b3393b3bbbb3443bbbeeeeeeeeeeeeeeee4034444494494444444494499944444444444499944944444444944944444304eeeeeeee +0000000000000007bb3b3bbbbb3bbb3b70000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000000000077b3bb33bbb333bb3b77000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +000000000000077703b3bbbbb3b3b3b077700000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000007700007777b03bbb333bbbbbb077770000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000777700077777b333bbbbb33b30bb77777000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0077777700777777b3bbb3bb30033bbb77777700eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +77777777077777773bb330bbbb3b33bb77777770eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +7777777777777777bbbbb0bbb33bb3bb77777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee eeeeccccccee1eeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceee eeeceeccccccceeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceee eeeeccffccccceeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceee diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua index 30ed2ca7..5e1600b5 100644 --- a/src/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -56,6 +56,18 @@ return { [108]= tile_data(sprite_id_location(0, 2), 0), -- 108 @ (12, 6) [109]= tile_data(sprite_id_location(0, 2), 0), -- 109 @ (13, 6) [110]= tile_data(sprite_id_location(0, 2), 0), -- 110 @ (14, 6) + -- proto (black and white tiles being their own collision masks) + -- must match tile_data.lua + -- if too heavy, surround with #itest and create a separate spritesheet for itests with only polygonal tiles + -- stored in some proto_data.p8 or test_data.p8 + -- this will allow us to reuse the extra space left by removing proto tiles for release (adding FX, etc.) + [32] = tile_data(sprite_id_location(0, 2), 0), -- 32 @ (0, 2) FULL TILE # + [80] = tile_data(sprite_id_location(0, 5), 0), -- 80 @ (0, 5) HALF TILE (4px high) = + [96] = tile_data(sprite_id_location(0, 6), 0), -- 96 @ (0, 6) FLAT LOW TILE (2px high) _ + [64] = tile_data(sprite_id_location(0, 4), 0), -- 64 @ (0, 4) BOTTOM-RIGHT QUARTER TILE (4px high) r + [112]= tile_data(sprite_id_location(1, 7), -0.125), -- 112 @ (1, 7) ASCENDING 45 / + [113]= tile_data(sprite_id_location(0, 7), -0.0625), -- 113 @ (0, 7) ASCENDING 22.5 < + [116]= tile_data(sprite_id_location(4, 7), 0.125), -- 116 @ (4, 7) DESCENDING 45 \ } } diff --git a/src/data/tile_data.lua b/src/data/tile_data.lua deleted file mode 100644 index 3e7806ac..00000000 --- a/src/data/tile_data.lua +++ /dev/null @@ -1,29 +0,0 @@ --- this script is similar to tile_test_data, but has some parts --- useful for itest in pico8, whereas tile_test_data is only for busted utests/itests --- it is used by tilemap for the dsl ---#if busted -local tile_test_data = require("test_data/tile_test_data") ---#endif - -tile_symbol_to_ids = { - ['.'] = 0, -- empty - ['#'] = 64, -- full tile - ['/'] = 65, -- ascending slope 45 - ['\\'] = 66, -- descending slope 45 - ['<'] = 67, -- ascending slope 22.5 -} - --- for itests that need map setup, we exceptionally not teardown --- the map since we would need to store a backup of the original map --- and we don't care, since each itest will build its own mock map -function setup_map_data() ---#if busted - tile_test_data.setup() ---#endif -end - -function teardown_map_data() ---#if busted - tile_test_data.teardown() ---#endif -end diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 1c4dba2b..e1bce892 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -884,7 +884,7 @@ describe('player_char', function () before_each(function () -- create a full tile at (1, 1), i.e. (8, 8) to (15, 15) px - mock_mset(1, 1, 64) + mock_mset(1, 1, full_tile_id) end) -- on the sides @@ -971,7 +971,7 @@ describe('player_char', function () before_each(function () -- create a half-tile at (1, 1), top-left at (8, 12), top-right at (15, 16) included - mock_mset(1, 1, 70) + mock_mset(1, 1, half_tile_id) end) -- just above @@ -1061,7 +1061,7 @@ describe('player_char', function () before_each(function () -- create an ascending slope at (1, 1), i.e. (8, 15) to (15, 8) px - mock_mset(1, 1, 65) + mock_mset(1, 1, asc_slope_45_id) end) it('should return 0.0625, -45/360 if just above slope column 0', function () @@ -1130,7 +1130,7 @@ describe('player_char', function () before_each(function () -- create a descending slope at (1, 1), i.e. (8, 8) to (15, 15) px - mock_mset(1, 1, 66) + mock_mset(1, 1, desc_slope_45_id) end) it('. should return 0.0625, 45/360 if right sensors are just a little above column 0', function () @@ -1195,7 +1195,7 @@ describe('player_char', function () before_each(function () -- create an ascending slope 22.5 at (1, 1), i.e. (8, 14) to (15, 11) px - mock_mset(1, 1, 67) + mock_mset(1, 1, asc_slope_22_id) end) it('should return -4, -22.5/360 if below column 7 by 4px)', function () @@ -1209,7 +1209,7 @@ describe('player_char', function () before_each(function () -- create a quarter-tile at (1, 1), i.e. (12, 12) to (15, 15) px -- note that the quarter-tile is made of 2 subtiles of slope 0, hence overall slope is considered 0, not an average slope between min and max height - mock_mset(1, 1, 71) + mock_mset(1, 1, bottom_right_quarter_tile_id) end) it('should return ground_query_info(max_ground_snap_height + 1, nil) if just at the bottom of the tile, on the left part, so in the air (and not 0 just because it is at height 0)', function () @@ -1244,8 +1244,8 @@ describe('player_char', function () -- 11111111 -- 11111111 23 - mock_mset(1, 1, 72) - mock_mset(1, 2, 64) + mock_mset(1, 1, flat_low_tile_id) + mock_mset(1, 2, full_tile_id) end) it('should return -4, 0 if below top by 4px, with character crossing 2 tiles', function () @@ -1263,7 +1263,7 @@ describe('player_char', function () before_each(function () -- create a full tile at (1, 1), i.e. (8, 8) to (15, 15) px - mock_mset(1, 1, 64) + mock_mset(1, 1, full_tile_id) end) it('should do nothing when character is not touching ground at all, and return false', function () @@ -1304,7 +1304,7 @@ describe('player_char', function () before_each(function () -- create a descending slope at (1, 1), i.e. (8, 8) to (15, 15) px - mock_mset(1, 1, 66) + mock_mset(1, 1, desc_slope_45_id) end) it('should do nothing when character is not touching ground at all, and return false', function () @@ -1907,6 +1907,8 @@ describe('player_char', function () player_char._clamp_ground_speed:clear() end) + -- WIP since change of call order + it('should counter the ground speed in the opposite direction of motion when moving upward a 45-degree slope', function () pc:_update_ground_speed() @@ -2800,7 +2802,7 @@ describe('player_char', function () describe('(with flat ground)', function () before_each(function () - mock_mset(0, 1, 64) -- full tile + mock_mset(0, 1, full_tile_id) -- full tile end) -- in the tests below, we can use pc_data.center_height_standing directly instead @@ -2919,11 +2921,11 @@ describe('player_char', function () before_each(function () -- X X -- XXX - mock_mset(0, 0, 64) -- full tile (left wall) - mock_mset(0, 1, 64) -- full tile - mock_mset(1, 1, 64) -- full tile - mock_mset(2, 0, 64) -- full tile - mock_mset(2, 1, 64) -- full tile (right wall) + mock_mset(0, 0, full_tile_id) -- full tile (left wall) + mock_mset(0, 1, full_tile_id) -- full tile + mock_mset(1, 1, full_tile_id) -- full tile + mock_mset(2, 0, full_tile_id) -- full tile + mock_mset(2, 1, full_tile_id) -- full tile (right wall) end) it('when stepping left and hitting the wall, preserve x and block', function () @@ -2975,8 +2977,8 @@ describe('player_char', function () before_each(function () -- X -- X - mock_mset(0, 1, 64) -- full tile (ground) - mock_mset(1, 0, 64) -- full tile (wall without ground below) + mock_mset(0, 1, full_tile_id) -- full tile (ground) + mock_mset(1, 0, full_tile_id) -- full tile (wall without ground below) end) -- it will fail until _compute_signed_distance_to_closest_ground @@ -3009,8 +3011,8 @@ describe('player_char', function () before_each(function () -- X -- = - mock_mset(0, 1, 70) -- bottom half-tile - mock_mset(1, 0, 64) -- full tile (head wall) + mock_mset(0, 1, half_tile_id) -- bottom half-tile + mock_mset(1, 0, full_tile_id) -- full tile (head wall) end) -- it will fail until _compute_signed_distance_to_closest_ground @@ -3046,8 +3048,8 @@ describe('player_char', function () before_each(function () -- / -- X - mock_mset(0, 1, 64) -- full tile (ground) - mock_mset(1, 0, 65) -- ascending slope 45 + mock_mset(0, 1, full_tile_id) -- full tile (ground) + mock_mset(1, 0, asc_slope_45_id) -- ascending slope 45 end) it('when stepping right from the bottom of the ascending slope, increment x and adjust y', function () @@ -3078,10 +3080,10 @@ describe('player_char', function () before_each(function () -- X X -- X/X - mock_mset(0, 0, 64) -- 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, 64) -- full tile (wall) - mock_mset(1, 1, 65) -- ascending slope 45 - mock_mset(2, 0, 64) -- full tile (wall) + mock_mset(0, 0, 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, full_tile_id) -- full tile (wall) + mock_mset(1, 1, asc_slope_45_id) -- ascending slope 45 + mock_mset(2, 0, 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 () @@ -3243,7 +3245,7 @@ describe('player_char', function () before_each(function () -- .X - mock_mset(1, 0, 64) -- full tile (act like a full ceiling if position is at bottom) + mock_mset(1, 0, full_tile_id) -- full tile (act like a full ceiling if position is at bottom) end) it('should return false for sensor position just above the bottom of the tile', function () @@ -3291,7 +3293,7 @@ describe('player_char', function () before_each(function () -- / - mock_mset(0, 0, 65) + mock_mset(0, 0, asc_slope_45_id) end) it('should return false for sensor position on the left of the tile', function () @@ -4016,7 +4018,7 @@ describe('player_char', function () before_each(function () -- X - mock_mset(0, 0, 64) -- full tile + mock_mset(0, 0, full_tile_id) -- full tile end) -- in the tests below, we can use pc_data.full/center_height_standing directly instead diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index aad546a0..d533bc15 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -31,15 +31,18 @@ require("engine/test/assertions") local integrationtest = require("engine/test/integrationtest") local itest_manager, integration_test = integrationtest.itest_manager, integrationtest.integration_test -local tile_data = require("data/tile_data") local tilemap = require("engine/data/tilemap") -- dsl interpretation requirements 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") +--#if busted +local tile_test_data = require("test_data/tile_test_data") +--#endif -- helper function to access stage_stage quickly if current state -- is stage, as it is not a singleton anymore @@ -84,8 +87,6 @@ local function generate_function_table(module, enum_types, prefix) local t = {} for type_name, enum_type in pairs(enum_types) do t[enum_type] = module[prefix..type_name] - printh("t[enum_type] = module[prefix..type_name] => t["..enum_type.."] = module["..prefix..".."..type_name.."]") - printh("t[enum_type]: "..dump(t[enum_type])) end return t end @@ -610,7 +611,9 @@ function itest_dsl_parser.create_itest(name, dsli) current_stage_state.player_char.control_mode = control_modes.puppet if dsli.stage_name == '#' then -- load tilemap data and build it from ascii - setup_map_data() +--#if busted + tile_test_data.setup() +--#endif dsli.tilemap:load() else -- load stage by name when api is ready @@ -625,7 +628,9 @@ function itest_dsl_parser.create_itest(name, dsli) if dsli.stage_name == '#' then -- clear tilemap and unload tilemap data tilemap.clear_map() - teardown_map_data() +--#if busted + tile_test_data.teardown() +--#endif end end end diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua index ce89e11d..954e2f50 100644 --- a/src/itest/itest_dsl_utest.lua +++ b/src/itest/itest_dsl_utest.lua @@ -11,8 +11,9 @@ local itest_manager, time_trigger, integration_test = get_members(integratio local itest_dsl = require("itest/itest_dsl") local gameplay_value_data, generate_function_table = get_members(itest_dsl, "gameplay_value_data", "generate_function_table") +-- get_members is convenient to hide underscores with proxy refs local eval_pc_bottom_pos, eval_pc_velocity, eval_pc_ground_spd, eval_pc_motion_state, eval_pc_slope = get_members(itest_dsl, - "eval_pc_bottom_pos", "eval_pc_velocity", "eval_pc_ground_spd", "eval_pc_motion_state", "eval_pc_slope") + "_eval_pc_bottom_pos", "_eval_pc_velocity", "_eval_pc_ground_spd", "_eval_pc_motion_state", "_eval_pc_slope") local command, expectation = get_members(itest_dsl, "command", "expectation") local dsl_itest, itest_dsl_parser = get_members(itest_dsl, @@ -22,7 +23,7 @@ local stage_state = require("ingame/stage_state") local picosonic_app = require("application/picosonic_app") local player_char = require("ingame/playercharacter") local pc_data = require("data/playercharacter_data") - +local tile_test_data = require("test_data/tile_test_data") describe('itest_dsl', function () @@ -216,7 +217,7 @@ describe('itest_dsl', function () end) it('should call warp_bottom_to on the current player character', function () - itest_dsl.execute_warp({vector(1, 3)}) + itest_dsl._execute_warp({vector(1, 3)}) assert.spy(player_char.warp_bottom_to).was_called(1) assert.spy(player_char.warp_bottom_to).was_called_with(match.ref(state.player_char), vector(1, 3)) @@ -227,13 +228,13 @@ describe('itest_dsl', function () describe('"execute_set', function () it('should set pc velocity to (1, -3)', function () - itest_dsl.execute_set({"pc_velocity", vector(1, -3)}) + itest_dsl._execute_set({"pc_velocity", vector(1, -3)}) assert.are_equal(vector(1, -3), state.player_char.velocity) end) it('should fail with unsupported gp_value_type for setting', function () assert.has_error(function () - itest_dsl.execute_set({"pc_slope", -2}) + itest_dsl._execute_set({"pc_slope", -2}) end, "itest_dsl.set_pc_slope is not defined") end) @@ -242,7 +243,7 @@ describe('itest_dsl', function () describe('execute_set_control_mode', function () it('should set the control mode', function () - itest_dsl.execute_set_control_mode({control_modes.puppet}) + itest_dsl._execute_set_control_mode({control_modes.puppet}) assert.are_equal(control_modes.puppet, state.player_char.control_mode) end) @@ -251,7 +252,7 @@ describe('itest_dsl', function () describe('execute_set_motion_mode', function () it('should set the motion mode', function () - itest_dsl.execute_set_motion_mode({motion_modes.debug}) + itest_dsl._execute_set_motion_mode({motion_modes.debug}) assert.are_equal(motion_modes.debug, state.player_char.motion_mode) end) @@ -260,7 +261,7 @@ describe('itest_dsl', function () describe('execute_move', function () it('should set the move intention of the current player character to the directional unit vector matching his horizontal direction', function () - itest_dsl.execute_move({horizontal_dirs.right}) + itest_dsl._execute_move({horizontal_dirs.right}) assert.are_equal(vector(1, 0), state.player_char.move_intention) end) @@ -270,7 +271,7 @@ describe('itest_dsl', function () it('should set the move intention of the current player character to vector zero', function () state.player_char.move_intention = vector(99, -99) - itest_dsl.execute_stop({}) + itest_dsl._execute_stop({}) assert.are_equal(vector.zero(), state.player_char.move_intention) end) @@ -279,7 +280,7 @@ describe('itest_dsl', function () describe('execute_jump', function () it('should set the jump intention and hold jump intention to true', function () - itest_dsl.execute_jump({}) + itest_dsl._execute_jump({}) assert.are_same({true, true}, {state.player_char.jump_intention, state.player_char.hold_jump_intention}) end) @@ -290,7 +291,7 @@ describe('itest_dsl', function () it('should set the hold jump intention to false', function () state.player_char.hold_jump_intention = true - itest_dsl.execute_stop_jump({}) + itest_dsl._execute_stop_jump({}) assert.is_false(state.player_char.hold_jump_intention) end) @@ -300,7 +301,7 @@ describe('itest_dsl', function () it('should set the simulated button down state to true', function () input.simulated_buttons_down[0][button_ids.x] = false - itest_dsl.execute_press({button_ids.x}) + itest_dsl._execute_press({button_ids.x}) assert.is_true(input.simulated_buttons_down[0][button_ids.x]) end) @@ -310,7 +311,7 @@ describe('itest_dsl', function () it('should set the simulated button down state to true', function () input.simulated_buttons_down[0][button_ids.up] = true - itest_dsl.execute_release({button_ids.up}) + itest_dsl._execute_release({button_ids.up}) assert.is_false(input.simulated_buttons_down[0][button_ids.up]) end) @@ -443,21 +444,21 @@ describe('itest_dsl', function () -- spying should be enough, but we stub so it's easier to call these functions -- without calling the symmetrical one (e.g. teardown may fail with nil reference -- if setup is not called first) - stub(_G, "setup_map_data") - stub(_G, "teardown_map_data") + stub(tile_test_data, "setup") + stub(tile_test_data, "teardown") end) teardown(function () - setup_map_data:revert() - teardown_map_data:revert() + tile_test_data.setup:revert() + tile_test_data.teardown:revert() end) after_each(function () itest_dsl_parser:init() flow:init() pico8:clear_map() - setup_map_data:clear() - teardown_map_data:clear() + tile_test_data.setup:clear() + tile_test_data.teardown:clear() end) describe('init', function () @@ -900,7 +901,7 @@ expect tilemap.clear_map:clear() end) - it('setup should call setup_map_data and load on the tilemap if custom stage definition', function () + it('setup should call tile_test_data.setup and load on the tilemap if custom stage definition', function () local dsli = dsl_itest() dsli.gamestate_type = ':stage' dsli.stage_name = "#" @@ -918,7 +919,7 @@ expect assert.are_equal(control_modes.puppet, state.player_char.control_mode) -- implementation - local s_data = assert.spy(setup_map_data) + local s_data = assert.spy(tile_test_data.setup) s_data.was_called(1) s_data.was_called_with() local s_load = assert.spy(tilemap.load) @@ -926,7 +927,7 @@ expect s_load.was_called_with(match.ref(dsli.tilemap)) end) - it('teardown should call clear_map and teardown_map_data if custom stage definition', function () + it('teardown should call clear_map and tile_test_data.teardown if custom stage definition', function () local dsli = dsl_itest() dsli.gamestate_type = ':stage' dsli.stage_name = "#" @@ -944,7 +945,7 @@ expect local s_clear = assert.spy(tilemap.clear_map) s_clear.was_called(1) s_clear.was_called_with() - local s_teardown = assert.spy(teardown_map_data) + local s_teardown = assert.spy(tile_test_data.teardown) s_teardown.was_called(1) s_teardown.was_called_with() end) diff --git a/src/itests/itestplayercharacter.lua b/src/itests/itestplayercharacter.lua index 90988296..4f230db1 100644 --- a/src/itests/itestplayercharacter.lua +++ b/src/itests/itestplayercharacter.lua @@ -6,10 +6,6 @@ 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 tile_data = require("data/tile_data") ---#if busted -local tile_test_data = require("test_data/tile_test_data") ---#endif local itest diff --git a/src/platformer/world_utest.lua b/src/platformer/world_utest.lua index 5f15bad7..62377f33 100644 --- a/src/platformer/world_utest.lua +++ b/src/platformer/world_utest.lua @@ -47,7 +47,7 @@ describe('world (with mock tiles data setup)', function () before_each(function () -- create an ascending slope 22.5 at (1, 1), i.e. (8, 14) to (15, 11) px - mock_mset(1, 1, 67) + mock_mset(1, 1, asc_slope_22_id) end) it('should return 3 on column 3', function () @@ -64,7 +64,7 @@ describe('world (with mock tiles data setup)', function () before_each(function () -- create a full tile at (1, 1), i.e. (8, 8) to (15, 15) px - mock_mset(1, 1, 64) + mock_mset(1, 1, full_tile_id) end) it('should return {false, nil} on (7, 7)', function () @@ -143,7 +143,7 @@ describe('world (with mock tiles data setup)', function () before_each(function () -- create an ascending slope at (1, 1), i.e. (8, 15) to (15, 8) px - mock_mset(1, 1, 65) + mock_mset(1, 1, asc_slope_45_id) end) it('should return {false, nil} on (8, 14)', function () diff --git a/src/test_data/tile_data_utest.lua b/src/test_data/tile_data_utest.lua deleted file mode 100644 index 93147b0e..00000000 --- a/src/test_data/tile_data_utest.lua +++ /dev/null @@ -1,38 +0,0 @@ -require("engine/test/bustedhelper") -require("data/tile_data") -local tile_test_data = require("test_data/tile_test_data") - -describe('tiledata', function () - - setup(function () - stub(tile_test_data, "setup") - stub(tile_test_data, "teardown") - end) - - teardown(function () - tile_test_data.setup:revert() - tile_test_data.teardown:revert() - end) - - after_each(function () - tile_test_data.setup:clear() - tile_test_data.teardown:clear() - end) - - describe('setup_map_data', function () - it('should call setup on tile_test_data (busted only)', function () - setup_map_data() - assert.spy(tile_test_data.setup).was_called(1) - assert.spy(tile_test_data.setup).was_called_with() - end) - end) - - describe('teardown_map_data', function () - it('should call teardown on tile_test_data (busted only)', function () - teardown_map_data() - assert.spy(tile_test_data.teardown).was_called(1) - assert.spy(tile_test_data.teardown).was_called_with() - end) - end) - -end) diff --git a/src/test_data/tile_test_data.lua b/src/test_data/tile_test_data.lua index 0f16596a..b01cbebf 100644 --- a/src/test_data/tile_test_data.lua +++ b/src/test_data/tile_test_data.lua @@ -5,44 +5,62 @@ local tile = require("platformer/tile") local collision_data = require("data/collision_data") local stub = require("luassert.stub") -local tile_test_data = {} - local height_array_init_mock +-- IDs of tiles used for tests only (black and white in spritesheet, never used in real game) +no_tile_id = 0 +full_tile_id = 32 +half_tile_id = 80 +flat_low_tile_id = 96 +bottom_right_quarter_tile_id = 64 +asc_slope_45_id = 112 +desc_slope_45_id = 116 +asc_slope_22_id = 113 + +-- symbol mapping for itests +-- (could also be used for utests instead of manual mock_mset, but need to extract parse_tilemap +-- from itest_dsl) +tile_symbol_to_ids = { + ['.'] = no_tile_id, -- empty + ['#'] = full_tile_id, -- full tile + ['='] = half_tile_id, -- half tile (4px high) + ['_'] = flat_low_tile_id, -- flat low tile (2px high) + ['r'] = bottom_right_quarter_tile_id, -- bottom-right quarter tile (4px high) + ['/'] = asc_slope_45_id, -- ascending slope 45 + ['\\'] = desc_slope_45_id, -- descending slope 45 + ['<'] = asc_slope_22_id, -- ascending slope 22.5 +} + +local tile_test_data = {} + function tile_test_data.setup() -- mock sprite flags fset(1, sprite_flags.collision, true) -- invalid tile (missing collision mask id location below) - fset(64, sprite_flags.collision, true) -- full tile - fset(65, sprite_flags.collision, true) -- ascending slope 45 - fset(66, sprite_flags.collision, true) -- descending slope 45 - fset(67, sprite_flags.collision, true) -- ascending slope 22.5 offset by 2 - fset(68, sprite_flags.collision, true) -- wavy horizontal almost full tile - fset(70, sprite_flags.collision, true) -- half-tile (bottom half) - fset(71, sprite_flags.collision, true) -- quarter-tile (bottom-right half) - fset(72, sprite_flags.collision, true) -- low-tile (bottom quarter) - fset(73, sprite_flags.collision, true) -- high-tile (3/4 filled) + fset(full_tile_id, sprite_flags.collision, true) -- full tile + fset(asc_slope_45_id, sprite_flags.collision, true) -- ascending slope 45 + fset(desc_slope_45_id, sprite_flags.collision, true) -- descending slope 45 + fset(asc_slope_22_id, sprite_flags.collision, true) -- ascending slope 22.5 offset by 2 + fset(half_tile_id, sprite_flags.collision, true) -- half-tile (bottom half) + fset(bottom_right_quarter_tile_id, sprite_flags.collision, true) -- quarter-tile (bottom-right half) + fset(flat_low_tile_id, sprite_flags.collision, true) -- low-tile (bottom quarter) -- mock height array _init so it doesn't have to dig in sprite data, inaccessible from busted height_array_init_mock = stub(tile.height_array, "_init", function (self, tile_data) local tile_mask_id_location = tile_data.id_loc - if tile_mask_id_location == collision_data.tiles_data[64].id_loc then + if tile_mask_id_location == collision_data.tiles_data[full_tile_id].id_loc then self._array = {8, 8, 8, 8, 8, 8, 8, 8} -- full tile - elseif tile_mask_id_location == collision_data.tiles_data[65].id_loc then + elseif tile_mask_id_location == collision_data.tiles_data[asc_slope_45_id].id_loc then self._array = {1, 2, 3, 4, 5, 6, 7, 8} -- ascending slope 45 - elseif tile_mask_id_location == collision_data.tiles_data[66].id_loc then + elseif tile_mask_id_location == collision_data.tiles_data[desc_slope_45_id].id_loc then self._array = {8, 7, 6, 5, 4, 3, 2, 1} -- descending slope 45 - elseif tile_mask_id_location == collision_data.tiles_data[67].id_loc then + elseif tile_mask_id_location == collision_data.tiles_data[asc_slope_22_id].id_loc then self._array = {2, 2, 3, 3, 4, 4, 5, 5} -- ascending slope 22.5 - elseif tile_mask_id_location == collision_data.tiles_data[68].id_loc then - self._array = {8, 8, 7, 6, 6, 7, 6, 7} -- wavy horizontal almost full tile - elseif tile_mask_id_location == collision_data.tiles_data[70].id_loc then + elseif tile_mask_id_location == collision_data.tiles_data[half_tile_id].id_loc then self._array = {4, 4, 4, 4, 4, 4, 4, 4} -- half-tile (bottom half) - elseif tile_mask_id_location == collision_data.tiles_data[71].id_loc then + elseif tile_mask_id_location == collision_data.tiles_data[bottom_right_quarter_tile_id].id_loc then self._array = {0, 0, 0, 0, 4, 4, 4, 4} -- quarter-tile (bottom-right quarter) - elseif tile_mask_id_location == collision_data.tiles_data[72].id_loc then + elseif tile_mask_id_location == collision_data.tiles_data[flat_low_tile_id].id_loc then self._array = {2, 2, 2, 2, 2, 2, 2, 2} -- low-tile (bottom quarter) - elseif tile_mask_id_location == collision_data.tiles_data[73].id_loc then - self._array = {6, 6, 6, 6, 6, 6, 6, 6} -- high-tile (3/4 filled) else self._array = "invalid" end @@ -68,6 +86,17 @@ function mock_mset(x, y, v) mset(x, y, v) end -return tile_test_data +--#endif + +-- prevent busted from parsing both versions of tile_test_data +--[[#pico8 +-- fallback implementation if busted symbol is not defined +-- (picotool fails on empty file due to empty self._tokens) +--#ifn busted +local tile_test_data = {"symbol tile_test_data is undefined"} --#endif + +--#pico8]] + +return tile_test_data From a9d8f1edc70be1799c0bcd39c7c56e54efd73777 Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 15:21:47 +0200 Subject: [PATCH 023/112] [PLAYER CHARACTER] Cleaned up update_ground_speed (no previous_ground_speed param) and fixed test considering now application order (slope factor, then intention) --- src/ingame/playercharacter.lua | 27 +++++++++++++-------------- src/ingame/playercharacter_utest.lua | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 58f16055..907dcea3 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -470,22 +470,21 @@ end -- update ground speed function player_char:_update_ground_speed() - -- In order to process move intention and slope effect as independently as possible, - -- we process intention first, then compute slope contribution based on the ground speed - -- *before* intention was processed. - -- We could do the opposite, but since _update_ground_speed_by_intention relies on clamping, - -- it would be a bit harder (we'd need both previous_ground_speed and some - -- fictive_future_ground_speed = previous_ground_speed + ground_speed_delta, then compute - -- some clamped_ground_speed_delta and reapply it to the actual self.ground_speed) - local previous_ground_speed = self.ground_speed - -- testing different order so friction applies and stops low slope - self:_update_ground_speed_by_slope(previous_ground_speed) - self:_update_ground_speed_by_intention(previous_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 + -- 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, + -- Progressive Ascending Steep Slope Factor feature won't be applied the first frame. + -- But it should be OK overall. + self:_update_ground_speed_by_slope() + self:_update_ground_speed_by_intention() self:_clamp_ground_speed() end -- update ground speed based on current slope -function player_char:_update_ground_speed_by_slope(previous_ground_speed) +function player_char:_update_ground_speed_by_slope() local is_ascending_slope = false if self.slope_angle ~= 0 then @@ -499,7 +498,7 @@ function player_char:_update_ground_speed_by_slope(previous_ground_speed) -- Resolves: character was suddenly stopped by longer slopes when starting ascension with low momentum, -- falling back to the flat ground behind, and repeating, causing a glitch-like oscillation local ascending_slope_factor = 1 - if previous_ground_speed ~= 0 and abs(self.slope_angle) >= pc_data.steep_slope_min_angle and sgn(previous_ground_speed) ~= sgn(self.slope_angle) then + if self.ground_speed ~= 0 and abs(self.slope_angle) >= pc_data.steep_slope_min_angle and sgn(self.ground_speed) ~= sgn(self.slope_angle) then is_ascending_slope = true local ascending_slope_duration = pc_data.progressive_ascending_slope_duration local progressive_ascending_slope_factor = 1 @@ -518,7 +517,7 @@ function player_char:_update_ground_speed_by_slope(previous_ground_speed) end -- update ground speed based on current move intention -function player_char:_update_ground_speed_by_intention(previous_ground_speed) +function player_char:_update_ground_speed_by_intention() if self.move_intention.x ~= 0 then if self.ground_speed == 0 or sgn(self.ground_speed) == sgn(self.move_intention.x) then diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index e1bce892..23d4759f 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1886,7 +1886,7 @@ describe('player_char', function () end) -- _update_platformer_motion_grounded - describe('_update_ground_speed', function () + describe('update_ground_speed', function () setup(function () -- the only reason we spy and not stub is to test the interface in the first test below @@ -1907,17 +1907,22 @@ describe('player_char', function () player_char._clamp_ground_speed:clear() end) - -- WIP since change of call order - - it('should counter the ground speed in the opposite direction of motion when moving upward a 45-degree slope', function () - pc:_update_ground_speed() + -- 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('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() - assert.are_equal(pc_data.ground_accel_frame2 - pc_data.slope_accel_factor_frame2 * sin(-1/8), pc.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) it('should update ground speed based on slope, then intention', function () @@ -1927,7 +1932,7 @@ describe('player_char', function () -- implementation 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), 2.5) + assert.spy(player_char._update_ground_speed_by_slope).was_called_with(match.ref(pc)) assert.spy(player_char._update_ground_speed_by_intention).was_called(1) assert.spy(player_char._update_ground_speed_by_intention).was_called_with(match.ref(pc)) assert.spy(player_char._clamp_ground_speed).was_called(1) From f1a4be001b9541534095baabac65444819325016 Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 17:29:31 +0200 Subject: [PATCH 024/112] [PLAYER CHARACTER] Transfer velocity along slope tangent to ground speed on landing ! Added vector_ext which increased token count from 9252 to 9526 (although we were already above the limit) ! --- pico-boots | 2 +- src/ingame/playercharacter.lua | 15 ++++-- src/ingame/playercharacter_utest.lua | 73 +++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/pico-boots b/pico-boots index 7396dcff..b170cfe8 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 7396dcffa67edb94cecd09ca82f5bd3ba0fae001 +Subproject commit b170cfe8680929c1dafc2eda2290883a250ffb9a diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 907dcea3..efaee838 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -2,6 +2,7 @@ require("engine/application/constants") require("engine/core/class") require("engine/core/helper") require("engine/core/math") +-- require("engine/core/vector_ext") --#if log local _logging = require("engine/debug/logging") --#endif @@ -363,6 +364,10 @@ function player_char:_check_escape_from_ground() end -- enter motion state, reset state vars appropriately +-- refactor: consider separate methods go_airborne or land +-- so you can pass more specific arguments +-- current, self.slope_angle must have been previously set when +-- entering ground state function player_char:_enter_motion_state(next_motion_state) if next_motion_state == motion_states.falling then -- we have just left the ground without jumping, enter falling state @@ -380,16 +385,20 @@ function player_char:_enter_motion_state(next_motion_state) elseif next_motion_state == motion_states.grounded then -- we have just reached the ground (and possibly escaped), -- reset values airborne vars - self.velocity.y = 0 -- no velocity retain yet on y + self.ground_speed = self.velocity:dot(vector.unit_from_angle(self.slope_angle)) + self:_clamp_ground_speed() self.has_jumped_this_frame = false -- optional since consumed immediately in _update_platformer_motion_airborne self.has_interrupted_jump = false self.anim_spr:play("idle") end + -- store previous compact state before changing motion state local was_compact = self:is_compact() + -- update motion state self.motion_state = next_motion_state + -- adjust center when switching compact mode if not was_compact and self:is_compact() then -- character became compact (e.g. crouching or start jumping), -- move it slightly down to keep center position continuity @@ -407,7 +416,7 @@ function player_char:_update_platformer_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.motion_state == motion_states.grounded then - self:_check_jump() -- this may change the motion state to air_spin + self:_check_jump() -- this may change the motion state to air_spin and affect branching below end if self:is_grounded() then @@ -431,7 +440,7 @@ 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(cos(self.slope_angle), -sin(self.slope_angle)) + self.velocity = self.ground_speed * vector.unit_from_angle(self.slope_angle) -- we can now update position and slope self.position = ground_motion_result.position diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 23d4759f..5ea59353 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1359,7 +1359,7 @@ describe('player_char', function () animated_sprite.play:clear() end) - it('should enter passed state: falling and reset ground-specific state vars', function () + it('should enter passed state: falling, reset ground-specific state vars, no animation change', function () -- character starts grounded pc:_enter_motion_state(motion_states.falling) @@ -1376,7 +1376,7 @@ describe('player_char', function () assert.spy(animated_sprite.play).was_not_called() end) - it('should enter passed state: air_spin and reset ground-specific state vars', function () + it('should enter passed state: air_spin, reset ground-specific state vars, play spin animation', function () -- character starts grounded pc:_enter_motion_state(motion_states.air_spin) @@ -1395,25 +1395,84 @@ describe('player_char', function () end) -- bugfix history: . - it('should enter passed state: grounded and reset speed y and has_interrupted_jump', function () + it('should enter passed state: grounded, reset has_jumped_this_frame/has_interrupted_jump', function () pc.motion_state = motion_states.falling pc:_enter_motion_state(motion_states.grounded) assert.are_same({ motion_states.grounded, - 0, false, false }, { pc.motion_state, - pc.velocity.y, pc.has_jumped_this_frame, pc.has_interrupted_jump }) - assert.spy(animated_sprite.play).was_called(1) - assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "idle") + end) + + it('(falling -> grounded, 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 + + pc:_enter_motion_state(motion_states.grounded) + + assert.are_equal(0, pc.ground_speed) + end) + + it('(falling -> grounded, 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 + + pc:_enter_motion_state(motion_states.grounded) + + assert.are_equal(2, pc.ground_speed) + end) + + it('(falling -> grounded, velocity X = 5 (over max) on flat ground) should transfer velocity X clamped to ground speed', function () + pc.motion_state = motion_states.falling + pc.velocity.x = pc_data.max_ground_speed + 2 + pc.velocity.y = 5 + + pc:_enter_motion_state(motion_states.grounded) + + assert.are_equal(pc_data.max_ground_speed, pc.ground_speed) + end) + + it('(falling -> grounded, 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/12 -- 30 deg/360 deg + + pc:_enter_motion_state(motion_states.grounded) + + assert.are_equal(1, pc.ground_speed) + end) + + it('(falling -> grounded, 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 = 0.125 -- 45 deg/360 deg + + pc:_enter_motion_state(motion_states.grounded) + + assert.is_true(almost_eq_with_message(0, pc.ground_speed)) + end) + + it('(falling -> grounded, 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 = 0.125 -- 45 deg/360 deg + + pc:_enter_motion_state(motion_states.grounded) + + assert.is_true(almost_eq_with_message(1/sqrt(2), pc.ground_speed)) end) it('should adjust center position down when becoming compact', function () From ee348b484818a7afa2a90248e456394fe7aae3d3 Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 17:31:18 +0200 Subject: [PATCH 025/112] [CI] Travis: build game before tests so we always have build info even if a few tests fail --- .travis.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index cc7b709e..06f1ebbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,14 +52,17 @@ before_script: - ln -s "$(pwd)/tool/picotool-master/p8tool" "$HOME/.local/bin/p8tool" script: - # test (including rendered headless itests thanks to ENABLE_RENDER=1) - - ./test.sh -m all - # coverage - - bash <(curl -s https://codecov.io/bash) # build game and itest to make sure everything works fine + # (do it before tests although busted tests can be run independently, + # because sometimes a few minor tests fail but we still want to know + # if we can build, and we are under the token limit) - ./build_game.sh debug - ./build_game.sh release - ./build_itest.sh + # test (including rendered headless itests thanks to ENABLE_RENDER=1) + - ./test.sh -m all + # coverage + - bash <(curl -s https://codecov.io/bash) deploy: provider: releases From 66f4a77c7437ee0c502a362754c1884dfa33b57f Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 18:47:37 +0200 Subject: [PATCH 026/112] [PLAYER CHARACTER] Re-added missing vector_ext [ENGINE] Updated pico-boots (Travis will now fail on too many tokens) --- pico-boots | 2 +- src/ingame/playercharacter.lua | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pico-boots b/pico-boots index b170cfe8..24cdbec0 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit b170cfe8680929c1dafc2eda2290883a250ffb9a +Subproject commit 24cdbec0ed27fbfa737a5d98c35e5e66a6555ec6 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index efaee838..0bbfd5e0 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -2,7 +2,7 @@ require("engine/application/constants") require("engine/core/class") require("engine/core/helper") require("engine/core/math") --- require("engine/core/vector_ext") +require("engine/core/vector_ext") --#if log local _logging = require("engine/debug/logging") --#endif @@ -383,10 +383,11 @@ function player_char:_enter_motion_state(next_motion_state) self.should_jump = false self.anim_spr:play("spin") elseif next_motion_state == motion_states.grounded then - -- we have just reached the ground (and possibly escaped), - -- reset values airborne vars + -- transfer part of velocity tangential to slope to ground speed self.ground_speed = self.velocity:dot(vector.unit_from_angle(self.slope_angle)) self:_clamp_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.has_interrupted_jump = false self.anim_spr:play("idle") From c8804755f69f173175f0648ad2d6886c985a0f5d Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 18:58:19 +0200 Subject: [PATCH 027/112] [UI] Adapted requires to overlay and label now in their own files --- pico-boots | 2 +- src/ingame/stage_state.lua | 4 ++-- src/ingame/stage_state_utest.lua | 17 +++++++++-------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pico-boots b/pico-boots index 24cdbec0..02b69b06 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 24cdbec0ed27fbfa737a5d98c35e5e66a6555ec6 +Subproject commit 02b69b063e0b0922aeb4a930bee2ab1bcc396416 diff --git a/src/ingame/stage_state.lua b/src/ingame/stage_state.lua index a63f095e..7eebad8c 100644 --- a/src/ingame/stage_state.lua +++ b/src/ingame/stage_state.lua @@ -4,7 +4,7 @@ require("engine/core/math") require("engine/render/color") local flow = require("engine/application/flow") local gamestate = require("engine/application/gamestate") -local ui = require("engine/ui/ui") +local overlay = require("engine/ui/overlay") local player_char = require("ingame/playercharacter") local stage_data = require("data/stage_data") @@ -40,7 +40,7 @@ function stage_state:_init() self.camera_pos = vector.zero() -- title overlay - self.title_overlay = ui.overlay(0) + self.title_overlay = overlay(0) end function stage_state:on_enter() diff --git a/src/ingame/stage_state_utest.lua b/src/ingame/stage_state_utest.lua index f22f6896..faac04a1 100644 --- a/src/ingame/stage_state_utest.lua +++ b/src/ingame/stage_state_utest.lua @@ -3,7 +3,8 @@ local stage_state = require("ingame/stage_state") local flow = require("engine/application/flow") local gamestate = require("engine/application/gamestate") -local ui = require("engine/ui/ui") +local overlay = require("engine/ui/overlay") +local label = require("engine/ui/label") local picosonic_app = require("application/picosonic_app") local stage_data = require("data/stage_data") @@ -43,7 +44,7 @@ describe('stage_state', function () nil, false, vector.zero(), - ui.overlay(0) + overlay(0) }, { state.type, @@ -114,19 +115,19 @@ describe('stage_state', function () describe('on_exit', function () setup(function () - stub(ui.overlay, "clear_labels") + stub(overlay, "clear_labels") stub(picosonic_app, "stop_all_coroutines") stub(stage_state, "stop_bgm") end) teardown(function () - ui.overlay.clear_labels:revert() + overlay.clear_labels:revert() picosonic_app.stop_all_coroutines:revert() stage_state.stop_bgm:revert() end) after_each(function () - ui.overlay.clear_labels:clear() + overlay.clear_labels:clear() picosonic_app.stop_all_coroutines:clear() stage_state.stop_bgm:clear() end) @@ -148,7 +149,7 @@ describe('stage_state', function () end) it('should call title_overlay:clear_labels', function () - local s = assert.spy(ui.overlay.clear_labels) + local s = assert.spy(overlay.clear_labels) s.was_called(1) s.was_called_with(match.ref(state.title_overlay)) end) @@ -488,7 +489,7 @@ describe('stage_state', function () for i = 1, stage_data.show_stage_title_delay * state.app.fps - 1 do coresume(on_show_stage_title_async, state) end - assert.are_equal(ui.label(state.curr_stage_data.title, vector(50, 30), colors.white), state.title_overlay.labels["title"]) + assert.are_equal(label(state.curr_stage_data.title, vector(50, 30), colors.white), state.title_overlay.labels["title"]) -- reach last frame now to check if label just disappeared coresume(on_show_stage_title_async, state) @@ -508,7 +509,7 @@ describe('stage_state', function () map_stub = stub(_G, "map") spy.on(stage_state, "render_environment") player_char_render_stub = stub(player_char, "render") - title_overlay_draw_labels_stub = stub(ui.overlay, "draw_labels") + title_overlay_draw_labels_stub = stub(overlay, "draw_labels") end) teardown(function () From 38b688ead8c8bdc0c58e311adfdd3294c2b81bcb Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 19:03:31 +0200 Subject: [PATCH 028/112] [TOKEN] Removed unused require ui in titlemenu, tokens 9552 -> 8881 --- src/menu/titlemenu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu/titlemenu.lua b/src/menu/titlemenu.lua index 145fad2f..31d92d6b 100644 --- a/src/menu/titlemenu.lua +++ b/src/menu/titlemenu.lua @@ -3,7 +3,7 @@ require("engine/render/color") local input = require("engine/input/input") local flow = require("engine/application/flow") local gamestate = require("engine/application/gamestate") -local ui = require("engine/ui/ui") +-- local ui = require("engine/ui/ui") local titlemenu = derived_class(gamestate) From c37b170bb918753ca564c28fd7484062593eb2c8 Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 20:44:39 +0200 Subject: [PATCH 029/112] [TOKEN] Minor token reduction in player character -> 8881 --- src/ingame/playercharacter.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 0bbfd5e0..90a6f2e4 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -1099,8 +1099,7 @@ function player_char:_next_air_step(direction, ref_motion_result) -- it is still technically considered in the air -- if this step is blocked by landing, there is no extra motion, -- but character will enter grounded state - ref_motion_result.is_landing = true - ref_motion_result.slope_angle = next_slope_angle + ref_motion_result.is_landing, ref_motion_result.slope_angle = true, next_slope_angle else ref_motion_result.is_blocked_by_wall = true log("is blocked by wall", "trace") @@ -1108,8 +1107,7 @@ function player_char:_next_air_step(direction, ref_motion_result) elseif signed_distance_to_closest_ground > 0 then -- 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 = false - ref_motion_result.slope_angle = nil + ref_motion_result.is_landing, ref_motion_result.slope_angle = false--, nil elseif ref_motion_result.is_landing then -- if we enter this, direction must be horizontal, so update slope angle with new ground ref_motion_result.slope_angle = next_slope_angle @@ -1173,8 +1171,8 @@ end function player_char:_update_velocity_debug() -- update velocity from input -- in debug mode, cardinal speeds are independent and max speed applies to each - self:_update_velocity_component_debug("x") - self:_update_velocity_component_debug("y") + self:_update_velocity_component_debug "x" + self:_update_velocity_component_debug "y" end --#endif From a2b54960b098748592af7e53107707a23e3744ac Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 21:07:55 +0200 Subject: [PATCH 030/112] [TOKEN] Player character now uses engine common --- pico-boots | 2 +- src/ingame/playercharacter.lua | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pico-boots b/pico-boots index 02b69b06..1177ee51 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 02b69b063e0b0922aeb4a930bee2ab1bcc396416 +Subproject commit 1177ee51e095afb00cb6635746d8f203143f9cac diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 90a6f2e4..325d33b4 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -1,8 +1,4 @@ -require("engine/application/constants") -require("engine/core/class") -require("engine/core/helper") -require("engine/core/math") -require("engine/core/vector_ext") +require("engine/common") --#if log local _logging = require("engine/debug/logging") --#endif From c31f91201d71bbcb883b612c9ce56407fd32308a Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 23:00:21 +0200 Subject: [PATCH 031/112] [TOKEN] Added require("engine/common") to main scripts Removed it in other scripts Token -> 8867 --- pico-boots | 2 +- src/ingame/playercharacter.lua | 1 - src/itest_main.lua | 1 + src/main.lua | 4 +++- src/test_data/tile_test_data.lua | 4 +++- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pico-boots b/pico-boots index 1177ee51..3c4fa110 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 1177ee51e095afb00cb6635746d8f203143f9cac +Subproject commit 3c4fa110b5cb5d2ba8056cbc4064736b332e75f4 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 325d33b4..ae79b0e9 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -1,4 +1,3 @@ -require("engine/common") --#if log local _logging = require("engine/debug/logging") --#endif diff --git a/src/itest_main.lua b/src/itest_main.lua index a26d8745..a6e8057a 100644 --- a/src/itest_main.lua +++ b/src/itest_main.lua @@ -2,6 +2,7 @@ -- must require at main top, to be used in any required modules from here require("engine/pico8/api") +require("engine/common") local integrationtest = require("engine/test/integrationtest") local itest_manager = integrationtest.itest_manager diff --git a/src/main.lua b/src/main.lua index b99c905d..0e386a76 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,7 +1,9 @@ -- main entry file for the game --- we must require engine/pico8/api at the top of our main.lua, so API bridges apply to all modules +-- must require at main top, to be used in any required modules from here require("engine/pico8/api") +require("engine/common") + -- 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") diff --git a/src/test_data/tile_test_data.lua b/src/test_data/tile_test_data.lua index b01cbebf..82b4a1bb 100644 --- a/src/test_data/tile_test_data.lua +++ b/src/test_data/tile_test_data.lua @@ -1,6 +1,8 @@ --#if busted -require("engine/test/pico8api") +-- pico8api should have been required in an including script, +-- since we are used busted, hence bustedhelper + local tile = require("platformer/tile") local collision_data = require("data/collision_data") local stub = require("luassert.stub") From 12cfbd88a915c4d06da3a8d1b2e9364078bfb10f Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 23:04:18 +0200 Subject: [PATCH 032/112] [TEST] chmod +x build_pico8_utests.sh --- build_pico8_utests.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 build_pico8_utests.sh diff --git a/build_pico8_utests.sh b/build_pico8_utests.sh old mode 100644 new mode 100755 From 932bc259e5d065d626a6c5392ff98d1cb1d337e3 Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 23:21:09 +0200 Subject: [PATCH 033/112] [TEST] Fixed pico8 utests - chmod +x build_pico8_utests.sh and run_pico8_utests.sh - require pico8/api and engine/common in utest_main - fixed data tests in utestdata to match new tiles (real and proto) --- run_pico8_utests.sh | 2 +- src/utest_main.lua | 4 ++++ src/utests/utestdata.lua | 33 +++++++++++++++++---------------- 3 files changed, 22 insertions(+), 17 deletions(-) mode change 100644 => 100755 run_pico8_utests.sh diff --git a/run_pico8_utests.sh b/run_pico8_utests.sh old mode 100644 new mode 100755 index a96d4868..c0bf5680 --- a/run_pico8_utests.sh +++ b/run_pico8_utests.sh @@ -7,7 +7,7 @@ cartridge_stem="picosonic_pico8_utests_all" version="3.0" -run_cmd="pico8_0.1.11g_dev -run build/${cartridge_stem}_v${version}_debug.p8 -screenshot_scale 4 -gif_scale 4 $@" +run_cmd="pico8 -run build/${cartridge_stem}_v${version}_debug.p8 -screenshot_scale 4 -gif_scale 4 $@" # 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/src/utest_main.lua b/src/utest_main.lua index cf6f80f9..2f272208 100644 --- a/src/utest_main.lua +++ b/src/utest_main.lua @@ -3,6 +3,10 @@ -- otherwise busted tests should be enough -- each utest should be put inside the src/utests folder with the name utest{something}.lua +-- must require at main top, to be used in any required modules from here +require("engine/pico8/api") +require("engine/common") + local unittest = require("engine/test/unittest") local utest_manager, unit_test = unittest.utest_manager, unittest.unit_test -- tag to add require for pico8 utests files (should be in utests/) diff --git a/src/utests/utestdata.lua b/src/utests/utestdata.lua index c8975120..a685a101 100644 --- a/src/utests/utestdata.lua +++ b/src/utests/utestdata.lua @@ -11,33 +11,34 @@ local collision_data = require("data/collision_data") -- after minification if keys are not protected with ["key"] syntax) local playercharacter_data = require("data/playercharacter_data") -check('sprite_id_location(0, 4) should have collision flag set', function () - local sprite_id = sprite_id_location(0, 4):to_sprite_id() - assert(fget(sprite_id, sprite_flags.collision), "sprite_id_location(0, 4) has collision flag unset") +check('sprite_id_location(1, 3) should have collision flag set', function (utest_name) + local sprite_id = sprite_id_location(1, 3):to_sprite_id() + assert(fget(sprite_id, sprite_flags.collision), "sprite_id_location(0, 4) has collision flag unset", utest_name) end) -check('sprite_id_location(0, 4) should have collision mask id set to location below', function () - local sprite_id = sprite_id_location(0, 4):to_sprite_id() - assert(collision_data.tiles_data[sprite_id] == tile_data(sprite_id_location(0, 5), 0)) +check('sprite_id_location(1, 3) should have collision mask id set to location above, angle atan2(8, 2)', function (utest_name) + local sprite_id = sprite_id_location(1, 3):to_sprite_id() + assert(collision_data.tiles_data[sprite_id] == tile_data(sprite_id_location(1, 2), atan2(8, 2)), utest_name) end) -check('. height_array._fill_array on sprite_id_location(0, 5) should fill the array with tile mask data: full', function () +check('height_array._fill_array on sprite_id_location(2, 3) should fill the array with tile mask data: full', function (utest_name) local array = {} - height_array._fill_array(array, sprite_id_location(0, 5)) - assert(are_same_with_message({8, 8, 8, 8, 8, 8, 8, 8}, array)) + height_array._fill_array(array, sprite_id_location(2, 3)) + assert(are_same_with_message({8, 8, 8, 8, 8, 8, 8, 8}, array), utest_name) end) -- bugfix history: after switching to pink transparency, all my tiles became square blocks -check('= height_array._fill_array on sprite_id_location(0, 5) the array with tile mask data: descending slope 45', function () +-- warning: it's a proto tile, if you strip it from final build later, test another tile instead +check('= height_array._fill_array on sprite_id_location(1, 7) the array with tile mask data: descending slope 45', function (utest_name) local array = {} - height_array._fill_array(array, sprite_id_location(1, 5)) - assert(are_same_with_message({1, 2, 3, 4, 5, 6, 7, 8}, array)) + height_array._fill_array(array, sprite_id_location(1, 7)) + assert(are_same_with_message({1, 2, 3, 4, 5, 6, 7, 8}, array), utest_name) end) -check('sonic_sprite_data_table preserved key "idle"', function () - assert(playercharacter_data.sonic_sprite_data_table["idle"] ~= nil) +check('sonic_sprite_data_table preserved key "idle"', function (utest_name) + assert(playercharacter_data.sonic_sprite_data_table["idle"] ~= nil, utest_name) end) -check('sonic_animated_sprite_data_table preserved key "idle"', function () - assert(playercharacter_data.sonic_animated_sprite_data_table["idle"] ~= nil) +check('sonic_animated_sprite_data_table preserved key "idle"', function (utest_name) + assert(playercharacter_data.sonic_animated_sprite_data_table["idle"] ~= nil, utest_name) end) From fcc4e299663ab081713b6cf2cc6e822b5dd1211c Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 23:42:26 +0200 Subject: [PATCH 034/112] [TOKEN] -> 8813. Removed require common modules from all scripts --- pico-boots | 2 +- src/application/picosonic_app.lua | 2 -- src/data/collision_data.lua | 1 - src/ingame/playercharacter_utest.lua | 1 - src/ingame/stage_state.lua | 2 -- src/itest/itest_dsl.lua | 1 - src/itest/itest_dsl_utest.lua | 2 -- src/menu/credits.lua | 1 - src/menu/titlemenu.lua | 1 - src/platformer/tile.lua | 2 -- src/platformer/tile_utest.lua | 1 - src/sandbox.lua | 3 --- src/utests/utestdata.lua | 1 - 13 files changed, 1 insertion(+), 19 deletions(-) diff --git a/pico-boots b/pico-boots index 3c4fa110..99e64ea8 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 3c4fa110b5cb5d2ba8056cbc4064736b332e75f4 +Subproject commit 99e64ea8c311ff6bbdd491b772e5d8650433e8cb diff --git a/src/application/picosonic_app.lua b/src/application/picosonic_app.lua index f0537846..7e51865e 100644 --- a/src/application/picosonic_app.lua +++ b/src/application/picosonic_app.lua @@ -1,9 +1,7 @@ -- custom game application -- used by main and itest_main -require("engine/application/constants") local gameapp = require("engine/application/gameapp") -require("engine/core/class") local input = require("engine/input/input") --#if tuner diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua index 5e1600b5..99824c98 100644 --- a/src/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -1,4 +1,3 @@ -require("engine/core/math") local tile = require("platformer/tile") local tile_data = tile.tile_data diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 5ea59353..e0585233 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1,5 +1,4 @@ require("engine/test/bustedhelper") -require("engine/core/math") local animated_sprite = require("engine/render/animated_sprite") local player_char = require("ingame/playercharacter") diff --git a/src/ingame/stage_state.lua b/src/ingame/stage_state.lua index 7eebad8c..35c342b5 100644 --- a/src/ingame/stage_state.lua +++ b/src/ingame/stage_state.lua @@ -1,6 +1,4 @@ -require("engine/core/class") require("engine/core/coroutine") -require("engine/core/math") require("engine/render/color") local flow = require("engine/application/flow") local gamestate = require("engine/application/gamestate") diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index d533bc15..0ad2ee5a 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -26,7 +26,6 @@ expect gp_value_type expect a gameplay value to be equal to (...) --]] -require("engine/core/helper") require("engine/test/assertions") local integrationtest = require("engine/test/integrationtest") local itest_manager, integration_test = integrationtest.itest_manager, integrationtest.integration_test diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua index 954e2f50..e8267a8d 100644 --- a/src/itest/itest_dsl_utest.lua +++ b/src/itest/itest_dsl_utest.lua @@ -1,6 +1,4 @@ require("engine/test/bustedhelper") -require("engine/core/helper") -require("engine/core/math") local flow = require("engine/application/flow") local tilemap = require("engine/data/tilemap") local input = require("engine/input/input") diff --git a/src/menu/credits.lua b/src/menu/credits.lua index db443730..5bbeaa98 100644 --- a/src/menu/credits.lua +++ b/src/menu/credits.lua @@ -1,5 +1,4 @@ require("engine/application/flow") -require("engine/core/class") require("engine/render/color") local gamestate = require("engine/application/gamestate") diff --git a/src/menu/titlemenu.lua b/src/menu/titlemenu.lua index 31d92d6b..43fccc1a 100644 --- a/src/menu/titlemenu.lua +++ b/src/menu/titlemenu.lua @@ -1,4 +1,3 @@ -require("engine/core/class") require("engine/render/color") local input = require("engine/input/input") local flow = require("engine/application/flow") diff --git a/src/platformer/tile.lua b/src/platformer/tile.lua index c41529bd..873ba9a0 100644 --- a/src/platformer/tile.lua +++ b/src/platformer/tile.lua @@ -1,5 +1,3 @@ -require("engine/core/class") - local tile = {} local tile_data = new_struct() diff --git a/src/platformer/tile_utest.lua b/src/platformer/tile_utest.lua index 5899808a..1ddba6d5 100644 --- a/src/platformer/tile_utest.lua +++ b/src/platformer/tile_utest.lua @@ -1,5 +1,4 @@ require("engine/test/bustedhelper") -require("engine/core/math") local tile = require("platformer/tile") local tile_data = tile.tile_data local height_array = tile.height_array diff --git a/src/sandbox.lua b/src/sandbox.lua index 4023cd81..afa08bdd 100644 --- a/src/sandbox.lua +++ b/src/sandbox.lua @@ -1,6 +1,3 @@ -require("engine/core/math") -require("engine/core/helper") - -- caveats -- syntax error: malformed number near 27..d diff --git a/src/utests/utestdata.lua b/src/utests/utestdata.lua index a685a101..c0be4ce7 100644 --- a/src/utests/utestdata.lua +++ b/src/utests/utestdata.lua @@ -1,5 +1,4 @@ require("engine/test/unittest") -require("engine/core/math") local tile = require("platformer/tile") local height_array = tile.height_array local tile_data = tile.tile_data From 490bf17e8d3363abec36f056cc64f9ecfc6b68a1 Mon Sep 17 00:00:00 2001 From: huulong Date: Thu, 30 Jul 2020 23:48:58 +0200 Subject: [PATCH 035/112] [TOKEN] -> 8792. Removed require color (now common) in scripts --- pico-boots | 2 +- src/data/playercharacter_data.lua | 1 - src/ingame/stage_state.lua | 1 - src/menu/credits.lua | 1 - src/menu/titlemenu.lua | 1 - src/resources/visual.lua | 1 - 6 files changed, 1 insertion(+), 6 deletions(-) diff --git a/pico-boots b/pico-boots index 99e64ea8..97371526 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 99e64ea8c311ff6bbdd491b772e5d8650433e8cb +Subproject commit 97371526bc529e30be5cc20949861ac61759314f diff --git a/src/data/playercharacter_data.lua b/src/data/playercharacter_data.lua index 937cd5e0..9b9d7f3b 100644 --- a/src/data/playercharacter_data.lua +++ b/src/data/playercharacter_data.lua @@ -1,4 +1,3 @@ -require("engine/render/color") local sprite_data = require("engine/render/sprite_data") local animated_sprite_data = require("engine/render/animated_sprite_data") diff --git a/src/ingame/stage_state.lua b/src/ingame/stage_state.lua index 35c342b5..5c0fa291 100644 --- a/src/ingame/stage_state.lua +++ b/src/ingame/stage_state.lua @@ -1,5 +1,4 @@ require("engine/core/coroutine") -require("engine/render/color") local flow = require("engine/application/flow") local gamestate = require("engine/application/gamestate") local overlay = require("engine/ui/overlay") diff --git a/src/menu/credits.lua b/src/menu/credits.lua index 5bbeaa98..151d05af 100644 --- a/src/menu/credits.lua +++ b/src/menu/credits.lua @@ -1,5 +1,4 @@ require("engine/application/flow") -require("engine/render/color") local gamestate = require("engine/application/gamestate") diff --git a/src/menu/titlemenu.lua b/src/menu/titlemenu.lua index 43fccc1a..3ae5d61a 100644 --- a/src/menu/titlemenu.lua +++ b/src/menu/titlemenu.lua @@ -1,4 +1,3 @@ -require("engine/render/color") local input = require("engine/input/input") local flow = require("engine/application/flow") local gamestate = require("engine/application/gamestate") diff --git a/src/resources/visual.lua b/src/resources/visual.lua index fd6c957c..599eb46d 100644 --- a/src/resources/visual.lua +++ b/src/resources/visual.lua @@ -1,4 +1,3 @@ -require("engine/render/color") local sprite_data = require("engine/render/sprite_data") local visual = {} From 742169edeb624344d0a0fc30de1d16911d0efdb4 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 31 Jul 2020 02:09:48 +0200 Subject: [PATCH 036/112] [ENGINE] Updated pico-boots (slight token reduction -> 8788, README) --- pico-boots | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pico-boots b/pico-boots index 97371526..cb69359c 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 97371526bc529e30be5cc20949861ac61759314f +Subproject commit cb69359c217cdb59bec8c3ed8424f4a40c42a52b From 40babc8776e3f1aee822188837e39d600550a6c5 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 15:44:13 +0200 Subject: [PATCH 037/112] [PLAYER CHARACTER] Fixed Sonic losing momentum when landing on ascending slope due to being considered blocked by wall. This was done by: - adding ground snapping (step up) on landing too - (for simplification) merging just landing on ground and landing inside ground then escape (this changes behavior by 1 frame in perfect landing situations, for the best) Known issue: Sonic may keep air spinning when landing on a descending slope and continuing to go forward --- pico-boots | 2 +- src/ingame/playercharacter.lua | 113 +++++++++------- src/ingame/playercharacter_utest.lua | 195 ++++++++++++++++++++++++--- 3 files changed, 242 insertions(+), 68 deletions(-) diff --git a/pico-boots b/pico-boots index cb69359c..2135be4e 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit cb69359c217cdb59bec8c3ed8424f4a40c42a52b +Subproject commit 2135be4e88beeee82d0498a503b716c214fe2445 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index ae79b0e9..454214cf 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -346,13 +346,12 @@ end function player_char:_check_escape_from_ground() local query_info = self:_compute_ground_sensors_signed_distance(self.position) local signed_distance_to_closest_ground, next_slope_angle = query_info.signed_distance, query_info.slope_angle - local should_escape = signed_distance_to_closest_ground < 0 and abs(signed_distance_to_closest_ground) <= pc_data.max_ground_escape_height - if should_escape then + if signed_distance_to_closest_ground <= 0 and - signed_distance_to_closest_ground <= pc_data.max_ground_escape_height then + -- character is either just touching ground (signed_distance_to_closest_ground == 0) + -- or inside ground: + -- - snap character up to ground top (it does nothing if already touching ground) + -- - set slope angle to new ground self.position.y = self.position.y + signed_distance_to_closest_ground - end - if signed_distance_to_closest_ground == 0 or should_escape then - -- character was either touching ground, or inside it and escaped - -- so update his slope angle self.slope_angle = next_slope_angle end return signed_distance_to_closest_ground <= 0 @@ -378,7 +377,7 @@ function player_char:_enter_motion_state(next_motion_state) self.should_jump = false self.anim_spr:play("spin") elseif next_motion_state == motion_states.grounded then - -- transfer part of velocity tangential to slope to ground speed + -- Momentum: transfer part of velocity tangential to slope to ground speed (self.slope_angle must have been set previously) self.ground_speed = self.velocity:dot(vector.unit_from_angle(self.slope_angle)) self:_clamp_ground_speed() -- we have just reached the ground (and possibly escaped), @@ -694,13 +693,17 @@ function player_char:_next_ground_step(horizontal_dir, ref_motion_result) -- check if next position is inside/above ground local query_info = self:_compute_ground_sensors_signed_distance(next_position_candidate) 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 + + -- merge < 0 and == 0 cases together to spare tokens + -- when 0, next_position_candidate.y will simpy not change + if signed_distance_to_closest_ground <= 0 then -- position is inside ground, check if we can step up during this step - local penetration_height = - signed_distance_to_closest_ground - if penetration_height <= pc_data.max_ground_escape_height then + -- refactor: code is similar to _check_escape_from_ground and above all _next_air_step + if - signed_distance_to_closest_ground <= pc_data.max_ground_escape_height then -- step up - next_position_candidate.y = next_position_candidate.y - penetration_height - -- if we left the ground during a previous step, cancel that (step up land, very rare) + next_position_candidate.y = next_position_candidate.y + signed_distance_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 @@ -710,6 +713,7 @@ function player_char:_next_ground_step(horizontal_dir, ref_motion_result) 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) if signed_distance_to_closest_ground <= pc_data.max_ground_snap_height then -- step down next_position_candidate.y = next_position_candidate.y + signed_distance_to_closest_ground @@ -726,10 +730,6 @@ function player_char:_next_ground_step(horizontal_dir, ref_motion_result) -- and applying it this frame ref_motion_result.is_falling = true end - else - -- step flat - -- if character left the ground during a previous step, cancel that (very rare) - ref_motion_result.is_falling = false end if not ref_motion_result.is_blocked then @@ -747,6 +747,8 @@ function player_char:_next_ground_step(horizontal_dir, ref_motion_result) -- 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 @@ -1064,7 +1066,8 @@ function player_char:_next_air_step(direction, ref_motion_result) log("step_vec: "..step_vec, "trace") log("next_position_candidate: "..next_position_candidate, "trace") - -- we can only hit walls or the ground when moving left, right or down + -- 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 @@ -1074,39 +1077,42 @@ function player_char:_next_air_step(direction, ref_motion_result) log("signed_distance_to_closest_ground: "..signed_distance_to_closest_ground, "trace") - -- check if the character has hit a ground or a wall - if signed_distance_to_closest_ground < 0 then - -- we do not activate step up during air motion, so any pixel above the character's bottom - -- is considered a hard obstacle - -- however, if we want to allow jump from an ascending sheer angle directly onto a platform, - -- as suggested by the SPG (http://info.sonicretro.org/SPG:Solid_Tiles#Ceiling_Sensors_.28C_and_D.29) - -- where ground detection from the air is done when moving downward or when moving upward, but faster horizontally - -- than vertically, then we would not only need to add the x vs y spd check but also enable *snap* to ground - -- from the last step position, since we may miss a few pixels from here to reach the ground - -- (otherwise, the obstacle may was well be considered as a wall, as we're doing now) - -- Then we would have a check more symmetrical to below, with - -- `if self.velocity.y > 0 or abs(self.velocity.x) > abs(self.velocity.y)` - -- Depending on the direction, we consider we were blocked by either a ceiling or a wall - if direction == directions.down then - -- 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 - -- 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 grounded state - ref_motion_result.is_landing, ref_motion_result.slope_angle = true, next_slope_angle - else - ref_motion_result.is_blocked_by_wall = true - log("is blocked by wall", "trace") + -- 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 touching or entering 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 grounded state + ref_motion_result.is_landing, ref_motion_result.slope_angle = true, next_slope_angle + log("is landing, setting slope angle to "..next_slope_angle, "trace") + else + ref_motion_result.is_blocked_by_wall = true + log("is blocked by wall", "trace") + end + elseif signed_distance_to_closest_ground > 0 then + -- 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 end - elseif signed_distance_to_closest_ground > 0 then - -- 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 - elseif ref_motion_result.is_landing then - -- if we enter this, direction must be horizontal, so update slope angle with new ground - ref_motion_result.slope_angle = next_slope_angle - log("is landing, setting slope angle to "..next_slope_angle, "trace") end end @@ -1118,13 +1124,18 @@ function player_char:_next_air_step(direction, ref_motion_result) -- then there is no need to check further, though -- 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 not ref_motion_result.is_blocked_by_wall and - (self.velocity.y < 0 or abs(self.velocity.x) > abs(self.velocity.y)) then + -- 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. + if not ref_motion_result.is_blocked_by_wall --[[and + (self.velocity.y < 0 or abs(self.velocity.x) > abs(self.velocity.y))--]] then local is_blocked_by_ceiling_at_next = self:_is_blocked_by_ceiling_at(next_position_candidate) if is_blocked_by_ceiling_at_next then if direction == directions.up then diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index e0585233..c0d87179 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1275,6 +1275,7 @@ describe('player_char', function () it('should do nothing when character is just on top of the ground, update slope to 0 and return true', function () pc:set_bottom_center(vector(12, 8)) + pc.slope_angle = 0.25 -- just to verify that slope angle is updated local result = pc:_check_escape_from_ground() -- interface @@ -2865,6 +2866,8 @@ describe('player_char', function () describe('(with flat ground)', function () before_each(function () + -- . + -- # mock_mset(0, 1, full_tile_id) -- full tile end) @@ -2982,8 +2985,8 @@ describe('player_char', function () describe('(with walls)', function () before_each(function () - -- X X - -- XXX + -- # # + -- ### mock_mset(0, 0, full_tile_id) -- full tile (left wall) mock_mset(0, 1, full_tile_id) -- full tile mock_mset(1, 1, full_tile_id) -- full tile @@ -3038,8 +3041,8 @@ describe('player_char', function () describe('(with wall without ground below)', function () before_each(function () - -- X - -- X + -- # + -- # mock_mset(0, 1, full_tile_id) -- full tile (ground) mock_mset(1, 0, full_tile_id) -- full tile (wall without ground below) end) @@ -3072,7 +3075,7 @@ describe('player_char', function () describe('(with head wall)', function () before_each(function () - -- X + -- # -- = mock_mset(0, 1, half_tile_id) -- bottom half-tile mock_mset(1, 0, full_tile_id) -- full tile (head wall) @@ -3110,7 +3113,7 @@ describe('player_char', function () before_each(function () -- / - -- X + -- # mock_mset(0, 1, full_tile_id) -- full tile (ground) mock_mset(1, 0, asc_slope_45_id) -- ascending slope 45 end) @@ -3141,8 +3144,8 @@ describe('player_char', function () describe('(with ascending slope and wall)', function () before_each(function () - -- X X - -- X/X + -- # # + -- #/# mock_mset(0, 0, 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, full_tile_id) -- full tile (wall) mock_mset(1, 1, asc_slope_45_id) -- ascending slope 45 @@ -3307,7 +3310,7 @@ describe('player_char', function () describe('(1 full tile)', function () before_each(function () - -- .X + -- .# mock_mset(1, 0, full_tile_id) -- full tile (act like a full ceiling if position is at bottom) end) @@ -4080,7 +4083,7 @@ describe('player_char', function () describe('(with flat ground)', function () before_each(function () - -- X + -- # mock_mset(0, 0, full_tile_id) -- full tile end) @@ -4114,7 +4117,7 @@ describe('player_char', function () ) end) - it('direction down into ground should not move, and flag is_landing with slope_angle', function () + 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 @@ -4139,7 +4142,60 @@ describe('player_char', function () ) end) - it('direction left into wall via ground should not move, and flag is_blocked_by_wall', function () + it('direction left exactly onto ground should step left, and flag is_landing with slop_angle 0', function () + pc.velocity.x = -3 + pc.velocity.y = 0 + + local motion_result = motion.air_motion_result( + vector(11, 0 - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.left, motion_result) + + assert.are_equal(motion.air_motion_result( + vector(10, 0 - pc_data.center_height_standing), + false, + false, + true, + 0 + ), + motion_result + ) + end) + + it('direction right exactly onto ground should step right, and flag is_landing with slop_angle 0', function () + pc.velocity.x = 3 + pc.velocity.y = 0 + + local motion_result = motion.air_motion_result( + vector(-3, 0 - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.right, motion_result) + + assert.are_equal(motion.air_motion_result( + vector(-2, 0 - pc_data.center_height_standing), + false, + false, + true, + 0 + ), + motion_result + ) + 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 + local motion_result = motion.air_motion_result( vector(11, 1 - pc_data.center_height_standing), false, @@ -4151,17 +4207,47 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) assert.are_equal(motion.air_motion_result( - vector(11, 1 - pc_data.center_height_standing), + vector(10, 0 - pc_data.center_height_standing), + false, + false, true, + 0 + ), + motion_result + ) + 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 + + local motion_result = motion.air_motion_result( + vector(-3, 1 - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.right, motion_result) + + assert.are_equal(motion.air_motion_result( + vector(-2, 0 - pc_data.center_height_standing), false, false, - nil + true, + 0 ), motion_result ) end) - it('direction right into wall via ground should not move, and flag is_blocked_by_wall', function () + -- extra tests for sheer horizontally velocity check + + 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 + local motion_result = motion.air_motion_result( vector(-3, 1 - pc_data.center_height_standing), false, @@ -4173,7 +4259,82 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) assert.are_equal(motion.air_motion_result( - vector(-3, 1 - pc_data.center_height_standing), + vector(-2, 0 - pc_data.center_height_standing), + false, + false, + true, + 0 + ), + motion_result + ) + end) + + 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 + + local motion_result = motion.air_motion_result( + vector(-3, 1 - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.right, motion_result) + + assert.are_equal(motion.air_motion_result( + vector(-2, 1 - pc_data.center_height_standing), + false, + false, + false, + nil + ), + motion_result + ) + end) + + 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 motion_result = motion.air_motion_result( + vector(11, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.left, motion_result) + + assert.are_equal(motion.air_motion_result( + vector(11, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing), + true, + false, + false, + nil + ), + motion_result + ) + 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 + + local motion_result = motion.air_motion_result( + vector(-3, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing), + false, + false, + false, + nil + ) + + pc:_next_air_step(directions.right, motion_result) + + assert.are_equal(motion.air_motion_result( + vector(-3, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing), true, false, false, @@ -4183,6 +4344,8 @@ describe('player_char', function () ) end) + -- ceiling tests below also try sheer vs high angle (but no separate test with velocity.y 0 and not 0) + 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 From 77182855faa74c33f9ab10f48e5692df9ec7acc1 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 16:41:28 +0200 Subject: [PATCH 038/112] [PLAYER CHARACTER] Quick fix for Sonic staying airborne on descending slopes by stepping X before Y (may not work when Quadrants are added) --- src/ingame/playercharacter.lua | 19 ++++++++++++++----- src/ingame/playercharacter_utest.lua | 6 +++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 454214cf..d39e064a 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -957,13 +957,22 @@ function player_char:_compute_air_motion_result() -- 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.) - - -- we focus on landing/ceiling first, and prefer simplicity to precision as long as motion seems ok, - -- so we choose c. - self:_advance_in_air_along(motion_result, self.velocity, "y") - log("=> "..motion_result, "trace") + -- 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 grounded, + -- following the ground as usual). self:_advance_in_air_along(motion_result, self.velocity, "x") log("=> "..motion_result, "trace") + self:_advance_in_air_along(motion_result, self.velocity, "y") + log("=> "..motion_result, "trace") return motion_result end diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index c0d87179..fc3055c7 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -3766,9 +3766,9 @@ describe('player_char', function () 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 y before x, we add a check here: - -- if we have already moved from initial pos.x = 4.5 (see test below), block any motion along y - if ref_motion_result.position.x == 4.5 then + -- 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 From 4d3014d1df5a9e6882f6bd524239ec9e7f0411dd Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 16:58:54 +0200 Subject: [PATCH 039/112] [PLAYER CHARACTER] Restored skip ceiling check high-angle downward motion [DATA] Added platforms to step step up landing and walls --- data/data.p8 | 34 +++++++++++++++++----------------- src/ingame/playercharacter.lua | 6 ++++-- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/data/data.p8 b/data/data.p8 index c5dd7004..f09e7fa4 100644 --- a/data/data.p8 +++ b/data/data.p8 @@ -132,7 +132,7 @@ eeeeeeeeeeeeeeee9999999999999999999999999999999999999999999999999999999999999999 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 __gff__ -0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010100000000000000000000010101010000000000000000000000000101010100000101010101010101000001010101000001010101010101010000000101000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000001010101010100000000000000000001010101010000000000000000000000010101010100000101010101010101000101010101000001010101010101010001010101010000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 __map__ 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 @@ -142,24 +142,24 @@ __map__ 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010106768696a6b6c6d6e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101010101010101010101010101010101010105758595a5b5c5d5e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101010101010101010101010101010101010106768696a6b6c6d6e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010105758595a5b5c5d5e1010101010101010101010101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010106768696a6b6c6d6e1010101010101010101010101010101010101010101010101010101010101010101010101010106768696a6b6c6d6e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +101010101010101010101010101010101010101010101010101010106768696a6b6c6d6e1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -10101010101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -10101010102223241010101010102422232222245758595a5b5c5d5e5758595a6768696a6b6c6d6e5b5c5d5e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010105758595a5b5c5d5e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010106768696a6b6c6d6e101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101010101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e101010101010101010101010105758595a5b5c5d5e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101010102223241010101010102422232222245758595a5b5c5d5e5758595a6768696a6b6c6d6e5b5c5d5e1010101010101010106768696a6b6c6d6e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 10101030313233343536363135313234323334326768696a6b6c6d6e6768696a52535253525352536b6c6d6e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101041424342434243424342434243424342525352535253525352535253525352535253525352535253101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101051525352535253525352535253525352525352535253525352535253525352535253525352535253101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index d39e064a..bb055664 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -1143,8 +1143,8 @@ function player_char:_next_air_step(direction, ref_motion_result) -- 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. - if not ref_motion_result.is_blocked_by_wall --[[and - (self.velocity.y < 0 or abs(self.velocity.x) > abs(self.velocity.y))--]] then + if not ref_motion_result.is_blocked_by_wall and + (self.velocity.y < 0 or abs(self.velocity.x) > abs(self.velocity.y)) then local is_blocked_by_ceiling_at_next = self:_is_blocked_by_ceiling_at(next_position_candidate) if is_blocked_by_ceiling_at_next then if direction == directions.up then @@ -1156,7 +1156,9 @@ function player_char:_next_air_step(direction, ref_motion_result) -- 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", "trace") end end end From 910150a7ec9f17aadc127a8f8afbc7be878a5549 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 17:06:16 +0200 Subject: [PATCH 040/112] [TRAVIS] Fixed comment on script order Do not run Travis build if commit message contains (WIP) or #no_travis (like this one) --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 06f1ebbf..01dfe2ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,6 @@ +# Do not trigger Travis if commit message contains "(WIP)" or "#no_travis" (case-insensitive) +if: commit_message !~ /(\(WIP\)|#no_travis)/ + os: linux language: python # Can use any language here, but if it's not 'python' @@ -53,9 +56,7 @@ before_script: script: # build game and itest to make sure everything works fine - # (do it before tests although busted tests can be run independently, - # because sometimes a few minor tests fail but we still want to know - # if we can build, and we are under the token limit) + # (even if build fails, tests will be run independently thanks to busted) - ./build_game.sh debug - ./build_game.sh release - ./build_itest.sh From bf170f12772bae105cdb57b4303850a05d0f6d1d Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 17:59:37 +0200 Subject: [PATCH 041/112] [PLAYER CHARACTER] Added air drag under conditions on velocity --- src/data/playercharacter_data.lua | 14 ++++++ src/ingame/playercharacter.lua | 11 +++++ src/ingame/playercharacter_utest.lua | 73 +++++++++++++++++++++++++--- src/itests/itestplayercharacter.lua | 8 +++ 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/data/playercharacter_data.lua b/src/data/playercharacter_data.lua index 9b9d7f3b..eb37fcb9 100644 --- a/src/data/playercharacter_data.lua +++ b/src/data/playercharacter_data.lua @@ -4,6 +4,10 @@ local animated_sprite_data = require("engine/render/animated_sprite_data") local playercharacter_data = { -- platformer motion + -- values in px, px/frame, px/frame^2 are *2 compared to SPG since we work with 8px tiles + -- for values in px, px/frame, px/frame^2, I added /64 + -- for degrees, /360 form + -- (for readability) -- ground acceleration (px/frame^2) ground_accel_frame2 = 0.0234375, -- 1.5/64 @@ -41,6 +45,16 @@ local playercharacter_data = { -- air acceleration on x axis (px/frames^2) air_accel_x_frame2 = 0.046875, -- 3/64 + -- air drag factor applied every frame, at 60 FPS + air_drag_factor_per_frame = 0.96875, + + -- min absolute velocity x for which air drag is applied + air_drag_min_velocity_x = 0.25, -- 16/64 + + -- maximum absolute velocity y for which air drag is applied + -- the actual range is ] -air_drag_max_abs_velocity_y, 0 [ + air_drag_max_abs_velocity_y = 8, -- 512/64 + -- ground acceleration (px/frame) max_ground_speed = 3, diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index bb055664..70fc752a 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -482,6 +482,7 @@ function player_char:_update_ground_speed() -- 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) self:_update_ground_speed_by_slope() self:_update_ground_speed_by_intention() self:_clamp_ground_speed() @@ -886,6 +887,8 @@ function player_char:_update_platformer_motion_airborne() self.orientation = signed_speed_to_dir(self.move_intention.x) end + self:apply_air_drag() + -- apply air motion local air_motion_result = self:_compute_air_motion_result() @@ -926,6 +929,14 @@ function player_char:_check_hold_jump() end end +function player_char:apply_air_drag() + local vel = self.velocity -- ref + if vel.y < 0 and vel.y > - pc_data.air_drag_max_abs_velocity_y and + abs(vel.x) >= pc_data.air_drag_min_velocity_x then + vel.x = vel.x * pc_data.air_drag_factor_per_frame + end +end + -- return {next_position: vector, is_blocked_by_ceiling: bool, is_blocked_by_wall: 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 diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index fc3055c7..7ed9a4a4 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -3576,10 +3576,11 @@ describe('player_char', function () end) -- compute_air_motion_result_mock (vector(2, 8), false, false, false) - describe('(when _compute_air_motion_result returns a motion result with is_blocked_by_wall: false, is_blocked_by_ceiling: true)', function () + 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 () setup(function () - compute_air_motion_result_mock = stub(player_char, "_compute_air_motion_result", function (self) + stub(player_char, "_compute_air_motion_result", function (self) return motion.air_motion_result( vector(2, 8), false, -- not the focus, but verified @@ -3588,14 +3589,19 @@ describe('player_char', function () nil ) end) + stub(player_char, "apply_air_drag", function (self) + self.velocity.x = 0.9 * self.velocity.x + end) end) teardown(function () - compute_air_motion_result_mock:revert() + player_char._compute_air_motion_result:revert() + player_char.apply_air_drag:revert() end) after_each(function () - compute_air_motion_result_mock:clear() + player_char._compute_air_motion_result:clear() + player_char.apply_air_drag:clear() end) it('should set velocity.y to 0', function () @@ -3610,12 +3616,17 @@ describe('player_char', function () assert.are_equal(0, pc.velocity.y) end) - it('should preserve velocity.x', function () + it('should apply air drag, then preserve velocity.x on hit ceiling', function () pc.velocity = vector(10, -10) pc:_update_platformer_motion_airborne() - assert.are_equal(10, pc.velocity.x) + -- 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)) + + -- value test + assert.are_equal(9, pc.velocity.x) end) end) -- compute_air_motion_result_mock (is_blocked_by_ceiling: true) @@ -3745,6 +3756,56 @@ describe('player_char', function () end) + describe('apply_air_drag', function () + + 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) + + pc:apply_air_drag() + + assert.are_equal(vector(0.25, 0), pc.velocity) + 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) + + pc:apply_air_drag() + + assert.are_equal(vector(0.25, 7), pc.velocity) + 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) + + pc:apply_air_drag() + + assert.are_equal(vector(0.1, -7), pc.velocity) + 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) + + pc:apply_air_drag() + + -- velocity x should be = 0.2421875 + assert.are_equal(vector(0.25 * pc_data.air_drag_factor_per_frame, -7), pc.velocity) + 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) + + pc:apply_air_drag() + + assert.are_equal(vector(0.25, -8), pc.velocity) + end) + + end) + describe('_compute_air_motion_result', function () it('(when velocity is zero) should return air_motion_result with initial position and no hits', function () diff --git a/src/itests/itestplayercharacter.lua b/src/itests/itestplayercharacter.lua index 4f230db1..6f25adc8 100644 --- a/src/itests/itestplayercharacter.lua +++ b/src/itests/itestplayercharacter.lua @@ -89,6 +89,14 @@ expect pc_motion_state grounded expect pc_ground_spd 0 expect pc_velocity 0 0 ]]) + +-- commented out to fit in 65536 chars +-- but you can run them anytime as headless itests with busted +-- however, note that airborne tests are broken due to the new air drag feature +-- so either set air_drag_factor_per_frame to 0 for the itests, +-- or update all the airborne itests +-- or use broader expectations such as Sonic being in a certain area or reaching a large trigger + --[=[ -- calculation notes: From d64cd10ea5ce9a094f44b6ce260f44f5e09f011a Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 18:23:54 +0200 Subject: [PATCH 042/112] [PLAYER CHARACTER] Do not check for jump interrupt during normal fall Cannot be tested now except by printing log, but when Sonic can run upward and auto "fall" with negative velocity, releasing jump button (previously held) should not suddenly change velocity Y --- src/ingame/playercharacter.lua | 10 ++++-- src/ingame/playercharacter_utest.lua | 51 ++++++++++++++++++++++------ src/main.lua | 2 +- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 70fc752a..0af232a8 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -875,9 +875,13 @@ function player_char:_update_platformer_motion_airborne() self.velocity.y = self.velocity.y + pc_data.gravity_frame2 end - -- check if player is continuing or interrupting jump *after* applying gravity - -- this means gravity will *not* be applied during the hop/interrupt jump frame - self:_check_hold_jump() + -- only allow jump interrupt if character has jumped on its own (no fall) + -- there is no has_jumped flag so the closest is to check for air_spin + if self.motion_state == motion_states.air_spin then + -- check if player is continuing or interrupting jump *after* applying gravity + -- this means gravity will *not* be applied during the hop/interrupt jump frame + self:_check_hold_jump() + end if self.move_intention.x ~= 0 then -- apply x acceleration via intention (if not 0) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 7ed9a4a4..207811f2 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -3419,10 +3419,12 @@ describe('player_char', function () setup(function () spy.on(player_char, "_enter_motion_state") + spy.on(player_char, "_check_hold_jump") end) teardown(function () player_char._enter_motion_state:revert() + player_char._check_hold_jump:revert() end) before_each(function () @@ -3430,6 +3432,7 @@ describe('player_char', function () 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() 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 () @@ -3455,71 +3458,100 @@ describe('player_char', function () 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.hold_jump_intention = false pc:_update_platformer_motion_airborne() - -- interface: we are assessing the effect of _check_hold_jump directly + -- 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.hold_jump_intention = false pc:_update_platformer_motion_airborne() - -- interface: we are assessing the effect of _check_hold_jump directly + -- 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.hold_jump_intention = true pc:_update_platformer_motion_airborne() - -- interface: we are assessing the effect of _check_hold_jump directly + -- 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.hold_jump_intention = true pc:_update_platformer_motion_airborne() - -- interface: we are assessing the effect of _check_hold_jump directly + -- 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.hold_jump_intention = false pc:_update_platformer_motion_airborne() - -- interface: we are assessing the effect of _check_hold_jump directly + -- 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 set to speed y to interrupt speed (no gravity added) when interrupting actual jump', function () - pc.velocity.y = -1 -- must be >= -pc_data.jump_interrupt_speed_frame (-2) + 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.hold_jump_intention = false pc:_update_platformer_motion_airborne() - -- interface: we are assessing the effect of _check_hold_jump directly - assert.are_same({-1 + pc_data.gravity_frame2, false}, {pc.velocity.y, pc.has_jumped_this_frame}) + -- 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) it('should apply air accel x', function () @@ -3528,7 +3560,6 @@ describe('player_char', function () pc:_update_platformer_motion_airborne() - -- interface: we are assessing the effect of _check_hold_jump directly assert.are_equal(4 - pc_data.air_accel_x_frame2, pc.velocity.x) end) diff --git a/src/main.lua b/src/main.lua index 0e386a76..dc050d2c 100644 --- a/src/main.lua +++ b/src/main.lua @@ -47,7 +47,7 @@ function _init() ['itest'] = true, ['log'] = true, ['ui'] = true, - ['trace'] = true, + -- ['trace'] = true, -- ['frame'] = true, -- game From 2ee25473fb997a97e2dc91cefe34ab23c0ee3f28 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 19:41:13 +0200 Subject: [PATCH 043/112] [TEST] Fixed debug motion mode itest by reenabling #cheat in build_itest.sh --- build_itest.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build_itest.sh b/build_itest.sh index c3a3fd33..eec19e32 100755 --- a/build_itest.sh +++ b/build_itest.sh @@ -19,9 +19,8 @@ cartridge_stem="picosonic_itest_all" version="3.0" config='itest' # symbols='assert,log,visual_logger,tuner,profiler,mouse,itest' -# for now, we don't set extra symbols like cheat to make it lighter, but it's still possible -# to test cheats in headless itests as busted preserves all (non-#pico8) code -symbols='assert,log,itest' +# cheat needed to set debug motion mode +symbols='assert,log,itest,cheat' # Build from itest main for all itests "$picoboots_scripts_path/build_cartridge.sh" \ From 3c6f9dd2142c6e66665d2fb350defd3a2125bf8b Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 19:42:13 +0200 Subject: [PATCH 044/112] [TEST] Fixed itest requiring tilemap by extracting data needed for PICO-8 itests from the busted-only tile_test_data --- src/ingame/playercharacter_utest.lua | 41 ++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 207811f2..1aa352a4 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -602,14 +602,42 @@ describe('player_char', function () pc:_handle_input() -- implementation - assert.spy(pc._toggle_debug_motion).was_called(1) - assert.spy(pc._toggle_debug_motion).was_called_with(match.ref(pc)) + assert.spy(player_char._toggle_debug_motion).was_called(1) + assert.spy(player_char._toggle_debug_motion).was_called_with(match.ref(pc)) end) end) describe('_toggle_debug_motion', function () + setup(function () + stub(player_char, "set_motion_mode") + end) + + teardown(function () + player_char.set_motion_mode:revert() + end) + + after_each(function () + input:init() + + player_char.set_motion_mode:clear() + end) + + it('(motion mode is debug) it should toggle motion mode to platformer', function () + pc.motion_mode = motion_modes.platformer + + pc:_toggle_debug_motion() + + -- implementation + assert.spy(player_char.set_motion_mode).was_called(1) + assert.spy(player_char.set_motion_mode).was_called_with(match.ref(pc), 2) + end) + + end) + + describe('set_motion_mode', function () + setup(function () -- don't stub, we need to check if the motion mode actually changed after toggle > spawn_at spy.on(player_char, "spawn_at") @@ -625,23 +653,24 @@ describe('player_char', function () player_char.spawn_at:clear() end) - it('(motion mode is platformer) it should toggle motion mode to debug', function () + it('(to debug) should set motion mode to debug a and reset debug velocity', function () pc.motion_mode = motion_modes.platformer + pc.debug_velocity = vector(1, 2) + pc:_toggle_debug_motion() + assert.are_equal(motion_modes.debug, pc.motion_mode) assert.are_equal(vector.zero(), pc.debug_velocity) end) - it('(motion mode is debug) it should toggle motion mode to platformer', function () + it('(to platformer) should set motion mode to platformer and respawn as current position', function () local previous_position = pc.position -- in case we change it during the spawn pc.motion_mode = motion_modes.debug pc:_toggle_debug_motion() - -- interface (partial) assert.are_equal(motion_modes.platformer, pc.motion_mode) - -- implementation assert.spy(pc.spawn_at).was_called(1) assert.spy(pc.spawn_at).was_called_with(match.ref(pc), previous_position) end) From 5afde87bb777da4d84bbf1db662b9aa4b0137af7 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 19:43:29 +0200 Subject: [PATCH 045/112] [TEST] Fixed itest_dsl _parse_ methods not using local defined from protected enums --- src/itest/itest_dsl.lua | 5 +++-- src/test_data/tile_representation.lua | 27 +++++++++++++++++++++++++++ src/test_data/tile_test_data.lua | 25 +------------------------ 3 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 src/test_data/tile_representation.lua diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index 0ad2ee5a..6c32e506 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -39,6 +39,7 @@ local input = require("engine/input/input") local player_char = require("ingame/playercharacter") local pc_data = require("data/playercharacter_data") +require("test_data/tile_representation") --#if busted local tile_test_data = require("test_data/tile_test_data") --#endif @@ -257,14 +258,14 @@ function itest_dsl._parse_motion_state(arg_strings) assert(#arg_strings == 1, "_parse_motion_state: got "..#arg_strings.." args, expected 1") local motion_state = motion_states_protected[arg_strings[1]] assert(motion_state, "motion_states_protected["..arg_strings[1].."] is not defined") - return motion_states[arg_strings[1]] + return motion_state end function itest_dsl._parse_button_id(arg_strings) assert(#arg_strings == 1, "_parse_button_id: got "..#arg_strings.." args, expected 1") local button_id = button_ids_protected[arg_strings[1]] assert(button_id, "button_ids_protected["..arg_strings[1].."] is not defined") - return button_ids[arg_strings[1]] + return button_id end function itest_dsl._parse_gp_value(arg_strings) diff --git a/src/test_data/tile_representation.lua b/src/test_data/tile_representation.lua new file mode 100644 index 00000000..97ec17a6 --- /dev/null +++ b/src/test_data/tile_representation.lua @@ -0,0 +1,27 @@ +-- this file is used by busted tests creating mock tilemaps on the go, +-- but also PICO-8 itests so we extracted it from tile_test_data +-- so it can be required safely from itest_dsl + +-- IDs of tiles used for tests only (black and white in spritesheet, never used in real game) +no_tile_id = 0 +full_tile_id = 32 +half_tile_id = 80 +flat_low_tile_id = 96 +bottom_right_quarter_tile_id = 64 +asc_slope_45_id = 112 +desc_slope_45_id = 116 +asc_slope_22_id = 113 + +-- symbol mapping for itests +-- (could also be used for utests instead of manual mock_mset, but need to extract parse_tilemap +-- from itest_dsl) +tile_symbol_to_ids = { + ['.'] = no_tile_id, -- empty + ['#'] = full_tile_id, -- full tile + ['='] = half_tile_id, -- half tile (4px high) + ['_'] = flat_low_tile_id, -- flat low tile (2px high) + ['r'] = bottom_right_quarter_tile_id, -- bottom-right quarter tile (4px high) + ['/'] = asc_slope_45_id, -- ascending slope 45 + ['\\'] = desc_slope_45_id, -- descending slope 45 + ['<'] = asc_slope_22_id, -- ascending slope 22.5 +} diff --git a/src/test_data/tile_test_data.lua b/src/test_data/tile_test_data.lua index 82b4a1bb..ff1245c6 100644 --- a/src/test_data/tile_test_data.lua +++ b/src/test_data/tile_test_data.lua @@ -6,33 +6,10 @@ local tile = require("platformer/tile") local collision_data = require("data/collision_data") local stub = require("luassert.stub") +require("test_data/tile_representation") local height_array_init_mock --- IDs of tiles used for tests only (black and white in spritesheet, never used in real game) -no_tile_id = 0 -full_tile_id = 32 -half_tile_id = 80 -flat_low_tile_id = 96 -bottom_right_quarter_tile_id = 64 -asc_slope_45_id = 112 -desc_slope_45_id = 116 -asc_slope_22_id = 113 - --- symbol mapping for itests --- (could also be used for utests instead of manual mock_mset, but need to extract parse_tilemap --- from itest_dsl) -tile_symbol_to_ids = { - ['.'] = no_tile_id, -- empty - ['#'] = full_tile_id, -- full tile - ['='] = half_tile_id, -- half tile (4px high) - ['_'] = flat_low_tile_id, -- flat low tile (2px high) - ['r'] = bottom_right_quarter_tile_id, -- bottom-right quarter tile (4px high) - ['/'] = asc_slope_45_id, -- ascending slope 45 - ['\\'] = desc_slope_45_id, -- descending slope 45 - ['<'] = asc_slope_22_id, -- ascending slope 22.5 -} - local tile_test_data = {} function tile_test_data.setup() From eef13df52f531eb5984372a3e421d779cf3c0439 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 19:44:27 +0200 Subject: [PATCH 046/112] [PLAYER CHARACTER] #cheat Extracted set_motion_mode from _toggle_debug_motion --- src/ingame/playercharacter.lua | 17 +++++++++++------ src/itest/itest_dsl.lua | 2 +- src/itest/itest_dsl_utest.lua | 11 ++++++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 0af232a8..e18090d0 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -210,13 +210,18 @@ end --#if cheat function player_char:_toggle_debug_motion() - if self.motion_mode == motion_modes.debug then - -- respawn character at current position. this will in particular: - -- - set the motion mode back to platformer - -- - detect ground and update the motion state correctly + -- 1 -> 2 (debug) + -- 2 -> 1 (platformer) + self:set_motion_mode(self.motion_mode % 2 + 1) +end + +function player_char:set_motion_mode(val) + self.motion_mode = val + if val == motion_modes.platformer then + -- respawn character at current position + -- this will detect ground and update the motion state correctly self:spawn_at(self.position) - else -- self.motion_mode == motion_modes.platformer - self.motion_mode = motion_modes.debug + else -- self.motion_mode == motion_modes.debug self.debug_velocity = vector.zero() end end diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index 6c32e506..0a769979 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -318,7 +318,7 @@ end function itest_dsl._execute_set_motion_mode(args) local current_stage_state = get_current_state_as_stage() - current_stage_state.player_char.motion_mode = args[1] + current_stage_state.player_char:set_motion_mode(args[1]) end function itest_dsl._execute_move(args) diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua index e8267a8d..21016a34 100644 --- a/src/itest/itest_dsl_utest.lua +++ b/src/itest/itest_dsl_utest.lua @@ -249,9 +249,18 @@ describe('itest_dsl', function () describe('execute_set_motion_mode', function () + setup(function () + stub(player_char, "set_motion_mode") + end) + + teardown(function () + player_char.set_motion_mode:revert() + end) + it('should set the motion mode', function () itest_dsl._execute_set_motion_mode({motion_modes.debug}) - assert.are_equal(motion_modes.debug, state.player_char.motion_mode) + assert.spy(player_char.set_motion_mode).was_called(1) + assert.spy(player_char.set_motion_mode).was_called_with(match.ref(state.player_char), motion_modes.debug) end) end) From c2b85c84a03551089d67ac9ca849330120a3e613 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sat, 1 Aug 2020 19:56:40 +0200 Subject: [PATCH 047/112] [PLAYER CHARACTER] Added top speed Y (fall) --- src/data/playercharacter_data.lua | 3 +++ src/ingame/playercharacter.lua | 4 ++++ src/ingame/playercharacter_utest.lua | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/src/data/playercharacter_data.lua b/src/data/playercharacter_data.lua index eb37fcb9..7234fdd2 100644 --- a/src/data/playercharacter_data.lua +++ b/src/data/playercharacter_data.lua @@ -58,6 +58,9 @@ local playercharacter_data = { -- ground acceleration (px/frame) max_ground_speed = 3, + -- max air speed (very high, probably won't happen unless Sonic falls in bottomless pit) + max_air_velocity_y = 32, -- 2048/64 + -- initial variable jump speed (Sonic) (px/frame) initial_var_jump_speed_frame = 3.25, -- 208/64 = 3 + 16/64 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index e18090d0..0e45485e 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -898,6 +898,10 @@ function player_char:_update_platformer_motion_airborne() self:apply_air_drag() + if self.velocity.y > pc_data.max_air_velocity_y then + self.velocity.y = pc_data.max_air_velocity_y + end + -- apply air motion local air_motion_result = self:_compute_air_motion_result() diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 1aa352a4..305da1c5 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -3612,6 +3612,14 @@ describe('player_char', function () assert.are_equal(horizontal_dirs.right, pc.orientation) end) + it('should clamp velocity Y if beyond limit (positive)', function () + pc.velocity.y = 1000 + + pc:_update_platformer_motion_airborne() + + assert.are_equal(pc_data.max_air_velocity_y, pc.velocity.y) + end) + -- bugfix history: -- . it('should update position with air motion result position', function () From 45d036e00a7e15753ca8cea273b6f2110cbd5991 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 2 Aug 2020 20:42:22 +0200 Subject: [PATCH 048/112] [SCRIPT] chmod +x edit_data.sh and edit_metadata.sh --- edit_data.sh | 0 edit_metadata.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 edit_data.sh mode change 100644 => 100755 edit_metadata.sh diff --git a/edit_data.sh b/edit_data.sh old mode 100644 new mode 100755 diff --git a/edit_metadata.sh b/edit_metadata.sh old mode 100644 new mode 100755 From 5d5fb1ca8ff47fddaa50c12b9984b63d5673958f Mon Sep 17 00:00:00 2001 From: huulong Date: Mon, 3 Aug 2020 02:06:19 +0200 Subject: [PATCH 049/112] [TILES] Added loop tiles + proto high ascending slope (8, -4) - Reversed Y convention for all slope angles (oppose sin) to match PICO-8 convention - Fix ascending/descending slope checks by checking sign of sin(angle) instead of sign of angle (will also take convention automatically into account) - Added partial loop and proto tiles in level to test medium slope and prepare test for quadrant feature --- data/data.p8 | 166 +++++++++++++-------------- pico-boots | 2 +- src/data/collision_data.lua | 72 ++++++++---- src/ingame/playercharacter.lua | 16 ++- src/ingame/playercharacter_utest.lua | 150 ++++++++++++------------ 5 files changed, 222 insertions(+), 184 deletions(-) diff --git a/data/data.p8 b/data/data.p8 index f09e7fa4..a23e0def 100644 --- a/data/data.p8 +++ b/data/data.p8 @@ -3,70 +3,70 @@ version 29 __lua__ __gfx__ -eeeeeeeee5eeeeeeeeeecccccceeceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee575eeeeeeeeceeccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -ee7ee7ee5775eeeeeeeeccffccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eee77eee57775eeeeeecccfcc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eee77eee577775eeeecccccc770cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -ee7ee7ee57755eeeeeceeecc770ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee575eeeeeeeeeccccf77f0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeee5eeeeeeeee77ffcfffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -8888888888888888ee7777ccfeee77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -8888888888888888eee77ecccfcf77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -8888888888888888eeeeeecccc7ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -8888888888888888eeeee7cee788eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -8888888888888888eee0877ee8878eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -8888888888888888eee0888eee788eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -8888888888888888eeee8778eee08eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -8888888888888888eeee0888eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -7777777700000000eeeeeeeeeeeeeeeeeeeeeeee0000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -7777777700007777eeeeeeeeeeeeeeeeeeeeeeee7777000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -7777777777777777eeeeeeeeeeeeeeeeeeeeeeee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -7777777777777777eeeeeeeeeeeeeeeeeeeeeeee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -7777777777777777eeeeeeeeeeeeeeeeeeeeeeee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -7777777777777777eeeeeeeeeeebeeeeeeeee9ee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -7777777777777777eeeeeeeeebeb9e9eee9e99ee7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -7777777777777777e9eeee9ebb39b9b9e9be9beb7777777777777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeeeee9ee9eb9bb9b9bb9b9b9b9b9b39b9be9ee9eeeeeeee9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeee9ee9bb9bb9bb9bbbb9bbb9bbb9bbbb9bb9bb9ee9ee9ee9e9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeeb9bbbbb9bb9bbbbbbbbbbbbbbbbbbbbbbb9bbbbb9bb9bb9b9beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeee9bbbb93bbbbbb39b3bbbbbbbbbbbbbbbbbbbb39bbbbbbbb9bbb9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeee9bbbbb03bbbb30b30bbbbb0bbbbbb3bbbbbb30bbbbbbbbbbbbb9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeeb3b3bb03b30b30b00bb03b03bbbbb030b0bb30bb3bbbbb3bbb3beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeebeb0b3003003003003000b00b033b00030b3003b0bb3b30b33ebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -eeeeeebe00000000000400400403003003004000b000000003000303ebeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -0000000004440444000440400440440044404440eeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000eeeeeeee -0000000004444444404444440444444044444440eeeeeeeeeeeeeeee0000000000000000000000000000777777770000000000000000000000000000eeeeeeee -0000000004444444444444444444444444444440eeeeeeeeeeeeeeee0000000000000000000000007777777777777777000000000000000000000000eeeeeeee -0000000004444494444449499444444449444440eeeeeeeeeeeeeeee0000000000000000000077777777777777777777777700000000000000000000eeeeeeee -0000777744444499444444499494444499444444eeeeeeeeeeeeeeee0000000000007777777777777777777777777777777777777777000000000000eeeeeeee -0000777704444499444494999944444499444440eeeeeeeeeeeeeeee0000000077777777777777777777777777777777777777777777777700000000eeeeeeee -0000777744444494444494994949444449444444eeeeeeeeeeeeeeee0000777777777777777777777777777777777777777777777777777777770000eeeeeeee -0000777704444494444494494949444449444440eeeeeeeeeeeeeeee7777777777777777777777777777777777777777777777777777777777777777eeeeeeee -0000000004444444444444499949444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9ebeebe9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -0000000004444494444449499444444449444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eeeb9e9bbbbbb9e9beee9eeeeeeeeeeeeeeeeeeeeeeeeeeeee -0000000004444444444449499444444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eee9ee9eebb9b9bbbbbb9b9bbee9ee9eee9eeeeeeeeeeeeeeeeeeeeee -0000000004444494444444499949444449444440eeeeeeeeeeeeeeeeeeeeeeeee9eeee9ee99ebb9bbbbbbbbbbbbbbbbbb9bbe99ee9eeee9eeeeeeeeeeeeeeeee -7777777744444499444444499944444499444444eeeeeeeeeeeeeeeeeeeeeeeee9be9b9bb93bbb9bbbbb9bbbbbb9bbbbb9bbb39bb9b9eb9eeeeeeeeeeeeeeeee -7777777704444499444494949994444499444440eeeeeeeeeeeeeeeeeee9ee9bb9bb9bbbbb3bbbbbb3bbbbb33bbbbb3bbbbbb3bbbbb9bb9bb9ee9eeeeeeeeeee -7777777744444494444494949949444449444444eeeeeeeeeeeeeeeee9e99b9bbbbb93b9bb3bbbbbb33b3b3003b3b33bbbbbb3bb9b39bbbbb9b99e9eeeeeeeee -7777777704444494444494999949444449444440eeeeeeeeeeeeeeee99b99bb3bbbbb3b9bbbb3b3b3303030000303033b3b3bbbb9b3bbbbb3bb99b99eeeeeeee -0000000004444494444444999949444449444440eeeeeeeeeeeeeeeb9bbb9bbbbbbbbbb3bbbbbbb300000004400000003bbbbbbb3bbbbbbbbbb9bbb9beeeeeee -0000000004444494444494999944444449444440eeeeeeeeeeeeee9bbbbbbbbbbb0bb3b3bb0bb3b304404004400404403b3bb0bb3b3bb0bbbbbbbbbbb9eeeeee -0000000004444444434499999449444444444440eeeeeeeeeeeeee9bbbbb300b3003b0303003b0304444440440444444030b3003030b3003b003bbbbb9eeeeee -00000000044444944b3439499949444449444440eeeeeeeeeeeeeeb3b3bb04030040300400403004444444444444444440030400400304003040bb3b3beeeeee -00000000b44444993bb4b949999944449944444beeeeeeeeeeeeeebeb0bb44404444004444440044444444444444444444004444440044440444bb0bebeeeeee -00000000bb4444bbbbbb3999999944b3bb4444bbeeeeeeeeeeeeeebe30b3444044444044444440449444444444444449440444444404444404443b03ebeeeeee -777777774b344bbbb33bb939993b43bbbbb443b4eeeeeeeeeeeeeeee00b4444444444444444444499449444444449449944444444444444444444b00eeeeeeee -77777777bbb3443bbb33b3b3393b3bbbb3443bbbeeeeeeeeeeeeeeee4034444494494444444494499944444444444499944944444444944944444304eeeeeeee -0000000000000007bb3b3bbbbb3bbb3b70000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -0000000000000077b3bb33bbb333bb3b77000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -000000000000077703b3bbbbb3b3b3b077700000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -0000007700007777b03bbb333bbbbbb077770000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -0000777700077777b333bbbbb33b30bb77777000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -0077777700777777b3bbb3bb30033bbb77777700eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -77777777077777773bb330bbbb3b33bb77777770eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee -7777777777777777bbbbb0bbb33bb3bb77777777eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +eeeeeeeee5eeeeeeeeeecccccceeceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4444444444444440044444444444444477777777777777777777777777777777 +eeeeeeee575eeeeeeeeceeccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee449449444444440ee00444444494494477777777777777000077777777777777 +ee7ee7ee5775eeeeeeeeccffccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4499444444000eeeeee004444444994477777777777700000000777777777777 +eee77eee57775eeeeeecccfcc7ccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4449444440eeeeeeeeeeee004444944477777777770000000000007777777777 +eee77eee577775eeeecccccc770cceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee44444440eeeeeeeeeeeeeeee0444444477777777000000000000000077777777 +ee7ee7ee57755eeeeeceeecc770ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4494440eeeeeeeeeeeeeeeeee444494477777770000000000000000007777777 +eeeeeeee575eeeeeeeeeccccf77f0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee444440eeeeeeeeeeeeeeeeeeee04444477777700000000000000000000777777 +eeeeeeeee5eeeeeeeee77ffcfffeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee44444eeeeeeeeeeeeeeeeeeeeee0444477777000000000000000000000077777 +8888888888888888ee7777ccfeee77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4440eeeeeeeeeeeeeeeeeeeeeeee044477770000000000000000000000007777 +8888888888888888eee77ecccfcf77eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9440eeeeeeeeeeeeeeeeeeeeeeee044977770000000000000000000000007777 +8888888888888888eeeeeecccc7ceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee944eeeeeeeeeeeeeeeeeeeeeeeeee44977700000000000000000000000000777 +8888888888888888eeeee7cee788eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee440eeeeeeeeeeeeeeeeeeeeeeeeee04477700000000000000000000000000777 +8888888888888888eee0877ee8878eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee44eeeeeeeeeeeeeeeeeeeeeeeeeeee4477000000000000000000000000000077 +8888888888888888eee0888eee788eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee0477000000000000000000000000000077 +8888888888888888eeee8778eee08eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4eeeeeeeeeeeeeeeeeeeeeeeeeeeeee470000000000000000000000000000007 +8888888888888888eeee0888eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee4eeeeeeeeeeeeeeeeeeeeeeeeeeeeee470000000000000000000000000000007 +7777777700000000eeeeeeeeeeeeeeeeeeeeeeee0000000000000000eeeeeeee4eeeeeeeeeeeeeeeeeeeeeeeeeeeeee470000000000000000000000000000007 +7777777700007777eeeeeeeeeeeeeeeeeeeeeeee7777000000000000eeeeeeee4eeeeeeeeeeeeeeeeeeeeeeeeeeeeee470000000000000000000000000000007 +7777777777777777eeeeeeeeeeeeeeeeeeeeeeee7777777777777777eeeeeeee40eeeeeeeeeeeeeeeeeeeeeeeeeeee0477000000000000000000000000000077 +7777777777777777eeeeeeeeeeeeeeeeeeeeeeee7777777777777777eeeeeeee44eeeeeeeeeeeeeeeeeeeeeeeeeeee4477000000000000000000000000000077 +7777777777777777eeeeeeeeeeeeeeeeeeeeeeee7777777777777777eeeeeeee440eeeeeeeeeeeeeeeeeeeeeeeeee04477700000000000000000000000000777 +7777777777777777eeeeeeeeeeebeeeeeeeee9ee7777777777777777eeeeeeee944eeeeeeeeeeeeeeeeeeeeeeeeee44977700000000000000000000000000777 +7777777777777777eeeeeeeeebeb9e9eee9e99ee7777777777777777eeeeeeee9440eeeeeeeeeeeeeeeeeeeeeeee044977770000000000000000000000007777 +7777777777777777e9eeee9ebb39b9b9e9be9beb7777777777777777eeeeeeee4440eeeeeeeeeeeeeeeeeeeeeeee044477770000000000000000000000007777 +eeeeeeeeeee9ee9eb9bb9b9bb9b9b9b9b9b39b9be9ee9eeeeeeee9eeeeeeeeee44440eeeeeeeeeeeeeeeeeeeeee0444477777000000000000000000000077777 +eeeeeeee9ee9bb9bb9bb9bbbb9bbb9bbb9bbbb9bb9bb9ee9ee9ee9e9eeeeeeee444440eeeeeeeeeeeeeeeeeeee44444477777700000000000000000000777777 +eeeeeeeb9bbbbb9bb9bbbbbbbbbbbbbbbbbbbbbbb9bbbbb9bb9bb9b9beeeeeee4494444eeeeeeeeeeeeeeeeee044494477777770000000000000000007777777 +eeeeee9bbbb93bbbbbb39b3bbbbbbbbbbbbbbbbbbbb39bbbbbbbb9bbb9eeeeee44444440eeeeeeeeeeeeeeee0444444477777777000000000000000077777777 +eeeeee9bbbbb03bbbb30b30bbbbb0bbbbbb3bbbbbb30bbbbbbbbbbbbb9eeeeee4449444440eeeeeeeeeeee044444944477777777770000000000007777777777 +eeeeeeb3b3bb03b30b30b00bb03b03bbbbb030b0bb30bb3bbbbb3bbb3beeeeee449944444440beeeeeeb04444444994477777777777700000000777777777777 +eeeeeebeb0b3003003003003000b00b033b00030b3003b0bb3b30b33ebeeeeee44944944444bb4beeb4bb4444494494477777777777777000077777777777777 +eeeeeebe00000000000400400403003003004000b000000003000303ebeeeeee44444444444bbbbbbbbbb4444444444477777777777777777777777777777777 +0000000004440444000440404440440044404440eeeeeeeeeeeeeeeeeeeeeeee44444444444b3bbbbbb3b44444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000004444444404444444444444044444440eeeeeeeeeeeeeeeeeeeeeeee4444444444430bbbbbb0344444444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000004444444444444444444444444444440eeeeeeeeeeeeeeeeeeeeeeee4444444494404b3bb3b4044944444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000004444494444449499444444449444440eeeeeeeeeeeeeeeeeeeeeeee444494444444430bb034444444494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000777744444499444444499494444499444444eeeeeeeeeeeeeeeeeeeeeeee44444449944440433404444994444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000777704444499444494999944444499444440eeeeeeeeeeeeeeeeeeeeeeee44444499994444400444449999444444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000777744444494444494994949444449444444eeeeeeeeeeeeeeeeeeeeeeee44449494994944444444949949494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000777704444494444494494949444449444440eeeeeeeeeeeeeeeeeeeeeeee44449494944944444444944949494444eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000004444444444444499949444444444440eeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000eeeeeeee +0000000004444494444449499444444449444440eeeeeeeeeeeeeeee0000000000000000000000000000777777770000000000000000000000000000eeeeeeee +0000000004444444444449499444444444444440eeeeeeeeeeeeeeee0000000000000000000000007777777777777777000000000000000000000000eeeeeeee +0000000004444494444444499949444449444440eeeeeeeeeeeeeeee0000000000000000000077777777777777777777777700000000000000000000eeeeeeee +7777777744444499444444499944444499444444eeeeeeeeeeeeeeee0000000000007777777777777777777777777777777777777777000000000000eeeeeeee +7777777704444499444494949994444499444440eeeeeeeeeeeeeeee0000000077777777777777777777777777777777777777777777777700000000eeeeeeee +7777777744444494444494949949444449444444eeeeeeeeeeeeeeee0000777777777777777777777777777777777777777777777777777777770000eeeeeeee +7777777704444494444494999949444449444440eeeeeeeeeeeeeeee7777777777777777777777777777777777777777777777777777777777777777eeeeeeee +0000000004444494444444999949444449444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9ebeebe9eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000004444494444494999944444449444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eeeb9e9bbbbbb9e9beee9eeeeeeeeeeeeeeeeeeeeeeeeeeeee +0000000004444444434499999449444444444440eeeeeeeeeeeeeeeeeeeeeeeeeeeeee9eee9ee9eebb9b9bbbbbb9b9bbee9ee9eee9eeeeeeeeeeeeeeeeeeeeee +00000000044444944b3439499949444449444440eeeeeeeeeeeeeeeeeeeeeeeee9eeee9ee99ebb9bbbbbbbbbbbbbbbbbb9bbe99ee9eeee9eeeeeeeeeeeeeeeee +00000000b44444993bb4b949999944449944444beeeeeeeeeeeeeeeeeeeeeeeee9be9b9bb93bbb9bbbbb9bbbbbb9bbbbb9bbb39bb9b9eb9eeeeeeeeeeeeeeeee +00000000bb4444bbbbbb3999999944b3bb4444bbeeeeeeeeeeeeeeeeeee9ee9bb9bb9bbbbb3bbbbbb3bbbbb33bbbbb3bbbbbb3bbbbb9bb9bb9ee9eeeeeeeeeee +777777774b344bbbb33bb939993b43bbbbb443b4eeeeeeeeeeeeeeeee9e99b9bbbbb93b9bb3bbbbbb33b3b3003b3b33bbbbbb3bb9b39bbbbb9b99e9eeeeeeeee +77777777bbb3443bbb33b3b3393b3bbbb3443bbbeeeeeeeeeeeeeeee99b99bb3bbbbb3b9bbbb3b3b3303030000303033b3b3bbbb9b3bbbbb3bb99b99eeeeeeee +0000000000000007bb3b3bbbbb3bbb3b7000000000000077eeeeeeeb9bbb9bbbbbbbbbb3bbbbbbb300000004400000003bbbbbbb3bbbbbbbbbb9bbb9beeeeeee +0000000000000077b3bb33bbb333bb3b7700000000007777eeeeee9bbbbbbbbbbb0bb3b3bb0bb3b304404004400404403b3bb0bb3b3bb0bbbbbbbbbbb9eeeeee +000000000000077703b3bbbbb3b3b3b07770000000777777eeeeee9bbbbb300b3003b0303003b0304444440440444444030b3003030b3003b003bbbbb9eeeeee +0000007700007777b03bbb333bbbbbb07777000077777777eeeeeeb3b3bb04030040300400403004444444444444444440030400400304003040bb3b3beeeeee +0000777700077777b333bbbbb33b30bb7777700077777777eeeeeebeb0bb44404444004444440044444444444444444444004444440044440444bb0bebeeeeee +0077777700777777b3bbb3bb30033bbb7777770077777777eeeeeebe30b3444044444044444440449444444444444449440444444404444404443b03ebeeeeee +77777777077777773bb330bbbb3b33bb7777777077777777eeeeeeee00b4444444444444444444499449444444449449944444444444444444444b00eeeeeeee +7777777777777777bbbbb0bbb33bb3bb7777777777777777eeeeeeee4034444494494444444494499944444444444499944944444444944944444304eeeeeeee eeeeccccccee1eeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeeccccceeeeeeeeeeecccccceeceeeeeeecccccceeceeeeeeecccccceeceee eeeceeccccccceeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeececccccceeeeeeeeceeccccccceeeeeeceeccccccceeeeeeceeccccccceee eeeeccffccccceeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffcccceeeeeeeeccffccccceeeeeeeccffccccceeeeeeeccffccccceee @@ -132,7 +132,7 @@ eeeeeeeeeeeeeeee9999999999999999999999999999999999999999999999999999999999999999 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 __gff__ -0000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000001010101010100000000000000000001010101010000000000000000000000010101010100000101010101010101000101010101000001010101010101010001010101010000000000000000000000 +0000000000000000010101010000000000000000000000000100000100000000010000000000000001000001000000000001010101010100010101010000000001010101010000000000000000000000010101010100000000000000000000000101010101000001010101010101010001010101010100010101010101010100 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 __map__ 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 @@ -142,25 +142,25 @@ __map__ 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010106768696a6b6c6d6e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -10101010101010101010101010101010101010105758595a5b5c5d5e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -10101010101010101010101010101010101010106768696a6b6c6d6e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010666768696a6b6c6d6e6f1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010101010101010101010101010101010101010101010101010101010101010767778797a7b7c7d7e7f1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101010101010101010101010101010101010666768696a6b6c6d6e6f101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101010101010101010101010101010101010767778797a7b7c7d7e7f101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010105758595a5b5c5d5e1010101010101010101010101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010106768696a6b6c6d6e1010101010101010101010101010101010101010101010101010101010101010101010101010106768696a6b6c6d6e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -101010101010101010101010101010101010101010101010101010106768696a6b6c6d6e1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010105758595a5b5c5d5e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010106768696a6b6c6d6e101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -1010101010101010101010101010101010101010101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -10101010101010101010101010101010101010101010101010101010101010105758595a5b5c5d5e101010101010101010101010105758595a5b5c5d5e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -10101010102223241010101010102422232222245758595a5b5c5d5e5758595a6768696a6b6c6d6e5b5c5d5e1010101010101010106768696a6b6c6d6e10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 -10101030313233343536363135313234323334326768696a6b6c6d6e6768696a52535253525352536b6c6d6e101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010666768696a6b6c6d6e6f10101010101010101010101010101010101010101010101010101010101010101010101010666768696a6b6c6d6e6f101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1010101010767778797a7b7c7d7e7f10101010101010101010101010101010101010101010101010101010101010101010101010767778797a7b7c7d7e7f101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +101010101010101010101010101010101010101010101010101010666768696a6b6c6d6e6f10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +101022232422232422232410101010101010101010101010101010767778797a7b7c7d7e7f10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +3031333432333432333434363710101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1041535253525352535253441010666768696a6b6c6d6e6f1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10515308091010100a0b52541010767778797a7b7c7d7e7f1010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10515318191010101a1b52541010101010101010101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +105154101010101010105154101010101010101010103a751010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10515410101010101010101010101010101010103a7510101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +1051541010101010101010101010101010103a75101010101010101010101010101010101010101010102020202020202020201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10515328291010101010101010101010101010101010101010101010101010106768696a6b6c6d6e101010101010101010101010666768696a6b6c6d6e6f101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10515338392223241010101010102422232222246768696a6b6c6d6e6768696a7778797a7b7c7d7e6b6c6d6e6f10101010101010767778797a7b7c7d7e7f101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 +10101030493233343536363135313234323334327778797a7b7c7d7e7778797a52535253525352537b7c7d7e7f1010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101041424342434243424342434243424342525352535253525352535253525352535253525352535253101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101051525352535253525352535253525352525352535253525352535253525352535253525352535253101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1010101051525352535253525352535253525352525352535253525352535253525352535253525352535253101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 diff --git a/pico-boots b/pico-boots index 2135be4e..674e9974 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 2135be4e88beeee82d0498a503b716c214fe2445 +Subproject commit 674e99749d3c475cac5f577230ec4511317bdac0 diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua index 99824c98..6582ed68 100644 --- a/src/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -18,12 +18,16 @@ return { -- mock data in the end. we'll probably create a pico8 tile data to data string -- converter so we can edit visually, but also generate data code reusable -- for headless tests + + -- all angles are defined with atan2 using top-left XY convention to avoid issues + -- proto tiles may pass values manually, but in this case make sure to enter the angle + -- between 0 and 1, as if Sonic was running a loop counter-clockwise tiles_data = { - [49] = tile_data(sprite_id_location(1, 2), atan2(8, 2)), -- 49 @ (1, 3) + [49] = tile_data(sprite_id_location(1, 2), atan2(8, -2)), -- 49 @ (1, 3) [50] = tile_data(sprite_id_location(0, 2), 0), -- 50 @ (2, 3) [51] = tile_data(sprite_id_location(0, 2), 0), -- 51 @ (3, 3) [52] = tile_data(sprite_id_location(0, 2), 0), -- 52 @ (4, 3) - [53] = tile_data(sprite_id_location(5, 2), atan2(8, -2)), -- 53 @ (5, 3) + [53] = tile_data(sprite_id_location(5, 2), atan2(8, 2)), -- 53 @ (5, 3) [54] = tile_data(sprite_id_location(6, 2), 0), -- 54 @ (6, 3) [65] = tile_data(sprite_id_location(0, 2), 0), -- 65 @ (1, 4) [66] = tile_data(sprite_id_location(0, 2), 0), -- 66 @ (2, 4) @@ -39,22 +43,47 @@ return { [100]= tile_data(sprite_id_location(0, 2), 0), --100 @ (4, 6) [114]= tile_data(sprite_id_location(0, 2), 0), --114 @ (2, 7) [115]= tile_data(sprite_id_location(0, 2), 0), --115 @ (3, 7) - [87] = tile_data(sprite_id_location(7, 4), atan2(8, 2)), -- 87 @ (7, 5) - [88] = tile_data(sprite_id_location(8, 4), atan2(8, 1)), -- 88 @ (8, 5) - [89] = tile_data(sprite_id_location(9, 4), atan2(8, 2)), -- 89 @ (9, 5) - [90] = tile_data(sprite_id_location(10, 4), atan2(8, 2)), -- 90 @ (10, 5) - [91] = tile_data(sprite_id_location(11, 4), atan2(8, -2)), -- 91 @ (11, 5) - [92] = tile_data(sprite_id_location(12, 4), atan2(8, -2)), -- 92 @ (12, 5) - [93] = tile_data(sprite_id_location(13, 4), atan2(8, -1)), -- 93 @ (13, 5) - [94] = tile_data(sprite_id_location(14, 4), atan2(8, -2)), -- 94 @ (14, 5) - [103]= tile_data(sprite_id_location(0, 2), 0), -- 103 @ (7, 6) - [104]= tile_data(sprite_id_location(0, 2), 0), -- 104 @ (8, 6) - [105]= tile_data(sprite_id_location(0, 2), 0), -- 105 @ (9, 6) - [106]= tile_data(sprite_id_location(0, 2), 0), -- 106 @ (10, 6) - [107]= tile_data(sprite_id_location(0, 2), 0), -- 107 @ (11, 6) - [108]= tile_data(sprite_id_location(0, 2), 0), -- 108 @ (12, 6) - [109]= tile_data(sprite_id_location(0, 2), 0), -- 109 @ (13, 6) - [110]= tile_data(sprite_id_location(0, 2), 0), -- 110 @ (14, 6) + -- low slopes ascending and descending + [103] = tile_data(sprite_id_location(7, 5), atan2(8, -2)), -- 87 @ (7, 6) + [104] = tile_data(sprite_id_location(8, 5), atan2(8, -1)), -- 88 @ (8, 6) + [105] = tile_data(sprite_id_location(9, 5), atan2(8, -2)), -- 89 @ (9, 6) + [106] = tile_data(sprite_id_location(10, 5), atan2(8, -2)), -- 90 @ (10, 6) + [107] = tile_data(sprite_id_location(11, 5), atan2(8, 2)), -- 91 @ (11, 6) + [108] = tile_data(sprite_id_location(12, 5), atan2(8, 2)), -- 92 @ (12, 6) + [109] = tile_data(sprite_id_location(13, 5), atan2(8, 1)), -- 93 @ (13, 6) + [110] = tile_data(sprite_id_location(14, 5), atan2(8, 2)), -- 94 @ (14, 6) + -- bottom of said slopes (full tiles) + [119]= tile_data(sprite_id_location(0, 2), 0), -- 119 @ (7, 7) + [120]= tile_data(sprite_id_location(0, 2), 0), -- 120 @ (8, 7) + [121]= tile_data(sprite_id_location(0, 2), 0), -- 121 @ (9, 7) + [122]= tile_data(sprite_id_location(0, 2), 0), -- 122 @ (10, 7) + [123]= tile_data(sprite_id_location(0, 2), 0), -- 123 @ (11, 7) + [124]= tile_data(sprite_id_location(0, 2), 0), -- 124 @ (12, 7) + [125]= tile_data(sprite_id_location(0, 2), 0), -- 125 @ (13, 7) + [126]= tile_data(sprite_id_location(0, 2), 0), -- 126 @ (14, 7) + -- loop (start from top-left tile, then rotate clockwise) + -- note that we always write angles as atan2(dx, dy) with motion (dx, dy) + -- as if Sonic was running the loop counter-clockwise + -- this allows to identify ceiling angles vs floor angles easily + -- (ceiling angles between 0.25 and 0.75) + [8]= tile_data(sprite_id_location(12, 0), atan2(-4, 4)), -- 8 @ (8, 0) + [9]= tile_data(sprite_id_location(13, 0), atan2(-8, 4)), -- 9 @ (9, 0) + [10]= tile_data(sprite_id_location(14, 0), atan2(-8, -4)), -- 10 @ (10, 0) + [11]= tile_data(sprite_id_location(15, 0), atan2(-4, -4)), -- 11 @ (11, 0) + [27]= tile_data(sprite_id_location(15, 1), atan2(-4, -8)), -- 27 @ (11, 1) + [43]= tile_data(sprite_id_location(15, 2), atan2(4, -8)), -- 43 @ (11, 2) + [59]= tile_data(sprite_id_location(15, 3), atan2(4, -4)), -- 59 @ (11, 3) + [58]= tile_data(sprite_id_location(14, 3), atan2(8, -4)), -- 58 @ (10, 3) + [57]= tile_data(sprite_id_location(13, 3), atan2(8, 4)), -- 57 @ (9, 3) + [56]= tile_data(sprite_id_location(12, 3), atan2(4, 4)), -- 56 @ (8, 3) + [40]= tile_data(sprite_id_location(12, 2), atan2(4, 8)), -- 40 @ (8, 2) + [24]= tile_data(sprite_id_location(12, 1), atan2(-4, 8)), -- 24 @ (8, 1) + -- loop bottom ground (full) + [72]= tile_data(sprite_id_location(12, 4), 0), -- 72 @ (8, 4) + [73]= tile_data(sprite_id_location(13, 4), 0), -- 73 @ (9, 4) + [74]= tile_data(sprite_id_location(14, 4), 0), -- 74 @ (10, 4) + [75]= tile_data(sprite_id_location(15, 4), 0), -- 75 @ (11, 4) + -- proto (black and white tiles being their own collision masks) -- must match tile_data.lua -- if too heavy, surround with #itest and create a separate spritesheet for itests with only polygonal tiles @@ -64,9 +93,10 @@ return { [80] = tile_data(sprite_id_location(0, 5), 0), -- 80 @ (0, 5) HALF TILE (4px high) = [96] = tile_data(sprite_id_location(0, 6), 0), -- 96 @ (0, 6) FLAT LOW TILE (2px high) _ [64] = tile_data(sprite_id_location(0, 4), 0), -- 64 @ (0, 4) BOTTOM-RIGHT QUARTER TILE (4px high) r - [112]= tile_data(sprite_id_location(1, 7), -0.125), -- 112 @ (1, 7) ASCENDING 45 / - [113]= tile_data(sprite_id_location(0, 7), -0.0625), -- 113 @ (0, 7) ASCENDING 22.5 < - [116]= tile_data(sprite_id_location(4, 7), 0.125), -- 116 @ (4, 7) DESCENDING 45 \ + [112]= tile_data(sprite_id_location(1, 7), 0.125), -- 112 @ (1, 7) ASCENDING 45 / + [113]= tile_data(sprite_id_location(0, 7), 0.0625), -- 113 @ (0, 7) ASCENDING 22.5 < + [116]= tile_data(sprite_id_location(4, 7), 1-0.125), -- 116 @ (4, 7) DESCENDING 45 \ + [117]= tile_data(sprite_id_location(5, 7), atan2(8, -4)), -- 117 @ (5, 7) higher 2:1 ascending slope (completes 58 from loop) } } diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 0e45485e..d4a3503d 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -508,7 +508,9 @@ function player_char:_update_ground_speed_by_slope() -- Resolves: character was suddenly stopped by longer slopes when starting ascension with low momentum, -- falling back to the flat ground behind, and repeating, causing a glitch-like oscillation local ascending_slope_factor = 1 - if self.ground_speed ~= 0 and abs(self.slope_angle) >= pc_data.steep_slope_min_angle and sgn(self.ground_speed) ~= sgn(self.slope_angle) then + -- make sure to compare sin in abs value (steep_slope_min_angle is between 0 and 0.25 so we know its sin is negative) + -- since slope angle is module 1 and cannot be directly compared (and you'd need to use (slope_angle + 0.5) % 1 - 0.5 to be sure) + if self.ground_speed ~= 0 and abs(sin(self.slope_angle)) >= sin(-pc_data.steep_slope_min_angle) and sgn(self.ground_speed) ~= sgn(sin(self.slope_angle)) then is_ascending_slope = true local ascending_slope_duration = pc_data.progressive_ascending_slope_duration local progressive_ascending_slope_factor = 1 @@ -517,7 +519,9 @@ function player_char:_update_ground_speed_by_slope() ascending_slope_factor = self.ascending_slope_time / ascending_slope_duration end - self.ground_speed = self.ground_speed - ascending_slope_factor * pc_data.slope_accel_factor_frame2 * sin(self.slope_angle) + -- slope angle is mostly defined with atan2(dx, dy) which follows top-left origin BUT counter-clockwise angle convention + -- sin also follows this convention, so ultimately + is OK + self.ground_speed = self.ground_speed + ascending_slope_factor * pc_data.slope_accel_factor_frame2 * sin(self.slope_angle) end if not is_ascending_slope then @@ -542,7 +546,9 @@ function player_char:_update_ground_speed_by_intention() -- considering slope factor alone -- Resolves: character descending a steep slope was braking and turning back too suddenly local ground_decel_factor = 1 - if abs(self.slope_angle) >= pc_data.steep_slope_min_angle and sgn(self.ground_speed) == sgn(self.slope_angle) then + -- make sure to compare sin in abs value (steep_slope_min_angle is between 0 and 0.25 so we know its sin is negative) + -- since slope angle is module 1 and cannot be directly compared (and you'd need to use (slope_angle + 0.5) % 1 - 0.5 to be sure) + if abs(sin(self.slope_angle)) >= sin(-pc_data.steep_slope_min_angle) and sgn(self.ground_speed) == sgn(sin(self.slope_angle)) then -- character is trying to brake on a descending slope ground_decel_factor = pc_data.ground_decel_descending_slope_factor end @@ -572,7 +578,9 @@ function player_char:_update_ground_speed_by_intention() -- Effect: the character will automatically run down a steep slope and accumulate acceleration downward -- without friction -- Resolves: the character was moving down a steep slope very slowly because of friction - if abs(self.slope_angle) <= pc_data.steep_slope_min_angle or sgn(self.ground_speed) ~= sgn(self.slope_angle) then + -- make sure to compare sin in abs value (steep_slope_min_angle is between 0 and 0.25 so we know its sin is negative) + -- since slope angle is module 1 and cannot be directly compared (and you'd need to use (slope_angle + 0.5) % 1 - 0.5 to be sure) + if abs(sin(self.slope_angle)) <= sin(-pc_data.steep_slope_min_angle) or sgn(self.ground_speed) ~= sgn(sin(self.slope_angle)) then self.ground_speed = sgn(self.ground_speed) * max(0, abs(self.ground_speed) - pc_data.ground_friction_frame2) end end diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 305da1c5..0567bcbd 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1092,58 +1092,58 @@ describe('player_char', function () mock_mset(1, 1, asc_slope_45_id) end) - it('should return 0.0625, -45/360 if just above slope column 0', function () - assert.are_equal(ground_query_info(0.0625, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 - 0.0625))) + it('should return 0.0625, 45/360 if just above slope column 0', function () + assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 - 0.0625))) end) - it('should return 0, -45/360 if at the top of column 0', function () - assert.are_equal(ground_query_info(0, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15))) + it('should return 0, 45/360 if at the top of column 0', function () + assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15))) end) - it('. should return 0.0625, -45/360 if just above slope column 4', function () - assert.are_equal(ground_query_info(0.0625, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 11 - 0.0625))) + it('. should return 0.0625, 45/360 if just above slope column 4', function () + assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 11 - 0.0625))) end) - it('. should return 0, -45/360 if at the top of column 4', function () - assert.are_equal(ground_query_info(0, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 11))) + it('. should return 0, 45/360 if at the top of column 4', function () + assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 11))) end) - it('should return -2, -45/360 if 2px below column 4', function () - assert.are_equal(ground_query_info(-2, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 13))) + it('should return -2, 45/360 if 2px below column 4', function () + assert.are_equal(ground_query_info(-2, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 13))) end) - it('should return 0.0625, -45/360 if right sensor is just above slope column 0', function () - assert.are_equal(ground_query_info(0.0625, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 - 0.0625))) + it('should return 0.0625, 45/360 if right sensor is just above slope column 0', function () + assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 - 0.0625))) end) - it('should return 0, -45/360 if right sensor is at the top of column 0', function () - assert.are_equal(ground_query_info(0, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 8))) + it('should return 0, 45/360 if right sensor is at the top of column 0', function () + assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 8))) end) - it('should return -3, -45/360 if 3px below column 0', function () - assert.are_equal(ground_query_info(-3, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 11))) + it('should return -3, 45/360 if 3px below column 0', function () + assert.are_equal(ground_query_info(-3, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 11))) end) - it('. should return 0.0625, -45/360 if just above slope column 3', function () - assert.are_equal(ground_query_info(0.0625, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 12 - 0.0625))) + it('. should return 0.0625, 45/360 if just above slope column 3', function () + assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 12 - 0.0625))) end) - it('. should return 0, -45/360 if at the top of column 3', function () - assert.are_equal(ground_query_info(0, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 12))) + it('. should return 0, 45/360 if at the top of column 3', function () + assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 12))) 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(-4, -45/360) if 4 (<= max_ground_escape_height) below the 2nd column top', function () - assert.are_equal(ground_query_info(-4, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 16 + 2))) + it('should return ground_query_info(-4, 45/360) if 4 (<= max_ground_escape_height) below the 2nd column top', function () + assert.are_equal(ground_query_info(-4, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 16 + 2))) end) - it('should return ground_query_info(-max_ground_escape_height - 1, -45/360) if max_ground_escape_height - 1 below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) + it('should return ground_query_info(-max_ground_escape_height - 1, 45/360) if max_ground_escape_height - 1 below the bottom', function () + assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) end) - it('should return ground_query_info(-max_ground_escape_height - 1, -45/360) if max_ground_escape_height below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, -45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) + it('should return ground_query_info(-max_ground_escape_height - 1, 45/360) if max_ground_escape_height below the bottom', function () + assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) end) -- step up distance reached, character considered in the air @@ -1161,60 +1161,60 @@ describe('player_char', function () mock_mset(1, 1, desc_slope_45_id) end) - it('. should return 0.0625, 45/360 if right sensors are just a little above column 0', function () - assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 - 0.0625))) + it('. should return 0.0625, 1-45/360 if right sensors are just a little above column 0', function () + assert.are_equal(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 - 0.0625))) end) - it('should return 0, 45/360 if right sensors is at the top of column 0', function () - assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) + it('should return 0, 1-45/360 if right sensors is at the top of column 0', function () + assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) end) - it('should return -1, 45/360 if right sensors is below column 0 by 1px', function () - assert.are_equal(ground_query_info(-1, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 9))) + it('should return -1, 1-45/360 if right sensors is below column 0 by 1px', function () + assert.are_equal(ground_query_info(-1, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 9))) end) - it('should return 1, 45/360 if 1px above slope column 1', function () - assert.are_equal(ground_query_info(1, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 8))) + it('should return 1, 1-45/360 if 1px above slope column 1', function () + assert.are_equal(ground_query_info(1, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 8))) end) - it('should return 0, 45/360 if at the top of column 1', function () - assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 9))) + it('should return 0, 1-45/360 if at the top of column 1', function () + assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 9))) end) - it('should return -2, 45/360 if 2px below column 1', function () - assert.are_equal(ground_query_info(-2, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 11))) + it('should return -2, 1-45/360 if 2px below column 1', function () + assert.are_equal(ground_query_info(-2, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 11))) end) - it('should return 0.0625, 45/360 if just above slope column 0', function () - assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 - 0.0625))) + it('should return 0.0625, 1-45/360 if just above slope column 0', function () + assert.are_equal(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 - 0.0625))) end) - it('should return 0, 45/360 if at the top of column 0', function () - assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) + it('should return 0, 1-45/360 if at the top of column 0', function () + assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) end) - it('should return -3, 45/360 if 3px below column 0', function () - assert.are_equal(ground_query_info(-3, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 11))) + it('should return -3, 1-45/360 if 3px below column 0', function () + assert.are_equal(ground_query_info(-3, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 11))) end) - it('. should return 0.0625, 45/360 if just above slope column 3', function () - assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 11 - 0.0625))) + it('. should return 0.0625, 1-45/360 if just above slope column 3', function () + assert.are_equal(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 11 - 0.0625))) end) - it('. should return 0, 45/360 if at the top of column 3', function () - assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 11))) + it('. should return 0, 1-45/360 if at the top of column 3', function () + assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 11))) end) - it('should return -4, 45/360 if 4px below column 3', function () - assert.are_equal(ground_query_info(-4, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 15))) + it('should return -4, 1-45/360 if 4px below column 3', function () + assert.are_equal(ground_query_info(-4, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 15))) end) - it('should return 0.0625, 45/360 if just above slope column 7', function () - assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 15 - 0.0625))) + it('should return 0.0625, 1-45/360 if just above slope column 7', function () + assert.are_equal(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 15 - 0.0625))) end) - it('should return 0 if, 45/360 at the top of column 7', function () - assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 15))) + it('should return 0 if, 1-45/360 at the top of column 7', function () + assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 15))) end) end) @@ -1226,8 +1226,8 @@ describe('player_char', function () mock_mset(1, 1, asc_slope_22_id) end) - it('should return -4, -22.5/360 if below column 7 by 4px)', function () - assert.are_equal(ground_query_info(-4, -22.5/360), pc:_compute_signed_distance_to_closest_ground(vector(14, 15))) + it('should return -4, 22.5/360 if below column 7 by 4px)', function () + assert.are_equal(ground_query_info(-4, 22.5/360), pc:_compute_signed_distance_to_closest_ground(vector(14, 15))) end) end) @@ -1344,20 +1344,20 @@ describe('player_char', function () assert.are_same({vector(15, 10), 0, false}, {pc:get_bottom_center(), pc.slope_angle, result}) end) - it('should do nothing when character is just on top of the ground, update slope to 45/360 and return true', function () + 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)) local result = pc:_check_escape_from_ground() -- interface - assert.are_same({vector(15, 12), 45/360, true}, {pc:get_bottom_center(), pc.slope_angle, result}) + assert.are_same({vector(15, 12), 1-45/360, true}, {pc:get_bottom_center(), pc.slope_angle, result}) end) - it('should move the character upward just enough to escape ground if character is inside ground, update slope to 45/360 and return true', function () + 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)) local result = pc:_check_escape_from_ground() -- interface - assert.are_same({vector(15, 12), 45/360, true}, {pc:get_bottom_center(), pc.slope_angle, result}) + assert.are_same({vector(15, 12), 1-45/360, true}, {pc:get_bottom_center(), pc.slope_angle, result}) end) it('should do nothing when character is too deep inside the ground, and return true', function () @@ -2003,7 +2003,7 @@ describe('player_char', function () it('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.slope_angle = 1/8 -- 45 deg ascending pc.move_intention.x = 1 pc:_update_ground_speed() @@ -2054,7 +2054,7 @@ describe('player_char', function () 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.slope_angle = 0.125 -- sin(0.125) = -sqrt(2)/2 pc.ascending_slope_time = 0 pc:_update_ground_speed_by_slope(1.8) @@ -2071,7 +2071,7 @@ describe('player_char', function () 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.slope_angle = 0.125 -- sin(0.125) = -sqrt(2)/2 pc.ascending_slope_time = 0.1 pc:_update_ground_speed_by_slope(1.8) @@ -2088,7 +2088,7 @@ describe('player_char', function () 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.slope_angle = 0.125 -- sin(0.125) = -sqrt(2)/2 pc.ascending_slope_time = pc_data.progressive_ascending_slope_duration pc:_update_ground_speed_by_slope(1.8) @@ -2105,7 +2105,7 @@ describe('player_char', function () 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.slope_angle = 0.0625 pc.ascending_slope_time = 77 pc:_update_ground_speed_by_slope(1.8) @@ -2122,7 +2122,7 @@ describe('player_char', function () 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 = 0.125 -- sin(0.125) = - sqrt(2)/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) @@ -2183,7 +2183,7 @@ describe('player_char', function () pc.orientation = horizontal_dirs.right pc.ground_speed = 1.5 pc.move_intention.x = -1 - pc.slope_angle = 0.125 + pc.slope_angle = 1-0.125 pc:_update_ground_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}) @@ -2193,7 +2193,7 @@ describe('player_char', function () pc.orientation = horizontal_dirs.right pc.ground_speed = 1.5 pc.move_intention.x = -1 - pc.slope_angle = 0.0625 + pc.slope_angle = 1-0.0625 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_decel_frame2}, {pc.orientation, pc.ground_speed}) @@ -2288,7 +2288,7 @@ describe('player_char', function () 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.slope_angle = 0.125 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.right, 1.5 - pc_data.ground_friction_frame2}, {pc.orientation, pc.ground_speed}) @@ -2299,7 +2299,7 @@ describe('player_char', function () 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 = 0.125 + pc.slope_angle = 1-0.125 pc:_update_ground_speed_by_intention() assert.are_same({horizontal_dirs.right, 1.5}, {pc.orientation, pc.ground_speed}) @@ -3160,7 +3160,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(6, 7 - pc_data.center_height_standing), - -45/360, + 45/360, false, false ), @@ -3184,7 +3184,7 @@ describe('player_char', function () 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( vector(12, 9 - pc_data.center_height_standing), - -45/360, + 45/360, false, false ) @@ -3194,7 +3194,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(11, 10 - pc_data.center_height_standing), - -45/360, + 45/360, false, false ), @@ -3205,7 +3205,7 @@ describe('player_char', function () 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( vector(12, 9 - pc_data.center_height_standing), - -45/360, + 45/360, false, false ) @@ -3215,7 +3215,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(13, 8 - pc_data.center_height_standing), - -45/360, + 45/360, false, false ), From a3462f4b11632f110dfe131f75697acbd2cbc363 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Tue, 4 Aug 2020 20:31:54 +0200 Subject: [PATCH 050/112] [TEST] Fixed last utests since changing slope angle sign convention For tests already with correct negative sign, still switched to (1-angle) format adding almost eq check if necessary --- src/ingame/playercharacter_utest.lua | 44 +++++++++++++++------------- src/platformer/world_utest.lua | 12 ++++---- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 0567bcbd..74cb4709 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1475,18 +1475,20 @@ describe('player_char', function () pc.motion_state = motion_states.falling pc.velocity.x = sqrt(3)/2 pc.velocity.y = 0.5 - pc.slope_angle = 1/12 -- 30 deg/360 deg + pc.slope_angle = 1-1/12 -- 30 deg/360 deg pc:_enter_motion_state(motion_states.grounded) - assert.are_equal(1, pc.ground_speed) + -- 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) it('(falling -> grounded, 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 = 0.125 -- 45 deg/360 deg + pc.slope_angle = 1-0.125 -- 45 deg/360 deg pc:_enter_motion_state(motion_states.grounded) @@ -1497,7 +1499,7 @@ describe('player_char', function () pc.motion_state = motion_states.falling pc.velocity.x = -4 pc.velocity.y = 5 - pc.slope_angle = 0.125 -- 45 deg/360 deg + pc.slope_angle = 1-0.125 -- -45 deg/360 deg pc:_enter_motion_state(motion_states.grounded) @@ -1747,7 +1749,7 @@ describe('player_char', function () 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.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}) @@ -1759,7 +1761,7 @@ describe('player_char', function () end) it('should set the slope angle to 0.25', function () - pc.slope_angle = -0.25 + pc.slope_angle = 1-0.25 pc:_update_platformer_motion_grounded() assert.are_equal(0.25, pc.slope_angle) end) @@ -1849,7 +1851,7 @@ describe('player_char', function () end) it('should set the slope angle to 0.5', function () - pc.slope_angle = -0.25 + pc.slope_angle = 1-0.25 pc:_update_platformer_motion_grounded() assert.are_equal(0.5, pc.slope_angle) end) @@ -1894,7 +1896,7 @@ describe('player_char', function () 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.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}) @@ -2402,7 +2404,7 @@ describe('player_char', function () next_ground_step_mock = stub(player_char, "_next_ground_step", function (self, horizontal_dir, motion_result) local step_vec = horizontal_dir_vectors[horizontal_dir] motion_result.position = motion_result.position + step_vec - motion_result.slope_angle = -0.125 + motion_result.slope_angle = 1-0.125 end) end) @@ -2433,12 +2435,12 @@ describe('player_char', function () -- ?? 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.position = vector(3, 4) - pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 + pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = 1 -- * slope cos = 0.5 assert.are_equal(motion.ground_motion_result( vector(3.5, 4), - -1/6, -- character has not moved by a full pixel, so visible position and slope remains the same + 1-1/6, -- character has not moved by a full pixel, so visible position and slope remains the same false, false ), @@ -2453,7 +2455,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(4, 4), - -0.125, + 1-0.125, false, false ), @@ -2467,7 +2469,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(0.5, 4), - -0.125, + 1-0.125, false, false ), @@ -2568,7 +2570,7 @@ describe('player_char', function () -- 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/6 -- cos(-pi/3) = 1/2 + pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = 1 -- * slope cos = -0.5 assert.are_equal(motion.ground_motion_result( @@ -2585,7 +2587,7 @@ describe('player_char', function () -- 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.position = vector(-4, 4) - pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 + pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = -2 -- * slope cos = -1 assert.are_equal(motion.ground_motion_result( @@ -2638,13 +2640,13 @@ describe('player_char', function () -- ?? 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.position = vector(4, 4) - pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 + 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 assert.are_equal(motion.ground_motion_result( vector(4.75, 4), - -1/6, -- character has not moved by a full pixel, so visible position and slope remains the same + 1-1/6, -- character has not moved by a full pixel, so visible position and slope remains the same false, false ), @@ -2655,12 +2657,12 @@ describe('player_char', function () 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.position = vector(-4.1, 4) - pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 + pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = -1.5 -- * slope cos = -0.75 assert.are_equal(motion.ground_motion_result( vector(-4.85, 4), - -1/6, -- character has not moved by a full pixel, so visible position and slope remains the same + 1-1/6, -- character has not moved by a full pixel, so visible position and slope remains the same false, false ), @@ -2670,7 +2672,7 @@ describe('player_char', function () 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.position = vector(4, 4) - pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 + 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 @@ -2686,7 +2688,7 @@ describe('player_char', function () 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.position = vector(-4, 4) - pc.slope_angle = -1/6 -- cos(-pi/3) = 1/2 + pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = -3 -- * slope cos = -1.5 assert.are_equal(motion.ground_motion_result( diff --git a/src/platformer/world_utest.lua b/src/platformer/world_utest.lua index 62377f33..3cc8e032 100644 --- a/src/platformer/world_utest.lua +++ b/src/platformer/world_utest.lua @@ -51,7 +51,7 @@ describe('world (with mock tiles data setup)', function () end) it('should return 3 on column 3', function () - assert.are_same({3, -22.5 / 360}, {world._compute_column_height_at(location(1, 1), 3)}) + assert.are_same({3, 22.5 / 360}, {world._compute_column_height_at(location(1, 1), 3)}) end) end) @@ -151,7 +151,7 @@ describe('world (with mock tiles data setup)', function () end) it('should return true on (8, 15)', function () - assert.are_same({true, -45/360}, {world.get_pixel_collision_info(8, 15)}) + assert.are_same({true, 45/360}, {world.get_pixel_collision_info(8, 15)}) end) it('should return {false, nil} on (8, 16)', function () @@ -163,11 +163,11 @@ describe('world (with mock tiles data setup)', function () end) it('should return true on (9, 14)', function () - assert.are_same({true, -45/360}, {world.get_pixel_collision_info(9, 14)}) + assert.are_same({true, 45/360}, {world.get_pixel_collision_info(9, 14)}) end) it('should return true on (9, 15)', function () - assert.are_same({true, -45/360}, {world.get_pixel_collision_info(9, 15)}) + assert.are_same({true, 45/360}, {world.get_pixel_collision_info(9, 15)}) end) it('should return {false, nil} on (9, 16)', function () @@ -179,11 +179,11 @@ describe('world (with mock tiles data setup)', function () end) it('should return true on (15, 8)', function () - assert.are_same({true, -45/360}, {world.get_pixel_collision_info(15, 8)}) + assert.are_same({true, 45/360}, {world.get_pixel_collision_info(15, 8)}) end) it('should return true on (15, 15)', function () - assert.are_same({true, -45/360}, {world.get_pixel_collision_info(15, 15)}) + assert.are_same({true, 45/360}, {world.get_pixel_collision_info(15, 15)}) end) it('should return {false, nil} on (15, 16)', function () From dc9de1ac9563fdc5edf212e1400d14b68ca8f470 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sun, 9 Aug 2020 13:29:39 +0200 Subject: [PATCH 051/112] [TOKENS] 9183 -> 9501 Serialized sprite data to reduce tokens Token count increased, so I definitely need to reduce serialization code and serialize more data --- pico-boots | 2 +- src/data/playercharacter_data.lua | 42 ++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/pico-boots b/pico-boots index 674e9974..dc8a5168 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 674e99749d3c475cac5f577230ec4511317bdac0 +Subproject commit dc8a5168239f961663adc6f2835f52584d3673f4 diff --git a/src/data/playercharacter_data.lua b/src/data/playercharacter_data.lua index 7234fdd2..685ca6b2 100644 --- a/src/data/playercharacter_data.lua +++ b/src/data/playercharacter_data.lua @@ -1,3 +1,4 @@ +local serialization = require("engine/data/serialization") local sprite_data = require("engine/render/sprite_data") local animated_sprite_data = require("engine/render/animated_sprite_data") @@ -114,21 +115,32 @@ local playercharacter_data = { -- sprite -- stand right - sonic_sprite_data_table = { - ["idle"] = sprite_data(sprite_id_location(0, 8), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run1"] = sprite_data(sprite_id_location(2, 8), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run2"] = sprite_data(sprite_id_location(4, 8), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run3"] = sprite_data(sprite_id_location(6, 8), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run4"] = sprite_data(sprite_id_location(8, 8), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run5"] = sprite_data(sprite_id_location(10, 8), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run6"] = sprite_data(sprite_id_location(12, 8), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run7"] = sprite_data(sprite_id_location(14, 8), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run8"] = sprite_data(sprite_id_location(0, 10), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run9"] = sprite_data(sprite_id_location(2, 10), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run10"] = sprite_data(sprite_id_location(4, 10), tile_vector(2, 2), vector(11, 8), colors.pink), - ["run11"] = sprite_data(sprite_id_location(6, 10), tile_vector(2, 2), vector(11, 8), colors.pink), - ["spin"] = sprite_data(sprite_id_location(0, 12), tile_vector(2, 2), vector(5, 5), colors.pink), - }, + -- colors.pink: 14 + sonic_sprite_data_table = serialization.parse_expression( + -- anim name = sprite_data( + -- id_loc, span, pivot, transparent_color (14: pink)) + [[{ + ["idle"] = {{0, 8}, {2, 2}, {11, 8}, 14}, + ["run1"] = {{2, 8}, {2, 2}, {11, 8}, 14}, + ["run2"] = {{4, 8}, {2, 2}, {11, 8}, 14}, + ["run3"] = {{6, 8}, {2, 2}, {11, 8}, 14}, + ["run4"] = {{8, 8}, {2, 2}, {11, 8}, 14}, + ["run5"] = {{10, 8}, {2, 2}, {11, 8}, 14}, + ["run6"] = {{12, 8}, {2, 2}, {11, 8}, 14}, + ["run7"] = {{14, 8}, {2, 2}, {11, 8}, 14}, + ["run8"] = {{0, 10}, {2, 2}, {11, 8}, 14}, + ["run9"] = {{2, 10}, {2, 2}, {11, 8}, 14}, + ["run10"] = {{4, 10}, {2, 2}, {11, 8}, 14}, + ["run11"] = {{6, 10}, {2, 2}, {11, 8}, 14}, + ["spin"] = {{0, 12}, {2, 2}, {5, 5}, 14}, + }]], function (t) + return sprite_data( + sprite_id_location(t[1][1], t[1][2]), -- id_loc + tile_vector(t[2][1], t[2][2]), -- span + vector(t[3][1], t[3][2]), -- pivot + t[4] -- transparent_color + ) + end), -- minimum playback speed for "run" animation, to avoid very slow animation -- 5/16: the 5 counters the 5 duration frames of ["run"] below, 1/8 to represent max duration 8 in SPG:Animations From 4f36bc05d8666081f5f724b488404a8ba9b9f8a4 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sun, 9 Aug 2020 18:10:00 +0200 Subject: [PATCH 052/112] [DATA] Simplified PC data by removing [" and "] for keys --- src/data/playercharacter_data.lua | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/data/playercharacter_data.lua b/src/data/playercharacter_data.lua index 685ca6b2..584b413e 100644 --- a/src/data/playercharacter_data.lua +++ b/src/data/playercharacter_data.lua @@ -117,28 +117,28 @@ local playercharacter_data = { -- stand right -- colors.pink: 14 sonic_sprite_data_table = serialization.parse_expression( - -- anim name = sprite_data( - -- id_loc, span, pivot, transparent_color (14: pink)) + --anim_name = sprite_data( + -- id_loc, span, pivot, transparent_color (14: pink)) [[{ - ["idle"] = {{0, 8}, {2, 2}, {11, 8}, 14}, - ["run1"] = {{2, 8}, {2, 2}, {11, 8}, 14}, - ["run2"] = {{4, 8}, {2, 2}, {11, 8}, 14}, - ["run3"] = {{6, 8}, {2, 2}, {11, 8}, 14}, - ["run4"] = {{8, 8}, {2, 2}, {11, 8}, 14}, - ["run5"] = {{10, 8}, {2, 2}, {11, 8}, 14}, - ["run6"] = {{12, 8}, {2, 2}, {11, 8}, 14}, - ["run7"] = {{14, 8}, {2, 2}, {11, 8}, 14}, - ["run8"] = {{0, 10}, {2, 2}, {11, 8}, 14}, - ["run9"] = {{2, 10}, {2, 2}, {11, 8}, 14}, - ["run10"] = {{4, 10}, {2, 2}, {11, 8}, 14}, - ["run11"] = {{6, 10}, {2, 2}, {11, 8}, 14}, - ["spin"] = {{0, 12}, {2, 2}, {5, 5}, 14}, + idle = {{0, 8}, {2, 2}, {11, 8}, 14}, + run1 = {{2, 8}, {2, 2}, {11, 8}, 14}, + run2 = {{4, 8}, {2, 2}, {11, 8}, 14}, + run3 = {{6, 8}, {2, 2}, {11, 8}, 14}, + run4 = {{8, 8}, {2, 2}, {11, 8}, 14}, + run5 = {{10, 8}, {2, 2}, {11, 8}, 14}, + run6 = {{12, 8}, {2, 2}, {11, 8}, 14}, + run7 = {{14, 8}, {2, 2}, {11, 8}, 14}, + run8 = {{0, 10}, {2, 2}, {11, 8}, 14}, + run9 = {{2, 10}, {2, 2}, {11, 8}, 14}, + run10 = {{4, 10}, {2, 2}, {11, 8}, 14}, + run11 = {{6, 10}, {2, 2}, {11, 8}, 14}, + spin = {{0, 12}, {2, 2}, {5, 5}, 14}, }]], function (t) return sprite_data( sprite_id_location(t[1][1], t[1][2]), -- id_loc tile_vector(t[2][1], t[2][2]), -- span vector(t[3][1], t[3][2]), -- pivot - t[4] -- transparent_color + t[4] -- transparent_color ) end), From b579c3e5e4cc7dbcf3833cce566924f2936aec17 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sun, 9 Aug 2020 18:10:32 +0200 Subject: [PATCH 053/112] [TOKENS] -> 8663 Serialized collision_data --- src/data/collision_data.lua | 233 ++++++++++++++++++++++++------------ 1 file changed, 158 insertions(+), 75 deletions(-) diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua index 6582ed68..8ed46409 100644 --- a/src/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -1,3 +1,5 @@ +local serialization = require("engine/data/serialization") + local tile = require("platformer/tile") local tile_data = tile.tile_data @@ -22,81 +24,162 @@ return { -- all angles are defined with atan2 using top-left XY convention to avoid issues -- proto tiles may pass values manually, but in this case make sure to enter the angle -- between 0 and 1, as if Sonic was running a loop counter-clockwise - tiles_data = { - [49] = tile_data(sprite_id_location(1, 2), atan2(8, -2)), -- 49 @ (1, 3) - [50] = tile_data(sprite_id_location(0, 2), 0), -- 50 @ (2, 3) - [51] = tile_data(sprite_id_location(0, 2), 0), -- 51 @ (3, 3) - [52] = tile_data(sprite_id_location(0, 2), 0), -- 52 @ (4, 3) - [53] = tile_data(sprite_id_location(5, 2), atan2(8, 2)), -- 53 @ (5, 3) - [54] = tile_data(sprite_id_location(6, 2), 0), -- 54 @ (6, 3) - [65] = tile_data(sprite_id_location(0, 2), 0), -- 65 @ (1, 4) - [66] = tile_data(sprite_id_location(0, 2), 0), -- 66 @ (2, 4) - [67] = tile_data(sprite_id_location(0, 2), 0), -- 67 @ (3, 4) - [68] = tile_data(sprite_id_location(0, 2), 0), -- 68 @ (4, 4) - [81] = tile_data(sprite_id_location(0, 2), 0), -- 81 @ (1, 5) - [82] = tile_data(sprite_id_location(0, 2), 0), -- 82 @ (2, 5) - [83] = tile_data(sprite_id_location(0, 2), 0), -- 83 @ (3, 5) - [84] = tile_data(sprite_id_location(0, 2), 0), -- 84 @ (4, 5) - [97] = tile_data(sprite_id_location(0, 2), 0), -- 97 @ (1, 6) - [98] = tile_data(sprite_id_location(0, 2), 0), -- 98 @ (2, 6) - [99] = tile_data(sprite_id_location(0, 2), 0), -- 99 @ (3, 6) - [100]= tile_data(sprite_id_location(0, 2), 0), --100 @ (4, 6) - [114]= tile_data(sprite_id_location(0, 2), 0), --114 @ (2, 7) - [115]= tile_data(sprite_id_location(0, 2), 0), --115 @ (3, 7) - -- low slopes ascending and descending - [103] = tile_data(sprite_id_location(7, 5), atan2(8, -2)), -- 87 @ (7, 6) - [104] = tile_data(sprite_id_location(8, 5), atan2(8, -1)), -- 88 @ (8, 6) - [105] = tile_data(sprite_id_location(9, 5), atan2(8, -2)), -- 89 @ (9, 6) - [106] = tile_data(sprite_id_location(10, 5), atan2(8, -2)), -- 90 @ (10, 6) - [107] = tile_data(sprite_id_location(11, 5), atan2(8, 2)), -- 91 @ (11, 6) - [108] = tile_data(sprite_id_location(12, 5), atan2(8, 2)), -- 92 @ (12, 6) - [109] = tile_data(sprite_id_location(13, 5), atan2(8, 1)), -- 93 @ (13, 6) - [110] = tile_data(sprite_id_location(14, 5), atan2(8, 2)), -- 94 @ (14, 6) - -- bottom of said slopes (full tiles) - [119]= tile_data(sprite_id_location(0, 2), 0), -- 119 @ (7, 7) - [120]= tile_data(sprite_id_location(0, 2), 0), -- 120 @ (8, 7) - [121]= tile_data(sprite_id_location(0, 2), 0), -- 121 @ (9, 7) - [122]= tile_data(sprite_id_location(0, 2), 0), -- 122 @ (10, 7) - [123]= tile_data(sprite_id_location(0, 2), 0), -- 123 @ (11, 7) - [124]= tile_data(sprite_id_location(0, 2), 0), -- 124 @ (12, 7) - [125]= tile_data(sprite_id_location(0, 2), 0), -- 125 @ (13, 7) - [126]= tile_data(sprite_id_location(0, 2), 0), -- 126 @ (14, 7) - -- loop (start from top-left tile, then rotate clockwise) - -- note that we always write angles as atan2(dx, dy) with motion (dx, dy) - -- as if Sonic was running the loop counter-clockwise - -- this allows to identify ceiling angles vs floor angles easily - -- (ceiling angles between 0.25 and 0.75) - [8]= tile_data(sprite_id_location(12, 0), atan2(-4, 4)), -- 8 @ (8, 0) - [9]= tile_data(sprite_id_location(13, 0), atan2(-8, 4)), -- 9 @ (9, 0) - [10]= tile_data(sprite_id_location(14, 0), atan2(-8, -4)), -- 10 @ (10, 0) - [11]= tile_data(sprite_id_location(15, 0), atan2(-4, -4)), -- 11 @ (11, 0) - [27]= tile_data(sprite_id_location(15, 1), atan2(-4, -8)), -- 27 @ (11, 1) - [43]= tile_data(sprite_id_location(15, 2), atan2(4, -8)), -- 43 @ (11, 2) - [59]= tile_data(sprite_id_location(15, 3), atan2(4, -4)), -- 59 @ (11, 3) - [58]= tile_data(sprite_id_location(14, 3), atan2(8, -4)), -- 58 @ (10, 3) - [57]= tile_data(sprite_id_location(13, 3), atan2(8, 4)), -- 57 @ (9, 3) - [56]= tile_data(sprite_id_location(12, 3), atan2(4, 4)), -- 56 @ (8, 3) - [40]= tile_data(sprite_id_location(12, 2), atan2(4, 8)), -- 40 @ (8, 2) - [24]= tile_data(sprite_id_location(12, 1), atan2(-4, 8)), -- 24 @ (8, 1) - -- loop bottom ground (full) - [72]= tile_data(sprite_id_location(12, 4), 0), -- 72 @ (8, 4) - [73]= tile_data(sprite_id_location(13, 4), 0), -- 73 @ (9, 4) - [74]= tile_data(sprite_id_location(14, 4), 0), -- 74 @ (10, 4) - [75]= tile_data(sprite_id_location(15, 4), 0), -- 75 @ (11, 4) - -- proto (black and white tiles being their own collision masks) - -- must match tile_data.lua - -- if too heavy, surround with #itest and create a separate spritesheet for itests with only polygonal tiles - -- stored in some proto_data.p8 or test_data.p8 - -- this will allow us to reuse the extra space left by removing proto tiles for release (adding FX, etc.) - [32] = tile_data(sprite_id_location(0, 2), 0), -- 32 @ (0, 2) FULL TILE # - [80] = tile_data(sprite_id_location(0, 5), 0), -- 80 @ (0, 5) HALF TILE (4px high) = - [96] = tile_data(sprite_id_location(0, 6), 0), -- 96 @ (0, 6) FLAT LOW TILE (2px high) _ - [64] = tile_data(sprite_id_location(0, 4), 0), -- 64 @ (0, 4) BOTTOM-RIGHT QUARTER TILE (4px high) r - [112]= tile_data(sprite_id_location(1, 7), 0.125), -- 112 @ (1, 7) ASCENDING 45 / - [113]= tile_data(sprite_id_location(0, 7), 0.0625), -- 113 @ (0, 7) ASCENDING 22.5 < - [116]= tile_data(sprite_id_location(4, 7), 1-0.125), -- 116 @ (4, 7) DESCENDING 45 \ - [117]= tile_data(sprite_id_location(5, 7), atan2(8, -4)), -- 117 @ (5, 7) higher 2:1 ascending slope (completes 58 from loop) - } + --[[ + EXPLANATION OF EACH TILE ID <-> (I, J) IN SPRITESHEET + (put in comments outside data string because we don't have a preprocess step able to strip comments + inside data strings, nor is parse_expression able to ignore comments) + # common tiles (flat or very low slope) + 49 @ (1, 3) + 50 @ (2, 3) + 51 @ (3, 3) + 52 @ (4, 3) + 53 @ (5, 3) + 54 @ (6, 3) + 65 @ (1, 4) + 66 @ (2, 4) + 67 @ (3, 4) + 68 @ (4, 4) + 81 @ (1, 5) + 82 @ (2, 5) + 83 @ (3, 5) + 84 @ (4, 5) + 97 @ (1, 6) + 98 @ (2, 6) + 99 @ (3, 6) + 100 @ (4, 6) + 114 @ (2, 7) + 115 @ (3, 7) + # low slopes ascending and descending + 87 @ (7, 6) + 88 @ (8, 6) + 89 @ (9, 6) + 90 @ (10, 6) + 91 @ (11, 6) + 92 @ (12, 6) + 93 @ (13, 6) + 94 @ (14, 6) + # bottom of said slopes (full tiles) + 119 @ (7, 7) + 120 @ (8, 7) + 121 @ (9, 7) + 122 @ (10, 7) + 123 @ (11, 7) + 124 @ (12, 7) + 125 @ (13, 7) + 126 @ (14, 7) + # loop (start from top-left tile, then rotate clockwise) + # note that we always write angles as atan2(dx, dy) with motion (dx, dy) + # as if Sonic was running the loop counter-clockwise + # this allows to identify ceiling angles vs floor angles easily + # (ceiling angles between 0.25 and 0.75) + 8 @ (8, 0) + 9 @ (9, 0) + 10 @ (10, 0) + 11 @ (11, 0) + 27 @ (11, 1) + 43 @ (11, 2) + 59 @ (11, 3) + 58 @ (10, 3) + 57 @ (9, 3) + 56 @ (8, 3) + 40 @ (8, 2) + 24 @ (8, 1) + # loop bottom ground (full) + 72 @ (8, 4) + 73 @ (9, 4) + 74 @ (10, 4) + 75 @ (11, 4) + + # proto (black and white tiles being their own collision masks) + # must match tile_data.lua + # if too heavy, surround with #itest and create a separate spritesheet for itests with only polygonal tiles + # stored in some proto_data.p8 or test_data.p8 + # this will allow us to reuse the extra space left by removing proto tiles for release (adding FX, etc.) + 32 @ (0, 2) FULL TILE # + 80 @ (0, 5) HALF TILE (4px high) = + 96 @ (0, 6) FLAT LOW TILE (2px high) _ + 64 @ (0, 4) BOTTOM-RIGHT QUARTER TILE (4px high) r + 112 @ (1, 7) ASCENDING 45 / slope_angle: 0.125 = atan2(1, -1) + 113 @ (0, 7) ASCENDING 22.5 < slope_angle: 0.0625 ~= atan2(8, -4) (actually 0.0738) but kept for historical utest/itest reasons + 116 @ (4, 7) DESCENDING 45 \ slope_angle: 1-0.125 = atan2(1, 1) + 117 @ (5, 7) higher 2:1 ascending slope (completes 58 from loop) + --]] + + tiles_data = serialization.parse_expression( + --[tile_id] = tile_data( + -- id_loc, slope_angle=atan2(x, y) or angle (proto only)) + [[{ + [49] = {{1, 2}, {8, -2}}, + [50] = {{0, 2}, {8, 0}}, + [51] = {{0, 2}, {8, 0}}, + [52] = {{0, 2}, {8, 0}}, + [53] = {{5, 2}, {8, 2}}, + [54] = {{6, 2}, {8, 0}}, + [65] = {{0, 2}, {8, 0}}, + [66] = {{0, 2}, {8, 0}}, + [67] = {{0, 2}, {8, 0}}, + [68] = {{0, 2}, {8, 0}}, + [81] = {{0, 2}, {8, 0}}, + [82] = {{0, 2}, {8, 0}}, + [83] = {{0, 2}, {8, 0}}, + [84] = {{0, 2}, {8, 0}}, + [97] = {{0, 2}, {8, 0}}, + [98] = {{0, 2}, {8, 0}}, + [99] = {{0, 2}, {8, 0}}, + [100]= {{0, 2}, {8, 0}}, + [114]= {{0, 2}, {8, 0}}, + [115]= {{0, 2}, {8, 0}}, + + [103] = {{7, 5}, {8, -2}}, + [104] = {{8, 5}, {8, -1}}, + [105] = {{9, 5}, {8, -2}}, + [106] = {{10, 5}, {8, -2}}, + [107] = {{11, 5}, {8, 2}}, + [108] = {{12, 5}, {8, 2}}, + [109] = {{13, 5}, {8, 1}}, + [110] = {{14, 5}, {8, 2}}, + + [119]= {{0, 2}, {8, 0}}, + [120]= {{0, 2}, {8, 0}}, + [121]= {{0, 2}, {8, 0}}, + [122]= {{0, 2}, {8, 0}}, + [123]= {{0, 2}, {8, 0}}, + [124]= {{0, 2}, {8, 0}}, + [125]= {{0, 2}, {8, 0}}, + [126]= {{0, 2}, {8, 0}}, + + [8]= {{12, 0}, {-4, 4}}, + [9]= {{13, 0}, {-8, 4}}, + [10]= {{14, 0}, {-8, -4}}, + [11]= {{15, 0}, {-4, -4}}, + [27]= {{15, 1}, {-4, -8}}, + [43]= {{15, 2}, {4, -8}}, + [59]= {{15, 3}, {4, -4}}, + [58]= {{14, 3}, {8, -4}}, + [57]= {{13, 3}, {8, 4}}, + [56]= {{12, 3}, {4, 4}}, + [40]= {{12, 2}, {4, 8}}, + [24]= {{12, 1}, {-4, 8}}, + + [72]= {{12, 4}, {8, 0}}, + [73]= {{13, 4}, {8, 0}}, + [74]= {{14, 4}, {8, 0}}, + [75]= {{15, 4}, {8, 0}}, + + [32] = {{0, 2}, {8, 0}}, + [80] = {{0, 5}, {8, 0}}, + [96] = {{0, 6}, {8, 0}}, + [64] = {{0, 4}, {8, 0}}, + [112]= {{1, 7}, {8, -8}}, + [113]= {{0, 7}, 0.0625}, + [116]= {{4, 7}, {8, 8}}, + [117]= {{5, 7}, {8, -4}}, + }]], function (t) + -- t[2] may be {x, y} to use for atan2 or slope_angle directly + -- this is only for [113], if we update utests/itests to use the more correct atan2(8, -4) then we can get rid of + -- that ternary check + return tile_data(sprite_id_location(t[1][1], t[1][2]), type(t[2]) == 'table' and atan2(t[2][1], t[2][2]) or t[2]) + end) } From 0c092f558e70923451f171148a30530898e7d550 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sun, 9 Aug 2020 19:01:13 +0200 Subject: [PATCH 054/112] [TOKENS] -> 8661 minor reduction --- src/data/stage_data.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/stage_data.lua b/src/data/stage_data.lua index ed1ad8f9..4f3c8418 100644 --- a/src/data/stage_data.lua +++ b/src/data/stage_data.lua @@ -26,7 +26,7 @@ return { spawn_location = location(5, 20), -- the x to reach to finish the stage - goal_x = 100 * 8, + goal_x = 800, -- 100 tiles -- background color background_color = colors.dark_blue, From c5b0af1f313b6a78424fdf929121073cfe9d2482 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sun, 9 Aug 2020 19:06:18 +0200 Subject: [PATCH 055/112] [TOKENS] -> 8618 Serialized animated sprite data --- src/data/playercharacter_data.lua | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/data/playercharacter_data.lua b/src/data/playercharacter_data.lua index 584b413e..2e916f30 100644 --- a/src/data/playercharacter_data.lua +++ b/src/data/playercharacter_data.lua @@ -150,11 +150,16 @@ local playercharacter_data = { } -- define animated sprite data in a second step, as it needs sprite data to be defined first -playercharacter_data.sonic_animated_sprite_data_table = { - ["idle"] = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"idle"}, 10, true), - ["run"] = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, - {"run1", "run2", "run3", "run4", "run5", "run6", "run7", "run8", "run9", "run10", "run11"}, 5, true), - ["spin"] = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, {"spin"}, 10, true), -} +playercharacter_data.sonic_animated_sprite_data_table = serialization.parse_expression( + --[anim_name] = animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, + -- sprite_keys, step_frames, loop_mode) + [[{ + idle = {{"idle"}, 10, true}, + run = {{"run1", "run2", "run3", "run4", "run5", "run6", "run7", "run8", "run9", "run10", "run11"}, + 5, true}, + spin = {{"spin"}, 10, true}, +}]], function (t) + return animated_sprite_data.create(playercharacter_data.sonic_sprite_data_table, t[1], t[2], t[3]) +end) return playercharacter_data From 11d2aad18359b6f07f2b2284924688cdb77c3e7b Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sun, 9 Aug 2020 19:41:58 +0200 Subject: [PATCH 056/112] [TOKENS] -> 8287 Updated submodule which extracts string (and dump) from helper and pico-sonic is not using it --- pico-boots | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pico-boots b/pico-boots index dc8a5168..79c1e981 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit dc8a5168239f961663adc6f2835f52584d3673f4 +Subproject commit 79c1e9817933e2fd2bb5bde200e2935f84b8cce1 From 05cf3d2a2751ae4523846a1279ca5aea5b50bb12 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sun, 9 Aug 2020 19:55:11 +0200 Subject: [PATCH 057/112] [TOKENS] -> 8252 Updated pico-boots to deprecate string_tonum --- pico-boots | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pico-boots b/pico-boots index 79c1e981..ce81407e 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 79c1e9817933e2fd2bb5bde200e2935f84b8cce1 +Subproject commit ce81407e209fad4b6113730920850274ce8fffe0 From 326f0bd600b1b04fb722a537e577374a079bca29 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sun, 9 Aug 2020 19:59:39 +0200 Subject: [PATCH 058/112] [TOKENS] -> 7680 Updated pico-boots to extract more functions from helper into string (which is not used in pico-sonic) --- pico-boots | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pico-boots b/pico-boots index ce81407e..5e43c71f 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit ce81407e209fad4b6113730920850274ce8fffe0 +Subproject commit 5e43c71fdbc24f20d6de1f23c36ca3feeef84e5f From 47857588ca33c3de87b9b114e8928a8f102f555c Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Sun, 9 Aug 2020 20:15:32 +0200 Subject: [PATCH 059/112] [ENGINE] Updated build script --- pico-boots | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pico-boots b/pico-boots index 5e43c71f..034a0015 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 5e43c71fdbc24f20d6de1f23c36ca3feeef84e5f +Subproject commit 034a001530ea25a4c4bcfff699aa99278fde44d6 From e8f1031cb03ffcf8ae38e5ed91269d2cc4bc3347 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Tue, 11 Aug 2020 12:19:10 +0200 Subject: [PATCH 060/112] [VERSION] Bumped to 3.1 [CHANGELOG] Added changes for 3.1 --- .travis.yml | 2 +- CHANGELOG.md | 13 +++++++++++++ README.md | 2 +- build_game.sh | 2 +- build_itest.sh | 2 +- build_pico8_utests.sh | 2 +- export_cartridge_release.p8 | 8 ++++---- install_cartridge_linux.sh | 2 +- run_game.sh | 2 +- run_itest.sh | 2 +- run_pico8_utests.sh | 2 +- 11 files changed, 26 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 01dfe2ab..28f62753 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,7 +69,7 @@ deploy: provider: releases api_key: secure: bfXQQ0AXGHgXiq0xOxhYQ2AXX/flQnxJh/+eA/HUGfwdoPDq0QTdqFA/3jEMWkJSsFKEBVKDjJGCt24QPxUIjTu91r1wyCNdL2KlNfnogRjWAVutRZxB/OC2HWR3kJtPjkFQBCsOXHBxGI3hMJL7LWr5WfNsSGMbcRMfvphxFT3ER8XBHAUEJY6roITm6noHroqQt8Uye+0+rkGqJ8QslKRqq8qBZMZeOiOrh6SBdlhsGw0KqNno/dMXQxx2ZCrh/VUeWjNvxzXe/mZjfBPbhvyecN7jz+FytEdAhdt1Dy37hhyOAkDfxLGGsH1YAAfinH8uFwoSRo0MH8fuhdXpT7jUXuAgP9/RS0FEiZDdX+J/FdncCbnoDfE9B4Dt3L3srISeiNwxKK5sx2kzyWvftK30pV1+zEgnbVEKGPIIeGb5wYWSCmzHf+CfLMk+bzeznTrpo/irY/vjoRBefNaVWXqLygrNWxM1uIMJae+OA3MYeUSYd1lpCyRw98i3GC7si68M9OaDeLoDjnqOLqvhurB/RmLzCU7mCYipn2kxykAOdevWN73cyx9VhdFy2GPE5VDw6EO6ZQP04KaeYxP2pgR4ts2kYWpVvf1PGg+2yN4QMkVhrWV+6dG2jtUO0BrCqt5Tpw0I3C3aFmBjjzFBBuKsZpr2yUG3roxnu1Dhww0= - file: build/picosonic_v3.0_release.p8 + file: build/picosonic_v3.1_release.p8 on: tags: 'true' skip_cleanup: 'true' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1667e754..efa6e17a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.1] - 2020-08-11 +### Added +- Angel Island tiles +- Serialization module (represent data as string) + +### Changed +- Original feature: Reduced Deceleration on Descending Slope +- Original feature: No Friction on Steep Descending Slope +- Original feature: Progressive Ascending Slope Factor +- Set minimum playback speed for Running animation +- Reduced tokens heavily by extracting code in modules and using data serialization + ## [3.0] - 2020-07-11 ### Added - Game: split airborne state into falling and air_spin to only play Spin animation on jump @@ -84,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Test: all busted unit tests in separator folder tests [Unreleased]: https://github.com/hsandt/sonic-pico8/compare/v3.0...HEAD +[3.1]: https://github.com/hsandt/sonic-pico8/compare/v3.0-sprite-anim...v3.1 [3.0]: https://github.com/hsandt/sonic-pico8/compare/v2.3-sprite-anim...v3.0 [2.3-sprite-anim]: https://github.com/hsandt/sonic-pico8/compare/v2.2...v2.3-sprite-anim [2.2]: https://github.com/hsandt/sonic-pico8/compare/v2.1...v2.2 diff --git a/README.md b/README.md index 475d4e91..ab2d941d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Works with PICO-8 0.2.0i and 0.2.1b. ## Features -Version: 3.0 +Version: 3.1 ### Physics diff --git a/build_game.sh b/build_game.sh index 6a4a911b..015880cf 100755 --- a/build_game.sh +++ b/build_game.sh @@ -13,7 +13,7 @@ build_output_path="$(dirname "$0")/build" author="hsandt" title="pico-sonic" cartridge_stem="picosonic" -version="3.0" +version="3.1" help() { echo "Build a PICO-8 cartridge with the passed config." diff --git a/build_itest.sh b/build_itest.sh index eec19e32..19647064 100755 --- a/build_itest.sh +++ b/build_itest.sh @@ -16,7 +16,7 @@ build_output_path="$(dirname "$0")/build" author="hsandt" title="pico-sonic itests (all)" cartridge_stem="picosonic_itest_all" -version="3.0" +version="3.1" config='itest' # symbols='assert,log,visual_logger,tuner,profiler,mouse,itest' # cheat needed to set debug motion mode diff --git a/build_pico8_utests.sh b/build_pico8_utests.sh index 072cf581..e187db18 100755 --- a/build_pico8_utests.sh +++ b/build_pico8_utests.sh @@ -16,7 +16,7 @@ build_output_path="$(dirname "$0")/build" author="hsandt" title="pico-sonic pico8 utests (all)" cartridge_stem="picosonic_pico8_utests_all" -version="3.0" +version="3.1" config='debug' symbols='assert,log' diff --git a/export_cartridge_release.p8 b/export_cartridge_release.p8 index 34d51581..c903709d 100644 --- a/export_cartridge_release.p8 +++ b/export_cartridge_release.p8 @@ -10,14 +10,14 @@ __lua__ -- Paths are relative to PICO-8 carts directory. cd("picosonic") -load("picosonic_v3.0_release.p8") +load("picosonic_v3.1_release.p8") -- png cartridge export is done via SAVE -- the metadata label is used automatically -save("picosonic_v3.0_release.p8.png") +save("picosonic_v3.1_release.p8.png") -- other exports are done via EXPORT, and can use an icon -- instead of the .p8.png label -- icon is a 16x16 square => -s 2 tiles wide -- with top-left at sprite 2 => -i 2 -- on pink (color 14) background => -c 14 -export("picosonic_v3.0_release.bin -i 2 -s 2 -c 14") -export("picosonic_v3.0_release.html -i 2 -s 2 -c 14") +export("picosonic_v3.1_release.bin -i 2 -s 2 -c 14") +export("picosonic_v3.1_release.html -i 2 -s 2 -c 14") diff --git a/install_cartridge_linux.sh b/install_cartridge_linux.sh index 47315108..c7c48b0f 100755 --- a/install_cartridge_linux.sh +++ b/install_cartridge_linux.sh @@ -12,7 +12,7 @@ fi # Configuration: cartridge cartridge_stem="picosonic" -version="3.0" +version="3.1" config="$1"; shift # option "png" will export the png cartridge diff --git a/run_game.sh b/run_game.sh index 2c7ae77e..5ae37187 100755 --- a/run_game.sh +++ b/run_game.sh @@ -5,7 +5,7 @@ # Configuration: cartridge cartridge_stem="picosonic" -version="3.0" +version="3.1" config="$1"; shift run_cmd="pico8 -run build/${cartridge_stem}_v${version}_${config}.p8 -screenshot_scale 4 -gif_scale 4 $@" diff --git a/run_itest.sh b/run_itest.sh index 1264ae99..83306908 100755 --- a/run_itest.sh +++ b/run_itest.sh @@ -5,7 +5,7 @@ # Configuration: cartridge cartridge_stem="picosonic_itest_all" -version="3.0" +version="3.1" run_cmd="pico8 -run build/${cartridge_stem}_v${version}_itest.p8 -screenshot_scale 4 -gif_scale 4 $@" diff --git a/run_pico8_utests.sh b/run_pico8_utests.sh index c0bf5680..d10d7d67 100755 --- a/run_pico8_utests.sh +++ b/run_pico8_utests.sh @@ -5,7 +5,7 @@ # Configuration: cartridge cartridge_stem="picosonic_pico8_utests_all" -version="3.0" +version="3.1" run_cmd="pico8 -run build/${cartridge_stem}_v${version}_debug.p8 -screenshot_scale 4 -gif_scale 4 $@" From 289d4e3404be8d0b3b8cd5a7968a53ad6d827975 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Tue, 11 Aug 2020 12:42:18 +0200 Subject: [PATCH 061/112] HOTFIX v3.1 (official release) [CI] Travis: do not build_itest.sh, it will fail as more than 65536 chars --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 28f62753..7415dcbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -59,7 +59,10 @@ script: # (even if build fails, tests will be run independently thanks to busted) - ./build_game.sh debug - ./build_game.sh release - - ./build_itest.sh + # disabled build_itest.sh in Travis until strings are compressed + # because character count is over 65536 and that systematically failed the build + # while build release works fine + # - ./build_itest.sh # test (including rendered headless itests thanks to ENABLE_RENDER=1) - ./test.sh -m all # coverage From 99bef21267ce2c5fd28db03cdbfa30377a5dfb99 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Tue, 11 Aug 2020 17:36:19 +0200 Subject: [PATCH 062/112] [PLAYER CHARACTER] Added quadrant direction member, updated with slope angle (down in the air) --- src/ingame/playercharacter.lua | 34 ++++++++- src/ingame/playercharacter_utest.lua | 108 ++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 14 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index d4a3503d..a7cfd00d 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -55,6 +55,7 @@ local player_char = new_class() -- control_mode control_modes control mode: human (default) or ai -- motion_mode (cheat) motion_modes motion mode: platformer (under gravity) or debug (fly around) -- motion_state motion_states motion state (platformer mode only) +-- quadrant directions cardinal direction of character's own down vector (to walk on wall and ceiling) -- orientation horizontal_dirs direction faced by character -- position vector current position (character center "between" pixels) -- ground_speed float current speed along the ground (~px/frame) @@ -86,13 +87,15 @@ function player_char:_setup() self.motion_mode = motion_modes.platformer --#endif self.motion_state = motion_states.grounded + self.quadrant = directions.down self.orientation = horizontal_dirs.right self.position = vector.zero() self.ground_speed = 0. self.velocity = vector.zero() self.debug_velocity = vector.zero() - -- slope_angle starts at 0 instead of nil to match grounded state above (first spawn will set this anyway) + -- slope_angle starts at 0 instead of nil to match grounded state above + -- (if spawning in the air, fine, next update will reset angle to nil) self.slope_angle = 0. self.ascending_slope_time = 0. @@ -167,6 +170,29 @@ function player_char:move_by(delta_vector) self.position = self.position + delta_vector end +-- set slope angle and update quadrant +function player_char:set_slope_angle_with_quadrant(angle) + 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)") + + self.slope_angle = angle + + -- priority to horizontal quadrants at the boundaries + -- (just so 45-deg slope is recognized as left/right and character can get control-locked + -- to mimic hysteresis motion when trying to walk up slide in Hydrocity Zone) + -- 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 + self.quadrant = directions.down + elseif angle <= 0.375 then + self.quadrant = directions.right + elseif angle < 0.625 then + self.quadrant = directions.up + else -- angle <= 0.875 + self.quadrant = directions.left + end + + printh("self.quadrant: "..dump(self.quadrant)) +end + function player_char:update() self:_handle_input() self:_update_motion() @@ -357,7 +383,7 @@ function player_char:_check_escape_from_ground() -- - snap character up to ground top (it does nothing if already touching ground) -- - set slope angle to new ground self.position.y = self.position.y + signed_distance_to_closest_ground - self.slope_angle = next_slope_angle + self:set_slope_angle_with_quadrant(next_slope_angle) end return signed_distance_to_closest_ground <= 0 end @@ -444,7 +470,7 @@ function player_char:_update_platformer_motion_grounded() -- we can now update position and slope self.position = ground_motion_result.position - self.slope_angle = ground_motion_result.slope_angle + self:set_slope_angle_with_quadrant(ground_motion_result.slope_angle) -- todo: reset jump intention on fall... we don't want character to cancel a natural fall by releasing jump button -- (does not happen because of negative jump speed interrupt threshold, but could happen @@ -925,7 +951,7 @@ function player_char:_update_platformer_motion_airborne() end if air_motion_result.is_landing then - self.slope_angle = air_motion_result.slope_angle + self:set_slope_angle_with_quadrant(air_motion_result.slope_angle) self:_enter_motion_state(motion_states.grounded) end diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 74cb4709..7a46d878 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -132,6 +132,7 @@ describe('player_char', function () control_modes.human, motion_modes.platformer, motion_states.grounded, + directions.down, horizontal_dirs.right, vector.zero(), @@ -152,6 +153,7 @@ describe('player_char', function () pc.control_mode, pc.motion_mode, pc.motion_state, + pc.quadrant, pc.orientation, pc.position, @@ -443,6 +445,76 @@ describe('player_char', function () end) end) + describe('set_slope_angle_with_quadrant', function () + + it('should set slope_angle to passed angle', function () + pc.slope_angle = 0.5 + pc:set_slope_angle_with_quadrant(0.25) + assert.are_equal(0.25, pc.slope_angle) + end) + + it('should set quadrant to down for slope_angle: nil', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(nil) + assert.are_equal(directions.down, pc.quadrant) + end) + + it('should set quadrant to down for slope_angle: 1-0.124', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(1-0.124) + assert.are_equal(directions.down, pc.quadrant) + end) + + it('should set quadrant to down for slope_angle: 0', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(0) + assert.are_equal(directions.down, pc.quadrant) + end) + + it('should set quadrant to down for slope_angle: 0.124', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(0.124) + assert.are_equal(directions.down, pc.quadrant) + end) + + it('should set quadrant to right for slope_angle: 0.25-0.125', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(0.25-0.125) + assert.are_equal(directions.right, pc.quadrant) + end) + + it('should set quadrant to right for slope_angle: 0.25+0.125', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(0.25+0.125) + assert.are_equal(directions.right, pc.quadrant) + end) + + it('should set quadrant to up for slope_angle: 0.5-0.124', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(0.5-0.124) + assert.are_equal(directions.up, pc.quadrant) + end) + + it('should set quadrant to up for slope_angle: 0.5+0.124', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(0.5+0.124) + assert.are_equal(directions.up, pc.quadrant) + end) + + it('should set quadrant to left for slope_angle: 0.75-0.125', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(0.75-0.125) + assert.are_equal(directions.left, pc.quadrant) + end) + + it('should set quadrant to left for slope_angle: 0.75+0.125', function () + pc.quadrant = nil + pc:set_slope_angle_with_quadrant(0.75+0.125) + assert.are_equal(directions.left, pc.quadrant) + end) + + end) + describe('update', function () setup(function () @@ -1685,6 +1757,7 @@ describe('player_char', function () setup(function () spy.on(animated_sprite, "play") + 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 @@ -1695,6 +1768,7 @@ describe('player_char', function () teardown(function () animated_sprite.play:revert() + player_char.set_slope_angle_with_quadrant:revert() update_ground_speed_mock:revert() enter_motion_state_stub:revert() @@ -1708,6 +1782,8 @@ describe('player_char', function () end) after_each(function () + player_char.set_slope_angle_with_quadrant:clear() + update_ground_speed_mock:clear() enter_motion_state_stub:clear() check_jump_intention_stub:clear() @@ -1760,10 +1836,11 @@ describe('player_char', function () assert.are_equal(vector(3, 4), pc.position) end) - it('should set the slope angle to 0.25', function () + it('should call set_slope_angle_with_quadrant with 0.25', function () pc.slope_angle = 1-0.25 pc:_update_platformer_motion_grounded() - assert.are_equal(0.25, pc.slope_angle) + 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 call _check_jump_intention, not _enter_motion_state (not falling)', function () @@ -1850,10 +1927,11 @@ describe('player_char', function () assert.are_equal(vector(3, 4), pc.position) end) - it('should set the slope angle to 0.5', function () + it('should call set_slope_angle_with_quadrant with 0.5', function () pc.slope_angle = 1-0.25 pc:_update_platformer_motion_grounded() - assert.are_equal(0.5, pc.slope_angle) + 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 play the idle animation (ground speed ~= 0)', function () @@ -1917,9 +1995,11 @@ describe('player_char', function () assert.are_equal(vector(3, 4), pc.position) end) - it('should set the slope angle to nil', function () + it('should call set_slope_angle_with_quadrant to nil', function () + pc.slope_angle = 0 pc:_update_platformer_motion_grounded() - assert.is_nil(pc.slope_angle) + 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) end) @@ -1967,9 +2047,11 @@ describe('player_char', function () assert.are_equal(vector(3, 4), pc.position) end) - it('should set the slope angle to nil', function () + it('should call set_slope_angle_with_quadrant to nil', function () + pc.slope_angle = 0 pc:_update_platformer_motion_grounded() - assert.is_nil(pc.slope_angle) + 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) end) @@ -3451,11 +3533,13 @@ describe('player_char', function () setup(function () spy.on(player_char, "_enter_motion_state") spy.on(player_char, "_check_hold_jump") + spy.on(player_char, "set_slope_angle_with_quadrant") end) teardown(function () player_char._enter_motion_state:revert() player_char._check_hold_jump:revert() + player_char.set_slope_angle_with_quadrant:revert() end) before_each(function () @@ -3464,6 +3548,7 @@ describe('player_char', function () -- 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_slope_angle_with_quadrant:clear() 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 () @@ -3767,14 +3852,17 @@ describe('player_char', function () compute_air_motion_result_mock:clear() end) - it('should enter grounded state with slope_angle: 0.5', function () + it('should enter grounded state and set_slope_angle_with_quadrant: 0.5', function () + pc.slope_angle = 0 + pc:_update_platformer_motion_airborne() -- implementation assert.spy(pc._enter_motion_state).was_called(1) assert.spy(pc._enter_motion_state).was_called_with(match.ref(pc), motion_states.grounded) - assert.are_equal(0.5, pc.slope_angle) + 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) end) -- compute_air_motion_result_mock (is_blocked_by_wall: true) From 518b402cc51fbae6f366c16cf9b5b76ffdf97dc8 Mon Sep 17 00:00:00 2001 From: Hyper Sonic Date: Thu, 13 Aug 2020 10:27:06 +0200 Subject: [PATCH 063/112] [PLAYER CHARACTER] Render player character sprite rotated by slope angle --- pico-boots | 2 +- src/ingame/playercharacter.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pico-boots b/pico-boots index 034a0015..4f50813a 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 034a001530ea25a4c4bcfff699aa99278fde44d6 +Subproject commit 4f50813ac8c2e34bf177bed3cedba7ccc1b44bd1 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index a7cfd00d..f8d84c7f 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -189,8 +189,6 @@ function player_char:set_slope_angle_with_quadrant(angle) else -- angle <= 0.875 self.quadrant = directions.left end - - printh("self.quadrant: "..dump(self.quadrant)) end function player_char:update() @@ -1271,7 +1269,9 @@ end -- render the player character sprite at its current position function player_char:render() local flip_x = self.orientation == horizontal_dirs.left - self.anim_spr:render(self.position, flip_x) + -- for now, no snapping, follow slope a la Freedom Planet + local sprite_angle = self.slope_angle + self.anim_spr:render(self.position, flip_x, false, sprite_angle) end return player_char From 72d454c538e2276e1280394c4a425b5f05294ebb Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 14 Aug 2020 12:26:30 +0200 Subject: [PATCH 064/112] [TEST] Fixed PC utest since adding flip_y (false) and angle (slope_angle) to render --- pico-boots | 2 +- src/ingame/playercharacter_utest.lua | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pico-boots b/pico-boots index 4f50813a..47be685d 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 4f50813ac8c2e34bf177bed3cedba7ccc1b44bd1 +Subproject commit 47be685df7000f374b2b646f06451e4e2c9195aa diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 7a46d878..04364497 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -4863,24 +4863,26 @@ describe('player_char', function () anim_spr_render_stub:clear() end) - it('(when character is facing left) should call render on sonic sprite data: idle with the character\'s position, flipped x', function () + it('(when character is facing left) should call render on sonic sprite data: idle with the character\'s position, flipped x, current slope angle', function () pc.position = vector(12, 8) pc.orientation = horizontal_dirs.left + pc.slope_angle = 0.125 pc:render() assert.spy(anim_spr_render_stub).was_called(1) - assert.spy(anim_spr_render_stub).was_called_with(match.ref(pc.anim_spr), vector(12, 8), true) + assert.spy(anim_spr_render_stub).was_called_with(match.ref(pc.anim_spr), vector(12, 8), true, false, 0.125) end) - it('(when character is facing right) should call render on sonic sprite data: idle with the character\'s position, not flipped x', function () + it('(when character is facing right) should call render on sonic sprite data: idle with the character\'s position, not flipped x, current slope angle', function () pc.position = vector(12, 8) pc.orientation = horizontal_dirs.right + pc.slope_angle = 1-0.125 pc:render() assert.spy(anim_spr_render_stub).was_called(1) - assert.spy(anim_spr_render_stub).was_called_with(match.ref(pc.anim_spr), vector(12, 8), false) + assert.spy(anim_spr_render_stub).was_called_with(match.ref(pc.anim_spr), vector(12, 8), false, false, 1-0.125) end) end) From bf045d8de73a43586a7872b76d77cb5a729e26ef Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 14 Aug 2020 15:34:41 +0200 Subject: [PATCH 065/112] [API] Use tonum instead of deprecated string_tonum in itest_dsl its _utest --- src/itest/itest_dsl.lua | 4 ++-- src/itest/itest_dsl_utest.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index 0a769979..c1938e2d 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -225,12 +225,12 @@ end function itest_dsl._parse_number(arg_strings) assert(#arg_strings == 1, "_parse_number: got "..#arg_strings.." args, expected 1") - return string_tonum(arg_strings[1]) + return tonum(arg_strings[1]) end function itest_dsl._parse_vector(arg_strings) assert(#arg_strings == 2, "_parse_vector: got "..#arg_strings.." args, expected 2") - return vector(string_tonum(arg_strings[1]), string_tonum(arg_strings[2])) + return vector(tonum(arg_strings[1]), tonum(arg_strings[2])) end function itest_dsl._parse_horizontal_dir(arg_strings) diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua index 21016a34..c5d21beb 100644 --- a/src/itest/itest_dsl_utest.lua +++ b/src/itest/itest_dsl_utest.lua @@ -515,7 +515,7 @@ describe('itest_dsl', function () setup(function () stub(itest_dsl_parser, "parse_gamestate_definition", function (lines) - local tile_id = string_tonum(lines[3]) + local tile_id = tonum(lines[3]) return lines[1], lines[2], tilemap({ From b64c58e8bce80d6ff10c2279a07a3ae396ec33a1 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 14 Aug 2020 15:41:12 +0200 Subject: [PATCH 066/112] [TILE] Quadrant tiles step 1: split tile data structs - raw_tile_collision_data for mask tile id + slope angle - tile_collision_data for processed info with height and width array - currently, only height array is implemented and backward-compatible with previous version, just use collision_data.get_tile_collision_data - more uniform test data, added upper level ascending slope 22.5 --- src/data/collision_data.lua | 333 +++++++++++---------- src/data/raw_tile_collision_data.lua | 24 ++ src/data/raw_tile_collision_data_utest.lua | 24 ++ src/data/tile_collision_data.lua | 77 +++++ src/data/tile_collision_data_utest.lua | 71 +++++ src/platformer/tile.lua | 72 ----- src/platformer/tile_utest.lua | 118 -------- src/platformer/world.lua | 11 +- src/platformer/world_utest.lua | 4 +- src/test_data/tile_representation.lua | 2 + src/test_data/tile_test_data.lua | 43 +-- 11 files changed, 390 insertions(+), 389 deletions(-) create mode 100644 src/data/raw_tile_collision_data.lua create mode 100644 src/data/raw_tile_collision_data_utest.lua create mode 100644 src/data/tile_collision_data.lua create mode 100644 src/data/tile_collision_data_utest.lua delete mode 100644 src/platformer/tile.lua delete mode 100644 src/platformer/tile_utest.lua diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua index 8ed46409..cd59ea94 100644 --- a/src/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -1,185 +1,190 @@ local serialization = require("engine/data/serialization") -local tile = require("platformer/tile") -local tile_data = tile.tile_data +local raw_tile_collision_data = require("data/raw_tile_collision_data") +local tile_collision_data = require("data/tile_collision_data") sprite_flags = { collision = 0 } -return { - - -- table mapping tile sprite id to tile data (collision mask + slope) - -- the mask is generally placed just below the visual tile in pico8 sprite editor, - -- hence the location @ (i, j) but the sprite_id_location(i, j + 1) - -- this will be completed as tiles are added, adding extra information - -- such as "mirror_y: true" for upside-down tiles - -- for readability we also indicate the sprite id location in comment - -- note that for mockup, tile_test_data now contains the mock height arrays - -- while this contains the slopes, which is bad practice; we'll need to centralize - -- mock data in the end. we'll probably create a pico8 tile data to data string - -- converter so we can edit visually, but also generate data code reusable - -- for headless tests - - -- all angles are defined with atan2 using top-left XY convention to avoid issues - -- proto tiles may pass values manually, but in this case make sure to enter the angle - -- between 0 and 1, as if Sonic was running a loop counter-clockwise +-- table mapping tile sprite id to tile data (collision mask + slope) +-- the mask is generally placed just below the visual tile in pico8 sprite editor, +-- hence the location @ (i, j) but the sprite_id_location(i, j + 1) +-- this will be completed as tiles are added, adding extra information +-- such as "mirror_y: true" for upside-down tiles +-- for readability we also indicate the sprite id location in comment +-- note that for mockup, tile_test_data now contains the mock height arrays +-- while this contains the slopes, which is bad practice; we'll need to centralize +-- mock data in the end. we'll probably create a pico8 tile data to data string +-- converter so we can edit visually, but also generate data code reusable +-- for headless tests - --[[ - EXPLANATION OF EACH TILE ID <-> (I, J) IN SPRITESHEET - (put in comments outside data string because we don't have a preprocess step able to strip comments - inside data strings, nor is parse_expression able to ignore comments) - # common tiles (flat or very low slope) - 49 @ (1, 3) - 50 @ (2, 3) - 51 @ (3, 3) - 52 @ (4, 3) - 53 @ (5, 3) - 54 @ (6, 3) - 65 @ (1, 4) - 66 @ (2, 4) - 67 @ (3, 4) - 68 @ (4, 4) - 81 @ (1, 5) - 82 @ (2, 5) - 83 @ (3, 5) - 84 @ (4, 5) - 97 @ (1, 6) - 98 @ (2, 6) - 99 @ (3, 6) - 100 @ (4, 6) - 114 @ (2, 7) - 115 @ (3, 7) - # low slopes ascending and descending - 87 @ (7, 6) - 88 @ (8, 6) - 89 @ (9, 6) - 90 @ (10, 6) - 91 @ (11, 6) - 92 @ (12, 6) - 93 @ (13, 6) - 94 @ (14, 6) - # bottom of said slopes (full tiles) - 119 @ (7, 7) - 120 @ (8, 7) - 121 @ (9, 7) - 122 @ (10, 7) - 123 @ (11, 7) - 124 @ (12, 7) - 125 @ (13, 7) - 126 @ (14, 7) - # loop (start from top-left tile, then rotate clockwise) - # note that we always write angles as atan2(dx, dy) with motion (dx, dy) - # as if Sonic was running the loop counter-clockwise - # this allows to identify ceiling angles vs floor angles easily - # (ceiling angles between 0.25 and 0.75) - 8 @ (8, 0) - 9 @ (9, 0) - 10 @ (10, 0) - 11 @ (11, 0) - 27 @ (11, 1) - 43 @ (11, 2) - 59 @ (11, 3) - 58 @ (10, 3) - 57 @ (9, 3) - 56 @ (8, 3) - 40 @ (8, 2) - 24 @ (8, 1) - # loop bottom ground (full) - 72 @ (8, 4) - 73 @ (9, 4) - 74 @ (10, 4) - 75 @ (11, 4) +-- all angles are defined with atan2 using top-left XY convention to avoid issues +-- proto tiles may pass values manually, but in this case make sure to enter the angle +-- between 0 and 1, as if Sonic was running a loop counter-clockwise - # proto (black and white tiles being their own collision masks) - # must match tile_data.lua - # if too heavy, surround with #itest and create a separate spritesheet for itests with only polygonal tiles - # stored in some proto_data.p8 or test_data.p8 - # this will allow us to reuse the extra space left by removing proto tiles for release (adding FX, etc.) - 32 @ (0, 2) FULL TILE # - 80 @ (0, 5) HALF TILE (4px high) = - 96 @ (0, 6) FLAT LOW TILE (2px high) _ - 64 @ (0, 4) BOTTOM-RIGHT QUARTER TILE (4px high) r - 112 @ (1, 7) ASCENDING 45 / slope_angle: 0.125 = atan2(1, -1) - 113 @ (0, 7) ASCENDING 22.5 < slope_angle: 0.0625 ~= atan2(8, -4) (actually 0.0738) but kept for historical utest/itest reasons - 116 @ (4, 7) DESCENDING 45 \ slope_angle: 1-0.125 = atan2(1, 1) - 117 @ (5, 7) higher 2:1 ascending slope (completes 58 from loop) - --]] +--[[ + EXPLANATION OF EACH TILE ID <-> (I, J) IN SPRITESHEET + (put in comments outside data string because we don't have a preprocess step able to strip comments + inside data strings, nor is parse_expression able to ignore comments) + # common tiles (flat or very low slope) + 49 @ (1, 3) + 50 @ (2, 3) + 51 @ (3, 3) + 52 @ (4, 3) + 53 @ (5, 3) + 54 @ (6, 3) + 65 @ (1, 4) + 66 @ (2, 4) + 67 @ (3, 4) + 68 @ (4, 4) + 81 @ (1, 5) + 82 @ (2, 5) + 83 @ (3, 5) + 84 @ (4, 5) + 97 @ (1, 6) + 98 @ (2, 6) + 99 @ (3, 6) + 100 @ (4, 6) + 114 @ (2, 7) + 115 @ (3, 7) + # low slopes ascending and descending + 87 @ (7, 6) + 88 @ (8, 6) + 89 @ (9, 6) + 90 @ (10, 6) + 91 @ (11, 6) + 92 @ (12, 6) + 93 @ (13, 6) + 94 @ (14, 6) + # bottom of said slopes (full tiles) + 119 @ (7, 7) + 120 @ (8, 7) + 121 @ (9, 7) + 122 @ (10, 7) + 123 @ (11, 7) + 124 @ (12, 7) + 125 @ (13, 7) + 126 @ (14, 7) + # loop (start from top-left tile, then rotate clockwise) + # note that we always write angles as atan2(dx, dy) with motion (dx, dy) + # as if Sonic was running the loop counter-clockwise + # this allows to identify ceiling angles vs floor angles easily + # (ceiling angles between 0.25 and 0.75) + 8 @ (8, 0) + 9 @ (9, 0) + 10 @ (10, 0) + 11 @ (11, 0) + 27 @ (11, 1) + 43 @ (11, 2) + 59 @ (11, 3) + 58 @ (10, 3) + 57 @ (9, 3) + 56 @ (8, 3) + 40 @ (8, 2) + 24 @ (8, 1) + # loop bottom ground (full) + 72 @ (8, 4) + 73 @ (9, 4) + 74 @ (10, 4) + 75 @ (11, 4) - tiles_data = serialization.parse_expression( - --[tile_id] = tile_data( - -- id_loc, slope_angle=atan2(x, y) or angle (proto only)) - [[{ - [49] = {{1, 2}, {8, -2}}, - [50] = {{0, 2}, {8, 0}}, - [51] = {{0, 2}, {8, 0}}, - [52] = {{0, 2}, {8, 0}}, - [53] = {{5, 2}, {8, 2}}, - [54] = {{6, 2}, {8, 0}}, - [65] = {{0, 2}, {8, 0}}, - [66] = {{0, 2}, {8, 0}}, - [67] = {{0, 2}, {8, 0}}, - [68] = {{0, 2}, {8, 0}}, - [81] = {{0, 2}, {8, 0}}, - [82] = {{0, 2}, {8, 0}}, - [83] = {{0, 2}, {8, 0}}, - [84] = {{0, 2}, {8, 0}}, - [97] = {{0, 2}, {8, 0}}, - [98] = {{0, 2}, {8, 0}}, - [99] = {{0, 2}, {8, 0}}, - [100]= {{0, 2}, {8, 0}}, - [114]= {{0, 2}, {8, 0}}, - [115]= {{0, 2}, {8, 0}}, + # proto (black and white tiles being their own collision masks) + # must match tile_data.lua + # if too heavy, surround with #itest and create a separate spritesheet for itests with only polygonal tiles + # stored in some proto_data.p8 or test_data.p8 + # this will allow us to reuse the extra space left by removing proto tiles for release (adding FX, etc.) + 32 @ (0, 2) FULL TILE # + 80 @ (0, 5) HALF TILE (4px high) = + 96 @ (0, 6) FLAT LOW TILE (2px high) _ + 64 @ (0, 4) BOTTOM-RIGHT QUARTER TILE (4px high) r + 112 @ (1, 7) ASCENDING 45 / slope_angle: 0.125 = atan2(1, -1) + 113 @ (0, 7) ASCENDING 22.5 < slope_angle: 0.0625 ~= atan2(8, -4) (actually 0.0738) but kept for historical utest/itest reasons + 116 @ (4, 7) DESCENDING 45 \ slope_angle: 1-0.125 = atan2(1, 1) + 117 @ (5, 7) higher 2:1 ascending slope (completes 58 from loop) + --]] +local raw_tiles_data = serialization.parse_expression( + --[tile_id] = tile_data( + -- id_loc, slope_angle=atan2(x, y) or angle (proto only)) + [[{ + [49] = {{1, 2}, {8, -2}}, + [50] = {{0, 2}, {8, 0}}, + [51] = {{0, 2}, {8, 0}}, + [52] = {{0, 2}, {8, 0}}, + [53] = {{5, 2}, {8, 2}}, + [54] = {{6, 2}, {8, 0}}, + [65] = {{0, 2}, {8, 0}}, + [66] = {{0, 2}, {8, 0}}, + [67] = {{0, 2}, {8, 0}}, + [68] = {{0, 2}, {8, 0}}, + [81] = {{0, 2}, {8, 0}}, + [82] = {{0, 2}, {8, 0}}, + [83] = {{0, 2}, {8, 0}}, + [84] = {{0, 2}, {8, 0}}, + [97] = {{0, 2}, {8, 0}}, + [98] = {{0, 2}, {8, 0}}, + [99] = {{0, 2}, {8, 0}}, + [100]= {{0, 2}, {8, 0}}, + [114]= {{0, 2}, {8, 0}}, + [115]= {{0, 2}, {8, 0}}, - [103] = {{7, 5}, {8, -2}}, - [104] = {{8, 5}, {8, -1}}, - [105] = {{9, 5}, {8, -2}}, - [106] = {{10, 5}, {8, -2}}, - [107] = {{11, 5}, {8, 2}}, - [108] = {{12, 5}, {8, 2}}, - [109] = {{13, 5}, {8, 1}}, - [110] = {{14, 5}, {8, 2}}, + [103] = {{7, 5}, {8, -2}}, + [104] = {{8, 5}, {8, -1}}, + [105] = {{9, 5}, {8, -2}}, + [106] = {{10, 5}, {8, -2}}, + [107] = {{11, 5}, {8, 2}}, + [108] = {{12, 5}, {8, 2}}, + [109] = {{13, 5}, {8, 1}}, + [110] = {{14, 5}, {8, 2}}, - [119]= {{0, 2}, {8, 0}}, - [120]= {{0, 2}, {8, 0}}, - [121]= {{0, 2}, {8, 0}}, - [122]= {{0, 2}, {8, 0}}, - [123]= {{0, 2}, {8, 0}}, - [124]= {{0, 2}, {8, 0}}, - [125]= {{0, 2}, {8, 0}}, - [126]= {{0, 2}, {8, 0}}, + [119]= {{0, 2}, {8, 0}}, + [120]= {{0, 2}, {8, 0}}, + [121]= {{0, 2}, {8, 0}}, + [122]= {{0, 2}, {8, 0}}, + [123]= {{0, 2}, {8, 0}}, + [124]= {{0, 2}, {8, 0}}, + [125]= {{0, 2}, {8, 0}}, + [126]= {{0, 2}, {8, 0}}, - [8]= {{12, 0}, {-4, 4}}, - [9]= {{13, 0}, {-8, 4}}, - [10]= {{14, 0}, {-8, -4}}, - [11]= {{15, 0}, {-4, -4}}, - [27]= {{15, 1}, {-4, -8}}, - [43]= {{15, 2}, {4, -8}}, - [59]= {{15, 3}, {4, -4}}, - [58]= {{14, 3}, {8, -4}}, - [57]= {{13, 3}, {8, 4}}, - [56]= {{12, 3}, {4, 4}}, - [40]= {{12, 2}, {4, 8}}, - [24]= {{12, 1}, {-4, 8}}, + [8]= {{12, 0}, {-4, 4}}, + [9]= {{13, 0}, {-8, 4}}, + [10]= {{14, 0}, {-8, -4}}, + [11]= {{15, 0}, {-4, -4}}, + [27]= {{15, 1}, {-4, -8}}, + [43]= {{15, 2}, {4, -8}}, + [59]= {{15, 3}, {4, -4}}, + [58]= {{14, 3}, {8, -4}}, + [57]= {{13, 3}, {8, 4}}, + [56]= {{12, 3}, {4, 4}}, + [40]= {{12, 2}, {4, 8}}, + [24]= {{12, 1}, {-4, 8}}, - [72]= {{12, 4}, {8, 0}}, - [73]= {{13, 4}, {8, 0}}, - [74]= {{14, 4}, {8, 0}}, - [75]= {{15, 4}, {8, 0}}, + [72]= {{12, 4}, {8, 0}}, + [73]= {{13, 4}, {8, 0}}, + [74]= {{14, 4}, {8, 0}}, + [75]= {{15, 4}, {8, 0}}, - [32] = {{0, 2}, {8, 0}}, - [80] = {{0, 5}, {8, 0}}, - [96] = {{0, 6}, {8, 0}}, - [64] = {{0, 4}, {8, 0}}, - [112]= {{1, 7}, {8, -8}}, - [113]= {{0, 7}, 0.0625}, - [116]= {{4, 7}, {8, 8}}, - [117]= {{5, 7}, {8, -4}}, + [32] = {{0, 2}, {8, 0}}, + [80] = {{0, 5}, {8, 0}}, + [96] = {{0, 6}, {8, 0}}, + [64] = {{0, 4}, {8, 0}}, + [112]= {{1, 7}, {8, -8}}, + [113]= {{0, 7}, 0.0625}, + [116]= {{4, 7}, {8, 8}}, + [117]= {{5, 7}, {8, -4}}, }]], function (t) -- t[2] may be {x, y} to use for atan2 or slope_angle directly -- this is only for [113], if we update utests/itests to use the more correct atan2(8, -4) then we can get rid of -- that ternary check - return tile_data(sprite_id_location(t[1][1], t[1][2]), type(t[2]) == 'table' and atan2(t[2][1], t[2][2]) or t[2]) - end) + return raw_tile_collision_data(sprite_id_location(t[1][1], t[1][2]), type(t[2]) == 'table' and atan2(t[2][1], t[2][2]) or t[2]) + end +) +local tiles_collision_data = transform(raw_tiles_data, tile_collision_data.from_raw_tile_collision_data) + +return { + -- proxy getter is only here to make stubbing possible in tile_test_data + get_tile_collision_data = function (tile_id) + return tiles_collision_data[tile_id] + end } diff --git a/src/data/raw_tile_collision_data.lua b/src/data/raw_tile_collision_data.lua new file mode 100644 index 00000000..e096a98a --- /dev/null +++ b/src/data/raw_tile_collision_data.lua @@ -0,0 +1,24 @@ +-- Raw tile data as stored directly in code +-- It determines the id location of a collision mask sprite +-- and a slope angle (simply because it's simpler to define angles manually +-- than deduce it from a collision mask which may have bumps or an ambiguous angle +-- such as +1 or +2 in height depending on context). +-- Once processed in combination with either PICO-8 spritesheet +-- or a mockup process (for busted), collision mask data (height and row arrays) +-- will be injected, giving a fully-fledged tile_data. +local raw_tile_collision_data = new_struct() + +-- id_loc sprite_id_location sprite location on the spritesheet +-- slope_angle float slope angle in turn ratio (0.0 to 1.0, positive clockwise) +function raw_tile_collision_data:_init(id_loc, slope_angle) + self.id_loc = id_loc + self.slope_angle = slope_angle +end + +--#if log +function raw_tile_collision_data:_tostring() + return "raw_tile_collision_data("..joinstr(", ", self.id_loc:_tostring(), self.slope_angle)..")" +end +--#endif + +return raw_tile_collision_data diff --git a/src/data/raw_tile_collision_data_utest.lua b/src/data/raw_tile_collision_data_utest.lua new file mode 100644 index 00000000..b639283e --- /dev/null +++ b/src/data/raw_tile_collision_data_utest.lua @@ -0,0 +1,24 @@ +require("engine/test/bustedhelper") +local raw_tile_collision_data = require("data/raw_tile_collision_data") + +describe('raw_tile_collision_data', function () + + describe('_init', function () + + it('should create a raw tile data setting the sprite id location and the slope angle', function () + local td = raw_tile_collision_data(sprite_id_location(1, 2), 0.125) + assert.are_same({sprite_id_location(1, 2), 0.125}, {td.id_loc, td.slope_angle}) + end) + + end) + + describe('_tostring', function () + + it('should return "height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)"', function () + local td = raw_tile_collision_data(sprite_id_location(1, 2), 0.125) + assert.are_equal("raw_tile_collision_data(sprite_id_location(1, 2), 0.125)", td:_tostring()) + end) + + end) + +end) diff --git a/src/data/tile_collision_data.lua b/src/data/tile_collision_data.lua new file mode 100644 index 00000000..09266f72 --- /dev/null +++ b/src/data/tile_collision_data.lua @@ -0,0 +1,77 @@ +local tile = {} + +local tile_collision_data = new_struct() + +-- 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) +function tile_collision_data:_init(height_array, width_array, slope_angle) + self.height_array = height_array + self.width_array = width_array + self.slope_angle = slope_angle +end + +-- return the height for a column index starting at 0, from left to right +function tile_collision_data:get_height(column_index0) + return self.height_array[column_index0 + 1] -- adapt 0-index to 1-index +end + +-- return the width for a row index starting at 0, from top to bottom +function tile_collision_data:get_width(row_index0) + return self.width_array[row_index0 + 1] -- adapt 0-index to 1-index +end + +function tile_collision_data.from_raw_tile_collision_data(raw_data) + return tile_collision_data( + tile_collision_data.read_height_array(raw_data.id_loc, slope_angle), + tile_collision_data.read_width_array(raw_data.id_loc, slope_angle), + raw_data.slope_angle + ) +end + +-- Read tile mask located at tile_mask_id_location: sprite_id_location +-- We assume that the tile mask is only compounded or white (more exactly non-black) +-- vertical segments aka "columns" touching the top (if interior is up) or bottom (if interior is down) +-- of the tile. +function tile_collision_data.read_height_array(tile_mask_id_location, slope_angle) + local array = {} + -- todo: support ceiling + local tile_mask_topleft_position = tile_mask_id_location:to_topleft_position() + -- iterate over columns from left to right, searching for the highest filled pixel + for dx = 0, tile_size - 1 do + -- iterate from the top of the column and stop at the first filled pixel (we assume + -- lower pixels are also filled for readability of the tile mask, but not enforced) + local mask_height = 0 + for dy = 0, tile_size - 1 do + local tile_mask_color = sget(tile_mask_topleft_position.x + dx, tile_mask_topleft_position.y + dy) + -- we use black (0) as transparent mask color + if tile_mask_color ~= 0 then + mask_height = tile_size - dy + break + end + end + add(array, mask_height) + end + return array +end + +function tile_collision_data.read_width_array(tile_mask_id_location, slope_angle) + -- todo: probably make a unique function read_array and pass some parameter x/y + -- to distinguish height and width parsing + return {} +end + +return tile_collision_data diff --git a/src/data/tile_collision_data_utest.lua b/src/data/tile_collision_data_utest.lua new file mode 100644 index 00000000..d6d1efc5 --- /dev/null +++ b/src/data/tile_collision_data_utest.lua @@ -0,0 +1,71 @@ +require("engine/test/bustedhelper") +local tile_collision_data = require("data/tile_collision_data") + +describe('tile_collision_data', function () + + describe("mocking _fill_array", function () + + describe('_init', function () + + it('should create a tile_collision_data with reciprocal arrays and slope angle', function () + local tcd = tile_collision_data({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, 0.125) + assert.are_same({{1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, 0.125}, {tcd.height_array, tcd.width_array, tcd.slope_angle}) + end) + + end) + + describe('get_height', function () + + it('should return the height at the given column index', function () + local tcd = tile_collision_data({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, 0.125) + assert.are_equal(2, tcd:get_height(2)) + end) + + end) + + describe('get_width', function () + + it('should return the width at the given column index', function () + local tcd = tile_collision_data({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, 0.125) + assert.are_equal(2, tcd:get_width(4)) + end) + + end) + + end) + + describe('read_height_array', function () + + local sget_mock + + setup(function () + -- simulate an sget that would return the pixel of a tile mask + -- if coordinates fall in the sprite at location (1, 2), i.e. [8-15] x [16-23], + -- where mock_height_array contains the respective height of the mask columns + -- for each column from left to right + local mock_height_array = {2, 3, 5, 6, 0, 1, 4, 2} + sget_mock = stub(_G, "sget", function (x, y) + if x >= 8 and x <= 15 and y >= 16 and y <= 23 then + -- return filled pixel color iff below mask height on this column + local height = mock_height_array[x - 7] + if y - 16 >= tile_size - height then + return 1 + else + return 0 + end + end + end) + end) + + teardown(function () + sget_mock:revert() + end) + + it('should fill the array with ', function () + local array = tile_collision_data.read_height_array(sprite_id_location(1, 2), 0) + assert.are_same({2, 3, 5, 6, 0, 1, 4, 2}, array) + end) + + end) + +end) diff --git a/src/platformer/tile.lua b/src/platformer/tile.lua deleted file mode 100644 index 873ba9a0..00000000 --- a/src/platformer/tile.lua +++ /dev/null @@ -1,72 +0,0 @@ -local tile = {} - -local tile_data = new_struct() -tile.tile_data = tile_data - --- id_loc sprite_id_location sprite location on the spritesheet --- slope_angle float slope angle in turn ratio (0.0 to 1.0, positive clockwise) -function tile_data:_init(id_loc, slope_angle) - self.id_loc = id_loc - self.slope_angle = slope_angle -end - ---#if log -function tile_data:_tostring() - return "tile_data("..joinstr(", ", self.id_loc:_tostring(), self.slope_angle)..")" -end ---#endif - - -local height_array = new_struct() -tile.height_array = height_array - --- tile_data_value tile_data tile data to generate the height array from --- _array [int] sequence of heights of a tile collision mask column per index, --- counting index from the left, height from the bottom --- it is filled based on tile_mask_id_location --- slope_angle float slope angle in turn ratio (0.0 to 1.0) -function height_array:_init(tile_data_value) - self._array = {} - self._fill_array(self._array, tile_data_value.id_loc) - self.slope_angle = tile_data_value.slope_angle -end - ---#if log -function height_array:_tostring() - return "height_array("..joinstr(", ", "{"..joinstr_table(", ", self._array).."}", self.slope_angle)..")" -end ---#endif - --- return the height for a column index starting at 0, from left to right -function height_array:get_height(column_index0) - return self._array[column_index0 + 1] -- adapt 0-index to 1-index -end - - --- fill the passed array with height data based on the sprite mask --- located at tile_mask_id_location: sprite_id_location --- pass an empty array so it is only filled with the computed values --- the tile mask must represent the collision mask of a tile, with columns --- of non-transparent (black) pixels filled from the bottom, --- or at least the upper edge of said mask (we don't check what is below --- the edge once we found the first non-transparent pixel from top to bottom) -function height_array._fill_array(array, tile_mask_id_location) - local tile_mask_topleft_position = tile_mask_id_location:to_topleft_position() - -- iterate over columns from left to right, searching for the highest filled pixel - for dx = 0, tile_size - 1 do - -- iterate from the top of the column and stop at the first filled pixel (we assume - -- lower pixels are also filled for readability of the tile mask, but not enforced) - local mask_height = 0 - for dy = 0, tile_size - 1 do - local tile_mask_color = sget(tile_mask_topleft_position.x + dx, tile_mask_topleft_position.y + dy) - -- we use black (0) as transparent mask color - if tile_mask_color ~= 0 then - mask_height = tile_size - dy - break - end - end - add(array, mask_height) - end -end - -return tile diff --git a/src/platformer/tile_utest.lua b/src/platformer/tile_utest.lua deleted file mode 100644 index 1ddba6d5..00000000 --- a/src/platformer/tile_utest.lua +++ /dev/null @@ -1,118 +0,0 @@ -require("engine/test/bustedhelper") -local tile = require("platformer/tile") -local tile_data = tile.tile_data -local height_array = tile.height_array - -describe('tile', function () - - describe('tile_data', function () - - describe('_init', function () - - it('should create a tile data setting the sprite id location and the slope angle', function () - local td = tile_data(sprite_id_location(1, 2), 0.125) - assert.are_same({sprite_id_location(1, 2), 0.125}, {td.id_loc, td.slope_angle}) - end) - - end) - - describe('_tostring', function () - - it('should return "height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)"', function () - local td = tile_data(sprite_id_location(1, 2), 0.125) - assert.are_equal("tile_data(sprite_id_location(1, 2), 0.125)", td:_tostring()) - end) - - end) - - end) - - describe('height_array', function () - - describe("mocking _fill_array", function () - - local fill_array_mock - - setup(function () - fill_array_mock = stub(height_array, "_fill_array", function (array, tile_mask_sprite_id_location) - for i = 1, tile_size do - array[i] = tile_mask_sprite_id_location.i + tile_mask_sprite_id_location.j + i - end - end) - end) - - teardown(function () - fill_array_mock:revert() - end) - - after_each(function () - fill_array_mock:clear() - end) - - describe('_init', function () - - it('should create a height array using fill_array and setting the slope angle', function () - local h_array = height_array(tile_data(sprite_id_location(1, 2), 0.125)) - assert.are_same({{4, 5, 6, 7, 8, 9, 10, 11}, 0.125}, {h_array._array, h_array.slope_angle}) - end) - - end) - - describe('_tostring', function () - - it('should return "height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)"', function () - local h_array = height_array(tile_data(sprite_id_location(1, 2), 0.125)) - assert.are_equal("height_array({4, 5, 6, 7, 8, 9, 10, 11}, 0.125)", h_array:_tostring()) - end) - - end) - - describe('get_height', function () - - it('should return the height at the given column index', function () - local h_array = height_array(tile_data(sprite_id_location(1, 2), 0.125)) - assert.are_equal(6, h_array:get_height(2)) - end) - - end) - - end) - - describe('_fill_array', function () - - local sget_mock - - setup(function () - -- simulate an sget that would return the pixel of a tile mask - -- if coordinates fall in the sprite at location (1, 2), i.e. [8-15] x [16-23], - -- where mock_height_array contains the respective height of the mask columns - -- for each column from left to right - local mock_height_array = {2, 3, 5, 6, 0, 1, 4, 2} - sget_mock = stub(_G, "sget", function (x, y) - if x >= 8 and x <= 15 and y >= 16 and y <= 23 then - -- return filled pixel color iff below mask height on this column - local height = mock_height_array[x - 7] - if y - 16 >= tile_size - height then - return 1 - else - return 0 - end - end - end) - end) - - teardown(function () - sget_mock:revert() - end) - - it('should fill the array with ', function () - local array = {} - height_array._fill_array(array, sprite_id_location(1, 2)) - assert.are_same({2, 3, 5, 6, 0, 1, 4, 2}, array) - end) - - end) - - end) - -end) diff --git a/src/platformer/world.lua b/src/platformer/world.lua index e2782c24..f68edcf7 100644 --- a/src/platformer/world.lua +++ b/src/platformer/world.lua @@ -1,4 +1,4 @@ -local tile = require("platformer/tile") +local tile_collision_data = require("data/tile_collision_data") local collision_data = require("data/collision_data") local world = {} @@ -18,14 +18,13 @@ function world._compute_column_height_at(tile_location, column_index0) if current_tile_collision_flag then -- get the tile collision mask - local tile_data_value = collision_data.tiles_data[current_tile_id] - assert(tile_data_value, "collision_data.tiles_data does not contain entry for sprite id: "..current_tile_id..", yet it has the collision flag set") + local tcd = collision_data.get_tile_collision_data(current_tile_id) + assert(tcd, "collision_data.tiles_collision_data does not contain entry for sprite id: "..current_tile_id..", yet it has the collision flag set") - if tile_data_value then + if tcd then -- optimize: cache collision height array on game start (otherwise, we get all the data every time, -- including the unused slope angle) - local h_array = tile.height_array(tile_data_value) - return h_array:get_height(column_index0), h_array.slope_angle + return tcd:get_height(column_index0), tcd.slope_angle end end diff --git a/src/platformer/world_utest.lua b/src/platformer/world_utest.lua index 3cc8e032..239b2f01 100644 --- a/src/platformer/world_utest.lua +++ b/src/platformer/world_utest.lua @@ -38,7 +38,7 @@ describe('world (with mock tiles data setup)', function () assert.has_error(function () world._compute_column_height_at(location(1, 1), 0) end, - "collision_data.tiles_data does not contain entry for sprite id: 1, yet it has the collision flag set") + "collision_data.tiles_collision_data does not contain entry for sprite id: 1, yet it has the collision flag set") end) end) @@ -50,7 +50,7 @@ describe('world (with mock tiles data setup)', function () mock_mset(1, 1, asc_slope_22_id) end) - it('should return 3 on column 3', function () + it('#solo should return 3 on column 3', function () assert.are_same({3, 22.5 / 360}, {world._compute_column_height_at(location(1, 1), 3)}) end) diff --git a/src/test_data/tile_representation.lua b/src/test_data/tile_representation.lua index 97ec17a6..a1f19b00 100644 --- a/src/test_data/tile_representation.lua +++ b/src/test_data/tile_representation.lua @@ -11,6 +11,7 @@ bottom_right_quarter_tile_id = 64 asc_slope_45_id = 112 desc_slope_45_id = 116 asc_slope_22_id = 113 +asc_slope_22_upper_level_id = 117 -- symbol mapping for itests -- (could also be used for utests instead of manual mock_mset, but need to extract parse_tilemap @@ -24,4 +25,5 @@ tile_symbol_to_ids = { ['/'] = asc_slope_45_id, -- ascending slope 45 ['\\'] = desc_slope_45_id, -- descending slope 45 ['<'] = asc_slope_22_id, -- ascending slope 22.5 + ['y'] = asc_slope_22_upper_level_id, -- ascending slope upper level 22.5 } diff --git a/src/test_data/tile_test_data.lua b/src/test_data/tile_test_data.lua index ff1245c6..6fe7def3 100644 --- a/src/test_data/tile_test_data.lua +++ b/src/test_data/tile_test_data.lua @@ -3,12 +3,22 @@ -- pico8api should have been required in an including script, -- since we are used busted, hence bustedhelper -local tile = require("platformer/tile") local collision_data = require("data/collision_data") +local tile_collision_data = require("data/tile_collision_data") local stub = require("luassert.stub") require("test_data/tile_representation") -local height_array_init_mock +local mock_tile_collision_data = { + -- collision_data values + PICO-8 spritesheet must match our mockup data + [full_tile_id] = tile_collision_data({8, 8, 8, 8, 8, 8, 8, 8}, {8, 8, 8, 8, 8, 8, 8, 8}, atan2(8, 0)), + [half_tile_id] = tile_collision_data({4, 4, 4, 4, 4, 4, 4, 4}, {0, 0, 0, 0, 8, 8, 8, 8}, atan2(8, 0)), + [flat_low_tile_id] = tile_collision_data({2, 2, 2, 2, 2, 2, 2, 2}, {0, 0, 0, 0, 0, 0, 8, 8}, atan2(8, 0)), + [bottom_right_quarter_tile_id] = tile_collision_data({0, 0, 0, 0, 4, 4, 4, 4}, {0, 0, 0, 0, 4, 4, 4, 4}, atan2(8, 0)), + [asc_slope_45_id] = tile_collision_data({1, 2, 3, 4, 5, 6, 7, 8}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, -8)), + [asc_slope_22_id] = tile_collision_data({2, 2, 3, 3, 4, 4, 5, 5}, {0, 0, 0, 2, 4, 6, 8, 8}, 0.0625), + [desc_slope_45_id] = tile_collision_data({8, 7, 6, 5, 4, 3, 2, 1}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, 8)), + [asc_slope_22_upper_level_id] = tile_collision_data({5, 5, 6, 6, 7, 7, 8, 8}, {2, 4, 6, 8, 8, 8, 8, 8}, atan2(8, -4)), +} local tile_test_data = {} @@ -24,36 +34,15 @@ function tile_test_data.setup() fset(flat_low_tile_id, sprite_flags.collision, true) -- low-tile (bottom quarter) -- mock height array _init so it doesn't have to dig in sprite data, inaccessible from busted - height_array_init_mock = stub(tile.height_array, "_init", function (self, tile_data) - local tile_mask_id_location = tile_data.id_loc - if tile_mask_id_location == collision_data.tiles_data[full_tile_id].id_loc then - self._array = {8, 8, 8, 8, 8, 8, 8, 8} -- full tile - elseif tile_mask_id_location == collision_data.tiles_data[asc_slope_45_id].id_loc then - self._array = {1, 2, 3, 4, 5, 6, 7, 8} -- ascending slope 45 - elseif tile_mask_id_location == collision_data.tiles_data[desc_slope_45_id].id_loc then - self._array = {8, 7, 6, 5, 4, 3, 2, 1} -- descending slope 45 - elseif tile_mask_id_location == collision_data.tiles_data[asc_slope_22_id].id_loc then - self._array = {2, 2, 3, 3, 4, 4, 5, 5} -- ascending slope 22.5 - elseif tile_mask_id_location == collision_data.tiles_data[half_tile_id].id_loc then - self._array = {4, 4, 4, 4, 4, 4, 4, 4} -- half-tile (bottom half) - elseif tile_mask_id_location == collision_data.tiles_data[bottom_right_quarter_tile_id].id_loc then - self._array = {0, 0, 0, 0, 4, 4, 4, 4} -- quarter-tile (bottom-right quarter) - elseif tile_mask_id_location == collision_data.tiles_data[flat_low_tile_id].id_loc then - self._array = {2, 2, 2, 2, 2, 2, 2, 2} -- low-tile (bottom quarter) - else - self._array = "invalid" - end - -- we trust the collision_data value to match our mockups - -- if they don't, we need to override that value in the cases above - self.slope_angle = tile_data.slope_angle + stub(collision_data, "get_tile_collision_data", function (current_tile_id) + return mock_tile_collision_data[current_tile_id] end) - end function tile_test_data.teardown() pico8:clear_spriteflags() - height_array_init_mock:revert() + collision_data.get_tile_collision_data:revert() end -- helper safety function that verifies that mock tile data is active when creating mock maps for utests @@ -61,7 +50,7 @@ end function mock_mset(x, y, v) -- verify that tile_test_data.setup has been called since the last tile_test_data.teardown -- just check if the mock of height_array exists and is active - assert(height_array_init_mock and not height_array_init_mock.reverted, "mock_mset: tile_test_data.setup has not been called since the last tile_test_data.teardown") + assert(collision_data.get_tile_collision_data and not collision_data.get_tile_collision_data.reverted, "mock_mset: tile_test_data.setup has not been called since the last tile_test_data.teardown") mset(x, y, v) end From 3c94b4f5a963aeef1a30ad22b18bc67eab3fce95 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 14 Aug 2020 15:50:40 +0200 Subject: [PATCH 067/112] [DATA] Better explanation/name for visual tile vs collision mask sprite --- src/data/collision_data.lua | 4 ++-- src/data/raw_tile_collision_data.lua | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua index cd59ea94..cd669bb1 100644 --- a/src/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -7,7 +7,7 @@ sprite_flags = { collision = 0 } --- table mapping tile sprite id to tile data (collision mask + slope) +-- table mapping visual tile sprite id to tile collision data (collision mask sprite id location + slope) -- the mask is generally placed just below the visual tile in pico8 sprite editor, -- hence the location @ (i, j) but the sprite_id_location(i, j + 1) -- this will be completed as tiles are added, adding extra information @@ -105,7 +105,7 @@ sprite_flags = { --]] local raw_tiles_data = serialization.parse_expression( --[tile_id] = tile_data( - -- id_loc, slope_angle=atan2(x, y) or angle (proto only)) + -- mask_tile_id_loc, slope_angle=atan2(x, y) or angle (proto only)) [[{ [49] = {{1, 2}, {8, -2}}, [50] = {{0, 2}, {8, 0}}, diff --git a/src/data/raw_tile_collision_data.lua b/src/data/raw_tile_collision_data.lua index e096a98a..c1e0bf46 100644 --- a/src/data/raw_tile_collision_data.lua +++ b/src/data/raw_tile_collision_data.lua @@ -8,16 +8,16 @@ -- will be injected, giving a fully-fledged tile_data. local raw_tile_collision_data = new_struct() --- id_loc sprite_id_location sprite location on the spritesheet --- slope_angle float slope angle in turn ratio (0.0 to 1.0, positive clockwise) -function raw_tile_collision_data:_init(id_loc, slope_angle) - self.id_loc = id_loc +-- mask_tile_id_loc sprite_id_location sprite location of the collision mask for this tile on the spritesheet +-- slope_angle float slope angle in turn ratio (0.0 to 1.0, positive clockwise) +function raw_tile_collision_data:_init(mask_tile_id_loc, slope_angle) + self.mask_tile_id_loc = mask_tile_id_loc self.slope_angle = slope_angle end --#if log function raw_tile_collision_data:_tostring() - return "raw_tile_collision_data("..joinstr(", ", self.id_loc:_tostring(), self.slope_angle)..")" + return "raw_tile_collision_data("..joinstr(", ", self.mask_tile_id_loc:_tostring(), self.slope_angle)..")" end --#endif From cba147fc509003e857a5f15cc5ba70c71bfb8653 Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 15 Aug 2020 22:55:44 +0200 Subject: [PATCH 068/112] [TILE] Generalized tile collision data processing to all interior directions --- pico-boots | 2 +- src/data/tile_collision_data.lua | 151 ++++++++++++++--- src/data/tile_collision_data_utest.lua | 226 +++++++++++++++++++++---- 3 files changed, 324 insertions(+), 55 deletions(-) diff --git a/pico-boots b/pico-boots index 47be685d..d3ffe4d2 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 47be685df7000f374b2b646f06451e4e2c9195aa +Subproject commit d3ffe4d252425b852770a4343ceddbcea4947493 diff --git a/src/data/tile_collision_data.lua b/src/data/tile_collision_data.lua index 09266f72..42845f3b 100644 --- a/src/data/tile_collision_data.lua +++ b/src/data/tile_collision_data.lua @@ -1,5 +1,10 @@ local tile = {} +-- struct that contains directly usable data on tile collision +-- semantically, it is almost equivalent to raw_tile_collision_data +-- (technically it loses the mask sprite id location) and redundant, +-- but by precomputing more detailed data using PICO-8 spritesheet / busted mocks, +-- tile_collision_data makes it possible to check for collision with ground very easily local tile_collision_data = new_struct() -- height_array [int] sequence of column heights of a tile collision mask per column index, @@ -18,10 +23,16 @@ local tile_collision_data = new_struct() -- 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) -function tile_collision_data:_init(height_array, width_array, slope_angle) +-- 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(height_array, width_array, slope_angle, interior_v, interior_h) 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 end -- return the height for a column index starting at 0, from left to right @@ -35,43 +46,141 @@ function tile_collision_data:get_width(row_index0) end function tile_collision_data.from_raw_tile_collision_data(raw_data) + assert(raw_data.slope_angle >= 0 and raw_data.slope_angle < 1, "tile_collision_data.from_raw_tile_collision_data: raw_data.slope_angle is "..raw_data.slope_angle..", 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 + local interior_v = raw_data.slope_angle < 0.5 and horizontal_dirs.right or horizontal_dirs.left + local interior_h = (raw_data.slope_angle < 0.25 or raw_data.slope_angle > 0.75) and vertical_dirs.down or vertical_dirs.up + return tile_collision_data( - tile_collision_data.read_height_array(raw_data.id_loc, slope_angle), - tile_collision_data.read_width_array(raw_data.id_loc, slope_angle), - raw_data.slope_angle + tile_collision_data.read_height_array(raw_data.mask_tile_id_loc, interior_v), + tile_collision_data.read_width_array(raw_data.mask_tile_id_loc, interior_h), + raw_data.slope_angle, + interior_v, + interior_h ) end --- Read tile mask located at tile_mask_id_location: sprite_id_location +-- return a stateful range iterator +-- this function is generic enough to be in picoboots helper +-- but right now it's only used in this script so it is local +-- normally we should make it accessible from other modules +-- and test it, but it's likely to be correct since it was copied from +-- http://lua-users.org/wiki/RangeIterator and this module's tests are passing +local function range(from, to, step) + step = step or 1 + return function(_, lastvalue) + local nextvalue = lastvalue + step + if step > 0 and nextvalue <= to or step < 0 and nextvalue >= to or + step == 0 + then + return nextvalue + end + end, nil, from - step -- from - step: trick to start at from on the first step +end + +-- check for collision pixel at tile_mask_x + dx, tile_mask_y + dy, +-- and if one is found, return the length of the mask strip segment (height or width that constitutes +-- a collision array0) in the direction we were iterating on in calling context, thanks to segment_length_evaluator +-- (it is needed because height is read on y, width on x, and depending on the interior's +-- direction, we iterated from a different side and measure the length differently) +-- if no collision pixel at this position, return nil +function tile_collision_data.check_collision_pixel(tile_mask_x, tile_mask_y, dx, dy, interior_v, interior_h, segment_length_evaluator) + local tile_mask_color = sget(tile_mask_x + dx, tile_mask_y + dy) + -- we use black (0) as transparent mask color + if tile_mask_color ~= 0 then + -- segment_length_evaluator will either use dx & interior_v or dy & interior_h, + -- but we don't know from here so pass both + return segment_length_evaluator(dx, dy, interior_v, interior_h) + end +end + +-- return height to fill collision height array when finding first collision pixel at dy, +-- vertical interior on interior_v side +function tile_collision_data.evaluate_collision_height(dx, dy, interior_v, interior_h) + if interior_v == vertical_dirs.down then + -- we were iterating from the sky to the floor + -- so the height is complementary to our iteration distance + -- if we hit a collision pixel on first iteration (dy == 0), + -- then the column is full (return tile_size), so no offset + return tile_size - dy + else + -- we were iterating from the bottom to the ceiling, but backward, + -- so our iteration index tells us how far from the top we are + -- if we hit a collision pixel on first iteration (dy == tile_size - 1) + -- then the column is full (return tile_size), so need offset +1 + return dy + 1 + end +end + +-- return height to fill collision width array when finding first collision pixel at dx, +-- horizontal interior on interior_h side +function tile_collision_data.evaluate_collision_width(dx, dy, interior_v, interior_h) + -- see comments in evaluate_collision_height and transpose everything + if interior_h == horizontal_dirs.right then + return tile_size - dx + else + return dx + 1 + end +end + +-- Read tile mask collision height array located at tile_mask_id_location: sprite_id_location -- We assume that the tile mask is only compounded or white (more exactly non-black) -- vertical segments aka "columns" touching the top (if interior is up) or bottom (if interior is down) -- of the tile. -function tile_collision_data.read_height_array(tile_mask_id_location, slope_angle) +function tile_collision_data.read_height_array(tile_mask_id_location, interior_v) local array = {} - -- todo: support ceiling + local tile_mask_topleft_position = tile_mask_id_location:to_topleft_position() - -- iterate over columns from left to right, searching for the highest filled pixel + + -- range returns a tuple so we need to pack and unpack later + local y_range = interior_v == vertical_dirs.down and {range(0, tile_size - 1)} or {range(tile_size - 1, 0, -1)} + + -- iterate over columns from left to right (order doesn't matter as long as we fill the height array in the same order) for dx = 0, tile_size - 1 do - -- iterate from the top of the column and stop at the first filled pixel (we assume - -- lower pixels are also filled for readability of the tile mask, but not enforced) - local mask_height = 0 - for dy = 0, tile_size - 1 do - local tile_mask_color = sget(tile_mask_topleft_position.x + dx, tile_mask_topleft_position.y + dy) - -- we use black (0) as transparent mask color - if tile_mask_color ~= 0 then - mask_height = tile_size - dy + -- iterate from the opposite side of the vertical interior (e.g. bottom if interior is ceiling) + -- so we can find the collision pixel the farthest from the interior, which really represents the column height + for dy in unpack(y_range) do + -- no need to pass interior_h (but we need dx to check pixel at this position) + column_height = tile_collision_data.check_collision_pixel(tile_mask_topleft_position.x, tile_mask_topleft_position.y, dx, dy, interior_v, nil, tile_collision_data.evaluate_collision_height) + if column_height then + -- collision pixel found at column_height + -- break so we can immediately store the column_height in the array break end end - add(array, mask_height) + if not column_height then + -- no pixel found at all, so column width is 0 + column_height = 0 + end + add(array, column_height) end return array end -function tile_collision_data.read_width_array(tile_mask_id_location, slope_angle) - -- todo: probably make a unique function read_array and pass some parameter x/y - -- to distinguish height and width parsing - return {} +-- see read_height_array and transpose everything for comments: +-- height -> width +-- x <-> y +-- interior_v -> interior_h +-- up -> left and down -> right +function tile_collision_data.read_width_array(tile_mask_id_location, interior_h) + local array = {} + local tile_mask_topleft_position = tile_mask_id_location:to_topleft_position() + local x_range = interior_h == horizontal_dirs.right and {range(0, tile_size - 1)} or {range(tile_size - 1, 0, -1)} + + for dy = 0, tile_size - 1 do + for dx in unpack(x_range) do + row_width = tile_collision_data.check_collision_pixel(tile_mask_topleft_position.x, tile_mask_topleft_position.y, dx, dy, nil, interior_h, tile_collision_data.evaluate_collision_width) + if row_width then + break + end + end + if not row_width then + row_width = 0 + end + add(array, row_width) + end + return array end return tile_collision_data diff --git a/src/data/tile_collision_data_utest.lua b/src/data/tile_collision_data_utest.lua index d6d1efc5..3bcbf6aa 100644 --- a/src/data/tile_collision_data_utest.lua +++ b/src/data/tile_collision_data_utest.lua @@ -1,69 +1,229 @@ require("engine/test/bustedhelper") local tile_collision_data = require("data/tile_collision_data") +local raw_tile_collision_data = require("data/raw_tile_collision_data") + +-- when we have to mock tile sprite data in PICO-8, +-- we use the following + +-- mask tile 1: bottom-right asc slope +-- pixel representation: +-- ........ +-- ........ +-- ........ +-- ........ +-- ......## +-- ....#### +-- ..###### +-- ######## + +-- mask tile 2: top-left concave ceiling +-- pixel representation: +-- ######## +-- ######.. +-- ####.... +-- ###..... +-- ##...... +-- ##...... +-- #....... +-- #....... + describe('tile_collision_data', function () - describe("mocking _fill_array", function () + describe('_init', function () - describe('_init', function () + it('should create a tile_collision_data with reciprocal arrays and slope angle', function () + local tcd = tile_collision_data({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4), horizontal_dirs.right, vertical_dirs.down) + assert.are_same({{1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4)}, {tcd.height_array, tcd.width_array, tcd.slope_angle}) + end) - it('should create a tile_collision_data with reciprocal arrays and slope angle', function () - local tcd = tile_collision_data({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, 0.125) - assert.are_same({{1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, 0.125}, {tcd.height_array, tcd.width_array, tcd.slope_angle}) - end) + end) + describe('get_height', function () + + it('should return the height at the given column index', function () + local tcd = tile_collision_data({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4)) + assert.are_equal(2, tcd:get_height(2)) end) - describe('get_height', function () + end) - it('should return the height at the given column index', function () - local tcd = tile_collision_data({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, 0.125) - assert.are_equal(2, tcd:get_height(2)) - end) + describe('get_width', function () + it('should return the width at the given column index', function () + local tcd = tile_collision_data({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4)) + assert.are_equal(2, tcd:get_width(4)) end) - describe('get_width', function () + end) + + describe('from_raw_tile_collision_data', function () - it('should return the width at the given column index', function () - local tcd = tile_collision_data({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, 0.125) - assert.are_equal(2, tcd:get_width(4)) + setup(function () + stub(tile_collision_data, "read_height_array", function (tile_mask_id_location, slope_angle) + printh("tile_mask_id_location: "..dump(tile_mask_id_location)) + if tile_mask_id_location == 1 then + return {1, 1, 2, 2, 3, 3, 4, 4} + else + return {8, 6, 4, 3, 2, 2, 1, 1} + end end) + stub(tile_collision_data, "read_width_array", function (tile_mask_id_location, slope_angle) + if tile_mask_id_location == 1 then + return {0, 0, 0, 0, 2, 4, 6, 8} + else + return {8, 6, 4, 3, 2, 2, 1, 1} + end + end) + end) + teardown(function () + tile_collision_data.read_height_array:revert() + 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 () + local raw_data = raw_tile_collision_data(1, atan2(8, -4)) + local tcd = tile_collision_data.from_raw_tile_collision_data(raw_data) + -- 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({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4), horizontal_dirs.right, vertical_dirs.down), tcd) + end) + + it('should return a tile_collision_data containing (mock tile 2) height/width array, slope angle, derived interior directions', function () + local raw_data = raw_tile_collision_data(2, atan2(-8, 8)) + local tcd = tile_collision_data.from_raw_tile_collision_data(raw_data) + -- 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({8, 6, 4, 3, 2, 2, 1, 1}, {8, 6, 4, 3, 2, 2, 1, 1}, atan2(-8, 8), horizontal_dirs.left, vertical_dirs.up), tcd) end) end) - describe('read_height_array', function () + describe('(mock sget)', function () local sget_mock setup(function () + local mock_mask_dot_matrix1 = { + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 1, 1}, + {0, 0, 0, 0, 1, 1, 1, 1}, + {0, 0, 1, 1, 1, 1, 1, 1}, + {1, 1, 1, 1, 1, 1, 1, 1}, + } + + local mock_mask_dot_matrix2 = { + {1, 1, 1, 1, 1, 1, 1, 1}, + {1, 1, 1, 1, 1, 1, 0, 0}, + {1, 1, 1, 1, 0, 0, 0, 0}, + {1, 1, 1, 0, 0, 0, 0, 0}, + {1, 1, 0, 0, 0, 0, 0, 0}, + {1, 1, 0, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0, 0, 0}, + } + -- simulate an sget that would return the pixel of a tile mask - -- if coordinates fall in the sprite at location (1, 2), i.e. [8-15] x [16-23], - -- where mock_height_array contains the respective height of the mask columns - -- for each column from left to right - local mock_height_array = {2, 3, 5, 6, 0, 1, 4, 2} - sget_mock = stub(_G, "sget", function (x, y) - if x >= 8 and x <= 15 and y >= 16 and y <= 23 then - -- return filled pixel color iff below mask height on this column - local height = mock_height_array[x - 7] - if y - 16 >= tile_size - height then - return 1 - else - return 0 - end + -- if coordinates fall in the sprite 1 at location (1, 0), i.e. [8-15] x [0-8], + -- or sprite 2 at location (2, 0), i.e. [16-23] x [0-8] + stub(_G, "sget", function (x, y) + if x >= 8 and x <= 15 and y >= 0 and y <= 8 then + -- convert offset to 1-based Lua index + -- multi-dimensional array above is first indexed by row (j), then column (i) + return mock_mask_dot_matrix1[y+1][x-8+1] + elseif x >= 16 and x <= 23 and y >= 0 and y <= 8 then + return mock_mask_dot_matrix2[y+1][x-16+1] end + return 0 end) end) teardown(function () - sget_mock:revert() + sget:revert() + end) + + -- read_height_array utests could be done without mocking sget + -- and mocking check_collision_pixel instead, but since we had already written the utests below + -- (which check the final result without stubbing) before extracting check_collision_pixel, + -- it was simpler to just keep them, that to create a stub for check_collision_pixel that would cheat a lot + -- with the passed arguments + describe('read_height_array', function () + + it('should return an array with respective column heights, from left to right', function () + local array = tile_collision_data.read_height_array(sprite_id_location(1, 0), vertical_dirs.down) + assert.are_same({1, 1, 2, 2, 3, 3, 4, 4}, array) + end) + + it('should return an array with respective column heights, from left to right', function () + local array = tile_collision_data.read_height_array(sprite_id_location(2, 0), vertical_dirs.up) + assert.are_same({8, 6, 4, 3, 2, 2, 1, 1}, array) + end) + + it('should return an array with respective column rows, from top to bottom', function () + local array = tile_collision_data.read_width_array(sprite_id_location(1, 0), horizontal_dirs.right) + assert.are_same({0, 0, 0, 0, 2, 4, 6, 8}, array) + end) + + it('should return an array with respective column rows, from top to bottom', function () + local array = tile_collision_data.read_width_array(sprite_id_location(2, 0), horizontal_dirs.left) + assert.are_same({8, 6, 4, 3, 2, 2, 1, 1}, array) + end) + + end) + + describe('check_collision_pixel', function () + + it('(mock tile 1) should return nil when column pixel falls on empty pixel (interior down)', function () + -- note that 5 from top means 6th pixel on this column from the top + local column_height = tile_collision_data.check_collision_pixel(8, 0, 2, 5, vertical_dirs.down, nil, tile_collision_data.evaluate_collision_height) + assert.are_equal(nil, column_height) + end) + + it('(mock tile 1) should return 2 when column pixel falls on collision pixel at height 2 from bottom (interior down)', function () + local column_height = tile_collision_data.check_collision_pixel(8, 0, 2, 6, vertical_dirs.down, nil, tile_collision_data.evaluate_collision_height) + assert.are_equal(2, column_height) + end) + + it('(mock tile 2) should return nil when column pixel falls on empty pixel (interior up)', function () + local column_height = tile_collision_data.check_collision_pixel(16, 0, 2, 4, vertical_dirs.up, nil, tile_collision_data.evaluate_collision_height) + assert.are_equal(nil, column_height) + end) + + it('(mock tile 2) should return 4 when column pixel falls on collision pixel at height 2 from top (interior up)', function () + local column_height = tile_collision_data.check_collision_pixel(16, 0, 2, 3, vertical_dirs.up, nil, tile_collision_data.evaluate_collision_height) + assert.are_equal(4, column_height) + end) + + end) + + end) -- stub sget + + describe('evaluate_collision_height', function () + + it('return tile_size - dy for interior down', function () + local column_height = tile_collision_data.evaluate_collision_height(nil, 2, vertical_dirs.down, nil) + assert.are_equal(6, column_height) + end) + + it('return dy + 1 for interior up', function () + local column_height = tile_collision_data.evaluate_collision_height(nil, 3, vertical_dirs.up, nil) + assert.are_equal(4, column_height) + end) + + end) + + describe('evaluate_collision_width', function () + + it('return tile_size - dx for interior down', function () + local row_width = tile_collision_data.evaluate_collision_width(2, nil, nil, horizontal_dirs.right) + assert.are_equal(6, row_width) end) - it('should fill the array with ', function () - local array = tile_collision_data.read_height_array(sprite_id_location(1, 2), 0) - assert.are_same({2, 3, 5, 6, 0, 1, 4, 2}, array) + it('return dx + 1 for interior up', function () + local row_width = tile_collision_data.evaluate_collision_width(3, nil, nil, horizontal_dirs.left) + assert.are_equal(4, row_width) end) end) From 1e2e999b3e99059b2a68a77bd68e3ee7a39a34b3 Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 15 Aug 2020 22:57:42 +0200 Subject: [PATCH 069/112] [TEST] Fixed raw_tile_collision_data utest (member rename) Removed dump --- src/data/raw_tile_collision_data_utest.lua | 2 +- src/data/tile_collision_data_utest.lua | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/data/raw_tile_collision_data_utest.lua b/src/data/raw_tile_collision_data_utest.lua index b639283e..443bfce3 100644 --- a/src/data/raw_tile_collision_data_utest.lua +++ b/src/data/raw_tile_collision_data_utest.lua @@ -7,7 +7,7 @@ describe('raw_tile_collision_data', function () it('should create a raw tile data setting the sprite id location and the slope angle', function () local td = raw_tile_collision_data(sprite_id_location(1, 2), 0.125) - assert.are_same({sprite_id_location(1, 2), 0.125}, {td.id_loc, td.slope_angle}) + assert.are_same({sprite_id_location(1, 2), 0.125}, {td.mask_tile_id_loc, td.slope_angle}) end) end) diff --git a/src/data/tile_collision_data_utest.lua b/src/data/tile_collision_data_utest.lua index 3bcbf6aa..849f865d 100644 --- a/src/data/tile_collision_data_utest.lua +++ b/src/data/tile_collision_data_utest.lua @@ -61,7 +61,6 @@ describe('tile_collision_data', function () setup(function () stub(tile_collision_data, "read_height_array", function (tile_mask_id_location, slope_angle) - printh("tile_mask_id_location: "..dump(tile_mask_id_location)) if tile_mask_id_location == 1 then return {1, 1, 2, 2, 3, 3, 4, 4} else From 55585f057ba4f60899b42a178ef504e89ebf6ef7 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 16 Aug 2020 00:14:59 +0200 Subject: [PATCH 070/112] [PLAYER CHARACTER] (WIP) Added get_relative_slope_angle and started converting ground motion to quadrant-relative coordinates --- src/ingame/playercharacter.lua | 66 ++++++++++++++++++++-------- src/ingame/playercharacter_utest.lua | 40 +++++++++++++++++ 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index f8d84c7f..723e874e 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -55,7 +55,7 @@ local player_char = new_class() -- control_mode control_modes control mode: human (default) or ai -- motion_mode (cheat) motion_modes motion mode: platformer (under gravity) or debug (fly around) -- motion_state motion_states motion state (platformer mode only) --- quadrant directions cardinal direction of character's own down vector (to walk on wall and ceiling) +-- quadrant directions down vector of quadrant where character is located (down on floor, up on ceiling, left/right on walls) -- orientation horizontal_dirs direction faced by character -- position vector current position (character center "between" pixels) -- ground_speed float current speed along the ground (~px/frame) @@ -125,6 +125,14 @@ function player_char:get_full_height() return self:is_compact() and pc_data.full_height_compact or pc_data.full_height_standing end +function player_char:get_relative_slope_angle() + -- a math trick to transform direction enum value to angle + -- (down being 0, up 0.25, etc.) + -- make sure not to change directions enum values order! + local quadrant_angle = 0.25 * (3-self.quadrant) % 4 + return self.slope_angle - quadrant_angle +end + -- spawn character at given position, detecting ground/air on arrival function player_char:spawn_at(position) self:_setup() @@ -633,22 +641,42 @@ function player_char:_compute_ground_motion_result() ) end - local horizontal_dir = signed_speed_to_dir(self.ground_speed) - - -- initialise result with floored x (we will reinject subpixels if character didn't touch a wall) - -- note that left and right are not completely symmetrical since floor is asymmetrical + -- from here we will be talking about "relative horizontal direction" + -- since all ground motion will be relative to Sonic and its current ground/quadrant + -- e.g. on a left wall, left is up and right is down + -- on the ceiling, left is right and right is left + -- similarly, Sonic is moving along the "relative horizontal axis" + -- and bumps/slopes in the ground will cause "relative vertical motion" + -- on the "relative up" and "relative down" + -- e.g. on a left wall, relative up is right and relative down is left + -- finally, we work with a "relative slope angle" which is just the slope angle + -- subtracted by the quadrant angle + -- we will prefix values with "r" for "relative", e.g. "rx" and "ry" + local relative_horizontal_dir = signed_speed_to_dir(self.ground_speed) + + -- 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 relative height, so the relative vertical coordinate is already integer + -- note that relative 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( - vector(floored_x, self.position.y), + vector(floored_x, floored_y), self.slope_angle, false, false ) - -- only full pixels matter for collisions, but subpixels may sum up to a full pixel + -- 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 signed_distance_x = self.ground_speed * cos(self.slope_angle) - local max_column_distance = player_char._compute_max_pixel_distance(self.position.x, signed_distance_x) + local signed_distance_rx = self.ground_speed * cos(self:get_relative_slope_angle()) + local max_column_distance = player_char._compute_max_pixel_distance(self.position.x, signed_distance_rx) -- 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 @@ -656,19 +684,19 @@ function player_char:_compute_ground_motion_result() -- touch the ground again some pixels farther) local column_distance_before_step = 0 while column_distance_before_step < max_column_distance and not motion_result.is_blocked do - self:_next_ground_step(horizontal_dir, motion_result) + self:_next_ground_step(relative_horizontal_dir, motion_result) column_distance_before_step = column_distance_before_step + 1 end -- check if we need to add or cut subpixels if not motion_result.is_blocked then - -- local max_distance_x = abs(signed_distance_x) + -- local max_distance_x = abs(signed_distance_rx) -- local distance_to_floored_x = abs(motion_result.position.x - floored_x) -- since character was not blocked, we know that we have reached a column distance of max_column_distance -- local are_subpixels_left = max_distance_x > distance_to_floored_x -- 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 = self.position.x + signed_distance_x > motion_result.position.x + local are_subpixels_left = self.position.x + signed_distance_rx > motion_result.position.x if are_subpixels_left then -- character has not been blocked and has some subpixels left to go @@ -679,9 +707,9 @@ function player_char:_compute_ground_motion_result() -- when moving left, the subpixels are a small "backward" motion to the right and should -- never hit a wall back local is_blocked_by_extra_step = false - if signed_distance_x > 0 then + if signed_distance_rx > 0 then local extra_step_motion_result = motion_result:copy() - self:_next_ground_step(horizontal_dir, extra_step_motion_result) + self:_next_ground_step(relative_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 @@ -697,8 +725,8 @@ function player_char:_compute_ground_motion_result() -- 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(signed_distance_x) * (max_distance_x - distance_to_floored_x) - motion_result.position.x = self.position.x + signed_distance_x + -- sign(signed_distance_rx) * (max_distance_x - distance_to_floored_x) + motion_result.position.x = self.position.x + signed_distance_rx end end end @@ -718,14 +746,14 @@ function player_char._compute_max_pixel_distance(initial_position_coord, velocit end -- update ref_motion_result: motion.ground_motion_result for a character trying to move --- by 1 pixel step in horizontal_dir, taking obstacles into account +-- by 1 pixel step in relative_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.x should be floored for these steps -- (some functions assert when giving subpixel coordinates) -function player_char:_next_ground_step(horizontal_dir, ref_motion_result) +function player_char:_next_ground_step(relative_horizontal_dir, ref_motion_result) -- compute candidate position on next step. only flat slopes supported - local step_vec = horizontal_dir_vectors[horizontal_dir] + local step_vec = relative_horizontal_dir_vectors[relative_horizontal_dir] local next_position_candidate = ref_motion_result.position + step_vec -- check if next position is inside/above ground diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 04364497..b617f0a1 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -250,6 +250,46 @@ describe('player_char', function () end) + describe('get_relative_slope_angle', function () + + it('should return 0 when vertically relative to the quadrant (down)', function () + pc.quadrant = directions.down + pc.slope_angle = 0 + assert.are_equal(0, pc:get_relative_slope_angle()) + end) + + it('should return 0 when vertically relative to the quadrant (right)', function () + pc.quadrant = directions.right + pc.slope_angle = 0.25 + assert.are_equal(0, pc:get_relative_slope_angle()) + end) + + it('should return 0 when vertically relative to the quadrant (up)', function () + pc.quadrant = directions.up + pc.slope_angle = 0.5 + assert.are_equal(0, pc:get_relative_slope_angle()) + end) + + it('should return 0 when vertically relative to the quadrant (left)', function () + pc.quadrant = directions.left + pc.slope_angle = 0.75 + assert.are_equal(0, pc:get_relative_slope_angle()) + end) + + it('should return 0.1 when tilted by 0.1 from quadrant (right) vertical', function () + pc.quadrant = directions.right + pc.slope_angle = 0.35 + assert.is_true(almost_eq_with_message(0.1, pc:get_relative_slope_angle())) + end) + + it('should return -0.1 when tilted by -0.1 from quadrant (left) vertical', function () + pc.quadrant = directions.left + pc.slope_angle = 0.65 + assert.is_true(almost_eq_with_message(-0.1, pc:get_relative_slope_angle())) + end) + + end) + describe('spawn_at', function () setup(function () From cdace578d5741ccbfe4ccb367859b5086d0f34a9 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 16 Aug 2020 11:32:43 +0200 Subject: [PATCH 071/112] [PLAYER CHARACTER] Changed "relative" prefix to "quadrant" Added get_quadrant_x (retrocompatible with non-quadrant motion) --- src/ingame/playercharacter.lua | 64 ++++++++++++++++++---------- src/ingame/playercharacter_utest.lua | 46 +++++++++++++++++--- 2 files changed, 81 insertions(+), 29 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 723e874e..0a575189 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -125,7 +125,7 @@ function player_char:get_full_height() return self:is_compact() and pc_data.full_height_compact or pc_data.full_height_standing end -function player_char:get_relative_slope_angle() +function player_char:get_quadrant_slope_angle() -- a math trick to transform direction enum value to angle -- (down being 0, up 0.25, etc.) -- make sure not to change directions enum values order! @@ -133,6 +133,14 @@ function player_char:get_relative_slope_angle() return self.slope_angle - quadrant_angle end +-- return the "x" coordinate in current quadrant, i.e. the coordinate +-- on the quadrant horizontal axis +function player_char:get_quadrant_x() + -- directions value-dependent trick: left and right are 0 and 2 (even) + -- whereas up and down are 1 and 3 (odd), so check for parity + return self.quadrant % 2 == 0 and self.position.y or self.position.x +end + -- spawn character at given position, detecting ground/air on arrival function player_char:spawn_at(position) self:_setup() @@ -641,25 +649,37 @@ function player_char:_compute_ground_motion_result() ) end - -- from here we will be talking about "relative horizontal direction" - -- since all ground motion will be relative to Sonic and its current ground/quadrant - -- e.g. on a left wall, left is up and right is down - -- on the ceiling, left is right and right is left - -- similarly, Sonic is moving along the "relative horizontal axis" - -- and bumps/slopes in the ground will cause "relative vertical motion" - -- on the "relative up" and "relative down" - -- e.g. on a left wall, relative up is right and relative down is left - -- finally, we work with a "relative slope angle" which is just the slope angle - -- subtracted by the quadrant angle - -- we will prefix values with "r" for "relative", e.g. "rx" and "ry" - local relative_horizontal_dir = signed_speed_to_dir(self.ground_speed) + -- 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) + -- - 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) + -- we prefix values with "q" for "quadrant", e.g. "qx" and "qy" + local quadrant_horizontal_dir = signed_speed_to_dir(self.ground_speed) -- 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 relative height, so the relative vertical coordinate is already integer - -- note that relative left and right motion is not completely symmetrical + -- to the ground quadrant height, so the quadrant vertical coordinate is already integer + -- 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) @@ -675,8 +695,8 @@ function player_char:_compute_ground_motion_result() -- 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 signed_distance_rx = self.ground_speed * cos(self:get_relative_slope_angle()) - local max_column_distance = player_char._compute_max_pixel_distance(self.position.x, signed_distance_rx) + local signed_distance_rx = self.ground_speed * cos(self:get_quadrant_slope_angle()) + local max_column_distance = player_char._compute_max_pixel_distance(self:get_quadrant_x(), signed_distance_rx) -- 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 @@ -684,7 +704,7 @@ function player_char:_compute_ground_motion_result() -- touch the ground again some pixels farther) local column_distance_before_step = 0 while column_distance_before_step < max_column_distance and not motion_result.is_blocked do - self:_next_ground_step(relative_horizontal_dir, motion_result) + self:_next_ground_step(quadrant_horizontal_dir, motion_result) column_distance_before_step = column_distance_before_step + 1 end @@ -709,7 +729,7 @@ function player_char:_compute_ground_motion_result() local is_blocked_by_extra_step = false if signed_distance_rx > 0 then local extra_step_motion_result = motion_result:copy() - self:_next_ground_step(relative_horizontal_dir, extra_step_motion_result) + 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 @@ -746,14 +766,14 @@ function player_char._compute_max_pixel_distance(initial_position_coord, velocit end -- update ref_motion_result: motion.ground_motion_result for a character trying to move --- by 1 pixel step in relative_horizontal_dir, taking obstacles into account +-- 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.x should be floored for these steps -- (some functions assert when giving subpixel coordinates) -function player_char:_next_ground_step(relative_horizontal_dir, ref_motion_result) +function player_char:_next_ground_step(quadrant_horizontal_dir, ref_motion_result) -- compute candidate position on next step. only flat slopes supported - local step_vec = relative_horizontal_dir_vectors[relative_horizontal_dir] + local step_vec = horizontal_dir_vectors[quadrant_horizontal_dir] local next_position_candidate = ref_motion_result.position + step_vec -- check if next position is inside/above ground diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index b617f0a1..071a94d5 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -250,42 +250,74 @@ describe('player_char', function () end) - describe('get_relative_slope_angle', function () + describe('get_quadrant_slope_angle', function () it('should return 0 when vertically relative to the quadrant (down)', function () pc.quadrant = directions.down pc.slope_angle = 0 - assert.are_equal(0, pc:get_relative_slope_angle()) + assert.are_equal(0, pc:get_quadrant_slope_angle()) end) it('should return 0 when vertically relative to the quadrant (right)', function () pc.quadrant = directions.right pc.slope_angle = 0.25 - assert.are_equal(0, pc:get_relative_slope_angle()) + assert.are_equal(0, pc:get_quadrant_slope_angle()) end) it('should return 0 when vertically relative to the quadrant (up)', function () pc.quadrant = directions.up pc.slope_angle = 0.5 - assert.are_equal(0, pc:get_relative_slope_angle()) + assert.are_equal(0, pc:get_quadrant_slope_angle()) end) it('should return 0 when vertically relative to the quadrant (left)', function () pc.quadrant = directions.left pc.slope_angle = 0.75 - assert.are_equal(0, pc:get_relative_slope_angle()) + assert.are_equal(0, pc:get_quadrant_slope_angle()) end) it('should return 0.1 when tilted by 0.1 from quadrant (right) vertical', function () pc.quadrant = directions.right pc.slope_angle = 0.35 - assert.is_true(almost_eq_with_message(0.1, pc:get_relative_slope_angle())) + assert.is_true(almost_eq_with_message(0.1, pc:get_quadrant_slope_angle())) end) it('should return -0.1 when tilted by -0.1 from quadrant (left) vertical', function () pc.quadrant = directions.left pc.slope_angle = 0.65 - assert.is_true(almost_eq_with_message(-0.1, pc:get_relative_slope_angle())) + assert.is_true(almost_eq_with_message(-0.1, pc:get_quadrant_slope_angle())) + end) + + end) + + describe('get_quadrant_x', function () + + it('should return position.x when quadrant is down', function () + pc.quadrant = directions.down + pc.position.x = 10 + pc.position.y = 20 + assert.are_equal(10, pc:get_quadrant_x()) + end) + + it('should return position.x when quadrant is up', function () + pc.quadrant = directions.up + pc.position.x = 10 + pc.position.y = 20 + assert.are_equal(10, pc:get_quadrant_x()) + end) + + it('should return position.y when quadrant is right', function () + pc.quadrant = directions.right + pc.position.x = 10 + pc.position.y = 20 + assert.are_equal(20, pc:get_quadrant_x()) + end) + + it('should return position.y when quadrant is left', function () + pc.quadrant = directions.left + pc.position.x = 10 + pc.position.y = 20 + assert.are_equal(20, pc:get_quadrant_x()) end) end) From 244368ee259402a55d50c16e9def5ca74d58d381 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 16 Aug 2020 12:14:50 +0200 Subject: [PATCH 072/112] [PLAYER CHARACTER] Finished adapting _compute_ground_motion_result to quadrant using the new get_position_quadrant_x and set_position_quadrant_x (does not support non-down quadrants until _next_ground_step is adapted too) --- src/ingame/playercharacter.lua | 73 ++++++++++++++++++---------- src/ingame/playercharacter_utest.lua | 68 ++++++++++++++++++++++++-- 2 files changed, 110 insertions(+), 31 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 0a575189..999e3acd 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -133,12 +133,32 @@ function player_char:get_quadrant_slope_angle() return self.slope_angle - quadrant_angle end +-- return the horizontal coordinate of a position vector in current quadrant +-- (x if down/up, y if right/left) +-- we insist on position because quadrant meaning differs for directional vectors +-- (sign of x and y would matter) +function player_char:get_position_quadrant_x(pos) + -- directions value-dependent trick: left and right are 0 and 2 (even) + -- whereas up and down are 1 and 3 (odd), so check for parity + return self.quadrant % 2 == 0 and pos.y or pos.x +end + -- return the "x" coordinate in current quadrant, i.e. the coordinate -- on the quadrant horizontal axis function player_char:get_quadrant_x() + return self:get_position_quadrant_x(self.position) +end + +-- set the horizontal coordinate of a position vector in current quadrant +-- (x if down/up, y if right/left) to value +function player_char:set_position_quadrant_x(pos, value) -- directions value-dependent trick: left and right are 0 and 2 (even) -- whereas up and down are 1 and 3 (odd), so check for parity - return self.quadrant % 2 == 0 and self.position.y or self.position.x + if self.quadrant % 2 == 0 then + pos.y = value + else + pos.x = value + end end -- spawn character at given position, detecting ground/air on arrival @@ -671,7 +691,9 @@ function player_char:_compute_ground_motion_result() -- - 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) - -- we prefix values with "q" for "quadrant", e.g. "qx" and "qy" + -- 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 local quadrant_horizontal_dir = signed_speed_to_dir(self.ground_speed) -- initialise result with floored coords, it's not to easily visualize @@ -692,42 +714,41 @@ function player_char:_compute_ground_motion_result() false ) + local qx = self:get_quadrant_x() + -- 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 signed_distance_rx = self.ground_speed * cos(self:get_quadrant_slope_angle()) - local max_column_distance = player_char._compute_max_pixel_distance(self:get_quadrant_x(), signed_distance_rx) + local signed_distance_qx = self.ground_speed * cos(self:get_quadrant_slope_angle()) + -- max_distance_qx is always integer + local max_distance_qx = player_char._compute_max_pixel_distance(qx, signed_distance_qx) - -- 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 + -- 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 column_distance_before_step = 0 - while column_distance_before_step < max_column_distance and not motion_result.is_blocked do + 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) - column_distance_before_step = column_distance_before_step + 1 + 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 - -- local max_distance_x = abs(signed_distance_rx) - -- local distance_to_floored_x = abs(motion_result.position.x - floored_x) - -- since character was not blocked, we know that we have reached a column distance of max_column_distance - -- local are_subpixels_left = max_distance_x > distance_to_floored_x - -- 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 = self.position.x + signed_distance_rx > motion_result.position.x + -- 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 + signed_distance_qx > self:get_position_quadrant_x(motion_result.position) 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, we decide to check if those - -- subpixels would leak to hitting a wall on the right, and cut them if so, + -- 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, the subpixels are a small "backward" motion to the right and should + -- 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 signed_distance_rx > 0 then + if signed_distance_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 @@ -736,17 +757,15 @@ function player_char:_compute_ground_motion_result() end end - -- unless moving right and hitting a wall due to subpixels, apply the remaining subpixels - -- as they cannot affect collision anymore. when moving left, they go a little backward + -- 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 wall at all, so add the remaining subpixels - -- (it's simpler to just recompute the full motion in x; don't touch y tough, + -- 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 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(signed_distance_rx) * (max_distance_x - distance_to_floored_x) - motion_result.position.x = self.position.x + signed_distance_rx + self:set_position_quadrant_x(motion_result.position, qx + signed_distance_qx) end end end diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 071a94d5..d6be48af 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -290,30 +290,58 @@ describe('player_char', function () end) + describe('get_position_quadrant_x', function () + + it('should return pos.x when quadrant is down', function () + pc.quadrant = directions.down + assert.are_equal(10, pc:get_position_quadrant_x(vector(10, 20))) + end) + + it('should return pos.x when quadrant is up', function () + pc.quadrant = directions.up + assert.are_equal(10, pc:get_position_quadrant_x(vector(10, 20))) + end) + + it('should return pos.y when quadrant is right', function () + pc.quadrant = directions.right + assert.are_equal(20, pc:get_position_quadrant_x(vector(10, 20))) + end) + + it('should return pos.y when quadrant is left', function () + pc.quadrant = directions.left + assert.are_equal(20, pc:get_position_quadrant_x(vector(10, 20))) + end) + + end) + describe('get_quadrant_x', function () - it('should return position.x when quadrant is down', function () + -- we already wrote tests for this method before extracting get_position_quadrant_x, + -- so we decided not to rewrite them with stub for get_position_quadrant_x and keep checking + -- final result + + it('should return self.position.x when quadrant is down', function () pc.quadrant = directions.down pc.position.x = 10 pc.position.y = 20 assert.are_equal(10, pc:get_quadrant_x()) end) - it('should return position.x when quadrant is up', function () + it('should return self.position.x when quadrant is up', function () pc.quadrant = directions.up pc.position.x = 10 pc.position.y = 20 assert.are_equal(10, pc:get_quadrant_x()) end) - it('should return position.y when quadrant is right', function () + it('should return self.position.y when quadrant is right', function () pc.quadrant = directions.right pc.position.x = 10 pc.position.y = 20 assert.are_equal(20, pc:get_quadrant_x()) end) - it('should return position.y when quadrant is left', function () + it('should return self.position.y when quadrant is left', function () pc.quadrant = directions.left pc.position.x = 10 pc.position.y = 20 @@ -322,6 +350,38 @@ describe('player_char', function () end) + describe('set_position_quadrant_x', function () + + it('should set pos.x when quadrant is down', function () + pc.quadrant = directions.down + local p = vector(10, 20) + pc:set_position_quadrant_x(p, 30) + assert.are_same(vector(30, 20), p) + end) + + it('should set pos.x when quadrant is up', function () + pc.quadrant = directions.up + local p = vector(10, 20) + pc:set_position_quadrant_x(p, 30) + assert.are_same(vector(30, 20), p) + end) + + it('should set pos.y when quadrant is right', function () + pc.quadrant = directions.right + local p = vector(10, 20) + pc:set_position_quadrant_x(p, 30) + assert.are_same(vector(10, 30), p) + end) + + it('should set pos.y when quadrant is left', function () + pc.quadrant = directions.left + local p = vector(10, 20) + pc:set_position_quadrant_x(p, 30) + assert.are_same(vector(10, 30), p) + end) + + end) + describe('spawn_at', function () setup(function () From 901e245ffaa072fa7cd110757265a594728d197d Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 16 Aug 2020 12:53:28 +0200 Subject: [PATCH 073/112] [PLAYER CHARACTER] (WIP) Adapt _next_ground_step to quadrants Added get_quadrant_slope_angle and quadrant_rotated --- pico-boots | 2 +- src/ingame/playercharacter.lua | 24 +++++++++---- src/ingame/playercharacter_utest.lua | 50 +++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/pico-boots b/pico-boots index d3ffe4d2..33e6e245 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit d3ffe4d252425b852770a4343ceddbcea4947493 +Subproject commit 33e6e245a62ad5444933d7af4ad6fd57a9648743 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 999e3acd..2a5aa5a9 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -125,18 +125,30 @@ function player_char:get_full_height() return self:is_compact() and pc_data.full_height_compact or pc_data.full_height_standing end -function player_char:get_quadrant_slope_angle() +-- return quadrant tangent right angle (not quadrant interior direction angle!) +-- convenient to use since it is 0 when quadrant is down +function player_char:get_quadrant_right_angle() -- a math trick to transform direction enum value to angle -- (down being 0, up 0.25, etc.) -- make sure not to change directions enum values order! - local quadrant_angle = 0.25 * (3-self.quadrant) % 4 - return self.slope_angle - quadrant_angle + return 0.25 * (3-self.quadrant) % 4 +end + +-- return slope angle, relative to quadrant tangent right +function player_char:get_quadrant_slope_angle() + return self.slope_angle - self:get_quadrant_right_angle() +end + +-- return copy of vector rotated by quadrant right angle +-- this is a forward transformation, and therefore useful for intention (ground motion) +function player_char:quadrant_rotated(v) + return v:rotated(self:get_quadrant_right_angle()) end -- return the horizontal coordinate of a position vector in current quadrant -- (x if down/up, y if right/left) -- we insist on position because quadrant meaning differs for directional vectors --- (sign of x and y would matter) +-- (where sign of x and y would matter with rotation, as in quadrant_rotated) function player_char:get_position_quadrant_x(pos) -- directions value-dependent trick: left and right are 0 and 2 (even) -- whereas up and down are 1 and 3 (odd), so check for parity @@ -788,11 +800,11 @@ end -- 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.x should be floored for these steps +-- 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) -- compute candidate position on next step. only flat slopes supported - local step_vec = horizontal_dir_vectors[quadrant_horizontal_dir] + local step_vec = self:quadrant_rotated(horizontal_dir_vectors[quadrant_horizontal_dir]) local next_position_candidate = ref_motion_result.position + step_vec -- check if next position is inside/above ground diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index d6be48af..b997c029 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -250,6 +250,30 @@ describe('player_char', function () end) + describe('get_quadrant_right_angle', function () + + it('should return 0 when quadrant is down', function () + pc.quadrant = directions.down + assert.are_equal(0, pc:get_quadrant_right_angle()) + end) + + it('should return 0.25 when quadrant is right', function () + pc.quadrant = directions.right + assert.are_equal(0.25, pc:get_quadrant_right_angle()) + end) + + it('should return 0.5 when quadrant is up', function () + pc.quadrant = directions.up + assert.are_equal(0.5, pc:get_quadrant_right_angle()) + end) + + it('should return 0.75 when quadrant is left', function () + pc.quadrant = directions.left + assert.are_equal(0.75, pc:get_quadrant_right_angle()) + end) + + end) + describe('get_quadrant_slope_angle', function () it('should return 0 when vertically relative to the quadrant (down)', function () @@ -290,6 +314,30 @@ describe('player_char', function () end) + describe('quadrant_rotated', function () + + it('should return same vector content when quadrant is down', function () + pc.quadrant = directions.down + assert.are_same(vector(1, -2), pc:quadrant_rotated(vector(1, -2))) + end) + + it('should return vector rotated by 0.25 when quadrant is right', function () + pc.quadrant = directions.right + assert.is_true(almost_eq_with_message(vector(-2, -1), pc:quadrant_rotated(vector(1, -2)))) + end) + + it('should return vector rotated by 0.5 when quadrant is up', function () + pc.quadrant = directions.up + assert.is_true(almost_eq_with_message(vector(-1, 2), pc:quadrant_rotated(vector(1, -2)))) + end) + + it('should return vector rotated by 0.75 when quadrant is left', function () + pc.quadrant = directions.left + assert.is_true(almost_eq_with_message(vector(2, 1), pc:quadrant_rotated(vector(1, -2)))) + end) + + end) + describe('get_position_quadrant_x', function () it('should return pos.x when quadrant is down', function () @@ -3102,7 +3150,7 @@ describe('player_char', function () end) -- _compute_ground_motion_result - describe('_next_ground_step', function () + describe('#solo _next_ground_step', function () -- for these utests, we assume that _compute_ground_sensors_signed_distance and -- _is_blocked_by_ceiling are correct, From e0859435b52a0266146d9b766109e14a2443e6b0 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 16 Aug 2020 14:49:40 +0200 Subject: [PATCH 074/112] [CI] Travis: do not build_game.sh debug anymore (currently over 65536 characters but don't want to fail if build release works) --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7415dcbf..19d1a887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,7 +57,10 @@ before_script: script: # build game and itest to make sure everything works fine # (even if build fails, tests will be run independently thanks to busted) - - ./build_game.sh debug + # disabled build_game.sh debug because character count may get over 65536 + # easily when working at the limit, and build release is what really counts + # (although it's bad sign for us if we cannot debug the game) + # - ./build_game.sh debug - ./build_game.sh release # disabled build_itest.sh in Travis until strings are compressed # because character count is over 65536 and that systematically failed the build From 0db1c29cd9bfdd8562e617835f195ea7ad3034c0 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 16 Aug 2020 17:43:12 +0200 Subject: [PATCH 075/112] [PLAYER CHARACTER] Adapted _get_ground_sensor_position_from to quadrant Added get_quadrant_right/down ! Tokens -> 8628 ! Not compatible with previous motion on steep slopes, but compatible with motion of floor (quadrant down) --- pico-boots | 2 +- src/ingame/playercharacter.lua | 124 +++++++++++++++++++-------- src/ingame/playercharacter_utest.lua | 64 ++++++++++++-- 3 files changed, 145 insertions(+), 45 deletions(-) diff --git a/pico-boots b/pico-boots index 33e6e245..2c242ca5 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 33e6e245a62ad5444933d7af4ad6fd57a9648743 +Subproject commit 2c242ca549d699fcd8cafcb64a1a4fd2742e0014 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 2a5aa5a9..a33e42a8 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -139,17 +139,27 @@ function player_char:get_quadrant_slope_angle() return self.slope_angle - self:get_quadrant_right_angle() end +-- return quadrant tangent right (forward) unit vector +function player_char:get_quadrant_right() + return dir_vectors[rotate_dir_90_ccw(self.quadrant)] +end + +-- return quadrant normal down (interior) unit vector +function player_char:get_quadrant_down() + return dir_vectors[self.quadrant] +end + -- return copy of vector rotated by quadrant right angle -- this is a forward transformation, and therefore useful for intention (ground motion) function player_char:quadrant_rotated(v) return v:rotated(self:get_quadrant_right_angle()) end --- return the horizontal coordinate of a position vector in current quadrant +-- return the horizontal coordinate of a vector in current quadrant -- (x if down/up, y if right/left) --- we insist on position because quadrant meaning differs for directional vectors --- (where sign of x and y would matter with rotation, as in quadrant_rotated) -function player_char:get_position_quadrant_x(pos) +-- make sure to always extract quadrant coordinates after doing operations +-- with quadrant-rotated vectors, to always have matching contribution signs +function player_char:get_quadrant_x_coord(pos) -- directions value-dependent trick: left and right are 0 and 2 (even) -- whereas up and down are 1 and 3 (odd), so check for parity return self.quadrant % 2 == 0 and pos.y or pos.x @@ -158,7 +168,7 @@ end -- return the "x" coordinate in current quadrant, i.e. the coordinate -- on the quadrant horizontal axis function player_char:get_quadrant_x() - return self:get_position_quadrant_x(self.position) + return self:get_quadrant_x_coord(self.position) end -- set the horizontal coordinate of a position vector in current quadrant @@ -334,7 +344,8 @@ function player_char:_compute_ground_sensors_signed_distance(center_position) local query_info = self:_compute_signed_distance_to_closest_ground(sensor_position) local signed_distance, slope_angle = query_info.signed_distance, query_info.slope_angle - -- apply ground priority rule: highest ground, then velocity x sign breaks tie, then horizontal direction breaks tie + -- apply ground priority rule: highest ground, then ground speed (velocity X in the air) sign breaks tie, + -- then q-horizontal direction breaks tie -- store the biggest penetration height among sensors if signed_distance < min_signed_distance then @@ -355,29 +366,41 @@ end function player_char:_get_prioritized_dir() if self:is_grounded() then + -- on the ground, ground speed decides priority if self.ground_speed ~= 0 then return signed_speed_to_dir(self.ground_speed) end else + -- in the air, no quadrant, just use velocity X if self.velocity.x ~= 0 then return signed_speed_to_dir(self.velocity.x) end end + -- if not moving, orientation decides priority return self.orientation end --- return the position of the ground sensor in horizontal_dir when the character center is at center_position +-- 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, horizontal_dir) - - -- ignore subpixels from center position in x - local x_floored_center_position = vector(flr(center_position.x), center_position.y) - local x_floored_bottom_center = x_floored_center_position + vector(0, self:get_center_height()) +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 + if contains({directions.up, directions.down}, self.quadrant) then + x = flr(x) + else + y = flr(y) + end + -- from character center, move down by center height to get the character bottom center + local qx_floored_bottom_center = vector(x, y) + self:get_center_height() * self:get_quadrant_down() -- using a ground_sensor_extent_x in .5 and flooring +/- this value allows us to get the checked column x (the x corresponds to the left of that column) - local offset_x = flr(horizontal_dir_signs[horizontal_dir] * pc_data.ground_sensor_extent_x) + -- rotate proper vector (initially horizontal) for quadrant compatibility + local offset_qx_vector = self:quadrant_rotated(vector(flr(horizontal_dir_signs[quadrant_horizontal_dir] * pc_data.ground_sensor_extent_x), 0)) - return x_floored_bottom_center + vector(offset_x, 0) + return qx_floored_bottom_center + offset_qx_vector end -- return (signed_distance, slope_angle) where: @@ -387,9 +410,9 @@ end -- if no closest ground is detected, this defaults to max_ground_snap_height+1 (character in the air) -- - slope_angle is the slope angle of the detected ground (whether character is touching it, above or below) -- the closest ground is detected in the range [-max_ground_escape_height-1, max_ground_snap_height+1] --- around the sensor_position.y, so it's easy to know if the character can step up/down, +-- around the sensor_position's qy, so it's easy to know if the character can q-step up/down, -- and so that it's meaningful to check for ceiling obstacles after the character did his best to step --- the test should be tile-insensitive so it is possible to detect step up/down in vertical-neighboring tiles +-- the test should be tile-insensitive so it is possible to detect q-step up/down in vertical-neighboring tiles function player_char:_compute_signed_distance_to_closest_ground(sensor_position) assert(flr(sensor_position.x) == sensor_position.x, "player_char:_compute_signed_distance_to_closest_ground: sensor_position.x must be floored") @@ -688,6 +711,13 @@ function player_char:_compute_ground_motion_result() -- ("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) @@ -703,6 +733,7 @@ function player_char:_compute_ground_motion_result() -- - 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 @@ -712,7 +743,8 @@ function player_char:_compute_ground_motion_result() -- 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 is already integer + -- 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 @@ -731,9 +763,18 @@ function player_char:_compute_ground_motion_result() -- 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 signed_distance_qx = self.ground_speed * cos(self:get_quadrant_slope_angle()) + local ground_based_signed_distance_qx = self.ground_speed * cos(self:get_quadrant_slope_angle()) + -- 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() + -- tokens: if get_quadrant_right_angle is not used elsewhere, consider the version below + -- which is more lengthy but doesn't require get_quadrant_right_angle definition so ultimately more compact + -- 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 = self:get_quadrant_x_coord(ground_velocity_projected_on_quadrant_right) + -- max_distance_qx is always integer - local max_distance_qx = player_char._compute_max_pixel_distance(qx, signed_distance_qx) + 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 @@ -749,7 +790,7 @@ function player_char:_compute_ground_motion_result() 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 + signed_distance_qx > self:get_position_quadrant_x(motion_result.position) + local are_subpixels_left = qx + projected_velocity_qx > self:get_quadrant_x_coord(motion_result.position) if are_subpixels_left then -- character has not been blocked and has some subpixels left to go @@ -760,7 +801,7 @@ function player_char:_compute_ground_motion_result() -- 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 signed_distance_qx > 0 then + 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 @@ -773,11 +814,12 @@ function player_char:_compute_ground_motion_result() -- 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 tough, - -- as it depends on the shape of the ground) + -- (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 - self:set_position_quadrant_x(motion_result.position, qx + signed_distance_qx) + self:set_position_quadrant_x(motion_result.position, qx + projected_velocity_qx) end end end @@ -785,7 +827,7 @@ function player_char:_compute_ground_motion_result() return motion_result end --- return the number of new pixel columns explored when moving from initial_position_coord (x or y) +-- 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) @@ -811,14 +853,22 @@ function player_char:_next_ground_step(quadrant_horizontal_dir, ref_motion_resul local query_info = self:_compute_ground_sensors_signed_distance(next_position_candidate) local signed_distance_to_closest_ground, next_slope_angle = query_info.signed_distance, query_info.slope_angle + -- 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, + -- 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 simpy not change + -- 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 if - signed_distance_to_closest_ground <= pc_data.max_ground_escape_height then -- step up - next_position_candidate.y = next_position_candidate.y + signed_distance_to_closest_ground + 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 @@ -833,34 +883,36 @@ function player_char:_next_ground_step(quadrant_horizontal_dir, ref_motion_resul -- (step down is during ground motion only) if signed_distance_to_closest_ground <= pc_data.max_ground_snap_height then -- step down - next_position_candidate.y = next_position_candidate.y + signed_distance_to_closest_ground + next_position_candidate:add_inplace(vector_to_closest_ground) -- if character left the ground during a previous step, cancel that (step down land, very rare) ref_motion_result.is_falling = false 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 - -- caution: we are not updating y at all, which means the character starts + -- 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 upward/downward, preserving - -- velocity y from last frame - -- so when adding momentum, consider reusing the last delta y (e.g. signed_distance_to_closest_ground) + -- momentum to take over and send the character along qy, preserving + -- velocity qvy from last frame + -- so when adding momentum, consider reusing the last delta qy (e.g. vector_to_closest_ground.y) -- and applying it this frame ref_motion_result.is_falling = true end end if not ref_motion_result.is_blocked then - -- character is not blocked by a steep step up/wall, but we need to check if it is - -- blocked by a ceiling too low; in the extreme case, a diagonal tile pattern + -- 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) + -- 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 wall sensors are 1px farther from the character center + -- 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) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index b997c029..b863667d 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -314,6 +314,54 @@ describe('player_char', function () end) + describe('get_quadrant_right', function () + + it('should return vector(1, 0) when quadrant is down', function () + pc.quadrant = directions.down + assert.are_equal(vector(1, 0), pc:get_quadrant_right()) + end) + + it('should return vector(-1, 0) when quadrant is up', function () + pc.quadrant = directions.up + assert.are_equal(vector(-1, 0), pc:get_quadrant_right()) + end) + + it('should return vector(0, -1) when quadrant is right', function () + pc.quadrant = directions.right + assert.are_equal(vector(0, -1), pc:get_quadrant_right()) + end) + + it('should return vector(0, 1) when quadrant is left', function () + pc.quadrant = directions.left + assert.are_equal(vector(0, 1), pc:get_quadrant_right()) + end) + + end) + + describe('get_quadrant_down', function () + + it('should return vector(0, 1) when quadrant is down', function () + pc.quadrant = directions.down + assert.are_equal(vector(0, 1), pc:get_quadrant_down()) + end) + + it('should return vector(0, -1) when quadrant is up', function () + pc.quadrant = directions.up + assert.are_equal(vector(0, -1), pc:get_quadrant_down()) + end) + + it('should return vector(1, 0) when quadrant is right', function () + pc.quadrant = directions.right + assert.are_equal(vector(1, 0), pc:get_quadrant_down()) + end) + + it('should return vector(-1, 0) when quadrant is left', function () + pc.quadrant = directions.left + assert.are_equal(vector(-1, 0), pc:get_quadrant_down()) + end) + + end) + describe('quadrant_rotated', function () it('should return same vector content when quadrant is down', function () @@ -338,34 +386,34 @@ describe('player_char', function () end) - describe('get_position_quadrant_x', function () + describe('get_quadrant_x_coord', function () it('should return pos.x when quadrant is down', function () pc.quadrant = directions.down - assert.are_equal(10, pc:get_position_quadrant_x(vector(10, 20))) + assert.are_equal(10, pc:get_quadrant_x_coord(vector(10, 20))) end) it('should return pos.x when quadrant is up', function () pc.quadrant = directions.up - assert.are_equal(10, pc:get_position_quadrant_x(vector(10, 20))) + assert.are_equal(10, pc:get_quadrant_x_coord(vector(10, 20))) end) it('should return pos.y when quadrant is right', function () pc.quadrant = directions.right - assert.are_equal(20, pc:get_position_quadrant_x(vector(10, 20))) + assert.are_equal(20, pc:get_quadrant_x_coord(vector(10, 20))) end) it('should return pos.y when quadrant is left', function () pc.quadrant = directions.left - assert.are_equal(20, pc:get_position_quadrant_x(vector(10, 20))) + assert.are_equal(20, pc:get_quadrant_x_coord(vector(10, 20))) end) end) describe('get_quadrant_x', function () - -- we already wrote tests for this method before extracting get_position_quadrant_x, - -- so we decided not to rewrite them with stub for get_position_quadrant_x and keep checking + -- we already wrote tests for this method before extracting get_quadrant_x_coord, + -- so we decided not to rewrite them with stub for get_quadrant_x_coord and keep checking -- final result it('should return self.position.x when quadrant is down', function () @@ -3150,7 +3198,7 @@ describe('player_char', function () end) -- _compute_ground_motion_result - describe('#solo _next_ground_step', function () + describe('_next_ground_step', function () -- for these utests, we assume that _compute_ground_sensors_signed_distance and -- _is_blocked_by_ceiling are correct, From 359b826d38bdaa5160232369e49f398a696bd7b5 Mon Sep 17 00:00:00 2001 From: huulong Date: Mon, 17 Aug 2020 16:21:01 +0200 Subject: [PATCH 076/112] [PLAYER CHARACTER] Switched _compute_signed_distance_to_closest_ground to tile-based iteration like _is_column_blocked_by_ceiling_at, for better performance Also to prepare adaptation to quadrant as we'll only need rotated tile steps instead of rotated pixel steps --- src/ingame/playercharacter.lua | 84 ++++++++++++++++++++++------ src/ingame/playercharacter_utest.lua | 41 ++++++++++---- src/platformer/world.lua | 5 +- 3 files changed, 97 insertions(+), 33 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index a33e42a8..1d719dd1 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -416,25 +416,74 @@ end function player_char:_compute_signed_distance_to_closest_ground(sensor_position) assert(flr(sensor_position.x) == sensor_position.x, "player_char:_compute_signed_distance_to_closest_ground: sensor_position.x must be floored") - initial_y = flr(sensor_position.y) - - -- check the presence of a collider pixel from top to bottom, from max step up - 1 to min step up (we don't go until + 1 - -- because if we found nothing until min step down, signed distance will be max step down + 1 anyway) - local query_info = motion.ground_query_info(pc_data.max_ground_snap_height + 1, nil) - for offset_y = -pc_data.max_ground_escape_height - 1, pc_data.max_ground_snap_height do - local does_collide, slope_angle = world.get_pixel_collision_info(sensor_position.x, initial_y + offset_y) - if does_collide then - -- signed_distance is just the current offset, minus the initial subpixel fraction that we ignored for the pixel test iteration - local fraction_y = sensor_position.y - initial_y - query_info = motion.ground_query_info(offset_y - fraction_y, slope_angle) -- slope_angle may still be nil if we are inside ground - break - else - -- optimization: use extra info from is_collision_pixel to skip pixels that we know are empty already thx to the column system + + -- we used to flr sensor_position.y at this point, + -- but actually collision checks don't mind the fractions + -- in addition, we will automatically get the correct signed distance to ground with fractional part! + + + -- check the presence of a colliding column from top to bottom, with offset from -(max step up) to +(min step down) + -- because we work by columns and not pixels, we iterate over tiles directly, so deduce tile locations + -- from sensor + offset position (in qy) + -- we are effectively finding the tiles covered (even partially) by the q-vertical segment between the edge positions + -- where the character can snap up (escape) and snap down + local snap_zone_top = sensor_position + vector(0, - pc_data.max_ground_escape_height) + local snap_zone_bottom = sensor_position + vector(0, pc_data.max_ground_snap_height) + + -- start at the top, remember the bottom to end the iteration + local curr_tile_loc = snap_zone_top:to_location() + local snap_zone_bottom_loc = snap_zone_bottom:to_location() + local sensor_location_topleft = curr_tile_loc:to_topleft_position() + local column_index0 = sensor_position.x - sensor_location_topleft.x -- from 0 to tile_size - 1 + + -- keep looping until we find ground to snap up/down, or ground too far for that, + -- or we've reached the last tile (too low to snap down) + while true do + + local current_tile_top = curr_tile_loc:to_topleft_position().y + local current_tile_bottom = current_tile_top + tile_size + + -- check for ground (by column) in currently checked tile, sensor X + local ground_array_height, slope_angle = world._compute_column_height_at(curr_tile_loc, column_index0) + + -- a column height of 0 doesn't mean that there is ground just below relative offset y = 0, + -- but that the column is empty and we don't know what is more 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 ground_array_height > 0 then + -- signed distance to closest ground is positive when above ground + -- 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 sensor_position.y - current_tile_bottom + -- then subtract ground_array_height and you get the signed distance to the current ground column + local signed_distance_to_closest_ground = current_tile_bottom - sensor_position.y - ground_array_height + if signed_distance_to_closest_ground < -pc_data.max_ground_escape_height then + -- ground found, but character is too deep inside to snap up + -- return edge case (-pc_data.max_ground_escape_height - 1, 0) + -- the slope angle 0 allows to still have character stand straight up visually, + -- but he's probably stuck inside the ground... + return motion.ground_query_info(-pc_data.max_ground_escape_height - 1, 0) + 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 + return motion.ground_query_info(signed_distance_to_closest_ground, slope_angle) + end + -- else: ground has been found, but it is too far below character's feet + -- to snap down. This can only happen on the last tile we iterate on + -- (since it was computed to be at the snap down limit), + -- which means we will enter the "end of iteration" block below + assert(curr_tile_loc.j == snap_zone_bottom_loc.j) + end + + if curr_tile_loc.j == snap_zone_bottom_loc.j then + -- end of iteration, and no ground found or too far below to snap down + -- return edge case for ground considered too far below + -- (pc_data.max_ground_snap_height + 1, nil) + return motion.ground_query_info(pc_data.max_ground_snap_height + 1, nil) end - end - -- return signed distance and slope angle (the latter may be nil) - return query_info + curr_tile_loc.j = curr_tile_loc.j + 1 + end end @@ -964,6 +1013,7 @@ function player_char:_is_column_blocked_by_ceiling_at(sensor_position) while true do + -- TODO QUADRANT -- move 1 tile up from the start, as we can only hit ceiling from a tile above with non-rotated tiles -- note: when we add rotated tiles, we will need to handle ceiling tiles (tiles rotated by 180) -- starting from the current tile, because unlike ground tiles, they may actually block diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index b863667d..790a611a 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1217,7 +1217,7 @@ describe('player_char', function () -- on the sides - it('+ should return ground_query_info(max_ground_snap_height+1, nil) if just at ground height but slightly on the left', function () + it('should return ground_query_info(max_ground_snap_height+1, nil) if just at ground height but slightly on the left', function () assert.are_equal(ground_query_info(pc_data.max_ground_snap_height+1, nil), pc:_compute_signed_distance_to_closest_ground(vector(7, 8))) end) @@ -1279,12 +1279,16 @@ 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(-max_ground_escape_height - 1, 0) if max_ground_escape_height - 1 below the bottom', function () + it('should return ground_query_info(-max_ground_escape_height - 1, 0) (clamped) if max_ground_escape_height - 1 below the bottom', function () assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) end) - it('should return ground_query_info(-max_ground_escape_height - 1, 0) if max_ground_escape_height below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) + it('should return ground_query_info(max_ground_snap_height + 1, nil) if max_ground_escape_height below the bottom', function () + -- this used to be (-pc_data.max_ground_escape_height - 1, 0) but when I switched the algo to a tile-based iteration + -- instead of pixel-based iteration for performance, I noticed there was no need to check for the pixel/tile at sensor_position.y - pc_data.max_ground_escape_height - 1, + -- just sensor_position.y - pc_data.max_ground_escape_height is enough (if there's really something as close at 5px from Sonic's bottom, + -- his head will hit ceiling anyway) + assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) end) -- step up distance reached, character considered in the air @@ -1373,8 +1377,9 @@ describe('player_char', function () assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) end) - it('should return ground_query_info(-max_ground_escape_height - 1, 0) if max_ground_escape_height below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) + it('should return ground_query_info(max_ground_snap_height + 1, nil) if max_ground_escape_height below the bottom', function () + -- same remark as in equivalent test with full tile, used to give same result as test above, now give same result as test below + assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) end) -- step up distance reached, character considered in the air @@ -1400,6 +1405,18 @@ describe('player_char', function () assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15))) end) + it('should return 5 (max_ground_snap_height+1 clamping), nil if 7px above column 0, i.e. at top-most pixel of the ascending slope tile', function () + assert.are_equal(ground_query_info(5, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) + end) + + it('should return 5 (max_ground_snap_height+1), nil if 8px above column 0, i.e. at bottom-most pixel of tile just above the ascending slope tile', function () + assert.are_equal(ground_query_info(5, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 7))) + end) + + it('should return 5 (max_ground_snap_height+1), nil if 15px above column 0, i.e. at top-most pixel of tile just above the ascending slope tile', function () + assert.are_equal(ground_query_info(5, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 0))) + end) + it('. should return 0.0625, 45/360 if just above slope column 4', function () assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 11 - 0.0625))) end) @@ -1438,18 +1455,18 @@ describe('player_char', function () assert.are_equal(ground_query_info(-4, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 16 + 2))) end) - it('should return ground_query_info(-max_ground_escape_height - 1, 45/360) if max_ground_escape_height - 1 below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) + it('should return ground_query_info(-(max_ground_escape_height - 1), 45/360) if max_ground_escape_height - 1 below the top of column 0', function () + assert.are_equal(ground_query_info(-(pc_data.max_ground_escape_height - 1), 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height - 1))) end) - it('should return ground_query_info(-max_ground_escape_height - 1, 45/360) if max_ground_escape_height below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) + it('should return ground_query_info(-max_ground_escape_height, 45/360) if max_ground_escape_height below the top of column 0', function () + assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height))) end) -- step up distance reached, character considered in the air - it('should return ground_query_info(max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 below the bottom', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height + 1))) + it('should return ground_query_info(max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 below the top of column 0', function () + assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height + 1))) end) end) diff --git a/src/platformer/world.lua b/src/platformer/world.lua index f68edcf7..8a384964 100644 --- a/src/platformer/world.lua +++ b/src/platformer/world.lua @@ -22,8 +22,6 @@ function world._compute_column_height_at(tile_location, column_index0) assert(tcd, "collision_data.tiles_collision_data does not contain entry for sprite id: "..current_tile_id..", yet it has the collision flag set") if tcd then - -- optimize: cache collision height array on game start (otherwise, we get all the data every time, - -- including the unused slope angle) return tcd:get_height(column_index0), tcd.slope_angle end @@ -31,8 +29,7 @@ function world._compute_column_height_at(tile_location, column_index0) end - -- returning nil is optional in Lua but it makes it clearer than we expect 2 values - return 0, nil + return 0--, nil end From 5817bdf924730f833f21e6a5873675f0fa1ef645 Mon Sep 17 00:00:00 2001 From: huulong Date: Mon, 17 Aug 2020 17:37:21 +0200 Subject: [PATCH 077/112] [PLAYER CHARACTER] Adapted _compute_signed_distance_to_closest_ground to quadrant by adding more quadrant helpers to get q-bottom and q-dy, and iterating on tiles by tile_vector step (using new location __add) --- pico-boots | 2 +- src/ingame/playercharacter.lua | 115 +++++++++++++++++++-------- src/ingame/playercharacter_utest.lua | 96 ++++++++++++++++++++++ 3 files changed, 181 insertions(+), 32 deletions(-) diff --git a/pico-boots b/pico-boots index 2c242ca5..f8468dc9 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 2c242ca549d699fcd8cafcb64a1a4fd2742e0014 +Subproject commit f8468dc9d081dbdbd8be2b4c1b9c1d985ce8c9ae diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 1d719dd1..9ef7dfa8 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -165,6 +165,16 @@ function player_char:get_quadrant_x_coord(pos) return self.quadrant % 2 == 0 and pos.y or pos.x end +-- same, but for qy +function player_char:get_quadrant_y_coord(pos) + return self.quadrant % 2 == 1 and pos.y or pos.x +end + +-- same, but for qj (tilemap location) +function player_char:get_quadrant_j_coord(pos) + return self.quadrant % 2 == 1 and pos.j or pos.i +end + -- return the "x" coordinate in current quadrant, i.e. the coordinate -- on the quadrant horizontal axis function player_char:get_quadrant_x() @@ -183,6 +193,32 @@ function player_char:set_position_quadrant_x(pos, value) end end +-- return the qy of the q-bottom edge of a tile +-- e.g. left edge x if quadrant is left, bottom edge y if quadrant is down +function player_char:get_tile_qbottom(tile_loc) + -- to avoid if/elseif and handle everything in one formula: + -- - start from tile center + -- - move in quadrant (down) direction by a half-tile + -- - get qy + -- tile_size / 2 = 4 + return self:get_quadrant_y_coord(tile_loc:to_center_position() + 4 * self:get_quadrant_down()) +end + +-- return the difference from qy1 to qy2, +-- but apply sign change to respect q-up, +-- i.e. if qy1 represents a value higher in quadrant frame than qy2, +-- result should be positive +function player_char:sub_qy(qy1, qy2) + -- directions value-dependent trick: up and left are 0 and 1 (< 2) + -- and only those have a reversed qy + -- quadrant down has the normal operation, as usual + if self.quadrant < 2 then + return qy2 - qy1 + else + return qy1 - qy2 + end +end + -- spawn character at given position, detecting ground/air on arrival function player_char:spawn_at(position) self:_setup() @@ -388,7 +424,9 @@ function player_char:_get_ground_sensor_position_from(center_position, quadrant_ -- 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 - if contains({directions.up, directions.down}, self.quadrant) then + + -- vertical: up (1) and down (3) + if self.quadrant % 2 == 1 then x = flr(x) else y = flr(y) @@ -404,63 +442,78 @@ function player_char:_get_ground_sensor_position_from(center_position, quadrant_ end -- return (signed_distance, slope_angle) where: --- - signed distance to closest ground from floored sensor_position, +-- - signed distance to closest ground from sensor_position, -- either negative when (in abs, penetration height, clamped to max_ground_escape_height+1) -- or positive (actual distance to ground, clamped to max_ground_snap_height+1) -- if no closest ground is detected, this defaults to max_ground_snap_height+1 (character in the air) -- - slope_angle is the slope angle of the detected ground (whether character is touching it, above or below) -- the closest ground is detected in the range [-max_ground_escape_height-1, max_ground_snap_height+1] -- around the sensor_position's qy, so it's easy to know if the character can q-step up/down, --- and so that it's meaningful to check for ceiling obstacles after the character did his best to step +-- and so that it's meaningful to check for q-ceiling obstacles after the character did his best to step -- the test should be tile-insensitive so it is possible to detect q-step up/down in vertical-neighboring tiles function player_char:_compute_signed_distance_to_closest_ground(sensor_position) - assert(flr(sensor_position.x) == sensor_position.x, "player_char:_compute_signed_distance_to_closest_ground: sensor_position.x must be floored") + assert(self:get_quadrant_x_coord(sensor_position) % 1 == 0, "player_char:_compute_signed_distance_to_closest_ground: sensor_position.x must be floored") - -- we used to flr sensor_position.y at this point, + -- we used to flr sensor_position.y (would now be qy) at this point, -- but actually collision checks don't mind the fractions -- in addition, we will automatically get the correct signed distance to ground with fractional part! - - -- check the presence of a colliding column from top to bottom, with offset from -(max step up) to +(min step down) - -- because we work by columns and not pixels, we iterate over tiles directly, so deduce tile locations + -- check the presence of a colliding q-column from q-top to q-bottom, with offset from -(max step up) to +(min step down) + -- because we work by q-columns and not pixels, we iterate over tiles directly, so deduce tile locations -- from sensor + offset position (in qy) -- we are effectively finding the tiles covered (even partially) by the q-vertical segment between the edge positions -- where the character can snap up (escape) and snap down - local snap_zone_top = sensor_position + vector(0, - pc_data.max_ground_escape_height) - local snap_zone_bottom = sensor_position + vector(0, pc_data.max_ground_snap_height) + local snap_zone_qtop = sensor_position + self:quadrant_rotated(vector(0, - pc_data.max_ground_escape_height)) + local snap_zone_qbottom = sensor_position + self:quadrant_rotated(vector(0, pc_data.max_ground_snap_height)) -- start at the top, remember the bottom to end the iteration - local curr_tile_loc = snap_zone_top:to_location() - local snap_zone_bottom_loc = snap_zone_bottom:to_location() + local curr_tile_loc = snap_zone_qtop:to_location() + -- for last tile we only care about qj so just store that + local last_tile_qj = self:get_quadrant_j_coord(snap_zone_qbottom:to_location()) + local sensor_location_topleft = curr_tile_loc:to_topleft_position() - local column_index0 = sensor_position.x - sensor_location_topleft.x -- from 0 to tile_size - 1 + -- we *always* iterate on columns from left to right, rows from top to bottom, + -- and columns/rows are stored exactly like that in collision data (not CCW or anything) + -- so unlike other operations, the subtraction from topleft (combined with qx coord) is correct + -- to get column index for qcolumn height later, without the need to quadrant-rotate vectors first + local qcolumn_index0 = self:get_quadrant_x_coord(sensor_position - sensor_location_topleft) -- from 0 to tile_size - 1 + + -- we iterate on tiles along quadrant down, so just convert it to tile_vector + -- to allow step addition + local quadrant_down = self:get_quadrant_down() + local tile_loc_step = tile_vector(quadrant_down.x, quadrant_down.y) -- keep looping until we find ground to snap up/down, or ground too far for that, -- or we've reached the last tile (too low to snap down) while true do - local current_tile_top = curr_tile_loc:to_topleft_position().y - local current_tile_bottom = current_tile_top + tile_size + local curr_tile_j = self:get_quadrant_j_coord(curr_tile_loc) + + -- get q-bottom of tile to compare heights easily later + local current_tile_qbottom = self:get_tile_qbottom(curr_tile_loc) -- check for ground (by column) in currently checked tile, sensor X - local ground_array_height, slope_angle = world._compute_column_height_at(curr_tile_loc, column_index0) + local qcolumn_height, slope_angle = world._compute_column_height_at(curr_tile_loc, qcolumn_index0) + -- TODO: retrocompatible on floor, but need method below to truly enable + -- loop motion + -- local qcolumn_height, slope_angle = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0) - -- a column height of 0 doesn't mean that there is ground just below relative offset y = 0, - -- but that the column is empty and we don't know what is more below + -- 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 -- 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 ground_array_height > 0 then + if qcolumn_height > 0 then -- signed distance to closest ground is positive when above ground -- 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 sensor_position.y - current_tile_bottom - -- then subtract ground_array_height and you get the signed distance to the current ground column - local signed_distance_to_closest_ground = current_tile_bottom - sensor_position.y - ground_array_height + -- 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 column + local signed_distance_to_closest_ground = self:sub_qy(current_tile_qbottom, sensor_position.y) - qcolumn_height if signed_distance_to_closest_ground < -pc_data.max_ground_escape_height then - -- ground found, but character is too deep inside to snap up + -- ground found, but character is too deep inside to snap q-up -- return edge case (-pc_data.max_ground_escape_height - 1, 0) - -- the slope angle 0 allows to still have character stand straight up visually, + -- the slope angle 0 allows to still have character stand straight (world) up visually, -- but he's probably stuck inside the ground... return motion.ground_query_info(-pc_data.max_ground_escape_height - 1, 0) elseif signed_distance_to_closest_ground <= pc_data.max_ground_snap_height then @@ -468,21 +521,21 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- to allow snapping + set slope angle return motion.ground_query_info(signed_distance_to_closest_ground, slope_angle) end - -- else: ground has been found, but it is too far below character's feet - -- to snap down. This can only happen on the last tile we iterate on - -- (since it was computed to be at the snap down limit), + -- else: ground has been found, but it is too far below character's q-feet + -- to snap q-down. This can only happen on the last tile we iterate on + -- (since it was computed to be at the snap q-down limit), -- which means we will enter the "end of iteration" block below - assert(curr_tile_loc.j == snap_zone_bottom_loc.j) + assert(curr_tile_j == last_tile_qj) end - if curr_tile_loc.j == snap_zone_bottom_loc.j then - -- end of iteration, and no ground found or too far below to snap down + if curr_tile_j == last_tile_qj then + -- 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(pc_data.max_ground_snap_height + 1, nil) end - curr_tile_loc.j = curr_tile_loc.j + 1 + curr_tile_loc = curr_tile_loc + tile_loc_step end end diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 790a611a..57de0e4f 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -410,6 +410,54 @@ describe('player_char', function () end) + describe('get_quadrant_y_coord', function () + + it('should return pos.y when quadrant is down', function () + pc.quadrant = directions.down + assert.are_equal(20, pc:get_quadrant_y_coord(vector(10, 20))) + end) + + it('should return pos.y when quadrant is up', function () + pc.quadrant = directions.up + assert.are_equal(20, pc:get_quadrant_y_coord(vector(10, 20))) + end) + + it('should return pos.y when quadrant is right', function () + pc.quadrant = directions.right + assert.are_equal(10, pc:get_quadrant_y_coord(vector(10, 20))) + end) + + it('should return pos.y when quadrant is left', function () + pc.quadrant = directions.left + assert.are_equal(10, pc:get_quadrant_y_coord(vector(10, 20))) + end) + + end) + + describe('get_quadrant_j_coord', function () + + it('should return loc.j when quadrant is down', function () + pc.quadrant = directions.down + assert.are_equal(2, pc:get_quadrant_j_coord(location(1, 2))) + end) + + it('should return loc.j when quadrant is up', function () + pc.quadrant = directions.up + assert.are_equal(2, pc:get_quadrant_j_coord(location(1, 2))) + end) + + it('should return loc.j when quadrant is right', function () + pc.quadrant = directions.right + assert.are_equal(1, pc:get_quadrant_j_coord(location(1, 2))) + end) + + it('should return loc.j when quadrant is left', function () + pc.quadrant = directions.left + assert.are_equal(1, pc:get_quadrant_j_coord(location(1, 2))) + end) + + end) + describe('get_quadrant_x', function () -- we already wrote tests for this method before extracting get_quadrant_x_coord, @@ -478,6 +526,54 @@ describe('player_char', function () end) + describe('get_tile_qbottom', function () + + it('should return tile world bottom when quadrant is down', function () + pc.quadrant = directions.down + assert.are_equal(24, pc:get_tile_qbottom(location(1, 2))) + end) + + it('should return tile world top when quadrant is up', function () + pc.quadrant = directions.up + assert.are_equal(16, pc:get_tile_qbottom(location(1, 2))) + end) + + it('should return world right when quadrant is right', function () + pc.quadrant = directions.right + assert.are_equal(16, pc:get_tile_qbottom(location(1, 2))) + end) + + it('should return world left when quadrant is left', function () + pc.quadrant = directions.left + assert.are_equal(8, pc:get_tile_qbottom(location(1, 2))) + end) + + end) + + describe('sub_qy', function () + + it('should return qy1 - qy2 when quadrant is down', function () + pc.quadrant = directions.down + assert.are_equal(7, pc:sub_qy(10, 3)) + end) + + it('should return qy2 - qy1 when quadrant is up', function () + pc.quadrant = directions.up + assert.are_equal(7, pc:sub_qy(3, 10)) + end) + + it('should return qy1 - qy2 when quadrant is right', function () + pc.quadrant = directions.right + assert.are_equal(7, pc:sub_qy(10, 3)) + end) + + it('should return qy2 - qy1 when quadrant is left', function () + pc.quadrant = directions.left + assert.are_equal(7, pc:sub_qy(3, 10)) + end) + + end) + describe('spawn_at', function () setup(function () From c4becc2270b7a94da10f5def2d98388288380932 Mon Sep 17 00:00:00 2001 From: huulong Date: Mon, 17 Aug 2020 18:16:36 +0200 Subject: [PATCH 078/112] [WORLD] Adapted _compute_(q)column_height_at to quadrant Added proto loop_topleft tile to test --- src/data/collision_data.lua | 2 + src/ingame/playercharacter.lua | 8 ++-- src/platformer/world.lua | 17 ++++++--- src/platformer/world_utest.lua | 54 ++++++++++++++++++++++----- src/test_data/tile_representation.lua | 1 + src/test_data/tile_test_data.lua | 10 +++-- 6 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua index cd669bb1..a69b5461 100644 --- a/src/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -102,6 +102,7 @@ sprite_flags = { 113 @ (0, 7) ASCENDING 22.5 < slope_angle: 0.0625 ~= atan2(8, -4) (actually 0.0738) but kept for historical utest/itest reasons 116 @ (4, 7) DESCENDING 45 \ slope_angle: 1-0.125 = atan2(1, 1) 117 @ (5, 7) higher 2:1 ascending slope (completes 58 from loop) + 12 @ (12, 0) LOOP TOP-LEFT: reusing mask of loop top-left with itself --]] local raw_tiles_data = serialization.parse_expression( --[tile_id] = tile_data( @@ -172,6 +173,7 @@ local raw_tiles_data = serialization.parse_expression( [113]= {{0, 7}, 0.0625}, [116]= {{4, 7}, {8, 8}}, [117]= {{5, 7}, {8, -4}}, + [12]= {{12, 0}, {-4, 4}} }]], function (t) -- t[2] may be {x, y} to use for atan2 or slope_angle directly -- this is only for [113], if we update utests/itests to use the more correct atan2(8, -4) then we can get rid of diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 9ef7dfa8..1236be1b 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -494,10 +494,7 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) local current_tile_qbottom = self:get_tile_qbottom(curr_tile_loc) -- check for ground (by column) in currently checked tile, sensor X - local qcolumn_height, slope_angle = world._compute_column_height_at(curr_tile_loc, qcolumn_index0) - -- TODO: retrocompatible on floor, but need method below to truly enable - -- loop motion - -- local qcolumn_height, slope_angle = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0) + local qcolumn_height, slope_angle = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0, self.quadrant) -- 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 @@ -1087,7 +1084,8 @@ function player_char:_is_column_blocked_by_ceiling_at(sensor_position) return false end - local ground_array_height, _ = world._compute_column_height_at(curr_tile_loc, column_index0) + -- TODO: pass self.quadrant and adapt coordinates around for quadrant + local ground_array_height, _ = world._compute_qcolumn_height_at(curr_tile_loc, column_index0, directions.down) if ground_array_height ~= nil and ground_array_height > 0 then -- with non-rotated tiles, we are sure to hit the ceiling at this point -- because ceiling is always at a tile bottom, and we return false diff --git a/src/platformer/world.lua b/src/platformer/world.lua index 8a384964..4f662c62 100644 --- a/src/platformer/world.lua +++ b/src/platformer/world.lua @@ -3,10 +3,11 @@ local collision_data = require("data/collision_data") local world = {} --- return (column_height, slope_angle) where: --- - column_height is the column height at tile_location on column_index0, or 0 if there is no colliding tile +-- return (qcolumn_height, slope_angle) where: +-- - qcolumn_height is the qcolumn height at tile_location on qcolumn_index0, or 0 if there is no colliding tile +-- (if quadrant is horizontal, qcolum = row, but indices are always top to bottom, left to right) -- - slope_angle is the slope angle of the corresponding tile, or nil if there is no colliding tile -function world._compute_column_height_at(tile_location, column_index0) +function world._compute_qcolumn_height_at(tile_location, qcolumn_index0, quadrant) -- only consider valid tiles; consider there are no colliding tiles outside the map area if tile_location.i >= 0 and tile_location.i < 128 and tile_location.j >= 0 and tile_location.j < 64 then @@ -22,7 +23,12 @@ function world._compute_column_height_at(tile_location, column_index0) assert(tcd, "collision_data.tiles_collision_data does not contain entry for sprite id: "..current_tile_id..", yet it has the collision flag set") if tcd then - return tcd:get_height(column_index0), tcd.slope_angle + -- up (1) and down (3) are odd + if quadrant % 2 == 1 then + return tcd:get_height(qcolumn_index0), tcd.slope_angle + else + return tcd:get_width(qcolumn_index0), tcd.slope_angle + end end end @@ -33,6 +39,7 @@ function world._compute_column_height_at(tile_location, column_index0) end +-- DEPRECATED, remove to spare tokens -- return (true, slope_angle) if there is a collision pixel at (x, y), -- where slope_angle is the slope angle in this tile (even if (x, y) is inside ground), -- and (false, nil) if there is no collision @@ -47,7 +54,7 @@ function world.get_pixel_collision_info(x, y) -- environment local column_index0 = x - left -- from 0 to tile_size - 1 - local ground_array_height, slope_angle = world._compute_column_height_at(location, column_index0) + local ground_array_height, slope_angle = world._compute_qcolumn_height_at(location, column_index0, directions.down) -- if column is empty, there cannot be any pixel collision if ground_array_height > 0 then diff --git a/src/platformer/world_utest.lua b/src/platformer/world_utest.lua index 239b2f01..9292f35d 100644 --- a/src/platformer/world_utest.lua +++ b/src/platformer/world_utest.lua @@ -17,14 +17,14 @@ describe('world (with mock tiles data setup)', function () pico8:clear_map() end) - describe('_compute_column_height_at', function () + describe('_compute_qcolumn_height_at', function () - it('should return (0, nil) if tile location is outside map area', function () - assert.are_same({0, nil}, {world._compute_column_height_at(location(-1, 2), 0)}) + it('should return (0, nil) if tile location is outside map area (any quadrant)', function () + assert.are_same({0, nil}, {world._compute_qcolumn_height_at(location(-1, 2), 0, directions.down)}) end) - it('should return (0, nil) if tile has collision flag unset', function () - assert.are_same({0, nil}, {world._compute_column_height_at(location(1, 1), 0)}) + it('should return (0, nil) if tile has collision flag unset (any quadrant)', function () + assert.are_same({0, nil}, {world._compute_qcolumn_height_at(location(1, 1), 0, directions.right)}) end) describe('with invalid tile', function () @@ -34,9 +34,9 @@ describe('world (with mock tiles data setup)', function () mock_mset(1, 1, 1) end) - it('should assert if tile has collision flag set but no collision mask id associated', function () + it('should assert if tile has collision flag set but no collision mask id associated (any quadrant)', function () assert.has_error(function () - world._compute_column_height_at(location(1, 1), 0) + world._compute_qcolumn_height_at(location(1, 1), 0, directions.up) end, "collision_data.tiles_collision_data does not contain entry for sprite id: 1, yet it has the collision flag set") end) @@ -50,8 +50,44 @@ describe('world (with mock tiles data setup)', function () mock_mset(1, 1, asc_slope_22_id) end) - it('#solo should return 3 on column 3', function () - assert.are_same({3, 22.5 / 360}, {world._compute_column_height_at(location(1, 1), 3)}) + it('should return 3 on column 3 (quadrant down)', function () + assert.are_same({3, 22.5 / 360}, {world._compute_qcolumn_height_at(location(1, 1), 3, directions.down)}) + end) + + it('should return 3 on column 3 (quadrant up)', function () + assert.are_same({3, 22.5 / 360}, {world._compute_qcolumn_height_at(location(1, 1), 3, directions.up)}) + end) + + it('should return 3 on column 3 (quadrant right)', function () + assert.are_same({2, 22.5 / 360}, {world._compute_qcolumn_height_at(location(1, 1), 3, directions.right)}) + end) + + it('should return 3 on column 3 (quadrant left)', function () + assert.are_same({2, 22.5 / 360}, {world._compute_qcolumn_height_at(location(1, 1), 3, directions.left)}) + end) + + end) + + describe('with loop top-left tile', function () + + before_each(function () + mock_mset(1, 1, loop_topleft) + end) + + it('should return 5 on column 6 (quadrant down)', function () + assert.are_same({6, atan2(-4, 4)}, {world._compute_qcolumn_height_at(location(1, 1), 6, directions.down)}) + end) + + it('should return 5 on column 6 (quadrant up)', function () + assert.are_same({6, atan2(-4, 4)}, {world._compute_qcolumn_height_at(location(1, 1), 6, directions.up)}) + end) + + it('should return 5 on column 6 (quadrant right)', function () + assert.are_same({6, atan2(-4, 4)}, {world._compute_qcolumn_height_at(location(1, 1), 6, directions.right)}) + end) + + it('should return 5 on column 6 (quadrant left)', function () + assert.are_same({6, atan2(-4, 4)}, {world._compute_qcolumn_height_at(location(1, 1), 6, directions.left)}) end) end) diff --git a/src/test_data/tile_representation.lua b/src/test_data/tile_representation.lua index a1f19b00..48cb8d11 100644 --- a/src/test_data/tile_representation.lua +++ b/src/test_data/tile_representation.lua @@ -12,6 +12,7 @@ asc_slope_45_id = 112 desc_slope_45_id = 116 asc_slope_22_id = 113 asc_slope_22_upper_level_id = 117 +loop_topleft = 12 -- symbol mapping for itests -- (could also be used for utests instead of manual mock_mset, but need to extract parse_tilemap diff --git a/src/test_data/tile_test_data.lua b/src/test_data/tile_test_data.lua index 6fe7def3..006088a2 100644 --- a/src/test_data/tile_test_data.lua +++ b/src/test_data/tile_test_data.lua @@ -18,6 +18,7 @@ local mock_tile_collision_data = { [asc_slope_22_id] = tile_collision_data({2, 2, 3, 3, 4, 4, 5, 5}, {0, 0, 0, 2, 4, 6, 8, 8}, 0.0625), [desc_slope_45_id] = tile_collision_data({8, 7, 6, 5, 4, 3, 2, 1}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, 8)), [asc_slope_22_upper_level_id] = tile_collision_data({5, 5, 6, 6, 7, 7, 8, 8}, {2, 4, 6, 8, 8, 8, 8, 8}, atan2(8, -4)), + [loop_topleft] = tile_collision_data({8, 8, 8, 8, 8, 7, 6, 5}, {8, 8, 8, 8, 8, 7, 6, 5}, atan2(-4, 4)), } local tile_test_data = {} @@ -26,12 +27,13 @@ function tile_test_data.setup() -- mock sprite flags fset(1, sprite_flags.collision, true) -- invalid tile (missing collision mask id location below) fset(full_tile_id, sprite_flags.collision, true) -- full tile - fset(asc_slope_45_id, sprite_flags.collision, true) -- ascending slope 45 - fset(desc_slope_45_id, sprite_flags.collision, true) -- descending slope 45 - fset(asc_slope_22_id, sprite_flags.collision, true) -- ascending slope 22.5 offset by 2 fset(half_tile_id, sprite_flags.collision, true) -- half-tile (bottom half) - fset(bottom_right_quarter_tile_id, sprite_flags.collision, true) -- quarter-tile (bottom-right half) fset(flat_low_tile_id, sprite_flags.collision, true) -- low-tile (bottom quarter) + fset(bottom_right_quarter_tile_id, sprite_flags.collision, true) -- quarter-tile (bottom-right half) + fset(asc_slope_45_id, sprite_flags.collision, true) -- ascending slope 45 + fset(asc_slope_22_id, sprite_flags.collision, true) -- ascending slope 22.5 offset by 2 + fset(desc_slope_45_id, sprite_flags.collision, true) -- descending slope 45 + fset(loop_topleft, sprite_flags.collision, true) -- low-tile (bottom quarter) -- mock height array _init so it doesn't have to dig in sprite data, inaccessible from busted stub(collision_data, "get_tile_collision_data", function (current_tile_id) From af38d9dbc681cef986f77171fc029a5a12861075 Mon Sep 17 00:00:00 2001 From: huulong Date: Tue, 18 Aug 2020 15:34:08 +0200 Subject: [PATCH 079/112] [TEST] Completed 100% coverage on tile_collision_data utest --- src/data/tile_collision_data_utest.lua | 32 +++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/data/tile_collision_data_utest.lua b/src/data/tile_collision_data_utest.lua index 849f865d..57eeda57 100644 --- a/src/data/tile_collision_data_utest.lua +++ b/src/data/tile_collision_data_utest.lua @@ -6,7 +6,8 @@ local raw_tile_collision_data = require("data/raw_tile_collision_data") -- when we have to mock tile sprite data in PICO-8, -- we use the following --- mask tile 1: bottom-right asc slope +-- mask tile 1: bottom-right asc slope variant with column 0 empty +-- (just to cover case column_height = 0 in read_height_array) -- pixel representation: -- ........ -- ........ @@ -15,7 +16,7 @@ local raw_tile_collision_data = require("data/raw_tile_collision_data") -- ......## -- ....#### -- ..###### --- ######## +-- .####### -- mask tile 2: top-left concave ceiling -- pixel representation: @@ -33,8 +34,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({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4), horizontal_dirs.right, vertical_dirs.down) - assert.are_same({{1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4)}, {tcd.height_array, tcd.width_array, tcd.slope_angle}) + local tcd = tile_collision_data({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({{0, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 7}, atan2(8, -4)}, {tcd.height_array, tcd.width_array, tcd.slope_angle}) end) end) @@ -42,7 +43,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({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4)) + local tcd = tile_collision_data({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) @@ -51,7 +52,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({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4)) + local tcd = tile_collision_data({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) @@ -62,14 +63,14 @@ describe('tile_collision_data', function () setup(function () stub(tile_collision_data, "read_height_array", function (tile_mask_id_location, slope_angle) if tile_mask_id_location == 1 then - return {1, 1, 2, 2, 3, 3, 4, 4} + return {0, 1, 2, 2, 3, 3, 4, 4} else return {8, 6, 4, 3, 2, 2, 1, 1} end end) stub(tile_collision_data, "read_width_array", function (tile_mask_id_location, slope_angle) if tile_mask_id_location == 1 then - return {0, 0, 0, 0, 2, 4, 6, 8} + return {0, 0, 0, 0, 2, 4, 6, 7} else return {8, 6, 4, 3, 2, 2, 1, 1} end @@ -85,7 +86,7 @@ describe('tile_collision_data', function () local raw_data = raw_tile_collision_data(1, atan2(8, -4)) local tcd = tile_collision_data.from_raw_tile_collision_data(raw_data) -- 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({1, 1, 2, 2, 3, 3, 4, 4}, {0, 0, 0, 0, 2, 4, 6, 8}, atan2(8, -4), horizontal_dirs.right, vertical_dirs.down), tcd) + assert.are_same(tile_collision_data({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), tcd) end) it('should return a tile_collision_data containing (mock tile 2) height/width array, slope angle, derived interior directions', function () @@ -110,7 +111,7 @@ describe('tile_collision_data', function () {0, 0, 0, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 1, 1, 1, 1}, {0, 0, 1, 1, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1}, + {0, 1, 1, 1, 1, 1, 1, 1}, } local mock_mask_dot_matrix2 = { @@ -143,16 +144,17 @@ describe('tile_collision_data', function () sget:revert() end) - -- read_height_array utests could be done without mocking sget + -- read_height/width_array utests could be done without mocking sget -- and mocking check_collision_pixel instead, but since we had already written the utests below -- (which check the final result without stubbing) before extracting check_collision_pixel, -- it was simpler to just keep them, that to create a stub for check_collision_pixel that would cheat a lot -- with the passed arguments + describe('read_height_array', function () it('should return an array with respective column heights, from left to right', function () local array = tile_collision_data.read_height_array(sprite_id_location(1, 0), vertical_dirs.down) - assert.are_same({1, 1, 2, 2, 3, 3, 4, 4}, array) + assert.are_same({0, 1, 2, 2, 3, 3, 4, 4}, array) end) it('should return an array with respective column heights, from left to right', function () @@ -160,9 +162,13 @@ describe('tile_collision_data', function () assert.are_same({8, 6, 4, 3, 2, 2, 1, 1}, array) end) + end) + + describe('read_width_array', function () + it('should return an array with respective column rows, from top to bottom', function () local array = tile_collision_data.read_width_array(sprite_id_location(1, 0), horizontal_dirs.right) - assert.are_same({0, 0, 0, 0, 2, 4, 6, 8}, array) + assert.are_same({0, 0, 0, 0, 2, 4, 6, 7}, array) end) it('should return an array with respective column rows, from top to bottom', function () From d2b62e4b4dce21a9ad328b9ba95c34dec667f455 Mon Sep 17 00:00:00 2001 From: huulong Date: Tue, 18 Aug 2020 15:35:02 +0200 Subject: [PATCH 080/112] [TEST] Added _compute_ground_motion_result test cases for all quadrants --- src/ingame/playercharacter_utest.lua | 387 ++++++++++++++++++++++++--- 1 file changed, 346 insertions(+), 41 deletions(-) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 57de0e4f..fbd00b76 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -2817,17 +2817,63 @@ describe('player_char', function () pc:_compute_ground_motion_result() ) end) + + it('(wall right) should return the current position and slope, is_blocked: false, is_falling: false', function () + pc.position = vector(3, 4.5) + pc.quadrant = directions.right + pc.slope_angle = 0.25 + + assert.are_equal(motion.ground_motion_result( + vector(3, 4.5), + 0.25, + false, + false + ), + pc:_compute_ground_motion_result() + ) + end) + + it('(ceiling) should return the current position and slope, is_blocked: false, is_falling: false', function () + pc.position = vector(3, 4.5) + pc.quadrant = directions.up + pc.slope_angle = 0.5 + + assert.are_equal(motion.ground_motion_result( + vector(3, 4.5), + 0.5, + false, + false + ), + pc:_compute_ground_motion_result() + ) + end) + + it('(wall left) should return the current position and slope, is_blocked: false, is_falling: false', function () + pc.position = vector(3, 4.5) + pc.quadrant = directions.left + pc.slope_angle = 0.75 + + assert.are_equal(motion.ground_motion_result( + vector(3, 4.5), + 0.75, + false, + false + ), + pc:_compute_ground_motion_result() + ) + end) + end) - describe('(when _next_ground_step moves motion_result.position.x by 1px in the horizontal_dir without blocking nor falling)', function () + describe('(when _next_ground_step moves motion_result.position by 1px in the quadrant_horizontal_dir without blocking nor falling)', function () local next_ground_step_mock setup(function () - next_ground_step_mock = stub(player_char, "_next_ground_step", function (self, horizontal_dir, motion_result) - local step_vec = horizontal_dir_vectors[horizontal_dir] + 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 - motion_result.slope_angle = 1-0.125 + motion_result.slope_angle = (self:get_quadrant_right_angle() - 0.01) % 1 end) end) @@ -2878,7 +2924,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(4, 4), - 1-0.125, + 1-0.01, false, false ), @@ -2892,7 +2938,23 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(0.5, 4), - 1-0.125, + 1-0.01, + false, + false + ), + pc:_compute_ground_motion_result() + ) + 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.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 + + assert.are_equal(motion.ground_motion_result( + 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 ), @@ -2900,23 +2962,73 @@ describe('player_char', function () ) end) + 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.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 + + -- 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)) + + -- 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_equal(motion.ground_motion_result( + vector(2, 4), + 0.5-0.01, + false, + false + ), + result + ) + 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.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 + + -- 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)) + + -- 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_equal(motion.ground_motion_result( + vector(3, 5), + 0.75-0.01, + false, + false + ), + result + ) + end) + end) - describe('(when _next_ground_step moves motion_result.position.x by 1px in the horizontal_dir, but blocks when motion_result.position.x <= -5 or x >= 5)', function () + 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 () local next_ground_step_mock setup(function () - next_ground_step_mock = stub(player_char, "_next_ground_step", function (self, horizontal_dir, motion_result) - local step_vec = horizontal_dir_vectors[horizontal_dir] - -- x < -4 <=> x <= -5 for an integer as passed to step functions, + 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 then + 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 - motion_result.slope_angle = 0.125 + motion_result.slope_angle = (self:get_quadrant_right_angle() + 0.01) % 1 end end) end) @@ -2932,7 +3044,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(5, 4), - 0.125, + 0.01, false, false ), @@ -2947,7 +3059,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(-5, 4), - 0.125, + 0.01, false, false ), @@ -2964,7 +3076,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(5, 4), - 0.125, + 0.01, false, false ), @@ -2980,7 +3092,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(-5, 4), - 0.125, + 0.01, false, false ), @@ -2998,7 +3110,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(5, 4), - 0.125, -- new slope angle, no relation with initial one + 0.01, -- new slope angle, no relation with initial one false, false ), @@ -3015,7 +3127,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(-5, 4), - 0.125, -- new slope angle, no relation with initial one + 0.01, -- new slope angle, no relation with initial one false, false ), @@ -3033,7 +3145,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(5, 4), - 0.125, + 0.01, true, false ), @@ -3051,7 +3163,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(-5, 4), - 0.125, + 0.01, true, false ), @@ -3101,7 +3213,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(5, 4), - 0.125, + 0.01, true, false ), @@ -3116,7 +3228,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(-5, 4), - 0.125, + 0.01, true, false ), @@ -3213,7 +3325,7 @@ describe('player_char', function () ) end) - it('(vector(3, 4) at speed 3) should return vector(5, 4), slope before blocked, is_blocked: false, is_falling: false', function () + it('(vector(3, 4) at speed 3) should return vector(5, 4), slope before blocked, is_blocked: true, is_falling: false', function () pc.position = vector(3, 4) pc.ground_speed = 3.5 -- we assume _compute_max_pixel_distance is correct, so it should return 3 @@ -3221,7 +3333,7 @@ describe('player_char', function () assert.are_equal(motion.ground_motion_result( vector(5, 4), - 0.125, + 0.01, true, false ), @@ -3229,15 +3341,72 @@ describe('player_char', function () ) end) - it('(vector(-3, 4) at speed -3) should return vector(-5, 4), slope before blocked, is_blocked: false, is_falling: false', function () + it('(vector(-3, 4) at speed -3) should return vector(-5, 4), slope before blocked, is_blocked: true, is_falling: false', function () 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 + -- but because of the blocking, we stop at x=-5 instead of -6.5 assert.are_equal(motion.ground_motion_result( vector(-5, 4), - 0.125, + 0.01, + true, + false + ), + pc:_compute_ground_motion_result() + ) + end) + + 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.position = vector(3, -3) + pc.ground_speed = 3.5 + pc.quadrant = directions.right + pc.slope_angle = 0.25 + + -- 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_equal(motion.ground_motion_result( + vector(3, -5), + 0.25 + 0.01, + true, + false + ), + pc:_compute_ground_motion_result() + ) + end) + + 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.position = vector(-3, 3) + pc.ground_speed = 3.5 + pc.quadrant = directions.up + pc.slope_angle = 0.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_equal(motion.ground_motion_result( + vector(-5, 3), + 0.5 + 0.01, + true, + false + ), + pc:_compute_ground_motion_result() + ) + 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.position = vector(3, 3) + pc.ground_speed = 3.5 + pc.quadrant = directions.left + pc.slope_angle = 0.75 + + -- 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_equal(motion.ground_motion_result( + vector(3, 5), + 0.75 + 0.01, true, false ), @@ -3248,24 +3417,46 @@ describe('player_char', function () end) -- bugfix history: the mock was wrong (was using updated position instead of original_position) - describe('. (when _next_ground_step moves motion_result.position.x by 1px in the horizontal_dir on x < 7, falls on 5 <= x < 7 and blocks on x >= 7)', function () + 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 () local next_ground_step_mock setup(function () - next_ground_step_mock = stub(player_char, "_next_ground_step", function (self, horizontal_dir, motion_result) - local step_vec = horizontal_dir_vectors[horizontal_dir] - local original_position = motion_result.position - if original_position.x < 7 then - motion_result.position = original_position + step_vec - motion_result.slope_angle = 0.25 + 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]) + -- in native Lua, precision errors on cos/sin cause inexact step_vec, fix them now to allow ~= 0 check + if almost_eq(step_vec.x, 0, 1e-15) then + step_vec.x = 0 end - if original_position.x >= 5 then + local original_position = motion_result.position + printh("step_vec: "..dump(step_vec)) + if step_vec.x ~= 0 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 + 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 + printh("falling!") + 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 + printh("motion_result: "..dump(motion_result)) + 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 end) @@ -3275,7 +3466,7 @@ describe('player_char', function () next_ground_step_mock:revert() end) - it('(vector(3, 4) at speed 3) should return vector(6, 4), slope_angle: nil, is_blocked: false, is_falling: false', function () + it('(vector(3, 4) at speed 3) should return vector(6, 4), slope_angle: nil, is_blocked: false, is_falling: true', function () pc.position = vector(3, 4) pc.ground_speed = 3 -- we assume _compute_max_pixel_distance is correct, so it should return 3 @@ -3291,7 +3482,7 @@ describe('player_char', function () ) end) - it('(vector(3, 4) at speed 3) should return vector(7, 4), slope_angle: nil, is_blocked: false, is_falling: false', function () + it('(vector(3, 4) at speed 5) should return vector(7, 4), slope_angle: nil, is_blocked: true, is_falling: true', function () pc.position = vector(3, 4) pc.ground_speed = 5 -- we assume _compute_max_pixel_distance is correct, so it should return 3 @@ -3307,6 +3498,120 @@ describe('player_char', function () ) end) + it('(right wall, vector(4, 3) at speed -3 (moving down)) should return vector(4, 6), slope_angle: nil, is_blocked: false, is_falling: true', function () + pc.position = vector(4, 3) + pc.ground_speed = -3 + pc.quadrant = directions.right + pc.slope_angle = 0.25 + + -- 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.are_equal(motion.ground_motion_result( + vector(4, 6), + nil, + false, + true + ), + pc:_compute_ground_motion_result() + ) + end) + + it('(right wall, vector(4, 3) at speed -5 (moving down)) should return vector(7, 4), slope_angle: nil, is_blocked: true, is_falling: true', function () + pc.position = vector(4, 3) + pc.ground_speed = -5 + pc.quadrant = directions.right + pc.slope_angle = 0.25 + + -- we assume _compute_max_pixel_distance is correct, so it should return 3 + -- we are falling then blocked on 7 + + assert.are_equal(motion.ground_motion_result( + vector(4, 7), + nil, + true, + true + ), + pc:_compute_ground_motion_result() + ) + end) + + it('(ceiling, vector(3, 4) at speed -3 (moving right)) should return vector(4, 6), slope_angle: nil, is_blocked: false, is_falling: true', function () + pc.position = vector(3, 4) + pc.ground_speed = -3 + pc.quadrant = directions.up + pc.slope_angle = 0.5 + + -- 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 + + assert.are_equal(motion.ground_motion_result( + vector(3, 6), + nil, + false, + true + ), + pc:_compute_ground_motion_result() + ) + end) + + it('(ceiling, vector(3, 4) at speed -5 (moving right)) should return vector(7, 4), slope_angle: nil, is_blocked: true, is_falling: true', function () + pc.position = vector(3, 4) + pc.ground_speed = -5 + pc.quadrant = directions.up + pc.slope_angle = 0.5 + + -- we assume _compute_max_pixel_distance is correct, so it should return 3 + -- we are falling then blocked on 7 + + assert.are_equal(motion.ground_motion_result( + vector(3, 7), + nil, + true, + true + ), + pc:_compute_ground_motion_result() + ) + end) + + it('(left wall, vector(4, 3) at speed 3 (moving down)) should return vector(4, 6), slope_angle: nil, is_blocked: false, is_falling: true', function () + pc.position = vector(4, 3) + pc.ground_speed = 3 + pc.quadrant = directions.left + pc.slope_angle = 0.75 + + -- 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.are_equal(motion.ground_motion_result( + vector(4, 6), + nil, + false, + true + ), + pc:_compute_ground_motion_result() + ) + end) + + it('(left wall, vector(4, 3) at speed 5 (moving down)) should return vector(7, 4), slope_angle: nil, is_blocked: true, is_falling: true', function () + pc.position = vector(4, 3) + pc.ground_speed = 5 + pc.quadrant = directions.left + pc.slope_angle = 0.75 + + -- we assume _compute_max_pixel_distance is correct, so it should return 3 + -- we are falling then blocked on 7 + + assert.are_equal(motion.ground_motion_result( + vector(4, 7), + nil, + true, + true + ), + pc:_compute_ground_motion_result() + ) + end) + end) end) -- _compute_ground_motion_result From 47e779690bdff660bcc16080c95db4db55f9c821 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 16:46:49 +0200 Subject: [PATCH 081/112] [PLAYER CHARACTER] Completed quadrant conversion for _compute_signed_distance_to_closest_ground and _is_column_blocked_by_ceiling_at quadrant_rotated is now optimized/precise for busted Moved quadrant helper functions to world Design change: only check ceiling from 5px above sensor Misc fixes, including design tweaking in edge cases --- src/data/tile_collision_data.lua | 41 +- src/data/tile_collision_data_utest.lua | 90 +++- src/ingame/playercharacter.lua | 261 ++++++----- src/ingame/playercharacter_utest.lua | 575 +++++++++++++++---------- src/platformer/world.lua | 178 +++++++- src/platformer/world_utest.lua | 309 ++++++++++++- src/test_data/tile_test_data.lua | 35 +- 7 files changed, 1092 insertions(+), 397 deletions(-) diff --git a/src/data/tile_collision_data.lua b/src/data/tile_collision_data.lua index 42845f3b..6997a442 100644 --- a/src/data/tile_collision_data.lua +++ b/src/data/tile_collision_data.lua @@ -45,12 +45,49 @@ function tile_collision_data:get_width(row_index0) return self.width_array[row_index0 + 1] -- adapt 0-index to 1-index end +-- helper function: return true iff array only contains 0 or 8 +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 + for v in all(array) do + if v ~= 0 and v ~= 8 then + return false + end + end + + return true +end + +-- return true iff tile is made of empty/full columns only +-- (height array only contains 0 or 8) +-- in practice, those columns should be contiguous (else the row widths cannot be defined) +-- and the tile is a rectangle of height 8 +function tile_collision_data:is_full_vertical_rectangle() + return is_full_or_empty(self.height_array) +end + +-- return true iff tile is made of empty/full columns only +-- (height array only contains 0 or 8) +-- in practice, those columns should be contiguous (else the row widths cannot be defined) +-- and the tile is a rectangle of height 8 +function tile_collision_data:is_full_horizontal_rectangle() + return is_full_or_empty(self.width_array) +end + +-- return tuple (interior_v, interior_h) for a slope angle +function tile_collision_data.slope_angle_to_interiors(slope_angle) + local interior_v = (slope_angle < 0.25 or slope_angle > 0.75) 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(raw_data) assert(raw_data.slope_angle >= 0 and raw_data.slope_angle < 1, "tile_collision_data.from_raw_tile_collision_data: raw_data.slope_angle is "..raw_data.slope_angle..", 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 - local interior_v = raw_data.slope_angle < 0.5 and horizontal_dirs.right or horizontal_dirs.left - local interior_h = (raw_data.slope_angle < 0.25 or raw_data.slope_angle > 0.75) and vertical_dirs.down or vertical_dirs.up + local interior_v, interior_h = tile_collision_data.slope_angle_to_interiors(raw_data.slope_angle) return tile_collision_data( tile_collision_data.read_height_array(raw_data.mask_tile_id_loc, interior_v), diff --git a/src/data/tile_collision_data_utest.lua b/src/data/tile_collision_data_utest.lua index 57eeda57..6c645797 100644 --- a/src/data/tile_collision_data_utest.lua +++ b/src/data/tile_collision_data_utest.lua @@ -29,6 +29,17 @@ local raw_tile_collision_data = require("data/raw_tile_collision_data") -- #....... -- #....... +-- mask tile 3: top-right concave ceiling (to check asymmetry) +-- pixel representation: +-- ######## +-- ..###### +-- ....#### +-- .....### +-- ......## +-- ......## +-- .......# +-- .......# + describe('tile_collision_data', function () describe('_init', function () @@ -58,19 +69,85 @@ describe('tile_collision_data', function () end) + describe('is_full_vertical_rectangle', function () + + -- just skip defining width array, we don't use it + -- if you want to now what it should be normally, + -- 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({0, 0, 0, 0, 0, 0, 0, 0}, {}, 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({8, 8, 8, 0, 0, 0, 0, 0}, {}, 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({4, 4, 4, 4, 3, 3, 3, 3}, {}, atan2(8, -1)) + assert.is_false(tcd:is_full_vertical_rectangle()) + end) + + end) + + describe('is_full_horizontal_rectangle', function () + + it('should return true when empty', function () + local tcd = tile_collision_data({}, {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({}, {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({}, {0, 0, 0, 0, 4, 8, 8, 8}, atan2(8, -1)) + assert.is_false(tcd:is_full_horizontal_rectangle()) + end) + + end) + + describe('slope_angle_to_interiors', function () + + it('should return a down, right for bottom-right tile', function () + assert.are_same({vertical_dirs.down, horizontal_dirs.right}, {tile_collision_data.slope_angle_to_interiors(atan2(8, -4))}) + end) + + it('should return a tile_collision_data containing (mock tile 2) height/width array, slope angle, derived interior directions', function () + assert.are_same({vertical_dirs.up, horizontal_dirs.left}, {tile_collision_data.slope_angle_to_interiors(atan2(-8, 8))}) + end) + + it('should return a tile_collision_data containing (mock tile 3) height/width array, slope angle, derived interior directions', function () + assert.are_same({vertical_dirs.up, horizontal_dirs.right}, {tile_collision_data.slope_angle_to_interiors(atan2(-8, -8))}) + end) + + end) + describe('from_raw_tile_collision_data', function () + -- we wrote these utests before extracting slope_angle_to_interiors + -- so we don't stub slope_angle_to_interiors and check final result directly + setup(function () stub(tile_collision_data, "read_height_array", function (tile_mask_id_location, slope_angle) if tile_mask_id_location == 1 then return {0, 1, 2, 2, 3, 3, 4, 4} - else + elseif tile_mask_id_location == 2 then return {8, 6, 4, 3, 2, 2, 1, 1} + else + return {1, 1, 2, 2, 3, 4, 6, 8} end end) stub(tile_collision_data, "read_width_array", function (tile_mask_id_location, slope_angle) if tile_mask_id_location == 1 then return {0, 0, 0, 0, 2, 4, 6, 7} + elseif tile_mask_id_location == 2 then + return {8, 6, 4, 3, 2, 2, 1, 1} else return {8, 6, 4, 3, 2, 2, 1, 1} end @@ -86,14 +163,19 @@ describe('tile_collision_data', function () local raw_data = raw_tile_collision_data(1, atan2(8, -4)) local tcd = tile_collision_data.from_raw_tile_collision_data(raw_data) -- 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({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), tcd) + assert.are_same(tile_collision_data({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) end) it('should return a tile_collision_data containing (mock tile 2) height/width array, slope angle, derived interior directions', function () local raw_data = raw_tile_collision_data(2, atan2(-8, 8)) local tcd = tile_collision_data.from_raw_tile_collision_data(raw_data) - -- 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({8, 6, 4, 3, 2, 2, 1, 1}, {8, 6, 4, 3, 2, 2, 1, 1}, atan2(-8, 8), horizontal_dirs.left, vertical_dirs.up), tcd) + assert.are_same(tile_collision_data({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) + end) + + it('should return a tile_collision_data containing (mock tile 3) height/width array, slope angle, derived interior directions', function () + local raw_data = raw_tile_collision_data(2, atan2(-8, -8)) + local tcd = tile_collision_data.from_raw_tile_collision_data(raw_data) + assert.are_same(tile_collision_data({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) end) end) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 1236be1b..77135d95 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -126,12 +126,9 @@ function player_char:get_full_height() end -- return quadrant tangent right angle (not quadrant interior direction angle!) --- convenient to use since it is 0 when quadrant is down +-- down -> 0, right -> 0.25, up -> 0.5, left -> 0.75 function player_char:get_quadrant_right_angle() - -- a math trick to transform direction enum value to angle - -- (down being 0, up 0.25, etc.) - -- make sure not to change directions enum values order! - return 0.25 * (3-self.quadrant) % 4 + return world.quadrant_to_right_angle(self.quadrant) end -- return slope angle, relative to quadrant tangent right @@ -152,71 +149,24 @@ end -- return copy of vector rotated by quadrant right angle -- this is a forward transformation, and therefore useful for intention (ground motion) function player_char:quadrant_rotated(v) +--[[#pico8 return v:rotated(self:get_quadrant_right_angle()) -end - --- return the horizontal coordinate of a vector in current quadrant --- (x if down/up, y if right/left) --- make sure to always extract quadrant coordinates after doing operations --- with quadrant-rotated vectors, to always have matching contribution signs -function player_char:get_quadrant_x_coord(pos) - -- directions value-dependent trick: left and right are 0 and 2 (even) - -- whereas up and down are 1 and 3 (odd), so check for parity - return self.quadrant % 2 == 0 and pos.y or pos.x -end - --- same, but for qy -function player_char:get_quadrant_y_coord(pos) - return self.quadrant % 2 == 1 and pos.y or pos.x -end - --- same, but for qj (tilemap location) -function player_char:get_quadrant_j_coord(pos) - return self.quadrant % 2 == 1 and pos.j or pos.i -end - --- return the "x" coordinate in current quadrant, i.e. the coordinate --- on the quadrant horizontal axis -function player_char:get_quadrant_x() - return self:get_quadrant_x_coord(self.position) -end - --- set the horizontal coordinate of a position vector in current quadrant --- (x if down/up, y if right/left) to value -function player_char:set_position_quadrant_x(pos, value) - -- directions value-dependent trick: left and right are 0 and 2 (even) - -- whereas up and down are 1 and 3 (odd), so check for parity - if self.quadrant % 2 == 0 then - pos.y = value - else - pos.x = value - end -end - --- return the qy of the q-bottom edge of a tile --- e.g. left edge x if quadrant is left, bottom edge y if quadrant is down -function player_char:get_tile_qbottom(tile_loc) - -- to avoid if/elseif and handle everything in one formula: - -- - start from tile center - -- - move in quadrant (down) direction by a half-tile - -- - get qy - -- tile_size / 2 = 4 - return self:get_quadrant_y_coord(tile_loc:to_center_position() + 4 * self:get_quadrant_down()) -end - --- return the difference from qy1 to qy2, --- but apply sign change to respect q-up, --- i.e. if qy1 represents a value higher in quadrant frame than qy2, --- result should be positive -function player_char:sub_qy(qy1, qy2) - -- directions value-dependent trick: up and left are 0 and 1 (< 2) - -- and only those have a reversed qy - -- quadrant down has the normal operation, as usual - if self.quadrant < 2 then - return qy2 - qy1 - else - return qy1 - qy2 +--#pico8]] +--#if busted + -- native Lua's floating point numbers cause small precision errors with cos/sin + -- so prefer perfect quadrant rotations (PICO-8 could also use this, but requires more tokens) + -- when testing, make sure to temporarily uncomment the pico8 block above + -- and comment the busted block below, so you can confirm that the pico8 version is valid too + if self.quadrant == directions.down then + return v:copy() + elseif self.quadrant == directions.right then + return v:rotated_90_ccw() + elseif self.quadrant == directions.up then + return -v + else -- self.quadrant == directions.left + return v:rotated_90_cw() end +--#end end -- spawn character at given position, detecting ground/air on arrival @@ -270,19 +220,7 @@ function player_char:set_slope_angle_with_quadrant(angle) self.slope_angle = angle - -- priority to horizontal quadrants at the boundaries - -- (just so 45-deg slope is recognized as left/right and character can get control-locked - -- to mimic hysteresis motion when trying to walk up slide in Hydrocity Zone) - -- 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 - self.quadrant = directions.down - elseif angle <= 0.375 then - self.quadrant = directions.right - elseif angle < 0.625 then - self.quadrant = directions.up - else -- angle <= 0.875 - self.quadrant = directions.left - end + self.quadrant = world.angle_to_quadrant(angle) end function player_char:update() @@ -434,9 +372,13 @@ function player_char:_get_ground_sensor_position_from(center_position, quadrant_ -- from character center, move down by center height to get the character bottom center local qx_floored_bottom_center = vector(x, y) + self:get_center_height() * self:get_quadrant_down() + -- using a ground_sensor_extent_x in .5 and flooring +/- this value allows us to get the checked column x (the x corresponds to the left of that column) - -- rotate proper vector (initially horizontal) for quadrant compatibility - local offset_qx_vector = self:quadrant_rotated(vector(flr(horizontal_dir_signs[quadrant_horizontal_dir] * pc_data.ground_sensor_extent_x), 0)) + -- 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(vector(horizontal_dir_signs[quadrant_horizontal_dir] * pc_data.ground_sensor_extent_x, 0)) + -- brutal way to floor coordinates are 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 @@ -453,7 +395,7 @@ end -- the test should be tile-insensitive so it is possible to detect q-step up/down in vertical-neighboring tiles function player_char:_compute_signed_distance_to_closest_ground(sensor_position) - assert(self:get_quadrant_x_coord(sensor_position) % 1 == 0, "player_char:_compute_signed_distance_to_closest_ground: sensor_position.x must be floored") + assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_compute_signed_distance_to_closest_ground: sensor_position qx must be floored") -- we used to flr sensor_position.y (would now be qy) at this point, -- but actually collision checks don't mind the fractions @@ -464,20 +406,21 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- from sensor + offset position (in qy) -- we are effectively finding the tiles covered (even partially) by the q-vertical segment between the edge positions -- where the character can snap up (escape) and snap down - local snap_zone_qtop = sensor_position + self:quadrant_rotated(vector(0, - pc_data.max_ground_escape_height)) + local snap_zone_qtop = sensor_position + self:quadrant_rotated(vector(0, - pc_data.max_ground_escape_height - 1)) local snap_zone_qbottom = sensor_position + self:quadrant_rotated(vector(0, pc_data.max_ground_snap_height)) -- start at the top, remember the bottom to end the iteration local curr_tile_loc = snap_zone_qtop:to_location() - -- for last tile we only care about qj so just store that - local last_tile_qj = self:get_quadrant_j_coord(snap_zone_qbottom:to_location()) - local sensor_location_topleft = curr_tile_loc:to_topleft_position() + + -- last tile to iterate on (at q-bottom) + local last_tile_loc = snap_zone_qbottom:to_location() + -- we *always* iterate on columns from left to right, rows from top to bottom, -- and columns/rows are stored exactly like that in collision data (not CCW or anything) -- so unlike other operations, the subtraction from topleft (combined with qx coord) is correct -- to get column index for qcolumn height later, without the need to quadrant-rotate vectors first - local qcolumn_index0 = self:get_quadrant_x_coord(sensor_position - sensor_location_topleft) -- from 0 to tile_size - 1 + local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - sensor_location_topleft, self.quadrant) -- from 0 to tile_size - 1 -- we iterate on tiles along quadrant down, so just convert it to tile_vector -- to allow step addition @@ -488,12 +431,12 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- or we've reached the last tile (too low to snap down) while true do - local curr_tile_j = self:get_quadrant_j_coord(curr_tile_loc) + local curr_tile_j = world.get_quadrant_j_coord(curr_tile_loc, self.quadrant) -- get q-bottom of tile to compare heights easily later - local current_tile_qbottom = self:get_tile_qbottom(curr_tile_loc) + local current_tile_qbottom = world.get_tile_qbottom(curr_tile_loc, self.quadrant) - -- check for ground (by column) in currently checked tile, sensor X + -- check for ground (by q-column) in currently checked tile, sensor qX local qcolumn_height, slope_angle = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0, self.quadrant) -- a q-column height of 0 doesn't mean that there is ground just below relative offset qy = 0, @@ -506,7 +449,7 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- 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 column - local signed_distance_to_closest_ground = self:sub_qy(current_tile_qbottom, sensor_position.y) - qcolumn_height + local signed_distance_to_closest_ground = world.sub_qy(current_tile_qbottom, world.get_quadrant_y_coord(sensor_position, self.quadrant), self.quadrant) - qcolumn_height if signed_distance_to_closest_ground < -pc_data.max_ground_escape_height then -- ground found, but character is too deep inside to snap q-up -- return edge case (-pc_data.max_ground_escape_height - 1, 0) @@ -522,10 +465,10 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- to snap q-down. This can only happen on the last tile we iterate on -- (since it was computed to be at the snap q-down limit), -- which means we will enter the "end of iteration" block below - assert(curr_tile_j == last_tile_qj) + assert(curr_tile_loc == last_tile_loc) end - if curr_tile_j == last_tile_qj then + if curr_tile_loc == last_tile_loc then -- 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) @@ -857,7 +800,7 @@ function player_char:_compute_ground_motion_result() false ) - local qx = self:get_quadrant_x() + local qx = world.get_quadrant_x_coord(self.position, self.quadrant) -- only full pixels matter for collisions, but subpixels (of last position + delta motion) -- may sum up to a full pixel, @@ -870,7 +813,7 @@ function player_char:_compute_ground_motion_result() -- tokens: if get_quadrant_right_angle is not used elsewhere, consider the version below -- which is more lengthy but doesn't require get_quadrant_right_angle definition so ultimately more compact -- 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 = self:get_quadrant_x_coord(ground_velocity_projected_on_quadrant_right) + local projected_velocity_qx = world.get_quadrant_x_coord(ground_velocity_projected_on_quadrant_right, self.quadrant) -- max_distance_qx is always integer local max_distance_qx = player_char._compute_max_pixel_distance(qx, projected_velocity_qx) @@ -889,7 +832,7 @@ function player_char:_compute_ground_motion_result() 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 > self:get_quadrant_x_coord(motion_result.position) + local are_subpixels_left = qx + projected_velocity_qx > world.get_quadrant_x_coord(motion_result.position, self.quadrant) if are_subpixels_left then -- character has not been blocked and has some subpixels left to go @@ -918,7 +861,7 @@ function player_char:_compute_ground_motion_result() -- 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 - self:set_position_quadrant_x(motion_result.position, qx + projected_velocity_qx) + world.set_position_quadrant_x(motion_result.position, qx + projected_velocity_qx, self.quadrant) end end end @@ -944,20 +887,28 @@ end -- 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), "trace") + -- 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 + log("step_vec: "..step_vec, "trace") + log("next_position_candidate: "..next_position_candidate, "trace") + -- check if next position is inside/above ground local query_info = self:_compute_ground_sensors_signed_distance(next_position_candidate) local signed_distance_to_closest_ground, next_slope_angle = query_info.signed_distance, query_info.slope_angle + log("signed_distance_to_closest_ground: "..signed_distance_to_closest_ground, "trace") + -- 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, -- 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 @@ -966,7 +917,7 @@ function player_char:_next_ground_step(quadrant_horizontal_dir, ref_motion_resul -- 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 if - signed_distance_to_closest_ground <= pc_data.max_ground_escape_height then - -- step up + -- 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) @@ -1054,50 +1005,81 @@ end -- so the step up itself will be ignored (e.g. when moving from a flat ground to an ascending slope) function player_char:_is_column_blocked_by_ceiling_at(sensor_position) - assert(flr(sensor_position.x) == sensor_position.x, "player_char:_is_column_blocked_by_ceiling_at: sensor_position.x must be floored") - - -- find the tile where this sensor is located - local curr_tile_loc = sensor_position:to_location() + -- a good part of the code is similar to _compute_signed_distance_to_closest_ground + -- maybe not enough to factorize, but check its comments to understand better how we + -- iterate over tiles + + assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_is_column_blocked_by_ceiling_at: sensor_position qx must be floored") + + -- 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) + local ceiling_detection_zone_qtop = sensor_position + self:quadrant_rotated(vector(0, - self:get_full_height())) + -- 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_signed_distance_to_closest_ground) + local ceiling_detection_zone_qbottom = sensor_position + self:quadrant_rotated(vector(0, - pc_data.max_ground_escape_height - 1)) + + -- define sensor tile location and tile iteration bounds + local sensor_tile_loc = sensor_position:to_location() + local curr_tile_loc = ceiling_detection_zone_qbottom:to_location() + local start_tile_loc = curr_tile_loc:copy() local sensor_location_topleft = curr_tile_loc:to_topleft_position() - local column_index0 = sensor_position.x - sensor_location_topleft.x -- from 0 to tile_size - 1 + -- last tile to iterate on (at q-top) + local last_tile_loc = ceiling_detection_zone_qtop:to_location() + + local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - sensor_location_topleft, self.quadrant) -- from 0 to tile_size - 1 + + -- we iterate on tiles along quadrant up, so just convert it to tile_vector + -- to allow step addition + local quadrant_opp = oppose_dir(self.quadrant) + local quadrant_up = dir_vectors[quadrant_opp] + -- since we got quadrant down, we must oppose; + -- if you don't like the 2 extra tokens, just iterate from top to bottom instead + local tile_loc_step = tile_vector(quadrant_up.x, quadrant_up.y) + + -- iterate from bottom (Sonic feet) to top (Sonic head) while true do - -- TODO QUADRANT - -- move 1 tile up from the start, as we can only hit ceiling from a tile above with non-rotated tiles - -- note: when we add rotated tiles, we will need to handle ceiling tiles (tiles rotated by 180) - -- starting from the current tile, because unlike ground tiles, they may actually block - -- the character's head despite being in his current tile location - -- so we'll need to move the decrement statement to the end of the loop and add a tile rotation check - -- in addition we'll need to _compute_column_bottom_height_at() to handle variable ceiling height along a tile - -- rather than just checking if _compute_column_height_at() > 0 - -- to avoid tile rotation check, we can also check if _compute_column_bottom_height_at() is lower than the feet (so we can ignore it) - -- (90 and 270-rotated tiles will be ignored as they are not supposed to block the character's head) - curr_tile_loc.j = curr_tile_loc.j - 1 - local current_tile_top = curr_tile_loc:to_topleft_position().y - local current_tile_bottom = current_tile_top + tile_size - - -- if the bottom of next ceiling to check is already higher than, or equal to - -- one character height, if cannot block him, so return false - local height_distance = sensor_position.y - current_tile_bottom - if height_distance >= self:get_full_height() then - return false + -- on the first tile, we don't cannot really be blocked by a ground + -- with the same interior direction as quadrant <=> opposite to quadrant_opp + -- (imagine Sonic standing on a half-tile; this definitely cannot be ceiling) + -- so we do not consider the reverse collision with full tile_size q-height with them + -- if you're unsure, try to force-set this to false and you'll see utests like + -- '(1 ascending slope 45) should return false for sensor position on the left of the tile' + -- failing + local ignore_reverse = start_tile_loc == curr_tile_loc + + -- get q-top of tile to compare heights easily later + -- there is no get_tile_qtop, so use quadrant_opp with get_tile_qbottom + local current_tile_qtop = world.get_tile_qbottom(curr_tile_loc, quadrant_opp) + + -- check for ceiling (by q-column) in currently checked tile, sensor qX + -- make sure to mirror Y of quadrant to get q-column height seen from q-above + -- we don't need slope_angle, commented out for token reduction + local qcolumn_height--[[, _slope_angle]] = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0, quadrant_opp, ignore_reverse) + + if qcolumn_height > 0 then + local head_top_position = sensor_position + vector(0, -self:get_full_height()) + local signed_distance_to_closest_ceiling = world.sub_qy(current_tile_qtop, world.get_quadrant_y_coord(head_top_position, self.quadrant), quadrant_opp) - qcolumn_height + if signed_distance_to_closest_ceiling < 0 then + -- head (or feet) inside ceiling + return true + else + -- head far touching ceiling or has some gap from ceiling + return false + end end - -- TODO: pass self.quadrant and adapt coordinates around for quadrant - local ground_array_height, _ = world._compute_qcolumn_height_at(curr_tile_loc, column_index0, directions.down) - if ground_array_height ~= nil and ground_array_height > 0 then - -- with non-rotated tiles, we are sure to hit the ceiling at this point - -- because ceiling is always at a tile bottom, and we return false - -- as soon as we go up farther than a character's height - return true - -- with ceiling tiles, we will need to check if the ceiling column height - -- hits the head or not. if it doesn't stop here, return false, - -- the head is below the ceiling: - -- local height_distance = sensor_position.y - current_tile_bottom - -- return height_distance < self:get_full_height() + if curr_tile_loc == last_tile_loc then + -- end of iteration, and no ceiling found + return false end + curr_tile_loc = curr_tile_loc + tile_loc_step + end end @@ -1431,6 +1413,13 @@ function player_char:_next_air_step(direction, ref_motion_result) -- 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 extactly 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 if not ref_motion_result.is_blocked_by_wall and (self.velocity.y < 0 or abs(self.velocity.x) > abs(self.velocity.y)) then local is_blocked_by_ceiling_at_next = self:_is_blocked_by_ceiling_at(next_position_candidate) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index fbd00b76..6aaf8539 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -252,6 +252,9 @@ describe('player_char', function () describe('get_quadrant_right_angle', function () + -- we had already written utests before extracting world.quadrant_to_right_angle + -- so we kept the tests checking final result instead of spy call + it('should return 0 when quadrant is down', function () pc.quadrant = directions.down assert.are_equal(0, pc:get_quadrant_right_angle()) @@ -369,6 +372,9 @@ describe('player_char', function () assert.are_same(vector(1, -2), pc:quadrant_rotated(vector(1, -2))) end) + -- busted implementation is exact and should pass without almost_eq, + -- but they are useful when testing the pico8 implementation + it('should return vector rotated by 0.25 when quadrant is right', function () pc.quadrant = directions.right assert.is_true(almost_eq_with_message(vector(-2, -1), pc:quadrant_rotated(vector(1, -2)))) @@ -386,194 +392,6 @@ describe('player_char', function () end) - describe('get_quadrant_x_coord', function () - - it('should return pos.x when quadrant is down', function () - pc.quadrant = directions.down - assert.are_equal(10, pc:get_quadrant_x_coord(vector(10, 20))) - end) - - it('should return pos.x when quadrant is up', function () - pc.quadrant = directions.up - assert.are_equal(10, pc:get_quadrant_x_coord(vector(10, 20))) - end) - - it('should return pos.y when quadrant is right', function () - pc.quadrant = directions.right - assert.are_equal(20, pc:get_quadrant_x_coord(vector(10, 20))) - end) - - it('should return pos.y when quadrant is left', function () - pc.quadrant = directions.left - assert.are_equal(20, pc:get_quadrant_x_coord(vector(10, 20))) - end) - - end) - - describe('get_quadrant_y_coord', function () - - it('should return pos.y when quadrant is down', function () - pc.quadrant = directions.down - assert.are_equal(20, pc:get_quadrant_y_coord(vector(10, 20))) - end) - - it('should return pos.y when quadrant is up', function () - pc.quadrant = directions.up - assert.are_equal(20, pc:get_quadrant_y_coord(vector(10, 20))) - end) - - it('should return pos.y when quadrant is right', function () - pc.quadrant = directions.right - assert.are_equal(10, pc:get_quadrant_y_coord(vector(10, 20))) - end) - - it('should return pos.y when quadrant is left', function () - pc.quadrant = directions.left - assert.are_equal(10, pc:get_quadrant_y_coord(vector(10, 20))) - end) - - end) - - describe('get_quadrant_j_coord', function () - - it('should return loc.j when quadrant is down', function () - pc.quadrant = directions.down - assert.are_equal(2, pc:get_quadrant_j_coord(location(1, 2))) - end) - - it('should return loc.j when quadrant is up', function () - pc.quadrant = directions.up - assert.are_equal(2, pc:get_quadrant_j_coord(location(1, 2))) - end) - - it('should return loc.j when quadrant is right', function () - pc.quadrant = directions.right - assert.are_equal(1, pc:get_quadrant_j_coord(location(1, 2))) - end) - - it('should return loc.j when quadrant is left', function () - pc.quadrant = directions.left - assert.are_equal(1, pc:get_quadrant_j_coord(location(1, 2))) - end) - - end) - - describe('get_quadrant_x', function () - - -- we already wrote tests for this method before extracting get_quadrant_x_coord, - -- so we decided not to rewrite them with stub for get_quadrant_x_coord and keep checking - -- final result - - it('should return self.position.x when quadrant is down', function () - pc.quadrant = directions.down - pc.position.x = 10 - pc.position.y = 20 - assert.are_equal(10, pc:get_quadrant_x()) - end) - - it('should return self.position.x when quadrant is up', function () - pc.quadrant = directions.up - pc.position.x = 10 - pc.position.y = 20 - assert.are_equal(10, pc:get_quadrant_x()) - end) - - it('should return self.position.y when quadrant is right', function () - pc.quadrant = directions.right - pc.position.x = 10 - pc.position.y = 20 - assert.are_equal(20, pc:get_quadrant_x()) - end) - - it('should return self.position.y when quadrant is left', function () - pc.quadrant = directions.left - pc.position.x = 10 - pc.position.y = 20 - assert.are_equal(20, pc:get_quadrant_x()) - end) - - end) - - describe('set_position_quadrant_x', function () - - it('should set pos.x when quadrant is down', function () - pc.quadrant = directions.down - local p = vector(10, 20) - pc:set_position_quadrant_x(p, 30) - assert.are_same(vector(30, 20), p) - end) - - it('should set pos.x when quadrant is up', function () - pc.quadrant = directions.up - local p = vector(10, 20) - pc:set_position_quadrant_x(p, 30) - assert.are_same(vector(30, 20), p) - end) - - it('should set pos.y when quadrant is right', function () - pc.quadrant = directions.right - local p = vector(10, 20) - pc:set_position_quadrant_x(p, 30) - assert.are_same(vector(10, 30), p) - end) - - it('should set pos.y when quadrant is left', function () - pc.quadrant = directions.left - local p = vector(10, 20) - pc:set_position_quadrant_x(p, 30) - assert.are_same(vector(10, 30), p) - end) - - end) - - describe('get_tile_qbottom', function () - - it('should return tile world bottom when quadrant is down', function () - pc.quadrant = directions.down - assert.are_equal(24, pc:get_tile_qbottom(location(1, 2))) - end) - - it('should return tile world top when quadrant is up', function () - pc.quadrant = directions.up - assert.are_equal(16, pc:get_tile_qbottom(location(1, 2))) - end) - - it('should return world right when quadrant is right', function () - pc.quadrant = directions.right - assert.are_equal(16, pc:get_tile_qbottom(location(1, 2))) - end) - - it('should return world left when quadrant is left', function () - pc.quadrant = directions.left - assert.are_equal(8, pc:get_tile_qbottom(location(1, 2))) - end) - - end) - - describe('sub_qy', function () - - it('should return qy1 - qy2 when quadrant is down', function () - pc.quadrant = directions.down - assert.are_equal(7, pc:sub_qy(10, 3)) - end) - - it('should return qy2 - qy1 when quadrant is up', function () - pc.quadrant = directions.up - assert.are_equal(7, pc:sub_qy(3, 10)) - end) - - it('should return qy1 - qy2 when quadrant is right', function () - pc.quadrant = directions.right - assert.are_equal(7, pc:sub_qy(10, 3)) - end) - - it('should return qy2 - qy1 when quadrant is left', function () - pc.quadrant = directions.left - assert.are_equal(7, pc:sub_qy(3, 10)) - end) - - end) - describe('spawn_at', function () setup(function () @@ -1300,6 +1118,38 @@ describe('player_char', function () assert.are_equal(vector(12, 10 + 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.right)) end) + -- for other quadrants, just check the more complex case of coords with fractions + + it('(right wall) should return the position q-down-left of the x-floored character center when horizontal dir is left', function () + pc.quadrant = directions.right + assert.are_equal(vector(10 + 11, 12), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.left)) + end) + + it('(right wall) should return the position q-down-left of the x-floored character center when horizontal dir is right', function () + pc.quadrant = directions.right + assert.are_equal(vector(10 + 11, 7), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.right)) + end) + + it('(ceiling) should return the position q-down-left of the x-floored character center when horizontal dir is left', function () + pc.quadrant = directions.up + assert.are_equal(vector(12, 10 - 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.left)) + end) + + it('(ceiling) should return the position q-down-left of the x-floored character center when horizontal dir is right', function () + pc.quadrant = directions.up + assert.are_equal(vector(7, 10 - 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.right)) + end) + + it('(left wall) should return the position q-down-left of the x-floored character center when horizontal dir is left', function () + pc.quadrant = directions.left + assert.are_equal(vector(10 - 11, 7), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.left)) + end) + + it('(left wall) should return the position q-down-left of the x-floored character center when horizontal dir is right', function () + pc.quadrant = directions.left + assert.are_equal(vector(10 -11, 12), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.right)) + end) + end) describe('_compute_signed_distance_to_closest_ground', function () @@ -1375,16 +1225,13 @@ 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(-max_ground_escape_height - 1, 0) (clamped) if max_ground_escape_height - 1 below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) + it('should return ground_query_info(- max_ground_escape_height - 1, 0) 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_equal(ground_query_info(- pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) end) - it('should return ground_query_info(max_ground_snap_height + 1, nil) if max_ground_escape_height below the bottom', function () - -- this used to be (-pc_data.max_ground_escape_height - 1, 0) but when I switched the algo to a tile-based iteration - -- instead of pixel-based iteration for performance, I noticed there was no need to check for the pixel/tile at sensor_position.y - pc_data.max_ground_escape_height - 1, - -- just sensor_position.y - pc_data.max_ground_escape_height is enough (if there's really something as close at 5px from Sonic's bottom, - -- his head will hit ceiling anyway) - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) + it('should return ground_query_info(-max_ground_escape_height - 1, 0) (clamped) if max_ground_escape_height - 1 below the bottom', function () + assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) end) -- step up distance reached, character considered in the air @@ -1393,6 +1240,104 @@ describe('player_char', function () assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height + 1))) end) + -- other quadrants (only the trickiest cases) + + -- right wall + + it('(right wall) should return ground_query_info(max_ground_snap_height + 1, nil) if too far from the wall', function () + pc.quadrant = directions.right + + assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(0, 12))) + end) + + it('(right wall) should return ground_query_info(2, 0) if 2 pixels from the wall', function () + pc.quadrant = directions.right + + assert.are_equal(ground_query_info(2, 0.25), pc:_compute_signed_distance_to_closest_ground(vector(6, 12))) + end) + + it('(right wall) should return ground_query_info(-2, 0) if 2 pixels inside the wall', function () + pc.quadrant = directions.right + + assert.are_equal(ground_query_info(-2, 0.25), pc:_compute_signed_distance_to_closest_ground(vector(10, 12))) + end) + + it('(right wall) should return ground_query_info(-max_ground_escape_height - 1, 0) if too far inside the wall', function () + pc.quadrant = directions.right + + assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(14, 12))) + end) + + -- ceiling + + it('(ceiling) should return ground_query_info(max_ground_snap_height + 1, nil) if too far from the wall', function () + pc.quadrant = directions.up + + assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(12, 24))) + end) + + it('(ceiling) should return ground_query_info(2, 0) if 2 pixels from the wall', function () + pc.quadrant = directions.up + + assert.are_equal(ground_query_info(2, 0.5), pc:_compute_signed_distance_to_closest_ground(vector(12, 18))) + end) + + it('(ceiling) should return ground_query_info(-2, 0) if 2 pixels inside the wall', function () + pc.quadrant = directions.up + + assert.are_equal(ground_query_info(-2, 0.5), pc:_compute_signed_distance_to_closest_ground(vector(12, 14))) + end) + + it('(ceiling) should return ground_query_info(-max_ground_escape_height - 1, 0) if too far inside the wall', function () + pc.quadrant = directions.up + + assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8))) + end) + + -- left wall + + it('(left wall) should return ground_query_info(max_ground_snap_height + 1, nil) if too far from the wall', function () + pc.quadrant = directions.left + + assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(24, 12))) + end) + + it('(left wall) should return ground_query_info(2, 0) if 2 pixels from the wall', function () + pc.quadrant = directions.left + + assert.are_equal(ground_query_info(2, 0.75), pc:_compute_signed_distance_to_closest_ground(vector(18, 12))) + end) + + it('(left wall) should return ground_query_info(-2, 0) if 2 pixels inside the wall', function () + pc.quadrant = directions.left + + assert.are_equal(ground_query_info(-2, 0.75), pc:_compute_signed_distance_to_closest_ground(vector(14, 12))) + end) + + it('(left wall) should return ground_query_info(-max_ground_escape_height - 1, 0) if too far inside the wall', function () + pc.quadrant = directions.left + + assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(10, 12))) + end) + + end) + + describe('with 2 full flat tiles', function () + + before_each(function () + mock_mset(0, 0, full_tile_id) + mock_mset(0, 1, full_tile_id) + end) + + -- test below verifies that I check 1 extra px above max_ground_escape_height (see snap_zone_qtop definition) + -- even if it reaches another tile, so I don't think it's over and escape + -- 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(-max_ground_escape_height - 1, 0) if max_ground_escape_height + 1 inside, including max_ground_escape_height in current tile', function () + assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(4, 8 + pc_data.max_ground_escape_height))) + end) + end) describe('with half flat tile', function () @@ -1473,15 +1418,14 @@ describe('player_char', function () assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) end) - it('should return ground_query_info(max_ground_snap_height + 1, nil) if max_ground_escape_height below the bottom', function () - -- same remark as in equivalent test with full tile, used to give same result as test above, now give same result as test below - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) + it('should return ground_query_info(-max_ground_escape_height - 1, 0) if max_ground_escape_height below the bottom', function () + assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(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(max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 below the bottom', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height + 1))) + it('should return ground_query_info(max_ground_snap_height + 1, nil) if max_ground_snap_height + 1 below the bottom', function () + assert.are_equal(ground_query_info(pc_data.max_ground_escape_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height + 1))) end) end) @@ -1561,8 +1505,8 @@ describe('player_char', function () -- step up distance reached, character considered in the air - it('should return ground_query_info(max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 below the top of column 0', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height + 1))) + it('should return ground_query_info(-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_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height + 1))) end) end) @@ -1574,7 +1518,7 @@ describe('player_char', function () mock_mset(1, 1, desc_slope_45_id) end) - it('. should return 0.0625, 1-45/360 if right sensors are just a little above column 0', function () + it('should return 0.0625, 1-45/360 if right sensors are just a little above column 0', function () assert.are_equal(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 - 0.0625))) end) @@ -3424,12 +3368,8 @@ describe('player_char', function () 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]) - -- in native Lua, precision errors on cos/sin cause inexact step_vec, fix them now to allow ~= 0 check - if almost_eq(step_vec.x, 0, 1e-15) then - step_vec.x = 0 - end local original_position = motion_result.position - printh("step_vec: "..dump(step_vec)) + -- 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 @@ -3437,7 +3377,6 @@ describe('player_char', function () end if original_position.x >= 5 then if original_position.x < 7 then - printh("falling!") motion_result.is_falling = true motion_result.slope_angle = nil -- mimic actual implementation else @@ -3447,7 +3386,6 @@ describe('player_char', function () else -- moving on y (quadrant is left or right) if original_position.y < 7 then motion_result.position = original_position + step_vec - printh("motion_result: "..dump(motion_result)) motion_result.slope_angle = 0.25 end if original_position.y >= 5 then @@ -3546,7 +3484,7 @@ describe('player_char', function () -- we are falling but not blocked, so we continue running in the air until x=6 assert.are_equal(motion.ground_motion_result( - vector(3, 6), + vector(6, 4), nil, false, true @@ -3565,7 +3503,7 @@ describe('player_char', function () -- we are falling then blocked on 7 assert.are_equal(motion.ground_motion_result( - vector(3, 7), + vector(7, 4), nil, true, true @@ -3739,6 +3677,168 @@ describe('player_char', function () ) end) + -- for other quadrants we only test the most common cases + + 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 + + -- 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( + vector(0 - pc_data.center_height_standing, 12), + 0, + false, + false + ) + + -- step flat + pc:_next_ground_step(horizontal_dirs.right, motion_result) + + assert.are_equal(motion.ground_motion_result( + vector(0 - pc_data.center_height_standing, 11), + 0.25, + false, + false + ), + motion_result + ) + 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 + + -- 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( + vector(0 - pc_data.center_height_standing, 6), + 0, + false, + false + ) + + -- step fall + pc:_next_ground_step(horizontal_dirs.right, motion_result) + + assert.are_equal(motion.ground_motion_result( + vector(0 - pc_data.center_height_standing, 5), + nil, + false, + true + ), + motion_result + ) + end) + + -- FIXME + + 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 + + -- 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( + vector(-1, 16 + pc_data.center_height_standing), + 0, + false, + false + ) + + -- step flat + pc:_next_ground_step(horizontal_dirs.right, motion_result) + + assert.are_equal(motion.ground_motion_result( + vector(-2, 16 + pc_data.center_height_standing), + 0.5, + false, + false + ), + motion_result + ) + 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 + + -- 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( + vector(-2, 16 + pc_data.center_height_standing), + 0, + false, + false + ) + + -- step fall + pc:_next_ground_step(horizontal_dirs.right, motion_result) + + assert.are_equal(motion.ground_motion_result( + vector(-3, 16 + pc_data.center_height_standing), + nil, + false, + true + ), + motion_result + ) + end) + + 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 + + -- 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( + vector(8 + pc_data.center_height_standing, 15), + 0, + false, + false + ) + + -- step flat + pc:_next_ground_step(horizontal_dirs.right, motion_result) + + assert.are_equal(motion.ground_motion_result( + vector(8 + pc_data.center_height_standing, 16), + 0.75, + false, + false + ), + motion_result + ) + 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 + + -- 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( + vector(0 - pc_data.center_height_standing, 16), + 0, + false, + false + ) + + -- step fall + pc:_next_ground_step(horizontal_dirs.right, motion_result) + + assert.are_equal(motion.ground_motion_result( + vector(0 - pc_data.center_height_standing, 17), + nil, + false, + true + ), + motion_result + ) + end) + + -- TODO: more tests, but test lower-level _compute_ground_sensors_signed_distance first + end) describe('(with walls)', function () @@ -4050,7 +4150,7 @@ describe('player_char', function () setup(function () stub(player_char, "get_full_height", function () - return 11 + return 16 end) end) @@ -4073,9 +4173,9 @@ describe('player_char', function () mock_mset(1, 0, full_tile_id) -- full tile (act like a full ceiling if position is at bottom) end) - it('should return false for sensor position just above the bottom of the tile', function () - -- here, the current tile is the full tile, and we only check tiles above, so we detect nothing - assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(8, 7.9))) + it('should return true for sensor position just above the bottom of the tile', function () + -- with new implementation, we check tile even at foot level + assert.is_true(pc:_is_column_blocked_by_ceiling_at(vector(8, 7.9))) end) it('should return false for sensor position on the left of the tile', function () @@ -4100,7 +4200,7 @@ describe('player_char', function () end) it('should return true for sensor position below the tile, at character height - 1px', function () - assert.is_true(pc:_is_column_blocked_by_ceiling_at(vector(12, 8 + 11 - 1))) + assert.is_true(pc:_is_column_blocked_by_ceiling_at(vector(12, 8 + 16 - 1))) end) -- bugfix history: @@ -4109,7 +4209,36 @@ describe('player_char', function () -- the ground_array_height check (computing height_distance from tile bottom instead of top) -- to pass it in this case too it('should return false for sensor position below the tile, at character height', function () - assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(12, 8 + 11))) + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(12, 8 + 16))) + end) + + end) + + describe('(1 half-tile)', function () + + before_each(function () + -- = + mock_mset(0, 0, half_tile_id) + end) + + it('should return false 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_false(pc:_is_column_blocked_by_ceiling_at(vector(4, 6))) + end) + + it('should return true 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_false(pc:_is_column_blocked_by_ceiling_at(vector(4, 8))) + end) + + it('should return true 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_false(pc:_is_column_blocked_by_ceiling_at(vector(4, 10))) end) end) @@ -4122,16 +4251,12 @@ describe('player_char', function () end) it('should return false for sensor position on the left of the tile', function () - -- normally the character should step up and pass this position during the next-step pass - -- and this returns false so the character won't be blocked assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(0, 7))) end) it('should return true for sensor position at the bottom-left of the tile', function () - -- technically this is still a step up, but we consider it is the next-step method's fault - -- if it didn't step up correctly so we afford to return true and block the character, - -- as it makes more simple code - assert.is_true(pc:_is_column_blocked_by_ceiling_at(vector(0, 8))) + -- we now start checking ceiling a few pixels q-above character feet, so slope not detected as ceiling + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(0, 8))) end) end) diff --git a/src/platformer/world.lua b/src/platformer/world.lua index 4f662c62..4f7d4a98 100644 --- a/src/platformer/world.lua +++ b/src/platformer/world.lua @@ -3,11 +3,102 @@ local collision_data = require("data/collision_data") local world = {} +-- return quadrant in which angle is contained (non-injective) +function world.angle_to_quadrant(angle) + -- priority to horizontal quadrants at the boundaries + -- (just so 45-deg slope is recognized as left/right and character can get control-locked + -- to mimic hysteresis motion when trying to walk up slide in Hydrocity Zone) + -- looks like Classic Sonic gives priority to vertical quadrants (down, up) though, so + -- consider changing edge cases and use angle directly for lock calculation + -- 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 + -- 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 + elseif angle <= 0.375 then + return directions.right + elseif angle < 0.625 then + return directions.up + else -- angle <= 0.875 + return directions.left + end +end + +-- return quadrant tangent right angle +-- (not reciprocal of angle_to_quadrant since it is not injective) +-- down -> 0, right -> 0.25, up -> 0.5, left -> 0.75 +function world.quadrant_to_right_angle(quadrant) + -- a math trick to transform direction enum value to angle + -- make sure not to change directions enum values order! + return 0.25 * (3-quadrant) % 4 +end + +-- return the horizontal coordinate of a vector in given quadrant +-- (x if down/up, y if right/left) +-- make sure to always extract quadrant coordinates after doing operations +-- with quadrant-rotated vectors, to always have matching contribution signs +function world.get_quadrant_x_coord(pos, quadrant) + -- directions value-dependent trick: left and right are 0 and 2 (even) + -- whereas up and down are 1 and 3 (odd), so check for parity + return quadrant % 2 == 0 and pos.y or pos.x +end + +-- same, but for qy +function world.get_quadrant_y_coord(pos, quadrant) + return quadrant % 2 == 1 and pos.y or pos.x +end + +-- same, but for qj (tilemap location) +function world.get_quadrant_j_coord(pos, quadrant) + return quadrant % 2 == 1 and pos.j or pos.i +end + + +-- set the horizontal coordinate of a position vector in current quadrant +-- (x if down/up, y if right/left) to value +function world.set_position_quadrant_x(pos, value, quadrant) + -- directions value-dependent trick: left and right are 0 and 2 (even) + -- whereas up and down are 1 and 3 (odd), so check for parity + if quadrant % 2 == 0 then + pos.y = value + else + pos.x = value + end +end + +-- return the difference from qy1 to qy2, +-- but apply sign change to respect q-up, +-- i.e. if qy1 represents a value higher in quadrant frame than qy2, +-- result should be positive +function world.sub_qy(qy1, qy2, quadrant) + -- directions value-dependent trick: up and left are 0 and 1 (< 2) + -- and only those have a reversed qy + -- quadrant down has the normal operation, as usual + if quadrant < 2 then + return qy2 - qy1 + else + return qy1 - qy2 + end +end + +-- return the qy of the q-bottom edge of a tile for a given quadrant +-- e.g. left edge x if quadrant is left, bottom edge y if quadrant is down +function world.get_tile_qbottom(tile_loc, quadrant) + -- to avoid if/elseif and handle everything in one formula: + -- - start from tile center + -- - move in quadrant (down) direction by a half-tile + -- - get qy + -- tile_size / 2 = 4 + return world.get_quadrant_y_coord(tile_loc:to_center_position() + 4 * dir_vectors[quadrant], quadrant) +end + -- return (qcolumn_height, slope_angle) where: -- - qcolumn_height is the qcolumn height at tile_location on qcolumn_index0, or 0 if there is no colliding tile -- (if quadrant is horizontal, qcolum = row, but indices are always top to bottom, left to right) -- - slope_angle is the slope angle of the corresponding tile, or nil if there is no colliding tile -function world._compute_qcolumn_height_at(tile_location, qcolumn_index0, quadrant) +-- if ignore_reverse is true, return 0, nil if the tile interior is opposed to quadrant interior direction +-- this is useful for ceiling check on character's current tile and actually matches Classic Sonic behavior better +function world._compute_qcolumn_height_at(tile_location, qcolumn_index0, quadrant, ignore_reverse) -- only consider valid tiles; consider there are no colliding tiles outside the map area if tile_location.i >= 0 and tile_location.i < 128 and tile_location.j >= 0 and tile_location.j < 64 then @@ -23,11 +114,90 @@ function world._compute_qcolumn_height_at(tile_location, qcolumn_index0, quadran assert(tcd, "collision_data.tiles_collision_data does not contain entry for sprite id: "..current_tile_id..", yet it has the collision flag set") if tcd then - -- up (1) and down (3) are odd + -- if quadrant matches interior (h or v) direction, use q-column + -- (character is walking on the "normal" side of the tile) + -- if quadrant is opposed to interior (h or v) direction, use all-or-nothing + -- (character is walking on the "reverse" side of the tile, which is flat + -- on square edge (q-height = 8) except when q-column is completely empty) + -- in addition, the slope_angle is always the quadrant right angle (0, 0.25, 0.5, 0.75) + -- to simulate flat ground + -- full tiles must have an arbitrary angle multiple of 0.25, typically 0, and will be associated + -- to interiors based on angle_to_quadrant (one of the interiors will be arbitrarily chosen + -- since an angle edge case), then the algo will cover the reverse side cases, so 8, 0.25x will always be returned + -- ex: interior right-down, character quadrant left + -- ........ + -- ........ <- sensor here finds nothing (angle still 0.75) + -- ........ + -- ......## <- sensor here finds immediate ground as if row was ######## (and angle 0.75) + -- .....### + -- ....#### + -- ...##### + -- ...##### + + -- walking on the flat part of a tile on the normal side does *not* set + -- the slope to quadrant right angle + -- ........ + -- #....... + -- ###..... + -- #####... <- A + -- ######.. + -- ######## + -- ######## <- the inclined angle used at A will be considered + -- ######## + + -- however, if the tile is made *only* of full columns or full rows + -- (this includes the full tile), quadrant right angle is always used + -- (similarly to reverse sides, because we consider it's an edge case anyway) + -- ........ + -- ........ + -- ........ + -- ........ <- (0, 0.75) + -- ........ + -- ######## + -- ######## <- (8, 0.75) (flat side of rectangle) + -- ######## + + 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 + if quadrant % 2 == 1 then - return tcd:get_height(qcolumn_index0), tcd.slope_angle + -- floor/ceiling (quadrant down/up) + local height = tcd:get_height(qcolumn_index0) + if tcd.interior_v == vertical_dirs.down and quadrant == directions.up or + tcd.interior_v == vertical_dirs.up and quadrant == directions.down then + -- reverse side, all-or-nothing with right angle + -- unless we ignore reverse (still at sensor tile during ceiling check) + -- and the tile is not covering fully spanned vertically, + -- in which case reverse doesn't make sense as angle-to-quadrant conversion + -- is arbitrary on square angles, so the tile interior_v could be up or down + 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 + -- flat side of rectangle (or empty region near flat side) + return height, world.quadrant_to_right_angle(quadrant) + end + -- normal side + return height, tcd.slope_angle else - return tcd:get_width(qcolumn_index0), tcd.slope_angle + -- right wall/left wall (quadrant right/left) + local width = tcd:get_width(qcolumn_index0) + if tcd.interior_h == horizontal_dirs.right and quadrant == directions.left or + tcd.interior_h == horizontal_dirs.left and quadrant == directions.right then + 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 + -- flat side of rectangle (or empty region near flat side) + return width, world.quadrant_to_right_angle(quadrant) + 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 9292f35d..4165cb33 100644 --- a/src/platformer/world_utest.lua +++ b/src/platformer/world_utest.lua @@ -17,6 +17,206 @@ describe('world (with mock tiles data setup)', function () pico8:clear_map() end) + describe('angle_to_quadrant', function () + + it('should return quadrant down for slope_angle: nil', function () + assert.are_equal(directions.down, world.angle_to_quadrant(nil)) + end) + + it('should return quadrant to down for slope_angle: 1-0.124', function () + assert.are_equal(directions.down, world.angle_to_quadrant(1-0.124)) + end) + + it('should return quadrant to down for slope_angle: 0', function () + assert.are_equal(directions.down, world.angle_to_quadrant(0)) + end) + + it('should return quadrant to down for slope_angle: 0.124', function () + assert.are_equal(directions.down, world.angle_to_quadrant(0.124)) + end) + + it('should return quadrant to right for slope_angle: 0.25-0.125', function () + assert.are_equal(directions.right, world.angle_to_quadrant(0.25-0.125)) + end) + + it('should return quadrant to right for slope_angle: 0.25+0.125', function () + assert.are_equal(directions.right, world.angle_to_quadrant(0.25+0.125)) + end) + + it('should return quadrant to up for slope_angle: 0.5-0.124', function () + assert.are_equal(directions.up, world.angle_to_quadrant(0.5-0.124)) + end) + + it('should return quadrant to up for slope_angle: 0.5+0.124', function () + assert.are_equal(directions.up, world.angle_to_quadrant(0.5+0.124)) + end) + + it('should return quadrant to left for slope_angle: 0.75-0.125', function () + assert.are_equal(directions.left, world.angle_to_quadrant(0.75-0.125)) + end) + + it('should return quadrant to left for slope_angle: 0.75+0.125', function () + assert.are_equal(directions.left, world.angle_to_quadrant(0.75+0.125)) + end) + + end) + + describe('quadrant_to_right_angle', function () + + -- we had already written utests before extracting world.quadrant_to_right_angle + -- so we kept the tests checking final result instead of spy call + + it('should return 0 when quadrant is down', function () + assert.are_equal(0, world.quadrant_to_right_angle(directions.down)) + end) + + it('should return 0.25 when quadrant is right', function () + assert.are_equal(0.25, world.quadrant_to_right_angle(directions.right)) + end) + + it('should return 0.5 when quadrant is up', function () + assert.are_equal(0.5, world.quadrant_to_right_angle(directions.up)) + end) + + it('should return 0.75 when quadrant is left', function () + assert.are_equal(0.75, world.quadrant_to_right_angle(directions.left)) + end) + + end) + + + describe('get_quadrant_x_coord', function () + + it('should return pos.x when quadrant is down', function () + + assert.are_equal(10, world.get_quadrant_x_coord(vector(10, 20), directions.down)) + end) + + it('should return pos.x when quadrant is up', function () + + assert.are_equal(10, world.get_quadrant_x_coord(vector(10, 20), directions.up)) + end) + + it('should return pos.y when quadrant is right', function () + + assert.are_equal(20, world.get_quadrant_x_coord(vector(10, 20), directions.right)) + end) + + it('should return pos.y when quadrant is left', function () + + assert.are_equal(20, world.get_quadrant_x_coord(vector(10, 20), directions.left)) + end) + + end) + + describe('get_quadrant_y_coord', function () + + it('should return pos.y when quadrant is down', function () + assert.are_equal(20, world.get_quadrant_y_coord(vector(10, 20), directions.down)) + end) + + it('should return pos.y when quadrant is up', function () + assert.are_equal(20, world.get_quadrant_y_coord(vector(10, 20), directions.up)) + end) + + it('should return pos.y when quadrant is right', function () + assert.are_equal(10, world.get_quadrant_y_coord(vector(10, 20), directions.right)) + end) + + it('should return pos.y when quadrant is left', function () + assert.are_equal(10, world.get_quadrant_y_coord(vector(10, 20), directions.left)) + end) + + end) + + describe('get_quadrant_j_coord', function () + + it('should return loc.j when quadrant is down', function () + assert.are_equal(2, world.get_quadrant_j_coord(location(1, 2), directions.down)) + end) + + it('should return loc.j when quadrant is up', function () + assert.are_equal(2, world.get_quadrant_j_coord(location(1, 2), directions.up)) + end) + + it('should return loc.j when quadrant is right', function () + assert.are_equal(1, world.get_quadrant_j_coord(location(1, 2), directions.right)) + end) + + it('should return loc.j when quadrant is left', function () + assert.are_equal(1, world.get_quadrant_j_coord(location(1, 2), directions.left)) + end) + + end) + + describe('set_position_quadrant_x', function () + + it('should set pos.x when quadrant is down', function () + local p = vector(10, 20) + world.set_position_quadrant_x(p, 30, directions.down) + assert.are_same(vector(30, 20), p) + end) + + it('should set pos.x when quadrant is up', function () + local p = vector(10, 20) + world.set_position_quadrant_x(p, 30, directions.up) + assert.are_same(vector(30, 20), p) + end) + + it('should set pos.y when quadrant is right', function () + local p = vector(10, 20) + world.set_position_quadrant_x(p, 30, directions.right) + assert.are_same(vector(10, 30), p) + end) + + it('should set pos.y when quadrant is left', function () + local p = vector(10, 20) + world.set_position_quadrant_x(p, 30, directions.left) + assert.are_same(vector(10, 30), p) + end) + + end) + + describe('sub_qy', function () + + it('should return qy1 - qy2 when quadrant is down', function () + assert.are_equal(7, world.sub_qy(10, 3, directions.down)) + end) + + it('should return qy2 - qy1 when quadrant is up', function () + assert.are_equal(7, world.sub_qy(3, 10, directions.up)) + end) + + it('should return qy1 - qy2 when quadrant is right', function () + assert.are_equal(7, world.sub_qy(10, 3, directions.right)) + end) + + it('should return qy2 - qy1 when quadrant is left', function () + assert.are_equal(7, world.sub_qy(3, 10, directions.left)) + end) + + end) + + describe('get_tile_qbottom', function () + + it('should return tile world bottom when quadrant is down', function () + assert.are_equal(24, world.get_tile_qbottom(location(1, 2), directions.down)) + end) + + it('should return tile world top when quadrant is up', function () + assert.are_equal(16, world.get_tile_qbottom(location(1, 2), directions.up)) + end) + + it('should return world right when quadrant is right', function () + assert.are_equal(16, world.get_tile_qbottom(location(1, 2), directions.right)) + end) + + it('should return world left when quadrant is left', function () + assert.are_equal(8, world.get_tile_qbottom(location(1, 2), directions.left)) + end) + + end) + describe('_compute_qcolumn_height_at', function () it('should return (0, nil) if tile location is outside map area (any quadrant)', function () @@ -43,27 +243,61 @@ describe('world (with mock tiles data setup)', function () end) - describe('with ascending slope 22.5 offset by 2', function () + -- 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 + describe('with bottom_right_quarter_tile_id offset by 2', function () before_each(function () -- create an ascending slope 22.5 at (1, 1), i.e. (8, 14) to (15, 11) px - mock_mset(1, 1, asc_slope_22_id) + mock_mset(1, 1, 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)}) end) - it('should return 3 on column 3 (quadrant down)', function () - assert.are_same({3, 22.5 / 360}, {world._compute_qcolumn_height_at(location(1, 1), 3, directions.down)}) + it('should return 4 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 3 on column 3 (quadrant up)', function () - assert.are_same({3, 22.5 / 360}, {world._compute_qcolumn_height_at(location(1, 1), 3, directions.up)}) + 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)}) end) - it('should return 3 on column 3 (quadrant right)', function () - assert.are_same({2, 22.5 / 360}, {world._compute_qcolumn_height_at(location(1, 1), 3, directions.right)}) + it('should return 8 (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 3 on column 3 (quadrant left)', function () - assert.are_same({2, 22.5 / 360}, {world._compute_qcolumn_height_at(location(1, 1), 3, directions.left)}) + it('should return 0 (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 () + 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)}) + 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)}) + 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)}) + end) + + it('should return 8 (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 () + 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 () + assert.are_same({0, nil}, {world._compute_qcolumn_height_at(location(1, 1), 4, directions.left, true)}) end) end) @@ -74,26 +308,69 @@ describe('world (with mock tiles data setup)', function () mock_mset(1, 1, loop_topleft) end) - it('should return 5 on column 6 (quadrant down)', function () - assert.are_same({6, atan2(-4, 4)}, {world._compute_qcolumn_height_at(location(1, 1), 6, directions.down)}) + it('should return 8 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 5 on column 6 (quadrant up)', function () + it('should return 6 on column 6 (quadrant up)', function () assert.are_same({6, atan2(-4, 4)}, {world._compute_qcolumn_height_at(location(1, 1), 6, directions.up)}) end) - it('should return 5 on column 6 (quadrant right)', function () - assert.are_same({6, atan2(-4, 4)}, {world._compute_qcolumn_height_at(location(1, 1), 6, directions.right)}) + it('should return 8 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 5 on column 6 (quadrant left)', function () + it('should return 6 on row 6 (quadrant left)', function () assert.are_same({6, atan2(-4, 4)}, {world._compute_qcolumn_height_at(location(1, 1), 6, directions.left)}) end) end) + describe('with half-tile', function () + + before_each(function () + mock_mset(1, 1, half_tile_id) + end) + + -- half-tile allows us to test is_rectangle case + -- in principle we should also have a vertically-split half-tile + -- 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 () + 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 () + 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 () + 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)}) + end) + + it('should return 0 (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)}) + end) + + it('should return 8 (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) + end) + -- deprecated (correct, but not optimized) + describe('get_pixel_collision_info', function () describe('with full flat tile', function () diff --git a/src/test_data/tile_test_data.lua b/src/test_data/tile_test_data.lua index 006088a2..cfd318ba 100644 --- a/src/test_data/tile_test_data.lua +++ b/src/test_data/tile_test_data.lua @@ -8,19 +8,34 @@ local tile_collision_data = require("data/tile_collision_data") local stub = require("luassert.stub") require("test_data/tile_representation") -local mock_tile_collision_data = { +local mock_raw_tile_collision_data = { -- collision_data values + PICO-8 spritesheet must match our mockup data - [full_tile_id] = tile_collision_data({8, 8, 8, 8, 8, 8, 8, 8}, {8, 8, 8, 8, 8, 8, 8, 8}, atan2(8, 0)), - [half_tile_id] = tile_collision_data({4, 4, 4, 4, 4, 4, 4, 4}, {0, 0, 0, 0, 8, 8, 8, 8}, atan2(8, 0)), - [flat_low_tile_id] = tile_collision_data({2, 2, 2, 2, 2, 2, 2, 2}, {0, 0, 0, 0, 0, 0, 8, 8}, atan2(8, 0)), - [bottom_right_quarter_tile_id] = tile_collision_data({0, 0, 0, 0, 4, 4, 4, 4}, {0, 0, 0, 0, 4, 4, 4, 4}, atan2(8, 0)), - [asc_slope_45_id] = tile_collision_data({1, 2, 3, 4, 5, 6, 7, 8}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, -8)), - [asc_slope_22_id] = tile_collision_data({2, 2, 3, 3, 4, 4, 5, 5}, {0, 0, 0, 2, 4, 6, 8, 8}, 0.0625), - [desc_slope_45_id] = tile_collision_data({8, 7, 6, 5, 4, 3, 2, 1}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, 8)), - [asc_slope_22_upper_level_id] = tile_collision_data({5, 5, 6, 6, 7, 7, 8, 8}, {2, 4, 6, 8, 8, 8, 8, 8}, atan2(8, -4)), - [loop_topleft] = tile_collision_data({8, 8, 8, 8, 8, 7, 6, 5}, {8, 8, 8, 8, 8, 7, 6, 5}, atan2(-4, 4)), + [full_tile_id] = {{8, 8, 8, 8, 8, 8, 8, 8}, {8, 8, 8, 8, 8, 8, 8, 8}, atan2(8, 0)}, + [half_tile_id] = {{4, 4, 4, 4, 4, 4, 4, 4}, {0, 0, 0, 0, 8, 8, 8, 8}, atan2(8, 0)}, + [flat_low_tile_id] = {{2, 2, 2, 2, 2, 2, 2, 2}, {0, 0, 0, 0, 0, 0, 8, 8}, atan2(8, 0)}, + [bottom_right_quarter_tile_id] = {{0, 0, 0, 0, 4, 4, 4, 4}, {0, 0, 0, 0, 4, 4, 4, 4}, atan2(8, 0)}, + [asc_slope_45_id] = {{1, 2, 3, 4, 5, 6, 7, 8}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, -8)}, + [asc_slope_22_id] = {{2, 2, 3, 3, 4, 4, 5, 5}, {0, 0, 0, 2, 4, 6, 8, 8}, 0.0625}, + [desc_slope_45_id] = {{8, 7, 6, 5, 4, 3, 2, 1}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, 8)}, + [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)}, + [loop_topleft] = {{8, 8, 8, 8, 8, 7, 6, 5}, {8, 8, 8, 8, 8, 7, 6, 5}, atan2(-4, 4)}, } +-- process data above to generate interior_v/h automatically, so we don't have to add them manually +-- for each tile (and it's actually what PICO-8 build does in collision_data to define tiles_collision_data) +local mock_tile_collision_data = transform(mock_raw_tile_collision_data, function(raw_data) + local slope_angle = raw_data[3] + local interior_v, interior_h = tile_collision_data.slope_angle_to_interiors(slope_angle) + + return tile_collision_data( + raw_data[1], + raw_data[2], + slope_angle, + interior_v, + interior_h + ) +end) + local tile_test_data = {} function tile_test_data.setup() From 711ad17128b96fdea6c33126a05ba28c83975057 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 16:51:17 +0200 Subject: [PATCH 082/112] [PLAYER CHARACTER] Fixed #endif Tested and confirm loop quadrant working (run only) --- pico-boots | 2 +- src/ingame/playercharacter.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pico-boots b/pico-boots index f8468dc9..b41d81bd 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit f8468dc9d081dbdbd8be2b4c1b9c1d985ce8c9ae +Subproject commit b41d81bd99b3e2691ab4ecc757a62dcd1d7113bf diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 77135d95..9c489c02 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -166,7 +166,7 @@ function player_char:quadrant_rotated(v) else -- self.quadrant == directions.left return v:rotated_90_cw() end ---#end +--#endif end -- spawn character at given position, detecting ground/air on arrival From a16bd5c1303ef6c358a97a0815f90e2c1616aa7f Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 17:13:44 +0200 Subject: [PATCH 083/112] [PLAYER CHARACTER] Jump orthogonally to ground (slope angle) Fixed quadrant not resetting on jump/fall which made Sonic jump through floors and ceilings --- src/ingame/playercharacter.lua | 11 ++++++---- src/ingame/playercharacter_utest.lua | 31 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 9c489c02..d7d8bdb5 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -508,14 +508,14 @@ function player_char:_enter_motion_state(next_motion_state) if next_motion_state == motion_states.falling then -- we have just left the ground without jumping, enter falling state -- and since ground speed is now unused, reset it for clarity + self:set_slope_angle_with_quadrant(nil) self.ground_speed = 0 - self.slope_angle = nil self.should_jump = false elseif next_motion_state == motion_states.air_spin then -- we have just jumped, enter air_spin state -- and since ground speed is now unused, reset it for clarity + self:set_slope_angle_with_quadrant(nil) self.ground_speed = 0 - self.slope_angle = nil self.should_jump = false self.anim_spr:play("spin") elseif next_motion_state == motion_states.grounded then @@ -1107,8 +1107,11 @@ function player_char:_check_jump() -- 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 -- don't apply gravity during such a frame) - -- limitation: only support flat ground for now - self.velocity.y = self.velocity.y - pc_data.initial_var_jump_speed_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() + self.velocity:add_inplace(jump_impulse) self:_enter_motion_state(motion_states.air_spin) self.has_jumped_this_frame = true return true diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 6aaf8539..09bad11e 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1733,16 +1733,20 @@ describe('player_char', function () setup(function () spy.on(animated_sprite, "play") + spy.on(player_char, "set_slope_angle_with_quadrant") -- spy not stub in case the resulting slope_angle/quadrant matters end) teardown(function () animated_sprite.play:revert() + player_char.set_slope_angle_with_quadrant: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 + -- for set_slope_angle_with_quadrant, after_each would be fine too before_each(function () animated_sprite.play:clear() + player_char.set_slope_angle_with_quadrant:clear() end) it('should enter passed state: falling, reset ground-specific state vars, no animation change', function () @@ -1762,6 +1766,14 @@ describe('player_char', function () assert.spy(animated_sprite.play).was_not_called() end) + it('(grounded -> falling) should call set_slope_angle_with_quadrant(nil)', function () + -- character starts grounded + pc:_enter_motion_state(motion_states.falling) + + 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) + it('should enter passed state: air_spin, reset ground-specific state vars, play spin animation', function () -- character starts grounded pc:_enter_motion_state(motion_states.air_spin) @@ -1780,6 +1792,14 @@ describe('player_char', function () assert.spy(animated_sprite.play).was_called_with(match.ref(pc.anim_spr), "spin") end) + it('(grounded -> air_spin) should call set_slope_angle_with_quadrant(nil)', function () + -- character starts grounded + pc:_enter_motion_state(motion_states.air_spin) + + 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) + -- bugfix history: . it('should enter passed state: grounded, reset has_jumped_this_frame/has_interrupted_jump', function () pc.motion_state = motion_states.falling @@ -4297,6 +4317,17 @@ describe('player_char', function () assert.are_same({true, vector(4.1, -4.25), motion_states.air_spin, true}, {result, pc.velocity, pc.motion_state, pc.has_jumped_this_frame}) end) + it('should add impulse along ground normal when slope_angle is not 0 (and we should jump)', function () + pc.velocity = vector(2, -2) + pc.should_jump = true + pc.slope_angle = 0.125 + + pc:_check_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 () From 8ee1ca3da73494c2d66e567459bde911ceb4712e Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 18:08:04 +0200 Subject: [PATCH 084/112] [RUN] Added -gif_len 60 to capture up to 1mn of GIF in PICO-8 --- run_game.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_game.sh b/run_game.sh index 5ae37187..ca0497db 100755 --- a/run_game.sh +++ b/run_game.sh @@ -8,7 +8,7 @@ cartridge_stem="picosonic" version="3.1" config="$1"; shift -run_cmd="pico8 -run build/${cartridge_stem}_v${version}_${config}.p8 -screenshot_scale 4 -gif_scale 4 $@" +run_cmd="pico8 -run build/${cartridge_stem}_v${version}_${config}.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, From 92ab615ec39c68b34a71b7bed552f6f1ff25c6b2 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 18:08:23 +0200 Subject: [PATCH 085/112] [TOKENS] Start tracking tokens in analysis file to easily diff progression --- analysis/analysis_tokens_engine+game.py | 515 ++++++++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100644 analysis/analysis_tokens_engine+game.py diff --git a/analysis/analysis_tokens_engine+game.py b/analysis/analysis_tokens_engine+game.py new file mode 100644 index 00000000..c64a7a46 --- /dev/null +++ b/analysis/analysis_tokens_engine+game.py @@ -0,0 +1,515 @@ +Analyzing lua scripts in intermediate/release/pico-boots... + +common.lua (common.p8) +by analyze_script +- version: 16 +- lines: 20 +- chars: 779 +- tokens: 21 +- compressed chars: 489 + +animated_sprite_data.lua (animated_sprite_data.p8) +by analyze_script +- version: 16 +- lines: 40 +- chars: 1526 +- tokens: 97 +- compressed chars: 687 + +sprite_data.lua (sprite_data.p8) +by analyze_script +- version: 16 +- lines: 61 +- chars: 1885 +- tokens: 184 +- compressed chars: 808 + +sprite.lua (sprite.p8) +by analyze_script +- version: 16 +- lines: 95 +- chars: 4922 +- tokens: 246 +- compressed chars: 2282 + +animated_sprite.lua (animated_sprite.p8) +by analyze_script +- version: 16 +- lines: 124 +- chars: 6732 +- tokens: 275 +- compressed chars: 2476 + +color.lua (color.p8) +by analyze_script +- version: 16 +- lines: 33 +- chars: 567 +- tokens: 82 +- compressed chars: 363 + +input.lua (input.p8) +by analyze_script +- version: 16 +- lines: 189 +- chars: 6144 +- tokens: 398 +- compressed chars: 1730 + +constants.lua (constants.p8) +by analyze_script +- version: 16 +- lines: 20 +- chars: 370 +- tokens: 27 +- compressed chars: 259 + +gameapp.lua (gameapp.p8) +by analyze_script +- version: 16 +- lines: 192 +- chars: 6748 +- tokens: 303 +- compressed chars: 2572 + +coroutine_runner.lua (coroutine_runner.p8) +by analyze_script +- version: 16 +- lines: 52 +- chars: 2085 +- tokens: 117 +- compressed chars: 986 + +gamestate.lua (gamestate.p8) +by analyze_script +- version: 16 +- lines: 28 +- chars: 870 +- tokens: 38 +- compressed chars: 479 + +manager.lua (manager.p8) +by analyze_script +- version: 16 +- lines: 24 +- chars: 1027 +- tokens: 39 +- compressed chars: 538 + +flow.lua (flow.p8) +by analyze_script +- version: 16 +- lines: 102 +- chars: 2933 +- tokens: 122 +- compressed chars: 1287 + +assertions.lua (assertions.p8) +by analyze_script +- version: 16 +- lines: 34 +- chars: 1736 +- tokens: 147 +- compressed chars: 656 + +unittest_helper.lua (unittest_helper.p8) +by analyze_script +- version: 16 +- lines: 16 +- chars: 826 +- tokens: 69 +- compressed chars: 446 + +unittest.lua (unittest.p8) +by analyze_script +- version: 16 +- lines: 61 +- chars: 2055 +- tokens: 91 +- compressed chars: 1137 + +integrationtest.lua (integrationtest.p8) +by analyze_script +- version: 16 +- lines: 469 +- chars: 16810 +- tokens: 933 +- compressed chars: 5966 + +label.lua (label.p8) +by analyze_script +- version: 16 +- lines: 20 +- chars: 542 +- tokens: 47 +- compressed chars: 310 + +ui.lua (ui.p8) +by analyze_script +- version: 16 +- lines: 191 +- chars: 6363 +- tokens: 656 +- compressed chars: 1996 + +overlay.lua (overlay.p8) +by analyze_script +- version: 16 +- lines: 58 +- chars: 1491 +- tokens: 131 +- compressed chars: 745 + +api.lua (api.p8) +by analyze_script +- version: 16 +- lines: 6 +- chars: 178 +- tokens: 6 +- compressed chars: 155 + +serialization.lua (serialization.p8) +by analyze_script +- version: 16 +- lines: 276 +- chars: 13385 +- tokens: 587 +- compressed chars: 4900 + +tilemap.lua (tilemap.p8) +by analyze_script +- version: 16 +- lines: 28 +- chars: 608 +- tokens: 70 +- compressed chars: 382 + +direction_ext.lua (direction_ext.p8) +by analyze_script +- version: 16 +- lines: 29 +- chars: 598 +- tokens: 72 +- compressed chars: 209 + +string.lua (string.p8) +by analyze_script +- version: 16 +- lines: 181 +- chars: 6030 +- tokens: 412 +- compressed chars: 2845 + +class.lua (class.p8) +by analyze_script +- version: 16 +- lines: 178 +- chars: 7470 +- tokens: 327 +- compressed chars: 3107 + +random.lua (random.p8) +by analyze_script +- version: 16 +- lines: 18 +- chars: 479 +- tokens: 36 +- compressed chars: 287 + +datastruct.lua (datastruct.p8) +by analyze_script +- version: 16 +- lines: 97 +- chars: 3239 +- tokens: 283 +- compressed chars: 1422 + +coroutine.lua (coroutine.p8) +by analyze_script +- version: 16 +- lines: 11 +- chars: 355 +- tokens: 20 +- compressed chars: 242 + +vector_ext.lua (vector_ext.p8) +by analyze_script +- version: 16 +- lines: 107 +- chars: 3435 +- tokens: 297 +- compressed chars: 1198 + +helper.lua (helper.p8) +by analyze_script +- version: 16 +- lines: 154 +- chars: 5069 +- tokens: 303 +- compressed chars: 2220 + +math.lua (math.p8) +by analyze_script +- version: 16 +- lines: 194 +- chars: 5100 +- tokens: 435 +- compressed chars: 2076 + +dump.lua (dump.p8) +by analyze_script +- version: 16 +- lines: 3 +- chars: 34 +- tokens: 0 +- compressed chars: 35 + +debug_window.lua (debug_window.p8) +by analyze_script +- version: 16 +- lines: 36 +- chars: 742 +- tokens: 91 +- compressed chars: 393 + +codetuner.lua (codetuner.p8) +by analyze_script +- version: 16 +- lines: 16 +- chars: 247 +- tokens: 13 +- compressed chars: 182 + +visual_logger.lua (visual_logger.p8) +by analyze_script +- version: 16 +- lines: 11 +- chars: 297 +- tokens: 6 +- compressed chars: 232 + +profiler.lua (profiler.p8) +by analyze_script +- version: 16 +- lines: 11 +- chars: 285 +- tokens: 6 +- compressed chars: 220 + +logging.lua (logging.p8) +by analyze_script +- version: 16 +- lines: 11 +- chars: 271 +- tokens: 6 +- compressed chars: 221 + +collision.lua (collision.p8) +by analyze_script +- version: 16 +- lines: 142 +- chars: 6933 +- tokens: 359 +- compressed chars: 2512 + +pico8wtk.lua (pico8wtk.p8) +by analyze_script +- version: 16 +- lines: 761 +- chars: 12907 +- tokens: 2877 +- compressed chars: 4315 + +Analyzing lua scripts in intermediate/release/src... + +itest_main.lua (itest_main.p8) +by analyze_script +- version: 16 +- lines: 70 +- chars: 2031 +- tokens: 128 +- compressed chars: 936 + +main.lua (main.p8) +by analyze_script +- version: 16 +- lines: 35 +- chars: 618 +- tokens: 42 +- compressed chars: 417 + +sandbox.lua (sandbox.p8) +by analyze_script +- version: 16 +- lines: 23 +- chars: 426 +- tokens: 31 +- compressed chars: 340 + +utest_main.lua (utest_main.p8) +by analyze_script +- version: 16 +- lines: 20 +- chars: 692 +- tokens: 26 +- compressed chars: 443 + +picosonic_app.lua (picosonic_app.p8) +by analyze_script +- version: 16 +- lines: 44 +- chars: 790 +- tokens: 73 +- compressed chars: 389 + +playercharacter.lua (playercharacter.p8) +by analyze_script +- version: 16 +- lines: 1399 +- chars: 72546 +- tokens: 2886 +- compressed chars: 26910 + +stage_state.lua (stage_state.p8) +by analyze_script +- version: 16 +- lines: 213 +- chars: 5604 +- tokens: 495 +- compressed chars: 2024 + +itesttitlemenu.lua (itesttitlemenu.p8) +by analyze_script +- version: 16 +- lines: 50 +- chars: 1453 +- tokens: 128 +- compressed chars: 677 + +itestplayercharacter.lua (itestplayercharacter.p8) +by analyze_script +- version: 16 +- lines: 350 +- chars: 19759 +- tokens: 182 +- compressed chars: 7122 + +visual.lua (visual.p8) +by analyze_script +- version: 16 +- lines: 12 +- chars: 191 +- tokens: 17 +- compressed chars: 122 + +audio.lua (audio.p8) +by analyze_script +- version: 16 +- lines: 16 +- chars: 223 +- tokens: 25 +- compressed chars: 135 + +world.lua (world.p8) +by analyze_script +- version: 16 +- lines: 242 +- chars: 10480 +- tokens: 488 +- compressed chars: 3993 + +motion.lua (motion.p8) +by analyze_script +- version: 16 +- lines: 72 +- chars: 3050 +- tokens: 136 +- compressed chars: 1155 + +credits.lua (credits.p8) +by analyze_script +- version: 16 +- lines: 27 +- chars: 434 +- tokens: 39 +- compressed chars: 257 + +titlemenu.lua (titlemenu.p8) +by analyze_script +- version: 16 +- lines: 66 +- chars: 1601 +- tokens: 184 +- compressed chars: 666 + +raw_tile_collision_data.lua (raw_tile_collision_data.p8) +by analyze_script +- version: 16 +- lines: 21 +- chars: 1018 +- tokens: 21 +- compressed chars: 624 + +collision_data.lua (collision_data.p8) +by analyze_script +- version: 16 +- lines: 47 +- chars: 6347 +- tokens: 92 +- compressed chars: 2938 + +tile_collision_data.lua (tile_collision_data.p8) +by analyze_script +- version: 16 +- lines: 224 +- chars: 10589 +- tokens: 525 +- compressed chars: 3715 + +playercharacter_data.lua (playercharacter_data.p8) +by analyze_script +- version: 16 +- lines: 148 +- chars: 7247 +- tokens: 215 +- compressed chars: 3030 + +stage_data.lua (stage_data.p8) +by analyze_script +- version: 16 +- lines: 42 +- chars: 865 +- tokens: 60 +- compressed chars: 542 + +tile_test_data.lua (tile_test_data.p8) +by analyze_script +- version: 16 +- lines: 11 +- chars: 313 +- tokens: 6 +- compressed chars: 228 + +tile_representation.lua (tile_representation.p8) +by analyze_script +- version: 16 +- lines: 32 +- chars: 1223 +- tokens: 78 +- compressed chars: 664 + +itest_dsl.lua (itest_dsl.p8) +by analyze_script +- version: 16 +- lines: 668 +- chars: 25360 +- tokens: 1640 +- compressed chars: 8913 + +utestdata.lua (utestdata.p8) +by analyze_script +- version: 16 +- lines: 39 +- chars: 1718 +- tokens: 112 +- compressed chars: 825 From c4a52af18d6ec494791d5c1420ca9fb908036d61 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 18:40:56 +0200 Subject: [PATCH 086/112] [PLAYER CHARACTERS] Fixed get_bottom_center to take quadrant into account [TOKENS] -> 9127 by stripping get_bottom_center but for #itest --- analysis/analysis_tokens_engine+game.py | 8 ++++---- src/ingame/playercharacter.lua | 4 +++- src/ingame/playercharacter_utest.lua | 14 ++++++++++---- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/analysis/analysis_tokens_engine+game.py b/analysis/analysis_tokens_engine+game.py index c64a7a46..c92d28ce 100644 --- a/analysis/analysis_tokens_engine+game.py +++ b/analysis/analysis_tokens_engine+game.py @@ -365,10 +365,10 @@ playercharacter.lua (playercharacter.p8) by analyze_script - version: 16 -- lines: 1399 -- chars: 72546 -- tokens: 2886 -- compressed chars: 26910 +- lines: 1396 +- chars: 72440 +- tokens: 2871 +- compressed chars: 26903 stage_state.lua (stage_state.p8) by analyze_script diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index d7d8bdb5..11300a60 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -200,9 +200,11 @@ function player_char:warp_bottom_to(bottom_position) end -- move the player character so that the bottom center is at the given position +--#if itest function player_char:get_bottom_center() - return self.position + vector(0, self:get_center_height()) + return self.position + self:get_center_height() * self:get_quadrant_down() end +--#endif -- move the player character so that the bottom center is at the given position function player_char:set_bottom_center(bottom_center_position) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 09bad11e..0021867a 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -541,7 +541,7 @@ describe('player_char', function () end) - describe('get_bottom_center', function () + describe('#solo get_bottom_center', function () setup(function () stub(player_char, "get_center_height", function () @@ -553,9 +553,15 @@ describe('player_char', function () player_char.get_center_height:revert() end) - it('(10 0 3) => at (10 6)', function () + it('(10, 0) => (10, center_height)', function () pc.position = vector(10, 0) - assert.are_equal(vector(10, 0 + 11), pc:get_bottom_center()) + assert.are_equal(vector(10, 11), pc:get_bottom_center()) + end) + + it('(10, 0) quadrant left => (10 - center_height, 0)', function () + pc.position = vector(10, 0) + pc.quadrant = directions.left + assert.are_equal(vector(-1, 0), pc:get_bottom_center()) end) end) @@ -572,7 +578,7 @@ describe('player_char', function () player_char.get_center_height:revert() end) - it('set_bottom_center (10 6) => at (10 0)', function () + it('set_bottom_center (10, center_height) => (10, 0)', function () pc:set_bottom_center(vector(10, 0 + 11)) assert.are_equal(vector(10, 0), pc.position) end) From 13341f0ca37a6f473e187b6504f67e502186032b Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 18:45:17 +0200 Subject: [PATCH 087/112] [TOKENS] -> 9099 Strip set_bottom_center but for utests, inlined move_by only used in #cheat --- src/ingame/playercharacter.lua | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 11300a60..a350c216 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -206,15 +206,12 @@ function player_char:get_bottom_center() end --#endif +--#if busted -- move the player character so that the bottom center is at the given position function player_char:set_bottom_center(bottom_center_position) self.position = bottom_center_position - vector(0, self:get_center_height()) end - --- move the player character from delta_vector in px -function player_char:move_by(delta_vector) - self.position = self.position + delta_vector -end +--#endif -- set slope angle and update quadrant function player_char:set_slope_angle_with_quadrant(angle) @@ -1464,7 +1461,7 @@ function player_char:_update_debug() -- it's much more complicated to access app from here (e.g. via flow.curr_state) -- just to get delta_time, so we just use the constant as we know we are at 60 FPS -- otherwise we'd have to change utests to init app+flow each time - self:move_by(self.debug_velocity * delta_time60) + self.position = self.position + self.debug_velocity * delta_time60 end function player_char:_update_velocity_debug() From 66de2fe98e3f27deecd1f056e98ca1972afc10e6 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 18:59:46 +0200 Subject: [PATCH 088/112] [BUILD] Build debug works again by using fewer symbols --- build_game.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_game.sh b/build_game.sh index 015880cf..9adb6787 100755 --- a/build_game.sh +++ b/build_game.sh @@ -70,9 +70,9 @@ fi symbols='' if [[ $config == 'debug' ]]; then - symbols='assert,deprecated,log,visual_logger,tuner,profiler,mouse,cheat,sandbox' + # symbols='assert,deprecated,log,visual_logger,tuner,profiler,mouse,cheat,sandbox' # lighter config (to remain under 65536 chars) - # symbols='assert,deprecated,log,cheat,sandbox' + symbols='assert,deprecated,log,cheat,sandbox' elif [[ $config == 'debug-ultrafast' ]]; then symbols='assert,deprecated,log,cheat,sandbox,ultrafast' elif [[ $config == 'cheat' ]]; then From da6e8bfeb089c1b231c8ee31b4af7eaf267ac9aa Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 19:16:53 +0200 Subject: [PATCH 089/112] [WORLD] Changed angle_to_quadrant to give priority to vertical directions [TOKENS] -> 9092 Inlined get_quadrant_slope_angle [TEST] Fixed set_bottom_center --- src/ingame/playercharacter.lua | 9 +-- src/ingame/playercharacter_utest.lua | 102 +++++++++------------------ src/platformer/world.lua | 15 ++-- src/platformer/world_utest.lua | 36 +++++----- 4 files changed, 60 insertions(+), 102 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index a350c216..d5f0374b 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -131,11 +131,6 @@ function player_char:get_quadrant_right_angle() return world.quadrant_to_right_angle(self.quadrant) end --- return slope angle, relative to quadrant tangent right -function player_char:get_quadrant_slope_angle() - return self.slope_angle - self:get_quadrant_right_angle() -end - -- return quadrant tangent right (forward) unit vector function player_char:get_quadrant_right() return dir_vectors[rotate_dir_90_ccw(self.quadrant)] @@ -209,7 +204,7 @@ end --#if busted -- move the player character so that the bottom center is at the given position function player_char:set_bottom_center(bottom_center_position) - self.position = bottom_center_position - vector(0, self:get_center_height()) + self.position = bottom_center_position - self:get_center_height() * self:get_quadrant_down() end --#endif @@ -804,7 +799,7 @@ function player_char:_compute_ground_motion_result() -- 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:get_quadrant_slope_angle()) + local ground_based_signed_distance_qx = self.ground_speed * cos(self.slope_angle - self:get_quadrant_right_angle()) -- 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 diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 0021867a..26fe6eb8 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -277,46 +277,6 @@ describe('player_char', function () end) - describe('get_quadrant_slope_angle', function () - - it('should return 0 when vertically relative to the quadrant (down)', function () - pc.quadrant = directions.down - pc.slope_angle = 0 - assert.are_equal(0, pc:get_quadrant_slope_angle()) - end) - - it('should return 0 when vertically relative to the quadrant (right)', function () - pc.quadrant = directions.right - pc.slope_angle = 0.25 - assert.are_equal(0, pc:get_quadrant_slope_angle()) - end) - - it('should return 0 when vertically relative to the quadrant (up)', function () - pc.quadrant = directions.up - pc.slope_angle = 0.5 - assert.are_equal(0, pc:get_quadrant_slope_angle()) - end) - - it('should return 0 when vertically relative to the quadrant (left)', function () - pc.quadrant = directions.left - pc.slope_angle = 0.75 - assert.are_equal(0, pc:get_quadrant_slope_angle()) - end) - - it('should return 0.1 when tilted by 0.1 from quadrant (right) vertical', function () - pc.quadrant = directions.right - pc.slope_angle = 0.35 - assert.is_true(almost_eq_with_message(0.1, pc:get_quadrant_slope_angle())) - end) - - it('should return -0.1 when tilted by -0.1 from quadrant (left) vertical', function () - pc.quadrant = directions.left - pc.slope_angle = 0.65 - assert.is_true(almost_eq_with_message(-0.1, pc:get_quadrant_slope_angle())) - end) - - end) - describe('get_quadrant_right', function () it('should return vector(1, 0) when quadrant is down', function () @@ -579,18 +539,16 @@ describe('player_char', function () end) it('set_bottom_center (10, center_height) => (10, 0)', function () - pc:set_bottom_center(vector(10, 0 + 11)) + pc:set_bottom_center(vector(10, 11)) assert.are_equal(vector(10, 0), pc.position) end) - end) - - describe('move_by', function () - it('at (4 -4) move_by (-5 4) => at (-1 0)', function () - pc.position = vector(4, -4) - pc:move_by(vector(-5, 4)) - assert.are_equal(vector(-1, 0), pc.position) + it('set_bottom_center (10 + center_height, 0) quadrant right => (10, 0)', function () + pc.quadrant = directions.right + pc:set_bottom_center(vector(10 + 11, 0)) + assert.are_equal(vector(10, 0), pc.position) end) + end) describe('set_slope_angle_with_quadrant', function () @@ -607,9 +565,9 @@ describe('player_char', function () assert.are_equal(directions.down, pc.quadrant) end) - it('should set quadrant to down for slope_angle: 1-0.124', function () + it('should set quadrant to down for slope_angle: 1-0.125', function () pc.quadrant = nil - pc:set_slope_angle_with_quadrant(1-0.124) + pc:set_slope_angle_with_quadrant(1-0.125) assert.are_equal(directions.down, pc.quadrant) end) @@ -619,45 +577,45 @@ describe('player_char', function () assert.are_equal(directions.down, pc.quadrant) end) - it('should set quadrant to down for slope_angle: 0.124', function () + it('should set quadrant to down for slope_angle: 0.125', function () pc.quadrant = nil - pc:set_slope_angle_with_quadrant(0.124) + pc:set_slope_angle_with_quadrant(0.125) assert.are_equal(directions.down, pc.quadrant) end) - it('should set quadrant to right for slope_angle: 0.25-0.125', function () + it('should set quadrant to right for slope_angle: 0.25-0.124', function () pc.quadrant = nil - pc:set_slope_angle_with_quadrant(0.25-0.125) + pc:set_slope_angle_with_quadrant(0.25-0.124) assert.are_equal(directions.right, pc.quadrant) end) - it('should set quadrant to right for slope_angle: 0.25+0.125', function () + it('should set quadrant to right for slope_angle: 0.25+0.124', function () pc.quadrant = nil - pc:set_slope_angle_with_quadrant(0.25+0.125) + pc:set_slope_angle_with_quadrant(0.25+0.124) assert.are_equal(directions.right, pc.quadrant) end) - it('should set quadrant to up for slope_angle: 0.5-0.124', function () + it('should set quadrant to up for slope_angle: 0.5-0.125', function () pc.quadrant = nil - pc:set_slope_angle_with_quadrant(0.5-0.124) + pc:set_slope_angle_with_quadrant(0.5-0.125) assert.are_equal(directions.up, pc.quadrant) end) - it('should set quadrant to up for slope_angle: 0.5+0.124', function () + it('should set quadrant to up for slope_angle: 0.5+0.125', function () pc.quadrant = nil - pc:set_slope_angle_with_quadrant(0.5+0.124) + pc:set_slope_angle_with_quadrant(0.5+0.125) assert.are_equal(directions.up, pc.quadrant) end) - it('should set quadrant to left for slope_angle: 0.75-0.125', function () + it('should set quadrant to left for slope_angle: 0.75-0.124', function () pc.quadrant = nil - pc:set_slope_angle_with_quadrant(0.75-0.125) + pc:set_slope_angle_with_quadrant(0.75-0.124) assert.are_equal(directions.left, pc.quadrant) end) - it('should set quadrant to left for slope_angle: 0.75+0.125', function () + it('should set quadrant to left for slope_angle: 0.75+0.124', function () pc.quadrant = nil - pc:set_slope_angle_with_quadrant(0.75+0.125) + pc:set_slope_angle_with_quadrant(0.75+0.124) assert.are_equal(directions.left, pc.quadrant) end) @@ -1692,6 +1650,10 @@ describe('player_char', function () 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 + describe('with descending slope 45', function () before_each(function () @@ -5610,7 +5572,7 @@ describe('player_char', function () it('when move intention is (-1, 1), update 1 frame => at (3.867 -3.867)', function () pc.move_intention = vector(-1, 1) pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time60) + pc.position:add_inplace(pc.debug_velocity * delta_time60) assert.is_true(almost_eq_with_message(vector(3.8667, -3.8667), pc.position)) end) @@ -5618,7 +5580,7 @@ describe('player_char', function () pc.move_intention = vector(-1, 1) for i=1,10 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time60) + pc.position:add_inplace(pc.debug_velocity * delta_time60) end assert.is_true(almost_eq_with_message(vector(-2.73, 2.73), pc.position)) assert.is_true(almost_eq_with_message(vector(-60, 60), pc.debug_velocity)) -- at max speed @@ -5628,12 +5590,12 @@ describe('player_char', function () pc.move_intention = vector(-1, 1) for i=1,10 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time60) + pc.position:add_inplace(pc.debug_velocity * delta_time60) end pc.move_intention = vector.zero() for i=1,5 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time60) + pc.position:add_inplace(pc.debug_velocity * delta_time60) end assert.is_true(almost_eq_with_message(vector(-20, 20), pc.debug_velocity, 0.01)) end) @@ -5642,12 +5604,12 @@ describe('player_char', function () pc.move_intention = vector(-1, 1) for i=1,10 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time60) + pc.position:add_inplace(pc.debug_velocity * delta_time60) end pc.move_intention = vector.zero() for i=1,8 do pc:_update_velocity_debug() - pc:move_by(pc.debug_velocity * delta_time60) + pc.position:add_inplace(pc.debug_velocity * delta_time60) end assert.is_true(almost_eq_with_message(vector.zero(), pc.debug_velocity)) end) diff --git a/src/platformer/world.lua b/src/platformer/world.lua index 4f7d4a98..74152343 100644 --- a/src/platformer/world.lua +++ b/src/platformer/world.lua @@ -5,21 +5,18 @@ local world = {} -- return quadrant in which angle is contained (non-injective) function world.angle_to_quadrant(angle) - -- priority to horizontal quadrants at the boundaries - -- (just so 45-deg slope is recognized as left/right and character can get control-locked - -- to mimic hysteresis motion when trying to walk up slide in Hydrocity Zone) - -- looks like Classic Sonic gives priority to vertical quadrants (down, up) though, so - -- consider changing edge cases and use angle directly for lock calculation + -- 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 -- 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 + if not angle or angle >= 0.875 or angle <= 0.125 then return directions.down - elseif angle <= 0.375 then + elseif angle < 0.375 then return directions.right - elseif angle < 0.625 then + elseif angle <= 0.625 then return directions.up - else -- angle <= 0.875 + else -- angle < 0.875 return directions.left end end diff --git a/src/platformer/world_utest.lua b/src/platformer/world_utest.lua index 4165cb33..13ac98b4 100644 --- a/src/platformer/world_utest.lua +++ b/src/platformer/world_utest.lua @@ -23,40 +23,44 @@ describe('world (with mock tiles data setup)', function () assert.are_equal(directions.down, world.angle_to_quadrant(nil)) end) - it('should return quadrant to down for slope_angle: 1-0.124', function () - assert.are_equal(directions.down, world.angle_to_quadrant(1-0.124)) + it('should return quadrant down for slope_angle: 1-0.125', function () + assert.are_equal(directions.down, world.angle_to_quadrant(1-0.125)) end) - it('should return quadrant to down for slope_angle: 0', function () + it('should return quadrant down for slope_angle: 0', function () assert.are_equal(directions.down, world.angle_to_quadrant(0)) end) - it('should return quadrant to down for slope_angle: 0.124', function () + it('should return quadrant down for slope_angle: 0.124', function () assert.are_equal(directions.down, world.angle_to_quadrant(0.124)) end) - it('should return quadrant to right for slope_angle: 0.25-0.125', function () - assert.are_equal(directions.right, world.angle_to_quadrant(0.25-0.125)) + it('should return quadrant down for slope_angle: 0.25-0.125', function () + assert.are_equal(directions.down, world.angle_to_quadrant(0.25-0.125)) end) - it('should return quadrant to right for slope_angle: 0.25+0.125', function () - assert.are_equal(directions.right, world.angle_to_quadrant(0.25+0.125)) + it('should return quadrant right for slope_angle: 0.25-0.124', function () + assert.are_equal(directions.right, world.angle_to_quadrant(0.25-0.124)) end) - it('should return quadrant to up for slope_angle: 0.5-0.124', function () - assert.are_equal(directions.up, world.angle_to_quadrant(0.5-0.124)) + it('should return quadrant right for slope_angle: 0.25+0.124', function () + assert.are_equal(directions.right, world.angle_to_quadrant(0.25+0.124)) end) - it('should return quadrant to up for slope_angle: 0.5+0.124', function () - assert.are_equal(directions.up, world.angle_to_quadrant(0.5+0.124)) + it('should return quadrant up for slope_angle: 0.5-0.125', function () + assert.are_equal(directions.up, world.angle_to_quadrant(0.5-0.125)) end) - it('should return quadrant to left for slope_angle: 0.75-0.125', function () - assert.are_equal(directions.left, world.angle_to_quadrant(0.75-0.125)) + it('should return quadrant to up for slope_angle: 0.5+0.125', function () + assert.are_equal(directions.up, world.angle_to_quadrant(0.5+0.125)) end) - it('should return quadrant to left for slope_angle: 0.75+0.125', function () - assert.are_equal(directions.left, world.angle_to_quadrant(0.75+0.125)) + it('should return quadrant to left for slope_angle: 0.75-0.124', function () + assert.are_equal(directions.left, world.angle_to_quadrant(0.75-0.124)) + end) + + it('should return quadrant to left for slope_angle: 0.75+0.124', function () + assert.are_equal(directions.left, world.angle_to_quadrant(0.75+0.124)) end) end) From 174e5d3a710530c869909577f21ec14db871c4d6 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 19:27:12 +0200 Subject: [PATCH 090/112] [TOKENS] -> 9087 by simplifying for loop --- src/ingame/playercharacter.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index d5f0374b..67bab346 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -305,7 +305,9 @@ function player_char:_compute_ground_sensors_signed_distance(center_position) local highest_ground_slope_angle = nil -- check both ground sensors for ground - for i in all({horizontal_dirs.left, horizontal_dirs.right}) do + for i=1,2 do + -- equivalent to: + -- for i in all({horizontal_dirs.left, horizontal_dirs.right}) do -- check that ground sensor #i is on top of or below the mask column local sensor_position = self:_get_ground_sensor_position_from(center_position, i) From 3c4a4232569c7c6d8640591bde68a6d82a7f4c26 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 19:30:12 +0200 Subject: [PATCH 091/112] [TOKENS] -> 9083 by merging conditions --- src/ingame/playercharacter.lua | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 67bab346..f52b9e0b 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -318,13 +318,12 @@ function player_char:_compute_ground_sensors_signed_distance(center_position) -- then q-horizontal direction breaks tie -- store the biggest penetration height among sensors - if signed_distance < min_signed_distance then - -- this ground is higher than the previous one, store new height and slope angle - min_signed_distance = signed_distance - highest_ground_slope_angle = slope_angle - elseif signed_distance == min_signed_distance and self:_get_prioritized_dir() == i then - -- this ground has the same height as the previous one, but character orientation - -- makes him stand on that one rather than the previous one, so we use its slope + -- case a: this ground is higher than the previous one, store new height and slope angle + -- case b: this ground has the same height as the previous one, but character orientation + -- makes him stand on that one rather than the previous one, so we use its slope + -- check both cases in condition below + if signed_distance < min_signed_distance or signed_distance == min_signed_distance and self:_get_prioritized_dir() == i then + min_signed_distance = signed_distance -- does nothing in case b highest_ground_slope_angle = slope_angle end From c464c36239cc620d3160a982e911586bfb643162 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 19:42:44 +0200 Subject: [PATCH 092/112] [TOKENS] -> 9065 by reusing quadrant_down/up in _compute_signed_distance_to_closest_ground and _is_column_blocked_by_ceiling_at --- src/ingame/playercharacter.lua | 19 +++++++++---------- src/ingame/playercharacter_utest.lua | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index f52b9e0b..d79702d3 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -367,7 +367,6 @@ function player_char:_get_ground_sensor_position_from(center_position, quadrant_ -- from character center, move down by center height to get the character bottom center local qx_floored_bottom_center = vector(x, y) + self:get_center_height() * self:get_quadrant_down() - -- using a ground_sensor_extent_x in .5 and flooring +/- this value allows us to get the checked column x (the x corresponds to the left of that column) -- 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 @@ -392,6 +391,8 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_compute_signed_distance_to_closest_ground: sensor_position qx must be floored") + local quadrant_down = self:get_quadrant_down() + -- we used to flr sensor_position.y (would now be qy) at this point, -- but actually collision checks don't mind the fractions -- in addition, we will automatically get the correct signed distance to ground with fractional part! @@ -401,8 +402,8 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- from sensor + offset position (in qy) -- we are effectively finding the tiles covered (even partially) by the q-vertical segment between the edge positions -- where the character can snap up (escape) and snap down - local snap_zone_qtop = sensor_position + self:quadrant_rotated(vector(0, - pc_data.max_ground_escape_height - 1)) - local snap_zone_qbottom = sensor_position + self:quadrant_rotated(vector(0, pc_data.max_ground_snap_height)) + local snap_zone_qtop = sensor_position - (pc_data.max_ground_escape_height + 1) * quadrant_down + local snap_zone_qbottom = sensor_position + pc_data.max_ground_snap_height * quadrant_down -- start at the top, remember the bottom to end the iteration local curr_tile_loc = snap_zone_qtop:to_location() @@ -419,7 +420,6 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- we iterate on tiles along quadrant down, so just convert it to tile_vector -- to allow step addition - local quadrant_down = self:get_quadrant_down() local tile_loc_step = tile_vector(quadrant_down.x, quadrant_down.y) -- keep looping until we find ground to snap up/down, or ground too far for that, @@ -1006,15 +1006,18 @@ function player_char:_is_column_blocked_by_ceiling_at(sensor_position) assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_is_column_blocked_by_ceiling_at: sensor_position qx must be floored") + local quadrant_opp = oppose_dir(self.quadrant) + local quadrant_up = dir_vectors[quadrant_opp] + -- 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) - local ceiling_detection_zone_qtop = sensor_position + self:quadrant_rotated(vector(0, - self:get_full_height())) + local ceiling_detection_zone_qtop = sensor_position + self:get_full_height() * quadrant_up -- 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_signed_distance_to_closest_ground) - local ceiling_detection_zone_qbottom = sensor_position + self:quadrant_rotated(vector(0, - pc_data.max_ground_escape_height - 1)) + local ceiling_detection_zone_qbottom = sensor_position + (pc_data.max_ground_escape_height + 1) * quadrant_up -- define sensor tile location and tile iteration bounds local sensor_tile_loc = sensor_position:to_location() @@ -1029,10 +1032,6 @@ function player_char:_is_column_blocked_by_ceiling_at(sensor_position) -- we iterate on tiles along quadrant up, so just convert it to tile_vector -- to allow step addition - local quadrant_opp = oppose_dir(self.quadrant) - local quadrant_up = dir_vectors[quadrant_opp] - -- since we got quadrant down, we must oppose; - -- if you don't like the 2 extra tokens, just iterate from top to bottom instead local tile_loc_step = tile_vector(quadrant_up.x, quadrant_up.y) -- iterate from bottom (Sonic feet) to top (Sonic head) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 26fe6eb8..2062362d 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -501,7 +501,7 @@ describe('player_char', function () end) - describe('#solo get_bottom_center', function () + describe('get_bottom_center', function () setup(function () stub(player_char, "get_center_height", function () From 345ee932c600432a3c1413672f09ef088b41359e Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 19:47:21 +0200 Subject: [PATCH 093/112] [TOKENS] -> 9064 by storing self.quadrant in local var --- src/ingame/playercharacter.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index d79702d3..97fba8e8 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -392,6 +392,7 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_compute_signed_distance_to_closest_ground: sensor_position qx must be floored") local quadrant_down = self:get_quadrant_down() + local quadrant = self.quadrant -- we used to flr sensor_position.y (would now be qy) at this point, -- but actually collision checks don't mind the fractions @@ -416,7 +417,7 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- and columns/rows are stored exactly like that in collision data (not CCW or anything) -- so unlike other operations, the subtraction from topleft (combined with qx coord) is correct -- to get column index for qcolumn height later, without the need to quadrant-rotate vectors first - local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - sensor_location_topleft, self.quadrant) -- from 0 to tile_size - 1 + local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - sensor_location_topleft, quadrant) -- from 0 to tile_size - 1 -- we iterate on tiles along quadrant down, so just convert it to tile_vector -- to allow step addition @@ -426,13 +427,13 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- or we've reached the last tile (too low to snap down) while true do - local curr_tile_j = world.get_quadrant_j_coord(curr_tile_loc, self.quadrant) + local curr_tile_j = world.get_quadrant_j_coord(curr_tile_loc, quadrant) -- get q-bottom of tile to compare heights easily later - local current_tile_qbottom = world.get_tile_qbottom(curr_tile_loc, self.quadrant) + local current_tile_qbottom = world.get_tile_qbottom(curr_tile_loc, quadrant) -- check for ground (by q-column) in currently checked tile, sensor qX - local qcolumn_height, slope_angle = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0, self.quadrant) + local qcolumn_height, slope_angle = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0, quadrant) -- 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 @@ -444,7 +445,7 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- 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 column - local signed_distance_to_closest_ground = world.sub_qy(current_tile_qbottom, world.get_quadrant_y_coord(sensor_position, self.quadrant), self.quadrant) - qcolumn_height + local signed_distance_to_closest_ground = world.sub_qy(current_tile_qbottom, world.get_quadrant_y_coord(sensor_position, quadrant), self.quadrant) - qcolumn_height if signed_distance_to_closest_ground < -pc_data.max_ground_escape_height then -- ground found, but character is too deep inside to snap q-up -- return edge case (-pc_data.max_ground_escape_height - 1, 0) From 71a963ee333627bec5f4700499f6a56ee6917488 Mon Sep 17 00:00:00 2001 From: huulong Date: Fri, 21 Aug 2020 20:29:35 +0200 Subject: [PATCH 094/112] [TOKENS] -> 9060 Removed get_quadrant_right_angle, more quadrant in local vars Fixed issue in build due to luamin bug with brackets --- src/ingame/playercharacter.lua | 48 ++++++++++++++-------------- src/ingame/playercharacter_utest.lua | 34 +++----------------- 2 files changed, 28 insertions(+), 54 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 97fba8e8..9ac828e8 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -125,12 +125,6 @@ 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 angle (not quadrant interior direction angle!) --- down -> 0, right -> 0.25, up -> 0.5, left -> 0.75 -function player_char:get_quadrant_right_angle() - return world.quadrant_to_right_angle(self.quadrant) -end - -- return quadrant tangent right (forward) unit vector function player_char:get_quadrant_right() return dir_vectors[rotate_dir_90_ccw(self.quadrant)] @@ -145,7 +139,7 @@ end -- this is a forward transformation, and therefore useful for intention (ground motion) function player_char:quadrant_rotated(v) --[[#pico8 - return v:rotated(self:get_quadrant_right_angle()) + return v:rotated(world.quadrant_to_right_angle(self.quadrant)) --#pico8]] --#if busted -- native Lua's floating point numbers cause small precision errors with cos/sin @@ -387,12 +381,14 @@ end -- around the sensor_position's qy, so it's easy to know if the character can q-step up/down, -- and so that it's meaningful to check for q-ceiling obstacles after the character did his best to step -- the test should be tile-insensitive so it is possible to detect q-step up/down in vertical-neighboring tiles + function player_char:_compute_signed_distance_to_closest_ground(sensor_position) - assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_compute_signed_distance_to_closest_ground: sensor_position qx must be floored") + local quadrant = self.quadrant + + assert(world.get_quadrant_x_coord(sensor_position, quadrant) % 1 == 0, "player_char:_compute_signed_distance_to_closest_ground: sensor_position qx must be floored") local quadrant_down = self:get_quadrant_down() - local quadrant = self.quadrant -- we used to flr sensor_position.y (would now be qy) at this point, -- but actually collision checks don't mind the fractions @@ -445,7 +441,7 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- 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 column - local signed_distance_to_closest_ground = world.sub_qy(current_tile_qbottom, world.get_quadrant_y_coord(sensor_position, quadrant), self.quadrant) - qcolumn_height + local signed_distance_to_closest_ground = world.sub_qy(current_tile_qbottom, world.get_quadrant_y_coord(sensor_position, quadrant), quadrant) - qcolumn_height if signed_distance_to_closest_ground < -pc_data.max_ground_escape_height then -- ground found, but character is too deep inside to snap q-up -- return edge case (-pc_data.max_ground_escape_height - 1, 0) @@ -740,6 +736,7 @@ function player_char:_compute_ground_motion_result() false, false ) + end -- from here we will be considering positions, velocities, angles relatively @@ -775,7 +772,6 @@ function player_char:_compute_ground_motion_result() -- 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 - local quadrant_horizontal_dir = signed_speed_to_dir(self.ground_speed) -- initialise result with floored coords, it's not to easily visualize -- pixel by pixel motion at integer coordinates (we will reinject subpixels @@ -796,20 +792,21 @@ function player_char:_compute_ground_motion_result() false ) - local qx = world.get_quadrant_x_coord(self.position, self.quadrant) + local 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 - self:get_quadrant_right_angle()) + 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() - -- tokens: if get_quadrant_right_angle is not used elsewhere, consider the version below - -- which is more lengthy but doesn't require get_quadrant_right_angle definition so ultimately more compact + -- 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, self.quadrant) + 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) @@ -828,7 +825,7 @@ function player_char:_compute_ground_motion_result() 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, self.quadrant) + 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 @@ -857,7 +854,7 @@ function player_char:_compute_ground_motion_result() -- 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, self.quadrant) + world.set_position_quadrant_x(motion_result.position, qx + projected_velocity_qx, quadrant) end end end @@ -904,7 +901,6 @@ function player_char:_next_ground_step(quadrant_horizontal_dir, ref_motion_resul -- 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 @@ -1005,9 +1001,11 @@ function player_char:_is_column_blocked_by_ceiling_at(sensor_position) -- maybe not enough to factorize, but check its comments to understand better how we -- iterate over tiles - assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_is_column_blocked_by_ceiling_at: sensor_position qx must be floored") + local quadrant = self.quadrant + + assert(world.get_quadrant_x_coord(sensor_position, quadrant) % 1 == 0, "player_char:_is_column_blocked_by_ceiling_at: sensor_position qx must be floored") - local quadrant_opp = oppose_dir(self.quadrant) + local quadrant_opp = oppose_dir(quadrant) local quadrant_up = dir_vectors[quadrant_opp] -- top must be q-above bottom or we will get stuck in infinite loop @@ -1018,7 +1016,9 @@ function player_char:_is_column_blocked_by_ceiling_at(sensor_position) -- 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_signed_distance_to_closest_ground) - local ceiling_detection_zone_qbottom = sensor_position + (pc_data.max_ground_escape_height + 1) * quadrant_up + -- ridiculous intermediate variable to work around luamin bug a + (b + c) * d => a + b + c * d + local max_ground_escape_height_plus1 = pc_data.max_ground_escape_height + 1 + local ceiling_detection_zone_qbottom = sensor_position + max_ground_escape_height_plus1 * quadrant_up -- define sensor tile location and tile iteration bounds local sensor_tile_loc = sensor_position:to_location() @@ -1029,7 +1029,7 @@ function player_char:_is_column_blocked_by_ceiling_at(sensor_position) -- last tile to iterate on (at q-top) local last_tile_loc = ceiling_detection_zone_qtop:to_location() - local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - sensor_location_topleft, self.quadrant) -- from 0 to tile_size - 1 + local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - sensor_location_topleft, quadrant) -- from 0 to tile_size - 1 -- we iterate on tiles along quadrant up, so just convert it to tile_vector -- to allow step addition @@ -1058,7 +1058,7 @@ function player_char:_is_column_blocked_by_ceiling_at(sensor_position) if qcolumn_height > 0 then local head_top_position = sensor_position + vector(0, -self:get_full_height()) - local signed_distance_to_closest_ceiling = world.sub_qy(current_tile_qtop, world.get_quadrant_y_coord(head_top_position, self.quadrant), quadrant_opp) - qcolumn_height + local signed_distance_to_closest_ceiling = world.sub_qy(current_tile_qtop, world.get_quadrant_y_coord(head_top_position, quadrant), quadrant_opp) - qcolumn_height if signed_distance_to_closest_ceiling < 0 then -- head (or feet) inside ceiling return true diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 2062362d..02727bd7 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -4,6 +4,7 @@ local animated_sprite = require("engine/render/animated_sprite") local player_char = require("ingame/playercharacter") local input = require("engine/input/input") local motion = require("platformer/motion") +local world = require("platformer/world") local ground_query_info = motion.ground_query_info local pc_data = require("data/playercharacter_data") local tile_test_data = require("test_data/tile_test_data") @@ -250,33 +251,6 @@ describe('player_char', function () end) - describe('get_quadrant_right_angle', function () - - -- we had already written utests before extracting world.quadrant_to_right_angle - -- so we kept the tests checking final result instead of spy call - - it('should return 0 when quadrant is down', function () - pc.quadrant = directions.down - assert.are_equal(0, pc:get_quadrant_right_angle()) - end) - - it('should return 0.25 when quadrant is right', function () - pc.quadrant = directions.right - assert.are_equal(0.25, pc:get_quadrant_right_angle()) - end) - - it('should return 0.5 when quadrant is up', function () - pc.quadrant = directions.up - assert.are_equal(0.5, pc:get_quadrant_right_angle()) - end) - - it('should return 0.75 when quadrant is left', function () - pc.quadrant = directions.left - assert.are_equal(0.75, pc:get_quadrant_right_angle()) - end) - - end) - describe('get_quadrant_right', function () it('should return vector(1, 0) when quadrant is down', function () @@ -2805,7 +2779,7 @@ describe('player_char', 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 - motion_result.slope_angle = (self:get_quadrant_right_angle() - 0.01) % 1 + motion_result.slope_angle = (world.quadrant_to_right_angle(self.quadrant) - 0.01) % 1 end) end) @@ -2960,7 +2934,7 @@ describe('player_char', function () motion_result.is_blocked = true else motion_result.position = motion_result.position + step_vec - motion_result.slope_angle = (self:get_quadrant_right_angle() + 0.01) % 1 + motion_result.slope_angle = (world.quadrant_to_right_angle(self.quadrant) + 0.01) % 1 end end) end) @@ -3542,7 +3516,7 @@ describe('player_char', function () end) -- _compute_ground_motion_result - describe('_next_ground_step', function () + describe('#solo _next_ground_step', function () -- for these utests, we assume that _compute_ground_sensors_signed_distance and -- _is_blocked_by_ceiling are correct, From 1d22306a8c0a1f1c19bdb9f569efaf06c330301a Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 22 Aug 2020 13:10:41 +0200 Subject: [PATCH 095/112] [TOKENS] -> 9057 Factorized _compute_signed_distance_to_closest_ground and _is_column_blocked_by_ceiling_at by extracting iterate_over_collision_tiles for common tile iteration block --- analysis/analysis_tokens_engine+game.py | 20 +- src/data/tile_collision_data.lua | 4 +- src/data/tile_collision_data_utest.lua | 20 ++ src/ingame/playercharacter.lua | 253 +++++++++++++----------- src/ingame/playercharacter_utest.lua | 36 +++- src/platformer/world.lua | 2 + 6 files changed, 204 insertions(+), 131 deletions(-) diff --git a/analysis/analysis_tokens_engine+game.py b/analysis/analysis_tokens_engine+game.py index c92d28ce..30c8e131 100644 --- a/analysis/analysis_tokens_engine+game.py +++ b/analysis/analysis_tokens_engine+game.py @@ -365,10 +365,10 @@ playercharacter.lua (playercharacter.p8) by analyze_script - version: 16 -- lines: 1396 -- chars: 72440 -- tokens: 2871 -- compressed chars: 26903 +- lines: 1401 +- chars: 73541 +- tokens: 2805 +- compressed chars: 27471 stage_state.lua (stage_state.p8) by analyze_script @@ -413,10 +413,10 @@ world.lua (world.p8) by analyze_script - version: 16 -- lines: 242 -- chars: 10480 +- lines: 240 +- chars: 10206 - tokens: 488 -- compressed chars: 3993 +- compressed chars: 3839 motion.lua (motion.p8) by analyze_script @@ -461,10 +461,10 @@ tile_collision_data.lua (tile_collision_data.p8) by analyze_script - version: 16 -- lines: 224 -- chars: 10589 +- lines: 225 +- chars: 10658 - tokens: 525 -- compressed chars: 3715 +- compressed chars: 3732 playercharacter_data.lua (playercharacter_data.p8) by analyze_script diff --git a/src/data/tile_collision_data.lua b/src/data/tile_collision_data.lua index 6997a442..be300005 100644 --- a/src/data/tile_collision_data.lua +++ b/src/data/tile_collision_data.lua @@ -78,7 +78,9 @@ end -- return tuple (interior_v, interior_h) for a slope angle function tile_collision_data.slope_angle_to_interiors(slope_angle) - local interior_v = (slope_angle < 0.25 or slope_angle > 0.75) and vertical_dirs.down or vertical_dirs.up + assert(slope_angle % 1 == slope_angle) + -- in edge cases (square angles), interior direction is arbitrary + local interior_v = (slope_angle < 0.25 or slope_angle >= 0.75) 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 diff --git a/src/data/tile_collision_data_utest.lua b/src/data/tile_collision_data_utest.lua index 6c645797..09b6542d 100644 --- a/src/data/tile_collision_data_utest.lua +++ b/src/data/tile_collision_data_utest.lua @@ -114,6 +114,26 @@ describe('tile_collision_data', function () describe('slope_angle_to_interiors', function () + -- cardinals + + it('should return down, right (edge case) for 0', function () + assert.are_same({vertical_dirs.down, horizontal_dirs.right}, {tile_collision_data.slope_angle_to_interiors(0)}) + end) + + it('should return up (edge case), right for 0.25', function () + assert.are_same({vertical_dirs.up, horizontal_dirs.right}, {tile_collision_data.slope_angle_to_interiors(0.25)}) + end) + + it('should return up, left (edge case) for 0.5', function () + assert.are_same({vertical_dirs.up, horizontal_dirs.left}, {tile_collision_data.slope_angle_to_interiors(0.5)}) + end) + + it('should return down (edge case), left for 0.75', function () + assert.are_same({vertical_dirs.down, horizontal_dirs.left}, {tile_collision_data.slope_angle_to_interiors(0.75)}) + end) + + -- diagonals + it('should return a down, right for bottom-right tile', function () assert.are_same({vertical_dirs.down, horizontal_dirs.right}, {tile_collision_data.slope_angle_to_interiors(atan2(8, -4))}) end) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 9ac828e8..3ff4138e 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -371,6 +371,114 @@ function player_char:_get_ground_sensor_position_from(center_position, quadrant_ return qx_floored_bottom_center + offset_qx_vector end +-- helper method for _compute_signed_distance_to_closest_ground and _is_blocked_by_ceiling_at +-- it iterates over tiles from start to last, providing distance from sensor_position (foot or head) +-- to q-column q-top (with reverse support) to a custom callback +-- pass it a quadrant of interest (direction used to check collisions), iteration start and last tile locations +local function iterate_over_collision_tiles(collision_check_quadrant, start_tile_loc, last_tile_loc, sensor_position, collider_distance_callback, no_collider_callback, ignore_reverse_on_start_tile) + assert(world.get_quadrant_x_coord(sensor_position, collision_check_quadrant) % 1 == 0, "iterate_over_collision_tiles: sensor_position qx must be floored") + + local collision_check_quadrant_down = dir_vectors[collision_check_quadrant] + + -- we iterate on tiles along quadrant down, so just convert it to tile_vector + -- to allow step addition + local tile_loc_step = tile_vector(collision_check_quadrant_down.x, collision_check_quadrant_down.y) + local start_tile_topleft = start_tile_loc:to_topleft_position() + + -- start iteration from start_tile_loc + local curr_tile_loc = start_tile_loc:copy() + + -- we *always* iterate on columns from left to right, rows from top to bottom, + -- and columns/rows are stored exactly like that in collision data (not CCW or anything) + -- so unlike other operations, the subtraction from topleft (combined with qx coord) is correct + -- to get column index for qcolumn height later, without the need to quadrant-rotate vectors first + -- note that we use start_tile_topleft instead of the sensor_position:to_location():to_topleft_position() + -- they may differ on qy (ceiling iteration starts a little higher than sensor position) + -- but they have the same qx, so the operation is valid, and equivalent to using sensor location topleft, + -- but with fewer tokens as we don't need the extra conversion + local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - start_tile_topleft, collision_check_quadrant) -- from 0 to tile_size - 1 + + -- keep looping until callback is satisfied (in general we found a collision or neary ground) + -- or we've reached the last tile + while true do + -- Ceiling ignore reverse full tiles on first tile. Comment from _is_column_blocked_by_ceiling_at + -- before extracting iterate_over_collision_tiles + -- on the first tile, we don't cannot really be blocked by a ground + -- with the same interior direction as quadrant <=> opposite to quadrant_opp + -- (imagine Sonic standing on a half-tile; this definitely cannot be ceiling) + -- so we do not consider the reverse collision with full tile_size q-height with them + -- if you're unsure, try to force-set this to false and you'll see utests like + -- '(1 ascending slope 45) should return false for sensor position on the left of the tile' + -- failing + local ignore_reverse = ignore_reverse_on_start_tile and start_tile_loc == curr_tile_loc + + -- get q-bottom of tile to compare heights easily later + -- 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_tile_loc, collision_check_quadrant) + + -- check for ground (by q-column) in currently checked tile, at sensor qX + local qcolumn_height, slope_angle = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0, collision_check_quadrant, ignore_reverse) + + -- 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 + -- 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 + -- signed distance to closest ground/ceiling is positive when q-above ground/q-below ceiling + -- 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 + 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 + -- let caller decide how to handle the presence of collider + local result = collider_distance_callback(signed_distance_to_closest_collider, slope_angle) + + -- we cannot 2x return from a called function directly, so instead, we check if a result was returned + -- if so, we return from the caller + if result then + return result + end + + -- else (can only happen in _compute_signed_distance_to_closest_ground): ground has been found, but it is too far below character's q-feet + -- to snap q-down. This can only happen on the last tile we iterate on + -- (since it was computed to be at the snap q-down limit), + -- which means we will enter the "end of iteration" block below + assert(curr_tile_loc == last_tile_loc) + end + + if curr_tile_loc == last_tile_loc then + -- let caller decide how to handle the end of iteration without finding any collider + return no_collider_callback() + end + + curr_tile_loc = curr_tile_loc + tile_loc_step + end +end + +-- actual body of _compute_signed_distance_to_closest_ground passed to iterate_over_collision_tiles +-- return nil if no clear result and we must continue to iterate (until the last tile) +local function ground_check_collider_distance_callback(signed_distance_to_closest_ground, slope_angle) + if signed_distance_to_closest_ground < -pc_data.max_ground_escape_height then + -- ground found, but character is too deep inside to snap q-up + -- return edge case (-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... + return motion.ground_query_info(-pc_data.max_ground_escape_height - 1, 0) + 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 + return motion.ground_query_info(signed_distance_to_closest_ground, slope_angle) + end +end + +-- actual body of _compute_signed_distance_to_closest_ground passed to iterate_over_collision_tiles +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(pc_data.max_ground_snap_height + 1, nil) +end + -- return (signed_distance, slope_angle) where: -- - signed distance to closest ground from sensor_position, -- either negative when (in abs, penetration height, clamped to max_ground_escape_height+1) @@ -403,73 +511,17 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) local snap_zone_qbottom = sensor_position + pc_data.max_ground_snap_height * quadrant_down -- start at the top, remember the bottom to end the iteration - local curr_tile_loc = snap_zone_qtop:to_location() - local sensor_location_topleft = curr_tile_loc:to_topleft_position() + local start_tile_loc = snap_zone_qtop:to_location() + local sensor_location_topleft = start_tile_loc:to_topleft_position() -- last tile to iterate on (at q-bottom) local last_tile_loc = snap_zone_qbottom:to_location() - -- we *always* iterate on columns from left to right, rows from top to bottom, - -- and columns/rows are stored exactly like that in collision data (not CCW or anything) - -- so unlike other operations, the subtraction from topleft (combined with qx coord) is correct - -- to get column index for qcolumn height later, without the need to quadrant-rotate vectors first - local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - sensor_location_topleft, quadrant) -- from 0 to tile_size - 1 - -- we iterate on tiles along quadrant down, so just convert it to tile_vector -- to allow step addition local tile_loc_step = tile_vector(quadrant_down.x, quadrant_down.y) - -- keep looping until we find ground to snap up/down, or ground too far for that, - -- or we've reached the last tile (too low to snap down) - while true do - - local curr_tile_j = world.get_quadrant_j_coord(curr_tile_loc, quadrant) - - -- get q-bottom of tile to compare heights easily later - local current_tile_qbottom = world.get_tile_qbottom(curr_tile_loc, quadrant) - - -- check for ground (by q-column) in currently checked tile, sensor qX - local qcolumn_height, slope_angle = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0, quadrant) - - -- 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 - -- 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 - -- signed distance to closest ground is positive when above ground - -- 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 column - local signed_distance_to_closest_ground = world.sub_qy(current_tile_qbottom, world.get_quadrant_y_coord(sensor_position, quadrant), quadrant) - qcolumn_height - if signed_distance_to_closest_ground < -pc_data.max_ground_escape_height then - -- ground found, but character is too deep inside to snap q-up - -- return edge case (-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... - return motion.ground_query_info(-pc_data.max_ground_escape_height - 1, 0) - 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 - return motion.ground_query_info(signed_distance_to_closest_ground, slope_angle) - end - -- else: ground has been found, but it is too far below character's q-feet - -- to snap q-down. This can only happen on the last tile we iterate on - -- (since it was computed to be at the snap q-down limit), - -- which means we will enter the "end of iteration" block below - assert(curr_tile_loc == last_tile_loc) - end - - if curr_tile_loc == last_tile_loc then - -- 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(pc_data.max_ground_snap_height + 1, nil) - end - - curr_tile_loc = curr_tile_loc + tile_loc_step - end - + return iterate_over_collision_tiles(quadrant, start_tile_loc, last_tile_loc, sensor_position, ground_check_collider_distance_callback, ground_check_no_collider_callback) end -- verifies if character is inside ground, and push him upward outside if inside but not too deep inside @@ -989,6 +1041,26 @@ function player_char:_is_blocked_by_ceiling_at(center_position) return false end + +-- actual body of _is_column_blocked_by_ceiling_at passed to iterate_over_collision_tiles +-- return nil if no clear result and we must continue to iterate (until the last tile) +-- slope_angle is not used, so we aggressively remove it to gain 1 token +local function ceiling_check_collider_distance_callback(signed_distance_to_closest_ceiling) --, slope_angle) + if signed_distance_to_closest_ceiling < 0 then + -- head (or feet) inside ceiling + return true + else + -- head far touching ceiling or has some gap from ceiling + return false + end +end + +-- actual body of _compute_signed_distance_to_closest_ceiling passed to iterate_over_collision_tiles +local function ceiling_check_no_collider_callback() + -- end of iteration, and no ceiling found + return false +end + -- return true iff there is a ceiling above in the column of sensor_position, in a tile above -- sensor_position's tile, within a height lower than a character's height -- note that we return true even if the detected obstacle is lower than one step up's height, @@ -997,15 +1069,9 @@ end -- so the step up itself will be ignored (e.g. when moving from a flat ground to an ascending slope) function player_char:_is_column_blocked_by_ceiling_at(sensor_position) - -- a good part of the code is similar to _compute_signed_distance_to_closest_ground - -- maybe not enough to factorize, but check its comments to understand better how we - -- iterate over tiles - - local quadrant = self.quadrant - - assert(world.get_quadrant_x_coord(sensor_position, quadrant) % 1 == 0, "player_char:_is_column_blocked_by_ceiling_at: sensor_position qx must be floored") + assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_is_column_blocked_by_ceiling_at: sensor_position qx must be floored") - local quadrant_opp = oppose_dir(quadrant) + local quadrant_opp = oppose_dir(self.quadrant) local quadrant_up = dir_vectors[quadrant_opp] -- top must be q-above bottom or we will get stuck in infinite loop @@ -1022,61 +1088,14 @@ function player_char:_is_column_blocked_by_ceiling_at(sensor_position) -- define sensor tile location and tile iteration bounds local sensor_tile_loc = sensor_position:to_location() - local curr_tile_loc = ceiling_detection_zone_qbottom:to_location() - local start_tile_loc = curr_tile_loc:copy() - local sensor_location_topleft = curr_tile_loc:to_topleft_position() + local start_tile_loc = ceiling_detection_zone_qbottom:to_location() -- last tile to iterate on (at q-top) local last_tile_loc = ceiling_detection_zone_qtop:to_location() - local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - sensor_location_topleft, quadrant) -- from 0 to tile_size - 1 - - -- we iterate on tiles along quadrant up, so just convert it to tile_vector - -- to allow step addition - local tile_loc_step = tile_vector(quadrant_up.x, quadrant_up.y) - - -- iterate from bottom (Sonic feet) to top (Sonic head) - while true do - - -- on the first tile, we don't cannot really be blocked by a ground - -- with the same interior direction as quadrant <=> opposite to quadrant_opp - -- (imagine Sonic standing on a half-tile; this definitely cannot be ceiling) - -- so we do not consider the reverse collision with full tile_size q-height with them - -- if you're unsure, try to force-set this to false and you'll see utests like - -- '(1 ascending slope 45) should return false for sensor position on the left of the tile' - -- failing - local ignore_reverse = start_tile_loc == curr_tile_loc - - -- get q-top of tile to compare heights easily later - -- there is no get_tile_qtop, so use quadrant_opp with get_tile_qbottom - local current_tile_qtop = world.get_tile_qbottom(curr_tile_loc, quadrant_opp) - - -- check for ceiling (by q-column) in currently checked tile, sensor qX - -- make sure to mirror Y of quadrant to get q-column height seen from q-above - -- we don't need slope_angle, commented out for token reduction - local qcolumn_height--[[, _slope_angle]] = world._compute_qcolumn_height_at(curr_tile_loc, qcolumn_index0, quadrant_opp, ignore_reverse) - - if qcolumn_height > 0 then - local head_top_position = sensor_position + vector(0, -self:get_full_height()) - local signed_distance_to_closest_ceiling = world.sub_qy(current_tile_qtop, world.get_quadrant_y_coord(head_top_position, quadrant), quadrant_opp) - qcolumn_height - if signed_distance_to_closest_ceiling < 0 then - -- head (or feet) inside ceiling - return true - else - -- head far touching ceiling or has some gap from ceiling - return false - end - end - - if curr_tile_loc == last_tile_loc then - -- end of iteration, and no ceiling found - return false - end - - curr_tile_loc = curr_tile_loc + tile_loc_step - - end + local head_top_position = sensor_position + self:get_full_height() * quadrant_up + return iterate_over_collision_tiles(quadrant_opp, start_tile_loc, last_tile_loc, head_top_position, ceiling_check_collider_distance_callback, ceiling_check_no_collider_callback, --[[ignore_reverse_on_start_tile:]] true) end -- if character intends to jump, prepare jump for next frame diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index 02727bd7..ab8f1623 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -3516,7 +3516,7 @@ describe('player_char', function () end) -- _compute_ground_motion_result - describe('#solo _next_ground_step', function () + describe('_next_ground_step', function () -- for these utests, we assume that _compute_ground_sensors_signed_distance and -- _is_blocked_by_ceiling are correct, @@ -4189,20 +4189,50 @@ describe('player_char', function () assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(4, 6))) end) - it('should return true for sensor position at the bottom of the tile', function () + it('should return false 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_false(pc:_is_column_blocked_by_ceiling_at(vector(4, 8))) end) - it('should return true for sensor position 2 px below tile (so that 4px above is inside tile)', function () + it('should return false 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_false(pc:_is_column_blocked_by_ceiling_at(vector(4, 10))) end) + it('should return false for quadrant left, sensor position 5 px q-inside tile', function () + pc.quadrant = directions.left + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(3, 4))) + end) + + it('should return true for quadrant left, sensor position 6 px q-inside tile', function () + pc.quadrant = directions.left + assert.is_true(pc:_is_column_blocked_by_ceiling_at(vector(2, 4))) + end) + + it('should return false for quadrant right, sensor position 5 px q-inside tile', function () + pc.quadrant = directions.right + assert.is_false(pc:_is_column_blocked_by_ceiling_at(vector(4, 4))) + end) + + it('should return true for quadrant right, sensor position 6 px q-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_true(pc:_is_column_blocked_by_ceiling_at(vector(6, 4))) + end) + end) describe('(1 ascending slope 45)', function () diff --git a/src/platformer/world.lua b/src/platformer/world.lua index 74152343..ed5d19e9 100644 --- a/src/platformer/world.lua +++ b/src/platformer/world.lua @@ -97,6 +97,8 @@ end -- this is useful for ceiling check on character's current tile and actually matches Classic Sonic behavior better function world._compute_qcolumn_height_at(tile_location, qcolumn_index0, quadrant, ignore_reverse) + assert(0 <= qcolumn_index0 and qcolumn_index0 < 8, "world._compute_qcolumn_height_at: invalid qcolumn_index0 "..qcolumn_index0) + -- only consider valid tiles; consider there are no colliding tiles outside the map area if tile_location.i >= 0 and tile_location.i < 128 and tile_location.j >= 0 and tile_location.j < 64 then From cca920850740614deefd0af66734e707e4c0b292 Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 22 Aug 2020 15:06:35 +0200 Subject: [PATCH 096/112] [TOKENS] Moved almost everything to iterate_over_collision_tiles replacing tile location parameters with simple offsets --- src/ingame/playercharacter.lua | 81 ++++++++++++++-------------------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 3ff4138e..cefb3ade 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -372,21 +372,30 @@ function player_char:_get_ground_sensor_position_from(center_position, quadrant_ end -- helper method for _compute_signed_distance_to_closest_ground and _is_blocked_by_ceiling_at --- it iterates over tiles from start to last, providing distance from sensor_position (foot or head) --- to q-column q-top (with reverse support) to a custom callback +-- 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 a custom callback -- pass it a quadrant of interest (direction used to check collisions), iteration start and last tile locations -local function iterate_over_collision_tiles(collision_check_quadrant, start_tile_loc, last_tile_loc, sensor_position, collider_distance_callback, no_collider_callback, ignore_reverse_on_start_tile) - assert(world.get_quadrant_x_coord(sensor_position, collision_check_quadrant) % 1 == 0, "iterate_over_collision_tiles: sensor_position qx must be floored") - +local function iterate_over_collision_tiles(collision_check_quadrant, start_tile_offset_qy, last_tile_offset_qy, sensor_position_base, sensor_offset_qy, collider_distance_callback, no_collider_callback, ignore_reverse_on_start_tile) + -- get check quadrant down vector (for ceiling check, it's actually up relative to character quadrant) local collision_check_quadrant_down = dir_vectors[collision_check_quadrant] + -- 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) + + -- 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) + local start_tile_loc = (sensor_position + start_tile_offset_qy * collision_check_quadrant_down):to_location() + local last_tile_loc = (sensor_position + last_tile_offset_qy * collision_check_quadrant_down):to_location() + + -- precompute start tile topleft (we're actually only interested in sensor location topleft, + -- and both have the same qx) + local start_tile_topleft = start_tile_loc:to_topleft_position() + -- we iterate on tiles along quadrant down, so just convert it to tile_vector -- to allow step addition local tile_loc_step = tile_vector(collision_check_quadrant_down.x, collision_check_quadrant_down.y) - local start_tile_topleft = start_tile_loc:to_topleft_position() - - -- start iteration from start_tile_loc - local curr_tile_loc = start_tile_loc:copy() -- we *always* iterate on columns from left to right, rows from top to bottom, -- and columns/rows are stored exactly like that in collision data (not CCW or anything) @@ -398,6 +407,9 @@ local function iterate_over_collision_tiles(collision_check_quadrant, start_tile -- but with fewer tokens as we don't need the extra conversion local qcolumn_index0 = world.get_quadrant_x_coord(sensor_position - start_tile_topleft, collision_check_quadrant) -- from 0 to tile_size - 1 + -- start iteration from start_tile_loc + local curr_tile_loc = start_tile_loc:copy() + -- keep looping until callback is satisfied (in general we found a collision or neary ground) -- or we've reached the last tile while true do @@ -491,12 +503,7 @@ end -- the test should be tile-insensitive so it is possible to detect q-step up/down in vertical-neighboring tiles function player_char:_compute_signed_distance_to_closest_ground(sensor_position) - - local quadrant = self.quadrant - - assert(world.get_quadrant_x_coord(sensor_position, quadrant) % 1 == 0, "player_char:_compute_signed_distance_to_closest_ground: sensor_position qx must be floored") - - local quadrant_down = self:get_quadrant_down() + assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_compute_signed_distance_to_closest_ground: sensor_position qx must be floored") -- we used to flr sensor_position.y (would now be qy) at this point, -- but actually collision checks don't mind the fractions @@ -507,21 +514,7 @@ function player_char:_compute_signed_distance_to_closest_ground(sensor_position) -- from sensor + offset position (in qy) -- we are effectively finding the tiles covered (even partially) by the q-vertical segment between the edge positions -- where the character can snap up (escape) and snap down - local snap_zone_qtop = sensor_position - (pc_data.max_ground_escape_height + 1) * quadrant_down - local snap_zone_qbottom = sensor_position + pc_data.max_ground_snap_height * quadrant_down - - -- start at the top, remember the bottom to end the iteration - local start_tile_loc = snap_zone_qtop:to_location() - local sensor_location_topleft = start_tile_loc:to_topleft_position() - - -- last tile to iterate on (at q-bottom) - local last_tile_loc = snap_zone_qbottom:to_location() - - -- we iterate on tiles along quadrant down, so just convert it to tile_vector - -- to allow step addition - local tile_loc_step = tile_vector(quadrant_down.x, quadrant_down.y) - - return iterate_over_collision_tiles(quadrant, start_tile_loc, last_tile_loc, sensor_position, ground_check_collider_distance_callback, ground_check_no_collider_callback) + return iterate_over_collision_tiles(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 -- verifies if character is inside ground, and push him upward outside if inside but not too deep inside @@ -1068,34 +1061,28 @@ end -- 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:_is_column_blocked_by_ceiling_at(sensor_position) - assert(world.get_quadrant_x_coord(sensor_position, self.quadrant) % 1 == 0, "player_char:_is_column_blocked_by_ceiling_at: sensor_position qx must be floored") - local quadrant_opp = oppose_dir(self.quadrant) - local quadrant_up = dir_vectors[quadrant_opp] + -- 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) - local ceiling_detection_zone_qtop = sensor_position + self:get_full_height() * quadrant_up + -- 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_signed_distance_to_closest_ground) - -- ridiculous intermediate variable to work around luamin bug a + (b + c) * d => a + b + c * d - local max_ground_escape_height_plus1 = pc_data.max_ground_escape_height + 1 - local ceiling_detection_zone_qbottom = sensor_position + max_ground_escape_height_plus1 * quadrant_up - - -- define sensor tile location and tile iteration bounds - local sensor_tile_loc = sensor_position:to_location() - local start_tile_loc = ceiling_detection_zone_qbottom:to_location() - - -- last tile to iterate on (at q-top) - local last_tile_loc = ceiling_detection_zone_qtop:to_location() - - local head_top_position = sensor_position + self:get_full_height() * quadrant_up - return iterate_over_collision_tiles(quadrant_opp, start_tile_loc, last_tile_loc, head_top_position, ceiling_check_collider_distance_callback, ceiling_check_no_collider_callback, --[[ignore_reverse_on_start_tile:]] true) + -- 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(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 -- if character intends to jump, prepare jump for next frame From c36bce379786f1e8934b9c6272fb3b84ffe0e2bf Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 22 Aug 2020 15:32:07 +0200 Subject: [PATCH 097/112] [TOKENS] -> 9003 Fixed p8tool build by using intermediate var instead of (): syntax We lost 6 tokens but still much better than previous 9057 --- src/ingame/playercharacter.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index cefb3ade..3da68264 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -386,8 +386,12 @@ local function iterate_over_collision_tiles(collision_check_quadrant, start_tile -- 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) - local start_tile_loc = (sensor_position + start_tile_offset_qy * collision_check_quadrant_down):to_location() - local last_tile_loc = (sensor_position + last_tile_offset_qy * collision_check_quadrant_down):to_location() + -- p8tool has a bug that prevents support of (complex expression):method() syntax (although PICO-8 does support it) + -- so add an intermediate var (we lose 6 tokens total because of this) + local start_tile_repr_pos = sensor_position + start_tile_offset_qy * collision_check_quadrant_down + local start_tile_loc = start_tile_repr_pos:to_location() + local last_tile_repr_pos = sensor_position + last_tile_offset_qy * collision_check_quadrant_down + local last_tile_loc = last_tile_repr_pos:to_location() -- precompute start tile topleft (we're actually only interested in sensor location topleft, -- and both have the same qx) From 89dabeae93f21f6cf93d5da97366c24bb662cef7 Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 22 Aug 2020 19:57:46 +0200 Subject: [PATCH 098/112] [TOKENS] -> 8946 Update pico-boots to use are_same_shallow for struct equality in pico-boots Define custom equality for itest_dsl to compensate (for utests are_same only) --- pico-boots | 2 +- src/itest/itest_dsl.lua | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pico-boots b/pico-boots index b41d81bd..434ebe1f 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit b41d81bd99b3e2691ab4ecc757a62dcd1d7113bf +Subproject commit 434ebe1f7e70cef673a0cba4ced7d2b33f6d6208 diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index c1938e2d..0f51a7b8 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -417,6 +417,13 @@ function command:_init(cmd_type, args) self.args = args end +--#if busted +-- this custom equality is defined for utests only, see comment on tilemap.__eq +function command.__eq(lhs, rhs) + return lhs.type == rhs.type and are_same_shallow(lhs.args, rhs.args) +end +--#endif + -- expectation struct From 635e949e2f42945c448e1ee340b182a02af4bce5 Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 22 Aug 2020 20:05:26 +0200 Subject: [PATCH 099/112] [TOKENS] -> 8848 Strip _update_velocity_component_debug unless #cheat as only used by _update_velocity_debug --- src/ingame/playercharacter.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 3da68264..1cc7420e 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -1477,8 +1477,6 @@ function player_char:_update_velocity_debug() self:_update_velocity_component_debug "y" end ---#endif - -- update the velocity component for coordinate "x" or "y" with debug motion -- coord string "x" or "y" function player_char:_update_velocity_component_debug(coord) @@ -1495,6 +1493,8 @@ function player_char:_update_velocity_component_debug(coord) end end +--#endif + -- render the player character sprite at its current position function player_char:render() local flip_x = self.orientation == horizontal_dirs.left From 21650a5569f9be956f7c6cddd7270b3e223619f8 Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 22 Aug 2020 20:10:24 +0200 Subject: [PATCH 100/112] [TOKENS] -> 8819 Updated pico-boots to isolate enum (not required in release) Require enum in itest_dsl only --- pico-boots | 2 +- src/ingame/playercharacter.lua | 2 +- src/itest/itest_dsl.lua | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pico-boots b/pico-boots index 434ebe1f..6357bf1f 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 434ebe1f7e70cef673a0cba4ced7d2b33f6d6208 +Subproject commit 6357bf1f8261fdd468a1cb3c74352b7ef11baf08 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 1cc7420e..5a3112b6 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -1498,7 +1498,7 @@ end -- render the player character sprite at its current position function player_char:render() local flip_x = self.orientation == horizontal_dirs.left - -- for now, no snapping, follow slope a la Freedom Planet + -- for now, no snapping, follow slope a la Freedom Planet / Sonic Mania local sprite_angle = self.slope_angle self.anim_spr:render(self.position, flip_x, false, sprite_angle) end diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index 0f51a7b8..bb5ec184 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -26,6 +26,7 @@ expect gp_value_type expect a gameplay value to be equal to (...) --]] +require("engine/core/enum") require("engine/test/assertions") local integrationtest = require("engine/test/integrationtest") local itest_manager, integration_test = integrationtest.itest_manager, integrationtest.integration_test From bed2455ffeffa41e457393f56bad696733f13de7 Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 22 Aug 2020 21:26:31 +0200 Subject: [PATCH 101/112] [TEST] Added busted-only tile_collision_data equality for utestdata also updated to new tile collision data format --- src/data/tile_collision_data.lua | 7 +++++++ src/data/tile_collision_data_utest.lua | 12 +++++++++++- src/utests/utestdata.lua | 27 +++++++++++++------------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/data/tile_collision_data.lua b/src/data/tile_collision_data.lua index be300005..2172badd 100644 --- a/src/data/tile_collision_data.lua +++ b/src/data/tile_collision_data.lua @@ -35,6 +35,13 @@ function tile_collision_data:_init(height_array, width_array, slope_angle, inter self.interior_h = interior_h end +--#if p8utest +function tile_collision_data.__eq(lhs, rhs) + return are_same_shallow(lhs.height_array, rhs.height_array) and are_same_shallow(lhs.width_array, rhs.width_array) and + are_same_shallow({lhs.slope_angle, lhs.interior_v, lhs.interior_h}, {rhs.slope_angle, rhs.interior_v, rhs.interior_h}) +end +--#endif + -- return the height for a column index starting at 0, from left to right function tile_collision_data:get_height(column_index0) return self.height_array[column_index0 + 1] -- adapt 0-index to 1-index diff --git a/src/data/tile_collision_data_utest.lua b/src/data/tile_collision_data_utest.lua index 09b6542d..edf5b693 100644 --- a/src/data/tile_collision_data_utest.lua +++ b/src/data/tile_collision_data_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local tile_collision_data = require("data/tile_collision_data") local raw_tile_collision_data = require("data/raw_tile_collision_data") @@ -51,6 +51,16 @@ describe('tile_collision_data', function () end) + describe('__eq (p8utest only)', function () + + it('should compare POD members and array contents', function () + local tcd1 = tile_collision_data({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) + local tcd2 = tile_collision_data({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_equal(tcd1, tcd2) + end) + + end) + describe('get_height', function () it('should return the height at the given column index', function () diff --git a/src/utests/utestdata.lua b/src/utests/utestdata.lua index c0be4ce7..eff51284 100644 --- a/src/utests/utestdata.lua +++ b/src/utests/utestdata.lua @@ -1,7 +1,5 @@ -require("engine/test/unittest") -local tile = require("platformer/tile") -local height_array = tile.height_array -local tile_data = tile.tile_data +require("engine/test/p8utest") +local tile_collision_data = require("data/tile_collision_data") -- data to test local collision_data = require("data/collision_data") @@ -10,25 +8,26 @@ local collision_data = require("data/collision_data") -- after minification if keys are not protected with ["key"] syntax) local playercharacter_data = require("data/playercharacter_data") -check('sprite_id_location(1, 3) should have collision flag set', function (utest_name) - local sprite_id = sprite_id_location(1, 3):to_sprite_id() +check('sprite_id_location(8, 0) (loop top-left) should have collision flag set', function (utest_name) + local sprite_id = sprite_id_location(8, 0):to_sprite_id() assert(fget(sprite_id, sprite_flags.collision), "sprite_id_location(0, 4) has collision flag unset", utest_name) end) -check('sprite_id_location(1, 3) should have collision mask id set to location above, angle atan2(8, 2)', function (utest_name) - local sprite_id = sprite_id_location(1, 3):to_sprite_id() - assert(collision_data.tiles_data[sprite_id] == tile_data(sprite_id_location(1, 2), atan2(8, 2)), utest_name) +check('sprite_id_location(8, 0) (loop top-left) should have collision mask id set to location above, angle atan2(-4, 4)', function (utest_name) + local sprite_id = sprite_id_location(8, 0):to_sprite_id() + local tcd = collision_data.get_tile_collision_data(sprite_id) + assert(tcd == tile_collision_data({8, 8, 8, 8, 8, 7, 6, 5}, {8, 8, 8, 8, 8, 7, 6, 5}, atan2(-4, 4), directions.down, directions.left), utest_name) end) -check('height_array._fill_array on sprite_id_location(2, 3) should fill the array with tile mask data: full', function (utest_name) - local array = {} - height_array._fill_array(array, sprite_id_location(2, 3)) +check('tile_collision_data.read_height_array on sprite_id_location(2, 3) should return an array with tile mask data: full', function (utest_name) + local array = tile_collision_data.read_height_array(sprite_id_location(2, 3), directions.down) assert(are_same_with_message({8, 8, 8, 8, 8, 8, 8, 8}, array), utest_name) end) --- bugfix history: after switching to pink transparency, all my tiles became square blocks +-- bugfix history: +-- = after switching to pink transparency, all my tiles became square blocks -- warning: it's a proto tile, if you strip it from final build later, test another tile instead -check('= height_array._fill_array on sprite_id_location(1, 7) the array with tile mask data: descending slope 45', function (utest_name) +check('tile_collision_data.read_height_array on sprite_id_location(1, 7) return an array with tile mask data: descending slope 45', function (utest_name) local array = {} height_array._fill_array(array, sprite_id_location(1, 7)) assert(are_same_with_message({1, 2, 3, 4, 5, 6, 7, 8}, array), utest_name) From 4941620d322570e18ba3c5b1c326e798d550d087 Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 22 Aug 2020 21:27:32 +0200 Subject: [PATCH 102/112] [MODULE] Updated all scripts to new game common + game bustedhelper system Also use new p8utest convention with symbol definition --- build_pico8_utests.sh | 5 +++-- pico-boots | 2 +- src/application/picosonic_app_utest.lua | 2 +- src/common.lua | 8 ++++++++ src/data/raw_tile_collision_data_utest.lua | 2 +- src/ingame/playercharacter_utest.lua | 2 +- src/ingame/stage_state_utest.lua | 2 +- src/itest/itest_dsl_utest.lua | 12 +++++++++++- src/itest_main.lua | 1 + src/main.lua | 1 + src/menu/credits_utest.lua | 2 +- src/menu/titlemenu_utest.lua | 2 +- src/platformer/motion_utest.lua | 2 +- src/platformer/world_utest.lua | 2 +- src/test/bustedhelper.lua | 8 ++++++++ src/tests/headless_itests_utest.lua | 2 +- src/utest_main.lua | 6 +++--- 17 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 src/common.lua create mode 100644 src/test/bustedhelper.lua diff --git a/build_pico8_utests.sh b/build_pico8_utests.sh index e187db18..6e24dd73 100755 --- a/build_pico8_utests.sh +++ b/build_pico8_utests.sh @@ -18,12 +18,13 @@ title="pico-sonic pico8 utests (all)" cartridge_stem="picosonic_pico8_utests_all" version="3.1" config='debug' -symbols='assert,log' +symbols='assert,log,p8utest' # Build from itest main for all pico8 utests # Note that a pico8 utest_main build is much smaller than a normal build, # so minification is not required in general; however it is useful to spot # issues in the real build like unprotected sprite animation keys being minified +# So just set minify-level to 0-2 depending on your needs "$picoboots_scripts_path/build_cartridge.sh" \ "$game_src_path" utest_main.lua utests \ -d "$data_path/data.p8" -M "$data_path/metadata.p8" \ @@ -32,4 +33,4 @@ symbols='assert,log' -o "${cartridge_stem}_v${version}" \ -c "$config" \ -s "$symbols" \ - --minify-level 2 + --minify-level 1 diff --git a/pico-boots b/pico-boots index 6357bf1f..8f8472c7 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 6357bf1f8261fdd468a1cb3c74352b7ef11baf08 +Subproject commit 8f8472c78a6c0348c682b9038d3bff6c6cc03e61 diff --git a/src/application/picosonic_app_utest.lua b/src/application/picosonic_app_utest.lua index 5d179895..29aae6b7 100644 --- a/src/application/picosonic_app_utest.lua +++ b/src/application/picosonic_app_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local picosonic_app = require("application/picosonic_app") local flow = require("engine/application/flow") diff --git a/src/common.lua b/src/common.lua new file mode 100644 index 00000000..2d65429f --- /dev/null +++ b/src/common.lua @@ -0,0 +1,8 @@ +-- Require all common game 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 game. +-- Usage: add require("common") at the top of each of your main scripts +-- (along with "engine/common") and in bustedhelper (after pico8api) + +require("engine/core/direction_ext") +require("engine/core/vector_ext") diff --git a/src/data/raw_tile_collision_data_utest.lua b/src/data/raw_tile_collision_data_utest.lua index 443bfce3..b73e6bca 100644 --- a/src/data/raw_tile_collision_data_utest.lua +++ b/src/data/raw_tile_collision_data_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local raw_tile_collision_data = require("data/raw_tile_collision_data") describe('raw_tile_collision_data', function () diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index ab8f1623..c9384265 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local animated_sprite = require("engine/render/animated_sprite") local player_char = require("ingame/playercharacter") diff --git a/src/ingame/stage_state_utest.lua b/src/ingame/stage_state_utest.lua index faac04a1..e82d9748 100644 --- a/src/ingame/stage_state_utest.lua +++ b/src/ingame/stage_state_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local stage_state = require("ingame/stage_state") local flow = require("engine/application/flow") diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua index c5d21beb..85596521 100644 --- a/src/itest/itest_dsl_utest.lua +++ b/src/itest/itest_dsl_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local flow = require("engine/application/flow") local tilemap = require("engine/data/tilemap") local input = require("engine/input/input") @@ -421,6 +421,16 @@ describe('itest_dsl', function () end) + describe('__eq (busted only)', function () + + it('should compare POD members and array contents', function () + local cmd1 = command(command_types.move, {horizontal_dirs.left}) + local cmd2 = command(command_types.move, {horizontal_dirs.left}) + assert.are_equal(cmd1, cmd2) + end) + + end) + describe('expectation', function () describe('_init', function () diff --git a/src/itest_main.lua b/src/itest_main.lua index a6e8057a..5fdc2df5 100644 --- a/src/itest_main.lua +++ b/src/itest_main.lua @@ -3,6 +3,7 @@ -- must require at main top, to be used in any required modules from here require("engine/pico8/api") require("engine/common") +require("common") local integrationtest = require("engine/test/integrationtest") local itest_manager = integrationtest.itest_manager diff --git a/src/main.lua b/src/main.lua index dc050d2c..33fef94e 100644 --- a/src/main.lua +++ b/src/main.lua @@ -3,6 +3,7 @@ -- must require at main top, to be used in any required modules from here require("engine/pico8/api") require("engine/common") +require("common") -- we also require codetuner so any file can used tuned() -- if tuner symbol is defined, then we also initialize it in _init diff --git a/src/menu/credits_utest.lua b/src/menu/credits_utest.lua index a66f9d50..b9ab54c6 100644 --- a/src/menu/credits_utest.lua +++ b/src/menu/credits_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local picosonic_app = require("application/picosonic_app") local credits = require("menu/credits") diff --git a/src/menu/titlemenu_utest.lua b/src/menu/titlemenu_utest.lua index 7fae05a9..846e530b 100644 --- a/src/menu/titlemenu_utest.lua +++ b/src/menu/titlemenu_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local input = require("engine/input/input") local flow = require("engine/application/flow") local gamestate = require("engine/application/gamestate") diff --git a/src/platformer/motion_utest.lua b/src/platformer/motion_utest.lua index 6ff21900..8668fd2b 100644 --- a/src/platformer/motion_utest.lua +++ b/src/platformer/motion_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local motion = require("platformer/motion") local ground_query_info = motion.ground_query_info local ground_motion_result, air_motion_result = get_members(motion, diff --git a/src/platformer/world_utest.lua b/src/platformer/world_utest.lua index 13ac98b4..2c15bfef 100644 --- a/src/platformer/world_utest.lua +++ b/src/platformer/world_utest.lua @@ -1,4 +1,4 @@ -require("engine/test/bustedhelper") +require("test/bustedhelper") local world = require("platformer/world") local tile_test_data = require("test_data/tile_test_data") diff --git a/src/test/bustedhelper.lua b/src/test/bustedhelper.lua new file mode 100644 index 00000000..8f5074b2 --- /dev/null +++ b/src/test/bustedhelper.lua @@ -0,0 +1,8 @@ +-- engine bustedhelper equivalent for game project +-- it simply adds game common module, since the original bustedhelper.lua +-- is part of engine and therefore cannot reference game modules +-- Usage: +-- in your game utests, always require("test/bustedhelper") at the top +-- instead of "engine/test/bustedhelper" +require("engine/test/bustedhelper") +require("common") diff --git a/src/tests/headless_itests_utest.lua b/src/tests/headless_itests_utest.lua index cefd5701..3f57b779 100644 --- a/src/tests/headless_itests_utest.lua +++ b/src/tests/headless_itests_utest.lua @@ -1,5 +1,5 @@ -- todo: use busted --helper=.../bustedhelper instead of all the bustedhelper requires! -require("engine/test/bustedhelper") +require("test/bustedhelper") require("engine/test/headless_itest") require("engine/test/integrationtest") local logging = require("engine/debug/logging") diff --git a/src/utest_main.lua b/src/utest_main.lua index 2f272208..ef40069d 100644 --- a/src/utest_main.lua +++ b/src/utest_main.lua @@ -6,9 +6,9 @@ -- must require at main top, to be used in any required modules from here require("engine/pico8/api") require("engine/common") +require("common") -local unittest = require("engine/test/unittest") -local utest_manager, unit_test = unittest.utest_manager, unittest.unit_test +local p8utest = require("engine/test/p8utest") -- tag to add require for pico8 utests files (should be in utests/) --[[add_require]] @@ -18,5 +18,5 @@ logging.logger:register_stream(logging.console_log_stream) --#endif function _init() - utest_manager:run_all_tests() + p8utest.utest_manager:run_all_tests() end From 7b481fe3d4263a2d9b94dc06e68a5725fe46f032 Mon Sep 17 00:00:00 2001 From: huulong Date: Sat, 22 Aug 2020 21:31:24 +0200 Subject: [PATCH 103/112] [TEST] Fixed missing ".0" in itest_dsl expectated message since quadrant feature --- src/itest/itest_dsl_utest.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua index 85596521..5e5e786b 100644 --- a/src/itest/itest_dsl_utest.lua +++ b/src/itest/itest_dsl_utest.lua @@ -877,8 +877,10 @@ expect -- 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 + -- since we added quadrants, even integer coordinates receive float transformation, + -- hence the .0 on passed position 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, 45)\nExpected:\nvector(10, 45)\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" assert.are_same({false, expected_message}, {test.final_assertion()}) From 7f496eb8894070d1149371b528ffe69069fdc636 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 23 Aug 2020 19:44:17 +0200 Subject: [PATCH 104/112] [TEST] Removed custom struct __eq to match new convention Struct __eq remains shallow, but utests will manually check raw content --- pico-boots | 2 +- src/data/tile_collision_data.lua | 7 ------ src/data/tile_collision_data_utest.lua | 10 -------- src/itest/itest_dsl.lua | 7 ------ src/itest/itest_dsl_utest.lua | 32 +++++++++----------------- 5 files changed, 12 insertions(+), 46 deletions(-) diff --git a/pico-boots b/pico-boots index 8f8472c7..81f4f0ac 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 8f8472c78a6c0348c682b9038d3bff6c6cc03e61 +Subproject commit 81f4f0aca3980078852c4aab9ad3df4caddc21a9 diff --git a/src/data/tile_collision_data.lua b/src/data/tile_collision_data.lua index 2172badd..be300005 100644 --- a/src/data/tile_collision_data.lua +++ b/src/data/tile_collision_data.lua @@ -35,13 +35,6 @@ function tile_collision_data:_init(height_array, width_array, slope_angle, inter self.interior_h = interior_h end ---#if p8utest -function tile_collision_data.__eq(lhs, rhs) - return are_same_shallow(lhs.height_array, rhs.height_array) and are_same_shallow(lhs.width_array, rhs.width_array) and - are_same_shallow({lhs.slope_angle, lhs.interior_v, lhs.interior_h}, {rhs.slope_angle, rhs.interior_v, rhs.interior_h}) -end ---#endif - -- return the height for a column index starting at 0, from left to right function tile_collision_data:get_height(column_index0) return self.height_array[column_index0 + 1] -- adapt 0-index to 1-index diff --git a/src/data/tile_collision_data_utest.lua b/src/data/tile_collision_data_utest.lua index edf5b693..cbfd91cb 100644 --- a/src/data/tile_collision_data_utest.lua +++ b/src/data/tile_collision_data_utest.lua @@ -51,16 +51,6 @@ describe('tile_collision_data', function () end) - describe('__eq (p8utest only)', function () - - it('should compare POD members and array contents', function () - local tcd1 = tile_collision_data({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) - local tcd2 = tile_collision_data({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_equal(tcd1, tcd2) - end) - - end) - describe('get_height', function () it('should return the height at the given column index', function () diff --git a/src/itest/itest_dsl.lua b/src/itest/itest_dsl.lua index bb5ec184..0fc20f9c 100644 --- a/src/itest/itest_dsl.lua +++ b/src/itest/itest_dsl.lua @@ -418,13 +418,6 @@ function command:_init(cmd_type, args) self.args = args end ---#if busted --- this custom equality is defined for utests only, see comment on tilemap.__eq -function command.__eq(lhs, rhs) - return lhs.type == rhs.type and are_same_shallow(lhs.args, rhs.args) -end ---#endif - -- expectation struct diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua index 5e5e786b..52161c34 100644 --- a/src/itest/itest_dsl_utest.lua +++ b/src/itest/itest_dsl_utest.lua @@ -421,16 +421,6 @@ describe('itest_dsl', function () end) - describe('__eq (busted only)', function () - - it('should compare POD members and array contents', function () - local cmd1 = command(command_types.move, {horizontal_dirs.left}) - local cmd2 = command(command_types.move, {horizontal_dirs.left}) - assert.are_equal(cmd1, cmd2) - end) - - end) - describe('expectation', function () describe('_init', function () @@ -563,7 +553,7 @@ expect -- interface assert.is_not_nil(dsli) - assert.are_same( + assert.is_true(are_same_with_message( { -- no ':stage' here, it's still interpret as plain text at this point '@stage', @@ -582,7 +572,7 @@ expect dsli.stage_name, dsli.tilemap, dsli.commands - }) + })) end) end) @@ -656,7 +646,7 @@ expect local gamestate_type, stage_name, tm, next_line_index = itest_dsl_parser.parse_gamestate_definition(dsli_lines) -- interface - assert.are_same( + assert.is_true(are_same_with_message( { ':stage', '#', @@ -671,7 +661,7 @@ expect stage_name, tm, next_line_index - }) + })) end) end) @@ -691,12 +681,12 @@ expect ".... (ignored)" } local tm, next_line_index = itest_dsl_parser.parse_tilemap(tilemap_text) - assert.are_same( + assert.is_true(are_same_with_message( { tilemap({}), 3 }, - {tm, next_line_index}) + {tm, next_line_index})) end) it('should return a tilemap data with tiles corresponding to the tile symbols in the string', function () @@ -710,7 +700,7 @@ expect "(ignored)" } local tm, next_line_index = itest_dsl_parser.parse_tilemap(tilemap_text) - assert.are_same( + assert.is_true(are_same_with_message( { tilemap({ { 0, 0, 0, 0}, @@ -719,7 +709,7 @@ expect }), 6 }, - {tm, next_line_index}) + {tm, next_line_index})) end) it('should assert if there as fewer than 2 lines', function () @@ -766,7 +756,7 @@ expect describe('parse_action_sequence', function () - it('should return a sequence of commands read in lines, starting at next_line_index', function () + it('#solo should return a sequence of commands read in lines, starting at next_line_index', function () local dsli_lines = { "???", "???", @@ -780,7 +770,7 @@ expect "expect pc_velocity 2 -3.5" } local commands = itest_dsl_parser.parse_action_sequence(dsli_lines, 5) - assert.are_same( + assert.is_true(are_same_with_message( { command(command_types.warp, { vector(12, 45) } ), command(command_types.wait, { 1 } ), @@ -789,7 +779,7 @@ expect command(command_types.expect, {"pc_bottom_pos", vector(10, 45)}), command(command_types.expect, {"pc_velocity", vector(2, -3.5)}), }, - commands) + commands)) end) it('should assert if an unknown command is found', function () From d05c3308d85e36af628a0766b8377d772cf9e152 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 23 Aug 2020 19:45:04 +0200 Subject: [PATCH 105/112] [TEST] Fixed collision data to match PICO-8 spritesheet utestdata now uses new assert_log, fixed utests --- src/data/collision_data.lua | 10 +++++----- src/test_data/tile_representation.lua | 4 ++-- src/test_data/tile_test_data.lua | 2 +- src/utests/utestdata.lua | 23 +++++++++++------------ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/data/collision_data.lua b/src/data/collision_data.lua index a69b5461..94c04f1d 100644 --- a/src/data/collision_data.lua +++ b/src/data/collision_data.lua @@ -98,8 +98,8 @@ sprite_flags = { 80 @ (0, 5) HALF TILE (4px high) = 96 @ (0, 6) FLAT LOW TILE (2px high) _ 64 @ (0, 4) BOTTOM-RIGHT QUARTER TILE (4px high) r - 112 @ (1, 7) ASCENDING 45 / slope_angle: 0.125 = atan2(1, -1) - 113 @ (0, 7) ASCENDING 22.5 < slope_angle: 0.0625 ~= atan2(8, -4) (actually 0.0738) but kept for historical utest/itest reasons + 112 @ (0, 7) ASCENDING 22.5 < slope_angle: 0.0625 ~= atan2(8, -4) (actually 0.0738) but kept for historical utest/itest reasons + 113 @ (1, 7) ASCENDING 45 / slope_angle: 0.125 = atan2(1, -1) 116 @ (4, 7) DESCENDING 45 \ slope_angle: 1-0.125 = atan2(1, 1) 117 @ (5, 7) higher 2:1 ascending slope (completes 58 from loop) 12 @ (12, 0) LOOP TOP-LEFT: reusing mask of loop top-left with itself @@ -169,14 +169,14 @@ local raw_tiles_data = serialization.parse_expression( [80] = {{0, 5}, {8, 0}}, [96] = {{0, 6}, {8, 0}}, [64] = {{0, 4}, {8, 0}}, - [112]= {{1, 7}, {8, -8}}, - [113]= {{0, 7}, 0.0625}, + [112]= {{0, 7}, 0.0625}, + [113]= {{1, 7}, {8, -8}}, [116]= {{4, 7}, {8, 8}}, [117]= {{5, 7}, {8, -4}}, [12]= {{12, 0}, {-4, 4}} }]], function (t) -- t[2] may be {x, y} to use for atan2 or slope_angle directly - -- this is only for [113], if we update utests/itests to use the more correct atan2(8, -4) then we can get rid of + -- this is only for [112], if we update utests/itests to use the more correct atan2(8, -4) then we can get rid of -- that ternary check return raw_tile_collision_data(sprite_id_location(t[1][1], t[1][2]), type(t[2]) == 'table' and atan2(t[2][1], t[2][2]) or t[2]) end diff --git a/src/test_data/tile_representation.lua b/src/test_data/tile_representation.lua index 48cb8d11..699bd2fc 100644 --- a/src/test_data/tile_representation.lua +++ b/src/test_data/tile_representation.lua @@ -8,9 +8,9 @@ full_tile_id = 32 half_tile_id = 80 flat_low_tile_id = 96 bottom_right_quarter_tile_id = 64 -asc_slope_45_id = 112 +asc_slope_45_id = 113 desc_slope_45_id = 116 -asc_slope_22_id = 113 +asc_slope_22_id = 112 asc_slope_22_upper_level_id = 117 loop_topleft = 12 diff --git a/src/test_data/tile_test_data.lua b/src/test_data/tile_test_data.lua index cfd318ba..c09ba511 100644 --- a/src/test_data/tile_test_data.lua +++ b/src/test_data/tile_test_data.lua @@ -15,8 +15,8 @@ local mock_raw_tile_collision_data = { [flat_low_tile_id] = {{2, 2, 2, 2, 2, 2, 2, 2}, {0, 0, 0, 0, 0, 0, 8, 8}, atan2(8, 0)}, [bottom_right_quarter_tile_id] = {{0, 0, 0, 0, 4, 4, 4, 4}, {0, 0, 0, 0, 4, 4, 4, 4}, atan2(8, 0)}, [asc_slope_45_id] = {{1, 2, 3, 4, 5, 6, 7, 8}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, -8)}, - [asc_slope_22_id] = {{2, 2, 3, 3, 4, 4, 5, 5}, {0, 0, 0, 2, 4, 6, 8, 8}, 0.0625}, [desc_slope_45_id] = {{8, 7, 6, 5, 4, 3, 2, 1}, {1, 2, 3, 4, 5, 6, 7, 8}, atan2(8, 8)}, + [asc_slope_22_id] = {{2, 2, 3, 3, 4, 4, 5, 5}, {0, 0, 0, 2, 4, 6, 8, 8}, 0.0625}, [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)}, [loop_topleft] = {{8, 8, 8, 8, 8, 7, 6, 5}, {8, 8, 8, 8, 8, 7, 6, 5}, atan2(-4, 4)}, } diff --git a/src/utests/utestdata.lua b/src/utests/utestdata.lua index eff51284..f8f490e2 100644 --- a/src/utests/utestdata.lua +++ b/src/utests/utestdata.lua @@ -10,33 +10,32 @@ local playercharacter_data = require("data/playercharacter_data") check('sprite_id_location(8, 0) (loop top-left) should have collision flag set', function (utest_name) local sprite_id = sprite_id_location(8, 0):to_sprite_id() - assert(fget(sprite_id, sprite_flags.collision), "sprite_id_location(0, 4) has collision flag unset", utest_name) + assert_log(utest_name, fget(sprite_id, sprite_flags.collision), "sprite_id_location(0, 4) has collision flag unset") end) -check('sprite_id_location(8, 0) (loop top-left) should have collision mask id set to location above, angle atan2(-4, 4)', function (utest_name) +check('sprite_id_location(8, 0) (loop top-left) should have collision arrays of loop top-left, angle atan2(-4, 4), interior up-left', function (utest_name) local sprite_id = sprite_id_location(8, 0):to_sprite_id() local tcd = collision_data.get_tile_collision_data(sprite_id) - assert(tcd == tile_collision_data({8, 8, 8, 8, 8, 7, 6, 5}, {8, 8, 8, 8, 8, 7, 6, 5}, atan2(-4, 4), directions.down, directions.left), utest_name) + assert_log(utest_name, are_same_with_message(tcd, tile_collision_data({8, 8, 8, 8, 8, 7, 6, 5}, {8, 8, 8, 8, 8, 7, 6, 5}, atan2(-4, 4), vertical_dirs.up, horizontal_dirs.left))) end) -check('tile_collision_data.read_height_array on sprite_id_location(2, 3) should return an array with tile mask data: full', function (utest_name) - local array = tile_collision_data.read_height_array(sprite_id_location(2, 3), directions.down) - assert(are_same_with_message({8, 8, 8, 8, 8, 8, 8, 8}, array), utest_name) +check('tile_collision_data.read_height_array on sprite_id_location(0, 0) should return an array with tile mask data: full', function (utest_name) + local array = tile_collision_data.read_height_array(sprite_id_location(0, 2), vertical_dirs.down) + assert_log(utest_name, are_same_with_message({8, 8, 8, 8, 8, 8, 8, 8}, array)) end) -- bugfix history: -- = after switching to pink transparency, all my tiles became square blocks -- warning: it's a proto tile, if you strip it from final build later, test another tile instead -check('tile_collision_data.read_height_array on sprite_id_location(1, 7) return an array with tile mask data: descending slope 45', function (utest_name) - local array = {} - height_array._fill_array(array, sprite_id_location(1, 7)) - assert(are_same_with_message({1, 2, 3, 4, 5, 6, 7, 8}, array), utest_name) +check('tile_collision_data.read_height_array on sprite_id_location(1, 7) return an array with tile mask data: ascending slope 45', function (utest_name) + local array = tile_collision_data.read_height_array(sprite_id_location(1, 7), vertical_dirs.down) + assert_log(utest_name, are_same_with_message({1, 2, 3, 4, 5, 6, 7, 8}, array)) end) check('sonic_sprite_data_table preserved key "idle"', function (utest_name) - assert(playercharacter_data.sonic_sprite_data_table["idle"] ~= nil, utest_name) + assert_log(utest_name, playercharacter_data.sonic_sprite_data_table["idle"] ~= nil, 'Expected playercharacter_data.sonic_sprite_data_table["idle"] to not be nil') end) check('sonic_animated_sprite_data_table preserved key "idle"', function (utest_name) - assert(playercharacter_data.sonic_animated_sprite_data_table["idle"] ~= nil, utest_name) + assert_log(utest_name, playercharacter_data.sonic_animated_sprite_data_table["idle"] ~= nil, 'Expected playercharacter_data.sonic_animated_sprite_data_table["idle"] to not be nil') end) From 7ae57251087913200550b7a6004508c63497f954 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 23 Aug 2020 20:09:09 +0200 Subject: [PATCH 106/112] [TOKENS] -> 8793 Replace tile location comparison with manual member comparison since pico-boots removed struct_eq in debug/release build Removed get_quadrant_j_coord since we eventually didn't choose to just compare tile location qj, which would be more semantic but would take more tokens This is also to prepare complete removal of struct_eq --- pico-boots | 2 +- src/ingame/playercharacter.lua | 6 ++++-- src/platformer/world.lua | 6 ------ src/platformer/world_utest.lua | 20 -------------------- 4 files changed, 5 insertions(+), 29 deletions(-) diff --git a/pico-boots b/pico-boots index 81f4f0ac..696d592a 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 81f4f0aca3980078852c4aab9ad3df4caddc21a9 +Subproject commit 696d592a663cc1eec5e8ae559b3b2d0892a44db9 diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 5a3112b6..e48e58af 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -459,10 +459,12 @@ local function iterate_over_collision_tiles(collision_check_quadrant, start_tile -- to snap q-down. This can only happen on the last tile we iterate on -- (since it was computed to be at the snap q-down limit), -- which means we will enter the "end of iteration" block below - assert(curr_tile_loc == last_tile_loc) + assert(curr_tile_loc.i == last_tile_loc.i and curr_tile_loc.j == last_tile_loc.j) end - if curr_tile_loc == last_tile_loc then + -- since we only iterate on qj, we really only care about qj (which is i when quadrant is horizontal) + -- but it costed more token to define get_quadrant_j_coord than to just compare both coords + if curr_tile_loc.i == last_tile_loc.i and curr_tile_loc.j == last_tile_loc.j then -- let caller decide how to handle the end of iteration without finding any collider return no_collider_callback() end diff --git a/src/platformer/world.lua b/src/platformer/world.lua index ed5d19e9..fada2ad6 100644 --- a/src/platformer/world.lua +++ b/src/platformer/world.lua @@ -45,12 +45,6 @@ function world.get_quadrant_y_coord(pos, quadrant) return quadrant % 2 == 1 and pos.y or pos.x end --- same, but for qj (tilemap location) -function world.get_quadrant_j_coord(pos, quadrant) - return quadrant % 2 == 1 and pos.j or pos.i -end - - -- set the horizontal coordinate of a position vector in current quadrant -- (x if down/up, y if right/left) to value function world.set_position_quadrant_x(pos, value, quadrant) diff --git a/src/platformer/world_utest.lua b/src/platformer/world_utest.lua index 2c15bfef..7c73acdd 100644 --- a/src/platformer/world_utest.lua +++ b/src/platformer/world_utest.lua @@ -133,26 +133,6 @@ describe('world (with mock tiles data setup)', function () end) - describe('get_quadrant_j_coord', function () - - it('should return loc.j when quadrant is down', function () - assert.are_equal(2, world.get_quadrant_j_coord(location(1, 2), directions.down)) - end) - - it('should return loc.j when quadrant is up', function () - assert.are_equal(2, world.get_quadrant_j_coord(location(1, 2), directions.up)) - end) - - it('should return loc.j when quadrant is right', function () - assert.are_equal(1, world.get_quadrant_j_coord(location(1, 2), directions.right)) - end) - - it('should return loc.j when quadrant is left', function () - assert.are_equal(1, world.get_quadrant_j_coord(location(1, 2), directions.left)) - end) - - end) - describe('set_position_quadrant_x', function () it('should set pos.x when quadrant is down', function () From 4bf117a83d12bfd32fc477a70c2c3d7fca7a0af9 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 23 Aug 2020 20:21:56 +0200 Subject: [PATCH 107/112] [TOKENS] Reduced tokens slightly again in (): causing p8tool issue by replacing intermediate expression with static method + self call --- src/ingame/playercharacter.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index e48e58af..653543c3 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -387,11 +387,9 @@ local function iterate_over_collision_tiles(collision_check_quadrant, start_tile -- 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) -- p8tool has a bug that prevents support of (complex expression):method() syntax (although PICO-8 does support it) - -- so add an intermediate var (we lose 6 tokens total because of this) - local start_tile_repr_pos = sensor_position + start_tile_offset_qy * collision_check_quadrant_down - local start_tile_loc = start_tile_repr_pos:to_location() - local last_tile_repr_pos = sensor_position + last_tile_offset_qy * collision_check_quadrant_down - local last_tile_loc = last_tile_repr_pos:to_location() + -- so we play on the fact that method = function bound to self and just write the .static_method(self) syntax (same token count) + local start_tile_loc = vector.to_location(sensor_position + start_tile_offset_qy * collision_check_quadrant_down) + local last_tile_loc = vector.to_location(sensor_position + last_tile_offset_qy * collision_check_quadrant_down) -- precompute start tile topleft (we're actually only interested in sensor location topleft, -- and both have the same qx) From 27c84be37461a73181853704ef08dc8339409e96 Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 23 Aug 2020 21:58:54 +0200 Subject: [PATCH 108/112] [PLAYER CHARACTER] Reverted to struct comparison start_tile_loc == curr_tile_loc since there was already another comparison, making the __eq profitable token-wise Also reduced token slightly by using vector.is_zero which was defined for __eq anyway --- pico-boots | 2 +- src/ingame/playercharacter.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pico-boots b/pico-boots index 696d592a..b65fcb14 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit 696d592a663cc1eec5e8ae559b3b2d0892a44db9 +Subproject commit b65fcb14af9e4b3902650ade46ee99340650b26d diff --git a/src/ingame/playercharacter.lua b/src/ingame/playercharacter.lua index 653543c3..bcc0f2d8 100644 --- a/src/ingame/playercharacter.lua +++ b/src/ingame/playercharacter.lua @@ -457,12 +457,12 @@ local function iterate_over_collision_tiles(collision_check_quadrant, start_tile -- to snap q-down. This can only happen on the last tile we iterate on -- (since it was computed to be at the snap q-down limit), -- which means we will enter the "end of iteration" block below - assert(curr_tile_loc.i == last_tile_loc.i and curr_tile_loc.j == last_tile_loc.j) + assert(curr_tile_loc == last_tile_loc) end -- since we only iterate on qj, we really only care about qj (which is i when quadrant is horizontal) -- but it costed more token to define get_quadrant_j_coord than to just compare both coords - if curr_tile_loc.i == last_tile_loc.i and curr_tile_loc.j == last_tile_loc.j then + if curr_tile_loc == last_tile_loc then -- let caller decide how to handle the end of iteration without finding any collider return no_collider_callback() end @@ -1212,7 +1212,7 @@ end function player_char:_compute_air_motion_result() -- 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 - if self.velocity == vector.zero() then + if self.velocity:is_zero() then return motion.air_motion_result( self.position, false, From fefe5a3cccdc0c5834f1f6043c14fb0195a1adfb Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 23 Aug 2020 21:59:51 +0200 Subject: [PATCH 109/112] [TEST] Fix remaining tests after pico-boots not defining struct eq anymore by replacing are_equal with are_same for all structs (even vector/location, just for better debug in case of inequality) --- src/ingame/playercharacter_utest.lua | 436 +++++++++++++-------------- src/ingame/stage_state_utest.lua | 14 +- src/itest/itest_dsl_utest.lua | 22 +- 3 files changed, 236 insertions(+), 236 deletions(-) diff --git a/src/ingame/playercharacter_utest.lua b/src/ingame/playercharacter_utest.lua index c9384265..de6dc42a 100644 --- a/src/ingame/playercharacter_utest.lua +++ b/src/ingame/playercharacter_utest.lua @@ -255,22 +255,22 @@ describe('player_char', function () it('should return vector(1, 0) when quadrant is down', function () pc.quadrant = directions.down - assert.are_equal(vector(1, 0), pc:get_quadrant_right()) + assert.are_same(vector(1, 0), pc:get_quadrant_right()) end) it('should return vector(-1, 0) when quadrant is up', function () pc.quadrant = directions.up - assert.are_equal(vector(-1, 0), pc:get_quadrant_right()) + assert.are_same(vector(-1, 0), pc:get_quadrant_right()) end) it('should return vector(0, -1) when quadrant is right', function () pc.quadrant = directions.right - assert.are_equal(vector(0, -1), pc:get_quadrant_right()) + assert.are_same(vector(0, -1), pc:get_quadrant_right()) end) it('should return vector(0, 1) when quadrant is left', function () pc.quadrant = directions.left - assert.are_equal(vector(0, 1), pc:get_quadrant_right()) + assert.are_same(vector(0, 1), pc:get_quadrant_right()) end) end) @@ -279,22 +279,22 @@ describe('player_char', function () it('should return vector(0, 1) when quadrant is down', function () pc.quadrant = directions.down - assert.are_equal(vector(0, 1), pc:get_quadrant_down()) + assert.are_same(vector(0, 1), pc:get_quadrant_down()) end) it('should return vector(0, -1) when quadrant is up', function () pc.quadrant = directions.up - assert.are_equal(vector(0, -1), pc:get_quadrant_down()) + assert.are_same(vector(0, -1), pc:get_quadrant_down()) end) it('should return vector(1, 0) when quadrant is right', function () pc.quadrant = directions.right - assert.are_equal(vector(1, 0), pc:get_quadrant_down()) + assert.are_same(vector(1, 0), pc:get_quadrant_down()) end) it('should return vector(-1, 0) when quadrant is left', function () pc.quadrant = directions.left - assert.are_equal(vector(-1, 0), pc:get_quadrant_down()) + assert.are_same(vector(-1, 0), pc:get_quadrant_down()) end) end) @@ -396,7 +396,7 @@ describe('player_char', function () it('should set the character\'s position', function () pc:warp_to(vector(56, 12)) - assert.are_equal(vector(56, 12), pc.position) + assert.are_same(vector(56, 12), pc.position) end) describe('(_check_escape_from_ground returns false)', function () @@ -489,13 +489,13 @@ describe('player_char', function () it('(10, 0) => (10, center_height)', function () pc.position = vector(10, 0) - assert.are_equal(vector(10, 11), pc:get_bottom_center()) + assert.are_same(vector(10, 11), pc:get_bottom_center()) end) it('(10, 0) quadrant left => (10 - center_height, 0)', function () pc.position = vector(10, 0) pc.quadrant = directions.left - assert.are_equal(vector(-1, 0), pc:get_bottom_center()) + assert.are_same(vector(-1, 0), pc:get_bottom_center()) end) end) @@ -514,13 +514,13 @@ describe('player_char', function () it('set_bottom_center (10, center_height) => (10, 0)', function () pc:set_bottom_center(vector(10, 11)) - assert.are_equal(vector(10, 0), pc.position) + assert.are_same(vector(10, 0), pc.position) end) it('set_bottom_center (10 + center_height, 0) quadrant right => (10, 0)', function () pc.quadrant = directions.right pc:set_bottom_center(vector(10 + 11, 0)) - assert.are_equal(vector(10, 0), pc.position) + assert.are_same(vector(10, 0), pc.position) end) end) @@ -655,10 +655,10 @@ describe('player_char', function () it('should do nothing', function () input.players_btn_states[0][button_ids.left] = btn_states.pressed pc:_handle_input() - assert.are_equal(vector:zero(), pc.move_intention) + assert.are_same(vector:zero(), pc.move_intention) input.players_btn_states[0][button_ids.up] = btn_states.pressed pc:_handle_input() - assert.are_equal(vector:zero(), pc.move_intention) + assert.are_same(vector:zero(), pc.move_intention) end) end) @@ -668,67 +668,67 @@ describe('player_char', function () it('(when input left in down) it should update the player character\'s move intention by (-1, 0)', function () input.players_btn_states[0][button_ids.left] = btn_states.pressed pc:_handle_input() - assert.are_equal(vector(-1, 0), pc.move_intention) + assert.are_same(vector(-1, 0), pc.move_intention) end) it('(when input right in down) it should update the player character\'s move intention by (1, 0)', function () input.players_btn_states[0][button_ids.right] = btn_states.just_pressed pc:_handle_input() - assert.are_equal(vector(1, 0), pc.move_intention) + assert.are_same(vector(1, 0), pc.move_intention) end) it('(when input left and right are down) it should update the player character\'s move intention by (-1, 0)', function () input.players_btn_states[0][button_ids.left] = btn_states.pressed input.players_btn_states[0][button_ids.right] = btn_states.just_pressed pc:_handle_input() - assert.are_equal(vector(-1, 0), pc.move_intention) + assert.are_same(vector(-1, 0), pc.move_intention) end) it('(when input up in down) it should update the player character\'s move intention by (-1, 0)', function () input.players_btn_states[0][button_ids.up] = btn_states.pressed pc:_handle_input() - assert.are_equal(vector(0, -1), pc.move_intention) + assert.are_same(vector(0, -1), pc.move_intention) end) it('(when input down in down) it should update the player character\'s move intention by (0, 1)', function () input.players_btn_states[0][button_ids.down] = btn_states.pressed pc:_handle_input() - assert.are_equal(vector(0, 1), pc.move_intention) + assert.are_same(vector(0, 1), pc.move_intention) end) it('(when input up and down are down) it should update the player character\'s move intention by (0, -1)', function () input.players_btn_states[0][button_ids.up] = btn_states.just_pressed input.players_btn_states[0][button_ids.down] = btn_states.pressed pc:_handle_input() - assert.are_equal(vector(0, -1), pc.move_intention) + assert.are_same(vector(0, -1), pc.move_intention) end) it('(when input left and up are down) it should update the player character\'s move intention by (-1, -1)', function () input.players_btn_states[0][button_ids.left] = btn_states.just_pressed input.players_btn_states[0][button_ids.up] = btn_states.just_pressed pc:_handle_input() - assert.are_equal(vector(-1, -1), pc.move_intention) + assert.are_same(vector(-1, -1), pc.move_intention) end) it('(when input left and down are down) it should update the player character\'s move intention by (-1, 1)', function () input.players_btn_states[0][button_ids.left] = btn_states.just_pressed input.players_btn_states[0][button_ids.down] = btn_states.just_pressed pc:_handle_input() - assert.are_equal(vector(-1, 1), pc.move_intention) + assert.are_same(vector(-1, 1), pc.move_intention) end) it('(when input right and up are down) it should update the player character\'s move intention by (1, -1)', function () input.players_btn_states[0][button_ids.right] = btn_states.just_pressed input.players_btn_states[0][button_ids.up] = btn_states.just_pressed pc:_handle_input() - assert.are_equal(vector(1, -1), pc.move_intention) + assert.are_same(vector(1, -1), pc.move_intention) end) it('(when input right and down are down) it should update the player character\'s move intention by (1, 1)', function () input.players_btn_states[0][button_ids.right] = btn_states.just_pressed input.players_btn_states[0][button_ids.down] = btn_states.just_pressed pc:_handle_input() - assert.are_equal(vector(1, 1), pc.move_intention) + assert.are_same(vector(1, 1), pc.move_intention) end) it('(when input o is released) it should update the player character\'s jump intention to false, hold jump intention to false', function () @@ -812,7 +812,7 @@ describe('player_char', function () pc:_toggle_debug_motion() assert.are_equal(motion_modes.debug, pc.motion_mode) - assert.are_equal(vector.zero(), pc.debug_velocity) + assert.are_same(vector.zero(), pc.debug_velocity) end) it('(to platformer) should set motion mode to platformer and respawn as current position', function () @@ -1041,51 +1041,51 @@ describe('player_char', function () end) it('should return the position down-left of the character center when horizontal dir is left', function () - assert.are_equal(vector(7, 10 + 11), pc:_get_ground_sensor_position_from(vector(10, 10), horizontal_dirs.left)) + assert.are_same(vector(7, 10 + 11), pc:_get_ground_sensor_position_from(vector(10, 10), horizontal_dirs.left)) end) it('should return the position down-left of the x-floored character center when horizontal dir is left', function () - assert.are_equal(vector(7, 10 + 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.left)) + assert.are_same(vector(7, 10 + 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.left)) end) it('should return the position down-left of the character center when horizontal dir is right', function () - assert.are_equal(vector(12, 10 + 11), pc:_get_ground_sensor_position_from(vector(10, 10), horizontal_dirs.right)) + assert.are_same(vector(12, 10 + 11), pc:_get_ground_sensor_position_from(vector(10, 10), horizontal_dirs.right)) end) it('should return the position down-left of the x-floored character center when horizontal dir is right', function () - assert.are_equal(vector(12, 10 + 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.right)) + assert.are_same(vector(12, 10 + 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.right)) end) -- for other quadrants, just check the more complex case of coords with fractions it('(right wall) should return the position q-down-left of the x-floored character center when horizontal dir is left', function () pc.quadrant = directions.right - assert.are_equal(vector(10 + 11, 12), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.left)) + assert.are_same(vector(10 + 11, 12), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.left)) end) it('(right wall) should return the position q-down-left of the x-floored character center when horizontal dir is right', function () pc.quadrant = directions.right - assert.are_equal(vector(10 + 11, 7), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.right)) + assert.are_same(vector(10 + 11, 7), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.right)) end) it('(ceiling) should return the position q-down-left of the x-floored character center when horizontal dir is left', function () pc.quadrant = directions.up - assert.are_equal(vector(12, 10 - 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.left)) + assert.are_same(vector(12, 10 - 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.left)) end) it('(ceiling) should return the position q-down-left of the x-floored character center when horizontal dir is right', function () pc.quadrant = directions.up - assert.are_equal(vector(7, 10 - 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.right)) + assert.are_same(vector(7, 10 - 11), pc:_get_ground_sensor_position_from(vector(10.9, 10), horizontal_dirs.right)) end) it('(left wall) should return the position q-down-left of the x-floored character center when horizontal dir is left', function () pc.quadrant = directions.left - assert.are_equal(vector(10 - 11, 7), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.left)) + assert.are_same(vector(10 - 11, 7), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.left)) end) it('(left wall) should return the position q-down-left of the x-floored character center when horizontal dir is right', function () pc.quadrant = directions.left - assert.are_equal(vector(10 -11, 12), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.right)) + assert.are_same(vector(10 -11, 12), pc:_get_ground_sensor_position_from(vector(10, 10.9), horizontal_dirs.right)) end) end) @@ -1102,80 +1102,80 @@ describe('player_char', function () -- on the sides it('should return ground_query_info(max_ground_snap_height+1, nil) if just at ground height but slightly on the left', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height+1, nil), pc:_compute_signed_distance_to_closest_ground(vector(7, 8))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height+1, nil), pc:_compute_signed_distance_to_closest_ground(vector(7, 8))) end) it('should return ground_query_info(max_ground_snap_height+1, nil) if just at ground height but slightly on the right', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height+1, nil), pc:_compute_signed_distance_to_closest_ground(vector(16, 8))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height+1, nil), pc:_compute_signed_distance_to_closest_ground(vector(16, 8))) end) -- above it('should return ground_query_info(max_ground_snap_height+1, nil) if above the tile by 8 max_ground_snap_height+2)', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height+1, nil), pc:_compute_signed_distance_to_closest_ground(vector(12, 8 - (pc_data.max_ground_snap_height + 2)))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height+1, nil), pc:_compute_signed_distance_to_closest_ground(vector(12, 8 - (pc_data.max_ground_snap_height + 2)))) end) it('should return ground_query_info(max_ground_snap_height, 0) if above the tile by max_ground_snap_height', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8 - pc_data.max_ground_snap_height))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8 - pc_data.max_ground_snap_height))) end) it('should return ground_query_info(0.0625, 0) if just a above the tile by 0.0625 (<= max_ground_snap_height)', function () - assert.are_equal(ground_query_info(0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8 - 0.0625))) end) -- on top it('should return ground_query_info(0, 0) if just at the top of the topleft-most pixel of the tile', function () - assert.are_equal(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) + assert.are_same(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) end) it('should return ground_query_info(0, 0) if just at the top of tile, in the middle', function () - assert.are_equal(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8))) + assert.are_same(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8))) end) it('should return ground_query_info(0, 0) if just at the top of the right-most pixel', function () - assert.are_equal(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 8))) + assert.are_same(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 8))) end) -- just below the top it('should return ground_query_info(-0.0625, 0) if 0.0625 inside the top-left pixel', function () - assert.are_equal(ground_query_info(-0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 + 0.0625))) + assert.are_same(ground_query_info(-0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 + 0.0625))) end) it('should return ground_query_info(-0.0625, 0) if 0.0625 inside the top-right pixel', function () - assert.are_equal(ground_query_info(-0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 + 0.0625))) + assert.are_same(ground_query_info(-0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 + 0.0625))) end) -- going deeper it('should return ground_query_info(-1.5, 0) if 1.5 (<= max_ground_escape_height) inside vertically', function () - assert.are_equal(ground_query_info(-1.5, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8 + 1.5))) + assert.are_same(ground_query_info(-1.5, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8 + 1.5))) end) it('should return ground_query_info(-max_ground_escape_height, 0) if max_ground_escape_height inside', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 + pc_data.max_ground_escape_height))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 + pc_data.max_ground_escape_height))) end) it('should return ground_query_info(-max_ground_escape_height - 1, 0) if max_ground_escape_height + 2 inside', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 + pc_data.max_ground_escape_height + 2))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 + pc_data.max_ground_escape_height + 2))) 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(- max_ground_escape_height - 1, 0) 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_equal(ground_query_info(- pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) + assert.are_same(ground_query_info(- pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) end) it('should return ground_query_info(-max_ground_escape_height - 1, 0) (clamped) if max_ground_escape_height - 1 below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) end) -- step up distance reached, character considered in the air it('should return ground_query_info(max_ground_snap_height + 1, nil) if max_ground_escape_height + 1 below the bottom', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height + 1))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height + 1))) end) -- other quadrants (only the trickiest cases) @@ -1185,25 +1185,25 @@ describe('player_char', function () it('(right wall) should return ground_query_info(max_ground_snap_height + 1, nil) if too far from the wall', function () pc.quadrant = directions.right - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(0, 12))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(0, 12))) end) it('(right wall) should return ground_query_info(2, 0) if 2 pixels from the wall', function () pc.quadrant = directions.right - assert.are_equal(ground_query_info(2, 0.25), pc:_compute_signed_distance_to_closest_ground(vector(6, 12))) + assert.are_same(ground_query_info(2, 0.25), pc:_compute_signed_distance_to_closest_ground(vector(6, 12))) end) it('(right wall) should return ground_query_info(-2, 0) if 2 pixels inside the wall', function () pc.quadrant = directions.right - assert.are_equal(ground_query_info(-2, 0.25), pc:_compute_signed_distance_to_closest_ground(vector(10, 12))) + assert.are_same(ground_query_info(-2, 0.25), pc:_compute_signed_distance_to_closest_ground(vector(10, 12))) end) it('(right wall) should return ground_query_info(-max_ground_escape_height - 1, 0) if too far inside the wall', function () pc.quadrant = directions.right - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(14, 12))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(14, 12))) end) -- ceiling @@ -1211,25 +1211,25 @@ describe('player_char', function () it('(ceiling) should return ground_query_info(max_ground_snap_height + 1, nil) if too far from the wall', function () pc.quadrant = directions.up - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(12, 24))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(12, 24))) end) it('(ceiling) should return ground_query_info(2, 0) if 2 pixels from the wall', function () pc.quadrant = directions.up - assert.are_equal(ground_query_info(2, 0.5), pc:_compute_signed_distance_to_closest_ground(vector(12, 18))) + assert.are_same(ground_query_info(2, 0.5), pc:_compute_signed_distance_to_closest_ground(vector(12, 18))) end) it('(ceiling) should return ground_query_info(-2, 0) if 2 pixels inside the wall', function () pc.quadrant = directions.up - assert.are_equal(ground_query_info(-2, 0.5), pc:_compute_signed_distance_to_closest_ground(vector(12, 14))) + assert.are_same(ground_query_info(-2, 0.5), pc:_compute_signed_distance_to_closest_ground(vector(12, 14))) end) it('(ceiling) should return ground_query_info(-max_ground_escape_height - 1, 0) if too far inside the wall', function () pc.quadrant = directions.up - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 8))) end) -- left wall @@ -1237,25 +1237,25 @@ describe('player_char', function () it('(left wall) should return ground_query_info(max_ground_snap_height + 1, nil) if too far from the wall', function () pc.quadrant = directions.left - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(24, 12))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(24, 12))) end) it('(left wall) should return ground_query_info(2, 0) if 2 pixels from the wall', function () pc.quadrant = directions.left - assert.are_equal(ground_query_info(2, 0.75), pc:_compute_signed_distance_to_closest_ground(vector(18, 12))) + assert.are_same(ground_query_info(2, 0.75), pc:_compute_signed_distance_to_closest_ground(vector(18, 12))) end) it('(left wall) should return ground_query_info(-2, 0) if 2 pixels inside the wall', function () pc.quadrant = directions.left - assert.are_equal(ground_query_info(-2, 0.75), pc:_compute_signed_distance_to_closest_ground(vector(14, 12))) + assert.are_same(ground_query_info(-2, 0.75), pc:_compute_signed_distance_to_closest_ground(vector(14, 12))) end) it('(left wall) should return ground_query_info(-max_ground_escape_height - 1, 0) if too far inside the wall', function () pc.quadrant = directions.left - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(10, 12))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(10, 12))) end) end) @@ -1273,7 +1273,7 @@ describe('player_char', function () -- only to land inside the tile above it('should return ground_query_info(-max_ground_escape_height - 1, 0) if max_ground_escape_height + 1 inside, including max_ground_escape_height in current tile', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(4, 8 + pc_data.max_ground_escape_height))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(4, 8 + pc_data.max_ground_escape_height))) end) end) @@ -1288,82 +1288,82 @@ describe('player_char', function () -- just above it('should return 0.0625, 0 if just a little above the tile', function () - assert.are_equal(ground_query_info(0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 12 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 12 - 0.0625))) end) -- on top it('+ should return ground_query_info(max_ground_snap_height + 1, nil) if just touching the left of the tile at the ground\'s height', function () -- right ground sensor @ (7.5, 12) - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(7, 12))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(7, 12))) end) it('should return 0, 0 if just at the top of the topleft-most pixel of the tile', function () - assert.are_equal(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 12))) + assert.are_same(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 12))) end) it('should return 0, 0 if just at the top of tile, in the middle', function () - assert.are_equal(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 12))) + assert.are_same(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 12))) end) it('should return 0, 0 if just at the top of the right-most pixel', function () - assert.are_equal(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 12))) + assert.are_same(ground_query_info(0, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 12))) end) it('should return ground_query_info(max_ground_snap_height + 1, nil) if in the air on the right of the tile', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(16, 12))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(16, 12))) end) -- just inside the top it('should return ground_query_info(max_ground_snap_height + 1, nil) if just on the left of the topleft pixel, y at 0.0625 below the top', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(7, 12 + 0.0625))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(7, 12 + 0.0625))) end) it('should return -0.0625, 0 if 0.0625 inside the topleft pixel', function () - assert.are_equal(ground_query_info(-0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 12 + 0.0625))) + assert.are_same(ground_query_info(-0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 12 + 0.0625))) end) it('should return -0.0625, 0 if 0.0625 inside the topright pixel', function () - assert.are_equal(ground_query_info(-0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 12 + 0.0625))) + assert.are_same(ground_query_info(-0.0625, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 12 + 0.0625))) end) it('should return ground_query_info(max_ground_snap_height + 1, nil) if just on the right of the topright pixel, y at 0.0625 below the top', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(16, 12 + 0.0625))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(16, 12 + 0.0625))) end) -- just inside the bottom it('should return ground_query_info(max_ground_snap_height + 1, nil) if just on the left of the topleft pixel, y at 0.0625 above the bottom', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(7, 16 - 0.0625))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(7, 16 - 0.0625))) end) it('should return -(4 - 0.0625), 0 if 0.0625 inside the topleft pixel', function () - assert.are_equal(ground_query_info(-(4 - 0.0625), 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 16 - 0.0625))) + assert.are_same(ground_query_info(-(4 - 0.0625), 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 16 - 0.0625))) end) it('should return -(4 - 0.0625), 0 if 0.0625 inside the topright pixel', function () - assert.are_equal(ground_query_info(-(4 - 0.0625), 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 - 0.0625))) + assert.are_same(ground_query_info(-(4 - 0.0625), 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 - 0.0625))) end) it('should return ground_query_info(max_ground_snap_height + 1, nil) if just on the right of the topright pixel, y at 0.0625 above the bottom', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(16, 16 - 0.0625))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(16, 16 - 0.0625))) 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(-max_ground_escape_height - 1, 0) if max_ground_escape_height - 1 below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height - 1))) end) it('should return ground_query_info(-max_ground_escape_height - 1, 0) if max_ground_escape_height below the bottom', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(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(max_ground_snap_height + 1, nil) if max_ground_snap_height + 1 below the bottom', function () - assert.are_equal(ground_query_info(pc_data.max_ground_escape_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height + 1))) + assert.are_same(ground_query_info(pc_data.max_ground_escape_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(15, 16 + pc_data.max_ground_escape_height + 1))) end) end) @@ -1376,75 +1376,75 @@ describe('player_char', function () end) it('should return 0.0625, 45/360 if just above slope column 0', function () - assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 - 0.0625))) end) it('should return 0, 45/360 if at the top of column 0', function () - assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15))) + assert.are_same(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15))) end) it('should return 5 (max_ground_snap_height+1 clamping), nil if 7px above column 0, i.e. at top-most pixel of the ascending slope tile', function () - assert.are_equal(ground_query_info(5, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) + assert.are_same(ground_query_info(5, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) end) it('should return 5 (max_ground_snap_height+1), nil if 8px above column 0, i.e. at bottom-most pixel of tile just above the ascending slope tile', function () - assert.are_equal(ground_query_info(5, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 7))) + assert.are_same(ground_query_info(5, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 7))) end) it('should return 5 (max_ground_snap_height+1), nil if 15px above column 0, i.e. at top-most pixel of tile just above the ascending slope tile', function () - assert.are_equal(ground_query_info(5, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 0))) + assert.are_same(ground_query_info(5, nil), pc:_compute_signed_distance_to_closest_ground(vector(8, 0))) end) it('. should return 0.0625, 45/360 if just above slope column 4', function () - assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 11 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 11 - 0.0625))) end) it('. should return 0, 45/360 if at the top of column 4', function () - assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 11))) + assert.are_same(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 11))) end) it('should return -2, 45/360 if 2px below column 4', function () - assert.are_equal(ground_query_info(-2, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 13))) + assert.are_same(ground_query_info(-2, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(12, 13))) end) it('should return 0.0625, 45/360 if right sensor is just above slope column 0', function () - assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 8 - 0.0625))) end) it('should return 0, 45/360 if right sensor is at the top of column 0', function () - assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 8))) + assert.are_same(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 8))) end) it('should return -3, 45/360 if 3px below column 0', function () - assert.are_equal(ground_query_info(-3, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 11))) + assert.are_same(ground_query_info(-3, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 11))) end) it('. should return 0.0625, 45/360 if just above slope column 3', function () - assert.are_equal(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 12 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 12 - 0.0625))) end) it('. should return 0, 45/360 if at the top of column 3', function () - assert.are_equal(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 12))) + assert.are_same(ground_query_info(0, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 12))) 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(-4, 45/360) if 4 (<= max_ground_escape_height) below the 2nd column top', function () - assert.are_equal(ground_query_info(-4, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 16 + 2))) + assert.are_same(ground_query_info(-4, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 16 + 2))) end) it('should return ground_query_info(-(max_ground_escape_height - 1), 45/360) if max_ground_escape_height - 1 below the top of column 0', function () - assert.are_equal(ground_query_info(-(pc_data.max_ground_escape_height - 1), 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height - 1))) + assert.are_same(ground_query_info(-(pc_data.max_ground_escape_height - 1), 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height - 1))) end) it('should return ground_query_info(-max_ground_escape_height, 45/360) if max_ground_escape_height below the top of column 0', function () - assert.are_equal(ground_query_info(-pc_data.max_ground_escape_height, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height, 45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height))) end) -- step up distance reached, character considered in the air it('should return ground_query_info(-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_equal(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height + 1))) + assert.are_same(ground_query_info(-pc_data.max_ground_escape_height - 1, 0), pc:_compute_signed_distance_to_closest_ground(vector(8, 15 + pc_data.max_ground_escape_height + 1))) end) end) @@ -1457,59 +1457,59 @@ describe('player_char', function () end) it('should return 0.0625, 1-45/360 if right sensors are just a little above column 0', function () - assert.are_equal(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 - 0.0625))) end) it('should return 0, 1-45/360 if right sensors is at the top of column 0', function () - assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) + assert.are_same(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) end) it('should return -1, 1-45/360 if right sensors is below column 0 by 1px', function () - assert.are_equal(ground_query_info(-1, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 9))) + assert.are_same(ground_query_info(-1, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 9))) end) it('should return 1, 1-45/360 if 1px above slope column 1', function () - assert.are_equal(ground_query_info(1, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 8))) + assert.are_same(ground_query_info(1, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 8))) end) it('should return 0, 1-45/360 if at the top of column 1', function () - assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 9))) + assert.are_same(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 9))) end) it('should return -2, 1-45/360 if 2px below column 1', function () - assert.are_equal(ground_query_info(-2, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 11))) + assert.are_same(ground_query_info(-2, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(9, 11))) end) it('should return 0.0625, 1-45/360 if just above slope column 0', function () - assert.are_equal(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8 - 0.0625))) end) it('should return 0, 1-45/360 if at the top of column 0', function () - assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) + assert.are_same(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 8))) end) it('should return -3, 1-45/360 if 3px below column 0', function () - assert.are_equal(ground_query_info(-3, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 11))) + assert.are_same(ground_query_info(-3, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(8, 11))) end) it('. should return 0.0625, 1-45/360 if just above slope column 3', function () - assert.are_equal(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 11 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 11 - 0.0625))) end) it('. should return 0, 1-45/360 if at the top of column 3', function () - assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 11))) + assert.are_same(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 11))) end) it('should return -4, 1-45/360 if 4px below column 3', function () - assert.are_equal(ground_query_info(-4, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 15))) + assert.are_same(ground_query_info(-4, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(11, 15))) end) it('should return 0.0625, 1-45/360 if just above slope column 7', function () - assert.are_equal(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 15 - 0.0625))) + assert.are_same(ground_query_info(0.0625, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 15 - 0.0625))) end) it('should return 0 if, 1-45/360 at the top of column 7', function () - assert.are_equal(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 15))) + assert.are_same(ground_query_info(0, 1-45/360), pc:_compute_signed_distance_to_closest_ground(vector(15, 15))) end) end) @@ -1522,7 +1522,7 @@ describe('player_char', function () end) it('should return -4, 22.5/360 if below column 7 by 4px)', function () - assert.are_equal(ground_query_info(-4, 22.5/360), pc:_compute_signed_distance_to_closest_ground(vector(14, 15))) + assert.are_same(ground_query_info(-4, 22.5/360), pc:_compute_signed_distance_to_closest_ground(vector(14, 15))) end) end) @@ -1536,11 +1536,11 @@ describe('player_char', function () end) it('should return ground_query_info(max_ground_snap_height + 1, nil) if just at the bottom of the tile, on the left part, so in the air (and not 0 just because it is at height 0)', function () - assert.are_equal(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(11, 16))) + assert.are_same(ground_query_info(pc_data.max_ground_snap_height + 1, nil), pc:_compute_signed_distance_to_closest_ground(vector(11, 16))) end) it('should return -2, 0 if below tile by 2px', function () - assert.are_equal(ground_query_info(-2, 0), pc:_compute_signed_distance_to_closest_ground(vector(14, 14))) + assert.are_same(ground_query_info(-2, 0), pc:_compute_signed_distance_to_closest_ground(vector(14, 14))) end) end) @@ -1573,7 +1573,7 @@ describe('player_char', function () it('should return -4, 0 if below top by 4px, with character crossing 2 tiles', function () -- interface - assert.are_equal(ground_query_info(-4, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 18))) + assert.are_same(ground_query_info(-4, 0), pc:_compute_signed_distance_to_closest_ground(vector(12, 18))) end) end) @@ -2080,7 +2080,7 @@ describe('player_char', function () it('should set the position to vector(3, 4)', function () pc:_update_platformer_motion_grounded() - assert.are_equal(vector(3, 4), pc.position) + assert.are_same(vector(3, 4), pc.position) end) it('should call set_slope_angle_with_quadrant with 0.25', function () @@ -2171,7 +2171,7 @@ describe('player_char', function () it('should set the position to vector(3, 4)', function () pc:_update_platformer_motion_grounded() - assert.are_equal(vector(3, 4), pc.position) + assert.are_same(vector(3, 4), pc.position) end) it('should call set_slope_angle_with_quadrant with 0.5', function () @@ -2239,7 +2239,7 @@ describe('player_char', function () it('should set the position to vector(3, 4)', function () pc:_update_platformer_motion_grounded() - assert.are_equal(vector(3, 4), pc.position) + assert.are_same(vector(3, 4), pc.position) end) it('should call set_slope_angle_with_quadrant to nil', function () @@ -2291,7 +2291,7 @@ describe('player_char', function () it('should set the position to vector(3, 4)', function () pc:_update_platformer_motion_grounded() - assert.are_equal(vector(3, 4), pc.position) + assert.are_same(vector(3, 4), pc.position) end) it('should call set_slope_angle_with_quadrant to nil', function () @@ -2700,7 +2700,7 @@ describe('player_char', function () pc.position = vector(3, 4) pc.slope_angle = 0.125 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3, 4), 0.125, false, @@ -2714,7 +2714,7 @@ describe('player_char', function () pc.position = vector(3.5, 4) pc.slope_angle = 0.125 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3.5, 4), 0.125, false, @@ -2729,7 +2729,7 @@ describe('player_char', function () pc.quadrant = directions.right pc.slope_angle = 0.25 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3, 4.5), 0.25, false, @@ -2744,7 +2744,7 @@ describe('player_char', function () pc.quadrant = directions.up pc.slope_angle = 0.5 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3, 4.5), 0.5, false, @@ -2759,7 +2759,7 @@ describe('player_char', function () pc.quadrant = directions.left pc.slope_angle = 0.75 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3, 4.5), 0.75, false, @@ -2796,7 +2796,7 @@ describe('player_char', function () -- 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_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3.5, 4), 0, -- character has not moved by a full pixel, so visible position and slope remains the same false, @@ -2813,7 +2813,7 @@ describe('player_char', function () pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = 1 -- * slope cos = 0.5 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3.5, 4), 1-1/6, -- character has not moved by a full pixel, so visible position and slope remains the same false, @@ -2828,7 +2828,7 @@ describe('player_char', function () pc.ground_speed = 0.5 -- we assume _compute_max_pixel_distance is correct, so it should return 1 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(4, 4), 1-0.01, false, @@ -2842,7 +2842,7 @@ describe('player_char', function () pc.position = vector(3, 4) pc.ground_speed = -2.5 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(0.5, 4), 1-0.01, false, @@ -2858,7 +2858,7 @@ describe('player_char', function () pc.slope_angle = 0.25-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = 2 -- * slope cos = 1 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3, 3), 0.25-0.01, -- character has not moved by a full pixel, so visible position and slope remains the same false, @@ -2882,7 +2882,7 @@ describe('player_char', function () -- 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_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(2, 4), 0.5-0.01, false, @@ -2906,7 +2906,7 @@ describe('player_char', function () -- 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_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3, 5), 0.75-0.01, false, @@ -2948,7 +2948,7 @@ describe('player_char', function () pc.ground_speed = 1.5 -- we assume _compute_max_pixel_distance is correct, so it should return 2 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 4), 0.01, false, @@ -2963,7 +2963,7 @@ describe('player_char', function () pc.ground_speed = -1.5 -- we assume _compute_max_pixel_distance is correct, so it should return 2 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-5, 4), 0.01, false, @@ -2980,7 +2980,7 @@ describe('player_char', function () pc.ground_speed = 0.5 -- we assume _compute_max_pixel_distance is correct, so it should return 1 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 4), 0.01, false, @@ -2996,7 +2996,7 @@ describe('player_char', function () pc.ground_speed = -1 -- we assume _compute_max_pixel_distance is correct, so it should return 1 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-5, 4), 0.01, false, @@ -3014,7 +3014,7 @@ describe('player_char', function () pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = 1 -- * slope cos = -0.5 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 4), 0.01, -- new slope angle, no relation with initial one false, @@ -3031,7 +3031,7 @@ describe('player_char', function () pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = -2 -- * slope cos = -1 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-5, 4), 0.01, -- new slope angle, no relation with initial one false, @@ -3049,7 +3049,7 @@ describe('player_char', function () -- going "into" the wall, we floor them and consider character as blocked -- (unlike Classic Sonic that would simply ignore subpixels) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 4), 0.01, true, @@ -3067,7 +3067,7 @@ describe('player_char', function () -- going "into" the wall, we floor them and consider character as blocked -- (unlike Classic Sonic that would simply ignore subpixels) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-5, 4), 0.01, true, @@ -3085,7 +3085,7 @@ describe('player_char', function () 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 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(4.75, 4), 1-1/6, -- character has not moved by a full pixel, so visible position and slope remains the same false, @@ -3101,7 +3101,7 @@ describe('player_char', function () pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = -1.5 -- * slope cos = -0.75 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-4.85, 4), 1-1/6, -- character has not moved by a full pixel, so visible position and slope remains the same false, @@ -3117,7 +3117,7 @@ describe('player_char', function () pc.ground_speed = 3 -- * slope cos = 1.5 -- but here, even with the slope cos, charaacter will hit wall - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 4), 0.01, true, @@ -3132,7 +3132,7 @@ describe('player_char', function () pc.slope_angle = 1-1/6 -- cos(-pi/3) = 1/2 pc.ground_speed = -3 -- * slope cos = -1.5 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-5, 4), 0.01, true, @@ -3152,7 +3152,7 @@ describe('player_char', function () -- the character is already touching the wall, so any motion, even of just a few subpixels, -- is considered blocked - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 4), 0, -- character couldn't move at all, so we preserved the initial slope angle true, @@ -3169,7 +3169,7 @@ describe('player_char', function () -- the character is already touching the wall, so any motion, even of just a few subpixels, -- is considered blocked - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-5, 4), 0, -- character couldn't move at all, so we preserved the initial slope angle true, @@ -3189,7 +3189,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 1 -- but we will be blocked by the wall anyway - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( 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, @@ -3205,7 +3205,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 1 -- but we will be blocked by the wall anyway - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( 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! @@ -3221,7 +3221,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 1 -- but we will be blocked by the wall anyway - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( 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 @@ -3237,7 +3237,7 @@ describe('player_char', 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 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 4), 0.01, true, @@ -3253,7 +3253,7 @@ describe('player_char', 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 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-5, 4), 0.01, true, @@ -3272,7 +3272,7 @@ describe('player_char', function () -- 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_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3, -5), 0.25 + 0.01, true, @@ -3291,7 +3291,7 @@ describe('player_char', 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 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-5, 3), 0.5 + 0.01, true, @@ -3310,7 +3310,7 @@ describe('player_char', function () -- 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_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3, 5), 0.75 + 0.01, true, @@ -3372,7 +3372,7 @@ describe('player_char', function () -- 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 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(6, 4), nil, false, @@ -3388,7 +3388,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- we are falling then blocked on 7 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(7, 4), nil, true, @@ -3407,7 +3407,7 @@ describe('player_char', function () -- 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.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(4, 6), nil, false, @@ -3426,7 +3426,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- we are falling then blocked on 7 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(4, 7), nil, true, @@ -3445,7 +3445,7 @@ describe('player_char', function () -- 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 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(6, 4), nil, false, @@ -3464,7 +3464,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- we are falling then blocked on 7 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(7, 4), nil, true, @@ -3483,7 +3483,7 @@ describe('player_char', function () -- 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.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(4, 6), nil, false, @@ -3502,7 +3502,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct, so it should return 3 -- we are falling then blocked on 7 - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(4, 7), nil, true, @@ -3545,7 +3545,7 @@ describe('player_char', function () -- step flat pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-2, 8 - pc_data.center_height_standing), 0, false, @@ -3566,7 +3566,7 @@ describe('player_char', function () -- step flat pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(10, 8 - pc_data.center_height_standing), 0, false, @@ -3587,7 +3587,7 @@ describe('player_char', function () -- step fall pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-3, 8 - pc_data.center_height_standing), nil, false, @@ -3608,7 +3608,7 @@ describe('player_char', function () -- step fall pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(11, 8 - pc_data.center_height_standing), nil, false, @@ -3629,7 +3629,7 @@ describe('player_char', function () -- step land (very rare) pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-2, 8 - pc_data.center_height_standing), 0, false, @@ -3657,7 +3657,7 @@ describe('player_char', function () -- step flat pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(0 - pc_data.center_height_standing, 11), 0.25, false, @@ -3683,7 +3683,7 @@ describe('player_char', function () -- step fall pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(0 - pc_data.center_height_standing, 5), nil, false, @@ -3711,7 +3711,7 @@ describe('player_char', function () -- step flat pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-2, 16 + pc_data.center_height_standing), 0.5, false, @@ -3737,7 +3737,7 @@ describe('player_char', function () -- step fall pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(-3, 16 + pc_data.center_height_standing), nil, false, @@ -3763,7 +3763,7 @@ describe('player_char', function () -- step flat pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(8 + pc_data.center_height_standing, 16), 0.75, false, @@ -3789,7 +3789,7 @@ describe('player_char', function () -- step fall pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(0 - pc_data.center_height_standing, 17), nil, false, @@ -3826,7 +3826,7 @@ describe('player_char', function () -- step block pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(3, 8 - pc_data.center_height_standing), 0, true, @@ -3847,7 +3847,7 @@ describe('player_char', function () -- step block pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 8 - pc_data.center_height_standing), 0, true, @@ -3881,7 +3881,7 @@ describe('player_char', function () -- step block pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 8 - pc_data.center_height_standing), 0, true, @@ -3915,7 +3915,7 @@ describe('player_char', function () -- step block pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(5, 12 - pc_data.center_height_standing), 0, true, @@ -3950,7 +3950,7 @@ describe('player_char', function () -- step down pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(6, 7 - pc_data.center_height_standing), 45/360, false, @@ -3984,7 +3984,7 @@ describe('player_char', function () -- step down pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(11, 10 - pc_data.center_height_standing), 45/360, false, @@ -4005,7 +4005,7 @@ describe('player_char', function () -- step up pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(13, 8 - pc_data.center_height_standing), 45/360, false, @@ -4026,7 +4026,7 @@ describe('player_char', function () -- step up blocked pc:_next_ground_step(horizontal_dirs.right, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(13, 10 - pc_data.center_height_standing), -45/360, true, @@ -4047,7 +4047,7 @@ describe('player_char', function () -- step down blocked pc:_next_ground_step(horizontal_dirs.left, motion_result) - assert.are_equal(motion.ground_motion_result( + assert.are_same(motion.ground_motion_result( vector(11, 10 - pc_data.center_height_standing), -45/360, true, @@ -4183,7 +4183,7 @@ describe('player_char', function () mock_mset(0, 0, half_tile_id) end) - it('should return false for sensor position in the middle of the tile', function () + it('#solo should return false 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_false(pc:_is_column_blocked_by_ceiling_at(vector(4, 6))) @@ -4488,7 +4488,7 @@ describe('player_char', function () pc:_update_platformer_motion_airborne() - assert.are_equal(vector(2, 8), pc.position) + assert.are_same(vector(2, 8), pc.position) end) it('should preserve velocity.y', function () @@ -4696,7 +4696,7 @@ describe('player_char', function () pc:apply_air_drag() - assert.are_equal(vector(0.25, 0), pc.velocity) + assert.are_same(vector(0.25, 0), pc.velocity) end) it('(when velocity is 0.25 7) should do nothing', function () @@ -4705,7 +4705,7 @@ describe('player_char', function () pc:apply_air_drag() - assert.are_equal(vector(0.25, 7), pc.velocity) + assert.are_same(vector(0.25, 7), pc.velocity) end) it('(when velocity is 0.1 -7) should do nothing', function () @@ -4714,7 +4714,7 @@ describe('player_char', function () pc:apply_air_drag() - assert.are_equal(vector(0.1, -7), pc.velocity) + assert.are_same(vector(0.1, -7), pc.velocity) end) it('(when velocity is 0.25 -7) should do nothing', function () @@ -4724,7 +4724,7 @@ describe('player_char', function () pc:apply_air_drag() -- velocity x should be = 0.2421875 - assert.are_equal(vector(0.25 * pc_data.air_drag_factor_per_frame, -7), pc.velocity) + assert.are_same(vector(0.25 * pc_data.air_drag_factor_per_frame, -7), pc.velocity) end) it('(when velocity is 0.25 -8) should do nothing', function () @@ -4733,7 +4733,7 @@ describe('player_char', function () pc:apply_air_drag() - assert.are_equal(vector(0.25, -8), pc.velocity) + assert.are_same(vector(0.25, -8), pc.velocity) end) end) @@ -4742,7 +4742,7 @@ describe('player_char', function () it('(when velocity is zero) should return air_motion_result with initial position and no hits', function () pc.position = vector(4, 8) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(4, 8), false, false, @@ -4785,7 +4785,7 @@ describe('player_char', function () -- character should advance of (5, -6) resulting in pos (9.5, 2) -- interface: check that the final result is correct - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(9.5, 2), false, true, -- hit ceiling @@ -4840,7 +4840,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(0.5, 99), "x") - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(0.5, 10), false, false, @@ -4862,7 +4862,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(0.5, 99), "x") - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(0.7, 10), false, false, @@ -4884,7 +4884,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(0.5, 99), "x") - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(1, 10), false, false, @@ -4906,7 +4906,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(2.7, 99), "x") - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(3.1, 10), false, false, @@ -4928,7 +4928,7 @@ describe('player_char', function () -- we assume _compute_max_pixel_distance is correct pc:_advance_in_air_along(motion_result, vector(2.7, 99), "x") - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(5, 10), true, false, @@ -4978,7 +4978,7 @@ describe('player_char', function () pc:_next_air_step(directions.up, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(2, 6), false, false, @@ -5000,7 +5000,7 @@ describe('player_char', function () pc:_next_air_step(directions.down, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(2, 8), false, false, @@ -5022,7 +5022,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(1, 7), false, false, @@ -5044,7 +5044,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(3, 7), false, false, @@ -5099,7 +5099,7 @@ describe('player_char', function () pc:_next_air_step(directions.up, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(4, 8 + pc_data.full_height_standing - pc_data.center_height_standing), false, true, @@ -5124,7 +5124,7 @@ describe('player_char', function () pc:_next_air_step(directions.down, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(4, 0 - pc_data.center_height_standing), false, false, @@ -5149,7 +5149,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(10, 0 - pc_data.center_height_standing), false, false, @@ -5174,7 +5174,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(-2, 0 - pc_data.center_height_standing), false, false, @@ -5199,7 +5199,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(10, 0 - pc_data.center_height_standing), false, false, @@ -5224,7 +5224,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(-2, 0 - pc_data.center_height_standing), false, false, @@ -5251,7 +5251,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(-2, 0 - pc_data.center_height_standing), false, false, @@ -5276,7 +5276,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(-2, 1 - pc_data.center_height_standing), false, false, @@ -5301,7 +5301,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(11, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing), true, false, @@ -5326,7 +5326,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(-3, pc_data.max_ground_escape_height + 1 - pc_data.center_height_standing), true, false, @@ -5354,7 +5354,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(11, 7 + pc_data.full_height_standing - pc_data.center_height_standing), true, false, @@ -5380,7 +5380,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(10, 7 + pc_data.full_height_standing - pc_data.center_height_standing), false, false, @@ -5406,7 +5406,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(-3, 7 + pc_data.full_height_standing - pc_data.center_height_standing), true, false, @@ -5432,7 +5432,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(-2, 7 + pc_data.full_height_standing - pc_data.center_height_standing), false, false, @@ -5457,7 +5457,7 @@ describe('player_char', function () pc:_next_air_step(directions.right, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(-2, 0 - pc_data.center_height_standing), false, false, @@ -5482,7 +5482,7 @@ describe('player_char', function () pc:_next_air_step(directions.left, motion_result) - assert.are_equal(motion.air_motion_result( + assert.are_same(motion.air_motion_result( vector(-3, 0 - pc_data.center_height_standing), false, false, @@ -5520,7 +5520,7 @@ describe('player_char', function () pc:_update_debug() assert.spy(update_velocity_debug_mock).was_called(1) assert.spy(update_velocity_debug_mock).was_called_with(match.ref(pc)) - assert.are_equal(vector(1, 2) + vector(4, -3) * delta_time60, pc.position) + assert.are_same(vector(1, 2) + vector(4, -3) * delta_time60, pc.position) end) end) diff --git a/src/ingame/stage_state_utest.lua b/src/ingame/stage_state_utest.lua index e82d9748..cd2765d5 100644 --- a/src/ingame/stage_state_utest.lua +++ b/src/ingame/stage_state_utest.lua @@ -96,7 +96,7 @@ describe('stage_state', function () end) it('should initialize camera at origin', function () - assert.are_equal(vector:zero(), state.camera_pos) + assert.are_same(vector:zero(), state.camera_pos) end) it('should call start_coroutine_method on show_stage_title_async', function () @@ -222,7 +222,7 @@ describe('stage_state', function () it('should move the camera to player position', function () state:update_camera() - assert.are_equal(vector(12, 24), state.camera_pos) + assert.are_same(vector(12, 24), state.camera_pos) end) end) @@ -489,7 +489,7 @@ describe('stage_state', function () for i = 1, stage_data.show_stage_title_delay * state.app.fps - 1 do coresume(on_show_stage_title_async, state) end - assert.are_equal(label(state.curr_stage_data.title, vector(50, 30), colors.white), state.title_overlay.labels["title"]) + assert.are_same(label(state.curr_stage_data.title, vector(50, 30), colors.white), state.title_overlay.labels["title"]) -- reach last frame now to check if label just disappeared coresume(on_show_stage_title_async, state) @@ -530,7 +530,7 @@ describe('stage_state', function () it('render_title_overlay should call title_overlay:draw_labels', function () state:render_title_overlay() - assert.are_equal(vector.zero(), vector(pico8.camera_x, pico8.camera_y)) + assert.are_same(vector.zero(), vector(pico8.camera_x, pico8.camera_y)) assert.spy(title_overlay_draw_labels_stub).was_called(1) assert.spy(title_overlay_draw_labels_stub).was_called_with(state.title_overlay) end) @@ -538,7 +538,7 @@ describe('stage_state', function () it('render_background should reset camera position, call rectfill on the whole screen with stage background color', function () state.camera_pos = vector(24, 13) state:render_background() - assert.are_equal(vector(0, 0), vector(pico8.camera_x, pico8.camera_y)) + assert.are_same(vector(0, 0), vector(pico8.camera_x, pico8.camera_y)) assert.spy(rectfill_stub).was_called(1) assert.spy(rectfill_stub).was_called_with(0, 0, 127, 127, state.curr_stage_data.background_color) end) @@ -546,7 +546,7 @@ describe('stage_state', function () it('render_stage_elements should set camera position, call map for environment and player_char:render', function () state.camera_pos = vector(24, 13) state:render_stage_elements() - assert.are_equal(vector(24 - 128 / 2, 13 - 128 / 2), vector(pico8.camera_x, pico8.camera_y)) + assert.are_same(vector(24 - 128 / 2, 13 - 128 / 2), vector(pico8.camera_x, pico8.camera_y)) assert.spy(state.render_environment).was_called(1) assert.spy(state.render_environment).was_called_with(match.ref(state)) assert.spy(player_char_render_stub).was_called(1) @@ -556,7 +556,7 @@ describe('stage_state', function () it('set_camera_offset_stage should set the pico8 camera so that it is centered on the camera position', function () state.camera_pos = vector(24, 13) state:set_camera_offset_stage() - assert.are_equal(vector(24 - 128 / 2, 13 - 128 / 2), vector(pico8.camera_x, pico8.camera_y)) + assert.are_same(vector(24 - 128 / 2, 13 - 128 / 2), vector(pico8.camera_x, pico8.camera_y)) end) describe('(after set_camera_offset_stage)', function () diff --git a/src/itest/itest_dsl_utest.lua b/src/itest/itest_dsl_utest.lua index 52161c34..2225d79f 100644 --- a/src/itest/itest_dsl_utest.lua +++ b/src/itest/itest_dsl_utest.lua @@ -101,7 +101,7 @@ describe('itest_dsl', function () end) it('should return the 2 coordinate string arguments as vector', function () - assert.are_equal(vector(2, -3.5), itest_dsl._parse_vector({"2", "-3.5"})) + assert.are_same(vector(2, -3.5), itest_dsl._parse_vector({"2", "-3.5"})) end) end) @@ -227,7 +227,7 @@ describe('itest_dsl', function () it('should set pc velocity to (1, -3)', function () itest_dsl._execute_set({"pc_velocity", vector(1, -3)}) - assert.are_equal(vector(1, -3), state.player_char.velocity) + assert.are_same(vector(1, -3), state.player_char.velocity) end) it('should fail with unsupported gp_value_type for setting', function () @@ -269,7 +269,7 @@ describe('itest_dsl', function () it('should set the move intention of the current player character to the directional unit vector matching his horizontal direction', function () itest_dsl._execute_move({horizontal_dirs.right}) - assert.are_equal(vector(1, 0), state.player_char.move_intention) + assert.are_same(vector(1, 0), state.player_char.move_intention) end) end) @@ -279,7 +279,7 @@ describe('itest_dsl', function () it('should set the move intention of the current player character to vector zero', function () state.player_char.move_intention = vector(99, -99) itest_dsl._execute_stop({}) - assert.are_equal(vector.zero(), state.player_char.move_intention) + assert.are_same(vector.zero(), state.player_char.move_intention) end) end) @@ -337,7 +337,7 @@ describe('itest_dsl', function () it('should return the bottom position of the current player character', function () state.player_char:set_bottom_center(vector(12, 47)) - assert.are_equal(vector(12, 47), eval_pc_bottom_pos()) + assert.are_same(vector(12, 47), eval_pc_bottom_pos()) end) end) @@ -346,7 +346,7 @@ describe('itest_dsl', function () it('should return the velocity of the current player character', function () state.player_char.velocity = vector(1, -4) - assert.are_equal(vector(1, -4), eval_pc_velocity()) + assert.are_same(vector(1, -4), eval_pc_velocity()) end) end) @@ -392,7 +392,7 @@ describe('itest_dsl', function () it('should return the velocity the current player character', function () itest_dsl.set_pc_velocity(vector(1, -4)) - assert.are_equal(vector(1, -4), state.player_char.velocity) + assert.are_same(vector(1, -4), state.player_char.velocity) end) end) @@ -859,11 +859,11 @@ expect -- verify warp callback behavior test.action_sequence[1].callback() assert.is_not_nil(state.player_char) - assert.are_equal(vector(12, 45 - pc_data.center_height_standing), state.player_char.position) + assert.are_same(vector(12, 45 - pc_data.center_height_standing), state.player_char.position) -- verify move callback behavior test.action_sequence[3].callback() - assert.are_equal(vector(-1, 0), state.player_char.move_intention) + 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 @@ -996,7 +996,7 @@ expect it('should set the current time_trigger of the parser to one with the passed interval, in frames', function () itest_dsl_parser:_wait(12) - assert.are_equal(time_trigger(12, true), itest_dsl_parser._last_time_trigger) + assert.are_same(time_trigger(12, true), itest_dsl_parser._last_time_trigger) end) it('should add a dummy action with any previous time trigger, then set the last time trigger to the new one', function () @@ -1005,7 +1005,7 @@ expect assert.are_equal(1, #itest_dsl_parser._itest.action_sequence) local action = itest_dsl_parser._itest.action_sequence[1] assert.are_same({time_trigger(4, true), nil}, {action.trigger, action.callback}) - assert.are_equal(time_trigger(8, true), itest_dsl_parser._last_time_trigger) + assert.are_same(time_trigger(8, true), itest_dsl_parser._last_time_trigger) end) end) From d8fc4a88986cb764588ddbf4a81da2b65c8f7f9e Mon Sep 17 00:00:00 2001 From: huulong Date: Sun, 23 Aug 2020 22:10:59 +0200 Subject: [PATCH 110/112] [TOKENS] -> 8769 Updated pico-boots to remove are_same_shallow --- pico-boots | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pico-boots b/pico-boots index b65fcb14..d28e52c5 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit b65fcb14af9e4b3902650ade46ee99340650b26d +Subproject commit d28e52c543ed716b02fd3f1b4e52b211c8c2d6ac From f39a7a5448c0f533b0ca6bcb32f3807b0a9df9d3 Mon Sep 17 00:00:00 2001 From: huulong Date: Mon, 24 Aug 2020 00:54:12 +0200 Subject: [PATCH 111/112] [TOKENS] -> 8709 Update pico-boots to extract unused helpers except is_empty --- pico-boots | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pico-boots b/pico-boots index d28e52c5..654e6e10 160000 --- a/pico-boots +++ b/pico-boots @@ -1 +1 @@ -Subproject commit d28e52c543ed716b02fd3f1b4e52b211c8c2d6ac +Subproject commit 654e6e101bacecca12370967c3ff9d2d33041b2a From 5df56e2a9fb3a9574dfcad53220285c8bbb36d65 Mon Sep 17 00:00:00 2001 From: huulong Date: Tue, 25 Aug 2020 16:24:32 +0200 Subject: [PATCH 112/112] [VERSION] Bumped to v3.2 --- .travis.yml | 2 +- CHANGELOG.md | 4 +++- README.md | 2 +- build_game.sh | 2 +- build_itest.sh | 2 +- build_pico8_utests.sh | 2 +- export_cartridge_release.p8 | 8 ++++---- install_cartridge_linux.sh | 2 +- run_game.sh | 2 +- run_itest.sh | 2 +- run_pico8_utests.sh | 2 +- 11 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 19d1a887..de12732f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -75,7 +75,7 @@ deploy: provider: releases api_key: secure: bfXQQ0AXGHgXiq0xOxhYQ2AXX/flQnxJh/+eA/HUGfwdoPDq0QTdqFA/3jEMWkJSsFKEBVKDjJGCt24QPxUIjTu91r1wyCNdL2KlNfnogRjWAVutRZxB/OC2HWR3kJtPjkFQBCsOXHBxGI3hMJL7LWr5WfNsSGMbcRMfvphxFT3ER8XBHAUEJY6roITm6noHroqQt8Uye+0+rkGqJ8QslKRqq8qBZMZeOiOrh6SBdlhsGw0KqNno/dMXQxx2ZCrh/VUeWjNvxzXe/mZjfBPbhvyecN7jz+FytEdAhdt1Dy37hhyOAkDfxLGGsH1YAAfinH8uFwoSRo0MH8fuhdXpT7jUXuAgP9/RS0FEiZDdX+J/FdncCbnoDfE9B4Dt3L3srISeiNwxKK5sx2kzyWvftK30pV1+zEgnbVEKGPIIeGb5wYWSCmzHf+CfLMk+bzeznTrpo/irY/vjoRBefNaVWXqLygrNWxM1uIMJae+OA3MYeUSYd1lpCyRw98i3GC7si68M9OaDeLoDjnqOLqvhurB/RmLzCU7mCYipn2kxykAOdevWN73cyx9VhdFy2GPE5VDw6EO6ZQP04KaeYxP2pgR4ts2kYWpVvf1PGg+2yN4QMkVhrWV+6dG2jtUO0BrCqt5Tpw0I3C3aFmBjjzFBBuKsZpr2yUG3roxnu1Dhww0= - file: build/picosonic_v3.1_release.p8 + file: build/picosonic_v3.2_release.p8 on: tags: 'true' skip_cleanup: 'true' diff --git a/CHANGELOG.md b/CHANGELOG.md index efa6e17a..ce87eda5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [3.2] - 2020-08-25 +### Added +- Loop quadrant system (walk at any angle, jump orthogonally) ## [3.1] - 2020-08-11 ### Added diff --git a/README.md b/README.md index ab2d941d..31075745 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Works with PICO-8 0.2.0i and 0.2.1b. ## Features -Version: 3.1 +Version: 3.2 ### Physics diff --git a/build_game.sh b/build_game.sh index 9adb6787..86256167 100755 --- a/build_game.sh +++ b/build_game.sh @@ -13,7 +13,7 @@ build_output_path="$(dirname "$0")/build" author="hsandt" title="pico-sonic" cartridge_stem="picosonic" -version="3.1" +version="3.2" help() { echo "Build a PICO-8 cartridge with the passed config." diff --git a/build_itest.sh b/build_itest.sh index 19647064..3373212b 100755 --- a/build_itest.sh +++ b/build_itest.sh @@ -16,7 +16,7 @@ build_output_path="$(dirname "$0")/build" author="hsandt" title="pico-sonic itests (all)" cartridge_stem="picosonic_itest_all" -version="3.1" +version="3.2" config='itest' # symbols='assert,log,visual_logger,tuner,profiler,mouse,itest' # cheat needed to set debug motion mode diff --git a/build_pico8_utests.sh b/build_pico8_utests.sh index 6e24dd73..88ea1478 100755 --- a/build_pico8_utests.sh +++ b/build_pico8_utests.sh @@ -16,7 +16,7 @@ build_output_path="$(dirname "$0")/build" author="hsandt" title="pico-sonic pico8 utests (all)" cartridge_stem="picosonic_pico8_utests_all" -version="3.1" +version="3.2" config='debug' symbols='assert,log,p8utest' diff --git a/export_cartridge_release.p8 b/export_cartridge_release.p8 index c903709d..42a20d38 100644 --- a/export_cartridge_release.p8 +++ b/export_cartridge_release.p8 @@ -10,14 +10,14 @@ __lua__ -- Paths are relative to PICO-8 carts directory. cd("picosonic") -load("picosonic_v3.1_release.p8") +load("picosonic_v3.2_release.p8") -- png cartridge export is done via SAVE -- the metadata label is used automatically -save("picosonic_v3.1_release.p8.png") +save("picosonic_v3.2_release.p8.png") -- other exports are done via EXPORT, and can use an icon -- instead of the .p8.png label -- icon is a 16x16 square => -s 2 tiles wide -- with top-left at sprite 2 => -i 2 -- on pink (color 14) background => -c 14 -export("picosonic_v3.1_release.bin -i 2 -s 2 -c 14") -export("picosonic_v3.1_release.html -i 2 -s 2 -c 14") +export("picosonic_v3.2_release.bin -i 2 -s 2 -c 14") +export("picosonic_v3.2_release.html -i 2 -s 2 -c 14") diff --git a/install_cartridge_linux.sh b/install_cartridge_linux.sh index c7c48b0f..7d69748c 100755 --- a/install_cartridge_linux.sh +++ b/install_cartridge_linux.sh @@ -12,7 +12,7 @@ fi # Configuration: cartridge cartridge_stem="picosonic" -version="3.1" +version="3.2" config="$1"; shift # option "png" will export the png cartridge diff --git a/run_game.sh b/run_game.sh index ca0497db..01678ac0 100755 --- a/run_game.sh +++ b/run_game.sh @@ -5,7 +5,7 @@ # Configuration: cartridge cartridge_stem="picosonic" -version="3.1" +version="3.2" config="$1"; shift run_cmd="pico8 -run build/${cartridge_stem}_v${version}_${config}.p8 -screenshot_scale 4 -gif_scale 4 -gif_len 60 $@" diff --git a/run_itest.sh b/run_itest.sh index 83306908..de54d06a 100755 --- a/run_itest.sh +++ b/run_itest.sh @@ -5,7 +5,7 @@ # Configuration: cartridge cartridge_stem="picosonic_itest_all" -version="3.1" +version="3.2" run_cmd="pico8 -run build/${cartridge_stem}_v${version}_itest.p8 -screenshot_scale 4 -gif_scale 4 $@" diff --git a/run_pico8_utests.sh b/run_pico8_utests.sh index d10d7d67..e827f39d 100755 --- a/run_pico8_utests.sh +++ b/run_pico8_utests.sh @@ -5,7 +5,7 @@ # Configuration: cartridge cartridge_stem="picosonic_pico8_utests_all" -version="3.1" +version="3.2" run_cmd="pico8 -run build/${cartridge_stem}_v${version}_debug.p8 -screenshot_scale 4 -gif_scale 4 $@"