Dokumentiere Schwangerschaften bei Verlobten und prüfe Übergänge
All checks were successful
Deploy to production / deploy (push) Successful in 2m35s

This commit is contained in:
Torsten Schulz (local)
2026-11-04 15:01:55 +01:00
parent 8e8e92aa62
commit c64f599fd7
5 changed files with 93 additions and 9 deletions

View File

@@ -32,10 +32,22 @@ und der [Endocrine Society zur zeitlichen Entwicklung](https://www.endocrine.org
## Spielregeln und API
Die Heiratsgrenze bleibt unabhängig davon bei 14 Spieljahren für beide
Verlobten. Automatische Schwangerschaften in bestehenden Ehen und Liebschaften
Verlobten. Automatische Schwangerschaften in bestehenden Ehen, Verlobungen und Liebschaften
sowie der Backend-Sonderweg nach einer Hochzeit prüfen das individuelle
Fruchtbarkeitsalter beider Eltern. Die bisherigen Beziehungsarten,
Schwangerschaftschancen und Schwangerschaftsdauern gelten weiterhin.
Fruchtbarkeitsalter beider Eltern. Verlobungen bleiben als Beziehungsstatus erhalten.
Verlobte benötigen keinen Liebschaftsunterhalt oder vorherigen Abrechnungslauf.
Die Zuneigung muss mindestens 45 betragen; bei fehlendem Beziehungszustand gilt
wie im Backend der Standardwert 50. Inaktive Beziehungen und verstorbene Partner
sind ausgeschlossen. Die Chance beträgt pro Prüfung 4 Prozent, ab Zuneigung 75
zwei Prozentpunkte mehr; hohe Sichtbarkeit bei Zuneigung unter 50 reduziert sie
um zwei Prozentpunkte. Schwangerschaftsdauer und Sperren gegen parallele
Schwangerschaften entsprechen dem bestehenden Liebschaftsweg.
Eine Geburt während der Verlobung nutzt die vorhandene Kennzeichnung für
außereheliche Kinder (`birth_context = 'lover'`, zunächst `hidden_bastard` und
nicht öffentlich bekannt). Heiratet das Paar vor der Geburt, wird das Kind über
den bestehenden Geburtsweg als ehelich erfasst. Die Schwangerschaft bleibt dabei
erhalten. Für diese Freischaltung ist keine weitere Datenbankmigration nötig.
Eine bereits geplante Geburt wird nicht nachträglich durch die neuen Werte
blockiert. Die Migration verändert weder Schwangerschaftstermine noch

View File

@@ -98,7 +98,7 @@ test('database migration, persistence and daemon conception paths', {
await t.test('daemon checks both parents and preserves already scheduled births', {
skip: !process.env.DAEMON_SQL_PATH,
}, async () => {
}, async (t) => {
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]*?)"#;`));
@@ -109,13 +109,17 @@ test('database migration, persistence and daemon conception paths', {
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');
INSERT INTO falukant_type.relationship VALUES (1, 'married'), (2, 'lover'), (3, 'engaged'), (4, 'wooing');
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.child_relation (
father_character_id INT, mother_character_id INT, child_character_id INT,
name_set BOOLEAN, legitimacy TEXT, birth_context TEXT, public_known BOOLEAN,
created_at TIMESTAMPTZ, updated_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
@@ -131,6 +135,7 @@ test('database migration, persistence and daemon conception paths', {
['QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE', 1],
['QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES', 1],
['QUERY_GET_LOVER_PREGNANCY_CANDIDATES', 2],
['QUERY_GET_LOVER_PREGNANCY_CANDIDATES', 3],
]) {
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 -
@@ -149,6 +154,73 @@ test('database migration, persistence and daemon conception paths', {
}
}
}
await t.test('engaged couples need no lover maintenance and retain their relationship', async () => {
const candidates = () => db.query(sql('QUERY_GET_LOVER_PREGNANCY_CANDIDATES')).then(([rows]) => rows);
await db.query(`UPDATE falukant_data.character SET birthdate = CURRENT_DATE - INTERVAL '13 days',
pregnancy_due_at = NULL, health = 100;
UPDATE falukant_data.relationship SET relationship_type_id = 3, marriage_pregnancy_due_at = NULL;
UPDATE falukant_data.relationship_state SET affection = 50, lover_role = NULL,
maintenance_level = 0, last_monthly_processed_at = NULL`);
let rows = await candidates();
assert.equal(rows.length, 1);
assert.equal(rows[0].relationship_type, 'engaged');
assert.equal(rows[0].mother_cid, 1);
assert.equal(rows[0].father_cid, 2);
// Selection and conception must work for either partner as the player.
await db.query('INSERT INTO falukant_data.falukant_user VALUES (10, 100)');
for (const playerId of [1, 2]) {
await db.query('UPDATE falukant_data.character SET user_id = CASE WHEN id = :id THEN 10 ELSE NULL END', {
replacements: { id: playerId },
});
rows = await candidates();
assert.equal(rows.length, 1);
assert.equal(rows[0][playerId === 1 ? 'mother_uid' : 'father_uid'], 10);
}
await db.query('UPDATE falukant_data.relationship SET character1_id = 2, character2_id = 1');
rows = await candidates();
assert.equal(rows[0].mother_cid, 1);
assert.equal(rows[0].father_cid, 2);
for (const id of [1, 2]) {
await db.query('UPDATE falukant_data.character SET health = 0 WHERE id = :id', { replacements: { id } });
assert.equal((await candidates()).length, 0);
await db.query('UPDATE falukant_data.character SET health = 100');
}
await db.query('UPDATE falukant_data.relationship_state SET active = false');
assert.equal((await candidates()).length, 0);
await db.query('UPDATE falukant_data.relationship_state SET active = true, affection = 44');
assert.equal((await candidates()).length, 0);
await db.query('DELETE FROM falukant_data.relationship_state');
assert.equal((await candidates()).length, 1);
for (const type of [1, 2, 4]) {
await db.query('UPDATE falukant_data.relationship SET relationship_type_id = :type', { replacements: { type } });
assert.equal((await candidates()).length, 0);
}
await db.query('UPDATE falukant_data.relationship SET relationship_type_id = 3');
await db.query(sql('QUERY_SET_CHARACTER_PREGNANCY_FROM_LOVER'), { bind: [1, 2] });
assert.equal((await candidates()).length, 0, 'Existing pregnancy blocks another conception');
const [[pregnancy]] = await db.query('SELECT pregnancy_due_at, pregnancy_father_character_id FROM falukant_data.character WHERE id = 1');
assert.equal(pregnancy.pregnancy_father_character_id, 2);
assert.ok(pregnancy.pregnancy_due_at > new Date());
const [notDue] = await db.query(sql('QUERY_GET_PLANNED_CHARACTER_BIRTH_DELIVERIES'));
assert.equal(notDue.length, 0);
await db.query("UPDATE falukant_data.character SET pregnancy_due_at = NOW() - INTERVAL '1 hour' WHERE id = 1");
for (const [type, context] of [[3, 'lover'], [1, 'marriage']]) {
await db.query('UPDATE falukant_data.relationship SET relationship_type_id = :type', { replacements: { type } });
const [births] = await db.query(sql('QUERY_GET_PLANNED_CHARACTER_BIRTH_DELIVERIES'));
assert.equal(births.length, 1);
assert.equal(births[0].birth_context, context);
const [[stillPregnant]] = await db.query('SELECT pregnancy_father_character_id FROM falukant_data.character WHERE id = 1');
assert.equal(stillPregnant.pregnancy_father_character_id, 2);
}
await db.query('UPDATE falukant_data.relationship SET relationship_type_id = 3');
await db.query(sql('QUERY_INSERT_CHILD_RELATION_PLANNED_BIRTH'), { bind: [2, 1, 3, 'lover'] });
const [[child]] = await db.query('SELECT legitimacy, birth_context, public_known FROM falukant_data.child_relation');
assert.deepEqual(child, { legitimacy: 'hidden_bastard', birth_context: 'lover', public_known: false });
const [[relationship]] = await db.query('SELECT relationship_type_id FROM falukant_data.relationship');
assert.equal(relationship.relationship_type_id, 3);
await db.query('DELETE FROM falukant_data.child_relation');
});
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 -

View File

@@ -704,7 +704,7 @@
},
"mood": "Stimmung",
"progress": "Zuneigung",
"jumpToPartyForm": "Hochzeitsfeier veranstalten (Nötig für Hochzeit und Kinder)"
"jumpToPartyForm": "Hochzeitsfeier veranstalten"
},
"marriageState": {
"stable": "Stabil",

View File

@@ -692,7 +692,7 @@
},
"mood": "Estado de ánimo",
"progress": "Afecto",
"jumpToPartyForm": "Organizar banquete de boda (necesario para boda e hijos)"
"jumpToPartyForm": "Organizar banquete de boda"
},
"marriageState": {
"stable": "Estable",

View File

@@ -696,7 +696,7 @@
},
"mood": "Humeur",
"progress": "affection",
"jumpToPartyForm": "Organiser une fête de mariage (nécessaire pour le mariage et les enfants)"
"jumpToPartyForm": "Organiser une fête de mariage"
},
"marriageState": {
"stable": "Écurie",