diff --git a/CMakeLists.txt b/CMakeLists.txt
index cb50b102d2..911e7509e5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -36,6 +36,8 @@ add_library(${PROJECT_NAME} OBJECT
src/async_op.h
src/algo.h
src/algo.cpp
+ src/animation_helper.cpp
+ src/animation_helper.h
src/attribute.h
src/attribute.cpp
src/audio.cpp
diff --git a/Makefile.am b/Makefile.am
index 02992d0f91..73056fbb7a 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -14,6 +14,8 @@ libeasyrpg_player_a_SOURCES = \
src/async_op.h \
src/algo.h \
src/algo.cpp \
+ src/animation_helper.cpp \
+ src/animation_helper.h \
src/attribute.h \
src/attribute.cpp \
src/audio.cpp \
diff --git a/src/animation_helper.cpp b/src/animation_helper.cpp
new file mode 100644
index 0000000000..c0c46b9fb3
--- /dev/null
+++ b/src/animation_helper.cpp
@@ -0,0 +1,253 @@
+/*
+ * 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 .
+ */
+
+#include "animation_helper.h"
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+ constexpr double PI = 3.14159265358979323846;
+
+ // Forward declarations
+ double HandleElasticEasing(Animation_Helper::EaseType type, double t, double b, double c, double d);
+ double HandleBounceEasing(Animation_Helper::EaseType type, double t, double b, double c, double d);
+
+ struct EasingData {
+ std::string_view name;
+ double x1, y1, x2, y2;
+ Animation_Helper::EaseType type;
+ };
+
+ const EasingData EASING_DATA[] = {
+ {"linear", 0.250, 0.250, 0.750, 0.750, Animation_Helper::EaseType::Linear},
+ {"ease", 0.250, 0.100, 0.250, 1.000, Animation_Helper::EaseType::Ease},
+ {"easeIn", 0.420, 0.000, 1.000, 1.000, Animation_Helper::EaseType::EaseIn},
+ {"easeOut", 0.000, 0.000, 0.580, 1.000, Animation_Helper::EaseType::EaseOut},
+ {"easeInOut", 0.420, 0.000, 0.580, 1.000, Animation_Helper::EaseType::EaseInOut},
+ {"quadIn", 0.550, 0.085, 0.680, 0.530, Animation_Helper::EaseType::QuadIn},
+ {"quadOut", 0.250, 0.460, 0.450, 0.940, Animation_Helper::EaseType::QuadOut},
+ {"quadInOut", 0.455, 0.030, 0.515, 0.955, Animation_Helper::EaseType::QuadInOut},
+ {"cubicIn", 0.550, 0.055, 0.675, 0.190, Animation_Helper::EaseType::CubicIn},
+ {"cubicOut", 0.215, 0.610, 0.355, 1.000, Animation_Helper::EaseType::CubicOut},
+ {"cubicInOut", 0.645, 0.045, 0.355, 1.000, Animation_Helper::EaseType::CubicInOut},
+ {"quartIn", 0.895, 0.030, 0.685, 0.220, Animation_Helper::EaseType::QuartIn},
+ {"quartOut", 0.165, 0.840, 0.440, 1.000, Animation_Helper::EaseType::QuartOut},
+ {"quartInOut", 0.770, 0.000, 0.175, 1.000, Animation_Helper::EaseType::QuartInOut},
+ {"quintIn", 0.755, 0.050, 0.855, 0.060, Animation_Helper::EaseType::QuintIn},
+ {"quintOut", 0.230, 1.000, 0.320, 1.000, Animation_Helper::EaseType::QuintOut},
+ {"quintInOut", 0.860, 0.000, 0.070, 1.000, Animation_Helper::EaseType::QuintInOut},
+ {"sineIn", 0.470, 0.000, 0.745, 0.715, Animation_Helper::EaseType::SineIn},
+ {"sineOut", 0.390, 0.575, 0.565, 1.000, Animation_Helper::EaseType::SineOut},
+ {"sineInOut", 0.445, 0.050, 0.550, 0.950, Animation_Helper::EaseType::SineInOut},
+ {"expoIn", 0.950, 0.050, 0.795, 0.035, Animation_Helper::EaseType::ExpoIn},
+ {"expoOut", 0.190, 1.000, 0.220, 1.000, Animation_Helper::EaseType::ExpoOut},
+ {"expoInOut", 1.000, 0.000, 0.000, 1.000, Animation_Helper::EaseType::ExpoInOut},
+ {"circIn", 0.600, 0.040, 0.980, 0.335, Animation_Helper::EaseType::CircIn},
+ {"circOut", 0.075, 0.820, 0.165, 1.000, Animation_Helper::EaseType::CircOut},
+ {"circInOut", 0.785, 0.135, 0.150, 0.860, Animation_Helper::EaseType::CircInOut},
+ {"backIn", 0.600, -0.280, 0.735, 0.045, Animation_Helper::EaseType::BackIn},
+ {"backOut", 0.175, 0.885, 0.320, 1.275, Animation_Helper::EaseType::BackOut},
+ {"backInOut", 0.680, -0.550, 0.265, 1.550, Animation_Helper::EaseType::BackInOut},
+ // Special cases (no bezier points needed)
+ {"elasticIn", 0, 0, 0, 0, Animation_Helper::EaseType::ElasticIn},
+ {"elasticOut", 0, 0, 0, 0, Animation_Helper::EaseType::ElasticOut},
+ {"elasticInOut", 0, 0, 0, 0, Animation_Helper::EaseType::ElasticInOut},
+ {"bounceIn", 0, 0, 0, 0, Animation_Helper::EaseType::BounceIn},
+ {"bounceOut", 0, 0, 0, 0, Animation_Helper::EaseType::BounceOut},
+ {"bounceInOut", 0, 0, 0, 0, Animation_Helper::EaseType::BounceInOut}
+ };
+
+ double HandleElasticEasing(Animation_Helper::EaseType type, double t, double b, double c, double d) {
+ constexpr double pi2 = 2.0 * PI;
+ const double p = d * 0.3;
+ const double s = p / 4.0;
+
+ if (t <= 0.0) return b;
+ if (t >= 1.0) return b + c;
+
+ switch (type) {
+ case Animation_Helper::EaseType::ElasticIn: {
+ const double post_fix = c * std::pow(2.0, 10.0 * (t - 1.0));
+ return -(post_fix * std::sin((t * d - s) * pi2 / p)) + b;
+ }
+ case Animation_Helper::EaseType::ElasticOut: {
+ return (c * std::pow(2.0, -10.0 * t) * std::sin((t * d - s) * pi2 / p) + c + b);
+ }
+ case Animation_Helper::EaseType::ElasticInOut: {
+ if (t < 0.5) {
+ const double post_fix = c * std::pow(2.0, 10.0 * (2.0 * t - 1.0));
+ return -0.5 * (post_fix * std::sin(((2.0 * t - 1.0) * d - s) * pi2 / p)) + b;
+ }
+ const double post_fix = c * std::pow(2.0, -10.0 * (2.0 * t - 1.0));
+ return post_fix * std::sin(((2.0 * t - 1.0) * d - s) * pi2 / p) * 0.5 + c + b;
+ }
+ default:
+ return b + c * t;
+ }
+ }
+
+ double HandleBounceEasing(Animation_Helper::EaseType type, double t, double b, double c, double d) {
+ switch (type) {
+ case Animation_Helper::EaseType::BounceIn:
+ return c - HandleBounceEasing(Animation_Helper::EaseType::BounceOut, 1.0 - t, 0, c, d) + b;
+ case Animation_Helper::EaseType::BounceOut:
+ if (t < (1.0 / 2.75)) {
+ return c * (7.5625 * t * t) + b;
+ } else if (t < (2.0 / 2.75)) {
+ t -= (1.5 / 2.75);
+ return c * (7.5625 * t * t + 0.75) + b;
+ } else if (t < (2.5 / 2.75)) {
+ t -= (2.25 / 2.75);
+ return c * (7.5625 * t * t + 0.9375) + b;
+ }
+ t -= (2.625 / 2.75);
+ return c * (7.5625 * t * t + 0.984375) + b;
+ case Animation_Helper::EaseType::BounceInOut:
+ if (t < 0.5) {
+ return HandleBounceEasing(Animation_Helper::EaseType::BounceIn, t * 2.0, 0, c, d) * 0.5 + b;
+ }
+ return HandleBounceEasing(Animation_Helper::EaseType::BounceOut, t * 2.0 - 1.0, 0, c, d) * 0.5 + c * 0.5 + b;
+ default:
+ return b + c * t;
+ }
+ }
+}
+
+namespace Animation_Helper {
+
+double CubicBezier(double progress, double x1, double y1, double x2, double y2) {
+ // This is a cubic Bezier curve for animation timing:
+ // P₀(0,0) - start point, always fixed
+ // P₁(x1,y1) - first control point
+ // P₂(x2,y2) - second control point
+ // P₃(1,1) - end point, always fixed
+ //
+ // x coordinates (x1,x2) control the timing curve's shape
+ // y coordinates (y1,y2) control the rate of change at those points
+
+ ////progress = std::clamp(progress, 0.0, 1.0);
+
+ // Since we need to find y for a given x (progress),
+ // we need to solve for t where the curve's x equals our progress
+ // This is a rough approximation using a few iterations
+ double t = progress; // Initial guess
+
+ // Newton's method to find better t value
+ for (int i = 0; i < 5; i++) { // Usually converges in 4-5 iterations
+ const double currentT = t;
+ const double oneMinusT = 1.0 - t;
+
+ // Calculate x(t) - current x position on curve
+ const double x = 3.0 * oneMinusT * oneMinusT * t * x1 +
+ 3.0 * oneMinusT * t * t * x2 +
+ t * t * t;
+
+ // If we're close enough to desired x, calculate final y
+ if (std::abs(x - progress) < 0.001) {
+ break;
+ }
+
+ // Calculate x'(t) - derivative of x with respect to t
+ const double dx = 3.0 * oneMinusT * oneMinusT * x1 +
+ 6.0 * oneMinusT * t * (x2 - x1) +
+ 3.0 * t * t * (1 - x2);
+
+ // Avoid division by zero
+ if (std::abs(dx) < 0.0001) {
+ break;
+ }
+
+ // Newton iteration
+ t = t - (x - progress) / dx;
+ //// t = std::clamp(t, 0.0, 1.0);
+
+ // If t hasn't changed significantly, we're done
+ if (std::abs(t - currentT) < 0.0001) {
+ break;
+ }
+ }
+
+ // Calculate final y value using found t
+ const double oneMinusT = 1.0 - t;
+ return 3.0 * oneMinusT * oneMinusT * t * y1 +
+ 3.0 * oneMinusT * t * t * y2 +
+ t * t * t;
+}
+
+EaseType StringToEaseType(std::string_view type_name) {
+ for (const auto& data : EASING_DATA) {
+ if (data.name == type_name) {
+ return data.type;
+ }
+ }
+ return EaseType::Linear;
+}
+
+double GetEasedTime(EaseType type, double t, double b, double c, double d) {
+ t = std::clamp(t, 0.0, d);
+ const double normalized_t = t / d;
+
+ // Handle special cases first
+ switch (type) {
+ case EaseType::ElasticIn:
+ case EaseType::ElasticOut:
+ case EaseType::ElasticInOut:
+ return HandleElasticEasing(type, normalized_t, b, c, d);
+ case EaseType::BounceIn:
+ case EaseType::BounceOut:
+ case EaseType::BounceInOut:
+ return HandleBounceEasing(type, normalized_t, b, c, d);
+ default:
+ break;
+ }
+
+ // Find bezier points for the type
+ for (const auto& data : EASING_DATA) {
+ if (data.type == type) {
+ return b + c * CubicBezier(normalized_t, data.x1, data.y1, data.x2, data.y2);
+ }
+ }
+
+ return b + c * normalized_t; // Linear fallback
+}
+
+double GetEasedTime(const std::string& easing_type, double t, double b, double c, double d) {
+ if (easing_type.substr(0, 6) == "bezier") {
+ std::vector params;
+ size_t start = easing_type.find('(') + 1;
+ size_t end = easing_type.find(')');
+ if (start != std::string::npos && end != std::string::npos) {
+ std::string values = easing_type.substr(start, end - start);
+ std::istringstream iss(values);
+ double val;
+ while (iss >> val) {
+ params.push_back(val);
+ iss.ignore();
+ }
+ if (params.size() == 4) {
+ return b + c * CubicBezier(t/d, params[0], params[1], params[2], params[3]);
+ }
+ }
+ }
+
+ return GetEasedTime(StringToEaseType(easing_type), t, b, c, d);
+}
+
+} // namespace Animation_Helper
diff --git a/src/animation_helper.h b/src/animation_helper.h
new file mode 100644
index 0000000000..563599a86b
--- /dev/null
+++ b/src/animation_helper.h
@@ -0,0 +1,125 @@
+/*
+ * 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 .
+ */
+
+#ifndef ANIMATION_HELPER_H
+#define ANIMATION_HELPER_H
+
+#include
+#include
+
+namespace Animation_Helper {
+ /**
+ * Supported easing types for animations.
+ * These match CSS animation timing functions for familiarity.
+ */
+ enum class EaseType {
+ Linear,
+ // Basic
+ Ease,
+ EaseIn,
+ EaseOut,
+ EaseInOut,
+ // Quadratic
+ QuadIn,
+ QuadOut,
+ QuadInOut,
+ // Cubic
+ CubicIn,
+ CubicOut,
+ CubicInOut,
+ // Quartic
+ QuartIn,
+ QuartOut,
+ QuartInOut,
+ // Quintic
+ QuintIn,
+ QuintOut,
+ QuintInOut,
+ // Sinusoidal
+ SineIn,
+ SineOut,
+ SineInOut,
+ // Exponential
+ ExpoIn,
+ ExpoOut,
+ ExpoInOut,
+ // Circular
+ CircIn,
+ CircOut,
+ CircInOut,
+ // Back
+ BackIn,
+ BackOut,
+ BackInOut,
+ // Elastic
+ ElasticIn,
+ ElasticOut,
+ ElasticInOut,
+ // Bounce
+ BounceIn,
+ BounceOut,
+ BounceInOut
+ };
+
+ /**
+ * Converts a string to an EaseType enum value
+ * @param type_name The name of the easing type
+ * @return The corresponding EaseType enum value
+ */
+ EaseType StringToEaseType(std::string_view type_name);
+
+ /**
+ * Calculates a point on a cubic Bezier curve for animation timing
+ * @param progress Current animation progress (0 to 1)
+ * @param x1 X coordinate of first control point, controls when early changes occur
+ * @param y1 Y coordinate of first control point, controls magnitude of early changes
+ * @param x2 X coordinate of second control point, controls when later changes occur
+ * @param y2 Y coordinate of second control point, controls magnitude of later changes
+ * @return The calculated animation progress value
+ *
+ * Control points affect the animation as follows:
+ * - x values control timing (when changes happen)
+ * - y values control magnitude (how much change occurs)
+ * - Values > current progress create overshoots
+ * - Values < current progress create undershoots
+ */
+ double CubicBezier(double progress, double x1, double y1, double x2, double y2);
+
+ /**
+ * Gets the eased time value based on the specified easing type
+ * @param easing_type Type of easing to apply
+ * @param t Current time (0 to 1)
+ * @param b Start value
+ * @param c Change in value (end - start)
+ * @param d Duration
+ * @return The eased value
+ */
+ double GetEasedTime(const std::string& easing_type, double t, double b, double c, double d);
+
+ /**
+ * Gets the eased time value using enum-based easing type
+ * @param type The easing type enum
+ * @param t Current time (0 to 1)
+ * @param b Start value
+ * @param c Change in value (end - start)
+ * @param d Duration
+ * @return The eased value
+ */
+ double GetEasedTime(EaseType type, double t, double b, double c, double d);
+}
+
+#endif
diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp
index 91821c45e0..4200dc448e 100644
--- a/src/game_interpreter.cpp
+++ b/src/game_interpreter.cpp
@@ -26,6 +26,7 @@
#include
#include "game_interpreter.h"
#include "async_handler.h"
+#include "animation_helper.h"
#include "audio.h"
#include "game_dynrpg.h"
#include "filefinder.h"
@@ -786,6 +787,8 @@ bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) {
return CommandManiacCallCommand(com);
case Cmd::Maniac_GetGameInfo:
return CommandManiacGetGameInfo(com);
+ case Cmd::EasyRpg_AnimateVariable:
+ return CommandEasyRpgAnimateVariable(com);
case Cmd::EasyRpg_SetInterpreterFlag:
return CommandEasyRpgSetInterpreterFlag(com);
case Cmd::EasyRpg_ProcessJson:
@@ -5319,6 +5322,105 @@ bool Game_Interpreter::CommandManiacCallCommand(lcf::rpg::EventCommand const& co
return true;
}
+bool Game_Interpreter::CommandEasyRpgAnimateVariable(lcf::rpg::EventCommand const& com) {
+ struct AnimationParams {
+ int32_t target;
+ int32_t start;
+ int32_t end;
+ int32_t duration;
+ std::string easing_start;
+ std::string easing_end;
+ };
+
+ // Parse command parameters
+ AnimationParams params{
+ ValueOrVariable(com.parameters[0], com.parameters[1]),
+ ValueOrVariable(com.parameters[2], com.parameters[3]),
+ ValueOrVariable(com.parameters[4], com.parameters[5]),
+ ValueOrVariable(com.parameters[6], com.parameters[7]),
+ ToString(com.string),
+ "null"
+ };
+
+ // Extract dual easing types if present
+ const auto separator_pos = params.easing_start.find('/');
+ if (separator_pos != std::string::npos) {
+ params.easing_end = params.easing_start.substr(separator_pos + 1);
+ params.easing_start = params.easing_start.substr(0, separator_pos);
+ }
+
+ // Prepare animation commands
+ std::vector cmd_list;
+ cmd_list.reserve(params.duration * 2); // Pre-allocate for efficiency
+
+ lcf::rpg::EventCommand wait_com;
+ wait_com.code = static_cast(Cmd::Wait);
+
+ lcf::rpg::EventCommand var_com;
+ var_com.code = static_cast(Cmd::ControlVars);
+ std::vector var_params = {
+ 0,
+ params.target,
+ 0,
+ 0,
+ 0,
+ params.end
+ };
+
+ const double step_size = 1.0 / params.duration;
+ const int half_duration = params.duration / 2;
+
+ // Generate animation steps
+ for (int step = 1; step <= params.duration; ++step) {
+ const double current_time = step * step_size;
+ double normalized_time;
+ const std::string& current_easing = (params.easing_end == "null")
+ ? params.easing_start
+ : (step <= half_duration ? params.easing_start : params.easing_end);
+
+ // Calculate normalized time based on single or dual easing
+ if (params.easing_end == "null") {
+ normalized_time = current_time;
+ }
+ else {
+ // For dual easing, each half uses its own normalized time from 0 to 1
+ normalized_time = step <= half_duration
+ ? (current_time * 2.0) // First half: 0->1
+ : ((current_time - 0.5) * 2.0); // Second half: 0->1
+ }
+
+ // Get eased time for current segment
+ const double eased_time = Animation_Helper::GetEasedTime(current_easing, normalized_time, 0, 1, 1);
+
+ // Calculate interpolated value
+ double interpolated;
+ if (params.easing_end == "null") {
+ // Single easing - interpolate directly from start to end
+ interpolated = params.start + eased_time * (params.end - params.start);
+ }
+ else {
+ // Dual easing - interpolate each half separately
+ const double mid_point = (params.start + params.end) * 0.5;
+ if (step <= half_duration) {
+ interpolated = params.start + eased_time * (mid_point - params.start);
+ }
+ else {
+ interpolated = mid_point + eased_time * (params.end - mid_point);
+ }
+ }
+
+ // Update and push commands
+ var_params.back() = static_cast(interpolated);
+ var_com.parameters = lcf::DBArray(var_params.begin(), var_params.end());
+
+ cmd_list.push_back(var_com);
+ cmd_list.push_back(wait_com);
+ }
+
+ Push(cmd_list, 0, false);
+ return true;
+}
+
bool Game_Interpreter::CommandEasyRpgSetInterpreterFlag(lcf::rpg::EventCommand const& com) {
if (!Player::HasEasyRpgExtensions()) {
return true;
diff --git a/src/game_interpreter.h b/src/game_interpreter.h
index 04bf3afda3..d69d9b8315 100644
--- a/src/game_interpreter.h
+++ b/src/game_interpreter.h
@@ -297,6 +297,7 @@ class Game_Interpreter : public Game_BaseInterpreterContext
bool CommandManiacSetGameOption(lcf::rpg::EventCommand const& com);
bool CommandManiacControlStrings(lcf::rpg::EventCommand const& com);
bool CommandManiacCallCommand(lcf::rpg::EventCommand const& com);
+ bool CommandEasyRpgAnimateVariable(lcf::rpg::EventCommand const& com);
bool CommandEasyRpgSetInterpreterFlag(lcf::rpg::EventCommand const& com);
bool CommandEasyRpgProcessJson(lcf::rpg::EventCommand const& com);
bool CommandEasyRpgCloneMapEvent(lcf::rpg::EventCommand const& com);