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
This commit is contained in:
1 parent
a34f42d53e
commit
00a9c1c26c
48 files changed
+5799
-435
No files matched your search
@@ -19,6 +19,8 @@ void main() {
|
||||
uint cs = cells[i], ct = cells[t];
|
||||
vec2 et = ext[t];
|
||||
if ((it.x >> 31) != 0u) cs ^= DIR_BIT; // gas bounced: flip horizontal direction
|
||||
terrain_age[t] = (cell_mat(cs) << 16) | 0x100u;
|
||||
terrain_age[i] = (cell_mat(ct) << 16) | 0x100u;
|
||||
cells[t] = cs;
|
||||
ext[t] = vec2(uintBitsToFloat(it.z), uintBitsToFloat(it.w));
|
||||
cells[i] = ct;
|
||||
|
||||
@@ -54,20 +54,6 @@ void main() {
|
||||
int fireHp = int(mz << 16) >> 16; // fire_hp as the signed 16 bits
|
||||
bool noBurnOut = fireHp == -1; // exactly -1 never burns out (0070e060); other negative values satisfy fire_hp < age
|
||||
|
||||
{ // the biome's reaction_freeze_chance / reaction_unfreeze_chance (0070e8c0; FREEZING, HOT): r % (cold_freezes_chance_rev + 1) < freeze turns the cell into its
|
||||
// cold_freezes_to_material when the random neighbour is free; r % (warmth_melts_chance_rev + 1) < unfreeze turns it into its warmth_melts_to_material when that
|
||||
// neighbour is free or of another material (the cell-type gate +0xa0 and the contact predicate 004ac0f0 of the freeze are not ported)
|
||||
const uint bb = bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y)));
|
||||
const uint fc = (bb >> 16) & 0xFFu, uc = bb >> 24;
|
||||
if ((fc | uc) != 0u && !burning) {
|
||||
const uvec2 cv = bm_conversions(m);
|
||||
const uint nb = cell_at(p + D4[hash3(seed, 41u, 9u) & 3u]);
|
||||
const uint nm = nb == NONE ? 0u : cell_mat(nb);
|
||||
if (fc != 0u && (cv.x & 0xFFFFu) != 0u && (hash3(seed, 42u, 9u) & 0x7FFFu) % ((cv.x >> 16) + 1u) < fc && nm == 0u) { cells[i] = ((cv.x & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; }
|
||||
if (uc != 0u && (cv.y & 0xFFFFu) != 0u && (hash3(seed, 43u, 9u) & 0x7FFFu) % ((cv.y >> 16) + 1u) < uc && nm != m) { cells[i] = ((cv.y & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; }
|
||||
}
|
||||
}
|
||||
|
||||
if (!burning && (f & F_GAS) != 0u) { // CGasCell update 0070a670: with probability (gas_speed + 1) / 101 (else the update only drifts the gas), intensity -= rand % 3,
|
||||
// fade = (char) intensity; a positive one then spreads (rand % 101 < fade, then 0070f9e0) - the targets below count this smoke as a source
|
||||
const uint raw = (birth[i] >> 24) & 255u;
|
||||
|
||||
@@ -13,6 +13,8 @@ layout(std430, set = 0, binding = 1) readonly buffer Mats { uvec4 mats[]; };
|
||||
layout(std430, set = 0, binding = 2) buffer Pixels { uint pixels[]; };
|
||||
layout(std430, set = 0, binding = 3) buffer Ext { vec2 ext[]; };
|
||||
layout(std430, set = 0, binding = 42) buffer Birth { uint birth[]; }; // the frame a cell got its material (cell age = frame - birth, 0070ebf0 req_lifetime)
|
||||
// Material stamp (high 16), CLiquidCell movement state (bits 8..11), rest age (low byte).
|
||||
layout(std430, set = 0, binding = 65) buffer TerrainAge { uint terrain_age[]; };
|
||||
layout(std430, set = 0, binding = 4) buffer Claims { uint claims[]; };
|
||||
// per cell: x = target index | (flip direction << 31), y = priority, z/w = new vy/vx bits
|
||||
layout(std430, set = 0, binding = 5) buffer Intent { uvec4 intent[]; };
|
||||
@@ -66,6 +68,24 @@ uint with_fade(uint b, int f) { return (b & BIRTH_MASK) | (uint(f & 255) << 24);
|
||||
|
||||
uint cell_mat(uint c) { return c & 0xFFFFu; }
|
||||
uint mflags(uint m) { return mats[3u * m].y; }
|
||||
// 0070b1d0: state 3 and zero vertical accumulator; support is not probed.
|
||||
uint liquid_state(uint i, uint m) {
|
||||
return (terrain_age[i] >> 16) == m ? (terrain_age[i] >> 8) & 15u : 3u;
|
||||
}
|
||||
bool liquid_freeze_ready(ivec2 p, uint c) {
|
||||
const uint m = cell_mat(c);
|
||||
const int i = p.y * pc.gw + p.x;
|
||||
return (mflags(m) & F_LIQCELL) == 0u || (liquid_state(uint(i), m) == 3u && ext[i].x == 0.0);
|
||||
}
|
||||
// ChangeMaterial preserves the movement accumulators for nonburning same-class cells.
|
||||
void material_changed(int i, uint oldCell, uint m) {
|
||||
const uint old = cell_mat(oldCell);
|
||||
const uint classMask = F_LIQCELL | F_GAS | F_FIRE | F_AIR;
|
||||
const bool sameClass = (oldCell & BURN_BIT) == 0u && (mflags(old) & classMask) == (mflags(m) & classMask);
|
||||
terrain_age[i] = (m << 16) | (sameClass && (terrain_age[i] >> 16) == old ? (terrain_age[i] & 0xFFFFu) : 0x300u);
|
||||
if (!sameClass) ext[i] = vec2(0.0);
|
||||
birth[i] = birth_frame();
|
||||
}
|
||||
float mdensity(uint m) { return uintBitsToFloat(mats[3u * m].z); }
|
||||
float mgravity(uint m) { return uintBitsToFloat(mats[3u * m].w); }
|
||||
int mviscosity(uint m) { return int(mats[3u * m + 1u].x & 0xFFFFu); }
|
||||
|
||||
+22
-56
@@ -46,6 +46,7 @@ layout(std430, set = 0, binding = 23) buffer Ctrl {
|
||||
vec4 hand2; // host: held wand: pivot
|
||||
ivec4 burnN; // rigid_burn.comp: x the burnt pixels; y: the player's respawns, z: saved by SAVING_GRACE, w: an extra life used (the host reads and clears them)
|
||||
} ctrl;
|
||||
#include "hit_liquid.glsl"
|
||||
|
||||
const int ST_HOVER = 128; // an item that hovers (a wand in the world): no physics, a small bob (play_hover_animation)
|
||||
const int ST_SWIMMER = 256; // the immersion probe at ex.w (the integer buoyancy_check_offset_y) is tested
|
||||
@@ -54,20 +55,19 @@ const int ST_GROUNDED = 1, ST_CANJUMP = 2, ST_CANFLY = 4, ST_DEAD = 8, ST_LIQUID
|
||||
// rigid bodies are solid for creatures and the player: rigid_raster.comp marks the cells their masks cover (read here with a one-tick delay)
|
||||
layout(std430, set = 0, binding = 40) readonly buffer BodyGrid { uint bodyGrid[]; };
|
||||
layout(std430, set = 0, binding = 46) readonly buffer GlowTab { uint gtab[]; }; // per material: gfx_glow in the low 16 bits, the stain statuses it gives in bits 16-21 (WET, OILED, BLOODY, ALCOHOLIC, SLIMY, RADIOACTIVE)
|
||||
bool simplePhysics = false;
|
||||
bool body_solid(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) return false;
|
||||
const uint b = bodyGrid[p.y * pc.gw + p.x];
|
||||
return b != 0u && (b & 0x80000000u) == 0u; // (bit 31: a body that is no platform - the lanterns and their chains do not block, main.cpp isPlatform)
|
||||
}
|
||||
// 00bb6820: the leak emitter's offset is the local hit point, clamped to the HitboxComponent (a corpse part: +-12 here); without one (ex7.w, a prop) as it is, zero beyond 256 px
|
||||
vec2 leak_offset(Ent e, vec2 rel) { return e.ex7.w > 0.5 ? (dot(rel, rel) > 65536.0 ? vec2(0.0) : rel) : clamp(rel, vec2(-12.0), vec2(12.0)); }
|
||||
bool solid_cell_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) return false; // outside the window there is no ground (such entities are frozen)
|
||||
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
|
||||
if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) return true;
|
||||
return body_solid(w);
|
||||
return !simplePhysics && body_solid(w);
|
||||
}
|
||||
// The character's cell test 0063ef70: a platform cell counts only when one of its four neighbours (below, left, right, above) is one too - a lone pixel (a crumb of
|
||||
// rock, a single body pixel) does not stop the player. (Its other exception, the bodies held by telekinesis - 00c2dfc0 stamps their b2Body user data +0x48 with
|
||||
@@ -112,6 +112,9 @@ bool line_clear(vec2 a, vec2 b) { // no solid cell between two points (3-cell s
|
||||
return true;
|
||||
}
|
||||
bool box_hits(ivec2 o, vec4 box) {
|
||||
// SimplePhysicsSystem (00c17fc0 -> 0063f6b0) traces the entity origin
|
||||
// through grid cells. The character box/platform push-out is a different system.
|
||||
if (simplePhysics) return solid_cell_at(o);
|
||||
ivec2 lo = o + ivec2(int(box.x), int(box.z)), hi = o + ivec2(int(box.y), int(box.w)); // hi exclusive
|
||||
for (int y = lo.y; y < hi.y; ++y)
|
||||
for (int x = lo.x; x < hi.x; ++x)
|
||||
@@ -120,32 +123,6 @@ bool box_hits(ivec2 o, vec4 box) {
|
||||
}
|
||||
|
||||
// Blood: real particles of the creature's blood material that fly out along the hit and become cells where they land.
|
||||
void blood_burst(uint seed, vec2 pos, vec2 dirv, int count, float speed, int matp1, uint colour) {
|
||||
if (matp1 <= 0) return;
|
||||
for (int k = 0; k < count && k < 64; ++k) {
|
||||
float r1 = float(hash3(seed, uint(k), 1u) & 0xFFFFu) / 65536.0, r2 = float(hash3(seed, uint(k), 2u) & 0xFFFFu) / 65536.0;
|
||||
vec2 v = dirv * (0.4 + r1 * 1.2) * speed + vec2((r2 - 0.5) * 1.4 * speed, -0.3 - r1 * 0.9 * speed);
|
||||
emit_material_particle(pos + vec2(0.0, -3.0), v, 70.0 + r2 * 40.0, PART_GRAV, colour, matp1 - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Liquid of a living hit (00ab9a90): the count does not depend on the damage, N = trunc((u * (40 - 20) + 20) * blood_multiplier + 0.5) (the incoming multiplier and the world
|
||||
// multiplier are 1); the first droplet leaves along the hit, each next one is turned by a random angle (-0.6..0.6 rad) from the previous one, a speed of 0.5..1.25 times
|
||||
// the base, at the hit point +-1.5. The velocity is 20 x s x the packet impulse (px/s): a projectile's velocity x ragdoll_force_multiplier (00c091f0), an explosion's radial force (00657960).
|
||||
void hit_blood(uint seed, vec2 hit, vec2 dirv, float bloodMult, int matp1, uint colour, float speedMul) {
|
||||
if (matp1 <= 0 || bloodMult <= 0.0) return;
|
||||
const float u0 = float(hash3(seed, 0u, 11u) & 0x7FFFu) / 32767.0;
|
||||
int n = int((u0 * 20.0 + 20.0) * bloodMult + 0.5);
|
||||
vec2 v = dirv;
|
||||
for (int k = 0; k < n && k < 64; ++k) {
|
||||
float u1 = float(hash3(seed, uint(k), 12u) & 0x7FFFu) / 32767.0, u2 = float(hash3(seed, uint(k), 13u) & 0x7FFFu) / 32767.0;
|
||||
float u3 = float(hash3(seed, uint(k), 14u) & 0x7FFFu) / 32767.0, u4 = float(hash3(seed, uint(k), 15u) & 0x7FFFu) / 32767.0;
|
||||
float sp = 0.5 + 0.75 * u1, a = -0.6 + 1.2 * u2;
|
||||
v = vec2(v.x * cos(a) - v.y * sin(a), v.x * sin(a) + v.y * cos(a));
|
||||
emit_material_particle(vec2(hit.x - 1.5 + 3.0 * u4, hit.y - 1.5 + 3.0 * u3), v * (20.0 * sp / 60.0) * speedMul, 70.0 + u2 * 40.0, PART_GRAV, colour, matp1 - 1); // (20 s v px/s, v the packet impulse)
|
||||
}
|
||||
}
|
||||
|
||||
layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits (see edits.comp): flames falling from a burning creature
|
||||
bool extinguishes_at(ivec2 w) { // water and the like put a burning creature out
|
||||
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
|
||||
@@ -166,6 +143,7 @@ void main() {
|
||||
if (id >= uint(ctrl.inp.w)) return;
|
||||
Ent e = ents[id];
|
||||
int kind = e.st.x;
|
||||
simplePhysics = kind == 4 && (e.st.w & ST_RIGID) == 0;
|
||||
if (kind == 0 || (e.st.w & ST_DEAD) != 0) return;
|
||||
const bool is_prop = kind == 4;
|
||||
if (is_prop && e.ex7.y > 0.0) { // a corpse part with a liquid inventory: the particle emitter of the ragdoll piece (docs/BLOOD_EMITTER_LIFECYCLE.md)
|
||||
@@ -177,49 +155,35 @@ void main() {
|
||||
vec4 vr = e.ex;
|
||||
float nextf = e.ex2.x, mode = e.ex2.y;
|
||||
const uint fr = uint(ctrl.inp2.x);
|
||||
const int dmg = atomicExchange(ents[id].ex8.z, 0);
|
||||
if (dmg > 90) { // 00bb6820: strictly more than min_damage_to_leak (0.09)
|
||||
const vec2 rel = vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001;
|
||||
const uint sd = hash3(id, fr, 53u);
|
||||
float u[8];
|
||||
for (int k = 0; k < 8; ++k) u[k] = float(hash3(sd, uint(k), 54u) & 0x7FFFu) / 32767.0;
|
||||
if (u[7] < 1.0) { // u < leak_on_damage_percent (1): fails only for the draw 32767 / 32767
|
||||
vec2 d = leak_offset(e, rel);
|
||||
const float dl = length(d);
|
||||
d = dl > 1e-4 ? d / dl : vec2(0.0);
|
||||
const float dx = d.x * (0.85 + 0.29999995 * u[0]), dy = d.y * (0.85 + 0.29999995 * u[1]);
|
||||
const float speed = 100.0 * (0.7 + (1.1 - 0.7) * u[2]);
|
||||
const float vx = speed * dx, vy = speed * dy;
|
||||
vr = vec4(vx * (0.6 + 0.39999998 * u[3]), vx * (1.0 + 0.39999998 * u[4]), vy * (0.6 + 0.39999998 * u[5]), vy * (1.0 + 0.39999998 * u[6]));
|
||||
mode = 2.0;
|
||||
}
|
||||
}
|
||||
atomicExchange(ents[id].ex8.z, 0); // Per-hit reconfiguration already happened at the source.
|
||||
if (mode > 0.5 && float(fr) >= nextf && e.ex7.z > 0.0) {
|
||||
const uint seed = hash3(id, fr, 51u);
|
||||
nextf = float(fr) + ((e.ex2.z > 0.5 && mode < 1.5) ? 3.0 + float(hash3(seed, 3u, 58u) % 3u) : 1.0 + float(seed & 1u)); // the next emission is scheduled before anything is spawned (3-5 frames when the cosmetic spray exists and no damage has rewritten the emitter)
|
||||
const int cnt = mode > 1.5 ? 1 : 1 + int(hash3(seed, 7u, 56u) % 3u);
|
||||
if (reserve >= float(cnt)) {
|
||||
if ((bloodPolicy[id].state.x & 2u) == 0u && reserve >= float(cnt)) {
|
||||
const float ca = cos(e.a.z), sa = sin(e.a.z);
|
||||
int made = 0;
|
||||
for (int k = 0; k < cnt; ++k) {
|
||||
const uint r = hash3(seed, uint(k), 52u);
|
||||
const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 57u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 57u) & 0xFFFFu) / 65536.0;
|
||||
const vec2 lo = leak_offset(e, vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001);
|
||||
const vec2 lo = vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001;
|
||||
const vec2 off = mode > 1.5 ? vec2(ca * lo.x - sa * lo.y, sa * lo.x + ca * lo.y) : vec2(0.0); // the damage event's offset (the hit point), turned with the body
|
||||
const vec2 at = e.a.xy + off + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py); // rect (-1,-1) .. (1,1)
|
||||
const vec2 along = (e.ex7.w < 0.5 || (bloodPolicy[id].state.x & 32u) != 0u) ? mix(e.ex10.xy, e.a.xy, float(k + 1) / float(cnt)) : e.a.xy;
|
||||
const vec2 at = along + off + vec2(-0.5 + 2.0 * px, -0.5 + 2.0 * py); // rect (-1,-1) .. (1,1)
|
||||
if (solid_cell_at(ivec2(floor(at)))) break;
|
||||
vec2 v = vec2(mix(vr.x, vr.y, ux), mix(vr.z, vr.w, uy)) * (1.0 / 60.0); // px/s -> cells/frame
|
||||
vec2 v = vec2(mix(min(vr.x, vr.y), max(vr.x, vr.y), ux), mix(min(vr.z, vr.w), max(vr.z, vr.w), uy)) * (1.0 / 60.0); // px/s -> cells/frame
|
||||
v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y); // the entity's rotation
|
||||
if (emit_material_particle(at, v, 70.0 + float((r >> 24) & 0x3Fu), PART_GRAV, uint(e.ex3.y), int(e.ex7.z) - 1)) ++made; // only the particles that were created cost reserve (00bc75e0)
|
||||
}
|
||||
reserve -= float(made);
|
||||
ents[id].ex10.xy = e.a.xy;
|
||||
}
|
||||
}
|
||||
if (e.ex2.z > 0.5 && e.ex7.z > 0.0 && float(fr) >= e.ex6.z) { // blood_spray_create_some_cosmetic: a second emitter on the same reserve, 1-3 cosmetic droplets (no cells) every 1-2 frames
|
||||
const uint seed2 = hash3(id, fr, 59u);
|
||||
ents[id].ex6.z = float(fr) + 1.0 + float(seed2 & 1u);
|
||||
const int cnt2 = 1 + int(hash3(seed2, 7u, 56u) % 3u);
|
||||
if (reserve >= float(cnt2)) {
|
||||
const int cnt2 = 1 + int(hash3(seed2, 7u, 56u) % 2u);
|
||||
if ((bloodPolicy[id].state.x & 2u) == 0u && reserve >= float(cnt2)) {
|
||||
float u2[8];
|
||||
for (int k = 0; k < 8; ++k) u2[k] = float(hash3(seed2, uint(k), 60u) & 0x7FFFu) / 32767.0;
|
||||
const vec2 d = e.ex6.xy * vec2(0.85 + 0.29999995 * u2[0], 0.85 + 0.29999995 * u2[1]); // the same formula as the death emitter, other draws
|
||||
@@ -231,13 +195,15 @@ void main() {
|
||||
for (int k = 0; k < cnt2; ++k) {
|
||||
const uint r = hash3(seed2, uint(k), 61u);
|
||||
const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 62u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 62u) & 0xFFFFu) / 65536.0;
|
||||
const vec2 at = e.a.xy + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py);
|
||||
const vec2 along = mix(e.ex10.zw, e.a.xy, float(k + 1) / float(cnt2));
|
||||
const vec2 at = along + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py);
|
||||
if (solid_cell_at(ivec2(floor(at)))) break;
|
||||
vec2 v = vec2(mix(cr.x, cr.y, ux), mix(cr.z, cr.w, uy)) * (1.0 / 60.0);
|
||||
vec2 v = vec2(mix(min(cr.x, cr.y), max(cr.x, cr.y), ux), mix(min(cr.z, cr.w), max(cr.z, cr.w), uy)) * (1.0 / 60.0);
|
||||
v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y);
|
||||
if (emit_particle_hit(at, v, 25.0 + float((r >> 24) & 0x1Fu), 0.1, uint(e.ex3.y), int(e.ex7.z) - 1) != 0xFFFFFFFFu) ++made; // a droplet without a cell: it fades in the air and stains where it lands (it owns no cell: 00716ce0)
|
||||
}
|
||||
reserve -= float(made);
|
||||
ents[id].ex10.zw = e.a.xy;
|
||||
}
|
||||
}
|
||||
ents[id].ex = vr; ents[id].ex2.x = nextf; ents[id].ex2.y = mode; ents[id].ex7.y = max(reserve, 0.0);
|
||||
@@ -260,7 +226,7 @@ void main() {
|
||||
int energy = e.x.y;
|
||||
// pushed out of solid cells (sand piles landing on it, an explosion refilling, a wall appearing): find the nearest free spot above or beside
|
||||
const bool rigidBody = is_prop && (flags & ST_RIGID) != 0; // moved by the rigid-body engine: its own mask must not count as an obstacle
|
||||
if (kind != 0 && !rigidBody && box_hits(ip, e.box)) {
|
||||
if (kind != 0 && !rigidBody && !simplePhysics && box_hits(ip, e.box)) {
|
||||
bool freed = false;
|
||||
for (int k = 1; k <= 12 && !freed; ++k) {
|
||||
const ivec2 cand[5] = ivec2[5](ivec2(0, -k), ivec2(-k, -k), ivec2(k, -k), ivec2(-k, 0), ivec2(k, 0));
|
||||
@@ -403,11 +369,11 @@ void main() {
|
||||
hit_flash(id, dmg > 0, (types & 0x10000) != 0);
|
||||
if (dmg > 0) {
|
||||
hitv = vec2(float(atomicExchange(ents[id].aux.x, 0)), float(atomicExchange(ents[id].aux.y, 0))) * 0.001;
|
||||
hit_blood(hash3(id, uint(ctrl.inp2.x), 31u), pos + vec2(0.0, 0.5 * (e.box.z + e.box.w)), hitv, e.ex7.x, e.ex3.x, uint(e.ex3.y), 1.0); // (hitv: the packet impulse, px/s)
|
||||
}
|
||||
}
|
||||
// melee attack
|
||||
if (tAlive && !frozen && (ents[tgt].ex8.x & 8192) == 0 && e.ex.z > 0.5 && abs(dx) < e.ai.z && abs(dy) < e.ai.z + 4.0 && cooldown <= 0.0) {
|
||||
hit_liquid(tgt, e.ai.w * dealt_mult(e) * ents[tgt].ex9.w, ents[tgt].a.xy, vec2(0.0), 1.0, 1.0, 1, hash3(id, uint(pc.frame), 801u));
|
||||
atomicAdd(ents[tgt].x.x, int(e.ai.w * dealt_mult(e) * ents[tgt].ex9.w * 1000.0)); // the target's melee multiplier
|
||||
if (e.ai.w > 0.0 && e.ai.w * dealt_mult(e) * ents[tgt].ex9.w <= 0.0 && ents[tgt].st.x >= 1 && ents[tgt].st.x <= 3) ents[tgt].ex11.w = float(pc.frame); // (a blocked hit: BLOCK on the host)
|
||||
ents[id].rg2.w = 1 | ((ctrl.inp2.x & 0xFFFFFF) << 4);
|
||||
|
||||
@@ -12,5 +12,5 @@ layout(std430, set = 0, binding = 39) buffer EntPrevW { vec4 prevw[]; };
|
||||
void main() {
|
||||
uint i = gl_GlobalInvocationID.x;
|
||||
if (i >= uint(ctrl.inp.w)) return;
|
||||
prevw[i] = vec4(ents[i].a.xy, 0.0, 0.0);
|
||||
prevw[i] = ents[i].a; // rigid bodies also need their previous angle
|
||||
}
|
||||
+27
-4
@@ -29,6 +29,7 @@ layout(std430, set = 0, binding = 34) readonly buffer FxTex { uint fxtex[]; };
|
||||
#include "fx_host.glsl"
|
||||
|
||||
#include "fx_state.glsl"
|
||||
#include "hit_liquid.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; };
|
||||
@@ -103,10 +104,30 @@ void note_block(int victim, float raw, float dealt) {
|
||||
// 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));
|
||||
const HitLiquidPolicy policy = bloodPolicy[slot];
|
||||
if ((policy.state.x & 1u) == 0u || policy.inventory.x <= 0.0 || dmg >= ents[slot].hp.x || dmg <= policy.inventory.y) return;
|
||||
const uint serial = uint(atomicAdd(ents[slot].ex8.z, 1));
|
||||
const uint seed = hash3(uint(slot), uint(pc.frame), serial + floatBitsToUint(hit.x));
|
||||
const float probability = float(hash3(seed, 7u, 54u) & 32767u) / 32767.0;
|
||||
if (probability >= policy.inventory.x || (policy.state.x & 16u) == 0u) return;
|
||||
vec2 rel = hit - ents[slot].a.xy;
|
||||
if (policy.inventory.z != 0.0) rel.x /= policy.inventory.z;
|
||||
if (policy.inventory.w != 0.0) rel.y /= policy.inventory.w;
|
||||
const float ca = cos(ents[slot].a.z), sa = sin(ents[slot].a.z);
|
||||
rel = vec2(ca * rel.x + sa * rel.y, -sa * rel.x + ca * rel.y);
|
||||
if ((policy.state.x & 4u) != 0u) rel = clamp(rel, policy.bounds.xz, policy.bounds.yw);
|
||||
else if (dot(rel, rel) > 65536.0) rel = vec2(0.0);
|
||||
const float dl = length(rel);
|
||||
const vec2 direction = dl > 0.0 ? rel / dl : vec2(0.0);
|
||||
float u[7];
|
||||
for (int k = 0; k < 7; ++k) u[k] = float(hash3(seed, uint(k), 54u) & 32767u) / 32767.0;
|
||||
const float speed = 100.0 * mix(policy.pressure.x, policy.pressure.y, u[2]);
|
||||
const vec2 v = direction * (vec2(0.85) + 0.29999995 * vec2(u[0], u[1])) * speed;
|
||||
ents[slot].ex = vec4(v.x * (0.6 + 0.39999998 * u[3]), v.x * (1.0 + 0.39999998 * u[4]),
|
||||
v.y * (0.6 + 0.39999998 * u[5]), v.y * (1.0 + 0.39999998 * u[6]));
|
||||
ents[slot].ex2.y = 2.0; // Keep the emitter's next scheduled frame.
|
||||
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)
|
||||
@@ -206,3 +227,5 @@ float hit_damage(int k, float proj, int tb, vec4 f) {
|
||||
+ 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;
|
||||
}
|
||||
|
||||
#include "hit_liquid_channels.glsl"
|
||||
@@ -4,4 +4,6 @@ layout(std430, set = 0, binding = 35) buffer FxHost {
|
||||
vec4 hcast[1024]; // 2 lists x 256 requests x 2 vec4: (x, y, dir x, dir y), (node or -boom-1, team, seed, flags)
|
||||
vec4 hnodes[8192]; // 2048 nodes x 4 vec4
|
||||
ivec4 hspecs[16384]; // 4096 specs x 4 ivec4
|
||||
ivec4 hcollision; // x: compact collision target count, rebuilt on the GPU before flight
|
||||
int htargets[4096]; // ascending entity slots; preserves first-hit ordering
|
||||
};
|
||||
@@ -44,14 +44,14 @@ void displaced_particle(uint id, FPart p) {
|
||||
uint seed = id * 7u + uint(pc.frame);
|
||||
ivec2 here = ivec2(floor(pos));
|
||||
if (kind == 3u) { // waiting for a free place
|
||||
if (reinsert(cw, here, seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; }
|
||||
if (reinsert(cw, here, seed, floatBitsToUint(p.b.z))) { release_part(id); return; }
|
||||
// No place: out of the window it is gone (00712390: a particle out of the loaded area dies; it waited there forever and filled the pool - 1000+ after 25 s).
|
||||
// In the window the search of its column ended on a solid (a pocket under an overhang, a pool under ice, a column shifted into the rock): it drifts a cell,
|
||||
// down into a free cell (air, gas), else sideways, and searches again from there; after 10 s it is dropped. (It used to wait where it was: blood and water
|
||||
// pixels frozen in the air near pools.)
|
||||
const ivec2 hq = here - ivec2(pc.view_x, pc.view_y);
|
||||
const uint tries = p.c.w & 0xFFFFFu;
|
||||
if (hq.x < 0 || hq.y < 0 || hq.x >= pc.gw || hq.y >= pc.gh || tries >= 600u) { fparts[id].b.x = 0.0; return; }
|
||||
if (hq.x < 0 || hq.y < 0 || hq.x >= pc.gw || hq.y >= pc.gh || tries >= 600u) { release_part(id); return; }
|
||||
ivec2 step = ivec2(0, 1);
|
||||
if (hq.y + 1 >= pc.gh || (mflags(cell_mat(cells[(hq.y + 1) * pc.gw + hq.x])) & (F_STATIC | F_LIQUID)) != 0u) {
|
||||
const int sx = (hash3(seed, tries, 67u) & 1u) != 0u ? 1 : -1;
|
||||
@@ -112,7 +112,7 @@ void displaced_particle(uint id, FPart p) {
|
||||
} else if (!dead) cur = target; // beyond the 60 probes the end point is taken as it is
|
||||
pos = cur;
|
||||
if (dead) {
|
||||
if (reinsert(cw, ivec2(floor(pos)), seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; }
|
||||
if (reinsert(cw, ivec2(floor(pos)), seed, floatBitsToUint(p.b.z))) { release_part(id); return; }
|
||||
kind = 3u; // no place yet: it keeps trying
|
||||
}
|
||||
fparts[id].a = vec4(pos, vel);
|
||||
@@ -129,7 +129,7 @@ void main() {
|
||||
uint id = gl_GlobalInvocationID.x;
|
||||
if (id >= uint(FPART_MAX)) return;
|
||||
FPart p = fparts[id];
|
||||
if (p.b.x <= 0.0) return;
|
||||
if (p.b.x <= 0.0 || p.b.x >= 2.9e38) return; // Allocation may still be initializing this record.
|
||||
if (p.c.y != 0u) { displaced_particle(id, p); return; }
|
||||
const bool real = p.b.y < 0.0;
|
||||
vec2 pos = p.a.xy, vel = p.a.zw;
|
||||
@@ -217,7 +217,6 @@ void main() {
|
||||
vel += vec2(-sin(ang), cos(ang)) * force / 60.0;
|
||||
}
|
||||
fparts[id].a = vec4(pos, vel);
|
||||
fparts[id].b.x = p.b.x;
|
||||
if (landed) { // 00716600 / 0071fba0: the cell goes back into the grid where the particle ended, else the first free (or lighter) cell up to 128 above, drifting
|
||||
// sideways; with no place yet it waits as a returning cell (not lost: a barrel's 750 units of spill all come out)
|
||||
const uint cw = uint(landMat) | (((hash3(id + uint(pc.frame) * 7u, uint(landMat), 91u) >> 16) & 0xFFu) << 16);
|
||||
@@ -225,11 +224,13 @@ void main() {
|
||||
const ivec2 lq = ivec2(floor(landPos)) - ivec2(pc.view_x, pc.view_y);
|
||||
if (lq.x >= 0 && lq.y >= 0 && lq.x < pc.gw && lq.y < pc.gh && !reinsert(cw, ivec2(floor(landPos)), id * 7u + uint(pc.frame), bv)) {
|
||||
fparts[id].a = vec4(landPos, 0.0, 0.0);
|
||||
fparts[id].b = vec4(1.0e6, 1.0, uintBitsToFloat(bv), 0.0);
|
||||
fparts[id].c = uvec4(cw, 3u, 0u, 0u);
|
||||
}
|
||||
fparts[id].b = vec4(1.0e6, 1.0, uintBitsToFloat(bv), 0.0);
|
||||
} else release_part(id);
|
||||
return;
|
||||
}
|
||||
if (p.b.x <= 0.0) release_part(id);
|
||||
else fparts[id].b.x = p.b.x;
|
||||
}
|
||||
if (p.b.x <= 0.0) return;
|
||||
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
||||
|
||||
+6
-10
@@ -4,23 +4,19 @@ const int FP_MAX = 32768, FPART_MAX = 16384, FANIM_MAX = 256, FANIM_ALL = 512;
|
||||
struct FProj { vec4 a; vec4 b; vec4 c; ivec4 d; vec4 e; vec4 f; }; // d.w: the cast serial (0 = dead; the host's twins match it), d.z bit 10 = friendly fire of the shot; f: the wand's damage_electricity_add, damage_ice_add, knockback_force add, trail material (material + 1 | amount << 12) // e: rotation angle, angular velocity (thrown bodies) // pos, vel / damage, life left, gravity, friction / explosion radius, damage, age, bounces / type, spec, fired mask + team, alive
|
||||
struct FPart { vec4 a; vec4 b; uvec4 c; }; // pos, vel / life, max life, gravity, colour bits (rgb, bit 24 = fades)
|
||||
struct FAnim { vec4 a; ivec4 b; vec4 c; }; // x, y, light radius, light strength / boom, age, max age, colour bits / angle
|
||||
layout(std430, set = 0, binding = 36) buffer FxState {
|
||||
layout(std430, set = 0, binding = 36) coherent buffer FxState {
|
||||
uint fcnt[4]; // projectile ring, particle ring, animation ring
|
||||
FProj fprojs[FP_MAX]; // (src/gun/gpu_fx.h kMaxProj)
|
||||
FPart fparts[16384];
|
||||
FAnim fanims[512];
|
||||
};
|
||||
|
||||
// Integer alias of the same storage, solely for atomically claiming a particle's
|
||||
// life word. A plain float test allowed concurrent searches to return the same slot.
|
||||
layout(std430, set = 0, binding = 36) coherent buffer FxStateAtomic { uint fxwords[]; };
|
||||
// A free record (00717400 / 007174e0: the pools return null when they are full, a live particle is never replaced): the search starts at the ring position and takes the first dead
|
||||
// record of the next 64; -1 when there is none.
|
||||
int alloc_part() {
|
||||
const uint s0 = atomicAdd(fcnt[1], 1u);
|
||||
for (uint k = 0u; k < 64u; ++k) {
|
||||
const uint s = (s0 + k) % uint(FPART_MAX);
|
||||
if (fparts[s].b.x <= 0.0) return int(s);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
#include "particle_alloc.glsl"
|
||||
|
||||
// colour bits: rgb, bit 24 = fades with its life, bit 25 = additive. c.w of a cosmetic: bit 1 = no grid collision (collide_with_grid 0), bit 2 = the bounce is spent
|
||||
// (00712010 sets the bounce flag 6a:20 on every record; fx_particles.comp)
|
||||
@@ -39,7 +35,7 @@ bool cell_free_at(vec2 w) {
|
||||
return q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cell_mat(cells[q.y * pc.gw + q.x]) == 0u;
|
||||
}
|
||||
void emit_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive) { emit_particle_g(pos, vel, life, grav, rgb, fade, additive, true); }
|
||||
// A droplet without a cell that ends where it meets any cell (the cosmetic blood spray): c.x = 1; with a material (c.z = material + 1) it leaves a cell of it there, so the spray feeds the pools.
|
||||
// A droplet without a cell that ends where it meets any cell (the cosmetic blood spray): c.x = 1; c.z = material + 1 supplies the stain colour. It never owns or deposits a cell.
|
||||
uint emit_particle_hit(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // returns 0xFFFFFFFF when the pool is full
|
||||
const int si = alloc_part();
|
||||
if (si < 0) return 0xFFFFFFFFu;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Native hit-liquid policy (00ab9a90). Called at the damage source, once per channel.
|
||||
struct HitLiquidPolicy { uvec4 liquid; uvec4 state; vec4 bounds; vec4 inventory; vec4 pressure; };
|
||||
layout(std430, set = 0, binding = 67) readonly buffer HitLiquid {
|
||||
ivec4 bloodSettings;
|
||||
HitLiquidPolicy bloodPolicy[4096];
|
||||
vec4 bloodProjectile[512];
|
||||
};
|
||||
void hit_liquid(int target, float damage, vec2 hit, vec2 impulse, float incoming, float speedScale, int fx, uint seed) {
|
||||
if (damage <= 0.0 || incoming <= 0.0 || fx == 0 || (bloodPolicy[target].state.x & 2u) != 0u) return;
|
||||
const uvec4 policy = bloodPolicy[target].liquid;
|
||||
int matp1 = int(policy.x & 65535u);
|
||||
uint colour = policy.z;
|
||||
if (bloodSettings.y != 0 && (policy.x >> 16) != 0u) { matp1 = int(policy.x >> 16); colour = policy.w; }
|
||||
if (matp1 == 0) return;
|
||||
const float u = float(hash3(seed, 0u, 11u) & 32767u) / 32767.0;
|
||||
const int count = int(((u * 20.0 + 20.0) * uintBitsToFloat(policy.y) + 0.5) * incoming) * bloodSettings.x;
|
||||
vec2 v = impulse;
|
||||
for (int k = 0; k < count; ++k) {
|
||||
const float su = float(hash3(seed, uint(k), 12u) & 32767u) / 32767.0;
|
||||
const float au = float(hash3(seed, uint(k), 13u) & 32767u) / 32767.0;
|
||||
const float yu = float(hash3(seed, uint(k), 14u) & 32767u) / 32767.0;
|
||||
const float xu = float(hash3(seed, uint(k), 15u) & 32767u) / 32767.0;
|
||||
const float a = -0.6 + 1.2 * au;
|
||||
v = vec2(v.x * cos(a) - v.y * sin(a), v.x * sin(a) + v.y * cos(a));
|
||||
// The constructor adds +0.5 after the hit handler's +/-1.5 jitter.
|
||||
const vec2 pos = hit - vec2(1.0) + 3.0 * vec2(xu, yu);
|
||||
const ivec2 p = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
|
||||
if (any(lessThan(p, ivec2(0))) || any(greaterThanEqual(p, ivec2(pc.gw, pc.gh)))) continue;
|
||||
emit_material_particle(pos, v * ((20.0 / 60.0) * (0.5 + 0.75 * su) * speedScale), 70.0 + au * 40.0, PART_GRAV, colour, matp1 - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// 00ab8f10: one hit-liquid event per positive damage channel, in native order.
|
||||
void projectile_hit_liquid(int k, float projectile, int tb, vec4 adds, float scale, vec2 hit, vec2 impulse, uint seed) {
|
||||
const Ent v = ents[k];
|
||||
const vec4 a = ftf(tb + 12), b = ftf(tb + 13), c = ftf(tb + 14);
|
||||
const float channel[13] = float[13](projectile * v.ex9.x, (a.y + adds.x) * v.ex12.x,
|
||||
c.w * v.ex9.y, a.z * v.ex9.z, a.x * v.ex9.w, b.y * v.ex12.y,
|
||||
b.x * v.ex12.z, (a.w + adds.y) * v.ex12.w, c.x * v.ex13.x,
|
||||
b.z * v.ex13.z, b.w * v.ex13.w, c.y * v.ex14.y, c.z * v.ex14.z);
|
||||
const vec2 factors = bloodProjectile[tb / TYPE_STRIDE].xy;
|
||||
const int fx = (fti(tb + 11).y >> 4) & 15;
|
||||
for (int i = 0; i < 13; ++i) {
|
||||
if (i == 8) continue; // Healing is a negative damage channel.
|
||||
hit_liquid(k, channel[i] * scale, hit, impulse, factors.x, factors.y, fx, hash3(seed, uint(i), 411u));
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -46,6 +46,7 @@ void main() {
|
||||
uint c = cells[i];
|
||||
uint m = cell_mat(c);
|
||||
uint f = mflags(m);
|
||||
if (pc.pass_ == 0 && (terrain_age[i] >> 16) != m) terrain_age[i] = (m << 16) | 0x300u;
|
||||
if ((f & (F_AIR | F_STATIC)) != 0u) return;
|
||||
|
||||
vec2 e = ext[i];
|
||||
@@ -84,7 +85,7 @@ void main() {
|
||||
const ivec2 bp = p + ivec2(0, 1);
|
||||
release = r2 < ceilP && in_bounds(bp) && cell_mat(cells[idx(bp)]) == 0u;
|
||||
}
|
||||
if (release) cells[i] = c & ~(ATTACH_BIT | (3u << 24));
|
||||
if (release) { cells[i] = c & ~(ATTACH_BIT | (3u << 24)); terrain_age[i] = (m << 16) | 0x300u; }
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -256,6 +257,13 @@ void main() {
|
||||
|
||||
if ((f & F_FIRE) != 0u && target == NONE && (rnd(i, 30u) & 15u) == 0u) { cells[i] = 0u; return; } // 00709fc0: the extinction test (1/16) sits at the tail: a successful move and the early returns bypass it
|
||||
if ((f & F_GAS) != 0u && target != NONE) nvy = vy + 1.0; // 0070aad0: the gas age counts its successful moves
|
||||
// 0070b160: only a resting powder whose state age reaches 20 becomes terrain.
|
||||
// Successful moves reset both swapped cells below in apply.comp.
|
||||
if ((f & F_LIQCELL) != 0u) {
|
||||
const bool resting = target == NONE && nvy == 0.0;
|
||||
const uint age = resting ? min((terrain_age[i] & 255u) + (P == 0 ? 1u : 0u), 255u) : 0u;
|
||||
terrain_age[i] = (m << 16) | (resting ? 0x300u : 0x100u) | age;
|
||||
}
|
||||
if (target != NONE) {
|
||||
intent[i] = uvec4(target | (flip << 31), prio, floatBitsToUint(nvy), floatBitsToUint(nvx));
|
||||
atomicMax(claims[i], prio);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
int alloc_part() {
|
||||
const uint s0 = atomicAdd(fcnt[1], 1u);
|
||||
for (uint k = 0u; k < 64u; ++k) {
|
||||
const uint s = (s0 + k) % uint(FPART_MAX);
|
||||
const uint word = 4u + uint(FP_MAX) * 24u + s * 12u + 4u;
|
||||
const uint previous = atomicAdd(fxwords[word], 0u);
|
||||
if (uintBitsToFloat(previous) <= 0.0 && atomicCompSwap(fxwords[word], previous, floatBitsToUint(3.0e38)) == previous) return int(s);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void release_part(uint slot) {
|
||||
// Publish a free slot only after its previous owner has finished all writes.
|
||||
memoryBarrierBuffer();
|
||||
atomicExchange(fxwords[4u + uint(FP_MAX) * 24u + slot * 12u + 4u], 0u);
|
||||
}
|
||||
+28
-5
@@ -58,7 +58,9 @@ bool shooter_safe(int flags, int age) { const int f = (flags >> 16) & 255; retur
|
||||
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 aheadBody = body_slot(gAhead), hereBody = body_slot(pos);
|
||||
for (int candidate = 0; candidate < hcollision.x; ++candidate) {
|
||||
const int k = htargets[candidate];
|
||||
if (ents[k].ex19.w == -float(gl_GlobalInvocationID.x + 1u)) continue; // its own damage proxy
|
||||
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
|
||||
@@ -73,7 +75,7 @@ bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit)
|
||||
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)
|
||||
if (kind == 4 && ents[k].ex7.w > 0.5) { if (aheadBody == k || hereBody == 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; }
|
||||
@@ -83,7 +85,9 @@ bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit)
|
||||
|
||||
bool creature_at(vec2 pos, float r, out int hit) {
|
||||
hit = -1;
|
||||
for (int k = 1; k < ctrl.inp.w; ++k) {
|
||||
for (int candidate = 0; candidate < hcollision.x; ++candidate) {
|
||||
const int k = htargets[candidate];
|
||||
if (k == 0) continue;
|
||||
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;
|
||||
@@ -131,8 +135,6 @@ void explode(vec2 pos, float radius, float damage, int type, int team, int shoot
|
||||
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
|
||||
@@ -160,6 +162,8 @@ void explode(vec2 pos, float radius, float damage, int type, int team, int shoot
|
||||
{ // 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;
|
||||
hit_liquid(k, dk, c, away * a, 1.0, 1.0, 1, hash3(critSeed, uint(k), 711u));
|
||||
if (kind == 4) body_damage(k, c, dk);
|
||||
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);
|
||||
@@ -328,6 +332,7 @@ void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team,
|
||||
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)
|
||||
hit_liquid(e, dmg, pts[k], v * 60.0 * ftf(vb + 3).x, 1.0, 1.0, 1, hash3(id, uint(k), uint(pc.frame)));
|
||||
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));
|
||||
@@ -375,6 +380,7 @@ void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team,
|
||||
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;
|
||||
projectile_hit_liquid(hk, p.b.x, tb, p.f, boost_of(tw, hk) * fadeK, tip, tipV * 60.0 * ftf(tb + 9).z, hash3(id, uint(pc.frame), 251u));
|
||||
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));
|
||||
@@ -396,6 +402,22 @@ void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team,
|
||||
|
||||
void main() {
|
||||
uint id = gl_GlobalInvocationID.x;
|
||||
if (pc.pass_ == 2) {
|
||||
// Query once per simulation tick, shared by every flight substep.
|
||||
// Dead creatures and props are omitted. Worm segments are validated
|
||||
// against their root by victim_at, preserving its existing hit rules.
|
||||
if (id == 0u) {
|
||||
int count = 0;
|
||||
for (int k = 0; k < min(ctrl.inp.w, 4096); ++k) {
|
||||
const int kind = ents[k].st.x;
|
||||
if (kind != 0 && (ents[k].st.w & 8) != 0) continue;
|
||||
if (kind == 1 || kind == 2 || kind == 3 || (kind == 4 && ents[k].hp.x < 1.0e8) ||
|
||||
(kind == 0 && (ents[k].st.w & 64) != 0 && ents[k].ex19.w > 0.5)) htargets[count++] = k;
|
||||
}
|
||||
hcollision.x = count;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (id >= uint(FP_MAX)) return;
|
||||
FProj p = fprojs[id];
|
||||
if (p.d.w == 0) return;
|
||||
@@ -574,6 +596,7 @@ void main() {
|
||||
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);
|
||||
projectile_hit_liquid(hk, p.b.x * speedScale, tb, p.f, boost_of(t1.w, hk) * cm, pos, vel * 60.0 * ftf(tb + 9).z, hash3(id, uint(pc.frame), uint(age)));
|
||||
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
|
||||
|
||||
+133
-69
@@ -3,56 +3,61 @@
|
||||
layout(local_size_x = 8, local_size_y = 8) in;
|
||||
#include "common.glsl"
|
||||
|
||||
// Material reactions (liquids mixing, acid, water on lava, ...). The grid is cut into disjoint pairs of neighbouring cells; pc.pass_
|
||||
// (0..3) selects the pairing: 0/1 horizontal pairs starting at even/odd columns, 2/3 vertical pairs. Both cells of a pair evaluate the
|
||||
// same reaction (symmetric hash) and each applies its own side, so no cell is written twice. Four passes visit every adjacency once per frame.
|
||||
// Material reactions: pass 4 plans one attempt per source, pass 5 commits thermal
|
||||
// changes. Passes 0..3 visit disjoint horizontal/vertical pairs; one invocation
|
||||
// owns both outputs. Ordinary passes consume the plan, never a freshly converted material.
|
||||
layout(std430, set = 0, binding = 28) readonly buffer RHead { uint rhead[]; }; // [a * n + b] = (offset << 8) | count
|
||||
layout(std430, set = 0, binding = 23) buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; ivec4 burnN; ivec4 burnEv[256]; ivec4 reactN; ivec4 reactEv[64]; } ctrl; // reactEv: entities to spawn (x, y, entity index + 1)
|
||||
layout(std430, set = 0, binding = 29) readonly buffer RRec { uint rrec[]; }; // 5 words per entry: outs (a | b << 16, 0xFFFF = same), probability / 65536
|
||||
|
||||
void main() {
|
||||
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||
if (p.x >= pc.gw || p.y >= pc.gh) return;
|
||||
int ph = pc.pass_ & 3;
|
||||
ivec2 q;
|
||||
if (ph < 2) q = p + ivec2(((p.x & 1) == ph) ? 1 : -1, 0);
|
||||
else q = p + ivec2(0, ((p.y & 1) == (ph - 2)) ? 1 : -1);
|
||||
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
|
||||
int i = p.y * pc.gw + p.x, j = q.y * pc.gw + q.x;
|
||||
uint ci = cells[i], cj = cells[j];
|
||||
uint mi = cell_mat(ci), mj = cell_mat(cj);
|
||||
if (mi == 0u && mj == 0u) return;
|
||||
uint n = mats[mats.length() - 1u].w;
|
||||
uint h = rhead[mi * n + mj];
|
||||
uint cnt = h & 255u;
|
||||
if (cnt == 0u) return;
|
||||
uint lo = uint(min(i, j)), hi = uint(max(i, j));
|
||||
uint rr = hash3(lo, hi, uint(pc.frame * 4 + ph) ^ 0x51u);
|
||||
uint k = 0u; // the record of the pair table: R % count with the roll R that also gates the attempt (0070eaf0)
|
||||
// 0070eaf0: every cell that has a reaction threshold (CellData + 0x22c: the largest probability of a reaction it is the first input of; air is no cell and never starts one) tries once per
|
||||
// update with ONE random neighbour of four, rolls R (0..10000) against min(that threshold, the record's probability) and the record must accept the direction: the pair gets the chance of
|
||||
// each of its two cells separately (rec / 4 for a material next to air, not rec / 2).
|
||||
{
|
||||
const bool iLo = i < j;
|
||||
const uint mA = iLo ? mi : mj, mB = iLo ? mj : mi;
|
||||
const ivec2 pA = iLo ? p : q, pB = iLo ? q : p;
|
||||
const uint hA = rhead[mA * n + mB], hB = rhead[mB * n + mA];
|
||||
const uint eA = ((hA >> 8) + k % max(hA & 255u, 1u)) * 5u, eB = ((hB >> 8) + k % max(hB & 255u, 1u)) * 5u;
|
||||
const uint pwA = rrec[eA + 1u], pwB = rrec[eB + 1u];
|
||||
const ivec2 dAB = pB - pA;
|
||||
const uint relA = dAB.y < 0 ? 1u : dAB.y > 0 ? 2u : dAB.x < 0 ? 3u : 4u, relB = relA == 1u ? 2u : relA == 2u ? 1u : relA == 3u ? 4u : 3u;
|
||||
const uint dirA = (pwA >> 16) & 7u, dirB = (pwB >> 16) & 7u;
|
||||
// each cell has its own roll R = next15 % 10001: the material threshold (CellData + 0x22c, positive) and the record's threshold both accept R <= threshold, the record is R % count
|
||||
const uint RA = (hash(rr ^ 0xA2u) & 0x7FFFu) % 10001u, RB = (hash(rr ^ 0xB2u) & 0x7FFFu) % 10001u;
|
||||
const uint kA = RA % max(hA & 255u, 1u), kB = RB % max(hB & 255u, 1u);
|
||||
const uint eA2 = ((hA >> 8) + kA) * 5u, eB2 = ((hB >> 8) + kB) * 5u;
|
||||
const uint matA = rhead[n * n + mA], matB = rhead[n * n + mB];
|
||||
const bool fireA = mA != 0u && (hA & 255u) != 0u && matA > 0u && RA <= matA && RA <= (rrec[eA2 + 1u] & 0xFFFFu) && (((rrec[eA2 + 1u] >> 16) & 7u) == 0u || ((rrec[eA2 + 1u] >> 16) & 7u) == relA) && (hash(rr ^ 0xA1u) & 3u) == 0u;
|
||||
const bool fireB = mB != 0u && (hB & 255u) != 0u && matB > 0u && RB <= matB && RB <= (rrec[eB2 + 1u] & 0xFFFFu) && (((rrec[eB2 + 1u] >> 16) & 7u) == 0u || ((rrec[eB2 + 1u] >> 16) & 7u) == relB) && (hash(rr ^ 0xB1u) & 3u) == 0u;
|
||||
if (!fireA && !fireB) return;
|
||||
k = fireA ? kA : kB;
|
||||
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
|
||||
#include "biome_mods.glsl"
|
||||
const ivec2 RD4[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1));
|
||||
uint canonical(uint m) { const uint n = mats[mats.length() - 1u].w; return rhead[2u * n * (n + 1u) + m]; }
|
||||
uint draw15(uint seed, uint salt) { return hash(seed ^ salt) & 0x7FFFu; }
|
||||
// One plan per source/update, stored in movement scratch after movement completes.
|
||||
// y: 0 idle, 1 combined, 2 fast, 3 material change. z: shared R, w: neighbour / target.
|
||||
void plan_reaction(ivec2 p, int i) {
|
||||
const uint c = cells[i], m = cell_mat(c), f = mflags(m);
|
||||
const uint n = mats[mats.length() - 1u].w, stride = n * (n + 1u);
|
||||
const uint seed = hash3(uint(p.x + pc.view_x), uint(p.y + pc.view_y), uint(pc.frame) ^ 0x7C6E0u);
|
||||
uint route = 2u;
|
||||
// Worker +0x460 forces a full/combined update after ten ordinary updates.
|
||||
// Initial loaded cells also receive the initial full update.
|
||||
const bool full = (pc.frame % 11) == 0;
|
||||
const bool passive = (f & F_STATIC) != 0u || ((f & F_POWDER) != 0u && (c & ATTACH_BIT) != 0u);
|
||||
const bool slowBranch = full && (!passive || draw15(seed, 1u) % 101u < 20u);
|
||||
if (slowBranch) route = !passive || draw15(seed, 2u) % 101u < 50u ? 1u : 0u;
|
||||
if (m == 0u || (!full && !passive && (f & F_LIQCELL) != 0u && liquid_state(uint(i), m) == 3u && (terrain_age[i] & 255u) > 49u)) route = 0u;
|
||||
const uint R = draw15(seed, 3u) % 10001u, dir = draw15(seed, 4u) & 3u;
|
||||
intent[i] = uvec4(c, route, R, dir);
|
||||
if (route == 1u && (c & BURN_BIT) == 0u) {
|
||||
const uint bb = bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y)));
|
||||
const uint fc = (bb >> 16) & 255u, uc = bb >> 24;
|
||||
if ((fc | uc) != 0u) {
|
||||
const uvec2 cv = bm_conversions(m);
|
||||
const bool cold = fc != 0u && (cv.x & 65535u) != 0u && draw15(seed, 5u) % ((cv.x >> 16) + 1u) < fc && liquid_freeze_ready(p, c);
|
||||
const bool warm = uc != 0u && (cv.y & 65535u) != 0u && draw15(seed, 6u) % ((cv.y >> 16) + 1u) < uc;
|
||||
if (cold || warm) {
|
||||
// A successful cold flag owns the attempt, including a blocked contact.
|
||||
intent[i].y = 0u;
|
||||
const ivec2 q = p + RD4[dir];
|
||||
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
|
||||
const uint nm = cell_mat(cells[q.y * pc.gw + q.x]);
|
||||
const bool allow = cold ? nm == 0u || (mflags(nm) & F_LIQCELL) == 0u || (mflags(nm) & F_POWDER) != 0u : canonical(nm) != canonical(m);
|
||||
if (allow) { intent[i].y = 3u; intent[i].w = ((cold ? cv.x : cv.y) & 65535u) - 1u; }
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint e = ((h >> 8) + k % cnt) * 5u;
|
||||
// 0070ad30 is reached only on the passive full-update tail, after neither
|
||||
// reaction method returned. It is not unconditional terrain cleanup.
|
||||
if (full && passive && route != 1u && rhead[stride + n * n + m] == 0u && (c & BURN_BIT) == 0u && p.x > 0 && p.y > 0 && p.x + 1 < pc.gw && p.y + 1 < pc.gh) {
|
||||
if (cell_mat(cells[i-1]) == 0u && cell_mat(cells[i+1]) == 0u && cell_mat(cells[i-pc.gw]) == 0u && cell_mat(cells[i+pc.gw]) == 0u) { intent[i].y = 3u; intent[i].w = 0u; }
|
||||
}
|
||||
}
|
||||
|
||||
void apply_side(ivec2 p, ivec2 q, int i, int j, uint ci, uint cj, uint mi, uint mj, uint e, uint rr) {
|
||||
uint outs = rrec[e];
|
||||
uint third = rrec[e + 2u];
|
||||
uint blob = rrec[e + 3u];
|
||||
@@ -60,31 +65,13 @@ void main() {
|
||||
bool second = (mi == mj) && (i > j); // equal materials: the lower cell takes the first output
|
||||
uint o = second ? (outs >> 16) : (outs & 0xFFFFu);
|
||||
const bool firstIsI0 = (rrec[e + 4u] & 0x80000000u) == 0u; // (the record is the mirrored one when the first input is the second material of the pair)
|
||||
const uint reqLife = blob >> 20;
|
||||
if (reqLife > 0u && birth_age(birth[firstIsI0 ? i : j]) < reqLife) return; // req_lifetime: the first cell must be old enough
|
||||
if (((blob >> 19) & 1u) != 0u && firstIsI0 && !second) { // destroy_horizontally_lonely_pixels: the first output becomes air when both horizontal neighbours are empty (both probed)
|
||||
const bool le = p.x > 0 && cell_mat(cells[i - 1]) == 0u, ri = p.x + 1 < pc.gw && cell_mat(cells[i + 1]) == 0u;
|
||||
if (le && ri) o = 0u;
|
||||
}
|
||||
// three inputs (0070ebf0): a third cell next to the first input's cell (a cardinal neighbour chosen at random; the second cell's place is mirrored to the other side)
|
||||
// must hold the third material; the thread of the first cell writes its output
|
||||
if ((third & 0xFFFFu) != 0xFFFFu) {
|
||||
const bool firstIsI = firstIsI0;
|
||||
const ivec2 pf = firstIsI ? p : q, ps = firstIsI ? q : p;
|
||||
const ivec2 dirs[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1));
|
||||
ivec2 p3 = pf + dirs[(rr >> 20) & 3u];
|
||||
if (p3 == ps) p3 = 2 * pf - ps;
|
||||
if (p3.x < 0 || p3.y < 0 || p3.x >= pc.gw || p3.y >= pc.gh) return;
|
||||
const int k3 = p3.y * pc.gw + p3.x;
|
||||
const uint c3 = cells[k3];
|
||||
if (cell_mat(c3) != (third & 0xFFFFu)) return;
|
||||
const uint o3 = (third >> 16) & 0xFFFFu;
|
||||
if (firstIsI && o3 != 0xFFFFu && o3 != cell_mat(c3)) { atomicCompSwap(cells[k3], c3, o3 | (c3 & 0x00FF0000u)); birth[k3] = birth_frame(); }
|
||||
}
|
||||
if (o == 0xFFFFu || o == mi) return; // this side stays as it is
|
||||
cells[i] = o | (ci & 0x00FF0000u); // keeps the colour variation, clears burning and direction bits
|
||||
ext[i] = vec2(0.0);
|
||||
birth[i] = birth_frame();
|
||||
material_changed(i, ci, o);
|
||||
if (entityIdx != 0u && firstIsI0 && !second) { // the reaction loads an entity at the position of the first input (the host spawns it)
|
||||
const int ei = atomicAdd(ctrl.reactN.x, 1);
|
||||
if (ei < 64) ctrl.reactEv[ei] = ivec4(p.x + pc.view_x, p.y + pc.view_y, int(entityIdx), 0);
|
||||
@@ -100,14 +87,16 @@ void main() {
|
||||
for (int ray = 0; ray < nr; ++ray) {
|
||||
const float a = float(ray) * 6.2831853 / float(nr);
|
||||
const vec2 dir = vec2(cos(a), sin(a));
|
||||
vec2 samplePos = vec2(p + ivec2(pc.view_x, pc.view_y));
|
||||
for (int st = 1; st <= int(rad); ++st) {
|
||||
const ivec2 t = p + ivec2(trunc(dir * float(st)));
|
||||
samplePos += dir;
|
||||
const ivec2 t = ivec2(samplePos) - ivec2(pc.view_x, pc.view_y);
|
||||
if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) break;
|
||||
const int ti = t.y * pc.gw + t.x;
|
||||
const uint tc = cells[ti], tm = cell_mat(tc);
|
||||
if (tm == o) continue;
|
||||
if (tm != mi) break;
|
||||
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); } // a converted cell starts afresh (0070d0b0)
|
||||
if (canonical(tm) == canonical(o)) continue;
|
||||
if (canonical(tm) != canonical(mi)) break;
|
||||
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { material_changed(ti, tc, o); } // same-class movement state survives (0070d0b0)
|
||||
}
|
||||
}
|
||||
} else if (rad > 0u) { // direct mode (0071ab50, restriction explicitly off): every loaded cell of the disk dx^2 + dy^2 < r^2 takes the output, whatever it holds
|
||||
@@ -120,8 +109,83 @@ void main() {
|
||||
const int ti = t.y * pc.gw + t.x;
|
||||
const uint tc = cells[ti];
|
||||
if (cell_mat(tc) == o) continue;
|
||||
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); }
|
||||
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { material_changed(ti, tc, o); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||
if (p.x >= pc.gw || p.y >= pc.gh) return;
|
||||
const int i = p.y * pc.gw + p.x;
|
||||
if (pc.pass_ == 4) { plan_reaction(p, i); return; }
|
||||
if (pc.pass_ == 5) {
|
||||
if (intent[i].y == 3u && cells[i] == intent[i].x) {
|
||||
cells[i] = intent[i].w | (intent[i].x & 0xFF0000u);
|
||||
material_changed(i, intent[i].x, intent[i].w);
|
||||
}
|
||||
return;
|
||||
}
|
||||
int ph = pc.pass_ & 3;
|
||||
ivec2 q;
|
||||
if (ph < 2) q = p + ivec2(((p.x & 1) == ph) ? 1 : -1, 0);
|
||||
else q = p + ivec2(0, ((p.y & 1) == (ph - 2)) ? 1 : -1);
|
||||
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
|
||||
const int j = q.y * pc.gw + q.x;
|
||||
if (i > j) return; // One invocation owns both outputs; no half-reaction race.
|
||||
uint ci = cells[i], cj = cells[j];
|
||||
// A conversion must not start a second reaction as its new material this tick.
|
||||
if (ci != intent[i].x || cj != intent[j].x) return;
|
||||
uint mi = cell_mat(ci), mj = cell_mat(cj);
|
||||
if (mi == 0u && mj == 0u) return;
|
||||
const uint n = mats[mats.length() - 1u].w, stride = n * (n + 1u);
|
||||
const int a = min(i, j), b = max(i, j);
|
||||
const ivec2 pa = i < j ? p : q, pb = i < j ? q : p;
|
||||
const uint ma = cell_mat(intent[a].x), mb = cell_mat(intent[b].x);
|
||||
uint selectedBase = 0u, k = 0u;
|
||||
bool found = false;
|
||||
for (int side = 0; side < 2; ++side) {
|
||||
const int src = side == 0 ? a : b;
|
||||
const uint route = intent[src].y;
|
||||
if (route != 1u && route != 2u) continue;
|
||||
const ivec2 delta = side == 0 ? pb - pa : pa - pb;
|
||||
if (RD4[intent[src].w] != delta) continue;
|
||||
const uint sm = side == 0 ? ma : mb, tm = side == 0 ? mb : ma;
|
||||
const uint base = route == 2u ? stride : 0u;
|
||||
const uint h = rhead[base + sm * n + tm], cnt = h & 255u, R = intent[src].z;
|
||||
if (sm == 0u || cnt == 0u || rhead[base + n * n + sm] == 0u || R > rhead[base + n * n + sm]) continue;
|
||||
const uint candidate = R % cnt, pw = rrec[((h >> 8) + candidate) * 5u + 1u];
|
||||
const uint rel = delta.y < 0 ? 1u : delta.y > 0 ? 2u : delta.x < 0 ? 3u : 4u;
|
||||
if (R > (pw & 65535u) || (((pw >> 16) & 7u) != 0u && ((pw >> 16) & 7u) != rel)) continue;
|
||||
selectedBase = base; k = candidate; found = true; break;
|
||||
}
|
||||
if (!found) return;
|
||||
const uint h = rhead[selectedBase + mi * n + mj], cnt = h & 255u;
|
||||
if (cnt == 0u) return;
|
||||
const uint rr = hash3(uint(a), uint(b), uint(pc.frame) ^ 0x51u);
|
||||
uint e = ((h >> 8) + k % cnt) * 5u;
|
||||
const uint third = rrec[e + 2u];
|
||||
const bool firstIsI0 = (rrec[e + 4u] & 0x80000000u) == 0u;
|
||||
const uint reqLife = rrec[e + 3u] >> 20;
|
||||
if (reqLife > 0u && birth_age(birth[firstIsI0 ? i : j]) < reqLife) return;
|
||||
// three inputs (0070ebf0): a third cell next to the first input's cell (a cardinal neighbour chosen at random; the second cell's place is mirrored to the other side)
|
||||
// must hold the third material; the thread of the first cell writes its output
|
||||
if ((third & 0xFFFFu) != 0xFFFFu) {
|
||||
const bool firstIsI = firstIsI0;
|
||||
const ivec2 pf = firstIsI ? p : q, ps = firstIsI ? q : p;
|
||||
const ivec2 dirs[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1));
|
||||
ivec2 p3 = pf + dirs[(rr >> 20) & 3u];
|
||||
if (p3 == ps) p3 = 2 * pf - ps;
|
||||
if (p3.x < 0 || p3.y < 0 || p3.x >= pc.gw || p3.y >= pc.gh) return;
|
||||
const int k3 = p3.y * pc.gw + p3.x;
|
||||
const uint c3 = cells[k3];
|
||||
if (canonical(cell_mat(c3)) != canonical(third & 0xFFFFu)) return;
|
||||
const uint o3 = (third >> 16) & 0xFFFFu;
|
||||
if (o3 != 0xFFFFu && o3 != cell_mat(c3)) { if (atomicCompSwap(cells[k3], c3, o3 | (c3 & 0x00FF0000u)) == c3) material_changed(k3, c3, o3); }
|
||||
}
|
||||
apply_side(p, q, i, j, ci, cj, mi, mj, e, rr);
|
||||
const uint reverseHead = rhead[selectedBase + mj * n + mi];
|
||||
if ((reverseHead & 255u) != 0u)
|
||||
apply_side(q, p, j, i, cj, ci, mj, mi, ((reverseHead >> 8) + k % (reverseHead & 255u)) * 5u, rr);
|
||||
}
|
||||
@@ -20,6 +20,53 @@ layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; /
|
||||
uint flame_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m]; return v != 0u ? v - 1u : dflt; }
|
||||
uint smoke_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m + 1u]; return v != 0u ? v - 1u : dflt; }
|
||||
|
||||
#include "biome_mods.glsl"
|
||||
layout(std430, set = 0, binding = 28) readonly buffer RHead { uint rhead[]; };
|
||||
layout(std430, set = 0, binding = 29) readonly buffer RRec { uint rrec[]; };
|
||||
const ivec2 TD4[4] = ivec2[4](ivec2(0,-1), ivec2(-1,0), ivec2(1,0), ivec2(0,1));
|
||||
uint native_material(uint m) { const uint n = mats[mats.length()-1u].w; return rhead[2u*n*(n+1u)+m]; }
|
||||
uint body_neighbor(ivec2 q, Body B, int s, vec2 com, float cs, float sn, uint m, uint base) {
|
||||
const ivec2 grid = q - ivec2(pc.view_x, pc.view_y);
|
||||
if (grid.x < 0 || grid.y < 0 || grid.x >= pc.gw || grid.y >= pc.gh) return NONE;
|
||||
const ivec2 local = ivec2(floor(rot2(cs, -sn, vec2(q) + 0.5 - B.p.xy) + com));
|
||||
if (shape_solid(s, local.x, local.y) && (burn[base + uint(local.y*shi(s,0)+local.x)] & BURNT) == 0u) return m;
|
||||
return cell_mat(cells[grid.y*pc.gw+grid.x]);
|
||||
}
|
||||
// Solid-cell 0070e060 calls the combined/fast paths when body record +0x2e
|
||||
// permits them. Ice glass must keep that path after leaving the terrain grid.
|
||||
int solid_conversion(ivec2 wc, Body B, int s, vec2 com, float cs, float sn, uint m, uint base, uint seed) {
|
||||
if ((B.i.z & 65536) != 0) return -1; // native collapse record disables reactions
|
||||
const uint r = hash3(seed, uint(pc.frame), 40u) & 32767u;
|
||||
const uint R = r % 10001u;
|
||||
const uint dir = hash3(seed, uint(pc.frame), 41u) & 3u;
|
||||
const ivec2 q = wc + TD4[dir];
|
||||
const uint nm = body_neighbor(q, B, s, com, cs, sn, m, base);
|
||||
if (nm == NONE) return -1;
|
||||
const bool full = (pc.frame % 11) == 0;
|
||||
if (full) {
|
||||
const uint bb = bm_bytes(vec2(wc));
|
||||
const uint fc = (bb >> 16) & 255u, uc = bb >> 24;
|
||||
const uvec2 cv = bm_conversions(m);
|
||||
const bool cold = fc != 0u && (cv.x & 65535u) != 0u && (hash3(seed,uint(pc.frame),42u)&32767u) % ((cv.x>>16)+1u) < fc;
|
||||
const bool warm = uc != 0u && (cv.y & 65535u) != 0u && (hash3(seed,uint(pc.frame),43u)&32767u) % ((cv.y>>16)+1u) < uc;
|
||||
if (cold) return nm == 0u || (mflags(nm)&F_LIQCELL) == 0u || (mflags(nm)&F_POWDER) != 0u ? int((cv.x&65535u)-1u) : -1;
|
||||
if (warm) return native_material(nm) != native_material(m) ? int((cv.y&65535u)-1u) : -1;
|
||||
}
|
||||
const uint n = mats[mats.length()-1u].w, table = full ? 0u : n*(n+1u);
|
||||
const uint h = rhead[table + m*n+nm], count = h & 255u;
|
||||
if (count == 0u || rhead[table+n*n+m] == 0u || R > rhead[table+n*n+m]) return -1;
|
||||
const uint e = ((h>>8) + R%count)*5u, pw = rrec[e+1u];
|
||||
const uint requiredDir = (pw>>16)&7u, actualDir = dir == 0u ? 1u : dir == 1u ? 3u : dir == 2u ? 4u : 2u;
|
||||
if (R > (pw&65535u) || (requiredDir != 0u && requiredDir != actualDir)) return -1;
|
||||
// Complex sprite reactions need per-pixel material storage; leave those to
|
||||
// the existing fracture path rather than applying only half their outputs.
|
||||
if ((rrec[e+2u]&65535u) != 65535u || (rrec[e+3u]&65535u) != 0u || (rrec[e+4u]&0x7FFFFFFFu) != 0u) return -1;
|
||||
const uint target = rrec[e]&65535u, other = rrec[e]>>16;
|
||||
if (target == 65535u || native_material(target) == native_material(m)) return -1;
|
||||
if (other != 65535u && native_material(other) != native_material(nm)) return -1;
|
||||
return int(target);
|
||||
}
|
||||
|
||||
void main() {
|
||||
int bi = int(gl_WorkGroupID.x);
|
||||
if (bi >= bhdr.x) return;
|
||||
@@ -29,6 +76,13 @@ void main() {
|
||||
if (matp1 == 0u) return;
|
||||
const uint m = matp1 - 1u;
|
||||
const uint mz = mats[3u * m + 2u].z, mw = mats[3u * m + 2u].w;
|
||||
// Most rigid materials have no fast reactions. Avoid visiting their pixels
|
||||
// on the ten ordinary updates between combined updates.
|
||||
if (((mz >> 16) & 1u) == 0u) {
|
||||
if ((B.i.z & 65536) != 0) return;
|
||||
const uint n = mats[mats.length()-1u].w;
|
||||
if ((pc.frame % 11) != 0 && rhead[n*(n+1u)+n*n+m] == 0u) return;
|
||||
}
|
||||
const int s = B.i.x;
|
||||
const int w = shi(s, 0), h = shi(s, 1);
|
||||
if (w * h > 4096) return;
|
||||
@@ -50,6 +104,24 @@ void main() {
|
||||
const ivec2 wc = ivec2(floor(wp));
|
||||
const uint seed = uint(bi) * 4099u + uint(px);
|
||||
if ((st & BURNING) == 0u) {
|
||||
const int target = solid_conversion(wc, B, s, com, cs, sn, m, base, seed);
|
||||
if (target >= 0 && (mflags(uint(target)) & (F_LIQCELL | F_GAS | F_FIRE | F_AIR)) != 0u) {
|
||||
const ivec2 grid = wc - ivec2(pc.view_x, pc.view_y);
|
||||
if (grid.x >= 0 && grid.y >= 0 && grid.x < pc.gw && grid.y < pc.gh) {
|
||||
const int index = grid.y*pc.gw+grid.x;
|
||||
const int ei = atomicAdd(ctrl.burnN.x, 1);
|
||||
if (ei < 256) {
|
||||
if (atomicCompSwap(cells[index], 0u, uint(target)) == 0u) {
|
||||
material_changed(index, 0u, uint(target));
|
||||
ctrl.burnEv[ei] = ivec4(bi, px, target+1, 0);
|
||||
burn[base+uint(px)] = BURNT;
|
||||
continue;
|
||||
}
|
||||
ctrl.burnEv[ei] = ivec4(-1);
|
||||
} else atomicAdd(ctrl.burnN.x, -1);
|
||||
}
|
||||
}
|
||||
if (((mz >> 16) & 1u) == 0u) continue;
|
||||
// fire sources in the pixel's cell and the four around it (cells are free under a body)
|
||||
float keep = 1.0, bestFade = -1.0;
|
||||
bool wet = false;
|
||||
|
||||
+21
-1
@@ -69,8 +69,8 @@ void main() {
|
||||
if (e.st.x != 0 && (fc & 7) >= 3) gFlash = (fc & 8) != 0 ? 0xFF0000u : 0xFFFFFFu;
|
||||
}
|
||||
gFade = (e.st.x == 1 || e.st.x == 2 || e.st.x == 3) ? 0.0 : clamp(e.ex10.w, 0.0, 1.0); // INVISIBILITY active (a prop's ex8.xy is the hit point of its leak) (00b015d0 case 0x51: its sprites are told not to draw)
|
||||
{ vec2 pv = prevpos[h0.x].xy; if (distance(pv, e.a.xy) < 10.0) e.a.xy = mix(pv, e.a.xy, ctrl.view.z); } // drawn between the last two ticks
|
||||
const bool rigid = (e.st.w & (32 | 64)) != 0; // drawn rotated by the body angle in a.z (64: the part of a Verlet chain, angle in ex7.x)
|
||||
if (!rigid || (e.st.w & 64) != 0) { vec2 pv = prevpos[h0.x].xy; if (distance(pv, e.a.xy) < 10.0) e.a.xy = mix(pv, e.a.xy, ctrl.view.z); }
|
||||
bool mobile = e.st.x != 0 && !rigid;
|
||||
bool moving = mobile && abs(e.a.z) > 0.08;
|
||||
ivec4 an = moving ? hw : hi; // tex, w, h, frames | wait << 16
|
||||
@@ -88,6 +88,26 @@ void main() {
|
||||
for (int k = int(gl_LocalInvocationID.x); k < min(bhdr.x, 1024); k += 64) if (bodies[k].i.y == int(h0.x) && (bodies[k].i.z & 2) == 0) sBody = k;
|
||||
barrier();
|
||||
const int body = (e.st.w & 64) == 0 && w * h <= 4096 ? sBody : -1;
|
||||
if ((e.st.w & 64) == 0 && sBody >= 0) {
|
||||
// Interpolate one rigid pose. Mixing the old origin with the current
|
||||
// angle moves the image away from its collision body, particularly
|
||||
// when the sprite anchor is far from the centre of mass.
|
||||
const vec4 previous = prevpos[h0.x];
|
||||
const vec2 centre = bodies[sBody].p.xy;
|
||||
const float ca = cos(e.a.z), sa = sin(e.a.z);
|
||||
const vec2 offset = e.a.xy - centre;
|
||||
const vec2 local = vec2(ca * offset.x + sa * offset.y, -sa * offset.x + ca * offset.y);
|
||||
const float cp = cos(previous.z), sp = sin(previous.z);
|
||||
const vec2 previousCentre = previous.xy - vec2(cp * local.x - sp * local.y, sp * local.x + cp * local.y);
|
||||
// The solver clamps translation to 20 pixels/tick. Larger changes
|
||||
// are teleports and should be drawn immediately.
|
||||
if (distance(previousCentre, centre) <= 20.01) {
|
||||
const float delta = atan(sin(e.a.z - previous.z), cos(e.a.z - previous.z));
|
||||
e.a.z = previous.z + delta * ctrl.view.z;
|
||||
const float ci = cos(e.a.z), si = sin(e.a.z);
|
||||
e.a.xy = mix(previousCentre, centre, ctrl.view.z) + vec2(ci * local.x - si * local.y, si * local.x + ci * local.y);
|
||||
}
|
||||
}
|
||||
int rf = (pc.frame / wait + int(h0.x)) % max(frames, 1);
|
||||
const float ang = (e.st.w & 64) != 0 ? e.ex7.x : e.a.z;
|
||||
const float sx = (e.st.w & 64) != 0 && e.ex7.y > 0.0 ? e.ex7.y : 1.0; // a part stretched along its length (the segments of a limb: IKLimbComponent)
|
||||
|
||||
@@ -16,6 +16,7 @@ layout(std430, set = 0, binding = 10) readonly buffer CellsOld { uint cells_old[
|
||||
layout(std430, set = 0, binding = 11) readonly buffer ExtOld { vec2 ext_old[]; };
|
||||
layout(std430, set = 0, binding = 52) readonly buffer BirthOld { uint birth_old[]; }; // the ages before the scroll
|
||||
|
||||
layout(std430, set = 0, binding = 66) readonly buffer TerrainAgeOld { uint terrain_age_old[]; };
|
||||
layout(std430, set = 0, binding = 12) readonly buffer Scene { uint scene[]; }; // pixel scenes: 0 = none, else material + 1
|
||||
|
||||
layout(std430, set = 0, binding = 18) readonly buffer Override { uint ovr[]; }; // cells restored from the persistent store (0xFFFFFFFF = none)
|
||||
@@ -428,6 +429,7 @@ void main() {
|
||||
if (pc.shift_mode == 1) { // scrolling: reuse the simulated overlap, generate only the exposed strips
|
||||
ivec2 src = p + ivec2(pc.shift_x, pc.shift_y);
|
||||
if (src.x >= 0 && src.y >= 0 && src.x < pc.gw && src.y < pc.gh) {
|
||||
terrain_age[i] = terrain_age_old[src.y * pc.gw + src.x];
|
||||
cells[i] = cells_old[src.y * pc.gw + src.x];
|
||||
ext[i] = ext_old[src.y * pc.gw + src.x];
|
||||
birth[i] = birth_old[src.y * pc.gw + src.x]; // a cell that stays keeps its age (0070d1e0: the age belongs to the cell)
|
||||
@@ -435,6 +437,7 @@ void main() {
|
||||
}
|
||||
}
|
||||
birth[i] = birth_frame();
|
||||
terrain_age[i] = 0u;
|
||||
if (ovr[i] != 0xFFFFFFFFu) { // a cell that comes back from the store (only its word is kept): a burning cell or a flame gets the fade a new cell of its material has
|
||||
// (0070f450: the material's temperature_of_fire), so a fire that went out of the window goes on when it comes back instead of dying at its next update
|
||||
const uint oc = ovr[i];
|
||||
|
||||
Reference in new issue
Block a user