#include "gun/effects.h" #include #include #include namespace ng { namespace { float num(const EntityComponent& c, const char* k, float d) { auto it = c.attrs.find(k); return it == c.attrs.end() ? d : (float)std::atof(it->second.c_str()); } float num(const std::map& a, const char* k, float d) { auto it = a.find(k); return it == a.end() ? d : (float)std::atof(it->second.c_str()); } uint32_t average(const SpriteFrames& s) { uint64_t r = 0, g = 0, b = 0, n = 0; for (size_t i = 0; i < (size_t)s.w * s.h && i < s.texels.size(); ++i) { const uint32_t t = s.texels[i]; if ((t >> 24) < 128) continue; r += (t >> 16) & 255; g += (t >> 8) & 255; b += t & 255; ++n; } if (!n) return 0xFFFFB040u; return 0xFF000000u | (uint32_t)(r / n) << 16 | (uint32_t)(g / n) << 8 | (uint32_t)(b / n); } } // namespace const SpriteFrames* EffectLib::sprite(const std::string& file, const std::string& animation) { if (file.empty()) return nullptr; const std::string key = file + "|" + animation; auto it = sprites_.find(key); if (it == sprites_.end()) { auto s = std::make_unique(); if (!load_sprite_frames(wak_, file, animation, *s)) s.reset(); it = sprites_.emplace(key, std::move(s)).first; } return it->second.get(); } uint32_t EffectLib::sprite_color(const SpriteFrames* s) const { return s ? average(*s) : 0xFFFFB040u; } ExplosionFx EffectLib::explosion(const std::map& attrs) { ExplosionFx e; auto str = [&](const char* k) { auto it = attrs.find(k); return it == attrs.end() ? std::string() : it->second; }; e.radius = num(attrs, "config_explosion.explosion_radius", 0); e.shake = num(attrs, "config_explosion.camera_shake", 0); e.sprite = sprite(str("config_explosion.explosion_sprite")); e.sparks = num(attrs, "config_explosion.sparks_enabled", 0) != 0; e.sparks0 = (int)num(attrs, "config_explosion.sparks_count_min", 0); e.sparks1 = (int)num(attrs, "config_explosion.sparks_count_max", 0); e.material_sparks = num(attrs, "config_explosion.material_sparks_enabled", 0) != 0; e.msparks0 = (int)num(attrs, "config_explosion.material_sparks_count_min", 0); e.msparks1 = (int)num(attrs, "config_explosion.material_sparks_count_max", 0); e.light = num(attrs, "config_explosion.light_enabled", 1) != 0; // (the constructor's defaults, 004b2780: on, 0.08 s, (255, 217, 180), radius x 8) e.light_color = 0xFF000000u | ((uint32_t)std::clamp((int)num(attrs, "config_explosion.light_r", 255), 0, 255) << 16) | ((uint32_t)std::clamp((int)num(attrs, "config_explosion.light_g", 217), 0, 255) << 8) | (uint32_t)std::clamp((int)num(attrs, "config_explosion.light_b", 180), 0, 255); e.light_radius = e.radius * num(attrs, "config_explosion.light_radius_coeff", 8.0f); e.light_frames = std::max(1, (int)std::lround(num(attrs, "config_explosion.light_fade_time", 0.08f) * 60.0f)); { std::string sm = str("config_explosion.spark_material"); if (sm.empty()) sm = "spark"; if (material_color) if (const uint32_t c = material_color(sm)) { const float a = material_alpha ? material_alpha(sm) : 1.0f; auto ch = [&](int sh) { return (uint32_t)std::lround(((c >> sh) & 0xFFu) * a) << sh; }; e.spark_color = 0xFF000000u | ch(16) | ch(8) | ch(0); } } if (e.sprite) e.color = average(*e.sprite); e.material_color = e.color; const std::string mat = str("config_explosion.create_cell_material"); if (!mat.empty() && material_color) { const uint32_t c = material_color(mat); if (c) e.material_color = c; } e.valid = e.sprite || e.sparks || e.material_sparks; return e; } void EffectLib::muzzle(const EntityDef& def, ExplosionFx& out) { for (const EntityComponent* c : def.all("SpriteComponent")) { if (c->get("_enabled", "1") == "0") continue; std::string file = c->get("image_file"); const size_t p = file.find("$["); // "name_0$[1-5].png": one of five variants, the first is used if (p != std::string::npos) { const size_t e = file.find(']', p); if (e != std::string::npos) file = file.substr(0, p) + std::to_string(std::atoi(file.c_str() + p + 2)) + file.substr(e + 1); } out.sprite = sprite(file, c->get("rect_animation")); if (!out.sprite) continue; out.pivot_x = (float)std::atof(c->get("offset_x", "0").c_str()); out.pivot_y = (float)std::atof(c->get("offset_y", "0").c_str()); out.additive = c->get("additive", "0") == "1"; out.wait = 2; out.color = average(*out.sprite); out.valid = true; break; } } void EffectLib::projectile(const EntityDef& def, ProjFx& fx) { for (const EntityComponent* c : def.all("LightComponent")) { if (c->get("_enabled", "1") == "0") continue; fx.light = {true, (int)num(*c, "r", 255), (int)num(*c, "g", 255), (int)num(*c, "b", 255), num(*c, "radius", 20)}; break; } for (const EntityComponent* c : def.all("ProjectileComponent")) { if (num(*c, "shoot_light_flash_radius", 0) > 0) fx.shoot_flash = {true, (int)num(*c, "shoot_light_flash_r", 255), (int)num(*c, "shoot_light_flash_g", 255), (int)num(*c, "shoot_light_flash_b", 255), num(*c, "shoot_light_flash_radius", 0)}; auto attrs = c->attrs; if (!def.all("PhysicsBodyComponent").empty() && !def.all("PhysicsBodyCollisionDamageComponent").empty() && !def.all("DamageModelComponent").empty()) // a breakable thrown item: its explosion is the ExplodeOnDamage one (projectiles.cpp break_speed) for (const EntityComponent* eod : def.all("ExplodeOnDamageComponent")) { for (const auto& [k, v] : eod->attrs) if (k.compare(0, 17, "config_explosion.") == 0) attrs[k] = v; break; } fx.boom = explosion(attrs); break; } for (const EntityComponent* c : def.all("SpriteComponent")) if (c->get("_enabled", "1") != "0" && c->get("additive", "0") == "1") fx.additive = true; for (const EntityComponent* c : def.all("ParticleEmitterComponent")) { if (c->get("_enabled", "1") == "0" || c->get("is_emitting", "1") == "0" || (num(*c, "emit_cosmetic_particles", 0) == 0 && num(*c, "create_real_particles", 0) == 0)) continue; if (!c->get("image_animation_file").empty()) continue; // (an image animation - the pixels of a picture, frame by frame, played once or looped: the host emits it on the projectile's twin, main.cpp) EmitterDef e; const std::string mat = c->get("emitted_material_name"); e.real = num(*c, "create_real_particles", 0) != 0 && !mat.empty(); e.only_space = num(*c, "emit_only_if_there_is_space", 0) != 0; e.material = mat; if (material_color && !mat.empty()) { const uint32_t col = material_color(mat); if (col) e.color = col; } e.glow = !material_glow || mat.empty() || material_glow(mat); e.rmin = num(*c, "area_circle_radius.min", 0); e.rmax = num(*c, "area_circle_radius.max", 0); if (material_alpha && !mat.empty() && !e.glow) e.alpha = (uint32_t)std::lround(std::clamp(material_alpha(mat), 0.0f, 1.0f) * 255.0f); if (material_alpha && !mat.empty() && !e.real && e.glow && num(*c, "render_ultrabright", 0) == 0) { // (premultiplied: these particles are additive; render_ultrabright (6b:10) draws them at the full colour) const float a = material_alpha(mat); auto ch = [&](int sh) { return (uint32_t)std::lround(((e.color >> sh) & 0xFFu) * a) << sh; }; e.color = (e.color & 0xFF000000u) | ch(16) | ch(8) | ch(0); } e.trail = num(*c, "is_trail", 0) != 0; e.trail_gap = num(*c, "trail_gap", 0); e.delay = std::max(0, (int)num(*c, "delay_frames", 0)); e.away = num(*c, "velocity_always_away_from_center", 0); e.attractor = num(*c, "attractor_force", 0); e.as_long = num(*c, "draw_as_long", 0) != 0; e.xv0 = num(*c, "x_vel_min", 0) / 60.0f; e.xv1 = num(*c, "x_vel_max", 0) / 60.0f; e.yv0 = num(*c, "y_vel_min", 0) / 60.0f; e.yv1 = num(*c, "y_vel_max", 0) / 60.0f; e.gravity = num(*c, "gravity.y", 0) / 3600.0f; e.count0 = (int)num(*c, "count_min", 1); e.count1 = std::max(e.count0, (int)num(*c, "count_max", 1)); e.life0 = num(*c, "lifetime_min", 0.2f) * 60.0f; e.life1 = std::max(e.life0, num(*c, "lifetime_max", 0.2f) * 60.0f); e.interval0 = std::max(1, (int)num(*c, "emission_interval_min_frames", 1)); e.interval1 = std::max(e.interval0, (int)num(*c, "emission_interval_max_frames", 1)); e.jitter = num(*c, "airflow_force", 0) * 0.004f; e.ox0 = num(*c, "x_pos_offset_min", 0); e.ox1 = num(*c, "x_pos_offset_max", 0); e.oy0 = num(*c, "y_pos_offset_min", 0); e.oy1 = num(*c, "y_pos_offset_max", 0); e.fade = num(*c, "fade_based_on_lifetime", 0) != 0; // (the component default is 0; 43 projectile emitters leave it out) e.collide = num(*c, "collide_with_grid", 1) != 0; e.force = num(*c, "cosmetic_force_create", 1) != 0; fx.emitters.push_back(e); } } } // namespace ng