Files
yourpart3/backend/services/falukantService.js
Torsten Schulz (local) 4699488ce1 Änderung: Aktualisierung der .gitignore und Anpassung der Berechnung im FalukantService
Änderungen:
- Hinzufügung von neuen Skripten (.depall.sh, .depfe.sh, .depbe.sh) zur .gitignore, um unerwünschte Dateien vom Tracking auszuschließen.
- Anpassung der Berechnung des `changeValue` im FalukantService, um den höchsten Charakterwert anstelle des Durchschnitts zu verwenden, was die Konsistenz mit der Frontend-Logik verbessert.

Diese Anpassungen optimieren die Dateiverwaltung und verbessern die Logik zur Berechnung von Werten im Backend.
2025-09-15 15:02:34 +02:00

3195 lines
123 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import BaseService from './BaseService.js';
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';
import FalukantCharacter from '../models/falukant/data/character.js';
import RegionData from '../models/falukant/data/region.js';
import RegionType from '../models/falukant/type/region.js';
import FalukantStock from '../models/falukant/data/stock.js';
import FalukantStockType from '../models/falukant/type/stock.js';
import TitleOfNobility from '../models/falukant/type/title_of_nobility.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 ProductType from '../models/falukant/type/product.js';
import Knowledge from '../models/falukant/data/product_knowledge.js';
import Inventory from '../models/falukant/data/inventory.js';
import MoneyFlow from '../models/falukant/log/moneyflow.js';
import User from '../models/community/user.js';
import { notifyUser } from '../utils/socket.js';
import { differenceInDays } from 'date-fns';
import { updateFalukantUserMoney } from '../utils/sequelize.js';
import BuyableStock from '../models/falukant/data/buyable_stock.js';
import DirectorProposal from '../models/falukant/data/director_proposal.js';
import Director from '../models/falukant/data/director.js';
import DaySell from '../models/falukant/log/daysell.js';
import MarriageProposal from '../models/falukant/data/marriage_proposal.js';
import RelationshipType from '../models/falukant/type/relationship.js';
import Relationship from '../models/falukant/data/relationship.js';
import PromotionalGift from '../models/falukant/type/promotional_gift.js';
import PromotionalGiftCharacterTrait from '../models/falukant/predefine/promotional_gift_character_trait.js';
import PromotionalGiftMood from '../models/falukant/predefine/promotional_gift_mood.js';
import PromotionalGiftLog from '../models/falukant/log/promotional_gift.js';
import CharacterTrait from '../models/falukant/type/character_trait.js';
import Mood from '../models/falukant/type/mood.js';
import UserHouse from '../models/falukant/data/user_house.js';
import HouseType from '../models/falukant/type/house.js';
import BuyableHouse from '../models/falukant/data/buyable_house.js';
import PartyType from '../models/falukant/type/party.js';
import MusicType from '../models/falukant/type/music.js';
import BanquetteType from '../models/falukant/type/banquette.js';
import Party from '../models/falukant/data/party.js';
import ChildRelation from '../models/falukant/data/child_relation.js';
import Learning from '../models/falukant/data/learning.js';
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';
import UndergroundType from '../models/falukant/type/underground.js';
import Notification from '../models/falukant/log/notification.js';
import PoliticalOffice from '../models/falukant/data/political_office.js';
import Underground from '../models/falukant/data/underground.js';
function calcAge(birthdate) {
const b = new Date(birthdate); b.setHours(0, 0);
const now = new Date(); now.setHours(0, 0);
return differenceInDays(now, b);
}
async function getFalukantUserOrFail(hashedId) {
const user = await FalukantUser.findOne({
include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }]
});
if (!user) throw new Error('User not found');
return user;
}
async function getBranchOrFail(userId, branchId) {
const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: userId } });
if (!branch) throw new Error('Branch not found');
return branch;
}
function calcSellPrice(product, knowledgeFactor = 0) {
const max = product.sellCost;
const min = max * 0.6;
return min + (max - min) * (knowledgeFactor / 100);
}
function calculateMarriageCost(titleOfNobility, age) {
const minTitle = 1;
const adjustedTitle = titleOfNobility - minTitle + 1;
const baseCost = 500;
return baseCost * Math.pow(adjustedTitle, 1.3) - (age - 12) * 20;
}
class PreconditionError extends Error {
constructor(label) {
super(label);
this.name = 'PreconditionError';
this.status = 412;
}
}
class FalukantService extends BaseService {
static KNOWLEDGE_MAX = 99;
static COST_CONFIG = {
one: { min: 50, max: 5000 },
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 }
];
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) {
console.log('🔍 getFalukantUserByHashedId called with hashedId:', hashedId);
const user = await FalukantUser.findOne({
include: [
{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } },
{
model: FalukantCharacter,
as: 'character',
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'id'] },
{ model: CharacterTrait, as: 'traits', attributes: ['id', 'tr'] }
],
attributes: ['id', 'birthdate', 'gender', 'moodId', 'health']
},
{
model: UserHouse,
as: 'userHouse',
attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'],
include: [
{
model: HouseType,
as: 'houseType',
attributes: ['labelTr', 'position']
}
]
},
]
});
console.log('🔍 getFalukantUserByHashedId result:', user ? 'User found' : 'User not found');
if (!user) throw new Error('User not found');
return user;
}
async getUser(hashedUserId) {
const u = await FalukantUser.findOne({
include: [
{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId: hashedUserId } },
{
model: FalukantCharacter,
as: 'character',
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }
],
attributes: ['birthdate', 'gender']
},
{
model: RegionData,
as: 'mainBranchRegion',
include: [{ model: RegionType, as: 'regionType' }],
attributes: ['name']
},
{
model: Branch,
as: 'branches',
include: [
{ model: BranchType, as: 'branchType', attributes: ['labelTr'] },
{
model: RegionData,
as: 'region',
include: [{ model: RegionType, as: 'regionType' }],
attributes: ['name']
}
]
},
{
model: UserHouse,
as: 'userHouse',
include: [
{
model: HouseType,
as: 'houseType',
'attributes': ['labelTr', 'position']
},
],
attributes: ['roofCondition'],
},
],
attributes: ['money', 'creditAmount', 'todayCreditTaken',]
});
if (!u) throw new Error('User not found');
if (u.character?.birthdate) u.character.setDataValue('age', calcAge(u.character.birthdate));
return u;
}
async randomFirstName(gender) {
const names = await FalukantPredefineFirstname.findAll({ where: { gender } });
return names[Math.floor(Math.random() * names.length)].name;
}
async randomLastName() {
const names = await FalukantPredefineLastname.findAll();
return names[Math.floor(Math.random() * names.length)].name;
}
async createUser(hashedUserId, gender, firstName, lastName) {
const user = await this.getUserByHashedId(hashedUserId);
if (await FalukantUser.findOne({ where: { userId: user.id } })) throw new Error('User already exists in Falukant.');
let fnObj = await FalukantPredefineFirstname.findOne({ where: { name: firstName } });
let lnObj = await FalukantPredefineLastname.findOne({ where: { name: lastName } });
if (!fnObj) fnObj = await FalukantPredefineFirstname.create({ name: firstName, gender });
if (!lnObj) lnObj = await FalukantPredefineLastname.create({ name: lastName });
const region = await RegionData.findOne({
order: Sequelize.fn('RANDOM'),
limit: 1,
include: [{ model: RegionType, as: 'regionType', where: { labelTr: 'city' } }]
});
if (!region) throw new Error('No region found with the label "city".');
const nobility = await TitleOfNobility.findOne({ where: { labelTr: 'noncivil' } });
if (!nobility) throw new Error('No title of nobility found with the label "noncivil".');
const falukantUser = await FalukantUser.create({
userId: user.id, money: 50, creditAmount: 0, todayCreditTaken: 0, creditInterestRate: 0, mainBranchRegionId: region.id
});
const date = new Date(); date.setDate(date.getDate() - 14);
const ch = await FalukantCharacter.create({
userId: falukantUser.id, regionId: region.id, firstName: fnObj.id, lastName: lnObj.id, gender, birthdate: date, titleOfNobility: nobility.id
});
falukantUser.character = ch;
const bType = await BranchType.findOne({ where: { labelTr: 'fullstack' } });
const branch = await Branch.create({ falukantUserId: falukantUser.id, regionId: region.id, branchTypeId: bType.id });
const stType = await FalukantStockType.findOne({ where: [{ label_tr: 'wood' }] });
await FalukantStock.create({ branchId: branch.id, stockTypeId: stType.id, quantity: 20 });
notifyUser(user.hashedId, 'reloadmenu', {});
return falukantUser;
}
async getInfo(hashedUserId) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) throw new Error('User not found');
const falukantUser = await FalukantUser.findOne({
include: [
{ model: User, as: 'user', attributes: ['hashedId'], where: { hashedId: hashedUserId } },
{
model: FalukantCharacter,
as: 'character',
attributes: ['birthdate', 'health'],
include: [
{
model: Relationship,
as: 'relationshipsAsCharacter1',
required: false,
attributes: ['id', 'character2Id', 'relationshipTypeId'],
include: [{
model: RelationshipType,
as: 'relationshipType',
attributes: ['tr'],
where: { tr: { [Op.not]: 'lover' } }
}]
},
{
model: Relationship,
as: 'relationshipsAsCharacter2',
required: false,
attributes: ['id', 'character1Id', 'relationshipTypeId'],
include: [{
model: RelationshipType,
as: 'relationshipType',
attributes: ['tr'],
where: { tr: { [Op.not]: 'lover' } }
}]
}
]
},
],
attributes: ['id', 'money']
});
if (!falukantUser) throw new Error('User not found');
if (falukantUser.character?.birthdate) falukantUser.character.setDataValue('age', calcAge(falukantUser.character.birthdate));
// Aggregate status additions: children counts and unread notifications
try {
const bm = (step, payload = {}) => {
try { console.log(`[BLOCKMARKER][falukant.getInfo] ${step}`, payload); } catch (_) { /* ignore */ }
};
bm('aggregate.start', { userId: user.id, falukantUserId: falukantUser.id });
// Determine all character IDs belonging to the user
if (!falukantUser.id) {
bm('aggregate.noFalukantUserId');
throw new Error('Missing falukantUser.id in getInfo aggregation');
}
const userCharacterIdsRows = await FalukantCharacter.findAll({
attributes: ['id'],
where: { userId: falukantUser.id },
raw: true
});
const userCharacterIds = userCharacterIdsRows.map(r => r.id);
bm('aggregate.userCharacters', { count: userCharacterIds.length, ids: userCharacterIds.slice(0, 5) });
// Count distinct children for any of the user's characters (as father or mother)
let childrenCount = 0;
let unbaptisedChildrenCount = 0;
if (userCharacterIds.length > 0) {
const childRels = await ChildRelation.findAll({
attributes: ['childCharacterId'],
where: {
[Op.or]: [
{ fatherCharacterId: { [Op.in]: userCharacterIds } },
{ motherCharacterId: { [Op.in]: userCharacterIds } },
]
},
raw: true
});
const distinctChildIds = new Set(childRels.map(r => r.childCharacterId));
childrenCount = distinctChildIds.size;
bm('aggregate.children', { relations: childRels.length, distinct: childrenCount, sample: Array.from(distinctChildIds).slice(0, 5) });
const unbaptised = await ChildRelation.findAll({
attributes: ['childCharacterId'],
where: {
nameSet: false,
[Op.or]: [
{ fatherCharacterId: { [Op.in]: userCharacterIds } },
{ motherCharacterId: { [Op.in]: userCharacterIds } },
]
},
raw: true
});
const distinctUnbaptisedIds = new Set(unbaptised.map(r => r.childCharacterId));
unbaptisedChildrenCount = distinctUnbaptisedIds.size;
bm('aggregate.unbaptised', { relations: unbaptised.length, distinct: unbaptisedChildrenCount, sample: Array.from(distinctUnbaptisedIds).slice(0, 5) });
}
// Unread notifications count
const unreadNotifications = await Notification.count({ where: { userId: falukantUser.id, shown: false } });
bm('aggregate.unread', { unreadNotifications });
falukantUser.setDataValue('childrenCount', childrenCount);
falukantUser.setDataValue('unbaptisedChildrenCount', unbaptisedChildrenCount);
falukantUser.setDataValue('unreadNotifications', unreadNotifications);
bm('aggregate.done', { childrenCount, unbaptisedChildrenCount });
} catch (e) {
console.error('Error aggregating status info:', e);
falukantUser.setDataValue('childrenCount', 0);
falukantUser.setDataValue('unbaptisedChildrenCount', 0);
falukantUser.setDataValue('unreadNotifications', 0);
}
return falukantUser;
}
async getBranches(hashedUserId) {
const u = await getFalukantUserOrFail(hashedUserId);
const bs = await Branch.findAll({
where: { falukantUserId: u.id },
include: [
{ model: BranchType, as: 'branchType', attributes: ['labelTr'] },
{ model: RegionData, as: 'region', attributes: ['name'] }
],
attributes: ['id', 'regionId'],
order: [['branchTypeId', 'ASC']]
});
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({
where: { id: branchId, falukantUserId: u.id },
include: [
{ model: BranchType, as: 'branchType', attributes: ['labelTr'] },
{ model: RegionData, as: 'region', attributes: ['name'] },
{
model: Production,
as: 'productions',
attributes: ['quantity', 'startTimestamp'],
include: [{ model: ProductType, as: 'productType', attributes: ['id', 'category', 'labelTr', 'sellCost', 'productionTime'] }]
}
],
attributes: ['id', 'regionId']
});
if (!br) throw new Error('Branch not found');
return br;
}
async getStock(hashedUserId, branchId) {
const u = await getFalukantUserOrFail(hashedUserId);
const b = await getBranchOrFail(u.id, branchId);
return FalukantStock.findAll({ where: { regionId: b.regionId, userId: u.id } });
}
async createStock(hashedUserId, branchId, stockData) {
const u = await getFalukantUserOrFail(hashedUserId);
const b = await getBranchOrFail(u.id, branchId);
return FalukantStock.create({
userId: u.id, regionId: b.regionId, stockTypeId: stockData.stockTypeId, quantity: stockData.quantity
});
}
async createProduction(hashedUserId, branchId, productId, quantity) {
const u = await getFalukantUserOrFail(hashedUserId);
const b = await getBranchOrFail(u.id, branchId);
const p = await ProductType.findOne({ where: { id: productId } });
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);
const cost = quantity * p.category * 6;
if (u.money < cost) throw new Error('notenoughmoney');
const r = await updateFalukantUserMoney(u.id, -cost, 'Production cost', u.id);
if (!r.success) throw new Error('Failed to update money');
const d = await Production.create({ branchId: b.id, productId, quantity });
notifyUser(u.user.hashedId, 'falukantUpdateStatus', {});
notifyUser(u.user.hashedId, 'falukantBranchUpdate', { branchId: b.id });
return d;
}
async getProduction(hashedUserId, branchId) {
const u = await getFalukantUserOrFail(hashedUserId);
const b = await getBranchOrFail(u.id, branchId);
return Production.findOne({ where: { regionId: b.regionId } });
}
async getProducts(hashedUserId) {
const u = await getFalukantUserOrFail(hashedUserId);
const c = await FalukantCharacter.findOne({ where: { userId: u.id } });
if (!c) {
throw new Error(`No FalukantCharacter found for user with id ${u.id}`);
}
const ps = await ProductType.findAll({
where: { category: { [Op.lte]: u.certificate } },
include: [{ model: Knowledge, as: 'knowledges', attributes: ['knowledge'], where: { characterId: c.id } }],
attributes: ['labelTr', 'id', 'sellCost', 'productionTime', 'category']
});
return ps;
}
async getInventory(hashedUserId, branchId) {
const u = await getFalukantUserOrFail(hashedUserId);
const f = branchId ? { id: branchId, falukantUserId: u.id } : { falukantUserId: u.id };
const br = await Branch.findAll({
where: f,
include: [
{ model: FalukantStock, as: 'stocks', include: [{ model: FalukantStockType, as: 'stockType' }] },
{ model: RegionData, as: 'region', include: [{ model: RegionType, as: 'regionType' }] }
]
});
const stockIds = br.flatMap(b => b.stocks.map(s => s.id));
const inv = await Inventory.findAll({
where: { stockId: stockIds },
include: [
{
model: FalukantStock,
as: 'stock',
include: [
{
model: Branch,
as: 'branch',
include: [{ model: RegionData, as: 'region', include: [{ model: RegionType, as: 'regionType' }] }]
},
{ model: FalukantStockType, as: 'stockType' }
]
},
{ model: ProductType, as: 'productType' }
]
});
const grouped = inv.reduce((acc, i) => {
const r = i.stock.branch.region;
const k = `${r.id}-${i.productType.id}-${i.quality}`;
acc[k] = acc[k] || { region: r, product: i.productType, quality: i.quality, totalQuantity: 0 };
acc[k].totalQuantity += i.quantity;
return acc;
}, {});
return Object.values(grouped).sort((a, b) => {
if (a.region.id !== b.region.id) return a.region.id - b.region.id;
if (a.product.id !== b.product.id) return a.product.id - b.product.id;
return a.quality - b.quality;
});
}
async sellProduct(hashedUserId, branchId, productId, quality, quantity) {
const user = await getFalukantUserOrFail(hashedUserId);
const branch = await getBranchOrFail(user.id, branchId);
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!character) throw new Error('No character found for user');
const stock = await FalukantStock.findOne({ where: { branchId: branch.id } });
if (!stock) throw new Error('Stock not found');
const inventory = await Inventory.findAll({
where: { quality },
include: [
{
model: ProductType,
as: 'productType',
required: true,
where: { id: productId },
include: [
{
model: Knowledge,
as: 'knowledges',
required: false,
where: { characterId: character.id }
}
]
}
]
});
if (!inventory.length) {
throw new Error('No inventory found');
}
const available = inventory.reduce((sum, i) => sum + i.quantity, 0);
if (available < quantity) throw new Error('Not enough inventory available');
const item = inventory[0].productType;
const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
const revenue = quantity * calcSellPrice(item, knowledgeVal);
const moneyResult = await updateFalukantUserMoney(user.id, revenue, 'Product sale', user.id);
if (!moneyResult.success) throw new Error('Failed to update money');
let remaining = quantity;
for (const inv of inventory) {
if (inv.quantity <= remaining) {
remaining -= inv.quantity;
await inv.destroy();
} else {
await inv.update({ quantity: inv.quantity - remaining });
remaining = 0;
break;
}
}
await this.addSellItem(branchId, user.id, productId, quantity);
console.log('[FalukantService.sellProduct] emitting events for user', user.user.hashedId, 'branch', branch?.id);
notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
return { success: true };
}
async sellAllProducts(hashedUserId, branchId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const branch = await Branch.findOne({
where: { id: branchId, falukantUserId: falukantUser.id },
include: [{ model: FalukantStock, as: 'stocks' }]
});
if (!branch) throw new Error('Branch not found');
const stockIds = branch.stocks.map(s => s.id);
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id } });
if (!character) throw new Error('No character for user');
const inventory = await Inventory.findAll({
where: { stockId: stockIds },
include: [
{
model: ProductType,
as: 'productType',
include: [
{
model: Knowledge,
as: 'knowledges',
required: false,
where: {
characterId: character.id
}
}
]
},
{
model: FalukantStock,
as: 'stock',
include: [
{
model: Branch,
as: 'branch'
},
{
model: FalukantStockType,
as: 'stockType'
}
]
}
]
});
if (!inventory.length) return { success: true, revenue: 0 };
let total = 0;
for (const item of inventory) {
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
total += item.quantity * calcSellPrice(item.productType, knowledgeVal);
await this.addSellItem(item.stock.branch.id, falukantUser.id, item.productType.id, item.quantity);
}
const moneyResult = await updateFalukantUserMoney(
falukantUser.id,
total,
'Sell all products',
falukantUser.id
);
if (!moneyResult.success) throw new Error('Failed to update money');
for (const item of inventory) {
await Inventory.destroy({ where: { id: item.id } });
}
console.log('[FalukantService.sellAllProducts] emitting events for user', falukantUser.user.hashedId, 'branch', branchId, 'revenue', total, 'items', inventory.length);
notifyUser(falukantUser.user.hashedId, 'falukantUpdateStatus', {});
notifyUser(falukantUser.user.hashedId, 'falukantBranchUpdate', { branchId });
return { success: true, revenue: total };
}
async addSellItem(branchId, userId, productId, quantity) {
const branch = await Branch.findOne({
where: { id: branchId },
})
;
const daySell = await DaySell.findOne({
where: {
regionId: branch.regionId,
productId: productId,
sellerId: userId,
}
});
if (daySell) {
daySell.quantity += quantity;
await daySell.save();
} else {
await DaySell.create({
regionId: branch.regionId,
productId: productId,
sellerId: userId,
quantity: quantity,
});
}
}
async moneyHistory(hashedUserId, page = 1, filter = '') {
const u = await getFalukantUserOrFail(hashedUserId);
const limit = 25, offset = (page - 1) * limit;
const w = { falukantUserId: u.id };
if (filter) w.activity = { [Op.iLike]: `%${filter}%` };
const { rows, count } = await MoneyFlow.findAndCountAll({
where: w, order: [['time', 'DESC']], limit, offset
});
return { data: rows, total: count, currentPage: page, totalPages: Math.ceil(count / limit) };
}
async getStorage(hashedUserId, branchId) {
const user = await getFalukantUserOrFail(hashedUserId);
const branch = await getBranchOrFail(user.id, branchId);
const stocks = await FalukantStock.findAll({
where: { branchId: branch.id },
include: [{ model: FalukantStockType, as: 'stockType' }],
});
const stockIds = stocks.map(s => s.id);
const inventoryItems = await Inventory.findAll({
where: { stockId: stockIds },
include: [
{
model: FalukantStock,
as: 'stock',
include: [{ model: FalukantStockType, as: 'stockType' }],
},
],
});
let totalUsedCapacity = 0;
const usageByType = {};
for (const s of stocks) {
const stId = s.stockTypeId;
if (!usageByType[stId]) {
usageByType[stId] = {
stockTypeId: stId,
stockTypeLabelTr: s.stockType?.labelTr || `stockType:${stId}`,
totalCapacity: 0,
used: 0
};
}
usageByType[stId].totalCapacity += s.quantity;
}
for (const item of inventoryItems) {
totalUsedCapacity += item.quantity;
const stId = item.stock.stockTypeId;
if (!usageByType[stId]) {
usageByType[stId] = {
stockTypeId: stId,
stockTypeLabelTr: item.stock.stockType?.labelTr || `stockType:${stId}`,
totalCapacity: 0,
used: 0
};
}
usageByType[stId].used += item.quantity;
}
const buyableStocks = await BuyableStock.findAll({
where: { regionId: branch.regionId },
include: [{ model: FalukantStockType, as: 'stockType' }],
});
const buyableByType = {};
for (const b of buyableStocks) {
const stId = b.stockTypeId;
if (!buyableByType[stId]) {
buyableByType[stId] = {
stockTypeId: stId,
stockTypeLabelTr: b.stockType?.labelTr || `stockType:${stId}`,
quantity: 0
};
}
buyableByType[stId].quantity += b.quantity;
}
let maxCapacity = stocks.reduce((sum, s) => sum + s.quantity, 0);
return {
branchId,
totalUsedCapacity,
maxCapacity,
usageByType: Object.values(usageByType),
buyableByType: Object.values(buyableByType)
};
}
async buyStorage(hashedUserId, branchId, amount, stockTypeId) {
const user = await getFalukantUserOrFail(hashedUserId);
const branch = await getBranchOrFail(user.id, branchId);
const buyableStocks = await BuyableStock.findAll({
where: { regionId: branch.regionId, stockTypeId },
include: [{ model: FalukantStockType, as: 'stockType' }]
});
if (!buyableStocks || buyableStocks.length === 0) {
throw new Error('Not enough buyable stock');
}
const totalAvailable = buyableStocks.reduce((sum, entry) => sum + entry.quantity, 0);
if (totalAvailable < amount) {
throw new Error('Not enough buyable stock');
}
const costPerUnit = buyableStocks[0].stockType.cost;
const totalCost = costPerUnit * amount;
if (user.money < totalCost) {
throw new Error('notenoughmoney');
}
const moneyResult = await updateFalukantUserMoney(
user.id,
-totalCost,
`Buy storage (type: ${buyableStocks[0].stockType.labelTr})`,
user.id
);
if (!moneyResult.success) {
throw new Error('Failed to update money');
}
let remainingToDeduct = amount;
for (const entry of buyableStocks) {
if (entry.quantity > remainingToDeduct) {
entry.quantity -= remainingToDeduct;
await entry.save();
remainingToDeduct = 0;
break;
} else if (entry.quantity === remainingToDeduct) {
await entry.destroy();
remainingToDeduct = 0;
break;
} else {
remainingToDeduct -= entry.quantity;
await entry.destroy();
}
}
let stock = await FalukantStock.findOne({
where: { branchId: branch.id, stockTypeId },
include: [{ model: FalukantStockType, as: 'stockType' }]
});
if (!stock) {
stock = await FalukantStock.create({
branchId: branch.id,
stockTypeId,
quantity: amount,
});
} else {
stock.quantity += amount;
await stock.save();
}
notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId });
return {
success: true,
bought: amount,
totalCost,
stockType: buyableStocks[0].stockType.labelTr
};
}
async sellStorage(hashedUserId, branchId, amount, stockTypeId) {
const user = await getFalukantUserOrFail(hashedUserId);
const branch = await getBranchOrFail(user.id, branchId);
const stock = await FalukantStock.findOne({
where: { branchId: branch.id, stockTypeId },
include: [{ model: FalukantStockType, as: 'stockType' }]
});
if (!stock || stock.quantity < amount) throw new Error('Not enough stock to sell');
const costPerUnit = stock.stockType.cost;
const totalRevenue = costPerUnit * amount;
const moneyResult = await updateFalukantUserMoney(
user.id,
totalRevenue,
`Sell storage (type: ${stock.stockType.labelTr})`,
user.id
);
if (!moneyResult.success) throw new Error('Failed to update money');
stock.quantity -= amount;
await stock.save();
const buyable = await BuyableStock.findOne({
where: { regionId: branch.regionId, stockTypeId },
include: [{ model: FalukantStockType, as: 'stockType' }]
});
if (!buyable) throw new Error('No buyable record found for this region and stockType');
buyable.quantity += amount;
await buyable.save();
return { success: true, sold: amount, totalRevenue, stockType: stock.stockType.labelTr };
}
async notifyRegionUsersAboutStockChange(regionId) {
const users = await FalukantCharacter.findAll({
where: {
regionId: regionId
},
include: [
{
model: FalukantUser,
as: 'user',
include: [
{
model: User,
as: 'user'
},
{
model: Branch,
as: 'branch',
where: {
regionId: regionId
}
}
]
}
]
});
for (const user of users) {
notifyUser(user.user[0].user[0].hashedId, 'stock_change', { branchId: user.user[0].branch[0].id });
}
}
async getStockTypes() {
return FalukantStockType.findAll();
}
async getStockOverview() {
const items = await Inventory.findAll({
include: [
{
model: FalukantStock,
as: 'stock',
include: [
{
model: Branch,
as: 'branch',
include: [{ model: RegionData, as: 'region' }]
}
]
},
{
model: ProductType,
as: 'productType'
}
]
});
const result = items.map(inv => ({
regionName: inv.stock?.branch?.region?.name || '???',
productLabelTr: inv.productType?.labelTr || '???',
quantity: inv.quantity
}));
return result;
}
async getAllProductions(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const productions = await Production.findAll({
include: [
{
model: Branch,
as: 'branch',
include: [
{ model: RegionData, as: 'region', attributes: ['name'] }
],
where: { falukantUserId: user.id }
},
{ model: ProductType, as: 'productType', attributes: ['labelTr', 'productionTime'] }
],
attributes: ['startTimestamp', 'quantity'],
});
const formattedProductions = productions.map((production) => {
const startTimestamp = new Date(production.startTimestamp).getTime();
const endTimestamp = startTimestamp + production.productType.productionTime * 60 * 1000;
return {
cityName: production.branch.region.name,
productName: production.productType.labelTr,
quantity: production.quantity,
endTimestamp: new Date(endTimestamp).toISOString(),
};
});
formattedProductions.sort((a, b) => new Date(a.endTimestamp) - new Date(b.endTimestamp));
return formattedProductions;
}
async getDirectorProposals(hashedUserId, branchId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
if (!user) {
throw new Error('User not found');
}
const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: user.id } });
if (!branch) {
throw new Error('Branch not found or does not belong to the user');
}
const { falukantUserId, regionId } = branch;
await this.deleteExpiredProposals();
const existingProposals = await this.fetchProposals(falukantUserId, regionId);
if (existingProposals.length > 0) {
return this.formatProposals(existingProposals);
}
await this.generateProposals(falukantUserId, regionId);
const newProposals = await this.fetchProposals(falukantUserId, regionId);
return this.formatProposals(newProposals);
}
async deleteExpiredProposals() {
const expirationTime = new Date(Date.now() - 24 * 60 * 60 * 1000);
await DirectorProposal.destroy({
where: {
createdAt: {
[Op.lt]: expirationTime,
},
},
});
}
async fetchProposals(falukantUserId, regionId) {
return DirectorProposal.findAll({
where: { employerUserId: falukantUserId },
include: [
{
model: FalukantCharacter,
as: 'character',
attributes: ['firstName', 'lastName', 'birthdate', 'titleOfNobility', 'gender'],
where: { regionId },
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName' },
{ model: FalukantPredefineLastname, as: 'definedLastName' },
{ model: TitleOfNobility, as: 'nobleTitle' },
{
model: Knowledge,
as: 'knowledges',
include: [
{ model: ProductType, as: 'productType' },
]
},
],
},
],
});
}
async generateProposals(falukantUserId, regionId) {
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: [
{
model: TitleOfNobility,
as: 'nobleTitle',
attributes: ['level'],
},
],
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,
});
}
} catch (error) {
console.log(error.message, error.stack);
throw new Error(error.message);
}
}
async calculateAverageKnowledge(characterId) {
const averageKnowledge = await Knowledge.findAll({
where: { characterId },
attributes: [[Sequelize.fn('AVG', Sequelize.col('knowledge')), 'avgKnowledge']],
raw: true,
});
return parseFloat(averageKnowledge[0]?.avgKnowledge || 0);
}
formatProposals(proposals) {
return proposals.map((proposal) => {
const age = Math.floor((Date.now() - new Date(proposal.character.birthdate)) / (24 * 60 * 60 * 1000));
const knowledge = proposal.character.knowledges?.map(k => ({
productId: k.productId,
value: k.knowledge,
labelTr: k.productType.labelTr,
})) || [];
return {
id: proposal.id,
proposedIncome: proposal.proposedIncome,
character: {
name: `${proposal.character.definedFirstName.name} ${proposal.character.definedLastName.name}`,
title: proposal.character.nobleTitle.labelTr,
age,
knowledge,
gender: proposal.character.gender,
},
};
});
}
async convertProposalToDirector(hashedUserId, proposalId) {
const user = await getFalukantUserOrFail(hashedUserId);
if (!user) {
throw new Error('User not found');
}
const proposal = await DirectorProposal.findOne(
{
where: { id: proposalId },
include: [
{ model: FalukantCharacter, as: 'character' },
]
}
);
if (!proposal || proposal.employerUserId !== user.id) {
throw new Error('Proposal does not belong to the user');
}
const existingDirector = await Director.findOne({
where: {
employerUserId: user.id
},
include: [
{
model: FalukantCharacter,
as: 'character',
where: {
regionId: proposal.character.regionId,
}
},
]
});
if (existingDirector) {
throw new Error('A director already exists for this region');
}
const newDirector = await Director.create({
directorCharacterId: proposal.directorCharacterId,
employerUserId: proposal.employerUserId,
income: proposal.proposedIncome,
});
const regionUserDirectorProposals = await DirectorProposal.findAll({
where: {
employerUserId: proposal.employerUserId,
},
include: [
{
model: FalukantCharacter,
as: 'character',
where: {
regionId: proposal.character.regionId,
}
},
]
});
if (regionUserDirectorProposals.length > 0) {
for (const p of regionUserDirectorProposals) {
await p.destroy();
}
}
notifyUser(hashedUserId, 'directorchanged');
return newDirector;
}
async getDirectorForBranch(hashedUserId, branchId) {
const user = await getFalukantUserOrFail(hashedUserId);
if (!user) {
throw new Error('User not found');
}
const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: user.id } });
if (!branch) {
throw new Error('Branch not found or does not belong to the user');
}
const director = await Director.findOne({
where: { employerUserId: user.id },
include: [
{
model: FalukantCharacter,
as: 'character',
attributes: ['firstName', 'lastName', 'birthdate', 'titleOfNobility', 'gender'],
where: {
regionId: branch.regionId,
},
include: [
{
model: TitleOfNobility,
as: 'nobleTitle'
},
{
model: FalukantPredefineFirstname,
as: 'definedFirstName'
},
{
model: FalukantPredefineLastname,
as: 'definedLastName'
},
]
},
],
});
if (!director) {
return null;
}
const age = Math.floor((Date.now() - new Date(director.character.birthdate)) / (24 * 60 * 60 * 1000));
return {
director: {
id: director.id,
character: {
name: `${director.character.definedFirstName.name} ${director.character.definedLastName.name}`,
title: director.character.nobleTitle.labelTr,
age,
gender: director.character.gender,
},
income: director.income,
satisfaction: director.satisfaction,
mayProduce: director.mayProduce,
maySell: director.maySell,
mayStartTransport: director.mayStartTransport,
},
};
}
async getAllDirectors(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
if (!user) {
throw new Error('User not found');
}
const directors = await Director.findAll({
where: { employerUserId: user.id },
include: [
{
model: FalukantCharacter,
as: 'character',
attributes: ['id', 'birthdate', 'gender'],
include: [
{
model: TitleOfNobility,
as: 'nobleTitle',
attributes: ['labelTr', 'level'],
},
{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
attributes: ['name'],
},
{
model: FalukantPredefineLastname,
as: 'definedLastName',
attributes: ['name'],
},
{
model: Knowledge,
as: 'knowledges',
attributes: ['productId', 'knowledge'],
include: [
{
model: ProductType,
as: 'productType',
attributes: ['labelTr'],
},
],
},
{
model: RegionData,
as: 'region',
attributes: ['name']
}
]
},
],
attributes: ['id', 'satisfaction', 'income'],
});
return directors.map(director => {
// 1) avgKnowledge berechnen
const knowledges = director.character.knowledges || [];
const avgKnowledge = knowledges.length
? knowledges.reduce((sum, k) => sum + k.knowledge, 0) / knowledges.length
: 0;
// 2) wishedIncome anhand der JS-Formel
const wishedIncome = Math.round(
director.character.nobleTitle.level * Math.pow(1.231, avgKnowledge / 1.5)
);
return {
id: director.id,
satisfaction: director.satisfaction,
character: director.character,
age: calcAge(director.character.birthdate),
income: director.income,
region: director.character.region.name,
wishedIncome,
};
});
}
async updateDirector(hashedUserId, directorId, income) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const director = await Director.findOne({
where: {
id: directorId,
employerUserId: user.id
}
});
if (!director) {
throw new Error('Director not found');
}
director.update({ income: income });
return { success: true };
}
async setSetting(hashedUserId, branchId, directorId, settingKey, value) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const branch = await Branch.findOne({
where: {
id: branchId,
falukantUserId: user.id,
},
});
if (!branch) {
return null;
}
const director = await Director.findOne({
where: {
id: directorId,
employerUserId: user.id,
},
include: [{
model: FalukantCharacter,
as: 'character',
where: {
regionId: branch.regionId,
}
}]
});
if (!director) {
return null;
}
const updateData = {};
updateData[settingKey] = value || false;
await Director.update(updateData, {
where: {
id: director.id,
},
});
return { result: 'ok' };
}
async getFamily(hashedUserId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
if (!user) throw new Error('User not found');
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!character) throw new Error('Character not found for this user');
let relationships = await Relationship.findAll({
where: { character1Id: character.id },
attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress'],
include: [
{
model: FalukantCharacter, as: 'character2',
attributes: ['id', 'birthdate', 'gender', 'moodId'],
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] },
{ model: CharacterTrait, as: 'traits' },
{ model: Mood, as: 'mood' },
]
},
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'] }
]
});
relationships = relationships.map(r => ({
createdAt: r.createdAt,
widowFirstName2: r.widowFirstName2,
progress: r.nextStepProgress,
character2: {
id: r.character2.id,
age: calcAge(r.character2.birthdate),
gender: r.character2.gender,
firstName: r.character2.definedFirstName?.name || 'Unknown',
nobleTitle: r.character2.nobleTitle?.labelTr || '',
mood: r.character2.mood,
traits: r.character2.traits
},
relationshipType: r.relationshipType.tr
}));
const charsWithChildren = await FalukantCharacter.findAll({
where: { userId: user.id },
include: [
{
model: ChildRelation,
as: 'childrenFather',
include: [{
model: FalukantCharacter,
as: 'child',
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
}]
},
{
model: ChildRelation,
as: 'childrenMother',
include: [{
model: FalukantCharacter,
as: 'child',
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
}]
}
]
});
const children = [];
for (const parentChar of charsWithChildren) {
const allRels = [
...(parentChar.childrenFather || []),
...(parentChar.childrenMother || [])
];
for (const rel of allRels) {
const kid = rel.child;
children.push({
name: kid.definedFirstName?.name || 'Unknown',
gender: kid.gender,
age: calcAge(kid.birthdate),
hasName: rel.nameSet,
_createdAt: rel.createdAt,
});
}
}
// Sort children globally by relation createdAt ascending (older first)
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
const inProgress = ['wooing', 'engaged', 'married'];
const family = {
relationships: relationships.filter(r => inProgress.includes(r.relationshipType)),
lovers: relationships.filter(r => r.relationshipType === 'lover'),
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
children: children.map(({ _createdAt, ...rest }) => rest),
possiblePartners: []
};
const ownAge = calcAge(character.birthdate);
if (ownAge >= 12 && family.relationships.length === 0) {
family.possiblePartners = await this.getPossiblePartners(character.id);
if (family.possiblePartners.length === 0) {
await this.createPossiblePartners(
character.id,
character.gender,
character.regionId,
character.titleOfNobility,
ownAge
);
family.possiblePartners = await this.getPossiblePartners(character.id);
}
}
return family;
}
async getPossiblePartners(requestingCharacterId) {
const proposals = await MarriageProposal.findAll({
where: {
requesterCharacterId: requestingCharacterId,
},
include: [
{
model: FalukantCharacter,
as: 'proposedCharacter',
attributes: ['id', 'firstName', 'lastName', 'gender', 'regionId', 'birthdate'],
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] },
],
},
],
});
return proposals.map(proposal => {
const birthdate = new Date(proposal.proposedCharacter.birthdate);
const age = calcAge(birthdate);
return {
id: proposal.id,
requesterCharacterId: proposal.requesterCharacterId,
proposedCharacterId: proposal.proposedCharacter.id,
proposedCharacterName: `${proposal.proposedCharacter.definedFirstName?.name} ${proposal.proposedCharacter.definedLastName?.name}`,
proposedCharacterGender: proposal.proposedCharacter.gender,
proposedCharacterRegionId: proposal.proposedCharacter.regionId,
proposedCharacterAge: age,
proposedCharacterNobleTitle: proposal.proposedCharacter.nobleTitle.labelTr,
cost: proposal.cost,
};
});
}
async createPossiblePartners(requestingCharacterId, requestingCharacterGender, requestingRegionId, requestingCharacterTitleOfNobility, ownAge) {
try {
const minTitleResult = await TitleOfNobility.findOne({
order: [['id', 'ASC']],
attributes: ['id'],
});
if (!minTitleResult) {
throw new Error('No title of nobility found');
}
const minTitle = minTitleResult.id;
const potentialPartners = await FalukantCharacter.findAll({
where: {
id: { [Op.ne]: requestingCharacterId },
gender: { [Op.ne]: requestingCharacterGender },
regionId: requestingRegionId,
createdAt: { [Op.lt]: new Date(new Date() - 12 * 24 * 60 * 60 * 1000) },
titleOfNobility: { [Op.between]: [requestingCharacterTitleOfNobility - 1, requestingCharacterTitleOfNobility + 1] }
},
order: [
[Sequelize.literal(`ABS((EXTRACT(EPOCH FROM (NOW() - "birthdate")) / 86400) - ${ownAge})`), 'ASC']
],
limit: 5,
});
const proposals = potentialPartners.map(partner => {
const age = calcAge(partner.birthdate);
return {
requesterCharacterId: requestingCharacterId,
proposedCharacterId: partner.id,
cost: calculateMarriageCost(partner.titleOfNobility, age, minTitle),
};
});
await MarriageProposal.bulkCreate(proposals);
} catch (error) {
console.error('Error creating possible partners:', error);
throw error;
}
}
async acceptMarriageProposal(hashedUserId, proposedCharacterId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!user) {
throw new Error('User not found');
}
const proposal = await MarriageProposal.findOne({
where: {
requesterCharacterId: character.id,
proposedCharacterId: proposedCharacterId,
},
});
if (!proposal) {
throw new Error('Proposal not found');
}
if (user.money < proposal.cost) {
throw new Error('Not enough money to accept the proposal');
}
const moneyResult = await updateFalukantUserMoney(user.id, -proposal.cost, 'Marriage cost', user.id);
if (!moneyResult.success) {
throw new Error('Failed to update money');
}
const marriedType = await RelationshipType.findOne({
where: { tr: 'wooing' },
});
if (!marriedType) {
throw new Error('Relationship type "married" not found');
}
await Relationship.create({
character1Id: proposal.requesterCharacterId,
character2Id: proposal.proposedCharacterId,
relationshipTypeId: marriedType.id,
});
await MarriageProposal.destroy({
where: { requesterCharacterId: character.id },
})
;
return { success: true, message: 'Marriage proposal accepted' };
}
async getGifts(hashedUserId) {
// 1) Mein User & Character
const user = await this.getFalukantUserByHashedId(hashedUserId);
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 (!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'],
where: { mood_id: relatedMoodId },
required: false // Gifts ohne Mood-Match bleiben erhalten, haben dann leeres Array
},
{
model: PromotionalGiftCharacterTrait,
as: 'characterTraits',
attributes: ['trait_id', 'suitability'],
where: { trait_id: relatedTraitIds },
required: false // Gifts ohne Trait-Match bleiben erhalten
}
]
});
// 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) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
console.log(user);
const children = await ChildRelation.findAll({
where: {
[Op.or]: [
{ fatherCharacterId: user.character.id },
{ motherCharacterId: user.character.id }
]
},
include: [
{
model: FalukantCharacter,
as: 'child',
attributes: ['id', 'birthdate'],
include: [
{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
attributes: ['name']
},
{
model: Knowledge,
as: 'knowledges',
attributes: ['knowledge'],
include: [
{
model: ProductType,
as: 'productType',
attributes: ['id', 'labelTr']
}
]
}
]
},
]
});
return children.map(rel => ({
id: rel.child.id,
name: rel.child.definedFirstName.name,
age: calcAge(rel.child.birthdate),
knowledge: rel.child.knowledges.map(k => ({
id: k.productType.id,
tr: k.productType.labelTr,
knowledge: k.knowledge
}))
}));
}
async sendGift(hashedUserId, giftId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const lowestTitle = await TitleOfNobility.findOne({ order: [['id', 'ASC']] });
const currentMoodId = user.character.moodId;
if (currentMoodId == null) {
throw new Error('moodNotSet');
}
const relation = await Relationship.findOne({
where: { character1Id: user.character.id },
include: [{
model: RelationshipType,
as: 'relationshipType',
where: { tr: 'wooing' }
}]
});
if (!relation) {
throw new Error('notRelated');
}
const lastGift = await PromotionalGiftLog.findOne({
where: { senderCharacterId: user.character.id },
order: [['createdAt', 'DESC']],
limit: 1
});
if (lastGift && (lastGift.createdAt.getTime() + 3_600_000) > Date.now()) {
throw new PreconditionError('tooOften');
}
const gift = await PromotionalGift.findOne({
where: { id: giftId },
include: [
{
model: PromotionalGiftCharacterTrait,
as: 'characterTraits',
where: { trait_id: { [Op.in]: user.character.traits.map(t => t.id) } },
required: false
},
{
model: PromotionalGiftMood,
as: 'promotionalgiftmoods',
where: { mood_id: currentMoodId },
required: false
}
]
});
if (!gift) {
throw new Error('notFound');
}
const cost = await this.getGiftCost(
gift.value,
user.character.nobleTitle.id,
lowestTitle.id
);
if (user.money < cost) {
throw new PreconditionError('insufficientFunds');
}
const traits = gift.characterTraits;
if (!traits.length) {
throw new Error('noTraits');
}
// Finde den höchsten Charakterwert (wie im Frontend)
const highestCharacterValue = Math.max(...traits.map(ct => ct.suitability));
const moodRecord = gift.promotionalgiftmoods[0];
if (!moodRecord) {
throw new Error('noMoodData');
}
const moodSuitability = moodRecord.suitability;
// Gleiche Berechnung wie im Frontend: (moodValue + highestCharacterValue) / 2
const changeValue = Math.round((moodSuitability + highestCharacterValue) / 2);
await updateFalukantUserMoney(user.id, -cost, 'Gift cost', user.id);
await relation.update({ nextStepProgress: relation.nextStepProgress + changeValue });
await PromotionalGiftLog.create({
senderCharacterId: user.character.id,
recipientCharacterId: relation.character2Id,
giftId,
changeValue
});
this.checkProposalProgress(relation);
return { success: true, message: 'sent' };
}
async checkProposalProgress(relation) {
const { nextStepProgress } = relation;
if (nextStepProgress >= 100) {
const engagedStatus = await RelationshipType.findOne({ where: { tr: 'engaged' } });
await relation.update({ nextStepProgress: 0, relationshipTypeId: engagedStatus.id });
const user = await User.findOne({
include: [{
model: FalukantUser,
as: 'falukantData',
include: [{
model: FalukantCharacter,
as: 'character',
where: { id: relation.character1Id }
}]
}]
});
await notifyUser(user.hashedId, 'familychanged');
}
}
async getGiftCost(value, titleOfNobility, lowestTitleOfNobility) {
const titleLevel = titleOfNobility - lowestTitleOfNobility + 1;
return Math.round(value * Math.pow(1 + titleLevel * 0.3, 1.3) * 100) / 100;
}
async getTitlesOfNobility() {
return TitleOfNobility.findAll();
}
async getHouseTypes() {
// return House
}
async getMoodAffect() {
return PromotionalGiftMood.findAll();
}
async getCharacterAffect() {
return PromotionalGiftCharacterTrait.findAll();
}
async getUserHouse(hashedUserId) {
try {
const user = await User.findOne({
where: { hashedId: hashedUserId },
include: [{
model: FalukantUser,
as: 'falukantData',
include: [{
model: UserHouse,
as: 'userHouse',
include: [{
model: HouseType,
as: 'houseType',
attributes: ['position', 'cost']
}],
attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition']
}],
}
]
});
return user.falukantData[0].userHouse ?? { position: 0, roofCondition: 100, wallCondition: 100, floorCondition: 100, windowCondition: 100 };
} catch (error) {
console.log(error);
return {};
}
}
async getBuyableHouses(hashedUserId) {
try {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const houses = await BuyableHouse.findAll({
include: [{
model: HouseType,
as: 'houseType',
attributes: ['position', 'cost', 'labelTr'],
where: {
minimumNobleTitle: {
[Op.lte]: user.character.nobleTitle.id
}
}
}],
attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition', 'id'],
order: [
[{ model: HouseType, as: 'houseType' }, 'position', 'DESC'],
['wallCondition', 'DESC'],
['roofCondition', 'DESC'],
['floorCondition', 'DESC'],
['windowCondition', 'DESC']
]
});
return houses;
} catch (error) {
console.error('Fehler beim Laden der kaufbaren Häuser:', error);
throw error;
}
}
async buyUserHouse(hashedUserId, houseId) {
try {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const house = await BuyableHouse.findByPk(houseId, {
include: [{
model: HouseType,
as: 'houseType',
}],
});
if (!house) {
throw new Error('Das Haus wurde nicht gefunden.');
}
const housePrice = this.housePrice(house);
const oldHouse = await UserHouse.findOne({ where: { userId: falukantUser.id } });
if (Number(falukantUser.money) < Number(housePrice)) {
throw new Error('notenoughmoney.');
}
if (oldHouse) {
await oldHouse.destroy();
}
await UserHouse.create({
userId: falukantUser.id,
houseTypeId: house.houseTypeId,
});
await house.destroy();
await updateFalukantUserMoney(falukantUser.id, -housePrice, "housebuy", falukantUser.id);
const user = await User.findByPk(falukantUser.userId);
notifyUser(user.hashedId, 'falukantHouseUpdate', {});
return {};
} catch (error) {
console.error('Fehler beim Kaufen des Hauses:', error);
throw error;
}
}
housePrice(house) {
const houseQuality = (house.roofCondition + house.windowCondition + house.floorCondition + house.wallCondition) / 4;
return (house.houseType.cost / 100 * houseQuality).toFixed(2);
}
async getPartyTypes(hashedUserId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const engagedCount = await Relationship.count({
include: [
{
model: RelationshipType,
as: 'relationshipType',
where: { tr: 'engaged' },
required: true
},
{
model: FalukantCharacter,
as: 'character1',
where: { userId: falukantUser.id },
required: false
},
{
model: FalukantCharacter,
as: 'character2',
where: { userId: falukantUser.id },
required: false
}
],
where: {
[Op.or]: [
{ '$character1.user_id$': falukantUser.id },
{ '$character2.user_id$': falukantUser.id }
]
}
});
const orConditions = [{ forMarriage: false }];
if (engagedCount > 0) {
orConditions.push({ forMarriage: true });
}
const partyTypes = await PartyType.findAll({
where: {
[Op.or]: orConditions
},
order: [['cost', 'ASC']]
});
const musicTypes = await MusicType.findAll();
const banquetteTypes = await BanquetteType.findAll();
return { partyTypes, musicTypes, banquetteTypes };
}
async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const since = new Date(Date.now() - 24 * 3600 * 1000);
const already = await Party.findOne({
where: {
falukantUserId: falukantUser.id,
partyTypeId,
createdAt: { [Op.gte]: since },
},
attributes: ['id']
});
if (already) {
throw new Error('Diese Party wurde bereits innerhalb der letzten 24 Stunden bestellt');
}
const [ptype, music, banquette] = await Promise.all([
PartyType.findByPk(partyTypeId),
MusicType.findByPk(musicId),
BanquetteType.findByPk(banquetteId),
]);
if (!ptype || !music || !banquette) {
throw new Error('Ungültige Party-, Musik- oder Bankett-Auswahl');
}
const nobilities = nobilityIds.length
? await TitleOfNobility.findAll({ where: { id: { [Op.in]: nobilityIds } } })
: [];
let cost = (ptype.cost || 0) + (music.cost || 0) + (banquette.cost || 0);
cost += (50 / servantRatio - 1) * 1000;
const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0);
cost += nobilityCost;
if (Number(falukantUser.money) < cost) {
throw new Error('Nicht genügend Guthaben für diese Party');
}
const moneyResult = await updateFalukantUserMoney(
falukantUser.id,
-cost,
'partyOrder',
falukantUser.id
);
if (!moneyResult.success) {
throw new Error('Geld konnte nicht abgezogen werden');
}
const party = await Party.create({
partyTypeId,
falukantUserId: falukantUser.id,
musicTypeId: musicId,
banquetteTypeId: banquetteId,
servantRatio,
cost: cost
});
if (nobilityIds.length) {
await party.addInvitedNobilities(nobilityIds);
}
const user = await User.findByPk(falukantUser.userId);
notifyUser(user.hashedId, 'falukantPartyUpdate', {
partyId: party.id,
cost,
});
return { 'success': true };
}
async getParties(hashedUserId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const parties = await Party.findAll({
where: { falukantUserId: falukantUser.id },
include: [
{
model: PartyType,
as: 'partyType',
attributes: ['tr'],
},
{
model: MusicType,
as: 'musicType',
attributes: ['tr'],
},
{
model: BanquetteType,
as: 'banquetteType',
attributes: ['tr'],
},
],
order: [['createdAt', 'DESC']],
attributes: ['id', 'createdAt', 'servantRatio', 'cost'],
});
return parties;
}
async getNotBaptisedChildren(hashedUserId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const children = await ChildRelation.findAll({
include: [
{
model: FalukantCharacter,
as: 'father',
where: {
userId: falukantUser.id,
},
required: false,
},
{
model: FalukantCharacter,
as: 'mother',
where: {
userId: falukantUser.id,
},
required: false,
},
{
model: FalukantCharacter,
as: 'child',
required: true,
include: [
{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
required: true,
},
]
},
],
where: {
nameSet: false,
},
order: [['createdAt', 'DESC']],
});
return children.map(child => {
return {
id: child.child.id,
gender: child.child.gender,
age: calcAge(child.child.birthdate),
proposedFirstName: child.child.definedFirstName.name,
};
});
}
async baptise(hashedUserId, childId, firstName) {
try {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const parentCharacter = await FalukantCharacter.findOne({
where: {
userId: falukantUser.id,
},
});
if (!parentCharacter) {
throw new Error('Parent character not found');
}
const child = await FalukantCharacter.findOne({
where: {
id: childId,
},
});
if (!child) {
throw new Error('Child not found');
}
const childRelation = await ChildRelation.findOne({
where: {
[Op.or]: [
{
fatherCharacterId: parentCharacter.id,
childCharacterId: child.id,
},
{
motherCharacterId: parentCharacter.id,
childCharacterId: child.id,
}
]
}
});
if (!childRelation) {
throw new Error('Child relation not found');
}
await childRelation.update({
nameSet: true,
});
let firstNameObject = await FalukantPredefineFirstname.findOne({
where: {
name: firstName,
gender: child.gender,
},
});
if (!firstNameObject) {
firstNameObject = await FalukantPredefineFirstname.create({
name: firstName,
gender: child.gender,
});
}
await child.update({
firstName: firstNameObject.id,
});
updateFalukantUserMoney(falukantUser.id, -50, 'Baptism', falukantUser.id);
// Trigger status bar refresh for the user after baptism
notifyUser(hashedUserId, 'falukantUpdateStatus', {});
return { success: true };
} catch (error) {
throw new Error(error.message);
}
}
async getEducation(hashedUserId) {
try {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const education = await Learning.findAll({
where: {
createdAt: { [Op.gt]: new Date().getTime() - 1000 * 60 * 60 * 24 },
},
include: [
{
model: LearnRecipient,
as: 'recipient',
attributes: ['tr']
},
{
model: ProductType,
as: 'productType',
attributes: ['labelTr']
},
{
model: FalukantUser,
as: 'learner',
where: {
id: falukantUser.id
},
attributes: []
},
{
model: FalukantCharacter,
as: 'learningCharacter',
attributes: ['id']
}
],
attributes: ['createdAt'],
});
return education;
} catch (error) {
console.log(error);
throw new Error(error.message);
}
}
computeCost(percent, mode) {
const cfg = FalukantService.COST_CONFIG[mode];
// clamp percent auf [0, KNOWLEDGE_MAX]
const p = Math.min(Math.max(percent, 0), FalukantService.KNOWLEDGE_MAX)
/ FalukantService.KNOWLEDGE_MAX;
return Math.round(cfg.min + (cfg.max - cfg.min) * p);
}
async sendToSchool(hashedUserId, item, student, studentId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
// 1) prüfen, ob schon in Arbeit
const education = await this.getEducation(hashedUserId);
const already = education.some(e =>
e.recipient.tr === student &&
(studentId == null || e.learningCharacter?.id === studentId)
);
if (already) throw new Error('Already learning this character');
// 2) Empfänger holen
const rec = await LearnRecipient.findOne({ where: { tr: student } });
if (!rec) throw new Error('Character not found');
// 3) Wissens-Prozentsatz ermitteln
let percent;
if (item === 'all') {
const all = await this.getKnowledgeForAll(hashedUserId, student, studentId);
const sum = all.reduce((s, k) => s + k.knowledge, 0);
percent = sum / all.length;
} else {
const single = await this.getKnowledgeSingle(hashedUserId, student, studentId, item);
percent = single.knowledge;
}
// 4) Kosten berechnen
const mode = item === 'all' ? 'all' : 'one';
const cost = this.computeCost(percent, mode);
// 5) Kontostand prüfen
if (parseFloat(falukantUser.money) < cost) {
throw new Error('Not enough money');
}
// 6) Learning anlegen
await Learning.create({
learningRecipientId: rec.id,
associatedLearningCharacterId: studentId,
associatedFalukantUserId: falukantUser.id,
productId: item === 'all' ? null : item,
learnAllProducts: (item === 'all')
});
// 7) Geld abziehen
const upd = await updateFalukantUserMoney(
falukantUser.id,
-cost,
item === 'all' ? 'learnAll' : `learnItem:${item}`,
falukantUser.id
);
if (!upd.success) throw new Error(upd.message);
return true;
}
async getBankOverview(hashedUserId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
if (!falukantUser) throw new Error('User not found');
// 1) offene Schulden
const totalDebt = await Credit.sum('remaining_amount', {
where: { falukant_user_id: falukantUser.id }
}) || 0;
// 2) Häuser ermitteln
const userHouses = await UserHouse.findAll({
where: { userId: falukantUser.id },
include: [{ model: HouseType, as: 'houseType', attributes: ['cost'] }]
});
// 3) Hauswert berechnen: buyCost * 0.8
let houseValue = 0;
for (const uh of userHouses) {
const { roofCondition, wallCondition, floorCondition, windowCondition } = uh;
const qualityAvg = (roofCondition + wallCondition + floorCondition + windowCondition) / 4;
const buyWorth = (uh.houseType.cost / 100) * qualityAvg;
houseValue += buyWorth * 0.8;
}
// 4) Filialwert (1000 pro Branch)
const branchCount = await Branch.count({ where: { falukantUserId: falukantUser.id } });
const branchValue = branchCount * 1000;
// 5) Maximaler Kredit und verfügbare Linie
const maxCredit = Math.floor(houseValue + branchValue);
const availableCredit = maxCredit - totalDebt;
// 6) aktive Kredite laden
const activeCredits = await Credit.findAll({
where: { falukantUserId: falukantUser.id },
attributes: ['id', 'amount', 'remainingAmount', 'interestRate']
});
return {
money: falukantUser.money,
totalDebt,
maxCredit,
availableCredit,
activeCredits,
fee: 7
};
}
async getBankCredits(hashedUserId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
if (!falukantUser) throw new Error('User not found');
const credits = await Credit.findAll({
where: { falukantUserId: falukantUser.id },
});
return credits;
}
async takeBankCredits(hashedUserId, height) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
if (!falukantUser) throw new Error('User not found');
const financialData = await this.getBankOverview(hashedUserId);
if (financialData.availableCredit < height) {
throw new Error('Not enough credit');
}
const newCredit = await Credit.create({
falukantUserId: falukantUser.id,
amount: height,
remainingAmount: height,
interestRate: financialData.fee,
});
updateFalukantUserMoney(falukantUser.id, height, 'credit taken', falukantUser.id);
return { height: newCredit.amount, fee: newCredit.interestRate };
}
async getNobility(hashedUserId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const nobility = await TitleOfNobility.findOne({
include: [
{
model: FalukantCharacter,
as: 'charactersWithNobleTitle',
attributes: ['gender'],
where: {
userId: falukantUser.id
}
},
{
model: TitleRequirement,
as: 'requirements',
attributes: ['requirementType', 'requirementValue']
}
],
attributes: ['labelTr', 'level']
});
const currentTitleLevel = nobility.level;
const nextTitle = await TitleOfNobility.findOne({
where: {
level: currentTitleLevel + 1
},
include: [
{
model: TitleRequirement,
as: 'requirements',
}
],
attributes: ['labelTr']
});
return {
current: nobility,
next: nextTitle
};
}
async advanceNobility(hashedUserId) {
const nobility = await this.getNobility(hashedUserId);
if (!nobility || !nobility.next) {
throw new Error('User does not have a nobility');
}
const nextTitle = nobility.next.toJSON();
const user = await this.getFalukantUserByHashedId(hashedUserId);
let fulfilled = true;
let cost = 0;
for (const requirement of nextTitle.requirements) {
switch (requirement.requirementType) {
case 'money':
fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement);
break;
case 'cost':
fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement);
cost = requirement.requirementValue;
break;
case 'branches':
fulfilled = fulfilled && await this.checkBranchesRequirement(hashedUserId, requirement);
break;
default:
fulfilled = false;
};
}
if (!fulfilled) {
throw new Error('Requirements not fulfilled');
}
const newTitle = await TitleOfNobility.findOne({
where: { level: nobility.current.level + 1 }
});
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
await character.update({ titleOfNobility: newTitle.id });
if (cost > 0) {
updateFalukantUserMoney(user.id, -cost, 'new nobility title', user.id);
}
return {};
}
async checkMoneyRequirement(user, requirement) {
return user.money >= requirement.requirementValue;
}
async checkBranchesRequirement(hashedUserId, requirement) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
}
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({
where: { characterId: user.character.id },
order: [['createdAt', 'DESC']],
});
return {
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 } }),
};
}
async healthActivity(hashedUserId, activity) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const lastHealthActivity = await HealthActivity.findOne({
where: {
characterId: user.character.id,
activityTr: activity,
createdAt: {
[Op.gte]: new Date(new Date().setDate(new Date().getDate() - 1))
}
},
order: [['createdAt', 'DESC']],
limit: 1
});
if (lastHealthActivity) {
throw new Error('too close');
}
const activityObject = FalukantService.HEALTH_ACTIVITIES.find((a) => a.tr === activity);
if (!activityObject) {
throw new Error('invalid');
}
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),
cost: activityObject.cost
});
updateFalukantUserMoney(user.id, -activityObject.cost, 'health.' + activity);
return { success: true };
}
async healthChange(user, delta) {
const char = await FalukantCharacter.findOne({
where: {
id: user.character.id
}
});
await char.update({
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 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 nonexistent 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) NoncivilTitel 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 };
}
async getUndergroundTypes(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const undergroundTypes = await UndergroundType.findAll();
return undergroundTypes;
}
async getNotifications(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const notifications = await Notification.findAll({
where: { userId: user.id, shown: false },
order: [['createdAt', 'DESC']]
});
return notifications;
}
async getAllNotifications(hashedUserId, page = 1, size = 10) {
const user = await getFalukantUserOrFail(hashedUserId);
const limit = Math.max(1, Math.min(Number(size) || 10, 100));
const offset = Math.max(0, ((Number(page) || 1) - 1) * limit);
const { rows, count } = await Notification.findAndCountAll({
where: { userId: user.id },
order: [['createdAt', 'DESC']],
offset,
limit,
});
return { items: rows, total: count, page: Number(page) || 1, size: limit };
}
async markNotificationsShown(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const [count] = await Notification.update(
{ shown: true },
{ where: { userId: user.id, shown: false } }
);
return { updated: count };
}
async getPoliticalOfficeHolders(hashedUserId) {
const user = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
where: {
userId: user.id
}
});
const relevantRegionIds = await this.getRegionAndParentIds(character.regionId);
const now = new Date();
const histories = await PoliticalOffice.findAll({
where: {
regionId: {
[Op.in]: relevantRegionIds
},
},
include: [{
model: FalukantCharacter,
as: 'holder',
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }
],
attributes: ['id', 'gender']
}, {
model: PoliticalOfficeType,
as: 'type',
}]
});
// Unikate nach character.id
const map = new Map();
histories.forEach(h => {
const c = h.holder;
if (c && c.id && !map.has(c.id)) {
map.set(c.id, {
id: c.id,
name: `${c.definedFirstName.name} ${c.definedLastName.name}`,
title: c.nobleTitle.labelTr,
officeType: h.type.name,
gender: c.gender
});
}
});
return Array.from(map.values());
}
async getRegionAndParentIds(regionId) {
const relevantRegionIds = new Set();
let currentRegionId = regionId;
while (currentRegionId !== null) {
relevantRegionIds.add(currentRegionId);
const region = await RegionData.findByPk(currentRegionId, {
attributes: ['parentId']
});
if (region && region.parentId) {
currentRegionId = region.parentId;
} else {
currentRegionId = null; // Keine weitere Elternregion gefunden
}
}
return Array.from(relevantRegionIds);
}
// vorher: async searchUsers(q) {
async searchUsers(hashedUserId, q) {
// User-Prüfung wie bei anderen Methoden
await getFalukantUserOrFail(hashedUserId); // wir brauchen das Ergebnis hier nicht weiter, nur Validierung
const chars = await FalukantCharacter.findAll({
where: {
userId: { [Op.ne]: null },
[Op.or]: [
{ '$user.user.username$': { [Op.iLike]: `%${q}%` } },
{ '$definedFirstName.name$': { [Op.iLike]: `%${q}%` } },
{ '$definedLastName.name$': { [Op.iLike]: `%${q}%` } }
]
},
include: [
{
model: FalukantUser,
as: 'user',
include: [
{
model: User,
as: 'user',
attributes: ['username']
}
]
},
{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
attributes: ['name']
},
{
model: FalukantPredefineLastname,
as: 'definedLastName',
attributes: ['name']
},
{
model: RegionData,
as: 'region',
attributes: ['name']
}
],
limit: 50,
raw: false
});
// Debug-Log (optional)
console.log('FalukantService.searchUsers raw result for', q, chars);
const mapped = chars
.map(c => ({
username: c.user?.user?.username || null,
firstname: c.definedFirstName?.name || null,
lastname: c.definedLastName?.name || null,
town: c.region?.name || null
}))
.filter(u => u.username);
console.log('FalukantService.searchUsers mapped result for', q, mapped);
return mapped;
}
async createUndergroundActivity(hashedUserId, payload) {
const { typeId, victimUsername, target, goal, politicalTargets } = payload;
// 1) Performer auflösen
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
if (!falukantUser || !falukantUser.character) {
throw new Error('Performer not found');
}
const performerChar = falukantUser.character;
// 2) Victim auflösen über Username (inner join)
const victimChar = await FalukantCharacter.findOne({
include: [
{
model: FalukantUser,
as: 'user',
required: true, // inner join
attributes: [],
include: [
{
model: User,
as: 'user',
required: true, // inner join
where: { username: victimUsername },
attributes: []
}
]
}
]
});
if (!victimChar) {
throw new PreconditionError('Victim character not found');
}
// 3) Selbstangriff verhindern
if (victimChar.id === performerChar.id) {
throw new PreconditionError('Cannot target yourself');
}
// 4) Typ-spezifische Validierung
const undergroundType = await UndergroundType.findByPk(typeId);
if (!undergroundType) {
throw new Error('Invalid underground type');
}
if (undergroundType.tr === 'sabotage') {
if (!target) {
throw new PreconditionError('Sabotage target missing');
}
}
if (undergroundType.tr === 'corrupt_politician') {
if (!goal) {
throw new PreconditionError('Corrupt goal missing');
}
// politicalTargets kann optional sein, falls benötigt prüfen
}
// 5) Eintrag anlegen (optional: in Transaction)
const newEntry = await Underground.create({
undergroundTypeId: typeId,
performerId: performerChar.id,
victimId: victimChar.id,
result: null,
parameters: {
target: target || null,
goal: goal || null,
politicalTargets: politicalTargets || null
}
});
return newEntry;
}
async getUndergroundAttacks(hashedUserId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const character = await FalukantCharacter.findOne({
where: { userId: falukantUser.id }
});
if (!character) throw new Error('Character not found');
const charId = character.id;
const attacks = await Underground.findAll({
where: {
[Op.or]: [
{ performerId: charId },
{ victimId: charId }
]
},
include: [
{
model: FalukantCharacter,
as: 'performer',
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{
model: FalukantUser,
as: 'user',
include: [{ model: User, as: 'user', attributes: ['username'] }],
attributes: []
}
],
attributes: ['id', 'gender']
},
{
model: FalukantCharacter,
as: 'victim',
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{
model: FalukantUser,
as: 'user',
include: [{ model: User, as: 'user', attributes: ['username'] }],
attributes: []
}
],
attributes: ['id', 'gender']
},
{
model: UndergroundType,
as: 'undergroundType', // hier der korrekte Alias
attributes: ['tr']
}
],
order: [['createdAt', 'DESC']]
});
const formatCharacter = (c) => {
if (!c) return null;
return {
id: c.id,
username: c.user?.user?.username || null,
firstname: c.definedFirstName?.name || null,
lastname: c.definedLastName?.name || null,
gender: c.gender
};
};
const mapped = attacks.map(a => ({
id: a.id,
result: a.result,
createdAt: a.createdAt,
updatedAt: a.updatedAt,
type: a.undergroundType?.tr || null, // angepasst
parameters: a.parameters,
performer: formatCharacter(a.performer),
victim: formatCharacter(a.victim),
success: !!a.result
}));
return {
sent: mapped.filter(a => a.performer?.id === charId),
received: mapped.filter(a => a.victim?.id === charId),
all: mapped
};
}
}
export default new FalukantService();