Spell Learn Dialog Anzeige funktioniert
This commit is contained in:
@@ -2079,7 +2079,7 @@ func GetAvailableSkillsNewSystem(c *gin.Context) {
|
||||
|
||||
// GetAvailableSpellsNewSystem gibt alle verfügbaren Zauber mit Lernkosten zurück (POST mit LernCostRequest)
|
||||
func GetAvailableSpellsNewSystem(c *gin.Context) {
|
||||
characterID := c.Param("id")
|
||||
//characterID := c.Param("id")
|
||||
|
||||
// Parse LernCostRequest aus POST body
|
||||
var baseRequest gsmaster.LernCostRequest
|
||||
@@ -2089,7 +2089,7 @@ func GetAvailableSpellsNewSystem(c *gin.Context) {
|
||||
}
|
||||
|
||||
var character models.Char
|
||||
if err := database.DB.Preload("Zauber").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&character, characterID).Error; err != nil {
|
||||
if err := database.DB.Preload("Zauber").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&character, baseRequest.CharId).Error; err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Character not found")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
package character
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"bamort/database"
|
||||
"bamort/gsmaster"
|
||||
"bamort/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetAvailableSpellsNewSystem(t *testing.T) {
|
||||
// Setup test database with real data
|
||||
database.SetupTestDB(true, true)
|
||||
defer database.ResetTestDB()
|
||||
|
||||
// Setup Gin in test mode
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
t.Run("Get available spells for character ID 20", func(t *testing.T) {
|
||||
|
||||
// Test Request wie im Frontend
|
||||
request := gsmaster.LernCostRequest{
|
||||
CharId: 18,
|
||||
Type: "spell",
|
||||
Action: "learn",
|
||||
UsePP: 0,
|
||||
UseGold: 0,
|
||||
Reward: stringPtr("default"),
|
||||
}
|
||||
|
||||
// Convert request to JSON
|
||||
requestJSON, err := json.Marshal(request)
|
||||
assert.NoError(t, err, "Should marshal request")
|
||||
|
||||
// Create HTTP request
|
||||
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(requestJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Create response recorder
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create Gin context
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
fmt.Printf("Test: Get available spells for character ID 20\n")
|
||||
fmt.Printf("Request: %s\n", string(requestJSON))
|
||||
|
||||
// Call the handler function
|
||||
GetAvailableSpellsNewSystem(c)
|
||||
|
||||
// Print the response for debugging
|
||||
fmt.Printf("Response Status: %d\n", w.Code)
|
||||
fmt.Printf("Response Body: %s\n", w.Body.String())
|
||||
|
||||
// Assert response status
|
||||
assert.Equal(t, http.StatusOK, w.Code, "Status code should be 200 OK")
|
||||
|
||||
// Parse response
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err, "Response should be valid JSON")
|
||||
|
||||
// Check response structure
|
||||
assert.Contains(t, response, "spells_by_school", "Response should contain spells_by_school field")
|
||||
|
||||
// Check if spells_by_school is an object
|
||||
spellsBySchool, ok := response["spells_by_school"].(map[string]interface{})
|
||||
assert.True(t, ok, "spells_by_school should be an object")
|
||||
|
||||
fmt.Printf("Found %d spell schools\n", len(spellsBySchool))
|
||||
|
||||
// Check each school
|
||||
totalSpells := 0
|
||||
for schoolName, schoolSpells := range spellsBySchool {
|
||||
fmt.Printf("School: %s\n", schoolName)
|
||||
|
||||
spells, ok := schoolSpells.([]interface{})
|
||||
assert.True(t, ok, fmt.Sprintf("School %s should contain an array of spells", schoolName))
|
||||
|
||||
totalSpells += len(spells)
|
||||
fmt.Printf(" Spells in school: %d\n", len(spells))
|
||||
|
||||
// Check structure of first spell in school if exists
|
||||
if len(spells) > 0 {
|
||||
firstSpell, ok := spells[0].(map[string]interface{})
|
||||
assert.True(t, ok, "First spell should be an object")
|
||||
|
||||
// Check required fields
|
||||
assert.Contains(t, firstSpell, "name", "Spell should have name field")
|
||||
assert.Contains(t, firstSpell, "level", "Spell should have level field")
|
||||
assert.Contains(t, firstSpell, "epCost", "Spell should have epCost field")
|
||||
assert.Contains(t, firstSpell, "goldCost", "Spell should have goldCost field")
|
||||
|
||||
spellName, ok := firstSpell["name"].(string)
|
||||
assert.True(t, ok, "Spell name should be a string")
|
||||
assert.NotEmpty(t, spellName, "Spell name should not be empty")
|
||||
|
||||
level, ok := firstSpell["level"].(float64)
|
||||
assert.True(t, ok, "Spell level should be a number")
|
||||
assert.GreaterOrEqual(t, level, float64(0), "Spell level should be at least 0")
|
||||
|
||||
epCost, ok := firstSpell["epCost"].(float64)
|
||||
assert.True(t, ok, "EP cost should be a number")
|
||||
assert.GreaterOrEqual(t, epCost, float64(0), "EP cost should be non-negative")
|
||||
|
||||
goldCost, ok := firstSpell["goldCost"].(float64)
|
||||
assert.True(t, ok, "Gold cost should be a number")
|
||||
assert.GreaterOrEqual(t, goldCost, float64(0), "Gold cost should be non-negative")
|
||||
|
||||
fmt.Printf(" Example spell: %s (Level %v, EP: %v, Gold: %v)\n",
|
||||
spellName, level, epCost, goldCost)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Total spells found: %d\n", totalSpells)
|
||||
assert.Greater(t, totalSpells, 0, "Should have at least some spells available")
|
||||
})
|
||||
|
||||
t.Run("Test with different reward types", func(t *testing.T) {
|
||||
// Verwende existierenden Charakter
|
||||
rewardTypes := []string{"default", "noGold", "halveep", "halveepnoGold"}
|
||||
|
||||
for _, rewardType := range rewardTypes {
|
||||
t.Run(fmt.Sprintf("RewardType_%s", rewardType), func(t *testing.T) {
|
||||
request := gsmaster.LernCostRequest{
|
||||
CharId: 18,
|
||||
Type: "spell",
|
||||
Action: "learn",
|
||||
UsePP: 0,
|
||||
UseGold: 0,
|
||||
Reward: stringPtr(rewardType),
|
||||
}
|
||||
|
||||
requestJSON, err := json.Marshal(request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(requestJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
GetAvailableSpellsNewSystem(c)
|
||||
|
||||
fmt.Printf("Testing reward type: %s - Status: %d\n", rewardType, w.Code)
|
||||
assert.Equal(t, http.StatusOK, w.Code, fmt.Sprintf("Should work with reward type %s", rewardType))
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err, "Response should be valid JSON")
|
||||
assert.Contains(t, response, "spells_by_school", "Should contain spells_by_school")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Test with invalid character ID", func(t *testing.T) {
|
||||
request := gsmaster.LernCostRequest{
|
||||
CharId: 99999, // Non-existent character
|
||||
Type: "spell",
|
||||
Action: "learn",
|
||||
UsePP: 0,
|
||||
UseGold: 0,
|
||||
Reward: stringPtr("default"),
|
||||
}
|
||||
|
||||
requestJSON, err := json.Marshal(request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(requestJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
GetAvailableSpellsNewSystem(c)
|
||||
|
||||
fmt.Printf("Testing invalid character ID - Status: %d\n", w.Code)
|
||||
// Der Handler verwendet nicht den "id" Parameter aus der URL, sondern CharId aus dem Request Body
|
||||
// Daher wird kein 404 zurückgegeben, sondern ein leeres Ergebnis oder Fehler
|
||||
assert.True(t, w.Code == http.StatusNotFound || w.Code == http.StatusOK,
|
||||
"Should return 404 or 200 for non-existent character")
|
||||
})
|
||||
|
||||
t.Run("Test with invalid request format", func(t *testing.T) {
|
||||
invalidJSON := []byte(`{"invalid": "request"}`)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(invalidJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
GetAvailableSpellsNewSystem(c)
|
||||
|
||||
fmt.Printf("Testing invalid request format - Status: %d\n", w.Code)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code, "Should return 400 for invalid request")
|
||||
})
|
||||
|
||||
t.Run("Test excluding already learned spells", func(t *testing.T) {
|
||||
// Füge dem Charakter einen Zauber hinzu
|
||||
learnedSpell := models.SkZauber{
|
||||
BamortCharTrait: models.BamortCharTrait{
|
||||
BamortBase: models.BamortBase{
|
||||
Name: "Feuerball",
|
||||
},
|
||||
CharacterID: 20,
|
||||
},
|
||||
Beschreibung: "Ein mächtiger Feuerball",
|
||||
Bonus: 5,
|
||||
Quelle: "Test",
|
||||
}
|
||||
|
||||
err := database.DB.Create(&learnedSpell).Error
|
||||
assert.NoError(t, err, "Should create learned spell")
|
||||
|
||||
request := gsmaster.LernCostRequest{
|
||||
CharId: 18,
|
||||
Type: "spell",
|
||||
Action: "learn",
|
||||
UsePP: 0,
|
||||
UseGold: 0,
|
||||
Reward: stringPtr("default"),
|
||||
}
|
||||
|
||||
requestJSON, err := json.Marshal(request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(requestJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
GetAvailableSpellsNewSystem(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code, "Should return 200 OK")
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
spellsBySchool := response["spells_by_school"].(map[string]interface{})
|
||||
|
||||
// Prüfe, dass "Feuerball" nicht in der Liste ist
|
||||
foundFeuerball := false
|
||||
for _, schoolSpells := range spellsBySchool {
|
||||
spells := schoolSpells.([]interface{})
|
||||
for _, spell := range spells {
|
||||
spellObj := spell.(map[string]interface{})
|
||||
if spellObj["name"].(string) == "Feuerball" {
|
||||
foundFeuerball = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.False(t, foundFeuerball, "Already learned spell 'Feuerball' should not be in available spells")
|
||||
fmt.Printf("Correctly excluded already learned spell 'Feuerball'\n")
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to create string pointer
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
@@ -37,6 +37,10 @@ var (
|
||||
)
|
||||
|
||||
func ConnectDatabase() *gorm.DB {
|
||||
SetupTestDB()
|
||||
return DB
|
||||
}
|
||||
func ConnectDatabaseOrig() *gorm.DB {
|
||||
dsn := "bamort:bG4)efozrc@tcp(192.168.0.5:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
database, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,691 @@
|
||||
<template>
|
||||
<div v-if="show" class="modal-overlay" @click.self="closeDialog">
|
||||
<div class="modal-content modal-wide">
|
||||
<h3>{{ $t('spells.learn.title') }}</h3>
|
||||
|
||||
<!-- Aktuelle Ressourcen -->
|
||||
<div class="current-resources">
|
||||
<div class="resource-display-card">
|
||||
<span class="resource-icon">⚡</span>
|
||||
<div class="resource-info">
|
||||
<div class="resource-label">Erfahrungspunkte</div>
|
||||
<div class="resource-amount">{{ character.erfahrungsschatz?.ep || 0 }} EP</div>
|
||||
<div class="resource-remaining">
|
||||
<small :class="{ 'text-warning': remainingEP < 50, 'text-danger': remainingEP <= 0 }">
|
||||
Verbleibend: {{ remainingEP }} EP
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-display-card">
|
||||
<span class="resource-icon">💰</span>
|
||||
<div class="resource-info">
|
||||
<div class="resource-label">Gold</div>
|
||||
<div class="resource-amount">{{ character.vermoegen?.goldstücke || 0 }} GS</div>
|
||||
<div class="resource-remaining">
|
||||
<small :class="{ 'text-warning': remainingGold < 20, 'text-danger': remainingGold <= 0 }">
|
||||
Nach Lernen: {{ remainingGold }} GS
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Belohnungsart, Suche und Sortierung -->
|
||||
<div class="form-group form-row">
|
||||
<div class="form-col form-col-main">
|
||||
<label>Lernen als Belohnung:</label>
|
||||
<select v-model="selectedRewardType" :disabled="isLoadingRewardTypes">
|
||||
<option value="" disabled>
|
||||
{{ isLoadingRewardTypes ? 'Lade Belohnungsarten...' : 'Belohnungsart wählen' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="rewardType in availableRewardTypes"
|
||||
:key="rewardType.value"
|
||||
:value="rewardType.value"
|
||||
>
|
||||
{{ rewardType.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-col form-col-input">
|
||||
<label>{{ $t('spells.learn.search.label') }}</label>
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
type="text"
|
||||
:placeholder="$t('spells.learn.search.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-col form-col-input">
|
||||
<label>Sortierung:</label>
|
||||
<select v-model="sortBy">
|
||||
<option value="name">Name</option>
|
||||
<option value="epCost">EP Kosten</option>
|
||||
<option value="goldCost">Gold Kosten</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schule Buttons -->
|
||||
<div class="form-group">
|
||||
<label>{{ $t('spells.learn.school.label') }}</label>
|
||||
<div class="school-buttons">
|
||||
<button
|
||||
class="school-btn"
|
||||
:class="{ 'active': selectedSchool === '' }"
|
||||
@click="selectedSchool = ''"
|
||||
>
|
||||
{{ $t('spells.learn.school.all') }}
|
||||
</button>
|
||||
<button
|
||||
v-for="school in availableSchools"
|
||||
:key="school"
|
||||
class="school-btn"
|
||||
:class="{ 'active': selectedSchool === school }"
|
||||
@click="selectedSchool = school"
|
||||
>
|
||||
{{ school }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verfügbare Zauber -->
|
||||
<div class="form-group">
|
||||
<label>{{ $t('spells.learn.available') }}</label>
|
||||
<div v-if="filteredSpells.length > 0" class="learning-levels">
|
||||
<div
|
||||
v-for="spell in filteredSpells"
|
||||
:key="spell.name"
|
||||
class="level-option"
|
||||
:class="{ 'selected': selectedSpell?.name === spell.name }"
|
||||
@click="selectSpell(spell)"
|
||||
>
|
||||
<div class="level-header">
|
||||
<span class="level-target">{{ spell.name }}</span>
|
||||
<span class="level-cost">
|
||||
<span v-if="spell.epCost">{{ spell.epCost }} EP</span>
|
||||
<span v-if="spell.epCost && spell.goldCost"> + </span>
|
||||
<span v-if="spell.goldCost">{{ spell.goldCost }} GS</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!isLoading" class="no-spells">
|
||||
Keine Zauber verfügbar
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ausgewählter Zauber Details -->
|
||||
<div v-if="selectedSpell" class="form-group">
|
||||
<div class="selection-summary">
|
||||
<strong>Ausgewählt:</strong> {{ selectedSpell.name }}
|
||||
<br>
|
||||
<span class="cost-summary">
|
||||
Lernkosten:
|
||||
<span v-if="selectedSpell.epCost">{{ selectedSpell.epCost }} EP</span>
|
||||
<span v-if="selectedSpell.epCost && selectedSpell.goldCost"> + </span>
|
||||
<span v-if="selectedSpell.goldCost">{{ selectedSpell.goldCost }} GS</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="loading-message">
|
||||
{{ $t('common.loading') }}
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button
|
||||
@click="learnSpell"
|
||||
class="btn-confirm"
|
||||
:disabled="!selectedSpell || isLoading"
|
||||
>
|
||||
{{ isLoading ? 'Wird gelernt...' : $t('spells.learn.action') }}
|
||||
</button>
|
||||
<button @click="closeDialog" class="btn-cancel" :disabled="isLoading">
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import API from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: "SpellLearnDialog",
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
character: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close', 'spell-learned'],
|
||||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
selectedSchool: '',
|
||||
selectedRewardType: '',
|
||||
sortBy: 'name',
|
||||
spellsBySchool: {},
|
||||
selectedSpell: null,
|
||||
isLoading: false,
|
||||
availableRewardTypes: [],
|
||||
isLoadingRewardTypes: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
remainingEP() {
|
||||
const currentEP = this.character.erfahrungsschatz?.ep || 0;
|
||||
const spellEPCost = this.selectedSpell?.epCost || 0;
|
||||
return Math.max(0, currentEP - spellEPCost);
|
||||
},
|
||||
|
||||
remainingGold() {
|
||||
const currentGold = this.character.vermoegen?.goldstücke || 0;
|
||||
const spellGoldCost = this.selectedSpell?.goldCost || 0;
|
||||
return Math.max(0, currentGold - spellGoldCost);
|
||||
},
|
||||
|
||||
totalCosts() {
|
||||
if (!this.selectedSpell) return 0;
|
||||
return (this.selectedSpell.epCost || 0) + (this.selectedSpell.goldCost || 0);
|
||||
},
|
||||
|
||||
filteredSpells() {
|
||||
let allSpells = [];
|
||||
|
||||
// Sammle alle Zauber aus allen Schulen
|
||||
Object.keys(this.spellsBySchool).forEach(school => {
|
||||
if (!this.selectedSchool || school === this.selectedSchool) {
|
||||
allSpells = allSpells.concat(this.spellsBySchool[school]);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter nach Suchterm
|
||||
let filtered = allSpells.filter(spell => {
|
||||
const matchesSearch = !this.searchTerm ||
|
||||
spell.name.toLowerCase().includes(this.searchTerm.toLowerCase());
|
||||
|
||||
return matchesSearch;
|
||||
});
|
||||
|
||||
// Sortierung
|
||||
return filtered.sort((a, b) => {
|
||||
switch (this.sortBy) {
|
||||
case 'epCost':
|
||||
return (a.epCost || 0) - (b.epCost || 0);
|
||||
case 'goldCost':
|
||||
return (a.goldCost || 0) - (b.goldCost || 0);
|
||||
case 'name':
|
||||
default:
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
availableSchools() {
|
||||
return Object.keys(this.spellsBySchool).sort();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
this.resetForm();
|
||||
this.loadRewardTypes();
|
||||
}
|
||||
},
|
||||
selectedRewardType() {
|
||||
if (this.selectedRewardType) {
|
||||
this.loadAvailableSpells();
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$api = API;
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.searchTerm = '';
|
||||
this.selectedSchool = '';
|
||||
this.selectedRewardType = '';
|
||||
this.sortBy = 'name';
|
||||
this.spellsBySchool = {};
|
||||
this.selectedSpell = null;
|
||||
this.availableRewardTypes = [];
|
||||
},
|
||||
|
||||
async loadRewardTypes() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
console.error('No authentication token available');
|
||||
this.availableRewardTypes = [{
|
||||
value: 'error',
|
||||
label: 'Anmeldung erforderlich'
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoadingRewardTypes = true;
|
||||
try {
|
||||
const response = await this.$api.get(`/api/characters/${this.character.id}/reward-types`, {
|
||||
params: {
|
||||
learning_type: 'spell',
|
||||
skill_name: 'spell',
|
||||
current_level: 0,
|
||||
skill_type: 'spell'
|
||||
}
|
||||
});
|
||||
|
||||
this.availableRewardTypes = response.data.reward_types || [];
|
||||
|
||||
// Setze Default-Belohnungsart wenn verfügbar
|
||||
if (this.availableRewardTypes.length > 0 && !this.selectedRewardType) {
|
||||
const defaultReward = this.availableRewardTypes.find(r => r.value === 'default');
|
||||
this.selectedRewardType = defaultReward ? defaultReward.value : this.availableRewardTypes[0].value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Belohnungsarten:', error);
|
||||
this.availableRewardTypes = [{
|
||||
value: 'default',
|
||||
label: 'Standard'
|
||||
}];
|
||||
this.selectedRewardType = 'default';
|
||||
} finally {
|
||||
this.isLoadingRewardTypes = false;
|
||||
}
|
||||
},
|
||||
|
||||
performInitialSearch() {
|
||||
// Beim Öffnen alle Zauber laden
|
||||
this.loadAvailableSpells();
|
||||
},
|
||||
|
||||
async loadAvailableSpells() {
|
||||
if (!this.selectedRewardType) return;
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
|
||||
// Erstelle LernCostRequest wie vom Backend erwartet
|
||||
const request = {
|
||||
char_id: this.character.id,
|
||||
type: 'spell',
|
||||
action: 'learn',
|
||||
use_pp: 0,
|
||||
use_gold: 0,
|
||||
reward: this.selectedRewardType
|
||||
};
|
||||
|
||||
const response = await this.$api.post('/api/characters/available-spells-new', request);
|
||||
this.spellsBySchool = response.data.spells_by_school || {};
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der verfügbaren Zauber:', error);
|
||||
this.spellsBySchool = {};
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
selectSpell(spell) {
|
||||
this.selectedSpell = this.selectedSpell?.name === spell.name ? null : spell;
|
||||
},
|
||||
|
||||
async learnSpell() {
|
||||
if (!this.selectedSpell || this.isLoading) return;
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
|
||||
const response = await this.$api.post(`/characters/${this.character.id}/learn-spell-new`, {
|
||||
char_id: this.character.id,
|
||||
name: this.selectedSpell.name,
|
||||
type: 'spell',
|
||||
action: 'learn',
|
||||
use_pp: 0,
|
||||
use_gold: 0,
|
||||
reward: this.selectedRewardType
|
||||
});
|
||||
|
||||
this.$emit('spell-learned', {
|
||||
spell: this.selectedSpell,
|
||||
response: response.data
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Lernen des Zaubers:', error);
|
||||
alert('Fehler beim Lernen des Zaubers: ' + (error.response?.data?.message || error.message));
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
animation: modalSlideIn 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-wide {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
/* Ressourcen-Anzeige im Dialog */
|
||||
.current-resources {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.resource-display-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.resource-display-card .resource-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.resource-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.resource-label {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.resource-amount {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.resource-remaining {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.resource-remaining small {
|
||||
color: #6c757d;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #f0ad4e !important;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #d9534f !important;
|
||||
}
|
||||
|
||||
/* Filter und Sortierung */
|
||||
.school-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.school-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.school-btn:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.school-btn.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
/* Zauber-Auswahl */
|
||||
.selection-summary {
|
||||
background: #e7f3ff;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.cost-summary {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.learning-levels {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.level-option {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f1f1;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.level-option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.level-option:hover:not(.disabled) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.level-option.selected {
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.level-option.disabled {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.level-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.level-target {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.level-cost {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.level-option.disabled .level-cost {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.no-spells {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #1da766;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-col-main {
|
||||
flex: 2;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.form-col-input {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
padding: 8px 20px;
|
||||
background: #1da766;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-confirm:hover:not(:disabled) {
|
||||
background: #16a085;
|
||||
}
|
||||
|
||||
.btn-confirm:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
padding: 8px 20px;
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-cancel:hover:not(:disabled) {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,25 @@
|
||||
<template>
|
||||
<div class="cd-view">
|
||||
<h2>{{ character.name }}'s Zauber</h2>
|
||||
<!-- Header mit Lernmodus-Kontrollen -->
|
||||
<div class="header-section">
|
||||
<h2>{{ character.name }}'s Zauber</h2>
|
||||
|
||||
<div class="learning-mode-controls">
|
||||
<!-- Lernmodus Toggle Button -->
|
||||
<button
|
||||
@click="showLearnNewDialog"
|
||||
class="btn-learning-mode"
|
||||
title="Neuen Zauber lernen"
|
||||
>
|
||||
<span class="icon">🎓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cd-list">
|
||||
<table class="cd-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<tr class="cd-table-header">
|
||||
<th>{{ $t('spell.name') }}</th>
|
||||
<th>{{ $t('spell.description') }}</th>
|
||||
<th>{{ $t('spell.bonus') }}</th>
|
||||
@@ -12,7 +27,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="spell in character.zauber">
|
||||
<template v-for="spell in character.zauber" :key="spell.id || spell.name">
|
||||
<tr>
|
||||
<td>{{ spell.name || '-' }}</td>
|
||||
<td>{{ spell.beschreibung || '-' }}</td>
|
||||
@@ -23,22 +38,103 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div> <!--- end cd-list-->
|
||||
|
||||
<!-- Dialog für neue Zauber lernen -->
|
||||
<SpellLearnDialog
|
||||
:character="character"
|
||||
:show="showLearnDialog"
|
||||
@close="closeDialogs"
|
||||
@spell-learned="handleSpellLearned"
|
||||
/>
|
||||
</div> <!--- end character -datasheet-->
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.cd-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cd-table-header {
|
||||
background-color: #1da766;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Header mit Lernmodus-Kontrollen */
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.learning-mode-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Lernmodus Toggle Button */
|
||||
.btn-learning-mode {
|
||||
padding: 8px 16px;
|
||||
border: 2px solid #1da766;
|
||||
background: white;
|
||||
color: #1da766;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-learning-mode:hover {
|
||||
background: #1da766;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<script>
|
||||
import API from '@/utils/api'
|
||||
import SpellLearnDialog from './SpellLearnDialog.vue'
|
||||
|
||||
export default {
|
||||
name: "SpellView",
|
||||
components: {
|
||||
SpellLearnDialog
|
||||
},
|
||||
props: {
|
||||
character: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showLearnDialog: false,
|
||||
isLoading: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$api = API;
|
||||
},
|
||||
methods: {
|
||||
showLearnNewDialog() {
|
||||
this.showLearnDialog = true;
|
||||
},
|
||||
|
||||
closeDialogs() {
|
||||
this.showLearnDialog = false;
|
||||
},
|
||||
|
||||
handleSpellLearned(eventData) {
|
||||
this.$emit('character-updated');
|
||||
this.closeDialogs();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
+31
-3
@@ -78,10 +78,34 @@ export default {
|
||||
wirkungsbereich:'Wirkungsbereich',
|
||||
wirkungsdauer:'Wirkungsdauer',
|
||||
ursprung:'Ursprung',
|
||||
quelle:'Quelle',
|
||||
system:'System',
|
||||
bonus:'Bonus',
|
||||
|
||||
quelle:'Quelle',
|
||||
},
|
||||
spells: {
|
||||
learn: {
|
||||
title: 'Neuen Zauber lernen',
|
||||
search: {
|
||||
label: 'Suche',
|
||||
placeholder: 'Zauber suchen...'
|
||||
},
|
||||
school: {
|
||||
label: 'Schule',
|
||||
all: 'Alle Schulen'
|
||||
},
|
||||
available: 'Verfügbare Zauber',
|
||||
selected: 'Ausgewählter Zauber',
|
||||
costs: {
|
||||
title: 'Lernkosten',
|
||||
ep: 'Erfahrungspunkte',
|
||||
gold: 'Gold',
|
||||
total: 'Gesamt'
|
||||
},
|
||||
action: 'Zauber lernen'
|
||||
},
|
||||
costs: {
|
||||
ep: 'EP',
|
||||
gold: 'Gold'
|
||||
}
|
||||
},
|
||||
Spell:'Zauber',
|
||||
weapon:{
|
||||
@@ -128,6 +152,10 @@ export default {
|
||||
},
|
||||
search:'Suche',
|
||||
Skill:'Fertigkeit',
|
||||
common: {
|
||||
loading: 'Laden...',
|
||||
cancel: 'Abbrechen'
|
||||
},
|
||||
experience: {
|
||||
title: 'Erfahrung & Vermögen',
|
||||
experience_points: 'Erfahrungspunkte',
|
||||
|
||||
Reference in New Issue
Block a user