feat(#22): gamificação RPG — XP, quests, conquistas, cosméticos e personagem SVG pixel art

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-26 21:46:41 -03:00
co-authored by Claude Sonnet 4.6
parent 9a964423fd
commit c1964ea036
21 changed files with 1346 additions and 10 deletions
+98
View File
@@ -0,0 +1,98 @@
package service_test
import (
"context"
"testing"
"financeiro-carvalho/internal/model"
"financeiro-carvalho/internal/service"
)
type mockGameRepo struct {
profile model.PlayerProfile
achievements map[string]bool
}
func newMockGameRepo() *mockGameRepo {
return &mockGameRepo{
profile: model.PlayerProfile{ID: 1, Level: 1, XP: 0, XPToNext: 100},
achievements: map[string]bool{},
}
}
func (m *mockGameRepo) GetProfile(_ context.Context) (*model.PlayerProfile, error) {
cp := m.profile
return &cp, nil
}
func (m *mockGameRepo) AddXP(_ context.Context, _ string, amount int, _ string) (*model.PlayerProfile, error) {
m.profile.XP += amount
for m.profile.XP >= m.profile.XPToNext {
m.profile.XP -= m.profile.XPToNext
m.profile.Level++
m.profile.XPToNext = m.profile.Level * m.profile.Level * 100
}
cp := m.profile
return &cp, nil
}
func (m *mockGameRepo) ListActiveQuests(_ context.Context) ([]model.PlayerQuest, error) {
return nil, nil
}
func (m *mockGameRepo) EnsurePlayerQuest(_ context.Context, _ int, _ string) (int, error) { return 1, nil }
func (m *mockGameRepo) IncrementQuestCount(_ context.Context, _, _ string, _ int) error { return nil }
func (m *mockGameRepo) UpdateQuestPct(_ context.Context, _ string, _ float64) error { return nil }
func (m *mockGameRepo) ClaimQuest(_ context.Context, _ int) (int, error) { return 50, nil }
func (m *mockGameRepo) ListAchievements(_ context.Context) ([]model.Achievement, error) { return nil, nil }
func (m *mockGameRepo) IsAchievementUnlocked(_ context.Context, c string) (bool, error) {
return m.achievements[c], nil
}
func (m *mockGameRepo) UnlockAchievement(_ context.Context, c string) (int, error) {
m.achievements[c] = true
return 100, nil
}
func (m *mockGameRepo) ListCosmetics(_ context.Context) ([]model.Cosmetic, error) { return nil, nil }
func (m *mockGameRepo) EquipCosmetic(_ context.Context, _ int) error { return nil }
func (m *mockGameRepo) UnlockCosmeticsForLevel(_ context.Context, _ int) error { return nil }
func TestGame_XPAccumulates(t *testing.T) {
repo := newMockGameRepo()
svc := service.NewGameService(repo)
p, err := svc.AwardXP(context.Background(), "test", 50, "test")
if err != nil {
t.Fatal(err)
}
if p.XP != 50 {
t.Errorf("expected xp=50, got %d", p.XP)
}
}
func TestGame_LevelUpAtThreshold(t *testing.T) {
repo := newMockGameRepo()
svc := service.NewGameService(repo)
// level 1 threshold is 100 XP
p, err := svc.AwardXP(context.Background(), "test", 100, "test")
if err != nil {
t.Fatal(err)
}
if p.Level != 2 {
t.Errorf("expected level=2 after 100 XP, got %d", p.Level)
}
if p.XP != 0 {
t.Errorf("expected remaining xp=0, got %d", p.XP)
}
}
func TestGame_AchievementNotDuplicated(t *testing.T) {
repo := newMockGameRepo()
svc := service.NewGameService(repo)
svc.CheckAndUnlockAchievement(context.Background(), "first_transaction")
svc.CheckAndUnlockAchievement(context.Background(), "first_transaction")
// second call should be a no-op — XP only awarded once
// repo tracks it as unlocked after first call
if !repo.achievements["first_transaction"] {
t.Error("achievement should be marked unlocked")
}
}