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.title') }}

+
+
+
{{ $t('falukant.church.summary.highestCurrentOffice') }}
+
{{ highestCurrentOfficeLabel }}
+
+
+
{{ $t('falukant.church.summary.availableApplications') }}
+
{{ availablePositions.length }}
+
+
+
{{ $t('falukant.church.summary.supervisedApplications') }}
+
{{ supervisedApplications.length }}
+
+
+ +
+

{{ $t('falukant.church.summary.guidance') }}

+
+
@@ -87,6 +106,7 @@ {{ $t('falukant.church.available.office') }} {{ $t('falukant.church.available.region') }} {{ $t('falukant.church.available.supervisor') }} + {{ $t('falukant.church.available.decision') }} {{ $t('falukant.church.available.seats') }} {{ $t('falukant.church.available.action') }} @@ -101,6 +121,7 @@ + {{ decisionModeLabel(pos.decisionMode) }} {{ pos.availableSeats }}