372 lines
12 KiB
Vue
372 lines
12 KiB
Vue
<template>
|
|
<div class="character-creation">
|
|
<div class="creation-header">
|
|
<h1>Create New Character</h1>
|
|
<div class="progress-indicator">
|
|
<div
|
|
v-for="step in steps"
|
|
:key="step.number"
|
|
:class="['step', {
|
|
active: currentStep === step.number,
|
|
completed: currentStep > step.number,
|
|
clickable: currentStep > step.number || currentStep === step.number
|
|
}]"
|
|
@click="navigateToStep(step.number)"
|
|
>
|
|
<span class="step-number">{{ step.number }}</span>
|
|
<span class="step-title">{{ step.title }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="creation-content">
|
|
<!-- Step 1: Basic Information -->
|
|
<CharacterBasicInfo
|
|
v-if="currentStep === 1"
|
|
:session-data="sessionData"
|
|
@next="handleNext"
|
|
@save="saveProgress"
|
|
/>
|
|
|
|
<!-- Step 2: Attributes -->
|
|
<CharacterAttributes
|
|
v-if="currentStep === 2"
|
|
:session-data="sessionData"
|
|
@next="handleNext"
|
|
@previous="handlePrevious"
|
|
@save="saveProgress"
|
|
/>
|
|
|
|
<!-- Step 3: Derived Values -->
|
|
<CharacterDerivedValues
|
|
v-if="currentStep === 3"
|
|
:session-data="sessionData"
|
|
@next="handleNext"
|
|
@previous="handlePrevious"
|
|
@save="saveProgress"
|
|
/>
|
|
|
|
<!-- Step 4: Skills -->
|
|
<CharacterSkills
|
|
v-if="currentStep === 4"
|
|
:session-data="sessionData"
|
|
:skill-categories="skillCategories"
|
|
@previous="handlePrevious"
|
|
@next="handleNext"
|
|
@save="saveProgress"
|
|
/>
|
|
|
|
<!-- Step 5: Spells -->
|
|
<CharacterSpells
|
|
v-if="currentStep === 5"
|
|
:session-data="sessionData"
|
|
:skill-categories="skillCategories"
|
|
@previous="handlePrevious"
|
|
@finalize="handleFinalize"
|
|
@save="saveProgress"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Session Info -->
|
|
<div class="session-info">
|
|
<p>Session expires: {{ formatDate(sessionData.expires_at) }}</p>
|
|
<button @click="deleteDraft" class="delete-btn">Delete Draft</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import API from '../utils/api'
|
|
import CharacterBasicInfo from './CharacterCreation/CharacterBasicInfo.vue'
|
|
import CharacterAttributes from './CharacterCreation/CharacterAttributes.vue'
|
|
import CharacterDerivedValues from './CharacterCreation/CharacterDerivedValues.vue'
|
|
import CharacterSkills from './CharacterCreation/CharacterSkills.vue'
|
|
import CharacterSpells from './CharacterCreation/CharacterSpells.vue'
|
|
|
|
export default {
|
|
name: 'CharacterCreation',
|
|
components: {
|
|
CharacterBasicInfo,
|
|
CharacterAttributes,
|
|
CharacterDerivedValues,
|
|
CharacterSkills,
|
|
CharacterSpells,
|
|
},
|
|
props: {
|
|
sessionId: {
|
|
type: String,
|
|
required: true,
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
currentStep: 1,
|
|
sessionData: {
|
|
id: '',
|
|
name: '',
|
|
rasse: '',
|
|
typ: '',
|
|
herkunft: '',
|
|
glaube: '',
|
|
geschlecht: '',
|
|
stand: '',
|
|
attributes: {},
|
|
derived_values: {},
|
|
skills: [],
|
|
spells: [],
|
|
skill_points: {},
|
|
spell_points: {},
|
|
expires_at: null,
|
|
current_step: 1,
|
|
},
|
|
steps: [
|
|
{ number: 1, title: 'Basic Info' },
|
|
{ number: 2, title: 'Attributes' },
|
|
{ number: 3, title: 'Derived Values' },
|
|
{ number: 4, title: 'Skills' },
|
|
{ number: 5, title: 'Spells' },
|
|
],
|
|
skillCategories: [],
|
|
}
|
|
},
|
|
async created() {
|
|
await this.loadSession()
|
|
await this.loadSkillCategories()
|
|
},
|
|
methods: {
|
|
async loadSession(preserveCurrentStep = false) {
|
|
try {
|
|
const token = localStorage.getItem('token')
|
|
const response = await API.get(`/api/characters/create-session/${this.sessionId}`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
|
|
this.sessionData = response.data
|
|
// Only update currentStep if not preserving it
|
|
if (!preserveCurrentStep) {
|
|
this.currentStep = response.data.current_step || 1
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading session:', error)
|
|
this.$router.push('/dashboard')
|
|
}
|
|
},
|
|
|
|
async loadSkillCategories() {
|
|
try {
|
|
const token = localStorage.getItem('token')
|
|
const response = await API.get('/api/characters/skill-categories', {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
|
|
this.skillCategories = response.data.categories || []
|
|
} catch (error) {
|
|
console.error('Error loading skill categories:', error)
|
|
// Fallback dummy data
|
|
this.skillCategories = [
|
|
{ name: 'körperlich', display_name: 'Körperliche Fertigkeiten', max_points: 200, points: 200 },
|
|
{ name: 'gesellschaftlich', display_name: 'Gesellschaftliche Fertigkeiten', max_points: 150, points: 150 },
|
|
{ name: 'natur', display_name: 'Natur Fertigkeiten', max_points: 100, points: 100 },
|
|
{ name: 'wissen', display_name: 'Wissens Fertigkeiten', max_points: 180, points: 180 },
|
|
{ name: 'handwerk', display_name: 'Handwerks Fertigkeiten', max_points: 120, points: 120 },
|
|
{ name: 'zauber', display_name: 'Zauber', max_points: 300, points: 300 },
|
|
]
|
|
}
|
|
},
|
|
|
|
async handleNext(data) {
|
|
try {
|
|
// Merge the new data
|
|
this.sessionData = { ...this.sessionData, ...data }
|
|
|
|
// Save progress for current step before moving to next
|
|
await this.saveProgressForStep(this.currentStep, data)
|
|
|
|
// Reload session data from backend to ensure consistent state
|
|
// Preserve currentStep as we'll increment it after
|
|
await this.loadSession(true)
|
|
|
|
// Move to next step
|
|
this.currentStep++
|
|
} catch (error) {
|
|
console.error('Failed to save progress before moving to next step:', error)
|
|
// Don't move to next step if save failed
|
|
}
|
|
},
|
|
|
|
async saveProgressForStep(step, data) {
|
|
try {
|
|
const token = localStorage.getItem('token')
|
|
|
|
let endpoint = ''
|
|
let payload = {}
|
|
|
|
switch (step) {
|
|
case 1:
|
|
endpoint = `/api/characters/create-session/${this.sessionId}/basic`
|
|
// Handle both old format and new basic_info format
|
|
const basicInfo = data.basic_info || data
|
|
payload = {
|
|
name: basicInfo.name || this.sessionData.name || '',
|
|
geschlecht: basicInfo.geschlecht || this.sessionData.geschlecht || '',
|
|
rasse: basicInfo.rasse || this.sessionData.rasse || '',
|
|
typ: basicInfo.typ || this.sessionData.typ || '',
|
|
herkunft: basicInfo.herkunft || this.sessionData.herkunft || '',
|
|
stand: basicInfo.stand || this.sessionData.stand || '',
|
|
glaube: basicInfo.glaube || this.sessionData.glaube || '',
|
|
}
|
|
// Validate that all required fields are present
|
|
if (!payload.name || !payload.geschlecht || !payload.rasse || !payload.typ || !payload.herkunft || !payload.stand) {
|
|
throw new Error(`Missing required fields: name=${payload.name}, geschlecht=${payload.geschlecht}, rasse=${payload.rasse}, typ=${payload.typ}, herkunft=${payload.herkunft}, stand=${payload.stand}`)
|
|
}
|
|
break
|
|
case 2:
|
|
endpoint = `/api/characters/create-session/${this.sessionId}/attributes`
|
|
payload = data.attributes || data
|
|
break
|
|
case 3:
|
|
endpoint = `/api/characters/create-session/${this.sessionId}/derived`
|
|
payload = data.derived_values || data
|
|
break
|
|
case 4:
|
|
endpoint = `/api/characters/create-session/${this.sessionId}/skills`
|
|
payload = {
|
|
skills: data.skills || this.sessionData.skills,
|
|
spells: data.spells || this.sessionData.spells,
|
|
skill_points: data.skill_points || this.sessionData.skill_points,
|
|
}
|
|
break
|
|
}
|
|
|
|
if (endpoint) {
|
|
const response = await API.put(endpoint, payload, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving progress for step', step, ':', error)
|
|
|
|
// Provide more specific error messages
|
|
if (error.response && error.response.status === 401) {
|
|
alert('Your session has expired. Please log in again.')
|
|
} else if (error.response && error.response.status === 400) {
|
|
const errorMsg = error.response.data?.error || 'Invalid data submitted'
|
|
//alert(`Error saving character data: ${errorMsg}`)
|
|
} //else {
|
|
//alert('Failed to save character data. Please try again.')
|
|
//}
|
|
throw error // Re-throw to handle in calling function
|
|
}
|
|
},
|
|
|
|
handlePrevious() {
|
|
this.currentStep--
|
|
},
|
|
|
|
navigateToStep(stepNumber) {
|
|
// Only allow navigation to current step or previously completed steps
|
|
if (stepNumber <= this.currentStep) {
|
|
this.currentStep = stepNumber
|
|
// Save current progress before switching steps (no data parameter needed here)
|
|
this.saveProgress().catch(error => {
|
|
console.error('Failed to save progress during navigation:', error)
|
|
})
|
|
}
|
|
},
|
|
|
|
async saveProgress(data = null) {
|
|
try {
|
|
// Use provided data or current sessionData as fallback
|
|
const dataToSave = data || this.sessionData
|
|
|
|
// Update sessionData with new data if provided
|
|
if (data) {
|
|
this.sessionData = { ...this.sessionData, ...data }
|
|
}
|
|
|
|
// Save progress for current step
|
|
await this.saveProgressForStep(this.currentStep, dataToSave)
|
|
} catch (error) {
|
|
console.error('Failed to save progress:', error)
|
|
throw error
|
|
}
|
|
},
|
|
|
|
getUserIdFromToken() {
|
|
try {
|
|
const token = localStorage.getItem('token')
|
|
if (!token) return null
|
|
|
|
// Decode JWT token to get user ID
|
|
const base64Url = token.split('.')[1]
|
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
|
const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
|
|
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
|
}).join(''))
|
|
|
|
const payload = JSON.parse(jsonPayload)
|
|
return payload.user_id || payload.userID || payload.sub || null
|
|
} catch (error) {
|
|
console.error('Error decoding token:', error)
|
|
return null
|
|
}
|
|
},
|
|
|
|
async handleFinalize() {
|
|
try {
|
|
const token = localStorage.getItem('token')
|
|
const userId = this.getUserIdFromToken()
|
|
|
|
const requestBody = {}
|
|
if (userId) {
|
|
requestBody.user_id = userId
|
|
}
|
|
|
|
const response = await API.post(`/api/characters/create-session/${this.sessionId}/finalize`, requestBody, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
|
|
const characterId = response.data.character_id
|
|
|
|
// Success message
|
|
//alert('Character successfully created!')
|
|
|
|
// Navigate to character view or back to character list
|
|
this.$router.push(`/character/${characterId}`)
|
|
} catch (error) {
|
|
console.error('Error finalizing character:', error)
|
|
if (error.response?.data?.error) {
|
|
alert(`Error: ${error.response.data.error}`)
|
|
} else {
|
|
alert('Fehler beim Abschließen der Charakter-Erstellung')
|
|
}
|
|
}
|
|
},
|
|
|
|
async deleteDraft() {
|
|
if (confirm('Are you sure you want to delete this character draft?')) {
|
|
try {
|
|
const token = localStorage.getItem('token')
|
|
await API.delete(`/api/characters/create-session/${this.sessionId}`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
|
|
this.$router.push('/dashboard')
|
|
} catch (error) {
|
|
console.error('Error deleting session:', error)
|
|
}
|
|
}
|
|
},
|
|
|
|
formatDate(dateString) {
|
|
if (!dateString) return ''
|
|
return new Date(dateString).toLocaleDateString()
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
/* All styles moved to main.css */
|
|
</style>
|