Files
Noita-GPU-Vulkan/src/gun/projectiles.cpp
T
Gaël 00a9c1c26c Lot of bugfixes
* Adding clipper to generate the terrain's negative powder-contour offset.
* Fixing RDB issues (fracture, active trigger, performances)
* Fixing blood emission
* Fixing the freezing formula on liquids
* Fixing Steve spawning due to a missed test.
* Reaction table properly build
2026-10-21 20:46:13 +02:00

510 lines
34 KiB
C++

#include "gun/projectiles.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <sstream>
namespace ng {
namespace {
float attr(const EntityComponent* c, const char* k, float d) {
if (!c) return d;
auto it = c->attrs.find(k);
return it == c->attrs.end() ? d : (float)std::atof(it->second.c_str());
}
const EntityComponent* first(const EntityDef& d, const char* type) {
auto v = d.all(type);
return v.empty() ? nullptr : v.front();
}
constexpr double kDeg = 3.14159265358979323846 / 180.0;
} // namespace
std::string ProjectileSim::script_definition_key(const EntityDef& entity) {
std::string state = entity.tags;
for (const auto& c : entity.comps) {
state += c.type;
if (c.type == "VariableStorageComponent") continue;
for (const auto& [key, value] : c.attrs) {
// _ng_next and _ng_img_t are emitter clocks, not changes to its effect definition.
if (key.rfind("_ng_", 0) == 0 || key == "mWhoShot" || key == "mEntityThatShot" || key == "mShooterHerdId" || key.rfind("mVelocity", 0) == 0) continue;
state += key + "=" + value + ";";
}
}
return entity.path + "#script" + std::to_string(std::hash<std::string>()(state));
}
const ProjDef* ProjectileSim::def(const std::string& file) {
auto it = defs_.find(file);
if (it != defs_.end()) return &it->second;
if (const EntityDef* e = db_.get(file)) return def_from(file, *e);
return &(defs_[file] = ProjDef());
}
void ProjectileSim::disable_gpu_lua() {
gpu_lua_enabled = false;
for (auto& kv : defs_) if (kv.second.gpu_lua_every > 0) {
kv.second.gpu_lua_every = 0;
kv.second.needs_twin = true;
}
}
// The entity has components the GPU does not simulate (CellEater, MagicConvertMaterial, area effects, scripts...): the host keeps a runtime twin of the projectile
static bool host_runs(const EntityDef& e, bool gpu_script = false) {
if (!e.all("DamageModelComponent").empty()) return true;
for (const char* rich : {"CellEaterComponent", "MagicConvertMaterialComponent", "GameAreaEffectComponent", "AreaDamageComponent", "LightningComponent", "LaserEmitterComponent", "MaterialSeaSpawnerComponent", "TeleportProjectileComponent", "EnergyShieldComponent", "ExplosionComponent", "LooseGroundComponent", "ArcComponent", "LightningComponent", "BlackHoleComponent", "MaterialSuckerComponent"})
if (!e.all(rich).empty()) return true;
if (!gpu_script) for (const EntityComponent* lc : e.all("LuaComponent")) for (const auto& kv : lc->attrs) if (kv.first.rfind("script_", 0) == 0 && !kv.second.empty()) return true;
return false;
}
static bool gpu_script_contract(const EntityDef& e) {
if (!e.children.empty()) return false;
// Conservative: unknown components may require spawn/death/host effects.
for (const auto& c : e.comps)
if (c.type != "LuaComponent" && c.type != "ProjectileComponent" && c.type != "VelocityComponent" &&
c.type != "SpriteComponent" && c.type != "ParticleEmitterComponent" && c.type != "AudioComponent" &&
c.type != "LightComponent" && c.type != "VariableStorageComponent") return false;
const auto scripts = e.all("LuaComponent"), projectiles = e.all("ProjectileComponent"), velocities = e.all("VelocityComponent");
if (scripts.size() != 1 || projectiles.size() != 1 || velocities.size() != 1) return false;
const auto* lc = scripts.front();
if (lc->get("_enabled", "1") == "0" || projectiles.front()->get("_enabled", "1") == "0" || velocities.front()->get("_enabled", "1") == "0") return false;
if (lc->get("script_source_file") != "data/scripts/projectiles/grenade_anti_gravity.lua" || lc->get("execute_every_n_frame", "1") != "2") return false;
for (const char* key : {"execute_on_added", "execute_on_removed", "execute_times", "remove_after_executed", "enable_coroutines", "call_init_function"})
if (lc->get(key, "0") != "0") return false;
for (const auto& kv : lc->attrs) if (kv.first.rfind("script_", 0) == 0 && kv.first != "script_source_file" && !kv.second.empty()) return false;
return true;
}
static bool gpu_extra_contract(const EntityDef& e) {
if (!e.children.empty()) return false;
for (const auto& c : e.comps)
if (c.type != "HomingComponent" && c.type != "ParticleEmitterComponent" && c.type != "LightComponent" && c.type != "HitEffectComponent" && c.type != "VariableStorageComponent") return false;
return true;
}
static void parse_homing(const EntityDef& e, ProjDef& d) {
for (const EntityComponent* hc : e.all("HomingComponent")) {
if (hc->get("_enabled", "1") == "0" || d.homing.on) continue;
auto f = [&](const char* k, float v) { auto it = hc->attrs.find(k); return it == hc->attrs.end() ? v : (float)std::atof(it->second.c_str()); };
d.homing.on = true;
d.homing.detect = f("detect_distance", 150); d.homing.mult = f("homing_velocity_multiplier", 0.9f); d.homing.coeff = f("homing_targeting_coeff", 160); d.homing.max_turn = f("max_turn_rate", 0.05f);
d.homing.rotate = f("just_rotate_towards_target", 0) != 0; d.homing.who_shot = f("target_who_shot", 0) != 0;
d.homing.target = (int)f("predefined_target", 0);
const std::string tag = hc->get("target_tag", "homing_target");
d.homing.tag_bits = tag == "mortal" ? 512 : tag == "prey" ? 1024 : tag == "homing_target" ? 2048 : tag == "player_unit" ? 4096 : 0;
}
}
const ProjDef* ProjectileSim::def_from(const std::string& file, const EntityDef& entity, const std::string& extras) {
auto it0 = defs_.find(file);
if (it0 != defs_.end()) return &it0->second;
ProjDef d;
d.file = entity.path;
// Match EntityRuntime's CSV handling, including spaces around a tag.
std::istringstream tags(entity.tags);std::string tag;
while(std::getline(tags,tag,',')) {
auto begin=tag.find_first_not_of(' '), end=tag.find_last_not_of(' ');
if(begin!=std::string::npos) {
const auto token = tag.substr(begin,end-begin+1);
if(token=="projectile")d.query_projectile=true;
if(token=="orbit_projectile")d.orbiting=true;
}
}
unsigned velocity_count=0;
for(const auto* component:entity.all("VelocityComponent"))if(component->get("_enabled","1")!="0")++velocity_count;
d.query_velocity = velocity_count == 1;
d.query_unsupported = d.query_projectile && velocity_count > 1;
{
const EntityDef* e = &entity;
const EntityComponent* pc = first(*e, "ProjectileComponent");
// A thrown item that breaks (egg_monster.xml): PhysicsBodyCollisionDamageComponent (00be12a0) turns a new contact faster than speed_threshold into speed x damage_multiplier
// of damage, the DamageModel dies, ExplodeOnDamageComponent (00ae35d0, explode_on_death_percent) explodes with ITS config_explosion (load_this_entity: the hatchling).
// One impact decides it here (the hp is not kept between impacts): the break speed is max(speed_threshold, hp / damage_multiplier)
EntityComponent breakPc;
if (pc && !e->all("PhysicsBodyComponent").empty()) {
const EntityComponent* cd = first(*e, "PhysicsBodyCollisionDamageComponent");
const EntityComponent* dm = first(*e, "DamageModelComponent");
const EntityComponent* eod = first(*e, "ExplodeOnDamageComponent");
if (cd && dm && eod && attr(eod, "explode_on_death_percent", 1) > 0) {
const float mult = attr(cd, "damage_multiplier", 0.016667f), hp = attr(dm, "hp", 1);
d.break_speed = std::max(attr(cd, "speed_threshold", 60), mult > 0 ? hp / mult : 1e9f);
breakPc = *pc;
for (auto& [k, v] : eod->attrs) if (k.compare(0, 17, "config_explosion.") == 0) breakPc.attrs[k] = v;
breakPc.attrs["on_death_explode"] = "1";
pc = &breakPc;
}
}
const EntityComponent* vc = first(*e, "VelocityComponent");
d.speed_min = attr(pc, "speed_min", 300) / 60.0f;
d.speed_max = attr(pc, "speed_max", 300) / 60.0f;
d.damage = attr(pc, "damage", 0.1f) + attr(pc, "damage_by_type.projectile", 0.0f);
{
static const char* chan[12] = {"melee", "electricity", "fire", "ice", "slice", "drill", "radioactive", "poison", "healing", "curse", "holy", "explosion"};
for (int i = 0; i < 12; ++i) d.dmg_type[i] = attr(pc, (std::string("damage_by_type.") + chan[i]).c_str(), 0.0f);
}
d.lifetime = (int)attr(pc, "lifetime", 60);
d.on_collision_die = attr(pc, "on_collision_die", 1) != 0;
d.collide_with_world = attr(pc, "collide_with_world", 1) != 0; d.collide_with_entities = attr(pc, "collide_with_entities", 1) != 0; d.penetrate_entities = attr(pc, "penetrate_entities", 0) != 0;
d.damage_every_x_frames = std::max(1, (int)attr(pc, "damage_every_x_frames", 2));
d.bounces_left = (int)attr(pc, "bounces_left", 0);
d.die_on_liquid = attr(pc, "die_on_liquid_collision", 0) != 0;
d.displace_liquid = attr(vc, "displace_liquid", 1) != 0; // VelocityComponent, constructor 00776280: on
d.invisible = e->all("SpriteComponent").empty(); // (no SpriteComponent: the projectile itself draws nothing - wall_piece.xml is only its green particles and its light)
if (!pc) { // a loaded particle entity: SetStartVelocityComponent, LifetimeComponent, DieIfSpeedBelowComponent of its file
d.particle = true;
d.creature = first(*e, "AnimalAIComponent") || first(*e, "PhysicsAIComponent") || first(*e, "WormAIComponent");
if (d.creature) d.needs_twin = true; // (its twin event makes the creature)
d.damage = 0;
d.on_collision_die = false;
if (!first(*e, "SetStartVelocityComponent")) { d.speed_min = d.speed_max = 0; d.gravity = 0; } // a static spell entity (touch_*, fields, seas): it stays where it was cast
if (const EntityComponent* lt = first(*e, "LifetimeComponent")) d.lifetime = (int)attr(lt, "lifetime", 60);
if (const EntityComponent* ds = first(*e, "DieIfSpeedBelowComponent")) d.die_speed = attr(ds, "min_speed", 0) / 60.0f;
}
if (pc) { auto lt = pc->attrs.find("config_explosion.load_this_entity"); if (lt != pc->attrs.end()) d.load_files = lt->second; }
if (pc) for (int k = (int)attr(pc, "config_explosion.electricity_count", 0); k > 0; --k) d.load_files += (d.load_files.empty() ? "" : ",") + std::string("data/entities/misc/electricity.xml"); // 00657960: electricity_count loads of electricity.xml at the blast // the host spawns these as real entities (runtime) when the explosion happens
parse_homing(*e, d);
d.gpu_lua_every = gpu_lua_enabled && gpu_script_contract(*e) ? 2 : 0;
if (host_runs(*e, d.gpu_lua_every != 0)) d.needs_twin = true;
if (pc) { // damage_game_effect_entities: effect entity files loaded on the entity the projectile hits
const std::string ge = pc->get("damage_game_effect_entities");
for (size_t pos = 0; pos < ge.size();) { size_t end = ge.find(',', pos); if (end == std::string::npos) end = ge.size(); if (end > pos) d.hit_effects.push_back({"LOAD_GAME_EFFECT", ge.substr(pos, end - pos), "", "", 0.0f}); pos = end + 1; }
}
for (const EntityComponent* he : e->all("HitEffectComponent"))
d.hit_effects.push_back({he->get("effect_hit", ""), he->get("value_string", ""), he->get("condition_effect", ""), he->get("condition_status", ""), (float)std::atof(he->get("value", "0").c_str())});
d.friendly_fire = attr(pc, "friendly_fire", 0) != 0;
d.dont_hit_player = attr(pc, "dont_hit_player", 0) != 0;
d.collide_with_shooter_frames = (int)attr(pc, "collide_with_shooter_frames", -1); // (-1: never hits its shooter)
if (d.orbiting) {
d.friendly_fire = true;
d.dont_hit_player = false;
d.collide_with_shooter_frames = 6;
}
d.physics = !e->all("PhysicsBodyComponent").empty();
if (d.physics) d.on_collision_die = false;
d.explode_on_death = attr(pc, "on_death_explode", 0) != 0 || attr(pc, "on_lifetime_out_explode", 0) != 0;
d.explosion_radius = attr(pc, "config_explosion.explosion_radius", 20); // (the ConfigExplosion constructor 004b2780: radius 20, damage 5, hole_enabled on)
d.explosion_damage = attr(pc, "config_explosion.damage", 5);
if (!std::isfinite(d.explosion_radius) || d.explosion_radius < 0) d.explosion_radius = 0; // (a script's -inf / NaN: spells_to_power.lua's log(0); the game can crash on it)
if (!std::isfinite(d.explosion_damage)) d.explosion_damage = 0;
d.hole = attr(pc, "config_explosion.hole_enabled", 1) != 0;
if (pc) { auto it = pc->attrs.find("config_explosion.create_cell_material"); d.explosion_material = it != pc->attrs.end() ? it->second : "fire"; } // ConfigExplosion constructor 004b2780: create_cell_material "fire", create_cell_probability 5
d.explosion_cell_probability = attr(pc, "config_explosion.create_cell_probability", 5);
d.max_durability = attr(pc, "config_explosion.max_durability_to_destroy", 10);
d.stains = attr(pc, "config_explosion.stains_enabled", 1) != 0; d.stains_radius = attr(pc, "config_explosion.stains_radius", 0);
d.physics_min = attr(pc, "config_explosion.physics_explosion_power.min", 0);
d.physics_max = attr(pc, "config_explosion.physics_explosion_power.max", 0.2f);
d.ragdoll_explosion_mult = attr(pc, "config_explosion.physics_multiplier_ragdoll_force", 1.0f);
d.ray_energy = attr(pc, "config_explosion.ray_energy", 20000);
d.explosion_audio = attr(pc, "config_explosion.audio_enabled", 1) != 0;
if (pc) { auto it = pc->attrs.find("config_explosion.audio_event_name"); if (it != pc->attrs.end()) d.explosion_audio_event = it->second; }
d.explosion_audio_liquid = attr(pc, "config_explosion.audio_liquid_amount_normalized", 0);
d.knockback_force = attr(pc, "config_explosion.knockback_force", 1.0f);
d.damage_mortals = attr(pc, "config_explosion.damage_mortals", 1) != 0;
d.dont_damage_shooter = attr(pc, "explosion_dont_damage_shooter", 0) != 0;
d.not_scaled_by_gamefx = attr(pc, "config_explosion.not_scaled_by_gamefx", 0) != 0;
d.hit_knockback_force = attr(pc, "knockback_force", 0.0f);
d.blood_count_multiplier = attr(pc, "blood_count_multiplier", 1.0f);
d.ragdoll_force_multiplier = attr(pc, "ragdoll_force_multiplier", 0.025f);
if (pc) { // (the packet's RAGDOLL_FX: 00c091f0 copies ragdoll_fx_on_collision)
static const char* fxNames[10] = {"NONE", "NORMAL", "BLOOD_EXPLOSION", "BLOOD_SPRAY", "FROZEN", "CONVERT_TO_MATERIAL", "CUSTOM_RAGDOLL_ENTITY", "DISINTEGRATED", "NO_RAGDOLL_FILE", "PLAYER_RAGDOLL_CAMERA"};
const auto it = pc->attrs.find("ragdoll_fx_on_collision");
if (it != pc->attrs.end()) for (int k = 0; k < 10; ++k) if (it->second == fxNames[k]) d.ragdoll_fx = k;
}
d.velocity_mass = attr(vc, "mass", 0.05f); // (VelocityComponent mass, default 0.05: component documentation)
if (!d.particle) d.spread_rad = attr(pc, "direction_random_rad", 0);
if (pc) { // world collision (00c073b0)
d.penetrate_world = attr(pc, "penetrate_world", 0) != 0;
d.penetrate_world_velocity_coeff = attr(pc, "penetrate_world_velocity_coeff", 0.6f);
d.ground_penetration_coeff = attr(pc, "ground_penetration_coeff", 0);
d.ground_penetration_max_durability = (int)attr(pc, "ground_penetration_max_durability_to_destroy", 0);
d.bounce_energy = attr(pc, "bounce_energy", 0.5f);
d.bounce_always = attr(pc, "bounce_always", 0) != 0;
d.bounce_at_any_angle = attr(pc, "bounce_at_any_angle", 0) != 0;
d.on_collision_remove_projectile = attr(pc, "on_collision_remove_projectile", 0) != 0;
d.die_on_low_velocity = attr(pc, "die_on_low_velocity", 0) != 0;
d.die_on_low_velocity_limit = attr(pc, "die_on_low_velocity_limit", 50);
d.physics_impulse_coeff = attr(pc, "physics_impulse_coeff", 300);
d.damage_scaled_by_speed = attr(pc, "damage_scaled_by_speed", 0) != 0;
d.damage_scale_max_speed = attr(pc, "damage_scale_max_speed", 0);
d.lifetime_randomness = (int)attr(pc, "lifetime_randomness", 0);
d.bounce_fx_file = pc->get("bounce_fx_file", "");
if (attr(pc, "on_collision_spawn_entity", 1) != 0) { d.spawn_entity = pc->get("spawn_entity", ""); d.spawn_entity_is_projectile = attr(pc, "spawn_entity_is_projectile", 0) != 0; }
{ const std::string pt = pc->get("projectile_type", ""); d.projectile_type = pt == "LIGHTNING" ? 1 : pt == "VERLET" ? 2 : pt == "MATERIAL_PARTICLE" ? 3 : 0; }
if (d.projectile_type == 2) { // VERLET: the chain is the child entity with a VerletPhysicsComponent, its point sprites its SpriteComponents; the damage the root's VerletWeaponComponent
ProjDef::Verlet& v = d.verlet;
for (const EntityComponent* vw : e->all("VerletWeaponComponent")) {
v.dmg_radius = attr(vw, "damage_radius", 5); v.dmg_min = attr(vw, "damage_min_step", 0.01f); v.dmg_max = attr(vw, "damage_max", 1); v.dmg_coeff = attr(vw, "damage_coeff", 1);
v.impulse = attr(vw, "impulse_coeff", 1); v.fade = (int)attr(vw, "fade_duration_frames", 10);
v.force_radius = attr(vw, "physics_force_radius", 3); v.phys_impulse = attr(vw, "physics_impulse_coeff", 1);
break;
}
for (const EntityDef::Child& ch : e->children) {
const EntityDef* cd = db_.get(ch.path);
if (!cd || cd->all("VerletPhysicsComponent").empty()) continue;
const EntityComponent* vp = cd->all("VerletPhysicsComponent").front();
v.points = std::clamp((int)attr(vp, "num_points", 2), 2, 16); v.rest = attr(vp, "resting_distance", 2); v.stiff = attr(vp, "stiffness", 1);
v.damp = attr(vp, "velocity_dampening", 0.99f); v.liquid_damp = attr(vp, "liquid_damping", 0.7f); v.cells = attr(vp, "collide_with_cells", 1) != 0;
for (const EntityComponent* sc : cd->all("SpriteComponent")) {
SpriteFrames sf;
ProjDef::Verlet::Part part;
if (load_sprite_frames(wak_, sc->get("image_file"), sc->get("rect_animation"), sf)) { part.tex.assign(sf.texels.begin(), sf.texels.begin() + (size_t)sf.w * sf.h); part.w = sf.w; part.h = sf.h; }
part.ox = attr(sc, "offset_x", 0); part.oy = attr(sc, "offset_y", 0);
v.parts.push_back(std::move(part));
}
v.on = true;
break;
}
}
if (attr(pc, "on_death_emit_particle", 0) != 0 || pc->get("projectile_type", "") == "MATERIAL_PARTICLE") { d.death_particle = pc->get("on_death_emit_particle_type", ""); d.death_particle_count = (int)attr(pc, "on_death_emit_particle_count", 1); }
}
if (vc) {
d.gravity = attr(vc, "gravity_y", 400) / 3600.0f;
d.air_friction = attr(vc, "air_friction", 0.55f); // constructor 00776280
d.liquid_drag = attr(vc, "liquid_drag", 1.0f);
d.gravity_x = attr(vc, "gravity_x", 0) / 3600.0f;
d.terminal_velocity = attr(vc, "terminal_velocity", 1000);
d.apply_terminal_velocity = attr(vc, "apply_terminal_velocity", 1) != 0;
} else {
d.gravity = 0;
}
for (const EntityComponent* c : e->all("SpriteComponent")) {
if (c->get("_enabled", "1") == "0") continue;
SpriteFrames sf;
if (load_sprite_frames(wak_, c->get("image_file"), c->get("rect_animation"), sf)) {
d.sprite = std::move(sf.texels); d.sprite_w = sf.w; d.sprite_h = sf.h; d.sprite_frames = sf.frames; d.sprite_wait = sf.wait;
break;
}
}
if (d.sprite.empty()) { // physics projectiles (bombs) are drawn from their physics image
for (const EntityComponent* c : e->all("PhysicsImageShapeComponent")) {
SpriteFrames sf;
if (load_sprite_frames(wak_, c->get("image_file"), "", sf)) { if (fxlib && fxlib->material_color && !c->get("material").empty()) tint_key_image(sf.texels, fxlib->material_color(c->get("material"))); d.sprite = std::move(sf.texels); d.sprite_w = sf.w; d.sprite_h = sf.h; d.sprite_frames = 1; break; }
}
}
if (fxlib) {
fxlib->projectile(*e, d.fx);
if (pc) {
auto mf = pc->attrs.find("muzzle_flash_file");
if (mf != pc->attrs.end()) if (const EntityDef* fd = db_.get(mf->second)) fxlib->muzzle(*fd, d.fx.muzzle);
}
}
if (!d.sprite.empty()) { // colour for sprite-less rendering: average of the opaque pixels
uint64_t r = 0, g = 0, b = 0, n = 0;
for (uint32_t t : std::vector<uint32_t>(d.sprite.begin(), d.sprite.begin() + (size_t)d.sprite_w * d.sprite_h)) { if ((t >> 24) < 128) continue; r += (t >> 16) & 255; g += (t >> 8) & 255; b += t & 255; ++n; }
if (n) d.color = 0xFF000000u | (uint32_t)(r / n) << 16 | (uint32_t)(g / n) << 8 | (uint32_t)(b / n);
}
}
if (!extras.empty()) apply_extras(d, extras);
d.extras = extras;
return &(defs_[file] = d);
}
void ProjectileSim::apply_extras(ProjDef& d, const std::string& extrasIn) {
std::string extras = extrasIn;
const size_t cp = extras.find("|cm=");
if (cp != std::string::npos) { // the shot's material / material_amount (00c0b440: config_explosion create_cell_material +41c, create_cell_probability += amount +434)
size_t ce = extras.find('|', cp + 4); if (ce == std::string::npos) ce = extras.size();
const std::string cm = extras.substr(cp + 4, ce - cp - 4);
extras = extras.substr(0, cp) + extras.substr(ce);
const size_t colon = cm.find(':');
if (colon != std::string::npos) { d.explosion_material = cm.substr(0, colon); d.explosion_cell_probability += (float)std::atof(cm.c_str() + colon + 1); }
}
const size_t gp = extras.find("|ge=");
if (gp != std::string::npos) { // the shot's game_effect_entities (00c0b440: forwarded through the effect-application helper): effect files loaded on the hit entity
const std::string ge = extras.substr(gp + 4);
extras = extras.substr(0, gp);
for (size_t pos = 0; pos < ge.size();) { size_t end = ge.find(',', pos); if (end == std::string::npos) end = ge.size(); if (end > pos) d.hit_effects.push_back({"LOAD_GAME_EFFECT", ge.substr(pos, end - pos), "", "", 0.0f}); pos = end + 1; }
}
size_t pos = 0;
while (pos < extras.size() && fxlib) {
size_t end = extras.find(',', pos);
if (end == std::string::npos) end = extras.size();
const std::string path = extras.substr(pos, end - pos);
pos = end + 1;
if (path.empty()) continue;
if (const EntityDef* e = db_.get(path)) {
if (d.gpu_lua_every > 0 && !gpu_extra_contract(*e)) d.needs_twin = true;
if (host_runs(*e)) d.needs_twin = true; // an extra entity with scripts (SPELLS_TO_POWER, the orbit / clipping / accelerating shots...): it lives as a child of the projectile's twin
for (const EntityComponent* he : e->all("HitEffectComponent")) // the effects of the extra entities (the hit fx modifiers of the wands)
d.hit_effects.push_back({he->get("effect_hit", ""), he->get("value_string", ""), he->get("condition_effect", ""), he->get("condition_status", ""), (float)std::atof(he->get("value", "0").c_str())});
parse_homing(*e, d); // (a homing spell adds its HomingComponent through the extra entities)
for (const EntityComponent* lc : e->all("LuaComponent")) { // the bounce modifiers (bounce_explosion.lua, bounce_spark, bounce_laser, bounce_larpa): a run-once script of the extra entity sets the
// projectile's bounce_fx_file; the engine runs it at the projectile's first update, before any bounce, and the twin's retype comes two frames late (a shot at a near
// wall bounced without its explosion), so the file it assigns is taken here
const std::string sf = lc->get("script_source_file", "");
std::vector<uint8_t> raw;
if (sf.empty() || !wak_.read(sf, raw)) continue;
const std::string src(raw.begin(), raw.end());
const size_t k = src.find("vars.bounce_fx_file");
const size_t q0 = k == std::string::npos ? k : src.find('"', k), q1 = q0 == std::string::npos ? q0 : src.find('"', q0 + 1);
if (q1 != std::string::npos) d.bounce_fx_file = src.substr(q0 + 1, q1 - q0 - 1);
}
ProjFx tmp;
fxlib->projectile(*e, tmp);
d.fx.emitters.insert(d.fx.emitters.end(), tmp.emitters.begin(), tmp.emitters.end());
if (!d.fx.light.on && tmp.light.on) d.fx.light = tmp.light;
}
}
}
const ProjDef* ProjectileSim::def(const std::string& file, const std::string& extras) {
if (extras.empty()) return def(file);
const std::string key = file + "|" + extras;
auto it = defs_.find(key);
if (it != defs_.end()) return &it->second;
ProjDef d = *def(file);
apply_extras(d, extras);
d.extras = extras;
return &(defs_[key] = d);
}
const ProjDef* ProjectileSim::explosion_variant(const ProjDef* base, float add_r, float add_dmg) {
if (!base || !(add_r > 0.0f)) return base;
const auto key = std::make_tuple(base, add_r, add_dmg);
if (auto it = xvars_.find(key); it != xvars_.end()) return &it->second;
ProjDef d = *base;
const float R = std::max(base->explosion_radius + add_r, add_r); // (00c0c539: the larger of the component's new radius and the cast's)
const char* spr = nullptr;
float dmin = -1.0f, ray = -1.0f, shake = -1.0f;
int s0 = d.fx.boom.sparks0, s1 = d.fx.boom.sparks1;
bool sparks = d.fx.boom.sparks;
if (R >= 128.0f) { dmin = 20.0f; spr = "data/particles/explosion_128.xml"; sparks = true; s0 = std::max(s0, 100); s1 = std::max(s1, 200); ray = 1200000.0f; shake = 60.0f; }
else if (R >= 64.0f) { dmin = 15.0f; spr = "data/particles/explosion_128.xml"; sparks = true; s0 = 20; s1 = 40; ray = 800000.0f; shake = 40.0f; }
else if (R >= 32.0f) { dmin = 13.0f; spr = "data/particles/explosion_064.xml"; sparks = true; s0 = std::max(s0, 7); s1 = std::max(s1, 20); ray = 500000.0f; shake = 30.0f; }
else if (R >= 16.0f) { spr = "data/particles/explosion_032.xml"; sparks = true; s0 = std::max(s0, 5); s1 = std::max(s1, 10); ray = 200000.0f; shake = 20.0f; }
else if (R >= 12.0f) spr = "data/particles/explosion_012.xml";
else if (R >= 8.0f) spr = "data/particles/explosion_008.xml";
if (dmin >= 0.0f) d.explosion_damage = std::max(base->explosion_damage + add_dmg, dmin) - add_dmg;
if (ray >= 0.0f) d.ray_energy = std::max(d.ray_energy, ray);
d.stains_radius = 1.5f * R; // (+0xec)
if (R >= 10.0f) d.explosion_cell_probability = std::max(d.explosion_cell_probability, 5.0f); // (+0xa8, the material default "fire"; and shake_vegetation on, not modelled)
ExplosionFx& fx = d.fx.boom;
if (spr && fxlib) if (const SpriteFrames* sf = fxlib->sprite(spr)) { const bool ownMat = fx.material_color != fx.color; fx.sprite = sf; fx.color = fxlib->sprite_color(sf); if (!ownMat) fx.material_color = fx.color; }
fx.sparks = sparks; fx.sparks0 = s0; fx.sparks1 = s1;
if (shake >= 0.0f) fx.shake = std::max(fx.shake, shake);
const float coeff = fx.radius > 0.0f && fx.light_radius > 0.0f ? fx.light_radius / fx.radius : 8.0f;
fx.radius = R; fx.light_radius = R * coeff;
fx.wait = 0; // (explosion_sprite_lifetime 0: the sprite plays its own animation)
fx.valid = fx.sprite || fx.sparks || fx.material_sparks;
return &(xvars_[key] = std::move(d));
}
void ProjectileSim::cast(const std::shared_ptr<ShotResult>& owner, const Shot& shot, double x, double y, double dirx, double diry, double child_mult) {
const double base = std::atan2(diry, dirx);
const int n = (int)shot.projectiles.size();
const double spread = std::max(0.0, shot.state.get("spread_degrees", 0)) * kDeg;
const double pattern = shot.state.get("pattern_degrees", 0) * kDeg;
const double speed_mult = shot.state.get("speed_multiplier", 1) * child_mult;
std::uniform_real_distribution<double> U(0.0, 1.0);
for (int i = 0; i < n; ++i) {
const ShotProjectile& sp = shot.projectiles[i];
const ProjDef* d = explosion_variant(def(sp.file), (float)shot.state.get("explosion_radius", 0), (float)shot.state.get("damage_explosion_add", 0));
SimProjectile p;
double ang = base + (U(rng_) - 0.5) * 2.0 * spread * 0.5 + (U(rng_) - 0.5) * 2.0 * d->spread_rad;
if (n > 1 && pattern > 0) ang += -pattern * 0.5 + pattern * (double)i / (double)(n - 1);
const double speed = (d->speed_min + (d->speed_max - d->speed_min) * U(rng_)) * speed_mult;
p.x = x; p.y = y;
p.vx = std::cos(ang) * speed; p.vy = std::sin(ang) * speed;
p.life = std::max(1, d->lifetime + (int)shot.state.get("lifetime_add", 0));
p.damage = d->damage + (float)(shot.state.get("damage_projectile_add") + shot.state.get("damage_melee_add") + shot.state.get("damage_electricity_add") +
shot.state.get("damage_fire_add") + shot.state.get("damage_ice_add") + shot.state.get("damage_slice_add") +
shot.state.get("damage_drill_add"));
p.gravity = d->gravity + shot.state.get("gravity", 0) / 3600.0;
p.friction = d->air_friction;
p.bounces = (int)shot.state.get("bounces", 0);
p.def = d;
p.explosion_radius = d->explosion_radius + (float)shot.state.get("explosion_radius", 0);
p.explosion_damage = d->explosion_damage + (float)shot.state.get("damage_explosion_add", 0);
p.owner = owner;
p.spec = &sp;
p.fired.assign(sp.triggers.size(), 0);
projectiles.push_back(std::move(p));
}
}
void ProjectileSim::fire_triggers(SimProjectile& p, int mode, double dx, double dy) {
if (!p.spec) return;
std::uniform_real_distribution<double> U(0.0, 1.0);
for (size_t i = 0; i < p.spec->triggers.size(); ++i) {
const auto k = p.spec->triggers[i].type;
const bool go = mode == 1 ? (k == ShotTrigger::Timer && p.spec->triggers[i].delay > 0 && p.age == p.spec->triggers[i].delay)
: (mode == 2 || mode == 3) ? (k == ShotTrigger::Timer || k == ShotTrigger::HitWorld)
: mode == 4 ? k == ShotTrigger::Timer : k == ShotTrigger::Death;
if (!go) continue;
double vx = dx, vy = dy;
if (mode == 5 && (vx * vx + vy * vy) * 3600.0 < 100.0) { const double a = U(rng_) * 6.2831855; vx = std::cos(a); vy = std::sin(a); } // 00c0e3e0: at rest, a random direction
fire_trigger(p, (int)i, vx, vy, mode == 3, mode == 2);
}
}
void ProjectileSim::fire_trigger(SimProjectile& p, int index, double dx, double dy, bool keep, bool world_hit) {
if (p.fired[index]) return;
if (!keep) p.fired[index] = 1;
const ShotTrigger& t = p.spec->triggers[index];
if (!t.shot) return;
const double len = std::hypot(dx, dy);
if (len < 1e-6) { dx = 0; dy = 1; } else { dx /= len; dy /= len; }
const double x = p.x, y = p.y; // at the projectile itself (00c0b440)
auto owner = p.owner;
const Shot* sub = t.shot.get();
const double cm = world_hit ? sub->state.get("child_speed_multiplier", 1) : 1.0; // 00c06250: a world hit scales the payload by its own child_speed_multiplier
deferred_.push_back([this, owner, sub, x, y, dx, dy, cm]() { cast(owner, *sub, x, y, dx, dy, cm); });
}
void ProjectileSim::kill(SimProjectile& p, const Hooks& h, bool hit_world) {
if (!p.alive) return;
p.alive = false;
if (hit_world) fire_triggers(p, 2, -p.vx, -p.vy); // (the hit that stops it: no surface normal here, back the way it came)
if (p.age >= p.life) fire_triggers(p, 4, p.vx, p.vy);
fire_triggers(p, 5, p.vx, p.vy);
if (p.def && p.def->explode_on_death && p.explosion_radius > 0) {
if (h.area_damage) h.area_damage(p.x, p.y, p.explosion_radius, p.def->damage_mortals ? p.explosion_damage : 0.0, p.def->knockback_force);
if (h.dig && p.def->hole) h.dig(p.x, p.y, p.explosion_radius, p.def->max_durability);
if (h.spawn_material && !p.def->explosion_material.empty() && p.def->explosion_cell_probability > 0)
h.spawn_material(p.x, p.y, p.explosion_radius, p.def->explosion_material, p.def->explosion_cell_probability / 100.0);
}
}
void ProjectileSim::step(const Hooks& h) {
for (SimProjectile& p : projectiles) {
if (!p.alive) continue;
++p.age;
fire_triggers(p, 1, p.vx, p.vy); // the countdown runs before the flight of the update (00c06c60)
p.vy += p.gravity;
if (p.friction > 0) { const double f = 1.0 - p.friction / 60.0; p.vx *= f; p.vy *= f; }
const int steps = std::max(1, (int)std::ceil(std::max(std::fabs(p.vx), std::fabs(p.vy))));
for (int i = 0; i < steps && p.alive; ++i) {
const double nx = p.x + p.vx / steps, ny = p.y + p.vy / steps;
if (h.solid && h.solid((int)std::floor(nx), (int)std::floor(ny))) {
if (p.bounces > 0 || !p.def->on_collision_die) { // bounce off the surface (axis that is blocked)
const bool bx = h.solid((int)std::floor(nx), (int)std::floor(p.y));
const bool by = h.solid((int)std::floor(p.x), (int)std::floor(ny));
if (bx || !by) p.vx = -p.vx * 0.85;
if (by || !bx) p.vy = -p.vy * 0.85;
fire_triggers(p, 2, p.vx, p.vy); // 00c06250: a world hit fires the hit-world and waiting timers in the bounced direction, the projectile lives on
if (p.bounces > 0) --p.bounces;
else if (std::hypot(p.vx, p.vy) < 0.2) p.alive = false;
break;
}
kill(p, h, true);
break;
}
p.x = nx; p.y = ny;
if (h.hit_creature && h.hit_creature(p.x, p.y, 2.0, p.damage)) { fire_triggers(p, 3, p.vx, p.vy); kill(p, h, false); break; }
}
if (!p.alive) continue;
if (p.age >= p.life) kill(p, h, false);
}
projectiles.erase(std::remove_if(projectiles.begin(), projectiles.end(), [](const SimProjectile& p) { return !p.alive; }), projectiles.end());
auto pending = std::move(deferred_);
deferred_.clear();
for (auto& f : pending) f();
}
} // namespace ng