feat: craft/drops — 10 matériaux, 12 recettes, drop rate variable
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 32s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 32s
- 10 matériaux Égouts/Désert (Poil de Rat → Œil du Sphinx) - 12 items craftables dont 1 legendary (Sceptre Prophétique) - 12 recettes cross-zone avec ingrédients cohérents - 15 monstres mappés à leur drop (tous les Égouts/Désert) - Drop rate variable par difficulté relative (25-80%) - Quantité drop variable (1-3 selon boss/difficulté)
This commit is contained in:
@@ -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 {
|
||||
|
||||
337
src/database/seed-craft-drops.ts
Normal file
337
src/database/seed-craft-drops.ts
Normal file
@@ -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<Material>[] = [
|
||||
// É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<Item>[] = [
|
||||
// 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<string, string> = {
|
||||
// É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<string, string> = {};
|
||||
|
||||
// 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<string, string> = {};
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user