basic character sheet included
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>DSA 5e Charakter Bogen</title>
|
<title>DSA 5e Charakter Bogen</title>
|
||||||
</head>
|
</head>
|
||||||
|
39
src/App.tsx
39
src/App.tsx
@@ -1,35 +1,12 @@
|
|||||||
import { useState } from 'react'
|
import CharacterSheet from './components/CharacterSheet';
|
||||||
import reactLogo from './assets/react.svg'
|
import './App.css';
|
||||||
import viteLogo from '/vite.svg'
|
|
||||||
import './App.css'
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
return (
|
||||||
|
<div className="App">
|
||||||
return (
|
<CharacterSheet />
|
||||||
<>
|
</div>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
19
src/main.tsx
19
src/main.tsx
@@ -1,10 +1,11 @@
|
|||||||
import { StrictMode } from 'react'
|
// src/main.tsx
|
||||||
import { createRoot } from 'react-dom/client'
|
import React from 'react';
|
||||||
import './index.css'
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App.tsx'
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<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