diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 2777674..a5bf0d1 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -72,9 +72,10 @@ function calcAge(birthdate) { return differenceInDays(now, b); } -async function getFalukantUserOrFail(hashedId) { +async function getFalukantUserOrFail(hashedId, options = {}) { const user = await FalukantUser.findOne({ - include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }] + include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }], + transaction: options.transaction }); if (!user) throw new Error('User not found'); return user; @@ -1682,7 +1683,7 @@ class FalukantService extends BaseService { // Konsistenz-Garantie: Verkauf, DaySell-Log, Geldbuchung und Inventory-Löschung müssen atomar sein. // Sonst kann es (wie beobachtet) zu "teilweise verkauft/gelöscht" kommen. return await sequelize.transaction(async (t) => { - const falukantUser = await getFalukantUserOrFail(hashedUserId); + const falukantUser = await getFalukantUserOrFail(hashedUserId, { transaction: t }); const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: falukantUser.id }, include: [{ model: FalukantStock, as: 'stocks' }], @@ -3281,70 +3282,124 @@ class FalukantService extends BaseService { } async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio) { - const falukantUser = await getFalukantUserOrFail(hashedUserId); - const since = new Date(Date.now() - 24 * 3600 * 1000); - const already = await Party.findOne({ - where: { - falukantUserId: falukantUser.id, + // Reputation-Logik: Party steigert Reputation um 1..5 (bestes Fest 5, kleinstes 1). + // Wir leiten "Ausstattung" aus den Party-Kosten ab (linear zwischen min/max möglicher Konfiguration), + // und deckeln Reputation bei 100. + return await sequelize.transaction(async (t) => { + const falukantUser = await getFalukantUserOrFail(hashedUserId); + const since = new Date(Date.now() - 24 * 3600 * 1000); + const already = await Party.findOne({ + where: { + falukantUserId: falukantUser.id, + partyTypeId, + createdAt: { [Op.gte]: since }, + }, + attributes: ['id'], + transaction: t + }); + if (already) { + throw new Error('Diese Party wurde bereits innerhalb der letzten 24 Stunden bestellt'); + } + + const [ptype, music, banquette] = await Promise.all([ + PartyType.findByPk(partyTypeId, { transaction: t }), + MusicType.findByPk(musicId, { transaction: t }), + BanquetteType.findByPk(banquetteId, { transaction: t }), + ]); + if (!ptype || !music || !banquette) { + throw new Error('Ungültige Party-, Musik- oder Bankett-Auswahl'); + } + + const nobilities = nobilityIds && nobilityIds.length + ? await TitleOfNobility.findAll({ where: { id: { [Op.in]: nobilityIds } }, transaction: t }) + : []; + + // Prüfe, ob alle angegebenen IDs gefunden wurden + if (nobilityIds && nobilityIds.length > 0 && nobilities.length !== nobilityIds.length) { + throw new Error('Einige ausgewählte Adelstitel existieren nicht'); + } + + let cost = (ptype.cost || 0) + (music.cost || 0) + (banquette.cost || 0); + cost += (50 / servantRatio - 1) * 1000; + const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0); + cost += nobilityCost; + + if (Number(falukantUser.money) < cost) { + throw new Error('Nicht genügend Guthaben für diese Party'); + } + + // min/max mögliche Kosten für die Skalierung (nur für Reputation; Party-Preis bleibt wie berechnet) + const [allPartyTypes, allMusicTypes, allBanquetteTypes, allNobilityTitles] = await Promise.all([ + PartyType.findAll({ attributes: ['cost'], transaction: t }), + MusicType.findAll({ attributes: ['cost'], transaction: t }), + BanquetteType.findAll({ attributes: ['cost'], transaction: t }), + TitleOfNobility.findAll({ attributes: ['id'], transaction: t }), + ]); + const minParty = allPartyTypes.length ? Math.min(...allPartyTypes.map(x => Number(x.cost || 0))) : 0; + const maxParty = allPartyTypes.length ? Math.max(...allPartyTypes.map(x => Number(x.cost || 0))) : 0; + const minMusic = allMusicTypes.length ? Math.min(...allMusicTypes.map(x => Number(x.cost || 0))) : 0; + const maxMusic = allMusicTypes.length ? Math.max(...allMusicTypes.map(x => Number(x.cost || 0))) : 0; + const minBanq = allBanquetteTypes.length ? Math.min(...allBanquetteTypes.map(x => Number(x.cost || 0))) : 0; + const maxBanq = allBanquetteTypes.length ? Math.max(...allBanquetteTypes.map(x => Number(x.cost || 0))) : 0; + const servantsMin = 0; // servantRatio=50 => (50/50 - 1)*1000 = 0 + const servantsMax = (50 / 1 - 1) * 1000; // servantRatio=1 => 49k + const nobilityMax = (allNobilityTitles || []).reduce((sum, n) => sum + ((Number(n.id) ^ 5) * 1000), 0); + + const minCostPossible = (minParty || 0) + (minMusic || 0) + (minBanq || 0) + servantsMin; + const maxCostPossible = (maxParty || 0) + (maxMusic || 0) + (maxBanq || 0) + servantsMax + (nobilityMax || 0); + const denom = Math.max(1, (maxCostPossible - minCostPossible)); + const score = Math.min(1, Math.max(0, (cost - minCostPossible) / denom)); + const reputationGain = 1 + Math.round(score * 4); // 1..5 + + const character = await FalukantCharacter.findOne({ + where: { userId: falukantUser.id }, + attributes: ['id', 'reputation'], + transaction: t + }); + if (!character) throw new Error('No character for user'); + + // Geld abziehen + const moneyResult = await updateFalukantUserMoney( + falukantUser.id, + -cost, + 'partyOrder', + falukantUser.id, + t + ); + if (!moneyResult.success) { + throw new Error('Geld konnte nicht abgezogen werden'); + } + + const party = await Party.create({ partyTypeId, - createdAt: { [Op.gte]: since }, - }, - attributes: ['id'] + falukantUserId: falukantUser.id, + musicTypeId: musicId, + banquetteTypeId: banquetteId, + servantRatio, + cost: cost + }, { transaction: t }); + + if (nobilities.length > 0) { + await party.addInvitedNobilities(nobilities, { transaction: t }); + } + + // Reputation erhöhen (0..100) + await character.update( + { reputation: Sequelize.literal(`LEAST(100, COALESCE(reputation,0) + ${reputationGain})`) }, + { transaction: t } + ); + + const user = await User.findByPk(falukantUser.userId, { transaction: t }); + notifyUser(user.hashedId, 'falukantPartyUpdate', { + partyId: party.id, + cost, + reputationGain, + }); + // Statusbar kann sich damit ebenfalls aktualisieren + notifyUser(user.hashedId, 'falukantUpdateStatus', {}); + + return { success: true, reputationGain }; }); - if (already) { - throw new Error('Diese Party wurde bereits innerhalb der letzten 24 Stunden bestellt'); - } - const [ptype, music, banquette] = await Promise.all([ - PartyType.findByPk(partyTypeId), - MusicType.findByPk(musicId), - BanquetteType.findByPk(banquetteId), - ]); - if (!ptype || !music || !banquette) { - throw new Error('Ungültige Party-, Musik- oder Bankett-Auswahl'); - } - const nobilities = nobilityIds && nobilityIds.length - ? await TitleOfNobility.findAll({ where: { id: { [Op.in]: nobilityIds } } }) - : []; - - // Prüfe, ob alle angegebenen IDs gefunden wurden - if (nobilityIds && nobilityIds.length > 0 && nobilities.length !== nobilityIds.length) { - throw new Error('Einige ausgewählte Adelstitel existieren nicht'); - } - - let cost = (ptype.cost || 0) + (music.cost || 0) + (banquette.cost || 0); - cost += (50 / servantRatio - 1) * 1000; - const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0); - cost += nobilityCost; - if (Number(falukantUser.money) < cost) { - throw new Error('Nicht genügend Guthaben für diese Party'); - } - const moneyResult = await updateFalukantUserMoney( - falukantUser.id, - -cost, - 'partyOrder', - falukantUser.id - ); - if (!moneyResult.success) { - throw new Error('Geld konnte nicht abgezogen werden'); - } - const party = await Party.create({ - partyTypeId, - falukantUserId: falukantUser.id, - musicTypeId: musicId, - banquetteTypeId: banquetteId, - servantRatio, - cost: cost - }); - if (nobilities.length > 0) { - // Verwende die bereits geladenen Objekte - await party.addInvitedNobilities(nobilities); - } - const user = await User.findByPk(falukantUser.userId); - notifyUser(user.hashedId, 'falukantPartyUpdate', { - partyId: party.id, - cost, - }); - return { 'success': true }; } async getParties(hashedUserId) {