feat: enhance PetDisplay and SkillTreeModal with resource management and skill allocation logic
This commit is contained in:
parent
78c0f52c39
commit
c2e5bf92a3
@ -1,5 +1,5 @@
|
|||||||
import { Pet } from '../types/Pet';
|
import { Pet } from '../types/Pet';
|
||||||
import { Brain, Dumbbell, Heart, Sparkles, Coins, Pizza, Trash2, Trophy } from 'lucide-react';
|
import { Brain, Dumbbell, Smile, Sparkles, Coins, Pizza, Trash2, Trophy, Heart, Star } 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,12 +11,13 @@ interface PetDisplayProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
||||||
const StatBar = ({ value, maxValue, label, icon: Icon }: { value: number; maxValue: number; label: string; icon: any }) => (
|
const StatBar = ({ value, maxValue, label, icon: Icon }:
|
||||||
<div className="flex items-center space-x-2">
|
{ value: number; maxValue: number; label: string; icon: any; }) => (
|
||||||
|
<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>
|
||||||
@ -54,6 +55,14 @@ 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}
|
||||||
@ -70,7 +79,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={Heart}
|
icon={Smile}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -108,6 +117,7 @@ export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
|||||||
{showSkillTreeModal && (
|
{showSkillTreeModal && (
|
||||||
<SkillTreeModal
|
<SkillTreeModal
|
||||||
petId={pet.id}
|
petId={pet.id}
|
||||||
|
petResources={pet.resources}
|
||||||
onPetUpdate={onPetUpdate}
|
onPetUpdate={onPetUpdate}
|
||||||
onClose={() => setShowSkillTreeModal(false)
|
onClose={() => setShowSkillTreeModal(false)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Pet } from '../../types/Pet';
|
import { Pet, Resources } from '../../types/Pet';
|
||||||
import { Skill, PetSkill, SkillType } from '../../types/Skills';
|
import { Skill, PetSkill, SkillType } from '../../types/Skills';
|
||||||
import { fetchPets, getAllSkills, getPetSkills, postAllocatePetSkill } from '../../services/api/api';
|
import { fetchPets, getAllSkills, getPetSkills, postAllocatePetSkill } from '../../services/api/api';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
@ -8,10 +8,11 @@ import styles from './SkillTreeModal.module.css';
|
|||||||
interface SkillTreeModalProps {
|
interface SkillTreeModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
petId: string;
|
petId: string;
|
||||||
|
petResources: Resources;
|
||||||
onPetUpdate: (updatedPet: Pet) => void;
|
onPetUpdate: (updatedPet: Pet) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SkillTreeModal({ onClose, petId, onPetUpdate }: SkillTreeModalProps) {
|
export default function SkillTreeModal({ onClose, petId, petResources, onPetUpdate }: SkillTreeModalProps) {
|
||||||
const [skills, setSkills] = useState<Skill[]>([]);
|
const [skills, setSkills] = useState<Skill[]>([]);
|
||||||
const [petSkills, setPetSkills] = useState<PetSkill[]>([]);
|
const [petSkills, setPetSkills] = useState<PetSkill[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -55,87 +56,110 @@ export default function SkillTreeModal({ onClose, petId, onPetUpdate }: SkillTre
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canAllocateSkill = (skill: Skill) => {
|
const canAllocateSkill = (skill: Skill) => {
|
||||||
if (!skill.skillsIdRequired) return true;
|
// check if all required skills are owned, and has all the required resources
|
||||||
return petSkills.some(ps => ps.skillId === skill.skillsIdRequired![0]);
|
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 getRequiredSkillName = (skillId: number[] | null) => {
|
const getRequiredSkillNames = (skillId: number[] | null) => {
|
||||||
if (!skillId) return null;
|
if (!skillId) return null;
|
||||||
return skills.find(s => s.id === skillId[0])?.name;
|
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 renderSkillNode = (skill: Skill) => {
|
||||||
console.log('Rendering skill:', skill);
|
|
||||||
|
|
||||||
const owned = petSkills.some(ps => ps.skillId === skill.id);
|
const owned = petSkills.some(ps => ps.skillId === skill.id);
|
||||||
const tier = getSkillTier(skill.id);
|
const tier = getSkillTier(skill.id);
|
||||||
const canAllocate = canAllocateSkill(skill);
|
const isMaxTier = tier === 'III';
|
||||||
const requiredSkillName = getRequiredSkillName(skill.skillsIdRequired);
|
const canAllocate = !isMaxTier && canAllocateSkill(skill);
|
||||||
|
const nextTierEffect = getNextTierEffect(tier);
|
||||||
|
const requiredSkillNames = getRequiredSkillNames(skill.skillsIdRequired);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={skill.id}
|
key={skill.id}
|
||||||
className={`
|
className={`
|
||||||
relative p-4 rounded-lg border-2
|
relative p-4 rounded-lg border-2 flex flex-col
|
||||||
${owned ? 'border-green-500 bg-gray-700' : 'border-gray-600 bg-gray-800'}
|
${isMaxTier ? 'border-green-500 bg-gray-700' :
|
||||||
${!owned && !canAllocate ? 'opacity-50' : ''}
|
owned ? 'border-blue-500 bg-gray-700' : 'border-gray-600 bg-gray-800'}
|
||||||
hover:border-blue-500 transition-colors
|
transition-colors
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex-grow">
|
||||||
{/* <img src={skill.icon} alt={skill.name} className="w-6 h-6" /> */}
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span>{skill.icon}</span>
|
<span>{skill.icon}</span>
|
||||||
<h3 className="font-bold">{skill.name}</h3>
|
<h3 className="font-bold">{skill.name}</h3>
|
||||||
{tier && (
|
{tier && (
|
||||||
<span className="px-2 py-1 bg-blue-600 rounded text-xs">
|
<span className={`px-2 py-1 rounded text-xs ${isMaxTier ? 'bg-green-600' : 'bg-blue-600'}`}>
|
||||||
Tier {tier}
|
Tier {tier}
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-300 mb-2">{skill.description}</p>
|
|
||||||
{!canAllocate && requiredSkillName && (
|
|
||||||
<p className="text-red-500 text-sm mb-2">Required skill: {requiredSkillName}</p>
|
|
||||||
)}
|
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
{skill.effects.map((effect, idx) => (
|
|
||||||
<div key={idx}>
|
|
||||||
Tier {effect.tier}: {effect.effect} ({effect.value})
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{!owned && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleAllocateSkill(skill.id)}
|
|
||||||
disabled={!canAllocate || allocating}
|
|
||||||
className={`
|
|
||||||
mt-2 px-3 py-1 rounded text-sm
|
|
||||||
${canAllocate
|
|
||||||
? 'bg-blue-600 hover:bg-blue-700'
|
|
||||||
: 'bg-gray-600 cursor-not-allowed'}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{allocating ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
`Learn (${skill.pointsCost} SP)`
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
)}
|
<p className="text-sm text-gray-300 mb-2">{skill.description}</p>
|
||||||
{owned && (
|
|
||||||
|
<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
|
<button
|
||||||
onClick={() => handleAllocateSkill(skill.id)}
|
onClick={() => handleAllocateSkill(skill.id)}
|
||||||
disabled={!canAllocate || allocating}
|
disabled={!canAllocate || allocating}
|
||||||
className={`
|
className={`
|
||||||
mt-2 px-3 py-1 rounded text-sm
|
mt-auto px-3 py-1 rounded text-sm
|
||||||
${canAllocate
|
${canAllocate
|
||||||
? 'bg-green-600 hover:bg-green-700'
|
? owned
|
||||||
|
? 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
: 'bg-green-600 hover:bg-green-700'
|
||||||
: 'bg-gray-600 cursor-not-allowed'}
|
: 'bg-gray-600 cursor-not-allowed'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{allocating ? (
|
{allocating ? (
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
`Upgrade (${skill.pointsCost} SP)`
|
owned ? 'Upgrade' : 'Learn'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -25,7 +25,6 @@ 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;
|
||||||
|
@ -5,12 +5,17 @@ export interface Skill {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
type: SkillType;
|
type: SkillType;
|
||||||
pointsCost: number;
|
skillRequirements: SkillRequirement[];
|
||||||
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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user