feat(falukant): enhance product pricing and nobility advancement features
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s

- Updated the `getAllProductPricesInRegion` method in `FalukantService` to accept additional parameters for network worth and branch ID, improving pricing calculations based on user branches.
- Enhanced the nobility advancement logic in `NobilityView` to display unmet requirements clearly, providing users with better feedback on advancement conditions.
- Refactored the revenue calculation in `ProductionSection` to utilize a cached product prices object, optimizing performance and reducing redundant API calls.
- Updated localization files to include new translations for attack sources across multiple languages, enhancing the user experience for diverse audiences.
- Removed obsolete C++ worker references and streamlined the retention logic for production logs, ensuring efficient data management.
This commit is contained in:
Torsten Schulz (local)
2026-04-09 09:08:32 +02:00
parent 360bb59a4e
commit 1118a691b9
20 changed files with 216 additions and 85 deletions

View File

@@ -5821,51 +5821,61 @@ class FalukantService extends BaseService {
const oneWeekAgo = new Date(now.getTime());
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
if (user.lastNobilityAdvanceAt > oneWeekAgo) {
throw new Error('too soon');
const last = new Date(user.lastNobilityAdvanceAt);
const retryAt = new Date(last.getTime());
retryAt.setDate(retryAt.getDate() + 7);
throw { status: 412, message: 'nobilityTooSoon', retryAt: retryAt.toISOString() };
}
}
const nextTitle = nobility.next.toJSON();
let fulfilled = true;
let cost = 0;
const unmet = [];
for (const requirement of nextTitle.requirements) {
let ok = true;
switch (requirement.requirementType) {
case 'money':
fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement);
ok = await this.checkMoneyRequirement(user, requirement);
break;
case 'cost':
fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement);
ok = await this.checkMoneyRequirement(user, requirement);
cost = requirement.requirementValue;
break;
case 'branches':
fulfilled = fulfilled && await this.checkBranchesRequirement(hashedUserId, requirement);
ok = await this.checkBranchesRequirement(hashedUserId, requirement);
break;
case 'reputation':
fulfilled = fulfilled && await this.checkReputationRequirement(user, requirement);
ok = await this.checkReputationRequirement(user, requirement);
break;
case 'house_position':
fulfilled = fulfilled && await this.checkHousePositionRequirement(user, requirement);
ok = await this.checkHousePositionRequirement(user, requirement);
break;
case 'house_condition':
fulfilled = fulfilled && await this.checkHouseConditionRequirement(user, requirement);
ok = await this.checkHouseConditionRequirement(user, requirement);
break;
case 'office_rank_any':
fulfilled = fulfilled && await this.checkOfficeRankAnyRequirement(user, requirement);
ok = await this.checkOfficeRankAnyRequirement(user, requirement);
break;
case 'office_rank_political':
fulfilled = fulfilled && await this.checkOfficeRankPoliticalRequirement(user, requirement);
ok = await this.checkOfficeRankPoliticalRequirement(user, requirement);
break;
case 'lover_count_max':
fulfilled = fulfilled && await this.checkLoverCountMaxRequirement(user, requirement);
ok = await this.checkLoverCountMaxRequirement(user, requirement);
break;
case 'lover_count_min':
fulfilled = fulfilled && await this.checkLoverCountMinRequirement(user, requirement);
ok = await this.checkLoverCountMinRequirement(user, requirement);
break;
default:
fulfilled = false;
};
ok = false;
}
if (!ok) {
unmet.push({
type: requirement.requirementType,
required: requirement.requirementValue,
});
}
}
if (!fulfilled) {
throw new Error('Requirements not fulfilled');
if (unmet.length > 0) {
throw { status: 412, message: 'nobilityRequirements', unmet };
}
const newTitle = await TitleOfNobility.findOne({
where: { level: nobility.current.level + 1 }
@@ -6793,13 +6803,42 @@ class FalukantService extends BaseService {
}
}
async getAllProductPricesInRegion(hashedUserId, regionId) {
/**
* Regionale Verkaufspreise (Wissen + town_product_worth), für Ertrags-/Gewinn-Tabelle.
* @param {{ networkWorth?: boolean, branchId?: number|null }} options — bei networkWorth: MAX(worth_percent)
* über alle Regionen der eigenen Niederlassungen (wie Daemon mit Fahrzeug / Filialnetz).
*/
async getAllProductPricesInRegion(hashedUserId, regionId, options = {}) {
try {
const networkWorth = Boolean(options.networkWorth);
const branchId = options.branchId != null ? Number(options.branchId) : null;
const user = await this.getFalukantUserByHashedId(hashedUserId);
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!character) {
throw new Error(`No FalukantCharacter found for user with id ${user.id}`);
}
let worthRegionIds = [regionId];
if (networkWorth) {
if (!branchId || Number.isNaN(branchId)) {
throw new Error('branchId is required when networkWorth is set');
}
const ownBranch = await Branch.findOne({
where: { id: branchId, falukantUserId: user.id },
attributes: ['id']
});
if (!ownBranch) {
throw new Error('Branch not found or not owned by user');
}
const userBranches = await Branch.findAll({
where: { falukantUserId: user.id },
attributes: ['regionId']
});
const ids = [...new Set(userBranches.map((b) => b.regionId).filter((id) => id != null))];
worthRegionIds = ids.length > 0 ? ids : [regionId];
}
const [products, knowledges, townWorths] = await Promise.all([
ProductType.findAll({ attributes: ['id', 'sellCost'] }),
Knowledge.findAll({
@@ -6807,17 +6846,22 @@ class FalukantService extends BaseService {
attributes: ['productId', 'knowledge']
}),
TownProductWorth.findAll({
where: { regionId: regionId },
attributes: ['productId', 'worthPercent']
where: { regionId: { [Op.in]: worthRegionIds } },
attributes: ['productId', 'regionId', 'worthPercent']
})
]);
const knowledgeMap = new Map(knowledges.map(k => [k.productId, k.knowledge || 0]));
const worthMap = new Map(townWorths.map(tw => [tw.productId, tw.worthPercent || 50]));
const maxWorthByProduct = new Map();
for (const tw of townWorths) {
const w = tw.worthPercent ?? 50;
const prev = maxWorthByProduct.get(tw.productId);
maxWorthByProduct.set(tw.productId, prev == null ? w : Math.max(prev, w));
}
const prices = {};
for (const product of products) {
const worthPercent = worthMap.get(product.id) ?? 50;
const worthPercent = maxWorthByProduct.get(product.id) ?? 50;
const knowledgeFactor = knowledgeMap.get(product.id) || 0;
const price = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
if (price !== null) prices[product.id] = price;
@@ -6987,11 +7031,20 @@ ORDER BY r.id`,
for (const product of products) {
const knowledgeFactor = knowledgeByProduct.get(product.id) || 0;
const currentPrice = priceByProduct.get(product.id) ?? product.sellCost ?? 0;
let currentRegionalPrice = currentPrice;
if (currentRegionId) {
const clientPriceRaw = priceByProduct.get(product.id);
const clientPriceNum = clientPriceRaw != null && clientPriceRaw !== ''
? Number(clientPriceRaw)
: NaN;
let currentRegionalPrice;
if (!Number.isNaN(clientPriceNum)) {
// Referenzpreis wie in der Ertrags-Tabelle (z. B. MAX-Worth über Filialen)
currentRegionalPrice = clientPriceNum;
} else if (currentRegionId) {
const wp = worthByProductRegion.get(`${product.id}-${currentRegionId}`) ?? 50;
currentRegionalPrice = calcRegionalSellPriceSync(product, knowledgeFactor, wp) ?? currentPrice;
currentRegionalPrice = calcRegionalSellPriceSync(product, knowledgeFactor, wp)
?? Number(product.sellCost) ?? 0;
} else {
currentRegionalPrice = Number(product.sellCost) || 0;
}
const results = [];