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:
@@ -216,6 +216,10 @@ const menuStructure = {
|
|||||||
visible: ["all"],
|
visible: ["all"],
|
||||||
path: "/settings/language-assistant"
|
path: "/settings/language-assistant"
|
||||||
},
|
},
|
||||||
|
falukant: {
|
||||||
|
visible: ["all"],
|
||||||
|
path: "/settings/falukant"
|
||||||
|
},
|
||||||
personal: {
|
personal: {
|
||||||
visible: ["all"],
|
visible: ["all"],
|
||||||
path: "/settings/personal"
|
path: "/settings/personal"
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ function calcAge(birthdate) {
|
|||||||
return differenceInDays(now, b);
|
return differenceInDays(now, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ein realer Tag entspricht einem Falukant-Spieljahr.
|
||||||
|
const FAMILY_AGE = Object.freeze({
|
||||||
|
MIN_WOOING: 12,
|
||||||
|
MIN_MARRIAGE: 14,
|
||||||
|
MIN_HOUSEHOLD_AND_CHILDREN: 16,
|
||||||
|
});
|
||||||
|
|
||||||
async function getFalukantUserOrFail(hashedId) {
|
async function getFalukantUserOrFail(hashedId) {
|
||||||
const user = await FalukantUser.findOne({
|
const user = await FalukantUser.findOne({
|
||||||
include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }]
|
include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }]
|
||||||
@@ -3571,6 +3578,17 @@ class FalukantService extends BaseService {
|
|||||||
return { scheduled: false };
|
return { scheduled: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parents = await FalukantCharacter.unscoped().findAll({
|
||||||
|
where: { id: [motherId, fatherId] },
|
||||||
|
attributes: ['id', 'birthdate'],
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
parents.length !== 2
|
||||||
|
|| parents.some((parent) => calcAge(parent.birthdate) < FAMILY_AGE.MIN_HOUSEHOLD_AND_CHILDREN)
|
||||||
|
) {
|
||||||
|
return { scheduled: false };
|
||||||
|
}
|
||||||
|
|
||||||
const stateRow = await RelationshipState.findOne({ where: { relationshipId } });
|
const stateRow = await RelationshipState.findOne({ where: { relationshipId } });
|
||||||
const flags = stateRow?.flagsJson && typeof stateRow.flagsJson === 'object'
|
const flags = stateRow?.flagsJson && typeof stateRow.flagsJson === 'object'
|
||||||
? { ...stateRow.flagsJson }
|
? { ...stateRow.flagsJson }
|
||||||
@@ -3899,10 +3917,10 @@ class FalukantService extends BaseService {
|
|||||||
pregnancy,
|
pregnancy,
|
||||||
};
|
};
|
||||||
const ownAge = calcAge(character.birthdate);
|
const ownAge = calcAge(character.birthdate);
|
||||||
if (ownAge >= 12) {
|
if (ownAge >= FAMILY_AGE.MIN_WOOING) {
|
||||||
family.possibleLovers = await this.getPossibleLovers(character.id);
|
family.possibleLovers = await this.getPossibleLovers(character.id);
|
||||||
}
|
}
|
||||||
if (ownAge >= 12 && family.relationships.length === 0) {
|
if (ownAge >= FAMILY_AGE.MIN_WOOING && family.relationships.length === 0) {
|
||||||
family.possiblePartners = await this.getPossiblePartners(character.id);
|
family.possiblePartners = await this.getPossiblePartners(character.id);
|
||||||
if (family.possiblePartners.length === 0) {
|
if (family.possiblePartners.length === 0) {
|
||||||
await this.createPossiblePartners(
|
await this.createPossiblePartners(
|
||||||
@@ -3937,7 +3955,7 @@ class FalukantService extends BaseService {
|
|||||||
attributes: ['id', 'regionId', 'birthdate', 'titleOfNobility']
|
attributes: ['id', 'regionId', 'birthdate', 'titleOfNobility']
|
||||||
});
|
});
|
||||||
if (!requester?.id) return [];
|
if (!requester?.id) return [];
|
||||||
if (calcAge(requester.birthdate) < 12) return [];
|
if (calcAge(requester.birthdate) < FAMILY_AGE.MIN_WOOING) return [];
|
||||||
|
|
||||||
const existingRelationships = await Relationship.findAll({
|
const existingRelationships = await Relationship.findAll({
|
||||||
where: {
|
where: {
|
||||||
@@ -4549,6 +4567,7 @@ class FalukantService extends BaseService {
|
|||||||
id: { [Op.ne]: requestingCharacterId },
|
id: { [Op.ne]: requestingCharacterId },
|
||||||
gender: { [Op.ne]: requestingCharacterGender },
|
gender: { [Op.ne]: requestingCharacterGender },
|
||||||
regionId: requestingRegionId,
|
regionId: requestingRegionId,
|
||||||
|
birthdate: { [Op.lte]: new Date(Date.now() - FAMILY_AGE.MIN_WOOING * 24 * 60 * 60 * 1000) },
|
||||||
createdAt: { [Op.lt]: new Date(new Date() - 12 * 24 * 60 * 60 * 1000) },
|
createdAt: { [Op.lt]: new Date(new Date() - 12 * 24 * 60 * 60 * 1000) },
|
||||||
titleOfNobility: { [Op.between]: [requestingCharacterTitleOfNobility - 1, requestingCharacterTitleOfNobility + 1] }
|
titleOfNobility: { [Op.between]: [requestingCharacterTitleOfNobility - 1, requestingCharacterTitleOfNobility + 1] }
|
||||||
},
|
},
|
||||||
@@ -4588,6 +4607,18 @@ class FalukantService extends BaseService {
|
|||||||
if (!proposal) {
|
if (!proposal) {
|
||||||
throw new Error('Proposal not found');
|
throw new Error('Proposal not found');
|
||||||
}
|
}
|
||||||
|
const proposedCharacter = await FalukantCharacter.findByPk(proposal.proposedCharacterId, {
|
||||||
|
attributes: ['id', 'birthdate'],
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
!proposedCharacter
|
||||||
|
|| calcAge(character.birthdate) < FAMILY_AGE.MIN_WOOING
|
||||||
|
|| calcAge(proposedCharacter.birthdate) < FAMILY_AGE.MIN_WOOING
|
||||||
|
) {
|
||||||
|
const error = new Error('Werbung ist erst ab 12 Spieljahren möglich');
|
||||||
|
error.status = 422;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
if (user.money < proposal.cost) {
|
if (user.money < proposal.cost) {
|
||||||
throw new Error('Not enough money to accept the proposal');
|
throw new Error('Not enough money to accept the proposal');
|
||||||
}
|
}
|
||||||
@@ -5510,7 +5541,12 @@ class FalukantService extends BaseService {
|
|||||||
|
|
||||||
async getPartyTypes(hashedUserId) {
|
async getPartyTypes(hashedUserId) {
|
||||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||||
const engagedCount = await Relationship.count({
|
const character = await FalukantCharacter.findOne({
|
||||||
|
where: { userId: falukantUser.id },
|
||||||
|
attributes: ['id', 'birthdate'],
|
||||||
|
});
|
||||||
|
const engagedCount = character && calcAge(character.birthdate) >= FAMILY_AGE.MIN_MARRIAGE
|
||||||
|
? await Relationship.count({
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: RelationshipType,
|
model: RelationshipType,
|
||||||
@@ -5537,7 +5573,8 @@ class FalukantService extends BaseService {
|
|||||||
{ '$character2.user_id$': falukantUser.id }
|
{ '$character2.user_id$': falukantUser.id }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
: 0;
|
||||||
const orConditions = [{ forMarriage: false }];
|
const orConditions = [{ forMarriage: false }];
|
||||||
if (engagedCount > 0) {
|
if (engagedCount > 0) {
|
||||||
orConditions.push({ forMarriage: true });
|
orConditions.push({ forMarriage: true });
|
||||||
@@ -5586,7 +5623,35 @@ class FalukantService extends BaseService {
|
|||||||
throw new Error('Einige ausgewählte Adelstitel existieren nicht');
|
throw new Error('Einige ausgewählte Adelstitel existieren nicht');
|
||||||
}
|
}
|
||||||
|
|
||||||
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, attributes: ['titleOfNobility'] });
|
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, attributes: ['id', 'birthdate', 'titleOfNobility'] });
|
||||||
|
if (ptype.forMarriage) {
|
||||||
|
if (!character || calcAge(character.birthdate) < FAMILY_AGE.MIN_MARRIAGE) {
|
||||||
|
const error = new Error('Eine Hochzeit ist erst ab 14 Spieljahren möglich');
|
||||||
|
error.status = 422;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const engagement = await Relationship.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ character1Id: character.id },
|
||||||
|
{ character2Id: character.id },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: [{ model: RelationshipType, as: 'relationshipType', where: { tr: 'engaged' } }],
|
||||||
|
attributes: ['character1Id', 'character2Id'],
|
||||||
|
});
|
||||||
|
const partnerId = engagement?.character1Id === character.id
|
||||||
|
? engagement.character2Id
|
||||||
|
: engagement?.character1Id;
|
||||||
|
const partner = partnerId
|
||||||
|
? await FalukantCharacter.findByPk(partnerId, { attributes: ['birthdate'] })
|
||||||
|
: null;
|
||||||
|
if (!partner || calcAge(partner.birthdate) < FAMILY_AGE.MIN_MARRIAGE) {
|
||||||
|
const error = new Error('Beide Verlobten müssen mindestens 14 Spieljahre alt sein');
|
||||||
|
error.status = 422;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
const partyTypeCost = character && (await isPartyTypeFreeForTitle(character.titleOfNobility, ptype.id, ptype.tr)) ? 0 : (ptype.cost || 0);
|
const partyTypeCost = character && (await isPartyTypeFreeForTitle(character.titleOfNobility, ptype.id, ptype.tr)) ? 0 : (ptype.cost || 0);
|
||||||
let cost = partyTypeCost + (music.cost || 0) + (banquette.cost || 0);
|
let cost = partyTypeCost + (music.cost || 0) + (banquette.cost || 0);
|
||||||
cost += (50 / servantRatio - 1) * 1000;
|
cost += (50 / servantRatio - 1) * 1000;
|
||||||
@@ -6747,7 +6812,7 @@ class FalukantService extends BaseService {
|
|||||||
{
|
{
|
||||||
model: FalukantCharacter,
|
model: FalukantCharacter,
|
||||||
as: 'holder',
|
as: 'holder',
|
||||||
attributes: ['id', 'gender'],
|
attributes: ['id', 'gender', 'birthdate'],
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: FalukantPredefineFirstname,
|
model: FalukantPredefineFirstname,
|
||||||
@@ -6828,7 +6893,8 @@ class FalukantService extends BaseService {
|
|||||||
definedFirstName: o.holder.definedFirstName,
|
definedFirstName: o.holder.definedFirstName,
|
||||||
definedLastName: o.holder.definedLastName,
|
definedLastName: o.holder.definedLastName,
|
||||||
nobleTitle: o.holder.nobleTitle,
|
nobleTitle: o.holder.nobleTitle,
|
||||||
gender: o.holder.gender
|
gender: o.holder.gender,
|
||||||
|
age: o.holder.birthdate ? calcAge(o.holder.birthdate) : null
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
termEnds,
|
termEnds,
|
||||||
@@ -8302,7 +8368,7 @@ ORDER BY r.id`,
|
|||||||
{
|
{
|
||||||
model: FalukantCharacter,
|
model: FalukantCharacter,
|
||||||
as: 'holder',
|
as: 'holder',
|
||||||
attributes: ['id', 'gender'],
|
attributes: ['id', 'gender', 'birthdate'],
|
||||||
required: false,
|
required: false,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
@@ -8368,7 +8434,8 @@ ORDER BY r.id`,
|
|||||||
id: o.holder.id,
|
id: o.holder.id,
|
||||||
name: `${o.holder.definedFirstName?.name || ''} ${o.holder.definedLastName?.name || ''}`.trim(),
|
name: `${o.holder.definedFirstName?.name || ''} ${o.holder.definedLastName?.name || ''}`.trim(),
|
||||||
gender: o.holder.gender,
|
gender: o.holder.gender,
|
||||||
title: o.holder.nobleTitle?.labelTr
|
title: o.holder.nobleTitle?.labelTr,
|
||||||
|
age: o.holder.birthdate ? calcAge(o.holder.birthdate) : null
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
supervisor: o.supervisor
|
supervisor: o.supervisor
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ class SettingsService extends BaseService{
|
|||||||
adult_verification_status: { datatype: 'string', setting: 'account', orderId: 910, minAge: 18 },
|
adult_verification_status: { datatype: 'string', setting: 'account', orderId: 910, minAge: 18 },
|
||||||
adult_verification_request: { datatype: 'string', setting: 'account', orderId: 911, minAge: 18 },
|
adult_verification_request: { datatype: 'string', setting: 'account', orderId: 911, minAge: 18 },
|
||||||
adult_upload_blocked: { datatype: 'bool', setting: 'account', orderId: 912, minAge: 18 },
|
adult_upload_blocked: { datatype: 'bool', setting: 'account', orderId: 912, minAge: 18 },
|
||||||
|
falukantPortraitStyle: {
|
||||||
|
datatype: 'singleselect',
|
||||||
|
setting: 'falukant',
|
||||||
|
orderId: 920,
|
||||||
|
options: ['classic', 'medieval']
|
||||||
|
},
|
||||||
|
falukantFigureMode: {
|
||||||
|
datatype: 'singleselect',
|
||||||
|
setting: 'falukant',
|
||||||
|
orderId: 921,
|
||||||
|
options: ['portraits', '3d', 'both']
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const definition = specialTypes[description];
|
const definition = specialTypes[description];
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
@@ -59,9 +71,38 @@ class SettingsService extends BaseService{
|
|||||||
immutable: false
|
immutable: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (Array.isArray(definition.options) && definition.options.length > 0) {
|
||||||
|
let orderId = 1;
|
||||||
|
for (const option of definition.options) {
|
||||||
|
await UserParamValue.findOrCreate({
|
||||||
|
where: {
|
||||||
|
userParamTypeId: paramType.id,
|
||||||
|
value: option
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
userParamTypeId: paramType.id,
|
||||||
|
value: option,
|
||||||
|
orderId: orderId++
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return paramType;
|
return paramType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ensureSettingsGroup(type) {
|
||||||
|
const groupDefinitions = {
|
||||||
|
falukant: ['falukantPortraitStyle', 'falukantFigureMode']
|
||||||
|
};
|
||||||
|
const descriptions = groupDefinitions[type];
|
||||||
|
if (!descriptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const description of descriptions) {
|
||||||
|
await this.ensureSpecialUserParamType(description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parseAdultVerificationRequest(value) {
|
parseAdultVerificationRequest(value) {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
try {
|
try {
|
||||||
@@ -298,6 +339,7 @@ class SettingsService extends BaseService{
|
|||||||
}
|
}
|
||||||
|
|
||||||
async filterSettings(hashedUserId, type) {
|
async filterSettings(hashedUserId, type) {
|
||||||
|
await this.ensureSettingsGroup(type);
|
||||||
const user = await this.getUserByHashedId(hashedUserId);
|
const user = await this.getUserByHashedId(hashedUserId);
|
||||||
const userParams = await this.getUserParams(user.id, ['birthdate', 'gender']);
|
const userParams = await this.getUserParams(user.id, ['birthdate', 'gender']);
|
||||||
let birthdate = null;
|
let birthdate = null;
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ const initializeSettings = async () => {
|
|||||||
where: { name: 'languageAssistant' },
|
where: { name: 'languageAssistant' },
|
||||||
defaults: { name: 'languageAssistant' }
|
defaults: { name: 'languageAssistant' }
|
||||||
});
|
});
|
||||||
|
await SettingsType.findOrCreate({
|
||||||
|
where: { name: 'falukant' },
|
||||||
|
defaults: { name: 'falukant' }
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default initializeSettings;
|
export default initializeSettings;
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ const initializeTypes = async () => {
|
|||||||
adult_upload_blocked: { type: 'bool', setting: 'account', minAge: 18 },
|
adult_upload_blocked: { type: 'bool', setting: 'account', minAge: 18 },
|
||||||
llm_settings: { type: 'string', setting: 'languageAssistant' },
|
llm_settings: { type: 'string', setting: 'languageAssistant' },
|
||||||
llm_api_key: { type: 'string', setting: 'languageAssistant' },
|
llm_api_key: { type: 'string', setting: 'languageAssistant' },
|
||||||
|
falukantPortraitStyle: { type: 'singleselect', setting: 'falukant' },
|
||||||
|
falukantFigureMode: { type: 'singleselect', setting: 'falukant' },
|
||||||
};
|
};
|
||||||
let orderId = 1;
|
let orderId = 1;
|
||||||
for (const key of Object.keys(userParams)) {
|
for (const key of Object.keys(userParams)) {
|
||||||
@@ -78,7 +80,9 @@ const initializeTypes = async () => {
|
|||||||
interestedInGender: ['male', 'female'],
|
interestedInGender: ['male', 'female'],
|
||||||
smokes: ['never', 'socially', 'often', 'daily'],
|
smokes: ['never', 'socially', 'often', 'daily'],
|
||||||
drinks: ['never', 'socially', 'often', 'daily'],
|
drinks: ['never', 'socially', 'often', 'daily'],
|
||||||
brasize: ['Keine', 'AA', 'A', 'B', 'C', 'D', 'E (DD)', 'F (E)', 'G (F)', 'H (FF)', 'I (G)', 'J (GG)', 'K (H)']
|
brasize: ['Keine', 'AA', 'A', 'B', 'C', 'D', 'E (DD)', 'F (E)', 'G (F)', 'H (FF)', 'I (G)', 'J (GG)', 'K (H)'],
|
||||||
|
falukantPortraitStyle: ['classic', 'medieval'],
|
||||||
|
falukantFigureMode: ['portraits', '3d', 'both']
|
||||||
};
|
};
|
||||||
Object.keys(valuesList).forEach(async (key) => {
|
Object.keys(valuesList).forEach(async (key) => {
|
||||||
const values = valuesList[key];
|
const values = valuesList[key];
|
||||||
|
|||||||
78
docs/FALUKANT_FAMILY_AGE_RULES.md
Normal file
78
docs/FALUKANT_FAMILY_AGE_RULES.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Falukant: Altersregeln für Werbung, Ehe und Kinder
|
||||||
|
|
||||||
|
## Verbindliche Spielregel
|
||||||
|
|
||||||
|
Ein realer Kalendertag entspricht einem Falukant-Spieljahr. Die Altersprüfung
|
||||||
|
verwendet daher die Differenz zwischen `CURRENT_DATE` und `birthdate` in Tagen.
|
||||||
|
|
||||||
|
| Bereich | Mindestalter | Regel |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| Werbung | 12 | Partner können vorgeschlagen und ein Vorschlag kann angenommen werden. |
|
||||||
|
| Hochzeit | 14 | Eine Verlobung kann durch eine Hochzeitsfeier vollzogen werden. |
|
||||||
|
| Gemeinsamer Haushalt und Kinder | 16 | Erst dann darf die Spielmechanik eine Schwangerschaft oder Geburt aus einer Ehe erzeugen. |
|
||||||
|
|
||||||
|
Die Regeln gelten für **beide** Personen einer Beziehung. Sie bilden bewusst
|
||||||
|
Spielphasen ab; es gibt keine individuelle Prüfung körperlicher Reife und keine
|
||||||
|
explizite Sexualmechanik.
|
||||||
|
|
||||||
|
## Bereits im Backend und Daemon umgesetzt
|
||||||
|
|
||||||
|
- `backend/services/falukantService.js`
|
||||||
|
- erzeugt Heiratsvorschläge nur für mindestens 12 Jahre alte Partner;
|
||||||
|
- prüft beim Annehmen eines Vorschlags beide Alter erneut;
|
||||||
|
- bietet den Hochzeitstyp erst ab 14 an und prüft beim Bestellen der Feier
|
||||||
|
beide Verlobte serverseitig;
|
||||||
|
- plant die Hochzeits-Schwangerschaft erst, wenn beide mindestens 16 sind.
|
||||||
|
- `src/valuerecalculationworker.h`
|
||||||
|
- vollzieht die Hochzeit nach der mindestens einen Tag alten Hochzeitsfeier
|
||||||
|
nur, wenn beide Verlobte mindestens 14 sind.
|
||||||
|
- `src/usercharacterworker.h`
|
||||||
|
- berücksichtigt für die bestehende zufällige Ehe-Kinderlogik ausschließlich
|
||||||
|
Paare, bei denen beide mindestens 16 sind.
|
||||||
|
|
||||||
|
## Auftrag für den externen Daemon
|
||||||
|
|
||||||
|
Falls der produktive Daemon aus diesem Repository herausgelöst ist, müssen die
|
||||||
|
folgenden Guards in dessen gleichnamige Queries übernommen werden.
|
||||||
|
|
||||||
|
### 1. Übergang `engaged` → `married`
|
||||||
|
|
||||||
|
Beim Verarbeiten einer Hochzeit sind beide Charaktere zu prüfen:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
c.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
||||||
|
AND partner.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
||||||
|
```
|
||||||
|
|
||||||
|
Die bestehende Wartezeit von `party.created_at <= NOW() - INTERVAL '1 day'`
|
||||||
|
bleibt unverändert. Erfüllt ein Paar die Altersregel noch nicht, bleibt es
|
||||||
|
verlobt; die bereits angelegte Feier wird nicht verworfen und kann nach Erreichen
|
||||||
|
des Alters vollzogen werden.
|
||||||
|
|
||||||
|
### 2. Eheliche Schwangerschaften und Geburten
|
||||||
|
|
||||||
|
Jeder automatische Empfängnis- oder Geburtenkandidat muss beide Bedingungen
|
||||||
|
erfüllen:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
mother.birthdate <= CURRENT_DATE - INTERVAL '16 days'
|
||||||
|
AND father.birthdate <= CURRENT_DATE - INTERVAL '16 days'
|
||||||
|
```
|
||||||
|
|
||||||
|
Das gilt sowohl für die Zufallslogik als auch für einen geplanten
|
||||||
|
`pregnancy_due_at`-Pfad. Beim geplanten Pfad soll der Daemon die Felder nicht
|
||||||
|
leeren, solange mindestens ein Elternteil noch unter 16 ist; die Schwangerschaft
|
||||||
|
wird erst verarbeitet, sobald beide die Grenze erreicht haben. Admin-Tools
|
||||||
|
können weiterhin ein bewusstes, separat protokolliertes Override anbieten.
|
||||||
|
|
||||||
|
### 3. Tests für den Daemon
|
||||||
|
|
||||||
|
- 13/13 Jahre, verlobt, Hochzeitsfeier älter als 24 Stunden: bleibt `engaged`.
|
||||||
|
- 14/14 Jahre, verlobt, Hochzeitsfeier älter als 24 Stunden: wird `married`.
|
||||||
|
- Verheiratet, ein Elternteil 15: keine automatische Schwangerschaft/Geburt.
|
||||||
|
- Verheiratet, beide 16: normaler Schwangerschafts-/Geburtspfad ist möglich.
|
||||||
|
|
||||||
|
## Bestehende Daten
|
||||||
|
|
||||||
|
Es ist keine Migration nötig. Bereits bestehende Ehen bleiben bestehen; die
|
||||||
|
Grenzen steuern ausschließlich neue Übergänge und automatische Kinderereignisse.
|
||||||
BIN
frontend/public/images/falukant/avatar/female00-medieval.png
Normal file
BIN
frontend/public/images/falukant/avatar/female00-medieval.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
BIN
frontend/public/images/falukant/avatar/female01-medieval.png
Normal file
BIN
frontend/public/images/falukant/avatar/female01-medieval.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
BIN
frontend/public/images/falukant/avatar/female02-medieval.png
Normal file
BIN
frontend/public/images/falukant/avatar/female02-medieval.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
BIN
frontend/public/images/falukant/avatar/male00-medieval.png
Normal file
BIN
frontend/public/images/falukant/avatar/male00-medieval.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
BIN
frontend/public/images/falukant/avatar/male01-medieval.png
Normal file
BIN
frontend/public/images/falukant/avatar/male01-medieval.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
BIN
frontend/public/images/falukant/avatar/male02-medieval.png
Normal file
BIN
frontend/public/images/falukant/avatar/male02-medieval.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -68,6 +68,7 @@ const TITLE_MAP = {
|
|||||||
'Flirt settings': 'sectionBar.titles.flirtSettings',
|
'Flirt settings': 'sectionBar.titles.flirtSettings',
|
||||||
'Account settings': 'sectionBar.titles.accountSettings',
|
'Account settings': 'sectionBar.titles.accountSettings',
|
||||||
'Language assistant settings': 'sectionBar.titles.languageAssistantSettings',
|
'Language assistant settings': 'sectionBar.titles.languageAssistantSettings',
|
||||||
|
'Falukant settings': 'sectionBar.titles.falukantSettings',
|
||||||
Interests: 'sectionBar.titles.interests',
|
Interests: 'sectionBar.titles.interests',
|
||||||
AdminInterests: 'sectionBar.titles.adminInterests',
|
AdminInterests: 'sectionBar.titles.adminInterests',
|
||||||
AdminUsers: 'sectionBar.titles.adminUsers',
|
AdminUsers: 'sectionBar.titles.adminUsers',
|
||||||
|
|||||||
@@ -4,51 +4,51 @@
|
|||||||
<tr v-for="setting in settings" :key="setting.id">
|
<tr v-for="setting in settings" :key="setting.id">
|
||||||
<td>
|
<td>
|
||||||
<InputStringWidget v-if="setting.datatype == 'string'"
|
<InputStringWidget v-if="setting.datatype == 'string'"
|
||||||
:labelTr="`settings.personal.label.${setting.name}`"
|
:labelTr="settingLabelTr(setting.name)"
|
||||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||||
:value=setting.value
|
:value=setting.value
|
||||||
:disabled="setting.immutable && setting.value ? true : false"
|
:disabled="setting.immutable && setting.value ? true : false"
|
||||||
:list="languagesList()" @input="handleInput(setting.id, $event)" />
|
:list="languagesList()" @input="handleInput(setting.id, $event)" />
|
||||||
|
|
||||||
<DateInputWidget v-else-if="setting.datatype == 'date'"
|
<DateInputWidget v-else-if="setting.datatype == 'date'"
|
||||||
:labelTr="`settings.personal.label.${setting.name}`"
|
:labelTr="settingLabelTr(setting.name)"
|
||||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||||
:value=setting.value
|
:value=setting.value
|
||||||
:disabled="setting.immutable && setting.value ? true : false"
|
:disabled="setting.immutable && setting.value ? true : false"
|
||||||
@input="handleInput(setting.id, $event)" />
|
@input="handleInput(setting.id, $event)" />
|
||||||
|
|
||||||
<SelectDropdownWidget v-else-if="setting.datatype == 'singleselect'"
|
<SelectDropdownWidget v-else-if="setting.datatype == 'singleselect'"
|
||||||
:labelTr="`settings.personal.label.${setting.name}`"
|
:labelTr="settingLabelTr(setting.name)"
|
||||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||||
:value=setting.value
|
:value=setting.value
|
||||||
:disabled="setting.immutable && setting.value ? true : false"
|
:disabled="setting.immutable && setting.value ? true : false"
|
||||||
:list="getSettingOptions(setting.name, setting.options)"
|
:list="getSettingOptions(setting.name, setting.options)"
|
||||||
@input="handleInput(setting.id, $event)" />
|
@input="handleInput(setting.id, $event)" />
|
||||||
|
|
||||||
<InputNumberWidget v-else-if="setting.datatype == 'int'"
|
<InputNumberWidget v-else-if="setting.datatype == 'int'"
|
||||||
:labelTr="`settings.personal.label.${setting.name}`"
|
:labelTr="settingLabelTr(setting.name)"
|
||||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||||
:value="convertToInt(setting.value)"
|
:value="convertToInt(setting.value)"
|
||||||
:disabled="setting.immutable && setting.value ? true : false"
|
:disabled="setting.immutable && setting.value ? true : false"
|
||||||
min="0" max="200" @input="handleInput(setting.id, $event)" />
|
min="0" max="200" @input="handleInput(setting.id, $event)" />
|
||||||
|
|
||||||
<FloatInputWidget v-else-if="setting.datatype == 'float'"
|
<FloatInputWidget v-else-if="setting.datatype == 'float'"
|
||||||
:labelTr="`settings.personal.label.${setting.name}`"
|
:labelTr="settingLabelTr(setting.name)"
|
||||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||||
:value="convertToFloat(setting.value)"
|
:value="convertToFloat(setting.value)"
|
||||||
:disabled="setting.immutable && setting.value ? true : false"
|
:disabled="setting.immutable && setting.value ? true : false"
|
||||||
@input="handleInput(setting.id, $event)" />
|
@input="handleInput(setting.id, $event)" />
|
||||||
|
|
||||||
<CheckboxWidget v-else-if="setting.datatype == 'bool'"
|
<CheckboxWidget v-else-if="setting.datatype == 'bool'"
|
||||||
:labelTr="`settings.personal.label.${setting.name}`"
|
:labelTr="settingLabelTr(setting.name)"
|
||||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||||
:value="convertToBool(setting.value)"
|
:value="convertToBool(setting.value)"
|
||||||
:disabled="setting.immutable && setting.value ? true : false"
|
:disabled="setting.immutable && setting.value ? true : false"
|
||||||
@input="handleInput(setting.id, $event)" />
|
@input="handleInput(setting.id, $event)" />
|
||||||
|
|
||||||
<MultiselectWidget v-else-if="setting.datatype == 'multiselect'"
|
<MultiselectWidget v-else-if="setting.datatype == 'multiselect'"
|
||||||
:labelTr="`settings.personal.label.${setting.name}`"
|
:labelTr="settingLabelTr(setting.name)"
|
||||||
:tooltipTr="setting.immutable ? $t('settings.immutable.tooltip') : `settings.personal.tooltip.${setting.name}`"
|
:tooltipTr="settingTooltipTr(setting.name, setting.immutable)"
|
||||||
:value="setting.value"
|
:value="setting.value"
|
||||||
:disabled="setting.immutable && setting.value ? true : false"
|
:disabled="setting.immutable && setting.value ? true : false"
|
||||||
:list="getSettingOptions(setting.name, setting.options)"
|
:list="getSettingOptions(setting.name, setting.options)"
|
||||||
@@ -144,10 +144,39 @@ export default {
|
|||||||
return options.map((option) => {
|
return options.map((option) => {
|
||||||
return {
|
return {
|
||||||
value: option.id,
|
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) {
|
async handleInput(settingId, value) {
|
||||||
if (['object', 'array'].includes(typeof value)) {
|
if (['object', 'array'].includes(typeof value)) {
|
||||||
return;
|
return;
|
||||||
@@ -232,7 +261,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fieldNames = immutableFields.map(field =>
|
const fieldNames = immutableFields.map(field =>
|
||||||
this.$t(`settings.personal.label.${field.name}`)
|
this.$t(this.settingLabelTr(field.name))
|
||||||
).join(', ');
|
).join(', ');
|
||||||
|
|
||||||
return this.$t('settings.immutable.supportMessage.specific', { fields: fieldNames });
|
return this.$t('settings.immutable.supportMessage.specific', { fields: fieldNames });
|
||||||
|
|||||||
@@ -9,6 +9,16 @@
|
|||||||
<div v-else class="director-info-container">
|
<div v-else class="director-info-container">
|
||||||
<!-- Linke Seite: Stammdaten & Wissen (aus /falukant/directors) -->
|
<!-- Linke Seite: Stammdaten & Wissen (aus /falukant/directors) -->
|
||||||
<div class="director-main">
|
<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">
|
<h3 class="director-name">
|
||||||
{{ $t('falukant.titles.' + director.character.gender + '.' + director.character.nobleTitle.labelTr) }}
|
{{ $t('falukant.titles.' + director.character.gender + '.' + director.character.nobleTitle.labelTr) }}
|
||||||
{{ director.character.definedFirstName.name }} {{ director.character.definedLastName.name }}
|
{{ director.character.definedFirstName.name }} {{ director.character.definedLastName.name }}
|
||||||
@@ -192,7 +202,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
|
import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
|
||||||
|
import FalukantCharacterVisual from '@/components/falukant/FalukantCharacterVisual.vue';
|
||||||
import { showError, showInfo, showSuccess } from '@/utils/feedback.js';
|
import { showError, showInfo, showSuccess } from '@/utils/feedback.js';
|
||||||
|
import { FALUKANT_VISUAL_DEFAULTS } from '@/utils/falukantVisualSettings.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "DirectorInfo",
|
name: "DirectorInfo",
|
||||||
@@ -200,9 +212,11 @@ export default {
|
|||||||
branchId: { type: Number, required: true },
|
branchId: { type: Number, required: true },
|
||||||
vehicles: { type: Array, default: () => [] },
|
vehicles: { type: Array, default: () => [] },
|
||||||
branches: { type: Array, default: () => [] },
|
branches: { type: Array, default: () => [] },
|
||||||
|
visualSettings: { type: Object, default: () => ({ ...FALUKANT_VISUAL_DEFAULTS }) },
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
NewDirectorDialog
|
NewDirectorDialog,
|
||||||
|
FalukantCharacterVisual,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -484,6 +498,10 @@ export default {
|
|||||||
flex: 2;
|
flex: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.director-visual {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.director-actions {
|
.director-actions {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
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>
|
||||||
@@ -173,6 +173,7 @@
|
|||||||
"flirtSettings": "Flirt",
|
"flirtSettings": "Flirt",
|
||||||
"accountSettings": "Account",
|
"accountSettings": "Account",
|
||||||
"languageAssistantSettings": "Katabang sa pinulongan",
|
"languageAssistantSettings": "Katabang sa pinulongan",
|
||||||
|
"falukantSettings": "Falukant",
|
||||||
"interests": "Mga interes",
|
"interests": "Mga interes",
|
||||||
"adminInterests": "Pagdumala sa interes",
|
"adminInterests": "Pagdumala sa interes",
|
||||||
"adminUsers": "Mga user",
|
"adminUsers": "Mga user",
|
||||||
|
|||||||
@@ -138,6 +138,26 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"title": "Panagway"
|
"title": "Panagway"
|
||||||
},
|
},
|
||||||
|
"falukant": {
|
||||||
|
"title": "Falukant",
|
||||||
|
"label": {
|
||||||
|
"falukantPortraitStyle": "Estilo sa portrait",
|
||||||
|
"falukantFigureMode": "Paagi sa pagpakita"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"falukantPortraitStyle": "Pilia ang estilo sa mga portrait sa Falukant.",
|
||||||
|
"falukantFigureMode": "Pilia kung portraits, 3D nga mga hulagway, o pareho ang ipakita."
|
||||||
|
},
|
||||||
|
"falukantPortraitStyle": {
|
||||||
|
"classic": "Klasiko",
|
||||||
|
"medieval": "Medyebal"
|
||||||
|
},
|
||||||
|
"falukantFigureMode": {
|
||||||
|
"portraits": "Portraits lang",
|
||||||
|
"3d": "3D nga mga hulagway lang",
|
||||||
|
"both": "Portraits ug 3D nga mga hulagway"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sexuality": {
|
"sexuality": {
|
||||||
"title": "Sekswalidad"
|
"title": "Sekswalidad"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
"flirtSettings": "Flirt",
|
"flirtSettings": "Flirt",
|
||||||
"accountSettings": "Account",
|
"accountSettings": "Account",
|
||||||
"languageAssistantSettings": "Sprachassistent",
|
"languageAssistantSettings": "Sprachassistent",
|
||||||
|
"falukantSettings": "Falukant",
|
||||||
"interests": "Interessen",
|
"interests": "Interessen",
|
||||||
"adminInterests": "Interessenverwaltung",
|
"adminInterests": "Interessenverwaltung",
|
||||||
"adminUsers": "Benutzer",
|
"adminUsers": "Benutzer",
|
||||||
|
|||||||
@@ -138,6 +138,26 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"title": "Aussehen"
|
"title": "Aussehen"
|
||||||
},
|
},
|
||||||
|
"falukant": {
|
||||||
|
"title": "Falukant",
|
||||||
|
"label": {
|
||||||
|
"falukantPortraitStyle": "Portrait-Stil",
|
||||||
|
"falukantFigureMode": "Darstellung"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"falukantPortraitStyle": "Wähle den Stil der Falukant-Portraits.",
|
||||||
|
"falukantFigureMode": "Lege fest, ob Portraits, 3D-Figuren oder beides angezeigt werden sollen."
|
||||||
|
},
|
||||||
|
"falukantPortraitStyle": {
|
||||||
|
"classic": "Klassisch",
|
||||||
|
"medieval": "Mittelalterlich"
|
||||||
|
},
|
||||||
|
"falukantFigureMode": {
|
||||||
|
"portraits": "Nur Portraits",
|
||||||
|
"3d": "Nur 3D-Figuren",
|
||||||
|
"both": "Portraits und 3D-Figuren"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sexuality": {
|
"sexuality": {
|
||||||
"title": "Sexualität"
|
"title": "Sexualität"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
"flirtSettings": "Flirt",
|
"flirtSettings": "Flirt",
|
||||||
"accountSettings": "Account",
|
"accountSettings": "Account",
|
||||||
"languageAssistantSettings": "Language assistant",
|
"languageAssistantSettings": "Language assistant",
|
||||||
|
"falukantSettings": "Falukant",
|
||||||
"interests": "Interests",
|
"interests": "Interests",
|
||||||
"adminInterests": "Interest administration",
|
"adminInterests": "Interest administration",
|
||||||
"adminUsers": "Users",
|
"adminUsers": "Users",
|
||||||
|
|||||||
@@ -138,6 +138,26 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"title": "Appearance"
|
"title": "Appearance"
|
||||||
},
|
},
|
||||||
|
"falukant": {
|
||||||
|
"title": "Falukant",
|
||||||
|
"label": {
|
||||||
|
"falukantPortraitStyle": "Portrait style",
|
||||||
|
"falukantFigureMode": "Display mode"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"falukantPortraitStyle": "Choose the style for Falukant portraits.",
|
||||||
|
"falukantFigureMode": "Choose whether Falukant should show portraits, 3D characters, or both."
|
||||||
|
},
|
||||||
|
"falukantPortraitStyle": {
|
||||||
|
"classic": "Classic",
|
||||||
|
"medieval": "Medieval"
|
||||||
|
},
|
||||||
|
"falukantFigureMode": {
|
||||||
|
"portraits": "Portraits only",
|
||||||
|
"3d": "3D characters only",
|
||||||
|
"both": "Portraits and 3D characters"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sexuality": {
|
"sexuality": {
|
||||||
"title": "Sexuality"
|
"title": "Sexuality"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
"flirtSettings": "Coqueteo",
|
"flirtSettings": "Coqueteo",
|
||||||
"accountSettings": "Cuenta",
|
"accountSettings": "Cuenta",
|
||||||
"languageAssistantSettings": "Asistente de idiomas",
|
"languageAssistantSettings": "Asistente de idiomas",
|
||||||
|
"falukantSettings": "Falukant",
|
||||||
"interests": "Intereses",
|
"interests": "Intereses",
|
||||||
"adminInterests": "Administración de intereses",
|
"adminInterests": "Administración de intereses",
|
||||||
"adminUsers": "Usuarios",
|
"adminUsers": "Usuarios",
|
||||||
|
|||||||
@@ -138,6 +138,26 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"title": "Apariencia"
|
"title": "Apariencia"
|
||||||
},
|
},
|
||||||
|
"falukant": {
|
||||||
|
"title": "Falukant",
|
||||||
|
"label": {
|
||||||
|
"falukantPortraitStyle": "Estilo del retrato",
|
||||||
|
"falukantFigureMode": "Modo de visualización"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"falukantPortraitStyle": "Elige el estilo de los retratos de Falukant.",
|
||||||
|
"falukantFigureMode": "Elige si Falukant debe mostrar retratos, figuras 3D o ambos."
|
||||||
|
},
|
||||||
|
"falukantPortraitStyle": {
|
||||||
|
"classic": "Clásico",
|
||||||
|
"medieval": "Medieval"
|
||||||
|
},
|
||||||
|
"falukantFigureMode": {
|
||||||
|
"portraits": "Solo retratos",
|
||||||
|
"3d": "Solo figuras 3D",
|
||||||
|
"both": "Retratos y figuras 3D"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sexuality": {
|
"sexuality": {
|
||||||
"title": "Sexualidad"
|
"title": "Sexualidad"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
"flirtSettings": "flirter",
|
"flirtSettings": "flirter",
|
||||||
"accountSettings": "compte",
|
"accountSettings": "compte",
|
||||||
"languageAssistantSettings": "Assistant vocal",
|
"languageAssistantSettings": "Assistant vocal",
|
||||||
|
"falukantSettings": "Falukant",
|
||||||
"interests": "Interessen",
|
"interests": "Interessen",
|
||||||
"adminInterests": "Gestion des intérêts",
|
"adminInterests": "Gestion des intérêts",
|
||||||
"adminUsers": "Benutzer",
|
"adminUsers": "Benutzer",
|
||||||
|
|||||||
@@ -138,6 +138,26 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"title": "Regarder"
|
"title": "Regarder"
|
||||||
},
|
},
|
||||||
|
"falukant": {
|
||||||
|
"title": "Falukant",
|
||||||
|
"label": {
|
||||||
|
"falukantPortraitStyle": "Style de portrait",
|
||||||
|
"falukantFigureMode": "Mode d'affichage"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"falukantPortraitStyle": "Choisissez le style des portraits Falukant.",
|
||||||
|
"falukantFigureMode": "Choisissez si Falukant doit afficher des portraits, des personnages 3D ou les deux."
|
||||||
|
},
|
||||||
|
"falukantPortraitStyle": {
|
||||||
|
"classic": "Classique",
|
||||||
|
"medieval": "Médiéval"
|
||||||
|
},
|
||||||
|
"falukantFigureMode": {
|
||||||
|
"portraits": "Portraits uniquement",
|
||||||
|
"3d": "Personnages 3D uniquement",
|
||||||
|
"both": "Portraits et personnages 3D"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sexuality": {
|
"sexuality": {
|
||||||
"title": "sexualité"
|
"title": "sexualité"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const SexualitySettingsView = () => import('../views/settings/SexualityView.vue'
|
|||||||
const AccountSettingsView = () => import('../views/settings/AccountView.vue');
|
const AccountSettingsView = () => import('../views/settings/AccountView.vue');
|
||||||
const InterestsView = () => import('../views/settings/InterestsView.vue');
|
const InterestsView = () => import('../views/settings/InterestsView.vue');
|
||||||
const LanguageAssistantView = () => import('../views/settings/LanguageAssistantView.vue');
|
const LanguageAssistantView = () => import('../views/settings/LanguageAssistantView.vue');
|
||||||
|
const FalukantSettingsView = () => import('../views/settings/FalukantView.vue');
|
||||||
|
|
||||||
const settingsRoutes = [
|
const settingsRoutes = [
|
||||||
{
|
{
|
||||||
@@ -49,6 +50,12 @@ const settingsRoutes = [
|
|||||||
component: LanguageAssistantView,
|
component: LanguageAssistantView,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/falukant',
|
||||||
|
name: 'Falukant settings',
|
||||||
|
component: FalukantSettingsView,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default settingsRoutes;
|
export default settingsRoutes;
|
||||||
|
|||||||
60
frontend/src/utils/falukantVisualSettings.js
Normal file
60
frontend/src/utils/falukantVisualSettings.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import apiClient from '@/utils/axios.js';
|
||||||
|
|
||||||
|
export const FALUKANT_VISUAL_DEFAULTS = Object.freeze({
|
||||||
|
portraitStyle: 'classic',
|
||||||
|
figureMode: 'both',
|
||||||
|
});
|
||||||
|
|
||||||
|
export function showFalukantPortraits(figureMode) {
|
||||||
|
return figureMode === 'portraits' || figureMode === 'both';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showFalukant3dFigures(figureMode) {
|
||||||
|
return figureMode === '3d' || figureMode === 'both';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFalukantPortraitAtlas(genderRaw, portraitStyle) {
|
||||||
|
const gender = String(genderRaw || 'male').toLowerCase() === 'female' ? 'female' : 'male';
|
||||||
|
|
||||||
|
if (portraitStyle === 'medieval') {
|
||||||
|
// Falukant liefert hier noch keine Haarfarbe; bis dahin geschlechtsbasierter Atlas-Fallback.
|
||||||
|
return gender === 'female'
|
||||||
|
? '/images/falukant/avatar/female00-medieval.png'
|
||||||
|
: '/images/falukant/avatar/male02-medieval.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/images/falukant/avatar/${gender}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadFalukantVisualSettings(userId) {
|
||||||
|
if (!userId) {
|
||||||
|
return { ...FALUKANT_VISUAL_DEFAULTS };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.post('/api/settings/filter', {
|
||||||
|
userid: userId,
|
||||||
|
type: 'falukant'
|
||||||
|
});
|
||||||
|
|
||||||
|
const values = { ...FALUKANT_VISUAL_DEFAULTS };
|
||||||
|
for (const setting of Array.isArray(data) ? data : []) {
|
||||||
|
const selected = Array.isArray(setting.options)
|
||||||
|
? setting.options.find((option) => String(option.id) === String(setting.value))
|
||||||
|
: null;
|
||||||
|
const normalizedValue = selected?.value || null;
|
||||||
|
|
||||||
|
if (setting.name === 'falukantPortraitStyle' && normalizedValue) {
|
||||||
|
values.portraitStyle = normalizedValue;
|
||||||
|
}
|
||||||
|
if (setting.name === 'falukantFigureMode' && normalizedValue) {
|
||||||
|
values.figureMode = normalizedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading Falukant visual settings:', error);
|
||||||
|
return { ...FALUKANT_VISUAL_DEFAULTS };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,6 +80,7 @@
|
|||||||
:branchId="selectedBranch.id"
|
:branchId="selectedBranch.id"
|
||||||
:vehicles="vehicles"
|
:vehicles="vehicles"
|
||||||
:branches="branches"
|
:branches="branches"
|
||||||
|
:visual-settings="visualSettings"
|
||||||
ref="directorInfo"
|
ref="directorInfo"
|
||||||
@transportCreated="handleTransportCreated"
|
@transportCreated="handleTransportCreated"
|
||||||
/>
|
/>
|
||||||
@@ -386,6 +387,10 @@ import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue';
|
|||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
|
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
|
||||||
|
import {
|
||||||
|
FALUKANT_VISUAL_DEFAULTS,
|
||||||
|
loadFalukantVisualSettings,
|
||||||
|
} from '@/utils/falukantVisualSettings.js';
|
||||||
|
|
||||||
const CERTIFICATE_PRODUCT_LEVELS = [
|
const CERTIFICATE_PRODUCT_LEVELS = [
|
||||||
{ level: 1, products: ['fish', 'meat', 'leather', 'wood', 'stone', 'milk', 'cheese', 'bread', 'wheat', 'grain', 'carrot'] },
|
{ level: 1, products: ['fish', 'meat', 'leather', 'wood', 'stone', 'milk', 'cheese', 'bread', 'wheat', 'grain', 'carrot'] },
|
||||||
@@ -476,6 +481,7 @@ export default {
|
|||||||
inDebtorsPrison: false
|
inDebtorsPrison: false
|
||||||
},
|
},
|
||||||
pendingBranchRefresh: null,
|
pendingBranchRefresh: null,
|
||||||
|
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -528,6 +534,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||||
await this.loadBranches();
|
await this.loadBranches();
|
||||||
|
|
||||||
const branchId = this.$route.params.branchId;
|
const branchId = this.$route.params.branchId;
|
||||||
|
|||||||
@@ -41,10 +41,23 @@
|
|||||||
<tr v-for="person in baptismList" :key="person.id">
|
<tr v-for="person in baptismList" :key="person.id">
|
||||||
<td>{{ $t(`falukant.church.baptism.gender.${person.gender}`) }}</td>
|
<td>{{ $t(`falukant.church.baptism.gender.${person.gender}`) }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
<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" />
|
<input type="text" v-model="person.proposedFirstName" />
|
||||||
<button @click="newName(person)">
|
<button @click="newName(person)">
|
||||||
{{ $t('falukant.church.baptism.table.newName') }}
|
{{ $t('falukant.church.baptism.table.newName') }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ person.age }}</td>
|
<td>{{ person.age }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -76,8 +89,21 @@
|
|||||||
<td>{{ pos.region.name }}</td>
|
<td>{{ pos.region.name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="pos.character">
|
<span v-if="pos.character">
|
||||||
|
<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'}`) }}
|
{{ $t(`falukant.titles.${pos.character.gender}.${pos.character.title || 'noncivil'}`) }}
|
||||||
{{ pos.character.name }}
|
{{ pos.character.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>—</span>
|
<span v-else>—</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -187,7 +213,12 @@ import MessageDialog from '@/dialogues/standard/MessageDialog.vue'
|
|||||||
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue'
|
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue'
|
||||||
import apiClient from '@/utils/axios.js'
|
import apiClient from '@/utils/axios.js'
|
||||||
import SimpleTabs from '@/components/SimpleTabs.vue'
|
import SimpleTabs from '@/components/SimpleTabs.vue'
|
||||||
|
import FalukantCharacterVisual from '@/components/falukant/FalukantCharacterVisual.vue'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
import {
|
||||||
|
FALUKANT_VISUAL_DEFAULTS,
|
||||||
|
loadFalukantVisualSettings,
|
||||||
|
} from '@/utils/falukantVisualSettings.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChurchView',
|
name: 'ChurchView',
|
||||||
@@ -196,6 +227,7 @@ export default {
|
|||||||
MessageDialog,
|
MessageDialog,
|
||||||
ErrorDialog,
|
ErrorDialog,
|
||||||
SimpleTabs,
|
SimpleTabs,
|
||||||
|
FalukantCharacterVisual,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -215,7 +247,8 @@ export default {
|
|||||||
current: false,
|
current: false,
|
||||||
available: false,
|
available: false,
|
||||||
applications: false
|
applications: false
|
||||||
}
|
},
|
||||||
|
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -236,6 +269,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||||
await this.loadNotBaptisedChildren();
|
await this.loadNotBaptisedChildren();
|
||||||
await this.loadOwnCharacterId();
|
await this.loadOwnCharacterId();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -495,6 +529,21 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</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>
|
<style scoped>
|
||||||
h2 {
|
h2 {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
|||||||
@@ -149,10 +149,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="character-media character-media--spouse">
|
<div class="character-media character-media--spouse">
|
||||||
<div
|
<div
|
||||||
|
v-if="showPortraitAvatar"
|
||||||
class="character-avatar character-avatar--spouse"
|
class="character-avatar character-avatar--spouse"
|
||||||
:style="getCharacterAvatarStyle(relationships[0].character2, 0.72)"
|
:style="getCharacterAvatarStyle(relationships[0].character2, 0.72)"
|
||||||
></div>
|
></div>
|
||||||
<div class="character-3d-frame character-3d-frame--spouse">
|
<div v-if="showCharacterFigure3d" class="character-3d-frame character-3d-frame--spouse">
|
||||||
<Character3D
|
<Character3D
|
||||||
:gender="relationships[0].character2.gender"
|
:gender="relationships[0].character2.gender"
|
||||||
:age="relationships[0].character2.age"
|
:age="relationships[0].character2.age"
|
||||||
@@ -328,10 +329,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="character-media character-media--compact child-character-media">
|
<div class="character-media character-media--compact child-character-media">
|
||||||
<div
|
<div
|
||||||
|
v-if="showPortraitAvatar"
|
||||||
class="character-avatar character-avatar--compact"
|
class="character-avatar character-avatar--compact"
|
||||||
:style="getCharacterAvatarStyle(child, 0.5)"
|
:style="getCharacterAvatarStyle(child, 0.5)"
|
||||||
></div>
|
></div>
|
||||||
<div class="character-3d-frame character-3d-frame--compact">
|
<div v-if="showCharacterFigure3d" class="character-3d-frame character-3d-frame--compact">
|
||||||
<Character3D
|
<Character3D
|
||||||
:gender="child.gender"
|
:gender="child.gender"
|
||||||
:age="child.age"
|
:age="child.age"
|
||||||
@@ -406,10 +408,11 @@
|
|||||||
<div class="lover-card__header">
|
<div class="lover-card__header">
|
||||||
<div class="character-media character-media--compact lover-character-media">
|
<div class="character-media character-media--compact lover-character-media">
|
||||||
<div
|
<div
|
||||||
|
v-if="showPortraitAvatar"
|
||||||
class="character-avatar character-avatar--compact"
|
class="character-avatar character-avatar--compact"
|
||||||
:style="getCharacterAvatarStyle(lover, 0.5)"
|
:style="getCharacterAvatarStyle(lover, 0.5)"
|
||||||
></div>
|
></div>
|
||||||
<div class="character-3d-frame character-3d-frame--compact">
|
<div v-if="showCharacterFigure3d" class="character-3d-frame character-3d-frame--compact">
|
||||||
<Character3D
|
<Character3D
|
||||||
:gender="lover.gender"
|
:gender="lover.gender"
|
||||||
:age="lover.age"
|
:age="lover.age"
|
||||||
@@ -581,6 +584,13 @@ import Character3D from '@/components/Character3D.vue'
|
|||||||
import apiClient from '@/utils/axios.js'
|
import apiClient from '@/utils/axios.js'
|
||||||
import { confirmAction, showError, showInfo, showSuccess } from '@/utils/feedback.js'
|
import { confirmAction, showError, showInfo, showSuccess } from '@/utils/feedback.js'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
import {
|
||||||
|
FALUKANT_VISUAL_DEFAULTS,
|
||||||
|
loadFalukantVisualSettings,
|
||||||
|
resolveFalukantPortraitAtlas,
|
||||||
|
showFalukant3dFigures,
|
||||||
|
showFalukantPortraits,
|
||||||
|
} from '@/utils/falukantVisualSettings.js'
|
||||||
|
|
||||||
const WOOING_PROGRESS_TARGET = 70
|
const WOOING_PROGRESS_TARGET = 70
|
||||||
const MARRIAGE_GIFT_COSTS = {
|
const MARRIAGE_GIFT_COSTS = {
|
||||||
@@ -662,11 +672,18 @@ export default {
|
|||||||
selectedChild: null,
|
selectedChild: null,
|
||||||
pendingFamilyRefresh: null,
|
pendingFamilyRefresh: null,
|
||||||
familyTab: 'partner',
|
familyTab: 'partner',
|
||||||
|
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['socket', 'daemonSocket', 'user']),
|
...mapState(['socket', 'daemonSocket', 'user']),
|
||||||
isDev() { return !import.meta.env.PROD },
|
isDev() { return !import.meta.env.PROD },
|
||||||
|
showPortraitAvatar() {
|
||||||
|
return showFalukantPortraits(this.visualSettings.figureMode)
|
||||||
|
},
|
||||||
|
showCharacterFigure3d() {
|
||||||
|
return showFalukant3dFigures(this.visualSettings.figureMode)
|
||||||
|
},
|
||||||
marriageGiftCosts() {
|
marriageGiftCosts() {
|
||||||
return MARRIAGE_GIFT_COSTS;
|
return MARRIAGE_GIFT_COSTS;
|
||||||
},
|
},
|
||||||
@@ -712,6 +729,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||||
await this.loadOwnCharacter();
|
await this.loadOwnCharacter();
|
||||||
await this.loadFamilyData();
|
await this.loadFamilyData();
|
||||||
await this.loadGifts();
|
await this.loadGifts();
|
||||||
@@ -1068,7 +1086,7 @@ export default {
|
|||||||
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
|
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
|
||||||
const safeScale = Number(scale) > 0 ? Number(scale) : 1;
|
const safeScale = Number(scale) > 0 ? Number(scale) : 1;
|
||||||
return {
|
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`,
|
backgroundPosition: `-${Math.round(position.x * safeScale)}px -${Math.round(position.y * safeScale)}px`,
|
||||||
backgroundSize: `${Math.round(1792 * safeScale)}px ${Math.round(1024 * safeScale)}px`,
|
backgroundSize: `${Math.round(1792 * safeScale)}px ${Math.round(1024 * safeScale)}px`,
|
||||||
width: `${Math.round(genderData.width * safeScale)}px`,
|
width: `${Math.round(genderData.width * safeScale)}px`,
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div v-if="falukantUser?.character && !falukantUser?.debtorsPrison?.inDebtorsPrison" class="imagecontainer">
|
<div v-if="falukantUser?.character && !falukantUser?.debtorsPrison?.inDebtorsPrison" class="imagecontainer">
|
||||||
<div :style="getAvatarStyle" class="avatar"></div>
|
<div v-if="showPortraitAvatar" :style="getAvatarStyle" class="avatar"></div>
|
||||||
<div class="house-with-character">
|
<div v-if="showCharacterFigure3d" class="house-with-character">
|
||||||
<div :style="getHouseStyle" class="house"></div>
|
<div :style="getHouseStyle" class="house"></div>
|
||||||
<div class="character-foreground">
|
<div class="character-foreground">
|
||||||
<Character3D
|
<Character3D
|
||||||
@@ -336,6 +336,13 @@ import Character3D from '@/components/Character3D.vue';
|
|||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
|
import {
|
||||||
|
FALUKANT_VISUAL_DEFAULTS,
|
||||||
|
loadFalukantVisualSettings,
|
||||||
|
resolveFalukantPortraitAtlas,
|
||||||
|
showFalukant3dFigures,
|
||||||
|
showFalukantPortraits,
|
||||||
|
} from '@/utils/falukantVisualSettings.js';
|
||||||
|
|
||||||
const AVATAR_POSITIONS = {
|
const AVATAR_POSITIONS = {
|
||||||
male: {
|
male: {
|
||||||
@@ -396,14 +403,21 @@ export default {
|
|||||||
potentialHeirs: [],
|
potentialHeirs: [],
|
||||||
loadingHeirs: false,
|
loadingHeirs: false,
|
||||||
pendingOverviewRefresh: null,
|
pendingOverviewRefresh: null,
|
||||||
|
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['socket', 'daemonSocket', 'user']),
|
...mapState(['socket', 'daemonSocket', 'user']),
|
||||||
|
showPortraitAvatar() {
|
||||||
|
return showFalukantPortraits(this.visualSettings.figureMode);
|
||||||
|
},
|
||||||
|
showCharacterFigure3d() {
|
||||||
|
return showFalukant3dFigures(this.visualSettings.figureMode);
|
||||||
|
},
|
||||||
getAvatarStyle() {
|
getAvatarStyle() {
|
||||||
if (!this.falukantUser || !this.falukantUser.character) return {};
|
if (!this.falukantUser || !this.falukantUser.character) return {};
|
||||||
const { gender, age } = this.falukantUser.character;
|
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 ageGroup = this.getAgeGroup(age);
|
||||||
const genderData = AVATAR_POSITIONS[gender] || {};
|
const genderData = AVATAR_POSITIONS[gender] || {};
|
||||||
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
|
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
|
||||||
@@ -560,6 +574,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||||
await this.fetchFalukantUser();
|
await this.fetchFalukantUser();
|
||||||
if (!this.falukantUser?.character) {
|
if (!this.falukantUser?.character) {
|
||||||
await this.fetchPotentialHeirs();
|
await this.fetchPotentialHeirs();
|
||||||
|
|||||||
@@ -16,11 +16,22 @@
|
|||||||
<span>{{ pos.region.name }}</span>
|
<span>{{ pos.region.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="politics-card__meta">
|
<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">
|
<div class="politics-card__meta-row">
|
||||||
<span class="politics-card__meta-label">{{ $t('falukant.politics.current.holder') }}:</span>
|
<span class="politics-card__meta-label">{{ $t('falukant.politics.current.holder') }}:</span>
|
||||||
<span class="politics-card__meta-value">
|
<span class="politics-card__meta-value">
|
||||||
<template v-if="pos.character">
|
<template v-if="pos.character">
|
||||||
{{ pos.character.definedFirstName.name }} {{ pos.character.definedLastName.name }}
|
{{ formatPoliticalCharacterName(pos.character) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>—</template>
|
<template v-else>—</template>
|
||||||
</span>
|
</span>
|
||||||
@@ -272,16 +283,22 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||||
|
import FalukantCharacterVisual from '@/components/falukant/FalukantCharacterVisual.vue';
|
||||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||||
import Multiselect from 'vue-multiselect';
|
import Multiselect from 'vue-multiselect';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
import {
|
||||||
|
FALUKANT_VISUAL_DEFAULTS,
|
||||||
|
loadFalukantVisualSettings,
|
||||||
|
} from '@/utils/falukantVisualSettings.js';
|
||||||
|
|
||||||
const debugLog = () => {};
|
const debugLog = () => {};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PoliticsView',
|
name: 'PoliticsView',
|
||||||
components: { StatusBar, SimpleTabs, Multiselect },
|
components: { StatusBar, FalukantCharacterVisual, SimpleTabs, Multiselect },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeTab: 'current',
|
activeTab: 'current',
|
||||||
@@ -314,10 +331,12 @@ export default {
|
|||||||
openPolitics: false,
|
openPolitics: false,
|
||||||
elections: false,
|
elections: false,
|
||||||
powers: false
|
powers: false
|
||||||
}
|
},
|
||||||
|
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(['user']),
|
||||||
hasAnyPowers() {
|
hasAnyPowers() {
|
||||||
const p = this.myPowers;
|
const p = this.myPowers;
|
||||||
if (!p) return false;
|
if (!p) return false;
|
||||||
@@ -339,11 +358,17 @@ export default {
|
|||||||
return this.elections.some(e => !e.voted);
|
return this.elections.some(e => !e.voted);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
this.loadOwnCharacterId();
|
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
|
||||||
this.loadCurrentPositions();
|
await this.loadOwnCharacterId();
|
||||||
|
await this.loadCurrentPositions();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatPoliticalCharacterName(character) {
|
||||||
|
const firstName = character?.definedFirstName?.name || '';
|
||||||
|
const lastName = character?.definedLastName?.name || '';
|
||||||
|
return `${firstName} ${lastName}`.trim();
|
||||||
|
},
|
||||||
politicsBenefitItems(pos) {
|
politicsBenefitItems(pos) {
|
||||||
const raw = pos?.benefit;
|
const raw = pos?.benefit;
|
||||||
if (!Array.isArray(raw) || !raw.length) {
|
if (!Array.isArray(raw) || !raw.length) {
|
||||||
@@ -719,6 +744,12 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.politics-card__visual {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.politics-view {
|
.politics-view {
|
||||||
max-width: var(--content-max-width);
|
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>
|
||||||
@@ -123,7 +123,9 @@ private:
|
|||||||
ON fu1.id = c1.user_id
|
ON fu1.id = c1.user_id
|
||||||
LEFT JOIN falukant_data.falukant_user fu2
|
LEFT JOIN falukant_data.falukant_user fu2
|
||||||
ON fu2.id = c2.user_id
|
ON fu2.id = c2.user_id
|
||||||
WHERE random()*100 < (
|
WHERE c1.birthdate <= CURRENT_DATE - INTERVAL '16 days'
|
||||||
|
AND c2.birthdate <= CURRENT_DATE - INTERVAL '16 days'
|
||||||
|
AND random()*100 < (
|
||||||
100.0 /
|
100.0 /
|
||||||
(1
|
(1
|
||||||
+ EXP(
|
+ EXP(
|
||||||
|
|||||||
@@ -118,6 +118,21 @@ private:
|
|||||||
ON rt2.id = rel2.relationship_type_id
|
ON rt2.id = rel2.relationship_type_id
|
||||||
AND rt2.tr = 'engaged'
|
AND rt2.tr = 'engaged'
|
||||||
WHERE p.created_at <= NOW() - INTERVAL '1 day'
|
WHERE p.created_at <= NOW() - INTERVAL '1 day'
|
||||||
|
-- Ein realer Tag entspricht einem Spieljahr: Hochzeit ab 14.
|
||||||
|
AND c.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
||||||
|
AND (
|
||||||
|
(rel2.character1_id = c.id AND EXISTS (
|
||||||
|
SELECT 1 FROM falukant_data."character" partner
|
||||||
|
WHERE partner.id = rel2.character2_id
|
||||||
|
AND partner.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
||||||
|
))
|
||||||
|
OR
|
||||||
|
(rel2.character2_id = c.id AND EXISTS (
|
||||||
|
SELECT 1 FROM falukant_data."character" partner
|
||||||
|
WHERE partner.id = rel2.character1_id
|
||||||
|
AND partner.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
||||||
|
))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
RETURNING character1_id, character2_id
|
RETURNING character1_id, character2_id
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user