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_game.ling
# rt_game.ling - the game runtime: scene loading and switching, the player controller, camera rigs,
# triggers, timelines and cutscenes, HUD overlays, 2-D menus with 3-D backgrounds, music, MIDI,
# scripts hooks, co-op networking. The editor's Play uses this in its viewport; exported games
# run it full screen from the generated main.ling.
fn game_quit() { return st_get("game_quit", 0.0) }
fn game_time() { return st_get("game_t", 0.0) }
fn game_dt() { return st_get("game_dt", 0.016) }
fn game_var(k) { return st_get("gv_" + k, 0.0) }
fn game_set(k, v) { st_set("gv_" + k, v) }
fn game_root() { return st_get("proj_root", "") }
fn game_path(p) {
if p == "" { return "" }
if p_is_abs(p) || game_root() == "" { return p }
if str_starts_with(p_norm(p), game_root()) { return p_norm(p) }
return p_join(game_root(), p)
}
# ── boot / shutdown ──────────────────────────────────────────────────────────
fn game_boot(start_scene, title, w, h) {
game_setup_common(title, w, h)
bind first = start_scene
if st_get("game_embedded", 0.0) < 0.5 && st_get("game_start_override", "") != "" { bind first = st_get("game_start_override", "") }
game_goto(first)
}
# play a scene from text (the editor plays unsaved scenes this way)
fn game_boot_text(text, title, w, h) {
game_setup_common(title, w, h)
nd_use("")
sc_parse(text)
game_scene_enter()
}
fn game_setup_common(title, w, h) {
st_set("game_music_cur", "")
st_set("game_quit", 0.0)
st_set("game_t", 0.0)
st_set("game_now", time_now())
st_set("game_scene_t", 0.0)
st_set("game_title", title)
st_set("game_say_until", 0.0)
st_set("game_say_text", "")
st_set("game_msg", "")
st_lclear("game_stack")
st_set("game_cut_return", "")
st_set("game_hud", "")
st_set("game_bg", "")
if st_get("game_embedded", 0.0) < 0.5 {
# --shot N takes a screenshot after N frames and quits (headless checks); --size WxH overrides the window
bind args = plat_args()
bind ai = 0.0
while ai < len(args) {
if list_get(args, ai) == "--shot" && ai + 1.0 < len(args) { st_set("game_shot_at", to_number(list_get(args, ai + 1.0))) }
if list_get(args, ai) == "--scene" && ai + 1.0 < len(args) { st_set("game_start_override", list_get(args, ai + 1.0)) }
if list_get(args, ai) == "--autoplay" { st_set("game_autoplay", 1.0) }
if list_get(args, ai) == "--shots" && ai + 1.0 < len(args) { st_set("game_shots", list_get(args, ai + 1.0)) }
if list_get(args, ai) == "--size" && ai + 1.0 < len(args) {
bind sp = str_split(list_get(args, ai + 1.0), "x")
if len(sp) > 1.5 { bind w = to_number(list_get(sp, 0)) bind h = to_number(list_get(sp, 1)) }
}
bind ai = ai + 1.0
}
st_set("game_frames", 0.0)
open_window(w, h, title, 1.0)
px_flat()
if st_get("font", 0.0 - 1.0) < 0.0 || st_has("font") < 0.5 {
bind f = font_load(game_path("assets/fonts/ui.ttf"))
if f < 0.0 { bind f = font_load("assets/font.ttf") }
st_set("font", f)
}
if game_root() == "" { st_set("proj_root", fs_cwd()) }
}
}
fn game_shutdown() {
if plat_has_audio() { plat_music_stop() }
st_set("game_music_cur", "")
st_set("game_quit", 1.0)
}
# ── scenes ───────────────────────────────────────────────────────────────────
fn game_goto(path) {
bind full = game_path(path)
nd_use("")
if !sc_load(full) {
st_set("game_msg", "Scene not found: " + path)
return false
}
bind prev = st_get("game_scene", "")
if prev != "" && prev != path && st_get("game_nav_back", 0.0) < 0.5 {
st_lpush("game_stack", prev)
while st_llen("game_stack") > 16.0 { st_ldel("game_stack", 0.0) }
}
st_set("game_nav_back", 0.0)
st_set("game_scene", path)
game_scene_enter()
return true
}
fn game_scene_enter() {
st_set("game_scene_t", 0.0)
st_set("game_scene_t0", st_get("game_t", 0.0))
st_lclear("trig_done")
cut_reset()
# HUD overlay named by the scene
st_set("game_hud", "")
bind hud = sc_get("hud", "")
if hud != "" { game_push_hud(hud) }
# 3-D background of a 2-D menu
st_set("game_bg", "")
bind bg = sc_ex("bg3d", "")
if bg != "" {
nd_use("b")
if sc_load(game_path(bg)) { st_set("game_bg", bg) }
nd_use("")
}
# music
if plat_has_audio() {
bind m = sc_get("music", "")
if m != "" {
# the same track carries on across scenes (title -> mission select -> difficulty)
if m != st_get("game_music_cur", "") {
bind mid = plat_music_load(game_path(m))
if mid >= 0.0 { plat_music_volume(0.8) plat_music_play(mid) st_set("game_music_cur", m) }
}
} else if sc_get("kind", "3d") != "cutscene" { plat_music_stop() st_set("game_music_cur", "") }
}
# player spawn
bind p = game_player()
bind s = nd_find_tag("player")
bind sidx = 0.0 - 1.0
bind i = 0.0
while i < nd_count() {
if nd_get(i, "kind") == "spawn" && (nd_get(i, "tag") == "player" || sidx < 0.0) { bind sidx = i }
bind i = i + 1.0
}
if p >= 0.0 && sidx >= 0.0 {
bind wp = nd_world_pos(sidx)
nd_set(p, "px", list_get(wp, 0))
nd_set(p, "py", list_get(wp, 1) - nd_get(p, "sy"))
nd_set(p, "pz", list_get(wp, 2))
nd_set(p, "ry", nd_get(sidx, "ry"))
}
st_set("pl_vy", 0.0)
st_set("pl_ground", 1.0)
# camera start
bind c = game_camera_node()
if c >= 0.0 && nd_get(c, "b") < 0.5 {
bind wp = nd_world_pos(c)
cam_set(list_get(wp, 0), list_get(wp, 1), list_get(wp, 2), nd_get(c, "ry"), nd_get(c, "rx"), max(0.5, nd_get(c, "a")))
} else if p >= 0.0 {
bind wp = nd_world_pos(p)
cam_orbit(list_get(wp, 0), list_get(wp, 1) - 1.0, list_get(wp, 2), nd_get(p, "ry"), 0.3, 9.0, 2.0)
} else {
cam_orbit(sc_get("camx", 0.0), sc_get("camy", 0.0), sc_get("camz", 0.0), sc_get("camyaw", 0.6), sc_get("campitch", 0.4), sc_get("camzd", 16.0), 2.0)
}
# music / midi nodes with autoplay, model animation state
bind i = 0.0
while i < nd_count() {
bind k = nd_get(i, "kind")
if k == "music" && nd_get(i, "c") > 0.5 && plat_has_audio() && nd_get(i, "src") != "" {
bind mid = plat_music_load(game_path(nd_get(i, "src")))
if mid >= 0.0 { plat_music_volume(nd_get(i, "a")) plat_music_play(mid) st_set("game_music_cur", "") }
}
if k == "audio" && nd_get(i, "c") > 0.5 && plat_has_audio() && nd_get(i, "src") != "" {
bind sid = plat_sample_load(game_path(nd_get(i, "src")))
if sid >= 0.0 {
bind wp = nd_world_pos(i)
plat_sample_play(sid, list_get(wp, 0), list_get(wp, 1), list_get(wp, 2), nd_get(i, "d"), nd_get(i, "a"), nd_get(i, "b"))
}
}
if k == "midi" && nd_get(i, "src") != "" && plat_has_midi() {
bind mid = plat_midi_load(game_path(nd_get(i, "src")))
st_set("midi_" + num_str(nd_id(i)), mid)
if mid >= 0.0 { st_set("midin_" + num_str(nd_id(i)), plat_midi_bars(mid)) }
}
bind i = i + 1.0
}
if sc_ex("mode", "") == "fps" { fps_enter() } else { fps_leave() }
if sc_ex("mode", "") == "brick" { brick_enter() } else { brick_leave() }
if sc_ex("mode", "") == "soul" { soul_enter() } else { soul_leave() }
if sc_ex("mode", "") == "story" { story_enter() } else { story_leave() }
game_scripts_call("ready", 0.0, "")
}
fn game_push_hud(path) {
nd_use("h")
bind ok = sc_load(game_path(path))
nd_use("")
if ok { st_set("game_hud", path) }
return ok
}
fn game_pop_hud() { st_set("game_hud", "") }
fn game_play_cutscene(path) {
st_set("game_cut_return", st_get("game_scene", ""))
game_goto(path)
}
# ── queries ──────────────────────────────────────────────────────────────────
fn game_player() {
bind n = nd_count()
bind i = 0.0
while i < n {
if nd_get(i, "kind") == "actor" && nd_get(i, "tag") == "player" { return i }
bind i = i + 1.0
}
return 0.0 - 1.0
}
fn game_camera_node() {
bind n = nd_count()
bind i = 0.0
while i < n {
if nd_get(i, "kind") == "camera" && nd_get(i, "vis") > 0.5 { return i }
bind i = i + 1.0
}
return 0.0 - 1.0
}
fn game_find(name) { return nd_find_name(name) }
fn game_find_tag(tag) { return nd_find_tag(tag) }
fn game_spawn(kind, name, x, y, z) {
bind i = nd_add(kind, name, 0.0)
nd_set(i, "px", x)
nd_set(i, "py", y)
nd_set(i, "pz", z)
return i
}
fn game_destroy(i) {
if i >= 0.0 && i < nd_count() { nd_del_id(nd_id(i)) }
}
fn game_respawn(i) {
bind sidx = nd_find_tag("player")
bind j = 0.0
while j < nd_count() {
if nd_get(j, "kind") == "spawn" { bind sidx = j }
bind j = j + 1.0
}
if sidx >= 0.0 && nd_get(sidx, "kind") == "spawn" {
bind wp = nd_world_pos(sidx)
nd_set(i, "px", list_get(wp, 0))
nd_set(i, "py", list_get(wp, 1) - nd_get(i, "sy"))
nd_set(i, "pz", list_get(wp, 2))
} else { nd_set(i, "px", 0.0) nd_set(i, "py", 0.0 - nd_get(i, "sy")) nd_set(i, "pz", 0.0) }
st_set("pl_vy", 0.0)
}
fn game_say(speaker, text, secs) {
st_set("game_say_speaker", speaker)
st_set("game_say_text", text)
st_set("game_say_until", st_get("game_t", 0.0) + secs)
}
fn game_sound(path, x, y, z) {
if !plat_has_audio() { return 0.0 }
bind sid = plat_sample_load(game_path(path))
if sid >= 0.0 { plat_sample_play(sid, x, y, z, 1.0, 1.0, 0.0) }
return 0.0
}
fn game_music(path) {
if !plat_has_audio() { return 0.0 }
bind mid = plat_music_load(game_path(path))
if mid >= 0.0 { plat_music_play(mid) st_set("game_music_cur", path) }
return 0.0
}
fn game_sfx(freq, dur) {
if plat_has_audio() { plat_sfx(0.0, 0.0, 0.0, 1.0, freq, 0.3, dur, "sine") }
}
fn game_key(name) { return key_down(name) }
fn game_key_pressed(name) { return key_pressed(name) }
fn game_axis_x() {
bind v = 0.0
if key_down("a") || (key_down("left") && !fps_on()) { bind v = v - 1.0 }
if key_down("d") || (key_down("right") && !fps_on()) { bind v = v + 1.0 }
if plat_gamepad_connected() { bind v = v + plat_gamepad_lx() }
return clampf(v, 0.0 - 1.0, 1.0)
}
fn game_axis_y() {
bind v = 0.0
if key_down("w") || key_down("up") { bind v = v + 1.0 }
if key_down("s") || key_down("down") { bind v = v - 1.0 }
if plat_gamepad_connected() { bind v = v - plat_gamepad_ly() }
return clampf(v, 0.0 - 1.0, 1.0)
}
# MIDI helpers (midi node i)
fn game_midi_notes(i) { return st_get("midin_" + num_str(nd_id(i)), []) }
fn game_on_beat(i, t) {
bind notes = game_midi_notes(i)
bind n = floor(len(notes) / 3.0)
bind k = 0.0
while k < n {
bind nt = list_get(notes, k * 3.0)
if abs(nt - t) < 0.06 { return true }
if nt > t + 0.1 { return false }
bind k = k + 1.0
}
return false
}
# ── scripts ──────────────────────────────────────────────────────────────────
fn game_scripts_call(hook, dt, arg) {
bind n = nd_count()
bind i = 0.0
while i < n {
bind s = nd_get(i, "scr")
if s != "" && nd_get(i, "vis") > 0.5 { user_script(s, hook, i, dt, arg) }
bind i = i + 1.0
}
}
fn game_input_keys() { return ["space", "enter", "e", "f", "q", "r", "tab", "1", "2", "3", "4", "escape", "j", "k", "x", "z"] }
# ── per-frame ────────────────────────────────────────────────────────────────
fn game_frame() {
bind now = time_now()
bind dt = clampf(now - st_get("game_now", now), 0.0, 0.05)
st_set("game_now", now)
st_set("game_dt", dt)
st_set("game_t", st_get("game_t", 0.0) + dt)
st_set("game_scene_t", st_get("game_scene_t", 0.0) + dt)
if st_get("game_embedded", 0.0) < 0.5 { u_begin() }
game_update(dt)
bind W = get_width()
bind H = get_height()
game_render(0.0, 0.0, W, H)
if st_get("game_embedded", 0.0) < 0.5 {
u_end()
present()
if key_pressed("escape") && sc_get("kind", "3d") != "2d" && !fps_on() && !brick_on() && !soul_on() && !story_on() { st_set("game_quit", 1.0) }
st_set("game_frames", st_get("game_frames", 0.0) + 1.0)
if st_get("game_shot_at", 0.0) > 0.5 && st_get("game_frames", 0.0) >= st_get("game_shot_at", 0.0) {
plat_screenshot("game")
st_set("game_quit", 1.0)
}
# --shots 60,200,400: a screenshot at each frame, quit after the last
if st_get("game_shots", "") != "" {
bind sh = nums_parse(st_get("game_shots", ""))
bind fr = st_get("game_frames", 0.0)
bind k = 0.0
while k < len(sh) {
if list_get(sh, k) == fr { plat_screenshot("f" + num_str(fr)) }
bind k = k + 1.0
}
if fr >= list_get(sh, len(sh) - 1.0) { st_set("game_quit", 1.0) }
}
}
}
fn game_update(dt) {
nd_use("")
bind kind = sc_get("kind", "3d")
bind t = st_get("game_scene_t", 0.0)
bind ln = sc_get("len", 10.0)
bind tl_t = t
if kind != "cutscene" && ln > 0.0 { bind tl_t = t - floor(t / ln) * ln }
st_set("game_tl_t", tl_t)
# network
net_poll()
# cutscene tracks
if kind == "cutscene" || clip_count() > 0.0 {
cut_tick(t, true)
bind ev = st_get("cut_event", "")
if ev != "" { game_handle_event(ev, st_get("cut_event_arg", "")) }
if kind == "cutscene" && (t > ln || (key_pressed("escape") && st_get("game_embedded", 0.0) < 0.5)) { game_cutscene_done() return 0.0 }
}
if brick_on() {
brick_update(dt)
} else if story_on() {
story_update(dt)
} else if soul_on() {
soul_update(dt)
} else if fps_on() {
fps_update(dt)
if fps_on() && st_get("fp_paused", 0.0) < 0.5 && st_get("fp_dead", 0.0) < 0.5 { game_triggers() }
} else {
if kind == "3d" || (kind == "cutscene" && game_player() >= 0.0) {
game_player_update(dt)
game_triggers()
}
if kind != "cutscene" { game_camera_update(dt) }
}
if kind == "2d" { game_menu_keys() }
# input hooks
bind keys = game_input_keys()
bind k = 0.0
while k < len(keys) {
if key_pressed(list_get(keys, k)) { game_scripts_call("input", dt, list_get(keys, k)) }
bind k = k + 1.0
}
game_scripts_call("update", dt, "")
# background 3-D scene of a menu: slow orbit
if st_get("game_bg", "") != "" {
nd_use("b")
bind spin = to_number(sc_ex("bgspin", "0.12"))
bind sway = to_number(sc_ex("bgsway", "0"))
bind yaw = sc_get("camyaw", 0.0) + st_get("game_t", 0.0) * spin + sin(st_get("game_t", 0.0) * 0.21) * sway
cam_orbit(sc_get("camx", 0.0), sc_get("camy", 0.0), sc_get("camz", 0.0), yaw, sc_get("campitch", 0.35), sc_get("camzd", 16.0), to_number(sc_ex("bgzd", "2")))
nd_use("")
}
return 0.0
}
# arrows / W S move the selection between the scene's buttons, Enter / Space press it, Esc goes back
fn game_menu_keys() {
bind btns = hud_buttons()
if len(btns) < 0.5 { return 0.0 }
bind cur = l_index(btns, st_get("menu_sel", 0.0))
bind moved = false
if key_pressed("down") || key_pressed("s") { bind cur = cur + 1.0 bind moved = true }
if key_pressed("up") || key_pressed("w") { bind cur = cur - 1.0 bind moved = true }
if plat_gamepad_connected() {
bind ly = plat_gamepad_ly()
bind was = st_get("pad_y_was", 0.0)
if ly > 0.6 && was < 0.6 { bind cur = cur + 1.0 bind moved = true }
if ly < 0.0 - 0.6 && was > 0.0 - 0.6 { bind cur = cur - 1.0 bind moved = true }
st_set("pad_y_was", ly)
if plat_gamepad_button(0.0) && st_get("pad_a_was", 0.0) < 0.5 { st_set("hud_key_fire", 1.0) }
st_set("pad_a_was", bool_num(plat_gamepad_button(0.0)))
}
if cur < 0.0 { bind cur = sel(moved, len(btns) - 1.0, 0.0) }
if cur >= len(btns) { bind cur = 0.0 }
if moved || l_index(btns, st_get("menu_sel", 0.0)) < 0.0 {
st_set("menu_sel", list_get(btns, cur))
if moved && plat_has_audio() { plat_ui_sound("tick") }
}
if key_pressed("enter") || key_pressed("space") { st_set("hud_key_fire", 1.0) }
if key_pressed("escape") && st_get("game_embedded", 0.0) < 0.5 { game_action("back", 0.0 - 1.0) }
return 0.0
}
fn game_cutscene_done() {
bind nxt = sc_get("next", "")
bind back = st_get("game_cut_return", "")
if nxt != "" { game_goto(nxt) }
else if back != "" { st_set("game_cut_return", "") game_goto(back) }
else { st_set("game_quit", 1.0) }
}
fn game_handle_event(ev, arg) {
if ev == "scene" && arg != "" { game_goto(arg) }
else if ev == "call" { game_scripts_call("trigger", 0.0, arg) }
else if ev == "end" { game_cutscene_done() }
else if ev == "wait" { st_set("game_wait", 1.0) }
}
# "goto:path", "cutscene:path", "say:text", "sound:file", "music:file", "call:hook", "hud:path", "quit", "set:k=v"
fn game_action(act, source_i) {
if act == "" { return 0.0 }
if str_find(act, ";") >= 0.0 {
bind parts = str_split(act, ";")
bind pi = 0.0
while pi < len(parts) {
game_action(str_trim(list_get(parts, pi)), source_i)
bind pi = pi + 1.0
}
return 0.0
}
bind kind = act
bind arg = ""
bind ci = str_find(act, ":")
if ci >= 0.0 {
bind kind = str_slice(act, 0.0, ci)
bind arg = str_slice(act, ci + 1.0, str_len(act) - ci - 1.0)
}
if kind == "goto" && str_starts_with(arg, "@") { bind arg = st_get("gs_" + str_slice(arg, 1.0, str_len(arg) - 1.0), "") }
if kind == "goto" && arg != "" { game_goto(arg) }
else if kind == "store" {
bind eq = str_find(arg, "=")
if eq > 0.0 { st_set("gs_" + str_slice(arg, 0.0, eq), str_slice(arg, eq + 1.0, str_len(arg) - eq - 1.0)) }
}
else if kind == "exit" { fps_try_exit() }
else if kind == "cutscene" && arg != "" { game_play_cutscene(arg) }
else if kind == "say" { game_say("", arg, 3.0) }
else if kind == "sound" { game_sound(arg, cam_x(), cam_y(), cam_z()) }
else if kind == "music" { game_music(arg) }
else if kind == "call" { game_scripts_call("trigger", 0.0, arg) }
else if kind == "hud" { game_push_hud(arg) }
else if kind == "quit" { st_set("game_quit", 1.0) }
else if kind == "set" {
bind kv = str_split(arg, "=")
if len(kv) > 1.0 { game_set(list_get(kv, 0), to_number(list_get(kv, 1))) }
}
else if kind == "add" {
bind kv = str_split(arg, "=")
if len(kv) > 1.0 { game_set(list_get(kv, 0), game_var(list_get(kv, 0)) + to_number(list_get(kv, 1))) }
}
else if kind == "back" {
bind back = st_get("game_cut_return", "")
if back != "" { st_set("game_cut_return", "") st_set("game_nav_back", 1.0) game_goto(back) }
else if st_llen("game_stack") > 0.5 {
bind prev = st_lget("game_stack", st_llen("game_stack") - 1.0)
st_ldel("game_stack", st_llen("game_stack") - 1.0)
st_set("game_nav_back", 1.0)
game_goto(prev)
} else if st_get("game_embedded", 0.0) < 0.5 { st_set("game_quit", 1.0) }
}
else if kind == "host" { net_host_game(sel(arg == "", 7777.0, to_number(arg))) game_say("", "Hosting on port " + sel(arg == "", "7777", arg) + " - a friend can Join with your IP", 4.0) }
else if kind == "join" {
bind jp = str_split(arg, ":")
net_join_game(list_get(jp, 0), sel(len(jp) > 1.0, to_number(list_get(jp, 1)), 7777.0))
game_say("", "Joining " + arg, 3.0)
}
else if kind == "vol" {
bind v = clampf(st_get("gv_volume", 0.8) + to_number(arg), 0.0, 1.0)
game_set("volume", v)
if plat_has_audio() { plat_volume(v) plat_music_volume(v) }
game_say("", "Volume " + num_str(round(v * 100.0)) + "%", 1.5)
}
else if kind == "toggle" { game_set(arg, 1.0 - game_var(arg)) game_say("", arg + sel(game_var(arg) > 0.5, " on", " off"), 1.5) }
else if kind == "lang" {
bind langs = ["English", "Daemon", "Thai", "Korean", "Japanese", "Chinese"]
bind li = (game_var("lang") + 1.0) % 6.0
game_set("lang", li)
game_say("", "Language: " + list_get(langs, li) + " (the LingDev edition speaks English)", 2.5)
}
return 0.0
}
# ── player controller: WASD / gamepad, jump, gravity, solid boxes ────────────
fn game_player_update(dt) {
bind p = game_player()
if p < 0.0 { return 0.0 }
bind speed = max(0.5, nd_get(p, "d"))
bind ax = game_axis_x()
bind ay = game_axis_y()
# camera-relative on the ground plane
bind yaw = cam_yaw()
bind fx = sin(yaw)
bind fz = cos(yaw)
bind rx = cos(yaw)
bind rz = 0.0 - sin(yaw)
bind mx = (fx * ay + rx * ax)
bind mz = (fz * ay + rz * ax)
bind ml = sqrt(mx * mx + mz * mz)
if ml > 1.0 { bind mx = mx / ml bind mz = mz / ml }
bind x = nd_get(p, "px")
bind y = nd_get(p, "py")
bind z = nd_get(p, "pz")
bind hx = nd_get(p, "sx")
bind hy = nd_get(p, "sy")
bind hz = nd_get(p, "sz")
bind nx = x + mx * speed * dt
bind nz = z + mz * speed * dt
if ml > 0.05 { nd_set(p, "ry", atan2(mx, mz)) }
# vertical: +y is down
bind vy = st_get("pl_vy", 0.0)
bind grounded = st_get("pl_ground", 0.0) > 0.5
if grounded && !fps_on() && (key_pressed("space") || plat_gamepad_button(0.0)) { bind vy = 0.0 - sqrt(2.0 * sc_get("grav", 9.0) * 2.2) bind grounded = false }
bind vy = vy + sc_get("grav", 9.0) * dt
bind ny = y + vy * dt
bind floor_y = sc_get("floor", 0.0)
bind grounded = false
if ny + hy >= floor_y { bind ny = floor_y - hy bind vy = 0.0 bind grounded = true }
# solids
bind n = nd_count()
bind i = 0.0
while i < n {
bind k = nd_get(i, "kind")
if i != p && nd_get(i, "vis") > 0.5 && (nd_get(i, "tag") == "solid" || (k == "box" && nd_get(i, "tag") != "pickup" && nd_get(i, "tag") != "deco")) && (k == "box" || k == "plane") {
bind wp = nd_world_pos_t(i, st_get("game_tl_t", 0.0))
bind bx = list_get(wp, 0)
bind by = list_get(wp, 1)
bind bz = list_get(wp, 2)
bind sx = nd_get(i, "sx")
bind sy = nd_get(i, "sy")
bind sz = nd_get(i, "sz")
if k == "plane" { bind sy = 0.05 }
# landing on top
bind over = nx + hx > bx - sx && nx - hx < bx + sx && nz + hz > bz - sz && nz - hz < bz + sz
if over {
bind top = by - sy
if y + hy <= top + 0.08 && ny + hy >= top && vy >= 0.0 { bind ny = top - hy bind vy = 0.0 bind grounded = true }
else if ny + hy > top && ny - hy < by + sy && k == "box" {
# side push-out on the shallower axis
bind ox = sel(nx > bx, (bx + sx) - (nx - hx), (nx + hx) - (bx - sx))
bind oz = sel(nz > bz, (bz + sz) - (nz - hz), (nz + hz) - (bz - sz))
if ox < oz { bind nx = sel(nx > bx, bx + sx + hx, bx - sx - hx) } else { bind nz = sel(nz > bz, bz + sz + hz, bz - sz - hz) }
}
}
}
bind i = i + 1.0
}
nd_set(p, "px", nx)
nd_set(p, "py", ny)
nd_set(p, "pz", nz)
st_set("pl_vy", vy)
st_set("pl_ground", bool_num(grounded))
# overlap hooks with scripted nodes
bind i = 0.0
while i < n {
if i != p && nd_get(i, "scr") != "" && nd_get(i, "kind") != "trigger" {
bind wp = nd_world_pos(i)
if abs(list_get(wp, 0) - nx) < nd_get(i, "sx") + hx && abs(list_get(wp, 2) - nz) < nd_get(i, "sz") + hz && abs(list_get(wp, 1) - ny) < nd_get(i, "sy") + hy { user_script(nd_get(i, "scr"), "hit", i, dt, p) }
}
bind i = i + 1.0
}
return 0.0
}
fn game_triggers() {
bind p = game_player()
if p < 0.0 { return 0.0 }
bind x = nd_get(p, "px")
bind y = nd_get(p, "py")
bind z = nd_get(p, "pz")
bind n = nd_count()
bind i = 0.0
while i < n {
if nd_get(i, "kind") == "trigger" && nd_get(i, "vis") > 0.5 {
bind wp = nd_world_pos(i)
bind inside = abs(list_get(wp, 0) - x) < nd_get(i, "sx") && abs(list_get(wp, 1) - y) < nd_get(i, "sy") + 0.5 && abs(list_get(wp, 2) - z) < nd_get(i, "sz")
bind key = "trig_in_" + num_str(nd_id(i))
bind was = st_get(key, 0.0) > 0.5
if inside && !was {
bind once = nd_get(i, "a") > 0.5
bind done = l_has(sl_to_list("trig_done"), nd_id(i))
if !(once && done) {
if once { st_lpush("trig_done", nd_id(i)) }
game_action(nd_get(i, "txt"), i)
if nd_get(i, "scr") != "" { user_script(nd_get(i, "scr"), "trigger", i, 0.0, p) }
user_script(nd_get(p, "scr"), "trigger", p, 0.0, i)
}
}
st_set(key, bool_num(inside))
}
bind i = i + 1.0
}
return 0.0
}
# ── camera rigs ──────────────────────────────────────────────────────────────
fn game_camera_update(dt) {
bind c = game_camera_node()
bind p = game_player()
bind t = st_get("game_tl_t", 0.0)
if c < 0.0 {
if p >= 0.0 {
bind wp = nd_world_pos(p)
bind yaw = st_get("cam_follow_yaw", nd_get(p, "ry"))
# orbit with the mouse right button, otherwise ease behind the player
if mouse_down_right() { bind yaw = yaw + u_dx() * 0.006 } else { bind yaw = ease_angle(yaw, nd_get(p, "ry"), 0.0, "linear") + 0.0 }
st_set("cam_follow_yaw", yaw)
cam_orbit(list_get(wp, 0), list_get(wp, 1) - 0.8, list_get(wp, 2), yaw, 0.32, 9.0, 2.0)
}
return 0.0
}
bind rig = nd_get(c, "b")
bind zd = max(0.5, nd_get(c, "a"))
bind dist = max(0.5, nd_get(c, "d"))
bind target = nd_find_name(nd_get(c, "txt"))
if target < 0.0 { bind target = p }
if rig < 0.5 || rig > 2.5 {
bind wp = nd_world_pos_t(c, t)
cam_set(list_get(wp, 0), list_get(wp, 1), list_get(wp, 2), nd_world_yaw_t(c, t), nd_val(c, "rx", t), zd)
} else if rig < 1.5 {
bind tx = 0.0 bind ty = 0.0 bind tz = 0.0
if target >= 0.0 { bind wp = nd_world_pos(target) bind tx = list_get(wp, 0) bind ty = list_get(wp, 1) bind tz = list_get(wp, 2) }
bind yaw = nd_get(c, "ry") + st_get("game_t", 0.0) * nd_get(c, "c")
if mouse_down_right() { nd_set(c, "ry", nd_get(c, "ry") + u_dx() * 0.006) }
cam_orbit(tx, ty, tz, yaw, nd_get(c, "rx"), dist, zd)
} else {
if target >= 0.0 {
bind wp = nd_world_pos(target)
bind yaw = st_get("cam_follow_yaw", nd_get(target, "ry"))
if mouse_down_right() { bind yaw = yaw + u_dx() * 0.006 } else { bind yaw = yaw + (nd_get(target, "ry") - yaw) * min(1.0, dt * 2.0) }
st_set("cam_follow_yaw", yaw)
cam_orbit(list_get(wp, 0), list_get(wp, 1) - 0.8, list_get(wp, 2), yaw, nd_get(c, "rx"), dist, zd)
}
}
return 0.0
}
# ── rendering ────────────────────────────────────────────────────────────────
fn game_render(x, y, w, h) {
nd_use("")
bind kind = sc_get("kind", "3d")
bind t = st_get("game_tl_t", 0.0)
if kind == "cutscene" { bind t = st_get("game_scene_t", 0.0) }
cam_viewport(x, y, w, h)
if kind == "2d" {
g_rect(x, y, w, h, sc_get("bg", 0.0))
if st_get("game_bg", "") != "" {
nd_use("b")
draw_scene_game(st_get("game_t", 0.0))
nd_use("")
}
bind fit = hud_fit(x, y, w, h)
bind act = hud_draw(t, list_get(fit, 0), list_get(fit, 1), list_get(fit, 2), true)
if act != "" { game_action(act, nd_find(st_get("hud_action_node", 0.0))) }
} else {
g_rect(x, y, w, h, sc_get("bg", 0.0))
draw_scene_game(t)
if fps_on() { fps_draw_view() }
game_scripts_call("draw", 0.0, "")
if story_on() { plat_post_now() }
if st_get("game_hud", "") != "" {
nd_use("h")
bind fit = hud_fit(x, y, w, h)
bind act = hud_draw(st_get("game_t", 0.0), list_get(fit, 0), list_get(fit, 1), list_get(fit, 2), true)
nd_use("")
if act != "" { game_action(act, 0.0 - 1.0) }
}
cut_overlay(x, y, w, h)
if fps_on() { fps_hud(x, y, w, h) }
if brick_on() { brick_hud(x, y, w, h) }
if soul_on() { soul_hud(x, y, w, h) }
if story_on() { story_hud(x, y, w, h) }
}
# script subtitles
if st_get("game_say_until", 0.0) > st_get("game_t", 0.0) && st_get("game_say_text", "") != "" {
bind txt = st_get("game_say_text", "")
bind sp = st_get("game_say_speaker", "")
bind size = max(11.0, h * 0.026)
bind bw = g_text_w(size, txt) + 40.0 + sel(sp != "", g_text_w(size, sp + ": "), 0.0)
bind bx = x + (w - bw) * 0.5
bind by = y + h * 0.82
g_rrect_a(bx, by, bw, size + 18.0, 6.0, C_BG_DEEP, 0.8)
if sp != "" { g_text(bx + 20.0, by + 9.0, size, C_YELLOW, sp + ": ") g_text(bx + 20.0 + g_text_w(size, sp + ": "), by + 9.0, size, C_TEXT_STRONG, txt) }
else { g_text(bx + 20.0, by + 9.0, size, C_TEXT_STRONG, txt) }
}
bind msg = st_get("game_msg", "")
if msg != "" { g_text(x + 12.0, y + h - 24.0, 12.0, C_RED, msg) }
}
# ── co-op networking (desktop): share the player transform ──────────────────
fn net_host_game(port) {
if !plat_has_net() { return 0.0 }
plat_net_host(port)
st_set("net_on", 1.0)
}
fn net_join_game(ip, port) {
if !plat_has_net() { return 0.0 }
plat_net_join(ip, port)
st_set("net_on", 1.0)
}
fn net_send_state() {
if st_get("net_on", 0.0) < 0.5 { return 0.0 }
bind p = game_player()
if p < 0.0 { return 0.0 }
plat_net_send("P|" + num_str(nd_get(p, "px")) + "|" + num_str(nd_get(p, "py")) + "|" + num_str(nd_get(p, "pz")) + "|" + num_str(nd_get(p, "ry")))
}
fn net_poll() {
if st_get("net_on", 0.0) < 0.5 { return 0.0 }
net_send_state()
bind msg = plat_net_recv()
bind guard = 0.0
while msg != "" && guard < 32.0 {
bind f = str_split(msg, "|")
if list_get(f, 0) == "P" && len(f) >= 5.0 {
bind peer = nd_find_tag("peer")
if peer < 0.0 {
bind peer = game_spawn("actor", "Peer", to_number(list_get(f, 1)), to_number(list_get(f, 2)), to_number(list_get(f, 3)))
nd_set(peer, "tag", "peer")
nd_set(peer, "col", C_GREEN)
}
nd_set(peer, "px", to_number(list_get(f, 1)))
nd_set(peer, "py", to_number(list_get(f, 2)))
nd_set(peer, "pz", to_number(list_get(f, 3)))
nd_set(peer, "ry", to_number(list_get(f, 4)))
} else { game_scripts_call("net", 0.0, msg) }
bind msg = plat_net_recv()
bind guard = guard + 1.0
}
return 0.0
}