Files
yourpart3/backend/services/falukantService.js
2025-05-08 17:38:51 +02:00

1473 lines
59 KiB
JavaScript

import BaseService from './BaseService.js';
import { Sequelize, Op, where } from 'sequelize';
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';
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 {
async getFalukantUserByHashedId(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']
},
{
model: UserHouse,
as: 'userHouse',
attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'],
include: [
{
model: HouseType,
as: 'houseType',
attributes: ['labelTr', 'position']
}
]
},
]
});
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']
}
]
}
],
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
});
const stType = await FalukantStockType.findOne({ where: [{ label_tr: 'wood' }] });
await FalukantStock.create({ userId: falukantUser.id, regionId: region.id, stockTypeId: stType.id, quantity: 10 });
falukantUser.character = ch;
const bType = await BranchType.findOne({ where: { labelTr: 'fullstack' } });
await Branch.create({ falukantUserId: falukantUser.id, regionId: region.id, branchTypeId: bType.id });
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'] }
],
attributes: ['money']
});
if (!falukantUser) throw new Error('User not found');
if (falukantUser.character?.birthdate) falukantUser.character.setDataValue('age', calcAge(falukantUser.character.birthdate));
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 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');
}
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);
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 } });
}
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) {
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),
},
},
order: Sequelize.fn('RANDOM'),
});
if (!directorCharacter) {
throw new Error('No directors available for the region');
}
const avgKnowledge = await this.calculateAverageKnowledge(directorCharacter.id);
const proposedIncome = Math.round(
directorCharacter.titleOfNobility * Math.pow(1.231, avgKnowledge / 1.5)
);
await DirectorProposal.create({
directorCharacterId: directorCharacter.id,
employerUserId: falukantUserId,
proposedIncome,
});
}
}
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 proposal of regionUserDirectorProposals) {
await DirectorProposal.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 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');
}
const family = {
relationships: [],
deathPartners: [],
children: [],
lovers: [],
possiblePartners: [],
};
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((relationship) => ({
createdAt: relationship.createdAt,
widowFirstName2: relationship.widowFirstName2,
progress: relationship.nextStepProgress,
character2: {
id: relationship.character2.id,
age: calcAge(relationship.character2.birthdate),
gender: relationship.character2.gender,
firstName: relationship.character2.definedFirstName?.name || 'Unknown',
nobleTitle: relationship.character2.nobleTitle?.labelTr || '',
mood: relationship.character2.mood,
characterTrait: relationship.character2.traits,
},
relationshipType: relationship.relationshipType.tr,
}));
family.relationships = relationships.filter((relationship) => ['wooing', 'engaged', 'married'].includes(relationship.relationshipType));
family.lovers = relationships.filter((relationship) => ['lover'].includes(relationship.relationshipType.tr));
family.deathPartners = relationships.filter((relationship) => ['widowed'].includes(relationship.relationshipType.tr));
const ownAge = calcAge(character.birthdate);
if (ownAge < 12) {
family.possiblePartners = [];
} else if (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) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const character = await FalukantCharacter.findOne({
where: { userId: user.id },
});
if (!character) {
throw new Error('Character not found');
}
let gifts = await PromotionalGift.findAll({
include: [
{
model: PromotionalGiftMood,
as: 'promotionalgiftmoods',
attributes: ['mood_id', 'suitability']
},
{
model: PromotionalGiftCharacterTrait,
as: 'characterTraits',
attributes: ['trait_id', 'suitability']
}
]
});
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,
};
}));
}
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');
}
const traitAvg = traits.reduce((sum, ct) => sum + ct.suitability, 0) / traits.length;
const moodRecord = gift.promotionalgiftmoods[0];
if (!moodRecord) {
throw new Error('noMoodData');
}
const moodSuitability = moodRecord.suitability;
const changeValue = Math.round(traitAvg + moodSuitability - 5);
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']
}],
}
]
});
console.log(user.falukantData[0].userHouse);
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'],
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 (falukantUser.money < 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);
}
}
export default new FalukantService();