dice roller added

This commit is contained in:
2025-06-15 18:59:26 +02:00
parent 7f280fc741
commit e623ca6ead
5 changed files with 773 additions and 16 deletions

View File

@@ -546,3 +546,430 @@ body {
background: var(--ts-background-primary, var(--background-color)); background: var(--ts-background-primary, var(--background-color));
transition: background-color 0.3s ease; 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;
}
}

View File

@@ -7,6 +7,7 @@ import Skills from './Skills';
import Spells from './Spells'; import Spells from './Spells';
import CombatValues from './CombatValues'; import CombatValues from './CombatValues';
import ThemeToggle from './ThemeToggle'; import ThemeToggle from './ThemeToggle';
import DSADiceRoller from "./DSADiceRoller.tsx";
import iconUrl from '/icon.png'; import iconUrl from '/icon.png';
import './CharacterSheet.css'; import './CharacterSheet.css';
@@ -77,6 +78,9 @@ const CharacterSheet: React.FC = () => {
{/* Fertigkeiten */} {/* Fertigkeiten */}
<Skills character={character} setCharacter={setCharacter} /> <Skills character={character} setCharacter={setCharacter} />
{/* Würfel-Integration - kommt nach Skills */}
<DSADiceRoller character={character} />
{/* Zauber - nur anzeigen wenn Astralenergie > 0 oder explizit gewünscht */} {/* Zauber - nur anzeigen wenn Astralenergie > 0 oder explizit gewünscht */}
{(isSpellcaster || Object.keys(character.spells).length > 0) && ( {(isSpellcaster || Object.keys(character.spells).length > 0) && (
<Spells character={character} setCharacter={setCharacter} /> <Spells character={character} setCharacter={setCharacter} />

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

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React from 'react';
import type { DSACharacter, DSASpell, DSAAttributes } from '../types/character'; import type { DSACharacter, DSASpell} from '../types/character';
interface SpellsProps { interface SpellsProps {
character: DSACharacter; character: DSACharacter;
@@ -7,19 +7,19 @@ interface SpellsProps {
} }
const Spells: React.FC<SpellsProps> = ({ character, setCharacter }) => { const Spells: React.FC<SpellsProps> = ({ character, setCharacter }) => {
const [newSpell, setNewSpell] = useState<Partial<DSASpell>>({ // const [newSpell, setNewSpell] = useState<Partial<DSASpell>>({
name: '', // name: '',
tradition: [], // tradition: [],
attributes: ['cleverness', 'intuition', 'charisma'], // attributes: ['cleverness', 'intuition', 'charisma'],
skillValue: 0, // skillValue: 0,
aspCost: '4 AsP', // aspCost: '4 AsP',
castingTime: '2 Aktionen', // castingTime: '2 Aktionen',
range: 'Berührung', // range: 'Berührung',
duration: 'Sofort', // duration: 'Sofort',
difficulty: 0, // difficulty: 0,
description: '', // description: '',
effect: '' // effect: ''
}); // });
// Häufige DSA 5e Zauber für Quick-Add // Häufige DSA 5e Zauber für Quick-Add
const commonSpells: Partial<DSASpell>[] = [ const commonSpells: Partial<DSASpell>[] = [

View 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**'}`;
};