This commit is contained in:
Gaël committed 2026-10-19 20:59:15 +02:00
commit 87bff164f1
416 files changed
+279512

No files matched your search

+722
View File
@@ -0,0 +1,722 @@
#version 450
#extension GL_GOOGLE_include_directive : require
layout(local_size_x = 64) in;
#include "common.glsl"
#include "fx_state.glsl"
#include "fx_host.glsl"
// Entity simulation: gravity, walking, jumping, flying and a basic AI, one invocation per entity, once per frame.
// Entities collide with the live cell grid (solid = static or powder cells). Entities that are not fully inside the
// simulated window are frozen. Kinds: 0 static, 1 walker, 2 flyer, 3 player (index 0, controlled by the host).
struct Ent {
vec4 a; // position xy, velocity xy (cells, cells per frame)
vec4 box; // min_x, max_x, min_y, max_y
vec4 mv; // walk speed, max speed x, jump velocity (negative = up), gravity
vec4 ai; // detection range x, y, melee distance, melee damage
ivec4 st; // kind, facing, timer, flags (1 grounded, 2 can jump, 4 can fly, 8 dead, 16 in liquid)
vec4 hp; // hp, attack cooldown, max fall speed, climb-over height
ivec4 x; // damage taken this frame (milli-hp), jetpack energy / idle flag, spawn x (player) / ranged cooldown, spawn y
ivec4 rg; // ranged attack: projectile type (-1 none), frames between shots, min distance, max distance
ivec4 rg2; // projectiles per shot (min, max), muzzle offset y, the attack of the last attacking tick (1 melee, 2 ranged | tick << 4)
ivec4 aux; // player: maximum hp (milli-hp); creatures: velocity of the last hit (milli-cells per frame)
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 (1 projectile, 2 explosion, 4 fire, 8 melee), 0
vec4 ex4; // air in the lungs (frames), air maximum (0 = no air needed), 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
};
struct Proj { vec4 a; vec4 b; vec4 c; }; // pos, vel / damage, life, gravity, colour bits / owner (0 creature, 1 player)
struct PType { vec4 a; vec4 b; }; // speed min, max, damage, lifetime / gravity, colour bits, 0, 0
layout(std430, set = 0, binding = 24) readonly buffer PTypes { PType ptypes[]; };
layout(std430, set = 0, binding = 25) buffer Projs { Proj projs[]; };
layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; };
layout(std430, set = 0, binding = 23) buffer Ctrl {
ivec4 inp; // input x (-1/0/1), jump held, attack pressed this frame, entity count
ivec4 inp2; // frame, has player, attack hits (debug), projectile counter
ivec4 inp3; // aim x, aim y (window cells), fire, player projectile type
vec4 outp; // player: x, y, hp, facing (written by the shader, read by the host)
vec4 outp2; // player: levitation energy 0..1, fire cooldown, 0, 0
vec4 view; // host: camera shift of the last tick (cells), interpolation alpha, gravity scale
vec4 hand0; // host: held wand: aim point (world cells), enabled, mirror
ivec4 hand1; // host: held wand: texel offset, w, h
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;
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
const int ST_GROUNDED = 1, ST_CANJUMP = 2, ST_CANFLY = 4, ST_DEAD = 8, ST_LIQUID = 16, ST_RIGID = 32; // ST_RIGID: moved by the rigid-body engine (rigid_*.comp), only damage is handled here
// 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 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);
}
// 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
// the frame - does not apply: TelekinesisComponent is not done.)
bool solid_at(ivec2 w) {
if (!solid_cell_at(w)) return false;
return solid_cell_at(w + ivec2(0, 1)) || solid_cell_at(w + ivec2(-1, 0)) || solid_cell_at(w + ivec2(1, 0)) || solid_cell_at(w + ivec2(0, -1));
}
// What a creature or the player takes by damage type (bit 1 projectile, 2 explosion, 4 fire, 8 melee): the multipliers of the game effects, ex9 = projectile, explosion, fire, melee
float taken_mult(vec4 m, int types) {
if ((types & 2) != 0) return m.y;
if ((types & 8) != 0) return m.w;
if ((types & 4) != 0) return m.z;
return m.x;
}
// The hit flash (00ab5b00, called by TakeDamage 00ab9a90 for damage > 0 that is not fire alone): every sprite of the entity is drawn with
// sprite_damage_highlight (shader 6: white, the sprite's alpha) or, for a critical hit, sprite_damage_critical_hit_highlight (7: red) in its third shader slot;
// at most once per 5 frames (frame - DamageModel +0x238 > 4), and the DamageModelSystem update (00ab6d00) clears the slot at +0x238 + 3: 3 frames of flash.
// ex19.w counts it down: 5 at the hit, drawn while >= 3 (sprites.comp), a new flash at 0; + 8 for a critical hit. (Worm segments keep their root slot there.)
void hit_flash(uint id, bool hit, bool crit) {
const int c = int(ents[id].ex19.w + 0.5);
int n = max((c & 7) - 1, 0);
bool red = (c & 8) != 0;
if (hit && n == 0) { n = 5; red = crit; }
ents[id].ex19.w = n == 0 ? 0.0 : float(n + (red ? 8 : 0));
}
// What an entity deals: the DAMAGE_MULTIPLIER effect (the perk glass cannon) etc.; the host stores the factor in ex7.y (0 = none)
float dealt_mult(Ent s) { return s.ex7.y > 0.0 ? s.ex7.y : 1.0; }
bool liquid_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;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
if (m == 0u) return false;
uint f = mflags(m);
return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too)
}
bool line_clear(vec2 a, vec2 b) { // no solid cell between two points (3-cell steps)
vec2 d = b - a;
int n = int(length(d) / 3.0);
for (int i = 1; i < n; ++i) if (solid_cell_at(ivec2(floor(a + d * (float(i) / float(n)))))) return false;
return true;
}
bool box_hits(ivec2 o, vec4 box) {
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)
if (solid_at(ivec2(x, y))) return true;
return false;
}
// 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);
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
return m != 0u && ((mats[3u * m + 2u].z >> 18) & 1u) != 0u;
}
// script_damage_about_to_be_received (the host sets 65536 in ex8.x when the entity or a child has one): the hp a hit takes goes to a running total instead
// (ex14.w, milli-hp, written only by the entity's own thread); the host runs the scripts on what was added and changes the hp by their result
float take_damage(uint id, float v) {
if ((ents[id].ex8.x & 65536) != 0) { ents[id].ex14.w += v * 1000.0; return 0.0; }
return v;
}
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= uint(ctrl.inp.w)) return;
Ent e = ents[id];
int kind = e.st.x;
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)
// ex = (x_vel_min, x_vel_max, y_vel_min, y_vel_max) in px/s (local), ex2 = (next emit frame, mode 0 off / 1 death leakage (1-3 per emission) / 2 damage leakage (1), 0, mass),
// ex7 = (-, reserve, material + 1, -). A damage event (min_damage_to_leak 0.09, leak_on_damage_percent 1 compared with u in [0,1)) rewrites the velocity ranges from the clamped
// local hit offset (00bb6820) and switches to one droplet per emission; the interval is 1-2 frames either way, the schedule advances before the spawn, a request larger than the
// reserve emits nothing, the reserve only falls by the droplets that were created, and a blocked spawn cell ends the burst (emit_only_if_there_is_space).
float reserve = e.ex7.y;
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;
}
}
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)) {
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 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)
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
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);
}
}
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)) {
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
const float base = (360.0 + 0.7 * 1200.0 * u2[2]) * e.ex2.w, speed = 0.7 * base + (1.1 * base - 0.7 * base) * u2[3];
const vec2 v0 = speed * d;
const vec4 cr = vec4(v0.x * (0.8 + 0.19999999 * u2[4]), v0.x * (1.0 + 0.20000005 * u2[5]), v0.y * (0.8 + 0.19999999 * u2[6]), v0.y * (1.0 + 0.20000005 * u2[7]));
const float ca = cos(e.a.z), sa = sin(e.a.z);
int made = 0;
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);
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);
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].ex = vr; ents[id].ex2.x = nextf; ents[id].ex2.y = mode; ents[id].ex7.y = max(reserve, 0.0);
}
vec2 pos = e.a.xy, vel = e.a.zw;
ivec2 ip = ivec2(floor(pos));
// frozen unless the collision box (plus margin) is inside the simulated window
ivec2 wp = ip - ivec2(pc.view_x, pc.view_y);
if (wp.x < 3 || wp.y < 3 || wp.x > pc.gw - 3 || wp.y > pc.gh - 3) return;
uint rnd = hash3(id, uint(ctrl.inp2.x), 911u);
int flags = e.st.w;
int types = 0; // damage types taken this tick (creatures)
vec2 hitv = vec2(0.0); // direction of the hit
float dmgTick = 0.0; // damage taken this tick
int dir = e.st.y;
int timer = e.st.z;
float hp = e.hp.x, cooldown = max(e.hp.y - 1.0, 0.0);
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)) {
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));
for (int c = 0; c < 5 && !freed; ++c)
if (!box_hits(ip + cand[c], e.box)) { ip += cand[c]; pos = vec2(float(ip.x) + fract(pos.x), float(ip.y)); vel.y = min(vel.y, 0.0); freed = true; }
}
}
bool grounded = !rigidBody && box_hits(ip + ivec2(0, 1), e.box);
const bool wasGrounded = grounded;
bool liquid = liquid_at(ip);
if (kind == 1 || kind == 2 || kind == 3) { // stains: standing in water, oil, blood... leaves its status on the creature for a while (ex8.z: bits | frame << 8)
uint sm = 0u;
for (int sy = 0; sy < 2; ++sy) {
const ivec2 sp = ip + ivec2(0, -4 * sy) - ivec2(pc.view_x, pc.view_y);
if (sp.x >= 0 && sp.y >= 0 && sp.x < pc.gw && sp.y < pc.gh) sm |= (gtab[cell_mat(cells[sp.y * pc.gw + sp.x])] >> 16) & 63u;
}
if (sm != 0u) {
const uint old = ((uint(ents[id].ex8.z) >> 8) + 900u > uint(pc.frame & 0xFFFFFF)) ? (uint(ents[id].ex8.z) & 63u) : 0u;
ents[id].ex8.z = int((sm | old) | (uint(pc.frame & 0xFFFFFF) << 8));
}
}
// swimming (CharacterPlatformingComponent swim_*): with the buoyancy check point (CharacterDataComponent buoyancy_check_offset_y) under liquid a walker or
// the player is nearly weightless (swim_idle_buoyancy_coeff), every velocity is damped (swim_drag, swim_extra_horizontal_drag) and the up key swims up
const bool swimming = (kind == 1 || kind == 3) && (e.st.w & ST_SWIMMER) != 0 && liquid_at(ivec2(pos) + ivec2(0, int(e.ex.w))); // float coordinates truncate toward zero (00a7ea90)
float walk = e.mv.x, vmax = e.mv.y;
if (e.ex15.z > 0.5 && (int(e.ex15.w) & 32) != 0) walk *= 2.0; // the run button of a creature the host runs (00a25d50: it has a prey or a threat; 00a7ea90 doubles the speed)
float target = 0.0;
bool want_jump = false, thrust = false;
// a flyer's ControlsComponent: mButtonDownFly and mFlyingTargetY (ex18.z), set by the path steer (00a29b20 -> 00a8d340) and kept between them
bool flyOn = kind == 2;
float flyTY = e.ex18.w < -1.0e8 ? pos.y : e.ex18.z;
vec4 pl = ents[0].a; // the player (if there is one)
bool has_player = ctrl.inp2.y != 0;
bool player_alive = has_player && (ents[0].st.w & ST_DEAD) == 0;
const bool frozen = (e.ex8.x & 1) != 0; // a game effect with disable_movement (FROZEN, PARALYSIS...): no walking, jumping, attacking
if (kind == 3) { // ---- the player ----
target = frozen ? 0.0 : float(ctrl.inp.x) * walk;
vec2 aim_w = vec2(ctrl.inp3.xy) + vec2(float(pc.view_x), float(pc.view_y)); // aim point in world cells
if (ctrl.inp3.x >= 0) dir = aim_w.x >= pos.x ? 1 : -1; else if (ctrl.inp.x != 0) dir = ctrl.inp.x; // face the mouse
cooldown = max(cooldown - 0.0, 0.0);
if (ctrl.inp3.z != 0 && !frozen && cooldown <= 0.0 && ctrl.inp3.w >= 0) { // fire the spark bolt toward the mouse
PType t = ptypes[ctrl.inp3.w];
vec2 from = pos + vec2(0.0, -3.0);
vec2 dv = aim_w - from;
vec2 dirv = length(dv) > 0.5 ? normalize(dv) : vec2(float(dir), 0.0);
uint r2 = hash3(id, uint(ctrl.inp2.x), 77u);
float ang = (float(r2 & 255u) / 255.0 - 0.5) * 0.04;
dirv = vec2(dirv.x * cos(ang) - dirv.y * sin(ang), dirv.x * sin(ang) + dirv.y * cos(ang));
uint slot = uint(atomicAdd(ctrl.inp2.w, 1)) % 512u;
projs[slot].a = vec4(from, dirv * t.a.y);
projs[slot].b = vec4(t.a.z, t.a.w, t.b.x, t.b.y);
projs[slot].c = vec4(1.0, 0.0, 0.0, 0.0);
cooldown = 9.0;
}
want_jump = ctrl.inp.y != 0 && !frozen;
{ // the jetpack (CharacterDataComponent fly_time_max, fly_recharge_spd, fly_recharge_spd_ground): flying burns one frame of fuel per frame, on the ground it refills at 6 seconds per second,
// in the air the fuel trickles back at fly_recharge_spd as soon as the key is up (units of 1/64 frame)
const float emax = (e.ex2.x > 0.0 ? e.ex2.x : 180.0) * 64.0;
float en = float(energy);
thrust = want_jump && !grounded && en >= 64.0; // the jetpack works in liquids too (the swim correction comes after it)
if (grounded) en = min(emax, en + 6.0 * 64.0);
else if (thrust) en -= 64.0;
else en = min(emax, en + (e.ex2.y > 0.0 ? e.ex2.y : 0.4) * 64.0);
energy = int(en);
}
// damage taken from enemies this frame
int dmg = atomicExchange(ents[id].x.x, 0);
const float hpBeforeHit = hp;
{ const int pty = atomicExchange(ents[id].ex3.z, 0); hp -= take_damage(id, float(dmg) * 0.001); if (pty != 0) ents[id].ex3.w = (pty & 0xFF) | ((ctrl.inp2.x & 0xFFFFF) << 8); hit_flash(id, dmg > 0, (pty & 0x10000) != 0); } // (the accumulator already holds the multiplied damage: every source applies the victim's multiplier of its type) (ex3.w: the types of this tick | tick << 8, for the damage sounds)
// TakeDamage (00ab9a90) on a lethal hit: SAVING_GRACE (effect 0x2a) leaves 1 / 25 hp (1 HP shown) when there was more, else an unspent RESPAWN (0x2d, extra life: ex8.x
// bit 131072, the host clears it) sets 100 / 25 hp, at most the maximum; the host is told (ctrl burnN.z / w) for the message and the PROTECTION_ALL of 60 / 30 frames
if (hp <= 0.0 && (e.ex8.x & 16) != 0 && hpBeforeHit > 0.04) { hp = 0.04; atomicAdd(ctrl.burnN.z, 1); }
else if (hp <= 0.0 && (ents[id].ex8.x & 131072) != 0) { hp = min(4.0, e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0); atomicAnd(ents[id].ex8.x, ~131072); atomicAdd(ctrl.burnN.w, 1); }
if (hp <= 0.0 && (e.ex8.x & 32768) == 0) { // respawn at the spawn point (a polymorphed body is not: the host ends the polymorph, 32768)
pos = vec2(float(e.x.z), float(e.x.w)); vel = vec2(0.0); ip = ivec2(floor(pos));
hp = e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0;
atomicAdd(ctrl.burnN.y, 1);
}
} else if (kind == 1 || kind == 2) { // ---- creatures ----
// the target of the creature: the player; a charmed creature (game effect CHARM) fights the nearest creature of another herd instead
vec4 tp = pl; bool tAlive = player_alive; int tgt = 0;
const bool berserk = (e.ex8.x & 4) != 0 && (e.ex8.x & 2) == 0; // BERSERK (00a28e10): it attacks every mortal, the player and its own herd included, unless berserk_dont_attack_friends (bit 3)
if ((e.ex8.x & 2) != 0 || berserk) {
float best = 1.0e9;
tgt = -1;
for (int k = berserk ? 0 : 1; k < ctrl.inp.w; ++k) {
if (k == int(id) || (ents[k].st.w & ST_DEAD) != 0 || (ents[k].ex8.x & 8192) != 0) continue; // (an invisible one is not seen)
if (k == 0 ? (!berserk || ents[0].st.x != 3) : (ents[k].st.x != 1 && ents[k].st.x != 2)) continue;
if (berserk ? ((e.ex8.x & 8) != 0 && ents[k].ex8.y == e.ex8.y) : ents[k].ex8.y == e.ex8.y) continue;
const float dk = distance(ents[k].a.xy, pos);
if (dk < best && abs(ents[k].a.x - pos.x) < e.ai.x && abs(ents[k].a.y - pos.y) < e.ai.y) { best = dk; tgt = k; }
}
tAlive = tgt >= 0;
if (tgt >= 0) tp = ents[tgt].a; else tgt = 0;
}
float dx = tp.x - pos.x, dy = tp.y - pos.y;
// sees the player: within the detection range and, for creatures with a limited field of view (creature_detection_angular_range_deg), inside the cone in front of them
vec2 toP = vec2(tp.x, tp.y - 4.0) - vec2(pos.x, pos.y + e.ex.y);
bool inCone = e.ex.x <= -0.999 || (length(toP) > 1.0 && dot(normalize(toP), vec2(float(dir), 0.0)) >= e.ex.x);
bool sees = tAlive && !frozen && (ents[tgt].ex8.x & 8192) == 0 && abs(dx) < e.ai.x && abs(dy) < e.ai.y && inCone;
types = atomicExchange(ents[id].ex3.z, 0); // damage types taken since the last tick
timer -= 1;
if (timer <= 0) { // pick a new idle/wander action
uint r = rnd;
if ((r % 3u) == 0u) timer = 20 + int((r >> 4) % 50u);
else { dir = ((r >> 8) & 1u) == 0u ? -1 : 1; timer = 60 + int((r >> 4) % 120u); }
e.x.y = (r % 3u) == 0u ? 1 : 0; // 1 = idling for this action
}
bool idle = e.x.y == 1;
if (sees) { dir = dx >= 0.0 ? 1 : -1; idle = false; }
if (e.ex15.z > 0.5) {
// the host's AnimalAI (src/ai, AnimalAISystem 00a22540 + PathFindingSystem 00bca030): mode 1 heads for the next node of its path (a flyer in both axes), mode 2 stands;
// the attacks below stay the GPU's
target = 0.0;
if ((int(e.ex15.w) & 2) != 0 || (int(e.ex15.w) & 8) != 0) target = vel.x; // (from the launch on, also on the ground: a lob down off a ledge starts into the floor and slides off the edge at its speed) // a jump of the path: no control in the air (a dash: for its whole flight time)
else if (e.ex15.z < 1.5) {
const float sdx = e.ex15.x - pos.x;
if (abs(sdx) > 1.0) { dir = sdx > 0.0 ? 1 : -1; target = float(dir) * walk; }
// a flyer (00a29b20): the fly key with the node's y as mFlyingTargetY; a can_walk flyer with ground within 10 px below and the node not above it lets go
// of the key and walks (the host's flag 16)
if (kind == 2) { flyOn = (int(e.ex15.w) & 16) == 0; if (flyOn) flyTY = e.ex15.y; }
// (a fish swims up and down by its buoyancy: the swim correction below)
}
} else {
if (!idle) target = float(dir) * walk;
if (kind == 1) {
// do not walk off ledges while wandering; turn around at walls (handled below), jump after the player
if (!sees && grounded && !idle) {
ivec2 ahead = ip + ivec2(dir * 5, 0);
if (!box_hits(ahead + ivec2(0, 4), e.box) && !box_hits(ahead + ivec2(0, 8), e.box)) { dir = -dir; target = float(dir) * walk; }
}
} else if (sees) flyTY = tp.y - 8.0; // flyer without the host's AI: the fly key, toward the player's height when it sees one (else it holds the last height)
}
{ // hits: projectiles and explosions (damage accumulator), the blood sprays out along the hit
int dmg = atomicExchange(ents[id].x.x, 0);
hp -= take_damage(id, float(dmg) * 0.001); // (already multiplied at the source by the victim's multiplier of the damage type)
dmgTick += float(dmg) * 0.001;
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) {
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);
atomicOr(ents[tgt].ex3.z, 8);
ents[tgt].ex8.w = int(id + 1) | ((pc.frame & 0xFFFFF) << 12); // the target was hit by this creature
cooldown = 50.0;
}
// ranged attack: cast the creature's projectile (a node of the GPU projectile system, team 2) at the player when in range with a clear line
int rcd = max(e.x.z - 1, 0);
if (e.rg.x >= 0 && tAlive && !frozen) {
vec2 from = pos + vec2(0.0, float(e.rg2.z)), to = tp.xy + vec2(0.0, -4.0);
float dist = length(to - from);
if (sees && rcd == 0 && dist >= float(e.rg.z) && dist <= float(e.rg.w) && line_clear(from, to)) {
int count = e.rg2.x + int(rnd % uint(e.rg2.y - e.rg2.x + 1));
float speed = max(e.ex2.x, 0.1), tt = dist / speed;
vec2 aimv = (to - from) / tt + vec2(0.0, -0.5 * e.ex2.y * tt); // launch velocity that compensates the gravity over the flight time
for (int k = 0; k < count; ++k) {
uint r2 = hash3(id, uint(ctrl.inp2.x), uint(k) + 5u);
float ang = (float((r2 >> 8) & 255u) / 255.0 - 0.5) * (0.08 + 0.05 * float(count - 1));
vec2 d0 = normalize(aimv);
vec2 dirv = vec2(d0.x * cos(ang) - d0.y * sin(ang), d0.x * sin(ang) + d0.y * cos(ang));
int list = pc.frame & 1;
int slot = list == 0 ? atomicAdd(hhead.x, 1) : atomicAdd(hhead.y, 1);
if (slot < 256) {
hcast[(list * 256 + slot) * 2] = vec4(from, dirv);
hcast[(list * 256 + slot) * 2 + 1] = vec4(float(e.rg.x), float(2 + (e.ex8.y << 3)), float(r2 & 0xFFFFFFu), float(1 + (id + 1) * 2)); // r1.w: muzzle flash flag, the shooter's slot + 1 in the higher bits
}
}
rcd = e.rg.y;
ents[id].rg2.w = 2 | ((ctrl.inp2.x & 0xFFFFFF) << 4); // the attack of the tick (1 melee, 2 ranged | tick << 4): the host plays its sprite events (attack sounds)
}
}
e.x.z = rcd;
// (the player's kick on creatures is the host's: KickSystem 00bf2040, 18 frames after the press - its damage goes through the damage accumulator)
if (hp <= 0.0) flags |= ST_DEAD;
}
if (frozen) { target = 0.0; want_jump = false; thrust = false; }
// soft separation: creatures and the player do not stack on top of each other
// (characters do not push each other: the engine has no creature-creature collision - CharacterCollisionComponent only collides with the world)
// ---- damage: projectiles and explosions (the atomic accumulator), contact with harmful materials, fire ----
if (is_prop && e.hp.x < 1.0e8) {
int dmg = atomicExchange(ents[id].x.x, 0);
hp -= float(dmg) * 0.001; // (the material damage of 00ab6010 / 00ab7410 comes through this accumulator from the host)
hit_flash(id, dmg > 0, (atomicExchange(ents[id].ex3.z, 0) & 0x10000) != 0);
if (hp <= 0.0) flags |= ST_DEAD;
}
if (kind == 1 || kind == 3 || kind == 2) { // fire hurts: cells of fire sources or burning cells overlapping the box
int burn = 0;
ivec2 lo = ip + ivec2(int(e.box.x), int(e.box.z)), hi = ip + ivec2(int(e.box.y), int(e.box.w));
for (int y = lo.y; y < hi.y; ++y)
for (int x = lo.x; x < hi.x; ++x) {
ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
uint cw = cells[q.y * pc.gw + q.x];
uint cm = cell_mat(cw);
if (cm != 0u && (((mats[3u * cm + 2u].z >> 17) & 1u) != 0u || (cw & BURN_BIT) != 0u)) ++burn;
}
if (burn > 0) types |= 4; // (touching fire does no damage of its own: only the materials of materials_that_damage and the burning do)
// ---- burning (DamageModelSystem): ignition in the cell scan 00ab6010, the burning update 00ab7a70 ----
// ignition (fire_probability_of_ignition != 0): the cells under the body that burn or are of an on_fire material (on_fire counts 2, not the creature's own sparks)
// add up; above 4, rand(0, 100) < the strength (fade) of such a cell sets mFireFramesLeft to max(left, seconds x 60): 12 s, 0 with a status that protects from
// fire (WET, OILED, BLOODY, SLIMY, RADIOACTIVE), 20 s ALCOHOLIC, 30 s OILED (in that order, the last one wins)
const uint stains = ((uint(e.ex8.z) >> 8) + 900u > uint(pc.frame & 0xFFFFFF)) ? (uint(e.ex8.z) & 63u) : 0u;
const bool fireProt = (stains & (1u | 2u | 4u | 16u | 32u)) != 0u;
float fireLeft = e.ex4.w, fireProb = e.ex16.z;
if (e.ex2.z > 0.0) {
int cnt = 0, fade = 0;
ivec2 lo2 = ip + ivec2(int(e.box.x), int(e.box.z)), hi2 = ip + ivec2(int(e.box.y), int(e.box.w));
for (int y = lo2.y; y < hi2.y; ++y)
for (int x = lo2.x; x < hi2.x; ++x) {
ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
uint cw = cells[q.y * pc.gw + q.x];
uint cm = cell_mat(cw);
if (cm == 0u) continue;
const bool onFireMat = ((mats[3u * cm + 2u].z >> 17) & 1u) != 0u;
if (!onFireMat && (cw & BURN_BIT) == 0u) continue;
cnt += onFireMat ? 2 : 1;
int f = int(birth[q.y * pc.gw + q.x]) >> 24; // (the cell's fade, its strength: a fire cell without one has its temperature_of_fire)
if (f == 0 && (cw & BURN_BIT) == 0u) f = int((mats[3u * cm + 2u].w >> 8) & 255u);
fade = f;
}
if (cnt > 4 && int(hash3(id, uint(ctrl.inp2.x), 61u) % 101u) < fade) {
float secs = 12.0;
if (fireProt) secs = 0.0;
if ((stains & 8u) != 0u) secs = 20.0;
if ((stains & 2u) != 0u) secs = 30.0;
fireLeft = max(fireLeft, floor(secs * 60.0 + 0.5));
if (e.ex4.w <= 0.0 && fireLeft > 0.0) fireProb = 50.0; // (mIsOnFire changes: mFireProbability 50)
}
}
if (fireLeft > 0.0) {
// 00ab7a70: fire_how_much_fire_generates times a random point of the hitbox: an empty cell gets a spark (the creature's own: it does not ignite it), a burning cell
// keeps the fire going (mFireProbability + 1, at most 100), each liquid cell met counts 3 against it when more than half of the tries met liquid; out when
// mFireProbability or mFireFramesLeft (-1 a frame, -2 with a fire-protecting status, +1 OILED) reaches 0. The damage while it burns: max(0.8, max hp) x 0.02
// per second, twice that for anything but the player (fire_damage_amount only switches it on; the engine delivers the sum every 9 - 12 frames, here each frame)
const int nTry = max(int(e.ex2.z) - 1, 0);
int lit = 0, wet = 0;
const uint spark = mats[mats.length() - 2u].x;
for (int k = 0; k < nTry && k < 10; ++k) {
const float rx = float(hash3(id, uint(ctrl.inp2.x), 70u + uint(k)) & 0xFFFFu) / 65536.0, ry = float(hash3(id, uint(ctrl.inp2.x), 90u + uint(k)) & 0xFFFFu) / 65536.0;
const ivec2 w = ivec2(floor(pos + vec2(e.box.x + (e.box.y - e.box.x) * rx, e.box.z + (e.box.w - e.box.z) * ry)));
const ivec2 q = w - ivec2(pc.view_x, pc.view_y);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue;
const int qi = q.y * pc.gw + q.x;
uint cw = cells[qi];
if (cell_mat(cw) == 0u && spark != 0u && atomicCompSwap(cells[qi], 0u, spark | (hash3(id, uint(k), 65u) & 0xFF0000u)) == 0u) { birth[qi] = birth_frame(); lit = 1; continue; }
cw = cells[qi];
const uint cm = cell_mat(cw);
if (cm != 0u && ((cw & BURN_BIT) != 0u || (mflags(cm) & F_FIRE) != 0u)) lit = 1;
else if (cm != 0u && (mflags(cm) & F_LIQUID) != 0u) wet += 3;
}
fireProb = min(fireProb + float(lit), 100.0);
if (nTry / 2 < wet) fireProb -= float(wet);
fireLeft -= 1.0; // (the extra -1 of a fire-protecting status and the +1 of OILED: the host, from the status system's stain amounts)
if (lit == 1 && e.ex2.w != 0.0) {
const float maxhp = kind == 3 ? float(e.aux.x) * 0.001 : float(e.x.w) * 0.001;
hp -= take_damage(id, max(0.8, maxhp) * 0.02 / 60.0 * (kind == 3 ? 1.0 : 2.0) * e.ex9.z);
}
types |= 4;
if (fireProb < 1.0 || fireLeft < 1.0) { fireLeft = 0.0; fireProb = 0.0; }
}
ents[id].ex16.z = fireProb;
ents[id].ex4.w = fireLeft;
// air (00ab81b0, frames here): with the head in liquid air_in_lungs runs down 1 per frame; out of it (ex16.w counts the frames) it refills 1 per frame, 5 after
// 20 frames, up to air_in_lungs_max; at 0.2 s or less of air (also while it refills) air_lack_of_damage x dt is lost each frame
if (e.ex4.y > 0.0) {
float air = e.ex4.x, outF = e.ex16.w;
if (liquid_at(ip + ivec2(0, int(e.box.z)))) { outF = 0.0; air -= 1.0; }
else { outF = min(outF + 1.0, 1.0e6); if (air < e.ex4.y) air = min(e.ex4.y, air + (outF > 20.0 ? 5.0 : 1.0)); }
if (air <= 12.0) { air = max(air, 0.0); hp -= take_damage(id, e.ex4.z); types |= 32; }
ents[id].ex4.x = air; ents[id].ex16.w = outF;
}
if (kind != 3 && hp <= 0.0) flags |= ST_DEAD;
if ((types & (1 | 2 | 8)) != 0) ents[id].ex3.w = (types & 0xFF) | ((ctrl.inp2.x & 0xFFFFF) << 8); // the damage types of this tick | tick << 8 (the host's damage sounds, 00ab9a90)
}
if ((kind == 1 || kind == 2) && (flags & ST_DEAD) != 0 && (e.st.w & ST_DEAD) == 0) { // it dies now: blood bursts, the host spawns the corpse and runs the death scripts
float maxhp = float(e.x.w) * 0.001;
// KillMe (00abb7d0): the RAGDOLL_FX of the killing hit (bits 12-15; the packet's default NORMAL), a NORMAL projectile or explosion kill turns into BLOOD_SPRAY when
// rand % 100 + 1 <= 20 (01153024); BLOOD_EXPLOSION (2) makes the parts without joints: the gib (ragdoll_fx_forced is the host's)
int rfx = (types >> 12) & 15;
if (rfx == 0 && (types & 3) != 0) rfx = 1;
if (rfx == 1 && (types & 3) != 0 && int(hash3(id, uint(ctrl.inp2.x), 77u) % 100u) + 1 <= 20) rfx = 3;
bool gib = rfx == 2;
float hl = length(hitv);
vec2 hd = hl > 0.01 ? hitv / hl : vec2(0.0, -1.0);
int n = 14 + min(int(maxhp * 30.0), 24);
// no burst of its own: the lethal hit already made its liquid, the corpse parts carry the rest of the blood (finite reserves, see the part branch above)
ents[id].ex3.w = (types & 0xEF) | (gib ? 16 : 0) | ((ctrl.inp2.x & 0xFFFFF) << 8) | (rfx << 28); // (bits 28-31: the RAGDOLL_FX of the death)
ents[id].aux.z = int(hitv.x * 1000.0); ents[id].aux.w = int(hitv.y * 1000.0);
}
if (is_prop && (flags & ST_HOVER) != 0) {
const vec2 home = e.ex6.zw;
ents[id].a = vec4(home.x, home.y + 2.0 * sin(3.0 * float(pc.frame) / 60.0), 0.0, 0.0); // 00b83980 (ItemSystem, play_hover_animation): home y + 2 sin(3 t), t in seconds; the sprite turned by -pi/2
return;
}
if (is_prop && (flags & ST_RIGID) != 0) {
ents[id].st = ivec4(kind, dir, timer, flags);
ents[id].hp = vec4(hp, cooldown, e.hp.z, e.hp.w);
return;
}
// ---- physics ----
bool fly = kind == 2 && flyOn;
if (kind == 2) ents[id].ex18.zw = vec2(flyTY, fly ? (e.ex18.w < -1.0e8 ? flyTY : (e.ex18.y > 0.5 ? e.ex18.w + (flyTY - e.ex18.w) * 0.05 : flyTY)) : -1.0e9);
if (fly && (int(e.ex15.w) & 8) == 0) { // (a flyer's dash: the host's velocity for its flight)
// 00a7ea90, fly_model_player 0: mSmoothedFlyingTargetY eases to mFlyingTargetY by 0.05 a frame (fly_smooth_y; set at once when the key goes down); the vertical speed
// goes toward clamp(fly_speed_mult x (smoothed - y) - g dt, -fly_speed_max_up, fly_speed_max_down) by fly_speed_change_spd (snapped at the target), then the gravity
const float sY = ents[id].ex18.w;
const float tv = clamp(e.ex17.x * (sY - pos.y) / 60.0 - e.mv.w, -e.ex17.z, e.ex17.w);
const float nv = vel.y + e.ex17.y * (tv - vel.y);
vel.y = (sign(tv - nv) != sign(tv - vel.y)) ? tv : nv;
vel.y += e.mv.w;
} else if (!fly) {
vel.y += e.mv.w * ctrl.view.w; // gravity; the swim correction (docs/WATER_PHYSICS.md) follows the input staging below
if (kind == 4) vel *= 1.0 - e.ex5.x; // 00c370c0: air friction of the item's VelocityComponent (per frame) after gravity, then the liquid drag, then the terminal speed (vector)
else vel.y = min(vel.y, e.hp.z);
if (grounded && vel.y > 0.0) vel.y = 0.0; // resting on the ground: no gravity build-up (it made the sub-cell height creep 0.97 and snap back every 5 ticks)
}
// The knockback state of the platforming update (00a7ea90 lines 384-433): after a hit with knockback the horizontal velocity stays the kick's x (+0x148) and the controls are off; it ends
// after at least 3 frames when the body is on the ground (is_on_ground), stood in liquid at the buoyancy probe (x, y + buoyancy_check_offset_y) or collided horizontally
// the frame before (mCollidedHorizontally), and for the player after 60 frames (+0x14c, set to 60 by the kick 00ab9910; precision_jumping_max_duration_frames is
// -1 = no limit in every file)
vec4 kst = e.ex10; // the knockback state: x = the kick's horizontal velocity, y = frames since, z = 1 in the state (2 when it also hit a wall the frame before)
if ((kind == 1 || kind == 3) && (e.ex11.y != 0.0 || e.ex11.z != 0.0)) { // a queued kick (00a7ea90 lines 384-395): added to the staged velocity, CONFUSION inverts its x, it becomes the state's horizontal velocity
vec2 kk = e.ex11.yz;
if ((e.ex8.x & 128) != 0) kk.x = -kk.x;
vel += kk;
kst = vec4(kk.x, 0.0, 1.0, e.ex10.w);
ents[id].ex11.yz = vec2(0.0);
}
bool knocked = (kind == 1 || kind == 3) && kst.z > 0.5;
if (knocked) {
const float cnt = kst.y + 1.0;
if ((cnt >= 3.0 && (grounded || kst.z > 1.5 || liquid_at(ivec2(pos) + ivec2(0, int(e.ex.w))))) || (kind == 3 && cnt >= 60.0)) { ents[id].ex10.xyz = vec3(0.0); knocked = false; }
else { ents[id].ex10.xyz = vec3(kst.x, cnt, 1.0); vel.x = kst.x; }
}
if (!knocked) {
if (kind == 3 && thrust) { const float lf = (e.ex8.x & 16384) != 0 ? 1.75 : 1.0; vel.y = max(vel.y - 0.3 * lf, -1.45 * lf); } // levitation (FASTER_LEVITATION 0x4b: the two flying speeds of CharacterPlatformingComponent x 1.75)
else if (want_jump && grounded && kind != 2 && (kind == 3 || (flags & ST_CANJUMP) != 0)) vel.y = e.mv.z; // jump
if (kind == 2 && fly) target = sign(target) * e.ex18.x; // (flying: fly_velocity_x instead of run_velocity, 00a7ea90)
if (kind == 1 || kind == 2 || kind == 3) { // 00a7ea90: v += accel_x x (target - v) each frame (accel_x_air while flying in the air), the target when it would pass it
const float f = ((kind == 3 && thrust) || fly) && !grounded ? e.ex16.y : e.ex16.x;
const float nv = vel.x + f * (target - vel.x);
vel.x = (sign(target - nv) != sign(target - vel.x)) ? target : nv;
} else vel.x += clamp(target - vel.x, -0.12, 0.12);
// (the lob of a path jump keeps its speed from the launch until the host ends the jump: on the launch tick the creature still stands on the ground - capped
// there, a hop flew at walking speed and fell back where it started, and a lob down off a ledge never left it: zombies jumping in place)
if (!(((kind == 1) && (int(e.ex15.w) & 2) != 0) || ((kind == 1 || kind == 2) && (int(e.ex15.w) & 8) != 0))) vel.x = clamp(vel.x, -vmax, vmax); // (a dash keeps its speed for its flight, a flyer's too) // (the lob of a path jump keeps its speed)
}
// player splash (00a7ea90 / 00a809f0), queued BEFORE the swim correction below (the speed it tests and throws is the one before buoyancy and drag): the first swimming frame after more than 15 frames out of the liquid, faster than 40 px/s, falling, not on the ground: the 11x11 square
// around the position throws its liquid forward, v = s Rot(a) v_entity, s = 0.1 + 0.7 u1, a = 0.4 u2 - 0.2 (the original draws two numbers for every one of the 121 places)
if (kind == 3) {
float fns = e.ex6.y;
if (swimming) {
if (fns > 15.0 && length(vel) * 60.0 > 40.0 && vel.y > 0.0 && !grounded)
for (int dy = -5; dy <= 5; ++dy) for (int dx = -5; dx <= 5; ++dx) {
const uint rs = hash3(uint(id) * 131u + uint(dx + 5 + 11 * (dy + 5)), uint(pc.frame), 73u);
const float s1 = 0.1 + 0.7 * float(rs & 0x7FFFu) / 32767.0, a = 0.4 * float((rs >> 15) & 0x7FFFu) / 32767.0 - 0.2;
displace_cell(ivec2(pos) + ivec2(dx, dy), s1 * (mat2(cos(a), sin(a), -sin(a), cos(a)) * vel));
}
fns = 0.0;
} else fns += 1.0;
ents[id].ex6.y = fns;
}
if (swimming && !fly) {
// 00a7ea90: after gravity, input, jetpack: b = down ? swim_down : fly key ? swim_up : swim_idle; vx = extra * (drag * vx); vy = drag * (vy - b * G), G = pixel_gravity * dt
const float G = e.mv.w * ctrl.view.w;
// Down has priority over Fly (the down key is ControlsComponent.mButtonDownDown, the up one mButtonDownFly): down gives swim_down_buoyancy_coeff, fly swim_up_, nothing swim_idle_
const bool downKey = kind == 3 && ctrl.inp3.z != 0 && !frozen;
const float fishDy = (int(e.ex15.w) & 4) != 0 && e.ex15.z > 0.5 && e.ex15.z < 1.5 ? e.ex15.y - pos.y : 0.0; // a fish's up / down buttons (AdvancedFishAI's ControlsComponent)
const float b = kind == 3 ? (downKey ? e.ex6.x : (want_jump ? e.ex5.y : e.ex5.x)) : (fishDy > 2.0 ? e.ex6.x : (fishDy < -2.0 ? e.ex5.y : (want_jump && !grounded ? e.ex5.y : e.ex5.x)));
vel.x = e.ex5.w * (e.ex5.z * vel.x);
vel.y = e.ex5.z * (vel.y - b * G);
}
// 00c370c0 counters: only when the integer position changed, 3x3 count of liquid that is not powder; displacement 75 % of the time when on the ground
{
ivec2 last = ivec2(e.ex6.zw);
const ivec2 tp = ivec2(pos); // truncation toward zero (00c370c0)
if (tp != last) {
int n = 0;
for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) if (liquid_nonsand(tp + ivec2(dx, dy))) ++n;
ents[id].ex6.z = float(tp.x); ents[id].ex6.w = float(tp.y);
if (n > 0 && kind == 4) vel *= 1.0 - e.ex5.y * (1.0 / 60.0) * float(n); // 00c370c0: an item with a VelocityComponent loses liquid_drag * dt * N of its velocity (unclamped)
if (n > 0 && (kind == 1 || kind == 2) && !swimming && e.ex10.w > 0.0) vel *= 1.0 - e.ex10.w * (1.0 / 60.0) * float(n); // 00c370c0 liquid drag of creatures that do not swim (liquid_drag of VelocityComponent, default 1)
if (n > 0 && grounded) { // every qualifying cell: r % 101 < 75, then it leaves as a particle with -Rot(a) * 0.1 v, a = 0.6 u - 0.3 (the velocity of a cell per frame is the velocity per second / 60 for both)
for (int dy = 1; dy >= -1; --dy) for (int dx = -1; dx <= 1; ++dx) {
const uint rs = hash3(uint(id) * 9u + uint(dx + 1 + 3 * (dy + 1)), uint(pc.frame), 71u);
if (rs % 101u >= 75u) continue;
const float ang = 0.6 * float((rs >> 8) & 0x7FFFu) / 32767.0 - 0.3;
displace_cell(tp + ivec2(dx, dy), -(mat2(cos(ang), sin(ang), -sin(ang), cos(ang)) * (0.1 * vel)));
}
}
}
}
if (kind == 4) { const float sp = length(vel); if (sp > e.hp.z) vel *= e.hp.z / sp; } // 00c370c0: the terminal speed limits the vector length and rescales both axes
// horizontal move in one-cell steps, climbing small steps when standing
float nx = pos.x + vel.x;
int steps = int(floor(nx)) - ip.x;
int sx = steps > 0 ? 1 : -1;
bool blocked = false;
for (int s = 0; s < abs(steps); ++s) {
ivec2 np = ip + ivec2(sx, 0);
if (!box_hits(np, e.box)) { ip = np; continue; }
bool ok = false;
if (grounded) { // (a flyer skimming the floor climbs its small steps too: it stopped dead at a plank's edge)
for (int k = 1; k <= int(e.hp.w) && !ok; ++k) {
ivec2 cp = np - ivec2(0, k);
if (!box_hits(cp, e.box)) { ip = cp; ok = true; }
}
}
if (!ok) { blocked = true; break; }
}
pos.x = blocked ? float(ip.x) + fract(pos.x) : nx;
if (blocked && !(kind == 1 && (int(e.ex15.w) & 2) != 0)) vel.x = 0.0;
if (knocked) ents[id].ex10.z = blocked ? 2.0 : 1.0; // (mCollidedHorizontally for the knockback end) // (the lob of a path jump keeps its speed against a wall: it rises past a steep slope's edge and goes on, not straight up and back down where it started)
// vertical move
pos.y = float(ip.y) + fract(pos.y);
float ny = pos.y + vel.y;
int vsteps = int(floor(ny)) - ip.y;
int sy = vsteps > 0 ? 1 : -1;
bool hit = false;
for (int s = 0; s < abs(vsteps); ++s) {
ivec2 np = ip + ivec2(0, sy);
if (!box_hits(np, e.box)) { ip = np; continue; }
hit = true;
break;
}
if (hit) { pos.y = float(ip.y); vel.y = 0.0; } else pos.y = ny;
grounded = box_hits(ip + ivec2(0, 1), e.box);
if (wasGrounded && !grounded && (kind == 1 || kind == 3) && vel.y > -0.05 && vel.y < 0.6 && !swimming) { // walking down a slope: the body follows the ground (up to the climb height) instead of hopping from step to step
for (int k = 1; k <= int(max(e.hp.w, 4.0)); ++k)
if (!box_hits(ip + ivec2(0, k), e.box) && box_hits(ip + ivec2(0, k + 1), e.box)) { ip.y += k; pos.y = float(ip.y); vel.y = 0.0; grounded = true; break; }
}
// creatures: turn around at walls, hop over obstacles when they can jump - only the GPU's own fallback walkers: a creature the host runs (ex15.z) moves by its
// path alone (its jumps are the AnimalAI's prepared lobs; this random 3-in-4 hop at -jump_velocity_y was a second controller, docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-04)
if (kind == 1 && blocked && e.ex15.z <= 0.5) {
if (grounded && (flags & ST_CANJUMP) != 0 && (e.x.y == 0) && (rnd & 3u) != 0u) vel.y = e.mv.z;
else dir = -dir;
}
flags = (flags & ~(ST_GROUNDED | ST_LIQUID)) | (grounded ? ST_GROUNDED : 0) | (liquid ? ST_LIQUID : 0);
if ((e.st.w & 1024) != 0) { pos = e.a.xy; vel = e.a.zw; } // moved by the host (a worm: WormSystem): only its damage and death are handled here
ents[id].a = vec4(pos, vel);
ents[id].st = ivec4(kind, dir, timer, flags);
ents[id].hp = vec4(min(hp, kind == 3 && e.aux.x > 0 ? float(e.aux.x) * 0.001 : 1e9), cooldown, e.hp.z, e.hp.w);
ents[id].x.y = kind == 3 ? energy : e.x.y;
if (kind != 3) ents[id].x.z = e.x.z;
if (kind == 3) { ctrl.outp = vec4(pos, hp, float(dir)); ctrl.outp2 = vec4(float(energy) / ((e.ex2.x > 0.0 ? e.ex2.x : 180.0) * 64.0), cooldown, e.ex4.y > 0.0 ? ents[id].ex4.x / e.ex4.y : 1.0, ents[id].ex4.w > 0.0 ? 1.0 : 0.0); }
}