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 CastSolution — World 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, mengesethasFishOnHook = true, berpindah ke.fishOnHook, mengirimFishingEvent.fishBite.tick(deltaTime:)mendapat kasus.fishOnHook(reeling seperti.hookInWater);.reelingsekarang kembali ke.fishOnHook(bukan otomatis ke.hookInWater) kalauhasFishOnHookbernilai true.simulateFlyingHook()meresethasFishOnHook=falsedan mengocok ulangbiteTimer = Float.random(in: 1.5...4.0)setiap kali splash — peluang gigitan acak independen tiap lemparan.reelHook(deltaTime:)mengirimFishingEvent.catchFishhanya kalau hook sampai ke joran denganhasFishOnHook == true; tarikan kosong tidak mengirim apa-apa..fishBite/.catchFishsudah didefinisikan sebelumnya dan sudah ditangani olehFishingHaptics.swiftmilik 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/lineBreakmasih 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:
WorldViewModelmendapatbiteExpireTimer,lineStrainTimer, konstantabiteExpireDuration(2,5 detik),lineBreakReelThreshold(0,9 kecepatan reel),lineBreakSustainDuration(1,2 detik).updateBiteExpiry(deltaTime:)berjalan selama.fishOnHooksaat tidak sedang reeling; ketika habis waktu, menghapushasFishOnHook, mengembalikan fase ke.hookInWaterdenganbiteTimerbaru, mengirimFishingEvent.fishEscaped.updateLineStrain(deltaTime:)berjalan selama.reelingsaat ada ikan terkait; terakumulasi hanya selagireelSpeed >= 0,9, direset kalau tidak; bertahan selama 1,2 detik akan menonaktifkan hook, menghapus ikan, mengembalikan fase ke.idle, mengirimFishingEvent.lineBreak.- Menjeda reeling di tengah fight sekarang mereset
biteExpireTimersebelum kembali ke.fishOnHook(supaya jam kabur tidak diam-diam berkurang lewat batas jeda-reel/lanjut). simulateFlyingHook()mereset kedua timer baru ini setiap kali splash.HapticManagermendapatfishEscaped()— pola tiga-pulsa yang memudar, berbeda darilineBreak()'s pulsa tunggal tajam (sebelumnya.fishEscapedsalah di-alias ke haptiklineBreakdiFishingHaptics.handle(_:)— sekarang tiap kasus dipetakan ke metodenya sendiri).
Kecepatan Reel Dinamis (ADR-025):
- Akar masalah ditemukan: kasus
.reelmenghitungspeed = min(1, rotationRateMagnitude / max(profile.reelRotationRate, 1)). Karena ambangreelRotationRateyang terkalibrasi hampir selalu di bawah 1,0,max(_, 1)membuat penyebutnya rata di sekitar 1, sementara reeling sungguhan gampang melebihi 1 rad/detik — sehinggaspeedlangsung jenuh (saturasi) ke 1,0 hampir seketika melewati ambang deteksi. - Ditambahkan
reelFullRotationRate: DoublekeMotionProfileberdampingan denganreelRotationRateyang 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 mengesetreelFullRotationRate = max(reelRotationRate + 0,3, peakRate)— puncak yang diperagakan pemain sendiri jadi acuan nilai 1,0. - Penyimpanan
MotionProfilelewatUserDefaultssudah memakaitry?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) : 0lineTension += (reelPull - effectiveFishPullRate) * deltaTime, dibatasi 0 sampai 1effectiveFishPullRate = 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.