Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 60 additions & 25 deletions src/Concerns/GiveExperience.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ public function addPoints(

[$amount, $appliedMultipliers] = $this->resolveMultipliers($amount, $multiplier);

if ($this->experience()->doesntExist()) {
$experience = $this->loadedExperience();

if (! $experience) {
$startingLevel = config(key: 'level-up.starting_level');

$level = $levelClass::query()
Expand All @@ -54,82 +56,86 @@ public function addPoints(
);
}

$this->experience()->create(attributes: [
$experience = $this->experienceRelation()->create(attributes: [
'level_id' => $level->id,
'experience_points' => $amount,
]);

$this->load('experience');
$this->setRelation('experience', $experience);

$this->dispatchEvent($amount, $type, $reason, $appliedMultipliers, $multiplier);
$this->dispatchEvent($experience, $amount, $type, $reason, $appliedMultipliers, $multiplier);

if ($level->level > $startingLevel) {
for ($lvl = $startingLevel; $lvl <= $level->level; $lvl++) {
event(new UserLevelledUp(user: $this, level: $lvl));
}
}

return $this->experience;
return $experience;
}

if ($this->levelCapExceedsUserLevel()) {
return $this->experience;
return $experience;
}

$this->experience->increment(column: 'experience_points', amount: $amount);
$experience->increment(column: 'experience_points', amount: $amount);

$this->dispatchEvent($amount, $type, $reason, $appliedMultipliers, $multiplier);
$this->dispatchEvent($experience, $amount, $type, $reason, $appliedMultipliers, $multiplier);

return $this->experience;
return $experience;
}

public function experience(): HasOne
{
return $this->hasOne(related: config('level-up.models.experience'));
return $this->experienceRelation();
}

public function getLevel(): int
{
return $this->experience?->status?->level ?? 0;
return $this->loadedExperience()?->status?->level ?? 0;
}

public function experienceHistory(): HasMany
{
return $this->hasMany(related: config('level-up.models.experience_audit'));
return $this->experienceHistoryRelation();
}

public function deductPoints(int $amount, ?string $reason = null): Experience
{
throw_unless($this->experience()->exists(), Exception::class, 'User has no experience record.');
$experience = $this->loadedExperience();

$this->experience->decrement(column: 'experience_points', amount: $amount);
throw_unless($experience, Exception::class, 'User has no experience record.');

$experience->decrement(column: 'experience_points', amount: $amount);

event(new PointsDecreased(
pointsDecreasedBy: $amount,
totalPoints: $this->experience->experience_points,
totalPoints: $experience->experience_points,
reason: $reason,
user: $this,
));

return $this->experience;
return $experience;
}

public function setPoints(int $amount): Experience
{
throw_unless($this->experience()->exists(), Exception::class, message: 'User has no experience record.');
$experience = $this->loadedExperience();

throw_unless($experience, Exception::class, message: 'User has no experience record.');

$this->experience->update(attributes: [
$experience->update(attributes: [
'experience_points' => $amount,
]);

return $this->experience;
return $experience;
}

public function nextLevelAt(?int $checkAgainst = null, bool $showAsPercentage = false): int
{
$levelClass = config(key: 'level-up.models.level');

$nextLevel = $levelClass::firstWhere(column: 'level', operator: '=', value: is_null($checkAgainst) ? $this->getLevel() + 1 : $checkAgainst);
$nextLevel = $levelClass::firstWhere(column: 'level', operator: '=', value: $checkAgainst ?? $this->getLevel() + 1);

if ($this->levelCapExceedsUserLevel()) {
return 0;
Expand Down Expand Up @@ -159,7 +165,7 @@ public function nextLevelAt(?int $checkAgainst = null, bool $showAsPercentage =

public function getPoints(): int
{
return $this->experience?->experience_points ?? 0;
return $this->loadedExperience()?->experience_points ?? 0;
}

public function levelUp(int $to): void
Expand All @@ -173,10 +179,14 @@ public function levelUp(int $to): void

throw_unless($level, InvalidArgumentException::class, "Level {$to} does not exist.");

$experience = $this->loadedExperience();

throw_unless($experience, Exception::class, message: 'User has no experience record.');

$previousLevel = $this->getLevel();

$this->experience->status()->associate(model: $level);
$this->experience->save();
$experience->status()->associate(model: $level);
$experience->save();

for ($lvl = $previousLevel + 1; $lvl <= $to; $lvl++) {
event(new UserLevelledUp(user: $this, level: $lvl));
Expand Down Expand Up @@ -237,13 +247,13 @@ protected function applyStackingStrategy(int $amount, Collection $multiplierValu
});
}

protected function dispatchEvent(int $amount, string $type, ?string $reason, ?Collection $appliedMultipliers = null, int|float|null $inlineMultiplier = null): void
protected function dispatchEvent(Experience $experience, int $amount, string $type, ?string $reason, ?Collection $appliedMultipliers = null, int|float|null $inlineMultiplier = null): void
{
$auditData = $this->buildMultiplierAuditData($appliedMultipliers, $inlineMultiplier);

event(new PointsIncreased(
pointsAdded: $amount,
totalPoints: $this->experience->experience_points,
totalPoints: $experience->experience_points,
type: $type,
reason: $reason,
user: $this,
Expand Down Expand Up @@ -275,4 +285,29 @@ protected function levelCapExceedsUserLevel(): bool
&& $this->getLevel() >= config(key: 'level-up.level_cap.level')
&& ! config(key: 'level-up.level_cap.points_continue');
}

private function experienceRelation(): HasOne
{
return $this->hasOne(related: config('level-up.models.experience'));
}

private function experienceHistoryRelation(): HasMany
{
return $this->hasMany(related: config('level-up.models.experience_audit'));
}

private function loadedExperience(): ?Experience
{
if ($this->relationLoaded('experience')) {
return $this->getRelation('experience');
}

$experience = $this->experienceRelation()->first();

if ($experience !== null) {
$this->setRelation('experience', $experience);
}

return $experience;
}
}
29 changes: 17 additions & 12 deletions src/Concerns/HasChallenges.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ trait HasChallenges
{
public function challenges(): BelongsToMany
{
return $this->belongsToMany(related: config(key: 'level-up.models.challenge'), table: 'challenge_user')
->using(config(key: 'level-up.models.challenge_user'))
->withPivot(columns: ['progress', 'completed_at'])
->withTimestamps();
return $this->challengesRelation();
}

/**
Expand All @@ -39,10 +36,10 @@ public function enrollInChallenge(Challenge $challenge): void
message: 'This challenge has expired.',
);

$existingPivot = $this->challenges()->where('challenge_id', $challenge->id)->first()?->pivot;
$existingPivot = $this->challengesRelation()->where('challenge_id', $challenge->id)->first()?->pivot;

if ($existingPivot && $existingPivot->completed_at !== null && $challenge->is_repeatable) {
$this->challenges()->updateExistingPivot($challenge->id, [
$this->challengesRelation()->updateExistingPivot($challenge->id, [
'progress' => $this->freshChallengeProgress($challenge),
'completed_at' => null,
]);
Expand All @@ -61,7 +58,7 @@ public function enrollInChallenge(Challenge $challenge): void
);

try {
$this->challenges()->attach($challenge->id, [
$this->challengesRelation()->attach($challenge->id, [
'progress' => $this->freshChallengeProgress($challenge),
]);
} catch (UniqueConstraintViolationException) {
Expand All @@ -76,7 +73,7 @@ public function enrollInChallenge(Challenge $challenge): void
*/
public function unenrollFromChallenge(Challenge $challenge): void
{
$pivot = $this->challenges()->where('challenge_id', $challenge->id)->first()?->pivot;
$pivot = $this->challengesRelation()->where('challenge_id', $challenge->id)->first()?->pivot;

throw_if(
condition: ! $pivot,
Expand All @@ -90,26 +87,26 @@ public function unenrollFromChallenge(Challenge $challenge): void
message: 'Cannot unenroll from a completed challenge.',
);

$this->challenges()->detach($challenge->id);
$this->challengesRelation()->detach($challenge->id);

event(new ChallengeUnenrolled(challenge: $challenge, user: $this));
}

public function activeChallenges(): BelongsToMany
{
return $this->challenges()
return $this->challengesRelation()
->whereNull(columns: 'challenge_user.completed_at');
}

public function completedChallenges(): BelongsToMany
{
return $this->challenges()
return $this->challengesRelation()
->whereNotNull(columns: 'challenge_user.completed_at');
}

public function getChallengeProgress(Challenge $challenge): ?array
{
$pivot = $this->challenges()->where('challenge_id', $challenge->id)->first()?->pivot;
$pivot = $this->challengesRelation()->where('challenge_id', $challenge->id)->first()?->pivot;

return $pivot?->progress;
}
Expand All @@ -127,6 +124,14 @@ public function getChallengeCompletionPercentage(Challenge $challenge): ?float
return round(num: ($completed / count($progress)) * 100, precision: 1);
}

private function challengesRelation(): BelongsToMany
{
return $this->belongsToMany(related: config(key: 'level-up.models.challenge'), table: 'challenge_user')
->using(config(key: 'level-up.models.challenge_user'))
->withPivot(columns: ['progress', 'completed_at'])
->withTimestamps();
}

private function freshChallengeProgress(Challenge $challenge): array
{
return resolve(ChallengeService::class)->initializeProgress(
Expand Down
19 changes: 12 additions & 7 deletions src/Concerns/HasStreaks.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ public function recordStreak(Activity $activity): void
if ($diffInDays > 1) {
$this->resetStreak($activity);

event(new StreakBroken($this, $activity, $this->streaks()->whereBelongsTo(related: $activity)->first()));
event(new StreakBroken($this, $activity, $this->streaksRelation()->whereBelongsTo(related: $activity)->first()));

return;
}

$streak = $this->streaks()->whereBelongsTo(related: $activity);
$streak = $this->streaksRelation()->whereBelongsTo(related: $activity);
$streak->increment(column: 'count');
$streak->update(values: ['activity_at' => now()]);

Expand All @@ -53,7 +53,7 @@ public function recordStreak(Activity $activity): void

public function streaks(): HasMany
{
return $this->hasMany(related: config('level-up.models.streak'));
return $this->streaksRelation();
}

public function resetStreak(Activity $activity): void
Expand All @@ -62,7 +62,7 @@ public function resetStreak(Activity $activity): void
$this->archiveStreak($activity);
}

$this->streaks()
$this->streaksRelation()
->whereBelongsTo(related: $activity)
->update([
'count' => 1,
Expand All @@ -72,7 +72,7 @@ public function resetStreak(Activity $activity): void

public function getCurrentStreakCount(Activity $activity): int
{
return $this->streaks()->whereBelongsTo(related: $activity)->first()?->count ?? 0;
return $this->streaksRelation()->whereBelongsTo(related: $activity)->first()?->count ?? 0;
}

public function hasStreakToday(Activity $activity): bool
Expand Down Expand Up @@ -119,7 +119,7 @@ public function isStreakFrozen(Activity $activity): bool

protected function hasStreakForActivity(Activity $activity): bool
{
return $this->streaks()
return $this->streaksRelation()
->whereBelongsTo(related: $activity)
->exists();
}
Expand All @@ -140,7 +140,7 @@ protected function startNewStreak(Activity $activity): Model|Streak

protected function getStreakLastActivity(Activity $activity): ?Streak
{
return $this->streaks()
return $this->streaksRelation()
->whereBelongsTo(related: $activity)
->latest(column: 'activity_at')
->first();
Expand Down Expand Up @@ -183,4 +183,9 @@ protected function archiveStreak(Activity $activity): void
'ended_at' => $latestStreak->activity_at,
]);
}

private function streaksRelation(): HasMany
{
return $this->hasMany(related: config('level-up.models.streak'));
}
}
Loading
Loading