267 lines
15 KiB
Plaintext
267 lines
15 KiB
Plaintext
#version 450
|
|
#extension GL_GOOGLE_include_directive : require
|
|
layout(local_size_x = 8, local_size_y = 8) in;
|
|
#include "common.glsl"
|
|
|
|
// Movement intent. Every mobile cell decides at most one move per pass following the
|
|
// original engine's rules (noita.exe CLiquidCell / CGasCell / CFireCell updates, see
|
|
// docs/ENGINE_ANALYSIS.md). Conflicts are resolved afterwards by claim arbitration, then
|
|
// apply.comp performs the winning swaps. Several passes per frame stand in for the
|
|
// original's multi-cell steps.
|
|
|
|
uint rnd(uint i, uint salt) { return hash3(i, uint(pc.frame) * 16u + uint(pc.pass_), salt + uint(pc.seed)); }
|
|
int rand100(uint i, uint salt) { return int(rnd(i, salt) % 101u); } // rand() % 101
|
|
float rand01(uint i, uint salt) { return float(rnd(i, salt) & 0x7FFFu) / 32767.0; }
|
|
|
|
bool in_bounds(ivec2 p) { return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh; }
|
|
uint idx(ivec2 p) { return uint(p.y * pc.gw + p.x); }
|
|
bool is_air(uint m) { return (mflags(m) & F_AIR) != 0u; }
|
|
|
|
// FUN_0070b200: may a liquid/powder `mm` enter a cell holding material `tm`?
|
|
bool can_enter_liquid(uint mm, uint tm, bool roll25) {
|
|
uint tf = mflags(tm);
|
|
if ((tf & F_AIR) != 0u) return true;
|
|
if ((tf & (F_GAS | F_FIRE)) != 0u) return true; // gas and fire are always displaced
|
|
if ((tf & F_STATIC) != 0u) return false; // solids are never entered
|
|
if (!roll25) return false; // dense-through-light swaps only 25% of the time
|
|
if ((mflags(mm) & F_SAND) != 0u && (tf & F_SAND) != 0u) return false; // powder never through powder
|
|
return mdensity(tm) < mdensity(mm);
|
|
}
|
|
|
|
// FUN_0070a5a0: may a gas `mm` enter a cell holding material `tm`?
|
|
bool can_rise(uint mm, uint tm, bool roll25) {
|
|
uint tf = mflags(tm);
|
|
if ((tf & F_AIR) != 0u) return true;
|
|
if ((tf & (F_STATIC | F_LIQUID)) != 0u) return false;
|
|
if (tm == mm) return false;
|
|
return roll25 && mdensity(tm) > mdensity(mm);
|
|
}
|
|
|
|
void main() {
|
|
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
|
if (p.x >= pc.gw || p.y >= pc.gh) return;
|
|
uint i = idx(p);
|
|
intent[i] = uvec4(NONE, 0u, 0u, 0u);
|
|
|
|
uint c = cells[i];
|
|
uint m = cell_mat(c);
|
|
uint f = mflags(m);
|
|
if ((f & (F_AIR | F_STATIC)) != 0u) return;
|
|
|
|
vec2 e = ext[i];
|
|
float vy = e.x, vx = e.y;
|
|
float nvy = vy, nvx = vx;
|
|
uint target = NONE;
|
|
uint flip = 0u;
|
|
uint prio = ((hash3(i, uint(pc.frame) * 16u + uint(pc.pass_), 777u + uint(pc.seed)) & 0xFFFu) << 20) | i; // unique per cell (12 random bits, the cell index in 20)
|
|
bool roll25 = rand100(i, 11u) > 74;
|
|
const int P = pc.pass_;
|
|
|
|
if ((f & F_LIQUID) != 0u) {
|
|
const bool sand = (f & F_SAND) != 0u;
|
|
{ // 0070c6e0: an active liquid / powder with a positive lifetime disappears when its age (frames since it got its material) reaches it
|
|
const int lt = mlifetime(m);
|
|
if (lt > 0 && int(birth_age(birth[i])) >= lt) { cells[i] = 0u; ext[i] = vec2(0.0); return; }
|
|
}
|
|
// Powders with liquid_sticks_to_ceiling (soil, mud, moss...): 0070b2d0 attaches them to the cell on one side / above before they would fall, 0070c6e0 releases them again.
|
|
const uint ceilP = sand ? mceiling(m) : 0u;
|
|
if (ceilP > 0u) {
|
|
const ivec2 W = p + ivec2(pc.view_x, pc.view_y);
|
|
if ((c & ATTACH_BIT) != 0u) { // passive: never moves by itself
|
|
if (P == 0) {
|
|
const uint r1 = rnd(i, 91u) % 101u;
|
|
if (r1 < 20u) { // ceiling check
|
|
const uint st = (c >> 24) & 3u;
|
|
const ivec2 sp2 = p + (st == 1u ? ivec2(1, 0) : st == 3u ? ivec2(-1, 0) : ivec2(0, -1));
|
|
bool support = false;
|
|
if (in_bounds(sp2)) {
|
|
const uint nc = cells[idx(sp2)], nm = cell_mat(nc);
|
|
support = nm != 0u && ((mflags(nm) & F_STATIC) != 0u || ((mflags(nm) & F_SAND) != 0u && (nc & ATTACH_BIT) != 0u));
|
|
}
|
|
bool release = !support;
|
|
if (support) {
|
|
const uint r2 = rnd(i, 92u) % 101u;
|
|
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));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
// attachment: one location chosen by a hash of the coordinates (0070acc0): even rows give 7 + u % 3 (7 = left, else above), odd rows 5 - u % 3 (5 = right, else above)
|
|
{
|
|
int u = W.x + 0x67577458 + 91 * W.y;
|
|
u = int((uint((u >> 13) ^ u)) * 214013u + 2531011u);
|
|
u = ((u >> ((W.x & 15) + 1)) ^ u) & 32767;
|
|
const int code = (W.y & 1) == 0 ? 7 + u % 3 : 5 - u % 3;
|
|
const ivec2 ap = p + (code == 5 ? ivec2(1, 0) : code == 7 ? ivec2(-1, 0) : ivec2(0, -1));
|
|
if (in_bounds(ap)) {
|
|
const uint nc = cells[idx(ap)], nm = cell_mat(nc);
|
|
if (nm != 0u && ((mflags(nm) & F_STATIC) != 0u || ((mflags(nm) & F_SAND) != 0u && (nc & ATTACH_BIT) != 0u))) {
|
|
cells[i] = (c & ~(3u << 24)) | ATTACH_BIT | ((code == 5 ? 1u : code == 7 ? 3u : 2u) << 24);
|
|
ext[i] = vec2(0.0);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const float g = mgravity(m);
|
|
const bool falling = vy > 0.0;
|
|
// powders that are already falling skip a frame with probability (1 - gravity) * 0.5
|
|
if (sand && falling && rand01(i, 3u) < (1.0 - g) * 0.5) return;
|
|
|
|
const int extra = vy > 1.0 ? int(ceil((vy - 1.0) / 2.0)) : 0; // extra cells per frame from accumulated velocity
|
|
const bool downAllowed = (P == 0) || (P <= extra);
|
|
|
|
// drift of extra fall steps: 1/8 left, 1/8 right, else straight; liquids follow vx when it is large
|
|
int dx = 0;
|
|
if (P > 0) {
|
|
if (!sand && abs(vx) >= 1.0) dx = vx >= 0.0 ? 1 : -1;
|
|
else if (!sand) {
|
|
// 0070b2d0 / 0070b7c0: the drift is drawn afresh only when (r & 127) < 64 (slime: < 12), otherwise the previous one of this update stays; the extra passes of one frame
|
|
// replay that chain
|
|
const uint gateLimit = (f & F_SLIME) != 0u ? 12u : 64u;
|
|
for (int k = 1; k <= P; ++k) {
|
|
const uint rg = hash3(i, uint(pc.frame) * 16u + uint(k), 5u + uint(pc.seed));
|
|
if (((rg >> 16) & 127u) < gateLimit) {
|
|
const uint r = hash3(i, uint(pc.frame) * 16u + uint(k), 6u + uint(pc.seed)) & 7u;
|
|
dx = r == 1u ? -1 : (r == 2u ? 1 : 0);
|
|
}
|
|
}
|
|
} else if ((rnd(i, 5u) & 1u) == 0u) {
|
|
uint r = rnd(i, 6u) & 7u;
|
|
dx = r == 1u ? -1 : (r == 2u ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
bool belowFree = false;
|
|
bool belowFalling = false;
|
|
ivec2 below = p + ivec2(0, 1);
|
|
if (in_bounds(below)) {
|
|
const uint bm = cell_mat(cells[idx(below)]);
|
|
belowFree = can_enter_liquid(m, bm, roll25);
|
|
// The original updates bottom-up, so a falling cell below has already moved by the time this cell
|
|
// runs. We decide from a snapshot, so wait for it instead of sliding sideways past it.
|
|
if ((mflags(bm) & F_LIQUID) != 0u) {
|
|
// already falling, or about to: it has free space of its own below it
|
|
belowFalling = ext[idx(below)].x > 0.0;
|
|
ivec2 below2 = below + ivec2(0, 1);
|
|
if (!belowFalling && in_bounds(below2))
|
|
belowFalling = can_enter_liquid(bm, cell_mat(cells[idx(below2)]), false);
|
|
}
|
|
}
|
|
|
|
if (downAllowed) {
|
|
ivec2 t = p + ivec2(dx, 1);
|
|
if (in_bounds(t) && can_enter_liquid(m, cell_mat(cells[idx(t)]), roll25)) {
|
|
target = idx(t);
|
|
if (P == 0) nvy = falling ? vy + g * 0.25 : 1.0e-4; // 0070b2d0: the falling state adds gravity / 4, a newly falling cell resets vy (the tiny value marks the falling state)
|
|
// 0070bdc0: after the fall the lateral loop still runs (while |vx| >= 1: a cell sideways, vx *= liquid_damping), so the sideways speed of a falling liquid
|
|
// dies within a frame or two; the GPU makes one move per pass, so the damping is applied here (a drop kept vx = +-1 from a diagonal slide and drifted
|
|
// diagonally on every extra fall step: the diagonal chains of falling water / oil)
|
|
nvx = (!sand && (f & F_SLIME) == 0u && P == 0 && abs(vx) >= 1.0) ? vx * mdamping(m) : vx;
|
|
}
|
|
}
|
|
|
|
if (target == NONE && !belowFree && belowFalling) return; // wait for the falling cell below to move on
|
|
|
|
if (target == NONE && P == 0) {
|
|
if (belowFree == false) nvy = 0.0; // blocked below: stop falling
|
|
if (sand) {
|
|
// diagonal slide with probability liquid_viscosity %, left first then right
|
|
int visc = mviscosity(m);
|
|
if (visc < 100 && rand100(i, 7u) < visc) {
|
|
ivec2 l = p + ivec2(-1, 1), r = p + ivec2(1, 1);
|
|
if (in_bounds(l) && can_enter_liquid(m, cell_mat(cells[idx(l)]), roll25)) target = idx(l);
|
|
else if (in_bounds(r) && can_enter_liquid(m, cell_mat(cells[idx(r)]), rand100(i, 12u) > 74)) target = idx(r);
|
|
}
|
|
} else if ((f & F_SLIME) != 0u && !belowFree) {
|
|
// 0070b7c0 (slime): the viscosity roll ends the move (no diagonal fallback), then left-down, right-down in this order, then a lateral step only when exactly one side is
|
|
// free: vx += 0.9, one move when |vx| >= 1 (vx -= 1); two free sides do nothing
|
|
int visc = mviscosity(m);
|
|
if (!(visc < 100 && rand100(i, 7u) < visc)) {
|
|
ivec2 l = p + ivec2(-1, 1), r = p + ivec2(1, 1);
|
|
if (in_bounds(l) && can_enter_liquid(m, cell_mat(cells[idx(l)]), roll25)) target = idx(l);
|
|
else if (in_bounds(r) && can_enter_liquid(m, cell_mat(cells[idx(r)]), roll25)) target = idx(r);
|
|
else {
|
|
ivec2 lp = p + ivec2(-1, 0), rp = p + ivec2(1, 0);
|
|
bool lv = in_bounds(lp), rv = in_bounds(rp);
|
|
bool lo = lv && cell_mat(cells[idx(lp)]) != 0u, ro = rv && cell_mat(cells[idx(rp)]) != 0u;
|
|
if (rv && !ro && (!lv || lo)) { nvx = vx + 0.9; if (nvx >= 1.0) { target = idx(rp); nvx -= 1.0; } }
|
|
else if (lv && !lo && (!rv || ro)) { nvx = vx - 0.9; if (nvx <= -1.0) { target = idx(lp); nvx += 1.0; } }
|
|
}
|
|
}
|
|
} else if (!belowFree) {
|
|
// lateral flow: accelerate toward the single free side, then move when |vx| reaches 1
|
|
ivec2 lp = p + ivec2(-1, 0), rp = p + ivec2(1, 0);
|
|
bool lf = in_bounds(lp) && can_enter_liquid(m, cell_mat(cells[idx(lp)]), roll25);
|
|
bool rf = in_bounds(rp) && can_enter_liquid(m, cell_mat(cells[idx(rp)]), rand100(i, 13u) > 74);
|
|
if (lf != rf) {
|
|
nvx = vx + (rf ? mflow(m) : -mflow(m));
|
|
if (rf && nvx >= 1.0) { target = idx(rp); nvx *= mdamping(m); }
|
|
else if (lf && nvx <= -1.0) { target = idx(lp); nvx *= mdamping(m); }
|
|
}
|
|
if (target == NONE) {
|
|
// random diagonal-down side, then the other
|
|
int dir = (rnd(i, 8u) & 1u) != 0u ? 1 : -1;
|
|
ivec2 a = p + ivec2(dir, 1), b = p + ivec2(-dir, 1);
|
|
if (in_bounds(a) && can_enter_liquid(m, cell_mat(cells[idx(a)]), roll25)) { target = idx(a); nvx = float(dir); }
|
|
else if (in_bounds(b) && can_enter_liquid(m, cell_mat(cells[idx(b)]), rand100(i, 14u) > 74)) { target = idx(b); nvx = float(-dir); }
|
|
}
|
|
}
|
|
}
|
|
|
|
// damped ballistic lateral travel while |vx| >= 1 (one cell per pass, vx *= damping per cell)
|
|
if (target == NONE && !sand && (f & F_SLIME) == 0u && !belowFree && abs(nvx) >= 1.0) { // (slime has no inertia loop: 0070b7c0 returns)
|
|
int s = nvx >= 0.0 ? 1 : -1;
|
|
ivec2 t = p + ivec2(s, 0);
|
|
if (in_bounds(t) && can_enter_liquid(m, cell_mat(cells[idx(t)]), roll25)) { target = idx(t); nvx *= mdamping(m); }
|
|
// blocked: the velocity stays (the original keeps it until its state / activity logic changes it)
|
|
}
|
|
} else if ((f & F_GAS) != 0u) {
|
|
if (P != 0) return; // gases act once per frame
|
|
const int lifetime = mlifetime(m);
|
|
if (lifetime > 0 && vy >= float(lifetime)) { cells[i] = 0u; ext[i] = vec2(0.0); return; } // 0070a670: the move counter (0070aad0 counts the successful moves) against the lifetime
|
|
uint sp = mgasbits(m);
|
|
if (rand100(i, 21u) <= int(sp & 255u)) {
|
|
if (rand100(i, 22u) < int((sp >> 8) & 255u)) {
|
|
ivec2 t = p + ivec2(0, -1);
|
|
if (in_bounds(t) && can_rise(m, cell_mat(cells[idx(t)]), roll25)) target = idx(t);
|
|
}
|
|
if (target == NONE && rand100(i, 23u) < int((sp >> 16) & 255u)) {
|
|
int dir = (c & DIR_BIT) != 0u ? 1 : -1;
|
|
ivec2 a = p + ivec2(dir, 0), b = p + ivec2(-dir, 0);
|
|
if (in_bounds(a) && can_rise(m, cell_mat(cells[idx(a)]), roll25)) target = idx(a);
|
|
else if (in_bounds(b) && can_rise(m, cell_mat(cells[idx(b)]), rand100(i, 25u) > 74)) { target = idx(b); flip = 1u; }
|
|
}
|
|
if (target == NONE && rand100(i, 24u) < int((sp >> 24) & 255u)) {
|
|
ivec2 t = p + ivec2(0, 1);
|
|
if (in_bounds(t) && is_air(cell_mat(cells[idx(t)]))) target = idx(t); // downward only into empty cells
|
|
}
|
|
}
|
|
} else if ((f & F_FIRE) != 0u) {
|
|
if (P != 0) return;
|
|
if (rand100(i, 31u) < 35) {
|
|
ivec2 up = p + ivec2(0, -1);
|
|
if (in_bounds(up) && is_air(cell_mat(cells[idx(up)]))) target = idx(up);
|
|
else if (rand100(i, 32u) >= 35) {
|
|
int s = rand100(i, 33u) < 50 ? -1 : 1;
|
|
ivec2 a = p + ivec2(s, 0), b = p + ivec2(-s, 0);
|
|
if (in_bounds(a) && is_air(cell_mat(cells[idx(a)]))) target = idx(a);
|
|
else if (in_bounds(b) && is_air(cell_mat(cells[idx(b)]))) target = idx(b);
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
if (target != NONE) {
|
|
intent[i] = uvec4(target | (flip << 31), prio, floatBitsToUint(nvy), floatBitsToUint(nvx));
|
|
atomicMax(claims[i], prio);
|
|
atomicMax(claims[target], prio);
|
|
} else {
|
|
ext[i] = vec2(nvy, nvx); // resting updates (velocity bookkeeping)
|
|
}
|
|
}
|