灵符lingfu

lingdev

↓ 0 downloads

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).

Windows · x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdev
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (136)

ling/cutscene.ling
# cutscene.ling - the cutscene editor: six tracks (camera, dialog, anim, audio, fx, event) of
# clips on a shared ruler. Drag to move, drag the right edge to resize, double-click to edit,
# right-click a track to add. The viewport previews the camera track at the playhead.

fn cs_px(t, gx) { return gx + (t - st_get("tl_off", 0.0)) * st_get("tl_zoom", 60.0) }
fn cs_time(px, gx) { return st_get("tl_off", 0.0) + (px - gx) / st_get("tl_zoom", 60.0) }
fn track_col(tr) {
    if tr == "camera" { return C_BLUE }
    if tr == "dialog" { return C_YELLOW }
    if tr == "anim" { return C_ORANGE }
    if tr == "audio" { return C_INDIGO }
    if tr == "fx" { return C_VIOLET }
    return C_GREEN
}
fn clip_label(i) {
    bind k = clip_get(i, "kind")
    bind a = clip_get(i, "a")
    bind b = clip_get(i, "b")
    bind tr = clip_get(i, "track")
    if tr == "dialog" { return sel(a != "", a + ": ", "") + b }
    if tr == "camera" { return k + sel(clip_get(i, "node") > 0.0, " -> " + nd_gid(clip_get(i, "node"), "name"), "") }
    if tr == "anim" { return k + " " + nd_gid(clip_get(i, "node"), "name") }
    if tr == "audio" { return k + " " + p_base(b) }
    if tr == "event" { return k + " " + sel(k == "call", a, b) }
    return k
}

fn cutscene_panel(x, y, w, h) {
    bind lw = 110.0
    bind rulerh = 26.0
    bind rh = 30.0
    bind gx = x + lw
    bind gw = w - lw - 12.0
    bind t = st_get("tl_t", 0.0)
    bind ln = max(0.1, sc_get("len", 10.0))
    bind zoom = st_get("tl_zoom", 60.0)
    g_rect(x, y, lw, rulerh, C_BG_PANEL)
    ds_word(x + 8.0, y + 7.0, 10.0, "CUTSCENE", ds_col("bind"), true)
    g_rect(gx, y, gw + 12.0, rulerh, C_RULER)
    u_hsep(x, y + rulerh - 1.0, w)
    bind ruler_hot = u_hot(gx, y, gw, rulerh)
    if u_hot(gx, y, gw, h) && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 {
        if u_ctrl() {
            bind mt = cs_time(u_mx(), gx)
            bind zoom = clampf(zoom * pow(1.15, u_scroll()), 8.0, 2000.0)
            st_set("tl_zoom", zoom)
            st_set("tl_off", max(0.0, mt - (u_mx() - gx) / zoom))
        } else { st_set("tl_off", max(0.0, st_get("tl_off", 0.0) - u_scroll() * 30.0 / zoom)) }
        st_set("wheel_used", 1.0)
    }
    bind step = 1.0
    while step * zoom < 60.0 { bind step = step * 2.0 }
    while step * zoom > 240.0 && step > 0.125 { bind step = step / 2.0 }
    u_clip(gx, y, gw, h)
    bind tt = floor(st_get("tl_off", 0.0) / step) * step
    while cs_px(tt, gx) < gx + gw {
        bind px = cs_px(tt, gx)
        if px >= gx {
            g_rect(px, y + rulerh - 8.0, 1.0, 8.0, C_TEXT_DIM)
            g_text(px + 3.0, y + 4.0, 10.0, C_TEXT_DIM, sel(step >= 1.0, num_str(tt) + "s", fmt2(tt)))
            g_rect(px, y + rulerh, 1.0, h - rulerh, C_GRID_MAJ)
        }
        bind tt = tt + step
    }
    bind ex = cs_px(ln, gx)
    g_rect(ex, y, 2.0, h, C_LOOP_EDGE)
    g_rect_a(ex, y + rulerh, gw, h - rulerh, 0.0, 0.35)
    bind end_hot = u_hot(ex - 6.0, y, 12.0, rulerh)
    if end_hot { u_tip("Cutscene length " + fmt1(ln) + " s (drag)") }
    if end_hot && u_pressed() { u_set_active("cs_end")  undo_push() }
    if u_active() == "cs_end" {
        if u_down() { sc_set("len", max(0.2, round(cs_time(u_mx(), gx) * 10.0) / 10.0)) } else { u_set_active("") }
    }
    if ruler_hot && u_pressed() && !end_hot { u_set_active("cs_scrub") }
    if u_active() == "cs_scrub" {
        if u_down() { st_set("tl_t", clampf(round(cs_time(u_mx(), gx) * 100.0) / 100.0, 0.0, ln)) } else { u_set_active("") }
    }
    u_unclip()
    # tracks
    bind tracks = clip_tracks()
    bind top = y + rulerh
    bind tr = 0.0
    while tr < len(tracks) {
        bind name = list_get(tracks, tr)
        bind ry = top + tr * rh
        bind col = track_col(name)
        g_rect(x, ry, lw, rh, C_BG_SIDE)
        u_hsep(x, ry + rh - 1.0, w)
        g_rect(x, ry, 3.0, rh, col)
        g_text(x + 12.0, ry + 9.0, 11.0, C_TEXT, name)
        if u_hot(x + lw - 24.0, ry + 5.0, 20.0, 20.0) { u_tip("Add a " + name + " clip at the playhead") }
        if u_icon_button("cs_add_" + name, x + lw - 24.0, ry + 5.0, 20.0, 20.0, "plus", 10.0, col, false) { ctx_open("track", name, x + lw, ry + rh) }
        g_rect(gx, ry, gw, rh, sel(tr % 2.0 < 0.5, C_BG_DEEP, 0.0))
        bind lane_hot = u_hot(gx, ry, gw, rh)
        if lane_hot && u_rpressed() { st_set("cs_at", round(cs_time(u_mx(), gx) * 10.0) / 10.0)  ctx_open("track", name, u_mx(), u_my()) }
        u_clip(gx, ry, gw, rh)
        bind cl = clip_sorted(name)
        bind c = 0.0
        while c < len(cl) {
            bind ci = list_get(cl, c)
            bind c0 = clip_get(ci, "t")
            bind d = max(0.05, clip_get(ci, "d"))
            bind cx0 = cs_px(c0, gx)
            bind cw = max(6.0, d * zoom)
            bind selected = st_get("clip_sel", 0.0 - 1.0) == ci
            bind chot = lane_hot && u_in(cx0, ry + 3.0, cw, rh - 6.0)
            bind active = t >= c0 && t < c0 + d
            g_rrect_a(cx0, ry + 3.0, cw, rh - 6.0, 3.0, col, sel(selected, 0.95, sel(active, 0.75, 0.5)))
            if selected { g_rframe(cx0, ry + 3.0, cw, rh - 6.0, 3.0, C_WHITE) }
            g_text_fit(cx0 + 5.0, ry + 9.0, cw - 10.0, 11.0, sel(selected || active, C_WHITE, C_BG_DEEP), clip_label(ci))
            bind edge_hot = lane_hot && abs(u_mx() - (cx0 + cw)) < 5.0 && u_my() >= ry && u_my() < ry + rh
            if edge_hot { g_rect(cx0 + cw - 2.0, ry + 3.0, 3.0, rh - 6.0, C_WHITE) }
            if (chot || edge_hot) && u_active() == "" {
                u_tip(clip_label(ci) + "  " + fmt2(c0) + " s + " + fmt2(d) + " s  (double-click to edit)")
                if u_pressed() {
                    st_set("clip_sel", ci)
                    undo_push()
                    st_set("cd_t0", c0)
                    st_set("cd_d0", d)
                    st_set("cd_x0", u_mx())
                    if edge_hot { u_set_active("cs_resize") } else { u_set_active("cs_move") }
                    if u_double() { u_set_active("")  modal_open("clip") }
                }
                if u_rpressed() { st_set("clip_sel", ci)  ctx_open("clip", num_str(ci), u_mx(), u_my()) }
            }
            bind c = c + 1.0
        }
        u_unclip()
        bind tr = tr + 1.0
    }
    bind ci = st_get("clip_sel", 0.0 - 1.0)
    if u_active() == "cs_move" {
        if u_down() && ci >= 0.0 && ci < clip_count() { clip_set(ci, "t", max(0.0, round((st_get("cd_t0", 0.0) + (u_mx() - st_get("cd_x0", 0.0)) / zoom) * 20.0) / 20.0)) } else { u_set_active("") }
    }
    if u_active() == "cs_resize" {
        if u_down() && ci >= 0.0 && ci < clip_count() { clip_set(ci, "d", max(0.1, round((st_get("cd_d0", 1.0) + (u_mx() - st_get("cd_x0", 0.0)) / zoom) * 20.0) / 20.0)) } else { u_set_active("") }
    }
    # below the tracks: hints + preview switch
    bind by = top + len(tracks) * rh + 6.0
    if by + 20.0 < y + h {
        bind v = ds_switch("cs_preview", x + 12.0, by, 14.0, "PREVIEW CAMERA", st_get("cut_preview", 1.0) > 0.5, ds_col("call"), "Look through the cutscene camera in the viewport")
        if v >= 0.0 { st_set("cut_preview", v) }
        bind k = sc_get("kind", "3d")
        if k != "cutscene" {
            g_text(x + 250.0, by, 11.0, C_TEXT_DIM, "This is a " + k + " scene: clips still run in play mode (events, dialog, fx).")
            if u_button("cs_convert", x + w - 150.0, by - 4.0, 138.0, 22.0, "Make it a cutscene", 0.0) { do_action("scene.to_cutscene") }
        } else {
            g_text_fit(x + 250.0, by, w - 262.0, 11.0, C_TEXT_DIM, "Space plays. Right-click a track to add clips. Next scene: " + sel(sc_get("next", "") == "", "(none - set in Scene Settings)", sc_get("next", "")))
        }
    }
    # playhead
    u_clip(gx, y, gw, h)
    bind px = cs_px(t, gx)
    g_rect(px, y, 1.0, h, C_PLAYHEAD)
    g_tri(px - 6.0, y, px + 6.0, y, px, y + 8.0, C_PLAYHEAD)
    u_unclip()
    u_vsep(gx, y, h)
}

# ── clip actions ─────────────────────────────────────────────────────────────
fn clip_ctx_action(a, arg) {
    bind ci = st_get("clip_sel", 0.0 - 1.0)
    if str_starts_with(a, "clip.new:") {
        bind p = str_split(a, ":")
        bind tr = list_get(p, 1)
        bind kind = list_get(p, 2)
        undo_push()
        bind at = st_get("cs_at", st_get("tl_t", 0.0))
        st_set("cs_at", st_get("tl_t", 0.0))
        bind node = sel_main()
        bind d = 3.0
        bind c = ""
        bind b = ""
        bind aa = ""
        if tr == "camera" {
            if kind == "move" { bind c = st_get("ed_cam_pose", "0,-3,-8,0,0.25,2") + ";" + st_get("ed_cam_pose", "0,-3,-8,0,0.25,2")  bind d = 4.0 }
            else if kind == "cut" { bind c = st_get("ed_cam_pose", "0,-3,-8,0,0.25,2")  bind d = 3.0 }
            else if kind == "orbit" { bind c = "0,-1,0,8,0.25,0.35"  bind d = 6.0 }
            else { bind c = "0,-1.5,5,0.2"  bind d = 6.0 }
        } else if tr == "dialog" {
            bind aa = sel(kind == "say", "Narrator", "")
            bind b = sel(kind == "say", "New line", "Title")
            bind d = sel(kind == "say", 3.5, 2.5)
        } else if tr == "anim" {
            if kind == "moveto" {
                bind ni = nd_find(node)
                if ni >= 0.0 { bind c = fmt2(nd_get(ni, "px") + 2.0) + "," + fmt2(nd_get(ni, "py")) + "," + fmt2(nd_get(ni, "pz")) } else { bind c = "0,0,0" }
                bind d = 2.0
            } else if kind == "vis" { bind c = "0"  bind d = 1.0 }
            else { bind aa = "0"  bind d = 2.0 }
        } else if tr == "audio" { bind c = "1"  bind d = sel(kind == "music", 1.0, 0.5) }
        else if tr == "fx" { bind c = "1"  bind d = sel(kind == "letterbox", 8.0, sel(kind == "flash", 0.4, sel(kind == "shake", 0.8, 1.5))) }
        else {
            bind d = 0.2
            if kind == "scene" { bind b = sc_get("next", "") }
        }
        bind ni = clip_add(tr, at, d, kind, aa, b, c, sel(tr == "anim" || tr == "camera", node, 0.0), sel(tr == "camera" && kind == "move", "cubic_in_out", "linear"))
        st_set("clip_sel", ni)
        if sc_get("len", 10.0) < at + d { sc_set("len", at + d) }
        modal_open("clip")
        return 0.0
    }
    if ci < 0.0 || ci >= clip_count() { return 0.0 }
    if a == "clip.edit" { modal_open("clip") }
    else if a == "clip.delete" { undo_push()  clip_del(ci)  st_set("clip_sel", 0.0 - 1.0) }
    else if a == "clip.dup" {
        undo_push()
        bind ni = clip_add(clip_get(ci, "track"), clip_get(ci, "t") + clip_get(ci, "d"), clip_get(ci, "d"), clip_get(ci, "kind"), clip_get(ci, "a"), clip_get(ci, "b"), clip_get(ci, "c"), clip_get(ci, "node"), clip_get(ci, "e"))
        st_set("clip_sel", ni)
    }
    else if a == "clip.split" {
        bind t = st_get("tl_t", 0.0)
        bind c0 = clip_get(ci, "t")
        bind d = clip_get(ci, "d")
        if t > c0 + 0.05 && t < c0 + d - 0.05 {
            undo_push()
            clip_set(ci, "d", t - c0)
            clip_add(clip_get(ci, "track"), t, c0 + d - t, clip_get(ci, "kind"), clip_get(ci, "a"), clip_get(ci, "b"), clip_get(ci, "c"), clip_get(ci, "node"), clip_get(ci, "e"))
        }
    }
    return 0.0
}
fn clip_text_submit(v) {
    bind ci = st_get("clip_sel", 0.0 - 1.0)
    if ci < 0.0 || ci >= clip_count() { return 0.0 }
    undo_push()
    clip_set(ci, st_get("clip_field", "b"), v)
    modal_open("clip")
    return 0.0
}

# ── clip editor window ───────────────────────────────────────────────────────
fn modal_clip() {
    bind ci = st_get("clip_sel", 0.0 - 1.0)
    if ci < 0.0 || ci >= clip_count() { modal_close()  return 0.0 }
    bind tr = clip_get(ci, "track")
    win_begin("Clip - " + tr, 600.0, 440.0)
    bind x = st_get("wx", 0.0)
    bind y = st_get("wy", 0.0)
    bind yy = y + 6.0
    bind kinds = []
    bind kn = []
    if tr == "camera" { bind kinds = ["move", "cut", "orbit", "follow"]  bind kn = ["move (travel between two poses)", "cut (jump to a pose)", "orbit a target", "follow a node"] }
    else if tr == "dialog" { bind kinds = ["say", "title"]  bind kn = ["say (subtitle + voice)", "title card"] }
    else if tr == "anim" { bind kinds = ["moveto", "vis", "clip"]  bind kn = ["move node to", "show / hide node", "play model clip"] }
    else if tr == "audio" { bind kinds = ["play", "music", "stop"]  bind kn = ["play a sound", "play music", "stop music"] }
    else if tr == "fx" { bind kinds = ["fade_in", "fade_out", "letterbox", "flash", "shake"]  bind kn = ["fade in", "fade out", "letterbox", "flash", "shake"] }
    else { bind kinds = ["scene", "call", "wait", "end"]  bind kn = ["go to scene", "call script hook", "wait for input", "end cutscene"] }
    g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Kind")
    bind ki = u_combo("clip_kind", x + 110.0, yy, 300.0, 26.0, kn, max(0.0, l_index(kinds, clip_get(ci, "kind"))))
    if ki >= 0.0 { undo_push()  clip_set(ci, "kind", list_get(kinds, ki)) }
    bind yy = yy + 34.0
    g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Start / length")
    bind nv = u_dragval("clip_t", x + 110.0, yy, 90.0, 26.0, clip_get(ci, "t"), 0.0, 3600.0, 0.02, fmt2(clip_get(ci, "t")) + " s")
    if u_has(nv) { clip_set(ci, "t", round(nv * 100.0) / 100.0) }
    bind nv = u_dragval("clip_d", x + 206.0, yy, 90.0, 26.0, clip_get(ci, "d"), 0.05, 3600.0, 0.02, fmt2(clip_get(ci, "d")) + " s")
    if u_has(nv) { clip_set(ci, "d", round(nv * 100.0) / 100.0) }
    g_text(x + 306.0, yy + 6.0, 12.0, C_TEXT, "Ease")
    bind en = ease_names()
    bind ei = u_combo("clip_e", x + 346.0, yy, 170.0, 26.0, en, max(0.0, l_index(en, clip_get(ci, "e"))))
    if ei >= 0.0 { clip_set(ci, "e", list_get(en, ei)) }
    bind yy = yy + 34.0
    bind kind = clip_get(ci, "kind")
    # node pick
    if tr == "camera" || tr == "anim" || tr == "dialog" {
        g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Node")
        bind names = ["(none)"]
        bind ids = [0.0]
        bind cur = 0.0
        bind i = 0.0
        while i < nd_count() {
            if !nd_is_2d(nd_get(i, "kind")) {
                bind names = list_push(names, nd_get(i, "name"))
                bind ids = list_push(ids, nd_id(i))
                if nd_id(i) == clip_get(ci, "node") { bind cur = len(ids) - 1.0 }
            }
            bind i = i + 1.0
        }
        bind pi = u_combo("clip_node", x + 110.0, yy, 300.0, 26.0, names, cur)
        if pi >= 0.0 { clip_set(ci, "node", list_get(ids, pi)) }
        bind yy = yy + 34.0
    }
    if tr == "dialog" {
        g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Speaker")
        bind sv = u_textfield("clip_a", x + 110.0, yy, 300.0, 26.0, clip_get(ci, "a"))
        if sv != clip_get(ci, "a") { clip_set(ci, "a", sv) }
        bind yy = yy + 34.0
        g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Line")
        bind bv = u_textfield("clip_b", x + 110.0, yy, 400.0, 26.0, clip_get(ci, "b"))
        if bv != clip_get(ci, "b") { clip_set(ci, "b", bv) }
        if u_button("clip_b_big", x + 516.0, yy, 60.0, 26.0, "Edit", 0.0) { st_set("clip_field", "b")  modal_text("Dialog line", "clip_text", clip_get(ci, "b")) }
        bind yy = yy + 34.0
        g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Voice file")
        bind cv = u_textfield("clip_c", x + 110.0, yy, 340.0, 26.0, clip_get(ci, "c"))
        if cv != clip_get(ci, "c") { clip_set(ci, "c", cv) }
        if u_button("clip_c_pick", x + 456.0, yy, 60.0, 26.0, "Pick", 0.0) { st_set("clip_field", "c")  picker_open("open", "Voice-over file", "wav,ogg,mp3,flac", "clip.set_file", st_get("proj_source", p_join(proj_root(), "assets")), "") }
        if u_button("clip_c_dur", x + 520.0, yy, 56.0, 26.0, "Fit", 0.0) {
            if plat_has_audio() && clip_get(ci, "c") != "" {
                bind sid = plat_sample_load(cut_path(clip_get(ci, "c")))
                bind dur = plat_sample_duration(sid)
                if dur > 0.05 { clip_set(ci, "d", round(dur * 100.0) / 100.0 + 0.3)  status("Length set from the voice file: " + fmt2(dur) + " s") }
                else { status("Could not read the voice file") }
            } else { status("Voice durations need the desktop runtime") }
        }
        bind yy = yy + 34.0
    } else if tr == "camera" {
        if kind == "move" || kind == "cut" {
            g_text_wrap(x + 16.0, yy, 560.0, 11.0, C_TEXT_DIM, "Poses are x,y,z,yaw,pitch,zdist. A move travels from the first pose to the second with the ease above.")
            bind yy = yy + 30.0
            bind poses = str_split(clip_get(ci, "c"), ";")
            bind p0 = list_get(poses, 0)
            bind p1 = sel(len(poses) > 1.0, list_get(poses, 1), p0)
            g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "From")
            bind v0 = u_textfield("clip_p0", x + 110.0, yy, 300.0, 26.0, p0)
            if u_button("clip_p0_cam", x + 416.0, yy, 160.0, 26.0, "Use editor camera", 0.0) { bind v0 = st_get("ed_cam_pose", p0) }
            bind yy = yy + 34.0
            bind v1 = p1
            if kind == "move" {
                g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "To")
                bind v1 = u_textfield("clip_p1", x + 110.0, yy, 300.0, 26.0, p1)
                if u_button("clip_p1_cam", x + 416.0, yy, 160.0, 26.0, "Use editor camera", 0.0) { bind v1 = st_get("ed_cam_pose", p1) }
                bind yy = yy + 34.0
            }
            bind nc = sel(kind == "move", v0 + ";" + v1, v0)
            if nc != clip_get(ci, "c") { clip_set(ci, "c", nc) }
            g_text(x + 16.0, yy, 11.0, C_TEXT_DIM, "Tip: switch off Preview Camera, orbit the view, then press Use editor camera.")
            bind yy = yy + 24.0
        } else if kind == "orbit" {
            bind prm = nums_parse(clip_get(ci, "c"))
            g_text_wrap(x + 16.0, yy, 560.0, 11.0, C_TEXT_DIM, "Orbits the node (or the point tx,ty,tz) at a distance; turns = revolutions over the clip.")
            bind yy = yy + 30.0
            bind tx = nums_get(prm, 0, 0.0)  bind ty = nums_get(prm, 1, 0.0 - 1.0)  bind tz = nums_get(prm, 2, 0.0)
            bind dist = nums_get(prm, 3, 8.0)  bind pitch = nums_get(prm, 4, 0.25)  bind turns = nums_get(prm, 5, 0.35)
            bind a1 = u_dragval("co_tx", x + 16.0, yy, 80.0, 26.0, tx, 0.0 - 1000.0, 1000.0, 0.02, "x " + fmt1(tx))
            bind a2 = u_dragval("co_ty", x + 100.0, yy, 80.0, 26.0, ty, 0.0 - 1000.0, 1000.0, 0.02, "y " + fmt1(ty))
            bind a3 = u_dragval("co_tz", x + 184.0, yy, 80.0, 26.0, tz, 0.0 - 1000.0, 1000.0, 0.02, "z " + fmt1(tz))
            bind a4 = u_dragval("co_d", x + 268.0, yy, 90.0, 26.0, dist, 0.5, 500.0, 0.05, "dist " + fmt1(dist))
            bind a5 = u_dragval("co_p", x + 362.0, yy, 90.0, 26.0, pitch, 0.0 - 1.5, 1.5, 0.005, "pitch " + fmt2(pitch))
            bind a6 = u_dragval("co_r", x + 456.0, yy, 90.0, 26.0, turns, 0.0 - 5.0, 5.0, 0.005, "turns " + fmt2(turns))
            if u_has(a1) { bind tx = a1 }
            if u_has(a2) { bind ty = a2 }
            if u_has(a3) { bind tz = a3 }
            if u_has(a4) { bind dist = a4 }
            if u_has(a5) { bind pitch = a5 }
            if u_has(a6) { bind turns = a6 }
            if u_has(a1) || u_has(a2) || u_has(a3) || u_has(a4) || u_has(a5) || u_has(a6) { clip_set(ci, "c", nums_text([tx, ty, tz, dist, pitch, turns])) }
            bind yy = yy + 34.0
        } else {
            bind prm = nums_parse(clip_get(ci, "c"))
            g_text_wrap(x + 16.0, yy, 560.0, 11.0, C_TEXT_DIM, "Follows the node from behind: side / height / distance offsets and the pitch.")
            bind yy = yy + 30.0
            bind ox = nums_get(prm, 0, 0.0)  bind oy = nums_get(prm, 1, 0.0 - 1.5)  bind oz = nums_get(prm, 2, 5.0)  bind pitch = nums_get(prm, 3, 0.2)
            bind a1 = u_dragval("cf_x", x + 16.0, yy, 90.0, 26.0, ox, 0.0 - 50.0, 50.0, 0.02, "side " + fmt1(ox))
            bind a2 = u_dragval("cf_y", x + 110.0, yy, 90.0, 26.0, oy, 0.0 - 50.0, 50.0, 0.02, "height " + fmt1(oy))
            bind a3 = u_dragval("cf_z", x + 204.0, yy, 90.0, 26.0, oz, 0.1, 100.0, 0.02, "back " + fmt1(oz))
            bind a4 = u_dragval("cf_p", x + 298.0, yy, 90.0, 26.0, pitch, 0.0 - 1.5, 1.5, 0.005, "pitch " + fmt2(pitch))
            if u_has(a1) { bind ox = a1 }
            if u_has(a2) { bind oy = a2 }
            if u_has(a3) { bind oz = a3 }
            if u_has(a4) { bind pitch = a4 }
            if u_has(a1) || u_has(a2) || u_has(a3) || u_has(a4) { clip_set(ci, "c", nums_text([ox, oy, oz, pitch])) }
            bind yy = yy + 34.0
        }
    } else if tr == "anim" {
        if kind == "moveto" {
            bind prm = nums_parse(clip_get(ci, "c"))
            bind px = nums_get(prm, 0, 0.0)  bind py = nums_get(prm, 1, 0.0)  bind pz = nums_get(prm, 2, 0.0)
            g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Target")
            bind a1 = u_dragval("cm_x", x + 110.0, yy, 90.0, 26.0, px, 0.0 - 10000.0, 10000.0, 0.02, "x " + fmt1(px))
            bind a2 = u_dragval("cm_y", x + 204.0, yy, 90.0, 26.0, py, 0.0 - 10000.0, 10000.0, 0.02, "y " + fmt1(py))
            bind a3 = u_dragval("cm_z", x + 298.0, yy, 90.0, 26.0, pz, 0.0 - 10000.0, 10000.0, 0.02, "z " + fmt1(pz))
            if u_has(a1) { bind px = a1 }
            if u_has(a2) { bind py = a2 }
            if u_has(a3) { bind pz = a3 }
            if u_has(a1) || u_has(a2) || u_has(a3) { clip_set(ci, "c", nums_text([px, py, pz])) }
            if u_button("cm_here", x + 396.0, yy, 150.0, 26.0, "From node position", 0.0) {
                bind ni = nd_find(clip_get(ci, "node"))
                if ni >= 0.0 { clip_set(ci, "c", nums_text([nd_get(ni, "px"), nd_get(ni, "py"), nd_get(ni, "pz")])) }
            }
            bind yy = yy + 34.0
        } else if kind == "vis" {
            bind v = ds_switch("cv_on", x + 16.0, yy, 16.0, "VISIBLE", to_number(clip_get(ci, "c")) > 0.5, ds_col("ctl"), "")
            if v >= 0.0 { clip_set(ci, "c", num_str(v)) }
            bind yy = yy + 30.0
        } else {
            g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Clip index")
            bind nv = u_dragval("ca_idx", x + 110.0, yy, 90.0, 26.0, to_number(clip_get(ci, "a")), 0.0, 64.0, 0.05, num_str(floor(to_number(clip_get(ci, "a")))))
            if u_has(nv) { clip_set(ci, "a", num_str(floor(nv))) }
            bind yy = yy + 34.0
        }
    } else if tr == "audio" {
        if kind != "stop" {
            g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "File")
            bind bv = u_textfield("clip_b", x + 110.0, yy, 340.0, 26.0, clip_get(ci, "b"))
            if bv != clip_get(ci, "b") { clip_set(ci, "b", bv) }
            if u_button("clip_b_pick", x + 456.0, yy, 60.0, 26.0, "Pick", 0.0) { st_set("clip_field", "b")  picker_open("open", "Sound file", "wav,ogg,mp3,flac", "clip.set_file", st_get("proj_source", p_join(proj_root(), "assets")), "") }
            bind yy = yy + 34.0
            bind vol = to_number(clip_get(ci, "c"))
            if vol <= 0.0 { bind vol = 1.0 }
            g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Volume")
            bind nv = u_hslider("clip_vol", x + 110.0, yy + 2.0, 200.0, 22.0, vol, 0.0, 1.0, C_INDIGO)
            if u_has(nv) { clip_set(ci, "c", fmt2(nv)) }
            bind yy = yy + 34.0
        }
    } else if tr == "fx" {
        bind s = to_number(clip_get(ci, "c"))
        if s <= 0.0 { bind s = 1.0 }
        g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Strength")
        bind nv = u_hslider("clip_fx", x + 110.0, yy + 2.0, 200.0, 22.0, s, 0.05, 1.0, C_VIOLET)
        if u_has(nv) { clip_set(ci, "c", fmt2(nv)) }
        bind yy = yy + 34.0
    } else {
        if kind == "scene" {
            g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Scene")
            bind bv = u_textfield("clip_b", x + 110.0, yy, 340.0, 26.0, clip_get(ci, "b"))
            if bv != clip_get(ci, "b") { clip_set(ci, "b", bv) }
            if u_button("clip_scene_pick", x + 456.0, yy, 60.0, 26.0, "Pick", 0.0) { st_set("clip_field", "b")  picker_open("open", "Scene to go to", "lscene,lcut", "clip.set_file", p_join(proj_root(), "scenes"), "") }
            bind yy = yy + 34.0
        } else if kind == "call" {
            g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Hook arg")
            bind av = u_textfield("clip_a", x + 110.0, yy, 340.0, 26.0, clip_get(ci, "a"))
            if av != clip_get(ci, "a") { clip_set(ci, "a", av) }
            g_text(x + 16.0, yy + 34.0, 11.0, C_TEXT_DIM, "Every scripted node gets <script>_trigger(i, \"" + clip_get(ci, "a") + "\").")
            bind yy = yy + 60.0
        } else { g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT_DIM, sel(kind == "end", "Ends the cutscene: goes to the scene's Next scene (Scene Settings) or back where it came from.", "Pauses the cutscene until a key is pressed."))  bind yy = yy + 34.0 }
    }
    bind by = y + 440.0 - 76.0
    if u_button("clip_ok", x + 16.0, by, 100.0, 26.0, "Close", C_BLUE) { modal_close() }
    if u_button("clip_del", x + 124.0, by, 100.0, 26.0, "Delete", C_RED) { undo_push()  clip_del(ci)  st_set("clip_sel", 0.0 - 1.0)  modal_close() }
    if u_button("clip_go", x + 232.0, by, 150.0, 26.0, "Playhead to clip", 0.0) { st_set("tl_t", clip_get(ci, "t")) }
    return 0.0
}

← Back to index