diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js
index dae4501..4b5fe95 100644
--- a/backend/services/falukantService.js
+++ b/backend/services/falukantService.js
@@ -6965,6 +6965,66 @@ ORDER BY r.id`,
}
}
+ async getChurchCareerInfo(characterId) {
+ const [currentOffices, approvedApplications] = await Promise.all([
+ ChurchOffice.findAll({
+ where: { characterId },
+ include: [{
+ model: ChurchOfficeType,
+ as: 'type',
+ attributes: ['id', 'name', 'hierarchyLevel']
+ }]
+ }),
+ ChurchApplication.findAll({
+ where: {
+ characterId,
+ status: 'approved'
+ },
+ include: [{
+ model: ChurchOfficeType,
+ as: 'officeType',
+ attributes: ['id', 'name', 'hierarchyLevel']
+ }]
+ })
+ ]);
+
+ const currentLevels = currentOffices.map((office) => Number(office.type?.hierarchyLevel ?? -1));
+ const approvedLevels = approvedApplications.map((application) => Number(application.officeType?.hierarchyLevel ?? -1));
+ const highestCurrentLevel = currentLevels.length ? Math.max(...currentLevels) : -1;
+ const highestEverLevel = [...currentLevels, ...approvedLevels].length
+ ? Math.max(...currentLevels, ...approvedLevels)
+ : -1;
+
+ const currentHighest = currentOffices
+ .map((office) => office.type)
+ .filter(Boolean)
+ .sort((a, b) => Number(b.hierarchyLevel || 0) - Number(a.hierarchyLevel || 0))[0] || null;
+
+ const allAttained = [
+ ...currentOffices.map((office) => office.type),
+ ...approvedApplications.map((application) => application.officeType)
+ ]
+ .filter(Boolean)
+ .sort((a, b) => Number(b.hierarchyLevel || 0) - Number(a.hierarchyLevel || 0));
+
+ const highestEver = allAttained[0] || null;
+
+ return {
+ highestCurrentLevel,
+ highestEverLevel,
+ highestCurrentOffice: currentHighest ? {
+ id: currentHighest.id,
+ name: currentHighest.name,
+ hierarchyLevel: currentHighest.hierarchyLevel
+ } : null,
+ highestEverOffice: highestEver ? {
+ id: highestEver.id,
+ name: highestEver.name,
+ hierarchyLevel: highestEver.hierarchyLevel
+ } : null
+ };
+ }
+
async getAvailableChurchPositions(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
@@ -6975,6 +7035,8 @@ ORDER BY r.id`,
return [];
}
+ const churchCareer = await this.getChurchCareerInfo(character.id);
+
// Prüfe welche Kirchenämter der Charakter bereits innehat
const heldOffices = await ChurchOffice.findAll({
where: { characterId: character.id },
@@ -7029,7 +7091,8 @@ ORDER BY r.id`,
const availablePositions = [];
for (const officeType of officeTypes) {
- // Prüfe Voraussetzungen: Hat der User bereits das erforderliche niedrigere Amt?
+ // Prüfe Voraussetzungen: Maßgeblich ist die höchste bisherige Kirchenlaufbahn,
+ // nicht nur das aktuell gehaltene Amt.
const requirement = officeType.requirements?.[0];
console.log(`[getAvailableChurchPositions] Checking ${officeType.name} (id=${officeType.id}, hierarchyLevel=${officeType.hierarchyLevel}):`, {
@@ -7043,9 +7106,12 @@ ORDER BY r.id`,
// Wenn eine Voraussetzung definiert ist
const prerequisiteId = requirement.prerequisiteOfficeTypeId;
if (prerequisiteId !== null && prerequisiteId !== undefined) {
- // Prüfe ob der User das erforderliche Amt innehat
- if (!heldOfficeTypeIds.includes(prerequisiteId)) {
- console.log(`[getAvailableChurchPositions] Skipping ${officeType.name}: User doesn't have prerequisite office ${prerequisiteId}. Held offices:`, heldOfficeTypeIds);
+ const prerequisiteOffice = await ChurchOfficeType.findByPk(prerequisiteId, {
+ attributes: ['id', 'hierarchyLevel']
+ });
+ const requiredLevel = Number(prerequisiteOffice?.hierarchyLevel ?? (officeType.hierarchyLevel - 1));
+ if (churchCareer.highestEverLevel < requiredLevel) {
+ console.log(`[getAvailableChurchPositions] Skipping ${officeType.name}: User career too low for prerequisite level ${requiredLevel}. Highest ever:`, churchCareer.highestEverLevel);
continue; // Voraussetzung nicht erfüllt - User hat das erforderliche Amt nicht
}
}
@@ -7113,6 +7179,7 @@ ORDER BY r.id`,
if (availableSeats > 0) {
// Finde den Supervisor (höheres Amt in derselben Region oder Eltern-Region)
let supervisor = null;
+ let decisionMode = officeType.hierarchyLevel === 0 ? 'entry' : 'interim';
const higherOfficeTypeIds = await ChurchOfficeType.findAll({
where: {
hierarchyLevel: { [Op.gt]: officeType.hierarchyLevel }
@@ -7135,7 +7202,21 @@ ORDER BY r.id`,
{
model: FalukantCharacter,
as: 'holder',
- attributes: ['id']
+ attributes: ['id', 'userId'],
+ include: [
+ {
+ model: FalukantPredefineFirstname,
+ as: 'definedFirstName',
+ attributes: ['name'],
+ required: false
+ },
+ {
+ model: FalukantPredefineLastname,
+ as: 'definedLastName',
+ attributes: ['name'],
+ required: false
+ }
+ ]
}
],
order: [
@@ -7147,8 +7228,10 @@ ORDER BY r.id`,
if (supervisorOffice && supervisorOffice.holder) {
supervisor = {
id: supervisorOffice.holder.id,
- name: 'Supervisor' // Wird später geladen falls nötig
+ name: `${supervisorOffice.holder.definedFirstName?.name || ''} ${supervisorOffice.holder.definedLastName?.name || ''}`.trim() || 'Supervisor',
+ controlledBy: supervisorOffice.holder.userId ? 'player' : 'npc'
};
+ decisionMode = supervisor.controlledBy;
}
}
@@ -7163,7 +7246,8 @@ ORDER BY r.id`,
},
regionId: region.id,
availableSeats: availableSeats,
- supervisor: supervisor
+ supervisor: supervisor,
+ decisionMode
});
}
}
@@ -7280,13 +7364,31 @@ ORDER BY r.id`,
attributes: ['id', 'regionId']
});
if (!character) {
- throw new Error('Character not found');
+ throw new Error('falukant.church.available.errors.characterNotFound');
}
+ const churchCareer = await this.getChurchCareerInfo(character.id);
+
// Prüfe ob Position verfügbar ist
const officeType = await ChurchOfficeType.findByPk(officeTypeId);
if (!officeType) {
- throw new Error('Office type not found');
+ throw new Error('falukant.church.available.errors.officeTypeNotFound');
+ }
+
+ const requirement = await ChurchOfficeRequirement.findOne({
+ where: { officeTypeId },
+ attributes: ['prerequisiteOfficeTypeId']
+ });
+ if (requirement?.prerequisiteOfficeTypeId) {
+ const prerequisiteOffice = await ChurchOfficeType.findByPk(requirement.prerequisiteOfficeTypeId, {
+ attributes: ['hierarchyLevel']
+ });
+ const requiredLevel = Number(prerequisiteOffice?.hierarchyLevel ?? (officeType.hierarchyLevel - 1));
+ if (churchCareer.highestEverLevel < requiredLevel) {
+ throw new Error('falukant.church.available.errors.churchCareerTooLow');
+ }
+ } else if (officeType.hierarchyLevel > 0 && churchCareer.highestEverLevel < officeType.hierarchyLevel - 1) {
+ throw new Error('falukant.church.available.errors.churchCareerTooLow');
}
const occupiedCount = await ChurchOffice.count({
@@ -7297,7 +7399,7 @@ ORDER BY r.id`,
});
if (occupiedCount >= officeType.seatsPerRegion) {
- throw new Error('No available seats');
+ throw new Error('falukant.church.available.errors.noAvailableSeats');
}
// Finde Supervisor (nur wenn es nicht die niedrigste Position ist)
@@ -7334,16 +7436,13 @@ ORDER BY r.id`,
limit: 1
});
- if (!supervisorOffice) {
- throw new Error('No supervisor position exists in this region. Higher church offices must be filled before you can apply.');
+ if (supervisorOffice?.holder) {
+ supervisorId = supervisorOffice.holder.id;
+ } else {
+ supervisorId = null;
}
- if (!supervisorOffice.holder) {
- const officeName = supervisorOffice.type?.name || 'higher';
- throw new Error(`The ${officeName} position in this region is vacant. It must be filled before you can apply.`);
- }
- supervisorId = supervisorOffice.holder.id;
} else {
- throw new Error('No supervisor office type found');
+ supervisorId = null;
}
}
// Für Einstiegspositionen (hierarchyLevel 0) ist kein Supervisor erforderlich
@@ -7359,7 +7458,7 @@ ORDER BY r.id`,
});
if (existingApplication) {
- throw new Error('Application already exists');
+ throw new Error('falukant.church.available.errors.applicationAlreadyExists');
}
// Erstelle Bewerbung
diff --git a/docs/FALUKANT_CHURCH_ADVANCEMENT_DAEMON_SPEC.md b/docs/FALUKANT_CHURCH_ADVANCEMENT_DAEMON_SPEC.md
new file mode 100644
index 0000000..32957dd
--- /dev/null
+++ b/docs/FALUKANT_CHURCH_ADVANCEMENT_DAEMON_SPEC.md
@@ -0,0 +1,328 @@
+# Falukant: Kirchenämter, Aufstieg und NPC-Besetzung
+
+Dieses Dokument beschreibt das Zielmodell für das Kirchensystem in Falukant. Es fokussiert auf drei Probleme:
+
+- Spieler können sich derzeit nicht sinnvoll auf höhere kirchliche Ämter bewerben.
+- Nicht alle Ämter werden besetzt.
+- NPCs sollen sich ebenfalls bewerben und Ämter aktiv besetzen.
+
+Der Daemon soll die laufende Besetzung und Beförderung übernehmen. Der eigentliche Antrag des Spielers bleibt ein aktiver Spielzug in der UI.
+
+## 1. Zielbild
+
+Kirchliche Ämter sollen ein lebendes Hierarchiesystem sein:
+
+- Leere Ämter werden nach und nach besetzt.
+- Spieler und NPCs konkurrieren um offene Positionen.
+- Höhere Amtsträger entscheiden über untere Ebenen.
+- Wo kein Spieler-Entscheider vorhanden ist, übernimmt ein NPC-Amtsträger die Entscheidung.
+- Wo ganze Hierarchieebenen leer sind, darf das System kontrolliert von unten nach oben oder über Interimslogik nachbesetzen.
+
+## 2. Grundregeln
+
+### 2.1 Bewerbung
+
+- Ein Spieler beantragt ein Amt weiterhin aktiv über die UI.
+- NPCs bewerben sich nicht per UI, sondern durch den Daemon.
+- Es darf gleichzeitig mehrere Bewerber für dieselbe Position geben.
+- Eine Bewerbung ist immer regionsbezogen.
+
+### 2.2 Hierarchie
+
+Kirchenämter bleiben über `church_office_type.hierarchy_level` geordnet.
+
+Der normale Aufstiegspfad ist:
+
+1. `lay-preacher`
+2. `village-priest`
+3. `parish-priest`
+4. `dean`
+5. `archdeacon`
+6. `bishop`
+7. `archbishop`
+8. `cardinal`
+9. `pope`
+
+### 2.3 Bewerbung auf höhere Ämter
+
+Der aktuelle Fehler "man kann sich nicht auf höhere Positionen bewerben als man gerade hat" soll ersetzt werden durch:
+
+- Ein Charakter darf sich auf das nächsthöhere sinnvolle Amt bewerben.
+- Zusätzlich darf ein Charakter sich auf ein höheres Amt bewerben, wenn sein bisher höchstes Kirchenamt die Mindestvoraussetzung erfüllt.
+- Das System soll nicht nur aktuelle Ämter, sondern auch die bisher höchste kirchliche Laufbahn berücksichtigen.
+
+Daraus folgt:
+
+- Es reicht nicht, nur aktuelle `church_office` zu prüfen.
+- Es muss ein Konzept von `highestChurchOfficeRankEver` geben.
+
+## 3. Entscheidungsmodell für Bewerbungen
+
+### 3.1 Grundsatz
+
+Über eine Bewerbung entscheidet immer das direkt übergeordnete Amt.
+
+Beispiele:
+
+- Über `village-priest` entscheidet `parish-priest`.
+- Über `parish-priest` entscheidet `dean`.
+- Über `dean` entscheidet `archdeacon`.
+
+### 3.2 Wenn der direkte Vorgesetzte fehlt
+
+Falls das direkt übergeordnete Amt in der relevanten Aufsichtskette nicht besetzt ist:
+
+- Das System sucht das nächsthöhere besetzte Amt.
+- Falls überhaupt kein höheres Amt vorhanden ist, greift ein Interimsmodus.
+
+Interimsmodus:
+
+- Für die untersten Ebenen darf der Daemon nach Reputation und Eignung direkt besetzen.
+- Für hohe Ämter oberhalb von `bishop` soll das nur sehr zurückhaltend geschehen.
+
+## 4. NPC-Bewerbungen
+
+### 4.1 Ziel
+
+NPCs sollen das Kirchensystem lebendig halten und offene Ämter nach und nach füllen.
+
+### 4.2 Wann NPCs sich bewerben
+
+Der Daemon prüft täglich:
+
+- offene Sitze pro Region und Amt
+- vorhandene Spielerbewerbungen
+- vorhandene NPC-Kandidaten
+
+NPC-Bewerbungen entstehen bevorzugt wenn:
+
+- ein Amt offen ist
+- keine ausreichende Zahl an Bewerbungen existiert
+- in der Region oder der Elternregion geeignete NPCs vorhanden sind
+
+### 4.3 Geeignete NPCs
+
+Ein NPC ist grundsätzlich geeignet, wenn:
+
+- er lebt
+- er nicht bereits ein gleiches oder höheres unvereinbares Kirchenamt innehat
+- sein bisher höchstes Kirchenamt oder seine bisherige Laufbahn die Stufe plausibel macht
+- seine Reputation ausreichend ist
+
+Zusätzliche Faktoren für NPC-Eignung:
+
+- Alter
+- Gesundheit
+- Adelstitel
+- Reputation
+- bestehendes Kirchenamt
+- bisher höchstes Kirchenamt
+
+## 5. Auswahl- und Beförderungslogik
+
+### 5.1 Bewertungswert
+
+Für jede Bewerbung wird ein Score berechnet:
+
+`churchCandidateScore`
+
+Bestandteile:
+
+- bisher höchstes Kirchenamt
+- aktuelles Kirchenamt
+- Reputation
+- Adelstitel
+- Alter in idealem Bereich
+- regionale Nähe
+- ggf. geringe Bonuspunkte für lange Wartezeit
+
+### 5.2 Entscheidung durch Spieler
+
+Wenn der zuständige Vorgesetzte ein Spielercharakter ist:
+
+- Die Bewerbung erscheint wie bisher in der UI.
+- Der Spieler kann annehmen oder ablehnen.
+- Solange eine Spielerentscheidung aussteht, entscheidet der Daemon nicht automatisch.
+
+Optionaler Timeout:
+
+- Nach längerer Untätigkeit darf später ein automatischer Verfall oder eine automatische Daemon-Entscheidung ergänzt werden.
+- Das ist nicht Teil der ersten Ausbaustufe.
+
+### 5.3 Entscheidung durch NPC
+
+Wenn der zuständige Vorgesetzte ein NPC ist:
+
+- Der Daemon entscheidet automatisch.
+- Maßgeblich ist primär der Bewerber-Score.
+- Zusätzlich wirkt die Reputation des NPC-Vorgesetzten als "Strengefaktor".
+
+## 6. Reputation des NPC-Vorgesetzten
+
+Wenn ein NPC ein Amt innehat, entscheidet er über die unter ihm liegende Position anhand von Reputation.
+
+Das bedeutet:
+
+- Ein angesehener NPC-Vorgesetzter bevorzugt reputationsstarke, standesgemäße und stabile Bewerber.
+- Ein schwacher oder verrufener NPC-Vorgesetzter entscheidet unberechenbarer.
+
+Empfohlenes Modell:
+
+- `supervisorInfluence = supervisor.reputation / 100`
+- je höher dieser Wert, desto stärker zählt der objektive Bewerber-Score
+- bei niedriger Reputation steigt der Zufallsanteil
+
+Praktische Wirkung:
+
+- Hohe NPC-Reputation:
+ - bessere, berechenbarere Besetzung
+- Niedrige NPC-Reputation:
+ - mehr Fehlbesetzungen
+ - mehr schwankende Entscheidungen
+
+## 7. Fehlende historische Kirchenlaufbahn
+
+Damit ein Charakter sich später auf höhere Ämter bewerben kann, braucht das System mehr als nur aktuelle `church_office`.
+
+Es wird deshalb ein persistierter Höchstwert benötigt:
+
+- `highestChurchOfficeRankEver`
+
+Empfehlung:
+
+- eigener Wert am Charakter oder in einer Laufbahntabelle
+- beim erstmaligen Erreichen eines höheren Kirchenamts aktualisieren
+- bei Verlust des Amts nicht zurücksetzen
+
+Ohne diesen Wert bleibt höherer Aufstieg nach Amtsverlust oder Umstrukturierung unzuverlässig.
+
+## 8. Verfügbarkeit in der UI
+
+Die UI soll später drei Dinge klar darstellen:
+
+- aktuelle Ämter
+- verfügbare Bewerbungen
+- eigene höchste Kirchenlaufbahn
+
+Zusätzlich sinnvoll:
+
+- ob die Entscheidung durch einen Spieler oder NPC getroffen wird
+- wer der zuständige Vorgesetzte ist
+- ob eine Position automatisch nachbesetzt wird
+
+## 9. Daemon-Aufgaben
+
+Der Daemon soll täglich folgende Schritte ausführen:
+
+### 9.1 Kirchenlage erfassen
+
+- offene Sitze je `church_office_type` und Region zählen
+- aktuelle Amtsträger laden
+- Spielerbewerbungen laden
+- NPC-Kandidaten bestimmen
+
+### 9.2 NPC-Bewerbungen erzeugen
+
+- für vakante Positionen fehlende NPC-Bewerbungen anlegen
+- keine Doppelbewerbungen für dieselbe Position erzeugen
+
+### 9.3 Bewerbungen bewerten
+
+- Bewerber-Score berechnen
+- zuständigen Vorgesetzten ermitteln
+- falls NPC-Vorgesetzter: Entscheidung automatisch treffen
+- falls Spieler-Vorgesetzter: Bewerbung offen lassen
+
+### 9.4 Beförderungen und Besetzungen durchführen
+
+- `church_office` anlegen oder aktualisieren
+- alte widersprechende Bewerbungen schließen
+- `highestChurchOfficeRankEver` aktualisieren
+
+### 9.5 Sonderfall komplett leere Hierarchie
+
+Wenn eine Hierarchiestufe samt Vorgesetzten fehlt:
+
+- untere Ebene darf durch den Daemon interimistisch mit dem besten Kandidaten besetzt werden
+- dies soll selten und regelgeleitet geschehen
+- für hohe Spitzenämter deutlich restriktiver als für niedrige Ämter
+
+## 10. Event-Kommunikation zwischen Daemon und UI
+
+Neue oder präzisierte Events:
+
+### 10.1 `falukantUpdateChurch`
+
+```json
+{
+ "event": "falukantUpdateChurch",
+ "user_id": 123,
+ "reason": "applications"
+}
+```
+
+Zulässige `reason`-Werte:
+
+- `applications`
+- `appointment`
+- `promotion`
+- `vacancy_fill`
+- `npc_decision`
+
+### 10.2 UI-Reaktion
+
+- `applications`:
+ - Bewerbungslisten neu laden
+- `appointment`:
+ - aktuelle Ämter und verfügbare Ämter neu laden
+- `promotion`:
+ - aktuelle Ämter, verfügbare Ämter, ggf. Sozialstatus/Ansehen neu laden
+- `vacancy_fill`:
+ - aktuelle Ämter und verfügbare Positionen neu laden
+- `npc_decision`:
+ - supervised applications und current positions neu laden
+
+Zusätzlich kann weiterhin `falukantUpdateStatus` gesendet werden.
+
+## 11. Backend-Anpassungen außerhalb des Daemons
+
+Die Daemon-Logik allein reicht nicht. Das Backend muss angepasst werden:
+
+- `getAvailableChurchPositions()` darf nicht nur aktuelle Ämter als Voraussetzung ansehen
+- es muss die bisher höchste Kirchenlaufbahn berücksichtigen
+- freie Positionen dürfen nicht nur an schon exakt lineare Amtshalter gebunden sein
+- Spielerbewerbungen und NPC-Bewerbungen müssen dieselbe Bewertungslogik unterstützen
+
+## 12. Empfohlene Umsetzung in Phasen
+
+### Phase C1
+
+- Konzept `highestChurchOfficeRankEver` einführen
+- `getAvailableChurchPositions()` auf höchste Kirchenlaufbahn erweitern
+- UI lesbar machen
+
+### Phase C2
+
+- NPC-Bewerbungen im Daemon
+- automatische NPC-Entscheidungen
+
+### Phase C3
+
+- Interimsbesetzung für leere Hierarchien
+- Feintuning von Reputation und Zufall
+
+## 13. Wichtige Designentscheidungen
+
+- Spieleraufstieg bleibt antragsbasiert
+- NPCs füllen das System aktiv auf
+- hohe Reputation eines NPC-Vorgesetzten verbessert die Besetzungsqualität
+- höhere Ämter sollen auch dann erreichbar bleiben, wenn der Charakter das Voramt nicht mehr aktuell innehat
+- komplett leere Kirchenstrukturen dürfen sich wieder aufbauen
+
+## 14. Offene Punkte
+
+- Wo genau `highestChurchOfficeRankEver` gespeichert wird
+- ob es zusätzlich `highestChurchOfficeTypeEver` geben soll
+- ob automatische NPC-Entscheidungen ein Timeout für offene Spielerbewerbungen bekommen
+- wie stark Reputation gegenüber Adelstitel und Alter gewichtet wird
+
diff --git a/frontend/src/components/DashboardWidget.vue b/frontend/src/components/DashboardWidget.vue
index 3236452..4f54a40 100644
--- a/frontend/src/components/DashboardWidget.vue
+++ b/frontend/src/components/DashboardWidget.vue
@@ -108,7 +108,7 @@ export default {
},
setupSocketListeners() {
this.teardownSocketListeners();
- const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'];
+ const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'];
if (this.daemonSocket) {
this._daemonMessageHandler = (event) => {
if (event.data === 'ping') return;
@@ -126,6 +126,9 @@ export default {
this._familySocketHandler = (data) => {
if (this.matchesCurrentUser(data)) this.queueFetchData();
};
+ this._churchSocketHandler = (data) => {
+ if (this.matchesCurrentUser(data)) this.queueFetchData();
+ };
this._childrenSocketHandler = (data) => {
if (this.matchesCurrentUser(data)) this.queueFetchData();
};
@@ -136,6 +139,7 @@ export default {
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
+ this.socket.on('falukantUpdateChurch', this._churchSocketHandler);
this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('falukantBranchUpdate', this._branchSocketHandler);
@@ -149,6 +153,7 @@ export default {
if (this.socket) {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
+ if (this._churchSocketHandler) this.socket.off('falukantUpdateChurch', this._churchSocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._branchSocketHandler) this.socket.off('falukantBranchUpdate', this._branchSocketHandler);
diff --git a/frontend/src/components/falukant/StatusBar.vue b/frontend/src/components/falukant/StatusBar.vue
index b5df39d..7397b0c 100644
--- a/frontend/src/components/falukant/StatusBar.vue
+++ b/frontend/src/components/falukant/StatusBar.vue
@@ -176,6 +176,7 @@ export default {
if (!this.socket) return;
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
+ this._churchSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateChurch', ...data });
this._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
this._productionCertificateSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
this._stockSocketHandler = (data) => this.handleEvent({ event: 'stock_change', ...data });
@@ -183,6 +184,7 @@ export default {
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
+ this.socket.on('falukantUpdateChurch', this._churchSocketHandler);
this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('stock_change', this._stockSocketHandler);
@@ -192,6 +194,7 @@ export default {
if (this.socket) {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
+ if (this._churchSocketHandler) this.socket.off('falukantUpdateChurch', this._churchSocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler);
@@ -204,7 +207,7 @@ export default {
this._daemonHandler = (event) => {
try {
const data = JSON.parse(event.data);
- if (['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'].includes(data.event)) {
+ if (['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'].includes(data.event)) {
this.handleEvent(data);
}
} catch (_) {}
@@ -243,6 +246,7 @@ export default {
switch (eventData.event) {
case 'falukantUpdateStatus':
case 'falukantUpdateFamily':
+ case 'falukantUpdateChurch':
case 'children_update':
case 'falukantUpdateProductionCertificate':
case 'stock_change':
diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json
index 3d09e78..5fb1251 100644
--- a/frontend/src/i18n/locales/de/falukant.json
+++ b/frontend/src/i18n/locales/de/falukant.json
@@ -1024,10 +1024,18 @@
"church": {
"title": "Kirche",
"tabs": {
+ "baptism": "Taufen",
"current": "Aktuelle Positionen",
"available": "Verfügbare Positionen",
"applications": "Bewerbungen"
},
+ "summary": {
+ "highestCurrentOffice": "Höchstes aktuelles Amt",
+ "availableApplications": "Mögliche Bewerbungen",
+ "supervisedApplications": "Zu entscheidende Bewerbungen",
+ "guidance": "Kirchenämter steigen stufenweise auf. Über Bewerbungen entscheidet in der Regel das nächsthöhere Amt; falls dort kein Spieler sitzt, kann später ein NPC entscheiden.",
+ "none": "Noch kein Kirchenamt"
+ },
"current": {
"office": "Amt",
"region": "Region",
@@ -1039,11 +1047,25 @@
"office": "Amt",
"region": "Region",
"supervisor": "Vorgesetzter",
+ "decision": "Entscheidung durch",
+ "decisionType": {
+ "entry": "Direkter Einstieg",
+ "player": "Spieler",
+ "npc": "NPC",
+ "interim": "Interim"
+ },
"seats": "Verfügbare Plätze",
"action": "Aktion",
"apply": "Bewerben",
"applySuccess": "Bewerbung erfolgreich eingereicht.",
"applyError": "Fehler beim Einreichen der Bewerbung.",
+ "errors": {
+ "characterNotFound": "Dein Charakter konnte nicht gefunden werden.",
+ "officeTypeNotFound": "Das Kirchenamt wurde nicht gefunden.",
+ "churchCareerTooLow": "Deine bisherige kirchliche Laufbahn reicht für dieses Amt noch nicht aus.",
+ "noAvailableSeats": "Für dieses Kirchenamt sind derzeit keine Plätze frei.",
+ "applicationAlreadyExists": "Für dieses Kirchenamt in dieser Region besteht bereits eine offene Bewerbung."
+ },
"none": "Keine verfügbaren Positionen."
},
"applications": {
diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json
index 1a67465..8b39401 100644
--- a/frontend/src/i18n/locales/en/falukant.json
+++ b/frontend/src/i18n/locales/en/falukant.json
@@ -651,10 +651,18 @@
"church": {
"title": "Church",
"tabs": {
+ "baptism": "Baptism",
"current": "Current Positions",
"available": "Available Positions",
"applications": "Applications"
},
+ "summary": {
+ "highestCurrentOffice": "Highest current office",
+ "availableApplications": "Possible applications",
+ "supervisedApplications": "Applications to decide",
+ "guidance": "Church offices usually progress step by step. Applications are normally decided by the next higher office; if no player holds it, an NPC may later decide.",
+ "none": "No church office yet"
+ },
"current": {
"office": "Office",
"region": "Region",
@@ -666,11 +674,25 @@
"office": "Office",
"region": "Region",
"supervisor": "Supervisor",
+ "decision": "Decision by",
+ "decisionType": {
+ "entry": "Direct entry",
+ "player": "Player",
+ "npc": "NPC",
+ "interim": "Interim"
+ },
"seats": "Available Seats",
"action": "Action",
"apply": "Apply",
"applySuccess": "Application submitted successfully.",
"applyError": "Error submitting application.",
+ "errors": {
+ "characterNotFound": "Your character could not be found.",
+ "officeTypeNotFound": "The church office could not be found.",
+ "churchCareerTooLow": "Your previous church career is not yet sufficient for this office.",
+ "noAvailableSeats": "There are currently no free seats for this church office.",
+ "applicationAlreadyExists": "There is already an open application for this church office in this region."
+ },
"none": "No available positions."
},
"applications": {
diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json
index 321abce..045e3e5 100644
--- a/frontend/src/i18n/locales/es/falukant.json
+++ b/frontend/src/i18n/locales/es/falukant.json
@@ -967,6 +967,64 @@
},
"church": {
"title": "Iglesia",
+ "tabs": {
+ "baptism": "Bautizos",
+ "current": "Cargos actuales",
+ "available": "Cargos disponibles",
+ "applications": "Solicitudes"
+ },
+ "summary": {
+ "highestCurrentOffice": "Cargo actual más alto",
+ "availableApplications": "Solicitudes posibles",
+ "supervisedApplications": "Solicitudes por decidir",
+ "guidance": "Los cargos eclesiásticos suelen ascender paso a paso. Las solicitudes normalmente las decide el cargo inmediatamente superior; si no hay jugador en ese puesto, más adelante puede decidir un NPC.",
+ "none": "Todavía sin cargo eclesiástico"
+ },
+ "current": {
+ "office": "Cargo",
+ "region": "Región",
+ "holder": "Titular",
+ "supervisor": "Superior",
+ "none": "No hay cargos actuales."
+ },
+ "available": {
+ "office": "Cargo",
+ "region": "Región",
+ "supervisor": "Superior",
+ "decision": "Decide",
+ "decisionType": {
+ "entry": "Acceso directo",
+ "player": "Jugador",
+ "npc": "NPC",
+ "interim": "Interino"
+ },
+ "seats": "Plazas disponibles",
+ "action": "Acción",
+ "apply": "Solicitar",
+ "applySuccess": "Solicitud enviada correctamente.",
+ "applyError": "Error al enviar la solicitud.",
+ "errors": {
+ "characterNotFound": "No se pudo encontrar tu personaje.",
+ "officeTypeNotFound": "No se encontró el cargo eclesiástico.",
+ "churchCareerTooLow": "Tu trayectoria eclesiástica todavía no es suficiente para este cargo.",
+ "noAvailableSeats": "Actualmente no hay plazas libres para este cargo eclesiástico.",
+ "applicationAlreadyExists": "Ya existe una solicitud abierta para este cargo eclesiástico en esta región."
+ },
+ "none": "No hay cargos disponibles."
+ },
+ "applications": {
+ "office": "Cargo",
+ "region": "Región",
+ "applicant": "Solicitante",
+ "date": "Fecha",
+ "action": "Acción",
+ "approve": "Aceptar",
+ "reject": "Rechazar",
+ "approveSuccess": "Solicitud aceptada.",
+ "rejectSuccess": "Solicitud rechazada.",
+ "decideError": "Error al tomar la decisión.",
+ "none": "No hay solicitudes."
+ },
"baptism": {
"title": "Bautizos",
"table": {
diff --git a/frontend/src/views/falukant/ChurchView.vue b/frontend/src/views/falukant/ChurchView.vue
index a48e15e..23348d6 100644
--- a/frontend/src/views/falukant/ChurchView.vue
+++ b/frontend/src/views/falukant/ChurchView.vue
@@ -3,6 +3,25 @@
{{ $t('falukant.church.summary.guidance') }}
+