// Shared declarations of the GPU wand/projectile/effect system (proj_spawn, proj_step, fx_particles, fx_anims, rigid_events). // The CPU only runs the spell script (gun.lua) and uploads its result: tables of projectile types, effect definitions, and the shot tree // (nodes and specs) of every cast. Flight, collisions, triggers, explosions, damage, emitters, particles, lights and animations run here. #include "common.glsl" #include "far.glsl" struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x, rg, rg2, aux; vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0 vec4 ex2; // ranged attack: projectile speed, gravity ivec4 ex3; // blood material + 1, blood colour, damage types taken since the last check (bit 1 projectile, 2 explosion, 4 fire, 8 melee), 0 vec4 ex4; // air in the lungs (frames), air maximum (0 = does not need air), damage per frame without air, frames left on fire vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y) vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending }; layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; }; layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; }; layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl; layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // entity positions before the last tick (for interpolation) layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits, two vec4 each (see edits.comp) layout(std430, set = 0, binding = 33) readonly buffer FxTables { uvec4 ft[]; }; // projectile types, explosion effects, emitters (words, see gun/gpu_fx.h) layout(std430, set = 0, binding = 34) readonly buffer FxTex { uint fxtex[]; }; // sprite frames (ARGB) of projectiles and explosions #include "fx_host.glsl" #include "fx_state.glsl" // rigid bodies (read only): projectiles stop at them layout(std430, set = 0, binding = 30) readonly buffer RShapes { uint rsh[]; }; struct RBody { vec4 p; vec4 v; ivec4 i; vec4 q; vec4 l; vec4 n; vec4 m; ivec4 jn; vec4 ja0; vec4 ja1; vec4 ja2; vec4 ja3; }; layout(std430, set = 0, binding = 31) readonly buffer RBodies { ivec4 rhdr; RBody rbodies[]; }; layout(std430, set = 0, binding = 37) buffer Xev { ivec4 xhead; vec4 xe[512]; }; // explosion events for the rigid bodies: x, y, radius, power struct FLight { vec4 a; vec4 b; }; // x, y, radius, strength / rgb layout(std430, set = 0, binding = 38) buffer FxLights { uint lcount; uint lpad[3]; FLight lights[512]; }; const int TYPE_STRIDE = 18, BOOM_BASE = 9216, BOOM_STRIDE = 4, EMIT_BASE = 9472, EMIT_STRIDE = 4, LOAD_BASE = 13568; // (512 types: the scripts' variants of a projectile are types of their own) float uf(uint u) { return uintBitsToFloat(u); } vec4 ftf(int i) { return uintBitsToFloat(ft[i]); } ivec4 fti(int i) { return ivec4(ft[i]); } float rnd01(uint h) { return float(h & 0xFFFFu) / 65536.0; } bool solid_at(ivec2 w) { ivec2 p = w - ivec2(pc.view_x, pc.view_y); if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) { // outside the window: a resident far block (far.glsl), else a wall const int s = far_slot(w); if (s == 0) return true; const uint fm = cell_mat(farcells[far_index(s, w)]); return fm != 0u && (mflags(fm) & (F_STATIC | F_SAND)) != 0u; } uint m = cell_mat(cells[p.y * pc.gw + p.x]); return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u; } void add_light(vec2 pos, float radius, float strength, vec3 rgb) { uint i = atomicAdd(lcount, 1u); if (i < 512u) { lights[i].a = vec4(pos, radius, strength); lights[i].b = vec4(rgb, 0.0); } } uint add_anim(vec2 pos, int boom, float radius, float strength, int maxage, uint rgb, float angle) { // the record uint s = atomicAdd(fcnt[2], 1u) % uint(FANIM_MAX); fanims[s].a = vec4(pos, radius, strength); fanims[s].b = ivec4(boom, 0, maxage, int(rgb & 0xFFFFFFu)); fanims[s].c = vec4(angle, 0.0, 0.0, 0.0); return s; } void add_edit(vec4 q, vec4 r) { uint i = uint(atomicAdd(ehead.x, 1)); if (i < 256u) { ee[2u * i] = q; ee[2u * i + 1u] = r; } } // A world point inside a rigid body's pixel mask? (the grid is filled by rigid_raster.comp) layout(std430, set = 0, binding = 40) readonly buffer BodyGrid { uint bodyGrid[]; }; bool body_hit(vec2 w) { ivec2 p = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y); return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh && bodyGrid[p.y * pc.gw + p.x] != 0u; } // The entity slot of the rigid body that covers a world point, -1 when none. int body_slot(vec2 w) { ivec2 p = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y); if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return -1; return int(bodyGrid[p.y * pc.gw + p.x] & 0x7FFFFFFFu) - 1; // (bit 31: the characters-pass mark of rigid_raster.comp) } // Who damaged an entity last: the slot + 1 of the attacker (0 = unknown) in the low 12 bits of ex8.w, the frame in the rest. The host reads it when the hp drops (script_damage_received). void note_attacker(int victim, int attacker_plus_1) { if (attacker_plus_1 > 0 && attacker_plus_1 < 4096) ents[victim].ex8.w = attacker_plus_1 | ((pc.frame & 0xFFFFF) << 12); } // A hit that the victim's multipliers took to nothing (a protection): the frame in ex11.w of a creature or the player, for the host's BLOCK message (00b071f0) void note_block(int victim, float raw, float dealt) { const int kind = ents[victim].st.x; if (raw > 0.0 && dealt <= 0.0 && (kind == 1 || kind == 2 || kind == 3)) ents[victim].ex11.w = float(pc.frame); } // A damage event on a body with a liquid inventory: the corpse part emits (entities.comp). void body_damage(int slot, vec2 hit, float dmg) { if (slot < 0 || ents[slot].ex7.y <= 0.0) return; atomicAdd(ents[slot].ex8.z, max(1, int(dmg * 1000.0))); const vec2 r = hit - ents[slot].a.xy; const float ca = cos(ents[slot].a.z), sa = sin(ents[slot].a.z); const vec2 rel = vec2(ca * r.x + sa * r.y, -sa * r.x + ca * r.y); // in the body's frame (entities.comp turns the leak back with the rotation) atomicExchange(ents[slot].ex8.x, int(rel.x * 1000.0)); atomicExchange(ents[slot].ex8.y, int(rel.y * 1000.0)); } // The damage multiplier the perks give a shooter; applied ONCE when the shot is made (Message_Shot, 00b06870: projectile damage, and the explosion damage unless not_scaled_by_gamefx) float dealt_of(int shooter) { if (shooter <= 0) return 1.0; const Ent s = ents[shooter - 1]; return (s.st.x == 1 || s.st.x == 2 || s.st.x == 3) && s.ex7.y > 0.0 ? s.ex7.y : 1.0; } // The liquid material at a position (0 when there is none). uint liquid_at_pos(vec2 pos) { ivec2 p = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return 0u; uint m = cell_mat(cells[p.y * pc.gw + p.x]); if (m == 0u) return 0u; uint f = mflags(m); return ((f & F_LIQUID) != 0u && (f & (F_SAND | F_STATIC)) == 0u) ? m : 0u; } // A projectile pushes the liquid cells around its position aside (to the nearest free cell away from it). void displace_liquid(vec2 pos) { { ivec2 p0 = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); if (p0.x < 1 || p0.y < 1 || p0.x >= pc.gw - 1 || p0.y >= pc.gh - 1) return; uint m0 = cell_mat(cells[p0.y * pc.gw + p0.x]); if (m0 == 0u || (mflags(m0) & F_LIQUID) == 0u) return; } // only when the projectile is inside a liquid for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) { ivec2 w = ivec2(floor(pos)) + ivec2(dx, dy); ivec2 p = w - ivec2(pc.view_x, pc.view_y); if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue; int i = p.y * pc.gw + p.x; uint c = cells[i]; uint m = cell_mat(c); if (m == 0u) continue; uint f = mflags(m); if ((f & F_LIQUID) == 0u || (f & (F_SAND | F_STATIC)) != 0u) continue; vec2 d = vec2(w) + 0.5 - pos; d = length(d) < 0.01 ? vec2(0.0, -1.0) : normalize(d); for (int r = 1; r <= 6; ++r) { ivec2 t = w + ivec2(round(d * float(r))); ivec2 q = t - ivec2(pc.view_x, pc.view_y); if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) break; int ti = q.y * pc.gw + q.x; if (atomicCompSwap(cells[ti], 0u, c) == 0u) { ext[ti] = ext[i]; cells[i] = 0u; ext[i] = vec2(0.0); break; } } } } vec3 rgb_of(uint c) { return vec3(float((c >> 16) & 255u), float((c >> 8) & 255u), float(c & 255u)) / 255.0; } // Explosion visuals: animated sprite, light flash, sparks and material sparks. // radius >= 0: the explosion's own radius (a shot with Explosive Projectile: the file's 2 + 15 per modifier); the light is that x light_radius_coeff (00657960), the table // holds it for the file's radius void boom_fx(int boom, vec2 pos, uint seed, float radius) { int b = BOOM_BASE + boom * BOOM_STRIDE; ivec4 b0 = fti(b), b1 = fti(b + 1), b2 = fti(b + 2); vec4 b3 = ftf(b + 3); float rad = b3.x; atomicMax(lpad[0], uint(b3.y * 1500.0)); // camera shake (milli-cells), decays in fx_anims int frames = max(b0.w, 1), wait = max(b1.x, 1); int maxage = b0.z > 0 ? frames * wait + 2 : 9; // the light (ConfigExplosion light_*): radius explosion_radius x light_radius_coeff, colour light_r/g/b, fading over light_fade_time (b1.w bits 4-15 / 16-27) const bool lightOn = (b1.w & 4) != 0; float lrad = float((uint(b1.w) >> 4) & 0xFFFu); if (radius >= 0.0) lrad = rad > 0.0 ? lrad * radius / rad : radius * 8.0; // (the coefficient default 8: 004b2780) lrad = min(lrad, 4095.0); const int lframes = max(1, int((uint(b1.w) >> 16) & 0xFFFu)); const uint an = add_anim(pos, boom, lightOn ? max(lrad, 9.0) : 0.0, lightOn ? 0.6 : 0.0, max(max(maxage, 9), lightOn ? lframes : 0), uint(b1.y), 0.0); if (lightOn) fanims[an].c.y = float(lframes); // (the light fades over its own time, the sprite plays over its own) const int sp0 = int(uint(b2.x) & 0xFFu); const uint sparkRgb = uint(b2.x) >> 8; int ns = (b1.w & 1) != 0 ? sp0 + int(rnd01(hash3(seed, 1u, 7u)) * float(b2.y - sp0 + 1)) : 0; for (int i = 0; i < ns && i < 64; ++i) { float a = rnd01(hash3(seed, uint(i), 11u)) * 6.2832, sp = 0.8 + rnd01(hash3(seed, uint(i), 12u)) * 2.4; emit_particle(pos, vec2(cos(a), sin(a)) * sp - vec2(0.0, 0.6), 18.0 + rnd01(hash3(seed, uint(i), 13u)) * 27.0, 0.1, sparkRgb, true, true); } int nm = (b1.w & 2) != 0 ? b2.z + int(rnd01(hash3(seed, 2u, 8u)) * float(b2.w - b2.z + 1)) : 0; if ((b1.w & 10) == 10 && b0.z == 0) { // an ambient emitter (a portal): material sparks spread over a disc of the radius, drifting away from its centre, no gravity, long-lived, additive for (int i = 0; i < nm && i < 160; ++i) { float a = rnd01(hash3(seed, uint(i), 31u)) * 6.2832, rr = sqrt(rnd01(hash3(seed, uint(i), 32u))) * rad, sp = 0.05 + rnd01(hash3(seed, uint(i), 33u)) * 0.25; vec2 d = vec2(cos(a), sin(a)); emit_particle(pos + d * rr, d * sp, 150.0 + rnd01(hash3(seed, uint(i), 34u)) * 60.0, 0.0, uint(b1.z), true, true); } nm = 0; } for (int i = 0; i < nm && i < 64; ++i) { float a = rnd01(hash3(seed, uint(i), 21u)) * 6.2832, sp = 0.5 + rnd01(hash3(seed, uint(i), 22u)) * 1.5; emit_particle(pos, vec2(cos(a), sin(a)) * sp - vec2(0.0, 0.4), 20.0 + rnd01(hash3(seed, uint(i), 23u)) * 30.0, 0.12, uint(b1.z), true, true); } } // The damage of a projectile hit on entity k (TakeDamage_Impl 00ab9a90: every damage type goes through the victim's multiplier of that type): the projectile channel, the // damage_by_type channels of the type (table words 12-14: melee, electricity, fire, ice / slice, drill, radioactive, poison / healing, curse, holy, explosion) and the wand's adds (f.x electricity, f.y ice) float hit_damage(int k, float proj, int tb, vec4 f) { const Ent v = ents[k]; const vec4 c0 = ftf(tb + 12), c1 = ftf(tb + 13), c2 = ftf(tb + 14); return proj * v.ex9.x + c0.x * v.ex9.w + (c0.y + f.x) * v.ex12.x + c0.z * v.ex9.z + (c0.w + f.y) * v.ex12.w + c1.x * v.ex12.z + c1.y * v.ex12.y + c1.z * v.ex13.z + c1.w * v.ex13.w + c2.x * v.ex13.x + c2.y * v.ex14.y + c2.z * v.ex14.z + c2.w * v.ex9.y; }