Spaces:
Paused
Paused
File size: 8,559 Bytes
3a7a84c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | "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>
);
}; |