Bab 11: Sejarah Pembangunan Aplikasi — Bagian 3 (Terbaru) & Status Saat Ini

Bagian ini adalah lanjutan sekaligus penutup dari catatan sejarah pembangunan aplikasi game mancing "CtSPoC". Cakupannya adalah ADR-045 sampai ADR-076, ditambah beberapa perubahan kecil di hari yang sama (amandemen) dan beberapa catatan serah-terima (handoff) yang tidak diberi nomor ADR. Periode ini adalah fase paling baru dalam proyek, dan yang membuatnya istimewa: di sinilah untuk pertama kalinya tim mendapat umpan balik nyata dari screenshot aplikasi yang benar-benar berjalan di perangkat — sebelumnya semua perubahan hanya diverifikasi lewat "typecheck" (cek tipe kode), tidak pernah benar-benar dicoba main. Urutan di bawah ini disusun kronologis, sama seperti sumber aslinya.

1. Cutscene "Tertangkap" Sebelum Layar Hasil (ADR-045)

Tanggal: 2026-07-20. Pemain minta ada adegan sinematik (cutscene) seperti game lain — ketika ikan/pelampung (bobber) sudah dekat dermaga dan input reel terakhir masuk, tampilkan adegan "melompat" dengan judul "CATCH", yang nantinya akan dikaitkan dengan animasi Blender. Ini lanjutan dari pekerjaan menarik kail ke dermaga (ADR-044). Keputusan desain: cutscene ini dibuat sebagai status tampilan murni di `WorldViewModel`, BUKAN kasus baru di `HookPhase` — karena `HookPhase` adalah kontrak bersama yang juga dipakai oleh Rod (untuk haptic/izin aksi), dan arsitektur proyek ini sengaja memisahkan logika tampilan gameplay dari Rod. `HookPhase.result` yang sudah ada cukup untuk mengunci input (`allowsCasting`/`allowsReeling` sama-sama false). Perubahan: `WorldViewModel` dapat properti baru `isShowingCatchCutscene`, struct `CatchCutscene{origin, elapsed}`, durasi cutscene `catchCutsceneDuration = 1.3 detik`, tinggi lompatan `catchCutsceneJumpHeight = 0.6 meter`. Fungsi `reelHook(deltaTime:)` sekarang memanggil `fishFight.resolveCatch()` lalu `beginCatchCutscene(from: hookEntity.position)` dan `emit(.catchFish)`, bukan langsung menonaktifkan kail. Fungsi baru `updateCatchCutscene(deltaTime:)` membuat lompatan sementara (placeholder): kail melengkung naik-turun pakai rumus `sin(t*.pi) * jumpHeight`, lalu kail dinonaktifkan setelah durasi habis — ini eksplisit ditandai sebagai penempatan sementara karena belum ada mesh ikan terpisah. Dibuat juga file baru `CatchCutsceneView.swift` — overlay "CATCH!" yang tidak bisa ditutup paksa, dengan animasi scale/opacity pegas. `ContentView` menambahkan overlay ini, dan `ResultView` diatur supaya tidak muncul bersamaan. Fungsi `returnToMenu()` me-reset flag cutscene baru ini supaya tidak nyangkut "true" terus di sesi berikutnya. Hasil: kode lolos typecheck, tapi belum pernah dicoba langsung — memang ditandai sebagai versi awal, menunggu aset ikan/animasi asli yang baru datang jauh setelahnya (ADR-068 ke atas). (tag: Mekanik Cast/Fight, UI)

2. Potongan Kamera saat Cutscene Tertangkap (ADR-046)

Tanggal: 2026-07-20. Langsung setelah ADR-045, permintaan berikutnya: "kamera harus langsung memotong ke arah bobber/ikan supaya terasa seperti cutscene." Akar masalahnya: fungsi `updateCameraFollow()` sebelumnya sengaja mengecualikan fase `.result` (fase tempat cutscene berjalan) dari logika mengikuti kamera, sehingga lompatan tadi terjadi di luar bidang pandang kamera. Perubahan: ditambahkan offset kamera baru `catchCutsceneCameraOffset = [0, 0.9, 1.6]` (lebih rapat dibanding offset saat fight yaitu `[0, 1.5, 3.0]`). `updateCameraFollow()` diberi cabang baru di awal: selama cutscene aktif, kamera melihat ke `hookEntity` dari offset tersebut lalu langsung berhenti (early return). Hasil: lolos typecheck, belum dicoba langsung. (tag: Mekanik Cast/Fight)

3. Cutscene Macet di Puncak Lompatan (ADR-047)

Tanggal: 2026-07-20. Alasan: "ikan/bobber perlu diam sejenak di puncak supaya cutscene tidak terasa terlalu singkat." Masalahnya, rumus lengkung tunggal `sin(t*pi)` cuma menyentuh titik tertinggi sekejap saja. Perubahan: satu konstanta durasi diganti tiga fase durasi terpisah: `catchCutsceneRiseDuration` (0.35 detik), `catchCutsceneFreezeDuration` (0.9 detik), `catchCutsceneSettleDuration` (0.35 detik) — total naik dari 1.3 detik jadi 1.6 detik. Perhitungan tinggi ditulis ulang jadi tiga cabang: naik (`sin(t*pi/2)`), diam datar, lalu turun (`cos(t*pi/2)`), yang disambung mulus tanpa lompatan nilai di batas antar-fase. Hasil: lolos typecheck, belum dicoba langsung — semua durasi masih perkiraan, menunggu playtest. (tag: Mekanik Cast/Fight)

4. Cutscene Tertangkap Dibuat Lebih Dramatis (ADR-048)

Tanggal: 2026-07-20. Permintaan: "buat lebih dramatis" — setelah dicek, pemicu/kamera/waktu sudah benar, tapi secara visual terasa datar (kamera diam menyorot bola placeholder, judul memudar di atas HUD yang tidak berubah). Empat perubahan tampilan (tidak menyentuh logika): (1) Kamera mendekat (push-in): nama `catchCutsceneCameraOffset` diganti `catchCutsceneCameraEndOffset`, ditambah `catchCutsceneCameraStartOffset = [0, 1.2, 2.4]` dan `catchCutsceneTotalDuration`; kamera sekarang bergerak halus (smoothstep) dari posisi awal ke akhir sepanjang durasi total. (2) `CatchCutsceneView` diberi garis hitam ala bioskop (letterbox, 0 ke 64pt selama 0.25 detik), vignette melingkar plus cahaya kuning di belakang judul, redaman pegas diubah dari 0.55 ke 0.45 (jadi ada efek memantul yang terlihat), ukuran font dari 64 ke 72. (3) `ContentView`: HUD (indikator tegangan, kotak jumlah tangkapan, teks error) memudar jadi transparan penuh selama cutscene berjalan. (4) Sempat dicek apakah perlu menambah suara efek tangkapan — ternyata sudah ada (`catch_splash.wav` via `emit(.catchFish)`), jadi tidak perlu diubah. Hasil: lolos typecheck, belum dicoba langsung. (tag: UI, Mekanik Cast/Fight)

5. Daftar Perangkat Terpasangkan (Allowlist) Lewat CoreBluetooth (ADR-049)

Tanggal: 2026-07-20. Alasan: "list saved device, biar 1 hp ga konek ke mac lain" — permintaan daftar pairing/allowlist, ini pekerjaan pertama dari tiga pekerjaan hari itu. Perubahan: file baru `World/World/Transport/PairedDeviceStore.swift` menyimpan identifier/nama `CBPeripheral` yang sudah dipasangkan ke `UserDefaults`. Di `TransportHost.swift`: sistem mencoba `retrievePeripherals(withIdentifiers:)` untuk ID tersimpan lebih dulu, mengabaikan perangkat lain yang terdeteksi setelah berhasil terpasang, menyimpan begitu koneksi pertama sukses penuh, dan ditambah fungsi `forgetPairedDevice()`. Di sisi Rod, dibuat file baru `PairedCentralStore.swift` yang menyimpan identifier `CBCentral` yang terpasang (namanya tidak tersedia). `TransportClient.swift` juga diubah supaya mengabaikan subscriber lain begitu sudah terpasang, dan menyimpan subscriber pertama jika belum terpasang. Yang menarik: ditemukan juga bug kebocoran data yang nyata secara tidak sengaja — fungsi `sendUnreliable`/`drainReliableQueue` memanggil `updateValue(_:for:onSubscribedCentrals: nil)`, dan `nil` di sini berarti mengirim ke SEMUA central yang subscribe, bukan cuma yang "terhubung". Artinya Mac lain yang belum dipasangkan tapi ikut subscribe bisa diam-diam menerima data asli `RodState`/event, padahal pembukuan aplikasi sendiri tidak pernah mencatat ada koneksi. Bug ini diperbaiki lewat properti komputasi baru `sendTargets` (`subscribedCentral.map{[$0]}`) yang menggantikan `nil` di kedua titik panggilan. Di sisi UI: `MainMenuView` mendapat teks "Paired with `<name>`" dan tombol "Forget Device"; Rod's Developer Details mendapat baris "Paired Mac" dan tombol "Forget Paired Mac". Hasil: modul Shared plus full typecheck lolos, tapi belum dicoba langsung dengan dua perangkat fisik — khususnya belum dipastikan apakah `retrievePeripherals` bisa selalu menemukan Rod yang sudah dikenal setelah aplikasi benar-benar di-restart, dan apakah skenario "perangkat kedua diabaikan" betul-betul jalan. (Catatan: bug pairing ini muncul lagi nanti di ADR-062.) (tag: Networking, Bugfix)

6. Gerbang "Start Casting" yang Eksplisit (ADR-050)

Tanggal: 2026-07-20 (pekerjaan ke-2 dari 3). Alasan: pemain ingin ada tombol "Start Casting" plus meter cast, dengan lemparan (cast) hanya terdaftar saat sentakan (flick) setelah memegang telepon ke belakang dulu, dan tombolnya nonaktif kecuali kail sedang idle. Setelah membaca `RodMotionInterpreter`/`CastResolver`, ditemukan mekanik "tarik ke belakang lalu sentak" (ADR-032) sudah ada secara utuh. Yang kurang sebenarnya: cast sebelumnya selalu "aktif" hanya berdasarkan kedekatan pose, tanpa langkah niat/arm eksplisit. Perubahan: `RodMotionInterpreter.interpret(_:)` diberi parameter wajib baru `isCastingArmed: Bool`. Kasus baru `case .cast where !isCastingArmed:` memperlakukan pose sebagai idle apa pun kedekatannya dengan `castPose`. `RodViewModel` mendapat `isCastingArmed`, fungsi `startCasting()` (hanya bisa jalan jika `isFishing && hookPhase==.idle && !isCastingArmed`); flag ini dibersihkan begitu cast asli terjadi atau `hookPhase` keluar dari `.idle`. Tombol "Start Casting" ditambahkan di Rod's `ContentView` (labelnya berubah jadi "Casting Armed…" selama aktif). `CastMeterView` tidak perlu diubah karena visibilitasnya sudah mengikuti `motionPhase` yang masuk fase mengisi daya (charging), jadi gerbang ini otomatis berfungsi. Hasil: lolos typecheck, belum dicoba langsung — rasa arm/disarm, dan mekanik tarik-sentak yang mendasarinya, belum dikonfirmasi di perangkat. (tag: Mekanik Cast/Fight)

7. Perbaikan Rig Rod: Penghalusan Bend, Putaran Reel, Perbaikan Line-Follow (ADR-051)

Tanggal: 2026-07-20 (pekerjaan ke-3 dari 3). Alasan: "benerin rod (rod diem or bend, reel muter, line ngikut)" — perbaiki rod yang terlihat diam/melengkung aneh, buat reel berputar, buat tali (line) mengikuti dengan benar. Setelah membaca penuh `RodRigController`/`WorldViewModel`, tidak ditemukan bug jelas pada rotasi keseluruhan rig. File `rod_main.usdz` diekstrak lewat unzip+usdcat: dikonfirmasi hanya ada 6 joint (Root/Handle/Rod_01-03/Tip), dan tidak ada geometri reel/spool sama sekali. Perubahan: fungsi `applyBend(pitch:roll:)` sekarang `mutating` dan menyaring input dengan low-pass filter (`bendSmoothing = 0.25`) sebelum menghitung sudut joint — ini dugaan perbaikan untuk "rod diem or bend" (noise sensor terbaca sebagai kedipan), belum dikonfirmasi langsung. Ditambahkan `reelEntity` placeholder (bola pipih, warna metalik gelap) yang ditempel di joint `Handle`; fungsi baru `applyReelSpin(reelSpeed:isReeling:deltaTime:)`, kecepatan putar 6.0 rad/detik dipilih sendiri (karena tidak ada geometri reel asli untuk dijadikan acuan). Fungsi `applyLineTension(_:)` sekarang membalik-putar sumbu kendur (sag) mengikuti orientasi dunia dari joint tip (sebelumnya memakai sumbu lokal tetap `[1,0,0]`) — ini memperbaiki bug garis bercabang (line-fork) yang sebelumnya sudah diprediksi dalam catatan "Known Remaining Issue". Hasil: lolos typecheck (hanya modul World, Rod tidak diuji ulang terpisah pada entri ini), belum dicoba langsung — ketiga perbaikan eksplisit ditandai sebagai versi awal/perkiraan. (tag: Rendering, Bugfix)

8. Bobber Mengambang: Goyangan Idle + "Interest Dip" Sebelum Gigit (ADR-052)

Tanggal: 2026-07-20. Alasan: "bobber nya celap celup... kurang realistis" — bobber terlalu diam, ikan menggigit terlalu instan tanpa ancang-ancang. Ditemukan `hookEntity.position.y` memang tidak pernah disentuh selama menunggu (diam sempurna). Juga ditemukan `FishFightController.isFishInterested` (ADR-029) sudah ada dengan jendela waktu 0.8-2.0 detik sebelum gigit yang tepat, tapi tidak ada konsumen visualnya (hanya haptic dan suara). Perubahan: ditambahkan `idleBobAmplitude/Frequency` (0.02 m / 0.5 Hz) dan `interestDipAmplitude/Frequency` (0.06 m / 2.0 Hz). Fungsi baru `updateBobberFloat(deltaTime:)` menumpuk goyangan idle terus-menerus ditambah dip ke bawah saja selama `isFishInterested` aktif. Saling eksklusif dengan logika reeling di tick yang sama. Fase direset ke 0 setiap cast baru. Hasil: lolos typecheck, belum dicoba langsung — konstanta belum diukur dari rekaman referensi. (tag: Fish AI, Mekanik Cast/Fight)

9. Arah Fight: Juking Kiri/Kanan dengan Waktu Acak (ADR-053)

Tanggal: 2026-07-20. Alasan: pemain ingin ikan menarik kiri/kanan dengan pengacak cepat, mengharuskan pemain melawan dengan menarik rod ke arah berlawanan, berlangsung sekitar 5-10 detik per fight. Perubahan: `FishFightConfig` mendapat `fightDirectionMinDuration/MaxDuration` (5.0/10.0 detik), `SwitchMinInterval/MaxInterval` (0.6/1.8 detik), `RollThreshold` (0.15 rad), `CounterRelief` (0.25 tegangan/detik), `Penalty` (0.20 tegangan/detik). `FishFightController` mendapat enum baru `FightDirection` (.left/.right); gigitan ikan memicu arah/durasi/waktu ganti acak; `updateFight(...)` sekarang wajib menerima parameter `rodRoll: Double`; fungsi baru `updateFightDirection` menilai benar-tidaknya melawan arah, lalu menambahkan relief/penalty ke dalam rumus tegangan. `WorldViewModel` mendapat `updateFightDirectionDrift` yang menggeser posisi X kail secara kosmetik saja (tidak memengaruhi perhitungan tegangan). HUD: `LineTensionView` mendapat baris baru "Fish pulling ←/→" / "Lean rod →/←". Hasil: kedua modul lolos typecheck, belum dicoba langsung — ketujuh angka baru semuanya dipilih sendiri. (Catatan: nilai penalty ini nanti dipotong setengah di amandemen ADR-039 di bawah.) (tag: Fish AI, Mekanik Cast/Fight)

10. Timer Tahan untuk Cast, Haptic Diperkuat, Tier 3/4 Diringankan (ADR-054)

Tanggal: 2026-07-20. Tiga permintaan sekaligus: tampilkan meter cast di UI World memakai timer tahan (semakin lama ditahan, semakin jauh lemparannya saat disentak); haptic dirasa terlalu lemah, tingkatkan intensitasnya; ringankan sedikit tier ikan 3 dan 4. Perubahan: (1) Kekuatan cast diubah dari jarak tarik-mundur (ADR-032) menjadi timer durasi tahan: `CastSnapshot.backswingDistance` diganti `backswingHoldDuration`; batas `CastConfig` diganti jadi batas durasi tahan (0.15/1.5 detik); `RodMotionInterpreter` sekarang melacak `castHoldStartTimestamp`, bukan jarak puncak; rumus campuran `holdPower*(0.7+0.3*flickPower)` tetap dipakai, hanya sumber dasarnya yang berubah. (2) Haptic diperkuat: cast dari 0.55 ke 0.75, splash dari 0.32 ke 0.45, reelTick dari 0.20 ke 0.40, puncak fishBite dari 0.65 ke 0.85, puncak catchFish dari 0.95 ke 1.00, lineBreak dari 0.90 ke 1.00, puncak fishEscaped dari 0.50 ke 0.70, fishInterested dari 0.22 ke 0.35. Intensitas pita tegangan berubah dari 0.30/0.45/0.70 jadi 0.50/0.70/0.95, ketajaman (sharpness) dari 0.30/0.45/0.60 jadi 0.40/0.55/0.75. (3) Tier 3/4 diringankan: resistensi `fish_c` dari 0.75 ke 0.60, `scott` dari 0.95 ke 0.75 (laju kenaikan tegangan bersih untuk fish_c turun dari sekitar +0.11/detik ke +0.18/detik, dan untuk scott dari sekitar +0.02/detik ke +0.11/detik — catatan: penjelasan sumber terbalik tapi angka apa adanya); lalu diretune lagi jadi angka datar 0.10 untuk fish_b/fish_c/scott (hanya fish_a yang tetap 0.35). Hasil: lolos typecheck di kedua modul, belum dicoba langsung — semua batas/haptic/resistensi masih pilihan tangan. (tag: Mekanik Cast/Fight, UI)

11. Tombol Reset Progres (ADR-055)

Tanggal: 2026-07-20. Alasan: "bisa di tambahin button buat reset level" — pemain ingin cara mengembalikan progres ke level 1. Perubahan: `ProgressionStore.reset()` menghapus jumlah tangkapan yang tersimpan; `FishFightController.resetProgression()` menyinkronkan ulang level dan menghapus pengumuman naik level; `WorldViewModel.resetProgression()` sebagai penerus panggilan; `MainMenuView` mendapat tombol destruktif "Reset Progress" di samping "Forget Device". Hasil: lolos typecheck, belum dicoba langsung. (tag: UI)

12. Perbaikan: Timer Tahan Cast Ter-reset karena Kedipan Klasifikasi (koreksi ADR-054)

Tanggal: 2026-07-20. Dilaporkan: "cast mechanism nge bug" tepat setelah mencoba ADR-054. Akar masalah: `castHoldStartTimestamp` di-reset setiap kali `previousClassification != .cast` — artinya, setiap kali klasifikasi baru saja menjadi `.cast` lagi. Noise sensor biasa bisa membuat klasifikasi berpindah `.cast`→`.idle`→`.cast` dalam satu frame saja, dan itu diam-diam menol-kan timer tahan setiap kali terjadi. Ada celah kedua: timer hanya dibersihkan di dalam cabang "disarmed tapi masih terklasifikasi cast", jadi timestamp basi dari percobaan sebelumnya bisa bertahan jika pose sempat kembali penuh ke idle di antara percobaan. Perbaikan: ditambahkan penjaga di level atas yang berjalan setiap frame sebelum switch klasifikasi: `if !isCastingArmed { castHoldStartTimestamp = nil }`. Kasus `.cast:` sekarang hanya memulai timer `if castHoldStartTimestamp == nil` (kebal terhadap kedipan satu-frame). Nama `enteredCast` diganti jadi `isFreshCastEntry` untuk kejelasan. Hasil: lolos typecheck, belum dicoba langsung — perbaikan ini disimpulkan dari membaca kode, bukan direproduksi di perangkat; ditandai sebagai "hal pertama yang dicek" jika cast masih terasa salah. (tag: Bugfix, Mekanik Cast/Fight)

13. Kekuatan Cast Menghapus Sentakan (Flick) Sepenuhnya; Timer Langsung Ditampilkan (ADR-056)

Tanggal: 2026-07-20. Alasan: "remove flick totally... tampilin cast meter power yang nge charge di World view" — pemain ingin gestur sentak (flick) dihapus sepenuhnya, diganti tampilan timer tahan murni di World, dengan jarak baru terdaftar setelah disentak. Perubahan: `CastResolver.resolve(_:)` menghapus istilah `flickPower` sepenuhnya, `normalizedPower = holdPower` langsung. Dihapus juga `CastSnapshot.angularVelocity`, `CastConfig.minimum/maximumCastVelocity`, `RodMotionInput.rotationRateX/Y/Z` (sudah dicek pakai grep, tidak ada pemakai lain) — `rotationRateMagnitude` (dipakai untuk kecepatan reel) tidak terpengaruh. Ditambahkan `castHoldDuration`/`releasedHoldDuration` ke `RodMotionInterpretation`, disalurkan lewat `RodState`. Ditemukan juga bahwa panggilan `CastMeterView` ternyata sepenuhnya dikomentari (di-nonaktifkan) di `ContentView.swift` — komponennya sendiri sebenarnya sudah benar sejak ADR-045, hanya tidak pernah ditampilkan. Ini dibongkar komentarnya, dan ditambahkan tampilan "Holding %.1fs". Hasil: lolos typecheck, dicek pakai grep memastikan tidak ada sisa referensi ke simbol yang dihapus. Belum dicoba langsung. (tag: Mekanik Cast/Fight)

14. Kekuatan Cast Menjadi Mini-Game Timing yang Berosilasi (ADR-057)

Tanggal: 2026-07-20. Alasan: permintaan desain ulang penuh: meter harus naik-turun terus-menerus selama diarmed (seperti game "cari momen yang tepat"), dan sentakan (flick) mendaftarkan nilai apa pun posisi meter saat itu. Perubahan: batas `CastConfig` diganti dengan `oscillationPeriod = 2.0 detik`. `CastResolver.resolve(_:)`: `power = (1-cos(holdDuration*2π/period))/2` — osilasi kontinu menggantikan rampa linear berbatas. Bagian lain di pipeline (pelacakan timestamp, gerbang arm, deteksi flick, pengiriman data) tidak diubah. Hasil: lolos typecheck, dicek pakai grep memastikan konstanta lama sudah benar-benar hilang. Belum dicoba langsung — periode 2.0 detik masih perkiraan awal. (Dikoreksi di entri berikutnya.) (tag: Mekanik Cast/Fight)

15. Perbaikan: Re-arming Mewarisi Kekuatan Cast Beku dari Cast Sebelumnya (koreksi ADR-057)

Tanggal: 2026-07-20. Dilaporkan: "power nya meningkat terlalu cepat, sampai sampai ketika aku pencet 'start casting', tiba tiba powernya udah max aja". Akar masalah: early-return `followThrough` (yang memutar ulang `releasedCastPower` yang sudah beku) hanya digerbang oleh cooldown lokal Rod pasca-rilis (sekitar 0.15-0.8 detik), tidak bergantung pada `isCastingArmed`. Melakukan arm ulang dalam jendela singkat itu membuat kekuatan cast sebelumnya (yang mungkin sudah dekat puncak) terus terputar ulang. Perbaikan: setiap kali `isCastingArmed` bernilai true, `castEndsAt` di-reset ke waktu sekarang SEBELUM pengecekan followThrough — arm baru selalu menang atas cooldown yang sedang berjalan. Selain itu `oscillationPeriod` diperlambat dari 2.0 detik ke 3.5 detik sesuai masukan "terlalu cepat" secara lebih umum. Hasil: lolos typecheck, belum dicoba langsung — belum dipastikan apakah ini sepenuhnya menjelaskan gejalanya, dan apakah 3.5 detik terasa "perlahan". (tag: Bugfix, Mekanik Cast/Fight)

16. Serah Terima Sesi: WINDOW_2.md

Tanggal: 2026-07-20. Entri dokumentasi murni. Ditambahkan file `WINDOW_2.md` yang mencakup ADR-041 sampai ADR-057 (plus dua amandemen koreksi yang sudah diuji langsung untuk ADR-054/057). Isinya: peringatan status git (pekerjaan belum di-commit di atas ujung `dev` yaitu `68d3f4d`, insiden detached-HEAD di tengah sesi beserta penyelesaiannya, beberapa stash tak terkait yang sudah ada sebelumnya), ringkasan kronologis (TL;DR), tabel ADR lengkap, peringatan standar "diverifikasi hanya lewat typecheck, belum pernah dimainkan", bagian masalah yang diketahui (menandai untuk ketiga kalinya masalah tabrakan nomor ADR-030/031 yang belum terselesaikan), dan saran langkah berikutnya. Fakta penting yang dicatat: ADR-044 sampai ADR-048 BUKAN pekerjaan sesi ini sendiri — semua itu masuk lewat penggabungan (merge) cabang paralel rekan tim `feat/playtest-refine`, itulah sebabnya batch pekerjaan sesi ini sendiri harus diberi nomor ulang dari 044-047 menjadi 049-052. (tag: Build/tooling)

17. Perbaikan Dokumentasi: Tabrakan Nomor ADR-030/031 Diselesaikan (menjadi ADR-058/059)

Tanggal: 2026-07-21. Alasan: "Beresin dulu known issue under WINDOW_2.md" — memperbaiki tabrakan nomor yang sudah ditandai sebelumnya. Penyelesaian: ternyata ada dua ADR-030 dan dua ADR-031 yang independen. Pasangan tentang progresi gameplay (ADR-030 "Resistance Scales With Rolled Size", ADR-031 "Cast/Fight Distance Fixes") dipertahankan apa adanya — karena sudah banyak dirujuk silang di tempat lain, jadi lebih mahal untuk diberi nomor ulang. Pasangan yang masuk lewat merge (ADR-030 "World Explicitly Authors First-Person Camera", awalnya ADR-016; ADR-031 "Transport Delivery Mode Matches Message Freshness") diberi nomor ulang menjadi ADR-058/059. Sebelumnya sudah di-grep ke seluruh repo lebih dulu; ditemukan nol referensi internal berdasarkan nomor yang perlu diperbarui. Narasi historis "Merge: feat/asset-integration" sengaja tidak disentuh (karena itu catatan akurat tentang masa lalu, bukan referensi yang masih hidup). Hasil: perubahan dokumentasi murni, `git diff --check` bersih, tidak ada yang perlu di-typecheck. (tag: Build/tooling)

18. Haptic Rod Dimaksimalkan ke Batas Atas CoreHaptics (ADR-060)

Tanggal: 2026-07-21. Alasan: "Increase haptics feedback... Multiply it to the largest number the phone supports." Perubahan: setiap nilai intensitas di `HapticManager` dinaikkan ke 1.0 (cast, splash, reelTick, keempat pulsa fishBite, pulsa kedua catchFish, ketiga pulsa fishEscaped, fishInterested). `lineBreak` sudah 1.0 sebelumnya. `lowBandIntensity/mediumBandIntensity/highBandIntensity` di `FishFightConfig` dinaikkan dari 0.50/0.70/0.95 (yang diatur di ADR-054) menjadi 1.0/1.0/1.0. `sharpness` (ketajaman) sengaja tidak diubah di mana pun — sekarang itulah satu-satunya pembeda antara tiga pita tegangan fight, karena intensitasnya sudah seragam maksimal. Hasil: kedua modul lolos typecheck (dibangun dengan target macOS 14.0 + simulator iOS 17.0 kali ini — catatan: target versi ini berbeda dari entri lain yang pakai macOS 26). Belum dicoba langsung — belum dipastikan apakah intensitas seragam maksimal masih bisa menyampaikan rasa tekanan yang meningkat (yang sebelumnya sebagian dibawa lewat intensitas). (tag: Mekanik Cast/Fight)

19. Perbaikan: Lima Catatan Playtest (ADR-061)

Tanggal: 2026-07-21. Satu ADR yang mencakup lima catatan playtest terpisah: (1) Meter cast terlambat merespons saat "Start Casting" ditekan: logika mengisi-daya/osilasi dipindah keluar dari switch klasifikasi pose bagian `.cast`, menjadi blok `if isCastingArmed {...}` tersendiri yang berjalan tanpa syarat sebelum switch itu — memperbaiki keterlambatan respons meter. (2) Kail mengambang dekat dermaga sebelum benar-benar mendarat: logika naik yang digerakkan `dockLiftSpeed` plus penjaga `distance3D > 0.15` diganti dengan langsung "snap" ke `hookLiftTargetPosition` begitu berada dalam jarak `hookLiftDistance` (1.0 m). Konstanta `dockLiftSpeed` yang sudah tidak terpakai dihapus. (3) Jarak cast menentukan ukuran ikan, memakai zona diskret bukan langit-langit kontinu: ditambahkan enum `CastZone` (hijau/kuning/merah, membagi jarak cast jadi tiga bagian) dan `sizeRollWindow` per zona (hijau [0, 0.375), kuning [0.375, 0.875), merah [0.625, 1.0]) yang diturunkan dengan membalik rumus rating bintang. `sizeCeilingRatio`/`biasedRoll(ceilingRatio:)` diganti dengan versi jendela lantai+langit-langit. (4) Penutupan layar hasil: ditambahkan `RodState.isCastingArmed`; penjaga di `startCasting()` sekarang menerima `hookPhase == .idle || .result`; penanganan `.hookPhase` di Rod sekarang hanya membersihkan flag arm untuk fase selain `.idle`/`.result` — supaya gema `.result` dari World sendiri tidak langsung membersihkan status arm. `ResultView` kehilangan tombol "Continue Fishing", digantikan teks: "Press 'Start Casting' on Rod to fish again." `WorldViewModel.handle(_:)` memanggil `continueFishing()` saat `isCastingArmed` yang masuk bernilai true. `HookPhase.allowsCasting` sendiri TIDAK disentuh (masih hanya `.idle`) — artinya sentakan fisik tetap tidak bisa memicu cast sampai putaran balik dari World selesai. (5) Tombol Retry Connection: ditambahkan `WorldViewModel.retryConnection()` (memutus lalu menyambung ulang); `MainMenuView` mendapat tombol "Retry Connection" yang muncul selama `!isRodReady`. Hasil: kedua modul lolos typecheck (kali ini pakai macOS 26, dengan catatan RealityKit butuh macOS 15+ dibanding target 14.0 yang dipakai untuk cek Shared-only biasa). Belum dicoba langsung di perangkat fisik — belum dipastikan apakah tier zona cast terasa pas sebagai jendela keras, dan apakah retryConnection benar-benar memulihkan status CoreBluetooth yang macet. (tag: Mekanik Cast/Fight, Bugfix, UI)

20. Perbaikan: Macet di Menu Utama — Pairing Rod yang Basi Tidak Punya Jalan Pemulihan (ADR-062)

Tanggal: 2026-07-21. Dilaporkan: World macet di layar "Press Start Fishing" padahal UI Rod sendiri menunjukkan `isFishing` bernilai true dan koneksi BLE terlihat sepenuhnya tersambung (`.ready`/`.streaming`). Percobaan pertama (ternyata bukan penyebabnya): diduga ada kegagalan decode skema akibat field baru `isCastingArmed` jika hanya salah satu sisi yang dibangun ulang. Cara decode di kedua transport diubah dari `try?`/`continue` (diam-diam dibuang) menjadi `do`/`catch` dengan `#if DEBUG print`. Pengguna mencoba ulang dan TIDAK ada error decode yang tercatat — dugaan ini disingkirkan, tapi logging-nya tetap dipertahankan sebagai perbaikan diagnostik permanen (dan justru inilah yang membuktikan dugaan pertama salah, dengan cepat). Percobaan kedua (penyebab asli): petunjuk kunci dari pengguna — "Forget Device" di World membuat Rod sama sekali tidak bisa terhubung lagi setelahnya, bahkan bertahan setelah aplikasi di-restart penuh. `PairedDeviceStore`/`PairedCentralStore` (ADR-049) disimpan secara independen di masing-masing sisi — "Forget Device" di World hanya menghapus identifier miliknya sendiri, tidak pernah menghapus milik Rod. Setelah membaca `TransportClient.didSubscribeTo`: CoreBluetooth tidak memberi API bagi peripheral untuk menolak subscription, jadi central yang tidak cocok tetap bisa subscribe di level protokol (World tetap benar melihat `.ready`/`.streaming`), tapi lapisan aplikasi Rod tidak pernah menetapkan `subscribedCentral`, sehingga `sendTargets` tetap kosong dan Rod tidak pernah mengirim apa pun — termasuk pesan `.state` yang pertama sekali pun. Fungsi `forgetPairedDevice()` sebenarnya sudah ada dan berfungsi, hanya saja tidak ada UI yang bisa memanggilnya (tombol asli Rod berada di bagian Developer Details yang dikomentari/nonaktif, sudah ditandai di WINDOW_2.md sebagai kemungkinan pekerjaan orang lain yang belum selesai). Perbaikan: ditambahkan `pairedDeviceRow` mandiri langsung di tampilan utama Rod (bukan dengan membongkar komentar blok Developer Details) — meniru baris serupa milik World: "Paired with `<short ID>`" plus tombol destruktif "Forget Paired Mac". Hasil: kedua modul lolos typecheck, belum dicoba langsung dalam reproduksi nyata kondisi macet dua-perangkat — diagnosis dilakukan lewat membaca kode dan eliminasi, bukan reproduksi di perangkat. (tag: Networking, Bugfix)

21. Perbaikan: Meter Cast Terisi Normal tapi Kail Tidak Pernah Melempar — Setiap Cast Diam-Diam Hilang Lewat BLE (ADR-063)

Tanggal: 2026-07-21. Dilaporkan tepat setelah perbaikan ADR-062 dikonfirmasi berhasil: meter cast terisi/dilepas dengan benar di Rod, tapi kail tidak pernah melempar di World. Didiagnosis langsung selama beberapa putaran memakai logging sementara yang hasilnya dibaca ulang dari console pelapor. Urutan diagnosis: (1) Logging di pemanggil `RodMotionInterpreter` menunjukkan osilasi benar, pelepasan bersih, `hookPhase`/flag benar — menyingkirkan kemungkinan logika di sisi Rod. (2) Logging di `WorldViewModel.handle(_:)`/`launchHook(solution:)` menunjukkan NOL baris log `[CAST]` di console World selama tes koneksi stabil — pesan memang tidak pernah sampai sama sekali. (3) Ketidakstabilan koneksi disingkirkan sebagai penyebab (urutan reconnect awal cuma noise saat start-up; koneksi tetap `.ready`/`.streaming` sepanjang waktu). (4) Logging langsung di `TransportClient.sendUnreliable(_:)` (pengiriman BLE di Rod) menemukan penyebab sebenarnya: setiap pesan `.state` yang berisi `castSolution` mencatat log "DROPPED (too big for one chunk): ~531-542 bytes, budget=512, chunks=2", berulang selama puluhan frame. `sendUnreliable(_:)` ternyata tidak pernah memecah pesan (fragmentasi) — pesan yang melebihi anggaran (budget) diam-diam dibuang, tanpa logging sebelumnya. Perbaikan: `RodState` diberi `CodingKeys` eksplisit yang memetakan setiap properti ke nama pendek di jalur kabel (`timestamp`→"ts", `castSolution`→"cs", `castHoldDuration`→"chd", `isCastingArmed`→"ca", dst.) — murni memperkecil format data di jalur kabel, tanpa mengubah tipe atau titik panggilan. Tes encode mandiri mengonfirmasi: 540 byte (kunci default) menjadi 419 byte (CodingKeys baru) untuk payload representatif yang terisi penuh — sisa margin 93 byte di bawah anggaran 512 byte. `TransportClient.sendUnreliable(_:)` sekarang mencatat log baik untuk kasus dibuang-karena-kelebihan-anggaran maupun kasus antrean-penuh (`updateValue` mengembalikan false), keduanya di bawah `#if DEBUG`. Logging diagnostik sementara yang dibuat khusus untuk investigasi ini dihapus (hanya logging permanen `sendUnreliable` dan logging kegagalan decode dari ADR-062 yang dipertahankan). Hasil: kedua modul lolos typecheck; tes encode secara empiris mengonfirmasi pengurangan byte. BELUM dikonfirmasi ulang di perangkat pelapor sendiri bahwa cast sekarang benar-benar sampai ke World — perbaikan ini menjawab persis angka byte yang tertangkap, tapi lingkaran belum ditutup dengan konfirmasi akhir "sekarang sudah jalan" pada saat catatan ini ditulis. (tag: Networking, Bugfix)

22. Fitur: Alur Onboarding, Panduan Kalibrasi Langsung, Kalibrasi Kapan Saja saat Idle (ADR-064)

Tanggal: 2026-07-21. Alasan: menambahkan onboarding sebelum bermain (penjelasan game, tujuan, level, tutorial kalibrasi); membuat kalibrasi bisa dilakukan kapan saja selama `HookPhase == .idle` (tidak cuma di Menu Utama); menambahkan teks panduan kalibrasi langsung di World termasuk tombol mana yang harus ditekan. Ini menutup celah "World Main Menu + Tutorial + Calibration" yang sudah ditandai terbuka sejak ADR-029/038. Perubahan: `CalibrationStep` dipindah dari enum lokal Rod yang tidak bisa dikirim lewat jaringan (`MotionCalibrationStep`) menjadi file baru `Shared/Sources/Shared/Models/Rod/CalibrationStep.swift` (`.idle/.cast/.reel/.complete`). `RodState` mendapat `calibrationStep` (kunci kabel "cal"); tes encode mengonfirmasi 436 byte untuk payload terbesar, margin sekitar 76 byte di bawah anggaran 512 byte. `RodViewModel.calibrateMotion()` mendapat penjaga `hookPhase == .idle` — inilah yang sebenarnya membuat kalibrasi ulang bisa dilakukan di tengah sesi. Syarat nonaktif tombol Calibrate di Rod dilonggarkan menjadi `!hasMotionSample || hookPhase != .idle`; `startCastingButton` mendapat syarat nonaktif tambahan `calibrationStep != .complete`. File baru `World/World/Views/CalibrationGuidanceView.swift` — menampilkan judul/instruksi per langkah plus ajakan "Press '<label>' on your Rod". `MainMenuView`: tampilan panduan menggantikan baris "press Start Fishing" saat `isRodReady && calibrationStep != .complete`; ditambahkan tombol "How to Play". `ContentView`: overlay baru `recalibrationOverlay` muncul saat `screen == .playing && hookPhase == .idle && calibrationStep != .complete`. File baru `World/World/Views/OnboardingView.swift`: tiga layar statis yang bisa dibolak-balik manual (Welcome, Main Goal, Levels & Fish Tiers memakai ambang batas nyata 3/5/10/1 untuk small/medium/big/boss) dengan tombol Back/Next, titik halaman, "Skip Intro", dan tombol akhir "Let's Calibrate". Onboarding ini sendiri tidak menyertakan instruksi kalibrasi — itu diserahkan ke panduan milik MainMenuView. `WorldViewModel`: `Screen` mendapat kasus `.onboarding`; saat inisialisasi membaca flag tersimpan `world.hasSeenOnboarding`; ditambahkan `completeOnboarding()`/`showOnboarding()`. Perbaikan sampingan: komentar dokumentasi `ProgressionStore` yang menyebut skema tiga-level 10/10 yang sudah basi diperbaiki agar cocok dengan skema empat-level 3/5/10/1 yang benar-benar dipakai (ditemukan saat menulis halaman Levels di onboarding). Hasil: kedua modul lolos typecheck, tes encode mengonfirmasi margin aman. Belum dicoba langsung — ritme/naskah onboarding, dan keterbacaan kalibrasi ulang di tengah sesi (tidak ada petunjuk dalam gameplay yang mengarah ke tombol Calibrate di Rod), keduanya ditandai belum terverifikasi. (tag: UI, Mekanik Cast/Fight)

23. Retune: Kecepatan Tarikan Ikan −50%, Kecepatan Reel Pemain +50% (amandemen ADR-039)

Tanggal: 2026-07-21. Alasan: dua masukan rasa main: kecepatan tarikan ikan terlalu cepat (dipotong 50% di semua tingkat kesulitan); kecepatan menarik reel terlalu lambat (dinaikkan 50%). Perubahan: `FishFightConfig.basePullSpeed` dari 0.7 ke 0.35 (menskalakan `fishPullSpeed` secara seragam karena ini istilah dasar perkalian; `pullSpeedResistanceMultiplier` tidak disentuh). Konstanta `WorldViewModel.reelSpeed` dari 3.0 ke 4.5; batas minimum pasangannya dari `max(0.35,...)` ke `max(0.525,...)`. Hasil: lolos typecheck di kedua modul, belum dicoba langsung. (tag: Mekanik Cast/Fight)

24. Retune: Kedua Nilai Dipotong 25% Lagi (amandemen ADR-039, hari yang sama)

Tanggal: 2026-07-21. Tindak lanjut di hari yang sama: "kurangin keduanya by 25%." `basePullSpeed` dari 0.35 ke 0.2625; `reelSpeed` dari 4.5 ke 3.375; batas minimum dari 0.525 ke 0.39375. Semua diskalakan seragam, tidak perlu perubahan per-tier. Hasil: lolos typecheck, belum dicoba langsung. (tag: Mekanik Cast/Fight)

25. Fitur: Penalti Arah Fight Diringankan, Kecepatan Reel-In Diskalakan Mengikuti Tegangan sebagai Risk/Reward (amandemen ADR-039, hari yang sama)

Tanggal: 2026-07-21. Alasan: dua permintaan terkait: kurangi penalti selama status juking "Fish Pulling"; buat kecepatan reel-in mengikuti tegangan (zona hijau lebih lambat, zona merah lebih cepat) sebagai mekanik risk/reward. Perubahan: (1) `fightDirectionPenalty` dari 0.20 ke 0.10 (dipotong setengah; `fightDirectionCounterRelief` tidak diubah — hanya hukumannya yang diringankan, karena tidak ada persentase pasti yang diminta jadi dipotong setengah sebagai perkiraan pertama). (2) Ditambahkan `reelSpeedSafeTension` (0.7, sama dengan tepi zona hijau HUD), `reelSpeedDangerTension` (0.85, sama dengan tepi zona merah), `reelPullSpeedSafe` (0.5 m/detik), `reelPullSpeedDanger` (1.0 m/detik). Properti komputasi baru `FishFightController.reelPullSpeed` melakukan interpolasi linear di antara keduanya berdasarkan tegangan. `playerSpeed` di `WorldViewModel.reelHook` sekarang membaca nilai ini hanya selama `hasFishOnHook` bernilai true; menarik tali kosong (tanpa ikan) tetap memakai `reelSpeed` datar (3.375). Batas minimum diubah dari 0.39375 menjadi angka datar 0.1 (batas lama akan mendominasi rentang baru yang jauh lebih sempit). Hasil: kedua modul lolos typecheck, belum dicoba langsung — ditandai belum jelas apakah rentang 0.5-1.0 m/detik masih memungkinkan pemain menangkap ikan di zona aman sama sekali. (Catatan: rentang ini ditemukan tidak bisa dimainkan sama sekali di entri 27 di bawah.) (tag: Mekanik Cast/Fight)

26. Fitur: Loop Suasana Idle dan SFX Kemeriahan Tangkapan Besar (ADR-065)

Tanggal: 2026-07-21. Alasan: pemain menambahkan aset `idle.wav`/`reel_fight.wav` dan meminta: mainkan idle.wav selama status dasar/idle di sekitar 30% volume; mainkan reel_fight.wav saat tangkapan 4 bintang atau lebih, dengan volume yang sama. Perubahan: `SoundManager.Sound` mendapat `.idle`/`.reelFight` (keduanya .wav, volume default 0.3). `FishingSounds` mendapat `isIdleLoopActive`, `updateIdleLoop(active:)`, `bigCatch()` (sekali putar, dipanggil langsung karena `FishingEvent.catchFish` tidak membawa jumlah bintang). `WorldViewModel.tick` menjalankan loop idle; cabang tangkapan di `reelHook` mengecek `stars >= 4` lalu memanggil `bigCatch()`. Hasil: lolos typecheck, belum dicoba langsung — belum dipastikan apakah asetnya benar-benar termuat, dan apakah 30% adalah kekencangan relatif yang pas. DIGANTI HAMPIR SEKETIKA oleh entri 28 (revisi ADR-065) di bawah. (tag: UI)

27. Retune: Kecepatan Reel yang Diskalakan Tegangan Ternyata Tidak Bisa Dimainkan — 0.5/1.0 diubah jadi 2.0/4.0 m/detik (amandemen ADR-039)

Tanggal: 2026-07-21. Dilaporkan tepat setelah retune risk/reward di entri 25: "the gameplay is not even playable now. coba base nya 2 m/s instead of 0.5-1, dan dari 2 m/s di green area, naik jadi 4 m/s di red." Catatan: ditemukan `reelPullSpeedSafe/Danger` ternyata sudah berada di 3.375/5.0 (nilai antara yang sempat diedit di luar percakapan ini, di antara giliran) — nilai ini dijadikan titik awal yang sah. Perubahan: `reelPullSpeedSafe` menjadi 2.0, `reelPullSpeedDanger` menjadi 4.0 (rasio 2x yang sama tetap dipertahankan). Ambang batas interpolasi (0.7/0.85) tidak diubah. Hasil: lolos typecheck, belum dicoba langsung — peringatan yang sama seperti sebelumnya. (tag: Mekanik Cast/Fight)

28. Revisi: reel_fight.wav Didefinisikan Ulang dari Kemeriahan Bersyarat Bintang Menjadi Musik Fight dengan Fade-Out (revisi ADR-065)

Tanggal: 2026-07-21. Alasan: tepat setelah ADR-065 selesai, diminta desain ulang penuh: "reel fight music harusnya hanya played ketika hook status nya ada fish. selama hook idle/inWater aja, play music idle. ketika berhasil catch fish, matikan perlahan music reel fight." — dari kemeriahan sekali-putar bersyarat 4-bintang menjadi musik fight yang mengikuti status dengan fade-out. Perubahan: `SoundManager.stopLoop(_:fadeDuration:)` mendapat parameter fade (default 0, tetap kompatibel dengan kode lama) memakai `AVAudioPlayer.setVolume(_:fadeDuration:)`. `FishingSounds`: ditambahkan `reelFightFadeDuration = 1.5 detik`, `isReelFightLoopActive`; `bigCatch()` dihapus sepenuhnya; ditambahkan `updateReelFightLoop(active:)`. Syarat aktif loop idle diperluas mencakup `.hookInWater` juga. Pengaman berhenti otomatis ditambah/diperluas menyesuaikan. `WorldViewModel.tick`: syarat loop idle diperluas; ditambahkan `updateReelFightLoop(active: hookPhase == .fishOnHook || .reeling)`. Cabang tangkapan di `reelHook` menghapus pengecekan `stars >= 4`/panggilan `bigCatch()` sepenuhnya — fade-out sekarang otomatis terjadi begitu status yang menggerakkan loop keluar dari fase aktifnya. Hasil: lolos typecheck, belum dicoba langsung — rasa fade 1.5 detik, dan apakah pemisahan musik idle/fight terasa menyatu di batas transisi, keduanya masih terbuka. (tag: UI)

29. Perbaikan: Cast Pendek (Zona Hijau) Bisa Mendarat di Dalam Area Dermaga Sendiri (ADR-066)

Tanggal: 2026-07-21. Alasan: "jarak si cast nya... jadi kuning sekarang distancenya hijau, merah tetap, dan kuning yang baru itu between hijau dan merah (biar gaada lagi case cast di hijau tapi nembus pier kebawah)" — cast terpendek bisa mendarat di dalam mesh dermaga itu sendiri. Diagnosis: rumus jangkauan proyektil dikerjakan manual lalu diverifikasi dengan skrip mandiri. Rumus lama (`castSpeed*(0.5+power*1.5)`) membuat cast terpendek (power=0) hanya sekitar 0.46 m — jauh lebih pendek dari sekitar 2.4 m ruang aman yang dibutuhkan (mesh dermaga menjangkau sampai Z=-4.5 menurut ADR-043, rod dipegang dekat Z=-2.1). Simulasi balistik memang sama sekali tidak punya kesadaran akan tabrakan (collision). Perbaikan: rumus kecepatan di `launchHook` diubah menjadi `castSpeed*(1.4 + power*0.6)` — dipilih supaya langit-langit di power=1 tetap sama secara matematis (`castSpeed*2.0`) sementara lantai di power=0 naik dari 0.5x menjadi 1.4x. Tabel jarak yang diverifikasi (lama→baru): power 0.00: 0.46→3.62 m; 0.33: 3.65→9.40 m; 0.66: 11.37→16.51 m; 1.00: 24.09→24.09 m (tidak berubah). Estimasi jarak yang ditampilkan di Rod sendiri (`CastMeterConfig`, yang memang sudah terpisah dari fisika sebenarnya) sengaja tidak disentuh — di luar cakupan. Hasil: lolos typecheck; skrip Python mandiri mengonfirmasi tabel secara numerik. Belum dicoba langsung — belum dipastikan apakah 3.62 m terasa cukup lega (atau malah pas di batas). (tag: Mekanik Cast/Fight, Bugfix)

30. Fitur: SFX fish_catched, Pengurangan Tegangan (Slacking) Diringankan 30% (ADR-067)

Tanggal: 2026-07-21. Tiga hal: (1) Musik idle selama `.hookInWater` — dicek dulu, ternyata sudah diterapkan (masuk lewat revisi ADR-065); tidak ada perubahan kode, langsung dikonfirmasi ke pengguna alih-alih diedit ulang membabi buta. (2) Pengguna menambahkan `fish_catched.wav`; `SoundManager` mendapat `.fishCatched` (volume default 0.85); kasus `.catchFish` di `FishingSounds.handle(_:)` sekarang juga memutar suara ini bersamaan dengan suara splash yang sudah ada (ditambahkan, bukan menggantikan). (3) `FishFightConfig.baseFishPullRate` dari 0.15 ke 0.105 (dipotong 30%) — memakai pola penskalaan hanya-di-istilah-dasar yang sama seperti dipakai sebelumnya untuk `basePullSpeed`. Hasil: lolos typecheck, belum dicoba langsung. (tag: UI, Mekanik Cast/Fight)

31. Fitur: Animasi Ikan Tertangkap Diintegrasikan (tanpa nomor ADR — entri integrasi aset, belakangan menjadi ADR-068)

Tanggal: 2026-07-20 (catatan: tanggalnya lebih awal dari entri-entri di sekitarnya pada hari yang sama, tampak di luar urutan kronologis ketat dalam file sumber, kemungkinan artefak hasil merge). Alasan: "skrg kita butuh animasi ikan ketangkep buat scene barusan" — cutscene (ADR-045) membutuhkan animasi ikan tertangkap yang nyata, menggantikan bola placeholder. Pengguna menunjuk ke repo aset saudara `new-blender-project`. Riset: membaca `CLAUDE.md`, `FishAssets/AGENTS.md`, dan `FishAssets/APP_TEAM_HANDOFF.md` milik repo aset (catatan serah-terima dari sesi sebelumnya soal fitur pratinjau ikan AR yang sudah ditunda). Catatan itu mendokumentasikan cacat per-spesies di perangkat: 5 dari 8 spesies yang sudah lengkap masih punya cacat orientasi/normal/ekspor UV yang belum terselesaikan (Skipjack Tuna, Grouper, Giant Trevally dikonfirmasi masih rusak; Bluefin/Yellowfin sudah diperbaiki tapi belum dikonfirmasi di perangkat). Hanya dua spesies yang disebut andal: Sailfish (ditandai sebagai pengecualian, "jangan disentuh") dan Mekong Giant Catfish (disebut sebagai templat acuan sebenarnya untuk memperbaiki spesies lain). Spesies yang dipilih: Mekong Giant Catfish (dibanding Sailfish), semata-mata karena ini pipeline "normal" yang bukan pengecualian. Diverifikasi secara independen (bukan sekadar dipercaya) lewat unzip+usdcat bahwa `MekongCatfish_Caught.usdz` benar-benar berisi `Skeleton "CatfishArm"` plus `SkelAnimation "Catfish_Caught"` dengan `rotations.timeSamples`. Perubahan: file usdz 4.9MB disalin ke `CtSPoC/World/World/Assets/FishCaught/mekong_catfish_caught.usdz`, diberi nama ulang jadi snake_case. `shasum -a 256` mengonfirmasi berkasnya identik byte-per-byte sebelum dan sesudah disalin. Dicatat ini adalah kategori aset baru (`Assets/FishCaught/`) yang tidak tercakup dalam aturan sinkronisasi khusus-Environment milik repo sumber. `WorldViewModel.makeSceneEntity()`: memuat `caughtFishEntity` sekali, nonaktif secara default, sebagai saudara dari `hookEntity`. Ditambahkan `caughtFishAuthoredLength = 2.0` (sesuai Source_Log aset tersebut). Fungsi `beginCatchCutscene`: menonaktifkan `hookEntity`, mengaktifkan `caughtFishEntity`, menskalakan ke `max(rolledLength/authoredLength, 0.35)` supaya ukuran tampilan mengikuti hasil roll tangkapan sebenarnya, lalu memutar `availableAnimations.first`. Perhitungan tinggi difaktorkan ke fungsi bersama `catchCutsceneHeight(elapsed:)`. Push-in kamera di `updateCameraFollow` sekarang menghitung fokus langsung dari `origin + height`, bukan lagi dari `hookEntity.position` yang sudah basi. Yang secara eksplisit belum dikerjakan: pemetaan spesies per-tier (setiap tangkapan menampilkan spesies ini apa pun tier-nya — `FishCatalog` tidak punya field referensi aset, ini keputusan konten yang masih terbuka, khususnya untuk "scott" si ikan boss); tidak ada koreksi orientasi yang diterapkan (mempercayai klaim repo sumber bahwa tidak perlu koreksi, belum dikonfirmasi di renderer ini); atribusi CC-BY untuk aset ini (Sketchfab "Ikan Patin", CC-BY 4.0) dicatat sebagai wajib tapi BELUM dipenuhi di mana pun — belum ada layar kredit yang dibuat (ada di cabang lain yang belum di-merge). Hasil: lolos typecheck, shasum cocok, pemeriksaan usdz mengonfirmasi animasi nyata. Belum dicoba langsung — skala tampilan/framing, apakah `.first` benar-benar mengarah ke klip yang dimaksud, dan orientasi ruang dunia (tanpa koreksi apa pun) semuanya belum terverifikasi. Entri ini belakangan menjadi ADR-068 menurut rujukan-rujukan berikutnya (nomor ADR-nya sendiri tidak muncul di judul bagian ini, tapi dirujuk balik oleh ADR-069 dan seterusnya). (tag: Fish AI, Rendering)

32. Perbaikan: Ikan Tertangkap Menghadap ke Rod, Gerakan Meronta Dipercepat (ADR-069)

Tanggal: 2026-07-20. Dilaporkan sebagai umpan balik playtest langsung pertama: "ikannya masih make animasi preview, kurang cocok, harusnya animasi ikan ngamuk ketika ditangkep... orientasi nya ngikuti arah pancing" — ikan terlihat seperti memakai animasi Preview, seharusnya meronta dan menghadap ke rod. Diagnosis: dikonfirmasi ulang (bukan cuma dipercaya lagi) lewat unzip+usdcat bahwa kuaternion ke-11 joint memang berubah dari frame ke frame sejak frame 0 — ini animasi nyata. Hanya ada satu `SkelAnimation`, dan sudah terikat (bound) secara eksplisit. Penyebab dipersempit menjadi: ikan yang secara teknis sudah beranimasi tapi tetap duduk pada rotasi identitas default RealityKit sepanjang waktu (ADR-068 memang tidak pernah mengatur orientasi sama sekali). Investigasi sumbu: membaca `Skeleton.bindTransforms` langsung: badan ikan berjalan sepanjang sumbu Y lokal (kepala Y≈-0.6, ekor Y≈+0.5/+0.75), sumbu punggung-perut sepanjang Z lokal (punggung Z≈+0.1, perut Z≈-0.1). Dikonfirmasi lewat metadata `upAxis="Z"` di file bahwa ekspor usdz ini mempertahankan konvensi Z-up asli Blender (berbeda dari eksportir glTF saudaranya yang menukar sumbu jadi Y-up, sesuai AGENTS.md §5.3 repo tersebut) — ini dicek langsung, bukan diasumsikan, karena dua jalur ekspor itu memang bisa saja berbeda. Perubahan: ditambahkan `fishOrientation(facing:)` — konstruksi dua langkah: menyelaraskan sumbu -Y lokal ke arah depan lewat rute terpendek, lalu mengoreksi roll di sekitar arah depan supaya sumbu +Z lokal mendekati arah atas dunia. Ditambahkan `catchCutsceneFishOrientation(at:)` — arah depan dihitung dari posisi ujung rod (tip) saat itu dikurangi posisi ikan saat itu (meniru cara `launchHook` menurunkan arah bidik cast dari ujung rod). Kedua fungsi cutscene mengatur `caughtFishEntity.orientation` setiap frame dari hasil ini. Ditambahkan `catchCutsceneFishAnimationSpeed = 1.4`, diterapkan lewat `AnimationPlaybackController.speed` — ini menjawab kesan "ngamuk/ngepak" lewat tempo, satu-satunya cara yang tersedia tanpa menulis ulang animasi di Blender (dokumentasi repo sumber mencatat gerakan spesies ini memang sengaja dibuat lebih lembut/lambat dibanding ikan lain). Hasil: lolos typecheck, belum dicoba langsung — perbaikan ini menjawab umpan balik playtest pertama tapi belum diuji sendiri; identifikasi sumbu sudah dicek lewat file, tapi perilaku koreksi roll, pilihan acuan ujung rod, dan kecepatan 1.4x semuanya masih tahap pertama. Pendekatan orientasi ini nantinya dibalik dua kali lagi (ADR-072, ADR-073). (tag: Fish AI, Bugfix)

33. Serah Terima: Animasi "Tertangkap" Meronta Baru + Rig Baru Diminta dari Repo Aset Blender (tanpa ADR CtSPoC)

Tanggal: 2026-07-20. Alasan: putaran umpan balik kedua: "butuh animasi baru dengan rigging baru... ikan ngamuk dan ada slowmotion ketika di cut scene ketangkep." Pengguna ingin rigging/animasi baru plus slow-motion untuk kesan tangkapan yang lebih dramatis. Kendala yang ditemukan: sesi yang berakar di CtSPoC ini tidak punya koneksi Blender/MCP yang aktif — pencarian tool ("blender") tidak menemukan apa-apa (`.mcp.json` milik repo saudara hanya tersambung otomatis kalau sesi berakar di sana). Pengguna ditanya lewat `AskUserQuestion` bagaimana melanjutkan (slow-motion in-app sekarang / spesifikasi serah-terima / keduanya). Pengguna memilih hanya spesifikasi serah-terima. Yang ditulis: file baru `new-blender-project/FishAssets/APP_TEAM_HANDOFF_CAUGHT_STRUGGLE.md`, mengikuti preseden dari `APP_TEAM_HANDOFF.md` yang sudah ada (tidak terkait, sedang berjalan, tidak disentuh). Isinya: alasan permintaan, definisi "ngamuk" (rontaan beramplitudo besar, kibasan kepala, sirip kaku, kontras dengan klip lembut yang sudah ada), bentuk garis waktu yang disarankan (rontaan cepat 0-1 detik, sapuan puncak 1-2.5 detik yang dimaksudkan jatuh di slow-motion, ekor tenang opsional), pertanyaan terbuka soal membuat ulang klip Mekong Catfish vs. spesies baru (belum diputuskan), hirarki joint dan konvensi sumbu rig saat ini secara persis (dibaca langsung dari usdz yang sudah dikirim, bukan diasumsikan), preferensi kuat untuk mempertahankan konvensi itu supaya sumbu yang di-hardcode di `fishOrientation(facing:)` tidak perlu diubah, langkah ekspor/pengiriman yang meniru proses verifikasi-shasum ADR-068, dan pewarisan lisensi CC-BY. Hasil: tidak ada kode CtSPoC yang berubah. Entri ini sendiri adalah dokumentasi serah-terima, belum ada keputusan arsitektur (ADR) di sini. (tag: Fish AI, Build/tooling)

34. Fitur: Slow-Motion di Dalam Kode untuk Fase Freeze Cutscene Tangkapan (ADR-070)

Tanggal: 2026-07-20. Alasan: tindak lanjut: pengguna bilang "coba lagi" tanpa konteks; ditanya langsung tiga opsi mana yang mau dicoba ulang, pengguna memilih menerapkan slow-motion di kode sekarang, memakai aset yang sudah ada. Bagian animasi baru tetap tinggal di dokumen serah-terima. Perubahan: ditambahkan `catchCutsceneSlowMotionFactor = 0.3`, `catchCutsceneSlowMotionEaseFraction = 0.2`. Fungsi baru `catchCutsceneTimeDilation(elapsed:)`: mengembalikan nilai 1 di luar fase freeze; di dalam fase itu, melandai dari 1 ke 0.3 selama 20% pertama, bertahan datar selama 60% tengah, lalu melandai kembali selama 20% terakhir. `updateCatchCutscene`: `cutscene.elapsed` sekarang maju sebesar `deltaTime * dilation` — karena baik posisi (`catchCutsceneHeight`) maupun gerakan kamera sudah membaca `elapsed`, perubahan ini saja memperlambat gerakan ikan dan kamera bersamaan. Ditambahkan penyimpanan `caughtFishAnimationController` (sebelumnya dibuang begitu saja), yang sekarang di-retarget setiap frame ke `speed = catchCutsceneFishAnimationSpeed * dilation` supaya animasi meronta ikan sendiri juga ikut masuk bullet-time. Efek pada waktu: durasi cerita nominal fase freeze (0.9 detik) tidak berubah, tapi waktu nyata (wall-clock) sekarang sekitar 2.3-2.4 detik dengan konstanta ini — total durasi cutscene sekarang sekitar 3 detik, naik dari sekitar 1.6 detik. Hasil: lolos typecheck, belum dicoba langsung — faktor 0.3, fraksi ease 0.2, dan hasil sekitar 3 detik semuanya dipilih sendiri; apakah terasa "dramatis" masih terbuka. (tag: Fish AI, Rendering)

35. Dokumen Serah-Terima Diperbarui Menyesuaikan ADR-070

Tanggal: 2026-07-20. Alasan: pengguna mengonfirmasi pekerjaan animasi/rig baru tetap diserahterimakan ke sesi Blender-MCP mendatang ("hand it off to the Blender repo, there'll be an agent there [later]"). Karena ADR-070 dibuat setelah dokumen serah-terima asli ditulis, deskripsi slow-motion di dokumen itu jadi basi/menyesatkan. Perubahan: `APP_TEAM_HANDOFF_CAUGHT_STRUGGLE.md` diperbarui di tempat: ditambahkan bagian tentang cara kerja slow-motion ADR-070 yang sebenarnya (durasi fase, kurva dilasi, waktu nyata yang dihasilkan) supaya puncak klip baru bisa dirancang agar jatuh tepat di dalam jeda slow-motion yang sebenarnya; panduan garis waktu ditulis ulang dari detik absolut menjadi sepertiga-sepertiga yang dirujuk silang ke fase nyata; daftar cek ekspor diperbarui supaya menyebutkan perlunya menyetel ulang konstanta slow-motion jika proporsi klip baru berbeda. `APP_TEAM_HANDOFF.md` dan pekerjaan lain yang belum di-commit di repo aset tidak disentuh. Catatan status git: dokumen serah-terima ini tetap belum dilacak (`??`) di repo aset — belum di-commit, sesuai preseden; repo itu punya banyak pekerjaan tak terkait yang belum di-commit dan meng-commit-nya memang tidak diminta. Hasil: pembaruan dokumentasi murni di repo saudara, tidak ada file CtSPoC yang berubah. (tag: Fish AI, Build/tooling)

36. Fitur: Animasi "Tertangkap" Baru Diintegrasikan, Kecepatan Diturunkan agar Puncak Jatuh di Tengah Slow-Motion (ADR-071)

Tanggal: 2026-07-20. Alasan: "udah ada animasi mekong fish yang baru untuk cut scene baru, implementasiin dong" — sesi Blender-MCP (yang bekerja di repo saudara) sudah mengambil alih serah-terima dan menyelesaikannya. Dikonfirmasi lewat CHECKPOINT.md milik repo tersebut (CP44/CP45) dan fakta bahwa dokumen serah-terima sudah dihapus sesuai instruksinya sendiri ("delete when done"). Yang dikerjakan sesi Blender (dibaca dari log mereka, bukan diturunkan ulang): klip "Caught" Mekong Giant Catfish yang sama dianimasikan ulang di tempat (model/rig sama, bukan spesies baru — sesuai rekomendasi dokumen serah-terima sendiri). Klip baru: 90 frame/24fps = 3.75 detik (lama: 72 frame/3.0 detik), tiga babak: rontaan liar (0-29), puncak lengkung tubuh penuh (30-59, puncaknya di frame 44 ≈1.83 detik/≈49% perjalanan), landai menurun (60-89). Sumbu rig dikonfirmasi ulang secara independen bahwa tidak berubah. Penyalinan aset: `mekong_catfish_caught.usdz` ditimpa di tempat. `shasum -a 256` mengonfirmasi hash barunya berbeda dari yang lama (perubahan nyata) dan cocok persis dengan sumbernya. Diverifikasi ulang secara independen (bukan sekadar dipercaya) lewat unzip+usdcat: `endTimeCode=89`, `timeCodesPerSecond=24` (mengonfirmasi 90 frame), masih satu `SkelAnimation` yang terikat, dan `bindTransforms` identik byte-per-byte dengan file sebelumnya untuk semua 11 joint — mengonfirmasi rig-nya memang benar-benar tidak berubah. Perubahan kode: ditambahkan `caughtFishAnimationPeakTime = 1.83`. `catchCutsceneFishAnimationSpeed` diubah dari nilai tetap `1.4` menjadi properti KOMPUTASI: `peakTime / (riseDuration + freezeDuration/2)` = 2.2875 pada konstanta saat ini — diturunkan supaya puncaknya selalu jatuh di dalam jeda freeze/slow-motion apa pun perubahan rise/freeze di masa depan. Sempat dipertimbangkan memperpanjang `freezeDuration` sebagai gantinya, tapi ditolak — perkiraan kasar menunjukkan total cutscene bisa 8+ detik (diperkuat oleh dilasi), tidak layak untuk sesuatu yang diulang setiap tangkapan. Hasil: lolos typecheck, shasum plus metadata usdz terverifikasi, aritmetika penurunan rumus dicek ulang secara manual. Belum dicoba langsung — perhitungan matematisnya solid berdasarkan asumsi yang dinyatakan, tapi apakah kecepatan sekitar 2.29x dari dasar terlihat cukup ganas (atau malah terlalu cepat/blur) masih terbuka. (tag: Fish AI, Rendering)

37. Merge: origin/main ke feat/playtest-refine — Tabrakan Nomor ADR-049/050/051/052 (menjadi ADR-064/065/066/067)

Tanggal: 2026-07-21. Alasan: "pull main, resolve conflict." Cabang `main` sudah maju cukup jauh lewat `dev` (pairing perangkat, gerbang arm cast, perbaikan rig rod, bobber mengambang, penulisan ulang osilasi cast, haptic dimaksimalkan, lima perbaikan playtest, pemulihan pairing, perbaikan kunci kabel `RodState`) dan sudah memakai ulang nomor ADR 049-052 untuk keputusan-keputusan itu, yang langsung bertabrakan dengan ADR-049-052 milik sesi ini sendiri (pekerjaan cutscene ikan). Ini adalah kejadian KETIGA dari pola tabrakan persis yang sama di proyek ini (sebelumnya ADR-030/031, dan sebelumnya lagi tabrakan ADR-044) — akar penyebabnya sama tiap kali: cabang-cabang independen memberi nomor maju dari titik cabang yang sama tanpa koordinasi. Urutan penyelesaian: (1) Pekerjaan sesi ini sendiri yang belum di-commit di-commit dulu (`5b021b6`) sebagai titik aman yang bersih. (2) `git merge origin/main --no-edit` — ada dua konflik nyata (`ARCHITECTURE_DECISIONS.md`, `CHECKPOINT.md`); semua yang lain, termasuk `WorldViewModel.swift` walau kedua cabang sama-sama banyak mengubahnya, ter-merge otomatis dengan bersih. (3) ADR-063 ke bawah sampai 049 milik `origin/main` dipertahankan sebagai acuan; empat ADR milik sesi ini sendiri diberi nomor ulang: 052→067, 051→066, 050→065, 049→064. (4) Penomoran ulang yang sama diterapkan ke entri `CHECKPOINT.md` (header dan referensi di dalam teks). (5) `SOFTWARE_ARCHITECTURE_DOCUMENT.md` dan `WorldViewModel.swift` ter-merge otomatis dengan campuran nyata antara referensi ADR-049-052 yang sah milik `main` dan milik sesi ini — setiap baris kemunculan dibaca satu per satu sesuai konteksnya untuk diklasifikasi dengan benar sebelum diberi nama ulang, bukan cari-ganti membabi buta. (6) Grep ke seluruh repo setelahnya mengonfirmasi tidak ada referensi ambigu yang tersisa (satu kecocokan ADR-050 di `GAMEPLAY_ARCHITECTURE_V1.md` dikonfirmasi sebagai referensi sah milik `main`, dan dengan sengaja dibiarkan). Hasil: kedua target Shared dibangun ulang dengan bersih; World/Rod lolos typecheck; `git diff --check` bersih; dikonfirmasi tidak ada deklarasi ganda di `WorldViewModel.swift`; fungsi `tick(deltaTime:)` dibaca langsung setelah merge untuk memastikan cabang-cabang baru milik `main` dan pekerjaan sesi ini sendiri berada di kasus-kasus yang benar-benar terpisah, tidak tumpang tindih. File `.xcodeproj/project.pbxproj` dan `xcuserdata` dikecualikan sesuai praktik standar sesi ini. BELUM dicoba langsung — khususnya belum dipastikan apakah fitur-fitur milik `main` (gerbang arm casting, bobber mengambang, rig rod) dan pekerjaan cutscene tangkapan sesi ini benar-benar berperilaku benar BERSAMA-SAMA saat dijalankan, hanya dipastikan bahwa kode gabungannya berhasil dikompilasi dan terbaca logis. (tag: Build/tooling)

38. Perbaikan: Ikan Tertangkap Memakai Pose Horizontal Tetap, Bukan Menghadap Rod (ADR-072)

Tanggal: 2026-07-21. Dilaporkan sebagai kesan pertama setelah merge: "animasi ngamuk nya masih belum bener, ikannya masih normal vertical belum horizontal seperti kondisi ditangkep" — ikan masih vertikal, tidak horizontal seperti kondisi tangkapan sesungguhnya. Akar masalah: orientasi dinamis menghadap-rod milik ADR-069 bergantung pada posisi ujung rod yang selalu berubah (mengikuti kemiringan telepon pemain saat itu, `rodState.orientation`) — hubungan yang tidak terprediksi ini menghasilkan pose hampir vertikal, mendongak ke atas menggantung, alih-alih tampilan klasik "mengangkat hasil tangkapan" menyamping (broadside). Perbaikan: `catchCutsceneFishOrientation` diubah dari fungsi dinamis per-panggilan menjadi properti KOMPUTASI KONSTAN: selalu menghadap arah dunia +X, memakai kembali helper `fishOrientation(facing:)` yang sudah ada tanpa diubah. Pencarian posisi ujung rod dihapus sepenuhnya. Dipilih +X secara khusus karena geometri kamera cutscene (semua offset berbagi X=0, baik ikan maupun kamera tidak pernah bergeser keluar dari garis itu) menjamin ikan yang menghadap sepanjang sumbu X akan terlihat tegak lurus terhadap kamera apa pun tinggi lompatan/gerakan dolly/lengkungan rod — lebih sederhana dan lebih andal dibanding menghitung arah "terlihat horizontal" dari status rod yang tidak terprediksi. Hasil: lolos typecheck, belum dicoba langsung — belum dipastikan apakah +X (dibanding -X) adalah pilihan arah yang benar (ditandai sebagai kemungkinan tinggal membalik tanda jika salah). Ini DIBALIK LAGI di entri berikutnya persis. (tag: Fish AI, Bugfix)

39. Perbaikan: Ikan Tertangkap Kembali ke Pose Vertikal Tetap "Tergantung Terkait" (ADR-073)

Tanggal: 2026-07-21. Dilaporkan satu giliran setelah ADR-072 tayang: keluhan awal pengguna "masih horizontal" ambigu, diperjelas lewat `AskUserQuestion` — keluhan sebenarnya: "ikannya kaya berenang, harusnya kepala nya di x, vertical, skrg horizontal kaya posisi berenang" (ikan terlihat seperti sedang berenang, seharusnya vertikal kepala di atas, bukan horizontal). Ini adalah PEMBALIKAN LANGSUNG dari arah yang dipilih ADR-072, hanya satu iterasi setelahnya. Perubahan: `catchCutsceneFishOrientation` diganti dengan konstruksi matriks rotasi langsung (tidak lagi memakai `fishOrientation(facing:)`, karena langkah koreksi roll-nya menjadi degenerate saat arah yang dituju persis arah atas dunia): sumbu -Y lokal (kepala) diarahkan ke +Y dunia (atas); sumbu X lokal (lateral/sisi tipis) diarahkan ke -Z dunia (sejajar arah pandang kamera, supaya kamera melihat siluet menyamping penuh); sumbu Z lokal (punggung) dihitung dari hasil perkalian silang (cross product) dua sumbu sebelumnya (tetap proper, tidak dicerminkan). Verifikasi: ditulis skrip REPL Swift mandiri yang menerapkan kuaternion hasil ke sumbu-sumbu lokal yang sudah diketahui — dikonfirmasi kepala jatuh di posisi dunia (0, ~1, 0) dan sumbu lateral di (0, 0, ~-1), cocok persis dengan yang dimaksud (verifikasi numerik, bukan sekadar diturunkan di atas kertas). Hasil: lolos typecheck, belum dicoba langsung — pilihan di antara dua opsi yang sama-sama valid untuk arah dunia punggung/lateral (pilihan sembarang begitu "kepala di atas, menyamping" terpenuhi) masih belum dikonfirmasi di perangkat, pertanyaan terbuka yang sama yang diwarisi dari ADR-072. (tag: Fish AI, Bugfix)

40. Fitur: Slow-Motion di Akhir Cutscene, Durasi +1.5 Detik, Kail/Tali Tetap Menempel (ADR-074)

Tanggal: 2026-07-21. Dua permintaan "sentuhan akhir" sekaligus: tambahkan slow-motion menjelang cutscene berakhir plus perpanjang durasi total 1.5 detik; juga buat tali/kail tetap terlihat menempel ke ikan selama cutscene (sebelumnya begitu saja menghilang). Perubahan: `catchCutsceneSettleDuration` dari 0.35 ke 1.85 (+1.5 detik sesuai permintaan — belakangan ditemukan cara menghitungnya keliru, lihat entri 41). `catchCutsceneTimeDilation` digeneralisasi supaya bentuk ease-in/tahan/ease-out yang sama bisa diterapkan pada fase freeze ATAU fase settle, memakai ulang konstanta yang sama untuk keduanya. Tidak ada perubahan lain yang diperlukan — push-in kamera dan penyesuaian ulang kecepatan animasi sudah sama-sama menurunkan nilainya dari dilasi yang sama setiap tick, jadi memperpanjang ke fase settle otomatis memperlambat semuanya bersamaan. Secara eksplisit ditandai sebagai risiko terbuka terbesar: karena slow-motion mengalikan waktu wall-clock jauh melebihi waktu cerita nominal (efek yang sama seperti ADR-070), total waktu nyata cutscene sekarang kira-kira 7+ detik (naik dari sekitar 3 detik) — belum dikonfirmasi langsung. Ditambahkan `caughtFishHeadOffset = 0.6` (dari jarak Y bindTransform joint `Head`) dan `catchCutsceneLineTension = 0.8` (sama dengan tegangan pertengahan fight). `beginCatchCutscene`/`updateCatchCutscene` sekarang memposisikan ulang `hookEntity` di kepala ikan setiap frame, bukan lagi menonaktifkannya. Panggilan tegangan tali di `tick` sekarang bercabang ke nilai tegangan cutscene selama cutscene aktif (sebelumnya tali sepenuhnya kendur sepanjang fase `.result`). Hasil: lolos typecheck, belum dicoba langsung — dua risiko terbuka ditandai: total panjang sekitar 7+ detik, dan apakah offset kail (yang belakangan ditemukan salah — lihat entri 41) benar-benar jatuh di mulut ikan secara visual. (tag: Fish AI, Bugfix)

41. Perbaikan: Cutscene Ditune Ulang Menjadi Tepat 5 Detik; Kail Diposisikan/Diskalakan Ulang ke Mulut Sebenarnya (ADR-075)

Tanggal: 2026-07-21. Dilaporkan sebagai screenshot pertama sungguhan dari aplikasi yang dilampirkan langsung ke pesan — mengonfirmasi ikan sudah benar vertikal/menyamping, judul "CATCH!" sudah tampil (validasi langsung pertama untuk perbaikan ADR-073). Tapi: "jadiin 5 detik deh, lalu masih aga broken begini bomber nya" — pengguna ingin total tepat 5 detik, dan kail/bobber terlihat duduk di atas/menutupi mata dan kepala ikan, jelas salah. Perbaikan durasi (diselesaikan dengan benar, bukan tebakan lagi): "+1.5 detik" datar milik ADR-074 diterapkan pada waktu CERITA tanpa memperhitungkan berapa banyak dilasi slow-motion mengalikannya menjadi waktu NYATA. Hubungan sebenarnya diturunkan: untuk fase dengan durasi cerita D di bawah bentuk dilasi yang sama, waktu nyata = D × C, di mana C = integral dari 0 sampai 1 dari du/dilation(u) adalah konstanta yang tidak bergantung pada D (proporsionalitas ini dibuktikan lewat substitusi). C dihitung sekitar 2.688 lewat integrasi numerik (python3/numpy/trapz), lalu persamaan `0.35 + C×(0.9+settle) = 5.0` diselesaikan langsung untuk mencari nilai settle. Perbaikan offset kail: `caughtFishHeadOffset=0.6` milik ADR-074 diambil dari posisi bind-pose joint `Head` — tapi sebuah joint skeletal itu titik poros (pivot), bukan permukaan mesh yang terlihat. Atribut `extent` usdz diperiksa ulang (kotak pembatas/bounding box mesh yang sebenarnya diukur): `[(-0.26,-1.00,-0.36),(0.26,1.00,0.36)]` — geometri sebenarnya (moncong/kumis) mencapai Y≈-1.0, jauh melewati -0.6 milik joint, ini persis menjelaskan kenapa kail jatuh terlalu pendek dan mendarat di kepala. Perubahan: `catchCutsceneSettleDuration` dari 1.85 ke 0.83 (hasil turunan, menyasar tepat 5.0 detik waktu nyata). `caughtFishHeadOffset` dari 0.6 ke 1.0 (bersumber dari extent mesh). Skala `hookEntity.scale` sekarang disamakan dengan skala `caughtFishEntity.scale` (sebelumnya tetap di radius dasar 0.07 m apa pun rentang skala tampilan ikan 0.35-1.0, membuat kail terlihat raksasa untuk tangkapan kecil). Direset ke 1 di `launchHook` untuk cast berikutnya. Hasil: lolos typecheck; matematika durasi dikonfirmasi independen oleh python3/numpy; `extent` usdz diambil ulang langsung (tidak lagi mempercayai perkiraan berbasis bindTransforms sebelumnya). Belum dicoba langsung dengan nilai-nilai spesifik ini — matematika durasinya pasti sesuai model, tapi apakah kail sekarang jatuh tepat di ujung moncong (atau masih pendek, atau malah kelewat) belum dikonfirmasi ulang lewat screenshot kedua pada saat itu. (tag: Fish AI, Bugfix)

42. Penyetelan: Offset Kail Digeser 1.0 → 1.15 ("dikiiittt lagi")

Tanggal: 2026-07-21. Screenshot langsung KEDUA mengonfirmasi perbaikan orientasi (ADR-073) benar-benar berhasil, dan kail sudah jauh lebih dekat tapi masih sedikit kurang sampai ke ujung moncong. Umpan balik: "dikiiittt lagi" (sedikit lagi). Ini mengamandemen ADR-075 di tempat (masih belum di-commit, jadi tidak diberi nomor ADR-076 terpisah untuk tindak lanjut hari yang sama ini): `caughtFishHeadOffset` dari 1.0 ke 1.15 — memberi sedikit ruang lebih dari ukuran extent yang terukur, bukan pas persis menempel, mengikuti pola yang sudah dipakai untuk offset ruang aman dermaga (ADR-043). Hasil: lolos typecheck, belum dicoba langsung dengan nilai 1.15 secara spesifik — masih terbuka apakah ini sudah pas atau perlu digeser lagi, dan terpisah, apakah total durasi 5.0 detik terasa pas begitu benar-benar dimainkan. (tag: Fish AI)

43. Fitur: Tali Cutscene Khusus, Kail ke Ujung Rod (ADR-076)

Tanggal: 2026-07-21. Alasan: screenshot KETIGA mengonfirmasi posisi kail sudah "lebih baik", diikuti: "kalo tambahin tali pancing nya bisa ga" (bisakah ditambah tali pancingnya?) — screenshot menunjukkan kail mengambang tanpa apa pun yang menghubungkannya ke rod. Investigasi: `RodRigController.applyLineTension(_:)` dibaca langsung — fungsi ini hanya membengkokkan rantai tulang (bone) yang sudah ada milik `rod_line.usdz` lewat rotasi prosedural, tanpa jalur kode untuk memperpanjang jangkauan total rig. Kail cutscene berada sekitar 2.6 m lebih jauh dari titik jangkar rod dibanding jarak fight pertengahan mana pun yang pernah dirancang untuk dijangkau rig ini (menurut koordinat ADR-042/043) — ini dikonfirmasi sebagai penyebab sebenarnya, bukan sekadar diasumsikan dari screenshot saja. Perubahan: ditambahkan `cutsceneLineEntity: ModelEntity?` — sebuah silinder tipis (`generateCylinder(height:1, radius:0.012)`, material `SimpleMaterial` putih polos), nonaktif secara default. Fungsi baru `updateCutsceneLine(from:)`: meregangkan/mengarahkan silinder ini setiap frame supaya persis membentang dari posisi kail saat itu ke ujung rod yang sedang bergerak — menskalakan sumbu Y lokal ke jarak yang terukur, memposisikan di titik tengah, memutar lewat `simd_quatf(from:[0,1,0], to: normalize(rodPoint-hookPosition))`. Dipanggil dari kedua fungsi cutscene tepat setelah masing-masing menghitung posisi kail. Nonaktif bersamaan dengan kail/ikan di titik-titik pembersihan. Kenapa memakai entitas baru, bukan meregangkan `rod_line.usdz` yang sudah ada: sempat dipertimbangkan menggerakkan rig berkulit (skinned) yang ada melampaui panjang bind-pose-nya, tapi ditolak — `AGENTS.md` §13 milik repo aset mendokumentasikan kelas kegagalan terkait ("Animating Multiple Bone Chains", risiko mesh robek/terdistorsi) untuk rig ikan, dan risiko yang sama berlaku untuk aset berkulit apa pun yang didorong melewati geometri yang dirancang aslinya. Silinder polos yang digerakkan lewat perhitungan vektor langsung menghindari semua ini sepenuhnya; cutscene ini juga sudah memaksa tegangan konstan (ADR-074) sehingga respons dinamis `rod_line` bahkan tidak relevan di sini. Hasil: lolos typecheck, belum dicoba langsung — belum dipastikan apakah silinder putih polos dengan ketebalan ini terlihat meyakinkan sebagai tali pancing, dan apakah membentangkan jarak penuh sekitar 2.6+ m terlihat wajar saat dirender. Ini adalah ENTRI TERAKHIR dalam file sumber. (tag: Fish AI, Rendering)

Status Aplikasi Saat Ini

Berdasarkan segelintir entri terakhir (ADR-072 sampai ADR-076, semuanya bertanggal 2026-07-21), berikut ringkasan status paling akhir yang diketahui.

Yang sudah selesai dan dikonfirmasi berjalan lewat screenshot langsung (berdasarkan beberapa putaran umpan balik nyata dari pengguna, bukan cuma typecheck): pipeline cutscene tangkapan sudah sepenuhnya dibangun dan sudah diiterasi berdasarkan tiga screenshot sungguhan dari aplikasi yang berjalan — ini pertama kalinya untuk area kerja ikan/cutscene proyek ini, yang sebelum titik ini hanya pernah diverifikasi lewat `swiftc -typecheck` dan tidak pernah benar-benar dilihat berjalan. Orientasi ikan (pose vertikal, kepala di atas, menyamping ke kamera hasil ADR-073) sudah dikonfirmasi benar di perangkat lewat screenshot. Overlay judul "CATCH!" sudah dikonfirmasi tampil dengan benar di perangkat. Posisi kail di mulut ikan sudah ditune berulang lewat dua screenshot (offset 1.0 dari ADR-075 digeser jadi 1.15) dan sudah "lebih baik" menurut screenshot ketiga, meski belum sepenuhnya dipastikan bersih. Klip animasi "Caught" baru yang lebih ganas sepanjang 90 frame sudah dibuat di repo aset Blender saudara (lewat proses serah-terima yang terdokumentasi) dan diintegrasikan, dengan puncak dramatisnya (frame 44, sekitar 1.83 detik) diturunkan secara matematis supaya jatuh tepat di dalam jeda slow-motion (ADR-071). Total durasi cutscene sudah ditune secara eksplisit menjadi tepat 5.0 detik waktu nyata (ADR-075), dengan memperhitungkan dengan benar bagaimana dilasi slow-motion mengalikan waktu cerita menjadi waktu wall-clock — ini penurunan rumus sungguhan (kalkulus/integrasi numerik), bukan tebakan, setelah percobaan "+1.5 detik" datar sebelumnya (ADR-074) salah memahami hubungan tersebut dan menghasilkan estimasi sekitar 7+ detik yang belum sempat diuji.

Yang baru dibangun tapi BELUM dikonfirmasi langsung: ADR-076 (entri terbaru, terakhir): tali cutscene khusus berbasis silinder yang menghubungkan kail ke ujung rod, ditambahkan karena kail sebelumnya mengambang tanpa apa pun yang menghubungkannya secara visual ke rod. Ini belum pernah dilihat hasil render-nya — belum ada screenshot untuk ini.

Masalah/isu yang diketahui dan masih ditandai terbuka pada akhir rentang ini: Perbaikan cast lewat BLE di ADR-063 (pengecilan format kabel `RodState` lewat `CodingKeys`) belum pernah dikonfirmasi tuntas secara penuh di perangkat pelapor aslinya — perbaikan ini menjawab persis jumlah byte yang tertangkap langsung, tapi tidak ada catatan konfirmasi akhir "sekarang sudah jalan" dalam rentang ini. Pemetaan spesies ikan per-tier masih keputusan konten/desain yang terbuka — setiap tangkapan menampilkan Mekong Giant Catfish apa pun tier-nya, termasuk untuk "scott", si ikan boss yang sudah diberi nama. Atribusi CC-BY untuk aset Mekong Giant Catfish (Sketchfab "Ikan Patin") sudah dicatat sebagai wajib tapi belum dipenuhi di mana pun dalam aplikasi yang dikirim — belum ada layar kredit yang dibuat (ada di cabang `feat/ui-slice` terpisah yang belum di-merge). Pola tabrakan penomoran ADR sekarang sudah terjadi TIGA kali dalam sejarah proyek ini (ADR-030/031, tabrakan ADR-044 sebelumnya, dan ADR-049-052 dalam rentang ini) — akar penyebabnya sama tiap kali (cabang-cabang paralel memberi nomor maju secara independen). Belum ada perbaikan struktural (misalnya proses reservasi nomor) yang diadopsi, hanya penomoran ulang setelah kejadian setiap kalinya. Banyak nilai rasa/penyetelan di sepanjang rentang ini (intensitas haptic, ambang batas arah fight, kecepatan reel, rumus jarak cast, ritme onboarding, ketebalan tali cutscene) masih tahap pertama/dipilih sendiri dan secara eksplisit ditandai belum terverifikasi menunggu playtest langsung lebih lanjut — meski rentang ini menunjukkan pergeseran nyata dari "diverifikasi hanya lewat typecheck" menuju penyetelan yang benar-benar iteratif berdasarkan screenshot langsung menjelang akhirnya.

Arah keseluruhan: rentang ini mendokumentasikan masa transisi proyek dari rezim verifikasi yang sepenuhnya hanya-typecheck (berlaku untuk hampir seluruh rentang ini) menuju putaran umpan balik visual nyata di perangkat yang pertama kalinya, baru muncul di segelintir entri terakhir (mulai ADR-072). Area fitur cutscene-tangkapan mengalami iterasi yang sangat cepat dan erat antara agen pengkodean di sisi CtSPoC dan agen terpisah di sisi Blender yang bekerja di repo aset saudara `new-blender-project`, dikoordinasikan lewat dokumen serah-terima tertulis, bukan lewat akses tool langsung — karena tidak ada tool Blender MCP yang bisa dijangkau dari sesi yang berakar di CtSPoC.