Update product definitions and revenue calculations in Falukant: Adjust product sell costs and production times for better balance. Refactor revenue calculations to focus on profit per minute instead of revenue per minute. Enhance localization files to include new terms related to product unlocks and certificate levels in English, German, and Spanish, improving user experience across languages.
This commit is contained in:
@@ -97,16 +97,26 @@ async function getBranchOrFail(userId, branchId) {
|
|||||||
return branch;
|
return branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const PRODUCTION_COST_BASE = 6.0;
|
||||||
* Gesamtkosten für eine Produktionscharge (früher: quantity * category * 6 pro Einheit).
|
const PRODUCTION_COST_PER_PRODUCT_CATEGORY = 1.0;
|
||||||
* Pro Einheit jetzt 2 * Kategorie (z.B. Kat.1 = 2, Kat.2 = 4), damit Nettoerlös und
|
const PRODUCTION_HEADROOM_DISCOUNT_PER_STEP = 0.035;
|
||||||
* typischer town_product_worth (oft 40–60 %, siehe Model-Hook) zusammenpassen.
|
const PRODUCTION_HEADROOM_DISCOUNT_CAP = 0.14;
|
||||||
*/
|
|
||||||
function productionCostTotal(quantity, category) {
|
function productionPieceCost(certificate, category) {
|
||||||
const c = Math.max(1, Number(category) || 1);
|
const c = Math.max(1, Number(category) || 1);
|
||||||
|
const cert = Math.max(1, Number(certificate) || 1);
|
||||||
|
const raw = PRODUCTION_COST_BASE + (c * PRODUCTION_COST_PER_PRODUCT_CATEGORY);
|
||||||
|
const headroom = Math.max(0, cert - c);
|
||||||
|
const discount = Math.min(
|
||||||
|
headroom * PRODUCTION_HEADROOM_DISCOUNT_PER_STEP,
|
||||||
|
PRODUCTION_HEADROOM_DISCOUNT_CAP
|
||||||
|
);
|
||||||
|
return raw * (1 - discount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function productionCostTotal(quantity, category, certificate) {
|
||||||
const q = Math.min(100, Math.max(1, Number(quantity) || 1));
|
const q = Math.min(100, Math.max(1, Number(quantity) || 1));
|
||||||
const perUnit = 2 * c;
|
return q * productionPieceCost(certificate, category);
|
||||||
return q * perUnit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Regionaler Nachfragewert: sehr niedrige Werte aus der DB sonst unspielbar; Decke nach oben bei 100. */
|
/** Regionaler Nachfragewert: sehr niedrige Werte aus der DB sonst unspielbar; Decke nach oben bei 100. */
|
||||||
@@ -2053,7 +2063,7 @@ class FalukantService extends BaseService {
|
|||||||
}
|
}
|
||||||
if (!p) throw new Error('Product not found');
|
if (!p) throw new Error('Product not found');
|
||||||
quantity = Math.min(100, quantity);
|
quantity = Math.min(100, quantity);
|
||||||
const cost = productionCostTotal(quantity, p.category);
|
const cost = productionCostTotal(quantity, p.category, u.certificate);
|
||||||
if (u.money < cost) throw new Error('notenoughmoney');
|
if (u.money < cost) throw new Error('notenoughmoney');
|
||||||
const r = await updateFalukantUserMoney(u.id, -cost, 'Production cost', u.id);
|
const r = await updateFalukantUserMoney(u.id, -cost, 'Production cost', u.id);
|
||||||
if (!r.success) throw new Error('Failed to update money');
|
if (!r.success) throw new Error('Failed to update money');
|
||||||
|
|||||||
51
backend/sql/rebalance_product_certificates.sql
Normal file
51
backend/sql/rebalance_product_certificates.sql
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE falukant_type.product
|
||||||
|
SET sell_cost = CASE label_tr
|
||||||
|
WHEN 'wheat' THEN 7
|
||||||
|
WHEN 'grain' THEN 7
|
||||||
|
WHEN 'carrot' THEN 4
|
||||||
|
WHEN 'fish' THEN 7
|
||||||
|
WHEN 'meat' THEN 7
|
||||||
|
WHEN 'leather' THEN 7
|
||||||
|
WHEN 'wood' THEN 7
|
||||||
|
WHEN 'stone' THEN 7
|
||||||
|
WHEN 'milk' THEN 5
|
||||||
|
WHEN 'cheese' THEN 5
|
||||||
|
WHEN 'bread' THEN 5
|
||||||
|
WHEN 'beer' THEN 22
|
||||||
|
WHEN 'iron' THEN 24
|
||||||
|
WHEN 'copper' THEN 24
|
||||||
|
WHEN 'spices' THEN 42
|
||||||
|
WHEN 'salt' THEN 24
|
||||||
|
WHEN 'sugar' THEN 24
|
||||||
|
WHEN 'vinegar' THEN 24
|
||||||
|
WHEN 'cotton' THEN 24
|
||||||
|
WHEN 'wine' THEN 24
|
||||||
|
WHEN 'gold' THEN 40
|
||||||
|
WHEN 'diamond' THEN 40
|
||||||
|
WHEN 'furniture' THEN 40
|
||||||
|
WHEN 'clothing' THEN 40
|
||||||
|
WHEN 'jewelry' THEN 58
|
||||||
|
WHEN 'painting' THEN 58
|
||||||
|
WHEN 'book' THEN 58
|
||||||
|
WHEN 'weapon' THEN 58
|
||||||
|
WHEN 'armor' THEN 58
|
||||||
|
WHEN 'shield' THEN 58
|
||||||
|
WHEN 'horse' THEN 78
|
||||||
|
WHEN 'ox' THEN 78
|
||||||
|
ELSE sell_cost
|
||||||
|
END,
|
||||||
|
production_time = CASE label_tr
|
||||||
|
WHEN 'carrot' THEN 2
|
||||||
|
ELSE production_time
|
||||||
|
END
|
||||||
|
WHERE label_tr IN (
|
||||||
|
'wheat', 'grain', 'carrot', 'fish', 'meat', 'leather', 'wood', 'stone',
|
||||||
|
'milk', 'cheese', 'bread', 'beer', 'iron', 'copper', 'spices', 'salt',
|
||||||
|
'sugar', 'vinegar', 'cotton', 'wine', 'gold', 'diamond', 'furniture',
|
||||||
|
'clothing', 'jewelry', 'painting', 'book', 'weapon', 'armor', 'shield',
|
||||||
|
'horse', 'ox'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -250,37 +250,36 @@ async function initializeFalukantProducts() {
|
|||||||
const baseProducts = [
|
const baseProducts = [
|
||||||
{ labelTr: 'wheat', category: 1, productionTime: 2, sellCost: 7 },
|
{ labelTr: 'wheat', category: 1, productionTime: 2, sellCost: 7 },
|
||||||
{ labelTr: 'grain', category: 1, productionTime: 2, sellCost: 7 },
|
{ labelTr: 'grain', category: 1, productionTime: 2, sellCost: 7 },
|
||||||
// Wie Weizen/Getreide (kein Debug-Tempo mehr); Verkaufspreis wie Milch/Brot (Kat. 1, schnelle Ware)
|
{ labelTr: 'carrot', category: 1, productionTime: 2, sellCost: 4 },
|
||||||
{ labelTr: 'carrot', category: 1, productionTime: 2, sellCost: 6 },
|
|
||||||
{ labelTr: 'fish', category: 1, productionTime: 2, sellCost: 7 },
|
{ labelTr: 'fish', category: 1, productionTime: 2, sellCost: 7 },
|
||||||
{ labelTr: 'meat', category: 1, productionTime: 2, sellCost: 7 },
|
{ labelTr: 'meat', category: 1, productionTime: 2, sellCost: 7 },
|
||||||
{ labelTr: 'leather', category: 1, productionTime: 2, sellCost: 7 },
|
{ labelTr: 'leather', category: 1, productionTime: 2, sellCost: 7 },
|
||||||
{ labelTr: 'wood', category: 1, productionTime: 2, sellCost: 7 },
|
{ labelTr: 'wood', category: 1, productionTime: 2, sellCost: 7 },
|
||||||
{ labelTr: 'stone', category: 1, productionTime: 2, sellCost: 7 },
|
{ labelTr: 'stone', category: 1, productionTime: 2, sellCost: 7 },
|
||||||
{ labelTr: 'milk', category: 1, productionTime: 1, sellCost: 6 },
|
{ labelTr: 'milk', category: 1, productionTime: 1, sellCost: 5 },
|
||||||
{ labelTr: 'cheese', category: 1, productionTime: 1, sellCost: 6 },
|
{ labelTr: 'cheese', category: 1, productionTime: 1, sellCost: 5 },
|
||||||
{ labelTr: 'bread', category: 1, productionTime: 1, sellCost: 6 },
|
{ labelTr: 'bread', category: 1, productionTime: 1, sellCost: 5 },
|
||||||
{ labelTr: 'beer', category: 2, productionTime: 3, sellCost: 6 },
|
{ labelTr: 'beer', category: 2, productionTime: 3, sellCost: 22 },
|
||||||
{ labelTr: 'iron', category: 2, productionTime: 4, sellCost: 15 },
|
{ labelTr: 'iron', category: 2, productionTime: 4, sellCost: 24 },
|
||||||
{ labelTr: 'copper', category: 2, productionTime: 4, sellCost: 15 },
|
{ labelTr: 'copper', category: 2, productionTime: 4, sellCost: 24 },
|
||||||
{ labelTr: 'spices', category: 2, productionTime: 8, sellCost: 30 },
|
{ labelTr: 'spices', category: 2, productionTime: 8, sellCost: 42 },
|
||||||
{ labelTr: 'salt', category: 2, productionTime: 4, sellCost: 15 },
|
{ labelTr: 'salt', category: 2, productionTime: 4, sellCost: 24 },
|
||||||
{ labelTr: 'sugar', category: 2, productionTime: 4, sellCost: 15 },
|
{ labelTr: 'sugar', category: 2, productionTime: 4, sellCost: 24 },
|
||||||
{ labelTr: 'vinegar', category: 2, productionTime: 4, sellCost: 15 },
|
{ labelTr: 'vinegar', category: 2, productionTime: 4, sellCost: 24 },
|
||||||
{ labelTr: 'cotton', category: 2, productionTime: 4, sellCost: 15 },
|
{ labelTr: 'cotton', category: 2, productionTime: 4, sellCost: 24 },
|
||||||
{ labelTr: 'wine', category: 2, productionTime: 4, sellCost: 15 },
|
{ labelTr: 'wine', category: 2, productionTime: 4, sellCost: 24 },
|
||||||
{ labelTr: 'gold', category: 3, productionTime: 4, sellCost: 30 },
|
{ labelTr: 'gold', category: 3, productionTime: 4, sellCost: 40 },
|
||||||
{ labelTr: 'diamond', category: 3, productionTime: 4, sellCost: 30 },
|
{ labelTr: 'diamond', category: 3, productionTime: 4, sellCost: 40 },
|
||||||
{ labelTr: 'furniture', category: 3, productionTime: 4, sellCost: 30 },
|
{ labelTr: 'furniture', category: 3, productionTime: 4, sellCost: 40 },
|
||||||
{ labelTr: 'clothing', category: 3, productionTime: 4, sellCost: 30 },
|
{ labelTr: 'clothing', category: 3, productionTime: 4, sellCost: 40 },
|
||||||
{ labelTr: 'jewelry', category: 4, productionTime: 5, sellCost: 60 },
|
{ labelTr: 'jewelry', category: 4, productionTime: 5, sellCost: 58 },
|
||||||
{ labelTr: 'painting', category: 4, productionTime: 5, sellCost: 60 },
|
{ labelTr: 'painting', category: 4, productionTime: 5, sellCost: 58 },
|
||||||
{ labelTr: 'book', category: 4, productionTime: 5, sellCost: 60 },
|
{ labelTr: 'book', category: 4, productionTime: 5, sellCost: 58 },
|
||||||
{ labelTr: 'weapon', category: 4, productionTime: 5, sellCost: 60 },
|
{ labelTr: 'weapon', category: 4, productionTime: 5, sellCost: 58 },
|
||||||
{ labelTr: 'armor', category: 4, productionTime: 5, sellCost: 60 },
|
{ labelTr: 'armor', category: 4, productionTime: 5, sellCost: 58 },
|
||||||
{ labelTr: 'shield', category: 4, productionTime: 5, sellCost: 60 },
|
{ labelTr: 'shield', category: 4, productionTime: 5, sellCost: 58 },
|
||||||
{ labelTr: 'horse', category: 5, productionTime: 5, sellCost: 60 },
|
{ labelTr: 'horse', category: 5, productionTime: 5, sellCost: 78 },
|
||||||
{ labelTr: 'ox', category: 5, productionTime: 5, sellCost: 60 },
|
{ labelTr: 'ox', category: 5, productionTime: 5, sellCost: 78 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const productsToInsert = baseProducts.map(p => ({
|
const productsToInsert = baseProducts.map(p => ({
|
||||||
|
|||||||
@@ -64,11 +64,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { showApiError } from '@/utils/feedback.js';
|
import { showApiError } from '@/utils/feedback.js';
|
||||||
|
|
||||||
|
const PRODUCTION_COST_BASE = 6.0;
|
||||||
|
const PRODUCTION_COST_PER_PRODUCT_CATEGORY = 1.0;
|
||||||
|
const PRODUCTION_HEADROOM_DISCOUNT_PER_STEP = 0.035;
|
||||||
|
const PRODUCTION_HEADROOM_DISCOUNT_CAP = 0.14;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ProductionSection",
|
name: "ProductionSection",
|
||||||
props: {
|
props: {
|
||||||
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 },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -125,7 +132,7 @@
|
|||||||
calculateProductionCost() {
|
calculateProductionCost() {
|
||||||
if (!this.products) return 0;
|
if (!this.products) return 0;
|
||||||
const product = this.products.find(p => p.id === this.selectedProduct);
|
const product = this.products.find(p => p.id === this.selectedProduct);
|
||||||
return product ? 6 * product.category * this.productionQuantity : 0;
|
return product ? this.calculateProductionPieceCost(product.category) * this.productionQuantity : 0;
|
||||||
},
|
},
|
||||||
calculateProductionDuration(productId) {
|
calculateProductionDuration(productId) {
|
||||||
if (!this.products || !productId) return 0;
|
if (!this.products || !productId) return 0;
|
||||||
@@ -152,6 +159,17 @@
|
|||||||
const secondsLeft = Math.max(Math.floor((end - this.currentTime) / 1000), 0);
|
const secondsLeft = Math.max(Math.floor((end - this.currentTime) / 1000), 0);
|
||||||
return secondsLeft;
|
return secondsLeft;
|
||||||
},
|
},
|
||||||
|
calculateProductionPieceCost(productCategory) {
|
||||||
|
const category = Math.max(1, Number(productCategory) || 1);
|
||||||
|
const certificate = Math.max(1, Number(this.currentCertificate) || 1);
|
||||||
|
const raw = PRODUCTION_COST_BASE + (category * PRODUCTION_COST_PER_PRODUCT_CATEGORY);
|
||||||
|
const headroom = Math.max(0, certificate - category);
|
||||||
|
const discount = Math.min(
|
||||||
|
headroom * PRODUCTION_HEADROOM_DISCOUNT_PER_STEP,
|
||||||
|
PRODUCTION_HEADROOM_DISCOUNT_CAP
|
||||||
|
);
|
||||||
|
return raw * (1 - discount);
|
||||||
|
},
|
||||||
async startProduction() {
|
async startProduction() {
|
||||||
if (this.selectedProduct && this.productionQuantity > 0) {
|
if (this.selectedProduct && this.productionQuantity > 0) {
|
||||||
this.productionQuantity = Math.min(this.productionQuantity, 200);
|
this.productionQuantity = Math.min(this.productionQuantity, 200);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="product in products" :key="product.id" :class="{ highlight: product.id === productWithMaxRevenuePerMinute?.id }">
|
<tr v-for="product in products" :key="product.id" :class="{ highlight: product.id === productWithMaxProfitPerMinute?.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>
|
||||||
@@ -64,12 +64,12 @@
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
productWithMaxRevenuePerMinute() {
|
productWithMaxProfitPerMinute() {
|
||||||
if (!this.products || this.products.length === 0) return null;
|
if (!this.products || this.products.length === 0) return null;
|
||||||
return this.products.reduce((maxProduct, currentProduct) => {
|
return this.products.reduce((maxProduct, currentProduct) => {
|
||||||
const currentRevenue = parseFloat(this.calculateProductRevenue(currentProduct).perMinute);
|
const currentProfit = parseFloat(this.calculateProductProfit(currentProduct).perMinute);
|
||||||
const maxRevenue = maxProduct ? parseFloat(this.calculateProductRevenue(maxProduct).perMinute) : 0;
|
const maxProfit = maxProduct ? parseFloat(this.calculateProductProfit(maxProduct).perMinute) : Number.NEGATIVE_INFINITY;
|
||||||
return currentRevenue > maxRevenue ? currentProduct : maxProduct;
|
return currentProfit > maxProfit ? currentProduct : maxProduct;
|
||||||
}, null);
|
}, null);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -223,4 +223,4 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,8 @@
|
|||||||
"description": "Zeigt die aktuelle Stufe und die Bedingungen für den nächsten Aufstieg.",
|
"description": "Zeigt die aktuelle Stufe und die Bedingungen für den nächsten Aufstieg.",
|
||||||
"current": "Aktuell",
|
"current": "Aktuell",
|
||||||
"next": "Nächste Stufe",
|
"next": "Nächste Stufe",
|
||||||
|
"levelMatrix": "Produkte nach Zertifikatsstufe",
|
||||||
|
"levelLabel": "Stufe {level}",
|
||||||
"score": "Wertung",
|
"score": "Wertung",
|
||||||
"ready": "Für den nächsten Aufstieg bereit",
|
"ready": "Für den nächsten Aufstieg bereit",
|
||||||
"notReady": "Bedingungen noch nicht erfüllt",
|
"notReady": "Bedingungen noch nicht erfüllt",
|
||||||
@@ -259,6 +261,12 @@
|
|||||||
"selectionBlocked": "Neue Ausbauten sind im Schuldturm gesperrt."
|
"selectionBlocked": "Neue Ausbauten sind im Schuldturm gesperrt."
|
||||||
},
|
},
|
||||||
"currentCertificate": "Derzeitiges Zertifikat",
|
"currentCertificate": "Derzeitiges Zertifikat",
|
||||||
|
"certificate": {
|
||||||
|
"title": "Produktfreigaben",
|
||||||
|
"description": "Zeigt, welche Produkte dein derzeitiges Zertifikat freischaltet und was mit der nächsten Stufe dazukommt.",
|
||||||
|
"currentUnlocks": "Derzeit freigeschaltet",
|
||||||
|
"nextUnlocks": "Neu mit Stufe {level}"
|
||||||
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"director": "Direktor",
|
"director": "Direktor",
|
||||||
"inventory": "Inventar",
|
"inventory": "Inventar",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
"description": "Shows your current level and the requirements for the next promotion.",
|
"description": "Shows your current level and the requirements for the next promotion.",
|
||||||
"current": "Current",
|
"current": "Current",
|
||||||
"next": "Next level",
|
"next": "Next level",
|
||||||
|
"levelMatrix": "Products by certificate level",
|
||||||
|
"levelLabel": "Level {level}",
|
||||||
"score": "Score",
|
"score": "Score",
|
||||||
"ready": "Ready for the next promotion",
|
"ready": "Ready for the next promotion",
|
||||||
"notReady": "Requirements not met yet",
|
"notReady": "Requirements not met yet",
|
||||||
@@ -313,6 +315,12 @@
|
|||||||
"selectionBlocked": "New expansions are blocked while imprisoned for debt."
|
"selectionBlocked": "New expansions are blocked while imprisoned for debt."
|
||||||
},
|
},
|
||||||
"currentCertificate": "Current certificate",
|
"currentCertificate": "Current certificate",
|
||||||
|
"certificate": {
|
||||||
|
"title": "Product unlocks",
|
||||||
|
"description": "Shows which products your current certificate unlocks and what the next level adds.",
|
||||||
|
"currentUnlocks": "Currently unlocked",
|
||||||
|
"nextUnlocks": "New with level {level}"
|
||||||
|
},
|
||||||
"selection": {
|
"selection": {
|
||||||
"title": "Branch Selection",
|
"title": "Branch Selection",
|
||||||
"selected": "Selected Branch",
|
"selected": "Selected Branch",
|
||||||
|
|||||||
@@ -133,6 +133,8 @@
|
|||||||
"description": "Muestra tu nivel actual y las condiciones para el siguiente ascenso.",
|
"description": "Muestra tu nivel actual y las condiciones para el siguiente ascenso.",
|
||||||
"current": "Actual",
|
"current": "Actual",
|
||||||
"next": "Siguiente nivel",
|
"next": "Siguiente nivel",
|
||||||
|
"levelMatrix": "Productos por nivel de certificado",
|
||||||
|
"levelLabel": "Nivel {level}",
|
||||||
"score": "Puntuación",
|
"score": "Puntuación",
|
||||||
"ready": "Listo para el siguiente ascenso",
|
"ready": "Listo para el siguiente ascenso",
|
||||||
"notReady": "Condiciones aún no cumplidas",
|
"notReady": "Condiciones aún no cumplidas",
|
||||||
@@ -247,6 +249,12 @@
|
|||||||
"selectionBlocked": "Las nuevas ampliaciones están bloqueadas en la prisión por deudas."
|
"selectionBlocked": "Las nuevas ampliaciones están bloqueadas en la prisión por deudas."
|
||||||
},
|
},
|
||||||
"currentCertificate": "Certificado actual",
|
"currentCertificate": "Certificado actual",
|
||||||
|
"certificate": {
|
||||||
|
"title": "Desbloqueos de productos",
|
||||||
|
"description": "Muestra qué productos habilita tu certificado actual y qué añade el siguiente nivel.",
|
||||||
|
"currentUnlocks": "Actualmente desbloqueado",
|
||||||
|
"nextUnlocks": "Nuevo con el nivel {level}"
|
||||||
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"director": "Director",
|
"director": "Director",
|
||||||
"inventory": "Inventario",
|
"inventory": "Inventario",
|
||||||
|
|||||||
@@ -15,6 +15,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="branch-certificate surface-card">
|
||||||
|
<div class="branch-certificate__header">
|
||||||
|
<div>
|
||||||
|
<h3>{{ $t('falukant.branch.certificate.title') }}</h3>
|
||||||
|
<p>{{ $t('falukant.branch.certificate.description') }}</p>
|
||||||
|
</div>
|
||||||
|
<span class="branch-hero__badge">
|
||||||
|
{{ $t('falukant.branch.currentCertificate') }}: {{ currentCertificate ?? 1 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="branch-certificate__grid">
|
||||||
|
<article class="branch-certificate__block">
|
||||||
|
<h4>{{ $t('falukant.branch.certificate.currentUnlocks') }}</h4>
|
||||||
|
<p>{{ currentCertificateProducts.join(', ') }}</p>
|
||||||
|
</article>
|
||||||
|
<article v-if="nextCertificateProducts.length" class="branch-certificate__block">
|
||||||
|
<h4>{{ $t('falukant.branch.certificate.nextUnlocks', { level: nextCertificateLevel }) }}</h4>
|
||||||
|
<p>{{ nextCertificateProducts.join(', ') }}</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
v-if="debtorsPrison.active"
|
v-if="debtorsPrison.active"
|
||||||
class="branch-debt-warning surface-card"
|
class="branch-debt-warning surface-card"
|
||||||
@@ -84,6 +106,7 @@
|
|||||||
<ProductionSection
|
<ProductionSection
|
||||||
:branchId="selectedBranch.id"
|
:branchId="selectedBranch.id"
|
||||||
:products="products"
|
:products="products"
|
||||||
|
:current-certificate="currentCertificate"
|
||||||
ref="productionSection"
|
ref="productionSection"
|
||||||
/>
|
/>
|
||||||
<!-- Tax summary for production -->
|
<!-- Tax summary for production -->
|
||||||
@@ -363,6 +386,19 @@ import apiClient from '@/utils/axios.js';
|
|||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
|
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
|
||||||
|
|
||||||
|
const CERTIFICATE_PRODUCT_LEVELS = [
|
||||||
|
{ level: 1, products: ['fish', 'meat', 'leather', 'wood', 'stone', 'milk', 'cheese', 'bread', 'wheat', 'grain', 'carrot'] },
|
||||||
|
{ level: 2, products: ['beer', 'iron', 'copper', 'spices', 'salt', 'sugar', 'vinegar', 'cotton', 'wine'] },
|
||||||
|
{ level: 3, products: ['gold', 'diamond', 'furniture', 'clothing'] },
|
||||||
|
{ level: 4, products: ['jewelry', 'painting', 'book', 'weapon', 'armor', 'shield'] },
|
||||||
|
{ level: 5, products: ['horse', 'ox'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRODUCTION_COST_BASE = 6.0;
|
||||||
|
const PRODUCTION_COST_PER_PRODUCT_CATEGORY = 1.0;
|
||||||
|
const PRODUCTION_HEADROOM_DISCOUNT_PER_STEP = 0.035;
|
||||||
|
const PRODUCTION_HEADROOM_DISCOUNT_CAP = 0.14;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "BranchView",
|
name: "BranchView",
|
||||||
components: {
|
components: {
|
||||||
@@ -468,6 +504,23 @@ export default {
|
|||||||
// 10% Rabatt für Reparatur aller Fahrzeuge
|
// 10% Rabatt für Reparatur aller Fahrzeuge
|
||||||
return Math.round(totalCost * 0.9);
|
return Math.round(totalCost * 0.9);
|
||||||
},
|
},
|
||||||
|
currentCertificateProducts() {
|
||||||
|
const certificate = Number(this.currentCertificate || 1);
|
||||||
|
return CERTIFICATE_PRODUCT_LEVELS
|
||||||
|
.filter((entry) => entry.level <= certificate)
|
||||||
|
.flatMap((entry) => entry.products)
|
||||||
|
.map((productKey) => this.$t(`falukant.product.${productKey}`));
|
||||||
|
},
|
||||||
|
nextCertificateLevel() {
|
||||||
|
const current = Number(this.currentCertificate || 1);
|
||||||
|
return current < CERTIFICATE_PRODUCT_LEVELS.length ? current + 1 : null;
|
||||||
|
},
|
||||||
|
nextCertificateProducts() {
|
||||||
|
const nextEntry = CERTIFICATE_PRODUCT_LEVELS.find((entry) => entry.level === this.nextCertificateLevel);
|
||||||
|
return nextEntry
|
||||||
|
? nextEntry.products.map((productKey) => this.$t(`falukant.product.${productKey}`))
|
||||||
|
: [];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@@ -540,6 +593,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
calculateProductionPieceCost(productCategory) {
|
||||||
|
const category = Math.max(1, Number(productCategory) || 1);
|
||||||
|
const certificate = Math.max(1, Number(this.currentCertificate) || 1);
|
||||||
|
const raw = PRODUCTION_COST_BASE + (category * PRODUCTION_COST_PER_PRODUCT_CATEGORY);
|
||||||
|
const headroom = Math.max(0, certificate - category);
|
||||||
|
const discount = Math.min(
|
||||||
|
headroom * PRODUCTION_HEADROOM_DISCOUNT_PER_STEP,
|
||||||
|
PRODUCTION_HEADROOM_DISCOUNT_CAP
|
||||||
|
);
|
||||||
|
return raw * (1 - discount);
|
||||||
|
},
|
||||||
matchesCurrentUser(eventData) {
|
matchesCurrentUser(eventData) {
|
||||||
if (eventData?.user_id == null) {
|
if (eventData?.user_id == null) {
|
||||||
return true;
|
return true;
|
||||||
@@ -798,7 +862,7 @@ export default {
|
|||||||
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr }
|
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr }
|
||||||
= this.calculateProductRevenue(product);
|
= this.calculateProductRevenue(product);
|
||||||
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
|
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
|
||||||
const costPerUnit = 6 * product.category;
|
const costPerUnit = this.calculateProductionPieceCost(product.category);
|
||||||
const profitAbsolute = revenueAbsolute - costPerUnit;
|
const profitAbsolute = revenueAbsolute - costPerUnit;
|
||||||
const costPerMinute = product.productionTime > 0
|
const costPerMinute = product.productionTime > 0
|
||||||
? costPerUnit / product.productionTime
|
? costPerUnit / product.productionTime
|
||||||
@@ -1267,6 +1331,45 @@ export default {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.branch-certificate {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-certificate__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-certificate__header p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-certificate__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-certificate__block {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: rgba(138, 84, 17, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-certificate__block h4 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-certificate__block p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.branch-tab-content {
|
.branch-tab-content {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
|
|||||||
@@ -187,6 +187,21 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article class="certificate-panel__block">
|
||||||
|
<h4>{{ $t('falukant.overview.certificate.levelMatrix') }}</h4>
|
||||||
|
<div class="certificate-level-list">
|
||||||
|
<div
|
||||||
|
v-for="entry in certificateLevelMatrix"
|
||||||
|
:key="entry.level"
|
||||||
|
class="certificate-level-list__item"
|
||||||
|
:class="{ 'is-current': entry.level === (falukantUser?.certificate ?? 1) }"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('falukant.overview.certificate.levelLabel', { level: entry.level }) }}</strong>
|
||||||
|
<span>{{ entry.products.join(', ') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -338,6 +353,14 @@ const AVATAR_POSITIONS = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CERTIFICATE_PRODUCT_LEVELS = [
|
||||||
|
{ level: 1, products: ['fish', 'meat', 'leather', 'wood', 'stone', 'milk', 'cheese', 'bread', 'wheat', 'grain', 'carrot'] },
|
||||||
|
{ level: 2, products: ['beer', 'iron', 'copper', 'spices', 'salt', 'sugar', 'vinegar', 'cotton', 'wine'] },
|
||||||
|
{ level: 3, products: ['gold', 'diamond', 'furniture', 'clothing'] },
|
||||||
|
{ level: 4, products: ['jewelry', 'painting', 'book', 'weapon', 'armor', 'shield'] },
|
||||||
|
{ level: 5, products: ['horse', 'ox'] },
|
||||||
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FalukantOverviewView',
|
name: 'FalukantOverviewView',
|
||||||
components: {
|
components: {
|
||||||
@@ -409,6 +432,12 @@ export default {
|
|||||||
certificateProgress() {
|
certificateProgress() {
|
||||||
return this.falukantUser?.certificateProgress || null;
|
return this.falukantUser?.certificateProgress || null;
|
||||||
},
|
},
|
||||||
|
certificateLevelMatrix() {
|
||||||
|
return CERTIFICATE_PRODUCT_LEVELS.map((entry) => ({
|
||||||
|
level: entry.level,
|
||||||
|
products: entry.products.map((productKey) => this.$t(`falukant.product.${productKey}`)),
|
||||||
|
}));
|
||||||
|
},
|
||||||
routineActions() {
|
routineActions() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -851,6 +880,32 @@ export default {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.certificate-level-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-level-list__item {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgba(138, 84, 17, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-level-list__item strong {
|
||||||
|
color: #7a4b12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-level-list__item span {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-level-list__item.is-current {
|
||||||
|
background: rgba(68, 138, 86, 0.12);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(68, 138, 86, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
.summary-card,
|
.summary-card,
|
||||||
.routine-card {
|
.routine-card {
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
|
|||||||
Reference in New Issue
Block a user