cccc70338d
El prefijo venía del nombre de la app Django (`home`), jubilada y borrada hoy, así que ya no significaba nada: home_stripelog -> stripelog. La BD se sigue llamando django_wow por historia (renombrarla es otra operación). Comprobado antes de tocar nada: 0 colisiones con nombres existentes, 0 palabras reservadas (contrastado contra las 260 de MySQL), sin vistas ni triggers que dependieran de ellas. El RENAME va en una sola sentencia porque así es atómico, y MySQL reapunta solo las 4 claves ajenas entre estas tablas. 170 referencias actualizadas en 37 ficheros. Se dejó a propósito sql/drop_home_securitytoken_authuser_fk.sql sin tocar: es una migración ya aplicada y reescribirla sería falsear el historial. De paso salió un fallo previo: prices.ts consultaba `home_restoreitemprice`, una tabla que NO existe. El try/catch de priceFrom se comía el error y devolvía siempre el precio por defecto, así que el precio de restaurar objetos nunca fue configurable. Se deja documentado en el código; crear la tabla es otra decisión. Verificado tras aplicar: 0 tablas home_, las 51 siguen ahí, y el conteo exacto de filas cuadra con el volcado previo (store_item 3068, item_data 44873, stripelog 35, store_order 26, noticia 50). La web responde 200 en todas las rutas y la portada carga las noticias sin errores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
7.2 KiB
TypeScript
128 lines
7.2 KiB
TypeScript
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<QuestCategory, number> = {
|
|
reputation: 60 * 60 * 1000,
|
|
class: 30 * 60 * 1000,
|
|
specific: 10 * 60 * 1000,
|
|
}
|
|
|
|
export interface QuestOption {
|
|
key: string
|
|
category: Exclude<QuestCategory, 'specific'>
|
|
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<QuestStatus[]> {
|
|
if (quests.length === 0) return []
|
|
const ph = quests.map(() => '?').join(',')
|
|
const rewarded = new Set<number>()
|
|
const inProgress = new Set<number>()
|
|
try {
|
|
const [r] = await db(DB.characters).query<RowDataPacket[]>(
|
|
`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<RowDataPacket[]>(
|
|
`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<number> {
|
|
try {
|
|
const [rows] = await db(DB.default).query<RowDataPacket[]>(
|
|
'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<void> {
|
|
try {
|
|
await db(DB.default).query(
|
|
'INSERT INTO questsearchhistory (character_name, category, searched_at) VALUES (?, ?, NOW())',
|
|
[character, category],
|
|
)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|