diff --git a/backend/controllers/falukantController.js b/backend/controllers/falukantController.js index 7169d3c..cd3ddd2 100644 --- a/backend/controllers/falukantController.js +++ b/backend/controllers/falukantController.js @@ -25,6 +25,13 @@ class FalukantController { this.acceptMarriageProposal = this.acceptMarriageProposal.bind(this); this.getGifts = this.getGifts.bind(this); this.sendGift = this.sendGift.bind(this); + this.getHouseTypes = this.getHouseTypes.bind(this); + this.getTitelsOfNobility = this.getTitelsOfNobility.bind(this); + this.getMoodAffect = this.getMoodAffect.bind(this); + this.getCharacterAffect = this.getCharacterAffect.bind(this); + this.getUserHouse = this.getUserHouse.bind(this); + this.getBuyableHouses = this.getBuyableHouses.bind(this); + this.buyUserHouse = this.buyUserHouse.bind(this); } async getUser(req, res) { @@ -368,16 +375,17 @@ class FalukantController { async sendGift(req, res) { try { - const { userid: hashedUserId } = req.headers; - const { giftId} = req.body; - const result = await FalukantService.sendGift(hashedUserId, giftId); - res.status(200).json(result); + const { userid: hashedUserId } = req.headers; + const { giftId } = req.body; + const result = await FalukantService.sendGift(hashedUserId, giftId); + res.status(200).json(result); } catch (error) { - res.status(500).json({ error: error.message }); - console.log(error); + const status = error.status === 412 ? 412 : 500; + res.status(status).json({ error: error.message }); + console.error(error); } - } - + } + async getTitelsOfNobility(req, res) { try { const { userid: hashedUserId } = req.headers; @@ -399,6 +407,63 @@ class FalukantController { console.log(error); } } + + async getMoodAffect(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const result = await FalukantService.getMoodAffect(hashedUserId); + res.status(200).json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + console.log(error); + } + } + + async getCharacterAffect(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const result = await FalukantService.getCharacterAffect(hashedUserId); + res.status(200).json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + console.log(error); + } + } + + async getUserHouse(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const result = await FalukantService.getUserHouse(hashedUserId); + console.log(result); + res.status(200).json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + console.log(error); + } + } + + async getBuyableHouses(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const result = await FalukantService.getBuyableHouses(hashedUserId); + res.status(200).json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + console.log(error); + } + } + + async buyUserHouse(req, res) { + try { + const { userid: hashedUserId } = req.headers; + const { houseId } = req.body; + const result = await FalukantService.buyUserHouse(hashedUserId, houseId); + res.status(201).json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + console.log(error); + } + } } export default FalukantController; diff --git a/backend/models/associations.js b/backend/models/associations.js index 75877ca..8dd4b42 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -62,6 +62,9 @@ import PromotionalGiftMood from './falukant/predefine/promotional_gift_mood.js'; import RelationshipType from './falukant/type/relationship.js'; import Relationship from './falukant/data/relationship.js'; import PromotionalGiftLog from './falukant/log/promotional_gift.js'; +import HouseType from './falukant/type/house.js'; +import BuyableHouse from './falukant/data/buyable_house.js'; +import UserHouse from './falukant/data/user_house.js'; export default function setupAssociations() { // UserParam related associations @@ -352,4 +355,16 @@ export default function setupAssociations() { PromotionalGiftCharacterTrait.belongsTo(PromotionalGift, { foreignKey: 'gift_id', as: 'promotionalgiftcharactertrait' }); PromotionalGiftMood.belongsTo(PromotionalGift, { foreignKey: 'gift_id', as: 'promotionalgiftcharactermood' }); + + HouseType.hasMany(BuyableHouse, { foreignKey: 'houseTypeId', as: 'buyableHouses' }); + BuyableHouse.belongsTo(HouseType, { foreignKey: 'houseTypeId', as: 'houseType' }); + + HouseType.hasMany(UserHouse, { foreignKey: 'houseTypeId', as: 'userHouses' }); + UserHouse.belongsTo(HouseType, { foreignKey: 'houseTypeId', as: 'houseType' }); + + FalukantUser.hasOne(UserHouse, { foreignKey: 'userId', as: 'userHouse' }); + UserHouse.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'houseUser' }); + + TitleOfNobility.hasMany(HouseType, { foreignKey: 'minimumNobleTitle', as: 'houseTypes' }); + HouseType.belongsTo(TitleOfNobility, { foreignKey: 'minimumNobleTitle', as: 'titleOfNobility' }); } diff --git a/backend/models/falukant/data/buyable_house.js b/backend/models/falukant/data/buyable_house.js new file mode 100644 index 0000000..2797080 --- /dev/null +++ b/backend/models/falukant/data/buyable_house.js @@ -0,0 +1,40 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; + +class BuyableHouse extends Model { } + +BuyableHouse.init({ + roofCondition: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 100 + }, + floorCondition: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 100 + }, + wallCondition: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 100 + }, + windowCondition: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 100 + }, + houseTypeId: { + type: DataTypes.INTEGER, + allowNull: false, + } +}, { + sequelize, + modelName: 'BuyableHouse', + tableName: 'buyable_house', + schema: 'falukant_data', + timestamps: false, + underscored: true, +}); + +export default BuyableHouse; diff --git a/backend/models/falukant/data/user_house.js b/backend/models/falukant/data/user_house.js new file mode 100644 index 0000000..2004b85 --- /dev/null +++ b/backend/models/falukant/data/user_house.js @@ -0,0 +1,46 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; + +class UserHouse extends Model { } + +UserHouse.init({ + roofCondition: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 100 + }, + floorCondition: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 100 + }, + wallCondition: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 100 + }, + windowCondition: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 100 + }, + houseTypeId: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 1 + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 1 + } +}, { + sequelize, + modelName: 'UserHouse', + tableName: 'user_house', + schema: 'falukant_data', + timestamps: false, + underscored: true, +}); + +export default UserHouse; diff --git a/backend/models/falukant/type/house.js b/backend/models/falukant/type/house.js new file mode 100644 index 0000000..6c1ec75 --- /dev/null +++ b/backend/models/falukant/type/house.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../../../utils/sequelize.js'; + +class HouseType extends Model { } + +HouseType.init({ + labelTr: { + type: DataTypes.STRING, + allowNull: false, + }, + cost: { + type: DataTypes.INTEGER, + allowNull: false, + }, + position: { + type: DataTypes.INTEGER, + allowNull: false, + }, + minimumNobleTitle: { + type: DataTypes.INTEGER, + allowNull: false, + }, +}, { + sequelize, + modelName: 'HouseType', + tableName: 'house', + schema: 'falukant_type', + timestamps: false, + underscored: true, + indexes: [ + { + unique: true, + fields: ['label_tr'] + } + ], +}); + +export default HouseType; diff --git a/backend/models/index.js b/backend/models/index.js index 8928ab6..9c2f268 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -66,6 +66,9 @@ import PromotionalGiftCharacterTrait from './falukant/predefine/promotional_gift import PromotionalGiftMood from './falukant/predefine/promotional_gift_mood.js'; import Relationship from './falukant/data/relationship.js'; import PromotionalGiftLog from './falukant/log/promotional_gift.js'; +import HouseType from './falukant/type/house.js'; +import BuyableHouse from './falukant/data/buyable_house.js'; +import UserHouse from './falukant/data/user_house.js'; const models = { SettingsType, @@ -136,6 +139,9 @@ const models = { PromotionalGiftCharacterTrait, PromotionalGiftMood, PromotionalGiftLog, + HouseType, + BuyableHouse, + UserHouse, }; export default models; diff --git a/backend/routers/falukantRouter.js b/backend/routers/falukantRouter.js index 83d15ca..0472b81 100644 --- a/backend/routers/falukantRouter.js +++ b/backend/routers/falukantRouter.js @@ -36,5 +36,9 @@ router.post('/family/gift', falukantController.sendGift); router.get('/family', falukantController.getFamily); router.get('/nobility/titels', falukantController.getTitelsOfNobility); router.get('/houses/types', falukantController.getHouseTypes); - +router.get('/houses/buyable', falukantController.getBuyableHouses); +router.get('/mood/affect', falukantController.getMoodAffect); +router.get('/character/affect', falukantController.getCharacterAffect); +router.get('/houses', falukantController.getUserHouse); +router.post('/houses', falukantController.buyUserHouse); export default router; diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index d5418e7..892ee61 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -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(); diff --git a/backend/utils/falukant/initializeFalukantTypes.js b/backend/utils/falukant/initializeFalukantTypes.js index 1d89894..8621c03 100644 --- a/backend/utils/falukant/initializeFalukantTypes.js +++ b/backend/utils/falukant/initializeFalukantTypes.js @@ -6,6 +6,8 @@ import CharacterTrait from "../../models/falukant/type/character_trait.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 HouseType from '../../models/falukant/type/house.js'; +import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js"; export const initializeFalukantTypes = async () => { await initializeFalukantTypeRegions(); @@ -14,6 +16,7 @@ export const initializeFalukantTypes = async () => { await initializeFalukantCharacterTraits(); await initializeFalukantPromotionalGifts(); await initializePromotionalGiftMoodLinks(); + await initializeFalukantHouseTypes(); }; const regionTypes = []; @@ -204,6 +207,52 @@ const promotionalGiftMoodLinks = [ { gift: "Horse", mood: "nervous", suitability: 4 }, ]; +const houseTypes = [ + { labelTr: 'Unter der Brücke', abbr: 'under_bridge', cost: 10, position: 1, minimumTitle: 'noncivil' }, + { labelTr: 'Strohhütte', abbr: 'straw_hut', cost: 20, position: 2, minimumTitle: 'noncivil' }, + { labelTr: 'Holzhaus', abbr: 'wooden_house', cost: 50, position: 3, minimumTitle: 'civil' }, + { labelTr: 'Hinterhofzimmer', abbr: 'backyard_room', cost: 5, position: 4, minimumTitle: 'civil' }, + { labelTr: 'Kleines Familienhaus', abbr: 'family_house', cost: 100, position: 5, minimumTitle: 'sir' }, + { labelTr: 'Stadthaus', abbr: 'townhouse', cost: 200, position: 6, minimumTitle: 'townlord' }, + { labelTr: 'Villa', abbr: 'villa', cost: 500, position: 7, minimumTitle: 'knight' }, + { labelTr: 'Herrenhaus', abbr: 'mansion', cost: 1000, position: 8, minimumTitle: 'ruler' }, + { labelTr: 'Schloss', abbr: 'castle', cost: 5000, position: 9, minimumTitle: 'prince-regent' }, +]; + +{ + const giftNames = promotionalGifts.map(g => g.name); + const traitNames = characterTraits.map(t => t.name); + + giftNames.forEach(giftName => { + traitNames.forEach(traitName => { + if (!promotionalGiftTraitLinks.some(l => l.gift === giftName && l.trait === traitName)) { + promotionalGiftTraitLinks.push({ + gift: giftName, + trait: traitName, + suitability: Math.floor(Math.random() * 5) + 1, + }); + } + }); + }); +} + +{ + const giftNames = promotionalGifts.map(g => g.name); + const moodNames = moods.map(m => m.name); + + giftNames.forEach(giftName => { + moodNames.forEach(moodName => { + if (!promotionalGiftMoodLinks.some(l => l.gift === giftName && l.mood === moodName)) { + promotionalGiftMoodLinks.push({ + gift: giftName, + mood: moodName, + suitability: Math.floor(Math.random() * 5) + 1, + }); + } + }); + }); +} + const initializeFalukantTypeRegions = async () => { for (const regionType of regionTypeTrs) { const [regionTypeRecord] = await RegionType.findOrCreate({ @@ -303,6 +352,8 @@ export const initializePromotionalGiftTraitLinks = async () => { }, defaults: { suitability: link.suitability, + gift_id: gift.id, + trait_id: trait.id, }, }); } @@ -316,15 +367,27 @@ export const initializePromotionalGiftMoodLinks = async () => { console.error(`Gift or Mood not found for: ${link.gift}, ${link.mood}`); continue; } - await PromotionalGiftMood.findOrCreate({ - where: { - gift_id: gift.id, - mood_id: mood.id, - }, - defaults: { - suitability: link.suitability, - }, + + await PromotionalGiftMood.create({ + gift_id: gift.id, + mood_id: mood.id, + suitability: link.suitability, + }).catch(err => { + if (err.name !== 'SequelizeUniqueConstraintError') throw err; }); } }; +export const initializeFalukantHouseTypes = async () => { + for (const ht of houseTypes) { + const [record, created] = await HouseType.findOrCreate({ + where: { labelTr: ht.abbr }, + defaults: { + cost: ht.cost, + imageUrl: null, + position: ht.position, + minimumNobleTitle: await TitleOfNobility.findOne({ where: { labelTr: ht.minimumTitle } }).then(title => title.id), + } + }); + } +}; diff --git a/dump.rdb b/dump.rdb index b057372..7c978bd 100644 Binary files a/dump.rdb and b/dump.rdb differ diff --git a/frontend/public/images/falukant/houses.png b/frontend/public/images/falukant/houses.png new file mode 100644 index 0000000..f685a46 Binary files /dev/null and b/frontend/public/images/falukant/houses.png differ diff --git a/frontend/src/components/falukant/StatusBar.vue b/frontend/src/components/falukant/StatusBar.vue index cb59efb..e7de2f6 100644 --- a/frontend/src/components/falukant/StatusBar.vue +++ b/frontend/src/components/falukant/StatusBar.vue @@ -70,7 +70,7 @@ export default { } this.statusItems = [ { key: "age", icon: "👶", value: age }, - { key: "wealth", icon: "💰", value: money }, + { key: "wealth", icon: "💰", value: Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(money) }, { key: "health", icon: "❤️", value: healthStatus }, { key: "events", icon: "📰", value: events || null }, ]; @@ -80,12 +80,15 @@ export default { }, async handleDaemonSocketMessage(event) { try { + if (event.data === 'ping') { + return; + } const data = JSON.parse(event.data); if (data.event === "falukantUpdateStatus") { this.fetchStatus(); } } catch (error) { - console.error("Error parsing daemonSocket message:", error); + console.error("Error parsing daemonSocket message:", error, event.data); } }, openPage(url, hasSubmenu = false) { diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index 7a64ef9..c98ba44 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -211,8 +211,22 @@ "accept": "Werbung mit diesem Partner starten", "wooing": { "gifts": "Werbegeschenke", - "sendGift": "Werbegeschenk senden" - } + "sendGift": "Werbegeschenk senden", + "gift": "Geschenk", + "value": "Kosten", + "effect": "Wirkung" + }, + "giftAffect": { + "0": "Keiner", + "1": "Sehr niedrig", + "2": "Niedrig", + "3": "Mittel", + "4": "Hoch", + "5": "Sehr hoch" + + }, + "mood": "Stimmung", + "progress": "Zuneigung" }, "relationships": { "name": "Name" @@ -242,6 +256,15 @@ "addSpouse": "Ehepartner hinzufügen", "viewDetails": "Details anzeigen", "remove": "Entfernen" + }, + "sendgift": { + "error": { + "nogiftselected": "Bitte wähle ein Geschenk aus.", + "generic": "Ein unbekannter Fehler ist aufgetreten.", + "tooOften": "Du kannst nicht so oft Geschenke machen.", + "insufficientFunds": "Du hast nicht genug Geld." + }, + "success": "Das Geschenk wurde überreicht." } }, "product": { @@ -291,9 +314,15 @@ "changeValue": "Wertänderung", "time": "Zeit", "activities": { - "Product sale": "Produktverkauf", + "Product sale": "Produkte verkauft", "Production cost": "Produktionskosten", - "Sell all products": "Alle Produkte verkaufen" + "Sell all products": "Alle Produkte verkauft", + "sell products": "Produkte verkauft", + "director starts production": "Direktor beginnt Produktion", + "Buy storage (type: field)": "Lagerplatz gekauft (Typ: Feld)", + "Buy storage (type: iron)": "Lagerplatz gekauft (Typ: Eisen)", + "Buy storage (type: stone)": "Lagerplatz gekauft (Typ: Stein)", + "Buy storage (type: wood)": "Lagerplatz gekauft (Typ: Holz)" } }, "newdirector": { @@ -327,6 +356,63 @@ "Cat": "Katze", "Dog": "Hund", "Horse": "Pferd" + }, + "mood": { + "happy": "Glücklich", + "sad": "Traurig", + "angry": "Wütend", + "scared": "Verängstigt", + "surprised": "Überrascht", + "normal": "Normal" + }, + "character": { + "brave": "Mutig", + "kind": "Freundlich", + "greedy": "Gierig", + "wise": "Weise", + "loyal": "Loyal", + "cunning": "Listig", + "generous": "Großzügig", + "arrogant": "Arrogant", + "honest": "Ehrlich", + "ambitious": "Ehrgeizig", + "patient": "Geduldig", + "impatient": "Ungeduldig", + "selfish": "Egoistisch", + "charismatic": "Charismatisch", + "empathetic": "Einfühlsam", + "timid": "Schüchtern", + "stubborn": "Stur", + "resourceful": "Einfallsreich", + "reckless": "Rücksichtslos", + "disciplined": "Diszipliniert", + "optimistic": "Optimistisch", + "pessimistic": "Pessimistisch", + "manipulative": "Manipulativ", + "independent": "Unabhängig", + "dependent": "Abhängig", + "adventurous": "Abenteuerlustig", + "humble": "Bescheiden", + "vengeful": "Rachsüchtig", + "pragmatic": "Pragmatisch", + "idealistic": "Idealistisch" + }, + "house": { + "title": "Haus", + "statusreport": "Zustand des Hauses", + "element": "Bereich", + "state": "Zustand", + "buyablehouses": "Kaufe ein Haus", + "buy": "Kaufen", + "price": "Kaufpreis", + "worth": "Restwert", + "sell": "Verkaufen", + "status": { + "roofCondition": "Dach", + "wallCondition": "Wände", + "floorCondition": "Böden", + "windowCondition": "Fenster" + } } } } \ No newline at end of file diff --git a/frontend/src/router/falukantRoutes.js b/frontend/src/router/falukantRoutes.js index dfd8c71..29e639c 100644 --- a/frontend/src/router/falukantRoutes.js +++ b/frontend/src/router/falukantRoutes.js @@ -3,6 +3,7 @@ import Createview from '../views/falukant/CreateView.vue'; import FalukantOverviewView from '../views/falukant/OverviewView.vue'; import MoneyHistoryView from '../views/falukant/MoneyHistoryView.vue'; import FamilyView from '../views/falukant/FamilyView.vue'; +import HouseView from '../views/falukant/HouseView.vue'; const falukantRoutes = [ { @@ -35,6 +36,12 @@ const falukantRoutes = [ component: FamilyView, meta: { requiresAuth: true } }, + { + path: '/falukant/house', + name: 'HouseView', + component: HouseView, + meta: { requiresAuth: true }, + }, ]; export default falukantRoutes; diff --git a/frontend/src/views/falukant/FamilyView.vue b/frontend/src/views/falukant/FamilyView.vue index 06d4161..b59603f 100644 --- a/frontend/src/views/falukant/FamilyView.vue +++ b/frontend/src/views/falukant/FamilyView.vue @@ -3,31 +3,50 @@
-

{{ $t('falukant.family.title') }}

-

{{ $t('falukant.family.spouse.title') }}

- - - - - - - - - - - - - -
{{ $t('falukant.family.relationships.name') }} - {{ $t('falukant.titles.' + relationships[0].character2.gender + '.' + - relationships[0].character2.nobleTitle) }} - {{ relationships[0].character2.firstName }} -
{{ $t('falukant.family.spouse.age') }}{{ relationships[0].character2.age }}
{{ $t('falukant.family.spouse.status') }}{{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}
+
+ + + + + + + + + + + + + + + + + + + + + +
{{ $t('falukant.family.relationships.name') }} + {{ $t('falukant.titles.' + relationships[0].character2.gender + '.' + + relationships[0].character2.nobleTitle) }} + {{ relationships[0].character2.firstName }} +
{{ $t('falukant.family.spouse.age') }}{{ relationships[0].character2.age }}
{{ $t('falukant.family.spouse.mood') }}{{ $t(`falukant.mood.${relationships[0].character2.mood.tr}`) }}
{{ $t('falukant.family.spouse.status') }}{{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}
{{ $t('falukant.family.spouse.progress') }} +
+
+
+
+
    +
  • {{ $t(`falukant.character.${characteristic.tr}`) }}
  • +
+

{{ $t('falukant.family.spouse.wooing.gifts') }}

@@ -35,6 +54,7 @@ + @@ -42,12 +62,14 @@ +
{{ $t('falukant.family.spouse.wooing.gift') }}{{ $t('falukant.family.spouse.wooing.effect') }} {{ $t('falukant.family.spouse.wooing.value') }}
{{ $t(`falukant.gifts.${gift.name}`) }}{{ $t(`falukant.family.spouse.giftAffect.${getEffect(gift)}`) }} {{ formatCost(gift.cost) }}
- +
@@ -67,7 +89,7 @@ v-model="selectedProposalId"> {{ $t(`falukant.titles.${proposal.proposedCharacterGender}.${proposal.proposedCharacterNobleTitle}`) - }} {{ proposal.proposedCharacterName }} + }} {{ proposal.proposedCharacterName }} {{ proposal.proposedCharacterAge }} {{ formatCost(proposal.cost) }} @@ -80,7 +102,6 @@
-

{{ $t('falukant.family.children.title') }}

@@ -131,9 +152,6 @@
- - - @@ -284,4 +356,28 @@ export default { h2 { padding-top: 20px; } + +.relationship>table, +.relationship>ul { + display: inline-block; + margin-right: 1em; + vertical-align: top; +} + +.relationship>ul { + list-style: none; +} + +.progress { + width: 100%; + background-color: #e5e7eb; + border-radius: 0.25rem; + overflow: hidden; + height: 1rem; +} + +.progress-inner { + height: 100%; + transition: width 0.3s ease, background-color 0.3s ease; +} \ No newline at end of file diff --git a/frontend/src/views/falukant/HouseView.vue b/frontend/src/views/falukant/HouseView.vue new file mode 100644 index 0000000..4165723 --- /dev/null +++ b/frontend/src/views/falukant/HouseView.vue @@ -0,0 +1,259 @@ + + + + diff --git a/frontend/src/views/falukant/OverviewView.vue b/frontend/src/views/falukant/OverviewView.vue index 736c95e..f42d0da 100644 --- a/frontend/src/views/falukant/OverviewView.vue +++ b/frontend/src/views/falukant/OverviewView.vue @@ -91,6 +91,7 @@
+
@@ -168,6 +169,24 @@ export default { height: `${height}px`, }; }, + getHouseStyle() { + if (!this.falukantUser) return {}; + const imageUrl = '/images/falukant/houses.png'; + const housePosition = this.falukantUser.house ? this.falukantUser.house.type.position : 0; + const x = housePosition % 3; + const y = Math.floor(housePosition / 3); + return { + backgroundImage: `url(${imageUrl})`, + backgroundPosition: `-${x * 341}px -${y * 341}px`, + backgroundSize: "341px 341px", + width: "114px", + height: "114px", + }; + }, + getAgeColor(age) { + const ageGroup = this.getAgeGroup(age); + return ageGroup === 'child' ? 'blue' : ageGroup === 'teen' ? 'green' : ageGroup === 'adult' ? 'red' : 'gray'; + }, moneyValue() { const m = this.falukantUser?.money; return typeof m === 'string' ? parseFloat(m) : m; @@ -298,7 +317,16 @@ export default { image-rendering: crisp-edges; } +.house { + border: 1px solid #ccc; + border-radius: 4px; + background-repeat: no-repeat; + background-size: cover; + image-rendering: crisp-edges; +} + h2 { padding-top: 20px; } +