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/importers.ling
# importers.ling - turn an existing Ling game folder into a LingDev project.
#
# Soul Symphony (../soul-symphony-ling) gets a faithful conversion: the press-start title over the
# hue-cycling palace pieces and ink horizon, the main menu rows (load game, new game, level select,
# multiplayer, options, language, quit) over the knotwork mandala, level select / multiplayer /
# options screens, one cutscene per chapter with every dialogue line, speaker and voice file on
# tracks and a backdrop in the chapter's mood, and the hub cathedral as a walkable level with the
# orb player. Models are the game's own .lmesh files; nothing is copied.
fn import_ling_game(root) {
if root == "" || !fs_is_dir(root) { status("Pick the game's folder") return 0.0 }
if file_exists(p_join(root, "cutscenes_dialog.txt")) && fs_is_dir(p_join(root, "game/voice")) { return import_soul_symphony(root) }
return import_generic(root)
}
# ── generic: index assets, title menu, model gallery ─────────────────────────
fn import_generic(root) {
bind name = p_base(root)
bind dir = p_join(st_get("app_root", "."), "projects")
plat_mkdir(dir)
if !proj_create(dir, name, 0.0, 1280.0, 720.0) { return 0.0 }
st_set("proj_source", p_norm(root))
st_set("proj_title", name)
assets_scan()
new_scene("3d")
sc_set("name", "Gallery")
bind k = 0.0
bind i = 0.0
while i < st_llen("as_path") && k < 12.0 {
if assets_kind(st_lget("as_ext", i)) == "model" {
bind ni = nd_add("model", p_stem(st_lget("as_path", i)), 0.0)
nd_set(ni, "src", st_lget("as_path", i))
nd_set(ni, "px", (k % 4.0) * 5.0 - 7.5)
nd_set(ni, "pz", floor(k / 4.0) * 5.0)
nd_set(ni, "py", 0.0)
bind k = k + 1.0
}
bind i = i + 1.0
}
bind a = nd_add("actor", "Player", 0.0)
nd_set(a, "py", 0.0 - 1.0)
nd_set(a, "pz", 0.0 - 6.0)
save_scene(p_join(proj_root(), "scenes/gallery.lscene"))
import_title_menu(name, "", ["Gallery|goto:scenes/gallery.lscene", "Quit|quit"], "scenes/gallery.lscene")
st_set("proj_start", "scenes/title.lscene")
import_write_run_original(root)
proj_save()
gen_write_all()
open_scene(p_join(proj_root(), "scenes/title.lscene"))
status("Imported " + name + ": " + num_str(k) + " models in the gallery, assets indexed, run_original.bat written")
return 0.0
}
# a framed-button menu (generic imports)
fn import_title_menu(title, logo, buttons, bg3d) {
sc_new("Title", "2d")
st_lclear("undo")
st_lclear("redo")
sel_clear()
st_set("tl_t", 0.0)
view_reset_camera()
sc_set("bg", 526603.0)
if bg3d != "" { sc_set("ex", "bg3d=" + bg3d) }
bind p = nd_add("panel", "Menu", 0.0)
nd_set(p, "px", 60.0)
nd_set(p, "py", 120.0)
nd_set(p, "w", 420.0)
nd_set(p, "h", 480.0)
nd_set(p, "a", 0.82)
nd_set(p, "col", 1447706.0)
if logo != "" {
bind im = nd_add("image", "Logo", nd_id(p))
nd_set(im, "src", logo)
nd_set(im, "px", 30.0)
nd_set(im, "py", 24.0)
nd_set(im, "w", 360.0)
nd_set(im, "h", 150.0)
} else {
bind l = nd_add("label", "Title", nd_id(p))
nd_set(l, "txt", title)
nd_set(l, "px", 30.0)
nd_set(l, "py", 30.0)
nd_set(l, "w", 360.0)
nd_set(l, "a", 32.0)
}
bind by = 200.0
bind i = 0.0
while i < len(buttons) {
bind parts = str_split(list_get(buttons, i), "|")
bind b = nd_add("button", list_get(parts, 0), nd_id(p))
nd_set(b, "txt", list_get(parts, 0))
nd_set(b, "scr", list_get(parts, 1))
nd_set(b, "px", 60.0)
nd_set(b, "py", by)
nd_set(b, "w", 300.0)
nd_set(b, "h", 34.0)
nd_set(b, "col", c_pal(i))
bind by = by + 42.0
bind i = i + 1.0
}
nd_set(p, "h", max(200.0, by + 20.0))
bind hint = nd_add("label", "Hint", 0.0)
nd_set(hint, "txt", "Made with LingDev - WASD move, Space jump, Esc back")
nd_set(hint, "px", 0.0)
nd_set(hint, "py", 0.0 - 36.0)
nd_set(hint, "w", 1280.0)
nd_set(hint, "a", 14.0)
nd_set(hint, "anc", "bc")
nd_set(hint, "col", C_TEXT_DIM)
save_scene(p_join(proj_root(), "scenes/title.lscene"))
}
fn import_write_run_original(root) {
bind entry = sel(file_exists(p_join(root, "main.ling")), "main.ling", sel(file_exists(p_join(root, "game/main.ling")), "game/main.ling", ""))
if entry == "" { return 0.0 }
bind exe = str_replace(ling_exe_guess(), "/", "\\")
plat_write(p_join(proj_root(), "run_original.bat"), "@echo off\r\nrem Plays the original game from its own folder.\r\ncd /d \"" + str_replace(root, "/", "\\") + "\"\r\nset LING=" + exe + "\r\nif not exist \"%LING%\" set LING=ling\r\n\"%LING%\" run --interp " + str_replace(entry, "/", "\\") + " %*\r\nif errorlevel 1 pause\r\n")
plat_write(p_join(proj_root(), "run_original.sh"), "#!/usr/bin/env sh\ncd \"" + root + "\"\n${LING:-ling} run --interp " + entry + " \"$@\"\n")
return 0.0
}
# ── Soul Symphony ────────────────────────────────────────────────────────────
fn ss_scene_id(header) {
bind l = str_lower(header)
bind out = ""
bind i = 0.0
while i < str_len(l) {
bind ch = str_slice(l, i, 1.0)
if str_find("abcdefghijklmnopqrstuvwxyz0123456789", ch) >= 0.0 { bind out = out + ch }
bind i = i + 1.0
}
if str_starts_with(out, "postlevel") { return "post_level" + str_slice(out, 9.0, 4.0) }
return out
}
fn ss_role(speaker) { return str_replace(str_lower(str_trim(speaker)), " ", "_") }
fn ss_two(n) {
if n < 10.0 { return "0" + num_str(n) }
return num_str(n)
}
fn ss_title(id) {
if id == "intro" { return "Intro - the poem in the snow globe" }
if id == "bonus" { return "Bonus - the leftover note" }
if id == "post_level1" { return "Prelude - the festival of first notes" }
if str_starts_with(id, "chapter") { return "Chapter " + str_slice(id, 7.0, 3.0) }
if str_starts_with(id, "finale") { return "Finale " + str_slice(id, 6.0, 3.0) }
return id
}
# the music a scene module plays: the first music/*.wav stem its source mentions
fn ss_scene_music(root, id, stems) {
bind src = plat_read(p_join(root, "game/scenes/" + id + ".ling"))
if src == "" { return "" }
bind best = ""
bind bestpos = 999999999.0
bind i = 0.0
while i < len(stems) {
bind s = list_get(stems, i)
bind pos = str_find(src, "\"" + s)
if pos < 0.0 { bind pos = str_find(src, s + "_data") }
if pos >= 0.0 && pos < bestpos { bind best = s bind bestpos = pos }
bind i = i + 1.0
}
if best == "" { return "" }
return p_join(root, "music/" + best + ".wav")
}
fn ss_music_stems(root) {
bind out = []
bind items = plat_list(p_join(root, "music"))
bind n = len(items) / 3.0
bind i = 0.0
while i < n {
bind nm = list_get(items, i * 3.0)
if !is_true(list_get(items, i * 3.0 + 1.0)) && p_ext(nm) == "wav" { bind out = list_push(out, p_stem(nm)) }
bind i = i + 1.0
}
return out
}
# the scene camera pose; save_scene stores the editor view, so set both
fn ss_view(x, y, z, yaw, pitch, dist) {
sc_set("camx", x)
sc_set("camy", y)
sc_set("camz", z)
sc_set("camyaw", yaw)
sc_set("campitch", pitch)
sc_set("camzd", dist)
st_set("ed_tx", x)
st_set("ed_ty", y)
st_set("ed_tz", z)
st_set("ed_yaw", yaw)
st_set("ed_pitch", pitch)
st_set("ed_dist", dist)
}
fn ss_mesh(root, name) { return p_join(root, "game/" + name + ".lmesh") }
fn ss_music_file(root, stem) {
bind p = p_join(root, "music/" + stem + ".wav")
if file_exists(p) { return p }
return ""
}
fn import_soul_symphony(root) {
bind dir = p_join(st_get("app_root", "."), "projects")
plat_mkdir(dir)
if !proj_create(dir, "SoulSymphony", 0.0, 1280.0, 720.0) { return 0.0 }
# the template level is not part of this game
plat_delete(p_join(proj_root(), "scenes/level.lscene"))
proj_remove_scene("scenes/level.lscene")
st_set("proj_source", p_norm(root))
st_set("proj_title", "Soul Symphony - LingDev Edition")
st_set("proj_author", "Ling Lin")
plat_mkdir(p_join(proj_root(), "assets/fonts"))
plat_copy(p_join(st_get("app_root", "."), "assets/fonts/Daemon.otf"), p_join(proj_root(), "assets/fonts/Daemon.otf"))
bind stems = ss_music_stems(root)
# parse the dialogue export into scenes
bind text = plat_read(p_join(root, "cutscenes_dialog.txt"))
bind lines = str_split(str_replace(text, "\r", ""), "\n")
bind order = []
bind cur = ""
st_lclear("ss_lines")
st_lclear("ss_scene")
bind i = 0.0
while i < len(lines) {
bind ln = str_trim(list_get(lines, i))
if ln != "" {
bind ci = str_find(ln, ":")
if ci < 0.0 {
bind id = ss_scene_id(ln)
if fs_is_dir(p_join(root, "game/voice/" + id)) { bind cur = id bind order = list_push(order, id) } else { bind cur = "" }
} else if cur != "" {
st_lpush("ss_scene", cur)
st_lpush("ss_lines", ln)
}
}
bind i = i + 1.0
}
# scenes with voice files but no lines in the export (intro, prelude, bonus, finales)
bind vdirs = plat_list(p_join(root, "game/voice"))
bind vi = 0.0
while vi < len(vdirs) / 3.0 {
bind vid = list_get(vdirs, vi * 3.0)
if is_true(list_get(vdirs, vi * 3.0 + 1.0)) && !l_has(order, vid) {
bind added = ss_lines_from_module(root, vid)
if added > 0.0 { bind order = ss_insert_scene(order, vid) }
}
bind vi = vi + 1.0
}
# story order for Next: intro -> prelude -> chapters -> finales; bonus returns to the menu
bind made = 0.0
bind total_lines = 0.0
bind s = 0.0
while s < len(order) {
bind id = list_get(order, s)
bind nxt = "scenes/menu.lscene"
if id != "bonus" {
bind k = s + 1.0
while k < len(order) && list_get(order, k) == "bonus" { bind k = k + 1.0 }
if k < len(order) { bind nxt = "scenes/" + list_get(order, k) + ".lscene" }
}
bind n = ss_build_cutscene(root, id, nxt, stems)
bind total_lines = total_lines + n
bind made = made + 1.0
bind s = s + 1.0
}
ss_build_hub(root)
ss_build_title_bg(root)
ss_build_menu_bg(root)
ss_build_menus(root, order)
st_set("proj_start", "scenes/title.lscene")
import_write_run_original(root)
proj_save()
gen_write_all()
assets_mark_stale()
open_scene(p_join(proj_root(), "scenes/title.lscene"))
status("Soul Symphony imported: " + num_str(made) + " cutscenes, " + num_str(total_lines) + " voiced lines, title + menus, hub cathedral. run_original.bat plays the original.")
return 0.0
}
# keep the story order: intro first, prelude after chapter1's gate, bonus and finales last
fn ss_insert_scene(order, id) {
if id == "intro" { return l_insert_at(order, 0.0, id) }
if id == "post_level1" {
bind k = l_index(order, "chapter1")
if k >= 0.0 { return l_insert_at(order, k, id) }
}
return list_push(order, id)
}
# the English strings of a scene module's dialogue function (its "== 1 {" block up to the closing "]");
# the speakers come from the voice file names NN_<role>_en.wav
fn ss_lines_from_module(root, id) {
bind src = plat_read(p_join(root, "game/scenes/" + id + ".ling"))
bind files = plat_list(p_join(root, "game/voice/" + id))
bind roles = []
bind fi = 0.0
while fi < len(files) / 3.0 {
bind nm = list_get(files, fi * 3.0)
if str_ends_with(nm, "_en.wav") {
bind parts = str_split(nm, "_")
if len(parts) >= 3.0 {
bind role = list_get(parts, 1)
bind pi = 2.0
while pi < len(parts) - 1.0 { bind role = role + "_" + list_get(parts, pi) bind pi = pi + 1.0 }
bind roles = list_push(roles, role)
}
}
bind fi = fi + 1.0
}
if len(roles) < 1.0 { return 0.0 }
bind texts = []
if src != "" {
bind at = str_find(src, "== 1 {")
bind guard = 0.0
while at >= 0.0 && guard < 12.0 {
bind rest = str_slice(src, at, str_len(src) - at)
bind close = str_find(rest, "]")
bind block = sel(close > 0.0, str_slice(rest, 0.0, close), "")
bind qs = str_split(block, "\"")
bind cand = []
bind qi = 1.0
while qi < len(qs) {
bind piece = list_get(qs, qi)
if str_len(str_trim(piece)) > 2.0 { bind cand = list_push(cand, piece) }
bind qi = qi + 2.0
}
if len(cand) > len(texts) && len(cand) <= len(roles) + 2.0 { bind texts = cand }
bind tail = str_slice(src, at + 6.0, str_len(src) - at - 6.0)
bind nxt = str_find(tail, "== 1 {")
if nxt < 0.0 { bind at = 0.0 - 1.0 } else { bind at = at + 6.0 + nxt }
bind guard = guard + 1.0
}
}
bind i = 0.0
while i < len(roles) {
bind role = list_get(roles, i)
bind speaker = str_upper(str_slice(role, 0.0, 1.0)) + str_replace(str_slice(role, 1.0, str_len(role) - 1.0), "_", " ")
bind line = sel(i < len(texts), list_get(texts, i), "")
st_lpush("ss_scene", id)
st_lpush("ss_lines", speaker + ": " + line)
bind i = i + 1.0
}
return len(roles)
}
# ── shared pieces ────────────────────────────────────────────────────────────
fn ss_node(kind, name, x, y, z, sx, sy, sz, col) {
bind i = nd_add(kind, name, 0.0)
nd_set(i, "px", x)
nd_set(i, "py", y)
nd_set(i, "pz", z)
nd_set(i, "sx", sx)
nd_set(i, "sy", sy)
nd_set(i, "sz", sz)
nd_set(i, "col", col)
return i
}
fn ss_model(root, name, label, x, y, z, scale, yaw, col) {
bind i = ss_node("model", label, x, y, z, scale, scale, scale, col)
nd_set(i, "src", ss_mesh(root, name))
nd_set(i, "ry", yaw)
return i
}
fn ss_swarm(name, x, y, z, count, radius, shape, size, drift, col, extra) {
bind i = ss_node("swarm", name, x, y, z, size, size, size, col)
nd_set(i, "a", count)
nd_set(i, "b", radius)
nd_set(i, "c", shape)
nd_set(i, "d", drift)
nd_set(i, "ex", extra)
return i
}
fn ss_knot(name, x, y, z, count, outer, inner, speed, size) {
bind i = ss_node("knot", name, x, y, z, size, size, size, C_WHITE)
nd_set(i, "a", count)
nd_set(i, "b", outer)
nd_set(i, "c", inner)
nd_set(i, "d", speed)
nd_set(i, "ex", "rainbow=1")
return i
}
fn ss_light(name, x, y, z, col, intensity, radius) {
bind i = ss_node("light", name, x, y, z, 1.0, 1.0, 1.0, col)
nd_set(i, "a", intensity)
nd_set(i, "b", radius)
return i
}
# a circular path: keys on px / pz (and py when bob > 0), `turns` revolutions over [t0, t1]
fn ss_orbit_keys(i, cx, cy, cz, r, bob, t0, t1, turns) {
bind steps = 16.0
bind k = 0.0
while k <= steps {
bind u = k / steps
bind a = u * 6.2831853 * turns
bind t = t0 + (t1 - t0) * u
key_add(nd_id(i), "px", t, cx + cos(a) * r, "linear")
key_add(nd_id(i), "pz", t, cz + sin(a) * r, "linear")
if bob > 0.0 { key_add(nd_id(i), "py", t, cy + sin(a * 2.0) * bob, "linear") }
bind k = k + 1.0
}
}
fn ss_spin_keys(i, t1, turns) {
key_add(nd_id(i), "ry", 0.0, 0.0, "linear")
key_add(nd_id(i), "ry", t1, 6.2831853 * turns, "linear")
}
# the five palace pieces as the original title draws them: (0, 1.02, 6), scale 12, turned to face us
fn ss_palace_pieces(root, x, y, z, scale, hue) {
bind k = 0.0
while k < 5.0 {
bind m = ss_model(root, "palace_p" + num_str(k), "Palace " + num_str(k), x, y, z, scale, 3.1416, c_pal(k + 3.0))
if hue > 0.0 { nd_set(m, "ex", "hue=" + fmt2(hue) + ";gain=1.1") }
bind k = k + 1.0
}
}
# ── title: press start over the palace / ink horizon ─────────────────────────
fn ss_build_title_bg(root) {
new_scene("3d")
sc_set("name", "Title backdrop")
sc_set("bg", c_rgb(4.0, 0.0, 8.0))
sc_set("amb", 0.4)
sc_set("ex", "fog=30,72,158,14,60;bgspin=0;bgsway=0.05;bgzd=2.8;shade=2;rim=0.25")
ss_view(0.0, 0.0, 0.0, 0.0, 0.0, 22.0)
bind g = nd_find_name("Ground")
if g >= 0.0 { nd_set(g, "py", 3.0) nd_set(g, "sx", 400.0) nd_set(g, "sz", 400.0) nd_set(g, "col", c_rgb(255.0, 60.0, 140.0)) nd_set(g, "name", "Ink horizon") }
bind sun = nd_find_name("Sun")
if sun >= 0.0 { nd_set(sun, "px", 0.0) nd_set(sun, "py", 0.0 - 4.0) nd_set(sun, "pz", 0.0 - 6.0) nd_set(sun, "col", c_rgb(255.0, 225.0, 165.0)) nd_set(sun, "a", 3.0) nd_set(sun, "b", 14.0) }
ss_light("Blue rim", 0.0 - 4.0, 0.0 - 2.5, 2.0, c_rgb(76.0, 204.0, 255.0), 1.5, 10.0)
bind cam = nd_find_name("Camera")
if cam >= 0.0 { nd_set(cam, "px", 0.0) nd_set(cam, "py", 0.0) nd_set(cam, "pz", 0.0 - 22.0) nd_set(cam, "rx", 0.0) nd_set(cam, "ry", 0.0) nd_set(cam, "a", 2.8) }
bind spawn = nd_find_name("Spawn")
if spawn >= 0.0 { nd_del_id(nd_id(spawn)) }
ss_palace_pieces(root, 0.0, 1.02, 6.0, 12.0, 0.85)
bind pool = ss_node("cylinder", "Moon pool", 0.0, 2.95, 6.0, 9.0, 0.02, 4.0, c_rgb(120.0, 70.0, 150.0))
ss_swarm("Wire cubes", 0.0, 0.0 - 1.0, 0.0 - 2.0, 18.0, 9.0, 0.0, 1.1, 0.12, c_rgb(120.0, 220.0, 255.0), "rainbow=1")
ss_swarm("Letters", 0.0, 0.0 - 2.0, 2.0, 24.0, 7.0, 1.0, 0.12, 0.25, c_rgb(255.0, 240.0, 200.0), "rainbow=1")
ss_swarm("Stars", 0.0, 0.0 - 20.0, 30.0, 160.0, 60.0, 1.0, 0.05, 0.02, c_rgb(200.0, 210.0, 255.0), "")
ss_knot("Mandala", 0.0, 0.0 - 1.0, 0.0 - 6.0, 24.0, 4.2, 1.7, 0.05, 0.22)
save_scene(p_join(proj_root(), "scenes/title_bg.lscene"))
}
# the menu background: fill (2,2,9), warm front light, the Celtic knot chain of rainbow cubes
fn ss_build_menu_bg(root) {
new_scene("3d")
sc_set("name", "Menu backdrop")
sc_set("bg", c_rgb(2.0, 2.0, 9.0))
sc_set("amb", 0.62)
sc_set("ex", "bgspin=0;bgzd=2.8;rim=0.3")
ss_view(0.0, 0.0, 0.0, 0.0, 0.0, 22.0)
bind g = nd_find_name("Ground")
if g >= 0.0 { nd_del_id(nd_id(g)) }
bind spawn = nd_find_name("Spawn")
if spawn >= 0.0 { nd_del_id(nd_id(spawn)) }
bind sun = nd_find_name("Sun")
if sun >= 0.0 { nd_set(sun, "px", 0.0) nd_set(sun, "py", 0.0 - 5.0) nd_set(sun, "pz", 0.0 - 13.0) nd_set(sun, "col", c_rgb(255.0, 235.0, 209.0)) nd_set(sun, "a", 1.25) nd_set(sun, "b", 280.0) }
bind cam = nd_find_name("Camera")
if cam >= 0.0 { nd_set(cam, "px", 0.0) nd_set(cam, "py", 0.0) nd_set(cam, "pz", 0.0 - 22.0) nd_set(cam, "rx", 0.0) nd_set(cam, "ry", 0.0) nd_set(cam, "a", 2.8) }
ss_knot("Mandala", 0.0, 0.0, 0.0, 30.0, 6.0, 2.4, 0.06, 0.3)
bind k = 0.0
while k < 3.0 {
bind l = ss_light("Chain light " + num_str(k + 1.0), 6.0 * cos(k * 2.094), 6.0 * sin(k * 2.094), 0.0 - 4.0, c_pal(k * 2.0), 1.2, 120.0)
ss_orbit_keys(l, 0.0, 0.0, 0.0 - 4.0, 6.0, 0.0, 0.0, 10.0, 1.0)
bind k = k + 1.0
}
sc_set("len", 10.0)
save_scene(p_join(proj_root(), "scenes/menu_bg.lscene"))
}
# a row of the original's menu: plain text, centred, ROYGBIV by index, the ink cursor on the selected one
fn ss_row(label, action, idx, y, size, font) {
bind b = nd_add("button", label, 0.0)
nd_set(b, "txt", label)
nd_set(b, "scr", action)
nd_set(b, "px", 0.0)
nd_set(b, "py", y)
nd_set(b, "w", 520.0)
nd_set(b, "h", size + 14.0)
nd_set(b, "a", size)
nd_set(b, "anc", "tc")
nd_set(b, "col", c_mix(c_pal(idx), C_WHITE, 0.25))
nd_set(b, "ex", "style=text" + sel(font == "", "", ";font=" + font))
return b
}
fn ss_menu_scene(name, file, bg, rows, hint) {
sc_new(name, "2d")
st_lclear("undo")
st_lclear("redo")
sel_clear()
view_reset_camera()
sc_set("bg", c_rgb(2.0, 2.0, 9.0))
sc_set("ex", "bg3d=" + bg)
bind n = len(rows)
bind spacing = 46.0
bind y0 = 360.0 - (n - 1.0) * spacing * 0.5 - 20.0
bind i = 0.0
while i < n {
bind p = str_split(list_get(rows, i), "|")
ss_row(list_get(p, 0), list_get(p, 1), i, y0 + i * spacing, 26.0, "")
bind i = i + 1.0
}
if hint != "" {
bind h = nd_add("label", "Hint", 0.0)
nd_set(h, "txt", hint)
nd_set(h, "px", 0.0)
nd_set(h, "py", 0.0 - 40.0)
nd_set(h, "w", 1280.0)
nd_set(h, "a", 13.0)
nd_set(h, "anc", "bc")
nd_set(h, "col", c_rgb(150.0, 150.0, 190.0))
}
save_scene(p_join(proj_root(), "scenes/" + file))
}
fn ss_build_menus(root, order) {
# press start
sc_new("Title", "2d")
st_lclear("undo")
st_lclear("redo")
sel_clear()
view_reset_camera()
sc_set("bg", c_rgb(4.0, 0.0, 8.0))
sc_set("ex", "bg3d=scenes/title_bg.lscene")
bind logo = p_join(root, "image/SoulSymphonyLogo.png")
if file_exists(logo) {
bind im = nd_add("image", "Logo", 0.0)
nd_set(im, "src", logo)
nd_set(im, "px", 0.0)
nd_set(im, "py", 150.0)
nd_set(im, "w", 620.0)
nd_set(im, "h", 258.0)
nd_set(im, "anc", "tc")
key_add(nd_id(im), "py", 0.0, 150.0, "linear")
key_add(nd_id(im), "py", 2.0, 162.0, "sine_in_out")
key_add(nd_id(im), "py", 4.0, 150.0, "sine_in_out")
sc_set("len", 4.0)
}
bind ps = ss_row("press start", "goto:scenes/menu.lscene", 7.0, 0.0 - 120.0, 22.0, "Daemon")
nd_set(ps, "anc", "bc")
nd_set(ps, "col", c_rgb(230.0, 230.0, 255.0))
nd_set(ps, "ex", "style=text;font=Daemon;blink=1")
bind sub = nd_add("label", "Subtitle", 0.0)
nd_set(sub, "txt", "A spiritual 3D adventure in the soul realm - LingDev edition")
nd_set(sub, "px", 0.0)
nd_set(sub, "py", 0.0 - 60.0)
nd_set(sub, "w", 1280.0)
nd_set(sub, "a", 13.0)
nd_set(sub, "anc", "bc")
nd_set(sub, "col", c_rgb(150.0, 150.0, 190.0))
save_scene(p_join(proj_root(), "scenes/title.lscene"))
# main menu: the original rows, in its order
bind first = sel(l_has(order, "intro"), "intro", list_get(order, 0))
ss_menu_scene("Main menu", "menu.lscene", "scenes/menu_bg.lscene", ["load game|say:Saved games stay in the original (run_original.bat); the LingDev edition starts from the Level select", "new game|goto:scenes/" + first + ".lscene", "level select|goto:scenes/levels.lscene", "multiplayer|goto:scenes/multiplayer.lscene", "options|goto:scenes/options.lscene", "language|lang", "quit|quit"], "arrows / W S choose enter / space select esc back - Main_Menu.wav plays in the original")
# level select: hub + the story scenes in order
bind rows = ["hub cathedral (walk)|goto:scenes/hub.lscene"]
bind s = 0.0
while s < len(order) {
bind id = list_get(order, s)
bind rows = list_push(rows, str_lower(ss_title(id)) + "|goto:scenes/" + id + ".lscene")
bind s = s + 1.0
}
bind rows = list_push(rows, "back|back")
ss_menu_scene("Level select", "levels.lscene", "scenes/menu_bg.lscene", rows, "the procedural levels between the chapters live in the original game")
ss_menu_scene("Multiplayer", "multiplayer.lscene", "scenes/menu_bg.lscene", ["host a game (port 7777)|host:7777", "join 127.0.0.1|join:127.0.0.1:7777", "back|back"], "co-op shares the orb's position with one friend (desktop runtime, TCP)")
ss_menu_scene("Options", "options.lscene", "scenes/menu_bg.lscene", ["music volume +|vol:0.1", "music volume -|vol:-0.1", "subtitles on / off|toggle:subtitles", "language|lang", "back|back"], "")
# music for the menus (the original: Main_Menu.wav)
bind mm = ss_music_file(root, "Main_Menu")
if mm != "" {
open_scene(p_join(proj_root(), "scenes/menu.lscene"))
sc_set("music", mm)
save_scene(sc_get("path", ""))
}
bind tm = ss_music_file(root, "SoulSymphony")
if tm == "" { bind tm = ss_music_file(root, "Main_Menu") }
if tm != "" {
open_scene(p_join(proj_root(), "scenes/title.lscene"))
sc_set("music", tm)
save_scene(sc_get("path", ""))
}
}
# ── cutscenes ────────────────────────────────────────────────────────────────
fn ss_build_cutscene(root, id, next_scene, stems) {
new_scene("cutscene")
while clip_count() > 0.0 { clip_del(0.0) }
sc_set("name", ss_title(id))
sc_set("bg", c_rgb(6.0, 6.0, 20.0))
sc_set("amb", 0.35)
sc_set("next", next_scene)
bind music = ss_scene_music(root, id, stems)
bind g = nd_find_name("Ground")
if g >= 0.0 { nd_set(g, "sx", 60.0) nd_set(g, "sz", 60.0) nd_set(g, "col", c_rgb(14.0, 18.0, 40.0)) }
bind spawn = nd_find_name("Spawn")
if spawn >= 0.0 { nd_del_id(nd_id(spawn)) }
bind king = ss_model(root, "king", "King", 0.0 - 2.2, 0.0, 0.0, 2.2, 0.5, C_YELLOW)
bind queen = ss_model(root, "queen", "Queen", 2.2, 0.0, 0.0, 2.2, 0.0 - 0.5, C_VIOLET)
nd_set(king, "tag", "king")
nd_set(queen, "tag", "queen")
ss_light("Rim", 0.0 - 6.0, 0.0 - 5.0, 0.0 - 6.0, C_BLUE, 1.8, 30.0)
# dialogue lines -> clips with voice-over; the camera cuts between shots like the original's blocking
bind t = 1.2
bind idx = 0.0
bind n = 0.0
bind i = 0.0
while i < st_llen("ss_scene") {
if st_lget("ss_scene", i) == id {
bind ln = st_lget("ss_lines", i)
bind ci = str_find(ln, ":")
bind speaker = str_trim(str_slice(ln, 0.0, ci))
bind line = str_trim(str_slice(ln, ci + 1.0, str_len(ln) - ci - 1.0))
bind role = ss_role(speaker)
bind vo = p_join(root, "game/voice/" + id + "/" + ss_two(idx) + "_" + role + "_en.wav")
bind d = 2.2 + str_len(line) * 0.055
if file_exists(vo) {
if plat_has_audio() {
bind sid = plat_sample_load(vo)
bind dur = plat_sample_duration(sid)
if dur > 0.2 { bind d = dur + 0.35 }
}
} else { bind vo = "" }
bind who = sel(role == "king", nd_id(king), sel(role == "queen", nd_id(queen), 0.0))
clip_add("dialog", t, d, "say", speaker, line, vo, who, "linear")
bind shot = idx % 4.0
if who > 0.0 {
if shot == 0.0 { clip_add("camera", t, d, "orbit", "", "", "0,-1.5,0,5,0.18," + sel(role == "king", "0.12", "-0.12"), who, "linear") }
else if shot == 1.0 { clip_add("camera", t, d, "follow", "", "", "1.2,-1.6,4.2,0.12", who, "linear") }
else if shot == 2.0 { clip_add("camera", t, d, "orbit", "", "", "0,-1.2,0,3.6,0.08,0.06", who, "linear") }
else { clip_add("camera", t, d, "orbit", "", "", "0,-2,0,7,0.3,-0.08", who, "linear") }
} else {
if shot == 1.0 { clip_add("camera", t, d, "orbit", "", "", "0,-3,0,14,0.35,0.08", 0.0, "linear") }
else { clip_add("camera", t, d, "orbit", "", "", "0,-2,0,9,0.22,0.1", 0.0, "linear") }
}
bind t = t + d + 0.25
bind idx = idx + 1.0
bind n = n + 1.0
}
bind i = i + 1.0
}
bind total = t + 1.5
sc_set("len", total)
clip_add("fx", 0.0, 1.5, "fade_in", "", "", "1", 0.0, "linear")
clip_add("fx", 0.0, total, "letterbox", "", "", "1", 0.0, "linear")
clip_add("fx", total - 1.5, 1.5, "fade_out", "", "", "1", 0.0, "linear")
if music != "" { clip_add("audio", 0.0, 1.0, "music", "", music, "0.6", 0.0, "linear") }
clip_add("event", total, 0.1, "end", "", "", "", 0.0, "linear")
ss_decorate(root, id, king, queen, total)
save_scene(p_join(proj_root(), "scenes/" + id + ".lscene"))
return n
}
# each chapter's backdrop in the mood of the original module
fn ss_decorate(root, id, king, queen, total) {
bind g = nd_find_name("Ground")
if id == "intro" {
sc_set("bg", c_rgb(2.0, 2.0, 12.0))
if g >= 0.0 { nd_set(g, "vis", 0.0) }
nd_set(king, "px", 0.0 - 7.0) nd_set(king, "py", 2.0) nd_set(king, "pz", 14.0) nd_set(king, "sx", 6.0) nd_set(king, "sy", 6.0) nd_set(king, "sz", 6.0)
nd_set(queen, "px", 7.0) nd_set(queen, "py", 2.0) nd_set(queen, "pz", 14.0) nd_set(queen, "sx", 6.0) nd_set(queen, "sy", 6.0) nd_set(queen, "sz", 6.0)
bind planet = ss_node("sphere", "Planet", 0.0, 0.0 - 3.0, 2.0, 3.0, 3.0, 3.0, c_rgb(40.0, 110.0, 235.0))
ss_spin_keys(planet, total, 2.0)
bind ring = ss_node("torus", "Ring", 0.0, 0.0 - 3.0, 2.0, 5.2, 0.3, 5.2, c_rgb(240.0, 175.0, 60.0))
nd_set(ring, "rx", 0.35)
ss_spin_keys(ring, total, 0.0 - 1.5)
ss_swarm("Stars", 0.0, 0.0 - 6.0, 10.0, 240.0, 70.0, 1.0, 0.07, 0.02, c_rgb(220.0, 225.0, 255.0), "")
ss_swarm("Comets", 0.0, 0.0 - 4.0, 4.0, 10.0, 16.0, 0.0, 0.35, 1.2, c_rgb(255.0, 230.0, 160.0), "rainbow=1")
bind i = 0.0
while i < clip_count() {
if clip_get(i, "track") == "camera" { clip_set(i, "c", "0,-3,2,12,0.18,0.12") clip_set(i, "node", 0.0) }
bind i = i + 1.0
}
} else if id == "post_level1" {
sc_set("bg", c_rgb(70.0, 28.0, 62.0))
if g >= 0.0 { nd_set(g, "col", c_rgb(44.0, 72.0, 36.0)) }
ss_swarm("Lanterns", 0.0, 0.0 - 5.0, 4.0, 24.0, 12.0, 2.0, 0.35, 0.3, c_rgb(255.0, 170.0, 60.0), "")
ss_swarm("Leaves", 0.0, 0.0 - 3.0, 0.0, 60.0, 14.0, 0.0, 0.12, 0.8, c_rgb(220.0, 120.0, 50.0), "")
ss_light("Lantern glow", 0.0, 0.0 - 6.0, 4.0, c_rgb(255.0, 180.0, 90.0), 2.5, 40.0)
} else if id == "chapter1" {
sc_set("bg", c_rgb(12.0, 22.0, 48.0))
if g >= 0.0 { nd_set(g, "col", c_rgb(34.0, 96.0, 46.0)) }
bind k = 0.0
while k < 10.0 {
bind ang = k * 0.9 + 0.4
bind r = 9.0 + (k % 3.0) * 4.0
bind tx = cos(ang) * r
bind tz = sin(ang) * r + 6.0
ss_node("cylinder", "Trunk " + num_str(k + 1.0), tx, 0.0 - 1.2, tz, 0.25, 1.2, 0.25, c_rgb(90.0, 60.0, 40.0))
ss_node("cone", "Tree " + num_str(k + 1.0), tx, 0.0 - 4.2, tz, 1.6 + (k % 2.0) * 0.5, 2.2, 1.6 + (k % 2.0) * 0.5, c_rgb(40.0 + k * 6.0, 140.0, 60.0))
bind k = k + 1.0
}
ss_swarm("Fireflies", 0.0, 0.0 - 2.0, 4.0, 40.0, 12.0, 1.0, 0.09, 0.6, c_rgb(255.0, 240.0, 120.0), "")
} else if id == "chapter2" {
sc_set("bg", c_rgb(3.0, 3.0, 14.0))
sc_set("ex", "fog=20,30,80,22,70")
if g >= 0.0 { nd_set(g, "col", c_rgb(10.0, 14.0, 36.0)) }
bind moon = ss_node("sphere", "The eye", 0.0, 0.0 - 16.0, 22.0, 4.5, 4.5, 4.5, c_rgb(236.0, 226.0, 206.0))
bind e1 = nd_add("sphere", "Pupil L", nd_id(moon))
nd_set(e1, "px", 0.0 - 1.4) nd_set(e1, "py", 0.0 - 0.6) nd_set(e1, "pz", 0.0 - 3.9) nd_set(e1, "sx", 0.7) nd_set(e1, "sy", 0.7) nd_set(e1, "sz", 0.7) nd_set(e1, "col", c_rgb(40.0, 20.0, 30.0))
bind e2 = nd_add("sphere", "Pupil R", nd_id(moon))
nd_set(e2, "px", 1.4) nd_set(e2, "py", 0.0 - 0.6) nd_set(e2, "pz", 0.0 - 3.9) nd_set(e2, "sx", 0.7) nd_set(e2, "sy", 0.7) nd_set(e2, "sz", 0.7) nd_set(e2, "col", c_rgb(40.0, 20.0, 30.0))
bind mouth = nd_add("torus", "Grin", nd_id(moon))
nd_set(mouth, "py", 1.6) nd_set(mouth, "pz", 0.0 - 3.6) nd_set(mouth, "sx", 1.6) nd_set(mouth, "sy", 0.4) nd_set(mouth, "sz", 1.0) nd_set(mouth, "rx", 1.4) nd_set(mouth, "col", c_rgb(60.0, 25.0, 40.0))
key_add(nd_id(moon), "py", 0.0, 0.0 - 16.0, "linear")
key_add(nd_id(moon), "py", total, 0.0 - 11.0, "sine_in_out")
ss_model(root, "palace", "Old palace", 0.0, 0.0, 34.0, 12.0, 3.1416, C_INDIGO)
bind k = 0.0
while k < 14.0 {
ss_node("cylinder", "Reed " + num_str(k + 1.0), cos(k * 1.3) * (6.0 + (k % 4.0) * 2.0), 0.0 - 1.0, 8.0 + sin(k * 1.3) * 5.0, 0.05, 1.0 + (k % 3.0) * 0.3, 0.05, c_rgb(70.0, 110.0, 60.0))
bind k = k + 1.0
}
ss_swarm("Fireflies", 0.0, 0.0 - 2.0, 6.0, 60.0, 14.0, 1.0, 0.08, 0.7, c_rgb(255.0, 235.0, 110.0), "")
ss_light("Lantern", 0.0, 0.0 - 4.0, 30.0, c_rgb(255.0, 170.0, 80.0), 2.0, 60.0)
} else if id == "chapter3" {
sc_set("bg", c_rgb(18.0, 10.0, 30.0))
if g >= 0.0 { nd_set(g, "col", c_rgb(34.0, 22.0, 52.0)) }
bind pal = ss_model(root, "palace", "High court", 0.0, 0.0, 12.0, 12.0, 3.1416, C_INDIGO)
nd_set(pal, "ex", "hue=0.85;gain=1.05")
bind k = 0.0
while k < 6.0 {
ss_node("cylinder", "Pillar " + num_str(k + 1.0), cos(k * 1.047) * 9.0, 0.0 - 4.0, sin(k * 1.047) * 9.0 + 2.0, 0.6, 4.0, 0.6, c_rgb(90.0, 70.0, 130.0))
bind k = k + 1.0
}
ss_swarm("Instruments", 0.0, 0.0 - 3.0, 2.0, 10.0, 6.0, 2.0, 0.3, 0.5, c_rgb(250.0, 210.0, 120.0), "rainbow=1")
bind comet = ss_node("sphere", "Cold comet", 0.0 - 60.0, 0.0 - 30.0, 40.0, 0.8, 0.8, 0.8, c_rgb(200.0, 240.0, 255.0))
key_add(nd_id(comet), "px", 0.0, 0.0 - 60.0, "linear")
key_add(nd_id(comet), "px", total - 8.0, 0.0 - 60.0, "linear")
key_add(nd_id(comet), "px", total - 3.0, 60.0, "quad_in")
key_add(nd_id(comet), "py", total - 8.0, 0.0 - 30.0, "linear")
key_add(nd_id(comet), "py", total - 3.0, 0.0 - 2.0, "quad_in")
clip_add("fx", total - 3.2, 0.6, "flash", "", "", "1", 0.0, "linear")
clip_add("fx", total - 3.0, 1.2, "shake", "", "", "1", 0.0, "linear")
} else if id == "chapter4" {
sc_set("bg", c_rgb(0.0, 0.0, 0.0))
if g >= 0.0 { nd_set(g, "vis", 0.0) }
bind k = 0.0
while k < 14.0 {
bind ring = ss_node("torus", "Tunnel " + num_str(k + 1.0), 0.0, 0.0 - 2.0, k * 5.0 - 12.0, 7.0, 7.0, 7.0, c_pal(k))
nd_set(ring, "rx", 1.5708)
ss_spin_keys(ring, total, sel(k % 2.0 < 0.5, 1.0, 0.0 - 1.0))
bind k = k + 1.0
}
bind k = 0.0
while k < 4.0 {
bind drum = ss_node("cylinder", "Bronze drum " + num_str(k + 1.0), cos(k * 1.57) * 4.5, 0.0 - 2.0 + sin(k * 1.57) * 4.5, 6.0 + k * 3.0, 1.0, 0.25, 1.0, c_rgb(190.0, 120.0, 60.0))
nd_set(drum, "rx", 1.5708)
bind k = k + 1.0
}
bind water = ss_node("torus", "Water spirit", 3.0, 0.0 - 2.5, 2.0, 1.2, 0.5, 1.2, c_rgb(80.0, 180.0, 255.0))
ss_orbit_keys(water, 0.0, 0.0 - 2.5, 2.0, 3.5, 1.0, 0.0, total, 3.0)
bind fire = ss_node("cone", "Fire spirit", 0.0 - 3.0, 0.0 - 2.5, 2.0, 0.9, 1.4, 0.9, c_rgb(255.0, 120.0, 40.0))
ss_orbit_keys(fire, 0.0, 0.0 - 2.5, 2.0, 3.5, 1.0, 0.0, total, 0.0 - 3.0)
ss_swarm("Embers", 0.0, 0.0 - 2.0, 10.0, 120.0, 20.0, 1.0, 0.08, 1.0, c_rgb(255.0, 200.0, 120.0), "rainbow=1")
ss_light("Spirit light", 0.0, 0.0 - 3.0, 2.0, c_rgb(120.0, 200.0, 255.0), 2.5, 30.0)
} else if id == "chapter5" {
sc_set("bg", c_rgb(40.0, 0.0, 60.0))
if g >= 0.0 { nd_set(g, "col", c_rgb(60.0, 10.0, 80.0)) }
bind cold = ss_node("sphere", "The cold light", 0.0, 0.0 - 6.0, 12.0, 2.6, 2.6, 2.6, c_rgb(220.0, 250.0, 255.0))
key_add(nd_id(cold), "sx", 0.0, 2.6, "linear")
key_add(nd_id(cold), "sx", total * 0.6, 4.0, "sine_in_out")
key_add(nd_id(cold), "sx", total, 0.4, "quad_in")
ss_light("Cold glow", 0.0, 0.0 - 6.0, 12.0, c_rgb(200.0, 240.0, 255.0), 4.0, 60.0)
ss_swarm("Halo", 0.0, 0.0 - 5.0, 6.0, 30.0, 10.0, 0.0, 0.4, 1.5, c_rgb(255.0, 220.0, 255.0), "rainbow=1")
ss_swarm("Snow", 0.0, 0.0 - 10.0, 6.0, 200.0, 30.0, 1.0, 0.06, 0.5, c_rgb(240.0, 245.0, 255.0), "")
bind ft = 2.0
while ft < total * 0.6 {
clip_add("fx", ft, 0.35, "flash", "", "", "0.5", 0.0, "linear")
bind ft = ft + 1.6
}
clip_add("fx", total * 0.2, 0.8, "shake", "", "", "1", 0.0, "linear")
clip_add("fx", total * 0.4, 0.8, "shake", "", "", "1", 0.0, "linear")
clip_add("fx", total * 0.58, 1.2, "shake", "", "", "1", 0.0, "linear")
} else if id == "chapter6" {
sc_set("bg", c_rgb(16.0, 6.0, 26.0))
if g >= 0.0 { nd_set(g, "vis", 0.0) }
ss_model(root, "palace_p0", "Breathing floor", 0.0, 2.0, 4.0, 8.0, 3.1416, C_INDIGO)
bind piece = ["palace_p1", "palace_p2", "palace_p4", "palace_p1", "palace_p2"]
bind k = 0.0
while k < 5.0 {
bind m = ss_model(root, list_get(piece, k), "Shard " + num_str(k + 1.0), 0.0, 0.0 - 3.0, 4.0, 3.0, 0.0, c_pal(k + 1.0))
nd_set(m, "ex", "hue=0.5;gain=1.1")
ss_orbit_keys(m, 0.0, 0.0 - 3.0, 4.0, 9.0 + k, 1.5, 0.0, total, 1.0 + k * 0.2)
bind k = k + 1.0
}
bind daemon = ss_node("sphere", "Daemon", 2.0, 0.0 - 2.0, 2.0, 0.9, 0.9, 0.9, c_rgb(95.0, 30.0, 85.0))
bind eye1 = nd_add("sphere", "Eye L", nd_id(daemon))
nd_set(eye1, "px", 0.0 - 0.35) nd_set(eye1, "py", 0.0 - 0.15) nd_set(eye1, "pz", 0.0 - 0.75) nd_set(eye1, "sx", 0.22) nd_set(eye1, "sy", 0.22) nd_set(eye1, "sz", 0.22) nd_set(eye1, "col", c_rgb(255.0, 205.0, 60.0))
bind eye2 = nd_add("sphere", "Eye R", nd_id(daemon))
nd_set(eye2, "px", 0.35) nd_set(eye2, "py", 0.0 - 0.15) nd_set(eye2, "pz", 0.0 - 0.75) nd_set(eye2, "sx", 0.22) nd_set(eye2, "sy", 0.22) nd_set(eye2, "sz", 0.22) nd_set(eye2, "col", c_rgb(255.0, 205.0, 60.0))
bind w1 = nd_add("box", "Wing L", nd_id(daemon))
nd_set(w1, "px", 0.0 - 1.1) nd_set(w1, "py", 0.0 - 0.3) nd_set(w1, "sx", 0.6) nd_set(w1, "sy", 0.35) nd_set(w1, "sz", 0.05) nd_set(w1, "col", c_rgb(150.0, 70.0, 140.0))
bind w2 = nd_add("box", "Wing R", nd_id(daemon))
nd_set(w2, "px", 1.1) nd_set(w2, "py", 0.0 - 0.3) nd_set(w2, "sx", 0.6) nd_set(w2, "sy", 0.35) nd_set(w2, "sz", 0.05) nd_set(w2, "col", c_rgb(150.0, 70.0, 140.0))
key_add(nd_id(daemon), "py", 0.0, 0.0 - 2.0, "linear")
key_add(nd_id(daemon), "py", 1.5, 0.0 - 2.6, "sine_in_out")
key_add(nd_id(daemon), "py", 3.0, 0.0 - 2.0, "sine_in_out")
ss_knot("Letter ring", 0.0, 0.0 - 3.0, 4.0, 26.0, 5.0, 1.8, 0.04, 0.25)
ss_light("Warm aura", 2.0, 0.0 - 3.0, 1.0, c_rgb(255.0, 170.0, 120.0), 2.0, 20.0)
} else if id == "chapter7" {
sc_set("bg", c_rgb(2.0, 4.0, 16.0))
if g >= 0.0 { nd_set(g, "py", 0.5) nd_set(g, "sx", 300.0) nd_set(g, "sz", 300.0) nd_set(g, "col", c_rgb(6.0, 10.0, 40.0)) nd_set(g, "name", "Star sea") }
ss_swarm("Stars", 0.0, 0.0 - 30.0, 20.0, 220.0, 60.0, 1.0, 0.07, 0.02, c_rgb(220.0, 225.0, 255.0), "")
ss_swarm("Reflections", 0.0, 12.0, 20.0, 120.0, 60.0, 1.0, 0.05, 0.02, c_rgb(120.0, 130.0, 200.0), "flat=1")
ss_swarm("Lantern buoys", 0.0, 0.0 - 0.6, 10.0, 12.0, 20.0, 2.0, 0.35, 0.2, c_rgb(255.0, 170.0, 70.0), "flat=1")
bind wheel = ss_node("torus", "Galaxy wheel", 0.0, 0.0 - 24.0, 70.0, 24.0, 24.0, 24.0, c_rgb(150.0, 90.0, 220.0))
nd_set(wheel, "rx", 1.2)
ss_spin_keys(wheel, total, 0.3)
bind train = ss_node("box", "Train of light", 0.0 - 80.0, 0.0 - 6.0, 40.0, 7.0, 0.3, 0.5, c_rgb(255.0, 240.0, 200.0))
key_add(nd_id(train), "px", 0.0, 0.0 - 90.0, "linear")
key_add(nd_id(train), "px", total * 0.25, 0.0 - 90.0, "linear")
key_add(nd_id(train), "px", total * 0.45, 90.0, "linear")
bind daemon = ss_node("sphere", "Daemon", 0.0, 0.0 - 3.0, 3.0, 0.7, 0.7, 0.7, c_rgb(95.0, 30.0, 85.0))
ss_orbit_keys(daemon, 0.0, 0.0 - 3.0, 3.0, 4.0, 0.8, 0.0, total, 2.0)
ss_light("Moon", 0.0, 0.0 - 30.0, 30.0, c_rgb(180.0, 200.0, 255.0), 2.5, 120.0)
} else if id == "chapter8" {
sc_set("bg", c_rgb(236.0, 80.0, 120.0))
sc_set("amb", 0.5)
if g >= 0.0 { nd_set(g, "col", c_rgb(250.0, 150.0, 60.0)) }
ss_swarm("Screentone", 0.0, 0.0 - 8.0, 10.0, 160.0, 40.0, 1.0, 0.12, 0.1, c_rgb(120.0, 30.0, 70.0), "")
bind k = 0.0
while k < 5.0 {
bind m = ss_model(root, "palace_p" + num_str(k), "Palace piece " + num_str(k), cos(k * 1.257) * 30.0, 0.0 - 20.0 - k * 4.0, 20.0 + sin(k * 1.257) * 20.0, 10.0, 3.1416, c_pal(k))
key_add(nd_id(m), "px", 0.0, cos(k * 1.257) * 30.0, "linear")
key_add(nd_id(m), "px", total * 0.6, 0.0, "cubic_in_out")
key_add(nd_id(m), "py", 0.0, 0.0 - 20.0 - k * 4.0, "linear")
key_add(nd_id(m), "py", total * 0.6, 1.0, "cubic_in_out")
key_add(nd_id(m), "pz", 0.0, 20.0 + sin(k * 1.257) * 20.0, "linear")
key_add(nd_id(m), "pz", total * 0.6, 10.0, "cubic_in_out")
key_add(nd_id(m), "ry", 0.0, k * 2.0, "linear")
key_add(nd_id(m), "ry", total * 0.6, 3.1416, "cubic_in_out")
bind k = k + 1.0
}
bind daemon = ss_node("sphere", "Daemon", 0.0, 0.0 - 4.0, 4.0, 0.9, 0.9, 0.9, c_rgb(95.0, 30.0, 85.0))
key_add(nd_id(daemon), "py", 0.0, 0.0 - 4.0, "linear")
key_add(nd_id(daemon), "py", 1.0, 0.0 - 4.6, "sine_in_out")
key_add(nd_id(daemon), "py", 2.0, 0.0 - 4.0, "sine_in_out")
ss_knot("Letter choir", 0.0, 0.0 - 6.0, 6.0, 30.0, 5.0, 2.0, 0.08, 0.3)
} else if str_starts_with(id, "finale") {
sc_set("bg", c_rgb(6.0, 4.0, 12.0))
if g >= 0.0 { nd_set(g, "vis", 0.0) }
ss_model(root, "palace_p0", "Palace floor", 0.0, 2.0, 6.0, 10.0, 3.1416, C_INDIGO)
ss_node("box", "The gap", 0.0, 1.9, 6.0, 6.0, 0.08, 1.5, c_rgb(0.0, 0.0, 0.0))
bind coda = ss_node("sphere", "Coda", 3.0, 0.0 - 2.0, 4.0, 0.4, 0.4, 0.4, c_rgb(255.0, 250.0, 160.0))
ss_orbit_keys(coda, 0.0, 0.0 - 2.5, 4.0, 4.0, 1.0, 0.0, total, 6.0)
bind daemon = ss_node("sphere", "Daemon", 0.0 - 3.0, 0.0 - 2.0, 3.0, 0.8, 0.8, 0.8, c_rgb(95.0, 30.0, 85.0))
ss_swarm("Stars", 0.0, 0.0 - 20.0, 20.0, 200.0, 60.0, 1.0, 0.06, 0.02, c_rgb(200.0, 210.0, 255.0), "")
if id != "finale1" {
bind water = ss_node("torus", "Water spirit", 4.0, 0.0 - 3.0, 2.0, 1.2, 0.5, 1.2, c_rgb(80.0, 180.0, 255.0))
ss_orbit_keys(water, 0.0, 0.0 - 3.0, 4.0, 5.0, 1.2, 0.0, total, 2.0)
bind fire = ss_node("cone", "Fire spirit", 0.0 - 4.0, 0.0 - 3.0, 2.0, 0.9, 1.4, 0.9, c_rgb(255.0, 120.0, 40.0))
ss_orbit_keys(fire, 0.0, 0.0 - 3.0, 4.0, 5.0, 1.2, 0.0, total, 0.0 - 2.0)
}
if id == "finale3" {
ss_knot("Final chord", 0.0, 0.0 - 5.0, 6.0, 36.0, 6.0, 2.4, 0.1, 0.3)
clip_add("fx", total - 2.5, 1.0, "flash", "", "", "1", 0.0, "linear")
}
ss_light("Lamp", 0.0, 0.0 - 6.0, 2.0, c_rgb(255.0, 200.0, 150.0), 2.0, 40.0)
} else if id == "bonus" {
sc_set("bg", c_rgb(4.0, 4.0, 10.0))
if g >= 0.0 { nd_set(g, "col", c_rgb(10.0, 14.0, 30.0)) }
nd_set(king, "vis", 0.0)
nd_set(queen, "vis", 0.0)
bind loop = ss_node("torus", "Loop-de-loop", 0.0, 0.0 - 8.0, 10.0, 8.0, 8.0, 8.0, c_rgb(80.0, 220.0, 255.0))
nd_set(loop, "rx", 1.5708)
bind k = 0.0
while k < 6.0 {
bind r = ss_node("torus", "Rail " + num_str(k + 1.0), 0.0, 0.0 - 2.0, 0.0 - 10.0 + k * 4.0, 2.5, 2.5, 2.5, c_pal(k))
nd_set(r, "rx", 1.5708)
bind k = k + 1.0
}
bind coda = ss_node("sphere", "Coda", 0.0, 0.0 - 2.0, 0.0, 0.5, 0.5, 0.5, c_rgb(255.0, 250.0, 160.0))
bind steps = 24.0
bind s = 0.0
while s <= steps {
bind u = s / steps
bind a = u * 6.2831853 * 2.0
key_add(nd_id(coda), "px", total * u, sin(a) * 0.5, "linear")
key_add(nd_id(coda), "py", total * u, 0.0 - 8.0 + cos(a) * 8.0, "linear")
key_add(nd_id(coda), "pz", total * u, 10.0 + sin(a) * 8.0, "linear")
bind s = s + 1.0
}
bind i = 0.0
while i < clip_count() {
if clip_get(i, "track") == "camera" { clip_set(i, "kind", "follow") clip_set(i, "c", "0,-1.5,6,0.2") clip_set(i, "node", nd_id(coda)) }
bind i = i + 1.0
}
ss_swarm("Sparks", 0.0, 0.0 - 6.0, 8.0, 80.0, 16.0, 1.0, 0.1, 2.0, c_rgb(255.0, 240.0, 160.0), "rainbow=1")
clip_add("fx", total - 2.0, 1.2, "flash", "", "", "1", 0.0, "linear")
}
}
# ── the hub cathedral from game/level.ling: floor y=3, ceiling -171, room +-256, doors +-40, four wings
fn ss_build_hub(root) {
new_scene("3d")
sc_set("name", "Hub Cathedral")
sc_set("bg", c_rgb(4.0, 5.0, 14.0))
sc_set("floor", 3.0)
sc_set("amb", 0.3)
sc_set("ex", "fog=8,10,30,160,900")
bind music = ss_music_file(root, "SoulSymphony")
if music != "" { sc_set("music", music) }
bind g = nd_find_name("Ground")
if g >= 0.0 { nd_set(g, "py", 3.0) nd_set(g, "sx", 1000.0) nd_set(g, "sz", 1000.0) nd_set(g, "col", c_rgb(12.0, 16.0, 46.0)) }
bind ceil = ss_node("plane", "Ceiling", 0.0, 0.0 - 171.0, 0.0, 300.0, 0.02, 300.0, c_rgb(8.0, 10.0, 30.0))
nd_set(ceil, "tag", "")
bind wallc = c_rgb(28.0, 34.0, 82.0)
bind hgt = 87.0
bind cy = 3.0 - hgt
ss_wall("Wall N-left", 0.0 - 148.0, cy, 0.0 - 256.0, 108.0, hgt, 2.0, wallc)
ss_wall("Wall N-right", 148.0, cy, 0.0 - 256.0, 108.0, hgt, 2.0, wallc)
ss_wall("Wall S-left", 0.0 - 148.0, cy, 256.0, 108.0, hgt, 2.0, wallc)
ss_wall("Wall S-right", 148.0, cy, 256.0, 108.0, hgt, 2.0, wallc)
ss_wall("Wall W-near", 0.0 - 256.0, cy, 0.0 - 148.0, 2.0, hgt, 108.0, wallc)
ss_wall("Wall W-far", 0.0 - 256.0, cy, 148.0, 2.0, hgt, 108.0, wallc)
ss_wall("Wall E-near", 256.0, cy, 0.0 - 148.0, 2.0, hgt, 108.0, wallc)
ss_wall("Wall E-far", 256.0, cy, 148.0, 2.0, hgt, 108.0, wallc)
# the four wings: corridors 80 wide running out to z / x = +-960
ss_wall("North corridor W", 0.0 - 41.0, cy, 0.0 - 608.0, 1.0, hgt, 352.0, wallc)
ss_wall("North corridor E", 41.0, cy, 0.0 - 608.0, 1.0, hgt, 352.0, wallc)
ss_wall("South corridor W", 0.0 - 41.0, cy, 608.0, 1.0, hgt, 352.0, wallc)
ss_wall("South corridor E", 41.0, cy, 608.0, 1.0, hgt, 352.0, wallc)
ss_wall("West corridor N", 0.0 - 608.0, cy, 0.0 - 41.0, 352.0, hgt, 1.0, wallc)
ss_wall("West corridor S", 0.0 - 608.0, cy, 41.0, 352.0, hgt, 1.0, wallc)
ss_wall("East corridor N", 608.0, cy, 0.0 - 41.0, 352.0, hgt, 1.0, wallc)
ss_wall("East corridor S", 608.0, cy, 41.0, 352.0, hgt, 1.0, wallc)
bind pc = c_rgb(40.0, 30.0, 70.0)
bind k = 0.0
while k < 8.0 {
bind ang = k * 0.7854 + 0.3927
ss_wall("Pillar " + num_str(k + 1.0), cos(ang) * 120.0, cy, sin(ang) * 120.0, 14.0, hgt, 14.0, pc)
bind k = k + 1.0
}
# the court
bind king = ss_model(root, "king", "King", 0.0 - 14.0, 3.0, 60.0, 9.0, 3.1416, C_YELLOW)
bind queen = ss_model(root, "queen", "Queen", 14.0, 3.0, 60.0, 9.0, 3.1416, C_VIOLET)
bind temple = ss_model(root, "palace_temple", "Temple", 0.0, 3.0, 200.0, 60.0, 3.1416, C_INDIGO)
nd_set(temple, "ex", "hue=0.3;gain=1.0")
ss_knot("Court mandala", 0.0, 0.0 - 30.0, 60.0, 30.0, 18.0, 7.0, 0.04, 1.0)
ss_swarm("Dust", 0.0, 0.0 - 40.0, 0.0, 200.0, 200.0, 1.0, 0.25, 0.1, c_rgb(120.0, 140.0, 220.0), "")
# the orb, spawn, camera, lights, triggers to the wings
bind a = nd_add("actor", "Orb", 0.0)
nd_set(a, "px", 0.0)
nd_set(a, "py", 2.1)
nd_set(a, "pz", 0.0 - 40.0)
nd_set(a, "sx", 0.9)
nd_set(a, "sy", 0.9)
nd_set(a, "sz", 0.9)
nd_set(a, "d", 16.0)
nd_set(a, "col", c_rgb(235.0, 240.0, 255.0))
nd_set(a, "scr", "player")
nd_set(a, "ex", "shape=sphere")
bind s = nd_add("spawn", "Spawn", 0.0)
nd_set(s, "py", 3.0)
nd_set(s, "pz", 0.0 - 40.0)
bind c = nd_find_name("Camera")
if c >= 0.0 { nd_set(c, "b", 2.0) nd_set(c, "txt", "Orb") nd_set(c, "d", 22.0) nd_set(c, "rx", 0.32) }
bind l = nd_find_name("Sun")
if l >= 0.0 { nd_set(l, "py", 0.0 - 80.0) nd_set(l, "b", 600.0) nd_set(l, "a", 3.0) nd_set(l, "col", c_rgb(200.0, 210.0, 255.0)) }
bind k = 0.0
while k < 8.0 {
bind ang = k * 0.7854
ss_light("Lantern " + num_str(k + 1.0), cos(ang) * 70.0, 0.0 - 12.0, sin(ang) * 70.0, c_pal(k), 2.0, 90.0)
bind k = k + 1.0
}
bind wings = ["North wing|0|-250", "South wing|0|250", "West wing|-250|0", "East wing|250|0"]
bind i = 0.0
while i < 4.0 {
bind p = str_split(list_get(wings, i), "|")
bind t = nd_add("trigger", list_get(p, 0), 0.0)
nd_set(t, "px", to_number(list_get(p, 1)))
nd_set(t, "py", 0.0)
nd_set(t, "pz", to_number(list_get(p, 2)))
nd_set(t, "sx", 40.0)
nd_set(t, "sy", 6.0)
nd_set(t, "sz", 12.0)
nd_set(t, "a", 0.0)
nd_set(t, "txt", "say:" + list_get(p, 0) + " - the levels beyond are generated by the original game (proc_gen_level.ling)")
bind i = i + 1.0
}
bind t2 = nd_add("trigger", "Court", 0.0)
nd_set(t2, "py", 0.0)
nd_set(t2, "pz", 40.0)
nd_set(t2, "sx", 24.0)
nd_set(t2, "sy", 6.0)
nd_set(t2, "sz", 8.0)
nd_set(t2, "a", 1.0)
nd_set(t2, "txt", "cutscene:scenes/chapter1.lscene")
bind t3 = nd_add("trigger", "Back to the menu", 0.0)
nd_set(t3, "py", 0.0)
nd_set(t3, "pz", 0.0 - 240.0)
nd_set(t3, "sx", 40.0)
nd_set(t3, "sy", 6.0)
nd_set(t3, "sz", 6.0)
nd_set(t3, "a", 0.0)
nd_set(t3, "txt", "say:Esc returns to the menu")
save_scene(p_join(proj_root(), "scenes/hub.lscene"))
}
fn ss_wall(name, x, y, z, sx, sy, sz, col) {
bind b = nd_add("box", name, 0.0)
nd_set(b, "px", x)
nd_set(b, "py", y)
nd_set(b, "pz", z)
nd_set(b, "sx", sx)
nd_set(b, "sy", sy)
nd_set(b, "sz", sz)
nd_set(b, "col", col)
nd_set(b, "tag", "solid")
return b
}