From e623ca6ead128916d4d3bb1009c4f5007be95fea Mon Sep 17 00:00:00 2001 From: Matthias Puchstein Date: Sun, 15 Jun 2025 18:59:26 +0200 Subject: [PATCH] dice roller added --- src/components/CharacterSheet.css | 429 +++++++++++++++++++++++++++++- src/components/CharacterSheet.tsx | 4 + src/components/DSADiceRoller.tsx | 199 ++++++++++++++ src/components/Spells.tsx | 30 +-- src/hooks/useTaleSpireDice.ts | 127 +++++++++ 5 files changed, 773 insertions(+), 16 deletions(-) create mode 100644 src/components/DSADiceRoller.tsx create mode 100644 src/hooks/useTaleSpireDice.ts diff --git a/src/components/CharacterSheet.css b/src/components/CharacterSheet.css index 5613270..18e90b7 100644 --- a/src/components/CharacterSheet.css +++ b/src/components/CharacterSheet.css @@ -545,4 +545,431 @@ button:active { body { background: var(--ts-background-primary, var(--background-color)); transition: background-color 0.3s ease; -} \ No newline at end of file +} + +/* 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; + } +} diff --git a/src/components/CharacterSheet.tsx b/src/components/CharacterSheet.tsx index b10232f..fd2b71a 100644 --- a/src/components/CharacterSheet.tsx +++ b/src/components/CharacterSheet.tsx @@ -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 */} + {/* Würfel-Integration - kommt nach Skills */} + + {/* Zauber - nur anzeigen wenn Astralenergie > 0 oder explizit gewünscht */} {(isSpellcaster || Object.keys(character.spells).length > 0) && ( diff --git a/src/components/DSADiceRoller.tsx b/src/components/DSADiceRoller.tsx new file mode 100644 index 0000000..27bd2eb --- /dev/null +++ b/src/components/DSADiceRoller.tsx @@ -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 = ({ character }) => { + const { rollDSACheck, rollSimpleDice, isAvailable } = useTaleSpireDice(); + const [modifier, setModifier] = useState(0); + const [lastResult, setLastResult] = useState(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 ( +
+

🎲 Würfelproben

+ + {!isAvailable && ( +
+ ⚠️ TaleSpire Dice API nicht verfügbar - Development Mode aktiv +
+ )} + + {/* Modifikator */} +
+ + setModifier(parseInt(e.target.value) || 0)} + min="-10" + max="10" + className="modifier-input" + /> +
+ + {/* Letztes Ergebnis */} + {lastResult && ( +
+

Letztes Ergebnis:

+
+
Würfel: {lastResult.dice?.join(', ')}
+ {lastResult.type === 'initiative' ? ( +
Initiative: {lastResult.total}
+ ) : ( + <> +
{lastResult.success ? '✅ Erfolg' : '❌ Fehlschlag'}
+ {lastResult.success && ( +
Qualitätsstufe: {lastResult.qualityLevel}
+ )} + + )} +
+
+ )} + + {/* Eigenschaften */} +
+

Eigenschaftsproben

+
+ {Object.entries(character.attributes).map(([key, value]) => ( + + ))} +
+
+ + {/* Fertigkeiten */} + {Object.keys(character.skills).length > 0 && ( +
+

Fertigkeitsproben

+
+ {Object.values(character.skills).slice(0, 6).map((skill) => ( + + ))} +
+
+ )} + + {/* Zauberproben */} + {Object.keys(character.spells).length > 0 && ( +
+

Zauberproben

+
+ {Object.values(character.spells).slice(0, 4).map((spell) => ( + + ))} +
+
+ )} + + {/* Kampf */} +
+

Kampfwürfe

+
+ + +
+
+
+ ); +}; + +export default DSADiceRoller; \ No newline at end of file diff --git a/src/components/Spells.tsx b/src/components/Spells.tsx index 37620b2..65533d6 100644 --- a/src/components/Spells.tsx +++ b/src/components/Spells.tsx @@ -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 = ({ character, setCharacter }) => { - const [newSpell, setNewSpell] = useState>({ - 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>({ + // 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[] = [ diff --git a/src/hooks/useTaleSpireDice.ts b/src/hooks/useTaleSpireDice.ts new file mode 100644 index 0000000..a585241 --- /dev/null +++ b/src/hooks/useTaleSpireDice.ts @@ -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; + 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**'}`; +}; \ No newline at end of file