Bab 9: Sejarah Pembangunan Aplikasi — Bagian 1 (Awal Proyek)

Bagian ini menceritakan awal mula proyek aplikasi "Catch the Scott" (disingkat CtS/CtSPoC), sebuah Proof of Concept (PoC) kontroler spasial untuk game mancing. Ada dua aplikasi: Rod (pengendali di iPhone, memegang sensor gerak CoreMotion, Nearby Interaction, dan haptik Core Haptics) dan World (perender di macOS pakai RealityKit, memegang logika gameplay/simulasi/HUD). Keduanya terhubung lewat paket Swift bersama bernama Shared — lapisan kontrak yang platform-independen (hanya pakai Foundation), disinkronkan lewat MultipeerConnectivity. Bagian ini mencakup 30 entri log pertama, dari fondasi pipeline sampai awal sistem "fight" (perlawanan) ikan, termasuk keputusan arsitektur ADR-022 sampai ADR-030. Beberapa keputusan di sini kemudian di-supersede (digantikan) oleh keputusan berikutnya dalam rentang yang sama — semua kejadian itu ditandai jelas di bawah.

1. Pipeline PoC Pertama (2026-07-16)

(Arsitektur/Fondasi) Ini adalah PoC pertama yang bisa jalan ujung ke ujung: Rod membaca gerak + data spasial, mengirim RodState lewat MultipeerConnectivity ke World, dan World memakainya untuk menggerakkan visualisasi RealityKit. Mekanik gameplay/mancing sengaja belum dikerjakan di tahap ini — tujuannya baru membangun jalur kontroler-spasial (ponsel sebagai alat kendali joran) dulu, sebelum menambah logika permainan di atasnya.

Yang berubah: paket Shared mendapat SpatialState (jarak/arah dari Nearby Interaction), RodState (satu sumber kebenaran: timestamp, data spasial, pitch/roll/yaw), ConnectionState, SpatialStatus, TransportProtocol (connect/disconnect/send secara async), dan pesan jaringan NetworkMessage.discoveryToken(Data) serta .state(RodState). Paket Shared hanya boleh mengimpor Foundation, tidak boleh mengimpor framework khusus platform Apple.

Di sisi Rod: MotionManager membaca data gerak lewat CoreMotion pada frekuensi 30 Hz (pitch/roll/yaw). Siklus hidup Nearby Interaction: membuat sesi NISession, menerbitkan token discovery lokal, menerima token dari lawan bicara, menjalankan NINearbyPeerConfiguration, lalu mengubah jarak/arah jadi SpatialState. TransportClient (pakai MultipeerConnectivity) mencari layanan World, mengundang peer, melacak status koneksi, dan meng-encode NetworkMessage ke JSON. RodViewModel (jalan di MainActor) menggabungkan data gerak + data spasial jadi RodState lalu mengirimnya begitu transport siap. Ada UI debug yang menampilkan semua nilai mentah.

Di sisi World: ditemukan batasan platform — SDK macOS menandai NISession/NIDiscoveryToken/NINearbyPeerConfiguration sebagai tidak tersedia di macOS, jadi World tidak bisa menjalankan Nearby Interaction sendiri. Ini dicatat sebagai ADR-014 ("Nearby Interaction Ownership") — Rod yang sepenuhnya memegang Nearby Interaction, lalu mengirim hasil datanya lewat RodState. Akibatnya: World/World/Spatial/NearbyInteractionManager.swift dihapus, semua deskripsi izin penggunaan Nearby Interaction di World dihapus, dan spatialManager/rekonstruksi spasial dihapus dari WorldViewModel — sekarang World langsung memakai RodState yang diterima. File sensor Rod dipindah ke Rod/Rod/Sensors/; hanya Rod yang mengimpor modul NearbyInteraction. TransportHost (MultipeerConnectivity) mengiklankan diri, menerima undangan, dan men-decode NetworkMessage. WorldViewModel menggerakkan sebuah kubus RealityKit yang posisinya mengikuti arah/jarak dan orientasinya mengikuti pitch/roll/yaw.

Konfigurasi lain: deskripsi penggunaan Info.plist dibuat untuk gerak Rod + Nearby Interaction; World mendeklarasikan tidak memakai Nearby Interaction sama sekali. Blok #Preview bawaan SwiftUI dihapus karena tidak bisa jalan di lingkungan CLI yang di-sandbox.

Hasil: Shared/Rod/World lolos typecheck masing-masing; git diff --check lolos; pemeriksaan statis mengonfirmasi Shared tidak mengimpor framework platform apa pun. Belum selesai: build Xcode penuh lewat graph paket SwiftPM lokal (terhambat sandbox saat eksekusi manifest/cache), belum ada uji coba nyata iPhone-ke-Mac, belum ada validasi hardware Nearby Interaction. Langkah berikutnya: buka workspace di Mac pengembangan, build & jalankan kedua aplikasi, pastikan alur discovery/koneksi/gerak/state berjalan ujung ke ujung, dan tunda haptik sampai pipeline stabil.

Sub-catatan riwayat commit (1a): commit ea63d27 "Spatial Controller Foundation" adalah PoC awal di atas. Commit 0c6454b "Move All Sensing to Rod" adalah implementasi konkret dari batasan ADR-014: menghapus manager Nearby Interaction di World, mengubah alur World dari Transport -> spatial manager -> scene menjadi Transport -> RodState -> WorldViewModel -> scene, memindah sensor Rod ke Rod/Rod/Sensors/, dan menambah deklarasi platform iOS/macOS v26 ke Shared/Package.swift. Commit 11127d2 "Runtime Transport Diagnostics and Rod Asset" menambahkan file Info.plist eksplisit, aset World/World/Assets/Rod.usdz sebagai visual RealityKit utama (dengan kubus merah sebagai cadangan bila gagal dimuat), log diagnostik untuk init/browse/advertise/invite/status-peer transport, dan PeerInfo di Shared sebagai model identitas-peer untuk masa depan (belakangan dihapus sebagai kode mati, lihat entri §22 nanti). Baseline sebelumnya: 998b84d (target/workspace awal), 9d8f252 (file placeholder diganti nama, ditambah placeholder gerak/jaringan awal di Shared — ini juga posisi branch main), dan a3e3533 (dokumen arsitektur + nama skeleton awal). Status branch saat itu: dev berada di 11127d2 (sama dengan origin/dev); main masih di 9d8f252.

2. Vertical Slice Gameplay — Cast dan Reel

(Mekanik Cast/Fight) Ini loop permainan pertama yang bisa dimainkan, belum ada ikan/skor/tali/VFX/aset yang dipoles: Idle -> Casting -> Hook Flying -> Hook In Water -> Reeling -> Idle. Dasarnya adalah dokumen roadmap CtSPoC_Gameplay_Vertical_Slice_Roadmap.md — tujuannya membuktikan loop gameplay dulu sebelum menambah konten.

Yang berubah: Shared mendapat enum RodAction (idle/casting/ reeling); RodState mendapat field action + reelSpeed. Di Rod, MotionManager membaca userAcceleration.z (untuk cast) dan besar kecepatan rotasi (untuk reeling). RodActionDetector (belakangan dihapus, lihat entri §5 di bawah): percepatan Z positif > 1,25 memicu .casting, aktif selama 0,25 detik dengan cooldown 0,8 detik; besar rotasi > 1,4 memicu .reeling, dinormalisasi jadi RodState.reelSpeed (0 sampai 1). Di World, WorldViewModel membangun satu scene root: entity USDZ Rod (+ kubus merah cadangan), bola kuning kecil sebagai hook (mata kail), dan bidang air biru pada ketinggian tetap. Simulasi berjalan tetap di 60 Hz: saat .casting hook diluncurkan ke depan dengan integrasi kecepatan manual + gravitasi; saat kena bidang air, hook berhenti di fase hookInWater; saat .reeling hook bergerak ke arah joran sesuai kecepatan reel yang dikirim; setelah sampai di joran, hook disembunyikan dan kembali ke .idle. Yang sengaja belum dikerjakan: render tali, AI ikan/perlekatan ikan, skor/progres tangkapan, haptik/VFX.

Hasil: Shared/Rod/World lolos typecheck, git diff --check lolos. Belum ada catatan validasi runtime.

3. Audit Dokumentasi Arsitektur Casting

(Dokumentasi/Arsitektur) Ini tindak lanjut yang hanya berupa dokumentasi, sebagai persiapan sebelum implementasi fase "charging" (mengisi tenaga lemparan). AGENTS.md sekarang mewajibkan Rod/Shared memegang fase charging dan power-nya, World memegang presentasi HUD, dan pemisahan tegas antara transform spasial joran dengan nilai gameplay cast. Dicatat sebagai ADR-018 (keputusan: charging lewat gestur + rod-root diam di tempat, hanya hook yang bergerak saat cast) di ARCHITECTURE_DECISIONS.md. Verifikasi: hanya compile Shared yang ditarget + git diff --check (tidak ada kode yang berubah).

4. Refaktor Orientasi Joran

(Rendering/Mekanik Cast) Berdasar roadmap CtSPoC_Rod_Orientation_Refactor.md. World sekarang memisahkan transform pelacakan langsung dari transform aset visual statis:

Scene Root -> RodRoot (posisi/orientasi dari RodState) -> Rod.usdz (offset pitch/yaw/roll statis) -> Hook

WorldViewModel sekarang menyimpan referensi terpisah controllerEntity (RodRoot) dan rodModel (Rod.usdz); fungsi updateRodPose() hanya menerapkan pelacakan langsung ke RodRoot, sedangkan updateModelOrientation() hanya menerapkan koreksi aset ke Rod.usdz.

Alasannya: model Rod.usdz ter-import terbalik, jadi butuh koreksi pitch 180 derajat. Build DEBUG mendapat slider Pitch/Yaw/Roll langsung di RealityView supaya offset bisa disetel tanpa build ulang.

Catatan: updateModelOrientation() dan slider-nya belakangan dihapus sebagai kode mati (lihat entri §22) setelah offset-nya final dan tidak perlu disetel lagi secara live.

5. Haptik Gameplay (dan Perbaikan Bug)

(Mekanik Cast/Fight, Perbaikan Bug) Berdasar roadmap CtSPoC_Haptics_Design_Roadmap.md. Haptik berbasis event yang seluruhnya hidup di dalam Rod: Rod motion / hasil gameplay World -> FishingEvent -> FishingHaptics -> HapticManager -> Core Haptics. Ditambahkan kasus FishingEvent baru di Shared: cast, splash hook, tick reel, ikan menggigit, ikan kabur, tangkap, tali putus — semuanya dikirim lewat NetworkMessage.fishingEvent tanpa perlu menambah Core Haptics ke Shared atau World.

Yang berubah: Rod memainkan pulsa cast saat transisi ke .casting, tick reel intensitas-rendah berulang yang skalanya mengikuti kecepatan reel, dan pulsa splash ringan saat World melaporkan hook sudah sampai air. HapticManager juga sudah punya pola (belum dipakai di titik ini) untuk ikan menggigit/tertangkap/tali putus. World mengirim event hookSplash saat hook kena air — ini penggunaan pertama event-event tersebut secara nyata.

Hasil: World/Rod/Shared lolos typecheck, hanya Rod yang mengimpor CoreHaptics, git diff --check lolos.

6. Batasan Gerak Joran dan Casting

(Mekanik Cast/Fight) Berdasar desain CtSPoC_Rod_Motion_Constraints_and_Casting_Design.md. Diperkenalkan RodMotionInterpreter: CoreMotion -> RodMotionInterpreter -> RodState yang sudah dibatasi -> World.

Yang berubah: Orientasi rumah (home) — sampel gerak pertama jadi acuan rumah; UI debug Rod bisa mengkalibrasi ulang. Pitch/roll/yaw dihitung relatif terhadap acuan itu. Nilai roll rumah netral ditetapkan 2,0 radian supaya kontroler yang baru dikalibrasi langsung valid.

Batasan (constraints): pose virtual joran dijepit (clamp) ke rentang pitch -0,3 sampai 1,0, roll 1,0 sampai 3,0, yaw -2,5 sampai 2,5 radian sebelum dikirim ke World. Pose mentah (belum di-clamp) divalidasi lebih dulu — kalau pose perangkat di luar rentang, hasilnya .invalid (menonaktifkan cast/reel) tapi tetap mengirim pose visual yang sudah di-clamp. Orientasi dihaluskan dengan low-pass filter faktor 0,18. Siklus hidup lokal baru: invalid, ready, casting, waiting, reeling.

Perombakan casting: RodActionDetector beserta logika ambang-percepatan-nya (dari entri §2) dihapus. Casting sekarang butuh pose valid + delta pitch relatif ≥ 0,12 radian + cooldown yang sudah habis; aktif selama 0,25 detik, lalu cooldown 0,5 detik sebelum cast berikutnya. Reeling masih digerakkan oleh kecepatan rotasi tapi kini juga digerbang oleh validitas pose.

File yang terlibat: Shared/.../Gameplay/RodMotionInterpreter.swift (baru), MotionManager.swift (menambah timestamp), RodViewModel.swift (memegang interpreter), Rod/ContentView.swift (fase/validitas + UI kalibrasi ulang). File RodActionDetector.swift dihapus.

Hasil: Shared/Rod lolos typecheck; git diff --check lolos. Build SwiftPM penuh masih terhambat eksekusi manifest sandbox.

7. Sinkronisasi Fase Hook

(Mekanik Cast/Fight) Berdasar desain CtSPoC_Hook_Phase_Synchronization_Design.md. World menjadi satu-satunya pemegang otoritas siklus hidup hook, disinkronkan ke Rod: simulasi hook di World -> HookPhase -> transport -> gerbang casting di RodViewModel.

Yang berubah: ditambahkan Shared/.../Models/HookPhase.swift berisi idle/flying/hookInWater/reeling. NetworkMessage mendapat kasus baru .hookPhase(HookPhase). Enum HookPhase lokal lama di World dihapus. Semua perubahan siklus hidup di World kini disatukan lewat satu fungsi bantu setHookPhase(_:) (peluncuran, hook kena air, reeling, selesai — semua lewat fungsi ini). RodViewModel melacak hookPhase yang diterima dan menekan aksi .casting setiap kali fase yang disinkronkan bukan .idle. UI Rod menampilkan baik fase gerak lokal maupun fase hook yang disinkronkan.

Hasil: git diff --check lolos; pencarian grep mengonfirmasi tidak ada perubahan fase World secara langsung di luar setHookPhase(_:). Verifikasi runtime dua-perangkat masih tertunda.

8. Kalibrasi Gerak dan Pengenalan Gestur

(Mekanik Cast/Fight) Berdasar desain CtSPoC_Motion_Calibration_and_Gesture_Recognition_Design.md. Ditambahkan profil kalibrasi yang tersimpan (persisted) di antara data mentah CoreMotion dan validasi gestur: CoreMotion -> MotionProfile -> RodMotionInterpreter -> gerbang gameplay.

Yang berubah: tipe baru di Shared — MotionProfile, CastProfile, ReelProfile, LiftProfile — dengan nilai default konservatif untuk peluncuran pertama. RodMotionInput sekarang juga membawa data percepatan pengguna (user acceleration). Deteksi cast memakai kombinasi delta pitch terkalibrasi + percepatan maju + durasi terkalibrasi — menolak ayunan mundur/kurang kuat. Deteksi reel memakai nilai kecepatan-rotasi/kecepatan-maksimum dari profil. Tanda (sign) percepatan-maju yang terkalibrasi disimpan supaya gestur fisik yang sama tetap berfungsi terlepas dari arah sumbu Z perangkat, sementara ayunan arah-berlawanan tetap ditolak. Profil disimpan di UserDefaults Rod; kalibrasi ulang memperbaruinya. Rod sekarang memblokir semua output gestur sampai langkah kalibrasi netral/cast/reel selesai — layar kalibrasi jadi hal pertama yang ditampilkan.

9. Refaktor Sistem Gerak

(Mekanik Cast/Fight, Refaktor) Sistem dibangun ulang berbasis konsep pose terkalibrasi, bukan lagi ambang-batas delta.

Yang berubah: tipe murni baru MotionPose (hanya pitch/roll/yaw); RodMotionPose dipertahankan sebagai alias kompatibilitas. MotionProfile dibangun ulang berbasis pose terkalibrasi idlePose/castPose/reelPose (metadata dipersempit jadi hanya konteks percepatan-cast + konteks aktivitas-reel). RodMotionInterpreter ditulis ulang untuk mengklasifikasi orientasi saat ini berdasarkan pose kalibrasi terdekat, dikombinasikan dengan percepatan (untuk cast) / kecepatan rotasi (untuk reel) — tidak lagi membandingkan delta pitch mentah. Wizard kalibrasi dibangun ulang berbasis penangkapan pose: sampel Idle dirata-rata, sampel Cast stabil terakhir, sampel Reel dirata-rata; tidak ada aksi yang dikeluarkan sampai ketiganya tertangkap dan tersimpan. Output debug sekarang menjelaskan pose yang terpilih + jarak ke Idle/Cast/Reel, bukan lagi pengecekan ambang mentah. Acuan Idle sekarang dipasang pada saat langkah Idle ditangkap, sebelum langkah Cast/Reel dilanjutkan — ini mencegah joran "melompat" (snapping) ketika kalibrasi selesai (baik preview maupun RodState pasca-kalibrasi memakai kerangka relatif yang sama sejak titik itu). Gerbang fase-hook di RodViewModel di bagian hilir tidak berubah.

Hasil: git diff --check lolos; build SwiftPM/Xcode penuh masih terhambat lingkungan (error izin plugin macro Observation + SourcePackages); validasi perangkat masih tertunda.

10. Alur Kalibrasi Gerak untuk Penggunaan Pertama

(Mekanik Cast/Fight) Ini tindak lanjut dari sisi UX. Ditambahkan bagian "Motion Calibration" yang terlihat jelas di tampilan Rod, ditampilkan pertama kali, melaporkan langkah saat ini + jumlah sampel. Alur berurutan: tangkap netral -> lakukan+tangkap cast alami -> lakukan+tangkap reel alami. RodViewModel menurunkan nilai pitch/percepatan/durasi/ kecepatan-rotasi/kecepatan-maksimum yang dipersonalisasi dari sampel langsung selama langkah cast/reel, lalu menyimpannya. Tidak ada aksi cast/reel yang dikeluarkan sebelum profil lengkap; profil yang sudah ada memulihkan pose rumah saat aplikasi dibuka. Kalibrasi ulang mengembalikan profil ke langkah netral. Validasi maju tetap mempertahankan tanda percepatan terkalibrasi (menolak ayunan arah berlawanan).

Hasil: Shared lolos typecheck, git diff --check lolos. Inspeksi xcodebuild Rod terhambat pembatasan tulis SourcePackages; verifikasi perangkat masih tertunda.

11. Tinjauan Kepatuhan Arsitektur Kalibrasi

(Perbaikan Bug/Arsitektur) Dokumen tinjauan Calibration_Architecture_Refactor.md menemukan celah arsitektur nyata: pose Idle yang tersimpan sudah ada, tapi renderer masih merekonstruksi orientasi dari nilai Euler relatif, sehingga RodState jadi ambigu (field yang sama dipakai untuk gerak relatif-gameplay dan orientasi visual sekaligus).

Perbaikan: ditambahkan RodOrientation — tipe kuaternion Codable yang dinormalisasi di Shared (mendukung inversi, perkalian, konversi Euler di titik batas gerak). RodMotionInterpreter menyimpan profile.calibrationReference saat Idle ditangkap, lalu menerapkan inverse(reference) * current setiap frame; kuaternion yang sudah dikoreksi ini dikirim bersama data pose relatif-gameplay. RodState sekarang membawa orientation sebagai sumber kebenaran untuk renderer (field pitch/roll/yaw lama tetap ada hanya untuk kompatibilitas/debug). WorldViewModel sekarang hanya memakai RodState.orientation untuk rotasi — tidak ada lagi perhitungan offset kalibrasi, tidak ada pemeriksaan profil, tidak ada penyimpanan status kalibrasi di World. Urutan acuan-Idle-sebelum-Cast/Reel (dari entri §9) dipertahankan supaya preview dan runtime pasca-selesai memakai pipeline terkoreksi yang sama.

Hasil — kepatuhan dikonfirmasi: kalibrasi memegang penangkapan/koreksi acuan; gameplay boleh memakai pose relatif; World tidak tahu-menahu soal kalibrasi; renderer hanya memakai orientasi yang sudah terkoreksi. Compile tertarget + git diff --check lolos. Validasi perangkat penuh masih terhambat lingkungan (izin SourcePackages / plugin Observation).

12. Investigasi Pipeline Runtime

(Perbaikan Bug/Proses) Dokumen checkpoint "RealityKit Fishing Controller Checkpoint", 2026-07-17. Ini adalah validasi seluruh pipeline sebelum perubahan lebih lanjut: CMDeviceMotion -> MotionManager -> RodMotionInterpreter -> RodViewModel -> RodState -> Multipeer Transport -> WorldViewModel -> RealityKit.

Investigasi: log sementara ditambahkan di keempat titik batas. Pada percobaan pertama, hanya log World yang muncul dengan orientasi identitas (default) — kelihatannya seperti pipeline gerak Rod tidak jalan sama sekali. Kesimpulan itu dibuang: penyebab sebenarnya adalah instrumentasi yang terpasang di jalur eksekusi/build yang salah, bukan bug di sisi Rod. Setelah target run diperbaiki, log Rod muncul normal. Akar masalahnya bersifat lingkungan/prosedural, bukan matematika kuaternion, kalibrasi, transport, atau rendering.

Hasil: pipeline gerak, loop update World, transport, dan infrastruktur debug semua terkonfirmasi berjalan; tidak ditemukan regresi arsitektur.

Pelajaran teknis yang dicatat: debugging runtime harus memastikan eksekusi dulu sebelum mendiagnosis arsitektur atau matematika — urutannya: (1) pastikan kode benar-benar jalan, (2) pastikan log benar-benar tereksekusi, (3) pastikan nilai benar-benar berubah, (4) baru selidiki matematika/transport/rendering. Log sementara sifatnya hanya alat diagnosis, bukan dependensi. Prioritas berikutnya yang ditandai: orientasi Idle yang stabil, pose relatif-gameplay, klasifikasi Idle/Cast/Reel yang andal, dengan World tetap memakai orientasi secara independen.

13. Arsitektur Cast Resolver

(Mekanik Cast/Fight) Desain "Cast Resolver Architecture", 2026-07-17. Ini memisahkan interpretasi gerak fisik dari penerjemahan ke gameplay: MotionManager -> RodMotionInterpreter -> CastSnapshot -> RodState -> CastResolver -> CastSolution -> fisika hook di World.

Yang berubah: tipe Shared baru CastVector3, CastSnapshot, CastConfig, CastSolution, CastResolver. Sampel gerak diperluas dengan komponen kecepatan-sudut supaya satu cast snapshot menangkap keadaan pelepasan (release) secara lengkap, bukan cuma besarannya saja. RodMotionInterpreter mengeluarkan satu CastSnapshot yang tidak bisa diubah (immutable) saat masuk fase casting (masih hanya memegang interpretasi/kalibrasi, bukan penyeimbangan gameplay). RodState mengangkut snapshot opsional ini. CastResolver menurunkan arah horizontal dari orientasi terkoreksi, memproyeksikan keluar arah bidik vertikal, menormalisasi kecepatan sudut jadi power (0 sampai 1), dan menghitung sudut peluncuran dari CastConfig. World menyelesaikan (resolve) snapshot sekali saja dan membangun kecepatan awal hook dari CastSolutionWorld tidak lagi membaca CoreMotion atau kecepatan-sudut mentah secara langsung.

Hasil: compile Shared untuk cast/motion lolos; git diff --check lolos. Penyetelan lintasan (trajectory) dua-perangkat secara runtime masih tertunda.

14. Integrasi Cast Resolver dan Penggerbangan Status Hook

(Mekanik Cast/Fight) Dokumen sprint "Pending Sprint Instructions - Cast Resolver Integration & Hook State Gating", 2026-07-17.

Jembatan Cast Resolver: RodViewModel (bukan interpreter) sekarang yang memegang pemanggilan CastResolver, mengubah snapshot jadi CastSolution sebelum menerbitkan RodState. RodState membawa CastSolution sekali-pakai (one-shot) dan mengisi directionX/Y/Z + distance ternormalisasi untuk UI Controller State yang sudah ada. World hanya memakai solusi ini — tidak ada resolusi snapshot, tidak ada pembacaan gerak mentah.

Penggerbangan Status Hook: ditambahkan HookPhase.allowsCasting / .allowsReeling sebagai aturan izin terpusat tunggal. Casting hanya diizinkan saat .idle; reeling hanya saat .hookInWater atau .reeling. RodViewModel memakai aturan ini untuk menekan aksi yang tidak valid (misalnya tidak bisa melaporkan .reeling saat fase masih .idle). Otoritas siklus hidup hook di World tidak berubah.

Hasil: kontrak sprint Shared lolos compile; git diff --check lolos. Verifikasi runtime dua-perangkat masih tertunda.

15. Meteran Cast Debug dan Cast Solution yang Persisten

(Mekanik Cast/Fight, UI) "Feature Proposal - Debug Cast Meter & Persistent Cast Solution", 2026-07-17.

Yang berubah: CastMeterConfig dengan zona pendek/menengah/jauh yang bisa dikonfigurasi — nilai default 6 m / 12 m / 18 m. RodViewModel memetakan power cast ternormalisasi hasil resolve jadi jarak meteran yang deterministik (bukan fisika mentah). Ditambahkan Rod Cast Meter yang terlihat (zona hijau/kuning/merah + indikator power). CastSolution terbaru disimpan di RodViewModel dan terus diterbitkan ulang dalam RodState, jadi arah/jarak tetap terlihat setelah event cast satu-frame selesai; hanya di-reset saat ada cast baru atau saat World melaporkan hook kembali ke .idle. RodState.distance sekarang memakai jarak dari resolver/meteran, dengan jarak spasial sebagai cadangan kalau tidak ada cast solution yang aktif.

Hasil: Shared lolos compile; git diff --check lolos. Persistensi UI runtime + rasa lintasan/meteran dua-perangkat masih tertunda. (Catatan: meteran di sisi Rod ini belakangan dihapus total dan dibangun ulang sebagai HUD milik World — lihat entri §19.)

16. Tindak Lanjut Arsitektur Casting — Charging via Gestur, HUD di World, Pemisahan Transform Rod

(Mekanik Cast/Fight, UI) Dokumen desain tertanggal 2026-07-18, mengimplementasikan kontrak charging ADR-018 dari entri §3.

Charging via Gestur: fase gerak baru backswing, charging, forwardSwing, release, followThrough. RodMotionInterpreter melaporkan power cast ternormalisasi secara langsung selama gestur berlangsung dan membekukan power release selama fase FollowThrough. RodState mengangkut motionPhase + castPower supaya World bisa menganimasikan HUD langsung tanpa menyentuh CoreMotion. CastSolution tetap hanya dibuat saat release — tetap jadi status cast gameplay yang beku (frozen).

HUD di World: Cast Meter sekarang memakai RodState.motionPhase/ castPower/castSolution; terlihat saat charging/release, menampilkan nilai solusi yang beku selama release/follow-through. Rod tetap bebas dari presentasi HUD gameplay.

Pemisahan Transform Rod: RodState.directionX/Y/Z/distance sekarang murni khusus spasial. World memosisikan joran dari status spasial; fisika hook hanya memakai CastSolution (arah/power/jarak/sudut peluncuran). Joran dan hook tetap entity yang terpisah.

Hasil: Shared lolos compile; git diff --check lolos. Validasi penuh dua-perangkat untuk HUD-charging + joran-diam masih menunggu hardware.

17. Pemulihan Memori Sesi

(Dokumentasi/Proses) File SESSION_MEMORY.md (yang sempat hilang dari working tree) dipulihkan — sekarang mencakup struktur proyek lengkap, tech stack, fase-fase charging via gestur, pipeline kalibrasi yang sudah dikoreksi, kepemilikan CastSolution, kepemilikan HUD World, invarian joran-diam, aturan dokumentasi, dan perintah verifikasi tertarget.

18. Memori Sesi Terminal

(Dokumentasi/Proses) Ditambahkan SESSION_MEMORY.md (dokumen serah terima ringkas untuk sesi AI berbasis terminal): batasan proyek, kontrak sumber-kebenaran, alur kalibrasi/CastResolver, aturan HookPhase, kepemilikan UI, perintah verifikasi, status fitur saat ini, titik masuk dokumen. Juga mendokumentasikan tanggung jawab tech-stack: SwiftUI, RealityKit, CoreMotion, Nearby Interaction, MultipeerConnectivity, Core Haptics, paket Shared. Alur kerja sesi sekarang mewajibkan setiap tugas memperbarui dokumen arsitektur/desain terkait DAN menambahkan entri baru bergaya commit-git ke CHECKPOINT.md (cakupan, perubahan file/alur, verifikasi, keterbatasan yang tersisa) — riwayat checkpoint lama harus tetap utuh (inilah alasan kenapa file ini tidak pernah menghapus entri lama, hanya menandainya sebagai di-supersede/digantikan).

19. Refaktor Arsitektur Cast Meter — Satu Sumber Kebenaran & HUD di World

(UI/Mekanik Cast) "Cast Meter Architecture Refactor - Single Source of Truth & World HUD", 2026-07-17.

Yang berubah: presentasi Cast Meter serta status power/jarak cast dihapus total dari UI Rod. CastSolution di dalam RodState sekarang jadi satu-satunya status cast yang otoritatif — Rod tidak lagi menyimpan duplikat castPower/castDistance/solusi terbaru. Ditambahkan World/World/Views/CastMeterView.swift — World sekarang memegang meteran hijau/kuning/merah, hanya membaca viewModel.rodState.castSolution (nol perhitungan gameplay, murni tampilan). Peran Rod menyempit jadi hanya: sensing/kalibrasi/pengenalan gestur/pemanggilan CastResolver/transport; World memegang presentasi gameplay. Aturan persistensi dari entri §15 tidak berubah.

Hasil: pencarian grep mengonfirmasi Rod tidak lagi punya referensi CastMeterView/castPower/castDistance/solusi duplikat. Shared lolos compile; git diff --check lolos.

20. Pembaruan Dokumentasi Arsitektur Gameplay V1

(Dokumentasi/Produk) Hanya dokumentasi, sumber "Catch the Scott - Gameplay Architecture V1", 2026-07-18. Tidak ada perubahan Swift/Xcode/aset.

Yang berubah: ditambahkan GAMEPLAY_ARCHITECTURE_V1.md sebagai sumber kebenaran produk/gameplay. Prinsip inti yang dibakukan: Rod adalah perangkat input, World adalah permainannya. World memegang menu, tutorial, panduan kalibrasi, HUD, simulasi hook/ikan, tegangan tali, hasil, dan progres. Permukaan minimal Rod di V1: gerak, kalibrasi, niat cast/reel, tombol, status koneksi, output haptik. Loop gameplay penuh terdokumentasi (koneksi -> tangkap/kabur -> hasil -> lanjut). Kontrak didefinisikan untuk ketertarikan-ikan, gigitan, fight, tegangan-tali, ritme-haptik, pemilihan spesies ikan, progres perlengkapan. Dokumen AGENTS/ADR/software-architecture/ spatial-architecture/session-memory semuanya diperbarui merujuk ke V1.

Hasil: dikonfirmasi diff hanya berisi file Markdown; git diff --check lolos. Compile kode sengaja dilewati (tugas ini murni dokumentasi).

21. Refaktor Arsitektur Controller dan Hook di World (Perbaikan Bug)

(Perbaikan Bug/Mekanik Cast) "World Controller & Hook Architecture Refactor", 2026-07-18. Dicatat sebagai ADR-019.

Bug yang ditemukan: WorldViewModel.updateRodPose() memosisikan RodRoot memakai RodState.direction * distance — artinya kontroler (joran) itu sendiri ikut bergerak seperti proyektil cast, yang jelas salah; seharusnya jorannya diam, hanya hook yang terbang.

Perbaikan: RodRoot sekarang tetap di offset relatif-kamera yang tetap; updateRodPose() hanya menerapkan RodState.orientation. Ketergantungan kontroler pada arah/jarak cast dihapus total. Pemakaian CastSolution dibatasi hanya untuk perhitungan kecepatan-peluncuran hook. Pergerakan hook-saja tetap dipertahankan untuk terbang/splash/reeling/reset. Perhitungan titik-asal-joran yang sekarang tidak dipakai dihapus dari peluncuran hook.

Hasil: git diff --check + compile Shared tertarget lolos. Validasi runtime perangkat penuh masih tertunda (resolusi paket/simulator tidak tersedia di lingkungan ini).

22. Bersih-bersih Kesalahan Dokumentasi dan Kode Mati (2026-07-18)

(Build/Tooling, Perbaikan Bug) Ini tinjauan seluruh repo + pembersihan. Konten di bawah folder Architecture / diperlakukan sebagai log pengembangan historis (tidak wajib mengikuti status saat ini) — hanya kesalahan penomoran nyata yang diperbaiki di sana; sisa pekerjaan menghapus jalur kode mati yang tidak lagi dipanggil siapa pun.

Dokumentasi: duplikat "ADR-014 --- Debug First" diberi nomor ulang jadi ADR-021 (karena bentrok dengan ADR-014 yang asli, Nearby Interaction Ownership, dari entri §1). Tidak ada konten lain di ARCHITECTURE_DECISIONS.md / SOFTWARE_ARCHITECTURE_DOCUMENT.md / SPATIAL_CONTROLLER_DOCUMENT.md yang disentuh — perbedaannya dengan bentuk RodState/tata letak folder saat ini diterima sebagai catatan historis.

Kode mati yang dihapus: Shared/.../Models/PeerInfo.swift dan MotionPacket.swift (tidak ada referensi tersisa di mana pun — PeerInfo ditambahkan di commit 11127d2 pada entri §1a sebagai scaffolding masa depan, tidak pernah dipakai); Shared/.../Debug/MotionRecording.swift (hanya komentar header, tanpa implementasi); kasus NetworkMessage.hello / .motion (tidak pernah dibuat di mana pun; RodViewModel.handle(_:) sekarang hanya mencocokkan kasus yang belum ditangani, yaitu .state); WorldViewModel.loadRodEntity() (jalur alternatif memuat Rod.usdz yang sudah digantikan makeArrow()/makeSceneEntity(), tidak pernah dipanggil); WorldViewModel.modelPitch/modelYaw/modelRoll + updateModelOrientation() (dari refaktor orientasi di entri §4) + helper #if DEBUG orientationSlider(_:value:) di World/ContentView.swift (tidak ada satu pun yang lagi terjangkau dari body view saat ini — slider penyetelan tidak lagi dibutuhkan setelah offset 180° final). Diagnostik print() pengembangan yang tersisa digerbang di balik #if DEBUG di TransportClient.swift, TransportHost.swift, RodViewModel.swift. Implementasi alternatif didNotStartAdvertisingPeer yang di-comment-out dan basi dihapus dari TransportHost.swift.

Sengaja dibiarkan (sesuai arahan): MotionDebugView yang di-comment-out di Rod/ContentView.swift dan bagian CastMeterView yang di-comment-out di World/ContentView.swift — keduanya sudah terimplementasi penuh, sengaja dinonaktifkan bukan dihapus. (Preferensi "jangan hapus, nonaktifkan saja" ini dipakai lagi persis sama di entri §29 untuk baris-baris debug di Rod.)

Hasil: compile Shared tertarget (mencakup seluruh set model termasuk NetworkMessage) lolos bersih. git diff --check lolos. Grep mengonfirmasi tidak ada referensi tersisa ke simbol mana pun yang dihapus. Build Xcode/perangkat penuh masih terhambat lingkungan, sama seperti setiap entri sebelumnya.

23. Gameplay V1 - Kemunculan Ikan (Fish A) — belakangan di-revert, lihat entri §24

(AI Ikan) (2026-07-18) Ini adalah irisan sistem-ikan pertama: satu entity ikan yang bisa muncul (spawn). ADR-022.

Yang berubah: World/World/Gameplay/FishManager.swift — manager @MainActor yang memegang satu ModelEntity bernama "Fish A": spawn/despawn/berenang idle. Hanya di World, Rod/Shared tidak disentuh. Badan ikan berupa mesh bola (generateSphere) yang diskalakan tidak seragam [1, 0.6, 2.2] untuk meniru bentuk ikan memanjang (SDK RealityKit di sini tidak punya primitif kapsul, dan belum ada aset ikan sungguhan) — memakai konvensi mesh-primitif yang sama dengan panah/hook joran. WorldViewModel mendapat sceneRoot + satu instance FishManager. setHookPhase(_:) memunculkan ikan saat transisi .flying -> .hookInWater, dan menghilangkannya saat kembali ke .idle. tick(deltaTime:) memperbarui ikan tiap frame (tidak melakukan apa-apa kalau tidak ada yang muncul). Ikan berenang dalam loop idle melingkar lambat mengelilingi titik acak, menghadap arah geraknya. Belum ada data pencarian/ketertarikan/gigitan/fight/spesies.

Hasil: typecheck mandiri + compile Shared lolos; git diff --check lolos; WorldViewModel.swift tetap di 289 baris (di bawah pedoman 300-baris untuk ViewModel di AGENTS.md). Build perangkat penuh/verifikasi visual di dalam scene tidak tercatat pernah dijalankan.

⚠️ Digantikan (superseded) di hari yang sama — lihat entri §24: ikan visual dihapus dan ADR-022 secara eksplisit ditandai sebagai digantikan, demi membuat HookPhase lebih dulu mencerminkan status nyata ikan-menggigit sebelum ada render apa pun.

24. Gameplay V1 - Fase Hook Menangkap Ikan (Ikan Visual di-Revert)

(AI Ikan, Perbaikan Bug) (2026-07-18) Ini perubahan arah, di hari yang sama dengan entri §23: untuk sementara tidak ada ikan visual di World — prioritasnya membuat HookPhase mencerminkan status permainan nyata (ikan tertangkap di hook) sebelum ada rendering apa pun. ADR-023, yang menandai ADR-022 sebagai Digantikan (Superseded) (bukan dihapus).

Yang berubah: FishManager.swift beserta seluruh direktorinya dihapus — tidak ada lagi entity/mesh/visual ikan apa pun di World. Pengait sceneRoot/fishManager di WorldViewModel di-revert. Ditambahkan HookPhase.fishOnHook (di antara hookInWater dan reeling); allowsReeling sekarang mencakup fase ini juga. WorldViewModel melacak keterikatan ikan sebagai status simulasi privat (biteTimer: Float, hasFishOnHook: Bool), bukan dimasukkan ke dalam HookPhase itu sendiri:

  • updateBiteTimer(deltaTime:) berjalan tiap tick selama .hookInWater; saat waktunya habis, mengeset hasFishOnHook = true, berpindah ke .fishOnHook, mengirim FishingEvent.fishBite.
  • tick(deltaTime:) mendapat kasus .fishOnHook (reeling seperti .hookInWater); .reeling sekarang kembali ke .fishOnHook (bukan otomatis ke .hookInWater) kalau hasFishOnHook bernilai true.
  • simulateFlyingHook() mereset hasFishOnHook=false dan mengocok ulang biteTimer = Float.random(in: 1.5...4.0) setiap kali splash — peluang gigitan acak independen tiap lemparan.
  • reelHook(deltaTime:) mengirim FishingEvent.catchFish hanya kalau hook sampai ke joran dengan hasFishOnHook == true; tarikan kosong tidak mengirim apa-apa.
  • .fishBite/.catchFish sudah didefinisikan sebelumnya dan sudah ditangani oleh FishingHaptics.swift milik Rod (dari entri §5), tapi ini adalah pemicu nyata pertama kalinya dari World.
  • Yang sengaja belum diimplementasikan: ketertarikan-ikan sebagai hal terpisah dari gigitan, tegangan tali, kabur (fishEscaped/lineBreak masih belum pernah dikirim), data spesies, ikan visual apa pun. Gigitan selalu berhasil kalau di-reel.

Hasil: compile Shared yang diperluas (menambah FishingEvent, NetworkMessage, ConnectionState, SpatialStatus, Transport) lolos bersih. Grep mengonfirmasi switch hookPhase di WorldViewModel adalah satu-satunya switch HookPhase yang exhaustive (mencakup semua kasus) di seluruh repo dan sudah menangani .fishOnHook. git diff --check lolos. Build perangkat penuh/verifikasi runtime di-scene/haptik belum tercatat pernah diamati.

25. Gameplay V1 - Haptik Kabur/Tali-Putus dan Kecepatan Reel Dinamis

(AI Ikan, Mekanik Cast/Fight, Perbaikan Bug) (2026-07-18) Konfirmasi kerja sebelumnya di perangkat nyata: alur hook/gigitan/reel dari entri §24 tervalidasi ujung ke ujung di hardware — ini entri pertama dalam rentang ini dengan konfirmasi nyata di perangkat. Ada dua tindak lanjut: (1) haptik fishEscaped/lineBreak sudah ada tapi tidak pernah dipicu apa pun; (2) RodState.reelSpeed terbaca konstan 1,000 atau 0,000 saja, bukannya berskala mengikuti kecepatan rotasi sungguhan. ADR-024 dan ADR-025.

Ikan Kabur / Tali Putus (ADR-024) — ⚠️ belakangan digantikan oleh ADR-028, lihat entri §28:

  • WorldViewModel mendapat biteExpireTimer, lineStrainTimer, konstanta biteExpireDuration (2,5 detik), lineBreakReelThreshold (0,9 kecepatan reel), lineBreakSustainDuration (1,2 detik).
  • updateBiteExpiry(deltaTime:) berjalan selama .fishOnHook saat tidak sedang reeling; ketika habis waktu, menghapus hasFishOnHook, mengembalikan fase ke .hookInWater dengan biteTimer baru, mengirim FishingEvent.fishEscaped.
  • updateLineStrain(deltaTime:) berjalan selama .reeling saat ada ikan terkait; terakumulasi hanya selagi reelSpeed >= 0,9, direset kalau tidak; bertahan selama 1,2 detik akan menonaktifkan hook, menghapus ikan, mengembalikan fase ke .idle, mengirim FishingEvent.lineBreak.
  • Menjeda reeling di tengah fight sekarang mereset biteExpireTimer sebelum kembali ke .fishOnHook (supaya jam kabur tidak diam-diam berkurang lewat batas jeda-reel/lanjut).
  • simulateFlyingHook() mereset kedua timer baru ini setiap kali splash.
  • HapticManager mendapat fishEscaped() — pola tiga-pulsa yang memudar, berbeda dari lineBreak()'s pulsa tunggal tajam (sebelumnya .fishEscaped salah di-alias ke haptik lineBreak di FishingHaptics.handle(_:) — sekarang tiap kasus dipetakan ke metodenya sendiri).

Kecepatan Reel Dinamis (ADR-025):

  • Akar masalah ditemukan: kasus .reel menghitung speed = min(1, rotationRateMagnitude / max(profile.reelRotationRate, 1)). Karena ambang reelRotationRate yang terkalibrasi hampir selalu di bawah 1,0, max(_, 1) membuat penyebutnya rata di sekitar 1, sementara reeling sungguhan gampang melebihi 1 rad/detik — sehingga speed langsung jenuh (saturasi) ke 1,0 hampir seketika melewati ambang deteksi.
  • Ditambahkan reelFullRotationRate: Double ke MotionProfile berdampingan dengan reelRotationRate yang sudah ada.
  • Rumus baru: speed = clamp((rotationRateMagnitude - reelRotationRate) / (reelFullRotationRate - reelRotationRate), 0...1) — kurva naik yang kontinu, bukan lagi rasio yang jenuh.
  • Langkah kalibrasi sekarang juga merekam peakRate (nilai maksimum, bukan rata-rata, dari kecepatan rotasi selama sampel reel yang diperagakan) dan mengeset reelFullRotationRate = max(reelRotationRate + 0,3, peakRate) — puncak yang diperagakan pemain sendiri jadi acuan nilai 1,0.
  • Penyimpanan MotionProfile lewat UserDefaults sudah memakai try? di sekitar decode, jadi profil format-lama yang tersimpan aman jatuh kembali ke kebutuhan kalibrasi ulang, bukan crash.

Hasil: compile Shared (set yang diperluas) lolos. Shared dibangun sebagai modul simulator-iOS dan typecheck FishingHaptics.swift, HapticManager.swift, RodViewModel.swift lengkap + MotionManager.swift/ NearbyInteractionManager.swift/TransportClient.swift — semua lolos. git diff --check lolos. Build perangkat penuh masih terhambat lingkungan; alur kabur/tali-putus, kedua pola haptik baru, dan rasa kecepatan-reel yang baru belum diamati di hardware.

26. Gameplay V1 - Model FishSpecies, Tegangan Tali Kontinu, HUD Tangkapan

(AI Ikan, Mekanik Cast/Fight, UI) (2026-07-18) Alasan: tindak lanjut setelah entri §25 dikonfirmasi — ingin (1) bar tegangan 0-sampai-1 yang sungguhan, bukan timer ambang-tersustain yang biner; (2) HUD World untuk sukses-tangkap/jumlah-berjalan; (3) data ikan dipisah jadi model spesiesnya sendiri untuk pertukaran-aset di masa depan. ADR-026.

Model Data FishSpecies: ditambahkan Shared/.../Models/FishSpecies.swift — struct Codable/Sendable/Identifiable (id, name, weightRange, lengthRange, resistance: Double 0 sampai 1, value: Int), plus FishSpeciesRange (pasangan min/max Codable, karena ClosedRange tidak bisa otomatis Codable). Tidak ada referensi mesh/aset — Shared tetap hanya Foundation. Ditambahkan World/.../Gameplay/FishCatalog.swift: daftar konten milik World (static let all, saat ini satu entri, "fish_a" / "Fish A") + randomSpecies(). Menambah spesies = tinggal menambahkan satu entri data.

FishFightController diekstrak dari WorldViewModel: ObservableObject @MainActor baru (World/.../Gameplay/FishFightController.swift) yang memegang seluruh status fight: hasFishOnHook, lineTension: Float (@Published, 0 sampai 1), caughtCount, lastCatchResult: FishCatchResult?, timer gigitan/kabur, spesies/berat/panjang yang sedang terkait. WorldViewModel.tick(deltaTime:) sekarang mendelegasikan ke controller ini (updateSearching, updateWaiting, pauseFight, updateFighting(deltaTime:reelSpeed:), resolveCatch, resetForNewCast) daripada mengelola timer secara langsung. lineTension menggantikan mekanik ambang-tersustain biner dari ADR-024 dengan nilai kontinu: naik selagi reeling di atas kecepatan-reel-aman per-spesies (effectiveSafeReelSpeed) pada laju per-spesies (effectiveTensionRiseRate), meluruh (decay) kalau tidak, tali putus di nilai 1,0. resistance spesies juga menskalakan masa tenggang reaksi-gigitan (effectiveBiteExpireDuration) — ikan yang lebih kuat memberi waktu reaksi lebih singkat dan lebih cepat memutus tali di bawah tekanan. Ditambahkan baru World/.../Gameplay/FishCatchResult.swift: struct non-Codable khusus World (speciesName, weight, length, outcome: .caught/.escaped/.lineBreak) untuk ditampilkan di HUD.

HUD di World: ditambahkan baru World/.../Views/LineTensionView.swift — bar 0-sampai-1 hijau/kuning/merah terikat ke fishFight.lineTension, terlihat saat hasFishOnHook, label "SNAPPING" muncul di atas 0,8. ContentView.swift sekarang membangun WorldViewModel lewat init() eksplisit dan mengamati viewModel.fishFight secara terpisah sebagai @ObservedObject-nya sendiri. Ditambahkan GroupBox "Catch Results" (jumlah tangkapan + outcome terakhir: spesies, berat dalam kg, panjang dalam cm, label tertangkap/kabur/tali-putus).

Hasil: Shared dibangun ulang sebagai modul macOS maupun simulator-iOS (ditemukan lewat perintah find, bukan path hardcoded, karena ada reorganisasi tak terkait yang berjalan bersamaan di Shared/Sources/Shared/Models/ menjadi subfolder Device/, Gameplay/, Rod/, Spatial/, di luar sesi ini — dibiarkan apa adanya karena SwiftPM tidak peduli tata letak subfolder, dan FishSpecies.swift ditambahkan langsung ke folder Models/ yang datar, bukan ke dalam subfolder mana pun). Typecheck sisi-macOS dan sisi-iOS penuh lolos (satu peringatan tak terkait yang sudah ada sebelumnya: binding controllerEntity yang tidak dipakai di launchHook(solution:) — peringatan yang sama ini muncul lagi di beberapa entri berikutnya, selalu dicatat sebagai sudah-ada-sebelumnya/tak-terkait). git diff --check lolos. Bar tegangan/HUD tangkapan di layar belum tercatat pernah diamati berjalan.

27. Gameplay V1 - Tegangan Tali Mengikuti Durasi Fight, Bukan Kecepatan Reel

(AI Ikan, Perbaikan Bug) (2026-07-18) Alasan: permintaan perubahan atas meteran tegangan entri §26 — disinkronkan ke waktu reel, bukan kecepatan reel, jadi reeling terlalu lama otomatis memaksimalkan meteran dan memutus tali terlepas dari cepat-lambatnya. ADR-027, secara eksplisit hanya mengubah rumus tegangan ADR-026.

Yang berubah: FishFightController.updateFighting(deltaTime:reelSpeed:) berubah jadi updateFighting(deltaTime:) — parameter reelSpeed dan perbandingan kecepatan-aman/decay dihapus. lineTension += deltaTime / effectiveMaxFightDuration, dibatasi maksimum 1; mencapai 1 tetap memutus tali. baseSafeReelSpeed/ baseTensionRiseRate/tensionDecayRate beserta variabel efektif turunannya dihapus. Ditambahkan baseMaxFightDuration: Float = 6,0 dan effectiveMaxFightDuration = max(2,0, baseMaxFightDuration * (1 - resistance * 0,5)) — anggaran total waktu-fight per-spesies menggantikan ide kecepatan-reel-aman per-spesies. Tegangan sekarang tidak punya cabang decay — hanya berubah selama benar-benar sedang fight (.reeling + pemain sedang reeling), jadi menjeda malah membekukan (bukan mengurangi) nilainya. Titik pemanggilan di WorldViewModel diperbarui (argumen reelSpeed: dibuang). Tidak perlu perubahan HUD.

Hasil: daftar file typecheck macOS sama seperti entri §26, lolos bersih (peringatan sama yang sudah ada sebelumnya). git diff --check lolos. Rasa sesungguhnya di perangkat untuk jendela 6 detik tetap (berskala spesies) ini belum diamati.

⚠️ Digantikan (superseded) — lihat entri §28, di hari yang sama: pengujian di perangkat menemukan desain ini cacat (tegangan membeku saat idle, bukannya turun; dan timer kabur yang datar dan tidak terhubung ke apa pun), dan baik ADR-024 maupun ADR-027 digantikan oleh satu mekanik terpadu.

28. Gameplay V1 - Zona Aman Tegangan Terpadu, Ikan Melawan Secara Fisik

(AI Ikan, Perbaikan Bug, UI) (2026-07-18) Alasan: pengujian di perangkat atas entri §27 memunculkan dua masalah nyata: (1) bar tegangan tidak pernah berubah selagi joran diam (idle), (2) ikan kabur lewat timer datar ~2,5 detik yang tidak terhubung ke apa pun yang bisa dilihat pemain (bukan "ikan menarik hook menjauh" seperti yang dimaksudkan). Membaca ulang bagian "Line Tension" di GAMEPLAY_ARCHITECTURE_V1.md mengonfirmasi mekanik V1 yang dimaksudkan sejak awal sudah pernah ditentukan: satu bar tegangan dengan zona aman — tegangan terlalu rendah ATAU terlalu tinggi sama-sama membuat ikan lepas. Entri ini menggantikan dua mekanisme terpisah yang dibangun lewat ADR-023/024/026/027 dengan mekanik tunggal itu. ADR-028, menandai ADR-024 dan ADR-027 sebagai Digantikan (Superseded).

Penulisan ulang FishFightController: updateWaiting, pauseFight, biteExpireTimer, baseBiteExpireDuration/ effectiveBiteExpireDuration, updateFighting(deltaTime:), baseMaxFightDuration/effectiveMaxFightDuration — semuanya dihapus, digantikan satu metode: updateFight(deltaTime:isReeling:reelSpeed:) -> FightFailure?, dipanggil tiap tick selagi hasFishOnHook (mencakup baik .fishOnHook maupun .reeling):

  • reelPull = isReeling ? reelSpeed * baseReelTensionRate(0,5) : 0
  • lineTension += (reelPull - effectiveFishPullRate) * deltaTime, dibatasi 0 sampai 1
  • effectiveFishPullRate = baseFishPullRate(0,15) * (1 + resistance)
  • lineTension <= 0 -> .escaped; >= 1 -> .lineBreak.

Enum FightFailure baru (.escaped/.lineBreak) jadi tipe kembalian (return type). updateSearching sekarang mengeset lineTension = initialTension (0,5) saat ada gigitan, bukan 0 — memberi pemain ruang bereaksi ke dua arah sejak awal. Ditambahkan fishPullSpeed: Float (kecepatan m/detik berskala spesies, 0 kalau tidak ada yang terkait) — satu-satunya permukaan baru yang diekspos ke WorldViewModel; controller tetap tidak tahu apa-apa soal RealityKit/entity/posisi.

WorldViewModel: ikan menyeret hook secara fisik: kasus tick .fishOnHook/.reeling digabung jadi satu (case .fishOnHook, .reeling:) karena keduanya sekarang menjalankan logika reel-atau-hanyut-plus-tegangan yang sama. Ditambahkan driftHookAway(deltaTime:): selagi ada ikan terkait dan tidak sedang di-reel, hook bergerak menjauh dari joran sepanjang sumbu joran-ke-hook yang sudah ada (versi negasi dari vektor yang sudah dihitung reelHook), diskalakan oleh fishFight.fishPullSpeed — jadi ikan yang terkait tapi diabaikan sekarang terlihat berenang menjauh membawa hook, dan reeling terlihat menariknya kembali. Ditambahkan resolveFightFailure(_:): .escaped mengembalikan fase ke .hookInWater (tetap memancing) + mengirim .fishEscaped; .lineBreak menonaktifkan hook, mengembalikan fase ke .idle (perlu cast ulang) + mengirim .lineBreak — bentuk event sama seperti alur tangkap yang sudah ada.

HUD: LineTensionView sekarang mewarnai zona aman yang terlihat (0,3 sampai 0,7, bernuansa hijau), bukan lagi ramp satu-arah hijau-ke-merah. Di bawah 0,15 menampilkan "LOSING FISH"; di atas 0,85 tetap menampilkan "SNAPPING". Ditambahkan label ujung "Slack"/"Snap" (mengikuti konvensi "Near"/"Far" milik CastMeterView).

Hasil: daftar file typecheck macOS sama seperti entri §26/27, lolos (peringatan sama yang sudah ada sebelumnya). git diff --check lolos. Build perangkat penuh masih terhambat; rasa tarik-menarik (tug-of-war) sesungguhnya dan HUD zona-aman baru belum diamati berjalan.

29. Sprint Gameplay V1 - Layar Hasil, UI Minimal Rod, Ketertarikan Ikan

(AI Ikan, UI, Mekanik Cast/Fight) (2026-07-18) Alasan: sprint ini diruang-lingkupkan dari analisis kesenjangan terhadap GAMEPLAY_ARCHITECTURE_V1.md, diminta di hari yang sama ("apalagi yang belom ke develop dari architecture plan v1 itu?"). Tiga kesenjangan teratas yang ditangani: layar hasil, UI produksi minimal Rod, fase Fish Interested (ikan tertarik). ADR-029.

Shared: HookPhase mendapat .result — dikecualikan dari allowsCasting/allowsReeling, hanya dimasuki setelah tangkapan berhasil, hanya keluar lewat WorldViewModel.continueFishing(). FishingEvent mendapat .fishInterested, dikirim sekali saat ikan mulai tertarik tapi belum menggigit.

World: Ketertarikan Ikan: updateSearching(deltaTime:) sekarang mengembalikan SearchEvent? {.interested, .bite}, bukan lagi Bool, digerakkan oleh dua timer berurutan (masing-masing 0,8 sampai 2,0 detik: interestTimer lalu biteTimer) menggantikan timer tunggal 1,5 sampai 4,0 detik — menjaga total waktu-ke-gigitan tetap kira-kira di rentang yang sama. Ditambahkan isFishInterested: Bool (@Published). Kasus tick .hookInWater sekarang switch berdasar SearchEvent? dan mengirim .fishInterested atau .fishBite sesuai kondisinya.

World: Layar Hasil: FishCatchResult mendapat stars: Int, coins: Int, isNewRecord: Bool (nol/false kecuali .caught). resolve(outcome:) menghitung nilai-nilai ini hanya untuk .caught: stars dari seberapa dekat berat tangkapan ke berat maksimum spesies (1 sampai 5), coins = species.value * (0,6 + 0,1 * stars), isNewRecord dari FishRecordStore yang baru. Ditambahkan World/.../Gameplay/FishRecordStore.swift — menyimpan tangkapan terberat per ID spesies ke UserDefaults sebagai JSON (pola yang sama dengan MotionProfile milik Rod); bisa disuntikkan (injectable) ke FishFightController (default memakai store sungguhan). Alur tangkap di WorldViewModel.reelHook() sekarang berpindah ke .result (bukan .idle) saat ikan berhasil ditangkap; tarikan kosong tetap langsung ke .idle. Ditambahkan continueFishing() (hanya berlaku kalau hookPhase == .result, kembali ke .idle). tick() mendapat case .result: break untuk kelengkapan switch. Ditambahkan World/.../Views/ResultView.swift: overlay layar-penuh (spesies, berat, panjang, bintang 1-5, koin, lencana opsional "NEW RECORD", tombol "Continue Fishing") — "Return to Menu" dari dokumen produk BELUM diimplementasikan, belum ada Main Menu sama sekali. ContentView.body dipecah jadi ZStack berisi konten yang sudah ada + ResultView kondisional. Penyempitan cakupan yang disengaja: kabur dan tali-putus tidak melewati .result — keduanya tetap memakai transisi langsung dari ADR-028. Alasannya dicatat: konten hasil di dokumen produk dideskripsikan mengikuti "tangkapan berhasil" secara spesifik, dan menggerbang setiap kabur/tali-putus di balik modal yang harus di-dismiss dianggap merepotkan mengingat belum ada cooldown antar-gigitan; baris HUD "Catch Results" inline tetap menampilkan hasil-hasil tersebut.

Rod: Antarmuka Minimal V1: RodViewModel mendapat isFishing: Bool (@Published private(set)) dan startFishing()/endFishing()startFishing() hanya berlaku setelah calibrationStep == .complete. Gerbang izin yang sudah ada di updateAction() mendapat syarat tambahan di depan: !isFishing, jadi gestur cast/reel hanya hidup di antara start/end eksplisit, bukan seketika kalibrasi selesai. Rod/ContentView.swift ditulis ulang: tampilan utama sekarang hanya judul, status koneksi, dan tepat tiga tombol (Start Fishing / tombol kalibrasi per-langkah / End Fishing) — semua baris debug sebelumnya (status koneksi/spasial/gerak, langkah/jumlah-sampel kalibrasi, pitch/roll/yaw/action/ reelSpeed mentah) dipindah ke dalam DisclosureGroup ("Developer Details") yang tertutup secara default, bukan dihapus — preferensi yang sama seperti "nonaktifkan jangan hapus" di entri §22 untuk MotionDebugView/CastMeterView. Kesenjangan yang ditandai, belum diperbaiki di sprint ini: dokumen produk menetapkan instruksi kalibrasi seharusnya seluruhnya ada di World; teks calibrationInstruction milik Rod masih tetap ada di Rod (sudah ada sebelumnya, dilacak di bawah kesenjangan Main-Menu/Tutorial/Instruksi-Kalibrasi World yang masih terbuka).

Rod: Haptik Ketertarikan Berkala: HapticManager mendapat fishInterested() — satu transien lembut tunggal (0,22 / 0,15), sengaja dibuat lebih tumpul dari pola empat-pulsa fishBite(). FishingHaptics mendapat interestPulse(now:), dibatasi (throttled) ke 1,1 detik (idiom sama dengan reelTick yang sudah ada), tidak melakukan apa-apa kecuali flag internal isFishInterested diset; handle(.fishInterested) mengeset flag ini, setiap kasus event lain menghapusnya; ditambahkan stopInterest() untuk pembersihan dari luar. Kasus .hookPhase(phase) di RodViewModel.handle(_:) sekarang memanggil stopInterest() setiap kali fase bukan .hookInWater (mencegah pulsa ketertarikan basi terus berlanjut setelah gigitan/kabur/cast baru). updateAction() memanggil interestPulse() tanpa syarat di setiap sampel gerak (callback berulang yang sama yang menggerakkan reelTick).

Hasil: perintah verifikasi standar lolos. Daftar typecheck macOS: WorldViewModel, ContentView, FishCatalog, FishCatchResult, FishFightController, FishRecordStore, LineTensionView, ResultView, CastMeterView, TransportHost — lolos bersih (peringatan controllerEntity yang sama sudah ada sebelumnya). Typecheck simulator-iOS: RodViewModel, ContentView, FishingHaptics, HapticManager, MotionManager, NearbyInteractionManager, TransportClient — lolos bersih, tanpa peringatan. git diff --check lolos. Build perangkat penuh masih terhambat lingkungan — belum ada satu pun dari tombol minimal Rod, layar hasil, atau pulsa haptik ketertarikan yang diamati berjalan di hardware.

30. Gameplay V1 - Resistance Mengikuti Skala Ukuran Ikan yang Ter-roll

(AI Ikan, Perbaikan Bug) (2026-07-18) Alasan: bug yang dilaporkan pengguna, di hari yang sama dengan entri §29: "tension belom corespond dengan fish weight (alias masih linear untuk semua fish weight). seharusnya, semakin fish weight * length mendekati angka max, maka nilai resistance juga mengikuti." (Tegangan belum berkorespondensi dengan berat ikan — masih datar untuk semua berat; seharusnya makin berat*panjang mendekati nilai maksimum spesies, nilai resistance juga ikut naik.) ADR-030.

Yang berubah: resistance pada FishFightController tidak lagi langsung membaca currentSpecies?.resistance. Ditambahkan sizeRatio: Float privat (0 sampai 1): posisi hasil kali berat*panjang tangkapan ini (currentWeight * currentLength) di antara nilai minimum/maksimum hasil-kali berat*panjang spesies (weightRange.min/max * lengthRange.min/max), dibatasi 0 sampai 1 dengan cara yang sama seperti stars(forWeight:in:) sudah membatasi rasionya. resistance sekarang jadi species.resistance * sizeRatio — sebuah batas atas (ceiling) yang berskala per-tangkapan, menggantikan konstanta datar per-spesies. Nilai ini mengalir tanpa perubahan ke effectiveFishPullRate/fishPullSpeed (rumus dari ADR-026/028). FishSpecies, FishCatalog, dan semua perhitungan fight lainnya tidak terpengaruh — ini murni soal bagaimana nilai resistance diturunkan.

Hasil: daftar file typecheck macOS sama seperti entri §26-29, lolos bersih (peringatan sama yang sudah ada sebelumnya). git diff --check lolos. Hanya satu spesies (fish_a) yang ada di FishCatalog pada titik ini, jadi perubahan ini secara eksplisit ditandai sebagai tidak-terverifikasi-lewat-variasi-playtest di luar "fish_a kecil melawan lebih lemah daripada fish_a besar" — belum dikonfirmasi di hardware.