Spaces:
Paused
Paused
| "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> | |
| ); | |
| }; |