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:
@@ -76,6 +76,8 @@
|
||||
branchId: { type: Number, required: true },
|
||||
products: { type: Array, required: true },
|
||||
currentCertificate: { type: Number, default: 1 },
|
||||
/** Gleiche regionalen Preise wie Ertrags-Tabelle (BranchView / prices-in-region, optional MAX-Worth). */
|
||||
productPricesCache: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -194,10 +196,15 @@
|
||||
if (!product.knowledges || product.knowledges.length === 0) {
|
||||
return { absolute: 0, perMinute: 0 };
|
||||
}
|
||||
const knowledgeFactor = product.knowledges[0].knowledge || 0;
|
||||
const maxPrice = product.sellCost;
|
||||
const minPrice = maxPrice * 0.6;
|
||||
const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
||||
let revenuePerUnit;
|
||||
if (this.productPricesCache[product.id] !== undefined) {
|
||||
revenuePerUnit = this.productPricesCache[product.id];
|
||||
} 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;
|
||||
return {
|
||||
absolute: revenuePerUnit.toFixed(2),
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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>{{ product.knowledges && product.knowledges[0] ? product.knowledges[0].knowledge : 0 }}</td>
|
||||
<td>{{ calculateProductRevenue(product).absolute }}</td>
|
||||
@@ -72,13 +76,19 @@
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
productWithMaxProfitPerMinute() {
|
||||
if (!this.products || this.products.length === 0) return null;
|
||||
return this.products.reduce((maxProduct, currentProduct) => {
|
||||
const currentProfit = parseFloat(this.calculateProductProfit(currentProduct).perMinute);
|
||||
const maxProfit = maxProduct ? parseFloat(this.calculateProductProfit(maxProduct).perMinute) : Number.NEGATIVE_INFINITY;
|
||||
return currentProfit > maxProfit ? currentProduct : maxProduct;
|
||||
}, null);
|
||||
/** Gleiche Metrik wie Daemon: Gewinn/Minute ≈ (Erlös/Stück − Stückkosten) / Produktionszeit; höchste zuerst. */
|
||||
sortedProductsByProfitPerMinute() {
|
||||
if (!this.products || this.products.length === 0) return [];
|
||||
return [...this.products].sort((a, b) => {
|
||||
const pb = parseFloat(this.calculateProductProfit(b).perMinute);
|
||||
const pa = parseFloat(this.calculateProductProfit(a).perMinute);
|
||||
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() {
|
||||
|
||||
@@ -1190,6 +1190,7 @@
|
||||
"loot": "Bilin"
|
||||
},
|
||||
"attacks": {
|
||||
"source": "Pikas nga bahin",
|
||||
"target": "Tig-atake",
|
||||
"date": "Petsa",
|
||||
"success": "Kalampusan",
|
||||
|
||||
@@ -1574,6 +1574,7 @@
|
||||
"loot": "Beute"
|
||||
},
|
||||
"attacks": {
|
||||
"source": "Gegenüber",
|
||||
"target": "Angreifer",
|
||||
"date": "Datum",
|
||||
"success": "Erfolg",
|
||||
|
||||
@@ -1191,6 +1191,7 @@
|
||||
"loot": "Loot"
|
||||
},
|
||||
"attacks": {
|
||||
"source": "Other party",
|
||||
"target": "Attacker",
|
||||
"date": "Date",
|
||||
"success": "Success",
|
||||
|
||||
@@ -1574,6 +1574,7 @@
|
||||
"loot": "Botín"
|
||||
},
|
||||
"attacks": {
|
||||
"source": "Contraparte",
|
||||
"target": "Atacante",
|
||||
"date": "Fecha",
|
||||
"success": "Éxito",
|
||||
|
||||
@@ -1572,6 +1572,7 @@
|
||||
"loot": "proie"
|
||||
},
|
||||
"attacks": {
|
||||
"source": "Autre partie",
|
||||
"target": "attaquant",
|
||||
"date": "Date",
|
||||
"success": "Succès",
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
:branchId="selectedBranch.id"
|
||||
:products="products"
|
||||
:current-certificate="currentCertificate"
|
||||
:product-prices-cache="productPricesCache"
|
||||
ref="productionSection"
|
||||
/>
|
||||
<!-- Tax summary for production -->
|
||||
@@ -435,7 +436,8 @@ export default {
|
||||
vehicles: [],
|
||||
activeTab: 'production',
|
||||
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: [
|
||||
{ value: 'production', label: 'falukant.branch.tabs.production' },
|
||||
{ value: 'inventory', label: 'falukant.branch.tabs.inventory' },
|
||||
@@ -625,6 +627,11 @@ export default {
|
||||
this.$refs.statusBar?.fetchStatus();
|
||||
await this.loadCurrentCertificate();
|
||||
await this.loadProducts();
|
||||
if (this.selectedBranch) {
|
||||
await this.loadVehicles();
|
||||
this.productPricesCacheKey = null;
|
||||
await this.loadProductPricesForCurrentBranch();
|
||||
}
|
||||
this.$refs.productionSection?.loadProductions();
|
||||
this.$refs.productionSection?.loadStorage();
|
||||
this.$refs.storageSection?.loadStorageData();
|
||||
@@ -732,20 +739,23 @@ export default {
|
||||
async loadProductPricesForCurrentBranch() {
|
||||
if (!this.selectedBranch || !this.selectedBranch.regionId) {
|
||||
this.productPricesCache = {};
|
||||
this.productPricesCacheRegionId = null;
|
||||
this.productPricesCacheKey = null;
|
||||
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;
|
||||
}
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/products/prices-in-region', {
|
||||
params: {
|
||||
regionId: this.selectedBranch.regionId
|
||||
}
|
||||
});
|
||||
const params = { regionId: this.selectedBranch.regionId };
|
||||
if (useNetworkWorth) {
|
||||
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.productPricesCacheRegionId = this.selectedBranch.regionId;
|
||||
this.productPricesCacheKey = cacheKey;
|
||||
} catch (error) {
|
||||
console.error(`Error loading product prices for region ${this.selectedBranch.regionId}:`, error);
|
||||
// Fallback: Lade Preise einzeln (alte Methode)
|
||||
@@ -770,7 +780,7 @@ export default {
|
||||
}
|
||||
}
|
||||
this.productPricesCache = prices;
|
||||
this.productPricesCacheRegionId = this.selectedBranch?.regionId ?? null;
|
||||
this.productPricesCacheKey = cacheKey;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -31,8 +31,24 @@
|
||||
{{ $t('falukant.nobility.nextTitle') }}:
|
||||
<strong>{{ $t(`falukant.titles.${gender}.${next.labelTr}`) }}</strong>
|
||||
</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">
|
||||
<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) }}
|
||||
</li>
|
||||
</ul>
|
||||
@@ -74,7 +90,9 @@
|
||||
highestPoliticalOffice: null,
|
||||
highestOfficeAny: null,
|
||||
nextAdvanceAt: null,
|
||||
isAdvancing: false
|
||||
isAdvancing: false,
|
||||
/** Nach fehlgeschlagenem POST /nobility: vom Server gelieferte unmet [{ type, required }] */
|
||||
advanceUnmetList: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -124,6 +142,7 @@
|
||||
this.highestPoliticalOffice = data.highestPoliticalOffice || null;
|
||||
this.highestOfficeAny = data.highestOfficeAny || null;
|
||||
this.nextAdvanceAt = data.nextAdvanceAt || null;
|
||||
this.advanceUnmetList = null;
|
||||
} catch (err) {
|
||||
console.error('Error loading nobility:', err);
|
||||
}
|
||||
@@ -131,6 +150,7 @@
|
||||
async applyAdvance() {
|
||||
if (!this.canAdvance || this.isAdvancing) return;
|
||||
this.isAdvancing = true;
|
||||
this.advanceUnmetList = null;
|
||||
try {
|
||||
await apiClient.post('/api/falukant/nobility');
|
||||
await this.loadNobility();
|
||||
@@ -144,10 +164,7 @@
|
||||
const msg = this.$t('falukant.nobility.errors.tooSoon');
|
||||
this.$root.$refs.errorDialog?.open(retryStr ? `${msg} — ${this.$t('falukant.nobility.cooldown', { date: retryStr })}` : msg);
|
||||
} else if (resp.data?.message === 'nobilityRequirements') {
|
||||
const 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}`);
|
||||
this.advanceUnmetList = Array.isArray(resp.data?.unmet) ? resp.data.unmet : [];
|
||||
} else {
|
||||
this.$root.$refs.errorDialog?.open(this.$t('falukant.nobility.errors.generic'));
|
||||
}
|
||||
@@ -186,11 +203,18 @@
|
||||
}
|
||||
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) {
|
||||
if (!info?.name) {
|
||||
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;
|
||||
return this.$t('falukant.nobility.officeWithRank', { label, rank: info.rank });
|
||||
},
|
||||
@@ -248,6 +272,25 @@
|
||||
list-style: disc inside;
|
||||
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 {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
|
||||
Reference in New Issue
Block a user