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_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")
}