Falukant production, family and administration enhancements

This commit is contained in:
Torsten Schulz
2025-04-14 15:17:35 +02:00
parent 90b4f51dcb
commit b15d93a798
77 changed files with 2429 additions and 1093 deletions

View File

@@ -1,5 +1,5 @@
import BaseService from './BaseService.js';
import { Sequelize, Op } from 'sequelize';
import { Sequelize, Op, where } from 'sequelize';
import FalukantPredefineFirstname from '../models/falukant/predefine/firstname.js';
import FalukantPredefineLastname from '../models/falukant/predefine/lastname.js';
@@ -25,7 +25,14 @@ 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);
@@ -53,11 +60,33 @@ function calcSellPrice(product, knowledgeFactor = 0) {
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) {
return FalukantUser.findOne({
include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { 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) {
@@ -137,7 +166,7 @@ class FalukantService extends BaseService {
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({ userId: falukantUser.id, regionId: region.id, branchTypeId: bType.id });
await Branch.create({ falukantUserId: falukantUser.id, regionId: region.id, branchTypeId: bType.id });
notifyUser(user.hashedId, 'reloadmenu', {});
return falukantUser;
}
@@ -209,7 +238,12 @@ class FalukantService extends BaseService {
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);
@@ -293,7 +327,7 @@ class FalukantService extends BaseService {
const stock = await FalukantStock.findOne({ where: { branchId: branch.id } });
if (!stock) throw new Error('Stock not found');
const inventory = await Inventory.findAll({
where: { stockId: stock.id, quality },
where: { quality },
include: [
{
model: ProductType,
@@ -311,8 +345,12 @@ class FalukantService extends BaseService {
}
]
});
if (!inventory.length) throw new Error('No inventory found');
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;
@@ -330,7 +368,7 @@ class FalukantService extends BaseService {
break;
}
}
await this.addSellItem(branchId, falukantUser.id, productId, quantity);
await this.addSellItem(branchId, user.id, productId, quantity);
notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
return { success: true };
@@ -384,7 +422,7 @@ class FalukantService extends BaseService {
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[0].branch[0].id, falukantUser.id, item.productType.id, item.quantity);
await this.addSellItem(item.stock.branch.id, falukantUser.id, item.productType.id, item.quantity);
}
const moneyResult = await updateFalukantUserMoney(
falukantUser.id,
@@ -397,7 +435,7 @@ class FalukantService extends BaseService {
await Inventory.destroy({ where: { id: item.id } });
}
notifyUser(falukantUser.user.hashedId, 'falukantUpdateStatus', {});
notifyUser(falukantUser.user.hashedId, 'falukantBranchUpdate', {});
notifyUser(falukantUser.user.hashedId, 'falukantBranchUpdate', { branchId });
return { success: true, revenue: total };
}
@@ -408,7 +446,7 @@ class FalukantService extends BaseService {
;
const daySell = await DaySell.findOne({
where: {
regionId: regionId,
regionId: branch.regionId,
productId: productId,
sellerId: userId,
}
@@ -418,7 +456,7 @@ class FalukantService extends BaseService {
await daySell.save();
} else {
await DaySell.create({
regionId: regionId,
regionId: branch.regionId,
productId: productId,
sellerId: userId,
quantity: quantity,
@@ -528,13 +566,22 @@ class FalukantService extends BaseService {
if (!moneyResult.success) throw new Error('Failed to update money');
buyable.quantity -= amount;
await buyable.save();
const stock = await FalukantStock.findOne({
let stock = await FalukantStock.findOne({
where: { branchId: branch.id, stockTypeId },
include: [{ model: FalukantStockType, as: 'stockType' }]
});
if (!stock) throw new Error('No stock record found for this branch and 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 };
}
@@ -775,10 +822,13 @@ class FalukantService extends BaseService {
}
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 },
@@ -787,10 +837,12 @@ class FalukantService extends BaseService {
]
}
);
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
@@ -808,6 +860,7 @@ class FalukantService extends BaseService {
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,
@@ -827,11 +880,13 @@ class FalukantService extends BaseService {
},
]
});
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;
}
@@ -932,59 +987,268 @@ class FalukantService extends BaseService {
return { result: 'ok' };
}
async getMarriageProposals(hashedUserId) {
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 midnight = new Date();
midnight.setHours(0, 0, 0, 0);
await MarriageProposal.destroy({
where: {
[Op.or]: [
{ requesterCharacterId: character.id },
{ proposedCharacterId: character.id },
],
createdAt: {
[Op.lt]: midnight,
},
},
});
let proposals = await MarriageProposal.findAll({
where: {
[Op.or]: [
{ requesterCharacterId: character.id },
{ proposedCharacterId: character.id },
],
},
});
if (proposals.length === 0) {
const proposalCount = Math.floor(Math.random() * 4) + 3; // 36
const thirteenDaysAgo = new Date(Date.now() - 13 * 24 * 60 * 60 * 1000);
const possiblePartners = await FalukantCharacter.findAll({
const family = {
relationships: [],
deathPartners: [],
children: [],
lovers: [],
possiblePartners: [],
};
let relationships = await Relationship.findAll(
{
where: {
id: { [Op.ne]: character.id },
createdAt: { [Op.lt]: thirteenDaysAgo },
character1Id: character.id,
},
order: [sequelize.fn('RANDOM')],
});
if (possiblePartners.length === 0) {
return [];
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'],
},
],
}
const newProposals = [];
for (let i = 0; i < proposalCount; i++) {
const partner = possiblePartners[i % possiblePartners.length];
const createdProposal = await MarriageProposal.create({
requesterCharacterId: character.id,
proposedCharacterId: partner.id,
courtingProgress: 0,
});
newProposals.push(createdProposal);
);
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);
}
proposals = newProposals;
}
return proposals;
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
}
}