2288 lines
89 KiB
JavaScript
2288 lines
89 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';
|
|
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';
|
|
|
|
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 }
|
|
];
|
|
|
|
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', 'health']
|
|
},
|
|
{
|
|
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'],
|
|
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: ['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),
|
|
},
|
|
include: [
|
|
{
|
|
model: TitleOfNobility,
|
|
as: 'nobleTitle',
|
|
attributes: ['level'],
|
|
},
|
|
]
|
|
},
|
|
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.nobleTitle.level * 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 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,
|
|
});
|
|
}
|
|
}
|
|
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,
|
|
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) {
|
|
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 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');
|
|
}
|
|
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']
|
|
}],
|
|
}
|
|
]
|
|
});
|
|
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 (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);
|
|
}
|
|
|
|
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) {
|
|
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 = 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);
|
|
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);
|
|
}
|
|
}
|
|
|
|
export default new FalukantService();
|