mmy / components /editor-sections /StatsSection.tsx
Mohammad Shahid
first commit
3a7a84c
"use client";
import { useMemo, useEffect } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { z } from 'zod';
import { heroFormSchema } from '@/lib/validators';
import { Badge } from '@/components/ui/badge';
import { FormControl, FormField, FormItem, FormLabel, FormDescription } from '@/components/ui/form';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ATTACK_TYPE_COST, COOLDOWN_TIERS, STAT_TIERS, TOTAL_POINTS } from '@/constants';
type FormSchemaType = z.infer<typeof heroFormSchema>;
interface StatsSectionProps {
form: UseFormReturn<FormSchemaType>;
}
// Helper to find the cost (tier index) from a stat value
const getStatCost = (stat: keyof typeof STAT_TIERS, value: number) => STAT_TIERS[stat].indexOf(value);
export const StatsSection = ({ form }: StatsSectionProps) => {
const watchedValues = form.watch();
const pointsSpent = useMemo(() => {
let total = 0;
const { heroHealth, heroSpeed, basicAttack } = watchedValues;
// 1. Health, Speed, and Attack points
total += getStatCost('Health', heroHealth);
total += getStatCost('Speed', heroSpeed);
let attackCost = getStatCost('Attack', basicAttack.damage);
// 2. The 'Contact Tax'
if (basicAttack.type === 'CONTACT') {
attackCost += 1;
}
total += attackCost;
// 3. Attack Type points
total += ATTACK_TYPE_COST[basicAttack.type] || 0;
// 4. Cooldown points
if (basicAttack.type === 'MELEE') {
total += COOLDOWN_TIERS.MELEE.find(t => t.value === basicAttack.cooldown)?.cost || 0;
} else if (basicAttack.type === 'RANGED') {
total += COOLDOWN_TIERS.RANGED.find(t => t.value === basicAttack.cooldown)?.cost || 0;
}
return total;
}, [watchedValues]);
// Effect to set a global form error if points are not exactly 12
useEffect(() => {
if (pointsSpent !== TOTAL_POINTS) {
form.setError('root.stats', {
type: 'manual',
message: `You must spend exactly ${TOTAL_POINTS} points. Currently spending ${pointsSpent}.`
});
} else {
form.clearErrors('root.stats');
}
}, [pointsSpent, form]);
const handleAttackTypeChange = (type: 'CONTACT' | 'MELEE' | 'RANGED') => {
form.setValue('basicAttack.type', type, { shouldValidate: true });
// When changing type, reset cooldown to the cheapest valid option
if (type === 'MELEE') {
form.setValue('basicAttack.cooldown', COOLDOWN_TIERS.MELEE[0].value, { shouldValidate: true });
} else if (type === 'RANGED') {
form.setValue('basicAttack.cooldown', COOLDOWN_TIERS.RANGED[0].value, { shouldValidate: true });
} else { // CONTACT
form.setValue('basicAttack.cooldown', 0, { shouldValidate: true });
}
}
return (
<div className="p-4 border rounded-lg">
<div className="flex justify-between items-center mb-2">
<h3 className="text-lg font-semibold">Stats & Abilities</h3>
<Badge variant={pointsSpent === TOTAL_POINTS ? 'default' : 'destructive'}>
Points Spent: {pointsSpent} / {TOTAL_POINTS}
</Badge>
</div>
{form.formState.errors.root?.stats && (
<p className="text-sm text-destructive mb-4">{form.formState.errors.root.stats.message}</p>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Tier Selectors for core stats */}
<FormField control={form.control} name="heroHealth" render={({ field }) => (
<FormItem>
<FormLabel>Health ({field.value} HP)</FormLabel>
<Select onValueChange={(val) => field.onChange(parseInt(val))} value={String(field.value)}>
<FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>
<SelectContent>
{STAT_TIERS.Health.map((val, i) => <SelectItem key={val} value={String(val)}>Tier {i} ({i} pts)</SelectItem>)}
</SelectContent>
</Select>
</FormItem>
)}/>
<FormField control={form.control} name="heroSpeed" render={({ field }) => (
<FormItem>
<FormLabel>Speed ({field.value}x)</FormLabel>
<Select onValueChange={(val) => field.onChange(parseFloat(val))} value={String(field.value)}>
<FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>
<SelectContent>
{STAT_TIERS.Speed.map((val, i) => <SelectItem key={val} value={String(val)}>Tier {i} ({i} pts)</SelectItem>)}
</SelectContent>
</Select>
</FormItem>
)}/>
<FormField control={form.control} name="basicAttack.damage" render={({ field }) => (
<FormItem>
<FormLabel>Attack ({field.value} Dmg)</FormLabel>
<Select onValueChange={(val) => field.onChange(parseInt(val))} value={String(field.value)}>
<FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>
<SelectContent>
{STAT_TIERS.Attack.map((val, i) => {
const cost = watchedValues.basicAttack.type === 'CONTACT' ? i + 1 : i;
return <SelectItem key={val} value={String(val)}>Tier {i} ({cost} pts)</SelectItem>
})}
</SelectContent>
</Select>
{watchedValues.basicAttack.type === 'CONTACT' && <FormDescription>+1 pt cost for Contact type.</FormDescription>}
</FormItem>
)}/>
{/* Attack Type & Cooldown */}
<FormField control={form.control} name="basicAttack.type" render={({ field }) => (
<FormItem>
<FormLabel>Attack Type</FormLabel>
<Select onValueChange={handleAttackTypeChange} value={field.value}>
<FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>
<SelectContent>
<SelectItem value="CONTACT">Contact ({ATTACK_TYPE_COST.CONTACT} pt)</SelectItem>
<SelectItem value="MELEE">Melee ({ATTACK_TYPE_COST.MELEE} pts)</SelectItem>
<SelectItem value="RANGED">Ranged ({ATTACK_TYPE_COST.RANGED} pts)</SelectItem>
</SelectContent>
</Select>
</FormItem>
)}/>
{watchedValues.basicAttack.type === 'MELEE' && (
<>
<FormField control={form.control} name="basicAttack.cooldown" render={({ field }) => (
<FormItem>
<FormLabel>Melee Cooldown</FormLabel>
<Select onValueChange={(val) => field.onChange(parseFloat(val))} value={String(field.value)}>
<FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>
<SelectContent>
{COOLDOWN_TIERS.MELEE.map(t => <SelectItem key={t.value} value={String(t.value)}>{t.value}s ({t.cost} pts)</SelectItem>)}
</SelectContent>
</Select>
</FormItem>
)}/>
<FormField control={form.control} name="basicAttack.range" render={({ field }) => (
<FormItem>
<FormLabel>Range (tiles)</FormLabel>
<FormControl>
<input
type="number"
min={1}
max={1000}
step={1}
className="input border p-2 input-bordered w-full"
value={field.value}
onChange={e => field.onChange(Number(e.target.value))}
/>
</FormControl>
</FormItem>
)}/>
</>
)}
{watchedValues.basicAttack.type === 'RANGED' && (
<FormField control={form.control} name="basicAttack.cooldown" render={({ field }) => (
<FormItem>
<FormLabel>Ranged Cooldown</FormLabel>
<Select onValueChange={(val) => field.onChange(parseFloat(val))} value={String(field.value)}>
<FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>
<SelectContent>
{COOLDOWN_TIERS.RANGED.map(t => <SelectItem key={t.value} value={String(t.value)}>{t.value}s ({t.cost} pts)</SelectItem>)}
</SelectContent>
</Select>
</FormItem>
)}/>
)}
</div>
</div>
);
};