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 +33 -11
- package/commands/hunt.md +29 -6
- package/commands/quest.md +33 -10
- package/package.json +1 -1
- package/src/commands/boss.js +35 -40
- package/src/commands/hunt.js +76 -18
- package/src/commands/quest.js +78 -20
- package/src/commands/set.js +96 -22
- package/src/tibia-items.js +218 -0
package/commands/boss.md
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Detailed boss mechanics, phases, strategy by vocation, and loot
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
-
|
|
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
package/src/commands/boss.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
50
|
+
// ── Prompt con datos reales ───────────────────────────────────────────────
|
|
51
|
+
const spinner2 = ora({ text: 'Consultando el libro de los bosses...', color: 'yellow' }).start();
|
|
63
52
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
53
|
+
const prompt = `Personaje: ${character.name} | ${character.vocation} | Level ${character.level}
|
|
54
|
+
${contextBlock}
|
|
55
|
+
Explícame todo sobre el boss: ${targetBoss}
|
|
67
56
|
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
spinner2.stop();
|
|
83
78
|
error(`Error al consultar la IA: ${err.message}`);
|
|
84
79
|
}
|
|
85
80
|
}
|
package/src/commands/hunt.js
CHANGED
|
@@ -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? (
|
|
63
|
+
message: highlight('¿Alguna restricción o preferencia? (Enter para saltar):'),
|
|
49
64
|
},
|
|
50
65
|
]);
|
|
51
66
|
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
125
|
+
3. Profit estimado
|
|
65
126
|
4. Requisitos de acceso (quests, level, etc.)
|
|
66
|
-
5. Tips específicos para
|
|
67
|
-
6. ¿
|
|
127
|
+
5. Tips específicos para ${character.vocation}
|
|
128
|
+
6. ¿Funciona solo o necesita party?
|
|
68
129
|
|
|
69
|
-
|
|
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(
|
|
76
|
-
|
|
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
|
-
|
|
141
|
+
spinner3.stop();
|
|
84
142
|
error(`Error al consultar la IA: ${err.message}`);
|
|
85
143
|
}
|
|
86
144
|
}
|
package/src/commands/quest.js
CHANGED
|
@@ -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
|
|
55
|
+
message: highlight('¿Algo específico o quests ya completadas? (Enter para saltar):'),
|
|
42
56
|
},
|
|
43
57
|
]);
|
|
44
58
|
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
Party disponible: ${answers.party ? 'Sí' : 'No
|
|
49
|
-
${answers.contexto ? `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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
70
|
-
|
|
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
|
-
|
|
135
|
+
spinner3.stop();
|
|
78
136
|
error(`Error al consultar la IA: ${err.message}`);
|
|
79
137
|
}
|
|
80
138
|
}
|
package/src/commands/set.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
-
Presupuesto: ${answers.budget}
|
|
57
|
-
${answers.zona ? `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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
124
|
+
if (verifiedCount === 0) {
|
|
125
|
+
error('No se pudo verificar ningún ítem en TibiaWiki. Intenta de nuevo.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
76
128
|
|
|
77
|
-
|
|
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
|
|
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(
|
|
84
|
-
|
|
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
|
-
|
|
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
|
+
}
|