feat: quest page restructurée — combat/métiers/dailies/arcs séparés
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
Frontend: 4 sections distinctes sur la page quêtes: - Quêtes actives (3 slots combat uniquement) - Quêtes de combat (stagger: max 3 affichées, "+N après celles-ci") - 🔨 Métiers (forge/craft — hors pool, toujours disponibles) - 🔄 Dailies (répétables en fond) Backend: craft/forge quests ne comptent plus dans le MAX_ACTIVE_QUESTS.
This commit is contained in:
@@ -214,25 +214,37 @@ export function QuestPage() {
|
|||||||
|
|
||||||
if (loadActive || loadAvail) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
if (loadActive || loadAvail) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
||||||
|
|
||||||
// Séparer quêtes narratives (slots limités) et répétables (toujours en fond)
|
const isCraftQuest = (q: any) => ['forge_item', 'craft_item'].includes(q.objectiveType ?? q.quest?.objectiveType);
|
||||||
const activeStory = active?.filter((pq: any) => !pq.quest.repeatable) ?? [];
|
const isCombatQuest = (q: any) => !isCraftQuest(q);
|
||||||
const activeDaily = active?.filter((pq: any) => pq.quest.repeatable) ?? [];
|
|
||||||
const availableStory = available?.filter((q: any) => !q.repeatable) ?? [];
|
// Split by category
|
||||||
const availableDaily = available?.filter((q: any) => q.repeatable) ?? [];
|
const activeAll = active ?? [];
|
||||||
|
const activeCombat = activeAll.filter((pq: any) => !pq.quest.repeatable && isCombatQuest(pq));
|
||||||
|
const activeCraft = activeAll.filter((pq: any) => !pq.quest.repeatable && isCraftQuest(pq));
|
||||||
|
const activeDaily = activeAll.filter((pq: any) => pq.quest.repeatable);
|
||||||
|
|
||||||
|
const availableAll = available ?? [];
|
||||||
|
const availableCombat = availableAll.filter((q: any) => !q.repeatable && isCombatQuest(q));
|
||||||
|
const availableCraft = availableAll.filter((q: any) => !q.repeatable && isCraftQuest(q));
|
||||||
|
const availableDaily = availableAll.filter((q: any) => q.repeatable);
|
||||||
|
|
||||||
|
// Stagger: show max 3 combat quests at a time
|
||||||
|
const shownCombat = availableCombat.slice(0, 3);
|
||||||
|
const hiddenCount = availableCombat.length - shownCombat.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{ margin: '0 0 1rem', color: '#f4c94e', fontSize: 20 }}>📜 Quêtes</h2>
|
<h2 style={{ margin: '0 0 1rem', color: '#f4c94e', fontSize: 20 }}>📜 Quêtes</h2>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||||
{/* Active story quests */}
|
{/* Active combat quests */}
|
||||||
<div>
|
<div>
|
||||||
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
|
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
|
||||||
Quêtes actives ({activeStory.length}/3)
|
Quêtes actives ({activeCombat.length}/3)
|
||||||
</p>
|
</p>
|
||||||
{activeStory.length > 0 ? (
|
{activeCombat.length > 0 ? (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
{activeStory.map((pq: any) => <QuestCard key={pq.id} pq={pq} mode="active" />)}
|
{activeCombat.map((pq: any) => <QuestCard key={pq.id} pq={pq} mode="active" />)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="card" style={{ padding: '1.5rem', textAlign: 'center', color: '#6b7a99', fontSize: 13 }}>
|
<div className="card" style={{ padding: '1.5rem', textAlign: 'center', color: '#6b7a99', fontSize: 13 }}>
|
||||||
@@ -241,23 +253,41 @@ export function QuestPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Available story quests */}
|
{/* Available combat quests (staggered) */}
|
||||||
<div>
|
<div>
|
||||||
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
|
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
|
||||||
Quêtes disponibles
|
Quêtes de combat
|
||||||
</p>
|
</p>
|
||||||
{availableStory.length > 0 ? (
|
{shownCombat.length > 0 ? (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
{availableStory.map((q: any) => <QuestCard key={q.id} pq={q} mode="available" />)}
|
{shownCombat.map((q: any) => <QuestCard key={q.id} pq={q} mode="available" />)}
|
||||||
|
{hiddenCount > 0 && (
|
||||||
|
<div style={{ textAlign: 'center', fontSize: 11, color: '#6b7a99', padding: 4 }}>
|
||||||
|
+{hiddenCount} quête{hiddenCount > 1 ? 's' : ''} supplémentaire{hiddenCount > 1 ? 's' : ''} après celles-ci
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="card" style={{ padding: '1.5rem', textAlign: 'center', color: '#6b7a99', fontSize: 13 }}>
|
<div className="card" style={{ padding: '1.5rem', textAlign: 'center', color: '#6b7a99', fontSize: 13 }}>
|
||||||
Toutes les quêtes sont acceptées ou complétées
|
Toutes les quêtes de combat sont complétées
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Métiers (craft/forge — hors pool, comme les dailies) */}
|
||||||
|
{(activeCraft.length > 0 || availableCraft.length > 0) && (
|
||||||
|
<div style={{ marginTop: '1.5rem' }}>
|
||||||
|
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
|
||||||
|
🔨 Métiers
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 6 }}>
|
||||||
|
{activeCraft.map((pq: any) => <QuestCard key={pq.id} pq={pq} mode="active" />)}
|
||||||
|
{availableCraft.map((q: any) => <QuestCard key={q.id} pq={q} mode="available" />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tâches quotidiennes (répétables — toujours en fond) */}
|
{/* Tâches quotidiennes (répétables — toujours en fond) */}
|
||||||
<div style={{ marginTop: '1.5rem' }}>
|
<div style={{ marginTop: '1.5rem' }}>
|
||||||
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
|
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
|
||||||
|
|||||||
@@ -92,17 +92,20 @@ export class QuestService {
|
|||||||
throw new BadRequestException(`Niveau ${quest.minLevel} requis`);
|
throw new BadRequestException(`Niveau ${quest.minLevel} requis`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check active quest count (repeatable quests don't count toward the limit)
|
// Check active quest count
|
||||||
if (!quest.repeatable) {
|
// Repeatable + craft/forge quests don't count toward the 3-slot limit
|
||||||
const activeNonRepeatable = await this.playerQuestRepo
|
const isCraftQuest = ['forge_item', 'craft_item'].includes(quest.objectiveType);
|
||||||
|
if (!quest.repeatable && !isCraftQuest) {
|
||||||
|
const activeCombat = await this.playerQuestRepo
|
||||||
.createQueryBuilder('pq')
|
.createQueryBuilder('pq')
|
||||||
.innerJoin('pq.quest', 'q')
|
.innerJoin('pq.quest', 'q')
|
||||||
.where('pq.character_id = :characterId', { characterId })
|
.where('pq.character_id = :characterId', { characterId })
|
||||||
.andWhere('pq.status = :status', { status: 'active' })
|
.andWhere('pq.status = :status', { status: 'active' })
|
||||||
.andWhere('q.repeatable = false')
|
.andWhere('q.repeatable = false')
|
||||||
|
.andWhere('q.objective_type NOT IN (:...types)', { types: ['forge_item', 'craft_item'] })
|
||||||
.getCount();
|
.getCount();
|
||||||
if (activeNonRepeatable >= MAX_ACTIVE_QUESTS) {
|
if (activeCombat >= MAX_ACTIVE_QUESTS) {
|
||||||
throw new BadRequestException(`Maximum ${MAX_ACTIVE_QUESTS} quêtes actives (hors répétables)`);
|
throw new BadRequestException(`Maximum ${MAX_ACTIVE_QUESTS} quêtes de combat actives`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user