Add cooldown feature for reputation actions in FalukantService and update UI components
- Introduced a cooldown mechanism for reputation actions, limiting execution to once per configured interval. - Updated FalukantService to handle cooldown logic and return remaining cooldown time. - Enhanced ReputationView component to display cooldown status and prevent action execution during cooldown. - Added translations for cooldown messages in both German and English locales.
This commit is contained in:
@@ -330,6 +330,8 @@ class PreconditionError extends Error {
|
|||||||
class FalukantService extends BaseService {
|
class FalukantService extends BaseService {
|
||||||
static KNOWLEDGE_MAX = 99;
|
static KNOWLEDGE_MAX = 99;
|
||||||
static REPUTATION_ACTION_DAILY_CAP = Number(process.env.FALUKANT_REPUTATION_ACTION_DAILY_CAP ?? 10);
|
static REPUTATION_ACTION_DAILY_CAP = Number(process.env.FALUKANT_REPUTATION_ACTION_DAILY_CAP ?? 10);
|
||||||
|
static REPUTATION_ACTION_COOLDOWN_MINUTES = Number(process.env.FALUKANT_REPUTATION_ACTION_COOLDOWN_MINUTES ?? 60);
|
||||||
|
static RANDOM_EVENT_DAILY_ENABLED = String(process.env.FALUKANT_RANDOM_EVENT_DAILY_ENABLED ?? '1') === '1';
|
||||||
static COST_CONFIG = {
|
static COST_CONFIG = {
|
||||||
one: { min: 50, max: 5000 },
|
one: { min: 50, max: 5000 },
|
||||||
all: { min: 400, max: 40000 }
|
all: { min: 400, max: 40000 }
|
||||||
@@ -500,7 +502,7 @@ class FalukantService extends BaseService {
|
|||||||
{
|
{
|
||||||
model: FalukantCharacter,
|
model: FalukantCharacter,
|
||||||
as: 'character',
|
as: 'character',
|
||||||
attributes: ['birthdate', 'health', 'reputation'],
|
attributes: ['id', 'regionId', 'birthdate', 'health', 'reputation'],
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Relationship,
|
model: Relationship,
|
||||||
@@ -553,6 +555,16 @@ class FalukantService extends BaseService {
|
|||||||
const userCharacterIds = userCharacterIdsRows.map(r => r.id);
|
const userCharacterIds = userCharacterIdsRows.map(r => r.id);
|
||||||
bm('aggregate.userCharacters', { count: userCharacterIds.length, ids: userCharacterIds.slice(0, 5) });
|
bm('aggregate.userCharacters', { count: userCharacterIds.length, ids: userCharacterIds.slice(0, 5) });
|
||||||
|
|
||||||
|
// Daily random event (once per calendar day per user) -> stored as Notification random_event.*
|
||||||
|
// Frontend already supports JSON-encoded tr: {"tr":"random_event.windfall","amount":123}
|
||||||
|
try {
|
||||||
|
if (FalukantService.RANDOM_EVENT_DAILY_ENABLED) {
|
||||||
|
await this._maybeCreateDailyRandomEvent(falukantUser, user);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Falukant] daily random event failed (non-fatal):', e?.message || e);
|
||||||
|
}
|
||||||
|
|
||||||
// Count distinct children for any of the user's characters (as father or mother)
|
// Count distinct children for any of the user's characters (as father or mother)
|
||||||
let childrenCount = 0;
|
let childrenCount = 0;
|
||||||
let unbaptisedChildrenCount = 0;
|
let unbaptisedChildrenCount = 0;
|
||||||
@@ -605,6 +617,99 @@ class FalukantService extends BaseService {
|
|||||||
return falukantUser;
|
return falukantUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _maybeCreateDailyRandomEvent(falukantUser, user) {
|
||||||
|
if (!falukantUser?.id) return null;
|
||||||
|
|
||||||
|
// Already created today?
|
||||||
|
const since = new Date();
|
||||||
|
since.setHours(0, 0, 0, 0);
|
||||||
|
const already = await Notification.count({
|
||||||
|
where: {
|
||||||
|
userId: falukantUser.id,
|
||||||
|
createdAt: { [Op.gte]: since },
|
||||||
|
[Op.or]: [
|
||||||
|
{ tr: { [Op.like]: 'random_event.%' } },
|
||||||
|
{ tr: { [Op.like]: '%\"tr\":\"random_event.%' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (already > 0) return null;
|
||||||
|
|
||||||
|
// Choose an event (guaranteed once/day, random type)
|
||||||
|
const events = [
|
||||||
|
{ id: 'windfall', weight: 25 },
|
||||||
|
{ id: 'theft', weight: 20 },
|
||||||
|
{ id: 'character_illness', weight: 20 },
|
||||||
|
{ id: 'character_recovery', weight: 15 },
|
||||||
|
{ id: 'character_accident', weight: 10 },
|
||||||
|
{ id: 'regional_festival', weight: 10 },
|
||||||
|
];
|
||||||
|
const total = events.reduce((s, e) => s + e.weight, 0);
|
||||||
|
let r = Math.random() * total;
|
||||||
|
let chosen = events[0].id;
|
||||||
|
for (const e of events) {
|
||||||
|
r -= e.weight;
|
||||||
|
if (r <= 0) { chosen = e.id; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = { tr: `random_event.${chosen}` };
|
||||||
|
|
||||||
|
return await sequelize.transaction(async (t) => {
|
||||||
|
// Reload current values inside tx
|
||||||
|
const freshUser = await FalukantUser.findByPk(falukantUser.id, { transaction: t });
|
||||||
|
const character = await FalukantCharacter.findOne({
|
||||||
|
where: { userId: falukantUser.id },
|
||||||
|
include: [
|
||||||
|
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'], required: false },
|
||||||
|
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'], required: false },
|
||||||
|
],
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Effects (keine harten Datenlöschungen)
|
||||||
|
if (chosen === 'windfall') {
|
||||||
|
const amount = Math.floor(Math.random() * 901) + 100; // 100..1000
|
||||||
|
payload.amount = amount;
|
||||||
|
await updateFalukantUserMoney(falukantUser.id, amount, 'random_event.windfall', falukantUser.id, t);
|
||||||
|
} else if (chosen === 'theft') {
|
||||||
|
const maxLoss = Math.max(0, Math.min(500, Math.floor(Number(freshUser?.money || 0))));
|
||||||
|
const amount = maxLoss > 0 ? (Math.floor(Math.random() * maxLoss) + 1) : 0;
|
||||||
|
payload.amount = amount;
|
||||||
|
if (amount > 0) {
|
||||||
|
await updateFalukantUserMoney(falukantUser.id, -amount, 'random_event.theft', falukantUser.id, t);
|
||||||
|
}
|
||||||
|
} else if (chosen === 'character_illness' || chosen === 'character_recovery' || chosen === 'character_accident') {
|
||||||
|
const name = [character?.definedFirstName?.name, character?.definedLastName?.name].filter(Boolean).join(' ').trim();
|
||||||
|
payload.characterName = name || null;
|
||||||
|
let delta = 0;
|
||||||
|
if (chosen === 'character_illness') delta = -(Math.floor(Math.random() * 11) + 5); // -5..-15
|
||||||
|
if (chosen === 'character_recovery') delta = (Math.floor(Math.random() * 11) + 5); // +5..+15
|
||||||
|
if (chosen === 'character_accident') delta = -(Math.floor(Math.random() * 16) + 10); // -10..-25
|
||||||
|
payload.healthChange = delta > 0 ? `+${delta}` : `${delta}`;
|
||||||
|
if (character) {
|
||||||
|
const next = Math.min(100, Math.max(0, Number(character.health || 0) + delta));
|
||||||
|
await character.update({ health: next }, { transaction: t });
|
||||||
|
}
|
||||||
|
} else if (chosen === 'regional_festival') {
|
||||||
|
const regionId = character?.regionId || falukantUser?.mainBranchRegionId || null;
|
||||||
|
if (regionId) {
|
||||||
|
const region = await RegionData.findByPk(regionId, { attributes: ['name'], transaction: t });
|
||||||
|
payload.regionName = region?.name || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store notification as JSON string so frontend can interpolate params
|
||||||
|
await Notification.create(
|
||||||
|
{ userId: falukantUser.id, tr: JSON.stringify(payload), shown: false },
|
||||||
|
{ transaction: t }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make statusbar update (unread count, etc.)
|
||||||
|
try { notifyUser(user.hashedId, 'falukantUpdateStatus', {}); } catch (_) {}
|
||||||
|
return payload;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getBranches(hashedUserId) {
|
async getBranches(hashedUserId) {
|
||||||
const u = await getFalukantUserOrFail(hashedUserId);
|
const u = await getFalukantUserOrFail(hashedUserId);
|
||||||
const bs = await Branch.findAll({
|
const bs = await Branch.findAll({
|
||||||
@@ -1610,76 +1715,103 @@ class FalukantService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sellProduct(hashedUserId, branchId, productId, quality, quantity) {
|
async sellProduct(hashedUserId, branchId, productId, quality, quantity) {
|
||||||
const user = await getFalukantUserOrFail(hashedUserId);
|
// Konsistenz wie sellAll: nur aus Stocks dieses Branches verkaufen und alles atomar ausführen
|
||||||
const branch = await getBranchOrFail(user.id, branchId);
|
return await sequelize.transaction(async (t) => {
|
||||||
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
const user = await getFalukantUserOrFail(hashedUserId, { transaction: t });
|
||||||
if (!character) throw new Error('No character found for user');
|
const branch = await getBranchOrFail(user.id, branchId);
|
||||||
const stock = await FalukantStock.findOne({ where: { branchId: branch.id } });
|
|
||||||
if (!stock) throw new Error('Stock not found');
|
|
||||||
const inventory = await Inventory.findAll({
|
|
||||||
where: { quality },
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: ProductType,
|
|
||||||
as: 'productType',
|
|
||||||
required: true,
|
|
||||||
where: { id: productId },
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Knowledge,
|
|
||||||
as: 'knowledges',
|
|
||||||
required: false,
|
|
||||||
where: { characterId: character.id }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
if (!inventory.length) {
|
|
||||||
throw new Error('No inventory found');
|
|
||||||
}
|
|
||||||
const available = inventory.reduce((sum, i) => sum + i.quantity, 0);
|
|
||||||
if (available < quantity) throw new Error('Not enough inventory available');
|
|
||||||
const item = inventory[0].productType;
|
|
||||||
const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
|
|
||||||
const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
|
|
||||||
|
|
||||||
// compute cumulative tax (region + ancestors) with political exemptions and inflate price so seller net is unchanged
|
const character = await FalukantCharacter.findOne({ where: { userId: user.id }, transaction: t });
|
||||||
const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, branch.regionId);
|
if (!character) throw new Error('No character found for user');
|
||||||
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
|
|
||||||
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
|
|
||||||
const revenue = quantity * adjustedPricePerUnit;
|
|
||||||
|
|
||||||
// compute tax and net
|
const stocks = await FalukantStock.findAll({
|
||||||
const taxValue = Math.round((revenue * cumulativeTax / 100) * 100) / 100;
|
where: { branchId: branch.id },
|
||||||
const net = Math.round((revenue - taxValue) * 100) / 100;
|
attributes: ['id'],
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
const stockIds = stocks.map(s => s.id);
|
||||||
|
if (!stockIds.length) throw new Error('Stock not found');
|
||||||
|
|
||||||
// Book net to seller
|
const inventory = await Inventory.findAll({
|
||||||
const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id);
|
where: {
|
||||||
if (!moneyResult.success) throw new Error('Failed to update money for seller');
|
stockId: { [Op.in]: stockIds },
|
||||||
|
productId,
|
||||||
|
quality
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: ProductType,
|
||||||
|
as: 'productType',
|
||||||
|
required: true,
|
||||||
|
where: { id: productId },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Knowledge,
|
||||||
|
as: 'knowledges',
|
||||||
|
required: false,
|
||||||
|
where: { characterId: character.id }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [['producedAt', 'ASC'], ['id', 'ASC']],
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
|
||||||
// Book tax to treasury (if configured)
|
if (!inventory.length) {
|
||||||
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
|
throw new Error('No inventory found');
|
||||||
if (treasuryId && taxValue > 0) {
|
|
||||||
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id);
|
|
||||||
if (!taxResult.success) throw new Error('Failed to update money for treasury');
|
|
||||||
}
|
|
||||||
let remaining = quantity;
|
|
||||||
for (const inv of inventory) {
|
|
||||||
if (inv.quantity <= remaining) {
|
|
||||||
remaining -= inv.quantity;
|
|
||||||
await inv.destroy();
|
|
||||||
} else {
|
|
||||||
await inv.update({ quantity: inv.quantity - remaining });
|
|
||||||
remaining = 0;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
await this.addSellItem(branchId, user.id, productId, quantity);
|
const available = inventory.reduce((sum, i) => sum + i.quantity, 0);
|
||||||
console.log('[FalukantService.sellProduct] emitting events for user', user.user.hashedId, 'branch', branch?.id);
|
if (available < quantity) throw new Error('Not enough inventory available');
|
||||||
notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
|
|
||||||
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
|
const item = inventory[0].productType;
|
||||||
return { success: true };
|
const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
|
||||||
|
const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
|
||||||
|
|
||||||
|
// compute cumulative tax (region + ancestors) with political exemptions and inflate price so seller net is unchanged
|
||||||
|
const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, branch.regionId);
|
||||||
|
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
|
||||||
|
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
|
||||||
|
const revenue = quantity * adjustedPricePerUnit;
|
||||||
|
|
||||||
|
// compute tax and net
|
||||||
|
const taxValue = Math.round((revenue * cumulativeTax / 100) * 100) / 100;
|
||||||
|
const net = Math.round((revenue - taxValue) * 100) / 100;
|
||||||
|
|
||||||
|
// Book net to seller (in tx)
|
||||||
|
const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id, t);
|
||||||
|
if (!moneyResult.success) throw new Error('Failed to update money for seller');
|
||||||
|
|
||||||
|
// Book tax to treasury (if configured)
|
||||||
|
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
|
||||||
|
if (treasuryId && taxValue > 0) {
|
||||||
|
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id, t);
|
||||||
|
if (!taxResult.success) throw new Error('Failed to update money for treasury');
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = quantity;
|
||||||
|
for (const inv of inventory) {
|
||||||
|
if (remaining <= 0) break;
|
||||||
|
if (inv.quantity <= remaining) {
|
||||||
|
remaining -= inv.quantity;
|
||||||
|
await inv.destroy({ transaction: t });
|
||||||
|
} else {
|
||||||
|
await inv.update({ quantity: inv.quantity - remaining }, { transaction: t });
|
||||||
|
remaining = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (remaining !== 0) {
|
||||||
|
throw new Error(`Inventory deduction mismatch (remaining=${remaining})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.addSellItem(branchId, user.id, productId, quantity, t);
|
||||||
|
|
||||||
|
// notify after successful commit (we can still emit here; worst-case it's slightly early)
|
||||||
|
notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
|
||||||
|
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sellAllProducts(hashedUserId, branchId) {
|
async sellAllProducts(hashedUserId, branchId) {
|
||||||
@@ -1827,28 +1959,26 @@ class FalukantService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSellItem(branchId, userId, productId, quantity) {
|
async addSellItem(branchId, userId, productId, quantity, transaction = null) {
|
||||||
const branch = await Branch.findOne({
|
const branch = await Branch.findOne({
|
||||||
where: { id: branchId },
|
where: { id: branchId },
|
||||||
})
|
attributes: ['id', 'regionId'],
|
||||||
;
|
transaction: transaction || undefined
|
||||||
const daySell = await DaySell.findOne({
|
});
|
||||||
|
if (!branch) throw new Error(`Branch not found (branchId: ${branchId})`);
|
||||||
|
|
||||||
|
const [daySell, created] = await DaySell.findOrCreate({
|
||||||
where: {
|
where: {
|
||||||
regionId: branch.regionId,
|
regionId: branch.regionId,
|
||||||
productId: productId,
|
productId: productId,
|
||||||
sellerId: userId,
|
sellerId: userId,
|
||||||
}
|
},
|
||||||
|
defaults: { quantity: quantity },
|
||||||
|
transaction: transaction || undefined
|
||||||
});
|
});
|
||||||
if (daySell) {
|
if (!created) {
|
||||||
daySell.quantity += quantity;
|
daySell.quantity += quantity;
|
||||||
await daySell.save();
|
await daySell.save({ transaction: transaction || undefined });
|
||||||
} else {
|
|
||||||
await DaySell.create({
|
|
||||||
regionId: branch.regionId,
|
|
||||||
productId: productId,
|
|
||||||
sellerId: userId,
|
|
||||||
quantity: quantity,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3301,7 +3431,33 @@ class FalukantService extends BaseService {
|
|||||||
);
|
);
|
||||||
const dailyRemaining = Math.max(0, Number(dailyCap) - Number(dailyUsed || 0));
|
const dailyRemaining = Math.max(0, Number(dailyCap) - Number(dailyUsed || 0));
|
||||||
|
|
||||||
if (!actionTypes.length) return { dailyCap, dailyUsed: Number(dailyUsed || 0), dailyRemaining, actions: [] };
|
// Globaler Cooldown: max. 1 Aktion pro Stunde (oder konfigurierbar) unabhängig vom Typ
|
||||||
|
const cooldownMinutes = FalukantService.REPUTATION_ACTION_COOLDOWN_MINUTES;
|
||||||
|
const [{ lastTs }] = await sequelize.query(
|
||||||
|
`
|
||||||
|
SELECT MAX(action_timestamp) AS "lastTs"
|
||||||
|
FROM falukant_log.reputation_action
|
||||||
|
WHERE falukant_user_id = :uid
|
||||||
|
`,
|
||||||
|
{ replacements: { uid: falukantUser.id }, type: sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
let cooldownRemainingSec = 0;
|
||||||
|
if (lastTs) {
|
||||||
|
const last = new Date(lastTs).getTime();
|
||||||
|
const nextAllowed = last + cooldownMinutes * 60 * 1000;
|
||||||
|
cooldownRemainingSec = Math.max(0, Math.ceil((nextAllowed - Date.now()) / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actionTypes.length) {
|
||||||
|
return {
|
||||||
|
dailyCap,
|
||||||
|
dailyUsed: Number(dailyUsed || 0),
|
||||||
|
dailyRemaining,
|
||||||
|
cooldownMinutes,
|
||||||
|
cooldownRemainingSec,
|
||||||
|
actions: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// counts in einem Query – aber pro Typ in seinem "Decay-Fenster" (default 7 Tage)
|
// counts in einem Query – aber pro Typ in seinem "Decay-Fenster" (default 7 Tage)
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -3330,7 +3486,14 @@ class FalukantService extends BaseService {
|
|||||||
currentGain: gain,
|
currentGain: gain,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { dailyCap, dailyUsed: Number(dailyUsed || 0), dailyRemaining, actions };
|
return {
|
||||||
|
dailyCap,
|
||||||
|
dailyUsed: Number(dailyUsed || 0),
|
||||||
|
dailyRemaining,
|
||||||
|
cooldownMinutes,
|
||||||
|
cooldownRemainingSec,
|
||||||
|
actions
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeReputationAction(hashedUserId, actionTypeId) {
|
async executeReputationAction(hashedUserId, actionTypeId) {
|
||||||
@@ -3339,6 +3502,26 @@ class FalukantService extends BaseService {
|
|||||||
const actionType = await ReputationActionType.findByPk(actionTypeId, { transaction: t });
|
const actionType = await ReputationActionType.findByPk(actionTypeId, { transaction: t });
|
||||||
if (!actionType) throw new Error('Unbekannte Aktion');
|
if (!actionType) throw new Error('Unbekannte Aktion');
|
||||||
|
|
||||||
|
// Globaler Cooldown (unabhängig vom Aktionstyp): max. 1 pro Stunde
|
||||||
|
const cooldownMinutes = FalukantService.REPUTATION_ACTION_COOLDOWN_MINUTES;
|
||||||
|
const [{ lastTs }] = await sequelize.query(
|
||||||
|
`
|
||||||
|
SELECT MAX(action_timestamp) AS "lastTs"
|
||||||
|
FROM falukant_log.reputation_action
|
||||||
|
WHERE falukant_user_id = :uid
|
||||||
|
`,
|
||||||
|
{ replacements: { uid: falukantUser.id }, type: sequelize.QueryTypes.SELECT, transaction: t }
|
||||||
|
);
|
||||||
|
if (lastTs) {
|
||||||
|
const last = new Date(lastTs).getTime();
|
||||||
|
const nextAllowed = last + cooldownMinutes * 60 * 1000;
|
||||||
|
const remainingSec = Math.max(0, Math.ceil((nextAllowed - Date.now()) / 1000));
|
||||||
|
if (remainingSec > 0) {
|
||||||
|
const remainingMin = Math.ceil(remainingSec / 60);
|
||||||
|
throw new Error(`Sozialstatus-Aktionen sind nur ${cooldownMinutes} Minutenweise möglich. Bitte warte noch ca. ${remainingMin} Minuten.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const character = await FalukantCharacter.findOne({
|
const character = await FalukantCharacter.findOne({
|
||||||
where: { userId: falukantUser.id },
|
where: { userId: falukantUser.id },
|
||||||
attributes: ['id', 'reputation'],
|
attributes: ['id', 'reputation'],
|
||||||
|
|||||||
@@ -794,6 +794,7 @@
|
|||||||
"running": "Läuft...",
|
"running": "Läuft...",
|
||||||
"none": "Keine Aktionen verfügbar.",
|
"none": "Keine Aktionen verfügbar.",
|
||||||
"dailyLimit": "Heute noch verfügbar: {remaining} / {cap} Reputation (durch Aktionen).",
|
"dailyLimit": "Heute noch verfügbar: {remaining} / {cap} Reputation (durch Aktionen).",
|
||||||
|
"cooldown": "Nächste Sozialstatus-Aktion in ca. {minutes} Minuten möglich.",
|
||||||
"success": "Aktion erfolgreich! Reputation +{gain}, Kosten {cost}.",
|
"success": "Aktion erfolgreich! Reputation +{gain}, Kosten {cost}.",
|
||||||
"successSimple": "Aktion erfolgreich!",
|
"successSimple": "Aktion erfolgreich!",
|
||||||
"type": {
|
"type": {
|
||||||
|
|||||||
@@ -218,6 +218,7 @@
|
|||||||
"running": "Running...",
|
"running": "Running...",
|
||||||
"none": "No actions available.",
|
"none": "No actions available.",
|
||||||
"dailyLimit": "Available today: {remaining} / {cap} reputation (from actions).",
|
"dailyLimit": "Available today: {remaining} / {cap} reputation (from actions).",
|
||||||
|
"cooldown": "Next social status action available in about {minutes} minutes.",
|
||||||
"success": "Action successful! Reputation +{gain}, cost {cost}.",
|
"success": "Action successful! Reputation +{gain}, cost {cost}.",
|
||||||
"successSimple": "Action successful!",
|
"successSimple": "Action successful!",
|
||||||
"type": {
|
"type": {
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<p v-if="reputationActionsDailyCap != null" class="reputation-actions-daily">
|
<p v-if="reputationActionsDailyCap != null" class="reputation-actions-daily">
|
||||||
{{ $t('falukant.reputation.actions.dailyLimit', { remaining: reputationActionsDailyRemaining, cap: reputationActionsDailyCap }) }}
|
{{ $t('falukant.reputation.actions.dailyLimit', { remaining: reputationActionsDailyRemaining, cap: reputationActionsDailyCap }) }}
|
||||||
</p>
|
</p>
|
||||||
|
<p v-if="reputationActionsCooldownRemainingSec > 0" class="reputation-actions-cooldown">
|
||||||
|
{{ $t('falukant.reputation.actions.cooldown', { minutes: Math.ceil(reputationActionsCooldownRemainingSec / 60) }) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<table v-if="reputationActions.length">
|
<table v-if="reputationActions.length">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -43,7 +46,7 @@
|
|||||||
<td>+{{ Number(a.currentGain || 0) }}</td>
|
<td>+{{ Number(a.currentGain || 0) }}</td>
|
||||||
<td>{{ Number(a.timesUsed || 0) }}</td>
|
<td>{{ Number(a.timesUsed || 0) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" :disabled="runningActionId === a.id"
|
<button type="button" :disabled="runningActionId === a.id || reputationActionsCooldownRemainingSec > 0"
|
||||||
@click.prevent="executeReputationAction(a)">
|
@click.prevent="executeReputationAction(a)">
|
||||||
{{ runningActionId === a.id ? $t('falukant.reputation.actions.running') : $t('falukant.reputation.actions.execute') }}
|
{{ runningActionId === a.id ? $t('falukant.reputation.actions.running') : $t('falukant.reputation.actions.execute') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -218,6 +221,8 @@ export default {
|
|||||||
reputationActionsDailyCap: null,
|
reputationActionsDailyCap: null,
|
||||||
reputationActionsDailyUsed: null,
|
reputationActionsDailyUsed: null,
|
||||||
reputationActionsDailyRemaining: null,
|
reputationActionsDailyRemaining: null,
|
||||||
|
reputationActionsCooldownMinutes: null,
|
||||||
|
reputationActionsCooldownRemainingSec: 0,
|
||||||
runningActionId: null,
|
runningActionId: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -262,6 +267,8 @@ export default {
|
|||||||
this.reputationActionsDailyCap = data?.dailyCap ?? null;
|
this.reputationActionsDailyCap = data?.dailyCap ?? null;
|
||||||
this.reputationActionsDailyUsed = data?.dailyUsed ?? null;
|
this.reputationActionsDailyUsed = data?.dailyUsed ?? null;
|
||||||
this.reputationActionsDailyRemaining = data?.dailyRemaining ?? null;
|
this.reputationActionsDailyRemaining = data?.dailyRemaining ?? null;
|
||||||
|
this.reputationActionsCooldownMinutes = data?.cooldownMinutes ?? null;
|
||||||
|
this.reputationActionsCooldownRemainingSec = Number(data?.cooldownRemainingSec ?? 0) || 0;
|
||||||
this.reputationActions = Array.isArray(data?.actions) ? data.actions : [];
|
this.reputationActions = Array.isArray(data?.actions) ? data.actions : [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load reputation actions', e);
|
console.error('Failed to load reputation actions', e);
|
||||||
@@ -269,11 +276,14 @@ export default {
|
|||||||
this.reputationActionsDailyCap = null;
|
this.reputationActionsDailyCap = null;
|
||||||
this.reputationActionsDailyUsed = null;
|
this.reputationActionsDailyUsed = null;
|
||||||
this.reputationActionsDailyRemaining = null;
|
this.reputationActionsDailyRemaining = null;
|
||||||
|
this.reputationActionsCooldownMinutes = null;
|
||||||
|
this.reputationActionsCooldownRemainingSec = 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async executeReputationAction(action) {
|
async executeReputationAction(action) {
|
||||||
if (!action?.id) return;
|
if (!action?.id) return;
|
||||||
if (this.runningActionId) return;
|
if (this.runningActionId) return;
|
||||||
|
if (this.reputationActionsCooldownRemainingSec > 0) return;
|
||||||
this.runningActionId = action.id;
|
this.runningActionId = action.id;
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.post('/api/falukant/reputation/actions', { actionTypeId: action.id });
|
const { data } = await apiClient.post('/api/falukant/reputation/actions', { actionTypeId: action.id });
|
||||||
@@ -409,4 +419,9 @@ table th {
|
|||||||
margin: 0.5rem 0 1rem;
|
margin: 0.5rem 0 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reputation-actions-cooldown {
|
||||||
|
margin: -0.5rem 0 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user