feat(falukant): add character visual representation and settings
- Introduced FalukantCharacterVisual component to display character portraits and 3D figures based on user settings. - Updated DirectorInfo, ChurchView, PoliticsView, and other components to utilize the new character visual component. - Added visual settings management for users, allowing selection between portrait styles and display modes (portraits, 3D, or both). - Implemented age rules for engagement and marriage in the backend, ensuring compliance with specified age limits. - Created a new settings view for Falukant to manage visual preferences. - Added new images for medieval character portraits. - Updated translations for new settings and features in multiple languages.
This commit is contained in:
@@ -68,6 +68,7 @@ const TITLE_MAP = {
|
||||
'Flirt settings': 'sectionBar.titles.flirtSettings',
|
||||
'Account settings': 'sectionBar.titles.accountSettings',
|
||||
'Language assistant settings': 'sectionBar.titles.languageAssistantSettings',
|
||||
'Falukant settings': 'sectionBar.titles.falukantSettings',
|
||||
Interests: 'sectionBar.titles.interests',
|
||||
AdminInterests: 'sectionBar.titles.adminInterests',
|
||||
AdminUsers: 'sectionBar.titles.adminUsers',
|
||||
|
||||
@@ -4,51 +4,51 @@
|
||||
<tr v-for="setting in settings" :key="setting.id">
|
||||
<td>
|
||||
<InputStringWidget v-if="setting.datatype == 'string'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
||||
:labelTr="settingLabelTr(setting.name)"
|
||||
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||
:value=setting.value
|
||||
:disabled="setting.immutable && setting.value ? true : false"
|
||||
:list="languagesList()" @input="handleInput(setting.id, $event)" />
|
||||
|
||||
<DateInputWidget v-else-if="setting.datatype == 'date'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
||||
:labelTr="settingLabelTr(setting.name)"
|
||||
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||
:value=setting.value
|
||||
:disabled="setting.immutable && setting.value ? true : false"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<SelectDropdownWidget v-else-if="setting.datatype == 'singleselect'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
||||
:labelTr="settingLabelTr(setting.name)"
|
||||
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||
:value=setting.value
|
||||
:disabled="setting.immutable && setting.value ? true : false"
|
||||
:list="getSettingOptions(setting.name, setting.options)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<InputNumberWidget v-else-if="setting.datatype == 'int'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
||||
:labelTr="settingLabelTr(setting.name)"
|
||||
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||
:value="convertToInt(setting.value)"
|
||||
:disabled="setting.immutable && setting.value ? true : false"
|
||||
min="0" max="200" @input="handleInput(setting.id, $event)" />
|
||||
|
||||
<FloatInputWidget v-else-if="setting.datatype == 'float'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
||||
:labelTr="settingLabelTr(setting.name)"
|
||||
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||
:value="convertToFloat(setting.value)"
|
||||
:disabled="setting.immutable && setting.value ? true : false"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<CheckboxWidget v-else-if="setting.datatype == 'bool'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
||||
:labelTr="settingLabelTr(setting.name)"
|
||||
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||
:value="convertToBool(setting.value)"
|
||||
:disabled="setting.immutable && setting.value ? true : false"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<MultiselectWidget v-else-if="setting.datatype == 'multiselect'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
||||
:labelTr="settingLabelTr(setting.name)"
|
||||
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||
:value="setting.value"
|
||||
:disabled="setting.immutable && setting.value ? true : false"
|
||||
:list="getSettingOptions(setting.name, setting.options)"
|
||||
@@ -144,10 +144,39 @@ export default {
|
||||
return options.map((option) => {
|
||||
return {
|
||||
value: option.id,
|
||||
captionTr: `settings.personal.${fieldName}.${option.value}`
|
||||
captionTr: this.settingOptionTr(fieldName, option.value)
|
||||
}
|
||||
});
|
||||
},
|
||||
settingLabelTr(fieldName) {
|
||||
return this.resolveSettingTr([
|
||||
`settings.${this.settingsType}.label.${fieldName}`,
|
||||
`settings.personal.label.${fieldName}`
|
||||
]);
|
||||
},
|
||||
settingTooltipTr(fieldName, immutable) {
|
||||
if (immutable) {
|
||||
return this.$t('settings.immutable.tooltip');
|
||||
}
|
||||
return this.resolveSettingTr([
|
||||
`settings.${this.settingsType}.tooltip.${fieldName}`,
|
||||
`settings.personal.tooltip.${fieldName}`
|
||||
]);
|
||||
},
|
||||
settingOptionTr(fieldName, optionValue) {
|
||||
return this.resolveSettingTr([
|
||||
`settings.${this.settingsType}.${fieldName}.${optionValue}`,
|
||||
`settings.personal.${fieldName}.${optionValue}`
|
||||
]);
|
||||
},
|
||||
resolveSettingTr(candidates) {
|
||||
for (const key of candidates) {
|
||||
if (this.$te(key)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return candidates[0];
|
||||
},
|
||||
async handleInput(settingId, value) {
|
||||
if (['object', 'array'].includes(typeof value)) {
|
||||
return;
|
||||
@@ -232,7 +261,7 @@ export default {
|
||||
}
|
||||
|
||||
const fieldNames = immutableFields.map(field =>
|
||||
this.$t(`settings.personal.label.${field.name}`)
|
||||
this.$t(this.settingLabelTr(field.name))
|
||||
).join(', ');
|
||||
|
||||
return this.$t('settings.immutable.supportMessage.specific', { fields: fieldNames });
|
||||
|
||||
@@ -9,6 +9,16 @@
|
||||
<div v-else class="director-info-container">
|
||||
<!-- Linke Seite: Stammdaten & Wissen (aus /falukant/directors) -->
|
||||
<div class="director-main">
|
||||
<div class="director-visual" v-if="director.character">
|
||||
<FalukantCharacterVisual
|
||||
:gender="director.character.gender"
|
||||
:age="director.character.age"
|
||||
:visual-settings="visualSettings"
|
||||
:portrait-scale="0.58"
|
||||
:figure-width="124"
|
||||
:figure-height="156"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="director-name">
|
||||
{{ $t('falukant.titles.' + director.character.gender + '.' + director.character.nobleTitle.labelTr) }}
|
||||
{{ director.character.definedFirstName.name }} {{ director.character.definedLastName.name }}
|
||||
@@ -192,7 +202,9 @@
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
|
||||
import FalukantCharacterVisual from '@/components/falukant/FalukantCharacterVisual.vue';
|
||||
import { showError, showInfo, showSuccess } from '@/utils/feedback.js';
|
||||
import { FALUKANT_VISUAL_DEFAULTS } from '@/utils/falukantVisualSettings.js';
|
||||
|
||||
export default {
|
||||
name: "DirectorInfo",
|
||||
@@ -200,9 +212,11 @@ export default {
|
||||
branchId: { type: Number, required: true },
|
||||
vehicles: { type: Array, default: () => [] },
|
||||
branches: { type: Array, default: () => [] },
|
||||
visualSettings: { type: Object, default: () => ({ ...FALUKANT_VISUAL_DEFAULTS }) },
|
||||
},
|
||||
components: {
|
||||
NewDirectorDialog
|
||||
NewDirectorDialog,
|
||||
FalukantCharacterVisual,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -484,6 +498,10 @@ export default {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.director-visual {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.director-actions {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
189
frontend/src/components/falukant/FalukantCharacterVisual.vue
Normal file
189
frontend/src/components/falukant/FalukantCharacterVisual.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div v-if="shouldRender" class="falukant-character-visual" :class="{ 'is-compact': compact }">
|
||||
<div
|
||||
v-if="showPortrait"
|
||||
class="falukant-character-visual__portrait"
|
||||
:style="portraitStyle"
|
||||
:aria-label="`${gender || 'male'} portrait`"
|
||||
/>
|
||||
<div v-if="showFigure" class="falukant-character-visual__figure" :style="figureStyle">
|
||||
<Character3D
|
||||
:gender="normalizedGender"
|
||||
:age="normalizedAge"
|
||||
:lightweight="lightweight"
|
||||
:noBackground="noBackground"
|
||||
:lazy="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Character3D from '@/components/Character3D.vue';
|
||||
import {
|
||||
FALUKANT_VISUAL_DEFAULTS,
|
||||
resolveFalukantPortraitAtlas,
|
||||
showFalukant3dFigures,
|
||||
showFalukantPortraits,
|
||||
} from '@/utils/falukantVisualSettings.js';
|
||||
|
||||
const AVATAR_POSITIONS = {
|
||||
male: {
|
||||
width: 195,
|
||||
height: 300,
|
||||
positions: {
|
||||
'0-1': { x: 161, y: 28 },
|
||||
'2-3': { x: 802, y: 28 },
|
||||
'4-6': { x: 1014, y: 28 },
|
||||
'7-10': { x: 800, y: 368 },
|
||||
'11-13': { x: 373, y: 368 },
|
||||
'14-16': { x: 1441, y: 28 },
|
||||
'17-20': { x: 1441, y: 368 },
|
||||
'21-30': { x: 1014, y: 368 },
|
||||
'31-45': { x: 1227, y: 368 },
|
||||
'45-55': { x: 803, y: 687 },
|
||||
'55+': { x: 1441, y: 687 },
|
||||
},
|
||||
},
|
||||
female: {
|
||||
width: 223,
|
||||
height: 298,
|
||||
positions: {
|
||||
'0-1': { x: 302, y: 66 },
|
||||
'2-3': { x: 792, y: 66 },
|
||||
'4-6': { x: 62, y: 66 },
|
||||
'7-10': { x: 1034, y: 66 },
|
||||
'11-13': { x: 1278, y: 66 },
|
||||
'14-16': { x: 303, y: 392 },
|
||||
'17-20': { x: 1525, y: 392 },
|
||||
'21-30': { x: 1278, y: 392 },
|
||||
'31-45': { x: 547, y: 718 },
|
||||
'45-55': { x: 1034, y: 718 },
|
||||
'55+': { x: 1525, y: 718 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'FalukantCharacterVisual',
|
||||
components: {
|
||||
Character3D,
|
||||
},
|
||||
props: {
|
||||
gender: {
|
||||
type: String,
|
||||
default: 'male',
|
||||
},
|
||||
age: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
visualSettings: {
|
||||
type: Object,
|
||||
default: () => ({ ...FALUKANT_VISUAL_DEFAULTS }),
|
||||
},
|
||||
portraitScale: {
|
||||
type: Number,
|
||||
default: 0.42,
|
||||
},
|
||||
figureWidth: {
|
||||
type: Number,
|
||||
default: 92,
|
||||
},
|
||||
figureHeight: {
|
||||
type: Number,
|
||||
default: 118,
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
lightweight: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
noBackground: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
normalizedGender() {
|
||||
return String(this.gender || 'male').toLowerCase() === 'female' ? 'female' : 'male';
|
||||
},
|
||||
normalizedAge() {
|
||||
return Number.isFinite(this.age) ? this.age : null;
|
||||
},
|
||||
showPortrait() {
|
||||
return showFalukantPortraits(this.visualSettings?.figureMode);
|
||||
},
|
||||
showFigure() {
|
||||
return showFalukant3dFigures(this.visualSettings?.figureMode);
|
||||
},
|
||||
shouldRender() {
|
||||
return this.showPortrait || this.showFigure;
|
||||
},
|
||||
figureStyle() {
|
||||
return {
|
||||
width: `${this.figureWidth}px`,
|
||||
height: `${this.figureHeight}px`,
|
||||
};
|
||||
},
|
||||
portraitStyle() {
|
||||
const genderData = AVATAR_POSITIONS[this.normalizedGender] || AVATAR_POSITIONS.male;
|
||||
const position = genderData.positions?.[this.ageGroup] || { x: 0, y: 0 };
|
||||
const safeScale = Number(this.portraitScale) > 0 ? Number(this.portraitScale) : 1;
|
||||
return {
|
||||
backgroundImage: `url(${resolveFalukantPortraitAtlas(this.normalizedGender, this.visualSettings?.portraitStyle)})`,
|
||||
backgroundPosition: `-${Math.round(position.x * safeScale)}px -${Math.round(position.y * safeScale)}px`,
|
||||
backgroundSize: `${Math.round(1792 * safeScale)}px ${Math.round(1024 * safeScale)}px`,
|
||||
width: `${Math.round(genderData.width * safeScale)}px`,
|
||||
height: `${Math.round(genderData.height * safeScale)}px`,
|
||||
};
|
||||
},
|
||||
ageGroup() {
|
||||
const age = this.normalizedAge;
|
||||
if (age == null || age <= 1) return '0-1';
|
||||
if (age <= 3) return '2-3';
|
||||
if (age <= 6) return '4-6';
|
||||
if (age <= 10) return '7-10';
|
||||
if (age <= 13) return '11-13';
|
||||
if (age <= 16) return '14-16';
|
||||
if (age <= 20) return '17-20';
|
||||
if (age <= 30) return '21-30';
|
||||
if (age <= 45) return '31-45';
|
||||
if (age <= 55) return '45-55';
|
||||
return '55+';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.falukant-character-visual {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.falukant-character-visual__portrait {
|
||||
background-repeat: no-repeat;
|
||||
background-color: rgba(255, 250, 242, 0.9);
|
||||
border: 1px solid rgba(116, 85, 49, 0.22);
|
||||
border-radius: 0.7rem;
|
||||
box-shadow: 0 8px 18px rgba(46, 28, 14, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.falukant-character-visual__figure {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.is-compact {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user