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
+13 -1
View File
@@ -20,8 +20,20 @@ var m003 string
//go:embed sql/004_accounts.sql
var m004 string
//go:embed sql/005_game_profile.sql
var m005 string
//go:embed sql/006_quests.sql
var m006 string
//go:embed sql/007_achievements.sql
var m007 string
//go:embed sql/008_cosmetics.sql
var m008 string
func Run(ctx context.Context, pool *pgxpool.Pool) error {
migrations := []string{m001, m002, m003, m004}
migrations := []string{m001, m002, m003, m004, m005, m006, m007, m008}
for i, sql := range migrations {
if _, err := pool.Exec(ctx, sql); err != nil {
return fmt.Errorf("migration %03d: %w", i+1, err)
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS player_profile (
id SERIAL PRIMARY KEY,
level INTEGER NOT NULL DEFAULT 1,
xp INTEGER NOT NULL DEFAULT 0,
xp_to_next INTEGER NOT NULL DEFAULT 100
);
INSERT INTO player_profile (level, xp, xp_to_next) VALUES (1, 0, 100)
ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS xp_events (
id SERIAL PRIMARY KEY,
event_type VARCHAR(50) NOT NULL,
xp_earned INTEGER NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
INSERT INTO schema_migrations (version) VALUES (5) ON CONFLICT DO NOTHING;
@@ -0,0 +1,32 @@
CREATE TABLE IF NOT EXISTS quests (
id SERIAL PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description TEXT,
quest_type VARCHAR(20) NOT NULL CHECK (quest_type IN ('daily','weekly','monthly')),
target_count INTEGER,
target_pct NUMERIC(5,2),
xp_reward INTEGER NOT NULL DEFAULT 50,
active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE IF NOT EXISTS player_quests (
id SERIAL PRIMARY KEY,
quest_id INTEGER NOT NULL REFERENCES quests(id) ON DELETE CASCADE,
period VARCHAR(10) NOT NULL,
current_count INTEGER NOT NULL DEFAULT 0,
current_pct NUMERIC(5,2) NOT NULL DEFAULT 0,
completed BOOLEAN NOT NULL DEFAULT FALSE,
completed_at TIMESTAMP WITH TIME ZONE,
claimed BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE (quest_id, period)
);
INSERT INTO quests (title, description, quest_type, target_count, target_pct, xp_reward) VALUES
('Economista do Mês', 'Poupe ≥40% da receita este mês', 'monthly', NULL, 40, 200),
('Capitão Organizado', 'Importe 1 extrato esta semana', 'weekly', 1, NULL, 50),
('Categorizador Ativo', 'Categorize 5 transações hoje', 'daily', 5, NULL, 30),
('Controlador Semanal', 'Registre ao menos 3 transações esta semana', 'weekly', 3, NULL, 40),
('Mês Sem Surpresas', 'Cubra todas as recorrências do mês', 'monthly', NULL, NULL, 150)
ON CONFLICT DO NOTHING;
INSERT INTO schema_migrations (version) VALUES (6) ON CONFLICT DO NOTHING;
@@ -0,0 +1,30 @@
CREATE TABLE IF NOT EXISTS achievements (
id SERIAL PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description TEXT,
icon VARCHAR(10) NOT NULL DEFAULT '🏆',
xp_reward INTEGER NOT NULL DEFAULT 100,
unlock_condition VARCHAR(100) NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS player_achievements (
id SERIAL PRIMARY KEY,
achievement_id INTEGER NOT NULL REFERENCES achievements(id) ON DELETE CASCADE UNIQUE,
earned_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
INSERT INTO achievements (title, description, icon, xp_reward, unlock_condition) VALUES
('Primeiro Passo', 'Registre sua primeira transação manual', '👣', 50, 'first_transaction'),
('Explorador de Extratos', 'Importe seu primeiro extrato', '📄', 100, 'first_import'),
('Meta Atingida!', 'Poupe 40% pela primeira vez', '🎯', 200, 'first_40pct'),
('Tri-Campeão', 'Poupe 40% por 3 meses seguidos', '🥇', 500, 'three_months_40pct'),
('Veleirista Econômico', 'Gaste menos de R$500 no Veleiro em um mês', '', 150, 'veleiro_under_500'),
('Nível 5', 'Alcance o nível 5', '', 100, 'level_5'),
('Nível 10', 'Alcance o nível 10', '🌟', 300, 'level_10'),
('Mestre das Categorias', 'Categorize 100 transações', '🗂️', 200, 'categorize_100'),
('Semana Perfeita', 'Importe extrato por 4 semanas seguidas', '📅', 250, 'four_weeks_import'),
('Guardião da Saúde', 'Registre gasto com Saúde/Insulina em 3 meses', '💉', 300, 'health_3months'),
('Mês Limpo', 'Todas as recorrências cobertas em um mês', '', 200, 'full_recurring_month')
ON CONFLICT DO NOTHING;
INSERT INTO schema_migrations (version) VALUES (7) ON CONFLICT DO NOTHING;
@@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS cosmetics (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('hat','outfit','accessory')),
unlock_level INTEGER NOT NULL,
css_data JSONB NOT NULL DEFAULT '{}'
);
CREATE TABLE IF NOT EXISTS player_cosmetics (
id SERIAL PRIMARY KEY,
cosmetic_id INTEGER NOT NULL REFERENCES cosmetics(id) ON DELETE CASCADE UNIQUE,
equipped BOOLEAN NOT NULL DEFAULT FALSE,
unlocked_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
INSERT INTO cosmetics (name, type, unlock_level, css_data) VALUES
('Chapéu de Aventureiro', 'hat', 2, '{"color":"#8B4513","accent":"#d4a85a"}'),
('Roupa de Marinheiro', 'outfit', 3, '{"primary":"#1e3a5f","accent":"#ffffff"}'),
('Óculos de Sol', 'accessory', 4, '{"color":"#1a1a1a"}'),
('Chapéu de Capitão', 'hat', 5, '{"color":"#1e293b","badge":"#ffd700"}'),
('Roupa Dourada', 'outfit', 7, '{"primary":"#d4af37","accent":"#ffd700"}'),
('Coroa de Campeão', 'hat', 10, '{"color":"#ffd700","gems":"#e11d48"}')
ON CONFLICT DO NOTHING;
INSERT INTO schema_migrations (version) VALUES (8) ON CONFLICT DO NOTHING;