Compare commits
No commits in common. "c2e5bf92a37767f6ebe95b7f6b59bd2ae8659e06" and "243c50a1a026dae74591db0cd20221214d81525a" have entirely different histories.
c2e5bf92a3
...
243c50a1a0
@ -1,5 +1,5 @@
|
|||||||
import { Pet } from '../types/Pet';
|
import { Pet } from '../types/Pet';
|
||||||
import { Brain, Dumbbell, Smile, Sparkles, Coins, Pizza, Trash2, Trophy, Heart, Star } from 'lucide-react';
|
import { Brain, Dumbbell, Heart, Sparkles, Coins, Pizza, Trash2, Trophy } from 'lucide-react';
|
||||||
import { PET_CLASSES } from '../data/petClasses';
|
import { PET_CLASSES } from '../data/petClasses';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import InventoryModal from './modal/InventoryModal';
|
import InventoryModal from './modal/InventoryModal';
|
||||||
@ -11,13 +11,12 @@ interface PetDisplayProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
||||||
const StatBar = ({ value, maxValue, label, icon: Icon }:
|
const StatBar = ({ value, maxValue, label, icon: Icon }: { value: number; maxValue: number; label: string; icon: any }) => (
|
||||||
{ value: number; maxValue: number; label: string; icon: any; }) => (
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex items-center space-x-2" title={label}>
|
|
||||||
<Icon className="w-5 h-5" />
|
<Icon className="w-5 h-5" />
|
||||||
<div className="flex-1 bg-gray-700 rounded-full h-4">
|
<div className="flex-1 bg-gray-700 rounded-full h-4">
|
||||||
<div
|
<div
|
||||||
className={`bg-blue-500 rounded-full h-4`}
|
className="bg-blue-500 rounded-full h-4"
|
||||||
style={{ width: `${(value / maxValue) * 100}%` }}
|
style={{ width: `${(value / maxValue) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -55,14 +54,6 @@ export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
<div className="space-y-4 mb-6">
|
||||||
<div className="space-y-4 mb-6">
|
|
||||||
<StatBar
|
|
||||||
value={pet.health}
|
|
||||||
maxValue={pet.maxHealth}
|
|
||||||
label="Health"
|
|
||||||
icon={Heart}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<StatBar
|
<StatBar
|
||||||
value={pet.stats.intelligence}
|
value={pet.stats.intelligence}
|
||||||
maxValue={pet.stats.maxIntelligence}
|
maxValue={pet.stats.maxIntelligence}
|
||||||
@ -79,7 +70,7 @@ export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
|||||||
value={pet.stats.charisma}
|
value={pet.stats.charisma}
|
||||||
maxValue={pet.stats.maxCharisma}
|
maxValue={pet.stats.maxCharisma}
|
||||||
label="Charisma"
|
label="Charisma"
|
||||||
icon={Smile}
|
icon={Heart}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -116,11 +107,7 @@ export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
|||||||
)}
|
)}
|
||||||
{showSkillTreeModal && (
|
{showSkillTreeModal && (
|
||||||
<SkillTreeModal
|
<SkillTreeModal
|
||||||
petId={pet.id}
|
onClose={() => setShowSkillTreeModal(false)}
|
||||||
petResources={pet.resources}
|
|
||||||
onPetUpdate={onPetUpdate}
|
|
||||||
onClose={() => setShowSkillTreeModal(false)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,7 +35,7 @@ export default function InventoryModal({ inventory, petId, onClose, onPetUpdate
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
<div className="bg-gray-800 p-6 rounded-xl max-w-2xl w-full">
|
<div className="bg-gray-800 p-6 rounded-xl max-w-2xl w-full">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-2xl font-bold">Inventory</h2>
|
<h2 className="text-2xl font-bold">Inventory</h2>
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
.customScroll {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: #4B5563 #1F2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customScroll::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customScroll::-webkit-scrollbar-track {
|
|
||||||
background: #1F2937;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customScroll::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #4B5563;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customScroll::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: #6B7280;
|
|
||||||
}
|
|
@ -1,204 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { Pet, Resources } from '../../types/Pet';
|
|
||||||
import { Skill, PetSkill, SkillType } from '../../types/Skills';
|
|
||||||
import { fetchPets, getAllSkills, getPetSkills, postAllocatePetSkill } from '../../services/api/api';
|
|
||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
import styles from './SkillTreeModal.module.css';
|
|
||||||
|
|
||||||
interface SkillTreeModalProps {
|
interface SkillTreeModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
petId: string;
|
|
||||||
petResources: Resources;
|
|
||||||
onPetUpdate: (updatedPet: Pet) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SkillTreeModal({ onClose, petId, petResources, onPetUpdate }: SkillTreeModalProps) {
|
export default function SkillTreeModal({ onClose }: SkillTreeModalProps) {
|
||||||
const [skills, setSkills] = useState<Skill[]>([]);
|
|
||||||
const [petSkills, setPetSkills] = useState<PetSkill[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [allocating, setAllocating] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const [allSkills, ownedSkills] = await Promise.all([
|
|
||||||
getAllSkills(),
|
|
||||||
getPetSkills(petId)
|
|
||||||
]);
|
|
||||||
setSkills(allSkills);
|
|
||||||
setPetSkills(ownedSkills);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching skills:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, [petId]);
|
|
||||||
|
|
||||||
const handleAllocateSkill = async (skillId: number) => {
|
|
||||||
try {
|
|
||||||
setAllocating(true);
|
|
||||||
const updatedPetSkill = await postAllocatePetSkill(petId, skillId);
|
|
||||||
setPetSkills([...petSkills, updatedPetSkill]);
|
|
||||||
// Refresh pet data to update skill points
|
|
||||||
const updatedPet = await fetchPets();
|
|
||||||
onPetUpdate(updatedPet[0]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error allocating skill:', error);
|
|
||||||
} finally {
|
|
||||||
setAllocating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSkillTier = (skillId: number) => {
|
|
||||||
return petSkills.find(ps => ps.skillId === skillId)?.currentTier;
|
|
||||||
};
|
|
||||||
|
|
||||||
const canAllocateSkill = (skill: Skill) => {
|
|
||||||
// check if all required skills are owned, and has all the required resources
|
|
||||||
const requiredSkills = skills.filter(s => skill.skillsIdRequired?.includes(s.id));
|
|
||||||
const requiredSkillIds = requiredSkills.map(s => s.id);
|
|
||||||
const ownedRequiredSkills = petSkills.filter(ps => requiredSkillIds.includes(ps.skillId));
|
|
||||||
const hasAllRequiredSkills = requiredSkills.length === ownedRequiredSkills.length;
|
|
||||||
|
|
||||||
const hasAllResources = skill.skillRequirements.every(req => {
|
|
||||||
const resourceValue = petResources[req.resource.toLowerCase() as keyof Resources];
|
|
||||||
return resourceValue >= req.cost;
|
|
||||||
});
|
|
||||||
|
|
||||||
return hasAllRequiredSkills && hasAllResources;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRequiredSkillNames = (skillId: number[] | null) => {
|
|
||||||
if (!skillId) return null;
|
|
||||||
return skills.filter(s => skillId.includes(s.id)).map(s => s.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNextTierEffect = (currentTier?: string) => {
|
|
||||||
if (!currentTier) return 'I';
|
|
||||||
if (currentTier === 'I') return 'II';
|
|
||||||
if (currentTier === 'II') return 'III';
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSkillNode = (skill: Skill) => {
|
|
||||||
const owned = petSkills.some(ps => ps.skillId === skill.id);
|
|
||||||
const tier = getSkillTier(skill.id);
|
|
||||||
const isMaxTier = tier === 'III';
|
|
||||||
const canAllocate = !isMaxTier && canAllocateSkill(skill);
|
|
||||||
const nextTierEffect = getNextTierEffect(tier);
|
|
||||||
const requiredSkillNames = getRequiredSkillNames(skill.skillsIdRequired);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={skill.id}
|
|
||||||
className={`
|
|
||||||
relative p-4 rounded-lg border-2 flex flex-col
|
|
||||||
${isMaxTier ? 'border-green-500 bg-gray-700' :
|
|
||||||
owned ? 'border-blue-500 bg-gray-700' : 'border-gray-600 bg-gray-800'}
|
|
||||||
transition-colors
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className="flex-grow">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<span>{skill.icon}</span>
|
|
||||||
<h3 className="font-bold">{skill.name}</h3>
|
|
||||||
{tier && (
|
|
||||||
<span className={`px-2 py-1 rounded text-xs ${isMaxTier ? 'bg-green-600' : 'bg-blue-600'}`}>
|
|
||||||
Tier {tier}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-300 mb-2">{skill.description}</p>
|
|
||||||
|
|
||||||
<div className="text-sm space-y-2 mb-3">
|
|
||||||
{!isMaxTier && nextTierEffect && (
|
|
||||||
<div className="text-blue-300">
|
|
||||||
Next: Tier {nextTierEffect}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isMaxTier && nextTierEffect && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-gray-400 font-medium">Requirements:</div>
|
|
||||||
{requiredSkillNames && requiredSkillNames.map((name, idx) => (
|
|
||||||
<div key={idx} className={`text-xs ${canAllocate ? 'text-green-400' : 'text-red-400'}`}>
|
|
||||||
• Requires: {name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{skill.skillRequirements.map((req, idx) => (
|
|
||||||
<div key={idx} className={`text-xs ${petResources[req.resource.toLowerCase() as keyof Resources] >= req.cost
|
|
||||||
? 'text-green-400' : 'text-red-400'
|
|
||||||
}`}>
|
|
||||||
• {req.resource}: {req.cost}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isMaxTier ? (
|
|
||||||
<div className="mt-auto px-3 py-1 rounded text-sm bg-green-600 text-center">
|
|
||||||
Mastered
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => handleAllocateSkill(skill.id)}
|
|
||||||
disabled={!canAllocate || allocating}
|
|
||||||
className={`
|
|
||||||
mt-auto px-3 py-1 rounded text-sm
|
|
||||||
${canAllocate
|
|
||||||
? owned
|
|
||||||
? 'bg-blue-600 hover:bg-blue-700'
|
|
||||||
: 'bg-green-600 hover:bg-green-700'
|
|
||||||
: 'bg-gray-600 cursor-not-allowed'}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{allocating ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
owned ? 'Upgrade' : 'Learn'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSkillTree = () => {
|
|
||||||
const groundSkills = skills.filter(s => s.type === 'GROUND');
|
|
||||||
const grandSkills = skills.filter(s => s.type === 'GRAND');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{groundSkills.map(renderSkillNode)}
|
|
||||||
</div>
|
|
||||||
<div className="border-t border-gray-600 pt-6">
|
|
||||||
<h2 className="text-xl font-bold mb-4">Advanced Skills</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{grandSkills.map(renderSkillNode)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
<div
|
||||||
<div className={`bg-gray-800 p-6 rounded-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto ${styles.customScroll}`}>
|
className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
||||||
|
>
|
||||||
|
<div className="bg-gray-800 p-6 rounded-xl max-w-2xl w-full">
|
||||||
<h2 className="text-2xl font-bold mb-4">Skill Tree</h2>
|
<h2 className="text-2xl font-bold mb-4">Skill Tree</h2>
|
||||||
|
<p className="text-gray-400 mb-6">Skill tree modal under development.</p>
|
||||||
{loading ? (
|
|
||||||
<div className="flex justify-center items-center h-40">
|
|
||||||
<Loader2 className="w-8 h-8 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
renderSkillTree()
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-6 flex justify-end">
|
<div className="mt-6 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
@ -38,7 +38,7 @@ export async function getAllSkills(): Promise<Skill[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getPetSkills(petId: string): Promise<PetSkill[]> {
|
export async function getPetSkills(petId: string): Promise<PetSkill[]> {
|
||||||
const response = await api.get<PetSkill[]>(`/api/v1/skill/${petId}/skills`);
|
const response = await api.get<PetSkill[]>(`/api/v1/skill/${petId}/pet`);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ export interface Pet {
|
|||||||
stats: PetStats;
|
stats: PetStats;
|
||||||
resources: Resources;
|
resources: Resources;
|
||||||
level: number;
|
level: number;
|
||||||
|
experience: number;
|
||||||
health: number;
|
health: number;
|
||||||
maxHealth: number;
|
maxHealth: number;
|
||||||
petGatherAction: PetGatherAction;
|
petGatherAction: PetGatherAction;
|
||||||
|
@ -1,34 +1,26 @@
|
|||||||
export type SkillType = 'GROUND' | 'GRAND';
|
|
||||||
|
|
||||||
export interface Skill {
|
export interface Skill {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
type: SkillType;
|
type: string;
|
||||||
skillRequirements: SkillRequirement[];
|
pointsCost: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
skillsIdRequired: number[] | null;
|
skillsIdRequired: number | null;
|
||||||
effects: SkillEffect[];
|
effects: SkillEffect[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkillRequirement {
|
|
||||||
cost: number;
|
|
||||||
resource: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SkillEffect {
|
export interface SkillEffect {
|
||||||
id: number;
|
id: number;
|
||||||
skillId: number;
|
skillId: number;
|
||||||
tier: SkillTier;
|
tier: string;
|
||||||
effect: string;
|
effect: string;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SkillTier = 'I' | 'II' | 'III';
|
|
||||||
export interface PetSkill {
|
export interface PetSkill {
|
||||||
id: number;
|
id: number;
|
||||||
petId: string;
|
petId: string;
|
||||||
skillId: number;
|
skillId: number;
|
||||||
skill: Skill;
|
skill: Skill;
|
||||||
currentTier: SkillTier;
|
currentTier: string;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user