Spiel erweitert

This commit is contained in:
Torsten Schulz
2025-06-02 11:26:45 +02:00
parent a9e6c82275
commit 5029be81e9
56 changed files with 4549 additions and 436 deletions

View File

@@ -20,6 +20,7 @@ class FalukantController {
this.getDirectorProposals = this.getDirectorProposals.bind(this);
this.convertProposalToDirector = this.convertProposalToDirector.bind(this);
this.getDirectorForBranch = this.getDirectorForBranch.bind(this);
this.getAllDirectors = this.getAllDirectors.bind(this);
this.setSetting = this.setSetting.bind(this);
this.getFamily = this.getFamily.bind(this);
this.acceptMarriageProposal = this.acceptMarriageProposal.bind(this);
@@ -32,6 +33,21 @@ class FalukantController {
this.getUserHouse = this.getUserHouse.bind(this);
this.getBuyableHouses = this.getBuyableHouses.bind(this);
this.buyUserHouse = this.buyUserHouse.bind(this);
this.getPartyTypes = this.getPartyTypes.bind(this);
this.createParty = this.createParty.bind(this);
this.getParties = this.getParties.bind(this);
this.getNotBaptisedChildren = this.getNotBaptisedChildren.bind(this);
this.baptise = this.baptise.bind(this);
this.getEducation = this.getEducation.bind(this);
this.getChildren = this.getChildren.bind(this);
this.sendToSchool = this.sendToSchool.bind(this);
this.getBankOverview = this.getBankOverview.bind(this);
this.getBankCredits = this.getBankCredits.bind(this);
this.takeBankCredits = this.takeBankCredits.bind(this);
this.getNobility = this.getNobility.bind(this);
this.advanceNobility = this.advanceNobility.bind(this);
this.getHealth = this.getHealth.bind(this);
this.healthActivity = this.healthActivity.bind(this);
}
async getUser(req, res) {
@@ -324,6 +340,27 @@ class FalukantController {
}
}
async getAllDirectors(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getAllDirectors(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async updateDirector(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { directorId, income } = req.body;
const result = await FalukantService.updateDirector(hashedUserId, directorId, income);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async setSetting(req, res) {
try {
const { userid: hashedUserId } = req.headers;
@@ -373,6 +410,17 @@ class FalukantController {
}
}
async getChildren(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getChildren(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async sendGift(req, res) {
try {
const { userid: hashedUserId } = req.headers;
@@ -464,6 +512,165 @@ class FalukantController {
console.log(error);
}
}
async getPartyTypes(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getPartyTypes(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async createParty(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
const result = await FalukantService.createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
res.status(201).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async getParties(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getParties(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async getNotBaptisedChildren(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getNotBaptisedChildren(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async baptise(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { characterId: childId, firstName } = req.body;
const result = await FalukantService.baptise(hashedUserId, childId, firstName);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async getEducation(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getEducation(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async sendToSchool(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { item, student, studentId } = req.body;
const result = await FalukantService.sendToSchool(hashedUserId, item, student, studentId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async getBankOverview(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getBankOverview(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async getBankCredits(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getBankCredits(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async takeBankCredits(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { height } = req.body;
const result = await FalukantService.takeBankCredits(hashedUserId, height);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async getNobility(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getNobility(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async advanceNobility(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.advanceNobility(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async getHealth(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const result = await FalukantService.getHealth(hashedUserId);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
async healthActivity(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const { measureTr: activity } = req.body;
const result = await FalukantService.healthActivity(hashedUserId, activity);
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
console.log(error);
}
}
}
export default FalukantController;

View File

@@ -105,6 +105,10 @@ const menuStructure = {
visible: ["hasfalukantaccount"],
path: "/falukant/nobility"
},
church: {
visible: ["hasfalukantaccount"],
path: "/falukant/church"
},
politics: {
visible: ["hasfalukantaccount"],
path: "/falukant/politics"

View File

@@ -65,6 +65,26 @@ 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';
import PartyType from './falukant/type/party.js';
import Party from './falukant/data/party.js';
import MusicType from './falukant/type/music.js';
import BanquetteType from './falukant/type/banquette.js';
import PartyInvitedNobility from './falukant/data/partyInvitedNobility.js';
import ChildRelation from './falukant/data/child_relation.js';
import Learning from './falukant/data/learning.js';
import LearnRecipient from './falukant/type/learn_recipient.js';
import Credit from './falukant/data/credit.js';
import DebtorsPrism from './falukant/data/debtors_prism.js';
import HealthActivity from './falukant/log/health_activity.js';
import Election from './falukant/data/election.js';
import ElectionResult from './falukant/data/election_result.js';
import Candidate from './falukant/data/candidate.js';
import Vote from './falukant/data/vote.js';
import PoliticalOfficeType from './falukant/type/political_office_type.js';
import PoliticalOffice from './falukant/data/political_office.js';
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
export default function setupAssociations() {
// UserParam related associations
@@ -315,31 +335,31 @@ export default function setupAssociations() {
Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' });
FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' });
MarriageProposal.belongsTo(FalukantCharacter, {foreignKey: 'requesterCharacterId', as: 'requesterCharacter', });
MarriageProposal.belongsTo(FalukantCharacter, { foreignKey: 'requesterCharacterId', as: 'requesterCharacter', });
FalukantCharacter.hasMany(MarriageProposal, { foreignKey: 'requesterCharacterId', as: 'initiatedProposals' });
MarriageProposal.belongsTo(FalukantCharacter, {foreignKey: 'proposedCharacterId', as: 'proposedCharacter', });
FalukantCharacter.hasMany(MarriageProposal, {foreignKey: 'proposedCharacterId', as: 'receivedProposals' });
MarriageProposal.belongsTo(FalukantCharacter, { foreignKey: 'proposedCharacterId', as: 'proposedCharacter', });
FalukantCharacter.hasMany(MarriageProposal, { foreignKey: 'proposedCharacterId', as: 'receivedProposals' });
FalukantCharacter.belongsToMany(CharacterTrait, {through: FalukantCharacterTrait, foreignKey: 'character_id', as: 'traits', });
CharacterTrait.belongsToMany(FalukantCharacter, {through: FalukantCharacterTrait, foreignKey: 'trait_id', as: 'characters', });
FalukantCharacter.belongsToMany(CharacterTrait, { through: FalukantCharacterTrait, foreignKey: 'character_id', as: 'traits', });
CharacterTrait.belongsToMany(FalukantCharacter, { through: FalukantCharacterTrait, foreignKey: 'trait_id', as: 'characters', });
Mood.hasMany(FalukantCharacter, {foreignKey: 'mood_id', as: 'moods'});
FalukantCharacter.belongsTo(Mood, {foreignKey: 'mood_id', as: 'mood'});
Mood.hasMany(FalukantCharacter, { foreignKey: 'mood_id', as: 'moods' });
FalukantCharacter.belongsTo(Mood, { foreignKey: 'mood_id', as: 'mood' });
PromotionalGift.belongsToMany(CharacterTrait, {through: PromotionalGiftCharacterTrait, foreignKey: 'gift_id', as: 'traits',});
CharacterTrait.belongsToMany(PromotionalGift, {through: PromotionalGiftCharacterTrait, foreignKey: 'trait_id', as: 'gifts',});
PromotionalGift.belongsToMany(CharacterTrait, { through: PromotionalGiftCharacterTrait, foreignKey: 'gift_id', as: 'traits', });
CharacterTrait.belongsToMany(PromotionalGift, { through: PromotionalGiftCharacterTrait, foreignKey: 'trait_id', as: 'gifts', });
PromotionalGift.belongsToMany(Mood, {through: PromotionalGiftMood, foreignKey: 'gift_id', as: 'moods',});
Mood.belongsToMany(PromotionalGift, {through: PromotionalGiftMood, foreignKey: 'mood_id', as: 'gifts',});
PromotionalGift.belongsToMany(Mood, { through: PromotionalGiftMood, foreignKey: 'gift_id', as: 'moods', });
Mood.belongsToMany(PromotionalGift, { through: PromotionalGiftMood, foreignKey: 'mood_id', as: 'gifts', });
Relationship.belongsTo(RelationshipType, { foreignKey: 'relationshipTypeId', as: 'relationshipType' });
RelationshipType.hasMany(Relationship, { foreignKey: 'relationshipTypeId', as: 'relationships' });
Relationship.belongsTo(FalukantCharacter, {foreignKey: 'character1Id', as: 'character1', });
Relationship.belongsTo(FalukantCharacter, {foreignKey: 'character2Id', as: 'character2', });
FalukantCharacter.hasMany(Relationship, {foreignKey: 'character1Id', as: 'relationshipsAsCharacter1', });
FalukantCharacter.hasMany(Relationship, {foreignKey: 'character2Id', as: 'relationshipsAsCharacter2', });
Relationship.belongsTo(FalukantCharacter, { foreignKey: 'character1Id', as: 'character1', });
Relationship.belongsTo(FalukantCharacter, { foreignKey: 'character2Id', as: 'character2', });
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character1Id', as: 'relationshipsAsCharacter1', });
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character2Id', as: 'relationshipsAsCharacter2', });
PromotionalGiftLog.belongsTo(PromotionalGift, { foreignKey: 'giftId', as: 'gift' });
PromotionalGift.hasMany(PromotionalGiftLog, { foreignKey: 'giftId', as: 'logs' });
@@ -367,4 +387,262 @@ export default function setupAssociations() {
TitleOfNobility.hasMany(HouseType, { foreignKey: 'minimumNobleTitle', as: 'houseTypes' });
HouseType.belongsTo(TitleOfNobility, { foreignKey: 'minimumNobleTitle', as: 'titleOfNobility' });
PartyType.hasMany(Party, { foreignKey: 'partyTypeId', as: 'parties' });
Party.belongsTo(PartyType, { foreignKey: 'partyTypeId', as: 'partyType' });
MusicType.hasMany(Party, { foreignKey: 'musicTypeId', as: 'parties' });
Party.belongsTo(MusicType, { foreignKey: 'musicTypeId', as: 'musicType' });
BanquetteType.hasMany(Party, { foreignKey: 'banquetteTypeId', as: 'parties' });
Party.belongsTo(BanquetteType, { foreignKey: 'banquetteTypeId', as: 'banquetteType' });
FalukantUser.hasMany(Party, { foreignKey: 'falukantUserId', as: 'parties' });
Party.belongsTo(FalukantUser, { foreignKey: 'falukantUserId', as: 'partyUser' });
Party.belongsToMany(TitleOfNobility, {
through: PartyInvitedNobility,
foreignKey: 'party_id',
otherKey: 'title_of_nobility_id',
as: 'invitedNobilities',
});
TitleOfNobility.belongsToMany(Party, {
through: PartyInvitedNobility,
foreignKey: 'title_of_nobility_id',
otherKey: 'party_id',
as: 'partiesInvitedTo',
});
ChildRelation.belongsTo(FalukantCharacter, {
foreignKey: 'fatherCharacterId',
as: 'father'
});
FalukantCharacter.hasMany(ChildRelation, {
foreignKey: 'fatherCharacterId',
as: 'childrenFather'
});
ChildRelation.belongsTo(FalukantCharacter, {
foreignKey: 'motherCharacterId',
as: 'mother'
});
FalukantCharacter.hasMany(ChildRelation, {
foreignKey: 'motherCharacterId',
as: 'childrenMother'
});
ChildRelation.belongsTo(FalukantCharacter, {
foreignKey: 'childCharacterId',
as: 'child'
});
FalukantCharacter.hasMany(ChildRelation, {
foreignKey: 'childCharacterId',
as: 'parentRelations'
});
Learning.belongsTo(LearnRecipient, {
foreignKey: 'learningRecipientId',
as: 'recipient'
}
);
LearnRecipient.hasMany(Learning, {
foreignKey: 'learningRecipientId',
as: 'learnings'
});
Learning.belongsTo(FalukantUser, {
foreignKey: 'associatedFalukantUserId',
as: 'learner'
}
);
FalukantUser.hasMany(Learning, {
foreignKey: 'associatedFalukantUserId',
as: 'learnings'
});
Learning.belongsTo(ProductType, {
foreignKey: 'productId',
as: 'productType'
});
ProductType.hasMany(Learning, {
foreignKey: 'productId',
as: 'learnings'
});
Learning.belongsTo(FalukantCharacter, {
foreignKey: 'associatedLearningCharacterId',
as: 'learningCharacter'
});
FalukantCharacter.hasMany(Learning, {
foreignKey: 'associatedLearningCharacterId',
as: 'learningsCharacter'
});
FalukantUser.hasMany(Credit, {
foreignKey: 'falukantUserId',
as: 'credits'
});
Credit.belongsTo(FalukantUser, {
foreignKey: 'falukantUserId',
as: 'user'
});
FalukantCharacter.hasMany(DebtorsPrism, {
foreignKey: 'character_id',
as: 'debtorsPrisms'
});
DebtorsPrism.belongsTo(FalukantCharacter, {
foreignKey: 'character_id',
as: 'character'
});
HealthActivity.belongsTo(FalukantCharacter, {
foreignKey: 'character_id',
as: 'character'
});
FalukantCharacter.hasMany(HealthActivity, {
foreignKey: 'character_id',
as: 'healthActivities'
});
// — Political Offices —
// predefine requirements for office
PoliticalOfficeRequirement.belongsTo(PoliticalOfficeType, {
foreignKey: 'officeTypeId',
as: 'officeType'
});
PoliticalOfficeType.hasMany(PoliticalOfficeRequirement, {
foreignKey: 'officeTypeId',
as: 'requirements'
});
// predefine benefits for office
PoliticalOfficeBenefit.belongsTo(
PoliticalOfficeBenefitType,
{ foreignKey: 'benefitTypeId', as: 'benefitDefinition' }
);
PoliticalOfficeBenefitType.hasMany(
PoliticalOfficeBenefit,
{ foreignKey: 'benefitTypeId', as: 'benefitDefinitions' }
);
// tie benefits back to office type
PoliticalOfficeBenefit.belongsTo(PoliticalOfficeType, {
foreignKey: 'officeTypeId',
as: 'officeType'
});
PoliticalOfficeType.hasMany(PoliticalOfficeBenefit, {
foreignKey: 'officeTypeId',
as: 'benefits'
});
// actual office holdings
PoliticalOffice.belongsTo(PoliticalOfficeType, {
foreignKey: 'officeTypeId',
as: 'type'
});
PoliticalOfficeType.hasMany(PoliticalOffice, {
foreignKey: 'officeTypeId',
as: 'offices'
});
PoliticalOffice.belongsTo(FalukantCharacter, {
foreignKey: 'characterId',
as: 'holder'
});
FalukantCharacter.hasMany(PoliticalOffice, {
foreignKey: 'characterId',
as: 'heldOffices'
});
// elections
Election.belongsTo(PoliticalOfficeType, {
foreignKey: 'officeTypeId',
as: 'officeType'
});
PoliticalOfficeType.hasMany(Election, {
foreignKey: 'officeTypeId',
as: 'elections'
});
// candidates in an election
Candidate.belongsTo(Election, {
foreignKey: 'electionId',
as: 'election'
});
Election.hasMany(Candidate, {
foreignKey: 'electionId',
as: 'candidates'
});
Candidate.belongsTo(FalukantCharacter, {
foreignKey: 'characterId',
as: 'character'
});
FalukantCharacter.hasMany(Candidate, {
foreignKey: 'characterId',
as: 'candidacies'
});
// votes cast
Vote.belongsTo(Election, {
foreignKey: 'electionId',
as: 'election'
});
Election.hasMany(Vote, {
foreignKey: 'electionId',
as: 'votes'
});
Vote.belongsTo(Candidate, {
foreignKey: 'candidateId',
as: 'candidate'
});
Candidate.hasMany(Vote, {
foreignKey: 'candidateId',
as: 'votes'
});
Vote.belongsTo(FalukantCharacter, {
foreignKey: 'voterCharacterId',
as: 'voter'
});
FalukantCharacter.hasMany(Vote, {
foreignKey: 'voterCharacterId',
as: 'votesCast'
});
// election results
ElectionResult.belongsTo(Election, {
foreignKey: 'electionId',
as: 'election'
});
Election.hasMany(ElectionResult, {
foreignKey: 'electionId',
as: 'results'
});
ElectionResult.belongsTo(Candidate, {
foreignKey: 'candidateId',
as: 'candidate'
});
Candidate.hasMany(ElectionResult, {
foreignKey: 'candidateId',
as: 'results'
});
PoliticalOffice.belongsTo(RegionData, {
foreignKey: 'regionId',
as: 'region'
});
RegionData.hasMany(PoliticalOffice, {
foreignKey: 'regionId',
as: 'offices'
});
}

View File

@@ -0,0 +1,30 @@
// falukant/data/candidate.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class Candidate extends Model {}
Candidate.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
election_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
character_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
sequelize,
modelName: 'Candidate',
tableName: 'candidate',
schema: 'falukant_data',
timestamps: true,
underscored: true,
});
export default Candidate;

View File

@@ -0,0 +1,45 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ChildRelation extends Model {}
ChildRelation.init(
{
fatherCharacterId: {
type: DataTypes.INTEGER,
allowNull: false,
},
motherCharacterId: {
type: DataTypes.INTEGER,
allowNull: false,
},
childCharacterId: {
type: DataTypes.INTEGER,
allowNull: false,
},
fatherName: {
type: DataTypes.STRING,
allowNull: false,
},
motherName: {
type: DataTypes.STRING,
allowNull: false,
},
nameSet: {
type: DataTypes.BOOLEAN,
allowNull: false,
default: false,
},
},
{
sequelize,
modelName: 'ChildRelation',
tableName: 'child_relation', // exakter Tabellenname
schema: 'falukant_data', // exaktes Schema
freezeTableName: true, // keine Pluralisierung
timestamps: true,
underscored: true,
}
);
export default ChildRelation;

View File

@@ -0,0 +1,37 @@
// models/falukant/data/credit.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class Credit extends Model {}
Credit.init({
// aufgenommener Kredit-Betrag
amount: {
type: DataTypes.DECIMAL(14,2),
allowNull: false,
},
// noch offener Kreditbetrag
remainingAmount: {
type: DataTypes.DECIMAL(14,2),
allowNull: false,
},
// Zinssatz als Prozentsatz (z.B. 3.5 für 3.5%)
interestRate: {
type: DataTypes.DECIMAL(5,2),
allowNull: false,
},
// Verknüpfung auf FalukantUser
falukantUserId: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
sequelize,
modelName: 'Credit',
tableName: 'credit',
schema: 'falukant_data',
timestamps: true,
underscored: true,
});
export default Credit;

View File

@@ -0,0 +1,21 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class DebtorsPrism extends Model {}
DebtorsPrism.init({
// Verknüpfung auf FalukantCharacter
characterId: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
sequelize,
modelName: 'DebtorsPrism',
tableName: 'debtors_prism',
schema: 'falukant_data',
timestamps: true,
underscored: true,
});
export default DebtorsPrism;

View File

@@ -0,0 +1,34 @@
// falukant/data/election.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class Election extends Model {}
Election.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
political_office_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
date: {
type: DataTypes.DATE,
allowNull: false,
},
posts_to_fill: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
sequelize,
modelName: 'Election',
tableName: 'election',
schema: 'falukant_data',
timestamps: true,
underscored: true,
});
export default Election;

View File

@@ -0,0 +1,34 @@
// falukant/data/election_result.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ElectionResult extends Model {}
ElectionResult.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
election_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
candidate_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
votes_received: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
sequelize,
modelName: 'ElectionResult',
tableName: 'election_result',
schema: 'falukant_data',
timestamps: true,
underscored: true,
});
export default ElectionResult;

View File

@@ -0,0 +1,48 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class Learning extends Model {}
Learning.init(
{
learningRecipientId: {
type: DataTypes.INTEGER,
allowNull: false,
},
productId: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null,
},
learnAllProducts: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
associatedFalukantUserId: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null,
},
associatedLearningCharacterId: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null,
},
learningIsExecuted: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
}
},
{
sequelize,
modelName: 'Learning',
tableName: 'learning',
schema: 'falukant_data',
timestamps: true,
underscored: true,
}
);
export default Learning;

View File

@@ -0,0 +1,30 @@
// falukant/data/occupied_political_office.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class OccupiedPoliticalOffice extends Model {}
OccupiedPoliticalOffice.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
political_office_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
character_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
sequelize,
modelName: 'OccupiedPoliticalOffice',
tableName: 'occupied_political_office',
schema: 'falukant_data',
timestamps: true,
underscored: true,
});
export default OccupiedPoliticalOffice;

View File

@@ -0,0 +1,46 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class Party extends Model {}
Party.init({
partyTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'party_type_id'
},
falukantUserId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'falukant_user_id'
},
musicTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'music_type'
},
banquetteTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'banquette_type'
},
servantRatio: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'servant_ratio'
},
cost: {
type: DataTypes.FLOAT,
allowNull: false,
defaultValue: 0
},
}, {
sequelize,
modelName: 'Party',
tableName: 'party',
schema: 'falukant_data',
timestamps: true,
underscored: true,
});
export default Party;

View File

@@ -0,0 +1,26 @@
import { Model, DataTypes } from 'sequelize'
import { sequelize } from '../../../utils/sequelize.js'
class PartyInvitedNobility extends Model {}
PartyInvitedNobility.init({
partyId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'party_id'
},
titleOfNobilityId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'title_of_nobility_id'
}
}, {
sequelize,
modelName: 'PartyInvitedNobility',
tableName: 'party_invited_nobility',
schema: 'falukant_data',
timestamps: false,
underscored: true
})
export default PartyInvitedNobility

View File

@@ -0,0 +1,34 @@
// backend/models/falukant/data/political_office.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class PoliticalOffice extends Model {}
PoliticalOffice.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
officeTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
},
characterId: {
type: DataTypes.INTEGER,
allowNull: false,
},
regionId: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
sequelize,
modelName: 'PoliticalOffice',
tableName: 'political_office',
schema: 'falukant_data',
timestamps: true,
underscored: true,
});
export default PoliticalOffice;

View File

@@ -0,0 +1,39 @@
// falukant/data/vote.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class Vote extends Model {}
Vote.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
election_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
voter_character_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
candidate_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
timestamp: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
}, {
sequelize,
modelName: 'Vote',
tableName: 'vote',
schema: 'falukant_data',
timestamps: false,
underscored: true,
});
export default Vote;

View File

@@ -0,0 +1,37 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class HealthActivity extends Model { }
HealthActivity.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
characterId: {
type: DataTypes.INTEGER,
allowNull: false,
},
activityTr: {
type: DataTypes.STRING,
allowNull: false,
},
cost: {
type: DataTypes.FLOAT,
allowNull: false,
},
successPercentage: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
sequelize,
modelName: 'health_activity',
tableName: 'health_activity',
schema: 'falukant_log',
timestamps: true,
underscored: true,
});
export default HealthActivity;

View File

@@ -0,0 +1,45 @@
// falukant/predefine/political_office_benefit.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
import PoliticalOfficeBenefitType from '../type/political_office_benefit_type.js';
class PoliticalOfficeBenefit extends Model {}
PoliticalOfficeBenefit.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
political_office_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
benefit_type_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
value: {
type: DataTypes.JSONB,
allowNull: false,
},
}, {
sequelize,
modelName: 'PoliticalOfficeBenefit',
tableName: 'political_office_benefit',
schema: 'falukant_predefine',
timestamps: false,
underscored: true,
});
// Association
PoliticalOfficeBenefit.belongsTo(PoliticalOfficeBenefitType, {
foreignKey: 'benefit_type_id',
as: 'benefitType'
});
PoliticalOfficeBenefitType.hasMany(PoliticalOfficeBenefit, {
foreignKey: 'benefit_type_id',
as: 'benefits'
});
export default PoliticalOfficeBenefit;

View File

@@ -0,0 +1,30 @@
// falukant/predefine/political_office_prerequisite.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class PoliticalOfficePrerequisite extends Model {}
PoliticalOfficePrerequisite.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
political_office_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
prerequisite: {
type: DataTypes.JSONB,
allowNull: false,
},
}, {
sequelize,
modelName: 'PoliticalOfficePrerequisite',
tableName: 'political_office_prerequisite',
schema: 'falukant_predefine',
timestamps: false,
underscored: true,
});
export default PoliticalOfficePrerequisite;

View File

@@ -0,0 +1,31 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class BanquetteType extends Model {}
BanquetteType.init(
{
tr: {
type: DataTypes.STRING,
allowNull: false,
},
cost: {
type: DataTypes.INTEGER,
allowNull: false,
},
reputationGrowth: {
type: DataTypes.INTEGER,
allowNull: false,
},
},
{
sequelize,
modelName: 'BanquetteType',
tableName: 'banquette',
schema: 'falukant_type',
timestamps: false,
underscored: true,
}
);
export default BanquetteType;

View File

@@ -0,0 +1,23 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class LearnRecipient extends Model {}
LearnRecipient.init(
{
tr: {
type: DataTypes.STRING,
allowNull: false,
},
},
{
sequelize,
modelName: 'LearnRecipient',
tableName: 'learn_recipient',
schema: 'falukant_type',
timestamps: false,
underscored: true,
}
);
export default LearnRecipient;

View File

@@ -0,0 +1,31 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class MusicType extends Model {}
MusicType.init(
{
tr: {
type: DataTypes.STRING,
allowNull: false,
},
cost: {
type: DataTypes.INTEGER,
allowNull: false,
},
reputationGrowth: {
type: DataTypes.INTEGER,
allowNull: false,
},
},
{
sequelize,
modelName: 'MusicType',
tableName: 'music',
schema: 'falukant_type',
timestamps: false,
underscored: true,
}
);
export default MusicType;

View File

@@ -0,0 +1,36 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class PartyType extends Model {}
PartyType.init(
{
tr: {
type: DataTypes.STRING,
allowNull: false,
},
cost: {
type: DataTypes.INTEGER,
allowNull: false,
},
forMarriage: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
reputationGrowth: {
type: DataTypes.INTEGER,
allowNull: false,
}
},
{
sequelize,
modelName: 'PartyType',
tableName: 'party',
schema: 'falukant_type',
timestamps: false,
underscored: true,
}
);
export default PartyType;

View File

@@ -0,0 +1,26 @@
// falukant/type/political_office_benefit_type.js
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class PoliticalOfficeBenefitType extends Model {}
PoliticalOfficeBenefitType.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
tr: {
type: DataTypes.STRING,
allowNull: false,
},
}, {
sequelize,
modelName: 'PoliticalOfficeBenefitType',
tableName: 'political_office_benefit_type',
schema: 'falukant_type',
timestamps: false,
underscored: true,
});
export default PoliticalOfficeBenefitType;

View File

@@ -0,0 +1,38 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class PoliticalOfficeType extends Model {}
PoliticalOfficeType.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
seatsPerRegion: {
type: DataTypes.INTEGER,
allowNull: false,
},
regionType: {
type: DataTypes.STRING,
allowNull: false,
},
termLength: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
}, {
sequelize,
modelName: 'PoliticalOfficeType',
tableName: 'political_office_type',
schema: 'falukant_type',
timestamps: false,
underscored: true,
});
export default PoliticalOfficeType;

View File

@@ -8,6 +8,11 @@ TitleOfNobility.init({
type: DataTypes.STRING,
allowNull: false,
},
level: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
}, {
sequelize,
modelName: 'Title',

View File

@@ -4,12 +4,16 @@ import { sequelize } from '../../../utils/sequelize.js';
class TitleRequirement extends Model { }
TitleRequirement.init({
titleId: {
id : {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
titleId: {
type: DataTypes.INTEGER,
allowNull: false,
},
requirementType: {
type: DataTypes.STRING,
allowNull: false,
@@ -25,6 +29,13 @@ TitleRequirement.init({
schema: 'falukant_type',
timestamps: false,
underscored: true,
indexes: [
{
unique: true,
fields: ['title_id', 'requirement_type'],
name: 'title_requirement_titleid_reqtype_unique'
}
]
});
export default TitleRequirement;

View File

@@ -1,3 +1,5 @@
// models/index.js
import SettingsType from './type/settings.js';
import UserParamValue from './type/user_param_value.js';
import UserParamType from './type/user_param.js';
@@ -32,6 +34,7 @@ import MessageHistory from './forum/message_history.js';
import MessageImage from './forum/message_image.js';
import ForumForumPermission from './forum/forum_forum_permission.js';
import Friendship from './community/friendship.js';
import FalukantUser from './falukant/data/user.js';
import RegionType from './falukant/type/region.js';
import RegionData from './falukant/data/region.js';
@@ -58,90 +61,136 @@ import DaySell from './falukant/log/daysell.js';
import Notification from './falukant/log/notification.js';
import MarriageProposal from './falukant/data/marriage_proposal.js';
import RelationshipType from './falukant/type/relationship.js';
import Relationship from './falukant/data/relationship.js';
import CharacterTrait from './falukant/type/character_trait.js';
import FalukantCharacterTrait from './falukant/data/falukant_character_trait.js';
import Mood from './falukant/type/mood.js';
import PromotionalGift from './falukant/type/promotional_gift.js';
import PromotionalGiftCharacterTrait from './falukant/predefine/promotional_gift_character_trait.js';
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';
import PartyType from './falukant/type/party.js';
import Party from './falukant/data/party.js';
import MusicType from './falukant/type/music.js';
import BanquetteType from './falukant/type/banquette.js';
import PartyInvitedNobility from './falukant/data/partyInvitedNobility.js';
import ChildRelation from './falukant/data/child_relation.js';
import LearnRecipient from './falukant/type/learn_recipient.js';
import Learning from './falukant/data/learning.js';
import Credit from './falukant/data/credit.js';
import DebtorsPrism from './falukant/data/debtors_prism.js';
import HealthActivity from './falukant/log/health_activity.js';
// — Politische Ämter (Politics) —
import PoliticalOfficeType from './falukant/type/political_office_type.js';
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
import PoliticalOffice from './falukant/data/political_office.js';
import Election from './falukant/data/election.js';
import Candidate from './falukant/data/candidate.js';
import Vote from './falukant/data/vote.js';
import ElectionResult from './falukant/data/election_result.js';
const models = {
SettingsType,
UserParamValue,
UserParamType,
UserRightType,
User,
UserParam,
Login,
UserRight,
InterestType,
InterestTranslationType,
Interest,
ContactMessage,
UserParamVisibilityType,
UserParamVisibility,
Folder,
Image,
ImageVisibilityType,
ImageVisibilityUser,
FolderImageVisibility,
ImageImageVisibility,
FolderVisibilityUser,
GuestbookEntry,
DiaryHistory,
Diary,
Forum,
ForumPermission,
ForumForumPermission,
ForumUserPermission,
Title,
TitleHistory,
Message,
MessageHistory,
MessageImage,
Friendship,
RegionType,
RegionData,
FalukantUser,
FalukantPredefineFirstname,
FalukantPredefineLastname,
FalukantCharacter,
FalukantStock,
FalukantStockType,
ProductType,
Knowledge,
TitleOfNobility,
TitleRequirement,
BranchType,
Branch,
Production,
Inventory,
BuyableStock,
MoneyFlow,
Director,
DirectorProposal,
TownProductWorth,
DayProduction,
DaySell,
Notification,
MarriageProposal,
RelationshipType,
Relationship,
CharacterTrait,
FalukantCharacterTrait,
Mood,
PromotionalGift,
PromotionalGiftCharacterTrait,
PromotionalGiftMood,
PromotionalGiftLog,
HouseType,
BuyableHouse,
UserHouse,
SettingsType,
UserParamValue,
UserParamType,
UserRightType,
User,
UserParam,
Login,
UserRight,
InterestType,
InterestTranslationType,
Interest,
ContactMessage,
UserParamVisibilityType,
UserParamVisibility,
Folder,
Image,
ImageVisibilityType,
ImageVisibilityUser,
FolderImageVisibility,
ImageImageVisibility,
FolderVisibilityUser,
GuestbookEntry,
DiaryHistory,
Diary,
Forum,
ForumPermission,
ForumForumPermission,
ForumUserPermission,
Title,
TitleHistory,
Message,
MessageHistory,
MessageImage,
Friendship,
// Falukant core
RegionType,
RegionData,
FalukantUser,
FalukantPredefineFirstname,
FalukantPredefineLastname,
FalukantCharacter,
FalukantStock,
FalukantStockType,
ProductType,
Knowledge,
TitleOfNobility,
TitleRequirement,
BranchType,
Branch,
Production,
Inventory,
BuyableStock,
MoneyFlow,
Director,
DirectorProposal,
TownProductWorth,
DayProduction,
DaySell,
Notification,
MarriageProposal,
RelationshipType,
Relationship,
CharacterTrait,
FalukantCharacterTrait,
Mood,
PromotionalGift,
PromotionalGiftCharacterTrait,
PromotionalGiftMood,
PromotionalGiftLog,
HouseType,
BuyableHouse,
UserHouse,
PartyType,
MusicType,
BanquetteType,
Party,
PartyInvitedNobility,
ChildRelation,
LearnRecipient,
Learning,
Credit,
DebtorsPrism,
HealthActivity,
// Politics
PoliticalOfficeType,
PoliticalOfficeRequirement,
PoliticalOfficeBenefitType,
PoliticalOfficeBenefit,
PoliticalOffice,
Election,
Candidate,
Vote,
ElectionResult,
};
export default models;

View File

@@ -180,6 +180,67 @@ export async function createTriggers() {
$function$;
`;
const createChildRelationNameFunction = `
CREATE OR REPLACE FUNCTION falukant_data.populate_child_relation_names()
RETURNS TRIGGER AS $$
DECLARE
v_first_name TEXT;
v_last_name TEXT;
v_full_father TEXT;
v_full_mother TEXT;
BEGIN
-- Vaternamen holen
SELECT pf.name, pl.name
INTO v_first_name, v_last_name
FROM falukant_data.character c
JOIN falukant_predefine.firstname pf ON pf.id = c.first_name
JOIN falukant_predefine.lastname pl ON pl.id = c.last_name
WHERE c.id = NEW.father_character_id;
v_full_father := v_first_name || ' ' || v_last_name;
-- Mutternamen holen
SELECT pf.name, pl.name
INTO v_first_name, v_last_name
FROM falukant_data.character c
JOIN falukant_predefine.firstname pf ON pf.id = c.first_name
JOIN falukant_predefine.lastname pl ON pl.id = c.last_name
WHERE c.id = NEW.mother_character_id;
v_full_mother := v_first_name || ' ' || v_last_name;
-- Felder füllen
NEW.father_name := v_full_father;
NEW.mother_name := v_full_mother;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`;
const createChildRelationNameTrigger = `
DROP TRIGGER IF EXISTS trg_child_relation_populate_names
ON falukant_data.child_relation;
CREATE TRIGGER trg_child_relation_populate_names
BEFORE INSERT ON falukant_data.child_relation
FOR EACH ROW
EXECUTE FUNCTION falukant_data.populate_child_relation_names();
`;
const createRandomMoodUpdateMethod = `
CREATE OR REPLACE FUNCTION falukant_data.get_random_mood_id()
RETURNS INTEGER AS $$
BEGIN
RETURN (
SELECT id
FROM falukant_type.mood
ORDER BY random()
LIMIT 1
);
END;
$$ LANGUAGE plpgsql VOLATILE;
`;
try {
await sequelize.query(createTriggerFunction);
await sequelize.query(createInsertTrigger);
@@ -193,6 +254,9 @@ export async function createTriggers() {
await sequelize.query(createKnowledgeTriggerMethod);
await sequelize.query(createKnowledgeTrigger);
await sequelize.query(updateMoney);
await sequelize.query(createChildRelationNameFunction);
await sequelize.query(createChildRelationNameTrigger);
await sequelize.query(createRandomMoodUpdateMethod);
await initializeCharacterTraitTrigger();
console.log('Triggers created successfully');
@@ -250,4 +314,3 @@ export const initializeCharacterTraitTrigger = async () => {
console.error('❌ Fehler beim Erstellen des Triggers:', error);
}
};

View File

@@ -6,6 +6,8 @@ const falukantController = new FalukantController();
router.get('/user', falukantController.getUser);
router.post('/user', falukantController.createUser);
router.get('/mood/affect', falukantController.getMoodAffect);
router.get('/character/affect', falukantController.getCharacterAffect);
router.get('/name/randomfirstname/:gender', falukantController.randomFirstName);
router.get('/name/randomlastname', falukantController.randomLastName);
router.get('/info', falukantController.getInfo);
@@ -30,15 +32,30 @@ router.post('/director/proposal', falukantController.getDirectorProposals);
router.post('/director/convertproposal', falukantController.convertProposalToDirector);
router.post('/director/settings', falukantController.setSetting);
router.get('/director/:branchId', falukantController.getDirectorForBranch);
router.get('/directors', falukantController.getAllDirectors);
router.post('/directors', falukantController.updateDirector);
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
router.get('/family/gifts', falukantController.getGifts);
router.get('/family/children', falukantController.getChildren);
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);
router.get('/party/types', falukantController.getPartyTypes);
router.post('/party', falukantController.createParty);
router.get('/party', falukantController.getParties);
router.get('/family/notbaptised', falukantController.getNotBaptisedChildren);
router.post('/church/baptise', falukantController.baptise);
router.get('/education', falukantController.getEducation);
router.post('/education', falukantController.sendToSchool);
router.get('/bank/overview', falukantController.getBankOverview);
router.get('/bank/credits', falukantController.getBankCredits);
router.post('/bank/credits', falukantController.takeBankCredits);
router.get('/nobility', falukantController.getNobility);
router.post('/nobility', falukantController.advanceNobility);
router.get('/health', falukantController.getHealth);
router.post('/health', falukantController.healthActivity)
export default router;

File diff suppressed because it is too large Load Diff

View File

@@ -267,73 +267,81 @@ async function initializeFalukantProducts() {
}
async function initializeFalukantTitles() {
await TitleOfNobility.bulkCreate([
{ labelTr: "noncivil" },
{ labelTr: "civil" },
{ labelTr: "sir" },
{ labelTr: "townlord" },
{ labelTr: "by" },
{ labelTr: "landlord" },
{ labelTr: "knight" },
{ labelTr: "baron" },
{ labelTr: "count" },
{ labelTr: "palsgrave" },
{ labelTr: "margrave" },
{ labelTr: "landgrave" },
{ labelTr: "ruler" },
{ labelTr: "elector" },
{ labelTr: "imperial-prince" },
{ labelTr: "duke" },
{ labelTr: "grand-duke" },
{ labelTr: "prince-regent" },
{ labelTr: "king" },
], {
updateOnDuplicate: ['labelTr'],
});
try {
await TitleOfNobility.bulkCreate([
{ labelTr: "noncivil", level: 1 },
{ labelTr: "civil", level: 2 },
{ labelTr: "sir", level: 3 },
{ labelTr: "townlord", level: 4 },
{ labelTr: "by", level: 5 },
{ labelTr: "landlord", level: 6 },
{ labelTr: "knight", level: 7 },
{ labelTr: "baron", level: 8 },
{ labelTr: "count", level: 9 },
{ labelTr: "palsgrave", level: 10 },
{ labelTr: "margrave", level: 11 },
{ labelTr: "landgrave", level: 12 },
{ labelTr: "ruler", level: 13 },
{ labelTr: "elector", level: 14 },
{ labelTr: "imperial-prince", level: 15 },
{ labelTr: "duke", level: 16 },
{ labelTr: "grand-duke", level: 17 },
{ labelTr: "prince-regent", level: 18 },
{ labelTr: "king", level: 19 },
], {
updateOnDuplicate: ['labelTr'],
});
} catch (error) {
console.error('Error initializing Falukant titles:', error);
}
}
async function initializeFalukantTitleRequirements() {
const titleRequirements = [
{ labelTr: "civil", requirements: [{ type: "money", value: 500 }] },
{ labelTr: "sir", requirements: [{ type: "branches", value: 2 }] },
{ labelTr: "townlord", requirements: [] },
{ labelTr: "by", requirements: [] },
{ labelTr: "landlord", requirements: [] },
{ labelTr: "knight", requirements: [] },
{ labelTr: "baron", requirements: [{ type: "branches", value: 4 }] },
{ labelTr: "count", requirements: [] },
{ labelTr: "palsgrave", requirements: [] },
{ labelTr: "margrave", requirements: [] },
{ labelTr: "landgrave", requirements: [] },
{ labelTr: "ruler", requirements: [] },
{ labelTr: "elector", requirements: [] },
{ labelTr: "imperial-prince", requirements: [] },
{ labelTr: "duke", requirements: [] },
{ labelTr: "grand-duke", requirements: [] },
{ labelTr: "prince-regent", requirements: [] },
{ labelTr: "king", requirements: [] },
{ labelTr: "civil", requirements: [{ type: "money", value: 5000 }, { type: "cost", value: 500 }] },
{ labelTr: "sir", requirements: [{ type: "branches", value: 2 }, { type: "cost", value: 1000 }] },
{ labelTr: "townlord", requirements: [{ type: "cost", value: 3000 }] },
{ labelTr: "by", requirements: [{ type: "cost", value: 6000 }] },
{ labelTr: "landlord", requirements: [{ type: "cost", value: 9000 }] },
{ labelTr: "knight", requirements: [{ type: "cost", value: 11000 }] },
{ labelTr: "baron", requirements: [{ type: "branches", value: 4 }, { type: "cost", value: 15000 }] },
{ labelTr: "count", requirements: [{ type: "cost", value: 19000 }] },
{ labelTr: "palsgrave", requirements: [{ type: "cost", value: 25000 }] },
{ labelTr: "margrave", requirements: [{ type: "cost", value: 33000 }] },
{ labelTr: "landgrave", requirements: [{ type: "cost", value: 47000 }] },
{ labelTr: "ruler", requirements: [{ type: "cost", value: 66000 }] },
{ labelTr: "elector", requirements: [{ type: "cost", value: 79000 }] },
{ labelTr: "imperial-prince", requirements: [{ type: "cost", value: 99999 }] },
{ labelTr: "duke", requirements: [{ type: "cost", value: 130000 }] },
{ labelTr: "grand-duke",requirements: [{ type: "cost", value: 170000 }] },
{ labelTr: "prince-regent", requirements: [{ type: "cost", value: 270000 }] },
{ labelTr: "king", requirements: [{ type: "cost", value: 500000 }] },
];
const titles = await TitleOfNobility.findAll();
const requirementsToInsert = [];
for (let i = 0; i < titleRequirements.length; i++) {
const titleRequirement = titleRequirements[i];
const title = titles.find(t => t.labelTr === titleRequirement.labelTr);
const titleReq = titleRequirements[i];
const title = titles.find(t => t.labelTr === titleReq.labelTr);
if (!title) continue;
if (i > 1) {
const moneyRequirement = {
type: "money",
titleReq.requirements.push({
type: "money",
value: 5000 * Math.pow(3, i - 1),
};
titleRequirement.requirements.push(moneyRequirement);
});
}
for (const requirement of titleRequirement.requirements) {
for (const req of titleReq.requirements) {
requirementsToInsert.push({
titleId: title.id,
requirementType: requirement.type,
requirementValue: requirement.value,
titleId: title.id,
requirementType: req.type,
requirementValue: req.value,
});
}
}
await TitleRequirement.bulkCreate(requirementsToInsert, { ignoreDuplicates: true });
}

View File

@@ -8,6 +8,10 @@ import PromotionalGiftCharacterTrait from "../../models/falukant/predefine/promo
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";
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 LearnRecipient from "../../models/falukant/type/learn_recipient.js";
export const initializeFalukantTypes = async () => {
await initializeFalukantTypeRegions();
@@ -17,6 +21,10 @@ export const initializeFalukantTypes = async () => {
await initializeFalukantPromotionalGifts();
await initializePromotionalGiftMoodLinks();
await initializeFalukantHouseTypes();
await initializeFalukantPartyTypes();
await initializeFalukantMusicTypes();
await initializeFalukantBanquetteTypes();
await initializeLearnerTypes();
};
const regionTypes = [];
@@ -208,15 +216,44 @@ const promotionalGiftMoodLinks = [
];
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' },
{ labelTr: 'Unter der Brücke', abbr: 'under_bridge', cost: 0, position: 1, minimumTitle: 'noncivil' },
{ labelTr: 'Strohhütte', abbr: 'straw_hut', cost: 100, position: 2, minimumTitle: 'noncivil' },
{ labelTr: 'Holzhaus', abbr: 'wooden_house', cost: 5000, position: 3, minimumTitle: 'civil' },
{ labelTr: 'Hinterhofzimmer', abbr: 'backyard_room', cost: 75000, position: 4, minimumTitle: 'civil' },
{ labelTr: 'Kleines Familienhaus', abbr: 'family_house', cost: 273000, position: 5, minimumTitle: 'sir' },
{ labelTr: 'Stadthaus', abbr: 'townhouse', cost: 719432, position: 6, minimumTitle: 'townlord' },
{ labelTr: 'Villa', abbr: 'villa', cost: 3500000, position: 7, minimumTitle: 'knight' },
{ labelTr: 'Herrenhaus', abbr: 'mansion', cost: 18000000, position: 8, minimumTitle: 'ruler' },
{ labelTr: 'Schloss', abbr: 'castle', cost: 500000000, position: 9, minimumTitle: 'prince-regent' },
];
const partyTypes = [
{ labelTr: 'wedding', cost: 50, forMarriage: true, reputationGrowth: 5 },
{ labelTr: 'ball', cost: 250, forMarriage: false, reputationGrowth: 7 },
{ labelTr: 'town fair', cost: 1000, forMarriage: false, reputationGrowth: 10 },
{ labelTr: 'royal feast', cost: 50000, forMarriage: false, reputationGrowth: 25 },
];
const musicTypes = [
{ type: 'none', cost: 0, reputationGrowth: 0 },
{ type: 'bard', cost: 100, reputationGrowth: 2 },
{ type: 'villageBand', cost: 2500, reputationGrowth: 5 },
{ type: 'chamberOrchestra', cost: 12000, reputationGrowth: 10 },
{ type: 'symphonyOrchestra', cost: 37000, reputationGrowth: 15 },
{ type: 'symphonyOrchestraWithChorusAndSolists', cost: 500000, reputationGrowth: 25 },
];
const banquetteTypes = [
{ type: 'bread', cost: 5, reputationGrowth: 0 },
{ type: 'roastWithBeer', cost: 200, reputationGrowth: 5 },
{ type: 'poultryWithVegetablesAndWine', cost: 5000, reputationGrowth: 10 },
{ type: 'extensiveBuffet', cost: 100000, reputationGrowth: 20 }
];
const learnerTypes = [
{ tr: 'self', },
{ tr: 'children', },
{ tr: 'director', },
];
{
@@ -391,3 +428,54 @@ export const initializeFalukantHouseTypes = async () => {
});
}
};
export const initializeFalukantPartyTypes = async () => {
for (const pt of partyTypes) {
const [record, created] = await PartyType.findOrCreate({
where: { tr: pt.labelTr },
defaults: {
cost: pt.cost,
tr: pt.labelTr,
forMarriage: pt.forMarriage,
reputationGrowth: pt.reputationGrowth,
}
});
}
}
export const initializeFalukantMusicTypes = async () => {
for (const mt of musicTypes) {
const [record, created] = await MusicType.findOrCreate({
where: { tr: mt.type },
defaults: {
cost: mt.cost,
tr: mt.type,
reputationGrowth: mt.reputationGrowth,
}
});
}
}
export const initializeFalukantBanquetteTypes = async () => {
for (const bt of banquetteTypes) {
const [record, created] = await BanquetteType.findOrCreate({
where: { tr: bt.type },
defaults: {
tr: bt.type,
cost: bt.cost,
reputationGrowth: bt.reputationGrowth,
}
});
}
};
export const initializeLearnerTypes = async () => {
for (const lt of learnerTypes) {
const [record, created] = await LearnRecipient.findOrCreate({
where: { tr: lt.tr },
defaults: {
tr: lt.tr,
}
});
}
}

BIN
dump.rdb

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

View File

@@ -0,0 +1,56 @@
<template>
<div class="simple-tabs">
<button
v-for="tab in tabs"
:key="tab.value"
:class="['simple-tab', { active: internalValue === tab.value }]"
@click="$emit('update:modelValue', tab.value)"
>
<slot name="label" :tab="tab">
{{ $t(tab.label) }}
</slot>
</button>
</div>
</template>
<script>
export default {
name: 'SimpleTabs',
props: {
tabs: {
type: Array,
required: true,
},
modelValue: {
type: [String, Number],
required: true,
},
},
computed: {
internalValue() {
return this.modelValue;
}
}
};
</script>
<style scoped>
.simple-tabs {
display: flex;
margin-top: 1rem;
}
.simple-tab {
padding: 0.5rem 1rem;
background: #fff;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.simple-tab.active {
background: #F9A22C;
color: #000;
}
</style>

View File

@@ -1,9 +1,12 @@
<template>
<div class="statusbar">
<template v-for="item in statusItems" :key="item.key">
<div class="status-item" v-if="item.value !== null" :title="$t(`falukant.statusbar.${item.key}`)">
<div class="status-item" v-if="item.value !== null && item.image == null" :title="$t(`falukant.statusbar.${item.key}`)">
<span class="status-icon">{{ item.icon }}: {{ item.value }}</span>
</div>
<div class="status-item" v-else-if="item.image !== null" :title="$t(`falukant.statusbar.${item.key}`)">
<span class="status-icon">{{ item.icon }}:</span> <img :src="'/images/icons/falukant/relationship-' + item.image + '.png'" class="relationship-icon" />
</div>
</template>
<span v-if="statusItems.length > 0">
<template v-for="(menuItem, key) in menu.falukant.children" :key="menuItem.id" >
@@ -23,9 +26,10 @@ export default {
return {
statusItems: [
{ key: "age", icon: "👶", value: 0 },
{ key: "relationship", icon: "💑", image: null },
{ key: "wealth", icon: "💰", value: 0 },
{ key: "health", icon: "❤️", value: "Good" },
{ key: "events", icon: "📰", value: null },
{ key: "events", icon: "📰", value: null, image: null },
],
};
},
@@ -56,6 +60,9 @@ export default {
const response = await apiClient.get("/api/falukant/info");
const { money, character, events } = response.data;
const { age, health } = character;
const relationship = response.data.character.relationshipsAsCharacter1[0]?.relationshipType?.tr
|| response.data.character.relationshipsAsCharacter2[0]?.relationshipType?.tr
|| null;
let healthStatus = '';
if (health > 90) {
healthStatus = this.$t("falukant.health.amazing");
@@ -70,9 +77,10 @@ export default {
}
this.statusItems = [
{ key: "age", icon: "👶", value: age },
{ key: "relationship", icon: "💑", image: relationship },
{ 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 },
{ key: "events", icon: "📰", value: events || null, image: null },
];
} catch (error) {
console.error("Error fetching status:", error);
@@ -112,7 +120,7 @@ export default {
border: 1px solid #ccc;
border-radius: 4px;
width: calc(100% + 40px);
gap: 2em;
gap: 1.2em;
margin: -21px -20px 1.5em -20px;
position: fixed;
}
@@ -120,6 +128,8 @@ export default {
.status-item {
text-align: center;
cursor: pointer;
display: inline-flex;
align-items: center;
}
.status-icon {
@@ -132,4 +142,9 @@ export default {
cursor: pointer;
padding: 4px 2px 0 0;
}
.relationship-icon {
max-width: 24px;
max-height: 24px;
}
</style>

View File

@@ -226,7 +226,8 @@
},
"mood": "Stimmung",
"progress": "Zuneigung"
"progress": "Zuneigung",
"jumpToPartyForm": "Hochzeitsfeier veranstalten (Nötig für Hochzeit und Kinder)"
},
"relationships": {
"name": "Name"
@@ -238,7 +239,8 @@
"actions": "Aktionen",
"none": "Keine Kinder vorhanden.",
"detailButton": "Details anzeigen",
"addChild": "Kind hinzufügen"
"addChild": "Kind hinzufügen",
"baptism": "Taufen"
},
"lovers": {
"title": "Liebhaber",
@@ -407,12 +409,192 @@
"price": "Kaufpreis",
"worth": "Restwert",
"sell": "Verkaufen",
"renovate": "Renovieren",
"renovateAll": "Komplett renovieren",
"status": {
"roofCondition": "Dach",
"wallCondition": "Wände",
"floorCondition": "Böden",
"windowCondition": "Fenster"
},
"type": {
"backyard_room": "Hinterhofzimmer",
"wooden_house": "Holzhütte",
"straw_hut": "Strohhütte"
}
},
"nobility": {
"title": "Sozialstatus",
"tabs": {
"overview": "Übersicht",
"advance": "Erweitern"
},
"nextTitle": "Nächster möglicher Titel",
"requirement": {
"money": "Vermögen mindestens {amount}",
"cost": "Kosten: {amount}",
"branches": "Mindestens {amount} Niederlassungen"
},
"advance": {
"confirm": "Aufsteigen beantragen"
}
},
"reputation": {
"title": "Reputation",
"overview": {
"title": "Übersicht"
},
"party": {
"title": "Feste",
"totalCost": "Gesamtkosten",
"order": "Fest veranstalten",
"inProgress": "Feste in Vorbereitung",
"completed": "Abgeschlossene Feste",
"newpartyview": {
"open": "Neues Fest erstellen",
"close": "Neues Fest verbergen",
"type": "Art des Festes"
},
"music": {
"label": "Musik",
"none": "Ohne Musik",
"bard": "Ein Barde",
"villageBand": "Eine Dorfkapelle",
"chamberOrchestra": "Ein Kammerorchester",
"symphonyOrchestra": "Ein Sinfonieorchester",
"symphonyOrchestraWithChorusAndSolists": "Ein Sinfonieorchester mit Chor und Solisten"
},
"banquette": {
"label": "Essen",
"bread": "Brot",
"roastWithBeer": "Rostbraten mit Bier",
"poultryWithVegetablesAndWine": "Geflügel mit Gemüse und Wein",
"extensiveBuffet": "Festliches Essen"
},
"servants": {
"label": "Ein Bediensteter pro ",
"perPersons": " Personen"
},
"esteemedInvites": {
"label": "Eingeladene Stände"
},
"type": "Festart",
"cost": "Kosten",
"date": "Datum"
}
},
"party": {
"type": {
"ball": "Ball",
"wedding": "Hochzeit",
"royal feast": "Königliches Bankett",
"town fair": "Stadtmarkt"
}
},
"church": {
"title": "Kirche",
"baptism": {
"title": "Taufen",
"table": {
"name": "Vorname",
"gender": "Geschlecht",
"age": "Alter",
"baptise": "Taufen (50)",
"newName": "Namen vorschlagen"
},
"gender": {
"male": "Junge",
"female": "Mädchen"
},
"success": "Das Kind wurde getauft.",
"error": "Das Kind konnte nicht getauft werden."
}
},
"education": {
"title": "Bildung",
"self": {
"title": "Eigene Bildung"
},
"children": {
"title": "Kinderbildung"
},
"director": {
"title": "Direktoren-Ausbildung"
},
"table": {
"article": "Produkt",
"knowledge": "Wissen",
"activity": "Aktivität"
},
"learn": "Weiterbilden",
"learnAll": "In allem weiterbilden"
},
"bank": {
"title": "Bank",
"account": {
"title": "Kontostand",
"balance": "Kontostand",
"totalDebt": "Ausstände",
"maxCredit": "Maximaler Kredit",
"availableCredit": "Verfügbarer Kredit"
},
"credits": {
"title": "Kredite",
"none": "Derzeit hast Du keinen Kredit aufgenommen.",
"amount": "Betrag",
"remaining": "Verbleibend",
"interestRate": "Zinssatz",
"table": {
"name": "Name",
"amount": "Betrag",
"reason": "Grund",
"date": "Datum"
},
"payoff": {
"title": "Neuen Kredit aufnehmen",
"height": "Kredithöhe",
"remaining": "Verbleibende mögliche Kredithöhe",
"fee": "Kreditzins",
"feeHeight": "Rate (a 10 Raten)",
"total": "Gesamtsumme",
"confirm": "Kredit aufnehmen"
}
}
},
"director": {
"title": "Direktoren",
"branch": "Niederlassung",
"income": "Einkommen",
"satisfaction": "Zufriedenheit",
"name": "Name",
"age": "Alter",
"knowledge": {
"title": "Wissen",
"knowledge": "Wissen"
},
"product": "Produkt",
"updateButton": "Gehalt aktualisieren",
"wishedIncome": "Gewünschtes Einkommen"
},
"healthview": {
"title": "Gesundheit",
"age": "Alter",
"status": "Gesundheitszustand",
"measuresTaken": "Ergriffene Maßnahmen",
"measure": "Maßnahme",
"date": "Datum",
"cost": "Kosten",
"success": "Erfolg",
"selectMeasure": "Maßnahme",
"perform": "Durchführen",
"measures": {
"pill": "Tablette",
"doctor": "Arztbesuch",
"witch": "Hexe",
"drunkOfLife": "Trunk des Lebens",
"barber": "Barbier"
},
"choose": "Bitte auswählen"
}
}
}

View File

@@ -65,7 +65,8 @@
"politics": "Politik",
"education": "Bildung",
"health": "Gesundheit",
"bank": "Bank"
"bank": "Bank",
"church": "Kirche"
}
}
}

View File

@@ -4,6 +4,13 @@ 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';
import NobilityView from '../views/falukant/NobilityView.vue';
import ReputationView from '../views/falukant/ReputationView.vue';
import ChurchView from '../views/falukant/ChurchView.vue';
import EducationView from '../views/falukant/EducationView.vue';
import BankView from '../views/falukant/BankView.vue';
import DirectorView from '../views/falukant/DirectorView.vue';
import HealthView from '../views/falukant/HealthView.vue';
const falukantRoutes = [
{
@@ -42,6 +49,48 @@ const falukantRoutes = [
component: HouseView,
meta: { requiresAuth: true },
},
{
path: '/falukant/nobility',
name: 'NobilityView',
component: NobilityView,
meta: { requiresAuth: true }
},
{
path: '/falukant/reputation',
name: 'ReputationView',
component: ReputationView,
meta: { requiresAuth: true }
},
{
path: '/falukant/church',
name: 'ChurchView',
component: ChurchView,
meta: { requiresAuth: true }
},
{
path: '/falukant/education',
name: 'EducationView',
component: EducationView,
meta: { requiresAuth: true }
},
{
path: '/falukant/bank',
name: 'BankView',
component: BankView,
meta: { requiresAuth: true }
},
{
path: '/falukant/directors',
name: 'DirectorView',
component: DirectorView,
meta: { requiresAuth: true }
},
{
path: '/falukant/health',
name: 'HealthView',
component: HealthView,
meta: { requiresAuth: true }
},
];
export default falukantRoutes;

View File

@@ -0,0 +1,175 @@
<template>
<div class="contenthidden">
<StatusBar />
<div class="contentscroll">
<h2>{{ $t('falukant.bank.title') }}</h2>
<SimpleTabs v-model="activeTab" :tabs="tabs" />
<!-- OVERVIEW -->
<div v-if="activeTab === 'account'">
<div class="account-section">
<table>
<tr>
<td>{{ $t('falukant.bank.account.balance') }}</td>
<td>{{ formatCost(bankOverview.money) }}</td>
</tr>
<tr>
<td>{{ $t('falukant.bank.account.totalDebt') }}</td>
<td>{{ formatCost(bankOverview.totalDebt) }}</td>
</tr>
<tr>
<td>{{ $t('falukant.bank.account.maxCredit') }}</td>
<td>{{ formatCost(bankOverview.maxCredit) }}</td>
</tr>
<tr>
<td>{{ $t('falukant.bank.account.availableCredit') }}</td>
<td>{{ formatCost(bankOverview.availableCredit) }}</td>
</tr>
</table>
</div>
</div>
<!-- ACTIVE CREDITS -->
<div v-else-if="activeTab === 'credits'">
<div class="credits-section">
<div v-if="bankOverview.activeCredits?.length">
<table class="credits-table">
<thead>
<tr>
<th>{{ $t('falukant.bank.credits.amount') }}</th>
<th>{{ $t('falukant.bank.credits.remaining') }}</th>
<th>{{ $t('falukant.bank.credits.interestRate') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="credit in bankOverview.activeCredits" :key="credit.id">
<td>{{ formatCost(credit.amount) }}</td>
<td>{{ formatCost(credit.remainingAmount) }}</td>
<td>{{ credit.interestRate }}%</td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<p>{{ $t('falukant.bank.credits.none') }}</p>
</div>
</div>
</div>
<!-- PAYOFF INLINE -->
<div v-else-if="activeTab === 'payoff'">
<div class="payoff-section">
<label>
{{ $t('falukant.bank.credits.payoff.height') }}:
<input
type="number"
v-model="selectedCredit"
:min="0"
:max="bankOverview.availableCredit"
value="0"
/>
</label>
<div v-if="selectedCredit">
<p>{{ $t('falukant.bank.credits.payoff.remaining') }}: {{ formatCost(bankOverview.availableCredit - selectedCredit) }}</p>
<p>{{ $t('falukant.bank.credits.payoff.fee') }}: {{ formatCost(bankOverview.fee) }}</p>
<p>{{ $t('falukant.bank.credits.payoff.feeHeight') }}: {{ formatCost(feeRate()) }}</p>
<p>
<strong>{{ $t('falukant.bank.credits.payoff.total') }}: {{ formatCost(creditCost()) }}</strong>
</p>
<button @click="confirmPayoff" class="button" :disabled="!selectedCredit">
{{ $t('falukant.bank.credits.payoff.confirm') }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import SimpleTabs from '@/components/SimpleTabs.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
export default {
name: 'BankView',
components: { StatusBar, SimpleTabs },
data() {
return {
activeTab: 'account',
tabs: [
{ value: 'account', label: 'falukant.bank.account.title' },
{ value: 'credits', label: 'falukant.bank.credits.title' },
{ value: 'payoff', label: 'falukant.bank.credits.payoff.title' }
],
bankOverview: {
money: 0,
totalDebt: 0,
maxCredit: 0,
availableCredit: 0,
activeCredits: []
},
selectedCreditId: null,
selectedCredit: null,
earlyPayoffFee: 0
};
},
computed: {
...mapState(['daemonSocket'])
},
async mounted() {
await this.loadBankOverview();
if (this.daemonSocket) this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
},
beforeUnmount() {
if (this.daemonSocket) this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
},
methods: {
async loadBankOverview() {
try {
const { data } = await apiClient.get('/api/falukant/bank/overview');
this.bankOverview = data;
} catch (err) {
console.error(err);
}
},
async confirmPayoff() {
try {
await apiClient.post('/api/falukant/bank/credits', {
height: this.selectedCredit
});
await this.loadBankOverview();
this.selectedCredit = null;
this.activeTab = 'credits';
} catch (err) {
console.error(err);
}
},
handleDaemonMessage(msg) {
try {
if (['falukantUpdateStatus', 'moneyChange', 'creditChange'].includes(msg.event)) {
this.loadBankOverview();
}
} catch (err) {
console.error(evt, err);
}
},
feeRate() {
return this.bankOverview.fee * this.selectedCredit / 100 + this.selectedCredit / 10;
},
creditCost() {
return this.selectedCredit + (this.bankOverview.fee * 10 * this.selectedCredit / 100);
},
formatCost(val) {
return new Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(val);
}
}
};
</script>
<style scoped lang="scss">
h2 { padding-top: 20px; }
</style>

View File

@@ -69,15 +69,15 @@ export default {
"falukantUpdateStatus",
"falukantBranchUpdate",
];
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
events.forEach(eventName => {
if (this.socket) {
this.socket.on(eventName, (data) => {
this.handleEvent({ event: eventName, ...data });
});
}
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
});
},
beforeUnmount() {

View File

@@ -0,0 +1,143 @@
<template>
<div class="contenthidden">
<StatusBar />
<div class="contentscroll">
<h2>{{ $t('falukant.church.title') }}</h2>
<SimpleTabs v-model="activeTab" :tabs="tabs" />
<div class="tab-content">
<div v-if="activeTab === 'baptism'">
<h3>{{ $t('falukant.church.baptism.title') }}</h3>
<table>
<thead>
<tr>
<th>{{ $t('falukant.church.baptism.table.gender') }}</th>
<th>{{ $t('falukant.church.baptism.table.name') }}</th>
<th>{{ $t('falukant.church.baptism.table.age') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="person in baptismList" :key="person.id">
<td>{{ $t(`falukant.church.baptism.gender.${person.gender}`) }}</td>
<td>
<input type="text" v-model="person.proposedFirstName" />
<button @click="newName(person)">
{{ $t('falukant.church.baptism.table.newName') }}
</button>
</td>
<td>{{ person.age }}</td>
<td>
<button @click="baptise(person)">
{{ $t('falukant.church.baptism.table.baptise') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue'
import MessageDialog from '@/dialogues/standard/MessageDialog.vue'
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue'
import apiClient from '@/utils/axios.js'
import SimpleTabs from '@/components/SimpleTabs.vue'
export default {
name: 'ChurchView',
components: {
StatusBar,
MessageDialog,
ErrorDialog,
SimpleTabs,
},
data() {
return {
activeTab: 'baptism',
tabs: [
{ value: 'baptism', label: 'falukant.church.baptism.title' },
],
baptismList: []
}
},
async mounted() {
await this.loadNotBaptisedChildren()
},
methods: {
async loadNotBaptisedChildren() {
try {
const { data } = await apiClient.get('/api/falukant/family/notbaptised')
this.baptismList = data
} catch (err) {
console.error(err)
}
},
async newName(person) {
try {
const { data } = await apiClient.get(
`/api/falukant/name/randomfirstname/${person.gender}`
)
person.proposedFirstName = data.name ?? data
} catch (err) {
console.error(err)
}
},
async baptise(person) {
try {
await apiClient.post('/api/falukant/church/baptise', {
characterId: person.id,
firstName: person.proposedFirstName
})
this.loadNotBaptisedChildren();
this.$root.$refs.messageDialog.open('tr:falukant.church.baptism.success')
} catch (err) {
console.error(err)
this.$root.$refs.errorDialog.open('tr:falukant.church.baptism.error')
}
}
}
}
</script>
<style scoped>
h2 {
padding-top: 20px;
}
.simple-tabs {
display: flex;
margin-top: 1rem;
}
.simple-tab {
padding: 0.5rem 1rem;
background: #fff;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.simple-tab.active {
background: #F9A22C;
color: #000;
}
.tab-content {
margin-top: 1rem;
}
input[type="text"] {
width: 140px;
margin-right: 0.5rem;
box-sizing: border-box;
}
th {
text-align: left;
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<div class="director-view">
<StatusBar />
<div class="content-container">
<!-- Left: Director list -->
<div class="list-panel">
<h2>{{ $t('falukant.director.title') }}</h2>
<table class="director-table">
<thead>
<tr>
<th>{{ $t('falukant.director.name') }}</th>
<th>{{ $t('falukant.director.branch') }}</th>
<th>{{ $t('falukant.director.age') }}</th>
<th>{{ $t('falukant.director.satisfaction') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="dir in directors" :key="dir.id" @click="selectDirector(dir)"
:class="{ selected: dir.id === selected?.id }" class="director-row">
<td>
{{ $t(`falukant.titles.${dir.character.gender}.${dir.character.nobleTitle.labelTr}`) }}
{{ dir.character.definedFirstName.name }} {{ dir.character.definedLastName.name }}
</td>
<td>{{ dir.region || '-' }}</td>
<td>{{ dir.age }}</td>
<td>{{ dir.satisfaction }} %</td>
</tr>
</tbody>
</table>
</div>
<!-- Right: Selected director detail -->
<div class="detail-panel" v-if="selected">
<h2>
{{ $t(`falukant.titles.${selected.character.gender}.${selected.character.nobleTitle.labelTr}`) }}
{{ selected.character.definedFirstName.name }} {{ selected.character.definedLastName.name }}
</h2>
<p>{{ $t('falukant.director.age') }}: {{ selected.age }}</p>
<h3>{{ $t('falukant.director.knowledge.title') }}</h3>
<div class="table-container">
<table class="knowledge-table">
<thead>
<tr>
<th>{{ $t('falukant.director.product') }}</th>
<th>{{ $t('falukant.director.knowledge.knowledge') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in selected.character.knowledges" :key="item.productId">
<td>{{ $t(`falukant.product.${item.productType.labelTr}`) }}</td>
<td>{{ item.knowledge }} %</td>
</tr>
</tbody>
</table>
</div>
<div class="actions">
<div>
<label>
{{ $t('falukant.director.satisfaction') }}:
<span> {{ selected.satisfaction }} %</span>
</label>
</div>
<div>
<label>
{{ $t('falukant.director.income') }}:
<input type="text" v-model="selected.income" />
</label>
<span v-if="selected.satisfaction < 100" @click="setWishedIncome" class="link">({{ $t('falukant.director.wishedIncome') }}: {{ selected.wishedIncome }})</span>
</div>
<div>
<button @click="updateDirector">{{ $t('falukant.director.updateButton') }}</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
export default {
name: 'DirectorView',
components: { StatusBar },
data() {
return {
directors: [],
selected: null,
editIncome: '',
editSatisfaction: 0
};
},
computed: {
...mapState(['daemonSocket'])
},
async mounted() {
await this.loadDirectors();
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
},
beforeUnmount() {
if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
}
},
methods: {
async loadDirectors() {
try {
const { data } = await apiClient.get('/api/falukant/directors');
this.directors = data.map(d => ({
...d,
branchName: d.branch?.regionName || null
}));
} catch (err) {
console.error('Error loading directors', err);
}
},
selectDirector(dir) {
this.selected = { ...dir };
this.editIncome = dir.income;
this.editSatisfaction = dir.satisfaction;
},
async updateDirector() {
try {
await apiClient.post('/api/falukant/directors', {
directorId: this.selected.id,
income: this.selected.income,
});
await this.loadDirectors();
this.selected = this.directors.find(d => d.id === this.selected.id);
} catch (err) {
console.error('Error updating director', err);
}
},
handleDaemonMessage(evt) {
try {
if (evt.data === 'ping') {
return;
}
const msg = JSON.parse(evt.data);
if (msg.event === 'directorchanged') {
this.loadDirectors();
if (this.selected) {
const updated = this.directors.find(d => d.id === this.selected.id);
if (updated) {
this.selected = { ...updated };
}
}
}
} catch (err) {
console.error('Error parsing daemon message', err, evt.data);
}
},
setWishedIncome() {
this.selected.income = this.selected.wishedIncome;
}
}
};
</script>
<style scoped>
.director-view .content-container {
display: flex;
gap: 20px;
}
.list-panel {
flex: 1;
overflow-y: auto;
}
.detail-panel {
flex: 1;
padding: 10px;
border-left: 1px solid #ccc;
}
.director-table,
.knowledge-table {
border-collapse: collapse;
}
.director-table th,
.director-table td,
.knowledge-table th,
.knowledge-table td {
border: 1px solid #ddd;
padding: 8px;
}
.selected {
background-color: #f0f8ff;
}
.actions {
margin-top: 10px;
display: flex;
gap: 10px;
align-items: center;
}
.actions label {
display: flex;
align-items: center;
gap: 5px;
}
button {
padding: 6px 12px;
cursor: pointer;
}
h2 {
padding-top: 20px;
}
.table-container {
max-height: 50vh;
/* maximal 50% der Viewport-Höhe */
min-height: 5em;
/* mindestens 5em hoch */
overflow-y: auto;
/* nur vertikales Scrollen */
}
.knowledge-table {
border-collapse: collapse;
table-layout: fixed;
/* Spalten fest verteilen */
}
/* Header-Zellen kleben oben im scrollenden Container */
.knowledge-table thead th {
position: sticky;
top: 0;
background: white;
/* Hintergrund, damit darunterliegender Inhalt nicht durchscheint */
z-index: 1;
/* sicherstellen, dass der Header immer oben liegt */
padding: 0.5em;
border: 1px solid #ccc;
}
/* Zellen-Styles für Körper und Kopf */
.knowledge-table th,
.knowledge-table td {
padding: 0.5em;
border: 1px solid #ccc;
text-align: left;
}
.director-row {
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,300 @@
<template>
<div class="contenthidden">
<StatusBar />
<div class="contentscroll">
<h2>{{ $t('falukant.education.title') }}</h2>
<SimpleTabs v-model="activeTab" :tabs="tabs" />
<!-- SELF -->
<div v-if="activeTab === 'self'">
<table>
<thead>
<tr>
<th>{{ $t('falukant.education.table.article') }}</th>
<th>{{ $t('falukant.education.table.knowledge') }}</th>
<th>{{ $t('falukant.education.table.activity') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product.id">
<td>{{ $t(`falukant.product.${product.labelTr}`) }}</td>
<td>{{ product.knowledges[0].knowledge }} %</td>
<td>
<button
v-if="ownRunningEducations.length === 0"
@click="learnItem(product.id, 'self')"
>
{{ $t('falukant.education.learn') }}
({{ formatCost(getSelfCost(product.knowledges[0].knowledge)) }})
</button>
</td>
</tr>
</tbody>
</table>
<div>
<button
v-if="ownRunningEducations.length === 0"
@click="learnAll('self')"
>
{{ $t('falukant.education.learnAll') }}
({{ formatCost(getSelfAllCost()) }})
</button>
</div>
</div>
<!-- CHILDREN -->
<div v-else-if="activeTab === 'children'">
<div>
<select v-model="activeChild">
<option v-for="child in children" :key="child.id" :value="child.id">
{{ child.name }} ({{ child.age }})
</option>
</select>
</div>
<table v-if="activeChild">
<thead>
<tr>
<th>{{ $t('falukant.education.table.article') }}</th>
<th>{{ $t('falukant.education.table.knowledge') }}</th>
<th>{{ $t('falukant.education.table.activity') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product.id">
<td>{{ $t(`falukant.product.${product.labelTr}`) }}</td>
<td>{{ getChildKnowledge(product.id) }} %</td>
<td>
<button
v-if="childNotInLearning()"
@click="learnItem(product.id, 'children', activeChild)"
>
{{ $t('falukant.education.learn') }}
({{ formatCost(getChildCost(product.id)) }})
</button>
</td>
</tr>
</tbody>
</table>
<div>
<button
v-if="childrenRunningEducations.length === 0"
@click="learnAll('children', activeChild)"
>
{{ $t('falukant.education.learnAll') }}
({{ formatCost(getChildrenAllCost(activeChild)) }})
</button>
</div>
</div>
<!-- DIRECTOR -->
<div v-else-if="activeTab === 'director'">
<div>
<select v-model="activeDirector">
<option v-for="director in directors" :key="director.id" :value="director.id">
{{ director.character.nobleTitle.tr }}
{{ director.character.definedFirstName.name }}
{{ director.character.definedLastName.name }}
</option>
</select>
</div>
<table v-if="activeDirector">
<thead>
<tr>
<th>{{ $t('falukant.education.table.article') }}</th>
<th>{{ $t('falukant.education.table.knowledge') }}</th>
<th>{{ $t('falukant.education.table.activity') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product.id">
<td>{{ $t(`falukant.product.${product.labelTr}`) }}</td>
<td>{{ getDirectorKnowledge(product.id) }} %</td>
<td>
<button
v-if="directorNotInLearning()"
@click="learnItem(product.id, 'director', getDirectorCharacterId())"
>
{{ $t('falukant.education.learn') }}
({{ formatCost(getDirectorCost(product.id)) }})
</button>
</td>
</tr>
</tbody>
</table>
<div v-if="activeDirector">
<button
v-if="directorNotInLearning()"
@click="learnAll('director', getDirectorCharacterId())"
>
{{ $t('falukant.education.learnAll') }}
({{ formatCost(getDirectorAllCost(getDirectorCharacterId())) }})
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import MessageDialog from '@/dialogues/standard/MessageDialog.vue';
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
import apiClient from '@/utils/axios.js';
import SimpleTabs from '@/components/SimpleTabs.vue'
const KNOWLEDGE_MAX = 99;
const COST_CONFIG = {
one: { min: 50, max: 5000 },
all: { min: 400, max: 40000 }
};
export default {
name: 'EducationView',
components: { StatusBar, MessageDialog, ErrorDialog, SimpleTabs },
data() {
return {
activeTab: 'self',
tabs: [
{ value: 'self', label: 'falukant.education.self.title' },
{ value: 'children', label: 'falukant.education.children.title' },
{ value: 'director', label: 'falukant.education.director.title' }
],
products: [],
ownRunningEducations: [],
childrenRunningEducations: [],
directorRunningEducations: [],
directors: [],
activeDirector: null,
children: [],
activeChild: null,
}
},
async mounted() {
await this.loadProducts();
await this.loadEducations();
await this.loadDirectors();
await this.loadChildren();
},
methods: {
// Basis-Funktion: lineare Interpolation
computeCost(knowledgePercent, type = 'one') {
const cfg = COST_CONFIG[type];
const f = Math.min(Math.max(knowledgePercent, 0), KNOWLEDGE_MAX) / KNOWLEDGE_MAX;
return cfg.min + (cfg.max - cfg.min) * f;
},
formatCost(value) {
return Math.round(value).toLocaleString(this.$i18n.locale || 'de-DE');
},
// SELF
getSelfCost(knowledge) {
return this.computeCost(knowledge, 'one');
},
getSelfAllCost() {
const avg = this.products.reduce((sum, p) => sum + (p.knowledges[0].knowledge||0), 0) / this.products.length;
return this.computeCost(avg, 'all');
},
// CHILD
getChildKnowledge(productId) {
const child = this.children.find(c => c.id === this.activeChild);
if (!child?.knowledge) return 0;
const e = child.knowledge.find(k => k.id === productId);
return e ? e.knowledge : 0;
},
getChildCost(productId) {
return this.computeCost(this.getChildKnowledge(productId), 'one');
},
getChildrenAllCost(childId) {
const child = this.children.find(c => c.id === childId);
const avg = (child.knowledge || []).reduce((s,k) => s + k.knowledge, 0) / (child.knowledge?.length||1);
return this.computeCost(avg, 'all');
},
childNotInLearning() {
const child = this.children.find(c => c.id === this.activeChild);
return !this.childrenRunningEducations.some(e => e.learningCharacter.id === child.id);
},
// DIRECTOR
getDirectorKnowledge(productId) {
const dir = this.directors.find(d => d.id === this.activeDirector);
const know = dir?.character?.knowledges?.find(k => k.productId === productId);
return know ? know.knowledge : 0;
},
getDirectorCost(productId) {
return this.computeCost(this.getDirectorKnowledge(productId), 'one');
},
getDirectorAllCost(dirCharId) {
const dir = this.directors.find(d => d.character.id === dirCharId);
const avg = (dir.character.knowledges || []).reduce((s,k) => s + k.knowledge, 0) / (dir.character.knowledges.length||1);
return this.computeCost(avg, 'all');
},
getDirectorCharacterId() {
return this.directors.find(d => d.id === this.activeDirector)?.character?.id;
},
directorNotInLearning() {
const dirCharId = this.getDirectorCharacterId();
return !this.directorRunningEducations.some(e => e.learningCharacter.id === dirCharId);
},
// Laden & Aktionen
async loadProducts() {
const r = await apiClient.get('/api/falukant/products');
this.products = r.data;
},
async loadEducations() {
const r = await apiClient.get('/api/falukant/education');
this.ownRunningEducations = r.data.filter(e => e.recipient.tr === 'self');
this.childrenRunningEducations = r.data.filter(e => e.recipient.tr === 'children');
this.directorRunningEducations = r.data.filter(e => e.recipient.tr === 'director');
},
async loadDirectors() {
const r = await apiClient.get('/api/falukant/directors');
this.directors = r.data;
this.activeDirector = this.directors[0]?.id;
},
async loadChildren() {
const r = await apiClient.get('/api/falukant/family/children');
this.children = r.data;
this.activeChild = this.children[0]?.id;
},
async learnItem(item, student, studentId) {
await apiClient.post('/api/falukant/education', { item, student, studentId });
await this.loadEducations();
},
async learnAll(student, studentId) {
await apiClient.post('/api/falukant/education', { item: 'all', student, studentId });
await this.loadEducations();
}
}
}
</script>
<style scoped>
h2 {
padding-top: 20px;
}
.simple-tabs {
display: flex;
margin-top: 1rem;
}
.simple-tab {
padding: 0.5rem 1rem;
background: #fff;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.simple-tab.active {
background: #F9A22C;
color: #000;
}
.tab-content {
margin-top: 1rem;
}
</style>

View File

@@ -41,6 +41,10 @@
</div>
</td>
</tr>
<tr v-if="relationships[0].relationshipType === 'engaged'" colspan="2">
<button @click="jumpToPartyForm">{{ $t('falukant.family.spouse.jumpToPartyForm')
}}</button>
</tr>
</table>
<ul>
<li v-for="characteristic in relationships[0].character2.characterTrait"
@@ -115,10 +119,12 @@
</thead>
<tbody>
<tr v-for="(child, index) in children" :key="index">
<td>
{{ $t('falukant.titles.' + child.gender + '.' + child.title) }}
<td v-if="child.hasName">
{{ child.name }}
</td>
<td v-else>
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism') }}</button>
</td>
<td>{{ child.age }}</td>
<td>
<button @click="showChildDetails(child)">
@@ -191,6 +197,9 @@ export default {
await this.loadGifts();
await this.loadMoodAffects();
await this.loadCharacterAffects();
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
},
methods: {
async loadFamilyData() {
@@ -247,10 +256,10 @@ export default {
return;
}
try {
await apiClient.post('/api/falukant/family/gift'
, { giftId: this.selectedGiftId });
this.loadFamilyData();
this.$root.$refs.messageDialog.open('tr:falukant.family.sendgift.success');
await apiClient.post('/api/falukant/family/gift'
, { giftId: this.selectedGiftId });
this.loadFamilyData();
this.$root.$refs.messageDialog.open('tr:falukant.family.sendgift.success');
} catch (error) {
console.log(error.response);
if (error.response.status === 412) {
@@ -285,6 +294,26 @@ export default {
const green = Math.round(255 * pct);
return `rgb(${red}, ${green}, 0)`;
},
jumpToPartyForm() {
this.$router.push({
name: 'ReputationView',
query: { tab: 'party' }
});
},
jumpToChurchForm() {
this.$router.push({
name: 'ChurchView',
});
},
handleDaemonMessage() {
const message = JSON.parse(event.data);
if (message.event === 'children_update') {
this.loadFamilyData();
}
}
}
}
</script>
@@ -369,15 +398,15 @@ h2 {
}
.progress {
width: 100%;
background-color: #e5e7eb;
border-radius: 0.25rem;
overflow: hidden;
height: 1rem;
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;
height: 100%;
transition: width 0.3s ease, background-color 0.3s ease;
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div>
<StatusBar />
<h2>{{ $t('falukant.healthview.title') }}</h2>
<div class="content-container">
<div class="info-panel">
<p>{{ $t('falukant.healthview.age') }}: {{ age }}</p>
<p>{{ $t('falukant.healthview.status') }}: {{ healthState }}</p>
</div>
<div class="measures-panel">
<h3>{{ $t('falukant.healthview.measuresTaken') }}</h3>
<table class="measures-table">
<thead>
<tr>
<th>{{ $t('falukant.healthview.measure') }}</th>
<th>{{ $t('falukant.healthview.date') }}</th>
<th>{{ $t('falukant.healthview.success') }}</th>
<th>{{ $t('falukant.healthview.cost') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in measuresTaken" :key="entry.id">
<td>{{ $t(`falukant.healthview.measures.${entry.tr}`) }}</td>
<td>{{ formatDate(entry.createdAt) }}</td>
<td>{{ entry.success }}</td>
<td>{{ formatPrice(entry.cost) }}</td>
</tr>
</tbody>
</table>
<div class="actions">
<label>
{{ $t('falukant.healthview.selectMeasure') }}:
<select v-model="selectedTr">
<option value="" disabled>{{ $t('falukant.healthview.choose') }}</option>
<option v-for="m in availableMeasures" :key="m.tr" :value="m.tr">
{{ $t(`falukant.healthview.measures.${m.tr}`) }} ({{ formatPrice(m.cost) }})
</option>
</select>
</label>
<button @click="performMeasure" :disabled="!selectedMeasure">
{{ $t('falukant.healthview.perform') }}
<span v-if="selectedMeasure"> ({{ formatPrice(selectedMeasure.cost) }})</span>
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
export default {
name: 'HealthView',
components: { StatusBar },
data() {
return {
age: 0,
healthStatus: 0,
measuresTaken: [],
availableMeasures: [],
selectedTr: '',
};
},
computed: {
...mapState(['daemonSocket']),
/**
* Selected measure object based on selectedTr
*/
selectedMeasure() {
return this.availableMeasures.find(m => m.tr === this.selectedTr) || null;
},
/**
* Health state translation key based on status
*/
healthState() {
if (this.healthStatus > 90) return this.$t('falukant.health.amazing');
if (this.healthStatus > 75) return this.$t('falukant.health.good');
if (this.healthStatus > 50) return this.$t('falukant.health.normal');
if (this.healthStatus > 25) return this.$t('falukant.health.bad');
return this.$t('falukant.health.very_bad');
}
},
async mounted() {
await this.loadHealthData();
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
},
beforeUnmount() {
if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
}
},
methods: {
async loadHealthData() {
try {
const { data } = await apiClient.get('/api/falukant/health');
this.age = data.age;
this.healthStatus = data.status;
this.measuresTaken = data.history;
this.availableMeasures = data.healthActivities;
} catch (err) {
console.error('Error loading health data', err);
}
},
formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString();
},
async performMeasure() {
if (!this.selectedMeasure) return;
try {
await apiClient.post('/api/falukant/health', {
measureTr: this.selectedTr
});
await this.loadHealthData();
this.selectedTr = '';
} catch (err) {
console.error('Error performing measure', err);
}
},
handleDaemonMessage(evt) {
if (evt.data === 'ping') return;
const msg = JSON.parse(evt.data);
if (msg.event === 'healthupdated') {
this.loadHealthData();
}
},
formatPrice(value) {
return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
},
}
};
</script>
<style scoped>
h2 {
padding-top: 20px;
margin: 0 0 10px;
}
.content-container {
display: flex;
gap: 20px;
}
.info-panel {
flex: 1;
padding: 10px;
}
.measures-panel {
flex: 2;
padding: 10px;
border-left: 1px solid #ccc;
}
.measures-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1em;
}
.measures-table th,
.measures-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
}
button {
padding: 6px 12px;
cursor: pointer;
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<div class="houseView">
<div class="house-view">
<StatusBar />
<h2>{{ $t('falukant.house.title') }}</h2>
<div class="existingHouse">
<div :style="houseStyle(picturePosition)" class="house"></div>
<div class="statusreport">
<div class="existing-house">
<div :style="houseType ? houseStyle(houseType.position, 341) : {}" class="house"></div>
<div class="status-panel">
<h3>{{ $t('falukant.house.statusreport') }}</h3>
<table>
<thead>
@@ -15,46 +15,54 @@
</tr>
</thead>
<tbody>
<tr v-for="status, index in status">
<td>{{ $t(`falukant.house.status.${index}`) }}</td>
<td>{{ status }} %</td>
<td><button v-if="status < 100">{{ $t('falukant.house.renovate') }} ({{
$t('falukant.house.cost') }}: {{ getRenovationCost(index, status) }}</button></td>
<tr v-for="(value, key) in status" :key="key">
<td>{{ $t(`falukant.house.status.${key}`) }}</td>
<td>{{ value }}%</td>
<td>
<button v-if="value < 100" @click="renovate(key)">
{{ $t('falukant.house.renovate') }} ({{ getRenovationCost(key, value) }})
</button>
</td>
</tr>
<tr>
<td>{{ $t('falukant.house.worth') }}</td>
<td>{{ getWorth(status) }}</td>
<td><button @click="sellHouse">{{ $t('falukant.house.sell') }}</button></td>
<td>{{ getWorth() }} {{ currency }}</td>
<td>
<button @click="renovateAll" :disabled="allRenovated">
{{ $t('falukant.house.renovateAll') }} ({{ getAllRenovationCost() }})
</button>
<button @click="sellHouse">
{{ $t('falukant.house.sell') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="buyablehouses">
<div class="buyable-houses">
<h3>{{ $t('falukant.house.buyablehouses') }}</h3>
<div style="overflow:auto">
<div style="display: flex; flex-direction: row" v-for="house in buyableHouses">
<div style="width:100px; height:100px; display: hidden;">
<div :style="houseStyle(house.houseType.position)" class="housePreview buyableHouseInfo"></div>
</div>
<div class="buyableHouseInfo">
<h4 style="display: inline;">{{ $t('falukant.house.statusreport') }}</h4>
<div class="houses-list">
<div v-for="house in buyableHouses" :key="house.id" class="house-item">
<div :style="house.houseType ? houseStyle(house.houseType.position, 114) : {}" class="house-preview"></div>
<div class="house-info">
<h4>{{ $t(`falukant.house.type.${house.houseType.labelTr}`) }}</h4>
<table>
<tbody>
<template v-for="value, key in house">
<tr v-if="key != 'houseType' && key != 'id'">
<td>{{ $t(`falukant.house.status.${key}`) }}</td>
<td>{{ value }} %</td>
</tr>
</template>
<tr v-for="(val, prop) in house" :key="prop"
v-if="['roofCondition','wallCondition','floorCondition','windowCondition'].includes(prop)">
<td>{{ $t(`falukant.house.status.${prop}`) }}</td>
<td>{{ val }}%</td>
</tr>
</tbody>
</table>
</div>
<div>
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
</div>
<div>
<button @click="buyHouse(house.id)">{{ $t('falukant.house.buy') }}</button>
<div>
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
</div>
<button @click="buyHouse(house.id)">
{{ $t('falukant.house.buy') }}
</button>
</div>
</div>
</div>
@@ -65,195 +73,209 @@
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from "vuex";
import { mapState } from 'vuex';
export default {
name: 'HouseView',
components: {
StatusBar
},
components: { StatusBar },
data() {
return {
houseTypes: [],
userHouse: {},
userHouse: null,
houseType: {},
status: {},
buyableHouses: [],
picturePosition: 0,
}
},
methods: {
async loadHouseTypes() {
try {
const houseTypesResult = await apiClient.get('/api/falukant/houses/types');
this.houseTypes = houseTypesResult.data;
} catch (error) {
}
},
async loadUserHouse() {
try {
const userHouseResult = await apiClient.get('/api/falukant/houses');
Object.assign(this.userHouse, userHouseResult.data);
const { houseType, ...houseStatus } = this.userHouse;
this.status = houseStatus;
this.picturePosition = parseInt(houseType.position);
this.houseType = houseType;
} catch (error) {
console.error('Fehler beim Laden des Hauses:', error);
this.userHouse = null;
this.status = null;
}
},
async loadBuyableHouses() {
try {
const buyableHousesResult = await apiClient.get('/api/falukant/houses/buyable');
this.buyableHouses = buyableHousesResult.data;
} catch (error) {
console.error('Fehler beim Laden der kaufbaren Häuser:', error);
}
},
houseStyle(housePosition) {
const columns = 3;
const spriteSize = 341; // Breite & Höhe eines einzelnen Hauses
let calculatePosition = Math.max(housePosition - 1, 0);
const x = (calculatePosition % columns) * spriteSize;
const y = Math.floor(calculatePosition / columns) * spriteSize;
return {
backgroundImage: 'url("/images/falukant/houses.png")',
backgroundPosition: `-${x}px -${y}px`,
backgroundSize: `${columns * spriteSize}px auto`, // z.B. 1023px auto
};
},
buyCost(house) {
const houseQuality = (house.roofCondition + house.windowCondition + house.floorCondition + house.wallCondition) / 4;
return (house.houseType.cost / 100 * houseQuality).toFixed(2);
},
getWorth() {
const house = {...this.userHouse, houseType: this.houseType};
const buyWorth = this.buyCost(house);
return (buyWorth * 0.8).toFixed(2);
},
async buyHouse(houseId) {
try {
const response = await apiClient.post('/api/falukant/houses',
{
houseId: houseId,
}
);
this.$router.push({ name: 'HouseView' });
} catch (error) {
console.error('Fehler beim Kaufen des Hauses:', error);
}
},
async getHouseData() {
await this.loadUserHouse();
await this.loadBuyableHouses();
}
currency: '€'
};
},
computed: {
...mapState(['socket', 'daemonSocket']),
getHouseStyle() {
if (!this.userHouse || this.userHouse.position === undefined || this.userHouse.position === null) {
return {};
}
return this.houseStyle(this.userHouse.position);
},
getHouseType(position) {
const houseType = this.houseTypes[position];
return houseType;
},
getHouseStatus(position) {
const houseStatus = this.houseStatuses[position];
return houseStatus;
},
getRenovationCost(index, status) {
const houseType = this.houseTypes[position];
const renovationCost = houseType.renovationCosts[status];
return renovationCost;
},
allRenovated() {
return Object.values(this.status).every(v => v >= 100);
}
},
created() {
methods: {
async loadData() {
try {
const userRes = await apiClient.get('/api/falukant/houses');
this.userHouse = userRes.data;
this.houseType = this.userHouse.houseType;
const { roofCondition, wallCondition, floorCondition, windowCondition } = this.userHouse;
this.status = { roofCondition, wallCondition, floorCondition, windowCondition };
const buyRes = await apiClient.get('/api/falukant/houses/buyable');
this.buyableHouses = buyRes.data;
} catch (err) {
console.error('Error loading house data', err);
}
},
houseStyle(position, picSize) {
const columns = 3;
const size = picSize;
const index = position - 1;
const x = (index % columns) * size;
const y = Math.floor(index / columns) * size;
return {
backgroundImage: 'url("/images/falukant/houses.png")',
backgroundPosition: `-${x}px -${y}px`,
backgroundSize: `${columns * size}px auto`
};
},
formatPrice(value) {
return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
},
getRenovationCost(key, value) {
const base = this.userHouse.houseType.cost || 0;
const weights = { roofCondition: 0.25, wallCondition: 0.25, floorCondition: 0.25, windowCondition: 0.25 };
const weight = weights[key] || 0;
const missing = 100 - value;
const cost = (missing / 100) * base * weight;
return this.formatPrice(cost);
},
getAllRenovationCost() {
const total = Object.keys(this.status).reduce((sum, k) => {
const raw = parseFloat(this.getRenovationCost(k, this.status[k]).replace(/\./g, '').replace(',', '.'));
return sum + (isNaN(raw) ? 0 : raw);
}, 0);
return this.formatPrice(total * 0.8);
},
getWorth() {
const vals = Object.values(this.status);
if (!vals.length) return this.formatPrice(0);
const avg = vals.reduce((s, v) => s + v, 0) / vals.length;
const price = this.houseType.cost || 0;
return this.formatPrice(price * avg / 100 * 0.8);
},
buyCost(house) {
const avg = (house.roofCondition + house.wallCondition + house.floorCondition + house.windowCondition) / 4;
return this.formatPrice(house.houseType.cost * avg / 100);
},
async renovate(key) {
try {
await apiClient.post('/api/falukant/houses/renovate', { element: key });
await this.loadData();
} catch (err) {
console.error('Error renovating', err);
}
},
async renovateAll() {
try {
await apiClient.post('/api/falukant/houses/renovate-all');
await this.loadData();
} catch (err) {
console.error('Error renovating all', err);
}
},
async sellHouse() {
try {
await apiClient.post('/api/falukant/houses/sell');
await this.loadData();
} catch (err) {
console.error('Error selling house', err);
}
},
async buyHouse(id) {
try {
await apiClient.post('/api/falukant/houses', { houseId: id });
await this.loadData();
} catch (err) {
console.error('Error buying house', err);
}
},
handleDaemonMessage(evt) {
try {
const msg = JSON.parse(evt.data);
if (msg.event === 'houseupdated') this.loadData();
} catch {}
}
},
async mounted() {
this.loadHouseTypes();
await this.getHouseData();
if (this.socket) {
this.socket.on("falukantHouseUpdate", this.getHouseData);
}
if (this.daemonSocket) {
this.daemonSocket.addEventListener("message", this.handleDaemonSocketMessage);
}
await this.loadData();
if (this.socket) this.socket.on('falukantHouseUpdate', this.loadData);
if (this.daemonSocket) this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
},
beforeUnmount() {
if (this.socket) {
this.socket.off("falukantHouseUpdate", this.fetchStatus);
}
if (this.daemonSocket) {
this.daemonSocket.removeEventListener("message", this.handleDaemonSocketMessage);
}
},
watch: {
if (this.socket) this.socket.off('falukantHouseUpdate', this.loadData);
if (this.daemonSocket) this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
}
}
};
</script>
<style lang="scss" scoped>
<style scoped>
.house-view {
display: flex;
flex-direction: column;
gap: 20px;
}
h2 {
padding-top: 20px;
margin: 0 0 10px;
}
.existingHouse {
display: block;
width: auto;
height: 255px;
}
Element {
background-position: 71px 54px;
.existing-house {
display: flex;
gap: 20px;
}
.house {
border: 1px solid #ccc;
border-radius: 4px;
background-repeat: no-repeat;
image-rendering: crisp-edges;
transform: scale(0.7);
transform-origin: top left;
display: inline-block;
overflow: hidden;
width: 341px;
height: 341px;
}
.statusreport {
display: inline-block;
vertical-align: top;
height: 250px;
}
.buyableHouseInfo {
vertical-align: top;
}
.housePreview {
transform: scale(0.2);
width: 341px;
height: 341px;
transform-origin: top left;
background-repeat: no-repeat;
image-rendering: crisp-edges;
border: 1px solid #ccc;
border-radius: 4px;
}
.houseView {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
.status-panel {
flex: 1;
}
.buyablehouses {
.buyable-houses {
display: flex;
flex-direction: column;
overflow: hidden;
gap: 10px;
}
.houses-list {
display: flex;
flex-direction: column; /* vertical list */
gap: 20px;
max-height: 400px;
overflow-y: auto; /* vertical scroll if needed */
}
.house-item {
display: flex;
flex-direction: column;
gap: 5px;
align-items: center;
}
.house-preview {
width: 100px;
height: 100px;
background-repeat: no-repeat;
image-rendering: crisp-edges;
border: 1px solid #ccc;
border-radius: 4px;
background-size: contain; /* scale image to container */
background-position: center; /* center sprite */
}
table {
width: 100%;
border-collapse: collapse;
}
table th,
table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
button {
padding: 6px 12px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,130 @@
<template>
<div class="contenthidden">
<StatusBar />
<div class="contentscroll">
<h2>{{ $t('falukant.nobility.title') }}</h2>
<SimpleTabs v-model="activeTab" :tabs="tabs" />
<!-- OVERVIEW -->
<div v-if="activeTab === 'overview'">
<div class="nobility-section">
<p>
<strong>
{{ $t(`falukant.titles.${gender}.${current.labelTr}`) }}
</strong>
</p>
</div>
</div>
<!-- ADVANCE -->
<div v-else-if="activeTab === 'advance'">
<div class="advance-section">
<p>
{{ $t('falukant.nobility.nextTitle') }}:
<strong>{{ $t(`falukant.titles.${gender}.${next.labelTr}`) }}</strong>
</p>
<ul class="prerequisites">
<li v-for="req in next.requirements" :key="req.titleId">
{{ $t(`falukant.nobility.requirement.${req.requirementType}`, { amount: formatCost(req.requirementValue) }) }}
</li>
</ul>
<button @click="applyAdvance" class="button" :disabled="!canAdvance || isAdvancing">
<span v-if="!isAdvancing">{{ $t('falukant.nobility.advance.confirm') }}</span>
<span v-else>{{ $t('falukant.nobility.advance.processing') }}</span>
</button>
<span>-&gt;{{ canAdvance }}, {{ isAdvancing }}&lt;-</span>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import SimpleTabs from '@/components/SimpleTabs.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
export default {
name: 'NobilityView',
components: { StatusBar, SimpleTabs },
data() {
return {
activeTab: 'overview',
tabs: [
{ value: 'overview', label: 'falukant.nobility.tabs.overview' },
{ value: 'advance', label: 'falukant.nobility.tabs.advance' }
],
current: { labelTr: '', requirements: [], charactersWithNobleTitle: [] },
next: { labelTr: '', requirements: [] },
isAdvancing: false
};
},
computed: {
...mapState(['daemonSocket', 'falukantData']),
gender() {
return this.current.charactersWithNobleTitle[0]?.gender || 'male';
},
canAdvance() {
return true;
}
},
async mounted() {
await this.loadNobility();
if (this.daemonSocket) this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
},
beforeUnmount() {
if (this.daemonSocket) this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
},
methods: {
async loadNobility() {
try {
const { data } = await apiClient.get('/api/falukant/nobility');
this.current = data.current;
this.next = data.next;
} catch (err) {
console.error('Error loading nobility:', err);
}
},
async applyAdvance() {
if (!this.canAdvance || this.isAdvancing) return;
this.isAdvancing = true;
try {
await apiClient.post('/api/falukant/nobility/advance');
await this.loadNobility();
} catch (err) {
console.error('Error advancing nobility:', err);
} finally {
this.isAdvancing = false;
}
},
handleDaemonMessage(evt) {
if (evt.data === 'ping') return;
const msg = JSON.parse(evt.data);
if (['nobilityChange', 'moneyChange'].includes(msg.event)) this.loadNobility();
},
formatCost(val) {
return new Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(val);
},
async applyAdvance() {
await apiClient.post('/api/falukant/nobility');
}
}
};
</script>
<style scoped lang="scss">
h2 { padding-top: 20px; }
.nobility-section,
.advance-section {
border: 1px solid #ccc;
border-radius: 4px;
margin: 10px 0;
padding: 10px;
}
.prerequisites {
list-style: disc inside;
margin-bottom: 1rem;
}
</style>

View File

@@ -0,0 +1,301 @@
<template>
<div class="reputation-view">
<StatusBar />
<h2>{{ $t('falukant.reputation.title') }}</h2>
<div class="simple-tabs">
<button v-for="tab in tabs" :key="tab.value" :class="['simple-tab', { active: activeTab === tab.value }]"
@click="activeTab = tab.value">
{{ $t(tab.label) }}
</button>
</div>
<div class="tab-content">
<div v-if="activeTab === 'overview'">
<p>Deine aktuelle Reputation: </p>
</div>
<div v-else-if="activeTab === 'party'">
<button @click="toggleNewPartyView">
{{ $t('falukant.reputation.party.newpartyview.' + (newPartyView ? 'close' : 'open')) }}
</button>
<div v-if="newPartyView" class="new-party-form">
<label>
{{ $t('falukant.reputation.party.newpartyview.type') }}:
<select v-model.number="newPartyTypeId">
<option v-for="type in partyTypes" :key="type.id" :value="type.id">
{{ $t('falukant.party.type.' + type.tr) }}
</option>
</select>
</label>
<div v-if="newPartyTypeId" class="party-options">
<label>
{{ $t('falukant.reputation.party.music.label') }}:
<select v-model.number="musicId">
<option v-for="m in musicTypes" :key="m.id" :value="m.id">
{{ $t(`falukant.reputation.party.music.${m.tr}`) }}
</option>
</select>
</label>
<label>
{{ $t('falukant.reputation.party.banquette.label') }}:
<select v-model.number="banquetteId">
<option v-for="b in banquetteTypes" :key="b.id" :value="b.id">
{{ $t(`falukant.reputation.party.banquette.${b.tr}`) }}
</option>
</select>
</label>
<label>
{{ $t('falukant.reputation.party.servants.label') }}:
<input type="number" v-model.number="servantRatio" min="1" max="50" />
{{ $t('falukant.reputation.party.servants.perPersons') }}
</label>
<label>
{{ $t('falukant.reputation.party.esteemedInvites.label') }}:
<multiselect v-model="selectedNobilityIds" :options="nobilityTitles" :multiple="true"
track-by="id" label="labelTr" :close-on-select="false" :preserve-search="true"
placeholder="">
<template #option="{ option }">
{{ $t('falukant.titles.male.' + option.labelTr) }}
</template>
<template #tag="{ option, remove }">
<span class="multiselect__tag">
{{ $t('falukant.titles.male.' + option.labelTr) }}
<i @click="remove(option.id)" class="multiselect__tag-icon"></i>
</span>
</template>
</multiselect>
</label>
<p class="total-cost">
{{ $t('falukant.reputation.party.totalCost') }}:
{{ formattedCost }}
</p>
</div>
<div>
<button @click="orderParty()">
{{ $t('falukant.reputation.party.order') }}
</button>
</div>
</div>
<!-- In-Progress Parties -->
<div class="separator-class">
<h3>{{ $t('falukant.reputation.party.inProgress') }}</h3>
<table>
<thead>
<tr>
<th>{{ $t('falukant.reputation.party.type') }}</th>
<th>{{ $t('falukant.reputation.party.music.label') }}</th>
<th>{{ $t('falukant.reputation.party.banquette.label') }}</th>
<th>{{ $t('falukant.reputation.party.servants.label') }}</th>
<th>{{ $t('falukant.reputation.party.cost') }}</th>
<th>{{ $t('falukant.reputation.party.date') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="party in inProgressParties" :key="party.id">
<td>{{ $t('falukant.party.type.' + party.partyType.tr) }}</td>
<td>{{ $t('falukant.reputation.party.music.' + party.musicType.tr) }}</td>
<td>{{ $t('falukant.reputation.party.banquette.' + party.banquetteType.tr) }}</td>
<td>{{ party.servantRatio }}</td>
<td>{{ party.cost.toLocaleString($i18n.locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</td>
<td>{{ new Date(party.createdAt).toLocaleString() }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Completed Parties -->
<div class="separator-class">
<h3>{{ $t('falukant.reputation.party.completed') }}</h3>
<table>
<thead>
<tr>
<th>{{ $t('falukant.reputation.party.type') }}</th>
<th>{{ $t('falukant.reputation.party.music.label') }}</th>
<th>{{ $t('falukant.reputation.party.banquette.label') }}</th>
<th>{{ $t('falukant.reputation.party.servants.label') }}</th>
<th>{{ $t('falukant.reputation.party.cost') }}</th>
<th>{{ $t('falukant.reputation.party.date') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="party in completedParties" :key="party.id">
<td>{{ $t('falukant.party.type.' + party.partyType.tr) }}</td>
<td>{{ $t('falukant.reputation.party.music.' + party.musicType.tr) }}</td>
<td>{{ $t('falukant.reputation.party.banquette.' + party.banquetteType.tr) }}</td>
<td>{{ party.servantRatio }}</td>
<td>{{ party.cost.toLocaleString($i18n.locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</td>
<td>{{ new Date(party.createdAt).toLocaleString() }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue'
import apiClient from '@/utils/axios.js'
import Multiselect from 'vue-multiselect'
export default {
name: 'ReputationView',
components: { StatusBar, Multiselect },
data() {
return {
activeTab: 'overview',
tabs: [
{ value: 'overview', label: 'falukant.reputation.overview.title' },
{ value: 'party', label: 'falukant.reputation.party.title' }
],
newPartyView: false,
newPartyTypeId: null,
partyTypes: [],
musicId: null,
musicTypes: [],
banquetteId: null,
banquetteTypes: [],
nobilityTitles: [],
selectedNobilityIds: [],
servantRatio: 50,
inProgressParties: [],
completedParties: []
}
},
methods: {
toggleNewPartyView() {
this.newPartyView = !this.newPartyView
},
async loadPartyTypes() {
const { data } = await apiClient.get('/api/falukant/party/types');
this.partyTypes = data.partyTypes;
this.musicTypes = data.musicTypes;
this.banquetteTypes = data.banquetteTypes;
this.musicId = this.musicTypes[0]?.id;
this.banquetteId = this.banquetteTypes[0]?.id;
},
async loadParties() {
const { data } = await apiClient.get('/api/falukant/party');
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
this.inProgressParties = data.filter(party => {
const partyDate = new Date(party.createdAt);
return partyDate > yesterday;
});
this.completedParties = data.filter(party => {
const partyDate = new Date(party.createdAt);
return partyDate <= yesterday;
});
},
async loadNobilityTitles() {
this.nobilityTitles = await apiClient.get('/api/falukant/nobility/titels').then(r => r.data)
},
async orderParty() {
await apiClient.post('/api/falukant/party', {
partyTypeId: this.newPartyTypeId,
musicId: this.musicId,
banquetteId: this.banquetteId,
nobilityIds: this.selectedNobilityIds.map(n => n.id ?? n),
servantRatio: this.servantRatio
});
this.toggleNewPartyView();
}
},
computed: {
formattedCost() {
const type = this.partyTypes.find(t => t.id === this.newPartyTypeId) || {};
const music = this.musicTypes.find(m => m.id === this.musicId) || {};
const banquette = this.banquetteTypes.find(b => b.id === this.banquetteId) || {};
let cost = (type.cost || 0) + (music.cost || 0) + (banquette.cost || 0);
cost += (50 / this.servantRatio - 1) * 1000;
let nobilityCost = this.selectedNobilityIds.reduce((sum, id) => {
const nob = this.nobilityTitles.find(n => n.id === id)
return sum + ((nob?.id ^ 5) * 1000)
}, 0);
cost += nobilityCost;
const locale = this.$i18n?.locale || 'de-DE';
return cost.toLocaleString(locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
},
async mounted() {
const tabFromQuery = this.$route?.query?.tab;
if (['overview','party'].includes(tabFromQuery)) {
this.activeTab = tabFromQuery;
}
await this.loadPartyTypes();
await this.loadNobilityTitles();
await this.loadParties();
}
}
</script>
<style scoped>
h2 {
padding-top: 20px;
}
.simple-tabs {
display: flex;
margin-top: 1rem;
}
.simple-tab {
padding: 0.5rem 1rem;
background: #fff;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.simple-tab.active {
background: #F9A22C;
color: #000;
}
.tab-content {
margin-top: 1rem;
}
.new-party-form {
margin-top: 0.5rem;
}
.party-options {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.total-cost {
font-weight: bold;
margin-top: 1rem;
}
.multiselect {
display: inline-block !important;
vertical-align: middle;
}
table th {
text-align: left;
}
.separator-class {
border-top: 1px solid #ccc;
margin-top: 1em;
}
</style>