- Tabela `profiles` + coluna `profile_id` em todas as entidades (categories, transactions, recurring_expenses, accounts, player_profile, xp_events, player_quests, player_achievements, player_cosmetics) - Dados existentes migrados para profile_id = 1 (Manoel) - CLI `./api create-user --name <n> --password <p>` cria perfil com seed de categorias e player_profile; faz upsert de senha se já existir - Auth substituída: cookie+APP_PASSWORD → JWT Bearer 24h (HS256) - Middleware RequireAuth injeta profile_id no context de todas as rotas - Todos os repositórios filtram por profile_id do context - Endpoints: POST /api/auth/login, GET /api/auth/me, POST /api/auth/change-password, POST /api/logout - Frontend: auth store usa localStorage (fc_token/fc_profile), api.ts envia Authorization header, LoginView usa campo name - SettingsView reescrita com troca de senha e logout - docker-compose.yml: remove APP_USERNAME/APP_PASSWORD, adiciona JWT_SECRET Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
107 lines
3.2 KiB
Vue
107 lines
3.2 KiB
Vue
<template>
|
|
<svg
|
|
:width="cols * scale"
|
|
:height="rows * scale"
|
|
:viewBox="`0 0 ${cols * scale} ${rows * scale}`"
|
|
shape-rendering="crispEdges"
|
|
class="fc-sprite"
|
|
:class="{ 'fc-sprite--bob': bob && !celebrate, 'fc-sprite--celebrate': celebrate }"
|
|
aria-label="Personagem pixel art"
|
|
>
|
|
<rect
|
|
v-for="(px, i) in pixels"
|
|
:key="i"
|
|
:x="px.x * scale"
|
|
:y="px.y * scale"
|
|
:width="scale"
|
|
:height="scale"
|
|
:fill="colors[px.k]"
|
|
/>
|
|
</svg>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
/**
|
|
* Renders Manoel as a 17x24 pixel sprite.
|
|
*
|
|
* The pixel grid is imported from `sprite-data.json`. Each cell's character
|
|
* is a color *slot key* — '.' is transparent, otherwise the key maps into
|
|
* `defaultColors` (which the user can override via the `theme` prop).
|
|
*
|
|
* Cosmetics work by overriding slots. For example, `Camisa Tripulante` sets
|
|
* `{ c: '#0ea5e9', C: '#0369a1' }`. Merge equipped cosmetics' color maps in
|
|
* your store and pass the result here.
|
|
*/
|
|
|
|
import { computed } from 'vue'
|
|
// In Vite/TS, ensure `resolveJsonModule: true` and adjust import path:
|
|
import spriteData from '../sprites/sprite-data.json'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
/** Pixel zoom factor. 2 = mobile, 3 = widget, 4 = personagem hero (default), 6 = focus. */
|
|
scale?: number
|
|
/** Per-slot color overrides (cosmetics). Keys are sprite-data color slots. */
|
|
theme?: Partial<Record<string, string>>
|
|
/** Idle bob animation. Off when celebrate is on. */
|
|
bob?: boolean
|
|
/** Celebration pose (use after quest complete, ~3s). */
|
|
celebrate?: boolean
|
|
}>(), {
|
|
scale: 4,
|
|
bob: true,
|
|
})
|
|
|
|
const rows = spriteData.rows
|
|
const cols = spriteData.cols
|
|
|
|
const defaultColors: Record<string, string> = {
|
|
L: 'var(--fc-sprite-outline, #10131c)',
|
|
s: 'var(--fc-sprite-skin, #f3c79b)',
|
|
S: 'var(--fc-sprite-skin-shade, #c98863)',
|
|
e: 'var(--fc-sprite-eye, #10131c)',
|
|
m: 'var(--fc-sprite-mouth, #80484b)',
|
|
h: 'var(--fc-sprite-hat, #ffd84d)',
|
|
w: 'var(--fc-sprite-band, #fde68a)',
|
|
c: 'var(--fc-sprite-shirt, #00f0ff)',
|
|
C: 'var(--fc-sprite-shirt-shade, #0369a1)',
|
|
p: 'var(--fc-sprite-pants, #3a1f6e)',
|
|
P: 'var(--fc-sprite-pants-shade, #2d1857)',
|
|
b: 'var(--fc-sprite-boots, #0f172a)',
|
|
}
|
|
|
|
const colors = computed(() => ({ ...defaultColors, ...(props.theme ?? {}) }))
|
|
|
|
const pixels = computed(() => {
|
|
const out: { x: number; y: number; k: string }[] = []
|
|
spriteData.grid.forEach((row: string, y: number) => {
|
|
for (let x = 0; x < row.length; x++) {
|
|
const k = row[x]
|
|
if (k === '.' || !colors.value[k]) continue
|
|
out.push({ x, y, k })
|
|
}
|
|
})
|
|
return out
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-sprite {
|
|
image-rendering: pixelated;
|
|
display: inline-block;
|
|
line-height: 0;
|
|
}
|
|
|
|
@keyframes fc-sprite-bob {
|
|
0%, 100% { transform: translateY(0); }
|
|
50% { transform: translateY(-3px); }
|
|
}
|
|
.fc-sprite--bob { animation: fc-sprite-bob 2.2s steps(2) infinite; }
|
|
|
|
@keyframes fc-sprite-celebrate {
|
|
0%, 100% { transform: translateY(0) rotate(0deg); }
|
|
25% { transform: translateY(-6px) rotate(-3deg); }
|
|
75% { transform: translateY(-6px) rotate(3deg); }
|
|
}
|
|
.fc-sprite--celebrate { animation: fc-sprite-celebrate 1s ease-in-out infinite; }
|
|
</style>
|