feat: repeatable quests hors pool 3 slots + section tâches quotidiennes
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 32s

Répétables ne comptent plus dans le MAX_ACTIVE_QUESTS (3).
Frontend: section séparée "Tâches quotidiennes" en grille 3 colonnes,
quêtes narratives en haut avec les slots limités.
Prépare le terrain pour le hub village (forgeron, taverne, etc.).
This commit is contained in:
2026-03-24 16:57:57 +01:00
parent af247a1c6b
commit 9fac9e123b
2 changed files with 36 additions and 15 deletions

View File

@@ -168,37 +168,41 @@ 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>;
const activeCount = active?.length ?? 0; // Séparer quêtes narratives (slots limités) et répétables (toujours en fond)
const activeStory = active?.filter((pq: any) => !pq.quest.repeatable) ?? [];
const activeDaily = active?.filter((pq: any) => pq.quest.repeatable) ?? [];
const availableStory = available?.filter((q: any) => !q.repeatable) ?? [];
const availableDaily = available?.filter((q: any) => q.repeatable) ?? [];
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 quests */} {/* Active story 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 ({activeCount}/3) Quêtes actives ({activeStory.length}/3)
</p> </p>
{active && active.length > 0 ? ( {activeStory.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{active.map((pq: any) => <QuestCard key={pq.id} pq={pq} mode="active" />)} {activeStory.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 }}>
Aucune quête active acceptez-en dans le panneau de droite Aucune quête active acceptez-en à droite
</div> </div>
)} )}
</div> </div>
{/* Available quests */} {/* Available story 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 disponibles Quêtes disponibles
</p> </p>
{available && available.length > 0 ? ( {availableStory.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{available.map((q: any) => <QuestCard key={q.id} pq={q} mode="available" />)} {availableStory.map((q: any) => <QuestCard key={q.id} pq={q} mode="available" />)}
</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 }}>
@@ -208,6 +212,17 @@ export function QuestPage() {
</div> </div>
</div> </div>
{/* Tâches quotidiennes (répétables — toujours en fond) */}
<div style={{ marginTop: '1.5rem' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: 12, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase' }}>
🔄 Tâches quotidiennes
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 6 }}>
{activeDaily.map((pq: any) => <QuestCard key={pq.id} pq={pq} mode="active" />)}
{availableDaily.map((q: any) => <QuestCard key={q.id} pq={q} mode="available" />)}
</div>
</div>
{/* Arcs narratifs */} {/* Arcs narratifs */}
{arcs && arcs.length > 0 && ( {arcs && arcs.length > 0 && (
<div style={{ marginTop: '1.5rem' }}> <div style={{ marginTop: '1.5rem' }}>

View File

@@ -83,12 +83,18 @@ export class QuestService {
throw new BadRequestException(`Niveau ${quest.minLevel} requis`); throw new BadRequestException(`Niveau ${quest.minLevel} requis`);
} }
// Check active quest count // Check active quest count (repeatable quests don't count toward the limit)
const activeCount = await this.playerQuestRepo.count({ if (!quest.repeatable) {
where: { characterId, status: 'active' }, const activeNonRepeatable = await this.playerQuestRepo
}); .createQueryBuilder('pq')
if (activeCount >= MAX_ACTIVE_QUESTS) { .innerJoin('pq.quest', 'q')
throw new BadRequestException(`Maximum ${MAX_ACTIVE_QUESTS} quêtes actives`); .where('pq.character_id = :characterId', { characterId })
.andWhere('pq.status = :status', { status: 'active' })
.andWhere('q.repeatable = false')
.getCount();
if (activeNonRepeatable >= MAX_ACTIVE_QUESTS) {
throw new BadRequestException(`Maximum ${MAX_ACTIVE_QUESTS} quêtes actives (hors répétables)`);
}
} }
// Check not already active // Check not already active