Gamification System
Watermelon uses a dual-loop progression system to keep users engaged: XP-driven level progression and rank-tier prestige.
XP System
Earning XP
| Action | XP | Note |
|---|---|---|
| Play a song | +5 | Minimum 30s listen |
| Complete a song | +15 | Listen to >80% of track |
| Daily login streak | +20 | Multiplied by streak multiplier |
| Create a playlist | +25 | Per new playlist |
| Discover new artist | +10 | Artist not played before |
| Maintain streak (3d) | ×1.5 | 3-consecutive-day bonus |
| Maintain streak (7d) | ×2.0 | Weekly bonus |
| Maintain streak (14d) | ×2.5 | Fortnight bonus |
| Maintain streak (30d) | ×3.0 | Monthly bonus |
Level Formula
XP Level = GREATEST(1, FLOOR(SQRT(xp_total / 100.0)))| Level | XP Required | Multiplier |
|---|---|---|
| 1 | 0 | 1.0× |
| 2 | 100 | 1.1× |
| 3 | 400 | 1.2× |
| 5 | 2,500 | 1.4× |
| 10 | 10,000 | 1.9× |
| 20 | 40,000 | 2.9× |
| 30 | 90,000 | 3.9× |
The multiplier visually reinforces progression even at low levels.
XP Descriptor
fun xpDescriptor(xp: Int): String = when (xp) {
0 -> "Fresh listener ✨"
in 1..(100 / 2) -> "Gaining momentum 🔁"
in (100 / 2)..(100 * 2) -> "Solid start 👊"
in (100 * 2)..(100 * 5) -> "Groove locked ✨"
in (100 * 5)..(100 * 10) -> "Addicted 🎶"
else -> "Music is life 💖"
}Rank Tiers
Rank is computed automatically from hours listened via PostgreSQL triggers.
| Hours | Tier Name | Emoji |
|---|---|---|
| 0 | Seed Listener | 🌱 |
| 5 | Sprout Wave | 🍃 |
| 15 | Pulse Rider | 🎧 |
| 35 | Echo Drift | 🌊 |
| 60 | Resonance | 🎶 |
| 100 | Vinyl Hunter | 📀 |
| 160 | Frequency Soul | 🎵 |
| 250 | NovaBeat | 🌌 |
| 350 | Harmonic Flow | 💿 |
| 500 | Reverb X | 🔥 |
| 700 | Soundrift | ⚡ |
| 950 | Celestia Tone | 🌠 |
| 1200 | Wave Architect | 🎼 |
| 1600 | Spectrum Lord | 🌈 |
| 2000 | Eternal Echo | 👑 |
Achievement Badges
Stored in achievements table and unlocked via triggers on user_actions and play_sessions.
Unlock Rules (trigger-driven)
| Badge | Trigger Condition |
|---|---|
| 🎵 First Listen | First PLAY action |
| 🦉 Night Owl | Play between 02:00–05:00 |
| 🧭 Explorer | 10 unique artists in history |
| 📋 Playlist Pro | Create 5 playlists |
| 🐦 Early Bird | Play before 07:00 |
| 🏃 Marathoner | hours_listened >= 50 |
Badge Display
// ProfileScreen.kt — vertical card with emoji + description
Card(
modifier = Modifier.size(170.dp, 120.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)
)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(badge.emoji, fontSize = 32.sp)
Text(badge.name, fontWeight = FontWeight.Bold)
Text(badge.description, fontSize = 11.sp)
}
}Streak System
- Streak = consecutive days with at least one play session
- Tracked in
profiles.streak_days - Daily notification: “Your streak is at 3 days 🔥“
Leaderboard
Updated hourly via cron or trigger:
SELECT
id,
display_name,
email,
hours_listened,
songs_played,
rank_tier,
xp_level
FROM profiles
WHERE is_banned = false
ORDER BY hours_listened DESC
LIMIT 50;Live at: /leaderboard
Profile Screen
The profile screen surfaces all gamification data:
- Header — Avatar (DiceBear), name, rank badge
- Stats Grid — Rank, Level, Hours, Songs, Multiplier
- Streak — Current / longest streak
- Achievements — Horizontal scrolling badge cards
- XPSection — Progress bar to next level with descriptor
For the SQL migration scripts, see the Database Schema guide.