diff --git a/backend/docs/falukant-character-development.md b/backend/docs/falukant-character-development.md new file mode 100644 index 0000000..7472c61 --- /dev/null +++ b/backend/docs/falukant-character-development.md @@ -0,0 +1,74 @@ +# Individuelle Charakterentwicklung + +`character.puberty_start_age` und `character.fertile_from_age` sind dauerhaft +gespeicherte ganze Spieljahre. Ein realer Kalendertag entspricht einem Spieljahr. +Die Sequelize-Attribute heißen `pubertyStartAge` und `fertileFromAge`. + +## Vergabe + +Die Migration `20261104000000-add-character-development-ages.cjs` ergänzt beide +Werte für bestehende Charaktere. Ein Datenbanktrigger initialisiert sie auch für +neue Charaktere, einschließlich der direkten SQL-Inserts des Daemons für NPCs +und Neugeborene. Normale Updates, auch das Übergeben von NULL, würfeln bereits +gespeicherte Werte nicht neu aus. Explizite gültige Änderungen bleiben möglich. + +Das vereinfachte Spielmodell verwendet folgende Verteilung: + +- Weibliche Charaktere: Pubertätsbeginn zwischen 8 und 13 Spieljahren. +- Männliche und sonstige Charaktere: Pubertätsbeginn zwischen 9 und 14 Spieljahren. +- Zwei gemittelte Zufallsziehungen gewichten die Mitte stärker als die Ränder. +- Fruchtbarkeitsalter: Pubertätsbeginn plus zufällig zwei oder drei Spieljahre. + +Die Abstände und Wahrscheinlichkeiten sind Modellentscheidungen. Sie stellen +keine medizinische Vorhersage und keine Schätzung mittelalterlicher +Bevölkerungsdaten dar. Der Beginn der Pubertät und das Erreichen der +Fortpflanzungsfähigkeit werden als getrennte Ereignisse modelliert. Als +Orientierung für die Bandbreite dienen die Informationen des +[NHS zum Pubertätsbeginn](https://www.nhs.uk/conditions/early-or-delayed-puberty/) +und der [Endocrine Society zur zeitlichen Entwicklung](https://www.endocrine.org/patient-engagement/endocrine-library/precocious-puberty). + +## Spielregeln und API + +Die Heiratsgrenze bleibt unabhängig davon bei 14 Spieljahren für beide +Verlobten. Automatische Schwangerschaften in bestehenden Ehen und Liebschaften +sowie der Backend-Sonderweg nach einer Hochzeit prüfen das individuelle +Fruchtbarkeitsalter beider Eltern. Die bisherigen Beziehungsarten, +Schwangerschaftschancen und Schwangerschaftsdauern gelten weiterhin. + +Eine bereits geplante Geburt wird nicht nachträglich durch die neuen Werte +blockiert. Die Migration verändert weder Schwangerschaftstermine noch +Beziehungen. Admin-Funktionen zum expliziten Setzen von Schwangerschaften +bleiben manuelle Eingriffe. + +Die Charakterdaten unter `/api/falukant/info` enthalten beide Alterswerte. +Die Familienantwort enthält für den eigenen Charakter, Partner und Kinder +zusätzlich `development` mit beiden Werten sowie `hasReachedPuberty` und +`hasReachedFertility`. Diese booleschen Werte werden beim Abruf aus dem Alter +abgeleitet. Es gibt keine Änderungen an Porträts oder 3D-Modellen. + +## Bereitstellung + +Zuerst die Backend-Migration ausführen, dann Backend und Daemon gemeinsam +aktualisieren bzw. neu starten. Beide Anwendungen benötigen die neuen Spalten. +Die Daemon-Anpassungen liegen im benachbarten Repository `ypdaemon`, in +`src/worker/sql.rs`. Ein Rollback muss auch die vorherigen Anwendungsversionen +wiederherstellen, bevor die neuen Spalten entfernt werden. + +## Prüfung + +```sh +cd backend +node --test tools/characterDevelopment.test.mjs +``` + +Die Datenbanktests sind ohne `TEST_DATABASE_URL` übersprungen. Mit einer +explizit angegebenen, isolierten PostgreSQL-Datenbank namens +`yourpart_development_test` prüfen sie Migration, Backfill, direkte Inserts, +stabile Werte und Rollback. Diese Tests ersetzen die Schemas `falukant_data` +und `falukant_type` ausschließlich in dieser Testdatenbank. + +Wird zusätzlich `DAEMON_SQL_PATH` auf `ypdaemon/src/worker/sql.rs` gesetzt, +führen sie die tatsächlichen Daemon-Abfragen aus: beide Eltern genau an und +unter ihrer Altersgrenze, Ehe- und Liebschaftswege sowie bestehende Geburten. +Für die Prüfung der Voraussetzungen wird der Zufallswurf im Test auf null +gesetzt. diff --git a/backend/migrations-active/20261104000000-add-character-development-ages.cjs b/backend/migrations-active/20261104000000-add-character-development-ages.cjs new file mode 100644 index 0000000..665ba61 --- /dev/null +++ b/backend/migrations-active/20261104000000-add-character-development-ages.cjs @@ -0,0 +1,64 @@ +'use strict'; + +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.sequelize.query(` + ALTER TABLE falukant_data.character + ADD COLUMN puberty_start_age SMALLINT, + ADD COLUMN fertile_from_age SMALLINT; + + -- Shared by Sequelize and raw daemon INSERTs, including newborns and NPCs. + -- One real calendar day is one game year. These are game distributions, + -- not medical predictions or reconstructed medieval population data. + CREATE FUNCTION falukant_data.initialize_character_development() + RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + IF TG_OP = 'UPDATE' THEN + NEW.puberty_start_age := COALESCE(NEW.puberty_start_age, OLD.puberty_start_age); + NEW.fertile_from_age := COALESCE(NEW.fertile_from_age, OLD.fertile_from_age); + END IF; + IF NEW.puberty_start_age IS NULL THEN + NEW.puberty_start_age := + CASE WHEN lower(NEW.gender) IN ('female', 'f') THEN 8 ELSE 9 END + + floor((random() + random()) * 3)::int; + END IF; + IF NEW.fertile_from_age IS NULL THEN + NEW.fertile_from_age := NEW.puberty_start_age + 2 + floor(random() * 2)::int; + END IF; + RETURN NEW; + END; + $$; + + CREATE TRIGGER initialize_character_development + BEFORE INSERT OR UPDATE ON falukant_data.character + FOR EACH ROW EXECUTE FUNCTION falukant_data.initialize_character_development(); + + -- Existing pregnancies and relationships are deliberately untouched. + UPDATE falukant_data.character SET puberty_start_age = puberty_start_age; + + ALTER TABLE falukant_data.character + ALTER COLUMN puberty_start_age SET NOT NULL, + ALTER COLUMN fertile_from_age SET NOT NULL, + ADD CONSTRAINT character_development_ages_valid CHECK ( + puberty_start_age BETWEEN 8 AND 18 + AND fertile_from_age > puberty_start_age + AND fertile_from_age <= 21 + ); + `, { transaction }); + }); + }, + + async down(queryInterface) { + await queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.sequelize.query(` + DROP TRIGGER initialize_character_development ON falukant_data.character; + DROP FUNCTION falukant_data.initialize_character_development(); + ALTER TABLE falukant_data.character + DROP CONSTRAINT character_development_ages_valid, + DROP COLUMN puberty_start_age, + DROP COLUMN fertile_from_age; + `, { transaction }); + }); + } +}; diff --git a/backend/models/falukant/data/character.js b/backend/models/falukant/data/character.js index 74f36e2..d472ca9 100755 --- a/backend/models/falukant/data/character.js +++ b/backend/models/falukant/data/character.js @@ -23,6 +23,17 @@ FalukantCharacter.init( defaultValue: DataTypes.NOW}, gender: { type: DataTypes.STRING}, + // Initialized once by the DB trigger, also for raw daemon INSERTs. + // Nullable here so Sequelize lets omitted values reach that trigger; + // the database enforces NOT NULL and the ordering of both ages. + pubertyStartAge: { + type: DataTypes.SMALLINT, + allowNull: true, + }, + fertileFromAge: { + type: DataTypes.SMALLINT, + allowNull: true, + }, health: { type: DataTypes.INTEGER, allowNull: false, diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 426b2de..0a0c084 100755 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -5,6 +5,7 @@ import FalukantPredefineFirstname from '../models/falukant/predefine/firstname.j import FalukantPredefineLastname from '../models/falukant/predefine/lastname.js'; import FalukantUser from '../models/falukant/data/user.js'; import FalukantCharacter from '../models/falukant/data/character.js'; +import { getCharacterDevelopment, hasReachedFertility } from '../utils/falukant/characterDevelopment.js'; import RegionData from '../models/falukant/data/region.js'; import RegionType from '../models/falukant/type/region.js'; import FalukantStock from '../models/falukant/data/stock.js'; @@ -96,7 +97,6 @@ function calcAge(birthdate) { const FAMILY_AGE = Object.freeze({ MIN_WOOING: 10, MIN_MARRIAGE: 14, - MIN_HOUSEHOLD_AND_CHILDREN: 14, }); async function getFalukantUserOrFail(hashedId) { @@ -979,7 +979,7 @@ class FalukantService extends BaseService { { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'id', 'level'] } ], // reputation: für Adel-Voraussetzungen u.a. (checkReputationRequirement nutzt user.character ohne Nachladen) - attributes: ['id', 'birthdate', 'gender', 'moodId', 'health', 'reputation'] + attributes: ['id', 'birthdate', 'gender', 'moodId', 'health', 'reputation', 'pubertyStartAge', 'fertileFromAge'] }, ] }); @@ -1141,7 +1141,7 @@ class FalukantService extends BaseService { { model: FalukantCharacter, as: 'character', - attributes: ['id', 'birthdate', 'health', 'reputation', 'titleOfNobility'], + attributes: ['id', 'birthdate', 'health', 'reputation', 'titleOfNobility', 'pubertyStartAge', 'fertileFromAge'], include: [ { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] }, @@ -3649,11 +3649,11 @@ class FalukantService extends BaseService { const parents = await FalukantCharacter.unscoped().findAll({ where: { id: [motherId, fatherId] }, - attributes: ['id', 'birthdate'], + attributes: ['id', 'birthdate', 'fertileFromAge'], }); if ( parents.length !== 2 - || parents.some((parent) => calcAge(parent.birthdate) < FAMILY_AGE.MIN_HOUSEHOLD_AND_CHILDREN) + || parents.some((parent) => !hasReachedFertility(parent, calcAge(parent.birthdate))) ) { return { scheduled: false }; } @@ -3733,7 +3733,7 @@ class FalukantService extends BaseService { RelationshipType.findAll({ where: { id: typeIds }, attributes: ['id', 'tr'] }), FalukantCharacter.findAll({ where: { id: char2Ids }, - attributes: ['id', 'birthdate', 'gender', 'moodId'], + attributes: ['id', 'birthdate', 'gender', 'moodId', 'pubertyStartAge', 'fertileFromAge'], include: [ { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }, @@ -3774,6 +3774,7 @@ class FalukantService extends BaseService { character2: c2 ? { id: c2.id, age: calcAge(c2.birthdate), + development: getCharacterDevelopment(c2, calcAge(c2.birthdate)), gender: c2.gender, firstName: c2.definedFirstName?.name || 'Unknown', nobleTitle: c2.nobleTitle?.labelTr || '', @@ -3820,7 +3821,7 @@ class FalukantService extends BaseService { const childChars = childCharIds.length ? await FalukantCharacter.findAll({ where: { id: childCharIds }, - attributes: ['id', 'birthdate', 'gender', 'regionId', 'titleOfNobility'], + attributes: ['id', 'birthdate', 'gender', 'regionId', 'titleOfNobility', 'pubertyStartAge', 'fertileFromAge'], include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }] }) : []; @@ -3882,6 +3883,7 @@ class FalukantService extends BaseService { name: kid?.definedFirstName?.name || 'Unknown', gender: kid?.gender, age: kid?.birthdate ? calcAge(kid.birthdate) : null, + development: getCharacterDevelopment(kid, kid?.birthdate ? calcAge(kid.birthdate) : null), hasName: rel.nameSet, isHeir: rel.isHeir || false, legitimacy: rel.legitimacy || 'legitimate', @@ -3998,8 +4000,13 @@ class FalukantService extends BaseService { label: baseTension.label }; const family = { + development: getCharacterDevelopment(character, calcAge(character.birthdate)), + minMarriageAge: FAMILY_AGE.MIN_MARRIAGE, relationships: activeRelationships.map((r) => ({ ...r, + canCelebrateWedding: r.relationshipType === 'engaged' + && calcAge(character.birthdate) >= FAMILY_AGE.MIN_MARRIAGE + && r.character2?.age >= FAMILY_AGE.MIN_MARRIAGE, marriageSatisfaction: r.relationshipType === 'married' ? (r.state?.marriageSatisfaction ?? 100) : null, @@ -5739,7 +5746,7 @@ class FalukantService extends BaseService { }); const musicTypes = await MusicType.findAll(); const banquetteTypes = await BanquetteType.findAll(); - return { partyTypes, musicTypes, banquetteTypes, weddingCandidates }; + return { partyTypes, musicTypes, banquetteTypes, weddingCandidates, minMarriageAge: FAMILY_AGE.MIN_MARRIAGE }; } async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio, relationshipId = null) { diff --git a/backend/tools/characterDevelopment.test.mjs b/backend/tools/characterDevelopment.test.mjs new file mode 100644 index 0000000..895b62b --- /dev/null +++ b/backend/tools/characterDevelopment.test.mjs @@ -0,0 +1,150 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import { Sequelize } from 'sequelize'; +import migration from '../migrations-active/20261104000000-add-character-development-ages.cjs'; +import { getCharacterDevelopment, hasReachedFertility } from '../utils/falukant/characterDevelopment.js'; + +test('development follows each character, independently of the marriage age', () => { + const early = { pubertyStartAge: 10, fertileFromAge: 13 }; + const late = { pubertyStartAge: 13, fertileFromAge: 16 }; + assert.equal(hasReachedFertility(early, 12), false); + assert.equal(hasReachedFertility(early, 13), true); + assert.equal(hasReachedFertility(late, 14), false); + assert.equal(hasReachedFertility(late, 16), true); + assert.deepEqual(getCharacterDevelopment(early, 11), { + ...early, hasReachedPuberty: true, hasReachedFertility: false, + }); + for (const missing of [null, {}, { fertileFromAge: null }, { fertileFromAge: 0 }]) { + assert.equal(hasReachedFertility(missing, 30), false); + } + assert.equal(getCharacterDevelopment(early, null).hasReachedPuberty, false); +}); + +// Only an explicitly provided, disposable test database may be used. +// DAEMON_SQL_PATH additionally exercises the actual Rust SQL against PostgreSQL. +test('database migration, persistence and daemon conception paths', { + skip: !process.env.TEST_DATABASE_URL, +}, async (t) => { + const db = new Sequelize(process.env.TEST_DATABASE_URL, { logging: false }); + try { + const [[{ name }]] = await db.query('SELECT current_database() AS name'); + assert.equal(name, 'yourpart_development_test', 'Use the isolated development test database'); + await db.query(` + DROP SCHEMA IF EXISTS falukant_data CASCADE; + DROP SCHEMA IF EXISTS falukant_type CASCADE; + CREATE SCHEMA falukant_data; + CREATE SCHEMA falukant_type; + CREATE TABLE falukant_data.character ( + id SERIAL PRIMARY KEY, gender TEXT, birthdate TIMESTAMPTZ DEFAULT NOW(), + user_id INT, title_of_nobility INT DEFAULT 1, last_name INT DEFAULT 1, + region_id INT DEFAULT 1, health INT DEFAULT 100, + pregnancy_due_at TIMESTAMPTZ, pregnancy_father_character_id INT, + updated_at TIMESTAMPTZ DEFAULT NOW() + ); + INSERT INTO falukant_data.character (gender, pregnancy_due_at) + VALUES ('female', NOW() + INTERVAL '1 hour'), ('male', NULL); + `); + const qi = db.getQueryInterface(); + await migration.up(qi); + const [before] = await db.query('SELECT * FROM falukant_data.character ORDER BY id'); + assert.ok(before[0].pregnancy_due_at); + assert.equal(before[0].puberty_start_age >= 8, true); + assert.equal(before[0].fertile_from_age > before[0].puberty_start_age, true); + await db.query("UPDATE falukant_data.character SET health = 99, puberty_start_age = NULL, fertile_from_age = NULL"); + const [after] = await db.query('SELECT * FROM falukant_data.character ORDER BY id'); + assert.deepEqual(after.map(c => [c.puberty_start_age, c.fertile_from_age]), + before.map(c => [c.puberty_start_age, c.fertile_from_age])); + assert.deepEqual(after[0].pregnancy_due_at, before[0].pregnancy_due_at); + + // Raw inserts represent newborns/NPCs created outside Sequelize. + await db.query(`INSERT INTO falukant_data.character (gender) + SELECT CASE WHEN i % 2 = 0 THEN 'female' ELSE 'male' END + FROM generate_series(1, 1000) i`); + const [[stats]] = await db.query(`SELECT + count(DISTINCT puberty_start_age)::int AS starts, + count(DISTINCT fertile_from_age)::int AS fertile, + count(*) FILTER (WHERE fertile_from_age <= puberty_start_age)::int AS invalid + FROM falukant_data.character`); + assert.ok(stats.starts > 1 && stats.fertile > 1); + assert.equal(stats.invalid, 0); + await assert.rejects(db.query(`INSERT INTO falukant_data.character + (puberty_start_age, fertile_from_age) VALUES (12, 11)`)); + + await t.test('daemon checks both parents and preserves already scheduled births', { + skip: !process.env.DAEMON_SQL_PATH, + }, async () => { + const source = await readFile(process.env.DAEMON_SQL_PATH, 'utf8'); + const sql = (name) => { + const match = source.match(new RegExp(`pub const ${name}: &str = r#"([\\s\\S]*?)"#;`)); + assert.ok(match, name); + // Remove chance from these eligibility tests only. + return match[1].replaceAll('random()', '0.0'); + }; + await db.query(` + TRUNCATE falukant_data.character RESTART IDENTITY; + CREATE TABLE falukant_type.relationship (id INT PRIMARY KEY, tr TEXT); + INSERT INTO falukant_type.relationship VALUES (1, 'married'), (2, 'lover'); + CREATE TABLE falukant_data.relationship ( + id INT PRIMARY KEY, character1_id INT, character2_id INT, + relationship_type_id INT, marriage_pregnancy_due_at TIMESTAMPTZ + ); + CREATE TABLE falukant_data.falukant_user (id INT PRIMARY KEY, user_id INT); + CREATE TABLE falukant_data.child_relation (mother_character_id INT, created_at TIMESTAMPTZ); + CREATE TABLE falukant_data.relationship_state ( + relationship_id INT, active BOOLEAN, affection INT, maintenance_level INT, + last_monthly_processed_at TIMESTAMPTZ, lover_role TEXT, visibility INT + ); + INSERT INTO falukant_data.character + (gender, birthdate, puberty_start_age, fertile_from_age) + VALUES ('female', CURRENT_DATE - INTERVAL '13 days', 10, 13), + ('male', CURRENT_DATE - INTERVAL '13 days', 10, 13); + INSERT INTO falukant_data.relationship VALUES (1, 1, 2, 1, NULL); + INSERT INTO falukant_data.relationship_state VALUES (1, true, 80, 50, NOW(), 'lover', 20); + `); + for (const [queryName, type] of [ + ['QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE', 1], + ['QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES', 1], + ['QUERY_GET_LOVER_PREGNANCY_CANDIDATES', 2], + ]) { + for (const [motherAge, fatherAge, eligible] of [[13, 13, true], [12, 13, false], [13, 12, false]]) { + await db.query(`UPDATE falukant_data.character SET birthdate = CURRENT_DATE - + (CASE WHEN id = 1 THEN :motherAge ELSE :fatherAge END) * INTERVAL '1 day', + pregnancy_due_at = NULL; + UPDATE falukant_data.relationship SET relationship_type_id = :type, + marriage_pregnancy_due_at = NULL`, { + replacements: { motherAge, fatherAge, type }, + }); + const [rows] = await db.query(sql(queryName)); + if (queryName === 'QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE') { + const [[relationship]] = await db.query('SELECT marriage_pregnancy_due_at FROM falukant_data.relationship'); + assert.equal(relationship.marriage_pregnancy_due_at != null, eligible, `${queryName}: ${motherAge}/${fatherAge}`); + } else { + assert.equal(rows.length > 0, eligible, `${queryName}: ${motherAge}/${fatherAge}`); + } + } + } + await db.query(`UPDATE falukant_data.relationship SET marriage_pregnancy_due_at = NULL`); + for (const [motherAge, fatherAge, eligible] of [[12, 13, false], [13, 12, false], [13, 13, true]]) { + await db.query(`UPDATE falukant_data.character SET birthdate = CURRENT_DATE - + (CASE WHEN id = 1 THEN :motherAge ELSE :fatherAge END) * INTERVAL '1 day', + pregnancy_due_at = NULL`, { replacements: { motherAge, fatherAge } }); + const [rows] = await db.query(sql('QUERY_SET_CHARACTER_PREGNANCY_FROM_LOVER'), { bind: [1, 2] }); + assert.equal(rows.length > 0, eligible); + } + // A backfilled threshold must not cancel an existing marriage pregnancy. + await db.query(`UPDATE falukant_data.character SET fertile_from_age = 17, pregnancy_due_at = NULL; + UPDATE falukant_data.relationship SET relationship_type_id = 1, + marriage_pregnancy_due_at = NOW() - INTERVAL '1 hour'`); + const [births] = await db.query(sql('QUERY_GET_MARRIAGE_BIRTH_DELIVERIES')); + assert.equal(births.length, 1); + }); + await migration.down(qi); + const [[columns]] = await db.query(`SELECT count(*)::int AS count FROM information_schema.columns + WHERE table_schema = 'falukant_data' AND table_name = 'character' + AND column_name IN ('puberty_start_age', 'fertile_from_age')`); + assert.equal(columns.count, 0); + } finally { + await db.close(); + } +}); diff --git a/backend/utils/falukant/characterDevelopment.js b/backend/utils/falukant/characterDevelopment.js new file mode 100644 index 0000000..cda1219 --- /dev/null +++ b/backend/utils/falukant/characterDevelopment.js @@ -0,0 +1,17 @@ +// Ages are whole game years (one real calendar day per year). +export function hasReachedFertility(character, age) { + const threshold = character?.fertileFromAge; + return Number.isInteger(threshold) && threshold > 0 + && Number.isFinite(age) && age >= threshold; +} + +export function getCharacterDevelopment(character, age) { + const pubertyStartAge = character?.pubertyStartAge ?? null; + return { + pubertyStartAge, + fertileFromAge: character?.fertileFromAge ?? null, + hasReachedPuberty: Number.isInteger(pubertyStartAge) && pubertyStartAge > 0 + && Number.isFinite(age) && age >= pubertyStartAge, + hasReachedFertility: hasReachedFertility(character, age), + }; +} diff --git a/frontend/src/i18n/locales/ceb/falukant.json b/frontend/src/i18n/locales/ceb/falukant.json index 41ede09..ffd855d 100755 --- a/frontend/src/i18n/locales/ceb/falukant.json +++ b/frontend/src/i18n/locales/ceb/falukant.json @@ -1056,6 +1056,7 @@ } }, "spouse": { + "weddingAgeHint": "Mahimong magsaulog og kasal kon ang duha ka managtrato labing menos {age} ka tuig na sa dula.", "traitsToggle": "Character traits", "marriageSatisfaction": "Kasal Satisfaction", "marriageState": "Kasal State", @@ -1315,6 +1316,7 @@ "order": "Umara og party", "inProgress": "Mga party nga giandam", "completed": "Nahuman nga mga party", + "weddingUnavailable": "Ang kasal nagkinahanglan og managtrato nga ang duha labing menos {age} ka tuig na sa dula.", "newpartyview": { "open": "Create New Party", "close": "Hide New Party", diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index 958eca1..331d10b 100755 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -667,6 +667,7 @@ "dueHint": "Voraussichtlicher Geburtstermin" }, "spouse": { + "weddingAgeHint": "Eine Hochzeitsfeier ist möglich, sobald beide Verlobten mindestens {age} Spieljahre alt sind.", "title": "Beziehung", "traitsToggle": "Charaktereigenschaften", "name": "Name", @@ -1302,6 +1303,7 @@ "order": "Fest veranstalten", "inProgress": "Feste in Vorbereitung", "completed": "Abgeschlossene Feste", + "weddingUnavailable": "Für eine Hochzeit muss ein verlobtes Paar vorhanden sein, bei dem beide mindestens {age} Spieljahre alt sind.", "newpartyview": { "open": "Neues Fest erstellen", "close": "Neues Fest verbergen", diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index f5e596f..0c0d7dd 100755 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -937,6 +937,7 @@ } }, "spouse": { + "weddingAgeHint": "You can hold a wedding celebration once both partners are at least {age} game years old.", "traitsToggle": "Character traits", "marriageSatisfaction": "Marriage Satisfaction", "marriageState": "Marriage State", @@ -1282,6 +1283,7 @@ "order": "Order Party", "inProgress": "Parties in Preparation", "completed": "Completed Parties", + "weddingUnavailable": "A wedding requires an engaged couple whose partners are both at least {age} game years old.", "newpartyview": { "open": "Create New Party", "close": "Hide New Party", diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json index 2b9265d..3b9fb9e 100755 --- a/frontend/src/i18n/locales/es/falukant.json +++ b/frontend/src/i18n/locales/es/falukant.json @@ -655,6 +655,7 @@ "dueHint": "Fecha prevista de nacimiento" }, "spouse": { + "weddingAgeHint": "Puedes celebrar una boda cuando ambos prometidos tengan al menos {age} años de juego.", "title": "Relación", "traitsToggle": "Rasgos de carácter", "name": "Nombre", @@ -1276,6 +1277,7 @@ "order": "Organizar fiesta", "inProgress": "Fiestas en preparación", "completed": "Fiestas completadas", + "weddingUnavailable": "Una boda requiere una pareja comprometida en la que ambos tengan al menos {age} años de juego.", "newpartyview": { "open": "Crear nueva fiesta", "close": "Ocultar nueva fiesta", diff --git a/frontend/src/i18n/locales/fr/falukant.json b/frontend/src/i18n/locales/fr/falukant.json index cdeaeaf..0c8abc9 100755 --- a/frontend/src/i18n/locales/fr/falukant.json +++ b/frontend/src/i18n/locales/fr/falukant.json @@ -659,6 +659,7 @@ "dueHint": "Date d'échéance prévue" }, "spouse": { + "weddingAgeHint": "Une fête de mariage est possible dès que les deux fiancés ont au moins {age} ans dans le jeu.", "title": "Relation", "traitsToggle": "Traits de caractère", "name": "nom", @@ -1271,6 +1272,7 @@ "order": "Organiser un festival", "inProgress": "Des festivals en préparation", "completed": "Festivals terminés", + "weddingUnavailable": "Un mariage nécessite un couple fiancé dont les deux partenaires ont au moins {age} ans dans le jeu.", "newpartyview": { "open": "Créer un nouveau festival", "close": "Masquer le nouveau festival", diff --git a/frontend/src/views/falukant/FamilyView.vue b/frontend/src/views/falukant/FamilyView.vue index ae5927f..c8fac2d 100755 --- a/frontend/src/views/falukant/FamilyView.vue +++ b/frontend/src/views/falukant/FamilyView.vue @@ -132,8 +132,9 @@
- +

{{ $t('falukant.family.spouse.weddingAgeHint', { age: minMarriageAge }) }}

[child.childCharacterId, this.selectedChildProposalIds[child.childCharacterId] || null]) diff --git a/frontend/src/views/falukant/ReputationView.vue b/frontend/src/views/falukant/ReputationView.vue index eeeec53..e87fd2e 100755 --- a/frontend/src/views/falukant/ReputationView.vue +++ b/frontend/src/views/falukant/ReputationView.vue @@ -66,6 +66,9 @@
+

+ {{ $t('falukant.reputation.party.weddingUnavailable', { age: minMarriageAge }) }} +