Add heir selection functionality in Falukant module

- Implemented getPotentialHeirs and selectHeir methods in FalukantService to allow users to retrieve and select potential heirs based on specific criteria.
- Updated FalukantController to wrap new methods with user authentication and added corresponding routes in FalukantRouter.
- Enhanced OverviewView component to display heir selection UI when no character is present, including loading states and error handling.
- Added translations for heir selection messages in both German and English locales to improve user experience.
This commit is contained in:
Torsten Schulz (local)
2026-01-07 10:29:16 +01:00
parent c17af04cbf
commit c90b7785c0
6 changed files with 280 additions and 11 deletions

View File

@@ -93,6 +93,8 @@ class FalukantController {
return result; return result;
}); });
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId)); this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId)); this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
this.getGifts = this._wrapWithUser((userId) => { this.getGifts = this._wrapWithUser((userId) => {
console.log('🔍 getGifts called with userId:', userId); console.log('🔍 getGifts called with userId:', userId);

View File

@@ -39,6 +39,8 @@ router.get('/directors', falukantController.getAllDirectors);
router.post('/directors', falukantController.updateDirector); router.post('/directors', falukantController.updateDirector);
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal); router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
router.post('/family/set-heir', falukantController.setHeir); router.post('/family/set-heir', falukantController.setHeir);
router.get('/heirs/potential', falukantController.getPotentialHeirs);
router.post('/heirs/select', falukantController.selectHeir);
router.get('/family/gifts', falukantController.getGifts); router.get('/family/gifts', falukantController.getGifts);
router.get('/family/children', falukantController.getChildren); router.get('/family/children', falukantController.getChildren);
router.post('/family/gift', falukantController.sendGift); router.post('/family/gift', falukantController.sendGift);

View File

@@ -2925,6 +2925,107 @@ class FalukantService extends BaseService {
return { success: true, childCharacterId }; return { success: true, childCharacterId };
} }
async getPotentialHeirs(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
// Prüfe, ob der User bereits einen Charakter hat
const existingCharacter = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (existingCharacter) {
throw new Error('User already has a character');
}
if (!user.mainBranchRegionId) {
throw new Error('User has no main branch region');
}
// Hole den noncivil Titel
const noncivilTitle = await TitleOfNobility.findOne({ where: { labelTr: 'noncivil' } });
if (!noncivilTitle) {
throw new Error('Noncivil title not found');
}
// Berechne das Datum für 10-14 Jahre alt (in Tagen)
const now = new Date();
now.setHours(0, 0, 0, 0);
const minDate = new Date(now);
minDate.setDate(minDate.getDate() - 14); // 14 Tage = 14 Jahre
const maxDate = new Date(now);
maxDate.setDate(maxDate.getDate() - 10); // 10 Tage = 10 Jahre
// Hole zufällige Charaktere aus der Hauptregion, die 10-14 Jahre alt sind
// und keinen userId haben (also noch keinem User zugeordnet sind)
// und den noncivil Titel haben
const potentialHeirs = await FalukantCharacter.findAll({
where: {
regionId: user.mainBranchRegionId,
userId: null,
titleOfNobility: noncivilTitle.id,
birthdate: {
[Op.between]: [minDate, maxDate]
}
},
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] }
],
order: Sequelize.fn('RANDOM'),
limit: 5
});
// Berechne das Alter für jeden Charakter
return potentialHeirs.map(heir => {
const age = calcAge(heir.birthdate);
return {
id: heir.id,
definedFirstName: heir.definedFirstName,
definedLastName: heir.definedLastName,
gender: heir.gender,
age: age
};
});
}
async selectHeir(hashedUserId, heirId) {
const user = await getFalukantUserOrFail(hashedUserId);
// Prüfe, ob der User bereits einen Charakter hat
const existingCharacter = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (existingCharacter) {
throw new Error('User already has a character');
}
// Hole den Erben-Charakter
const heir = await FalukantCharacter.findOne({
where: {
id: heirId,
userId: null // Stelle sicher, dass er noch keinem User zugeordnet ist
}
});
if (!heir) {
throw new Error('Heir not found or already assigned');
}
// Prüfe, ob der Erbe in der Hauptregion ist
if (heir.regionId !== user.mainBranchRegionId) {
throw new Error('Heir is not in main branch region');
}
// Prüfe das Alter (10-14 Jahre)
const age = calcAge(heir.birthdate);
if (age < 10 || age > 14) {
throw new Error('Heir age is not between 10 and 14');
}
// Weise den Charakter dem User zu
await heir.update({ userId: user.id });
// Benachrichtige den User
notifyUser(hashedUserId, 'falukantUserUpdated', {});
return { success: true, characterId: heir.id };
}
async getPossiblePartners(requestingCharacterId) { async getPossiblePartners(requestingCharacterId) {
const proposals = await MarriageProposal.findAll({ const proposals = await MarriageProposal.findAll({
where: { where: {

View File

@@ -135,6 +135,14 @@
"store": "Verkauf", "store": "Verkauf",
"fullstack": "Produktion mit Verkauf" "fullstack": "Produktion mit Verkauf"
} }
},
"heirSelection": {
"title": "Charakter verloren - Erben auswählen",
"description": "Dein Charakter wurde durch einen Fehler verloren. Bitte wähle einen Erben aus deiner Hauptregion aus, um fortzufahren.",
"loading": "Lade mögliche Erben...",
"noHeirs": "Es wurden keine passenden Erben gefunden.",
"select": "Als Erben wählen",
"error": "Fehler beim Auswählen des Erben."
} }
}, },
"titles": { "titles": {

View File

@@ -206,6 +206,39 @@
} }
} }
}, },
"overview": {
"title": "Falukant - Overview",
"metadata": {
"title": "Personal",
"name": "Name",
"money": "Wealth",
"age": "Age",
"mainbranch": "Home City",
"nobleTitle": "Status"
},
"productions": {
"title": "Productions"
},
"stock": {
"title": "Stock"
},
"branches": {
"title": "Branches",
"level": {
"production": "Production",
"store": "Store",
"fullstack": "Production with Store"
}
},
"heirSelection": {
"title": "Character Lost - Select Heir",
"description": "Your character was lost due to an error. Please select an heir from your main region to continue.",
"loading": "Loading potential heirs...",
"noHeirs": "No suitable heirs were found.",
"select": "Select as Heir",
"error": "Error selecting heir."
}
},
"nobility": { "nobility": {
"cooldown": "You can only advance again on {date}." "cooldown": "You can only advance again on {date}."
}, },

View File

@@ -2,19 +2,45 @@
<div> <div>
<StatusBar /> <StatusBar />
<h2>{{ $t('falukant.overview.title') }}</h2> <h2>{{ $t('falukant.overview.title') }}</h2>
<div class="overviewcontainer">
<!-- Erben-Auswahl wenn kein Charakter vorhanden -->
<div v-if="!falukantUser?.character" class="heir-selection-container">
<h3>{{ $t('falukant.overview.heirSelection.title') }}</h3>
<p>{{ $t('falukant.overview.heirSelection.description') }}</p>
<div v-if="loadingHeirs" class="loading">{{ $t('falukant.overview.heirSelection.loading') }}</div>
<div v-else-if="potentialHeirs.length === 0" class="no-heirs">
{{ $t('falukant.overview.heirSelection.noHeirs') }}
</div>
<div v-else class="heirs-list">
<div v-for="heir in potentialHeirs" :key="heir.id" class="heir-card">
<div class="heir-info">
<div class="heir-name">
{{ $t(`falukant.titles.${heir.gender}.noncivil`) }}
{{ heir.definedFirstName.name }} {{ heir.definedLastName.name }}
</div>
<div class="heir-age">{{ $t('falukant.overview.metadata.age') }}: {{ heir.age }}</div>
</div>
<button @click="selectHeir(heir.id)" class="select-heir-button">
{{ $t('falukant.overview.heirSelection.select') }}
</button>
</div>
</div>
</div>
<!-- Normale Übersicht wenn Charakter vorhanden -->
<div v-else class="overviewcontainer">
<div> <div>
<h3>{{ $t('falukant.overview.metadata.title') }}</h3> <h3>{{ $t('falukant.overview.metadata.title') }}</h3>
<table> <table>
<tr> <tr>
<td>{{ $t('falukant.overview.metadata.name') }}</td> <td>{{ $t('falukant.overview.metadata.name') }}</td>
<td>{{ falukantUser?.character.definedFirstName.name }} {{ <td>{{ falukantUser?.character?.definedFirstName?.name }} {{
falukantUser?.character.definedLastName.name }}</td> falukantUser?.character?.definedLastName?.name }}</td>
</tr> </tr>
<tr> <tr>
<td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td> <td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td>
<td>{{ $t('falukant.titles.' + falukantUser?.character.gender + '.' + <td>{{ $t('falukant.titles.' + falukantUser?.character?.gender + '.' +
falukantUser?.character.nobleTitle.labelTr) }}</td> falukantUser?.character?.nobleTitle?.labelTr) }}</td>
</tr> </tr>
<tr> <tr>
<td>{{ $t('falukant.overview.metadata.money') }}</td> <td>{{ $t('falukant.overview.metadata.money') }}</td>
@@ -26,11 +52,11 @@
</tr> </tr>
<tr> <tr>
<td>{{ $t('falukant.overview.metadata.age') }}</td> <td>{{ $t('falukant.overview.metadata.age') }}</td>
<td>{{ falukantUser?.character.age }}</td> <td>{{ falukantUser?.character?.age }}</td>
</tr> </tr>
<tr> <tr>
<td>{{ $t('falukant.overview.metadata.mainbranch') }}</td> <td>{{ $t('falukant.overview.metadata.mainbranch') }}</td>
<td>{{ falukantUser?.mainBranchRegion.name }}</td> <td>{{ falukantUser?.mainBranchRegion?.name }}</td>
</tr> </tr>
</table> </table>
</div> </div>
@@ -90,7 +116,7 @@
</table> </table>
</div> </div>
</div> </div>
<div class="imagecontainer"> <div v-if="falukantUser?.character" class="imagecontainer">
<div :style="getAvatarStyle" class="avatar"></div> <div :style="getAvatarStyle" class="avatar"></div>
<div :style="getHouseStyle" class="house"></div> <div :style="getHouseStyle" class="house"></div>
</div> </div>
@@ -149,12 +175,14 @@ export default {
falukantUser: null, falukantUser: null,
allStock: [], allStock: [],
productions: [], productions: [],
potentialHeirs: [],
loadingHeirs: false,
}; };
}, },
computed: { computed: {
...mapState(['socket']), ...mapState(['socket']),
getAvatarStyle() { getAvatarStyle() {
if (!this.falukantUser) return {}; if (!this.falukantUser || !this.falukantUser.character) return {};
const { gender, age } = this.falukantUser.character; const { gender, age } = this.falukantUser.character;
const imageUrl = `/images/falukant/avatar/${gender}.png`; const imageUrl = `/images/falukant/avatar/${gender}.png`;
const ageGroup = this.getAgeGroup(age); const ageGroup = this.getAgeGroup(age);
@@ -212,8 +240,12 @@ export default {
}, },
async mounted() { async mounted() {
await this.fetchFalukantUser(); await this.fetchFalukantUser();
await this.fetchAllStock(); if (!this.falukantUser?.character) {
await this.fetchProductions(); await this.fetchPotentialHeirs();
} else {
await this.fetchAllStock();
await this.fetchProductions();
}
// Daemon WebSocket deaktiviert - verwende Socket.io für alle Events // Daemon WebSocket deaktiviert - verwende Socket.io für alle Events
this.setupSocketEvents(); this.setupSocketEvents();
}, },
@@ -306,6 +338,33 @@ export default {
formatDate(timestamp) { formatDate(timestamp) {
return new Date(timestamp).toLocaleString(); return new Date(timestamp).toLocaleString();
}, },
async fetchPotentialHeirs() {
if (!this.falukantUser?.mainBranchRegion?.id) return;
this.loadingHeirs = true;
try {
const response = await apiClient.get('/api/falukant/heirs/potential');
this.potentialHeirs = response.data || [];
} catch (error) {
console.error('Error fetching potential heirs:', error);
this.potentialHeirs = [];
} finally {
this.loadingHeirs = false;
}
},
async selectHeir(heirId) {
try {
await apiClient.post('/api/falukant/heirs/select', { heirId });
// Lade User-Daten neu
await this.fetchFalukantUser();
if (this.falukantUser?.character) {
await this.fetchAllStock();
await this.fetchProductions();
}
} catch (error) {
console.error('Error selecting heir:', error);
alert(this.$t('falukant.overview.heirSelection.error'));
}
},
}, },
}; };
</script> </script>
@@ -348,4 +407,68 @@ export default {
h2 { h2 {
padding-top: 20px; padding-top: 20px;
} }
.heir-selection-container {
border: 2px solid #dc3545;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
background-color: #fff3cd;
}
.heir-selection-container h3 {
margin-top: 0;
color: #856404;
}
.heirs-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
margin-top: 20px;
}
.heir-card {
border: 1px solid #ccc;
border-radius: 4px;
padding: 15px;
background-color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.heir-info {
flex: 1;
}
.heir-name {
font-weight: bold;
margin-bottom: 5px;
}
.heir-age {
color: #666;
font-size: 0.9em;
}
.select-heir-button {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.select-heir-button:hover {
background-color: #218838;
}
.loading, .no-heirs {
text-align: center;
padding: 20px;
color: #666;
}
</style> </style>