Some fixes and additions
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import BaseService from './BaseService.js';
|
||||
import { Sequelize, Op, where } from 'sequelize';
|
||||
|
||||
import { Sequelize, Op } from 'sequelize';
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import FalukantPredefineFirstname from '../models/falukant/predefine/firstname.js';
|
||||
import FalukantPredefineLastname from '../models/falukant/predefine/lastname.js';
|
||||
import FalukantUser from '../models/falukant/data/user.js';
|
||||
@@ -47,6 +47,12 @@ import LearnRecipient from '../models/falukant/type/learn_recipient.js';
|
||||
import Credit from '../models/falukant/data/credit.js';
|
||||
import TitleRequirement from '../models/falukant/type/title_requirement.js';
|
||||
import HealthActivity from '../models/falukant/log/health_activity.js';
|
||||
import Election from '../models/falukant/data/election.js';
|
||||
import PoliticalOfficeType from '../models/falukant/type/political_office_type.js';
|
||||
import Candidate from '../models/falukant/data/candidate.js';
|
||||
import Vote from '../models/falukant/data/vote.js';
|
||||
import PoliticalOfficePrerequisite from '../models/falukant/predefine/political_office_prerequisite.js';
|
||||
import PoliticalOfficeHistory from '../models/falukant/log/political_office_history.js';
|
||||
|
||||
function calcAge(birthdate) {
|
||||
const b = new Date(birthdate); b.setHours(0, 0);
|
||||
@@ -96,13 +102,33 @@ class FalukantService extends BaseService {
|
||||
all: { min: 400, max: 40000 }
|
||||
};
|
||||
static HEALTH_ACTIVITIES = [
|
||||
{ tr: "barber", method: "healthBarber", cost: 10 },
|
||||
{ tr: "doctor", method: "healthDoctor", cost: 50 },
|
||||
{ tr: "witch", method: "healthWitch", cost: 500 },
|
||||
{ tr: "pill", method: "healthPill", cost: 5000 },
|
||||
{ tr: "drunkOfLife", method: "healthDruckOfLife", cost:5000000 }
|
||||
];
|
||||
|
||||
{ tr: "barber", method: "healthBarber", cost: 10 },
|
||||
{ tr: "doctor", method: "healthDoctor", cost: 50 },
|
||||
{ tr: "witch", method: "healthWitch", cost: 500 },
|
||||
{ tr: "pill", method: "healthPill", cost: 5000 },
|
||||
{ tr: "drunkOfLife", method: "healthDruckOfLife", cost: 5000000 }
|
||||
];
|
||||
|
||||
static RECURSIVE_REGION_SEARCH = `
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT
|
||||
r.id,
|
||||
r.parent_id
|
||||
FROM falukant_data.region r
|
||||
join falukant_data."character" c
|
||||
on c.region_id = r.id
|
||||
WHERE c.user_id = :user_id
|
||||
UNION ALL
|
||||
SELECT
|
||||
r.id,
|
||||
r.parent_id
|
||||
FROM falukant_data.region r
|
||||
JOIN ancestors a ON r.id = a.parent_id
|
||||
)
|
||||
SELECT id
|
||||
FROM ancestors;
|
||||
`;
|
||||
|
||||
async getFalukantUserByHashedId(hashedId) {
|
||||
const user = await FalukantUser.findOne({
|
||||
include: [
|
||||
@@ -168,9 +194,21 @@ class FalukantService extends BaseService {
|
||||
attributes: ['name']
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
model: UserHouse,
|
||||
as: 'userHouse',
|
||||
include: [
|
||||
{
|
||||
model: HouseType,
|
||||
as: 'houseType',
|
||||
'attributes': ['labelTr', 'position']
|
||||
},
|
||||
],
|
||||
attributes: ['roofCondition'],
|
||||
},
|
||||
],
|
||||
attributes: ['money', 'creditAmount', 'todayCreditTaken']
|
||||
attributes: ['money', 'creditAmount', 'todayCreditTaken',]
|
||||
});
|
||||
if (!u) throw new Error('User not found');
|
||||
if (u.character?.birthdate) u.character.setDataValue('age', calcAge(u.character.birthdate));
|
||||
@@ -277,6 +315,38 @@ class FalukantService extends BaseService {
|
||||
return bs.map(b => ({ ...b.toJSON(), isMainBranch: u.mainBranchRegionId === b.regionId }));
|
||||
}
|
||||
|
||||
async createBranch(hashedUserId, cityId, branchTypeId) {
|
||||
const user = await getFalukantUserOrFail(hashedUserId);
|
||||
const branchType = await BranchType.findByPk(branchTypeId);
|
||||
if (!branchType) {
|
||||
throw new Error(`Unknown branchTypeId ${branchTypeId}`);
|
||||
}
|
||||
const existingCount = await Branch.count({
|
||||
where: { falukantUserId: user.id }
|
||||
});
|
||||
const exponentBase = Math.max(existingCount, 1);
|
||||
const rawCost = branchType.baseCost * Math.pow(exponentBase, 1.2);
|
||||
const cost = Math.round(rawCost * 100) / 100;
|
||||
await updateFalukantUserMoney(
|
||||
user.id,
|
||||
-cost,
|
||||
'create_branch'
|
||||
);
|
||||
const branch = await Branch.create({
|
||||
branchTypeId,
|
||||
regionId: cityId,
|
||||
falukantUserId: user.id
|
||||
});
|
||||
return branch;
|
||||
}
|
||||
|
||||
async getBranchTypes(hashedUserId) {
|
||||
const user = await getFalukantUserOrFail(hashedUserId);
|
||||
const branchTypes = await BranchType.findAll();
|
||||
return branchTypes;
|
||||
|
||||
}
|
||||
|
||||
async getBranch(hashedUserId, branchId) {
|
||||
const u = await getFalukantUserOrFail(hashedUserId);
|
||||
const br = await Branch.findOne({
|
||||
@@ -318,6 +388,8 @@ class FalukantService extends BaseService {
|
||||
const runningProductions = await Production.findAll({ where: { branchId: b.id } });
|
||||
if (runningProductions.length >= 2) {
|
||||
throw new Error('Too many productions');
|
||||
return; // wird später implementiert, wenn familie implementiert ist.
|
||||
|
||||
}
|
||||
if (!p) throw new Error('Product not found');
|
||||
quantity = Math.min(100, quantity);
|
||||
@@ -822,9 +894,12 @@ class FalukantService extends BaseService {
|
||||
await this.deleteExpiredProposals();
|
||||
const existingProposals = await this.fetchProposals(falukantUserId, regionId);
|
||||
if (existingProposals.length > 0) {
|
||||
console.log('Existing proposals:', existingProposals);
|
||||
return this.formatProposals(existingProposals);
|
||||
}
|
||||
console.log('No existing proposals, generating new ones');
|
||||
await this.generateProposals(falukantUserId, regionId);
|
||||
console.log('Fetch new proposals');
|
||||
const newProposals = await this.fetchProposals(falukantUserId, regionId);
|
||||
return this.formatProposals(newProposals);
|
||||
}
|
||||
@@ -867,13 +942,14 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
|
||||
async generateProposals(falukantUserId, regionId) {
|
||||
const proposalCount = Math.floor(Math.random() * 3) + 3;
|
||||
for (let i = 0; i < proposalCount; i++) {
|
||||
const directorCharacter = await FalukantCharacter.findOne({
|
||||
where: {
|
||||
regionId,
|
||||
createdAt: {
|
||||
[Op.lt]: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000),
|
||||
try {
|
||||
const threeWeeksAgo = new Date(Date.now() - 21 * 24 * 60 * 60 * 1000);
|
||||
const proposalCount = Math.floor(Math.random() * 3) + 3;
|
||||
for (let i = 0; i < proposalCount; i++) {
|
||||
const directorCharacter = await FalukantCharacter.findOne({
|
||||
where: {
|
||||
regionId,
|
||||
createdAt: { [Op.lt]: threeWeeksAgo },
|
||||
},
|
||||
include: [
|
||||
{
|
||||
@@ -881,22 +957,25 @@ class FalukantService extends BaseService {
|
||||
as: 'nobleTitle',
|
||||
attributes: ['level'],
|
||||
},
|
||||
]
|
||||
},
|
||||
order: Sequelize.fn('RANDOM'),
|
||||
});
|
||||
if (!directorCharacter) {
|
||||
throw new Error('No directors available for the region');
|
||||
],
|
||||
order: sequelize.literal('RANDOM()'),
|
||||
});
|
||||
if (!directorCharacter) {
|
||||
throw new Error('No directors available for the region');
|
||||
}
|
||||
const avgKnowledge = await this.calculateAverageKnowledge(directorCharacter.id);
|
||||
const proposedIncome = Math.round(
|
||||
directorCharacter.nobleTitle.level * Math.pow(1.231, avgKnowledge / 1.5)
|
||||
);
|
||||
await DirectorProposal.create({
|
||||
directorCharacterId: directorCharacter.id,
|
||||
employerUserId: falukantUserId,
|
||||
proposedIncome,
|
||||
});
|
||||
}
|
||||
const avgKnowledge = await this.calculateAverageKnowledge(directorCharacter.id);
|
||||
const proposedIncome = Math.round(
|
||||
directorCharacter.nobleTitle.level * Math.pow(1.231, avgKnowledge / 1.5)
|
||||
);
|
||||
await DirectorProposal.create({
|
||||
directorCharacterId: directorCharacter.id,
|
||||
employerUserId: falukantUserId,
|
||||
proposedIncome,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error.message, error.stack);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1394,39 +1473,65 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
|
||||
async getGifts(hashedUserId) {
|
||||
// 1) Mein User & Character
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
const character = await FalukantCharacter.findOne({
|
||||
where: { userId: user.id },
|
||||
const myChar = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
||||
if (!myChar) throw new Error('Character not found');
|
||||
|
||||
// 2) Beziehung finden und „anderen“ Character bestimmen
|
||||
const rel = await Relationship.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ character1Id: myChar.id },
|
||||
{ character2Id: myChar.id }
|
||||
]
|
||||
},
|
||||
include: [
|
||||
{ model: FalukantCharacter, as: 'character1', include: [{ model: CharacterTrait, as: 'traits' }] },
|
||||
{ model: FalukantCharacter, as: 'character2', include: [{ model: CharacterTrait, as: 'traits' }] }
|
||||
]
|
||||
});
|
||||
if (!character) {
|
||||
throw new Error('Character not found');
|
||||
}
|
||||
let gifts = await PromotionalGift.findAll({
|
||||
if (!rel) throw new Error('Beziehung nicht gefunden');
|
||||
|
||||
const relatedChar = rel.character1.id === myChar.id ? rel.character2 : rel.character1;
|
||||
|
||||
// 3) Trait-IDs und Mood des relatedChar
|
||||
const relatedTraitIds = relatedChar.traits.map(t => t.id);
|
||||
const relatedMoodId = relatedChar.moodId;
|
||||
|
||||
// 4) Gifts laden – aber nur die passenden Moods und Traits als Unter-Arrays
|
||||
const gifts = await PromotionalGift.findAll({
|
||||
include: [
|
||||
{
|
||||
model: PromotionalGiftMood,
|
||||
as: 'promotionalgiftmoods',
|
||||
attributes: ['mood_id', 'suitability']
|
||||
attributes: ['mood_id', 'suitability'],
|
||||
where: { mood_id: relatedMoodId },
|
||||
required: false // Gifts ohne Mood-Match bleiben erhalten, haben dann leeres Array
|
||||
},
|
||||
{
|
||||
model: PromotionalGiftCharacterTrait,
|
||||
as: 'characterTraits',
|
||||
attributes: ['trait_id', 'suitability']
|
||||
attributes: ['trait_id', 'suitability'],
|
||||
where: { trait_id: relatedTraitIds },
|
||||
required: false // Gifts ohne Trait-Match bleiben erhalten
|
||||
}
|
||||
]
|
||||
});
|
||||
const lowestTitleOfNobility = await TitleOfNobility.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
});
|
||||
return await Promise.all(gifts.map(async (gift) => {
|
||||
return {
|
||||
id: gift.id,
|
||||
name: gift.name,
|
||||
cost: await this.getGiftCost(gift.value, character.titleOfNobility, lowestTitleOfNobility.id),
|
||||
moodsAffects: gift.promotionalgiftmoods,
|
||||
charactersAffects: gift.characterTraits,
|
||||
};
|
||||
}));
|
||||
|
||||
// 5) Rest wie gehabt: Kosten berechnen und zurückgeben
|
||||
const lowestTitleOfNobility = await TitleOfNobility.findOne({ order: [['id', 'ASC']] });
|
||||
return Promise.all(gifts.map(async gift => ({
|
||||
id: gift.id,
|
||||
name: gift.name,
|
||||
cost: await this.getGiftCost(
|
||||
gift.value,
|
||||
myChar.titleOfNobility,
|
||||
lowestTitleOfNobility.id
|
||||
),
|
||||
moodsAffects: gift.promotionalgiftmoods, // nur Einträge mit relatedMoodId
|
||||
charactersAffects: gift.characterTraits // nur Einträge mit relatedTraitIds
|
||||
})));
|
||||
}
|
||||
|
||||
async getChildren(hashedUserId) {
|
||||
@@ -2199,8 +2304,8 @@ class FalukantService extends BaseService {
|
||||
|
||||
async getHealth(hashedUserId) {
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
const healthActivities = FalukantService.HEALTH_ACTIVITIES.map((activity) => {return { tr: activity.tr, cost: activity.cost }});
|
||||
const healthHistory = await HealthActivity.findAll({
|
||||
const healthActivities = FalukantService.HEALTH_ACTIVITIES.map((activity) => { return { tr: activity.tr, cost: activity.cost } });
|
||||
const healthHistory = await HealthActivity.findAll({
|
||||
where: { characterId: user.character.id },
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
@@ -2208,7 +2313,7 @@ class FalukantService extends BaseService {
|
||||
age: calcAge(user.character.birthdate),
|
||||
health: user.character.health,
|
||||
healthActivities: healthActivities,
|
||||
history: healthHistory.map((activity) => {return { tr: activity.activityTr, cost: activity.cost, createdAt: activity.createdAt, success: activity.successPercentage }}),
|
||||
history: healthHistory.map((activity) => { return { tr: activity.activityTr, cost: activity.cost, createdAt: activity.createdAt, success: activity.successPercentage } }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2232,14 +2337,14 @@ class FalukantService extends BaseService {
|
||||
if (!activityObject) {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
if (user.money - activityObject.cost < 0) {
|
||||
if (user.money - activityObject.cost < 0) {
|
||||
throw new Error('no money');
|
||||
}
|
||||
user.character.health -= activityObject.cost;
|
||||
await HealthActivity.create({
|
||||
characterId: user.character.id,
|
||||
activityTr: activity,
|
||||
successPercentage: await this[activityObject.method](user),
|
||||
successPercentage: await this[activityObject.method](user),
|
||||
cost: activityObject.cost
|
||||
});
|
||||
updateFalukantUserMoney(user.id, -activityObject.cost, 'health.' + activity);
|
||||
@@ -2256,32 +2361,424 @@ class FalukantService extends BaseService {
|
||||
health: Math.min(FalukantService.HEALTH_MAX || 100, Math.max(0, char.health + delta))
|
||||
});
|
||||
return delta;
|
||||
}
|
||||
|
||||
async healthBarber(user) {
|
||||
const raw = Math.floor(Math.random() * 11) - 5;
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
|
||||
async healthDoctor(user) {
|
||||
const raw = Math.floor(Math.random() * 8) - 2;
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
|
||||
async healthWitch(user) {
|
||||
const raw = Math.floor(Math.random() * 7) - 1;
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
|
||||
async healthPill(user) {
|
||||
const raw = Math.floor(Math.random() * 8);
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
|
||||
async healthDrunkOfLife(user) {
|
||||
const raw = Math.floor(Math.random() * 26);
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
}
|
||||
|
||||
async healthBarber(user) {
|
||||
const raw = Math.floor(Math.random() * 11) - 5;
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
|
||||
async healthDoctor(user) {
|
||||
const raw = Math.floor(Math.random() * 8) - 2;
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
|
||||
async healthWitch(user) {
|
||||
const raw = Math.floor(Math.random() * 7) - 1;
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
|
||||
async healthPill(user) {
|
||||
const raw = Math.floor(Math.random() * 8);
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
|
||||
async healthDrunkOfLife(user) {
|
||||
const raw = Math.floor(Math.random() * 26);
|
||||
return this.healthChange(user, raw);
|
||||
}
|
||||
|
||||
async getPoliticsOverview(hashedUserId) {
|
||||
|
||||
}
|
||||
|
||||
async getOpenPolitics(hashedUserId) {
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
if (!user || user.character.nobleTitle.labelTr === 'noncivil') {
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async getElections(hashedUserId) {
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
if (!user || user.character.nobleTitle.labelTr === 'noncivil') {
|
||||
return [];
|
||||
}
|
||||
const rows = await sequelize.query(
|
||||
FalukantService.RECURSIVE_REGION_SEARCH,
|
||||
{
|
||||
replacements: { user_id: user.id },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
const regionIds = rows.map(r => r.id);
|
||||
|
||||
// 3) Zeitbereich "heute"
|
||||
const todayStart = new Date();
|
||||
todayStart.setHours(0, 0, 0, 0);
|
||||
const todayEnd = new Date();
|
||||
todayEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
// 4) Wahlen laden (inkl. Kandidaten, Stimmen und Verknüpfungen)
|
||||
const rawElections = await Election.findAll({
|
||||
where: {
|
||||
regionId: { [Op.in]: regionIds },
|
||||
date: { [Op.between]: [todayStart, todayEnd] }
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: RegionData,
|
||||
as: 'region',
|
||||
attributes: ['name'],
|
||||
include: [{
|
||||
model: RegionType,
|
||||
as: 'regionType',
|
||||
attributes: ['labelTr']
|
||||
}]
|
||||
},
|
||||
{
|
||||
model: PoliticalOfficeType,
|
||||
as: 'officeType',
|
||||
attributes: ['name']
|
||||
},
|
||||
{
|
||||
model: Candidate,
|
||||
as: 'candidates',
|
||||
attributes: ['id'],
|
||||
include: [{
|
||||
model: FalukantCharacter,
|
||||
as: 'character',
|
||||
attributes: ['birthdate', 'gender'],
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
|
||||
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }
|
||||
]
|
||||
}]
|
||||
},
|
||||
{
|
||||
model: Vote,
|
||||
as: 'votes',
|
||||
attributes: ['candidateId'],
|
||||
where: {
|
||||
falukantUserId: user.id
|
||||
},
|
||||
required: false
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return rawElections.map(election => {
|
||||
const e = election.get({ plain: true });
|
||||
|
||||
const voted = Array.isArray(e.votes) && e.votes.length > 0;
|
||||
const reducedCandidates = (e.candidates || []).map(cand => {
|
||||
const ch = cand.character || {};
|
||||
const firstname = ch.definedFirstName?.name || '';
|
||||
const lastname = ch.definedLastName?.name || '';
|
||||
return {
|
||||
id: cand.id,
|
||||
title: ch.nobleTitle?.labelTr || null,
|
||||
name: `${firstname} ${lastname}`.trim(),
|
||||
age: calcAge(ch.birthdate),
|
||||
gender: ch.gender
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: e.id,
|
||||
officeType: { name: e.officeType.name },
|
||||
region: {
|
||||
name: e.region.name,
|
||||
regionType: { labelTr: e.region.regionType.labelTr }
|
||||
},
|
||||
date: e.date,
|
||||
postsToFill: e.postsToFill,
|
||||
candidates: reducedCandidates,
|
||||
voted: voted,
|
||||
votedFor: voted ? e.votes.map(vote => { return vote.candidateId }) : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async vote(hashedUserId, votes) {
|
||||
const elections = await this.getElections(hashedUserId);
|
||||
if (!Array.isArray(elections) || elections.length === 0) {
|
||||
throw new Error('No elections found');
|
||||
}
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const validElections = votes.filter(voteEntry => {
|
||||
const e = elections.find(el => el.id === voteEntry.electionId);
|
||||
return e && !e.voted;
|
||||
});
|
||||
|
||||
if (validElections.length === 0) {
|
||||
throw new Error('No valid elections to vote for (either non‐existent or already voted)');
|
||||
}
|
||||
validElections.forEach(voteEntry => {
|
||||
const e = elections.find(el => el.id === voteEntry.electionId);
|
||||
const allowedIds = e.candidates.map(c => c.id);
|
||||
voteEntry.candidateIds.forEach(cid => {
|
||||
if (!allowedIds.includes(cid)) {
|
||||
throw new Error(`Candidate ID ${cid} is not valid for election ${e.id}`);
|
||||
}
|
||||
});
|
||||
if (voteEntry.candidateIds.length > e.postsToFill) {
|
||||
throw new Error(`Too many candidates selected for election ${e.id}. Allowed: ${e.postsToFill}`);
|
||||
}
|
||||
});
|
||||
return await sequelize.transaction(async (tx) => {
|
||||
const toCreate = [];
|
||||
validElections.forEach(voteEntry => {
|
||||
voteEntry.candidateIds.forEach(candidateId => {
|
||||
toCreate.push({
|
||||
electionId: voteEntry.electionId,
|
||||
candidateId,
|
||||
falukantUserId: user.id
|
||||
});
|
||||
});
|
||||
});
|
||||
await Vote.bulkCreate(toCreate, {
|
||||
transaction: tx,
|
||||
ignoreDuplicates: true,
|
||||
returning: false
|
||||
});
|
||||
return { success: true };
|
||||
});
|
||||
}
|
||||
|
||||
async getOpenPolitics(hashedUserId) {
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
const characterId = user.character.id;
|
||||
const rows = await sequelize.query(
|
||||
FalukantService.RECURSIVE_REGION_SEARCH,
|
||||
{
|
||||
replacements: { user_id: user.id },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
const regionIds = rows.map(r => r.id);
|
||||
const histories = await PoliticalOfficeHistory.findAll({
|
||||
where: { characterId },
|
||||
attributes: ['officeTypeId', 'startDate', 'endDate']
|
||||
});
|
||||
const heldOfficeTypeIds = histories.map(h => h.officeTypeId);
|
||||
const allTypes = await PoliticalOfficeType.findAll({ attributes: ['id', 'name'] });
|
||||
const nameToId = Object.fromEntries(allTypes.map(t => [t.name, t.id]));
|
||||
const openPositions = await Election.findAll({
|
||||
where: {
|
||||
regionId: { [Op.in]: regionIds },
|
||||
date: { [Op.lt]: new Date() }
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: RegionData,
|
||||
as: 'region',
|
||||
attributes: ['name'],
|
||||
include: [
|
||||
{ model: RegionType, as: 'regionType', attributes: ['labelTr'] }
|
||||
]
|
||||
},
|
||||
{ model: Candidate, as: 'candidates' },
|
||||
{
|
||||
model: PoliticalOfficeType, as: 'officeType',
|
||||
include: [{ model: PoliticalOfficePrerequisite, as: 'prerequisites' }]
|
||||
}
|
||||
]
|
||||
});
|
||||
const result = openPositions
|
||||
.filter(election => {
|
||||
const prereqs = election.officeType.prerequisites || [];
|
||||
return prereqs.some(pr => {
|
||||
const jobs = pr.prerequisite.jobs;
|
||||
if (!Array.isArray(jobs) || jobs.length === 0) return true;
|
||||
return jobs.some(jobName => {
|
||||
const reqId = nameToId[jobName];
|
||||
return heldOfficeTypeIds.includes(reqId);
|
||||
});
|
||||
});
|
||||
})
|
||||
.map(election => {
|
||||
const e = election.get({ plain: true });
|
||||
const jobs = e.officeType.prerequisites[0]?.prerequisite.jobs || [];
|
||||
const matchingHistory = histories
|
||||
.filter(h => jobs.includes(allTypes.find(t => t.id === h.officeTypeId)?.name))
|
||||
.map(h => ({
|
||||
officeTypeId: h.officeTypeId,
|
||||
startDate: h.startDate,
|
||||
endDate: h.endDate
|
||||
}));
|
||||
|
||||
return {
|
||||
...e,
|
||||
history: matchingHistory
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async applyForElections(hashedUserId, electionIds) {
|
||||
// 1) Hole FalukantUser + Character
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
if (!user) {
|
||||
throw new Error('User nicht gefunden');
|
||||
}
|
||||
const character = user.character;
|
||||
if (!character) {
|
||||
throw new Error('Kein Charakter zum User gefunden');
|
||||
}
|
||||
|
||||
// 2) Noncivil‐Titel aussperren
|
||||
if (character.nobleTitle.labelTr === 'noncivil') {
|
||||
return { applied: [], skipped: electionIds };
|
||||
}
|
||||
|
||||
// 3) Ermittle die heute offenen Wahlen, auf die er zugreifen darf
|
||||
// (getElections liefert id, officeType, region, date, postsToFill, candidates, voted…)
|
||||
const openElections = await this.getElections(hashedUserId);
|
||||
const allowedIds = new Set(openElections.map(e => e.id));
|
||||
|
||||
// 4) Filter alle electionIds auf gültige/erlaubte
|
||||
const toTry = electionIds.filter(id => allowedIds.has(id));
|
||||
if (toTry.length === 0) {
|
||||
return { applied: [], skipped: electionIds };
|
||||
}
|
||||
|
||||
// 5) Prüfe, auf welche dieser Wahlen der Character bereits als Candidate eingetragen ist
|
||||
const existing = await Candidate.findAll({
|
||||
where: {
|
||||
electionId: { [Op.in]: toTry },
|
||||
characterId: character.id
|
||||
},
|
||||
attributes: ['electionId']
|
||||
});
|
||||
const alreadyIds = new Set(existing.map(c => c.electionId));
|
||||
|
||||
// 6) Erstelle Liste der Wahlen, für die er sich noch nicht beworben hat
|
||||
const newApplications = toTry.filter(id => !alreadyIds.has(id));
|
||||
const skipped = electionIds.filter(id => !newApplications.includes(id));
|
||||
|
||||
console.log(newApplications, skipped);
|
||||
|
||||
// 7) Bulk-Insert aller neuen Bewerbungen
|
||||
if (newApplications.length > 0) {
|
||||
const toInsert = newApplications.map(eid => ({
|
||||
electionId: eid,
|
||||
characterId: character.id
|
||||
}));
|
||||
await Candidate.bulkCreate(toInsert);
|
||||
}
|
||||
|
||||
return {
|
||||
applied: newApplications,
|
||||
skipped: skipped
|
||||
};
|
||||
}
|
||||
|
||||
async getRegions(hashedUserId) {
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
const regions = await RegionData.findAll({
|
||||
attributes: ['id', 'name', 'map'],
|
||||
include: [
|
||||
{
|
||||
model: RegionType,
|
||||
as: 'regionType',
|
||||
where: {
|
||||
labelTr: 'city'
|
||||
},
|
||||
attributes: ['labelTr']
|
||||
},
|
||||
{
|
||||
model: Branch,
|
||||
as: 'branches',
|
||||
where: {
|
||||
falukantUserId: user.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: BranchType,
|
||||
as: 'branchType',
|
||||
attributes: ['labelTr'],
|
||||
},
|
||||
],
|
||||
attributes: ['branchTypeId'],
|
||||
required: false,
|
||||
}
|
||||
]
|
||||
});
|
||||
return regions;
|
||||
}
|
||||
|
||||
async renovate(hashedUserId, element) {
|
||||
const user = await getFalukantUserOrFail(hashedUserId);
|
||||
const house = await UserHouse.findOne({
|
||||
where: { userId: user.id },
|
||||
include: [{ model: HouseType, as: 'houseType' }]
|
||||
});
|
||||
if (!house) throw new Error('House not found');
|
||||
const oldValue = house[element];
|
||||
if (oldValue >= 100) {
|
||||
return { cost: 0 };
|
||||
}
|
||||
const baseCost = house.houseType?.cost || 0;
|
||||
const cost = this._calculateRenovationCost(baseCost, element, oldValue);
|
||||
house[element] = 100;
|
||||
await house.save();
|
||||
await updateFalukantUserMoney(
|
||||
user.id,
|
||||
-cost,
|
||||
`renovation_${element}`
|
||||
);
|
||||
return { cost };
|
||||
}
|
||||
|
||||
_calculateRenovationCost(baseCost, key, currentVal) {
|
||||
const weights = {
|
||||
roofCondition: 0.25,
|
||||
wallCondition: 0.25,
|
||||
floorCondition: 0.25,
|
||||
windowCondition: 0.25
|
||||
};
|
||||
const weight = weights[key] || 0;
|
||||
const missing = 100 - currentVal;
|
||||
const raw = (missing / 100) * baseCost * weight;
|
||||
return Math.round(raw * 100) / 100;
|
||||
}
|
||||
|
||||
async renovateAll(hashedUserId) {
|
||||
const user = await getFalukantUserOrFail(hashedUserId);
|
||||
const house = await UserHouse.findOne({
|
||||
where: { userId: user.id },
|
||||
include: [{ model: HouseType, as: 'houseType' }]
|
||||
});
|
||||
if (!house) throw new Error('House not found');
|
||||
const baseCost = house.houseType?.cost || 0;
|
||||
const keys = ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'];
|
||||
let rawSum = 0;
|
||||
for (const key of keys) {
|
||||
const current = house[key];
|
||||
if (current < 100) {
|
||||
rawSum += this._calculateRenovationCost(baseCost, key, current);
|
||||
}
|
||||
}
|
||||
const totalCost = Math.round(rawSum * 0.8 * 100) / 100;
|
||||
for (const key of keys) {
|
||||
house[key] = 100;
|
||||
}
|
||||
await house.save();
|
||||
await updateFalukantUserMoney(
|
||||
user.id,
|
||||
-totalCost,
|
||||
'renovation_all'
|
||||
);
|
||||
|
||||
return { cost: totalCost };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new FalukantService();
|
||||
|
||||
Reference in New Issue
Block a user