From bdee84244fea29ba2490701e1eb986301975327c Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Wed, 10 Jun 2026 00:12:03 +0200 Subject: [PATCH] =?UTF-8?q?client:=20missions=20screen=20=E2=80=94=20narra?= =?UTF-8?q?tive,=20chips,=20chance=20bar,=20loot,=20abandon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- client/src/screens/MissionsScreen.tsx | 162 ++++++++++++++++++-------- 1 file changed, 115 insertions(+), 47 deletions(-) diff --git a/client/src/screens/MissionsScreen.tsx b/client/src/screens/MissionsScreen.tsx index cf3836e..e9d3162 100644 --- a/client/src/screens/MissionsScreen.tsx +++ b/client/src/screens/MissionsScreen.tsx @@ -1,7 +1,7 @@ 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 type { ActiveMission, AvailableMission, ClaimMissionResponse } from '../api/types'; import { useAuth } from '../auth/AuthContext'; import { Countdown } from '../components/Countdown'; import { useToast } from '../components/Toast'; @@ -13,10 +13,17 @@ import { 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([]); + const [available, setAvailable] = useState([]); const [active, setActive] = useState([]); const [result, setResult] = useState(null); const [busy, setBusy] = useState(false); @@ -33,7 +40,7 @@ export function MissionsScreen() { void reload(); }, [reload]); - async function start(mission: Mission) { + async function start(mission: AvailableMission) { setBusy(true); try { await api.startMission(mission.id); @@ -60,6 +67,23 @@ export function MissionsScreen() { } } + 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 ( @@ -71,25 +95,37 @@ export function MissionsScreen() {

In corso

    {active.map((m) => ( -
  • -
    +
  • +
    {m.mission.title} -

    - {MISSION_TYPE_LABELS[m.mission.type]} · premio{' '} - {formatMoney(m.mission.rewardMoney)} + {m.mission.rewardXp} XP -

    + {isReady(m) ? ( + + ) : ( + setTick((t) => t + 1)} /> + )}
    - {isReady(m) ? ( +

    + {MISSION_TYPE_LABELS[m.mission.type]} · premio{' '} + {formatMoney(m.mission.rewardMoney)} + {m.mission.rewardXp} XP + {m.mission.rewardItemName && + ` + ${m.mission.rewardItemQuantity}× ${m.mission.rewardItemName}`} +

    + {!isReady(m) && ( - ) : ( - setTick((t) => t + 1)} /> )}
  • ))} @@ -103,36 +139,64 @@ export function MissionsScreen() {

    Nessuna missione al momento. Riprova tra un minuto.

    ) : (
      - {available.map((mission) => ( -
    • -
      - {mission.title} - {difficultyStars(mission.difficulty)} -
      -

      - {MISSION_TYPE_LABELS[mission.type]} · durata{' '} - {formatDuration(mission.durationSeconds)} · rischio {formatPercent(mission.risk)} -

      - {mission.requiredItemName && ( -

      - Richiede: {mission.requiredItemQuantity}× {mission.requiredItemName} -

      - )} -
      - - {formatMoney(mission.rewardMoney)} · {mission.rewardXp} XP + {available.map((mission) => { + const locked = !mission.canStart; + return ( +
    • +
      + {mission.title} + {difficultyStars(mission.difficulty)} +
      + + {MISSION_TYPE_LABELS[mission.type]} - - -
    • - ))} +

      {mission.description}

      +

      + durata {formatDuration(mission.durationSeconds)} · rischio{' '} + {formatPercent(mission.risk)} +

      + +
      +
      + + successo {formatPercent(mission.estimatedSuccessChance)} + +
      + + {mission.requiredItemName && ( +

      + Richiede: {mission.requiredItemQuantity}× {mission.requiredItemName} +

      + )} + +
      + + {formatMoney(mission.rewardMoney)} · {mission.rewardXp} XP + {mission.rewardItemName && + ` · 🎁 ${mission.rewardItemQuantity}× ${mission.rewardItemName}`} + + {locked ? ( + 🔒 liv. {mission.minLevel} + ) : ( + + )} +
      + + ); + })}
    )} @@ -147,14 +211,18 @@ export function MissionsScreen() {
  • +{formatMoney(result.rewardMoney)}
  • +{result.rewardXp} XP
  • +{result.reputationChange} reputazione
  • - {result.levelsGained > 0 && ( -
  • - Sei salito al livello {result.player.level}! + {result.lootItemName && ( +
  • + 🎁 +{result.lootItemQuantity}× {result.lootItemName}
  • )} + {result.levelsGained > 0 && ( +
  • Sei salito al livello {result.player.level}!
  • + )}
) : (
    + {result.fine > 0 &&
  • 💸 multa: −{formatMoney(result.fine)}
  • }
  • {result.reputationChange} reputazione
  • Probabilità di successo: {formatPercent(result.successChance)}