Implement certificate progress feature in FalukantService and frontend: Add methods to calculate and retrieve certificate progress based on user attributes. Update localization files for English, German, and Spanish to include new terms related to certificate progress. Enhance OverviewView to display certificate details and requirements, improving user experience and clarity.
This commit is contained in:
@@ -14,6 +14,7 @@ import TitleBenefit from '../models/falukant/type/title_benefit.js';
|
||||
import Branch from '../models/falukant/data/branch.js';
|
||||
import BranchType from '../models/falukant/type/branch.js';
|
||||
import Production from '../models/falukant/data/production.js';
|
||||
import DayProduction from '../models/falukant/log/dayproduction.js';
|
||||
import ProductType from '../models/falukant/type/product.js';
|
||||
import Knowledge from '../models/falukant/data/product_knowledge.js';
|
||||
import Inventory from '../models/falukant/data/inventory.js';
|
||||
@@ -138,6 +139,83 @@ const POLITICAL_OFFICE_RANKS = {
|
||||
chancellor: 6
|
||||
};
|
||||
|
||||
const CERTIFICATE_THRESHOLDS = {
|
||||
2: { avgKnowledge: 15, completedProductions: 4, statusMode: 'none', statusRequiredCount: 0 },
|
||||
3: { avgKnowledge: 28, completedProductions: 15, statusMode: 'none', statusRequiredCount: 0 },
|
||||
4: {
|
||||
avgKnowledge: 45,
|
||||
completedProductions: 45,
|
||||
statusMode: 'one_of',
|
||||
statusRequiredCount: 1,
|
||||
options: [
|
||||
{ type: 'officePoints', required: 1 },
|
||||
{ type: 'nobilityPoints', required: 1 },
|
||||
{ type: 'reputationPoints', required: 2 },
|
||||
{ type: 'housePoints', required: 2 },
|
||||
],
|
||||
},
|
||||
5: {
|
||||
avgKnowledge: 60,
|
||||
completedProductions: 110,
|
||||
reputationPoints: 2,
|
||||
statusMode: 'two_of',
|
||||
statusRequiredCount: 2,
|
||||
options: [
|
||||
{ type: 'officePoints', required: 2 },
|
||||
{ type: 'nobilityPoints', required: 1 },
|
||||
{ type: 'housePoints', required: 2 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function getKnowledgePoints(avgKnowledge) {
|
||||
if (avgKnowledge >= 80) return 5;
|
||||
if (avgKnowledge >= 65) return 4;
|
||||
if (avgKnowledge >= 50) return 3;
|
||||
if (avgKnowledge >= 35) return 2;
|
||||
if (avgKnowledge >= 20) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getProductionPoints(completedProductions) {
|
||||
if (completedProductions >= 200) return 5;
|
||||
if (completedProductions >= 100) return 4;
|
||||
if (completedProductions >= 50) return 3;
|
||||
if (completedProductions >= 20) return 2;
|
||||
if (completedProductions >= 5) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getReputationPoints(reputation) {
|
||||
if (reputation >= 90) return 5;
|
||||
if (reputation >= 75) return 4;
|
||||
if (reputation >= 60) return 3;
|
||||
if (reputation >= 40) return 2;
|
||||
if (reputation >= 20) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getHousePoints(housePosition) {
|
||||
if (housePosition >= 10) return 5;
|
||||
if (housePosition >= 8) return 4;
|
||||
if (housePosition >= 6) return 3;
|
||||
if (housePosition >= 4) return 2;
|
||||
if (housePosition >= 2) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getNobilityPoints(nobilityLevel) {
|
||||
return Math.max(0, Math.min(5, (nobilityLevel || 0) - 1));
|
||||
}
|
||||
|
||||
function getTargetCertificateByScore(score) {
|
||||
if (score >= 3.8) return 5;
|
||||
if (score >= 2.8) return 4;
|
||||
if (score >= 1.8) return 3;
|
||||
if (score >= 0.9) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
|
||||
// Wenn worthPercent nicht übergeben wurde, hole es aus der Datenbank
|
||||
if (worthPercent === null) {
|
||||
@@ -823,9 +901,9 @@ class FalukantService extends BaseService {
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
|
||||
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }
|
||||
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'level'] }
|
||||
],
|
||||
attributes: ['birthdate', 'gender']
|
||||
attributes: ['id', 'birthdate', 'gender', 'reputation', 'titleOfNobility']
|
||||
},
|
||||
{
|
||||
model: RegionData,
|
||||
@@ -870,6 +948,7 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
if (userHouse) u.setDataValue('userHouse', userHouse);
|
||||
if (u.character?.birthdate) u.character.setDataValue('age', calcAge(u.character.birthdate));
|
||||
u.setDataValue('certificateProgress', await this.buildCertificateProgress(u));
|
||||
u.setDataValue('debtorsPrison', await this.getDebtorsPrisonStateForUser(u));
|
||||
return u;
|
||||
}
|
||||
@@ -2795,6 +2874,151 @@ class FalukantService extends BaseService {
|
||||
return parseFloat(averageKnowledge[0]?.avgKnowledge || 0);
|
||||
}
|
||||
|
||||
async getHighestChurchOfficeInfo(userId) {
|
||||
const character = await FalukantCharacter.findOne({
|
||||
where: { userId },
|
||||
attributes: ['id']
|
||||
});
|
||||
if (!character) return { rank: 0, name: null };
|
||||
|
||||
const churchOffices = await ChurchOffice.findAll({
|
||||
where: { characterId: character.id },
|
||||
include: [{ model: ChurchOfficeType, as: 'type', attributes: ['name', 'hierarchyLevel'] }],
|
||||
attributes: ['officeTypeId']
|
||||
});
|
||||
|
||||
const candidates = churchOffices
|
||||
.map((office) => ({
|
||||
rank: Number(office.type?.hierarchyLevel || 0),
|
||||
name: office.type?.name || null,
|
||||
}))
|
||||
.sort((a, b) => b.rank - a.rank);
|
||||
|
||||
return candidates[0] || { rank: 0, name: null };
|
||||
}
|
||||
|
||||
async buildCertificateProgress(user) {
|
||||
const character = user?.character || await FalukantCharacter.findOne({
|
||||
where: { userId: user.id },
|
||||
attributes: ['id', 'reputation', 'titleOfNobility']
|
||||
});
|
||||
if (!character?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [avgKnowledge, completedProductions, highestPoliticalOffice, highestChurchOffice, house, title] = await Promise.all([
|
||||
this.calculateAverageKnowledge(character.id),
|
||||
DayProduction.count({ where: { producerId: user.id } }),
|
||||
this.getHighestPoliticalOfficeInfo(user.id),
|
||||
this.getHighestChurchOfficeInfo(user.id),
|
||||
UserHouse.findOne({
|
||||
where: { userId: user.id },
|
||||
include: [{ model: HouseType, as: 'houseType', attributes: ['position', 'labelTr'] }],
|
||||
attributes: ['houseTypeId']
|
||||
}),
|
||||
TitleOfNobility.findOne({
|
||||
where: { id: character.titleOfNobility },
|
||||
attributes: ['level', 'labelTr']
|
||||
}),
|
||||
]);
|
||||
|
||||
const reputation = Number(character.reputation || 0);
|
||||
const housePosition = Number(house?.houseType?.position || 0);
|
||||
const highestPoliticalOfficeRank = Number(highestPoliticalOffice?.rank || 0);
|
||||
const highestChurchOfficeRank = Number(highestChurchOffice?.rank || 0);
|
||||
const highestOfficeRank = Math.max(highestPoliticalOfficeRank, highestChurchOfficeRank);
|
||||
const nobilityLevel = Number(title?.level || 0);
|
||||
|
||||
const knowledgePoints = getKnowledgePoints(avgKnowledge);
|
||||
const productionPoints = getProductionPoints(completedProductions);
|
||||
const officePoints = Math.min(5, highestOfficeRank);
|
||||
const nobilityPoints = getNobilityPoints(nobilityLevel);
|
||||
const reputationPoints = getReputationPoints(reputation);
|
||||
const housePoints = getHousePoints(housePosition);
|
||||
|
||||
const score = (
|
||||
knowledgePoints * 0.45 +
|
||||
productionPoints * 0.30 +
|
||||
officePoints * 0.08 +
|
||||
nobilityPoints * 0.05 +
|
||||
reputationPoints * 0.07 +
|
||||
housePoints * 0.05
|
||||
);
|
||||
|
||||
const currentCertificate = Number(user.certificate ?? 1);
|
||||
const targetCertificate = getTargetCertificateByScore(score);
|
||||
const nextCertificate = Math.min(5, currentCertificate + 1);
|
||||
const nextThreshold = CERTIFICATE_THRESHOLDS[nextCertificate] || null;
|
||||
|
||||
const currentValues = {
|
||||
avgKnowledge,
|
||||
completedProductions,
|
||||
highestPoliticalOfficeRank,
|
||||
highestChurchOfficeRank,
|
||||
highestOfficeRank,
|
||||
nobilityLevel,
|
||||
reputation,
|
||||
housePosition,
|
||||
knowledgePoints,
|
||||
productionPoints,
|
||||
officePoints,
|
||||
nobilityPoints,
|
||||
reputationPoints,
|
||||
housePoints,
|
||||
};
|
||||
|
||||
let statusRequirement = null;
|
||||
if (nextThreshold?.options?.length) {
|
||||
const options = nextThreshold.options.map((option) => ({
|
||||
...option,
|
||||
current: Number(currentValues[option.type] || 0),
|
||||
met: Number(currentValues[option.type] || 0) >= Number(option.required || 0),
|
||||
}));
|
||||
const metCount = options.filter((option) => option.met).length;
|
||||
statusRequirement = {
|
||||
mode: nextThreshold.statusMode,
|
||||
requiredCount: nextThreshold.statusRequiredCount,
|
||||
metCount,
|
||||
fulfilled: metCount >= nextThreshold.statusRequiredCount,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
const nextRequirements = nextThreshold ? [
|
||||
{
|
||||
type: 'avgKnowledge',
|
||||
current: avgKnowledge,
|
||||
required: nextThreshold.avgKnowledge,
|
||||
met: avgKnowledge >= nextThreshold.avgKnowledge,
|
||||
},
|
||||
{
|
||||
type: 'completedProductions',
|
||||
current: completedProductions,
|
||||
required: nextThreshold.completedProductions,
|
||||
met: completedProductions >= nextThreshold.completedProductions,
|
||||
},
|
||||
...(typeof nextThreshold.reputationPoints === 'number' ? [{
|
||||
type: 'reputationPoints',
|
||||
current: reputationPoints,
|
||||
required: nextThreshold.reputationPoints,
|
||||
met: reputationPoints >= nextThreshold.reputationPoints,
|
||||
}] : []),
|
||||
] : [];
|
||||
|
||||
return {
|
||||
currentCertificate,
|
||||
nextCertificate,
|
||||
score: Number(score.toFixed(2)),
|
||||
targetCertificate,
|
||||
currentValues,
|
||||
nextRequirements,
|
||||
statusRequirement,
|
||||
readyForNextCertificate: nextCertificate <= targetCertificate
|
||||
&& nextRequirements.every((requirement) => requirement.met)
|
||||
&& (!statusRequirement || statusRequirement.fulfilled),
|
||||
};
|
||||
}
|
||||
|
||||
formatProposals(proposals) {
|
||||
return proposals.map((proposal) => {
|
||||
const age = Math.floor((Date.now() - new Date(proposal.character.birthdate)) / (24 * 60 * 60 * 1000));
|
||||
|
||||
Reference in New Issue
Block a user