// Геймификация (ТЗ 3.13): микронаграда за закрытие задачи — конфетти
// и тост «+N XP» с похвалой. Только позитив: ничего не сгорает, серий нет.
// Особые закрытия (редкое растение, крупная задача, режим «3 вариантов»)
// празднуются «золотым» конфетти и расширенным тостом.
import { takeEarnedXp } from './api'
import type { EarnedReward } from './api'
import i18n from './i18n'
// Регистрация toast-провайдера кита (вызывается один раз в App.vue setup)
type ToastApi = { success: (opts: { title: string; text?: string }) => unknown }
let toast: ToastApi | null = null
export function initGamification(t: ToastApi): void {
toast = t
}
// Похвала — случайная фраза из локали (gamify.praise — массив строк)
function randomPraise(): string {
const messages = i18n.global.tm('gamify.praise')
const list = Array.isArray(messages) ? (messages as unknown[]) : []
const phrase = list.length ? list[Math.floor(Math.random() * list.length)] : ''
return typeof phrase === 'string' ? phrase : ''
}
// CSS-конфетти: частицы падают сверху с вращением, слой чистится сам.
// gold=true — «золотой» вариант: больше частиц, тёплая палитра.
const CONFETTI_COLORS = ['#7aa2f7', '#9ece6a', '#e0af68', '#bb9af7', '#f7768e', '#73daca']
const GOLD_COLORS = ['#d4a017', '#f1c40f', '#f5d76e', '#b8860b', '#ffffff', '#e6c34a']
function confetti(gold: boolean): void {
const colors = gold ? GOLD_COLORS : CONFETTI_COLORS
const layer = document.createElement('div')
layer.className = 'confetti-layer'
const pieces = gold ? 56 : 32
for (let i = 0; i < pieces; i++) {
const piece = document.createElement('span')
piece.className = 'confetti-piece'
piece.style.left = `${5 + Math.random() * 90}%`
piece.style.background = colors[i % colors.length]
piece.style.animationDelay = `${Math.random() * (gold ? 0.6 : 0.4)}s`
piece.style.animationDuration = `${1.1 + Math.random() * 0.9}s`
piece.style.setProperty('--rot', `${-540 + Math.floor(Math.random() * 1080)}deg`)
piece.style.setProperty('--drift', `${-60 + Math.floor(Math.random() * 120)}px`)
layer.appendChild(piece)
}
document.body.appendChild(layer)
window.setTimeout(() => layer.remove(), 2600)
}
// Текст тоста: похвала + приписки за редкое растение и режим «3 вариантов»
function rewardText(r: EarnedReward): string {
const parts: string[] = []
if (r.rarity === 'epic') parts.push(i18n.global.t('gamify.epicPlant'))
else if (r.rarity === 'rare') parts.push(i18n.global.t('gamify.rarePlant'))
parts.push(randomPraise())
return parts.filter(Boolean).join(' ')
}
// Вызывать после любого действия, которое могло закрыть задачу:
// если бэк начислил XP (заголовок X-Earned-XP) — праздник + тост
export function celebrateEarned(): void {
const reward = takeEarnedXp()
if (!reward || !reward.xp) return
const special = reward.rarity !== 'common' || reward.xp >= 25
confetti(special)
const title = reward.options
? `+${reward.xp} XP • ${i18n.global.t('gamify.chosen')}`
: `+${reward.xp} XP`
toast?.success({ title, text: rewardText(reward) })
}