- name
- level-up-development
- description
- Build and work with cjmellor/level-up features, including XP, levels, tiers, achievements, streaks, multipliers, challenges, leaderboards, leagues, and auditing.
## When to use this skill
Use this skill when working with gamification features — adding experience points, levels, tiers, achievements, streaks, multipliers, leaderboards, leagues, or auditing — using cjmellor/level-up.
## Core Concepts
- **XP and Levels** — Users earn experience points (XP) and automatically progress through a defined level structure.
- **Tiers** — Named status brackets (e.g. Bronze, Silver, Gold) based on XP thresholds, independent of numeric levels.
- **Achievements** — Unlockable rewards, optionally with progress tracking, optionally gated by tier.
- **Streaks** — Track consecutive daily activities with freeze support.
- **Multipliers** — Database-backed point modifiers with scoping, scheduling, and configurable stacking strategies.
- **Leaderboard** — Rank users by any metric (XP by default) with competition-style rank numbers, optionally scoped to a tier or a time period (day/week/month/custom).
- **Leagues** — A competitive cycle on one periodic Board: active users are grouped into small Cohorts within a Division each period and ranked within their Cohort. A Division is competition history, NOT a Tier (which is XP status) — a user holds both independently.
- **Auditing** — Automatic history of all XP changes, level-ups, and tier changes.
## Setup
### Install
```bash
composer require cjmellor/level-up
php artisan vendor:publish --tag="level-up-migrations"
php artisan migrate
php artisan vendor:publish --tag="level-up-config"
```
### Add Traits to the User Model
Add only the traits you need:
```php
use LevelUp\Experience\Concerns\GiveExperience;
use LevelUp\Experience\Concerns\HasAchievements;
use LevelUp\Experience\Concerns\HasChallenges;
use LevelUp\Experience\Concerns\HasLeagues;
use LevelUp\Experience\Concerns\HasStreaks;
use LevelUp\Experience\Concerns\HasTiers;
class User extends Authenticatable
{
use GiveExperience; // Required — XP and levels
use HasAchievements; // Optional — achievements
use HasStreaks; // Optional — streaks
use HasTiers; // Optional — tiers
use HasChallenges; // Optional — challenges
use HasLeagues; // Optional — leagues (divisions/cohorts)
}
```
`GiveExperience` is the foundation. The others are opt-in.
## Levels
### Define Levels
```php
use LevelUp\Experience\Models\Level;
Level::add(
['level' => 1, 'next_level_experience' => null],
['level' => 2, 'next_level_experience' => 100],
['level' => 3, 'next_level_experience' => 250],
['level' => 4, 'next_level_experience' => 500],
['level' => 5, 'next_level_experience' => 1000],
);
```
Level 1 must have `next_level_experience` set to `null` — it is the default starting point. Users level up automatically when their XP reaches the threshold. Throws `LevelExistsException` if a level number already exists.
### Level Queries
```php
$user->getLevel(); // Current level number (int), 0 if no experience
$user->getPoints(); // Current XP total (int), 0 if no experience
$user->nextLevelAt(); // XP remaining until next level (int)
$user->nextLevelAt(checkAgainst: 5); // XP remaining until level 5
$user->nextLevelAt(showAsPercentage: true); // Progress as 0-100 percentage
```
### Manual Level Up
```php
$user->levelUp(to: 5); // Jump to level 5
```
Throws `InvalidArgumentException` if the level does not exist. Fires `UserLevelledUp` for each intermediate level gained. Respects the level cap.
### Level Cap
Configured in `config/level-up.php`:
```php
'level_cap' => [
'enabled' => env('LEVEL_CAP_ENABLED', true),
'level' => env('LEVEL_CAP', 100),
'points_continue' => env('LEVEL_CAP_POINTS_CONTINUE', true),
],
```
When the cap is reached, the user stops levelling. If `points_continue` is `true`, XP still accumulates. If `false`, XP stops accumulating too.
## Experience Points (XP)
### Add Points
```php
$user->addPoints(50);
$user->addPoints(50, reason: 'Completed tutorial');
$user->addPoints(50, multiplier: 2);
$user->addPoints(50, type: AuditType::Add->value, reason: 'Bonus');
```
Creates an experience record if none exists, otherwise increments. Automatically levels up if the threshold is crossed. Points past the highest level's `next_level_experience` cap the user at the top level (no exception).
### Deduct Points
```php
$user->deductPoints(30);
$user->deductPoints(30, reason: 'Penalty');
```
Throws `Exception` if the user has no experience record.
### Set Points
```php
$user->setPoints(500);
```
Directly overwrites the XP total, then recalculates level and tier from the new total (firing `UserLevelledUp` / `UserTierUpdated` as needed). Throws `Exception` if the user has no experience record. Writes no audit record — it is an administrative override, invisible to periodic leaderboards.
### Get Points
```php
$user->getPoints(); // int, returns 0 if no experience record
```
## Multipliers
Multipliers are database-backed records that modify point calculations. They can be scoped to specific users or tiers, scheduled with time windows, and configured to stack using different strategies.
### Create Multipliers
```php
use LevelUp\Experience\Models\Multiplier;
Multiplier::create([
'name' => 'Weekend Bonus',
'multiplier' => 2.0,
'is_active' => true,
'starts_at' => now()->startOfWeekend(),
'expires_at' => now()->endOfWeekend(),
]);
```
Active multipliers are automatically applied when `addPoints()` is called. The `multiplier` value must be at least `0.01`. If both `starts_at` and `expires_at` are set, `starts_at` must be before `expires_at`.
### Scope Multipliers
Multipliers with no scopes apply to all users. Use the typed `scopeToUser` / `scopeToTier` methods to restrict:
```php
$multiplier->scopeToUser($user); // Specific user only
$multiplier->scopeToTier($goldTier); // Gold tier users only
$multiplier->scopeToUser($user)->scopeToTier($tier); // Both — chains via fluent return
// Variadic — attach multiple of the same type in one call
$multiplier->scopeToUser($alice, $bob, $carol);
$multiplier->scopeToTier($silver, $gold, $platinum);
// Reverse the scope
$multiplier->unscopeFromUser($user);
$multiplier->unscopeFromTier($goldTier);
// Check whether a multiplier has any scopes attached
$multiplier->isGlobal(); // true when no users or tiers attached
```
`scopeToUser` and `scopeToTier` are idempotent (they use `syncWithoutDetaching` internally) — calling them twice with the same model does not create duplicates.
### Inline Multiplier
```php
$user->addPoints(amount: 10, multiplier: 3); // 30 points
```
Inline multipliers stack with DB multipliers according to the configured strategy.
### Stacking Strategies
Configure in `config/level-up.php`:
```php
'multiplier' => [
'enabled' => env('MULTIPLIER_ENABLED', true),
'stack_strategy' => env('MULTIPLIER_STACK', 'compound'),
// 'compound' — multipliers multiply each other: 2 × 5 = 10x
// 'additive' — multipliers sum: 2 + 5 = 7x
// 'highest' — only the largest applies: max(2, 5) = 5x
],
```
### Query Multipliers
```php
Multiplier::active()->get(); // Currently active
Multiplier::active()->forUser($user)->get(); // Active for a specific user
Multiplier::scheduled()->get(); // Future (starts_at > now)
Multiplier::expired()->get(); // Past (expires_at < now)
```
### MultiplierApplied Event
Fires when multipliers modify a point calculation. Properties: `Model $user`, `Collection $multipliers`, `int $originalAmount`, `int $finalAmount`, `string $strategy`.
## Tiers
### Define Tiers
```php
use LevelUp\Experience\Models\Tier;
Tier::add(
['name' => 'Bronze', 'experience' => 0],
['name' => 'Silver', 'experience' => 500],
['name' => 'Gold', 'experience' => 2000],
['name' => 'Platinum', 'experience' => 5000, 'metadata' => ['color' => '#E5E4E2', 'icon' => 'crown']],
);
```
Tier names and experience values must be unique. Throws `TierExistsException` on duplicates. The `metadata` column is a flexible JSON field for any extra data (colours, icons, descriptions). The entire `add()` call is wrapped in a database transaction — if any tier fails, none are created.
### Automatic Tier Promotion
Tiers update automatically when XP changes. When `addPoints()` causes the user to cross a tier threshold, `experience.tier_id` is updated and a `UserTierUpdated` event fires with `TierDirection::Promoted`.
### Query Tiers
```php
$user->getTier(); // Current Tier model or null
$user->getNextTier(); // Next tier above current (Tier or null)
$user->tierProgress(); // Percentage 0-100 through current bracket
$user->nextTierAt(); // XP remaining until next tier
$user->isAtTier('Gold'); // Exact match (bool)
$user->isAtOrAboveTier('Silver'); // At or above (bool)
```
`getTier()` returns `null` if the user has no experience record or tiers are disabled.
### Demotion
By default, tiers use a **high-water mark** — once earned, they persist even if points decrease. Enable demotion to allow tier drops:
```
TIER_DEMOTION=true
```
When enabled, `deductPoints()` checks if the user should drop and fires `UserTierUpdated` with `TierDirection::Demoted`. The `newTier` property is nullable — it will be `null` if the user drops below all tier thresholds.
### Tier-Scoped Multipliers
Create a multiplier and scope it to a tier so it only applies to users at that tier:
```php
$multiplier = Multiplier::create([
'name' => 'Gold Bonus',
'multiplier' => 2.0,
'is_active' => true,
]);
$goldTier = Tier::where('name', 'Gold')->first();
$multiplier->scopeToTier($goldTier);
```
When a Gold-tier user calls `addPoints()`, this multiplier is automatically included.
### Tier-Gated Achievements
Restrict achievements so only users at a certain tier can earn them:
```php
$goldTier = Tier::where('name', 'Gold')->first();
Achievement::create([
'name' => 'Golden Streak',
'tier_id' => $goldTier->id,
]);
```
Attempting to grant to a user below Gold throws `TierRequirementNotMet`.
### Tier-Scaled Streak Freezes
Higher tiers get longer freeze durations:
```php
// config/level-up.php
'tiers' => [
'streak_freeze_days' => [
'Bronze' => 1,
'Silver' => 2,
'Gold' => 3,
'Platinum' => 7,
],
],
```
Falls back to the global `freeze_duration` if the tier is not listed or tiers are disabled.
### Tier-Scoped Leaderboards
```php
use LevelUp\Experience\Facades\Leaderboard;
Leaderboard::forTier('Gold')->generate();
Leaderboard::forTier($tierModel)->generate();
```
### Tier Config
```php
'tiers' => [
'enabled' => env('TIERS_ENABLED', true),
'demotion' => env('TIER_DEMOTION', false),
'streak_freeze_days' => [],
],
```
## Achievements
### Create Achievements
```php
use LevelUp\Experience\Models\Achievement;
Achievement::create([
'name' => 'First Login',
'is_secret' => false,
'description' => 'Log in for the first time',
'image' => 'storage/app/achievements/first-login.png',
]);
// Secret achievement (hidden until earned)
Achievement::create([
'name' => 'Hidden Gem',
'is_secret' => true,
]);
// Tier-gated achievement
Achievement::create([
'name' => 'Gold Member Badge',
'tier_id' => $goldTier->id,
]);
```
### Grant Achievement
```php
$user->grantAchievement($achievement);
// With progress (0-100)
$user->grantAchievement($achievement, progress: 50);
```
Throws `Exception` if progress exceeds 100, or if the user already has the achievement. Throws `TierRequirementNotMet` if tier-gated and user does not meet the tier requirement. `AchievementAwarded` event fires only when progress is `null` or `100`.
### Revoke Achievement
```php
View on GitHub