feat(falukant): add character visual representation and settings
All checks were successful
Deploy to production / deploy (push) Successful in 2m53s
All checks were successful
Deploy to production / deploy (push) Successful in 2m53s
- 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:
@@ -80,6 +80,7 @@
|
||||
:branchId="selectedBranch.id"
|
||||
:vehicles="vehicles"
|
||||
:branches="branches"
|
||||
:visual-settings="visualSettings"
|
||||
ref="directorInfo"
|
||||
@transportCreated="handleTransportCreated"
|
||||
/>
|
||||
@@ -386,6 +387,10 @@ import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapState } from 'vuex';
|
||||
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
|
||||
import {
|
||||
FALUKANT_VISUAL_DEFAULTS,
|
||||
loadFalukantVisualSettings,
|
||||
} from '@/utils/falukantVisualSettings.js';
|
||||
|
||||
const CERTIFICATE_PRODUCT_LEVELS = [
|
||||
{ level: 1, products: ['fish', 'meat', 'leather', 'wood', 'stone', 'milk', 'cheese', 'bread', 'wheat', 'grain', 'carrot'] },
|
||||
@@ -476,6 +481,7 @@ export default {
|
||||
inDebtorsPrison: false
|
||||
},
|
||||
pendingBranchRefresh: null,
|
||||
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||
};
|
||||
},
|
||||
|
||||
@@ -528,6 +534,7 @@ export default {
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||
await this.loadBranches();
|
||||
|
||||
const branchId = this.$route.params.branchId;
|
||||
|
||||
@@ -41,10 +41,23 @@
|
||||
<tr v-for="person in baptismList" :key="person.id">
|
||||
<td>{{ $t(`falukant.church.baptism.gender.${person.gender}`) }}</td>
|
||||
<td>
|
||||
<input type="text" v-model="person.proposedFirstName" />
|
||||
<button @click="newName(person)">
|
||||
{{ $t('falukant.church.baptism.table.newName') }}
|
||||
</button>
|
||||
<div class="church-person-cell">
|
||||
<FalukantCharacterVisual
|
||||
:gender="person.gender"
|
||||
:age="person.age"
|
||||
:visual-settings="visualSettings"
|
||||
:portrait-scale="0.32"
|
||||
:figure-width="68"
|
||||
:figure-height="88"
|
||||
:compact="true"
|
||||
/>
|
||||
<div class="church-person-cell__content">
|
||||
<input type="text" v-model="person.proposedFirstName" />
|
||||
<button @click="newName(person)">
|
||||
{{ $t('falukant.church.baptism.table.newName') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ person.age }}</td>
|
||||
<td>
|
||||
@@ -76,8 +89,21 @@
|
||||
<td>{{ pos.region.name }}</td>
|
||||
<td>
|
||||
<span v-if="pos.character">
|
||||
{{ $t(`falukant.titles.${pos.character.gender}.${pos.character.title || 'noncivil'}`) }}
|
||||
{{ pos.character.name }}
|
||||
<div class="church-person-cell">
|
||||
<FalukantCharacterVisual
|
||||
:gender="pos.character.gender"
|
||||
:age="pos.character.age"
|
||||
:visual-settings="visualSettings"
|
||||
:portrait-scale="0.32"
|
||||
:figure-width="68"
|
||||
:figure-height="88"
|
||||
:compact="true"
|
||||
/>
|
||||
<div class="church-person-cell__content">
|
||||
{{ $t(`falukant.titles.${pos.character.gender}.${pos.character.title || 'noncivil'}`) }}
|
||||
{{ pos.character.name }}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
@@ -187,7 +213,12 @@ import MessageDialog from '@/dialogues/standard/MessageDialog.vue'
|
||||
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue'
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import SimpleTabs from '@/components/SimpleTabs.vue'
|
||||
import FalukantCharacterVisual from '@/components/falukant/FalukantCharacterVisual.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import {
|
||||
FALUKANT_VISUAL_DEFAULTS,
|
||||
loadFalukantVisualSettings,
|
||||
} from '@/utils/falukantVisualSettings.js'
|
||||
|
||||
export default {
|
||||
name: 'ChurchView',
|
||||
@@ -196,6 +227,7 @@ export default {
|
||||
MessageDialog,
|
||||
ErrorDialog,
|
||||
SimpleTabs,
|
||||
FalukantCharacterVisual,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -215,7 +247,8 @@ export default {
|
||||
current: false,
|
||||
available: false,
|
||||
applications: false
|
||||
}
|
||||
},
|
||||
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -236,6 +269,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||
await this.loadNotBaptisedChildren();
|
||||
await this.loadOwnCharacterId();
|
||||
await Promise.all([
|
||||
@@ -495,6 +529,21 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.church-person-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.church-person-cell__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
h2 {
|
||||
padding-top: 20px;
|
||||
|
||||
@@ -149,10 +149,11 @@
|
||||
</div>
|
||||
<div class="character-media character-media--spouse">
|
||||
<div
|
||||
v-if="showPortraitAvatar"
|
||||
class="character-avatar character-avatar--spouse"
|
||||
:style="getCharacterAvatarStyle(relationships[0].character2, 0.72)"
|
||||
></div>
|
||||
<div class="character-3d-frame character-3d-frame--spouse">
|
||||
<div v-if="showCharacterFigure3d" class="character-3d-frame character-3d-frame--spouse">
|
||||
<Character3D
|
||||
:gender="relationships[0].character2.gender"
|
||||
:age="relationships[0].character2.age"
|
||||
@@ -328,10 +329,11 @@
|
||||
</div>
|
||||
<div class="character-media character-media--compact child-character-media">
|
||||
<div
|
||||
v-if="showPortraitAvatar"
|
||||
class="character-avatar character-avatar--compact"
|
||||
:style="getCharacterAvatarStyle(child, 0.5)"
|
||||
></div>
|
||||
<div class="character-3d-frame character-3d-frame--compact">
|
||||
<div v-if="showCharacterFigure3d" class="character-3d-frame character-3d-frame--compact">
|
||||
<Character3D
|
||||
:gender="child.gender"
|
||||
:age="child.age"
|
||||
@@ -406,10 +408,11 @@
|
||||
<div class="lover-card__header">
|
||||
<div class="character-media character-media--compact lover-character-media">
|
||||
<div
|
||||
v-if="showPortraitAvatar"
|
||||
class="character-avatar character-avatar--compact"
|
||||
:style="getCharacterAvatarStyle(lover, 0.5)"
|
||||
></div>
|
||||
<div class="character-3d-frame character-3d-frame--compact">
|
||||
<div v-if="showCharacterFigure3d" class="character-3d-frame character-3d-frame--compact">
|
||||
<Character3D
|
||||
:gender="lover.gender"
|
||||
:age="lover.age"
|
||||
@@ -581,6 +584,13 @@ import Character3D from '@/components/Character3D.vue'
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import { confirmAction, showError, showInfo, showSuccess } from '@/utils/feedback.js'
|
||||
import { mapState } from 'vuex'
|
||||
import {
|
||||
FALUKANT_VISUAL_DEFAULTS,
|
||||
loadFalukantVisualSettings,
|
||||
resolveFalukantPortraitAtlas,
|
||||
showFalukant3dFigures,
|
||||
showFalukantPortraits,
|
||||
} from '@/utils/falukantVisualSettings.js'
|
||||
|
||||
const WOOING_PROGRESS_TARGET = 70
|
||||
const MARRIAGE_GIFT_COSTS = {
|
||||
@@ -662,11 +672,18 @@ export default {
|
||||
selectedChild: null,
|
||||
pendingFamilyRefresh: null,
|
||||
familyTab: 'partner',
|
||||
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
isDev() { return !import.meta.env.PROD },
|
||||
showPortraitAvatar() {
|
||||
return showFalukantPortraits(this.visualSettings.figureMode)
|
||||
},
|
||||
showCharacterFigure3d() {
|
||||
return showFalukant3dFigures(this.visualSettings.figureMode)
|
||||
},
|
||||
marriageGiftCosts() {
|
||||
return MARRIAGE_GIFT_COSTS;
|
||||
},
|
||||
@@ -712,6 +729,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||
await this.loadOwnCharacter();
|
||||
await this.loadFamilyData();
|
||||
await this.loadGifts();
|
||||
@@ -1068,7 +1086,7 @@ export default {
|
||||
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
|
||||
const safeScale = Number(scale) > 0 ? Number(scale) : 1;
|
||||
return {
|
||||
backgroundImage: `url(/images/falukant/avatar/${gender}.png)`,
|
||||
backgroundImage: `url(${resolveFalukantPortraitAtlas(gender, 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`,
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
</section>
|
||||
|
||||
<div v-if="falukantUser?.character && !falukantUser?.debtorsPrison?.inDebtorsPrison" class="imagecontainer">
|
||||
<div :style="getAvatarStyle" class="avatar"></div>
|
||||
<div class="house-with-character">
|
||||
<div v-if="showPortraitAvatar" :style="getAvatarStyle" class="avatar"></div>
|
||||
<div v-if="showCharacterFigure3d" class="house-with-character">
|
||||
<div :style="getHouseStyle" class="house"></div>
|
||||
<div class="character-foreground">
|
||||
<Character3D
|
||||
@@ -336,6 +336,13 @@ import Character3D from '@/components/Character3D.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||
import { mapState } from 'vuex';
|
||||
import {
|
||||
FALUKANT_VISUAL_DEFAULTS,
|
||||
loadFalukantVisualSettings,
|
||||
resolveFalukantPortraitAtlas,
|
||||
showFalukant3dFigures,
|
||||
showFalukantPortraits,
|
||||
} from '@/utils/falukantVisualSettings.js';
|
||||
|
||||
const AVATAR_POSITIONS = {
|
||||
male: {
|
||||
@@ -396,14 +403,21 @@ export default {
|
||||
potentialHeirs: [],
|
||||
loadingHeirs: false,
|
||||
pendingOverviewRefresh: null,
|
||||
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
showPortraitAvatar() {
|
||||
return showFalukantPortraits(this.visualSettings.figureMode);
|
||||
},
|
||||
showCharacterFigure3d() {
|
||||
return showFalukant3dFigures(this.visualSettings.figureMode);
|
||||
},
|
||||
getAvatarStyle() {
|
||||
if (!this.falukantUser || !this.falukantUser.character) return {};
|
||||
const { gender, age } = this.falukantUser.character;
|
||||
const imageUrl = `/images/falukant/avatar/${gender}.png`;
|
||||
const imageUrl = resolveFalukantPortraitAtlas(gender, this.visualSettings.portraitStyle);
|
||||
const ageGroup = this.getAgeGroup(age);
|
||||
const genderData = AVATAR_POSITIONS[gender] || {};
|
||||
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
|
||||
@@ -560,6 +574,7 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||
await this.fetchFalukantUser();
|
||||
if (!this.falukantUser?.character) {
|
||||
await this.fetchPotentialHeirs();
|
||||
|
||||
@@ -16,11 +16,22 @@
|
||||
<span>{{ pos.region.name }}</span>
|
||||
</div>
|
||||
<div class="politics-card__meta">
|
||||
<div v-if="pos.character" class="politics-card__visual">
|
||||
<FalukantCharacterVisual
|
||||
:gender="pos.character.gender"
|
||||
:age="pos.character.age"
|
||||
:visual-settings="visualSettings"
|
||||
:portrait-scale="0.36"
|
||||
:figure-width="78"
|
||||
:figure-height="100"
|
||||
:compact="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="politics-card__meta-row">
|
||||
<span class="politics-card__meta-label">{{ $t('falukant.politics.current.holder') }}:</span>
|
||||
<span class="politics-card__meta-value">
|
||||
<template v-if="pos.character">
|
||||
{{ pos.character.definedFirstName.name }} {{ pos.character.definedLastName.name }}
|
||||
{{ formatPoliticalCharacterName(pos.character) }}
|
||||
</template>
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
@@ -272,16 +283,22 @@
|
||||
|
||||
<script>
|
||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||
import FalukantCharacterVisual from '@/components/falukant/FalukantCharacterVisual.vue';
|
||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
import { mapState } from 'vuex';
|
||||
import {
|
||||
FALUKANT_VISUAL_DEFAULTS,
|
||||
loadFalukantVisualSettings,
|
||||
} from '@/utils/falukantVisualSettings.js';
|
||||
|
||||
const debugLog = () => {};
|
||||
|
||||
export default {
|
||||
name: 'PoliticsView',
|
||||
components: { StatusBar, SimpleTabs, Multiselect },
|
||||
components: { StatusBar, FalukantCharacterVisual, SimpleTabs, Multiselect },
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'current',
|
||||
@@ -314,10 +331,12 @@ export default {
|
||||
openPolitics: false,
|
||||
elections: false,
|
||||
powers: false
|
||||
}
|
||||
},
|
||||
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
hasAnyPowers() {
|
||||
const p = this.myPowers;
|
||||
if (!p) return false;
|
||||
@@ -339,11 +358,17 @@ export default {
|
||||
return this.elections.some(e => !e.voted);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadOwnCharacterId();
|
||||
this.loadCurrentPositions();
|
||||
async mounted() {
|
||||
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||
await this.loadOwnCharacterId();
|
||||
await this.loadCurrentPositions();
|
||||
},
|
||||
methods: {
|
||||
formatPoliticalCharacterName(character) {
|
||||
const firstName = character?.definedFirstName?.name || '';
|
||||
const lastName = character?.definedLastName?.name || '';
|
||||
return `${firstName} ${lastName}`.trim();
|
||||
},
|
||||
politicsBenefitItems(pos) {
|
||||
const raw = pos?.benefit;
|
||||
if (!Array.isArray(raw) || !raw.length) {
|
||||
@@ -719,6 +744,12 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.politics-card__visual {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.politics-view {
|
||||
max-width: var(--content-max-width);
|
||||
|
||||
17
frontend/src/views/settings/FalukantView.vue
Normal file
17
frontend/src/views/settings/FalukantView.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>{{ $t("settings.falukant.title") }}</h2>
|
||||
<SettingsWidget :settingsType="'falukant'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsWidget from '@/components/SettingsWidget.vue';
|
||||
|
||||
export default {
|
||||
name: 'FalukantSettingsView',
|
||||
components: {
|
||||
SettingsWidget,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user