dice roller added
This commit is contained in:
@@ -546,3 +546,430 @@ body {
|
||||
background: var(--ts-background-primary, var(--background-color));
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Spells & Astralenergie Section */
|
||||
.spells .astral-energy {
|
||||
margin-bottom: 25px;
|
||||
padding: 15px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--ts-color-primary, var(--primary-color));
|
||||
}
|
||||
|
||||
.spells .astral-energy h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--ts-color-primary, var(--primary-color));
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Häufige Zauber Buttons */
|
||||
.spells .common-spells {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.spell-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.spell-add-button {
|
||||
padding: 10px 16px;
|
||||
background: var(--ts-color-secondary, var(--secondary-color));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.spell-add-button:hover:not(:disabled) {
|
||||
background: var(--ts-color-primary, var(--primary-color));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.spell-add-button:disabled {
|
||||
background: #666;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Zauberliste Grid */
|
||||
.spells-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.spell-item {
|
||||
border: 2px solid var(--ts-color-outline, var(--outline-color));
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spell-item:hover {
|
||||
border-color: var(--ts-color-secondary, var(--secondary-color));
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.spell-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.spell-header h4 {
|
||||
margin: 0;
|
||||
color: var(--ts-color-primary, var(--primary-color));
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Zauber-Info Sektion */
|
||||
.spell-info {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.spell-attributes {
|
||||
font-size: 0.85rem;
|
||||
color: var(--ts-color-secondary, var(--secondary-color));
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.spell-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--ts-color-on-surface, var(--text-color));
|
||||
}
|
||||
|
||||
.spell-details div {
|
||||
padding: 4px 8px;
|
||||
background: var(--ts-color-surface, var(--surface-color));
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--ts-color-outline, var(--outline-color));
|
||||
}
|
||||
|
||||
/* Fertigkeitswert Input */
|
||||
.spell-value {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.spell-value label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--ts-color-on-surface, var(--text-color));
|
||||
}
|
||||
|
||||
.spell-value input {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
border: 2px solid var(--ts-color-outline, var(--outline-color));
|
||||
}
|
||||
|
||||
.spell-value input:focus {
|
||||
border-color: var(--ts-color-primary, var(--primary-color));
|
||||
}
|
||||
|
||||
/* Gesamtwert Anzeige */
|
||||
.spell-total {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: linear-gradient(135deg,
|
||||
var(--ts-color-primary, var(--primary-color)),
|
||||
var(--ts-color-secondary, var(--secondary-color))
|
||||
);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Zauber-Beschreibung */
|
||||
.spell-description {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
font-style: italic;
|
||||
padding: 8px;
|
||||
background: var(--ts-color-surface, var(--surface-color));
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--ts-color-secondary, var(--secondary-color));
|
||||
}
|
||||
|
||||
/* "Keine Zauber" Nachricht */
|
||||
.no-spells {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
font-size: 1.1rem;
|
||||
padding: 40px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
border-radius: 12px;
|
||||
border: 2px dashed var(--ts-color-outline, var(--outline-color));
|
||||
}
|
||||
|
||||
/* Magic Button für Nicht-Zauberer */
|
||||
.add-magic-section {
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.add-magic-button {
|
||||
padding: 15px 30px;
|
||||
background: linear-gradient(135deg, #9d4edd, #7209b7);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(157, 78, 221, 0.3);
|
||||
}
|
||||
|
||||
.add-magic-button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(157, 78, 221, 0.4);
|
||||
background: linear-gradient(135deg, #7209b7, #560bad);
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.spells-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.spell-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.spell-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.spell-item {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.spell-header h4 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* DSA Dice Roller Styles */
|
||||
.dsa-dice-roller {
|
||||
border: 2px solid var(--ts-color-primary, var(--primary-color));
|
||||
background: linear-gradient(135deg,
|
||||
var(--ts-color-surface, var(--surface-color)),
|
||||
var(--ts-color-surface-variant, var(--surface-variant-color))
|
||||
);
|
||||
}
|
||||
|
||||
.dice-warning {
|
||||
background: var(--ts-color-warning, var(--warning-color));
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Modifikator Sektion */
|
||||
.modifier-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 25px;
|
||||
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));
|
||||
}
|
||||
|
||||
.modifier-input {
|
||||
width: 80px !important;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Würfelergebnis Anzeige */
|
||||
.last-result {
|
||||
margin-bottom: 25px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid;
|
||||
background: var(--ts-color-surface, var(--surface-color));
|
||||
animation: resultFadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.last-result.success {
|
||||
border-color: var(--ts-color-success, var(--success-color));
|
||||
box-shadow: 0 0 20px rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
.last-result.failure {
|
||||
border-color: var(--ts-color-error, var(--error-color));
|
||||
box-shadow: 0 0 20px rgba(255, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.last-result h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--ts-color-primary, var(--primary-color));
|
||||
}
|
||||
|
||||
.result-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Würfel-Sektionen */
|
||||
.dice-section {
|
||||
margin-bottom: 25px;
|
||||
padding: 20px;
|
||||
background: var(--ts-color-surface-variant, var(--surface-variant-color));
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--ts-color-outline, var(--outline-color));
|
||||
}
|
||||
|
||||
.dice-section h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--ts-color-secondary, var(--secondary-color));
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.dice-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Würfel-Buttons */
|
||||
.dice-button {
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dice-button::before {
|
||||
content: '🎲';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.dice-button {
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
/* Button-Varianten */
|
||||
.attribute-button {
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.attribute-button:hover {
|
||||
background: linear-gradient(135deg, #4f46e5, #3730a3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.skill-button {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.skill-button:hover {
|
||||
background: linear-gradient(135deg, #059669, #047857);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.spell-button {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.spell-button:hover {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.combat-button {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.combat-button:hover {
|
||||
background: linear-gradient(135deg, #dc2626, #b91c1c);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
/* Animationen */
|
||||
@keyframes resultFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dice-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.dice-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modifier-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import Skills from './Skills';
|
||||
import Spells from './Spells';
|
||||
import CombatValues from './CombatValues';
|
||||
import ThemeToggle from './ThemeToggle';
|
||||
import DSADiceRoller from "./DSADiceRoller.tsx";
|
||||
import iconUrl from '/icon.png';
|
||||
import './CharacterSheet.css';
|
||||
|
||||
@@ -77,6 +78,9 @@ const CharacterSheet: React.FC = () => {
|
||||
{/* Fertigkeiten */}
|
||||
<Skills character={character} setCharacter={setCharacter} />
|
||||
|
||||
{/* Würfel-Integration - kommt nach Skills */}
|
||||
<DSADiceRoller character={character} />
|
||||
|
||||
{/* Zauber - nur anzeigen wenn Astralenergie > 0 oder explizit gewünscht */}
|
||||
{(isSpellcaster || Object.keys(character.spells).length > 0) && (
|
||||
<Spells character={character} setCharacter={setCharacter} />
|
||||
|
199
src/components/DSADiceRoller.tsx
Normal file
199
src/components/DSADiceRoller.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { DSACharacter, DSASkill, DSASpell } from '../types/character';
|
||||
import { useTaleSpireDice } from '../hooks/useTaleSpireDice';
|
||||
|
||||
interface DSADiceRollerProps {
|
||||
character: DSACharacter;
|
||||
}
|
||||
|
||||
const DSADiceRoller: React.FC<DSADiceRollerProps> = ({ character }) => {
|
||||
const { rollDSACheck, rollSimpleDice, isAvailable } = useTaleSpireDice();
|
||||
const [modifier, setModifier] = useState(0);
|
||||
const [lastResult, setLastResult] = useState<any>(null);
|
||||
|
||||
const rollAttributeCheck = async (attributeKey: keyof typeof character.attributes) => {
|
||||
const attributeValue = character.attributes[attributeKey];
|
||||
try {
|
||||
const result = await rollDSACheck(
|
||||
[attributeValue, attributeValue, attributeValue],
|
||||
0,
|
||||
modifier,
|
||||
`${attributeKey.toUpperCase()} Probe`
|
||||
);
|
||||
setLastResult(result);
|
||||
} catch (error) {
|
||||
console.error('Dice roll failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const rollSkillCheck = async (skill: DSASkill) => {
|
||||
const [attr1, attr2, attr3] = skill.attributes;
|
||||
const attributes: [number, number, number] = [
|
||||
character.attributes[attr1],
|
||||
character.attributes[attr2],
|
||||
character.attributes[attr3]
|
||||
];
|
||||
|
||||
try {
|
||||
const result = await rollDSACheck(
|
||||
attributes,
|
||||
skill.value,
|
||||
modifier,
|
||||
`${skill.name} Probe`
|
||||
);
|
||||
setLastResult(result);
|
||||
} catch (error) {
|
||||
console.error('Skill roll failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const rollSpellCheck = async (spell: DSASpell) => {
|
||||
const [attr1, attr2, attr3] = spell.attributes;
|
||||
const attributes: [number, number, number] = [
|
||||
character.attributes[attr1],
|
||||
character.attributes[attr2],
|
||||
character.attributes[attr3]
|
||||
];
|
||||
|
||||
try {
|
||||
const result = await rollDSACheck(
|
||||
attributes,
|
||||
spell.skillValue,
|
||||
modifier + spell.difficulty,
|
||||
`${spell.name} Zauberprobe`
|
||||
);
|
||||
setLastResult(result);
|
||||
} catch (error) {
|
||||
console.error('Spell roll failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const rollInitiative = async () => {
|
||||
try {
|
||||
const result = await rollSimpleDice('1d6', 'Initiative');
|
||||
const total = result.total + character.combat.initiative;
|
||||
setLastResult({ ...result, total, type: 'initiative' });
|
||||
} catch (error) {
|
||||
console.error('Initiative roll failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dsa-dice-roller section">
|
||||
<h2>🎲 Würfelproben</h2>
|
||||
|
||||
{!isAvailable && (
|
||||
<div className="dice-warning">
|
||||
⚠️ TaleSpire Dice API nicht verfügbar - Development Mode aktiv
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modifikator */}
|
||||
<div className="modifier-section">
|
||||
<label htmlFor="modifier">Modifikator:</label>
|
||||
<input
|
||||
id="modifier"
|
||||
type="number"
|
||||
value={modifier}
|
||||
onChange={(e) => setModifier(parseInt(e.target.value) || 0)}
|
||||
min="-10"
|
||||
max="10"
|
||||
className="modifier-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Letztes Ergebnis */}
|
||||
{lastResult && (
|
||||
<div className={`last-result ${lastResult.success ? 'success' : 'failure'}`}>
|
||||
<h3>Letztes Ergebnis:</h3>
|
||||
<div className="result-details">
|
||||
<div>Würfel: {lastResult.dice?.join(', ')}</div>
|
||||
{lastResult.type === 'initiative' ? (
|
||||
<div>Initiative: {lastResult.total}</div>
|
||||
) : (
|
||||
<>
|
||||
<div>{lastResult.success ? '✅ Erfolg' : '❌ Fehlschlag'}</div>
|
||||
{lastResult.success && (
|
||||
<div>Qualitätsstufe: {lastResult.qualityLevel}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Eigenschaften */}
|
||||
<div className="dice-section">
|
||||
<h3>Eigenschaftsproben</h3>
|
||||
<div className="dice-buttons">
|
||||
{Object.entries(character.attributes).map(([key, value]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => rollAttributeCheck(key as keyof typeof character.attributes)}
|
||||
className="dice-button attribute-button"
|
||||
>
|
||||
{key.substring(0, 2).toUpperCase()} ({value})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fertigkeiten */}
|
||||
{Object.keys(character.skills).length > 0 && (
|
||||
<div className="dice-section">
|
||||
<h3>Fertigkeitsproben</h3>
|
||||
<div className="dice-buttons">
|
||||
{Object.values(character.skills).slice(0, 6).map((skill) => (
|
||||
<button
|
||||
key={skill.name}
|
||||
onClick={() => rollSkillCheck(skill)}
|
||||
className="dice-button skill-button"
|
||||
>
|
||||
{skill.name} (FW {skill.value})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zauberproben */}
|
||||
{Object.keys(character.spells).length > 0 && (
|
||||
<div className="dice-section">
|
||||
<h3>Zauberproben</h3>
|
||||
<div className="dice-buttons">
|
||||
{Object.values(character.spells).slice(0, 4).map((spell) => (
|
||||
<button
|
||||
key={spell.name}
|
||||
onClick={() => rollSpellCheck(spell)}
|
||||
className="dice-button spell-button"
|
||||
>
|
||||
{spell.name} (FW {spell.skillValue})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kampf */}
|
||||
<div className="dice-section">
|
||||
<h3>Kampfwürfe</h3>
|
||||
<div className="dice-buttons">
|
||||
<button
|
||||
onClick={rollInitiative}
|
||||
className="dice-button combat-button"
|
||||
>
|
||||
Initiative (INI {character.combat.initiative})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => rollSimpleDice('1d6', 'Schaden')}
|
||||
className="dice-button combat-button"
|
||||
>
|
||||
Schaden (1W6)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DSADiceRoller;
|
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { DSACharacter, DSASpell, DSAAttributes } from '../types/character';
|
||||
import React from 'react';
|
||||
import type { DSACharacter, DSASpell} from '../types/character';
|
||||
|
||||
interface SpellsProps {
|
||||
character: DSACharacter;
|
||||
@@ -7,19 +7,19 @@ interface SpellsProps {
|
||||
}
|
||||
|
||||
const Spells: React.FC<SpellsProps> = ({ character, setCharacter }) => {
|
||||
const [newSpell, setNewSpell] = useState<Partial<DSASpell>>({
|
||||
name: '',
|
||||
tradition: [],
|
||||
attributes: ['cleverness', 'intuition', 'charisma'],
|
||||
skillValue: 0,
|
||||
aspCost: '4 AsP',
|
||||
castingTime: '2 Aktionen',
|
||||
range: 'Berührung',
|
||||
duration: 'Sofort',
|
||||
difficulty: 0,
|
||||
description: '',
|
||||
effect: ''
|
||||
});
|
||||
// const [newSpell, setNewSpell] = useState<Partial<DSASpell>>({
|
||||
// name: '',
|
||||
// tradition: [],
|
||||
// attributes: ['cleverness', 'intuition', 'charisma'],
|
||||
// skillValue: 0,
|
||||
// aspCost: '4 AsP',
|
||||
// castingTime: '2 Aktionen',
|
||||
// range: 'Berührung',
|
||||
// duration: 'Sofort',
|
||||
// difficulty: 0,
|
||||
// description: '',
|
||||
// effect: ''
|
||||
// });
|
||||
|
||||
// Häufige DSA 5e Zauber für Quick-Add
|
||||
const commonSpells: Partial<DSASpell>[] = [
|
||||
|
127
src/hooks/useTaleSpireDice.ts
Normal file
127
src/hooks/useTaleSpireDice.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface DiceRollResult {
|
||||
dice: number[];
|
||||
total: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface TaleSpireAPI {
|
||||
rollDice: (diceString: string, label?: string) => Promise<DiceRollResult>;
|
||||
postToChat: (message: string) => void;
|
||||
}
|
||||
|
||||
// TaleSpire API global verfügbar machen
|
||||
declare global {
|
||||
interface Window {
|
||||
TS: TaleSpireAPI;
|
||||
}
|
||||
}
|
||||
|
||||
export const useTaleSpireDice = () => {
|
||||
const [isAvailable, setIsAvailable] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfen ob TaleSpire API verfügbar ist
|
||||
setIsAvailable(!!window.TS);
|
||||
}, []);
|
||||
|
||||
const rollDSACheck = async (
|
||||
attributes: [number, number, number],
|
||||
skillValue: number,
|
||||
modifier: number = 0,
|
||||
label: string = "DSA 5e Probe"
|
||||
): Promise<{
|
||||
dice: number[];
|
||||
success: boolean;
|
||||
qualityLevel: number;
|
||||
remainingPoints: number;
|
||||
}> => {
|
||||
if (!window.TS) {
|
||||
// Fallback für Development ohne TaleSpire
|
||||
const dice = [
|
||||
Math.floor(Math.random() * 20) + 1,
|
||||
Math.floor(Math.random() * 20) + 1,
|
||||
Math.floor(Math.random() * 20) + 1
|
||||
];
|
||||
return calculateDSAResult(dice, attributes, skillValue, modifier);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.TS.rollDice("3d20", label);
|
||||
const dsaResult = calculateDSAResult(result.dice, attributes, skillValue, modifier);
|
||||
|
||||
// Ergebnis in TaleSpire Chat posten
|
||||
const chatMessage = formatDSAChatMessage(dsaResult, attributes, skillValue, modifier, label);
|
||||
window.TS.postToChat(chatMessage);
|
||||
|
||||
return dsaResult;
|
||||
} catch (error) {
|
||||
console.error('TaleSpire Dice Roll failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const rollSimpleDice = async (diceString: string, label?: string) => {
|
||||
if (!window.TS) {
|
||||
// Fallback für Development
|
||||
const sides = parseInt(diceString.split('d')[1]) || 20;
|
||||
const count = parseInt(diceString.split('d')[0]) || 1;
|
||||
const dice = Array.from({ length: count }, () => Math.floor(Math.random() * sides) + 1);
|
||||
return { dice, total: dice.reduce((a, b) => a + b, 0), timestamp: Date.now() };
|
||||
}
|
||||
|
||||
return await window.TS.rollDice(diceString, label);
|
||||
};
|
||||
|
||||
return {
|
||||
isAvailable,
|
||||
rollDSACheck,
|
||||
rollSimpleDice
|
||||
};
|
||||
};
|
||||
|
||||
// DSA 5e Würfellogik
|
||||
const calculateDSAResult = (
|
||||
dice: number[],
|
||||
attributes: [number, number, number],
|
||||
skillValue: number,
|
||||
modifier: number
|
||||
) => {
|
||||
let remainingPoints = skillValue;
|
||||
const effectiveAttributes = attributes.map(attr => attr + modifier);
|
||||
|
||||
// Für jeden Würfel prüfen ob er das Attribut überschreitet
|
||||
dice.forEach((roll, index) => {
|
||||
if (roll > effectiveAttributes[index]) {
|
||||
remainingPoints -= (roll - effectiveAttributes[index]);
|
||||
}
|
||||
});
|
||||
|
||||
const success = remainingPoints >= 0;
|
||||
const qualityLevel = success ? Math.floor(remainingPoints / 3) : 0;
|
||||
|
||||
return {
|
||||
dice,
|
||||
success,
|
||||
qualityLevel,
|
||||
remainingPoints: Math.max(0, remainingPoints)
|
||||
};
|
||||
};
|
||||
|
||||
// Chat-Nachricht formatieren
|
||||
const formatDSAChatMessage = (
|
||||
result: any,
|
||||
attributes: [number, number, number],
|
||||
skillValue: number,
|
||||
modifier: number,
|
||||
label: string
|
||||
) => {
|
||||
const { dice, success, qualityLevel, remainingPoints } = result;
|
||||
const modifierText = modifier !== 0 ? ` (${modifier > 0 ? '+' : ''}${modifier})` : '';
|
||||
|
||||
return `🎲 **${label}**
|
||||
📊 Würfel: ${dice.join(', ')} vs ${attributes.join('/')}${modifierText}
|
||||
⚔️ FW: ${skillValue} | Übrig: ${remainingPoints}
|
||||
${success ? `✅ **Erfolg** (QL ${qualityLevel})` : '❌ **Fehlschlag**'}`;
|
||||
};
|
Reference in New Issue
Block a user