* introduced central package registry by package init function * dynamic registration of routes, model, migrations and initializers. * setting a docker compose project name to prevent shutdown of other containers with the same (composer)name * ai documentation * app template * Create tests for ALL API entpoints in ALL packages Based on current data. Ensure that all API endpoints used in frontend are tested. These tests are crucial for the next refactoring tasks. * adopting agent instructions for a more consistent coding style * added desired module layout and debugging information * Fix All Failing tests All failing tests are fixed now that makes the refactoring more easy since all tests must pass * restored routes for maintenance * added common translations * added new tests for API Endpoint * Merge branch 'separate_business_logic' * added lern and skill improvement cost editing * Set Docker image tag when building to prevent rebuild when nothing has changed * add and remove PP for Weaponskill fixed * add and remove PP for same named skills fixed * add new task
55 KiB
BaMoRT Frontend — Comprehensive Code Analysis
Generated: 2026-03-13
Table of Contents
- Project Setup
- Application Entry
- Router
- Pinia Stores
- Views (src/views/)
- Components (src/components/)
- API Layer
- i18n
- Styling
- Business Logic in Frontend
- User Flows
- Component Hierarchy
1. Project Setup
package.json
name: bamort-frontend
version: 0.2.3
type: module
Runtime dependencies:
| Package | Version | Purpose |
|---|---|---|
vue |
^3.5.13 | Core framework (Options API) |
vue-router |
^4.5.0 | Client-side routing |
pinia |
^2.3.0 | State management |
vue-i18n |
^11.0.0 | Internationalisation (DE + EN) |
axios |
^1.7.9 | HTTP client |
Dev dependencies:
| Package | Version | Purpose |
|---|---|---|
vite |
^6.0.1 | Build tool / dev server |
@vitejs/plugin-vue |
^5.2.1 | Vite Vue 3 plugin |
vite-plugin-vue-devtools |
^7.6.5 | Browser devtools integration |
No testing framework, no TypeScript compiler (.ts files exist only as type declaration stubs).
vite.config.js
export default defineConfig({
plugins: [vue(), vueDevTools()],
resolve: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }
},
server: {
host: ['bamort.trokan.de', '192.168.0.48', 'localhost', 'terra.local'],
}
})
Key notes:
@alias maps tosrc/- Dev server explicitly bound to multiple hostnames (network-accessible)
- Minification and source maps are commented out (disabled)
- No Vite proxy configuration — all API calls go directly to the configured
VITE_API_URL
index.html
Single-page app root. Mounts #app and loads src/main.js as an ES module. Uses favicon.png.
src/version.js
export const VERSION = '0.2.3'
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'
Exposes getVersion() and getGitCommit() consumed by LandingView and SystemInfoView.
2. Application Entry
src/main.js
import './assets/main.css'
import { createApp } from "vue"
import { createPinia } from 'pinia'
import App from "./App.vue"
import router from "./router"
import { i18n } from './stores/languageStore'
import UtilsPlugin from './utils/utilsPlugin'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(i18n)
app.use(UtilsPlugin)
app.mount("#app")
Order of registration:
- Pinia (state store)
- Vue Router
- vue-i18n instance (imported from
languageStore.js, not created here) UtilsPlugin(global properties: date helpers + dice rollers)
src/App.vue
Root component. Shows <Menu /> only when the user is logged-in (detected via localStorage.getItem('token')). The main <router-view /> is wrapped in a <main> that gets the CSS class full-width when the user is not logged in (centering forms on the page). Listens to storage and custom auth-changed window events to reactively update loggedIn.
UtilsPlugin (src/utils/utilsPlugin.js)
Installs these global properties on the Vue app instance:
| Property | Source | Description |
|---|---|---|
$formatDate |
dateUtils.js |
Format ISO date to locale string |
$formatDateTime |
dateUtils.js |
Format ISO datetime |
$formatRelativeDate |
dateUtils.js |
Relative time (e.g. "2 hours ago") |
$safeValue |
dateUtils.js |
Return value or fallback |
$capitalize |
dateUtils.js |
Capitalize string |
$rollDie |
randomUtils.js |
Roll single die (1–N) |
$rollDice |
randomUtils.js |
Roll multiple dice → array |
$rollDiceWithSum |
randomUtils.js |
Roll + return sum |
$rollNotation |
randomUtils.js |
Parse RPG notation e.g. "3d6+2" |
$randomBetween |
randomUtils.js |
Random int in range |
$randomChoice |
randomUtils.js |
Pick random element from array |
$shuffleArray |
randomUtils.js |
Fisher-Yates shuffle |
3. Router
src/router/index.js — createWebHistory (HTML5 mode, no hash).
Route Table
| Path | Name | Component | Auth? | Admin? |
|---|---|---|---|---|
/ |
Landing |
LandingView |
— | — |
/login |
Login |
LoginView |
— | — |
/register |
Register |
RegisterView |
— | — |
/forgot-password |
ForgotPassword |
ForgotPasswordView |
— | — |
/reset-password |
ResetPassword |
ResetPasswordView |
— | — |
/dashboard |
Dashboard |
DashboardView |
✓ | — |
/profile |
UserProfile |
UserProfileView |
✓ | — |
/users |
UserManagement |
UserManagementView |
✓ | ✓ |
/ausruestung/:characterId |
Ausruestung |
AusruestungView |
✓ | — |
/maintenance |
Maintenance |
MaintenanceView |
✓ | — |
/upload |
FileUpload |
FileUploadPage |
✓ | — |
/sponsors |
Sponsors |
SponsorsView |
— | — |
/help |
Help |
HelpView |
— | — |
/system-info |
SystemInfo |
SystemInfoView |
— | — |
/character/:id |
CharacterDetails |
CharacterDetails |
✓ | — |
/character/create/:sessionId |
CharacterCreation |
CharacterCreation |
✓ | — |
Static imports (loaded immediately — needed for unauthenticated pages): LandingView, LoginView, RegisterView, ForgotPasswordView, ResetPasswordView.
Lazy-loaded (code-split into separate chunks on first access): all other views and both character components.
Route params are passed as props for CharacterDetails (id) and CharacterCreation (sessionId).
Navigation Guard
router.beforeEach(async (to, from, next) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
next({ name: "Login" })
} else if (to.meta.requiresAdmin) {
const userStore = useUserStore()
if (!userStore.currentUser) await userStore.fetchCurrentUser()
if (!userStore.isAdmin) next({ name: "Dashboard" })
else next()
} else {
next()
}
})
isLoggedIn() simply checks localStorage.getItem("token"). The admin check lazy-loads the user profile if it hasn't been fetched yet.
4. Pinia Stores
userStore (src/stores/userStore.js)
defineStore('user', {
state: () => ({ currentUser: null, isLoading: false }),
getters: {
isAuthenticated: state => !!state.currentUser,
userRole: state => state.currentUser?.role || 'standard',
isAdmin: state => state.currentUser?.role === 'admin',
isMaintainer: state => state.currentUser?.role === 'maintainer'
|| state.currentUser?.role === 'admin',
isStandardUser: state => !!state.currentUser
},
actions: {
async fetchCurrentUser() { /* GET /api/user/profile */ },
clearUser() { this.currentUser = null }
}
})
- Fetches profile from
GET /api/user/profile. - On successful fetch, also updates the i18n locale from
profile.preferred_languageand persists it tolocalStorage. currentUsershape:{ id, username, display_name, email, role, preferred_language }.- Used by:
Menu.vue,CharacterDetails.vue,UserManagementView.vue, router guard.
languageStore (src/stores/languageStore.js)
export const i18n = createI18n({
legacy: false,
locale: localStorage.getItem('language') || 'de',
fallbackLocale: 'en',
messages: { de, en }
})
defineStore('language', {
state: () => ({ currentLanguage: localStorage.getItem('language') || 'de' }),
actions: {
setLanguage(lang) {
this.currentLanguage = lang
i18n.global.locale.value = lang
localStorage.setItem('language', lang)
}
}
})
Unusually, the i18n instance is created and exported from this file, then imported in main.js. Default language: de.
characterCreationStore (src/stores/characterCreation.js)
Manages the multi-step character wizard session state in-memory (backed by server-side session).
State:
{
sessionData: {
basic_info: null,
attributes: null,
derived_values: null,
skills: [],
skills_meta: { totalUsedPoints: 0, selectedCategory: null },
spells: [],
equipment: null
},
currentStep: 1, // 1–5
sessionId: null,
isLoading: false,
error: null
}
Getters: characterClass, characterStand, characterRace, hasSelectedSkills, totalSkillPoints, isValid (checks basic_info + attributes + derived_values are all non-null).
Actions: initializeSession(sessionId?), createNewSession(), loadSession(sessionId) (TODO), saveSession() (TODO), updateStepData(data), goToStep(n), nextStep(), previousStep(), clearSession().
Note: saveSession() and loadSession() are marked TODO — the actual session persistence is handled directly by CharacterCreation.vue via the API, bypassing this store.
counter (src/stores/counter.ts)
A boilerplate Pinia counter store from Vue scaffolding. Not used in the application.
5. Views (src/views/)
Most views are thin wrappers that delegate to a single component.
LandingView.vue
Purpose: Public landing page. Displays app logo, description, version info, and action links.
Data fetched: GET ${VITE_API_URL}/api/public/version (unauthenticated, uses raw axios not the API utility). Retries every 5 s up to 24 times if the backend is unreachable.
Logic:
isBackendAvailablecomputed: disables the login button until the backend responds.- Clears retry interval on
beforeUnmount. - Shows frontend version from
version.js(0.2.3) and backend version from API.
Components used: None (self-contained).
LoginView.vue
Thin wrapper: renders <LoginForm />.
RegisterView.vue
Thin wrapper: renders <RegisterForm />.
ForgotPasswordView.vue
Thin wrapper: renders <ForgotPasswordForm />.
ResetPasswordView.vue
Thin wrapper: renders <ResetPasswordForm />.
DashboardView.vue
Thin wrapper: renders <CharacterList />.
CharacterView.vue
Thin wrapper (legacy): passes $route.params.characterId to <CharacterDetails /> as :characterId. Route /character/:id uses CharacterDetails directly via lazy import; this view appears unused by the router.
AusruestungView.vue
Passes $route.params.characterId to <AusruestungList />. Legacy/simple standalone equipment list for a character — separate from the main character detail equipment tab.
MaintenanceView.vue
Thin wrapper: renders <Maintenance />.
UserProfileView.vue
Purpose: Self-service profile management for the logged-in user.
API calls:
GET /api/user/profile(on created)PUT /api/user/display-name— change display name (max 30 chars)PUT /api/user/language— change preferred language (updates i18n locale + localStorage)PUT /api/user/email— change emailPUT /api/user/password— change password (requires current + new + confirm)
Validation:
- Display name: max 30 chars
- Password: min 6 chars, must match confirmation
- Email: must differ from current, type="email"
- Duplicate email check via backend error message parsing
Sections rendered: User info display, Change Display Name, Change Language, Change Email, Change Password.
After language update: directly sets this.$i18n.locale and localStorage.language. Also updates userStore.currentUser.display_name after display name change.
UserManagementView.vue
Purpose: Admin panel for managing all users.
Guard: Route has requiresAdmin: true.
API calls:
GET /api/users— list all usersPUT /api/users/:id/role— change role (standard|maintainer|admin)DELETE /api/users/:id— delete userPUT /api/users/:id/password— admin password reset (no current password required)
Dialogs (modal overlays):
- Role change dialog — select from standard/maintainer/admin
- Delete confirmation dialog
- Password change dialog (min 6 chars, must confirm)
Protection: Buttons disabled for the current logged-in user's own row (cannot self-demote or self-delete). Detected via currentUser.id === user.id from userStore.
FileUploadPage.vue
Purpose: Import character data from VTT (Virtual Tabletop) JSON or CSV files.
API call: POST /api/importer/upload with multipart/form-data.
Validation: Checks MIME types (application/json, text/csv). VTT file is required; CSV is optional.
Note: Manually adds Authorization header even though the API interceptor already does this — a redundancy.
HelpView.vue
Static content page. Dynamically loads FAQ items from i18n keys help.faq1.question/.answer, help.faq2..., etc., iterating until a key is not found using $te().
SponsorsView.vue
Static content page listing contributors and special thanks. Links to GitHub and Ko-fi.
SystemInfoView.vue
Purpose: Displays system/technology information and live backend status.
API call: GET ${VITE_API_URL}/api/public/systeminfo (uses raw axios). Returns { version, gitCommit, userCount, charCount, dbVersion }.
Technology stack display: Hardcoded lists of Vue 3, Vite, Go 1.25, Gin, GORM, MariaDB, JWT, chromedp, Docker.
6. Components (src/components/)
Menu.vue
Purpose: Fixed top navigation bar (60px high, z-index 1000).
Authentication-dependent rendering:
- Always visible: Home, Info dropdown (Help, Sponsors, System Info)
- Logged-in only: Characters dropdown (Dashboard, Import Data)
- Maintainer/Admin only: Admin dropdown (Maintenance, User Management)
- Not logged-in only: Register link
- Right side (logged-in): User icon dropdown (Profile, Logout)
Hover dropdowns: Implemented with @mouseenter/@mouseleave + 200 ms setTimeout delay to prevent premature closing.
Logout flow: calls logout() from auth.js (removes token), clears userStore, dispatches auth-changed window event, navigates to /.
Auth change listener: window.addEventListener('auth-changed', ...) — re-fetches user profile on login or clears on logout.
Props/emits: None.
LoginForm.vue
Purpose: Login form.
API call: POST /login with { username, password }. On success: stores JWT in localStorage, calls userStore.fetchCurrentUser(), dispatches auth-changed, navigates to /dashboard.
Error handling: Displays "Invalid credentials" on any error.
Note: Hardcoded English labels — does not use $t() for the form labels (unlike most other components).
RegisterForm.vue
Purpose: Registration form.
Validation: Client-side password confirmation match check before API call.
API call: POST /register with { username, password, email }.
i18n: Fully translated via auth.* keys.
ForgotPasswordForm.vue
Purpose: Request a password reset email.
API call: POST /password-reset/request with { email, redirect_url: window.location.origin }.
UX: Shows success state (hides form, shows info text) on success without redirecting. Shows error message on failure.
ResetPasswordForm.vue
Handles the password-reset token link (not examined in detail — delegates to <ResetPasswordForm />).
CharacterList.vue
Purpose: Dashboard character listing with creation session management.
API calls:
GET /api/characters→{ self_owned: [...], others: [...] }GET /api/characters/create-sessions→{ sessions: [...] }POST /api/characters/create-session→{ session_id }— starts new character wizardDELETE /api/characters/create-session/:sessionId
Layout:
- "Create New Character" button (green dashed border)
<CharacterCreationSessions>sub-component (shows in-progress wizards)- Two column layout: owned characters | shared characters
- Each character shows: name, race, class, grade, owner, public/private badge
- Character names are
<router-link>to/character/:id
Props/emits: None (standalone).
Sub-components: CharacterCreationSessions.
CharacterCreationSessions.vue
Purpose: Shows active character creation sessions (drafts) on the dashboard.
Props: sessions: Array
Emits: continue-session(sessionId), delete-session(sessionId)
Renders: Grid of cards, one per session. Shows name, step progress (e.g. "Step 2/5"), race/class, last updated, expires. Clicking a card card navigates via continue-session emit.
CharacterDetails.vue
Purpose: Main character management hub. Fetches character data and dynamically renders sub-views through a tab-like submenu.
Props: id (string, from route)
API calls:
GET /api/characters/:id(on created + after any edit viarefreshCharacter)
Computed: isOwner — compares character.user_id with userStore.currentUser.id.
Sub-views rendered via <component :is="currentView"> with character and isOwner props:
DatasheetView(default)SkillViewWeaponViewSpellViewEquipmentViewExperianceViewDeleteCharView
Additional dialogs:
- 📄 Export button →
ExportDialog - 🌐/🔒 Visibility button (owner only) →
VisibilityDialog
Event handling: @character-updated from sub-views triggers refreshCharacter() which reloads from API.
DatasheetView.vue
Purpose: Character stats and biographical information display with inline editing.
Props: character: Object (required)
Emits: character-updated
Sub-components: ImageUploadCropper
Inline editing mechanism: Double-click (@dblclick) on any displayed value activates an input/select in its place. On blur or Enter, saveEdit(path) / saveProp(prop) PATCHes the character via PUT /api/characters/:id. On Escape, cancelEdit() restores display.
Stat fields (left column stat box):
| Label key | Character path |
|---|---|
stats.beauty |
eigenschaften.0.value |
stats.dexterity |
eigenschaften.1.value |
stats.agility |
eigenschaften.2.value |
stats.intelligence |
eigenschaften.3.value |
stats.constitution |
eigenschaften.4.value |
stats.charisma |
eigenschaften.5.value |
stats.strength |
eigenschaften.6.value |
stats.willpower |
eigenschaften.7.value |
stats.spelltalent |
eigenschaften.8.value |
stats.poisontolerance |
git |
stats.movement |
b.max |
stats.lifepoints |
lp.max |
stats.staminapoints |
ap.max |
stats.divinegrace |
bennies.gg |
stats.fatesfavor |
bennies.sg |
Character info fields (editable): name, typ (class), gender (select), grad, rasse (select), origin (select), social_class (select), spezialisierung (select), alter, hand (select: rechts/links/beidhändig), groesse, gewicht, merkmale.groesse, merkmale.breite, merkmale.augenfarbe, merkmale.haarfarbe, glaube (multi-select), merkmale.sonstige.
Select options: loaded lazily via GET /api/characters/:id/datasheet-options (only when a select-type field is first double-clicked).
glaube (beliefs) field: Multi-select stored as comma-separated string; displayed and edited as array.
Path traversal for saving: path.split('.') reduces into nested character object, then PUT /api/characters/:id with full character payload.
SkillView.vue
Purpose: Display and manage character skills (Fertigkeiten + Waffenfertigkeiten) and innate skills.
Props: character: Object (required), isOwner: Boolean
Emits: character-updated
Sub-components: SkillImproveDialog, SkillLearnDialog
Learning mode toggle: 🎓 button visible to owner. When active:
- Resources bar (EP + Gold) shown
- Additional action buttons: 📚 Learn New, ➕ Add Skill
- Each skill row shows ⬆️ Improve button
Table structure:
- Regular skills: grouped by category (
character.categorizedskills) — name, EW (Fertigkeitswert), bonus, PP (Praxispunkte), note - Weapon skills: from
character.waffenfertigkeiten - Innate skills: from
character.InnateSkills(right column, name + value only)
PP (Praxispunkte) adjustment: +/− buttons directly call POST /api/characters/practice-points.
Inline skill editing: Double-click on name, fertigkeitswert, or bemerkung → inline input; saved via PUT /api/characters/:skill_id/skill.
Dialogs:
SkillLearnDialog— learn new skill (complex, see below)- Improve Selection Dialog — simple modal; selects skill + PP count →
POST /api/characters/improve-skill - Add Skill Dialog — manually add skill by name/value →
POST /api/characters/:id/add-skill SkillImproveDialog— detailed multi-level improvement dialog
SkillImproveDialog.vue
Purpose: Detailed skill improvement dialog supporting multiple reward types and level sequences.
Props: character: Object, skill: Object, isVisible: Boolean, learningType: 'improve'|'learn'|'spell'
Emits: close, skill-updated
Features:
- Loads available improvement levels + costs from
GET /api/characters/lerncost-newsystemor similar - Loads reward types from API
- Shows live resource tracking (EP remaining, Gold remaining, PP remaining)
- Multi-level selection (click/deselect in sequence — must be consecutive)
- Reward types:
default(EP + Gold),noGold(EP only),halveepnoGold(half EP),pp(PP only),mixed(EP + PP) - Submits via
POST /api/characters/improve-skill-newsystemor similar
SkillLearnDialog.vue
Purpose: Learn a brand-new skill (not yet on the character).
Features:
- Drag & drop (HTML5 native) or click-to-select skills from available list to "to learn" list
- Category filter buttons (all categories shown as chips)
- Search filter input
- Sort by name or EP cost (ascending/descending toggle)
- Reward type selector: Standard (EP + Gold) or Nur EP (no gold)
- Live EP/Gold remaining display
- Multi-skill batch learning (multiple skills can be queued)
- Submits batch via
POST /api/characters/learn-skills(or similar)
SpellView.vue
Purpose: Display character spells.
Props: character: Object, isOwner: Boolean
Emits: character-updated
Sub-components: SpellLearnDialog
Table: name, description, bonus, source.
Learning: Owner sees 🎓 button → opens SpellLearnDialog.
SpellLearnDialog.vue
Purpose: Learn new spells with filtering, sorting, and multi-spell queuing.
Features:
- School (Schule) filter buttons
- Search term filter
- Sort by name, EP cost, Gold cost
- Reward type selector (loads from API)
- Available/selected two-column layout
- Per-spell affordability check
- Batch spell learning queue with total cost summary
- Submits via
POST /api/characters/learn-spells(or similar)
WeaponView.vue
Purpose: Display and manage character weapons.
Props: character: Object, isOwner: Boolean
Emits: character-updated
Table: name (* suffix if magical), description, weight, value, amount, contained_in, bonus (anb/abwb).
Add weapon dialog:
- Loads master weapon list from
GET /api/maintenance/weapons(or similar) - Text search filter
- Select + set amount
- Submits via
POST /api/equipment(or similar weapon endpoint)
Edit weapon dialog:
- Inline form for: description, is_magical checkbox, amount, value, attack bonus (anb), defense bonus (abwb), damage bonus (schb)
- Saves via
PUT /api/equipment/:id(or similar)
Delete weapon: DELETE /api/equipment/:id with confirm dialog using $t('weapon.confirmDelete') with {name} interpolation.
EquipmentView.vue
Purpose: Display and manage character non-weapon equipment.
Props: character: Object, isOwner: Boolean
Emits: character-updated
Table: name, description, weight, value, amount, contained_in, bonus.
Add equipment dialog:
- Loads master equipment from
GET /api/maintenance/equipment - Text search filter
- Select + set amount
- Submits:
POST /api/equipmentwith{ character_id, name, beschreibung, gewicht, wert, anzahl, bonus, beinhaltet_in, contained_in, container_type }
Delete: DELETE /api/equipment/:id with i18n confirm using {name} interpolation.
ExperianceView.vue
Purpose: Manage EP (experience points) and gold/wealth, with audit log.
Props: character: Object, isOwner: Boolean
Sub-components: AuditLogView
Computed: totalWealthInGS — currency conversion:
goldstücke + Math.floor(silberstücke / 10) + Math.floor(kupferstücke / 10)
(1 GS = 10 SS = 10 KS — Midgard RPG currency system)
EP management: Add/remove EP inputs → PUT /api/characters/:id/experience or similar.
Gold management: Add/remove gold inputs → PUT /api/characters/:id/wealth or similar.
After both operations: calls this.$refs.auditLog.loadAuditLog() to refresh the audit log.
AuditLogView.vue
Purpose: Display change history (EP/gold transactions) for a character.
Props: character: Object
API: GET /api/characters/:id/audit-log with query params for field filter and date range.
Filters:
- Field: all / experience_points / gold / silver / copper
- Date range: all time / today / this week / this month / custom (from–to date inputs)
- Group by date toggle
Statistics section: total_changes, ep_spent, ep_gained, gold_spent, gold_gained.
Entry display: Shows old → new value with difference (+/−), reason, optional notes. Color-coded: green for positive, red for negative changes.
ExportDialog.vue
Purpose: Multi-format character export modal.
Props: characterId, showDialog: Boolean
Emits: update:showDialog, export-success
Computed: canExport — false if no format selected, or PDF format but no template selected.
On created: loads templates from GET /api/pdf/templates.
Export formats:
| Format | API Call | Delivery |
|---|---|---|
GET /api/pdf/export/:id?template=X&showUserName=true → returns { filename }, then opens GET /api/pdf/file/:filename in new tab |
Browser window popup | |
| VTT | GET /api/importer/export/vtt/:id/file (blob) |
Programmatic download link |
| BaMoRT (JSON) | GET /api/transfer/download/:id (blob) |
Programmatic download link |
PDF export note: Gets filename from API first, then constructs full URL from API.defaults.baseURL + /api/pdf/file/:filename. Uses window.open(url, '_blank') synchronously (not before an async call) — avoids popup blockers per project convention.
VisibilityDialog.vue
Purpose: Change character visibility (private/public) and manage per-user sharing.
Props: characterId, currentVisibility: Boolean, showDialog: Boolean
Emits: update:showDialog, visibility-updated(isPublic)
Watches: showDialog — on open, loads available users and current shares.
API calls:
GET /api/characters/:id/available-users— users eligible to share withGET /api/characters/:id/shares(implied) — currently shared usersPUT /api/characters/:id/visibility+ share update
Features: Radio buttons for private/public. Searchable user list (filter by display name, username, email). Two-panel: "Add Users" | "Currently Shared With" with remove buttons.
DeleteCharView.vue
Purpose: Delete character confirmation panel (inside character detail submenu).
Props: character: Object, isOwner: Boolean
Emits: deleted, cancel
Behavior: Shows "not authorized" message for non-owners. For owners: confirmation text + "Yes, Delete" button → DELETE /api/characters/:id → navigates to /dashboard.
ImageUploadCropper.vue
Purpose: Upload and crop a character portrait image.
Props: characterId
Emits: image-updated
Features:
- File picker restricted to
image/* - HTML5 Canvas cropping with mouse drag selection
- Two crop shapes: rectangular or circular
- Live preview canvas (400×400)
POST /api/characters/:id/imagewith FormData containing crop coordinates
Implementation: Uses FileReader → Image → two <canvas> elements (source + preview). Mouse events track drag selection rectangle. On save, crops the selection to a new canvas and uploads as FormData.
Maintenance.vue
Purpose: Master data management hub for game system admins/maintainers.
API call: GET /api/maintenance — loads all master data at once into mdata object.
mdata shape: { skills, skillcategories, weaponskills, spells, spellcategories, equipment, weapons }
Sub-views (selected via bottom submenu):
maintenance/SkillView(default)maintenance/SpellViewmaintenance/EquipmentViewmaintenance/WeaponViewmaintenance/WeaponSkillViewmaintenance/BelieveViewmaintenance/GameSystemViewmaintenance/LitSourceViewmaintenance/MiscLookupViewmaintenance/SkillImprovementCostView
All sub-views receive mdata as prop.
maintenance/SkillView.vue
Purpose: CRUD for skill master data.
Features:
- Full text search + multi-field filters (category, difficulty, improvable, innate, bonus property)
- Sortable table (name, category columns)
- Inline row editing (click row → edit form appears in table)
- Create new skill inline
- API:
POST /api/maintenance/skills,PUT /api/maintenance/skills/:id,DELETE /api/maintenance/skills/:id
(Other maintenance views follow the same pattern for their respective domains.)
CharacterCreation.vue
Purpose: Multi-step character creation wizard.
Props: sessionId: String (required, from route)
5-step wizard:
CharacterBasicInfo— name, origin, belief, gender, race, class, social classCharacterAttributes— 7 core attributes (1–100 range, roll support)CharacterDerivedValues— calculated + adjustable secondary statsCharacterSkills— point-buy skill selection with category budgetsCharacterSpells— spell selection → finalize character
API calls:
GET /api/characters/create-session/:sessionId— load session stateGET /api/characters/skill-categories— available skill categories with point budgets- Step-specific save:
PUT /api/characters/create-session/:id/basic-infoetc. POST /api/characters/create— finalize (step 5)DELETE /api/characters/create-session/:id— delete draft
Session persistence: Each handleNext(data) saves the updated step data to the backend before incrementing currentStep. Session reloaded after each save to ensure consistent state.
Expiry info: Session expiry date shown at bottom. "Delete Draft" button available throughout.
CharacterCreation/CharacterBasicInfo.vue
Purpose: Step 1 of the wizard. Collects fundamental character identity.
Fields: Name (2–50 chars, required), Origin (select from API list), Belief/Glaube (typeahead search from API, minimum 2 chars to activate), Gender (male/female), Race (select), Class/Typ (select), Social Class (select or dice roll).
Dice roll for social class: 🎲 button (requires class to be selected first). Rolls 1d100 with class-specific modifier and maps to social class tier. Shows result in overlay that dismisses on click.
Races: loaded from backend (API call for character creation options).
Emits: next(formData), save(formData)
Validation: isValid computed checks all required fields are non-empty.
CharacterCreation/CharacterAttributes.vue
Purpose: Step 2 — set the 7 base attributes (St, Gs, Gw, Ko, In, Zt, Au) each 1–100.
Features:
- Individual roll buttons using
max(2d100)formula per attribute (except Au) - 🎲 "Roll All" button
- Race-specific Beauty (Au) restrictions: Elves ≥ 81, Gnomes/Dwarves ≤ 80
- Shows total points sum and average
- Disabled state display (shows "ENABLED"/"DISABLED" status indicator)
CharacterCreation/CharacterDerivedValues.vue
Purpose: Step 3 — calculate and optionally override secondary stats.
Fields: PA (personal charisma), WK (willpower), LP max (life points), AP max (adventure points), B max (movement), resistances, defense, bonuses, SG (fate's favor), GG (divine grace), GP (luck points).
Dice formulas displayed in i18n keys:
- PA = 1d100 + 4×(In/10) − 20
- WK = 1d100 + 2×(Ko/10 + In/10) − 20
- LP = 1d3 + 7 + (Ko/10)
- AP = 3d6 + class modifier
- B = 1d6 + race modifier
Recalculate button: Calls backend to recalculate from attributes.
Individual roll buttons per derived value with tooltip showing formula.
CharacterCreation/CharacterSkills.vue
Purpose: Step 4 — point-buy skill selection with category budgets.
Props: sessionData, skillCategories
Each category has a point budget (max_points). Skills have costs. Emits next, previous, save.
CharacterCreation/CharacterSpells.vue
Purpose: Step 5 — optional spell selection before finalization.
Props: sessionData, skillCategories
Emits: previous, finalize(finalData), save
Finalize calls POST /api/characters/create with complete session data. On success navigates to the new character's detail view.
AusruestungList.vue (legacy)
Purpose: Simple standalone equipment list (old design). Loaded via /ausruestung/:characterId route.
API: GET /ausruestung/:characterId (note: no /api prefix — likely a legacy endpoint).
Sub-component: AusruestungForm
AusruestungForm.vue (legacy)
Simple form to add equipment name/quantity/weight. Posts to /ausruestung (no /api prefix).
LanguageSwitcher.vue
Marked as "not used anymore" in a comment in the template. A simple <select> that calls languageStore.setLanguage(). Language switching is now done via UserProfileView.
HelloWorld.vue, TheWelcome.vue, WelcomeItem.vue
Vite/Vue scaffolding boilerplate — not used in the application.
7. API Layer
src/utils/api.js
import axios from 'axios'
const API = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'https://bamort-api.trokan.de'
})
// Request interceptor
API.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// Response interceptor
API.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
// redirect to /login is commented out
}
return Promise.reject(error)
}
)
export default API
Key facts:
- Default base URL:
https://bamort-api.trokan.de(production endpoint). In dev containers the env var overrides this tohttp://localhost:8180. - JWT is stored in and retrieved from
localStorage.getItem('token'). - 401 handler removes the token but does not automatically redirect (commented out). Components handle 401 errors individually.
API.defaults.baseURLis used inExportDialogto construct the PDF file download URL.
src/utils/auth.js
export function isLoggedIn() { return !!localStorage.getItem("token") }
export function logout() { localStorage.removeItem("token") }
Simple token-presence check. No JWT expiry validation, no PKCE, no refresh token mechanism.
All API endpoints used by the frontend
| Method | Endpoint | Used by |
|---|---|---|
POST |
/login |
LoginForm |
POST |
/register |
RegisterForm |
POST |
/password-reset/request |
ForgotPasswordForm |
GET |
/api/public/version |
LandingView |
GET |
/api/public/systeminfo |
SystemInfoView |
GET |
/api/user/profile |
userStore, UserProfileView |
PUT |
/api/user/display-name |
UserProfileView |
PUT |
/api/user/language |
UserProfileView |
PUT |
/api/user/email |
UserProfileView |
PUT |
/api/user/password |
UserProfileView |
GET |
/api/users |
UserManagementView |
PUT |
/api/users/:id/role |
UserManagementView |
DELETE |
/api/users/:id |
UserManagementView |
PUT |
/api/users/:id/password |
UserManagementView |
GET |
/api/characters |
CharacterList |
GET |
/api/characters/:id |
CharacterDetails |
PUT |
/api/characters/:id |
DatasheetView |
DELETE |
/api/characters/:id |
DeleteCharView |
GET |
/api/characters/:id/datasheet-options |
DatasheetView |
GET |
/api/characters/:id/image |
DatasheetView (imageSrc) |
POST |
/api/characters/:id/image |
ImageUploadCropper |
GET |
/api/characters/:id/audit-log |
AuditLogView |
GET |
/api/characters/:id/available-users |
VisibilityDialog |
POST |
/api/characters/create-session |
CharacterList |
GET |
/api/characters/create-sessions |
CharacterList |
GET |
/api/characters/create-session/:id |
CharacterCreation |
DELETE |
/api/characters/create-session/:id |
CharacterList, CharacterCreation |
GET |
/api/characters/skill-categories |
CharacterCreation |
POST |
/api/characters/improve-skill |
SkillView |
POST |
/api/characters/practice-points |
SkillView |
GET |
/api/pdf/templates |
ExportDialog |
GET |
/api/pdf/export/:id |
ExportDialog |
GET |
/api/pdf/file/:filename |
ExportDialog |
GET |
/api/importer/export/vtt/:id/file |
ExportDialog |
POST |
/api/importer/upload |
FileUploadPage |
GET |
/api/transfer/download/:id |
ExportDialog |
GET |
/api/equipment (maintenance) |
EquipmentView |
POST |
/api/equipment |
EquipmentView, WeaponView |
DELETE |
/api/equipment/:id |
EquipmentView, WeaponView |
PUT |
/api/equipment/:id |
WeaponView |
GET |
/api/maintenance |
Maintenance |
POST/PUT/DELETE |
/api/maintenance/skills |
maintenance/SkillView |
8. i18n
Configuration
- Library:
vue-i18nv11 (Composition API mode:legacy: false) - Default locale:
de(German), persisted inlocalStorage.language - Fallback locale:
en - Instance created in
languageStore.jsand exported asi18n
Languages
Two supported languages: German (de) and English (en).
Both locales are JavaScript objects (not JSON), exported as ES module defaults from single files:
src/locales/de(no.jsextension)src/locales/en(no.jsextension)
Translation Key Structure
| Top-level key | Domain |
|---|---|
common |
Shared labels: loading, cancel, save, edit, back, next |
auth |
Login/Registration form labels and messages |
forgotPassword |
Password reset flow |
import |
File import page |
menu |
Navigation menu items |
landing |
Landing page content |
equipment |
Equipment table, dialogs, messages |
skill |
Skill table headers and field labels |
spell |
Spell table headers and field labels |
weapon |
Weapon table, add/edit dialogs |
weaponskill |
Weapon skill table |
spells.learn |
Spell learning dialog |
stats |
Abbreviated stat names (St, Gs, Gw, Ko, In, Zt, Au, pA, Wk...) |
chars / singular view names |
View tab labels |
characters.list |
Dashboard character list UI |
characters.create |
Character wizard UI |
characters.basicInfo |
Wizard step 1: basic info |
characters.attributes |
Wizard step 2: attributes |
characters.derivedValues |
Wizard step 3: derived values |
characters.datasheet |
Datasheet edit hint |
experience |
EP + wealth section |
audit |
Audit log labels and filters |
export |
Export dialog labels and messages |
visibility |
Visibility/sharing dialog |
userManagement |
Admin user management |
profile |
User profile page |
deleteChar |
Delete character authorization message |
maintenance |
Maintenance section label |
maintmenu |
Maintenance sub-menu items |
believe |
Belief master data management |
gamesystem |
Game system master data |
litsource |
Literature source master data |
misc |
Misc lookup master data |
skillimprovement |
Skill improvement cost master data |
help |
Help page content + FAQ keys |
sponsors |
Sponsors page content |
systemInfo |
System info page content |
Note: Some keys exist only partially in EN (e.g., spells.learn labels use German text in the EN locale — incomplete translation).
Language switching
- Via
UserProfileView→PUT /api/user/language→ updatesthis.$i18n.localeandlocalStorage - Via
userStore.fetchCurrentUser()→ automatically appliesprofile.preferred_languageif set - The
LanguageSwitcher.vuecomponent exists but is marked unused.
9. Styling
Architecture
Global-first approach: All shared styles live in src/assets/main.css (imported in main.js as a side-effect import). Components use <style> (unscoped, contributing to global scope) or <style scoped> only for component-specific overrides.
Convention from vue.instructions.md: <template>, then <style scoped>, then <script> (template-first order, opposite of Vite default).
CSS Files
src/assets/base.css — CSS custom properties (variables):
:root {
--vt-c-black-soft: #222222; /* nav background */
--color-primary: #007bff; /* primary blue */
--color-bg-secondary: #f8f9fa;
--color-text-primary: #333;
--color-text-secondary: #495057;
--padding-xs/sm/md/lg /* 4/8/16/24px */
--margin-xs/sm/md/lg /* 4/8/16/24px */
--border-radius: 6px;
--box-shadow-light: 0 2px 4px rgba(0,0,0,0.1);
}
Dark mode support via @media (prefers-color-scheme: dark).
src/assets/main.css — Application shell and shared component styles:
Key CSS classes defined globally:
.top-nav— fixed 60px navigation bar (black background).main-content— flex-grow content area with top+bottom padding for fixed nav+submenu.fullwidth-container— full-width component wrapper with bottom padding for submenu.card— white bordered box with hover animation.grid-container,.grid-2/3/4-columns— responsive CSS grid layouts.page-header— section header with blue 2px bottom border.section-header— subsection header.submenu— fixed bottom tab bar for character/maintenance view switching.cd-table,.cd-table-header— data tables (green headers#1da766).list-item,.list-container,.charlist— character list cards.modal-overlay,.modal-content,.modal-header/body/footer— modal dialogs.form-group,.form-control,.form-row— form layout.btn,.btn-primary,.btn-secondary,.btn-danger,.btn-success— button variants.badge,.badge-success,.badge-danger,.badge-warning,.badge-secondary— status pills.loading,.empty-state— feedback states
Accent colour: #1da766 (green) used for table headers, learning mode buttons, active nav items.
Primary colour: #007bff (blue) used for page headers, primary buttons, links.
Component-specific styles
Most components either have no <style> block or minimal overrides. Examples:
SkillView.vue—@keyframes slideInfor learning mode animationEquipmentView.vue—.modal-fullscreenflex layout fixForgotPasswordForm.vue— local.reset-*classes for that form layout
10. Business Logic in Frontend
Form Validation
| Location | Validation |
|---|---|
RegisterForm |
Password == confirmPassword (client), email type input |
UserProfileView |
Display name max 30 chars, email != current, passwords min 6 chars + match |
UserManagementView |
Passwords min 6 chars + match |
CharacterBasicInfo |
All required fields non-empty (isValid computed) |
CharacterAttributes |
Race-specific beauty restrictions (Elves ≥81, Gnomes/Dwarves ≤80) |
FileUploadPage |
MIME type check for .json/.csv files |
SkillImproveDialog |
Must select at least one level, must be able to afford it |
SkillLearnDialog |
Skills must be affordable (EP + Gold check) |
Data Transformation
Character stat access (DatasheetView.getStat):
getStat(path) {
return path.split('.').reduce((obj, key) => obj?.[key], this.character) ?? '-'
}
Deep path traversal on nested character object. Same pattern for saving via pathParts loop.
Glaube (beliefs) encoding: Multi-select array ↔ comma-separated string conversion in DatasheetView.
Currency conversion (ExperianceView):
totalWealthInGS() {
return goldstücke + Math.floor(silberstücke / 10) + Math.floor(kupferstücke / 10)
}
Midgard RPG conversion: 1 GS = 10 SS = 10 KS.
Skill level improvement cost computation: Delegated to backend via API — frontend only displays what the server returns.
Character creation dice rolls:
rollDie(100)for attributes- Social class:
$rollDice(1, 100)+ class modifier → lookup table for result - Derived values: formulas like
1d100 + 4×(In/10) - 20for PA;1d3 + 7 + Ko/10for LP
Computed State
| Component | Computed | Logic |
|---|---|---|
LandingView |
isBackendAvailable |
backend version not in error states |
ExportDialog |
canExport |
format selected AND (if PDF) template selected |
VisibilityDialog |
filteredAvailableUsers |
exclude already-shared users, apply search query |
ExperianceView |
totalWealthInGS |
currency conversion formula |
CharacterDetails |
isOwner |
character.user_id === userStore.currentUser.id |
SkillImproveDialog |
remainingEP, remainingGold, remainingPP |
current resources minus selected cost |
SkillLearnDialog |
sortedFilteredSkills |
filter by category + search, sort by name/cost |
CharacterBasicInfo |
isValid |
all required fields filled |
userStore |
isAdmin, isMaintainer |
role string comparisons |
Auth State Management
Auth state is managed entirely via localStorage.token. No reactive store for the token itself — components call isLoggedIn() directly at mount time. The App.vue listens to storage events (cross-tab) and a custom auth-changed event (same-tab) to update the loggedIn data property and show/hide the navigation menu.
11. User Flows
Login Flow
/ (Landing) → click "Zum Login"
→ /login → LoginForm
→ POST /login { username, password }
→ JWT stored in localStorage.token
→ userStore.fetchCurrentUser() (GET /api/user/profile)
→ window.dispatchEvent('auth-changed')
→ App.vue re-checks auth, shows Menu
→ router.push('/dashboard')
→ /dashboard → DashboardView → CharacterList
Registration Flow
/register → RegisterForm
→ validates passwords match
→ POST /register { username, password, email }
→ shows success badge
→ user manually navigates to /login
Password Reset Flow
/forgot-password → ForgotPasswordForm
→ POST /password-reset/request { email, redirect_url }
→ shows success state (no redirect)
→ user receives email with link
→ link leads to /reset-password?token=X
→ ResetPasswordForm → POST /password-reset/confirm
Character List / Dashboard Flow
/dashboard → CharacterList
→ GET /api/characters → { self_owned, others }
→ GET /api/characters/create-sessions → { sessions }
→ renders owned + shared character links
→ renders active creation session cards
Character Creation Flow (New Character)
Dashboard → "Create New Character"
→ POST /api/characters/create-session → { session_id }
→ router.push('/character/create/:sessionId')
/character/create/:sessionId → CharacterCreation
→ GET /api/characters/create-session/:id (load state)
→ GET /api/characters/skill-categories (load skill budgets)
Step 1: CharacterBasicInfo
→ fill name, origin, belief, gender, race, class, social class
→ "Next: Attributes →"
→ PUT /api/characters/create-session/:id/step1 (save)
Step 2: CharacterAttributes
→ set 7 attributes (1–100), optionally roll dice
→ "Next: Derived Values →"
→ PUT /api/characters/create-session/:id/step2
Step 3: CharacterDerivedValues
→ view/adjust calculated values, optionally recalculate
→ "Next: Skills →"
→ PUT /api/characters/create-session/:id/step3
Step 4: CharacterSkills
→ select skills from category-budgeted list
→ "Next: Spells →"
→ PUT /api/characters/create-session/:id/step4
Step 5: CharacterSpells
→ optionally select spells
→ "Create Character" → POST /api/characters/create
→ router.push('/character/:newId')
Sessions can be resumed from the dashboard ("Continue Character Creation") or deleted.
Character Management Flow
/character/:id → CharacterDetails
→ GET /api/characters/:id
Tab: DatasheetView (default)
→ displays stats + bio info
→ double-click any field to edit inline
→ blur/Enter → PUT /api/characters/:id
Tab: SkillView
→ displays categorized skills + weapon skills + innate skills
→ owner: PP adjustment (+/− buttons)
→ owner: learning mode toggle (🎓)
→ SkillLearnDialog: learn new skill
→ SkillImproveDialog: improve existing skill
→ inline add skill dialog
Tab: WeaponView
→ displays weapons table
→ owner: add weapon (search + select from master data)
→ owner: edit weapon (magical, bonuses, value)
→ owner: delete weapon
Tab: SpellView
→ displays spells table
→ owner: 🎓 → SpellLearnDialog
Tab: EquipmentView
→ displays equipment table
→ owner: add equipment (search + select from master data)
→ owner: delete equipment
Tab: ExperianceView
→ EP display + add/remove
→ Gold display + add/remove
→ AuditLogView (filterable change history)
Tab: DeleteCharView
→ owner only: confirm + DELETE /api/characters/:id → /dashboard
PDF Export Flow
CharacterDetails → 📄 button → ExportDialog
Step 1: Load templates (GET /api/pdf/templates)
Step 2: Select format (PDF / VTT / BaMoRT JSON)
Step 3: If PDF → select template + optional showUserName checkbox
Step 4: Click "Export"
PDF path:
GET /api/pdf/export/:id?template=X → { filename }
window.open(API.baseURL + /api/pdf/file/filename, '_blank')
VTT path:
GET /api/importer/export/vtt/:id/file (blob)
→ programmatic <a download> click
BaMoRT JSON path:
GET /api/transfer/download/:id (blob)
→ programmatic <a download> click
Visibility / Sharing Flow
CharacterDetails → 🌐/🔒 button (owner only) → VisibilityDialog
→ GET /api/characters/:id/available-users
→ GET /api/characters/:id/shares (current)
→ Toggle private/public radio
→ Search + add users from available list
→ Remove users from shared list
→ "Save" → PUT /api/characters/:id/visibility + share update
→ emits visibility-updated → CharacterDetails updates character.public
Admin User Management Flow
Menu Admin → "User Management" (admin only)
→ /users → UserManagementView
→ GET /api/users
→ table of all users
Per user (excluding self):
"Change Role" → modal → select role → PUT /api/users/:id/role
"Change Password" → modal → new password → PUT /api/users/:id/password
"Delete" → confirm modal → DELETE /api/users/:id
12. Component Hierarchy
App.vue
├── Menu.vue [if logged in]
└── <router-view> (main content)
├── LandingView.vue [/]
├── LoginView.vue [/login]
│ └── LoginForm.vue
├── RegisterView.vue [/register]
│ └── RegisterForm.vue
├── ForgotPasswordView.vue [/forgot-password]
│ └── ForgotPasswordForm.vue
├── ResetPasswordView.vue [/reset-password]
│ └── ResetPasswordForm.vue
├── DashboardView.vue [/dashboard] *auth*
│ └── CharacterList.vue
│ └── CharacterCreationSessions.vue
├── UserProfileView.vue [/profile] *auth*
├── UserManagementView.vue [/users] *admin*
├── FileUploadPage.vue [/upload] *auth*
├── MaintenanceView.vue [/maintenance] *auth*
│ └── Maintenance.vue
│ ├── maintenance/SkillView.vue (default)
│ ├── maintenance/SpellView.vue
│ ├── maintenance/EquipmentView.vue
│ ├── maintenance/WeaponView.vue
│ ├── maintenance/WeaponSkillView.vue
│ ├── maintenance/BelieveView.vue
│ ├── maintenance/GameSystemView.vue
│ ├── maintenance/LitSourceView.vue
│ ├── maintenance/MiscLookupView.vue
│ └── maintenance/SkillImprovementCostView.vue
├── AusruestungView.vue [/ausruestung/:characterId] *auth*
│ └── AusruestungList.vue
│ └── AusruestungForm.vue
├── SponsorsView.vue [/sponsors]
├── HelpView.vue [/help]
├── SystemInfoView.vue [/system-info]
├── CharacterDetails.vue [/character/:id] *auth*
│ ├── ExportDialog.vue
│ ├── VisibilityDialog.vue
│ └── <dynamic component :is="currentView">
│ ├── DatasheetView.vue (default)
│ │ └── ImageUploadCropper.vue
│ ├── SkillView.vue
│ │ ├── SkillLearnDialog.vue
│ │ └── SkillImproveDialog.vue
│ ├── WeaponView.vue
│ ├── SpellView.vue
│ │ └── SpellLearnDialog.vue
│ ├── EquipmentView.vue
│ ├── ExperianceView.vue
│ │ └── AuditLogView.vue
│ └── DeleteCharView.vue
└── CharacterCreation.vue [/character/create/:sessionId] *auth*
├── CharacterCreation/CharacterBasicInfo.vue (step 1)
├── CharacterCreation/CharacterAttributes.vue (step 2)
├── CharacterCreation/CharacterDerivedValues.vue (step 3)
├── CharacterCreation/CharacterSkills.vue (step 4)
└── CharacterCreation/CharacterSpells.vue (step 5)
Component-to-Parent Emits Summary
| Component | Emits | Consumed by |
|---|---|---|
CharacterCreationSessions |
continue-session, delete-session |
CharacterList |
ExportDialog |
update:showDialog, export-success |
CharacterDetails |
VisibilityDialog |
update:showDialog, visibility-updated |
CharacterDetails |
DatasheetView |
character-updated |
CharacterDetails |
SkillView |
character-updated |
CharacterDetails |
WeaponView |
character-updated |
CharacterDetails |
EquipmentView |
character-updated |
CharacterDetails |
SpellView |
character-updated |
CharacterDetails |
SkillLearnDialog |
close, skill-learned |
SkillView |
SkillImproveDialog |
close, skill-updated |
SkillView |
SpellLearnDialog |
close, spell-learned |
SpellView |
ImageUploadCropper |
image-updated |
DatasheetView |
DeleteCharView |
deleted, cancel |
CharacterDetails |
AusruestungForm |
added |
AusruestungList |
CharacterBasicInfo |
next, save |
CharacterCreation |
CharacterAttributes |
next, previous, save |
CharacterCreation |
CharacterDerivedValues |
next, previous, save |
CharacterCreation |
CharacterSkills |
next, previous, save |
CharacterCreation |
CharacterSpells |
previous, finalize, save |
CharacterCreation |