Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions client/src/components/types/missionTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Super simple types for mission management
* Zustand uses the full Mission bindings, but these are for working data
*/

/**
* Simple mission data that Eliza returns (no complex types)
*/
export interface ElizaMissionData {
target_coins: number;
required_world: 'Forest' | 'Volcano' | 'Glacier';
required_golem: 'Fire' | 'Ice' | 'Stone';
description: string;
}

/**
* Simple display data for the UI (converted from Mission bindings)
*/
export interface MissionDisplayData {
id: number;
title: string;
description: string;
difficulty: 'Easy' | 'Mid' | 'Hard';
reward: number;
requiredWorld: string;
requiredGolem: string;
completed: boolean;
claimed?: boolean;
}

/**
* Creates fallback missions if Eliza fails
*/
export function createFallbackMissions(): ElizaMissionData[] {
return [
{
target_coins: 300,
required_world: 'Forest',
required_golem: 'Stone',
description: 'Collect 300 coins in the mystical forest with your stone golem'
},
{
target_coins: 500,
required_world: 'Volcano',
required_golem: 'Fire',
description: 'Gather 500 coins from the volcanic realm using your fire golem'
},
{
target_coins: 750,
required_world: 'Glacier',
required_golem: 'Ice',
description: 'Obtain 750 coins in the frozen wastes with your ice golem'
}
];
}

/**
* Validates if Eliza response has the correct structure
*/
export function isValidElizaMissionData(data: any): data is ElizaMissionData {
return (
typeof data === 'object' &&
typeof data.target_coins === 'number' &&
['Forest', 'Volcano', 'Glacier'].includes(data.required_world) &&
['Fire', 'Ice', 'Stone'].includes(data.required_golem) &&
typeof data.description === 'string' &&
data.target_coins > 0 &&
data.description.length > 0
);
}
125 changes: 125 additions & 0 deletions client/src/utils/TimeHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Time utilities for mission management
* Mirrors the Cairo timestamp utilities from the contract
*/

// Constants matching Cairo implementation
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
* @param timestamp Unix timestamp in seconds
* @returns Day number since Unix epoch
*/
export function unixTimestampToDay(timestamp: number): number {
return Math.floor(timestamp / SECONDS_PER_DAY);
}

/**
* 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
*/
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
}

/**
* Gets the current day number
* @returns Current day number since Unix epoch
*/
export function getCurrentDay(): number {
return unixTimestampToDay(getCurrentDayTimestamp());
}

/**
* Checks if two timestamps are from different days
* @param timestamp1 First timestamp in milliseconds (JS Date.now())
* @param timestamp2 Second timestamp in milliseconds (JS Date.now())
* @returns true if timestamps are from different days
*/
export function isDifferentDay(timestamp1: number, timestamp2: number): boolean {
const day1 = unixTimestampToDay(Math.floor(timestamp1 / 1000));
const day2 = unixTimestampToDay(Math.floor(timestamp2 / 1000));
return day1 !== day2;
}

/**
* Checks if a mission was created today
* @param missionCreatedAt Mission's created_at timestamp (from contract, in seconds)
* @returns true if mission was created today
*/
export function isMissionFromToday(missionCreatedAt: number): boolean {
const missionDay = unixTimestampToDay(missionCreatedAt);
const currentDay = getCurrentDay();
return missionDay === currentDay;
}

/**
* Checks if the missions cache is stale (from a different day)
* @param lastFetchTimestamp Last time missions were fetched (JS timestamp in ms)
* @returns true if cache is stale and needs refresh
*/
export function isMissionCacheStale(lastFetchTimestamp: number | null): boolean {
if (!lastFetchTimestamp) return true;

return isDifferentDay(lastFetchTimestamp, Date.now());
}

/**
* Gets a human-readable time until end of day
* @returns String like "5h 23m" until midnight
*/
export function getTimeUntilMidnight(): string {
const now = new Date();
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
const diff = midnight.getTime() - now.getTime();

const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));

if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
}

/**
* Formats a timestamp for debug/logging purposes
* @param timestamp Unix timestamp in seconds
* @returns Formatted date string
*/
export function formatTimestamp(timestamp: number): string {
return new Date(timestamp * 1000).toLocaleString();
}

/**
* Gets the timestamp for a specific day offset from today
* @param daysOffset Days to add/subtract from today (negative for past days)
* @returns Unix timestamp for start of that day in seconds
*/
export function getDayTimestamp(daysOffset: number = 0): number {
const now = new Date();
const targetDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysOffset);
return Math.floor(targetDate.getTime() / 1000);
}

// Debug utility to validate our time calculations
export function debugTimeUtils() {
const now = Date.now();
const currentDayTimestamp = getCurrentDayTimestamp();
const currentDay = getCurrentDay();

console.log('=== Time Utils Debug ===');
console.log('Current JS time (ms):', now);
console.log('Current day timestamp (s):', currentDayTimestamp);
console.log('Current day number:', currentDay);
console.log('Formatted current day:', formatTimestamp(currentDayTimestamp));
console.log('Time until midnight:', getTimeUntilMidnight());
console.log('Yesterday timestamp:', getDayTimestamp(-1));
console.log('Tomorrow timestamp:', getDayTimestamp(1));
}
61 changes: 59 additions & 2 deletions client/src/zustand/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { Player, Golem, World, Ranking } from '../dojo/bindings';
import { Player, Golem, World, Ranking, Mission } from '../dojo/bindings';

// Define application state interface
interface AppState {
Expand All @@ -12,6 +12,12 @@ interface AppState {
worlds: World[];
rankings: Ranking[];

// Missions data
missions: Mission[];
lastMissionFetch: number | null; // Timestamp of last fetch
isMissionsLoading: boolean;
missionsError: string | null;

// UI state
isLoading: boolean;
error: string | null;
Expand Down Expand Up @@ -46,6 +52,15 @@ interface AppActions {
setRankings: (rankings: Ranking[]) => void;
updateRanking: (ranking: Ranking) => void;

// Mission actions - NEW
setMissions: (missions: Mission[]) => void;
addMission: (mission: Mission) => void;
updateMissionStatus: (missionId: number, status: 'Pending' | 'Completed') => void;
setMissionsLoading: (loading: boolean) => void;
setMissionsError: (error: string | null) => void;
setLastMissionFetch: (timestamp: number) => void;
clearMissions: () => void;

// UI actions
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
Expand All @@ -68,6 +83,10 @@ const initialState: AppState = {
golems: [],
worlds: [],
rankings: [],
missions: [],
lastMissionFetch: null,
isMissionsLoading: false,
missionsError: null,
isLoading: false,
error: null,
currentGolem: null,
Expand All @@ -78,7 +97,7 @@ const initialState: AppState = {
// Create the store
const useAppStore = create<AppStore>()(
persist(
(set, _get) => ({
(set) => ({
// Initial state
...initialState,

Expand Down Expand Up @@ -150,6 +169,42 @@ const useAppStore = create<AppStore>()(
}
}),

// Mission actions
setMissions: (missions) => set({
missions,
lastMissionFetch: Date.now(),
missionsError: null
}),

addMission: (mission) => set((state) => ({
missions: [...state.missions, mission]
})),

updateMissionStatus: (missionId, status) => set((state) => ({
missions: state.missions.map(mission =>
mission.id === missionId
? {
...mission,
status: status === 'Completed'
? { activeVariant: 'Completed', Completed: 'Completed' } as any
: { activeVariant: 'Pending', Pending: 'Pending' } as any
}
: mission
)
})),

setMissionsLoading: (isMissionsLoading) => set({ isMissionsLoading }),

setMissionsError: (missionsError) => set({ missionsError }),

setLastMissionFetch: (lastMissionFetch) => set({ lastMissionFetch }),

clearMissions: () => set({
missions: [],
lastMissionFetch: null,
missionsError: null
}),

// UI actions
setLoading: (isLoading) => set({ isLoading }),
setError: (error) => set({ error }),
Expand All @@ -175,6 +230,8 @@ const useAppStore = create<AppStore>()(
worlds: state.worlds,
currentGolem: state.currentGolem,
currentWorld: state.currentWorld,
missions: state.missions,
lastMissionFetch: state.lastMissionFetch, // persist last fetch time
}),
}
)
Expand Down