From 1ffa80a2885cd5e38c5f57f6f1fb5fad9ab6b459 Mon Sep 17 00:00:00 2001 From: LizardKing777 <154367673+LizardKing777@users.noreply.github.com> Date: Sun, 22 Feb 2026 12:28:54 -0700 Subject: [PATCH 1/3] Particle effects Version 1 & 2 Both function independently of one another, so if a game was released using either version it should work flawlessly. --- src/dynrpg_particleV1.cpp | 415 +++++++++++++ src/dynrpg_particleV1.h | 24 + src/dynrpg_particleV2.cpp | 1181 +++++++++++++++++++++++++++++++++++++ src/dynrpg_particleV2.h | 25 + src/game_dynrpg.cpp | 12 +- 5 files changed, 1656 insertions(+), 1 deletion(-) create mode 100644 src/dynrpg_particleV1.cpp create mode 100644 src/dynrpg_particleV1.h create mode 100644 src/dynrpg_particleV2.cpp create mode 100644 src/dynrpg_particleV2.h diff --git a/src/dynrpg_particleV1.cpp b/src/dynrpg_particleV1.cpp new file mode 100644 index 0000000000..cb1d31c520 --- /dev/null +++ b/src/dynrpg_particleV1.cpp @@ -0,0 +1,415 @@ +/* + * This file is part of EasyRPG Player. + * ... (license header) ... + * Based on DynRPG Particle Effects V1 by Kazesui. (MIT license) + */ + +#include "dynrpg_particleV1.h" + +#include +#include +#include +#include +#include +#include + +#include "async_handler.h" +#include "drawable.h" +#include "drawable_mgr.h" +#include "bitmap.h" +#include "cache.h" +#include "game_screen.h" +#include "game_map.h" +#include "main_data.h" +#include "rand.h" +#include "output.h" + +constexpr double PI = 3.14159265358979323846; + +namespace { + + struct ParticleObj { + double size; + double x, dx, y, dy; + }; + + struct BurstObj { + double r, g, b, alpha; + std::list particles; + }; + + class ParticleEffectV1 : public Drawable { + public: + int amount, size; + int red, green, blue; + int rndX, rndY; + double spd, rndSpd; + double timeout, delay; + double angleS, angleE; + bool mask; + std::string filename; + + double dr, dg, db, ds, dA; + + std::list bursts; + BitmapRef image; + bool hasTexture; + + ParticleEffectV1() : Drawable(Priority_Weather) { + dr = dg = db = ds = 0; + red = green = blue = 255; + timeout = 30; + amount = 50; + delay = 0; + size = 2; + dA = 8.5; + spd = 2; + rndSpd = 2; + rndX = rndY = 0; + angleS = 0; + angleE = 2 * PI; + mask = false; + filename = "Particle"; + hasTexture = false; + + DrawableMgr::Register(this); + } + + ~ParticleEffectV1() override {} + + void setTimeout(double t) { + double time_diff = t - delay; + if (time_diff <= 0) time_diff = 1.0; + + dA = 255.0 / time_diff; + timeout = t; + } + + void setAngle(double a1, double a2) { + angleS = a1 * PI / 180.0; + angleE = (a2 - a1) * PI / 180.0; + } + + void setGrowth(int s, int newSize) { + size = s; + double t = (timeout > 0) ? timeout : 1.0; + ds = (newSize - size) / t; + } + + void colorFade(int r = 0, int g = 0, int b = 0) { + double t = (timeout > 0) ? timeout : 1.0; + dr = ((double)r - red) / t; + dg = ((double)g - green) / t; + db = ((double)b - blue) / t; + } + + void loadTexture() { + std::string name = filename; + + // Emulate RM2k3 legacy relative paths + size_t pos = name.find("DynPlugins/"); + if (pos != std::string::npos) name.erase(pos, 11); + pos = name.find("DynPlugins\\"); + if (pos != std::string::npos) name.erase(pos, 11); + + // Prevent Picture/Picture/ overlap + pos = name.find("Picture/"); + if (pos != std::string::npos) name.erase(pos, 8); + pos = name.find("Picture\\"); + if (pos != std::string::npos) name.erase(pos, 8); + + // Strip extension for EasyRPG Cache compatibility + pos = name.find_last_of("."); + if (pos != std::string::npos) name = name.substr(0, pos); + + if (!name.empty()) { + FileRequestAsync* req = AsyncHandler::RequestFile("Picture", name); + req->Start(); + image = Cache::Picture(name, mask); + hasTexture = true; + } + } + + void newBurst(int x, int y) { + BurstObj burst; + burst.r = red; + burst.g = green; + burst.b = blue; + burst.alpha = 255.0; + + if (!hasTexture && !filename.empty()) { + loadTexture(); + } + + for (int i = 0; i < amount; i++) { + double rand_val1 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + double rand_val2 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + double rand_val3 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + double rand_val4 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + + double rnd = angleS + rand_val1 * angleE; + double newSpd = (spd + rndSpd * rand_val2); + + ParticleObj pa; + pa.x = x + 2 * rndX * rand_val3 - rndX; + pa.y = y + 2 * rndY * rand_val4 - rndY; + pa.dx = newSpd * std::cos(rnd); + pa.dy = newSpd * std::sin(rnd); + pa.size = size; + + burst.particles.push_back(pa); + } + bursts.push_back(burst); + } + + void newHeart(int x, int y) { + BurstObj burst; + burst.r = red; + burst.g = green; + burst.b = blue; + burst.alpha = 255.0; + + if (!hasTexture && !filename.empty()) { + loadTexture(); + } + + for (int i = 0; i < amount; i++) { + double rand_val = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + double rnd = rand_val * 2 * PI; + + ParticleObj pa; + pa.x = x; + pa.y = y; + pa.dx = spd * (16 * std::pow(std::sin(rnd), 3)); + pa.dy = -spd * (13 * std::cos(rnd) - 5 * std::cos(2 * rnd) - 2 * std::cos(3 * rnd) - std::cos(4 * rnd)); + pa.size = size; + + burst.particles.push_back(pa); + } + bursts.push_back(burst); + } + + void killParticles() { + bursts.clear(); + } + + void Draw(Bitmap& dst) override; + }; + + std::vector v1_effects; + bool v1_draw = true; + +} // anonymous namespace + + +void ParticleEffectV1::Draw(Bitmap& dst) { + if (!v1_draw || bursts.empty()) return; + + int cam_x = Game_Map::GetDisplayX() / 16; + int cam_y = Game_Map::GetDisplayY() / 16; + + auto burstItr = bursts.begin(); + while (burstItr != bursts.end()) { + + // 1. Color Fade physics + burstItr->r += dr; + burstItr->g += dg; + burstItr->b += db; + + // 2. Alpha fade physics (Mimicking Kazesui's global delay decrementing) + if (this->delay <= 0) { + burstItr->alpha -= dA; + if (burstItr->alpha <= 0) { + burstItr = bursts.erase(burstItr); + continue; + } + } else { + this->delay--; + } + + int draw_alpha = std::clamp((int)burstItr->alpha, 0, 255); + int cur_r = std::clamp((int)burstItr->r, 0, 255); + int cur_g = std::clamp((int)burstItr->g, 0, 255); + int cur_b = std::clamp((int)burstItr->b, 0, 255); + + // Pre-tint the texture for this specific burst frame + BitmapRef colored_image; + if (hasTexture && image) { + colored_image = Bitmap::Create(image->GetWidth(), image->GetHeight(), true); + colored_image->Clear(); + + // Map 0-200% RM2k3 tone scale to EasyRPG's 0-255 offset scale where 128 is neutral + Tone tone(cur_r * 128 / 100, cur_g * 128 / 100, cur_b * 128 / 100, 128); + colored_image->ToneBlit(0, 0, *image, image->GetRect(), tone, Opacity::Opaque()); + } + + // 3. Particle Movement physics and drawing + auto partItr = burstItr->particles.begin(); + while (partItr != burstItr->particles.end()) { + partItr->x += partItr->dx; + partItr->y += partItr->dy; + partItr->size += ds; + + int draw_size = std::max(1, (int)partItr->size); + Rect dst_rect((int)partItr->x - cam_x, (int)partItr->y - cam_y, draw_size, draw_size); + + if (colored_image) { + dst.StretchBlit(dst_rect, *colored_image, colored_image->GetRect(), Opacity(draw_alpha)); + } else { + // Fallback if no texture was found + dst.FillRect(dst_rect, Color(cur_r, cur_g, cur_b, draw_alpha)); + } + partItr++; + } + burstItr++; + } +} + +DynRpg::ParticleV1::ParticleV1(Game_DynRpg& instance) : DynRpgPlugin("ParticleSystemV1", instance) {} + +DynRpg::ParticleV1::~ParticleV1() { + for (auto* pfx : v1_effects) { + delete pfx; + } + v1_effects.clear(); +} + +bool DynRpg::ParticleV1::Invoke(std::string_view func, dyn_arg_list args, bool& do_yield, Game_Interpreter* interpreter) { + + if (func == "new_effect") { + v1_effects.push_back(new ParticleEffectV1()); + return true; + } + if (func == "stop") { + v1_draw = false; + return true; + } + if (func == "start") { + v1_draw = true; + return true; + } + + // 1. Check if the command belongs to this plugin FIRST + if (func == "effect_burst" || func == "effect_heart" || func == "effect_color" || + func == "effect_colorfade" || func == "effect_amount" || func == "effect_random" || + func == "effect_angle" || func == "effect_growth" || func == "effect_speed" || + func == "effect_timeout" || func == "effect_delay" || func == "effect_file" || + func == "effect_kill") { + + // Lenient manual parsing to properly handle floats and omitted parameters + auto get_int = [&](size_t idx, int def = 0) { + if (idx < args.size() && !args[idx].empty()) { + try { return std::stoi(args[idx]); } catch (...) {} + } + return def; + }; + + auto get_double = [&](size_t idx, double def = 0.0) { + if (idx < args.size() && !args[idx].empty()) { + try { return std::stod(args[idx]); } catch (...) {} + } + return def; + }; + + auto get_string = [&](size_t idx, std::string def = "") { + if (idx < args.size() && !args[idx].empty()) return args[idx]; + return def; + }; + + // 2. Safely check the index and emulate the original silent failure! + int index = get_int(0, -1); + + if (index < 0 || index >= v1_effects.size()) { + // By returning "true" here, we tell EasyRPG: "I acknowledge this command, + // but I am intentionally ignoring it." + // This stops the "Unsupported" warning, and leaves the particle amount + // at its default value of 50, exactly recreating your original waterfall! + return true; + } + + if (func == "effect_burst") { + v1_effects[index]->newBurst(get_int(1, 0), get_int(2, 0)); + return true; + } + + if (func == "effect_heart") { + v1_effects[index]->newHeart(get_int(1, 0), get_int(2, 0)); + return true; + } + + if (func == "effect_color") { + v1_effects[index]->red = get_int(1, 255); + v1_effects[index]->green = get_int(2, 255); + v1_effects[index]->blue = get_int(3, 255); + return true; + } + + if (func == "effect_colorfade") { + v1_effects[index]->colorFade(get_int(1, 0), get_int(2, 0), get_int(3, 0)); + return true; + } + + if (func == "effect_amount") { + v1_effects[index]->amount = get_int(1, 50); + return true; + } + + if (func == "effect_random") { + v1_effects[index]->rndX = get_int(1, 0); + v1_effects[index]->rndY = get_int(2, 0); + return true; + } + + if (func == "effect_angle") { + v1_effects[index]->setAngle(get_double(1, 0.0), get_double(2, 360.0)); + return true; + } + + if (func == "effect_growth") { + v1_effects[index]->setGrowth(get_int(1, 2), get_int(2, 2)); + return true; + } + + if (func == "effect_speed") { + v1_effects[index]->spd = get_double(1, 2.0); + v1_effects[index]->rndSpd = get_double(2, 2.0); + return true; + } + + if (func == "effect_timeout") { + v1_effects[index]->setTimeout(get_double(1, 30.0)); + return true; + } + + if (func == "effect_delay") { + v1_effects[index]->delay = get_double(1, 0.0); + return true; + } + + if (func == "effect_file") { + std::string filename = get_string(1, "Particle"); + std::string mask = get_string(2, "false"); + v1_effects[index]->filename = filename; + v1_effects[index]->mask = (mask.substr(0, 4) == "true" || mask.substr(0, 4) == "TRUE"); + v1_effects[index]->loadTexture(); + return true; + } + + if (func == "effect_kill") { + v1_effects[index]->killParticles(); + return true; + } + } + + return false; +} + +void DynRpg::ParticleV1::Update() { + // Rendering is handled by DrawableMgr. +} + +void DynRpg::ParticleV1::OnMapChange() { + for (auto* pfx : v1_effects) { + pfx->killParticles(); + } +} diff --git a/src/dynrpg_particleV1.h b/src/dynrpg_particleV1.h new file mode 100644 index 0000000000..e35ed0de92 --- /dev/null +++ b/src/dynrpg_particleV1.h @@ -0,0 +1,24 @@ +/* + * This file is part of EasyRPG Player. + * ... (license header) ... + * Based on DynRPG Particle Effects V1 by Kazesui. (MIT license) + */ + +#ifndef EP_DYNRPG_PARTICLE_V1_H_ +#define EP_DYNRPG_PARTICLE_V1_H_ + +#include "game_dynrpg.h" + +namespace DynRpg { + class ParticleV1 : public DynRpgPlugin { + public: + ParticleV1(Game_DynRpg& instance); + ~ParticleV1() override; + + bool Invoke(std::string_view func, dyn_arg_list args, bool& do_yield, Game_Interpreter* interpreter) override; + void Update() override; + void OnMapChange(); + }; +} + +#endif diff --git a/src/dynrpg_particleV2.cpp b/src/dynrpg_particleV2.cpp new file mode 100644 index 0000000000..7257f907d0 --- /dev/null +++ b/src/dynrpg_particleV2.cpp @@ -0,0 +1,1181 @@ +/* + * This file is part of EasyRPG Player. + * ... (license header) ... + * This file has been refactored for clarity, modern C++, to fix bugs + * from the original porting effort, and to add Mode 7 support. + */ + +// Headers +#include "dynrpg_particleV2.h" +#include +#include +#include +#include +#include +#include +#include "async_handler.h" +#include "drawable.h" +#include "drawable_mgr.h" +#include "baseui.h" +#include "bitmap.h" +#include "cache.h" +#include "game_screen.h" +#include "game_pictures.h" +#include "game_map.h" +#include "game_switches.h" +#include "main_data.h" +#include "graphics.h" +#include "game_battle.h" +#include "scene.h" +#include "rand.h" + +// Lowest Z-order is drawn above. +// Follows the logic of RPGSS to prevent confusion. +constexpr Drawable::Z_t default_priority = Priority_Weather; // Default to character layer. + +class ParticleEffect; + +namespace { + typedef std::map ptag_t; + ptag_t pfx_list; +} + +void linear_fade(ParticleEffect* effect, uint32_t color0, uint32_t color1, int fade, int delay); + +class ParticleEffect : public Drawable { +public: + enum class RenderType { + Screen, // 2D Screen Space (Overlay) + Map, // Mode7 World Space (Flat on ground) + Sprite // Mode7 World Space (Billboard/Upright) + }; + + ParticleEffect(); + ~ParticleEffect() override; + void Draw(Bitmap& dst) override {}; + + // Determines if input coordinates are relative to the screen (true) or map (false) + bool isScreenSpaceEffect; + + virtual void clear() {}; + virtual void setSimul(int newSimul) {}; + virtual void setAmount(int newAmount); + void setAngle(float v1, float v2); + void setSecondaryAngle(float v); + virtual void setTimeout(int fade, int delay); + void setRad(int new_rad); + void setSpd(float new_spd); + void setGrowth(float ini_size, float end_size); + void setRandRad(int new_rnd_rad); + void setRandSpd(float new_rnd_spd); + void setRandPos(int new_rnd_x, int new_rnd_y); + void setInterval(uint32_t new_interval); + virtual void setTexture(std::string filename); + virtual void unloadTexture(); + void useScreenRelative(bool enabled); + virtual void setGeneratingFunction(std::string type) {} + void setGravityDirection(float angle, float factor); + void setAccelerationPoint(float x, float y, float factor); + void setColor0(uint8_t r, uint8_t g, uint8_t b); + void setColor1(uint8_t r, uint8_t g, uint8_t b); + void setRenderType(RenderType type); + void setZOffset(int offset); + void setLayer(int layer); + + static void create_trig_lut(); + + std::array palette; + +protected: + bool isScreenRelative; + RenderType renderType; + int z_offset; + Drawable::Z_t base_z; + + int r0; + int rand_r; + int rand_x; + int rand_y; + + float spd; + float rand_spd; + + float s0; + float s1; + float ds; + float da; + + float gx; + float gy; + + float ax0; + float ay0; + float afc; + + BitmapRef image; + bool hasTexture; // Track if a texture is loaded + + float beta; + float alpha; + float theta; + uint8_t fade; + uint8_t delay; + uint16_t amount; + uint32_t color0; + uint32_t color1; + uint32_t interval = 1; + uint32_t cur_interval = 1; + + void update_color(); + static float sin_lut[32]; +}; + +void linear_fade(ParticleEffect* effect, uint32_t color0, uint32_t color1, int fade, int delay) { + float r = (color0 >> 16) & 0xff; + float g = (color0 >> 8) & 0xff; + float b = (color0 & 0xff); + if (delay >= fade) delay = fade - 1; + + float dr, dg, db; + { + float end_r = (color1 >> 16) & 0xff; + float end_g = (color1 >> 8) & 0xff; + float end_b = (color1 & 0xff); + + if (fade - delay > 0) { + dr = (end_r - r) / (fade - delay); + dg = (end_g - g) / (fade - delay); + db = (end_b - b) / (fade - delay); + } else { + dr = dg = db = 0.0f; + } + } + + int i = 0; + for (; i < delay; ++i) { + effect->palette[i] = Color(r, g, b, 255); + } + for (; i < fade; ++i) { + effect->palette[i] = Color(r, g, b, 255); + r += dr; + g += dg; + b += db; + } +} + +float ParticleEffect::sin_lut[32]; + +ParticleEffect::ParticleEffect() : Drawable(0), isScreenSpaceEffect(false), isScreenRelative(false), renderType(RenderType::Map), z_offset(0), r0(50), rand_r(0), rand_x(0), rand_y(0), spd(0.5), rand_spd(0.5), +s0(1), s1(1), ds(0), gx(0), gy(0), ax0(0), ay0(0), afc(0), beta(6.2832), +alpha(0), theta(0), fade(30), delay(0), amount(50) { + base_z = default_priority; + SetZ(base_z); + + da = 255.0f / fade; + color0 = 0x00ffffff; + color1 = 0x00ffffff; + image = Bitmap::Create(1, 1, true); + hasTexture = false; + + DrawableMgr::Register(this); +} + +ParticleEffect::~ParticleEffect() {} + +void ParticleEffect::setTexture(std::string filename) { + FileRequestAsync* req = AsyncHandler::RequestFile("Picture", filename); + req->Start(); + image = Cache::Picture(filename, true); + hasTexture = true; +} + +void ParticleEffect::unloadTexture() { + image = Bitmap::Create(1, 1, true); + hasTexture = false; + linear_fade(this, color0, color1, fade, delay); +} + +void ParticleEffect::setGravityDirection(float angle, float factor) { + angle *= 0.0174532925; + gx = factor * cosf(angle) / 600.0; + gy = factor * sinf(angle) / 600.0; +} + +void ParticleEffect::setAccelerationPoint(float x, float y, float factor) { + afc = factor / 600.0; +{ + ax0 = x; + ay0 = y; +} +} + +void ParticleEffect::setGrowth(float ini_size, float end_size) { + s0 = ini_size; + s1 = end_size; + if (fade > 0) ds = (s1 - s0) / fade; else ds = 0; +} + +void ParticleEffect::useScreenRelative(bool enabled) { + isScreenRelative = enabled; +} + +void ParticleEffect::setAmount(int newAmount) { + amount = newAmount; +} + +void ParticleEffect::setAngle(float v1, float v2) { + v1 *= 0.0174532925; + v2 *= 0.0174532925; + beta = (v2 < 0) ? -v2 : v2; + alpha = v1 - v2 / 2; + // Auto-detect screen space effect if 360 degree emission + if (beta == 0.0f) { + isScreenSpaceEffect = true; + } +} + +void ParticleEffect::setSecondaryAngle(float v) { + while (v > 360) v -= 360; + while (v < -360) v += 360; + theta = v * 0.0174532925; +} + +void ParticleEffect::setSpd(float new_spd) { + spd = new_spd / 60.0; +} + +void ParticleEffect::setRandSpd(float new_rnd_spd) { + rand_spd = new_rnd_spd / 60.0; +} + +void ParticleEffect::setRad(int new_rad) { + r0 = new_rad; +} + +void ParticleEffect::setRandPos(int new_rnd_x, int new_rnd_y) { + rand_x = (new_rnd_x < 0) ? -new_rnd_x : new_rnd_x; + rand_y = (new_rnd_y < 0) ? -new_rnd_y : new_rnd_y; +} + +void ParticleEffect::setRandRad(int new_rnd_rad) { + rand_r = (new_rnd_rad < 0) ? -new_rnd_rad : new_rnd_rad; +} + +void ParticleEffect::setTimeout(int new_fade, int new_delay) { + if (new_fade > 255) new_fade = 255; + else if (new_fade <= 0) new_fade = 1; + if (new_delay >= new_fade) new_delay = new_fade - 1; + else if (new_delay < 0) new_delay = 0; + this->fade = new_fade; + this->delay = new_delay; + da = 255.0f / new_fade; + ds = (s1 - s0) / new_fade; + update_color(); +} + +void ParticleEffect::setColor0(uint8_t r_val, uint8_t g_val, uint8_t b_val) { + color0 = (r_val << 16) | (g_val << 8) | b_val; + update_color(); +} + +void ParticleEffect::setColor1(uint8_t r_val, uint8_t g_val, uint8_t b_val) { + color1 = (r_val << 16) | (g_val << 8) | b_val; + update_color(); +} + +void ParticleEffect::setInterval(uint32_t new_interval) { + if (new_interval < 1) { + return; + } + cur_interval = new_interval; + interval = new_interval; +} + +void ParticleEffect::setRenderType(RenderType type) { + renderType = type; +} + +void ParticleEffect::setZOffset(int offset) { + z_offset = offset; +} + +void ParticleEffect::setLayer(int layer) { + if (Game_Battle::IsBattleRunning()) { + base_z = Drawable::GetPriorityForBattleLayer(layer); + } else { + base_z = Drawable::GetPriorityForMapLayer(layer); + } + // The final Z value, including offsets, will be set in the Draw method. +} + + +void ParticleEffect::update_color() { + linear_fade(this, color0, color1, fade, delay); +} + +void ParticleEffect::create_trig_lut() { + double dr = 3.141592653589793 / 16.0; + for (int i = 0; i < 32; i++) + sin_lut[i] = sin(dr * i); +} + +class Stream : public ParticleEffect { +public: + Stream(); + ~Stream() = default; + void Draw(Bitmap& dst) override; + void clear() override; + void stopAll(); + void stop(std::string tag); + void start(int x, int y, std::string tag); + + void setSimul(int newSimul) override; + void setAmount(int newAmount) override; + void setTimeout(int fade, int delay) override; + void setGeneratingFunction(std::string type) override; + void setPosition(std::string tag, int x, int y); + +private: + uint8_t simulBeg; + uint8_t simulRun; + uint8_t simulCnt; + uint16_t simulMax; + + std::vector x; + std::vector y; + std::vector s; + std::vector dx; + std::vector dy; + std::vector itr; + std::vector str_x; + std::vector str_y; + std::vector pfx_ref; + std::vector end_cnt; + + std::map pfx_tag; + + void resize(); + void stream_to_end(uint8_t idx); + void start_to_stream(uint8_t idx); + + void (Stream::*init)(int, int, int); + void draw_block(Bitmap& dst, int, uint8_t, uint8_t, uint8_t, int16_t, int16_t); + + void init_basic(int a, int b, int idx); + void init_radial(int a, int b, int idx); +}; + +Stream::Stream() : ParticleEffect(), simulBeg(0), simulRun(0), simulCnt(0), simulMax(1) { + amount = 10; + resize(); + init = &Stream::init_basic; + update_color(); +} + +void Stream::start(int x0, int y0, std::string tag) { + if (pfx_tag.count(tag)) return; + if (simulCnt >= simulMax) resize(); + + // Convert Screen coordinate to World coordinate if this is a screen-space effect (e.g. Radial) + // This ensures that when rendered in Mode7, the effect appears at the correct map location + + + uint8_t idx = pfx_ref[simulCnt]; + + std::swap(pfx_ref[simulCnt], pfx_ref[simulRun]); + std::swap(pfx_ref[simulRun], pfx_ref[simulBeg]); + + pfx_tag[tag] = idx; + + end_cnt[idx] = fade - 1; + str_x[idx] = x0; + str_y[idx] = y0; + itr[idx] = 0; + simulBeg++; + simulRun++; + simulCnt++; +} + +void Stream::stop(std::string tag) { + auto pfx_itr = pfx_tag.find(tag); + if (pfx_itr == pfx_tag.end()) return; + uint8_t probe = pfx_itr->second; + + auto it = std::find(pfx_ref.begin(), pfx_ref.begin() + simulCnt, probe); + if (it == pfx_ref.begin() + simulCnt) return; + + simulRun--; + std::swap(*it, pfx_ref[simulRun]); + pfx_tag.erase(pfx_itr); +} + +void Stream::stopAll() { + simulBeg = 0; + simulRun = 0; + pfx_tag.clear(); +} + +void Stream::clear() { + simulBeg = 0; + simulRun = 0; + simulCnt = 0; + pfx_tag.clear(); +} + +void Stream::setGeneratingFunction(std::string type) { + std::transform(type.begin(), type.end(), type.begin(), ::tolower); + if (!type.substr(0, 8).compare("standard")) { + init = &Stream::init_basic; + // Standard: Input is Map/World coordinates, defaults to Map Plane + isScreenSpaceEffect = false; + renderType = RenderType::Map; + return; + } + if (!type.substr(0, 6).compare("radial")) { + init = &Stream::init_radial; + // Radial: Input is Screen coordinates, defaults to Screen Plane (Overlay) + isScreenSpaceEffect = true; + renderType = RenderType::Screen; + return; + } +} + +void Stream::init_basic(int a, int b, int idx) { + float x0 = str_x[idx]; + float y0 = str_y[idx]; + for (int i = a; i < b; i++) { + x[i] = x0 + 2 * rand_x * (float)rand() / RAND_MAX - rand_x; + y[i] = y0 + 2 * rand_y * (float)rand() / RAND_MAX - rand_y; + s[i] = s0; + + float tmp_angle = (float)rand() / RAND_MAX * beta + alpha; + float tmp_spd = spd + rand_spd * (float)rand() / RAND_MAX; + int v = tmp_angle / 0.1963495408; + tmp_angle = (tmp_angle - v * 0.1963495408) / 0.1963495408; + dx[i] = tmp_spd * (sin_lut[(v + 9) & 31] * tmp_angle + sin_lut[(v + 8) & 31] * (1 - tmp_angle)); + dy[i] = tmp_spd * (sin_lut[(v + 1) & 31] * tmp_angle + sin_lut[(v + 0) & 31] * (1 - tmp_angle)); + } +} + +void Stream::init_radial(int a, int b, int idx) { + float x0 = str_x[idx]; + float y0 = str_y[idx]; + for (int i = a; i < b; i++) { + float tmp_rnd = rand_r * (float)rand() / RAND_MAX; + float tmp_angle = (float)rand() / RAND_MAX * beta + alpha; + float tmp_spd = spd + rand_spd * (float)rand() / RAND_MAX; + int v = tmp_angle / 0.1963495408; + float p = (tmp_angle - v * 0.1963495408) / 0.1963495408; + + x[i] = x0 + (r0 + tmp_rnd) * (sin_lut[(v + 9) & 31] * p + sin_lut[(v + 8) & 31] * (1 - p)); + y[i] = y0 + (r0 + tmp_rnd) * (sin_lut[(v + 1) & 31] * p + sin_lut[(v + 0) & 31] * (1 - p)); + s[i] = s0; + + v = (tmp_angle + theta) / 0.1963495408; + p = (tmp_angle + theta - v * 0.1963495408) / 0.1963495408; + dx[i] = -tmp_spd * (sin_lut[(v + 9) & 31] * p + sin_lut[(v + 8) & 31] * (1 - p)); + dy[i] = -tmp_spd * (sin_lut[(v + 1) & 31] * p + sin_lut[(v + 0) & 31] * (1 - p)); + } +} + +void Stream::draw_block(Bitmap& dst, int ref, uint8_t n, uint8_t z, uint8_t c0, int16_t cam_x, int16_t cam_y) { + { + // --- Original 2D drawing logic --- + for (uint8_t i = 0; i < n; i++) { + int age = i + c0; + if (age >= fade) continue; + + int alpha = static_cast(255 - da * age); + Color color = palette[age]; + + int block_start_idx = ref + z * amount; + + for (int j = 0; j < amount; j++) { + int p_idx = block_start_idx + j; + float size = s[p_idx]; + float draw_x = x[p_idx] - cam_x - size / 2.0f; + float draw_y = y[p_idx] - cam_y - size / 2.0f - z_offset; + + Rect dst_rect(draw_x, draw_y, size, size); + + if (hasTexture) { + dst.StretchBlit(dst_rect, *image, image->GetRect(), Opacity(alpha)); + } else { + dst.FillRect(dst_rect, Color(color.red, color.green, color.blue, alpha)); + } + } + z = (z + 1) % fade; + } + } +} + +void Stream::setPosition(std::string tag, int x_pos, int y_pos) { + auto pfx_itr = pfx_tag.find(tag); + if (pfx_itr == pfx_tag.end()) return; + uint8_t probe = pfx_itr->second; + + auto it = std::find(pfx_ref.begin(), pfx_ref.begin() + simulCnt, probe); + if (it == pfx_ref.begin() + simulCnt) return; + auto i = std::distance(pfx_ref.begin(), it); + str_x[i] = x_pos; + str_y[i] = y_pos; +} + +void Stream::Draw(Bitmap& dst) { + if (simulCnt <= 0) return; + int cam_x = (isScreenRelative) ? 0 : Game_Map::GetDisplayX() / 16; + int cam_y = (isScreenRelative) ? 0 : Game_Map::GetDisplayY() / 16; + int block_size = amount * fade; + + // --- Physics Update Section --- + for (int i = 0; i < simulCnt; ++i) { + uint8_t p_ref = pfx_ref[i]; + int base_idx = p_ref * block_size; + for (int j = 0; j < block_size; ++j) { + int p_idx = base_idx + j; + x[p_idx] += dx[p_idx]; + y[p_idx] += dy[p_idx]; + float tx = ax0 - x[p_idx]; + float ty = ay0 - y[p_idx]; + float tsqr = sqrtf(tx*tx + ty*ty + 0.001); + dx[p_idx] += gx + afc * tx / tsqr; + dy[p_idx] += gy + afc * ty / tsqr; + s[p_idx] += ds; + } + } + + // --- Spawning Section --- + --cur_interval; + if (cur_interval == 0) { + for (int i = simulBeg; i < simulRun; i++) { + uint8_t idx = pfx_ref[i]; + uint8_t z = fade - itr[idx] - 1; + (this->*init)(z * amount + idx * block_size, (z + 1) * amount + idx * block_size, idx); + } + cur_interval = interval; + } + + // --- Drawing & Z-Update Section --- + { + SetZ(base_z + z_offset); + } + + int i = 0; + // Starting + for (; i < simulBeg; i++) { + uint8_t idx = pfx_ref[i]; + if (itr[idx] < fade) { + uint8_t z = fade - itr[idx] - 1; + (this->*init)(z * amount + idx * block_size, (z + 1) * amount + idx * block_size, idx); + + itr[idx]++; + draw_block(dst, idx * block_size, itr[idx], z, 0, cam_x, cam_y); + } + else start_to_stream(i--); + } + // Streaming + for (; i < simulRun; i++) { + uint8_t idx = pfx_ref[i]; + uint8_t z = fade - itr[idx] - 1; + itr[idx] = (itr[idx] + 1) % fade; + draw_block(dst, idx * block_size, fade, z, 0, cam_x, cam_y); + } + // Stopping + for (; i < simulCnt; i++) { + uint8_t idx = pfx_ref[i]; + uint8_t z = (fade - itr[idx]) % fade; + draw_block(dst, idx * block_size, end_cnt[idx]--, z, fade - end_cnt[idx], cam_x, cam_y); + if (end_cnt[idx] <= 0) + stream_to_end(i); + } +} + +void Stream::resize() { + simulMax *= 2; + size_t particle_pool_size = static_cast(amount) * fade * simulMax; + + x.resize(particle_pool_size); + y.resize(particle_pool_size); + s.resize(particle_pool_size); + dx.resize(particle_pool_size); + dy.resize(particle_pool_size); + + itr.resize(simulMax); + str_x.resize(simulMax); + str_y.resize(simulMax); + pfx_ref.resize(simulMax); + end_cnt.resize(simulMax); + + for (int i = simulMax / 2; i < simulMax; i++) { + pfx_ref[i] = i; + } +} + +void Stream::setAmount(int newAmount) { + amount = newAmount; + resize(); +} + +void Stream::setSimul(int newSimul) { + simulMax = newSimul; + resize(); + simulBeg = 0; + simulRun = 0; + simulCnt = 0; +} + +void Stream::setTimeout(int _fade, int _delay) { + if (_fade > 255) _fade = 255; + else if (_fade <= 0) _fade = 1; + if (_delay >= _fade) _delay = _fade - 1; + else if (_delay < 0) _delay = 0; + fade = _fade; + delay = _delay; + da = 255.0f / _fade; + ds = (s1 - s0) / _fade; + resize(); + update_color(); +} + +void Stream::start_to_stream(uint8_t idx) { + itr[pfx_ref[idx]] = 0; + --simulBeg; + std::swap(pfx_ref[simulBeg], pfx_ref[idx]); +} + +void Stream::stream_to_end(uint8_t idx) { + --simulCnt; + std::swap(pfx_ref[simulCnt], pfx_ref[idx]); +} + +class Burst : public ParticleEffect { +public: + Burst(); + ~Burst() = default; + void Draw(Bitmap& dst) override; + void clear() override; + void newBurst(int x, int y); + + void setSimul(int newSimul) override; + void setAmount(int newAmount) override; + void setGeneratingFunction(std::string type) override; + +private: + uint8_t simulCnt; + uint16_t simulMax; + + std::vector x; + std::vector y; + std::vector s; + std::vector dx; + std::vector dy; + std::vector itr; + std::vector origins; + + void resize(); + + void (Burst::*init)(int, int, int, int); + void (Burst::*draw_function)(Bitmap& dst, int, int); + + void init_basic(int x0, int y0, int a, int b); + void init_radial(int x0, int y0, int a, int b); + void draw_standard(Bitmap& dst, int cam_x, int cam_y); +}; + +Burst::Burst() : ParticleEffect(), simulCnt(0), simulMax(1) { + resize(); + init = &Burst::init_basic; + draw_function = &Burst::draw_standard; + update_color(); +} + +void Burst::setGeneratingFunction(std::string type) { + std::transform(type.begin(), type.end(), type.begin(), ::tolower); + if (!type.substr(0, 8).compare("standard")) { + init = &Burst::init_basic; + // Standard: Input is Map/World coordinates, defaults to Map Plane + isScreenSpaceEffect = false; + renderType = RenderType::Map; + return; + } + if (!type.substr(0, 6).compare("radial")) { + init = &Burst::init_radial; + // Radial: Input is Screen coordinates, defaults to Screen Plane (Overlay) + isScreenSpaceEffect = true; + renderType = RenderType::Screen; + return; + } +} + +void Burst::clear() { + simulCnt = 0; +} + +void Burst::newBurst(int x0, int y0) { + if (simulCnt >= simulMax) resize(); + + + itr[simulCnt] = 0; + origins[simulCnt] = { (float)x0, (float)y0 }; + + (this->*init)(x0, y0, simulCnt * amount, (simulCnt + 1) * amount); + simulCnt++; +} + +void Burst::init_basic(int x0, int y0, int a, int b) { + for (int i = a; i < b; i++) { + x[i] = x0 + 2 * rand_x * (float)rand() / RAND_MAX - rand_x; + y[i] = y0 + 2 * rand_y * (float)rand() / RAND_MAX - rand_y; + s[i] = s0; + + float tmp_angle = (float)rand() / RAND_MAX * beta + alpha; + float tmp_spd = spd + rand_spd * (float)rand() / RAND_MAX; + int v = tmp_angle / 0.1963495408; + tmp_angle = (tmp_angle - v * 0.1963495408) / 0.1963495408; + dx[i] = tmp_spd * (sin_lut[(v + 9) & 31] * tmp_angle + sin_lut[(v + 8) & 31] * (1 - tmp_angle)); + dy[i] = tmp_spd * (sin_lut[(v + 1) & 31] * tmp_angle + sin_lut[(v + 0) & 31] * (1 - tmp_angle)); + } +} + +void Burst::init_radial(int x0, int y0, int a, int b) { + for (int i = a; i < b; i++) { + float tmp_rnd = rand_r * (float)rand() / RAND_MAX; + float tmp_angle = (float)rand() / RAND_MAX * beta + alpha; + float tmp_spd = spd + rand_spd * (float)rand() / RAND_MAX; + int v = tmp_angle / 0.1963495408; + float p = (tmp_angle - v * 0.1963495408) / 0.1963495408; + + x[i] = x0 + (r0 + tmp_rnd) * (sin_lut[(v + 9) & 31] * p + sin_lut[(v + 8) & 31] * (1 - p)); + y[i] = y0 + (r0 + tmp_rnd) * (sin_lut[(v + 1) & 31] * p + sin_lut[(v + 0) & 31] * (1 - p)); + s[i] = s0; + + v = (tmp_angle + theta) / 0.1963495408; + p = (tmp_angle + theta - v * 0.1963495408) / 0.1963495408; + dx[i] = -tmp_spd * (sin_lut[(v + 9) & 31] * p + sin_lut[(v + 8) & 31] * (1 - p)); + dy[i] = -tmp_spd * (sin_lut[(v + 1) & 31] * p + sin_lut[(v + 0) & 31] * (1 - p)); + } +} + +void Burst::draw_standard(Bitmap& dst, int cam_x, int cam_y) { + for (int i = 0; i < simulCnt; i++) { + int age = itr[i]; + if (age >= fade) continue; + + itr[i]++; + int alpha = static_cast(255 - da * age); + Color color = palette[age]; + + float tx, ty, tsqr; + for (int j = i * amount; j < (i + 1) * amount; j++) { + x[j] += dx[j]; + y[j] += dy[j]; + tx = ax0 - x[j]; + ty = ay0 - y[j]; + tsqr = sqrtf(tx*tx + ty*ty + 0.001); + dx[j] += gx + afc * tx / tsqr; + dy[j] += gy + afc * ty / tsqr; + s[j] += ds; + Rect dst_rect(x[j] - cam_x - s[j] / 2, y[j] - cam_y - s[j] / 2 - z_offset, s[j], s[j]); + + if (hasTexture) { + dst.StretchBlit(dst_rect, *image, image->GetRect(), Opacity(alpha)); + } else { + dst.FillRect(dst_rect, Color(color.red, color.green, color.blue, alpha)); + } + } + } +} + +void Burst::resize() { + simulMax *= 2; + size_t particle_pool_size = static_cast(amount) * simulMax; + + x.resize(particle_pool_size); + y.resize(particle_pool_size); + s.resize(particle_pool_size); + dx.resize(particle_pool_size); + dy.resize(particle_pool_size); + itr.resize(simulMax); + origins.resize(simulMax); +} + +void Burst::setAmount(int newAmount) { + amount = newAmount; + resize(); +} + +void Burst::setSimul(int newSimul) { + simulMax = newSimul; + resize(); + simulCnt = 0; +} + +void Burst::Draw(Bitmap& dst) { + if (simulCnt <= 0) return; + + // Recycle dead bursts + for (int i = 0; i < simulCnt; ++i) { + if (itr[i] >= fade) { + simulCnt--; + if (i < simulCnt) { // If it's not the last one + // Copy the last active burst over the dead one + size_t dead_offset = i * amount; + size_t last_offset = simulCnt * amount; + std::copy_n(&x[last_offset], amount, &x[dead_offset]); + std::copy_n(&y[last_offset], amount, &y[dead_offset]); + std::copy_n(&s[last_offset], amount, &s[dead_offset]); + std::copy_n(&dx[last_offset], amount, &dx[dead_offset]); + std::copy_n(&dy[last_offset], amount, &dy[dead_offset]); + itr[i] = itr[simulCnt]; + origins[i] = origins[simulCnt]; + } + --i; // Re-check this index in case the swapped one was also dead + } + } + + int cam_x = (isScreenRelative) ? 0 : Game_Map::GetDisplayX() / 16; + int cam_y = (isScreenRelative) ? 0 : Game_Map::GetDisplayY() / 16; + + { + // Original 2D drawing logic + SetZ(base_z + z_offset); + (this->*draw_function)(dst, cam_x, cam_y); + } +} +// ============================================================================ +// DynRPG Plugin Interface Implementation +// ============================================================================ +namespace { + std::map> function_list; + + ParticleEffect* GetPfx(const std::string& tag) { + auto it = pfx_list.find(tag); + if (it != pfx_list.end()) { + return it->second; + } + Output::Debug("DynParticle: Particle effect with tag '{}' not found.", tag.c_str()); + return nullptr; + } +} + +static bool create_effect(dyn_arg_list args) { + auto func = "pfx_create_effect"; + bool okay; + std::string tag, type; + std::tie(tag, type) = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + if (pfx_list.count(tag)) return true; + std::transform(type.begin(), type.end(), type.begin(), ::tolower); + if (!type.substr(0, 5).compare("burst")) pfx_list[tag] = new Burst(); + else if (!type.substr(0, 6).compare("stream")) pfx_list[tag] = new Stream(); + return true; +} + +static bool destroy_effect(dyn_arg_list args) { + auto func = "pfx_destroy_effect"; + bool okay; + auto [tag] = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + auto itr = pfx_list.find(tag); + if (itr != pfx_list.end()) { + delete itr->second; + pfx_list.erase(itr); + } + return true; +} + +static bool destroy_all(dyn_arg_list) { + for (auto const& [tag, pfx] : pfx_list) { + delete pfx; + } + pfx_list.clear(); + return true; +} + +static bool does_effect_exist(dyn_arg_list args) { + auto func = "pfx_does_effect_exist"; + bool okay; + std::string tag; + int idx; + std::tie(tag, idx) = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + Main_Data::game_switches->Set(idx, pfx_list.count(tag)); + Game_Map::SetNeedRefresh(true); + return true; +} + +static bool burst(dyn_arg_list args) { + auto func = "pfx_burst"; + bool okay; + std::string tag; + int x, y; + std::tie(tag, x, y) = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + if (auto pfx = GetPfx(tag)) { + if (auto b = dynamic_cast(pfx)) { + b->newBurst(x, y); + } + } + return true; +} + +static bool start(dyn_arg_list args) { + auto func = "pfx_start"; + bool okay; + std::string tag1, tag2; + int x, y; + std::tie(tag1, tag2, x, y) = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + if (auto pfx = GetPfx(tag1)) { + if (auto s = dynamic_cast(pfx)) { + s->start(x, y, tag2); + } + } + return true; +} + +static bool stop(dyn_arg_list args) { + auto func = "pfx_stop"; + bool okay; + std::string tag1, tag2; + std::tie(tag1, tag2) = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + if (auto pfx = GetPfx(tag1)) { + if (auto s = dynamic_cast(pfx)) { + s->stop(tag2); + } + } + return true; +} + +static bool stopall(dyn_arg_list args) { + auto func = "pfx_stopall"; + bool okay; + auto [tag] = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + if (auto pfx = GetPfx(tag)) { + if (auto s = dynamic_cast(pfx)) { + s->stopAll(); + } + } + return true; +} + +static bool set_position(dyn_arg_list args) { + auto func = "pfx_set_position"; + bool okay; + std::string tag1, tag2; + int x, y; + std::tie(tag1, tag2, x, y) = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + if (auto pfx = GetPfx(tag1)) { + if (auto s = dynamic_cast(pfx)) { + s->setPosition(tag2, x, y); + } + } + return true; +} + +static bool set_interval(dyn_arg_list args) { + auto func = "pfx_set_interval"; + bool okay; + std::string tag; + int interval; + std::tie(tag, interval) = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + if (auto pfx = GetPfx(tag)) { + pfx->setInterval(interval); + } + return true; +} + +static bool load_effect(dyn_arg_list) { + return true; +} + +static bool SetZ(dyn_arg_list args) { + auto func = "pfx_set_z_offset"; + bool okay; + std::string tag; + int z; + std::tie(tag, z) = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + if (auto pfx = GetPfx(tag)) { + pfx->setZOffset(z); + } + return true; +} + +static bool SetLayer(dyn_arg_list args) { + auto func = "pfx_set_layer"; + bool okay; + std::string tag; + int layer; + std::tie(tag, layer) = DynRpg::ParseArgs(func, args, &okay); + if (!okay) return true; + if (auto pfx = GetPfx(tag)) { + pfx->setLayer(layer); + } + return true; +} + +// --- DynRpg::Particle Class Implementation --- + +DynRpg::Particle::Particle(Game_DynRpg& instance) : DynRpgPlugin("KazeParticles", instance) { + ParticleEffect::create_trig_lut(); + + if (function_list.empty()) { + function_list["pfx_destroy_all"] = &destroy_all; + function_list["pfx_create_effect"] = &create_effect; + function_list["pfx_destroy_effect"] = &destroy_effect; + function_list["pfx_does_effect_exist"] = &does_effect_exist; + function_list["pfx_burst"] = &burst; + function_list["pfx_start"] = &start; + function_list["pfx_stop"] = &stop; + function_list["pfx_stopall"] = &stopall; + + auto add_setter_1_int = [](const char* name, void (ParticleEffect::*setter)(int)) { + function_list[name] = [name, setter](dyn_arg_list args) { + bool okay; std::string tag; int val; + std::tie(tag, val) = DynRpg::ParseArgs(name, args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) (pfx->*setter)(val); + return true; + }; + }; + auto add_setter_2_int = [](const char* name, void (ParticleEffect::*setter)(int, int)) { + function_list[name] = [name, setter](dyn_arg_list args) { + bool okay; std::string tag; int val1, val2; + std::tie(tag, val1, val2) = DynRpg::ParseArgs(name, args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) (pfx->*setter)(val1, val2); + return true; + }; + }; + auto add_setter_2_float = [](const char* name, void (ParticleEffect::*setter)(float, float)) { + function_list[name] = [name, setter](dyn_arg_list args) { + bool okay; std::string tag; float val1, val2; + std::tie(tag, val1, val2) = DynRpg::ParseArgs(name, args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) (pfx->*setter)(val1, val2); + return true; + }; + }; + auto add_setter_3_int = [](const char* name, void (ParticleEffect::*setter)(uint8_t, uint8_t, uint8_t)) { + function_list[name] = [name, setter](dyn_arg_list args) { + bool okay; std::string tag; int r, g, b; + std::tie(tag, r, g, b) = DynRpg::ParseArgs(name, args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) (pfx->*setter)(r, g, b); + return true; + }; + }; + + add_setter_1_int("pfx_set_simul_effects", &ParticleEffect::setSimul); + add_setter_1_int("pfx_set_amount", &ParticleEffect::setAmount); + add_setter_2_int("pfx_set_timeout", &ParticleEffect::setTimeout); + add_setter_1_int("pfx_set_random_radius", &ParticleEffect::setRandRad); + add_setter_1_int("pfx_set_radius", &ParticleEffect::setRad); + add_setter_2_int("pfx_set_random_position", &ParticleEffect::setRandPos); + add_setter_3_int("pfx_set_initial_color", &ParticleEffect::setColor0); + add_setter_3_int("pfx_set_final_color", &ParticleEffect::setColor1); + add_setter_2_float("pfx_set_growth", &ParticleEffect::setGrowth); + add_setter_2_float("pfx_set_angle", &ParticleEffect::setAngle); + + function_list["pfx_set_velocity"] = [](dyn_arg_list args) { + bool okay; std::string tag; float speed, rand_speed; + std::tie(tag, speed, rand_speed) = DynRpg::ParseArgs("pfx_set_velocity", args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) { pfx->setSpd(speed); pfx->setRandSpd(rand_speed); } + return true; + }; + function_list["pfx_set_texture"] = [](dyn_arg_list args) { + bool okay; std::string tag, texture; + std::tie(tag, texture) = DynRpg::ParseArgs("pfx_set_texture", args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) pfx->setTexture(texture); + return true; + }; + function_list["pfx_set_acceleration_point"] = [](dyn_arg_list args) { + bool okay; std::string tag; float x, y, force; + std::tie(tag, x, y, force) = DynRpg::ParseArgs("pfx_set_acceleration_point", args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) pfx->setAccelerationPoint(x, y, force); + return true; + }; + function_list["pfx_set_gravity_direction"] = [](dyn_arg_list args) { + bool okay; std::string tag; float angle, force; + std::tie(tag, angle, force) = DynRpg::ParseArgs("pfx_set_gravity_direction", args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) pfx->setGravityDirection(angle, force); + return true; + }; + function_list["pfx_set_secondary_angle"] = [](dyn_arg_list args) { + bool okay; std::string tag; float angle; + std::tie(tag, angle) = DynRpg::ParseArgs("pfx_set_secondary_angle", args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) pfx->setSecondaryAngle(angle); + return true; + }; + function_list["pfx_set_generating_function"] = [](dyn_arg_list args) { + bool okay; std::string tag, type; + std::tie(tag, type) = DynRpg::ParseArgs("pfx_set_generating_function", args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) pfx->setGeneratingFunction(type); + return true; + }; + function_list["pfx_unload_texture"] = [](dyn_arg_list args) { + bool okay; std::string tag; + std::tie(tag) = DynRpg::ParseArgs("pfx_unload_texture", args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) pfx->unloadTexture(); + return true; + }; + function_list["pfx_use_screen_relative"] = [](dyn_arg_list args) { + bool okay; std::string tag, val; + std::tie(tag, val) = DynRpg::ParseArgs("pfx_use_screen_relative", args, &okay); + if (okay) if (auto pfx = GetPfx(tag)) pfx->useScreenRelative(val[0] == 't' || val[0] == 'T'); + return true; + }; + function_list["pfx_set_position"] = &set_position; + function_list["pfx_set_interval"] = &set_interval; + function_list["pfx_load_effect"] = &load_effect; + function_list["pfx_set_z_offset"] = &SetZ; + function_list["pfx_set_layer"] = &SetLayer; + function_list["pfx_set_render_plane"] = [](dyn_arg_list args) { + bool okay; std::string tag, plane; + std::tie(tag, plane) = DynRpg::ParseArgs("pfx_set_render_plane", args, &okay); + if (okay) { + if (auto pfx = GetPfx(tag)) { + std::transform(plane.begin(), plane.end(), plane.begin(), ::tolower); + if (plane == "screen") { + pfx->setRenderType(ParticleEffect::RenderType::Screen); + // Force screen coordinates for generating functions like Radial + pfx->isScreenSpaceEffect = true; + } else if (plane == "sprite" || plane == "event") { + pfx->setRenderType(ParticleEffect::RenderType::Sprite); + } else { + // Default to map plane + pfx->setRenderType(ParticleEffect::RenderType::Map); + } + } + } + return true; + }; + } +} + +DynRpg::Particle::~Particle() { + destroy_all({}); +} + +bool DynRpg::Particle::Invoke(std::string_view func, dyn_arg_list args, bool&, Game_Interpreter*) { + auto it = function_list.find(func); + if (it != function_list.end()) { + return it->second(args); + } + return false; +} + +void DynRpg::Particle::Update() { + if (!pfx_list.empty()) { + if ((Scene::instance && Scene::instance->type == Scene::Map) || Game_Battle::IsBattleRunning()) { + // Drawing is handled automatically by the DrawableMgr + } + } +} + +void DynRpg::Particle::OnMapChange() { + for (auto const& [tag, pfx] : pfx_list) { + pfx->clear(); + } +} diff --git a/src/dynrpg_particleV2.h b/src/dynrpg_particleV2.h new file mode 100644 index 0000000000..74f15683ec --- /dev/null +++ b/src/dynrpg_particleV2.h @@ -0,0 +1,25 @@ +/* + * This file is part of EasyRPG Player. + * ... (license header) ... + * Based on DynRPG Particle Effects by Kazesui. (MIT license) + */ + +#ifndef EP_DYNRPG_PARTICLE_H_ +#define EP_DYNRPG_PARTICLE_H_ + +#include "game_dynrpg.h" + +namespace DynRpg { + class Particle : public DynRpgPlugin { + public: + Particle(Game_DynRpg& instance); // <- Body removed, this is now a declaration + ~Particle() override; + + bool Invoke(std::string_view func, dyn_arg_list args, bool& do_yield, Game_Interpreter* interpreter) override; + void Update() override; + + void OnMapChange(); + }; +} + +#endif diff --git a/src/game_dynrpg.cpp b/src/game_dynrpg.cpp index bb4a6263f6..de633c7d1d 100644 --- a/src/game_dynrpg.cpp +++ b/src/game_dynrpg.cpp @@ -30,6 +30,9 @@ #include "dynrpg_easyrpg.h" #include "dynrpg_textplugin.h" +#include "dynrpg_particleV1.h" +#include "dynrpg_particleV2.h" + enum DynRpg_ParseMode { ParseMode_Function, @@ -180,12 +183,19 @@ void Game_DynRpg::InitPlugins() { } if (Player::IsPatchDynRpg() || Player::HasEasyRpgExtensions()) { - plugins.emplace_back(new DynRpg::EasyRpgPlugin(*this)); + plugins.emplace_back(new DynRpg::EasyRpgPlugin(*this)); + plugins.emplace_back(new DynRpg::ParticleV1(*this)); } if (Player::IsPatchDynRpg()) { plugins.emplace_back(new DynRpg::TextPlugin(*this)); } + + if (Player::IsPatchDynRpg()) { + plugins.emplace_back(new DynRpg::Particle(*this)); + } + + plugins_loaded = true; } From fe4829e273330ba80e3d2e1b9e34ead49ccb8abe Mon Sep 17 00:00:00 2001 From: LizardKing777 <154367673+LizardKing777@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:25:55 -0600 Subject: [PATCH 2/3] Update to sync with acceleration keys And added EasyRPG Distribution/GNU File info at top. Still trying to figure out Version 2 (I can get it to sync, but then we end up with a Vector Substack out of range error.) --- src/dynrpg_particleV1.cpp | 750 +++++++++++++++++++------------------- src/dynrpg_particleV1.h | 32 +- 2 files changed, 405 insertions(+), 377 deletions(-) diff --git a/src/dynrpg_particleV1.cpp b/src/dynrpg_particleV1.cpp index cb1d31c520..350d496bb9 100644 --- a/src/dynrpg_particleV1.cpp +++ b/src/dynrpg_particleV1.cpp @@ -1,6 +1,18 @@ -/* - * This file is part of EasyRPG Player. - * ... (license header) ... +/* + * 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 . * Based on DynRPG Particle Effects V1 by Kazesui. (MIT license) */ @@ -28,388 +40,392 @@ constexpr double PI = 3.14159265358979323846; namespace { - struct ParticleObj { - double size; - double x, dx, y, dy; - }; - - struct BurstObj { - double r, g, b, alpha; - std::list particles; - }; - - class ParticleEffectV1 : public Drawable { - public: - int amount, size; - int red, green, blue; - int rndX, rndY; - double spd, rndSpd; - double timeout, delay; - double angleS, angleE; - bool mask; - std::string filename; - - double dr, dg, db, ds, dA; - - std::list bursts; - BitmapRef image; - bool hasTexture; - - ParticleEffectV1() : Drawable(Priority_Weather) { - dr = dg = db = ds = 0; - red = green = blue = 255; - timeout = 30; - amount = 50; - delay = 0; - size = 2; - dA = 8.5; - spd = 2; - rndSpd = 2; - rndX = rndY = 0; - angleS = 0; - angleE = 2 * PI; - mask = false; - filename = "Particle"; - hasTexture = false; - - DrawableMgr::Register(this); - } - - ~ParticleEffectV1() override {} - - void setTimeout(double t) { - double time_diff = t - delay; - if (time_diff <= 0) time_diff = 1.0; - - dA = 255.0 / time_diff; - timeout = t; - } - - void setAngle(double a1, double a2) { - angleS = a1 * PI / 180.0; - angleE = (a2 - a1) * PI / 180.0; - } - - void setGrowth(int s, int newSize) { - size = s; - double t = (timeout > 0) ? timeout : 1.0; - ds = (newSize - size) / t; - } - - void colorFade(int r = 0, int g = 0, int b = 0) { - double t = (timeout > 0) ? timeout : 1.0; - dr = ((double)r - red) / t; - dg = ((double)g - green) / t; - db = ((double)b - blue) / t; - } - - void loadTexture() { - std::string name = filename; - - // Emulate RM2k3 legacy relative paths - size_t pos = name.find("DynPlugins/"); - if (pos != std::string::npos) name.erase(pos, 11); - pos = name.find("DynPlugins\\"); - if (pos != std::string::npos) name.erase(pos, 11); - - // Prevent Picture/Picture/ overlap - pos = name.find("Picture/"); - if (pos != std::string::npos) name.erase(pos, 8); - pos = name.find("Picture\\"); - if (pos != std::string::npos) name.erase(pos, 8); - - // Strip extension for EasyRPG Cache compatibility - pos = name.find_last_of("."); - if (pos != std::string::npos) name = name.substr(0, pos); - - if (!name.empty()) { - FileRequestAsync* req = AsyncHandler::RequestFile("Picture", name); - req->Start(); - image = Cache::Picture(name, mask); - hasTexture = true; - } - } - - void newBurst(int x, int y) { - BurstObj burst; - burst.r = red; - burst.g = green; - burst.b = blue; - burst.alpha = 255.0; - - if (!hasTexture && !filename.empty()) { - loadTexture(); - } - - for (int i = 0; i < amount; i++) { - double rand_val1 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; - double rand_val2 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; - double rand_val3 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; - double rand_val4 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; - - double rnd = angleS + rand_val1 * angleE; - double newSpd = (spd + rndSpd * rand_val2); - - ParticleObj pa; - pa.x = x + 2 * rndX * rand_val3 - rndX; - pa.y = y + 2 * rndY * rand_val4 - rndY; - pa.dx = newSpd * std::cos(rnd); - pa.dy = newSpd * std::sin(rnd); - pa.size = size; - - burst.particles.push_back(pa); - } - bursts.push_back(burst); - } - - void newHeart(int x, int y) { - BurstObj burst; - burst.r = red; - burst.g = green; - burst.b = blue; - burst.alpha = 255.0; - - if (!hasTexture && !filename.empty()) { - loadTexture(); - } - - for (int i = 0; i < amount; i++) { - double rand_val = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; - double rnd = rand_val * 2 * PI; - - ParticleObj pa; - pa.x = x; - pa.y = y; - pa.dx = spd * (16 * std::pow(std::sin(rnd), 3)); - pa.dy = -spd * (13 * std::cos(rnd) - 5 * std::cos(2 * rnd) - 2 * std::cos(3 * rnd) - std::cos(4 * rnd)); - pa.size = size; - - burst.particles.push_back(pa); - } - bursts.push_back(burst); - } - - void killParticles() { - bursts.clear(); - } - - void Draw(Bitmap& dst) override; - }; - - std::vector v1_effects; - bool v1_draw = true; + struct ParticleObj { + double size; + double x, dx, y, dy; + }; + + struct BurstObj { + double r, g, b, alpha; + std::list particles; + }; + + class ParticleEffectV1 : public Drawable { + public: + int amount, size; + int red, green, blue; + int rndX, rndY; + double spd, rndSpd; + double timeout, delay; + double angleS, angleE; + bool mask; + std::string filename; + + double dr, dg, db, ds, dA; + + std::list bursts; + BitmapRef image; + bool hasTexture; + + ParticleEffectV1() : Drawable(Priority_Weather) { + dr = dg = db = ds = 0; + red = green = blue = 255; + timeout = 30; + amount = 50; + delay = 0; + size = 2; + dA = 8.5; + spd = 2; + rndSpd = 2; + rndX = rndY = 0; + angleS = 0; + angleE = 2 * PI; + mask = false; + filename = "Particle"; + hasTexture = false; + + DrawableMgr::Register(this); + } + + ~ParticleEffectV1() override {} + + void setTimeout(double t) { + double time_diff = t - delay; + if (time_diff <= 0) time_diff = 1.0; + + dA = 255.0 / time_diff; + timeout = t; + } + + void setAngle(double a1, double a2) { + angleS = a1 * PI / 180.0; + angleE = (a2 - a1) * PI / 180.0; + } + + void setGrowth(int s, int newSize) { + size = s; + double t = (timeout > 0) ? timeout : 1.0; + ds = (newSize - size) / t; + } + + void colorFade(int r = 0, int g = 0, int b = 0) { + double t = (timeout > 0) ? timeout : 1.0; + dr = ((double)r - red) / t; + dg = ((double)g - green) / t; + db = ((double)b - blue) / t; + } + + void loadTexture() { + std::string name = filename; + + // Emulate RM2k3 legacy relative paths + size_t pos = name.find("DynPlugins/"); + if (pos != std::string::npos) name.erase(pos, 11); + pos = name.find("DynPlugins\\"); + if (pos != std::string::npos) name.erase(pos, 11); + + // Prevent Picture/Picture/ overlap + pos = name.find("Picture/"); + if (pos != std::string::npos) name.erase(pos, 8); + pos = name.find("Picture\\"); + if (pos != std::string::npos) name.erase(pos, 8); + + // Strip extension for EasyRPG Cache compatibility + pos = name.find_last_of("."); + if (pos != std::string::npos) name = name.substr(0, pos); + + if (!name.empty()) { + FileRequestAsync* req = AsyncHandler::RequestFile("Picture", name); + req->Start(); + image = Cache::Picture(name, mask); + hasTexture = true; + } + } + + void newBurst(int x, int y) { + BurstObj burst; + burst.r = red; + burst.g = green; + burst.b = blue; + burst.alpha = 255.0; + + if (!hasTexture && !filename.empty()) { + loadTexture(); + } + + for (int i = 0; i < amount; i++) { + double rand_val1 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + double rand_val2 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + double rand_val3 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + double rand_val4 = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + + double rnd = angleS + rand_val1 * angleE; + double newSpd = (spd + rndSpd * rand_val2); + + ParticleObj pa; + pa.x = x + 2 * rndX * rand_val3 - rndX; + pa.y = y + 2 * rndY * rand_val4 - rndY; + pa.dx = newSpd * std::cos(rnd); + pa.dy = newSpd * std::sin(rnd); + pa.size = size; + + burst.particles.push_back(pa); + } + bursts.push_back(burst); + } + + void newHeart(int x, int y) { + BurstObj burst; + burst.r = red; + burst.g = green; + burst.b = blue; + burst.alpha = 255.0; + + if (!hasTexture && !filename.empty()) { + loadTexture(); + } + + for (int i = 0; i < amount; i++) { + double rand_val = (double)Rand::GetRandomNumber(0, 32767) / 32767.0; + double rnd = rand_val * 2 * PI; + + ParticleObj pa; + pa.x = x; + pa.y = y; + pa.dx = spd * (16 * std::pow(std::sin(rnd), 3)); + pa.dy = -spd * (13 * std::cos(rnd) - 5 * std::cos(2 * rnd) - 2 * std::cos(3 * rnd) - std::cos(4 * rnd)); + pa.size = size; + + burst.particles.push_back(pa); + } + bursts.push_back(burst); + } + + void killParticles() { + bursts.clear(); + } + + void Update() { + if (bursts.empty()) return; + + auto burstItr = bursts.begin(); + while (burstItr != bursts.end()) { + // 1. Color Fade physics + burstItr->r += dr; + burstItr->g += dg; + burstItr->b += db; + + // 2. Alpha fade physics + if (this->delay <= 0) { + burstItr->alpha -= dA; + if (burstItr->alpha <= 0) { + burstItr = bursts.erase(burstItr); + continue; + } + } else { + this->delay--; + } + + // 3. Particle Movement physics + auto partItr = burstItr->particles.begin(); + while (partItr != burstItr->particles.end()) { + partItr->x += partItr->dx; + partItr->y += partItr->dy; + partItr->size += ds; + partItr++; + } + burstItr++; + } + } + + void Draw(Bitmap& dst) override; + }; + + std::vector v1_effects; + bool v1_draw = true; } // anonymous namespace void ParticleEffectV1::Draw(Bitmap& dst) { - if (!v1_draw || bursts.empty()) return; - - int cam_x = Game_Map::GetDisplayX() / 16; - int cam_y = Game_Map::GetDisplayY() / 16; - - auto burstItr = bursts.begin(); - while (burstItr != bursts.end()) { - - // 1. Color Fade physics - burstItr->r += dr; - burstItr->g += dg; - burstItr->b += db; - - // 2. Alpha fade physics (Mimicking Kazesui's global delay decrementing) - if (this->delay <= 0) { - burstItr->alpha -= dA; - if (burstItr->alpha <= 0) { - burstItr = bursts.erase(burstItr); - continue; - } - } else { - this->delay--; - } - - int draw_alpha = std::clamp((int)burstItr->alpha, 0, 255); - int cur_r = std::clamp((int)burstItr->r, 0, 255); - int cur_g = std::clamp((int)burstItr->g, 0, 255); - int cur_b = std::clamp((int)burstItr->b, 0, 255); - - // Pre-tint the texture for this specific burst frame - BitmapRef colored_image; - if (hasTexture && image) { - colored_image = Bitmap::Create(image->GetWidth(), image->GetHeight(), true); - colored_image->Clear(); - - // Map 0-200% RM2k3 tone scale to EasyRPG's 0-255 offset scale where 128 is neutral - Tone tone(cur_r * 128 / 100, cur_g * 128 / 100, cur_b * 128 / 100, 128); - colored_image->ToneBlit(0, 0, *image, image->GetRect(), tone, Opacity::Opaque()); - } - - // 3. Particle Movement physics and drawing - auto partItr = burstItr->particles.begin(); - while (partItr != burstItr->particles.end()) { - partItr->x += partItr->dx; - partItr->y += partItr->dy; - partItr->size += ds; - - int draw_size = std::max(1, (int)partItr->size); - Rect dst_rect((int)partItr->x - cam_x, (int)partItr->y - cam_y, draw_size, draw_size); - - if (colored_image) { - dst.StretchBlit(dst_rect, *colored_image, colored_image->GetRect(), Opacity(draw_alpha)); - } else { - // Fallback if no texture was found - dst.FillRect(dst_rect, Color(cur_r, cur_g, cur_b, draw_alpha)); - } - partItr++; - } - burstItr++; - } + if (!v1_draw || bursts.empty()) return; + + int cam_x = Game_Map::GetDisplayX() / 16; + int cam_y = Game_Map::GetDisplayY() / 16; + + for (auto& burst : bursts) { + int draw_alpha = std::clamp((int)burst.alpha, 0, 255); + int cur_r = std::clamp((int)burst.r, 0, 255); + int cur_g = std::clamp((int)burst.g, 0, 255); + int cur_b = std::clamp((int)burst.b, 0, 255); + + // Pre-tint the texture for this specific burst frame + BitmapRef colored_image; + if (hasTexture && image) { + colored_image = Bitmap::Create(image->GetWidth(), image->GetHeight(), true); + colored_image->Clear(); + + // Map 0-200% RM2k3 tone scale to EasyRPG's 0-255 offset scale where 128 is neutral + Tone tone(cur_r * 128 / 100, cur_g * 128 / 100, cur_b * 128 / 100, 128); + colored_image->ToneBlit(0, 0, *image, image->GetRect(), tone, Opacity::Opaque()); + } + + for (auto& particle : burst.particles) { + int draw_size = std::max(1, (int)particle.size); + Rect dst_rect((int)particle.x - cam_x, (int)particle.y - cam_y, draw_size, draw_size); + + if (colored_image) { + dst.StretchBlit(dst_rect, *colored_image, colored_image->GetRect(), Opacity(draw_alpha)); + } else { + // Fallback if no texture was found + dst.FillRect(dst_rect, Color(cur_r, cur_g, cur_b, draw_alpha)); + } + } + } } DynRpg::ParticleV1::ParticleV1(Game_DynRpg& instance) : DynRpgPlugin("ParticleSystemV1", instance) {} DynRpg::ParticleV1::~ParticleV1() { - for (auto* pfx : v1_effects) { - delete pfx; - } - v1_effects.clear(); + for (auto* pfx : v1_effects) { + delete pfx; + } + v1_effects.clear(); } bool DynRpg::ParticleV1::Invoke(std::string_view func, dyn_arg_list args, bool& do_yield, Game_Interpreter* interpreter) { - if (func == "new_effect") { - v1_effects.push_back(new ParticleEffectV1()); - return true; - } - if (func == "stop") { - v1_draw = false; - return true; - } - if (func == "start") { - v1_draw = true; - return true; - } - - // 1. Check if the command belongs to this plugin FIRST - if (func == "effect_burst" || func == "effect_heart" || func == "effect_color" || - func == "effect_colorfade" || func == "effect_amount" || func == "effect_random" || - func == "effect_angle" || func == "effect_growth" || func == "effect_speed" || - func == "effect_timeout" || func == "effect_delay" || func == "effect_file" || - func == "effect_kill") { - - // Lenient manual parsing to properly handle floats and omitted parameters - auto get_int = [&](size_t idx, int def = 0) { - if (idx < args.size() && !args[idx].empty()) { - try { return std::stoi(args[idx]); } catch (...) {} - } - return def; - }; - - auto get_double = [&](size_t idx, double def = 0.0) { - if (idx < args.size() && !args[idx].empty()) { - try { return std::stod(args[idx]); } catch (...) {} - } - return def; - }; - - auto get_string = [&](size_t idx, std::string def = "") { - if (idx < args.size() && !args[idx].empty()) return args[idx]; - return def; - }; - - // 2. Safely check the index and emulate the original silent failure! - int index = get_int(0, -1); - - if (index < 0 || index >= v1_effects.size()) { - // By returning "true" here, we tell EasyRPG: "I acknowledge this command, - // but I am intentionally ignoring it." - // This stops the "Unsupported" warning, and leaves the particle amount - // at its default value of 50, exactly recreating your original waterfall! - return true; - } - - if (func == "effect_burst") { - v1_effects[index]->newBurst(get_int(1, 0), get_int(2, 0)); - return true; - } - - if (func == "effect_heart") { - v1_effects[index]->newHeart(get_int(1, 0), get_int(2, 0)); - return true; - } - - if (func == "effect_color") { - v1_effects[index]->red = get_int(1, 255); - v1_effects[index]->green = get_int(2, 255); - v1_effects[index]->blue = get_int(3, 255); - return true; - } - - if (func == "effect_colorfade") { - v1_effects[index]->colorFade(get_int(1, 0), get_int(2, 0), get_int(3, 0)); - return true; - } - - if (func == "effect_amount") { - v1_effects[index]->amount = get_int(1, 50); - return true; - } - - if (func == "effect_random") { - v1_effects[index]->rndX = get_int(1, 0); - v1_effects[index]->rndY = get_int(2, 0); - return true; - } - - if (func == "effect_angle") { - v1_effects[index]->setAngle(get_double(1, 0.0), get_double(2, 360.0)); - return true; - } - - if (func == "effect_growth") { - v1_effects[index]->setGrowth(get_int(1, 2), get_int(2, 2)); - return true; - } - - if (func == "effect_speed") { - v1_effects[index]->spd = get_double(1, 2.0); - v1_effects[index]->rndSpd = get_double(2, 2.0); - return true; - } - - if (func == "effect_timeout") { - v1_effects[index]->setTimeout(get_double(1, 30.0)); - return true; - } - - if (func == "effect_delay") { - v1_effects[index]->delay = get_double(1, 0.0); - return true; - } - - if (func == "effect_file") { - std::string filename = get_string(1, "Particle"); - std::string mask = get_string(2, "false"); - v1_effects[index]->filename = filename; - v1_effects[index]->mask = (mask.substr(0, 4) == "true" || mask.substr(0, 4) == "TRUE"); - v1_effects[index]->loadTexture(); - return true; - } - - if (func == "effect_kill") { - v1_effects[index]->killParticles(); - return true; - } - } - - return false; + if (func == "new_effect") { + v1_effects.push_back(new ParticleEffectV1()); + return true; + } + if (func == "stop") { + v1_draw = false; + return true; + } + if (func == "start") { + v1_draw = true; + return true; + } + + if (func == "effect_burst" || func == "effect_heart" || func == "effect_color" || + func == "effect_colorfade" || func == "effect_amount" || func == "effect_random" || + func == "effect_angle" || func == "effect_growth" || func == "effect_speed" || + func == "effect_timeout" || func == "effect_delay" || func == "effect_file" || + func == "effect_kill") { + + auto get_int = [&](size_t idx, int def = 0) { + if (idx < args.size() && !args[idx].empty()) { + try { return std::stoi(args[idx]); } catch (...) {} + } + return def; + }; + + auto get_double = [&](size_t idx, double def = 0.0) { + if (idx < args.size() && !args[idx].empty()) { + try { return std::stod(args[idx]); } catch (...) {} + } + return def; + }; + + auto get_string = [&](size_t idx, std::string def = "") { + if (idx < args.size() && !args[idx].empty()) return args[idx]; + return def; + }; + + int index = get_int(0, -1); + + if (index < 0 || index >= (int)v1_effects.size()) { + return true; + } + + if (func == "effect_burst") { + v1_effects[index]->newBurst(get_int(1, 0), get_int(2, 0)); + return true; + } + + if (func == "effect_heart") { + v1_effects[index]->newHeart(get_int(1, 0), get_int(2, 0)); + return true; + } + + if (func == "effect_color") { + v1_effects[index]->red = get_int(1, 255); + v1_effects[index]->green = get_int(2, 255); + v1_effects[index]->blue = get_int(3, 255); + return true; + } + + if (func == "effect_colorfade") { + v1_effects[index]->colorFade(get_int(1, 0), get_int(2, 0), get_int(3, 0)); + return true; + } + + if (func == "effect_amount") { + v1_effects[index]->amount = get_int(1, 50); + return true; + } + + if (func == "effect_random") { + v1_effects[index]->rndX = get_int(1, 0); + v1_effects[index]->rndY = get_int(2, 0); + return true; + } + + if (func == "effect_angle") { + v1_effects[index]->setAngle(get_double(1, 0.0), get_double(2, 360.0)); + return true; + } + + if (func == "effect_growth") { + v1_effects[index]->setGrowth(get_int(1, 2), get_int(2, 2)); + return true; + } + + if (func == "effect_speed") { + v1_effects[index]->spd = get_double(1, 2.0); + v1_effects[index]->rndSpd = get_double(2, 2.0); + return true; + } + + if (func == "effect_timeout") { + v1_effects[index]->setTimeout(get_double(1, 30.0)); + return true; + } + + if (func == "effect_delay") { + v1_effects[index]->delay = get_double(1, 0.0); + return true; + } + + if (func == "effect_file") { + std::string filename = get_string(1, "Particle"); + std::string mask_str = get_string(2, "false"); + v1_effects[index]->filename = filename; + v1_effects[index]->mask = (mask_str.substr(0, 4) == "true" || mask_str.substr(0, 4) == "TRUE"); + v1_effects[index]->loadTexture(); + return true; + } + + if (func == "effect_kill") { + v1_effects[index]->killParticles(); + return true; + } + } + + return false; } void DynRpg::ParticleV1::Update() { - // Rendering is handled by DrawableMgr. + if (!v1_draw) return; + + for (auto* pfx : v1_effects) { + pfx->Update(); + } } void DynRpg::ParticleV1::OnMapChange() { - for (auto* pfx : v1_effects) { - pfx->killParticles(); - } + for (auto* pfx : v1_effects) { + pfx->killParticles(); + } } diff --git a/src/dynrpg_particleV1.h b/src/dynrpg_particleV1.h index e35ed0de92..9ac417a129 100644 --- a/src/dynrpg_particleV1.h +++ b/src/dynrpg_particleV1.h @@ -1,6 +1,18 @@ -/* - * This file is part of EasyRPG Player. - * ... (license header) ... +/* + * 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 . * Based on DynRPG Particle Effects V1 by Kazesui. (MIT license) */ @@ -10,15 +22,15 @@ #include "game_dynrpg.h" namespace DynRpg { - class ParticleV1 : public DynRpgPlugin { - public: - ParticleV1(Game_DynRpg& instance); - ~ParticleV1() override; + class ParticleV1 : public DynRpgPlugin { + public: + ParticleV1(Game_DynRpg& instance); + ~ParticleV1() override; - bool Invoke(std::string_view func, dyn_arg_list args, bool& do_yield, Game_Interpreter* interpreter) override; - void Update() override; + bool Invoke(std::string_view func, dyn_arg_list args, bool& do_yield, Game_Interpreter* interpreter) override; + void Update() override; void OnMapChange(); - }; + }; } #endif From 01693d2f8e2a5aa1385a4a95cc67f5d99426fc1c Mon Sep 17 00:00:00 2001 From: LizardKing777 <154367673+LizardKing777@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:29:30 -0600 Subject: [PATCH 3/3] Particle V2 now working with acceleration Got it figured out - V2 particle animations now move at whatever speed the game is moving at, without overloading the vector. --- src/dynrpg_particleV2.cpp | 806 +++++++++++++++++++------------------- src/dynrpg_particleV2.h | 17 +- 2 files changed, 418 insertions(+), 405 deletions(-) diff --git a/src/dynrpg_particleV2.cpp b/src/dynrpg_particleV2.cpp index 7257f907d0..fd00ee3b67 100644 --- a/src/dynrpg_particleV2.cpp +++ b/src/dynrpg_particleV2.cpp @@ -1,8 +1,18 @@ /* * This file is part of EasyRPG Player. - * ... (license header) ... - * This file has been refactored for clarity, modern C++, to fix bugs - * from the original porting effort, and to add Mode 7 support. + * + * 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 @@ -29,9 +39,7 @@ #include "scene.h" #include "rand.h" -// Lowest Z-order is drawn above. -// Follows the logic of RPGSS to prevent confusion. -constexpr Drawable::Z_t default_priority = Priority_Weather; // Default to character layer. +constexpr Drawable::Z_t default_priority = Priority_Weather; class ParticleEffect; @@ -52,14 +60,15 @@ class ParticleEffect : public Drawable { ParticleEffect(); ~ParticleEffect() override; - void Draw(Bitmap& dst) override {}; - // Determines if input coordinates are relative to the screen (true) or map (false) + virtual void Update() = 0; + void Draw(Bitmap& dst) override = 0; + bool isScreenSpaceEffect; - virtual void clear() {}; - virtual void setSimul(int newSimul) {}; - virtual void setAmount(int newAmount); + virtual void clear() = 0; + virtual void setSimul(int newSimul) = 0; + virtual void setAmount(int newAmount) = 0; void setAngle(float v1, float v2); void setSecondaryAngle(float v); virtual void setTimeout(int fade, int delay); @@ -92,37 +101,17 @@ class ParticleEffect : public Drawable { int z_offset; Drawable::Z_t base_z; - int r0; - int rand_r; - int rand_x; - int rand_y; - - float spd; - float rand_spd; - - float s0; - float s1; - float ds; - float da; - - float gx; - float gy; - - float ax0; - float ay0; - float afc; + int r0, rand_r, rand_x, rand_y; + float spd, rand_spd, s0, s1, ds, da; + float gx, gy, ax0, ay0, afc; BitmapRef image; - bool hasTexture; // Track if a texture is loaded + bool hasTexture; - float beta; - float alpha; - float theta; - uint8_t fade; - uint8_t delay; + float beta, alpha, theta; + uint8_t fade, delay; uint16_t amount; - uint32_t color0; - uint32_t color1; + uint32_t color0, color1; uint32_t interval = 1; uint32_t cur_interval = 1; @@ -137,18 +126,16 @@ void linear_fade(ParticleEffect* effect, uint32_t color0, uint32_t color1, int f if (delay >= fade) delay = fade - 1; float dr, dg, db; - { - float end_r = (color1 >> 16) & 0xff; - float end_g = (color1 >> 8) & 0xff; - float end_b = (color1 & 0xff); - - if (fade - delay > 0) { - dr = (end_r - r) / (fade - delay); - dg = (end_g - g) / (fade - delay); - db = (end_b - b) / (fade - delay); - } else { - dr = dg = db = 0.0f; - } + float end_r = (color1 >> 16) & 0xff; + float end_g = (color1 >> 8) & 0xff; + float end_b = (color1 & 0xff); + + if (fade - delay > 0) { + dr = (end_r - r) / (fade - delay); + dg = (end_g - g) / (fade - delay); + db = (end_b - b) / (fade - delay); + } else { + dr = dg = db = 0.0f; } int i = 0; @@ -192,7 +179,7 @@ void ParticleEffect::setTexture(std::string filename) { void ParticleEffect::unloadTexture() { image = Bitmap::Create(1, 1, true); hasTexture = false; - linear_fade(this, color0, color1, fade, delay); + update_color(); } void ParticleEffect::setGravityDirection(float angle, float factor) { @@ -203,11 +190,9 @@ void ParticleEffect::setGravityDirection(float angle, float factor) { void ParticleEffect::setAccelerationPoint(float x, float y, float factor) { afc = factor / 600.0; -{ ax0 = x; ay0 = y; } -} void ParticleEffect::setGrowth(float ini_size, float end_size) { s0 = ini_size; @@ -228,10 +213,7 @@ void ParticleEffect::setAngle(float v1, float v2) { v2 *= 0.0174532925; beta = (v2 < 0) ? -v2 : v2; alpha = v1 - v2 / 2; - // Auto-detect screen space effect if 360 degree emission - if (beta == 0.0f) { - isScreenSpaceEffect = true; - } + if (beta == 0.0f) isScreenSpaceEffect = true; } void ParticleEffect::setSecondaryAngle(float v) { @@ -284,9 +266,7 @@ void ParticleEffect::setColor1(uint8_t r_val, uint8_t g_val, uint8_t b_val) { } void ParticleEffect::setInterval(uint32_t new_interval) { - if (new_interval < 1) { - return; - } + if (new_interval < 1) return; cur_interval = new_interval; interval = new_interval; } @@ -305,10 +285,8 @@ void ParticleEffect::setLayer(int layer) { } else { base_z = Drawable::GetPriorityForMapLayer(layer); } - // The final Z value, including offsets, will be set in the Draw method. } - void ParticleEffect::update_color() { linear_fade(this, color0, color1, fade, delay); } @@ -319,10 +297,216 @@ void ParticleEffect::create_trig_lut() { sin_lut[i] = sin(dr * i); } +class Burst : public ParticleEffect { +public: + Burst(); + ~Burst() override = default; + + void Update() override; + void Draw(Bitmap& dst) override; + void clear() override; + void newBurst(int x, int y); + + void setSimul(int newSimul) override; + void setAmount(int newAmount) override; + void setTimeout(int fade, int delay) override; + void setGeneratingFunction(std::string type) override; + +private: + uint16_t simulCnt; + uint16_t simulMax; + + std::vector x, y, s, dx, dy; + std::vector itr; + std::vector origins; + + void reallocate(bool preserve); + + void (Burst::*init)(int, int, int, int); + + void init_basic(int x0, int y0, int a, int b); + void init_radial(int x0, int y0, int a, int b); +}; + +Burst::Burst() : ParticleEffect(), simulCnt(0), simulMax(1) { + reallocate(false); + init = &Burst::init_basic; + update_color(); +} + +void Burst::reallocate(bool preserve) { + size_t pool_size = static_cast(amount) * simulMax; + if (!preserve) { + x.assign(pool_size, 0.0f); + y.assign(pool_size, 0.0f); + s.assign(pool_size, 0.0f); + dx.assign(pool_size, 0.0f); + dy.assign(pool_size, 0.0f); + itr.assign(simulMax, 0); + origins.assign(simulMax, {0.0f, 0.0f}); + simulCnt = 0; + } else { + x.resize(pool_size, 0.0f); + y.resize(pool_size, 0.0f); + s.resize(pool_size, 0.0f); + dx.resize(pool_size, 0.0f); + dy.resize(pool_size, 0.0f); + itr.resize(simulMax, 0); + origins.resize(simulMax, {0.0f, 0.0f}); + } +} + +void Burst::setAmount(int newAmount) { + amount = newAmount; + reallocate(false); +} + +void Burst::setSimul(int newSimul) { + simulMax = newSimul; + reallocate(false); +} + +void Burst::setTimeout(int new_fade, int new_delay) { + ParticleEffect::setTimeout(new_fade, new_delay); + reallocate(false); +} + +void Burst::setGeneratingFunction(std::string type) { + std::transform(type.begin(), type.end(), type.begin(), ::tolower); + if (!type.substr(0, 8).compare("standard")) { + init = &Burst::init_basic; + isScreenSpaceEffect = false; + renderType = RenderType::Map; + return; + } + if (!type.substr(0, 6).compare("radial")) { + init = &Burst::init_radial; + isScreenSpaceEffect = true; + renderType = RenderType::Screen; + return; + } +} + +void Burst::clear() { + simulCnt = 0; +} + +void Burst::newBurst(int x0, int y0) { + if (simulCnt >= simulMax) { + simulMax *= 2; + reallocate(true); + } + + itr[simulCnt] = 0; + origins[simulCnt] = { (float)x0, (float)y0 }; + + (this->*init)(x0, y0, simulCnt * amount, (simulCnt + 1) * amount); + simulCnt++; +} + +void Burst::init_basic(int x0, int y0, int a, int b) { + for (int i = a; i < b; i++) { + x[i] = x0 + 2 * rand_x * (float)rand() / RAND_MAX - rand_x; + y[i] = y0 + 2 * rand_y * (float)rand() / RAND_MAX - rand_y; + s[i] = s0; + + float tmp_angle = (float)rand() / RAND_MAX * beta + alpha; + float tmp_spd = spd + rand_spd * (float)rand() / RAND_MAX; + int v = tmp_angle / 0.1963495408; + tmp_angle = (tmp_angle - v * 0.1963495408) / 0.1963495408; + dx[i] = tmp_spd * (sin_lut[(v + 9) & 31] * tmp_angle + sin_lut[(v + 8) & 31] * (1 - tmp_angle)); + dy[i] = tmp_spd * (sin_lut[(v + 1) & 31] * tmp_angle + sin_lut[(v + 0) & 31] * (1 - tmp_angle)); + } +} + +void Burst::init_radial(int x0, int y0, int a, int b) { + for (int i = a; i < b; i++) { + float tmp_rnd = rand_r * (float)rand() / RAND_MAX; + float tmp_angle = (float)rand() / RAND_MAX * beta + alpha; + float tmp_spd = spd + rand_spd * (float)rand() / RAND_MAX; + int v = tmp_angle / 0.1963495408; + float p = (tmp_angle - v * 0.1963495408) / 0.1963495408; + + x[i] = x0 + (r0 + tmp_rnd) * (sin_lut[(v + 9) & 31] * p + sin_lut[(v + 8) & 31] * (1 - p)); + y[i] = y0 + (r0 + tmp_rnd) * (sin_lut[(v + 1) & 31] * p + sin_lut[(v + 0) & 31] * (1 - p)); + s[i] = s0; + + v = (tmp_angle + theta) / 0.1963495408; + p = (tmp_angle + theta - v * 0.1963495408) / 0.1963495408; + dx[i] = -tmp_spd * (sin_lut[(v + 9) & 31] * p + sin_lut[(v + 8) & 31] * (1 - p)); + dy[i] = -tmp_spd * (sin_lut[(v + 1) & 31] * p + sin_lut[(v + 0) & 31] * (1 - p)); + } +} + +void Burst::Update() { + if (simulCnt <= 0) return; + + for (int i = 0; i < simulCnt; ++i) { + if (itr[i] >= fade) { + simulCnt--; + if (i < simulCnt) { + size_t dead_offset = i * amount; + size_t last_offset = simulCnt * amount; + std::copy_n(&x[last_offset], amount, &x[dead_offset]); + std::copy_n(&y[last_offset], amount, &y[dead_offset]); + std::copy_n(&s[last_offset], amount, &s[dead_offset]); + std::copy_n(&dx[last_offset], amount, &dx[dead_offset]); + std::copy_n(&dy[last_offset], amount, &dy[dead_offset]); + itr[i] = itr[simulCnt]; + origins[i] = origins[simulCnt]; + } + --i; + continue; + } + + itr[i]++; + + float tx, ty, tsqr; + for (int j = i * amount; j < (i + 1) * amount; j++) { + x[j] += dx[j]; + y[j] += dy[j]; + tx = ax0 - x[j]; + ty = ay0 - y[j]; + tsqr = sqrtf(tx*tx + ty*ty + 0.001); + dx[j] += gx + afc * tx / tsqr; + dy[j] += gy + afc * ty / tsqr; + s[j] += ds; + } + } +} + +void Burst::Draw(Bitmap& dst) { + if (simulCnt <= 0) return; + + int cam_x = (isScreenRelative) ? 0 : Game_Map::GetDisplayX() / 16; + int cam_y = (isScreenRelative) ? 0 : Game_Map::GetDisplayY() / 16; + + SetZ(base_z + z_offset); + + for (int i = 0; i < simulCnt; i++) { + int age = itr[i]; + if (age >= fade) continue; + + int alpha = static_cast(255 - da * age); + Color color = palette[age]; + + for (int j = i * amount; j < (i + 1) * amount; j++) { + Rect dst_rect(x[j] - cam_x - s[j] / 2, y[j] - cam_y - s[j] / 2 - z_offset, s[j], s[j]); + + if (hasTexture) { + dst.StretchBlit(dst_rect, *image, image->GetRect(), Opacity(alpha)); + } else { + dst.FillRect(dst_rect, Color(color.red, color.green, color.blue, alpha)); + } + } + } +} + class Stream : public ParticleEffect { public: Stream(); - ~Stream() = default; + ~Stream() override = default; + void Update() override; void Draw(Bitmap& dst) override; void clear() override; void stopAll(); @@ -336,30 +520,23 @@ class Stream : public ParticleEffect { void setPosition(std::string tag, int x, int y); private: - uint8_t simulBeg; - uint8_t simulRun; - uint8_t simulCnt; + uint16_t simulBeg; + uint16_t simulRun; + uint16_t simulCnt; uint16_t simulMax; - std::vector x; - std::vector y; - std::vector s; - std::vector dx; - std::vector dy; - std::vector itr; - std::vector str_x; - std::vector str_y; - std::vector pfx_ref; - std::vector end_cnt; + std::vector x, y, s, dx, dy; + std::vector itr, end_cnt; + std::vector str_x, str_y; + std::vector pfx_ref; std::map pfx_tag; - void resize(); - void stream_to_end(uint8_t idx); - void start_to_stream(uint8_t idx); + void reallocate(bool preserve); + void stream_to_end(uint16_t idx); + void start_to_stream(uint16_t idx); void (Stream::*init)(int, int, int); - void draw_block(Bitmap& dst, int, uint8_t, uint8_t, uint8_t, int16_t, int16_t); void init_basic(int a, int b, int idx); void init_radial(int a, int b, int idx); @@ -367,20 +544,57 @@ class Stream : public ParticleEffect { Stream::Stream() : ParticleEffect(), simulBeg(0), simulRun(0), simulCnt(0), simulMax(1) { amount = 10; - resize(); + reallocate(false); init = &Stream::init_basic; update_color(); } +void Stream::reallocate(bool preserve) { + size_t pool_size = static_cast(amount) * fade * simulMax; + if (!preserve) { + x.assign(pool_size, 0.0f); + y.assign(pool_size, 0.0f); + s.assign(pool_size, 0.0f); + dx.assign(pool_size, 0.0f); + dy.assign(pool_size, 0.0f); + itr.assign(simulMax, 0); + str_x.assign(simulMax, 0); + str_y.assign(simulMax, 0); + pfx_ref.resize(simulMax); + for (uint16_t i = 0; i < simulMax; i++) { + pfx_ref[i] = i; + } + end_cnt.assign(simulMax, 0); + simulBeg = 0; + simulRun = 0; + simulCnt = 0; + pfx_tag.clear(); + } else { + x.resize(pool_size, 0.0f); + y.resize(pool_size, 0.0f); + s.resize(pool_size, 0.0f); + dx.resize(pool_size, 0.0f); + dy.resize(pool_size, 0.0f); + itr.resize(simulMax, 0); + str_x.resize(simulMax, 0); + str_y.resize(simulMax, 0); + size_t old_simulMax = pfx_ref.size(); + pfx_ref.resize(simulMax); + for (uint16_t i = old_simulMax; i < simulMax; i++) { + pfx_ref[i] = i; + } + end_cnt.resize(simulMax, 0); + } +} + void Stream::start(int x0, int y0, std::string tag) { if (pfx_tag.count(tag)) return; - if (simulCnt >= simulMax) resize(); - - // Convert Screen coordinate to World coordinate if this is a screen-space effect (e.g. Radial) - // This ensures that when rendered in Mode7, the effect appears at the correct map location - + if (simulCnt >= simulMax) { + simulMax *= 2; + reallocate(true); + } - uint8_t idx = pfx_ref[simulCnt]; + uint16_t idx = pfx_ref[simulCnt]; std::swap(pfx_ref[simulCnt], pfx_ref[simulRun]); std::swap(pfx_ref[simulRun], pfx_ref[simulBeg]); @@ -426,14 +640,12 @@ void Stream::setGeneratingFunction(std::string type) { std::transform(type.begin(), type.end(), type.begin(), ::tolower); if (!type.substr(0, 8).compare("standard")) { init = &Stream::init_basic; - // Standard: Input is Map/World coordinates, defaults to Map Plane isScreenSpaceEffect = false; renderType = RenderType::Map; return; } if (!type.substr(0, 6).compare("radial")) { init = &Stream::init_radial; - // Radial: Input is Screen coordinates, defaults to Screen Plane (Overlay) isScreenSpaceEffect = true; renderType = RenderType::Screen; return; @@ -478,37 +690,6 @@ void Stream::init_radial(int a, int b, int idx) { } } -void Stream::draw_block(Bitmap& dst, int ref, uint8_t n, uint8_t z, uint8_t c0, int16_t cam_x, int16_t cam_y) { - { - // --- Original 2D drawing logic --- - for (uint8_t i = 0; i < n; i++) { - int age = i + c0; - if (age >= fade) continue; - - int alpha = static_cast(255 - da * age); - Color color = palette[age]; - - int block_start_idx = ref + z * amount; - - for (int j = 0; j < amount; j++) { - int p_idx = block_start_idx + j; - float size = s[p_idx]; - float draw_x = x[p_idx] - cam_x - size / 2.0f; - float draw_y = y[p_idx] - cam_y - size / 2.0f - z_offset; - - Rect dst_rect(draw_x, draw_y, size, size); - - if (hasTexture) { - dst.StretchBlit(dst_rect, *image, image->GetRect(), Opacity(alpha)); - } else { - dst.FillRect(dst_rect, Color(color.red, color.green, color.blue, alpha)); - } - } - z = (z + 1) % fade; - } - } -} - void Stream::setPosition(std::string tag, int x_pos, int y_pos) { auto pfx_itr = pfx_tag.find(tag); if (pfx_itr == pfx_tag.end()) return; @@ -517,19 +698,33 @@ void Stream::setPosition(std::string tag, int x_pos, int y_pos) { auto it = std::find(pfx_ref.begin(), pfx_ref.begin() + simulCnt, probe); if (it == pfx_ref.begin() + simulCnt) return; auto i = std::distance(pfx_ref.begin(), it); - str_x[i] = x_pos; - str_y[i] = y_pos; + str_x[pfx_ref[i]] = x_pos; + str_y[pfx_ref[i]] = y_pos; } -void Stream::Draw(Bitmap& dst) { +void Stream::setAmount(int newAmount) { + amount = newAmount; + reallocate(false); +} + +void Stream::setSimul(int newSimul) { + simulMax = newSimul; + reallocate(false); +} + +void Stream::setTimeout(int new_fade, int new_delay) { + ParticleEffect::setTimeout(new_fade, new_delay); + reallocate(false); +} + +void Stream::Update() { if (simulCnt <= 0) return; - int cam_x = (isScreenRelative) ? 0 : Game_Map::GetDisplayX() / 16; - int cam_y = (isScreenRelative) ? 0 : Game_Map::GetDisplayY() / 16; + int block_size = amount * fade; // --- Physics Update Section --- for (int i = 0; i < simulCnt; ++i) { - uint8_t p_ref = pfx_ref[i]; + uint16_t p_ref = pfx_ref[i]; int base_idx = p_ref * block_size; for (int j = 0; j < block_size; ++j) { int p_idx = base_idx + j; @@ -548,300 +743,109 @@ void Stream::Draw(Bitmap& dst) { --cur_interval; if (cur_interval == 0) { for (int i = simulBeg; i < simulRun; i++) { - uint8_t idx = pfx_ref[i]; + uint16_t idx = pfx_ref[i]; uint8_t z = fade - itr[idx] - 1; (this->*init)(z * amount + idx * block_size, (z + 1) * amount + idx * block_size, idx); } cur_interval = interval; } - // --- Drawing & Z-Update Section --- - { - SetZ(base_z + z_offset); - } - - int i = 0; + // --- State Update Section --- // Starting - for (; i < simulBeg; i++) { - uint8_t idx = pfx_ref[i]; + for (int i = 0; i < simulBeg; i++) { + uint16_t idx = pfx_ref[i]; if (itr[idx] < fade) { uint8_t z = fade - itr[idx] - 1; (this->*init)(z * amount + idx * block_size, (z + 1) * amount + idx * block_size, idx); - itr[idx]++; - draw_block(dst, idx * block_size, itr[idx], z, 0, cam_x, cam_y); } - else start_to_stream(i--); + else { + start_to_stream(i--); + } } + // Streaming - for (; i < simulRun; i++) { - uint8_t idx = pfx_ref[i]; - uint8_t z = fade - itr[idx] - 1; + for (int i = simulBeg; i < simulRun; i++) { + uint16_t idx = pfx_ref[i]; itr[idx] = (itr[idx] + 1) % fade; - draw_block(dst, idx * block_size, fade, z, 0, cam_x, cam_y); } + // Stopping - for (; i < simulCnt; i++) { - uint8_t idx = pfx_ref[i]; - uint8_t z = (fade - itr[idx]) % fade; - draw_block(dst, idx * block_size, end_cnt[idx]--, z, fade - end_cnt[idx], cam_x, cam_y); - if (end_cnt[idx] <= 0) - stream_to_end(i); + for (int i = simulRun; i < simulCnt; i++) { + uint16_t idx = pfx_ref[i]; + if (end_cnt[idx] == 0) { + stream_to_end(i--); + } else { + end_cnt[idx]--; + } } } -void Stream::resize() { - simulMax *= 2; - size_t particle_pool_size = static_cast(amount) * fade * simulMax; +void Stream::Draw(Bitmap& dst) { + if (simulCnt <= 0) return; - x.resize(particle_pool_size); - y.resize(particle_pool_size); - s.resize(particle_pool_size); - dx.resize(particle_pool_size); - dy.resize(particle_pool_size); + int cam_x = (isScreenRelative) ? 0 : Game_Map::GetDisplayX() / 16; + int cam_y = (isScreenRelative) ? 0 : Game_Map::GetDisplayY() / 16; + int block_size = amount * fade; - itr.resize(simulMax); - str_x.resize(simulMax); - str_y.resize(simulMax); - pfx_ref.resize(simulMax); - end_cnt.resize(simulMax); + SetZ(base_z + z_offset); - for (int i = simulMax / 2; i < simulMax; i++) { - pfx_ref[i] = i; - } -} + auto draw_block = [&](uint16_t idx, uint8_t n, uint8_t z, uint8_t c0) { + for (uint8_t i = 0; i < n; i++) { + int age = i + c0; + if (age >= fade) continue; -void Stream::setAmount(int newAmount) { - amount = newAmount; - resize(); -} + int alpha = static_cast(255 - da * age); + Color color = palette[age]; + int block_start_idx = idx * block_size + z * amount; -void Stream::setSimul(int newSimul) { - simulMax = newSimul; - resize(); - simulBeg = 0; - simulRun = 0; - simulCnt = 0; -} + for (int j = 0; j < amount; j++) { + int p_idx = block_start_idx + j; + float size = s[p_idx]; + Rect dst_rect(x[p_idx] - cam_x - size / 2.0f, y[p_idx] - cam_y - size / 2.0f - z_offset, size, size); -void Stream::setTimeout(int _fade, int _delay) { - if (_fade > 255) _fade = 255; - else if (_fade <= 0) _fade = 1; - if (_delay >= _fade) _delay = _fade - 1; - else if (_delay < 0) _delay = 0; - fade = _fade; - delay = _delay; - da = 255.0f / _fade; - ds = (s1 - s0) / _fade; - resize(); - update_color(); + if (hasTexture) { + dst.StretchBlit(dst_rect, *image, image->GetRect(), Opacity(alpha)); + } else { + dst.FillRect(dst_rect, Color(color.red, color.green, color.blue, alpha)); + } + } + z = (z + 1) % fade; + } + }; + + // Starting + for (int i = 0; i < simulBeg; i++) { + uint16_t idx = pfx_ref[i]; + uint8_t z = fade - itr[idx]; + draw_block(idx, itr[idx], z, 0); + } + // Streaming + for (int i = simulBeg; i < simulRun; i++) { + uint16_t idx = pfx_ref[i]; + uint8_t z_old = (itr[idx] == 0) ? 0 : fade - itr[idx]; + draw_block(idx, fade, z_old, 0); + } + // Stopping + for (int i = simulRun; i < simulCnt; i++) { + uint16_t idx = pfx_ref[i]; + uint8_t count = end_cnt[idx] + 1; + uint8_t z_old = (fade - itr[idx]) % fade; + draw_block(idx, count, z_old, fade - count); + } } -void Stream::start_to_stream(uint8_t idx) { +void Stream::start_to_stream(uint16_t idx) { itr[pfx_ref[idx]] = 0; --simulBeg; std::swap(pfx_ref[simulBeg], pfx_ref[idx]); } -void Stream::stream_to_end(uint8_t idx) { +void Stream::stream_to_end(uint16_t idx) { --simulCnt; std::swap(pfx_ref[simulCnt], pfx_ref[idx]); } -class Burst : public ParticleEffect { -public: - Burst(); - ~Burst() = default; - void Draw(Bitmap& dst) override; - void clear() override; - void newBurst(int x, int y); - - void setSimul(int newSimul) override; - void setAmount(int newAmount) override; - void setGeneratingFunction(std::string type) override; - -private: - uint8_t simulCnt; - uint16_t simulMax; - - std::vector x; - std::vector y; - std::vector s; - std::vector dx; - std::vector dy; - std::vector itr; - std::vector origins; - - void resize(); - - void (Burst::*init)(int, int, int, int); - void (Burst::*draw_function)(Bitmap& dst, int, int); - - void init_basic(int x0, int y0, int a, int b); - void init_radial(int x0, int y0, int a, int b); - void draw_standard(Bitmap& dst, int cam_x, int cam_y); -}; - -Burst::Burst() : ParticleEffect(), simulCnt(0), simulMax(1) { - resize(); - init = &Burst::init_basic; - draw_function = &Burst::draw_standard; - update_color(); -} - -void Burst::setGeneratingFunction(std::string type) { - std::transform(type.begin(), type.end(), type.begin(), ::tolower); - if (!type.substr(0, 8).compare("standard")) { - init = &Burst::init_basic; - // Standard: Input is Map/World coordinates, defaults to Map Plane - isScreenSpaceEffect = false; - renderType = RenderType::Map; - return; - } - if (!type.substr(0, 6).compare("radial")) { - init = &Burst::init_radial; - // Radial: Input is Screen coordinates, defaults to Screen Plane (Overlay) - isScreenSpaceEffect = true; - renderType = RenderType::Screen; - return; - } -} - -void Burst::clear() { - simulCnt = 0; -} - -void Burst::newBurst(int x0, int y0) { - if (simulCnt >= simulMax) resize(); - - - itr[simulCnt] = 0; - origins[simulCnt] = { (float)x0, (float)y0 }; - - (this->*init)(x0, y0, simulCnt * amount, (simulCnt + 1) * amount); - simulCnt++; -} - -void Burst::init_basic(int x0, int y0, int a, int b) { - for (int i = a; i < b; i++) { - x[i] = x0 + 2 * rand_x * (float)rand() / RAND_MAX - rand_x; - y[i] = y0 + 2 * rand_y * (float)rand() / RAND_MAX - rand_y; - s[i] = s0; - - float tmp_angle = (float)rand() / RAND_MAX * beta + alpha; - float tmp_spd = spd + rand_spd * (float)rand() / RAND_MAX; - int v = tmp_angle / 0.1963495408; - tmp_angle = (tmp_angle - v * 0.1963495408) / 0.1963495408; - dx[i] = tmp_spd * (sin_lut[(v + 9) & 31] * tmp_angle + sin_lut[(v + 8) & 31] * (1 - tmp_angle)); - dy[i] = tmp_spd * (sin_lut[(v + 1) & 31] * tmp_angle + sin_lut[(v + 0) & 31] * (1 - tmp_angle)); - } -} - -void Burst::init_radial(int x0, int y0, int a, int b) { - for (int i = a; i < b; i++) { - float tmp_rnd = rand_r * (float)rand() / RAND_MAX; - float tmp_angle = (float)rand() / RAND_MAX * beta + alpha; - float tmp_spd = spd + rand_spd * (float)rand() / RAND_MAX; - int v = tmp_angle / 0.1963495408; - float p = (tmp_angle - v * 0.1963495408) / 0.1963495408; - - x[i] = x0 + (r0 + tmp_rnd) * (sin_lut[(v + 9) & 31] * p + sin_lut[(v + 8) & 31] * (1 - p)); - y[i] = y0 + (r0 + tmp_rnd) * (sin_lut[(v + 1) & 31] * p + sin_lut[(v + 0) & 31] * (1 - p)); - s[i] = s0; - - v = (tmp_angle + theta) / 0.1963495408; - p = (tmp_angle + theta - v * 0.1963495408) / 0.1963495408; - dx[i] = -tmp_spd * (sin_lut[(v + 9) & 31] * p + sin_lut[(v + 8) & 31] * (1 - p)); - dy[i] = -tmp_spd * (sin_lut[(v + 1) & 31] * p + sin_lut[(v + 0) & 31] * (1 - p)); - } -} - -void Burst::draw_standard(Bitmap& dst, int cam_x, int cam_y) { - for (int i = 0; i < simulCnt; i++) { - int age = itr[i]; - if (age >= fade) continue; - - itr[i]++; - int alpha = static_cast(255 - da * age); - Color color = palette[age]; - - float tx, ty, tsqr; - for (int j = i * amount; j < (i + 1) * amount; j++) { - x[j] += dx[j]; - y[j] += dy[j]; - tx = ax0 - x[j]; - ty = ay0 - y[j]; - tsqr = sqrtf(tx*tx + ty*ty + 0.001); - dx[j] += gx + afc * tx / tsqr; - dy[j] += gy + afc * ty / tsqr; - s[j] += ds; - Rect dst_rect(x[j] - cam_x - s[j] / 2, y[j] - cam_y - s[j] / 2 - z_offset, s[j], s[j]); - - if (hasTexture) { - dst.StretchBlit(dst_rect, *image, image->GetRect(), Opacity(alpha)); - } else { - dst.FillRect(dst_rect, Color(color.red, color.green, color.blue, alpha)); - } - } - } -} - -void Burst::resize() { - simulMax *= 2; - size_t particle_pool_size = static_cast(amount) * simulMax; - - x.resize(particle_pool_size); - y.resize(particle_pool_size); - s.resize(particle_pool_size); - dx.resize(particle_pool_size); - dy.resize(particle_pool_size); - itr.resize(simulMax); - origins.resize(simulMax); -} - -void Burst::setAmount(int newAmount) { - amount = newAmount; - resize(); -} - -void Burst::setSimul(int newSimul) { - simulMax = newSimul; - resize(); - simulCnt = 0; -} - -void Burst::Draw(Bitmap& dst) { - if (simulCnt <= 0) return; - - // Recycle dead bursts - for (int i = 0; i < simulCnt; ++i) { - if (itr[i] >= fade) { - simulCnt--; - if (i < simulCnt) { // If it's not the last one - // Copy the last active burst over the dead one - size_t dead_offset = i * amount; - size_t last_offset = simulCnt * amount; - std::copy_n(&x[last_offset], amount, &x[dead_offset]); - std::copy_n(&y[last_offset], amount, &y[dead_offset]); - std::copy_n(&s[last_offset], amount, &s[dead_offset]); - std::copy_n(&dx[last_offset], amount, &dx[dead_offset]); - std::copy_n(&dy[last_offset], amount, &dy[dead_offset]); - itr[i] = itr[simulCnt]; - origins[i] = origins[simulCnt]; - } - --i; // Re-check this index in case the swapped one was also dead - } - } - - int cam_x = (isScreenRelative) ? 0 : Game_Map::GetDisplayX() / 16; - int cam_y = (isScreenRelative) ? 0 : Game_Map::GetDisplayY() / 16; - - { - // Original 2D drawing logic - SetZ(base_z + z_offset); - (this->*draw_function)(dst, cam_x, cam_y); - } -} // ============================================================================ // DynRPG Plugin Interface Implementation // ============================================================================ @@ -1019,8 +1023,6 @@ static bool SetLayer(dyn_arg_list args) { return true; } -// --- DynRpg::Particle Class Implementation --- - DynRpg::Particle::Particle(Game_DynRpg& instance) : DynRpgPlugin("KazeParticles", instance) { ParticleEffect::create_trig_lut(); @@ -1034,7 +1036,7 @@ DynRpg::Particle::Particle(Game_DynRpg& instance) : DynRpgPlugin("KazeParticles" function_list["pfx_stop"] = &stop; function_list["pfx_stopall"] = &stopall; - auto add_setter_1_int = [](const char* name, void (ParticleEffect::*setter)(int)) { + auto add_setter_1_int =[](const char* name, void (ParticleEffect::*setter)(int)) { function_list[name] = [name, setter](dyn_arg_list args) { bool okay; std::string tag; int val; std::tie(tag, val) = DynRpg::ParseArgs(name, args, &okay); @@ -1042,15 +1044,15 @@ DynRpg::Particle::Particle(Game_DynRpg& instance) : DynRpgPlugin("KazeParticles" return true; }; }; - auto add_setter_2_int = [](const char* name, void (ParticleEffect::*setter)(int, int)) { - function_list[name] = [name, setter](dyn_arg_list args) { + auto add_setter_2_int =[](const char* name, void (ParticleEffect::*setter)(int, int)) { + function_list[name] =[name, setter](dyn_arg_list args) { bool okay; std::string tag; int val1, val2; std::tie(tag, val1, val2) = DynRpg::ParseArgs(name, args, &okay); if (okay) if (auto pfx = GetPfx(tag)) (pfx->*setter)(val1, val2); return true; }; }; - auto add_setter_2_float = [](const char* name, void (ParticleEffect::*setter)(float, float)) { + auto add_setter_2_float =[](const char* name, void (ParticleEffect::*setter)(float, float)) { function_list[name] = [name, setter](dyn_arg_list args) { bool okay; std::string tag; float val1, val2; std::tie(tag, val1, val2) = DynRpg::ParseArgs(name, args, &okay); @@ -1058,7 +1060,7 @@ DynRpg::Particle::Particle(Game_DynRpg& instance) : DynRpgPlugin("KazeParticles" return true; }; }; - auto add_setter_3_int = [](const char* name, void (ParticleEffect::*setter)(uint8_t, uint8_t, uint8_t)) { + auto add_setter_3_int =[](const char* name, void (ParticleEffect::*setter)(uint8_t, uint8_t, uint8_t)) { function_list[name] = [name, setter](dyn_arg_list args) { bool okay; std::string tag; int r, g, b; std::tie(tag, r, g, b) = DynRpg::ParseArgs(name, args, &okay); @@ -1078,49 +1080,49 @@ DynRpg::Particle::Particle(Game_DynRpg& instance) : DynRpgPlugin("KazeParticles" add_setter_2_float("pfx_set_growth", &ParticleEffect::setGrowth); add_setter_2_float("pfx_set_angle", &ParticleEffect::setAngle); - function_list["pfx_set_velocity"] = [](dyn_arg_list args) { + function_list["pfx_set_velocity"] =[](dyn_arg_list args) { bool okay; std::string tag; float speed, rand_speed; std::tie(tag, speed, rand_speed) = DynRpg::ParseArgs("pfx_set_velocity", args, &okay); if (okay) if (auto pfx = GetPfx(tag)) { pfx->setSpd(speed); pfx->setRandSpd(rand_speed); } return true; }; - function_list["pfx_set_texture"] = [](dyn_arg_list args) { + function_list["pfx_set_texture"] =[](dyn_arg_list args) { bool okay; std::string tag, texture; std::tie(tag, texture) = DynRpg::ParseArgs("pfx_set_texture", args, &okay); if (okay) if (auto pfx = GetPfx(tag)) pfx->setTexture(texture); return true; }; - function_list["pfx_set_acceleration_point"] = [](dyn_arg_list args) { + function_list["pfx_set_acceleration_point"] =[](dyn_arg_list args) { bool okay; std::string tag; float x, y, force; std::tie(tag, x, y, force) = DynRpg::ParseArgs("pfx_set_acceleration_point", args, &okay); if (okay) if (auto pfx = GetPfx(tag)) pfx->setAccelerationPoint(x, y, force); return true; }; - function_list["pfx_set_gravity_direction"] = [](dyn_arg_list args) { + function_list["pfx_set_gravity_direction"] =[](dyn_arg_list args) { bool okay; std::string tag; float angle, force; std::tie(tag, angle, force) = DynRpg::ParseArgs("pfx_set_gravity_direction", args, &okay); if (okay) if (auto pfx = GetPfx(tag)) pfx->setGravityDirection(angle, force); return true; }; - function_list["pfx_set_secondary_angle"] = [](dyn_arg_list args) { + function_list["pfx_set_secondary_angle"] =[](dyn_arg_list args) { bool okay; std::string tag; float angle; std::tie(tag, angle) = DynRpg::ParseArgs("pfx_set_secondary_angle", args, &okay); if (okay) if (auto pfx = GetPfx(tag)) pfx->setSecondaryAngle(angle); return true; }; - function_list["pfx_set_generating_function"] = [](dyn_arg_list args) { + function_list["pfx_set_generating_function"] =[](dyn_arg_list args) { bool okay; std::string tag, type; std::tie(tag, type) = DynRpg::ParseArgs("pfx_set_generating_function", args, &okay); if (okay) if (auto pfx = GetPfx(tag)) pfx->setGeneratingFunction(type); return true; }; - function_list["pfx_unload_texture"] = [](dyn_arg_list args) { + function_list["pfx_unload_texture"] =[](dyn_arg_list args) { bool okay; std::string tag; std::tie(tag) = DynRpg::ParseArgs("pfx_unload_texture", args, &okay); if (okay) if (auto pfx = GetPfx(tag)) pfx->unloadTexture(); return true; }; - function_list["pfx_use_screen_relative"] = [](dyn_arg_list args) { + function_list["pfx_use_screen_relative"] =[](dyn_arg_list args) { bool okay; std::string tag, val; std::tie(tag, val) = DynRpg::ParseArgs("pfx_use_screen_relative", args, &okay); if (okay) if (auto pfx = GetPfx(tag)) pfx->useScreenRelative(val[0] == 't' || val[0] == 'T'); @@ -1131,7 +1133,7 @@ DynRpg::Particle::Particle(Game_DynRpg& instance) : DynRpgPlugin("KazeParticles" function_list["pfx_load_effect"] = &load_effect; function_list["pfx_set_z_offset"] = &SetZ; function_list["pfx_set_layer"] = &SetLayer; - function_list["pfx_set_render_plane"] = [](dyn_arg_list args) { + function_list["pfx_set_render_plane"] =[](dyn_arg_list args) { bool okay; std::string tag, plane; std::tie(tag, plane) = DynRpg::ParseArgs("pfx_set_render_plane", args, &okay); if (okay) { @@ -1139,12 +1141,10 @@ DynRpg::Particle::Particle(Game_DynRpg& instance) : DynRpgPlugin("KazeParticles" std::transform(plane.begin(), plane.end(), plane.begin(), ::tolower); if (plane == "screen") { pfx->setRenderType(ParticleEffect::RenderType::Screen); - // Force screen coordinates for generating functions like Radial pfx->isScreenSpaceEffect = true; } else if (plane == "sprite" || plane == "event") { pfx->setRenderType(ParticleEffect::RenderType::Sprite); } else { - // Default to map plane pfx->setRenderType(ParticleEffect::RenderType::Map); } } @@ -1169,7 +1169,9 @@ bool DynRpg::Particle::Invoke(std::string_view func, dyn_arg_list args, bool&, G void DynRpg::Particle::Update() { if (!pfx_list.empty()) { if ((Scene::instance && Scene::instance->type == Scene::Map) || Game_Battle::IsBattleRunning()) { - // Drawing is handled automatically by the DrawableMgr + for (auto const& [tag, pfx] : pfx_list) { + pfx->Update(); + } } } } diff --git a/src/dynrpg_particleV2.h b/src/dynrpg_particleV2.h index 74f15683ec..f845d0f492 100644 --- a/src/dynrpg_particleV2.h +++ b/src/dynrpg_particleV2.h @@ -1,7 +1,18 @@ /* * This file is part of EasyRPG Player. - * ... (license header) ... - * Based on DynRPG Particle Effects by Kazesui. (MIT license) + * + * 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 EP_DYNRPG_PARTICLE_H_ @@ -12,7 +23,7 @@ namespace DynRpg { class Particle : public DynRpgPlugin { public: - Particle(Game_DynRpg& instance); // <- Body removed, this is now a declaration + Particle(Game_DynRpg& instance); ~Particle() override; bool Invoke(std::string_view func, dyn_arg_list args, bool& do_yield, Game_Interpreter* interpreter) override;