diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 3e4842ba67..4efcaa2903 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -812,6 +812,8 @@ bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CmdSetup<&Game_Interpreter::CommandManiacGetGameInfo, 8>(com); case Cmd::EasyRpg_SetInterpreterFlag: return CmdSetup<&Game_Interpreter::CommandEasyRpgSetInterpreterFlag, 2>(com); + case static_cast(3032): // Maniac_Zoom + return CmdSetup<&Game_Interpreter_Map::CommandManiacZoom, 7>(com); case Cmd::EasyRpg_ProcessJson: return CmdSetup<&Game_Interpreter::CommandEasyRpgProcessJson, 8>(com); case Cmd::EasyRpg_CloneMapEvent: @@ -4364,6 +4366,46 @@ bool Game_Interpreter::CommandManiacGetGameInfo(lcf::rpg::EventCommand const& co return true; } +bool Game_Interpreter::CommandManiacZoom(lcf::rpg::EventCommand const& com) { + if (!Player::IsPatchManiac()) { + return true; + } + + if (com.parameters.size() < 7) { + Output::Warning("Maniac Zoom: Insufficient parameters"); + return true; + } + + // Parameter[0] contains the modes packed in 4-bit chunks + int center_x = ValueOrVariableBitfield(com, 0, 0, 1); + int center_y = ValueOrVariableBitfield(com, 0, 1, 2); + int rate = ValueOrVariableBitfield(com, 0, 2, 3); + int duration = ValueOrVariableBitfield(com, 0, 3, 4); + int layer = ValueOrVariableBitfield(com, 0, 4, 5); + bool wait = com.parameters[6] != 0; + + // Maniacs Patch behavior: Rate < 0 is invalid/ignored usually, but safe to clamp to 100% + if (rate < 0) { + rate = 100; + } + + // Layer 0 indicates cancellation/reset in Maniacs + if (layer <= 0) { + layer = 0; + // Reset rate to 100% when disabling to ensure clean state, + rate = 100; + } + + Main_Data::game_screen->SetZoom(center_x, center_y, rate, duration, layer); + + if (wait && duration > 0) { + SetupWaitFrames(duration); + } + + Output::Debug("Maniac Zoom: CenterX {}, CenterY {}, Rate {}, Duration {}, Layer {}", center_x, center_y, rate, duration, layer); + return true; +} + bool Game_Interpreter::CommandManiacGetSaveInfo(lcf::rpg::EventCommand const& com) { if (!Player::IsPatchManiac()) { return true; diff --git a/src/game_interpreter.h b/src/game_interpreter.h index e42c44462f..aedaa4c7be 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -311,6 +311,7 @@ class Game_Interpreter : public Game_BaseInterpreterContext bool CommandEasyRpgCloneMapEvent(lcf::rpg::EventCommand const& com); bool CommandEasyRpgDestroyMapEvent(lcf::rpg::EventCommand const& com); bool CommandManiacGetGameInfo(lcf::rpg::EventCommand const& com); + bool CommandManiacZoom(lcf::rpg::EventCommand const& com); void SetSubcommandIndex(int indent, int idx); uint8_t& ReserveSubcommandIndex(int indent); diff --git a/src/game_screen.cpp b/src/game_screen.cpp index 016ed4fab2..518e35c6ac 100644 --- a/src/game_screen.cpp +++ b/src/game_screen.cpp @@ -35,6 +35,7 @@ #include "flash.h" #include "shake.h" #include "rand.h" +#include "main_data.h" Game_Screen::Game_Screen() { @@ -312,6 +313,7 @@ void Game_Screen::UpdateScreenEffects() { data.tint_time_left = data.tint_time_left - 1; } + UpdateZoom(); Flash::Update(data.flash_current_level, data.flash_time_left, data.flash_continuous, @@ -357,6 +359,62 @@ void Game_Screen::Update() { UpdateBattleAnimation(); } +void Game_Screen::SetZoom(int x, int y, int rate, int duration, int layer) { + double real_rate = static_cast(rate) / 100.0; + + // Calculate the anchor point such that (x,y) appears at the screen center + double tx = x; + double ty = y; + + if (std::abs(real_rate - 1.0) > 0.001) { + tx = (Player::screen_width / 2.0 - x * real_rate) / (1.0 - real_rate); + ty = (Player::screen_height / 2.0 - y * real_rate) / (1.0 - real_rate); + } + + // Cap the anchor point to the screen borders to prevent large offsets + tx = Utils::Clamp(tx, 0.0, static_cast(Player::screen_width)); + ty = Utils::Clamp(ty, 0.0, static_cast(Player::screen_height)); + + // If layer is newly activated (was 0), snap current pos to target to avoid flying in from default/previous + if (maniac_zoom_layer == 0 && layer != 0) { + maniac_zoom_current_x = tx; + maniac_zoom_current_y = ty; + maniac_zoom_current_rate = 1.0; + } + + maniac_zoom_target_x = static_cast(std::round(tx)); + maniac_zoom_target_y = static_cast(std::round(ty)); + maniac_zoom_target_rate = real_rate; + maniac_zoom_time_left = duration; + maniac_zoom_layer = layer; + + // If duration is 0, apply immediately + if (duration <= 0) { + maniac_zoom_current_rate = maniac_zoom_target_rate; + maniac_zoom_current_x = static_cast(maniac_zoom_target_x); + maniac_zoom_current_y = static_cast(maniac_zoom_target_y); + } +} + + +void Game_Screen::UpdateZoom() { + if (maniac_zoom_time_left > 0) { + maniac_zoom_time_left--; + // Linear interpolation + double dt = static_cast(maniac_zoom_time_left + 1); + + maniac_zoom_current_rate += (maniac_zoom_target_rate - maniac_zoom_current_rate) / dt; + + maniac_zoom_current_x += (static_cast(maniac_zoom_target_x) - maniac_zoom_current_x) / dt; + maniac_zoom_current_y += (static_cast(maniac_zoom_target_y) - maniac_zoom_current_y) / dt; + } + else { + maniac_zoom_current_rate = maniac_zoom_target_rate; + maniac_zoom_current_x = static_cast(maniac_zoom_target_x); + maniac_zoom_current_y = static_cast(maniac_zoom_target_y); + } +} + int Game_Screen::ShowBattleAnimation(int animation_id, int target_id, bool global, int start_frame) { const lcf::rpg::Animation* anim = lcf::ReaderUtil::GetElement(lcf::Data::animations, animation_id); if (!anim) { diff --git a/src/game_screen.h b/src/game_screen.h index e6e11669c4..40d67ebfb1 100644 --- a/src/game_screen.h +++ b/src/game_screen.h @@ -132,6 +132,15 @@ class Game_Screen { */ void CancelBattleAnimation(); + // Maniac Zoom Support + void SetZoom(int x, int y, int rate, int duration, int layer); + void UpdateZoom(); + + int GetZoomLayer() const { return maniac_zoom_layer; } + double GetZoomRate() const { return maniac_zoom_current_rate; } + int GetZoomX() const { return Utils::RoundTo(maniac_zoom_current_x); } + int GetZoomY() const { return Utils::RoundTo(maniac_zoom_current_y); } + /** * Whether or not a battle animation is currently playing. */ @@ -194,6 +203,17 @@ class Game_Screen { protected: std::vector particles; + // Maniac Zoom State + double maniac_zoom_current_x = 0.0; + double maniac_zoom_current_y = 0.0; + int maniac_zoom_target_x = 0; + int maniac_zoom_target_y = 0; + + double maniac_zoom_current_rate = 1.0; + double maniac_zoom_target_rate = 1.0; + int maniac_zoom_time_left = 0; + int maniac_zoom_layer = 0; // 0 = Disabled + void StopWeather(); void UpdateRain(); void UpdateSnow(); diff --git a/src/graphics.cpp b/src/graphics.cpp index 7b661f91d2..e8f4177042 100644 --- a/src/graphics.cpp +++ b/src/graphics.cpp @@ -30,6 +30,10 @@ #include "drawable_mgr.h" #include "baseui.h" #include "game_clock.h" +#include "game_screen.h" +#include "main_data.h" +#include "game_map.h" + using namespace std::chrono_literals; @@ -118,9 +122,86 @@ void Graphics::Draw(Bitmap& dst) { LocalDraw(dst, min_z, max_z); } +static Drawable::Z_t GetZForManiacLayer(int layer) { + if (layer <= 0) return std::numeric_limits::min(); + if (layer >= 10) return std::numeric_limits::max(); + + // Layer 9 (Windows) includes Message Text (Overlay) + if (layer == 9) return Priority_Frame; + + // For layers 1-8, the range ends just before the start of the *next* logical layer group. + Drawable::Z_t next_layer_start = 0; + switch (layer) { + case 1: next_layer_start = Priority_TilesetBelow; break; // Panorama + case 2: next_layer_start = Priority_EventsBelow; break; // Lower Chipset + case 3: next_layer_start = Priority_Player; break; // Events Below Hero + case 4: next_layer_start = Priority_TilesetAbove; break; // Hero / Events Same Level + case 5: next_layer_start = Priority_EventsFlying; break; // Upper Chipset + case 6: next_layer_start = Priority_PictureNew; break; // Events Above Hero + case 7: next_layer_start = Priority_BattleAnimation; break; // Pictures (All priorities) + case 8: next_layer_start = Priority_Window; break; // Animations + } + + return next_layer_start - 1; +} + void Graphics::LocalDraw(Bitmap& dst, Drawable::Z_t min_z, Drawable::Z_t max_z) { auto& drawable_list = DrawableMgr::GetLocalList(); + // Maniac Zoom Handling + // Check if game_screen exists to prevent crash during init/cleanup + // Also ensure we are in a Map or Battle scene + if (Main_Data::game_screen && current_scene && + (current_scene->type == Scene::Map || current_scene->type == Scene::Battle)) + { + int zoom_layer = Main_Data::game_screen->GetZoomLayer(); + + if (zoom_layer > 0) { + Drawable::Z_t threshold_z = GetZForManiacLayer(zoom_layer); + + // Only intervene if the zoom layer is within the current drawing range + if (threshold_z >= min_z) { + static BitmapRef zoom_buffer; + // Ensure intermediate buffer exists and matches screen size + if (!zoom_buffer || zoom_buffer->GetWidth() != dst.GetWidth() || zoom_buffer->GetHeight() != dst.GetHeight()) { + zoom_buffer = Bitmap::Create(dst.GetWidth(), dst.GetHeight(), true); + } + + // 1. Prepare Buffer + if (min_z == std::numeric_limits::min()) { + // If rendering from the bottom, fill background to prevent trails. + dst.Fill(Color(0, 0, 0, 255)); + + // Draw the scene background (e.g. Panorama or color) into the buffer + current_scene->DrawBackground(*zoom_buffer); + } + else { + zoom_buffer->Clear(); + } + + // 2. Draw layers *affected* by zoom into the buffer + // We clip max_z to threshold_z + drawable_list.Draw(*zoom_buffer, min_z, std::min(max_z, threshold_z)); + + // 3. Apply Zoom Transform + double rate = Main_Data::game_screen->GetZoomRate(); + int cx = Main_Data::game_screen->GetZoomX(); + int cy = Main_Data::game_screen->GetZoomY(); + + dst.ZoomOpacityBlit(cx, cy, cx, cy, *zoom_buffer, zoom_buffer->GetRect(), rate, rate, Opacity::Opaque()); + + // 4. Continue rendering remaining layers normally (on top of the zoomed image) + // Adjust min_z to start after the threshold + if (max_z > threshold_z) { + drawable_list.Draw(dst, threshold_z + 1, max_z); + } + + return; + } + } + } + + // Standard Rendering (No Zoom or Game_Screen not ready) if (!drawable_list.empty() && min_z == std::numeric_limits::min()) { current_scene->DrawBackground(dst); }