灵符lingfu

lingdev

↓ 0 downloads

LingDev: 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).

Windows · x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdev
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (136)

ling/rt/rt_fps.ling
# rt_fps.ling - first-person stages in the spirit of the classic console spy shooters: mouse /
# keyboard / gamepad look, the arsenal (slappers, silenced pistol, SMG, assault rifle, shotgun)
# with first-person viewmodels, recoil, brass and muzzle flashes, hits by body zone with blood and
# decals, pickups (weapons, ammo, body armour, mission items), sliding doors with keycard locks,
# destructible consoles, mission objectives by difficulty, the health / armour arcs, the watch
# pause screen, mission complete and failed.
#
# A scene becomes a stage with `mode=fps` in its ex field; `obj=consoles_2,item_keycard,exit` lists
# its objectives and `stage=1` its number. Guards are rt_guard.ling's.

fn fps_on() { return st_get("fps_on", 0.0) > 0.5 }
fn fps_diff() { return clampf(game_var("difficulty"), 0.0, 2.0) }
fn fps_diff_name(d) { return list_get(["Agent", "Special Agent", "Elite Agent"], clampf(d, 0.0, 2.0)) }

# ── the arsenal ──────────────────────────────────────────────────────────────
fn fps_wget(w, f) {
    bind k = clampf(w, 0.0, 4.0)
    if f == "dmg" { return list_get([26.0, 34.0, 18.0, 26.0, 14.0], k) }
    if f == "rate" { return list_get([0.42, 0.26, 0.085, 0.11, 0.8], k) }
    if f == "mag" { return list_get([0.0, 7.0, 30.0, 30.0, 5.0], k) }
    if f == "spread" { return list_get([0.0, 0.006, 0.032, 0.012, 0.065], k) }
    if f == "pellets" { return list_get([1.0, 1.0, 1.0, 1.0, 6.0], k) }
    if f == "silent" { return list_get([1.0, 1.0, 0.0, 0.0, 0.0], k) }
    if f == "reload" { return list_get([0.0, 1.1, 1.4, 1.6, 2.0], k) }
    if f == "auto" { return list_get([0.0, 0.0, 1.0, 1.0, 0.0], k) }
    if f == "maxres" { return list_get([0.0, 84.0, 240.0, 240.0, 40.0], k) }
    if f == "tip" { return list_get([0.0, 0.36, 0.33, 0.47, 0.5], k) }
    return 0.0
}
fn fps_wname(w) { return list_get(["Slappers", "PK9 Silenced", "Vole SMG", "D5 Assault Rifle", "Riot Shotgun"], clampf(w, 0.0, 4.0)) }
fn fps_wkey(w) { return list_get(["unarmed", "pistol", "smg", "rifle", "shotgun"], clampf(w, 0.0, 4.0)) }
fn fps_wfromkey(k) {
    if k == "pistol" { return 1.0 }
    if k == "smg" { return 2.0 }
    if k == "rifle" { return 3.0 }
    if k == "shotgun" { return 4.0 }
    return 0.0 - 1.0
}
fn fps_owns(w) { return w == 0.0 || st_get("fp_own" + num_str(w), 0.0) > 0.5 }
fn fps_mag(w) { return st_get("fp_mag" + num_str(w), 0.0) }
fn fps_res(w) { return st_get("fp_res" + num_str(w), 0.0) }
fn fps_give_weapon(w, ammo) {
    bind first = !fps_owns(w)
    st_set("fp_own" + num_str(w), 1.0)
    if first {
        bind m = min(ammo, fps_wget(w, "mag"))
        st_set("fp_mag" + num_str(w), m)
        st_set("fp_res" + num_str(w), min(fps_wget(w, "maxres"), ammo - m))
    } else { st_set("fp_res" + num_str(w), min(fps_wget(w, "maxres"), fps_res(w) + ammo)) }
    return first
}

# ── entering / leaving a stage ───────────────────────────────────────────────
fn fps_enter() {
    st_set("fps_on", 1.0)
    fx_reset()
    fx_collect_solids()
    gd_reset()
    st_set("fp_hp", 100.0)
    st_set("fp_armor", 0.0)
    bind w = 1.0
    while w < 5.0 {
        st_set("fp_own" + num_str(w), 0.0)
        st_set("fp_mag" + num_str(w), 0.0)
        st_set("fp_res" + num_str(w), 0.0)
        bind w = w + 1.0
    }
    fps_give_weapon(1.0, 7.0 + 21.0)
    st_set("fp_w", 1.0)
    st_set("fp_cd", 0.0)
    st_set("fp_reload", 0.0)
    st_set("fp_kick", 0.0)
    st_set("fp_flash", 0.0)
    st_set("fp_punch", 0.0)
    st_set("fp_hurt_t", 0.0)
    st_set("fp_show_t", 0.0)
    st_set("fp_dead", 0.0)
    st_set("fp_dead_t", 0.0)
    st_set("fp_paused", 0.0)
    st_set("fp_msg", "")
    st_set("fp_msg_t", 0.0)
    st_set("fp_switch_t", 2.0)
    st_set("fp_shots", 0.0)
    st_set("fp_hits", 0.0)
    st_set("fp_kills", 0.0)
    st_set("fp_heads", 0.0)
    st_set("fp_cons", 0.0)
    st_set("fp_bob", 0.0)
    st_set("fp_trig_was", 0.0)
    st_set("fp_start", st_get("game_t", 0.0))
    st_set("fp_intro_t", 4.0)
    st_lclear("fp_items")
    st_lclear("fp_objdone")
    bind p = game_player()
    st_set("fp_yaw", sel(p >= 0.0, nd_get(p, "ry"), 0.0))
    st_set("fp_pitch", 0.0)
    if p >= 0.0 { st_set("fp_lx", nd_get(p, "px"))  st_set("fp_lz", nd_get(p, "pz")) }
    # doors remember where they close to
    bind n = nd_count()
    bind i = 0.0
    while i < n {
        if nd_get(i, "tag") == "door" { st_set("door_y" + num_str(i), nd_get(i, "py"))  st_set("door_o" + num_str(i), 0.0) }
        if nd_get(i, "tag") == "console" { st_set("con_hp" + num_str(i), to_number(nd_ex(i, "hp", "60"))) }
        bind i = i + 1.0
    }
    fps_objectives_build()
    plat_capture_mouse()
    return 0.0
}
fn fps_leave() {
    if fps_on() { plat_release_mouse() }
    st_set("fps_on", 0.0)
}

# ── objectives ───────────────────────────────────────────────────────────────
fn fps_objectives_build() {
    st_lclear("fp_obj")
    bind all = str_split(sc_ex("obj", "exit"), ",")
    bind d = fps_diff()
    bind i = 0.0
    while i < len(all) {
        bind o = str_trim(list_get(all, i))
        # Agent keeps the first objective and the exit; harder settings add the rest
        bind keep = d > 0.5 || i == 0.0 || o == "exit"
        if o != "" && keep { st_lpush("fp_obj", o) }
        bind i = i + 1.0
    }
    st_lclear("fp_objdone")
    bind i = 0.0
    while i < st_llen("fp_obj") {
        st_lpush("fp_objdone", 0.0)
        bind i = i + 1.0
    }
}
fn fps_has_item(name) { return l_has(sl_to_list("fp_items"), name) }
fn fps_obj_text(o) {
    bind p = str_split(o, "_")
    bind kind = list_get(p, 0)
    bind arg = sel(len(p) > 1.0, list_get(p, len(p) - 1.0), "")
    if kind == "consoles" { return "Destroy the alarm consoles (" + num_str(min(st_get("fp_cons", 0.0), to_number(arg))) + "/" + arg + ")" }
    if kind == "kill" {
        if arg == "all" { return "Eliminate all hostiles (" + num_str(gd_alive_count()) + " left)" }
        return "Eliminate " + arg + " hostiles (" + num_str(min(st_get("fp_kills", 0.0), to_number(arg))) + "/" + arg + ")"
    }
    if kind == "item" {
        if arg == "keycard" { return "Obtain the security keycard" }
        if arg == "disk" { return "Recover the data disk" }
        if arg == "plans" { return "Photograph the plans" }
        return "Collect the " + arg
    }
    if kind == "exit" { return "Reach the extraction point" }
    return o
}
fn fps_obj_done(o) {
    bind p = str_split(o, "_")
    bind kind = list_get(p, 0)
    bind arg = sel(len(p) > 1.0, list_get(p, len(p) - 1.0), "")
    if kind == "consoles" { return st_get("fp_cons", 0.0) >= to_number(arg) }
    if kind == "kill" {
        if arg == "all" { return gd_alive_count() < 0.5 }
        return st_get("fp_kills", 0.0) >= to_number(arg)
    }
    if kind == "item" { return fps_has_item(arg) }
    return false
}
fn fps_objectives_tick() {
    bind i = 0.0
    while i < st_llen("fp_obj") {
        bind o = st_lget("fp_obj", i)
        if st_lget("fp_objdone", i) < 0.5 && fps_obj_done(o) {
            st_lset("fp_objdone", i, 1.0)
            fps_message("Objective complete: " + fps_obj_text(o))
            plat_sfx(st_get("fp_x", 0.0), 0.0, st_get("fp_z", 0.0), 50.0, 660.0, 0.2, 0.25, "sine")
        }
        bind i = i + 1.0
    }
}
fn fps_ready_to_exit() {
    bind i = 0.0
    while i < st_llen("fp_obj") {
        if st_lget("fp_obj", i) != "exit" && st_lget("fp_objdone", i) < 0.5 { return false }
        bind i = i + 1.0
    }
    return true
}
fn fps_try_exit() {
    if !fps_on() || st_get("fp_dead", 0.0) > 0.5 { return 0.0 }
    if !fps_ready_to_exit() {
        fps_message("Objectives incomplete")
        return 0.0
    }
    fps_complete()
    return 0.0
}
fn fps_complete() {
    bind secs = st_get("game_t", 0.0) - st_get("fp_start", 0.0)
    bind m = floor(secs / 60.0)
    st_set("gs_time", num_str(m) + ":" + pad2(floor(secs - m * 60.0)))
    st_set("gs_acc", num_str(round(st_get("fp_hits", 0.0) / max(1.0, st_get("fp_shots", 0.0)) * 100.0)) + "%")
    st_set("gs_kills", num_str(st_get("fp_kills", 0.0)))
    st_set("gs_heads", num_str(st_get("fp_heads", 0.0)))
    st_set("gs_shots", num_str(st_get("fp_shots", 0.0)))
    st_set("gs_diff", fps_diff_name(fps_diff()))
    st_set("gs_stagename", sc_get("name", ""))
    st_set("gs_done" + sc_ex("stage", "1"), "COMPLETED  " + fps_diff_name(fps_diff()) + "  " + st_get("gs_time", ""))
    fps_leave()
    game_goto("scenes/complete.lscene")
}
fn fps_fail(reason) {
    st_set("gs_fail", reason)
    st_set("gs_stagename", sc_get("name", ""))
    st_set("gs_diff", fps_diff_name(fps_diff()))
    fps_leave()
    game_goto("scenes/failed.lscene")
}
fn fps_message(m) {
    st_set("fp_msg", m)
    st_set("fp_msg_t", 3.5)
}

# ── the player ───────────────────────────────────────────────────────────────
fn fps_hurt(dmg, fx, fz) {
    if st_get("fp_dead", 0.0) > 0.5 { return 0.0 }
    bind a = st_get("fp_armor", 0.0)
    bind left = dmg
    if a > 0.0 {
        bind take = min(a, dmg * 0.8)
        st_set("fp_armor", a - take)
        bind left = dmg - take
    }
    st_set("fp_hp", st_get("fp_hp", 100.0) - left)
    st_set("fp_hurt_t", 0.35)
    st_set("fp_show_t", 3.0)
    st_set("fp_hurt_dir", atan2(fx - st_get("fp_x", 0.0), fz - st_get("fp_z", 0.0)))
    plat_sfx(st_get("fp_x", 0.0), 0.0, st_get("fp_z", 0.0), 50.0, 110.0, 0.35, 0.12, "triangle")
    if st_get("fp_hp", 100.0) <= 0.0 {
        st_set("fp_dead", 1.0)
        st_set("fp_dead_t", 0.0)
        st_set("fp_hp", 0.0)
    }
    return 0.0
}

fn fps_update(dt) {
    # pause: the watch
    bind esc = key_pressed("escape") && st_get("game_embedded", 0.0) < 0.5
    if key_pressed("p") || key_pressed("tab") || esc {
        if st_get("fp_paused", 0.0) > 0.5 {
            st_set("fp_paused", 0.0)
            plat_capture_mouse()
        } else {
            st_set("fp_paused", 1.0)
            plat_release_mouse()
        }
    }
    if st_get("fp_paused", 0.0) > 0.5 {
        if key_pressed("q") { fps_fail("Mission aborted") }
        return 0.0
    }
    st_set("fp_msg_t", max(0.0, st_get("fp_msg_t", 0.0) - dt))
    st_set("fp_hurt_t", max(0.0, st_get("fp_hurt_t", 0.0) - dt))
    st_set("fp_show_t", max(0.0, st_get("fp_show_t", 0.0) - dt))
    st_set("fp_switch_t", max(0.0, st_get("fp_switch_t", 0.0) - dt))
    st_set("fp_intro_t", max(0.0, st_get("fp_intro_t", 0.0) - dt))
    st_set("fp_kick", max(0.0, st_get("fp_kick", 0.0) - dt * 9.0))
    st_set("fp_flash", max(0.0, st_get("fp_flash", 0.0) - dt))
    st_set("fp_punch", max(0.0, st_get("fp_punch", 0.0) - dt * 4.0))
    bind p = game_player()
    if p < 0.0 { return 0.0 }
    if st_get("fp_dead", 0.0) > 0.5 {
        st_set("fp_dead_t", st_get("fp_dead_t", 0.0) + dt)
        bind u = clampf(st_get("fp_dead_t", 0.0) / 1.2, 0.0, 1.0)
        bind eye = nd_get(p, "py") - nd_get(p, "sy") * 0.85 + u * 1.3
        cam_set(nd_get(p, "px"), eye, nd_get(p, "pz"), st_get("fp_yaw", 0.0), st_get("fp_pitch", 0.0) - u * 0.9, 0.3)
        gd_update_all(dt, nd_get(p, "px"), nd_get(p, "py"), nd_get(p, "pz"), eye)
        fx_update(dt)
        if st_get("fp_dead_t", 0.0) > 3.5 { fps_fail("Agent killed in action") }
        return 0.0
    }
    # look
    bind sens = 0.0028
    bind yaw = st_get("fp_yaw", 0.0) + mouse_dx() * sens
    bind pitch = st_get("fp_pitch", 0.0) + mouse_dy() * sens
    if key_down("left") { bind yaw = yaw - dt * 2.2 }
    if key_down("right") { bind yaw = yaw + dt * 2.2 }
    if key_down("pageup") { bind pitch = pitch - dt * 1.6 }
    if key_down("pagedown") { bind pitch = pitch + dt * 1.6 }
    if plat_gamepad_connected() {
        bind rx = plat_gamepad_rx()
        bind ry = plat_gamepad_ry()
        if abs(rx) > 0.15 { bind yaw = yaw + rx * dt * 2.8 }
        if abs(ry) > 0.15 { bind pitch = pitch + ry * dt * 2.0 }
    }
    bind pitch = clampf(pitch, 0.0 - 1.3, 1.3)
    st_set("fp_yaw", yaw)
    st_set("fp_pitch", pitch)
    # move (the shared controller does walls; the camera yaw decides "forward")
    cam_set(nd_get(p, "px"), nd_get(p, "py"), nd_get(p, "pz"), yaw, 0.0, 0.3)
    bind ox = nd_get(p, "px")
    bind oz = nd_get(p, "pz")
    game_player_update(dt)
    bind px = nd_get(p, "px")
    bind pz = nd_get(p, "pz")
    bind moved = sqrt((px - ox) * (px - ox) + (pz - oz) * (pz - oz))
    st_set("fp_speed", moved / max(0.0001, dt))
    st_set("fp_bob", st_get("fp_bob", 0.0) + moved * 2.4)
    bind bobv = sel(moved > 0.0005, sin(st_get("fp_bob", 0.0) * 2.0) * 0.035, 0.0)
    bind eye = nd_get(p, "py") - nd_get(p, "sy") * 0.85 + bobv
    st_set("fp_x", px)
    st_set("fp_z", pz)
    st_set("fp_eye", eye)
    cam_set(px, eye, pz, yaw, pitch - st_get("fp_kick", 0.0) * 0.05, 0.3)
    plat_listener(cos(yaw), sin(yaw), cos(pitch), sin(pitch))
    fps_weapons(dt)
    fps_pickups()
    fps_doors(dt)
    gd_update_all(dt, px, nd_get(p, "py"), pz, eye)
    fx_update(dt)
    fps_objectives_tick()
    return 0.0
}

# ── weapons ──────────────────────────────────────────────────────────────────
fn fps_switch(w) {
    if w == st_get("fp_w", 1.0) || !fps_owns(w) { return 0.0 }
    st_set("fp_w", w)
    st_set("fp_reload", 0.0)
    st_set("fp_cd", 0.25)
    st_set("fp_switch_t", 2.0)
    plat_sfx(st_get("fp_x", 0.0), 0.0, st_get("fp_z", 0.0), 50.0, 420.0, 0.12, 0.05, "square")
    return 0.0
}
fn fps_cycle(dir) {
    bind w = st_get("fp_w", 1.0)
    bind k = 0.0
    while k < 5.0 {
        bind w = w + dir
        if w > 4.0 { bind w = 0.0 }
        if w < 0.0 { bind w = 4.0 }
        if fps_owns(w) { fps_switch(w)  return 0.0 }
        bind k = k + 1.0
    }
    return 0.0
}
fn fps_weapons(dt) {
    bind w = st_get("fp_w", 1.0)
    bind keys = ["1", "2", "3", "4", "5"]
    bind k = 0.0
    while k < 5.0 {
        if key_pressed(list_get(keys, k)) { fps_switch(k) }
        bind k = k + 1.0
    }
    if key_pressed("q") || st_get("scroll", 0.0) < 0.0 { fps_cycle(0.0 - 1.0) }
    if key_pressed("e") || st_get("scroll", 0.0) > 0.0 { fps_cycle(1.0) }
    bind w = st_get("fp_w", 1.0)
    st_set("fp_cd", max(0.0, st_get("fp_cd", 0.0) - dt))
    # reloading
    if st_get("fp_reload", 0.0) > 0.0 {
        bind r = st_get("fp_reload", 0.0) - dt
        st_set("fp_reload", r)
        if r <= 0.0 {
            bind need = fps_wget(w, "mag") - fps_mag(w)
            bind take = min(need, fps_res(w))
            st_set("fp_mag" + num_str(w), fps_mag(w) + take)
            st_set("fp_res" + num_str(w), fps_res(w) - take)
            st_set("fp_reload", 0.0)
            plat_sfx(st_get("fp_x", 0.0), 0.0, st_get("fp_z", 0.0), 50.0, 340.0, 0.15, 0.04, "square")
        }
        return 0.0
    }
    if key_pressed("r") && w > 0.0 && fps_mag(w) < fps_wget(w, "mag") && fps_res(w) > 0.0 { fps_start_reload(w) }
    bind trig = mouse_down() || key_down("lctrl") || key_down("rctrl") || plat_gamepad_rt() > 0.5
    bind was = st_get("fp_trig_was", 0.0) > 0.5
    st_set("fp_trig_was", bool_num(trig))
    bind want = trig && (fps_wget(w, "auto") > 0.5 || !was)
    if want && st_get("fp_cd", 0.0) <= 0.0 {
        if w > 0.0 && fps_mag(w) < 1.0 {
            if !was { plat_sfx(st_get("fp_x", 0.0), 0.0, st_get("fp_z", 0.0), 50.0, 1400.0, 0.1, 0.02, "square") }
            if fps_res(w) > 0.0 { fps_start_reload(w) } else { st_set("fp_cd", 0.3) }
            return 0.0
        }
        fps_fire(w)
    }
    return 0.0
}
fn fps_start_reload(w) {
    st_set("fp_reload", fps_wget(w, "reload"))
    plat_sfx(st_get("fp_x", 0.0), 0.0, st_get("fp_z", 0.0), 50.0, 260.0, 0.14, 0.05, "square")
}
fn fps_fire(w) {
    st_set("fp_cd", fps_wget(w, "rate"))
    bind ox = cam_x()
    bind oy = cam_y()
    bind oz = cam_z()
    bind f = cam_forward()
    bind r = cam_right()
    bind u = cam_up()
    if w == 0.0 {
        # slappers: a short reach
        st_set("fp_punch", 1.0)
        plat_sfx(ox, oy, oz, 50.0, 140.0, 0.25, 0.07, "triangle")
        bind k = 0.0
        while k < gd_count() {
            bind h = gd_ray(k, ox, oy, oz, list_get(f, 0), list_get(f, 1), list_get(f, 2))
            if list_get(h, 0) >= 0.0 && list_get(h, 0) < 1.7 {
                gd_hit(k, list_get(h, 1), fps_wget(0.0, "dmg"), list_get(f, 0), list_get(f, 1), list_get(f, 2))
                st_set("fp_hits", st_get("fp_hits", 0.0) + 1.0)
            }
            bind k = k + 1.0
        }
        st_set("fp_shots", st_get("fp_shots", 0.0) + 1.0)
        return 0.0
    }
    st_set("fp_mag" + num_str(w), fps_mag(w) - 1.0)
    st_set("fp_shots", st_get("fp_shots", 0.0) + 1.0)
    st_set("fp_kick", 1.0)
    st_set("fp_flash", 0.05)
    if fps_wget(w, "silent") > 0.5 {
        plat_sfx(ox, oy, oz, 50.0, 3200.0, 0.16, 0.05, "noise")
        gd_alert_near(ox, oz, 4.0)
    } else {
        plat_sfx(ox, oy, oz, 50.0, sel(w == 4.0, 220.0, 650.0), sel(w == 4.0, 0.6, 0.42), sel(w == 4.0, 0.28, 0.1), "noise")
        gd_alert_near(ox, oz, 26.0)
        fx_light(ox + list_get(f, 0), oy + list_get(f, 1), oz + list_get(f, 2), c_rgb(255.0, 200.0, 110.0), 2.5, 8.0, 0.05)
    }
    # brass out of the right side
    bind gp = fps_vm_point(0.31, 0.0 - 0.24, 0.9)
    fx_burst(list_get(gp, 0), list_get(gp, 1), list_get(gp, 2), sel(w == 4.0, 0.0, 1.0), 2.0, list_get(r, 0) + list_get(u, 0), list_get(r, 1) + list_get(u, 1), list_get(r, 2) + list_get(u, 2), 1.8, 0.8)
    bind spread = fps_wget(w, "spread") * sel(st_get("fp_speed", 0.0) > 0.5, 1.6, 1.0)
    bind hit_any = false
    bind pellets = fps_wget(w, "pellets")
    bind pl = 0.0
    while pl < pellets {
        bind a = (rand() - 0.5) * 2.0 * spread
        bind b = (rand() - 0.5) * 2.0 * spread
        bind dx = list_get(f, 0) + list_get(r, 0) * a + list_get(u, 0) * b
        bind dy = list_get(f, 1) + list_get(r, 1) * a + list_get(u, 1) * b
        bind dz = list_get(f, 2) + list_get(r, 2) * a + list_get(u, 2) * b
        bind l = sqrt(dx * dx + dy * dy + dz * dz)
        bind dx = dx / l
        bind dy = dy / l
        bind dz = dz / l
        bind hw = ray_world(ox, oy, oz, dx, dy, dz, 120.0)
        bind tw = list_get(hw, 0)
        bind bg = 0.0 - 1.0
        bind bt = tw
        bind bz = 0.0
        bind k = 0.0
        while k < gd_count() {
            bind h = gd_ray(k, ox, oy, oz, dx, dy, dz)
            if list_get(h, 0) >= 0.0 && list_get(h, 0) < bt { bind bt = list_get(h, 0)  bind bg = k  bind bz = list_get(h, 1) }
            bind k = k + 1.0
        }
        if bg >= 0.0 {
            gd_hit(bg, bz, fps_wget(w, "dmg"), dx, dy, dz)
            bind hit_any = true
        } else if list_get(hw, 1) != 0.0 - 1.0 {
            bind hx = ox + dx * tw
            bind hy = oy + dy * tw
            bind hz = oz + dz * tw
            fx_decal(hx, hy, hz, list_get(hw, 2), 0.0, 0.03 + rand() * 0.015)
            fx_burst(hx, hy, hz, 2.0, 1.0, 0.0 - dx, 0.0 - dy, 0.0 - dz, 1.5, 2.2)
            if pl < 1.0 { fx_burst(hx, hy, hz, 1.0, 4.0, 0.0 - dx, 0.0 - dy, 0.0 - dz, 0.3, 0.2) }
            bind ni = list_get(hw, 1)
            if ni >= 0.0 { if nd_get(ni, "tag") == "console" { fps_console_hit(ni, fps_wget(w, "dmg")) } }
        }
        bind pl = pl + 1.0
    }
    if hit_any { st_set("fp_hits", st_get("fp_hits", 0.0) + 1.0) }
    return 0.0
}
fn fps_console_hit(i, dmg) {
    bind key = "con_hp" + num_str(i)
    bind hp = st_get(key, 60.0) - dmg
    st_set(key, hp)
    bind x = nd_get(i, "px")
    bind y = nd_get(i, "py")
    bind z = nd_get(i, "pz")
    fx_burst(x, y, z, 3.0, 1.0, 0.0, 0.0 - 1.0, 0.0, 2.0, 3.0)
    if hp <= 0.0 && nd_get(i, "tag") == "console" {
        nd_set(i, "tag", "wreck")
        nd_set(i, "col", c_mix(nd_get(i, "col"), 0.0, 0.75))
        st_set("fp_cons", st_get("fp_cons", 0.0) + 1.0)
        fx_burst(x, y, z, 14.0, 1.0, 0.0, 0.0 - 1.0, 0.0, 4.0, 5.0)
        fx_burst(x, y, z, 6.0, 3.0, 0.0, 0.0 - 1.0, 0.0, 3.5, 3.0)
        fx_burst(x, y - nd_get(i, "sy"), z, 4.0, 4.0, 0.0, 0.0 - 1.0, 0.0, 0.5, 0.6)
        fx_light(x, y - 1.0, z, c_rgb(255.0, 150.0, 60.0), 4.0, 14.0, 0.35)
        fx_decal(x, sc_get("floor", 0.0), z, 2.0, 2.0, max(nd_get(i, "sx"), nd_get(i, "sz")) + 0.6)
        plat_sfx(x, y, z, 12.0, 90.0, 0.8, 0.7, "noise")
        gd_alert_near(x, z, 30.0)
        fps_message("Console destroyed")
    }
}

# ── pickups and doors ────────────────────────────────────────────────────────
fn fps_pickups() {
    bind px = st_get("fp_x", 0.0)
    bind pz = st_get("fp_z", 0.0)
    bind n = nd_count()
    bind i = 0.0
    while i < n {
        if nd_get(i, "tag") == "pickup" && nd_get(i, "vis") > 0.5 {
            bind dx = nd_get(i, "px") - px
            bind dz = nd_get(i, "pz") - pz
            if dx * dx + dz * dz < 1.3 { fps_collect(i) }
        }
        bind i = i + 1.0
    }
}
fn fps_collect(i) {
    bind item = nd_ex(i, "item", "ammo")
    bind amount = to_number(nd_ex(i, "amount", "0"))
    bind w = fps_wfromkey(item)
    bind got = true
    if w > 0.0 {
        if amount <= 0.0 { bind amount = fps_wget(w, "mag") * 2.0 }
        bind first = fps_give_weapon(w, amount)
        if first {
            fps_message("Picked up a " + fps_wname(w))
            fps_switch(w)
        } else { fps_message("Picked up " + num_str(amount) + " rounds") }
    } else if item == "ammo" {
        # ammo for every gun carried
        bind k = 1.0
        while k < 5.0 {
            if fps_owns(k) { st_set("fp_res" + num_str(k), min(fps_wget(k, "maxres"), fps_res(k) + fps_wget(k, "mag"))) }
            bind k = k + 1.0
        }
        fps_message("Picked up ammunition")
    } else if item == "armor" {
        if st_get("fp_armor", 0.0) >= 100.0 { bind got = false } else {
            st_set("fp_armor", min(100.0, st_get("fp_armor", 0.0) + sel(amount > 0.0, amount, 100.0)))
            st_set("fp_show_t", 3.0)
            fps_message("Picked up body armour")
        }
    } else {
        st_lpush("fp_items", item)
        fps_message("Obtained: " + str_replace(item, "_", " "))
    }
    if got {
        nd_set(i, "vis", 0.0)
        plat_sfx(nd_get(i, "px"), nd_get(i, "py"), nd_get(i, "pz"), 50.0, 990.0, 0.2, 0.09, "sine")
    }
}
fn fps_doors(dt) {
    bind px = st_get("fp_x", 0.0)
    bind pz = st_get("fp_z", 0.0)
    bind n = nd_count()
    bind i = 0.0
    while i < n {
        if nd_get(i, "tag") == "door" {
            bind key = num_str(i)
            bind x = nd_get(i, "px")
            bind z = nd_get(i, "pz")
            bind ex = max(0.0, abs(px - x) - nd_get(i, "sx"))
            bind ez = max(0.0, abs(pz - z) - nd_get(i, "sz"))
            bind near = ex * ex + ez * ez < 4.4
            if !near {
                bind k = 0.0
                while k < gd_count() {
                    if gd_alive(k) && gget(k, "st") > 0.5 {
                        bind gi = gget(k, "node")
                        bind gx = max(0.0, abs(nd_get(gi, "px") - x) - nd_get(i, "sx"))
                        bind gz = max(0.0, abs(nd_get(gi, "pz") - z) - nd_get(i, "sz"))
                        if gx * gx + gz * gz < 3.0 { bind near = true }
                    }
                    bind k = k + 1.0
                }
            }
            bind lock = nd_ex(i, "lock", "")
            if near && lock != "" && !fps_has_item(lock) {
                bind near = false
                if st_get("door_msg" + key, 0.0) < 0.5 { fps_message("This door needs the " + lock)  st_set("door_msg" + key, 1.0) }
            } else if !near { st_set("door_msg" + key, 0.0) }
            bind o = st_get("door_o" + key, 0.0)
            bind target = sel(near, 1.0, 0.0)
            if o < 0.01 && target > 0.5 { plat_sfx(x, nd_get(i, "py"), z, 12.0, 180.0, 0.25, 0.4, "saw") }
            bind o = clampf(o + sel(near, dt * 2.6, 0.0 - dt * 1.6), 0.0, 1.0)
            st_set("door_o" + key, o)
            nd_set(i, "py", st_get("door_y" + key, nd_get(i, "py")) - o * (nd_get(i, "sy") * 2.0 + 0.05))
        }
        bind i = i + 1.0
    }
}

# ── drawing in the world (inside the scene's depth pass) ─────────────────────
fn fps_world_native() {
    gd_draw_all()
    fps_draw_pickups()
    fx_draw_native()
}
fn fps_draw_pickups() {
    bind t = st_get("game_t", 0.0)
    bind n = nd_count()
    bind i = 0.0
    while i < n {
        if nd_get(i, "tag") == "pickup" && nd_get(i, "vis") > 0.5 {
            bind x = nd_get(i, "px")
            bind z = nd_get(i, "pz")
            bind y = sc_get("floor", 0.0) - 0.35 - sin(t * 2.5 + i) * 0.06
            bind yaw = t * 1.6 + i
            bind item = nd_ex(i, "item", "ammo")
            bind w = fps_wfromkey(item)
            if w > 0.0 {
                bind ln = list_get([0.0, 0.16, 0.24, 0.34, 0.38], w)
                fx_box(c_rgb(38.0, 38.0, 44.0), x, y, z, 0.035, 0.05, ln, 0.0, yaw)
                fx_box(c_rgb(30.0, 30.0, 30.0), x - sin(yaw) * ln * 0.3, y + 0.08, z - cos(yaw) * ln * 0.3, 0.03, 0.07, 0.035, 0.3, yaw)
                if w == 2.0 || w == 3.0 { fx_box(c_rgb(50.0, 50.0, 54.0), x + sin(yaw) * 0.05, y + 0.1, z + cos(yaw) * 0.05, 0.025, 0.08, 0.03, 0.2, yaw) }
            } else if item == "ammo" {
                fx_box(c_rgb(70.0, 90.0, 50.0), x, y, z, 0.16, 0.1, 0.11, 0.0, yaw)
                fx_box(c_rgb(200.0, 170.0, 60.0), x, y - 0.11, z, 0.1, 0.012, 0.06, 0.0, yaw)
            } else if item == "armor" {
                fx_box(c_rgb(40.0, 70.0, 140.0), x, y - 0.1, z, 0.2, 0.26, 0.07, 0.0, yaw)
                fx_box(c_rgb(70.0, 110.0, 190.0), x, y - 0.3, z, 0.12, 0.04, 0.072, 0.0, yaw)
            } else if item == "keycard" {
                fx_box(c_rgb(235.0, 235.0, 240.0), x, y, z, 0.11, 0.07, 0.008, 0.0, yaw)
                fx_box(c_rgb(200.0, 40.0, 40.0), x, y - 0.03, z, 0.11, 0.015, 0.009, 0.0, yaw)
            } else if item == "disk" {
                fx_box(c_rgb(30.0, 30.0, 60.0), x, y, z, 0.1, 0.1, 0.012, 0.0, yaw)
                fx_box(c_rgb(190.0, 190.0, 200.0), x, y - 0.05, z, 0.06, 0.04, 0.013, 0.0, yaw)
            } else {
                fx_box(c_rgb(240.0, 200.0, 60.0), x, y, z, 0.12, 0.12, 0.12, t, yaw)
            }
            # a soft glow ring so items read at a distance
            plat_blend(1)
            fx_box(c_rgb(70.0, 60.0, 20.0), x, sc_get("floor", 0.0) - 0.02, z, 0.3, 0.005, 0.3, 0.0, yaw)
            plat_blend(0)
        }
        bind i = i + 1.0
    }
}

# ── the viewmodel ────────────────────────────────────────────────────────────
# camera-space point (right, up, forward) -> world
fn fps_vm_point(ox, oy, oz) {
    bind r = cam_right()
    bind u = cam_up()
    bind f = cam_forward()
    return [cam_x() + list_get(r, 0) * ox + list_get(u, 0) * oy + list_get(f, 0) * oz, cam_y() + list_get(r, 1) * ox + list_get(u, 1) * oy + list_get(f, 1) * oz, cam_z() + list_get(r, 2) * ox + list_get(u, 2) * oy + list_get(f, 2) * oz]
}
fn vm_box(c, ox, oy, oz, sx, sy, sz, tilt) {
    bind p = fps_vm_point(ox, oy, oz)
    fx_box(c, list_get(p, 0), list_get(p, 1), list_get(p, 2), sx, sy, sz, 0.0 - cam_pitch() + tilt, cam_yaw())
}
fn fps_draw_view() {
    if !fps_on() || st_get("fp_dead", 0.0) > 0.5 { return 0.0 }
    bind w = st_get("fp_w", 1.0)
    bind moving = st_get("fp_speed", 0.0) > 0.5
    bind bph = st_get("fp_bob", 0.0)
    bind bx = sel(moving, sin(bph) * 0.012, 0.0)
    bind by = sel(moving, 0.0 - abs(cos(bph)) * 0.01, sin(st_get("game_t", 0.0) * 1.4) * 0.003)
    bind kick = st_get("fp_kick", 0.0)
    bind rl = st_get("fp_reload", 0.0)
    bind dip = 0.0
    if rl > 0.0 {
        bind total = fps_wget(w, "reload")
        bind u = 1.0 - rl / max(0.01, total)
        bind dip = sin(u * 3.14159)
    }
    bind sw = st_get("fp_switch_t", 0.0)
    bind raise = sel(sw > 1.75, (sw - 1.75) * 4.0, 0.0)
    bind ox = 0.3 + bx
    bind oy = 0.0 - 0.27 + by - dip * 0.22 - raise * 0.4 + kick * 0.015
    bind oz = 0.86 - kick * 0.06
    bind tilt = dip * 0.6 - kick * 0.12
    bind skin = c_rgb(215.0, 170.0, 130.0)
    bind sleeve = c_rgb(32.0, 36.0, 52.0)
    bind metal = c_rgb(36.0, 36.0, 42.0)
    bind dark = c_rgb(18.0, 18.0, 20.0)
    if w == 0.0 {
        bind pu = st_get("fp_punch", 0.0)
        bind jab = sin(pu * 3.14159)
        vm_box(skin, 0.27 + bx, 0.0 - 0.25 + by + jab * 0.05, 0.8 + jab * 0.3, 0.05, 0.05, 0.06, 0.0)
        vm_box(sleeve, 0.29 + bx, 0.0 - 0.29 + by, 0.62 + jab * 0.28, 0.06, 0.06, 0.16, 0.0)
        vm_box(skin, 0.0 - 0.3 + bx, 0.0 - 0.28 + by, 0.78, 0.05, 0.05, 0.06, 0.0)
        vm_box(sleeve, 0.0 - 0.32 + bx, 0.0 - 0.32 + by, 0.6, 0.06, 0.06, 0.16, 0.0)
    } else {
        # hand and sleeve under every gun
        vm_box(skin, ox, oy - 0.06, oz - 0.06, 0.04, 0.045, 0.05, tilt)
        vm_box(sleeve, ox + 0.02, oy - 0.1, oz - 0.2, 0.055, 0.055, 0.13, tilt)
        if w == 1.0 {
            vm_box(metal, ox, oy + 0.02, oz + 0.04, 0.022, 0.028, 0.11, tilt)
            vm_box(dark, ox, oy - 0.04, oz - 0.04, 0.02, 0.05, 0.028, tilt + 0.3)
            vm_box(dark, ox, oy + 0.02, oz + 0.22, 0.018, 0.018, 0.08, tilt)
        } else if w == 2.0 {
            vm_box(metal, ox, oy + 0.01, oz + 0.04, 0.032, 0.042, 0.15, tilt)
            vm_box(dark, ox, oy - 0.09, oz + 0.07, 0.018, 0.07, 0.022, tilt + 0.15)
            vm_box(dark, ox, oy + 0.02, oz + 0.23, 0.012, 0.012, 0.05, tilt)
            vm_box(skin, ox - 0.06, oy - 0.05, oz + 0.12, 0.035, 0.04, 0.045, tilt)
        } else if w == 3.0 {
            vm_box(c_rgb(60.0, 54.0, 40.0), ox, oy + 0.01, oz + 0.06, 0.034, 0.045, 0.22, tilt)
            vm_box(dark, ox, oy + 0.02, oz + 0.33, 0.012, 0.012, 0.08, tilt)
            vm_box(dark, ox, oy - 0.1, oz + 0.1, 0.02, 0.07, 0.025, tilt + 0.35)
            vm_box(metal, ox, oy + 0.075, oz + 0.04, 0.018, 0.018, 0.07, tilt)
            vm_box(skin, ox - 0.07, oy - 0.04, oz + 0.2, 0.035, 0.04, 0.045, tilt)
        } else {
            vm_box(dark, ox, oy + 0.03, oz + 0.16, 0.024, 0.024, 0.3, tilt)
            vm_box(c_rgb(70.0, 46.0, 30.0), ox, oy - 0.01, oz + 0.2 - kick * 0.05, 0.03, 0.03, 0.07, tilt)
            vm_box(c_rgb(70.0, 46.0, 30.0), ox, oy - 0.03, oz - 0.1, 0.028, 0.045, 0.1, tilt + 0.2)
            vm_box(skin, ox - 0.05, oy - 0.04, oz + 0.2 - kick * 0.05, 0.035, 0.04, 0.045, tilt)
        }
        if st_get("fp_flash", 0.0) > 0.0 {
            bind tip = fps_vm_point(ox, oy + 0.025, oz + fps_wget(w, "tip"))
            fx_flash(list_get(tip, 0), list_get(tip, 1), list_get(tip, 2), cam_yaw(), cam_pitch(), sel(w == 4.0, 0.14, 0.09))
        }
    }
    plat_3d_flush()
    return 0.0
}

# ── HUD ──────────────────────────────────────────────────────────────────────
fn fps_arc(cx, cy, r, a0, a1, frac, c, dim) {
    bind n = 22.0
    bind k = 0.0
    while k < n {
        bind a = a0 + (a1 - a0) * (k + 0.5) / n
        bind on = (k + 0.5) / n <= frac
        px_disc(cx + cos(a) * r, cy + sin(a) * r, 4.2, sel(on, c, dim), sel(on, 0.95, 0.35))
        bind k = k + 1.0
    }
}
fn fps_hud(x, y, w, h) {
    if !fps_on() { return 0.0 }
    bind cx = x + w * 0.5
    bind cy = y + h * 0.5
    bind wpn = st_get("fp_w", 1.0)
    # damage tint
    if st_get("fp_hurt_t", 0.0) > 0.0 { g_rect_a(x, y, w, h, c_rgb(200.0, 0.0, 0.0), st_get("fp_hurt_t", 0.0) * 0.9) }
    if st_get("fp_dead", 0.0) > 0.5 {
        bind u = clampf(st_get("fp_dead_t", 0.0) / 2.0, 0.0, 1.0)
        g_rect_a(x, y, w, h, c_rgb(120.0, 0.0, 0.0), u * 0.6)
        if st_get("fp_dead_t", 0.0) > 1.2 { g_text_c(cx, cy - 20.0, 34.0, C_WHITE, "MISSION FAILED") }
        return 0.0
    }
    # crosshair
    bind cc = c_rgb(120.0, 255.0, 140.0)
    px_line(cx - 10.0, cy, cx - 4.0, cy, cc, 0.9)
    px_line(cx + 4.0, cy, cx + 10.0, cy, cc, 0.9)
    px_line(cx, cy - 10.0, cx, cy - 4.0, cc, 0.9)
    px_line(cx, cy + 4.0, cx, cy + 10.0, cc, 0.9)
    # health (left) and armour (right) arcs, shown after a hit or when low
    bind hp = st_get("fp_hp", 100.0)
    if st_get("fp_show_t", 0.0) > 0.0 || hp < 35.0 {
        bind r = h * 0.36
        fps_arc(cx, cy, r, 2.35, 3.93, hp / 100.0, c_rgb(235.0, 70.0, 50.0), c_rgb(90.0, 20.0, 20.0))
        fps_arc(cx, cy, r, 0.79, 0.0 - 0.79, st_get("fp_armor", 0.0) / 100.0, c_rgb(70.0, 140.0, 255.0), c_rgb(20.0, 35.0, 80.0))
    }
    # ammo, bottom right: the magazine as rounds, the reserve as a number
    if wpn > 0.0 {
        bind mag = fps_mag(wpn)
        bind cap = fps_wget(wpn, "mag")
        bind ax = x + w - 40.0
        bind ay = y + h - 60.0
        bind per = 15.0
        bind k = 0.0
        while k < cap {
            bind col = floor(k / per)
            bind row = k - col * per
            bind bxp = ax - row * 7.0
            bind byp = ay - col * 18.0
            g_rect(bxp, byp, 4.0, 13.0, sel(k < mag, c_rgb(235.0, 200.0, 90.0), c_rgb(60.0, 50.0, 30.0)))
            bind k = k + 1.0
        }
        g_text_r(x + w - 22.0, y + h - 36.0, 18.0, c_rgb(235.0, 210.0, 120.0), num_str(fps_res(wpn)))
        if st_get("fp_reload", 0.0) > 0.0 { g_text_r(x + w - 22.0, ay - 40.0, 13.0, C_WHITE, "RELOADING") }
        else if mag < 1.0 && fps_res(wpn) < 1.0 { g_text_r(x + w - 22.0, ay - 40.0, 13.0, c_rgb(255.0, 120.0, 100.0), "NO AMMO") }
    }
    if st_get("fp_switch_t", 0.0) > 0.0 {
        bind a = clampf(st_get("fp_switch_t", 0.0), 0.0, 1.0)
        g_text_c(cx, y + h - 70.0, 16.0, c_mix(c_rgb(30.0, 30.0, 30.0), C_WHITE, a), fps_wname(wpn))
    }
    # messages, top left
    if st_get("fp_msg_t", 0.0) > 0.0 {
        bind a = clampf(st_get("fp_msg_t", 0.0), 0.0, 1.0)
        g_text(x + 24.0, y + 22.0, 15.0, c_mix(0.0, c_rgb(230.0, 240.0, 210.0), a), st_get("fp_msg", ""))
    }
    # the opening caption: stage name and objectives
    if st_get("fp_intro_t", 0.0) > 0.0 {
        bind a = clampf(st_get("fp_intro_t", 0.0), 0.0, 1.0)
        g_text_c(cx, y + h * 0.2, 28.0, c_mix(0.0, C_WHITE, a), sc_get("name", ""))
        g_text_c(cx, y + h * 0.2 + 36.0, 13.0, c_mix(0.0, c_rgb(180.0, 200.0, 160.0), a), fps_diff_name(fps_diff()) + "  -  " + num_str(st_llen("fp_obj")) + " objectives")
    }
    if st_get("fp_paused", 0.0) > 0.5 { fps_watch(x, y, w, h) }
    return 0.0
}
# the pause screen: a watch face with the mission status
fn fps_watch(x, y, w, h) {
    g_rect_a(x, y, w, h, 0.0, 0.55)
    bind ww = min(560.0, w - 40.0)
    bind wh = min(380.0, h - 40.0)
    bind wx = x + (w - ww) * 0.5
    bind wy = y + (h - wh) * 0.5
    g_rrect(wx - 14.0, wy - 14.0, ww + 28.0, wh + 28.0, 40.0, c_rgb(150.0, 150.0, 158.0))
    g_rrect(wx - 6.0, wy - 6.0, ww + 12.0, wh + 12.0, 32.0, c_rgb(40.0, 40.0, 44.0))
    g_rrect(wx, wy, ww, wh, 26.0, c_rgb(150.0, 175.0, 120.0))
    bind ink = c_rgb(30.0, 40.0, 24.0)
    g_text(wx + 30.0, wy + 24.0, 20.0, ink, "MISSION STATUS")
    g_text_r(wx + ww - 30.0, wy + 28.0, 13.0, ink, sc_get("name", "") + "  /  " + fps_diff_name(fps_diff()))
    px_hline(wx + 30.0, wy + 54.0, ww - 60.0, ink, 0.8)
    bind ly = wy + 70.0
    bind i = 0.0
    while i < st_llen("fp_obj") {
        bind o = st_lget("fp_obj", i)
        bind done = st_lget("fp_objdone", i) > 0.5
        if o == "exit" { bind done = false }
        g_text(wx + 30.0, ly, 14.0, ink, list_get(["a", "b", "c", "d", "e", "f"], min(i, 5.0)) + ".  " + fps_obj_text(o))
        g_text_r(wx + ww - 30.0, ly, 14.0, sel(done, c_rgb(20.0, 90.0, 20.0), c_rgb(110.0, 30.0, 20.0)), sel(done, "COMPLETE", "INCOMPLETE"))
        bind ly = ly + 26.0
        bind i = i + 1.0
    }
    bind secs = st_get("game_t", 0.0) - st_get("fp_start", 0.0)
    g_text(wx + 30.0, wy + wh - 76.0, 13.0, ink, "Time " + num_str(floor(secs / 60.0)) + ":" + pad2(floor(secs % 60.0)) + "    Kills " + num_str(st_get("fp_kills", 0.0)) + "    Health " + num_str(round(st_get("fp_hp", 0.0))) + "    Armour " + num_str(round(st_get("fp_armor", 0.0))))
    g_text(wx + 30.0, wy + wh - 46.0, 13.0, ink, "P / Tab / Esc  resume        Q  abort mission")
}

← Back to index