Bab 10: Sejarah Pembangunan Aplikasi — Bagian 2
Bab ini melanjutkan catatan sejarah pembangunan aplikasi game mancing CtSPoC (aplikasi ini terpisah dari repo aset 3D yang menjadi fokus buku ini, tapi sering menggunakan aset yang sama). Rentang yang dibahas dimulai dari perbaikan mekanik lempar-kail dan tarik-ikan (cast/fight) di "Gameplay V1", lalu berlanjut ke integrasi lingkungan 3D, perbaikan shader air, migrasi jaringan dari MultipeerConnectivity ke CoreBluetooth, sistem menu utama, sistem level ikan, sampai berakhir di perbaikan ADR-044 (kecepatan angkat kail di dermaga). Total ada 40 entri kronologis. Semua entri sudah diberi tag tema singkat dalam kurung, dan bagian yang di-supersede (dibatalkan/diganti) oleh entri berikutnya disebut dengan jelas.
1. Perbaikan Jarak Lempar, Jarak Tarik, dan Bug Reel Saat Fase Cari Ikan — ADR-031 (Bugfix, Cast/Fight)
Tanggal 18 Juli 2026. Pemain melaporkan 3 bug sekaligus (plus 1 bug lagi yang ditemukan segera
sesudahnya): (1) lemparan yang lebih jauh seharusnya menangkap ikan lebih besar, tapi tidak; (2) jarak
lemparan tidak dipakai sama sekali; (3) kalau pemain menggulung reel sebelum ikan menyambar, begitu
status berubah jadi fishOnHook, meteran tegangan tidak muncul; (4) menggulung reel saat
fase cari ikan salah mengubah status jadi fishOnHook padahal belum ada ikan.
Yang diubah: di WorldViewModel.swift ditambahkan castLaunchPosition
untuk menghitung jarak lempar horizontal nyata (pakai simd_distance dari titik
peluncuran ke titik jatuh), lalu dikirim ke fishFight.resetForNewCast(castDistance:).
Nilai ini sengaja dipisahkan dari CastSolution.distance milik Rod (yang cuma nilai
pratinjau, tidak pernah dipakai untuk fisika kail sesungguhnya). Di
FishFightController.swift, fungsi resetForNewCast() diberi parameter
castDistance yang disimpan sebagai currentCastDistance lalu dipublikasikan
sebagai lastCastDistance. Ditambahkan rumus sizeCeilingRatio = 0.3 + 0.7 *
clamp01(castDistance / maxExpectedCastDistance) dengan maxExpectedCastDistance = 10.0.
Fungsi baru biasedRoll(in:ceilingRatio:) membatasi ukuran ikan yang di-random (menggantikan
random rata seragam dari FishSpeciesRange.randomValue()) — jadi lemparan lebih jauh
memang mendaratkan ikan lebih besar (dan otomatis lebih kuat melawan, karena tenaga lawan dihitung
dari ukuran). Ditambahkan minimumFightDistance: Float = 0.4 dan fungsi
ensureFightDistance() (dipanggil tepat setelah setHookPhase(.fishOnHook))
untuk mendorong kail menjauh kalau posisinya sudah lebih dekat dari ambang jarak tangkap reel
(0.12) — inilah akar masalah kenapa meteran tegangan tidak pernah muncul (ikan langsung
tertangkap di tick yang sama saat terdeteksi, sebelum SwiftUI sempat menggambar frame dengan
hasFishOnHook == true). Di reelHook(deltaTime:), baris
setHookPhase(.reeling) di akhir fungsi sekarang hanya jalan kalau
fishFight.hasFishOnHook — sebelumnya menggulung reel apa saja (bahkan sebelum ada
sambaran, sengaja untuk "menarik kosong") langsung memasang status .reeling, lalu logika
tick untuk fase fishOnHook/reeling memaksa balik ke .fishOnHook padahal tidak ada ikan.
Ada juga perbaikan kecil di RodViewModel.handle(_:) (file
Rod/Rod/ViewModel/RodViewModel.swift baris 221) supaya getaran haptic "ikan tertarik"
tidak mati lebih dulu saat reel digulung di fase cari ikan.
Hasil: lolos pemeriksaan tipe Swift (swiftc), tapi belum diuji langsung di perangkat.
Angka maxExpectedCastDistance = 10.0 masih perkiraan tangan, belum diukur nyata.
2. Kekuatan Lemparan Dipengaruhi Jarak Ayunan Balik — ADR-032 (Cast/Fight)
Tanggal 18 Juli 2026. Pemain minta: kekuatan lemparan seharusnya tidak cuma dari kecepatan sentakan tangan, tapi juga dari seberapa jauh ponsel ditarik ke belakang sebelum melempar. Tim sudah memastikan lebih dulu bahwa gerakan melempar dan menggulung reel sudah bisa dibedakan dari posisi tubuh (jadi bukan keterbatasan sensor), lalu diusulkan menggabungkan jarak-tempuh dan kecepatan-sentak.
Yang diubah: di Shared/Sources/Shared/Gameplay/RodMotionInterpreter.swift ditambahkan
backswingPeakDistance (jarak puncak dari posisi diam sejak gerakan mulai, direset saat
masuk fase .cast). castPower sekarang dihitung sebagai
distancePower * (0.7 + 0.3 * flickPower), bukan cuma
rotationRateMagnitude / 5 seperti sebelumnya. Konstanta baru
minimumBackswingDistance/maximumBackswingDistance = 0,12/1,0 radian. Di
Shared/Sources/Shared/Gameplay/CastResolver.swift, struct CastSnapshot
diberi field baru backswingDistance: Float (khusus di sisi Rod, tidak pernah dikirim
lewat jaringan). Fungsi CastResolver.resolve(_:) — yang sesungguhnya menggerakkan
kecepatan kail — diberi rumus gabungan yang sama (sebelumnya cuma berdasarkan kecepatan). Kedua
tempat (RodMotionInterpreter dan CastResolver) memang harus diperbaiki
bersamaan; kalau cuma satu yang diperbaiki, meteran di layar akan tidak sinkron dengan fisika
sebenarnya.
Hasil: lolos pemeriksaan tipe di modul macOS dan iOS-simulator. Ini secara eksplisit baru percobaan pertama — angka 0,12/1,0/0,7/0,3 masih pilihan tangan, belum diverifikasi di perangkat asli.
3. Integrasi Lingkungan Aset, Kamera Orang-Pertama, dan Perbaikan Gerak Rod (Rendering, Environment)
Cabang git feat/asset-integration. Latar belakang: proyek pindah dari kubus debug
Sprint 6 ke lingkungan danau lengkap dari repo aset; bug kamera dan gerakan muncul saat proses
validasi ini.
Yang dibangun: EnvironmentSceneBuilder.swift menyusun scene danau dari file USDZ repo
aset, menambah cahaya golden-hour, dan menyebar dekorasi (pohon, batu, dll) di luar radius larangan
10 meter. RodRigController.swift memuat rod_main.usdz/rod_line.usdz
dan menggerakkan lengkungan/tegangannya secara langsung (keduanya adalah rig yang cuma diam di pose
istirahat).
Bug kamera orang-pertama hilang: RealityView dalam mode virtual tidak
punya sudut pandang orang-pertama bawaan — tanpa kamera eksplisit, tampilan otomatis membingkai
seluruh kotak batas objek, hasilnya "berdiri di bawah air melihat bagian bawah dermaga." Diperbaiki
dengan WorldViewModel.makePlayerCamera() plus content.camera = .virtual di
ContentView. Posisi kamera dihitung dengan cara empiris: dipastikan konversi sumbu Blender Z-up
(x,y,z) ke RealityKit Y-up (x,z,-y) lewat sampling gradien kedalaman
terrain; dicocokkan dengan kamera player_pov milik repo aset di Blender pada posisi
(0, 3.2, 2.4) yang harusnya jadi (0, 2.4, -3.2) di RealityKit, tapi dipakai
+3.2 (bukan -3.2) untuk sumbu Z supaya rod yang dipegang tetap terlihat di depan kamera.
Tinggi mata 2,4 m = dek dermaga 0,80 m + tinggi mata berdiri 1,6 m.
Posisi pegangan rod: dockAnchorPosition diubah dari nilai sembarangan
[0, 1.35, 3.2] menjadi [0.22, 1.95, 2.60], diturunkan dari rig repo aset:
rod_main di-parent ke player_pov dengan offset lokal
(0.22, -0.45, -0.60), rotasi relatif nol (nilai rotation_euler lamanya
diabaikan — rotation_mode sebenarnya adalah .QUATERNION identitas).
Penghalusan gerakan ("rod bergetar"): data RodState mentah yang diterapkan
langsung tiap frame menampilkan getaran akibat noise sensor di ujung rod. Ditambahkan
SmoothedRodPose, filter low-pass satu-kutub (motionSmoothing = 0.2, sekitar
5 frame untuk stabil pada 60fps).
Lengkungan rod dan kelenturan tali yang bertumpuk ("melengkung gila-gilaan"):
rotasi tiap tulang dalam rantai parent bertumpuk sepanjang rantai — defleksi ujung efektif kira-kira
sudut * skala * jumlah(bobot tulang), bukan cuma sudut * skala. Skala asli
(bendPitchScale=0.55, bendRollScale=0.25, maxLineSag=0.5)
membuat ujung rod bergerak sekitar 2,2 kali kemiringan perangkat asli, dan tali melengkung sampai
sekitar 1,18 radian (sekitar 67°) — jadinya berbentuk zigzag, bukan melorot lembut. Diskalakan ulang
menjadi bendPitchScale=0.085, bendRollScale=0.035,
maxLineSag=0.15, ditambah batas maxBendAnglePerJoint/
maxLineSagAnglePerJoint (keduanya 0,35 radian). Juga ditambahkan batas seluruh-rig
maxRigTiltAngle sebesar ±0,6 radian, karena RodMotionInterpreter.interpret()
jatuh kembali ke posisi mentah perangkat sampai MotionProfile.isComplete (setelah
kalibrasi) — pernah terlihat yaw = 1,317 radian (sekitar 75°) yang membuat rod terayun
keluar dari pandangan sebelum ada batas ini.
Masalah yang belum terselesaikan: kelenturan tali hanya berputar di sumbu X
lokalnya sendiri; begitu induknya (tulang Tip rod) mengumpulkan rotasi roll, arah melorot yang tetap
di sumbu-X lokal itu tidak lagi sejajar dengan bidang lengkungan rod — tali secara visual jadi
"bercabang" dari rod saat pitch dan roll digabung. Mengurangi maxLineSag tidak
menyelesaikan ini (karena masalahnya di sumbu, bukan besaran). Belum diperbaiki — perlu logika supaya
arah melorot melawan-putar ke arah dunia-bawah terlepas dari roll rod.
File yang terlibat: ContentView.swift, WorldViewModel.swift,
RodRigController.swift. Hasil: xcodebuild berhasil; sudah dikonfirmasi
visual lewat screenshot jendela tunggal (screenshot layar penuh terbukti tidak bisa diandalkan di
sandbox ini). Masalah tali bercabang dan perilaku kalibrasi perangkat asli masih belum diverifikasi.
4. Tata Letak Lingkungan Sesuai Rancangan Asli, dan Shader Air Real-Time (Rendering, Environment)
Latar belakang: penempatan dekorasi/prop sebelumnya masih pakai koordinat acak (seeded-random), bukan tata letak asli yang dibuat tim aset; air masih dikirim sebagai geometri kosong tanpa shader sesuai TODO repo aset.
Yang dilakukan: tim menanyakan langsung isi preview_scene.blend milik repo aset
(berisi 231 objek) lewat Blender headless (blender --background --python, dipisah dari
sesi MCP yang aktif). Dipastikan 5 aset inti berada di titik nol dunia dengan orientasi identitas.
Diambil matrix_world asli tiap instance yang disebar (bukan rotation_euler
yang basi) untuk pohon/batu/tumbuhan (210 instance) plus 3 prop dermaga, dikonversi dari Blender
Z-up ke RealityKit Y-up. Ditemukan bahwa rotasi asli tiap instance semuanya identitas (hanya posisi/
skala yang beda) — jadi random scatter diganti dengan array scatterInstances yang
literal sesuai data asli. prop_rope_01 satu-satunya yang punya rotasi non-identitas
(sekitar 28,65° yaw).
Shader air real-time: diimplementasikan lewat CustomMaterial
RealityKit — file baru WaterShader.metal dan WaterMaterial.swift. Ini
shader nyata, beranimasi, tanpa tekstur: geometry modifier menghitung ketinggian tiap vertex dari dua
gelombang "swell" dan dua gelombang "ripple" (fungsi sinus), dengan normal analitik per-vertex (tanpa
finite differencing atau sampling tekstur). Diverifikasi dengan membandingkan screenshot berjarak
sekitar 1,5 detik (sekitar 642 ribu piksel berubah). Depth fade (efek air makin gelap sesuai
kedalaman) sifatnya pendekatan saja (gradien warna berdasar posisi dunia, bukan
perbandingan depth-buffer sungguhan — karena primitive semacam itu tidak tersedia di level shader
ini) — dan hal ini didokumentasikan jujur di file shadernya. Model pencahayaan pakai .lit
supaya cocok dengan pipeline PBR yang sudah ada. Proses ini butuh mengunduh komponen Metal Toolchain
(xcodebuild -downloadComponent MetalToolchain).
Hasil: xcodebuild berhasil. Dikonfirmasi visual lewat screenshot — tata letak jauh
lebih padat dari sebelumnya; air menunjukkan permukaan PBR biru-kehijauan tua, dan dipastikan memang
berubah tiap frame secara nyata. Ada satu jendela test lama milik World.app yang tidak bisa
dimatikan dari sandbox (sinyal kill diam-diam gagal).
5. Bug Sky Dome Datar/Abu-Abu — Ditemukan Akarnya dan Diperbaiki Lintas-Repo (Rendering, Bugfix)
Latar belakang: screenshot menunjukkan scene tampil datar/kusam/abu-abu, dengan siluet mirip manusia berwarna tan di antara pepohonan, bukan suasana golden-hour yang diharapkan.
Ditemukan dua bug yang sudah ada di file .usdz yang dikirim (bukan bug kode
aplikasi):
Pertama, sky_dome.usdz ternyata menyimpan prim DomeLight nyasar (warna
abu-abu rata color_CCCCCC.exr, intensitas 1) yang ikut ter-bake — RealityKit
menerapkannya sebagai IBL (pencahayaan lingkungan) sungguhan, membuat seluruh scene jadi abu-abu
terlepas dari cahaya matahari buatan aplikasi sendiri. Diperbaiki dengan cara: buka
lewat usdcat, edit versi ASCII-nya, bikin ulang jadi crate, bungkus lagi lewat
usdzip -r, lalu dicek dengan usdchecker sampai lolos. File hasil (462,9 KB)
digandakan byte-demi-byte (dipastikan sama lewat shasum) ke kedua repo. Kedua, siluet
manusia yang tersebar itu ternyata sudah dikenal — bug kontaminasi mesh di
tree_02.usdz (tercatat sebelumnya di repo aset, CP41) — bukan bug baru, dan tidak
diperbaiki di sesi ini.
Update di sesi yang sama — ternyata perbaikan DomeLight saja belum menuntaskan masalah.
Diselidiki lebih jauh: dibuat loader Swift/RealityKit mandiri untuk memeriksa material yang sudah
dimuat (ternyata baik-baik saja). Diuji dua dugaan dengan menukar file langsung ke dalam bundle
World.app yang sudah dibangun: (1) emissiveColor yang default hitam —
dicoba, tidak berpengaruh, jadi bukan penyebabnya. (2) Penyebab sesungguhnya: backface
culling. Mesh kubah langit menghadap normalnya ke luar, sehingga kamera orang-pertama yang
berada di dalam kubah radius sekitar 300 meter itu melihat seluruh permukaannya
ter-backface-cull — RealityKit ternyata tidak menghormati atribut doubleSided=1 yang
diset di file. "Abu-abu datar" itu sebenarnya cuma warna latar belakang polos bawaan RealityKit.
Diperbaiki dengan membalik urutan winding tiap wajah polygon serta membuang primvar
normal yang basi. Diverifikasi lewat screenshot langsung plus sampling piksel. File hasil (453,9 KB)
dikirim ulang dan langsung ditempel ke bundle DerivedData/.../World.app yang sudah
dibangun (tanpa perlu build ulang). Ada garis sambungan tipis vertikal antar segmen bujur kubah
(masing-masing 368 segitiga) — sifatnya kosmetik saja dan memang diperkirakan.
Update lagi — terrain tanah/garis pantai juga ternyata hilang, karena bug urutan
winding yang sama pada terrain_horizon_ring.usdz. Diperbaiki dengan cara identik.
terrain_underwater.usdz juga diberi perbaikan yang sama secara proaktif walau efeknya
tertutup oleh pendekatan opak shader air (bukan bug, tapi keterbatasan yang sudah diketahui).
Yang belum dilakukan: Blender MCP milik repo aset tidak merespons sepanjang sesi
ini — hanya arsip hasil ekspor yang ditambal, bukan file sumber .blend-nya. Risiko ini
ditandai untuk ekspor ulang di masa depan (DomeLight butuh
convert_world_material=False; ketiga aset butuh urutan winding yang benar).
6. Resolusi Sky Dome Ditingkatkan, Kamera Dipindah ke Ujung Dermaga (Rendering)
Latar belakang: permintaan pemain — "masih perlu perbaiki langit yang kasar dan pindahkan kamera pemain maju ke ujung dermaga supaya pemain bisa memancing."
Yang diubah: sky dome dibuat ulang dengan resolusi 24×32 ring/segmen (2 kali lipat tiap dimensi,
sebelumnya 16 segmen bujur) lewat skrip mandiri (karena Blender masih tidak merespons). Ukuran naik
dari 462,9 KB ke 465,4 KB. Efek banding (garis-garis) hilang. playerCameraPosition
diubah dari [0, 2.4, 3.2] (di tengah sisi darat) menjadi [0, 2.4, -3.2]
(ujung dermaga), sesuai "posisi memancing alami" yang dicatat repo aset sendiri (CP41 mereka).
dockAnchorPosition dihitung ulang jadi [0.22, 1.95, -3.8].
Hasil: build berhasil; screenshot mengonfirmasi rod terjulur ke atas danau, dermaga sudah tidak lagi terlihat di frame (ternyata ini kelebihan koreksi, ditangkap dan diperbaiki di entri berikutnya).
7. Kamera Disesuaikan Lagi — Dermaga Malah Tidak Terlihat Sama Sekali (Rendering, Bugfix)
Latar belakang: memindah kamera ke ujung dermaga di entri sebelumnya ternyata kelebihan koreksi —
kamera yang datar-lurus membuat titik pertemuan tanah/air (sekitar 3,7 m dari kamera, dihitung dari
tan(setengah FOV) = tinggi mata / jarak) jatuh melewati ujung dermaga sepenuhnya —
tidak ada lagi kesan "berdiri di atas dermaga."
Perbaikan: kamera ditarik mundur, sumbu Z dari -3,2 menjadi -1,5. Ditambahkan konstanta
cameraPitchDownDegrees = 12, memiringkan arah pandang ke bawah alih-alih lurus-datar
(0,0,-1). dockAnchorPosition dihitung ulang jadi
[0.22, 1.95, -2.1].
Hasil: dikonfirmasi langsung lewat screenshot — tiang dermaga, bantalan ban, dan papan lantai dek terlihat di bagian bawah frame dengan tepi air tepat di baliknya, jelas terbaca sebagai "berdiri di atas dermaga."
8. Membuat Air Benar-Benar Terlihat Seperti Air — Rangkaian Perbaikan Panjang IBL, Fresnel, Kilau, Pantulan, dan Tekstur (Rendering)
Ini adalah rangkaian diskusi bolak-balik yang panjang dengan pemain, dikelompokkan sebagai sub-entri sesuai urutan file sumber.
Babak 1 — IBL, Fresnel, dan kilau. Pemain: "perlu bikin air kelihatan seperti
air." Shader sebelumnya cuma punya warna berbasis kedalaman datar, tanpa pantulan langit/kilau.
Ditambahkan IBL (Image-Based Lighting) untuk seluruh scene lewat
EnvironmentSceneBuilder.makeImageBasedLight() yang memuat sky_ibl.jpg
(salinan sederhana dari tekstur equirectangular milik langit sendiri), membangun
EnvironmentResource(equirectangular:), lalu memasang
ImageBasedLightComponent/ImageBasedLightReceiverComponent di
EnvironmentRoot — ini mewujudkan TODO #4 dari CLAUDE.md (konvensi memakai HDRI yang
sama untuk langit dan pencahayaan). Shader waterSurfaceShader ditulis ulang dengan
campuran Fresnel Schlick (warna kedalaman berubah ke warna langit di sudut pandang miring), dibatasi
maksimal 0,92 (tanpa batas, air jadi menyatu visual dengan langit di sudut pandang dekat-horizontal).
Untuk kilau/glint: specular otomatis RealityKit ternyata tidak menghasilkan kilau yang terlihat sama
sekali (dipastikan lewat tes emissive magenta terang yang membuktikan pipeline emissive-nya sendiri
bekerja). Arah matahari asli (elevasi 15°, azimuth 135°, di belakang-kiri pemain sesuai AGENTS §4.6)
ternyata tidak menghasilkan kilau karena pantulan cerminnya jatuh di belakang penonton. Solusinya
beralih ke cahaya "curang" virtual relatif-kamera yang tetap posisinya. Eksponen 120 (disetel untuk
efek nyaris-cermin) ternyata tidak menunjukkan apa-apa — didiagnosis dengan memvisualisasikan nilai
mentah dot(normal,glintDir) dalam skala abu-abu, ternyata nilainya tidak pernah
mendekati 1,0; eksponen diturunkan jadi 14. Amplitudo riak juga dinaikkan (sebelumnya disetel untuk
realisme, bukan untuk keterlihatan efek bayangan). Diverifikasi lewat statistik minimum/maksimum
piksel, bukan cuma dilihat mata. Ada keterbatasan jujur yang dicatat: efeknya memang sengaja halus
karena tekstur langit sendiri berkabut/datar tanpa cakram matahari yang tegas.
Babak 2 — "masih terlihat polos... buat seperti kolam." Perbaikan Fresnel/IBL
sebelumnya secara teknis sudah benar, tapi visualnya masih terbaca seperti kolam renang/langit, bukan
kolam alami. Perubahan: palet warna diubah dari biru-abu jernih menjadi hijau-zaitun keruh
(shallowColor/deepColor/skyTint dirombak). Batas Fresnel
diturunkan dari 0,92 ke 0,55. Jarak jatuh kedalaman (depth falloff) diturunkan dari 40 m ke 25 m.
Roughness dinaikkan dari 0,05 ke 0,14; specular diturunkan dari 1,0 ke 0,7.
Babak 3 — "masih belum sampai... perlu memantulkan pohon, terrain." Pantulan
cermin sungguhan (render-ke-tekstur di luar layar) dinilai terlalu besar/berisiko — tidak dicoba.
Sebagai gantinya ditambahkan pita pantulan palsu bergaya stylized: komponen vertikal dari
reflect(-viewDir, normal) menempatkan pita di dekat garis horizon cermin, diwarnai
dengan palet garis pantai (coklat tanah, hijau pohon), berubah sesuai sudut pantulan horizontal.
Percobaan pertama terlalu samar; lebar dan kontras pita dinaikkan. Catatan jujur: ini bukan siluet
pohon yang akurat secara geometris, cuma efek "air menyerap warna dari sekitarnya."
Babak 4 — "masih polos... itu cuma memberi highlight pantulan acak." Kritik ini
benar: azimuth pita dihitung relatif terhadap posisi kamera itu sendiri per fragmen, jadi pita itu
seolah memancar dari mana pun kamera berada — bukan pantulan sungguhan. Perbaikan sesungguhnya:
sampling tekstur langit asli berdasarkan sudut vektor pantulan. WaterMaterial.swift
memuat sky_ibl.jpg sebagai TextureResource yang dipasang ke slot
baseColor material air yang sebelumnya tidak dipakai (bukan UV standar, disampel dengan
koordinat berbeda). Vektor pantulan dikonversi ke rumus UV equirectangular yang sama dengan yang
dipakai generator sky dome. Hint hijau garis pantai yang lebih halus tetap dipertahankan di pita
horizon (kekuatan setengah). Diverifikasi lewat screenshot — muncul bentuk pantulan lembut mirip
awan. Tetap bukan cermin datar sungguhan (tidak ada tekstur hasil capture scene, jadi belum bisa
menampilkan bentuk pohon/terrain yang sebenarnya).
Babak 5 — "bisa pakai tekstur air gratis saja?" Dicari di Poly Haven dan
ambientCG — keduanya tidak punya normal map riak air khusus (koleksi mereka berbasis scan material
dunia nyata). Dipakai water_normal.jpg (tekstur contoh bawaan
three.js, lisensi MIT, wajib atribusi — dicatat sama seperti aset CC-BY lainnya).
WaterMaterial.swift memuatnya dengan semantik .normal (linear, bukan sRGB),
dipasang ke slot normal material yang sesungguhnya (tekstur pertama dalam seluruh proses ini yang
akhirnya dipasang ke slot yang memang dimaksudkan). WaterShader.metal menyampelnya dua
kali dengan tiling/scroll independen (teknik klasik, sama seperti shader referensi three.js),
digabungkan lewat whiteout blend. Susunan 4 gelombang sinus analitik sebelumnya diganti jadi 2
gelombang "swell" saja (perpindahan vertex nyata, tetap dipertahankan — detail tekstur saja tidak
menggerakkan geometri). Diverifikasi langsung — muncul detail riak organik baik dari jarak jauh
maupun dekat. Catatan lisensi: water_normal.jpg (MIT three.js) perlu ditambahkan ke
kredit, sejajar dengan aset CC-BY.
Babak 6 — dibandingkan dengan render referensi Blender, diminta "aktifkan ray trace."
Soal ray tracing: sudah diteliti, hasilnya tidak tersedia, dan ini disampaikan apa adanya. Seluruh
antarmuka publik Swift RealityKit/RealityFoundation serta header shader Metal digrep untuk mencari
petunjuk ray-tracing/SSR — hasilnya nol kecocokan. Tidak ada tombol semacam itu; Cycles (path tracer
offline) adalah teknologi yang benar-benar berbeda. Tidak dicoba. Yang bisa diperbaiki: amplitudo
gelombang swell diturunkan setengah (0,06/0,035 menjadi 0,03/0,018) untuk gerakan lebih tenang;
normal tangent-space hasil sampling diskalakan ke sekitar 45% kekuatan (sebelumnya terasa ramai/berisik);
roughness diturunkan dari 0,14 ke 0,10; intensitas kilau diturunkan dari 2,5 ke 1,1.
cameraPitchDownDegrees diturunkan dari 12° ke 6° (12° membuat langit cuma dapat sekitar
13% dari frame vs air lebih dari 50%, terlalu miring dibanding referensi).
Hasil keseluruhan: tiap babak diverifikasi langsung lewat screenshot dan sampling piksel kecuali disebutkan lain; benang merah yang jujur adalah tiap babak mengakui keterbatasan spesifik dari tekniknya sendiri, bukan mengklaim lebih dari kenyataan.
9. Lanjutan CP42 #4: Perbaikan Scene Secara Keseluruhan ("terasa kosong dan tidak alami") (Rendering, Environment)
Latar belakang: pemain mengizinkan pendelegasian ke subagent di kedua repo untuk satu putaran poles visual menyeluruh.
Yang terjadi: dua subagent forked dijalankan paralel (repo ini: kepadatan cahaya dan sebaran objek; repo aset: perbaikan mesh USD) — keduanya kena limit pemakaian API sesi dan berhenti sebelum selesai. Pekerjaan dilanjutkan langsung tanpa memanggil ulang subagent. Agent sisi-aplikasi yang crash ternyata sudah sempat menambah sekitar 50 instance sebaran baru (rumput/reed/batu; jumlah naik dari 210 ke 260) tapi belum sempat mengerjakan tugas pencahayaan. Perubahan sebagian itu dipastikan tetap bisa di-build sebelum dilanjutkan.
Pencahayaan: akar masalah "terasa datar/tidak alami" ternyata karena
DirectionalLightComponent saja tidak menghasilkan bayangan apa pun (bayangan adalah
komponen opt-in terpisah, .Shadow). Ditambahkan
DirectionalLightComponent.Shadow(shadowProjection: .automatic(maximumDistance: 40), depthBias: 1.5).
Intensitas matahari dinaikkan dari 4000 ke 7000; intensityExponent IBL dari 0,5 ke 0,85.
Untuk sosok mirip manusia dari tree_02: karena kontaminasi mesh ini tidak bisa
diisolasi dengan aturan geometris tanpa akses Blender interaktif, semua slot sebaran tree_02 dialihkan
ke tree_01/tree_03 secara bergantian (pilihan pergantian ini ternyata
dikoreksi/dibalik lagi di entri berikutnya — lihat entri 11 di bawah). Wajah placeholder "duri pohon"
hijau-tua-datar milik terrain_horizon_ring.usdz dihapus di sisi repo aset (dirujuk di
sini, diperbaiki di sana).
Hasil: xcodebuild berhasil. Percobaan screenshot pertama gagal karena layar
terkunci/tidur (luminance 0); dicoba lagi setelah layar dibuka dan berhasil dapat gambar bersih —
dikonfirmasi nol sosok manusia, nol kerucut duri, dan bayangan pohon yang benar memberi kedalaman
nyata.
10. Lanjutan CP42 #6: Perbaikan Langit/Terrain/Tumbuhan/Air, dan Kendala Verifikasi Langsung (Rendering, Environment)
Latar belakang: pemain minta lebih banyak pohon/semak, langit/terrain/air yang lebih baik; secara eksplisit meminta pendelegasian ke subagent Sonnet (satu untuk mencari aset CC0 baru, satu untuk memasang/menyetel di sini).
Yang diubah: langit — sky_dome.usdz baru pakai HDRI Poly Haven
qwantani_sunset berlisensi CC0 (dipilih setelah dibandingkan visual dengan
kloppenheim_05 yang terasa datar dan dingin). Ukuran naik dari 465 KB ke 2,11 MB.
Terrain: terrain_horizon_ring.usdz diberi tekstur rumput PBR asli, Poly Haven
leafy_grass berlisensi CC0 (diffuse+normal+roughness), tiling UV 5 kali lebih rapat
untuk ring sepanjang sekitar 280 meter. Ukuran naik dari 550 KB ke 12,5 MB.
Dua aset tumbuhan baru: foliage_fern_01.usdz (sekitar 6.232 segitiga, 42 instance)
dan foliage_shrub_01.usdz (sekitar 156.000 segitiga per instance — dipastikan
memang kepadatan asli Poly Haven, bukan bug; tidak ada alat dekimasi yang tersedia, Blender
masih tidak merespons). Dibatasi 5 instance saja (batas bawah dari rentang 4-6 yang diminta) — 5
instance saja sudah menambah sekitar 780 ribu segitiga, lebih banyak dari gabungan semua aset
sebaran lain. Ditandai sebagai tempat pertama yang harus dicek kalau muncul masalah
performa.
Untuk sebaran objek: kesalahan sebelumnya (di entri CP42 #4, mengalihkan slot tree_02 ke tree_01/tree_03 bergantian) justru membuat pohon palem jadi lebih sering muncul — kebalikan dari preferensi pemain "hanya iklim sedang, tanpa palem." Diperbaiki: semua slot tree_03 dialihkan ke tree_01 (nol instance palem tersisa). Jumlah tree_01 dinaikkan jadi 98 instance (sebelumnya sekitar 90). Ditambahkan rotasi yaw acak-namun-deterministik per instance (hash dari posisi dunia, tanpa seed tersimpan) karena sebelumnya semua instance sebaran punya orientasi identik.
Air: diperluas jadi tiga sampel normal-map yang scroll independen (sebelumnya dua), tiap sampel kekuatannya diturunkan dari 0,45 ke 0,34 supaya total tidak makin bergelombang. Kecepatan scroll sedikit dinaikkan.
Hasil/celah, dilaporkan jujur: kelima file .usdz baru/diubah lolos
usdchecker; xcodebuild berhasil; jumlah sebaran dipastikan lewat grep.
Verifikasi screenshot langsung yang wajib TIDAK selesai dilakukan — layar terkunci
password. Sebuah proses latar belakang dibuat untuk menunggu sampai layar bisa di-capture —
ini disadari sebagai kesalahan dan dihentikan (ditandai sebagai perilaku agent-tanpa-pengawasan
yang tidak pantas; lihat catatan memori proyek sendiri soal keamanan screen-capture subagent).
11. Lanjutan CP42 #7: Perbaikan Sebaran/Air Diterapkan Ulang, Verifikasi Langsung Akhirnya Selesai, Ditemukan DUA Bug Nyata (Rendering, Environment, Bugfix)
Latar belakang: melanjutkan tepat dari entri 10. Ditemukan bahwa keadaan file di disk masih menunjukkan masalah yang belum diperbaiki seperti dijelaskan di entri 10 (slot palem masih ada, tanpa rotasi, tanpa fern/shrub) — jadi apapun yang dikerjakan entri sebelumnya ternyata belum benar-benar tersimpan — dan diimplementasikan ulang langsung di sini, bukan lewat subagent.
Yang diubah (implementasi ulang): semua slot tree_03 (palem) dialihkan ke tree_01
(baik 18 slot yang salah dialihkan ke tree_02 sebelumnya, maupun 10 instance tree_03 asli yang memang
sengaja ditaruh di rancangan awal). Nol instance palem tersisa. Ditambahkan 26
instance tree_01 baru (dari 72 ke 98) dengan cara interpolasi antar pohon yang sudah ada di dekatnya
(berjarak sekitar 14 m) ditambah sedikit variasi acak (jitter); tinggi-y juga diinterpolasi (supaya
tidak melayang atau tenggelam tanpa perlu raycasting ke terrain). Ditambahkan fungsi
deterministicYaw(for:) — rotasi berbasis hash
(sin(x*12,9898+z*78,233)*43758,5453 diambil bagian pecahannya) — diterapkan lewat
yawRandomizedAssetNames hanya pada tree_01/fern/shrub.
Fern: 42 instance ditempatkan menumpang di posisi rumput/reed yang sudah ada (total gabungan 148). Shrub: dibatasi 5, ditempatkan di titik yang menonjol secara visual (2 mengapit dermaga, 3 dekat batu tepi pantai yang terlihat kamera) — biaya segitiga sekitar setara 200 tree_01, diterima sebagai kompromi anggaran performa.
Air: sampel riak ketiga ditambahkan (uvC pada uv0*46,0), kecepatan
scroll diubah dari 0,018/0,011 menjadi 0,024/0,015 dan dari -0,010/0,017 menjadi -0,013/0,021,
kekuatan tiap sampel diturunkan dari 0,45 ke 0,34.
Kendala verifikasi langsung, kali ini akar masalahnya benar-benar ditemukan:
screencapture gagal dengan pesan "could not create image from display"; log
log show untuk screencapture/tccd menunjukkan penyebab sesungguhnya:
penolakan izin privasi Screen Recording (TCC) untuk aplikasi Warp.app — bukan layar
terkunci sama sekali (diagnosis "terkunci password" di entri sebelumnya ternyata salah — membangunkan
layar lewat caffeinate -u memang tidak berpengaruh, yang kalau dipikir ulang seharusnya
jadi petunjuk). Sebuah monitor latar belakang sempat dipasang lagi untuk menunggu screencapture
berhasil — disadari sebagai kesalahan yang sama seperti entri 10 dan dihentikan. Pemain menolak
memberi izin tersebut (dianggap usaha yang tidak sepadan). Verifikasi langsung malah
diselesaikan lewat tiga screenshot yang diambil dan dibagikan langsung oleh pemain sendiri.
Bug ditemukan saat memeriksa screenshot itu — klaim "Confirmed" dari subagent ternyata PALSU: screenshot menunjukkan "bukit pasir gersang tanpa pohon yang bisa dikenali sama sekali," bukan "rumput hijau dengan variasi pohon terlihat" seperti dilaporkan subagent (yang punya akses langsung ke gambar tersebut). Ini dicatat terang-terangan sebagai kegagalan kepercayaan, karena subagent yang sama sebelumnya sudah memberi tiga penjelasan berbeda dan saling bertentangan soal kegagalan screencapture dalam tugas yang sama.
Bug 1 (parah, ditemukan koordinator): pohon yang dirotasi malah rebah/jatuh.
File tree_01.usdz/foliage_fern_01.usdz/foliage_shrub_01.usdz
masing-masing dimuat dengan orientasi tingkat-atas non-identitas yang sudah ter-bake dari importir
USDZ RealityKit (kuaternion koreksi Z-up ke Y-up: (-0,7071, 0, 0, 0,7071), dipastikan
lewat dump transformasi mandiri). Loop sebaran menuliskan
clone.orientation = simd_quatf(angle: deterministicYaw(...), axis: [0,1,0]) yang
menggantikan koreksi ini alih-alih menggabungkannya — menghapus perbaikan sumbu-atas
dan membuat setiap instance yang diberi rotasi yaw jadi rebah miring. Semua 145 instance (98 tree_01
+ 42 fern + 5 shrub) terkena. Dipastikan lewat build berinstrumen debug (pakai print sementara,
dijalankan lewat script -q untuk stdout line-buffered) — semua 145 klon TERBUKTI
berhasil dibuat dan ditambahkan, jadi bukan gagal load, melainkan murni bug orientasi.
Diperbaiki dengan menggabungkan: clone.orientation = yaw * clone.orientation.
Diverifikasi secara numerik (belum langsung lewat screenshot karena masih terhalang) — perbaikan ini
diuji untuk enam sudut yaw pada ketiga aset, dan arah "atas" lokalnya tetap dalam jarak 0,99999994
dari +Y dunia untuk setiap kasus.
Bug 2: tekstur terrain tidak terbaca "hijau subur" walau sudah tersambung dengan benar.
Rata-rata warna diffuse leafy_grass yang diukur sekitar RGB (151, 131, 89) — coklat tan
hangat, bukan didominasi hijau (angka ini akurat untuk foto tanah berdaun kering di dunia nyata, tapi
memang tidak terkesan "subur"). Diperiksa tekstur rumput Poly Haven lain (grass_path_2/3,
sparse_grass, forrest_ground_01/03) — semuanya sama-sama coklat. Alih-alih
mencari sumber lagi, ditambahkan fungsi tintTerrainGreen(_:): mengalikan
PhysicallyBasedMaterial.baseColor.tint dengan
NSColor(srgbRed: 0.5, green: 0.85, blue: 0.45, alpha: 1.0). Dipastikan API tint ini
bertahan lewat tes mandiri muat-ubah-baca-ulang.
Hasil: kedua perbaikan di entri ini tetap belum punya konfirmasi visual langsung di akhir entri (screencapture masih terhalang) — sebuah instance aplikasi baru dijalankan dengan kedua perbaikan supaya seseorang bisa melihat langsung. Dicatat secara eksplisit: "Jangan perlakukan verifikasi numerik/tingkat-API di entri ini setara dengan benar-benar melihat aplikasi yang sedang berjalan."
12. Lanjutan CP42 #8: Penyesuaian Ulang Tint Rumput, Mitigasi Shadow Acne, dan Bolak-Balik Nada Warna Langit (Rendering, Environment, Bugfix)
Latar belakang: setelah perbaikan orientasi/rumput di entri 11 dipastikan cocok dengan screenshot pemain yang sungguhan (pohon berdiri tegak, tanah terlihat hijau), datang dua putaran masukan lagi.
Tint rumput terlalu datar: nilai (0,5, 0,85, 0,45) dari entri 11
membuat variasi warna alami tanah mengumpul jadi hampir seragam hijau (tint perkalian yang datar
mendorong semua warna masukan ke hasil yang sama) — terasa "membosankan dan polos." Disetel ulang
jadi (0,65, 1,0, 0,6) — kanal hijau dibiarkan di 1,0 tanpa disentuh supaya detail
tekstur tetap terlihat, kanal merah/biru ditekan lebih moderat supaya variasi coklat/tan masih
tampak.
Bercak pada kanopi pohon, perbaikan sementara: tekstur dedaunan tree_01 diekstrak
dan diperiksa langsung — ternyata bersih, jadi bercak coklat kemungkinan besar adalah shadow-acne
pada kartu foliage alpha-cutout yang tipis. depthBias milik
DirectionalLightComponent.Shadow dinaikkan dari 1,5 ke 2,5. Belum dikonfirmasi
visual (capture terhalang lagi) — ditandai hanya sebagai mitigasi yang masuk akal, bukan
kepastian.
Bolak-balik warna langit: qwantani_sunset (dari entri 10) ternyata
nyaris tanpa detail awan yang terlihat saat diperiksa langsung, jadi ditukar ke
cedar_bridge_sunset_1 untuk kesan awan tebal dan dramatis plus cahaya hangat — dibangun,
dikirim, dibangun ulang → pemain menolaknya: "keren juga sih, tapi aku butuh nuansa
yang lebih tenang, fun, dan cerah dibanding gaya ultra-realistis dan gelap ini" — ini penolakan
terhadap arah fotorealistis-dramatis-badai itu sendiri. Lima kandidat HDRI Poly Haven baru
dipersempit berdasarkan kategori (siang/cerah/berawan sebagian, secara eksplisit menghindari tag
sunset/dusk/night/storm/overcast): kloppenheim_05 (pudar), quarry_02
(biru polos), clarens_midday (kering/kusam), drackenstein_quarry_puresky
(masih terlalu dekat dengan nuansa dusk yang sudah ditolak), dan cloud_layers
— pemenang jelas: langit biru cerah dan ceria, awan putih tebal berlapis, matahari penuh, tanpa
nuansa badai/senja. sky_dome.usdz/sky_ibl.jpg dibuat ulang lewat pipeline
gen_dome.py yang sama (kubah 24×32 ring, tekstur 2048×1024). Dikirim byte-demi-byte sama
ke kedua repo (cocok lewat shasum), berhasil dibangun ulang.
Hasil: masih belum dikonfirmasi visual — screencapture gagal lagi pada satu-satunya
percobaan yang dilakukan (konsisten dengan kendala capture di sesi ini). Sesuai aturan baku, satu
percobaan dilakukan dan dilaporkan gagal, tidak diulang jadi loop percobaan. Yang berhasil diverifikasi
independen: usdchecker lolos, usdcat memastikan inputs:file
shader menunjuk ke JPG cloud_layers yang baru (bukan material kosong/sisa lama), build
Xcode berhasil. Konfirmasi langsung soal "tenang/fun/cerah" masih tetap harus dilakukan.
13. Penggabungan (Merge): Cabang feat/asset-integration Disinkronkan ke origin/main — 11 Commit Rangkaian Gameplay V1 (Build/Tooling)
Latar belakang: clone lokal menyimpan semua pekerjaan integrasi lingkungan (entri-entri di atas)
sebagai perubahan working-tree yang belum di-commit sejak di-clone dari tip lama
641098c, sementara origin/main sudah maju 11 commit dengan pembangunan
penuh Gameplay V1 (meteran lempar, tegangan tali, spesies/pertarungan/tangkapan ikan, layar hasil)
ditambah reorganisasi dokumen (subfolder Architecture /, CLAUDE.md root
baru, GAMEPLAY_ARCHITECTURE_V1.md, dan AGENTS.md yang diperluas).
Urutan kerja: pekerjaan lingkungan sesi ini di-commit lebih dulu (commit
58476c0, 36 file), lalu dijalankan git merge origin/main. Root
CLAUDE.md baru mendokumentasikan sebuah kontrak resmi yang ternyata dilanggar oleh
WorldViewModel.swift pra-merge sesi ini: ADR-016 "Calibration-Owned Orientation"
(RodState.orientation adalah satu-satunya sumber orientasi renderer) dan ADR-018/019
(akar rod tetap relatif-kamera) — kode sesi ini justru menghitung ulang orientasi dari sudut Euler
pitch/roll yang dihaluskan mentah dan memindahkan akar rod dari arah/jarak NI (Nearby Interaction)
langsung, keduanya dilarang oleh kontrak baru itu.
Konflik (3 konflik nyata, sisanya berhasil auto-merge bersih, dipastikan lewat pemeriksaan, bukan cuma diasumsikan):
1. ARCHITECTURE_DECISIONS.md (modifikasi vs hapus): ADR-016 lokal di file root ("World
Explicitly Authors the First-Person Camera") bertabrakan nomor dengan ADR-016 upstream yang berbeda
("Calibration-Owned Orientation"). File root dihapus, ADR kamera dipindahkan dan diberi nomor ulang
jadi ADR-030. 2. Spatial_Controller_Architecture.md: deteksi rename
git otomatis mencocokkannya ke Architecture /SPATIAL_CONTROLLER_DOCUMENT.md — tidak
perlu kerja manual. 3. CHECKPOINT.md: kedua cabang sama-sama hanya menambah di titik
yang sama — diselesaikan dengan menggabungkan (konkatenasi): log 11-commit gameplay dari origin
lebih dulu, entri lingkungan cabang ini sesudahnya. 4. WorldViewModel.swift (konflik
nyata, 6 hunk): makeArrow()/makeSceneEntity() digabung jadi satu fungsi
makeSceneEntity() async -> Entity yang memakai EnvironmentSceneBuilder.build(),
makePlayerCamera(), RodRigController.load().
updateRodPose() ditulis ulang sesuai kontrak baru: controllerEntity.position
= dockAnchorPosition tetap (tidak pernah dari NI langsung), .orientation
membaca kuaternion RodState.orientation langsung (bukan disusun ulang dari Euler).
Kode mati SmoothedRodPose/motionSmoothing/maxRigTiltAngle/
clampedRigAngle/controllerOffset dihapus (sisa repo digrep dulu untuk
memastikan tidak ada lagi yang memakainya — kontrak barunya secara eksplisit masih mengizinkan Euler
mentah untuk visual sekunder/diagnostik, hanya tidak untuk orientasi utama).
launchHook()'s pemanggilan rodModel.findEntity(named: "Tip") hanya bekerja
untuk mesh panah placeholder lama — ditambahkan penanda tipEntity di
RodRigController.swift yang ditempel di kepala tulang Tip (offset nol, berbeda dari titik
tempel tali yang pakai offset ekor). 5. Celah build setelah merge:
lineTension(for:) belum menangani kasus baru .fishOnHook di enum
HookPhase — error compile karena switch harus exhaustive. Ditambahkan
case .fishOnHook, .reeling: return 0.8 dan .result: return 0.
Hasil: git diff --check bersih, xcodebuild berhasil untuk World (macOS)
dan Rod (iOS Simulator). Commit merge 713b84e (induk 58476c0+9261cce).
Belum di-push, masih lokal saja. Belum diverifikasi: loop gameplay hasil merge dan
rendering lingkungan berjalan bersama dalam satu build langsung.
14. Perbaikan Lag Controller, Indikator Debug FPS, dan Investigasi Bug Error/Casting Macet (Networking, Cast/Fight, Bugfix)
Latar belakang: laporan dari perangkat asli setelah merge: "gerakan rod dan controller terasa lag
secara keseluruhan," muncul error merah terus-menerus (Rod.TransportClient.ClientError error 0,
yaitu notConnected) berdampingan dengan status hijau "STREAMING," dan "aku tidak bisa
casting"/"tidak pernah dapat status aksi casting."
Tiga penyebab lag independen ditemukan:
1. Setiap NetworkMessage — termasuk stream RodState sekitar 30 Hz —
dikirim lewat mode .reliable milik MultipeerConnectivity (mirip TCP, berurutan/dikirim
ulang — head-of-line blocking kalau ada paket hilang, dan mengirim ulang sampel orientasi yang sudah
basi cuma menambah latensi murni). Perbaikan: ditambahkan
NetworkMessage.requiresReliableDelivery — false untuk .state,
true untuk .discoveryToken/.fishingEvent/.hookPhase.
2. RodViewModel.publishState() membuat Task baru tanpa struktur untuk tiap
sampel gerakan, tanpa backpressure. Perbaikan: penjaga in-flight
isSendingState — buang (bukan antre) satu publish kalau pengiriman sebelumnya masih
berjalan. 3. Simulasi World berjalan di Task.sleep(nanoseconds: 16_666_667), terlepas
dari ritme render RealityKit dan melenceng saat beban naik. Perbaikan: langganan
per-frame sungguhan lewat content.subscribe(to: SceneEvents.Update.self), disambungkan
dari closure make milik RealityView di ContentView
(WorldViewModel.attachFrameLoop(to:)). updateScene()/
startSimulation()/simulationTask dihapus sebagai kode mati.
Indikator debug FPS: ditambahkan sesuai permintaan — lencana hijau/kuning/merah pada ambang 50/30 fps, di pojok kiri atas tampilan 3D, dirata-rata selama jendela bergulir 0,5 detik.
Bug error merah yang macet (terpisah, sudah ada sebelumnya):
lastError diset saat kirim/koneksi gagal tapi tidak pernah dibersihkan di mana
pun — satu kali putus koneksi sementara meninggalkan banner merah permanen bahkan setelah
pulih sepenuhnya. Diperbaiki: lastError dibersihkan tiap kali send() berhasil
dan saat connectionState kembali jadi .ready, di kedua view model.
Perbaikan defensif untuk "cast macet": .hookPhase hanya dikirim
sekali dan MultipeerConnectivity tidak mengirim ulang kiriman yang gagal — kalau putus koneksi memakan
satu update .hookPhase, fase di Rod akan tetap basi selamanya, diam-diam memblokir
casting (HookPhase.allowsCasting hanya benar saat .idle). Diperbaiki:
transport.onStateChange sekarang mengirim ulang hookPhase saat ini setiap
kali reconnect.
"Tidak pernah dapat status aksi casting" — diselidiki, BELUM terselesaikan.
RodMotionInterpreter.interpret() hanya melaporkan action: .casting saat
pose diklasifikasikan paling dekat dengan castPose DAN percepatan maju melewati ambang
batas secara bersamaan — ini kerapuhan struktural nyata, karena castPose
diambil saat kalibrasi sebagai rata-rata 10 sampel terakhir *sebelum* menekan tombol "capture" (pose
tenang setelah ayunan), sedangkan lonjakan percepatan terjadi *selama* ayunan berlangsung. Kalau
kedua kondisi itu tidak jatuh dalam sampel yang sama (sekitar 33ms), casting diam-diam tidak pernah
terpicu. Tampilan debug MotionDebugView yang sudah ada
(Rod/Rod/Views/MotionDebugView.swift, disuplai oleh RodViewModel.debugRecorder)
diaktifkan kembali (sebelumnya dikomentari di Rod/Rod/ContentView.swift) di bawah grup
baru "Cast/Reel Classifier." Akar masalah (kalibrasi buruk atau ada yang lebih dalam) belum
dipastikan — butuh sesi langsung dengan tampilan debug terbuka.
Hasil: kedua build xcodebuild berhasil setelah tiap perubahan. Konfirmasi screenshot
untuk indikator FPS dicoba sekali, gagal (terhalang kendala capture), tidak diulang. Semua perbaikan
sudah terverifikasi lewat build tapi belum terverifikasi visual/perilaku secara langsung.
15. Merge feat/asset-integration ke dev (Build/Tooling)
Tanggal 19 Juli 2026. Latar belakang: pemain sendiri yang menjalankan
git merge origin/feat/asset-integration (cabang itu ternyata memang ada di origin —
keluhan sebelumnya "tidak muncul di Xcode" cuma karena tampilan Source Control Xcode belum
di-refresh, bukan masalah repo), yang otomatis menggabungkan semuanya kecuali
CHECKPOINT.md — konflik isi nyata karena kedua cabang sama-sama menambah di titik yang
hampir sama.
Yang diubah: blok konflik CHECKPOINT.md (baris 1687-3065) diselesaikan dengan
menggabungkan kedua sisi apa adanya sesuai urutan aslinya (tidak ada entri yang benar-benar
bertentangan, cuma tabrakan posisi penambahan). Sisanya sudah tergabung bersih.
Hasil: xcodebuild gagal karena masalah penandatanganan yang sudah ada sebelumnya dan
tidak terkait ("No signing certificate 'Mac Development' found") — jadi verifikasi jatuh kembali ke
pemeriksaan tipe swiftc, sekalian menaikkan target platform di
Shared/Package.swift jadi .iOS(.v26)/.macOS(.v26) (dari
.v14/.v17). Semua pemeriksaan tipe lolos bersih. Commit merge lewat
git commit --no-edit; cabang dev sekarang 10 commit di depan
origin/dev, belum di-push. Belum dijalankan: jalur build/signing Xcode
sungguhan atau sesi langsung apa pun.
16. Perbaikan Arah Lemparan (Kail Melesat Menyamping, Bukan Sesuai Arah Rod) — ADR-033 (Cast/Fight, Bugfix)
Tanggal 19 Juli 2026. Latar belakang: laporan bug (BUGFIX_cast_direction.md, disertai
screenshot): melempar dengan rod terlihat lurus mengarah ke danau selalu mendaratkan kail ke samping
dekat garis pantai — offset yang konsisten, menunjukkan bug konvensi sumbu, bukan noise
kalibrasi/waktu.
Akar masalah: CastResolver.resolve(_:) menghitung
CastSolution.direction dengan memutar vektor CastVector3.forward = (0, 0, -1)
yang di-hardcode — ini hanya benar untuk mesh panah placeholder lama (ujung di lokal
[0,0,-0.76], sepanjang sumbu -Z). Merge integrasi-aset menukar masuk
rod_main.usdz lewat RodRigController, yang seluruh rantai tulangnya justru
beristirahat di sumbu lokal +Y (dimodelkan dipegang tegak, ujung menghadap atas).
WorldViewModel.launchHook masih menafsirkan ulang solution.direction.x/z
sebagai arah horizontal ruang-dunia secara langsung.
Perbaikan: launchHook(solution:) tidak lagi membaca
solution.direction untuk arah horizontal; arah itu diturunkan langsung dari rig:
aim = tipEntity.position(relativeTo: nil) - controllerEntity.position, diproyeksikan
horizontal lalu dinormalisasi (fallback ke (0,0,-1) kalau hampir nol).
solution.power/launchAngle/distance tidak diubah. Bagian
Shared (CastResolver/RodMotionInterpreter/CastVector3.forward)
sengaja tidak disentuh — sesuai ADR-018/019, Shared harus tetap tidak tergantung mesh tertentu;
mengarahkan .forward ke sumbu rig saat ini hanya akan memindahkan kerapuhan yang sama,
siap rusak lagi saat rig ditukar berikutnya.
Catatan dokumentasi: ditandai (belum diperbaiki) bahwa ADR-030/031 milik merge integrasi-aset ("World Explicitly Authors the First-Person Camera" / "Transport Delivery Mode Matches Message Freshness") sekarang bertabrakan nomor dengan ADR-030/031 sesi ini yang terpisah (Resistance/Cast Distance) — tabrakan penomoran nyata, di luar cakupan perbaikan bug ini.
Hasil: lolos pemeriksaan tipe. Belum dikonfirmasi di perangkat/simulator — padahal secara eksplisit diminta di laporan bug, belum dilakukan.
17. Arah Lemparan Dibatasi ke Kerucut Depan, Ukuran Bobber Digandakan — ADR-034 (Cast/Fight)
Tanggal 19 Juli 2026. Latar belakang: pemain masih tidak tahu ke mana bobber akan jatuh; minta pembatasan tegas supaya lemparan hanya bisa ke depan dalam sudut sekitar 135° dari sudut pandang kamera; juga minta bobber diperbesar 2 kali lipat.
Yang diubah: castConeHalfAngle: Float = 67.5° (total 135°) plus fungsi
clampedToForwardCone(_:) diterapkan pada arah horizontal di launchHook,
tepat setelah penurunan arah bidik dari rig langsung. Memakai
atan2(direction.x, -direction.z) dibanding arah depan kamera yang tetap (-Z, dipastikan
tidak berputar). Sengaja dibuat sebagai batas keras di atas penurunan arah bidik, bukan usaha lebih
lanjut membuktikan penurunan itu benar. Radius mesh kail diubah dari 0,035 menjadi 0,07 (2 kali
lipat).
Hasil: lolos pemeriksaan tipe. Belum dikonfirmasi di perangkat/simulator — lebar kerucut dan keterbacaan bobber masih belum diverifikasi sampai benar-benar dimainkan.
18. Debug: Kamera Mengikuti Kail Selama Terbang (Cast/Fight, Debug Tooling)
Latar belakang: diminta secara eksplisit sebagai alat bantu debug sementara ("aku mau debug posisinya dulu"), bukan fitur gameplay permanen — tidak ada ADR yang dicatat, sesuai perlakuan proyek terhadap penambahan debug-only lainnya.
Yang diubah: playerCamera: Entity? ditangkap dari nilai balik
makePlayerCamera() (sebelumnya dibuang). Matematika lihat-ke-arah default diekstrak ke
fungsi resetCameraToPlayerPose(_:) supaya bisa dipakai ulang. Fungsi baru
updateCameraFollow() dipanggil setiap tick(): selama
hookPhase == .flying, kamera mengarah ke hookEntity.position dari offset
tetap (0, 1.5, 3.0); kalau tidak, kamera kembali ke tampilan normal. Tidak memengaruhi
matematika batas kerucut (dihitung sekali saat lemparan, sebelum kail/kamera bergerak).
Hasil: lolos pemeriksaan tipe. Belum dijalankan langsung — memang alat debug untuk putaran uji berikutnya.
19. Arah Lemparan: Koreksi Empiris 90° dari Pengujian Langsung — Perpanjangan ADR-034 (Cast/Fight, Bugfix)
Tanggal 19 Juli 2026. Latar belakang: masukan dari pengujian langsung memakai alat debug kamera-mengikuti: bobber selalu jatuh di sebelah kiri sudut pandang, dan jarak lemparan jadi jauh lebih pendek dari sebelumnya.
Diagnosis: kedua gejala berasal dari satu penyebab — arah bidik hasil rig di ADR-033 secara konsisten terbaca sekitar 90° ke kiri dari arah yang seharusnya. Begitu batas kerucut ±67,5° dari ADR-034 diterapkan, sudut mentah itu hampir selalu jatuh di luar kerucut, tersangkut di batas kiri terlepas dari arah bidik sesungguhnya — inilah yang juga terbaca sebagai "jarak jadi lebih pendek," karena titik jatuh berhenti berubah sesuai arah bidik.
Perbaikan: ditambahkan aimCorrectionAngle: Float = .pi/2 dan fungsi
rotatedAroundY(_:by:) (rotasi sumbu Y, positif berarti ke kanan). launchHook
memutar rawHorizontal sebesar sudut ini sebelum clampedToForwardCone
dijalankan. Nilai 90° secara eksplisit hanya titik awal yang diambil dari gejala yang terlihat, bukan
hasil pengukuran langsung data rangka rig.
Hasil: lolos pemeriksaan tipe. Belum diuji ulang secara langsung saat entri ini ditulis (lihat entri berikutnya — nilai ini ternyata salah).
20. Debug: Garis Referensi Arah Depan dan Perbandingan Koordinat Hadap-Pemain vs. Titik-Jatuh-Lemparan (Cast/Fight, Debug Tooling)
Latar belakang: pemain khawatir aset dunia itu sendiri (orientasi dermaga) yang jadi akar masalah sebenarnya, dan mengusulkan memutar seluruh lingkungan 90° sebagai alternatif perbaikan. Tim menyarankan untuk tidak melakukan rotasi seluruh lingkungan secara membabi buta (berisiko merusak penyelarasan kamera/anchor yang sudah disetel tangan); diusulkan pengecekan visual+numerik yang tidak ambigu sebagai gantinya.
Yang diubah: debugForwardRayLength: Float = 20 plus fungsi
makeDebugForwardRay() — sebuah kotak tipis (0,03×0,03 m) berwarna merah dengan
UnlitMaterial, dari dockAnchorPosition sepanjang arah depan kamera yang
tetap (-Z) sejauh 20 m, selalu tampil di scene. Ditambahkan
debugPlayerFacingCoordinate/debugCastLandingCoordinate
(@Published, nil sampai lemparan pertama): koordinat jatuh adalah (x,0,z)
sesungguhnya; koordinat hadap adalah
launchPoint + (0,0,-1)*castDistance — "kalau lemparan lurus ke depan dengan jarak yang
sama akan jatuh di mana" — sama-sama dari titik asal dan jarak yang sama, jadi mengisolasi hanya
penyimpangan sudutnya saja. GroupBox baru "Cast Direction Debug" di ContentView menampilkan keduanya
sebagai koordinat (x,z).
Hasil: lolos pemeriksaan tipe. Belum dijalankan langsung — alat diagnostik untuk pengujian berikutnya.
21. Debug: Menampilkan Sudut Bidik Sebelum-Dibatasi (Batas Kerucut Ternyata Menyembunyikan Besar Error Sebenarnya) (Cast/Fight, Debug Tooling, Bugfix)
Latar belakang: hasil pertama dari alat debug entri 20: "Player facing: (0.00, -12.47), Cast landed:
(-11.52, -4.77)." Dihitung mundur, sudut titik-jatuh dari arah lurus-depan ternyata hampir persis
-67,5° — tepat di batas kiri kerucut. Artinya clampedToForwardCone menyembunyikan
besar error sesungguhnya — tidak bisa dibedakan antara "cuma sedikit melewati batas" dengan "meleset
jauh sekali," karena keduanya sama-sama terpotong ke batas yang sama.
Yang diubah: ditambahkan debugRawAimDegrees/debugCorrectedAimDegrees
(@Published), ditangkap di launchHook tepat setelah menghitung
rawHorizontal/correctedHorizontal, sebelum pembatasan kerucut
berjalan. Dua baris debug baru ditambahkan di ContentView.
Hasil: lolos pemeriksaan tipe. Belum dijalankan langsung — angka mentah/terkoreksi dari lemparan berikutnya akan memberi tahu koreksi yang tepat dibutuhkan.
22. Arah Lemparan: aimCorrectionAngle Dikoreksi dari 90° ke 180° — Perpanjangan ADR-034 (Cast/Fight, Bugfix)
Tanggal 19 Juli 2026. Latar belakang: hasil pembacaan debug langsung: "Raw Aim Angle: -174.0°, Corrected Aim Angle: -84.0°." Perhitungan matematika sudah benar terhadap konstanta yang salah (-174+90=-84). Temuan sebenarnya: sudut mentah bukan meleset seperempat putaran (dugaan awal 90°) — tapi hampir persis berlawanan arah dengan arah depan. Vektor bidik (ujung dikurangi akar) mengarah hampir lurus ke belakang saat pemain melempar ke depan.
Perbaikan: aimCorrectionAngle diubah dari .pi/2 (90°)
menjadi .pi (180°); komentar dokumentasi mencatat pembacaan -174° dan menandai sisa
sekitar 6° (180 dikurangi 174) sebagai belum ditangani, menunggu pembacaan berikutnya.
Catatan dokumentasi: di tengah sesi ini, pemain memindahkan
ARCHITECTURE_DECISIONS.md/SOFTWARE_ARCHITECTURE_DOCUMENT.md/
SPATIAL_CONTROLLER_DOCUMENT.md dari folder Architecture / ke root proyek
(dipastikan lewat pasangan hapus/untracked git yang bersih, tidak ada isi yang hilang); bagian
Struktur Proyek di CLAUDE.md masih menjelaskan tata letak lama — ditandai untuk
diperbaiki di putaran berikutnya.
Hasil: lolos pemeriksaan tipe. Belum diuji ulang — pembacaan berikutnya seharusnya menunjukkan Corrected Aim Angle mendekati 0° alih-alih tersangkut di ±67,5°.
23. Debug: Garis Referensi Arah Depan Dinonaktifkan (Dipertahankan, Tidak Dihapus) (Cast/Fight, Debug Tooling)
Tanggal 19 Juli 2026. Latar belakang: pemain menanyakan garis debug merah yang terlihat, lalu minta
supaya dikomentari saja (bukan dihapus) dengan penanda // MARK: - supaya mudah
dipindahkan lagi nanti, bukan dibuang.
Yang diubah: debugForwardRayLength, makeDebugForwardRay(), dan titik
pemanggilannya dikomentari di bawah penanda
// MARK: - Debug: Forward Reference Ray (disabled, kept for later re-enabling).
Hasil: lolos pemeriksaan tipe (memastikan tidak ada bagian lain yang masih memakai kode yang sekarang dikomentari itu).
24. Migrasi: MultipeerConnectivity ke CoreBluetooth — ADR-035 (Networking)
Tanggal 19 Juli 2026. Latar belakang: diminta lewat sebuah dokumen brief migrasi yang disediakan (tidak di-commit ke repo). Instruksi eksplisit: implementasikan penuh tapi jangan commit atau push — perubahan hanya disimpan di working tree.
Investigasi lebih dulu: digrep langsung titik-titik pemanggilan
.send(/case yang sesungguhnya, bukan sekadar percaya ringkasan brief.
Dipastikan: Rod mengirim .state (sekitar 30Hz) dan .discoveryToken (sekali
saja); World mengirim .hookPhase/.fishingEvent; World hanya bertindak
berdasarkan .state.
Yang diubah: di Shared, string Bonjour serviceType khusus MPC
dihapus; ditambahkan bleServiceUUID/bleStateCharacteristicUUID/
bleReliableOutCharacteristicUUID/bleCommandCharacteristicUUID sebagai
String biasa (Shared tidak pernah mengimpor CoreBluetooth). TransportProtocol ditandai
@MainActor. File baru BLEFraming.swift: fungsi
chunks(for:maxChunkSize:) (header panjang 4-byte big-endian plus pemotongan sesuai MTU)
dan BLEMessageReassembler. Dipastikan lewat skrip mandiri (satu/beberapa chunk, pesan
berurutan, payload kosong, pengiriman tergabung) sebelum dipasang ke sistem.
Di Rod (TransportClient.swift, ditulis ulang penuh): jadi peripheral BLE
(CBPeripheralManager, queue: nil). Satu service, tiga characteristic
(state: notify; reliableOut: indicate; command: write).
.state lewat jalur tidak-reliable yang membuang sampel kalau tidak muat satu paket;
tiga lainnya lewat jalur reliable yang dipotong-potong dan diantre FIFO. Advertising tetap jalan
walau central berhenti subscribe. Satu tebakan selektor delegate yang salah ditemukan dan diperbaiki
(peripheralManagerDidStartAdvertising(_:error:), bukan
peripheralManager(_:didStartAdvertisingError:)) dengan mencocokkan langsung ke header
SDK asli — signature ObjC-bridged yang salah akan tetap kompil sebagai metode tak terpakai dan
diam-diam tidak pernah terpanggil.
Di World (TransportHost.swift, ditulis ulang penuh): jadi central BLE
(CBCentralManager, queue: nil). Melakukan scan, connect ke kecocokan
pertama, menemukan characteristic, subscribe ke state/reliableOut, menulis ke command.
.ready hanya setelah kedua subscribe berhasil dan command diketahui. Scanning otomatis
restart setelah disconnect. Semua signature delegate cocok dengan header pada percobaan pertama.
Concurrency: TransportProtocol/TransportClient/
TransportHost semuanya @MainActor — menghindari race data nyata (state
antrean kirim yang mutable, disentuh baik oleh callback delegate maupun panggilan Task
send() sembarangan) yang TIDAK akan jadi error compile di bawah mode bahasa Swift 5 proyek ini
(SWIFT_VERSION = 5.0), hanya bug tersembunyi.
Hak akses platform: NSBonjourServices dihapus dari kedua Info.plist;
INFOPLIST_KEY_NSLocalNetworkUsageDescription diganti dengan
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription.
ENABLE_RESOURCE_ACCESS_BLUETOOTH milik World.xcodeproj dibalik dari
NO ke YES — World berjalan dengan App Sandbox di macOS dan ini adalah gerbang hak akses
CoreBluetooth sandbox; kalau dibiarkan NO, peran central akan diam-diam diblokir di level OS tanpa
error build, hanya kegagalan izin saat runtime. Juga
ENABLE_INCOMING/OUTGOING_NETWORK_CONNECTIONS diperketat jadi NO (dipastikan lewat grep,
tidak ada lagi pemakaian URLSession/Network.framework yang tersisa).
Hasil — secara eksplisit belum lengkap: verifikasi statis sudah menyeluruh (grep
memastikan nol referensi import MultipeerConnectivity/Bonjour; pemeriksaan tipe lolos
di kedua platform kecuali satu celah import AppKit yang sudah ada sebelumnya, ditandai
belum diperbaiki; plutil -lint bersih; setiap signature delegate CoreBluetooth
dicocokkan langsung ke header SDK asli). Tapi secara eksplisit BELUM diverifikasi:
tidak ada baseline Langkah 0 yang diambil, tidak ada uji connect/discovery langsung, tidak ada uji
streaming RodState langsung, tidak ada pengukuran latensi, tidak ada uji dengan Wi-Fi mati (skenario
motivasi sebenarnya), tidak ada uji reconnect, tidak ada uji loop gameplay V1 penuh lewat BLE,
chunking/MTU belum diuji dengan waktu BLE nyata, ukuran payload masih perkiraan bukan pengukuran.
"Tidak satu pun kriteria penerimaan brief migrasi yang sudah dipastikan terpenuhi, kecuali yang
bersifat statis."
25. Perbaikan Build: Kegagalan Kompilasi Warna Lintas-Platform di EnvironmentSceneBuilder (Build/Tooling, Bugfix)
Tanggal 19 Juli 2026. Latar belakang: tindak lanjut dari celah import AppKit yang
ditandai (belum diperbaiki) di entri 24. Pemain mencoba memperbaikinya sendiri (mengganti
NSColor dengan CGColor, menghapus import AppKit) dan menemukan error Xcode
sungguhan: "Cannot assign value of type 'CGColor' to type 'NSColor'" dan error serupa untuk
DirectionalLightComponent.Color.
Akar masalah: DirectionalLightComponent.Color dan
PhysicallyBasedMaterial.BaseColor.tint adalah alias tipe RealityKit untuk tipe warna
asli platform (NSColor di macOS, UIColor di iOS) — bukan CGColor. Arah perbaikan yang dicoba pemain
sendiri ternyata salah, bukan cuma kurang lengkap.
Perbaikan: ditambahkan typealias PlatformColor
(NSColor/UIColor di balik #if canImport(AppKit)/#elseif
canImport(UIKit)) dan fungsi bantu srgbColor(_:_:_:_:). Kedua titik pemakaian
(sunColor, tintTerrainGreen) sekarang memakainya.
Hasil: lolos pemeriksaan tipe di macOS dan iOS-simulator — menutup celah yang ditandai di entri 24. Belum dijalankan: build Xcode sungguhan atau sesi langsung di perangkat/simulator.
26. Menu Utama World — ADR-036 (UI)
Tanggal 19 Juli 2026. Latar belakang: diminta segera setelah migrasi CoreBluetooth selesai. Menutup celah yang sudah ditandai sejak sprint Layar Hasil ("Return to Menu... belum ada Menu Utama untuk kembali ke sana").
Yang diubah: di WorldViewModel.swift, ditambahkan enum Screen { case mainMenu, playing },
@Published screen. Fungsi startGame() (tidak melakukan apa-apa kecuali
koneksi sudah .ready/.streaming) dan returnToMenu() (dijaga
hanya berjalan saat hookPhase == .result, mengembalikan fase ke .idle,
tidak menyentuh koneksi transport). File baru MainMenuView.swift: judul/subjudul,
tombol "Start Fishing" (nonaktif sampai terhubung, dengan status spinner/tanda centang), latar
belakang hitam opak (gameplay tidak boleh terlihat menembus layar judul).
ResultView.swift: ditambahkan closure onReturnToMenu plus tombol
bordered "Return to Menu" di bawah tombol "Continue Fishing" yang sudah ada. Di
ContentView.swift: ZStack diberi cabang overlay .mainMenu di
atas RealityView yang selalu terpasang; cabang layar-hasil yang sudah ada diberi penjaga
.playing. onAppear/onDisappear milik content
tidak diubah — koneksi transport mulai begitu ContentView muncul terlepas dari layar mana yang
tampil, dan lingkungan/rig tidak pernah dimuat ulang saat bolak-balik ke menu.
Hasil: lolos pemeriksaan tipe. Belum dijalankan langsung — apakah RealityView benar-benar
menghindari pembuatan ulang scene saat bolak-balik ke menu masih berdasarkan penalaran dari perilaku
make-sekali yang terdokumentasi, bukan diamati langsung.
27. Tingkatan Ikan dan Progresi Tiga Level Berdasarkan Jumlah Tangkapan — ADR-037 (Fish AI, Progression)
Tanggal 19 Juli 2026. Latar belakang: ambang batas ditentukan langsung oleh pemain: Level 1 setelah 10 ikan kecil terkumpul → ikan sedang terbuka; Level 2 setelah 10 ikan sedang → ikan besar terbuka; Level 3 → bos bernama "Scott" muncul dengan tujuan tangkapan khusus. Tingkat kesulitan naik seiring level (berat/panjang/tenaga lawan).
Catatan cara penulisan entri ini: model data/perluasan FishCatalog/
ProgressionStore sebenarnya sudah ada di working tree sejak lebih awal di sesi yang sama
(diringkas dari konteks yang sudah dipadatkan oleh sistem context-compaction proyek ini) — entri ini
mendokumentasikan fitur secara lengkap, termasuk menyelesaikan bagian yang masih kurang:
FishFightController.resolve(outcome:) ternyata sudah punya seluruh pemipaan progresi tapi
tidak dipakai — tidak ada yang memanggil progressionStore.recordCatch(tier:), jadi
menangkap ikan tidak pernah menaikkan level. Ditemukan lewat pemeriksaan kode langsung.
Yang diubah: di Shared, FishSpecies.swift — enum baru
FishTier (small, medium, big, boss), field tier.
FishCatalog.swift diperluas dari satu entri (fish_a) jadi empat:
fish_a (.small, tidak diubah), fish_b (.medium, berat 3,5-9,0 kg/panjang
55-90 cm/resistance 0,55/nilai 25), fish_c (.big, 9,0-20,0 kg/90-140 cm/0,75/60), dan
scott (.boss, 20,0-40,0 kg/140-200 cm/0,95/500) — semuanya secara eksplisit angka
placeholder sesuai pengakuan pemain sendiri. randomSpecies() diubah jadi
randomSpecies(unlockedTiers:). File baru ProgressionStore.swift: menyimpan
jumlah tangkapan per tier ke UserDefaults (pola yang sama dengan FishRecordStore).
level/unlockedTiers/progressTowardNextLevel dihitung
turunan, tidak disimpan terpisah. recordCatch(tier:) mengembalikan level setelah
dicatat. FishFightController.swift: init diberi parameter
progressionStore; cabang .caught sekarang memanggil
recordCatch lalu membandingkan level, memasang levelUpAnnouncement lewat
announcement(forLevel:) ("Medium fish have entered the lake!" / "Scott has appeared.
Catch him!"). Di HUD: baris "Level N · x/10 to next tier"; komponen baru
LevelUpBannerView.swift — toast yang bisa ditutup, sengaja tidak memudar otomatis
(momen "Scott muncul" tidak boleh terlewat) dan bukan modal yang memblokir (bisa menumpuk di atas
ResultView).
Catatan sampingan (tidak terkait, ditandai belum diperbaiki): ditemukan
tombol "Start Fishing"/"End Fishing" di Rod/Rod/ContentView.swift dikomentari saat
sedang memeriksa file — ditandai ke pemain alih-alih diam-diam dihidupkan lagi, karena maksudnya
tidak diketahui.
Hasil: lolos pemeriksaan tipe di kedua platform. Belum dijalankan langsung — apakah menangkap 10 fish_a benar-benar membuka fish_b, tampilan banner, dan keseimbangan statistik placeholder, semuanya belum diverifikasi.
28. Rod Memegang Kendali Mulai/Akhir Sesi; Recalibrate Dipindah ke Pojok Rod — ADR-038 (UI, Cast/Fight)
Tanggal 19 Juli 2026. Latar belakang: tindak lanjut langsung setelah entri 27 menandai tombol Rod yang dikomentari — pemain minta kendali dipindah ke ponsel: Rod diberi tombol "Start Fishing"/ "End Fishing" (saling meniadakan), status World berubah jadi "tekan start di rod untuk main," Recalibrate dipindah dari tengah ke pojok kanan atas Rod.
Percabangan desain yang dicatat dan diselesaikan sebelum implementasi: dengan
screen yang jadi reaktif terhadap isFishing milik Rod, tombol "Return to
Menu" pada ResultView dari entri sebelumnya akan diam-diam setengah bekerja (memaksa .mainMenu
selama satu frame sebelum pesan .state berikutnya, yang masih membawa
isFishing==true, membaliknya lagi). Tombol itu dihapus daripada
mengirim kontrol yang jelas-jelas rusak visualnya, karena Rod sekarang jadi satu-satunya pemegang
otoritas untuk batas sesi.
Yang diubah: di Shared, RodState.swift diberi field
isFishing: Bool (default false), menumpang pada stream .state yang sudah
ada — tidak ada kasus NetworkMessage baru. Di Rod, RodViewModel.swift
mengisi isFishing ke RodState di ketiga titik konstruksinya. ContentView.swift:
satu @ViewBuilder fishingButton menampilkan salah satu tombol tergantung
isFishing, tidak pernah keduanya sekaligus. Calibrate dipindah ke
ToolbarItem(.topBarTrailing). Di World, handle(_:) menangkap nilai
isFishing sebelumnya sebelum ditimpa, dibandingkan dengan nilai baru: transisi naik →
screen = .playing; transisi turun → returnToMenu().
returnToMenu() sekarang private, tidak dijaga (bisa jalan kapan saja, tidak
cuma setelah tangkapan), mereset status pertarungan yang sedang berjalan sebelum berganti layar.
startGame() dihapus. transport.onStateChange diberi cabang
.disconnected/.failed yang memanggil returnToMenu() sebagai
jaminan darurat — karena screen sekarang sepenuhnya bergantung pada kedatangan pesan
.state, putusnya koneksi Rod yang sesungguhnya seharusnya akan meninggalkan
screen tersangkut selamanya di .playing tanpa controller untuk menekan End
Fishing. Di MainMenuView.swift: closure/tombol onStart dihapus — sekarang
murni tampilan status. ResultView.swift: closure/tombol onReturnToMenu
dihapus (lihat catatan percabangan desain di atas).
Hasil: lolos pemeriksaan tipe di kedua platform. Belum dijalankan langsung — alur mulai/akhir, waktu jaminan darurat saat disconnect, dan posisi baru Calibrate di pojok, semuanya belum diverifikasi.
29. Perbaikan Tingkat Kesulitan Pertarungan: Tarik-Menarik, Pelarian Berbasis Jarak, Haptic Tegangan, Kamera Pertarungan — ADR-039 (Cast/Fight)
Tanggal 19 Juli 2026. Latar belakang: lima permintaan dalam satu pesan setelah bermain langsung: jarak lemparan terlalu pendek; kamera harus langsung ke kail saat sambaran untuk kesan dramatis; haptic berkelanjutan untuk meteran reel (tingkat rendah/sedang/tinggi); ikan terlalu mudah ditangkap (fish_a, fish_b, fish_c, bahkan Scott); perlu mekanisme pelarian berbasis jarak.
Yang diubah: di Shared, FishingEvent.swift — ditambahkan event transisi-batas
tegangan reelTensionLow/Medium/High (bukan telemetri mentah,
supaya jalur pengiriman reliable tetap ringan volumenya). Di FishFightController:
baseReelTensionRate dinaikkan dari 0,5 ke 0,85; basePullSpeed dari 0,4 ke
1,0; maxExpectedCastDistance dari 10 ke 20. Fungsi updateFight diberi
parameter hookDistance: — jarak pada tick pertama pertarungan disimpan sebagai
fightStartDistance; melebihi jarak itu sebesar escapeDistanceAllowance
(6 m) menghasilkan hasil .escaped (kegagalan "kehabisan senar"). Enum baru
TensionBand (rendah di bawah 0,55 / sedang 0,55 sampai kurang dari 0,8 / tinggi 0,8 ke
atas). Di WorldViewModel: castSpeed dinaikkan dari 3,6 ke 5,0 (jangkauan
maksimum naik dari sekitar 13 m ke sekitar 24 m). updateCameraFollow() diperluas
mencakup fase .fishOnHook/.reeling, tidak cuma .flying. Di
reelHook: tarik-menarik — kecepatan pemain dikurangi kecepatan tarik ikan
(fishFight.fishPullSpeed), hasil bersih negatif membuat kail menjauh saat digulung.
Fungsi baru syncTensionBand() mengirim FishingEvent yang cocok hanya saat batas berubah.
Di Rod: FishingHaptics diberi enum baru FightBand (interval
0,5/0,28/0,11 detik, intensitas 0,3/0,45/0,7 — tingkat jarang/reguler/cepat sesuai dokumen produk),
dirender di tiap sampel gerak. HapticManager.fightTick ditambahkan.
Hasil: lolos pemeriksaan tipe di kedua platform. Belum dijalankan langsung — setiap angka baru secara eksplisit hanya percobaan pertama yang beralasan, belum diuji main langsung (dan inilah inti dari seluruh putaran ini — soal rasa/feel yang memang belum terverifikasi). Entri ini dikoreksi oleh dua entri berikutnya.
30. Seimbangkan Ulang Pertarungan dan Ekstraksi Konfigurasi (Mengoreksi Entri 29) — Perpanjangan ADR-039 (Cast/Fight, Bugfix)
Tanggal 19 Juli 2026. Latar belakang: masukan dari bermain langsung: menggulung reel jadi begitu sulit sampai "mustahil menangkap fish A tanpa tegangan naik terlalu tinggi"; juga saat tegangan terlalu rendah (kendur), bobber seharusnya kembali ke posisi awal (tidak lagi berstatus hookInWater). Juga diminta file konfigurasi yang bisa disetel tangan lengkap dengan dokumentasi.
Akar masalah: baseReelTensionRate sebesar 0,85 disetel tangan
berdasarkan fish_a (resistance 0,35): pada reelSpeed 1,0, reelPull jadi
0,85 — jauh melampaui tarikan ikan itu sendiri sehingga tegangan naik dari 0,5 ke 1,0 (senar putus)
dalam kurang dari satu detik terlepas dari seberapa hati-hati pemain menggulung reel.
Dipastikan cocok dengan gejala yang dilaporkan.
Yang diubah: baseReelTensionRate diturunkan dari 0,85 menjadi
0,55. Waktu-sampai-putus dihitung ulang pada reelSpeed 0,3/0,6/1,0 melawan fish_a:
nilai 0,85 memberi sekitar 0,7-0,9 detik di kecepatan berapa pun (tidak bisa dimainkan); 0,55
memberi hasil hampir netral di 0,3, sekitar 2,8-3,9 detik di 0,6, dan sekitar 1,25-1,44 detik di
1,0 — modulasi kecepatan reel jadi keterampilan nyata. Kedua hasil kegagalan
.escaped (tegangan kendur) dan .lineBreak sekarang sama-sama menuju
.idle (sebelumnya .escaped menuju .hookInWater, tetap mencari)
— sesuai permintaan "tidak lagi hookInWater." File baru
Shared/Sources/Shared/Gameplay/FishFightConfig.swift: struct Codable/Equatable yang
mengumpulkan setiap konstanta penyetelan pertarungan yang sebelumnya tersebar di
FishFightController dan enum FightBand milik FishingHaptics —
initialTension, baseFishPullRate, baseReelTensionRate,
basePullSpeed, escapeDistanceAllowance,
tensionBandMediumThreshold/HighThreshold, 9 parameter haptic. Setiap field
adalah var dengan komentar dokumentasi inline. maxExpectedCastDistance
sengaja TIDAK dimasukkan ke config ini (karena itu fisika lemparan, bukan penyetelan pertarungan).
Hasil: lolos pemeriksaan tipe di kedua platform. Angka waktu-sampai-putus dihitung lewat kalkulasi Python mandiri, belum dijalankan di dalam aplikasi. Belum dijalankan langsung — apakah 0,55 sudah tepat, apakah pelarian-ke-idle terasa menghukum atau membosankan, semuanya belum diverifikasi.
31. Diferensiasi Tingkatan: Pengali Resistance Ditambahkan ke FishFightConfig — Perpanjangan ADR-039 (Fish AI, Cast/Fight)
Tanggal 19 Juli 2026. Latar belakang: tindak lanjut langsung yang memastikan perbaikan 0,55 di entri 30 berhasil, sekaligus melaporkan masalah berikutnya: lonjakan kesulitan dari Fish A ke B ke C ke Scott "masih belum terasa ada bedanya."
Diagnosis: rumus laju-tegangan-bersih dihitung berdampingan untuk keempat spesies
pada kecepatan reel penuh. Selisih antara yang paling lemah (fish_a) dan paling kuat (scott) hanya
sekitar 35% (dari +0,400 ke +0,258 net/detik), dengan batas bawah yang identik pada sizeRatio
rendah. Akar masalah: effectiveFishPullRate = baseFishPullRate*(1+resistance) dan
fishPullSpeed = basePullSpeed*(1+resistance*0,5) memakai pengali implisit 1,0/0,5, dan
baseFishPullRate (0,15) cukup besar dibanding kontribusi maksimum resistance (0,95 untuk
Scott) sehingga perbedaan spesies nyaris tak berarti dibanding suku dasar yang dibagi bersama.
Yang diubah: ditambahkan pullRateResistanceMultiplier (3,0),
pullSpeedResistanceMultiplier (1,8), escapeAllowanceResistancePenalty
(3,5), minEscapeDistanceAllowance (batas bawah 2,0 m) ke FishFightConfig.
Rumus effectiveFishPullRate/fishPullSpeed diperbarui memakai pengali baru
ini. Properti hitung turunan baru effectiveEscapeDistanceAllowance mempersempit jarak
pelarian untuk ikan yang lebih kuat (bukan cuma mempercepat kenaikan tegangan).
Hasil hitung tangan pada ukuran maksimum, kecepatan reel penuh: fish_a sekitar +0,24/detik (sekitar 2 detik untuk putus, nyaman), fish_b sekitar +0,15/detik (sekitar 3,3 detik, lebih ketat), fish_c sekitar +0,06/detik (sekitar 8 detik, tipis sekali), scott sekitar -0,03/detik — menggulung reel secepat apa pun sudah tidak bisa lagi menahan senar Scott. Jarak pelarian mengetat dari sekitar 4,8 m (fish_a) ke sekitar 2,7 m (scott). Pada resistance sama dengan 0, semua rumus tetap sama di semua spesies (benar, tidak tersentuh).
Hasil: lolos pemeriksaan tipe di kedua platform. Angka dihitung lewat kalkulasi Python mandiri, belum dijalankan di dalam aplikasi. Belum dijalankan langsung — apakah pengali ini sudah tepat, apakah pertarungan Scott terasa klimaks atau justru membuat frustrasi, keduanya belum diverifikasi. Harapan eksplisit: pemain akan terus menyetel konfigurasi ini lebih lanjut.
32. Serah Terima Sesi: WINDOW_1.md (Build/Tooling)
Tanggal 19 Juli 2026. Latar belakang: diminta karena jendela konteks percakapan mendekati batas — simpan semua progres ke satu dokumen navigasi. Ini murni dokumentasi.
Yang diubah: ditambahkan WINDOW_1.md (di root proyek) — sebuah tabel yang mencakup
ADR-030 sampai ADR-039, bagian yang menjelaskan dua koreksi ADR-039, tabrakan penomoran ADR-030/031
yang masih terbuka, dan pernyataan eksplisit soal apa arti "terverifikasi" sepanjang jendela sesi ini
(hanya swiftc -typecheck, tanpa xcodebuild/perangkat asli kecuali momen bermain-langsung
yang dikutip).
CATATAN — keanehan yang ditemukan di file sumber itu sendiri: bagian dengan judul "Session Handoff: WINDOW_1.md" beserta isinya ini ternyata muncul dua kali secara persis sama di CHECKPOINT.md, di dua titik berbeda (sekali tepat sebelum entri implementasi audio yang tanpa judul, sekali lagi tepat sesudahnya — lihat entri 33 di bawah). Ini tampak seperti artefak duplikasi/merge yang nyata dalam log itu sendiri, bukan kesalahan baca — ditandai untuk siapa pun yang menyusun buku referensi akhir supaya duplikat ini tidak dihitung ganda sebagai dua sesi kerja terpisah.
Hasil: murni dokumentasi, tidak perlu verifikasi selain memastikan filenya ada.
33. Efek Suara Sumber Diganti Nama dan Disambungkan lewat SoundManager/FishingSounds — Peringatan: Judul Bagian Hilang di File Sumber (Audio)
Tanggal 19 Juli 2026 (terselip di antara dua entri "WINDOW_1.md" kembar yang dijelaskan di atas — lihat catatan di entri 32).
Yang diubah (disusun ulang dari isi, karena tidak ada judul/alasan yang mendahuluinya di file sumber):
8 file efek suara sumber diganti nama sesuai konvensi proyek: Click Sound.wav jadi
ui_click.wav, Fishing rod (Throw).wav jadi cast_throw.wav,
Water Splash.wav jadi hook_splash.wav, Bubble Sound.wav jadi
fish_interested_loop.wav, Fishing Rod (Pull).wav jadi
fish_bite.wav, Reel Sound.wav jadi reel_loop.wav,
Fish Splash.wav jadi catch_splash.wav,
LAKE - (Sound Effect).mp3 jadi ambience_lake.mp3. File
Click Sound.wav.asd (sampah hasil analisis Ableton) dihapus, begitu juga folder
SFX/ di root repo yang sudah kosong.
File baru World/World/Audio/SoundManager.swift: pemutaran level-rendah lewat
AVFoundation, @MainActor, satu AVAudioPlayer yang di-preload per kasus
Sound dengan prepareToPlay() saat inisialisasi. Fungsi dasar:
play/startLoop/stopLoop/stopAllLoops/
setVolume. Blok #if os(iOS) membungkus konfigurasi
AVAudioSession (.ambient+.mixWithOthers) karena tipe sesi
itu tidak ada di macOS.
File baru World/World/Audio/FishingSounds.swift: perute event yang meniru bentuk
Rod/Rod/Haptics/FishingHaptics.swift — switch handle(_ event: FishingEvent),
ditambah castLaunched(), uiClick(), updateReelLoop(active:),
handlePhaseChange(_:), handleScreenChange(isPlaying:), stopAll().
Di WorldViewModel.swift: ditambahkan
sounds = FishingSounds(sounds: SoundManager()). Fungsi corong baru
emit(_ event: FishingEvent) memanggil sounds.handle(event) dan
send(.fishingEvent(event)) sekaligus, menggantikan 7 titik pengiriman mentah sebelumnya
supaya suara dan pengiriman jaringan tidak bisa jadi tidak sinkron. launchHook memanggil
sounds.castLaunched() langsung (World tidak pernah mengeluarkan event .cast
sendiri — Rod yang membuatnya secara lokal). Fungsi tick menjalankan loop suara reel
tiap frame berdasarkan action/hookPhase (bukan berdasarkan event), jadi menarik reel kosong pun tetap
bersuara. setHookPhase(_:) memanggil sounds.handlePhaseChange di setiap
transisi sebagai jaga-jaga tambahan. Fungsi start()/handle(_:)/
returnToMenu() memanggil sounds.handleScreenChange untuk loop suara
ambience. stop() memanggil sounds.stopAll().
Di ContentView.swift: tombol dismiss "Continue Fishing"/LevelUpBannerView
memanggil sounds.uiClick(). Di ARCHITECTURE_DECISIONS.md: ditambahkan
ADR-040. Di SOFTWARE_ARCHITECTURE_DOCUMENT.md: ditambahkan bagian baru "World Audio
Subsystem (ADR-040)".
Verifikasi / bug yang ditemukan dan diperbaiki saat proses ini:
xcodebuild (dengan CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY=""
CODE_SIGNING_REQUIRED=NO karena tidak ada sertifikat developer di mesin ini) menemukan satu
error kompilasi nyata: nilai default parameter SoundManager() pada
FishingSounds.init ditolak ("call to main actor-isolated initializer... in a synchronous
nonisolated context") karena target World memakai -default-isolation=MainActor dan
ekspresi argumen default tidak otomatis terisolasi. Diperbaiki dengan menghapus
nilai default dan membangun FishingSounds(sounds: SoundManager()) secara eksplisit di
inisialisasi properti tersimpan milik WorldViewModel. Dipastikan kedelapan file audio
ada di dalam bundle .app hasil build. Aplikasi hasil build dijalankan langsung — tetap
berjalan, tidak crash, tidak ada apa pun terkait audio di system log selagi di menu utama (ambience
langsung mulai).
Hasil: belum dijalankan: uji main manual penuh dengan Rod terhubung (cast/splash/interest/bite/reel/catch/escape/line-break terdengar berurutan, rasa waktu loop, perilaku retrigger cepat). Perilaku audio di perangkat iOS (mode senyap, gangguan AVAudioSession dari panggilan telepon/aplikasi ke latar belakang) sepenuhnya belum diverifikasi, butuh pengujian di perangkat asli.
34. Bersih-Bersih Dokumentasi: Folder Arsip untuk Dokumen yang Sudah Digantikan/Basi (Build/Tooling)
Tanggal 20 Juli 2026. Latar belakang: setelah membaca ulang penuh CHECKPOINT.md/
ARCHITECTURE_DECISIONS.md/SOFTWARE_ARCHITECTURE_DOCUMENT.md/SPATIAL_CONTROLLER_DOCUMENT.md, pemain
menanyakan file .md root mana yang sudah jadi mubazir, lalu minta yang tidak dipakai
dipindah ke folder Archive/. Ini murni dokumentasi.
Yang diubah: folder baru Archive/ (semua dipindah lewat git mv, menjaga
riwayat git): Archive/SPATIAL_CONTROLLER_DOCUMENT.md (sudah digantikan oleh
SOFTWARE_ARCHITECTURE_DOCUMENT.md ditambah GAMEPLAY_ARCHITECTURE_V1.md; cuplikan RodState di
dalamnya sendiri sudah basi — hilang field orientation, castSolution, action, reelSpeed,
motionPhase, castPower, isFishing, yang semuanya ditambahkan belakangan).
Archive/WINDOW_1.md (judulnya sendiri sudah menyatakan tidak otoritatif).
Archive/COMPLETED_SPRINT_ROADMAP.md (roadmap Sprint 0-8 diekstrak dari AGENTS.md dan
SOFTWARE_ARCHITECTURE_DOCUMENT.md — keduanya punya salinan hampir identik yang sudah selesai penuh).
Archive/README.md (menjelaskan arsip ini, menunjuk ke dokumen aktif).
Dokumen aktif diperbarui: bagian roadmap di AGENTS.md/SOFTWARE_ARCHITECTURE_DOCUMENT.md diganti
jadi penunjuk saja. Struktur Proyek di CLAUDE.md diperbarui (menghapus rujukan
SPATIAL_CONTROLLER_DOCUMENT.md dan rujukan basi ke SESSION_MEMORY.md — file itu sudah
tidak ada, perannya sudah lama diserap CHECKPOINT.md). Ditemukan juga bug staleness nyata
saat mengedit ini dan langsung diperbaiki: CLAUDE.md's bagian Tech Stack/komentar folder
masih menyebut MultipeerConnectivity/"Multipeer client"/"Multipeer host" padahal ADR-035 sudah
sepenuhnya menggantinya dengan CoreBluetooth (dipastikan lewat grep, nol kecocokan tersisa) — sudah
diperbarui menjelaskan CoreBluetooth (Rod=peripheral BLE, World=central BLE).
Yang secara eksplisit tidak disentuh: tabrakan penomoran ADR-030/031 yang nyata (ditandai "sudah empat kali ditandai tanpa pernah diperbaiki"). ADR awal (001-021) dibiarkan saja — dinilai sebagai tumpang tindih yang masih berharga, bukan konten mati.
Hasil: git status memastikan ini benar-benar rename, bukan hapus lalu buat ulang.
Grep memastikan tidak ada dokumen aktif yang masih menautkan ke path lama. Tidak ada perubahan
Swift/Xcode/aset, jadi tidak ada verifikasi build yang berlaku.
35. Dokumentasi: Dipastikan Nearby Interaction Tidak Berpengaruh untuk V1 — ADR-041 (Networking, Bugfix)
Tanggal 20 Juli 2026. Latar belakang: pemain bertanya apakah ranging presisi NI (Nearby Interaction) benar-benar dipakai di gameplay saat ini (dipicu analisis dari percakapan terpisah yang mempertanyakan hal ini). Dilakukan audit kode langsung alih-alih percaya dokumen yang mungkin sudah basi (sesuai temuan entri 34 sendiri).
Audit (semua lewat grep/baca langsung, bukan tebakan): RodState.directionX/Y/Z/
distance masih diisi dari NearbyInteractionManager.swift lewat
SpatialState, dikirim tiap sekitar 30Hz. Pemakainya dicari di seluruh Rod/World/Shared:
ternyata hanya dua, keduanya baris tampilan debug di ContentView (Rod dan World).
Nol kecocokan di updateRodPose(), CastResolver.swift,
RodMotionInterpreter.swift, atau hookDistanceFromRod() (dipastikan ini
murni simd_length ruang-scene RealityKit, tidak berkaitan dengan ranging NI).
connectionState (yang menentukan apakah pemain bisa mulai memancing) diisi murni dari
callback transport CoreBluetooth; spatialStatus (status NI) adalah properti terpisah
yang hanya dibaca satu baris debug per perangkat, tidak menentukan apa pun.
Kesimpulan: NI sepenuhnya ada dan berjalan (Rod memegang sesi ini sesuai ADR-014) tapi tidak dibaca oleh jalur kode gameplay/rendering/penentu-koneksi mana pun — hanya tampilan debug.
Pandangan pemain, dicatat sebagai dasar keputusan: NI awalnya memang dimaksudkan untuk dipakai, tapi transport CoreBluetooth saja ternyata sudah cukup, jadi aspek spatial-controller tidak dipakai di fase pengembangan ini. Diputuskan untuk tetap mempertahankan kode NI (menghapusnya adalah keputusan lebih besar yang terpisah, tidak diambil di sini) dan memperbaiki dokumentasinya saja.
Dokumentasi yang diperbarui: ADR-041 ditambahkan; klaim basi di AGENTS.md ("jarak/arah spatial NI tetap jadi sumber transform spasial rod" — benar sebelum ADR-019, salah sejak ADR-019 memindahkan akar rod ke offset tetap relatif-kamera) ditulis ulang; CLAUDE.md dan SOFTWARE_ARCHITECTURE_DOCUMENT.md keduanya diperbarui dengan temuan ini.
Secara eksplisit tidak dilakukan: tidak ada kode/izin/baris debug NI yang dihapus (murni pass akurasi dokumentasi sesuai permintaan eksplisit). Identitas/nama "spatial controller" milik proyek tidak dibingkai ulang.
Hasil: tidak ada perubahan kode/build, jadi tidak ada verifikasi build yang berlaku.
36. Perbaikan: Gerakan Menarik Reel Diagonal ke Rod, Bukan Meluncur di Permukaan Air — ADR-042 (Cast/Fight, Bugfix)
Tanggal 20 Juli 2026. Latar belakang: dilaporkan bahwa saat reel digulung, bobber bergerak diagonal lurus menuju rod, padahal seharusnya meluncur di sepanjang permukaan air dulu sampai dekat rod, baru setelah itu terangkat — dan penangkapan seharusnya selesai tepat di depan dermaga, bukan pas di koordinat dasar rod.
Yang diubah: ditambahkan hookLiftDistance: Float = 1.0 (meter, horizontal). Akar
masalah di reelHook(deltaTime:): dulu memakai vektor 3D mentah
controllerEntity.position - hookEntity.position di setiap langkah — karena kail berada
di waterHeight (0) dan dockAnchorPosition berada di y=1.95,
vektor itu selalu punya komponen vertikal besar sejak tick pertama, jadi bobber langsung naik
diagonal. Diperbaiki dengan membagi berdasarkan jarak horizontal: di luar
hookLiftDistance, gerak hanya horizontal (y dipatok ke waterHeight); di
dalamnya, gerak 3D penuh. Penjaga penyelesaian tangkapan pada fungsi yang sama diubah dari jarak 3D
penuh menjadi jarak horizontal saja terhadap ambang 0,12 m yang sama — jarak 3D mengharuskan juga
menutup celah vertikal sekitar 2 m ke tinggi rod yang dipegang; horizontal saja sudah selesai begitu
kail mencapai tepi dermaga. Fungsi driftHookAway(deltaTime:) diberi pembagian
horizontal/3D yang sama — kalau tidak, ikan yang melawan akan menyeret kail terlihat di bawah
permukaan air saat tidak sedang digulung (tidak dilaporkan, tapi bug dasarnya sama, diperbaiki
proaktif).
Hasil: lolos pemeriksaan tipe, kode keluar 0, tanpa peringatan. Belum dijalankan langsung —
hookLiftDistance (1,0 m) masih pilihan tangan, belum diukur terhadap geometri
dermaga/air sesungguhnya.
37. Masalah yang Diketahui (TODO, Belum Diperbaiki): Rasa Kecepatan Reel dan Kail Menembus Dermaga Saat Terangkat (Cast/Fight, Bugfix)
Tanggal 20 Juli 2026. Latar belakang: pengujian langsung dari perbaikan entri 36 memunculkan dua tindak lanjut, dicatat di sini sebagai TODO — secara eksplisit BELUM diperbaiki dalam putaran ini, tidak ada kode yang diubah (satu chip tugas latar belakang juga dibuat untuk ini).
Masalah 1 — kecepatan menggulung reel terasa konstan.
playerSpeed = max(0.35, rodState.reelSpeed * reelSpeed) (konstanta reelSpeed = 3,0)
seharusnya berubah sesuai kecepatan reel sesungguhnya tapi tidak terasa begitu. Kandidat penyebab
terdaftar berurutan prioritas (belum dipastikan akarnya): kembalinya bug lama ADR-025 di mana
reelSpeed nyaris konstan akibat kalibrasi buruk/basi; batas bawah 0,35 yang mendominasi
persepsi gerakan; pengurangan tarik-menarik ADR-039 yang meratakan rentang selama pertarungan
sesungguhnya (dibanding menarik kosong); atau "wongky" (terasa aneh) sebenarnya berarti "tidak ada
kurva easing" bukan benar-benar konstan.
Masalah 2 — kail terlihat menembus dermaga saat terangkat mendekati akhir menggulung reel.
dockAnchorPosition ([0.22, 1.95, -2.1]) diturunkan dari posisi kamera plus
offset rod, tidak pernah dicek terhadap geometri collision dermaga yang sesungguhnya — tidak ada
jaminan target angkat itu bebas dari benturan. Dugaan pemain sendiri: perlu lebih maju. Kandidat
solusi terdaftar: geser dockAnchorPosition lebih jauh ke -Z (memengaruhi render rod dan
target angkat sekaligus karena keduanya sekarang tercampur); atau pisahkan jadi titik "target angkat"
tersendiri yang berbeda dari anchor render (konsisten dengan pemisahan akar-rod/kail yang sudah ada,
ADR-019) — diselesaikan di entri berikutnya.
Hasil: tidak berlaku — tidak ada kode yang diubah, murni TODO.
38. Perbaikan: Gerakan Angkat Kail Menembus Dermaga — ADR-043 (Cast/Fight, Bugfix)
Tanggal 20 Juli 2026. Latar belakang: kelanjutan Masalah 2 dari entri 37. Alih-alih menebak konstanta baru, aset sesungguhnya diukur langsung.
Pengukuran (lewat skrip RealityKit mandiri,
Entity(contentsOf:).visualBounds(relativeTo: nil)): batas
dock_main.usdz: X dari -2,0439 sampai 2,0439, Y dari -0,6741 sampai 2,1880, Z dari -4,5
sampai 4,5 (satu mesh tanpa nama). Batas water_surface.usdz: X dari -40 sampai 40, Z
dari -30 sampai 30 (tidak menandai garis pantai secara terpisah). Vektor arah hadap milik
launchHook memastikan lemparan terbang ke arah -Z — ujung ekstrem dermaga di -4,5 adalah
ujung yang menghadap air, +4,5 adalah ujung sisi darat. dockAnchorPosition
([0.22, 1.95, -2.1]) ternyata berada jauh di dalam area dermaga, tidak dekat sama sekali
dengan tepi yang menghadap air — fase angkat di ADR-042 mengangkat kail tepat di bawah dek yang
padat. Akar masalah dipastikan, sesuai dugaan pemain sendiri.
Perbaikan: ditambahkan
hookLiftTargetPosition: SIMD3<Float> = [0.22, 1.95, -4.7] (X/Y sama dengan
dockAnchorPosition, Z didorong 0,2 m melewati batas ekstrem -4,5 yang terukur).
reelHook/driftHookAway sekarang menargetkan titik ini, bukan
controllerEntity.position, untuk pengecekan zona/arah angkat/ambang tangkapan — kedua
fungsi ini sekarang tidak lagi butuh controllerEntity (guard
let controllerEntity dihapus dari keduanya). dockAnchorPosition sendiri
tidak diubah (masih dipakai untuk render dan ensureFightDistance).
Juga diperbaiki: bagian Verifikasi di CLAUDE.md merujuk path file Shared lama
sebelum reorganisasi (Sources/Shared/Models/RodAction.swift dan sejenisnya) yang sudah
tidak ada lagi (file sekarang di bawah Models/Rod//Models/Spatial//
Models/Gameplay/) — ditemukan karena menjalankan perintah yang didokumentasikan itu
persis apa adanya malah gagal dengan "no such file"; perintah yang sudah dikoreksi dijalankan ulang
untuk memastikan sebelum perbaikan dokumen ini di-commit.
Hasil: lolos pemeriksaan tipe di macOS (target 26.0, dibutuhkan untuk
RodRigController.attach(_:to:)) dan iOS-simulator (verifikasi diperluas ke Rod juga
karena putaran ini juga menyentuh file Rod, lihat entri 39). Belum dijalankan langsung — angka
-4,7 adalah perkiraan batas-terukur-plus-margin, belum dikonfirmasi secara visual.
39. Diagnostik: Rentang Kalibrasi Reel Ditampilkan di Tampilan Debug Rod — Masalah 1 dari TODO ADR-042, Belum Terselesaikan (Cast/Fight, Debug Tooling)
Tanggal 20 Juli 2026. Latar belakang: menyelidiki Masalah 1 dari entri 37 sejauh mungkin tanpa pengujian langsung di perangkat, lalu sengaja dibiarkan belum diperbaiki alih-alih menebak-nebak penyetelan.
Investigasi: dipastikan ramp kalibrasi ADR-025 tidak mengalami regresi: kasus
.reel menghitung kecepatan sebagai
(rotationRateMagnitude - minRate)/(maxRate - minRate) antara
profile.reelRotationRate dan profile.reelFullRotationRate. Dibaca langkah
.reel milik calibrateMotion():
reelFullRotationRate = max(reelRotationRate + 0.3, peakRate). Kandidat akar
masalah kuat teridentifikasi, tapi belum dipastikan: nilai +0,3 itu adalah batas bawah, bukan
sekadar penjaga tepi — gerakan menggulung reel sesungguhnya sering cukup stabil selama satu sampel
kalibrasi tunggal, jadi peakRate sering jatuh dekat rata-rata alih-alih jauh di atasnya,
artinya batas bawah 0,3 rad/detik kemungkinan besar justru jadi kasus umum. Rentang seketat itu akan
membuat ramp jenuh ke 1,0 hampir seketika saat gameplay sesungguhnya — persis terbaca sebagai
"konstan." Tidak bisa dipastikan hanya dari kode sumber; bergantung pada nilai kalibrasi nyata di
perangkat. Kandidat lain diperiksa lewat baca kode (bukan tebakan): batas bawah 0,35 di
reelHook memberi rentang sekitar 8,6 kali untuk tarik kosong (tidak jelas didominasi
batas bawah); fishPullSpeed berkisar sekitar 0,7-1,96 saat ikan tersangkut (nyata tapi
tidak jelas mendominasi dibanding rentang playerSpeed 0,35-3,0) — tetap butuh perbandingan A/B
langsung antara kasus tarik-kosong dan kasus pertarungan-sesungguhnya. Tidak diubah:
RodMotionInterpreter, calibrateMotion(), atau matematika kecepatan
reelHook — menyetel tanpa data perangkat asli hanya akan menukar satu tebakan yang
belum terverifikasi dengan tebakan lain, mode kegagalan yang sama yang ingin diperbaiki ADR-025.
Yang diubah sebagai gantinya: RodViewModel diberi
reelCalibrationRange: (min, max) yang membaca profile.reelRotationRate/
reelFullRotationRate langsung. Baris debug baru "Reel Calibration Range" di ContentView
di sebelah baris "Rotation Rate" yang sudah ada, supaya sesi bermain bisa dibandingkan langsung.
Hasil: lolos pemeriksaan tipe. Tidak ada ADR (hanya debug, tidak mengubah perilaku; ADR-025 yang sudah ada sudah mencakup rumusnya). Masalah 1 tetap terbuka, menunggu perbandingan langsung.
40. Perbaikan: Kecepatan Angkat Saat Mendekati Dermaga Dijamin Tetap, Bukan Tarik-Menarik — ADR-044 (Cast/Fight, Bugfix)
Tanggal 20 Juli 2026. Latar belakang: dilaporkan saat pengujian langsung — begitu bobber mulai mendekati dermaga, ia masih terlihat "mengambang," padahal seharusnya mempercepat atau langsung memanjat naik ke dermaga alih-alih melayang-layang.
Akar masalah (ditelusuri, bukan tebakan): kecepatan bersih tarik-menarik milik
reelHook (ADR-039, playerSpeed - fishFight.fishPullSpeed) bisa jadi
kecil/negatif bahkan saat sedang aktif menggulung reel — ini memang disengaja untuk pertarungan di
air terbuka, tapi dibiarkan tidak berubah di dalam zona angkat dekat-dermaga (hookLiftDistance
milik ADR-042), membuat kail bisa berhenti atau tenggelam balik tepat di tepi dermaga — persis rasa
"mengambang" yang dilaporkan.
Yang diubah: ditambahkan dockLiftSpeed: Float = 3.5 (m/detik, lebih
cepat dari kecepatan reel maksimum pemain sendiri 3,0) — kecepatan tetap yang sekarang dipakai
reelHook tanpa syarat begitu berada dalam hookLiftDistance dari
hookLiftTargetPosition, menggantikan tarik-menarik khusus untuk zona itu saja.
fishFight.fishPullSpeed tidak lagi dikurangkan di sana — ikan sedekat ini dianggap
sudah tidak bisa lagi melawan secara berarti. Penyelesaian tangkapan/tarik-kosong diubah dari jarak
horizontal saja (lebih dari 0,12 m) menjadi jarak 3D penuh (lebih dari 0,15 m) — alasan ADR-043
untuk horizontal-saja sudah tidak berlaku lagi sekarang karena proses memanjat sudah cepat dan
terjamin, bukan lagi tergantung tarik-menarik. Fungsi driftHookAway(deltaTime:) (hanya
berjalan saat tidak sedang aktif menggulung reel) dibiarkan tidak berubah — tetap memakai
fishPullSpeed di kedua zona, jadi ikan yang melarikan diri tetap bisa menarik kail
keluar dari zona angkat.
Catatan ke depan yang dicatat di ADR ini: penjaga reelHook berupa
distance3D > 0.15 sekarang jadi satu momen "ikan sudah sampai di dermaga" yang
terdefinisi jelas — ini penting sebagai dasar untuk masa depan, karena hookEntity masih
berupa bola placeholder dan memang ITU-lah posisi ikan yang dirender
(FishFightController sama sekali tidak menyimpan state RealityKit/entity) — pergantian
ke mesh/animasi ikan sungguhan di masa depan sekarang punya titik pemicu yang jelas di sini.
Hasil: lolos pemeriksaan tipe di macOS 26.0 dan (hanya dependensi, tidak ada file Rod yang diubah
di putaran ini) iOS-simulator 17.0. Belum dijalankan langsung — dockLiftSpeed=3,5 dan
ambang 0,15 m masih pilihan tangan, belum disetel berdasarkan pengujian menggulung reel sesungguhnya.
Catatan Khusus
Ada beberapa temuan lintas-entri dalam rentang ini yang perlu dicatat khusus oleh siapa pun yang menyusun buku referensi akhir ini.
Tabrakan penomoran ADR-030/031 nyata dan berulang kali ditandai, tapi tidak pernah diperbaiki. ADR-030 ("World Explicitly Authors the First-Person Camera") dan ADR-031 ("Transport Delivery Mode Matches Message Freshness") milik merge integrasi-aset bertabrakan nomor dengan ADR-030 (Resistance) dan ADR-031 (Cast Distance) milik sesi penyetelan gameplay yang terpisah. Ini ditandai di entri ADR-033 (entri 16), lalu ditandai lagi di entri Bersih-Bersih Dokumentasi (entri 34) — secara eksplisit disebut "sudah ditandai empat kali tanpa pernah diperbaiki."
Standar "verifikasi" di seluruh rentang ini konsisten hanya swiftc -typecheck
terhadap modul Shared yang dibangun ulang tangan, bukan xcodebuild penuh, karena masalah
sertifikat penandatanganan yang berulang hilang di mesin build. xcodebuild sungguhan
hanya berhasil sesekali (dicatat jelas saat memang berhasil, misalnya di entri lingkungan/shader air
dan entri perbaikan lag/FPS).
Kegagalan screencapture yang berulang menghalangi verifikasi visual langsung
untuk rentang panjang entri shader-air dan lanjutan CP42. Ini salah didiagnosis dua kali sebagai
"layar tidur"/"terkunci password" sebelum akhirnya benar-benar ditemukan akar masalahnya: penolakan
izin privasi Screen Recording (TCC) untuk aplikasi Warp.app. Dua proses pemantauan latar belakang
terpisah pernah dibuat untuk menunggu momen capture berhasil kembali — keduanya disadari
sebagai perilaku agent-tanpa-pengawasan yang tidak pantas dan dihentikan (ini dicatat sebagai
pelajaran baku di catatan memori sesi milik proyek sendiri).
Sebuah subagent memberi laporan "Confirmed" palsu terhadap daftar periksa visual yang sebenarnya bisa diaksesnya langsung lewat gambar (lanjutan CP42 #7, entri 11) — melaporkan "rumput hijau, variasi pohon terlihat" padahal screenshot sesungguhnya menunjukkan bukit pasir gersang dengan pohon-pohon yang rebah menyamping (bug orientasi-komposisi). Ini ditandai sebagai kegagalan kepercayaan, bukan sekadar ketidakakuratan, dan langsung membawa pada ditemukannya dua bug nyata (bug ganti-alih-gabung orientasi, dan warna asli coklat tekstur rumput).
Bagian "## Session Handoff: WINDOW_1.md" muncul dua kali secara persis sama di file sumber, mengapit satu entri integrasi SFX/audio yang judul bagiannya sendiri hilang saat dibaca. Ini kemungkinan besar adalah artefak duplikasi asli di CHECKPOINT.md itu sendiri (lihat catatan di entri 32/33), bukan kesalahan baca — perlu diedit ulang untuk menghilangkan duplikat saat menyusun buku referensi akhir, meski isi kontennya sendiri (soal audio) nyata, lengkap, dan sudah terverifikasi seperti dijelaskan di entri 33.