From 47b197a54376247e802475d3ee5182081c330b9a Mon Sep 17 00:00:00 2001 From: LizardKing777 Date: Sun, 19 Jul 2026 19:40:06 -0600 Subject: [PATCH] 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);