diff --git a/src/game_character.cpp b/src/game_character.cpp index 4e71fd57df3..50491eea69f 100644 --- a/src/game_character.cpp +++ b/src/game_character.cpp @@ -33,6 +33,7 @@ #include "rand.h" #include #include +#include Game_Character::Game_Character(Type type, lcf::rpg::SaveMapEventBase* d) : _type(type), _data(d) @@ -469,6 +470,20 @@ bool Game_Character::MakeWay(int from_x, int from_y, int to_x, int to_y) { return Game_Map::MakeWay(*this, from_x, from_y, to_x, to_y); } + +bool Game_Character::CheckWay(int from_x, int from_y, int to_x, int to_y) { + return Game_Map::CheckWay(*this, from_x, from_y, to_x, to_y); +} + + +bool Game_Character::CheckWayEx( + int from_x, int from_y, int to_x, int to_y, bool ignore_all_events, + std::unordered_set *ignore_some_events_by_id) { + return Game_Map::CheckWayEx(*this, from_x, from_y, to_x, to_y, + ignore_all_events, ignore_some_events_by_id); +} + + bool Game_Character::Move(int dir) { if (!IsStopping()) { return true; diff --git a/src/game_character.h b/src/game_character.h index 8d451fe9709..1b091431f81 100644 --- a/src/game_character.h +++ b/src/game_character.h @@ -21,6 +21,7 @@ // Headers #include #include +#include #include "color.h" #include "flash.h" #include @@ -585,6 +586,39 @@ class Game_Character { */ virtual bool MakeWay(int from_x, int from_y, int to_x, int to_y); + /** + * Check if this can move to the given tile, but without + * affecting the map. This is usually what you want to use + * for planning, e.g. path finding, where the move isn't + * meant to be actually executed just yet. + * + * @param from_x Moving from x position + * @param from_y Moving from y position + * @param to_x Moving from x position + * @param to_y Moving from y position + * + * @return true if the hypothetical movement of + * this event from (to_x, to_y) from (from_x, from_y) is possible + */ + virtual bool CheckWay(int from_x, int from_y, int to_x, int to_y); + + /** + * Like CheckWay, but allows ignoring all events in the check, + * or only some events specified by event id. + * + * @param from_x See CheckWay. + * @param from_y See CheckWay. + * @param to_x See CheckWay. + * @param to_y See Checkway. + * @param ignore_all_events If true, only consider map collision + * and completely ignore any events in the way. + * @param ignore_some_events_by_id If specified, all events with + * ids found in this list will be ignored in the collision check. + * @return true See CheckWay. + */ + virtual bool CheckWayEx(int from_x, int from_y, int to_x, int to_y, + bool ignore_all_events, std::unordered_set *ignore_some_events_by_id); + /** * Turns the character 90 Degree to the left. */ diff --git a/src/game_map.cpp b/src/game_map.cpp index 82ebf06d3cf..a1c95af37c3 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "async_handler.h" #include "options.h" @@ -337,7 +338,7 @@ std::unique_ptr Game_Map::loadMapFile(int map_id) { void Game_Map::SetupCommon() { if (!Tr::GetCurrentTranslationId().empty()) { - // Build our map translation id. + // Build our map translation id. std::stringstream ss; ss << "map" << std::setfill('0') << std::setw(4) << GetMapId() << ".po"; @@ -533,7 +534,7 @@ static void MakeWayUpdate(Game_Event& other) { } template -static bool MakeWayCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) { +static bool CheckWayTestCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) { if (&self == &other) { return false; } @@ -542,14 +543,22 @@ static bool MakeWayCollideEvent(int x, int y, const Game_Character& self, T& oth return false; } - // Force the other event to update, allowing them to possibly move out of the way. + return WouldCollide(self, other, self_conflict); +} + +template +static bool MakeWayCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) { + if (!CheckWayTestCollideEvent(x, y, self, other, self_conflict)) + return false; + + // Let the event try to move away: MakeWayUpdate(other); - if (!other.IsInPosition(x, y)) { + if (!other.IsInPosition(x, y)) return false; - } - return WouldCollide(self, other, self_conflict); + // Since it moved away, try again: + return CheckWayTestCollideEvent(x, y, self, other, self_conflict); } static Game_Vehicle::Type GetCollisionVehicleType(const Game_Character* ch) { @@ -559,18 +568,53 @@ static Game_Vehicle::Type GetCollisionVehicleType(const Game_Character* ch) { return Game_Vehicle::None; } -bool Game_Map::MakeWay(const Game_Character& self, +bool Game_Map::CheckWay(const Game_Character& self, int from_x, int from_y, int to_x, int to_y ) +{ + return CheckWayEx( + self, from_x, from_y, to_x, to_y, false, NULL, + NULL, NULL, NULL, NULL, NULL + ); +} + +bool Game_Map::CheckWayEx(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y, + bool ignore_events_and_vehicles, + std::unordered_set *ignore_some_events_by_id) { + return CheckWayEx( + self, from_x, from_y, to_x, to_y, + ignore_events_and_vehicles, + ignore_some_events_by_id, + NULL, NULL, NULL, NULL, NULL + ); +} + +bool Game_Map::CheckWayEx(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y, + bool ignore_events_and_vehicles, + std::unordered_set *ignore_some_events_by_id, + int *out_bit_from, + int *out_bit_to, + int *out_to_x, + int *out_to_y, + bool *out_self_conflict + ) { // Infer directions before we do any rounding. - const auto bit_from = GetPassableMask(from_x, from_y, to_x, to_y); - const auto bit_to = GetPassableMask(to_x, to_y, from_x, from_y); + const int bit_from = GetPassableMask(from_x, from_y, to_x, to_y); + const int bit_to = GetPassableMask(to_x, to_y, from_x, from_y); + if (out_bit_from) *out_bit_from = bit_from; + if (out_bit_to) *out_bit_to = bit_to; // Now round for looping maps. to_x = Game_Map::RoundX(to_x); to_y = Game_Map::RoundY(to_y); + if (out_to_x) *out_to_x = to_x; + if (out_to_y) *out_to_y = to_y; // Note, even for diagonal, if the tile is invalid we still check vertical/horizontal first! if (!Game_Map::IsValid(to_x, to_y)) { @@ -582,7 +626,6 @@ bool Game_Map::MakeWay(const Game_Character& self, } const auto vehicle_type = GetCollisionVehicleType(&self); - bool self_conflict = false; if (!self.IsJumping()) { // Check for self conflict. @@ -611,7 +654,63 @@ bool Game_Map::MakeWay(const Game_Character& self, } } } + if (out_self_conflict) *out_self_conflict = self_conflict; + if (vehicle_type != Game_Vehicle::Airship && !ignore_events_and_vehicles) { + // Check for collision with events on the target tile. + for (auto& other: GetEvents()) { + if (ignore_some_events_by_id != NULL && + ignore_some_events_by_id->find(other.GetId()) != + ignore_some_events_by_id->end()) + continue; + if (CheckWayTestCollideEvent(to_x, to_y, self, other, self_conflict)) { + return false; + } + } + auto& player = Main_Data::game_player; + if (player->GetVehicleType() == Game_Vehicle::None) { + if (CheckWayTestCollideEvent(to_x, to_y, self, *Main_Data::game_player, self_conflict)) { + return false; + } + } + for (auto vid: { Game_Vehicle::Boat, Game_Vehicle::Ship}) { + auto& other = vehicles[vid - 1]; + if (other.IsInCurrentMap()) { + if (CheckWayTestCollideEvent(to_x, to_y, self, other, self_conflict)) { + return false; + } + } + } + auto& airship = vehicles[Game_Vehicle::Airship - 1]; + if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) { + if (CheckWayTestCollideEvent(to_x, to_y, self, airship, self_conflict)) { + return false; + } + } + } + int bit = bit_to; + if (self.IsJumping()) { + bit = Passable::Down | Passable::Up | Passable::Left | Passable::Right; + } + + return IsPassableTile( + &self, bit, to_x, to_y, !ignore_events_and_vehicles, true + ); +} + +bool Game_Map::MakeWay(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y + ) +{ + // First, check basic passability but ignoring all events: + int bit_from, bit_to; + bool self_conflict; + if (!CheckWayEx(self, from_x, from_y, to_x, to_y, true, NULL, + &bit_from, &bit_to, + &to_x, &to_y, &self_conflict)) + return false; + const auto vehicle_type = GetCollisionVehicleType(&self); if (vehicle_type != Game_Vehicle::Airship) { // Check for collision with events on the target tile. for (auto& other: GetEvents()) { @@ -646,7 +745,7 @@ bool Game_Map::MakeWay(const Game_Character& self, bit = Passable::Down | Passable::Up | Passable::Left | Passable::Right; } - return IsPassableTile(&self, bit, to_x, to_y); + return IsPassableTile(&self, bit, to_x, to_y, true, false); } bool Game_Map::CanLandAirship(int x, int y) { @@ -742,77 +841,93 @@ bool Game_Map::IsPassableLowerTile(int bit, int tile_index) { return (passages_down[tile_id] & bit) != 0; } -bool Game_Map::IsPassableTile(const Game_Character* self, int bit, int x, int y) { +bool Game_Map::IsPassableTile( + const Game_Character* self, int bit, int x, int y + ) { + return IsPassableTile( + self, bit, x, y, true, true + ); +} + +bool Game_Map::IsPassableTile( + const Game_Character* self, int bit, int x, int y, + bool check_events_and_vehicles, bool check_map_geometry + ) { if (!IsValid(x, y)) return false; const auto vehicle_type = GetCollisionVehicleType(self); - - if (vehicle_type != Game_Vehicle::None) { - const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y)); - if (!terrain) { - Output::Warning("IsPassableTile: Invalid terrain at ({}, {})", x, y); - return false; - } - if (vehicle_type == Game_Vehicle::Boat && !terrain->boat_pass) { - return false; - } - if (vehicle_type == Game_Vehicle::Ship && !terrain->ship_pass) { - return false; - } - if (vehicle_type == Game_Vehicle::Airship) { - return terrain->airship_pass; + if (check_events_and_vehicles) { + if (vehicle_type != Game_Vehicle::None) { + const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y)); + if (!terrain) { + Output::Warning("IsPassableTile: Invalid terrain at ({}, {})", x, y); + return false; + } + if (vehicle_type == Game_Vehicle::Boat && !terrain->boat_pass) { + return false; + } + if (vehicle_type == Game_Vehicle::Ship && !terrain->ship_pass) { + return false; + } + if (vehicle_type == Game_Vehicle::Airship) { + return terrain->airship_pass; + } } - } - // Highest ID event with layer=below, not through, and a tile graphic wins. - int event_tile_id = 0; - for (auto& ev: events) { - if (self == &ev) { - continue; - } - if (!ev.IsActive() || ev.GetActivePage() == nullptr || ev.GetThrough()) { - continue; - } - if (ev.IsInPosition(x, y) && ev.GetLayer() == lcf::rpg::EventPage::Layers_below) { - int tile_id = ev.GetTileId(); - if (tile_id > 0) { - event_tile_id = tile_id; + // Highest ID event with layer=below, not through, and a tile graphic wins. + int event_tile_id = 0; + for (auto& ev: events) { + if (self == &ev) { + continue; + } + if (!ev.IsActive() || ev.GetActivePage() == nullptr || ev.GetThrough()) { + continue; + } + if (ev.IsInPosition(x, y) && ev.GetLayer() == lcf::rpg::EventPage::Layers_below) { + int tile_id = ev.GetTileId(); + if (tile_id > 0) { + event_tile_id = tile_id; + } } } - } - // If there was a below tile event, and the tile is not above - // Override the chipset with event tile behavior. - if (event_tile_id > 0 - && ((passages_up[event_tile_id] & Passable::Above) == 0)) { - switch (vehicle_type) { - case Game_Vehicle::None: - return ((passages_up[event_tile_id] & bit) != 0); - case Game_Vehicle::Boat: - case Game_Vehicle::Ship: - return false; - case Game_Vehicle::Airship: - break; - }; + // If there was a below tile event, and the tile is not above + // Override the chipset with event tile behavior. + if (event_tile_id > 0 + && ((passages_up[event_tile_id] & Passable::Above) == 0)) { + switch (vehicle_type) { + case Game_Vehicle::None: + return ((passages_up[event_tile_id] & bit) != 0); + case Game_Vehicle::Boat: + case Game_Vehicle::Ship: + return false; + case Game_Vehicle::Airship: + break; + }; + } } - int tile_index = x + y * GetTilesX(); - int tile_id = map->upper_layer[tile_index] - BLOCK_F; - tile_id = map_info.upper_tiles[tile_id]; + if (check_map_geometry) { + int tile_index = x + y * GetTilesX(); + int tile_id = map->upper_layer[tile_index] - BLOCK_F; + tile_id = map_info.upper_tiles[tile_id]; - if (vehicle_type == Game_Vehicle::Boat || vehicle_type == Game_Vehicle::Ship) { - if ((passages_up[tile_id] & Passable::Above) == 0) + if (vehicle_type == Game_Vehicle::Boat || vehicle_type == Game_Vehicle::Ship) { + if ((passages_up[tile_id] & Passable::Above) == 0) + return false; + return true; + } + + if ((passages_up[tile_id] & bit) == 0) return false; - return true; - } - if ((passages_up[tile_id] & bit) == 0) - return false; + if ((passages_up[tile_id] & Passable::Above) == 0) + return true; - if ((passages_up[tile_id] & Passable::Above) == 0) + return IsPassableLowerTile(bit, tile_index); + } else { return true; - - return IsPassableLowerTile(bit, tile_index); + } } int Game_Map::GetBushDepth(int x, int y) { diff --git a/src/game_map.h b/src/game_map.h index 074c3746bf7..008370b2b5e 100644 --- a/src/game_map.h +++ b/src/game_map.h @@ -22,6 +22,7 @@ #include #include #include +#include #include "system.h" #include "game_commonevent.h" #include "game_event.h" @@ -91,7 +92,7 @@ namespace Game_Map { */ void Quit(); - /** Disposes Game_Map. */ + /** Disposes Game_Map. */ void Dispose(); /** @@ -198,6 +199,61 @@ namespace Game_Map { int from_x, int from_y, int to_x, int to_y); + /** + * Check if a move of self is possible to (to_x,to_y). + * Any events that are blocked will also be checked for collision. + * + * Returns true if move is possible. + * + * @param self Character to move. + * @param from_x from tile x. + * @param from_y from tile y. + * @param to_x to new tile x. + * @param to_y to new tile y. + * @return whether move is possible. + */ + bool CheckWay(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y); + + /** + * Extended function of CheckWay that spits out some + * additional computed values for use in MakeWay. + * + * @param self See CheckWay. + * @param from_x See CheckWay. + * @param from_y See CheckWay. + * @param to_x See CheckWay. + * @param to_y See CheckWay. + * @param ignore_events_and_vehicles Whether to ignore + * all events and vehicles and only check map geometry. + * @param ignore_some_events_by_id Ignore some specific + * events by ID. + * @param out_bit_from Outputs bitmap mask for passability. + * @param out_bit_to Outputs bitmap mask for passability. + * @param out_to_x Target pos adjusted for map repeat. + * @param out_to_y Target pos adjusted for map repeat. + * @param out_self_conflict Outputs whether the moving self + * has a tile graphic that conflicts with the movement + * direction. + * @return See CheckWay. + */ + bool CheckWayEx(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y, + bool ignore_events_and_vehicles, + std::unordered_set *ignore_some_events_by_id, + int *out_bit_from, + int *out_bit_to, + int *out_to_x, + int *out_to_y, + bool *out_self_conflict); + bool CheckWayEx(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y, + bool ignore_all_events, + std::unordered_set *ignore_some_events_by_id); + /** * Gets if possible to land the airship at (x,y) * @@ -569,8 +625,14 @@ namespace Game_Map { * @param bit which direction bits to check * @param x target tile x. * @param y target tile y. + * @param check_events_and_vehicles Whether to consider events and vehicles. + * @param check_map_geometry Whether to take map collision into account. * @return whether is passable. */ + bool IsPassableTile( + const Game_Character* self, int bit, int x, int y, + bool check_events_and_vehicles, bool check_map_geometry + ); bool IsPassableTile(const Game_Character* self, int bit, int x, int y); /**