feat(sprint5): quest system + arcs + rebalance endurance/damage/xp
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s
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:
176
src/database/quests-seed.ts
Normal file
176
src/database/quests-seed.ts
Normal 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)');
|
||||
}
|
||||
Reference in New Issue
Block a user