Commit
This commit is contained in:
265
pwa/src/components/CharacterCreation.tsx
Normal file
265
pwa/src/components/CharacterCreation.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../hooks/useAuth'
|
||||
import './CharacterCreation.css'
|
||||
|
||||
function CharacterCreation() {
|
||||
const { createCharacter } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [strength, setStrength] = useState(0)
|
||||
const [agility, setAgility] = useState(0)
|
||||
const [endurance, setEndurance] = useState(0)
|
||||
const [intellect, setIntellect] = useState(0)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const TOTAL_POINTS = 20
|
||||
const usedPoints = strength + agility + endurance + intellect
|
||||
const remainingPoints = TOTAL_POINTS - usedPoints
|
||||
|
||||
const calculateHP = (str: number) => 30 + (str * 2)
|
||||
const calculateStamina = (end: number) => 20 + (end * 1)
|
||||
|
||||
const handleStatChange = (
|
||||
stat: 'strength' | 'agility' | 'endurance' | 'intellect',
|
||||
value: number
|
||||
) => {
|
||||
// Prevent negative values
|
||||
if (value < 0) return
|
||||
|
||||
const currentTotal = strength + agility + endurance + intellect
|
||||
const otherStats = currentTotal - (stat === 'strength' ? strength : stat === 'agility' ? agility : stat === 'endurance' ? endurance : intellect)
|
||||
|
||||
// Prevent exceeding total points
|
||||
if (otherStats + value > TOTAL_POINTS) {
|
||||
value = TOTAL_POINTS - otherStats
|
||||
}
|
||||
|
||||
switch (stat) {
|
||||
case 'strength':
|
||||
setStrength(value)
|
||||
break
|
||||
case 'agility':
|
||||
setAgility(value)
|
||||
break
|
||||
case 'endurance':
|
||||
setEndurance(value)
|
||||
break
|
||||
case 'intellect':
|
||||
setIntellect(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
// Validation
|
||||
if (name.length < 3 || name.length > 20) {
|
||||
setError('Name must be between 3 and 20 characters')
|
||||
return
|
||||
}
|
||||
|
||||
if (usedPoints !== TOTAL_POINTS) {
|
||||
setError(`You must allocate exactly ${TOTAL_POINTS} stat points (currently: ${usedPoints})`)
|
||||
return
|
||||
}
|
||||
|
||||
if (strength < 0 || agility < 0 || endurance < 0 || intellect < 0) {
|
||||
setError('Stats cannot be negative')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
await createCharacter({
|
||||
name,
|
||||
strength,
|
||||
agility,
|
||||
endurance,
|
||||
intellect,
|
||||
})
|
||||
navigate('/characters')
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to create character')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
navigate('/characters')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="character-creation-container">
|
||||
<div className="character-creation-card">
|
||||
<h1>Create Your Character</h1>
|
||||
<p className="subtitle">Choose your name and distribute your stat points</p>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Name Input */}
|
||||
<div className="form-section">
|
||||
<label htmlFor="name">Character Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter character name"
|
||||
minLength={3}
|
||||
maxLength={20}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
<p className="input-hint">3-20 characters, must be unique</p>
|
||||
</div>
|
||||
|
||||
{/* Stat Allocation */}
|
||||
<div className="form-section">
|
||||
<h2>Stat Allocation</h2>
|
||||
<div className="points-remaining">
|
||||
<span className={remainingPoints === 0 ? 'points-complete' : remainingPoints < 0 ? 'points-over' : ''}>
|
||||
Points Remaining: {remainingPoints} / {TOTAL_POINTS}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<StatInput
|
||||
label="Strength"
|
||||
icon="💪"
|
||||
value={strength}
|
||||
onChange={(v) => handleStatChange('strength', v)}
|
||||
description="Increases melee damage and carry capacity"
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<StatInput
|
||||
label="Agility"
|
||||
icon="⚡"
|
||||
value={agility}
|
||||
onChange={(v) => handleStatChange('agility', v)}
|
||||
description="Improves dodge chance and critical hits"
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<StatInput
|
||||
label="Endurance"
|
||||
icon="🛡️"
|
||||
value={endurance}
|
||||
onChange={(v) => handleStatChange('endurance', v)}
|
||||
description="Increases HP and stamina"
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<StatInput
|
||||
label="Intellect"
|
||||
icon="🧠"
|
||||
value={intellect}
|
||||
onChange={(v) => handleStatChange('intellect', v)}
|
||||
description="Enhances crafting and resource gathering"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Character Preview */}
|
||||
<div className="form-section character-preview">
|
||||
<h2>Character Preview</h2>
|
||||
<div className="preview-stats">
|
||||
<div className="preview-stat">
|
||||
<span className="preview-label">HP:</span>
|
||||
<span className="preview-value">{calculateHP(strength)}</span>
|
||||
</div>
|
||||
<div className="preview-stat">
|
||||
<span className="preview-label">Stamina:</span>
|
||||
<span className="preview-value">{calculateStamina(endurance)}</span>
|
||||
</div>
|
||||
<div className="preview-stat">
|
||||
<span className="preview-label">Level:</span>
|
||||
<span className="preview-value">1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="button-secondary"
|
||||
onClick={handleCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="button-primary"
|
||||
disabled={loading || remainingPoints !== 0}
|
||||
>
|
||||
{loading ? 'Creating...' : 'Create Character'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatInput({
|
||||
label,
|
||||
icon,
|
||||
value,
|
||||
onChange,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
label: string
|
||||
icon: string
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
description: string
|
||||
disabled: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="stat-input">
|
||||
<div className="stat-header">
|
||||
<span className="stat-icon">{icon}</span>
|
||||
<label>{label}</label>
|
||||
</div>
|
||||
<div className="stat-control">
|
||||
<button
|
||||
type="button"
|
||||
className="stat-button"
|
||||
onClick={() => onChange(value - 1)}
|
||||
disabled={disabled || value <= 0}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => onChange(parseInt(e.target.value) || 0)}
|
||||
min="0"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="stat-button"
|
||||
onClick={() => onChange(value + 1)}
|
||||
disabled={disabled}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<p className="stat-description">{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CharacterCreation
|
||||
Reference in New Issue
Block a user