feat(certificate): add score breakdown and recommendations for certificate progression
This commit is contained in:
@@ -282,6 +282,22 @@ function getScoreThresholdForCertificate(level) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCertificateScoreComponent(key, current, points, weight, steps) {
|
||||||
|
const next = steps.find((step) => step.points > points) || null;
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
current,
|
||||||
|
points,
|
||||||
|
maxPoints: 5,
|
||||||
|
weight,
|
||||||
|
contribution: Number((points * weight).toFixed(2)),
|
||||||
|
nextValue: next?.value ?? null,
|
||||||
|
nextPoints: next?.points ?? null,
|
||||||
|
gain: next ? Number(((next.points - points) * weight).toFixed(2)) : 0,
|
||||||
|
maxed: !next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
|
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
|
||||||
// Wenn worthPercent nicht übergeben wurde, hole es aus der Datenbank
|
// Wenn worthPercent nicht übergeben wurde, hole es aus der Datenbank
|
||||||
if (worthPercent === null) {
|
if (worthPercent === null) {
|
||||||
@@ -3122,6 +3138,36 @@ class FalukantService extends BaseService {
|
|||||||
const nextCertificate = Math.min(5, currentCertificate + 1);
|
const nextCertificate = Math.min(5, currentCertificate + 1);
|
||||||
const nextThreshold = CERTIFICATE_THRESHOLDS[nextCertificate] || null;
|
const nextThreshold = CERTIFICATE_THRESHOLDS[nextCertificate] || null;
|
||||||
const nextScoreThreshold = getScoreThresholdForCertificate(nextCertificate);
|
const nextScoreThreshold = getScoreThresholdForCertificate(nextCertificate);
|
||||||
|
const scoreBreakdown = [
|
||||||
|
buildCertificateScoreComponent('avgKnowledge', avgKnowledge, knowledgePoints, 0.45, [
|
||||||
|
{ value: 20, points: 1 }, { value: 35, points: 2 }, { value: 50, points: 3 },
|
||||||
|
{ value: 65, points: 4 }, { value: 80, points: 5 },
|
||||||
|
]),
|
||||||
|
buildCertificateScoreComponent('completedProductions', completedProductions, productionPoints, 0.30, [
|
||||||
|
{ value: 5, points: 1 }, { value: 20, points: 2 }, { value: 50, points: 3 },
|
||||||
|
{ value: 100, points: 4 }, { value: 200, points: 5 },
|
||||||
|
]),
|
||||||
|
buildCertificateScoreComponent('officePoints', highestOfficeRank, officePoints, 0.08, [
|
||||||
|
{ value: 1, points: 1 }, { value: 2, points: 2 }, { value: 3, points: 3 },
|
||||||
|
{ value: 4, points: 4 }, { value: 5, points: 5 },
|
||||||
|
]),
|
||||||
|
buildCertificateScoreComponent('reputationPoints', reputation, reputationPoints, 0.07, [
|
||||||
|
{ value: 20, points: 1 }, { value: 40, points: 2 }, { value: 60, points: 3 },
|
||||||
|
{ value: 75, points: 4 }, { value: 90, points: 5 },
|
||||||
|
]),
|
||||||
|
buildCertificateScoreComponent('housePoints', housePosition, housePoints, 0.05, [
|
||||||
|
{ value: 2, points: 1 }, { value: 4, points: 2 }, { value: 6, points: 3 },
|
||||||
|
{ value: 8, points: 4 }, { value: 10, points: 5 },
|
||||||
|
]),
|
||||||
|
buildCertificateScoreComponent('nobilityPoints', nobilityLevel, nobilityPoints, 0.05, [
|
||||||
|
{ value: 2, points: 1 }, { value: 3, points: 2 }, { value: 4, points: 3 },
|
||||||
|
{ value: 5, points: 4 }, { value: 6, points: 5 },
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
const scoreGap = Math.max(0, Number((nextScoreThreshold - score).toFixed(2)));
|
||||||
|
const scoreRecommendations = scoreBreakdown
|
||||||
|
.filter((component) => !component.maxed && component.gain > 0)
|
||||||
|
.sort((a, b) => b.gain - a.gain);
|
||||||
|
|
||||||
const currentValues = {
|
const currentValues = {
|
||||||
avgKnowledge,
|
avgKnowledge,
|
||||||
@@ -3194,6 +3240,9 @@ class FalukantService extends BaseService {
|
|||||||
score: Number(score.toFixed(2)),
|
score: Number(score.toFixed(2)),
|
||||||
targetCertificate,
|
targetCertificate,
|
||||||
nextScoreThreshold,
|
nextScoreThreshold,
|
||||||
|
scoreGap,
|
||||||
|
scoreBreakdown,
|
||||||
|
scoreRecommendations,
|
||||||
currentValues,
|
currentValues,
|
||||||
nextRequirements,
|
nextRequirements,
|
||||||
statusRequirement,
|
statusRequirement,
|
||||||
|
|||||||
@@ -196,6 +196,12 @@
|
|||||||
"notReady": "Für Stufe {next} müssen sowohl die Mindestanforderungen als auch die Wertungsgrenze von {threshold} erfüllt sein."
|
"notReady": "Für Stufe {next} müssen sowohl die Mindestanforderungen als auch die Wertungsgrenze von {threshold} erfüllt sein."
|
||||||
},
|
},
|
||||||
"scoreHowToRaise": "Die Wertungsgrenze steigt über gewichtete Punkte: Wissen (45%), abgeschlossene Produktionen (30%), Amtsstatus (8%), Ansehensstatus (7%), Hausstatus (5%) und Adelsstatus (5%). Für schnelle Fortschritte sind Wissen + viele abgeschlossene Produktionen die stärksten Hebel.",
|
"scoreHowToRaise": "Die Wertungsgrenze steigt über gewichtete Punkte: Wissen (45%), abgeschlossene Produktionen (30%), Amtsstatus (8%), Ansehensstatus (7%), Hausstatus (5%) und Adelsstatus (5%). Für schnelle Fortschritte sind Wissen + viele abgeschlossene Produktionen die stärksten Hebel.",
|
||||||
|
"scoreBreakdown": {
|
||||||
|
"title": "So erreichst du die nächste Wertung",
|
||||||
|
"gap": "Es fehlen noch {gap} Wertungspunkte bis {threshold}.",
|
||||||
|
"nextStep": "Nächste Stufe: von {current} auf {next} erhöhen (+{gain} Wertung).",
|
||||||
|
"maxed": "Bereits ausgeschöpft – weitere Steigerungen erhöhen die Wertung nicht."
|
||||||
|
},
|
||||||
"factors": "Aktuelle Werte",
|
"factors": "Aktuelle Werte",
|
||||||
"requirements": "Bedingungen für die nächste Stufe",
|
"requirements": "Bedingungen für die nächste Stufe",
|
||||||
"productionsSince": "Nur Produktionen ab {date} zählen (seit letztem Aufstieg, Bankrott oder Erbfolge).",
|
"productionsSince": "Nur Produktionen ab {date} zählen (seit letztem Aufstieg, Bankrott oder Erbfolge).",
|
||||||
|
|||||||
@@ -195,6 +195,12 @@
|
|||||||
"notReady": "Pour le niveau {next}, les exigences minimales et la limite de notation de {threshold} doivent être remplies."
|
"notReady": "Pour le niveau {next}, les exigences minimales et la limite de notation de {threshold} doivent être remplies."
|
||||||
},
|
},
|
||||||
"scoreHowToRaise": "Le seuil de note monte via des points pondérés : connaissance (45%), productions terminées (30%), statut d'office (8%), statut de réputation (7%), statut de maison (5%) et statut de noblesse (5%). Pour progresser vite, concentrez-vous sur la connaissance et de nombreuses productions terminées.",
|
"scoreHowToRaise": "Le seuil de note monte via des points pondérés : connaissance (45%), productions terminées (30%), statut d'office (8%), statut de réputation (7%), statut de maison (5%) et statut de noblesse (5%). Pour progresser vite, concentrez-vous sur la connaissance et de nombreuses productions terminées.",
|
||||||
|
"scoreBreakdown": {
|
||||||
|
"title": "Comment atteindre la prochaine note",
|
||||||
|
"gap": "Il manque encore {gap} point(s) de note pour atteindre {threshold}.",
|
||||||
|
"nextStep": "Prochaine étape : passer de {current} à {next} (+{gain} de note).",
|
||||||
|
"maxed": "Déjà au maximum : d'autres progrès n'augmentent pas la note."
|
||||||
|
},
|
||||||
"factors": "Valeurs actuelles",
|
"factors": "Valeurs actuelles",
|
||||||
"requirements": "Conditions pour la prochaine étape",
|
"requirements": "Conditions pour la prochaine étape",
|
||||||
"productionsSince": "Seules les productions à partir du {date} comptent (depuis la dernière promotion, faillite ou succession).",
|
"productionsSince": "Seules les productions à partir du {date} comptent (depuis la dernière promotion, faillite ou succession).",
|
||||||
|
|||||||
@@ -123,6 +123,32 @@
|
|||||||
{{ $t('falukant.overview.certificate.scoreHowToRaise') }}
|
{{ $t('falukant.overview.certificate.scoreHowToRaise') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<article v-if="certificateProgress.scoreBreakdown?.length" class="certificate-panel__block certificate-panel__breakdown">
|
||||||
|
<h4>{{ $t('falukant.overview.certificate.scoreBreakdown.title') }}</h4>
|
||||||
|
<p>
|
||||||
|
{{ $t('falukant.overview.certificate.scoreBreakdown.gap', {
|
||||||
|
gap: formatCertificateValue(certificateProgress.scoreGap, 2),
|
||||||
|
threshold: formatCertificateValue(certificateProgress.nextScoreThreshold, 1),
|
||||||
|
}) }}
|
||||||
|
</p>
|
||||||
|
<div class="certificate-score-components">
|
||||||
|
<div v-for="component in certificateProgress.scoreBreakdown" :key="component.key" class="certificate-score-components__item">
|
||||||
|
<div>
|
||||||
|
<span>{{ certificateRequirementLabel(component.key) }}</span>
|
||||||
|
<strong>{{ component.points }}/{{ component.maxPoints }} · {{ formatCertificateValue(component.contribution, 2) }}</strong>
|
||||||
|
</div>
|
||||||
|
<p v-if="!component.maxed">
|
||||||
|
{{ $t('falukant.overview.certificate.scoreBreakdown.nextStep', {
|
||||||
|
current: formatCertificateValue(component.current, 1),
|
||||||
|
next: formatCertificateValue(component.nextValue, 1),
|
||||||
|
gain: formatCertificateValue(component.gain, 2),
|
||||||
|
}) }}
|
||||||
|
</p>
|
||||||
|
<p v-else>{{ $t('falukant.overview.certificate.scoreBreakdown.maxed') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
<div class="certificate-panel__grid">
|
<div class="certificate-panel__grid">
|
||||||
<article class="certificate-panel__block">
|
<article class="certificate-panel__block">
|
||||||
<h4>{{ $t('falukant.overview.certificate.factors') }}</h4>
|
<h4>{{ $t('falukant.overview.certificate.factors') }}</h4>
|
||||||
@@ -1033,6 +1059,40 @@ export default {
|
|||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.certificate-panel__breakdown {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-panel__breakdown > p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-score-components {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-score-components__item {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid rgba(123, 93, 49, 0.16);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 252, 245, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-score-components__item > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-score-components__item p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
.certificate-panel__productions-since {
|
.certificate-panel__productions-since {
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user