129 lines
4.0 KiB
PHP
129 lines
4.0 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Support;
|
||
|
|
|
||
|
|
use App\Models\User;
|
||
|
|
use App\Models\WorkoutSession;
|
||
|
|
use App\Models\WorkoutSessionItem;
|
||
|
|
use Carbon\Carbon;
|
||
|
|
|
||
|
|
class Gamification
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* @return array{current_streak: int, longest_streak: int, badges: array<int, array{key: string, name: string, description: string, earned: bool}>}
|
||
|
|
*/
|
||
|
|
public static function forUser(User $user): array
|
||
|
|
{
|
||
|
|
$completedDates = WorkoutSession::where('user_id', $user->id)
|
||
|
|
->whereNotNull('completed_at')
|
||
|
|
->pluck('completed_at')
|
||
|
|
->map(fn ($dt) => $dt->toDateString())
|
||
|
|
->unique()
|
||
|
|
->sort()
|
||
|
|
->values();
|
||
|
|
|
||
|
|
$currentStreak = self::currentStreak($completedDates);
|
||
|
|
$longestStreak = self::longestStreak($completedDates);
|
||
|
|
|
||
|
|
$completedSessions = $completedDates->isNotEmpty()
|
||
|
|
? WorkoutSession::where('user_id', $user->id)->whereNotNull('completed_at')->count()
|
||
|
|
: 0;
|
||
|
|
|
||
|
|
$hasCenturyLift = WorkoutSessionItem::whereHas('session', fn ($q) => $q->where('user_id', $user->id))
|
||
|
|
->where('done', true)
|
||
|
|
->where('actual_weight', '>=', 100)
|
||
|
|
->exists();
|
||
|
|
|
||
|
|
$badges = [
|
||
|
|
[
|
||
|
|
'key' => 'first_steps',
|
||
|
|
'name' => 'First Steps',
|
||
|
|
'description' => 'Complete your first workout session.',
|
||
|
|
'earned' => $completedSessions >= 1,
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'consistent',
|
||
|
|
'name' => 'Consistent',
|
||
|
|
'description' => 'Complete 10 workout sessions.',
|
||
|
|
'earned' => $completedSessions >= 10,
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'dedicated',
|
||
|
|
'name' => 'Dedicated',
|
||
|
|
'description' => 'Complete 50 workout sessions.',
|
||
|
|
'earned' => $completedSessions >= 50,
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'week_warrior',
|
||
|
|
'name' => 'Week Warrior',
|
||
|
|
'description' => 'Reach a 7-day training streak.',
|
||
|
|
'earned' => $longestStreak >= 7,
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'iron_will',
|
||
|
|
'name' => 'Iron Will',
|
||
|
|
'description' => 'Reach a 30-day training streak.',
|
||
|
|
'earned' => $longestStreak >= 30,
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'century_club',
|
||
|
|
'name' => 'Century Club',
|
||
|
|
'description' => 'Log 100kg or more on a completed set.',
|
||
|
|
'earned' => $hasCenturyLift,
|
||
|
|
],
|
||
|
|
];
|
||
|
|
|
||
|
|
return [
|
||
|
|
'current_streak' => $currentStreak,
|
||
|
|
'longest_streak' => $longestStreak,
|
||
|
|
'badges' => $badges,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param \Illuminate\Support\Collection<int, string> $completedDates Sorted, unique Y-m-d strings.
|
||
|
|
*/
|
||
|
|
private static function currentStreak($completedDates): int
|
||
|
|
{
|
||
|
|
if ($completedDates->isEmpty()) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
$dateSet = $completedDates->flip();
|
||
|
|
$cursor = Carbon::today();
|
||
|
|
|
||
|
|
// A missed "today" doesn't break the streak until tomorrow - only walk
|
||
|
|
// back from yesterday if today has no completed session yet.
|
||
|
|
if (!$dateSet->has($cursor->toDateString())) {
|
||
|
|
$cursor = $cursor->copy()->subDay();
|
||
|
|
}
|
||
|
|
|
||
|
|
$streak = 0;
|
||
|
|
while ($dateSet->has($cursor->toDateString())) {
|
||
|
|
$streak++;
|
||
|
|
$cursor = $cursor->copy()->subDay();
|
||
|
|
}
|
||
|
|
|
||
|
|
return $streak;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param \Illuminate\Support\Collection<int, string> $completedDates Sorted, unique Y-m-d strings.
|
||
|
|
*/
|
||
|
|
private static function longestStreak($completedDates): int
|
||
|
|
{
|
||
|
|
$longest = 0;
|
||
|
|
$run = 0;
|
||
|
|
$previous = null;
|
||
|
|
|
||
|
|
foreach ($completedDates as $dateStr) {
|
||
|
|
$date = Carbon::parse($dateStr);
|
||
|
|
$run = ($previous && (int) abs($date->diffInDays($previous)) === 1) ? $run + 1 : 1;
|
||
|
|
$longest = max($longest, $run);
|
||
|
|
$previous = $date;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $longest;
|
||
|
|
}
|
||
|
|
}
|