479 lines
14 KiB
TypeScript
479 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import type React from "react";
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
useCallback,
|
|
useRef,
|
|
} from "react";
|
|
import {
|
|
type UserStats,
|
|
initialUserStats,
|
|
loadUserStats,
|
|
saveUserStats,
|
|
addExperience,
|
|
allocateStat,
|
|
deallocateStat,
|
|
addItemToInventory,
|
|
removeItemFromInventory,
|
|
useConsumableItem,
|
|
} from "@/utils/storage";
|
|
import type { InventoryItem } from "@/data/enemies";
|
|
// Add these imports
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import { Quest } from "@/interface/quest.interface";
|
|
|
|
// Update the UserContextType interface to include quest management functions
|
|
interface UserContextType {
|
|
userStats: UserStats;
|
|
setUserStats: React.Dispatch<React.SetStateAction<UserStats>>;
|
|
addExp: (exp: number) => void;
|
|
allocateStatPoint: (stat: keyof UserStats["stats"]) => void;
|
|
deallocateStatPoint: (stat: keyof UserStats["stats"]) => void;
|
|
completeQuest: (questId: string) => void;
|
|
updateQuestProgress: (questId: string, progress: number) => void;
|
|
updateQuest: (
|
|
questId: string,
|
|
updates: Partial<Omit<Quest, "id" | "completed" | "progress">>
|
|
) => void;
|
|
addCustomQuest: (
|
|
quest: Omit<Quest, "id" | "active" | "completed" | "progress">
|
|
) => void;
|
|
deleteQuest: (questId: string) => void;
|
|
addItem: (item: InventoryItem) => void;
|
|
removeItem: (itemId: string, quantity?: number) => void;
|
|
useItem: (itemId: string) => void;
|
|
addGold: (amount: number) => void;
|
|
levelUpCount: number;
|
|
showLevelUpModal: boolean;
|
|
setShowLevelUpModal: (show: boolean) => void;
|
|
resetLevelUpCount: () => void;
|
|
inCombat: boolean;
|
|
setInCombat: React.Dispatch<React.SetStateAction<boolean>>;
|
|
}
|
|
|
|
const UserContext = createContext<UserContextType | undefined>(undefined);
|
|
|
|
export function UserProvider({ children }: { children: React.ReactNode }) {
|
|
const [userStats, setUserStats] = useState<UserStats>(initialUserStats);
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
const [itemToUse, setItemToUse] = useState<string | null>(null);
|
|
// Add state to track level up events
|
|
const [levelUpCount, setLevelUpCount] = useState(0);
|
|
const [showLevelUpModal, setShowLevelUpModal] = useState(false);
|
|
// Track last recovery time for HP and MP regeneration
|
|
const [lastRecoveryTime, setLastRecoveryTime] = useState<number>(Date.now());
|
|
const recoveryIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
// Add state to track if player is in combat
|
|
const [inCombat, setInCombat] = useState(false);
|
|
|
|
// Reset level up count
|
|
const resetLevelUpCount = () => {
|
|
setLevelUpCount(0);
|
|
};
|
|
|
|
// Load user stats from localStorage on initial render
|
|
useEffect(() => {
|
|
let loadedStats = loadUserStats();
|
|
|
|
// Check if there's a last recovery time stored in localStorage
|
|
const storedLastRecoveryTime = localStorage.getItem("lastRecoveryTime");
|
|
if (storedLastRecoveryTime) {
|
|
setLastRecoveryTime(parseInt(storedLastRecoveryTime, 10));
|
|
}
|
|
|
|
// Check for recovery that should have happened while offline
|
|
if (storedLastRecoveryTime) {
|
|
const lastTime = parseInt(storedLastRecoveryTime, 10);
|
|
const now = Date.now();
|
|
const RECOVERY_INTERVAL = 500 * 60 * 10; // 5 minutes in milliseconds
|
|
const RECOVERY_PERCENTAGE = 0.1; // 10%
|
|
|
|
// Calculate how many recovery periods have passed
|
|
const timeDifference = now - lastTime;
|
|
const recoveryPeriods = Math.floor(timeDifference / RECOVERY_INTERVAL);
|
|
|
|
if (recoveryPeriods > 0) {
|
|
// Apply recovery for the time user was away
|
|
const newStats = { ...loadedStats };
|
|
|
|
if (newStats.hp < newStats.maxHp || newStats.mp < newStats.maxMp) {
|
|
// Calculate total recovery amount (capped at max values)
|
|
const hpRecoveryTotal = Math.min(
|
|
newStats.maxHp - newStats.hp,
|
|
Math.floor(newStats.maxHp * RECOVERY_PERCENTAGE * recoveryPeriods)
|
|
);
|
|
|
|
const mpRecoveryTotal = Math.min(
|
|
newStats.maxMp - newStats.mp,
|
|
Math.floor(newStats.maxMp * RECOVERY_PERCENTAGE * recoveryPeriods)
|
|
);
|
|
|
|
// Apply the recovery
|
|
newStats.hp = Math.min(newStats.maxHp, newStats.hp + hpRecoveryTotal);
|
|
newStats.mp = Math.min(newStats.maxMp, newStats.mp + mpRecoveryTotal);
|
|
|
|
// Update the recovery time to reflect the most recent recovery
|
|
const mostRecentRecoveryTime =
|
|
lastTime + recoveryPeriods * RECOVERY_INTERVAL;
|
|
setLastRecoveryTime(mostRecentRecoveryTime);
|
|
localStorage.setItem(
|
|
"lastRecoveryTime",
|
|
mostRecentRecoveryTime.toString()
|
|
);
|
|
|
|
// Set the updated stats
|
|
loadedStats = newStats;
|
|
}
|
|
}
|
|
}
|
|
|
|
setUserStats(loadedStats);
|
|
setIsInitialized(true);
|
|
}, []);
|
|
|
|
// Save user stats to localStorage whenever they change
|
|
useEffect(() => {
|
|
if (isInitialized) {
|
|
saveUserStats(userStats);
|
|
}
|
|
}, [userStats, isInitialized]);
|
|
|
|
// Natural HP and MP recovery system - 10% every 5 minutes when not in combat
|
|
useEffect(() => {
|
|
if (!isInitialized) return;
|
|
|
|
// Clear any existing interval
|
|
if (recoveryIntervalRef.current) {
|
|
clearInterval(recoveryIntervalRef.current);
|
|
}
|
|
|
|
const RECOVERY_INTERVAL = 500 * 60 * 10; // 5 minutes (changed from 5)
|
|
const RECOVERY_PERCENTAGE = 0.1; // 10%
|
|
const CHECK_INTERVAL = 60000; // Check every minute
|
|
|
|
// Only proceed with recovery if not in combat
|
|
if (!inCombat) {
|
|
// Check if player needs recovery (if HP or MP is below max)
|
|
const needsRecovery =
|
|
userStats.hp < userStats.maxHp || userStats.mp < userStats.maxMp;
|
|
|
|
if (needsRecovery) {
|
|
// Set up interval for recovery
|
|
recoveryIntervalRef.current = setInterval(() => {
|
|
const now = Date.now();
|
|
const timeSinceLastRecovery = now - lastRecoveryTime;
|
|
|
|
// Check if enough time has passed for recovery
|
|
if (timeSinceLastRecovery >= RECOVERY_INTERVAL) {
|
|
setUserStats((prevStats) => {
|
|
// Don't apply recovery if player is in combat
|
|
if (inCombat) return prevStats;
|
|
|
|
// Calculate recovery amounts (10% of max values)
|
|
const hpRecoveryAmount = Math.floor(
|
|
prevStats.maxHp * RECOVERY_PERCENTAGE
|
|
);
|
|
const mpRecoveryAmount = Math.floor(
|
|
prevStats.maxMp * RECOVERY_PERCENTAGE
|
|
);
|
|
|
|
// Calculate new HP and MP values, not exceeding max values
|
|
const newHp = Math.min(
|
|
prevStats.maxHp,
|
|
prevStats.hp + hpRecoveryAmount
|
|
);
|
|
const newMp = Math.min(
|
|
prevStats.maxMp,
|
|
prevStats.mp + mpRecoveryAmount
|
|
);
|
|
|
|
// Only update if there's actual recovery
|
|
if (newHp === prevStats.hp && newMp === prevStats.mp) {
|
|
return prevStats;
|
|
}
|
|
|
|
return {
|
|
...prevStats,
|
|
hp: newHp,
|
|
mp: newMp,
|
|
};
|
|
});
|
|
|
|
// Update last recovery time and store in localStorage
|
|
const newRecoveryTime = now;
|
|
setLastRecoveryTime(newRecoveryTime);
|
|
localStorage.setItem(
|
|
"lastRecoveryTime",
|
|
newRecoveryTime.toString()
|
|
);
|
|
}
|
|
}, CHECK_INTERVAL); // Check every minute
|
|
}
|
|
}
|
|
|
|
return () => {
|
|
if (recoveryIntervalRef.current) {
|
|
clearInterval(recoveryIntervalRef.current);
|
|
}
|
|
};
|
|
}, [
|
|
isInitialized,
|
|
userStats.hp,
|
|
userStats.mp,
|
|
userStats.maxHp,
|
|
userStats.maxMp,
|
|
lastRecoveryTime,
|
|
inCombat,
|
|
]);
|
|
|
|
// Update lastRecoveryTime in localStorage when component unmounts
|
|
useEffect(() => {
|
|
return () => {
|
|
localStorage.setItem("lastRecoveryTime", lastRecoveryTime.toString());
|
|
};
|
|
}, [lastRecoveryTime]);
|
|
|
|
// Add experience points
|
|
const addExp = (exp: number) => {
|
|
const prevLevel = userStats.level;
|
|
|
|
setUserStats((prevStats) => {
|
|
const updatedStats = addExperience(prevStats, exp);
|
|
|
|
// Check if level increased and by how much
|
|
const levelDifference = updatedStats.level - prevLevel;
|
|
|
|
if (levelDifference > 0) {
|
|
// Update level up count and show modal
|
|
setLevelUpCount(levelDifference);
|
|
setShowLevelUpModal(true);
|
|
}
|
|
|
|
return updatedStats;
|
|
});
|
|
};
|
|
|
|
// Allocate a stat point
|
|
const allocateStatPoint = (stat: keyof UserStats["stats"]) => {
|
|
setUserStats((prevStats) => allocateStat(prevStats, stat));
|
|
};
|
|
|
|
// Deallocate a stat point
|
|
const deallocateStatPoint = (stat: keyof UserStats["stats"]) => {
|
|
setUserStats((prevStats) => deallocateStat(prevStats, stat));
|
|
};
|
|
|
|
// Add these functions to the UserProvider component
|
|
// Complete a quest
|
|
const completeQuest = (questId: string) => {
|
|
const prevLevel = userStats.level;
|
|
|
|
setUserStats((prevStats) => {
|
|
// Find the quest
|
|
const quest = prevStats.quests.find((q) => q.id === questId);
|
|
if (!quest) return prevStats;
|
|
|
|
// First add the experience
|
|
let newStats = addExperience(prevStats, quest.expReward);
|
|
|
|
// Check if level increased and by how much
|
|
const levelDifference = newStats.level - prevLevel;
|
|
|
|
if (levelDifference > 0) {
|
|
// Update level up count and show modal
|
|
setLevelUpCount(levelDifference);
|
|
setShowLevelUpModal(true);
|
|
}
|
|
|
|
// Then add the stat points
|
|
newStats = {
|
|
...newStats,
|
|
statPoints: newStats.statPoints + quest.statPointsReward,
|
|
completedQuests: [...newStats.completedQuests, questId],
|
|
quests: newStats.quests.map((q) =>
|
|
q.id === questId
|
|
? {
|
|
...q,
|
|
progress: 100,
|
|
completed: true,
|
|
active: false,
|
|
completedAt: Date.now(), // Set completed timestamp
|
|
}
|
|
: q
|
|
),
|
|
};
|
|
|
|
// Apply stat rewards if any
|
|
if (quest.statRewards) {
|
|
Object.entries(quest.statRewards).forEach(([stat, value]) => {
|
|
if (value && value > 0) {
|
|
newStats.stats[stat as keyof UserStats["stats"]] += value;
|
|
}
|
|
});
|
|
|
|
// Update derived stats
|
|
if (quest.statRewards.vit) {
|
|
newStats.maxHp = Math.floor(
|
|
100 + newStats.level * 10 + newStats.stats.vit * 5
|
|
);
|
|
newStats.hp = newStats.maxHp; // Fully heal on stat increase
|
|
}
|
|
if (quest.statRewards.int) {
|
|
newStats.maxMp = Math.floor(
|
|
10 + newStats.level * 2 + newStats.stats.int * 2
|
|
);
|
|
newStats.mp = newStats.maxMp; // Fully restore MP on stat increase
|
|
}
|
|
}
|
|
|
|
// Add gold reward if any
|
|
if (quest.goldReward) {
|
|
newStats.gold += quest.goldReward;
|
|
}
|
|
|
|
// Add item rewards if any
|
|
if (quest.itemRewards && quest.itemRewards.length > 0) {
|
|
quest.itemRewards.forEach((item) => {
|
|
newStats = addItemToInventory(newStats, item);
|
|
});
|
|
}
|
|
|
|
return newStats;
|
|
});
|
|
};
|
|
|
|
// Update quest progress
|
|
const updateQuestProgress = (questId: string, progress: number) => {
|
|
setUserStats((prevStats) => ({
|
|
...prevStats,
|
|
quests: prevStats.quests.map((quest) =>
|
|
quest.id === questId ? { ...quest, progress } : quest
|
|
),
|
|
}));
|
|
};
|
|
|
|
// Add a custom quest
|
|
const addCustomQuest = (
|
|
quest: Omit<Quest, "id" | "active" | "completed" | "progress">
|
|
) => {
|
|
const newQuest = {
|
|
...quest,
|
|
id: uuidv4(),
|
|
active: true,
|
|
completed: false,
|
|
progress: 0,
|
|
isCustom: true,
|
|
createdAt: Date.now(), // Set created timestamp
|
|
};
|
|
|
|
setUserStats((prevStats) => ({
|
|
...prevStats,
|
|
quests: [...prevStats.quests, newQuest],
|
|
}));
|
|
};
|
|
|
|
// Delete a quest
|
|
const deleteQuest = (questId: string) => {
|
|
setUserStats((prevStats) => ({
|
|
...prevStats,
|
|
quests: prevStats.quests.filter((quest) => quest.id !== questId),
|
|
}));
|
|
};
|
|
|
|
// Add an item to inventory
|
|
const addItem = (item: InventoryItem) => {
|
|
setUserStats((prevStats) => addItemToInventory(prevStats, item));
|
|
};
|
|
|
|
// Remove an item from inventory
|
|
const removeItem = (itemId: string, quantity?: number) => {
|
|
setUserStats((prevStats) =>
|
|
removeItemFromInventory(prevStats, itemId, quantity)
|
|
);
|
|
};
|
|
|
|
// Use a consumable item
|
|
const useItem = (itemId: string) => {
|
|
setItemToUse(itemId);
|
|
};
|
|
|
|
// Add gold
|
|
const addGold = (amount: number) => {
|
|
setUserStats((prevStats) => ({
|
|
...prevStats,
|
|
gold: prevStats.gold + amount,
|
|
}));
|
|
};
|
|
|
|
const handleUseConsumableItem = useCallback(() => {
|
|
if (itemToUse) {
|
|
setUserStats((prevStats) => useConsumableItem(prevStats, itemToUse));
|
|
setItemToUse(null);
|
|
}
|
|
}, [itemToUse, setUserStats]);
|
|
|
|
useEffect(() => {
|
|
handleUseConsumableItem();
|
|
}, [handleUseConsumableItem]);
|
|
|
|
// Add update quest function
|
|
const updateQuest = (
|
|
questId: string,
|
|
updates: Partial<Omit<Quest, "id" | "completed" | "progress">>
|
|
) => {
|
|
setUserStats((prevStats) => ({
|
|
...prevStats,
|
|
quests: prevStats.quests.map((quest) =>
|
|
quest.id === questId
|
|
? {
|
|
...quest,
|
|
...updates,
|
|
}
|
|
: quest
|
|
),
|
|
}));
|
|
};
|
|
|
|
// Update the UserContext.Provider value
|
|
return (
|
|
<UserContext.Provider
|
|
value={{
|
|
userStats,
|
|
setUserStats,
|
|
addExp,
|
|
allocateStatPoint,
|
|
deallocateStatPoint,
|
|
completeQuest,
|
|
updateQuestProgress,
|
|
updateQuest,
|
|
addCustomQuest,
|
|
deleteQuest,
|
|
addItem,
|
|
removeItem,
|
|
useItem,
|
|
addGold,
|
|
levelUpCount,
|
|
showLevelUpModal,
|
|
setShowLevelUpModal,
|
|
resetLevelUpCount,
|
|
inCombat,
|
|
setInCombat,
|
|
}}
|
|
>
|
|
{children}
|
|
</UserContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useUser() {
|
|
const context = useContext(UserContext);
|
|
if (context === undefined) {
|
|
throw new Error("useUser must be used within a UserProvider");
|
|
}
|
|
return context;
|
|
}
|