tibiaway-ai 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands/boss.md CHANGED
@@ -1,20 +1,42 @@
1
1
  ---
2
- description: Detailed boss mechanics, phases, strategy by vocation, and loot information
2
+ description: Detailed boss mechanics, phases, strategy by vocation, and loot — backed by real TibiaWiki data
3
3
  ---
4
4
 
5
- Provide complete information about a Tibia boss. Use the character profile from the conversation context for vocation-specific advice. If no profile is available yet, ask for it first.
5
+ Provide complete information about a Tibia boss. Use the character profile from conversation context for vocation-specific advice. If no profile is available, ask for it first.
6
6
 
7
7
  If no boss name was provided, ask: "¿Qué boss quieres enfrentar?"
8
8
 
9
- Then cover:
10
- 1. **General description** — what it is, where it is, when/how it spawns
11
- 2. **Key stats** approximate HP, elemental weaknesses/resistances/immunities
12
- 3. **Main mechanics** — attacks and damage, special mechanics, summons, unique behaviors
13
- 4. **Phases** (if applicable) — behavior changes per phase
14
- 5. **Preparation for the player's vocation** optimal set, imbuements, supplies with quantities
15
- 6. **Strategy by vocation** — positioning, skills, rotation for their specific vocation
16
- 7. **Notable loot** — most valuable or interesting drops
17
- 8. **Common mistakes and tips** what NOT to do, non-obvious tricks
9
+ ## Step 1 — Fetch real data from TibiaWiki
10
+
11
+ Before answering, fetch the creature data using this URL:
12
+ `https://tibiawiki.dev/api/creatures/{boss name URL-encoded}`
13
+
14
+ Example: for "The Pale Count" `https://tibiawiki.dev/api/creatures/The%20Pale%20Count`
15
+
16
+ Extract from the response:
17
+ - `hp`real hit points
18
+ - `exp` — experience given
19
+ - `maxdmg` — maximum damage output
20
+ - `fireDmgMod`, `iceDmgMod`, `energyDmgMod`, `deathDmgMod`, `holyDmgMod`, `earthDmgMod`, `physicalDmgMod` — resistance modifiers (values >100% = weakness, <100% = resistance, 0% = immune)
21
+ - `paraimmune` — immune to paralyze?
22
+ - `loot` — array of dropped items with rarity
23
+ - `location` — where it's found
24
+
25
+ If the fetch fails or returns no results, warn the player that the name may not match TibiaWiki exactly, then proceed with general knowledge.
26
+
27
+ ## Step 2 — Answer using real data as ground truth
28
+
29
+ Cover these sections:
30
+
31
+ 1. **Descripción general** — qué es, dónde está, cuándo/cómo aparece
32
+ 2. **Estadísticas clave** — HP, exp, max daño (usa los valores reales obtenidos)
33
+ 3. **Resistencias y debilidades** — interpreta los mods: >100% = debilidad, <100% = resistencia, 0% = inmune
34
+ 4. **Mecánicas principales** — ataques, summons, habilidades únicas
35
+ 5. **Fases** (si aplica) — cambios de comportamiento por fase
36
+ 6. **Preparación para la vocación del jugador** — set, imbuements, supplies con cantidades
37
+ 7. **Estrategia por vocación** — posicionamiento, skills, rotación
38
+ 8. **Loot destacado** — usa el loot real de TibiaWiki, menciona rarezas
39
+ 9. **Errores comunes y tips** — qué NO hacer, trucos no obvios
18
40
 
19
41
  If the boss has instant-kill mechanics, call them out explicitly.
20
42
  Respond in Spanish.
package/commands/hunt.md CHANGED
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: Suggest optimal hunting spots based on character profile, objective, and party setup
2
+ description: Suggest optimal hunting spots backed by real TibiaWiki creature data
3
3
  ---
4
4
 
5
- Suggest the best hunting spots for the player. Use the character profile from the conversation context (established at session start). If no profile is available yet, ask for it first.
5
+ Suggest the best hunting spots for the player. Use the character profile from conversation context. If no profile is available, ask for it first.
6
6
 
7
7
  Ask the player:
8
8
  1. Objective: exp / loot / profit / task
@@ -10,13 +10,36 @@ Ask the player:
10
10
  3. Premium account?
11
11
  4. Any restrictions or preferences?
12
12
 
13
- Then provide 3-5 prioritized hunting spots with:
14
- - Location name and main creatures
15
- - Estimated exp/hour for their level and vocation
13
+ ## Step 1 Get creature names
14
+
15
+ Based on the player's profile and answers, mentally list 6-8 creature names that match good hunting spots for their level and vocation.
16
+
17
+ ## Step 2 — Fetch real creature data from TibiaWiki
18
+
19
+ For each creature, fetch:
20
+ `https://tibiawiki.dev/api/creatures/{creature name URL-encoded}`
21
+
22
+ Extract from each response:
23
+ - `hp`, `exp` — real values
24
+ - `maxdmg` — max damage
25
+ - `location` — where they spawn
26
+ - `bestiarylevel` — difficulty tier
27
+ - `loot` — notable drops
28
+ - Resistance modifiers (`fireDmgMod`, `iceDmgMod`, etc.)
29
+
30
+ Only use creatures that return valid data from TibiaWiki. Discard any that don't exist.
31
+
32
+ ## Step 3 — Build recommendations using verified data
33
+
34
+ Provide 3-5 prioritized hunting spots using only verified creatures.
35
+
36
+ For each spot include:
37
+ - Location name and main creatures (verified)
38
+ - Estimated exp/hour for their level and vocation (base it on the real `exp` value per creature)
16
39
  - Estimated profit (positive or negative)
17
40
  - Access requirements (quests, level, etc.)
18
41
  - Vocation-specific tips
19
42
  - Solo viability
20
43
 
21
- Be specific with numbers. Use ranges when exact data isn't available.
44
+ Be specific. Use the real HP and exp values from TibiaWiki to estimate rates.
22
45
  Respond in Spanish.
package/commands/quest.md CHANGED
@@ -1,23 +1,46 @@
1
1
  ---
2
- description: Recommend quests based on level, vocation, and player objectives
2
+ description: Recommend quests based on level, vocation, and objectives — backed by real TibiaWiki data
3
3
  ---
4
4
 
5
- Recommend Tibia quests for the player. Use the character profile from the conversation context. If no profile is available yet, ask for it first.
5
+ Recommend Tibia quests for the player. Use the character profile from conversation context. If no profile is available, ask for it first.
6
6
 
7
7
  Ask the player:
8
- 1. What are they looking for? (zone access / outfit / achievement / equipment reward / story)
8
+ 1. What are they looking for? (zone access / outfit / achievement / equipment reward / story / boss access)
9
9
  2. Do they have a party available?
10
10
  3. Anything specific they want or quests already completed?
11
11
 
12
- Then provide 3-5 recommended quests with:
13
- - Quest name
14
- - Why it's relevant for their profile and objective
15
- - Minimum recommended level
16
- - Prerequisites (other quests, items, etc.)
12
+ ## Step 1 Get quest names
13
+
14
+ Based on the player's profile and answers, identify 4-6 quest names that are relevant for their level and objective. Quest names in TibiaWiki typically end in "Quest" (e.g. "The Queen of the Banshees Quest").
15
+
16
+ ## Step 2 Fetch real quest data from TibiaWiki
17
+
18
+ For each quest, fetch:
19
+ `https://tibiawiki.dev/api/quests/{quest name URL-encoded}`
20
+
21
+ Extract from each response:
22
+ - `lvl` — minimum level required
23
+ - `lvlrec` — recommended level
24
+ - `premium` — requires premium account?
25
+ - `reward` — actual rewards
26
+ - `dangers` — enemies encountered
27
+ - `location` — where it takes place
28
+ - `legend` — brief description
29
+
30
+ Only use quests that return valid data. Discard any that don't exist.
31
+
32
+ ## Step 3 — Build recommendations using verified data
33
+
34
+ Provide 3-5 recommended quests using only verified quests.
35
+
36
+ For each quest include:
37
+ - Quest name and why it's relevant for their profile and objective
38
+ - Minimum level (use the real `lvl` value from TibiaWiki)
39
+ - Prerequisites (other quests, items needed)
17
40
  - Estimated difficulty (easy/medium/hard)
18
- - Main reward
41
+ - Main reward (use the real `reward` value from TibiaWiki)
19
42
  - Solo or party required?
20
- - Important tip or warning
43
+ - Main dangers and one key tip
21
44
 
22
45
  Prioritize quests with the best effort-to-reward ratio for their current situation.
23
46
  Respond in Spanish.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tibiaway-ai",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Tu guía de Tibia powered by AI. Your path through Tibia, powered by AI.",
5
5
  "bin": {
6
6
  "tibiaway": "bin/tibiaway.js",
@@ -3,7 +3,8 @@ import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { getCharacter } from '../utils/config.js';
5
5
  import { askClaude } from '../utils/api.js';
6
- import { error, highlight, separator } from '../utils/banner.js';
6
+ import { fetchCreature, formatCreatureForPrompt } from '../tibia-items.js';
7
+ import { error, highlight, separator, warn } from '../utils/banner.js';
7
8
 
8
9
  export async function handleBoss(bossName) {
9
10
  const character = getCharacter();
@@ -15,7 +16,6 @@ export async function handleBoss(bossName) {
15
16
  console.log(chalk.hex('#FF6B1A').bold('💀 BOSS ADVISOR\n'));
16
17
 
17
18
  let targetBoss = bossName;
18
-
19
19
  if (!targetBoss) {
20
20
  const answer = await inquirer.prompt([
21
21
  {
@@ -28,58 +28,53 @@ export async function handleBoss(bossName) {
28
28
  targetBoss = answer.boss.trim();
29
29
  }
30
30
 
31
- const prompt = `Explícame todo sobre el boss: ${targetBoss}
32
-
33
- Dame información completa y detallada:
34
-
35
- 1. DESCRIPCIÓN GENERAL
36
- - Qué es, dónde está, historia/lore relevante
37
- - Cuándo aparece / cómo se invoca / periodicidad
38
-
39
- 2. ESTADÍSTICAS CLAVE
40
- - HP aproximado
41
- - Debilidades elementales y resistencias
42
- - Inmunidades importantes
43
-
44
- 3. MECÁNICAS PRINCIPALES
45
- - Ataques que usa y daño aproximado
46
- - Mecánicas especiales (summons, fases, habilidades únicas)
47
- - Comportamientos que hay que conocer sí o sí
48
-
49
- 4. FASES (si aplica)
50
- - Descripción de cada fase y cambios de comportamiento
51
-
52
- 5. PREPARACIÓN RECOMENDADA PARA MI PERSONAJE
53
- - Set óptimo e imbuements
54
- - Supplies necesarios (con cantidades estimadas)
55
- - Level mínimo recomendado para mi vocación
31
+ console.log(chalk.hex('#888888')(`Analizando: ${targetBoss}\n`));
56
32
 
57
- 6. ESTRATEGIA POR VOCACIÓN
58
- - Cómo lo enfrento siendo ${character.vocation}
59
- - Posicionamiento, skills a usar, rotación
33
+ // ── Buscar datos reales en TibiaWiki ──────────────────────────────────────
34
+ const spinner1 = ora({ text: 'Verificando en TibiaWiki...', color: 'cyan' }).start();
35
+ const creatureData = await fetchCreature(targetBoss);
36
+ spinner1.stop();
37
+
38
+ let contextBlock = '';
39
+ if (creatureData) {
40
+ console.log(chalk.hex('#4ade80')(` ✓ Datos reales obtenidos de TibiaWiki`));
41
+ contextBlock = `
42
+ DATOS REALES DE TIBIAWIKI (usa estos como base — no inventes stats):
43
+ ${formatCreatureForPrompt(creatureData)}
44
+ `;
45
+ } else {
46
+ warn(`No se encontró "${targetBoss}" en TibiaWiki. Verifica el nombre exacto.`);
47
+ warn('Usando conocimiento general de IA (puede ser menos preciso).');
48
+ }
60
49
 
61
- 7. LOOT DESTACADO
62
- - Items más valiosos o interesantes del drop
50
+ // ── Prompt con datos reales ───────────────────────────────────────────────
51
+ const spinner2 = ora({ text: 'Consultando el libro de los bosses...', color: 'yellow' }).start();
63
52
 
64
- 8. ERRORES COMUNES Y TIPS
65
- - Qué NO hacer
66
- - Trucos que no son obvios
53
+ const prompt = `Personaje: ${character.name} | ${character.vocation} | Level ${character.level}
54
+ ${contextBlock}
55
+ Explícame todo sobre el boss: ${targetBoss}
67
56
 
68
- Sé específico. Si el boss tiene mecánicas que matan instant, mencionarlo claramente.`;
57
+ 1. DESCRIPCIÓN GENERAL qué es, dónde está, cuándo/cómo aparece
58
+ 2. ESTADÍSTICAS CLAVE — HP, resistencias y debilidades${creatureData ? ' (usa los datos reales de arriba)' : ''}
59
+ 3. MECÁNICAS PRINCIPALES — ataques, daño, summons, habilidades únicas
60
+ 4. FASES (si aplica) — cambios de comportamiento por fase
61
+ 5. PREPARACIÓN PARA MI PERSONAJE — set, imbuements, supplies con cantidades
62
+ 6. ESTRATEGIA PARA ${character.vocation} — posicionamiento, skills, rotación
63
+ 7. LOOT DESTACADO — ítems más valiosos${creatureData && creatureData.loot.length ? ' (usa el loot real de arriba)' : ''}
64
+ 8. ERRORES COMUNES Y TIPS — qué NO hacer, trucos no obvios
69
65
 
70
- console.log(chalk.hex('#888888')(`Analizando: ${targetBoss}\n`));
71
- const spinner = ora({ text: 'Consultando el libro de los bosses...', color: 'yellow' }).start();
66
+ Si el boss tiene mecánicas de instant kill, menciónalas claramente.`;
72
67
 
73
68
  try {
74
69
  const response = await askClaude(prompt);
75
- spinner.stop();
70
+ spinner2.stop();
76
71
  separator();
77
72
  console.log('');
78
73
  console.log(chalk.hex('#f0f0f0')(response));
79
74
  console.log('');
80
75
  separator();
81
76
  } catch (err) {
82
- spinner.stop();
77
+ spinner2.stop();
83
78
  error(`Error al consultar la IA: ${err.message}`);
84
79
  }
85
80
  }
@@ -3,8 +3,23 @@ import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { getCharacter } from '../utils/config.js';
5
5
  import { askClaude } from '../utils/api.js';
6
+ import { validateCreatures, formatCreaturesForPrompt } from '../tibia-items.js';
6
7
  import { error, highlight, separator } from '../utils/banner.js';
7
8
 
9
+ function extractCreatureNames(text) {
10
+ try {
11
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
12
+ if (jsonMatch) {
13
+ const arr = JSON.parse(jsonMatch[0]);
14
+ if (Array.isArray(arr)) return arr.filter(v => typeof v === 'string' && v.trim().length > 0);
15
+ }
16
+ } catch {}
17
+ // Fallback: extraer líneas que parecen nombres de criaturas
18
+ return text.split('\n')
19
+ .map(l => l.replace(/^[-*\d.]+\s*/, '').trim())
20
+ .filter(l => l.length > 2 && l.length < 60 && !l.includes(':'));
21
+ }
22
+
8
23
  export async function handleHunt() {
9
24
  const character = getCharacter();
10
25
  if (!character) {
@@ -45,42 +60,85 @@ export async function handleHunt() {
45
60
  {
46
61
  type: 'input',
47
62
  name: 'restricciones',
48
- message: highlight('¿Alguna restricción o preferencia? (zonas, criaturas, etc.) — Enter para saltar:'),
63
+ message: highlight('¿Alguna restricción o preferencia? (Enter para saltar):'),
49
64
  },
50
65
  ]);
51
66
 
52
- const prompt = `Dame las mejores recomendaciones de hunting spots para mi personaje.
67
+ // ── PASO 1: IA sugiere criaturas ──────────────────────────────────────────
68
+ console.log('');
69
+ const spinner1 = ora({ text: 'Consultando sugerencias de criaturas...', color: 'yellow' }).start();
70
+
71
+ const stepOnePrompt = `Personaje: ${character.name} | ${character.vocation} | Level ${character.level} | Mundo: ${character.world}
72
+ Objetivo: ${answers.objetivo} | Modalidad: ${answers.party} | Premium: ${answers.premium ? 'Sí' : 'No'}
73
+ ${answers.restricciones ? `Preferencias: ${answers.restricciones}` : ''}
74
+
75
+ Lista las 6 a 8 criaturas principales de los mejores hunting spots para este personaje.
76
+ Responde SOLO con un array JSON, sin texto adicional:
77
+ ["Criatura 1", "Criatura 2", "Criatura 3", ...]
78
+ Solo criaturas que existen en Tibia. Nombres exactos como aparecen en el juego.`;
53
79
 
54
- Objetivo: ${answers.objetivo}
55
- Modalidad: ${answers.party}
56
- Cuenta Premium: ${answers.premium ? 'Sí' : 'No'}
57
- ${answers.restricciones ? `Preferencias/restricciones: ${answers.restricciones}` : ''}
80
+ let creatureNames = [];
81
+ try {
82
+ const suggestion = await askClaude(stepOnePrompt);
83
+ spinner1.stop();
84
+ creatureNames = extractCreatureNames(suggestion);
85
+ } catch (err) {
86
+ spinner1.fail('Error al consultar la IA.');
87
+ error(err.message);
88
+ return;
89
+ }
58
90
 
59
- Por favor dame entre 3 y 5 hunting spots recomendados, ordenados por prioridad para mi objetivo.
91
+ if (creatureNames.length === 0) {
92
+ error('No se pudieron extraer nombres de criaturas.');
93
+ return;
94
+ }
95
+
96
+ // ── PASO 2: Validar contra TibiaWiki ─────────────────────────────────────
97
+ const spinner2 = ora({ text: `Verificando ${creatureNames.length} criaturas en TibiaWiki...`, color: 'cyan' }).start();
98
+ const { verified, notFound } = await validateCreatures(creatureNames);
99
+ spinner2.stop();
100
+
101
+ const verifiedCount = Object.keys(verified).length;
102
+ console.log(chalk.hex('#4ade80')(` ✓ ${verifiedCount} criaturas verificadas en TibiaWiki`));
103
+ if (notFound.length > 0) {
104
+ console.log(chalk.hex('#888')(` ✗ No encontradas: ${notFound.join(', ')}`));
105
+ }
106
+
107
+ // ── PASO 3: IA arma la recomendación con datos reales ────────────────────
108
+ console.log('');
109
+ const spinner3 = ora({ text: 'Armando tu guía de hunting...', color: 'yellow' }).start();
110
+
111
+ const creaturesData = formatCreaturesForPrompt(verified);
112
+
113
+ const stepThreePrompt = `Personaje: ${character.name} | ${character.vocation} | Level ${character.level} | Mundo: ${character.world}
114
+ Objetivo: ${answers.objetivo} | Modalidad: ${answers.party} | Premium: ${answers.premium ? 'Sí' : 'No'}
115
+ ${answers.restricciones ? `Preferencias: ${answers.restricciones}` : ''}
116
+
117
+ CRIATURAS VERIFICADAS EN TIBIAWIKI con stats reales:
118
+ ${creaturesData}
119
+
120
+ Con estas criaturas verificadas, dame 3 a 5 hunting spots recomendados ordenados por prioridad.
60
121
 
61
122
  Para cada spot incluye:
62
- 1. Nombre del lugar y criaturas principales
123
+ 1. Nombre del lugar y criaturas principales (de la lista verificada)
63
124
  2. Exp/hora estimada para mi level y vocación
64
- 3. Profit estimado (positivo o negativo)
125
+ 3. Profit estimado
65
126
  4. Requisitos de acceso (quests, level, etc.)
66
- 5. Tips específicos para mi vocación
67
- 6. ¿Necesita party o funciona bien solo?
127
+ 5. Tips específicos para ${character.vocation}
128
+ 6. ¿Funciona solo o necesita party?
68
129
 
69
- específico con los números. Si no tienes datos exactos, da rangos razonables.`;
70
-
71
- console.log('');
72
- const spinner = ora({ text: 'Consultando al oráculo tibian...', color: 'yellow' }).start();
130
+ Basa los datos de HP, exp y loot en los valores reales de TibiaWiki.`;
73
131
 
74
132
  try {
75
- const response = await askClaude(prompt);
76
- spinner.stop();
133
+ const response = await askClaude(stepThreePrompt);
134
+ spinner3.stop();
77
135
  separator();
78
136
  console.log('');
79
137
  console.log(chalk.hex('#f0f0f0')(response));
80
138
  console.log('');
81
139
  separator();
82
140
  } catch (err) {
83
- spinner.stop();
141
+ spinner3.stop();
84
142
  error(`Error al consultar la IA: ${err.message}`);
85
143
  }
86
144
  }
@@ -3,8 +3,22 @@ import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { getCharacter } from '../utils/config.js';
5
5
  import { askClaude } from '../utils/api.js';
6
+ import { validateQuests, formatQuestsForPrompt } from '../tibia-items.js';
6
7
  import { error, highlight, separator } from '../utils/banner.js';
7
8
 
9
+ function extractQuestNames(text) {
10
+ try {
11
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
12
+ if (jsonMatch) {
13
+ const arr = JSON.parse(jsonMatch[0]);
14
+ if (Array.isArray(arr)) return arr.filter(v => typeof v === 'string' && v.trim().length > 0);
15
+ }
16
+ } catch {}
17
+ return text.split('\n')
18
+ .map(l => l.replace(/^[-*\d.]+\s*/, '').trim())
19
+ .filter(l => l.length > 3 && l.length < 80 && !l.includes(':'));
20
+ }
21
+
8
22
  export async function handleQuest() {
9
23
  const character = getCharacter();
10
24
  if (!character) {
@@ -38,43 +52,87 @@ export async function handleQuest() {
38
52
  {
39
53
  type: 'input',
40
54
  name: 'contexto',
41
- message: highlight('¿Algo específico que quieras o que ya hayas completado? (Enter para saltar):'),
55
+ message: highlight('¿Algo específico o quests ya completadas? (Enter para saltar):'),
42
56
  },
43
57
  ]);
44
58
 
45
- const prompt = `Dame recomendaciones de quests para mi personaje.
59
+ // ── PASO 1: IA sugiere nombres de quests ──────────────────────────────────
60
+ console.log('');
61
+ const spinner1 = ora({ text: 'Buscando quests recomendadas...', color: 'yellow' }).start();
46
62
 
47
- Objetivo: ${answers.objetivo}
48
- Party disponible: ${answers.party ? 'Sí' : 'No (solo)'}
49
- ${answers.contexto ? `Contexto adicional: ${answers.contexto}` : ''}
63
+ const stepOnePrompt = `Personaje: ${character.name} | ${character.vocation} | Level ${character.level} | Mundo: ${character.world}
64
+ Objetivo: ${answers.objetivo} | Party disponible: ${answers.party ? 'Sí' : 'No'}
65
+ ${answers.contexto ? `Contexto: ${answers.contexto}` : ''}
50
66
 
51
- Dame entre 3 y 5 quests recomendadas para mi level y vocación.
67
+ Lista los nombres exactos de 4 a 6 quests de Tibia recomendadas para este personaje.
68
+ Responde SOLO con un array JSON, sin texto adicional:
69
+ ["Nombre Quest 1", "Nombre Quest 2", ...]
70
+ Solo quests que existen en Tibia. Nombres exactos como aparecen en TibiaWiki (generalmente terminan en "Quest").`;
52
71
 
53
- Para cada quest incluye:
54
- 1. Nombre de la quest
55
- 2. Por qué es relevante para mi perfil y objetivo
56
- 3. Level mínimo recomendado
57
- 4. Prerequisitos (otras quests, items, etc.)
58
- 5. Dificultad estimada (fácil/media/difícil)
59
- 6. Reward principal
60
- 7. ¿Necesita party o se puede hacer solo?
61
- 8. Tip o advertencia importante
72
+ let questNames = [];
73
+ try {
74
+ const suggestion = await askClaude(stepOnePrompt);
75
+ spinner1.stop();
76
+ questNames = extractQuestNames(suggestion);
77
+ } catch (err) {
78
+ spinner1.fail('Error al consultar la IA.');
79
+ error(err.message);
80
+ return;
81
+ }
62
82
 
63
- Prioriza quests con mejor relación esfuerzo/recompensa para mi situación actual.`;
83
+ if (questNames.length === 0) {
84
+ error('No se pudieron extraer nombres de quests.');
85
+ return;
86
+ }
64
87
 
88
+ // ── PASO 2: Validar contra TibiaWiki ─────────────────────────────────────
89
+ const spinner2 = ora({ text: `Verificando ${questNames.length} quests en TibiaWiki...`, color: 'cyan' }).start();
90
+ const { verified, notFound } = await validateQuests(questNames);
91
+ spinner2.stop();
92
+
93
+ const verifiedCount = Object.keys(verified).length;
94
+ console.log(chalk.hex('#4ade80')(` ✓ ${verifiedCount} quests verificadas en TibiaWiki`));
95
+ if (notFound.length > 0) {
96
+ console.log(chalk.hex('#888')(` ✗ No encontradas: ${notFound.join(', ')}`));
97
+ }
98
+
99
+ // ── PASO 3: IA arma la recomendación con datos reales ────────────────────
65
100
  console.log('');
66
- const spinner = ora({ text: 'Consultando el grimorio de quests...', color: 'yellow' }).start();
101
+ const spinner3 = ora({ text: 'Armando tu guía de quests...', color: 'yellow' }).start();
102
+
103
+ const questsData = formatQuestsForPrompt(verified);
104
+ const hasVerified = verifiedCount > 0;
105
+
106
+ const stepThreePrompt = `Personaje: ${character.name} | ${character.vocation} | Level ${character.level}
107
+ Objetivo: ${answers.objetivo} | Party disponible: ${answers.party ? 'Sí' : 'No'}
108
+ ${answers.contexto ? `Contexto: ${answers.contexto}` : ''}
109
+
110
+ ${hasVerified ? `QUESTS VERIFICADAS EN TIBIAWIKI con datos reales:
111
+ ${questsData}
112
+
113
+ Con estas quests verificadas, dame` : 'Dame'} 3 a 5 quests recomendadas para este personaje.
114
+
115
+ Para cada quest incluye:
116
+ 1. Nombre y por qué es relevante para el objetivo
117
+ 2. Level mínimo recomendado ${hasVerified ? '(usa el dato real de TibiaWiki)' : ''}
118
+ 3. Prerequisitos (otras quests, items necesarios)
119
+ 4. Dificultad estimada (fácil/media/difícil)
120
+ 5. Reward principal ${hasVerified ? '(usa el reward real de TibiaWiki)' : ''}
121
+ 6. ¿Solo o necesita party?
122
+ 7. Peligros principales y tip clave
123
+
124
+ Prioriza por mejor relación esfuerzo/recompensa para este personaje.`;
67
125
 
68
126
  try {
69
- const response = await askClaude(prompt);
70
- spinner.stop();
127
+ const response = await askClaude(stepThreePrompt);
128
+ spinner3.stop();
71
129
  separator();
72
130
  console.log('');
73
131
  console.log(chalk.hex('#f0f0f0')(response));
74
132
  console.log('');
75
133
  separator();
76
134
  } catch (err) {
77
- spinner.stop();
135
+ spinner3.stop();
78
136
  error(`Error al consultar la IA: ${err.message}`);
79
137
  }
80
138
  }
@@ -3,8 +3,33 @@ import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { getCharacter } from '../utils/config.js';
5
5
  import { askClaude } from '../utils/api.js';
6
+ import { validateItems, formatItemsForPrompt } from '../tibia-items.js';
6
7
  import { error, highlight, separator } from '../utils/banner.js';
7
8
 
9
+ const SLOTS = ['Helmet', 'Armor', 'Legs', 'Boots', 'Weapon', 'Offhand', 'Amulet', 'Ring'];
10
+
11
+ function extractItemNames(text) {
12
+ // Intenta parsear JSON del response de la IA
13
+ try {
14
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
15
+ if (jsonMatch) {
16
+ const obj = JSON.parse(jsonMatch[0]);
17
+ return Object.values(obj).filter(v => typeof v === 'string' && v.trim().length > 0);
18
+ }
19
+ } catch {}
20
+
21
+ // Fallback: extraer líneas con formato "Slot: Item Name"
22
+ const names = [];
23
+ for (const line of text.split('\n')) {
24
+ const match = line.match(/:\s*(.+)$/);
25
+ if (match) {
26
+ const val = match[1].trim().replace(/["""]/g, '');
27
+ if (val && val.length > 2 && !val.startsWith('-')) names.push(val);
28
+ }
29
+ }
30
+ return names;
31
+ }
32
+
8
33
  export async function handleSet() {
9
34
  const character = getCharacter();
10
35
  if (!character) {
@@ -50,45 +75,94 @@ export async function handleSet() {
50
75
  },
51
76
  ]);
52
77
 
53
- const prompt = `Arma el set óptimo para mi personaje en esta situación.
78
+ // ── PASO 1: La IA sugiere nombres de ítems ───────────────────────────────
79
+ console.log('');
80
+ const spinner1 = ora({ text: 'Consultando sugerencias de ítems...', color: 'yellow' }).start();
54
81
 
55
- Situación: ${answers.situacion}
56
- Presupuesto: ${answers.budget}
57
- ${answers.zona ? `Zona/criaturas: ${answers.zona}` : ''}
82
+ const stepOnePrompt = `Personaje: ${character.name} | ${character.vocation} | Level ${character.level} | Mundo: ${character.world}
83
+ Situación: ${answers.situacion} | Presupuesto: ${answers.budget}
84
+ ${answers.zona ? `Zona: ${answers.zona}` : ''}
58
85
  ${answers.items_actuales ? `Equipo actual: ${answers.items_actuales}` : ''}
59
86
 
60
- Dame:
61
- 1. SET RECOMENDADO - slot por slot:
62
- - Helmet / Armor / Legs / Boots / Shield (si aplica) / Weapon / Offhand / Amulet / Ring / Backpack
87
+ Lista los mejores ítems de Tibia para este personaje, uno por slot.
88
+ Responde SOLO con un JSON con este formato exacto, sin texto adicional:
89
+ {
90
+ "Helmet": "nombre exacto del ítem",
91
+ "Armor": "nombre exacto del ítem",
92
+ "Legs": "nombre exacto del ítem",
93
+ "Boots": "nombre exacto del ítem",
94
+ "Weapon": "nombre exacto del ítem",
95
+ "Offhand": "nombre exacto del ítem o null si no aplica",
96
+ "Amulet": "nombre exacto del ítem",
97
+ "Ring": "nombre exacto del ítem"
98
+ }
99
+ Solo ítems que existen en Tibia. Nombres exactos como aparecen en el juego.`;
63
100
 
64
- 2. IMBUEMENTS - por cada pieza que aplique:
65
- - Qué imbu poner y por qué
66
- - Versión económica vs versión ideal
101
+ let itemNames = [];
102
+ try {
103
+ const suggestion = await askClaude(stepOnePrompt);
104
+ spinner1.stop();
105
+ itemNames = extractItemNames(suggestion);
106
+ } catch (err) {
107
+ spinner1.fail('Error al consultar la IA.');
108
+ error(err.message);
109
+ return;
110
+ }
67
111
 
68
- 3. SUPPLIES recomendados por hora de hunt:
69
- - Potiones (tipo y cantidad)
70
- - Runes (si aplica)
71
- - Food
72
- - Otros consumibles importantes
112
+ if (itemNames.length === 0) {
113
+ error('No se pudieron extraer nombres de ítems de la respuesta.');
114
+ return;
115
+ }
116
+
117
+ // ── PASO 2: Validar contra TibiaWiki ─────────────────────────────────────
118
+ const spinner2 = ora({ text: `Verificando ${itemNames.length} ítems en TibiaWiki...`, color: 'cyan' }).start();
119
+ const { verified, notFound } = await validateItems(itemNames);
120
+ spinner2.stop();
121
+
122
+ const verifiedCount = Object.keys(verified).length;
73
123
 
74
- 4. ALTERNATIVAS BUDGET-FRIENDLY:
75
- - Opciones más baratas para cada slot crítico
124
+ if (verifiedCount === 0) {
125
+ error('No se pudo verificar ningún ítem en TibiaWiki. Intenta de nuevo.');
126
+ return;
127
+ }
76
128
 
77
- específico con nombres de ítems. Explica brevemente el por qué de cada elección importante.`;
129
+ console.log(chalk.hex('#4ade80')(` ✓ ${verifiedCount} ítems verificados en TibiaWiki`));
130
+ if (notFound.length > 0) {
131
+ console.log(chalk.hex('#888')(` ✗ No encontrados: ${notFound.join(', ')}`));
132
+ }
78
133
 
134
+ // ── PASO 3: La IA arma el set final con stats reales ─────────────────────
79
135
  console.log('');
80
- const spinner = ora({ text: 'Armando tu set óptimo...', color: 'yellow' }).start();
136
+ const spinner3 = ora({ text: 'Armando tu set con datos reales...', color: 'yellow' }).start();
137
+
138
+ const verifiedItemsList = formatItemsForPrompt(verified);
139
+
140
+ const stepThreePrompt = `Personaje: ${character.name} | ${character.vocation} | Level ${character.level}
141
+ Situación: ${answers.situacion} | Presupuesto: ${answers.budget}
142
+ ${answers.zona ? `Zona: ${answers.zona}` : ''}
143
+ ${answers.items_actuales ? `Equipo actual: ${answers.items_actuales}` : ''}
144
+
145
+ Ítems verificados en TibiaWiki con sus stats reales:
146
+ ${verifiedItemsList}
147
+
148
+ Con estos ítems verificados, dame:
149
+ 1. SET RECOMENDADO — slot por slot con justificación breve
150
+ 2. IMBUEMENTS — qué imbu poner en cada pieza con imbu slots, versión económica vs ideal
151
+ 3. SUPPLIES recomendados por hora de hunt
152
+ 4. ALTERNATIVAS más baratas para los slots más caros
153
+
154
+ Usa solo los ítems verificados de la lista. Si un slot quedó sin verificar, menciónalo.`;
81
155
 
82
156
  try {
83
- const response = await askClaude(prompt);
84
- spinner.stop();
157
+ const response = await askClaude(stepThreePrompt);
158
+ spinner3.stop();
85
159
  separator();
86
160
  console.log('');
87
161
  console.log(chalk.hex('#f0f0f0')(response));
88
162
  console.log('');
89
163
  separator();
90
164
  } catch (err) {
91
- spinner.stop();
165
+ spinner3.stop();
92
166
  error(`Error al consultar la IA: ${err.message}`);
93
167
  }
94
168
  }
@@ -0,0 +1,218 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ const TIBIAWIKI_BASE = 'https://tibiawiki.dev/api/items';
4
+ const CREATURES_BASE = 'https://tibiawiki.dev/api/creatures';
5
+ const QUESTS_BASE = 'https://tibiawiki.dev/api/quests';
6
+
7
+ export async function fetchItemDetails(itemName) {
8
+ try {
9
+ const res = await fetch(`${TIBIAWIKI_BASE}/${encodeURIComponent(itemName)}`, {
10
+ headers: { 'Accept': 'application/json' },
11
+ timeout: 6000,
12
+ });
13
+ if (!res.ok) return null;
14
+ const data = await res.json();
15
+ if (!data || !data.name) return null;
16
+ return {
17
+ name: data.name,
18
+ slot: data.slot || null,
19
+ armor: data.armor || null,
20
+ attack: data.attack || null,
21
+ defense: data.def || null,
22
+ levelRequired: data.levelrequired || null,
23
+ vocRequired: data.vocrequired || null,
24
+ imbuSlots: data.imbueslots || null,
25
+ attrib: data.attrib || null,
26
+ resist: data.resist || null,
27
+ primaryType: data.primarytype || null,
28
+ };
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export async function validateItems(itemNames) {
35
+ const results = await Promise.all(
36
+ itemNames.map(async (name) => {
37
+ const data = await fetchItemDetails(name);
38
+ return { name, data };
39
+ })
40
+ );
41
+
42
+ const verified = {};
43
+ const notFound = [];
44
+
45
+ for (const { name, data } of results) {
46
+ if (data) {
47
+ verified[name] = data;
48
+ } else {
49
+ notFound.push(name);
50
+ }
51
+ }
52
+
53
+ return { verified, notFound };
54
+ }
55
+
56
+ export function formatItemsForPrompt(verified) {
57
+ return Object.values(verified).map(item => {
58
+ const parts = [`${item.name}`];
59
+ if (item.slot) parts.push(`slot: ${item.slot}`);
60
+ if (item.armor) parts.push(`armor: ${item.armor}`);
61
+ if (item.attack) parts.push(`attack: ${item.attack}`);
62
+ if (item.defense) parts.push(`def: ${item.defense}`);
63
+ if (item.levelRequired) parts.push(`level req: ${item.levelRequired}`);
64
+ if (item.vocRequired) parts.push(`voc: ${item.vocRequired}`);
65
+ if (item.imbuSlots) parts.push(`imbu slots: ${item.imbuSlots}`);
66
+ if (item.attrib) parts.push(`attrib: ${item.attrib}`);
67
+ if (item.resist) parts.push(`resist: ${item.resist}`);
68
+ return parts.join(' | ');
69
+ }).join('\n');
70
+ }
71
+
72
+ // ── Wiki markup cleanup ────────────────────────────────────────────────────
73
+
74
+ function stripWiki(text) {
75
+ if (!text) return null;
76
+ return text
77
+ .replace(/\[\[([^\]|]+\|)?([^\]]+)\]\]/g, '$2') // [[link|text]] → text
78
+ .replace(/\{\{Max Damage\|([^}]+)\}\}/g, (_, inner) => {
79
+ // {{Max Damage|physical=500|fire=250}} → "físico 500, fuego 250"
80
+ return inner.split('|').map(part => {
81
+ const [k, v] = part.split('=');
82
+ return v ? `${k} ${v}` : k;
83
+ }).join(', ');
84
+ })
85
+ .replace(/\{\{[^}]+\}\}/g, '') // remove remaining templates
86
+ .replace(/'''|''|<[^>]+>/g, '') // bold, italic, html tags
87
+ .trim();
88
+ }
89
+
90
+ // ── Creatures ─────────────────────────────────────────────────────────────
91
+
92
+ export async function fetchCreature(name) {
93
+ try {
94
+ const res = await fetch(`${CREATURES_BASE}/${encodeURIComponent(name)}`, {
95
+ headers: { 'Accept': 'application/json' },
96
+ timeout: 6000,
97
+ });
98
+ if (!res.ok) return null;
99
+ const d = await res.json();
100
+ if (!d || !d.name) return null;
101
+ return {
102
+ name: d.name,
103
+ hp: d.hp || null,
104
+ exp: d.exp || null,
105
+ maxdmg: stripWiki(d.maxdmg),
106
+ armor: d.armor || null,
107
+ location: stripWiki(d.location),
108
+ isboss: d.isboss || null,
109
+ bestiarylevel: d.bestiarylevel || null,
110
+ paraimmune: d.paraimmune || null,
111
+ physicalDmgMod: d.physicalDmgMod || null,
112
+ fireDmgMod: d.fireDmgMod || null,
113
+ iceDmgMod: d.iceDmgMod || null,
114
+ energyDmgMod: d.energyDmgMod || null,
115
+ deathDmgMod: d.deathDmgMod || null,
116
+ holyDmgMod: d.holyDmgMod || null,
117
+ earthDmgMod: d.earthDmgMod || null,
118
+ loot: d.loot || [],
119
+ };
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ export async function validateCreatures(names) {
126
+ const results = await Promise.all(names.map(async (name) => {
127
+ const data = await fetchCreature(name);
128
+ return { name, data };
129
+ }));
130
+ const verified = {};
131
+ const notFound = [];
132
+ for (const { name, data } of results) {
133
+ if (data) verified[name] = data;
134
+ else notFound.push(name);
135
+ }
136
+ return { verified, notFound };
137
+ }
138
+
139
+ export function formatCreatureForPrompt(c) {
140
+ const parts = [c.name];
141
+ if (c.hp) parts.push(`HP: ${c.hp}`);
142
+ if (c.exp) parts.push(`Exp: ${c.exp}`);
143
+ if (c.maxdmg) parts.push(`Max dmg: ${c.maxdmg}`);
144
+ if (c.location) parts.push(`Ubicación: ${c.location}`);
145
+ if (c.bestiarylevel) parts.push(`Bestiary: ${c.bestiarylevel}`);
146
+ const mods = [];
147
+ if (c.physicalDmgMod && c.physicalDmgMod !== '100%') mods.push(`físico ${c.physicalDmgMod}`);
148
+ if (c.fireDmgMod && c.fireDmgMod !== '100%') mods.push(`fuego ${c.fireDmgMod}`);
149
+ if (c.iceDmgMod && c.iceDmgMod !== '100%') mods.push(`hielo ${c.iceDmgMod}`);
150
+ if (c.energyDmgMod && c.energyDmgMod !== '100%') mods.push(`energía ${c.energyDmgMod}`);
151
+ if (c.deathDmgMod && c.deathDmgMod !== '100%') mods.push(`muerte ${c.deathDmgMod}`);
152
+ if (c.holyDmgMod && c.holyDmgMod !== '100%') mods.push(`sagrado ${c.holyDmgMod}`);
153
+ if (c.earthDmgMod && c.earthDmgMod !== '100%') mods.push(`tierra ${c.earthDmgMod}`);
154
+ if (mods.length) parts.push(`Resistencias: ${mods.join(', ')}`);
155
+ if (c.paraimmune === 'yes') parts.push('Inmune a parálisis');
156
+ if (c.loot.length > 0) {
157
+ const topLoot = c.loot.slice(0, 6).map(l => l.itemName).join(', ');
158
+ parts.push(`Loot: ${topLoot}`);
159
+ }
160
+ return parts.join(' | ');
161
+ }
162
+
163
+ export function formatCreaturesForPrompt(verified) {
164
+ return Object.values(verified).map(formatCreatureForPrompt).join('\n');
165
+ }
166
+
167
+ // ── Quests ────────────────────────────────────────────────────────────────
168
+
169
+ export async function fetchQuest(name) {
170
+ try {
171
+ const res = await fetch(`${QUESTS_BASE}/${encodeURIComponent(name)}`, {
172
+ headers: { 'Accept': 'application/json' },
173
+ timeout: 6000,
174
+ });
175
+ if (!res.ok) return null;
176
+ const d = await res.json();
177
+ if (!d || !d.name) return null;
178
+ return {
179
+ name: d.name,
180
+ lvl: d.lvl || null,
181
+ lvlrec: d.lvlrec || null,
182
+ premium: d.premium || null,
183
+ reward: stripWiki(d.reward),
184
+ dangers: stripWiki(d.dangers),
185
+ location: stripWiki(d.location),
186
+ legend: stripWiki(d.legend),
187
+ };
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ export async function validateQuests(names) {
194
+ const results = await Promise.all(names.map(async (name) => {
195
+ const data = await fetchQuest(name);
196
+ return { name, data };
197
+ }));
198
+ const verified = {};
199
+ const notFound = [];
200
+ for (const { name, data } of results) {
201
+ if (data) verified[name] = data;
202
+ else notFound.push(name);
203
+ }
204
+ return { verified, notFound };
205
+ }
206
+
207
+ export function formatQuestsForPrompt(verified) {
208
+ return Object.values(verified).map(q => {
209
+ const parts = [q.name];
210
+ if (q.lvl) parts.push(`Level mín: ${q.lvl}`);
211
+ if (q.lvlrec) parts.push(`Level rec: ${q.lvlrec}`);
212
+ if (q.premium) parts.push(`Premium: ${q.premium}`);
213
+ if (q.reward) parts.push(`Reward: ${q.reward}`);
214
+ if (q.dangers) parts.push(`Peligros: ${q.dangers}`);
215
+ if (q.location) parts.push(`Ubicación: ${q.location}`);
216
+ return parts.join(' | ');
217
+ }).join('\n');
218
+ }