From 47b197a54376247e802475d3ee5182081c330b9a Mon Sep 17 00:00:00 2001 From: LizardKing777 Date: Sun, 19 Jul 2026 19:40:06 -0600 Subject: [PATCH 1/2] modified: src/game_character.cpp modified: src/game_character.h modified: src/game_event.cpp modified: src/game_map.cpp modified: src/game_player.cpp Fresh build off the latest upstream source. Adds checks Event triggering and Vehicle boarding/uboarding code to ensure new code is only executing when {_X} and/or {!Y} strings are detected. --- src/game_character.cpp | 162 ++++++++++++++++- src/game_character.h | 24 +++ src/game_event.cpp | 81 +++++++-- src/game_map.cpp | 275 +++++++++++++++++++--------- src/game_player.cpp | 393 +++++++++++++++++++++++++++++++---------- 5 files changed, 739 insertions(+), 196 deletions(-) diff --git a/src/game_character.cpp b/src/game_character.cpp index b9ade33e44..c12eb4233e 100644 --- a/src/game_character.cpp +++ b/src/game_character.cpp @@ -1123,7 +1123,32 @@ int Game_Character::GetSpriteY() const { } bool Game_Character::IsInPosition(int x, int y) const { - return ((GetX() == x) && (GetY() == y)); + int x1, x2, y1, y2; + GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + if (!is_expanded) { + // Standard Branch: Exact coordinate match + return GetX() == x && GetY() == y; + } + + // Extended Branch: Rectangle footprint match + if (GetY() + y1 > y || GetY() + y2 < y) { + return false; + } + + int left_edge = GetX() + x1; + int right_edge = GetX() + x2; + + if (Game_Map::LoopHorizontal()) { + int map_w = Game_Map::GetTilesX(); + for (int i = left_edge; i <= right_edge; ++i) { + if (Utils::PositiveModulo(i, map_w) == x) return true; + } + return false; + } + + return x >= left_edge && x <= right_edge; } int Game_Character::GetOpacity() const { @@ -1231,3 +1256,138 @@ void Game_Character::UpdateFacing() { SetFacing(dir); } } + +void Game_Character::GetTileOffsets(int& min_dx, int& max_dx, int& min_dy, int& max_dy) const { + min_dx = 0; + max_dx = 0; + min_dy = 0; + max_dy = 0; + + if (!_data) { + return; + } + + std::string_view name = _data->sprite_name; + if (name.empty()) { + return; + } + + // --- X-Axis Parsing: {_#ModeBias} --- + size_t x_bracket = name.find("{_"); + if (x_bracket != std::string_view::npos) { + size_t close = name.find("}", x_bracket); + if (close != std::string_view::npos) { + int width = 0; + char bias = '\0'; + char mode = '\0'; + std::string_view tag = name.substr(x_bracket + 2, close - (x_bracket + 2)); + for (size_t i = 0; i < tag.length(); ++i) { + char c = tag[i]; + if (c >= '0' && c <= '9') { + width = width * 10 + (c - '0'); + } else if (c == 'L' || c == 'l') { + bias = 'L'; + } else if (c == 'R' || c == 'r') { + bias = 'R'; + } else if (c == '-' || c == '+') { + mode = c; + } + } + if (width > 0) { + if (mode == '-') { + min_dx = -(width - 1); + max_dx = 0; + } else if (mode == '+') { + min_dx = 0; + max_dx = (width - 1); + } else { + if (width > 1 && width % 2 == 0 && bias == '\0') { + width--; + } + if (width > 1 && width % 2 == 0) { + int r = (width - 1) / 2; + if (bias == 'L') { + min_dx = -(r + 1); + max_dx = r; + } else { + min_dx = -r; + max_dx = r + 1; + } + } else { + int r = width / 2; + min_dx = -r; + max_dx = r; + } + } + } + } + } + + // --- Y-Axis Parsing: {!#ModeBias} --- + size_t y_bracket = name.find("{!"); + if (y_bracket != std::string_view::npos) { + size_t close = name.find("}", y_bracket); + if (close != std::string_view::npos) { + int height = 0; + char bias = '\0'; + char mode = '\0'; + std::string_view tag = name.substr(y_bracket + 2, close - (y_bracket + 2)); + for (size_t i = 0; i < tag.length(); ++i) { + char c = tag[i]; + if (c >= '0' && c <= '9') { + height = height * 10 + (c - '0'); + } else if (c == 'U' || c == 'u') { + bias = 'U'; + } else if (c == 'D' || c == 'd') { + bias = 'D'; + } else if (c == '^' || c == 'v' || c == '=') { + mode = c; + } + } + + if (mode == '\0') { + mode = '^'; + } + + if (height > 0) { + if (mode == '^') { + min_dy = -(height - 1); + max_dy = 0; + } else if (mode == 'v') { + min_dy = 0; + max_dy = (height - 1); + } else { + if (height > 1 && height % 2 == 0 && bias == '\0') { + height--; + } + if (height > 1 && height % 2 == 0) { + int r = (height - 1) / 2; + if (bias == 'U') { + min_dy = -(r + 1); + max_dy = r; + } else { + min_dy = -r; + max_dy = r + 1; + } + } else { + int r = height / 2; + min_dy = -r; + max_dy = r; + } + } + } + } + } +} + +int Game_Character::GetTileWidth() const { + int x1, x2, y1, y2; + GetTileOffsets(x1, x2, y1, y2); + return (x2 - x1) + 1; +} + +int Game_Character::GetTileHeight() const { + int x1, x2, y1, y2; + GetTileOffsets(x1, x2, y1, y2); + return (y2 - y1) + 1; +} diff --git a/src/game_character.h b/src/game_character.h index 2549fe516d..2cf1691021 100644 --- a/src/game_character.h +++ b/src/game_character.h @@ -808,6 +808,27 @@ class Game_Character { */ int GetDistanceYfromCharacter(const Game_Character& target) const; + /** + * Calculates the relative tile offsets based on filename tags. + * min_dx/max_dx handle horizontal ({_#}) and min_dy/max_dy handle vertical ({!#}). + */ + void GetTileOffsets(int& min_dx, int& max_dx, int& min_dy, int& max_dy) const; + + /** Returns the total width of the character in 16px tiles. */ + int GetTileWidth() const; + + /** Returns the total height of the character in 16px tiles. */ + int GetTileHeight() const; + + /** Returns the collision radius in units (1.0 = 16px) for pixel movement. */ + float GetHitboxRadius() const; + + /** Returns the absolute X center of the expanded hitbox in logical units. */ + float GetHitboxCenterX() const; + + /** Returns the absolute Y center of the expanded hitbox in logical units. */ + float GetHitboxCenterY() const; + /** * Tests if the character is currently on the tile at x/y or moving * towards it. @@ -815,6 +836,9 @@ class Game_Character { * @param x X tile position * @param y Y tile position * @return If on tile or moving towards + * Checks if the character footprint overlaps the given tile coordinates. + * Overrides the standard 1-to-1 coordinate check. + */ virtual bool IsInPosition(int x, int y) const; diff --git a/src/game_event.cpp b/src/game_event.cpp index 9eef4decda..835230bfde 100644 --- a/src/game_event.cpp +++ b/src/game_event.cpp @@ -359,34 +359,77 @@ bool Game_Event::CheckEventCollision() { if (GetTrigger() == lcf::rpg::EventPage::Trigger_collision && GetLayer() != lcf::rpg::EventPage::Layers_same && !Main_Data::game_player->IsMoveRouteOverwritten() - && !Game_Map::GetInterpreter().IsRunning() - && Main_Data::game_player->GetX() == GetX() - && Main_Data::game_player->GetY() == GetY()) + && !Game_Map::GetInterpreter().IsRunning()) { - ScheduleForegroundExecution(false, true); - SetStopCount(0); - return true; + int x1, x2, y1, y2; + GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + if (!is_expanded) { + // Standard logic + if (Main_Data::game_player->GetX() == GetX() && Main_Data::game_player->GetY() == GetY()) { + ScheduleForegroundExecution(false, true); + SetStopCount(0); + return true; + } + return false; + } + + for (int dy = y1; dy <= y2; ++dy) { + for (int dx = x1; dx <= x2; ++dx) { + if (Main_Data::game_player->IsInPosition(Game_Map::RoundX(GetX() + dx), Game_Map::RoundY(GetY() + dy))) { + ScheduleForegroundExecution(false, true); + SetStopCount(0); + return true; + } + } + } } return false; } void Game_Event::CheckCollisonOnMoveFailure() { - if (Game_Map::GetInterpreter().IsRunning()) { + if (Game_Map::GetInterpreter().IsRunning()) return; + + int x1, x2, y1, y2; + GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + if (!is_expanded) { + // Standard logic + int fx = Game_Map::XwithDirection(GetX(), GetDirection()); + int fy = Game_Map::YwithDirection(GetY(), GetDirection()); + if (Main_Data::game_player->GetX() == fx && Main_Data::game_player->GetY() == fy && + GetLayer() == lcf::rpg::EventPage::Layers_same && GetTrigger() == lcf::rpg::EventPage::Trigger_collision) { + ScheduleForegroundExecution(false, true); + SetStopCount(0); + } return; } - const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - - if (Main_Data::game_player->GetX() == front_x - && Main_Data::game_player->GetY() == front_y - && GetLayer() == lcf::rpg::EventPage::Layers_same - && GetTrigger() == lcf::rpg::EventPage::Trigger_collision) - { - ScheduleForegroundExecution(false, true); - // Events with trigger collision and layer same always reset their - // stop_count when they fail movement to a tile that the player inhabits. - SetStopCount(0); + int dir = GetDirection(); + if (dir == Up || dir == Down) { + int target_dy = (dir == Up) ? y1 : y2; + for (int dx = x1; dx <= x2; ++dx) { + int fx = Game_Map::RoundX(GetX() + dx); + int fy = Game_Map::RoundY(Game_Map::YwithDirection(GetY() + target_dy, dir)); + if (Main_Data::game_player->IsInPosition(fx, fy) && GetLayer() == lcf::rpg::EventPage::Layers_same && GetTrigger() == lcf::rpg::EventPage::Trigger_collision) { + ScheduleForegroundExecution(false, true); + SetStopCount(0); + return; + } + } + } else { + int target_dx = (dir == Left) ? x1 : x2; + for (int dy = y1; dy <= y2; ++dy) { + int fx = Game_Map::RoundX(Game_Map::XwithDirection(GetX() + target_dx, dir)); + int fy = Game_Map::RoundY(GetY() + dy); + if (Main_Data::game_player->IsInPosition(fx, fy) && GetLayer() == lcf::rpg::EventPage::Layers_same && GetTrigger() == lcf::rpg::EventPage::Trigger_collision) { + ScheduleForegroundExecution(false, true); + SetStopCount(0); + return; + } + } } } diff --git a/src/game_map.cpp b/src/game_map.cpp index b4acc5b50b..cf8d5224a9 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -825,112 +825,225 @@ bool Game_Map::CheckOrMakeWayEx(const Game_Character& self, bool make_way ) { - // Infer directions before we do any rounding. - 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); - - // Now round for looping maps. - to_x = Game_Map::RoundX(to_x); - to_y = Game_Map::RoundY(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)) { - return false; - } - if (self.GetThrough()) { return true; } - const auto vehicle_type = GetCollisionVehicleType(&self); - bool self_conflict = false; + // Retrieve offsets to determine if this is an expanded-hitbox character + int min_dx, max_dx, min_dy, max_dy; + self.GetTileOffsets(min_dx, max_dx, min_dy, max_dy); - // Depending on whether we're supposed to call MakeWayCollideEvent - // (which might change the map) or not, choose what to call: - auto CheckOrMakeCollideEvent = [&](auto& other) { - if (make_way) { - return MakeWayCollideEvent(to_x, to_y, self, other, self_conflict); - } else { - return CheckWayTestCollideEvent( - to_x, to_y, self, other, self_conflict - ); - } - }; + // Detect if any expansion tags are active + bool is_expanded = (min_dx != 0 || max_dx != 0 || min_dy != 0 || max_dy != 0); - if (!self.IsJumping()) { - // Check for self conflict. - // If this event has a tile graphic and the tile itself has passage blocked in the direction - // we want to move, flag it as "self conflicting" for use later. - if (self.GetLayer() == lcf::rpg::EventPage::Layers_below && self.GetTileId() != 0) { - int tile_id = self.GetTileId(); - if ((passages_up[tile_id] & bit_from) == 0) { - self_conflict = true; - } + // --- BRANCH 1: ORIGINAL LOGIC (Standard 1-tile events) --- + if (!is_expanded) { + // Infer directions before we do any rounding. + 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); + + // Now round for looping maps. + int t_to_x = Game_Map::RoundX(to_x); + int t_to_y = Game_Map::RoundY(to_y); + + // Note, even for diagonal, if the tile is invalid we still check vertical/horizontal first! + if (!Game_Map::IsValid(t_to_x, t_to_y)) { + return false; } - if (vehicle_type == Game_Vehicle::None) { - // Check that we are allowed to step off of the current tile. - // Note: Vehicles can always step off a tile. + const auto vehicle_type = GetCollisionVehicleType(&self); + bool self_conflict = false; - // The current coordinate can be invalid due to an out-of-bounds teleport or a "Set Location" event. - // Round it for looping maps to ensure the check passes - // This is not fully bug compatible to RPG_RT. Assuming the Y-Coordinate is out-of-bounds: When moving - // left or right the invalid Y will stay in RPG_RT preventing events from being triggered, but we wrap it - // inbounds after the first move. - from_x = Game_Map::RoundX(from_x); - from_y = Game_Map::RoundY(from_y); - if (!IsPassableTile(&self, bit_from, from_x, from_y)) { - return false; + // Depending on whether we're supposed to call MakeWayCollideEvent + // (which might change the map) or not, choose what to call: + auto CheckOrMakeCollideEvent = [&](auto& other) { + if (make_way) { + return MakeWayCollideEvent(t_to_x, t_to_y, self, other, self_conflict); + } else { + return CheckWayTestCollideEvent( + t_to_x, t_to_y, self, other, self_conflict + ); } - } - } - if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) { - // Check for collision with events on the target tile. - if (ignore_some_events_by_id.empty()) { - for (auto& other: GetEvents()) { - if (CheckOrMakeCollideEvent(other)) { - return false; + }; + + if (!self.IsJumping()) { + // Check for self conflict. + // If this event has a tile graphic and the tile itself has passage blocked in the direction + // we want to move, flag it as "self conflicting" for use later. + if (self.GetLayer() == lcf::rpg::EventPage::Layers_below && self.GetTileId() != 0) { + int tile_id = self.GetTileId(); + if ((passages_up[tile_id] & bit_from) == 0) { + self_conflict = true; } } - } else { - for (auto& other: GetEvents()) { - if (std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end()) - continue; - if (CheckOrMakeCollideEvent(other)) { + + if (vehicle_type == Game_Vehicle::None) { + // Check that we are allowed to step off of the current tile. + // Note: Vehicles can always step off a tile. + + // The current coordinate can be invalid due to an out-of-bounds teleport or a "Set Location" event. + // Round it for looping maps to ensure the check passes + // This is not fully bug compatible to RPG_RT. Assuming the Y-Coordinate is out-of-bounds: When moving + // left or right the invalid Y will stay in RPG_RT preventing events from being triggered, but we wrap it + // inbounds after the first move. + int t_from_x = Game_Map::RoundX(from_x); + int t_from_y = Game_Map::RoundY(from_y); + if (!IsPassableTile(&self, bit_from, t_from_x, t_from_y)) { return false; } } } - auto& player = Main_Data::game_player; - if (player->GetVehicleType() == Game_Vehicle::None) { - if (CheckOrMakeCollideEvent(*Main_Data::game_player)) { - return false; + if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) { + // Check for collision with events on the target tile. + if (ignore_some_events_by_id.empty()) { + for (auto& other : GetEvents()) { + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + } else { + for (auto& other : GetEvents()) { + if (std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end()) + continue; + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } } - } - for (auto vid: { Game_Vehicle::Boat, Game_Vehicle::Ship}) { - auto& other = vehicles[vid - 1]; - if (other.IsInCurrentMap()) { - if (CheckOrMakeCollideEvent(other)) { + + auto& player = Main_Data::game_player; + if (player->GetVehicleType() == Game_Vehicle::None) { + if (CheckOrMakeCollideEvent(*Main_Data::game_player)) { return false; } } - } - auto& airship = vehicles[Game_Vehicle::Airship - 1]; - if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) { - if (CheckOrMakeCollideEvent(airship)) { - return false; + for (auto vid : { Game_Vehicle::Boat, Game_Vehicle::Ship }) { + auto& other = vehicles[vid - 1]; + if (other.IsInCurrentMap()) { + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + } + auto& airship = vehicles[Game_Vehicle::Airship - 1]; + if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) { + if (CheckOrMakeCollideEvent(airship)) { + return false; + } } } - } - int bit = bit_to; - if (self.IsJumping()) { - bit = Passable::Down | Passable::Up | Passable::Left | Passable::Right; + + int bit = bit_to; + if (self.IsJumping()) bit = Passable::Down; + + return IsPassableTile(&self, bit, t_to_x, t_to_y, check_events_and_vehicles, true); } - return IsPassableTile( - &self, bit, to_x, to_y, check_events_and_vehicles, true - ); + // --- BRANCH 2: EXPANDED LOGIC (Large events with tags) --- + else { + const auto vehicle_type = GetCollisionVehicleType(&self); + + // Iterate through every tile in the footprint (Width x Height) + for (int dy = min_dy; dy <= max_dy; ++dy) { + for (int dx = min_dx; dx <= max_dx; ++dx) { + // Calculate coordinates for this specific segment of the footprint + int cur_from_x = from_x + dx; + int cur_from_y = from_y + dy; + int cur_to_x = to_x + dx; + int cur_to_y = to_y + dy; + + // Infer directions for this specific segment + const int bit_from = GetPassableMask(cur_from_x, cur_from_y, cur_to_x, cur_to_y); + const int bit_to = GetPassableMask(cur_to_x, cur_to_y, cur_from_x, cur_from_y); + + // Round coordinates for looping maps + int tile_from_x = Game_Map::RoundX(cur_from_x); + int tile_from_y = Game_Map::RoundY(cur_from_y); + int tile_to_x = Game_Map::RoundX(cur_to_x); + int tile_to_y = Game_Map::RoundY(cur_to_y); + + // Fail if any segment of the expanded box moves out of map bounds + if (!Game_Map::IsValid(tile_to_x, tile_to_y)) { + return false; + } + + bool self_conflict = false; + + // Check for self conflict for this segment + if (!self.IsJumping() && self.GetLayer() == lcf::rpg::EventPage::Layers_same && self.GetTileId() != 0) { + int tile_id = self.GetTileId(); + if ((passages_up[tile_id] & bit_from) == 0) { + self_conflict = true; + } + } + + // Helper for checking collisions with other entities for this segment + auto CheckOrMakeCollideEvent = [&](auto& other) { + if (make_way) { + return MakeWayCollideEvent(tile_to_x, tile_to_y, self, other, self_conflict); + } else { + return CheckWayTestCollideEvent(tile_to_x, tile_to_y, self, other, self_conflict); + } + }; + + // 1. Check Map Passability (Environment/Terrain/Vehicles) + if (!self.IsJumping()) { + if (vehicle_type == Game_Vehicle::None) { + // Segment must be able to step OFF its current tile + if (!IsPassableTile(&self, bit_from, tile_from_x, tile_from_y, check_events_and_vehicles, true)) { + return false; + } + } + } + + // Segment must be able to step ONTO its target tile + int bit_onto = self.IsJumping() ? Passable::Down : bit_to; + if (!IsPassableTile(&self, bit_onto, tile_to_x, tile_to_y, check_events_and_vehicles, true)) { + return false; + } + + // 2. Check Entity Collisions + if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) { + // Check collisions with all Map Events + for (auto& other : GetEvents()) { + if (!ignore_some_events_by_id.empty() && std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end()) + continue; + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + + // Check collision with Player + auto& player = Main_Data::game_player; + if (player->GetVehicleType() == Game_Vehicle::None) { + if (CheckOrMakeCollideEvent(*Main_Data::game_player)) { + return false; + } + } + + // Check collision with Boats/Ships + for (auto vid : { Game_Vehicle::Boat, Game_Vehicle::Ship }) { + auto& other = vehicles[vid - 1]; + if (other.IsInCurrentMap()) { + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + } + + // Check collision with Airship + auto& airship = vehicles[Game_Vehicle::Airship - 1]; + if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) { + if (CheckOrMakeCollideEvent(airship)) { + return false; + } + } + } + } + } + return true; + } } bool Game_Map::MakeWay(const Game_Character& self, diff --git a/src/game_player.cpp b/src/game_player.cpp index e52f73f46c..26da0ba48a 100644 --- a/src/game_player.cpp +++ b/src/game_player.cpp @@ -326,27 +326,38 @@ void Game_Player::UpdateNextMovementAction() { int move_dir = -1; switch (Input::dir4) { - case 2: - move_dir = Down; - break; - case 4: - move_dir = Left; - break; - case 6: - move_dir = Right; - break; - case 8: - move_dir = Up; - break; + case 2: move_dir = Down; break; + case 4: move_dir = Left; break; + case 6: move_dir = Right; break; + case 8: move_dir = Up; break; } + if (move_dir >= 0) { SetThrough((Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) || data()->move_route_through); Move(move_dir); ResetThrough(); + if (IsStopping()) { - int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, false); + int x1, x2, y1, y2; + const Game_Character* active_char = IsAboard() ? static_cast(GetVehicle()) : static_cast(this); + active_char->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + if (!is_expanded) { + // --- BRANCH: ORIGINAL LOGIC --- + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, false); + } else { + // --- BRANCH: EXTENDED BOUNDARIES --- + int dir = GetDirection(); + // Scan the leading edge of the expanded 2D block + for (int i = (dir == Up || dir == Down ? x1 : y1); i <= (dir == Up || dir == Down ? x2 : y2); ++i) { + int fx = GetX() + (dir == Up || dir == Down ? i : (dir == Left ? x1 - 1 : x2 + 1)); + int fy = GetY() + (dir == Left || dir == Right ? i : (dir == Up ? y1 - 1 : y2 + 1)); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, Game_Map::RoundX(fx), Game_Map::RoundY(fy), false); + } + } } } @@ -423,26 +434,67 @@ bool Game_Player::CheckActionEvent() { return false; } + int x1, x2, y1, y2; + const Game_Character* active_char = IsAboard() ? static_cast(GetVehicle()) : static_cast(this); + active_char->GetTileOffsets(x1, x2, y1, y2); + bool player_is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + if (!player_is_expanded) { + // --- BRANCH: ORIGINAL LOGIC --- + bool result = false; + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + + result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, true); + result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true); + + bool got_action = CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); + for (int i = 0; !got_action && i < 3; ++i) { + if (!Game_Map::IsCounter(front_x, front_y)) { + break; + } + + front_x = Game_Map::XwithDirection(front_x, GetDirection()); + front_y = Game_Map::YwithDirection(front_y, GetDirection()); + + got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); + } + return result || got_action; + } + + // --- BRANCH: EXTENDED BOUNDARIES --- + int dir = GetDirection(); bool result = false; - int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, true); + for (int i = (dir == Up || dir == Down ? x1 : y1); i <= (dir == Up || dir == Down ? x2 : y2); ++i) { + int fx = GetX() + (dir == Up || dir == Down ? i : (dir == Left ? x1 - 1 : x2 + 1)); + int fy = GetY() + (dir == Left || dir == Right ? i : (dir == Up ? y1 - 1 : y2 + 1)); + result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, Game_Map::RoundX(fx), Game_Map::RoundY(fy), true); + } + result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true); - // Counter tile loop stops only if you talk to an action event. - bool got_action = CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); - // RPG_RT allows maximum of 3 counter tiles - for (int i = 0; !got_action && i < 3; ++i) { - if (!Game_Map::IsCounter(front_x, front_y)) { - break; - } + bool got_action = false; + for (int i = (dir == Up || dir == Down ? x1 : y1); i <= (dir == Up || dir == Down ? x2 : y2); ++i) { + int fx = GetX() + (dir == Up || dir == Down ? i : (dir == Left ? x1 - 1 : x2 + 1)); + int fy = GetY() + (dir == Left || dir == Right ? i : (dir == Up ? y1 - 1 : y2 + 1)); + + int cur_fx = Game_Map::RoundX(fx); + int cur_fy = Game_Map::RoundY(fy); - front_x = Game_Map::XwithDirection(front_x, GetDirection()); - front_y = Game_Map::YwithDirection(front_y, GetDirection()); + got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, cur_fx, cur_fy, true); - got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); + for (int c = 0; !got_action && c < 3; ++c) { + if (!Game_Map::IsCounter(cur_fx, cur_fy)) { + break; + } + + cur_fx = Game_Map::RoundX(Game_Map::XwithDirection(cur_fx, dir)); + cur_fy = Game_Map::RoundY(Game_Map::YwithDirection(cur_fy, dir)); + got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, cur_fx, cur_fy, true); + } } + return result || got_action; } @@ -451,18 +503,33 @@ bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_d return false; } + int x1, x2, y1, y2; + const Game_Character* active_char = IsAboard() ? static_cast(GetVehicle()) : static_cast(this); + active_char->GetTileOffsets(x1, x2, y1, y2); + bool result = false; - for (auto& ev: Game_Map::GetEvents()) { - const auto trigger = ev.GetTrigger(); - if (ev.IsActive() - && ev.GetX() == GetX() - && ev.GetY() == GetY() - && ev.GetLayer() != lcf::rpg::EventPage::Layers_same - && trigger.has_value() - && triggers[*trigger]) { - SetEncounterCalling(false); - result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); + for (auto& ev : Game_Map::GetEvents()) { + auto trigger = ev.GetTrigger(); + if (!trigger || !ev.IsActive() || ev.GetLayer() == lcf::rpg::EventPage::Layers_same || !triggers[*trigger]) { + continue; + } + + // Check if any tile in the player/vehicle's expanded footprint overlaps the event's footprint + bool overlap = false; + for (int dy = y1; dy <= y2 && !overlap; ++dy) { + for (int dx = x1; dx <= x2 && !overlap; ++dx) { + if (ev.IsInPosition(Game_Map::RoundX(GetX() + dx), Game_Map::RoundY(GetY() + dy))) { + overlap = true; + } + } + } + + if (overlap) { + if (ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player)) { + result = true; + SetEncounterCalling(false); + } } } return result; @@ -475,15 +542,16 @@ bool Game_Player::CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool bool result = false; for (auto& ev : Game_Map::GetEvents()) { - const auto trigger = ev.GetTrigger(); - if (ev.IsActive() - && ev.GetX() == x - && ev.GetY() == y + auto trigger = ev.GetTrigger(); + if (trigger && ev.IsActive() + && ev.IsInPosition(x, y) && ev.GetLayer() == lcf::rpg::EventPage::Layers_same - && trigger.has_value() - && triggers[*trigger]) { - SetEncounterCalling(false); - result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player); + && triggers[*trigger]) + { + if (ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player)) { + result = true; + SetEncounterCalling(false); + } } } return result; @@ -516,45 +584,126 @@ bool Game_Player::GetOnVehicle() { assert(!IsDirectionDiagonal(GetDirection())); assert(!IsAboard()); - auto* vehicle = Game_Map::GetVehicle(Game_Vehicle::Airship); + // 1. Handle Airship Boarding + auto* airship = Game_Map::GetVehicle(Game_Vehicle::Airship); + if (airship->IsInPosition(GetX(), GetY()) && IsStopping() && airship->IsStopping()) { + int x1, x2, y1, y2; + airship->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + // Branch: Only run iterative centering if Airship is expanded + if (is_expanded) { + int tx = airship->GetX(); + int ty = airship->GetY(); + + while (GetX() != tx || GetY() != ty) { + int dx = tx - GetX(); + int dy = ty - GetY(); + + if (Game_Map::LoopHorizontal()) { + int w = Game_Map::GetTilesX(); + if (std::abs(dx) > w / 2) dx = (dx > 0) ? dx - w : dx + w; + } + if (Game_Map::LoopVertical()) { + int h = Game_Map::GetTilesY(); + if (std::abs(dy) > h / 2) dy = (dy > 0) ? dy - h : dy + h; + } + + int move_dir = -1; + if (dx != 0) move_dir = (dx > 0) ? Right : Left; + else if (dy != 0) move_dir = (dy > 0) ? Down : Up; + + if (move_dir == -1) break; + + SetThrough(true); + if (std::abs(dx) + std::abs(dy) == 1) { + Move(move_dir); + } else { + SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(move_dir))); + SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(move_dir))); + Game_Map::SetPositionX(GetSpriteX() - GetPanX()); + Game_Map::SetPositionY(GetSpriteY() - GetPanY()); + } + ResetThrough(); + } + } - if (vehicle->IsInPosition(GetX(), GetY()) && IsStopping() && vehicle->IsStopping()) { data()->vehicle = Game_Vehicle::Airship; data()->aboard = true; - - // Note: RPG_RT ignores the lock_facing flag here! SetFacing(Left); - data()->preboard_move_speed = GetMoveSpeed(); - SetMoveSpeed(vehicle->GetMoveSpeed()); - vehicle->StartAscent(); - Main_Data::game_player->SetFlying(vehicle->IsFlying()); - } else { - const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + SetMoveSpeed(airship->GetMoveSpeed()); + airship->StartAscent(); + Main_Data::game_player->SetFlying(airship->IsFlying()); + Main_Data::game_system->SetBeforeVehicleMusic(Main_Data::game_system->GetCurrentBGM()); + Main_Data::game_system->BgmPlay(airship->GetBGM()); + return true; + } - vehicle = Game_Map::GetVehicle(Game_Vehicle::Ship); - if (!vehicle->IsInPosition(front_x, front_y)) { - vehicle = Game_Map::GetVehicle(Game_Vehicle::Boat); - if (!vehicle->IsInPosition(front_x, front_y)) { - return false; - } - } + // 2. Handle Boat/Ship Boarding + const auto front_x = Game_Map::RoundX(Game_Map::XwithDirection(GetX(), GetDirection())); + const auto front_y = Game_Map::RoundY(Game_Map::YwithDirection(GetY(), GetDirection())); - if (!Game_Map::CanEmbarkShip(*this, front_x, front_y)) { - return false; + Game_Vehicle* vehicle = nullptr; + for (auto vid : { Game_Vehicle::Ship, Game_Vehicle::Boat }) { + auto* v = Game_Map::GetVehicle(vid); + if (v->IsInCurrentMap() && v->IsInPosition(front_x, front_y)) { + vehicle = v; + break; } + } + if (!vehicle || !Game_Map::CanEmbarkShip(*this, front_x, front_y)) { + return false; + } + + int x1, x2, y1, y2; + vehicle->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + // Branch: Iterative pathing for large ships, standard one-step for small boats + if (is_expanded) { + int tx = vehicle->GetX(); + int ty = vehicle->GetY(); + + while (GetX() != tx || GetY() != ty) { + int dx = tx - GetX(); + int dy = ty - GetY(); + + if (Game_Map::LoopHorizontal()) { + int w = Game_Map::GetTilesX(); + if (std::abs(dx) > w / 2) dx = (dx > 0) ? dx - w : dx + w; + } + if (Game_Map::LoopVertical()) { + int h = Game_Map::GetTilesY(); + if (std::abs(dy) > h / 2) dy = (dy > 0) ? dy - h : dy + h; + } + + int move_dir = -1; + if (dx != 0) move_dir = (dx > 0) ? Right : Left; + else if (dy != 0) move_dir = (dy > 0) ? Down : Up; + + if (move_dir == -1) break; + + SetThrough(true); + if (std::abs(dx) + std::abs(dy) == 1) { + Move(move_dir); + } else { + SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(move_dir))); + SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(move_dir))); + } + ResetThrough(); + } + } else { + // Standard Logic SetThrough(true); Move(GetDirection()); - // FIXME: RPG_RT resets through to move_route_through || not visible? ResetThrough(); - - data()->vehicle = vehicle->GetVehicleType(); - data()->preboard_move_speed = GetMoveSpeed(); - data()->boarding = true; } + data()->vehicle = vehicle->GetVehicleType(); + data()->preboard_move_speed = GetMoveSpeed(); + data()->boarding = true; Main_Data::game_system->SetBeforeVehicleMusic(Main_Data::game_system->GetCurrentBGM()); Main_Data::game_system->BgmPlay(vehicle->GetBGM()); return true; @@ -565,52 +714,106 @@ bool Game_Player::GetOffVehicle() { assert(IsAboard()); auto* vehicle = GetVehicle(); - if (!vehicle) { - return false; - } + if (!vehicle) return false; if (InAirship()) { - if (vehicle->IsAscendingOrDescending()) { - return false; - } - - // Note: RPG_RT ignores the lock_facing flag here! + if (vehicle->IsAscendingOrDescending()) return false; SetFacing(Left); vehicle->StartDescent(); return true; } - const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + int x1, x2, y1, y2; + vehicle->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + int dir = GetDirection(); + + // Branch: Extended clearing logic for large hitboxes + if (is_expanded) { + int steps = 0; + if (dir == Up) steps = std::abs(y1) + 1; + else if (dir == Down) steps = std::abs(y2) + 1; + else if (dir == Left) steps = std::abs(x1) + 1; + else if (dir == Right) steps = std::abs(x2) + 1; + + int target_x = GetX(); + int target_y = GetY(); + if (dir == Up) target_y -= steps; + else if (dir == Down) target_y += steps; + else if (dir == Left) target_x -= steps; + else if (dir == Right) target_x += steps; + + target_x = Game_Map::RoundX(target_x); + target_y = Game_Map::RoundY(target_y); + + if (!Game_Map::CanDisembarkShip(*this, target_x, target_y)) return false; + + vehicle->SetDefaultDirection(); + data()->aboard = false; + SetMoveSpeed(data()->preboard_move_speed); + data()->unboarding = true; + + for (int i = 0; i < steps; ++i) { + SetThrough(true); + if (i < steps - 1) { + SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(dir))); + SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(dir))); + } else { + Move(dir); + } + ResetThrough(); + } + } else { + // Standard Logic + const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - if (!Game_Map::CanDisembarkShip(*this, front_x, front_y)) { - return false; - } + if (!Game_Map::CanDisembarkShip(*this, front_x, front_y)) { + return false; + } - vehicle->SetDefaultDirection(); - data()->aboard = false; - SetMoveSpeed(data()->preboard_move_speed); - data()->unboarding = true; + vehicle->SetDefaultDirection(); + data()->aboard = false; + SetMoveSpeed(data()->preboard_move_speed); + data()->unboarding = true; - SetThrough(true); - Move(GetDirection()); - ResetThrough(); + SetThrough(true); + Move(GetDirection()); + ResetThrough(); + } data()->vehicle = 0; Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic()); - return true; } void Game_Player::ForceGetOffVehicle() { - if (!IsAboard()) { - return; - } + if (!IsAboard()) return; auto* vehicle = GetVehicle(); vehicle->ForceLand(); vehicle->SetDefaultDirection(); + int x1, x2, y1, y2; + vehicle->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + // Branch: Only snap hero to lead edge if vehicle is expanded + if (is_expanded && !Game_Map::CanDisembarkShip(*this, GetX(), GetY())) { + int dir = GetDirection(); + int steps = 0; + if (dir == Up) steps = std::abs(y1); + else if (dir == Down) steps = std::abs(y2); + else if (dir == Left) steps = std::abs(x1); + else if (dir == Right) steps = std::abs(x2); + + for (int i = 0; i < steps; ++i) { + SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(dir))); + SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(dir))); + } + } + data()->flying = false; data()->aboard = false; SetMoveSpeed(data()->preboard_move_speed); From 6ca3cd8b19f6f435b1eccddd64d13329a17763a3 Mon Sep 17 00:00:00 2001 From: LizardKing777 <154367673+LizardKing777@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:40:47 -0600 Subject: [PATCH 2/2] Formatting fixes and Comment Restoration Rebuilt game_player.cpp, game_map.cpp and game_event.cpp to make sure comments remain and format of code is preserved. --- src/game_event.cpp | 4 +- src/game_map.cpp | 4848 +++++++++++++++++++++---------------------- src/game_player.cpp | 2340 ++++++++++----------- 3 files changed, 3608 insertions(+), 3584 deletions(-) diff --git a/src/game_event.cpp b/src/game_event.cpp index 835230bfde..1c270bf17c 100644 --- a/src/game_event.cpp +++ b/src/game_event.cpp @@ -425,7 +425,9 @@ void Game_Event::CheckCollisonOnMoveFailure() { int fx = Game_Map::RoundX(Game_Map::XwithDirection(GetX() + target_dx, dir)); int fy = Game_Map::RoundY(GetY() + dy); if (Main_Data::game_player->IsInPosition(fx, fy) && GetLayer() == lcf::rpg::EventPage::Layers_same && GetTrigger() == lcf::rpg::EventPage::Trigger_collision) { - ScheduleForegroundExecution(false, true); + ScheduleForegroundExecution(false, true); + // Events with trigger collision and layer same always reset their + // stop_count when they fail movement to a tile that the player inhabits. SetStopCount(0); return; } diff --git a/src/game_map.cpp b/src/game_map.cpp index cf8d5224a9..97b24296e0 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -1,2424 +1,2424 @@ -/* - * This file is part of EasyRPG Player. - * - * EasyRPG Player is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * EasyRPG Player is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with EasyRPG Player. If not, see . - */ - -// Headers -#include -#include -#include -#include -#include -#include -#include -#include - -#include "async_handler.h" -#include "options.h" -#include "system.h" -#include "game_battle.h" -#include "game_battler.h" -#include "game_map.h" -#include "game_interpreter_map.h" -#include "game_switches.h" -#include "game_player.h" -#include "game_party.h" -#include "game_message.h" -#include "game_screen.h" -#include "game_pictures.h" -#include "game_variables.h" -#include "scene_battle.h" -#include "scene_map.h" -#include -#include -#include "map_data.h" -#include "main_data.h" -#include "output.h" -#include "util_macro.h" -#include "game_system.h" -#include "filefinder.h" -#include "player.h" -#include "input.h" -#include "utils.h" -#include "rand.h" -#include -#include -#include "scene_gameover.h" -#include "feature.h" - -namespace { - // Intended bad value, Game_Map::Init sets them correctly - int screen_width = -1; - int screen_height = -1; - - lcf::rpg::SaveMapInfo map_info; - lcf::rpg::SavePanorama panorama; - - bool need_refresh; - - int animation_type; - bool animation_fast; - std::vector passages_down; - std::vector passages_up; - std::vector events; - std::vector common_events; - std::unique_ptr map_cache; - - std::unique_ptr map; - - std::unique_ptr interpreter; - std::vector vehicles; - - lcf::rpg::Chipset* chipset; - - //FIXME: Find a better way to do this. - bool panorama_on_map_init = true; - bool reset_panorama_x_on_next_init = true; - bool reset_panorama_y_on_next_init = true; - - bool translation_changed = false; - - // Used when the current map is not in the maptree - const lcf::rpg::MapInfo empty_map_info; -} - -namespace Game_Map { -void SetupCommon(); -} - -void Game_Map::OnContinueFromBattle() { - Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeBattleMusic()); -} - -static Game_Map::Parallax::Params GetParallaxParams(); - -void Game_Map::Init() { - Dispose(); - - map_info = {}; - panorama = {}; - SetNeedRefresh(true); - - interpreter.reset(new Game_Interpreter_Map(true)); - map_cache.reset(new Caching::MapCache()); - - InitCommonEvents(); - - vehicles.clear(); - vehicles.emplace_back(Game_Vehicle::Boat); - vehicles.emplace_back(Game_Vehicle::Ship); - vehicles.emplace_back(Game_Vehicle::Airship); -} - -void Game_Map::InitCommonEvents() { - common_events.clear(); - common_events.reserve(lcf::Data::commonevents.size()); - for (const lcf::rpg::CommonEvent& ev : lcf::Data::commonevents) { - common_events.emplace_back(ev.ID); - } - translation_changed = false; -} - -void Game_Map::Dispose() { - events.clear(); - map.reset(); - map_info = {}; - panorama = {}; -} - -void Game_Map::Quit() { - Dispose(); - common_events.clear(); - interpreter.reset(); - map_cache.reset(); -} - -int Game_Map::GetMapSaveCount() { - return (Player::IsRPG2k3() && map->save_count_2k3e > 0) - ? map->save_count_2k3e - : map->save_count; -} - -void Game_Map::Setup(std::unique_ptr map_in) { - Dispose(); - - map = std::move(map_in); - - SetupCommon(); - - panorama_on_map_init = true; - Parallax::ClearChangedBG(); - - SetEncounterSteps(GetMapInfo().encounter_steps); - SetChipset(map->chipset_id); - - std::iota(map_info.lower_tiles.begin(), map_info.lower_tiles.end(), 0); - std::iota(map_info.upper_tiles.begin(), map_info.upper_tiles.end(), 0); - - // Save allowed - const auto* current_info = &GetMapInfo(); - int current_index = current_info->ID; - int can_save = current_info->save; - int can_escape = current_info->escape; - int can_teleport = current_info->teleport; - - while (can_save == lcf::rpg::MapInfo::TriState_parent - || can_escape == lcf::rpg::MapInfo::TriState_parent - || can_teleport == lcf::rpg::MapInfo::TriState_parent) - { - const auto* parent_info = &GetParentMapInfo(*current_info); - int parent_index = parent_info->ID; - if (parent_index == 0) { - // If parent is 0 and flag is parent, it's implicitly enabled. - break; - } - if (parent_index == current_index) { - Output::Warning("Map {} has parent pointing to itself!", current_index); - break; - } - current_info = parent_info; - if (can_save == lcf::rpg::MapInfo::TriState_parent) { - can_save = current_info->save; - } - if (can_escape == lcf::rpg::MapInfo::TriState_parent) { - can_escape = current_info->escape; - } - if (can_teleport == lcf::rpg::MapInfo::TriState_parent) { - can_teleport = current_info->teleport; - } - } - Main_Data::game_system->SetAllowSave(can_save != lcf::rpg::MapInfo::TriState_forbid); - Main_Data::game_system->SetAllowEscape(can_escape != lcf::rpg::MapInfo::TriState_forbid); - Main_Data::game_system->SetAllowTeleport(can_teleport != lcf::rpg::MapInfo::TriState_forbid); - - auto& player = *Main_Data::game_player; - - SetPositionX(player.GetX() * SCREEN_TILE_SIZE - player.GetPanX()); - SetPositionY(player.GetY() * SCREEN_TILE_SIZE - player.GetPanY()); - - // Update the save counts so that if the player saves the game - // events will properly resume upon loading. - Main_Data::game_player->UpdateSaveCounts(lcf::Data::system.save_count, GetMapSaveCount()); -} - -void Game_Map::SetupFromSave( - std::unique_ptr map_in, - lcf::rpg::SaveMapInfo save_map, - lcf::rpg::SaveVehicleLocation save_boat, - lcf::rpg::SaveVehicleLocation save_ship, - lcf::rpg::SaveVehicleLocation save_airship, - lcf::rpg::SaveEventExecState save_fg_exec, - lcf::rpg::SavePanorama save_pan, - std::vector save_ce) { - - map = std::move(map_in); - map_info = std::move(save_map); - panorama = std::move(save_pan); - - SetupCommon(); - - const bool is_db_save_compat = Main_Data::game_player->IsDatabaseCompatibleWithSave(lcf::Data::system.save_count); - const bool is_map_save_compat = Main_Data::game_player->IsMapCompatibleWithSave(GetMapSaveCount()); - - InitCommonEvents(); - - if (is_db_save_compat && is_map_save_compat) { - for (size_t i = 0; i < std::min(save_ce.size(), common_events.size()); ++i) { - common_events[i].SetSaveData(save_ce[i].parallel_event_execstate); - } - } - - if (is_map_save_compat) { - std::vector destroyed_event_ids; - - for (size_t i = 0, j = 0; i < events.size() && j < map_info.events.size(); ++i) { - auto& ev = events[i]; - auto& save_ev = map_info.events[j]; - if (ev.GetId() == save_ev.ID) { - ev.SetSaveData(save_ev); - ++j; - } else { - if (save_ev.ID > ev.GetId()) { - // assume that the event has been destroyed during gameplay via "DestroyMapEvent" - destroyed_event_ids.emplace_back(ev.GetId()); - } else { - Output::Debug("SetupFromSave: Unexpected ID {}/{}", save_ev.ID, ev.GetId()); - } - } - } - for (size_t i = 0; i < destroyed_event_ids.size(); ++i) { - DestroyMapEvent(destroyed_event_ids[i], true); - } - if (destroyed_event_ids.size() > 0) { - UpdateUnderlyingEventReferences(); - } - } - - // Handle cloned events in a separate loop, regardless of "is_map_save_compat" - if (Player::HasEasyRpgExtensions()) { - for (size_t i = 0; i < map_info.events.size(); ++i) { - auto& save_ev = map_info.events[i]; - bool is_cloned_evt = save_ev.easyrpg_clone_map_id > 0 || save_ev.easyrpg_clone_event_id > 0; - if (is_cloned_evt && CloneMapEvent( - save_ev.easyrpg_clone_map_id, save_ev.easyrpg_clone_event_id, - save_ev.position_x, save_ev.position_y, - save_ev.ID, "")) { // FIXME: Customized event names for saved events aren't part of liblcf/SaveMapEvent at the moment & thus cannot be restored - if (auto new_event = GetEvent(save_ev.ID); new_event != nullptr) { - new_event->SetSaveData(save_ev); - } - } - } - UpdateUnderlyingEventReferences(); - } - map_info.events.clear(); - interpreter->Clear(); - - GetVehicle(Game_Vehicle::Boat)->SetSaveData(std::move(save_boat)); - GetVehicle(Game_Vehicle::Ship)->SetSaveData(std::move(save_ship)); - GetVehicle(Game_Vehicle::Airship)->SetSaveData(std::move(save_airship)); - - if (is_map_save_compat) { - // Make main interpreter "busy" if save contained events to prevent auto-events from starting - interpreter->SetState(std::move(save_fg_exec)); - } - - SetEncounterSteps(map_info.encounter_steps); - - // RPG_RT bug: Chipset is not loaded. Fixed in 2k3E - if (Player::IsRPG2k3E()) { - SetChipset(map_info.chipset_id); - } else { - SetChipset(0); - } - - if (!is_map_save_compat) { - panorama = {}; - } - - // We want to support loading rm2k3e panning chunks - // but also not break other saves which don't have them. - // To solve this problem, we reuse the scrolling methods - // which always reset the position anyways when scroll_horz/vert - // is false. - // This produces compatible behavior for old RPG_RT saves, namely - // the pan_x/y is always forced to 0. - // If the later async code will load panorama, set the flag to not clear the offsets. - // FIXME: RPG_RT compatibility bug: Everytime we load a savegame with default panorama chunks, - // this causes them to get overwritten - // FIXME: RPG_RT compatibility bug: On async platforms, panorama async loading can - // cause panorama chunks to be out of sync. - Game_Map::Parallax::ChangeBG(GetParallaxParams()); -} - -std::unique_ptr Game_Map::LoadMapFile(int map_id) { - std::unique_ptr map; - - // Attempt to load either the EasyRPG map file or the RPG Maker map file first, depending on config. - // If it fails, try the other one. - // FIXME: Assert map was cached for async platforms - bool map_is_easyrpg_file = Player::player_config.prefer_easyrpg_map_files.Get(); - std::string map_name = Game_Map::ConstructMapName(map_id, map_is_easyrpg_file); - std::string map_file = FileFinder::Game().FindFile(map_name); - if (map_file.empty()) { - map_is_easyrpg_file = !map_is_easyrpg_file; - map_name = Game_Map::ConstructMapName(map_id, map_is_easyrpg_file); - map_file = FileFinder::Game().FindFile(map_name); - - if (map_file.empty()) { - Output::Error("Loading of Map {} failed.\nThe map was not found.", map_name); - return nullptr; - } - } - - auto map_stream = FileFinder::Game().OpenInputStream(map_file); - if (!map_stream) { - Output::Error("Loading of Map {} failed.\nMap not readable.", map_name); - return nullptr; - } - - if (map_is_easyrpg_file) { - map = lcf::LMU_Reader::LoadXml(map_stream); - } else { - map = lcf::LMU_Reader::Load(map_stream, Player::encoding); - if (Input::IsRecording()) { - map_stream.clear(); - map_stream.seekg(0); - Input::AddRecordingData(Input::RecordingData::Hash, - fmt::format("map{:04} {:#08x}", map_id, Utils::CRC32(map_stream))); - } - } - - Output::Debug("Loaded Map {}", map_name); - - if (map.get() == NULL) { - Output::ErrorStr(lcf::LcfReader::GetError()); - } - - return map; -} - -void Game_Map::SetupCommon() { - screen_width = (Player::screen_width / 16.0) * SCREEN_TILE_SIZE; - screen_height = (Player::screen_height / 16.0) * SCREEN_TILE_SIZE; - - if (!Tr::GetCurrentTranslationId().empty()) { - TranslateMapMessages(GetMapId(), *map); - } - SetNeedRefresh(true); - - PrintPathToMap(); - - if (translation_changed) { - InitCommonEvents(); - } - - map_cache->Clear(); - - CreateMapEvents(); -} - -void Game_Map::CreateMapEvents() { - events.reserve(map->events.size()); - for (auto& ev : map->events) { - events.emplace_back(GetMapId(), &ev); - AddEventToCache(ev); - } -} - -void Game_Map::AddEventToCache(const lcf::rpg::Event& ev) { - using Op = Caching::ObservedVarOps; - - for (const auto& pg : ev.pages) { - if (pg.condition.flags.switch_a) { - map_cache->AddEventAsRefreshTarget(pg.condition.switch_a_id, ev); - } - if (pg.condition.flags.switch_b) { - map_cache->AddEventAsRefreshTarget(pg.condition.switch_b_id, ev); - } - if (pg.condition.flags.variable) { - map_cache->AddEventAsRefreshTarget(pg.condition.variable_id, ev); - } - } -} - -void Game_Map::RemoveEventFromCache(const lcf::rpg::Event& ev) { - using Op = Caching::ObservedVarOps; - - for (const auto& pg : ev.pages) { - if (pg.condition.flags.switch_a) { - map_cache->RemoveEventAsRefreshTarget(pg.condition.switch_a_id, ev); - } - if (pg.condition.flags.switch_b) { - map_cache->RemoveEventAsRefreshTarget(pg.condition.switch_b_id, ev); - } - if (pg.condition.flags.variable) { - map_cache->RemoveEventAsRefreshTarget(pg.condition.variable_id, ev); - } - } -} - -void Game_Map::Caching::MapCache::Clear() { - for (int i = 0; i < static_cast(ObservedVarOps_END); i++) { - refresh_targets_by_varid[i].clear(); - } -} - -bool Game_Map::CloneMapEvent(int src_map_id, int src_event_id, int target_x, int target_y, int target_event_id, std::string_view target_name) { - std::unique_ptr source_map_storage; - const lcf::rpg::Map* source_map; - - if (src_map_id == GetMapId()) { - source_map = &GetMap(); - } else { - source_map_storage = Game_Map::LoadMapFile(src_map_id); - source_map = source_map_storage.get(); - - if (source_map_storage == nullptr) { - Output::Warning("CloneMapEvent: Invalid source map ID {}", src_map_id); - return false; - } - - if (!Tr::GetCurrentTranslationId().empty()) { - TranslateMapMessages(src_map_id, *source_map_storage); - } - } - - const lcf::rpg::Event* source_event = FindEventById(source_map->events, src_event_id); - if (source_event == nullptr) { - Output::Warning("CloneMapEvent: Event ID {} not found on source map {}", src_event_id, src_map_id); - return false; - } - - lcf::rpg::Event new_event = *source_event; - if (target_event_id > 0) { - DestroyMapEvent(target_event_id, true); - new_event.ID = target_event_id; - } else { - new_event.ID = GetNextAvailableEventId(); - } - new_event.x = target_x; - new_event.y = target_y; - - if (!target_name.empty()) { - new_event.name = lcf::DBString(target_name); - } - - // sorted insert - auto insert_it = map->events.insert( - std::upper_bound(map->events.begin(), map->events.end(), new_event, [](const auto& e, const auto& e2) { - return e.ID < e2.ID; - }), new_event); - - auto game_event = Game_Event(GetMapId(), &*insert_it); - game_event.data()->easyrpg_clone_event_id = src_event_id; - game_event.data()->easyrpg_clone_map_id = src_map_id; - - events.insert( - std::upper_bound(events.begin(), events.end(), game_event, [](const auto& e, const auto& e2) { - return e.GetId() < e2.GetId(); - }), std::move(game_event)); - - UpdateUnderlyingEventReferences(); - - AddEventToCache(new_event); - - Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get(); - if (scene) { - scene->spriteset->Refresh(); - SetNeedRefresh(true); - } - - return true; -} - -bool Game_Map::DestroyMapEvent(const int event_id, bool from_clone) { - const lcf::rpg::Event* event = FindEventById(map->events, event_id); - - if (event == nullptr) { - if (!from_clone) { - Output::Warning("DestroyMapEvent: Event ID {} not found on current map", event_id); - } - return true; - } - - // Remove event from cache - RemoveEventFromCache(*event); - - // Remove event from events vector - for (auto it = events.begin(); it != events.end(); ++it) { - if (it->GetId() == event_id) { - events.erase(it); - break; - } - } - - // Remove event from map - for (auto it = map->events.begin(); it != map->events.end(); ++it) { - if (it->ID == event_id) { - map->events.erase(it); - break; - } - } - - if (!from_clone) { - UpdateUnderlyingEventReferences(); - - Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get(); - scene->spriteset->Refresh(); - SetNeedRefresh(true); - } - - if (GetInterpreter().GetOriginalEventId() == event_id) { - // Prevent triggering "invalid event on stack" sanity check - GetInterpreter().ClearOriginalEventId(); - } - - return true; -} - -void Game_Map::TranslateMapMessages(int mapId, lcf::rpg::Map& map) { - std::stringstream ss; - ss << "map" << std::setfill('0') << std::setw(4) << mapId << ".po"; - Player::translation.RewriteMapMessages(ss.str(), map); -} - - -void Game_Map::UpdateUnderlyingEventReferences() { - // Update references because modifying the vector can reallocate - size_t idx = 0; - for (auto& ev : events) { - ev.SetUnderlyingEvent(&map->events.at(idx++)); - } - - Main_Data::game_screen->UpdateUnderlyingEventReferences(); -} - -const lcf::rpg::Event* Game_Map::FindEventById(const std::vector& events, int eventId) { - for (const auto& ev : events) { - if (ev.ID == eventId) { - return &ev; - } - } - return nullptr; -} - -int Game_Map::GetNextAvailableEventId() { - if (map->events.empty()) { - return 1; - } else { - return map->events.back().ID + 1; - } -} - -void Game_Map::PrepareSave(lcf::rpg::Save& save) { - save.foreground_event_execstate = interpreter->GetSaveState(); - - save.airship_location = GetVehicle(Game_Vehicle::Airship)->GetSaveData(); - save.ship_location = GetVehicle(Game_Vehicle::Ship)->GetSaveData(); - save.boat_location = GetVehicle(Game_Vehicle::Boat)->GetSaveData(); - - save.map_info = map_info; - save.map_info.chipset_id = GetChipset(); - if (save.map_info.chipset_id == GetOriginalChipset()) { - // This emulates RPG_RT behavior, where chipset id == 0 means use the default map chipset. - save.map_info.chipset_id = 0; - } - if (save.map_info.encounter_steps == GetOriginalEncounterSteps()) { - save.map_info.encounter_steps = -1; - } - // Note: RPG_RT does not use a sentinel for parallax parameters. Once the parallax BG is changed, it stays that way forever. - - save.map_info.events.clear(); - save.map_info.events.reserve(events.size()); - for (Game_Event& ev : events) { - save.map_info.events.push_back(ev.GetSaveData()); - } - - save.panorama = panorama; - - save.common_events.clear(); - save.common_events.reserve(common_events.size()); - for (Game_CommonEvent& ev : common_events) { - save.common_events.push_back(lcf::rpg::SaveCommonEvent()); - save.common_events.back().ID = ev.GetIndex(); - save.common_events.back().parallel_event_execstate = ev.GetSaveData(); - } -} - -void Game_Map::PlayBgm() { - const auto* current_info = &GetMapInfo(); - while (current_info->music_type == 0 && GetParentMapInfo(*current_info).ID != current_info->ID) { - current_info = &GetParentMapInfo(*current_info); - } - - if ((current_info->ID > 0) && !current_info->music.name.empty()) { - if (current_info->music_type == 1) { - return; - } - auto& music = current_info->music; - if (!Main_Data::game_player->IsAboard()) { - Main_Data::game_system->BgmPlay(music); - } else { - Main_Data::game_system->SetBeforeVehicleMusic(music); - } - } -} - -std::vector Game_Map::GetTilesLayer(int layer) { - return layer >= 1 ? map_info.upper_tiles : map_info.lower_tiles; -} - -void Game_Map::Refresh() { - if (GetMapId() > 0) { - for (Game_Event& ev : events) { - ev.RefreshPage(); - } - } - - need_refresh = false; -} - -Game_Interpreter_Map& Game_Map::GetInterpreter() { - assert(interpreter); - return *interpreter; -} - -void Game_Map::Scroll(int dx, int dy) { - int x = map_info.position_x; - AddScreenX(x, dx); - map_info.position_x = x; - - int y = map_info.position_y; - AddScreenY(y, dy); - map_info.position_y = y; - - if (dx == 0 && dy == 0) { - return; - } - - Main_Data::game_screen->OnMapScrolled(dx, dy); - Main_Data::game_pictures->OnMapScrolled(dx, dy); - Game_Map::Parallax::ScrollRight(dx); - Game_Map::Parallax::ScrollDown(dy); -} - -// Add inc to acc, clamping the result into the range [low, high]. -// If the result is clamped, inc is also modified to be actual amount -// that acc changed by. -static void ClampingAdd(int low, int high, int& acc, int& inc) { - int original_acc = acc; - // Do not use std::clamp here. When the map is smaller than the screen the - // upper bound is smaller than the lower bound making the function fail. - acc = std::max(low, std::min(high, acc + inc)); - inc = acc - original_acc; -} - -void Game_Map::AddScreenX(int& screen_x, int& inc) { - int map_width = GetTilesX() * SCREEN_TILE_SIZE; - if (LoopHorizontal()) { - screen_x = (screen_x + inc) % map_width; - } else { - ClampingAdd(0, map_width - screen_width, screen_x, inc); - } -} - -void Game_Map::AddScreenY(int& screen_y, int& inc) { - int map_height = GetTilesY() * SCREEN_TILE_SIZE; - if (LoopVertical()) { - screen_y = (screen_y + inc) % map_height; - } else { - ClampingAdd(0, map_height - screen_height, screen_y, inc); - } -} - -bool Game_Map::IsValid(int x, int y) { - return (x >= 0 && x < GetTilesX() && y >= 0 && y < GetTilesY()); -} - -static int GetPassableMask(int old_x, int old_y, int new_x, int new_y) { - int bit = 0; - if (new_x > old_x) { bit |= Passable::Right; } - if (new_x < old_x) { bit |= Passable::Left; } - if (new_y > old_y) { bit |= Passable::Down; } - if (new_y < old_y) { bit |= Passable::Up; } - return bit; -} - -static bool WouldCollide(const Game_Character& self, const Game_Character& other, bool self_conflict) { - if (self.GetThrough() || other.GetThrough()) { - return false; - } - - if (self.IsFlying() || other.IsFlying()) { - return false; - } - - if (!self.IsActive() || !other.IsActive()) { - return false; - } - - if (self.GetType() == Game_Character::Event - && other.GetType() == Game_Character::Event - && (self.IsOverlapForbidden() || other.IsOverlapForbidden())) { - return true; - } - - if (other.GetLayer() == lcf::rpg::EventPage::Layers_same && self_conflict) { - return true; - } - - if (self.GetLayer() == other.GetLayer()) { - return true; - } - - return false; -} - -template -static void MakeWayUpdate(T& other) { - other.Update(); -} - -static void MakeWayUpdate(Game_Event& other) { - other.Update(false); -} - -template -static bool CheckWayTestCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) { - if (&self == &other) { - return false; - } - - if (!other.IsInPosition(x, y)) { - return false; - } - - return WouldCollide(self, other, self_conflict); -} - -template -static bool MakeWayCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) { - if (&self == &other) { - return false; - } - - if (!other.IsInPosition(x, y)) { - return false; - } - - // Force the other event to update, allowing them to possibly move out of the way. - MakeWayUpdate(other); - - if (!other.IsInPosition(x, y)) { - return false; - } - - return WouldCollide(self, other, self_conflict); -} - -static Game_Vehicle::Type GetCollisionVehicleType(const Game_Character* ch) { - if (ch && ch->GetType() == Game_Character::Vehicle) { - return static_cast(static_cast(ch)->GetVehicleType()); - } - return Game_Vehicle::None; -} - -bool Game_Map::CheckWay(const Game_Character& self, - int from_x, int from_y, - int to_x, int to_y - ) -{ - return CheckOrMakeWayEx( - self, from_x, from_y, to_x, to_y, true, {}, false - ); -} - -bool Game_Map::CheckWay(const Game_Character& self, - int from_x, int from_y, - int to_x, int to_y, - bool check_events_and_vehicles, - Span ignore_some_events_by_id) { - return CheckOrMakeWayEx( - self, from_x, from_y, to_x, to_y, - check_events_and_vehicles, - ignore_some_events_by_id, false - ); -} - -bool Game_Map::CheckOrMakeWayEx(const Game_Character& self, - int from_x, int from_y, - int to_x, int to_y, - bool check_events_and_vehicles, - Span ignore_some_events_by_id, - bool make_way - ) -{ - if (self.GetThrough()) { - return true; - } - - // Retrieve offsets to determine if this is an expanded-hitbox character - int min_dx, max_dx, min_dy, max_dy; - self.GetTileOffsets(min_dx, max_dx, min_dy, max_dy); - - // Detect if any expansion tags are active - bool is_expanded = (min_dx != 0 || max_dx != 0 || min_dy != 0 || max_dy != 0); - - // --- BRANCH 1: ORIGINAL LOGIC (Standard 1-tile events) --- - if (!is_expanded) { - // Infer directions before we do any rounding. - 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); - - // Now round for looping maps. - int t_to_x = Game_Map::RoundX(to_x); - int t_to_y = Game_Map::RoundY(to_y); - - // Note, even for diagonal, if the tile is invalid we still check vertical/horizontal first! - if (!Game_Map::IsValid(t_to_x, t_to_y)) { - return false; - } - - const auto vehicle_type = GetCollisionVehicleType(&self); - bool self_conflict = false; - - // Depending on whether we're supposed to call MakeWayCollideEvent - // (which might change the map) or not, choose what to call: - auto CheckOrMakeCollideEvent = [&](auto& other) { - if (make_way) { - return MakeWayCollideEvent(t_to_x, t_to_y, self, other, self_conflict); - } else { - return CheckWayTestCollideEvent( - t_to_x, t_to_y, self, other, self_conflict - ); - } - }; - - if (!self.IsJumping()) { - // Check for self conflict. - // If this event has a tile graphic and the tile itself has passage blocked in the direction - // we want to move, flag it as "self conflicting" for use later. - if (self.GetLayer() == lcf::rpg::EventPage::Layers_below && self.GetTileId() != 0) { - int tile_id = self.GetTileId(); - if ((passages_up[tile_id] & bit_from) == 0) { - self_conflict = true; - } - } - - if (vehicle_type == Game_Vehicle::None) { - // Check that we are allowed to step off of the current tile. - // Note: Vehicles can always step off a tile. - - // The current coordinate can be invalid due to an out-of-bounds teleport or a "Set Location" event. - // Round it for looping maps to ensure the check passes - // This is not fully bug compatible to RPG_RT. Assuming the Y-Coordinate is out-of-bounds: When moving - // left or right the invalid Y will stay in RPG_RT preventing events from being triggered, but we wrap it - // inbounds after the first move. - int t_from_x = Game_Map::RoundX(from_x); - int t_from_y = Game_Map::RoundY(from_y); - if (!IsPassableTile(&self, bit_from, t_from_x, t_from_y)) { - return false; - } - } - } - - if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) { - // Check for collision with events on the target tile. - if (ignore_some_events_by_id.empty()) { - for (auto& other : GetEvents()) { - if (CheckOrMakeCollideEvent(other)) { - return false; - } - } - } else { - for (auto& other : GetEvents()) { - if (std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end()) - continue; - if (CheckOrMakeCollideEvent(other)) { - return false; - } - } - } - - auto& player = Main_Data::game_player; - if (player->GetVehicleType() == Game_Vehicle::None) { - if (CheckOrMakeCollideEvent(*Main_Data::game_player)) { - return false; - } - } - for (auto vid : { Game_Vehicle::Boat, Game_Vehicle::Ship }) { - auto& other = vehicles[vid - 1]; - if (other.IsInCurrentMap()) { - if (CheckOrMakeCollideEvent(other)) { - return false; - } - } - } - auto& airship = vehicles[Game_Vehicle::Airship - 1]; - if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) { - if (CheckOrMakeCollideEvent(airship)) { - return false; - } - } - } - - int bit = bit_to; - if (self.IsJumping()) bit = Passable::Down; - - return IsPassableTile(&self, bit, t_to_x, t_to_y, check_events_and_vehicles, true); - } - - // --- BRANCH 2: EXPANDED LOGIC (Large events with tags) --- - else { - const auto vehicle_type = GetCollisionVehicleType(&self); - - // Iterate through every tile in the footprint (Width x Height) - for (int dy = min_dy; dy <= max_dy; ++dy) { - for (int dx = min_dx; dx <= max_dx; ++dx) { - // Calculate coordinates for this specific segment of the footprint - int cur_from_x = from_x + dx; - int cur_from_y = from_y + dy; - int cur_to_x = to_x + dx; - int cur_to_y = to_y + dy; - - // Infer directions for this specific segment - const int bit_from = GetPassableMask(cur_from_x, cur_from_y, cur_to_x, cur_to_y); - const int bit_to = GetPassableMask(cur_to_x, cur_to_y, cur_from_x, cur_from_y); - - // Round coordinates for looping maps - int tile_from_x = Game_Map::RoundX(cur_from_x); - int tile_from_y = Game_Map::RoundY(cur_from_y); - int tile_to_x = Game_Map::RoundX(cur_to_x); - int tile_to_y = Game_Map::RoundY(cur_to_y); - - // Fail if any segment of the expanded box moves out of map bounds - if (!Game_Map::IsValid(tile_to_x, tile_to_y)) { - return false; - } - - bool self_conflict = false; - - // Check for self conflict for this segment - if (!self.IsJumping() && self.GetLayer() == lcf::rpg::EventPage::Layers_same && self.GetTileId() != 0) { - int tile_id = self.GetTileId(); - if ((passages_up[tile_id] & bit_from) == 0) { - self_conflict = true; - } - } - - // Helper for checking collisions with other entities for this segment - auto CheckOrMakeCollideEvent = [&](auto& other) { - if (make_way) { - return MakeWayCollideEvent(tile_to_x, tile_to_y, self, other, self_conflict); - } else { - return CheckWayTestCollideEvent(tile_to_x, tile_to_y, self, other, self_conflict); - } - }; - - // 1. Check Map Passability (Environment/Terrain/Vehicles) - if (!self.IsJumping()) { - if (vehicle_type == Game_Vehicle::None) { - // Segment must be able to step OFF its current tile - if (!IsPassableTile(&self, bit_from, tile_from_x, tile_from_y, check_events_and_vehicles, true)) { - return false; - } - } - } - - // Segment must be able to step ONTO its target tile - int bit_onto = self.IsJumping() ? Passable::Down : bit_to; - if (!IsPassableTile(&self, bit_onto, tile_to_x, tile_to_y, check_events_and_vehicles, true)) { - return false; - } - - // 2. Check Entity Collisions - if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) { - // Check collisions with all Map Events - for (auto& other : GetEvents()) { - if (!ignore_some_events_by_id.empty() && std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end()) - continue; - if (CheckOrMakeCollideEvent(other)) { - return false; - } - } - - // Check collision with Player - auto& player = Main_Data::game_player; - if (player->GetVehicleType() == Game_Vehicle::None) { - if (CheckOrMakeCollideEvent(*Main_Data::game_player)) { - return false; - } - } - - // Check collision with Boats/Ships - for (auto vid : { Game_Vehicle::Boat, Game_Vehicle::Ship }) { - auto& other = vehicles[vid - 1]; - if (other.IsInCurrentMap()) { - if (CheckOrMakeCollideEvent(other)) { - return false; - } - } - } - - // Check collision with Airship - auto& airship = vehicles[Game_Vehicle::Airship - 1]; - if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) { - if (CheckOrMakeCollideEvent(airship)) { - return false; - } - } - } - } - } - return true; - } -} - -bool Game_Map::MakeWay(const Game_Character& self, - int from_x, int from_y, - int to_x, int to_y - ) -{ - return CheckOrMakeWayEx( - self, from_x, from_y, to_x, to_y, true, {}, true - ); -} - - -bool Game_Map::CanLandAirship(int x, int y) { - if (!Game_Map::IsValid(x, y)) return false; - - const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y)); - if (!terrain) { - Output::Warning("CanLandAirship: Invalid terrain at ({}, {})", x, y); - return false; - } - if (!terrain->airship_land) { - return false; - } - - for (auto& ev: events) { - if (ev.IsInPosition(x, y) - && ev.IsActive() - && ev.GetActivePage() != nullptr) { - return false; - } - } - for (auto vid: { Game_Vehicle::Boat, Game_Vehicle::Ship }) { - auto& vehicle = vehicles[vid - 1]; - if (vehicle.IsInCurrentMap() && vehicle.IsInPosition(x, y)) { - return false; - } - } - - const int bit = Passable::Down | Passable::Right | Passable::Left | Passable::Up; - - int tile_index = x + y * GetTilesX(); - - if (!IsPassableLowerTile(bit, tile_index)) { - return false; - } - - int tile_id = map->upper_layer[tile_index] - BLOCK_F; - tile_id = map_info.upper_tiles[tile_id]; - - return (passages_up[tile_id] & bit) != 0; -} - -bool Game_Map::CanEmbarkShip(Game_Player& player, int x, int y) { - auto bit = GetPassableMask(player.GetX(), player.GetY(), x, y); - return IsPassableTile(&player, bit, player.GetX(), player.GetY()); -} - -bool Game_Map::CanDisembarkShip(Game_Player& player, int x, int y) { - if (!Game_Map::IsValid(x, y)) { - return false; - } - - for (auto& ev: GetEvents()) { - if (ev.IsInPosition(x, y) - && ev.GetLayer() == lcf::rpg::EventPage::Layers_same - && ev.IsActive() - && ev.GetActivePage() != nullptr) { - return false; - } - } - - int bit = GetPassableMask(x, y, player.GetX(), player.GetY()); - - return IsPassableTile(nullptr, bit, x, y); -} - -bool Game_Map::IsPassableLowerTile(int bit, int tile_index) { - int tile_raw_id = map->lower_layer[tile_index]; - int tile_id = 0; - - if (tile_raw_id >= BLOCK_E) { - tile_id = tile_raw_id - BLOCK_E; - tile_id = map_info.lower_tiles[tile_id] + BLOCK_E_INDEX; - - } else if (tile_raw_id >= BLOCK_D) { - tile_id = (tile_raw_id - BLOCK_D) / BLOCK_D_STRIDE + BLOCK_D_INDEX; - int autotile_id = (tile_raw_id - BLOCK_D) % BLOCK_D_STRIDE; - - if (((passages_down[tile_id] & Passable::Wall) != 0) && ( - (autotile_id >= 20 && autotile_id <= 23) || - (autotile_id >= 33 && autotile_id <= 37) || - autotile_id == 42 || autotile_id == 43 || - autotile_id == 45 || autotile_id == 46)) - return true; - - } else if (tile_raw_id >= BLOCK_C) { - tile_id = (tile_raw_id - BLOCK_C) / BLOCK_C_STRIDE + BLOCK_C_INDEX; - - } else if (map->lower_layer[tile_index] < BLOCK_C) { - tile_id = tile_raw_id / BLOCK_B_STRIDE; - } - - return (passages_down[tile_id] & bit) != 0; -} - -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 (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) { - if (ev.HasTileSprite()) { - event_tile_id = ev.GetTileId(); - } - } - } - - // 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 (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) - return false; - return true; - } - - if ((passages_up[tile_id] & bit) == 0) - return false; - - if ((passages_up[tile_id] & Passable::Above) == 0) - return true; - - return IsPassableLowerTile(bit, tile_index); - } else { - return true; - } -} - -int Game_Map::GetBushDepth(int x, int y) { - if (!Game_Map::IsValid(x, y)) return 0; - - const lcf::rpg::Terrain* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x,y)); - if (!terrain) { - Output::Warning("GetBushDepth: Invalid terrain at ({}, {})", x, y); - return 0; - } - return terrain->bush_depth; -} - -bool Game_Map::IsCounter(int x, int y) { - if (!Game_Map::IsValid(x, y)) return false; - - int const tile_id = map->upper_layer[x + y * GetTilesX()]; - if (tile_id < BLOCK_F) return false; - int const index = map_info.upper_tiles[tile_id - BLOCK_F]; - return !!(passages_up[index] & Passable::Counter); -} - -int Game_Map::GetTerrainTag(int x, int y) { - if (!chipset) { - // FIXME: Is this ever possible? - return 1; - } - - auto& terrain_data = chipset->terrain_data; - - if (terrain_data.empty()) { - // RPG_RT optimisation: When the terrain is all 1, no terrain data is stored - return 1; - } - - // Terrain tag wraps on looping maps - if (Game_Map::LoopHorizontal()) { - x = RoundX(x); - } - if (Game_Map::LoopVertical()) { - y = RoundY(y); - } - - // RPG_RT always uses the terrain of the first lower tile - // for out of bounds coordinates. - unsigned chip_index = 0; - - if (Game_Map::IsValid(x, y)) { - const auto chip_id = map->lower_layer[x + y * GetTilesX()]; - chip_index = ChipIdToIndex(chip_id); - - // Apply tile substitution - if (chip_index >= BLOCK_E_INDEX && chip_index < NUM_LOWER_TILES) { - chip_index = map_info.lower_tiles[chip_index - BLOCK_E_INDEX] + BLOCK_E_INDEX; - } - } - - assert(chip_index < terrain_data.size()); - - return terrain_data[chip_index]; -} - -Game_Event* Game_Map::GetEventAt(int x, int y, bool require_active) { - auto& events = GetEvents(); - for (auto iter = events.rbegin(); iter != events.rend(); ++iter) { - auto& ev = *iter; - if (ev.IsInPosition(x, y) && (!require_active || ev.IsActive())) { - return &ev; - } - } - return nullptr; -} - -bool Game_Map::LoopHorizontal() { - return map->scroll_type == lcf::rpg::Map::ScrollType_horizontal || map->scroll_type == lcf::rpg::Map::ScrollType_both; -} - -bool Game_Map::LoopVertical() { - return map->scroll_type == lcf::rpg::Map::ScrollType_vertical || map->scroll_type == lcf::rpg::Map::ScrollType_both; -} - -int Game_Map::RoundX(int x, int units) { - if (LoopHorizontal()) { - return Utils::PositiveModulo(x, GetTilesX() * units); - } else { - return x; - } -} - -int Game_Map::RoundY(int y, int units) { - if (LoopVertical()) { - return Utils::PositiveModulo(y, GetTilesY() * units); - } else { - return y; - } -} - -int Game_Map::RoundDx(int dx, int units) { - if (LoopHorizontal()) { - return Utils::PositiveModulo(std::abs(dx), GetTilesX() * units) * Utils::Sign(dx); - } else { - return dx; - } -} - -int Game_Map::RoundDy(int dy, int units) { - if (LoopVertical()) { - return Utils::PositiveModulo(std::abs(dy), GetTilesY() * units) * Utils::Sign(dy); - } else { - return dy; - } -} - -int Game_Map::XwithDirection(int x, int direction) { - return RoundX(x + (direction == lcf::rpg::EventPage::Direction_right ? 1 : direction == lcf::rpg::EventPage::Direction_left ? -1 : 0)); -} - -int Game_Map::YwithDirection(int y, int direction) { - return RoundY(y + (direction == lcf::rpg::EventPage::Direction_down ? 1 : direction == lcf::rpg::EventPage::Direction_up ? -1 : 0)); -} - -int Game_Map::CheckEvent(int x, int y) { - for (const Game_Event& ev : events) { - if (ev.IsInPosition(x, y)) { - return ev.GetId(); - } - } - - return 0; -} - -void Game_Map::Update(MapUpdateAsyncContext& actx, bool is_preupdate) { - if (GetNeedRefresh()) { - Refresh(); - } - - if (!actx.IsActive()) { - //If not resuming from async op ... - UpdateProcessedFlags(is_preupdate); - } - - if (!actx.IsActive() || actx.IsParallelCommonEvent()) { - if (!UpdateCommonEvents(actx)) { - // Suspend due to common event async op ... - return; - } - } - - if (!actx.IsActive() || actx.IsParallelMapEvent()) { - if (!UpdateMapEvents(actx)) { - // Suspend due to map event async op ... - return; - } - } - - if (is_preupdate) { - return; - } - - if (!actx.IsActive()) { - //If not resuming from async op ... - Main_Data::game_player->Update(); - - for (auto& vehicle: vehicles) { - if (vehicle.GetMapId() == GetMapId()) { - vehicle.Update(); - } - } - } - - if (!actx.IsActive() || actx.IsMessage()) { - if (!UpdateMessage(actx)) { - // Suspend due to message async op ... - return; - } - } - - if (!actx.IsActive()) { - Main_Data::game_party->UpdateTimers(); - Main_Data::game_screen->Update(); - Main_Data::game_pictures->Update(false); - } - - if (!actx.IsActive() || actx.IsForegroundEvent()) { - if (!UpdateForegroundEvents(actx)) { - // Suspend due to foreground event async op ... - return; - } - } - - Parallax::Update(); - - actx = {}; -} - -void Game_Map::UpdateProcessedFlags(bool is_preupdate) { - for (Game_Event& ev : events) { - ev.SetProcessed(false); - } - if (!is_preupdate) { - Main_Data::game_player->SetProcessed(false); - for (auto& vehicle: vehicles) { - if (vehicle.IsInCurrentMap()) { - vehicle.SetProcessed(false); - } - } - } -} - - -bool Game_Map::UpdateCommonEvents(MapUpdateAsyncContext& actx) { - int resume_ce = actx.GetParallelCommonEvent(); - - for (Game_CommonEvent& ev : common_events) { - bool resume_async = false; - if (resume_ce != 0) { - // If resuming, skip all until the event to resume from .. - if (ev.GetIndex() != resume_ce) { - continue; - } else { - resume_ce = 0; - resume_async = true; - } - } - - auto aop = ev.Update(resume_async); - if (aop.IsActive()) { - // Suspend due to this event .. - actx = MapUpdateAsyncContext::FromCommonEvent(ev.GetIndex(), aop); - return false; - } - } - - actx = {}; - return true; -} - -bool Game_Map::UpdateMapEvents(MapUpdateAsyncContext& actx) { - int resume_ev = actx.GetParallelMapEvent(); - - for (Game_Event& ev : events) { - bool resume_async = false; - if (resume_ev != 0) { - // If resuming, skip all until the event to resume from .. - if (ev.GetId() != resume_ev) { - continue; - } else { - resume_ev = 0; - resume_async = true; - } - } - - auto aop = ev.Update(resume_async); - if (aop.IsActive()) { - // Suspend due to this event .. - actx = MapUpdateAsyncContext::FromMapEvent(ev.GetId(), aop); - return false; - } - } - - actx = {}; - return true; -} - -bool Game_Map::UpdateMessage(MapUpdateAsyncContext& actx) { - // Message system does not support suspend and resume internally. So if the last frame the message - // produced an async event, the message loop finished completely. Therefore this frame we should - // resume *after* the message and not run it again. - if (!actx.IsActive()) { - auto aop = Game_Message::Update(); - if (aop.IsActive()) { - actx = MapUpdateAsyncContext::FromMessage(aop); - return false; - } - } - - actx = {}; - return true; -} - -bool Game_Map::UpdateForegroundEvents(MapUpdateAsyncContext& actx) { - auto& interp = GetInterpreter(); - - // If we resume from async op, we don't clear the loop index. - const bool resume_fg = actx.IsForegroundEvent(); - - // Run any event loaded from last frame. - interp.Update(!resume_fg); - if (interp.IsAsyncPending()) { - // Suspend due to this event .. - actx = MapUpdateAsyncContext::FromForegroundEvent(interp.GetAsyncOp()); - return false; - } - - while (!interp.IsRunning() && !interp.ReachedLoopLimit()) { - interp.Clear(); - - // This logic is probably one big loop in RPG_RT. We have to replicate - // it here because once we stop executing from this we should not - // clear anymore waiting flags. - if (Scene::instance->HasRequestedScene() && interp.GetLoopCount() > 0) { - break; - } - Game_CommonEvent* run_ce = nullptr; - - for (auto& ce: common_events) { - if (ce.IsWaitingForegroundExecution()) { - run_ce = &ce; - break; - } - } - if (run_ce) { - interp.Push(run_ce); - } - - Game_Event* run_ev = nullptr; - for (auto& ev: events) { - if (ev.IsWaitingForegroundExecution()) { - if (!ev.IsActive()) { - ev.ClearWaitingForegroundExecution(); - continue; - } - run_ev = &ev; - break; - } - } - if (run_ev) { - if (run_ev->WasStartedByDecisionKey()) { - interp.Push(run_ev); - } else if (auto t = run_ev->GetTrigger()) { - switch (*t) { - case lcf::rpg::EventPage::Trigger_touched: - interp.Push(run_ev); - break; - case lcf::rpg::EventPage::Trigger_collision: - interp.Push(run_ev); - break; - case lcf::rpg::EventPage::Trigger_auto_start: - interp.Push(run_ev); - break; - case lcf::rpg::EventPage::Trigger_action: - case lcf::rpg::EventPage::Trigger_parallel: - interp.Push(run_ev); - break; - } - } else { - interp.Push(run_ev); - } - run_ev->ClearWaitingForegroundExecution(); - } - - // If no events to run we're finished. - if (!interp.IsRunning()) { - break; - } - - interp.Update(false); - if (interp.IsAsyncPending()) { - // Suspend due to this event .. - actx = MapUpdateAsyncContext::FromForegroundEvent(interp.GetAsyncOp()); - return false; - } - } - - actx = {}; - return true; -} - -lcf::rpg::MapInfo const& Game_Map::GetMapInfo() { - return GetMapInfo(GetMapId()); -} - -lcf::rpg::MapInfo const& Game_Map::GetMapInfo(int map_id) { - for (const auto& mi: lcf::Data::treemap.maps) { - if (mi.ID == map_id) { - return mi; - } - } - - Output::Debug("Map {} not in Maptree", map_id); - return empty_map_info; -} - -const lcf::rpg::MapInfo& Game_Map::GetParentMapInfo() { - return GetParentMapInfo(GetMapInfo()); -} - -const lcf::rpg::MapInfo& Game_Map::GetParentMapInfo(const lcf::rpg::MapInfo& map_info) { - return GetMapInfo(map_info.parent_map); -} - -lcf::rpg::Map const& Game_Map::GetMap() { - return *map; -} - -int Game_Map::GetMapId() { - return Main_Data::game_player->GetMapId(); -} - -void Game_Map::PrintPathToMap() { - const auto* current_info = &GetMapInfo(); - std::ostringstream ss; - ss << current_info->name; - - current_info = &GetParentMapInfo(*current_info); - while (current_info->ID != 0 && current_info->ID != GetMapId()) { - ss << " < " << current_info->name; - current_info = &GetParentMapInfo(*current_info); - } - - Output::Debug("Tree: {}", ss.str()); -} - -int Game_Map::GetTilesX() { - return map->width; -} - -int Game_Map::GetTilesY() { - return map->height; -} - -int Game_Map::GetOriginalEncounterSteps() { - return GetMapInfo().encounter_steps; -} - -int Game_Map::GetEncounterSteps() { - return map_info.encounter_steps; -} - -void Game_Map::SetEncounterSteps(int step) { - if (step < 0) { - step = GetOriginalEncounterSteps(); - } - map_info.encounter_steps = step; -} - -std::vector Game_Map::GetEncountersAt(int x, int y) { - int terrain_tag = GetTerrainTag(Main_Data::game_player->GetX(), Main_Data::game_player->GetY()); - - std::function is_acceptable = [=](int troop_id) { - const lcf::rpg::Troop* troop = lcf::ReaderUtil::GetElement(lcf::Data::troops, troop_id); - if (!troop) { - Output::Warning("GetEncountersAt: Invalid troop ID {} in encounter list", troop_id); - return false; - } - - const auto& terrain_set = troop->terrain_set; - - // RPG_RT optimisation: Omitted entries are the default value (true) - return terrain_set.size() <= (unsigned)(terrain_tag - 1) || - terrain_set[terrain_tag - 1]; - }; - - std::vector out; - - for (unsigned int i = 0; i < lcf::Data::treemap.maps.size(); ++i) { - lcf::rpg::MapInfo& map = lcf::Data::treemap.maps[i]; - - if (map.ID == GetMapId()) { - for (const auto& enc : map.encounters) { - if (is_acceptable(enc.troop_id)) { - out.push_back(enc.troop_id); - } - } - } else if (map.parent_map == GetMapId() && map.type == lcf::rpg::TreeMap::MapType_area) { - // Area - Rect area_rect(map.area_rect.l, map.area_rect.t, map.area_rect.r - map.area_rect.l, map.area_rect.b - map.area_rect.t); - Rect player_rect(x, y, 1, 1); - - if (!player_rect.IsOutOfBounds(area_rect)) { - for (const lcf::rpg::Encounter& enc : map.encounters) { - if (is_acceptable(enc.troop_id)) { - out.push_back(enc.troop_id); - } - } - } - } - } - - return out; -} - -static void OnEncounterEnd(BattleResult result) { - if (result != BattleResult::Defeat) { - return; - } - - if (!Game_Battle::HasDeathHandler()) { - Scene::Push(std::make_shared()); - return; - } - - //2k3 death handler - - auto* ce = lcf::ReaderUtil::GetElement(common_events, Game_Battle::GetDeathHandlerCommonEvent()); - if (ce) { - auto& interp = Game_Map::GetInterpreter(); - interp.Push(ce); - } - - auto tt = Game_Battle::GetDeathHandlerTeleport(); - if (tt.IsActive()) { - Main_Data::game_player->ReserveTeleport(tt.GetMapId(), tt.GetX(), tt.GetY(), tt.GetDirection(), tt.GetType()); - } -} - -bool Game_Map::PrepareEncounter(BattleArgs& args) { - int x = Main_Data::game_player->GetX(); - int y = Main_Data::game_player->GetY(); - - std::vector encounters = GetEncountersAt(x, y); - - if (encounters.empty()) { - // No enemies on this map :( - return false; - } - - args.troop_id = encounters[Rand::GetRandomNumber(0, encounters.size() - 1)]; - - if (RuntimePatches::EncounterRandomnessAlert::HandleEncounter(args.troop_id)) { - //Cancel the battle setup - return false; - } - - if (Feature::HasRpg2kBattleSystem()) { - if (Rand::ChanceOf(1, 32)) { - args.first_strike = true; - } - } else { - const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y)); - if (!terrain) { - Output::Warning("PrepareEncounter: Invalid terrain at ({}, {})", x, y); - } else { - if (terrain->special_flags.back_party && Rand::PercentChance(terrain->special_back_party)) { - args.condition = lcf::rpg::System::BattleCondition_initiative; - } else if (terrain->special_flags.back_enemies && Rand::PercentChance(terrain->special_back_enemies)) { - args.condition = lcf::rpg::System::BattleCondition_back; - } else if (terrain->special_flags.lateral_party && Rand::PercentChance(terrain->special_lateral_party)) { - args.condition = lcf::rpg::System::BattleCondition_surround; - } else if (terrain->special_flags.lateral_enemies && Rand::PercentChance(terrain->special_lateral_enemies)) { - args.condition = lcf::rpg::System::BattleCondition_pincers; - } - } - } - - SetupBattle(args); - args.on_battle_end = OnEncounterEnd; - args.allow_escape = true; - - return true; -} - -void Game_Map::SetupBattle(BattleArgs& args) { - int x = Main_Data::game_player->GetX(); - int y = Main_Data::game_player->GetY(); - - args.terrain_id = GetTerrainTag(x, y); - - const auto* current_info = &GetMapInfo(); - while (current_info->background_type == 0 && GetParentMapInfo(*current_info).ID != current_info->ID) { - current_info = &GetParentMapInfo(*current_info); - } - - if (current_info->background_type == 2) { - args.background = ToString(current_info->background_name); - } -} - -std::vector& Game_Map::GetMapDataDown() { - return map->lower_layer; -} - -std::vector& Game_Map::GetMapDataUp() { - return map->upper_layer; -} - -int Game_Map::GetOriginalChipset() { - return map != nullptr ? map->chipset_id : 0; -} - -int Game_Map::GetChipset() { - return chipset != nullptr ? chipset->ID : 0; -} - -std::string_view Game_Map::GetChipsetName() { - return chipset != nullptr - ? std::string_view(chipset->chipset_name) - : std::string_view(""); -} - -int Game_Map::GetPositionX() { - return map_info.position_x; -} - -int Game_Map::GetDisplayX() { - return map_info.position_x + Main_Data::game_screen->GetShakeOffsetX() * 16; -} - -void Game_Map::SetPositionX(int x, bool reset_panorama) { - const int map_width = GetTilesX() * SCREEN_TILE_SIZE; - if (LoopHorizontal()) { - x = Utils::PositiveModulo(x, map_width); - - // If the map is too small to fit in the screen, add an offset corresponding to the black border's size - if (Player::game_config.fake_resolution.Get()) { - int map_width_in_pixels = Game_Map::GetTilesX() * TILE_SIZE; - if (map_width_in_pixels < Player::screen_width) { - x += ((Player::screen_width - map_width_in_pixels) / 2 / TILE_SIZE) * SCREEN_TILE_SIZE; - } - } - } else { - // Do not use std::clamp here. When the map is smaller than the screen the - // upper bound is smaller than the lower bound making the function fail. - x = std::max(0, std::min(map_width - screen_width, x)); - } - map_info.position_x = x; - if (reset_panorama) { - Parallax::SetPositionX(map_info.position_x); - Parallax::ResetPositionX(); - } -} - -int Game_Map::GetPositionY() { - return map_info.position_y; -} - -int Game_Map::GetDisplayY() { - return map_info.position_y + Main_Data::game_screen->GetShakeOffsetY() * 16; -} - -void Game_Map::SetPositionY(int y, bool reset_panorama) { - const int map_height = GetTilesY() * SCREEN_TILE_SIZE; - if (LoopVertical()) { - y = Utils::PositiveModulo(y, map_height); - - // If the map is too small to fit in the screen, add an offset corresponding to the black border's size - if (Player::game_config.fake_resolution.Get()) { - int map_height_in_pixels = Game_Map::GetTilesY() * TILE_SIZE; - if (map_height_in_pixels < Player::screen_height) { - y += ((Player::screen_height - map_height_in_pixels) / 2 / TILE_SIZE) * SCREEN_TILE_SIZE; - } - } - } else { - // Do not use std::clamp here. When the map is smaller than the screen the - // upper bound is smaller than the lower bound making the function fail. - y = std::max(0, std::min(map_height - screen_height, y)); - } - map_info.position_y = y; - if (reset_panorama) { - Parallax::SetPositionY(map_info.position_y); - Parallax::ResetPositionY(); - } -} - -bool Game_Map::GetNeedRefresh() { - int anti_lag_switch = Player::game_config.patch_anti_lag_switch.Get(); - if (anti_lag_switch > 0 && Main_Data::game_switches->Get(anti_lag_switch)) { - return false; - } - - return need_refresh; -} - -void Game_Map::SetNeedRefresh(bool refresh) { - need_refresh = refresh; -} - -void Game_Map::SetNeedRefreshForSwitchChange(int switch_id) { - if (need_refresh) - return; - if (map_cache->GetNeedRefresh(switch_id)) - SetNeedRefresh(true); -} - -void Game_Map::SetNeedRefreshForVarChange(int var_id) { - if (need_refresh) - return; - if (map_cache->GetNeedRefresh(var_id)) - SetNeedRefresh(true); -} - -void Game_Map::SetNeedRefreshForSwitchChange(std::initializer_list switch_ids) { - for (auto switch_id: switch_ids) { - SetNeedRefreshForSwitchChange(switch_id); - } -} - -void Game_Map::SetNeedRefreshForVarChange(std::initializer_list var_ids) { - for (auto var_id: var_ids) { - SetNeedRefreshForVarChange(var_id); - } -} - -std::vector& Game_Map::GetPassagesDown() { - return passages_down; -} - -std::vector& Game_Map::GetPassagesUp() { - return passages_up; -} - -int Game_Map::GetAnimationType() { - return animation_type; -} - -int Game_Map::GetAnimationSpeed() { - return (animation_fast ? 12 : 24); -} - -std::vector& Game_Map::GetEvents() { - return events; -} - -int Game_Map::GetHighestEventId() { - int id = 0; - for (auto& ev: events) { - id = std::max(id, ev.GetId()); - } - return id; -} - -Game_Event* Game_Map::GetEvent(int event_id) { - auto it = std::find_if(events.begin(), events.end(), - [&event_id](Game_Event& ev) {return ev.GetId() == event_id;}); - return it == events.end() ? nullptr : &(*it); -} - -std::vector& Game_Map::GetCommonEvents() { - return common_events; -} - -std::string_view Game_Map::GetMapName(int id) { - for (unsigned int i = 0; i < lcf::Data::treemap.maps.size(); ++i) { - if (lcf::Data::treemap.maps[i].ID == id) { - return lcf::Data::treemap.maps[i].name; - } - } - // nothing found - return {}; -} - -void Game_Map::SetChipset(int id) { - if (id == 0) { - // This emulates RPG_RT behavior, where chipset id == 0 means use the default map chipset. - id = GetOriginalChipset(); - } - map_info.chipset_id = id; - - if (!ReloadChipset()) { - Output::Warning("SetChipset: Invalid chipset ID {}", map_info.chipset_id); - } else { - passages_down = chipset->passable_data_lower; - passages_up = chipset->passable_data_upper; - animation_type = chipset->animation_type; - animation_fast = chipset->animation_speed != 0; - } - - if (passages_down.size() < 162) - passages_down.resize(162, (unsigned char) 0x0F); - if (passages_up.size() < 144) - passages_up.resize(144, (unsigned char) 0x0F); -} - -bool Game_Map::ReloadChipset() { - chipset = lcf::ReaderUtil::GetElement(lcf::Data::chipsets, map_info.chipset_id); - if (!chipset) { - return false; - } - return true; -} - -void Game_Map::OnTranslationChanged() { - ReloadChipset(); - // Marks common events for reload on map change - // This is not save to do while they are executing - translation_changed = true; -} - -Game_Vehicle* Game_Map::GetVehicle(Game_Vehicle::Type which) { - if (which == Game_Vehicle::Boat || - which == Game_Vehicle::Ship || - which == Game_Vehicle::Airship) { - return &vehicles[which - 1]; - } - - return nullptr; -} - -bool Game_Map::IsAnyEventStarting() { - for (Game_Event& ev : events) - if (ev.IsWaitingForegroundExecution() && !ev.GetList().empty() && ev.IsActive()) - return true; - - for (Game_CommonEvent& ev : common_events) - if (ev.IsWaitingForegroundExecution()) - return true; - - return false; -} - -bool Game_Map::IsAnyMovePending() { - auto check = [](auto& ev) { - return ev.IsMoveRouteOverwritten() && !ev.IsMoveRouteFinished(); - }; - const auto map_id = GetMapId(); - if (check(*Main_Data::game_player)) { - return true; - } - for (auto& vh: vehicles) { - if (vh.GetMapId() == map_id && check(vh)) { - return true; - } - } - for (auto& ev: events) { - if (check(ev)) { - return true; - } - } - - return false; -} - -void Game_Map::RemoveAllPendingMoves() { - const auto map_id = GetMapId(); - Main_Data::game_player->CancelMoveRoute(); - for (auto& vh: vehicles) { - if (vh.GetMapId() == map_id) { - vh.CancelMoveRoute(); - } - } - for (auto& ev: events) { - ev.CancelMoveRoute(); - } -} - -static int DoSubstitute(std::vector& tiles, int old_id, int new_id) { - int num_subst = 0; - for (size_t i = 0; i < tiles.size(); ++i) { - if (tiles[i] == old_id) { - tiles[i] = (uint8_t) new_id; - ++num_subst; - } - } - return num_subst; -} - -int Game_Map::SubstituteDown(int old_id, int new_id) { - return DoSubstitute(map_info.lower_tiles, old_id, new_id); -} - -int Game_Map::SubstituteUp(int old_id, int new_id) { - return DoSubstitute(map_info.upper_tiles, old_id, new_id); -} - -void Game_Map::ReplaceTileAt(int x, int y, int new_id, int layer) { - auto pos = x + y * map->width; - auto& layer_vec = layer >= 1 ? map->upper_layer : map->lower_layer; - layer_vec[pos] = static_cast(new_id); -} - -int Game_Map::GetTileIdAt(int x, int y, int layer, bool chip_id_or_index) { - if (x < 0 || x >= map->width || y < 0 || y >= map->height) { - return 0; // Return 0 for out-of-bounds coordinates - } - - auto pos = x + y * map->width; - auto& layer_vec = layer >= 1 ? map->upper_layer : map->lower_layer; - - int tile_output = chip_id_or_index ? layer_vec[pos] : ChipIdToIndex(layer_vec[pos]); - if (layer >= 1) tile_output -= BLOCK_F_INDEX; - - return tile_output; -} - -std::vector Game_Map::GetTilesIdAt(Rect coords, int layer, bool chip_id_or_index) { - std::vector tiles_collection; - for (int i = 0; i < coords.height; ++i) { - for (int j = 0; j < coords.width; ++j) { - tiles_collection.emplace_back(Game_Map::GetTileIdAt(coords.x + j, coords.y + i, layer, chip_id_or_index)); - } - } - return tiles_collection; -} - -std::string Game_Map::ConstructMapName(int map_id, bool is_easyrpg) { - std::stringstream ss; - ss << "Map" << std::setfill('0') << std::setw(4) << map_id; - if (is_easyrpg) { - return Player::fileext_map.MakeFilename(ss.str(), SUFFIX_EMU); - } else { - return Player::fileext_map.MakeFilename(ss.str(), SUFFIX_LMU); - } -} - -FileRequestAsync* Game_Map::RequestMap(int map_id) { -#ifdef __EMSCRIPTEN__ - Player::translation.RequestAndAddMap(map_id); -#endif - - auto* request = AsyncHandler::RequestFile(Game_Map::ConstructMapName(map_id, false)); - request->SetImportantFile(true); - return request; -} - -// MapEventCache -////////////////// -void Game_Map::Caching::MapEventCache::AddEvent(const lcf::rpg::Event& ev) { - auto id = ev.ID; - - if (std::find(event_ids.begin(), event_ids.end(), id) == event_ids.end()) { - event_ids.emplace_back(id); - } -} - -void Game_Map::Caching::MapEventCache::RemoveEvent(const lcf::rpg::Event& ev) { - auto id = ev.ID; - - auto it = std::find(event_ids.begin(), event_ids.end(), id); - - if (it != event_ids.end()) { - event_ids.erase(it); - } -} - -// Parallax -///////////// - -namespace { - int parallax_width; - int parallax_height; -} - -/* Helper function to get the current parallax parameters. If the default - * parallax for the current map was overridden by a "Change Parallax BG" - * command, the result is filled out from those values in the SaveMapInfo. - * Otherwise, the result is filled out from the default for the current map. - */ -static Game_Map::Parallax::Params GetParallaxParams() { - Game_Map::Parallax::Params params = {}; - - if (!map_info.parallax_name.empty()) { - params.name = map_info.parallax_name; - params.scroll_horz = map_info.parallax_horz; - params.scroll_horz_auto = map_info.parallax_horz_auto; - params.scroll_horz_speed = map_info.parallax_horz_speed; - params.scroll_vert = map_info.parallax_vert; - params.scroll_vert_auto = map_info.parallax_vert_auto; - params.scroll_vert_speed = map_info.parallax_vert_speed; - } else if (map->parallax_flag) { - // Default case when map parallax hasn't been overwritten. - params.name = ToString(map->parallax_name); - params.scroll_horz = map->parallax_loop_x; - params.scroll_horz_auto = map->parallax_auto_loop_x; - params.scroll_horz_speed = map->parallax_sx; - params.scroll_vert = map->parallax_loop_y; - params.scroll_vert_auto = map->parallax_auto_loop_y; - params.scroll_vert_speed = map->parallax_sy; - } else { - // No BG; use default-constructed Param - } - - return params; -} - -std::string Game_Map::Parallax::GetName() { - return GetParallaxParams().name; -} - -int Game_Map::Parallax::GetX() { - return (-panorama.pan_x / TILE_SIZE) / 2; -} - -int Game_Map::Parallax::GetY() { - return (-panorama.pan_y / TILE_SIZE) / 2; -} - -void Game_Map::Parallax::Initialize(int width, int height) { - parallax_width = width; - parallax_height = height; - - if (panorama_on_map_init) { - SetPositionX(map_info.position_x); - SetPositionY(map_info.position_y); - } - - if (reset_panorama_x_on_next_init) { - ResetPositionX(); - } - if (reset_panorama_y_on_next_init) { - ResetPositionY(); - } - - if (Player::IsRPG2k() && !panorama_on_map_init) { - SetPositionX(panorama.pan_x); - SetPositionY(panorama.pan_y); - } - - panorama_on_map_init = false; -} - -void Game_Map::Parallax::AddPositionX(int off_x) { - SetPositionX(panorama.pan_x + off_x); -} - -void Game_Map::Parallax::AddPositionY(int off_y) { - SetPositionY(panorama.pan_y + off_y); -} - -void Game_Map::Parallax::SetPositionX(int x) { - // FIXME: Fixes a crash with ChangeBG commands in events, but not correct. - // Real fix TBD - if (parallax_width) { - const int w = parallax_width * TILE_SIZE * 2; - panorama.pan_x = (x + w) % w; - } -} - -void Game_Map::Parallax::SetPositionY(int y) { - // FIXME: Fixes a crash with ChangeBG commands in events, but not correct. - // Real fix TBD - if (parallax_height) { - const int h = parallax_height * TILE_SIZE * 2; - panorama.pan_y = (y + h) % h; - } -} - -void Game_Map::Parallax::ResetPositionX() { - Params params = GetParallaxParams(); - - if (params.name.empty()) { - return; - } - - if (!params.scroll_horz && !LoopHorizontal()) { - // What is the width of the panorama to display on screen? - int pan_screen_width = Player::screen_width; - if (Player::game_config.fake_resolution.Get()) { - int map_width = Game_Map::GetTilesX() * TILE_SIZE; - if (map_width < pan_screen_width) { - pan_screen_width = map_width; - } - } - - int tiles_per_screen = pan_screen_width / TILE_SIZE; - if (pan_screen_width % TILE_SIZE != 0) { - ++tiles_per_screen; - } - - if (GetTilesX() > tiles_per_screen && parallax_width > pan_screen_width) { - const int w = (GetTilesX() - tiles_per_screen) * TILE_SIZE; - const int ph = 2 * std::min(w, parallax_width - pan_screen_width) * map_info.position_x / w; - if (Player::IsRPG2k()) { - SetPositionX(ph); - } else { - // 2k3 does not do the (% parallax_width * TILE_SIZE * 2) here - panorama.pan_x = ph; - } - } else { - panorama.pan_x = 0; - } - } -} - -void Game_Map::Parallax::ResetPositionY() { - Params params = GetParallaxParams(); - - if (params.name.empty()) { - return; - } - - if (!params.scroll_vert && !Game_Map::LoopVertical()) { - // What is the height of the panorama to display on screen? - int pan_screen_height = Player::screen_height; - if (Player::game_config.fake_resolution.Get()) { - int map_height = Game_Map::GetTilesY() * TILE_SIZE; - if (map_height < pan_screen_height) { - pan_screen_height = map_height; - } - } - - int tiles_per_screen = pan_screen_height / TILE_SIZE; - if (pan_screen_height % TILE_SIZE != 0) { - ++tiles_per_screen; - } - - if (GetTilesY() > tiles_per_screen && parallax_height > pan_screen_height) { - const int h = (GetTilesY() - tiles_per_screen) * TILE_SIZE; - const int pv = 2 * std::min(h, parallax_height - pan_screen_height) * map_info.position_y / h; - SetPositionY(pv); - } else { - panorama.pan_y = 0; - } - } -} - -void Game_Map::Parallax::ScrollRight(int distance) { - if (!distance) { - return; - } - - Params params = GetParallaxParams(); - if (params.name.empty()) { - return; - } - - if (params.scroll_horz) { - AddPositionX(distance); - return; - } - - if (Game_Map::LoopHorizontal()) { - return; - } - - ResetPositionX(); -} - -void Game_Map::Parallax::ScrollDown(int distance) { - if (!distance) { - return; - } - - Params params = GetParallaxParams(); - if (params.name.empty()) { - return; - } - - if (params.scroll_vert) { - AddPositionY(distance); - return; - } - - if (Game_Map::LoopVertical()) { - return; - } - - ResetPositionY(); -} - -void Game_Map::Parallax::Update() { - Params params = GetParallaxParams(); - - if (params.name.empty()) - return; - - auto scroll_amt = [](int speed) { - return speed < 0 ? (1 << -speed) : -(1 << speed); - }; - - if (params.scroll_horz - && params.scroll_horz_auto - && params.scroll_horz_speed != 0) { - AddPositionX(scroll_amt(params.scroll_horz_speed)); - } - - if (params.scroll_vert - && params.scroll_vert_auto - && params.scroll_vert_speed != 0) { - if (parallax_height != 0) { - AddPositionY(scroll_amt(params.scroll_vert_speed)); - } - } -} - -void Game_Map::Parallax::ChangeBG(const Params& params) { - map_info.parallax_name = params.name; - map_info.parallax_horz = params.scroll_horz; - map_info.parallax_horz_auto = params.scroll_horz_auto; - map_info.parallax_horz_speed = params.scroll_horz_speed; - map_info.parallax_vert = params.scroll_vert; - map_info.parallax_vert_auto = params.scroll_vert_auto; - map_info.parallax_vert_speed = params.scroll_vert_speed; - - reset_panorama_x_on_next_init = !Game_Map::LoopHorizontal() && !map_info.parallax_horz; - reset_panorama_y_on_next_init = !Game_Map::LoopVertical() && !map_info.parallax_vert; - - Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get(); - if (!scene || !scene->spriteset) - return; - scene->spriteset->ParallaxUpdated(); -} - -void Game_Map::Parallax::ClearChangedBG() { - Params params {}; // default Param indicates no override - ChangeBG(params); -} +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +// Headers +#include +#include +#include +#include +#include +#include +#include +#include + +#include "async_handler.h" +#include "options.h" +#include "system.h" +#include "game_battle.h" +#include "game_battler.h" +#include "game_map.h" +#include "game_interpreter_map.h" +#include "game_switches.h" +#include "game_player.h" +#include "game_party.h" +#include "game_message.h" +#include "game_screen.h" +#include "game_pictures.h" +#include "game_variables.h" +#include "scene_battle.h" +#include "scene_map.h" +#include +#include +#include "map_data.h" +#include "main_data.h" +#include "output.h" +#include "util_macro.h" +#include "game_system.h" +#include "filefinder.h" +#include "player.h" +#include "input.h" +#include "utils.h" +#include "rand.h" +#include +#include +#include "scene_gameover.h" +#include "feature.h" + +namespace { + // Intended bad value, Game_Map::Init sets them correctly + int screen_width = -1; + int screen_height = -1; + + lcf::rpg::SaveMapInfo map_info; + lcf::rpg::SavePanorama panorama; + + bool need_refresh; + + int animation_type; + bool animation_fast; + std::vector passages_down; + std::vector passages_up; + std::vector events; + std::vector common_events; + std::unique_ptr map_cache; + + std::unique_ptr map; + + std::unique_ptr interpreter; + std::vector vehicles; + + lcf::rpg::Chipset* chipset; + + //FIXME: Find a better way to do this. + bool panorama_on_map_init = true; + bool reset_panorama_x_on_next_init = true; + bool reset_panorama_y_on_next_init = true; + + bool translation_changed = false; + + // Used when the current map is not in the maptree + const lcf::rpg::MapInfo empty_map_info; +} + +namespace Game_Map { +void SetupCommon(); +} + +void Game_Map::OnContinueFromBattle() { + Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeBattleMusic()); +} + +static Game_Map::Parallax::Params GetParallaxParams(); + +void Game_Map::Init() { + Dispose(); + + map_info = {}; + panorama = {}; + SetNeedRefresh(true); + + interpreter.reset(new Game_Interpreter_Map(true)); + map_cache.reset(new Caching::MapCache()); + + InitCommonEvents(); + + vehicles.clear(); + vehicles.emplace_back(Game_Vehicle::Boat); + vehicles.emplace_back(Game_Vehicle::Ship); + vehicles.emplace_back(Game_Vehicle::Airship); +} + +void Game_Map::InitCommonEvents() { + common_events.clear(); + common_events.reserve(lcf::Data::commonevents.size()); + for (const lcf::rpg::CommonEvent& ev : lcf::Data::commonevents) { + common_events.emplace_back(ev.ID); + } + translation_changed = false; +} + +void Game_Map::Dispose() { + events.clear(); + map.reset(); + map_info = {}; + panorama = {}; +} + +void Game_Map::Quit() { + Dispose(); + common_events.clear(); + interpreter.reset(); + map_cache.reset(); +} + +int Game_Map::GetMapSaveCount() { + return (Player::IsRPG2k3() && map->save_count_2k3e > 0) + ? map->save_count_2k3e + : map->save_count; +} + +void Game_Map::Setup(std::unique_ptr map_in) { + Dispose(); + + map = std::move(map_in); + + SetupCommon(); + + panorama_on_map_init = true; + Parallax::ClearChangedBG(); + + SetEncounterSteps(GetMapInfo().encounter_steps); + SetChipset(map->chipset_id); + + std::iota(map_info.lower_tiles.begin(), map_info.lower_tiles.end(), 0); + std::iota(map_info.upper_tiles.begin(), map_info.upper_tiles.end(), 0); + + // Save allowed + const auto* current_info = &GetMapInfo(); + int current_index = current_info->ID; + int can_save = current_info->save; + int can_escape = current_info->escape; + int can_teleport = current_info->teleport; + + while (can_save == lcf::rpg::MapInfo::TriState_parent + || can_escape == lcf::rpg::MapInfo::TriState_parent + || can_teleport == lcf::rpg::MapInfo::TriState_parent) + { + const auto* parent_info = &GetParentMapInfo(*current_info); + int parent_index = parent_info->ID; + if (parent_index == 0) { + // If parent is 0 and flag is parent, it's implicitly enabled. + break; + } + if (parent_index == current_index) { + Output::Warning("Map {} has parent pointing to itself!", current_index); + break; + } + current_info = parent_info; + if (can_save == lcf::rpg::MapInfo::TriState_parent) { + can_save = current_info->save; + } + if (can_escape == lcf::rpg::MapInfo::TriState_parent) { + can_escape = current_info->escape; + } + if (can_teleport == lcf::rpg::MapInfo::TriState_parent) { + can_teleport = current_info->teleport; + } + } + Main_Data::game_system->SetAllowSave(can_save != lcf::rpg::MapInfo::TriState_forbid); + Main_Data::game_system->SetAllowEscape(can_escape != lcf::rpg::MapInfo::TriState_forbid); + Main_Data::game_system->SetAllowTeleport(can_teleport != lcf::rpg::MapInfo::TriState_forbid); + + auto& player = *Main_Data::game_player; + + SetPositionX(player.GetX() * SCREEN_TILE_SIZE - player.GetPanX()); + SetPositionY(player.GetY() * SCREEN_TILE_SIZE - player.GetPanY()); + + // Update the save counts so that if the player saves the game + // events will properly resume upon loading. + Main_Data::game_player->UpdateSaveCounts(lcf::Data::system.save_count, GetMapSaveCount()); +} + +void Game_Map::SetupFromSave( + std::unique_ptr map_in, + lcf::rpg::SaveMapInfo save_map, + lcf::rpg::SaveVehicleLocation save_boat, + lcf::rpg::SaveVehicleLocation save_ship, + lcf::rpg::SaveVehicleLocation save_airship, + lcf::rpg::SaveEventExecState save_fg_exec, + lcf::rpg::SavePanorama save_pan, + std::vector save_ce) { + + map = std::move(map_in); + map_info = std::move(save_map); + panorama = std::move(save_pan); + + SetupCommon(); + + const bool is_db_save_compat = Main_Data::game_player->IsDatabaseCompatibleWithSave(lcf::Data::system.save_count); + const bool is_map_save_compat = Main_Data::game_player->IsMapCompatibleWithSave(GetMapSaveCount()); + + InitCommonEvents(); + + if (is_db_save_compat && is_map_save_compat) { + for (size_t i = 0; i < std::min(save_ce.size(), common_events.size()); ++i) { + common_events[i].SetSaveData(save_ce[i].parallel_event_execstate); + } + } + + if (is_map_save_compat) { + std::vector destroyed_event_ids; + + for (size_t i = 0, j = 0; i < events.size() && j < map_info.events.size(); ++i) { + auto& ev = events[i]; + auto& save_ev = map_info.events[j]; + if (ev.GetId() == save_ev.ID) { + ev.SetSaveData(save_ev); + ++j; + } else { + if (save_ev.ID > ev.GetId()) { + // assume that the event has been destroyed during gameplay via "DestroyMapEvent" + destroyed_event_ids.emplace_back(ev.GetId()); + } else { + Output::Debug("SetupFromSave: Unexpected ID {}/{}", save_ev.ID, ev.GetId()); + } + } + } + for (size_t i = 0; i < destroyed_event_ids.size(); ++i) { + DestroyMapEvent(destroyed_event_ids[i], true); + } + if (destroyed_event_ids.size() > 0) { + UpdateUnderlyingEventReferences(); + } + } + + // Handle cloned events in a separate loop, regardless of "is_map_save_compat" + if (Player::HasEasyRpgExtensions()) { + for (size_t i = 0; i < map_info.events.size(); ++i) { + auto& save_ev = map_info.events[i]; + bool is_cloned_evt = save_ev.easyrpg_clone_map_id > 0 || save_ev.easyrpg_clone_event_id > 0; + if (is_cloned_evt && CloneMapEvent( + save_ev.easyrpg_clone_map_id, save_ev.easyrpg_clone_event_id, + save_ev.position_x, save_ev.position_y, + save_ev.ID, "")) { // FIXME: Customized event names for saved events aren't part of liblcf/SaveMapEvent at the moment & thus cannot be restored + if (auto new_event = GetEvent(save_ev.ID); new_event != nullptr) { + new_event->SetSaveData(save_ev); + } + } + } + UpdateUnderlyingEventReferences(); + } + map_info.events.clear(); + interpreter->Clear(); + + GetVehicle(Game_Vehicle::Boat)->SetSaveData(std::move(save_boat)); + GetVehicle(Game_Vehicle::Ship)->SetSaveData(std::move(save_ship)); + GetVehicle(Game_Vehicle::Airship)->SetSaveData(std::move(save_airship)); + + if (is_map_save_compat) { + // Make main interpreter "busy" if save contained events to prevent auto-events from starting + interpreter->SetState(std::move(save_fg_exec)); + } + + SetEncounterSteps(map_info.encounter_steps); + + // RPG_RT bug: Chipset is not loaded. Fixed in 2k3E + if (Player::IsRPG2k3E()) { + SetChipset(map_info.chipset_id); + } else { + SetChipset(0); + } + + if (!is_map_save_compat) { + panorama = {}; + } + + // We want to support loading rm2k3e panning chunks + // but also not break other saves which don't have them. + // To solve this problem, we reuse the scrolling methods + // which always reset the position anyways when scroll_horz/vert + // is false. + // This produces compatible behavior for old RPG_RT saves, namely + // the pan_x/y is always forced to 0. + // If the later async code will load panorama, set the flag to not clear the offsets. + // FIXME: RPG_RT compatibility bug: Everytime we load a savegame with default panorama chunks, + // this causes them to get overwritten + // FIXME: RPG_RT compatibility bug: On async platforms, panorama async loading can + // cause panorama chunks to be out of sync. + Game_Map::Parallax::ChangeBG(GetParallaxParams()); +} + +std::unique_ptr Game_Map::LoadMapFile(int map_id) { + std::unique_ptr map; + + // Attempt to load either the EasyRPG map file or the RPG Maker map file first, depending on config. + // If it fails, try the other one. + // FIXME: Assert map was cached for async platforms + bool map_is_easyrpg_file = Player::player_config.prefer_easyrpg_map_files.Get(); + std::string map_name = Game_Map::ConstructMapName(map_id, map_is_easyrpg_file); + std::string map_file = FileFinder::Game().FindFile(map_name); + if (map_file.empty()) { + map_is_easyrpg_file = !map_is_easyrpg_file; + map_name = Game_Map::ConstructMapName(map_id, map_is_easyrpg_file); + map_file = FileFinder::Game().FindFile(map_name); + + if (map_file.empty()) { + Output::Error("Loading of Map {} failed.\nThe map was not found.", map_name); + return nullptr; + } + } + + auto map_stream = FileFinder::Game().OpenInputStream(map_file); + if (!map_stream) { + Output::Error("Loading of Map {} failed.\nMap not readable.", map_name); + return nullptr; + } + + if (map_is_easyrpg_file) { + map = lcf::LMU_Reader::LoadXml(map_stream); + } else { + map = lcf::LMU_Reader::Load(map_stream, Player::encoding); + if (Input::IsRecording()) { + map_stream.clear(); + map_stream.seekg(0); + Input::AddRecordingData(Input::RecordingData::Hash, + fmt::format("map{:04} {:#08x}", map_id, Utils::CRC32(map_stream))); + } + } + + Output::Debug("Loaded Map {}", map_name); + + if (map.get() == NULL) { + Output::ErrorStr(lcf::LcfReader::GetError()); + } + + return map; +} + +void Game_Map::SetupCommon() { + screen_width = (Player::screen_width / 16.0) * SCREEN_TILE_SIZE; + screen_height = (Player::screen_height / 16.0) * SCREEN_TILE_SIZE; + + if (!Tr::GetCurrentTranslationId().empty()) { + TranslateMapMessages(GetMapId(), *map); + } + SetNeedRefresh(true); + + PrintPathToMap(); + + if (translation_changed) { + InitCommonEvents(); + } + + map_cache->Clear(); + + CreateMapEvents(); +} + +void Game_Map::CreateMapEvents() { + events.reserve(map->events.size()); + for (auto& ev : map->events) { + events.emplace_back(GetMapId(), &ev); + AddEventToCache(ev); + } +} + +void Game_Map::AddEventToCache(const lcf::rpg::Event& ev) { + using Op = Caching::ObservedVarOps; + + for (const auto& pg : ev.pages) { + if (pg.condition.flags.switch_a) { + map_cache->AddEventAsRefreshTarget(pg.condition.switch_a_id, ev); + } + if (pg.condition.flags.switch_b) { + map_cache->AddEventAsRefreshTarget(pg.condition.switch_b_id, ev); + } + if (pg.condition.flags.variable) { + map_cache->AddEventAsRefreshTarget(pg.condition.variable_id, ev); + } + } +} + +void Game_Map::RemoveEventFromCache(const lcf::rpg::Event& ev) { + using Op = Caching::ObservedVarOps; + + for (const auto& pg : ev.pages) { + if (pg.condition.flags.switch_a) { + map_cache->RemoveEventAsRefreshTarget(pg.condition.switch_a_id, ev); + } + if (pg.condition.flags.switch_b) { + map_cache->RemoveEventAsRefreshTarget(pg.condition.switch_b_id, ev); + } + if (pg.condition.flags.variable) { + map_cache->RemoveEventAsRefreshTarget(pg.condition.variable_id, ev); + } + } +} + +void Game_Map::Caching::MapCache::Clear() { + for (int i = 0; i < static_cast(ObservedVarOps_END); i++) { + refresh_targets_by_varid[i].clear(); + } +} + +bool Game_Map::CloneMapEvent(int src_map_id, int src_event_id, int target_x, int target_y, int target_event_id, std::string_view target_name) { + std::unique_ptr source_map_storage; + const lcf::rpg::Map* source_map; + + if (src_map_id == GetMapId()) { + source_map = &GetMap(); + } else { + source_map_storage = Game_Map::LoadMapFile(src_map_id); + source_map = source_map_storage.get(); + + if (source_map_storage == nullptr) { + Output::Warning("CloneMapEvent: Invalid source map ID {}", src_map_id); + return false; + } + + if (!Tr::GetCurrentTranslationId().empty()) { + TranslateMapMessages(src_map_id, *source_map_storage); + } + } + + const lcf::rpg::Event* source_event = FindEventById(source_map->events, src_event_id); + if (source_event == nullptr) { + Output::Warning("CloneMapEvent: Event ID {} not found on source map {}", src_event_id, src_map_id); + return false; + } + + lcf::rpg::Event new_event = *source_event; + if (target_event_id > 0) { + DestroyMapEvent(target_event_id, true); + new_event.ID = target_event_id; + } else { + new_event.ID = GetNextAvailableEventId(); + } + new_event.x = target_x; + new_event.y = target_y; + + if (!target_name.empty()) { + new_event.name = lcf::DBString(target_name); + } + + // sorted insert + auto insert_it = map->events.insert( + std::upper_bound(map->events.begin(), map->events.end(), new_event, [](const auto& e, const auto& e2) { + return e.ID < e2.ID; + }), new_event); + + auto game_event = Game_Event(GetMapId(), &*insert_it); + game_event.data()->easyrpg_clone_event_id = src_event_id; + game_event.data()->easyrpg_clone_map_id = src_map_id; + + events.insert( + std::upper_bound(events.begin(), events.end(), game_event, [](const auto& e, const auto& e2) { + return e.GetId() < e2.GetId(); + }), std::move(game_event)); + + UpdateUnderlyingEventReferences(); + + AddEventToCache(new_event); + + Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get(); + if (scene) { + scene->spriteset->Refresh(); + SetNeedRefresh(true); + } + + return true; +} + +bool Game_Map::DestroyMapEvent(const int event_id, bool from_clone) { + const lcf::rpg::Event* event = FindEventById(map->events, event_id); + + if (event == nullptr) { + if (!from_clone) { + Output::Warning("DestroyMapEvent: Event ID {} not found on current map", event_id); + } + return true; + } + + // Remove event from cache + RemoveEventFromCache(*event); + + // Remove event from events vector + for (auto it = events.begin(); it != events.end(); ++it) { + if (it->GetId() == event_id) { + events.erase(it); + break; + } + } + + // Remove event from map + for (auto it = map->events.begin(); it != map->events.end(); ++it) { + if (it->ID == event_id) { + map->events.erase(it); + break; + } + } + + if (!from_clone) { + UpdateUnderlyingEventReferences(); + + Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get(); + scene->spriteset->Refresh(); + SetNeedRefresh(true); + } + + if (GetInterpreter().GetOriginalEventId() == event_id) { + // Prevent triggering "invalid event on stack" sanity check + GetInterpreter().ClearOriginalEventId(); + } + + return true; +} + +void Game_Map::TranslateMapMessages(int mapId, lcf::rpg::Map& map) { + std::stringstream ss; + ss << "map" << std::setfill('0') << std::setw(4) << mapId << ".po"; + Player::translation.RewriteMapMessages(ss.str(), map); +} + + +void Game_Map::UpdateUnderlyingEventReferences() { + // Update references because modifying the vector can reallocate + size_t idx = 0; + for (auto& ev : events) { + ev.SetUnderlyingEvent(&map->events.at(idx++)); + } + + Main_Data::game_screen->UpdateUnderlyingEventReferences(); +} + +const lcf::rpg::Event* Game_Map::FindEventById(const std::vector& events, int eventId) { + for (const auto& ev : events) { + if (ev.ID == eventId) { + return &ev; + } + } + return nullptr; +} + +int Game_Map::GetNextAvailableEventId() { + if (map->events.empty()) { + return 1; + } else { + return map->events.back().ID + 1; + } +} + +void Game_Map::PrepareSave(lcf::rpg::Save& save) { + save.foreground_event_execstate = interpreter->GetSaveState(); + + save.airship_location = GetVehicle(Game_Vehicle::Airship)->GetSaveData(); + save.ship_location = GetVehicle(Game_Vehicle::Ship)->GetSaveData(); + save.boat_location = GetVehicle(Game_Vehicle::Boat)->GetSaveData(); + + save.map_info = map_info; + save.map_info.chipset_id = GetChipset(); + if (save.map_info.chipset_id == GetOriginalChipset()) { + // This emulates RPG_RT behavior, where chipset id == 0 means use the default map chipset. + save.map_info.chipset_id = 0; + } + if (save.map_info.encounter_steps == GetOriginalEncounterSteps()) { + save.map_info.encounter_steps = -1; + } + // Note: RPG_RT does not use a sentinel for parallax parameters. Once the parallax BG is changed, it stays that way forever. + + save.map_info.events.clear(); + save.map_info.events.reserve(events.size()); + for (Game_Event& ev : events) { + save.map_info.events.push_back(ev.GetSaveData()); + } + + save.panorama = panorama; + + save.common_events.clear(); + save.common_events.reserve(common_events.size()); + for (Game_CommonEvent& ev : common_events) { + save.common_events.push_back(lcf::rpg::SaveCommonEvent()); + save.common_events.back().ID = ev.GetIndex(); + save.common_events.back().parallel_event_execstate = ev.GetSaveData(); + } +} + +void Game_Map::PlayBgm() { + const auto* current_info = &GetMapInfo(); + while (current_info->music_type == 0 && GetParentMapInfo(*current_info).ID != current_info->ID) { + current_info = &GetParentMapInfo(*current_info); + } + + if ((current_info->ID > 0) && !current_info->music.name.empty()) { + if (current_info->music_type == 1) { + return; + } + auto& music = current_info->music; + if (!Main_Data::game_player->IsAboard()) { + Main_Data::game_system->BgmPlay(music); + } else { + Main_Data::game_system->SetBeforeVehicleMusic(music); + } + } +} + +std::vector Game_Map::GetTilesLayer(int layer) { + return layer >= 1 ? map_info.upper_tiles : map_info.lower_tiles; +} + +void Game_Map::Refresh() { + if (GetMapId() > 0) { + for (Game_Event& ev : events) { + ev.RefreshPage(); + } + } + + need_refresh = false; +} + +Game_Interpreter_Map& Game_Map::GetInterpreter() { + assert(interpreter); + return *interpreter; +} + +void Game_Map::Scroll(int dx, int dy) { + int x = map_info.position_x; + AddScreenX(x, dx); + map_info.position_x = x; + + int y = map_info.position_y; + AddScreenY(y, dy); + map_info.position_y = y; + + if (dx == 0 && dy == 0) { + return; + } + + Main_Data::game_screen->OnMapScrolled(dx, dy); + Main_Data::game_pictures->OnMapScrolled(dx, dy); + Game_Map::Parallax::ScrollRight(dx); + Game_Map::Parallax::ScrollDown(dy); +} + +// Add inc to acc, clamping the result into the range [low, high]. +// If the result is clamped, inc is also modified to be actual amount +// that acc changed by. +static void ClampingAdd(int low, int high, int& acc, int& inc) { + int original_acc = acc; + // Do not use std::clamp here. When the map is smaller than the screen the + // upper bound is smaller than the lower bound making the function fail. + acc = std::max(low, std::min(high, acc + inc)); + inc = acc - original_acc; +} + +void Game_Map::AddScreenX(int& screen_x, int& inc) { + int map_width = GetTilesX() * SCREEN_TILE_SIZE; + if (LoopHorizontal()) { + screen_x = (screen_x + inc) % map_width; + } else { + ClampingAdd(0, map_width - screen_width, screen_x, inc); + } +} + +void Game_Map::AddScreenY(int& screen_y, int& inc) { + int map_height = GetTilesY() * SCREEN_TILE_SIZE; + if (LoopVertical()) { + screen_y = (screen_y + inc) % map_height; + } else { + ClampingAdd(0, map_height - screen_height, screen_y, inc); + } +} + +bool Game_Map::IsValid(int x, int y) { + return (x >= 0 && x < GetTilesX() && y >= 0 && y < GetTilesY()); +} + +static int GetPassableMask(int old_x, int old_y, int new_x, int new_y) { + int bit = 0; + if (new_x > old_x) { bit |= Passable::Right; } + if (new_x < old_x) { bit |= Passable::Left; } + if (new_y > old_y) { bit |= Passable::Down; } + if (new_y < old_y) { bit |= Passable::Up; } + return bit; +} + +static bool WouldCollide(const Game_Character& self, const Game_Character& other, bool self_conflict) { + if (self.GetThrough() || other.GetThrough()) { + return false; + } + + if (self.IsFlying() || other.IsFlying()) { + return false; + } + + if (!self.IsActive() || !other.IsActive()) { + return false; + } + + if (self.GetType() == Game_Character::Event + && other.GetType() == Game_Character::Event + && (self.IsOverlapForbidden() || other.IsOverlapForbidden())) { + return true; + } + + if (other.GetLayer() == lcf::rpg::EventPage::Layers_same && self_conflict) { + return true; + } + + if (self.GetLayer() == other.GetLayer()) { + return true; + } + + return false; +} + +template +static void MakeWayUpdate(T& other) { + other.Update(); +} + +static void MakeWayUpdate(Game_Event& other) { + other.Update(false); +} + +template +static bool CheckWayTestCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) { + if (&self == &other) { + return false; + } + + if (!other.IsInPosition(x, y)) { + return false; + } + + return WouldCollide(self, other, self_conflict); +} + +template +static bool MakeWayCollideEvent(int x, int y, const Game_Character& self, T& other, bool self_conflict) { + if (&self == &other) { + return false; + } + + if (!other.IsInPosition(x, y)) { + return false; + } + + // Force the other event to update, allowing them to possibly move out of the way. + MakeWayUpdate(other); + + if (!other.IsInPosition(x, y)) { + return false; + } + + return WouldCollide(self, other, self_conflict); +} + +static Game_Vehicle::Type GetCollisionVehicleType(const Game_Character* ch) { + if (ch && ch->GetType() == Game_Character::Vehicle) { + return static_cast(static_cast(ch)->GetVehicleType()); + } + return Game_Vehicle::None; +} + +bool Game_Map::CheckWay(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y + ) +{ + return CheckOrMakeWayEx( + self, from_x, from_y, to_x, to_y, true, {}, false + ); +} + +bool Game_Map::CheckWay(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y, + bool check_events_and_vehicles, + Span ignore_some_events_by_id) { + return CheckOrMakeWayEx( + self, from_x, from_y, to_x, to_y, + check_events_and_vehicles, + ignore_some_events_by_id, false + ); +} + +bool Game_Map::CheckOrMakeWayEx(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y, + bool check_events_and_vehicles, + Span ignore_some_events_by_id, + bool make_way + ) +{ + if (self.GetThrough()) { + return true; + } + + // Retrieve offsets to determine if this is an expanded-hitbox character + int min_dx, max_dx, min_dy, max_dy; + self.GetTileOffsets(min_dx, max_dx, min_dy, max_dy); + + // Detect if any expansion tags are active + bool is_expanded = (min_dx != 0 || max_dx != 0 || min_dy != 0 || max_dy != 0); + + // --- BRANCH 1: ORIGINAL LOGIC (Standard 1-tile events) --- + if (!is_expanded) { + // Infer directions before we do any rounding. + 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); + + // Now round for looping maps. + int t_to_x = Game_Map::RoundX(to_x); + int t_to_y = Game_Map::RoundY(to_y); + + // Note, even for diagonal, if the tile is invalid we still check vertical/horizontal first! + if (!Game_Map::IsValid(t_to_x, t_to_y)) { + return false; + } + + const auto vehicle_type = GetCollisionVehicleType(&self); + bool self_conflict = false; + + // Depending on whether we're supposed to call MakeWayCollideEvent + // (which might change the map) or not, choose what to call: + auto CheckOrMakeCollideEvent = [&](auto& other) { + if (make_way) { + return MakeWayCollideEvent(t_to_x, t_to_y, self, other, self_conflict); + } else { + return CheckWayTestCollideEvent( + t_to_x, t_to_y, self, other, self_conflict + ); + } + }; + + if (!self.IsJumping()) { + // Check for self conflict. + // If this event has a tile graphic and the tile itself has passage blocked in the direction + // we want to move, flag it as "self conflicting" for use later. + if (self.GetLayer() == lcf::rpg::EventPage::Layers_below && self.GetTileId() != 0) { + int tile_id = self.GetTileId(); + if ((passages_up[tile_id] & bit_from) == 0) { + self_conflict = true; + } + } + + if (vehicle_type == Game_Vehicle::None) { + // Check that we are allowed to step off of the current tile. + // Note: Vehicles can always step off a tile. + + // The current coordinate can be invalid due to an out-of-bounds teleport or a "Set Location" event. + // Round it for looping maps to ensure the check passes + // This is not fully bug compatible to RPG_RT. Assuming the Y-Coordinate is out-of-bounds: When moving + // left or right the invalid Y will stay in RPG_RT preventing events from being triggered, but we wrap it + // inbounds after the first move. + int t_from_x = Game_Map::RoundX(from_x); + int t_from_y = Game_Map::RoundY(from_y); + if (!IsPassableTile(&self, bit_from, t_from_x, t_from_y)) { + return false; + } + } + } + + if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) { + // Check for collision with events on the target tile. + if (ignore_some_events_by_id.empty()) { + for (auto& other : GetEvents()) { + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + } else { + for (auto& other : GetEvents()) { + if (std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end()) + continue; + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + } + + auto& player = Main_Data::game_player; + if (player->GetVehicleType() == Game_Vehicle::None) { + if (CheckOrMakeCollideEvent(*Main_Data::game_player)) { + return false; + } + } + for (auto vid : { Game_Vehicle::Boat, Game_Vehicle::Ship }) { + auto& other = vehicles[vid - 1]; + if (other.IsInCurrentMap()) { + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + } + auto& airship = vehicles[Game_Vehicle::Airship - 1]; + if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) { + if (CheckOrMakeCollideEvent(airship)) { + return false; + } + } + } + + int bit = bit_to; + if (self.IsJumping()) bit = Passable::Down; + + return IsPassableTile(&self, bit, t_to_x, t_to_y, check_events_and_vehicles, true); + } + + // --- BRANCH 2: EXPANDED LOGIC (Large events with tags) --- + else { + const auto vehicle_type = GetCollisionVehicleType(&self); + + // Iterate through every tile in the footprint (Width x Height) + for (int dy = min_dy; dy <= max_dy; ++dy) { + for (int dx = min_dx; dx <= max_dx; ++dx) { + // Calculate coordinates for this specific segment of the footprint + int cur_from_x = from_x + dx; + int cur_from_y = from_y + dy; + int cur_to_x = to_x + dx; + int cur_to_y = to_y + dy; + + // Infer directions for this specific segment + const int bit_from = GetPassableMask(cur_from_x, cur_from_y, cur_to_x, cur_to_y); + const int bit_to = GetPassableMask(cur_to_x, cur_to_y, cur_from_x, cur_from_y); + + // Round coordinates for looping maps + int tile_from_x = Game_Map::RoundX(cur_from_x); + int tile_from_y = Game_Map::RoundY(cur_from_y); + int tile_to_x = Game_Map::RoundX(cur_to_x); + int tile_to_y = Game_Map::RoundY(cur_to_y); + + // Fail if any segment of the expanded box moves out of map bounds + if (!Game_Map::IsValid(tile_to_x, tile_to_y)) { + return false; + } + + bool self_conflict = false; + + // Check for self conflict for this segment + if (!self.IsJumping() && self.GetLayer() == lcf::rpg::EventPage::Layers_same && self.GetTileId() != 0) { + int tile_id = self.GetTileId(); + if ((passages_up[tile_id] & bit_from) == 0) { + self_conflict = true; + } + } + + // Helper for checking collisions with other entities for this segment + auto CheckOrMakeCollideEvent = [&](auto& other) { + if (make_way) { + return MakeWayCollideEvent(tile_to_x, tile_to_y, self, other, self_conflict); + } else { + return CheckWayTestCollideEvent(tile_to_x, tile_to_y, self, other, self_conflict); + } + }; + + // 1. Check Map Passability (Environment/Terrain/Vehicles) + if (!self.IsJumping()) { + if (vehicle_type == Game_Vehicle::None) { + // Segment must be able to step OFF its current tile + if (!IsPassableTile(&self, bit_from, tile_from_x, tile_from_y, check_events_and_vehicles, true)) { + return false; + } + } + } + + // Segment must be able to step ONTO its target tile + int bit_onto = self.IsJumping() ? Passable::Down : bit_to; + if (!IsPassableTile(&self, bit_onto, tile_to_x, tile_to_y, check_events_and_vehicles, true)) { + return false; + } + + // 2. Check Entity Collisions + if (vehicle_type != Game_Vehicle::Airship && check_events_and_vehicles) { + // Check collisions with all Map Events + for (auto& other : GetEvents()) { + if (!ignore_some_events_by_id.empty() && std::find(ignore_some_events_by_id.begin(), ignore_some_events_by_id.end(), other.GetId()) != ignore_some_events_by_id.end()) + continue; + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + + // Check collision with Player + auto& player = Main_Data::game_player; + if (player->GetVehicleType() == Game_Vehicle::None) { + if (CheckOrMakeCollideEvent(*Main_Data::game_player)) { + return false; + } + } + + // Check collision with Boats/Ships + for (auto vid : { Game_Vehicle::Boat, Game_Vehicle::Ship }) { + auto& other = vehicles[vid - 1]; + if (other.IsInCurrentMap()) { + if (CheckOrMakeCollideEvent(other)) { + return false; + } + } + } + + // Check collision with Airship + auto& airship = vehicles[Game_Vehicle::Airship - 1]; + if (airship.IsInCurrentMap() && self.GetType() != Game_Character::Player) { + if (CheckOrMakeCollideEvent(airship)) { + return false; + } + } + } + } + } + return true; + } +} + +bool Game_Map::MakeWay(const Game_Character& self, + int from_x, int from_y, + int to_x, int to_y + ) +{ + return CheckOrMakeWayEx( + self, from_x, from_y, to_x, to_y, true, {}, true + ); +} + + +bool Game_Map::CanLandAirship(int x, int y) { + if (!Game_Map::IsValid(x, y)) return false; + + const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y)); + if (!terrain) { + Output::Warning("CanLandAirship: Invalid terrain at ({}, {})", x, y); + return false; + } + if (!terrain->airship_land) { + return false; + } + + for (auto& ev: events) { + if (ev.IsInPosition(x, y) + && ev.IsActive() + && ev.GetActivePage() != nullptr) { + return false; + } + } + for (auto vid: { Game_Vehicle::Boat, Game_Vehicle::Ship }) { + auto& vehicle = vehicles[vid - 1]; + if (vehicle.IsInCurrentMap() && vehicle.IsInPosition(x, y)) { + return false; + } + } + + const int bit = Passable::Down | Passable::Right | Passable::Left | Passable::Up; + + int tile_index = x + y * GetTilesX(); + + if (!IsPassableLowerTile(bit, tile_index)) { + return false; + } + + int tile_id = map->upper_layer[tile_index] - BLOCK_F; + tile_id = map_info.upper_tiles[tile_id]; + + return (passages_up[tile_id] & bit) != 0; +} + +bool Game_Map::CanEmbarkShip(Game_Player& player, int x, int y) { + auto bit = GetPassableMask(player.GetX(), player.GetY(), x, y); + return IsPassableTile(&player, bit, player.GetX(), player.GetY()); +} + +bool Game_Map::CanDisembarkShip(Game_Player& player, int x, int y) { + if (!Game_Map::IsValid(x, y)) { + return false; + } + + for (auto& ev: GetEvents()) { + if (ev.IsInPosition(x, y) + && ev.GetLayer() == lcf::rpg::EventPage::Layers_same + && ev.IsActive() + && ev.GetActivePage() != nullptr) { + return false; + } + } + + int bit = GetPassableMask(x, y, player.GetX(), player.GetY()); + + return IsPassableTile(nullptr, bit, x, y); +} + +bool Game_Map::IsPassableLowerTile(int bit, int tile_index) { + int tile_raw_id = map->lower_layer[tile_index]; + int tile_id = 0; + + if (tile_raw_id >= BLOCK_E) { + tile_id = tile_raw_id - BLOCK_E; + tile_id = map_info.lower_tiles[tile_id] + BLOCK_E_INDEX; + + } else if (tile_raw_id >= BLOCK_D) { + tile_id = (tile_raw_id - BLOCK_D) / BLOCK_D_STRIDE + BLOCK_D_INDEX; + int autotile_id = (tile_raw_id - BLOCK_D) % BLOCK_D_STRIDE; + + if (((passages_down[tile_id] & Passable::Wall) != 0) && ( + (autotile_id >= 20 && autotile_id <= 23) || + (autotile_id >= 33 && autotile_id <= 37) || + autotile_id == 42 || autotile_id == 43 || + autotile_id == 45 || autotile_id == 46)) + return true; + + } else if (tile_raw_id >= BLOCK_C) { + tile_id = (tile_raw_id - BLOCK_C) / BLOCK_C_STRIDE + BLOCK_C_INDEX; + + } else if (map->lower_layer[tile_index] < BLOCK_C) { + tile_id = tile_raw_id / BLOCK_B_STRIDE; + } + + return (passages_down[tile_id] & bit) != 0; +} + +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 (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) { + if (ev.HasTileSprite()) { + event_tile_id = ev.GetTileId(); + } + } + } + + // 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 (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) + return false; + return true; + } + + if ((passages_up[tile_id] & bit) == 0) + return false; + + if ((passages_up[tile_id] & Passable::Above) == 0) + return true; + + return IsPassableLowerTile(bit, tile_index); + } else { + return true; + } +} + +int Game_Map::GetBushDepth(int x, int y) { + if (!Game_Map::IsValid(x, y)) return 0; + + const lcf::rpg::Terrain* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x,y)); + if (!terrain) { + Output::Warning("GetBushDepth: Invalid terrain at ({}, {})", x, y); + return 0; + } + return terrain->bush_depth; +} + +bool Game_Map::IsCounter(int x, int y) { + if (!Game_Map::IsValid(x, y)) return false; + + int const tile_id = map->upper_layer[x + y * GetTilesX()]; + if (tile_id < BLOCK_F) return false; + int const index = map_info.upper_tiles[tile_id - BLOCK_F]; + return !!(passages_up[index] & Passable::Counter); +} + +int Game_Map::GetTerrainTag(int x, int y) { + if (!chipset) { + // FIXME: Is this ever possible? + return 1; + } + + auto& terrain_data = chipset->terrain_data; + + if (terrain_data.empty()) { + // RPG_RT optimisation: When the terrain is all 1, no terrain data is stored + return 1; + } + + // Terrain tag wraps on looping maps + if (Game_Map::LoopHorizontal()) { + x = RoundX(x); + } + if (Game_Map::LoopVertical()) { + y = RoundY(y); + } + + // RPG_RT always uses the terrain of the first lower tile + // for out of bounds coordinates. + unsigned chip_index = 0; + + if (Game_Map::IsValid(x, y)) { + const auto chip_id = map->lower_layer[x + y * GetTilesX()]; + chip_index = ChipIdToIndex(chip_id); + + // Apply tile substitution + if (chip_index >= BLOCK_E_INDEX && chip_index < NUM_LOWER_TILES) { + chip_index = map_info.lower_tiles[chip_index - BLOCK_E_INDEX] + BLOCK_E_INDEX; + } + } + + assert(chip_index < terrain_data.size()); + + return terrain_data[chip_index]; +} + +Game_Event* Game_Map::GetEventAt(int x, int y, bool require_active) { + auto& events = GetEvents(); + for (auto iter = events.rbegin(); iter != events.rend(); ++iter) { + auto& ev = *iter; + if (ev.IsInPosition(x, y) && (!require_active || ev.IsActive())) { + return &ev; + } + } + return nullptr; +} + +bool Game_Map::LoopHorizontal() { + return map->scroll_type == lcf::rpg::Map::ScrollType_horizontal || map->scroll_type == lcf::rpg::Map::ScrollType_both; +} + +bool Game_Map::LoopVertical() { + return map->scroll_type == lcf::rpg::Map::ScrollType_vertical || map->scroll_type == lcf::rpg::Map::ScrollType_both; +} + +int Game_Map::RoundX(int x, int units) { + if (LoopHorizontal()) { + return Utils::PositiveModulo(x, GetTilesX() * units); + } else { + return x; + } +} + +int Game_Map::RoundY(int y, int units) { + if (LoopVertical()) { + return Utils::PositiveModulo(y, GetTilesY() * units); + } else { + return y; + } +} + +int Game_Map::RoundDx(int dx, int units) { + if (LoopHorizontal()) { + return Utils::PositiveModulo(std::abs(dx), GetTilesX() * units) * Utils::Sign(dx); + } else { + return dx; + } +} + +int Game_Map::RoundDy(int dy, int units) { + if (LoopVertical()) { + return Utils::PositiveModulo(std::abs(dy), GetTilesY() * units) * Utils::Sign(dy); + } else { + return dy; + } +} + +int Game_Map::XwithDirection(int x, int direction) { + return RoundX(x + (direction == lcf::rpg::EventPage::Direction_right ? 1 : direction == lcf::rpg::EventPage::Direction_left ? -1 : 0)); +} + +int Game_Map::YwithDirection(int y, int direction) { + return RoundY(y + (direction == lcf::rpg::EventPage::Direction_down ? 1 : direction == lcf::rpg::EventPage::Direction_up ? -1 : 0)); +} + +int Game_Map::CheckEvent(int x, int y) { + for (const Game_Event& ev : events) { + if (ev.IsInPosition(x, y)) { + return ev.GetId(); + } + } + + return 0; +} + +void Game_Map::Update(MapUpdateAsyncContext& actx, bool is_preupdate) { + if (GetNeedRefresh()) { + Refresh(); + } + + if (!actx.IsActive()) { + //If not resuming from async op ... + UpdateProcessedFlags(is_preupdate); + } + + if (!actx.IsActive() || actx.IsParallelCommonEvent()) { + if (!UpdateCommonEvents(actx)) { + // Suspend due to common event async op ... + return; + } + } + + if (!actx.IsActive() || actx.IsParallelMapEvent()) { + if (!UpdateMapEvents(actx)) { + // Suspend due to map event async op ... + return; + } + } + + if (is_preupdate) { + return; + } + + if (!actx.IsActive()) { + //If not resuming from async op ... + Main_Data::game_player->Update(); + + for (auto& vehicle: vehicles) { + if (vehicle.GetMapId() == GetMapId()) { + vehicle.Update(); + } + } + } + + if (!actx.IsActive() || actx.IsMessage()) { + if (!UpdateMessage(actx)) { + // Suspend due to message async op ... + return; + } + } + + if (!actx.IsActive()) { + Main_Data::game_party->UpdateTimers(); + Main_Data::game_screen->Update(); + Main_Data::game_pictures->Update(false); + } + + if (!actx.IsActive() || actx.IsForegroundEvent()) { + if (!UpdateForegroundEvents(actx)) { + // Suspend due to foreground event async op ... + return; + } + } + + Parallax::Update(); + + actx = {}; +} + +void Game_Map::UpdateProcessedFlags(bool is_preupdate) { + for (Game_Event& ev : events) { + ev.SetProcessed(false); + } + if (!is_preupdate) { + Main_Data::game_player->SetProcessed(false); + for (auto& vehicle: vehicles) { + if (vehicle.IsInCurrentMap()) { + vehicle.SetProcessed(false); + } + } + } +} + + +bool Game_Map::UpdateCommonEvents(MapUpdateAsyncContext& actx) { + int resume_ce = actx.GetParallelCommonEvent(); + + for (Game_CommonEvent& ev : common_events) { + bool resume_async = false; + if (resume_ce != 0) { + // If resuming, skip all until the event to resume from .. + if (ev.GetIndex() != resume_ce) { + continue; + } else { + resume_ce = 0; + resume_async = true; + } + } + + auto aop = ev.Update(resume_async); + if (aop.IsActive()) { + // Suspend due to this event .. + actx = MapUpdateAsyncContext::FromCommonEvent(ev.GetIndex(), aop); + return false; + } + } + + actx = {}; + return true; +} + +bool Game_Map::UpdateMapEvents(MapUpdateAsyncContext& actx) { + int resume_ev = actx.GetParallelMapEvent(); + + for (Game_Event& ev : events) { + bool resume_async = false; + if (resume_ev != 0) { + // If resuming, skip all until the event to resume from .. + if (ev.GetId() != resume_ev) { + continue; + } else { + resume_ev = 0; + resume_async = true; + } + } + + auto aop = ev.Update(resume_async); + if (aop.IsActive()) { + // Suspend due to this event .. + actx = MapUpdateAsyncContext::FromMapEvent(ev.GetId(), aop); + return false; + } + } + + actx = {}; + return true; +} + +bool Game_Map::UpdateMessage(MapUpdateAsyncContext& actx) { + // Message system does not support suspend and resume internally. So if the last frame the message + // produced an async event, the message loop finished completely. Therefore this frame we should + // resume *after* the message and not run it again. + if (!actx.IsActive()) { + auto aop = Game_Message::Update(); + if (aop.IsActive()) { + actx = MapUpdateAsyncContext::FromMessage(aop); + return false; + } + } + + actx = {}; + return true; +} + +bool Game_Map::UpdateForegroundEvents(MapUpdateAsyncContext& actx) { + auto& interp = GetInterpreter(); + + // If we resume from async op, we don't clear the loop index. + const bool resume_fg = actx.IsForegroundEvent(); + + // Run any event loaded from last frame. + interp.Update(!resume_fg); + if (interp.IsAsyncPending()) { + // Suspend due to this event .. + actx = MapUpdateAsyncContext::FromForegroundEvent(interp.GetAsyncOp()); + return false; + } + + while (!interp.IsRunning() && !interp.ReachedLoopLimit()) { + interp.Clear(); + + // This logic is probably one big loop in RPG_RT. We have to replicate + // it here because once we stop executing from this we should not + // clear anymore waiting flags. + if (Scene::instance->HasRequestedScene() && interp.GetLoopCount() > 0) { + break; + } + Game_CommonEvent* run_ce = nullptr; + + for (auto& ce: common_events) { + if (ce.IsWaitingForegroundExecution()) { + run_ce = &ce; + break; + } + } + if (run_ce) { + interp.Push(run_ce); + } + + Game_Event* run_ev = nullptr; + for (auto& ev: events) { + if (ev.IsWaitingForegroundExecution()) { + if (!ev.IsActive()) { + ev.ClearWaitingForegroundExecution(); + continue; + } + run_ev = &ev; + break; + } + } + if (run_ev) { + if (run_ev->WasStartedByDecisionKey()) { + interp.Push(run_ev); + } else if (auto t = run_ev->GetTrigger()) { + switch (*t) { + case lcf::rpg::EventPage::Trigger_touched: + interp.Push(run_ev); + break; + case lcf::rpg::EventPage::Trigger_collision: + interp.Push(run_ev); + break; + case lcf::rpg::EventPage::Trigger_auto_start: + interp.Push(run_ev); + break; + case lcf::rpg::EventPage::Trigger_action: + case lcf::rpg::EventPage::Trigger_parallel: + interp.Push(run_ev); + break; + } + } else { + interp.Push(run_ev); + } + run_ev->ClearWaitingForegroundExecution(); + } + + // If no events to run we're finished. + if (!interp.IsRunning()) { + break; + } + + interp.Update(false); + if (interp.IsAsyncPending()) { + // Suspend due to this event .. + actx = MapUpdateAsyncContext::FromForegroundEvent(interp.GetAsyncOp()); + return false; + } + } + + actx = {}; + return true; +} + +lcf::rpg::MapInfo const& Game_Map::GetMapInfo() { + return GetMapInfo(GetMapId()); +} + +lcf::rpg::MapInfo const& Game_Map::GetMapInfo(int map_id) { + for (const auto& mi: lcf::Data::treemap.maps) { + if (mi.ID == map_id) { + return mi; + } + } + + Output::Debug("Map {} not in Maptree", map_id); + return empty_map_info; +} + +const lcf::rpg::MapInfo& Game_Map::GetParentMapInfo() { + return GetParentMapInfo(GetMapInfo()); +} + +const lcf::rpg::MapInfo& Game_Map::GetParentMapInfo(const lcf::rpg::MapInfo& map_info) { + return GetMapInfo(map_info.parent_map); +} + +lcf::rpg::Map const& Game_Map::GetMap() { + return *map; +} + +int Game_Map::GetMapId() { + return Main_Data::game_player->GetMapId(); +} + +void Game_Map::PrintPathToMap() { + const auto* current_info = &GetMapInfo(); + std::ostringstream ss; + ss << current_info->name; + + current_info = &GetParentMapInfo(*current_info); + while (current_info->ID != 0 && current_info->ID != GetMapId()) { + ss << " < " << current_info->name; + current_info = &GetParentMapInfo(*current_info); + } + + Output::Debug("Tree: {}", ss.str()); +} + +int Game_Map::GetTilesX() { + return map->width; +} + +int Game_Map::GetTilesY() { + return map->height; +} + +int Game_Map::GetOriginalEncounterSteps() { + return GetMapInfo().encounter_steps; +} + +int Game_Map::GetEncounterSteps() { + return map_info.encounter_steps; +} + +void Game_Map::SetEncounterSteps(int step) { + if (step < 0) { + step = GetOriginalEncounterSteps(); + } + map_info.encounter_steps = step; +} + +std::vector Game_Map::GetEncountersAt(int x, int y) { + int terrain_tag = GetTerrainTag(Main_Data::game_player->GetX(), Main_Data::game_player->GetY()); + + std::function is_acceptable = [=](int troop_id) { + const lcf::rpg::Troop* troop = lcf::ReaderUtil::GetElement(lcf::Data::troops, troop_id); + if (!troop) { + Output::Warning("GetEncountersAt: Invalid troop ID {} in encounter list", troop_id); + return false; + } + + const auto& terrain_set = troop->terrain_set; + + // RPG_RT optimisation: Omitted entries are the default value (true) + return terrain_set.size() <= (unsigned)(terrain_tag - 1) || + terrain_set[terrain_tag - 1]; + }; + + std::vector out; + + for (unsigned int i = 0; i < lcf::Data::treemap.maps.size(); ++i) { + lcf::rpg::MapInfo& map = lcf::Data::treemap.maps[i]; + + if (map.ID == GetMapId()) { + for (const auto& enc : map.encounters) { + if (is_acceptable(enc.troop_id)) { + out.push_back(enc.troop_id); + } + } + } else if (map.parent_map == GetMapId() && map.type == lcf::rpg::TreeMap::MapType_area) { + // Area + Rect area_rect(map.area_rect.l, map.area_rect.t, map.area_rect.r - map.area_rect.l, map.area_rect.b - map.area_rect.t); + Rect player_rect(x, y, 1, 1); + + if (!player_rect.IsOutOfBounds(area_rect)) { + for (const lcf::rpg::Encounter& enc : map.encounters) { + if (is_acceptable(enc.troop_id)) { + out.push_back(enc.troop_id); + } + } + } + } + } + + return out; +} + +static void OnEncounterEnd(BattleResult result) { + if (result != BattleResult::Defeat) { + return; + } + + if (!Game_Battle::HasDeathHandler()) { + Scene::Push(std::make_shared()); + return; + } + + //2k3 death handler + + auto* ce = lcf::ReaderUtil::GetElement(common_events, Game_Battle::GetDeathHandlerCommonEvent()); + if (ce) { + auto& interp = Game_Map::GetInterpreter(); + interp.Push(ce); + } + + auto tt = Game_Battle::GetDeathHandlerTeleport(); + if (tt.IsActive()) { + Main_Data::game_player->ReserveTeleport(tt.GetMapId(), tt.GetX(), tt.GetY(), tt.GetDirection(), tt.GetType()); + } +} + +bool Game_Map::PrepareEncounter(BattleArgs& args) { + int x = Main_Data::game_player->GetX(); + int y = Main_Data::game_player->GetY(); + + std::vector encounters = GetEncountersAt(x, y); + + if (encounters.empty()) { + // No enemies on this map :( + return false; + } + + args.troop_id = encounters[Rand::GetRandomNumber(0, encounters.size() - 1)]; + + if (RuntimePatches::EncounterRandomnessAlert::HandleEncounter(args.troop_id)) { + //Cancel the battle setup + return false; + } + + if (Feature::HasRpg2kBattleSystem()) { + if (Rand::ChanceOf(1, 32)) { + args.first_strike = true; + } + } else { + const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, GetTerrainTag(x, y)); + if (!terrain) { + Output::Warning("PrepareEncounter: Invalid terrain at ({}, {})", x, y); + } else { + if (terrain->special_flags.back_party && Rand::PercentChance(terrain->special_back_party)) { + args.condition = lcf::rpg::System::BattleCondition_initiative; + } else if (terrain->special_flags.back_enemies && Rand::PercentChance(terrain->special_back_enemies)) { + args.condition = lcf::rpg::System::BattleCondition_back; + } else if (terrain->special_flags.lateral_party && Rand::PercentChance(terrain->special_lateral_party)) { + args.condition = lcf::rpg::System::BattleCondition_surround; + } else if (terrain->special_flags.lateral_enemies && Rand::PercentChance(terrain->special_lateral_enemies)) { + args.condition = lcf::rpg::System::BattleCondition_pincers; + } + } + } + + SetupBattle(args); + args.on_battle_end = OnEncounterEnd; + args.allow_escape = true; + + return true; +} + +void Game_Map::SetupBattle(BattleArgs& args) { + int x = Main_Data::game_player->GetX(); + int y = Main_Data::game_player->GetY(); + + args.terrain_id = GetTerrainTag(x, y); + + const auto* current_info = &GetMapInfo(); + while (current_info->background_type == 0 && GetParentMapInfo(*current_info).ID != current_info->ID) { + current_info = &GetParentMapInfo(*current_info); + } + + if (current_info->background_type == 2) { + args.background = ToString(current_info->background_name); + } +} + +std::vector& Game_Map::GetMapDataDown() { + return map->lower_layer; +} + +std::vector& Game_Map::GetMapDataUp() { + return map->upper_layer; +} + +int Game_Map::GetOriginalChipset() { + return map != nullptr ? map->chipset_id : 0; +} + +int Game_Map::GetChipset() { + return chipset != nullptr ? chipset->ID : 0; +} + +std::string_view Game_Map::GetChipsetName() { + return chipset != nullptr + ? std::string_view(chipset->chipset_name) + : std::string_view(""); +} + +int Game_Map::GetPositionX() { + return map_info.position_x; +} + +int Game_Map::GetDisplayX() { + return map_info.position_x + Main_Data::game_screen->GetShakeOffsetX() * 16; +} + +void Game_Map::SetPositionX(int x, bool reset_panorama) { + const int map_width = GetTilesX() * SCREEN_TILE_SIZE; + if (LoopHorizontal()) { + x = Utils::PositiveModulo(x, map_width); + + // If the map is too small to fit in the screen, add an offset corresponding to the black border's size + if (Player::game_config.fake_resolution.Get()) { + int map_width_in_pixels = Game_Map::GetTilesX() * TILE_SIZE; + if (map_width_in_pixels < Player::screen_width) { + x += ((Player::screen_width - map_width_in_pixels) / 2 / TILE_SIZE) * SCREEN_TILE_SIZE; + } + } + } else { + // Do not use std::clamp here. When the map is smaller than the screen the + // upper bound is smaller than the lower bound making the function fail. + x = std::max(0, std::min(map_width - screen_width, x)); + } + map_info.position_x = x; + if (reset_panorama) { + Parallax::SetPositionX(map_info.position_x); + Parallax::ResetPositionX(); + } +} + +int Game_Map::GetPositionY() { + return map_info.position_y; +} + +int Game_Map::GetDisplayY() { + return map_info.position_y + Main_Data::game_screen->GetShakeOffsetY() * 16; +} + +void Game_Map::SetPositionY(int y, bool reset_panorama) { + const int map_height = GetTilesY() * SCREEN_TILE_SIZE; + if (LoopVertical()) { + y = Utils::PositiveModulo(y, map_height); + + // If the map is too small to fit in the screen, add an offset corresponding to the black border's size + if (Player::game_config.fake_resolution.Get()) { + int map_height_in_pixels = Game_Map::GetTilesY() * TILE_SIZE; + if (map_height_in_pixels < Player::screen_height) { + y += ((Player::screen_height - map_height_in_pixels) / 2 / TILE_SIZE) * SCREEN_TILE_SIZE; + } + } + } else { + // Do not use std::clamp here. When the map is smaller than the screen the + // upper bound is smaller than the lower bound making the function fail. + y = std::max(0, std::min(map_height - screen_height, y)); + } + map_info.position_y = y; + if (reset_panorama) { + Parallax::SetPositionY(map_info.position_y); + Parallax::ResetPositionY(); + } +} + +bool Game_Map::GetNeedRefresh() { + int anti_lag_switch = Player::game_config.patch_anti_lag_switch.Get(); + if (anti_lag_switch > 0 && Main_Data::game_switches->Get(anti_lag_switch)) { + return false; + } + + return need_refresh; +} + +void Game_Map::SetNeedRefresh(bool refresh) { + need_refresh = refresh; +} + +void Game_Map::SetNeedRefreshForSwitchChange(int switch_id) { + if (need_refresh) + return; + if (map_cache->GetNeedRefresh(switch_id)) + SetNeedRefresh(true); +} + +void Game_Map::SetNeedRefreshForVarChange(int var_id) { + if (need_refresh) + return; + if (map_cache->GetNeedRefresh(var_id)) + SetNeedRefresh(true); +} + +void Game_Map::SetNeedRefreshForSwitchChange(std::initializer_list switch_ids) { + for (auto switch_id: switch_ids) { + SetNeedRefreshForSwitchChange(switch_id); + } +} + +void Game_Map::SetNeedRefreshForVarChange(std::initializer_list var_ids) { + for (auto var_id: var_ids) { + SetNeedRefreshForVarChange(var_id); + } +} + +std::vector& Game_Map::GetPassagesDown() { + return passages_down; +} + +std::vector& Game_Map::GetPassagesUp() { + return passages_up; +} + +int Game_Map::GetAnimationType() { + return animation_type; +} + +int Game_Map::GetAnimationSpeed() { + return (animation_fast ? 12 : 24); +} + +std::vector& Game_Map::GetEvents() { + return events; +} + +int Game_Map::GetHighestEventId() { + int id = 0; + for (auto& ev: events) { + id = std::max(id, ev.GetId()); + } + return id; +} + +Game_Event* Game_Map::GetEvent(int event_id) { + auto it = std::find_if(events.begin(), events.end(), + [&event_id](Game_Event& ev) {return ev.GetId() == event_id;}); + return it == events.end() ? nullptr : &(*it); +} + +std::vector& Game_Map::GetCommonEvents() { + return common_events; +} + +std::string_view Game_Map::GetMapName(int id) { + for (unsigned int i = 0; i < lcf::Data::treemap.maps.size(); ++i) { + if (lcf::Data::treemap.maps[i].ID == id) { + return lcf::Data::treemap.maps[i].name; + } + } + // nothing found + return {}; +} + +void Game_Map::SetChipset(int id) { + if (id == 0) { + // This emulates RPG_RT behavior, where chipset id == 0 means use the default map chipset. + id = GetOriginalChipset(); + } + map_info.chipset_id = id; + + if (!ReloadChipset()) { + Output::Warning("SetChipset: Invalid chipset ID {}", map_info.chipset_id); + } else { + passages_down = chipset->passable_data_lower; + passages_up = chipset->passable_data_upper; + animation_type = chipset->animation_type; + animation_fast = chipset->animation_speed != 0; + } + + if (passages_down.size() < 162) + passages_down.resize(162, (unsigned char) 0x0F); + if (passages_up.size() < 144) + passages_up.resize(144, (unsigned char) 0x0F); +} + +bool Game_Map::ReloadChipset() { + chipset = lcf::ReaderUtil::GetElement(lcf::Data::chipsets, map_info.chipset_id); + if (!chipset) { + return false; + } + return true; +} + +void Game_Map::OnTranslationChanged() { + ReloadChipset(); + // Marks common events for reload on map change + // This is not save to do while they are executing + translation_changed = true; +} + +Game_Vehicle* Game_Map::GetVehicle(Game_Vehicle::Type which) { + if (which == Game_Vehicle::Boat || + which == Game_Vehicle::Ship || + which == Game_Vehicle::Airship) { + return &vehicles[which - 1]; + } + + return nullptr; +} + +bool Game_Map::IsAnyEventStarting() { + for (Game_Event& ev : events) + if (ev.IsWaitingForegroundExecution() && !ev.GetList().empty() && ev.IsActive()) + return true; + + for (Game_CommonEvent& ev : common_events) + if (ev.IsWaitingForegroundExecution()) + return true; + + return false; +} + +bool Game_Map::IsAnyMovePending() { + auto check = [](auto& ev) { + return ev.IsMoveRouteOverwritten() && !ev.IsMoveRouteFinished(); + }; + const auto map_id = GetMapId(); + if (check(*Main_Data::game_player)) { + return true; + } + for (auto& vh: vehicles) { + if (vh.GetMapId() == map_id && check(vh)) { + return true; + } + } + for (auto& ev: events) { + if (check(ev)) { + return true; + } + } + + return false; +} + +void Game_Map::RemoveAllPendingMoves() { + const auto map_id = GetMapId(); + Main_Data::game_player->CancelMoveRoute(); + for (auto& vh: vehicles) { + if (vh.GetMapId() == map_id) { + vh.CancelMoveRoute(); + } + } + for (auto& ev: events) { + ev.CancelMoveRoute(); + } +} + +static int DoSubstitute(std::vector& tiles, int old_id, int new_id) { + int num_subst = 0; + for (size_t i = 0; i < tiles.size(); ++i) { + if (tiles[i] == old_id) { + tiles[i] = (uint8_t) new_id; + ++num_subst; + } + } + return num_subst; +} + +int Game_Map::SubstituteDown(int old_id, int new_id) { + return DoSubstitute(map_info.lower_tiles, old_id, new_id); +} + +int Game_Map::SubstituteUp(int old_id, int new_id) { + return DoSubstitute(map_info.upper_tiles, old_id, new_id); +} + +void Game_Map::ReplaceTileAt(int x, int y, int new_id, int layer) { + auto pos = x + y * map->width; + auto& layer_vec = layer >= 1 ? map->upper_layer : map->lower_layer; + layer_vec[pos] = static_cast(new_id); +} + +int Game_Map::GetTileIdAt(int x, int y, int layer, bool chip_id_or_index) { + if (x < 0 || x >= map->width || y < 0 || y >= map->height) { + return 0; // Return 0 for out-of-bounds coordinates + } + + auto pos = x + y * map->width; + auto& layer_vec = layer >= 1 ? map->upper_layer : map->lower_layer; + + int tile_output = chip_id_or_index ? layer_vec[pos] : ChipIdToIndex(layer_vec[pos]); + if (layer >= 1) tile_output -= BLOCK_F_INDEX; + + return tile_output; +} + +std::vector Game_Map::GetTilesIdAt(Rect coords, int layer, bool chip_id_or_index) { + std::vector tiles_collection; + for (int i = 0; i < coords.height; ++i) { + for (int j = 0; j < coords.width; ++j) { + tiles_collection.emplace_back(Game_Map::GetTileIdAt(coords.x + j, coords.y + i, layer, chip_id_or_index)); + } + } + return tiles_collection; +} + +std::string Game_Map::ConstructMapName(int map_id, bool is_easyrpg) { + std::stringstream ss; + ss << "Map" << std::setfill('0') << std::setw(4) << map_id; + if (is_easyrpg) { + return Player::fileext_map.MakeFilename(ss.str(), SUFFIX_EMU); + } else { + return Player::fileext_map.MakeFilename(ss.str(), SUFFIX_LMU); + } +} + +FileRequestAsync* Game_Map::RequestMap(int map_id) { +#ifdef __EMSCRIPTEN__ + Player::translation.RequestAndAddMap(map_id); +#endif + + auto* request = AsyncHandler::RequestFile(Game_Map::ConstructMapName(map_id, false)); + request->SetImportantFile(true); + return request; +} + +// MapEventCache +////////////////// +void Game_Map::Caching::MapEventCache::AddEvent(const lcf::rpg::Event& ev) { + auto id = ev.ID; + + if (std::find(event_ids.begin(), event_ids.end(), id) == event_ids.end()) { + event_ids.emplace_back(id); + } +} + +void Game_Map::Caching::MapEventCache::RemoveEvent(const lcf::rpg::Event& ev) { + auto id = ev.ID; + + auto it = std::find(event_ids.begin(), event_ids.end(), id); + + if (it != event_ids.end()) { + event_ids.erase(it); + } +} + +// Parallax +///////////// + +namespace { + int parallax_width; + int parallax_height; +} + +/* Helper function to get the current parallax parameters. If the default + * parallax for the current map was overridden by a "Change Parallax BG" + * command, the result is filled out from those values in the SaveMapInfo. + * Otherwise, the result is filled out from the default for the current map. + */ +static Game_Map::Parallax::Params GetParallaxParams() { + Game_Map::Parallax::Params params = {}; + + if (!map_info.parallax_name.empty()) { + params.name = map_info.parallax_name; + params.scroll_horz = map_info.parallax_horz; + params.scroll_horz_auto = map_info.parallax_horz_auto; + params.scroll_horz_speed = map_info.parallax_horz_speed; + params.scroll_vert = map_info.parallax_vert; + params.scroll_vert_auto = map_info.parallax_vert_auto; + params.scroll_vert_speed = map_info.parallax_vert_speed; + } else if (map->parallax_flag) { + // Default case when map parallax hasn't been overwritten. + params.name = ToString(map->parallax_name); + params.scroll_horz = map->parallax_loop_x; + params.scroll_horz_auto = map->parallax_auto_loop_x; + params.scroll_horz_speed = map->parallax_sx; + params.scroll_vert = map->parallax_loop_y; + params.scroll_vert_auto = map->parallax_auto_loop_y; + params.scroll_vert_speed = map->parallax_sy; + } else { + // No BG; use default-constructed Param + } + + return params; +} + +std::string Game_Map::Parallax::GetName() { + return GetParallaxParams().name; +} + +int Game_Map::Parallax::GetX() { + return (-panorama.pan_x / TILE_SIZE) / 2; +} + +int Game_Map::Parallax::GetY() { + return (-panorama.pan_y / TILE_SIZE) / 2; +} + +void Game_Map::Parallax::Initialize(int width, int height) { + parallax_width = width; + parallax_height = height; + + if (panorama_on_map_init) { + SetPositionX(map_info.position_x); + SetPositionY(map_info.position_y); + } + + if (reset_panorama_x_on_next_init) { + ResetPositionX(); + } + if (reset_panorama_y_on_next_init) { + ResetPositionY(); + } + + if (Player::IsRPG2k() && !panorama_on_map_init) { + SetPositionX(panorama.pan_x); + SetPositionY(panorama.pan_y); + } + + panorama_on_map_init = false; +} + +void Game_Map::Parallax::AddPositionX(int off_x) { + SetPositionX(panorama.pan_x + off_x); +} + +void Game_Map::Parallax::AddPositionY(int off_y) { + SetPositionY(panorama.pan_y + off_y); +} + +void Game_Map::Parallax::SetPositionX(int x) { + // FIXME: Fixes a crash with ChangeBG commands in events, but not correct. + // Real fix TBD + if (parallax_width) { + const int w = parallax_width * TILE_SIZE * 2; + panorama.pan_x = (x + w) % w; + } +} + +void Game_Map::Parallax::SetPositionY(int y) { + // FIXME: Fixes a crash with ChangeBG commands in events, but not correct. + // Real fix TBD + if (parallax_height) { + const int h = parallax_height * TILE_SIZE * 2; + panorama.pan_y = (y + h) % h; + } +} + +void Game_Map::Parallax::ResetPositionX() { + Params params = GetParallaxParams(); + + if (params.name.empty()) { + return; + } + + if (!params.scroll_horz && !LoopHorizontal()) { + // What is the width of the panorama to display on screen? + int pan_screen_width = Player::screen_width; + if (Player::game_config.fake_resolution.Get()) { + int map_width = Game_Map::GetTilesX() * TILE_SIZE; + if (map_width < pan_screen_width) { + pan_screen_width = map_width; + } + } + + int tiles_per_screen = pan_screen_width / TILE_SIZE; + if (pan_screen_width % TILE_SIZE != 0) { + ++tiles_per_screen; + } + + if (GetTilesX() > tiles_per_screen && parallax_width > pan_screen_width) { + const int w = (GetTilesX() - tiles_per_screen) * TILE_SIZE; + const int ph = 2 * std::min(w, parallax_width - pan_screen_width) * map_info.position_x / w; + if (Player::IsRPG2k()) { + SetPositionX(ph); + } else { + // 2k3 does not do the (% parallax_width * TILE_SIZE * 2) here + panorama.pan_x = ph; + } + } else { + panorama.pan_x = 0; + } + } +} + +void Game_Map::Parallax::ResetPositionY() { + Params params = GetParallaxParams(); + + if (params.name.empty()) { + return; + } + + if (!params.scroll_vert && !Game_Map::LoopVertical()) { + // What is the height of the panorama to display on screen? + int pan_screen_height = Player::screen_height; + if (Player::game_config.fake_resolution.Get()) { + int map_height = Game_Map::GetTilesY() * TILE_SIZE; + if (map_height < pan_screen_height) { + pan_screen_height = map_height; + } + } + + int tiles_per_screen = pan_screen_height / TILE_SIZE; + if (pan_screen_height % TILE_SIZE != 0) { + ++tiles_per_screen; + } + + if (GetTilesY() > tiles_per_screen && parallax_height > pan_screen_height) { + const int h = (GetTilesY() - tiles_per_screen) * TILE_SIZE; + const int pv = 2 * std::min(h, parallax_height - pan_screen_height) * map_info.position_y / h; + SetPositionY(pv); + } else { + panorama.pan_y = 0; + } + } +} + +void Game_Map::Parallax::ScrollRight(int distance) { + if (!distance) { + return; + } + + Params params = GetParallaxParams(); + if (params.name.empty()) { + return; + } + + if (params.scroll_horz) { + AddPositionX(distance); + return; + } + + if (Game_Map::LoopHorizontal()) { + return; + } + + ResetPositionX(); +} + +void Game_Map::Parallax::ScrollDown(int distance) { + if (!distance) { + return; + } + + Params params = GetParallaxParams(); + if (params.name.empty()) { + return; + } + + if (params.scroll_vert) { + AddPositionY(distance); + return; + } + + if (Game_Map::LoopVertical()) { + return; + } + + ResetPositionY(); +} + +void Game_Map::Parallax::Update() { + Params params = GetParallaxParams(); + + if (params.name.empty()) + return; + + auto scroll_amt = [](int speed) { + return speed < 0 ? (1 << -speed) : -(1 << speed); + }; + + if (params.scroll_horz + && params.scroll_horz_auto + && params.scroll_horz_speed != 0) { + AddPositionX(scroll_amt(params.scroll_horz_speed)); + } + + if (params.scroll_vert + && params.scroll_vert_auto + && params.scroll_vert_speed != 0) { + if (parallax_height != 0) { + AddPositionY(scroll_amt(params.scroll_vert_speed)); + } + } +} + +void Game_Map::Parallax::ChangeBG(const Params& params) { + map_info.parallax_name = params.name; + map_info.parallax_horz = params.scroll_horz; + map_info.parallax_horz_auto = params.scroll_horz_auto; + map_info.parallax_horz_speed = params.scroll_horz_speed; + map_info.parallax_vert = params.scroll_vert; + map_info.parallax_vert_auto = params.scroll_vert_auto; + map_info.parallax_vert_speed = params.scroll_vert_speed; + + reset_panorama_x_on_next_init = !Game_Map::LoopHorizontal() && !map_info.parallax_horz; + reset_panorama_y_on_next_init = !Game_Map::LoopVertical() && !map_info.parallax_vert; + + Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get(); + if (!scene || !scene->spriteset) + return; + scene->spriteset->ParallaxUpdated(); +} + +void Game_Map::Parallax::ClearChangedBG() { + Params params {}; // default Param indicates no override + ChangeBG(params); +} diff --git a/src/game_player.cpp b/src/game_player.cpp index 26da0ba48a..b303621044 100644 --- a/src/game_player.cpp +++ b/src/game_player.cpp @@ -1,1159 +1,1181 @@ -/* - * This file is part of EasyRPG Player. - * - * EasyRPG Player is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * EasyRPG Player is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with EasyRPG Player. If not, see . - */ - -// Headers -#include "game_player.h" -#include "async_handler.h" -#include "game_actor.h" -#include "game_map.h" -#include "game_message.h" -#include "game_party.h" -#include "game_system.h" -#include "game_screen.h" -#include "game_pictures.h" -#include "input.h" -#include "main_data.h" -#include "options.h" -#include "player.h" -#include "util_macro.h" -#include "game_switches.h" -#include "output.h" -#include "rand.h" -#include "utils.h" -#include -#include -#include "scene_battle.h" -#include "scene_menu.h" -#include -#include -#include -#include "scene_gameover.h" - -Game_Player::Game_Player(): Game_PlayerBase(Player) -{ - SetDirection(lcf::rpg::EventPage::Direction_down); - SetMoveSpeed(4); - SetAnimationType(lcf::rpg::EventPage::AnimType_non_continuous); -} - -void Game_Player::SetSaveData(lcf::rpg::SavePartyLocation save) -{ - *data() = std::move(save); - - SanitizeData("Party"); - - // RPG_RT will always reset the hero graphic on loading a save, even if - // a move route changed the graphic. - ResetGraphic(); -} - -lcf::rpg::SavePartyLocation Game_Player::GetSaveData() const { - return *data(); -} - -Drawable::Z_t Game_Player::GetScreenZ(int x_offset, int y_offset) const { - // Player is always "same layer as hero". - // When the Player is on the same Y-coordinate as an event the Player is always rendered first. - // This is different to events where, when Y is the same, the highest X-coordinate is rendered first. - // To ensure this, fake a very high X-coordinate of 65535 (all bits set) - // See base function for full explanation of the bitmask - return Game_Character::GetScreenZ(x_offset, y_offset) | (0xFFFFu << 16u); -} - -void Game_Player::ReserveTeleport(int map_id, int x, int y, int direction, TeleportTarget::Type tt) { - teleport_target = TeleportTarget(map_id, x, y, direction, tt); - - FileRequestAsync* request = Game_Map::RequestMap(map_id); - request->Start(); -} - -void Game_Player::ReserveTeleport(const lcf::rpg::SaveTarget& target) { - const auto* target_map_info = &Game_Map::GetMapInfo(target.map_id); - - if (target_map_info->type == lcf::rpg::TreeMap::MapType_area) { - // Area: Obtain the map the area belongs to - target_map_info = &Game_Map::GetParentMapInfo(*target_map_info); - } - - ReserveTeleport(target_map_info->ID, target.map_x, target.map_y, Down, TeleportTarget::eSkillTeleport); - - if (target.switch_on) { - Main_Data::game_switches->Set(target.switch_id, true); - Game_Map::SetNeedRefresh(true); - } -} - -void Game_Player::PerformTeleport() { - assert(IsPendingTeleport()); - if (!IsPendingTeleport()) { - return; - } - - if (teleport_target.GetMapId() <= 0) { - Output::Error("Invalid Teleport map id! mapid={} x={} y={} d={}", teleport_target.GetMapId(), - teleport_target.GetX(), teleport_target.GetY(), teleport_target.GetDirection()); - } - - const auto map_changed = (GetMapId() != teleport_target.GetMapId()); - MoveTo(teleport_target.GetMapId(), teleport_target.GetX(), teleport_target.GetY()); - - - if (teleport_target.GetDirection() >= 0) { - SetDirection(teleport_target.GetDirection()); - UpdateFacing(); - } - - if (map_changed && teleport_target.GetType() != TeleportTarget::eAsyncQuickTeleport) { - Main_Data::game_screen->OnMapChange(); - Main_Data::game_pictures->OnMapChange(); - Game_Map::GetInterpreter().OnMapChange(); - } - - ResetTeleportTarget(); -} - -void Game_Player::MoveTo(int map_id, int x, int y) { - const auto map_changed = (GetMapId() != map_id); - - Game_Character::MoveTo(map_id, x, y); - SetTotalEncounterRate(0); - SetMenuCalling(false); - - auto* vehicle = GetVehicle(); - if (vehicle) { - // RPG_RT doesn't check the aboard flag for this one - vehicle->MoveTo(map_id, x, y); - } - - if (map_changed) { - // FIXME: Assert map pre-loaded in cache. - - // pan_state does not reset when you change maps. - data()->pan_speed = lcf::rpg::SavePartyLocation::kPanSpeedDefault; - data()->pan_finish_x = GetDefaultPanX(); - data()->pan_finish_y = GetDefaultPanY(); - data()->pan_current_x = GetDefaultPanX(); - data()->pan_current_y = GetDefaultPanY(); - maniac_pan_current_x = static_cast(GetDefaultPanX()); - maniac_pan_current_y = static_cast(GetDefaultPanY()); - - ResetAnimation(); - - auto map = Game_Map::LoadMapFile(GetMapId()); - - Game_Map::Setup(std::move(map)); - Game_Map::PlayBgm(); - - // This Fixes an RPG_RT bug where the jumping flag doesn't get reset - // if you change maps during a jump - SetJumping(false); - } else { - Game_Map::SetPositionX(GetSpriteX() - GetPanX()); - Game_Map::SetPositionY(GetSpriteY() - GetPanY()); - } - - ResetGraphic(); -} - -bool Game_Player::MakeWay(int from_x, int from_y, int to_x, int to_y) { - if (IsAboard()) { - return GetVehicle()->MakeWay(from_x, from_y, to_x, to_y); - } - - return Game_Character::MakeWay(from_x, from_y, to_x, to_y); -} - -void Game_Player::MoveRouteSetSpriteGraphic(std::string sprite_name, int index) { - auto* vh = GetVehicle(); - if (vh) { - vh->MoveRouteSetSpriteGraphic(std::move(sprite_name), index); - } else { - Game_Character::MoveRouteSetSpriteGraphic(std::move(sprite_name), index); - } -} - -void Game_Player::UpdateScroll(int amount, bool was_jumping) { - if (IsPanLocked()) { - return; - } - - const auto map_width = Game_Map::GetTilesX() * SCREEN_TILE_SIZE; - const auto map_height = Game_Map::GetTilesY() * SCREEN_TILE_SIZE; - - // When using FakeResolution mode, if the map is too small to fit the screen - // we need to calculate the black border offset, in case of a looping map for example - int screen_offset_x = 0; - if (Player::game_config.fake_resolution.Get()) { - int map_width_in_pixels = Game_Map::GetTilesX() * TILE_SIZE; - if (map_width_in_pixels < Player::screen_width) { - screen_offset_x = ((Player::screen_width - map_width_in_pixels) / 2 / TILE_SIZE) * SCREEN_TILE_SIZE; - } - } - int screen_offset_y = 0; - if (Player::game_config.fake_resolution.Get()) { - int map_height_in_pixels = Game_Map::GetTilesY() * TILE_SIZE; - if (map_height_in_pixels < Player::screen_height) { - screen_offset_y = ((Player::screen_height - map_height_in_pixels) / 2 / TILE_SIZE) * SCREEN_TILE_SIZE; - } - } - - auto dx = (GetX() * SCREEN_TILE_SIZE) - Game_Map::GetPositionX() - GetPanX() + screen_offset_x; - auto dy = (GetY() * SCREEN_TILE_SIZE) - Game_Map::GetPositionY() - GetPanY() + screen_offset_y; - - dx = Utils::PositiveModulo(dx + map_width / 2, map_width) - map_width / 2; - dy = Utils::PositiveModulo(dy + map_height / 2, map_height) - map_height / 2; - - // If sx or sy equals zero, no scrolling is needed in the corresponding direction - const auto sx = Utils::Signum(dx); - const auto sy = Utils::Signum(dy); - - if (was_jumping) { - const auto jdx = sx * std::abs(GetX() - GetBeginJumpX()); - const auto jdy = sy * std::abs(GetY() - GetBeginJumpY()); - - Game_Map::Scroll(amount * jdx, amount * jdy); - - if (!IsJumping()) { - // RPG does this to fix rounding errors? - const auto x = SCREEN_TILE_SIZE * Utils::RoundTo(Game_Map::GetPositionX() / static_cast(SCREEN_TILE_SIZE)); - const auto y = SCREEN_TILE_SIZE * Utils::RoundTo(Game_Map::GetPositionY() / static_cast(SCREEN_TILE_SIZE)); - - // RPG_RT does adjust map position, but not panorama! - Game_Map::SetPositionX(x, false); - Game_Map::SetPositionY(y, false); - } - return; - } - - int move_sx = 0; - int move_sy = 0; - const auto d = GetDirection(); - if (sy < 0 && (d == Up || d == UpRight || d == UpLeft)) { - move_sy = sy; - } - if (sy > 0 && (d == Down || d == DownRight || d == DownLeft)) { - move_sy = sy; - } - if (sx > 0 && (d == Right || d == UpRight || d == DownRight)) { - move_sx = sx; - } - if (sx < 0 && (d == Left || d == UpLeft || d == DownLeft)) { - move_sx = sx; - } - - Game_Map::Scroll(move_sx * amount, move_sy * amount); -} - -bool Game_Player::UpdateAirship() { - auto* vehicle = GetVehicle(); - - // RPG_RT doesn't check vehicle, but we have to as we don't have another way to fetch it. - // Also in vanilla RPG_RT it's impossible for the hero to fly without the airship. - if (vehicle && vehicle->IsFlying()) { - if (vehicle->AnimateAscentDescent()) { - if (!vehicle->IsFlying()) { - // If we landed, them disembark - Main_Data::game_player->SetFlying(vehicle->IsFlying()); - data()->aboard = false; - SetFacing(Down); - data()->vehicle = 0; - SetMoveSpeed(data()->preboard_move_speed); - - Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic()); - } - - return true; - } - } - return false; -} - -void Game_Player::UpdateNextMovementAction() { - if (UpdateAirship()) { - return; - } - - UpdateMoveRoute(data()->move_route_index, data()->move_route, true); - - if (Game_Map::GetInterpreter().IsRunning()) { - SetMenuCalling(false); - return; - } - - if(IsPaused() || IsMoveRouteOverwritten() || Game_Message::IsMessageActive()) { - return; - } - - if (IsEncounterCalling()) { - SetMenuCalling(false); - SetEncounterCalling(false); - - BattleArgs args; - if (Game_Map::PrepareEncounter(args)) { - Scene::instance->SetRequestedScene(Scene_Battle::Create(std::move(args))); - return; - } - } - - if (IsMenuCalling()) { - SetMenuCalling(false); - - ResetAnimation(); - Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); - Game_Map::GetInterpreter().RequestMainMenuScene(); - return; - } - - CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); - - if (Game_Map::IsAnyEventStarting()) { - return; - } - - int move_dir = -1; - switch (Input::dir4) { - case 2: move_dir = Down; break; - case 4: move_dir = Left; break; - case 6: move_dir = Right; break; - case 8: move_dir = Up; break; - } - - if (move_dir >= 0) { - SetThrough((Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) || data()->move_route_through); - Move(move_dir); - ResetThrough(); - - if (IsStopping()) { - int x1, x2, y1, y2; - const Game_Character* active_char = IsAboard() ? static_cast(GetVehicle()) : static_cast(this); - active_char->GetTileOffsets(x1, x2, y1, y2); - bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); - - if (!is_expanded) { - // --- BRANCH: ORIGINAL LOGIC --- - int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, false); - } else { - // --- BRANCH: EXTENDED BOUNDARIES --- - int dir = GetDirection(); - // Scan the leading edge of the expanded 2D block - for (int i = (dir == Up || dir == Down ? x1 : y1); i <= (dir == Up || dir == Down ? x2 : y2); ++i) { - int fx = GetX() + (dir == Up || dir == Down ? i : (dir == Left ? x1 - 1 : x2 + 1)); - int fy = GetY() + (dir == Left || dir == Right ? i : (dir == Up ? y1 - 1 : y2 + 1)); - CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, Game_Map::RoundX(fx), Game_Map::RoundY(fy), false); - } - } - } - } - - if (IsStopping()) { - if (Input::IsTriggered(Input::DECISION)) { - if (!GetOnOffVehicle()) { - CheckActionEvent(); - } - } - return; - } - - Main_Data::game_party->IncSteps(); - if (Main_Data::game_party->ApplyStateDamage()) { - Main_Data::game_screen->FlashMapStepDamage(); - } - UpdateEncounterSteps(); -} - -void Game_Player::UpdateMovement(int amount) { - const bool was_jumping = IsJumping(); - - Game_Character::UpdateMovement(amount); - - UpdateScroll(amount, was_jumping); - - if (!IsMoveRouteOverwritten() && IsStopping()) { - TriggerSet triggers = { lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision }; - CheckEventTriggerHere(triggers, false); - } -} - -void Game_Player::Update() { - Game_Character::Update(); - - if (IsStopping()) { - if (data()->boarding) { - // Boarding completed - data()->aboard = true; - data()->boarding = false; - // Note: RPG_RT ignores the lock_facing flag here! - SetFacing(Left); - - auto* vehicle = GetVehicle(); - SetMoveSpeed(vehicle->GetMoveSpeed()); - } - if (data()->unboarding) { - // Unboarding completed - data()->unboarding = false; - } - } - - auto* vehicle = GetVehicle(); - - if (IsAboard() && vehicle) { - vehicle->SyncWithRider(this); - } - - UpdatePan(); - - // ESC-Menu calling - if (Main_Data::game_system->GetAllowMenu() - && !Game_Message::IsMessageActive() - && !Game_Map::GetInterpreter().IsRunning()) - { - if (Input::IsTriggered(Input::CANCEL)) { - SetMenuCalling(true); - } - } -} - -bool Game_Player::CheckActionEvent() { - if (IsFlying()) { - return false; - } - - int x1, x2, y1, y2; - const Game_Character* active_char = IsAboard() ? static_cast(GetVehicle()) : static_cast(this); - active_char->GetTileOffsets(x1, x2, y1, y2); - bool player_is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); - - if (!player_is_expanded) { - // --- BRANCH: ORIGINAL LOGIC --- - bool result = false; - int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - - result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, true); - result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true); - - bool got_action = CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); - for (int i = 0; !got_action && i < 3; ++i) { - if (!Game_Map::IsCounter(front_x, front_y)) { - break; - } - - front_x = Game_Map::XwithDirection(front_x, GetDirection()); - front_y = Game_Map::YwithDirection(front_y, GetDirection()); - - got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); - } - return result || got_action; - } - - // --- BRANCH: EXTENDED BOUNDARIES --- - int dir = GetDirection(); - bool result = false; - - for (int i = (dir == Up || dir == Down ? x1 : y1); i <= (dir == Up || dir == Down ? x2 : y2); ++i) { - int fx = GetX() + (dir == Up || dir == Down ? i : (dir == Left ? x1 - 1 : x2 + 1)); - int fy = GetY() + (dir == Left || dir == Right ? i : (dir == Up ? y1 - 1 : y2 + 1)); - result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, Game_Map::RoundX(fx), Game_Map::RoundY(fy), true); - } - - result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true); - - bool got_action = false; - for (int i = (dir == Up || dir == Down ? x1 : y1); i <= (dir == Up || dir == Down ? x2 : y2); ++i) { - int fx = GetX() + (dir == Up || dir == Down ? i : (dir == Left ? x1 - 1 : x2 + 1)); - int fy = GetY() + (dir == Left || dir == Right ? i : (dir == Up ? y1 - 1 : y2 + 1)); - - int cur_fx = Game_Map::RoundX(fx); - int cur_fy = Game_Map::RoundY(fy); - - got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, cur_fx, cur_fy, true); - - for (int c = 0; !got_action && c < 3; ++c) { - if (!Game_Map::IsCounter(cur_fx, cur_fy)) { - break; - } - - cur_fx = Game_Map::RoundX(Game_Map::XwithDirection(cur_fx, dir)); - cur_fy = Game_Map::RoundY(Game_Map::YwithDirection(cur_fy, dir)); - got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, cur_fx, cur_fy, true); - } - } - - return result || got_action; -} - -bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key, bool face_player) { - if (InAirship()) { - return false; - } - - int x1, x2, y1, y2; - const Game_Character* active_char = IsAboard() ? static_cast(GetVehicle()) : static_cast(this); - active_char->GetTileOffsets(x1, x2, y1, y2); - - bool result = false; - - for (auto& ev : Game_Map::GetEvents()) { - auto trigger = ev.GetTrigger(); - if (!trigger || !ev.IsActive() || ev.GetLayer() == lcf::rpg::EventPage::Layers_same || !triggers[*trigger]) { - continue; - } - - // Check if any tile in the player/vehicle's expanded footprint overlaps the event's footprint - bool overlap = false; - for (int dy = y1; dy <= y2 && !overlap; ++dy) { - for (int dx = x1; dx <= x2 && !overlap; ++dx) { - if (ev.IsInPosition(Game_Map::RoundX(GetX() + dx), Game_Map::RoundY(GetY() + dy))) { - overlap = true; - } - } - } - - if (overlap) { - if (ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player)) { - result = true; - SetEncounterCalling(false); - } - } - } - return result; -} - -bool Game_Player::CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key, bool face_player) { - if (InAirship()) { - return false; - } - bool result = false; - - for (auto& ev : Game_Map::GetEvents()) { - auto trigger = ev.GetTrigger(); - if (trigger && ev.IsActive() - && ev.IsInPosition(x, y) - && ev.GetLayer() == lcf::rpg::EventPage::Layers_same - && triggers[*trigger]) - { - if (ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player)) { - result = true; - SetEncounterCalling(false); - } - } - } - return result; -} - -void Game_Player::ResetGraphic() { - - auto* actor = Main_Data::game_party->GetActor(0); - if (actor == nullptr) { - SetSpriteGraphic("", 0); - SetTransparency(0); - return; - } - - SetSpriteGraphic(ToString(actor->GetSpriteName()), actor->GetSpriteIndex()); - SetTransparency(actor->GetSpriteTransparency()); -} - -bool Game_Player::GetOnOffVehicle() { - if (IsDirectionDiagonal(GetDirection())) { - SetDirection(GetFacing()); - } - - return IsAboard() - ? GetOffVehicle() - : GetOnVehicle(); -} - -bool Game_Player::GetOnVehicle() { - assert(!IsDirectionDiagonal(GetDirection())); - assert(!IsAboard()); - - // 1. Handle Airship Boarding - auto* airship = Game_Map::GetVehicle(Game_Vehicle::Airship); - if (airship->IsInPosition(GetX(), GetY()) && IsStopping() && airship->IsStopping()) { - int x1, x2, y1, y2; - airship->GetTileOffsets(x1, x2, y1, y2); - bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); - - // Branch: Only run iterative centering if Airship is expanded - if (is_expanded) { - int tx = airship->GetX(); - int ty = airship->GetY(); - - while (GetX() != tx || GetY() != ty) { - int dx = tx - GetX(); - int dy = ty - GetY(); - - if (Game_Map::LoopHorizontal()) { - int w = Game_Map::GetTilesX(); - if (std::abs(dx) > w / 2) dx = (dx > 0) ? dx - w : dx + w; - } - if (Game_Map::LoopVertical()) { - int h = Game_Map::GetTilesY(); - if (std::abs(dy) > h / 2) dy = (dy > 0) ? dy - h : dy + h; - } - - int move_dir = -1; - if (dx != 0) move_dir = (dx > 0) ? Right : Left; - else if (dy != 0) move_dir = (dy > 0) ? Down : Up; - - if (move_dir == -1) break; - - SetThrough(true); - if (std::abs(dx) + std::abs(dy) == 1) { - Move(move_dir); - } else { - SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(move_dir))); - SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(move_dir))); - Game_Map::SetPositionX(GetSpriteX() - GetPanX()); - Game_Map::SetPositionY(GetSpriteY() - GetPanY()); - } - ResetThrough(); - } - } - - data()->vehicle = Game_Vehicle::Airship; - data()->aboard = true; - SetFacing(Left); - data()->preboard_move_speed = GetMoveSpeed(); - SetMoveSpeed(airship->GetMoveSpeed()); - airship->StartAscent(); - Main_Data::game_player->SetFlying(airship->IsFlying()); - Main_Data::game_system->SetBeforeVehicleMusic(Main_Data::game_system->GetCurrentBGM()); - Main_Data::game_system->BgmPlay(airship->GetBGM()); - return true; - } - - // 2. Handle Boat/Ship Boarding - const auto front_x = Game_Map::RoundX(Game_Map::XwithDirection(GetX(), GetDirection())); - const auto front_y = Game_Map::RoundY(Game_Map::YwithDirection(GetY(), GetDirection())); - - Game_Vehicle* vehicle = nullptr; - for (auto vid : { Game_Vehicle::Ship, Game_Vehicle::Boat }) { - auto* v = Game_Map::GetVehicle(vid); - if (v->IsInCurrentMap() && v->IsInPosition(front_x, front_y)) { - vehicle = v; - break; - } - } - - if (!vehicle || !Game_Map::CanEmbarkShip(*this, front_x, front_y)) { - return false; - } - - int x1, x2, y1, y2; - vehicle->GetTileOffsets(x1, x2, y1, y2); - bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); - - // Branch: Iterative pathing for large ships, standard one-step for small boats - if (is_expanded) { - int tx = vehicle->GetX(); - int ty = vehicle->GetY(); - - while (GetX() != tx || GetY() != ty) { - int dx = tx - GetX(); - int dy = ty - GetY(); - - if (Game_Map::LoopHorizontal()) { - int w = Game_Map::GetTilesX(); - if (std::abs(dx) > w / 2) dx = (dx > 0) ? dx - w : dx + w; - } - if (Game_Map::LoopVertical()) { - int h = Game_Map::GetTilesY(); - if (std::abs(dy) > h / 2) dy = (dy > 0) ? dy - h : dy + h; - } - - int move_dir = -1; - if (dx != 0) move_dir = (dx > 0) ? Right : Left; - else if (dy != 0) move_dir = (dy > 0) ? Down : Up; - - if (move_dir == -1) break; - - SetThrough(true); - if (std::abs(dx) + std::abs(dy) == 1) { - Move(move_dir); - } else { - SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(move_dir))); - SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(move_dir))); - } - ResetThrough(); - } - } else { - // Standard Logic - SetThrough(true); - Move(GetDirection()); - ResetThrough(); - } - - data()->vehicle = vehicle->GetVehicleType(); - data()->preboard_move_speed = GetMoveSpeed(); - data()->boarding = true; - Main_Data::game_system->SetBeforeVehicleMusic(Main_Data::game_system->GetCurrentBGM()); - Main_Data::game_system->BgmPlay(vehicle->GetBGM()); - return true; -} - -bool Game_Player::GetOffVehicle() { - assert(!IsDirectionDiagonal(GetDirection())); - assert(IsAboard()); - - auto* vehicle = GetVehicle(); - if (!vehicle) return false; - - if (InAirship()) { - if (vehicle->IsAscendingOrDescending()) return false; - SetFacing(Left); - vehicle->StartDescent(); - return true; - } - - int x1, x2, y1, y2; - vehicle->GetTileOffsets(x1, x2, y1, y2); - bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); - - int dir = GetDirection(); - - // Branch: Extended clearing logic for large hitboxes - if (is_expanded) { - int steps = 0; - if (dir == Up) steps = std::abs(y1) + 1; - else if (dir == Down) steps = std::abs(y2) + 1; - else if (dir == Left) steps = std::abs(x1) + 1; - else if (dir == Right) steps = std::abs(x2) + 1; - - int target_x = GetX(); - int target_y = GetY(); - if (dir == Up) target_y -= steps; - else if (dir == Down) target_y += steps; - else if (dir == Left) target_x -= steps; - else if (dir == Right) target_x += steps; - - target_x = Game_Map::RoundX(target_x); - target_y = Game_Map::RoundY(target_y); - - if (!Game_Map::CanDisembarkShip(*this, target_x, target_y)) return false; - - vehicle->SetDefaultDirection(); - data()->aboard = false; - SetMoveSpeed(data()->preboard_move_speed); - data()->unboarding = true; - - for (int i = 0; i < steps; ++i) { - SetThrough(true); - if (i < steps - 1) { - SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(dir))); - SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(dir))); - } else { - Move(dir); - } - ResetThrough(); - } - } else { - // Standard Logic - const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection()); - const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection()); - - if (!Game_Map::CanDisembarkShip(*this, front_x, front_y)) { - return false; - } - - vehicle->SetDefaultDirection(); - data()->aboard = false; - SetMoveSpeed(data()->preboard_move_speed); - data()->unboarding = true; - - SetThrough(true); - Move(GetDirection()); - ResetThrough(); - } - - data()->vehicle = 0; - Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic()); - return true; -} - -void Game_Player::ForceGetOffVehicle() { - if (!IsAboard()) return; - - auto* vehicle = GetVehicle(); - vehicle->ForceLand(); - vehicle->SetDefaultDirection(); - - int x1, x2, y1, y2; - vehicle->GetTileOffsets(x1, x2, y1, y2); - bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); - - // Branch: Only snap hero to lead edge if vehicle is expanded - if (is_expanded && !Game_Map::CanDisembarkShip(*this, GetX(), GetY())) { - int dir = GetDirection(); - int steps = 0; - if (dir == Up) steps = std::abs(y1); - else if (dir == Down) steps = std::abs(y2); - else if (dir == Left) steps = std::abs(x1); - else if (dir == Right) steps = std::abs(x2); - - for (int i = 0; i < steps; ++i) { - SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(dir))); - SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(dir))); - } - } - - data()->flying = false; - data()->aboard = false; - SetMoveSpeed(data()->preboard_move_speed); - data()->unboarding = true; - data()->vehicle = 0; - Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic()); -} - -bool Game_Player::InVehicle() const { - return data()->vehicle > 0; -} - -bool Game_Player::InAirship() const { - return data()->vehicle == Game_Vehicle::Airship; -} - -Game_Vehicle* Game_Player::GetVehicle() const { - return Game_Map::GetVehicle((Game_Vehicle::Type) data()->vehicle); -} - -bool Game_Player::Move(int dir) { - if (!IsStopping()) { - return true; - } - - Game_Character::Move(dir); - if (IsStopping()) { - return false; - } - - if (InAirship()) { - return true; - } - - int terrain_id = Game_Map::GetTerrainTag(GetX(), GetY()); - const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, terrain_id); - bool red_flash = false; - - if (terrain) { - if (terrain->damage != 0) { - for (auto hero : Main_Data::game_party->GetActors()) { - if (terrain->damage < 0 || !hero->PreventsTerrainDamage()) { - if (terrain->damage > 0) { - red_flash = true; - } - if (terrain->easyrpg_damage_in_percent) { - int value = std::max(1, std::abs(hero->GetMaxHp() * terrain->damage / 100)); - hero->ChangeHp((terrain->damage > 0 ? -value : value), terrain->easyrpg_damage_can_kill); - } else { - hero->ChangeHp(-terrain->damage, terrain->easyrpg_damage_can_kill); - } - } - } - if (terrain->damage > 0 && terrain->easyrpg_damage_can_kill) { - if (!Main_Data::game_party->IsAnyActive() && Main_Data::game_party->GetBattlerCount() > 0) { - Scene::instance->SetRequestedScene(std::make_shared()); - return true; - } - } - } - if ((!terrain->on_damage_se || red_flash) && Player::IsRPG2k3()) { - Main_Data::game_system->SePlay(terrain->footstep); - } - } else { - Output::Warning("Player BeginMove: Invalid terrain ID {} at ({}, {})", terrain_id, GetX(), GetY()); - } - - if (red_flash) { - Main_Data::game_screen->FlashMapStepDamage(); - } - - return true; -} - -bool Game_Player::IsAboard() const { - return data()->aboard; -} - -bool Game_Player::IsBoardingOrUnboarding() const { - return data()->boarding || data()->unboarding; -} - -void Game_Player::UpdateEncounterSteps() { - if (Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) { - return; - } - - if(IsFlying()) { - return; - } - - const auto encounter_steps = Game_Map::GetEncounterSteps(); - - if (encounter_steps <= 0) { - SetTotalEncounterRate(0); - return; - } - - int x = GetX(); - int y = GetY(); - - const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, Game_Map::GetTerrainTag(x,y)); - if (!terrain) { - Output::Warning("UpdateEncounterSteps: Invalid terrain at ({}, {})", x, y); - return; - } - - data()->total_encounter_rate += terrain->encounter_rate; - - struct Row { - int ratio; - float pmod; - }; - -#if 1 - static constexpr Row enc_table[] = { - { 0, 0.0625}, - { 20, 0.125 }, - { 40, 0.25 }, - { 60, 0.5 }, - { 100, 2.0 }, - { 140, 4.0 }, - { 160, 8.0 }, - { 180, 16.0 }, - { INT_MAX, 16.0 } - }; -#else - //Old versions of RM2k used this table. - //Left here for posterity. - static constexpr Row enc_table[] = { - { 0, 0.5 }, - { 20, 2.0 / 3.0 }, - { 50, 5.0 / 6.0 }, - { 100, 6.0 / 5.0 }, - { 200, 3.0 / 2.0 }, - { INT_MAX, 3.0 / 2.0 } - }; -#endif - const auto ratio = GetTotalEncounterRate() / encounter_steps; - - auto& idx = last_encounter_idx; - while (ratio >= enc_table[idx+1].ratio) { - ++idx; - } - const auto& row = enc_table[idx]; - - const auto pmod = row.pmod; - const auto p = (1.0f / float(encounter_steps)) * pmod * (float(terrain->encounter_rate) / 100.0f); - - if (!Rand::PercentChance(p)) { - return; - } - - SetTotalEncounterRate(0); - SetEncounterCalling(true); -} - -void Game_Player::SetTotalEncounterRate(int rate) { - last_encounter_idx = 0; - data()->total_encounter_rate = rate; -} - -int Game_Player::GetDefaultPanX() { - return static_cast(std::ceil(static_cast(Player::screen_width) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; -} - -int Game_Player::GetDefaultPanY() { - return static_cast(std::ceil(static_cast(Player::screen_height) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; -} - -void Game_Player::LockPan() { - data()->pan_state = lcf::rpg::SavePartyLocation::PanState_fixed; -} - -void Game_Player::UnlockPan() { - data()->pan_state = lcf::rpg::SavePartyLocation::PanState_follow; -} - -void Game_Player::StartPan(int direction, int distance, int speed) { - distance *= SCREEN_TILE_SIZE; - - if (direction == PanUp) { - int new_pan = data()->pan_finish_y + distance; - data()->pan_finish_y = new_pan; - } else if (direction == PanRight) { - int new_pan = data()->pan_finish_x - distance; - data()->pan_finish_x = new_pan; - } else if (direction == PanDown) { - int new_pan = data()->pan_finish_y - distance; - data()->pan_finish_y = new_pan; - } else if (direction == PanLeft) { - int new_pan = data()->pan_finish_x + distance; - data()->pan_finish_x = new_pan; - } - - data()->pan_speed = 2 << speed; - - if (Player::IsPatchManiac()) { - // Maniac uses separate horizontal/vertical pan for everything - data()->maniac_horizontal_pan_speed = data()->pan_speed; - data()->maniac_vertical_pan_speed = data()->pan_speed; - } -} - -void Game_Player::StartPixelPan(int h, int v, int speed, bool interpolated, bool centered, bool relative) { - if (!Player::IsPatchManiac()) { - return; - } - - h *= TILE_SIZE; - v *= TILE_SIZE; - - maniac_pan_current_x = static_cast(data()->pan_current_x); - maniac_pan_current_y = static_cast(data()->pan_current_y); - - int new_pan_x; - int new_pan_y; - - if (relative && centered) { - int screen_width = static_cast(std::ceil(static_cast(Player::screen_width) / 2)) * TILE_SIZE; - int screen_height = static_cast(std::ceil(static_cast(Player::screen_height) / 2)) * TILE_SIZE; - new_pan_x = data()->pan_finish_x - (h - screen_width) * 0.5; - new_pan_y = data()->pan_finish_y - (v - screen_height) * 0.5; - } else if (relative) { - new_pan_x = data()->pan_finish_x - h; - new_pan_y = data()->pan_finish_y - v; - } else if (centered) { - new_pan_x = GetSpriteX() + GetDefaultPanX() - h; - new_pan_y = GetSpriteY() + GetDefaultPanY() - v; - } else { - new_pan_x = GetSpriteX() - h; - new_pan_y = GetSpriteY() - v; - } - - double h_speed; - double v_speed; - - if (speed == 0) { - // Instant pan if speed is zero - h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)); - v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)); - } else if (interpolated) { - // Interpolate distance by number of frames - h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)) / (speed + 1); - v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)) / (speed + 1); - } else { - // Multiply speed by 0.001 - h_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); - v_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); - } - - data()->pan_finish_x = new_pan_x; - data()->pan_finish_y = new_pan_y; - data()->maniac_horizontal_pan_speed = h_speed; - data()->maniac_vertical_pan_speed = v_speed; -} - -void Game_Player::ResetPan(int speed) { - data()->pan_finish_x = GetDefaultPanX(); - data()->pan_finish_y = GetDefaultPanY(); - data()->pan_speed = 2 << speed; - - if (Player::IsPatchManiac()) { - // Maniac uses separate horizontal/vertical pan for everything - data()->maniac_horizontal_pan_speed = data()->pan_speed; - data()->maniac_vertical_pan_speed = data()->pan_speed; - } -} - -int Game_Player::GetPanWait() { - bool is_maniac = Player::IsPatchManiac(); - const auto distance = std::max( - std::abs(data()->pan_current_x - data()->pan_finish_x), - std::abs(data()->pan_current_y - data()->pan_finish_y)); - const auto speed = !is_maniac ? data()->pan_speed : static_cast(std::max( - std::abs(data()->maniac_horizontal_pan_speed), - std::abs(data()->maniac_vertical_pan_speed))); - assert(speed > 0); - return distance / speed + (distance % speed != 0); -} - -// Fixes compilation error with OpenOrbis toolchain where std::abs fails to cast to double on values where it should -#if __PS4__ -# define PS4_WORKAROUND (double) -#else -# define PS4_WORKAROUND -#endif -void Game_Player::UpdatePan() { - if (!IsPanActive()) - return; - - const int step = data()->pan_speed; - const int pan_remain_x = data()->pan_current_x - data()->pan_finish_x; - const int pan_remain_y = data()->pan_current_y - data()->pan_finish_y; - - int dx; - int dy; - - if (Player::IsPatchManiac()) { - const double step_x = data()->maniac_horizontal_pan_speed; - const double step_y = data()->maniac_vertical_pan_speed; - - // Maniac uses doubles for smoother screen scrolling - double dx2 = std::min(step_x, PS4_WORKAROUND std::abs(static_cast(pan_remain_x))); - double dy2 = std::min(step_y, PS4_WORKAROUND std::abs(static_cast(pan_remain_y))); - - dx2 = pan_remain_x >= 0 ? dx2 : -dx2; - dy2 = pan_remain_y >= 0 ? dy2 : -dy2; - - maniac_pan_current_x -= dx2; - maniac_pan_current_y -= dy2; - - // Depending on the position, floor or ceil the value - dx = Utils::RoundTo(std::abs(maniac_pan_current_x)) == std::ceil(std::abs(maniac_pan_current_x)) ? static_cast(std::floor(dx2)) : static_cast(std::ceil(dx2)); - dy = Utils::RoundTo(std::abs(maniac_pan_current_y)) == std::ceil(std::abs(maniac_pan_current_y)) ? static_cast(std::floor(dy2)) : static_cast(std::ceil(dy2)); - } else { - dx = std::min(step, std::abs(pan_remain_x)); - dy = std::min(step, std::abs(pan_remain_y)); - - dx = pan_remain_x >= 0 ? dx : -dx; - dy = pan_remain_y >= 0 ? dy : -dy; - } - - int screen_x = Game_Map::GetPositionX(); - int screen_y = Game_Map::GetPositionY(); - - Game_Map::AddScreenX(screen_x, dx); - Game_Map::AddScreenY(screen_y, dy); - - // If we hit the edge of the map before pan finishes. - if (dx == 0 && dy == 0) { - return; - } - - Game_Map::Scroll(dx, dy); - - data()->pan_current_x -= dx; - data()->pan_current_y -= dy; -} - -bool Game_Player::TriggerEventAt(int x, int y, bool triggered_by_decision_key, bool face_player) { - return CheckEventTriggerThere({ lcf::rpg::EventPage::Trigger_action }, x, y, triggered_by_decision_key, face_player); -} +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +// Headers +#include "game_player.h" +#include "async_handler.h" +#include "game_actor.h" +#include "game_map.h" +#include "game_message.h" +#include "game_party.h" +#include "game_system.h" +#include "game_screen.h" +#include "game_pictures.h" +#include "input.h" +#include "main_data.h" +#include "options.h" +#include "player.h" +#include "util_macro.h" +#include "game_switches.h" +#include "output.h" +#include "rand.h" +#include "utils.h" +#include +#include +#include "scene_battle.h" +#include "scene_menu.h" +#include +#include +#include +#include "scene_gameover.h" + +Game_Player::Game_Player(): Game_PlayerBase(Player) +{ + SetDirection(lcf::rpg::EventPage::Direction_down); + SetMoveSpeed(4); + SetAnimationType(lcf::rpg::EventPage::AnimType_non_continuous); +} + +void Game_Player::SetSaveData(lcf::rpg::SavePartyLocation save) +{ + *data() = std::move(save); + + SanitizeData("Party"); + + // RPG_RT will always reset the hero graphic on loading a save, even if + // a move route changed the graphic. + ResetGraphic(); +} + +lcf::rpg::SavePartyLocation Game_Player::GetSaveData() const { + return *data(); +} + +Drawable::Z_t Game_Player::GetScreenZ(int x_offset, int y_offset) const { + // Player is always "same layer as hero". + // When the Player is on the same Y-coordinate as an event the Player is always rendered first. + // This is different to events where, when Y is the same, the highest X-coordinate is rendered first. + // To ensure this, fake a very high X-coordinate of 65535 (all bits set) + // See base function for full explanation of the bitmask + return Game_Character::GetScreenZ(x_offset, y_offset) | (0xFFFFu << 16u); +} + +void Game_Player::ReserveTeleport(int map_id, int x, int y, int direction, TeleportTarget::Type tt) { + teleport_target = TeleportTarget(map_id, x, y, direction, tt); + + FileRequestAsync* request = Game_Map::RequestMap(map_id); + request->Start(); +} + +void Game_Player::ReserveTeleport(const lcf::rpg::SaveTarget& target) { + const auto* target_map_info = &Game_Map::GetMapInfo(target.map_id); + + if (target_map_info->type == lcf::rpg::TreeMap::MapType_area) { + // Area: Obtain the map the area belongs to + target_map_info = &Game_Map::GetParentMapInfo(*target_map_info); + } + + ReserveTeleport(target_map_info->ID, target.map_x, target.map_y, Down, TeleportTarget::eSkillTeleport); + + if (target.switch_on) { + Main_Data::game_switches->Set(target.switch_id, true); + Game_Map::SetNeedRefresh(true); + } +} + +void Game_Player::PerformTeleport() { + assert(IsPendingTeleport()); + if (!IsPendingTeleport()) { + return; + } + + if (teleport_target.GetMapId() <= 0) { + Output::Error("Invalid Teleport map id! mapid={} x={} y={} d={}", teleport_target.GetMapId(), + teleport_target.GetX(), teleport_target.GetY(), teleport_target.GetDirection()); + } + + const auto map_changed = (GetMapId() != teleport_target.GetMapId()); + MoveTo(teleport_target.GetMapId(), teleport_target.GetX(), teleport_target.GetY()); + + + if (teleport_target.GetDirection() >= 0) { + SetDirection(teleport_target.GetDirection()); + UpdateFacing(); + } + + if (map_changed && teleport_target.GetType() != TeleportTarget::eAsyncQuickTeleport) { + Main_Data::game_screen->OnMapChange(); + Main_Data::game_pictures->OnMapChange(); + Game_Map::GetInterpreter().OnMapChange(); + } + + ResetTeleportTarget(); +} + +void Game_Player::MoveTo(int map_id, int x, int y) { + const auto map_changed = (GetMapId() != map_id); + + Game_Character::MoveTo(map_id, x, y); + SetTotalEncounterRate(0); + SetMenuCalling(false); + + auto* vehicle = GetVehicle(); + if (vehicle) { + // RPG_RT doesn't check the aboard flag for this one + vehicle->MoveTo(map_id, x, y); + } + + if (map_changed) { + // FIXME: Assert map pre-loaded in cache. + + // pan_state does not reset when you change maps. + data()->pan_speed = lcf::rpg::SavePartyLocation::kPanSpeedDefault; + data()->pan_finish_x = GetDefaultPanX(); + data()->pan_finish_y = GetDefaultPanY(); + data()->pan_current_x = GetDefaultPanX(); + data()->pan_current_y = GetDefaultPanY(); + maniac_pan_current_x = static_cast(GetDefaultPanX()); + maniac_pan_current_y = static_cast(GetDefaultPanY()); + + ResetAnimation(); + + auto map = Game_Map::LoadMapFile(GetMapId()); + + Game_Map::Setup(std::move(map)); + Game_Map::PlayBgm(); + + // This Fixes an RPG_RT bug where the jumping flag doesn't get reset + // if you change maps during a jump + SetJumping(false); + } else { + Game_Map::SetPositionX(GetSpriteX() - GetPanX()); + Game_Map::SetPositionY(GetSpriteY() - GetPanY()); + } + + ResetGraphic(); +} + +bool Game_Player::MakeWay(int from_x, int from_y, int to_x, int to_y) { + if (IsAboard()) { + return GetVehicle()->MakeWay(from_x, from_y, to_x, to_y); + } + + return Game_Character::MakeWay(from_x, from_y, to_x, to_y); +} + +void Game_Player::MoveRouteSetSpriteGraphic(std::string sprite_name, int index) { + auto* vh = GetVehicle(); + if (vh) { + vh->MoveRouteSetSpriteGraphic(std::move(sprite_name), index); + } else { + Game_Character::MoveRouteSetSpriteGraphic(std::move(sprite_name), index); + } +} + +void Game_Player::UpdateScroll(int amount, bool was_jumping) { + if (IsPanLocked()) { + return; + } + + const auto map_width = Game_Map::GetTilesX() * SCREEN_TILE_SIZE; + const auto map_height = Game_Map::GetTilesY() * SCREEN_TILE_SIZE; + + // When using FakeResolution mode, if the map is too small to fit the screen + // we need to calculate the black border offset, in case of a looping map for example + int screen_offset_x = 0; + if (Player::game_config.fake_resolution.Get()) { + int map_width_in_pixels = Game_Map::GetTilesX() * TILE_SIZE; + if (map_width_in_pixels < Player::screen_width) { + screen_offset_x = ((Player::screen_width - map_width_in_pixels) / 2 / TILE_SIZE) * SCREEN_TILE_SIZE; + } + } + int screen_offset_y = 0; + if (Player::game_config.fake_resolution.Get()) { + int map_height_in_pixels = Game_Map::GetTilesY() * TILE_SIZE; + if (map_height_in_pixels < Player::screen_height) { + screen_offset_y = ((Player::screen_height - map_height_in_pixels) / 2 / TILE_SIZE) * SCREEN_TILE_SIZE; + } + } + + auto dx = (GetX() * SCREEN_TILE_SIZE) - Game_Map::GetPositionX() - GetPanX() + screen_offset_x; + auto dy = (GetY() * SCREEN_TILE_SIZE) - Game_Map::GetPositionY() - GetPanY() + screen_offset_y; + + dx = Utils::PositiveModulo(dx + map_width / 2, map_width) - map_width / 2; + dy = Utils::PositiveModulo(dy + map_height / 2, map_height) - map_height / 2; + + // If sx or sy equals zero, no scrolling is needed in the corresponding direction + const auto sx = Utils::Signum(dx); + const auto sy = Utils::Signum(dy); + + if (was_jumping) { + const auto jdx = sx * std::abs(GetX() - GetBeginJumpX()); + const auto jdy = sy * std::abs(GetY() - GetBeginJumpY()); + + Game_Map::Scroll(amount * jdx, amount * jdy); + + if (!IsJumping()) { + // RPG does this to fix rounding errors? + const auto x = SCREEN_TILE_SIZE * Utils::RoundTo(Game_Map::GetPositionX() / static_cast(SCREEN_TILE_SIZE)); + const auto y = SCREEN_TILE_SIZE * Utils::RoundTo(Game_Map::GetPositionY() / static_cast(SCREEN_TILE_SIZE)); + + // RPG_RT does adjust map position, but not panorama! + Game_Map::SetPositionX(x, false); + Game_Map::SetPositionY(y, false); + } + return; + } + + int move_sx = 0; + int move_sy = 0; + const auto d = GetDirection(); + if (sy < 0 && (d == Up || d == UpRight || d == UpLeft)) { + move_sy = sy; + } + if (sy > 0 && (d == Down || d == DownRight || d == DownLeft)) { + move_sy = sy; + } + if (sx > 0 && (d == Right || d == UpRight || d == DownRight)) { + move_sx = sx; + } + if (sx < 0 && (d == Left || d == UpLeft || d == DownLeft)) { + move_sx = sx; + } + + Game_Map::Scroll(move_sx * amount, move_sy * amount); +} + +bool Game_Player::UpdateAirship() { + auto* vehicle = GetVehicle(); + + // RPG_RT doesn't check vehicle, but we have to as we don't have another way to fetch it. + // Also in vanilla RPG_RT it's impossible for the hero to fly without the airship. + if (vehicle && vehicle->IsFlying()) { + if (vehicle->AnimateAscentDescent()) { + if (!vehicle->IsFlying()) { + // If we landed, them disembark + Main_Data::game_player->SetFlying(vehicle->IsFlying()); + data()->aboard = false; + SetFacing(Down); + data()->vehicle = 0; + SetMoveSpeed(data()->preboard_move_speed); + + Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic()); + } + + return true; + } + } + return false; +} + +void Game_Player::UpdateNextMovementAction() { + if (UpdateAirship()) { + return; + } + + UpdateMoveRoute(data()->move_route_index, data()->move_route, true); + + if (Game_Map::GetInterpreter().IsRunning()) { + SetMenuCalling(false); + return; + } + + if(IsPaused() || IsMoveRouteOverwritten() || Game_Message::IsMessageActive()) { + return; + } + + if (IsEncounterCalling()) { + SetMenuCalling(false); + SetEncounterCalling(false); + + BattleArgs args; + if (Game_Map::PrepareEncounter(args)) { + Scene::instance->SetRequestedScene(Scene_Battle::Create(std::move(args))); + return; + } + } + + if (IsMenuCalling()) { + SetMenuCalling(false); + + ResetAnimation(); + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); + Game_Map::GetInterpreter().RequestMainMenuScene(); + return; + } + + CheckEventTriggerHere({ lcf::rpg::EventPage::Trigger_collision }, false); + + if (Game_Map::IsAnyEventStarting()) { + return; + } + + int move_dir = -1; + switch (Input::dir4) { + case 2: + move_dir = Down; + break; + case 4: + move_dir = Left; + break; + case 6: + move_dir = Right; + break; + case 8: + move_dir = Up; + break; + } + if (move_dir >= 0) { + SetThrough((Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) || data()->move_route_through); + Move(move_dir); + ResetThrough(); + if (IsStopping()) { + int x1, x2, y1, y2; + const Game_Character* active_char = IsAboard() ? static_cast(GetVehicle()) : static_cast(this); + active_char->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + if (!is_expanded) { + // --- BRANCH: ORIGINAL LOGIC --- + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, false); + } else { + // --- BRANCH: EXTENDED BOUNDARIES --- + int dir = GetDirection(); + // Scan the leading edge of the expanded 2D block + for (int i = (dir == Up || dir == Down ? x1 : y1); i <= (dir == Up || dir == Down ? x2 : y2); ++i) { + int fx = GetX() + (dir == Up || dir == Down ? i : (dir == Left ? x1 - 1 : x2 + 1)); + int fy = GetY() + (dir == Left || dir == Right ? i : (dir == Up ? y1 - 1 : y2 + 1)); + CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, Game_Map::RoundX(fx), Game_Map::RoundY(fy), false); + } + } + } + } + + if (IsStopping()) { + if (Input::IsTriggered(Input::DECISION)) { + if (!GetOnOffVehicle()) { + CheckActionEvent(); + } + } + return; + } + + Main_Data::game_party->IncSteps(); + if (Main_Data::game_party->ApplyStateDamage()) { + Main_Data::game_screen->FlashMapStepDamage(); + } + UpdateEncounterSteps(); +} + +void Game_Player::UpdateMovement(int amount) { + const bool was_jumping = IsJumping(); + + Game_Character::UpdateMovement(amount); + + UpdateScroll(amount, was_jumping); + + if (!IsMoveRouteOverwritten() && IsStopping()) { + TriggerSet triggers = { lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision }; + CheckEventTriggerHere(triggers, false); + } +} + +void Game_Player::Update() { + Game_Character::Update(); + + if (IsStopping()) { + if (data()->boarding) { + // Boarding completed + data()->aboard = true; + data()->boarding = false; + // Note: RPG_RT ignores the lock_facing flag here! + SetFacing(Left); + + auto* vehicle = GetVehicle(); + SetMoveSpeed(vehicle->GetMoveSpeed()); + } + if (data()->unboarding) { + // Unboarding completed + data()->unboarding = false; + } + } + + auto* vehicle = GetVehicle(); + + if (IsAboard() && vehicle) { + vehicle->SyncWithRider(this); + } + + UpdatePan(); + + // ESC-Menu calling + if (Main_Data::game_system->GetAllowMenu() + && !Game_Message::IsMessageActive() + && !Game_Map::GetInterpreter().IsRunning()) + { + if (Input::IsTriggered(Input::CANCEL)) { + SetMenuCalling(true); + } + } +} + +bool Game_Player::CheckActionEvent() { + if (IsFlying()) { + return false; + } + + + int x1, x2, y1, y2; + const Game_Character* active_char = IsAboard() ? static_cast(GetVehicle()) : static_cast(this); + active_char->GetTileOffsets(x1, x2, y1, y2); + bool player_is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + if (!player_is_expanded) { + // --- BRANCH: ORIGINAL LOGIC --- + bool result = false; + int front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + int front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + + result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, front_x, front_y, true); + result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true); + + bool got_action = CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); + for (int i = 0; !got_action && i < 3; ++i) { + if (!Game_Map::IsCounter(front_x, front_y)) { + break; + } + + front_x = Game_Map::XwithDirection(front_x, GetDirection()); + front_y = Game_Map::YwithDirection(front_y, GetDirection()); + + got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, front_x, front_y, true); + } + return result || got_action; + } + + // --- BRANCH: EXTENDED BOUNDARIES --- + int dir = GetDirection(); + + bool result = false; + for (int i = (dir == Up || dir == Down ? x1 : y1); i <= (dir == Up || dir == Down ? x2 : y2); ++i) { + int fx = GetX() + (dir == Up || dir == Down ? i : (dir == Left ? x1 - 1 : x2 + 1)); + int fy = GetY() + (dir == Left || dir == Right ? i : (dir == Up ? y1 - 1 : y2 + 1)); + result |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_touched, lcf::rpg::EventPage::Trigger_collision}, Game_Map::RoundX(fx), Game_Map::RoundY(fy), true); + } + result |= CheckEventTriggerHere({lcf::rpg::EventPage::Trigger_action}, true); + + // Counter tile loop stops only if you talk to an action event. + bool got_action = false; + // RPG_RT allows maximum of 3 counter tiles + for (int i = (dir == Up || dir == Down ? x1 : y1); i <= (dir == Up || dir == Down ? x2 : y2); ++i) { + int fx = GetX() + (dir == Up || dir == Down ? i : (dir == Left ? x1 - 1 : x2 + 1)); + int fy = GetY() + (dir == Left || dir == Right ? i : (dir == Up ? y1 - 1 : y2 + 1)); + + int cur_fx = Game_Map::RoundX(fx); + int cur_fy = Game_Map::RoundY(fy); + + got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, cur_fx, cur_fy, true); + + for (int c = 0; !got_action && c < 3; ++c) { + if (!Game_Map::IsCounter(cur_fx, cur_fy)) { + break; + } + + cur_fx = Game_Map::RoundX(Game_Map::XwithDirection(cur_fx, dir)); + cur_fy = Game_Map::RoundY(Game_Map::YwithDirection(cur_fy, dir)); + got_action |= CheckEventTriggerThere({lcf::rpg::EventPage::Trigger_action}, cur_fx, cur_fy, true); + } + } + + return result || got_action; +} + +bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key, bool face_player) { + if (InAirship()) { + return false; + } + +int x1, x2, y1, y2; + const Game_Character* active_char = IsAboard() ? static_cast(GetVehicle()) : static_cast(this); + active_char->GetTileOffsets(x1, x2, y1, y2); + + bool result = false; + + for (auto& ev : Game_Map::GetEvents()) { + auto trigger = ev.GetTrigger(); + if (!trigger || !ev.IsActive() || ev.GetLayer() == lcf::rpg::EventPage::Layers_same || !triggers[*trigger]) { + continue; + } + + // Check if any tile in the player/vehicle's expanded footprint overlaps the event's footprint + bool overlap = false; + for (int dy = y1; dy <= y2 && !overlap; ++dy) { + for (int dx = x1; dx <= x2 && !overlap; ++dx) { + if (ev.IsInPosition(Game_Map::RoundX(GetX() + dx), Game_Map::RoundY(GetY() + dy))) { + overlap = true; + } + } + } + + if (overlap) { + if (ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player)) { + result = true; + SetEncounterCalling(false); + } + } + } + return result; +} + +bool Game_Player::CheckEventTriggerThere(TriggerSet triggers, int x, int y, bool triggered_by_decision_key, bool face_player) { + if (InAirship()) { + return false; + } + bool result = false; + + for (auto& ev : Game_Map::GetEvents()) { + auto trigger = ev.GetTrigger(); + if (trigger && ev.IsActive() + && ev.IsInPosition(x, y) + && ev.GetLayer() == lcf::rpg::EventPage::Layers_same + && triggers[*trigger]) + { + if (ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player)) { + result = true; + SetEncounterCalling(false); + } + } + } + return result; +} + +void Game_Player::ResetGraphic() { + + auto* actor = Main_Data::game_party->GetActor(0); + if (actor == nullptr) { + SetSpriteGraphic("", 0); + SetTransparency(0); + return; + } + + SetSpriteGraphic(ToString(actor->GetSpriteName()), actor->GetSpriteIndex()); + SetTransparency(actor->GetSpriteTransparency()); +} + +bool Game_Player::GetOnOffVehicle() { + if (IsDirectionDiagonal(GetDirection())) { + SetDirection(GetFacing()); + } + + return IsAboard() + ? GetOffVehicle() + : GetOnVehicle(); +} + +bool Game_Player::GetOnVehicle() { + assert(!IsDirectionDiagonal(GetDirection())); + assert(!IsAboard()); + + // 1. Handle Airship Boarding + auto* airship = Game_Map::GetVehicle(Game_Vehicle::Airship); + if (airship->IsInPosition(GetX(), GetY()) && IsStopping() && airship->IsStopping()) { + int x1, x2, y1, y2; + airship->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + // Branch: Only run iterative centering if Airship is expanded + if (is_expanded) { + int tx = airship->GetX(); + int ty = airship->GetY(); + + while (GetX() != tx || GetY() != ty) { + int dx = tx - GetX(); + int dy = ty - GetY(); + + if (Game_Map::LoopHorizontal()) { + int w = Game_Map::GetTilesX(); + if (std::abs(dx) > w / 2) dx = (dx > 0) ? dx - w : dx + w; + } + if (Game_Map::LoopVertical()) { + int h = Game_Map::GetTilesY(); + if (std::abs(dy) > h / 2) dy = (dy > 0) ? dy - h : dy + h; + } + + int move_dir = -1; + if (dx != 0) move_dir = (dx > 0) ? Right : Left; + else if (dy != 0) move_dir = (dy > 0) ? Down : Up; + + if (move_dir == -1) break; + + SetThrough(true); + if (std::abs(dx) + std::abs(dy) == 1) { + Move(move_dir); + } else { + SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(move_dir))); + SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(move_dir))); + Game_Map::SetPositionX(GetSpriteX() - GetPanX()); + Game_Map::SetPositionY(GetSpriteY() - GetPanY()); + } + ResetThrough(); + } + } + data()->vehicle = Game_Vehicle::Airship; + data()->aboard = true; + + // Note: RPG_RT ignores the lock_facing flag here! + SetFacing(Left); + + data()->preboard_move_speed = GetMoveSpeed(); + SetMoveSpeed(airship->GetMoveSpeed()); + airship->StartAscent(); + Main_Data::game_player->SetFlying(airship->IsFlying()); + Main_Data::game_system->SetBeforeVehicleMusic(Main_Data::game_system->GetCurrentBGM()); + Main_Data::game_system->BgmPlay(airship->GetBGM()); + return true; + } + + // 2. Handle Boat/Ship Boarding + const auto front_x = Game_Map::RoundX(Game_Map::XwithDirection(GetX(), GetDirection())); + const auto front_y = Game_Map::RoundY(Game_Map::YwithDirection(GetY(), GetDirection())); + + Game_Vehicle* vehicle = nullptr; + for (auto vid : { Game_Vehicle::Ship, Game_Vehicle::Boat }) { + auto* v = Game_Map::GetVehicle(vid); + if (v->IsInCurrentMap() && v->IsInPosition(front_x, front_y)) { + vehicle = v; + break; + } + } + + if (!vehicle || !Game_Map::CanEmbarkShip(*this, front_x, front_y)) { + return false; + } + + int x1, x2, y1, y2; + vehicle->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + // Branch: Iterative pathing for large ships, standard one-step for small boats + if (is_expanded) { + int tx = vehicle->GetX(); + int ty = vehicle->GetY(); + + while (GetX() != tx || GetY() != ty) { + int dx = tx - GetX(); + int dy = ty - GetY(); + + if (Game_Map::LoopHorizontal()) { + int w = Game_Map::GetTilesX(); + if (std::abs(dx) > w / 2) dx = (dx > 0) ? dx - w : dx + w; + } + if (Game_Map::LoopVertical()) { + int h = Game_Map::GetTilesY(); + if (std::abs(dy) > h / 2) dy = (dy > 0) ? dy - h : dy + h; + } + + int move_dir = -1; + if (dx != 0) move_dir = (dx > 0) ? Right : Left; + else if (dy != 0) move_dir = (dy > 0) ? Down : Up; + + if (move_dir == -1) break; + + SetThrough(true); + if (std::abs(dx) + std::abs(dy) == 1) { + Move(move_dir); + } else { + SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(move_dir))); + SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(move_dir))); + } + ResetThrough(); + } + } else { + // Standard Logic + + SetThrough(true); + Move(GetDirection()); + // FIXME: RPG_RT resets through to move_route_through || not visible? + ResetThrough(); + + } + + data()->vehicle = vehicle->GetVehicleType(); + data()->preboard_move_speed = GetMoveSpeed(); + data()->boarding = true; + Main_Data::game_system->SetBeforeVehicleMusic(Main_Data::game_system->GetCurrentBGM()); + Main_Data::game_system->BgmPlay(vehicle->GetBGM()); + return true; +} + +bool Game_Player::GetOffVehicle() { + assert(!IsDirectionDiagonal(GetDirection())); + assert(IsAboard()); + + auto* vehicle = GetVehicle(); + if (!vehicle) { + return false; + } + + if (InAirship()) { + if (vehicle->IsAscendingOrDescending()) { + return false; + } + + // Note: RPG_RT ignores the lock_facing flag here! + SetFacing(Left); + vehicle->StartDescent(); + return true; + } + + int x1, x2, y1, y2; + vehicle->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + int dir = GetDirection(); + + // Branch: Extended clearing logic for large hitboxes + if (is_expanded) { + int steps = 0; + if (dir == Up) steps = std::abs(y1) + 1; + else if (dir == Down) steps = std::abs(y2) + 1; + else if (dir == Left) steps = std::abs(x1) + 1; + else if (dir == Right) steps = std::abs(x2) + 1; + + int target_x = GetX(); + int target_y = GetY(); + if (dir == Up) target_y -= steps; + else if (dir == Down) target_y += steps; + else if (dir == Left) target_x -= steps; + else if (dir == Right) target_x += steps; + + target_x = Game_Map::RoundX(target_x); + target_y = Game_Map::RoundY(target_y); + + if (!Game_Map::CanDisembarkShip(*this, target_x, target_y)) return false; + + vehicle->SetDefaultDirection(); + data()->aboard = false; + SetMoveSpeed(data()->preboard_move_speed); + data()->unboarding = true; + + for (int i = 0; i < steps; ++i) { + SetThrough(true); + if (i < steps - 1) { + SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(dir))); + SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(dir))); + } else { + Move(dir); + } + ResetThrough(); + } + } else { + // Standard Logic + const auto front_x = Game_Map::XwithDirection(GetX(), GetDirection()); + const auto front_y = Game_Map::YwithDirection(GetY(), GetDirection()); + + if (!Game_Map::CanDisembarkShip(*this, front_x, front_y)) { + return false; + } + + vehicle->SetDefaultDirection(); + data()->aboard = false; + SetMoveSpeed(data()->preboard_move_speed); + data()->unboarding = true; + + SetThrough(true); + Move(GetDirection()); + ResetThrough(); + } + + data()->vehicle = 0; + Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic()); + + return true; +} + +void Game_Player::ForceGetOffVehicle() { + if (!IsAboard()) { + return; + } + + auto* vehicle = GetVehicle(); + vehicle->ForceLand(); + vehicle->SetDefaultDirection(); + + int x1, x2, y1, y2; + vehicle->GetTileOffsets(x1, x2, y1, y2); + bool is_expanded = (x1 != 0 || x2 != 0 || y1 != 0 || y2 != 0); + + // Branch: Only snap hero to lead edge if vehicle is expanded + if (is_expanded && !Game_Map::CanDisembarkShip(*this, GetX(), GetY())) { + int dir = GetDirection(); + int steps = 0; + if (dir == Up) steps = std::abs(y1); + else if (dir == Down) steps = std::abs(y2); + else if (dir == Left) steps = std::abs(x1); + else if (dir == Right) steps = std::abs(x2); + + for (int i = 0; i < steps; ++i) { + SetX(Game_Map::RoundX(GetX() + GetDxFromDirection(dir))); + SetY(Game_Map::RoundY(GetY() + GetDyFromDirection(dir))); + } + } + + data()->flying = false; + data()->aboard = false; + SetMoveSpeed(data()->preboard_move_speed); + data()->unboarding = true; + data()->vehicle = 0; + Main_Data::game_system->BgmPlay(Main_Data::game_system->GetBeforeVehicleMusic()); +} + +bool Game_Player::InVehicle() const { + return data()->vehicle > 0; +} + +bool Game_Player::InAirship() const { + return data()->vehicle == Game_Vehicle::Airship; +} + +Game_Vehicle* Game_Player::GetVehicle() const { + return Game_Map::GetVehicle((Game_Vehicle::Type) data()->vehicle); +} + +bool Game_Player::Move(int dir) { + if (!IsStopping()) { + return true; + } + + Game_Character::Move(dir); + if (IsStopping()) { + return false; + } + + if (InAirship()) { + return true; + } + + int terrain_id = Game_Map::GetTerrainTag(GetX(), GetY()); + const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, terrain_id); + bool red_flash = false; + + if (terrain) { + if (terrain->damage != 0) { + for (auto hero : Main_Data::game_party->GetActors()) { + if (terrain->damage < 0 || !hero->PreventsTerrainDamage()) { + if (terrain->damage > 0) { + red_flash = true; + } + if (terrain->easyrpg_damage_in_percent) { + int value = std::max(1, std::abs(hero->GetMaxHp() * terrain->damage / 100)); + hero->ChangeHp((terrain->damage > 0 ? -value : value), terrain->easyrpg_damage_can_kill); + } else { + hero->ChangeHp(-terrain->damage, terrain->easyrpg_damage_can_kill); + } + } + } + if (terrain->damage > 0 && terrain->easyrpg_damage_can_kill) { + if (!Main_Data::game_party->IsAnyActive() && Main_Data::game_party->GetBattlerCount() > 0) { + Scene::instance->SetRequestedScene(std::make_shared()); + return true; + } + } + } + if ((!terrain->on_damage_se || red_flash) && Player::IsRPG2k3()) { + Main_Data::game_system->SePlay(terrain->footstep); + } + } else { + Output::Warning("Player BeginMove: Invalid terrain ID {} at ({}, {})", terrain_id, GetX(), GetY()); + } + + if (red_flash) { + Main_Data::game_screen->FlashMapStepDamage(); + } + + return true; +} + +bool Game_Player::IsAboard() const { + return data()->aboard; +} + +bool Game_Player::IsBoardingOrUnboarding() const { + return data()->boarding || data()->unboarding; +} + +void Game_Player::UpdateEncounterSteps() { + if (Player::debug_flag && Input::IsPressed(Input::DEBUG_THROUGH)) { + return; + } + + if(IsFlying()) { + return; + } + + const auto encounter_steps = Game_Map::GetEncounterSteps(); + + if (encounter_steps <= 0) { + SetTotalEncounterRate(0); + return; + } + + int x = GetX(); + int y = GetY(); + + const auto* terrain = lcf::ReaderUtil::GetElement(lcf::Data::terrains, Game_Map::GetTerrainTag(x,y)); + if (!terrain) { + Output::Warning("UpdateEncounterSteps: Invalid terrain at ({}, {})", x, y); + return; + } + + data()->total_encounter_rate += terrain->encounter_rate; + + struct Row { + int ratio; + float pmod; + }; + +#if 1 + static constexpr Row enc_table[] = { + { 0, 0.0625}, + { 20, 0.125 }, + { 40, 0.25 }, + { 60, 0.5 }, + { 100, 2.0 }, + { 140, 4.0 }, + { 160, 8.0 }, + { 180, 16.0 }, + { INT_MAX, 16.0 } + }; +#else + //Old versions of RM2k used this table. + //Left here for posterity. + static constexpr Row enc_table[] = { + { 0, 0.5 }, + { 20, 2.0 / 3.0 }, + { 50, 5.0 / 6.0 }, + { 100, 6.0 / 5.0 }, + { 200, 3.0 / 2.0 }, + { INT_MAX, 3.0 / 2.0 } + }; +#endif + const auto ratio = GetTotalEncounterRate() / encounter_steps; + + auto& idx = last_encounter_idx; + while (ratio >= enc_table[idx+1].ratio) { + ++idx; + } + const auto& row = enc_table[idx]; + + const auto pmod = row.pmod; + const auto p = (1.0f / float(encounter_steps)) * pmod * (float(terrain->encounter_rate) / 100.0f); + + if (!Rand::PercentChance(p)) { + return; + } + + SetTotalEncounterRate(0); + SetEncounterCalling(true); +} + +void Game_Player::SetTotalEncounterRate(int rate) { + last_encounter_idx = 0; + data()->total_encounter_rate = rate; +} + +int Game_Player::GetDefaultPanX() { + return static_cast(std::ceil(static_cast(Player::screen_width) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; +} + +int Game_Player::GetDefaultPanY() { + return static_cast(std::ceil(static_cast(Player::screen_height) / TILE_SIZE / 2) - 1) * SCREEN_TILE_SIZE; +} + +void Game_Player::LockPan() { + data()->pan_state = lcf::rpg::SavePartyLocation::PanState_fixed; +} + +void Game_Player::UnlockPan() { + data()->pan_state = lcf::rpg::SavePartyLocation::PanState_follow; +} + +void Game_Player::StartPan(int direction, int distance, int speed) { + distance *= SCREEN_TILE_SIZE; + + if (direction == PanUp) { + int new_pan = data()->pan_finish_y + distance; + data()->pan_finish_y = new_pan; + } else if (direction == PanRight) { + int new_pan = data()->pan_finish_x - distance; + data()->pan_finish_x = new_pan; + } else if (direction == PanDown) { + int new_pan = data()->pan_finish_y - distance; + data()->pan_finish_y = new_pan; + } else if (direction == PanLeft) { + int new_pan = data()->pan_finish_x + distance; + data()->pan_finish_x = new_pan; + } + + data()->pan_speed = 2 << speed; + + if (Player::IsPatchManiac()) { + // Maniac uses separate horizontal/vertical pan for everything + data()->maniac_horizontal_pan_speed = data()->pan_speed; + data()->maniac_vertical_pan_speed = data()->pan_speed; + } +} + +void Game_Player::StartPixelPan(int h, int v, int speed, bool interpolated, bool centered, bool relative) { + if (!Player::IsPatchManiac()) { + return; + } + + h *= TILE_SIZE; + v *= TILE_SIZE; + + maniac_pan_current_x = static_cast(data()->pan_current_x); + maniac_pan_current_y = static_cast(data()->pan_current_y); + + int new_pan_x; + int new_pan_y; + + if (relative && centered) { + int screen_width = static_cast(std::ceil(static_cast(Player::screen_width) / 2)) * TILE_SIZE; + int screen_height = static_cast(std::ceil(static_cast(Player::screen_height) / 2)) * TILE_SIZE; + new_pan_x = data()->pan_finish_x - (h - screen_width) * 0.5; + new_pan_y = data()->pan_finish_y - (v - screen_height) * 0.5; + } else if (relative) { + new_pan_x = data()->pan_finish_x - h; + new_pan_y = data()->pan_finish_y - v; + } else if (centered) { + new_pan_x = GetSpriteX() + GetDefaultPanX() - h; + new_pan_y = GetSpriteY() + GetDefaultPanY() - v; + } else { + new_pan_x = GetSpriteX() - h; + new_pan_y = GetSpriteY() - v; + } + + double h_speed; + double v_speed; + + if (speed == 0) { + // Instant pan if speed is zero + h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)); + v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)); + } else if (interpolated) { + // Interpolate distance by number of frames + h_speed = std::abs((static_cast(new_pan_x) - maniac_pan_current_x)) / (speed + 1); + v_speed = std::abs((static_cast(new_pan_y) - maniac_pan_current_y)) / (speed + 1); + } else { + // Multiply speed by 0.001 + h_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); + v_speed = std::max(static_cast(speed * TILE_SIZE * 0.001), 1.0); + } + + data()->pan_finish_x = new_pan_x; + data()->pan_finish_y = new_pan_y; + data()->maniac_horizontal_pan_speed = h_speed; + data()->maniac_vertical_pan_speed = v_speed; +} + +void Game_Player::ResetPan(int speed) { + data()->pan_finish_x = GetDefaultPanX(); + data()->pan_finish_y = GetDefaultPanY(); + data()->pan_speed = 2 << speed; + + if (Player::IsPatchManiac()) { + // Maniac uses separate horizontal/vertical pan for everything + data()->maniac_horizontal_pan_speed = data()->pan_speed; + data()->maniac_vertical_pan_speed = data()->pan_speed; + } +} + +int Game_Player::GetPanWait() { + bool is_maniac = Player::IsPatchManiac(); + const auto distance = std::max( + std::abs(data()->pan_current_x - data()->pan_finish_x), + std::abs(data()->pan_current_y - data()->pan_finish_y)); + const auto speed = !is_maniac ? data()->pan_speed : static_cast(std::max( + std::abs(data()->maniac_horizontal_pan_speed), + std::abs(data()->maniac_vertical_pan_speed))); + assert(speed > 0); + return distance / speed + (distance % speed != 0); +} + +// Fixes compilation error with OpenOrbis toolchain where std::abs fails to cast to double on values where it should +#if __PS4__ +# define PS4_WORKAROUND (double) +#else +# define PS4_WORKAROUND +#endif +void Game_Player::UpdatePan() { + if (!IsPanActive()) + return; + + const int step = data()->pan_speed; + const int pan_remain_x = data()->pan_current_x - data()->pan_finish_x; + const int pan_remain_y = data()->pan_current_y - data()->pan_finish_y; + + int dx; + int dy; + + if (Player::IsPatchManiac()) { + const double step_x = data()->maniac_horizontal_pan_speed; + const double step_y = data()->maniac_vertical_pan_speed; + + // Maniac uses doubles for smoother screen scrolling + double dx2 = std::min(step_x, PS4_WORKAROUND std::abs(static_cast(pan_remain_x))); + double dy2 = std::min(step_y, PS4_WORKAROUND std::abs(static_cast(pan_remain_y))); + + dx2 = pan_remain_x >= 0 ? dx2 : -dx2; + dy2 = pan_remain_y >= 0 ? dy2 : -dy2; + + maniac_pan_current_x -= dx2; + maniac_pan_current_y -= dy2; + + // Depending on the position, floor or ceil the value + dx = Utils::RoundTo(std::abs(maniac_pan_current_x)) == std::ceil(std::abs(maniac_pan_current_x)) ? static_cast(std::floor(dx2)) : static_cast(std::ceil(dx2)); + dy = Utils::RoundTo(std::abs(maniac_pan_current_y)) == std::ceil(std::abs(maniac_pan_current_y)) ? static_cast(std::floor(dy2)) : static_cast(std::ceil(dy2)); + } else { + dx = std::min(step, std::abs(pan_remain_x)); + dy = std::min(step, std::abs(pan_remain_y)); + + dx = pan_remain_x >= 0 ? dx : -dx; + dy = pan_remain_y >= 0 ? dy : -dy; + } + + int screen_x = Game_Map::GetPositionX(); + int screen_y = Game_Map::GetPositionY(); + + Game_Map::AddScreenX(screen_x, dx); + Game_Map::AddScreenY(screen_y, dy); + + // If we hit the edge of the map before pan finishes. + if (dx == 0 && dy == 0) { + return; + } + + Game_Map::Scroll(dx, dy); + + data()->pan_current_x -= dx; + data()->pan_current_y -= dy; +} + +bool Game_Player::TriggerEventAt(int x, int y, bool triggered_by_decision_key, bool face_player) { + return CheckEventTriggerThere({ lcf::rpg::EventPage::Trigger_action }, x, y, triggered_by_decision_key, face_player); +}