feat(sprint5): quest system + arcs + rebalance endurance/damage/xp
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s

Quest system:
  4 entities (quest_arcs, quests, player_quests, player_quest_arcs)
  Arc "Les Marais du Têtard" (4 quêtes narratives)
  3 quêtes standalone répétables (chasse/forge/craft)
  5 achievements liés (quests_completed + quest_arc_completed)
  Event-driven: combat/forge/craft/loot émettent quest.progress
  API: available, active, completed, accept, claim, arcs

Rebalance:
  Endurance coût combat 10→5, regen 6min→3min (20/h), repos 20→10
  Dégâts joueur +3 base (plus de combats de 13 tours au level 1)
  Défaite endurance penalty 50→25
  XP monstres réduite (25→8 Têtard, 130→50 Golem) — quêtes = source principale
This commit is contained in:
2026-03-24 16:34:37 +01:00
parent 93b34b1f7b
commit 7651f3d8aa
16 changed files with 775 additions and 6 deletions

176
src/database/quests-seed.ts Normal file
View File

@@ -0,0 +1,176 @@
import { DataSource } from 'typeorm';
import { QuestArc } from '../quest/quest-arc.entity';
import { Quest } from '../quest/quest.entity';
import { Achievement } from '../achievement/achievement.entity';
export async function seedQuests(dataSource: DataSource) {
const arcRepo = dataSource.getRepository(QuestArc);
const questRepo = dataSource.getRepository(Quest);
const achievementRepo = dataSource.getRepository(Achievement);
// --- Arc 1: Les Marais du Têtard ---
let arc = await arcRepo.findOne({ where: { name: 'Les Marais du Têtard' } });
if (!arc) {
arc = await arcRepo.save(arcRepo.create({
name: 'Les Marais du Têtard',
description: 'Les marais grouillent de créatures hostiles. Nettoyez la zone pour prouver votre valeur.',
zone: 'marais',
sortOrder: 1,
minLevel: 1,
}));
}
// Get monster IDs for targeted quests
const monsters = await dataSource.query('SELECT id, name FROM monsters');
const monsterMap = new Map<string, string>(monsters.map((m: any) => [m.name, m.id]));
const materials = await dataSource.query('SELECT id, name FROM materials');
const materialMap = new Map<string, string>(materials.map((m: any) => [m.name, m.id]));
const QUESTS = [
{
name: 'Premiers pas',
description: 'Éliminez 3 Têtards Vase pour sécuriser les abords du village.',
objectiveType: 'kill_monster',
objectiveTargetId: monsterMap.get('Têtard Vase') ?? null as string | null,
objectiveCount: 3,
rewardXp: 100,
rewardGold: 30,
rewardTitle: null,
arcId: arc.id,
arcOrder: 1,
minLevel: 1,
repeatable: false,
},
{
name: 'Chasseur de grenouilles',
description: 'Les Grenouilles Boueuses menacent les récoltes. Abattez-en 5.',
objectiveType: 'kill_monster',
objectiveTargetId: monsterMap.get('Grenouille Boueuse') ?? null as string | null,
objectiveCount: 5,
rewardXp: 200,
rewardGold: 60,
rewardTitle: null,
arcId: arc.id,
arcOrder: 2,
minLevel: 2,
repeatable: false,
},
{
name: 'Apprenti combattant',
description: 'Prouvez votre endurance en remportant 10 combats.',
objectiveType: 'kill_any',
objectiveTargetId: null,
objectiveCount: 10,
rewardXp: 150,
rewardGold: 50,
rewardTitle: null,
arcId: arc.id,
arcOrder: 3,
minLevel: 1,
repeatable: false,
},
{
name: 'Le gardien des marais',
description: 'Le Golem de Boue terrorise les marais depuis trop longtemps. Mettez fin à son règne.',
objectiveType: 'kill_monster',
objectiveTargetId: monsterMap.get('Golem de Boue') ?? null as string | null,
objectiveCount: 1,
rewardXp: 500,
rewardGold: 200,
rewardTitle: 'Nettoyeur des Marais',
arcId: arc.id,
arcOrder: 4,
minLevel: 5,
repeatable: false,
},
];
// Standalone repeatable quests (daily feel)
const STANDALONE = [
{
name: 'Chasse du jour',
description: 'Remportez 5 combats.',
objectiveType: 'kill_any',
objectiveTargetId: null,
objectiveCount: 5,
rewardXp: 80,
rewardGold: 25,
rewardTitle: null,
arcId: null,
arcOrder: 0,
minLevel: 1,
repeatable: true,
},
{
name: 'Apprenti forgeron',
description: 'Améliorez un item à la forge.',
objectiveType: 'forge_item',
objectiveTargetId: null,
objectiveCount: 1,
rewardXp: 60,
rewardGold: 20,
rewardTitle: null,
arcId: null,
arcOrder: 0,
minLevel: 1,
repeatable: true,
},
{
name: 'Artisan du jour',
description: 'Complétez un craft.',
objectiveType: 'craft_item',
objectiveTargetId: null,
objectiveCount: 1,
rewardXp: 50,
rewardGold: 15,
rewardTitle: null,
arcId: null,
arcOrder: 0,
minLevel: 1,
repeatable: true,
},
];
for (const data of [...QUESTS, ...STANDALONE]) {
const existing = await questRepo.findOne({ where: { name: data.name } });
if (!existing) {
await questRepo.save(questRepo.create(data));
}
}
// --- Quest achievements ---
const QUEST_ACHIEVEMENTS = [
{ key: 'quests_5', name: 'Aventurier Curieux', description: 'Compléter 5 quêtes', category: 'progression', tier: 'bronze', criteriaType: 'quests_completed', criteriaValue: 5, rewardGold: 100, rewardTitle: null },
{ key: 'quests_25', name: 'Héros du Peuple', description: 'Compléter 25 quêtes', category: 'progression', tier: 'silver', criteriaType: 'quests_completed', criteriaValue: 25, rewardGold: 500, rewardTitle: 'Héros du Peuple' },
{ key: 'quests_100', name: 'Légende Quêteuse', description: 'Compléter 100 quêtes', category: 'progression', tier: 'gold', criteriaType: 'quests_completed', criteriaValue: 100, rewardGold: 2000, rewardTitle: 'Légende Quêteuse' },
{ key: 'arc_1', name: 'Premier Arc', description: 'Compléter un arc narratif', category: 'progression', tier: 'bronze', criteriaType: 'quest_arc_completed', criteriaValue: 1, rewardGold: 300, rewardTitle: null },
{ key: 'arc_3', name: 'Conteur', description: 'Compléter 3 arcs narratifs', category: 'progression', tier: 'silver', criteriaType: 'quest_arc_completed', criteriaValue: 3, rewardGold: 1000, rewardTitle: 'Conteur' },
];
for (const data of QUEST_ACHIEVEMENTS) {
const existing = await achievementRepo.findOne({ where: { key: data.key } });
if (!existing) {
await achievementRepo.save(achievementRepo.create(data));
}
}
console.log(`✅ 1 arc + ${QUESTS.length + STANDALONE.length} quêtes + ${QUEST_ACHIEVEMENTS.length} achievements seedés`);
}
// Update monster XP rewards (nerf for quest-driven progression)
export async function nerfMonsterXp(dataSource: DataSource) {
const updates = [
{ name: 'Têtard Vase', xpReward: 8 },
{ name: 'Grenouille Boueuse', xpReward: 15 },
{ name: 'Serpent des Marais', xpReward: 25 },
{ name: 'Champi Vénéneux', xpReward: 20 },
{ name: 'Golem de Boue', xpReward: 50 },
];
for (const u of updates) {
await dataSource.query('UPDATE monsters SET xp_reward = ? WHERE name = ?', [u.xpReward, u.name]);
}
console.log('✅ XP monstres ajustée (nerf pour progression par quêtes)');
}