* 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
689 lines
51 KiB
Plaintext
689 lines
51 KiB
Plaintext
#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;
|
|
#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
|
|
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 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)
|
|
}
|
|
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 !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
|
|
// 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) {
|
|
// 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)
|
|
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.
|
|
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;
|
|
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)
|
|
// 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);
|
|
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 ((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 = 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 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(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) % 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
|
|
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 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(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);
|
|
}
|
|
|
|
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 && !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));
|
|
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;
|
|
}
|
|
}
|
|
// 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);
|
|
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); }
|
|
}
|