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..1c270bf17c 100644
--- a/src/game_event.cpp
+++ b/src/game_event.cpp
@@ -359,34 +359,79 @@ 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);
+ // 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 b4acc5b50b..97b24296e0 100644
--- a/src/game_map.cpp
+++ b/src/game_map.cpp
@@ -1,2311 +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
- )
-{
- // 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;
-
- // 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
- );
- }
- };
-
- 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.
- 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;
- }
- }
- }
- 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 | Passable::Up | Passable::Left | Passable::Right;
- }
-
- return IsPassableTile(
- &self, bit, to_x, to_y, check_events_and_vehicles, 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 e52f73f46c..b303621044 100644
--- a/src/game_player.cpp
+++ b/src/game_player.cpp
@@ -1,956 +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 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);
- }
- }
-
- 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;
- }
-
- 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);
-
- // 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;
- }
-
- 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;
-}
-
-bool Game_Player::CheckEventTriggerHere(TriggerSet triggers, bool triggered_by_decision_key, bool face_player) {
- if (InAirship()) {
- return false;
- }
-
- 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);
- }
- }
- 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()) {
- const auto trigger = ev.GetTrigger();
- if (ev.IsActive()
- && ev.GetX() == x
- && ev.GetY() == y
- && ev.GetLayer() == lcf::rpg::EventPage::Layers_same
- && trigger.has_value()
- && triggers[*trigger]) {
- SetEncounterCalling(false);
- result |= ev.ScheduleForegroundExecution(triggered_by_decision_key, face_player);
- }
- }
- 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());
-
- auto* vehicle = Game_Map::GetVehicle(Game_Vehicle::Airship);
-
- 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());
-
- 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;
- }
- }
-
- if (!Game_Map::CanEmbarkShip(*this, front_x, front_y)) {
- return false;
- }
-
- 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;
- }
-
- 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();
-
- 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);
+}