diff --git a/src/combat/combat.service.ts b/src/combat/combat.service.ts index 4b651d4..1700d81 100644 --- a/src/combat/combat.service.ts +++ b/src/combat/combat.service.ts @@ -19,6 +19,36 @@ import { } from './combat.engine'; const COMBAT_ENDURANCE_COST = 5; + +/** + * Drop rate variable basé sur la difficulté relative monstre vs joueur. + * Monstre facile = moins de drop, monstre difficile = plus de drop + quantité. + * Boss de zone (maxLevel ≥ 9 et spread ≥ 3) = 80% + 2-3 drops. + */ +function computeDropRate( + playerLevel: number, + monsterMinLevel: number, + monsterMaxLevel: number, +): { dropRate: number; dropQty: number } { + const monsterAvgLevel = (monsterMinLevel + monsterMaxLevel) / 2; + const diff = monsterAvgLevel - playerLevel; + const isBoss = (monsterMaxLevel - monsterMinLevel) >= 3 && monsterMaxLevel >= 9; + + if (isBoss) { + return { dropRate: 0.80, dropQty: 2 + (Math.random() < 0.5 ? 1 : 0) }; // 2-3 + } + if (diff >= 2) { + return { dropRate: 0.60, dropQty: 1 + (Math.random() < 0.3 ? 1 : 0) }; // 1-2 + } + if (diff >= 0) { + return { dropRate: 0.50, dropQty: 1 + (Math.random() < 0.2 ? 1 : 0) }; // 1-2 + } + if (diff >= -2) { + return { dropRate: 0.40, dropQty: 1 }; + } + // Très facile (level >> monstre) + return { dropRate: 0.25, dropQty: 1 }; +} const DEFEAT_ENDURANCE_PENALTY = 25; const DEFEAT_HP_RATIO = 0.2; // 20% hpMax à la défaite const VICTORY_HP_REGEN_RATIO = 0.1; // +10% hpMax à la victoire @@ -166,13 +196,16 @@ export class CombatService { } } - // Loot matériaux — 40% de chance après victoire + // Loot matériaux — drop rate variable par difficulté relative let lootMaterial: { name: string; quantity: number } | null = null; let lootedMaterialId: string | null = null; - if (result.winner === 'player' && monster.dropMaterialId && Math.random() < 0.4) { - await this.materialService.addMaterial(character.id, monster.dropMaterialId, 1); - lootMaterial = { name: 'matériau', quantity: 1 }; - lootedMaterialId = monster.dropMaterialId; + if (result.winner === 'player' && monster.dropMaterialId) { + const { dropRate, dropQty } = computeDropRate(character.level, monster.minLevel, monster.maxLevel); + if (Math.random() < dropRate) { + await this.materialService.addMaterial(character.id, monster.dropMaterialId, dropQty); + lootMaterial = { name: 'matériau', quantity: dropQty }; + lootedMaterialId = monster.dropMaterialId; + } } // Persister le log @@ -202,7 +235,7 @@ export class CombatService { } if (lootMaterial) { - summaryParts.push(`Loot : 1 matériau obtenu !`); + summaryParts.push(`Loot : ${lootMaterial.quantity} matériau${lootMaterial.quantity > 1 ? 'x' : ''} obtenu${lootMaterial.quantity > 1 ? 's' : ''} !`); } return { diff --git a/src/database/seed-craft-drops.ts b/src/database/seed-craft-drops.ts new file mode 100644 index 0000000..3e967fd --- /dev/null +++ b/src/database/seed-craft-drops.ts @@ -0,0 +1,337 @@ +import 'reflect-metadata'; +import { AppDataSource } from './data-source'; +import { Material } from '../material/material.entity'; +import { Item } from '../item/item.entity'; +import { Recipe } from '../craft/recipe.entity'; +import { Monster } from '../monster/monster.entity'; + +// ────────────────────────────────────────────── +// MATÉRIAUX — 10 nouveaux (Égouts + Désert) +// ────────────────────────────────────────────── + +const NEW_MATERIALS: Partial[] = [ + // Égouts + { name: 'Poil de Rat', description: 'Poil rêche arraché à un rat d\'égout. Sert de rembourrage.', rarity: 'common' }, + { name: 'Gelée Toxique', description: 'Substance corrosive récupérée sur un slime.', rarity: 'common' }, + { name: 'Fil de Soie Géant', description: 'Fil incroyablement résistant tissé par une araignée géante.', rarity: 'rare' }, + { name: 'Cuir de Croco', description: 'Cuir épais et écailleux, presque impénétrable.', rarity: 'rare' }, + { name: 'Couronne du Roi', description: 'Couronne tordue portée par le Roi des Rats. Irradie de pouvoir.', rarity: 'epic' }, + // Désert + { name: 'Dard de Scorpion', description: 'Dard venimeux encore suintant.', rarity: 'common' }, + { name: 'Plume de Vautour', description: 'Plume noire et résistante, légère comme le vent.', rarity: 'common' }, + { name: 'Bandelette Maudite', description: 'Tissu ancien imprégné de magie noire.', rarity: 'rare' }, + { name: 'Sable Cristallisé', description: 'Grain de sable fusionné en cristal par la chaleur du désert.', rarity: 'rare' }, + { name: 'Œil du Sphinx', description: 'Gemme mystique arrachée au Sphinx. Pulse d\'énergie ancienne.', rarity: 'epic' }, +]; + +// ────────────────────────────────────────────── +// ITEMS CRAFTABLES — 12 nouveaux +// ────────────────────────────────────────────── + +const NEW_ITEMS: Partial[] = [ + // Marais (2 nouveaux) + { name: 'Potion Antipoison', type: 'consumable', rarity: 'common', attackBonus: 0, defenseBonus: 0, forceBonus: 20, buyPrice: 0, minLevel: 1, zone: null, description: 'Neutralise les poisons et restaure 20 endurance.' }, + { name: 'Bouclier de Boue', type: 'armor', rarity: 'common', attackBonus: 0, defenseBonus: 4, buyPrice: 0, minLevel: 2, zone: 'marais', description: 'Forgé dans la boue cristallisée des golems.' }, + // Égouts (5 nouveaux) + { name: 'Lame Empoisonnée', type: 'weapon', rarity: 'rare', attackBonus: 11, defenseBonus: 0, buyPrice: 0, minLevel: 5, zone: 'egouts', description: 'Chaque coup inflige un poison insidieux.' }, + { name: 'Armure de Soie', type: 'armor', rarity: 'rare', attackBonus: 0, defenseBonus: 8, agiliteBonus: 2, buyPrice: 0, minLevel: 5, zone: 'egouts', description: 'Légère et résistante, tissée par des araignées géantes.' }, + { name: 'Plastron de Croco', type: 'armor', rarity: 'rare', attackBonus: 0, defenseBonus: 11, buyPrice: 0, minLevel: 7, zone: 'egouts', description: 'Cuir de crocodile assemblé en plastron imposant.' }, + { name: 'Grande Potion de Soin', type: 'consumable', rarity: 'rare', attackBonus: 0, defenseBonus: 0, forceBonus: 0, buyPrice: 0, minLevel: 4, zone: null, description: 'Restaure 80% des PV.' }, + { name: 'Épée du Roi', type: 'weapon', rarity: 'epic', attackBonus: 16, defenseBonus: 0, buyPrice: 0, minLevel: 8, zone: 'egouts', description: 'Forgée avec la couronne du Roi des Rats.' }, + // Désert (5 nouveaux) + { name: 'Dague du Scorpion', type: 'weapon', rarity: 'rare', attackBonus: 14, defenseBonus: 0, buyPrice: 0, minLevel: 9, zone: 'desert', description: 'Le venin du scorpion suinte encore de la lame.' }, + { name: 'Cape du Vautour', type: 'armor', rarity: 'rare', attackBonus: 0, defenseBonus: 12, agiliteBonus: 3, buyPrice: 0, minLevel: 9, zone: 'desert', description: 'Cape de plumes noires, silencieuse et protectrice.' }, + { name: 'Armure de la Momie', type: 'armor', rarity: 'epic', attackBonus: 0, defenseBonus: 16, buyPrice: 0, minLevel: 11, zone: 'desert', description: 'Bandelettes enchantées, dures comme le roc.' }, + { name: 'Lame du Désert', type: 'weapon', rarity: 'epic', attackBonus: 22, defenseBonus: 0, buyPrice: 0, minLevel: 11, zone: 'desert', description: 'Cristal de sable fusionné en lame tranchante.' }, + { name: 'Sceptre Prophétique', type: 'weapon', rarity: 'legendary', attackBonus: 30, defenseBonus: 0, intelligenceBonus: 5, buyPrice: 0, minLevel: 13, zone: 'desert', description: 'Seul artefact capable de canaliser la vision du Sphinx.' }, +]; + +// ────────────────────────────────────────────── +// MONSTER → MATERIAL LOOT MAPPING +// ────────────────────────────────────────────── + +const MONSTER_LOOT: Record = { + // Égouts + 'Rat d\'Égout': 'Poil de Rat', + 'Slime Toxique': 'Gelée Toxique', + 'Araignée Géante': 'Fil de Soie Géant', + 'Crocodile': 'Cuir de Croco', + 'Roi des Rats': 'Couronne du Roi', + // Désert + 'Scorpion': 'Dard de Scorpion', + 'Vautour': 'Plume de Vautour', + 'Momie': 'Bandelette Maudite', + 'Ver des Sables': 'Sable Cristallisé', + 'Sphinx': 'Œil du Sphinx', +}; + +// ────────────────────────────────────────────── +// RECETTES — 12 nouvelles (nom ingrédient → résolu en runtime) +// ────────────────────────────────────────────── + +interface RecipeDef { + name: string; + resultItemName: string; + craftDurationSeconds: number; + enduranceCost: number; + ingredientNames: { materialName: string; quantity: number }[]; +} + +const NEW_RECIPES: RecipeDef[] = [ + // Marais (2) + { + name: 'Craft Potion Antipoison', + resultItemName: 'Potion Antipoison', + craftDurationSeconds: 10, + enduranceCost: 5, + ingredientNames: [ + { materialName: 'Spores Vénéneuses', quantity: 2 }, + { materialName: 'Venin de Serpent', quantity: 1 }, + ], + }, + { + name: 'Craft Bouclier de Boue', + resultItemName: 'Bouclier de Boue', + craftDurationSeconds: 25, + enduranceCost: 10, + ingredientNames: [ + { materialName: 'Fragment de Boue', quantity: 4 }, + { materialName: 'Écailles de Grenouille', quantity: 2 }, + ], + }, + // Égouts (5) + { + name: 'Craft Lame Empoisonnée', + resultItemName: 'Lame Empoisonnée', + craftDurationSeconds: 45, + enduranceCost: 12, + ingredientNames: [ + { materialName: 'Gelée Toxique', quantity: 2 }, + { materialName: 'Venin de Serpent', quantity: 1 }, + { materialName: 'Poil de Rat', quantity: 3 }, + ], + }, + { + name: 'Craft Armure de Soie', + resultItemName: 'Armure de Soie', + craftDurationSeconds: 60, + enduranceCost: 14, + ingredientNames: [ + { materialName: 'Fil de Soie Géant', quantity: 4 }, + { materialName: 'Poil de Rat', quantity: 2 }, + ], + }, + { + name: 'Craft Plastron de Croco', + resultItemName: 'Plastron de Croco', + craftDurationSeconds: 90, + enduranceCost: 16, + ingredientNames: [ + { materialName: 'Cuir de Croco', quantity: 3 }, + { materialName: 'Fragment de Boue', quantity: 2 }, + { materialName: 'Fil de Soie Géant', quantity: 1 }, + ], + }, + { + name: 'Craft Grande Potion de Soin', + resultItemName: 'Grande Potion de Soin', + craftDurationSeconds: 20, + enduranceCost: 8, + ingredientNames: [ + { materialName: 'Gelée Toxique', quantity: 2 }, + { materialName: 'Spores Vénéneuses', quantity: 2 }, + ], + }, + { + name: 'Craft Épée du Roi', + resultItemName: 'Épée du Roi', + craftDurationSeconds: 120, + enduranceCost: 20, + ingredientNames: [ + { materialName: 'Couronne du Roi', quantity: 1 }, + { materialName: 'Cuir de Croco', quantity: 3 }, + { materialName: 'Gelée Toxique', quantity: 2 }, + ], + }, + // Désert (5) + { + name: 'Craft Dague du Scorpion', + resultItemName: 'Dague du Scorpion', + craftDurationSeconds: 60, + enduranceCost: 14, + ingredientNames: [ + { materialName: 'Dard de Scorpion', quantity: 3 }, + { materialName: 'Plume de Vautour', quantity: 2 }, + ], + }, + { + name: 'Craft Cape du Vautour', + resultItemName: 'Cape du Vautour', + craftDurationSeconds: 90, + enduranceCost: 16, + ingredientNames: [ + { materialName: 'Plume de Vautour', quantity: 4 }, + { materialName: 'Bandelette Maudite', quantity: 2 }, + ], + }, + { + name: 'Craft Armure de la Momie', + resultItemName: 'Armure de la Momie', + craftDurationSeconds: 150, + enduranceCost: 20, + ingredientNames: [ + { materialName: 'Bandelette Maudite', quantity: 3 }, + { materialName: 'Sable Cristallisé', quantity: 2 }, + { materialName: 'Cuir de Croco', quantity: 1 }, + ], + }, + { + name: 'Craft Lame du Désert', + resultItemName: 'Lame du Désert', + craftDurationSeconds: 180, + enduranceCost: 22, + ingredientNames: [ + { materialName: 'Sable Cristallisé', quantity: 3 }, + { materialName: 'Dard de Scorpion', quantity: 2 }, + { materialName: 'Bandelette Maudite', quantity: 1 }, + ], + }, + { + name: 'Craft Sceptre Prophétique', + resultItemName: 'Sceptre Prophétique', + craftDurationSeconds: 300, + enduranceCost: 25, + ingredientNames: [ + { materialName: 'Œil du Sphinx', quantity: 1 }, + { materialName: 'Sable Cristallisé', quantity: 3 }, + { materialName: 'Bandelette Maudite', quantity: 2 }, + ], + }, +]; + +// ────────────────────────────────────────────── +// SEED RUNNER +// ────────────────────────────────────────────── + +async function seed() { + await AppDataSource.initialize(); + console.log('DB connectée (MySQL)'); + + const materialRepo = AppDataSource.getRepository(Material); + const itemRepo = AppDataSource.getRepository(Item); + const recipeRepo = AppDataSource.getRepository(Recipe); + const monsterRepo = AppDataSource.getRepository(Monster); + + // 1. Seed matériaux + console.log('\n── Matériaux ──'); + const materialMap: Record = {}; + + // Charger les matériaux existants dans le map + const existingMats = await materialRepo.find(); + for (const mat of existingMats) { + materialMap[mat.name] = mat.id; + } + + for (const data of NEW_MATERIALS) { + if (materialMap[data.name!]) { + console.log(`⏭ ${data.name} (déjà présent)`); + continue; + } + const mat = await materialRepo.save(materialRepo.create(data)); + materialMap[mat.name] = mat.id; + console.log(`✅ ${data.name} (${data.rarity})`); + } + + // 2. Seed items craftables + console.log('\n── Items craftables ──'); + const itemMap: Record = {}; + + const existingItems = await itemRepo.find(); + for (const item of existingItems) { + itemMap[item.name] = item.id; + } + + for (const data of NEW_ITEMS) { + if (itemMap[data.name!]) { + console.log(`⏭ ${data.name} (déjà présent)`); + continue; + } + const item = await itemRepo.save(itemRepo.create(data)); + itemMap[item.name] = item.id; + console.log(`✅ ${data.name} (${data.rarity}, ${data.type})`); + } + + // 3. Mapper les drops des monstres Égouts/Désert + console.log('\n── Monster drops ──'); + for (const [monsterName, materialName] of Object.entries(MONSTER_LOOT)) { + const materialId = materialMap[materialName]; + if (!materialId) { + console.error(`❌ Matériau "${materialName}" introuvable pour ${monsterName}`); + continue; + } + const monster = await monsterRepo.findOne({ where: { name: monsterName } }); + if (!monster) { + console.error(`❌ Monstre "${monsterName}" introuvable`); + continue; + } + if (monster.dropMaterialId === materialId) { + console.log(`⏭ ${monsterName} → ${materialName} (déjà mappé)`); + continue; + } + monster.dropMaterialId = materialId; + await monsterRepo.save(monster); + console.log(`✅ ${monsterName} → ${materialName}`); + } + + // 4. Seed recettes + console.log('\n── Recettes ──'); + for (const recipeDef of NEW_RECIPES) { + const exists = await recipeRepo.findOne({ where: { name: recipeDef.name } }); + if (exists) { + console.log(`⏭ ${recipeDef.name} (déjà présente)`); + continue; + } + + const resultItemId = itemMap[recipeDef.resultItemName]; + if (!resultItemId) { + console.error(`❌ Item "${recipeDef.resultItemName}" introuvable pour recette ${recipeDef.name}`); + continue; + } + + const ingredients = recipeDef.ingredientNames.map((ing) => { + const matId = materialMap[ing.materialName]; + if (!matId) { + throw new Error(`Matériau "${ing.materialName}" introuvable pour recette ${recipeDef.name}`); + } + return { materialId: matId, quantity: ing.quantity }; + }); + + await recipeRepo.save( + recipeRepo.create({ + name: recipeDef.name, + resultItemId, + craftDurationSeconds: recipeDef.craftDurationSeconds, + enduranceCost: recipeDef.enduranceCost, + ingredients, + }), + ); + console.log(`✅ ${recipeDef.name} (${recipeDef.ingredientNames.length} ingrédients, ${recipeDef.craftDurationSeconds}s)`); + } + + // Résumé + const totalMats = await materialRepo.count(); + const totalItems = await itemRepo.count(); + const totalRecipes = await recipeRepo.count(); + const monstersWithDrop = await monsterRepo.count({ where: { dropMaterialId: undefined } as any }); + const totalMonsters = await monsterRepo.count(); + + console.log(`\n✅ Seed craft/drops terminé`); + console.log(` Matériaux: ${totalMats} | Items: ${totalItems} | Recettes: ${totalRecipes} | Monstres: ${totalMonsters}`); + + await AppDataSource.destroy(); +} + +seed().catch((err) => { + console.error('Seed craft/drops échoué :', err); + process.exit(1); +});