diff --git a/data/json/effects.json b/data/json/effects.json index cfbd430bae55..6ca7d501571a 100644 --- a/data/json/effects.json +++ b/data/json/effects.json @@ -396,6 +396,15 @@ "rating": "bad", "permanent": true }, + { + "type": "effect_type", + "id": "wall_clinging", + "name": [ "Clinging" ], + "desc": [ "Clinging to a vertical surface." ], + "rating": "neutral", + "show_in_info": true, + "permanent": true + }, { "type": "effect_type", "id": "blind", diff --git a/data/json/monstergroups/zombie_upgrades.json b/data/json/monstergroups/zombie_upgrades.json index d83f2eadcd19..d60596d091c3 100644 --- a/data/json/monstergroups/zombie_upgrades.json +++ b/data/json/monstergroups/zombie_upgrades.json @@ -132,6 +132,7 @@ "//": "crawling zombie upgrades", "monsters": [ { "monster": "mon_zombie_crawler_pupa", "freq": 500, "cost_multiplier": 2 }, + { "monster": "mon_zombie_crawler_clinger", "freq": 250, "cost_multiplier": 3 }, { "monster": "mon_zombie_crawler_pupa_decoy", "freq": 500, "cost_multiplier": 2 } ] }, diff --git a/data/json/monsters/defense_bot.json b/data/json/monsters/defense_bot.json index dc26a613929c..5f0bf65b16a0 100644 --- a/data/json/monsters/defense_bot.json +++ b/data/json/monsters/defense_bot.json @@ -236,7 +236,7 @@ "special_attacks": [ [ "TAZER", 5 ] ], "death_drops": { "groups": [ [ "robots", 4 ], [ "skitterbot", 1 ] ] }, "death_function": [ "BROKEN" ], - "flags": [ "SEES", "HEARS", "GOODHEARING", "ELECTRONIC", "COLDPROOF", "NO_BREATHE", "PATH_AVOID_DANGER_1", "BIOPROOF" ] + "flags": [ "SEES", "HEARS", "GOODHEARING", "ELECTRONIC", "COLDPROOF", "CLIMBS", "NO_BREATHE", "PATH_AVOID_DANGER_1", "BIOPROOF" ] }, { "id": "mon_science_bot", @@ -280,6 +280,7 @@ "ELECTRONIC", "COLDPROOF", "ACIDPROOF", + "CLIMBS", "NO_BREATHE", "PRIORITIZE_TARGETS", "PATH_AVOID_DANGER_2", diff --git a/data/json/monsters/fungus.json b/data/json/monsters/fungus.json index 76262237ab11..8479b91fd6da 100644 --- a/data/json/monsters/fungus.json +++ b/data/json/monsters/fungus.json @@ -520,7 +520,7 @@ "harvest": "arachnid", "special_attacks": [ [ "FUNGUS", 200 ] ], "death_function": [ "NORMAL", "FUNGUS" ], - "flags": [ "SEES", "SMELLS", "POISON", "CLIMBS", "PATH_AVOID_FIRE", "PATH_AVOID_FALL" ] + "flags": [ "SEES", "SMELLS", "POISON", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_spider_fungus", @@ -553,6 +553,6 @@ "harvest": "arachnid", "special_attacks": [ [ "FUNGAL_TRAIL", 3 ] ], "death_function": [ "NORMAL", "FUNGUS" ], - "flags": [ "SEES", "SMELLS", "VENOM", "WEBWALK", "CLIMBS", "PATH_AVOID_FIRE" ] + "flags": [ "SEES", "SMELLS", "VENOM", "WEBWALK", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] } ] diff --git a/data/json/monsters/insect_spider.json b/data/json/monsters/insect_spider.json index 9ff5040d95a3..76cc1f44e178 100644 --- a/data/json/monsters/insect_spider.json +++ b/data/json/monsters/insect_spider.json @@ -769,7 +769,18 @@ "anger_triggers": [ "STALK", "PLAYER_WEAK", "PLAYER_CLOSE" ], "death_function": [ "NORMAL" ], "fungalize_into": "mon_spider_fungus", - "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "WEBWALK", "CLIMBS", "HARDTOSHOOT", "PUSH_MON", "PATH_AVOID_FIRE" ], + "flags": [ + "SEES", + "SMELLS", + "HEARS", + "VENOM", + "WEBWALK", + "CLIMBS", + "CLIMBS_WALLS", + "HARDTOSHOOT", + "PUSH_MON", + "PATH_AVOID_FIRE" + ], "//": "No, they are not in fact the most venomous spider in the world." }, { @@ -803,7 +814,7 @@ "harvest": "arachnid", "anger_triggers": [ "STALK", "PLAYER_WEAK", "PLAYER_CLOSE" ], "death_function": [ "NORMAL" ], - "flags": [ "SEES", "SMELLS", "HEARS", "WEBWALK", "CLIMBS", "HARDTOSHOOT", "PATH_AVOID_FIRE" ] + "flags": [ "SEES", "SMELLS", "HEARS", "WEBWALK", "CLIMBS", "CLIMBS_WALLS", "HARDTOSHOOT", "PATH_AVOID_FIRE" ] }, { "id": "mon_spider_jumping_giant", @@ -838,7 +849,7 @@ "anger_triggers": [ "PLAYER_CLOSE" ], "death_function": [ "NORMAL" ], "fungalize_into": "mon_spider_fungus", - "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "HIT_AND_RUN", "CLIMBS", "PATH_AVOID_DANGER_1" ] + "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "HIT_AND_RUN", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_DANGER_1" ] }, { "id": "mon_spider_trapdoor_giant", @@ -871,7 +882,7 @@ "harvest": "arachnid", "death_function": [ "NORMAL" ], "fungalize_into": "mon_spider_fungus", - "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "GRABS", "CAN_DIG", "WEBWALK", "CLIMBS", "PATH_AVOID_FIRE" ] + "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "GRABS", "CAN_DIG", "WEBWALK", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_spider_web", @@ -905,7 +916,7 @@ "harvest": "arachnid", "death_function": [ "NORMAL" ], "fungalize_into": "mon_spider_fungus", - "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "WEBWALK", "CLIMBS", "PATH_AVOID_FIRE", "PATH_AVOID_FALL" ] + "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "WEBWALK", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_spider_web_s", @@ -937,7 +948,7 @@ "vision_night": 5, "harvest": "arachnid", "death_function": [ "NORMAL" ], - "flags": [ "SEES", "SMELLS", "HEARS", "WEBWALK", "STUMBLES", "CLIMBS", "PATH_AVOID_FIRE" ] + "flags": [ "SEES", "SMELLS", "HEARS", "WEBWALK", "STUMBLES", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_spider_widow_giant", @@ -972,7 +983,7 @@ "anger_triggers": [ "PLAYER_WEAK", "PLAYER_CLOSE" ], "death_function": [ "NORMAL" ], "fungalize_into": "mon_spider_fungus", - "flags": [ "SEES", "SMELLS", "HEARS", "BADVENOM", "WEBWALK", "CLIMBS", "PATH_AVOID_FIRE" ] + "flags": [ "SEES", "SMELLS", "HEARS", "BADVENOM", "WEBWALK", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_spider_widow_giant_s", @@ -1006,7 +1017,7 @@ "harvest": "arachnid", "anger_triggers": [ "PLAYER_WEAK", "PLAYER_CLOSE" ], "death_function": [ "NORMAL" ], - "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "WEBWALK", "CLIMBS", "PATH_AVOID_FIRE" ] + "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "WEBWALK", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_spider_wolf_giant", @@ -1041,7 +1052,7 @@ "anger_triggers": [ "STALK", "PLAYER_WEAK", "HURT", "PLAYER_CLOSE" ], "death_function": [ "NORMAL" ], "fungalize_into": "mon_spider_fungus", - "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "CLIMBS", "PATH_AVOID_FIRE", "PATH_AVOID_FALL" ] + "flags": [ "SEES", "SMELLS", "HEARS", "VENOM", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_wasp_larva", @@ -1453,7 +1464,7 @@ "death_function": [ "NORMAL" ], "fungalize_into": "mon_ant_fungus", "special_attacks": [ [ "EAT_FOOD", 3600 ] ], - "flags": [ "SEES", "HEARS", "SMELLS", "CLIMBS", "PATH_AVOID_FIRE", "PATH_AVOID_FALL" ] + "flags": [ "SEES", "HEARS", "SMELLS", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_ant_acid", @@ -1488,7 +1499,7 @@ "anger_triggers": [ "FRIEND_ATTACKED", "FRIEND_DIED", "HURT", "PLAYER_CLOSE" ], "death_function": [ "ACID", "NORMAL" ], "harvest": "arachnid_acid", - "flags": [ "ACIDPROOF", "CLIMBS", "HEARS", "POISON", "SEES", "SMELLS", "PATH_AVOID_FIRE", "PATH_AVOID_FALL" ] + "flags": [ "ACIDPROOF", "CLIMBS", "CLIMBS_WALLS", "HEARS", "POISON", "SEES", "SMELLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_ant_acid_larva", @@ -1514,7 +1525,7 @@ "upgrades": { "age_grow": 3, "into": "mon_ant_acid" }, "death_function": [ "ACID", "NORMAL" ], "harvest": "arachnid_acid", - "flags": [ "ACIDPROOF", "LARVA", "POISON", "SMELLS" ] + "flags": [ "ACIDPROOF", "CLIMBS", "CLIMBS_WALLS", "LARVA", "POISON", "SMELLS" ] }, { "id": "mon_ant_acid_queen", @@ -1546,7 +1557,7 @@ "anger_triggers": [ "FRIEND_ATTACKED", "FRIEND_DIED", "HURT", "PLAYER_CLOSE" ], "death_function": [ "NORMAL" ], "harvest": "acidant_queen", - "flags": [ "ACIDPROOF", "CLIMBS", "HEARS", "QUEEN", "SEES", "SMELLS", "PATH_AVOID_FIRE", "PATH_AVOID_FALL" ] + "flags": [ "ACIDPROOF", "CLIMBS", "CLIMBS_WALLS", "HEARS", "QUEEN", "SEES", "SMELLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_ant_acid_soldier", @@ -1581,7 +1592,7 @@ "anger_triggers": [ "FRIEND_ATTACKED", "FRIEND_DIED", "HURT", "PLAYER_CLOSE" ], "death_function": [ "ACID", "NORMAL" ], "harvest": "arachnid_acid", - "flags": [ "ACIDPROOF", "SHORTACIDTRAIL", "CLIMBS", "HEARS", "POISON", "SEES", "SMELLS", "PATH_AVOID_FIRE", "PATH_AVOID_FALL" ] + "flags": [ "ACIDPROOF", "SHORTACIDTRAIL", "CLIMBS", "CLIMBS_WALLS", "HEARS", "POISON", "SEES", "SMELLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_ant_larva", @@ -1607,7 +1618,7 @@ "harvest": "arachnid", "upgrades": { "age_grow": 3, "into": "mon_ant" }, "death_function": [ "NORMAL" ], - "flags": [ "SMELLS", "LARVA" ] + "flags": [ "CLIMBS", "CLIMBS_WALLS", "SMELLS", "LARVA" ] }, { "id": "mon_ant_queen", @@ -1638,7 +1649,7 @@ "anger_triggers": [ "FRIEND_ATTACKED", "FRIEND_DIED", "HURT" ], "death_function": [ "NORMAL" ], "fungalize_into": "mon_ant_fungus", - "flags": [ "SMELLS", "QUEEN", "CLIMBS", "PATH_AVOID_FIRE", "PATH_AVOID_FALL" ] + "flags": [ "SMELLS", "QUEEN", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_ant_soldier", @@ -1672,7 +1683,7 @@ "anger_triggers": [ "FRIEND_ATTACKED", "FRIEND_DIED", "HURT", "PLAYER_CLOSE" ], "death_function": [ "NORMAL" ], "fungalize_into": "mon_ant_fungus", - "flags": [ "SEES", "HEARS", "SMELLS", "CLIMBS", "PATH_AVOID_FIRE", "PATH_AVOID_FALL" ] + "flags": [ "SEES", "HEARS", "SMELLS", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_FIRE" ] }, { "id": "mon_locust", diff --git a/data/json/monsters/mutant_animal.json b/data/json/monsters/mutant_animal.json index fd04f026842e..0b33321b2fb9 100644 --- a/data/json/monsters/mutant_animal.json +++ b/data/json/monsters/mutant_animal.json @@ -359,7 +359,12 @@ "//": " 201 days gestation period. The fawn will stay with its mother for approximately one year, suckling for three to four months.", "baby_flags": [ "SPRING", "SUMMER" ], "special_attacks": [ [ "EAT_CROP", 7200 ] ], - "flags": [ "SEES", "HEARS", "SMELLS", "ANIMAL", "PATH_AVOID_DANGER_1", "WARM" ] + "petfood": { + "food": [ "CATTLEFOOD" ], + "feed": "The %s seems to like you!", + "pet": "The %s makes a happy arachnid-like chitter at you as you pat its head." + }, + "flags": [ "SEES", "HEARS", "SMELLS", "ANIMAL", "CLIMBS", "CLIMBS_WALLS", "PET_MOUNTABLE", "PATH_AVOID_DANGER_1", "WARM" ] }, { "id": "mon_deer_mutant_spider_fawn", @@ -391,7 +396,7 @@ "fear_triggers": [ "SOUND", "PLAYER_CLOSE" ], "death_function": [ "NORMAL" ], "upgrades": { "age_grow": 330, "into": "mon_deer_mutant_spider" }, - "flags": [ "SEES", "HEARS", "SMELLS", "ANIMAL", "PATH_AVOID_DANGER_1", "WARM", "STUMBLES" ] + "flags": [ "SEES", "HEARS", "SMELLS", "ANIMAL", "CLIMBS", "CLIMBS_WALLS", "PATH_AVOID_DANGER_1", "WARM", "STUMBLES" ] }, { "id": "mon_dog_mutant_mongrel", diff --git a/data/json/monsters/zed_misc.json b/data/json/monsters/zed_misc.json index 7790c2f4e8ed..da2153b4fc23 100644 --- a/data/json/monsters/zed_misc.json +++ b/data/json/monsters/zed_misc.json @@ -699,7 +699,7 @@ "id": "mon_zombie_predator", "type": "MONSTER", "name": { "str": "zombie predator" }, - "description": "With its joints in odd places and angles, this humanoid creature prowls across the landscape with surprising speed. Its teeth and arms are sharpened into fine points, and black ooze seeps out from cuts between its muscles.", + "description": "With its joints in odd places and angles, this humanoid creature prowls across the landscape with surprising speed, clawing up walls as easily as it bounds across open ground. Its teeth and arms are sharpened into fine points, and black ooze seeps out from cuts between its muscles.", "default_faction": "zombie", "bodytype": "human", "species": [ "ZOMBIE", "HUMAN" ], @@ -736,7 +736,20 @@ "death_drops": "default_zombie_death_drops", "death_function": [ "NORMAL" ], "burn_into": "mon_zombie_scorched", - "flags": [ "SEES", "HEARS", "SMELLS", "WARM", "BASHES", "POISON", "NO_BREATHE", "REVIVES", "PUSH_MON" ] + "flags": [ "SEES", "HEARS", "SMELLS", "WARM", "BASHES", "POISON", "NO_BREATHE", "REVIVES", "CLIMBS", "CLIMBS_WALLS", "PUSH_MON" ] + }, + { + "id": "mon_zombie_crawler_clinger", + "type": "MONSTER", + "copy-from": "mon_zombie_crawler", + "name": { "str": "clinger crawler" }, + "description": "Only the upper half remains of this corpse, making it eerily lightweight. It drags itself with raw arms, wedging shattered bone into cracks to slowly clamber up walls and ceilings.", + "color": "light_gray", + "volume": "40000 ml", + "weight": "45000 g", + "hp": 50, + "speed": 30, + "extend": { "flags": [ "CLIMBS", "CLIMBS_WALLS" ] } }, { "id": "mon_zombie_screecher", diff --git a/docs/en/mod/json/reference/json_flags.md b/docs/en/mod/json/reference/json_flags.md index 110b15bd0957..5ccc95dd7bb7 100644 --- a/docs/en/mod/json/reference/json_flags.md +++ b/docs/en/mod/json/reference/json_flags.md @@ -1065,6 +1065,7 @@ Multiple death functions can be used. Not all combinations make sense. - `CBM_TECH` May produce a CBM or two from 'bionics_tech' item group and a power CBM when butchered. - `CHITIN` May produce chitin when butchered. - `CLIMBS` Can climb. +- `CLIMBS_WALLS` Can climb walls and other sheer surfaces. - `CURRENT` this water is flowing. - `DESTROYS` Bashes down walls and more. (2.5x bash multiplier, where base is the critter's max melee bashing) diff --git a/src/handle_action.cpp b/src/handle_action.cpp index 1257843d9415..0b51f84cd2f9 100644 --- a/src/handle_action.cpp +++ b/src/handle_action.cpp @@ -1845,7 +1845,9 @@ bool game::handle_action() const bool can_use_stairs = mon->has_flag( MF_RIDEABLE_MECH ) || mon->has_flag( MF_MOUNTABLE_STAIRS ) || - mon->has_flag( MF_FLIES ); + mon->has_flag( MF_FLIES ) || + mon->has_flag( MF_CLIMBS ) || + mon->has_flag( MF_CLIMBS_WALLS ); if( !can_use_stairs ) { add_msg( m_info, _( "Your mount can't go downstairs while riding." ) ); @@ -1889,7 +1891,9 @@ bool game::handle_action() const bool can_use_stairs = mon->has_flag( MF_RIDEABLE_MECH ) || mon->has_flag( MF_MOUNTABLE_STAIRS ) || - mon->has_flag( MF_FLIES ); + mon->has_flag( MF_FLIES ) || + mon->has_flag( MF_CLIMBS ) || + mon->has_flag( MF_CLIMBS_WALLS ); if( !can_use_stairs ) { add_msg( m_info, _( "Your mount can't go upstairs or climb while riding." ) ); diff --git a/src/map.cpp b/src/map.cpp index 616ec3c9259a..6ccb0261e3b1 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -2640,8 +2640,8 @@ int map::climb_difficulty( const tripoint &p ) const return INT_MAX; } - int best_difficulty = INT_MAX; - int blocks_movement = 0; + auto best_difficulty = INT_MAX; + auto blocks_movement = 0; if( has_flag( "LADDER", p ) ) { // Really easy, but you have to stand on the tile return 1; @@ -2651,7 +2651,17 @@ int map::climb_difficulty( const tripoint &p ) const best_difficulty = 7; } - for( const auto &pt : points_in_radius( p, 1 ) ) { + const auto neighbor_range = points_in_radius( p, 1 ); + auto climb_points = std::vector( neighbor_range.begin(), neighbor_range.end() ); + + if( has_flag( TFLAG_NO_FLOOR, p ) ) { + const auto below = p + tripoint_below; + const auto below_range = points_in_radius( below, 1 ); + const auto below_points = std::vector( below_range.begin(), below_range.end() ); + climb_points.insert( climb_points.end(), below_points.begin(), below_points.end() ); + } + + for( const tripoint &pt : climb_points ) { if( impassable_ter_furn( pt ) ) { // TODO: Non-hardcoded climbability best_difficulty = std::min( best_difficulty, 10 ); diff --git a/src/monmove.cpp b/src/monmove.cpp index e6176e8aec49..9b60201a38c1 100644 --- a/src/monmove.cpp +++ b/src/monmove.cpp @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include #include #include @@ -73,6 +75,7 @@ static const efftype_id effect_pacified( "pacified" ); static const efftype_id effect_pushed( "pushed" ); static const efftype_id effect_stunned( "stunned" ); static const efftype_id effect_led_by_leash( "led_by_leash" ); +static const efftype_id effect_wall_clinging( "wall_clinging" ); static const itype_id itype_pressurized_tank( "pressurized_tank" ); @@ -82,6 +85,85 @@ static const species_id SPIDER( "SPIDER" ); static const species_id ZOMBIE( "ZOMBIE" ); static const std::string flag_AUTODOC_COUCH( "AUTODOC_COUCH" ); + +namespace +{ + +auto get_wall_support( const map &here, const tripoint &anchor ) -> std::optional +{ + const auto neighbor_range = points_in_radius( anchor, 1 ); + const std::vector neighbors( neighbor_range.begin(), neighbor_range.end() ); + + const auto support = std::ranges::find_if( neighbors, [&anchor, &here]( const tripoint & pt ) { + return pt.z == anchor.z && pt != anchor && here.impassable_ter_furn( pt ); + } ); + + if( support == neighbors.end() ) { + return std::nullopt; + } + + return *support; +} + +auto wall_support_count( const map &here, const tripoint &anchor ) -> int +{ + const auto neighbor_range = points_in_radius( anchor, 1 ); + auto neighbors = std::vector( neighbor_range.begin(), neighbor_range.end() ); + + const auto is_cardinal_support = [&here]( const tripoint & center, + const tripoint & pt ) { + const bool same_level = pt.z == center.z; + const bool cardinal = pt.x == center.x || pt.y == center.y; + return same_level && cardinal && here.impassable_ter_furn( pt ); + }; + + const auto same_level_supports = std::ranges::count_if( neighbors, + [&anchor, &is_cardinal_support]( const tripoint & pt ) { + return is_cardinal_support( anchor, pt ); + } ); + + if( !here.has_flag( TFLAG_NO_FLOOR, anchor ) ) { + return same_level_supports; + } + + const auto below = anchor + tripoint_below; + const auto below_range = points_in_radius( below, 1 ); + neighbors = std::vector( below_range.begin(), below_range.end() ); + + const auto below_supports = std::ranges::count_if( neighbors, + [&below, &is_cardinal_support]( const tripoint & pt ) { + return is_cardinal_support( below, pt ); + } ); + + return same_level_supports + below_supports; +} + +auto has_wall_support( const map &here, const tripoint &anchor ) -> bool +{ + return wall_support_count( here, anchor ) >= 1; +} + +auto anchored_on_wall( const map &here, const tripoint &pos ) -> bool +{ + const auto supports = wall_support_count( here, pos ); + const tripoint roof_above = pos + tripoint_above; + + if( here.has_flag( "ROOF", roof_above ) && here.inbounds( roof_above ) ) { + return true; + } + + if( here.has_flag( TFLAG_NO_FLOOR, pos ) ) { + const tripoint below = pos + tripoint_below; + if( here.has_floor( below ) ) { + return supports >= 1; + } + return supports >= 2; + } + + return supports >= 1; +} + +} // namespace static const std::string flag_LIQUID( "LIQUID" ); enum { @@ -255,22 +337,29 @@ bool monster::can_reach_to( const tripoint &p ) const const bool is_z_move = p.z != pos().z; if( !is_z_move || is_moving_out_of_reality ) { + if( here.has_flag( TFLAG_NO_FLOOR, p ) && !flies() ) { + if( climbs_walls() ) { + return anchored_on_wall( here, p ); + } + return false; + } return true; } + const auto can_wall_climb = can_wall_climb_to( p ); const bool is_going_up = p.z > pos().z; if( is_going_up ) { const bool has_up_ramp = here.has_flag( TFLAG_RAMP_UP, p + tripoint_below ); const bool has_stairs = here.has_flag( TFLAG_GOES_UP, pos() ); const bool can_fly_there = this->flies() && here.has_flag( TFLAG_NO_FLOOR, p ); - return has_up_ramp || has_stairs || can_fly_there; + return has_up_ramp || has_stairs || can_fly_there || can_wall_climb; } else { const bool has_down_ramp = here.has_flag( TFLAG_RAMP_DOWN, p + tripoint_above ); const bool has_stairs = here.has_flag( TFLAG_GOES_DOWN, pos() ); const bool can_fly_there = this->flies() && here.has_flag( TFLAG_NO_FLOOR, this->pos() ); - return has_down_ramp || has_stairs || can_fly_there; + return has_down_ramp || has_stairs || can_fly_there || can_wall_climb; } } @@ -283,7 +372,39 @@ bool monster::can_squeeze_to( const tripoint &p ) const bool monster::can_move_to( const tripoint &p ) const { - return can_reach_to( p ) && will_move_to( p ) && !has_flag( MF_STATIONARY ); + return can_reach_to( p ) && will_move_to( p ); +} + +auto monster::can_wall_climb_to( const tripoint &p ) const -> bool +{ + if( !climbs_walls() ) { + return false; + } + + const map &here = get_map(); + + if( !here.has_zlevels() ) { + return false; + } + + if( !here.inbounds( p ) || !here.passable( p ) ) { + return false; + } + + const auto from = pos(); + const auto dz = p.z - from.z; + + if( std::abs( dz ) != 1 ) { + return false; + } + + const auto &anchor = dz > 0 ? from : p; + const auto climb_diff = here.climb_difficulty( anchor ); + if( climb_diff > 10 ) { + return false; + } + + return has_wall_support( here, anchor ); } void monster::set_dest( const tripoint &p ) @@ -1191,14 +1312,18 @@ monster_action_t monster::decide_action() const const bool is_z_move = candidate.z != posz(); if( is_z_move ) { bool can_z_attack = fov_3d; + const auto wall_climb_move = can_wall_climb_to( candidate ); if( !here.valid_move( pos(), candidate, false, true, via_ramp ) ) { - can_z_move = false; - can_z_attack = false; + can_z_move = wall_climb_move; + can_z_attack = wall_climb_move && can_z_attack; } - if( can_z_move && candidate.z > posz() && !( via_ramp || flies() ) && - ( !can_climb() || !here.has_floor_or_support( candidate ) ) ) { - can_z_move = false; + if( can_z_move && candidate.z > posz() && !( via_ramp || flies() ) ) { + if( wall_climb_move ) { + // Wall climbers intentionally ignore floor/support checks. + } else if( !can_climb() || !here.has_floor_or_support( candidate ) ) { + can_z_move = false; + } } if( !can_z_move && @@ -1423,14 +1548,35 @@ void monster::execute_action( const monster_action_t &action ) player *dragged_foe = find_dragged_foe(); nursebot_operate( dragged_foe ); - // Floor / drowning / moves-negative guards. - if( !flies() && g->m.has_flag( TFLAG_NO_FLOOR, pos() ) ) { - g->m.creature_on_trap( *this, false ); + // Wall-cling handling plus floor / drowning / moves-negative guards. + const auto anchored_on_wall_now = climbs_walls() && anchored_on_wall( here, pos() ); + const auto anchored_on_wall_then = has_effect( effect_wall_clinging ); + + if( !flies() && !anchored_on_wall_now && + here.has_flag( TFLAG_NO_FLOOR, pos() ) ) { + here.creature_on_trap( *this, false ); if( is_dead() ) { return; } } + if( anchored_on_wall_now ) { + if( !anchored_on_wall_then && g->u.sees( *this ) ) { + const auto support_name = [&here, this]() -> std::string { + const auto support = get_wall_support( here, pos() ); + if( support.has_value() ) + { + return here.disp_name( *support ); + } + return std::string( _( "the wall" ) ); + }(); + add_msg( _( "The %1$s begins to climb up %2$s." ), name(), support_name ); + } + add_effect( effect_wall_clinging, 1_turns ); + } else if( anchored_on_wall_then ) { + remove_effect( effect_wall_clinging ); + } + if( die_if_drowning( pos(), 10 ) ) { return; } @@ -1897,10 +2043,13 @@ int monster::calc_climb_cost( const tripoint &f, const tripoint &t ) const return 100; } - if( climbs() && !g->m.has_flag( TFLAG_NO_FLOOR, t ) ) { - const int diff = g->m.climb_difficulty( f ); + if( climbs() ) { + const auto diff = g->m.climb_difficulty( f ); if( diff <= 10 ) { - return 150; + if( g->m.has_flag( TFLAG_NO_FLOOR, t ) && !climbs_walls() ) { + return 0; + } + return g->m.has_flag( TFLAG_NO_FLOOR, t ) ? 200 : 150; } } @@ -2129,6 +2278,7 @@ bool monster::move_to( const tripoint &p, bool force, bool step_on_critter, const bool going_up = p.z > pos().z; tripoint destination = p; + const bool destination_has_no_floor = g->m.has_flag( TFLAG_NO_FLOOR, destination ); // This is stair teleportation hackery. // TODO: Remove this in favor of stair alignment @@ -2167,6 +2317,7 @@ bool monster::move_to( const tripoint &p, bool force, bool step_on_critter, } } } + const auto wall_climb_move = can_wall_climb_to( destination ); if( critter != nullptr && !step_on_critter ) { return false; @@ -2186,10 +2337,11 @@ bool monster::move_to( const tripoint &p, bool force, bool step_on_critter, // is consistent even if the monster stumbles, // and the same regardless of the distance measurement mode. // Note: Keep this as float here or else it will cancel valid moves - const float cost = stagger_adjustment * - static_cast( climbs() && - g->m.has_flag( TFLAG_NO_FLOOR, p ) ? calc_climb_cost( pos(), destination ) : calc_movecost( pos(), - destination ) ); + const auto use_climb_cost = wall_climb_move || destination_has_no_floor; + const float base_cost = use_climb_cost ? + static_cast( calc_climb_cost( pos(), destination ) ) : + static_cast( calc_movecost( pos(), destination ) ); + const float cost = stagger_adjustment * base_cost; if( cost > 0.0f ) { moves -= static_cast( std::ceil( cost ) ); } else { @@ -2221,6 +2373,17 @@ bool monster::move_to( const tripoint &p, bool force, bool step_on_critter, } } + if( wall_climb_move && z_move && g->u.sees( *this ) ) { + const auto anchor = destination.z > posz() ? pos() : destination; + const auto support = get_wall_support( g->m, anchor ); + const std::string wall_name = support ? + g->m.disp_name( *support ) : + _( "the wall" ); + add_msg( _( "The %1$s climbs %2$s %3$s." ), name(), + destination.z > posz() ? _( "up" ) : _( "down" ), + wall_name ); + } + setpos( destination ); footsteps( destination ); set_underwater( will_be_water ); @@ -2259,10 +2422,22 @@ bool monster::move_to( const tripoint &p, bool force, bool step_on_critter, remove_effect( effect_no_sight ); } - g->m.creature_on_trap( *this ); - if( is_dead() ) { - return true; + const bool anchored_on_wall_move = climbs_walls() && destination_has_no_floor && + anchored_on_wall( g->m, destination ); + if( !( anchored_on_wall_move && on_ground ) ) { + g->m.creature_on_trap( *this ); + if( is_dead() ) { + return true; + } + } + + const bool anchored_on_wall_here = climbs_walls() && anchored_on_wall( g->m, pos() ); + if( anchored_on_wall_here ) { + add_effect( effect_wall_clinging, 1_turns ); + } else if( has_effect( effect_wall_clinging ) ) { + remove_effect( effect_wall_clinging ); } + if( !will_be_water && ( digs() || can_dig() ) ) { set_underwater( g->m.ter( pos() )->is_diggable() ); } diff --git a/src/monster.cpp b/src/monster.cpp index 3a41a14915dc..b07dd622d66d 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -1148,7 +1148,12 @@ bool monster::flies() const bool monster::climbs() const { - return has_flag( MF_CLIMBS ); + return has_flag( MF_CLIMBS ) || climbs_walls(); +} + +auto monster::climbs_walls() const -> bool +{ + return has_flag( MF_CLIMBS_WALLS ); } bool monster::swims() const diff --git a/src/monster.h b/src/monster.h index 306aa03e959d..95645fee2796 100644 --- a/src/monster.h +++ b/src/monster.h @@ -171,12 +171,13 @@ class monster : public Creature, public location_visitable bool can_hear() const; // MF_HEARS and no MF_DEAF bool can_submerge() const; // MF_AQUATIC or swims() or MF_NO_BREATH, and not MF_ELECTRONIC bool can_drown() const; // MF_AQUATIC or swims() or MF_NO_BREATHE or flies() - bool can_climb() const; // climbs() or flies() + bool can_climb() const; // climbs() or climbs_walls() or flies() bool digging() const override; // digs() or can_dig() and diggable terrain bool can_dig() const; bool digs() const; bool flies() const; bool climbs() const; + bool climbs_walls() const; bool swims() const; // Returns false if the monster is stunned, has 0 moves or otherwise wouldn't act this turn bool can_act() const; @@ -212,6 +213,7 @@ class monster : public Creature, public location_visitable bool can_move_to( const tripoint &p ) const; bool can_reach_to( const tripoint &p ) const; bool will_move_to( const tripoint &p ) const; + bool can_wall_climb_to( const tripoint &p ) const; bool can_squeeze_to( const tripoint &p ) const; bool will_reach( point p ); // Do we have plans to get to (x, y)? diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index 986807ef5ef7..94905626dd95 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -156,6 +156,7 @@ std::string enum_to_string( m_flag data ) case MF_CBM_SUBS: return "CBM_SUBS"; case MF_SWARMS: return "SWARMS"; case MF_CLIMBS: return "CLIMBS"; + case MF_CLIMBS_WALLS: return "CLIMBS_WALLS"; case MF_GROUP_MORALE: return "GROUP_MORALE"; case MF_INTERIOR_AMMO: return "INTERIOR_AMMO"; case MF_NIGHT_INVISIBILITY: return "NIGHT_INVISIBILITY"; @@ -1083,16 +1084,20 @@ void mtype::setup_pathfinding_deferred() this->route_settings.f_limit_based_on_max_dist = get_option( "PATHFINDING_MAX_F_LIMIT_BASED_ON_MAX_DIST" ); - const bool default_override = get_option( "PATHFINDING_DEFAULT_IS_OVERRIDE" ); - const float range_mult = get_option( "PATHFINDING_RANGE_MULT" ); + const auto default_override = get_option( "PATHFINDING_DEFAULT_IS_OVERRIDE" ); + const auto range_mult = get_option( "PATHFINDING_RANGE_MULT" ); - if( this->has_flag( MF_CLIMBS ) ) { + const auto climbs_walls = this->has_flag( MF_CLIMBS_WALLS ); + const auto flies = this->has_flag( MF_FLIES ); + + if( this->has_flag( MF_CLIMBS ) || climbs_walls ) { this->legacy_path_settings.climb_cost = 3; this->path_settings.climb_cost = 3.0; } - if( this->has_flag( MF_FLIES ) ) { + if( flies || climbs_walls ) { this->path_settings.can_fly = true; + this->path_settings.needs_wall_cling = climbs_walls && !flies; } const auto extract_into = [this]( std::string field, T & out ) { diff --git a/src/mtype.cpp b/src/mtype.cpp index 553ee4f68937..2e07e2f10872 100644 --- a/src/mtype.cpp +++ b/src/mtype.cpp @@ -355,7 +355,9 @@ void mtype::faction_display( catacurses::window &w, const point &top_left, const if( has_flag( MF_DIGS ) ) { trim_and_print( w, top_left + point( 0, ++y ), width, c_white, _( "It can burrow underground." ) ); } - if( has_flag( MF_CLIMBS ) ) { + if( has_flag( MF_CLIMBS_WALLS ) ) { + trim_and_print( w, top_left + point( 0, ++y ), width, c_white, _( "It can climb walls." ) ); + } else if( has_flag( MF_CLIMBS ) ) { trim_and_print( w, top_left + point( 0, ++y ), width, c_white, _( "It can climb." ) ); } if( has_flag( MF_GRABS ) ) { @@ -369,4 +371,4 @@ void mtype::faction_display( catacurses::window &w, const point &top_left, const } // Description fold_and_print( w, top_left + point( 0, y + 2 ), width, c_light_gray, get_description() ); -} \ No newline at end of file +} diff --git a/src/mtype.h b/src/mtype.h index aeedca3c7148..2382df6fedaa 100644 --- a/src/mtype.h +++ b/src/mtype.h @@ -157,6 +157,7 @@ enum m_flag : int { MF_GROUP_MORALE, // Monsters that are more courageous when near friends MF_INTERIOR_AMMO, // Monster contain's its ammo inside itself, no need to load on launch. Prevents ammo from being dropped on disable. MF_CLIMBS, // Monsters that can climb certain terrain and furniture + MF_CLIMBS_WALLS, // Monsters that can climb walls and other vertical surfaces MF_PACIFIST, // Monsters that will never use melee attack, useful for having them use grab without attacking the player MF_PUSH_MON, // Monsters that can push creatures out of their way MF_PUSH_VEH, // Monsters that can push vehicles out of their way diff --git a/src/pathfinding.cpp b/src/pathfinding.cpp index a8e865a22653..7668e5ac3652 100755 --- a/src/pathfinding.cpp +++ b/src/pathfinding.cpp @@ -56,12 +56,54 @@ static constexpr bool is_inf( float x ) #endif } +namespace +{ + +constexpr auto max_wall_climb_difficulty = 10; + +auto has_cardinal_wall_support( const map &here, const tripoint &anchor ) -> bool +{ + const auto neighbor_range = points_in_radius( anchor, 1 ); + auto neighbors = std::vector( neighbor_range.begin(), neighbor_range.end() ); + + const auto is_cardinal_support = [&here]( const tripoint & center, + const tripoint & pt ) { + const bool same_level = pt.z == center.z; + const bool cardinal = pt.x == center.x || pt.y == center.y; + return same_level && cardinal && here.impassable_ter_furn( pt ); + }; + + const auto same_level_support = std::ranges::any_of( neighbors, + [&anchor, &is_cardinal_support]( const tripoint & pt ) { + return pt != anchor && is_cardinal_support( anchor, pt ); + } ); + + if( same_level_support ) { + return true; + } + + if( !here.has_flag( TFLAG_NO_FLOOR, anchor ) ) { + return false; + } + + const auto below = anchor + tripoint_below; + const auto below_range = points_in_radius( below, 1 ); + neighbors = std::vector( below_range.begin(), below_range.end() ); + + return std::ranges::any_of( neighbors, [&below, &is_cardinal_support]( const tripoint & pt ) { + return is_cardinal_support( below, pt ); + } ); +} + +} // namespace + // PathfindingSettings impls int PathfindingSettings::z_move_type() const { int result = 0; result += this->can_fly ? 1 << 0 : 0; result += this->can_climb_stairs ? 1 << 1 : 0; + result += this->needs_wall_cling ? 1 << 2 : 0; return result; } // RouteSettings impls @@ -919,8 +961,19 @@ std::vector Pathfinding::get_route_3d( // Instead, we will **only** consider taking z_changes that bring us closer to target's Z level. const bool we_go_up = to.z > from.z; + const map &here = get_map(); + Pathfinding::update_z_caches( path_settings.can_fly ); + const auto can_wall_cling_to_change = [&here]( const Pathfinding::ZLevelChange & change, + const bool we_go_up ) { + const tripoint &anchor = we_go_up ? change.from : change.to; + if( here.climb_difficulty( anchor ) > max_wall_climb_difficulty ) { + return false; + } + return has_cardinal_wall_support( here, anchor ); + }; + // Determine our Z-path std::vector z_path; { @@ -1012,18 +1065,26 @@ std::vector Pathfinding::get_route_3d( continue; } Pathfinding::ZLevelChangeOpenAirPair z_pair = target[p]; + const Pathfinding::ZLevelChange *candidate = nullptr; if( we_go_up && z_pair.reach_from_below.has_value() ) { - const float dist = rl_dist_exact( tripoint( cur_origin_point, 0 ), tripoint( p, 0 ) ); - if( dist < best_distance ) { - best_z_change = *z_pair.reach_from_below; - best_distance = dist; - } + candidate = &*z_pair.reach_from_below; } else if( !we_go_up && z_pair.reach_from_above.has_value() ) { - const float dist = rl_dist_exact( tripoint( cur_origin_point, 0 ), tripoint( p, 0 ) ); - if( dist < best_distance ) { - best_z_change = *z_pair.reach_from_above; - best_distance = dist; - } + candidate = &*z_pair.reach_from_above; + } + + if( candidate == nullptr ) { + continue; + } + + if( path_settings.needs_wall_cling && + !can_wall_cling_to_change( *candidate, we_go_up ) ) { + continue; + } + + const float dist = rl_dist_exact( tripoint( cur_origin_point, 0 ), tripoint( p, 0 ) ); + if( dist < best_distance ) { + best_z_change = *candidate; + best_distance = dist; } } } diff --git a/src/pathfinding.h b/src/pathfinding.h index 3b622e4ac5a8..f48a22e27142 100755 --- a/src/pathfinding.h +++ b/src/pathfinding.h @@ -56,6 +56,10 @@ struct PathfindingSettings { // and travel over open air and go up and down from there bool can_fly = false; + // Do we require adjacent wall support to use open-air z-level changes? + // Used by wall climbers that are not true fliers. + bool needs_wall_cling = false; + // Can we climb stairs? `can_fly == true` overrides this value to be true. bool can_climb_stairs = false; @@ -345,4 +349,3 @@ class Pathfinding // such as change in terrain static void mark_dirty_z_cache(); }; - diff --git a/src/trapfunc.cpp b/src/trapfunc.cpp index ea578cda3e5d..ca4e5f9853f2 100644 --- a/src/trapfunc.cpp +++ b/src/trapfunc.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include "avatar.h" #include "bodypart.h" @@ -1122,8 +1124,16 @@ bool trapfunc::ledge( const tripoint &p, Creature *c, item * ) if( c == nullptr ) { return false; } + if( Character *ch = dynamic_cast( c ) ) { + if( ch->is_mounted() ) { + monster *mount = ch->mounted_creature.get(); + if( mount != nullptr && ( mount->flies() || mount->climbs() || mount->climbs_walls() ) ) { + return false; + } + } + } monster *m = dynamic_cast( c ); - if( m != nullptr && m->flies() ) { + if( m != nullptr && ( m->flies() || m->climbs() || m->climbs_walls() ) ) { return false; } if( !g->m.has_zlevels() ) {