lingdev
↓ 0 下载LingDev: a Godot / Unity-style game editor written in Ling. 2-D and 3-D scenes, Flash-style timeline and cutscenes, HUDs over 3-D, Ling scripting, audio / music / MIDI, builds for Windows, Linux, macOS, web, Raspberry Pi and LingOS. Ships with Ling Agent (an N64-style spy FPS), Ling Breaker (a 3-D breakout) and Moko's Picture Book (a kawaii storybook platformer).
关键词 game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdev代码 / 文件 (136)
ling/project.ling
# project.ling - projects (lingdev.toml), editor settings, recent list, selection, undo, scenes on disk.
# ── settings (settings.txt next to LingDev) ─────────────────────────────────
fn settings_path() { return p_join(st_get("app_root", "."), "settings.txt") }
fn settings_load() {
st_lclear("recent")
bind t = plat_read(settings_path())
if t == "" { return 0.0 }
bind rec = cfg_all(t, "recent")
bind i = 0.0
while i < len(rec) && i < 10.0 {
st_lpush("recent", list_get(rec, i))
bind i = i + 1.0
}
st_set("native_dialogs", to_number(cfg_get(t, "native_dialogs", "0")))
st_set("theme_black", to_number(cfg_get(t, "theme_black", "0")))
st_set("load_last", to_number(cfg_get(t, "load_last", "1")))
st_set("ui_sounds", to_number(cfg_get(t, "ui_sounds", "0")))
st_set("grid_step", to_number(cfg_get(t, "grid_step", "1")))
st_set("orbit_speed", to_number(cfg_get(t, "orbit_speed", "1")))
st_set("autosave", to_number(cfg_get(t, "autosave", "3")))
st_set("ling_exe", cfg_get(t, "ling_exe", ""))
st_set("left_w", to_number(cfg_get(t, "left_w", "260")))
st_set("insp_w", to_number(cfg_get(t, "insp_w", "300")))
st_set("dock_h", to_number(cfg_get(t, "dock_h", "240")))
recent_menu_rebuild()
return 0.0
}
fn settings_save() {
if !plat_can_write() { return 0.0 }
bind s = "# LingDev settings\n"
bind i = 0.0
while i < st_llen("recent") {
bind s = s + "recent=" + st_lget("recent", i) + "\n"
bind i = i + 1.0
}
bind s = s + "native_dialogs=" + num_str(st_get("native_dialogs", 0.0)) + "\n"
bind s = s + "theme_black=" + num_str(st_get("theme_black", 0.0)) + "\n"
bind s = s + "load_last=" + num_str(st_get("load_last", 1.0)) + "\n"
bind s = s + "ui_sounds=" + num_str(st_get("ui_sounds", 0.0)) + "\n"
bind s = s + "grid_step=" + num_str(st_get("grid_step", 1.0)) + "\n"
bind s = s + "orbit_speed=" + num_str(st_get("orbit_speed", 1.0)) + "\n"
bind s = s + "autosave=" + num_str(st_get("autosave", 3.0)) + "\n"
bind s = s + "ling_exe=" + st_get("ling_exe", "") + "\n"
bind s = s + "left_w=" + num_str(st_get("left_w", 260.0)) + "\n"
bind s = s + "insp_w=" + num_str(st_get("insp_w", 300.0)) + "\n"
bind s = s + "dock_h=" + num_str(st_get("dock_h", 240.0)) + "\n"
plat_write(settings_path(), s)
return 0.0
}
fn recent_add(path) {
bind keep = [path]
bind i = 0.0
while i < st_llen("recent") && len(keep) < 10.0 {
bind r = st_lget("recent", i)
if r != path { bind keep = list_push(keep, r) }
bind i = i + 1.0
}
sl_from_list("recent", keep)
recent_menu_rebuild()
settings_save()
}
fn recent_menu_rebuild() {
bind items = []
bind i = 0.0
while i < st_llen("recent") {
bind r = st_lget("recent", i)
bind items = list_push(items, p_base(p_dir(r)) + "|project.recent:" + num_str(i) + "||folder")
bind i = i + 1.0
}
if len(items) < 0.5 { bind items = ["(empty)|none||"] }
st_set("recent_menu", items)
}
# ── selection ────────────────────────────────────────────────────────────────
fn sel_main() {
if st_llen("sel_ids") < 0.5 { return 0.0 }
return st_lget("sel_ids", 0.0)
}
fn sel_count() { return st_llen("sel_ids") }
fn sel_has(id) {
bind i = 0.0
while i < st_llen("sel_ids") {
if st_lget("sel_ids", i) == id { return true }
bind i = i + 1.0
}
return false
}
fn sel_set(id) {
st_lclear("sel_ids")
if id > 0.0 { st_lpush("sel_ids", id) }
}
fn sel_add(id) {
if id > 0.0 && !sel_has(id) { st_lpush("sel_ids", id) }
}
fn sel_toggle(id) {
if sel_has(id) {
bind i = 0.0
while i < st_llen("sel_ids") {
if st_lget("sel_ids", i) == id { st_ldel("sel_ids", i) }
bind i = i + 1.0
}
} else { sel_add(id) }
}
fn sel_clear() { st_lclear("sel_ids") }
fn sel_ids() { return sl_to_list("sel_ids") }
# drop ids that no longer exist
fn sel_prune() {
bind i = st_llen("sel_ids") - 1.0
while i >= 0.0 {
if nd_find(st_lget("sel_ids", i)) < 0.0 { st_ldel("sel_ids", i) }
bind i = i - 1.0
}
}
fn sel_index() {
bind id = sel_main()
if id <= 0.0 { return 0.0 - 1.0 }
return nd_find(id)
}
# ── undo / redo (whole-scene snapshots) ──────────────────────────────────────
fn undo_push() {
st_lpush("undo", sc_serialize())
while st_llen("undo") > 60.0 { st_ldel("undo", 0.0) }
st_lclear("redo")
mark_dirty()
}
fn undo_do() {
bind n = st_llen("undo")
if n < 0.5 { status("Nothing to undo") return 0.0 }
st_lpush("redo", sc_serialize())
bind path = sc_get("path", "")
sc_parse(st_lget("undo", n - 1.0))
sc_set("path", path)
st_ldel("undo", n - 1.0)
sel_prune()
mark_dirty()
status("Undo")
return 0.0
}
fn redo_do() {
bind n = st_llen("redo")
if n < 0.5 { status("Nothing to redo") return 0.0 }
st_lpush("undo", sc_serialize())
bind path = sc_get("path", "")
sc_parse(st_lget("redo", n - 1.0))
sc_set("path", path)
st_ldel("redo", n - 1.0)
sel_prune()
mark_dirty()
status("Redo")
return 0.0
}
fn mark_dirty() { st_set("dirty", 1.0) }
fn status(msg) { st_set("status", msg) }
# ── project file (lingdev.toml: key=value lines) ─────────────────────────────
fn proj_root() { return st_get("proj_root", "") }
fn proj_abs(rel) {
if rel == "" { return "" }
if p_is_abs(rel) { return p_norm(rel) }
return p_join(proj_root(), rel)
}
fn proj_rel(path) {
if proj_root() == "" { return p_norm(path) }
return p_rel(proj_root(), path)
}
fn proj_file() { return p_join(proj_root(), "lingdev.toml") }
fn proj_close() {
st_set("proj_root", "")
st_set("proj_name", "")
st_set("proj_start", "")
st_set("proj_title", "")
st_set("proj_w", 1280.0)
st_set("proj_h", 720.0)
st_set("proj_fps", 60.0)
st_set("proj_source", "")
st_set("proj_icon", "")
st_set("proj_version", "0.1.0")
st_lclear("proj_scenes")
}
fn proj_load(file) {
bind t = plat_read(file)
if t == "" { return false }
proj_close()
bind root = p_dir(p_norm(file))
if !p_is_abs(root) { bind root = p_join(p_norm(fs_cwd()), root) }
st_set("proj_root", root)
st_set("proj_name", cfg_get(t, "name", p_base(p_dir(file))))
st_set("proj_start", cfg_get(t, "start", ""))
st_set("proj_title", cfg_get(t, "title", st_get("proj_name", "")))
bind win = str_split(cfg_get(t, "window", "1280x720"), "x")
st_set("proj_w", to_number(list_get(win, 0)))
st_set("proj_h", sel(len(win) > 1.0, to_number(list_get(win, 1)), 720.0))
st_set("proj_fps", to_number(cfg_get(t, "fps", "60")))
st_set("proj_source", cfg_get(t, "source_root", ""))
st_set("proj_icon", cfg_get(t, "icon", ""))
st_set("proj_version", cfg_get(t, "version", "0.1.0"))
st_set("proj_author", cfg_get(t, "author", ""))
sl_from_list("proj_scenes", cfg_all(t, "scene"))
recent_add(p_norm(file))
status("Opened project " + st_get("proj_name", ""))
return true
}
fn proj_save() {
if proj_root() == "" { return false }
bind s = "# LingDev project\n"
bind s = s + "name=" + st_get("proj_name", "") + "\n"
bind s = s + "title=" + st_get("proj_title", "") + "\n"
bind s = s + "version=" + st_get("proj_version", "0.1.0") + "\n"
bind s = s + "author=" + st_get("proj_author", "") + "\n"
bind s = s + "start=" + st_get("proj_start", "") + "\n"
bind s = s + "window=" + num_str(st_get("proj_w", 1280.0)) + "x" + num_str(st_get("proj_h", 720.0)) + "\n"
bind s = s + "fps=" + num_str(st_get("proj_fps", 60.0)) + "\n"
bind s = s + "icon=" + st_get("proj_icon", "") + "\n"
bind s = s + "source_root=" + st_get("proj_source", "") + "\n"
bind s = s + "assets=assets\nscripts=scripts\n"
bind i = 0.0
while i < st_llen("proj_scenes") {
bind s = s + "scene=" + st_lget("proj_scenes", i) + "\n"
bind i = i + 1.0
}
return plat_write(proj_file(), s)
}
fn proj_add_scene(rel) {
bind i = 0.0
while i < st_llen("proj_scenes") {
if st_lget("proj_scenes", i) == rel { return 0.0 }
bind i = i + 1.0
}
st_lpush("proj_scenes", rel)
if st_get("proj_start", "") == "" { st_set("proj_start", rel) }
proj_save()
return 0.0
}
fn proj_remove_scene(rel) {
bind i = st_llen("proj_scenes") - 1.0
while i >= 0.0 {
if st_lget("proj_scenes", i) == rel { st_ldel("proj_scenes", i) }
bind i = i - 1.0
}
proj_save()
}
# ── scenes on disk ───────────────────────────────────────────────────────────
fn open_scene(path) {
if path == "" { return false }
if !sc_load(path) {
status("Could not read " + path)
return false
}
st_lclear("undo")
st_lclear("redo")
sel_clear()
st_set("dirty", 0.0)
st_set("tl_t", 0.0)
st_set("tl_playing", 0.0)
view_restore_camera()
if proj_root() != "" { proj_add_scene(proj_rel(path)) }
assets_mark_stale()
status("Opened " + p_base(path))
return true
}
fn save_scene(path) {
if path == "" { return false }
view_store_camera()
if !sc_save(path) {
status("Could not write " + path + sel(plat_can_write(), "", " (this runtime is read-only)"))
return false
}
st_set("dirty", 0.0)
if proj_root() != "" { proj_add_scene(proj_rel(path)) }
assets_mark_stale()
status("Saved " + p_base(path))
return true
}
fn new_scene(kind) {
sc_new(sel(kind == "2d", "Menu", sel(kind == "cutscene", "Cutscene", "Level")), kind)
if kind == "3d" || kind == "cutscene" {
bind f = nd_add("plane", "Ground", 0.0)
nd_set(f, "tag", "solid")
bind l = nd_add("light", "Sun", 0.0)
nd_set(l, "px", 4.0)
nd_set(l, "py", 0.0 - 8.0)
nd_set(l, "pz", 0.0 - 4.0)
bind c = nd_add("camera", "Camera", 0.0)
if kind == "3d" {
bind s = nd_add("spawn", "Spawn", 0.0)
nd_set(s, "py", 0.0)
} else {
sc_set("len", 20.0)
clip_add("fx", 0.0, 1.5, "fade_in", "", "", "", 0.0, "linear")
clip_add("camera", 0.0, 8.0, "orbit", "", "", "0,-1,0,6,0.25", 0.0, "linear")
clip_add("dialog", 1.0, 4.0, "say", "Narrator", "A new cutscene.", "", 0.0, "linear")
}
} else {
bind p = nd_add("panel", "Panel", 0.0)
nd_set(p, "px", 440.0)
nd_set(p, "py", 180.0)
nd_set(p, "w", 400.0)
nd_set(p, "h", 360.0)
bind t = nd_add("label", "Title", nd_id(p))
nd_set(t, "px", 40.0)
nd_set(t, "py", 30.0)
nd_set(t, "w", 320.0)
nd_set(t, "a", 28.0)
nd_set(t, "txt", "My Game")
bind b = nd_add("button", "Play", nd_id(p))
nd_set(b, "px", 120.0)
nd_set(b, "py", 140.0)
nd_set(b, "txt", "Play")
nd_set(b, "scr", "goto:scenes/level.lscene")
bind q = nd_add("button", "Quit", nd_id(p))
nd_set(q, "px", 120.0)
nd_set(q, "py", 200.0)
nd_set(q, "txt", "Quit")
nd_set(q, "scr", "quit")
}
st_lclear("undo")
st_lclear("redo")
sel_clear()
st_set("dirty", 1.0)
st_set("tl_t", 0.0)
view_reset_camera()
status("New " + kind + " scene (unsaved)")
}
fn view_store_camera() {
sc_set("camx", st_get("ed_tx", 0.0))
sc_set("camy", st_get("ed_ty", 0.0))
sc_set("camz", st_get("ed_tz", 0.0))
sc_set("camyaw", st_get("ed_yaw", 0.0))
sc_set("campitch", st_get("ed_pitch", 0.3))
sc_set("camzd", st_get("ed_dist", 14.0))
}
fn view_restore_camera() {
st_set("ed_tx", sc_get("camx", 0.0))
st_set("ed_ty", sc_get("camy", 0.0))
st_set("ed_tz", sc_get("camz", 0.0))
st_set("ed_yaw", sc_get("camyaw", 0.0))
st_set("ed_pitch", sc_get("campitch", 0.3))
st_set("ed_dist", max(1.0, sc_get("camzd", 14.0)))
st_set("c2_zoom", 1.0)
st_set("c2_ox", 0.0)
st_set("c2_oy", 0.0)
}
fn view_reset_camera() {
st_set("ed_tx", 0.0)
st_set("ed_ty", 0.0 - 1.0)
st_set("ed_tz", 0.0)
st_set("ed_yaw", 0.6)
st_set("ed_pitch", 0.42)
st_set("ed_dist", 16.0)
st_set("c2_zoom", 1.0)
st_set("c2_ox", 0.0)
st_set("c2_oy", 0.0)
}
# ── project creation (templates) ─────────────────────────────────────────────
fn runtime_files() {
return ["theme.ling", "core.ling", "ds.ling", "icons.ling", "rt/rt_data.ling", "rt/rt_ease.ling", "rt/rt_cam.ling", "rt/rt_scene.ling", "rt/rt_sw3d.ling", "rt/rt_draw.ling", "rt/rt_cut.ling", "rt/rt_game.ling", "rt/rt_hud.ling", "rt/rt_ai.ling", "rt/rt_guard.ling", "rt/rt_fps.ling", "rt/rt_brick.ling", "rt/rt_soul.ling", "rt/rt_story.ling"]
}
# copy the runtime (and both platform layers) into <root>/rt
fn proj_install_runtime(root) {
bind app = st_get("app_root", ".")
plat_mkdir(p_join(root, "rt"))
bind files = runtime_files()
bind i = 0.0
while i < len(files) {
bind f = list_get(files, i)
plat_copy(p_join(p_join(app, "ling"), f), p_join(p_join(root, "rt"), p_base(f)))
bind i = i + 1.0
}
plat_copy(p_join(p_join(app, "ling"), "plat_desktop.ling"), p_join(p_join(root, "rt"), "plat_desktop.ling"))
plat_copy(p_join(p_join(app, "ling"), "plat_os.ling"), p_join(p_join(root, "rt"), "plat_os.ling"))
plat_mkdir(p_join(root, "assets/fonts"))
if !file_exists(p_join(root, "assets/fonts/ui.ttf")) { plat_copy(p_join(app, "assets/font.ttf"), p_join(root, "assets/fonts/ui.ttf")) }
}
fn proj_create(dir, name, tpl, w, h) {
bind root = p_join(dir, name)
if !plat_mkdir(root) {
status("Could not create " + root)
return false
}
plat_mkdir(p_join(root, "scenes"))
plat_mkdir(p_join(root, "scripts"))
plat_mkdir(p_join(root, "assets"))
plat_mkdir(p_join(root, "assets/models"))
plat_mkdir(p_join(root, "assets/images"))
plat_mkdir(p_join(root, "assets/audio"))
plat_mkdir(p_join(root, "assets/music"))
plat_mkdir(p_join(root, "assets/midi"))
proj_close()
st_set("proj_root", root)
st_set("proj_name", name)
st_set("proj_title", name)
st_set("proj_w", w)
st_set("proj_h", h)
st_set("proj_fps", 60.0)
st_set("proj_version", "0.1.0")
proj_install_runtime(root)
plat_write(p_join(root, "scripts/player.ling"), script_template("player"))
plat_write(p_join(root, "scripts/README.md"), "# scripts\n\nEach .ling file here is a script. Name a node's Script field after the file (without .ling)\nand define hooks: <name>_ready(i), <name>_update(i, dt), <name>_draw(i), <name>_input(i, key),\n<name>_trigger(i, other), <name>_hit(i, other), <name>_net(i, msg). See Help > Scripting Reference.\n")
# scenes by template
if tpl == 0.0 {
new_scene("3d")
save_scene(p_join(root, "scenes/level.lscene"))
} else if tpl == 1.0 {
new_scene("3d")
tpl_fill_level()
sc_set("hud", "scenes/hud.lscene")
save_scene(p_join(root, "scenes/level.lscene"))
new_scene("2d")
tpl_fill_hud()
save_scene(p_join(root, "scenes/hud.lscene"))
open_scene(p_join(root, "scenes/level.lscene"))
} else if tpl == 2.0 {
new_scene("3d")
tpl_fill_level()
save_scene(p_join(root, "scenes/level.lscene"))
new_scene("2d")
sc_set("name", "Main Menu")
sc_set("ex", "bg3d=scenes/level.lscene")
save_scene(p_join(root, "scenes/menu.lscene"))
st_set("proj_start", "scenes/menu.lscene")
} else if tpl == 3.0 {
new_scene("cutscene")
tpl_fill_cutscene()
save_scene(p_join(root, "scenes/intro.lscene"))
new_scene("3d")
tpl_fill_level()
save_scene(p_join(root, "scenes/level.lscene"))
open_scene(p_join(root, "scenes/intro.lscene"))
sc_set("next", "scenes/level.lscene")
save_scene(p_join(root, "scenes/intro.lscene"))
st_set("proj_start", "scenes/intro.lscene")
} else {
new_scene("3d")
tpl_fill_rhythm()
save_scene(p_join(root, "scenes/rhythm.lscene"))
}
proj_save()
gen_write_all()
recent_add(proj_file())
status("Created project " + name)
return true
}
fn tpl_fill_level() {
bind g = nd_find_name("Ground")
if g >= 0.0 { nd_set(g, "sx", 16.0) nd_set(g, "sz", 16.0) }
bind a = nd_add("actor", "Player", 0.0)
nd_set(a, "py", 0.0 - 1.0)
nd_set(a, "tag", "player")
nd_set(a, "scr", "player")
bind b = nd_add("box", "Crate", 0.0)
nd_set(b, "px", 4.0)
nd_set(b, "py", 0.0 - 1.0)
nd_set(b, "pz", 3.0)
nd_set(b, "tag", "solid")
bind b2 = nd_add("box", "Wall", 0.0)
nd_set(b2, "px", 0.0 - 6.0)
nd_set(b2, "py", 0.0 - 1.5)
nd_set(b2, "pz", 0.0 - 5.0)
nd_set(b2, "sx", 4.0)
nd_set(b2, "sy", 1.5)
nd_set(b2, "sz", 0.5)
nd_set(b2, "tag", "solid")
nd_set(b2, "col", C_INDIGO)
bind s = nd_add("sphere", "Orb", 0.0)
nd_set(s, "px", 0.0 - 3.0)
nd_set(s, "py", 0.0 - 2.5)
nd_set(s, "pz", 4.0)
nd_set(s, "sx", 0.6)
nd_set(s, "sy", 0.6)
nd_set(s, "sz", 0.6)
key_add(nd_id(s), "py", 0.0, 0.0 - 2.5, "linear")
key_add(nd_id(s), "py", 1.0, 0.0 - 3.5, "sine_in_out")
key_add(nd_id(s), "py", 2.0, 0.0 - 2.5, "sine_in_out")
key_add(nd_id(s), "ry", 0.0, 0.0, "linear")
key_add(nd_id(s), "ry", 2.0, 6.2831853, "linear")
sc_set("len", 2.0)
bind t = nd_add("trigger", "Goal", 0.0)
nd_set(t, "px", 8.0)
nd_set(t, "py", 0.0 - 1.0)
nd_set(t, "pz", 0.0 - 8.0)
nd_set(t, "tag", "goal")
nd_set(t, "txt", "say:You found the goal!")
bind c = nd_find_name("Camera")
if c >= 0.0 { nd_set(c, "b", 2.0) nd_set(c, "txt", "Player") nd_set(c, "d", 9.0) }
}
fn tpl_fill_hud() {
sc_clear()
sc_set("name", "HUD")
sc_set("kind", "2d")
sc_set("bg", 0.0)
sc_set("cw", 1280.0)
sc_set("ch", 720.0)
sc_set("fps", 30.0)
sc_set("len", 5.0)
sc_set("ex", "overlay=1")
bind hb = nd_add("bar", "Health", 0.0)
nd_set(hb, "px", 24.0)
nd_set(hb, "py", 24.0)
nd_set(hb, "w", 220.0)
nd_set(hb, "h", 18.0)
nd_set(hb, "tag", "health")
bind l = nd_add("label", "Score", 0.0)
nd_set(l, "px", 1060.0)
nd_set(l, "py", 22.0)
nd_set(l, "w", 200.0)
nd_set(l, "txt", "Score 0")
nd_set(l, "tag", "score")
nd_set(l, "a", 20.0)
bind h = nd_add("label", "Hint", 0.0)
nd_set(h, "px", 0.0)
nd_set(h, "py", 0.0 - 60.0)
nd_set(h, "w", 400.0)
nd_set(h, "txt", "WASD move Space jump Esc stop")
nd_set(h, "a", 14.0)
nd_set(h, "col", C_TEXT_DIM)
nd_set(h, "anc", "bc")
}
fn tpl_fill_cutscene() {
bind c = nd_find_name("Camera")
bind k = nd_add("box", "Hero", 0.0)
nd_set(k, "py", 0.0 - 1.0)
nd_set(k, "sx", 0.6)
nd_set(k, "sy", 1.0)
nd_set(k, "sz", 0.4)
nd_set(k, "col", C_ORANGE)
bind q = nd_add("sphere", "Spirit", 0.0)
nd_set(q, "px", 3.0)
nd_set(q, "py", 0.0 - 2.0)
nd_set(q, "sx", 0.5)
nd_set(q, "sy", 0.5)
nd_set(q, "sz", 0.5)
nd_set(q, "col", C_YELLOW)
key_add(nd_id(q), "px", 0.0, 3.0, "linear")
key_add(nd_id(q), "px", 6.0, 0.0 - 3.0, "sine_in_out")
key_add(nd_id(q), "py", 0.0, 0.0 - 2.0, "linear")
key_add(nd_id(q), "py", 3.0, 0.0 - 3.5, "sine_in_out")
key_add(nd_id(q), "py", 6.0, 0.0 - 2.0, "sine_in_out")
# clear the default clips from new_scene and lay out a short scene
while clip_count() > 0.0 { clip_del(0.0) }
sc_set("len", 16.0)
clip_add("fx", 0.0, 1.5, "fade_in", "", "", "", 0.0, "linear")
clip_add("fx", 0.0, 16.0, "letterbox", "", "", "", 0.0, "linear")
clip_add("camera", 0.0, 6.0, "orbit", "", "", "0,-1.2,0,7,0.2", nd_id(k), "linear")
clip_add("camera", 6.0, 5.0, "move", "", "", "4,-2,-6,-0.6,0.15,2;-4,-1.5,-6,0.6,0.1,2", 0.0, "cubic_in_out")
clip_add("camera", 11.0, 5.0, "follow", "", "", "0,-1,4,0.3", nd_id(q), "linear")
clip_add("dialog", 0.8, 3.5, "say", "Narrator", "Before form, there was vibration.", "", 0.0, "linear")
clip_add("dialog", 4.6, 3.2, "say", "Hero", "Who is calling from the light?", "", nd_id(k), "linear")
clip_add("dialog", 8.2, 3.6, "say", "Spirit", "The song remembers you. Follow.", "", nd_id(q), "linear")
clip_add("anim", 11.0, 4.0, "moveto", "", "", "-6,-2,8", nd_id(q), "cubic_in_out")
clip_add("fx", 14.5, 1.5, "fade_out", "", "", "", 0.0, "linear")
clip_add("event", 16.0, 0.1, "end", "", "", "", 0.0, "linear")
}
fn tpl_fill_rhythm() {
sc_set("name", "Rhythm")
bind m = nd_add("midi", "Melody", 0.0)
nd_set(m, "py", 0.0 - 3.0)
nd_set(m, "src", "assets/midi/song.mid")
nd_set(m, "b", 1.0)
nd_set(m, "scr", "rhythm")
bind a = nd_add("actor", "Player", 0.0)
nd_set(a, "py", 0.0 - 1.0)
nd_set(a, "tag", "player")
nd_set(a, "scr", "player")
plat_write(p_join(proj_root(), "scripts/rhythm.ling"), script_template("rhythm"))
}