lingdev
↓ 0 downloadsLingDev: a Godot / Unity-style game editor written in Ling. 2-D and 3-D scenes, Flash-style timeline and cutscenes, HUDs over 3-D, Ling scripting, audio / music / MIDI, builds for Windows, Linux, macOS, web, Raspberry Pi and LingOS. Ships with Ling Agent (an N64-style spy FPS), Ling Breaker (a 3-D breakout) and Moko's Picture Book (a kawaii storybook platformer).
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdevCode / Files (136)
ling/rt/rt_guard.ling
# rt_guard.ling - the actors and effects of first-person stages: guards (patrol, alert, chase,
# shoot), their articulated animation (idle, walk, run, aim, fire, hit reactions by body zone,
# three kinds of death), ray tests against the level, bullet / blood / scorch decals, particles
# (blood, sparks, brass, debris), muzzle flashes and short-lived lights.
#
# Guards are actor nodes tagged `guard`; their node `ex` holds weapon=1..4, look=soldier|guard|
# scientist|officer and patrol=x_z_x_z... Their feet stand at the node position.
# ── store helpers ────────────────────────────────────────────────────────────
fn gget(k, f) { return st_lget("gd_" + f, k) }
fn gset(k, f, v) { st_lset("gd_" + f, k, v) }
fn gd_count() { return st_llen("gd_node") }
fn gd_fields() { return ["node", "hp", "st", "an", "at", "ph", "cd", "los", "lc", "lx", "lz", "pi", "hz", "ht", "df", "drop", "fl", "aim", "yaw", "shots", "pool", "hx", "hz0"] }
# ── rays ─────────────────────────────────────────────────────────────────────
# entry distance of a ray into an axis-aligned box, or -1
fn ray_box(ox, oy, oz, dx, dy, dz, x0, y0, z0, x1, y1, z1) {
bind tmin = 0.0 - 1000000.0
bind tmax = 1000000.0
if abs(dx) < 0.000001 {
if ox < x0 || ox > x1 { return 0.0 - 1.0 }
} else {
bind ta = (x0 - ox) / dx
bind tb = (x1 - ox) / dx
bind tmin = max(tmin, min(ta, tb))
bind tmax = min(tmax, max(ta, tb))
}
if abs(dy) < 0.000001 {
if oy < y0 || oy > y1 { return 0.0 - 1.0 }
} else {
bind ta = (y0 - oy) / dy
bind tb = (y1 - oy) / dy
bind tmin = max(tmin, min(ta, tb))
bind tmax = min(tmax, max(ta, tb))
}
if abs(dz) < 0.000001 {
if oz < z0 || oz > z1 { return 0.0 - 1.0 }
} else {
bind ta = (z0 - oz) / dz
bind tb = (z1 - oz) / dz
bind tmin = max(tmin, min(ta, tb))
bind tmax = min(tmax, max(ta, tb))
}
if tmax < max(tmin, 0.0) { return 0.0 - 1.0 }
if tmin < 0.0 { return 0.0 }
return tmin
}
fn ray_sphere(ox, oy, oz, dx, dy, dz, cx, cy, cz, r) {
bind lx = ox - cx
bind ly = oy - cy
bind lz = oz - cz
bind b = lx * dx + ly * dy + lz * dz
bind c = lx * lx + ly * ly + lz * lz - r * r
bind h = b * b - c
if h < 0.0 { return 0.0 - 1.0 }
bind t = 0.0 - b - sqrt(h)
if t < 0.0 { return 0.0 - 1.0 }
return t
}
# the level nodes rays and walkers collide with
fn fx_is_solid(i) {
if nd_get(i, "vis") < 0.5 { return false }
if nd_get(i, "kind") != "box" { return false }
bind tag = nd_get(i, "tag")
return tag == "solid" || tag == "door" || tag == "console" || tag == "wreck" || tag == "glass"
}
fn fx_collect_solids() {
st_lclear("fx_solids")
bind n = nd_count()
bind i = 0.0
while i < n {
if nd_get(i, "kind") == "box" {
bind tag = nd_get(i, "tag")
if tag == "solid" || tag == "door" || tag == "console" || tag == "wreck" || tag == "glass" { st_lpush("fx_solids", i) }
}
bind i = i + 1.0
}
}
# nearest level hit along a ray: [t, node row (-2 floor, -1 none), face 0..5 = -x +x -y +y -z +z]
fn ray_world(ox, oy, oz, dx, dy, dz, maxt) {
bind best = maxt
bind bi = 0.0 - 1.0
bind n = st_llen("fx_solids")
bind k = 0.0
while k < n {
bind i = st_lget("fx_solids", k)
if nd_get(i, "vis") > 0.5 {
bind x = nd_get(i, "px")
bind y = nd_get(i, "py")
bind z = nd_get(i, "pz")
bind sx = nd_get(i, "sx")
bind sy = nd_get(i, "sy")
bind sz = nd_get(i, "sz")
bind t = ray_box(ox, oy, oz, dx, dy, dz, x - sx, y - sy, z - sz, x + sx, y + sy, z + sz)
if t >= 0.0 && t < best { bind best = t bind bi = i }
}
bind k = k + 1.0
}
bind fl = sc_get("floor", 0.0)
if dy > 0.0001 {
bind tf = (fl - oy) / dy
if tf >= 0.0 && tf < best { return [tf, 0.0 - 2.0, 2.0] }
}
if bi < 0.0 { return [maxt, 0.0 - 1.0, 0.0] }
# which face: the one the hit point lies closest to
bind hx = ox + dx * best
bind hy = oy + dy * best
bind hz = oz + dz * best
bind x = nd_get(bi, "px")
bind y = nd_get(bi, "py")
bind z = nd_get(bi, "pz")
bind sx = nd_get(bi, "sx")
bind sy = nd_get(bi, "sy")
bind sz = nd_get(bi, "sz")
bind dl = [abs(hx - (x - sx)), abs(hx - (x + sx)), abs(hy - (y - sy)), abs(hy - (y + sy)), abs(hz - (z - sz)), abs(hz - (z + sz))]
bind face = 0.0
bind m = list_get(dl, 0)
bind f = 1.0
while f < 6.0 {
if list_get(dl, f) < m { bind m = list_get(dl, f) bind face = f }
bind f = f + 1.0
}
return [best, bi, face]
}
# clear line between two points?
fn line_clear(ax, ay, az, bx, by, bz) {
bind dx = bx - ax
bind dy = by - ay
bind dz = bz - az
bind d = sqrt(dx * dx + dy * dy + dz * dz)
if d < 0.001 { return true }
bind h = ray_world(ax, ay, az, dx / d, dy / d, dz / d, d)
return list_get(h, 1) == 0.0 - 1.0
}
# can a walker of radius r stand at (x, z)?
fn fx_free(x, z, r, feet_y) {
bind n = st_llen("fx_solids")
bind k = 0.0
while k < n {
bind i = st_lget("fx_solids", k)
if nd_get(i, "vis") > 0.5 {
bind px = nd_get(i, "px")
bind pz = nd_get(i, "pz")
if abs(x - px) < nd_get(i, "sx") + r && abs(z - pz) < nd_get(i, "sz") + r {
bind top = nd_get(i, "py") - nd_get(i, "sy")
bind bot = nd_get(i, "py") + nd_get(i, "sy")
if bot > feet_y - 1.7 && top < feet_y - 0.35 { return false }
}
}
bind k = k + 1.0
}
return true
}
# ── effects: decals, particles, lights ───────────────────────────────────────
fn fx_reset() {
bind keys = ["dc_x", "dc_y", "dc_z", "dc_f", "dc_k", "dc_s", "dc_t", "pt_x", "pt_y", "pt_z", "pt_vx", "pt_vy", "pt_vz", "pt_l", "pt_k", "fl_x", "fl_y", "fl_z", "fl_c", "fl_i", "fl_r", "fl_l"]
bind i = 0.0
while i < len(keys) {
st_lclear(list_get(keys, i))
bind i = i + 1.0
}
st_set("dc_next", 0.0)
}
# kind 0 bullet hole, 1 blood, 2 scorch, 3 blood pool (grows)
fn fx_decal(x, y, z, face, kind, size) {
bind n = st_llen("dc_x")
bind off = 0.012 + n * 0.0004
bind nx = 0.0
bind ny = 0.0
bind nz = 0.0
if face == 0.0 { bind nx = 0.0 - off }
if face == 1.0 { bind nx = off }
if face == 2.0 { bind ny = 0.0 - off }
if face == 3.0 { bind ny = off }
if face == 4.0 { bind nz = 0.0 - off }
if face == 5.0 { bind nz = off }
if n < 64.0 {
st_lpush("dc_x", x + nx)
st_lpush("dc_y", y + ny)
st_lpush("dc_z", z + nz)
st_lpush("dc_f", face)
st_lpush("dc_k", kind)
st_lpush("dc_s", size)
st_lpush("dc_t", st_get("game_t", 0.0))
return 0.0
}
bind k = st_get("dc_next", 0.0) % 64.0
st_lset("dc_x", k, x + nx)
st_lset("dc_y", k, y + ny)
st_lset("dc_z", k, z + nz)
st_lset("dc_f", k, face)
st_lset("dc_k", k, kind)
st_lset("dc_s", k, size)
st_lset("dc_t", k, st_get("game_t", 0.0))
st_set("dc_next", k + 1.0)
return 0.0
}
# kind 0 blood, 1 spark, 2 brass, 3 debris, 4 smoke
fn fx_burst(x, y, z, n, kind, dx, dy, dz, speed, spread) {
bind i = 0.0
while i < n {
if st_llen("pt_x") >= 90.0 {
st_ldel("pt_x", 0.0) st_ldel("pt_y", 0.0) st_ldel("pt_z", 0.0)
st_ldel("pt_vx", 0.0) st_ldel("pt_vy", 0.0) st_ldel("pt_vz", 0.0)
st_ldel("pt_l", 0.0) st_ldel("pt_k", 0.0)
}
bind s = speed * (0.5 + rand())
st_lpush("pt_x", x)
st_lpush("pt_y", y)
st_lpush("pt_z", z)
st_lpush("pt_vx", dx * s + (rand() - 0.5) * spread)
st_lpush("pt_vy", dy * s + (rand() - 0.5) * spread)
st_lpush("pt_vz", dz * s + (rand() - 0.5) * spread)
st_lpush("pt_l", sel(kind == 2.0, 1.6, sel(kind == 4.0, 1.2, 0.5 + rand() * 0.6)))
st_lpush("pt_k", kind)
bind i = i + 1.0
}
}
fn fx_light(x, y, z, c, intensity, radius, life) {
st_lpush("fl_x", x)
st_lpush("fl_y", y)
st_lpush("fl_z", z)
st_lpush("fl_c", c)
st_lpush("fl_i", intensity)
st_lpush("fl_r", radius)
st_lpush("fl_l", life)
}
fn fx_update(dt) {
bind fl = sc_get("floor", 0.0)
bind g = 9.8
bind i = st_llen("pt_x") - 1.0
while i >= 0.0 {
bind l = st_lget("pt_l", i) - dt
if l <= 0.0 {
st_ldel("pt_x", i) st_ldel("pt_y", i) st_ldel("pt_z", i)
st_ldel("pt_vx", i) st_ldel("pt_vy", i) st_ldel("pt_vz", i)
st_ldel("pt_l", i) st_ldel("pt_k", i)
} else {
bind k = st_lget("pt_k", i)
bind vy = st_lget("pt_vy", i) + sel(k == 4.0, 0.0 - 0.6, g) * dt
bind y = st_lget("pt_y", i) + vy * dt
bind vx = st_lget("pt_vx", i)
bind vz = st_lget("pt_vz", i)
if y > fl - 0.01 && k != 4.0 {
bind y = fl - 0.01
if k == 2.0 { bind vy = 0.0 - vy * 0.35 bind vx = vx * 0.6 bind vz = vz * 0.6 } else { bind vy = 0.0 bind vx = vx * 0.3 bind vz = vz * 0.3 }
if k == 0.0 && st_lget("pt_l", i) > 0.15 { st_lset("pt_l", i, 0.12) }
}
st_lset("pt_x", i, st_lget("pt_x", i) + vx * dt)
st_lset("pt_y", i, y)
st_lset("pt_z", i, st_lget("pt_z", i) + vz * dt)
st_lset("pt_vx", i, vx)
st_lset("pt_vy", i, vy)
st_lset("pt_vz", i, vz)
st_lset("pt_l", i, l)
}
bind i = i - 1.0
}
bind i = st_llen("fl_x") - 1.0
while i >= 0.0 {
bind l = st_lget("fl_l", i) - dt
if l <= 0.0 {
st_ldel("fl_x", i) st_ldel("fl_y", i) st_ldel("fl_z", i) st_ldel("fl_c", i)
st_ldel("fl_i", i) st_ldel("fl_r", i) st_ldel("fl_l", i)
} else { st_lset("fl_l", i, l) }
bind i = i - 1.0
}
}
fn fx_lights_apply() {
bind i = 0.0
while i < st_llen("fl_x") {
bind c = st_lget("fl_c", i)
plat_light(st_lget("fl_x", i), st_lget("fl_y", i), st_lget("fl_z", i), c_r(c), c_g(c), c_b(c), st_lget("fl_i", i), st_lget("fl_r", i))
bind i = i + 1.0
}
}
fn fx_box(c, x, y, z, sx, sy, sz, rx, ry) {
plat_color(c)
plat_shape("box", x, y, z, sx, sy, sz, rx, ry, 0.0, 0.0)
}
# a flat square on a face, thin along the face normal
fn fx_flat(c, x, y, z, face, s) {
if face < 1.5 { fx_box(c, x, y, z, 0.006, s, s, 0.0, 0.0) }
else if face < 3.5 { fx_box(c, x, y, z, s, 0.006, s, 0.0, 0.0) }
else { fx_box(c, x, y, z, s, s, 0.006, 0.0, 0.0) }
}
fn fx_draw_native() {
bind now = st_get("game_t", 0.0)
bind i = 0.0
while i < st_llen("dc_x") {
bind x = st_lget("dc_x", i)
bind y = st_lget("dc_y", i)
bind z = st_lget("dc_z", i)
bind f = st_lget("dc_f", i)
bind k = st_lget("dc_k", i)
bind s = st_lget("dc_s", i)
if k == 0.0 {
fx_flat(c_rgb(22.0, 20.0, 18.0), x, y, z, f, s)
} else if k == 1.0 {
fx_flat(c_rgb(110.0, 4.0, 6.0), x, y, z, f, s)
fx_flat(c_rgb(90.0, 0.0, 4.0), x + sel(f < 1.5, 0.0, s * 0.6), y + s * 0.5, z + sel(f > 3.5, 0.0, s * 0.4), f, s * 0.45)
fx_flat(c_rgb(130.0, 10.0, 10.0), x - sel(f < 1.5, 0.0, s * 0.5), y + s * 0.9, z - sel(f > 3.5, 0.0, s * 0.3), f, s * 0.3)
} else if k == 2.0 {
fx_flat(c_rgb(12.0, 10.0, 10.0), x, y, z, f, s)
fx_flat(c_rgb(35.0, 30.0, 26.0), x, y, z, f, s * 1.4)
} else {
bind grow = clampf((now - st_lget("dc_t", i)) / 4.0, 0.08, 1.0)
fx_flat(c_rgb(95.0, 2.0, 4.0), x, y, z, f, s * grow)
fx_flat(c_rgb(70.0, 0.0, 2.0), x + s * 0.3 * grow, y, z - s * 0.2 * grow, f, s * 0.7 * grow)
}
bind i = i + 1.0
}
bind i = 0.0
while i < st_llen("pt_x") {
bind k = st_lget("pt_k", i)
bind x = st_lget("pt_x", i)
bind y = st_lget("pt_y", i)
bind z = st_lget("pt_z", i)
if k == 0.0 { fx_box(c_rgb(150.0, 6.0, 8.0), x, y, z, 0.035, 0.035, 0.035, 0.0, 0.0) }
else if k == 1.0 { fx_box(c_rgb(255.0, 220.0, 120.0), x, y, z, 0.02, 0.02, 0.02, 0.0, 0.0) }
else if k == 2.0 { fx_box(c_rgb(200.0, 160.0, 60.0), x, y, z, 0.012, 0.012, 0.03, st_lget("pt_l", i) * 9.0, st_lget("pt_l", i) * 5.0) }
else if k == 3.0 { fx_box(c_rgb(90.0, 90.0, 95.0), x, y, z, 0.06, 0.06, 0.06, st_lget("pt_l", i) * 4.0, st_lget("pt_l", i) * 3.0) }
else { bind sz = 0.15 + (1.2 - st_lget("pt_l", i)) * 0.4 fx_box(c_rgb(60.0, 60.0, 64.0), x, y, z, sz, sz, sz, st_lget("pt_l", i), 0.0) }
bind i = i + 1.0
}
}
# a starburst muzzle flash at (x,y,z) facing yaw / pitch
fn fx_flash(x, y, z, yaw, pitch, s) {
plat_blend(1)
bind spin = rand() * 3.14
fx_box(c_rgb(255.0, 230.0, 140.0), x, y, z, s * 0.35, s * 0.35, s * 0.35, spin, yaw)
fx_box(c_rgb(255.0, 160.0, 40.0), x, y, z, s * 0.08, s, s * 0.08, spin, yaw)
fx_box(c_rgb(255.0, 160.0, 40.0), x, y, z, s, s * 0.08, s * 0.08, spin, yaw)
fx_box(c_rgb(255.0, 200.0, 80.0), x, y, z, s * 0.1, s * 0.1, s * 0.9, 0.0 - pitch, yaw)
plat_blend(0)
}
# ── guards ───────────────────────────────────────────────────────────────────
fn gd_reset() {
bind f = gd_fields()
bind i = 0.0
while i < len(f) {
st_lclear("gd_" + list_get(f, i))
bind i = i + 1.0
}
bind n = nd_count()
bind i = 0.0
while i < n {
if nd_get(i, "kind") == "actor" && nd_get(i, "tag") == "guard" {
st_lpush("gd_node", i)
st_lpush("gd_hp", 100.0 * sel(fps_diff() > 1.5, 1.3, 1.0))
st_lpush("gd_st", 0.0)
st_lpush("gd_an", "idle")
st_lpush("gd_at", 0.0)
st_lpush("gd_ph", rand() * 6.28)
st_lpush("gd_cd", 0.0)
st_lpush("gd_los", 0.0)
st_lpush("gd_lc", rand() * 0.3)
st_lpush("gd_lx", nd_get(i, "px"))
st_lpush("gd_lz", nd_get(i, "pz"))
st_lpush("gd_pi", 0.0)
st_lpush("gd_hz", 0.0)
st_lpush("gd_ht", 0.0)
st_lpush("gd_df", 1.0)
st_lpush("gd_drop", 0.0)
st_lpush("gd_fl", 0.0)
st_lpush("gd_aim", 0.0)
st_lpush("gd_yaw", nd_get(i, "ry"))
st_lpush("gd_shots", 0.0)
st_lpush("gd_pool", 0.0)
st_lpush("gd_hx", nd_get(i, "px"))
st_lpush("gd_hz0", nd_get(i, "pz"))
}
bind i = i + 1.0
}
}
fn gd_weapon(k) { return clampf(to_number(nd_ex(gget(k, "node"), "weapon", "1")), 1.0, 4.0) }
fn gd_alive(k) { return gget(k, "st") < 1.5 }
fn gd_alive_count() {
bind c = 0.0
bind k = 0.0
while k < gd_count() {
if gd_alive(k) { bind c = c + 1.0 }
bind k = k + 1.0
}
return c
}
# alert every living guard within `r` of (x, z)
fn gd_alert_near(x, z, r) {
bind k = 0.0
while k < gd_count() {
if gget(k, "st") < 0.5 {
bind i = gget(k, "node")
bind dx = nd_get(i, "px") - x
bind dz = nd_get(i, "pz") - z
if dx * dx + dz * dz < r * r {
gset(k, "st", 1.0)
gset(k, "lx", x)
gset(k, "lz", z)
gset(k, "aim", 0.6)
}
}
bind k = k + 1.0
}
}
fn gd_set_anim(k, a) {
if gget(k, "an") != a {
gset(k, "an", a)
gset(k, "at", 0.0)
}
}
# walk toward (tx, tz); returns the distance left
fn gd_walk_to(k, tx, tz, speed, dt) {
bind i = gget(k, "node")
bind x = nd_get(i, "px")
bind z = nd_get(i, "pz")
bind dx = tx - x
bind dz = tz - z
bind d = sqrt(dx * dx + dz * dz)
if d < 0.05 { return d }
bind step = min(d, speed * dt)
bind nx = x + dx / d * step
bind nz = z + dz / d * step
bind fy = nd_get(i, "py")
if fx_free(nx, nz, 0.3, fy) { nd_set(i, "px", nx) nd_set(i, "pz", nz) }
else if fx_free(nx, z, 0.3, fy) { nd_set(i, "px", nx) }
else if fx_free(x, nz, 0.3, fy) { nd_set(i, "pz", nz) }
gd_face(k, atan2(dx, dz), dt * 6.0)
return d - step
}
fn gd_face(k, target, rate) {
bind y = gget(k, "yaw")
bind d = target - y
while d > 3.14159 { bind d = d - 6.28318 }
while d < 0.0 - 3.14159 { bind d = d + 6.28318 }
bind y = y + d * min(1.0, rate)
gset(k, "yaw", y)
nd_set(gget(k, "node"), "ry", y)
}
fn gd_patrol_points(k) {
bind raw = str_split(nd_ex(gget(k, "node"), "patrol", ""), "_")
bind out = []
bind i = 0.0
while i < len(raw) {
if str_trim(list_get(raw, i)) != "" { bind out = list_push(out, to_number(list_get(raw, i))) }
bind i = i + 1.0
}
return out
}
fn gd_update_all(dt, px, py, pz, eye_y) {
bind k = 0.0
while k < gd_count() {
gd_update(k, dt, px, py, pz, eye_y)
bind k = k + 1.0
}
}
fn gd_update(k, dt, px, py, pz, eye_y) {
bind i = gget(k, "node")
gset(k, "at", gget(k, "at") + dt)
gset(k, "fl", max(0.0, gget(k, "fl") - dt))
gset(k, "ht", max(0.0, gget(k, "ht") - dt))
if gget(k, "st") > 1.5 {
# dying: drop the weapon once down, then a growing pool of blood
if gget(k, "at") > 0.9 && gget(k, "drop") < 0.5 {
gset(k, "drop", 1.0)
bind fall = gget(k, "df")
bind yaw = gget(k, "yaw")
bind bx = nd_get(i, "px") - sin(yaw) * fall * 0.9
bind bz = nd_get(i, "pz") - cos(yaw) * fall * 0.9
fx_decal(bx, sc_get("floor", 0.0), bz, 2.0, 3.0, 0.55 + rand() * 0.25)
bind w = gd_weapon(k)
bind pk = game_spawn("box", "Dropped " + fps_wname(w), nd_get(i, "px") + sin(yaw) * 0.6, sc_get("floor", 0.0) - 0.15, nd_get(i, "pz") + cos(yaw) * 0.6)
nd_set(pk, "tag", "pickup")
nd_set(pk, "sx", 0.15) nd_set(pk, "sy", 0.15) nd_set(pk, "sz", 0.15)
nd_set(pk, "ex", "item=" + fps_wkey(w) + ";amount=" + num_str(fps_wget(w, "mag")))
}
return 0.0
}
bind x = nd_get(i, "px")
bind z = nd_get(i, "pz")
bind fy = nd_get(i, "py")
bind dxp = px - x
bind dzp = pz - z
bind dist = sqrt(dxp * dxp + dzp * dzp)
# line of sight, a few times a second
gset(k, "lc", gget(k, "lc") - dt)
if gget(k, "lc") <= 0.0 {
gset(k, "lc", 0.25 + rand() * 0.1)
bind see = false
if dist < 34.0 && st_get("fp_dead", 0.0) < 0.5 {
bind ang = atan2(dxp, dzp) - gget(k, "yaw")
while ang > 3.14159 { bind ang = ang - 6.28318 }
while ang < 0.0 - 3.14159 { bind ang = ang + 6.28318 }
bind in_view = abs(ang) < 1.1 || gget(k, "st") > 0.5 || dist < 2.5
if in_view { bind see = line_clear(x, fy - 1.6, z, px, eye_y, pz) }
}
gset(k, "los", bool_num(see))
if see {
if gget(k, "st") < 0.5 { gset(k, "aim", 0.7 - fps_diff() * 0.15) }
gset(k, "st", 1.0)
gset(k, "lx", px)
gset(k, "lz", pz)
}
}
bind spd_walk = 1.4
bind spd_run = 4.2 + fps_diff() * 0.4
if gget(k, "ht") > 0.0 {
gd_set_anim(k, "hit")
return 0.0
}
if gget(k, "st") < 0.5 {
bind pts = gd_patrol_points(k)
if len(pts) >= 4.0 {
bind n = floor(len(pts) / 2.0)
bind pi = gget(k, "pi") % n
bind left = gd_walk_to(k, list_get(pts, pi * 2.0), list_get(pts, pi * 2.0 + 1.0), spd_walk, dt)
gset(k, "ph", gget(k, "ph") + dt * 6.2)
gd_set_anim(k, "walk")
if left < 0.15 { gset(k, "pi", pi + 1.0) }
} else {
gd_set_anim(k, "idle")
# look around now and then
if floor(gget(k, "at") / 4.0) % 2.0 > 0.5 { gd_face(k, nd_get(i, "ry") + sin(gget(k, "at")) * 0.02, dt) }
}
return 0.0
}
# alerted
bind wp = gd_weapon(k)
if gget(k, "los") > 0.5 && dist < 26.0 {
gd_face(k, atan2(dxp, dzp), dt * 8.0)
if dist > 12.0 || (dist > 5.0 && fps_diff() > 1.5 && floor(gget(k, "at") / 2.0) % 3.0 == 2.0) {
gd_walk_to(k, px, pz, spd_run * 0.75, dt)
gset(k, "ph", gget(k, "ph") + dt * 11.0)
gd_set_anim(k, "run")
} else {
gd_set_anim(k, "aim")
gset(k, "aim", gget(k, "aim") - dt)
gset(k, "cd", gget(k, "cd") - dt)
if gget(k, "aim") <= 0.0 && gget(k, "cd") <= 0.0 { gd_fire(k, px, py, pz, eye_y, dist) }
}
} else {
# chase the last place the player was seen, then keep hunting
bind left = gd_walk_to(k, gget(k, "lx"), gget(k, "lz"), spd_run, dt)
gset(k, "ph", gget(k, "ph") + dt * 11.0)
gd_set_anim(k, "run")
if left < 0.4 {
gd_set_anim(k, "idle")
if gget(k, "at") > 2.5 { gset(k, "lx", px) gset(k, "lz", pz) }
}
}
return 0.0
}
fn gd_gun_tip(k) {
bind i = gget(k, "node")
bind yaw = gget(k, "yaw")
bind x = nd_get(i, "px") + sin(yaw) * 0.62 + cos(yaw) * 0.2
bind z = nd_get(i, "pz") + cos(yaw) * 0.62 - sin(yaw) * 0.2
return [x, nd_get(i, "py") - 1.38, z]
}
fn gd_fire(k, px, py, pz, eye_y, dist) {
bind w = gd_weapon(k)
bind diff = fps_diff()
bind rate = list_get([0.0, 0.7, 0.13, 0.17, 1.1], w) * list_get([1.7, 1.0, 0.72], diff)
gset(k, "cd", rate)
gset(k, "shots", gget(k, "shots") + 1.0)
if (w == 2.0 || w == 3.0) && gget(k, "shots") % 6.0 == 0.0 { gset(k, "cd", rate + 0.9) }
gset(k, "fl", 0.06)
bind tip = gd_gun_tip(k)
bind tx = list_get(tip, 0)
bind ty = list_get(tip, 1)
bind tz = list_get(tip, 2)
fx_light(tx, ty, tz, c_rgb(255.0, 190.0, 90.0), 2.2, 7.0, 0.06)
plat_sfx(tx, ty, tz, 6.0, sel(w == 4.0, 260.0, 700.0), sel(w == 4.0, 0.45, 0.32), sel(w == 4.0, 0.22, 0.09), "noise")
bind moving = st_get("fp_speed", 0.0) > 0.5
bind acc = list_get([0.16, 0.28, 0.42], diff) * clampf(1.25 - dist / 22.0, 0.12, 1.0) * sel(moving, 0.7, 1.0)
if w == 4.0 { bind acc = acc * clampf(1.6 - dist / 10.0, 0.0, 1.4) }
if rand() < acc {
bind dmg = list_get([0.0, 11.0, 7.0, 9.0, 22.0], w) * list_get([0.5, 1.0, 1.5], diff)
fps_hurt(dmg, list_get(tip, 0), list_get(tip, 2))
} else {
# the miss lands on whatever is behind the player
bind ax = px + (rand() - 0.5) * 2.4 - tx
bind ay = eye_y + (rand() - 0.5) * 1.6 - ty
bind az = pz + (rand() - 0.5) * 2.4 - tz
bind l = sqrt(ax * ax + ay * ay + az * az)
bind h = ray_world(tx, ty, tz, ax / l, ay / l, az / l, 60.0)
if list_get(h, 1) != 0.0 - 1.0 {
bind t = list_get(h, 0)
bind hx = tx + ax / l * t
bind hy = ty + ay / l * t
bind hz = tz + az / l * t
fx_decal(hx, hy, hz, list_get(h, 2), 0.0, 0.035 + rand() * 0.015)
fx_burst(hx, hy, hz, 3.0, 1.0, 0.0 - ax / l, 0.0 - ay / l, 0.0 - az / l, 2.0, 2.0)
plat_sfx(hx, hy, hz, 4.0, 2400.0, 0.12, 0.04, "noise")
}
}
}
# the bullet reached guard k in body zone z (0 head, 1 torso, 2 legs)
fn gd_hit(k, zone, dmg, dx, dy, dz) {
if !gd_alive(k) { return 0.0 }
bind i = gget(k, "node")
bind mult = list_get([5.0, 1.0, 0.65], zone)
bind hp = gget(k, "hp") - dmg * mult
gset(k, "hp", hp)
gset(k, "hz", zone)
gset(k, "st", 1.0)
gset(k, "lx", st_get("fp_x", 0.0))
gset(k, "lz", st_get("fp_z", 0.0))
bind hy = nd_get(i, "py") - list_get([1.62, 1.2, 0.5], zone)
fx_burst(nd_get(i, "px"), hy, nd_get(i, "pz"), 7.0, 0.0, dx, dy * 0.3 - 0.4, dz, 2.2, 1.8)
plat_sfx(nd_get(i, "px"), hy, nd_get(i, "pz"), 5.0, 160.0, 0.3, 0.08, "triangle")
# blood on the wall behind
bind h = ray_world(nd_get(i, "px"), hy, nd_get(i, "pz"), dx, dy, dz, 3.5)
if list_get(h, 1) != 0.0 - 1.0 {
bind t = list_get(h, 0)
fx_decal(nd_get(i, "px") + dx * t, hy + dy * t, nd_get(i, "pz") + dz * t, list_get(h, 2), 1.0, 0.16 + rand() * 0.12)
}
if hp <= 0.0 {
gset(k, "st", 2.0)
gd_set_anim(k, "die")
# fall away from the shot; a leg shot folds forward
bind away = (dx * sin(gget(k, "yaw")) + dz * cos(gget(k, "yaw")))
bind dir = sel(away > 0.0, 0.0 - 1.0, 1.0)
if zone == 2.0 { bind dir = 0.0 - 1.0 }
if zone == 1.0 && rand() < 0.35 { bind dir = 0.0 }
gset(k, "df", dir)
st_set("fp_kills", st_get("fp_kills", 0.0) + 1.0)
if zone == 0.0 { st_set("fp_heads", st_get("fp_heads", 0.0) + 1.0) }
return 1.0
}
gset(k, "ht", 0.32)
gset(k, "aim", max(gget(k, "aim"), 0.35))
return 0.0
}
# ray against guard k standing up: [t, zone] or [-1, 0]
fn gd_ray(k, ox, oy, oz, dx, dy, dz) {
if !gd_alive(k) { return [0.0 - 1.0, 0.0] }
bind i = gget(k, "node")
bind x = nd_get(i, "px")
bind y = nd_get(i, "py")
bind z = nd_get(i, "pz")
bind best = 0.0 - 1.0
bind zone = 0.0
bind th = ray_sphere(ox, oy, oz, dx, dy, dz, x, y - 1.63, z, 0.15)
if th >= 0.0 { bind best = th }
bind tt = ray_box(ox, oy, oz, dx, dy, dz, x - 0.24, y - 1.5, z - 0.24, x + 0.24, y - 0.92, z + 0.24)
if tt >= 0.0 && (best < 0.0 || tt < best - 0.05) { bind best = tt bind zone = 1.0 }
bind tl = ray_box(ox, oy, oz, dx, dy, dz, x - 0.22, y - 0.92, z - 0.2, x + 0.22, y, z + 0.2)
if tl >= 0.0 && (best < 0.0 || tl < best) { bind best = tl bind zone = 2.0 }
return [best, zone]
}
# ── guard animation and drawing ──────────────────────────────────────────────
# pose: [thighL, kneeL, thighR, kneeR, armL, elbowL, armR, elbowR, lean, head, bob, fall]
fn gd_pose(k) {
bind an = gget(k, "an")
bind at = gget(k, "at")
bind ph = gget(k, "ph")
bind t = st_get("game_t", 0.0) + gget(k, "node") * 0.7
if an == "walk" {
bind s = sin(ph)
return [s * 0.45, 0.1 + max(0.0, 0.0 - cos(ph)) * 0.55, 0.0 - s * 0.45, 0.1 + max(0.0, cos(ph)) * 0.55, 0.0 - s * 0.35, 0.2, 0.45 + s * 0.08, 0.9, 0.0 - 0.04, 0.0, abs(s) * 0.03, 0.0]
}
if an == "run" {
bind s = sin(ph)
return [s * 0.85, 0.25 + max(0.0, 0.0 - cos(ph)) * 1.2, 0.0 - s * 0.85, 0.25 + max(0.0, cos(ph)) * 1.2, 0.0 - s * 0.7, 1.2, 0.0 - s * 0.5 + 0.4, 1.3, 0.0 - 0.28, 0.12, abs(s) * 0.07, 0.0]
}
if an == "aim" {
bind kick = gget(k, "fl") * 4.0
return [0.15, 0.12, 0.0 - 0.18, 0.08, 1.25, 0.45, 1.57 - kick, 0.0, 0.0 - 0.05 + kick * 0.2, sin(t * 0.7) * 0.03, 0.0, 0.0]
}
if an == "hit" {
bind z = gget(k, "hz")
bind u = gget(k, "ht") / 0.32
if z == 0.0 { return [0.1, 0.1, 0.0 - 0.1, 0.1, 0.3, 0.4, 0.8, 0.6, 0.3 * u, 0.7 * u, 0.0, 0.0] }
if z == 2.0 { return [0.0 - 0.3 * u, 0.9 * u, 0.2, 0.2, 0.9 * u, 0.3, 0.6, 0.8, 0.0 - 0.35 * u, 0.0 - 0.2 * u, 0.0 - 0.12 * u, 0.0] }
return [0.1, 0.2, 0.0 - 0.1, 0.2, 0.9 * u, 0.8, 0.2 + 0.6 * u, 0.5, 0.45 * u, 0.25 * u, 0.0, 0.0]
}
if an == "die" {
bind dir = gget(k, "df")
bind u = ease("quad_in", clampf(at / 0.8, 0.0, 1.0))
bind settle = sel(at > 0.8, sin(clampf(at - 0.8, 0.0, 0.25) * 12.56) * 0.04, 0.0)
if dir == 0.0 {
# crumple: knees give, then the body tips forward
bind kn = ease("quad_out", clampf(at / 0.45, 0.0, 1.0)) * 1.9
bind f2 = ease("quad_in", clampf((at - 0.35) / 0.6, 0.0, 1.0))
return [0.0 - 0.6 * kn / 1.9, kn, 0.0 - 0.5 * kn / 1.9, kn * 0.9, 0.3, 0.4, 0.2, 0.3, 0.0 - 0.4 * f2, 0.3, 0.0 - 0.0 + kn * 0.28, 0.0 - 1.35 * f2 + settle]
}
return [0.2 * u, 0.3 * u, 0.0 - 0.1 * u, 0.5 * u, 1.6 * u, 0.2, 2.2 * u, 0.1, 0.25 * u * dir, 0.5 * u, 0.0, 1.48 * u * dir + settle]
}
# idle: breathing, gun held low, weight shifting
return [0.04, 0.05, 0.0 - 0.04, 0.05, 0.08 + sin(t * 1.3) * 0.03, 0.15, 0.55, 0.85, sin(t * 1.6) * 0.025, sin(t * 0.45) * 0.08, 0.0, 0.0]
}
fn gd_colors(look) {
# [jacket, trousers, head, hat, boots]
if look == "scientist" { return [c_rgb(225.0, 225.0, 218.0), c_rgb(90.0, 92.0, 100.0), c_rgb(225.0, 185.0, 150.0), c_rgb(70.0, 50.0, 40.0), c_rgb(30.0, 30.0, 30.0)] }
if look == "guard" { return [c_rgb(70.0, 82.0, 104.0), c_rgb(52.0, 60.0, 78.0), c_rgb(215.0, 172.0, 135.0), c_rgb(40.0, 44.0, 60.0), c_rgb(20.0, 20.0, 22.0)] }
if look == "officer" { return [c_rgb(60.0, 64.0, 58.0), c_rgb(48.0, 50.0, 46.0), c_rgb(220.0, 178.0, 140.0), c_rgb(150.0, 30.0, 30.0), c_rgb(15.0, 15.0, 15.0)] }
return [c_rgb(86.0, 98.0, 58.0), c_rgb(72.0, 82.0, 48.0), c_rgb(210.0, 170.0, 130.0), c_rgb(66.0, 76.0, 44.0), c_rgb(28.0, 24.0, 20.0)]
}
# body-local point -> world, through the fall (about the feet) and the yaw
fn gd_w(gx, gy, gz, fall, yaw, lx, ly, lz) {
bind p = r3_rot(lx, ly, lz, fall, yaw, 0.0)
return [gx + list_get(p, 0), gy + list_get(p, 1), gz + list_get(p, 2)]
}
# a limb segment of length L from pivot (px,py,pz) at angle a (about X); draws it, returns its end
fn gd_limb(c, gx, gy, gz, fall, yaw, px, py, pz, a, L, hw, hd) {
bind cy = py + cos(a) * L * 0.5
bind cz = pz + sin(a) * L * 0.5
bind w = gd_w(gx, gy, gz, fall, yaw, px, cy, cz)
fx_box(c, list_get(w, 0), list_get(w, 1), list_get(w, 2), hw, L * 0.5, hd, a + fall, yaw)
return [px, py + cos(a) * L, pz + sin(a) * L]
}
fn gd_draw(k) {
bind i = gget(k, "node")
bind gx = nd_get(i, "px")
bind gy = nd_get(i, "py")
bind gz = nd_get(i, "pz")
bind yaw = gget(k, "yaw")
bind p = gd_pose(k)
bind fall = list_get(p, 11)
bind lean = list_get(p, 8)
bind bob = list_get(p, 10)
bind cols = gd_colors(nd_ex(i, "look", "soldier"))
bind cj = list_get(cols, 0)
bind ct = list_get(cols, 1)
bind ch = list_get(cols, 2)
bind chat = list_get(cols, 3)
bind cb = list_get(cols, 4)
bind hip = 0.0 - 0.95 + bob
# legs
bind kneeL = gd_limb(ct, gx, gy, gz, fall, yaw, 0.0 - 0.11, hip, 0.0, list_get(p, 0), 0.46, 0.085, 0.095)
bind footL = gd_limb(ct, gx, gy, gz, fall, yaw, 0.0 - 0.11, list_get(kneeL, 1), list_get(kneeL, 2), list_get(p, 0) - list_get(p, 1), 0.46, 0.075, 0.085)
bind kneeR = gd_limb(ct, gx, gy, gz, fall, yaw, 0.11, hip, 0.0, list_get(p, 2), 0.46, 0.085, 0.095)
bind footR = gd_limb(ct, gx, gy, gz, fall, yaw, 0.11, list_get(kneeR, 1), list_get(kneeR, 2), list_get(p, 2) - list_get(p, 3), 0.46, 0.075, 0.085)
bind bl = gd_w(gx, gy, gz, fall, yaw, 0.0 - 0.11, list_get(footL, 1) - 0.04, list_get(footL, 2) + 0.06)
fx_box(cb, list_get(bl, 0), list_get(bl, 1), list_get(bl, 2), 0.08, 0.05, 0.14, fall, yaw)
bind br = gd_w(gx, gy, gz, fall, yaw, 0.11, list_get(footR, 1) - 0.04, list_get(footR, 2) + 0.06)
fx_box(cb, list_get(br, 0), list_get(br, 1), list_get(br, 2), 0.08, 0.05, 0.14, fall, yaw)
# torso (leans about the hips)
bind tc = gd_w(gx, gy, gz, fall, yaw, 0.0, hip - cos(lean) * 0.28, 0.0 - sin(lean) * 0.28)
fx_box(cj, list_get(tc, 0), list_get(tc, 1), list_get(tc, 2), 0.21, 0.29, 0.13, lean + fall, yaw)
bind belt = gd_w(gx, gy, gz, fall, yaw, 0.0, hip - 0.02, 0.0)
fx_box(cb, list_get(belt, 0), list_get(belt, 1), list_get(belt, 2), 0.215, 0.035, 0.135, lean + fall, yaw)
# head + hat
bind neck_y = hip - cos(lean) * 0.6
bind neck_z = 0.0 - sin(lean) * 0.6
bind ha = lean + list_get(p, 9)
bind hc = gd_w(gx, gy, gz, fall, yaw, 0.0, neck_y - cos(ha) * 0.12, neck_z - sin(ha) * 0.12)
fx_box(ch, list_get(hc, 0), list_get(hc, 1), list_get(hc, 2), 0.1, 0.12, 0.11, ha + fall, yaw)
bind hat = gd_w(gx, gy, gz, fall, yaw, 0.0, neck_y - cos(ha) * 0.25, neck_z - sin(ha) * 0.25)
fx_box(chat, list_get(hat, 0), list_get(hat, 1), list_get(hat, 2), 0.12, 0.04, 0.13, ha + fall, yaw)
# arms from the shoulders
bind sy = hip - cos(lean) * 0.52
bind sz = 0.0 - sin(lean) * 0.52
bind elL = gd_limb(cj, gx, gy, gz, fall, yaw, 0.0 - 0.27, sy, sz, lean + list_get(p, 4), 0.3, 0.06, 0.065)
bind haL = gd_limb(ch, gx, gy, gz, fall, yaw, 0.0 - 0.25, list_get(elL, 1), list_get(elL, 2), lean + list_get(p, 4) + list_get(p, 5), 0.28, 0.05, 0.055)
bind elR = gd_limb(cj, gx, gy, gz, fall, yaw, 0.27, sy, sz, lean + list_get(p, 6), 0.3, 0.06, 0.065)
bind fa = lean + list_get(p, 6) + list_get(p, 7)
bind haR = gd_limb(ch, gx, gy, gz, fall, yaw, 0.25, list_get(elR, 1), list_get(elR, 2), fa, 0.28, 0.05, 0.055)
# the gun in the right hand, along the forearm
bind w = gd_weapon(k)
bind glen = list_get([0.0, 0.12, 0.2, 0.3, 0.32], w)
bind gc = gd_w(gx, gy, gz, fall, yaw, 0.25, list_get(haR, 1) + cos(fa) * glen * 0.6, list_get(haR, 2) + sin(fa) * glen * 0.6)
fx_box(c_rgb(30.0, 30.0, 34.0), list_get(gc, 0), list_get(gc, 1), list_get(gc, 2), 0.03, 0.05, glen, fa - 1.5708 + fall, yaw)
if gget(k, "fl") > 0.0 {
bind tip = gd_w(gx, gy, gz, fall, yaw, 0.25, list_get(haR, 1) + cos(fa) * glen * 1.3, list_get(haR, 2) + sin(fa) * glen * 1.3)
fx_flash(list_get(tip, 0), list_get(tip, 1), list_get(tip, 2), yaw, 0.0, 0.16)
}
}
fn gd_draw_all() {
bind k = 0.0
while k < gd_count() {
if nd_get(gget(k, "node"), "vis") > 0.5 { gd_draw(k) }
bind k = k + 1.0
}
}