feat(falukant): enhance product pricing and nobility advancement features
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
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:
9
.cursor/rules/legacy-cpp-workers.mdc
Normal file
9
.cursor/rules/legacy-cpp-workers.mdc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
description: C++-Worker unter src/ sind obsolet — nicht erweitern oder als Quelle für Spiellogik nutzen
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Legacy C++ (`src/`)
|
||||||
|
|
||||||
|
- Verzeichnis **`src/`** (C++-Worker, WebSocket-Server): **obsolet**. Keine neuen Features, keine fachlichen Fixes dort planen oder umsetzen, sofern der Nutzer nicht ausdrücklich etwas anderes verlangt.
|
||||||
|
- Falukant-Hintergrundlogik: **Backend** (`backend/`), **externer Daemon**, **Frontend** — siehe `docs/LEGACY_CPP_WORKERS.md`.
|
||||||
@@ -1 +1,3 @@
|
|||||||
## zum testen des push
|
## zum testen des push
|
||||||
|
|
||||||
|
Hinweis: Das Verzeichnis **`src/`** (C++-Worker) ist veraltet; siehe [`docs/LEGACY_CPP_WORKERS.md`](docs/LEGACY_CPP_WORKERS.md).
|
||||||
@@ -245,7 +245,9 @@ class FalukantController {
|
|||||||
if (Number.isNaN(regionId)) {
|
if (Number.isNaN(regionId)) {
|
||||||
throw new Error('regionId is required');
|
throw new Error('regionId is required');
|
||||||
}
|
}
|
||||||
return this.service.getAllProductPricesInRegion(userId, regionId);
|
const networkWorth = req.query.networkWorth === '1' || req.query.networkWorth === 'true';
|
||||||
|
const branchId = req.query.branchId != null ? parseInt(req.query.branchId, 10) : null;
|
||||||
|
return this.service.getAllProductPricesInRegion(userId, regionId, { networkWorth, branchId });
|
||||||
});
|
});
|
||||||
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
||||||
const productId = parseInt(req.query.productId, 10);
|
const productId = parseInt(req.query.productId, 10);
|
||||||
|
|||||||
@@ -5821,51 +5821,61 @@ class FalukantService extends BaseService {
|
|||||||
const oneWeekAgo = new Date(now.getTime());
|
const oneWeekAgo = new Date(now.getTime());
|
||||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||||
if (user.lastNobilityAdvanceAt > oneWeekAgo) {
|
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();
|
const nextTitle = nobility.next.toJSON();
|
||||||
let fulfilled = true;
|
|
||||||
let cost = 0;
|
let cost = 0;
|
||||||
|
const unmet = [];
|
||||||
for (const requirement of nextTitle.requirements) {
|
for (const requirement of nextTitle.requirements) {
|
||||||
|
let ok = true;
|
||||||
switch (requirement.requirementType) {
|
switch (requirement.requirementType) {
|
||||||
case 'money':
|
case 'money':
|
||||||
fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement);
|
ok = await this.checkMoneyRequirement(user, requirement);
|
||||||
break;
|
break;
|
||||||
case 'cost':
|
case 'cost':
|
||||||
fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement);
|
ok = await this.checkMoneyRequirement(user, requirement);
|
||||||
cost = requirement.requirementValue;
|
cost = requirement.requirementValue;
|
||||||
break;
|
break;
|
||||||
case 'branches':
|
case 'branches':
|
||||||
fulfilled = fulfilled && await this.checkBranchesRequirement(hashedUserId, requirement);
|
ok = await this.checkBranchesRequirement(hashedUserId, requirement);
|
||||||
break;
|
break;
|
||||||
case 'reputation':
|
case 'reputation':
|
||||||
fulfilled = fulfilled && await this.checkReputationRequirement(user, requirement);
|
ok = await this.checkReputationRequirement(user, requirement);
|
||||||
break;
|
break;
|
||||||
case 'house_position':
|
case 'house_position':
|
||||||
fulfilled = fulfilled && await this.checkHousePositionRequirement(user, requirement);
|
ok = await this.checkHousePositionRequirement(user, requirement);
|
||||||
break;
|
break;
|
||||||
case 'house_condition':
|
case 'house_condition':
|
||||||
fulfilled = fulfilled && await this.checkHouseConditionRequirement(user, requirement);
|
ok = await this.checkHouseConditionRequirement(user, requirement);
|
||||||
break;
|
break;
|
||||||
case 'office_rank_any':
|
case 'office_rank_any':
|
||||||
fulfilled = fulfilled && await this.checkOfficeRankAnyRequirement(user, requirement);
|
ok = await this.checkOfficeRankAnyRequirement(user, requirement);
|
||||||
break;
|
break;
|
||||||
case 'office_rank_political':
|
case 'office_rank_political':
|
||||||
fulfilled = fulfilled && await this.checkOfficeRankPoliticalRequirement(user, requirement);
|
ok = await this.checkOfficeRankPoliticalRequirement(user, requirement);
|
||||||
break;
|
break;
|
||||||
case 'lover_count_max':
|
case 'lover_count_max':
|
||||||
fulfilled = fulfilled && await this.checkLoverCountMaxRequirement(user, requirement);
|
ok = await this.checkLoverCountMaxRequirement(user, requirement);
|
||||||
break;
|
break;
|
||||||
case 'lover_count_min':
|
case 'lover_count_min':
|
||||||
fulfilled = fulfilled && await this.checkLoverCountMinRequirement(user, requirement);
|
ok = await this.checkLoverCountMinRequirement(user, requirement);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
fulfilled = false;
|
ok = false;
|
||||||
};
|
}
|
||||||
|
if (!ok) {
|
||||||
|
unmet.push({
|
||||||
|
type: requirement.requirementType,
|
||||||
|
required: requirement.requirementValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!fulfilled) {
|
if (unmet.length > 0) {
|
||||||
throw new Error('Requirements not fulfilled');
|
throw { status: 412, message: 'nobilityRequirements', unmet };
|
||||||
}
|
}
|
||||||
const newTitle = await TitleOfNobility.findOne({
|
const newTitle = await TitleOfNobility.findOne({
|
||||||
where: { level: nobility.current.level + 1 }
|
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 {
|
try {
|
||||||
|
const networkWorth = Boolean(options.networkWorth);
|
||||||
|
const branchId = options.branchId != null ? Number(options.branchId) : null;
|
||||||
|
|
||||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
||||||
if (!character) {
|
if (!character) {
|
||||||
throw new Error(`No FalukantCharacter found for user with id ${user.id}`);
|
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([
|
const [products, knowledges, townWorths] = await Promise.all([
|
||||||
ProductType.findAll({ attributes: ['id', 'sellCost'] }),
|
ProductType.findAll({ attributes: ['id', 'sellCost'] }),
|
||||||
Knowledge.findAll({
|
Knowledge.findAll({
|
||||||
@@ -6807,17 +6846,22 @@ class FalukantService extends BaseService {
|
|||||||
attributes: ['productId', 'knowledge']
|
attributes: ['productId', 'knowledge']
|
||||||
}),
|
}),
|
||||||
TownProductWorth.findAll({
|
TownProductWorth.findAll({
|
||||||
where: { regionId: regionId },
|
where: { regionId: { [Op.in]: worthRegionIds } },
|
||||||
attributes: ['productId', 'worthPercent']
|
attributes: ['productId', 'regionId', 'worthPercent']
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const knowledgeMap = new Map(knowledges.map(k => [k.productId, k.knowledge || 0]));
|
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 = {};
|
const prices = {};
|
||||||
for (const product of products) {
|
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 knowledgeFactor = knowledgeMap.get(product.id) || 0;
|
||||||
const price = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
|
const price = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
|
||||||
if (price !== null) prices[product.id] = price;
|
if (price !== null) prices[product.id] = price;
|
||||||
@@ -6987,11 +7031,20 @@ ORDER BY r.id`,
|
|||||||
|
|
||||||
for (const product of products) {
|
for (const product of products) {
|
||||||
const knowledgeFactor = knowledgeByProduct.get(product.id) || 0;
|
const knowledgeFactor = knowledgeByProduct.get(product.id) || 0;
|
||||||
const currentPrice = priceByProduct.get(product.id) ?? product.sellCost ?? 0;
|
const clientPriceRaw = priceByProduct.get(product.id);
|
||||||
let currentRegionalPrice = currentPrice;
|
const clientPriceNum = clientPriceRaw != null && clientPriceRaw !== ''
|
||||||
if (currentRegionId) {
|
? 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;
|
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 = [];
|
const results = [];
|
||||||
|
|||||||
@@ -33,16 +33,14 @@ Backend-Implementierung: `getCertificateCompletedProductionCount` in `backend/se
|
|||||||
Stattdessen:
|
Stattdessen:
|
||||||
|
|
||||||
- Zähler-Reset über **`certificate_productions_count_since`** (siehe oben).
|
- Zähler-Reset über **`certificate_productions_count_since`** (siehe oben).
|
||||||
- Alte Zeilen werden mit **Retention 30 Tage** bereinigt, damit die Tabelle nicht unbegrenzt wächst.
|
- Alte Zeilen mit **Retention 30 Tage** bereinigen (z. B. im **YpDaemon** mit `QUERY_DELETE_OLD_PRODUCTIONS`), damit die Tabelle nicht unbegrenzt wächst:
|
||||||
|
|
||||||
In diesem Repo: **`UserCharacterWorker`** führt stündlich `QUERY_DELETE_OLD_PRODUCTIONS` aus:
|
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
DELETE FROM falukant_log.production
|
DELETE FROM falukant_log.production
|
||||||
WHERE COALESCE(production_timestamp, production_date::timestamp) < NOW() - INTERVAL '30 days';
|
WHERE COALESCE(production_timestamp, production_date::timestamp) < NOW() - INTERVAL '30 days';
|
||||||
```
|
```
|
||||||
|
|
||||||
Parallel löscht derselbe Worker weiterhin einzelne Zeilen nach dem Wissens-Update (`QUERY_DELETE_LOG_ENTRY`), sobald die Zeile „vom Vortag“ verarbeitet wurde (`QUERY_UPDATE_GET_ITEMS_TO_UPDATE` mit `COALESCE` für das Datumsfenster).
|
Die C++-Worker unter **`src/`** sind **[obsolet](LEGACY_CPP_WORKERS.md)** und werden für Retention oder Zertifikatslogik **nicht** mehr als Bezug genutzt.
|
||||||
|
|
||||||
## Migrationen
|
## Migrationen
|
||||||
|
|
||||||
|
|||||||
9
docs/LEGACY_CPP_WORKERS.md
Normal file
9
docs/LEGACY_CPP_WORKERS.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Legacy: C++-Worker (`src/`)
|
||||||
|
|
||||||
|
Die C++-Prozesse unter **`src/`** (z. B. `*worker.cpp` / `websocket_server`) gelten als **obsolet**.
|
||||||
|
|
||||||
|
- **Keine** neuen Features oder fachliche Korrekturen mehr in diesem Code pflegen.
|
||||||
|
- **Quelle der Wahrheit** für Hintergrundlogik (Zertifikat, Retention, Wirtschaft, …) ist der **externe Daemon** bzw. **Backend-Jobs**, nicht diese Worker.
|
||||||
|
- Beim Arbeiten an Falukant bitte **Backend** (`backend/`), **Frontend** und **Daemon-Repo** berücksichtigen; `src/` nur bei Bedarf für Build-/Entfernen-Fragen anfassen.
|
||||||
|
|
||||||
|
Siehe auch: [`FALUKANT_PRODUCTION_CERTIFICATE.md`](./FALUKANT_PRODUCTION_CERTIFICATE.md) (Zählung und Log-Retention ohne Bezug auf die C++-Worker).
|
||||||
@@ -568,6 +568,9 @@
|
|||||||
"repelled": "Repelled",
|
"repelled": "Repelled",
|
||||||
"partial_success": "Partial success",
|
"partial_success": "Partial success",
|
||||||
"major_success": "Major success"
|
"major_success": "Major success"
|
||||||
|
},
|
||||||
|
"attacks": {
|
||||||
|
"source": "Pikas nga bahin"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
branchId: { type: Number, required: true },
|
branchId: { type: Number, required: true },
|
||||||
products: { type: Array, required: true },
|
products: { type: Array, required: true },
|
||||||
currentCertificate: { type: Number, default: 1 },
|
currentCertificate: { type: Number, default: 1 },
|
||||||
|
/** Gleiche regionalen Preise wie Ertrags-Tabelle (BranchView / prices-in-region, optional MAX-Worth). */
|
||||||
|
productPricesCache: { type: Object, default: () => ({}) },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -194,10 +196,15 @@
|
|||||||
if (!product.knowledges || product.knowledges.length === 0) {
|
if (!product.knowledges || product.knowledges.length === 0) {
|
||||||
return { absolute: 0, perMinute: 0 };
|
return { absolute: 0, perMinute: 0 };
|
||||||
}
|
}
|
||||||
const knowledgeFactor = product.knowledges[0].knowledge || 0;
|
let revenuePerUnit;
|
||||||
const maxPrice = product.sellCost;
|
if (this.productPricesCache[product.id] !== undefined) {
|
||||||
const minPrice = maxPrice * 0.6;
|
revenuePerUnit = this.productPricesCache[product.id];
|
||||||
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
} else {
|
||||||
|
const knowledgeFactor = product.knowledges[0].knowledge || 0;
|
||||||
|
const maxPrice = product.sellCost;
|
||||||
|
const minPrice = maxPrice * 0.7; // KNOWLEDGE_PRICE_FLOOR wie backend/falukantProductEconomy
|
||||||
|
revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
||||||
|
}
|
||||||
const perMinute = product.productionTime > 0 ? revenuePerUnit / product.productionTime : 0;
|
const perMinute = product.productionTime > 0 ? revenuePerUnit / product.productionTime : 0;
|
||||||
return {
|
return {
|
||||||
absolute: revenuePerUnit.toFixed(2),
|
absolute: revenuePerUnit.toFixed(2),
|
||||||
|
|||||||
@@ -20,7 +20,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="product in products" :key="product.id" :class="{ highlight: product.id === productWithMaxProfitPerMinute?.id }">
|
<tr
|
||||||
|
v-for="product in sortedProductsByProfitPerMinute"
|
||||||
|
:key="product.id"
|
||||||
|
:class="{ highlight: product.id === topProductByProfitPerMinute?.id }"
|
||||||
|
>
|
||||||
<td>{{ $t(`falukant.product.${product.labelTr}`) }}</td>
|
<td>{{ $t(`falukant.product.${product.labelTr}`) }}</td>
|
||||||
<td>{{ product.knowledges && product.knowledges[0] ? product.knowledges[0].knowledge : 0 }}</td>
|
<td>{{ product.knowledges && product.knowledges[0] ? product.knowledges[0].knowledge : 0 }}</td>
|
||||||
<td>{{ calculateProductRevenue(product).absolute }}</td>
|
<td>{{ calculateProductRevenue(product).absolute }}</td>
|
||||||
@@ -72,13 +76,19 @@
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
productWithMaxProfitPerMinute() {
|
/** Gleiche Metrik wie Daemon: Gewinn/Minute ≈ (Erlös/Stück − Stückkosten) / Produktionszeit; höchste zuerst. */
|
||||||
if (!this.products || this.products.length === 0) return null;
|
sortedProductsByProfitPerMinute() {
|
||||||
return this.products.reduce((maxProduct, currentProduct) => {
|
if (!this.products || this.products.length === 0) return [];
|
||||||
const currentProfit = parseFloat(this.calculateProductProfit(currentProduct).perMinute);
|
return [...this.products].sort((a, b) => {
|
||||||
const maxProfit = maxProduct ? parseFloat(this.calculateProductProfit(maxProduct).perMinute) : Number.NEGATIVE_INFINITY;
|
const pb = parseFloat(this.calculateProductProfit(b).perMinute);
|
||||||
return currentProfit > maxProfit ? currentProduct : maxProduct;
|
const pa = parseFloat(this.calculateProductProfit(a).perMinute);
|
||||||
}, null);
|
if (pb !== pa) return pb - pa;
|
||||||
|
return (a.labelTr || '').localeCompare(b.labelTr || '');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
topProductByProfitPerMinute() {
|
||||||
|
const list = this.sortedProductsByProfitPerMinute;
|
||||||
|
return list.length ? list[0] : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
|||||||
@@ -1190,6 +1190,7 @@
|
|||||||
"loot": "Bilin"
|
"loot": "Bilin"
|
||||||
},
|
},
|
||||||
"attacks": {
|
"attacks": {
|
||||||
|
"source": "Pikas nga bahin",
|
||||||
"target": "Tig-atake",
|
"target": "Tig-atake",
|
||||||
"date": "Petsa",
|
"date": "Petsa",
|
||||||
"success": "Kalampusan",
|
"success": "Kalampusan",
|
||||||
|
|||||||
@@ -1574,6 +1574,7 @@
|
|||||||
"loot": "Beute"
|
"loot": "Beute"
|
||||||
},
|
},
|
||||||
"attacks": {
|
"attacks": {
|
||||||
|
"source": "Gegenüber",
|
||||||
"target": "Angreifer",
|
"target": "Angreifer",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
"success": "Erfolg",
|
"success": "Erfolg",
|
||||||
|
|||||||
@@ -1191,6 +1191,7 @@
|
|||||||
"loot": "Loot"
|
"loot": "Loot"
|
||||||
},
|
},
|
||||||
"attacks": {
|
"attacks": {
|
||||||
|
"source": "Other party",
|
||||||
"target": "Attacker",
|
"target": "Attacker",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
|
|||||||
@@ -1574,6 +1574,7 @@
|
|||||||
"loot": "Botín"
|
"loot": "Botín"
|
||||||
},
|
},
|
||||||
"attacks": {
|
"attacks": {
|
||||||
|
"source": "Contraparte",
|
||||||
"target": "Atacante",
|
"target": "Atacante",
|
||||||
"date": "Fecha",
|
"date": "Fecha",
|
||||||
"success": "Éxito",
|
"success": "Éxito",
|
||||||
|
|||||||
@@ -1572,6 +1572,7 @@
|
|||||||
"loot": "proie"
|
"loot": "proie"
|
||||||
},
|
},
|
||||||
"attacks": {
|
"attacks": {
|
||||||
|
"source": "Autre partie",
|
||||||
"target": "attaquant",
|
"target": "attaquant",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"success": "Succès",
|
"success": "Succès",
|
||||||
|
|||||||
@@ -107,6 +107,7 @@
|
|||||||
:branchId="selectedBranch.id"
|
:branchId="selectedBranch.id"
|
||||||
:products="products"
|
:products="products"
|
||||||
:current-certificate="currentCertificate"
|
:current-certificate="currentCertificate"
|
||||||
|
:product-prices-cache="productPricesCache"
|
||||||
ref="productionSection"
|
ref="productionSection"
|
||||||
/>
|
/>
|
||||||
<!-- Tax summary for production -->
|
<!-- Tax summary for production -->
|
||||||
@@ -435,7 +436,8 @@ export default {
|
|||||||
vehicles: [],
|
vehicles: [],
|
||||||
activeTab: 'production',
|
activeTab: 'production',
|
||||||
productPricesCache: {}, // Cache für regionale Preise: { productId: price }
|
productPricesCache: {}, // Cache für regionale Preise: { productId: price }
|
||||||
productPricesCacheRegionId: null, // regionId, für die der Cache gültig ist
|
/** Cache-Schlüssel: Region + ob MAX(worth) über alle Filialregionen (bei Fahrzeug) */
|
||||||
|
productPricesCacheKey: null,
|
||||||
tabs: [
|
tabs: [
|
||||||
{ value: 'production', label: 'falukant.branch.tabs.production' },
|
{ value: 'production', label: 'falukant.branch.tabs.production' },
|
||||||
{ value: 'inventory', label: 'falukant.branch.tabs.inventory' },
|
{ value: 'inventory', label: 'falukant.branch.tabs.inventory' },
|
||||||
@@ -625,6 +627,11 @@ export default {
|
|||||||
this.$refs.statusBar?.fetchStatus();
|
this.$refs.statusBar?.fetchStatus();
|
||||||
await this.loadCurrentCertificate();
|
await this.loadCurrentCertificate();
|
||||||
await this.loadProducts();
|
await this.loadProducts();
|
||||||
|
if (this.selectedBranch) {
|
||||||
|
await this.loadVehicles();
|
||||||
|
this.productPricesCacheKey = null;
|
||||||
|
await this.loadProductPricesForCurrentBranch();
|
||||||
|
}
|
||||||
this.$refs.productionSection?.loadProductions();
|
this.$refs.productionSection?.loadProductions();
|
||||||
this.$refs.productionSection?.loadStorage();
|
this.$refs.productionSection?.loadStorage();
|
||||||
this.$refs.storageSection?.loadStorageData();
|
this.$refs.storageSection?.loadStorageData();
|
||||||
@@ -732,20 +739,23 @@ export default {
|
|||||||
async loadProductPricesForCurrentBranch() {
|
async loadProductPricesForCurrentBranch() {
|
||||||
if (!this.selectedBranch || !this.selectedBranch.regionId) {
|
if (!this.selectedBranch || !this.selectedBranch.regionId) {
|
||||||
this.productPricesCache = {};
|
this.productPricesCache = {};
|
||||||
this.productPricesCacheRegionId = null;
|
this.productPricesCacheKey = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.productPricesCacheRegionId === this.selectedBranch.regionId && Object.keys(this.productPricesCache).length > 0) {
|
const useNetworkWorth = Array.isArray(this.vehicles) && this.vehicles.length > 0;
|
||||||
|
const cacheKey = `${this.selectedBranch.regionId}:${useNetworkWorth ? 'net' : 'loc'}:${this.selectedBranch.id}`;
|
||||||
|
if (this.productPricesCacheKey === cacheKey && Object.keys(this.productPricesCache).length > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get('/api/falukant/products/prices-in-region', {
|
const params = { regionId: this.selectedBranch.regionId };
|
||||||
params: {
|
if (useNetworkWorth) {
|
||||||
regionId: this.selectedBranch.regionId
|
params.networkWorth = '1';
|
||||||
}
|
params.branchId = this.selectedBranch.id;
|
||||||
});
|
}
|
||||||
|
const { data } = await apiClient.get('/api/falukant/products/prices-in-region', { params });
|
||||||
this.productPricesCache = data.prices || {};
|
this.productPricesCache = data.prices || {};
|
||||||
this.productPricesCacheRegionId = this.selectedBranch.regionId;
|
this.productPricesCacheKey = cacheKey;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error loading product prices for region ${this.selectedBranch.regionId}:`, error);
|
console.error(`Error loading product prices for region ${this.selectedBranch.regionId}:`, error);
|
||||||
// Fallback: Lade Preise einzeln (alte Methode)
|
// Fallback: Lade Preise einzeln (alte Methode)
|
||||||
@@ -770,7 +780,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.productPricesCache = prices;
|
this.productPricesCache = prices;
|
||||||
this.productPricesCacheRegionId = this.selectedBranch?.regionId ?? null;
|
this.productPricesCacheKey = cacheKey;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,24 @@
|
|||||||
{{ $t('falukant.nobility.nextTitle') }}:
|
{{ $t('falukant.nobility.nextTitle') }}:
|
||||||
<strong>{{ $t(`falukant.titles.${gender}.${next.labelTr}`) }}</strong>
|
<strong>{{ $t(`falukant.titles.${gender}.${next.labelTr}`) }}</strong>
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="advanceUnmetList && advanceUnmetList.length > 0"
|
||||||
|
class="nobility-unmet-banner"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<p class="nobility-unmet-banner__title">{{ $t('falukant.nobility.errors.unmet') }}</p>
|
||||||
|
<ul class="nobility-unmet-banner__list">
|
||||||
|
<li v-for="(u, idx) in advanceUnmetList" :key="idx">
|
||||||
|
{{ formatRequirement(u.type, u.required) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<ul class="prerequisites" v-if="next.requirements && next.requirements.length > 0">
|
<ul class="prerequisites" v-if="next.requirements && next.requirements.length > 0">
|
||||||
<li v-for="req in next.requirements" :key="req.titleId">
|
<li
|
||||||
|
v-for="req in next.requirements"
|
||||||
|
:key="`${req.requirementType}-${req.requirementValue}`"
|
||||||
|
:class="{ 'is-unmet': isRequirementUnmet(req) }"
|
||||||
|
>
|
||||||
{{ formatRequirement(req.requirementType, req.requirementValue) }}
|
{{ formatRequirement(req.requirementType, req.requirementValue) }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -74,7 +90,9 @@
|
|||||||
highestPoliticalOffice: null,
|
highestPoliticalOffice: null,
|
||||||
highestOfficeAny: null,
|
highestOfficeAny: null,
|
||||||
nextAdvanceAt: null,
|
nextAdvanceAt: null,
|
||||||
isAdvancing: false
|
isAdvancing: false,
|
||||||
|
/** Nach fehlgeschlagenem POST /nobility: vom Server gelieferte unmet [{ type, required }] */
|
||||||
|
advanceUnmetList: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -124,6 +142,7 @@
|
|||||||
this.highestPoliticalOffice = data.highestPoliticalOffice || null;
|
this.highestPoliticalOffice = data.highestPoliticalOffice || null;
|
||||||
this.highestOfficeAny = data.highestOfficeAny || null;
|
this.highestOfficeAny = data.highestOfficeAny || null;
|
||||||
this.nextAdvanceAt = data.nextAdvanceAt || null;
|
this.nextAdvanceAt = data.nextAdvanceAt || null;
|
||||||
|
this.advanceUnmetList = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading nobility:', err);
|
console.error('Error loading nobility:', err);
|
||||||
}
|
}
|
||||||
@@ -131,6 +150,7 @@
|
|||||||
async applyAdvance() {
|
async applyAdvance() {
|
||||||
if (!this.canAdvance || this.isAdvancing) return;
|
if (!this.canAdvance || this.isAdvancing) return;
|
||||||
this.isAdvancing = true;
|
this.isAdvancing = true;
|
||||||
|
this.advanceUnmetList = null;
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/api/falukant/nobility');
|
await apiClient.post('/api/falukant/nobility');
|
||||||
await this.loadNobility();
|
await this.loadNobility();
|
||||||
@@ -144,10 +164,7 @@
|
|||||||
const msg = this.$t('falukant.nobility.errors.tooSoon');
|
const msg = this.$t('falukant.nobility.errors.tooSoon');
|
||||||
this.$root.$refs.errorDialog?.open(retryStr ? `${msg} — ${this.$t('falukant.nobility.cooldown', { date: retryStr })}` : msg);
|
this.$root.$refs.errorDialog?.open(retryStr ? `${msg} — ${this.$t('falukant.nobility.cooldown', { date: retryStr })}` : msg);
|
||||||
} else if (resp.data?.message === 'nobilityRequirements') {
|
} else if (resp.data?.message === 'nobilityRequirements') {
|
||||||
const unmet = resp.data?.unmet || [];
|
this.advanceUnmetList = Array.isArray(resp.data?.unmet) ? resp.data.unmet : [];
|
||||||
const items = unmet.map(u => this.formatRequirement(u.type, u.required)).join('\n');
|
|
||||||
const base = this.$t('falukant.nobility.errors.unmet');
|
|
||||||
this.$root.$refs.errorDialog?.open(`${base}\n${items}`);
|
|
||||||
} else {
|
} else {
|
||||||
this.$root.$refs.errorDialog?.open(this.$t('falukant.nobility.errors.generic'));
|
this.$root.$refs.errorDialog?.open(this.$t('falukant.nobility.errors.generic'));
|
||||||
}
|
}
|
||||||
@@ -186,11 +203,18 @@
|
|||||||
}
|
}
|
||||||
return this.$t('falukant.nobility.requirement.unknown', { type, amount });
|
return this.$t('falukant.nobility.requirement.unknown', { type, amount });
|
||||||
},
|
},
|
||||||
|
isRequirementUnmet(req) {
|
||||||
|
if (!this.advanceUnmetList?.length || !req) return false;
|
||||||
|
return this.advanceUnmetList.some(
|
||||||
|
(u) => u.type === req.requirementType
|
||||||
|
&& String(u.required) === String(req.requirementValue)
|
||||||
|
);
|
||||||
|
},
|
||||||
formatOfficeInfo(info, source) {
|
formatOfficeInfo(info, source) {
|
||||||
if (!info?.name) {
|
if (!info?.name) {
|
||||||
return this.$t('falukant.nobility.none');
|
return this.$t('falukant.nobility.none');
|
||||||
}
|
}
|
||||||
const baseKey = source === 'church' ? 'falukant.church.offices' : 'falukant.politics.positions';
|
const baseKey = source === 'church' ? 'falukant.church.offices' : 'falukant.politics.offices';
|
||||||
const label = this.$te(`${baseKey}.${info.name}`) ? this.$t(`${baseKey}.${info.name}`) : info.name;
|
const label = this.$te(`${baseKey}.${info.name}`) ? this.$t(`${baseKey}.${info.name}`) : info.name;
|
||||||
return this.$t('falukant.nobility.officeWithRank', { label, rank: info.rank });
|
return this.$t('falukant.nobility.officeWithRank', { label, rank: info.rank });
|
||||||
},
|
},
|
||||||
@@ -248,6 +272,25 @@
|
|||||||
list-style: disc inside;
|
list-style: disc inside;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
.prerequisites li.is-unmet {
|
||||||
|
color: #a32020;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.nobility-unmet-banner {
|
||||||
|
border: 1px solid #d4a0a0;
|
||||||
|
background: rgba(180, 40, 40, 0.08);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.nobility-unmet-banner__title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.nobility-unmet-banner__list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
.cooldown-message {
|
.cooldown-message {
|
||||||
color: #666;
|
color: #666;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ void UserCharacterWorker::run() {
|
|||||||
processCharacterEvents();
|
processCharacterEvents();
|
||||||
updateCharactersMood();
|
updateCharactersMood();
|
||||||
handleCredits();
|
handleCredits();
|
||||||
deleteOldProductionLogs();
|
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
std::cerr << "[UserCharacterWorker] Fehler in processCharacterEvents: " << e.what() << std::endl;
|
std::cerr << "[UserCharacterWorker] Fehler in processCharacterEvents: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
@@ -234,17 +233,6 @@ void UserCharacterWorker::setNewMoney(int falukantUserId, double newAmount) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserCharacterWorker::deleteOldProductionLogs() {
|
|
||||||
try {
|
|
||||||
ConnectionGuard connGuard(pool);
|
|
||||||
auto &db = connGuard.get();
|
|
||||||
db.prepare("QUERY_DELETE_OLD_PRODUCTIONS", QUERY_DELETE_OLD_PRODUCTIONS);
|
|
||||||
db.execute("QUERY_DELETE_OLD_PRODUCTIONS");
|
|
||||||
} catch (const std::exception &e) {
|
|
||||||
std::cerr << "[UserCharacterWorker] Fehler in deleteOldProductionLogs: " << e.what() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UserCharacterWorker::recalculateKnowledge() {
|
void UserCharacterWorker::recalculateKnowledge() {
|
||||||
setCurrentStep("Get character data");
|
setCurrentStep("Get character data");
|
||||||
ConnectionGuard connGuard(pool);
|
ConnectionGuard connGuard(pool);
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ private:
|
|||||||
int calculateHealthChange(int age);
|
int calculateHealthChange(int age);
|
||||||
void handleCharacterDeath(int characterId);
|
void handleCharacterDeath(int characterId);
|
||||||
void recalculateKnowledge();
|
void recalculateKnowledge();
|
||||||
void deleteOldProductionLogs();
|
|
||||||
void processPregnancies();
|
void processPregnancies();
|
||||||
void handleCredits();
|
void handleCredits();
|
||||||
void setHeir(int characterId);
|
void setHeir(int characterId);
|
||||||
@@ -62,13 +61,7 @@ private:
|
|||||||
static constexpr const char *QUERY_UPDATE_GET_ITEMS_TO_UPDATE = R"(
|
static constexpr const char *QUERY_UPDATE_GET_ITEMS_TO_UPDATE = R"(
|
||||||
SELECT id, product_id, producer_id, quantity
|
SELECT id, product_id, producer_id, quantity
|
||||||
FROM falukant_log.production p
|
FROM falukant_log.production p
|
||||||
WHERE (COALESCE(p.production_timestamp, p.production_date::timestamp))::date < CURRENT_DATE
|
WHERE p.production_timestamp::date < current_date
|
||||||
)";
|
|
||||||
|
|
||||||
/** Log-Retention: ältere Zeilen entfernen (Daemon/UI-Zertifikatszählung braucht Historie nur begrenzt). */
|
|
||||||
static constexpr const char *QUERY_DELETE_OLD_PRODUCTIONS = R"(
|
|
||||||
DELETE FROM falukant_log.production
|
|
||||||
WHERE COALESCE(production_timestamp, production_date::timestamp) < NOW() - INTERVAL '30 days'
|
|
||||||
)";
|
)";
|
||||||
|
|
||||||
static constexpr const char *QUERY_UPDATE_GET_CHARACTER_IDS = R"(
|
static constexpr const char *QUERY_UPDATE_GET_CHARACTER_IDS = R"(
|
||||||
|
|||||||
@@ -72,9 +72,8 @@ void ValueRecalculationWorker::calculateProductKnowledge() {
|
|||||||
sendMessageToFalukantUsers(userId, message);
|
sendMessageToFalukantUsers(userId, message);
|
||||||
}
|
}
|
||||||
/* Kein DELETE mehr auf falukant_log.production um Mitternacht: Die Einträge werden für
|
/* Kein DELETE mehr auf falukant_log.production um Mitternacht: Die Einträge werden für
|
||||||
* Zertifikatsfortschritt (Backend + Daemon) benötigt.
|
* Zertifikatsfortschritt (u. a. Backend-Aggregation) benötigt und dürfen nicht täglich verworfen werden.
|
||||||
* UserCharacterWorker: nach Wissens-Update pro Zeile löschen; zusätzlich stündlich Retention
|
* Alte Zeilen räumt der UserCharacterWorker nach Wissens-Update weiterhin gezielt auf (pro Zeile). */
|
||||||
* QUERY_DELETE_OLD_PRODUCTIONS (30 Tage, COALESCE(timestamp, production_date)). */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ValueRecalculationWorker::calculateRegionalSellPrice() {
|
void ValueRecalculationWorker::calculateRegionalSellPrice() {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ private:
|
|||||||
SET knowledge = LEAST(100, k.knowledge + 1)
|
SET knowledge = LEAST(100, k.knowledge + 1)
|
||||||
FROM falukant_data."character" c
|
FROM falukant_data."character" c
|
||||||
JOIN falukant_log.production p
|
JOIN falukant_log.production p
|
||||||
ON DATE(COALESCE(p.production_timestamp, p.production_date::timestamp)) = CURRENT_DATE - INTERVAL '1 day'
|
ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day'
|
||||||
WHERE c.id = k.character_id
|
WHERE c.id = k.character_id
|
||||||
AND c.user_id = 18
|
AND c.user_id = 18
|
||||||
AND k.product_id = 10
|
AND k.product_id = 10
|
||||||
@@ -51,7 +51,7 @@ private:
|
|||||||
static constexpr const char *QUERY_GET_PRODUCERS_LAST_DAY = R"(
|
static constexpr const char *QUERY_GET_PRODUCERS_LAST_DAY = R"(
|
||||||
select p."producer_id"
|
select p."producer_id"
|
||||||
from falukant_log.production p
|
from falukant_log.production p
|
||||||
where date(COALESCE(p.production_timestamp, p.production_date::timestamp)) = CURRENT_DATE - interval '1 day'
|
where date(p."production_timestamp") = CURRENT_DATE - interval '1 day'
|
||||||
group by producer_id
|
group by producer_id
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user