creator_id !== auth()->id()) { return response()->json(['message' => 'Unauthorized'], 403); } $step = $hunt->steps()->create($request->validated()); return response()->json($step, 201); } public function validateStep(Request $request, Hunts $hunt, HuntSteps $step): JsonResponse { $validated = $request->validate([ 'latitude' => 'required|numeric', 'longitude' => 'required|numeric', ]); $user = $request->user(); if ((int) $step->hunt_id !== (int) $hunt->id) { return response()->json(['message' => 'Step does not belong to this hunt'], 404); } $participation = $user->participations()->where('hunt_id', $hunt->id)->first(); if (! $participation) { return response()->json(['message' => 'Not participating in this hunt'], 403); } if ($participation->pivot->status !== ParticipateStatus::InProgress->value) { return response()->json(['message' => 'Hunt is joined but not started'], 409); } if ((int) $participation->pivot->current_step_number !== (int) $step->step_number) { return response()->json(['message' => 'This is not your current step'], 400); } $distance = $this->distanceMeters( (float) $validated['latitude'], (float) $validated['longitude'], (float) $step->latitude, (float) $step->longitude, ); if ($distance > $step->radius_m) { return response()->json([ 'success' => false, 'message' => 'You are too far from the target.', 'distance_m' => round($distance), 'radius_m' => $step->radius_m, ], 400); } $nextStepNumber = (int) $step->step_number + 1; $nextStep = $hunt->steps()->where('step_number', $nextStepNumber)->first(); $updateData = []; if ($nextStep) { $updateData['current_step_number'] = $nextStepNumber; } else { $updateData['current_step_number'] = $nextStepNumber; $updateData['status'] = ParticipateStatus::Completed->value; } $hunt->participants()->updateExistingPivot($user->id, $updateData); $response = [ 'success' => true, 'message' => 'Step validated', 'revealed_hint' => [ 'title' => $step->title, 'hint_text' => $step->hint_text, 'hint_media_url' => $step->hint_media_url, ], ]; if ($nextStep) { $response['next_step'] = $this->stepTargetPayload($nextStep); } else { $response['message'] = 'Hunt completed'; $response['completed'] = true; } return response()->json($response); } /** * @return array */ private function stepTargetPayload(HuntSteps $step): array { return [ 'id' => $step->id, 'hunt_id' => $step->hunt_id, 'step_number' => $step->step_number, 'title' => $step->title, 'latitude' => $step->latitude, 'longitude' => $step->longitude, 'radius_m' => $step->radius_m, 'ar_model' => 'chest', ]; } private function distanceMeters(float $latitudeFrom, float $longitudeFrom, float $latitudeTo, float $longitudeTo): float { $earthRadius = 6371000; $latFrom = deg2rad($latitudeFrom); $lonFrom = deg2rad($longitudeFrom); $latTo = deg2rad($latitudeTo); $lonTo = deg2rad($longitudeTo); $latDelta = $latTo - $latFrom; $lonDelta = $lonTo - $lonFrom; $angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) + cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2))); return $angle * $earthRadius; } }