basic character sheet included
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DSA 5e Charakter Bogen</title>
|
||||
</head>
|
||||
|
35
src/App.tsx
35
src/App.tsx
@@ -1,35 +1,12 @@
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
import CharacterSheet from './components/CharacterSheet';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
<div className="App">
|
||||
<CharacterSheet />
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
62
src/components/Attributes.tsx
Normal file
62
src/components/Attributes.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import type {DSACharacter, DSAAttributes} from '../types/character';
|
||||
|
||||
interface AttributesProps {
|
||||
character: DSACharacter;
|
||||
setCharacter: React.Dispatch<React.SetStateAction<DSACharacter>>;
|
||||
}
|
||||
|
||||
const Attributes: React.FC<AttributesProps> = ({ character, setCharacter }) => {
|
||||
const updateAttribute = (attr: keyof DSAAttributes, value: number) => {
|
||||
// Input validation - clamp between 1 and 20
|
||||
const clampedValue = Math.min(20, Math.max(1, value));
|
||||
|
||||
setCharacter(prev => ({
|
||||
...prev,
|
||||
attributes: {
|
||||
...prev.attributes,
|
||||
[attr]: clampedValue
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const attributeDisplayNames: Record<keyof DSAAttributes, string> = {
|
||||
courage: "CO - Courage",
|
||||
cleverness: "CL - Cleverness",
|
||||
intuition: "IN - Intuition",
|
||||
charisma: "CH - Charisma",
|
||||
dexterity: "DE - Dexterity",
|
||||
agility: "AG - Agility",
|
||||
constitution: "CN - Constitution",
|
||||
strength: "ST - Strength"
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="attributes section">
|
||||
<h2>Attributes</h2>
|
||||
<div className="attributes-grid">
|
||||
{Object.entries(character.attributes).map(([key, value]) => (
|
||||
<div key={key} className="attribute">
|
||||
<label htmlFor={key}>
|
||||
{attributeDisplayNames[key as keyof DSAAttributes]}
|
||||
</label>
|
||||
<input
|
||||
id={key}
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => updateAttribute(
|
||||
key as keyof DSAAttributes,
|
||||
parseInt(e.target.value) || 8
|
||||
)}
|
||||
min="1"
|
||||
max="20"
|
||||
className="attribute-input"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Attributes;
|
82
src/components/BasicInfo.tsx
Normal file
82
src/components/BasicInfo.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import type {DSACharacter} from '../types/character';
|
||||
|
||||
interface BasicInfoProps {
|
||||
character: DSACharacter;
|
||||
setCharacter: React.Dispatch<React.SetStateAction<DSACharacter>>;
|
||||
}
|
||||
|
||||
const BasicInfo: React.FC<BasicInfoProps> = ({ character, setCharacter }) => {
|
||||
const updateField = (field: keyof DSACharacter, value: string) => {
|
||||
setCharacter(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="basic-info section">
|
||||
<h2>Basic Information</h2>
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={character.name}
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
placeholder="Enter character name"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="species">Species</label>
|
||||
<input
|
||||
id="species"
|
||||
type="text"
|
||||
value={character.species}
|
||||
onChange={(e) => updateField('species', e.target.value)}
|
||||
placeholder="e.g., Human, Elf, Dwarf"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="culture">Culture</label>
|
||||
<input
|
||||
id="culture"
|
||||
type="text"
|
||||
value={character.culture}
|
||||
onChange={(e) => updateField('culture', e.target.value)}
|
||||
placeholder="e.g., Middenrealmish, Thorwalian"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="profession">Profession</label>
|
||||
<input
|
||||
id="profession"
|
||||
type="text"
|
||||
value={character.profession}
|
||||
onChange={(e) => updateField('profession', e.target.value)}
|
||||
placeholder="e.g., Warrior, Mage, Scout"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="experience-level">Experience Level</label>
|
||||
<select
|
||||
id="experience-level"
|
||||
value={character.experienceLevel}
|
||||
onChange={(e) => updateField('experienceLevel', e.target.value)}
|
||||
>
|
||||
<option value="Inexperienced">Inexperienced</option>
|
||||
<option value="Average">Average</option>
|
||||
<option value="Experienced">Experienced</option>
|
||||
<option value="Competent">Competent</option>
|
||||
<option value="Masterful">Masterful</option>
|
||||
<option value="Brilliant">Brilliant</option>
|
||||
<option value="Legendary">Legendary</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicInfo;
|
492
src/components/CharacterSheet.css
Normal file
492
src/components/CharacterSheet.css
Normal file
@@ -0,0 +1,492 @@
|
||||
/* CSS Variables - TaleSpire Integration with Fallbacks */
|
||||
:root {
|
||||
/* Fallback colors if TaleSpire variables aren't available */
|
||||
--text-color: #222;
|
||||
--background-color: #f5f5f5;
|
||||
--surface-color: #ffffff;
|
||||
--surface-variant-color: #f9f9f9;
|
||||
--primary-color: #7b2cbf;
|
||||
--secondary-color: #3a0ca3;
|
||||
--accent-color: #f72585;
|
||||
--outline-color: #ccc;
|
||||
--error-color: #ff4444;
|
||||
--success-color: #4caf50;
|
||||
--warning-color: #ff9800;
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.character-sheet {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: 'Segoe UI', 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
color: var(--ts-color-on-surface, var(--text-color));
|
||||
background-color: var(--ts-color-surface, var(--surface-color));
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.character-sheet h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: var(--ts-color-primary, var(--primary-color));
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid var(--ts-color-outline, var(--outline-color));
|
||||
background: var(--ts-color-surface, var(--surface-color));
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin: 0 0 20px 0;
|
||||
color: var(--ts-color-primary, var(--primary-color));
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid var(--ts-color-primary, var(--primary-color));
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--ts-color-secondary, var(--secondary-color));
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: var(--ts-color-on-surface, var(--text-color));
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid var(--ts-color-outline, var(--outline-color));
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
background: var(--ts-color-surface, var(--surface-color));
|
||||
color: var(--ts-color-on-surface, var(--text-color));
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--ts-color-primary, var(--primary-color));
|
||||
box-shadow: 0 0 0 3px rgba(123, 44, 191, 0.1);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Attributes Section */
|
||||
.attributes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.attribute {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--ts-color-outline, var(--outline-color));
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.attribute:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.attribute label {
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
color: var(--ts-color-primary, var(--primary-color));
|
||||
}
|
||||
|
||||
.attribute-input {
|
||||
width: 80px !important;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Skills Section */
|
||||
.skills .common-skills {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.skill-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.skill-add-button {
|
||||
padding: 8px 14px;
|
||||
background: var(--ts-color-primary, var(--primary-color));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.skill-add-button:hover:not(:disabled) {
|
||||
background: var(--ts-color-secondary, var(--secondary-color));
|
||||
}
|
||||
|
||||
.skill-add-button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.custom-skill-add {
|
||||
margin-bottom: 25px;
|
||||
padding: 15px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.custom-skill-form {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.custom-skill-form button {
|
||||
background: var(--ts-color-secondary, var(--secondary-color));
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.custom-skill-form button:hover {
|
||||
background: var(--ts-color-primary, var(--primary-color));
|
||||
}
|
||||
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.skill-item {
|
||||
border: 2px solid var(--ts-color-outline, var(--outline-color));
|
||||
border-radius: 10px;
|
||||
padding: 18px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.skill-item:hover {
|
||||
border-color: var(--ts-color-primary, var(--primary-color));
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.skill-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skill-header h4 {
|
||||
margin: 0;
|
||||
color: var(--ts-color-primary, var(--primary-color));
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
background: var(--ts-color-error, var(--error-color));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remove-button:hover {
|
||||
background: #cc0000;
|
||||
}
|
||||
|
||||
.skill-attributes {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.skill-value {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skill-value label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.skill-value input {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.skill-total {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
background: var(--ts-color-primary, var(--primary-color));
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.no-skills {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
font-size: 1.1rem;
|
||||
padding: 30px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Combat Values Section */
|
||||
.combat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.combat-group {
|
||||
padding: 15px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--ts-color-outline, var(--outline-color));
|
||||
}
|
||||
|
||||
.combat-group h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--ts-color-secondary, var(--secondary-color));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.value-pair {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.value-pair label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.combat-simple {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.combat-simple > div {
|
||||
padding: 15px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--ts-color-outline, var(--outline-color));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.combat-simple label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--ts-color-secondary, var(--secondary-color));
|
||||
}
|
||||
|
||||
.combat-simple input {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.character-sheet {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.character-sheet h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.attributes-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.skills-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.combat-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.custom-skill-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.skill-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.attributes-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.value-pair {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.combat-simple {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--ts-color-primary, var(--primary-color));
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--ts-color-secondary, var(--secondary-color));
|
||||
}
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-1 { margin-bottom: 0.5rem; }
|
||||
.mb-2 { margin-bottom: 1rem; }
|
||||
.mb-3 { margin-bottom: 1.5rem; }
|
||||
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-1 { margin-top: 0.5rem; }
|
||||
.mt-2 { margin-top: 1rem; }
|
||||
.mt-3 { margin-top: 1.5rem; }
|
||||
|
||||
/* Header Styles */
|
||||
.character-sheet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
border-radius: 12px;
|
||||
border: 2px solid var(--ts-color-primary, var(--primary-color));
|
||||
}
|
||||
|
||||
.character-sheet-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.character-sheet-header h1 {
|
||||
margin: 0;
|
||||
color: var(--ts-color-primary, var(--primary-color));
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Responsive adjustment */
|
||||
@media (max-width: 768px) {
|
||||
.character-sheet-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.character-sheet-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.character-sheet-header h1 {
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
55
src/components/CharacterSheet.tsx
Normal file
55
src/components/CharacterSheet.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useState } from 'react';
|
||||
import type {DSACharacter} from '../types/character';
|
||||
import BasicInfo from './BasicInfo';
|
||||
import Attributes from './Attributes';
|
||||
import Skills from './Skills';
|
||||
import CombatValues from './CombatValues';
|
||||
import './CharacterSheet.css';
|
||||
|
||||
const initialCharacter: DSACharacter = {
|
||||
id: crypto.randomUUID(),
|
||||
name: '',
|
||||
species: '',
|
||||
culture: '',
|
||||
profession: '',
|
||||
experienceLevel: 'Experienced',
|
||||
attributes: {
|
||||
courage: 8,
|
||||
cleverness: 8,
|
||||
intuition: 8,
|
||||
charisma: 8,
|
||||
dexterity: 8,
|
||||
agility: 8,
|
||||
constitution: 8,
|
||||
strength: 8
|
||||
},
|
||||
skills: {},
|
||||
combat: {
|
||||
lifePoints: { max: 30, current: 30 },
|
||||
stamina: { max: 30, current: 30 },
|
||||
initiative: 10,
|
||||
speed: 8
|
||||
},
|
||||
advantages: [],
|
||||
disadvantages: [],
|
||||
equipment: []
|
||||
};
|
||||
|
||||
const CharacterSheet: React.FC = () => {
|
||||
const [character, setCharacter] = useState<DSACharacter>(initialCharacter);
|
||||
|
||||
return (
|
||||
<div className="character-sheet">
|
||||
<header className="character-sheet-header">
|
||||
<img src="/icon.svg" alt="DSA 5e Logo" className="character-sheet-icon" />
|
||||
<h1>DSA 5 Character Sheet</h1>
|
||||
</header>
|
||||
<BasicInfo character={character} setCharacter={setCharacter} />
|
||||
<Attributes character={character} setCharacter={setCharacter} />
|
||||
<Skills character={character} setCharacter={setCharacter} />
|
||||
<CombatValues character={character} setCharacter={setCharacter} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CharacterSheet;
|
118
src/components/CombatValues.tsx
Normal file
118
src/components/CombatValues.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
import type {DSACharacter} from '../types/character';
|
||||
|
||||
interface CombatValuesProps {
|
||||
character: DSACharacter;
|
||||
setCharacter: React.Dispatch<React.SetStateAction<DSACharacter>>;
|
||||
}
|
||||
|
||||
const CombatValues: React.FC<CombatValuesProps> = ({ character, setCharacter }) => {
|
||||
const updateCombatValue = (category: 'lifePoints' | 'stamina' | 'astralEnergy',
|
||||
field: 'max' | 'current',
|
||||
value: number) => {
|
||||
setCharacter(prev => ({
|
||||
...prev,
|
||||
combat: {
|
||||
...prev.combat,
|
||||
[category]: {
|
||||
...prev.combat[category],
|
||||
[field]: Math.max(0, value)
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const updateSimpleCombatValue = (field: 'initiative' | 'speed', value: number) => {
|
||||
setCharacter(prev => ({
|
||||
...prev,
|
||||
combat: {
|
||||
...prev.combat,
|
||||
[field]: Math.max(0, value)
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="combat-values section">
|
||||
<h2>Combat Values</h2>
|
||||
<div className="combat-grid">
|
||||
<div className="combat-group">
|
||||
<h3>Life Points</h3>
|
||||
<div className="value-pair">
|
||||
<div>
|
||||
<label htmlFor="lp-current">Current</label>
|
||||
<input
|
||||
id="lp-current"
|
||||
type="number"
|
||||
value={character.combat.lifePoints.current}
|
||||
onChange={(e) => updateCombatValue('lifePoints', 'current', parseInt(e.target.value) || 0)}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="lp-max">Maximum</label>
|
||||
<input
|
||||
id="lp-max"
|
||||
type="number"
|
||||
value={character.combat.lifePoints.max}
|
||||
onChange={(e) => updateCombatValue('lifePoints', 'max', parseInt(e.target.value) || 0)}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="combat-group">
|
||||
<h3>Stamina</h3>
|
||||
<div className="value-pair">
|
||||
<div>
|
||||
<label htmlFor="stamina-current">Current</label>
|
||||
<input
|
||||
id="stamina-current"
|
||||
type="number"
|
||||
value={character.combat.stamina.current}
|
||||
onChange={(e) => updateCombatValue('stamina', 'current', parseInt(e.target.value) || 0)}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="stamina-max">Maximum</label>
|
||||
<input
|
||||
id="stamina-max"
|
||||
type="number"
|
||||
value={character.combat.stamina.max}
|
||||
onChange={(e) => updateCombatValue('stamina', 'max', parseInt(e.target.value) || 0)}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="combat-simple">
|
||||
<div>
|
||||
<label htmlFor="initiative">Initiative</label>
|
||||
<input
|
||||
id="initiative"
|
||||
type="number"
|
||||
value={character.combat.initiative}
|
||||
onChange={(e) => updateSimpleCombatValue('initiative', parseInt(e.target.value) || 0)}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="speed">Speed</label>
|
||||
<input
|
||||
id="speed"
|
||||
type="number"
|
||||
value={character.combat.speed}
|
||||
onChange={(e) => updateSimpleCombatValue('speed', parseInt(e.target.value) || 0)}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CombatValues;
|
181
src/components/Skills.tsx
Normal file
181
src/components/Skills.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { DSACharacter, DSASkill, DSAAttributes } from '../types/character';
|
||||
|
||||
interface SkillsProps {
|
||||
character: DSACharacter;
|
||||
setCharacter: React.Dispatch<React.SetStateAction<DSACharacter>>;
|
||||
}
|
||||
|
||||
const Skills: React.FC<SkillsProps> = ({ character, setCharacter }) => {
|
||||
const [newSkillName, setNewSkillName] = useState('');
|
||||
const [selectedAttributes, setSelectedAttributes] = useState<[keyof DSAAttributes, keyof DSAAttributes, keyof DSAAttributes]>(['courage', 'cleverness', 'intuition']);
|
||||
|
||||
// Common DSA 5e skills with their attribute combinations
|
||||
const commonSkills: Record<string, [keyof DSAAttributes, keyof DSAAttributes, keyof DSAAttributes]> = {
|
||||
'Athletics': ['agility', 'constitution', 'strength'],
|
||||
'Stealth': ['courage', 'intuition', 'agility'],
|
||||
'Perception': ['cleverness', 'intuition', 'intuition'],
|
||||
'Persuasion': ['cleverness', 'intuition', 'charisma'],
|
||||
'Intimidation': ['courage', 'intuition', 'charisma'],
|
||||
'Survival': ['courage', 'cleverness', 'constitution'],
|
||||
'Weapons Training': ['courage', 'agility', 'strength'],
|
||||
'Spell Casting': ['cleverness', 'intuition', 'charisma'],
|
||||
};
|
||||
|
||||
const addSkill = (skillName: string, attributes: [keyof DSAAttributes, keyof DSAAttributes, keyof DSAAttributes]) => {
|
||||
const newSkill: DSASkill = {
|
||||
name: skillName,
|
||||
attributes: attributes,
|
||||
value: 0
|
||||
};
|
||||
|
||||
setCharacter(prev => ({
|
||||
...prev,
|
||||
skills: {
|
||||
...prev.skills,
|
||||
[skillName]: newSkill
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const updateSkillValue = (skillName: string, value: number) => {
|
||||
const clampedValue = Math.max(0, Math.min(20, value));
|
||||
|
||||
setCharacter(prev => ({
|
||||
...prev,
|
||||
skills: {
|
||||
...prev.skills,
|
||||
[skillName]: {
|
||||
...prev.skills[skillName],
|
||||
value: clampedValue
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const removeSkill = (skillName: string) => {
|
||||
setCharacter(prev => {
|
||||
const { [skillName]: removed, ...remainingSkills } = prev.skills;
|
||||
return {
|
||||
...prev,
|
||||
skills: remainingSkills
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const addCustomSkill = () => {
|
||||
if (newSkillName.trim() && !character.skills[newSkillName]) {
|
||||
addSkill(newSkillName.trim(), selectedAttributes);
|
||||
setNewSkillName('');
|
||||
}
|
||||
};
|
||||
|
||||
const calculateSkillCheck = (skill: DSASkill): number => {
|
||||
const [attr1, attr2, attr3] = skill.attributes;
|
||||
return character.attributes[attr1] + character.attributes[attr2] + character.attributes[attr3] + skill.value;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="skills section">
|
||||
<h2>Skills & Talents</h2>
|
||||
|
||||
{/* Quick Add Common Skills */}
|
||||
<div className="common-skills">
|
||||
<h3>Add Common Skills</h3>
|
||||
<div className="skill-buttons">
|
||||
{Object.entries(commonSkills).map(([skillName, attributes]) => (
|
||||
<button
|
||||
key={skillName}
|
||||
onClick={() => addSkill(skillName, attributes)}
|
||||
disabled={!!character.skills[skillName]}
|
||||
className="skill-add-button"
|
||||
>
|
||||
{skillName}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Skill Addition */}
|
||||
<div className="custom-skill-add">
|
||||
<h3>Add Custom Skill</h3>
|
||||
<div className="custom-skill-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Skill name"
|
||||
value={newSkillName}
|
||||
onChange={(e) => setNewSkillName(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
value={selectedAttributes[0]}
|
||||
onChange={(e) => setSelectedAttributes([e.target.value as keyof DSAAttributes, selectedAttributes[1], selectedAttributes[2]])}
|
||||
>
|
||||
{Object.keys(character.attributes).map(attr => (
|
||||
<option key={attr} value={attr}>{attr.toUpperCase()}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={selectedAttributes[1]}
|
||||
onChange={(e) => setSelectedAttributes([selectedAttributes[0], e.target.value as keyof DSAAttributes, selectedAttributes[2]])}
|
||||
>
|
||||
{Object.keys(character.attributes).map(attr => (
|
||||
<option key={attr} value={attr}>{attr.toUpperCase()}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={selectedAttributes[2]}
|
||||
onChange={(e) => setSelectedAttributes([selectedAttributes[0], selectedAttributes[1], e.target.value as keyof DSAAttributes])}
|
||||
>
|
||||
{Object.keys(character.attributes).map(attr => (
|
||||
<option key={attr} value={attr}>{attr.toUpperCase()}</option>
|
||||
))}
|
||||
</select>
|
||||
<button onClick={addCustomSkill}>Add Skill</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Skills List */}
|
||||
<div className="skills-list">
|
||||
<h3>Current Skills</h3>
|
||||
{Object.keys(character.skills).length === 0 ? (
|
||||
<p className="no-skills">No skills added yet. Add some skills above!</p>
|
||||
) : (
|
||||
<div className="skills-grid">
|
||||
{Object.entries(character.skills).map(([skillName, skill]) => (
|
||||
<div key={skillName} className="skill-item">
|
||||
<div className="skill-header">
|
||||
<h4>{skill.name}</h4>
|
||||
<button
|
||||
onClick={() => removeSkill(skillName)}
|
||||
className="remove-button"
|
||||
title="Remove skill"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="skill-attributes">
|
||||
<span>{skill.attributes.map(attr => attr.substring(0, 2).toUpperCase()).join('/')}</span>
|
||||
</div>
|
||||
<div className="skill-value">
|
||||
<label>Skill Value:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={skill.value}
|
||||
onChange={(e) => updateSkillValue(skillName, parseInt(e.target.value) || 0)}
|
||||
min="0"
|
||||
max="20"
|
||||
/>
|
||||
</div>
|
||||
<div className="skill-total">
|
||||
<strong>Total: {calculateSkillCheck(skill)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Skills;
|
17
src/main.tsx
17
src/main.tsx
@@ -1,10 +1,11 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
// src/main.tsx
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
15
src/styles/talespire-variables.css
Normal file
15
src/styles/talespire-variables.css
Normal file
@@ -0,0 +1,15 @@
|
||||
/* src/styles/talespire-variables.css */
|
||||
:root {
|
||||
/* TaleSpire Color Variables */
|
||||
--ts-color-surface: #ffffff;
|
||||
--ts-color-on-surface: #000000;
|
||||
--ts-color-surface-variant: #f9f9f9;
|
||||
--ts-color-primary: #7b2cbf;
|
||||
--ts-color-secondary: #3a0ca3;
|
||||
--ts-color-outline: #cccccc;
|
||||
--ts-color-error: #ff4444;
|
||||
|
||||
/* Add other TaleSpire variables as you discover them */
|
||||
--ts-color-background: #f5f5f5;
|
||||
--ts-color-on-background: #222222;
|
||||
}
|
48
src/types/character.ts
Normal file
48
src/types/character.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export interface DSAAttributes {
|
||||
courage: number; // Mut
|
||||
cleverness: number; // Klugheit
|
||||
intuition: number; // Intuition
|
||||
charisma: number; // Charisma
|
||||
dexterity: number; // Fingerfertigkeit
|
||||
agility: number; // Gewandtheit
|
||||
constitution: number; // Konstitution
|
||||
strength: number; // Körperkraft
|
||||
}
|
||||
|
||||
export interface DSASkill {
|
||||
name: string;
|
||||
attributes: [keyof DSAAttributes, keyof DSAAttributes, keyof DSAAttributes];
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface DSACombatValues {
|
||||
lifePoints: {
|
||||
max: number;
|
||||
current: number;
|
||||
};
|
||||
stamina: {
|
||||
max: number;
|
||||
current: number;
|
||||
};
|
||||
astralEnergy?: {
|
||||
max: number;
|
||||
current: number;
|
||||
};
|
||||
initiative: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
export interface DSACharacter {
|
||||
id: string;
|
||||
name: string;
|
||||
species: string; // Spezies
|
||||
culture: string; // Kultur
|
||||
profession: string; // Profession
|
||||
experienceLevel: string; // Erfahrungsgrad
|
||||
attributes: DSAAttributes;
|
||||
skills: Record<string, DSASkill>;
|
||||
combat: DSACombatValues;
|
||||
advantages: string[]; // Vorteile
|
||||
disadvantages: string[]; // Nachteile
|
||||
equipment: string[]; // Ausrüstung
|
||||
}
|
Reference in New Issue
Block a user