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'; 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 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'] }, { model: CharacterTrait, as: 'traits', attributes: ['id', 'tr'] } ], attributes: ['id', 'birthdate', 'gender'] }, ] }); 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); console.log(u); const c = await FalukantCharacter.findOne({ where: { userId: u.id } }); console.log(c); 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'); } console.log(inventory); const available = inventory.reduce((sum, i) => sum + i.quantity, 0); console.log(available); 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 buyable = await BuyableStock.findOne({ where: { regionId: branch.regionId, stockTypeId }, include: [{ model: FalukantStockType, as: 'stockType' }] }); if (!buyable || buyable.quantity < amount) throw new Error('Not enough buyable stock'); const costPerUnit = buyable.stockType.cost; const totalCost = costPerUnit * amount; if (user.money < totalCost) throw new Error('notenoughmoney'); const moneyResult = await updateFalukantUserMoney( user.id, -totalCost, `Buy storage (type: ${buyable.stockType.labelTr})`, user.id ); if (!moneyResult.success) throw new Error('Failed to update money'); buyable.quantity -= amount; await buyable.save(); 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, }); return { success: true, bought: amount, totalCost, stockType: buyable.stockType.labelTr }; } stock.quantity += amount; await stock.save(); notifyUser(user.user.hashedId, 'falukantUpdateStatus', {}); notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId }); return { success: true, bought: amount, totalCost, stockType: buyable.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) { console.log('convert proposal to director - start'); const user = await getFalukantUserOrFail(hashedUserId); console.log('convert proposal to director - check user'); if (!user) { throw new Error('User not found'); } console.log('convert proposal to director - find proposal', proposalId); const proposal = await DirectorProposal.findOne( { where: { id: proposalId }, include: [ { model: FalukantCharacter, as: 'character' }, ] } ); console.log('convert proposal to director - check proposal'); if (!proposal || proposal.employerUserId !== user.id) { throw new Error('Proposal does not belong to the user'); } console.log('convert proposal to director - check existing director', user, proposal); 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'); } console.log('convert proposal to director - create new director'); 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, } }, ] }); console.log('convert proposal to director - remove propsals'); if (regionUserDirectorProposals.length > 0) { for (const proposal of regionUserDirectorProposals) { await DirectorProposal.destroy(); } } console.log('convert proposal to director - notify user'); 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'], include: [ { model: FalukantCharacter, as: 'character2', attributes: ['id', 'birthdate', 'gender'], include: [ { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }, ], }, { model: RelationshipType, as: 'relationshipType', attributes: ['tr'], }, ], } ); relationships = relationships.map((relationship) => ({ createdAt: relationship.createdAt, widowFirstName2: relationship.widowFirstName2, 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 || '', }, 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)); 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); 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); console.log(proposal.proposedCharacter); 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) { 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] } }, 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) { console.log(user, proposal); 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: { character1Id: 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(); 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), }; })); } async sendGift(hashedUserId, giftId) { const user = await this.getFalukantUserByHashedId(hashedUserId); const lowestTitleOfNobility = await TitleOfNobility.findOne({ order: [['id', 'ASC']], }); const relation = Relationship.findOne({ where: { character1Id: user.character.id, }, include: [ { model: RelationshipType, as: 'relationshipType', where: { tr: 'wooing' }, } ], }); if (!relation) { throw new Error('User and character are not related'); } console.log(user); const gift = await PromotionalGift.findOne({ where: { id: giftId }, include: [ { model: PromotionalGiftCharacterTrait, as: 'characterTraits', where: { trait_id: { [Op.in]: user.character.characterTraits.map(trait => trait.id) }, }, }, { model: PromotionalGiftMood, as: 'promotionalgiftmoods', }, ] }); const cost = await this.getGiftCost(gift.value, user.character.titleOfNobility, lowestTitleOfNobility.id); if (user.money < cost) { console.log(user, user.money, cost); throw new Error('Not enough money to send the gift'); } console.log(JSON.stringify(gift)); const changeValue = gift.characterTraits.suitability + gift.promotionalgiftmoods.suitability - 4; this.updateFalukantUserMoney(user.id, -cost, 'Gift cost', user.id); await relation.update({ value: relation.value + changeValue }); await PromotionalGiftLog.create({ senderCharacterId: user.character.id, recipientCharacterId: relation.character2Id, giftId: giftId, changeValue: changeValue, }); return { success: true, message: 'Gift sent' }; } 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 } } export default new FalukantService();