#version 450 #extension GL_GOOGLE_include_directive : require layout(local_size_x = 64) in; #include "fx_common.glsl" layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; #include "biome_mods.glsl" // Flight of the wand projectiles, one invocation per projectile: gravity, air friction, collisions with the world (die or bounce) // and with creatures, triggers (timer, hit, death) that queue the next shot of the wand, explosions (damage, holes, material, push of rigid // bodies, visuals), particle emitters, light and the sprite. const int TF_COLLISION_DIE = 1, TF_EXPLODE = 2, TF_HOLE = 4, TF_ROTATE = 8, TF_ADDITIVE = 16, TF_PHYSICS = 32, TF_DIE_IN_LIQUID = 64, TF_FRIENDLY = 128, TF_NO_PLAYER = 256, TF_HIT_EFFECTS = 512; // The entity a projectile touches: the player or a creature / prop. The shooter's herd is spared (ex8.y of an entity holds its herd + 1) unless the projectile has friendly fire. // What the shooter deals: the DAMAGE_MULTIPLIER game effect etc. (ex7.y of creatures and the player, set by the host) // The critical hit of a hit: the chance (percent) of the shot, +100 with the CRITICAL_HIT_BOOST of the type while the hit entity has the condition, + what the shooter's game effects add (ex7.z); // the damage is multiplied by 5 (DAMAGE_CRITICAL_HIT_MULTIPLIER); a chance above 100 % multiplies the multiplier by the extra, the victim's critical_damage_resistance scales it float crit_mult(int chance, int flags, int shooter, int victim, uint seed) { float c = float(chance); const int cond = (flags >> 10) & 7; if (cond != 0 && victim >= 0) { // the HitEffectComponent conditions are on the HIT entity const Ent s = ents[victim]; const int z = s.ex8.z; const bool fresh = ((z >> 8) & 0xFFFFFF) + 900 > (pc.frame & 0xFFFFFF); bool ok = false; if (cond == 1) ok = s.ex4.w > 0.0; else if (cond == 2) ok = fresh && (z & 4) != 0; else if (cond == 3) ok = fresh && (z & 2) != 0; else if (cond == 4) ok = fresh && (z & 1) != 0; if (ok) c += 100.0; } if (shooter > 0) { const Ent s = ents[shooter - 1]; if (s.st.x == 1 || s.st.x == 2 || s.st.x == 3) c += s.ex7.z; } if (c <= 0.0) return 1.0; // 00ab9a90: rand % 100 + 1 <= chance; m = the packet's multiplier (5), x chance / 100 above 100 %; the victim's critical_damage_resistance r gives (1 - r) (m - 1) + 1, no crit at r >= 1 const float r = victim >= 0 ? ents[victim].ex19.x : 0.0; if (r >= 1.0 || float(hash3(seed, 17u, 99u) % 100u) >= c) return 1.0; const float m = c > 100.0 ? 5.0 * c / 100.0 : 5.0; return (1.0 - r) * (m - 1.0) + 1.0; } // A critical hit (00ab9a90: the multiplier after the resistance above 1): the host loads data/entities/particles/critical_hit.xml at the point of the message and marks the damage number (event kind -9) void note_crit(vec2 at, int victim) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(at, -9.0, float(victim)); if (victim >= 0) atomicOr(ents[victim].ex3.z, 0x10000); // a critical hit this tick: the victim's hit flash is red (00ab5b00: shader 6 + crit) } // DAMAGE_BOOST of a hit effect: the type flags hold the stain it needs on the victim (bits 24-26, 1 + the stain bit) and the factor (bits 27-30) float boost_of(int flags, int victim) { const int need = (flags >> 24) & 7, mult = (flags >> 27) & 15; if (need == 0 || mult == 0) return 1.0; const int z = ents[victim].ex8.z; return (((z >> (need - 1)) & 1) != 0 && ((z >> 8) & 0xFFFFFF) + 900 > (pc.frame & 0xFFFFFF)) ? float(mult) : 1.0; } vec2 gAhead; // the next point of the projectile's path (a body's pixel there is a hit on its entity) // 00c08d10: the shooter (mWhoShot) is hit only when collide_with_shooter_frames is not -1 and has run out (bits 16-23 of the flags: 255 = -1, never); friendly_fire // (00c091f0) only lets the shooter's herd be hit. A shot with the friendly_fire modifier (PIERCING_SHOT...) gets friendly_fire 1 and 6 frames (00c0b440): shot_flags. bool shooter_safe(int flags, int age) { const int f = (flags >> 16) & 255; return f == 255 || age < f; } int shot_flags(int typeFlags, int dz) { return ((dz >> 10) & 1) != 0 ? (typeFlags & ~(255 << 16)) | TF_FRIENDLY | (6 << 16) : typeFlags; } bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit) { hit = -1; for (int k = 0; k < ctrl.inp.w; ++k) { const int kind = ents[k].st.x; if (kind == 0 && (ents[k].st.w & 64) != 0 && ents[k].ex19.w > 0.5) { // a worm's segment (WormComponent hitbox_radius around every part): the hit goes to the worm const int root = int(ents[k].ex19.w) - 1; if (root <= 0 || root >= ctrl.inp.w || !(ents[root].st.x == 1 || ents[root].st.x == 2) || (ents[root].st.w & 8) != 0) continue; if (root == shooter - 1 && shooter_safe(flags, age)) continue; if ((flags & TF_FRIENDLY) == 0 && !(shooter > 0 && (ents[shooter - 1].ex8.x & 4) != 0) && herd != 0 && ents[root].ex8.y == herd) continue; if (distance(pos, ents[k].a.xy) <= ents[k].ex19.z + 1.0) { hit = root; return true; } continue; } if (k == 0 ? (kind != 3 || (flags & TF_NO_PLAYER) != 0) : !(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8))) continue; if ((ents[k].st.w & (8 | 4096)) != 0) continue; // (4096: its sprites, damage model and hitbox are switched off - Olematon away from an Evil Eye) if (k == shooter - 1 && shooter_safe(flags, age)) continue; if ((flags & TF_FRIENDLY) == 0 && !(shooter > 0 && (ents[shooter - 1].ex8.x & 4) != 0) && herd != 0 && ents[k].ex8.y == herd && kind != 4) continue; // (a berserk shooter has no herd: its projectiles hurt everybody) if (kind == 4 && ents[k].ex7.w > 0.5) { if (body_slot(gAhead) == k || body_slot(pos) == k) { hit = k; return true; } continue; } // no HitboxComponent: its pixels (00c073b0) const float r = k == 0 ? 1.0 : 2.0; const vec2 d = pos - ents[k].a.xy; if (d.x >= ents[k].box.x - r && d.x <= ents[k].box.y + r && d.y >= ents[k].box.z - r && d.y <= ents[k].box.w + r) { hit = k; return true; } } return false; } bool creature_at(vec2 pos, float r, out int hit) { hit = -1; for (int k = 1; k < ctrl.inp.w; ++k) { int kind = ents[k].st.x; if (!(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8)) || (ents[k].st.w & 8) != 0) continue; vec2 d = pos - ents[k].a.xy; if (d.x >= ents[k].box.x - r && d.x <= ents[k].box.y + r && d.y >= ents[k].box.z - r && d.y <= ents[k].box.w + r) { hit = k; return true; } } return false; } void queue_shot(int node, vec2 pos, vec2 vel, uint seed, int team, int shooter, int herd) { float len = length(vel); vec2 dir = len < 1e-6 ? vec2(0.0, 1.0) : vel / len; int next = (pc.frame + 1) & 1; int slot = next == 0 ? atomicAdd(hhead.x, 1) : atomicAdd(hhead.y, 1); if (slot >= 256) return; hcast[(next * 256 + slot) * 2] = vec4(pos, dir); // at the trigger's position itself (00c0b440 puts the new projectile there) hcast[(next * 256 + slot) * 2 + 1] = vec4(float(node), float(team + (herd << 3)), float(seed & 0xFFFFFFu), float(shooter * 2)); } // The triggers of a projectile (recorded by the natives 00b283e0 / 00b28510 / 00b28600 into three lists of the ProjectileComponent: timers +0x610, hit-world +0x61c, death +0x628; // kinds here 1 timer, 2 hit world, 3 death). mode 1: the update's countdown (00c06c60): a timer whose delay has run out fires and goes; 2: a hit on the world (00c073b0 -> 00c06250): // the hit-world and the waiting timers fire and go; 3: an impact on a creature (00c091f0): the hit-world and the waiting timers fire and stay (a projectile that pierces casts // them again at its next impact); 4: the lifetime runs out (00c06fc0): the waiting timers fire; 5: the projectile is removed (00c0e3e0): the death triggers fire void fire_triggers(int spec, inout int fired, int mode, int age, vec2 pos, vec2 vel, uint seed, int team, int shooter, int herd) { ivec4 s0 = hspecs[spec * 4]; for (int t = 0; t < min(s0.y, 3); ++t) { ivec4 tr = hspecs[spec * 4 + 1 + t]; // kind, delay, node if ((fired & (1 << t)) != 0 || tr.z < 0) continue; // (mode 1: the counter starts at the delay and fires when the decrement reaches 0: a delay of 0 never does) bool go = mode == 1 ? (tr.x == 1 && tr.y > 0 && age == tr.y) : (mode == 2 || mode == 3) ? (tr.x == 1 || tr.x == 2) : mode == 4 ? tr.x == 1 : tr.x == 3; if (!go) continue; vec2 v = vel; if (mode == 5 && dot(vel, vel) * 3600.0 < 100.0) { // 00c0e3e0: nearly at rest (|v| < 10 px/s): a random direction (and length 20..80 px/s) for each death trigger const float ang = rnd01(hash3(seed, uint(t), 131u)) * 6.2831855; v = vec2(cos(ang), sin(ang)) * (rnd01(hash3(seed, uint(t), 132u)) * 60.0 + 20.0) / 60.0; } if (mode != 3) fired |= 1 << t; queue_shot(mode == 2 ? tr.w : tr.z, pos, v, seed + uint(t), team, shooter, herd); // (.w: the payload with its child_speed_multiplier, 00c06250) } } void explode(vec2 pos, float radius, float damage, int type, int team, int shooter, int herd, int critChance, uint critSeed) { int tb = type * TYPE_STRIDE; vec4 t2 = ftf(tb + 2); ivec4 t1 = fti(tb + 1); int cellMat = int(ft[tb + 2].w); float cellProb = uf(ft[tb + 3].x); if (radius <= 0.0) return; for (int k = 1; k < ctrl.inp.w; ++k) // corpse parts in range bleed if (ents[k].st.x == 4 && ents[k].ex7.y > 0.0 && distance(ents[k].a.xy, pos) <= radius) body_damage(k, pos, 1.0); // 00657960 (docs/EXPLOSION_KNOCKBACK_AND_VINES.md): every entity whose position is within explosion_radius, except the config's dont_damage_this (the shooter only when the projectile has // explosion_dont_damage_shooter), takes the config's damage in full (only with damage_mortals; the perks scaled it at the Shot) and gets a knockback: scalar = radius * knockback_force * // 10 * (0.5 t + 0.5), t = 1 - distance / radius clamped; its direction is the radial physics force (lerp of physics_explosion_power.min / .max by t: zero = no kick); it is compared with // minimum_knockback_force before the division by the CharacterDataComponent mass, the vertical part is always upward, and a killing hit gives none const vec4 t8 = ftf(tb + 8); const bool mortals = t8.w > 0.5; const vec2 pw = unpackHalf2x16(ft[tb + 7].w); // physics_explosion_power.min / .max if (mortals && damage > 0.0) { for (int k = 0; k < ctrl.inp.w; ++k) { int kind = ents[k].st.x; if (k == 0 ? (ents[0].st.w & 8) != 0 : (!(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8)) || (ents[k].st.w & (8 | 4096)) != 0)) continue; if ((t1.w & 16384) != 0 && k == shooter - 1) continue; const vec2 c = ents[k].a.xy; const float dist = distance(c, pos); if (dist > radius) continue; const float ck = crit_mult(critChance, t1.w, shooter, k, critSeed + uint(k) * 7919u); if (ck > 1.0) note_crit(c, k); const float dk = damage * ents[k].ex9.y * ck; // the victim's explosion multiplier (and protections); the damage message carries the projectile's ConfigDamageCritical (00657960 copies chance / multiplier), TakeDamage rolls it per victim note_block(k, damage, dk); atomicAdd(ents[k].x.x, int(dk * 1000.0)); note_attacker(k, shooter); if (type >= 0) ents[k].ex19.yz = vec2(float(type + 1), float(pc.frame & 0xFFFFF)); atomicOr(ents[k].ex3.z, 2); const vec2 away = normalize(c - pos + vec2(0.0, -0.01)); { // 00657960: the packet's impulse = the direction x ((power.max - power.min) x (1 - d / R) + power.min) x physics_multiplier_ragdoll_force x 3600 (the blood of the hit // and the ragdoll take it); RAGDOLL_FX of the explosion packet: the constructor's NORMAL const float a = ((pw.y - pw.x) * clamp(1.0 - dist / radius, 0.0, 1.0) + pw.x) * (float((fti(tb + 11).y >> 8) & 4095) / 256.0) * 3600.0; if (k != 0) { // (not the player: its aux.x is the maximum hp - a blast there made the respawn hp 765) atomicExchange(ents[k].aux.x, int(away.x * a * 1000.0)); atomicExchange(ents[k].aux.y, int(away.y * a * 1000.0)); atomicAnd(ents[k].ex3.z, ~0xF000); atomicOr(ents[k].ex3.z, 1 << 12); } } const float tt = clamp(1.0 - dist / radius, 0.0, 1.0); const float physF = mix(pw.x, pw.y, tt); // (the multiplier +0x108 defaults to 1) const float sc = radius * t8.z * 10.0 * (0.5 * tt + 0.5); if ((kind == 1 || kind == 3) && t8.z != 0.0 && physF != 0.0 && dist > 0.0 && ents[k].hp.x - dk > 0.0 && abs(sc) >= ents[k].ex11.x && (ents[k].ex8.x & 96) == 0 && ents[k].ex8.y > 0) { // (96: KNOCKBACK or KNOCKBACK_IMMUNITY on it; ex8.y > 0: it has a GenomeDataComponent) const float mass = intBitsToFloat(ents[k].rg2.w); vec2 imp = (c - pos) / dist * sc; if (mass != 0.0) imp /= mass; imp.y = -abs(imp.y); ents[k].ex11.yz = imp / 60.0; // the pending kick (pixels per second -> cells per frame); the entity pass adds it, inverts it when confused and starts the knockback state } } } if ((t1.w & TF_HOLE) != 0) { if (radius <= 64.0) add_edit(vec4(pos, radius, t2.z), vec4(2.0, ftf(tb + 7).z, cellMat >= 0 ? float(cellMat) : 0.0, cellMat >= 0 ? cellProb * 100.0 : 0.0)); // angular rays (explode_rays.comp), which also create the cells of create_cell_material else add_edit(vec4(pos, radius * 0.5, t2.z), vec4(0.0)); } if (((fti(tb + 9).w >> 12) & 255) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -2.0, float(1 + type)); } // config_explosion.load_this_entity: the host spawns the entities (event kind -2) if (cellMat >= 0 && cellProb > 0.0 && ((t1.w & TF_HOLE) == 0 || radius > 64.0)) add_edit(vec4(pos, radius * 0.6, 0.0), vec4(1.0, float(cellMat), cellProb, 0.0)); { // the force on the rigid bodies (00657960): the event is a pair, (x, y, radius in pixels, -1) then (force min, force max) const uint ph = ft[tb + 7].w; const vec2 fr = unpackHalf2x16(ph); uint xp = uint(atomicAdd(xhead.x, 2)); if (xp + 1u < 512u) { xe[xp] = vec4(pos, radius, -1.0); xe[xp + 1u] = vec4(fr, float(1 + type), 0.0); } // (z: the type, for the explosion's sound) } } // The sprite of a projectile, rotated along the flight when it is not round. // scale: the entity's transform scale (a script's EntitySetTransform on the twin: summon_portal_vibrate.lua pulses the portal 0.1 .. 0.42); the host keeps it as -scale in e.x of // a projectile that is no thrown body (e.x is the tumble angle of those only) void draw_sprite(vec2 pos, vec2 vel, int age, ivec4 t1, ivec4 t5, ivec4 t6, float spin, float scale) { ivec2 sz = ivec2(t5.y, t5.z); scale = max(scale, 0.01); if (sz.x > 0 && sz.y > 0) { int frame = (age / max(t6.x, 1)) % max(t5.w, 1); float ang = (t1.w & TF_PHYSICS) != 0 ? spin : ((t1.w & TF_ROTATE) != 0 ? atan(vel.y, vel.x) : 0.0); // thrown bodies tumble, long sprites follow the flight float c = cos(ang), s = sin(ang); float r = 0.5 * length(vec2(sz)) * scale + 1.0; ivec2 lo = ivec2(floor(pos - vec2(r))); int n = int(2.0 * r) + 2; for (int j = 0; j < n * n; ++j) { ivec2 w = lo + ivec2(j % n, j / n); vec2 d = vec2(w) + 0.5 - pos; ivec2 sp = ivec2(floor(vec2(sz) * 0.5 + vec2(c * d.x + s * d.y, -s * d.x + c * d.y) / scale)); if (sp.x < 0 || sp.y < 0 || sp.x >= sz.x || sp.y >= sz.y) continue; ivec2 q = w - ivec2(pc.view_x, pc.view_y); if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; uint t = fxtex[t5.x + frame * sz.x * sz.y + sp.y * sz.x + sp.x]; if ((t >> 24) != 0u) entlayer[q.y * pc.gw + q.x] = (t1.w & TF_ADDITIVE) != 0 ? (0x01000000u | (t & 0xFFFFFFu)) : t; // alpha 1 marks additive light } } else if ((t1.w & 8192) == 0) { // (an entity with only emitters has no body to draw) ivec2 w = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); for (int y = -1; y <= 1; ++y) for (int x = -1; x <= 1; ++x) { ivec2 q = w + ivec2(x, y); if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = 0xFF000000u | uint(t6.y); } } } // ---- VERLET projectiles (the tentacles: projectile_type VERLET, a VerletPhysicsComponent chain in a child entity, a VerletWeaponComponent on the root) ---- // 00c0d290 (the shot, case 2): every point of the chain starts at the origin with prev = origin - aim x speed (so it moves speed px a frame), the projectile // itself does not move; each frame the chain integrates (00c37d30: cur + (cur - prev) x velocity_dampening, liquid_damping in a liquid; a solid cell // stops the point halfway or lifts it one cell) and relaxes twice (00c38310, stiffness x 0.5), its first point held at the origin (follow_entity_transform; // the weapon system 00c3fdc0 puts the chain on the shooter's hand every frame); the projectile's position is the last point. 00c3fdc0 hurts with every point: // min(damage_coeff x |v|^2, damage_max), nothing under damage_min_step, the boxes of the creatures within damage_radius (not the shooter; the player at most once in // 20 frames, mPlayerCooldownEnd), times (lifetime left) / fade_duration_frames in the last frames. 17 records a slot: the 16 points (position, previous), then // (the anchor: its offset from the shooter or its position, 1 when it follows the shooter, the player's cooldown end). // TODO(to-do #38): the stains of the cells (stain_cells_probability). layout(std430, set = 0, binding = 63) buffer VerletPts { vec4 vpts[]; }; const int VERLET_BASE = 13696, VERLET_STRIDE = 20; void verlet_draw(uint id, int vi, int n, float fadeK, uint seed) { const int vb = VERLET_BASE + vi * VERLET_STRIDE; const int nparts = fti(vb + 1).w; if (nparts <= 0) return; const uint base = id * 17u; for (int i = 0; i < n; ++i) { const ivec4 s = fti(vb + 4 + min(i, nparts - 1)); const vec4 sf = ftf(vb + 4 + min(i, nparts - 1)); if (s.x < 0) continue; const int w = s.y & 0xFFFF, h = s.y >> 16; if (w <= 0 || h <= 0) continue; const vec2 a = vpts[base + uint(i)].xy; vec2 d = i + 1 < n ? vpts[base + uint(i) + 1u].xy - a : (i > 0 ? a - vpts[base + uint(i) - 1u].xy : vec2(1.0, 0.0)); float len = length(d); const vec2 dir = len > 0.01 ? d / len : vec2(1.0, 0.0), nrm = vec2(-dir.y, dir.x); if (i + 1 >= n || len < float(w)) len = max(len, float(w)); // (the last point's sprite at its own length; scale_sprite_x stretches the others along their link) const vec2 lo = min(min(a + nrm * (-sf.w), a + nrm * (float(h) - sf.w)), min(a + dir * len + nrm * (-sf.w), a + dir * len + nrm * (float(h) - sf.w))); const vec2 hi = max(max(a + nrm * (-sf.w), a + nrm * (float(h) - sf.w)), max(a + dir * len + nrm * (-sf.w), a + dir * len + nrm * (float(h) - sf.w))); const ivec2 c0 = ivec2(floor(lo)) - 1, c1 = ivec2(ceil(hi)) + 1; if (c1.x - c0.x > 64 || c1.y - c0.y > 64) continue; for (int y = c0.y; y <= c1.y; ++y) for (int x = c0.x; x <= c1.x; ++x) { const vec2 rel = vec2(x, y) + 0.5 - a; const float u = dot(rel, dir), v = dot(rel, nrm) + sf.w; if (u < 0.0 || u >= len || v < 0.0 || v >= float(h)) continue; const int sx = min(int(u / len * float(w)), w - 1), sy = int(v); const uint t = fxtex[s.x + sy * w + sx]; if ((t >> 24) == 0u) continue; if (fadeK < 1.0 && rnd01(hash3(uint(x * 73 + y * 151), seed, 11u)) > fadeK) continue; // (the fade: pixels drop out) const ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y); if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = t; } } } bool verlet_blocking(vec2 p) { return solid_at(ivec2(int(floor(p.x)), int(ceil(p.y)))); } void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team, int spec, int shooter, int herd, uint seed) { const int vb = VERLET_BASE + vi * VERLET_STRIDE; const ivec4 v0i = fti(vb), v1i = fti(vb + 1); const vec4 v0 = ftf(vb), v1 = ftf(vb + 1), v2 = ftf(vb + 2); const int n = clamp(v0i.x, 2, 16); const float rest = v0.y, stiff = v0.z, damp = v0.w; const uint base = id * 17u; vec2 pts[16], prv[16]; const bool shooterOk = shooter > 0 && shooter - 1 < ctrl.inp.w && (ents[shooter - 1].st.w & 8) == 0; if (age <= 1) { // the shot (00c0d290 case 2) const vec2 origin = p.a.xy, v = p.a.zw * 60.0; // (the aim x the speed: speed px a frame) for (int k = 0; k < n; ++k) { pts[k] = origin; prv[k] = origin - v; } const bool follow = shooterOk && distance(origin, ents[shooter - 1].a.xy) < 24.0; // (cast from the hand: it follows the shooter; one a trigger made somewhere stays there) vpts[base + 16u] = vec4(follow ? origin - ents[shooter - 1].a.xy : origin, follow ? 1.0 : 0.0, -1.0); } else for (int k = 0; k < n; ++k) { const vec4 q = vpts[base + uint(k)]; pts[k] = q.xy; prv[k] = q.zw; } vec4 aux = vpts[base + 16u]; const vec2 anchor = aux.z > 0.5 ? (shooterOk ? ents[shooter - 1].a.xy + aux.xy : pts[0]) : aux.xy; if (age > 1) { for (int k = 1; k < n; ++k) { // 00c37d30 vec2 delta = (pts[k] - prv[k]) * damp, np = pts[k] + delta; if (v1i.x != 0 && verlet_blocking(np)) { const vec2 mid = (pts[k] + np) * 0.5; if (!verlet_blocking(mid)) np = mid; else if (!verlet_blocking(mid - vec2(0.0, 1.0))) np = pts[k] - vec2(0.0, 1.0); else np = pts[k]; } else if (liquid_nonsand(ivec2(floor(np)))) np = pts[k] + (pts[k] - prv[k]) * v1.y; prv[k] = pts[k]; pts[k] = np; } for (int it = 0; it < 2; ++it) // 00c38310 for (int k = 0; k + 1 < n; ++k) { const vec2 d = pts[k + 1] - pts[k]; const float l = length(d); if (l < 1e-5) continue; const float diff = (l - rest) / max(l, 0.01) * stiff * 0.5; pts[k] += d * diff * 0.5; pts[k + 1] -= d * diff * 0.5; } } pts[0] = anchor; prv[0] = anchor; const int lifeLeft = int(p.b.y) - age, fade = max(v1i.z, 1); const float fadeK = lifeLeft < fade ? max(float(lifeLeft), 0.0) / float(fade) : 1.0; // 00c3fdc0: the damage of every point for (int k = 0; k < n; ++k) { const vec2 v = pts[k] - prv[k]; float dmg = min(v2.w * dot(v, v), v2.z); if (dmg < v2.y) continue; dmg *= fadeK; if (dmg <= 0.0) continue; const float r = v2.x; for (int e = 0; e < ctrl.inp.w; ++e) { const int kind = ents[e].st.x; if (!(kind == 1 || kind == 2 || kind == 3) || (ents[e].st.w & (8 | 4096)) != 0 || e == shooter - 1) continue; const vec2 rel = pts[k] - ents[e].a.xy; if (rel.x < ents[e].box.x - r || rel.x > ents[e].box.y + r || rel.y < ents[e].box.z - r || rel.y > ents[e].box.w + r) continue; if (kind == 3) { if (float(pc.frame) < aux.w) continue; aux.w = float(pc.frame + 20); } // mPlayerCooldownEnd if (kind != 3 && herd != 0 && ents[e].ex8.y == herd) continue; // (the shooter's herd is spared) atomicAdd(ents[e].x.x, int(dmg * 1000.0)); note_attacker(e, shooter); ents[e].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF)); atomicOr(ents[e].ex3.z, 1); if (e != 0) { const vec2 imp = v * 60.0 * ftf(vb + 3).x; atomicExchange(ents[e].aux.x, int(imp.x * 1000.0)); atomicExchange(ents[e].aux.y, int(imp.y * 1000.0)); } // the damage packet's impulse: impulse_coeff x the point's velocity (the blood and the ragdoll of the hit) } } // 00c3fdc0: a point going back toward the shooter (its velocity toward the anchor) faster than 10 px a frame takes what is at it along: every entity whose position is within // damage_radius of the point (not the shooter; creatures, the player, items) gets the point's velocity (VelocityComponent mVelocity, a knockback for a character) for (int k = 1; k < n; ++k) { const vec2 v = pts[k] - prv[k]; if (dot(anchor - pts[k], v) <= 0.0 || dot(v, v) <= 100.0) continue; // (|v|^2 > 100: faster than 10 px a frame) const float r = v2.x; for (int e = 0; e < ctrl.inp.w; ++e) { const int kind = ents[e].st.x; if (!(kind == 1 || kind == 2 || kind == 3 || kind == 4) || (ents[e].st.w & (8 | 32 | 128 | 4096)) != 0 || e == shooter - 1) continue; // (not dead, not a rigid body, not a hovering wand, not hidden) const vec2 d = ents[e].a.xy - pts[k]; if (abs(d.x) > r || abs(d.y) > r) continue; if (kind == 1 || kind == 3) ents[e].ex11.yz = v - ents[e].a.zw; // (00ab9910: a character's knockback; the entity pass adds the kick and holds it in the knockback state) else ents[e].a.zw = v; } } // 00c3fdc0: the physics bodies within physics_force_radius of a point (a Box2D query of the box) get their velocity moved toward the point's: the host applies // v += (the point's displacement - v) x physics_impulse_coeff / 6 (event -30000 - the body's entity slot, the displacement as two halves) if (ftf(vb + 3).z != 0.0) for (int k = 1; k < n; ++k) { const float fr = ftf(vb + 3).y; int bs = -1; for (int q = 0; q < 5 && bs < 0; ++q) { const vec2 o = q == 0 ? vec2(0.0) : (q == 1 ? vec2(fr, 0.0) : (q == 2 ? vec2(-fr, 0.0) : (q == 3 ? vec2(0.0, fr) : vec2(0.0, -fr)))); if (body_hit(pts[k] + o)) bs = body_slot(pts[k] + o); } if (bs < 0) continue; uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(ftf(vb + 3).z, 0.0, -30000.0 - float(bs), uintBitsToFloat(packHalf2x16(pts[k] - prv[k]))); // (x: physics_impulse_coeff) } vpts[base + 16u] = aux; const vec2 tip = pts[n - 1], tipV = pts[n - 1] - prv[n - 1]; bool alive = true, expired = false; if (spec >= 0) fire_triggers(spec, fired, 1, age, tip, tipV, seed, team, shooter, herd); { // the ProjectileSystem's own hit (00c073b0 / 00c091f0) at the root, which 00c3fdc0 puts on the last point: the projectile's damage, and its hit triggers on every hit // (00c091f0 casts the +0x61c and +0x610 lists and removes nothing: a piercing tentacle with a trigger casts its payload at each collision) const int tflags = fti(tb + 11).y, tw = fti(tb + 1).w; int hk; gAhead = tip; if (age > 1 && (tflags & 8) == 0 && ((tflags & 4) == 0 || age % max(fti(tb + 11).z & 0xFFFF, 1) == 0) && victim_at(tip, shooter, herd, shot_flags(tw, p.d.z), age, hk)) { const float hitDmg = hit_damage(hk, p.b.x, tb, p.f) * boost_of(tw, hk) * fadeK; if (hitDmg > 0.0) atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0)); note_attacker(hk, shooter); ents[hk].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF)); atomicOr(ents[hk].ex3.z, 1); if (spec >= 0) fire_triggers(spec, fired, 3, age, tip, tipV, seed, team, shooter, herd); if ((tflags & 4) == 0) alive = false; } } if (age >= int(p.b.y)) { alive = false; expired = true; } if (!alive && spec >= 0) { if (expired) fire_triggers(spec, fired, 4, age, tip, tipV, seed, team, shooter, herd); fire_triggers(spec, fired, 5, age, tip, tipV, seed, team, shooter, herd); } for (int k = 0; k < n; ++k) vpts[base + uint(k)] = vec4(pts[k], prv[k]); if (alive) verlet_draw(id, vi, n, fadeK, seed); p.a = vec4(tip, 0.0, 0.0); p.c.z = float(age); p.d.z = fired | (team << 8) | (p.d.z & ~0x3FF); p.d.w = alive ? p.d.w : 0; fprojs[id] = p; } void main() { uint id = gl_GlobalInvocationID.x; if (id >= uint(FP_MAX)) return; FProj p = fprojs[id]; if (p.d.w == 0) return; int type = p.d.x, tb = type * TYPE_STRIDE; ivec4 t1 = fti(tb + 1), t3 = fti(tb + 3), t5 = fti(tb + 5), t6 = fti(tb + 6); vec4 t4 = ftf(tb + 4); const int vflag = fti(tb + 16).x; if ((vflag & 4096) != 0 && pc.pass_ == 1) { // a VERLET chain between ticks: drawn as it is const int vi = ((vflag >> 13) & 31) - 1; const int fade = max(fti(VERLET_BASE + vi * VERLET_STRIDE + 1).z, 1), lifeLeft = int(p.b.y) - int(p.c.z); verlet_draw(id, vi, clamp(fti(VERLET_BASE + vi * VERLET_STRIDE).x, 2, 16), lifeLeft < fade ? max(float(lifeLeft), 0.0) / float(fade) : 1.0, hash3(id, uint(pc.frame), 5u)); return; } if (pc.pass_ == 1) { // between ticks: only draw, between the last two positions draw_sprite(p.a.xy - p.a.zw * (1.0 - ctrl.view.z), p.a.zw, int(p.c.z), t1, t5, t6, p.e.x + p.e.y * ctrl.view.z, (t1.w & TF_PHYSICS) == 0 && p.e.x < 0.0 ? -p.e.x : 1.0); return; } uint seed = hash3(id, uint(pc.frame), 5u); vec2 start = p.a.xy, pos = p.a.xy, vel = p.a.zw; { // waiting at the edge of what is loaded (far.glsl): its next position outside the window, inside the far map, in a block not resident (a biome-map tile // without its area yet, or a block not generated yet): it stays where it is, its age frozen, and goes on from there once the block is resident const ivec2 nw = ivec2(floor(pos + vel)); const ivec2 lq = nw - ivec2(pc.view_x, pc.view_y); if ((lq.x < 0 || lq.y < 0 || lq.x >= pc.gw || lq.y >= pc.gh) && far_in_map(nw) && far_slot(nw) == 0) return; } int age = int(p.c.z) + 1, fired = p.d.z & 255, team = (p.d.z >> 8) & 3, bounces = int(p.c.w), spec = p.d.y, shooter = int(p.e.z), herd = int(p.e.w); if ((vflag & 4096) != 0) { verlet_step(id, p, tb, ((vflag >> 13) & 31) - 1, age, fired, team, spec, shooter, herd, seed); return; } // VERLET: the chain (verlet_step) if ((fti(tb + 11).x & 1) != 0) { // HomingComponent (00b323f0): the nearest entity with the target tag within detect_distance (or the shooter with target_who_shot) pulls the velocity const vec4 h = ftf(tb + 10); // detect_distance, homing_velocity_multiplier, homing_targeting_coeff, max_turn_rate const int hf = fti(tb + 11).x, shooterSlot = int(p.e.z); int target = -1; float bestD = h.x * h.x; // 00b323f0: predefined_target first (alive, not invisible), then the shooter with target_who_shot, then the nearest tagged entity; invisible ones (INVISIBILITY) never const bool shooterAlive = shooterSlot > 0 && (ents[shooterSlot - 1].st.w & 8) == 0 && (ents[shooterSlot - 1].ex8.x & 8192) == 0; const int pre = fti(tb + 11).w; vec2 preAim = vec2(0.0); bool preOk = false; if (pre > 0 && pre - 1 < ctrl.inp.w && (ents[pre - 1].st.w & 8) == 0 && (ents[pre - 1].ex8.x & 8192) == 0) target = pre - 1; else if (pre < 0 && -pre - 1 < FP_MAX && fprojs[-pre - 1].d.w != 0) { preAim = fprojs[-pre - 1].a.xy; preOk = true; } // a projectile (homing_projectile.lua aims at other shots) if (target >= 0 || preOk) {} else if ((hf & 4) != 0 && shooterAlive) target = shooterSlot - 1; else for (int k = 0; k < ctrl.inp.w; ++k) { const int kind = ents[k].st.x; if (!(kind == 1 || kind == 2 || kind == 3) || (ents[k].st.w & 8) != 0 || (ents[k].ex8.x & 8192) != 0 || k == shooterSlot - 1 || (ents[k].ex8.x & ((hf >> 4) & 0x1FFF)) == 0) continue; const vec2 dd = pos - ents[k].a.xy; const float dsq = dot(dd, dd); if (dsq < bestD) { bestD = dsq; target = k; } } if (target >= 0 || preOk) { const vec2 tp = preOk ? preAim : ents[target].a.xy + vec2(ents[target].box.x + ents[target].box.y, ents[target].box.z + ents[target].box.w) * 0.5; // the centre of its hitbox const vec2 delta = tp - pos; const float dist = length(delta); if (dist < h.x) { if ((hf & 2) == 0) { // the velocity keeps homing_velocity_multiplier of itself and gains homing_targeting_coeff x the offset x dt x (1 - distance / detect_distance), in pixels per second const float k = max(1.0 - dist / h.x, 0.0); vel = vel * h.y + h.z * delta * (1.0 / 60.0) * k / 60.0; } else { // just_rotate_towards_target: the velocity turns toward the target by at most max_turn_rate const float sp = length(vel); const float av = sp > 0.0 ? atan(vel.y, vel.x) : 0.0, at = dist > 0.0 ? atan(delta.y, delta.x) : 0.0; const float TAU = 6.2831855; float A = av - at; A = A - TAU * floor(A / TAU); const float turn = min(A, h.w); const float ang = (TAU - A <= A) ? turn : -turn; vel = vec2(cos(ang) * vel.x - sin(ang) * vel.y, cos(ang) * vel.y + sin(ang) * vel.x); } } } } // VelocityComponent (00c370c0): gravity, then v -= v dt air_friction (a negative friction speeds it up), then the terminal velocity (apply_terminal_velocity, pixels per second) const vec4 t15 = ftf(tb + 15), t16f = ftf(tb + 16), t17 = ftf(tb + 17); const int f16 = fti(tb + 16).x; vel.y += p.b.z * bm_gravity(pos); vel.x += t17.x; // (gravity_y x the biome's entity_gravity_y_multiplier, 00c370c0) vel -= vel * p.b.w; if ((f16 & 64) != 0 && (t1.w & TF_PHYSICS) == 0 && dot(vel, vel) * 3600.0 > t16f.z * t16f.z) vel = normalize(vel) * (t16f.z / 60.0); const bool inert = ((p.d.z >> 11) & 1) != 0; // on_collision_remove_projectile: the entity stuck where it hit, without its ProjectileComponent if (inert) vel = vec2(0.0); const bool removed = ((p.d.z >> 29) & 1) != 0; // the host removed it (EntityKill of its twin): the component goes, nothing of the update runs const bool wasStuck = (p.d.z & 0x800) != 0; if (spec >= 0 && !inert && !removed) fire_triggers(spec, fired, 1, age, pos, vel, seed, team, shooter, herd); // the countdown runs before the flight of the update (00c06c60) int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y))))); float spin = p.e.x, omega = p.e.y; bool alive = !removed, hitWorld = false, stuck = false, hitEntity = false; bool touchedWorld = false; vec2 trigVel = vec2(0.0); // a world hit this frame (bounce or stop): the hit-world triggers fire with the velocity reflected about the surface (00c06250) // a thrown physics body (the bomb) rests on its shape, not on its centre: the lowest point of its image counts as well (half the smaller side below the position) const vec2 foot = (t1.w & TF_PHYSICS) != 0 && t5.y > 0 ? vec2(0.0, 0.5 * float(min(t5.y, t5.z))) : vec2(0.0); const int tflags = fti(tb + 11).y; // 2: collide_with_world 0 (goes through terrain), 4: penetrate_entities (hits every damage_every_x_frames frames, does not die), 8: collide_with_entities 0 const bool noWorld = (tflags & 2) != 0 || inert; // penetrate_world (00c073b0): no collision with the world; when the path of the frame crosses it, the step is penetrate_world_velocity_coeff of the velocity const bool penWorld = (f16 & 1) != 0 && !noWorld; vec2 stepVel = vel; if (penWorld) { for (int i = 1; i <= steps; ++i) if (solid_at(ivec2(floor(pos + vel * (float(i) / float(steps)))))) { stepVel = vel * t15.x; break; } } const float gpEnergy = (f16 & 1) == 0 ? t15.y : 0.0; // ground_penetration_coeff x VelocityComponent mass for (int i = 0; i < steps && alive; ++i) { vec2 np = pos + stepVel / float(steps); const bool bodyAt = body_hit(np) && (shooter <= 0 || body_slot(np) != shooter - 1); // (not the body of its shooter: a physics creature shoots from inside itself) if (!noWorld && !penWorld && bodyAt && !solid_at(ivec2(floor(np)))) body_damage(body_slot(np), pos, p.b.x); // (the hit point: the last free position) bool blocked = !noWorld && !penWorld && (solid_at(ivec2(floor(np))) || solid_at(ivec2(floor(np + foot))) || bodyAt); if (blocked && gpEnergy > 0.0 && solid_at(ivec2(floor(np))) && !bodyAt) { // ground_penetration (00c08a30, per cell of the ray): the kinetic energy coeff x mass x 0.5 x speed^2 is spent on the cell's hp; a cell that loses all of it is removed and the // projectile flies on, slower; a cell harder than ground_penetration_max_durability_to_destroy or a speed of 10 px/s or less stops it const ivec2 c = ivec2(floor(np)); const ivec2 q = c - ivec2(pc.view_x, pc.view_y); const float v2 = dot(vel, vel) * 3600.0; if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && v2 > 100.0) { const int ci = q.y * pc.gw + q.x; const uint m = cells[ci] & 0xFFFFu; const float dur = uintBitsToFloat(mats[3u * m + 2u].y); const int maxd = fti(tb + 15).z; if (!(maxd > 0 && dur > float(maxd))) { const float energy = gpEnergy * 0.5 * v2; const float hp = float((mats[3u * m + 2u].z >> 20) * 50u); vel *= 1.0 - min(hp, energy) / energy; stepVel = vel; if (hp - min(hp, round(energy)) < 1.0) { cells[ci] = 0u; ext[ci] = vec2(0.0); blocked = false; } } } } if (blocked) { if (t1.w < 0 && solid_at(ivec2(floor(np)))) { vel = vec2(0.0); break; } // a particle entity (SimplePhysicsComponent, 00c17fc0): a hit on the terrain keeps the position before it and zeroes the velocity (DieIfSpeedBelow then removes it) if ((t1.w & TF_PHYSICS) != 0) { // a thrown body: bounces with loss, rolls and comes to rest; only the fuse makes it explode const int brk = (fti(tb + 11).z >> 16) & 0xFFFF; // a breakable one (the egg): an impact faster than its break speed kills it, it explodes (projectiles.cpp break_speed) if (brk > 0 && length(vel) * 60.0 > float(brk)) { alive = false; hitWorld = true; break; } bool bx = solid_at(ivec2(floor(vec2(np.x, pos.y)))) || solid_at(ivec2(floor(vec2(np.x, pos.y) + foot))), by = solid_at(ivec2(floor(vec2(pos.x, np.y)))) || solid_at(ivec2(floor(vec2(pos.x, np.y) + foot))); if (bx || !by) { vel.x = -vel.x * 0.4; vel.y *= 0.85; omega = -omega * 0.5 + vel.y * 0.15; } if (by || !bx) { vel.y = -vel.y * 0.4; vel.x *= 0.85; omega = omega * 0.6 + vel.x * 0.2; } if (length(vel) < 0.25) vel = vec2(0.0); break; } if (body_hit(np) && t16f.w > 0.0 && body_slot(np) >= 0) { // 00c073b0: the body hit gets ApplyLinearImpulse(the frame's displacement x physics_impulse_coeff x VelocityComponent mass / 10) at the point (event -10000 - the entity slot) uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(np, -10000.0 - float(body_slot(np)), uintBitsToFloat(packHalf2x16(vel * t16f.w * ftf(tb + 9).y / 10.0))); } // the hit (00c073b0): bounces_left counts down at every hit; with some left, the velocity is reflected about the surface normal (16 rays of 5 px around the last free position, // 0063fd80) and scaled by bounce_energy, unless the reflection points back into the wall (r.v <= 0.75, bounce_at_any_angle allows it); bounce_always then turns it around with a // random turn of +-0.1 rad. Without a bounce the projectile stops there (velocity 0) and the death of 00c08b90 follows: on_collision_die kills it, on_collision_remove_projectile // leaves it stuck, otherwise it stays where it stopped. const int bl = bounces; bounces = max(bl - 1, -1); bool bounced = false; vec2 nsum = vec2(0.0); int nh = 0; // the surface normal: 16 rays of 5 px around the last free position (0063fd80) for (int k = 0; k < 16; ++k) { const float a = float(k) * (6.2831855 / 16.0); const vec2 d = vec2(-sin(a), cos(a)) * 5.0; for (int s = 1; s <= 5; ++s) if (solid_at(ivec2(floor(pos + d * (float(s) / 5.0)))) || body_hit(pos + d * (float(s) / 5.0))) { nsum += d; ++nh; break; } } touchedWorld = true; trigVel = (nh > 0 && dot(nsum, nsum) > 0.0) ? vel - normalize(nsum) * 2.0 * dot(normalize(nsum), vel) : -vel; if (bl > 0) { const vec2 v = vel * 60.0; if (nh > 0 && dot(nsum, nsum) > 0.0) { const vec2 n = normalize(nsum); const vec2 r = v - n * 2.0 * dot(n, v); if (dot(r, v) > 0.75 || (f16 & 4) != 0 || ((p.d.z >> 28) & 1) != 0) { vel = r * t15.w / 60.0; bounced = true; } } if (!bounced && (f16 & 2) != 0) { const float a = rnd01(hash3(seed, uint(i), 97u)) * 0.2 - 0.1; const vec2 rv = -vel * t15.w; vel = vec2(cos(a) * rv.x - sin(a) * rv.y, sin(a) * rv.x + cos(a) * rv.y); bounced = true; } } if (bounced && (f16 & 2048) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -8.0, float(id)); } // bounce_fx_file: the host loads it at the bounce (00c073b0; event -8, the projectile's slot) if (bounced) { pos += vel; break; } // (the position of the frame: the last free one + the new velocity x dt) vel = vec2(0.0); if ((t1.w & TF_COLLISION_DIE) != 0) { alive = false; hitWorld = true; } else if ((f16 & 8) != 0) stuck = true; break; } pos = np; if (!inert && !stuck && (t1.w & TF_PHYSICS) == 0 && (tflags & 8) == 0 && ((tflags & 4) == 0 || age % max(fti(tb + 11).z & 0xFFFF, 1) == 0)) { int hk; // the herds decide who is hurt: everything except the herd of the shooter (unless friendly fire); never the player with dont_hit_player gAhead = pos + stepVel / float(steps); if (victim_at(pos, shooter, herd, shot_flags(t1.w, p.d.z), age, hk)) { // (the shot's friendly_fire modifier) float speedScale = 1.0; // damage_scaled_by_speed: speed / mInitialSpeed (or damage_scale_max_speed when > 0) if ((f16 & 128) != 0) { const float s0 = t17.y > 0.0 ? t17.y / 60.0 : p.e.y; speedScale = s0 > 0.0 ? length(vel) / s0 : 1.0; } const float cm = crit_mult((p.d.z >> 12) & 4095, t1.w, shooter, hk, seed); const float hitDmg = hit_damage(hk, p.b.x * speedScale, tb, p.f) * boost_of(t1.w, hk) * cm; if (cm > 1.0) note_crit(pos, hk); note_block(hk, p.b.x, hitDmg); atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0)); if (ents[hk].st.x == 4) body_damage(hk, pos, hitDmg); // a prop with a liquid inventory leaks if (ents[hk].st.x == 4 && ents[hk].ex7.w > 0.5 && t16f.w > 0.0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(gAhead, -10000.0 - float(hk), uintBitsToFloat(packHalf2x16(vel * t16f.w * ftf(tb + 9).y / 10.0))); } // and the impulse on its body { // 00c091f0: knockback scalar = ProjectileComponent.knockback_force x speed (pixels per second) x the projectile's VelocityComponent mass; the force vector = velocity x ragdoll_force_multiplier; // then the victim's gate (00ab9a90): abs(scalar) against minimum_knockback_force before the division by its mass, upward y, never on a killing hit const vec4 t9 = ftf(tb + 9); const int vk = ents[hk].st.x; const vec2 fv = vel * 60.0 * t9.z; const float kfh = t9.x + p.f.z; // ProjectileComponent knockback_force + the shot's knockback_force modifier const float sc = kfh * length(vel * 60.0) * t9.y; if (kfh != 0.0 && (vk == 1 || vk == 3) && length(fv) > 0.0 && ents[hk].hp.x - hitDmg > 0.0 && abs(sc) >= ents[hk].ex11.x && (ents[hk].ex8.x & 96) == 0 && ents[hk].ex8.y > 0) { const float mass = intBitsToFloat(ents[hk].rg2.w); vec2 imp = normalize(fv) * sc; if (mass != 0.0) imp /= mass; imp.y = -abs(imp.y); ents[hk].ex11.yz = imp / 60.0; } } note_attacker(hk, shooter); ents[hk].ex19.yz = vec2(float(type + 1), float(pc.frame & 0xFFFFF)); // the projectile of the hit (script_damage_received's projectile_thats_responsible) atomicOr(ents[hk].ex3.z, 1); // hit by a projectile if ((t1.w & TF_HIT_EFFECTS) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(float(hk), float(shooter), -1.0, float(1 + type)); } // the hit effects of the projectile: the host applies them (victim slot, shooter slot + 1, type + 1) if (hk != 0) { // the packet of 00c091f0: the impulse = velocity (px/s) x ragdoll_force_multiplier (the blood of the hit and the ragdoll take it), its RAGDOLL_FX const vec2 imp = vel * 60.0 * ftf(tb + 9).z; atomicExchange(ents[hk].aux.x, int(imp.x * 1000.0)); atomicExchange(ents[hk].aux.y, int(imp.y * 1000.0)); atomicAnd(ents[hk].ex3.z, ~0xF000); atomicOr(ents[hk].ex3.z, ((fti(tb + 11).y >> 4) & 15) << 12); } if (spec >= 0) fire_triggers(spec, fired, 3, age, pos, vel, seed, team, shooter, herd); // the impact (00c091f0) if ((tflags & 4) == 0) { alive = false; hitEntity = true; } // (a penetrating projectile flies on) } } } if (alive && t1.w < 0 && length(vel) < float(fti(tb + 9).w >> 20 & 4095) / 256.0) alive = false; // DieIfSpeedBelowComponent of a particle entity // 00c370c0: the count N of liquid cells (not powder) in the 3x3 around the new integer position (float coordinates truncate toward zero), taken only when that position changed. // Every candidate counts, then (displace_liquid on, 75 % of the draws) its cell becomes a particle that flies off against the motion (00717670: rotated, 0.1 x); the velocity keeps // 1 - liquid_drag * dt * N of itself (VelocityComponent liquid_drag, unclamped), and N above liquid_death_threshold kills the projectile (liquid_death_threshold, default 0: off). if (alive && any(notEqual(ivec2(pos), ivec2(start)))) { int n = 0; const ivec2 q = ivec2(pos); for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) if (liquid_nonsand(q + ivec2(dx, dy))) ++n; if (n > 0 && fti(tb + 8).y != 0) { for (int k = 0; k < 9; ++k) { const ivec2 c = q + ivec2(k % 3 - 1, k / 3 - 1); if (liquid_at_pos(vec2(c) + 0.5) == 0u || (hash3(seed, uint(k), 81u) % 101u) >= 75u) continue; const float ang = 0.6 * float(hash3(seed, uint(k), 82u) & 0x7FFFu) / 32767.0 - 0.3; displace_cell(c, -(mat2(cos(ang), sin(ang), -sin(ang), cos(ang)) * (0.1 * vel))); } } vel *= 1.0 - ftf(tb + 8).x * (1.0 / 60.0) * float(n); // (liquid_death_threshold of the VelocityComponent defaults to 0: no death by the count; die_on_liquid_collision is the collision with a liquid cell) if ((t1.w & TF_DIE_IN_LIQUID) != 0 && liquid_nonsand(ivec2(pos))) { alive = false; hitWorld = true; } } if (alive && !inert && (f16 & 16) != 0 && (t1.w & TF_PHYSICS) == 0 && dot(vel, vel) * 3600.0 <= t16f.y * t16f.y) { // die_on_low_velocity: the death of a hit (00c08b90) if ((t1.w & TF_COLLISION_DIE) != 0) alive = false; else if ((f16 & 8) != 0) stuck = true; } bool expired = false; if (alive && !inert && !stuck && age >= int(p.b.y)) { alive = false; expired = true; } // lifetime out (the inert entity has no ProjectileComponent, so no lifetime) if (!alive && fti(tb + 17).w != 0) { // on_death_emit_particle: on_death_emit_particle_count real particles of the material const int dp = fti(tb + 17).w, dmat = (dp & 0xFFFF) - 1, dn = min(dp >> 16, 64); for (int k = 0; k < dn; ++k) { const vec2 ev = (vec2(rnd01(hash3(seed, uint(k), 93u)), rnd01(hash3(seed, uint(k), 94u))) - 0.5) * 2.0 / 60.0 * 30.0; emit_material_particle(pos, ev, 30.0, PART_GRAV, mats[3u * uint(dmat)].x, dmat); } } if (!alive && (hitWorld || hitEntity) && (f16 & 1024) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -6.0, float(type)); } // on_collision_spawn_entity: the host loads spawn_entity there (00c08b90) // triggers (see fire_triggers): a hit on the world fires the hit-world and waiting timers in the reflected direction (also when the projectile bounces and lives on); the end of the // lifetime fires the waiting timers; the removal of the projectile, whatever ended it, fires the death triggers if (touchedWorld && spec >= 0) fire_triggers(spec, fired, 2, age, pos, trigVel, seed, team, shooter, herd); if (stuck && !wasStuck && spec >= 0) fire_triggers(spec, fired, 5, age, pos, vel, seed, team, shooter, herd); // on_collision_remove_projectile: the ProjectileComponent goes, its removal (00c0e3e0, slot 15 of the system) casts the death triggers if (!alive) { if (spec >= 0) { if (expired) fire_triggers(spec, fired, 4, age, pos, vel, seed, team, shooter, herd); fire_triggers(spec, fired, 5, age, pos, vel, seed, team, shooter, herd); } if ((t1.w & TF_EXPLODE) != 0) { explode(pos, p.c.x, p.c.y, type, team, shooter, herd, (p.d.z >> 12) & 4095, seed); if (((p.d.z >> 24) & 15) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -7.0, float((p.d.z >> 24) & 15)); } // lightning_count: that many electricity.xml at the blast (event kind -7) if (t3.y >= 0) boom_fx(t3.y, pos, seed, p.c.x); } // (a projectile that does not explode leaves nothing of its own when it dies: no sparks - wall pieces absorbed by Spells To Power vanish into their red poofs only) } if (alive && p.f.w > 0.5) { // the shot's trail_material: real particles of the material along the path (00c0b440: an emitter with count = amount, velocity +- sqrt(amount) x 0.5) const int tm = int(p.f.w), tmat = (tm & 4095) - 1, amount = tm >> 12; const float tv = sqrt(float(amount)) * 0.5 / 60.0; for (int k = 0; k < min(amount, 8); ++k) { const vec2 ep = mix(start, pos, rnd01(hash3(seed, uint(k), 71u))); const vec2 ev = (vec2(rnd01(hash3(seed, uint(k), 72u)), rnd01(hash3(seed, uint(k), 73u))) - 0.5) * 2.0 * tv; emit_material_particle(ep, ev, 20.0, PART_GRAV, mats[3u * uint(tmat)].x, tmat); } } if (alive) { // particle emitters (cosmetic) for (int e = 0; e < t3.w; ++e) { int eb = EMIT_BASE + (t3.z + e) * EMIT_STRIDE; ivec4 e0 = fti(eb), e2 = fti(eb + 2); vec4 e0f = ftf(eb), e1 = ftf(eb + 1), e3 = ftf(eb + 3); const int iv2 = max(e2.y & 0xFF, 2); // the mean emission interval in half frames (the min .. max interval of 00bc75e0 is random; 1..2 frames emits 2 times in 3 frames, not every frame) const float away = float(e2.y >> 16) / 60.0; // velocity_always_away_from_center, cells per frame const uint attr = uint(e2.y >> 8) & 0xFFu; // attractor_force { const int ph = age + int(id) + e * 3; if ((ph * 2) / iv2 == (ph * 2 - 2) / iv2) continue; } if (age < (e2.z >> 16)) continue; // delay_frames (summon_portal.xml's burst comes at frame 118) const int c0 = e2.z & 0xFFFF; int n = c0 + int(rnd01(hash3(seed, uint(e), 51u)) * float(e2.w - c0 + 1)); for (int k = 0; k < n && k < 64; ++k) { // (the giga black hole's emitters make 40 - 60 a frame) float tt = (e0.y & 1) != 0 ? rnd01(hash3(seed, uint(e * 64 + k), 52u)) : 1.0; vec2 ep = mix(start, pos, tt); ep += (vec2(rnd01(hash3(seed, uint(e * 64 + k), 59u)), rnd01(hash3(seed, uint(e * 64 + k), 60u))) - 0.5) * 2.0 * e3.zw; // x/y_pos_offset ranges { // area_circle_radius (00bc75e0): a point of the disc, radius sqrt(u) between min and max (quarter pixels in bits 8-19 / 20-31 of the flags) const float rmax = float((uint(e0.y) >> 8) & 0xFFFu) * 0.25, rmin = float((uint(e0.y) >> 20) & 0xFFFu) * 0.25; if (rmax > 0.0) { const float rr = sqrt(rnd01(hash3(seed, uint(e * 64 + k), 61u))) * (rmax - rmin) + rmin, aa = rnd01(hash3(seed, uint(e * 64 + k), 62u)) * 6.2831853; ep += vec2(cos(aa), sin(aa)) * rr; } } vec2 ev = vec2(mix(e1.x, e1.y, rnd01(hash3(seed, uint(e * 64 + k), 53u))), mix(e1.z, e1.w, rnd01(hash3(seed, uint(e * 64 + k), 54u)))); ev += (vec2(rnd01(hash3(seed, uint(k), 55u)), rnd01(hash3(seed, uint(k), 56u))) - 0.5) * 2.0 * e3.x; if (away != 0.0) { const vec2 dd = ep - pos; const float dl = length(dd); if (dl > 1e-4) ev += dd / dl * away; } // (00bc75e0: along the way out from the emitter) float life = max(2.0, mix(e0f.z, e0f.w, rnd01(hash3(seed, uint(e * 64 + k), 57u)))); if ((e0.y & 4) != 0) { // create_real_particles (00bc75e0 route +0x60): the material is created in the grid at the position (velocity and lifetime are ignored) through the cell // factory: into an empty cell, or over a gas or liquid one unless emit_only_if_there_is_space (+0x67, flag 64) const ivec2 q = ivec2(floor(ep)) - ivec2(pc.view_x, pc.view_y); const int mat = int(ft[eb + 3].y); if (mat > 0 && q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) { const int ci = q.y * pc.gw + q.x; const uint cur = cells[ci], cm = cell_mat(cur); const bool free = cm == 0u || ((e0.y & 64) == 0 && (mflags(cm) & (F_GAS | F_LIQCELL)) != 0u && (mflags(cm) & F_STATIC) == 0u); if (free && atomicCompSwap(cells[ci], cur, uint(mat) | (((hash3(seed, uint(e * 8 + k), 58u) >> 16) & 0xFFu) << 16)) == cur) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); } } } else if ((e0.y & 16) == 0 || cell_free_at(ep)) { // (bit 3: collide_with_grid 0, bit 4: cosmetic_force_create 0, bit 5: a material that does not glow - blended with its alpha) const bool blended = (e0.y & 32) != 0; const int ps = emit_particle_g(ep, ev, life, uf(ft[eb + 2].x), uint(e0.x) & 0xFFFFFFu, (e0.y & 2) != 0, !blended, (e0.y & 8) == 0); if (ps >= 0 && blended) fparts[ps].c.z = 255u - ((uint(e0.x) >> 24) & 0xFFu); if (ps >= 0 && (attr != 0u || (e0.y & 128) != 0)) fparts[ps].c.z |= (id & 0x7FFu) << 8 | attr << 19 | ((e0.y & 128) != 0 ? 0x8000000u : 0u); // the attractor (this projectile, its force) and draw_as_long, for fx_particles.comp } } } if (t4.w > 0.0) add_light(pos, min(t4.w, 36.0) * 0.55, 0.32, t4.xyz); draw_sprite(pos, vel, age, t1, t5, t6, spin, (t1.w & TF_PHYSICS) == 0 && spin < 0.0 ? -spin : 1.0); } if ((t1.w & TF_PHYSICS) != 0) { if (length(vel) < 0.01) omega *= 0.8; spin += omega; omega *= 0.99; } p.e = vec4(spin, omega, p.e.z, p.e.w); // (the shooter slot + 1 and its herd stay with the projectile) if (alive && !inert) vel *= bm_drag(pos); // 00c06c60: after the move the velocity is multiplied by the biome's projectile_drag_coeff (MOIST 0.965) p.a = vec4(pos, vel); p.c.z = float(age); p.c.w = float(bounces); if (!alive && (fti(tb + 11).y & 1) != 0) { uint xi = uint(atomicAdd(xhead.x, 2)); if (xi + 1u < 512u) { xe[xi] = vec4(pos, -5.0, float(id)); xe[xi + 1u] = vec4(float(p.d.w), 0.0, 0.0, 0.0); } } // the twin's projectile died (event kind -5) p.d.z = fired | (team << 8) | (p.d.z & ~0x3FF) | (p.d.z & 0x400) | (stuck ? 0x800 : 0); // bit 11: stuck without its ProjectileComponent (on_collision_remove_projectile); bits 12-23 crit, 24-27 lightning_count, 28 bounce at any angle stay p.d.w = alive ? p.d.w : 0; fprojs[id] = p; }