diff --git a/client/dev-dist/sw.js b/client/dev-dist/sw.js index 0243a08..7e1759c 100644 --- a/client/dev-dist/sw.js +++ b/client/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-86c9b217'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.vs5n5ncup7" + "revision": "0.9tvgq2asjt8" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/client/src/components/screens/Home/DailyMissionsModal.tsx b/client/src/components/screens/Home/DailyMissionsModal.tsx index 226b42a..b7bead2 100644 --- a/client/src/components/screens/Home/DailyMissionsModal.tsx +++ b/client/src/components/screens/Home/DailyMissionsModal.tsx @@ -10,9 +10,10 @@ import { MissionDisplayData } from "../../types/missionTypes"; import { useMissionQuery } from "../../../dojo/hooks/useMissionQuery"; import { useMissionSpawner } from "../../../dojo/hooks/useMissionSpawner"; import { useMissionData } from "../../../dojo/hooks/useMissionData"; +import { useMissionRewardClaimer } from "../../../dojo/hooks/useMissionRewardClaimer"; +import { usePlayer } from "../../../dojo/hooks/usePlayer"; interface DailyMissionsModalProps { - /** Callback to close the modal */ onClose: () => void } @@ -48,17 +49,25 @@ const getEnumVariant = (enumObj: any, defaultValue: string): string => { }; /** - * NEW FUNCTION: Determines if a mission is completed based on its status + * Determines if a mission is completed based on its status */ const isMissionCompleted = (mission: Mission): boolean => { const statusVariant = getEnumVariant(mission.status, 'Pending'); return statusVariant === 'Completed'; }; +/** + * Determines if a mission has been claimed based on its status + */ +const isMissionClaimed = (mission: Mission): boolean => { + const statusVariant = getEnumVariant(mission.status, 'Pending'); + return statusVariant === 'Claimed'; +}; + /** * Converts Mission bindings to display data for UI */ -const missionToDisplayData = (mission: Mission, claimedMissionIds: Set): MissionDisplayData => { +const missionToDisplayData = (mission: Mission): MissionDisplayData => { let difficulty: 'Easy' | 'Mid' | 'Hard' = 'Easy'; if (mission.target_coins >= 1000) difficulty = 'Hard'; else if (mission.target_coins >= 500) difficulty = 'Mid'; @@ -66,7 +75,7 @@ const missionToDisplayData = (mission: Mission, claimedMissionIds: Set): const worldVariant = getEnumVariant(mission.required_world, 'Forest'); const golemVariant = getEnumVariant(mission.required_golem, 'Fire'); const completed = isMissionCompleted(mission); - const claimed = claimedMissionIds.has(mission.id.toString()); + const claimed = isMissionClaimed(mission); const requiredWorld = worldVariant.charAt(0).toUpperCase() + worldVariant.slice(1); const requiredGolem = golemVariant.charAt(0).toUpperCase() + golemVariant.slice(1); @@ -95,20 +104,18 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { [account] ); - // Hooks modulares + // Hooks const { fetchTodayMissions, isLoading: isQuerying, error: queryError } = useMissionQuery(); const { spawnMissions, isSpawning, error: spawnError } = useMissionSpawner(); + const { claimMissionReward, isClaiming, error: claimError } = useMissionRewardClaimer(); + const { refetch: refetchPlayer } = usePlayer(); // Estado local const [missions, setMissions] = useState([]); const [isInitialized, setIsInitialized] = useState(false); const [showCelebration, setShowCelebration] = useState(false); const [claimedMission, setClaimedMission] = useState(null); - const [claimedMissionIds, setClaimedMissionIds] = useState>(new Set()); - - // NEW STATE: For claim reward process const [claimingMissionId, setClaimingMissionId] = useState(null); - const [claimError, setClaimError] = useState(null); // Procesar data const { todayMissions, hasData } = useMissionData(missions); @@ -117,70 +124,57 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { const isLoading = isQuerying || isSpawning; const error = queryError || spawnError || claimError; - // NEW FUNCTION: Refresh missions after claim + // Refresh missions after claim const refreshMissionsAfterClaim = useCallback(async () => { if (!playerAddress) return; try { - console.log("🔄 Refreshing missions after claim..."); const refreshedMissions = await fetchTodayMissions(playerAddress); setMissions(refreshedMissions); - console.log("✅ Missions refreshed successfully"); } catch (error) { - console.error("❌ Error refreshing missions:", error); + // Silently handle refresh error } }, [playerAddress, fetchTodayMissions]); - // NEW FUNCTION: Handle claim reward (placeholder for future implementation) + // Handle real claim reward with blockchain transaction const handleClaimReward = useCallback(async (mission: MissionDisplayData) => { - console.log(`🎯 Claiming reward for mission ${mission.id}:`, mission); - setClaimingMissionId(mission.id); - setClaimError(null); try { - // TODO: Implement actual claim reward transaction - // This would call a hook like useMissionRewardClaimer - // For now, we'll simulate the process - - console.log("🔄 Processing claim reward transaction..."); - - // Simulate API call delay - await new Promise(resolve => setTimeout(resolve, 2000)); + const result = await claimMissionReward( + parseInt(mission.id), + mission.difficulty // Use the mission's existing difficulty field + ); - // For now, just mark as claimed locally - // In the real implementation, this would be handled by the blockchain transaction - setClaimedMission(mission); - setShowCelebration(true); - setClaimedMissionIds(prev => new Set(prev).add(mission.id)); - - // Refresh missions from blockchain after successful claim - await refreshMissionsAfterClaim(); - - console.log("✅ Mission reward claimed successfully"); + if (result.success) { + // Optimistic update + setClaimedMission(mission); + setShowCelebration(true); + + // Refresh data from blockchain + await Promise.all([ + refreshMissionsAfterClaim(), + refetchPlayer() // Update player coins + ]); + } } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Failed to claim reward"; - setClaimError(errorMessage); - console.error("❌ Error claiming mission reward:", error); + // Error is handled by the hook } finally { setClaimingMissionId(null); } - }, [refreshMissionsAfterClaim]); + }, [claimMissionReward, refreshMissionsAfterClaim, refetchPlayer]); - // 🎯 ORQUESTACIÓN PRINCIPAL + // Initialize missions const initializeMissions = useCallback(async () => { if (!playerAddress || isInitialized) return; try { - console.log("📡 Checking for existing missions..."); const existingMissions = await fetchTodayMissions(playerAddress); if (existingMissions.length > 0) { - console.log(`✅ Found ${existingMissions.length} existing missions`); setMissions(existingMissions); } else { - console.log("🎲 No missions found, creating new ones..."); const spawnSuccess = await spawnMissions(playerAddress); if (spawnSuccess) { @@ -191,23 +185,21 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { setIsInitialized(true); } catch (error) { - console.error("❌ Error initializing missions:", error); + // Silently handle error } }, [playerAddress, isInitialized, fetchTodayMissions, spawnMissions]); - // Ejecutar al abrir modal + // Load missions when modal opens useEffect(() => { if (playerAddress) { initializeMissions(); } }, [playerAddress, initializeMissions]); - // Reset cuando cambia de usuario + // Reset state when user changes useEffect(() => { setMissions([]); setIsInitialized(false); - setClaimedMissionIds(new Set()); // NEW: Reset claimed missions - setClaimError(null); // NEW: Reset claim errors }, [playerAddress]); // Early return if no account @@ -244,12 +236,12 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { ); } - // Convertir misiones - UPDATED to pass claimedMissionIds + // Convert missions to display data const displayMissions: MissionDisplayData[] = todayMissions.map(mission => { - return missionToDisplayData(mission, claimedMissionIds); + return missionToDisplayData(mission); }); - // NEW FUNCTION: Get mission card styling based on status + // Get mission card styling based on status const getMissionCardStyling = (mission: MissionDisplayData) => { if (mission.completed) { if (mission.claimed) { @@ -286,6 +278,16 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { } }; + // Get fixed reward amount based on difficulty + const getFixedReward = (difficulty: MissionDisplayData['difficulty']): number => { + switch (difficulty) { + case 'Easy': return 100; + case 'Mid': return 250; + case 'Hard': return 500; + default: return 100; + } + }; + const handleCloseCelebration = () => { setShowCelebration(false); setClaimedMission(null); @@ -375,6 +377,7 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { {displayMissions.map((mission: MissionDisplayData, index: number) => { const styling = getMissionCardStyling(mission); const isClaimingThis = claimingMissionId === mission.id; + const fixedReward = getFixedReward(mission.difficulty); return ( - {mission.reward} + {fixedReward} - {/* NEW SECTION: Action Buttons */} + {/* Action Buttons */} {mission.completed && !mission.claimed && (
handleClaimReward(mission)} - disabled={isClaimingThis || claimingMissionId !== null} + disabled={isClaimingThis || isClaiming} > {isClaimingThis ? ( <> @@ -470,7 +473,7 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { Claiming... ) : ( - 'Claim Reward' + `Claim ${fixedReward} Coins` )}
@@ -518,7 +521,7 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
- {/* Animación de celebración */} + {/* Celebration animation */} {showCelebration && claimedMission && ( , default const toriiNodeToMission = (rawNode: RawMissionNode): Mission => { const worldEnumMap = { Volcano: "Volcano", Glacier: "Glacier", Forest: "Forest" }; const golemEnumMap = { Ice: "Ice", Stone: "Stone", Fire: "Fire" }; - const statusEnumMap = { Completed: "Completed", Pending: "Pending" }; + const statusEnumMap = { Completed: "Completed", Pending: "Pending", Claimed: "Claimed" }; const required_world = createCairoEnum(rawNode.required_world, worldEnumMap, "Forest"); const required_golem = createCairoEnum(rawNode.required_golem, golemEnumMap, "Fire"); @@ -98,9 +98,11 @@ const toriiNodeToMission = (rawNode: RawMissionNode): Mission => { if (statusObj.variant) { if (statusObj.variant.Pending !== undefined) statusKey = "Pending"; else if (statusObj.variant.Completed !== undefined) statusKey = "Completed"; + else if (statusObj.variant.Claimed !== undefined) statusKey = "Claimed"; } else if (statusObj.Pending !== undefined) statusKey = "Pending"; else if (statusObj.Completed !== undefined) statusKey = "Completed"; + else if (statusObj.Claimed !== undefined) statusKey = "Claimed"; } mission.status = new CairoCustomEnum({ [statusKey]: statusKey }); diff --git a/client/src/dojo/hooks/useMissionRewardClaimer.tsx b/client/src/dojo/hooks/useMissionRewardClaimer.tsx new file mode 100644 index 0000000..3b9d522 --- /dev/null +++ b/client/src/dojo/hooks/useMissionRewardClaimer.tsx @@ -0,0 +1,74 @@ +import { useState, useCallback } from "react"; +import { useAccount } from "@starknet-react/core"; +import { useDojoSDK } from "@dojoengine/sdk/react"; +import { Account } from "starknet"; +import { getRewardFromDifficulty, type MissionDifficulty } from '../../utils/missionRewards'; + +interface UseMissionRewardClaimerReturn { + isClaiming: boolean; + error: string | null; + claimMissionReward: (missionId: number, difficulty: MissionDifficulty) => Promise<{ success: boolean; error?: string }>; + clearError: () => void; +} + +export const useMissionRewardClaimer = (): UseMissionRewardClaimerReturn => { + const { client } = useDojoSDK(); + const { account } = useAccount(); + const [isClaiming, setIsClaiming] = useState(false); + const [error, setError] = useState(null); + + /** + * Claims mission reward by calling reward_current_mission contract + * Uses fixed reward amounts based on mission difficulty + */ + const claimMissionReward = useCallback(async ( + missionId: number, + difficulty: MissionDifficulty + ): Promise<{ success: boolean; error?: string }> => { + if (!account) { + return { success: false, error: "No account connected" }; + } + + setIsClaiming(true); + setError(null); + + try { + // Get fixed reward amount based on difficulty + const rewardAmount = getRewardFromDifficulty(difficulty); + + const tx = await client.game.rewardCurrentMission( + account as Account, + missionId, + rewardAmount + ); + + if (tx && tx.code === "SUCCESS") { + console.log(`Mission reward claimed successfully for mission ${missionId} with difficulty ${difficulty}`); + console.log("Transaction:", tx); + return { success: true }; + } else { + const errorMsg = `Transaction failed: ${tx?.code || 'Unknown error'}`; + setError(errorMsg); + return { success: false, error: errorMsg }; + } + + } catch (error) { + const errorMsg = error instanceof Error ? error.message : "Unknown transaction error"; + setError(errorMsg); + return { success: false, error: errorMsg }; + } finally { + setIsClaiming(false); + } + }, [account, client.game]); + + const clearError = useCallback(() => { + setError(null); + }, []); + + return { + isClaiming, + error, + claimMissionReward, + clearError + }; +}; \ No newline at end of file diff --git a/client/src/utils/missionRewards.ts b/client/src/utils/missionRewards.ts new file mode 100644 index 0000000..b7d0b22 --- /dev/null +++ b/client/src/utils/missionRewards.ts @@ -0,0 +1,29 @@ +/** + * Mission reward system based on difficulty + */ + +export type MissionDifficulty = 'Easy' | 'Mid' | 'Hard'; + +/** + * Fixed coin rewards by mission difficulty + */ +export const MISSION_REWARDS: Record = { + 'Easy': 100, + 'Mid': 250, + 'Hard': 500 +} as const; + +/** + * Gets the coin reward amount for a mission difficulty + */ +export const getMissionReward = (difficulty: MissionDifficulty): number => { + return MISSION_REWARDS[difficulty]; +}; + +/** + * Gets the reward amount for a mission based on its difficulty + * Uses the mission's existing difficulty field instead of calculating from target_coins + */ +export const getRewardFromDifficulty = (difficulty: MissionDifficulty): number => { + return getMissionReward(difficulty); +}; \ No newline at end of file