diff --git a/client/dev-dist/sw.js b/client/dev-dist/sw.js index c0439f7..eb2981b 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.73g75renlu8" + "revision": "0.g8ftgsh6a18" }], {}); 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 ee82743..567f291 100644 --- a/client/src/components/screens/Home/DailyMissionsModal.tsx +++ b/client/src/components/screens/Home/DailyMissionsModal.tsx @@ -1,60 +1,192 @@ import { motion, AnimatePresence } from "framer-motion" -import { useState } from "react" +import { useState, useEffect, useMemo, useCallback } from "react" +import { useAccount } from "@starknet-react/core"; +import { addAddressPadding } from "starknet"; import GolemTalkIcon from "../../../assets/icons/GolemTalkIcon.webp" import { ClaimMissionAnimation } from "./ClaimMissionAnimation" import coinIcon from "../../../assets/icons/CoinIcon.webp"; - -interface Mission { - id: string - title: string - description: string - difficulty: 'Easy' | 'Mid' | 'Hard' - reward: number - completed: boolean - claimed?: boolean -} +import { Mission } from "../../../dojo/bindings"; +import { MissionDisplayData } from "../../types/missionTypes"; +import { useMissionQuery } from "../../../dojo/hooks/useMissionQuery"; +import { useMissionSpawner } from "../../../dojo/hooks/useMissionSpawner"; +import { useMissionData } from "../../../dojo/hooks/useMissionData"; interface DailyMissionsModalProps { - /** Player's address for context */ - playerAddress: string /** Callback to close the modal */ onClose: () => void } +/** + * Safe function to extract enum variant + */ +const getEnumVariant = (enumObj: any, defaultValue: string): string => { + if (!enumObj) return defaultValue; + + if (typeof enumObj.activeVariant === 'function') { + try { + return enumObj.activeVariant(); + } catch (error) { + console.warn("activeVariant failed:", error); + } + } + + if (enumObj.variant && typeof enumObj.variant === 'object') { + const keys = Object.keys(enumObj.variant); + if (keys.length > 0) { + return keys[0]; + } + } + + if (typeof enumObj === 'object') { + const keys = Object.keys(enumObj); + if (keys.length > 0) { + return keys[0]; + } + } + + return defaultValue; +}; + +/** + * Converts Mission bindings to display data for UI + */ +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'; + + const worldVariant = getEnumVariant(mission.required_world, 'Forest'); + const golemVariant = getEnumVariant(mission.required_golem, 'Fire'); + const statusVariant = getEnumVariant(mission.status, 'Pending'); + + const requiredWorld = worldVariant.charAt(0).toUpperCase() + worldVariant.slice(1); + const requiredGolem = golemVariant.charAt(0).toUpperCase() + golemVariant.slice(1); + const completed = statusVariant === 'Completed'; + const title = mission.description.split(' ').slice(0, 3).join(' ') || 'Daily Mission'; + + const displayData: MissionDisplayData = { + id: mission.id.toString(), + title, + description: mission.description, + difficulty, + reward: mission.target_coins, + requiredWorld, + requiredGolem, + completed, + claimed: false + }; + + return displayData; +}; + export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { - const [showCelebration, setShowCelebration] = useState(false) - const [claimedMission, setClaimedMission] = useState(null) - const [missions, setMissions] = useState([ - { - id: "mission_1", - title: "First Steps", - description: "Complete 3 runs in any level", - difficulty: "Easy", - reward: 100, - completed: false, - claimed: false - }, - { - id: "mission_2", - title: "Speed Runner", - description: "Finish a run in under 2 minutes", - difficulty: "Mid", - reward: 250, - completed: false, - claimed: false - }, - { - id: "mission_3", - title: "Coin Master", - description: "Collect 1000 coins in a single run", - difficulty: "Hard", - reward: 500, - completed: true, - claimed: false + const { account } = useAccount(); + + const playerAddress = useMemo(() => + account ? addAddressPadding(account.address) : null, + [account] + ); + + // Hooks modulares + const { fetchTodayMissions, isLoading: isQuerying, error: queryError } = useMissionQuery(); + const { spawnMissions, isSpawning, error: spawnError } = useMissionSpawner(); + + // 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()); + + // Procesar data + const { todayMissions, hasData } = useMissionData(missions); + + // Estados combinados + const isLoading = isQuerying || isSpawning; + const error = queryError || spawnError; + + // 🎯 ORQUESTACIÓN PRINCIPAL + 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) { + const newMissions = await fetchTodayMissions(playerAddress); + setMissions(newMissions); + } + } + + setIsInitialized(true); + } catch (error) { + console.error("❌ Error initializing missions:", error); + } + }, [playerAddress, isInitialized, fetchTodayMissions, spawnMissions]); + + // Ejecutar al abrir modal + useEffect(() => { + if (playerAddress) { + initializeMissions(); } - ]) + }, [playerAddress, initializeMissions]); + + // Reset cuando cambia de usuario + useEffect(() => { + setMissions([]); + setIsInitialized(false); + }, [playerAddress]); - const getDifficultyStyle = (difficulty: Mission['difficulty']) => { + // Early return if no account + if (!account || !playerAddress) { + return ( + + e.stopPropagation()} + > +

Wallet Required

+

+ Please connect your wallet to view daily missions. +

+ + Close + +
+
+ ); + } + + // Convertir misiones + const displayMissions: MissionDisplayData[] = todayMissions.map(mission => { + const displayData = missionToDisplayData(mission); + displayData.claimed = claimedMissionIds.has(mission.id.toString()); + return displayData; + }); + + const getDifficultyStyle = (difficulty: MissionDisplayData['difficulty']) => { switch (difficulty) { case 'Easy': return 'bg-green-500 text-white' @@ -65,25 +197,26 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { default: return 'bg-gray-500 text-white' } - } + }; - const handleClaimReward = (mission: Mission) => { - setClaimedMission(mission) - setShowCelebration(true) - - setMissions(prevMissions => - prevMissions.map(m => - m.id === mission.id ? { ...m, claimed: true } : m - ) - ) - - console.log(`Claiming reward for mission: ${mission.id}, reward: ${mission.reward}`) - } + const handleClaimReward = (mission: MissionDisplayData) => { + setClaimedMission(mission); + setShowCelebration(true); + setClaimedMissionIds(prev => new Set(prev).add(mission.id)); + }; const handleCloseCelebration = () => { - setShowCelebration(false) - setClaimedMission(null) - } + setShowCelebration(false); + setClaimedMission(null); + }; + + // Loading states + const showLoading = isLoading; + const loadingText = isSpawning + ? "Creating your daily missions..." + : isQuerying + ? "Loading missions..." + : ""; return ( <> @@ -94,7 +227,6 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) { exit={{ opacity: 0 }} onClick={onClose} > - {/* Container principal con flex column */} e.stopPropagation()} > - {/* Imagen del golem - ahora dentro del flujo normal */} - {/* Card del modal con las misiones */}
- {/* Título del modal */}

Daily Missions

Complete missions to earn rewards!

- {/* Lista de misiones */} -
- {missions.map((mission: Mission, index: number) => ( + {showLoading && ( +
+

{loadingText}

+ {isSpawning && ( +

+ Generating missions with AI and storing on blockchain... +

+ )} +
+ )} + + {error && !showLoading && ( +
+
+

+ Failed to load missions: {error} +

+
+ initializeMissions()} + className="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-[5px] font-luckiest text-sm" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} > - {/* Header: Dificultad, Recompensa y Estado */} -
-
- {/* Etiqueta de dificultad */} - - {mission.difficulty} - - - {/* Recompensa */} -
- - {mission.reward} + Try Again + +
+ )} + + {!showLoading && !error && hasData && ( +
+ {displayMissions.map((mission: MissionDisplayData, index: number) => ( + +
+
+ + {mission.difficulty} - Coin Icon + +
+ + {mission.reward} + + Coin Icon +
+ + {mission.completed && ( +
+ +
+ )}
- - {/* Indicador de completado */} - {mission.completed && ( -
+

- + {mission.title} +

+

+ {mission.description} +

+ +
+ 🗺️ {mission.requiredWorld} + 🧌 {mission.requiredGolem} Golem
- )} -
+
- {/* Título y descripción */} -
-

- {mission.title} -

-

- {mission.description} -

-
+ {mission.completed && !mission.claimed && ( +
+ handleClaimReward(mission)} + > + Claim Reward + +
+ )} - {/* Botón de reclamar */} - {mission.completed && !mission.claimed && ( -
- handleClaimReward(mission)} - > - Claim Reward - -
- )} + {mission.completed && ( +
+ )} + + ))} +
+ )} - {/* Overlay para misiones completadas */} - {mission.completed && ( -
- )} - - ))} -
+ {!showLoading && !error && !hasData && ( +
+

+ No missions found for today +

+ initializeMissions()} + className="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-[5px] font-luckiest text-sm" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Generate Missions + +
+ )} - {/* Botón para cerrar */} {showTalkModal && (
- +
)} diff --git a/client/src/components/types/missionTypes.ts b/client/src/components/types/missionTypes.ts index d2ea909..4a7a49d 100644 --- a/client/src/components/types/missionTypes.ts +++ b/client/src/components/types/missionTypes.ts @@ -14,10 +14,11 @@ export interface ElizaMissionData { } /** - * Simple display data for the UI (converted from Mission bindings) + * Mission display data for the UI (compatible with ClaimMissionAnimation) + * Using string ID to match existing ClaimMissionAnimation component */ export interface MissionDisplayData { - id: number; + id: string; // Changed from number to string for compatibility title: string; description: string; difficulty: 'Easy' | 'Mid' | 'Hard'; @@ -28,6 +29,79 @@ export interface MissionDisplayData { claimed?: boolean; } +/** + * Parses Eliza response text to mission data + * Eliza returns plain text like: "150, coins, glacier, ice" + * No JSON parsing needed! + */ +export function parseElizaResponse(elizaResponse: string): ElizaMissionData | null { + try { + console.log("🔍 Parsing Eliza response:", elizaResponse); + + // Eliza returns plain text directly, not JSON! + // Split the text by commas and clean up + const parts = elizaResponse.split(',').map(part => part.trim().toLowerCase()); + + if (parts.length < 4) { + throw new Error(`Insufficient parts in Eliza response. Expected 4, got ${parts.length}`); + } + + // Extract values: [target_coins, "coins", world, golem] + const [coinsStr, , worldStr, golemStr] = parts; + + // Parse target coins + const target_coins = parseInt(coinsStr); + if (isNaN(target_coins) || target_coins <= 0) { + throw new Error(`Invalid target_coins: ${coinsStr}`); + } + + // Normalize and validate world + const worldMap: Record = { + 'forest': 'Forest', + 'volcano': 'Volcano', + 'glacier': 'Glacier', + 'ice': 'Glacier', // Alternative name + 'fire': 'Volcano', // Alternative name + 'lava': 'Volcano' // Alternative name + }; + + const required_world = worldMap[worldStr]; + if (!required_world) { + console.warn(`Unknown world "${worldStr}", defaulting to Forest`); + } + + // Normalize and validate golem + const golemMap: Record = { + 'fire': 'Fire', + 'ice': 'Ice', + 'stone': 'Stone', + 'rock': 'Stone' // Alternative name + }; + + const required_golem = golemMap[golemStr]; + if (!required_golem) { + console.warn(`Unknown golem "${golemStr}", defaulting to Stone`); + } + + // Create description + const description = `Collect ${target_coins} coins in the ${(required_world || 'Forest').toLowerCase()} using your ${(required_golem || 'Stone').toLowerCase()} golem`; + + const missionData: ElizaMissionData = { + target_coins, + required_world: required_world || 'Forest', + required_golem: required_golem || 'Stone', + description + }; + + console.log("✅ Successfully parsed Eliza mission:", missionData); + return missionData; + + } catch (error) { + console.error("❌ Error parsing Eliza response:", error); + return null; + } +} + /** * Creates fallback missions if Eliza fails */ @@ -55,7 +129,7 @@ export function createFallbackMissions(): ElizaMissionData[] { } /** - * Validates if Eliza response has the correct structure + * Validates if parsed data is a valid mission */ export function isValidElizaMissionData(data: any): data is ElizaMissionData { return ( diff --git a/client/src/dojo/hooks/useMissionData.tsx b/client/src/dojo/hooks/useMissionData.tsx new file mode 100644 index 0000000..24a203b --- /dev/null +++ b/client/src/dojo/hooks/useMissionData.tsx @@ -0,0 +1,84 @@ +import { useMemo } from "react"; +import { Mission } from '../bindings'; +import { isMissionFromToday } from '../../utils/TimeHelpers'; + +interface UseMissionDataReturn { + todayMissions: Mission[]; + pendingMissions: Mission[]; + completedMissions: Mission[]; + hasData: boolean; +} + +export const useMissionData = (missions: Mission[]): UseMissionDataReturn => { + + const todayMissions = useMemo(() => { + if (!Array.isArray(missions)) return []; + + return missions.filter(mission => { + try { + return isMissionFromToday(mission.created_at); + } catch (error) { + return false; + } + }); + }, [missions]); + + const pendingMissions = useMemo(() => { + return todayMissions.filter((mission) => { + try { + let statusVariant = "Pending"; + + if (mission.status && typeof mission.status.activeVariant === 'function') { + statusVariant = mission.status.activeVariant(); + } else if (mission.status && typeof mission.status === 'object') { + const statusObj = mission.status as any; + + if (statusObj.variant) { + if (statusObj.variant.Pending !== undefined) statusVariant = "Pending"; + else if (statusObj.variant.Completed !== undefined) statusVariant = "Completed"; + } + else if (statusObj.Pending !== undefined) statusVariant = "Pending"; + else if (statusObj.Completed !== undefined) statusVariant = "Completed"; + } + + return statusVariant === 'Pending'; + } catch (error) { + return true; // Assume pending on error + } + }); + }, [todayMissions]); + + const completedMissions = useMemo(() => { + return todayMissions.filter(mission => { + try { + let statusVariant = "Pending"; + + if (mission.status && typeof mission.status.activeVariant === 'function') { + statusVariant = mission.status.activeVariant(); + } else if (mission.status && typeof mission.status === 'object') { + const statusObj = mission.status as any; + + if (statusObj.variant) { + if (statusObj.variant.Completed !== undefined) statusVariant = "Completed"; + else if (statusObj.variant.Pending !== undefined) statusVariant = "Pending"; + } + else if (statusObj.Completed !== undefined) statusVariant = "Completed"; + else if (statusObj.Pending !== undefined) statusVariant = "Pending"; + } + + return statusVariant === 'Completed'; + } catch (error) { + return false; + } + }); + }, [todayMissions]); + + const hasData = todayMissions.length > 0; + + return { + todayMissions, + pendingMissions, + completedMissions, + hasData + }; +}; \ No newline at end of file diff --git a/client/src/dojo/hooks/useMissionQuery.tsx b/client/src/dojo/hooks/useMissionQuery.tsx new file mode 100644 index 0000000..4842e4f --- /dev/null +++ b/client/src/dojo/hooks/useMissionQuery.tsx @@ -0,0 +1,185 @@ +import { useState, useCallback } from "react"; +import { CairoCustomEnum } from "starknet"; +import { dojoConfig } from "../dojoConfig"; +import { Mission } from '../bindings'; +import { getCurrentDay } from '../../utils/TimeHelpers'; + +interface MissionEdge { + node: RawMissionNode; +} + +interface RawMissionNode { + id: string; + player_id: string; + target_coins: string; + required_world: any; + required_golem: any; + description: string; + status: any; + created_at: string; +} + +interface UseMissionQueryReturn { + missions: Mission[]; + isLoading: boolean; + error: string | null; + fetchTodayMissions: (playerAddress: string) => Promise; +} + +// Helper functions +const hexToNumber = (hexValue: string | number): number => { + if (typeof hexValue === 'number') return hexValue; + if (typeof hexValue === 'string' && hexValue.startsWith('0x')) { + return parseInt(hexValue, 16); + } + if (typeof hexValue === 'string') { + return parseInt(hexValue, 10); + } + return 0; +}; + +const createCairoEnum = (rawValue: any, enumMap: Record, defaultValue: string): CairoCustomEnum => { + if (!rawValue) { + return new CairoCustomEnum({ [defaultValue]: defaultValue }); + } + + if (typeof rawValue === 'string') { + const enumKey = enumMap[rawValue] ? rawValue : defaultValue; + return new CairoCustomEnum({ [enumKey]: enumKey }); + } + + if (typeof rawValue === 'object') { + if (rawValue.variant && typeof rawValue.variant === 'object') { + for (const [key] of Object.entries(rawValue.variant)) { + if (enumMap[key]) { + return new CairoCustomEnum({ [key]: key }); + } + } + } + + for (const [key] of Object.entries(rawValue)) { + if (enumMap[key]) { + return new CairoCustomEnum({ [key]: key }); + } + } + } + + return new CairoCustomEnum({ [defaultValue]: defaultValue }); +}; + +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 required_world = createCairoEnum(rawNode.required_world, worldEnumMap, "Forest"); + const required_golem = createCairoEnum(rawNode.required_golem, golemEnumMap, "Fire"); + const status = createCairoEnum(rawNode.status, statusEnumMap, "Pending"); + + const mission: Mission = { + id: hexToNumber(rawNode.id), + player_id: rawNode.player_id, + target_coins: hexToNumber(rawNode.target_coins), + required_world, + required_golem, + description: rawNode.description, + status, + created_at: hexToNumber(rawNode.created_at) + }; + + try { + mission.status.activeVariant(); + } catch (error) { + let statusKey = "Pending"; + + if (mission.status && typeof mission.status === 'object') { + const statusObj = mission.status as any; + + if (statusObj.variant) { + if (statusObj.variant.Pending !== undefined) statusKey = "Pending"; + else if (statusObj.variant.Completed !== undefined) statusKey = "Completed"; + } + else if (statusObj.Pending !== undefined) statusKey = "Pending"; + else if (statusObj.Completed !== undefined) statusKey = "Completed"; + } + + mission.status = new CairoCustomEnum({ [statusKey]: statusKey }); + } + + return mission; +}; + +export const useMissionQuery = (): UseMissionQueryReturn => { + const [missions, setMissions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchTodayMissions = useCallback(async (playerAddress: string): Promise => { + setIsLoading(true); + setError(null); + + try { + const currentDay = getCurrentDay(); + + const response = await fetch(dojoConfig.toriiUrl + "/graphql", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: ` + query GetMissions($playerAddress: ContractAddress!, $dayTimestamp: u32!) { + golemRunnerMissionModels( + where: { + player_id: $playerAddress, + created_at: $dayTimestamp + } + first: 10 + ) { + edges { + node { + id + player_id + target_coins + required_world + required_golem + description + status + created_at + } + } + } + } + `, + variables: { playerAddress, dayTimestamp: currentDay } + }), + }); + + const result = await response.json(); + + if (!result.data?.golemRunnerMissionModels?.edges) { + setMissions([]); + return []; + } + + const fetchedMissions = result.data.golemRunnerMissionModels.edges.map((edge: MissionEdge) => { + return toriiNodeToMission(edge.node); + }); + + setMissions(fetchedMissions); + return fetchedMissions; + + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to fetch missions"; + setError(errorMessage); + throw err; + } finally { + setIsLoading(false); + } + }, []); + + return { + missions, + isLoading, + error, + fetchTodayMissions + }; +}; \ No newline at end of file diff --git a/client/src/dojo/hooks/useMissionSpawner.tsx b/client/src/dojo/hooks/useMissionSpawner.tsx new file mode 100644 index 0000000..fa74a1c --- /dev/null +++ b/client/src/dojo/hooks/useMissionSpawner.tsx @@ -0,0 +1,149 @@ +import { useState, useCallback } from "react"; +import { useAccount } from "@starknet-react/core"; +import { useDojoSDK } from "@dojoengine/sdk/react"; +import { Account, CairoCustomEnum } from "starknet"; +import { AIAgentService } from '../../services/aiAgent'; +import { + ElizaMissionData, + createFallbackMissions, + parseElizaResponse +} from '../../components/types/missionTypes'; + +interface UseMissionSpawnerReturn { + isSpawning: boolean; + error: string | null; + spawnMissions: (playerAddress: string) => Promise; +} + +const elizaDataToCairoEnums = (elizaData: ElizaMissionData) => { + const worldMap: Record = { + 'Forest': new CairoCustomEnum({ Forest: "Forest" }), + 'Volcano': new CairoCustomEnum({ Volcano: "Volcano" }), + 'Glacier': new CairoCustomEnum({ Glacier: "Glacier" }) + }; + + const golemMap: Record = { + 'Fire': new CairoCustomEnum({ Fire: "Fire" }), + 'Ice': new CairoCustomEnum({ Ice: "Ice" }), + 'Stone': new CairoCustomEnum({ Stone: "Stone" }) + }; + + return { + target_coins: elizaData.target_coins, + required_world: worldMap[elizaData.required_world] || worldMap['Forest'], + required_golem: golemMap[elizaData.required_golem] || golemMap['Fire'], + description: elizaData.description + }; +}; + +export const useMissionSpawner = (): UseMissionSpawnerReturn => { + const { client } = useDojoSDK(); + const { account } = useAccount(); + const [isSpawning, setIsSpawning] = useState(false); + const [error, setError] = useState(null); + + const spawnMissions = useCallback(async (): Promise => { + if (!account) { + setError("No account connected"); + return false; + } + + setIsSpawning(true); + setError(null); + + try { + // Generate 3 missions from Eliza + const elizaMissions: ElizaMissionData[] = []; + const fallbackMissions = createFallbackMissions(); + + for (let i = 0; i < 3; i++) { + try { + console.log(`🤖 Requesting mission ${i + 1} from Eliza...`); + + // Just a small delay to avoid rate limiting + if (i > 0) { + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + const elizaResponse = await AIAgentService.getDailyMission(); + const elizaData = parseElizaResponse(elizaResponse); + + if (elizaData) { + elizaMissions.push(elizaData); + console.log(`✅ Mission ${i + 1} from Eliza:`, elizaData); + } else { + const fallback = fallbackMissions[i] || fallbackMissions[0]; + elizaMissions.push(fallback); + console.log(`⚠️ Mission ${i + 1} parse failed, using fallback:`, fallback); + } + + } catch (error) { + console.error(`❌ Error getting mission ${i + 1} from Eliza:`, error); + const fallback = fallbackMissions[i] || fallbackMissions[0]; + elizaMissions.push(fallback); + } + } + + // Write missions into the Dojo Contracts + const results = []; + + for (let i = 0; i < elizaMissions.length; i++) { + const elizaData = elizaMissions[i]; + try { + const cairoData = elizaDataToCairoEnums(elizaData); + + const tx = await client.game.createMission( + account as Account, + cairoData.target_coins, + cairoData.required_world, + cairoData.required_golem, + cairoData.description + ); + + if (tx && tx.code === "SUCCESS") { + console.log(`✅ Mission ${i + 1} created successfully in dojo`); + console.log("Transaction details:", tx); + results.push({ success: true }); + } else { + results.push({ success: false, error: `Transaction failed: ${tx?.code}` }); + } + + // Small delay between transactions + if (i < elizaMissions.length - 1) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + } catch (error) { + results.push({ + success: false, + error: error instanceof Error ? error.message : "Unknown error" + }); + } + } + + const successful = results.filter(result => result.success).length; + + if (successful === 0) { + throw new Error("All mission creation transactions failed"); + } + + // Wait for blockchain to process + await new Promise(resolve => setTimeout(resolve, 8000)); + + return true; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Failed to spawn missions"; + setError(errorMessage); + return false; + } finally { + setIsSpawning(false); + } + }, [account, client.game]); + + return { + isSpawning, + error, + spawnMissions + }; +}; \ No newline at end of file diff --git a/client/src/services/aiAgent.ts b/client/src/services/aiAgent.ts index cffdbab..5a3fdac 100644 --- a/client/src/services/aiAgent.ts +++ b/client/src/services/aiAgent.ts @@ -1,5 +1,3 @@ -import { db } from './db'; - const ELIZA_URL = import.meta.env.VITE_ELIZA_URL; interface AIMessageResponse { @@ -11,14 +9,13 @@ interface AIMessageResponse { export class AIAgentService { private static async fetchDailyMission(): Promise { try { - const response = await fetch(ELIZA_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - text: "Give me a daily mission for my golem", // TODO: Provide dinamically the golem name and map available + text: "Give me a daily mission for my golem", userId: "user", userName: "User" }) @@ -40,54 +37,11 @@ export class AIAgentService { } catch (error) { console.error('Error fetching daily mission:', error); - return 'Default mission: Complete 3 battles today!'; + throw error; } } - static async getDailyMission(playerAddress: string | undefined | null): Promise { - - if (!playerAddress || typeof playerAddress !== 'string') { - return 'Default mission: Complete 3 battles today!'; - } - - try { - const cachedMission = await db.dailyMissions - .where('playerAddress') - .equals(playerAddress) - .last(); - - if (cachedMission) { - const now = Date.now(); - const missionAge = now - cachedMission.timestamp; - const twentyFourHours = 24 * 60 * 60 * 1000; - - if (missionAge < twentyFourHours) { - return cachedMission.mission; - } - } - - const newMission = await this.fetchDailyMission(); - - if (!newMission) { - throw new Error('Failed to get new mission'); - } - - // First try to delete any existing mission for the player - await db.dailyMissions - .where('playerAddress') - .equals(playerAddress) - .delete(); - - // Then add the new mission - await db.dailyMissions.add({ - playerAddress, - mission: newMission, - timestamp: Date.now() - }); - return newMission; - } catch (error) { - console.error('Error in getDailyMission:', error); - return 'Default mission: Complete 3 battles today!'; - } + static async getDailyMission(): Promise { + return await this.fetchDailyMission(); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/client/src/utils/TimeHelpers.ts b/client/src/utils/TimeHelpers.ts index 268fbf9..ace07c3 100644 --- a/client/src/utils/TimeHelpers.ts +++ b/client/src/utils/TimeHelpers.ts @@ -8,8 +8,8 @@ export const SECONDS_PER_DAY = 86400; // 24 * 60 * 60 export const MILLISECONDS_PER_DAY = SECONDS_PER_DAY * 1000; /** - * Converts a Unix timestamp (in seconds) to day number - * Matches the Cairo implementation: timestamp / SECONDS_PER_DAY + * Converts a Unix timestamp (in seconds) to day number + * 🛠️ FIX: This should match Cairo exactly * @param timestamp Unix timestamp in seconds * @returns Day number since Unix epoch */ @@ -18,22 +18,25 @@ export function unixTimestampToDay(timestamp: number): number { } /** - * Gets the current day timestamp (start of day in seconds) - * This is what we'll use to query missions for "today" - * @returns Unix timestamp for start of current day in seconds + * 🛠️ FIX: Gets the current day NUMBER (not timestamp) + * This matches exactly what the Cairo contract does: + * Timestamp::unix_timestamp_to_day(current_timestamp) + * @returns Day number since Unix epoch (like 20261) */ -export function getCurrentDayTimestamp(): number { - const now = new Date(); - const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - return Math.floor(startOfDay.getTime() / 1000); // Convert to seconds +export function getCurrentDay(): number { + const nowSeconds = Math.floor(Date.now() / 1000); + return Math.floor(nowSeconds / SECONDS_PER_DAY); } /** - * Gets the current day number - * @returns Current day number since Unix epoch + * 🛠️ FIX: Gets current day timestamp for START of day + * This is different from getCurrentDay() - this gives start of day timestamp + * @returns Unix timestamp for start of current day in seconds */ -export function getCurrentDay(): number { - return unixTimestampToDay(getCurrentDayTimestamp()); +export function getCurrentDayTimestamp(): number { + const now = new Date(); + const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + return Math.floor(startOfDay.getTime() / 1000); } /** @@ -49,14 +52,15 @@ export function isDifferentDay(timestamp1: number, timestamp2: number): boolean } /** - * Checks if a mission was created today - * @param missionCreatedAt Mission's created_at timestamp (from contract, in seconds) + * 🛠️ FIX: Checks if a mission was created today + * Mission created_at is stored as DAY NUMBER in Cairo contract + * @param missionCreatedAt Mission's created_at (day number from contract) * @returns true if mission was created today */ export function isMissionFromToday(missionCreatedAt: number): boolean { - const missionDay = unixTimestampToDay(missionCreatedAt); const currentDay = getCurrentDay(); - return missionDay === currentDay; + console.log(`🔍 Comparing mission day ${missionCreatedAt} with current day ${currentDay}`); + return missionCreatedAt === currentDay; } /** @@ -108,6 +112,31 @@ export function getDayTimestamp(daysOffset: number = 0): number { return Math.floor(targetDate.getTime() / 1000); } +/** + * 🛠️ DEBUG: Function to validate our calculations + */ +export function debugTimeCalculations() { + const now = Date.now(); + const nowSeconds = Math.floor(now / 1000); + const currentDay = getCurrentDay(); + const currentDayTimestamp = getCurrentDayTimestamp(); + + console.log('=== Time Debug - Expected to match blockchain ==='); + console.log('Current time (ms):', now); + console.log('Current time (seconds):', nowSeconds); + console.log('Current DAY NUMBER (for GraphQL):', currentDay); + console.log('Start of day timestamp:', currentDayTimestamp); + console.log('Blockchain mission created_at example:', 20261); + console.log('Do they match timezone?', currentDay, 'vs', 20261); + + // Calculate what day 20261 represents + const blockchainDayAsDate = new Date(20261 * SECONDS_PER_DAY * 1000); + console.log('Blockchain day 20261 represents:', blockchainDayAsDate.toDateString()); + + const todayAsDay = getCurrentDay(); + console.log('Today as day number:', todayAsDay); +} + // Debug utility to validate our time calculations export function debugTimeUtils() { const now = Date.now(); diff --git a/client/src/zustand/store.ts b/client/src/zustand/store.ts index ad71b5e..80c0ad2 100644 --- a/client/src/zustand/store.ts +++ b/client/src/zustand/store.ts @@ -230,8 +230,6 @@ const useAppStore = create()( worlds: state.worlds, currentGolem: state.currentGolem, currentWorld: state.currentWorld, - missions: state.missions, - lastMissionFetch: state.lastMissionFetch, // persist last fetch time }), } )