basic character sheet included

This commit is contained in:
2025-06-15 16:23:28 +02:00
parent 8c19a897b8
commit b0bd4282c6
11 changed files with 1072 additions and 41 deletions

View File

@@ -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>

View File

@@ -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>
<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>
</>
)
return (
<div className="App">
<CharacterSheet />
</div>
);
}
export default App
export default App;

View 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;

View 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;

View 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;
}
}

View 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;

View 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
View 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;

View File

@@ -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>
<App />
</StrictMode>,
)
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View 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
View 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
}