Files
gioco-mobile/client/src/screens/MissionsScreen.tsx
T
davide bdee84244f client: missions screen — narrative, chips, chance bar, loot, abandon
Available missions now show description, type chip (colored by type),
success probability bar (green/gold/red), minLevel lock with 🔒.
Reward loot item displayed in footer. Active missions show reward
loot and an Abandon button (confirm dialog, −1 rep warning).
Result modal shows loot and fine details.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 00:12:03 +02:00

241 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react';
import { api } from '../api/api';
import { ApiError } from '../api/http';
import type { ActiveMission, AvailableMission, ClaimMissionResponse } 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';
/** Colore della barra probabilità: verde → oro → rosso. */
function chanceColor(chance: number): string {
if (chance >= 0.7) return '#4caf6e';
if (chance >= 0.45) return '#d4a017';
return '#d9534f';
}
export function MissionsScreen() {
const { setPlayer } = useAuth();
const { toast } = useToast();
const [available, setAvailable] = useState<AvailableMission[]>([]);
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: AvailableMission) {
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);
}
}
async function abandon(mission: ActiveMission) {
if (!window.confirm(`Abbandonare "${mission.mission.title}"? Perderai reputazione e la merce già consegnata.`)) {
return;
}
setBusy(true);
try {
const res = await api.abandonMission(mission.playerMissionId);
setPlayer(res.player);
toast(`Missione abbandonata (${res.reputationChange} reputazione)`, 'info');
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="mission">
<div className="mission__head">
<strong>{m.mission.title}</strong>
{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)} />
)}
</div>
<p className="muted small">
{MISSION_TYPE_LABELS[m.mission.type]} · premio{' '}
{formatMoney(m.mission.rewardMoney)} + {m.mission.rewardXp} XP
{m.mission.rewardItemName &&
` + ${m.mission.rewardItemQuantity}× ${m.mission.rewardItemName}`}
</p>
{!isReady(m) && (
<button
type="button"
className="btn btn--ghost btn--small mission__abandon"
disabled={busy}
onClick={() => void abandon(m)}
>
Abbandona
</button>
)}
</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) => {
const locked = !mission.canStart;
return (
<li key={mission.id} className={`mission${locked ? ' mission--locked' : ''}`}>
<div className="mission__head">
<strong>{mission.title}</strong>
<span className="mission__stars">{difficultyStars(mission.difficulty)}</span>
</div>
<span className={`chip chip--${mission.type.toLowerCase()}`}>
{MISSION_TYPE_LABELS[mission.type]}
</span>
<p className="mission__description">{mission.description}</p>
<p className="muted small">
durata {formatDuration(mission.durationSeconds)} · rischio{' '}
{formatPercent(mission.risk)}
</p>
<div className="chancebar" title="Probabilità di successo stimata">
<div
className="chancebar__fill"
style={{
width: `${mission.estimatedSuccessChance * 100}%`,
background: chanceColor(mission.estimatedSuccessChance),
}}
/>
<span className="chancebar__text">
successo {formatPercent(mission.estimatedSuccessChance)}
</span>
</div>
{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
{mission.rewardItemName &&
` · 🎁 ${mission.rewardItemQuantity}× ${mission.rewardItemName}`}
</span>
{locked ? (
<span className="mission__locked">🔒 liv. {mission.minLevel}</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.lootItemName && (
<li>
🎁 +{result.lootItemQuantity}× {result.lootItemName}
</li>
)}
{result.levelsGained > 0 && (
<li className="levelup">Sei salito al livello {result.player.level}!</li>
)}
</ul>
) : (
<ul className="result-list">
{result.fine > 0 && <li>💸 multa: {formatMoney(result.fine)}</li>}
<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>
);
}