173 lines
6.0 KiB
TypeScript
173 lines
6.0 KiB
TypeScript
|
|
import { useCallback, useEffect, useState } from 'react';
|
|||
|
|
import { api } from '../api/api';
|
|||
|
|
import { ApiError } from '../api/http';
|
|||
|
|
import type { ActiveMission, ClaimMissionResponse, Mission } from '../api/types';
|
|||
|
|
import { useAuth } from '../auth/AuthContext';
|
|||
|
|
import { Countdown } from '../components/Countdown';
|
|||
|
|
import { useToast } from '../components/Toast';
|
|||
|
|
import {
|
|||
|
|
difficultyStars,
|
|||
|
|
formatDuration,
|
|||
|
|
formatMoney,
|
|||
|
|
formatPercent,
|
|||
|
|
MISSION_TYPE_LABELS,
|
|||
|
|
} from '../shared/format';
|
|||
|
|
|
|||
|
|
export function MissionsScreen() {
|
|||
|
|
const { setPlayer } = useAuth();
|
|||
|
|
const { toast } = useToast();
|
|||
|
|
const [available, setAvailable] = useState<Mission[]>([]);
|
|||
|
|
const [active, setActive] = useState<ActiveMission[]>([]);
|
|||
|
|
const [result, setResult] = useState<ClaimMissionResponse | null>(null);
|
|||
|
|
const [busy, setBusy] = useState(false);
|
|||
|
|
// tick fittizio per ri-renderizzare quando un countdown arriva a zero
|
|||
|
|
const [, setTick] = useState(0);
|
|||
|
|
|
|||
|
|
const reload = useCallback(async () => {
|
|||
|
|
const [av, ac] = await Promise.all([api.missionsAvailable(), api.missionsActive()]);
|
|||
|
|
setAvailable(av.missions);
|
|||
|
|
setActive(ac.missions);
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
void reload();
|
|||
|
|
}, [reload]);
|
|||
|
|
|
|||
|
|
async function start(mission: Mission) {
|
|||
|
|
setBusy(true);
|
|||
|
|
try {
|
|||
|
|
await api.startMission(mission.id);
|
|||
|
|
toast(`Missione "${mission.title}" avviata`, 'success');
|
|||
|
|
await reload();
|
|||
|
|
} catch (err) {
|
|||
|
|
toast(err instanceof ApiError ? err.message : 'Errore imprevisto', 'error');
|
|||
|
|
} finally {
|
|||
|
|
setBusy(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function claim(playerMissionId: string) {
|
|||
|
|
setBusy(true);
|
|||
|
|
try {
|
|||
|
|
const res = await api.claimMission(playerMissionId);
|
|||
|
|
setResult(res);
|
|||
|
|
setPlayer(res.player);
|
|||
|
|
await reload();
|
|||
|
|
} catch (err) {
|
|||
|
|
toast(err instanceof ApiError ? err.message : 'Errore imprevisto', 'error');
|
|||
|
|
} finally {
|
|||
|
|
setBusy(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isReady = (m: ActiveMission) => new Date(m.completesAt).getTime() <= Date.now();
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="screen">
|
|||
|
|
<h2 className="screen__title">Missioni</h2>
|
|||
|
|
|
|||
|
|
{active.length > 0 && (
|
|||
|
|
<section className="card">
|
|||
|
|
<h3 className="card__title">In corso</h3>
|
|||
|
|
<ul className="list">
|
|||
|
|
{active.map((m) => (
|
|||
|
|
<li key={m.playerMissionId} className="list__row">
|
|||
|
|
<div>
|
|||
|
|
<strong>{m.mission.title}</strong>
|
|||
|
|
<p className="muted small">
|
|||
|
|
{MISSION_TYPE_LABELS[m.mission.type]} · premio{' '}
|
|||
|
|
{formatMoney(m.mission.rewardMoney)} + {m.mission.rewardXp} XP
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
{isReady(m) ? (
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="btn btn--primary btn--small"
|
|||
|
|
disabled={busy}
|
|||
|
|
onClick={() => void claim(m.playerMissionId)}
|
|||
|
|
>
|
|||
|
|
Riscuoti
|
|||
|
|
</button>
|
|||
|
|
) : (
|
|||
|
|
<Countdown target={m.completesAt} onDone={() => setTick((t) => t + 1)} />
|
|||
|
|
)}
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
</section>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<section className="card">
|
|||
|
|
<h3 className="card__title">Disponibili in città</h3>
|
|||
|
|
{available.length === 0 ? (
|
|||
|
|
<p className="muted">Nessuna missione al momento. Riprova tra un minuto.</p>
|
|||
|
|
) : (
|
|||
|
|
<ul className="list">
|
|||
|
|
{available.map((mission) => (
|
|||
|
|
<li key={mission.id} className="mission">
|
|||
|
|
<div className="mission__head">
|
|||
|
|
<strong>{mission.title}</strong>
|
|||
|
|
<span className="mission__stars">{difficultyStars(mission.difficulty)}</span>
|
|||
|
|
</div>
|
|||
|
|
<p className="muted small">
|
|||
|
|
{MISSION_TYPE_LABELS[mission.type]} · durata{' '}
|
|||
|
|
{formatDuration(mission.durationSeconds)} · rischio {formatPercent(mission.risk)}
|
|||
|
|
</p>
|
|||
|
|
{mission.requiredItemName && (
|
|||
|
|
<p className="mission__required">
|
|||
|
|
Richiede: {mission.requiredItemQuantity}× {mission.requiredItemName}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
<div className="mission__footer">
|
|||
|
|
<span className="mission__reward">
|
|||
|
|
{formatMoney(mission.rewardMoney)} · {mission.rewardXp} XP
|
|||
|
|
</span>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="btn btn--secondary btn--small"
|
|||
|
|
disabled={busy}
|
|||
|
|
onClick={() => void start(mission)}
|
|||
|
|
>
|
|||
|
|
Avvia
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
)}
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
{result && (
|
|||
|
|
<div className="modal-backdrop" onClick={() => setResult(null)}>
|
|||
|
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|||
|
|
<h3>{result.success ? '✅ Colpo riuscito!' : '❌ Missione fallita'}</h3>
|
|||
|
|
<p className="muted">"{result.missionTitle}"</p>
|
|||
|
|
{result.success ? (
|
|||
|
|
<ul className="result-list">
|
|||
|
|
<li>+{formatMoney(result.rewardMoney)}</li>
|
|||
|
|
<li>+{result.rewardXp} XP</li>
|
|||
|
|
<li>+{result.reputationChange} reputazione</li>
|
|||
|
|
{result.levelsGained > 0 && (
|
|||
|
|
<li className="levelup">
|
|||
|
|
Sei salito al livello {result.player.level}!
|
|||
|
|
</li>
|
|||
|
|
)}
|
|||
|
|
</ul>
|
|||
|
|
) : (
|
|||
|
|
<ul className="result-list">
|
|||
|
|
<li>{result.reputationChange} reputazione</li>
|
|||
|
|
<li className="muted small">
|
|||
|
|
Probabilità di successo: {formatPercent(result.successChance)}
|
|||
|
|
</li>
|
|||
|
|
</ul>
|
|||
|
|
)}
|
|||
|
|
<button type="button" className="btn btn--primary" onClick={() => setResult(null)}>
|
|||
|
|
Continua
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|