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_story.ling
# rt_story.ling - "story" mode: a kawaii pop-up storybook platformer in the spirit of Yoshi's Story.
# A scene with `mode=story` is a page. Box nodes tagged `ground` are solid, `cloud` are one-way
# platforms, `eggblock` give eggs when bumped, `spring` bounce, `fruit` (ex kind=apple|banana|
# grapes|melon|heart) are eaten, `puff` (ex kind=walker|flyer|spiky) are the enemies, `flower`,
# `tree` and `cloudface` are animated scenery and the `player` spawn is where Moko starts.
#
# Moko runs, jumps (hold for height), flutters (hold jump while falling), ground-pounds (down in the
# air), licks with the tongue (eats fruit, swallows puffs into eggs), and throws eggs (up to six
# follow behind). Eat 30 fruits to clear the page; the page's lucky fruit counts three. The smile
# flower's eight petals are health; hearts give two back. The world bobs to the music's beat.
fn story_on() { return st_get("sy_on", 0.0) > 0.5 }
# ── palette ──────────────────────────────────────────────────────────────────
fn sy_c(name) {
if name == "moko" { return c_rgb(255.0, 168.0, 196.0) }
if name == "moko_dark" { return c_rgb(232.0, 118.0, 158.0) }
if name == "belly" { return c_rgb(255.0, 246.0, 236.0) }
if name == "saddle" { return c_rgb(120.0, 214.0, 190.0) }
if name == "shoe" { return c_rgb(255.0, 196.0, 110.0) }
if name == "cheek" { return c_rgb(255.0, 120.0, 150.0) }
if name == "eye" { return c_rgb(40.0, 30.0, 52.0) }
if name == "white" { return c_rgb(255.0, 255.0, 255.0) }
if name == "tongue" { return c_rgb(255.0, 96.0, 140.0) }
return c_rgb(255.0, 255.0, 255.0)
}
fn sy_fruit_kinds() { return ["apple", "banana", "grapes", "melon", "heart"] }
fn sy_fruit_code(k) {
bind i = l_index(sy_fruit_kinds(), k)
if i < 0.0 { return 0.0 }
return i
}
fn sy_fruit_name(code) { return list_get(["Apple", "Banana", "Grapes", "Melon", "Heart"], clampf(code, 0.0, 4.0)) }
# ── sound ────────────────────────────────────────────────────────────────────
fn sy_sfx_names() { return ["jump", "flutter", "tongue", "nom", "gulp", "egg", "throw", "pop", "pound", "chime", "sparkle", "hurt", "spring", "bonk", "poof", "page", "blip", "heart", "clear"] }
fn sy_sfx_load() {
if st_get("sy_sfx_loaded", 0.0) > 0.5 || !plat_has_audio() { return 0.0 }
bind names = sy_sfx_names()
bind i = 0.0
while i < len(names) {
st_set("sy_sfx_" + list_get(names, i), plat_sample_load(game_path("assets/sfx/" + list_get(names, i) + ".wav")))
bind i = i + 1.0
}
st_set("sy_sfx_loaded", 1.0)
return 0.0
}
fn sy_sfx(name, vol, pitch) {
if !plat_has_audio() { return 0.0 }
bind id = st_get("sy_sfx_" + name, 0.0 - 1.0)
if id >= 0.0 { plat_sample_play_pitch(id, vol, pitch) }
return 0.0
}
# ── enter / leave ────────────────────────────────────────────────────────────
fn sy_lists() { return ["sd_x", "sd_y", "sd_hw", "sd_hh", "sd_k", "sd_n", "sd_bump", "sd_charge", "fr_x", "fr_y", "fr_k", "fr_ph", "fr_on", "en_x", "en_y", "en_vx", "en_vy", "en_k", "en_x0", "en_y0", "en_on", "en_t", "eg_x", "eg_y", "eg_vx", "eg_vy", "eg_t", "eg_b", "hx", "hy", "dc_x", "dc_y", "dc_z", "dc_k", "dc_s", "dc_c", "pp_x", "pp_y", "pp_t", "pp_txt", "pp_c"] }
fn story_enter() {
bind keys = sy_lists()
bind i = 0.0
while i < len(keys) {
st_lclear(list_get(keys, i))
bind i = i + 1.0
}
st_set("sy_on", 1.0)
sy_sfx_load()
plat_pfx_clear()
plat_pfx_max(3000.0)
st_set("sy_page", to_number(sc_ex("page", "1")))
st_set("sy_title", sc_ex("title", sc_get("name", "")))
st_set("sy_lucky", sy_fruit_code(sc_ex("lucky", "grapes")))
st_set("sy_goal", to_number(sc_ex("goal", "30")))
st_set("sy_bpm", to_number(sc_ex("bpm", "100")))
st_set("sy_floor_death", to_number(sc_ex("death", "12")))
bk_beats_load(sc_ex("beats", ""))
st_set("bk_last_pos", 0.0)
# world from the scene's nodes
bind n = nd_count()
bind sx = 0.0
bind sy = 0.0 - 2.0
bind i = 0.0
while i < n {
bind tag = nd_get(i, "tag")
if tag == "ground" || tag == "cloud" || tag == "eggblock" || tag == "spring" {
st_lpush("sd_x", nd_get(i, "px"))
st_lpush("sd_y", nd_get(i, "py"))
st_lpush("sd_hw", nd_get(i, "sx"))
st_lpush("sd_hh", nd_get(i, "sy"))
st_lpush("sd_k", l_index(["ground", "cloud", "eggblock", "spring"], tag))
st_lpush("sd_n", i)
st_lpush("sd_bump", 0.0)
st_lpush("sd_charge", to_number(nd_ex(i, "eggs", "3")))
} else if tag == "fruit" {
st_lpush("fr_x", nd_get(i, "px"))
st_lpush("fr_y", nd_get(i, "py"))
st_lpush("fr_k", sy_fruit_code(nd_ex(i, "kind", "apple")))
st_lpush("fr_ph", rand() * 6.28)
st_lpush("fr_on", 1.0)
} else if tag == "puff" {
st_lpush("en_x", nd_get(i, "px"))
st_lpush("en_y", nd_get(i, "py"))
st_lpush("en_vx", sel(rand() < 0.5, 0.0 - 1.6, 1.6))
st_lpush("en_vy", 0.0)
st_lpush("en_k", l_index(["walker", "flyer", "spiky"], nd_ex(i, "kind", "walker")))
st_lpush("en_x0", nd_get(i, "px"))
st_lpush("en_y0", nd_get(i, "py"))
st_lpush("en_on", 1.0)
st_lpush("en_t", rand() * 6.28)
} else if tag == "flower" || tag == "tree" || tag == "cloudface" {
st_lpush("dc_x", nd_get(i, "px"))
st_lpush("dc_y", nd_get(i, "py"))
st_lpush("dc_z", nd_get(i, "pz"))
st_lpush("dc_k", l_index(["flower", "tree", "cloudface"], tag))
st_lpush("dc_s", nd_get(i, "sx"))
st_lpush("dc_c", nd_get(i, "col"))
} else if tag == "player" {
bind sx = nd_get(i, "px")
bind sy = nd_get(i, "py")
}
bind i = i + 1.0
}
st_set("sy_px", sx) st_set("sy_py", sy) st_set("sy_vx", 0.0) st_set("sy_vy", 0.0)
st_set("sy_safe_x", sx) st_set("sy_safe_y", sy)
st_set("sy_face", 1.0)
st_set("sy_ground", 0.0)
st_set("sy_coyote", 0.0)
st_set("sy_buffer", 0.0)
st_set("sy_flutter", 0.0)
st_set("sy_flutter_fuel", 1.0)
st_set("sy_pound", 0.0)
st_set("sy_tongue", 0.0)
st_set("sy_full", 0.0)
st_set("sy_eggs", 0.0)
st_set("sy_petals", 8.0)
st_set("sy_inv", 0.0)
st_set("sy_fruits", 0.0)
st_set("sy_melons", 0.0)
st_set("sy_combo_note", 0.0)
st_set("sy_squash", 0.0)
st_set("sy_state", "intro")
st_set("sy_state_t", 0.0)
st_set("sy_time", 0.0)
st_set("sy_shake", 0.0)
st_set("sy_cam_x", sx)
st_set("sy_cam_y", sy)
bind k = 0.0
while k < 60.0 {
st_lpush("hx", sx)
st_lpush("hy", sy)
bind k = k + 1.0
}
plat_post(1.0, 1.0, 1.4, c_rgb(92.0, 54.0, 70.0), 0.1)
plat_outline_threshold(0.6)
return 0.0
}
fn story_leave() {
if story_on() {
plat_post(0.0, 0.0, 0.0, 0.0, 0.0)
plat_pfx_clear()
}
st_set("sy_on", 0.0)
return 0.0
}
# ── collision ────────────────────────────────────────────────────────────────
# the solid overlapping box (x, y, hw, hh) -> index or -1; one-way clouds only when `feet_prev`
# was above their top and the mover is falling
fn sy_hit_solid(x, y, hw, hh, vy, feet_prev) {
bind n = st_llen("sd_x")
bind i = 0.0
while i < n {
bind sx = st_lget("sd_x", i)
bind sw = st_lget("sd_hw", i)
if abs(x - sx) < hw + sw {
bind sy = st_lget("sd_y", i)
bind sh = st_lget("sd_hh", i)
if abs(y - sy) < hh + sh {
if st_lget("sd_k", i) == 1.0 {
if vy >= 0.0 && feet_prev <= sy - sh + 0.06 { return i }
} else { return i }
}
}
bind i = i + 1.0
}
return 0.0 - 1.0
}
# is there ground just under the point (x, y)?
fn sy_ground_at(x, y) {
return sy_hit_solid(x, y + 0.15, 0.05, 0.1, 1.0, y - 0.05) >= 0.0
}
# ── effects ──────────────────────────────────────────────────────────────────
fn sy_popup(x, y, txt, c) {
st_lpush("pp_x", x) st_lpush("pp_y", y) st_lpush("pp_t", 0.0) st_lpush("pp_txt", txt) st_lpush("pp_c", c)
return 0.0
}
fn sy_hearts(x, y, n) {
plat_pfx_burst(x, y, 0.0 - 0.3, n, 3.5, 0.9, 0.55, 0.1, c_rgb(255.0, 150.0, 190.0), c_rgb(255.0, 230.0, 240.0), 0.0, 0.0 - 0.4, 0.0, 0.0 - 2.5, 0.0)
return 0.0
}
fn sy_stars(x, y, n, c) {
plat_pfx_burst(x, y, 0.0 - 0.3, n, 6.0, 0.7, 0.4, 0.0, c, c_rgb(255.0, 255.0, 255.0), 1.0, 0.6, 0.0, 0.0 - 2.0, 0.0)
return 0.0
}
fn sy_dust(x, y) {
plat_pfx_burst(x, y, 0.0 - 0.2, 8.0, 2.2, 0.45, 0.45, 0.8, c_rgb(255.0, 250.0, 235.0), c_rgb(240.0, 228.0, 210.0), 0.0, 0.0 - 0.3, 0.0, 0.0 - 0.6, 0.0)
return 0.0
}
# ── eating ───────────────────────────────────────────────────────────────────
fn sy_eat_fruit(i) {
st_lset("fr_on", i, 0.0)
bind k = st_lget("fr_k", i)
bind x = st_lget("fr_x", i)
bind y = st_lget("fr_y", i)
if k == 4.0 {
st_set("sy_petals", min(8.0, st_get("sy_petals", 8.0) + 2.0))
sy_sfx("heart", 0.8, 1.0)
sy_hearts(x, y, 14.0)
sy_popup(x, y - 0.8, "+2 petals", c_rgb(255.0, 110.0, 160.0))
return 0.0
}
bind worth = 1.0
if k == st_get("sy_lucky", 2.0) { bind worth = 3.0 }
if k == 3.0 { st_set("sy_melons", st_get("sy_melons", 0.0) + 1.0) bind worth = max(worth, 3.0) }
st_set("sy_fruits", st_get("sy_fruits", 0.0) + worth)
# the chime climbs a major scale as Moko keeps eating
bind note = st_get("sy_combo_note", 0.0)
bind scale = [0.0, 2.0, 4.0, 5.0, 7.0, 9.0, 11.0, 12.0, 14.0, 16.0]
bind semi = list_get(scale, note - floor(note / 10.0) * 10.0)
sy_sfx("chime", 0.5, pow(2.0, semi / 12.0) * 0.5)
sy_sfx("nom", 0.7, 0.95 + rand() * 0.15)
st_set("sy_combo_note", note + 1.0)
st_set("sy_combo_t", 1.2)
st_set("sy_full", 0.25)
sy_hearts(x, y, 6.0)
if worth > 1.0 {
sy_sfx("sparkle", 0.7, 1.0)
sy_stars(x, y, 18.0, c_rgb(255.0, 230.0, 120.0))
sy_popup(x, y - 0.8, sel(k == 3.0, "Melon! +3", "Lucky! +3"), c_rgb(255.0, 170.0, 60.0))
}
if st_get("sy_fruits", 0.0) >= st_get("sy_goal", 30.0) && st_get("sy_state", "") == "play" {
st_set("sy_state", "clear")
st_set("sy_state_t", 0.0)
sy_sfx("clear", 0.9, 1.0)
}
return 0.0
}
fn sy_add_egg() {
if st_get("sy_eggs", 0.0) < 6.0 {
st_set("sy_eggs", st_get("sy_eggs", 0.0) + 1.0)
sy_sfx("egg", 0.7, 1.0 + st_get("sy_eggs", 0.0) * 0.05)
}
return 0.0
}
fn sy_kill_enemy(i, why) {
st_lset("en_on", i, 0.0)
bind x = st_lget("en_x", i)
bind y = st_lget("en_y", i)
plat_pfx_burst(x, y, 0.0 - 0.3, 16.0, 3.0, 0.6, 0.7, 0.1, c_rgb(255.0, 255.0, 255.0), c_rgb(230.0, 230.0, 255.0), 0.0, 0.0 - 0.5, 0.0, 0.0 - 1.0, 0.0)
sy_sfx("poof", 0.7, 0.95 + rand() * 0.2)
if why == "eat" { sy_sfx("gulp", 0.8, 1.0) sy_add_egg() st_set("sy_full", 0.4) }
return 0.0
}
fn sy_hurt(from_x) {
if st_get("sy_inv", 0.0) > 0.0 { return 0.0 }
st_set("sy_petals", st_get("sy_petals", 8.0) - 1.0)
st_set("sy_inv", 1.6)
st_set("sy_vx", sel(st_get("sy_px", 0.0) < from_x, 0.0 - 7.0, 7.0))
st_set("sy_vy", 0.0 - 8.0)
st_set("sy_shake", 0.3)
sy_sfx("hurt", 0.8, 1.0)
sy_stars(st_get("sy_px", 0.0), st_get("sy_py", 0.0) - 0.4, 10.0, c_rgb(255.0, 200.0, 220.0))
if st_get("sy_petals", 0.0) <= 0.0 {
st_set("sy_state", "oops")
st_set("sy_state_t", 0.0)
}
return 0.0
}
fn sy_bump_block(i) {
st_lset("sd_bump", i, 0.25)
if st_lget("sd_charge", i) > 0.0 {
st_lset("sd_charge", i, st_lget("sd_charge", i) - 1.0)
sy_add_egg()
sy_sfx("bonk", 0.7, 1.2)
sy_stars(st_lget("sd_x", i), st_lget("sd_y", i) - st_lget("sd_hh", i) - 0.4, 10.0, c_rgb(255.0, 240.0, 140.0))
} else { sy_sfx("bonk", 0.5, 0.8) }
return 0.0
}
# ── input ────────────────────────────────────────────────────────────────────
fn sy_edge(name, now) {
bind was = st_get("sy_in_" + name, 0.0) > 0.5
st_set("sy_in_" + name, sel(now, 1.0, 0.0))
return now && !was
}
# ── update ───────────────────────────────────────────────────────────────────
fn story_update(dt) {
if !story_on() { return 0.0 }
bind dt = min(dt, 0.033)
bind t = st_get("game_t", 0.0)
if key_pressed("p") || (key_pressed("escape") && st_get("game_embedded", 0.0) < 0.5) {
st_set("sy_paused", sel(st_get("sy_paused", 0.0) > 0.5, 0.0, 1.0))
sy_sfx("blip", 0.5, 1.0)
}
if st_get("sy_paused", 0.0) > 0.5 {
if key_pressed("q") { story_leave() game_goto(sc_ex("menu", "scenes/pages.lscene")) }
return 0.0
}
bind st = st_get("sy_state", "play")
st_set("sy_state_t", st_get("sy_state_t", 0.0) + dt)
bind stt = st_get("sy_state_t", 0.0)
# beat
bind pos = 0.0
if plat_has_audio() { bind pos = plat_music_pos() }
bind beat = bk_beat_hits("kick", pos) + bk_beat_hits("snare", pos)
st_set("bk_last_pos", pos)
if st_llen("bt_kick") < 1.0 {
# no MIDI: a steady bob at the page's tempo
bind spb = 60.0 / max(40.0, st_get("sy_bpm", 100.0))
if floor(t / spb) != floor((t - dt) / spb) { bind beat = 1.0 }
}
if beat > 0.0 { st_set("sy_beat", 1.0) }
st_set("sy_beat", max(0.0, st_get("sy_beat", 0.0) - dt * 5.0))
# timers
bind tl = ["sy_inv", "sy_full", "sy_tongue_cd", "sy_combo_t", "sy_shake", "sy_squash_t"]
bind i = 0.0
while i < len(tl) {
st_set(list_get(tl, i), max(0.0, st_get(list_get(tl, i), 0.0) - dt))
bind i = i + 1.0
}
if st_get("sy_combo_t", 0.0) <= 0.0 { st_set("sy_combo_note", 0.0) }
if st == "intro" {
if stt > 2.2 || (stt > 0.5 && (key_pressed("space") || key_pressed("enter") || plat_gamepad_button(0.0))) { st_set("sy_state", "play") st_set("sy_state_t", 0.0) bind st = "play" }
}
if st == "play" {
st_set("sy_time", st_get("sy_time", 0.0) + dt)
sy_player_step(dt)
}
if st == "clear" {
if floor(stt * 4.0) != floor((stt - dt) * 4.0) && stt < 2.5 { sy_stars(st_get("sy_px", 0.0) + (rand() - 0.5) * 6.0, st_get("sy_py", 0.0) - 2.0 - rand() * 3.0, 24.0, list_get([c_rgb(255.0, 170.0, 200.0), c_rgb(150.0, 220.0, 255.0), c_rgb(255.0, 230.0, 120.0), c_rgb(170.0, 240.0, 180.0)], floor(rand() * 3.99))) }
if stt > 3.6 { sy_page_done() }
}
if st == "oops" {
if stt > 2.0 {
story_leave()
game_goto(sc_get("path", ""))
return 0.0
}
}
sy_enemies_step(dt)
sy_eggs_step(dt)
# popups
bind i = st_llen("pp_t") - 1.0
while i >= 0.0 {
bind pt = st_lget("pp_t", i) + dt
if pt > 1.2 { st_ldel("pp_x", i) st_ldel("pp_y", i) st_ldel("pp_t", i) st_ldel("pp_txt", i) st_ldel("pp_c", i) } else { st_lset("pp_t", i, pt) }
bind i = i - 1.0
}
bind i = 0.0
while i < st_llen("sd_x") {
st_lset("sd_bump", i, max(0.0, st_lget("sd_bump", i) - dt))
bind i = i + 1.0
}
plat_pfx_step(dt, 9.0, 1.2, 1000.0, 0.3)
sy_camera(dt)
return 0.0
}
fn sy_player_step(dt) {
bind auto = st_get("game_autoplay", 0.0) > 0.5
bind t = st_get("game_t", 0.0)
# input
bind ax = 0.0
if key_down("a") || key_down("left") { bind ax = ax - 1.0 }
if key_down("d") || key_down("right") { bind ax = ax + 1.0 }
if abs(plat_gamepad_lx()) > 0.2 { bind ax = ax + plat_gamepad_lx() }
bind jump_held = key_down("space") || key_down("w") || key_down("up") || plat_gamepad_button(0.0)
bind down = key_down("s") || key_down("down") || plat_gamepad_ly() > 0.6
bind up = key_down("w") || key_down("up") || plat_gamepad_ly() < 0.0 - 0.6
bind run = key_down("lshift") || key_down("rshift") || plat_gamepad_button(2.0)
bind lick = key_down("j") || key_down("x") || mouse_down() || plat_gamepad_button(1.0)
bind throw = key_down("k") || key_down("c") || mouse_down_right() || plat_gamepad_button(3.0)
if auto {
bind ax = 1.0
bind ph = t - floor(t / 2.6) * 2.6
bind jump_held = ph < 0.9
bind lick = sy_enemy_near(2.8)
bind throw = ph > 1.2 && ph < 1.25
bind down = ph > 2.3 && ph < 2.35 && st_get("sy_ground", 0.0) < 0.5
}
bind jump_pressed = sy_edge("jump", jump_held)
bind lick_pressed = sy_edge("lick", lick)
bind throw_pressed = sy_edge("throw", throw)
bind down_pressed = sy_edge("down", down)
bind x = st_get("sy_px", 0.0)
bind y = st_get("sy_py", 0.0)
bind vx = st_get("sy_vx", 0.0)
bind vy = st_get("sy_vy", 0.0)
bind ground = st_get("sy_ground", 0.0) > 0.5
bind hw = 0.42
bind hh = 0.58
# facing
if ax > 0.1 { st_set("sy_face", 1.0) }
if ax < 0.0 - 0.1 { st_set("sy_face", 0.0 - 1.0) }
bind face = st_get("sy_face", 1.0)
# horizontal
bind maxs = sel(run, 9.0, 6.2)
bind acc = sel(ground, 42.0, 24.0)
bind target = ax * maxs
if st_get("sy_pound", 0.0) > 0.0 { bind target = 0.0 }
if vx < target { bind vx = min(target, vx + acc * dt) }
if vx > target { bind vx = max(target, vx - acc * dt) }
# jump buffering + coyote time
if jump_pressed { st_set("sy_buffer", 0.14) }
st_set("sy_buffer", max(0.0, st_get("sy_buffer", 0.0) - dt))
if ground { st_set("sy_coyote", 0.1) } else { st_set("sy_coyote", max(0.0, st_get("sy_coyote", 0.0) - dt)) }
if st_get("sy_buffer", 0.0) > 0.0 && st_get("sy_coyote", 0.0) > 0.0 && st_get("sy_pound", 0.0) <= 0.0 {
bind vy = 0.0 - 13.0
st_set("sy_buffer", 0.0)
st_set("sy_coyote", 0.0)
st_set("sy_flutter_fuel", 1.0)
st_set("sy_squash", 0.0 - 0.35)
sy_sfx("jump", 0.6, 0.95 + rand() * 0.1)
sy_dust(x, y + hh)
}
# gravity, the held-jump float and the flutter
bind g = 34.0
if vy < 0.0 && jump_held { bind g = 19.0 }
bind flutter = false
if !ground && jump_held && vy > 0.0 - 1.0 && st_get("sy_flutter_fuel", 0.0) > 0.0 && st_get("sy_pound", 0.0) <= 0.0 {
bind flutter = true
st_set("sy_flutter_fuel", st_get("sy_flutter_fuel", 0.0) - dt)
bind g = 0.0
bind vy = vy + (0.0 - 3.2 - vy) * min(1.0, dt * 9.0)
if floor(t * 2.8) != floor((t - dt) * 2.8) { sy_sfx("flutter", 0.45, 1.0 + rand() * 0.06) }
if rand() < 0.4 { sy_dust(x - face * 0.3, y + hh) }
}
st_set("sy_flutter", sel(flutter, 1.0, 0.0))
# ground pound: a spin in the air, then straight down
if down_pressed && !ground && st_get("sy_pound", 0.0) <= 0.0 {
st_set("sy_pound", 1.0)
st_set("sy_pound_t", 0.0)
bind vx = 0.0
bind vy = 0.0
sy_sfx("pound", 0.7, 1.0)
}
if st_get("sy_pound", 0.0) > 0.0 {
st_set("sy_pound_t", st_get("sy_pound_t", 0.0) + dt)
if st_get("sy_pound_t", 0.0) < 0.2 { bind vy = 0.0 bind g = 0.0 } else { bind vy = 26.0 bind g = 0.0 }
}
bind vy = min(20.0, vy + g * dt)
if st_get("sy_pound", 0.0) > 0.0 && st_get("sy_pound_t", 0.0) >= 0.2 { bind vy = 26.0 }
# move with sub-steps, x then y
bind steps = max(1.0, ceil(max(abs(vx), abs(vy)) * dt / 0.12))
bind h = dt / steps
bind landed = false
bind k = 0.0
while k < steps {
bind nx = x + vx * h
bind hit = sy_hit_solid(nx, y, hw, hh - 0.02, vy, y + hh)
if hit >= 0.0 && st_lget("sd_k", hit) != 1.0 {
if vx > 0.0 { bind nx = st_lget("sd_x", hit) - st_lget("sd_hw", hit) - hw - 0.001 } else { bind nx = st_lget("sd_x", hit) + st_lget("sd_hw", hit) + hw + 0.001 }
bind vx = 0.0
}
bind x = nx
bind ny = y + vy * h
bind hit = sy_hit_solid(x, ny, hw - 0.04, hh, vy, y + hh)
if hit >= 0.0 {
bind top = st_lget("sd_y", hit) - st_lget("sd_hh", hit)
bind bot = st_lget("sd_y", hit) + st_lget("sd_hh", hit)
bind kind = st_lget("sd_k", hit)
if vy >= 0.0 {
bind ny = top - hh - 0.001
if kind == 3.0 {
bind vy = 0.0 - 21.0
st_lset("sd_bump", hit, 0.3)
sy_sfx("spring", 0.8, 1.0)
st_set("sy_flutter_fuel", 1.0)
st_set("sy_pound", 0.0)
} else {
if st_get("sy_pound", 0.0) > 0.0 { sy_pound_land(x, ny + hh, hit) }
if vy > 6.0 { st_set("sy_squash", 0.35) sy_dust(x, ny + hh) }
bind vy = 0.0
bind landed = true
}
} else {
if kind != 1.0 {
bind ny = bot + hh + 0.001
bind vy = 0.0
if kind == 2.0 { sy_bump_block(hit) }
}
}
}
bind y = ny
bind k = k + 1.0
}
bind ground = landed || sy_ground_at(x - hw * 0.8, y + hh) || sy_ground_at(x + hw * 0.8, y + hh)
if ground && vy >= 0.0 {
st_set("sy_flutter_fuel", 1.0)
if st_get("sy_pound", 0.0) > 0.0 { st_set("sy_pound", 0.0) }
if sy_ground_at(x, y + hh) { st_set("sy_safe_x", x) st_set("sy_safe_y", y) }
}
st_set("sy_ground", sel(ground && vy >= 0.0, 1.0, 0.0))
# fell off the page
if y > st_get("sy_floor_death", 12.0) {
bind x = st_get("sy_safe_x", 0.0)
bind y = st_get("sy_safe_y", 0.0) - 1.0
bind vx = 0.0
bind vy = 0.0
st_set("sy_inv", 0.0)
sy_hurt(x)
sy_hurt_extra()
}
st_set("sy_px", x) st_set("sy_py", y) st_set("sy_vx", vx) st_set("sy_vy", vy)
st_set("sy_squash", st_get("sy_squash", 0.0) * pow(0.0006, dt))
# trail for the eggs
st_lpush("hx", x)
st_lpush("hy", y)
st_ldel("hx", 0.0)
st_ldel("hy", 0.0)
# the tongue
if lick_pressed && st_get("sy_tongue", 0.0) <= 0.0 && st_get("sy_tongue_cd", 0.0) <= 0.0 {
st_set("sy_tongue", 0.001)
st_set("sy_tongue_up", sel(up, 1.0, 0.0))
st_set("sy_tongue_hit", 0.0)
sy_sfx("tongue", 0.6, 1.0)
}
if st_get("sy_tongue", 0.0) > 0.0 {
bind tt = st_get("sy_tongue", 0.0) + dt
st_set("sy_tongue", tt)
if tt > 0.3 { st_set("sy_tongue", 0.0) st_set("sy_tongue_cd", 0.12) } else { sy_tongue_check() }
}
# throw an egg
if throw_pressed && st_get("sy_eggs", 0.0) > 0.0 {
st_set("sy_eggs", st_get("sy_eggs", 0.0) - 1.0)
bind evx = face * 13.0 + vx * 0.4
bind evy = 0.0 - 8.0
if up { bind evx = face * 7.0 bind evy = 0.0 - 15.0 }
st_lpush("eg_x", x + face * 0.6) st_lpush("eg_y", y - 0.3)
st_lpush("eg_vx", evx) st_lpush("eg_vy", evy) st_lpush("eg_t", 0.0) st_lpush("eg_b", 0.0)
sy_sfx("throw", 0.7, 1.0)
}
# fruit by touch
bind n = st_llen("fr_x")
bind i = 0.0
while i < n {
if st_lget("fr_on", i) > 0.5 {
if abs(st_lget("fr_x", i) - x) < 0.9 && abs(st_lget("fr_y", i) - y) < 1.0 { sy_eat_fruit(i) }
}
bind i = i + 1.0
}
return 0.0
}
fn sy_hurt_extra() {
st_set("sy_inv", 0.0)
sy_hurt(st_get("sy_px", 0.0))
return 0.0
}
fn sy_pound_land(x, feet, hit) {
st_set("sy_shake", 0.35)
sy_stars(x, feet - 0.2, 22.0, c_rgb(255.0, 230.0, 140.0))
sy_sfx("bonk", 0.6, 0.6)
if st_lget("sd_k", hit) == 2.0 { sy_bump_block(hit) }
bind i = 0.0
while i < st_llen("en_x") {
if st_lget("en_on", i) > 0.5 {
if abs(st_lget("en_x", i) - x) < 2.6 && abs(st_lget("en_y", i) - (feet - 0.6)) < 1.6 { sy_kill_enemy(i, "pound") sy_popup(st_lget("en_x", i), st_lget("en_y", i) - 1.0, "Bonk!", c_rgb(255.0, 140.0, 80.0)) }
}
bind i = i + 1.0
}
return 0.0
}
fn sy_tongue_tip() {
bind tt = st_get("sy_tongue", 0.0)
bind ext = sin(clampf(tt / 0.3, 0.0, 1.0) * 3.14159) * 3.3
bind face = st_get("sy_face", 1.0)
bind mx = st_get("sy_px", 0.0) + face * 0.55
bind my = st_get("sy_py", 0.0) - 0.35
if st_get("sy_tongue_up", 0.0) > 0.5 { return [mx + face * ext * 0.6, my - ext * 0.8] }
return [mx + face * ext, my]
}
fn sy_tongue_check() {
if st_get("sy_tongue_hit", 0.0) > 0.5 { return 0.0 }
bind tip = sy_tongue_tip()
bind tx = list_get(tip, 0)
bind ty = list_get(tip, 1)
bind i = 0.0
while i < st_llen("en_x") {
if st_lget("en_on", i) > 0.5 {
bind dx = st_lget("en_x", i) - tx
bind dy = st_lget("en_y", i) - ty
if dx * dx + dy * dy < 0.75 * 0.75 {
st_set("sy_tongue_hit", 1.0)
if st_lget("en_k", i) == 2.0 {
sy_sfx("bonk", 0.6, 1.4)
sy_stars(tx, ty, 8.0, c_rgb(255.0, 160.0, 90.0))
} else {
sy_kill_enemy(i, "eat")
sy_popup(tx, ty - 0.8, "Gulp! +egg", c_rgb(120.0, 200.0, 160.0))
}
return 0.0
}
}
bind i = i + 1.0
}
bind i = 0.0
while i < st_llen("fr_x") {
if st_lget("fr_on", i) > 0.5 {
bind dx = st_lget("fr_x", i) - tx
bind dy = st_lget("fr_y", i) - ty
if dx * dx + dy * dy < 0.8 * 0.8 { sy_eat_fruit(i) }
}
bind i = i + 1.0
}
return 0.0
}
fn sy_enemy_near(r) {
bind i = 0.0
while i < st_llen("en_x") {
if st_lget("en_on", i) > 0.5 && st_lget("en_k", i) != 2.0 {
bind dx = (st_lget("en_x", i) - st_get("sy_px", 0.0)) * st_get("sy_face", 1.0)
if dx > 0.0 && dx < r && abs(st_lget("en_y", i) - st_get("sy_py", 0.0)) < 1.2 { return true }
}
bind i = i + 1.0
}
return false
}
fn sy_enemies_step(dt) {
bind px = st_get("sy_px", 0.0)
bind py = st_get("sy_py", 0.0)
bind pvy = st_get("sy_vy", 0.0)
bind playing = st_get("sy_state", "") == "play"
bind n = st_llen("en_x")
bind i = 0.0
while i < n {
if st_lget("en_on", i) > 0.5 {
bind k = st_lget("en_k", i)
bind x = st_lget("en_x", i)
bind y = st_lget("en_y", i)
bind vx = st_lget("en_vx", i)
st_lset("en_t", i, st_lget("en_t", i) + dt)
bind et = st_lget("en_t", i)
if abs(x - px) < 30.0 {
if k == 1.0 {
# flyers bob and drift around their spot
bind x = st_lget("en_x0", i) + sin(et * 0.7) * 2.5
bind y = st_lget("en_y0", i) + sin(et * 2.2) * 0.8
} else {
bind spd = sel(k == 2.0, 1.0, 1.6)
if vx > 0.0 { bind vx = spd } else { bind vx = 0.0 - spd }
bind vy = min(18.0, st_lget("en_vy", i) + 30.0 * dt)
bind nx = x + vx * dt
# turn at walls and at ledges
bind wall = sy_hit_solid(nx + sel(vx > 0.0, 0.45, 0.0 - 0.45), y, 0.05, 0.3, 0.0, y) >= 0.0
bind ledge = !sy_ground_at(nx + sel(vx > 0.0, 0.5, 0.0 - 0.5), y + 0.45)
if wall || (ledge && sy_ground_at(x, y + 0.45)) { bind vx = 0.0 - vx } else { bind x = nx }
bind ny = y + vy * dt
bind hit = sy_hit_solid(x, ny, 0.4, 0.45, vy, y + 0.45)
if hit >= 0.0 && vy >= 0.0 { bind ny = st_lget("sd_y", hit) - st_lget("sd_hh", hit) - 0.45 bind vy = 0.0 }
bind y = ny
st_lset("en_vy", i, vy)
}
st_lset("en_x", i, x)
st_lset("en_y", i, y)
st_lset("en_vx", i, vx)
# touching Moko: stomp from above (not spiky), otherwise it hurts
if playing && abs(x - px) < 0.85 && abs(y - py) < 1.05 {
if pvy > 2.0 && py < y - 0.4 && k != 2.0 {
sy_kill_enemy(i, "stomp")
st_set("sy_vy", 0.0 - 9.0)
st_set("sy_flutter_fuel", 1.0)
sy_popup(x, y - 0.8, "Squish!", c_rgb(150.0, 170.0, 255.0))
} else { sy_hurt(x) }
}
}
}
bind i = i + 1.0
}
return 0.0
}
fn sy_eggs_step(dt) {
bind i = st_llen("eg_x") - 1.0
while i >= 0.0 {
bind t = st_lget("eg_t", i) + dt
bind x = st_lget("eg_x", i)
bind y = st_lget("eg_y", i)
bind vx = st_lget("eg_vx", i)
bind vy = st_lget("eg_vy", i) + 26.0 * dt
bind gone = t > 2.6
bind nx = x + vx * dt
bind ny = y + vy * dt
bind hit = sy_hit_solid(nx, ny, 0.25, 0.3, vy, y + 0.3)
if hit >= 0.0 {
if st_lget("sd_k", hit) == 2.0 { sy_bump_block(hit) bind gone = true }
if abs(ny - st_lget("sd_y", hit)) < st_lget("sd_hh", hit) * 0.7 && st_lget("sd_k", hit) != 1.0 { bind vx = 0.0 - vx * 0.7 bind nx = x } else { bind vy = 0.0 - abs(vy) * 0.62 bind ny = y }
st_lset("eg_b", i, st_lget("eg_b", i) + 1.0)
sy_sfx("bonk", 0.35, 1.6)
if st_lget("eg_b", i) > 3.0 { bind gone = true }
}
# eggs pop puffs (even spiky ones) and collect fruit they touch
bind e = 0.0
while e < st_llen("en_x") && !gone {
if st_lget("en_on", e) > 0.5 && abs(st_lget("en_x", e) - nx) < 0.75 && abs(st_lget("en_y", e) - ny) < 0.75 {
sy_kill_enemy(e, "egg")
sy_sfx("pop", 0.7, 1.0)
sy_popup(nx, ny - 0.8, "Pop!", c_rgb(255.0, 200.0, 90.0))
bind gone = true
}
bind e = e + 1.0
}
bind f = 0.0
while f < st_llen("fr_x") {
if st_lget("fr_on", f) > 0.5 && abs(st_lget("fr_x", f) - nx) < 0.7 && abs(st_lget("fr_y", f) - ny) < 0.7 { sy_eat_fruit(f) }
bind f = f + 1.0
}
if gone {
sy_stars(nx, ny, 10.0, c_rgb(255.0, 255.0, 255.0))
st_ldel("eg_x", i) st_ldel("eg_y", i) st_ldel("eg_vx", i) st_ldel("eg_vy", i) st_ldel("eg_t", i) st_ldel("eg_b", i)
} else {
st_lset("eg_x", i, nx) st_lset("eg_y", i, ny) st_lset("eg_vx", i, vx) st_lset("eg_vy", i, vy) st_lset("eg_t", i, t)
}
bind i = i - 1.0
}
return 0.0
}
fn sy_camera(dt) {
bind face = st_get("sy_face", 1.0)
bind tx = st_get("sy_px", 0.0) + face * 2.4
bind ty = min(st_get("sy_py", 0.0) - 1.4, 2.0)
bind cx = st_get("sy_cam_x", tx) + (tx - st_get("sy_cam_x", tx)) * min(1.0, dt * 3.5)
bind cy = st_get("sy_cam_y", ty) + (ty - st_get("sy_cam_y", ty)) * min(1.0, dt * 2.5)
st_set("sy_cam_x", cx)
st_set("sy_cam_y", cy)
bind sh = st_get("sy_shake", 0.0)
bind intro = 0.0
if st_get("sy_state", "") == "intro" { bind intro = 1.0 - clampf(st_get("sy_state_t", 0.0) / 2.2, 0.0, 1.0) }
cam_orbit(cx + (rand() - 0.5) * sh, cy + (rand() - 0.5) * sh, 0.0, 0.0, 0.12 + intro * 0.1, 11.5 + intro * 4.0, 1.0)
return 0.0
}
fn sy_page_done() {
st_set("gs_fruits", num_str(st_get("sy_fruits", 0.0)))
st_set("gs_melons", num_str(st_get("sy_melons", 0.0)))
bind secs = st_get("sy_time", 0.0)
bind m = floor(secs / 60.0)
st_set("gs_time", num_str(m) + ":" + pad2(floor(secs - m * 60.0)))
st_set("gs_pagename", st_get("sy_title", ""))
bind petals = st_get("sy_petals", 0.0)
st_set("gs_hearts", list_get(["", "*", "**", "***"], clampf(floor(petals / 3.0) + 1.0, 1.0, 3.0)))
st_set("gs_done" + num_str(st_get("sy_page", 1.0)), "Page cleared - " + num_str(st_get("sy_fruits", 0.0)) + " fruits, " + st_get("gs_time", ""))
st_set("gs_next", sc_ex("next", "scenes/pages.lscene"))
story_leave()
game_goto("scenes/cleared.lscene")
return 0.0
}
# ── drawing ──────────────────────────────────────────────────────────────────
# spheres with a level of detail by size: eyes and cheeks need far fewer facets than a hill
fn sy_ball(c, x, y, z, rx, ry, rz) {
plat_color(c)
bind r = max(rx, max(ry, rz))
if r < 0.16 { plat_sphere_lod(x, y, z, rx, ry, rz, 8.0, 5.0) }
else if r < 0.6 { plat_sphere_lod(x, y, z, rx, ry, rz, 12.0, 8.0) }
else { plat_sphere_lod(x, y, z, rx, ry, rz, 16.0, 10.0) }
return 0.0
}
fn sy_box(c, x, y, z, sx, sy, sz, rx, ry, rz) {
plat_color(c)
plat_shape("box", x, y, z, sx, sy, sz, rx, ry, rz, 0.0)
return 0.0
}
fn story_lights() {
if !story_on() { return 0.0 }
plat_light(st_get("sy_px", 0.0) - 6.0, 0.0 - 12.0, 0.0 - 10.0, 255.0, 246.0, 236.0, 1.4, 60.0)
plat_light(st_get("sy_px", 0.0) + 10.0, 0.0 - 6.0, 0.0 - 6.0, 255.0, 200.0, 230.0, 0.8 + st_get("sy_beat", 0.0) * 0.5, 40.0)
return 0.0
}
# Moko: a round pastel dino with a mint saddle, big shiny eyes and blushing cheeks
fn sy_draw_moko(t) {
bind x = st_get("sy_px", 0.0)
bind y = st_get("sy_py", 0.0)
bind f = st_get("sy_face", 1.0)
if st_get("sy_inv", 0.0) > 0.0 && floor(t * 16.0) - floor(t * 16.0 / 2.0) * 2.0 < 0.5 { return 0.0 }
bind sq = st_get("sy_squash", 0.0)
bind bob = sin(t * 9.0) * 0.04 * sel(abs(st_get("sy_vx", 0.0)) > 0.5 && st_get("sy_ground", 0.0) > 0.5, 1.0, 0.0)
bind sx = 1.0 + sq * 0.5
bind syy = 1.0 - sq
bind spin = 0.0
if st_get("sy_pound", 0.0) > 0.0 && st_get("sy_pound_t", 0.0) < 0.2 { bind spin = st_get("sy_pound_t", 0.0) / 0.2 * 6.283 }
bind full = st_get("sy_full", 0.0) > 0.0
bind by = y + 0.1 - bob
# body + belly + saddle
sy_ball(sy_c("moko"), x, by, 0.0, 0.52 * sx, 0.5 * syy, 0.48)
sy_ball(sy_c("belly"), x + f * 0.2, by + 0.08, 0.0 - 0.18, 0.32 * sx, 0.36 * syy, 0.3)
sy_ball(sy_c("saddle"), x - f * 0.12, by - 0.42 * syy, 0.0, 0.34, 0.12, 0.38)
# head (bigger when the cheeks are full)
bind hx = x + f * 0.32
bind hy = by - 0.62 * syy
bind hr = 0.42 + sel(full, 0.06, 0.0)
sy_ball(sy_c("moko"), hx, hy, 0.0, hr * sx, hr * syy, hr)
# snout
sy_ball(sy_c("moko"), hx + f * 0.32, hy + 0.08, 0.0, 0.28, 0.22 * syy, 0.27)
# eyes: white, dark pupils, shine; blink every few seconds
bind blink = sel(t - floor(t / 3.7) * 3.7 < 0.12, 0.25, 1.0)
bind ex = hx + f * 0.14
sy_ball(sy_c("white"), ex, hy - 0.16, 0.0 - 0.25, 0.14, 0.18 * blink, 0.08)
sy_ball(sy_c("white"), ex - f * 0.2, hy - 0.16, 0.0 - 0.22, 0.12, 0.16 * blink, 0.07)
if blink > 0.5 {
sy_ball(sy_c("eye"), ex + f * 0.03, hy - 0.14, 0.0 - 0.32, 0.08, 0.12, 0.04)
sy_ball(sy_c("eye"), ex - f * 0.17, hy - 0.14, 0.0 - 0.28, 0.07, 0.1, 0.04)
sy_ball(sy_c("white"), ex + f * 0.06, hy - 0.2, 0.0 - 0.36, 0.03, 0.03, 0.02)
sy_ball(sy_c("white"), ex - f * 0.14, hy - 0.2, 0.0 - 0.32, 0.025, 0.025, 0.02)
}
# cheeks
sy_ball(sy_c("cheek"), hx + f * 0.25, hy + 0.06, 0.0 - 0.22, 0.09, 0.05, 0.03)
# crest
sy_ball(sy_c("saddle"), hx - f * 0.25, hy - 0.35, 0.0, 0.1, 0.14, 0.1)
# tail
sy_ball(sy_c("moko_dark"), x - f * 0.55, by + 0.2, 0.0, 0.18, 0.12, 0.14)
# feet: running cycle, flutter whirl, tucked when pounding
bind ph = t * 14.0
bind fl = st_get("sy_flutter", 0.0) > 0.5
bind run = abs(st_get("sy_vx", 0.0)) > 0.5 && st_get("sy_ground", 0.0) > 0.5
bind k = 0.0
while k < 2.0 {
bind off = sel(k < 0.5, 0.0, 3.14159)
bind fx = x + f * 0.12 + sel(run, cos(ph + off) * 0.22, 0.0)
bind fy = y + 0.5 + sel(run, min(0.0, sin(ph + off)) * 0.12, 0.0)
if fl {
bind fx = x + cos(ph * 2.2 + off) * 0.28
bind fy = y + 0.42 + sin(ph * 2.2 + off) * 0.16
}
if spin > 0.0 { bind fy = y + 0.3 }
sy_ball(sy_c("shoe"), fx, fy, sel(k < 0.5, 0.0 - 0.18, 0.18), 0.2, 0.12, 0.14)
bind k = k + 1.0
}
# the tongue
if st_get("sy_tongue", 0.0) > 0.0 {
bind tip = sy_tongue_tip()
bind mx = x + f * 0.55
bind my = st_get("sy_py", 0.0) - 0.35
bind tx = list_get(tip, 0)
bind ty = list_get(tip, 1)
bind ln = sqrt((tx - mx) * (tx - mx) + (ty - my) * (ty - my))
bind ang = atan2(ty - my, tx - mx)
plat_color(sy_c("tongue"))
plat_shape("capsule", (mx + tx) * 0.5, (my + ty) * 0.5, 0.0 - 0.1, 0.08, ln * 0.5 + 0.08, 0.08, 0.0, 0.0, ang - 1.5708, 0.0)
sy_ball(sy_c("tongue"), tx, ty, 0.0 - 0.1, 0.18, 0.18, 0.18)
}
return 0.0
}
fn sy_draw_egg(x, y, z, s) {
sy_ball(c_rgb(255.0, 253.0, 246.0), x, y, z, 0.26 * s, 0.33 * s, 0.26 * s)
sy_ball(c_rgb(120.0, 214.0, 150.0), x + 0.1 * s, y - 0.1 * s, z - 0.2 * s, 0.08 * s, 0.08 * s, 0.05 * s)
sy_ball(c_rgb(120.0, 214.0, 150.0), x - 0.12 * s, y + 0.08 * s, z - 0.19 * s, 0.07 * s, 0.07 * s, 0.05 * s)
return 0.0
}
fn sy_draw_fruit(k, x, y, t, ph) {
bind b = sin(t * 2.5 + ph) * 0.12
bind yy = y + b
bind spin = t * 1.5 + ph
if k == 0.0 {
sy_ball(c_rgb(255.0, 90.0, 100.0), x, yy, 0.0, 0.34, 0.32, 0.34)
sy_ball(c_rgb(255.0, 180.0, 180.0), x - 0.1, yy - 0.12, 0.0 - 0.28, 0.07, 0.07, 0.03)
sy_box(c_rgb(130.0, 90.0, 60.0), x, yy - 0.36, 0.0, 0.03, 0.08, 0.03, 0.0, 0.0, 0.0)
sy_ball(c_rgb(110.0, 210.0, 120.0), x + 0.12, yy - 0.4, 0.0, 0.12, 0.05, 0.08)
} else if k == 1.0 {
plat_color(c_rgb(255.0, 230.0, 90.0))
plat_shape("capsule", x - 0.1, yy, 0.0, 0.12, 0.3, 0.12, 0.0, spin * 0.2, 0.5, 0.0)
plat_shape("capsule", x + 0.12, yy - 0.05, 0.0, 0.12, 0.28, 0.12, 0.0, spin * 0.2, 0.0 - 0.4, 0.0)
sy_box(c_rgb(130.0, 100.0, 60.0), x + 0.02, yy - 0.32, 0.0, 0.04, 0.06, 0.04, 0.0, 0.0, 0.0)
} else if k == 2.0 {
bind gx = [0.0, 0.0 - 0.15, 0.15, 0.0 - 0.08, 0.08, 0.0]
bind gy = [0.0 - 0.18, 0.0 - 0.05, 0.0 - 0.05, 0.1, 0.1, 0.24]
bind j = 0.0
while j < 6.0 {
sy_ball(c_rgb(170.0, 110.0, 230.0), x + list_get(gx, j), yy + list_get(gy, j), 0.0, 0.11, 0.11, 0.11)
bind j = j + 1.0
}
sy_ball(c_rgb(110.0, 210.0, 120.0), x + 0.08, yy - 0.32, 0.0, 0.12, 0.05, 0.08)
} else if k == 3.0 {
sy_ball(c_rgb(120.0, 210.0, 110.0), x, yy, 0.0, 0.48, 0.42, 0.42)
bind j = 0.0
while j < 3.0 {
sy_box(c_rgb(60.0, 150.0, 70.0), x - 0.25 + j * 0.25, yy, 0.0 - 0.4, 0.04, 0.38, 0.04, 0.0, 0.0, 0.0)
bind j = j + 1.0
}
} else {
bind pulse = 1.0 + 0.1 * sin(t * 6.0)
sy_ball(c_rgb(255.0, 110.0, 160.0), x - 0.14 * pulse, yy - 0.06, 0.0, 0.2 * pulse, 0.2 * pulse, 0.18)
sy_ball(c_rgb(255.0, 110.0, 160.0), x + 0.14 * pulse, yy - 0.06, 0.0, 0.2 * pulse, 0.2 * pulse, 0.18)
plat_color(c_rgb(255.0, 110.0, 160.0))
plat_shape("cone", x, yy + 0.16, 0.0, 0.3 * pulse, 0.26, 0.18, 3.14159, 0.0, 0.0, 0.0)
}
return 0.0
}
fn sy_draw_puff(k, x, y, t, vx) {
bind f = sel(vx >= 0.0, 1.0, 0.0 - 1.0)
bind wob = sin(t * 8.0) * 0.05
bind body = list_get([c_rgb(140.0, 190.0, 255.0), c_rgb(200.0, 160.0, 255.0), c_rgb(255.0, 170.0, 90.0)], clampf(k, 0.0, 2.0))
sy_ball(body, x, y - 0.05 + wob, 0.0, 0.42, 0.42 - wob, 0.4)
# the round mask with two eyes
sy_ball(c_rgb(255.0, 255.0, 255.0), x + f * 0.2, y - 0.08, 0.0 - 0.24, 0.26, 0.28, 0.12)
sy_ball(sy_c("eye"), x + f * 0.28, y - 0.14, 0.0 - 0.35, 0.05, 0.08, 0.03)
sy_ball(sy_c("eye"), x + f * 0.12, y - 0.14, 0.0 - 0.34, 0.05, 0.08, 0.03)
sy_ball(sy_c("cheek"), x + f * 0.26, y + 0.03, 0.0 - 0.35, 0.05, 0.03, 0.02)
if k == 1.0 {
# little wings
bind flap = sin(t * 18.0) * 0.5
sy_ball(c_rgb(255.0, 255.0, 255.0), x - 0.36, y - 0.3, 0.1, 0.26, 0.1, 0.12 + flap * 0.08)
sy_ball(c_rgb(255.0, 255.0, 255.0), x + 0.36, y - 0.3, 0.1, 0.26, 0.1, 0.12 - flap * 0.08)
} else {
bind ph = t * 10.0
sy_ball(c_rgb(255.0, 200.0, 120.0), x - 0.15 + cos(ph) * 0.06, y + 0.38, 0.0 - 0.1, 0.13, 0.08, 0.1)
sy_ball(c_rgb(255.0, 200.0, 120.0), x + 0.15 + cos(ph + 3.14) * 0.06, y + 0.38, 0.1, 0.13, 0.08, 0.1)
}
if k == 2.0 {
bind j = 0.0
while j < 6.0 {
bind a = j / 6.0 * 6.283 + 0.3
plat_color(c_rgb(255.0, 245.0, 220.0))
plat_shape("cone", x + cos(a) * 0.45, y - 0.05 + sin(a) * 0.45, 0.0, 0.1, 0.16, 0.1, 0.0, 0.0, a - 1.5708, 0.0)
bind j = j + 1.0
}
}
return 0.0
}
fn story_world_native() {
if !story_on() { return 0.0 }
bind t = st_get("game_t", 0.0)
bind beat = st_get("sy_beat", 0.0)
bind cx = st_get("sy_cam_x", 0.0)
# animated scenery: flowers sway, trees and cloud faces bob to the beat
bind i = 0.0
while i < st_llen("dc_x") {
bind x = st_lget("dc_x", i)
if abs(x - cx) < 24.0 + abs(st_lget("dc_z", i)) * 0.5 {
bind y = st_lget("dc_y", i)
bind z = st_lget("dc_z", i)
bind k = st_lget("dc_k", i)
bind s = st_lget("dc_s", i)
bind c = st_lget("dc_c", i)
if k == 0.0 {
bind sway = sin(t * 2.0 + x) * 0.18 + beat * 0.1
plat_color(c_rgb(110.0, 190.0, 110.0))
plat_shape("cylinder", x + sway * 0.4, y - 0.4 * s, z, 0.05 * s, 0.4 * s, 0.05 * s, 0.0, 0.0, sway, 0.0)
bind fx = x + sway * 0.8
bind fy = y - 0.85 * s - beat * 0.06
bind j = 0.0
while j < 5.0 {
bind a = j / 5.0 * 6.283 + t * 0.4
sy_ball(c, fx + cos(a) * 0.2 * s, fy + sin(a) * 0.2 * s, z - 0.05, 0.14 * s, 0.14 * s, 0.06 * s)
bind j = j + 1.0
}
sy_ball(c_rgb(255.0, 230.0, 120.0), fx, fy, z - 0.1, 0.12 * s, 0.12 * s, 0.08 * s)
} else if k == 1.0 {
bind sq = 1.0 + beat * 0.06
plat_color(c_rgb(190.0, 140.0, 110.0))
plat_shape("cylinder", x, y - 1.0 * s, z, 0.22 * s, 1.0 * s, 0.22 * s, 0.0, 0.0, 0.0, 0.0)
sy_ball(c, x, y - 2.3 * s * sq, z, 1.1 * s * sq, 0.9 * s * sq, 1.0 * s)
sy_ball(c, x - 0.7 * s, y - 1.9 * s * sq, z + 0.2, 0.7 * s * sq, 0.6 * s * sq, 0.7 * s)
sy_ball(c, x + 0.75 * s, y - 2.0 * s * sq, z + 0.1, 0.7 * s * sq, 0.6 * s * sq, 0.7 * s)
} else {
bind bx = x + sin(t * 0.3 + i) * 1.5
bind by = y + sin(t * 0.8 + i) * 0.3
sy_ball(c, bx, by, z, 1.4 * s, 0.8 * s, 0.8 * s)
sy_ball(c, bx - 1.2 * s, by + 0.2 * s, z, 0.9 * s, 0.6 * s, 0.6 * s)
sy_ball(c, bx + 1.2 * s, by + 0.2 * s, z, 0.9 * s, 0.6 * s, 0.6 * s)
sy_ball(sy_c("eye"), bx - 0.35 * s, by - 0.1 * s, z - 0.75 * s, 0.08 * s, 0.13 * s, 0.04)
sy_ball(sy_c("eye"), bx + 0.35 * s, by - 0.1 * s, z - 0.75 * s, 0.08 * s, 0.13 * s, 0.04)
sy_ball(sy_c("cheek"), bx - 0.6 * s, by + 0.15 * s, z - 0.7 * s, 0.14 * s, 0.07 * s, 0.03)
sy_ball(sy_c("cheek"), bx + 0.6 * s, by + 0.15 * s, z - 0.7 * s, 0.14 * s, 0.07 * s, 0.03)
}
}
bind i = i + 1.0
}
# egg blocks and springs (bounce when hit)
bind i = 0.0
while i < st_llen("sd_x") {
bind k = st_lget("sd_k", i)
bind x = st_lget("sd_x", i)
if k >= 2.0 && abs(x - cx) < 30.0 {
bind y = st_lget("sd_y", i)
bind hw = st_lget("sd_hw", i)
bind hh = st_lget("sd_hh", i)
bind bump = st_lget("sd_bump", i)
if k == 2.0 {
bind lift = sin(bump / 0.25 * 3.14159) * 0.25
bind c = sel(st_lget("sd_charge", i) > 0.0, c_rgb(255.0, 220.0, 120.0), c_rgb(220.0, 200.0, 180.0))
sy_box(c, x, y - lift, 0.0, hw, hh, hw, 0.0, 0.0, 0.0)
sy_draw_egg(x, y - lift, 0.0 - hw - 0.05, 0.9)
} else {
bind sq = 1.0 - sin(bump / 0.3 * 3.14159) * 0.4
plat_color(c_rgb(255.0, 120.0, 140.0))
plat_shape("cylinder", x, y + hh * 0.5, 0.0, hw, hh * 0.5, hw, 0.0, 0.0, 0.0, 0.0)
plat_color(c_rgb(255.0, 255.0, 255.0))
plat_shape("torus", x, y - hh * 0.3 * sq, 0.0, hw * 0.8, 0.12, hw * 0.8, 0.0, 0.0, 0.0, 0.0)
plat_color(c_rgb(255.0, 120.0, 140.0))
plat_shape("cylinder", x, y - hh * 0.8 * sq, 0.0, hw * 1.05, 0.12, hw * 1.05, 0.0, 0.0, 0.0, 0.0)
}
}
bind i = i + 1.0
}
# fruit
bind i = 0.0
while i < st_llen("fr_x") {
if st_lget("fr_on", i) > 0.5 && abs(st_lget("fr_x", i) - cx) < 26.0 {
sy_draw_fruit(st_lget("fr_k", i), st_lget("fr_x", i), st_lget("fr_y", i), t, st_lget("fr_ph", i))
if st_lget("fr_k", i) == st_get("sy_lucky", 2.0) || st_lget("fr_k", i) == 3.0 {
plat_tm_billboard(0.0 - 1.0, st_lget("fr_x", i), st_lget("fr_y", i), 0.3, 1.8 + sin(t * 4.0 + i) * 0.3, 0.0, c_rgb(255.0, 240.0, 160.0), 0.45, 1.0)
}
}
bind i = i + 1.0
}
# puffs
bind i = 0.0
while i < st_llen("en_x") {
if st_lget("en_on", i) > 0.5 && abs(st_lget("en_x", i) - cx) < 26.0 {
sy_draw_puff(st_lget("en_k", i), st_lget("en_x", i), st_lget("en_y", i), t + st_lget("en_t", i), st_lget("en_vx", i))
}
bind i = i + 1.0
}
# eggs following Moko along her trail, and eggs in flight
bind e = 0.0
while e < st_get("sy_eggs", 0.0) {
bind hi = max(0.0, 59.0 - (e + 1.0) * 7.0)
sy_draw_egg(st_lget("hx", hi), st_lget("hy", hi) + 0.25 + sin(t * 8.0 + e) * 0.05, 0.25, 0.9)
bind e = e + 1.0
}
bind i = 0.0
while i < st_llen("eg_x") {
sy_draw_egg(st_lget("eg_x", i), st_lget("eg_y", i), 0.0, 1.0)
bind i = i + 1.0
}
sy_draw_moko(t)
plat_pfx_draw()
return 0.0
}
# ── HUD ──────────────────────────────────────────────────────────────────────
fn sy_text_c(cx, y, size, c, s) {
bind w = g_text_w(size, s)
g_text(cx - w * 0.5 + 2.0, y + 2.0, size, c_rgb(255.0, 255.0, 255.0), s)
g_text(cx - w * 0.5, y, size, c, s)
return 0.0
}
fn story_hud(x, y, w, h) {
if !story_on() { return 0.0 }
bind s = h / 720.0
bind t = st_get("game_t", 0.0)
bind ink = c_rgb(110.0, 64.0, 90.0)
# the smile flower: eight petals around a smiling sun face
bind fx = x + 70.0 * s
bind fy = y + 70.0 * s
bind pet = st_get("sy_petals", 8.0)
bind k = 0.0
while k < 8.0 {
bind a = k / 8.0 * 6.283 - 1.5708 + sin(t * 2.0) * 0.05
bind c = c_rgb(255.0, 150.0, 190.0)
if k >= pet { bind c = c_rgb(225.0, 215.0, 222.0) }
g_disc(fx + cos(a) * 34.0 * s, fy + sin(a) * 34.0 * s, 15.0 * s, c)
bind k = k + 1.0
}
g_disc(fx, fy, 24.0 * s, c_rgb(255.0, 226.0, 110.0))
g_disc(fx - 8.0 * s, fy - 4.0 * s, 3.5 * s, ink)
g_disc(fx + 8.0 * s, fy - 4.0 * s, 3.5 * s, ink)
g_disc(fx - 13.0 * s, fy + 5.0 * s, 4.0 * s, c_rgb(255.0, 150.0, 160.0))
g_disc(fx + 13.0 * s, fy + 5.0 * s, 4.0 * s, c_rgb(255.0, 150.0, 160.0))
if pet > 3.0 {
g_line(fx - 6.0 * s, fy + 7.0 * s, fx, fy + 11.0 * s, ink)
g_line(fx, fy + 11.0 * s, fx + 6.0 * s, fy + 7.0 * s, ink)
} else {
g_line(fx - 6.0 * s, fy + 11.0 * s, fx, fy + 7.0 * s, ink)
g_line(fx, fy + 7.0 * s, fx + 6.0 * s, fy + 11.0 * s, ink)
}
# eggs
bind e = 0.0
while e < 6.0 {
bind c = sel(e < st_get("sy_eggs", 0.0), c_rgb(255.0, 252.0, 244.0), c_rgb(255.0, 255.0, 255.0))
bind a = sel(e < st_get("sy_eggs", 0.0), 1.0, 0.25)
g_disc_a(x + 130.0 * s + e * 26.0 * s, y + 92.0 * s, 9.0 * s, c, a)
if e < st_get("sy_eggs", 0.0) { g_disc(x + 133.0 * s + e * 26.0 * s, y + 89.0 * s, 2.5 * s, c_rgb(120.0, 214.0, 150.0)) }
bind e = e + 1.0
}
# fruit ribbon: 30 little fruits, filled as Moko eats
bind goal = st_get("sy_goal", 30.0)
bind got = st_get("sy_fruits", 0.0)
bind rx = x + w * 0.5 - 15.0 * 22.0 * s * 0.5
g_rrect_a(rx - 18.0 * s, y + 14.0 * s, 15.0 * 22.0 * s + 36.0 * s, 66.0 * s, 14.0 * s, c_rgb(255.0, 250.0, 240.0), 0.85)
bind k = 0.0
while k < goal {
bind col = floor(k / 2.0)
bind row = k - col * 2.0
bind dx = rx + col * 22.0 * s
bind dy = y + 32.0 * s + row * 26.0 * s
if k < got {
bind fc = list_get([c_rgb(255.0, 100.0, 110.0), c_rgb(255.0, 220.0, 90.0), c_rgb(175.0, 120.0, 235.0), c_rgb(120.0, 210.0, 110.0)], k - floor(k / 4.0) * 4.0)
g_disc(dx, dy, 8.5 * s, fc)
g_disc(dx - 2.5 * s, dy - 2.5 * s, 2.5 * s, c_rgb(255.0, 255.0, 255.0))
} else { g_disc_a(dx, dy, 7.0 * s, c_rgb(220.0, 200.0, 210.0), 0.7) }
bind k = k + 1.0
}
sy_text_c(x + w * 0.5, y + 84.0 * s, 15.0 * s, ink, num_str(min(got, goal)) + " / " + num_str(goal) + " fruits")
# lucky fruit
g_rrect_a(x + w - 200.0 * s, y + 14.0 * s, 180.0 * s, 44.0 * s, 12.0 * s, c_rgb(255.0, 250.0, 240.0), 0.85)
g_text(x + w - 186.0 * s, y + 26.0 * s, 15.0 * s, ink, "Lucky fruit: " + sy_fruit_name(st_get("sy_lucky", 2.0)))
# popups
bind i = 0.0
while i < st_llen("pp_x") {
bind pt = st_lget("pp_t", i)
bind p = cam_project(st_lget("pp_x", i), st_lget("pp_y", i) - pt * 1.2, 0.0)
if cam_visible(list_get(p, 2)) { sy_text_c(list_get(p, 0), list_get(p, 1), 18.0 * s * (1.0 + (1.2 - pt) * 0.15), st_lget("pp_c", i), st_lget("pp_txt", i)) }
bind i = i + 1.0
}
# cards
bind st = st_get("sy_state", "")
bind stt = st_get("sy_state_t", 0.0)
if st == "intro" {
bind a = clampf(stt * 2.0, 0.0, 1.0)
g_rrect_a(x + w * 0.5 - 300.0 * s, y + h * 0.34, 600.0 * s, 150.0 * s, 26.0 * s, c_rgb(255.0, 250.0, 242.0), 0.92 * a)
sy_text_c(x + w * 0.5, y + h * 0.34 + 20.0 * s, 18.0 * s, c_rgb(232.0, 118.0, 158.0), "Page " + num_str(st_get("sy_page", 1.0)))
sy_text_c(x + w * 0.5, y + h * 0.34 + 46.0 * s, 38.0 * s, ink, st_get("sy_title", ""))
sy_text_c(x + w * 0.5, y + h * 0.34 + 104.0 * s, 16.0 * s, c_rgb(160.0, 110.0, 140.0), "Eat " + num_str(goal) + " fruits! The lucky " + sy_fruit_name(st_get("sy_lucky", 2.0)) + " counts three.")
}
if st == "clear" {
bind pop = 1.0 + max(0.0, 0.4 - stt) * 1.5
sy_text_c(x + w * 0.5, y + h * 0.36, 64.0 * s * pop, c_rgb(255.0, 120.0, 170.0), "Yummy!")
sy_text_c(x + w * 0.5, y + h * 0.36 + 80.0 * s, 26.0 * s, ink, "Page clear!")
}
if st == "oops" {
g_rect_a(x, y, w, h, c_rgb(255.0, 240.0, 245.0), clampf(stt, 0.0, 0.7))
sy_text_c(x + w * 0.5, y + h * 0.4, 52.0 * s, c_rgb(232.0, 118.0, 158.0), "Oops!")
sy_text_c(x + w * 0.5, y + h * 0.4 + 70.0 * s, 20.0 * s, ink, "Let's try that page again")
}
if st_get("sy_paused", 0.0) > 0.5 {
g_rect_a(x, y, w, h, c_rgb(255.0, 245.0, 250.0), 0.7)
sy_text_c(x + w * 0.5, y + h * 0.4, 48.0 * s, ink, "Paused")
sy_text_c(x + w * 0.5, y + h * 0.4 + 64.0 * s, 16.0 * s, c_rgb(160.0, 110.0, 140.0), "P resume Q back to the book")
}
return 0.0
}