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_soul.ling
# rt_soul.ling - "soul" mode: the 2019 Soul Symphony (Panda3D, soul-symphony-zh) in Ling.
#
# A scene with `mode=soul` is either the title (`screen=title`) or a level (`level=level00`). The
# assets are the original's, converted by the project's tools/ssz_import.py: the .bam levels as
# .ltm textured meshes (+ .marks for their portals / NPCs / letters / monsters), every texture as
# a traced SVGZ, the sfx WAVs and the OGG soundtrack.
#
# Faithful to the original (worldcage.py, level.py, player.py, titleScreen.py):
# ball: Bullet sphere r 1.5, mass 4, restitution 0.75, gravity 9.81*8; WASD / arrows / left stick
# push it with force 100 along the world axes, Q / E / right stick torque it, Space / A jumps
# (impulse 160, a double jump), Shift / B brakes (damping 0.82); the camera sits at
# ball + (0, -48, +30) Panda units looking at it, 40 degree lens
# level: floor / wall / ceiling textures picked at random from the 100 patterns of each kind,
# their UVs scrolling and scaling with the level clock, colour-cycled through the seven
# colours, translucent (floor most, ceiling least)
# stars: textured sprites from the 209 star images falling at 90.2 u/s, alpha 0.5, shrinking
# letters: a-z in Daemon / Chesilin / Note / Music fonts on every letter mark, spinning, +1 each
# portals: base + flower spinning, colour-cycled; hold jump near one, release to warp
# NPCs: face + emblem planes spinning opposite ways, a name, a poem when you talk
# HUD: the rainbow health strip, 得分 in a random Chinese font, the tilted minimap,
# the spinning star mandalas; music: a random track per level, the title theme on the title
# Improvements: the soft see-through circle around the ball (walls between it and the camera fade),
# torches along the corridors lighting the walls per pixel, a little dark distance fog,
# depth-buffered sorting, smooth colour cycling instead of per-frame strobing, frame-blur trails
# set to a moderate amount, working yin-yang monsters (yin hurts, yang heals), a hub.
fn soul_on() { return st_get("ss_on", 0.0) > 0.5 }
fn ss_is_title() { return st_get("ss_screen", "") == "title" }
# ── palettes ─────────────────────────────────────────────────────────────────
fn ss_roygbiv(k) {
bind cols = [c_rgb(255.0, 0.0, 0.0), c_rgb(255.0, 128.0, 0.0), c_rgb(255.0, 255.0, 0.0), c_rgb(0.0, 255.0, 0.0), c_rgb(0.0, 0.0, 255.0), c_rgb(74.0, 0.0, 130.0), c_rgb(143.0, 0.0, 255.0)]
bind i = floor(k) - floor(floor(k) / 7.0) * 7.0
return list_get(cols, i)
}
# the level surfaces' seven: R O Y G B violet magenta
fn ss_surf(k) {
bind cols = [c_rgb(255.0, 0.0, 0.0), c_rgb(255.0, 128.0, 0.0), c_rgb(255.0, 255.0, 0.0), c_rgb(0.0, 255.0, 0.0), c_rgb(0.0, 0.0, 255.0), c_rgb(128.0, 0.0, 255.0), c_rgb(255.0, 0.0, 255.0)]
bind i = floor(k) - floor(floor(k) / 7.0) * 7.0
return list_get(cols, i)
}
# smooth position on a 7-colour wheel (t in colours)
fn ss_cycle(t, surf) {
bind a = floor(t)
bind f = t - a
if surf { return c_mix(ss_surf(a), ss_surf(a + 1.0), f) }
return c_mix(ss_roygbiv(a), ss_roygbiv(a + 1.0), f)
}
# one step of the [R, O, Y, G, B, I, V] array per rendered frame; each element passes its own
# offset so floor, walls, ceiling, ball, letters, portals and NPCs never share a colour
fn ss_strobe(off) { return ss_roygbiv(st_get("ss_frame", 0.0) + off) }
# the 36-step gradient the ball, letters and score use: red..violet in 6 segments
fn ss_grad(t) {
bind stops = [c_rgb(255.0, 0.0, 0.0), c_rgb(255.0, 128.0, 0.0), c_rgb(255.0, 255.0, 0.0), c_rgb(0.0, 255.0, 0.0), c_rgb(0.0, 255.0, 255.0), c_rgb(0.0, 0.0, 255.0), c_rgb(143.0, 0.0, 255.0)]
bind u = t - floor(t / 6.0) * 6.0
bind i = floor(u)
bind f = sin(1.5708 * (u - i))
return c_mix(list_get(stops, i), list_get(stops, i + 1.0), f)
}
# ── assets ───────────────────────────────────────────────────────────────────
fn ss_p(rel) { return game_path("assets/" + rel) }
fn ss_files(dir, ext) {
bind out = []
bind items = plat_list(ss_p(dir))
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) == ext { bind out = list_push(out, nm) }
bind i = i + 1.0
}
return out
}
fn ss_tex(rel, size) {
bind key = "sst:" + rel
if st_has(key) > 0.5 { return st_get(key, 0.0 - 1.0) }
bind id = plat_tex_load(ss_p(rel), size)
st_set(key, id)
return id
}
fn ss_lines(rel) {
bind text = plat_read(ss_p(rel))
bind out = []
bind ls = str_split(text, "\n")
bind i = 0.0
while i < len(ls) {
bind l = trim(list_get(ls, i))
if l != "" { bind out = list_push(out, l) }
bind i = i + 1.0
}
return out
}
fn ss_pick(l) { return list_get(l, floor(rand() * (len(l) - 0.001))) }
fn ss_assets() {
if st_get("ss_assets", 0.0) > 0.5 { return 0.0 }
st_set("ss_assets", 1.0)
st_set("ss_orb", plat_tm_load(ss_p("models/orb.ltm")))
st_set("ss_orb_top", ss_tex("textures/ball/ball_top.svgz", 256.0))
st_set("ss_orb_bot", ss_tex("textures/ball/ball_bottom.svgz", 256.0))
st_set("ss_orb_mid", ss_tex("textures/ball/ball_middle.svgz", 256.0))
st_set("ss_portal", plat_tm_load(ss_p("models/portal00.ltm")))
st_set("ss_ptex_base", ss_tex("textures/portals/base00.svgz", 256.0))
st_set("ss_ptex_flower", ss_tex("textures/portals/effect01.svgz", 256.0))
st_set("ss_npc", plat_tm_load(ss_p("models/npc01.ltm")))
st_set("ss_yinyang", ss_tex("textures/monster/yinyang.svg", 128.0))
st_set("ss_ui_star", ss_tex("textures/ui/star.svgz", 512.0))
st_set("ss_ui_star2", ss_tex("textures/ui/star2.svgz", 512.0))
st_set("ss_logo", ss_tex("textures/ui/SoulSymphonyLogoCN.svgz", 1024.0))
# the 209 star sprites, contiguous texture ids
bind stars = ss_files("textures/stars", "svgz")
bind first = 0.0 - 1.0
bind i = 0.0
while i < len(stars) {
bind id = plat_tex_load(ss_p("textures/stars/" + list_get(stars, i)), 64.0)
if i == 0.0 { bind first = id }
bind i = i + 1.0
}
st_set("ss_star0", first)
st_set("ss_nstars", len(stars))
st_set("ss_pat_floor", ss_files("textures/patterns/floor", "svgz"))
st_set("ss_pat_wall", ss_files("textures/patterns/wall", "svgz"))
st_set("ss_pat_ceil", ss_files("textures/patterns/ceiling", "svgz"))
st_set("ss_faces", ss_files("textures/npc/faces", "svgz"))
st_set("ss_emblems", ss_files("textures/npc/emblems", "svgz"))
st_set("ss_music", ss_files("music", "ogg"))
st_set("ss_text_fonts", ss_files("fonts/text", "ttf"))
st_set("ss_dialogs", ss_lines("text/dialog.txt"))
st_set("ss_names", ss_lines("text/names.txt"))
st_set("ss_portal_lines", ss_lines("text/portal.txt"))
bind gf = ["Daemon.otf", "Chesilin.otf", "Note.otf", "Music.otf"]
bind i = 0.0
while i < 4.0 {
st_set("ss_gfont" + num_str(i), plat_font_load(ss_p("fonts/konnarian/" + list_get(gf, i))))
bind i = i + 1.0
}
# sfx
bind names = ["ball-jump", "ball_roll", "boing00", "boing01", "boing02", "boing03", "boing04", "hover", "pickup", "start-dialog", "warp", "soul-symphony", "portal_loop", "portal_loop01", "portal_loop02", "portal_loop03", "portal_loop04", "portal_loop05", "portal_loop06"]
bind i = 0.0
while i < len(names) {
if plat_has_audio() { st_set("ss_sfx_" + list_get(names, i), plat_sample_load(ss_p("audio/" + list_get(names, i) + ".wav"))) }
bind i = i + 1.0
}
return 0.0
}
fn ss_sfx(name, vol, pitch) {
if !plat_has_audio() { return 0.0 - 1.0 }
bind id = st_get("ss_sfx_" + name, 0.0 - 1.0)
if id < 0.0 { return 0.0 - 1.0 }
return plat_sample_play_pitch(id, vol, pitch)
}
fn ss_text_font() {
bind fonts = st_get("ss_text_fonts", [])
if len(fonts) < 1.0 { return st_get("font", 0.0) }
bind nm = ss_pick(fonts)
bind key = "ssf:" + nm
if st_has(key) < 0.5 { st_set(key, plat_font_load(ss_p("fonts/text/" + nm))) }
bind f = st_get(key, 0.0 - 1.0)
if f < 0.0 { return st_get("font", 0.0) }
return f
}
# music: a random track per level, no repeat until all have played (bgm.py)
fn ss_next_track() {
bind all = st_get("ss_music", [])
if len(all) < 1.0 { return "" }
if st_llen("ss_mq") < 0.5 {
bind i = 0.0
while i < len(all) {
bind nm = list_get(all, i)
if str_find(nm, "标题") < 0.0 { st_lpush("ss_mq", nm) }
bind i = i + 1.0
}
}
bind k = floor(rand() * (st_llen("ss_mq") - 0.001))
bind nm = st_lget("ss_mq", k)
st_ldel("ss_mq", k)
return nm
}
fn ss_play_music(file, vol) {
if !plat_has_audio() || file == "" { return 0.0 }
bind mid = plat_music_load(ss_p("music/" + file))
if mid >= 0.0 { plat_music_volume(vol) plat_music_play(mid) }
st_set("ss_track", file)
return 0.0
}
# ── enter / leave ────────────────────────────────────────────────────────────
fn ss_lists() { return ["sp_x", "sp_y", "sp_z", "sp_v", "sn_x", "sn_y", "sn_z", "sn_face", "sn_emb", "sn_name", "sn_dlg", "sn_ph", "sl_x", "sl_y", "sl_z", "sl_f", "sl_ch", "sl_ph", "sm_x", "sm_y", "sm_z", "sm_vx", "sm_vy", "sm_vz", "sm_s", "sm_yin", "sm_cd", "sg_r", "sg_c", "sg_ch", "sg_roll"] }
fn soul_enter() {
bind keys = ss_lists()
bind i = 0.0
while i < len(keys) {
st_lclear(list_get(keys, i))
bind i = i + 1.0
}
st_set("ss_on", 1.0)
st_set("ss_screen", sc_ex("screen", "level"))
st_set("ss_t", 0.0)
st_set("ss_fade", 0.0)
st_set("ss_warp_t", 0.0 - 1.0)
st_set("ss_dlg", "")
st_set("ss_tag", "")
ss_assets()
plat_pfx_clear()
plat_pfx_max(3000.0)
plat_post(1.0, 1.0, 0.0, 0.0, 0.0)
plat_frame_blur(to_number(sc_ex("blur", "0.55")))
if ss_is_title() {
ss_title_enter()
} else {
ss_level_enter(sc_ex("level", "level00"))
}
return 0.0
}
fn soul_leave() {
if soul_on() {
plat_post(0.0, 0.0, 0.0, 0.0, 0.0)
plat_frame_blur(0.0)
plat_tm_fov(0.0, 0.0)
plat_tm_hole(0.0, 0.0, 0.0, 0.0)
plat_pfx_clear()
if st_get("ss_roll_v", 0.0 - 1.0) >= 0.0 { plat_sample_stop(st_get("ss_roll_v", 0.0)) }
st_set("ss_roll_v", 0.0 - 1.0)
bind i = 0.0
while i < st_llen("sp_v") {
if st_lget("sp_v", i) >= 0.0 { plat_sample_stop(st_lget("sp_v", i)) }
bind i = i + 1.0
}
}
st_set("ss_on", 0.0)
return 0.0
}
# ── the title ────────────────────────────────────────────────────────────────
fn ss_title_enter() {
ss_play_music("灵林 - 灵魂交响乐 - 标题画面.ogg", 1.0)
ss_sfx("soul-symphony", 1.0, 1.0)
# 16 rings x 16 glyphs (glyphRings.py): random lowercase in Daemon, in-plane turn 90*k
bind r = 0.0
while r < 16.0 {
bind c = 0.0
while c < 16.0 {
st_lpush("sg_r", r)
st_lpush("sg_c", c)
st_lpush("sg_ch", str_slice("abcdefghijklmnopqrstuvwxyz", floor(rand() * 25.99), 1.0))
st_lpush("sg_roll", floor(rand() * 4.99) * 1.5708)
bind c = c + 1.0
}
bind r = r + 1.0
}
return 0.0
}
fn ss_title_update(dt) {
if key_pressed("enter") || key_pressed("return") || plat_gamepad_button(7.0) || key_pressed("space") || (mouse_down() && st_get("ss_t", 0.0) > 0.5) {
if st_get("ss_warp_t", 0.0 - 1.0) < 0.0 { st_set("ss_warp_t", 0.0) st_set("ss_warp_to", sc_ex("next", "scenes/hub.lscene")) }
}
return 0.0
}
# Panda HPR (degrees) -> row-major 3x3 in Ling axes (x right, y down, z forward)
fn ss_hpr(h, p, r) {
bind ch = cos(h * 0.0174533)
bind sh = sin(h * 0.0174533)
bind cp = cos(p * 0.0174533)
bind sp = sin(p * 0.0174533)
bind cr = cos(r * 0.0174533)
bind sr = sin(r * 0.0174533)
# Panda: M = R(roll about y) * P(pitch about x) * H(heading about z), Z-up, applied to row vectors.
# As a column matrix in Panda axes: Hz * Px * Ry
bind a00 = ch * cr - sh * sp * sr
bind a01 = 0.0 - sh * cp
bind a02 = ch * sr + sh * sp * cr
bind a10 = sh * cr + ch * sp * sr
bind a11 = ch * cp
bind a12 = sh * sr - ch * sp * cr
bind a20 = 0.0 - cp * sr
bind a21 = sp
bind a22 = cp * cr
# Ling = S * A * S^-1 with S: (x, y, z)_panda -> (x, -z, y)
return [a00, 0.0 - a02, a01, 0.0 - a20, a22, 0.0 - a21, a10, 0.0 - a12, a11]
}
fn ss_m3(m, x, y, z) { return [list_get(m, 0) * x + list_get(m, 1) * y + list_get(m, 2) * z, list_get(m, 3) * x + list_get(m, 4) * y + list_get(m, 5) * z, list_get(m, 6) * x + list_get(m, 7) * y + list_get(m, 8) * z] }
fn ss_title_world(t) {
# the camera: straight down from 128 at the origin, its parent tumbling 0.05 deg / frame
bind tum = t * 3.0
bind m = ss_hpr(tum, tum * 0.5, tum)
bind eye = ss_m3(m, 0.0, 0.0 - 128.0 - sin(t * 10.0), 0.0)
cam_look_at(list_get(eye, 0), list_get(eye, 1), list_get(eye, 2) - 6.0, 0.0, 0.0, 0.0, 0.5)
cam_apply_native()
plat_tm_fov(40.0, st_get("vp_w", 1280.0))
bind f = st_get("ss_gfont0", 0.0 - 1.0)
if f < 0.0 { return 0.0 }
bind n = st_llen("sg_r")
bind k = 0.0
# every glyph gets a new ROYGBIV colour 15 times a second
bind tick = floor(t * 15.0)
while k < n {
bind r = st_lget("sg_r", k)
bind c = st_lget("sg_c", k)
bind sc = 32.0 - r * 2.0
bind ang = r * t
bind rm = ss_hpr(ang, ang, 0.0)
bind hd = 0.0 - (c + 1.0) * 22.5 * 0.0174533
bind lx = sin(hd) * 3.14159
bind lz = cos(hd) * 3.14159
# glyph plane = the ring plane (Panda XY = Ling XZ), baseline turned by the heading + roll
bind ro = hd + st_lget("sg_roll", k) + c * 0.01 * t * 0.0174533
bind ux = cos(ro)
bind uz = 0.0 - sin(ro)
bind p = ss_m3(rm, lx * sc, 0.0, lz * sc)
bind u = ss_m3(rm, ux, 0.0, uz)
bind v = ss_m3(rm, 0.0 - uz, 0.0, ux)
bind col = ss_strobe(k)
plat_color(col)
plat_text3d(f, list_get(p, 0), list_get(p, 1), list_get(p, 2), list_get(u, 0), list_get(u, 1), list_get(u, 2), list_get(v, 0), list_get(v, 1), list_get(v, 2), sc, st_lget("sg_ch", k))
bind k = k + 1.0
}
return 0.0
}
fn ss_title_hud(x, y, w, h) {
bind t = st_get("ss_t", 0.0)
bind cx = x + w * 0.5
bind cy = y + h * 0.5
bind half = h * 0.5
# star mandalas: 2x2 cards on aspect2d, pulsing 4 -> 0 -> 4 over 3 s, rolling +-60 deg/s,
# colour lerps through the seven at alpha 0.15
bind ph = t - floor(t / 3.0) * 3.0
bind s1 = 4.0
if ph < 1.0 { bind s1 = 4.0 * (1.0 - ph) } else if ph < 1.5 { bind s1 = 0.0 } else if ph < 2.5 { bind s1 = 4.0 * (ph - 1.5) }
bind s2 = 4.0 - s1
bind c1 = ss_strobe(5.0)
bind c2 = ss_strobe(6.0)
if s1 > 0.01 { plat_tm_sprite(st_get("ss_ui_star", 0.0 - 1.0), cx, cy, 2.0 * half * s1, 2.0 * half * s1, t * 60.0 * 0.0174533, c1, 0.15, 0.0) }
if s2 > 0.01 { plat_tm_sprite(st_get("ss_ui_star2", 0.0 - 1.0), cx, cy, 2.0 * half * s2, 2.0 * half * s2, 0.0 - t * 60.0 * 0.0174533, c2, 0.15, 0.0) }
# the logo: 3 x 1.3 aspect2d units, x scale waving 1.45..1.55 every half second, colour cycling
bind tp = t - floor(t)
bind sx = 1.45 + 0.1 * 0.5 * (1.0 + sin(6.28318 * 2.0 * tp))
bind lz = 0.04 * sin(t * 1.3)
plat_tm_sprite(st_get("ss_logo", 0.0 - 1.0), cx, cy - lz * half, 2.0 * half * sx, 2.0 * half * 0.65, 0.0, ss_strobe(3.0), 1.0, 0.0)
# a quiet hint (the original's press_start model was never shown)
if floor(t * 1.5) - floor(t * 1.5 / 2.0) * 2.0 < 0.5 { g_text_c(cx, y + h - 60.0 * h / 720.0, 16.0 * h / 720.0, c_rgb(230.0, 220.0, 255.0), "ENTER / START") }
return 0.0
}
# ── a level ──────────────────────────────────────────────────────────────────
fn ss_level_enter(name) {
st_set("ss_level", name)
bind mk = "ssm:" + name
if st_has(mk) < 0.5 {
bind m = plat_tm_load(ss_p("levels/" + name + ".ltm"))
st_set(mk, m)
st_set("ssc:" + name, plat_coll_build(m, 0.0 - 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 24.0))
st_set("sse:" + name, plat_tm_edges(m, plat_tm_group(m, "floor"), 3000.0))
}
bind m = st_get(mk, 0.0 - 1.0)
st_set("ss_mesh", m)
st_set("ss_coll", st_get("ssc:" + name, 0.0 - 1.0))
st_set("ss_gf", plat_tm_group(m, "floor"))
st_set("ss_gw", plat_tm_group(m, "wall"))
st_set("ss_gc", plat_tm_group(m, "ceil"))
st_set("ss_bounds", plat_tm_bounds(m, 0.0 - 1.0))
# torches: a spot on every other wall block, 7 units up from the floor
bind tk = "sstorch:" + name
if st_has(tk) < 0.5 { st_set(tk, ss_torch_place(plat_tm_wall_spots(m, plat_tm_group(m, "wall"), to_number(sc_ex("torch_spacing", "34")), 7.0), st_get("ssc:" + name, 0.0 - 1.0))) }
st_set("ss_torches", st_get(tk, []))
# textures: one random pattern per surface (level.py)
st_set("ss_tf", ss_tex("textures/patterns/floor/" + ss_pick(st_get("ss_pat_floor", ["x"])), 256.0))
st_set("ss_tw", ss_tex("textures/patterns/wall/" + ss_pick(st_get("ss_pat_wall", ["x"])), 256.0))
st_set("ss_tc", ss_tex("textures/patterns/ceiling/" + ss_pick(st_get("ss_pat_ceil", ["x"])), 256.0))
st_set("ss_cphase", rand() * 7.0)
# the level's empties
bind hub = sc_ex("hub", "0") == "1"
bind lines = str_split(plat_read(ss_p("levels/" + name + ".marks")), "\n")
bind li = 0.0
while li < len(lines) {
bind parts = str_split(trim(list_get(lines, li)), " ")
if len(parts) >= 4.0 {
bind nm = list_get(parts, 0)
bind px = to_number(list_get(parts, 1))
bind py = to_number(list_get(parts, 2))
bind pz = to_number(list_get(parts, 3))
if str_starts_with(nm, "portal") {
st_lpush("sp_x", px) st_lpush("sp_y", py) st_lpush("sp_z", pz)
bind v = 0.0 - 1.0
if plat_has_audio() {
bind loops = ["portal_loop", "portal_loop01", "portal_loop02", "portal_loop03", "portal_loop04", "portal_loop05", "portal_loop06"]
bind sid = st_get("ss_sfx_" + list_get(loops, st_llen("sp_x") - 1.0 - floor((st_llen("sp_x") - 1.0) / 7.0) * 7.0), 0.0 - 1.0)
if sid >= 0.0 { bind v = plat_sample_play(sid, px, py, pz, 0.0, 0.15, 1.0) }
}
st_lpush("sp_v", v)
} else if str_starts_with(nm, "npc") {
st_lpush("sn_x", px) st_lpush("sn_y", py) st_lpush("sn_z", pz)
st_lpush("sn_face", ss_tex("textures/npc/faces/" + ss_pick(st_get("ss_faces", ["x"])), 256.0))
st_lpush("sn_emb", ss_tex("textures/npc/emblems/" + ss_pick(st_get("ss_emblems", ["x"])), 256.0))
st_lpush("sn_name", ss_pick(st_get("ss_names", ["灵"])))
st_lpush("sn_dlg", floor(rand() * max(1.0, len(st_get("ss_dialogs", [])) - 0.01)))
st_lpush("sn_ph", rand() * 6.0)
} else if str_starts_with(nm, "letter") {
st_lpush("sl_x", px) st_lpush("sl_y", py) st_lpush("sl_z", pz)
# the hub's letters are notes: Note / Music glyphs; elsewhere all four fonts
bind fk = floor(rand() * 3.99)
if hub { bind fk = 2.0 + floor(rand() * 1.99) }
st_lpush("sl_f", fk)
st_lpush("sl_ch", str_slice("abcdefghijklmnopqrstuvwxyz", floor(rand() * 25.99), 1.0))
st_lpush("sl_ph", rand() * 6.0)
} else if str_starts_with(nm, "monster") {
ss_add_monster(px, py - 4.0, pz)
} else if str_starts_with(nm, "playerStart") {
st_set("ss_start", [px, py, pz])
}
}
bind li = li + 1.0
}
# monsters.place_monsters(10): around the letters when the level has no monster marks
bind k = st_llen("sm_x")
while k < 10.0 && st_llen("sl_x") > 0.0 {
bind j = floor(rand() * (st_llen("sl_x") - 0.01))
ss_add_monster(st_lget("sl_x", j) + (rand() - 0.5) * 20.0, st_lget("sl_y", j) - 6.0, st_lget("sl_z", j) + (rand() - 0.5) * 20.0)
bind k = k + 1.0
}
# the ball: dropped onto a random portal (+3 up), as worldcage.py does
bind sx = 0.0
bind sy = 0.0 - 10.0
bind sz = 0.0
if st_llen("sp_x") > 0.0 {
bind j = floor(rand() * (st_llen("sp_x") - 0.01))
bind sx = st_lget("sp_x", j)
bind sy = st_lget("sp_y", j) - 3.0
bind sz = st_lget("sp_z", j)
} else if len(st_get("ss_start", [])) > 2.0 {
bind sp = st_get("ss_start", [0.0, 0.0, 0.0])
bind sx = list_get(sp, 0)
bind sy = list_get(sp, 1) - 3.0
bind sz = list_get(sp, 2)
}
st_set("ss_bx", sx) st_set("ss_by", sy) st_set("ss_bz", sz)
st_set("ss_vx", 0.0) st_set("ss_vy", 0.0) st_set("ss_vz", 0.0)
st_set("ss_spawn", [sx, sy, sz])
st_set("ss_rot", [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0])
st_set("ss_spin", 0.0)
st_set("ss_jumps", 0.0)
st_set("ss_ground", 0.0)
st_set("ss_contact", 0.0)
if st_get("ss_run", 0.0) < 0.5 { st_set("ss_run", 1.0) st_set("ss_score", 0.0) st_set("ss_hp", 100.0) }
st_set("ss_hurt_t", 0.0)
st_set("ss_score_font", st_get("font", 0.0))
st_set("ss_star_rot", 0.0)
st_set("ss_star_rot2", 0.0)
# music + the rolling loop
ss_play_music(ss_next_track(), 0.8)
st_set("ss_roll_v", 0.0 - 1.0)
if plat_has_audio() && st_get("ss_sfx_ball_roll", 0.0 - 1.0) >= 0.0 { st_set("ss_roll_v", plat_sample_play_flat(st_get("ss_sfx_ball_roll", 0.0), 0.0, 1.0)) }
return 0.0
}
fn ss_add_monster(x, y, z) {
bind a = rand() * 6.283
st_lpush("sm_x", x) st_lpush("sm_y", y) st_lpush("sm_z", z)
st_lpush("sm_vx", cos(a) * 10.0) st_lpush("sm_vy", 0.0) st_lpush("sm_vz", sin(a) * 10.0)
st_lpush("sm_s", 1.0 + rand())
st_lpush("sm_yin", sel(rand() < 0.5, 1.0, 0.0))
st_lpush("sm_cd", 0.0)
return 0.0
}
# ── update ───────────────────────────────────────────────────────────────────
fn soul_update(dt) {
if !soul_on() { return 0.0 }
bind dt = min(dt, 0.05)
st_set("ss_t", st_get("ss_t", 0.0) + dt)
st_set("ss_frame", st_get("ss_frame", 0.0) + 1.0)
st_set("ss_fade", min(1.0, st_get("ss_fade", 0.0) + dt / 0.5))
if key_pressed("escape") && st_get("game_embedded", 0.0) < 0.5 { st_set("game_quit", 1.0) }
if key_pressed("f11") { plat_screenshot("soul") }
# warp / scene change after a 0.5 s fade out
if st_get("ss_warp_t", 0.0 - 1.0) >= 0.0 {
st_set("ss_warp_t", st_get("ss_warp_t", 0.0) + dt)
if st_get("ss_warp_t", 0.0) > 0.5 {
bind to = st_get("ss_warp_to", "")
soul_leave()
game_goto(to)
return 0.0
}
}
if ss_is_title() { return ss_title_update(dt) }
st_set("ss_clock", st_get("ss_clock", 0.0) + dt * 0.06)
ss_ball_update(dt)
ss_interact(dt)
ss_monsters(dt)
# stars: sprites raining around the ball (SoulParticles: 90.2 u/s, 0..6 s, 12.8 -> 1.28, alpha 0.5)
bind n = st_get("ss_nstars", 0.0)
if n > 0.0 {
bind bx = st_get("ss_bx", 0.0)
bind by = st_get("ss_by", 0.0)
bind bz = st_get("ss_bz", 0.0)
bind col = ss_strobe(4.0)
plat_pfx_rain(bx - 260.0, bz - 140.0, bx + 260.0, bz + 380.0, by - 160.0, 22.0, 90.2, 6.0, 12.8, 1.28, st_get("ss_star0", 0.0), n, 0.5, col, 120.0)
}
plat_pfx_step(dt, 0.0, 0.0, 1000000.0, 0.0)
# the camera: ball + (0, -48, +30) Panda = (0, -30, -48) here, looking at the ball
bind bx = st_get("ss_bx", 0.0)
bind by = st_get("ss_by", 0.0)
bind bz = st_get("ss_bz", 0.0)
cam_look_at(bx, by - 30.0, bz - 48.0, bx, by, bz, 0.5)
return 0.0
}
fn ss_ball_update(dt) {
bind m = 4.0
bind r = 1.5
bind g = 78.48
# input (worldcage.py: keys override the stick's components)
bind fx = 0.0
bind fz = 0.0
bind tq = 0.0
if abs(plat_gamepad_lx()) > 0.12 { bind fx = plat_gamepad_lx() }
if abs(plat_gamepad_ly()) > 0.12 { bind fz = 0.0 - plat_gamepad_ly() }
if abs(plat_gamepad_rx()) > 0.12 { bind tq = plat_gamepad_rx() }
if key_down("w") || key_down("up") { bind fz = 1.0 }
if key_down("s") || key_down("down") { bind fz = 0.0 - 1.0 }
if key_down("a") || key_down("left") { bind fx = 0.0 - 1.0 }
if key_down("d") || key_down("right") { bind fx = 1.0 }
if key_down("q") { bind tq = 1.0 }
if key_down("e") { bind tq = 0.0 - 1.0 }
if st_get("game_autoplay", 0.0) > 0.5 {
bind at = st_get("ss_t", 0.0)
bind fx = sin(at * 0.45)
bind fz = cos(at * 0.31)
}
bind talking = st_get("ss_talking", 0.0) > 0.5
if talking { bind fx = 0.0 bind fz = 0.0 }
bind brake = key_down("lshift") || key_down("rshift") || key_down("shift") || plat_gamepad_button(1.0)
bind vx = st_get("ss_vx", 0.0)
bind vy = st_get("ss_vy", 0.0)
bind vz = st_get("ss_vz", 0.0)
# jump: impulse 160 (dv 40 up), ground jump + one air jump
bind jump = key_pressed("space") || (plat_gamepad_button(0.0) && st_get("ss_pad_a", 0.0) < 0.5)
if st_get("game_autoplay", 0.0) > 0.5 && floor(st_get("ss_t", 0.0) / 3.0) != floor((st_get("ss_t", 0.0) - dt) / 3.0) { bind jump = true }
st_set("ss_pad_a", sel(plat_gamepad_button(0.0), 1.0, 0.0))
if jump && !ss_near_talkable() {
if st_get("ss_ground", 0.0) > 0.5 { st_set("ss_jumps", 0.0) }
if st_get("ss_jumps", 0.0) < 2.0 {
bind vy = vy - 160.0 / m
st_set("ss_jumps", st_get("ss_jumps", 0.0) + 1.0)
ss_sfx("ball-jump", 0.8, 1.0)
}
}
bind ax = fx * 100.0 / m
bind az = fz * 100.0 / m
# spin about the vertical (torque 100, sphere inertia 2/5 m r^2)
bind spin = st_get("ss_spin", 0.0) + tq * 100.0 / (0.4 * m * r * r) * dt
bind spin = spin * pow(0.5, dt)
st_set("ss_spin", spin)
bind coll = st_get("ss_coll", 0.0 - 1.0)
bind x = st_get("ss_bx", 0.0)
bind y = st_get("ss_by", 0.0)
bind z = st_get("ss_bz", 0.0)
bind sp = sqrt(vx * vx + vy * vy + vz * vz)
bind steps = clampf(ceil(sp * dt / 0.45), 1.0, 12.0)
bind h = dt / steps
bind ground = false
bind impact = 0.0
bind k = 0.0
while k < steps {
bind vx = vx + ax * h
bind vy = vy + g * h
bind vz = vz + az * h
if brake || talking {
bind damp = pow(1.0 - sel(talking, 0.999, 0.82), h)
bind vx = vx * damp
bind vy = vy * sel(talking, damp, pow(0.18, h))
bind vz = vz * damp
}
bind x = x + vx * h
bind y = y + vy * h
bind z = z + vz * h
bind res = plat_coll_sphere(coll, x, y, z, r)
if list_get(res, 6) > 0.0 {
bind x = list_get(res, 0)
bind y = list_get(res, 1)
bind z = list_get(res, 2)
bind nx = list_get(res, 3)
bind ny = list_get(res, 4)
bind nz = list_get(res, 5)
bind vn = vx * nx + vy * ny + vz * nz
if vn < 0.0 {
if vn < 0.0 - 6.0 {
# restitution 0.75
bind vx = vx - 1.75 * vn * nx
bind vy = vy - 1.75 * vn * ny
bind vz = vz - 1.75 * vn * nz
bind impact = max(impact, 0.0 - vn)
} else {
bind vx = vx - vn * nx
bind vy = vy - vn * ny
bind vz = vz - vn * nz
}
# Bullet friction 0.5 on a rolling ball: a little rolling resistance
bind fr = pow(0.82, h)
bind tvx = vx - (vx * nx + vy * ny + vz * nz) * nx
bind tvz = vz - (vx * nx + vy * ny + vz * nz) * nz
bind vx = vx - tvx * (1.0 - fr)
bind vz = vz - tvz * (1.0 - fr)
}
if ny < 0.0 - 0.45 { bind ground = true }
}
bind k = k + 1.0
}
if ground && st_get("ss_ground", 0.0) < 0.5 { st_set("ss_jumps", 0.0) }
st_set("ss_ground", sel(ground, 1.0, 0.0))
# bounce sound: a random boing, louder and higher for harder hits
if impact > 0.0 {
bind vol = clampf(impact / 40.0, 0.15, 1.0)
ss_sfx("boing0" + num_str(floor(rand() * 4.99)), vol, vol / 4.0 + 0.5 + rand() * 0.1)
}
# fell out of the world: back to the spawn
bind bnd = st_get("ss_bounds", [0.0, 0.0, 0.0, 0.0, 400.0, 0.0])
if y > list_get(bnd, 4) + 300.0 {
bind s = st_get("ss_spawn", [0.0, 0.0, 0.0])
bind x = list_get(s, 0)
bind y = list_get(s, 1)
bind z = list_get(s, 2)
bind vx = 0.0
bind vy = 0.0
bind vz = 0.0
}
st_set("ss_bx", x) st_set("ss_by", y) st_set("ss_bz", z)
st_set("ss_vx", vx) st_set("ss_vy", vy) st_set("ss_vz", vz)
# rolling: omega = up x v / r, plus the spin about up; integrate the orientation
bind wx = (0.0 - vz) / r
bind wy = 0.0 - spin
bind wz = vx / r
if !ground { bind wx = wx * 0.98 bind wz = wz * 0.98 }
bind wl = sqrt(wx * wx + wy * wy + wz * wz)
if wl * dt > 0.0001 {
st_set("ss_rot", ss_rot_mul(ss_axis_angle(wx / wl, wy / wl, wz / wl, wl * dt), st_get("ss_rot", [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0])))
}
# rolling sound follows the speed (player.py: playRate 0.05 * (|vx| + |vy| + |vz|))
bind rv = st_get("ss_roll_v", 0.0 - 1.0)
if rv >= 0.0 {
bind rate = 0.05 * (abs(vx) + abs(vy) + abs(vz))
plat_voice(rv, sel(ground && rate > 0.05, 0.7, 0.0), clampf(rate, 0.05, 3.0))
}
return 0.0
}
fn ss_axis_angle(x, y, z, a) {
bind c = cos(a)
bind s = sin(a)
bind t = 1.0 - c
return [t * x * x + c, t * x * y - s * z, t * x * z + s * y, t * x * y + s * z, t * y * y + c, t * y * z - s * x, t * x * z - s * y, t * y * z + s * x, t * z * z + c]
}
fn ss_rot_mul(a, b) {
bind out = []
bind i = 0.0
while i < 3.0 {
bind j = 0.0
while j < 3.0 {
bind out = list_push(out, list_get(a, i * 3.0) * list_get(b, j) + list_get(a, i * 3.0 + 1.0) * list_get(b, 3.0 + j) + list_get(a, i * 3.0 + 2.0) * list_get(b, 6.0 + j))
bind j = j + 1.0
}
bind i = i + 1.0
}
# re-orthonormalise the rows so rounding never shears the ball
bind r0 = [list_get(out, 0), list_get(out, 1), list_get(out, 2)]
bind l0 = sqrt(list_get(r0, 0) * list_get(r0, 0) + list_get(r0, 1) * list_get(r0, 1) + list_get(r0, 2) * list_get(r0, 2))
bind r1 = [list_get(out, 3), list_get(out, 4), list_get(out, 5)]
bind d = (list_get(r0, 0) * list_get(r1, 0) + list_get(r0, 1) * list_get(r1, 1) + list_get(r0, 2) * list_get(r1, 2)) / (l0 * l0)
bind r1x = list_get(r1, 0) - d * list_get(r0, 0)
bind r1y = list_get(r1, 1) - d * list_get(r0, 1)
bind r1z = list_get(r1, 2) - d * list_get(r0, 2)
bind l1 = sqrt(r1x * r1x + r1y * r1y + r1z * r1z)
bind ax = list_get(r0, 0) / l0
bind ay = list_get(r0, 1) / l0
bind az = list_get(r0, 2) / l0
bind bx = r1x / l1
bind by = r1y / l1
bind bz = r1z / l1
return [ax, ay, az, bx, by, bz, ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx]
}
# NPCs and portals within 5 (XZ) can be talked to with jump
fn ss_near(lx, lz, i, d) {
bind dx = st_lget(lx, i) - st_get("ss_bx", 0.0)
bind dz = st_lget(lz, i) - st_get("ss_bz", 0.0)
return dx * dx + dz * dz < d * d
}
fn ss_near_talkable() {
bind i = 0.0
while i < st_llen("sn_x") {
if ss_near("sn_x", "sn_z", i, 5.0) { return true }
bind i = i + 1.0
}
bind i = 0.0
while i < st_llen("sp_x") {
if ss_near("sp_x", "sp_z", i, 5.0) { return true }
bind i = i + 1.0
}
return false
}
fn ss_interact(dt) {
bind held = key_down("space") || plat_gamepad_button(0.0)
bind pressed = key_pressed("space") || (plat_gamepad_button(0.0) && st_get("ss_pad_a2", 0.0) < 0.5)
st_set("ss_pad_a2", sel(plat_gamepad_button(0.0), 1.0, 0.0))
# nametags
bind tag = 0.0 - 1.0
bind i = 0.0
while i < st_llen("sn_x") {
if ss_near("sn_x", "sn_z", i, 5.0) { bind tag = i }
bind i = i + 1.0
}
if tag >= 0.0 && st_get("ss_tag_i", 0.0 - 1.0) != tag { ss_sfx("hover", 0.7, 1.0) st_set("ss_tag_font", ss_text_font()) }
st_set("ss_tag_i", tag)
# talk to an NPC, or ask a portal
if pressed {
if tag >= 0.0 {
bind dl = st_get("ss_dialogs", [])
if len(dl) > 0.0 { st_set("ss_dlg", str_replace(list_get(dl, st_lget("sn_dlg", tag)), "|", "\n")) }
st_set("ss_dlg_font", ss_text_font())
st_set("ss_talking", 1.0)
ss_sfx("start-dialog", 0.8, 1.0)
} else {
bind p = 0.0
while p < st_llen("sp_x") {
if ss_near("sp_x", "sp_z", p, 5.0) {
st_set("ss_dlg", ss_pick(st_get("ss_portal_lines", ["出发吧!"])))
st_set("ss_dlg_font", ss_text_font())
st_set("ss_talking", 1.0)
st_set("ss_portal_ask", 1.0)
}
bind p = p + 1.0
}
}
}
if !held && st_get("ss_talking", 0.0) > 0.5 {
st_set("ss_talking", 0.0)
st_set("ss_dlg", "")
if st_get("ss_portal_ask", 0.0) > 0.5 {
st_set("ss_portal_ask", 0.0)
bind p = 0.0
while p < st_llen("sp_x") {
if ss_near("sp_x", "sp_z", p, 8.0) && st_get("ss_warp_t", 0.0 - 1.0) < 0.0 {
ss_sfx("warp", 1.0, 1.0)
st_set("ss_warp_t", 0.0)
st_set("ss_warp_to", ss_random_level())
}
bind p = p + 1.0
}
}
}
# letters: within 3 (XZ, height ignored) -> +1, pickup at a random pitch 0.1..2
bind i = st_llen("sl_x") - 1.0
while i >= 0.0 {
if ss_near("sl_x", "sl_z", i, 3.0) && abs(st_lget("sl_y", i) - st_get("ss_by", 0.0)) < 12.0 {
ss_sfx("pickup", 1.0, 0.1 + rand() * 1.9)
st_set("ss_score", st_get("ss_score", 0.0) + 1.0)
st_set("ss_score_font", ss_text_font())
plat_pfx_burst(st_lget("sl_x", i), st_lget("sl_y", i) - 2.0, st_lget("sl_z", i), 30.0, 14.0, 0.9, 1.4, 0.0, ss_strobe(i), c_rgb(255.0, 255.0, 255.0), 0.0, 0.3, 0.0, 0.0 - 4.0, 0.0)
st_ldel("sl_x", i) st_ldel("sl_y", i) st_ldel("sl_z", i) st_ldel("sl_f", i) st_ldel("sl_ch", i) st_ldel("sl_ph", i)
}
bind i = i - 1.0
}
return 0.0
}
fn ss_random_level() {
bind all = str_split(sc_ex("warps", "scenes/hub.lscene"), ",")
bind cur = sc_get("path", "")
bind k = 0.0
while k < 8.0 {
bind pick = ss_pick(all)
if !str_ends_with(cur, p_base(pick)) || len(all) < 2.0 { return pick }
bind k = k + 1.0
}
return ss_pick(all)
}
fn ss_monsters(dt) {
bind coll = st_get("ss_coll", 0.0 - 1.0)
bind bx = st_get("ss_bx", 0.0)
bind by = st_get("ss_by", 0.0)
bind bz = st_get("ss_bz", 0.0)
st_set("ss_hurt_t", max(0.0, st_get("ss_hurt_t", 0.0) - dt))
bind i = 0.0
while i < st_llen("sm_x") {
bind s = st_lget("sm_s", i)
bind vx = st_lget("sm_vx", i)
bind vy = st_lget("sm_vy", i) + 9.8 * dt
bind vz = st_lget("sm_vz", i)
bind x = st_lget("sm_x", i) + vx * dt
bind y = st_lget("sm_y", i) + vy * dt
bind z = st_lget("sm_z", i) + vz * dt
bind res = plat_coll_sphere(coll, x, y, z, s * 0.5)
if list_get(res, 6) > 0.0 {
bind x = list_get(res, 0)
bind y = list_get(res, 1)
bind z = list_get(res, 2)
bind nx = list_get(res, 3)
bind ny = list_get(res, 4)
bind nz = list_get(res, 5)
bind vn = vx * nx + vy * ny + vz * nz
if vn < 0.0 {
bind vx = vx - 2.0 * vn * nx
bind vy = vy - 1.6 * vn * ny
bind vz = vz - 2.0 * vn * nz
}
}
# keep wandering at 10 u/s
bind hs = sqrt(vx * vx + vz * vz)
if hs > 0.01 { bind vx = vx / hs * 10.0 bind vz = vz / hs * 10.0 }
if rand() < dt * 0.3 {
bind a = rand() * 6.283
bind vx = cos(a) * 10.0
bind vz = sin(a) * 10.0
}
st_lset("sm_x", i, x) st_lset("sm_y", i, y) st_lset("sm_z", i, z)
st_lset("sm_vx", i, vx) st_lset("sm_vy", i, vy) st_lset("sm_vz", i, vz)
# yin hurts 10, yang heals 10
bind dx = x - bx
bind dy = y - by
bind dz = z - bz
if dx * dx + dy * dy + dz * dz < (1.5 + s * 0.5) * (1.5 + s * 0.5) && st_get("ss_hurt_t", 0.0) <= 0.0 {
st_set("ss_hurt_t", 1.0)
bind yin = st_lget("sm_yin", i) > 0.5
st_set("ss_hp", clampf(st_get("ss_hp", 100.0) + sel(yin, 0.0 - 10.0, 10.0), 0.0, 100.0))
ss_sfx(sel(yin, "boing00", "pickup"), 0.8, sel(yin, 0.5, 1.5))
plat_pfx_burst(x, y, z, 20.0, 10.0, 0.6, 1.0, 0.0, sel(yin, c_rgb(40.0, 40.0, 40.0), c_rgb(255.0, 255.0, 255.0)), c_rgb(255.0, 0.0, 128.0), 0.0, 0.0, 0.0, 0.0, 0.0)
if st_get("ss_hp", 100.0) <= 0.0 {
st_set("ss_hp", 100.0)
bind sp = st_get("ss_spawn", [0.0, 0.0, 0.0])
st_set("ss_bx", list_get(sp, 0)) st_set("ss_by", list_get(sp, 1)) st_set("ss_bz", list_get(sp, 2))
st_set("ss_vx", 0.0) st_set("ss_vy", 0.0) st_set("ss_vz", 0.0)
}
}
bind i = i + 1.0
}
return 0.0
}
# ── drawing ──────────────────────────────────────────────────────────────────
fn soul_world_native() {
if !soul_on() { return 0.0 }
bind t = st_get("ss_t", 0.0)
if ss_is_title() { return ss_title_world(t) }
plat_tm_fov(40.0, st_get("vp_w", 1280.0))
# a little dark violet fog: the far level sinks into the night
bind fogv = nums_parse(sc_ex("fog", "6,3,14,110,560"))
plat_fog(nums_get(fogv, 0, 6.0), nums_get(fogv, 1, 3.0), nums_get(fogv, 2, 14.0), nums_get(fogv, 3, 110.0), nums_get(fogv, 4, 560.0))
ss_torch_lights(t)
bind m = st_get("ss_mesh", 0.0 - 1.0)
bind clock = st_get("ss_clock", 0.0)
bind bx = st_get("ss_bx", 0.0)
bind by = st_get("ss_by", 0.0)
bind bz = st_get("ss_bz", 0.0)
# the see-through circle around the ball for every surface in front of it
plat_tm_hole(bx, by, bz, 7.0)
bind off = 0.5 * sin(clock / 6.0)
bind fsc = 0.05 * sin(clock) + 2.5
bind cp = st_get("ss_cphase", 0.0) + t * 0.45
plat_tm_draw_lit(m, st_get("ss_gf", 0.0), st_get("ss_tf", 0.0), 0.0, 0.0, 0.0, 1.0, 0.0, ss_strobe(0.0), 0.82, off, off, fsc, fsc, 0.25, 0.0, 0.0, to_number(sc_ex("ambient", "0.42")))
plat_tm_draw_lit(m, st_get("ss_gw", 1.0), st_get("ss_tw", 0.0), 0.0, 0.0, 0.0, 1.0, 0.0, ss_strobe(2.0), 0.62, off, off, 0.5 * sin(clock) + 1.0, 0.25 * sin(clock) + 0.5, 0.25, 0.0, 0.0, to_number(sc_ex("ambient", "0.42")))
plat_tm_draw_lit(m, st_get("ss_gc", 2.0), st_get("ss_tc", 0.0), 0.0, 0.0, 0.0, 1.0, 0.0, ss_strobe(4.0), 0.38, off, off, fsc, fsc, 0.25, 0.0, 0.0, to_number(sc_ex("ambient", "0.42")))
plat_tm_hole(0.0, 0.0, 0.0, 0.0)
# portals: base + flower (portal00.bam), spinning and colour-cycled
bind pm = st_get("ss_portal", 0.0 - 1.0)
bind i = 0.0
while i < st_llen("sp_x") {
bind px = st_lget("sp_x", i)
bind py = st_lget("sp_y", i)
bind pz = st_lget("sp_z", i)
bind sb = 1.4 + 0.6 * sin(t * 2.0 + i)
plat_tm_draw(pm, 0.0, st_get("ss_ptex_base", 0.0), px, py, pz, sb, ss_hpr(0.0 - t * 60.0, 0.0, 0.0), ss_strobe(i + 3.0), 0.75, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0)
bind a = t * 90.0
plat_tm_draw(pm, 1.0, st_get("ss_ptex_flower", 0.0), px, py - 0.6, pz, 1.0 + sin(t * 5.0 + i), ss_hpr(a, a, a), ss_strobe(i + 5.0), (sin(t * 3.6) + 1.0) * 0.5, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0)
bind i = i + 1.0
}
ss_torches_draw(t)
# NPCs: face + emblem planes spinning opposite ways, bobbing
bind nm = st_get("ss_npc", 0.0 - 1.0)
bind i = 0.0
while i < st_llen("sn_x") {
bind nx = st_lget("sn_x", i)
bind ny = st_lget("sn_y", i) - 0.05 * sin(3.14159 * t + st_lget("sn_ph", i)) * 6.0
bind nz = st_lget("sn_z", i)
plat_tm_draw(nm, 0.0, st_lget("sn_face", i), nx, ny, nz, 1.0, ss_hpr(t * 60.0, 0.0, 0.0), ss_strobe(i + 2.0), 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0)
plat_tm_draw(nm, 1.0, st_lget("sn_emb", i), nx, ny, nz, 3.0, ss_hpr(0.0 - t * 60.0, 0.0, 0.0), ss_strobe(i + 6.0), 0.5, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0)
bind i = i + 1.0
}
# letters / notes: TextScale 4, spinning +1 deg a frame, colour from the 36-step gradient
bind i = 0.0
while i < st_llen("sl_x") {
bind f = st_get("ss_gfont" + num_str(st_lget("sl_f", i)), 0.0 - 1.0)
bind ddx = st_lget("sl_x", i) - bx
bind ddz = st_lget("sl_z", i) - bz
if f >= 0.0 && ddx * ddx + ddz * ddz < 220.0 * 220.0 {
bind a = t * 1.047 + st_lget("sl_ph", i)
bind ux = cos(a)
bind uz = sin(a)
bind lx = st_lget("sl_x", i) - ux * 1.2
bind lz = st_lget("sl_z", i) - uz * 1.2
plat_color(ss_strobe(i))
plat_text3d(f, lx, st_lget("sl_y", i), lz, ux, 0.0, uz, 0.0, 0.0 - 1.0, 0.0, 4.0, st_lget("sl_ch", i))
}
bind i = i + 1.0
}
# yin-yang monsters
bind i = 0.0
while i < st_llen("sm_x") {
plat_tm_billboard(st_get("ss_yinyang", 0.0 - 1.0), st_lget("sm_x", i), st_lget("sm_y", i), st_lget("sm_z", i), st_lget("sm_s", i) * 2.0, t * sel(st_lget("sm_yin", i) > 0.5, 3.0, 0.0 - 3.0), c_rgb(255.0, 255.0, 255.0), 1.0, 0.0)
bind i = i + 1.0
}
# the ball: orb.bam's three textured parts, rolling, tinted from the gradient
bind om = st_get("ss_orb", 0.0 - 1.0)
bind rot = st_get("ss_rot", [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0])
bind tint = ss_strobe(1.0)
plat_tm_draw(om, 0.0, st_get("ss_orb_top", 0.0), bx, by, bz, 1.0, rot, tint, 1.0, 0.0, 0.0, 1.0, 1.0, 0.35, 0.0, 0.0)
plat_tm_draw(om, 1.0, st_get("ss_orb_bot", 0.0), bx, by, bz, 1.0, rot, tint, 1.0, 0.0, 0.0, 1.0, 1.0, 0.35, 0.0, 0.0)
plat_tm_draw(om, 2.0, st_get("ss_orb_mid", 0.0), bx, by, bz, 1.0, rot, tint, 1.0, 0.0, 0.0, 1.0, 1.0, 0.35, 0.0, 0.0)
plat_pfx_draw()
return 0.0
}
# ── torches ──────────────────────────────────────────────────────────────────
# put each wall spot on the corridor side whose floor is close below, 7 units above that floor;
# spots high up a tall wall (no floor within 40) are dropped
fn ss_torch_place(sp, coll) {
bind out = []
bind n = floor(len(sp) / 6.0)
bind k = 0.0
while k < n {
bind i = k * 6.0
bind x = list_get(sp, i)
bind y = list_get(sp, i + 1.0)
bind z = list_get(sp, i + 2.0)
bind nx = list_get(sp, i + 3.0)
bind nz = list_get(sp, i + 5.0)
bind a = plat_coll_ray(coll, x + nx * 2.0, y - 2.0, z + nz * 2.0, 0.0, 1.0, 0.0, 40.0)
bind b = plat_coll_ray(coll, x - nx * 2.0, y - 2.0, z - nz * 2.0, 0.0, 1.0, 0.0, 40.0)
bind ta = list_get(a, 0)
bind tb = list_get(b, 0)
bind sgn = 0.0
bind t = 0.0
if ta > 0.0 { bind sgn = 1.0 bind t = ta }
if tb > 0.0 && (ta < 0.0 || tb < ta) { bind sgn = 0.0 - 1.0 bind t = tb }
if sgn != 0.0 {
bind fy = y - 2.0 + t
bind out = list_push(out, x)
bind out = list_push(out, fy - 7.0)
bind out = list_push(out, z)
bind out = list_push(out, nx * sgn)
bind out = list_push(out, 0.0)
bind out = list_push(out, nz * sgn)
}
bind k = k + 1.0
}
return out
}
# the flickering lights of the torches near the ball (they light the level per pixel)
fn ss_torch_lights(t) {
bind sp = st_get("ss_torches", [])
bind n = floor(len(sp) / 6.0)
bind bx = st_get("ss_bx", 0.0)
bind bz = st_get("ss_bz", 0.0)
bind k = 0.0
bind used = 0.0
while k < n && used < 24.0 {
bind i = k * 6.0
bind x = list_get(sp, i)
bind z = list_get(sp, i + 2.0)
if (x - bx) * (x - bx) + (z - bz) * (z - bz) < 300.0 * 300.0 {
bind fl = 0.85 + 0.15 * sin(t * 11.0 + k * 1.7) + 0.08 * sin(t * 23.0 + k)
plat_light(x + list_get(sp, i + 3.0) * 2.5, list_get(sp, i + 1.0) - 1.0, z + list_get(sp, i + 5.0) * 2.5, 255.0, 225.0, 180.0, 3.2 * fl, 58.0)
bind used = used + 1.0
}
bind k = k + 1.0
}
return 0.0
}
# the torches themselves: an iron bracket, a flame of layered glows, rising embers
fn ss_torches_draw(t) {
bind sp = st_get("ss_torches", [])
bind n = floor(len(sp) / 6.0)
bind bx = st_get("ss_bx", 0.0)
bind bz = st_get("ss_bz", 0.0)
bind k = 0.0
while k < n {
bind i = k * 6.0
bind x = list_get(sp, i)
bind y = list_get(sp, i + 1.0)
bind z = list_get(sp, i + 2.0)
if (x - bx) * (x - bx) + (z - bz) * (z - bz) < 300.0 * 300.0 {
bind nx = list_get(sp, i + 3.0)
bind nz = list_get(sp, i + 5.0)
bind ang = atan2(nx, nz)
plat_color(c_rgb(40.0, 26.0, 18.0))
plat_shape("box", x + nx * 0.6, y + 0.6, z + nz * 0.6, 0.3, 0.3, 0.6, 0.0, ang, 0.0, 0.0)
plat_shape("cylinder", x + nx * 1.2, y, z + nz * 1.2, 0.35, 1.4, 0.35, 0.0, 0.0, 0.0, 0.0)
bind fl = 0.8 + 0.2 * sin(t * 13.0 + k * 2.1)
bind fx = x + nx * 1.2
bind fz = z + nz * 1.2
plat_tm_billboard(0.0 - 1.0, fx, y - 2.4, fz, 12.0 * fl, 0.0, c_rgb(255.0, 110.0, 30.0), 0.6, 1.0)
plat_tm_billboard(0.0 - 1.0, fx, y - 2.1, fz, 5.5 * fl, 0.0, c_rgb(255.0, 210.0, 90.0), 0.9, 1.0)
plat_tm_billboard(0.0 - 1.0, fx, y - 1.7, fz, 1.6, 0.0, c_rgb(255.0, 255.0, 220.0), 1.0, 1.0)
if rand() < 0.15 {
plat_pfx_emit(fx + (rand() - 0.5) * 0.6, y - 2.4, fz + (rand() - 0.5) * 0.6, (rand() - 0.5) * 1.5, 0.0 - 4.0 - rand() * 3.0, (rand() - 0.5) * 1.5, 1.0, 0.35, 0.0, c_rgb(255.0, 200.0, 80.0), c_rgb(255.0, 60.0, 10.0), 0.0, 0.0, 0.0, 1.0)
}
}
bind k = k + 1.0
}
return 0.0
}
# ── HUD ──────────────────────────────────────────────────────────────────────
fn ss_text(f, x, y, px, c, s) {
plat_color(c)
plat_text_fill(f, x, y, px, s)
return 0.0
}
fn soul_hud(x, y, w, h) {
if !soul_on() { return 0.0 }
bind t = st_get("ss_t", 0.0)
bind half = h * 0.5
bind cx = x + w * 0.5
bind cy = y + h * 0.5
if ss_is_title() {
ss_title_hud(x, y, w, h)
} else {
# the spinning star mandalas (alpha 0.25 in game, turning with the ball's speed)
bind sp = sqrt(st_get("ss_vx", 0.0) * st_get("ss_vx", 0.0) + st_get("ss_vz", 0.0) * st_get("ss_vz", 0.0))
bind dt = st_get("game_dt", 0.016)
st_set("ss_star_rot", st_get("ss_star_rot", 0.0) + sp * 2.0 * dt * 0.0174533)
bind ph = t - floor(t / 3.0) * 3.0
bind s1 = 4.0
if ph < 1.0 { bind s1 = 4.0 * (1.0 - ph) } else if ph < 1.5 { bind s1 = 0.0 } else if ph < 2.5 { bind s1 = 4.0 * (ph - 1.5) }
bind s2 = 4.0 - s1
if s1 > 0.01 { plat_tm_sprite(st_get("ss_ui_star", 0.0 - 1.0), cx, cy, 2.0 * half * s1, 2.0 * half * s1, st_get("ss_star_rot", 0.0), ss_strobe(5.0), 0.18, 0.0) }
if s2 > 0.01 { plat_tm_sprite(st_get("ss_ui_star2", 0.0 - 1.0), cx, cy, 2.0 * half * s2, 2.0 * half * s2, 0.0 - st_get("ss_star_rot", 0.0), ss_strobe(6.0), 0.18, 0.0) }
# health: a strip along the top edge, 0.025 aspect2d units tall, cycling the rainbow
bind hp = st_get("ss_hp", 100.0) / 100.0
g_rect(x, y, w, half * 0.025, c_rgb(51.0, 51.0, 51.0))
g_rect(x, y, w * hp, half * 0.025, ss_strobe(0.0))
# score: right-aligned at aspect2d (0.98, 0.894), text scale 0.1
bind sf = st_get("ss_score_font", st_get("font", 0.0))
bind txt = "得分: " + num_str(st_get("ss_score", 0.0))
bind px = half * 0.1 * 1.4
bind tw = g_text_w(px, txt)
ss_text(sf, cx + 0.98 * half - tw, cy - 0.894 * half - px * 0.8, px, ss_strobe(2.0), txt)
# minimap: the level's floor outline tilted 45 degrees, bottom right, alpha 0.25
ss_minimap(x, y, w, h)
# NPC nametag
bind ti = st_get("ss_tag_i", 0.0 - 1.0)
if ti >= 0.0 && ti < st_llen("sn_x") {
bind p = ss_project(st_lget("sn_x", ti), st_lget("sn_y", ti) - 7.0, st_lget("sn_z", ti), x, y, w, h)
if list_get(p, 2) > 0.0 {
bind nm = st_lget("sn_name", ti)
bind f = st_get("ss_tag_font", st_get("font", 0.0))
ss_text(f, list_get(p, 0) - g_text_w(26.0 * h / 720.0, nm) * 0.5, list_get(p, 1), 26.0 * h / 720.0, ss_strobe(3.0), nm)
}
}
# dialog: centred, word-wrapped, a random Chinese font
bind dlg = st_get("ss_dlg", "")
if dlg != "" {
bind f = st_get("ss_dlg_font", st_get("font", 0.0))
bind lines = str_split(dlg, "\n")
bind sz = half * 0.08
bind ly = cy - len(lines) * sz * 0.65
bind k = 0.0
while k < len(lines) {
bind l = list_get(lines, k)
bind lw = g_text_w(sz, l)
ss_text(f, cx - lw * 0.5 + 2.0, ly + 2.0, sz, c_rgb(0.0, 0.0, 0.0), l)
ss_text(f, cx - lw * 0.5, ly, sz, c_rgb(255.0, 255.0, 255.0), l)
bind ly = ly + sz * 1.3
bind k = k + 1.0
}
}
}
# fades: in over 0.5 s on enter, out over 0.5 s before a warp
bind a = 1.0 - st_get("ss_fade", 1.0)
if st_get("ss_warp_t", 0.0 - 1.0) >= 0.0 { bind a = max(a, clampf(st_get("ss_warp_t", 0.0) / 0.5, 0.0, 1.0)) }
if a > 0.01 { g_rect_a(x, y, w, h, c_rgb(0.0, 0.0, 0.0), a) }
return 0.0
}
# world -> screen with the 40 degree lens; [sx, sy, visible]
fn ss_project(px, py, pz, x, y, w, h) {
bind v = cam_to_view(px, py, pz)
bind d = list_get(v, 2) + 0.5
if d < 0.5 { return [0.0, 0.0, 0.0] }
bind f = (w * 0.5) / tan(0.349066)
return [x + w * 0.5 + f * list_get(v, 0) / d, y + h * 0.5 + f * list_get(v, 1) / d, 1.0]
}
fn ss_minimap(x, y, w, h) {
bind e = st_get("sse:" + st_get("ss_level", ""), [])
bind n = floor(len(e) / 6.0)
if n < 1.0 { return 0.0 }
bind b = st_get("ss_bounds", [0.0 - 1.0, 0.0 - 1.0, 0.0 - 1.0, 1.0, 1.0, 1.0])
bind sx = list_get(b, 3) - list_get(b, 0)
bind sz = list_get(b, 5) - list_get(b, 2)
bind mx = (list_get(b, 3) + list_get(b, 0)) * 0.5
bind mz = (list_get(b, 5) + list_get(b, 2)) * 0.5
bind half = h * 0.5
bind size = half * 0.5
bind sc = size / max(sx, sz)
bind ox = x + w * 0.5 + 0.974 * half - size * 0.55
bind oy = y + h * 0.5 + 0.954 * half - size * 0.42
bind c = ss_strobe(5.0)
bind k = 0.0
while k < n {
bind i = k * 6.0
bind ax = ox + (list_get(e, i) - mx) * sc
bind ay = oy - (list_get(e, i + 2.0) - mz) * sc * 0.7071 + list_get(e, i + 1.0) * sc * 0.7071
bind bx = ox + (list_get(e, i + 3.0) - mx) * sc
bind by = oy - (list_get(e, i + 5.0) - mz) * sc * 0.7071 + list_get(e, i + 4.0) * sc * 0.7071
g_line_a(ax, ay, bx, by, c, 0.45)
bind k = k + 1.0
}
bind px = ox + (st_get("ss_bx", 0.0) - mx) * sc
bind py = oy - (st_get("ss_bz", 0.0) - mz) * sc * 0.7071 + st_get("ss_by", 0.0) * sc * 0.7071
g_disc(px, py, 3.5, ss_strobe(1.0))
return 0.0
}