Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/game_interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,8 @@
return CmdSetup<&Game_Interpreter::CommandManiacGetGameInfo, 8>(com);
case Cmd::EasyRpg_SetInterpreterFlag:
return CmdSetup<&Game_Interpreter::CommandEasyRpgSetInterpreterFlag, 2>(com);
case static_cast<Cmd>(3032): // Maniac_Zoom

Check warning on line 815 in src/game_interpreter.cpp

View workflow job for this annotation

GitHub Actions / ubuntu:24.04 (arm64)

case value '3032' not in enumerated type 'Game_Interpreter::Cmd' {aka 'lcf::rpg::EventCommand::Code'} [-Wswitch]

Check warning on line 815 in src/game_interpreter.cpp

View workflow job for this annotation

GitHub Actions / ubuntu:22.04 (x86_64)

case value '3032' not in enumerated type 'Game_Interpreter::Cmd' {aka 'lcf::rpg::EventCommand::Code'} [-Wswitch]

Check warning on line 815 in src/game_interpreter.cpp

View workflow job for this annotation

GitHub Actions / ubuntu:24.04 (x86_64)

case value '3032' not in enumerated type 'Game_Interpreter::Cmd' {aka 'lcf::rpg::EventCommand::Code'} [-Wswitch]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should open a PR to liblcf to add this command to the enum: https://github.com/EasyRPG/liblcf/blob/master/generator/csv/enums_easyrpg.csv#L14

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah there could be directly a PR that adds all the maniac event commands at once.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah my bad, if there is more than one missing, I would say don't worry about it as a blocker on this PR, but someone should clean up the missing ones

return CmdSetup<&Game_Interpreter_Map::CommandManiacZoom, 7>(com);
case Cmd::EasyRpg_ProcessJson:
return CmdSetup<&Game_Interpreter::CommandEasyRpgProcessJson, 8>(com);
case Cmd::EasyRpg_CloneMapEvent:
Expand Down Expand Up @@ -4364,6 +4366,46 @@
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;
Expand Down
1 change: 1 addition & 0 deletions src/game_interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
58 changes: 58 additions & 0 deletions src/game_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "flash.h"
#include "shake.h"
#include "rand.h"
#include "main_data.h"

Game_Screen::Game_Screen()
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<double>(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<double>(Player::screen_width));
ty = Utils::Clamp(ty, 0.0, static_cast<double>(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<int>(std::round(tx));
maniac_zoom_target_y = static_cast<int>(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<double>(maniac_zoom_target_x);
maniac_zoom_current_y = static_cast<double>(maniac_zoom_target_y);
}
}


void Game_Screen::UpdateZoom() {
if (maniac_zoom_time_left > 0) {
maniac_zoom_time_left--;
// Linear interpolation
double dt = static_cast<double>(maniac_zoom_time_left + 1);

maniac_zoom_current_rate += (maniac_zoom_target_rate - maniac_zoom_current_rate) / dt;

maniac_zoom_current_x += (static_cast<double>(maniac_zoom_target_x) - maniac_zoom_current_x) / dt;
maniac_zoom_current_y += (static_cast<double>(maniac_zoom_target_y) - maniac_zoom_current_y) / dt;
}
else {
maniac_zoom_current_rate = maniac_zoom_target_rate;
maniac_zoom_current_x = static_cast<double>(maniac_zoom_target_x);
maniac_zoom_current_y = static_cast<double>(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) {
Expand Down
20 changes: 20 additions & 0 deletions src/game_screen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(maniac_zoom_current_x); }
int GetZoomY() const { return Utils::RoundTo<int>(maniac_zoom_current_y); }

/**
* Whether or not a battle animation is currently playing.
*/
Expand Down Expand Up @@ -194,6 +203,17 @@ class Game_Screen {
protected:
std::vector<Particle> 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();
Expand Down
81 changes: 81 additions & 0 deletions src/graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Drawable::Z_t>::min();
if (layer >= 10) return std::numeric_limits<Drawable::Z_t>::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<Drawable::Z_t>::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<Drawable::Z_t>::min()) {
current_scene->DrawBackground(dst);
}
Expand Down
Loading