From 3c060a089d8a7321a6bd18537d8d43a18df6ca46 Mon Sep 17 00:00:00 2001 From: Vollch Date: Mon, 27 Nov 2023 07:45:13 +0300 Subject: [PATCH] feat(port): allow vertical movement for avatar in water (#3764) * Allow vertical movement for avatar in water * Fix assumptions by visitable tests about the map * Entering water cube immediately submerges --------- Co-authored-by: Jason Jones --- .../terrain-liquids.json | 6 +- .../docs/en/mod/json/reference/json_flags.md | 3 +- src/avatar_action.cpp | 4 + src/game.cpp | 136 +++++++++++++---- src/mapdata.cpp | 1 + src/mapdata.h | 1 + tests/map_helpers.cpp | 22 +++ tests/map_helpers.h | 1 + tests/player_helpers.cpp | 1 + tests/visitable_remove_test.cpp | 2 + tests/water_movement_test.cpp | 137 ++++++++++++++++++ 11 files changed, 279 insertions(+), 35 deletions(-) create mode 100644 tests/water_movement_test.cpp diff --git a/data/json/furniture_and_terrain/terrain-liquids.json b/data/json/furniture_and_terrain/terrain-liquids.json index e504114adcec..7d4d03862da5 100644 --- a/data/json/furniture_and_terrain/terrain-liquids.json +++ b/data/json/furniture_and_terrain/terrain-liquids.json @@ -35,7 +35,7 @@ "looks_like": "t_water_sh", "color": "blue", "move_cost": 8, - "flags": [ "TRANSPARENT", "LIQUID", "NO_SCENT", "SWIMMABLE", "DEEP_WATER", "FISHABLE" ], + "flags": [ "TRANSPARENT", "LIQUID", "NO_SCENT", "SWIMMABLE", "DEEP_WATER", "FISHABLE", "GOES_DOWN" ], "connects_to": "WATER", "examine_action": "water_source" }, @@ -276,7 +276,7 @@ "symbol": "~", "color": "blue", "move_cost": 8, - "flags": [ "TRANSPARENT", "LIQUID", "NO_SCENT", "SWIMMABLE", "DEEP_WATER", "GOES_DOWN", "GOES_UP" ], + "flags": [ "TRANSPARENT", "LIQUID", "NO_SCENT", "SWIMMABLE", "DEEP_WATER", "WATER_CUBE", "GOES_DOWN", "GOES_UP" ], "connects_to": "WATER", "examine_action": "water_source" }, @@ -290,7 +290,7 @@ "looks_like": "t_sand", "color": "blue", "move_cost": 8, - "flags": [ "TRANSPARENT", "LIQUID", "NO_SCENT", "SWIMMABLE", "DEEP_WATER", "GOES_UP" ], + "flags": [ "TRANSPARENT", "LIQUID", "NO_SCENT", "SWIMMABLE", "DEEP_WATER", "WATER_CUBE", "GOES_UP" ], "connects_to": "WATER", "examine_action": "water_source" }, diff --git a/doc/src/content/docs/en/mod/json/reference/json_flags.md b/doc/src/content/docs/en/mod/json/reference/json_flags.md index 76131e01d110..ca66466e4088 100644 --- a/doc/src/content/docs/en/mod/json/reference/json_flags.md +++ b/doc/src/content/docs/en/mod/json/reference/json_flags.md @@ -532,7 +532,8 @@ List of known flags, used in both `terrain.json` and `furniture.json`. - `CONSOLE` Used as a computer. - `CONTAINER` Items on this square are hidden until looted by the player. - `DECONSTRUCT` Can be deconstructed. -- `DEEP_WATER` +- `DEEP_WATER` Deep enough to submerge things +- `WATER_CUBE` Water tile that is entirely water - `DESTROY_ITEM` Items that land here are destroyed. See also `NOITEM` - `DIFFICULT_Z` Most zombies will not be able to follow you up this terrain ( i.e a ladder ) - `DIGGABLE_CAN_DEEPEN` Diggable location can be dug again to make deeper (e.g. shallow pit to deep diff --git a/src/avatar_action.cpp b/src/avatar_action.cpp index 53f6d59fc403..0568eb9bae9a 100644 --- a/src/avatar_action.cpp +++ b/src/avatar_action.cpp @@ -540,6 +540,10 @@ void avatar_action::swim( map &m, avatar &you, const tripoint &p ) add_msg( _( "The water washes off the glowing goo!" ) ); you.remove_effect( effect_glowing ); } + if( m.has_flag( TFLAG_WATER_CUBE, p ) && !you.is_underwater() ) { + you.oxygen = 30 + 2 * you.str_cur; + you.set_underwater( true ); + } int movecost = you.swim_speed(); you.practice( skill_swimming, you.is_underwater() ? 2 : 1 ); if( movecost >= 500 ) { diff --git a/src/game.cpp b/src/game.cpp index df794db69fdb..f63bb3bb867f 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -10032,38 +10032,12 @@ void game::vertical_move( int movez, bool force, bool peeking ) } } - // > and < are used for diving underwater. - if( m.has_flag( "SWIMMABLE", u.pos() ) && m.has_flag( TFLAG_DEEP_WATER, u.pos() ) ) { - if( movez == -1 ) { - if( u.is_underwater() ) { - add_msg( m_info, _( "You are already underwater!" ) ); - return; - } - if( u.worn_with_flag( flag_FLOTATION ) ) { - add_msg( m_info, _( "You can't dive while wearing a flotation device." ) ); - return; - } - u.set_underwater( true ); - ///\EFFECT_STR increases breath-holding capacity while diving - u.oxygen = 30 + 2 * u.str_cur; - add_msg( _( "You dive underwater!" ) ); - } else { - if( u.swim_speed() < 500 || u.shoe_type_count( itype_swim_fins ) ) { - u.set_underwater( false ); - add_msg( _( "You surface." ) ); - } else { - add_msg( m_info, _( "You try to surface but can't!" ) ); - } - } - u.moves -= 100; - return; - } - // Force means we're going down, even if there's no staircase, etc. bool climbing = false; int move_cost = 100; tripoint stairs( u.posx(), u.posy(), u.posz() + movez ); - if( m.has_zlevels() && !force && movez == 1 && !m.has_flag( "GOES_UP", u.pos() ) ) { + if( m.has_zlevels() && !force && movez == 1 && !m.has_flag( "GOES_UP", u.pos() ) && + !u.is_underwater() ) { // Climbing if( m.has_floor_or_support( stairs ) ) { add_msg( m_info, _( "You can't climb here - there's a ceiling above your head." ) ); @@ -10126,10 +10100,12 @@ void game::vertical_move( int movez, bool force, bool peeking ) } } - if( !force && movez == -1 && !m.has_flag( "GOES_DOWN", u.pos() ) ) { + if( !force && movez == -1 && !m.has_flag( "GOES_DOWN", u.pos() ) && + !u.is_underwater() ) { add_msg( m_info, _( "You can't go down here!" ) ); return; - } else if( !climbing && !force && movez == 1 && !m.has_flag( "GOES_UP", u.pos() ) ) { + } else if( !climbing && !force && movez == 1 && !m.has_flag( "GOES_UP", u.pos() ) && + !u.is_underwater() ) { add_msg( m_info, _( "You can't go up here!" ) ); return; } @@ -10205,10 +10181,101 @@ void game::vertical_move( int movez, bool force, bool peeking ) maybetmp.load( tripoint( get_levx(), get_levy(), z_after ), false ); } + bool swimming = false; + bool surfacing = false; + bool submerging = false; + // > and < are used for diving underwater. + if( m.has_flag( TFLAG_SWIMMABLE, u.pos() ) ) { + swimming = true; + const ter_id &target_ter = m.ter( u.pos() + tripoint( 0, 0, movez ) ); + + // If we're in a water tile that has both air above and deep enough water to submerge in... + if( m.has_flag( TFLAG_DEEP_WATER, u.pos() ) && + !m.has_flag( TFLAG_WATER_CUBE, u.pos() ) ) { + // ...and we're trying to swim down + if( movez == -1 ) { + // ...and we're already submerged + if( u.is_underwater() ) { + // ...and there's more water beneath us. + if( target_ter->has_flag( TFLAG_WATER_CUBE ) ) { + // Then go ahead and move down. + add_msg( _( "You swim down." ) ); + } else { + // There's no more water beneath us. + add_msg( m_info, + _( "You are already underwater and there is no more water beneath you to swim down!" ) ); + return; + } + } + // ...and we're not already submerged. + else { + // Check for a flotation device first before allowing us to submerge. + if( u.worn_with_flag( flag_FLOTATION ) ) { + add_msg( m_info, _( "You can't dive while wearing a flotation device." ) ); + return; + } + + // Then dive under the surface. + u.oxygen = 30 + 2 * u.str_cur; + u.set_underwater( true ); + add_msg( _( "You dive underwater!" ) ); + submerging = true; + } + } + // ...and we're trying to surface + else if( movez == 1 ) { + // ... and we're already submerged + if( u.is_underwater() ) { + if( u.swim_speed() < 500 || u.shoe_type_count( itype_swim_fins ) ) { + u.set_underwater( false ); + add_msg( _( "You surface." ) ); + surfacing = true; + } else { + add_msg( m_info, _( "You try to surface but can't!" ) ); + return; + } + } + } + } + // If we're in a water tile that is entirely water + else if( m.has_flag( TFLAG_WATER_CUBE, u.pos() ) ) { + // If you're at this point, you should already be underwater, but force that to be the case. + if( !u.is_underwater() ) { + u.oxygen = 30 + 2 * u.str_cur; + u.set_underwater( true ); + } + + // ...and we're trying to swim down + if( movez == -1 ) { + // ...and there's more water beneath us. + if( target_ter->has_flag( TFLAG_WATER_CUBE ) ) { + // Then go ahead and move down. + add_msg( _( "You swim down." ) ); + } else { + add_msg( m_info, + _( "You are already underwater and there is no more water beneath you to swim down!" ) ); + return; + } + } + // ...and we're trying to move up + else if( movez == 1 ) { + // ...and there's more water above us us. + if( target_ter->has_flag( TFLAG_WATER_CUBE ) || + target_ter->has_flag( TFLAG_DEEP_WATER ) ) { + // Then go ahead and move up. + add_msg( _( "You swim up." ) ); + } else { + add_msg( m_info, _( "You are already underwater and there is no water above you to swim up!" ) ); + return; + } + } + } + } + // Find the corresponding staircase bool rope_ladder = false; // TODO: Remove the stairfinding, make the mapgen gen aligned maps - if( !force && !climbing ) { + if( !force && !climbing && !swimming ) { const std::optional pnt = find_or_make_stairs( maybetmp, z_after, rope_ladder, peeking ); if( !pnt ) { return; @@ -10300,6 +10367,13 @@ void game::vertical_move( int movez, bool force, bool peeking ) m.unboard_vehicle( np->pos() ); } } + + if( surfacing || submerging ) { + // Surfacing and submerging don't actually move us anywhere, and just + // toggle our underwater state in the same location. + return; + } + const tripoint old_pos = g->u.pos(); point submap_shift; vertical_shift( z_after ); diff --git a/src/mapdata.cpp b/src/mapdata.cpp index 17606a2f220d..92de4229e4bd 100644 --- a/src/mapdata.cpp +++ b/src/mapdata.cpp @@ -170,6 +170,7 @@ static const std::unordered_map ter_bitflags_map = { { "WALL", TFLAG_WALL }, // Badly defined. Used for roof support, mapgen, and fungalization result. { "NO_SCENT", TFLAG_NO_SCENT }, // cannot have scent values, which prevents scent diffusion through this tile { "DEEP_WATER", TFLAG_DEEP_WATER }, // Deep enough to submerge things + { "WATER_CUBE", TFLAG_WATER_CUBE }, // Water tile that is entirely water { "CURRENT", TFLAG_CURRENT }, // Water is flowing. { "HARVESTED", TFLAG_HARVESTED }, // harvested. will not bear fruit. { "PERMEABLE", TFLAG_PERMEABLE }, // gases can flow through. diff --git a/src/mapdata.h b/src/mapdata.h index 9056f3ba7c14..fdebf3f42a8e 100644 --- a/src/mapdata.h +++ b/src/mapdata.h @@ -294,6 +294,7 @@ enum ter_bitflags : int { TFLAG_UNSTABLE, TFLAG_WALL, TFLAG_DEEP_WATER, + TFLAG_WATER_CUBE, TFLAG_CURRENT, TFLAG_HARVESTED, TFLAG_PERMEABLE, diff --git a/tests/map_helpers.cpp b/tests/map_helpers.cpp index f4fab6673f3c..8f5f1f49f0e2 100644 --- a/tests/map_helpers.cpp +++ b/tests/map_helpers.cpp @@ -141,6 +141,28 @@ void build_test_map( const ter_id &terrain ) g->m.build_map_cache( 0, true ); } +void build_water_test_map( const ter_id &surface, const ter_id &mid, const ter_id &bottom ) +{ + constexpr int z_surface = 0; + constexpr int z_bottom = -2; + + map &here = get_map(); + for( const tripoint &p : here.points_in_rectangle( tripoint_zero, + tripoint( MAPSIZE * SEEX, MAPSIZE * SEEY, z_bottom ) ) ) { + + if( p.z == z_surface ) { + here.ter_set( p, surface ); + } else if( p.z < z_surface && p.z > z_bottom ) { + here.ter_set( p, mid ); + } else if( p.z == z_bottom ) { + here.ter_set( p, bottom ); + } + } + + here.invalidate_map_cache( 0 ); + here.build_map_cache( 0, true ); +} + void set_time( const time_point &time ) { calendar::turn = time; diff --git a/tests/map_helpers.h b/tests/map_helpers.h index 2b15597ff450..888744f5f76a 100644 --- a/tests/map_helpers.h +++ b/tests/map_helpers.h @@ -21,6 +21,7 @@ void put_player_underground(); monster &spawn_test_monster( const std::string &monster_type, const tripoint &start ); void clear_vehicles(); void build_test_map( const ter_id &terrain ); +void build_water_test_map( const ter_id &surface, const ter_id &mid, const ter_id &bottom ); void set_time( const time_point &time ); #endif // CATA_TESTS_MAP_HELPERS_H diff --git a/tests/player_helpers.cpp b/tests/player_helpers.cpp index 730700648cdc..b4cc19033ef6 100644 --- a/tests/player_helpers.cpp +++ b/tests/player_helpers.cpp @@ -106,6 +106,7 @@ void clear_character( player &dummy, bool debug_storage ) // Make sure we don't carry around weird effects. dummy.clear_effects(); // mark effects for removal dummy.process_effects(); // actually remove them + dummy.set_underwater( false ); // Make stats nominal. dummy.str_max = 8; diff --git a/tests/visitable_remove_test.cpp b/tests/visitable_remove_test.cpp index 95dde50c84b5..be4504f85746 100644 --- a/tests/visitable_remove_test.cpp +++ b/tests/visitable_remove_test.cpp @@ -16,6 +16,7 @@ #include "item_contents.h" #include "itype.h" #include "map.h" +#include "map_helpers.h" #include "map_selector.h" #include "player.h" #include "point.h" @@ -55,6 +56,7 @@ TEST_CASE( "visitable_remove", "[visitable]" ) p.inv_clear(); p.remove_primary_weapon(); p.wear_item( item::spawn( "backpack" ) ); // so we don't drop anything + clear_map(); // check if all tiles within radius are loaded within current submap and passable const auto suitable = []( const tripoint & pos, const int radius ) { diff --git a/tests/water_movement_test.cpp b/tests/water_movement_test.cpp new file mode 100644 index 000000000000..5d39edffb059 --- /dev/null +++ b/tests/water_movement_test.cpp @@ -0,0 +1,137 @@ +#include "catch/catch.hpp" + +#include + +#include "avatar.h" +#include "creature.h" +#include "game.h" +#include "map.h" +#include "map_helpers.h" +#include "player_helpers.h" +#include "type_id.h" + +TEST_CASE( "avatar diving", "[diving]" ) +{ + const ter_id t_water_dp( "t_water_dp" ); + const ter_id t_water_cube( "t_water_cube" ); + const ter_id t_lake_bed( "t_lake_bed" ); + + build_water_test_map( t_water_dp, t_water_cube, t_lake_bed ); + map &here = get_map(); + + clear_avatar(); + Character &dummy = get_player_character(); + const tripoint test_origin( 60, 60, 0 ); + + REQUIRE( here.ter( test_origin ) == t_water_dp ); + REQUIRE( here.ter( test_origin + tripoint_below ) == t_water_cube ); + REQUIRE( here.ter( test_origin + tripoint( 0, 0, -2 ) ) == t_lake_bed ); + + GIVEN( "avatar is above water at z0" ) { + dummy.set_underwater( false ); + dummy.setpos( test_origin ); + g->vertical_shift( 0 ); + + WHEN( "avatar dives down" ) { + g->vertical_move( -1, false ); + + THEN( "avatar is underwater at z0" ) { + REQUIRE( dummy.pos() == test_origin ); + REQUIRE( here.ter( dummy.pos() ) == ter_id( "t_water_dp" ) ); + REQUIRE( dummy.is_underwater() ); + } + } + + WHEN( "avatar swims up" ) { + g->vertical_move( 1, false ); + + THEN( "avatar is not underwater at z0" ) { + REQUIRE( dummy.pos() == test_origin ); + REQUIRE( here.ter( dummy.pos() ) == ter_id( "t_water_dp" ) ); + REQUIRE( !dummy.is_underwater() ); + } + } + } + + GIVEN( "avatar is underwater at z0" ) { + dummy.set_underwater( true ); + dummy.setpos( test_origin ); + g->vertical_shift( 0 ); + + WHEN( "avatar dives down" ) { + g->vertical_move( -1, false ); + + THEN( "avatar is underwater at z-1" ) { + REQUIRE( dummy.pos() == test_origin + tripoint_below ); + REQUIRE( here.ter( dummy.pos() ) == ter_id( "t_water_cube" ) ); + REQUIRE( dummy.is_underwater() ); + } + } + + WHEN( "avatar swims up" ) { + g->vertical_move( 1, false ); + + THEN( "avatar is not underwater at z0" ) { + REQUIRE( dummy.pos() == test_origin ); + REQUIRE( here.ter( dummy.pos() ) == ter_id( "t_water_dp" ) ); + REQUIRE( !dummy.is_underwater() ); + } + } + } + + GIVEN( "avatar is underwater at z-1" ) { + dummy.set_underwater( true ); + dummy.setpos( test_origin + tripoint_below ); + g->vertical_shift( -1 ); + + WHEN( "avatar dives down" ) { + g->vertical_move( -1, false ); + + THEN( "avatar is underwater at z-2" ) { + REQUIRE( dummy.pos() == test_origin + tripoint( 0, 0, -2 ) ); + REQUIRE( here.ter( dummy.pos() ) == ter_id( "t_lake_bed" ) ); + REQUIRE( dummy.is_underwater() ); + } + } + + WHEN( "avatar swims up" ) { + g->vertical_move( 1, false ); + + THEN( "avatar is underwater at z0" ) { + REQUIRE( dummy.pos() == test_origin ); + REQUIRE( here.ter( dummy.pos() ) == ter_id( "t_water_dp" ) ); + REQUIRE( dummy.is_underwater() ); + } + } + } + + GIVEN( "avatar is underwater at z-2" ) { + dummy.set_underwater( true ); + dummy.setpos( test_origin + tripoint( 0, 0, -2 ) ); + g->vertical_shift( -2 ); + + WHEN( "avatar dives down" ) { + g->vertical_move( -1, false ); + + THEN( "avatar is underwater at z-2" ) { + REQUIRE( dummy.pos() == test_origin + tripoint( 0, 0, -2 ) ); + REQUIRE( here.ter( dummy.pos() ) == ter_id( "t_lake_bed" ) ); + REQUIRE( dummy.is_underwater() ); + } + } + + WHEN( "avatar swims up" ) { + g->vertical_move( 1, false ); + + THEN( "avatar is underwater at z-1" ) { + REQUIRE( dummy.pos() == test_origin + tripoint_below ); + REQUIRE( here.ter( dummy.pos() ) == ter_id( "t_water_cube" ) ); + REQUIRE( dummy.is_underwater() ); + } + } + } + + // Put us back at 0. We shouldn't have to do this but other tests are + // making assumptions about what z-level they're on. + g->vertical_shift( 0 ); +}