import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' /** * Rastreador de misiones (/quest-character): comprueba el estado (completada / * en curso / sin empezar) de misiones o cadenas de misiones en un personaje. * * Categorías (cooldown por personaje): * - 'class' cadenas de misiones de clase — cada 30 min * - 'reputation' cadenas de misiones (reputación) — cada 1 h * - 'specific' una misión concreta por ID — cada 10 min */ export type QuestCategory = 'class' | 'reputation' | 'specific' export const COOLDOWN_MS: Record = { reputation: 60 * 60 * 1000, class: 30 * 60 * 1000, specific: 10 * 60 * 1000, } export interface QuestOption { key: string category: Exclude classCss: string | null // clase a la que aplica ('warlock'…) o null (cualquier clase) label: string link?: { type: 'spell' | 'item' | 'faction'; id: number } icon?: string minLevel: number quests: number[] // IDs de misión de la cadena (variantes de facción combinadas) } // IDs de misión verificados en la BD WotLK 3.3.5a (wotlkdb.com): la misión FINAL // que otorga cada habilidad, combinando variantes de facción/raza (el personaje // solo tendrá en su registro las de su facción/raza). Editables aquí si hiciera falta. export const QUEST_OPTIONS: QuestOption[] = [ // Brujo { key: 'wl_voidwalker', category: 'class', classCss: 'warlock', label: 'Invocar abisario', link: { type: 'spell', id: 697 }, minLevel: 10, icon: 'spell_shadow_summonvoidwalker', quests: [1689, 1504, 1471] }, { key: 'wl_succubus', category: 'class', classCss: 'warlock', label: 'Invocar súcubo', link: { type: 'spell', id: 712 }, minLevel: 20, icon: 'spell_shadow_summonsuccubus', quests: [1739, 1513, 1474] }, { key: 'wl_felhunter', category: 'class', classCss: 'warlock', label: 'Invocar manáfago', link: { type: 'spell', id: 691 }, minLevel: 30, icon: 'spell_shadow_summonfelhunter', quests: [1795] }, { key: 'wl_inferno', category: 'class', classCss: 'warlock', label: 'Inferno', link: { type: 'spell', id: 1122 }, minLevel: 50, icon: 'spell_shadow_summoninfernal', quests: [7603] }, { key: 'wl_doom', category: 'class', classCss: 'warlock', label: 'Ritual de fatalidad', link: { type: 'spell', id: 18540 }, minLevel: 60, icon: 'spell_shadow_antimagicshell', quests: [7583] }, // Cazador { key: 'hunter_pet', category: 'class', classCss: 'hunter', label: 'Habilidades de mascota', minLevel: 10, quests: [6103, 6086, 9675, 6081, 6089, 9673] }, // Chamán { key: 'sh_earth', category: 'class', classCss: 'shaman', label: 'Tótem de tierra', link: { type: 'item', id: 5175 }, minLevel: 4, icon: 'spell_totem_wardofdraining', quests: [9451, 1518, 1521] }, { key: 'sh_fire', category: 'class', classCss: 'shaman', label: 'Tótem de fuego', link: { type: 'item', id: 5176 }, minLevel: 10, icon: 'spell_totem_wardofdraining', quests: [9555, 1527] }, { key: 'sh_water', category: 'class', classCss: 'shaman', label: 'Tótem de agua', link: { type: 'item', id: 5177 }, minLevel: 20, icon: 'spell_totem_wardofdraining', quests: [9509, 96] }, { key: 'sh_air', category: 'class', classCss: 'shaman', label: 'Tótem de aire', link: { type: 'item', id: 5178 }, minLevel: 30, icon: 'spell_totem_wardofdraining', quests: [9554, 1531, 1532] }, // Druida { key: 'dr_bear', category: 'class', classCss: 'druid', label: 'Forma de oso', link: { type: 'spell', id: 5487 }, minLevel: 10, icon: 'ability_racial_bearform', quests: [6001, 6002] }, { key: 'dr_curepoison', category: 'class', classCss: 'druid', label: 'Curar envenenamiento', link: { type: 'spell', id: 8946 }, minLevel: 14, icon: 'spell_nature_nullifypoison', quests: [6125, 6130] }, { key: 'dr_aquatic', category: 'class', classCss: 'druid', label: 'Forma acuática', link: { type: 'spell', id: 1066 }, minLevel: 16, icon: 'ability_druid_aquaticform', quests: [5061, 31] }, { key: 'dr_flight', category: 'class', classCss: 'druid', label: 'Forma de vuelo presto', link: { type: 'spell', id: 40120 }, minLevel: 70, icon: 'ability_druid_flightform', quests: [11001] }, // Guerrero { key: 'wr_defensive', category: 'class', classCss: 'warrior', label: 'Actitud defensiva', link: { type: 'spell', id: 71 }, minLevel: 10, icon: 'ability_warrior_defensivestance', quests: [1665, 1678, 1683, 9582, 10350, 1498, 1819] }, { key: 'wr_berserker', category: 'class', classCss: 'warrior', label: 'Actitud rabiosa', link: { type: 'spell', id: 2458 }, minLevel: 30, icon: 'ability_racial_avatar', quests: [1719] }, // Paladín { key: 'pal_redemption', category: 'class', classCss: 'paladin', label: 'Redención', link: { type: 'spell', id: 7328 }, minLevel: 12, icon: 'spell_holy_resurrection', quests: [1788, 9598, 9685] }, // Reputación { key: 'rep_hodir', category: 'reputation', classCss: null, label: 'Los Hijos de Hodir', link: { type: 'faction', id: 1119 }, minLevel: 77, quests: [12843, 13064, 12915, 12956] }, ] /** Opciones que un personaje (clase + nivel) puede rastrear. */ export function optionsFor(classCss: string, level: number): QuestOption[] { return QUEST_OPTIONS.filter( (o) => (o.classCss === null || o.classCss === classCss) && level >= o.minLevel && o.quests.length > 0, ) } export type QuestState = 'rewarded' | 'inProgress' | 'none' export interface QuestStatus { quest: number state: QuestState } /** Estado de cada misión (completada/en curso/sin empezar) para un personaje. */ export async function questStatuses(guid: number, quests: number[]): Promise { if (quests.length === 0) return [] const ph = quests.map(() => '?').join(',') const rewarded = new Set() const inProgress = new Set() try { const [r] = await db(DB.characters).query( `SELECT quest FROM character_queststatus_rewarded WHERE guid = ? AND quest IN (${ph})`, [guid, ...quests], ) for (const row of r) rewarded.add(Number(row.quest)) const [p] = await db(DB.characters).query( `SELECT quest FROM character_queststatus WHERE guid = ? AND quest IN (${ph})`, [guid, ...quests], ) for (const row of p) inProgress.add(Number(row.quest)) } catch { /* ignore */ } return quests.map((q) => ({ quest: q, state: rewarded.has(q) ? 'rewarded' : inProgress.has(q) ? 'inProgress' : 'none', })) } /** Minutos restantes de cooldown para (personaje, categoría), o 0 si disponible. */ export async function cooldownRemainingMs(character: string, category: QuestCategory): Promise { try { const [rows] = await db(DB.default).query( 'SELECT searched_at FROM questsearchhistory WHERE character_name = ? AND category = ? ORDER BY searched_at DESC LIMIT 1', [character, category], ) if (!rows[0]) return 0 const diff = Date.now() - new Date(rows[0].searched_at).getTime() return diff < COOLDOWN_MS[category] ? COOLDOWN_MS[category] - diff : 0 } catch { return 0 } } export async function recordSearch(character: string, category: QuestCategory): Promise { try { await db(DB.default).query( 'INSERT INTO questsearchhistory (character_name, category, searched_at) VALUES (?, ?, NOW())', [character, category], ) } catch { /* ignore */ } }