Implemented houses

This commit is contained in:
Torsten Schulz
2025-05-08 17:38:51 +02:00
parent b15d93a798
commit a9e6c82275
17 changed files with 1129 additions and 156 deletions

View File

@@ -33,6 +33,10 @@ import PromotionalGiftCharacterTrait from '../models/falukant/predefine/promotio
import PromotionalGiftMood from '../models/falukant/predefine/promotional_gift_mood.js';
import PromotionalGiftLog from '../models/falukant/log/promotional_gift.js';
import CharacterTrait from '../models/falukant/type/character_trait.js';
import Mood from '../models/falukant/type/mood.js';
import UserHouse from '../models/falukant/data/user_house.js';
import HouseType from '../models/falukant/type/house.js';
import BuyableHouse from '../models/falukant/data/buyable_house.js';
function calcAge(birthdate) {
const b = new Date(birthdate); b.setHours(0, 0);
@@ -67,6 +71,14 @@ function calculateMarriageCost(titleOfNobility, age) {
return baseCost * Math.pow(adjustedTitle, 1.3) - (age - 12) * 20;
}
class PreconditionError extends Error {
constructor(label) {
super(label);
this.name = 'PreconditionError';
this.status = 412;
}
}
class FalukantService extends BaseService {
async getFalukantUserByHashedId(hashedId) {
const user = await FalukantUser.findOne({
@@ -78,10 +90,22 @@ class FalukantService extends BaseService {
include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] },
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'id'] },
{ model: CharacterTrait, as: 'traits', attributes: ['id', 'tr'] }
],
attributes: ['id', 'birthdate', 'gender']
attributes: ['id', 'birthdate', 'gender', 'moodId']
},
{
model: UserHouse,
as: 'userHouse',
attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'],
include: [
{
model: HouseType,
as: 'houseType',
attributes: ['labelTr', 'position']
}
]
},
]
});
@@ -262,9 +286,7 @@ class FalukantService extends BaseService {
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}`);
}
@@ -348,9 +370,7 @@ class FalukantService extends BaseService {
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;
@@ -443,7 +463,7 @@ class FalukantService extends BaseService {
const branch = await Branch.findOne({
where: { id: branchId },
})
;
;
const daySell = await DaySell.findOne({
where: {
regionId: branch.regionId,
@@ -549,23 +569,47 @@ class FalukantService extends BaseService {
async buyStorage(hashedUserId, branchId, amount, stockTypeId) {
const user = await getFalukantUserOrFail(hashedUserId);
const branch = await getBranchOrFail(user.id, branchId);
const buyable = await BuyableStock.findOne({
const buyableStocks = await BuyableStock.findAll({
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;
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');
if (user.money < totalCost) {
throw new Error('notenoughmoney');
}
const moneyResult = await updateFalukantUserMoney(
user.id,
-totalCost,
`Buy storage (type: ${buyable.stockType.labelTr})`,
`Buy storage (type: ${buyableStocks[0].stockType.labelTr})`,
user.id
);
if (!moneyResult.success) throw new Error('Failed to update money');
buyable.quantity -= amount;
await buyable.save();
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' }]
@@ -576,13 +620,19 @@ class FalukantService extends BaseService {
stockTypeId,
quantity: amount,
});
return { success: true, bought: amount, totalCost, stockType: buyable.stockType.labelTr };
} else {
stock.quantity += amount;
await stock.save();
}
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 };
return {
success: true,
bought: amount,
totalCost,
stockType: buyableStocks[0].stockType.labelTr
};
}
async sellStorage(hashedUserId, branchId, amount, stockTypeId) {
@@ -725,7 +775,7 @@ class FalukantService extends BaseService {
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({
@@ -736,7 +786,7 @@ class FalukantService extends BaseService {
},
});
}
async fetchProposals(falukantUserId, regionId) {
return DirectorProposal.findAll({
where: { employerUserId: falukantUserId },
@@ -750,19 +800,19 @@ class FalukantService extends BaseService {
{ model: FalukantPredefineFirstname, as: 'definedFirstName' },
{ model: FalukantPredefineLastname, as: 'definedLastName' },
{ model: TitleOfNobility, as: 'nobleTitle' },
{
model: Knowledge,
{
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++) {
@@ -789,7 +839,7 @@ class FalukantService extends BaseService {
});
}
}
async calculateAverageKnowledge(characterId) {
const averageKnowledge = await Knowledge.findAll({
where: { characterId },
@@ -798,7 +848,7 @@ class FalukantService extends BaseService {
});
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));
@@ -820,47 +870,40 @@ 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 },
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
const existingDirector = await Director.findOne({
where: {
employerUserId: user.id
},
include: [
{
model: FalukantCharacter,
{
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,
@@ -871,8 +914,8 @@ class FalukantService extends BaseService {
employerUserId: proposal.employerUserId,
},
include: [
{
model: FalukantCharacter,
{
model: FalukantCharacter,
as: 'character',
where: {
regionId: proposal.character.regionId,
@@ -880,13 +923,11 @@ 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;
}
@@ -978,7 +1019,7 @@ class FalukantService extends BaseService {
}
const updateData = {};
updateData[settingKey] = value || false;
await Director.update(updateData, {
where: {
id: director.id,
@@ -1008,15 +1049,17 @@ class FalukantService extends BaseService {
where: {
character1Id: character.id,
},
attributes: ['createdAt', 'widowFirstName2'],
attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress'],
include: [
{
model: FalukantCharacter,
as: 'character2',
attributes: ['id', 'birthdate', 'gender'],
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' },
],
},
{
@@ -1030,22 +1073,28 @@ class FalukantService extends BaseService {
relationships = relationships.map((relationship) => ({
createdAt: relationship.createdAt,
widowFirstName2: relationship.widowFirstName2,
progress: relationship.nextStepProgress,
character2: {
id: relationship.character2.id,
age: calcAge(relationship.character2.birthdate),
gender: relationship.character2.gender,
firstName: relationship.character2.definedFirstName?.name || 'Unknown',
nobleTitle: relationship.character2.nobleTitle?.labelTr || '',
mood: relationship.character2.mood,
characterTrait: relationship.character2.traits,
},
relationshipType: relationship.relationshipType.tr,
}));
family.relationships = relationships.filter((relationship) => ['wooing', 'engaged', 'married'].includes(relationship.relationshipType));
family.lovers = relationships.filter((relationship) => ['lover'].includes(relationship.relationshipType.tr));
family.deathPartners = relationships.filter((relationship) => ['widowed'].includes(relationship.relationshipType.tr));
if (family.relationships.length === 0 ) {
const ownAge = calcAge(character.birthdate);
if (ownAge < 12) {
family.possiblePartners = [];
} else if (family.relationships.length === 0) {
family.possiblePartners = await this.getPossiblePartners(character.id);
if (family.possiblePartners.length === 0) {
await this.createPossiblePartners(character.id, character.gender, character.regionId, character.titleOfNobility);
await this.createPossiblePartners(character.id, character.gender, character.regionId, character.titleOfNobility, ownAge);
family.possiblePartners = await this.getPossiblePartners(character.id);
}
}
@@ -1073,7 +1122,6 @@ class FalukantService extends BaseService {
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,
@@ -1087,27 +1135,32 @@ class FalukantService extends BaseService {
};
});
}
async createPossiblePartners(requestingCharacterId, requestingCharacterGender, requestingRegionId, requestingCharacterTitleOfNobility) {
async createPossiblePartners(requestingCharacterId, requestingCharacterGender, requestingRegionId, requestingCharacterTitleOfNobility, ownAge) {
try {
const minTitleResult = await TitleOfNobility.findOne({
order: [['id', 'ASC']],
order: [['id', 'ASC']],
attributes: ['id'],
});
if (!minTitleResult) {
throw new Error('No title of nobility found');
}
const minTitle = minTitleResult.id;
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) },
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 {
@@ -1130,8 +1183,8 @@ class FalukantService extends BaseService {
throw new Error('User not found');
}
const proposal = await MarriageProposal.findOne({
where: {
requesterCharacterId: character.id,
where: {
requesterCharacterId: character.id,
proposedCharacterId: proposedCharacterId,
},
});
@@ -1139,7 +1192,6 @@ class FalukantService extends BaseService {
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);
@@ -1158,12 +1210,12 @@ class FalukantService extends BaseService {
relationshipTypeId: marriedType.id,
});
await MarriageProposal.destroy({
where: { character1Id: character.id },
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({
@@ -1172,7 +1224,20 @@ class FalukantService extends BaseService {
if (!character) {
throw new Error('Character not found');
}
let gifts = await PromotionalGift.findAll();
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']],
});
@@ -1181,61 +1246,107 @@ class FalukantService extends BaseService {
id: gift.id,
name: gift.name,
cost: await this.getGiftCost(gift.value, character.titleOfNobility, lowestTitleOfNobility.id),
moodsAffects: gift.promotionalgiftmoods,
charactersAffects: gift.characterTraits,
};
}));
}
async sendGift(hashedUserId, giftId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const lowestTitleOfNobility = await TitleOfNobility.findOne({
order: [['id', 'ASC']],
});
const relation = Relationship.findOne({
where: {
character1Id: user.character.id,
},
include: [
{
model: RelationshipType,
as: 'relationshipType',
where: { tr: 'wooing' },
}
],
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('User and character are not related');
throw new Error('notRelated');
}
console.log(user);
const gift = await PromotionalGift.findOne({
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.characterTraits.map(trait => trait.id) }, },
where: { trait_id: { [Op.in]: user.character.traits.map(t => t.id) } },
required: false
},
{
model: PromotionalGiftMood,
as: 'promotionalgiftmoods',
},
where: { mood_id: currentMoodId },
required: false
}
]
});
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');
if (!gift) {
throw new Error('notFound');
}
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 });
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: giftId,
changeValue: changeValue,
giftId,
changeValue
});
return { success: true, message: 'Gift sent' };
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) {
@@ -1248,8 +1359,114 @@ class FalukantService extends BaseService {
}
async getHouseTypes() {
// return House
// return House
}
async getMoodAffect() {
return PromotionalGiftMood.findAll();
}
async getCharacterAffect() {
return PromotionalGiftCharacterTrait.findAll();
}
async getUserHouse(hashedUserId) {
try {
const user = await User.findOne({
where: { hashedId: hashedUserId },
include: [{
model: FalukantUser,
as: 'falukantData',
include: [{
model: UserHouse,
as: 'userHouse',
include: [{
model: HouseType,
as: 'houseType',
attributes: ['position', 'cost']
}],
attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition']
}],
}
]
});
console.log(user.falukantData[0].userHouse);
return user.falukantData[0].userHouse ?? { position: 0, roofCondition: 100, wallCondition: 100, floorCondition: 100, windowCondition: 100 };
} catch (error) {
console.log(error);
return {};
}
}
async getBuyableHouses(hashedUserId) {
try {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const houses = await BuyableHouse.findAll({
include: [{
model: HouseType,
as: 'houseType',
attributes: ['position', 'cost'],
where: {
minimumNobleTitle: {
[Op.lte]: user.character.nobleTitle.id
}
}
}],
attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition', 'id'],
order: [
[{ model: HouseType, as: 'houseType' }, 'position', 'DESC'],
['wallCondition', 'DESC'],
['roofCondition', 'DESC'],
['floorCondition', 'DESC'],
['windowCondition', 'DESC']
]
});
return houses;
} catch (error) {
console.error('Fehler beim Laden der kaufbaren Häuser:', error);
throw error;
}
}
async buyUserHouse(hashedUserId, houseId) {
try {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const house = await BuyableHouse.findByPk(houseId, {
include: [{
model: HouseType,
as: 'houseType',
}],
});
if (!house) {
throw new Error('Das Haus wurde nicht gefunden.');
}
const housePrice = this.housePrice(house);
const oldHouse = await UserHouse.findOne({ where: { userId: falukantUser.id } });
if (falukantUser.money < housePrice) {
throw new Error('notenoughmoney.');
}
if (oldHouse) {
await oldHouse.destroy();
}
await UserHouse.create({
userId: falukantUser.id,
houseTypeId: house.houseTypeId,
});
await house.destroy();
await updateFalukantUserMoney(falukantUser.id, -housePrice, "housebuy", falukantUser.id);
const user = await User.findByPk(falukantUser.userId);
notifyUser(user.hashedId, 'falukantHouseUpdate', {});
return {};
} catch (error) {
console.error('Fehler beim Kaufen des Hauses:', error);
throw error;
}
}
housePrice(house) {
const houseQuality = (house.roofCondition + house.windowCondition + house.floorCondition + house.wallCondition) / 4;
return (house.houseType.cost / 100 * houseQuality ).toFixed(2);
}
}
export default new FalukantService();