Skip to content

Commit

Permalink
Merge pull request #90 from cjmellor/feat/revoke-achievment
Browse files Browse the repository at this point in the history
feat: Add Revoke Achievmenet
  • Loading branch information
cjmellor authored Nov 10, 2024
2 parents e9a8ca2 + 8200c99 commit 2a46856
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 7 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,18 @@ Achievement::create([
]);
```

### Revoke Achievement

You can revoke an achievement from a user using the `revokeAchievement` method:

```php
$user->revokeAchievement($achievement);
```

The method will throw an exception if you try to revoke an achievement that the user doesn't have. You can revoke both standard and secret achievements, and it will also remove any associated progress.

When an achievement is revoked, a `AchievementRevoked` event is dispatched.

### Gain Achievement

To use Achievements in your User model, you must first add the Trait.
Expand Down Expand Up @@ -478,6 +490,13 @@ public Model $user,
> [!NOTE]
> This event only runs if the progress of the Achievement is 100%
**AchievementRevoked** - When an Achievement is detached from the User

```php
public Achievement $achievement,
public Model $user,
```

**AchievementProgressionIncreased** - When a Users’ progression for an Achievement is increased.

```php
Expand Down
27 changes: 21 additions & 6 deletions src/Concerns/HasAchievements.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use LevelUp\Experience\Events\AchievementAwarded;
use LevelUp\Experience\Events\AchievementProgressionIncreased;
use LevelUp\Experience\Events\AchievementRevoked;
use LevelUp\Experience\Models\Achievement;

trait HasAchievements
Expand All @@ -30,6 +31,12 @@ public function grantAchievement(Achievement $achievement, $progress = null): vo
$this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this)));
}

public function allAchievements(): BelongsToMany
{
return $this->belongsToMany(related: config(key: 'level-up.models.achievement'))
->withPivot(columns: 'progress');
}

public function achievements(): BelongsToMany
{
return $this->belongsToMany(related: config(key: 'level-up.models.achievement'))
Expand All @@ -50,12 +57,6 @@ public function incrementAchievementProgress(Achievement $achievement, int $amou
return $newProgress;
}

public function allAchievements(): BelongsToMany
{
return $this->belongsToMany(related: config(key: 'level-up.models.achievement'))
->withPivot(columns: 'progress');
}

public function achievementsWithProgress(): BelongsToMany
{
return $this->belongsToMany(related: config(key: 'level-up.models.achievement'))
Expand All @@ -70,4 +71,18 @@ public function secretAchievements(): BelongsToMany
->withPivot(columns: 'progress')
->where('is_secret', true);
}

/**
* Revoke an achievement from the user
*/
public function revokeAchievement(Achievement $achievement): void
{
if (! $this->allAchievements()->find($achievement->id)) {
throw new Exception(message: 'User does not have this Achievement');
}

$this->achievements()->detach($achievement->id);

event(new AchievementRevoked(achievement: $achievement, user: $this));
}
}
19 changes: 19 additions & 0 deletions src/Events/AchievementRevoked.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace LevelUp\Experience\Events;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use LevelUp\Experience\Models\Achievement;

class AchievementRevoked
{
use Dispatchable;

public function __construct(
public Achievement $achievement,
public Model $user,
) {
//
}
}
54 changes: 53 additions & 1 deletion tests/Concerns/HasAchievementsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use Illuminate\Support\Facades\Event;
use LevelUp\Experience\Events\AchievementAwarded;
use LevelUp\Experience\Events\AchievementProgressionIncreased;
use LevelUp\Experience\Events\AchievementRevoked;
use LevelUp\Experience\Models\Achievement;

beforeEach(closure: fn (): Achievement => $this->achievement = Achievement::factory()->create());
Expand Down Expand Up @@ -105,9 +106,60 @@
]);
});

test('a User cannot be granted the same Achievement twice', closure: function () {
test(description: 'a User cannot be granted the same Achievement twice', closure: function () {
$this->user->grantAchievement($this->achievement);
$this->user->grantAchievement($this->achievement);

expect($this->user)->achievements->toHaveCount(count: 1);
})->throws(exception: Exception::class, exceptionMessage: 'User already has this Achievement');

test(description: 'a User can have an Achievement revoked', closure: function (): void {
// First grant the achievement
$this->user->grantAchievement($this->achievement);

expect($this->user)->achievements->toHaveCount(count: 1);

// Now revoke it
$this->user->revokeAchievement($this->achievement);

expect($this->user->fresh())
->achievements->toHaveCount(count: 0)
->and($this->user->achievements()->where('achievement_id', $this->achievement->id)->exists())->toBeFalse();

$this->assertDatabaseMissing(table: 'achievement_user', data: [
'user_id' => $this->user->id,
'achievement_id' => $this->achievement->id,
]);
});

test(description: 'an Event runs when an Achievement is revoked', closure: function (): void {
Event::fake();

$this->user->grantAchievement($this->achievement);
$this->user->revokeAchievement($this->achievement);

Event::assertDispatched(AchievementRevoked::class);
});

it(description: 'throws an exception when revoking an unearned Achievement', closure: function (): void {
expect(fn () => $this->user->revokeAchievement($this->achievement))
->toThrow(exception: Exception::class, exceptionMessage: 'User does not have this Achievement');
});

test(description: 'revoking an Achievement with progress removes the progress', closure: function (): void {
$this->user->grantAchievement($this->achievement, 50);
expect($this->user->achievementsWithProgress)->toHaveCount(count: 1);

$this->user->revokeAchievement($this->achievement);
expect($this->user->fresh()->achievementsWithProgress)->toHaveCount(count: 0);
});

test(description: 'revoking a secret Achievement works correctly', closure: function (): void {
$secretAchievement = Achievement::factory()->secret()->create();
$this->user->grantAchievement($secretAchievement);

expect($this->user)->secretAchievements->toHaveCount(count: 1);

$this->user->revokeAchievement($secretAchievement);
expect($this->user->fresh())->secretAchievements->toHaveCount(count: 0);
});

0 comments on commit 2a46856

Please sign in to comment.