Add reputation actions feature to Falukant module
- Introduced new endpoints for retrieving and executing reputation actions in FalukantController and falukantRouter. - Implemented service methods in FalukantService to handle reputation actions, including daily limits and action execution logic. - Updated the frontend ReputationView component to display available actions and their details, including cost and potential reputation gain. - Added translations for reputation actions in both German and English locales. - Enhanced initialization logic to set up reputation action types in the database.
This commit is contained in:
@@ -65,6 +65,8 @@ import Weather from '../models/falukant/data/weather.js';
|
||||
import TownProductWorth from '../models/falukant/data/town_product_worth.js';
|
||||
import ProductWeatherEffect from '../models/falukant/type/product_weather_effect.js';
|
||||
import WeatherType from '../models/falukant/type/weather.js';
|
||||
import ReputationActionType from '../models/falukant/type/reputation_action.js';
|
||||
import ReputationActionLog from '../models/falukant/log/reputation_action.js';
|
||||
|
||||
function calcAge(birthdate) {
|
||||
const b = new Date(birthdate); b.setHours(0, 0);
|
||||
@@ -327,6 +329,7 @@ class PreconditionError extends Error {
|
||||
|
||||
class FalukantService extends BaseService {
|
||||
static KNOWLEDGE_MAX = 99;
|
||||
static REPUTATION_ACTION_DAILY_CAP = Number(process.env.FALUKANT_REPUTATION_ACTION_DAILY_CAP ?? 10);
|
||||
static COST_CONFIG = {
|
||||
one: { min: 50, max: 5000 },
|
||||
all: { min: 400, max: 40000 }
|
||||
@@ -3281,6 +3284,140 @@ class FalukantService extends BaseService {
|
||||
return { partyTypes, musicTypes, banquetteTypes };
|
||||
}
|
||||
|
||||
async getReputationActions(hashedUserId) {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
const actionTypes = await ReputationActionType.findAll({ order: [['cost', 'ASC']] });
|
||||
|
||||
// Tageslimit (global, aus Aktionen) – Anzeige im UI
|
||||
const dailyCap = FalukantService.REPUTATION_ACTION_DAILY_CAP;
|
||||
const [{ dailyUsed }] = await sequelize.query(
|
||||
`
|
||||
SELECT COALESCE(SUM(gain), 0)::int AS "dailyUsed"
|
||||
FROM falukant_log.reputation_action
|
||||
WHERE falukant_user_id = :uid
|
||||
AND action_timestamp >= date_trunc('day', now())
|
||||
`,
|
||||
{ replacements: { uid: falukantUser.id }, type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
const dailyRemaining = Math.max(0, Number(dailyCap) - Number(dailyUsed || 0));
|
||||
|
||||
if (!actionTypes.length) return { dailyCap, dailyUsed: Number(dailyUsed || 0), dailyRemaining, actions: [] };
|
||||
|
||||
// counts in einem Query – aber pro Typ in seinem "Decay-Fenster" (default 7 Tage)
|
||||
const now = Date.now();
|
||||
const actions = [];
|
||||
for (const t of actionTypes) {
|
||||
const windowDays = Number(t.decayWindowDays || 7);
|
||||
const since = new Date(now - windowDays * 24 * 3600 * 1000);
|
||||
const timesUsed = await ReputationActionLog.count({
|
||||
where: {
|
||||
falukantUserId: falukantUser.id,
|
||||
actionTypeId: t.id,
|
||||
actionTimestamp: { [Op.gte]: since },
|
||||
}
|
||||
});
|
||||
const raw = Number(t.baseGain) * Math.pow(Number(t.decayFactor), Number(timesUsed));
|
||||
const gain = Math.max(Number(t.minGain || 0), Math.ceil(raw));
|
||||
actions.push({
|
||||
id: t.id,
|
||||
tr: t.tr,
|
||||
cost: t.cost,
|
||||
baseGain: t.baseGain,
|
||||
decayFactor: t.decayFactor,
|
||||
minGain: t.minGain,
|
||||
decayWindowDays: t.decayWindowDays,
|
||||
timesUsed,
|
||||
currentGain: gain,
|
||||
});
|
||||
}
|
||||
return { dailyCap, dailyUsed: Number(dailyUsed || 0), dailyRemaining, actions };
|
||||
}
|
||||
|
||||
async executeReputationAction(hashedUserId, actionTypeId) {
|
||||
return await sequelize.transaction(async (t) => {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId, { transaction: t });
|
||||
const actionType = await ReputationActionType.findByPk(actionTypeId, { transaction: t });
|
||||
if (!actionType) throw new Error('Unbekannte Aktion');
|
||||
|
||||
const character = await FalukantCharacter.findOne({
|
||||
where: { userId: falukantUser.id },
|
||||
attributes: ['id', 'reputation'],
|
||||
transaction: t
|
||||
});
|
||||
if (!character) throw new Error('No character for user');
|
||||
|
||||
// Abnutzung nur innerhalb des Fensters (default 7 Tage)
|
||||
const windowDays = Number(actionType.decayWindowDays || 7);
|
||||
const since = new Date(Date.now() - windowDays * 24 * 3600 * 1000);
|
||||
const timesUsedBefore = await ReputationActionLog.count({
|
||||
where: {
|
||||
falukantUserId: falukantUser.id,
|
||||
actionTypeId: actionType.id,
|
||||
actionTimestamp: { [Op.gte]: since },
|
||||
},
|
||||
transaction: t
|
||||
});
|
||||
|
||||
const raw = Number(actionType.baseGain) * Math.pow(Number(actionType.decayFactor), Number(timesUsedBefore));
|
||||
const plannedGain = Math.max(Number(actionType.minGain || 0), Math.ceil(raw));
|
||||
|
||||
// Tageslimit aus Aktionen (global)
|
||||
const dailyCap = FalukantService.REPUTATION_ACTION_DAILY_CAP;
|
||||
const [{ dailyUsed }] = await sequelize.query(
|
||||
`
|
||||
SELECT COALESCE(SUM(gain), 0)::int AS "dailyUsed"
|
||||
FROM falukant_log.reputation_action
|
||||
WHERE falukant_user_id = :uid
|
||||
AND action_timestamp >= date_trunc('day', now())
|
||||
`,
|
||||
{ replacements: { uid: falukantUser.id }, type: sequelize.QueryTypes.SELECT, transaction: t }
|
||||
);
|
||||
const dailyRemaining = Math.max(0, Number(dailyCap) - Number(dailyUsed || 0));
|
||||
if (dailyRemaining <= 0) {
|
||||
throw new Error(`Tageslimit erreicht (max. ${dailyCap} Reputation pro Tag durch Aktionen)`);
|
||||
}
|
||||
const gain = Math.min(plannedGain, dailyRemaining);
|
||||
|
||||
if (gain <= 0) {
|
||||
throw new Error('Diese Aktion bringt aktuell keine Reputation mehr');
|
||||
}
|
||||
|
||||
const cost = Number(actionType.cost || 0);
|
||||
if (Number(falukantUser.money) < cost) {
|
||||
throw new Error('Nicht genügend Guthaben');
|
||||
}
|
||||
|
||||
const moneyResult = await updateFalukantUserMoney(
|
||||
falukantUser.id,
|
||||
-cost,
|
||||
`reputationAction.${actionType.tr}`,
|
||||
falukantUser.id,
|
||||
t
|
||||
);
|
||||
if (!moneyResult.success) throw new Error('Geld konnte nicht abgezogen werden');
|
||||
|
||||
await ReputationActionLog.create({
|
||||
falukantUserId: falukantUser.id,
|
||||
actionTypeId: actionType.id,
|
||||
cost,
|
||||
baseGain: actionType.baseGain,
|
||||
gain,
|
||||
timesUsedBefore: Number(timesUsedBefore),
|
||||
}, { transaction: t });
|
||||
|
||||
await character.update(
|
||||
{ reputation: Sequelize.literal(`LEAST(100, COALESCE(reputation,0) + ${gain})`) },
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
const user = await User.findByPk(falukantUser.userId, { transaction: t });
|
||||
notifyUser(user.hashedId, 'falukantUpdateStatus', {});
|
||||
notifyUser(user.hashedId, 'falukantReputationUpdate', { gain, actionTr: actionType.tr });
|
||||
|
||||
return { success: true, gain, plannedGain, dailyCap, dailyRemainingBefore: dailyRemaining, cost, actionTr: actionType.tr };
|
||||
});
|
||||
}
|
||||
|
||||
async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio) {
|
||||
// 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),
|
||||
|
||||
Reference in New Issue
Block a user