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
@@ -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,17 +61,39 @@ 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,
@@ -105,6 +130,8 @@ const models = {
MessageHistory,
MessageImage,
Friendship,
// Falukant core
RegionType,
RegionData,
FalukantUser,
@@ -142,6 +169,28 @@ const models = {
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() {
try {
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" },
{ 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 = {
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,
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() {
@@ -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>

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 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>
</template>
</tbody>
</table>
</div>
<div>
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
</div>
<div>
<button @click="buyHouse(house.id)">{{ $t('falukant.house.buy') }}</button>
<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
currency: '€'
};
},
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();
}
},
computed: {
...mapState(['socket', 'daemonSocket']),
getHouseStyle() {
if (!this.userHouse || this.userHouse.position === undefined || this.userHouse.position === null) {
return {};
allRenovated() {
return Object.values(this.status).every(v => v >= 100);
}
return this.houseStyle(this.userHouse.position);
},
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 };
getHouseType(position) {
const houseType = this.houseTypes[position];
return houseType;
const buyRes = await apiClient.get('/api/falukant/houses/buyable');
this.buyableHouses = buyRes.data;
} catch (err) {
console.error('Error loading house data', err);
}
},
getHouseStatus(position) {
const houseStatus = this.houseStatuses[position];
return houseStatus;
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`
};
},
getRenovationCost(index, status) {
const houseType = this.houseTypes[position];
const renovationCost = houseType.renovationCosts[status];
return renovationCost;
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);
},
created() {
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;
width: 341px;
height: 341px;
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;
border: 1px solid #ccc;
border-radius: 4px;
}
.statusreport {
display: inline-block;
vertical-align: top;
height: 250px;
.status-panel {
flex: 1;
}
.buyableHouseInfo {
vertical-align: top;
}
.housePreview {
transform: scale(0.2);
width: 341px;
height: 341px;
transform-origin: top left;
}
.houseView {
.buyable-houses {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
gap: 10px;
}
.buyablehouses {
.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;
overflow: hidden;
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>