Files
carvalho-finances/apps/api/internal/repository/game.go
T

283 lines
8.0 KiB
Go

package repository
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"financeiro-carvalho/internal/model"
)
type GameRepository struct {
pool *pgxpool.Pool
}
func NewGameRepository(pool *pgxpool.Pool) *GameRepository {
return &GameRepository{pool: pool}
}
func (r *GameRepository) GetProfile(ctx context.Context) (*model.PlayerProfile, error) {
var p model.PlayerProfile
err := r.pool.QueryRow(ctx, `SELECT id, level, xp, xp_to_next FROM player_profile LIMIT 1`).
Scan(&p.ID, &p.Level, &p.XP, &p.XPToNext)
return &p, err
}
// xpThreshold returns the XP required to reach the given level.
func xpThreshold(level int) int {
return level * level * 100
}
func (r *GameRepository) AddXP(ctx context.Context, eventType string, amount int, description string) (*model.PlayerProfile, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, `INSERT INTO xp_events (event_type, xp_earned, description) VALUES ($1, $2, $3)`,
eventType, amount, description)
if err != nil {
return nil, err
}
var p model.PlayerProfile
err = tx.QueryRow(ctx, `SELECT id, level, xp, xp_to_next FROM player_profile LIMIT 1`).
Scan(&p.ID, &p.Level, &p.XP, &p.XPToNext)
if err != nil {
return nil, err
}
p.XP += amount
for p.XP >= p.XPToNext {
p.XP -= p.XPToNext
p.Level++
p.XPToNext = xpThreshold(p.Level)
}
_, err = tx.Exec(ctx, `UPDATE player_profile SET level=$1, xp=$2, xp_to_next=$3 WHERE id=$4`,
p.Level, p.XP, p.XPToNext, p.ID)
if err != nil {
return nil, err
}
return &p, tx.Commit(ctx)
}
func (r *GameRepository) ListActiveQuests(ctx context.Context) ([]model.PlayerQuest, error) {
now := time.Now()
dailyPeriod := now.Format("2006-01-02")
weeklyPeriod := now.Format("2006") + "-W" + now.Format("01")
monthlyPeriod := now.Format("2006-01")
rows, err := r.pool.Query(ctx, `
SELECT q.id, q.title, q.quest_type, q.target_count, q.target_pct, q.xp_reward,
COALESCE(pq.id, 0),
COALESCE(pq.period, ''),
COALESCE(pq.current_count, 0),
COALESCE(pq.current_pct, 0),
COALESCE(pq.completed, false),
COALESCE(pq.claimed, false)
FROM quests q
LEFT JOIN player_quests pq ON pq.quest_id = q.id AND pq.period = CASE
WHEN q.quest_type = 'daily' THEN $1
WHEN q.quest_type = 'weekly' THEN $2
WHEN q.quest_type = 'monthly' THEN $3
END
WHERE q.active = true
ORDER BY q.quest_type, q.id
`, dailyPeriod, weeklyPeriod, monthlyPeriod)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.PlayerQuest
for rows.Next() {
var pq model.PlayerQuest
var pqID int
if err := rows.Scan(&pq.QuestID, &pq.Title, &pq.QuestType, &pq.TargetCount, &pq.TargetPct,
&pq.XPReward, &pqID, &pq.Period, &pq.CurrentCount, &pq.CurrentPct, &pq.Completed, &pq.Claimed); err != nil {
return nil, err
}
pq.ID = pqID
if pq.Period == "" {
switch pq.QuestType {
case "daily":
pq.Period = dailyPeriod
case "weekly":
pq.Period = weeklyPeriod
case "monthly":
pq.Period = monthlyPeriod
}
}
out = append(out, pq)
}
return out, rows.Err()
}
func (r *GameRepository) EnsurePlayerQuest(ctx context.Context, questID int, period string) (int, error) {
var id int
err := r.pool.QueryRow(ctx, `
INSERT INTO player_quests (quest_id, period) VALUES ($1, $2)
ON CONFLICT (quest_id, period) DO UPDATE SET quest_id = EXCLUDED.quest_id
RETURNING id
`, questID, period).Scan(&id)
return id, err
}
func (r *GameRepository) IncrementQuestCount(ctx context.Context, questType, period string, delta int) error {
_, err := r.pool.Exec(ctx, `
UPDATE player_quests pq
SET current_count = current_count + $1,
completed = (current_count + $1 >= q.target_count),
completed_at = CASE WHEN (current_count + $1 >= q.target_count) AND NOT completed THEN NOW() ELSE completed_at END
FROM quests q
WHERE pq.quest_id = q.id
AND q.quest_type = $2
AND pq.period = $3
AND q.target_count IS NOT NULL
AND NOT pq.claimed
`, delta, questType, period)
return err
}
func (r *GameRepository) UpdateQuestPct(ctx context.Context, period string, pct float64) error {
_, err := r.pool.Exec(ctx, `
UPDATE player_quests pq
SET current_pct = $1,
completed = ($1 >= q.target_pct),
completed_at = CASE WHEN ($1 >= q.target_pct) AND NOT completed THEN NOW() ELSE completed_at END
FROM quests q
WHERE pq.quest_id = q.id
AND q.quest_type = 'monthly'
AND pq.period = $2
AND q.target_pct IS NOT NULL
AND NOT pq.claimed
`, pct, period)
return err
}
func (r *GameRepository) ClaimQuest(ctx context.Context, playerQuestID int) (int, error) {
var xpReward int
err := r.pool.QueryRow(ctx, `
UPDATE player_quests pq
SET claimed = true
FROM quests q
WHERE pq.quest_id = q.id AND pq.id = $1 AND pq.completed AND NOT pq.claimed
RETURNING q.xp_reward
`, playerQuestID).Scan(&xpReward)
return xpReward, err
}
func (r *GameRepository) ListAchievements(ctx context.Context) ([]model.Achievement, error) {
rows, err := r.pool.Query(ctx, `
SELECT a.id, a.title, a.description, a.icon, a.xp_reward, a.unlock_condition,
pa.id IS NOT NULL,
COALESCE(pa.earned_at::text, '')
FROM achievements a
LEFT JOIN player_achievements pa ON pa.achievement_id = a.id
ORDER BY pa.earned_at DESC NULLS LAST, a.id
`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.Achievement
for rows.Next() {
var a model.Achievement
if err := rows.Scan(&a.ID, &a.Title, &a.Description, &a.Icon, &a.XPReward, &a.UnlockCondition, &a.Earned, &a.EarnedAt); err != nil {
return nil, err
}
out = append(out, a)
}
return out, rows.Err()
}
func (r *GameRepository) IsAchievementUnlocked(ctx context.Context, condition string) (bool, error) {
var count int
err := r.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM player_achievements pa
JOIN achievements a ON a.id = pa.achievement_id
WHERE a.unlock_condition = $1
`, condition).Scan(&count)
return count > 0, err
}
func (r *GameRepository) UnlockAchievement(ctx context.Context, condition string) (int, error) {
var xpReward int
err := r.pool.QueryRow(ctx, `
INSERT INTO player_achievements (achievement_id)
SELECT id FROM achievements WHERE unlock_condition = $1
ON CONFLICT (achievement_id) DO NOTHING
RETURNING (SELECT xp_reward FROM achievements WHERE unlock_condition = $1)
`, condition).Scan(&xpReward)
return xpReward, err
}
func (r *GameRepository) ListCosmetics(ctx context.Context) ([]model.Cosmetic, error) {
var level int
_ = r.pool.QueryRow(ctx, `SELECT level FROM player_profile LIMIT 1`).Scan(&level)
rows, err := r.pool.Query(ctx, `
SELECT c.id, c.name, c.type, c.unlock_level, c.css_data::text,
(c.unlock_level <= $1) AS unlocked,
COALESCE(pc.equipped, false)
FROM cosmetics c
LEFT JOIN player_cosmetics pc ON pc.cosmetic_id = c.id
ORDER BY c.unlock_level, c.id
`, level)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.Cosmetic
for rows.Next() {
var c model.Cosmetic
if err := rows.Scan(&c.ID, &c.Name, &c.Type, &c.UnlockLevel, &c.CSSData, &c.Unlocked, &c.Equipped); err != nil {
return nil, err
}
out = append(out, c)
}
return out, rows.Err()
}
func (r *GameRepository) EquipCosmetic(ctx context.Context, cosmeticID int) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// unequip same type
_, err = tx.Exec(ctx, `
UPDATE player_cosmetics pc SET equipped = false
FROM cosmetics c WHERE pc.cosmetic_id = c.id
AND c.type = (SELECT type FROM cosmetics WHERE id = $1)
`, cosmeticID)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `
INSERT INTO player_cosmetics (cosmetic_id, equipped) VALUES ($1, true)
ON CONFLICT (cosmetic_id) DO UPDATE SET equipped = true
`, cosmeticID)
if err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *GameRepository) UnlockCosmeticsForLevel(ctx context.Context, level int) error {
_, err := r.pool.Exec(ctx, `
INSERT INTO player_cosmetics (cosmetic_id)
SELECT id FROM cosmetics WHERE unlock_level <= $1
ON CONFLICT (cosmetic_id) DO NOTHING
`, level)
return err
}