灵符lingfu

soul-symphony-ling

↓ 0 downloads

Soul Symphony — Ling Edition. A spiritual 3D adventure in the soul realm: collect the letters of the world-song, clear the monster lattice, and duel in the arcade arena. Wind Waker-style toon renderer with volumetric light pools, SSAO, bloom and oil-slick tilt-shift — all ling-lang vectors.

Keywords game, 3d, toon, music, soul-symphony
lingfu install soul-symphony-ling
AuthorsLing Lin <taellinglin@gmail.com>
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (1947)

game/main.ling
ใช้ "../music/เพลงชื่อเรื่อง"
ใช้ "../music/songs"
ใช้ "../music/SoulSymphony_data"
ใช้ "../music/Battle_data"
ใช้ "../game/arena"
ใช้ "../game/boss"
ใช้ "../instruments/gong"
ใช้ "../instruments/yinyang"
ใช้ "../instruments/chime"
ใช้ "../instruments/gamelan"
ใช้ "../instruments/fanfare"
ใช้ "../music/Main_Menu_data"
ใช้ "../music/Seraph_data"
ใช้ "../music/Festival_data"
ใช้ "../game/proc_gen_level"
ใช้ "../game/monster"
ใช้ "../game/lattice"
ใช้ "../game/arcade"
ใช้ "../game/king_model"
ใช้ "../game/queen_model"
ใช้ "../game/companions"
ใช้ "render_optimized"
ใช้ "../game/scenes/intro"
ใช้ "../game/scenes/chapter1"
ใช้ "../game/scenes/chapter2"
ใช้ "../game/scenes/chapter3"
ใช้ "../game/scenes/chapter4"
ใช้ "../game/scenes/chapter5"
ใช้ "../game/scenes/chapter6"
ใช้ "../game/scenes/chapter7"
ใช้ "../game/scenes/chapter8"
ใช้ "../game/scenes/bonus"
ใช้ "../game/scenes/post_level1"
ใช้ "../game/scenes/finale1"
ใช้ "../game/scenes/finale2"
ใช้ "../game/scenes/finale3"
ใช้ "../game/bonus_level"
ใช้ "../game/online_world"
ใช้ "../font/Daemon"
ใช้ "../font/English"

ใช้ "color"
ใช้ "audio"
ใช้ "voice"
ใช้ "dialogue_font"
ใช้ "title"
ใช้ "play"
ใช้ "level"

# ════════════════════════════════════════════════════════════════════════════
# Soul Symphony Ling — เครื่องสถานะ + charge-jump + follow-cam + saves + ภาษา
# ════════════════════════════════════════════════════════════════════════════
bind ST_BOOT  = 0
bind ST_TITLE = 1
bind ST_MENU  = 2
bind ST_LOAD  = 3
bind ST_NEW   = 4
bind ST_MP    = 5
bind ST_OPT   = 6
bind ST_PLAY  = 7
bind ST_LANG  = 8
bind ST_CINE  = 9
bind ST_LEVELSLCT = 10
bind ST_LOBBY = 11
bind ST_ARCADE = 12
bind ST_ARCRESULT = 13
bind ST_HISCORE = 14
bind ST_JOINLIST = 15
bind ST_HOSTPW = 16
bind ST_JOINPW = 17
bind ST_STAGESEL = 18
bind ST_CH1 = 19
bind ST_POST_LVL1 = 20
bind ST_ONLINE = 21
bind ST_BONUS = 22
bind ST_CH2 = 23
bind ST_CH3 = 24
bind ST_CH4 = 25
bind ST_CH5 = 26
bind ST_CH6 = 27
bind ST_CH7 = 28
bind ST_CH8 = 29
bind ST_ONLINE_SEARCH = 30
bind ST_BONUSCINE = 31
bind ST_FINALE1 = 32
bind ST_FINALE2 = 33
bind ST_FINALE3 = 34
bind ST_DEATH = 35                                            # สตอรีตาย: ต่อ (กลับด่าน/บอสเดิม) หรือ ออกเมนู

# ── สเตตัสถาวร: สกิล + บัฟ 8 ช่อง + ฟีนิกซ์ดาวน์ → stats.dat (เซฟตอนชนะบอส/ออกเกม) ──
# เบลนด์สี [r,g,b] ไปหาแดงเลือดตามน้ำหนัก 0..1 (ใช้ตอนลูกโดนตี)
ฟังก์ชัน ผสมแดง(c, w) {
    คืน [
        floor(list_get(c,0) + (255.0 - list_get(c,0))*w),
        floor(list_get(c,1) * (1.0 - w*0.82)),
        floor(list_get(c,2) * (1.0 - w*0.82))
    ]
}

ฟังก์ชัน เซฟสเตตัส(สก, สต, ฟด) {
    bind line = to_str(สก)
    bind i = 0
    ขณะที่ i < 8 { bind line = line + "," + to_str(list_get(สต, i));  bind i = i + 1 }
    bind line = line + "," + to_str(ฟด)
    เขียนไฟล์("stats.dat", line)
    คืน 0
}


bind เริ่ม = do {
    # ── โหมดหน้าต่างจาก CLI: --fullscreen (ค่าเริ่มต้น) · --windowed · --resolution WxH ──
    #   หน้าต่าง/ความละเอียดต่ำ = fill-rate + present น้อยลง → FPS สูงขึ้น
    bind args = รับอาร์กิวเมนต์()
    bind wantFull = 1.0;  bind resW = 1280.0;  bind resH = 720.0;  bind wantLowLight = 1.0;  bind devStartLevel = 0.0
    bind ai = 0
    ขณะที่ ai < ความยาว(args) {
        bind a = list_get(args, ai)
        ถ้า a == "--windowed" { bind wantFull = 0.0 }
        ถ้า a == "--fullscreen" { bind wantFull = 1.0 }
        ถ้า a == "--low-light" { bind wantLowLight = 1.0 }
        ถ้า a == "--high-light" { bind wantLowLight = 0.0 }
        ถ้า a == "--level" { ถ้า ai + 1 < ความยาว(args) { bind devStartLevel = ปัดลง(list_get(args, ai+1) * 1.0) } }
        ถ้า a == "--resolution" { ถ้า ai + 1 < ความยาว(args) {
            bind rs = แยก(list_get(args, ai+1), "x")
            ถ้า ความยาว(rs) >= 2 { bind resW = list_get(rs, 0) * 1.0;  bind resH = list_get(rs, 1) * 1.0;  bind wantFull = 0.0 }
        } }
        bind ai = ai + 1
    }
    ถ้า wantFull > 0.5 { เปิดหน้าต่างเต็มจอ("Soul Symphony Ling") }
    มิฉะนั้น { เปิดหน้าต่าง(floor(resW), floor(resH)) }
    # ── แสงจริงจาก ling-lang ล่าสุด: เมชอบ/แลตทิซที่เสี่ยง speckle จะปิดแสงเฉพาะตอนวาด ──
    set_flat_shade(0.0)
    set_line_hue_cycle(10.0)                 # ไวร์เฟรมทุกเส้น: วนสีสเปกตรัมเร็ว (10 rad/วิ) — 0 = ปิด
    # ── Wind Waker post chain (ตั้งครั้งเดียว — sticky ทุกฉาก) ──
    #   โทนสมูท S-curve + ชีนไฮไลต์ (แรเงานุ่ม ไม่มีแถบสแครช) · AO สัมผัส ·
    #   บลูมเรือง HDR · ลบรอยหยัก — perfPanic จะปิด AO/บลูมชั่วคราวใน ST_PLAY
    # แถบเซลแบบแข็ง + ขอบนุ่ม + ชีนไฮไลต์: คอนทราสต์จัดแบบทูน (โหมด smooth lerp ทำโทนกลางลอยซีดเป็นนมทั้งพื้น)
    tone_smooth(0);  tone_bezier_off();  โทนขอบนุ่ม(0.32, 0.65)
    ตั้งเงาสัมผัส(0.4, 7, 14)
    ตั้งบลูม(0.5, 0.74)
    ลบรอยหยัก(1)
    bind จอกว้าง = ความกว้างจอ()
    bind จอสูง   = ความสูงจอ()
    bind จอFPS   = อัตรารีเฟรช()
    จับเมาส์()
    audio_volume(0.6)
    bind โหมดจอ = "เต็มจอไร้ขอบ";  ถ้า wantFull < 0.5 { bind โหมดจอ = "หน้าต่าง" }
    พิมพ์ฟ้า("   จอ " + to_str(จอกว้าง) + "x" + to_str(จอสูง) + " @ " + to_str(จอFPS) + "Hz (" + โหมดจอ + ")")

    bind kMesh = 0    # 0 = ราชา (วาดKing สด เพื่อแอนิเมชัน — เลิกอบ LOD)
    bind qMesh = 1    # 1 = ราชินี (วาดQueen สด)
    bind kCutMesh = mesh_load("game/king.lmesh")
    bind qCutMesh = mesh_load("game/queen.lmesh")
    bind palaceMesh = mesh_load("game/palace_temple.lmesh")     # วิหาร (คัตซีนบทสอง — แยกจาก model/palace.glb)
    bind palaceDecoMesh = mesh_load("game/palace_deco.lmesh")   # รุ้ง+เมฆประกอบวัง
    bind เพลงมูน = โหลดเพลง("music/MoonTime.wav")               # สกอร์บทสอง (สายมืด)
    bind ยาวเพลงมูน = ความยาวเพลง(เพลงมูน)
    # ชิ้นวัง 5 ส่วน (เฟรมร่วม) สำหรับวาร์ปท้ายคัตซีนบทสาม + สกอร์ Progress
    bind วังP0 = mesh_load("game/palace_p0.lmesh");  bind วังP1 = mesh_load("game/palace_p1.lmesh")
    bind วังP2 = mesh_load("game/palace_p2.lmesh");  bind วังP3 = mesh_load("game/palace_p3.lmesh")
    bind วังP4 = mesh_load("game/palace_p4.lmesh")
    bind เพลงโปรเกรส = โหลดเพลง("music/Progress.wav")           # สกอร์บทสาม
    bind ยาวเพลงโปรเกรส = ความยาวเพลง(เพลงโปรเกรส)
    bind เพลงเซราฟ = โหลดเพลง("music/Seraph.wav")               # เพลงแทรกช่วงวงออร์เคสตราเล่น + สกอร์คัตซีนโบนัส + ไฟนอล 3
    bind ยาวเพลงเซราฟ = ความยาวเพลง(เพลงเซราฟ)
    bind เพลงอินโทโร = โหลดเพลง("music/Intoro_Master.wav")      # สกอร์ไฟนอล 1 (หลังด่าน 9) — witchhouse หลอน
    bind ยาวเพลงอินโทโร = ความยาวเพลง(เพลงอินโทโร)
    bind เพลงเหรียญ = โหลดเพลง("music/MedalionMaster.wav")      # สกอร์ไฟนอล 2 (หลังด่าน 11) — UK ambient/experimental
    bind ยาวเพลงเหรียญ = ความยาวเพลง(เพลงเหรียญ)
    bind เพลงไซออน = โหลดเพลง("music/Psyon.wav")                # สกอร์บทสี่ (รูหนอน)
    bind ยาวเพลงไซออน = ความยาวเพลง(เพลงไซออน)
    bind เพลงเบสไลน์ = โหลดเพลง("music/Bassline.wav")           # สกอร์บทห้า (ศึกแสงเย็น) → จบด้วย เพลงเกม (Antartica)
    bind ยาวเพลงเบสไลน์ = ความยาวเพลง(เพลงเบสไลน์)
    bind เพลงเดมอน = โหลดเพลง("music/HealthyHell.wav")          # สกอร์บทหก (ห้องระหว่างโลก + เดมอนใจดี)
    bind ยาวเพลงเดมอน = ความยาวเพลง(เพลงเดมอน)
    bind เพลงสวิง = โหลดเพลง("music/Swing.wav")                 # สกอร์บทเจ็ด (บินข้ามทะเลดาว)
    bind ยาวเพลงสวิง = ความยาวเพลง(เพลงสวิง)
    bind เพลงหลัก       = โหลดเพลง("music/Soul Symphony_Colors_Mix_Master.wav")
    bind ความยาวเพลงหลัก = ความยาวเพลง(เพลงหลัก)
    bind ตัวอย่างSS2     = โหลดตัวอย่างเสียง("audio/soul-symphony-ii.wav")   # สติงไตเติล (SFX ครั้งเดียว ไม่ใช่ BGM วน)
    bind เพลงเกม        = โหลดเพลง("music/Antartica.wav")
    bind ยาวเพลงเกม     = ความยาวเพลง(เพลงเกม)
    bind เพลงเมนู       = โหลดเพลง("music/Main_Menu.wav")        # BGM เมนูหลัก (ต่างจากเพลงไตเติล)
    bind ยาวเพลงเมนู    = ความยาวเพลง(เพลงเมนู)
    bind เพลงออนไลน์    = โหลดเพลง("music/Soul Symphony_Interlude_Master.wav")
    bind ยาวเพลงออนไลน์ = ความยาวเพลง(เพลงออนไลน์)
    bind เพลงต่อสู้     = โหลดเพลง("music/Battle.wav")
    bind ยาวเพลงต่อสู้  = ความยาวเพลง(เพลงต่อสู้)
    bind เพลงบทหนึ่ง    = โหลดเพลง("music/Theme of the Marching Meadow.wav")
    bind ยาวเพลงบทหนึ่ง = ความยาวเพลง(เพลงบทหนึ่ง)
    bind เพลงเทศกาล     = โหลดเพลง("music/Festival.wav")          # สกอร์บทโหมโรง (งานเทศกาลโน้ตแรก หลังบอสทิวทอเรียล)
    bind ยาวเพลงเทศกาล  = ความยาวเพลง(เพลงเทศกาล)
    # ── เสียงอินเทอร์เฟซกาเมลัน: จูนตามโน้ต Main_Menu.mid (ลบแทร็ก 10 กลอง = ch9 อัตโนมัติ) ──
    bind mmData   = ข้อมูลเพลง_Main_Menu()
    bind navFreqs = list_get(mmData, 2)        # ความถี่โน้ต (จูนกาเมลัน)
    bind navCount = list_get(mmData, 0)
    bind navIdx   = 0.0                          # ก้าวไล่โน้ตเมื่อเลื่อนเมนู
    bind selPlay  = 0.0;  bind selStartT = 0.0;  bind selFired = 0.0;  bind selBaseF = 220.0   # สเกลสไลด์ตอนเลือก + เอคโค
    bind lastState = 0.0 - 1.0;  bind gdPrev = 0.0;  bind gaPrev2 = 0.0   # ตรวจเข้าเมนู / edge ดีแพด/A
    # ── แคชระยะเวลาเสียงพากย์ต่อฉาก (ไดนามิกตามภาษา) ──────────────────────────
    #   คำนวณครั้งเดียวตอนภาษาเปลี่ยน (ไม่ใช่ทุกเฟรม — ระยะเวลาเสียงพากย์ฉาก โหลดไฟล์
    #   จากดิสก์ N ครั้ง) แคชภาษา = -1 (ไม่ตรงรหัสภาษาใด ๆ) บังคับคำนวณครั้งแรกเสมอ
    bind บทกวีระยะเวลาแคช = [];  bind บทกวีระยะเวลาแคชภาษา = 0.0 - 1.0
    bind บท1ระยะเวลาแคช = [];    bind บท1ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind บท2ระยะเวลาแคช = [];    bind บท2ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind บท3ระยะเวลาแคช = [];    bind บท3ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind บท4ระยะเวลาแคช = [];    bind บท4ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind บท5ระยะเวลาแคช = [];    bind บท5ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind บท6ระยะเวลาแคช = [];    bind บท6ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind บท7ระยะเวลาแคช = [];    bind บท7ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind บท8ระยะเวลาแคช = [];    bind บท8ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind postLvl1ระยะเวลาแคช = [];  bind postLvl1ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind ไฟนอล1ระยะเวลาแคช = [];  bind ไฟนอล1ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind ไฟนอล2ระยะเวลาแคช = [];  bind ไฟนอล2ระยะเวลาแคชภาษา = 0.0 - 1.0
    bind ไฟนอล3ระยะเวลาแคช = [];  bind ไฟนอล3ระยะเวลาแคชภาษา = 0.0 - 1.0
    # เอดจ์คีย์บอร์ดของตัวเอง (กดค้าง ไม่ใช่กดปุ่ม) สำหรับ navHit/selHit เท่านั้น —
    # กดปุ่ม("up"/.../"enter") ที่นี่จะไปแย่ง edge เดียวกันกับ นำ()/กดปุ่ม ที่ตรรกะ
    # ของแต่ละสถานะใช้เปลี่ยนเคอร์เซอร์/ยืนยันจริง ๆ (อ่านซ้ำในเฟรมเดียวกัน → อันที่
    # อ่านก่อนกิน edge ไปหมด อีกอันที่เหลือไม่เห็น edge เลย) จึงต้องมี edge ของตัวเอง
    # ที่ไม่แตะ raw key state ของเอนจิ้นเลย เหมือน gdPrev/gaPrev2 ของจอยข้างบน
    bind upPrev = 0.0;  bind downPrev = 0.0;  bind leftPrev = 0.0;  bind rightPrev = 0.0;  bind enterPrev = 0.0
    # ── แฟนแฟร์ชนะ/แพ้ (Aurora Brass) — ซีเควนซ์โน้ตข้ามเฟรม ──
    bind fanfPlay = 0.0;  bind fanfStartT = 0.0;  bind fanfFired = 0.0;  bind fanfWin = 1.0
    bind fanfWinF  = [261.63, 329.63, 392.00, 523.25, 659.25, 783.99, 1046.50]   # โด-มี-โซล ขึ้น (ชนะ สง่า)
    bind fanfWinT  = [0.0, 0.13, 0.26, 0.42, 0.55, 0.68, 0.86]
    bind fanfLoseF = [392.00, 311.13, 261.63, 233.08, 174.61]                    # ตกลงไมเนอร์ (แพ้ เศร้า)
    bind fanfLoseT = [0.0, 0.24, 0.48, 0.74, 1.05]
    # เพลงเล่นในเกม: ใช้ Antarctica ถ้าเป็นไฟล์จริง มิฉะนั้น fallback เป็น Soul Symphony
    bind เพลงเล่น    = เพลงหลัก
    bind ยาวเพลงเล่น = ความยาวเพลงหลัก
    ถ้า ยาวเพลงเกม > 1.0 { bind เพลงเล่น = เพลงเกม;  bind ยาวเพลงเล่น = ยาวเพลงเกม }
    # ── ข้อมูลเพลง/ของเก็บ (ตั้งจริงต่อเลเวลใน รีเซ็ต จาก songs.ling) ──
    bind เพลงโหลดแล้ว = 0.0 - 1.0
    # แคชแฮนเดิลเพลงต่อ songIdx — กันการ decode WAV ซ้ำตอนสลับเพลง/เปลี่ยนรอบ (~32ms hitch)
    bind เพลงแคชId = [];  bind เพลงแคชH = [];  bind เพลงแคชLen = []
    bind sd0 = ข้อมูลเพลง_Antartica()
    bind bd0 = ข้อมูลเพลง_Battle()
    bind เก็บจำนวน  = list_get(sd0, 0)
    bind เก็บไกลฟ   = list_get(sd0, 1)
    bind เก็บความถี่ = list_get(sd0, 2)
    bind เก็บX      = list_get(sd0, 3)
    bind เก็บZ      = list_get(sd0, 4)
    bind ต่อสู้จำนวน  = list_get(bd0, 0)
    bind ต่อสู้ความถี่ = list_get(bd0, 2)
    bind sfxจำนวน   = เก็บจำนวน
    bind sfxความถี่  = เก็บความถี่
    bind โหมดต่อสู้  = 0.0
    bind ฮีทต่อสู้   = 0.0
    bind เฟดต่อสู้   = 0.0

    # ── โหลดตัวเลือก (options.cfg = "music;lang;sfxvol;bgmvol") ───────────────
    # ภาษาแยกสามค่า: ภาษา = UI/เมนู · ภาษาพากย์ = เสียงพากย์ · ภาษาซับ = ซับไตเติล
    # (options.cfg = "music;uilang;sfxvol;bgmvol;subsOn;voicelang;sublang" — ช่อง 5,6
    #  เพิ่มทีหลัง ไฟล์เก่าที่ไม่มีจะตกไปใช้ค่า UI เหมือนพฤติกรรมเดิมที่ใช้ค่าเดียวทุกที่)
    bind musicOn = 1.0;  bind ภาษา = 2.0;  bind ปริมาณSFX = 0.8;  bind ปริมาณBGM = 0.1;  bind แสดงบทพูด = 1.0  # Default to Thai
    bind ภาษาพากย์ = 0.0 - 1.0;  bind ภาษาซับ = 0.0 - 1.0        # -1 = ยังไม่ตั้ง → ตามภาษา UI
    bind cfg = อ่านไฟล์("options.cfg")
    ถ้า ความยาว(cfg) > 0 {
        bind cfgp = แยก(cfg, ";")
        ถ้า ความยาว(cfgp) > 0 { bind musicOn = floor(list_get(cfgp, 0) * 1.0) }
        ถ้า ความยาว(cfgp) > 1 { bind ภาษา = floor(list_get(cfgp, 1) * 1.0) }
        ถ้า ความยาว(cfgp) > 2 { bind ปริมาณSFX = list_get(cfgp, 2) * 1.0 }
        ถ้า ความยาว(cfgp) > 3 { bind ปริมาณBGM = list_get(cfgp, 3) * 1.0 }
        ถ้า ความยาว(cfgp) > 4 { bind แสดงบทพูด = floor(list_get(cfgp, 4) * 1.0) }
        ถ้า ความยาว(cfgp) > 5 { bind ภาษาพากย์ = floor(list_get(cfgp, 5) * 1.0) }
        ถ้า ความยาว(cfgp) > 6 { bind ภาษาซับ = floor(list_get(cfgp, 6) * 1.0) }
    }
    ถ้า ภาษาพากย์ < 0.0 { bind ภาษาพากย์ = ภาษา }
    ถ้า ภาษาซับ < 0.0 { bind ภาษาซับ = ภาษา }
    bind langTarget = 0.0                                        # จอเลือกภาษากำลังตั้งค่าอันไหน (ดู วาดหัวข้อเลือกภาษา)

    # ── เครือข่าย (net.cfg = "ip;port"; ค่าเริ่ม 127.0.0.1:7777) ──────────────
    bind netIP = "127.0.0.1";  bind netPort = 7777.0
    bind netCfg = อ่านไฟล์("net.cfg")
    ถ้า ความยาว(netCfg) > 0 {
        bind netP = แยก(netCfg, ";")
        ถ้า ความยาว(netP) > 0 { bind netIP = list_get(netP, 0) }
        ถ้า ความยาว(netP) > 1 { bind netPort = floor(list_get(netP, 1) * 1.0) }
    }
    bind discPort = netPort + 1   # พอร์ตค้นหาลอบบี้ LAN (UDP broadcast)
    bind netReady = 0
    bind srvSel = 0;  bind srvIPs = [];  bind srvInfos = []   # รายการเซิร์ฟเวอร์ที่เจอ
    bind hostPass = "";  bind joinPass = "";  bind joinPriv = 0.0;  bind wantPriv = 0.0;  bind authSent = 0.0   # รหัสผ่านเซิร์ฟส่วนตัว (ling-crypto)
    bind directJoin = 0.0   # ต่อตรงด้วย IP จาก net.cfg (เล่นข้ามเน็ต/ระบุเอง)

    # ── โหลดช่องเซฟ (saves.dat = "slot0;slot1;slot2") ───────────────────────
    bind slot0Data = "";  bind slot1Data = "";  bind slot2Data = ""
    bind sav = อ่านไฟล์("saves.dat")
    ถ้า ความยาว(sav) > 0 {
        bind savp = แยก(sav, ";")
        ถ้า ความยาว(savp) > 0 { bind slot0Data = list_get(savp, 0) }
        ถ้า ความยาว(savp) > 1 { bind slot1Data = list_get(savp, 1) }
        ถ้า ความยาว(savp) > 2 { bind slot2Data = list_get(savp, 2) }
    }
    # ── ความคืบหน้าสตอรี่ (progress.dat = ด่านสูงสุดที่เล่นผ่าน) → ล็อกด่านที่ยังไม่ถึง ──
    bind ด่านปลดล็อก = 1.0                                    # ทิวทอเรียล(0) + ด่าน 1 เปิดเสมอ
    bind progDat = อ่านไฟล์("progress.dat")
    ถ้า ความยาว(progDat) > 0 { bind ด่านปลดล็อก = progDat * 1.0 }
    ถ้า ด่านปลดล็อก < 1.0 { bind ด่านปลดล็อก = 1.0 }
    bind dieYaw = 0.0;  bind diePitch = 0.42;  bind dieVel = 0.01;  bind selTarget = 0.0   # ลูกเต๋าเลือกด่าน 12 หน้า (เอียงไปหา selTarget)

    # ── ไฮสกอร์ (highscores.dat = "name,score;name,score;...") ──
    bind hsNames = [];  bind hsScores = []
    bind hsEnt = แยก(อ่านไฟล์("highscores.dat"), ";")
    bind hi0 = 0
    ขณะที่ hi0 < ความยาว(hsEnt) {
        bind hpair = แยก(list_get(hsEnt, hi0), ",")
        ถ้า ความยาว(hpair) >= 2 {
            bind hsNames = list_push(hsNames, list_get(hpair, 0))
            bind hsScores = list_push(hsScores, list_get(hpair, 1) * 1.0)
        }
        bind hi0 = hi0 + 1
    }
    bind menuIdleT = 0.0;  bind hsInserted = 0.0   # ตัวจับเวลาไอเดิลเมนู + ธงแทรกคะแนนแล้ว
    bind gpNavT2 = 0.0;  bind gApr = 0.0;  bind gBpr = 0.0;  bind gSpr = 0.0   # จอย: ดีบาวซ์ทิศ + ขอบ A/B/Start (ทุกเมนู)
    bind gNavU = 0.0;  bind gNavD = 0.0;  bind gNavL = 0.0;  bind gNavR = 0.0;  bind gNavA = 0.0;  bind gNavB = 0.0
    bind debugHit = 0.0   # H: สลับแสดงดีบักฮิตบ็อกซ์ดาบ
    bind แสงต่ำ = wantLowLight;  bind tickแสงไฟ = 0.0   # โหมดแสงต่ำ (ลดภาระแสง): 1=เปิด (ค่าเริ่ม)
    bind impactT = 0.0;  bind impactX = 0.0;  bind impactZ = 0.0;  bind clashFX = 0.0   # อิมแพ็คปะทะ + ปะดาบ
    bind ลูกชื่อ = "player"   # ค่าเริ่มชื่อลูก (เผื่อข้ามหน้าปรับแต่ง)
    # ── วินิจฉัยจอย: แสดงสิ่งที่ gilrs ตรวจพบ (สำหรับคอนโทรลเลอร์ที่ระบุแปลก ๆ) ──
    จอยโพล()
    พิมพ์เขียว("  🎮 Gamepads (gilrs):")
    พิมพ์ฟ้า(จอยรายการ())

    พิมพ์เหลือง("══════════════════════════════════════════════")
    พิมพ์เหลือง("   Soul Symphony Ling — Made in Ling Country")
    พิมพ์ฟ้า("   ↑/↓ เลือก · Enter ตกลง · Esc ย้อน/พัก · K เซฟ")
    ถ้า แสงต่ำ > 0.5 { พิมพ์เหลือง("   Lighting: LOW (toggle L)") }
    มิฉะนั้น { พิมพ์ฟ้า("   Lighting: HIGH (toggle L)") }
    พิมพ์เหลือง("══════════════════════════════════════════════")

    bind สถานะ    = ST_BOOT
    bind sel       = 0
    bind ออกเกม    = 0
    bind langReturn = ST_MENU
    bind coopRole   = 0
    bind vsโหมด     = 0   # 0 = co-op, 1 = VS (PvP เครือข่าย)
    bind owIsHost      = 1.0   # ONLINE city shard: 1 = we host it, 0 = we joined an existing one
    bind onlineSearchT = 0.0
    bind owBundle      = []    # persistent state for วาดโลกออนไลน์ (state-threaded, see online_world.ling)
    # ── แชต ──
    bind chatActive = 0;  bind chatMsg = "";  bind chatMsgT = 0.0;  bind chatLog = []
    bind chatSend = "";  bind chatSendT = 0.0;  bind chat2Msg = "";  bind chat2T = 0.0;  bind chat2Last = ""
    # RPG: บัฟผู้เล่นสะสมข้ามด่าน [STR,AGI,VIT,FOC,DEF,LCK,SPD,PWR]
    bind ผู้เล่นสเตตัส = [0,0,0,0,0,0,0,0]
    # ── สหาย: HP/ตาย/ชุบชีวิต + Phoenix Down (ชุบสหายที่ตาย) ──
    bind kHP = 30.0;  bind qHP = 30.0;  bind สหายHปสูง = 30.0
    bind kAlive = 1.0;  bind qAlive = 1.0;  bind kDieT = 0.0;  bind qDieT = 0.0;  bind kRevT = 0.0;  bind qRevT = 0.0
    bind kHurtT = 0.0;  bind qHurtT = 0.0;  bind ฟีนิกซ์ดาวน์ = 1.0   # เริ่มมี 1 ชิ้น
    bind มอนเสียงCD = 0.0   # คูลดาวน์เสียงมอน (กัน 31 ตัวร้องพร้อมกัน → เสียงเฉพาะตัวกระจาย ~ทุก 0.2s)
    # เลขดาเมจ/ฮีลลอย (อาร์เคด) + ธงตีครั้งเดียวต่อสวิง
    bind popDmg = 0.0;  bind popDmgT = 0.0;  bind popDmgCrit = 0.0;  bind popHurt = 0.0;  bind popHurtT = 0.0;  bind popHeal = 0.0;  bind popHealT = 0.0;  bind slashHit = 0.0
    bind foeSleep = 0.0;  bind foeSlow = 0.0;  bind bombType = 0.0;  bind buffCD = [0.0,0.0,0.0]   # สถานะศัตรู + ชนิดระเบิดที่ถือ + คูลดาวน์บัฟอารีนา
    bind slashDir = 0.0   # ทิศฟัน 0..7 (นอน, ↘, ตั้ง, ↙, นอนกลับ, ↖, ตั้งกลับ, ↗) — วนทุกครั้งที่ฟัน
    bind บอสโหมด = 0
    bind อาร์เคดโหมด = 0
    # ── ปรับแต่งลูก (อาร์เคด): 3 เฉดสี + สแตมป์ + แบนด์ + ฟิลด์ที่เลือก + รอบ ──
    bind ฮิวBody = 0.55;  bind ฮิวStamp = 0.12;  bind ฮิวBand = 0.85
    bind ปรับStamp = 0.0;  bind ปรับBand = 1.0;  bind ปรับField = 0.0;  bind ปรับFX = 0.0   # ปรับFX: เอฟเฟกต์ (0 none/1 spin/2 pulse/3 orbit)
    bind stageSel = 0.0;  bind aiDiff = 1.0;  bind ฉากเฉด = 0.0   # เลือกสเตจ (สี) + เส้นโค้งความยาก AI (0 ง่าย/1 ปกติ/2 ยาก)
    bind ฉากทางออก = 4.0   # จำนวนทางออก/ทางเดินต่อห้องในแลตทิซ (1-4, ปรับได้)
    bind matchBT = 0.0;  bind matchBWin = 0.0                    # แบนเนอร์ Match Win!/Lose!
    bind รอบ = 1.0
    # ── ศัตรูอาร์เคด (สถานะ mutable) ──
    bind foeX = 35.0;  bind foeZ = 0.0;  bind foeY = 0.0;  bind foeVX = 0.0;  bind foeVZ = 0.0
    bind foeHP = 100.0;  bind foeHPMax = 100.0
    bind foeBodyHue = 0.0;  bind foeStampHue = 0.0;  bind foeBandHue = 0.0;  bind foeStamp = 0.0;  bind foeBand = 0.0
    bind foeDef = 0.0;  bind foeRes = 0.0;  bind foeAtk = 5.0;  bind foeCool = 1.5;  bind foeCoolT = 0.0;  bind foeDiff = 0.0
    bind foeName = "foe";  bind foeHurt = 0.0;  bind foeLunge = 0.0;  bind foeGuard = 0.0
    bind foeAgi = 0.4;  bind foeTech = 0.3;  bind foeAtt = 0.3;  bind foeSlashT = 0.0;  bind foeCombo = 0.0   # AI: agility/technique/attention (9 ระดับ) + ฟันดาบ
    bind callT = 0.0;  bind callMsg = "";  bind tauntT = 0.0;  bind ผู้ชนะรอบ = 0.0
    bind ภารกิจT = 0.0;  bind ภารกิจข้อความ = ""
    bind อาร์เคดไลฟ์ = 3.0;  bind อาร์เคดแต้ม = 0.0;  bind อาร์เคดชนะ = 0.0;  bind ผลพิมพ์ = 0.0;  bind arcContinueT = 0.0
    bind pickCD = [0.0,0.0,0.0,0.0,0.0,0.0]   # คูลดาวน์ดาบพิคอัพ 6 จุด (0 = พร้อม)
    bind swordType = 0.0;  bind weaponT = 0.0;  bind slashCD = 0.0   # ดาบที่ใช้ (0..5) + ตัวจับเวลา + คูลดาวน์ฟัน — อาร์เคด
    bind trailN = 26.0;  bind trailInit = 0.0                          # เทรลแหวนผู้เล่น/ศัตรู
    bind เอฟเฟกต์เทรล = 1.0                                            # สลับเทรล+วงแหวนโซนิค (ปุ่ม T) — ทดสอบคอขวด
    bind แสดงสหาย = 1.0                                                # สลับเมชสหาย ราชา/ราชินี (ปุ่ม Y) — ทดสอบคอขวด
    bind distortQ = 1.0                                                # คุณภาพบิดจอ: 0 ปิด · 1 เต็ม · 2 ครึ่งเรส (ปุ่ม U)
    bind trailBuf = [];  bind trailHead = 0.0
    bind foeTrailBuf = [];  bind foeTrailHead = 0.0
    bind critT = 0.0;  bind critX = 0.0;  bind critZ = 0.0             # เอฟเฟกต์ "Critical!"
    bind fxSat = 1.0;  bind fxSpin = 0.0;  bind micOpened = 0.0         # อิ่มสี (FFT เพลง) + หมุน (ไมค์) สำหรับเทรล/ริบบิ้น
    bind royg = [128.0, 128.0, 128.0]                                  # สี ROYGBIV จาก FFT (อัปเดตทุกเฟรม)
    bind liqAcc = 0.0;  bind liqMix = 0.0;  bind compLiqTick = 0.0     # ผู้เล่น: อัปเดต liquid ทุก 0.1s + cache mix
    bind humInit = 0.0;  bind humBase = 130.0;  bind humAmp = 0.04;  bind humAng = 0.0;  bind humMix = 0.0;  bind humDef = 0.0   # กันเสียงลูกสะดุดด้วยการทำ parameter smoothing
    # ── สหาย AI: ราชา(น้ำเงิน)/ราชินี(แดง) ในโหมดสตอรี่ ──
    bind compInit = 0.0;  bind kLiq = 0.0;  bind qLiq = 0.0;  bind kAtkT = 0.0;  bind qAtkT = 0.0   # แฮนเดิลของเหลว + คูลดาวน์โจมตี
    bind kTrail = [];  bind kThead = 0.0;  bind qTrail = [];  bind qThead = 0.0;  bind kSlashT = 0.0;  bind qSlashT = 0.0   # เทรลแหวน + ตัวจับเวลาฟันของสหาย
    bind kCharging = 0.0;  bind kCharge = 0.0;  bind kChargeTgt = 0.0;  bind qCharging = 0.0;  bind qCharge = 0.0;  bind qChargeTgt = 0.0   # charge-jump ของสหาย
    bind kx = 0.0;  bind kz = 0.0;  bind ky = 0.0;  bind kvx = 0.0;  bind kvy = 0.0;  bind kvz = 0.0;  bind kph = 0.0;  bind kyaw = 0.0;  bind kanim = 0.0;  bind kgnd = 1.0;  bind kstep = 0.0
    bind qx = 0.0;  bind qz = 0.0;  bind qy = 0.0;  bind qvx = 0.0;  bind qvy = 0.0;  bind qvz = 0.0;  bind qph = 0.0;  bind qyaw = 0.0;  bind qanim = 0.0;  bind qgnd = 1.0;  bind qstep = 0.0
    bind BOSS_Y = 0.0 - 55.0
    bind กรอบ      = 0.0
    bind bootStart = เวลาปัจจุบัน()
    bind chimeStep = 0.0
    bind flashChimePlayed = 0.0   # ไชม์ใหญ่ตอนแฟลชขาว (เล่นครั้งเดียว)
    bind เลเวลก่อน = 0.0 - 1.0   # ตรวจเปลี่ยนเลเวล → พรีโหลด
    # ── เบิร์สต์ไตเติล: ตารางเวลาโน้ต Soul Symphony (จาก MIDI) ──
    bind โน้ตเวลา  = เวลาโน้ต_SoulSymphony()
    bind โน้ตจำนวน = จำนวนโน้ต_SoulSymphony()
    bind ดัชนีโน้ต = 0
    bind เบิร์สต์  = 0.0

    bind paused    = 0
    bind gameMode  = 0
    bind optReturn = ST_MENU
    bind รีเซ็ต    = 1
    bind currentSlot = 0
    bind cineStart = 0.0
    bind prevLine = 0.0 - 1.0
    bind คอร์ดระฆัง = [523.0, 587.0, 659.0, 784.0, 880.0]   # เพนทาโทนิก C D E G A
    bind flashAmt = 0.0
    bind purified = 0
    bind บทหนึ่งเริ่ม = 0.0
    bind บทหนึ่งบรรทัดก่อน = 0.0 - 1.0
    bind postLvl1Start = 0.0
    bind postLvl1PrevLine = -1.0
    bind postLvl1ธาตุนับ = 0            # โคมห้าธาตุที่จุดเสียงไปแล้วในบทโหมโรง (one-fire ต่อดวง)
    bind bonusStart = 0.0
    bind bonusCX = 10.0
    bind bonusCY = -5.0
    bind bonusCZ = 0.0
    bind bonusVX = 0.0
    bind bonusVY = 0.0
    bind bonusVZ = 0.0
    bind bonusGrounded = 1.0
    bind bonusSquash = 0.0
    bind bonusLiq = 0
    bind ด่าน = 0
    bind คะแนน = 0
    bind เก็บแล้ว = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
    bind playStart = 0.0

    # ── บทกวีอินโทร 24 บรรทัด: บทกวี_บทพูด(ภาษา) ด้านบนคืนอาร์เรย์ตามภาษาที่เลือก ──
    bind บทหนึ่งพูด = [
        "chapter one. after the white silence, the queen still sleeps in glass.",
        "the king says the sky can only be soothed by a true lullaby.",
        "but the lullaby cannot be played without the meadow instrument.",
        "it is called the cradle reed, hidden beyond the marching meadow.",
        "bring it home, and we can wake the world without breaking it again.",
        "go now. the first note is waiting where the green winds turn gold."
    ]
    bind บทหนึ่งโน้ต = [293.66, 329.63, 392.00, 440.00, 523.25, 587.33]
    bind บทหนึ่งผู้พูด = [0,1,1,2,1,2]   # 0 narrator, 1 king, 2 queen
    bind startX = 0.0;  bind startY = 0.0 - 2.0;  bind startZ = 0.0

    # ── ออร์บ + ของเหลว (manual physics — สถานะจริงตั้งใน รีเซ็ต) ───────────────
    bind R = 1.5
    bind GROUND = 3.0
    bind liq  = liquid_new(48.0, 48.0)
    liquid_rainbow(liq, 1.0)
    # พื้นใช้ vector texture แบบกระเบื้องเติม (วาดพื้น) — ไม่ใช้ของเหลวแล้ว
    bind spDown = 0.0
    bind camX = 0.0;  bind camZ = 0.0 - 26.0
    bind camYaw = 0.0;  bind camPitch = 0.35
    bind cx = startX;  bind cy = startY;  bind cz = startZ
    bind vx = 0.0;  bind vy = 0.0;  bind vz = 0.0
    bind ดาบ = 0.0;  bind slashT = 0.0;  bind mbDown = 0.0
    bind guarding = 0.0
    bind guardT = 0.0;  bind guardHeld = 0.0
    bind สกิล = 0.0;  bind สกิลเริ่ม = 0.0;  bind เป้าสกิล = 1.0
    # ── โหลดสเตตัสถาวร (stats.dat): สกิล + บัฟ + ฟีนิกซ์ดาวน์ กลับมาทุกครั้งที่เปิดเกม ──
    bind stDat = อ่านไฟล์("stats.dat")
    ถ้า ความยาว(stDat) > 0 {
        bind stP = แยก(stDat, ",")
        ถ้า ความยาว(stP) > 0 { bind สกิลเริ่ม = list_get(stP, 0) * 1.0 }
        bind sti = 0
        ขณะที่ sti < 8 {
            ถ้า ความยาว(stP) > sti + 1 { bind ผู้เล่นสเตตัส = ตั้งรายการ(ผู้เล่นสเตตัส, sti, list_get(stP, sti + 1) * 1.0) }
            bind sti = sti + 1
        }
        ถ้า ความยาว(stP) > 9 { bind ฟีนิกซ์ดาวน์ = list_get(stP, 9) * 1.0 }
        พิมพ์ฟ้า("  💾 โหลดสเตตัส: สกิล " + to_str(สกิลเริ่ม))
    }
    bind มอนจำนวน = 0.0;  bind มอนเป็น = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1];  bind มอนAnim = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
    bind มอนHitT = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]   # ตัวจับเวลาแอนิเมชันโดนตี (วินาที)
    bind มอนDieT = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]   # ตัวจับเวลาแอนิเมชันตาย (1→0 = ยุบ+เผา)
    bind มอนseed = 1.0;  bind มอนspread = 45.0
    bind combo = 0.0;  bind arcFade = 0.0;  bind arcType = 0.0;  bind arcYaw = 0.0
    bind เลือกไอเทม = 1.0;  bind scrollAcc = 0.0;  bind slashNote = 0.0
    bind bombT = 0.0;  bind bombCool = 0.0
    bind swordSegScale = [0.86, 1.00, 1.08, 0.94, 1.18, 1.12]   # อาวุธแต่ละแบบ: ระยะคมดาบไม่เท่ากัน (คอลลิชันจริง)
    bind hudPerf = 0.0;  bind framePrevT = 0.0 - 1.0;  bind frameMs = 16.7;  bind frameMsSmooth = 16.7;  bind perfPanic = 0.0;  bind midiClockOn = 0.0;  bind fftCalls = 256.0;  bind particlesOn = 1.0
    bind flatShade = 0.0   # ค่าเริ่มต้น = รับแสงจริง; F11 สลับเป็น flat perf test
    bind toneMode = 0.0    # F12: สลับโหมด tone ramp (0=cel, 1=soft, 2=S-curve, 3=anime)
    bind perfSimMs = 0.0;  bind perfRenMs = 0.0;  bind perfLightMs = 0.0;  bind perfCpuMs = 0.0
    bind zLockOn = 0.0;  bind zLockValid = 0.0;  bind zLockX = 0.0;  bind zLockY = 0.0;  bind zLockZ = 0.0;  bind zTick = 0.0;  bind scenePrev = 0.0 - 1.0

    toneRampCel()          # default: 3-band hard cel

    ตั้งเสียงชื่อ()
    ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
    มิฉะนั้น { ระดับเพลง(0.0) }

    ถ้า devStartLevel > 0.5 {
        bind ด่าน = devStartLevel
        bind startX = 128.0;  bind startY = 0.0-2.0;  bind startZ = 128.0
        bind สกิลเริ่ม = 0.0;  bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
        bind บอสโหมด = 0;  bind อาร์เคดโหมด = 0;  bind สถานะ = ST_PLAY
        หยุดเพลง()
        พิมพ์ฟ้า("  dev start: level " + to_str(ด่าน))
    }

    # ════════════════════════════════════════════════════════════════════════

    ขณะที่ หน้าต่างเปิดอยู่() && ออกเกม == 0 {
        bind t = เวลาปัจจุบัน()
        bind perfT0 = t
        ถ้า framePrevT > 0.0 {
            bind frameMs = (t - framePrevT) * 1000.0
            ถ้า frameMs < 1.0 { bind frameMs = 1.0 }
            ถ้า frameMs > 200.0 { bind frameMs = 200.0 }
            bind frameMsSmooth = frameMsSmooth + (frameMs - frameMsSmooth) * 0.12
            ถ้า frameMsSmooth > 85.0 { bind perfPanic = 1.0 }
            ถ้า frameMsSmooth < 52.0 { bind perfPanic = 0.0 }
        }
        bind framePrevT = t
        จอยโพล()                           # อัปเดตจอยทุกเฟรม
        # ── ขอบนำทางจอยรวม (ทุกเมนูใช้ผ่าน นำ()): ดีบาวซ์ทิศ + ขอบ A(เลือก)/B(กลับ) ──
        ถ้า gpNavT2 > 0.0 { bind gpNavT2 = gpNavT2 - 0.016 }
        bind gNavU = 0.0;  bind gNavD = 0.0;  bind gNavL = 0.0;  bind gNavR = 0.0
        ถ้า gpNavT2 <= 0.0 {
            ถ้า จอยปุ่ม("dup") { bind gNavU = 1.0 };     ถ้า จอยแกน("ly") > 0.6 { bind gNavU = 1.0 }
            ถ้า จอยปุ่ม("ddown") { bind gNavD = 1.0 };    ถ้า จอยแกน("ly") < 0.0-0.6 { bind gNavD = 1.0 }
            ถ้า จอยปุ่ม("dleft") { bind gNavL = 1.0 };    ถ้า จอยแกน("lx") < 0.0-0.6 { bind gNavL = 1.0 }
            ถ้า จอยปุ่ม("dright") { bind gNavR = 1.0 };   ถ้า จอยแกน("lx") > 0.6 { bind gNavR = 1.0 }
            ถ้า gNavU + gNavD + gNavL + gNavR > 0.5 { bind gpNavT2 = 0.16 }
        }
        bind gAnow = จอยปุ่ม("a");  bind gNavA = 0.0;  ถ้า gAnow > 0.5 { ถ้า gApr < 0.5 { bind gNavA = 1.0 } };  bind gApr = gAnow
        bind gBnow = จอยปุ่ม("b");  bind gNavB = 0.0;  ถ้า gBnow > 0.5 { ถ้า gBpr < 0.5 { bind gNavB = 1.0 } };  bind gBpr = gBnow
        bind gSnow = จอยปุ่ม("start");  bind gNavS = 0.0;  ถ้า gSnow > 0.5 { ถ้า gSpr < 0.5 { bind gNavS = 1.0 } };  bind gSpr = gSnow
        # ════ เสียงอินเทอร์เฟซ (กาเมลัน) ทุกหน้าจอเมนู + BGM เมนูหลัก ════════════
        bind inMenu = 0.0
        ถ้า สถานะ == ST_MENU { bind inMenu = 1.0 }
        ถ้า สถานะ == ST_LOAD { bind inMenu = 1.0 }
        ถ้า สถานะ == ST_MP { bind inMenu = 1.0 }
        ถ้า สถานะ == ST_OPT { bind inMenu = 1.0 }
        ถ้า สถานะ == ST_LANG { bind inMenu = 1.0 }
        ถ้า สถานะ == ST_LEVELSLCT { bind inMenu = 1.0 }
        ถ้า สถานะ == ST_ONLINE { bind inMenu = 1.0 }
        ถ้า สถานะ == ST_ARCADE { bind inMenu = 1.0 }
        ถ้า สถานะ == ST_STAGESEL { bind inMenu = 1.0 }
        # BGM เมนูหลัก: เริ่ม Main_Menu.wav ตอนเข้าเมนู + วนเมื่อจบ
        ถ้า สถานะ == ST_MENU {
            bind sdif = สถานะ - lastState;  ถ้า sdif < 0.0 { bind sdif = 0.0 - sdif }
            ถ้า sdif > 0.5 { ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }
            ถ้า midiClockOn > 0.5 { ถ้า ยาวเพลงเมนู > 1.0 { ถ้า ตำแหน่งเพลง() >= ยาวเพลงเมนู - 0.5 { ค้นหาเพลง(0);  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } } } }
        }
        bind lastState = สถานะ
        # ── Scene cleanup on transition: reset transient FX/state between menus/scenes ──
        # Keyed on สถานะ + ด่าน + บอสโหมด + อาร์เคดโหมด, not สถานะ alone — level,
        # boss, and arcade changes all happen *inside* ST_PLAY, so a สถานะ-only
        # key never fired for them (lights/material/mesh-cache state from the
        # previous level or boss fight used to survive straight into the next).
        bind sceneKey = สถานะ*1000.0 + ด่าน*10.0 + บอสโหมด*2.0 + อาร์เคดโหมด
        ถ้า sceneKey != scenePrev {
            bind impactT = 0.0;  bind clashFX = 0.0;  bind critT = 0.0
            bind popDmgT = 0.0;  bind popHurtT = 0.0;  bind popHealT = 0.0
            bind chatActive = 0;  bind chatMsgT = 0.0;  bind chat2T = 0.0
            bind zLockOn = 0.0;  bind zLockValid = 0.0
            bind trailInit = 0.0;  bind trailHead = 0.0;  bind foeTrailHead = 0.0
            bind kThead = 0.0;  bind qThead = 0.0
            ถ้า สถานะ == ST_ARCADE { bind compInit = 0.0;  bind kAlive = 0.0;  bind qAlive = 0.0 }
            # ── ล้างสถานะเรนเดอร์ที่ค้างจากฉากก่อน (กันรั่วข้ามฉาก ไม่ว่าฉากเก่าจบ draw ตรงไหน) ──
            ล้างแสง()                                #  ไฟค้างไม่ส่องทะลุเข้าเมนู/ฉากถัดไป
            reset_normal_override()                  #  กันพื้นเลเวล/แลตทิซก่อนหน้าทิ้ง normal override ค้าง
            reset_material()                          #  กัน toon material (toon_bands ฯลฯ) ของพื้นเก่าค้างข้ามฉาก
            ตั้งแสงรอบข้าง(0.15)                       #  แสงรอบข้างกลับค่าเริ่มต้น (แต่ละฉากตั้งของตัวเองใหม่)
            โหมดผสม(0)                               #  เลิกโหมดบวกแสง (เคอร์เซอร์หมึก/HUD มักทิ้งไว้ที่ 1)
            set_depth_test(0)                        #  ปิด z-buffer (title/online เปิดเอง — กันค้าง)
            ตั้งหมอก(0, 0, 0, 0.0, 0.0)              #  หมอกปิด (กันหมอกน้ำเงินไตเติล/หมอกเกมเพลย์รั่ว)
            set_alpha(1.0)                            #  อัลฟาเต็ม (กันค่าโปร่งใสค้างจากเซอร์เฟส)
            ตั้งการแรเงา(2)                            #  กลับแรเงาโฮโลค่าเริ่มต้น (อาร์เคด/คัสตอมตั้ง cel เอง — กันรั่วข้ามฉาก)
            ตั้งขอบเรือง(0.6, 115, 217, 255)           #  ริมไลต์กลับค่าเริ่มต้น (อาร์เคด/คัสตอมปิดเอง)
            set_flat_shade(0.0)                        #  กัน flat(1) ของ HUD เฟรมก่อนรั่วเข้าฉากใหม่
            เบลอเฟรม(0.0)                              #  กัน afterimage คัตซีนรั่วเข้าเกมเพลย์
            # เข้า/ออกด่าน (proc-gen หรือ static) → ล้างเมชแคชห้อง กันห้องเก่าค้างหรือ
            # ปนกับซีดที่รีเจนใหม่หลัง reseed
            เมชแคชล้าง()
            # ── เงียบเสียงโทนต่อเนื่องทั้งหมด (ฮัมลูก slot 0-3 / โดรน) — ฉากใหม่ตั้งใหม่เอง ──
            bind clrT = 0
            ขณะที่ clrT < 16 { เสียงโทน(clrT, 0,0,0, 1.0, 200.0, 0.0, 0.0, 0.0);  bind clrT = clrT + 1 }
            # ── ตัดหางเสียงเอฟเฟกต์ one-shot ที่ค้าง (ฟัน/มอน/ระเบิด) ──
            # แต่ข้ามให้ฉากที่ "เปิดฉากด้วยเสียงทันที" เพราะจังหวะมันชนกันพอดี:
            # เฟรม N ตัวจัดการสถานะ (อยู่ใต้ hook นี้ในลูป) เซ็ตสถานะใหม่ แล้วบล็อกฉาก
            # ใต้ลงไปอีกก็ยิงเสียงเปิดฉากทันทีในเฟรมเดียวกัน — hook เพิ่งรันไปก่อนหน้า
            # จึงยังไม่เห็นการเปลี่ยน กว่าจะเห็นคือเฟรม N+1 แล้วสั่งหยุดทับเสียงที่เพิ่ง
            # เริ่มไป 1 เฟรม ผลคือ "บรรทัดแรกของทุกคัตซีนเงียบ" และสติงไตเติลไม่ดัง
            # ยอมให้หางเอฟเฟกต์สั้น ๆ (0.2-0.5 วิ) ลากเข้าคัตซีนดีกว่าเสียบทพูดแรกหาย
            bind ฉากเปิดด้วยเสียง = 0.0
            ถ้า สถานะ == ST_TITLE { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_CINE { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_POST_LVL1 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_BONUSCINE { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_CH1 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_CH2 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_CH3 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_CH4 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_CH5 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_CH6 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_CH7 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_CH8 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_FINALE1 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_FINALE2 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า สถานะ == ST_FINALE3 { bind ฉากเปิดด้วยเสียง = 1.0 }
            ถ้า ฉากเปิดด้วยเสียง < 0.5 { หยุดเอฟเฟกต์ทั้งหมด() }
        }
        bind scenePrev = sceneKey
        # ── เลื่อนเคอร์เซอร์ (ลูกศร/ดีแพด) → กาเมลันจูนตามโน้ต Main_Menu.mid ──
        ถ้า inMenu > 0.5 {
            bind navHit = 0.0
            bind upNow = 0.0;    ถ้า กดค้าง("up")    { bind upNow = 1.0 }
            bind downNow = 0.0;  ถ้า กดค้าง("down")  { bind downNow = 1.0 }
            bind leftNow = 0.0;  ถ้า กดค้าง("left")  { bind leftNow = 1.0 }
            bind rightNow = 0.0; ถ้า กดค้าง("right") { bind rightNow = 1.0 }
            ถ้า upNow > 0.5 { ถ้า upPrev < 0.5 { bind navHit = 1.0 } }
            ถ้า downNow > 0.5 { ถ้า downPrev < 0.5 { bind navHit = 1.0 } }
            ถ้า leftNow > 0.5 { ถ้า leftPrev < 0.5 { bind navHit = 1.0 } }
            ถ้า rightNow > 0.5 { ถ้า rightPrev < 0.5 { bind navHit = 1.0 } }
            bind upPrev = upNow;  bind downPrev = downNow;  bind leftPrev = leftNow;  bind rightPrev = rightNow
            bind gd = 0.0
            ถ้า จอยปุ่ม("dup") { bind gd = 1.0 };     ถ้า จอยปุ่ม("ddown") { bind gd = 1.0 }
            ถ้า จอยปุ่ม("dleft") { bind gd = 1.0 };    ถ้า จอยปุ่ม("dright") { bind gd = 1.0 }
            ถ้า gd > 0.5 { ถ้า gdPrev < 0.5 { bind navHit = 1.0 } }
            bind gdPrev = gd
            ถ้า navHit > 0.5 {
                bind nf = list_get(navFreqs, floor(navIdx))
                เล่นกาเมลัน(0.0, 0.0, 0.0, nf, 0.55 * ปริมาณSFX)
                bind navIdx = navIdx + 1.0;  ถ้า navIdx >= navCount { bind navIdx = 0.0 }
            }
            # ── เลือก (enter/A) → สไลด์ขึ้นสเกล + เอคโค ──
            bind selHit = 0.0
            bind enterNow = 0.0;  ถ้า กดค้าง("enter") { bind enterNow = 1.0 }
            ถ้า enterNow > 0.5 { ถ้า enterPrev < 0.5 { bind selHit = 1.0 } }
            bind enterPrev = enterNow
            bind gaB = 0.0;  ถ้า จอยปุ่ม("a") { bind gaB = 1.0 }
            ถ้า gaB > 0.5 { ถ้า gaPrev2 < 0.5 { bind selHit = 1.0 } }
            bind gaPrev2 = gaB
            ถ้า selHit > 0.5 { bind selPlay = 1.0;  bind selStartT = t;  bind selFired = 0.0;  bind selBaseF = 261.63 }
        }
        # ── แฟนแฟร์ชนะ/แพ้: ปล่อยโน้ตตามตารางเวลา (ข้ามเฟรม) ──
        ถ้า fanfPlay > 0.5 {
            bind ffF = fanfLoseF;  bind ffT = fanfLoseT;  bind ffN = 5.0
            ถ้า fanfWin > 0.5 { bind ffF = fanfWinF;  bind ffT = fanfWinT;  bind ffN = 7.0 }
            bind fgo = 1.0
            ขณะที่ fgo > 0.5 {
                ถ้า fanfFired >= ffN { bind fgo = 0.0;  bind fanfPlay = 0.0 }
                มิฉะนั้น {
                    bind fdue = list_get(ffT, floor(fanfFired))
                    ถ้า fanfStartT + fdue > t { bind fgo = 0.0 }
                    มิฉะนั้น {
                        เล่นแฟนแฟร์(0.0, 0.0, 0.0, list_get(ffF, floor(fanfFired)), 0.7 * ปริมาณSFX)
                        bind fanfFired = fanfFired + 1.0
                    }
                }
            }
        }
        # ── เลือกรายการ: สไลด์ขึ้นสเกล + เอคโค 3 ครั้ง (เบาลงเรื่อย ๆ) ──
        ถ้า selPlay > 0.5 {
            bind sgo = 1.0
            ขณะที่ sgo > 0.5 {
                ถ้า selFired >= 24.0 { bind sgo = 0.0;  bind selPlay = 0.0 }
                มิฉะนั้น {
                    bind pass = ปัดลง(selFired / 6.0)
                    bind step = selFired - pass*6.0
                    bind due  = pass*0.34 + step*0.045                                            # 6 โน้ตสเกล/รอบ, ห่างรอบ 0.34s
                    ถ้า selStartT + due > t { bind sgo = 0.0 }
                    มิฉะนั้น {
                        bind ratio = 1.0
                        ถ้า step > 0.5 { bind ratio = 1.122 }
                        ถ้า step > 1.5 { bind ratio = 1.26 }
                        ถ้า step > 2.5 { bind ratio = 1.498 }
                        ถ้า step > 3.5 { bind ratio = 1.682 }
                        ถ้า step > 4.5 { bind ratio = 2.0 }
                        bind eamp = 0.5                                                           # เอคโคเบาลงทุกรอบ
                        ถ้า pass > 0.5 { bind eamp = 0.26 }
                        ถ้า pass > 1.5 { bind eamp = 0.13 }
                        ถ้า pass > 2.5 { bind eamp = 0.07 }
                        เล่นกาเมลัน(0.0, 0.0, 0.0, selBaseF * ratio, eamp * ปริมาณSFX)
                        bind selFired = selFired + 1.0
                    }
                }
            }
        }
        # ── F12: สกรีนช็อต PNG → ./screenshots/ (ติดเวลา + โหมด) ──
        ถ้า กดปุ่ม("f12") {
            bind mdn = "game"
            ถ้า สถานะ == ST_BOOT { bind mdn = "boot" }
            ถ้า สถานะ == ST_TITLE { bind mdn = "title" }
            ถ้า สถานะ == ST_MENU { bind mdn = "mainmenu" }
            ถ้า สถานะ == ST_CINE { bind mdn = "intro" }
            ถ้า สถานะ == ST_CH1 { bind mdn = "chapter1" }
            ถ้า สถานะ == ST_CH2 { bind mdn = "chapter2" }
            ถ้า สถานะ == ST_CH3 { bind mdn = "chapter3" }
            ถ้า สถานะ == ST_CH4 { bind mdn = "chapter4" }
            ถ้า สถานะ == ST_CH5 { bind mdn = "chapter5" }
            ถ้า สถานะ == ST_CH6 { bind mdn = "chapter6" }
            ถ้า สถานะ == ST_CH7 { bind mdn = "chapter7" }
            ถ้า สถานะ == ST_CH8 { bind mdn = "chapter8" }
            ถ้า สถานะ == ST_POST_LVL1 { bind mdn = "post_level1" }
            ถ้า สถานะ == ST_ARCADE { bind mdn = "customize" }
            ถ้า สถานะ == ST_HISCORE { bind mdn = "highscores" }
            ถ้า สถานะ == ST_PLAY {
                bind mdn = "story"
                ถ้า อาร์เคดโหมด > 0.5 { bind mdn = "arcade" }
                ถ้า บอสโหมด > 0.5 { bind mdn = "boss" }
                ถ้า vsโหมด == 1 { bind mdn = "versus" }
            }
            บันทึกภาพ(mdn)
        }
        ถ้า กดปุ่ม("h") { bind debugHit = 1.0 - debugHit }   # H: สลับดีบักฮิตบ็อกซ์
        ถ้า กดปุ่ม("f10") { bind hudPerf = 1.0 - hudPerf }   # F10: สลับ HUD perf (ค่าเริ่มต้น = ซ่อน)
        ถ้า กดปุ่ม("f11") {                                   # F11: ปิดแสง → แรเงาแบนสีล้วน (วัดคอขวดแสง)
            bind flatShade = 1.0 - flatShade
            set_flat_shade(flatShade)
            ถ้า flatShade > 0.5 { พิมพ์เหลือง("  ▤ flat shading: ON (ปิดแสง — perf test)") }
            มิฉะนั้น { พิมพ์ฟ้า("  ▤ flat shading: OFF (แสงปกติ)") }
        }
        ถ้า กดปุ่ม("f12") {                                   # F12: สลับโหมด tone ramp
            bind toneMode = fract((toneMode + 1.0) / 4.0) * 4.0
            ถ้า toneMode < 0.5 { toneRampCel();     พิมพ์ฟ้า("  🎨 tone ramp: CEL (3-band hard)") }
            ถ้า toneMode > 0.5 { ถ้า toneMode < 1.5 { toneRampSoft();    พิมพ์ฟ้า("  🎨 tone ramp: SOFT (linear gradient)") } }
            ถ้า toneMode > 1.5 { ถ้า toneMode < 2.5 { toneRampSCurve();  พิมพ์ฟ้า("  🎨 tone ramp: S-CURVE (bezier)") } }
            ถ้า toneMode > 2.5 { toneRampAnime();   พิมพ์ฟ้า("  🎨 tone ramp: ANIME (punchy bands)") }
        }
        ถ้า กดปุ่ม("f9") {
            bind midiClockOn = 1.0 - midiClockOn
            ถ้า midiClockOn > 0.5 { พิมพ์ฟ้า("  ♫ midi clock: ON") }
            มิฉะนั้น { พิมพ์เหลือง("  ♫ midi clock: OFF (test)") }
        }
        ถ้า กดปุ่ม("f8") {
            bind particlesOn = 1.0 - particlesOn
            ถ้า particlesOn > 0.5 { พิมพ์ฟ้า("  ✨ particles: ON") }
            มิฉะนั้น { พิมพ์เหลือง("  ✨ particles: OFF (test)") }
        }
        bind dumpGraph = 0.0
        ถ้า กดปุ่ม("f7") { bind dumpGraph = 1.0 }
        ถ้า กดปุ่ม("g") { bind dumpGraph = 1.0 }
        ถ้า dumpGraph > 0.5 {
            bind sg = "scene graph snapshot\n"
            bind sg = sg + "state=" + to_str(สถานะ) + " mode=" + to_str(gameMode) + " level=" + to_str(ด่าน) + " score=" + to_str(คะแนน) + "\n"
            bind sg = sg + "camera=" + to_str(camX) + "," + to_str(cy - 9.0 - camPitch * 22.0) + "," + to_str(camZ) + " yaw=" + to_str(camYaw) + " pitch=" + to_str(camPitch) + "\n"
            bind sg = sg + "player=" + to_str(cx) + "," + to_str(cy) + "," + to_str(cz) + " vel=" + to_str(vx) + "," + to_str(vy) + "," + to_str(vz) + "\n"
            ถ้า อาร์เคดโหมด > 0.5 {
                bind sg = sg + "companions=disabled(arcade)\n"
            }
            มิฉะนั้น {
                bind sg = sg + "companion_king=" + to_str(kx) + "," + to_str(ky) + "," + to_str(kz) + " hp=" + to_str(kHP) + " alive=" + to_str(kAlive) + "\n"
                bind sg = sg + "companion_queen=" + to_str(qx) + "," + to_str(qy) + "," + to_str(qz) + " hp=" + to_str(qHP) + " alive=" + to_str(qAlive) + "\n"
            }
            bind sg = sg + "boss=" + to_str(BOSS_Y) + " active=" + to_str(บอสโหมด) + " arcade=" + to_str(อาร์เคดโหมด) + "\n"
            bind sg = sg + "monsters:\n"
            bind sgi = 0
            bind sgc = 0
            ขณะที่ sgi < มอนจำนวน {
                ถ้า list_get(มอนเป็น, sgi) > 0 {
                    bind sgc = sgc + 1
                    ถ้า sgc <= 24 {
                        bind sg = sg + "  m" + to_str(sgi) + "=" + to_str(list_get(มอนX, sgi)) + "," + to_str(list_get(มอนZ, sgi)) + " hp=" + to_str(list_get(มอนHP, sgi)) + "," + to_str(list_get(มอนHPmaxArr, sgi)) + "\n"
                    }
                }
                bind sgi = sgi + 1
            }
            เขียนไฟล์("scene_graph.txt", sg)
            เขียนไฟล์("game/scene_graph.txt", sg)
            เขียนไฟล์("recordings/scene_graph.txt", sg)
            พิมพ์ฟ้า("  🗺 scene graph dumped (scene_graph.txt, game/scene_graph.txt, recordings/scene_graph.txt)")
        }
        ถ้า กดปุ่ม("l") {
            bind แสงต่ำ = 1.0 - แสงต่ำ
            ถ้า แสงต่ำ > 0.5 { พิมพ์เหลือง("  ☼ lighting: LOW") }
            มิฉะนั้น { พิมพ์ฟ้า("  ☼ lighting: HIGH") }
        }
        ตั้งหมอก(0, 0, 0, 0.0, 0.0)        # หมอกปิดเริ่มต้น (เกมเพลย์เปิดเอง)

        # ── BOOT ──────────────────────────────────────────────────────────────
        ถ้า สถานะ == ST_BOOT {
            bind el = t - bootStart
            bind prog = el / 8.5                # บูตยาวขึ้น — ตัวอักษรลงตัว (~0.62) → ค้างไว้นาน → เฟดดำ
            bind fade = 1.0
            ถ้า prog > 0.88 { bind fade = (1.0 - prog) / 0.12 }   # ค้างตัวอักษรนานขึ้นก่อนเฟด (เฟด 0.88→1.0)
            ถ้า fade < 0.0 { bind fade = 0.0 }
            bind lfo = 0.0                   # ไม่มีโมดูเลชัน — แพดคอร์ดนิ่ง
            # ฮัม (คล้าย binaural beat) — เฟดเข้า (0→0.22) → ค้าง → เฟดออก (0.86→1.0)
            bind hum = 1.0
            ถ้า prog < 0.22 { bind hum = prog / 0.22 }
            ถ้า prog > 0.86 { bind hum = (1.0 - prog) / 0.14 }
            ถ้า hum < 0.0 { bind hum = 0.0 }
            ถ้า prog > 0.88 { ถ้า flashChimePlayed < 0.5 { เล่นไชม์ใหญ่(0,0,0, 523.25, 0.95*ปริมาณSFX);  bind flashChimePlayed = 1.0 } }   # ไชม์ใหญ่ตอนแฟลชขาว
            bind a = 0.26 * hum
            # Stereo music bus only (L/R): ลดจำนวน thread โทนต่อเนื่องในช่วงบูต
            เสียงโทน(0, 0,0,0, 1.0, 261.63, a,      lfo,      0.40*fade)
            เสียงโทน(1, 0,0,0, 1.0, 329.63, a*0.88, lfo*0.9,  0.36*fade)
            ผู้ฟัง(1.0, 0.0, 1.0, 0.0)
            # ── อาร์เพจจิโอไชม์คลื่น: ไล่เป็นคลื่น + คอร์ด, ดังขึ้น (crescendo) แล้วเฟดออก ──
            bind ai = ปัดลง(el / 0.13)
            ถ้า ai > chimeStep {
                bind chimeStep = ai
                bind cres = prog / 0.72;  ถ้า cres > 1.0 { bind cres = 1.0 }   # ไล่ดังขึ้นถึง prog 0.72
                bind camp = 0.7 * cres * fade                                   # crescendo → fade
                bind wv = ไซน์(el * 1.5) * 0.5 + 0.5            # 0..1 คลื่นช้า (waving)
                bind grp = ai - ปัดลง(ai / 3.0) * 3.0           # 0,1,2 ไล่ในกลุ่ม (arpeggio)
                bind ni = ปัดลง(wv * 5.0 + grp)
                ถ้า ni > 8 { bind ni = 8 };  ถ้า ni < 0 { bind ni = 0 }
                เสียงเอฟเฟกต์(0,0,0, 1.0, list_get(ไชม์, ni), camp, 0.75, "sine")
                ถ้า grp < 0.5 {                                  # ต้นกลุ่ม → คอร์ด (3rd + 5th)
                    bind n2 = ni + 2;  ถ้า n2 > 8 { bind n2 = 8 }
                    bind n3 = ni + 4;  ถ้า n3 > 8 { bind n3 = 8 }
                    เสียงเอฟเฟกต์(0,0,0, 1.0, list_get(ไชม์, n2), camp*0.62, 0.85, "sine")
                    เสียงเอฟเฟกต์(0,0,0, 1.0, list_get(ไชม์, n3), camp*0.46, 0.85, "sine")
                }
            }

            เติม(0, 0, 0)
            ตั้งแสงรอบข้าง(0.5)
            ตั้งกล้องมอง(0.0, 0.0, -24.0, 0.0, 0.0, 0.0)
            ตั้งระยะห่าง(2.8)
            วาดบูต(prog, el)
            แสดงผล()

            ถ้า prog >= 1.0 { bind สถานะ = ST_TITLE }
            ถ้า กดปุ่ม("enter") { bind สถานะ = ST_TITLE }
            ถ้า กดปุ่ม("space") { bind สถานะ = ST_TITLE }
            ถ้า สถานะ == ST_TITLE {
                เสียงโทน(0,0,0,0,1.0,261.0,0.0,0.0,0.0)
                เสียงโทน(1,0,0,0,1.0,329.0,0.0,0.0,0.0)
                เสียงโทน(2,0,0,0,1.0,392.0,0.0,0.0,0.0)
                เสียงโทน(3,0,0,0,1.0,493.0,0.0,0.0,0.0)
                ถ้า musicOn == 1 { เล่นเพลง(เพลงหลัก) }
                เล่นตัวอย่างเสียง(ตัวอย่างSS2, 0.0, 0.0, 0.0, 1.0, ปริมาณSFX, 0.0)   # สติงไตเติล ครั้งแรกที่ปรากฏเท่านั้น
            }
        }

        # ── TITLE ─────────────────────────────────────────────────────────────
        ถ้า สถานะ == ST_TITLE {
            bind เบิร์สต์ = เบิร์สต์ * 0.88
            bind songPos = ตำแหน่งเพลง()
            ถ้า ความยาวเพลงหลัก > 1.0 { ถ้า songPos >= ความยาวเพลงหลัก - 0.5 {
                ค้นหาเพลง(0)
                ถ้า musicOn == 1 { เล่นเพลง(เพลงหลัก) }
                bind ดัชนีโน้ต = 0
                bind songPos = 0.0
            } }
            ถ้า songPos < 0.05 { bind ดัชนีโน้ต = 0 }
            bind scanTitleNotes = 1
            ขณะที่ scanTitleNotes == 1 {
                ถ้า ดัชนีโน้ต >= โน้ตจำนวน { bind scanTitleNotes = 0 }
                มิฉะนั้น {
                    ถ้า list_get(โน้ตเวลา, ดัชนีโน้ต) <= songPos {
                        bind เบิร์สต์ = 1.0
                        bind ดัชนีโน้ต = ดัชนีโน้ต + 1
                    }
                    มิฉะนั้น { bind scanTitleNotes = 0 }
                }
            }
            วาดฉากชื่อเรื่อง(กรอบ, เบิร์สต์, ภาษา)

            # Input handling
            ถ้า กดปุ่ม("space") { พรีโหลด(0.4);  หยุดเพลง();  bind สถานะ = ST_MENU;  bind sel = 0;  bind menuIdleT = 0.0;  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }
            ถ้า นำ("enter", gNavA) { พรีโหลด(0.4);  หยุดเพลง();  bind สถานะ = ST_MENU;  bind sel = 0;  bind menuIdleT = 0.0;  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }

            # F12 bonus level trigger — เข้าคัตซีนโบนัส (โคด้า) ก่อน แล้วค่อยเข้าสนามจริง
            ถ้า กดปุ่ม("f12") {
                หยุดเพลง()
                bind สถานะ = ST_BONUSCINE
                bind บทโบนัสเริ่ม = เวลาปัจจุบัน()
                bind บทโบนัสบรรทัดก่อน = 0.0 - 1.0
                bind bonusCX = 10.0;  bind bonusCY = -5.0;  bind bonusCZ = 0.0
                bind bonusVX = 0.0;  bind bonusVY = 0.0;  bind bonusVZ = 0.0
                bind bonusGrounded = 1.0;  bind bonusSquash = 0.0
                bind bonusLiq = liquid_new(16.0, 16.0)
                liquid_rainbow(bonusLiq, 1.0)
                ถ้า musicOn == 1 { เล่นเพลง(เพลงเซราฟ);  ระดับเพลง(ปริมาณBGM * 0.85) }
                พิมพ์เหลือง("  ★ a secret door creaks open...")
            }
        }

        # ── MENU ──────────────────────────────────────────────────────────────
        ถ้า สถานะ == ST_MENU {
            bind menuIdleT = menuIdleT + 0.016
            ถ้า นำ("up", gNavU)   { bind sel = sel - 1;  ถ้า sel < 0 { bind sel = 8 };  bind menuIdleT = 0.0 }
            ถ้า นำ("down", gNavD) { bind sel = sel + 1;  ถ้า sel > 8 { bind sel = 0 };  bind menuIdleT = 0.0 }
            ถ้า menuIdleT > 16.0 { bind menuIdleT = 0.0;  bind สถานะ = ST_HISCORE }   # ไอเดิล → โชว์ไฮสกอร์
            วาดพื้นหลังเมนู(กรอบ)
            วาดเมนูภาษา(idsหลัก, sel, ภาษา, 0.0-7.0, 2.0, 0.85, t)
            แสดงผล()
            bind menuGo = 0.0
            ถ้า นำ("enter", gNavA) { bind menuGo = 1.0 }
            ถ้า menuGo > 0.5 {
                จอยสั่น(0.0, 0.25, 45)   # จอย: เลือกเมนู
                ถ้า sel == 0 { bind สถานะ = ST_LOAD;  bind sel = 0 }
                ถ้า sel == 1 { bind ด่าน = 0;  bind สกิลเริ่ม = 0.0;  bind สถานะ = ST_CINE;  bind cineStart = เวลาปัจจุบัน();  bind prevLine = 0.0 - 1.0;  bind flashAmt = 0.0;  bind purified = 0;  ถ้า musicOn == 1 { เล่นเพลง(เพลงหลัก) } }
                ถ้า sel == 2 { bind สถานะ = ST_ARCADE;  bind sel = 0 }
                ถ้า sel == 3 { bind สถานะ = ST_LEVELSLCT;  bind selTarget = ด่านปลดล็อก - 1.0;  ถ้า selTarget < 0.0 { bind selTarget = 0.0 };  ถ้า selTarget > 11.0 { bind selTarget = 11.0 } }   # เปิดมาที่ด่านปัจจุบัน
                ถ้า sel == 4 { bind สถานะ = ST_MP;  bind sel = 0 }
                ถ้า sel == 5 {
                    bind hostPass = "";  bind joinPass = "";  bind authSent = 0.0;  bind coopRole = 0;  bind vsโหมด = 0;  bind directJoin = 0.0
                    พิมพ์เขียว("  🌐 Online city shard: searching LAN for an existing shard...")
                    พิมพ์ฟ้า(เน็ตทดสอบ(netPort))
                    bind onlineSearchT = 0.0
                    bind สถานะ = ST_ONLINE_SEARCH;  bind sel = 0
                }
                ถ้า sel == 6 { bind optReturn = ST_MENU;  bind สถานะ = ST_OPT;  bind sel = 0 }
                ถ้า sel == 7 { bind langReturn = ST_MENU;  bind langTarget = 0;  bind สถานะ = ST_LANG;  bind sel = ภาษา }   # ตั้งภาษารวม: UI + พากย์ + ซับ
                ถ้า sel == 8 { bind สถานะ = ST_TITLE;  bind sel = 0;  ถ้า musicOn == 1 { เล่นเพลง(เพลงหลัก) } }   # "exit" → กลับหน้าจอไตเติล
            }
            ถ้า นำ("escape", gNavB) { bind สถานะ = ST_TITLE;  ถ้า musicOn == 1 { เล่นเพลง(เพลงหลัก) } }
            # F12 bonus level trigger — เข้าคัตซีนโบนัส (โคด้า) ก่อน แล้วค่อยเข้าสนามจริง
            ถ้า กดปุ่ม("f12") {
                หยุดเพลง()
                bind สถานะ = ST_BONUSCINE
                bind บทโบนัสเริ่ม = เวลาปัจจุบัน()
                bind บทโบนัสบรรทัดก่อน = 0.0 - 1.0
                bind bonusCX = 10.0;  bind bonusCY = -5.0;  bind bonusCZ = 0.0
                bind bonusVX = 0.0;  bind bonusVY = 0.0;  bind bonusVZ = 0.0
                bind bonusGrounded = 1.0;  bind bonusSquash = 0.0
                bind bonusLiq = liquid_new(16.0, 16.0)
                liquid_rainbow(bonusLiq, 1.0)
                ถ้า musicOn == 1 { เล่นเพลง(เพลงเซราฟ);  ระดับเพลง(ปริมาณBGM * 0.85) }
                พิมพ์เหลือง("  ★ a secret door creaks open...")
            }
        }

        # ── ARCADE: ปรับแต่งลูก (3 สี/สแตมป์/แบนด์/ชื่อ) → สู้กอนต์เลต ──
        ถ้า สถานะ == ST_ARCADE {
            text_poll()                                  # รับการพิมพ์ชื่อ
            ถ้า นำ("up", gNavU)   { bind ปรับField = ปรับField - 1.0;  ถ้า ปรับField < 0.0 { bind ปรับField = 6.0 } }
            ถ้า นำ("down", gNavD) { bind ปรับField = ปรับField + 1.0;  ถ้า ปรับField > 6.0 { bind ปรับField = 0.0 } }
            ถ้า นำ("left", gNavL) {
                ถ้า ปรับField == 0.0 { bind ฮิวBody  = ฮิวBody - 0.04;   ถ้า ฮิวBody < 0.0 { bind ฮิวBody = ฮิวBody + 1.0 } }
                ถ้า ปรับField == 1.0 { bind ฮิวStamp = ฮิวStamp - 0.04;  ถ้า ฮิวStamp < 0.0 { bind ฮิวStamp = ฮิวStamp + 1.0 } }
                ถ้า ปรับField == 2.0 { bind ฮิวBand  = ฮิวBand - 0.04;   ถ้า ฮิวBand < 0.0 { bind ฮิวBand = ฮิวBand + 1.0 } }
                ถ้า ปรับField == 3.0 { bind ปรับStamp = ปรับStamp - 1.0;  ถ้า ปรับStamp < 0.0 { bind ปรับStamp = STAMP_N - 1.0 } }
                ถ้า ปรับField == 4.0 { bind ปรับBand = ปรับBand - 1.0;   ถ้า ปรับBand < 0.0 { bind ปรับBand = BAND_N - 1.0 } }
                ถ้า ปรับField == 6.0 { bind ปรับFX = ปรับFX - 1.0;  ถ้า ปรับFX < 0.0 { bind ปรับFX = 3.0 } }
            }
            ถ้า นำ("right", gNavR) {
                ถ้า ปรับField == 0.0 { bind ฮิวBody  = ฮิวBody + 0.04;   ถ้า ฮิวBody > 1.0 { bind ฮิวBody = ฮิวBody - 1.0 } }
                ถ้า ปรับField == 1.0 { bind ฮิวStamp = ฮิวStamp + 0.04;  ถ้า ฮิวStamp > 1.0 { bind ฮิวStamp = ฮิวStamp - 1.0 } }
                ถ้า ปรับField == 2.0 { bind ฮิวBand  = ฮิวBand + 0.04;   ถ้า ฮิวBand > 1.0 { bind ฮิวBand = ฮิวBand - 1.0 } }
                ถ้า ปรับField == 3.0 { bind ปรับStamp = ปรับStamp + 1.0;  ถ้า ปรับStamp > STAMP_N - 1.0 { bind ปรับStamp = 0.0 } }
                ถ้า ปรับField == 4.0 { bind ปรับBand = ปรับBand + 1.0;   ถ้า ปรับBand > BAND_N - 1.0 { bind ปรับBand = 0.0 } }
                ถ้า ปรับField == 6.0 { bind ปรับFX = ปรับFX + 1.0;  ถ้า ปรับFX > 3.0 { bind ปรับFX = 0.0 } }
            }
            bind ลูกชื่อ = text_get()
            ถ้า ความยาว(ลูกชื่อ) < 1 { bind ลูกชื่อ = "player" }

            ตั้งการแรเงา(1)                            #  cel — ไม่ใช้แรเงาโฮโล (ชีนรุ้ง/ริมไซแอน) ในหน้าคัสตอม
            ตั้งขอบเรือง(0.0, 0, 0, 0)                 #  ปิดริมไลต์
            วาดพื้นหลังเมนู(กรอบ)
            ตั้งแสงรอบข้าง(1.0)
            bind cbody  = สีจากเฉด(ฮิวBody)
            bind cstamp = สีจากเฉด(ฮิวStamp)
            bind cband  = สีจากเฉด(ฮิวBand)
            วาดลูกแต่ง(7.5, 0.0, 0.0, 3.6,
                list_get(cbody,0),list_get(cbody,1),list_get(cbody,2),
                list_get(cstamp,0),list_get(cstamp,1),list_get(cstamp,2),
                list_get(cband,0),list_get(cband,1),list_get(cband,2),
                floor(ปรับStamp), floor(ปรับBand), t, floor(ปรับFX))
            โหมดผสม(1)
            ui_text_color(0.0, 1.0, t);  วาดสตริง("customize", 0.0-8.5, 0.0-8.5, 0.0, 0.85, 1)
            bind r0 = 0.0 - 4.5
            ถ้า ปรับField == 0.0 { ui_text_color(1.0, 1.0, t) }  มิฉะนั้น { ui_text_color(1.0, 0.0, t) }
            วาดสตริง("body", 0.0-13.5, r0, 0.0, 0.6, 1)
            วาดสว็อตช์(0.0-7.5, r0, 0.55, list_get(cbody,0),list_get(cbody,1),list_get(cbody,2))
            ถ้า ปรับField == 1.0 { ui_text_color(2.0, 1.0, t) }  มิฉะนั้น { ui_text_color(2.0, 0.0, t) }
            วาดสตริง("stamp col", 0.0-13.5, r0+1.6, 0.0, 0.6, 1)
            วาดสว็อตช์(0.0-7.5, r0+1.6, 0.55, list_get(cstamp,0),list_get(cstamp,1),list_get(cstamp,2))
            ถ้า ปรับField == 2.0 { ui_text_color(3.0, 1.0, t) }  มิฉะนั้น { ui_text_color(3.0, 0.0, t) }
            วาดสตริง("band col", 0.0-13.5, r0+3.2, 0.0, 0.6, 1)
            วาดสว็อตช์(0.0-7.5, r0+3.2, 0.55, list_get(cband,0),list_get(cband,1),list_get(cband,2))
            ถ้า ปรับField == 3.0 { ui_text_color(4.0, 1.0, t) }  มิฉะนั้น { ui_text_color(4.0, 0.0, t) }
            วาดสตริง("stamp", 0.0-13.5, r0+4.8, 0.0, 0.6, 1)
            ถ้า ปรับField == 4.0 { ui_text_color(5.0, 1.0, t) }  มิฉะนั้น { ui_text_color(5.0, 0.0, t) }
            วาดสตริง("band", 0.0-13.5, r0+6.4, 0.0, 0.6, 1)
            ถ้า ปรับField == 5.0 { ui_text_color(6.0, 1.0, t) }  มิฉะนั้น { ui_text_color(6.0, 0.0, t) }
            วาดสตริง("name", 0.0-13.5, r0+8.0, 0.0, 0.6, 1)
            ui_text_color(9.0, 1.0, t);  วาดสตริง(ลูกชื่อ, 0.0-8.5, r0+8.0, 0.0, 0.6, 1)
            ถ้า ปรับField == 6.0 { ui_text_color(7.0, 1.0, t) }  มิฉะนั้น { ui_text_color(7.0, 0.0, t) }
            วาดสตริง("effect", 0.0-13.5, r0+9.6, 0.0, 0.6, 1)
            bind fxnm = "none"
            ถ้า ปรับFX == 1.0 { bind fxnm = "spin" };  ถ้า ปรับFX == 2.0 { bind fxnm = "pulse" };  ถ้า ปรับFX == 3.0 { bind fxnm = "orbit" }
            ui_text_color(10.0, 1.0, t);  วาดสตริง(fxnm, 0.0-8.5, r0+9.6, 0.0, 0.6, 1)
            ui_text_color(11.0, 0.0, t);  วาดสตริง("enter to fight", 0.0-8.5, 10.2, 0.0, 0.55, 1)
            flush_3d();  โหมดผสม(0)
            แสดงผล()

            ถ้า นำ("enter", gNavA) { bind สถานะ = ST_STAGESEL;  bind stageSel = 0.0 }   # → เลือกสเตจ + ความยาก
            ถ้า นำ("escape", gNavB) { bind สถานะ = ST_MENU;  bind sel = 2;  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }
        }

        # ── STAGE SELECT: เลือกสเตจ (สี) + เส้นโค้งความยาก AI + พรีวิวกล้องแพน ──
        ถ้า สถานะ == ST_STAGESEL {
            ถ้า นำ("left", gNavL)  { bind stageSel = stageSel - 1.0;  ถ้า stageSel < 0.0 { bind stageSel = 3.0 } }
            ถ้า นำ("right", gNavR) { bind stageSel = stageSel + 1.0;  ถ้า stageSel > 3.0 { bind stageSel = 0.0 } }
            ถ้า นำ("up", gNavU)    { bind aiDiff = aiDiff + 1.0;  ถ้า aiDiff > 2.0 { bind aiDiff = 0.0 } }
            ถ้า นำ("down", gNavD)  { bind aiDiff = aiDiff - 1.0;  ถ้า aiDiff < 0.0 { bind aiDiff = 2.0 } }
            bind ฉากเฉด = stageSel / 4.0
            # ── พรีวิวสเตจ 3D + กล้องแพนรอบ ──
            เติม(5, 6, 14)
            ตั้งแสงรอบข้าง(0.75)
            bind pan = t * 0.25
            ตั้งกล้องมอง(โคไซน์(pan)*150.0, ARENA_FLOOR-70.0, ไซน์(pan)*150.0, 0.0, ARENA_FLOOR-10.0, 0.0)
            ตั้งระยะห่าง(2.4)
            วาดอารีนา(t, 1.0, ฉากเฉด)
            # ── UI ทับ (การ์ดสเตจ + ความยาก) ──
            ตั้งกล้องมอง(0.0, 0.0, 0.0-22.0, 0.0, 0.0, 0.0)
            ตั้งระยะห่าง(2.8)
            โหมดผสม(1)
            สีดินสอ(235, 235, 255);  วาดสตริง("select stage", 0.0, 0.0-9.5, 0.0, 0.9, 1)
            bind sc4 = 0
            ขณะที่ sc4 < 4 {
                bind shc = สีจากเฉด(sc4 / 4.0)
                bind sxp = 0.0 - 9.0 + sc4*6.0
                วาดสว็อตช์(sxp, 0.0-6.5, 1.6, list_get(shc,0), list_get(shc,1), list_get(shc,2))
                ถ้า sc4 == floor(stageSel) {
                    สีดินสอ(255, 255, 150)
                    bind ek=0
                    ขณะที่ ek < 16 { bind e0=ek*0.3927; bind e1=(ek+1)*0.3927;  วาดเส้น3มิติ(sxp+โคไซน์(e0)*2.1, 0.0-6.5+ไซน์(e0)*2.1, 0.0, sxp+โคไซน์(e1)*2.1, 0.0-6.5+ไซน์(e1)*2.1, 0.0);  bind ek=ek+1 }
                }
                bind sc4 = sc4 + 1
            }
            bind dnm = "normal";  ถ้า aiDiff == 0.0 { bind dnm = "easy" };  ถ้า aiDiff == 2.0 { bind dnm = "hard" }
            สีดินสอ(255, 220, 120);  วาดสตริง("difficulty", 0.0-4.0, 1.5, 0.0, 0.6, 1)
            สีดินสอ(200, 235, 255);  วาดสตริง(dnm, 4.0, 1.5, 0.0, 0.7, 1)
            สีดินสอ(120, 140, 180);  วาดสตริง("left/right stage  up/down difficulty  enter fight", 0.0, 9.5, 0.0, 0.5, 1)
            flush_3d();  โหมดผสม(0)
            แสดงผล()
            ถ้า นำ("enter", gNavA) {
                bind อาร์เคดโหมด = 1;  bind รอบ = 1.0;  bind บอสโหมด = 0;  bind vsโหมด = 0;  bind ด่าน = 0
                bind อาร์เคดไลฟ์ = 3.0;  bind อาร์เคดแต้ม = 0.0;  bind อาร์เคดชนะ = 0.0;  bind ผลพิมพ์ = 0.0;  bind arcContinueT = 0.0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 0.0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                หยุดเพลง()
                bind สถานะ = ST_PLAY
                ถ้า musicOn == 1 { เล่นเพลง(เพลงต่อสู้);  ระดับเพลง(ปริมาณBGM) }
            }
            ถ้า นำ("escape", gNavB) { bind สถานะ = ST_ARCADE }
        }

        # ── ARCADE RESULT: ตีรวมแต้ม (คอนโซลสี + จอ) — แพ้ + arcContinueT>0 เสนอ [enter] ต่อกลับเข้าสู้รอบเดิม ──
        ถ้า สถานะ == ST_ARCRESULT {
            bind arcOfferContinue = 0.0
            ถ้า อาร์เคดชนะ < 0.5 { ถ้า arcContinueT > 0.0 { bind arcOfferContinue = 1.0 } }
            ถ้า ผลพิมพ์ < 0.5 {
                bind ฟอย = floor(รอบ) - 1;  ถ้า ฟอย > 10 { bind ฟอย = 10 }
                ถ้า อาร์เคดชนะ > 0.5 { พิมพ์เหลือง("════ ARCADE CLEARED! ════") }  มิฉะนั้น { พิมพ์แดง("════ GAME OVER ════") }
                พิมพ์ฟ้า("  foes defeated: " + to_str(ฟอย) + " / 10")
                พิมพ์เขียว("  POINTS: " + to_str(floor(อาร์เคดแต้ม)))
                ถ้า arcOfferContinue > 0.5 { พิมพ์เหลือง("  press enter within 10s to continue") }
                bind ผลพิมพ์ = 1.0
            }
            วาดพื้นหลังเมนู(กรอบ)
            ตั้งแสงรอบข้าง(1.0)
            โหมดผสม(1)
            # ── ฉากชนะ/แพ้: รังสีหมุน + อนุภาค (ชนะ=ทอง · แพ้=แดง · เสนอต่อ=แดงสว่างขึ้น+หมุนไวขึ้น เร่งด่วน) ──
            bind rcr = 220;  bind rcg = 70;  bind rcb = 70
            bind rspin = 0.5
            ถ้า อาร์เคดชนะ > 0.5 { bind rcr = 255;  bind rcg = 215;  bind rcb = 90 }
            ถ้า arcOfferContinue > 0.5 { bind rcr = 255;  bind rcg = 90;  bind rcb = 90;  bind rspin = 0.5 + (1.0 - arcContinueT/10.0)*1.6 }
            bind rk = 0
            ขณะที่ rk < 18 {
                bind ra = rk*0.3491 + t*rspin
                bind rl = 5.5 + ไซน์(t*2.2 + rk)*1.8
                สีดินสอ(floor(rcr*0.45), floor(rcg*0.45), floor(rcb*0.45))
                วาดเส้น3มิติ(0.0, 0.0-3.0, 2.5,  โคไซน์(ra)*rl, 0.0-3.0+ไซน์(ra)*rl, 2.5)
                bind rk = rk + 1
            }
            ถ้า particlesOn > 0.5 { orb_particles(0.0, 0.0-3.0, 1.5, 3.2, 70.0, t*1.4, rcr, rcg, rcb) }
            ถ้า อาร์เคดชนะ > 0.5 { สีดินสอ(255, 230, 120);  วาดสตริง("victory", 0.0, 0.0-3.0, 0.0, 1.4, 1) }
            มิฉะนั้น { สีดินสอ(255, 120, 120);  วาดสตริง("game over", 0.0, 0.0-3.0, 0.0, 1.4, 1) }
            สีดินสอ(200, 210, 245);  วาดสตริง("foes defeated", 0.0, 0.0+0.5, 0.0, 0.6, 1)
            bind fdone = floor(รอบ) - 1;  ถ้า fdone > 10 { bind fdone = 10 }
            bind pk = 0
            ขณะที่ pk < fdone {
                สีดินสอ(240, 200, 80)
                วาดสามเหลี่ยม3มิติ(0.0-5.0+pk*1.0, 2.2, 0.0,  0.0-4.5+pk*1.0, 2.2, 0.0,  0.0-4.75+pk*1.0, 2.9, 0.0)
                bind pk = pk + 1
            }
            ถ้า arcOfferContinue > 0.5 {
                bind arcContinueT = arcContinueT - 0.016
                bind ccPulse = 0.85 + ไซน์(t*4.0)*0.14
                สีดินสอ(floor(255.0*ccPulse), floor(210.0*ccPulse), floor(90.0*ccPulse))
                วาดสตริง("continue? " + to_str(floor(arcContinueT) + 1.0), 0.0, 5.6, 0.0, 0.75, 1)
                สีดินสอ(150, 220, 165);  วาดสตริง("press enter to continue", 0.0, 6.4, 0.0, 0.5, 1)
                สีดินสอ(150, 160, 200);  วาดสตริง("press esc to bank points", 0.0, 6.95, 0.0, 0.4, 1)
                ถ้า arcContinueT <= 0.0 { bind arcContinueT = 0.0 }
            }
            มิฉะนั้น {
                สีดินสอ(150, 160, 200);  วาดสตริง("press enter for points", 0.0, 6.5, 0.0, 0.55, 1)
            }
            flush_3d();  โหมดผสม(0)
            แสดงผล()
            ถ้า นำ("enter", gNavA)  {
                ถ้า arcOfferContinue > 0.5 {
                    bind อาร์เคดไลฟ์ = 3.0;  bind arcContinueT = 0.0;  bind ผลพิมพ์ = 0.0
                    ถ้า musicOn == 1 { เล่นเพลง(เพลงต่อสู้);  ระดับเพลง(ปริมาณBGM) }
                    bind รีเซ็ต = 1;  bind สถานะ = ST_PLAY
                }
                มิฉะนั้น {
                    ถ้า hsInserted < 0.5 {
                        bind hnm = ลูกชื่อ;  ถ้า hnm == "" { bind hnm = "player" }
                        bind hres = แทรกไฮสกอร์(hsNames, hsScores, hnm, floor(อาร์เคดแต้ม))
                        bind hsNames = list_get(hres, 0);  bind hsScores = list_get(hres, 1)
                        บันทึกไฮสกอร์(hsNames, hsScores);  bind hsInserted = 1.0
                    }
                    หยุดเพลง()
                    bind ผลพิมพ์ = 0.0;  bind สถานะ = ST_HISCORE
                }
            }
            ถ้า นำ("escape", gNavB) { bind อาร์เคดโหมด = 0;  bind arcContinueT = 0.0;  bind ผลพิมพ์ = 0.0;  bind สถานะ = ST_MENU;  bind sel = 2;  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }
        }

        # ── STORY GAME OVER: ตาย → [enter] ต่อ กลับด่าน/บอสเดิม (บอสเลือดคงเดิม เพราะ รีเซ็ต ไม่แตะ) หรือ [esc] บันทึก+ออกเมนู ──
        ถ้า สถานะ == ST_DEATH {
            วาดพื้นหลังเมนู(กรอบ)
            ตั้งแสงรอบข้าง(1.0)
            ตั้งกล้องมอง(0.0, 0.0, -22.0, 0.0, 0.0, 0.0)
            ตั้งระยะห่าง(2.8)
            โหมดผสม(1)
            bind dcr = 210;  bind dcg = 55;  bind dcb = 55
            bind dk = 0
            ขณะที่ dk < 18 {
                bind da = dk*0.3491 - t*0.35
                bind dl = 5.2 + ไซน์(t*1.6 + dk)*1.4
                สีดินสอ(floor(dcr*0.4), floor(dcg*0.4), floor(dcb*0.4))
                วาดเส้น3มิติ(0.0, 0.0-3.0, 2.5,  โคไซน์(da)*dl, 0.0-3.0+ไซน์(da)*dl, 2.5)
                bind dk = dk + 1
            }
            ถ้า particlesOn > 0.5 { orb_particles(0.0, 0.0-3.0, 1.5, 2.6, 55.0, t*0.85, dcr, dcg, dcb) }
            bind dps = 1.5 + ไซน์(t*2.4)*0.09
            สีดินสอ(235, 90, 90);  วาดสตริง("you died", 0.0, 0.0-3.0, 0.0, dps, 1)
            สีดินสอ(200, 210, 245)
            ถ้า บอสโหมด > 0.5 { วาดสตริง("boss fight - level " + to_str(floor(ด่าน)), 0.0, 0.0+0.5, 0.0, 0.5, 1) }
            มิฉะนั้น { วาดสตริง("level " + to_str(floor(ด่าน)), 0.0, 0.0+0.5, 0.0, 0.5, 1) }
            bind dpp = 0.85 + ไซน์(t*3.2)*0.08
            สีดินสอ(floor(150.0*dpp), floor(230.0*dpp), floor(165.0*dpp));  วาดสตริง("press enter to continue", 0.0, 6.0, 0.0, 0.55, 1)
            สีดินสอ(150, 160, 200);  วาดสตริง("press esc for menu", 0.0, 6.6, 0.0, 0.42, 1)
            flush_3d();  โหมดผสม(0)
            แสดงผล()
            ถ้า นำ("enter", gNavA) { bind รีเซ็ต = 1;  bind สถานะ = ST_PLAY }
            ถ้า นำ("escape", gNavB) {
                เซฟสเตตัส(สกิล, ผู้เล่นสเตตัส, ฟีนิกซ์ดาวน์)
                หยุดเพลง();  bind บอสโหมด = 0;  bind สถานะ = ST_MENU;  bind sel = 0
                ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) }
            }
        }

        # ── HIGH SCORES (หลัง game over + ไอเดิลเมนู) ──
        ถ้า สถานะ == ST_HISCORE {
            วาดพื้นหลังเมนู(กรอบ)
            ตั้งแสงรอบข้าง(1.0)
            ตั้งกล้องมอง(0.0, 0.0, -22.0, 0.0, 0.0, 0.0)
            ตั้งระยะห่าง(2.8)
            วาดเอไออารีนาพื้นหลัง(t)                          # 4 ออร์บ AI สู้กันหลังตาราง
            วาดไฮสกอร์(hsNames, hsScores, t)
            โหมดผสม(1);  สีดินสอ(150, 160, 200);  วาดสตริง("press enter", 0.0, 7.4, 0.0, 0.5, 1);  flush_3d();  โหมดผสม(0)
            แสดงผล()
            ถ้า นำ("enter", gNavA)  { bind อาร์เคดโหมด = 0;  bind hsInserted = 0.0;  bind สถานะ = ST_MENU;  bind sel = 2;  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }
            ถ้า นำ("escape", gNavB) { bind อาร์เคดโหมด = 0;  bind hsInserted = 0.0;  bind สถานะ = ST_MENU;  bind sel = 2;  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }
            ถ้า นำ("up", gNavU)     { bind สถานะ = ST_MENU;  bind sel = 2;  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }
            ถ้า นำ("down", gNavD)   { bind สถานะ = ST_MENU;  bind sel = 2;  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }
        }

        # ── LEVEL SELECT ────────────────────────────────────────────────────────
        ถ้า สถานะ == ST_LEVELSLCT {
            # ลูกเต๋า 12 หน้า: ←/→ เลือกด่าน → เต๋าหมุนมาหาหน้านั้น (เลือกด่านปัจจุบันเมื่อเปิด)
            ถ้า นำ("left", gNavL)  { bind selTarget = selTarget - 1.0;  ถ้า selTarget < 0.0 { bind selTarget = 11.0 } }
            ถ้า นำ("right", gNavR) { bind selTarget = selTarget + 1.0;  ถ้า selTarget > 11.0 { bind selTarget = 0.0 } }
            ถ้า นำ("up", gNavU)    { bind selTarget = selTarget - 1.0;  ถ้า selTarget < 0.0 { bind selTarget = 11.0 } }
            ถ้า นำ("down", gNavD)  { bind selTarget = selTarget + 1.0;  ถ้า selTarget > 11.0 { bind selTarget = 0.0 } }
            bind tgt = เป้าหมุนเต๋า(selTarget)                # (yaw,pitch) ที่หน้านั้นหันเข้ากล้อง
            bind dYaw = list_get(tgt,0) - dieYaw;  ถ้า dYaw > 3.14159 { bind dYaw = dYaw - 6.2832 };  ถ้า dYaw < 0.0-3.14159 { bind dYaw = dYaw + 6.2832 }
            bind dieYaw = dieYaw + dYaw*0.16                  # เอียงเข้าหาเป้า (ทางสั้น)
            bind diePitch = diePitch + (list_get(tgt,1) - diePitch)*0.16
            bind sel = selTarget
            วาดพื้นหลังเมนู(กรอบ)
            วาดลูกเต๋าด่าน(dieYaw, diePitch, t, ด่านปลดล็อก, selTarget)
            แสดงผล()
            ถ้า นำ("enter", gNavA) { ถ้า (sel + 1) <= ด่านปลดล็อก {     # หน้า i → ด่าน i+1 (1–12)
                bind ด่าน = sel + 1
                bind สกิลเริ่ม = 0.0
                bind startX = 128.0;  bind startY = 0.0-2.0;  bind startZ = 128.0   # proc แลตทิซ: เกิดกลางห้อง (0,0)
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0;  bind บอสโหมด = 0   # เข้าด่านใหม่เสมอเริ่มสด ไม่ใช่ค้างสนามบอสจากรอบก่อน
                หยุดเพลง()
                bind สถานะ = ST_PLAY
            }  มิฉะนั้น { พิมพ์แดง("  🔒 ด่านยังไม่ปลดล็อก — เล่นด่านก่อนหน้าให้ผ่านก่อน") } }
            ถ้า นำ("escape", gNavB) { bind สถานะ = ST_MENU;  bind sel = 3 }
        }

        # ── ONLINE SEARCH: LAN-discover an existing "mmo" city shard for ~1s;      ──
        #    join it if found, else become the host — so a party on one LAN lands
        #    in one shared shard instead of everyone hosting their own.
        ถ้า สถานะ == ST_ONLINE_SEARCH {
            bind onlineSearchT = onlineSearchT + (1.0/60.0)
            bind dlist = เน็ตค้นหา(discPort)
            bind dlines = แยก(dlist, "\n")
            bind foundIP = ""
            bind di = 0
            ขณะที่ di < ความยาว(dlines) {
                bind dl = list_get(dlines, di)
                ถ้า ความยาว(dl) > 2 {
                    bind df = แยก(dl, "|")
                    ถ้า ความยาว(df) >= 3 { ถ้า list_get(df, 2) == "mmo" { ถ้า foundIP == "" { bind foundIP = list_get(df, 0) } } }
                }
                bind di = di + 1
            }
            วาดพื้นหลังเมนู(กรอบ)
            ตั้งกล้องมอง(0.0, 0.0, -22.0, 0.0, 0.0, 0.0);  ตั้งระยะห่าง(2.8)
            โหมดผสม(1)
            สีดินสอ(120, 200, 255);  วาดสตริง("online city shard", 0.0, 0.0-1.0, 0.0, 0.9, 1)
            สีดินสอ(150, 160, 200);  วาดสตริง("searching lan", 0.0, 0.5, 0.0, 0.55, 1)
            flush_3d();  โหมดผสม(0)
            แสดงผล()
            ถ้า foundIP != "" {
                bind coopRole = 1;  bind owIsHost = 0.0;  เน็ตจอย(foundIP, netPort)
                bind owBundle = รีเซ็ตโลกออนไลน์();  หยุดเพลง()
                ถ้า musicOn == 1 { เล่นเพลง(เพลงออนไลน์);  ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_ONLINE
            }  มิฉะนั้น { ถ้า onlineSearchT > 1.2 {
                bind coopRole = 0;  bind owIsHost = 1.0
                เน็ตโฮสต์(netPort);  เน็ตประกาศ(discPort, ลูกชื่อ + "|mmo|city|0")
                bind owBundle = รีเซ็ตโลกออนไลน์();  หยุดเพลง()
                ถ้า musicOn == 1 { เล่นเพลง(เพลงออนไลน์);  ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_ONLINE
            } }
            ถ้า นำ("escape", gNavB) { bind สถานะ = ST_MENU;  bind sel = 5 }
        }

        # ── ONLINE: โลกผังกระเบื้อง 16×16 ที่งอก/ผุตามเวลาจริง (datetime) ──────────
        ถ้า สถานะ == ST_ONLINE {
            ถ้า midiClockOn > 0.5 { ถ้า ยาวเพลงออนไลน์ > 1.0 { ถ้า ตำแหน่งเพลง() >= ยาวเพลงออนไลน์ - 0.5 { ค้นหาเพลง(0);  ถ้า musicOn == 1 { เล่นเพลง(เพลงออนไลน์) } } } }
            bind owBundle = วาดโลกออนไลน์(t, ลูกชื่อ, เน็ตสถานะ(), owIsHost, owBundle)
            แสดงผล()
            ถ้า นำ("escape", gNavB) { เน็ตหยุดประกาศ();  หยุดเพลง();  bind สถานะ = ST_MENU;  bind sel = 5;  ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) } }
        }

        # ── LOAD SAVE ─────────────────────────────────────────────────────────
        ถ้า สถานะ == ST_LOAD {
            ถ้า นำ("up", gNavU)   { bind sel = sel - 1;  ถ้า sel < 0 { bind sel = 2 } }
            ถ้า นำ("down", gNavD) { bind sel = sel + 1;  ถ้า sel > 2 { bind sel = 0 } }
            วาดพื้นหลังเมนู(กรอบ)
            วาดป้าย(ภาษา, 7, 0.0, 0.0-6.0, 1.0)                       # "load save" (localized)
            วาดเมนูภาษา([8, 8, 8], sel, ภาษา, 0.0-2.2, 2.2, 0.7, t)    # 3× "slot"
            # สถานะช่อง (saved / empty) — localized
            ถ้า slot0Data == "empty" { สีดินสอ(90,90,90);  วาดป้าย(ภาษา, 9, 6.5, 0.0-2.2, 0.55) }  มิฉะนั้น { สีดินสอ(80,200,130);  วาดป้าย(ภาษา, 10, 6.5, 0.0-2.2, 0.55) }
            ถ้า slot1Data == "empty" { สีดินสอ(90,90,90);  วาดป้าย(ภาษา, 9, 6.5, 0.0, 0.55) }      มิฉะนั้น { สีดินสอ(80,200,130);  วาดป้าย(ภาษา, 10, 6.5, 0.0, 0.55) }
            ถ้า slot2Data == "empty" { สีดินสอ(90,90,90);  วาดป้าย(ภาษา, 9, 6.5, 2.2, 0.55) }      มิฉะนั้น { สีดินสอ(80,200,130);  วาดป้าย(ภาษา, 10, 6.5, 2.2, 0.55) }
            # ไอคอนดาบ ถ้าช่องนั้นได้ดาบแล้ว
            ถ้า ช่องมีดาบ(slot0Data) > 0 { วาดไอคอนดาบ(9.5, 0.0-2.2) }
            ถ้า ช่องมีดาบ(slot1Data) > 0 { วาดไอคอนดาบ(9.5, 0.0) }
            ถ้า ช่องมีดาบ(slot2Data) > 0 { วาดไอคอนดาบ(9.5, 2.2) }
            flush_3d()
            แสดงผล()
            ถ้า นำ("enter", gNavA) {
                bind currentSlot = sel
                bind data = slot0Data
                ถ้า sel == 1 { bind data = slot1Data }
                ถ้า sel == 2 { bind data = slot2Data }
                bind startX = 0.0;  bind startY = 0.0 - 2.0;  bind startZ = 0.0
                bind สกิลเริ่ม = 0.0;  bind ด่าน = 0
                ถ้า data == "empty" { bind startX = 0.0 }
                มิฉะนั้น {
                    bind pp = แยก(data, ",")
                    bind startX = list_get(pp, 0) * 1.0
                    bind startY = list_get(pp, 1) * 1.0
                    bind startZ = list_get(pp, 2) * 1.0
                    ถ้า ความยาว(pp) > 3 { bind สกิลเริ่ม = list_get(pp, 3) * 1.0 }
                }
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                หยุดเพลง()
                bind สถานะ = ST_PLAY
            }
            ถ้า นำ("escape", gNavB) { bind สถานะ = ST_MENU;  bind sel = 0 }
        }

        # ── NEW GAME (cinematic) ──────────────────────────────────────────────
        ถ้า สถานะ == ST_NEW {
            เติม(2, 1, 6)
            ตั้งแสงรอบข้าง(0.5)
            ตั้งกล้องมอง(0.0, 0.0, -22.0, 0.0, 0.0, 0.0)
            ตั้งระยะห่าง(2.8)
            โหมดผสม(1)
            สีดินสอ(180, 160, 220)
            วาดข้อความ(คำ_บทหนึ่ง, 0.0, 0.0-1.0, 0.0, 1.2, ภาษา)
            ถ้า (floor(กรอบ * 0.04) % 2) == 0 {
                สีดินสอ(220, 220, 255)
                วาดข้อความ(คำ_ปุ่ม, 0.0, 4.0, 0.0, 0.6, ภาษา)
            }
            flush_3d()
            โหมดผสม(0)
            แสดงผล()
            ถ้า นำ("enter", gNavA) {
                bind currentSlot = 0
                bind startX = 0.0;  bind startY = 0.0 - 2.0;  bind startZ = 0.0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                หยุดเพลง()
                bind สถานะ = ST_PLAY
            }
            ถ้า นำ("escape", gNavB) { bind สถานะ = ST_MENU;  bind sel = 0 }
        }

        # ── MULTIPLAYER ───────────────────────────────────────────────────────
        ถ้า สถานะ == ST_MP {
            ถ้า นำ("up", gNavU)   { bind sel = sel - 1;  ถ้า sel < 0 { bind sel = 1 } }
            ถ้า นำ("down", gNavD) { bind sel = sel + 1;  ถ้า sel > 1 { bind sel = 0 } }
            ถ้า นำ("left", gNavL)  { bind vsโหมด = 0 }     # ←/→ เลือกโหมด
            ถ้า นำ("right", gNavR) { bind vsโหมด = 1 }
            ถ้า กดปุ่ม("p")     { bind wantPriv = 1.0 - wantPriv }   # P: สลับเซิร์ฟส่วนตัว
            วาดพื้นหลังเมนู(กรอบ)
            วาดข้อความ(คำ_มัลติ, 0.0, 0.0-6.5, 0.0, 1.0, ภาษา)
            วาดเมนู(เมนูมัลติ, sel, 0.9, 2.2, t, ภาษา)   # Host Game / Join Game (ลอบบี้)
            # ── โหมด (←/→): co-op หรือ VS ──
            ถ้า vsโหมด == 1 { สีดินสอ(255, 120, 120);  วาดข้อความ(คำ_วีเอส, 0.0, 4.2, 0.0, 0.6, 1) }
            มิฉะนั้น { สีดินสอ(120, 200, 255);  วาดข้อความ(คำ_โหมดโคออป, 0.0, 4.2, 0.0, 0.6, 1) }
            สีดินสอ(100, 115, 150)
            วาดข้อความ(คำ_โคออป, 0.0, 5.5, 0.0, 0.4, 1)
            ถ้า wantPriv > 0.5 { สีดินสอ(255, 200, 120);  วาดสตริง("private p", 0.0, 6.4, 0.0, 0.45, 1) }
            มิฉะนั้น { สีดินสอ(110, 130, 160);  วาดสตริง("public p", 0.0, 6.4, 0.0, 0.45, 1) }
            flush_3d()
            แสดงผล()
            ถ้า นำ("enter", gNavA) {
                bind coopRole = sel
                ถ้า sel == 0 {
                    ถ้า wantPriv > 0.5 { text_clear();  bind สถานะ = ST_HOSTPW }        # ส่วนตัว → ตั้งรหัสก่อน
                    มิฉะนั้น {
                        bind hostPass = "";  bind authSent = 0.0
                        พิมพ์เขียว("  🌐 Host self-test:")
                พิมพ์ฟ้า(เน็ตทดสอบ(netPort))
                พิมพ์เหลือง("  external play: port-forward TCP " + to_str(netPort) + " to this PC + allow it in the firewall; joiners use your PUBLIC IP (verify with an online port checker). On CGNAT ISPs port-forward can't work — use Tailscale/Hamachi.")
                เน็ตโฮสต์(netPort)
                        bind hMode = "coop";  ถ้า vsโหมด == 1 { bind hMode = "vs" }
                        เน็ตประกาศ(discPort, ลูกชื่อ + "|" + hMode + "|arena|0")
                        bind สถานะ = ST_LOBBY
                    }
                }
                มิฉะนั้น { bind srvSel = 0;  bind สถานะ = ST_JOINLIST }                  # Join: เปิดรายการเซิร์ฟเวอร์
            }
            ถ้า นำ("escape", gNavB) { bind สถานะ = ST_MENU;  bind sel = 4 }
        }

        # ── HOST: ตั้งรหัสผ่านเซิร์ฟส่วนตัว (พิมพ์ → Enter) ──
        ถ้า สถานะ == ST_HOSTPW {
            text_poll()
            วาดพื้นหลังเมนู(กรอบ)
            ตั้งกล้องมอง(0.0, 0.0, -22.0, 0.0, 0.0, 0.0);  ตั้งระยะห่าง(2.8)
            โหมดผสม(1)
            สีดินสอ(255, 200, 120);  วาดสตริง("set password", 0.0, 0.0-3.0, 0.0, 0.9, 1)
            สีดินสอ(230, 235, 255);  วาดสตริง(text_get(), 0.0, 0.0, 0.0, 0.7, 1)
            สีดินสอ(110, 125, 160);  วาดสตริง("enter host · esc back", 0.0, 6.5, 0.0, 0.46, 1)
            flush_3d();  โหมดผสม(0)
            แสดงผล()
            ถ้า นำ("enter", gNavA) {
                bind hostPass = text_get();  bind authSent = 0.0;  text_clear()
                พิมพ์เขียว("  🌐 Host self-test:")
                พิมพ์ฟ้า(เน็ตทดสอบ(netPort))
                พิมพ์เหลือง("  external play: port-forward TCP " + to_str(netPort) + " to this PC + allow it in the firewall; joiners use your PUBLIC IP (verify with an online port checker). On CGNAT ISPs port-forward can't work — use Tailscale/Hamachi.")
                เน็ตโฮสต์(netPort)
                bind hMode = "coop";  ถ้า vsโหมด == 1 { bind hMode = "vs" }
                เน็ตประกาศ(discPort, ลูกชื่อ + "|" + hMode + "|arena|1")
                bind สถานะ = ST_LOBBY
            }
            ถ้า นำ("escape", gNavB) { text_clear();  bind สถานะ = ST_MP;  bind sel = 0 }
        }

        # ── JOIN: ใส่รหัสผ่านเซิร์ฟส่วนตัว (พิมพ์ → Enter) ──
        ถ้า สถานะ == ST_JOINPW {
            text_poll()
            วาดพื้นหลังเมนู(กรอบ)
            ตั้งกล้องมอง(0.0, 0.0, -22.0, 0.0, 0.0, 0.0);  ตั้งระยะห่าง(2.8)
            โหมดผสม(1)
            สีดินสอ(255, 200, 120);  วาดสตริง("password", 0.0, 0.0-3.0, 0.0, 0.9, 1)
            สีดินสอ(230, 235, 255);  วาดสตริง(text_get(), 0.0, 0.0, 0.0, 0.7, 1)
            สีดินสอ(110, 125, 160);  วาดสตริง("enter join · esc back", 0.0, 6.5, 0.0, 0.46, 1)
            flush_3d();  โหมดผสม(0)
            แสดงผล()
            ถ้า นำ("enter", gNavA) {
                bind joinPass = text_get();  bind authSent = 0.0;  text_clear()
                bind tgtIP = netIP                                        # ต่อตรง = IP จาก net.cfg
                ถ้า directJoin < 0.5 { bind tgtIP = list_get(srvIPs, srvSel) }
                bind coopRole = 1;  เน็ตจอย(tgtIP, netPort);  bind สถานะ = ST_LOBBY
            }
            ถ้า นำ("escape", gNavB) { text_clear();  bind สถานะ = ST_JOINLIST }
        }

        # ── JOIN LOBBY: รายการเซิร์ฟเวอร์ LAN ที่เจอ (UDP discovery) ──
        ถ้า สถานะ == ST_JOINLIST {
            bind dlist = เน็ตค้นหา(discPort)            # "ip|name|mode|stage|private\n..."
            bind dlines = แยก(dlist, "\n")
            bind srvIPs = [];  bind srvInfos = []
            bind di = 0
            ขณะที่ di < ความยาว(dlines) {
                bind dl = list_get(dlines, di)
                ถ้า ความยาว(dl) > 2 {
                    bind df = แยก(dl, "|")
                    ถ้า ความยาว(df) >= 2 {
                        bind srvIPs = list_push(srvIPs, list_get(df, 0))
                        bind srvInfos = list_push(srvInfos, df)
                    }
                }
                bind di = di + 1
            }
            bind nsrv = ความยาว(srvIPs)
            ถ้า นำ("up", gNavU)   { bind srvSel = srvSel - 1;  ถ้า srvSel < 0 { bind srvSel = nsrv - 1 } }
            ถ้า นำ("down", gNavD) { bind srvSel = srvSel + 1;  ถ้า srvSel > nsrv - 1 { bind srvSel = 0 } }
            ถ้า srvSel < 0 { bind srvSel = 0 }
            วาดพื้นหลังเมนู(กรอบ)
            ตั้งกล้องมอง(0.0, 0.0, -22.0, 0.0, 0.0, 0.0);  ตั้งระยะห่าง(2.8)
            โหมดผสม(1)
            สีดินสอ(120, 200, 255);  วาดสตริง("open games", 0.0, 0.0-7.0, 0.0, 1.0, 1)
            ถ้า nsrv < 1 { สีดินสอ(150, 160, 200);  วาดสตริง("searching lan", 0.0, 0.0-1.0, 0.0, 0.6, 1) }
            bind sj = 0
            ขณะที่ sj < nsrv { ถ้า sj < 8 {
                bind info = list_get(srvInfos, sj)
                bind nm = list_get(info, 1);  bind md = "coop";  ถ้า ความยาว(info) >= 3 { bind md = list_get(info, 2) }
                bind yy = 0.0-4.5 + sj*1.5
                ถ้า sj == srvSel { สีดินสอ(255, 230, 120) }  มิฉะนั้น { สีดินสอ(170, 185, 220) }
                วาดสตริง(nm + " " + md, 0.0, yy, 0.0, 0.6, 1)
                bind sj = sj + 1
            }  มิฉะนั้น { bind sj = nsrv } }
            สีดินสอ(110, 125, 160);  วาดสตริง("enter join · d direct · esc back", 0.0, 7.3, 0.0, 0.44, 1)
            flush_3d();  โหมดผสม(0)
            แสดงผล()
            ถ้า นำ("enter", gNavA) { ถ้า nsrv > 0 {
                bind directJoin = 0.0;  bind sinfo = list_get(srvInfos, srvSel);  bind spriv = "0"
                ถ้า ความยาว(sinfo) >= 5 { bind spriv = list_get(sinfo, 4) }
                ถ้า spriv == "1" { bind joinPriv = 1.0;  text_clear();  bind สถานะ = ST_JOINPW }   # ส่วนตัว → ถามรหัส
                มิฉะนั้น { bind joinPriv = 0.0;  bind joinPass = "";  bind authSent = 0.0;  bind coopRole = 1;  เน็ตจอย(list_get(srvIPs, srvSel), netPort);  bind สถานะ = ST_LOBBY }
            } }
            ถ้า กดปุ่ม("d") { bind directJoin = 1.0;  text_clear();  bind สถานะ = ST_JOINPW }   # ต่อตรง IP จาก net.cfg (ข้ามเน็ต)
            ถ้า นำ("escape", gNavB) { bind สถานะ = ST_MP;  bind sel = 1 }
        }

        # ── LOBBY: รอการเชื่อมต่อเครือข่าย → เริ่ม co-op เมื่อ status==2 ──────────
        ถ้า สถานะ == ST_LOBBY {
            วาดพื้นหลังเมนู(กรอบ)
            ถ้า coopRole == 0 { สีดินสอ(120,200,255);  วาดข้อความ(คำ_โฮสต์, 0.0, 0.0-2.5, 0.0, 0.9, 1) }
            มิฉะนั้น { สีดินสอ(255,200,120);  วาดข้อความ(คำ_จอยน์, 0.0, 0.0-2.5, 0.0, 0.9, 1) }
            bind ns = เน็ตสถานะ()
            ถ้า ns == 2 { สีดินสอ(80,230,120);  วาดข้อความ(คำ_พร้อม, 0.0, 1.5, 0.0, 0.7, 1) }
            มิฉะนั้น { สีดินสอ(210,200,90);  วาดข้อความ(คำ_เชื่อม, 0.0, 1.5, 0.0, 0.7, 1) }
            flush_3d()
            แสดงผล()
            ถ้า ns == 2 {
                # ── จับมือ ling-crypto: ทั้งสองแฮชรหัส (ว่าง = สาธารณะ) ต้องตรงกัน ──
                bind myPass = hostPass;  ถ้า coopRole == 1 { bind myPass = joinPass }
                bind myAuth = "auth:" + แฮชเข้ารหัส(myPass)
                เน็ตส่ง(myAuth)                                   # ส่งทุกเฟรมจน peer ได้รับ
                ถ้า เน็ตรับ() == myAuth {                          # peer มีรหัสตรงกัน
                    bind currentSlot = 0
                    bind ด่าน = 0;  bind สกิลเริ่ม = 0.0
                    bind startX = 0.0;  bind startY = 0.0 - 2.0;  bind startZ = 0.0
                    bind gameMode = 1;  bind รีเซ็ต = 1;  bind paused = 0
                    หยุดเพลง()
                    bind สถานะ = ST_PLAY
                }
            }
            ถ้า นำ("escape", gNavB) { เน็ตหยุดประกาศ();  bind สถานะ = ST_MENU;  bind sel = 4 }
        }

        # ── OPTIONS ───────────────────────────────────────────────────────────
        ถ้า สถานะ == ST_OPT {
            # 9 แถว: 0 เพลง(เปิด/ปิด) · 1 บทพูด(เปิด/ปิด) · 2 SFX · 3 BGM
            #        · 4 ภาษา UI/เมนู · 5 ภาษาพากย์ · 6 ภาษาซับ · 7 เซฟ · 8 ยกเลิก
            ถ้า นำ("up", gNavU)   { bind sel = sel - 1;  ถ้า sel < 0 { bind sel = 8 } }
            ถ้า นำ("down", gNavD) { bind sel = sel + 1;  ถ้า sel > 8 { bind sel = 0 } }
            ถ้า sel == 2 {
                ถ้า นำ("left", gNavL)  { bind ปริมาณSFX = ปริมาณSFX - 0.1;  ถ้า ปริมาณSFX < 0.0 { bind ปริมาณSFX = 0.0 } }
                ถ้า นำ("right", gNavR) { bind ปริมาณSFX = ปริมาณSFX + 0.1;  ถ้า ปริมาณSFX > 1.0 { bind ปริมาณSFX = 1.0 } }
            }
            ถ้า sel == 3 {
                ถ้า นำ("left", gNavL)  { bind ปริมาณBGM = ปริมาณBGM - 0.1;  ถ้า ปริมาณBGM < 0.0 { bind ปริมาณBGM = 0.0 };  ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) } }
                ถ้า นำ("right", gNavR) { bind ปริมาณBGM = ปริมาณBGM + 0.1;  ถ้า ปริมาณBGM > 1.0 { bind ปริมาณBGM = 1.0 };  ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) } }
            }
            วาดพื้นหลังเมนู(กรอบ)
            วาดป้าย(ภาษา, 4, 0.0, 0.0-7.0, 0.9)
            bind oi = 0
            ขณะที่ oi < 9 {
                bind yy = 0.0 - 5.5 + oi * 1.32
                ถ้า oi == sel {
                    สีดินสอ(235, 220, 145)
                    วาดลูกศร(0.0-11.0, yy, 0.0, 0.6)
                }
                มิฉะนั้น { สีดินสอ(120, 120, 150) }
                ถ้า oi == 0 {
                    วาดข้อความ(คำ_เพลง, 0.0-4.0, yy, 0.0, 0.7, ภาษา)
                    ถ้า musicOn == 1 { สีดินสอ(60,230,120) }  มิฉะนั้น { สีดินสอ(80,80,90) }
                    วาดสามเหลี่ยม3มิติ(4.0, yy-0.5, 0.0, 5.0, yy-0.5, 0.0, 5.0, yy+0.5, 0.0)
                    วาดสามเหลี่ยม3มิติ(4.0, yy-0.5, 0.0, 5.0, yy+0.5, 0.0, 4.0, yy+0.5, 0.0)
                }
                ถ้า oi == 1 {
                    วาดข้อความ(คำ_บทพูด, 0.0-4.0, yy, 0.0, 0.7, 1)
                    ถ้า แสดงบทพูด == 1 { สีดินสอ(60,230,120) }  มิฉะนั้น { สีดินสอ(80,80,90) }
                    วาดสามเหลี่ยม3มิติ(5.5, yy-0.5, 0.0, 6.5, yy-0.5, 0.0, 6.5, yy+0.5, 0.0)
                    วาดสามเหลี่ยม3มิติ(5.5, yy-0.5, 0.0, 6.5, yy+0.5, 0.0, 5.5, yy+0.5, 0.0)
                }
                ถ้า oi == 2 { วาดข้อความ(คำ_sfx, 0.0-4.0, yy, 0.0, 0.7, 1);  แถบปริมาณ(ปริมาณSFX, yy) }
                ถ้า oi == 3 { วาดข้อความ(คำ_bgm, 0.0-4.0, yy, 0.0, 0.7, 1);  แถบปริมาณ(ปริมาณBGM, yy) }
                # สามแถวภาษา: หัวข้ออังกฤษทางซ้าย + ชื่อภาษาที่เลือกอยู่ (เขียนด้วยสคริปต์
                # ของภาษานั้นเอง) ทางขวา — เห็นค่าปัจจุบันได้โดยไม่ต้องเข้าไปในจอเลือก
                ถ้า oi == 4 {
                    วาดสตริง("ui / menu language:", 0.0-4.2, yy, 0.0, 0.44, 1)
                    วาดชื่อภาษา(ภาษา, 6.2, yy, 0.55)
                }
                ถ้า oi == 5 {
                    วาดสตริง("voiceover language:", 0.0-4.2, yy, 0.0, 0.44, 1)
                    วาดชื่อภาษา(ภาษาพากย์, 6.2, yy, 0.55)
                }
                ถ้า oi == 6 {
                    วาดสตริง("subtitle language:", 0.0-4.2, yy, 0.0, 0.44, 1)
                    วาดชื่อภาษา(ภาษาซับ, 6.2, yy, 0.55)
                }
                ถ้า oi == 7 { วาดป้าย(ภาษา, 15, 0.0, yy, 0.7) }
                ถ้า oi == 8 { วาดป้าย(ภาษา, 16, 0.0, yy, 0.7) }
                bind oi = oi + 1
            }
            flush_3d()
            แสดงผล()
            ถ้า นำ("enter", gNavA) {
                ถ้า sel == 0 { bind musicOn = 1 - musicOn;  ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }  มิฉะนั้น { ระดับเพลง(0.0) } }
                ถ้า sel == 1 { bind แสดงบทพูด = 1 - แสดงบทพูด }
                ถ้า sel == 4 { bind langReturn = ST_OPT;  bind langTarget = 1;  bind สถานะ = ST_LANG;  bind sel = ภาษา }
                ถ้า sel == 5 { bind langReturn = ST_OPT;  bind langTarget = 2;  bind สถานะ = ST_LANG;  bind sel = ภาษาพากย์ }
                ถ้า sel == 6 { bind langReturn = ST_OPT;  bind langTarget = 3;  bind สถานะ = ST_LANG;  bind sel = ภาษาซับ }
                ถ้า sel == 7 {
                    เขียนไฟล์("options.cfg", to_str(musicOn) + ";" + to_str(ภาษา) + ";" + to_str(ปริมาณSFX) + ";" + to_str(ปริมาณBGM) + ";" + to_str(แสดงบทพูด) + ";" + to_str(ภาษาพากย์) + ";" + to_str(ภาษาซับ))
                    bind สถานะ = optReturn;  ถ้า optReturn == ST_PLAY { bind paused = 1 };  bind sel = 0
                }
                ถ้า sel == 8 { bind สถานะ = optReturn;  ถ้า optReturn == ST_PLAY { bind paused = 1 };  bind sel = 0 }
            }
            ถ้า นำ("escape", gNavB) { bind สถานะ = optReturn;  ถ้า optReturn == ST_PLAY { bind paused = 1 };  bind sel = 0 }
        }

        # ── LANGUAGE ──────────────────────────────────────────────────────────
        ถ้า สถานะ == ST_LANG {
            ถ้า นำ("up", gNavU)   { bind sel = sel - 1;  ถ้า sel < 0 { bind sel = 5 } }
            ถ้า นำ("down", gNavD) { bind sel = sel + 1;  ถ้า sel > 5 { bind sel = 0 } }
            วาดพื้นหลังเมนู(กรอบ)
            วาดเมนูเลือกภาษา(sel, t, langTarget)
            แสดงผล()
            ถ้า นำ("enter", gNavA)  {
                # langTarget: 0 = ตั้งรวมทุกอย่าง (เมนูหลัก) · 1 = UI · 2 = พากย์ · 3 = ซับ
                ถ้า langTarget == 0 { bind ภาษา = sel;  bind ภาษาพากย์ = sel;  bind ภาษาซับ = sel }
                ถ้า langTarget == 1 { bind ภาษา = sel }
                ถ้า langTarget == 2 { bind ภาษาพากย์ = sel }
                ถ้า langTarget == 3 { bind ภาษาซับ = sel }
                # ตั้งภาษารวมจากเมนูหลัก = เขียนลงไฟล์เลย (หน้านั้นไม่มีปุ่มเซฟของตัวเอง)
                ถ้า langTarget == 0 {
                    เขียนไฟล์("options.cfg", to_str(musicOn) + ";" + to_str(ภาษา) + ";" + to_str(ปริมาณSFX) + ";" + to_str(ปริมาณBGM) + ";" + to_str(แสดงบทพูด) + ";" + to_str(ภาษาพากย์) + ";" + to_str(ภาษาซับ))
                }
                bind สถานะ = langReturn
                ถ้า langReturn == ST_MENU { bind sel = 6 }  มิฉะนั้น { bind sel = 3 + langTarget }
            }
            ถ้า นำ("escape", gNavB) { bind สถานะ = langReturn;  ถ้า langReturn == ST_MENU { bind sel = 6 }  มิฉะนั้น { bind sel = 3 + langTarget } }
        }

        # ── CINEMATIC: สโนว์โกลบดาว + บทกวี 24 บรรทัด → เฟดขาว → เล่น ──────────
        # ── INTRO CUTSCENE: บทกวีในสโนว์โกลบ (โค้ดฉากอยู่ที่ scenes/intro.ling) ──
        ถ้า สถานะ == ST_CINE {
            ถ้า บทกวีระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind บทกวีระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("intro", บทกวี_ผู้พูดรหัส(), ภาษาพากย์, บทกวี_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind บทกวีระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind อินโทรผล = เล่นฉากอินโทร(t, cineStart, prevLine, purified, flashAmt, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงหลัก, fftCalls, kCutMesh, qCutMesh, liq, particlesOn, คอร์ดระฆัง, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, บทกวีระยะเวลาแคช)
            bind prevLine = list_get(อินโทรผล, 0)
            bind purified = list_get(อินโทรผล, 1)
            bind flashAmt = list_get(อินโทรผล, 2)
            ถ้า list_get(อินโทรผล, 3) == 1 {
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 0.0
                หยุดเพลง()
                bind สถานะ = ST_PLAY
            }
        }

        # ── CHAPTER 1 CUTSCENE: ต่อจากอินโทร → ภารกิจหาเครื่องดนตรีกล่อมโลก ──
        ถ้า สถานะ == ST_CH1 {
            ถ้า บท1ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind บท1ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("chapter1", ฉากบทหนึ่ง_ผู้พูดรหัส(), ภาษาพากย์, ฉากบทหนึ่ง_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind บท1ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind บทหนึ่งผล = เล่นฉากบทหนึ่ง(t, บทหนึ่งเริ่ม, บทหนึ่งบรรทัดก่อน, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงบทหนึ่ง, ยาวเพลงบทหนึ่ง, kCutMesh, qCutMesh, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, บท1ระยะเวลาแคช)
            bind บทหนึ่งบรรทัดก่อน = list_get(บทหนึ่งผล, 0)
            ถ้า list_get(บทหนึ่งผล, 1) > 0.5 {
                bind ด่าน = 1
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 128.0;  bind startY = 0.0-2.0;  bind startZ = 128.0
                bind ภารกิจข้อความ = "quest: recover the cradle reed"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── CHAPTER 2 CUTSCENE: หลังผ่านเลเวล 2 → ดวงตาบนฟ้า + เควสต์เขาสมุทรใต้ระฆังจม ──
        ถ้า สถานะ == ST_CH2 {
            ถ้า บท2ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind บท2ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("chapter2", ฉากบทสอง_ผู้พูดรหัส(), ภาษาพากย์, ฉากบทสอง_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind บท2ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind บทสองผล = เล่นฉากบทสอง(t, บทสองเริ่ม, บทสองบรรทัดก่อน, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงมูน, ยาวเพลงมูน, kCutMesh, qCutMesh, palaceMesh, palaceDecoMesh, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, บท2ระยะเวลาแคช)
            bind บทสองบรรทัดก่อน = list_get(บทสองผล, 0)
            ถ้า list_get(บทสองผล, 1) > 0.5 {
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind ภารกิจข้อความ = "quest: find the tide horn under the drowned bell"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── CHAPTER 3 CUTSCENE: หลังผ่านเลเวล 3 → ราชสำนักอำนวยเพลง + ดาวหางฟาดภูเขาเงียบ ──
        ถ้า สถานะ == ST_CH3 {
            ถ้า บท3ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind บท3ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("chapter3", ฉากบทสาม_ผู้พูดรหัส(), ภาษาพากย์, ฉากบทสาม_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind บท3ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind บทสามผล = เล่นฉากบทสาม(t, บทสามเริ่ม, บทสามสถานะ, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงโปรเกรส, ยาวเพลงโปรเกรส, เพลงเซราฟ, kCutMesh, qCutMesh, วังP0, วังP1, วังP2, วังP3, วังP4, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, บท3ระยะเวลาแคช)
            bind บทสามสถานะ = list_get(บทสามผล, 0)
            ถ้า list_get(บทสามผล, 1) > 0.5 {
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind ภารกิจข้อความ = "quest: inspect what struck the silent mountain"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── CHAPTER 4 CUTSCENE: หลังผ่านเลเวล 4 → รูหนอน + ของขลังเวียดนาม + วิญญาณน้ำ/ไฟ ──
        ถ้า สถานะ == ST_CH4 {
            ถ้า บท4ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind บท4ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("chapter4", ฉากบทสี่_ผู้พูดรหัส(), ภาษาพากย์, ฉากบทสี่_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind บท4ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind บทสี่ผล = เล่นฉากบทสี่(t, บทสี่เริ่ม, บทสี่สถานะ, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงไซออน, ยาวเพลงไซออน, kCutMesh, qCutMesh, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, บท4ระยะเวลาแคช)
            bind บทสี่สถานะ = list_get(บทสี่ผล, 0)
            ถ้า list_get(บทสี่ผล, 1) > 0.5 {
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind ภารกิจข้อความ = "quest: the cold light knows your names - reach it first"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── CHAPTER 5 CUTSCENE: หลังผ่านเลเวล 5 → ศึกสหาย vs แสงเย็น + บทสรุป Antartica ──
        ถ้า สถานะ == ST_CH5 {
            ถ้า บท5ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind บท5ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("chapter5", ฉากบทห้า_ผู้พูดรหัส(), ภาษาพากย์, ฉากบทห้า_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind บท5ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind บทห้าผล = เล่นฉากบทห้า(t, บทห้าเริ่ม, บทห้าสถานะ, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงเบสไลน์, ยาวเพลงเบสไลน์, เพลงเกม, kCutMesh, qCutMesh, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, บท5ระยะเวลาแคช)
            bind บทห้าสถานะ = list_get(บทห้าผล, 0)
            ถ้า list_get(บทห้าผล, 1) > 0.5 {
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind ภารกิจข้อความ = "quest: carry the song home through the thaw"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── CHAPTER 6 CUTSCENE: หลังผ่านเลเวล 6 → ห้องทริประหว่างโลก + เดมอนใจดี ──
        ถ้า สถานะ == ST_CH6 {
            ถ้า บท6ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind บท6ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("chapter6", ฉากบทหก_ผู้พูดรหัส(), ภาษาพากย์, ฉากบทหก_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind บท6ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind บทหกผล = เล่นฉากบทหก(t, บทหกเริ่ม, บทหกสถานะ, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงเดมอน, ยาวเพลงเดมอน, kCutMesh, qCutMesh, วังP0, วังP1, วังP2, วังP3, วังP4, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, บท6ระยะเวลาแคช)
            bind บทหกสถานะ = list_get(บทหกผล, 0)
            ถ้า list_get(บทหกผล, 1) > 0.5 {
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind ภารกิจข้อความ = "quest: the daemon lights the way - find the missing letters"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── CHAPTER 7 CUTSCENE: หลังผ่านเลเวล 7 → บินตามเดมอนข้ามแม่น้ำดาวหลับ ──
        ถ้า สถานะ == ST_CH7 {
            ถ้า บท7ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind บท7ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("chapter7", ฉากบทเจ็ด_ผู้พูดรหัส(), ภาษาพากย์, ฉากบทเจ็ด_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind บท7ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind บทเจ็ดผล = เล่นฉากบทเจ็ด(t, บทเจ็ดเริ่ม, บทเจ็ดสถานะ, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงสวิง, ยาวเพลงสวิง, kCutMesh, qCutMesh, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, บท7ระยะเวลาแคช)
            bind บทเจ็ดสถานะ = list_get(บทเจ็ดผล, 0)
            ถ้า list_get(บทเจ็ดผล, 1) > 0.5 {
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind ภารกิจข้อความ = "quest: the lullaby remembers itself - follow it home"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── CHAPTER 8 CUTSCENE: หลังผ่านเลเวล 8 → มหากาพย์วังกลับบ้าน + ไอริสปิด ──
        ถ้า สถานะ == ST_CH8 {
            ถ้า บท8ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind บท8ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("chapter8", ฉากบทแปด_ผู้พูดรหัส(), ภาษาพากย์, ฉากบทแปด_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind บท8ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind บทแปดผล = เล่นฉากบทแปด(t, บทแปดเริ่ม, บทแปดสถานะ, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงเมนู, ยาวเพลงเมนู, kCutMesh, qCutMesh, วังP0, วังP1, วังP2, วังP3, วังP4, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, บท8ระยะเวลาแคช)
            bind บทแปดสถานะ = list_get(บทแปดผล, 0)
            ถ้า list_get(บทแปดผล, 1) > 0.5 {
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind ภารกิจข้อความ = "quest: the night is kind - play on"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── PRELUDE CUTSCENE: หลังบอสทิวทอเรียล → งานเทศกาลโน้ตแรก (บทโหมโรงของทุกบท) ──
        ถ้า สถานะ == ST_POST_LVL1 {
            ถ้า postLvl1ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind postLvl1ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("post_level1", โพสต์เลเวล1_ผู้พูดรหัส(), ภาษาพากย์, โพสต์เลเวล1_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind postLvl1ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind result = เล่นโพสต์เลเวล1(t, postLvl1Start, postLvl1PrevLine, postLvl1ธาตุนับ, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงเทศกาล, ยาวเพลงเทศกาล, kCutMesh, qCutMesh, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, postLvl1ระยะเวลาแคช)
            bind postLvl1PrevLine = list_get(result, 0)
            bind postLvl1ธาตุนับ = list_get(result, 1)
            ถ้า list_get(result, 2) > 0.5 {
                bind อาร์เคดโหมด = 0
                bind gameMode = 0;  bind รีเซ็ต = 1;  bind paused = 0
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind ภารกิจข้อความ = "quest: carry the first note out of the festival"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── BONUS CUTSCENE: โคด้า "โน้ตที่เหลือ" ก่อนเข้าสนามโรลเลอร์โคสเตอร์ ──
        ถ้า สถานะ == ST_BONUSCINE {
            bind บทโบนัสผล = เล่นฉากโบนัส(t, บทโบนัสเริ่ม, บทโบนัสบรรทัดก่อน, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงเซราฟ, ยาวเพลงเซราฟ, ภาษาพากย์, แสดงบทพูด, ภาษาซับ)
            bind บทโบนัสบรรทัดก่อน = list_get(บทโบนัสผล, 0)
            ถ้า list_get(บทโบนัสผล, 1) > 0.5 {
                หยุดเพลง()
                bind สถานะ = ST_BONUS
                bind bonusStart = เวลาปัจจุบัน()             # รีเซ็ตนาฬิกาสนามให้เริ่มนับหลังคัตซีนจบ ไม่ใช่ตอนกด F12
                พิมพ์เหลือง("  ★ entering bonus rollercoaster!")
            }
        }

        # ── FINALE I: หลังผ่านเลเวล 9 → ช่องว่างที่ไม่เคยร้อง (โคด้ากลับมา + เดมอนเตือน) ──
        ถ้า สถานะ == ST_FINALE1 {
            ถ้า ไฟนอล1ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind ไฟนอล1ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("finale1", ฉากไฟนอล1_ผู้พูดรหัส(), ภาษาพากย์, ฉากไฟนอล1_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind ไฟนอล1ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind ไฟนอล1ผล = เล่นฉากไฟนอล1(t, ไฟนอล1เริ่ม, ไฟนอล1บรรทัดก่อน, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงอินโทโร, ยาวเพลงอินโทโร, kCutMesh, qCutMesh, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, ไฟนอล1ระยะเวลาแคช)
            bind ไฟนอล1บรรทัดก่อน = list_get(ไฟนอล1ผล, 0)
            ถ้า list_get(ไฟนอล1ผล, 1) > 0.5 {
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind รีเซ็ต = 1
                bind ภารกิจข้อความ = "quest: the interval waits below - descend"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── FINALE II: หลังผ่านเลเวล 11 → คณะครบวงรวมตัวหน้าประตูเหรียญ ──
        ถ้า สถานะ == ST_FINALE2 {
            ถ้า ไฟนอล2ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind ไฟนอล2ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("finale2", ฉากไฟนอล2_ผู้พูดรหัส(), ภาษาพากย์, ฉากไฟนอล2_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind ไฟนอล2ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind ไฟนอล2ผล = เล่นฉากไฟนอล2(t, ไฟนอล2เริ่ม, ไฟนอล2บรรทัดก่อน, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงเหรียญ, ยาวเพลงเหรียญ, kCutMesh, qCutMesh, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, ไฟนอล2ระยะเวลาแคช)
            bind ไฟนอล2บรรทัดก่อน = list_get(ไฟนอล2ผล, 0)
            ถ้า list_get(ไฟนอล2ผล, 1) > 0.5 {
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind รีเซ็ต = 1
                bind ภารกิจข้อความ = "quest: strike the final chord as one"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        # ── FINALE III: หลังผ่านเลเวล 12 → บทเพลงสมบูรณ์ (TRUE ENDING) ──
        ถ้า สถานะ == ST_FINALE3 {
            ถ้า ไฟนอล3ระยะเวลาแคชภาษา != ภาษาพากย์ {
                bind ไฟนอล3ระยะเวลาแคช = ระยะเวลาเสียงพากย์ฉาก("finale3", ฉากไฟนอล3_ผู้พูดรหัส(), ภาษาพากย์, ฉากไฟนอล3_ระยะเวลา(รหัสภาษาเสียง(ภาษาพากย์)))
                bind ไฟนอล3ระยะเวลาแคชภาษา = ภาษาพากย์
            }
            bind ไฟนอล3ผล = เล่นฉากไฟนอล3(t, ไฟนอล3เริ่ม, ไฟนอล3บรรทัดก่อน, musicOn, ปริมาณBGM, ปริมาณSFX, เพลงเซราฟ, ยาวเพลงเซราฟ, kCutMesh, qCutMesh, liq, ภาษาพากย์, แสดงบทพูด, ภาษาซับ, ไฟนอล3ระยะเวลาแคช)
            bind ไฟนอล3บรรทัดก่อน = list_get(ไฟนอล3ผล, 0)
            ถ้า list_get(ไฟนอล3ผล, 1) > 0.5 {
                bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                bind รีเซ็ต = 1
                bind ภารกิจข้อความ = "the symphony is complete - the song plays on"
                bind ภารกิจT = 1.5
                หยุดเพลง()
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }
                bind สถานะ = ST_PLAY
            }
        }

        ถ้า สถานะ == ST_BONUS {
            # Bonus Rollercoaster Level: High-speed physics benchmark
            bind bonusResult = runBonusLevel(
                t,
                bonusStart,
                bonusCX, bonusCY, bonusCZ,
                bonusVX, bonusVY, bonusVZ,
                R,
                bonusGrounded,
                charge,
                bonusSquash,
                musicOn,
                ปริมาณBGM,
                bonusLiq
            )

            # Update state from result
            bind bonusCX = list_get(bonusResult, 0)
            bind bonusCY = list_get(bonusResult, 1)
            bind bonusCZ = list_get(bonusResult, 2)
            bind bonusVX = list_get(bonusResult, 3)
            bind bonusVY = list_get(bonusResult, 4)
            bind bonusVZ = list_get(bonusResult, 5)
            bind bonusGrounded = list_get(bonusResult, 6)
            bind bonusSquash = list_get(bonusResult, 7)
            bind shouldExit = list_get(bonusResult, 8)

            ถ้า shouldExit > 0.5 {
                # Return to menu
                หยุดเพลง()
                bind สถานะ = ST_MENU
                bind sel = 0
                bind menuIdleT = 0.0
                ถ้า musicOn == 1 { เล่นเพลง(เพลงเมนู) }
                พิมพ์ฟ้า("  ★ exited bonus level")
            }
        }

        # ── PLAY ──────────────────────────────────────────────────────────────
        ถ้า สถานะ == ST_PLAY {
            ถ้า รีเซ็ต == 1 {
                ถ้า musicOn == 1 { ระดับเพลง(ปริมาณBGM) }   # กันคัตซีน/โหมดพิเศษเผลอปล่อยระดับเพลงที่หรี่ไว้ค้าง — ซิงก์กลับค่าที่ตั้งไว้ทุกครั้งที่เข้าเกมเพลย์สด
                ถ้า บอสโหมด > 0.5 { bind อาร์เคดโหมด = 0 }
                # พรีโหลดเฉพาะตอนเปลี่ยนเลเวลจริง (ไม่ใช่ตอนเกิดใหม่)
                ถ้า ด่าน != เลเวลก่อน { พรีโหลด(0.5);  เมชแคชล้าง();  bind เลเวลก่อน = ด่าน }
                # ── ออโต้เซฟ: เริ่มด่านที่เกินปลดล็อกเดิม → บันทึกทันที (ครอบทุกเส้นทางเข้าด่าน: บอส/คัตซีน/เลือกด่าน) ──
                ถ้า ด่าน > ด่านปลดล็อก {
                    bind ด่านปลดล็อก = ด่าน
                    เขียนไฟล์("progress.dat", to_str(ด่านปลดล็อก))
                    พิมพ์ฟ้า("  💾 ออโต้เซฟ: ปลดล็อกด่าน " + to_str(ด่าน))
                }
                เติม(3, 2, 10);  แสดงผล()      # เฟรมโหลดสะอาดก่อนโปรเจนซิงโครนัส: กันลูกเต๋า/ไวร์เฟรมเลือกด่านค้างเป็นรอยลากตอนเปลี่ยนฉาก
                bind liq = liquid_new(16.0, 16.0)
                liquid_rainbow(liq, 1.0)
                bind liqAcc = 0.0;  bind liqMix = 0.0
                bind sx = 0.0
                ขณะที่ sx < 16.0 {
                    liquid_splat(liq, sx, 6.0, 1.0, 0.9, 1.2)
                    liquid_splat(liq, sx, 12.0, 0.0, 1.0, 1.3)
                    bind sx = sx + 1.0
                }
                # ── สถานะลูกแก้ว (manual physics แบบเดโม: ตำแหน่ง+ความเร็ว) ──
                bind cx = startX;  bind cy = startY;  bind cz = startZ
                bind vx = 0.0;  bind vy = 0.0;  bind vz = 0.0
                # วางลูกบนพื้นทันที (ไม่หล่นลง) — snap cy = ความสูงพื้นที่จุดเกิด - R
                bind spawnFloor = GROUND
                ถ้า ด่าน > 0.5 { bind spawnFloor = latticeHeight(startX, startZ, ด่าน, ฉากทางออก) }
                ถ้า บอสโหมด > 0.5 { bind spawnFloor = ความสูงอารีนา(startX, startZ) }
                bind cy = spawnFloor - R
                bind charge = 0.0;  bind spDown = 0.0;  bind squash = 0.0;  bind grounded = 1.0
                # ── ผู้เล่น 2 (co-op ในเครื่อง — ใช้เมื่อ gameMode==1) ──
                bind cx2 = startX + 7.0;  bind cy2 = spawnFloor - R;  bind cz2 = startZ
                bind vx2 = 0.0;  bind vy2 = 0.0;  bind vz2 = 0.0;  bind grounded2 = 0.0;  bind jp2 = 0.0
                bind camX = startX;  bind camZ = startZ - 26.0
                bind camYaw = 0.0;  bind camPitch = 0.35
                bind สกิล = สกิลเริ่ม
                bind ดาบ = 0.0;  ถ้า สกิล >= 1.0 { bind ดาบ = 1.0 }
                bind slashT = 0.0;  bind mbDown = 0.0
                bind guarding = 0.0;  bind guardT = 0.0;  bind guardHeld = 0.0
                bind combo = 0.0;  bind arcFade = 0.0;  bind arcType = 0.0;  bind arcYaw = 0.0
                bind เลือกไอเทม = 1.0;  bind scrollAcc = 0.0;  bind slashNote = 0.0
                bind bombT = 0.0;  bind bombCool = 0.0
                # ── อาวุธ 3-9 (AoE): สถานะเอฟเฟกต์ + เบิร์น/ฟรีซ ──
                bind wfxT = 0.0;  bind wfxType = 0.0;  bind wfxCool = 0.0;  bind wfxFire = 0.0
                bind wfxRad = 0.0;  bind wfxDmg = 0.0;  bind wfxX = 0.0;  bind wfxZ = 0.0
                bind burnT = 0.0;  bind freezeT = 0.0
                bind เป้าสกิล = 1.0;  ถ้า ด่าน > 0.5 { bind เป้าสกิล = ด่าน }
                # มอนสเตอร์: ทิวทอเรียล = 5 ตัว (1 ต่อห้อง: กลาง/เหนือ/ใต้/ออก/ตก); proc = ตามเลเวล
                bind มอนจำนวน = 5.0;  ถ้า ด่าน > 0.5 { bind มอนจำนวน = จำนวนมอน(ด่าน) }
                ถ้า vsโหมด == 1 { bind มอนจำนวน = 0.0 }   # VS = PvP ล้วน ไม่มีมอน
                ถ้า บอสโหมด > 0.5 { bind มอนจำนวน = 0.0 }   # บอส: ศัตรูคือบอส ไม่มีมอนคริสตัล (กันคำนวณ/ลอยค้าง)
                ถ้า อาร์เคดโหมด > 0.5 {                     # อาร์เคด: สร้างศัตรูรอบนี้ (สเตตัสตามรอบ)
                    bind มอนจำนวน = 0.0
                    bind fd = สร้างศัตรู(รอบ)
                    bind foeHPMax = list_get(fd,0);  bind foeHP = foeHPMax
                    bind foeDef = list_get(fd,1);  bind foeRes = list_get(fd,2);  bind foeAtk = list_get(fd,3)
                    bind foeCool = list_get(fd,4);  bind foeDiff = list_get(fd,5)
                    bind foeBodyHue = list_get(fd,6);  bind foeStampHue = list_get(fd,7);  bind foeBandHue = list_get(fd,8)
                    bind foeStamp = list_get(fd,9);  bind foeBand = list_get(fd,10);  bind foeName = list_get(fd,11)
                    bind foeX = 35.0;  bind foeZ = 0.0;  bind foeVX = 0.0;  bind foeVZ = 0.0
                    bind foeCoolT = foeCool;  bind foeGuard = 0.0;  bind foeHurt = 0.0;  bind foeLunge = 0.0;  bind ผู้ชนะรอบ = 0.0
                    bind flv = รอบ - 1.0;  ถ้า flv > 8.0 { bind flv = 8.0 }   # 9 ระดับ AI (0..8)
                    bind foeAgi  = 0.22 + (flv/8.0)*0.72   # ความคล่อง: ความเร็ว/หลบ
                    bind foeTech = 0.18 + (flv/8.0)*0.78   # เทคนิค: คอมโบ/คูลดาวน์ฟัน
                    bind foeAtt  = 0.12 + (flv/8.0)*0.82   # สมาธิ: การ์ด/หลบแบบโต้ตอบ
                    bind foeSlashT = 0.0;  bind foeCombo = 0.0
                    # ── เส้นโค้งความยาก AI: easy×0.7 · normal×1.03 · hard×1.36 (HP/พลัง/ทักษะ) ──
                    bind dk = 0.7 + aiDiff*0.33
                    bind foeHPMax = foeHPMax*dk;  bind foeHP = foeHPMax;  bind foeAtk = foeAtk*dk
                    bind foeAgi = foeAgi*dk;   ถ้า foeAgi > 0.98 { bind foeAgi = 0.98 }
                    bind foeTech = foeTech*dk;  ถ้า foeTech > 0.98 { bind foeTech = 0.98 }
                    bind foeAtt = foeAtt*dk;   ถ้า foeAtt > 0.98 { bind foeAtt = 0.98 }
                    bind callT = 2.8;  bind callMsg = ดึงคำเปิด();  bind tauntT = 6.0
                    bind pickCD = [0.0,0.0,0.0,0.0,0.0,0.0];  bind swordType = 0.0;  bind weaponT = 0.0;  bind slashCD = 0.0;  bind สกิล = 1.0;  bind ดาบ = 1.0;  bind เลือกไอเทม = 1.0
                }
                bind มอนseed = 0.0;  bind มอนspread = 45.0   # ทิวทอเรียล seed 0 (ไม่ชนกับด่าน 1) → ฐานมอน แมป 1 ตัว/ห้อง
                ถ้า ด่าน > 0.5 { bind มอนseed = ด่าน;  bind มอนspread = 165.0 }
                bind มอนเป็น = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
                bind มอนHitT = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
                bind มอนDieT = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
                bind มอนX = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
                bind มอนZ = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
                bind มอนAnim = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
                bind มอนไทป์ = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
                bind mtInit = 0
                ขณะที่ mtInit < 12 {
                    bind มอนไทป์ = ตั้งรายการ(มอนไทป์, mtInit, ชนิดมอน(mtInit, มอนseed))
                    bind mtInit = mtInit + 1
                }
                # ── RPG: เลือดมอน + ดรอปบัฟ + เลือดผู้เล่น ──
                bind มอนHPสูง = 9.0 + ด่าน * 3.2                       # อึดขึ้น ~2x (เดิม 4.0 + ด่าน*1.6) — ตายไม่ทันเห็นหน้า
                bind มอนHP = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
                bind มอนHPmaxArr = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]   # แคช HP สูงสุดต่อตัว (เลี่ยงเรียก สปีชีส์มอน ทุกเฟรมตอนวาด)
                bind มอนโกรธ = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]      # ตัวจับเวลาความแค้นต่อตัว (วิ): โดนตีแล้วไล่ล่าไม่เลิก
                bind mNtot = จำนวนมอน(มอนseed)
                bind mh = 0
                ขณะที่ mh < 31 {
                    bind hpmx = มอนHPสูง * list_get(สปีชีส์มอน(mh, มอนseed), 4)   # HP ต่อสายพันธุ์ (อึด/เปราะ) — คำนวณครั้งเดียว
                    bind มอนHPmaxArr = ตั้งรายการ(มอนHPmaxArr, mh, hpmx);  bind มอนHP = ตั้งรายการ(มอนHP, mh, hpmx)
                    bind bp0 = ฐานมอน(mh, mNtot, มอนseed, มอนspread)                # ตำแหน่งฐาน (ห้องทิวทอเรียล/ซันฟลาวเวอร์) → init แคช (freeze ระยะไกลถูกต้องตั้งแต่เฟรมแรก)
                    bind มอนX = ตั้งรายการ(มอนX, mh, list_get(bp0,0));  bind มอนZ = ตั้งรายการ(มอนZ, mh, list_get(bp0,1))
                    bind mh = mh + 1
                }
                bind ดรอปเป็น = [0,0,0,0,0,0,0,0,0,0,0,0]
                bind ดรอปX = [0,0,0,0,0,0,0,0,0,0,0,0]
                bind ดรอปZ = [0,0,0,0,0,0,0,0,0,0,0,0]
                bind ดรอปสเตตัส = [0,0,0,0,0,0,0,0,0,0,0,0]
                bind ดรอปค่า = [0,0,0,0,0,0,0,0,0,0,0,0]
                bind ชีวิตสูงสุด = 100.0 + list_get(ผู้เล่นสเตตัส, 2) * 6.0
                bind ชีวิต = ชีวิตสูงสุด
                bind บาดเจ็บ = 0.0
                bind ตีโดน = 0.0;  bind โดนตี = 0.0   # VS: ดาเมจที่เราทำ / ที่เรารับ
                bind plyR = 1.0;  bind plyG = 0.9;  bind plyB = 0.95   # สีไฟลูกเริ่มต้น (แอ่งแสงใช้ก่อนรอบไฟแรกในโหมดแสงต่ำ)
                # ── ประกาศเป้าหมายด่าน (แบนเนอร์ภารกิจ) — เว้นถ้ามีข้อความเควสต์ค้าง (เช่นจากคัตซีนบทหนึ่ง) ──
                ถ้า ภารกิจT <= 0.0 { ถ้า บอสโหมด < 0.5 { ถ้า อาร์เคดโหมด < 0.5 { ถ้า vsโหมด < 0.5 {
                    ถ้า ด่าน < 0.5 { bind ภารกิจข้อความ = "objective: collect all letters" }
                    มิฉะนั้น { bind ภารกิจข้อความ = "objective: clear all monsters" }
                    bind ภารกิจT = 1.5
                } } } }
                จับเมาส์()
                bind รีเซ็ต = 0
                set_zdist(32.0)
                bind playStart = t
                bind คะแนน = 0
                bind เก็บแล้ว = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
                # ── เลือกเพลงต่อเลเวล: สุ่ม (กระจายทุกเพลง) แต่ MIDI = WAV เดียวกัน ──
                #   ด่าน 0 (tutorial) = Purika (ดัชนี 6); อื่น ๆ สุ่มด้วย (7·ด่าน+3) mod N
                bind sk = ด่าน * 7.0 + 3.0
                bind songIdx = sk - ปัดลง(sk / จำนวนเพลง) * จำนวนเพลง
                ถ้า ด่าน < 0.5 { bind songIdx = 6.0 }
                ถ้า ด่าน > 9.5 { ถ้า ด่าน < 12.5 { bind songIdx = 13.0 } }   # ด่าน 10-12 → Progress
                ถ้า อาร์เคดโหมด > 0.5 { bind songIdx = ปัดลง(สุ่ม() * จำนวนเพลง) }   # อาร์เคด: สุ่มเพลงทุกศัตรู (SFX จูนตามเพลงนี้)
                bind sd = ข้อมูลเพลงดัชนี(songIdx)
                bind wavPath = เพลงWAV(songIdx)
                ถ้า บอสโหมด > 0.5 { bind sd = ข้อมูลเพลงบอส();  bind wavPath = เพลงบอสWAV();  bind songIdx = 0.0 - 99.0 }   # บอส → HealthyHell (ไม่อยู่ในสุ่ม BGM)
                bind เก็บจำนวน  = list_get(sd, 0)
                bind เก็บไกลฟ   = list_get(sd, 1)
                bind เก็บความถี่ = list_get(sd, 2)
                bind เก็บX      = list_get(sd, 3)
                bind เก็บZ      = list_get(sd, 4)
                # ── กันเฟรมดรอปจากเพลงที่มีโน้ตหนาแน่นมาก: สุ่มเก็บตัวอย่างคงเพดาน ──
                ถ้า เก็บจำนวน > 220.0 {
                    bind tgt = 220.0
                    bind step = เก็บจำนวน / tgt
                    bind ng = [];  bind nf = [];  bind nxs = [];  bind nzs = []
                    bind ดัชนีลอย = 0.0;  bind c = 0.0
                    ขณะที่ c < tgt {
                        bind idx = floor(ดัชนีลอย)
                        ถ้า idx > เก็บจำนวน - 1.0 { bind idx = เก็บจำนวน - 1.0 }
                        bind ng = list_push(ng, list_get(เก็บไกลฟ, idx))
                        bind nf = list_push(nf, list_get(เก็บความถี่, idx))
                        bind nxs = list_push(nxs, list_get(เก็บX, idx))
                        bind nzs = list_push(nzs, list_get(เก็บZ, idx))
                        bind ดัชนีลอย = ดัชนีลอย + step
                        bind c = c + 1.0
                    }
                    bind เก็บไกลฟ = ng
                    bind เก็บความถี่ = nf
                    bind เก็บX = nxs
                    bind เก็บZ = nzs
                    bind เก็บจำนวน = tgt
                }
                bind เก็บแล้ว = รายการศูนย์(floor(เก็บจำนวน))
                bind เก็บXแคช   = รายการศูนย์(floor(เก็บจำนวน))
                bind เก็บYแคช   = รายการศูนย์(floor(เก็บจำนวน))
                bind เก็บZแคช   = รายการศูนย์(floor(เก็บจำนวน))
                bind sfxจำนวน = เก็บจำนวน;  bind sfxความถี่ = เก็บความถี่
                bind โหมดต่อสู้ = 0.0;  bind ฮีทต่อสู้ = 0.0;  bind เฟดต่อสู้ = 0.0
                ถ้า musicOn == 1 {
                    หยุดเพลง()
                    # หาแคช: เคย decode songIdx นี้แล้วใช้แฮนเดิลเดิม (ไม่อ่าน WAV ซ้ำ → ไม่มี hitch)
                    bind ci = 0;  bind hitแคช = 0.0 - 1.0
                    ขณะที่ ci < ความยาว(เพลงแคชId) {
                        ถ้า list_get(เพลงแคชId, ci) == songIdx { bind hitแคช = ci }
                        bind ci = ci + 1
                    }
                    ถ้า hitแคช >= 0.0 {
                        bind เพลงเล่น = list_get(เพลงแคชH, hitแคช)
                        bind ยาวเพลงเล่น = list_get(เพลงแคชLen, hitแคช)
                    }
                    มิฉะนั้น {
                        bind เพลงเล่น = โหลดเพลง(wavPath)
                        bind ยาวเพลงเล่น = ความยาวเพลง(เพลงเล่น)
                        bind เพลงแคชId  = list_push(เพลงแคชId, songIdx)
                        bind เพลงแคชH   = list_push(เพลงแคชH, เพลงเล่น)
                        bind เพลงแคชLen = list_push(เพลงแคชLen, ยาวเพลงเล่น)
                    }
                    bind เพลงโหลดแล้ว = songIdx
                    เล่นเพลง(เพลงเล่น)
                }
            }

            # วนเพลงเกม (story)
            ถ้า midiClockOn > 0.5 { ถ้า gameMode == 0 { ถ้า musicOn == 1 { ถ้า ยาวเพลงเล่น > 1.0 {
                ถ้า ตำแหน่งเพลง() >= ยาวเพลงเล่น - 0.5 { ค้นหาเพลง(0);  เล่นเพลง(เพลงเล่น) }
            } } } }

            # ── Battle audio director: มอนใกล้/เปิดไฟต์ = สลับ Battle.wav, ฆ่าหมด = เฟดกลับเพลงเลเวล ──
            bind ใกล้มอน = 0.0
            bind mi2 = 0
            ขณะที่ mi2 < มอนจำนวน {
                ถ้า ใกล้มอน < 0.5 { ถ้า list_get(มอนเป็น, mi2) > 0 {
                    bind mdx2 = list_get(มอนX, mi2) - cx;  bind mdz2 = list_get(มอนZ, mi2) - cz
                    ถ้า mdx2*mdx2 + mdz2*mdz2 < 3844.0 { bind ใกล้มอน = 1.0 }
                } }
                bind mi2 = mi2 + 1
            }
            # อาร์เคด = ศัตรู (foe) ไม่ใช่มอน → ไม่เปิดเพลงต่อสู้ (มอนจำนวน=0 อยู่แล้ว → ใกล้มอน=0)
            # เพลงต่อสู้จุดเมื่อ "ตีโดนมอน" เท่านั้น (ตั้ง ฮีทต่อสู้=1.0 ในบล็อกต่อสู้); ที่นี่แค่ sustain/decay
            ถ้า ฮีทต่อสู้ > 0.0 { ถ้า ใกล้มอน > 0.5 { bind ฮีทต่อสู้ = 1.0 } }   # เริ่มไฟต์แล้ว + อยู่ในระยะมอนเป็น → คงไว้
            ถ้า ฮีทต่อสู้ > 0.0 { bind ฮีทต่อสู้ = ฮีทต่อสู้ - 0.016 }            # ฆ่า/ออกนอกระยะ → ลดลง ~1 วิ แล้วหยุด

            bind เข้าไฟต์ = 0.0
            ถ้า ฮีทต่อสู้ > 0.0 { bind เข้าไฟต์ = 1.0 }

            ถ้า musicOn == 1 {
                ถ้า เข้าไฟต์ > 0.5 {
                    ถ้า โหมดต่อสู้ < 0.5 {
                        bind โหมดต่อสู้ = 1.0;  bind เฟดต่อสู้ = 0.0
                        หยุดเพลง();  เล่นเพลง(เพลงต่อสู้)
                        bind sfxจำนวน = ต่อสู้จำนวน;  bind sfxความถี่ = ต่อสู้ความถี่
                    }
                    bind เฟดต่อสู้ = เฟดต่อสู้ + 0.12;  ถ้า เฟดต่อสู้ > 1.0 { bind เฟดต่อสู้ = 1.0 }   # เฟดอินเร็ว
                    bind vfade = เฟดต่อสู้;  ถ้า ฮีทต่อสู้ < vfade { bind vfade = ฮีทต่อสู้ }            # เฟดเอาต์ตามฮีท (~1 วิ)
                    ระดับเพลง(ปริมาณBGM * vfade)
                    ถ้า midiClockOn > 0.5 { ถ้า ยาวเพลงต่อสู้ > 1.0 { ถ้า ตำแหน่งเพลง() >= ยาวเพลงต่อสู้ - 0.5 { ค้นหาเพลง(0);  เล่นเพลง(เพลงต่อสู้) } } }
                }
                มิฉะนั้น {
                    ถ้า โหมดต่อสู้ > 0.5 {                                  # ฮีทหมด (เฟดจบ) → หยุดเพลงต่อสู้ กลับเพลงเลเวล
                        bind เฟดต่อสู้ = 0.0;  bind โหมดต่อสู้ = 0.0
                        bind sfxจำนวน = เก็บจำนวน;  bind sfxความถี่ = เก็บความถี่
                        หยุดเพลง();  เล่นเพลง(เพลงเล่น);  ระดับเพลง(ปริมาณBGM)
                    }
                }
            }

            # ── แชต: Enter เปิด/ส่ง, พิมพ์ด้วยคีย์บอร์ด (text_poll), Esc ยกเลิก ──
            ถ้า chatActive == 0 {
                ถ้า กดปุ่ม("enter") { bind chatActive = 1;  text_clear() }
            }
            มิฉะนั้น {
                bind chatBuf = text_poll()
                ถ้า กดปุ่ม("enter") {
                    bind m = text_get()
                    ถ้า m != "" {
                        bind chatMsg = m;  bind chatMsgT = 5.0
                        bind chatLog = list_push(chatLog, m)
                        bind chatSend = m;  bind chatSendT = 0.6   # ส่งข้ามเครือข่าย
                    }
                    text_clear();  bind chatActive = 0
                }
                ถ้า กดปุ่ม("escape") { text_clear();  bind chatActive = 0 }
            }
            ถ้า chatMsgT > 0.0 { bind chatMsgT = chatMsgT - 0.016 }
            ถ้า chat2T  > 0.0 { bind chat2T  = chat2T  - 0.016 }
            ถ้า chatSendT > 0.0 { bind chatSendT = chatSendT - 0.016 }
            ถ้า ภารกิจT > 0.0 { bind ภารกิจT = ภารกิจT - 0.016 }

            # Esc พักเกม (เฉพาะตอนไม่ได้แชต)
            ถ้า chatActive == 0 { ถ้า นำ("escape", gNavS) { bind paused = 1 - paused;  bind sel = 0 } }   # Esc / จอยปุ่ม Start = พัก/เล่นต่อ
            ถ้า chatActive == 0 { ถ้า กดปุ่ม("t") { bind เอฟเฟกต์เทรล = 1.0 - เอฟเฟกต์เทรล;  ถ้า เอฟเฟกต์เทรล > 0.5 { พิมพ์ฟ้า("  เทรล/วงแหวนโซนิค: เปิด") }  มิฉะนั้น { พิมพ์ฟ้า("  เทรล/วงแหวนโซนิค: ปิด") } } }   # T = สลับเทรล (ทดสอบคอขวด)
            ถ้า chatActive == 0 { ถ้า กดปุ่ม("y") { bind แสดงสหาย = 1.0 - แสดงสหาย;  ถ้า แสดงสหาย > 0.5 { พิมพ์ฟ้า("  เมชสหาย: เปิด") }  มิฉะนั้น { พิมพ์ฟ้า("  เมชสหาย: ปิด") } } }   # Y = สลับเมชสหาย (ทดสอบคอขวด)
            ถ้า chatActive == 0 { ถ้า กดปุ่ม("u") { bind distortQ = distortQ + 1.0;  ถ้า distortQ > 2.5 { bind distortQ = 0.0 }  ถ้า distortQ > 1.5 { พิมพ์ฟ้า("  บิดจอ: ครึ่งเรส (ถูก)") }  มิฉะนั้น { ถ้า distortQ > 0.5 { พิมพ์ฟ้า("  บิดจอ: เต็ม") }  มิฉะนั้น { พิมพ์ฟ้า("  บิดจอ: ปิด") } } } }   # U = วนคุณภาพบิดจอ

            bind doUpdate = 1
            ถ้า paused == 1 { ถ้า gameMode == 0 { bind doUpdate = 0 } }
            ถ้า chatActive == 1 { bind doUpdate = 0 }   # หยุดอินพุตเกมขณะพิมพ์

            ถ้า doUpdate == 1 {
                # ── cache มอนต่อเฟรม: ลดการคำนวณซ้ำ (ชนิด+ตำแหน่ง) ที่ถูกเรียกหลายลูป ──
                bind mc = 0
                ขณะที่ mc < มอนจำนวน {
                    bind mt = list_get(มอนไทป์, mc)
                    bind mx = list_get(มอนX, mc);  bind mz = list_get(มอนZ, mc);  bind manim = list_get(มอนAnim, mc)
                    ถ้า list_get(มอนเป็น, mc) > 0 {
                        # ── ตัวจับเวลาแอนิเมชัน: โดนตี (สั่น) ลดทุกเฟรม ──
                        bind mht = list_get(มอนHitT, mc);  ถ้า mht > 0.0 { bind มอนHitT = ตั้งรายการ(มอนHitT, mc, mht - 0.016) }
                        bind mdt = list_get(มอนDieT, mc)
                        ถ้า mdt > 0.0 {
                            # ── กำลังตาย: ยุบ+เผา อยู่กับที่ (ไม่วิ่ง AI); จบแล้วเอาออก ──
                            bind mdt = mdt - 0.02
                            ถ้า mdt <= 0.0 { bind mdt = 0.0;  bind มอนเป็น = ตั้งรายการ(มอนเป็น, mc, 0) }
                            bind มอนDieT = ตั้งรายการ(มอนDieT, mc, mdt)
                        }
                        มิฉะนั้น {
                            # แช่แข็งมอนไกล (เลย draw-cull ~200 + ระยะชน) → ข้าม ตำแหน่งมอน (สปีชีส์/กระจาย/pgRand/trig) = ประหยัด CPU มาก ตอนมอนเยอะ
                            bind fdx = cx - mx;  bind fdz = cz - mz
                            ถ้า fdx*fdx + fdz*fdz < 52900.0 {
                                bind mAng = list_get(มอนโกรธ, mc)
                                ถ้า mAng > 0.0 { bind mAng = mAng - 0.02;  ถ้า mAng < 0.0 { bind mAng = 0.0 } }
                                # ล่าตามระยะ: เข้าใกล้พอ = ได้กลิ่น ตั้งความแค้นเอง แม้ยังไม่โดนตี
                                ถ้า fdx*fdx + fdz*fdz < 4900.0 { ถ้า mAng < 3.0 { bind mAng = 3.0 } }
                                bind มอนโกรธ = ตั้งรายการ(มอนโกรธ, mc, mAng)
                                bind mpc = ตำแหน่งมอน(mc, มอนseed, t, มอนspread, mt, cx, cz, mAng)
                                bind mx = list_get(mpc,0);  bind mz = list_get(mpc,1);  bind manim = list_get(mpc,2)
                            }
                        }
                    }
                    bind มอนX = ตั้งรายการ(มอนX, mc, mx)
                    bind มอนZ = ตั้งรายการ(มอนZ, mc, mz)
                    bind มอนAnim = ตั้งรายการ(มอนAnim, mc, manim)
                    bind mc = mc + 1
                }
                # ── cache จุดเก็บต่อเฟรม: ใช้ร่วมกันทั้งชน/วาด ลดคำนวณตำแหน่งซ้ำ ──
                ถ้า บอสโหมด < 0.5 {
                    bind cc = 0
                    ขณะที่ cc < เก็บจำนวน {
                        bind gx = list_get(เก็บX, cc);  bind gy = LVL_F - 2.2;  bind gz = list_get(เก็บZ, cc)
                        ถ้า ด่าน > 0.5 {
                            bind gp = ตำแหน่งเก็บ2(cc, เก็บจำนวน, ด่าน)
                            bind gx = list_get(gp, 0);  bind gz = list_get(gp, 2)
                            bind gy = latticeHeight(gx, gz, ด่าน, ฉากทางออก) - 2.2    # วางบนพื้นแลตทิซ (อยู่บนสุด ไม่จมใต้พื้น)
                        }
                        มิฉะนั้น {
                            bind rm = cc - ปัดลง(cc/5.0)*5.0;  bind rcx = 0.0;  bind rcz = 0.0
                            ถ้า rm == 1.0 { bind rcz = 0.0-704.0 }
                            ถ้า rm == 2.0 { bind rcz = 704.0 }
                            ถ้า rm == 3.0 { bind rcx = 704.0 }
                            ถ้า rm == 4.0 { bind rcx = 0.0-704.0 }
                            bind gx = rcx + gx*1.5;  bind gz = rcz + gz*1.5;  bind gy = GROUND - 2.2
                        }
                        bind เก็บXแคช = ตั้งรายการ(เก็บXแคช, cc, gx)
                        bind เก็บYแคช = ตั้งรายการ(เก็บYแคช, cc, gy)
                        bind เก็บZแคช = ตั้งรายการ(เก็บZแคช, cc, gz)
                        bind cc = cc + 1
                    }
                }
                # ── เมาส์ = หมุนกล้องโคจร (yaw) + เงย/ก้ม (pitch) ──
                bind camYaw = camYaw + เมาส์X() * 0.005
                bind camPitch = camPitch + เมาส์Y() * 0.004
                bind grx = จอยแกน("rx");  bind gry = จอยแกน("ry")   # จอย: สติ๊กขวา = มองกล้อง
                ถ้า grx*grx + gry*gry > 0.04 { bind camYaw = camYaw + grx*0.045;  bind camPitch = camPitch + gry*0.035 }
                ถ้า camPitch < 0.0-0.45 { bind camPitch = 0.0-0.45 }
                ถ้า camPitch > 1.15 { bind camPitch = 1.15 }
                # ── การ์ด: คลิกขวา = ตั้งการ์ด (ลดดาเมจ 80%); หลุดเมื่อโจมตี หรือครบ 6 วิ ──
                bind gGuard = 0.0;  ถ้า เมาส์ขวา() { bind gGuard = 1.0 };  ถ้า จอยปุ่ม("l1") { bind gGuard = 1.0 };  ถ้า จอยปุ่ม("y") { bind gGuard = 1.0 }   # L1/Y = การ์ด(โล่)
                ถ้า gGuard > 0.5 {
                    ถ้า guardHeld < 0.5 { bind guardT = 0.0 }       # เพิ่งกด → รีเซ็ตจับเวลา
                    bind guardHeld = 1.0
                    bind guardT = guardT + 0.016
                    ถ้า guardT < 6.0 { bind guarding = 1.0 }  มิฉะนั้น { bind guarding = 0.0 }
                }
                มิฉะนั้น { bind guardHeld = 0.0;  bind guarding = 0.0;  bind guardT = 0.0 }
                # ── โจมตี: คลิกซ้าย / R1 / X = ฟันดาบ·ใช้ไอเทมที่เลือก · B = HyperBomb ตรง ──
                bind mbNow = 0.0
                ถ้า mouse_down() { bind mbNow = 1.0 }
                ถ้า จอยปุ่ม("r1") { bind mbNow = 1.0 }   # R1 = ฟัน
                ถ้า จอยปุ่ม("x") { bind mbNow = 1.0 }    # X = ฟันดาบ
                bind atkItem = เลือกไอเทม
                ถ้า จอยปุ่ม("b") { bind mbNow = 1.0;  bind atkItem = 2.0 }   # B = ระเบิด (ข้ามไอเทมที่เลือก)
                ถ้า mbNow > 0.5 { ถ้า mbDown < 0.5 {
                    bind guarding = 0.0;  bind guardT = 6.0          # โจมตี → การ์ดหลุด (จนปล่อยคลิกขวา)
                    # ── ไอเทม 1 = ดาบ: ฟันคอมโบ จูนกับโน้ต MIDI ──
                    ถ้า atkItem == 1.0 { ถ้า สกิล >= 1.0 { ถ้า slashCD <= 0.0 {
                        bind sCmbMax = 2.5
                        bind sstA = list_get(ดาบสเตตัส, floor(swordType));  bind sCmbMax = list_get(sstA,3) - 0.5   # ดาบเท่ 6 แบบ — ทั้งสตอรี่+อาร์เคด
                        # คูลดาวน์ฟัน: 1/4 ของเดิม แล้วลดต่อด้วยบัฟ AGI + เลเวลสกิล (ยิ่งเก่งยิ่งฟันไว) — ต่ำสุด 0.05 วิ
                        bind slashCD = list_get(sstA,0) * 0.25
                        bind slashCD = slashCD / (1.0 + list_get(ผู้เล่นสเตตัส,1)*0.03 + สกิล*0.04)
                        ถ้า slashCD < 0.05 { bind slashCD = 0.05 }
                        ถ้า อาร์เคดโหมด > 0.5 { bind slashCD = slashCD * 0.5 }   # อาร์เคด: คูลดาวน์ฟันครึ่งเดียว → ฟันถี่ขึ้น ~2 เท่า
                        bind slashT = 0.16;  bind slashHit = 0.0
                        bind slashDir = slashDir + 1.0;  ถ้า slashDir > 7.5 { bind slashDir = 0.0 }   # วนทิศฟัน
                        bind combo = combo + 1.0;  ถ้า combo > sCmbMax { bind combo = 0.0 }
                        bind aimYaw = camYaw                                          # K+M: เล็งกล้อง (เมาส์)
                        bind stickMag = จอยแกน("lx")*จอยแกน("lx") + จอยแกน("ly")*จอยแกน("ly")
                        ถ้า stickMag > 0.1 { ถ้า vx*vx + vz*vz > 0.0004 { bind aimYaw = atan2(vx, vz) } }   # จอย: เล็งทิศเคลื่อนที่
                        bind arcType = combo;  bind arcYaw = aimYaw;  bind arcFade = 0.5
                        bind slashNote = slashNote + 1.0
                        bind sidx = slashNote - ปัดลง(slashNote / sfxจำนวน) * sfxจำนวน
                        bind sfreq = list_get(sfxความถี่, sidx)
                        bind smul = 1.0
                        ถ้า combo == 1.0 { bind smul = 1.5 }
                        ถ้า combo == 2.0 { bind smul = 2.0 }
                        # ── สวูชดาบ: จูนกับโน้ตเพลง + นอยส์ ที่ปลายดาบ ──
                        bind swtx = cx + ไซน์(camYaw)*8.5;  bind swtz = cz + โคไซน์(camYaw)*8.5
                        เสียงเอฟเฟกต์(swtx, cy, swtz, 1.0, sfreq*smul*1.2, 0.32*ปริมาณSFX, 0.15, "saw")
                        เสียงเอฟเฟกต์(swtx, cy, swtz, 1.0, sfreq*smul*2.6, 0.16*ปริมาณSFX, 0.11, "noise")
                        # ── เสียงฟันตามผล: โดนเต็ม / โดนการ์ด / พลาด — เล่นที่จุดปะทะ ──
                        bind hx = cx + ไซน์(camYaw)*9.0;  bind hz = cz + โคไซน์(camYaw)*9.0
                        bind hitFound = 0.0;  bind hitGuard = 0.0
                        bind sj = 0
                        ขณะที่ sj < มอนจำนวน {
                            ถ้า hitFound < 0.5 { ถ้า list_get(มอนเป็น, sj) > 0 {
                                bind sdx = list_get(มอนX, sj)-cx;  bind sdz = list_get(มอนZ, sj)-cz
                                ถ้า sdx*sdx + sdz*sdz < 256.0 { ถ้า sdx*ไซน์(camYaw) + sdz*โคไซน์(camYaw) > 0.0 {
                                    bind hitFound = 1.0;  bind hx = list_get(มอนX, sj);  bind hz = list_get(มอนZ, sj)
                                } }
                            } }
                            bind sj = sj + 1
                        }
                        ถ้า hitFound > 0.5 {
                            ถ้า hitGuard > 0.5 { เสียงเอฟเฟกต์(hx, cy, hz, 1.0, 170.0, 0.55*ปริมาณSFX, 0.16, "square") }   # การ์ด: คลังก์ทึบ
                            มิฉะนั้น { เสียงเอฟเฟกต์(hx, cy, hz, 1.0, sfreq*smul*2.0, 0.6*ปริมาณSFX, 0.15, "sine") }        # โดนเต็ม: ติงสว่าง
                        }
                        มิฉะนั้น { เสียงเอฟเฟกต์(hx, cy, hz, 1.0, 130.0, 0.34*ปริมาณSFX, 0.2, "saw") }                       # พลาด: วูช
                        ถ้า vsโหมด == 1 { ถ้า (cx-cx2)*(cx-cx2) + (cz-cz2)*(cz-cz2) < 324.0 { bind ตีโดน = ตีโดน + 6.0*smul } }  # VS: ฟันโดนคู่ต่อสู้
                    } } }
                    # ── ไอเทม 2 = HyperBomb: ระเบิด AoE ฆ่ามอนรอบตัว (มีคูลดาวน์) ──
                    ถ้า atkItem == 2.0 { ถ้า สกิล >= 2.0 { ถ้า bombCool <= 0.0 {
                        bind bombT = 0.55;  bind bombCool = 2.5
                        เสียงเอฟเฟกต์(0,0,0, 1.0, 70.0, 0.7, 0.7, "saw")
                        bind bk = 0
                        ขณะที่ bk < มอนจำนวน {
                            ถ้า list_get(มอนเป็น, bk) > 0 {
                                bind bmx = list_get(มอนX, bk);  bind bmz = list_get(มอนZ, bk)
                                ถ้า (cx-bmx)*(cx-bmx) + (cz-bmz)*(cz-bmz) < 8100.0 { ถ้า list_get(มอนDieT, bk) <= 0.0 {
                                    bind มอนDieT = ตั้งรายการ(มอนDieT, bk, 1.0);  bind คะแนน = คะแนน + 5
                                } }
                            }
                            bind bk = bk + 1
                        }
                        # อาร์เคด: ระเบิดสตัน — หลับ + ช้า (เฉพาะศัตรู) + ดาเมจ ถ้าอยู่ในรัศมี
                        ถ้า อาร์เคดโหมด > 0.5 { ถ้า (cx-foeX)*(cx-foeX) + (cz-foeZ)*(cz-foeZ) < 8100.0 {
                            bind foeSleep = 1.8;  bind foeSlow = 3.5;  bind foeHP = foeHP - 6.0;  bind foeHurt = 0.3
                            bind popDmg = 6.0;  bind popDmgT = 0.75;  bind popDmgCrit = 0.0
                            เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, 60.0, 0.6*ปริมาณSFX, 0.5, "noise")
                        } }
                        พิมพ์แดง("  💥 HyperBomb!")
                    } } }
                    # ── ไอเทม 3-9 = อาวุธ AoE (รัศมี/ดาเมจ/คูลดาวน์/วิช่วล ต่างกัน) ──
                    ถ้า atkItem >= 3.0 { ถ้า สกิล >= atkItem { ถ้า wfxCool <= 0.0 {
                        bind wfxType = atkItem;  bind wfxT = 0.45;  bind wfxFire = 1.0;  bind wfxX = cx;  bind wfxZ = cz
                        bind wfxRad = 70.0;  bind wfxDmg = 4.0;  bind wfxCool = 1.2;  bind wsnd = 200.0
                        ถ้า atkItem == 3.0 { bind wfxRad = 80.0;  bind wfxDmg = 5.0;  bind wfxCool = 1.0;  bind wsnd = 320.0;  bind vx = vx + ไซน์(camYaw)*0.9;  bind vz = vz + โคไซน์(camYaw)*0.9 }   # Torpedo: พุ่ง
                        ถ้า atkItem == 4.0 { bind wfxRad = 95.0;  bind wfxDmg = 3.0;  bind wfxCool = 1.6;  bind wsnd = 150.0;  bind บาดเจ็บ = 1.3 }                                              # CounterAttack: ช็อค+อมตะชั่วครู่
                        ถ้า atkItem == 5.0 { bind wfxRad = 58.0;  bind wfxDmg = 7.0;  bind wfxCool = 0.8;  bind wsnd = 260.0;  bind slashT = 0.18;  bind arcType = 1.0;  bind arcYaw = camYaw;  bind arcFade = 0.5 }  # DualSword
                        ถ้า atkItem == 6.0 { bind wfxRad = 66.0;  bind wfxDmg = 2.0;  bind wfxCool = 0.5;  bind wsnd = 90.0;   bind burnT = 2.2 }                                              # Burn (DoT)
                        ถ้า atkItem == 7.0 { bind wfxRad = 115.0; bind wfxDmg = 1.5;  bind wfxCool = 2.2;  bind wsnd = 1300.0; bind freezeT = 2.6 }                                            # Freeze
                        ถ้า atkItem == 8.0 { bind wfxRad = 125.0; bind wfxDmg = 4.0;  bind wfxCool = 1.6;  bind wsnd = 520.0 }                                                                # Nova
                        ถ้า atkItem == 9.0 { bind wfxRad = 150.0; bind wfxDmg = 6.0;  bind wfxCool = 2.4;  bind wsnd = 60.0 }                                                                 # Quake
                        bind wni = slashNote - ปัดลง(slashNote / sfxจำนวน) * sfxจำนวน
                        bind wfreq = list_get(sfxความถี่, wni) * (wsnd / 200.0)        # โน้ตเพลง × คาแรกเตอร์อาวุธ (จูนตามแทร็ก active)
                        เสียงเอฟเฟกต์(cx, cy, cz, 1.0, wfreq, 0.6*ปริมาณSFX, 0.5, "saw")
                        bind slashNote = slashNote + 1.0
                    } } }
                } }
                # ── เบิร์น: ดาเมจต่อเนื่องรอบตัวขณะ burnT > 0 ──
                ถ้า burnT > 0.0 { bind burnT = burnT - 0.016;  bind wfxFire = 1.0;  bind wfxType = 6.0;  bind wfxRad = 66.0;  bind wfxDmg = 0.18;  bind wfxX = cx;  bind wfxZ = cz;  ถ้า wfxT < 0.1 { bind wfxT = 0.15 } }
                # ── ยิงอาวุธ AoE: ทำดาเมจมอนในรัศมี (+ ดรอปบัฟ เหมือนฟันดาบ) ──
                ถ้า wfxFire == 1.0 {
                    bind wk = 0
                    ขณะที่ wk < มอนจำนวน {
                        ถ้า list_get(มอนเป็น, wk) > 0 {
                            bind wmx = list_get(มอนX, wk);  bind wmz = list_get(มอนZ, wk)
                            ถ้า (cx-wmx)*(cx-wmx) + (cz-wmz)*(cz-wmz) < wfxRad*wfxRad {
                                bind มอนHP = ตั้งรายการ(มอนHP, wk, list_get(มอนHP, wk) - wfxDmg)  bind ฮีทต่อสู้ = 1.0
                                # ระเบิด/คลื่น: เสียหลักหนัก แฟลชยาวกว่าดาบ และกระเด็นออกจากจุดระเบิด
                                bind มอนHitT = ตั้งรายการ(มอนHitT, wk, 0.75)
                                bind มอนโกรธ = ตั้งรายการ(มอนโกรธ, wk, 9.0)
                                bind wbx = list_get(มอนX, wk) - cx;  bind wbz = list_get(มอนZ, wk) - cz
                                bind wbd = รากที่สอง(wbx*wbx + wbz*wbz) + 0.001
                                bind มอนX = ตั้งรายการ(มอนX, wk, list_get(มอนX, wk) + wbx/wbd * 9.0)
                                bind มอนZ = ตั้งรายการ(มอนZ, wk, list_get(มอนZ, wk) + wbz/wbd * 9.0)
                                เสียงมอนเจ็บ(list_get(สปีชีส์มอน(wk, มอนseed), 0), list_get(มอนX, wk), cy, list_get(มอนZ, wk), ปริมาณSFX, 1.0)
                                bind มอนHitT = ตั้งรายการ(มอนHitT, wk, 0.35)
                                ถ้า list_get(มอนHP, wk) <= 0.0 { ถ้า list_get(มอนDieT, wk) <= 0.0 {
                                    bind มอนDieT = ตั้งรายการ(มอนDieT, wk, 1.0);  bind คะแนน = คะแนน + 5
                                    bind wst = สเตตัสมอน(wk, ด่าน);  bind wsi = wk - ปัดลง(wk/8.0)*8.0
                                    bind ดรอปเป็น = ตั้งรายการ(ดรอปเป็น, wk, 1);  bind ดรอปX = ตั้งรายการ(ดรอปX, wk, wmx);  bind ดรอปZ = ตั้งรายการ(ดรอปZ, wk, wmz)
                                    bind ดรอปสเตตัส = ตั้งรายการ(ดรอปสเตตัส, wk, wsi);  bind ดรอปค่า = ตั้งรายการ(ดรอปค่า, wk, list_get(wst, wsi))
                                } }
                            }
                        }
                        bind wk = wk + 1
                    }
                    # VS เครือข่าย: โดนคู่ต่อสู้ (ball2) ด้วย
                    ถ้า vsโหมด == 1 { ถ้า (cx-cx2)*(cx-cx2) + (cz-cz2)*(cz-cz2) < wfxRad*wfxRad { bind ตีโดน = ตีโดน + wfxDmg } }
                    ถ้า อาร์เคดโหมด > 0.5 { ถ้า (cx-foeX)*(cx-foeX) + (cz-foeZ)*(cz-foeZ) < wfxRad*wfxRad {
                        bind dmgW = wfxDmg * (1.0 - foeRes);  ถ้า foeGuard > 0.5 { bind dmgW = dmgW * 0.3 }
                        bind foeHP = foeHP - dmgW;  bind foeHurt = 0.3
                    } }
                    bind wfxFire = 0.0
                }
                ถ้า wfxT > 0.0 { bind wfxT = wfxT - 0.016 }
                ถ้า wfxCool > 0.0 { bind wfxCool = wfxCool - 0.016 }
                ถ้า freezeT > 0.0 { bind freezeT = freezeT - 0.016 }
                bind mbDown = mbNow
                ถ้า slashT > 0.0 { bind slashT = slashT - 0.016 }
                ถ้า slashCD > 0.0 { bind slashCD = slashCD - 0.016 }
                ถ้า arcFade > 0.0 { bind arcFade = arcFade - 0.016 }
                ถ้า bombT > 0.0 { bind bombT = bombT - 0.016 }
                ถ้า bombCool > 0.0 { bind bombCool = bombCool - 0.016 }
                # ── เลือกไอเทม: เลข 1-9 หรือล้อเมาส์ (วนในสกิลที่ได้) ──
                ถ้า กดปุ่ม("1") { bind เลือกไอเทม = 1.0 }
                ถ้า กดปุ่ม("2") { bind เลือกไอเทม = 2.0 }
                ถ้า กดปุ่ม("3") { bind เลือกไอเทม = 3.0 }
                ถ้า กดปุ่ม("4") { bind เลือกไอเทม = 4.0 }
                ถ้า กดปุ่ม("5") { bind เลือกไอเทม = 5.0 }
                ถ้า กดปุ่ม("6") { bind เลือกไอเทม = 6.0 }
                ถ้า อาร์เคดโหมด < 0.5 { ถ้า กดปุ่ม("f") { bind swordType = swordType + 1.0;  ถ้า swordType > 5.5 { bind swordType = 0.0 };  เสียงเอฟเฟกต์(cx,cy,cz, 1.0, 520.0+swordType*40.0, 0.4*ปริมาณSFX, 0.12, "sine") } }   # สตอรี่: สลับดาบเท่ (1 ใน 6)
                ถ้า กดปุ่ม("7") { bind เลือกไอเทม = 7.0 }
                ถ้า กดปุ่ม("8") { bind เลือกไอเทม = 8.0 }
                ถ้า กดปุ่ม("9") { bind เลือกไอเทม = 9.0 }
                bind sc = mouse_scroll()
                ถ้า sc > 0.5 { bind เลือกไอเทม = เลือกไอเทม + 1.0 }
                ถ้า sc < 0.0-0.5 { bind เลือกไอเทม = เลือกไอเทม - 1.0 }
                ถ้า เลือกไอเทม > สกิล { bind เลือกไอเทม = 1.0 }
                ถ้า เลือกไอเทม < 1.0 { bind เลือกไอเทม = สกิล }
                # ── Shift = boost: เร่งความเร็วลูก ──
                bind shiftHeld = 0.0
                ถ้า กดค้าง("lshift") { bind shiftHeld = 1.0 }
                ถ้า กดค้าง("rshift") { bind shiftHeld = 1.0 }
                ถ้า จอยปุ่ม("rt") { bind shiftHeld = 1.0 }   # RT (ทริกเกอร์ขวา) = boost
                # ── Z-target (simple): กด Z สลับล็อกเป้า + วงแหวนเหนือเป้า ──
                ถ้า กดปุ่ม("z") {
                    bind zLockOn = 1.0 - zLockOn;  bind zLockValid = 0.0
                    ถ้า zLockOn > 0.5 { พิมพ์ฟ้า("  ◎ z-target: ON") }
                    มิฉะนั้น { พิมพ์เหลือง("  ◎ z-target: OFF") }
                }
                ถ้า zLockOn > 0.5 {
                    bind zTick = 1.0 - zTick
                    ถ้า zTick > 0.5 {
                        bind tHit = 0.0;  bind tBest = 999999.0
                        bind tx = 0.0;  bind tz = 0.0;  bind ty = GROUND
                        ถ้า บอสโหมด > 0.5 {
                            bind tx = 0.0;  bind tz = 0.0;  bind ty = BOSS_Y;  bind tHit = 1.0
                            bind tBest = cx*cx + cz*cz
                        }
                        มิฉะนั้น {
                            ถ้า อาร์เคดโหมด > 0.5 {
                                ถ้า foeHP > 0.0 {
                                    bind tx = foeX;  bind tz = foeZ;  bind ty = foeY
                                    bind tBest = (cx-foeX)*(cx-foeX) + (cz-foeZ)*(cz-foeZ);  bind tHit = 1.0
                                }
                            }
                            มิฉะนั้น {
                                bind lk = 0
                                ขณะที่ lk < มอนจำนวน {
                                    ถ้า list_get(มอนเป็น, lk) > 0 {
                                        bind lmx = list_get(มอนX, lk);  bind lmz = list_get(มอนZ, lk)
                                        bind ld2 = (cx-lmx)*(cx-lmx) + (cz-lmz)*(cz-lmz)
                                        ถ้า ld2 < tBest { bind tBest = ld2;  bind tx = lmx;  bind tz = lmz;  bind tHit = 1.0 }
                                    }
                                    bind lk = lk + 1
                                }
                                ถ้า tHit > 0.5 { ถ้า ด่าน > 0.5 { bind ty = latticeHeight(tx, tz, ด่าน, ฉากทางออก) } }
                            }
                        }
                        ถ้า tHit > 0.5 { ถ้า tBest < 22500.0 {
                            bind zLockX = tx;  bind zLockY = ty;  bind zLockZ = tz;  bind zLockValid = 1.0
                        } มิฉะนั้น { bind zLockValid = 0.0;  bind zLockOn = 0.0 } }
                        มิฉะนั้น { bind zLockValid = 0.0;  bind zLockOn = 0.0 }
                    }
                    ถ้า zLockValid > 0.5 {
                        bind tyl = atan2(zLockX - cx, zLockZ - cz)
                        bind dyl = tyl - camYaw
                        ขณะที่ dyl > 3.14159 { bind dyl = dyl - 6.28318 }
                        ขณะที่ dyl < 0.0-3.14159 { bind dyl = dyl + 6.28318 }
                        bind camYaw = camYaw + dyl * 0.22
                    }
                }
                bind fx = ไซน์(camYaw);  bind fz = โคไซน์(camYaw)
                bind rgx = fz;  bind rgz = 0.0 - fx
                # ── กลิ้ง: W/S = หน้า/หลัง, Q/E = ข้าง (สัมพัทธ์กล้อง) — แรงตรง (responsive) ──
                bind mvx = 0.0;  bind mvz = 0.0
                ถ้า กดค้าง("w") { bind mvx = mvx + fx;   bind mvz = mvz + fz }
                ถ้า กดค้าง("s") { bind mvx = mvx - fx;   bind mvz = mvz - fz }
                # A/D (และ Q/E) = กลิ้งซ้าย/ขวา สัมพัทธ์กล้อง
                ถ้า กดค้าง("d") { bind mvx = mvx + rgx;  bind mvz = mvz + rgz }
                ถ้า กดค้าง("a") { bind mvx = mvx - rgx;  bind mvz = mvz - rgz }
                ถ้า กดค้าง("e") { bind mvx = mvx + rgx;  bind mvz = mvz + rgz }
                ถ้า กดค้าง("q") { bind mvx = mvx - rgx;  bind mvz = mvz - rgz }
                # ── จอย: สติ๊กซ้าย = เดิน (สัมพัทธ์กล้อง) ──
                bind glx = จอยแกน("lx");  bind gly = จอยแกน("ly")
                ถ้า glx*glx + gly*gly > 0.04 { bind mvx = mvx + fx*gly + rgx*glx;  bind mvz = mvz + fz*gly + rgz*glx }
                bind ml = รากที่สอง(mvx*mvx + mvz*mvz)
                bind moveAcc = 0.12
                ถ้า shiftHeld > 0.5 { bind moveAcc = 0.18 }
                ถ้า ml > 0.01 {
                    bind vx = vx + (mvx/ml) * moveAcc
                    bind vz = vz + (mvz/ml) * moveAcc
                }
                ถ้า อาร์เคดโหมด > 0.5 { bind compInit = 0.0;  bind kAlive = 0.0;  bind qAlive = 0.0 }

                # ── เบรก (Alt ซ้าย/ขวา / LT) — หน่วงความเร็วแนวนอนนุ่ม ๆ ──
                ถ้า กดค้าง("lalt") { bind vx = vx * 0.86;  bind vz = vz * 0.86 }
                ถ้า กดค้าง("ralt") { bind vx = vx * 0.86;  bind vz = vz * 0.86 }
                ถ้า จอยปุ่ม("lt") { bind vx = vx * 0.86;  bind vz = vz * 0.86 }   # LT (ทริกเกอร์ซ้าย) = เบรก

                # ── charge jump: กดค้าง Space ตอนแตะพื้นเพื่อชาร์จ, ปล่อย → กระโดด ──
                bind spNow = 0.0
                ถ้า กดค้าง("space") { bind spNow = 1.0 }
                ถ้า จอยปุ่ม("a") { bind spNow = 1.0 }   # จอย: A = กระโดด
                ถ้า spNow > 0.5 {
                    ถ้า grounded > 0.5 {
                        bind charge = charge + 0.085
                        ถ้า charge > 2.8 { bind charge = 2.8 }
                    }
                }
                มิฉะนั้น {
                    ถ้า spDown > 0.5 { ถ้า charge > 0.01 {
                        bind vy = vy - (0.9 + charge)
                        จอยสั่น(0.12, 0.28 + charge*0.1, 55)   # จอย: กระโดด (แรงตามชาร์จ)
                    } }
                    bind charge = charge * 0.4
                }
                bind spDown = spNow

                # ── integrate (manual แบบเดโม): gravity + damping + ก้าวตำแหน่ง ──
                bind vy = vy + 0.060
                bind vx = vx * 0.93;  bind vz = vz * 0.93;  bind vy = vy * 0.999   # คุมแน่นขึ้น (ตอบสนองไว, ดริฟต์น้อย — เหมือน AI ระดับสูง)
                bind cx = cx + vx;  bind cy = cy + vy;  bind cz = cz + vz

                # ── พื้น/เพดาน เด้ง (ความสูงพื้นขึ้นกับเลเวล: ramp/ledge ใน Lv2) ──
                bind floorH = GROUND
                ถ้า ด่าน > 0.5 { bind floorH = latticeHeight(cx, cz, ด่าน, ฉากทางออก) }
                ถ้า บอสโหมด > 0.5 { bind floorH = ความสูงอารีนา(cx, cz) }
                ถ้า อาร์เคดโหมด > 0.5 { bind floorH = ความสูงอารีนา(cx, cz) }
                ถ้า cy > floorH - R {
                    bind cy = floorH - R
                    ถ้า vy > 0.35 {                                              # กระแทกแรงพอ → เด้ง + ระฆัง
                        bind squash = vy * 0.5;  ถ้า squash > 0.6 { bind squash = 0.6 }
                        เสียงชน(sfxความถี่, sfxจำนวน, vy, ปริมาณSFX, cx, cy, cz)   # ระฆังกระทบพื้น (จูนเพลง/ไฟต์)
                        จอยสั่น(0.2, 0.45, 70)   # จอย: เด้งพื้น
                        bind vy = 0.0 - vy * 0.72
                    }
                    มิฉะนั้น { bind vy = 0.0 }                                    # กระแทกเบา → วางนิ่งบนพื้น (เลิกเด้งจิ๋ว)
                }
                ถ้า cy < LVL_C + R { bind cy = LVL_C + R;  bind vy = 0.0 - vy * 0.6 }
                bind grounded = 0.0
                ถ้า cy > floorH - R - 1.2 { bind grounded = 1.0 }

                # ── ผนังเด้ง (union กล่อง — เลเวล 1 = plus, เลเวล 2 = ลาน) ──
                bind hb = หากล่อง(cx, cz, R)
                ถ้า ด่าน > 0.5 { bind hb = latticeBlock(cx, cz, cy, R, ด่าน, ฉากทางออก) }   # แลตทิซ: กันปีนผนัง 90° (ขึ้นชั้นได้เฉพาะทางลาด)
                ถ้า บอสโหมด > 0.5 { bind hb = หากล่องอารีนา(cx, cz, R) }
                ถ้า อาร์เคดโหมด > 0.5 { bind hb = หากล่องอารีนา(cx, cz, R) }
                ถ้า list_get(hb,0) > 0.0 {
                    bind nx = list_get(hb,1) - cx;  bind nz = list_get(hb,2) - cz
                    bind nl = รากที่สอง(nx*nx + nz*nz)
                    ถ้า nl > 0.001 {
                        bind nx = nx/nl;  bind nz = nz/nl
                        bind cx = list_get(hb,1);  bind cz = list_get(hb,2)
                        bind vd = vx*nx + vz*nz
                        ถ้า vd < 0.0 { bind vx = vx - 1.5*vd*nx;  bind vz = vz - 1.5*vd*nz;  เสียงชน(sfxความถี่, sfxจำนวน, 0.0-vd, ปริมาณSFX, cx, cy, cz) }
                    }
                }
                # ── ชนสิ่งกีดขวางทรงกระบอก: เสาฮับ (Lv1) / กล่องตัวอักษร (Lv2) ──
                #    รูปแบบ [x, z, รัศมี] × N
                bind BUMP = [160.0,160.0,18.0, 0.0-160.0,160.0,18.0, 160.0,0.0-160.0,18.0, 0.0-160.0,0.0-160.0,18.0]
                ถ้า ด่าน > 0.5 { bind BUMP = [] }   # แลตทิซ: ผนัง/ขั้นจัดการใน latticeBlock
                ถ้า บอสโหมด > 0.5 { bind BUMP = [] }
                bind pcount = ความยาว(BUMP)
                bind pi = 0
                ขณะที่ pi < pcount {
                    bind ppx = list_get(BUMP,pi);  bind ppz = list_get(BUMP,pi+1)
                    bind dxp = cx-ppx;  bind dzp = cz-ppz
                    bind dp = รากที่สอง(dxp*dxp + dzp*dzp);  bind rrp = list_get(BUMP,pi+2) + R
                    ถ้า dp < rrp { ถ้า dp > 0.01 {
                        bind nx2 = dxp/dp;  bind nz2 = dzp/dp
                        bind cx = ppx + nx2*rrp;  bind cz = ppz + nz2*rrp
                        bind vd2 = vx*nx2 + vz*nz2
                        ถ้า vd2 < 0.0 { bind vx = vx - 1.5*vd2*nx2;  bind vz = vz - 1.5*vd2*nz2;  เสียงชน(sfxความถี่, sfxจำนวน, 0.0-vd2, ปริมาณSFX, cx, cy, cz) }
                    } }
                    bind pi = pi + 3
                }
                # ── ผู้เล่น 2 = ผู้เล่นเครือข่าย: ส่งลูกเรา, รับลูก peer → ball2 ──
                #   Host เผด็จการ (ส่ง x;y;z;level;score;bits); Join สะท้อนสถานะ Host
                ถ้า gameMode == 1 {
                    bind cf = "_";  ถ้า chatSendT > 0.0 { bind cf = chatSend }   # "_" = ไม่มีแชต
                    ถ้า coopRole == 0 {
                        bind bits = ""
                        bind biq = 0
                        ขณะที่ biq < เก็บจำนวน {
                            ถ้า biq > 0 { bind bits = bits + "," }
                            bind bits = bits + to_str(list_get(เก็บแล้ว, biq))
                            bind biq = biq + 1
                        }
                        เน็ตส่ง(to_str(cx)+";"+to_str(cy)+";"+to_str(cz)+";"+to_str(ด่าน)+";"+to_str(คะแนน)+";"+bits+";"+cf+";"+to_str(ตีโดน))
                        bind rmsg = เน็ตรับ()
                        ถ้า rmsg != "" {
                            bind rp = แยก(rmsg, ";")
                            ถ้า ความยาว(rp) >= 3 {
                                bind cx2 = cx2 + (list_get(rp,0)*1.0 - cx2)*0.4
                                bind cy2 = cy2 + (list_get(rp,1)*1.0 - cy2)*0.4
                                bind cz2 = cz2 + (list_get(rp,2)*1.0 - cz2)*0.4
                            }
                            ถ้า ความยาว(rp) >= 5 {
                                bind pc = list_get(rp, 3)
                                ถ้า pc != "_" { ถ้า pc != chat2Last { bind chat2Msg = pc;  bind chat2T = 5.0;  bind chat2Last = pc;  bind chatLog = list_push(chatLog, pc) } }
                                ถ้า vsโหมด == 1 { bind โดนตี = list_get(rp,4)*1.0;  bind ชีวิต = ชีวิตสูงสุด - โดนตี }
                            }
                        }
                    }
                    มิฉะนั้น {
                        เน็ตส่ง(to_str(cx)+";"+to_str(cy)+";"+to_str(cz)+";"+cf+";"+to_str(ตีโดน))
                        bind hmsg = เน็ตรับ()
                        ถ้า hmsg != "" {
                            bind hp = แยก(hmsg, ";")
                            ถ้า ความยาว(hp) >= 6 {
                                bind cx2 = cx2 + (list_get(hp,0)*1.0 - cx2)*0.4
                                bind cy2 = cy2 + (list_get(hp,1)*1.0 - cy2)*0.4
                                bind cz2 = cz2 + (list_get(hp,2)*1.0 - cz2)*0.4
                                bind hlv = list_get(hp,3)*1.0
                                ถ้า hlv != ด่าน {
                                    bind ด่าน = hlv;  bind startX = 0.0;  bind startY = 0.0-2.0;  bind startZ = 180.0
                                    ถ้า hlv < 0.5 { bind startZ = 0.0 }
                                    bind รีเซ็ต = 1
                                }
                                bind คะแนน = floor(list_get(hp,4)*1.0)
                                bind bl = แยก(list_get(hp,5), ",")
                                bind bj = 0
                                ขณะที่ bj < ความยาว(bl) {
                                    ถ้า bj < เก็บจำนวน { bind เก็บแล้ว = ตั้งรายการ(เก็บแล้ว, bj, floor(list_get(bl,bj)*1.0)) }
                                    bind bj = bj + 1
                                }
                            }
                            ถ้า ความยาว(hp) >= 8 {
                                bind pc = list_get(hp, 6)
                                ถ้า pc != "_" { ถ้า pc != chat2Last { bind chat2Msg = pc;  bind chat2T = 5.0;  bind chat2Last = pc;  bind chatLog = list_push(chatLog, pc) } }
                                ถ้า vsโหมด == 1 { bind โดนตี = list_get(hp,7)*1.0;  bind ชีวิต = ชีวิตสูงสุด - โดนตี }
                            }
                        }
                    }
                }
                bind squash = squash * 0.85
            }

            bind speed = รากที่สอง(vx*vx + vy*vy + vz*vz)

            # ── ออดิโอ-รีแอกทีฟ: อิ่มสีตาม FFT เพลง + หมุนตามไมค์ (เทรล+ริบบิ้น) ──
            ถ้า micOpened < 0.5 { เปิดไมค์();  bind micOpened = 1.0 }
            bind fxBands = สเปกตรัมเพลง(เพลงเล่น, fftCalls)
            bind fxEnergy = list_get(fxBands,1) + list_get(fxBands,2) + list_get(fxBands,3)
            bind fxSat = 0.7 + fxEnergy*0.55;  ถ้า fxSat > 1.6 { bind fxSat = 1.6 }
            bind fxSpin = fxSpin + เสียงRMS()*0.6                      # สะสมมุมหมุนจากไมค์

            bind royg = สีรุ้งFFT(fxBands)                  # สี ROYGBIV จาก FFT เพลง → "คลื่นเสียง"
            # ── เทรลแหวน: บันทึกตำแหน่ง+ความเร็วผู้เล่นทุกเฟรม (ทุกโหมด) ──
            ถ้า trailInit < 0.5 { bind trailBuf = รายการศูนย์(floor(trailN)*6);  bind foeTrailBuf = รายการศูนย์(floor(trailN)*6);  bind trailInit = 1.0 }
            ถ้า เอฟเฟกต์เทรล > 0.5 {                         # เขียนเทรลเฉพาะเมื่อเปิด (กันงาน list หนักตอนปิด)
                bind trailBuf = เขียนเทรล(trailBuf, trailHead, cx, cy, cz, vx, vy, vz)
                bind trailHead = (trailHead + 1.0) % trailN
                ถ้า อาร์เคดโหมด > 0.5 {                       # รอยศัตรู (อาร์เคดเท่านั้น)
                    bind foeTrailBuf = เขียนเทรล(foeTrailBuf, foeTrailHead, foeX, foeY + 0.5, foeZ, foeVX, 0.0, foeVZ)
                    bind foeTrailHead = (foeTrailHead + 1.0) % trailN
                }
            }

            # (ไม่จำกัดความเร็ว — momentum เต็มที่; การหน่วงมาจาก soft-body damping 0.98)

            # ── เก็บไอเทมดาบ (อยู่ที่ (0, 40) ในฮับ) → ปลดล็อกการฟัน ──
            # ── ไอเทมสกิลประจำเลเวล (ด่าน0=ดาบที่ฮับ; proc=บนชั้นยกกลาง) ──
            ถ้า doUpdate == 1 { ถ้า สกิล < เป้าสกิล {
                bind itx = 0.0;  bind itz = 40.0
                ถ้า ด่าน > 0.5 { bind itz = 0.0 }
                ถ้า (cx-itx)*(cx-itx) + (cz-itz)*(cz-itz) < 64.0 {
                    bind สกิล = เป้าสกิล
                    ถ้า สกิล >= 1.0 { bind ดาบ = 1.0 }
                    เสียงเอฟเฟกต์(0,0,0, 1.0, 880.0, 0.5, 0.7, "sine")
                    พิมพ์ฟ้า("  ⚔ ได้สกิลระดับ " + to_str(สกิล) + "! (เซฟด้วย K)")
                }
            } }

            # ── ดาบชนแบบคอลลิชันจริง: สร้างเส้นคมดาบ (segment) ต่อเฟรม ──
            bind swordSegOn = 0.0
            bind sx0 = 0.0;  bind sz0 = 0.0;  bind sx1 = 0.0;  bind sz1 = 0.0
            bind sdx = 0.0;  bind sdz = 0.0;  bind sl2 = 1.0;  bind segRad = 1.9
            ถ้า slashT > 0.0 {
                bind sstSeg = list_get(ดาบสเตตัส, floor(swordType))
                bind segMul = list_get(swordSegScale, floor(swordType))
                bind segLen = list_get(sstSeg,2) * 0.85 * segMul;  ถ้า segLen > 20.0 { bind segLen = 20.0 }   # ยาวขึ้น (0.55->0.85, cap 12->20)
                bind segRad = 2.30 + segMul * 0.70                                                            # กว้างขึ้น (กวาดโดนง่ายขึ้น)
                bind segReach = R*0.85 + segLen
                bind sx0 = cx + ไซน์(arcYaw) * (R*0.85);  bind sz0 = cz + โคไซน์(arcYaw) * (R*0.85)
                bind sx1 = cx + ไซน์(arcYaw) * segReach;   bind sz1 = cz + โคไซน์(arcYaw) * segReach
                bind sdx = sx1 - sx0;  bind sdz = sz1 - sz0;  bind sl2 = sdx*sdx + sdz*sdz
                ถ้า sl2 < 0.0001 { bind sl2 = 0.0001 }
                bind swordSegOn = 1.0
            }

            # ── ต่อสู้มอนสเตอร์: ฟันโดน = ตาย+แต้ม; แตะตัว = กระเด็น ──
            ถ้า doUpdate == 1 { ถ้า มอนจำนวน > 0.5 {
                bind mj = 0
                ขณะที่ mj < มอนจำนวน {
                    ถ้า list_get(มอนเป็น, mj) > 0 {
                        bind mmx = list_get(มอนX, mj);  bind mmz = list_get(มอนZ, mj)
                        bind md = รากที่สอง((cx-mmx)*(cx-mmx) + (cz-mmz)*(cz-mmz))
                        bind swordHitMon = 0.0
                        ถ้า swordSegOn > 0.5 {
                            bind su = ((mmx-sx0)*sdx + (mmz-sz0)*sdz) / sl2
                            ถ้า su < 0.0 { bind su = 0.0 }
                            ถ้า su > 1.0 { bind su = 1.0 }
                            bind sqx = sx0 + sdx*su;  bind sqz = sz0 + sdz*su
                            bind srr = R + segRad
                            ถ้า (mmx-sqx)*(mmx-sqx) + (mmz-sqz)*(mmz-sqz) < srr*srr { bind swordHitMon = 1.0 }
                        }
                        ถ้า swordHitMon > 0.5 {
                            bind dmg = 2.0 + list_get(ผู้เล่นสเตตัส, 7) * 0.4   # PWR
                            bind มอนHP = ตั้งรายการ(มอนHP, mj, list_get(มอนHP, mj) - dmg)  bind ฮีทต่อสู้ = 1.0
                            # ดาบ: แฟลชสั้นคม + เซถอยเบา ๆ ตามแนวฟัน (ระเบิดเซแรงกว่านี้มาก)
                            bind มอนHitT = ตั้งรายการ(มอนHitT, mj, 0.35)
                            bind มอนโกรธ = ตั้งรายการ(มอนโกรธ, mj, 9.0)          # โดนฟัน = ล็อกเป้าไล่ 9 วิ
                            ถ้า md > 0.1 {
                                bind kbx = (mmx-cx)/md * 3.2;  bind kbz = (mmz-cz)/md * 3.2
                                bind มอนX = ตั้งรายการ(มอนX, mj, mmx + kbx)
                                bind มอนZ = ตั้งรายการ(มอนZ, mj, mmz + kbz)
                            }
                            เสียงมอนเจ็บ(list_get(สปีชีส์มอน(mj, มอนseed), 0), mmx, cy, mmz, ปริมาณSFX, 0.45)
                            ถ้า list_get(มอนHP, mj) <= 0.0 { ถ้า list_get(มอนDieT, mj) <= 0.0 {
                                bind มอนDieT = ตั้งรายการ(มอนDieT, mj, 1.0)
                                bind คะแนน = คะแนน + 5
                                bind st = สเตตัสมอน(mj, ด่าน)
                                bind sti = mj - ปัดลง(mj/8.0)*8.0
                                bind ดรอปเป็น = ตั้งรายการ(ดรอปเป็น, mj, 1)
                                bind ดรอปX = ตั้งรายการ(ดรอปX, mj, mmx)
                                bind ดรอปZ = ตั้งรายการ(ดรอปZ, mj, mmz)
                                bind ดรอปสเตตัส = ตั้งรายการ(ดรอปสเตตัส, mj, sti)
                                bind ดรอปค่า = ตั้งรายการ(ดรอปค่า, mj, list_get(st, sti))
                                ถ้า สุ่ม() < 0.12 { bind ดรอปสเตตัส = ตั้งรายการ(ดรอปสเตตัส, mj, 8);  bind ดรอปค่า = ตั้งรายการ(ดรอปค่า, mj, 1) }   # 12% → Phoenix Down
                                เสียงมอนตาย(list_get(สปีชีส์มอน(mj, มอนseed), 0), mmx, cy, mmz, ปริมาณSFX)
                                พิมพ์แดง("  ☠ มอนตาย! ดรอปบัฟ +" + to_str(list_get(st, sti)))
                            } }
                        }
                        มิฉะนั้น { ถ้า md < R + 6.0 { ถ้า md > 0.1 { ถ้า list_get(มอนDieT, mj) <= 0.0 {   # touch-to-beat: แตะ = ป๊อปคริสตัล + ดรอปบัฟ + กัดเล็ก ๆ
                            bind มอนDieT = ตั้งรายการ(มอนDieT, mj, 1.0);  bind คะแนน = คะแนน + 5
                            bind st2 = สเตตัสมอน(mj, ด่าน);  bind sti2 = mj - ปัดลง(mj/8.0)*8.0
                            bind ดรอปเป็น = ตั้งรายการ(ดรอปเป็น, mj, 1);  bind ดรอปX = ตั้งรายการ(ดรอปX, mj, mmx);  bind ดรอปZ = ตั้งรายการ(ดรอปZ, mj, mmz)
                            bind ดรอปสเตตัส = ตั้งรายการ(ดรอปสเตตัส, mj, sti2);  bind ดรอปค่า = ตั้งรายการ(ดรอปค่า, mj, list_get(st2, sti2))
                            bind vx = vx + (cx-mmx)/md * 0.9;  bind vz = vz + (cz-mmz)/md * 0.9   # กระเด็นออก
                            เสียงเอฟเฟกต์(mmx, cy, mmz, 1.0, 300.0, 0.35*ปริมาณSFX, 0.22, "saw")   # ป๊อปคริสตัล
                            โน้ตมอร์ฟ(mmx, cy, mmz, 1.0, 165.0, 0.45*ปริมาณSFX, 0.4, 3.0, 1.0)     # คริสตัลแตก = โลหะถูกบีบอัด (corruption core, full dark)
                            ถ้า บาดเจ็บ <= 0.0 { ถ้า freezeT <= 0.0 {                              # กัดคาร์นิวอร์ (PWR สูงขึ้น, การ์ดลด)
                                bind มดลด = 0.55 + ด่าน*0.11;  ถ้า มดลด > 1.0 { bind มดลด = 1.0 }   # ด่านต้นเจ็บน้อยลง
                                bind ชีวิต = ชีวิต - (2.0 + list_get(st2, 7) * 0.6) * (1.0 - guarding*0.85) * มดลด
                                bind บาดเจ็บ = 0.45;  bind popHurt = (2.0 + list_get(st2, 7) * 0.6);  bind popHurtT = 0.75
                                # เตะกลับ: แรงตามพลังของตัวที่กัด · ตั้งการ์ดกินดาเมจได้ 85%
                                # แต่แรงกระแทกยังมา (ยิ่งกว่าเดิม) — กันได้ ไม่ได้แปลว่ายืนนิ่ง
                                bind kbp = 1.4 + list_get(st2, 7) * 0.16
                                ถ้า guarding > 0.5 { bind kbp = kbp * 1.35 }
                                bind vx = vx + (cx-mmx)/md * kbp
                                bind vz = vz + (cz-mmz)/md * kbp
                            } }
                            พิมพ์แดง("  ☠ ป๊อป! ดรอปบัฟ +" + to_str(list_get(st2, sti2)))
                        } } } }
                    }
                    bind mj = mj + 1
                }
            } }

            # ── อาร์เคด: AI ศัตรู (ไล่/วน/พุ่งตี/การ์ด) + การปะทะ ลูก-vs-ลูก ──
            ถ้า doUpdate == 1 { ถ้า อาร์เคดโหมด > 0.5 {
                bind foeY = ความสูงอารีนา(foeX, foeZ) - R
                bind fdx = cx - foeX;  bind fdz = cz - foeZ
                bind fdist = รากที่สอง(fdx*fdx + fdz*fdz) + 0.001
                bind fnx = fdx/fdist;  bind fnz = fdz/fdist
                # ── ปะดาบ: ทั้งคู่ฟันพร้อมกัน + ใกล้ → เด้งคู่แรง + ยกเลิกฟัน + ประกายปะดาบ ──
                ถ้า slashT > 0.0 { ถ้า foeSlashT > 0.0 { ถ้า fdist < R*3.4 { ถ้า impactT <= 0.0 {
                    bind impactT = 0.40;  bind impactX = (cx+foeX)*0.5;  bind impactZ = (cz+foeZ)*0.5;  bind clashFX = 1.0
                    bind vx = vx + fnx*1.5;  bind vz = vz + fnz*1.5                  # เด้งคู่: ผู้เล่น
                    bind foeVX = foeVX - fnx*1.5;  bind foeVZ = foeVZ - fnz*1.5      # เด้งคู่: ศัตรู
                    bind slashHit = 1.0;  bind foeSlashT = 0.0                        # ปะกัน → ยกเลิกการฟันทั้งคู่
                    เสียงเอฟเฟกต์((cx+foeX)*0.5, cy, (cz+foeZ)*0.5, 1.0, 850.0, 0.6*ปริมาณSFX, 0.18, "square");  จอยสั่น(0.6, 0.85, 130)
                } } } }
                # ── ปะทะลูก-vs-ลูก: อิมแพ็ค + เด้งแยก (ไม่เสียเลือดจากการชน) ──
                ถ้า fdist < R*2.3 {
                    ถ้า impactT <= 0.0 { เสียงเอฟเฟกต์((cx+foeX)*0.5, cy, (cz+foeZ)*0.5, 1.0, 170.0, 0.45*ปริมาณSFX, 0.14, "square") }
                    bind impactT = 0.30;  bind impactX = (cx+foeX)*0.5;  bind impactZ = (cz+foeZ)*0.5
                    bind vx = vx + fnx*0.6;  bind vz = vz + fnz*0.6
                    bind foeVX = foeVX - fnx*0.6;  bind foeVZ = foeVZ - fnz*0.6
                }
                ถ้า impactT > 0.0 { bind impactT = impactT - 0.016 }
                ถ้า critT > 0.0 { bind critT = critT - 0.016 }
                # ── เคลื่อนที่: คล่อง(agility) คุมความเร็ว; ไล่ล่า + รักษาระยะ ──
                # ── สถานะ: หลับ (หยุดนิ่ง) / ช้า (เวลาช้าเฉพาะศัตรู, ผู้เล่นปกติ) ──
                bind slowK = 1.0;  ถ้า foeSlow > 0.0 { bind foeSlow = foeSlow - 0.016;  bind slowK = 0.4 }
                bind foeAct = 1.0;  ถ้า foeSleep > 0.0 { bind foeSleep = foeSleep - 0.016;  bind slowK = 0.0;  bind foeAct = 0.0 }
                bind fspeed = (0.13 + foeAgi*0.30) * slowK
                ถ้า fdist > 13.0 { bind foeVX = foeVX + fnx*fspeed;  bind foeVZ = foeVZ + fnz*fspeed }
                ถ้า fdist < 9.0 { bind foeVX = foeVX - fnx*fspeed*0.7;  bind foeVZ = foeVZ - fnz*fspeed*0.7 }
                bind foeVX = foeVX + (0.0-fnz)*fspeed*0.5*ไซน์(t*1.3 + foeX*0.2)
                bind foeVZ = foeVZ + fnx*fspeed*0.5*ไซน์(t*1.3 + foeX*0.2)
                # ── หลบ(evade): ผู้เล่นฟัน+ใกล้ → แดชข้าง (โอกาส ∝ สมาธิ, แรง ∝ คล่อง) ──
                bind plAtk = 0.0;  ถ้า slashT > 0.05 { bind plAtk = 1.0 }
                ถ้า plAtk > 0.5 { ถ้า fdist < 19.0 { ถ้า สุ่ม() < foeAtt*0.5 {
                    bind dside = 1.0;  ถ้า ไซน์(t*5.3 + foeZ*0.7) < 0.0 { bind dside = 0.0 - 1.0 }
                    bind dmag = 2.4 + foeAgi*4.2
                    bind foeVX = foeVX + (0.0-fnz)*dmag*dside;  bind foeVZ = foeVZ + fnx*dmag*dside
                    เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, 340.0, 0.26*ปริมาณSFX, 0.1, "noise")
                } } }
                bind foeVX = foeVX * 0.85;  bind foeVZ = foeVZ * 0.85
                bind foeX = foeX + foeVX;  bind foeZ = foeZ + foeVZ
                bind fbnd = หากล่องอารีนา(foeX, foeZ, R)
                ถ้า list_get(fbnd,0) > 0.0 { bind foeX = list_get(fbnd,1);  bind foeZ = list_get(fbnd,2);  bind foeVX = 0.0;  bind foeVZ = 0.0 }
                # ── การ์ดโต้ตอบ(attention): ผู้เล่นฟัน+ในระยะ → กัน (โอกาส ∝ สมาธิ) ──
                bind foeGuard = 0.0
                ถ้า plAtk > 0.5 { bind sstG = list_get(ดาบสเตตัส, swordType);  ถ้า fdist < list_get(sstG,2) + 3.0 { ถ้า สุ่ม() < foeAtt*0.85 { bind foeGuard = 1.0 } } }
                ถ้า foeHP < foeHPMax*0.3 { ถ้า สุ่ม() < foeAtt*0.2 { bind foeGuard = 1.0 } }
                # ── โจมตีดาบ(technique): คูลดาวน์/คอมโบ ∝ เทคนิค ──
                bind foeCoolT = foeCoolT - 0.016*slowK
                bind foeSlashT = foeSlashT - 0.016
                bind foeLunge = foeLunge - 0.016
                ถ้า foeAct > 0.5 { ถ้า foeCoolT <= 0.0 { ถ้า fdist < 15.0 { ถ้า foeSlashT <= 0.0 {
                    bind foeSlashT = 0.32;  bind foeGuard = 0.0
                    bind foeCombo = foeCombo + 1.0
                    ถ้า foeCombo > (1.0 + foeTech*3.0) { bind foeCombo = 0.0;  bind foeCoolT = foeCool * (1.25 - foeTech*0.55) }
                    มิฉะนั้น { bind foeCoolT = 0.16 }
                    bind foeVX = foeVX + fnx*(1.3 + foeAgi*1.7);  bind foeVZ = foeVZ + fnz*(1.3 + foeAgi*1.7)
                    bind fni = slashNote - ปัดลง(slashNote/sfxจำนวน)*sfxจำนวน;  bind ffreq = list_get(sfxความถี่, fni);  bind slashNote = slashNote + 1.0
                    เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, ffreq*1.6, 0.36*ปริมาณSFX, 0.16, "saw")
                    เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, ffreq*3.2, 0.2*ปริมาณSFX, 0.11, "noise")
                } } } }
                # ── ฟันโดนผู้เล่น: ดาเมจ + เสียง guard/hit ที่จุดปะทะกลางดาบ ──
                bind foeSwordHit = 0.0
                ถ้า foeSlashT > 0.16 {
                    bind fs0x = foeX + fnx*(R*0.8);  bind fs0z = foeZ + fnz*(R*0.8)
                    bind fs1x = foeX + fnx*(R*0.8 + 8.4);  bind fs1z = foeZ + fnz*(R*0.8 + 8.4)
                    bind fdxs = fs1x - fs0x;  bind fdzs = fs1z - fs0z;  bind fl2s = fdxs*fdxs + fdzs*fdzs
                    ถ้า fl2s < 0.0001 { bind fl2s = 0.0001 }
                    bind fu = ((cx-fs0x)*fdxs + (cz-fs0z)*fdzs) / fl2s
                    ถ้า fu < 0.0 { bind fu = 0.0 }
                    ถ้า fu > 1.0 { bind fu = 1.0 }
                    bind fqx = fs0x + fdxs*fu;  bind fqz = fs0z + fdzs*fu
                    bind frr = R + 2.2
                    ถ้า (cx-fqx)*(cx-fqx) + (cz-fqz)*(cz-fqz) < frr*frr { bind foeSwordHit = 1.0 }
                }
                ถ้า foeSwordHit > 0.5 { ถ้า บาดเจ็บ <= 0.0 {
                    bind hmx = (cx+foeX)*0.5;  bind hmy = (cy+foeY)*0.5;  bind hmz = (cz+foeZ)*0.5
                    bind dmgPl = foeAtk * (1.0 - guarding*0.85)
                    bind ชีวิต = ชีวิต - dmgPl
                    bind บาดเจ็บ = 0.5;  bind foeSlashT = 0.0
                    ถ้า guarding > 0.5 { จอยสั่น(0.4, 0.6, 90) }  มิฉะนั้น { จอยสั่น(0.85, 0.85, 220) }   # จอย: กันโดน / โดนเต็ม
                    bind popHurt = dmgPl;  bind popHurtT = 0.75
                    bind forceF = 0.7 + foeAtk*0.22 + foeAgi*1.2;  ถ้า guarding > 0.5 { bind forceF = forceF*0.4 }   # เด้ง: แรง ∝ พลัง+คล่อง (โค้ง)
                    ถ้า สุ่ม() < forceF*0.5 { bind kbF = forceF*(0.5 + forceF*0.22);  bind vx = vx + fnx*kbF;  bind vz = vz + fnz*kbF }
                    ถ้า guarding > 0.5 { เสียงเอฟเฟกต์(hmx, hmy, hmz, 1.0, 205.0, 0.6*ปริมาณSFX, 0.16, "square");  เสียงเอฟเฟกต์(hmx, hmy, hmz, 1.0, 90.0, 0.3*ปริมาณSFX, 0.13, "noise") }
                    มิฉะนั้น { เสียงเอฟเฟกต์(hmx, hmy, hmz, 1.0, 115.0, 0.62*ปริมาณSFX, 0.22, "saw");  เสียงเอฟเฟกต์(hmx, hmy, hmz, 1.0, 250.0, 0.34*ปริมาณSFX, 0.14, "noise");  bind callT = 1.6;  bind callMsg = ดึงคำตี() }
                } }
                ถ้า swordSegOn > 0.5 { ถ้า slashHit < 0.5 {
                    bind sstF = list_get(ดาบสเตตัส, swordType)
                    bind su2 = ((foeX-sx0)*sdx + (foeZ-sz0)*sdz) / sl2
                    ถ้า su2 < 0.0 { bind su2 = 0.0 }
                    ถ้า su2 > 1.0 { bind su2 = 1.0 }
                    bind sqx2 = sx0 + sdx*su2;  bind sqz2 = sz0 + sdz*su2
                    bind hbr2 = R + segRad + 0.2
                    ถ้า (foeX-sqx2)*(foeX-sqx2) + (foeZ-sqz2)*(foeZ-sqz2) < hbr2*hbr2 { ถ้า R > 0.0 {
                    bind slashHit = 1.0
                    bind dmgFoe = (1.6 + list_get(ผู้เล่นสเตตัส,7)*0.25) * list_get(sstF,1) * (1.0 - foeRes) - foeDef*0.15
                    ถ้า dmgFoe < 0.3 { bind dmgFoe = 0.3 }
                    # คริติคอล: โอกาส ∝ โชค (LCK สเตตัส 5) → ดาเมจ ×2.2
                    bind crit = 0.0;  ถ้า สุ่ม() < 0.05 + list_get(ผู้เล่นสเตตัส,5)*0.02 { bind crit = 1.0;  bind dmgFoe = dmgFoe * 2.2;  bind critT = 0.9;  bind critX = foeX;  bind critZ = foeZ;  เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, 880.0, 0.5*ปริมาณSFX, 0.18, "square");  เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, 1320.0, 0.4*ปริมาณSFX, 0.22, "sine") }
                    bind hitG = 0.0;  ถ้า foeGuard > 0.5 { bind dmgFoe = dmgFoe * 0.2;  bind hitG = 1.0 }
                    bind foeHP = foeHP - dmgFoe;  bind foeHurt = 0.3
                    ถ้า hitG > 0.5 { จอยสั่น(0.5, 0.7, 90) }  มิฉะนั้น { จอยสั่น(0.3, 0.55, 100) }   # จอย: โดนการ์ด / ตีโดน
                    bind popDmg = dmgFoe;  bind popDmgT = 0.75;  bind popDmgCrit = crit
                    # เด้งถอย: โอกาส+แรง ∝ แรงปะทะ (โค้ง — แรงมาก เด้งแน่+ไกลขึ้นเร็ว)
                    bind forceP = 1.0 + list_get(sstF,1)*0.9;  ถ้า crit > 0.5 { bind forceP = forceP*1.6 };  ถ้า hitG > 0.5 { bind forceP = forceP*0.4 }
                    ถ้า สุ่ม() < forceP*0.4 { bind kbM = forceP*(0.5 + forceP*0.22);  bind foeVX = foeVX - fnx*kbM;  bind foeVZ = foeVZ - fnz*kbM }
                    # ── ดาบแวมไพร์ (Nova 5): ดูดพลังจากสิ่งที่ฟัน → ฟื้นผู้ถือ ──
                    ถ้า swordType == 5.0 { bind hl = floor(dmgFoe*0.4);  ถ้า hl > 0.0 { bind ชีวิต = ชีวิต + hl;  ถ้า ชีวิต > ชีวิตสูงสุด { bind ชีวิต = ชีวิตสูงสุด }  bind popHeal = hl;  bind popHealT = 0.75;  เสียงเอฟเฟกต์(cx, cy, cz, 1.0, 520.0, 0.4*ปริมาณSFX, 0.2, "sine") } }
                    bind hfi = slashNote - ปัดลง(slashNote/sfxจำนวน)*sfxจำนวน;  bind hfr = list_get(sfxความถี่, hfi)
                    ถ้า hitG > 0.5 { เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, 210.0, 0.55*ปริมาณSFX, 0.16, "square");  เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, 95.0, 0.3*ปริมาณSFX, 0.13, "noise") }
                    มิฉะนั้น { เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, hfr*2.0, 0.6*ปริมาณSFX, 0.15, "sine");  เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, 150.0, 0.34*ปริมาณSFX, 0.16, "noise") }
                    ถ้า crit > 0.5 { เสียงเอฟเฟกต์(foeX, foeY, foeZ, 1.0, 1250.0, 0.42*ปริมาณSFX, 0.12, "square") }
                } } } }
                ถ้า foeHurt > 0.0 { bind foeHurt = foeHurt - 0.016 }
                ถ้า popDmgT > 0.0 { bind popDmgT = popDmgT - 0.016 }
                ถ้า popHurtT > 0.0 { bind popHurtT = popHurtT - 0.016 }
                ถ้า popHealT > 0.0 { bind popHealT = popHealT - 0.016 }
                ถ้า foeHP < foeHPMax*0.28 { ถ้า callT <= 0.0 { bind callT = 1.8;  bind callMsg = ดึงคำเพล() } }
                bind tauntT = tauntT - 0.016
                ถ้า tauntT <= 0.0 { bind tauntT = 7.0 + สุ่ม()*5.0;  ถ้า callT <= 0.0 { bind callT = 1.6;  bind callMsg = ดึงคำใบ้() } }
                ถ้า callT > 0.0 { bind callT = callT - 0.016 }
                # ── ดาบพิคอัพ: 6 จุดวงแหวน; เก็บ → ยืมดาบ (พลังพิเศษ) 18 วิ, รีสปอว์น 14 วิ ──
                bind sw_i = 0
                ขณะที่ sw_i < 6 {
                    ถ้า list_get(pickCD, sw_i) <= 0.0 {
                        bind swa = sw_i * 1.0472
                        bind swx = โคไซน์(swa)*22.0;  bind swz = ไซน์(swa)*22.0
                        ถ้า (cx-swx)*(cx-swx) + (cz-swz)*(cz-swz) < 30.0 {
                            bind swordType = sw_i;  bind weaponT = 18.0
                            bind สกิล = 1.0;  bind เลือกไอเทม = 1.0
                            bind pickCD = ตั้งรายการ(pickCD, sw_i, 14.0)
                            เสียงเอฟเฟกต์(swx, cy, swz, 1.0, 660.0, 0.55*ปริมาณSFX, 0.3, "sine")
                        }
                    }
                    มิฉะนั้น { bind pickCD = ตั้งรายการ(pickCD, sw_i, list_get(pickCD, sw_i) - 0.016) }
                    bind sw_i = sw_i + 1
                }
                # ── บัฟพาวเวอร์อัพ 3 จุด (วงใน r=12): 0,1 = ฮีล +25; 2 = LCK +1 (คริต) ──
                bind bf_i = 0
                ขณะที่ bf_i < 3 {
                    ถ้า list_get(buffCD, bf_i) <= 0.0 {
                        bind bfa = bf_i * 2.0944;  bind bfx = โคไซน์(bfa)*12.0;  bind bfz = ไซน์(bfa)*12.0
                        ถ้า (cx-bfx)*(cx-bfx) + (cz-bfz)*(cz-bfz) < 26.0 {
                            ถ้า bf_i < 2.0 {
                                bind ชีวิต = ชีวิต + 25.0;  ถ้า ชีวิต > ชีวิตสูงสุด { bind ชีวิต = ชีวิตสูงสุด }
                                bind popHeal = 25.0;  bind popHealT = 0.75;  เสียงเอฟเฟกต์(bfx, cy, bfz, 1.0, 720.0, 0.5*ปริมาณSFX, 0.3, "sine")
                            }
                            มิฉะนั้น { bind ผู้เล่นสเตตัส = ตั้งรายการ(ผู้เล่นสเตตัส, 5, list_get(ผู้เล่นสเตตัส,5) + 1.0);  เสียงเอฟเฟกต์(bfx, cy, bfz, 1.0, 980.0, 0.5*ปริมาณSFX, 0.3, "sine");  พิมพ์เหลือง("  ✦ LCK +1 (คริต)") }
                            bind buffCD = ตั้งรายการ(buffCD, bf_i, 16.0)
                        }
                    }
                    มิฉะนั้น { bind buffCD = ตั้งรายการ(buffCD, bf_i, list_get(buffCD, bf_i) - 0.016) }
                    bind bf_i = bf_i + 1
                }
                # ── หมดเวลาดาบ → กลับไปดาบพื้นฐาน ──
                ถ้า weaponT > 0.0 {
                    bind weaponT = weaponT - 0.016
                    ถ้า weaponT <= 0.0 { bind swordType = 0.0;  bind สกิล = 1.0;  bind เลือกไอเทม = 1.0 }
                }
                ถ้า foeHP <= 0.0 {
                    bind อาร์เคดแต้ม = อาร์เคดแต้ม + floor(รอบ*100.0 + ชีวิต*2.0)    # แต้ม: ตามรอบ + เลือดเหลือ
                    bind รอบ = รอบ + 1.0
                    bind matchBT = 1.7;  bind matchBWin = 1.0           # แบนเนอร์ Match Win!
                    bind fanfPlay = 1.0;  bind fanfStartT = t;  bind fanfFired = 0.0;  bind fanfWin = 1.0   # แฟนแฟร์ชนะ
                    ถ้า รอบ > 10.0 {                      # ล้ม 9 ศัตรู + บอส → คลีร์อาร์เคด
                        bind อาร์เคดชนะ = 1.0;  หยุดเพลง();  bind สถานะ = ST_ARCRESULT
                    }
                    มิฉะนั้น {
                        ถ้า รอบ >= 10.0 { พิมพ์ขาว("  ☠ FINAL BOSS approaches...") }  มิฉะนั้น { พิมพ์เขียว("  ★ Foe " + to_str(floor(รอบ)) + " of 9!") }
                        bind รีเซ็ต = 1
                    }
                }
            } }

            # ── ดรอปบัฟ: เก็บ → เพิ่มสเตตัสผู้เล่น (Stat +XX) + invuln นับถอย ──
            ถ้า doUpdate == 1 {
                ถ้า บาดเจ็บ > 0.0 { bind บาดเจ็บ = บาดเจ็บ - 0.016 }
                ถ้า ชีวิต <= 0.0 {
                    ถ้า vsโหมด == 1 { พิมพ์แดง("  ☠ KO! แพ้แล้ว — กลับเมนู");  bind บอสโหมด = 0;  bind สถานะ = ST_MENU;  bind sel = 0;  bind gameMode = 0;  bind vsโหมด = 0 }
                    มิฉะนั้น { ถ้า อาร์เคดโหมด > 0.5 {
                        bind อาร์เคดไลฟ์ = อาร์เคดไลฟ์ - 1.0
                        ถ้า อาร์เคดไลฟ์ <= 0.0 { bind อาร์�

← Back to index