Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de52b6f26d | ||
|
|
43dd1a3b7f | ||
|
|
22f1803e7d | ||
|
|
42e894d4e4 | ||
|
|
9b88a98a20 | ||
|
|
f2343098d2 | ||
|
|
57ab85fe10 | ||
|
|
ce36315b58 | ||
|
|
80d8caee88 | ||
|
|
b3607849d2 | ||
|
|
d901257be1 | ||
|
|
d7c59df225 | ||
|
|
f7e0d97174 | ||
|
|
2055c11fd9 | ||
|
|
f98352088e | ||
|
|
63d9aab66a | ||
|
|
5f9e0a5a49 | ||
|
|
9af974d2f2 | ||
|
|
c0f9fc8970 | ||
|
|
876ee2ab49 | ||
|
|
2977b152a2 | ||
|
|
c7d33525ff | ||
|
|
1774d7df88 | ||
|
|
2c58ef37c4 |
@@ -29,30 +29,30 @@ class FalukantController {
|
||||
// Dashboard widget: originaler Endpoint (siehe Commit 62d8cd7)
|
||||
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
|
||||
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
|
||||
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId));
|
||||
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId), { blockInDebtorsPrison: true });
|
||||
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
|
||||
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch));
|
||||
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId));
|
||||
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId), { blockInDebtorsPrison: true });
|
||||
this.createProduction = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, productId, quantity } = req.body;
|
||||
return this.service.createProduction(userId, branchId, productId, quantity);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getProduction = this._wrapWithUser((userId, req) => this.service.getProduction(userId, req.params.branchId));
|
||||
this.getStock = this._wrapWithUser((userId, req) => this.service.getStock(userId, req.params.branchId || null));
|
||||
this.createStock = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, stockTypeId, stockSize } = req.body;
|
||||
return this.service.createStock(userId, branchId, stockTypeId, stockSize);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getProducts = this._wrapWithUser((userId) => this.service.getProducts(userId));
|
||||
this.getInventory = this._wrapWithUser((userId, req) => this.service.getInventory(userId, req.params.branchId));
|
||||
this.sellProduct = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, productId, quality, quantity } = req.body;
|
||||
return this.service.sellProduct(userId, branchId, productId, quality, quantity);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.sellAllProducts = this._wrapWithUser((userId, req) => {
|
||||
const { branchId } = req.body;
|
||||
return this.service.sellAllProducts(userId, branchId);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.moneyHistory = this._wrapWithUser((userId, req) => {
|
||||
let { page, filter } = req.body;
|
||||
if (!page) page = 1;
|
||||
@@ -66,11 +66,11 @@ class FalukantController {
|
||||
this.buyStorage = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, amount, stockTypeId } = req.body;
|
||||
return this.service.buyStorage(userId, branchId, amount, stockTypeId);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.sellStorage = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, amount, stockTypeId } = req.body;
|
||||
return this.service.sellStorage(userId, branchId, amount, stockTypeId);
|
||||
}, { successStatus: 202 });
|
||||
}, { successStatus: 202, blockInDebtorsPrison: true });
|
||||
|
||||
this.getStockTypes = this._wrapSimple(() => this.service.getStockTypes());
|
||||
this.getStockOverview = this._wrapSimple(() => this.service.getStockOverview());
|
||||
@@ -80,18 +80,18 @@ class FalukantController {
|
||||
console.log('🔍 getDirectorProposals called with userId:', userId, 'branchId:', req.body.branchId);
|
||||
return this.service.getDirectorProposals(userId, req.body.branchId);
|
||||
});
|
||||
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId));
|
||||
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
||||
this.getDirectorForBranch = this._wrapWithUser((userId, req) => this.service.getDirectorForBranch(userId, req.params.branchId));
|
||||
this.getAllDirectors = this._wrapWithUser((userId) => this.service.getAllDirectors(userId));
|
||||
this.updateDirector = this._wrapWithUser((userId, req) => {
|
||||
const { directorId, income } = req.body;
|
||||
return this.service.updateDirector(userId, directorId, income);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.setSetting = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, directorId, settingKey, value } = req.body;
|
||||
return this.service.setSetting(userId, branchId, directorId, settingKey, value);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getFamily = this._wrapWithUser(async (userId) => {
|
||||
const result = await this.service.getFamily(userId);
|
||||
@@ -99,9 +99,9 @@ class FalukantController {
|
||||
return result;
|
||||
});
|
||||
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
|
||||
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
|
||||
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
|
||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
|
||||
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId), { blockInDebtorsPrison: true });
|
||||
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId), { blockInDebtorsPrison: true });
|
||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
||||
this.cancelWooing = this._wrapWithUser(async (userId) => {
|
||||
try {
|
||||
return await this.service.cancelWooing(userId);
|
||||
@@ -111,11 +111,25 @@ class FalukantController {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, { successStatus: 202 });
|
||||
}, { successStatus: 202, blockInDebtorsPrison: true });
|
||||
this.getGifts = this._wrapWithUser((userId) => {
|
||||
console.log('🔍 getGifts called with userId:', userId);
|
||||
return this.service.getGifts(userId);
|
||||
});
|
||||
this.setLoverMaintenance = this._wrapWithUser((userId, req) =>
|
||||
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true });
|
||||
this.createLoverRelationship = this._wrapWithUser((userId, req) =>
|
||||
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.spendTimeWithSpouse = this._wrapWithUser((userId) =>
|
||||
this.service.spendTimeWithSpouse(userId), { blockInDebtorsPrison: true });
|
||||
this.giftToSpouse = this._wrapWithUser((userId, req) =>
|
||||
this.service.giftToSpouse(userId, req.body?.giftLevel), { blockInDebtorsPrison: true });
|
||||
this.reconcileMarriage = this._wrapWithUser((userId) =>
|
||||
this.service.reconcileMarriage(userId), { blockInDebtorsPrison: true });
|
||||
this.acknowledgeLover = this._wrapWithUser((userId, req) =>
|
||||
this.service.acknowledgeLover(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
||||
this.endLoverRelationship = this._wrapWithUser((userId, req) =>
|
||||
this.service.endLoverRelationship(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
||||
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
|
||||
this.sendGift = this._wrapWithUser(async (userId, req) => {
|
||||
try {
|
||||
@@ -126,55 +140,59 @@ class FalukantController {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
|
||||
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
|
||||
this.executeReputationAction = this._wrapWithUser((userId, req) =>
|
||||
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201 });
|
||||
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
|
||||
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
|
||||
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
|
||||
this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId));
|
||||
this.getBuyableHouses = this._wrapWithUser((userId) => this.service.getBuyableHouses(userId));
|
||||
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201 });
|
||||
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.hireServants = this._wrapWithUser((userId, req) => this.service.hireServants(userId, req.body?.amount), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.dismissServants = this._wrapWithUser((userId, req) => this.service.dismissServants(userId, req.body?.amount), { blockInDebtorsPrison: true });
|
||||
this.setServantPayLevel = this._wrapWithUser((userId, req) => this.service.setServantPayLevel(userId, req.body?.payLevel), { blockInDebtorsPrison: true });
|
||||
this.tidyHousehold = this._wrapWithUser((userId) => this.service.tidyHousehold(userId), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
|
||||
this.createParty = this._wrapWithUser((userId, req) => {
|
||||
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
|
||||
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
|
||||
|
||||
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
|
||||
this.baptise = this._wrapWithUser((userId, req) => {
|
||||
const { characterId: childId, firstName } = req.body;
|
||||
return this.service.baptise(userId, childId, firstName);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
|
||||
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
|
||||
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
|
||||
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
|
||||
const { officeTypeId, regionId } = req.body;
|
||||
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
|
||||
const { applicationId, decision } = req.body;
|
||||
return this.service.decideOnChurchApplication(userId, applicationId, decision);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
|
||||
this.sendToSchool = this._wrapWithUser((userId, req) => {
|
||||
const { item, student, studentId } = req.body;
|
||||
return this.service.sendToSchool(userId, item, student, studentId);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getBankOverview = this._wrapWithUser((userId) => this.service.getBankOverview(userId));
|
||||
this.getBankCredits = this._wrapWithUser((userId) => this.service.getBankCredits(userId));
|
||||
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height));
|
||||
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getNobility = this._wrapWithUser((userId) => this.service.getNobility(userId));
|
||||
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
|
||||
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
|
||||
this.healthActivity = this._wrapWithUser(async (userId, req) => {
|
||||
@@ -186,13 +204,13 @@ class FalukantController {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
||||
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
||||
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
|
||||
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes));
|
||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
||||
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });
|
||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
||||
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
|
||||
@@ -230,10 +248,12 @@ class FalukantController {
|
||||
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
|
||||
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId);
|
||||
});
|
||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element), { blockInDebtorsPrison: true });
|
||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getUndergroundTypes = this._wrapWithUser((userId) => this.service.getUndergroundTypes(userId));
|
||||
this.getRaidTransportRegions = this._wrapWithUser((userId) => this.service.getRaidTransportRegions(userId));
|
||||
this.getUndergroundActivities = this._wrapWithUser((userId) => this.service.getUndergroundActivities(userId));
|
||||
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
|
||||
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
|
||||
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
|
||||
@@ -258,7 +278,7 @@ class FalukantController {
|
||||
throw { status: 400, message: 'goal is required for corrupt_politician' };
|
||||
}
|
||||
return this.service.createUndergroundActivity(userId, payload);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
|
||||
this.getUndergroundAttacks = this._wrapWithUser((userId, req) => {
|
||||
const direction = (req.query.direction || '').toLowerCase();
|
||||
@@ -272,14 +292,14 @@ class FalukantController {
|
||||
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
|
||||
this.buyVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.buyVehicles(userId, req.body),
|
||||
{ successStatus: 201 }
|
||||
{ successStatus: 201, blockInDebtorsPrison: true }
|
||||
);
|
||||
this.getVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.getVehicles(userId, req.query.regionId)
|
||||
);
|
||||
this.createTransport = this._wrapWithUser(
|
||||
(userId, req) => this.service.createTransport(userId, req.body),
|
||||
{ successStatus: 201 }
|
||||
{ successStatus: 201, blockInDebtorsPrison: true }
|
||||
);
|
||||
this.getTransportRoute = this._wrapWithUser(
|
||||
(userId, req) => this.service.getTransportRoute(userId, req.query)
|
||||
@@ -289,23 +309,26 @@ class FalukantController {
|
||||
);
|
||||
this.repairVehicle = this._wrapWithUser(
|
||||
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
|
||||
{ successStatus: 200 }
|
||||
{ successStatus: 200, blockInDebtorsPrison: true }
|
||||
);
|
||||
this.repairAllVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
|
||||
{ successStatus: 200 }
|
||||
{ successStatus: 200, blockInDebtorsPrison: true }
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
_wrapWithUser(fn, { successStatus = 200, postProcess } = {}) {
|
||||
_wrapWithUser(fn, { successStatus = 200, postProcess, blockInDebtorsPrison = false } = {}) {
|
||||
return async (req, res) => {
|
||||
try {
|
||||
const hashedUserId = extractHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(400).json({ error: 'Missing user identifier' });
|
||||
}
|
||||
if (blockInDebtorsPrison) {
|
||||
await this.service.assertActionAllowedOutsideDebtorsPrison(hashedUserId);
|
||||
}
|
||||
const result = await fn(hashedUserId, req, res);
|
||||
const toSend = postProcess ? postProcess(result) : result;
|
||||
res.status(successStatus).json(toSend);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS falukant_data.relationship_state (
|
||||
id serial PRIMARY KEY,
|
||||
relationship_id integer NOT NULL UNIQUE,
|
||||
marriage_satisfaction integer NOT NULL DEFAULT 55 CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100),
|
||||
marriage_public_stability integer NOT NULL DEFAULT 55 CHECK (marriage_public_stability >= 0 AND marriage_public_stability <= 100),
|
||||
lover_role text NULL CHECK (lover_role IN ('secret_affair', 'lover', 'mistress_or_favorite')),
|
||||
affection integer NOT NULL DEFAULT 50 CHECK (affection >= 0 AND affection <= 100),
|
||||
visibility integer NOT NULL DEFAULT 15 CHECK (visibility >= 0 AND visibility <= 100),
|
||||
discretion integer NOT NULL DEFAULT 50 CHECK (discretion >= 0 AND discretion <= 100),
|
||||
maintenance_level integer NOT NULL DEFAULT 50 CHECK (maintenance_level >= 0 AND maintenance_level <= 100),
|
||||
status_fit integer NOT NULL DEFAULT 0 CHECK (status_fit >= -2 AND status_fit <= 2),
|
||||
monthly_base_cost integer NOT NULL DEFAULT 0 CHECK (monthly_base_cost >= 0),
|
||||
months_underfunded integer NOT NULL DEFAULT 0 CHECK (months_underfunded >= 0),
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
acknowledged boolean NOT NULL DEFAULT false,
|
||||
exclusive_flag boolean NOT NULL DEFAULT false,
|
||||
last_monthly_processed_at timestamp with time zone NULL,
|
||||
last_daily_processed_at timestamp with time zone NULL,
|
||||
notes_json jsonb NULL,
|
||||
flags_json jsonb NULL,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT relationship_state_relationship_fk
|
||||
FOREIGN KEY (relationship_id)
|
||||
REFERENCES falukant_data.relationship(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS relationship_state_active_idx
|
||||
ON falukant_data.relationship_state (active);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS relationship_state_lover_role_idx
|
||||
ON falukant_data.relationship_state (lover_role);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS legitimacy text NOT NULL DEFAULT 'legitimate';
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS birth_context text NOT NULL DEFAULT 'marriage';
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS public_known boolean NOT NULL DEFAULT false;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'child_relation_legitimacy_chk'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD CONSTRAINT child_relation_legitimacy_chk
|
||||
CHECK (legitimacy IN ('legitimate', 'acknowledged_bastard', 'hidden_bastard'));
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'child_relation_birth_context_chk'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD CONSTRAINT child_relation_birth_context_chk
|
||||
CHECK (birth_context IN ('marriage', 'lover'));
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP CONSTRAINT IF EXISTS child_relation_birth_context_chk;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP CONSTRAINT IF EXISTS child_relation_legitimacy_chk;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP COLUMN IF EXISTS public_known;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP COLUMN IF EXISTS birth_context;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP COLUMN IF EXISTS legitimacy;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TABLE IF EXISTS falukant_data.relationship_state;
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO falukant_data.relationship_state (
|
||||
relationship_id,
|
||||
marriage_satisfaction,
|
||||
marriage_public_stability,
|
||||
lover_role,
|
||||
affection,
|
||||
visibility,
|
||||
discretion,
|
||||
maintenance_level,
|
||||
status_fit,
|
||||
monthly_base_cost,
|
||||
months_underfunded,
|
||||
active,
|
||||
acknowledged,
|
||||
exclusive_flag,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
r.id,
|
||||
55,
|
||||
55,
|
||||
CASE WHEN rt.tr = 'lover' THEN 'lover' ELSE NULL END,
|
||||
50,
|
||||
CASE WHEN rt.tr = 'lover' THEN 20 ELSE 15 END,
|
||||
CASE WHEN rt.tr = 'lover' THEN 45 ELSE 50 END,
|
||||
50,
|
||||
0,
|
||||
CASE WHEN rt.tr = 'lover' THEN 30 ELSE 0 END,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM falukant_data.relationship r
|
||||
INNER JOIN falukant_type.relationship rt
|
||||
ON rt.id = r.relationship_type_id
|
||||
LEFT JOIN falukant_data.relationship_state rs
|
||||
ON rs.relationship_id = r.id
|
||||
WHERE rs.id IS NULL
|
||||
AND rt.tr IN ('lover', 'wooing', 'engaged', 'married');
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
DELETE FROM falukant_data.relationship_state rs
|
||||
USING falukant_data.relationship r
|
||||
INNER JOIN falukant_type.relationship rt
|
||||
ON rt.id = r.relationship_type_id
|
||||
WHERE rs.relationship_id = r.id
|
||||
AND rt.tr IN ('lover', 'wooing', 'engaged', 'married');
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'servant_count',
|
||||
{
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
}
|
||||
);
|
||||
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'servant_quality',
|
||||
{
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50
|
||||
}
|
||||
);
|
||||
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'servant_pay_level',
|
||||
{
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'normal'
|
||||
}
|
||||
);
|
||||
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_order',
|
||||
{
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 55
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'household_order');
|
||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_pay_level');
|
||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_quality');
|
||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_count');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_tension_score',
|
||||
{
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 10
|
||||
}
|
||||
);
|
||||
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_tension_reasons_json',
|
||||
{
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_tension_reasons_json'
|
||||
);
|
||||
await queryInterface.removeColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_tension_score'
|
||||
);
|
||||
}
|
||||
};
|
||||
83
backend/migrations/20260323010000-expand-debtors-prism.cjs
Normal file
83
backend/migrations/20260323010000-expand-debtors-prism.cjs
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
|
||||
|
||||
await queryInterface.addColumn(table, 'status', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'delinquent'
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'entered_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'released_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'debt_at_entry', {
|
||||
type: Sequelize.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'remaining_debt', {
|
||||
type: Sequelize.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'days_overdue', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'reason', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'creditworthiness_penalty', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'next_forced_action', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'assets_seized_json', {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'public_known', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
|
||||
|
||||
await queryInterface.removeColumn(table, 'public_known');
|
||||
await queryInterface.removeColumn(table, 'assets_seized_json');
|
||||
await queryInterface.removeColumn(table, 'next_forced_action');
|
||||
await queryInterface.removeColumn(table, 'creditworthiness_penalty');
|
||||
await queryInterface.removeColumn(table, 'reason');
|
||||
await queryInterface.removeColumn(table, 'days_overdue');
|
||||
await queryInterface.removeColumn(table, 'remaining_debt');
|
||||
await queryInterface.removeColumn(table, 'debt_at_entry');
|
||||
await queryInterface.removeColumn(table, 'released_at');
|
||||
await queryInterface.removeColumn(table, 'entered_at');
|
||||
await queryInterface.removeColumn(table, 'status');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.transport
|
||||
ADD COLUMN IF NOT EXISTS guard_count integer NOT NULL DEFAULT 0;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.underground
|
||||
ALTER COLUMN victim_id DROP NOT NULL;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.transport
|
||||
DROP COLUMN IF EXISTS guard_count;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -68,6 +68,7 @@ import PromotionalGiftCharacterTrait from './falukant/predefine/promotional_gift
|
||||
import PromotionalGiftMood from './falukant/predefine/promotional_gift_mood.js';
|
||||
import RelationshipType from './falukant/type/relationship.js';
|
||||
import Relationship from './falukant/data/relationship.js';
|
||||
import RelationshipState from './falukant/data/relationship_state.js';
|
||||
import PromotionalGiftLog from './falukant/log/promotional_gift.js';
|
||||
import HouseType from './falukant/type/house.js';
|
||||
import BuyableHouse from './falukant/data/buyable_house.js';
|
||||
@@ -460,6 +461,8 @@ export default function setupAssociations() {
|
||||
Relationship.belongsTo(FalukantCharacter, { foreignKey: 'character2Id', as: 'character2', });
|
||||
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character1Id', as: 'relationshipsAsCharacter1', });
|
||||
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character2Id', as: 'relationshipsAsCharacter2', });
|
||||
Relationship.hasOne(RelationshipState, { foreignKey: 'relationshipId', as: 'state' });
|
||||
RelationshipState.belongsTo(Relationship, { foreignKey: 'relationshipId', as: 'relationship' });
|
||||
|
||||
PromotionalGiftLog.belongsTo(PromotionalGift, { foreignKey: 'giftId', as: 'gift' });
|
||||
PromotionalGift.hasMany(PromotionalGiftLog, { foreignKey: 'giftId', as: 'logs' });
|
||||
@@ -1095,4 +1098,3 @@ export default function setupAssociations() {
|
||||
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
User.hasMany(CalendarEvent, { foreignKey: 'userId', as: 'calendarEvents' });
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,25 @@ ChildRelation.init(
|
||||
isHeir: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
default: false}
|
||||
default: false},
|
||||
legitimacy: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'legitimate',
|
||||
validate: {
|
||||
isIn: [['legitimate', 'acknowledged_bastard', 'hidden_bastard']]
|
||||
}},
|
||||
birthContext: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'marriage',
|
||||
validate: {
|
||||
isIn: [['marriage', 'lover']]
|
||||
}},
|
||||
publicKnown: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
|
||||
@@ -7,7 +7,57 @@ DebtorsPrism.init({
|
||||
// Verknüpfung auf FalukantCharacter
|
||||
characterId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false}}, {
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'delinquent'
|
||||
},
|
||||
enteredAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
releasedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
debtAtEntry: {
|
||||
type: DataTypes.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
},
|
||||
remainingDebt: {
|
||||
type: DataTypes.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
},
|
||||
daysOverdue: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
reason: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
creditworthinessPenalty: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
nextForcedAction: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
assetsSeizedJson: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
publicKnown: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'DebtorsPrism',
|
||||
tableName: 'debtors_prism',
|
||||
|
||||
141
backend/models/falukant/data/relationship_state.js
Normal file
141
backend/models/falukant/data/relationship_state.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class RelationshipState extends Model {}
|
||||
|
||||
RelationshipState.init(
|
||||
{
|
||||
relationshipId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
marriageSatisfaction: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 55,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
marriagePublicStability: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 55,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
loverRole: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
isIn: [[null, 'secret_affair', 'lover', 'mistress_or_favorite'].filter(Boolean)],
|
||||
},
|
||||
},
|
||||
affection: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 15,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
discretion: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
maintenanceLevel: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
statusFit: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
validate: {
|
||||
min: -2,
|
||||
max: 2,
|
||||
},
|
||||
},
|
||||
monthlyBaseCost: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
validate: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
monthsUnderfunded: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
validate: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
acknowledged: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
exclusiveFlag: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
lastMonthlyProcessedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
lastDailyProcessedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
notesJson: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
},
|
||||
flagsJson: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'RelationshipState',
|
||||
tableName: 'relationship_state',
|
||||
schema: 'falukant_data',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default RelationshipState;
|
||||
@@ -25,6 +25,11 @@ Transport.init(
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
guardCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
@@ -38,4 +43,3 @@ Transport.init(
|
||||
|
||||
export default Transport;
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Underground.init({
|
||||
allowNull: false},
|
||||
victimId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false},
|
||||
allowNull: true},
|
||||
parameters: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true},
|
||||
|
||||
@@ -24,6 +24,35 @@ UserHouse.init({
|
||||
allowNull: false,
|
||||
defaultValue: 100
|
||||
},
|
||||
servantCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
servantQuality: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50
|
||||
},
|
||||
servantPayLevel: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'normal'
|
||||
},
|
||||
householdOrder: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 55
|
||||
},
|
||||
householdTensionScore: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 10
|
||||
},
|
||||
householdTensionReasonsJson: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
houseTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
|
||||
@@ -67,6 +67,7 @@ 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 RelationshipState from './falukant/data/relationship_state.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';
|
||||
@@ -219,6 +220,7 @@ const models = {
|
||||
MarriageProposal,
|
||||
RelationshipType,
|
||||
Relationship,
|
||||
RelationshipState,
|
||||
CharacterTrait,
|
||||
FalukantCharacterTrait,
|
||||
Mood,
|
||||
|
||||
@@ -18,32 +18,32 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"amqplib": "^0.10.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-redis": "^7.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"amqplib": "^0.10.9",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"connect-redis": "^9.0.0",
|
||||
"cors": "^2.8.6",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.1.7",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^4.19.2",
|
||||
"express-session": "^1.18.1",
|
||||
"i18n": "^0.15.1",
|
||||
"joi": "^17.13.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"multer": "^2.0.0",
|
||||
"mysql2": "^3.10.3",
|
||||
"nodemailer": "^7.0.11",
|
||||
"pg": "^8.12.0",
|
||||
"dompurify": "^3.3.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.19.0",
|
||||
"i18n": "^0.15.3",
|
||||
"joi": "^18.0.2",
|
||||
"jsdom": "^29.0.1",
|
||||
"multer": "^2.1.1",
|
||||
"mysql2": "^3.20.0",
|
||||
"nodemailer": "^8.0.3",
|
||||
"pg": "^8.20.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"redis": "^4.7.0",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.34.3",
|
||||
"socket.io": "^4.7.5",
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.0",
|
||||
"redis": "^5.11.0",
|
||||
"sequelize": "^6.37.8",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io": "^4.8.3",
|
||||
"uuid": "^13.0.0",
|
||||
"ws": "^8.20.0",
|
||||
"@gltf-transform/cli": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sequelize-cli": "^6.6.2"
|
||||
"sequelize-cli": "^6.6.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,13 @@ router.get('/dashboard-widget', falukantController.getDashboardWidget);
|
||||
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
|
||||
router.post('/family/cancel-wooing', falukantController.cancelWooing);
|
||||
router.post('/family/set-heir', falukantController.setHeir);
|
||||
router.post('/family/lover', falukantController.createLoverRelationship);
|
||||
router.post('/family/marriage/spend-time', falukantController.spendTimeWithSpouse);
|
||||
router.post('/family/marriage/gift', falukantController.giftToSpouse);
|
||||
router.post('/family/marriage/reconcile', falukantController.reconcileMarriage);
|
||||
router.post('/family/lover/:relationshipId/maintenance', falukantController.setLoverMaintenance);
|
||||
router.post('/family/lover/:relationshipId/acknowledge', falukantController.acknowledgeLover);
|
||||
router.post('/family/lover/:relationshipId/end', falukantController.endLoverRelationship);
|
||||
router.get('/heirs/potential', falukantController.getPotentialHeirs);
|
||||
router.post('/heirs/select', falukantController.selectHeir);
|
||||
router.get('/family/gifts', falukantController.getGifts);
|
||||
@@ -61,6 +68,10 @@ router.get('/houses/buyable', falukantController.getBuyableHouses);
|
||||
router.get('/houses', falukantController.getUserHouse);
|
||||
router.post('/houses/renovate-all', falukantController.renovateAll);
|
||||
router.post('/houses/renovate', falukantController.renovate);
|
||||
router.post('/houses/servants/hire', falukantController.hireServants);
|
||||
router.post('/houses/servants/dismiss', falukantController.dismissServants);
|
||||
router.post('/houses/servants/pay-level', falukantController.setServantPayLevel);
|
||||
router.post('/houses/order', falukantController.tidyHousehold);
|
||||
router.post('/houses', falukantController.buyUserHouse);
|
||||
router.get('/party/types', falukantController.getPartyTypes);
|
||||
router.post('/party', falukantController.createParty);
|
||||
@@ -101,6 +112,8 @@ router.post('/transports', falukantController.createTransport);
|
||||
router.get('/transports/route', falukantController.getTransportRoute);
|
||||
router.get('/transports/branch/:branchId', falukantController.getBranchTransports);
|
||||
router.get('/underground/types', falukantController.getUndergroundTypes);
|
||||
router.get('/underground/raid-regions', falukantController.getRaidTransportRegions);
|
||||
router.get('/underground/activities', falukantController.getUndergroundActivities);
|
||||
router.get('/notifications', falukantController.getNotifications);
|
||||
router.get('/notifications/all', falukantController.getAllNotifications);
|
||||
router.post('/notifications/mark-shown', falukantController.markNotificationsShown);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
5
backend/sql/add_household_tension_to_user_house.sql
Normal file
5
backend/sql/add_household_tension_to_user_house.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE falukant_data.user_house
|
||||
ADD COLUMN IF NOT EXISTS household_tension_score integer NOT NULL DEFAULT 10;
|
||||
|
||||
ALTER TABLE falukant_data.user_house
|
||||
ADD COLUMN IF NOT EXISTS household_tension_reasons_json jsonb NULL;
|
||||
88
backend/sql/add_relationship_state_and_child_legitimacy.sql
Normal file
88
backend/sql/add_relationship_state_and_child_legitimacy.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
-- PostgreSQL-only migration script.
|
||||
-- Dieses Projekt-Backend nutzt Schemas, JSONB und PostgreSQL-Datentypen.
|
||||
-- Nicht auf MariaDB/MySQL ausführen.
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS falukant_data.relationship_state (
|
||||
id serial PRIMARY KEY,
|
||||
relationship_id integer NOT NULL UNIQUE,
|
||||
marriage_satisfaction integer NOT NULL DEFAULT 55 CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100),
|
||||
marriage_public_stability integer NOT NULL DEFAULT 55 CHECK (marriage_public_stability >= 0 AND marriage_public_stability <= 100),
|
||||
lover_role text NULL CHECK (lover_role IN ('secret_affair', 'lover', 'mistress_or_favorite')),
|
||||
affection integer NOT NULL DEFAULT 50 CHECK (affection >= 0 AND affection <= 100),
|
||||
visibility integer NOT NULL DEFAULT 15 CHECK (visibility >= 0 AND visibility <= 100),
|
||||
discretion integer NOT NULL DEFAULT 50 CHECK (discretion >= 0 AND discretion <= 100),
|
||||
maintenance_level integer NOT NULL DEFAULT 50 CHECK (maintenance_level >= 0 AND maintenance_level <= 100),
|
||||
status_fit integer NOT NULL DEFAULT 0 CHECK (status_fit >= -2 AND status_fit <= 2),
|
||||
monthly_base_cost integer NOT NULL DEFAULT 0 CHECK (monthly_base_cost >= 0),
|
||||
months_underfunded integer NOT NULL DEFAULT 0 CHECK (months_underfunded >= 0),
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
acknowledged boolean NOT NULL DEFAULT false,
|
||||
exclusive_flag boolean NOT NULL DEFAULT false,
|
||||
last_monthly_processed_at timestamp with time zone NULL,
|
||||
last_daily_processed_at timestamp with time zone NULL,
|
||||
notes_json jsonb NULL,
|
||||
flags_json jsonb NULL,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT relationship_state_relationship_fk
|
||||
FOREIGN KEY (relationship_id)
|
||||
REFERENCES falukant_data.relationship(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS relationship_state_active_idx
|
||||
ON falukant_data.relationship_state (active);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS relationship_state_lover_role_idx
|
||||
ON falukant_data.relationship_state (lover_role);
|
||||
|
||||
ALTER TABLE IF EXISTS falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS legitimacy text NOT NULL DEFAULT 'legitimate';
|
||||
|
||||
ALTER TABLE IF EXISTS falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS birth_context text NOT NULL DEFAULT 'marriage';
|
||||
|
||||
ALTER TABLE IF EXISTS falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS public_known boolean NOT NULL DEFAULT false;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'child_relation_legitimacy_chk'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD CONSTRAINT child_relation_legitimacy_chk
|
||||
CHECK (legitimacy IN ('legitimate', 'acknowledged_bastard', 'hidden_bastard'));
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'child_relation_birth_context_chk'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD CONSTRAINT child_relation_birth_context_chk
|
||||
CHECK (birth_context IN ('marriage', 'lover'));
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Rollback separat bei Bedarf:
|
||||
-- BEGIN;
|
||||
-- ALTER TABLE falukant_data.child_relation DROP CONSTRAINT IF EXISTS child_relation_birth_context_chk;
|
||||
-- ALTER TABLE falukant_data.child_relation DROP CONSTRAINT IF EXISTS child_relation_legitimacy_chk;
|
||||
-- ALTER TABLE falukant_data.child_relation DROP COLUMN IF EXISTS public_known;
|
||||
-- ALTER TABLE falukant_data.child_relation DROP COLUMN IF EXISTS birth_context;
|
||||
-- ALTER TABLE falukant_data.child_relation DROP COLUMN IF EXISTS legitimacy;
|
||||
-- DROP TABLE IF EXISTS falukant_data.relationship_state;
|
||||
-- COMMIT;
|
||||
7
backend/sql/add_servants_to_user_house.sql
Normal file
7
backend/sql/add_servants_to_user_house.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- PostgreSQL only
|
||||
|
||||
ALTER TABLE falukant_data.user_house
|
||||
ADD COLUMN IF NOT EXISTS servant_count integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS servant_quality integer NOT NULL DEFAULT 50,
|
||||
ADD COLUMN IF NOT EXISTS servant_pay_level varchar(20) NOT NULL DEFAULT 'normal',
|
||||
ADD COLUMN IF NOT EXISTS household_order integer NOT NULL DEFAULT 55;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- PostgreSQL only
|
||||
|
||||
ALTER TABLE falukant_data.transport
|
||||
ADD COLUMN IF NOT EXISTS guard_count integer NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE falukant_data.underground
|
||||
ALTER COLUMN victim_id DROP NOT NULL;
|
||||
5
backend/sql/add_underground_investigate_affair_type.sql
Normal file
5
backend/sql/add_underground_investigate_affair_type.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- PostgreSQL-only
|
||||
INSERT INTO falukant_type.underground (tr, cost)
|
||||
VALUES ('investigate_affair', 7000)
|
||||
ON CONFLICT (tr) DO UPDATE
|
||||
SET cost = EXCLUDED.cost;
|
||||
5
backend/sql/add_underground_raid_transport_type.sql
Normal file
5
backend/sql/add_underground_raid_transport_type.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- PostgreSQL only
|
||||
INSERT INTO falukant_type.underground (tr, cost)
|
||||
VALUES ('raid_transport', 9000)
|
||||
ON CONFLICT (tr) DO UPDATE
|
||||
SET cost = EXCLUDED.cost;
|
||||
50
backend/sql/backfill_relationship_state.sql
Normal file
50
backend/sql/backfill_relationship_state.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
-- PostgreSQL-only backfill script.
|
||||
-- Dieses Projekt-Backend nutzt Schemas und PostgreSQL-spezifische SQL-Strukturen.
|
||||
-- Nicht auf MariaDB/MySQL ausführen.
|
||||
|
||||
BEGIN;
|
||||
|
||||
INSERT INTO falukant_data.relationship_state (
|
||||
relationship_id,
|
||||
marriage_satisfaction,
|
||||
marriage_public_stability,
|
||||
lover_role,
|
||||
affection,
|
||||
visibility,
|
||||
discretion,
|
||||
maintenance_level,
|
||||
status_fit,
|
||||
monthly_base_cost,
|
||||
months_underfunded,
|
||||
active,
|
||||
acknowledged,
|
||||
exclusive_flag,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
r.id,
|
||||
55,
|
||||
55,
|
||||
CASE WHEN rt.tr = 'lover' THEN 'lover' ELSE NULL END,
|
||||
50,
|
||||
CASE WHEN rt.tr = 'lover' THEN 20 ELSE 15 END,
|
||||
CASE WHEN rt.tr = 'lover' THEN 45 ELSE 50 END,
|
||||
50,
|
||||
0,
|
||||
CASE WHEN rt.tr = 'lover' THEN 30 ELSE 0 END,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM falukant_data.relationship r
|
||||
INNER JOIN falukant_type.relationship rt
|
||||
ON rt.id = r.relationship_type_id
|
||||
LEFT JOIN falukant_data.relationship_state rs
|
||||
ON rs.relationship_id = r.id
|
||||
WHERE rs.id IS NULL
|
||||
AND rt.tr IN ('lover', 'wooing', 'engaged', 'married');
|
||||
|
||||
COMMIT;
|
||||
32
backend/sql/expand_debtors_prism.sql
Normal file
32
backend/sql/expand_debtors_prism.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS status varchar(255) NOT NULL DEFAULT 'delinquent';
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS entered_at timestamp with time zone NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS released_at timestamp with time zone NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS debt_at_entry numeric(14,2) NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS remaining_debt numeric(14,2) NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS days_overdue integer NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS reason varchar(255) NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS creditworthiness_penalty integer NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS next_forced_action varchar(255) NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS assets_seized_json jsonb NULL;
|
||||
|
||||
ALTER TABLE falukant_data.debtors_prism
|
||||
ADD COLUMN IF NOT EXISTS public_known boolean NOT NULL DEFAULT false;
|
||||
132
backend/sql/update_nobility_requirements_extended.sql
Normal file
132
backend/sql/update_nobility_requirements_extended.sql
Normal file
@@ -0,0 +1,132 @@
|
||||
-- PostgreSQL only
|
||||
-- Ersetzt die Standesanforderungen fuer Falukant durch das erweiterte Profilmodell.
|
||||
|
||||
DELETE FROM falukant_type.title_requirement
|
||||
WHERE title_id IN (
|
||||
SELECT id
|
||||
FROM falukant_type.title
|
||||
WHERE label_tr IN (
|
||||
'civil', 'sir', 'townlord', 'by', 'landlord', 'knight',
|
||||
'baron', 'count', 'palsgrave', 'margrave', 'landgrave',
|
||||
'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke',
|
||||
'prince-regent', 'king'
|
||||
)
|
||||
);
|
||||
|
||||
INSERT INTO falukant_type.title_requirement (title_id, requirement_type, requirement_value)
|
||||
SELECT tm.id, req.requirement_type, req.requirement_value
|
||||
FROM (
|
||||
SELECT id, label_tr
|
||||
FROM falukant_type.title
|
||||
WHERE label_tr IN (
|
||||
'civil', 'sir', 'townlord', 'by', 'landlord', 'knight',
|
||||
'baron', 'count', 'palsgrave', 'margrave', 'landgrave',
|
||||
'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke',
|
||||
'prince-regent', 'king'
|
||||
)
|
||||
) tm
|
||||
JOIN (
|
||||
VALUES
|
||||
('civil', 'money', 5000::numeric),
|
||||
('civil', 'cost', 500::numeric),
|
||||
('civil', 'house_position', 2::numeric),
|
||||
|
||||
('sir', 'branches', 2::numeric),
|
||||
('sir', 'cost', 1000::numeric),
|
||||
('sir', 'house_position', 3::numeric),
|
||||
|
||||
('townlord', 'cost', 3000::numeric),
|
||||
('townlord', 'money', 12000::numeric),
|
||||
('townlord', 'reputation', 18::numeric),
|
||||
('townlord', 'house_position', 4::numeric),
|
||||
|
||||
('by', 'cost', 5000::numeric),
|
||||
('by', 'money', 18000::numeric),
|
||||
('by', 'house_position', 4::numeric),
|
||||
|
||||
('landlord', 'cost', 7500::numeric),
|
||||
('landlord', 'money', 26000::numeric),
|
||||
('landlord', 'reputation', 24::numeric),
|
||||
('landlord', 'house_position', 4::numeric),
|
||||
('landlord', 'house_condition', 60::numeric),
|
||||
|
||||
('knight', 'cost', 11000::numeric),
|
||||
('knight', 'money', 38000::numeric),
|
||||
('knight', 'office_rank_any', 1::numeric),
|
||||
('knight', 'house_position', 5::numeric),
|
||||
|
||||
('baron', 'branches', 4::numeric),
|
||||
('baron', 'cost', 16000::numeric),
|
||||
('baron', 'money', 55000::numeric),
|
||||
('baron', 'house_position', 5::numeric),
|
||||
|
||||
('count', 'cost', 23000::numeric),
|
||||
('count', 'money', 80000::numeric),
|
||||
('count', 'reputation', 32::numeric),
|
||||
('count', 'house_position', 5::numeric),
|
||||
('count', 'house_condition', 68::numeric),
|
||||
|
||||
('palsgrave', 'cost', 32000::numeric),
|
||||
('palsgrave', 'money', 115000::numeric),
|
||||
('palsgrave', 'office_rank_any', 2::numeric),
|
||||
('palsgrave', 'house_position', 6::numeric),
|
||||
|
||||
('margrave', 'cost', 45000::numeric),
|
||||
('margrave', 'money', 165000::numeric),
|
||||
('margrave', 'reputation', 40::numeric),
|
||||
('margrave', 'house_position', 6::numeric),
|
||||
('margrave', 'house_condition', 72::numeric),
|
||||
('margrave', 'lover_count_min', 1::numeric),
|
||||
|
||||
('landgrave', 'cost', 62000::numeric),
|
||||
('landgrave', 'money', 230000::numeric),
|
||||
('landgrave', 'office_rank_any', 3::numeric),
|
||||
('landgrave', 'house_position', 6::numeric),
|
||||
|
||||
('ruler', 'cost', 85000::numeric),
|
||||
('ruler', 'money', 320000::numeric),
|
||||
('ruler', 'reputation', 48::numeric),
|
||||
('ruler', 'house_position', 7::numeric),
|
||||
('ruler', 'house_condition', 76::numeric),
|
||||
|
||||
('elector', 'cost', 115000::numeric),
|
||||
('elector', 'money', 440000::numeric),
|
||||
('elector', 'office_rank_political', 4::numeric),
|
||||
('elector', 'house_position', 7::numeric),
|
||||
('elector', 'lover_count_max', 3::numeric),
|
||||
|
||||
('imperial-prince', 'cost', 155000::numeric),
|
||||
('imperial-prince', 'money', 600000::numeric),
|
||||
('imperial-prince', 'reputation', 56::numeric),
|
||||
('imperial-prince', 'house_position', 7::numeric),
|
||||
('imperial-prince', 'house_condition', 80::numeric),
|
||||
|
||||
('duke', 'cost', 205000::numeric),
|
||||
('duke', 'money', 820000::numeric),
|
||||
('duke', 'office_rank_any', 5::numeric),
|
||||
('duke', 'house_position', 8::numeric),
|
||||
|
||||
('grand-duke', 'cost', 270000::numeric),
|
||||
('grand-duke', 'money', 1120000::numeric),
|
||||
('grand-duke', 'reputation', 64::numeric),
|
||||
('grand-duke', 'house_position', 8::numeric),
|
||||
('grand-duke', 'house_condition', 84::numeric),
|
||||
('grand-duke', 'lover_count_min', 1::numeric),
|
||||
('grand-duke', 'lover_count_max', 3::numeric),
|
||||
|
||||
('prince-regent', 'cost', 360000::numeric),
|
||||
('prince-regent', 'money', 1520000::numeric),
|
||||
('prince-regent', 'office_rank_any', 6::numeric),
|
||||
('prince-regent', 'house_position', 9::numeric),
|
||||
|
||||
('king', 'cost', 500000::numeric),
|
||||
('king', 'money', 2100000::numeric),
|
||||
('king', 'reputation', 72::numeric),
|
||||
('king', 'house_position', 9::numeric),
|
||||
('king', 'house_condition', 88::numeric),
|
||||
('king', 'lover_count_min', 1::numeric),
|
||||
('king', 'lover_count_max', 4::numeric)
|
||||
) AS req(label_tr, requirement_type, requirement_value)
|
||||
ON req.label_tr = tm.label_tr
|
||||
ON CONFLICT (title_id, requirement_type)
|
||||
DO UPDATE SET requirement_value = EXCLUDED.requirement_value;
|
||||
@@ -297,24 +297,24 @@ async function initializeFalukantProducts() {
|
||||
|
||||
async function initializeFalukantTitleRequirements() {
|
||||
const titleRequirements = [
|
||||
{ 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 }] },
|
||||
{ labelTr: "civil", requirements: [{ type: "money", value: 5000 }, { type: "cost", value: 500 }, { type: "house_position", value: 2 }] },
|
||||
{ labelTr: "sir", requirements: [{ type: "branches", value: 2 }, { type: "cost", value: 1000 }, { type: "house_position", value: 3 }] },
|
||||
{ labelTr: "townlord", requirements: [{ type: "cost", value: 3000 }, { type: "money", value: 12000 }, { type: "reputation", value: 18 }, { type: "house_position", value: 4 }] },
|
||||
{ labelTr: "by", requirements: [{ type: "cost", value: 5000 }, { type: "money", value: 18000 }, { type: "house_position", value: 4 }] },
|
||||
{ labelTr: "landlord", requirements: [{ type: "cost", value: 7500 }, { type: "money", value: 26000 }, { type: "reputation", value: 24 }, { type: "house_position", value: 4 }, { type: "house_condition", value: 60 }] },
|
||||
{ labelTr: "knight", requirements: [{ type: "cost", value: 11000 }, { type: "money", value: 38000 }, { type: "office_rank_any", value: 1 }, { type: "house_position", value: 5 }] },
|
||||
{ labelTr: "baron", requirements: [{ type: "branches", value: 4 }, { type: "cost", value: 16000 }, { type: "money", value: 55000 }, { type: "house_position", value: 5 }] },
|
||||
{ labelTr: "count", requirements: [{ type: "cost", value: 23000 }, { type: "money", value: 80000 }, { type: "reputation", value: 32 }, { type: "house_position", value: 5 }, { type: "house_condition", value: 68 }] },
|
||||
{ labelTr: "palsgrave", requirements: [{ type: "cost", value: 32000 }, { type: "money", value: 115000 }, { type: "office_rank_any", value: 2 }, { type: "house_position", value: 6 }] },
|
||||
{ labelTr: "margrave", requirements: [{ type: "cost", value: 45000 }, { type: "money", value: 165000 }, { type: "reputation", value: 40 }, { type: "house_position", value: 6 }, { type: "house_condition", value: 72 }, { type: "lover_count_min", value: 1 }] },
|
||||
{ labelTr: "landgrave", requirements: [{ type: "cost", value: 62000 }, { type: "money", value: 230000 }, { type: "office_rank_any", value: 3 }, { type: "house_position", value: 6 }] },
|
||||
{ labelTr: "ruler", requirements: [{ type: "cost", value: 85000 }, { type: "money", value: 320000 }, { type: "reputation", value: 48 }, { type: "house_position", value: 7 }, { type: "house_condition", value: 76 }] },
|
||||
{ labelTr: "elector", requirements: [{ type: "cost", value: 115000 }, { type: "money", value: 440000 }, { type: "office_rank_political", value: 4 }, { type: "house_position", value: 7 }, { type: "lover_count_max", value: 3 }] },
|
||||
{ labelTr: "imperial-prince", requirements: [{ type: "cost", value: 155000 }, { type: "money", value: 600000 }, { type: "reputation", value: 56 }, { type: "house_position", value: 7 }, { type: "house_condition", value: 80 }] },
|
||||
{ labelTr: "duke", requirements: [{ type: "cost", value: 205000 }, { type: "money", value: 820000 }, { type: "office_rank_any", value: 5 }, { type: "house_position", value: 8 }] },
|
||||
{ labelTr: "grand-duke",requirements: [{ type: "cost", value: 270000 }, { type: "money", value: 1120000 }, { type: "reputation", value: 64 }, { type: "house_position", value: 8 }, { type: "house_condition", value: 84 }, { type: "lover_count_min", value: 1 }, { type: "lover_count_max", value: 3 }] },
|
||||
{ labelTr: "prince-regent", requirements: [{ type: "cost", value: 360000 }, { type: "money", value: 1520000 }, { type: "office_rank_any", value: 6 }, { type: "house_position", value: 9 }] },
|
||||
{ labelTr: "king", requirements: [{ type: "cost", value: 500000 }, { type: "money", value: 2100000 }, { type: "reputation", value: 72 }, { type: "house_position", value: 9 }, { type: "house_condition", value: 88 }, { type: "lover_count_min", value: 1 }, { type: "lover_count_max", value: 4 }] },
|
||||
];
|
||||
|
||||
const titles = await TitleOfNobility.findAll();
|
||||
@@ -325,13 +325,6 @@ async function initializeFalukantTitleRequirements() {
|
||||
const title = titles.find(t => t.labelTr === titleReq.labelTr);
|
||||
if (!title) continue;
|
||||
|
||||
if (i > 1) {
|
||||
titleReq.requirements.push({
|
||||
type: "money",
|
||||
value: 5000 * Math.pow(3, i - 1),
|
||||
});
|
||||
}
|
||||
|
||||
for (const req of titleReq.requirements) {
|
||||
requirementsToInsert.push({
|
||||
titleId: title.id,
|
||||
@@ -341,6 +334,7 @@ async function initializeFalukantTitleRequirements() {
|
||||
}
|
||||
}
|
||||
|
||||
await TitleRequirement.destroy({ where: {} });
|
||||
await TitleRequirement.bulkCreate(requirementsToInsert, { ignoreDuplicates: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -659,6 +659,14 @@ const undergroundTypes = [
|
||||
"tr": "rob",
|
||||
"cost": 500
|
||||
},
|
||||
{
|
||||
"tr": "investigate_affair",
|
||||
"cost": 7000
|
||||
},
|
||||
{
|
||||
"tr": "raid_transport",
|
||||
"cost": 9000
|
||||
},
|
||||
];
|
||||
|
||||
{
|
||||
|
||||
328
docs/FALUKANT_CHURCH_ADVANCEMENT_DAEMON_SPEC.md
Normal file
328
docs/FALUKANT_CHURCH_ADVANCEMENT_DAEMON_SPEC.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# Falukant: Kirchenämter, Aufstieg und NPC-Besetzung
|
||||
|
||||
Dieses Dokument beschreibt das Zielmodell für das Kirchensystem in Falukant. Es fokussiert auf drei Probleme:
|
||||
|
||||
- Spieler können sich derzeit nicht sinnvoll auf höhere kirchliche Ämter bewerben.
|
||||
- Nicht alle Ämter werden besetzt.
|
||||
- NPCs sollen sich ebenfalls bewerben und Ämter aktiv besetzen.
|
||||
|
||||
Der Daemon soll die laufende Besetzung und Beförderung übernehmen. Der eigentliche Antrag des Spielers bleibt ein aktiver Spielzug in der UI.
|
||||
|
||||
## 1. Zielbild
|
||||
|
||||
Kirchliche Ämter sollen ein lebendes Hierarchiesystem sein:
|
||||
|
||||
- Leere Ämter werden nach und nach besetzt.
|
||||
- Spieler und NPCs konkurrieren um offene Positionen.
|
||||
- Höhere Amtsträger entscheiden über untere Ebenen.
|
||||
- Wo kein Spieler-Entscheider vorhanden ist, übernimmt ein NPC-Amtsträger die Entscheidung.
|
||||
- Wo ganze Hierarchieebenen leer sind, darf das System kontrolliert von unten nach oben oder über Interimslogik nachbesetzen.
|
||||
|
||||
## 2. Grundregeln
|
||||
|
||||
### 2.1 Bewerbung
|
||||
|
||||
- Ein Spieler beantragt ein Amt weiterhin aktiv über die UI.
|
||||
- NPCs bewerben sich nicht per UI, sondern durch den Daemon.
|
||||
- Es darf gleichzeitig mehrere Bewerber für dieselbe Position geben.
|
||||
- Eine Bewerbung ist immer regionsbezogen.
|
||||
|
||||
### 2.2 Hierarchie
|
||||
|
||||
Kirchenämter bleiben über `church_office_type.hierarchy_level` geordnet.
|
||||
|
||||
Der normale Aufstiegspfad ist:
|
||||
|
||||
1. `lay-preacher`
|
||||
2. `village-priest`
|
||||
3. `parish-priest`
|
||||
4. `dean`
|
||||
5. `archdeacon`
|
||||
6. `bishop`
|
||||
7. `archbishop`
|
||||
8. `cardinal`
|
||||
9. `pope`
|
||||
|
||||
### 2.3 Bewerbung auf höhere Ämter
|
||||
|
||||
Der aktuelle Fehler "man kann sich nicht auf höhere Positionen bewerben als man gerade hat" soll ersetzt werden durch:
|
||||
|
||||
- Ein Charakter darf sich auf das nächsthöhere sinnvolle Amt bewerben.
|
||||
- Zusätzlich darf ein Charakter sich auf ein höheres Amt bewerben, wenn sein bisher höchstes Kirchenamt die Mindestvoraussetzung erfüllt.
|
||||
- Das System soll nicht nur aktuelle Ämter, sondern auch die bisher höchste kirchliche Laufbahn berücksichtigen.
|
||||
|
||||
Daraus folgt:
|
||||
|
||||
- Es reicht nicht, nur aktuelle `church_office` zu prüfen.
|
||||
- Es muss ein Konzept von `highestChurchOfficeRankEver` geben.
|
||||
|
||||
## 3. Entscheidungsmodell für Bewerbungen
|
||||
|
||||
### 3.1 Grundsatz
|
||||
|
||||
Über eine Bewerbung entscheidet immer das direkt übergeordnete Amt.
|
||||
|
||||
Beispiele:
|
||||
|
||||
- Über `village-priest` entscheidet `parish-priest`.
|
||||
- Über `parish-priest` entscheidet `dean`.
|
||||
- Über `dean` entscheidet `archdeacon`.
|
||||
|
||||
### 3.2 Wenn der direkte Vorgesetzte fehlt
|
||||
|
||||
Falls das direkt übergeordnete Amt in der relevanten Aufsichtskette nicht besetzt ist:
|
||||
|
||||
- Das System sucht das nächsthöhere besetzte Amt.
|
||||
- Falls überhaupt kein höheres Amt vorhanden ist, greift ein Interimsmodus.
|
||||
|
||||
Interimsmodus:
|
||||
|
||||
- Für die untersten Ebenen darf der Daemon nach Reputation und Eignung direkt besetzen.
|
||||
- Für hohe Ämter oberhalb von `bishop` soll das nur sehr zurückhaltend geschehen.
|
||||
|
||||
## 4. NPC-Bewerbungen
|
||||
|
||||
### 4.1 Ziel
|
||||
|
||||
NPCs sollen das Kirchensystem lebendig halten und offene Ämter nach und nach füllen.
|
||||
|
||||
### 4.2 Wann NPCs sich bewerben
|
||||
|
||||
Der Daemon prüft täglich:
|
||||
|
||||
- offene Sitze pro Region und Amt
|
||||
- vorhandene Spielerbewerbungen
|
||||
- vorhandene NPC-Kandidaten
|
||||
|
||||
NPC-Bewerbungen entstehen bevorzugt wenn:
|
||||
|
||||
- ein Amt offen ist
|
||||
- keine ausreichende Zahl an Bewerbungen existiert
|
||||
- in der Region oder der Elternregion geeignete NPCs vorhanden sind
|
||||
|
||||
### 4.3 Geeignete NPCs
|
||||
|
||||
Ein NPC ist grundsätzlich geeignet, wenn:
|
||||
|
||||
- er lebt
|
||||
- er nicht bereits ein gleiches oder höheres unvereinbares Kirchenamt innehat
|
||||
- sein bisher höchstes Kirchenamt oder seine bisherige Laufbahn die Stufe plausibel macht
|
||||
- seine Reputation ausreichend ist
|
||||
|
||||
Zusätzliche Faktoren für NPC-Eignung:
|
||||
|
||||
- Alter
|
||||
- Gesundheit
|
||||
- Adelstitel
|
||||
- Reputation
|
||||
- bestehendes Kirchenamt
|
||||
- bisher höchstes Kirchenamt
|
||||
|
||||
## 5. Auswahl- und Beförderungslogik
|
||||
|
||||
### 5.1 Bewertungswert
|
||||
|
||||
Für jede Bewerbung wird ein Score berechnet:
|
||||
|
||||
`churchCandidateScore`
|
||||
|
||||
Bestandteile:
|
||||
|
||||
- bisher höchstes Kirchenamt
|
||||
- aktuelles Kirchenamt
|
||||
- Reputation
|
||||
- Adelstitel
|
||||
- Alter in idealem Bereich
|
||||
- regionale Nähe
|
||||
- ggf. geringe Bonuspunkte für lange Wartezeit
|
||||
|
||||
### 5.2 Entscheidung durch Spieler
|
||||
|
||||
Wenn der zuständige Vorgesetzte ein Spielercharakter ist:
|
||||
|
||||
- Die Bewerbung erscheint wie bisher in der UI.
|
||||
- Der Spieler kann annehmen oder ablehnen.
|
||||
- Solange eine Spielerentscheidung aussteht, entscheidet der Daemon nicht automatisch.
|
||||
|
||||
Optionaler Timeout:
|
||||
|
||||
- Nach längerer Untätigkeit darf später ein automatischer Verfall oder eine automatische Daemon-Entscheidung ergänzt werden.
|
||||
- Das ist nicht Teil der ersten Ausbaustufe.
|
||||
|
||||
### 5.3 Entscheidung durch NPC
|
||||
|
||||
Wenn der zuständige Vorgesetzte ein NPC ist:
|
||||
|
||||
- Der Daemon entscheidet automatisch.
|
||||
- Maßgeblich ist primär der Bewerber-Score.
|
||||
- Zusätzlich wirkt die Reputation des NPC-Vorgesetzten als "Strengefaktor".
|
||||
|
||||
## 6. Reputation des NPC-Vorgesetzten
|
||||
|
||||
Wenn ein NPC ein Amt innehat, entscheidet er über die unter ihm liegende Position anhand von Reputation.
|
||||
|
||||
Das bedeutet:
|
||||
|
||||
- Ein angesehener NPC-Vorgesetzter bevorzugt reputationsstarke, standesgemäße und stabile Bewerber.
|
||||
- Ein schwacher oder verrufener NPC-Vorgesetzter entscheidet unberechenbarer.
|
||||
|
||||
Empfohlenes Modell:
|
||||
|
||||
- `supervisorInfluence = supervisor.reputation / 100`
|
||||
- je höher dieser Wert, desto stärker zählt der objektive Bewerber-Score
|
||||
- bei niedriger Reputation steigt der Zufallsanteil
|
||||
|
||||
Praktische Wirkung:
|
||||
|
||||
- Hohe NPC-Reputation:
|
||||
- bessere, berechenbarere Besetzung
|
||||
- Niedrige NPC-Reputation:
|
||||
- mehr Fehlbesetzungen
|
||||
- mehr schwankende Entscheidungen
|
||||
|
||||
## 7. Fehlende historische Kirchenlaufbahn
|
||||
|
||||
Damit ein Charakter sich später auf höhere Ämter bewerben kann, braucht das System mehr als nur aktuelle `church_office`.
|
||||
|
||||
Es wird deshalb ein persistierter Höchstwert benötigt:
|
||||
|
||||
- `highestChurchOfficeRankEver`
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- eigener Wert am Charakter oder in einer Laufbahntabelle
|
||||
- beim erstmaligen Erreichen eines höheren Kirchenamts aktualisieren
|
||||
- bei Verlust des Amts nicht zurücksetzen
|
||||
|
||||
Ohne diesen Wert bleibt höherer Aufstieg nach Amtsverlust oder Umstrukturierung unzuverlässig.
|
||||
|
||||
## 8. Verfügbarkeit in der UI
|
||||
|
||||
Die UI soll später drei Dinge klar darstellen:
|
||||
|
||||
- aktuelle Ämter
|
||||
- verfügbare Bewerbungen
|
||||
- eigene höchste Kirchenlaufbahn
|
||||
|
||||
Zusätzlich sinnvoll:
|
||||
|
||||
- ob die Entscheidung durch einen Spieler oder NPC getroffen wird
|
||||
- wer der zuständige Vorgesetzte ist
|
||||
- ob eine Position automatisch nachbesetzt wird
|
||||
|
||||
## 9. Daemon-Aufgaben
|
||||
|
||||
Der Daemon soll täglich folgende Schritte ausführen:
|
||||
|
||||
### 9.1 Kirchenlage erfassen
|
||||
|
||||
- offene Sitze je `church_office_type` und Region zählen
|
||||
- aktuelle Amtsträger laden
|
||||
- Spielerbewerbungen laden
|
||||
- NPC-Kandidaten bestimmen
|
||||
|
||||
### 9.2 NPC-Bewerbungen erzeugen
|
||||
|
||||
- für vakante Positionen fehlende NPC-Bewerbungen anlegen
|
||||
- keine Doppelbewerbungen für dieselbe Position erzeugen
|
||||
|
||||
### 9.3 Bewerbungen bewerten
|
||||
|
||||
- Bewerber-Score berechnen
|
||||
- zuständigen Vorgesetzten ermitteln
|
||||
- falls NPC-Vorgesetzter: Entscheidung automatisch treffen
|
||||
- falls Spieler-Vorgesetzter: Bewerbung offen lassen
|
||||
|
||||
### 9.4 Beförderungen und Besetzungen durchführen
|
||||
|
||||
- `church_office` anlegen oder aktualisieren
|
||||
- alte widersprechende Bewerbungen schließen
|
||||
- `highestChurchOfficeRankEver` aktualisieren
|
||||
|
||||
### 9.5 Sonderfall komplett leere Hierarchie
|
||||
|
||||
Wenn eine Hierarchiestufe samt Vorgesetzten fehlt:
|
||||
|
||||
- untere Ebene darf durch den Daemon interimistisch mit dem besten Kandidaten besetzt werden
|
||||
- dies soll selten und regelgeleitet geschehen
|
||||
- für hohe Spitzenämter deutlich restriktiver als für niedrige Ämter
|
||||
|
||||
## 10. Event-Kommunikation zwischen Daemon und UI
|
||||
|
||||
Neue oder präzisierte Events:
|
||||
|
||||
### 10.1 `falukantUpdateChurch`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateChurch",
|
||||
"user_id": 123,
|
||||
"reason": "applications"
|
||||
}
|
||||
```
|
||||
|
||||
Zulässige `reason`-Werte:
|
||||
|
||||
- `applications`
|
||||
- `appointment`
|
||||
- `promotion`
|
||||
- `vacancy_fill`
|
||||
- `npc_decision`
|
||||
|
||||
### 10.2 UI-Reaktion
|
||||
|
||||
- `applications`:
|
||||
- Bewerbungslisten neu laden
|
||||
- `appointment`:
|
||||
- aktuelle Ämter und verfügbare Ämter neu laden
|
||||
- `promotion`:
|
||||
- aktuelle Ämter, verfügbare Ämter, ggf. Sozialstatus/Ansehen neu laden
|
||||
- `vacancy_fill`:
|
||||
- aktuelle Ämter und verfügbare Positionen neu laden
|
||||
- `npc_decision`:
|
||||
- supervised applications und current positions neu laden
|
||||
|
||||
Zusätzlich kann weiterhin `falukantUpdateStatus` gesendet werden.
|
||||
|
||||
## 11. Backend-Anpassungen außerhalb des Daemons
|
||||
|
||||
Die Daemon-Logik allein reicht nicht. Das Backend muss angepasst werden:
|
||||
|
||||
- `getAvailableChurchPositions()` darf nicht nur aktuelle Ämter als Voraussetzung ansehen
|
||||
- es muss die bisher höchste Kirchenlaufbahn berücksichtigen
|
||||
- freie Positionen dürfen nicht nur an schon exakt lineare Amtshalter gebunden sein
|
||||
- Spielerbewerbungen und NPC-Bewerbungen müssen dieselbe Bewertungslogik unterstützen
|
||||
|
||||
## 12. Empfohlene Umsetzung in Phasen
|
||||
|
||||
### Phase C1
|
||||
|
||||
- Konzept `highestChurchOfficeRankEver` einführen
|
||||
- `getAvailableChurchPositions()` auf höchste Kirchenlaufbahn erweitern
|
||||
- UI lesbar machen
|
||||
|
||||
### Phase C2
|
||||
|
||||
- NPC-Bewerbungen im Daemon
|
||||
- automatische NPC-Entscheidungen
|
||||
|
||||
### Phase C3
|
||||
|
||||
- Interimsbesetzung für leere Hierarchien
|
||||
- Feintuning von Reputation und Zufall
|
||||
|
||||
## 13. Wichtige Designentscheidungen
|
||||
|
||||
- Spieleraufstieg bleibt antragsbasiert
|
||||
- NPCs füllen das System aktiv auf
|
||||
- hohe Reputation eines NPC-Vorgesetzten verbessert die Besetzungsqualität
|
||||
- höhere Ämter sollen auch dann erreichbar bleiben, wenn der Charakter das Voramt nicht mehr aktuell innehat
|
||||
- komplett leere Kirchenstrukturen dürfen sich wieder aufbauen
|
||||
|
||||
## 14. Offene Punkte
|
||||
|
||||
- Wo genau `highestChurchOfficeRankEver` gespeichert wird
|
||||
- ob es zusätzlich `highestChurchOfficeTypeEver` geben soll
|
||||
- ob automatische NPC-Entscheidungen ein Timeout für offene Spielerbewerbungen bekommen
|
||||
- wie stark Reputation gegenüber Adelstitel und Alter gewichtet wird
|
||||
|
||||
403
docs/FALUKANT_DEBTORS_PRISON_CONCEPT.md
Normal file
403
docs/FALUKANT_DEBTORS_PRISON_CONCEPT.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# Falukant: Schuldturm, Pfändung und wirtschaftlicher Zusammenbruch
|
||||
|
||||
Dieses Dokument beschreibt das Zielmodell für den **Schuldturm** in Falukant. Ausgangspunkt ist das bestehende Kreditsystem mit `credit` und dem bereits vorhandenen, aber noch ungenutzten Datenmodell `debtors_prism`.
|
||||
|
||||
## 1. Bestandsaufnahme
|
||||
|
||||
Bereits vorhanden:
|
||||
|
||||
- Kredite in `falukant_data.credit`
|
||||
- `amount`
|
||||
- `remaining_amount`
|
||||
- `interest_rate`
|
||||
- `falukant_user_id`
|
||||
- Bankübersicht in [BankView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/BankView.vue)
|
||||
- Modell `falukant_data.debtors_prism` über [debtors_prism.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/debtors_prism.js)
|
||||
- Kreditaufnahme und Bankübersicht im Backend in [falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js)
|
||||
|
||||
Noch nicht vorhanden:
|
||||
|
||||
- fällige Kreditraten mit Verzug
|
||||
- automatische Mahnlogik
|
||||
- echte Schuldturm-Logik
|
||||
- Pfändung / Verwertung von Vermögen
|
||||
- Reputations- und Sozialfolgen
|
||||
- Beziehungsfolgen für Liebhaber/Mätressen
|
||||
- UI für Haftstatus / wirtschaftlichen Zusammenbruch
|
||||
|
||||
Wichtig:
|
||||
|
||||
- `debtors_prism` existiert bereits, ist aber funktional bisher nicht eingebunden.
|
||||
- Ein Teil der eigentlichen Tick-Logik gehört in den externen Daemon.
|
||||
- Das Backend muss dennoch Datenmodell, APIs und UI-Basis bereitstellen.
|
||||
|
||||
## 2. Kernidee
|
||||
|
||||
Wer seine Kreditverpflichtungen **über 3 Tage** nicht bedient, kommt in den **Schuldturm**.
|
||||
|
||||
Schuldturm bedeutet:
|
||||
|
||||
- Verlust wirtschaftlicher Handlungsfähigkeit
|
||||
- staatliche / herrschaftliche Pfändung
|
||||
- Zwangsverwertung veräußerbarer Güter
|
||||
- sozialer und familiärer Absturz
|
||||
|
||||
Das System soll nicht nur eine Geldstrafe sein, sondern ein spürbarer Statuswechsel im Spiel.
|
||||
|
||||
## 3. Auslöser
|
||||
|
||||
### 3.1 Kreditverzug
|
||||
|
||||
Der Daemon prüft täglich:
|
||||
|
||||
- welche Kreditrate fällig war
|
||||
- ob sie bedient wurde
|
||||
- wie viele Verzugstage bestehen
|
||||
|
||||
Regel:
|
||||
|
||||
- `missed_days >= 3` bei mindestens einem aktiven Kredit
|
||||
- danach Eintritt in den Schuldturm
|
||||
|
||||
### 3.2 Verhältnis zu Bankrott
|
||||
|
||||
Schuldturm ist der **harte Bankrottpfad für private Kreditverschuldung**.
|
||||
|
||||
Das bedeutet:
|
||||
|
||||
- nicht jeder Geldmangel führt sofort in den Schuldturm
|
||||
- aber anhaltender Kreditverzug schon
|
||||
|
||||
Bankrott und Schuldturm können später getrennt modelliert werden:
|
||||
|
||||
- `wirtschaftlicher Bankrott`
|
||||
- `privater Kreditverzug / Schuldturm`
|
||||
|
||||
Für die erste Stufe dürfen sie aber gekoppelt sein.
|
||||
|
||||
## 4. Zustand "im Schuldturm"
|
||||
|
||||
Ein Charakter im Schuldturm hat:
|
||||
|
||||
- kein normales wirtschaftliches Standing
|
||||
- stark eingeschränkten Zugriff auf Vermögen
|
||||
- massive Reputations- und Standesfolgen
|
||||
|
||||
Empfohlene Effekte:
|
||||
|
||||
- keine neuen Kredite
|
||||
- keine neuen großen Investitionen
|
||||
- keine Standeserhöhung
|
||||
- keine neuen prestigeträchtigen Ämter
|
||||
- evtl. eingeschränkte politische / kirchliche Karriere
|
||||
|
||||
## 5. Pfändungsreihenfolge
|
||||
|
||||
Beim Eintritt in den Schuldturm oder im Anschluss über mehrere Ticks wird Vermögen verwertet.
|
||||
|
||||
Empfohlene Reihenfolge:
|
||||
|
||||
1. frei verfügbares Geld
|
||||
2. Transportmittel / Fahrzeuge
|
||||
3. Lagerbestände / verwertbare Waren
|
||||
4. Häuser / Hausbesitz
|
||||
5. Schließung von Standorten / Niederlassungen
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Nicht alles muss in einem Tick geschehen.
|
||||
- Sinnvoll ist ein mehrstufiger Abbau, damit die UI den Prozess sichtbar machen kann.
|
||||
|
||||
## 6. Verwertbare Güter
|
||||
|
||||
### 6.1 Fahrzeuge
|
||||
|
||||
Transportmittel sollen verkauft werden, sofern sie nicht unpfändbar markiert sind.
|
||||
|
||||
Folgen:
|
||||
|
||||
- geringere Handlungsfähigkeit
|
||||
- weniger Handelsoptionen
|
||||
|
||||
### 6.2 Lager und Waren
|
||||
|
||||
Lagerbestände und handelbare Waren sollen mit Abschlag verwertet werden.
|
||||
|
||||
Ziel:
|
||||
|
||||
- offene Kreditschuld reduzieren
|
||||
- laufende Produktion destabilisieren
|
||||
|
||||
### 6.3 Haus
|
||||
|
||||
Das Haus soll gepfändet werden, wenn die Schuld nicht anders gedeckt werden kann.
|
||||
|
||||
Folgen:
|
||||
|
||||
- Rückfall auf ein niedrigeres Haus
|
||||
- Einbruch bei Hauszustand, Hausstand und Dienerschaft
|
||||
- negative Effekte auf Ehe, Haushalt und Stand
|
||||
|
||||
### 6.4 Niederlassungen
|
||||
|
||||
Standorte sollen geschlossen werden können, wenn Fahrzeuge/Waren/Haus nicht ausreichen.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- zuerst unrentable oder niedrigwertige Niederlassungen
|
||||
- danach teurere / prestigeträchtigere
|
||||
|
||||
## 7. Soziale Folgen
|
||||
|
||||
### 7.1 Reputation
|
||||
|
||||
Beim Eintritt in den Schuldturm:
|
||||
|
||||
- spürbarer einmaliger Reputationsverlust
|
||||
|
||||
Während der Haft:
|
||||
|
||||
- täglicher oder periodischer weiterer Malus
|
||||
|
||||
### 7.2 Kreditwürdigkeit
|
||||
|
||||
Es braucht einen eigenen Zustand oder Wert:
|
||||
|
||||
- `creditworthiness`
|
||||
oder
|
||||
- `credit_penalty_level`
|
||||
|
||||
Folgen:
|
||||
|
||||
- geringere `availableCredit`
|
||||
- höhere Gebühren
|
||||
- evtl. komplette Kreditsperre für längere Zeit
|
||||
|
||||
### 7.3 Liebhaber / Mätressen
|
||||
|
||||
Liebhaber/Mätressen können abspringen.
|
||||
|
||||
Wirkung:
|
||||
|
||||
- hohe Chance bei geringer Zuneigung oder niedriger Finanzierung
|
||||
- höhere Chance bei öffentlich gewordenem Schuldturm
|
||||
- repräsentative Beziehungen brechen eher bei massivem Statusverlust
|
||||
|
||||
Mögliche Folgen:
|
||||
|
||||
- Beziehungsende
|
||||
- starke Senkung von `affection`
|
||||
- Sichtbarkeit eines Skandals
|
||||
|
||||
### 7.4 Ehe und Familie
|
||||
|
||||
Der Schuldturm soll auch auf Ehe und Hausfrieden wirken:
|
||||
|
||||
- `marriage_satisfaction` sinkt
|
||||
- `household_tension_score` steigt
|
||||
- Kinder-/Erbpfad kann instabiler werden
|
||||
|
||||
## 8. Bezug zu bereits existierenden Systemen
|
||||
|
||||
Der Schuldturm soll sich an bestehende Falukant-Systeme ankoppeln:
|
||||
|
||||
- Kredite
|
||||
- Haus / Dienerschaft
|
||||
- Familie / Liebschaften
|
||||
- Reputation
|
||||
- Produktionszertifikat
|
||||
- Sozialstatus
|
||||
|
||||
### 8.1 Produktionszertifikat
|
||||
|
||||
Bankrott / Schuldturm kann ein Sonderfall für Zertifikatsverlust sein.
|
||||
|
||||
Das passt zur bereits dokumentierten Regel:
|
||||
|
||||
- Herabstufung bei `Bankrott`
|
||||
|
||||
### 8.2 Sozialstatus
|
||||
|
||||
Während oder nach schwerem Schuldturm:
|
||||
|
||||
- kein Aufstieg im Stand
|
||||
- evtl. spätere Herabstufung im Extremfall
|
||||
|
||||
Für die erste Stufe reicht:
|
||||
|
||||
- Aufstieg blockieren
|
||||
|
||||
## 9. Daemon-Aufgaben
|
||||
|
||||
Der externe Daemon soll:
|
||||
|
||||
### 9.1 täglich prüfen
|
||||
|
||||
- fällige Kreditraten
|
||||
- bezahlte / unbezahlte Beträge
|
||||
- Verzugstage je Kredit oder Nutzer
|
||||
|
||||
### 9.2 Schuldturm auslösen
|
||||
|
||||
Wenn Verzug >= 3 Tage:
|
||||
|
||||
- Schuldturmstatus setzen
|
||||
- Reputations- und Kreditwürdigkeits-Malus anwenden
|
||||
- Socket-Events senden
|
||||
|
||||
### 9.3 Verwertung durchführen
|
||||
|
||||
In geordneter Reihenfolge:
|
||||
|
||||
- Geld abbuchen
|
||||
- Fahrzeuge verkaufen
|
||||
- Waren verwerten
|
||||
- Häuser pfänden
|
||||
- Niederlassungen schließen
|
||||
|
||||
### 9.4 Familienfolgen anwenden
|
||||
|
||||
- Ehe verschlechtern
|
||||
- Haushaltsspannung erhöhen
|
||||
- Liebschaften destabilisieren
|
||||
|
||||
## 10. Event-Kommunikation zwischen Daemon und UI
|
||||
|
||||
Neue Events:
|
||||
|
||||
### 10.1 `falukantUpdateDebt`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateDebt",
|
||||
"user_id": 123,
|
||||
"reason": "delinquency"
|
||||
}
|
||||
```
|
||||
|
||||
Mögliche `reason`:
|
||||
|
||||
- `delinquency`
|
||||
- `debtors_prison_entered`
|
||||
- `asset_seizure`
|
||||
- `branch_closure`
|
||||
- `vehicle_liquidation`
|
||||
- `house_seizure`
|
||||
- `debtors_prison_released`
|
||||
|
||||
### 10.2 UI-Reaktion
|
||||
|
||||
- Bankansicht neu laden
|
||||
- Haus neu laden
|
||||
- Niederlassungen neu laden
|
||||
- Statusbar / Dashboard neu laden
|
||||
- Familienansicht ggf. neu laden
|
||||
|
||||
Zusätzlich sinnvoll:
|
||||
|
||||
- Toast für Eintritt in den Schuldturm
|
||||
- Toast für Pfändung / Zwangsverkauf
|
||||
|
||||
## 11. Backend-Aufgaben außerhalb des Daemons
|
||||
|
||||
Das Backend muss:
|
||||
|
||||
- Schuldturmstatus lesbar machen
|
||||
- Bankansicht um Verzug / Haftstatus erweitern
|
||||
- veräußerbare Güter für den Daemon eindeutig bereitstellen
|
||||
- Endpunkte und UI-Infos für den Schuldturm liefern
|
||||
|
||||
### 11.1 Datenmodell
|
||||
|
||||
Da `debtors_prism` bereits existiert, bietet sich dieses Modell an für:
|
||||
|
||||
- `character_id`
|
||||
- `entered_at`
|
||||
- `released_at`
|
||||
- `status`
|
||||
- `debt_at_entry`
|
||||
- `remaining_debt`
|
||||
- `reason`
|
||||
|
||||
Falls die Tabelle noch nur `character_id` enthält, muss sie erweitert werden.
|
||||
|
||||
### 11.2 Bank-API
|
||||
|
||||
Die Bankübersicht soll später zusätzlich liefern:
|
||||
|
||||
- `inDebtorsPrison`
|
||||
- `daysOverdue`
|
||||
- `nextForcedAction`
|
||||
- `creditworthiness`
|
||||
|
||||
## 12. UI-Anforderungen
|
||||
|
||||
### 12.1 Bank
|
||||
|
||||
In [BankView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/BankView.vue):
|
||||
|
||||
- Hinweis auf Zahlungsverzug
|
||||
- Warnstufe bei 1 / 2 / 3 Tagen
|
||||
- eigener Block für Schuldturmstatus
|
||||
|
||||
### 12.2 Übersicht / Status
|
||||
|
||||
In Falukant-Overview / StatusBar:
|
||||
|
||||
- sichtbarer Status "Schuldturm"
|
||||
- evtl. reduzierter Handlungsstatus
|
||||
|
||||
### 12.3 Haus / Niederlassungen
|
||||
|
||||
- Hinweise bei Pfändung / Zwangsverkauf
|
||||
- Schließungsereignisse sichtbar machen
|
||||
|
||||
### 12.4 Familie
|
||||
|
||||
- Hinweise auf abgesprungene Liebhaber / Mätressen
|
||||
- Auswirkungen auf Ehe / Haushalt sichtbar
|
||||
|
||||
## 13. Empfohlene Umsetzung in Phasen
|
||||
|
||||
### Phase D1: Basis
|
||||
|
||||
- `debtors_prism` fachlich ausbauen
|
||||
- Bank-API um Verzug und Haftstatus erweitern
|
||||
- UI-Warnungen in Bank und Status
|
||||
|
||||
### Phase D2: Verwertung
|
||||
|
||||
- Fahrzeuge, Waren und Häuser als verwertbare Assets modellieren
|
||||
- Daemon führt Pfändung schrittweise aus
|
||||
|
||||
### Phase D3: Soziale Folgen
|
||||
|
||||
- Reputation
|
||||
- Kreditwürdigkeit
|
||||
- Liebhaber / Mätressen
|
||||
- Ehe / Hausfrieden
|
||||
|
||||
### Phase D4: Langfristige Folgen
|
||||
|
||||
- Produktionszertifikat
|
||||
- Stand / Karriereblockaden
|
||||
- eventuelle spätere Herabstufung
|
||||
|
||||
## 14. Offene Punkte
|
||||
|
||||
- genaue Kreditratenlogik im Daemon
|
||||
- wie stark Häuser und Niederlassungen mit Abschlag verkauft werden
|
||||
- ob Schuldturm zeitlich begrenzt oder rein schuldgetrieben endet
|
||||
- ob Kreditwürdigkeit als eigener numerischer Wert gespeichert wird
|
||||
|
||||
## 15. Empfehlung
|
||||
|
||||
Für die erste echte Umsetzung:
|
||||
|
||||
1. `debtors_prism` ausbauen
|
||||
2. Verzugstage im Daemon sauber pflegen
|
||||
3. Eintritt in den Schuldturm sichtbar machen
|
||||
4. zuerst Fahrzeuge/Waren/Haus, erst danach Niederlassungen
|
||||
|
||||
So bleibt der Spielzustand hart, aber nachvollziehbar und technisch gut integrierbar.
|
||||
|
||||
447
docs/FALUKANT_DEBTORS_PRISON_DAEMON_SPEC.md
Normal file
447
docs/FALUKANT_DEBTORS_PRISON_DAEMON_SPEC.md
Normal file
@@ -0,0 +1,447 @@
|
||||
# Falukant: Schuldturm und Pfändung - Daemon-Spezifikation
|
||||
|
||||
Dieses Dokument beschreibt die Umsetzung des **Schuldturm-Systems** im externen Daemon.
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Die projektseitigen DB-Felder, API-Erweiterungen, UI-Warnungen und Aktionssperren sind bereits umgesetzt.
|
||||
- Der Daemon ist die führende Quelle für:
|
||||
- Verzugstage
|
||||
- Eintritt in den Schuldturm
|
||||
- Pfändung und Verwertung
|
||||
- soziale Folgen
|
||||
- Freilassung
|
||||
|
||||
## 1. Bereits vorhandene Datenbasis
|
||||
|
||||
Bereits im Projekt vorhanden:
|
||||
|
||||
- `falukant_data.credit`
|
||||
- `falukant_data.debtors_prism`
|
||||
- `falukant_data.user_house`
|
||||
- inkl. `household_tension_score`
|
||||
- inkl. `household_tension_reasons_json`
|
||||
- Familien-/Liebschaftsdaten in:
|
||||
- `falukant_data.relationship`
|
||||
- `falukant_data.relationship_state`
|
||||
- `falukant_data.child_relation`
|
||||
|
||||
Bereits erweitert:
|
||||
|
||||
- `debtors_prism.status`
|
||||
- `debtors_prism.entered_at`
|
||||
- `debtors_prism.released_at`
|
||||
- `debtors_prism.debt_at_entry`
|
||||
- `debtors_prism.remaining_debt`
|
||||
- `debtors_prism.days_overdue`
|
||||
- `debtors_prism.reason`
|
||||
- `debtors_prism.creditworthiness_penalty`
|
||||
- `debtors_prism.next_forced_action`
|
||||
- `debtors_prism.assets_seized_json`
|
||||
- `debtors_prism.public_known`
|
||||
|
||||
Es sind für den Daemon derzeit keine weiteren DB-Änderungen nötig.
|
||||
|
||||
## 2. Grundregel
|
||||
|
||||
Ein Charakter kommt in den Schuldturm, wenn:
|
||||
|
||||
- mindestens ein aktiver Kredit offen ist
|
||||
- fällige Kreditbedienung ausbleibt
|
||||
- und `days_overdue >= 3`
|
||||
|
||||
Der Daemon prüft dies im Daily-Tick.
|
||||
|
||||
## 3. Zustände
|
||||
|
||||
`debtors_prism.status` verwendet mindestens:
|
||||
|
||||
- `delinquent`
|
||||
- `imprisoned`
|
||||
- `released`
|
||||
|
||||
Bedeutung:
|
||||
|
||||
- `delinquent`: Kreditverzug, aber noch nicht im Schuldturm
|
||||
- `imprisoned`: im Schuldturm, Verwertung läuft
|
||||
- `released`: historischer abgeschlossener Fall
|
||||
|
||||
## 4. Daily-Tick
|
||||
|
||||
Der Daily-Tick prüft pro Falukant-Nutzer:
|
||||
|
||||
1. aktive Kredite
|
||||
2. verbleibende Schuld
|
||||
3. geleistete Bedienung seit letztem Tick
|
||||
4. neue Verzugstage
|
||||
5. Schuldturm-Eintritt
|
||||
6. laufende soziale Folgen
|
||||
7. Verwertungsschritt
|
||||
|
||||
### 4.1 Verzugstage
|
||||
|
||||
Regel:
|
||||
|
||||
- wenn offene Schuld vorhanden und fällige Bedienung ausbleibt:
|
||||
- `days_overdue += 1`
|
||||
- wenn Kreditpflicht erfüllt wurde:
|
||||
- `days_overdue = 0`
|
||||
- falls nicht im Schuldturm
|
||||
|
||||
Wenn noch kein aktiver `debtors_prism`-Eintrag existiert:
|
||||
|
||||
- bei erstem Verzug `debtors_prism` anlegen mit
|
||||
- `status = 'delinquent'`
|
||||
- `days_overdue = 1`
|
||||
- `remaining_debt = aktuelle offene Schuld`
|
||||
- `next_forced_action = 'reminder'`
|
||||
|
||||
### 4.2 Warnstufen
|
||||
|
||||
Bei Verzug:
|
||||
|
||||
- Tag 1:
|
||||
- `next_forced_action = 'reminder'`
|
||||
- Event `falukantUpdateDebt` mit `reason: 'delinquency'`
|
||||
- Tag 2:
|
||||
- `next_forced_action = 'final_warning'`
|
||||
- Event `falukantUpdateDebt` mit `reason: 'delinquency'`
|
||||
- Tag 3:
|
||||
- Schuldturm-Eintritt
|
||||
|
||||
Für Warnstufen senden:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- zusätzlich `falukantUpdateStatus`
|
||||
|
||||
## 5. Eintritt in den Schuldturm
|
||||
|
||||
Bei `days_overdue >= 3`:
|
||||
|
||||
- `status = 'imprisoned'`
|
||||
- `entered_at = now()`
|
||||
- `released_at = null`
|
||||
- `debt_at_entry = aktuelle offene Schuld`
|
||||
- `remaining_debt = aktuelle offene Schuld`
|
||||
- `reason = 'credit_default'`
|
||||
- `creditworthiness_penalty += 45`
|
||||
- `next_forced_action = 'asset_seizure'`
|
||||
- `public_known = true`
|
||||
|
||||
### 5.1 Sofortfolgen bei Eintritt
|
||||
|
||||
Einmalig anwenden:
|
||||
|
||||
- Reputation deutlich senken
|
||||
- Empfehlung: `-12`
|
||||
- `marriage_satisfaction` senken
|
||||
- Empfehlung: `-10`
|
||||
- `household_tension_score` erhöhen
|
||||
- Empfehlung: `+15`
|
||||
- `household_tension_reasons_json` um `debtorsPrison` ergänzen
|
||||
|
||||
Zusätzlich:
|
||||
|
||||
- aktive Liebhaber/Mätressen sichtbar destabilisieren
|
||||
- mindestens `affection -= 4`
|
||||
- Kreditaufnahme und aktive Falukant-Aktionen bleiben projektseitig bereits gesperrt
|
||||
|
||||
Bei Eintritt senden:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'debtors_prison_entered'`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `reason: 'daily'`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
|
||||
## 6. Verwertung / Pfändung
|
||||
|
||||
Die Verwertung läuft nicht alles auf einmal, sondern schrittweise pro Tick.
|
||||
|
||||
Reihenfolge:
|
||||
|
||||
1. freies Geld
|
||||
2. Fahrzeuge
|
||||
3. Waren / Lagerbestände
|
||||
4. Haus
|
||||
5. Niederlassungen
|
||||
|
||||
Ziel:
|
||||
|
||||
- `remaining_debt` schrittweise senken
|
||||
- Fortschritt im UI sichtbar machen
|
||||
|
||||
### 6.1 Geld
|
||||
|
||||
Wenn `falukant_user.money > 0`:
|
||||
|
||||
- direkt zur Schuld tilgen
|
||||
- `remaining_debt -= eingezogener_betrag`
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'asset_seizure'`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
### 6.2 Fahrzeuge
|
||||
|
||||
Verkaufe zuerst:
|
||||
|
||||
- freie Fahrzeuge
|
||||
- dann weniger wertvolle Typen
|
||||
- keine Fahrzeuge in aktiven Transporten im selben Tick anfassen, falls technisch problematisch
|
||||
|
||||
Erlös:
|
||||
|
||||
- Empfehlung: `vehicle_type.cost * condition_factor * 0.55`
|
||||
|
||||
Zusätzlich in `assets_seized_json` protokollieren:
|
||||
|
||||
- Typ
|
||||
- Anzahl
|
||||
- Erlös
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'vehicle_liquidation'`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
### 6.3 Waren / Lager
|
||||
|
||||
Verwertbare Güter:
|
||||
|
||||
- Lagerbestände
|
||||
- Inventar
|
||||
- handelbare Waren
|
||||
|
||||
Erlös:
|
||||
|
||||
- Empfehlung: Marktwert mit Abschlag von `35% bis 50%`
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'asset_seizure'`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantBranchUpdate`
|
||||
|
||||
### 6.4 Haus
|
||||
|
||||
Wenn Restschuld nach Geld/Fahrzeugen/Waren weiter hoch ist:
|
||||
|
||||
- Haus pfänden
|
||||
- Spieler auf niedrigeres Haus oder Minimalhaus zurücksetzen
|
||||
- Dienerschaft reduzieren
|
||||
- `household_order` senken
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'house_seizure'`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `reason: 'daily'`
|
||||
|
||||
### 6.5 Niederlassungen
|
||||
|
||||
Wenn weiter nicht gedeckt:
|
||||
|
||||
- Niederlassungen schließen
|
||||
- zuerst niedrige Stufe / niedriger Wert
|
||||
- Hauptniederlassung nur als letzter Schritt
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'branch_closure'`
|
||||
- `falukantBranchUpdate`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
## 7. Laufende soziale Folgen im Schuldturm
|
||||
|
||||
Solange `status = 'imprisoned'`:
|
||||
|
||||
- täglicher Reputationsmalus
|
||||
- Empfehlung: `-2`
|
||||
- zusätzliche `creditworthiness_penalty += 1` pro Tag
|
||||
- `marriage_satisfaction -= 1`
|
||||
- `household_tension_score += 2`
|
||||
|
||||
Wenn aktive Liebschaften bestehen:
|
||||
|
||||
- `affection -= 2`
|
||||
- bei niedriger Zuneigung oder hoher Sichtbarkeit kann Beziehung enden
|
||||
|
||||
Empfohlene Absprungregel:
|
||||
|
||||
- wenn `affection <= 30` oder `months_underfunded >= 2`
|
||||
- Chance auf Beziehungsende prüfen
|
||||
- bei repräsentativen Beziehungen zusätzlich höhere Absprungchance, wenn `public_known = true`
|
||||
|
||||
Events bei sozialen Folgewirkungen:
|
||||
|
||||
- `falukantUpdateFamily`
|
||||
- `reason: 'daily'`
|
||||
- zusätzlich `falukantUpdateStatus`
|
||||
|
||||
## 8. Kreditwürdigkeit
|
||||
|
||||
Die UI rechnet bereits aus `creditworthiness_penalty` und Status einen sichtbaren Wert.
|
||||
|
||||
Der Daemon muss pflegen:
|
||||
|
||||
- `creditworthiness_penalty`
|
||||
- `status`
|
||||
- `days_overdue`
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Eintritt Schuldturm: `+45`
|
||||
- pro weiterem Hafttag: `+1`
|
||||
- Hauspfändung: zusätzlich `+10`
|
||||
- Niederlassungsschließung: zusätzlich `+8`
|
||||
|
||||
## 9. Freilassung
|
||||
|
||||
Freilassung, wenn:
|
||||
|
||||
- keine relevante Restschuld mehr offen ist
|
||||
oder
|
||||
- ein definierter Restwert unterschritten wird, falls ihr einen Bagatellgrenzwert wollt
|
||||
|
||||
Dann:
|
||||
|
||||
- `status = 'released'`
|
||||
- `released_at = now()`
|
||||
- `next_forced_action = null`
|
||||
- `days_overdue = 0`
|
||||
- `remaining_debt = 0`
|
||||
|
||||
Events:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- `reason: 'debtors_prison_released'`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `reason: 'daily'`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
|
||||
Keine automatische vollständige soziale Heilung:
|
||||
|
||||
- Reputation bleibt reduziert
|
||||
- Kreditwürdigkeit bleibt reduziert
|
||||
- Familie/Haus bleiben in Folgezuständen
|
||||
|
||||
## 10. Event-Kommunikation zur UI
|
||||
|
||||
Der Daemon sendet als Primärevent:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateDebt",
|
||||
"user_id": 123,
|
||||
"reason": "delinquency"
|
||||
}
|
||||
```
|
||||
|
||||
Mögliche `reason`:
|
||||
|
||||
- `delinquency`
|
||||
- `debtors_prison_entered`
|
||||
- `asset_seizure`
|
||||
- `vehicle_liquidation`
|
||||
- `house_seizure`
|
||||
- `branch_closure`
|
||||
- `debtors_prison_released`
|
||||
|
||||
### 10.1 Begleitevents
|
||||
|
||||
Je nach Folge zusätzlich:
|
||||
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
- `falukantUpdateFamily`
|
||||
|
||||
### 10.2 Empfohlene Minimalregeln
|
||||
|
||||
- `delinquency`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `debtors_prison_entered`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
- `asset_seizure`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- optional `falukantBranchUpdate`
|
||||
- `vehicle_liquidation`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `house_seizure`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantUpdateFamily`
|
||||
- `branch_closure`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantBranchUpdate`
|
||||
- `debtors_prison_released`:
|
||||
- `falukantUpdateDebt`
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantUpdateFamily`
|
||||
- `falukantHouseUpdate`
|
||||
- `falukantBranchUpdate`
|
||||
|
||||
## 11. Idempotenz
|
||||
|
||||
Der Worker muss idempotent arbeiten.
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Eintritt in den Schuldturm nicht mehrfach für denselben aktiven Fall auslösen
|
||||
- Verwertungsschritte nur einmal je Asset anwenden
|
||||
- `released` nicht erneut freisetzen
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- pro Tick Transaktion
|
||||
- pro Nutzer eine klare Reihenfolge
|
||||
- Änderungen in `assets_seized_json` protokollieren
|
||||
|
||||
## 12. Mindestumsetzung für Version 1
|
||||
|
||||
Pflicht:
|
||||
|
||||
1. Verzugstage pflegen
|
||||
2. Eintritt nach 3 Tagen
|
||||
3. Status und Penalty schreiben
|
||||
4. Geld zuerst einziehen
|
||||
5. danach Fahrzeuge
|
||||
6. Events senden
|
||||
|
||||
Danach:
|
||||
|
||||
7. Hauspfändung
|
||||
8. Niederlassungsschließung
|
||||
9. volle Familienfolgen
|
||||
|
||||
## 13. Hinweis an den Daemon
|
||||
|
||||
Die projektseitigen Grundlagen sind bereits umgesetzt:
|
||||
|
||||
- `debtors_prism` ist erweitert
|
||||
- Bank-/Haus-/Familien-/Übersichts-UI reagiert auf den Status
|
||||
- aktive Falukant-Aktionen werden im Backend bereits gesperrt, sobald `inDebtorsPrison = true`
|
||||
|
||||
Der Daemon muss daher vor allem die Zustände und Folgen zuverlässig schreiben und die dokumentierten Events senden.
|
||||
342
docs/FALUKANT_LOVERS_CONCEPT.md
Normal file
342
docs/FALUKANT_LOVERS_CONCEPT.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# Falukant: Konzept für Liebhaber, Geliebte und Mätressen
|
||||
|
||||
## Ziel
|
||||
|
||||
Das Familiensystem von Falukant soll neben Ehe, Verlobung und Nachkommen auch außereheliche Bindungen abbilden. Im frühen Mittelalter sind Liebhaberinnen, Geliebte und Mätressen kein moderner Privatbereich, sondern ein sozialer, wirtschaftlicher und standesabhängiger Faktor. Das System soll deshalb:
|
||||
|
||||
- zur Spielwelt passen
|
||||
- je nach Stand unterschiedlich bewertet werden
|
||||
- Ansehen, Frömmigkeit und Familienfrieden beeinflussen
|
||||
- laufende Kosten verursachen
|
||||
- Stoff für Ereignisse, Skandale und Machtspiele liefern
|
||||
|
||||
## Grundprinzip
|
||||
|
||||
Eine außereheliche Beziehung ist in Falukant weder pauschal erlaubt noch pauschal verboten. Entscheidend sind:
|
||||
|
||||
- öffentlicher Bekanntheitsgrad
|
||||
- sozialer Stand der Spielfigur
|
||||
- Familienstand der Spielfigur
|
||||
- gesellschaftliche Erwartung der Umgebung
|
||||
- Fähigkeit, die Beziehung finanziell und politisch zu tragen
|
||||
|
||||
Die gleiche Beziehung kann für einen niedrigen Stand ruinös, für einen reichen Stadtadeligen unerquicklich, aber handhabbar und für einen hohen Adeligen unter Bedingungen tolerierbar sein.
|
||||
|
||||
## Begriffe
|
||||
|
||||
Für die Mechanik sollten drei Hauptformen unterschieden werden:
|
||||
|
||||
### Heimliche Liebschaft
|
||||
|
||||
- diskrete Beziehung ohne offizielle Duldung
|
||||
- geringe laufende Grundkosten
|
||||
- erhöhtes Skandal- und Erpressungsrisiko
|
||||
- besonders gefährlich bei verheirateten Figuren
|
||||
|
||||
### Geliebte oder Liebhaber
|
||||
|
||||
- wiederkehrende, bekannte außereheliche Beziehung
|
||||
- im engeren Umfeld teilweise bekannt
|
||||
- mittlere Unterhaltskosten
|
||||
- spürbarer Einfluss auf Ehe, Hausstand und Ansehen
|
||||
|
||||
### Mätresse oder Favorit
|
||||
|
||||
- gesellschaftlich wahrnehmbare, dauerhaft unterhaltene Beziehung
|
||||
- vor allem für gehobene Stände denkbar
|
||||
- hohe regelmäßige Kosten
|
||||
- kann Status, Gerüchte, Neid und politische Verbindungen erzeugen
|
||||
|
||||
Hinweis für die Spielwelt: Für männliche und weibliche Spielfiguren soll das System symmetrisch funktionieren. Die gesellschaftliche Reaktion kann jedoch je nach Geschlecht und Stand unterschiedlich stark ausfallen.
|
||||
|
||||
## Standeslogik
|
||||
|
||||
Die Behandlung soll nicht nur von „gut oder schlecht“ abhängen, sondern vom Stand.
|
||||
|
||||
### Unfreie, Freie, einfache Bürger
|
||||
|
||||
- außereheliche Beziehungen werden schnell als Verschwendung oder Unsittlichkeit gewertet
|
||||
- schon geringe Zusatzkosten können den Haushalt destabilisieren
|
||||
- offenkundige Affären senken Ansehen deutlich
|
||||
- Heimlichkeit ist wichtiger als Repräsentation
|
||||
|
||||
Typische Wirkung:
|
||||
|
||||
- stärkerer Ansehensverlust
|
||||
- erhöhtes Risiko von Streit im Haus
|
||||
- kaum gesellschaftlicher Nutzen
|
||||
|
||||
### Wohlhabende Bürger, Patrizier, städtische Oberschicht
|
||||
|
||||
- diskrete Beziehungen können geduldet werden, wenn Haushalt und Ehe nach außen stabil bleiben
|
||||
- auffällige Affären schaden dem Ruf in Zünften, Rat und Nachbarschaft
|
||||
- die finanzielle Belastung ist tragbar, wird aber sichtbar
|
||||
|
||||
Typische Wirkung:
|
||||
|
||||
- bei Diskretion nur mäßiger Ansehensverlust
|
||||
- bei öffentlichem Bekanntwerden deutlicher Malus
|
||||
- gelegentlich soziale Vorteile über Kontakte der Geliebten möglich
|
||||
|
||||
### Niederer Adel
|
||||
|
||||
- Geliebte oder Mätressen sind nicht unvorstellbar, aber müssen „standesgemäß“ geführt werden
|
||||
- eine vernachlässigte Ehe oder ein niedriger sozialer Rang der Geliebten kann das Haus kompromittieren
|
||||
- uneheliche Kinder oder öffentliche Kränkungen des Ehepartners schaden besonders
|
||||
|
||||
Typische Wirkung:
|
||||
|
||||
- moderate bis starke Ansehensschwankungen je nach Öffentlichkeit
|
||||
- Frömmigkeit und Hausfrieden werden wichtiger
|
||||
- politische Nebeneffekte möglich
|
||||
|
||||
### Hoher Adel
|
||||
|
||||
- eine diskret und kostspielig unterhaltene Mätresse kann als Ausdruck von Macht und Überfluss toleriert werden
|
||||
- dieselbe Situation wird zum Skandal, wenn Haus, Kirche oder Erbfolge bedroht sind
|
||||
- das Problem ist weniger die bloße Existenz als die öffentliche Unordnung
|
||||
|
||||
Typische Wirkung:
|
||||
|
||||
- geringe oder neutrale Wirkung bei geordneter Diskretion
|
||||
- starker Malus bei Skandal, Erpressung, Streit mit Ehepartner oder unehelichen Erbansprüchen
|
||||
- hohe Unterhaltskosten sind Pflicht, nicht Kür
|
||||
|
||||
## Kernwerte pro Beziehung
|
||||
|
||||
Jede Liebhaber-Beziehung sollte mindestens diese Werte tragen:
|
||||
|
||||
- `type`: heimlich, geliebt, Mätresse/Favorit
|
||||
- `affection`: Zuneigung und Bindung
|
||||
- `visibility`: wie bekannt die Beziehung ist
|
||||
- `discretion`: wie gut sie verborgen oder kontrolliert wird
|
||||
- `maintenanceLevel`: wie aufwendig die Beziehung unterhalten wird
|
||||
- `monthlyCost`: laufende Kosten
|
||||
- `statusFit`: passt die Beziehung zum Stand der Spielfigur
|
||||
- `householdTension`: Spannungen im eigenen Haus
|
||||
- `scandalRisk`: Risiko für Gerüchte, Erpressung oder Entdeckung
|
||||
- `fertilityRisk`
|
||||
- `politicalValue`
|
||||
- `churchOffense`
|
||||
- `favoredByCourt`
|
||||
|
||||
## Laufende Kosten
|
||||
|
||||
Eine außereheliche Beziehung muss regelmäßig Geld kosten. Sonst wird sie spielerisch zu billig.
|
||||
|
||||
### Basiskosten
|
||||
|
||||
- Geschenke
|
||||
- Unterkunft oder Versorgung
|
||||
- Kleidung und Schmuck
|
||||
- Reisen, Botengänge, Treffen
|
||||
|
||||
### Zusätzliche Kosten bei gehobenen Formen
|
||||
|
||||
- eigenes Haus oder eigene Zimmer
|
||||
- Dienerschaft
|
||||
- Bewachung oder Diskretionsgeld
|
||||
- Kleidung auf Standesniveau
|
||||
- gesellschaftliche Geschenke
|
||||
|
||||
### Kostenlogik
|
||||
|
||||
Die Kosten sollen aus zwei Faktoren entstehen:
|
||||
|
||||
- Beziehungsform
|
||||
- Stand der Spielfigur
|
||||
|
||||
Beispielhaft:
|
||||
|
||||
- Heimliche Liebschaft: niedrige Grundkosten, aber höheres Risiko
|
||||
- Geliebte: mittlere planbare Kosten
|
||||
- Mätresse/Favorit: hohe planbare Kosten plus mögliche Sonderausgaben
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Ein hoher Adeliger darf eine Mätresse nicht billig führen.
|
||||
- Wer zu wenig investiert, verliert Diskretion, Zuneigung und Ansehen.
|
||||
|
||||
## Wirkung auf Ansehen
|
||||
|
||||
Ansehen soll nicht nur einmalig sinken, sondern über Zustände beeinflusst werden.
|
||||
|
||||
### Positive oder neutrale Fälle
|
||||
|
||||
- hohe Stände
|
||||
- gute Diskretion
|
||||
- Ehe und Haushalt wirken stabil
|
||||
- keine Erbfolgen oder offenen Kränkungen
|
||||
- Geliebte steht sozial nicht völlig außerhalb des Hauses
|
||||
|
||||
Mögliche Wirkung:
|
||||
|
||||
- kein Malus
|
||||
- geringer passiver Malus
|
||||
- in Ausnahmefällen leichter Statusbonus als Zeichen von Überfluss und Einfluss
|
||||
|
||||
### Negative Fälle
|
||||
|
||||
- Beziehung ist öffentlich
|
||||
- Spielfigur ist verheiratet
|
||||
- die Ehefrau oder der Ehemann wird sichtbar gedemütigt
|
||||
- die Geliebte passt nicht zum Stand
|
||||
- die Kosten ruinieren den Haushalt
|
||||
- die Kirche oder lokale Autoritäten greifen das Thema auf
|
||||
|
||||
Mögliche Wirkung:
|
||||
|
||||
- täglicher oder wöchentlicher Ansehensverlust
|
||||
- einmalige Skandalereignisse
|
||||
- höhere Kosten für Reputationspflege
|
||||
- Nachteile bei Standesaufstieg
|
||||
|
||||
## Wirkung auf Familienleben
|
||||
|
||||
Das System muss spürbar mit Ehe und Haushalt verbunden sein.
|
||||
|
||||
### Auf die Ehe
|
||||
|
||||
- Ehezufriedenheit sinkt
|
||||
- Streitwahrscheinlichkeit steigt
|
||||
- Geschenke oder Feste für den Ehepartner können Konflikte mildern
|
||||
- bei sehr hoher Spannung drohen Trennung, Rückzug oder öffentliche Kränkung
|
||||
|
||||
### Auf Kinder und Erbfolge
|
||||
|
||||
- uneheliche Kinder können später Ereignisse auslösen
|
||||
- anerkannte uneheliche Kinder können Hausfrieden und Stand belasten
|
||||
- je höher der Stand, desto wichtiger wird die Frage nach legitimer Erbfolge
|
||||
|
||||
### Auf den Familienbereich
|
||||
|
||||
In `FamilyView` sollte eine Liebhaber-Person nicht nur mit Name und Zuneigung erscheinen, sondern auch mit:
|
||||
|
||||
- Form der Beziehung
|
||||
- monatlichen Kosten
|
||||
- Bekanntheitsgrad
|
||||
- aktuellem Einfluss auf Hausfrieden
|
||||
- aktuellem Einfluss auf Ansehen
|
||||
|
||||
## Wirkung auf Kirche und Frömmigkeit
|
||||
|
||||
Für die Epoche ist die religiöse Dimension wichtig.
|
||||
|
||||
- Hohe Frömmigkeit plus öffentliche Affäre erzeugt stärkere Heuchelei-Strafe.
|
||||
- Niedrige Frömmigkeit macht Affären sozial nicht folgenlos, kann aber kirchliche Reaktionen weniger überraschend wirken lassen.
|
||||
- Kirchenspenden oder Bußhandlungen könnten später Skandale abmildern, aber nicht kostenlos neutralisieren.
|
||||
|
||||
## Ereignisse
|
||||
|
||||
Das System braucht nicht nur passive Werte, sondern Ereignisse.
|
||||
|
||||
### Alltägliche Ereignisse
|
||||
|
||||
- Wunsch nach Geschenk
|
||||
- Wunsch nach besserer Unterkunft
|
||||
- Streit mit Ehepartner
|
||||
- Bitte um öffentliche Anerkennung
|
||||
|
||||
### Risikoereignisse
|
||||
|
||||
- Gerücht am Hof oder in der Stadt
|
||||
- Erpressung durch Diener, Rivalen oder Geistliche
|
||||
- Schwangerschaft oder uneheliches Kind
|
||||
- Duell- oder Ehrenkonflikt
|
||||
- Forderung nach Versorgung eines Kindes
|
||||
|
||||
### Standesereignisse
|
||||
|
||||
- niedrige Stände: Nachbarschaftsgerede, wirtschaftliche Belastung, häuslicher Streit
|
||||
- Bürgerliche: Ratshausgerüchte, Zunftschaden, moralischer Druck
|
||||
- Adel: Hofklatsch, Machtfraktionen, Belastung der Erbfolge, kirchliche Einmischung
|
||||
|
||||
## Spielregeln zur Balance
|
||||
|
||||
Damit das System interessant bleibt und nicht zur reinen Strafe oder zum Gratisbonus wird:
|
||||
|
||||
- maximal eine aktiv unterhaltene Mätresse/Favorit gleichzeitig
|
||||
- mehrere heimliche Liebschaften sind möglich, aber das Skandalrisiko steigt stark
|
||||
- hohe Kosten müssen echte Opportunitätskosten erzeugen
|
||||
- Ansehen darf nicht einfach mit Geld zurückgekauft werden
|
||||
- zu geringe Versorgung verschlechtert Diskretion und Beziehung
|
||||
- eine Beziehung darf keinen simplen Gratisbonus auf Werte geben
|
||||
|
||||
## UI- und UX-Konzept
|
||||
|
||||
Der bestehende Bereich in [FamilyView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/FamilyView.vue) kann direkt ausgebaut werden.
|
||||
|
||||
### Anzeige pro Person
|
||||
|
||||
- Name und Titel
|
||||
- Rolle: heimliche Liebschaft, Geliebte, Mätresse/Favorit
|
||||
- Zuneigung
|
||||
- Bekanntheitsgrad
|
||||
- monatliche Kosten
|
||||
- Standespassung
|
||||
- aktueller Effekt auf Ansehen
|
||||
- aktueller Effekt auf Hausfrieden
|
||||
|
||||
### Aktionen
|
||||
|
||||
- beschenken
|
||||
- besser unterbringen
|
||||
- diskret halten
|
||||
- öffentlich anerkennen
|
||||
- Beziehung beenden
|
||||
- Versorgung reduzieren
|
||||
|
||||
### Hinweise
|
||||
|
||||
- Warnung bei drohendem Skandal
|
||||
- Warnung bei unpassender Standeswahl
|
||||
- Warnung bei zu geringer Versorgung
|
||||
- Hinweis, wenn die Beziehung die Ehe oder den Aufstieg belastet
|
||||
|
||||
## Umsetzungsphasen
|
||||
|
||||
### Phase 1: Grundsystem
|
||||
|
||||
- Beziehungen vom Typ `lover` im Familienbereich sauber anzeigen
|
||||
- Beziehungstypen unterscheiden
|
||||
- monatliche Kosten berechnen
|
||||
- passiven Einfluss auf Ansehen und Hausfrieden einführen
|
||||
|
||||
### Phase 2: Reibung und Entscheidungen
|
||||
|
||||
- Sichtbarkeit und Diskretion einführen
|
||||
- Ereignisse zu Streit, Geschenkforderungen und Gerüchten
|
||||
- Wechselwirkungen mit Ehe und Ansehen
|
||||
|
||||
### Phase 3: Tiefe Systeme
|
||||
|
||||
- uneheliche Kinder
|
||||
- Erpressung und kirchliche Reaktionen
|
||||
- politische oder hofbezogene Nebeneffekte
|
||||
- Standes- und Erbfolgekonflikte
|
||||
|
||||
## Konkrete Empfehlungsregel für Falukant
|
||||
|
||||
Als Startregel für die erste spielbare Version:
|
||||
|
||||
- jede Liebhaber-Beziehung hat laufende Monatskosten
|
||||
- jede Beziehung erzeugt je nach Stand einen passiven Ansehensmodifikator
|
||||
- verheiratete Figuren erhalten zusätzlich Hausfriedensverlust
|
||||
- hohe Stände können eine diskrete, gut unterhaltene Mätresse mit geringem oder neutralem Ansehensmalus führen
|
||||
- niedrige und mittlere Stände tragen bei öffentlicher Affäre deutlich stärkere Nachteile
|
||||
- unzureichende Versorgung erhöht pro Tick Sichtbarkeit, Streit und Skandalrisiko
|
||||
|
||||
Damit entsteht genau das gewünschte Spannungsfeld:
|
||||
|
||||
- romantisch oder politisch nützlich
|
||||
- aber nie kostenlos
|
||||
- gesellschaftlich nie neutral
|
||||
- je nach Stand anders lesbar und anders gefährlich
|
||||
|
||||
## Offene Designentscheidungen
|
||||
|
||||
Vor der technischen Umsetzung sollten noch drei Punkte festgelegt werden:
|
||||
|
||||
1. Soll es einen festen Wert `householdTension` geben oder soll das über bestehende Ehe-/Familienwerte laufen?
|
||||
2. Soll Frömmigkeit direkt mit dem Liebhaber-System gekoppelt werden oder erst in einer späteren Kirchenphase?
|
||||
3. Sollen uneheliche Kinder bereits in Phase 1 möglich sein oder erst ab Phase 3?
|
||||
263
docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md
Normal file
263
docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Falukant: Übergabedokument für den externen Daemon
|
||||
|
||||
## Zweck
|
||||
|
||||
Dieses Dokument ist die technische Übergabe an den externen Daemon, der nicht Teil dieses Projekts ist.
|
||||
|
||||
Es beschreibt:
|
||||
|
||||
- welche Daten der Daemon lesen muss
|
||||
- welche Regeln er anwenden soll
|
||||
- welche Felder er zurückschreiben muss
|
||||
- welche Ereignisse und Nebenwirkungen erwartet werden
|
||||
|
||||
Die fachlichen Regeln selbst stehen in:
|
||||
|
||||
- [FALUKANT_LOVERS_DAEMON_SPEC.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_SPEC.md)
|
||||
|
||||
Die lokale technische Datenbasis dieses Projekts steht in:
|
||||
|
||||
- [FALUKANT_LOVERS_TECHNICAL_CONCEPT.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_TECHNICAL_CONCEPT.md)
|
||||
|
||||
## Architekturgrenze
|
||||
|
||||
Wichtig:
|
||||
|
||||
- dieses Backend hält die Datenstruktur und liefert Family-/UI-Daten
|
||||
- der eigentliche Tick-Lauf für Kosten, Ansehen, Ehezufriedenheit und Kinder passiert im externen Daemon
|
||||
- der externe Daemon ist damit zuständig für die periodische Spiellogik
|
||||
|
||||
Dieses Projekt ist nicht zuständig für:
|
||||
|
||||
- die Scheduler-Ausführung
|
||||
- Tick-Zeitpunkte
|
||||
- operative Daemon-Laufzeit
|
||||
|
||||
## Datenquelle
|
||||
|
||||
Der externe Daemon arbeitet auf folgenden Tabellen:
|
||||
|
||||
- `falukant_data.relationship`
|
||||
- `falukant_data.relationship_state`
|
||||
- `falukant_data.character`
|
||||
- `falukant_data.child_relation`
|
||||
- `falukant_data.falukant_user`
|
||||
- `falukant_type.relationship`
|
||||
- `falukant_type.title`
|
||||
|
||||
Optional später:
|
||||
|
||||
- Notification-Tabellen
|
||||
- Frömmigkeits- oder Kirchen-bezogene Tabellen
|
||||
|
||||
## Mindestdatensatz pro Tick
|
||||
|
||||
Für jede aktive Liebschaft muss der Daemon laden:
|
||||
|
||||
- `relationship.id`
|
||||
- `relationship.character1_id`
|
||||
- `relationship.character2_id`
|
||||
- `relationship_type.tr`
|
||||
- `relationship_state.lover_role`
|
||||
- `relationship_state.affection`
|
||||
- `relationship_state.visibility`
|
||||
- `relationship_state.discretion`
|
||||
- `relationship_state.maintenance_level`
|
||||
- `relationship_state.status_fit`
|
||||
- `relationship_state.monthly_base_cost`
|
||||
- `relationship_state.months_underfunded`
|
||||
- `relationship_state.active`
|
||||
- `relationship_state.acknowledged`
|
||||
- `relationship_state.last_daily_processed_at`
|
||||
- `relationship_state.last_monthly_processed_at`
|
||||
|
||||
Zusätzlich pro beteiligter Figur:
|
||||
|
||||
- `character.id`
|
||||
- `character.user_id`
|
||||
- `character.gender`
|
||||
- `character.birthdate`
|
||||
- `character.reputation`
|
||||
- `character.title_of_nobility`
|
||||
|
||||
Zusätzlich für Geld:
|
||||
|
||||
- `falukant_user.id`
|
||||
- `falukant_user.money`
|
||||
|
||||
Zusätzlich für Ehekontext:
|
||||
|
||||
- aktive Beziehung vom Typ `married`, `engaged` oder `wooing`
|
||||
- `relationship_state.marriage_satisfaction`
|
||||
|
||||
Zusätzlich für Kinderprüfung:
|
||||
|
||||
- bestehende `child_relation` für dieselben Eltern
|
||||
|
||||
## Pflichtlogik Daily Tick
|
||||
|
||||
Der externe Daemon muss täglich:
|
||||
|
||||
1. Sichtbarkeit anpassen
|
||||
2. Diskretion anpassen
|
||||
3. Ehezufriedenheit anpassen
|
||||
4. Ansehen anpassen
|
||||
5. Skandalchance prüfen
|
||||
6. Zustände speichern
|
||||
7. optionale Benachrichtigung oder Log-Einträge erzeugen
|
||||
|
||||
### Daily Input
|
||||
|
||||
- alle aktiven `lover`-Beziehungen
|
||||
- zugehörige Ehebeziehung, falls vorhanden
|
||||
- Standesgruppe
|
||||
- das jüngere Alter der beiden Beteiligten `minAge`
|
||||
|
||||
### Daily Output
|
||||
|
||||
Rückzuschreiben:
|
||||
|
||||
- `relationship_state.visibility`
|
||||
- `relationship_state.discretion`
|
||||
- `relationship_state.marriage_satisfaction` der Ehebeziehung
|
||||
- `character.reputation`
|
||||
- `relationship_state.last_daily_processed_at`
|
||||
|
||||
Optional:
|
||||
|
||||
- Notification
|
||||
- Ereignislog
|
||||
|
||||
## Pflichtlogik Monthly Tick
|
||||
|
||||
Der externe Daemon muss monatlich:
|
||||
|
||||
1. Monatskosten berechnen
|
||||
2. Geld abbuchen
|
||||
3. Unterversorgung behandeln
|
||||
4. Kinderchance prüfen
|
||||
5. ggf. Kind anlegen
|
||||
6. Folgen auf Ansehen und Ehe anwenden
|
||||
7. Zustände speichern
|
||||
|
||||
### Monthly Output
|
||||
|
||||
Rückzuschreiben:
|
||||
|
||||
- `falukant_user.money`
|
||||
- Geldfluss-Log
|
||||
- `relationship_state.months_underfunded`
|
||||
- `relationship_state.affection`
|
||||
- `relationship_state.discretion`
|
||||
- `relationship_state.visibility`
|
||||
- `relationship_state.last_monthly_processed_at`
|
||||
- ggf. `child_relation`
|
||||
- ggf. neuer Kind-Charakter
|
||||
|
||||
## Formeln
|
||||
|
||||
Die verbindlichen Regeln und Formeln kommen aus:
|
||||
|
||||
- [FALUKANT_LOVERS_DAEMON_SPEC.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_SPEC.md)
|
||||
|
||||
Der externe Daemon soll insbesondere exakt übernehmen:
|
||||
|
||||
- Standesgruppen
|
||||
- Monatskostenformel
|
||||
- Unterversorgungsfolgen
|
||||
- Ehezufriedenheitslogik
|
||||
- Reputationslogik
|
||||
- Altersmalus bei zu jungen Liebschaften
|
||||
- Sichtbarkeits- und Diskretionslogik
|
||||
- Skandalchance
|
||||
- Kinderwahrscheinlichkeit
|
||||
|
||||
## Idempotenz
|
||||
|
||||
Der externe Daemon muss idempotent arbeiten.
|
||||
|
||||
Pflicht:
|
||||
|
||||
- Daily Tick nie zweimal für denselben Ingame-Tag auf dieselbe Beziehung anwenden
|
||||
- Monthly Tick nie zweimal für denselben Ingame-Monat auf dieselbe Beziehung anwenden
|
||||
|
||||
Pflichtfelder dafür:
|
||||
|
||||
- `last_daily_processed_at`
|
||||
- `last_monthly_processed_at`
|
||||
|
||||
## Transaktionsanforderungen
|
||||
|
||||
Folgende Monthly-Vorgänge müssen atomar laufen:
|
||||
|
||||
- Geldabbuchung
|
||||
- Statusänderung der Liebschaft
|
||||
- Kind-Erzeugung
|
||||
- Folgeänderung an Ansehen oder Ehe
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- pro verarbeiteter Beziehung eine DB-Transaktion
|
||||
|
||||
## Kind-Erzeugung
|
||||
|
||||
Bei erfolgreicher Monatsprüfung auf Kind:
|
||||
|
||||
1. neues Kind in `falukant_data.character` anlegen
|
||||
2. neue `child_relation` anlegen
|
||||
3. Felder setzen:
|
||||
- `birth_context = lover`
|
||||
- `legitimacy = hidden_bastard`
|
||||
- `public_known = false`
|
||||
|
||||
Wenn der Daemon Kinder nicht selbst anlegen soll, muss er stattdessen ein klar definiertes Create-Event an dieses Backend oder an ein anderes Backend-Modul senden. Standardempfehlung ist aber direkte DB-Erzeugung im Daemon.
|
||||
|
||||
## Gleichbehandlung der Geschlechter
|
||||
|
||||
Der externe Daemon muss dieselben Regeln für männliche und weibliche Spielfiguren anwenden.
|
||||
|
||||
Das betrifft:
|
||||
|
||||
- Kosten
|
||||
- Reputationswirkung
|
||||
- Ehezufriedenheit
|
||||
- Skandalrisiko
|
||||
- Status- und Sichtbarkeitslogik
|
||||
|
||||
Unterschiedlich ist nur die biologische Kinderentstehung im aktuellen Modell.
|
||||
|
||||
## Was dieses Backend dafür bereitstellt
|
||||
|
||||
Dieses Projekt stellt aktuell bereit:
|
||||
|
||||
- Datenstruktur für `relationship_state`
|
||||
- Datenstruktur für `child_relation`-Erweiterungen
|
||||
- Family-API mit lesbaren Zuständen
|
||||
|
||||
Später kann dieses Backend zusätzlich bereitstellen:
|
||||
|
||||
- Komfort-Endpunkte für Lover-Aktionen
|
||||
- Admin-/Debug-Ansichten
|
||||
- eventuelle Helper-Endpoints für den Daemon
|
||||
|
||||
## Erwartete externe Deliverables
|
||||
|
||||
Damit die externe Daemon-Umsetzung vollständig ist, werden dort mindestens benötigt:
|
||||
|
||||
1. Daily-Tick-Job
|
||||
2. Monthly-Tick-Job
|
||||
3. SQL- oder ORM-Zugriff auf die Falukant-Tabellen
|
||||
4. saubere Transaktionslogik
|
||||
5. Schutz gegen doppelte Verarbeitung
|
||||
6. Logging oder Monitoring für Tick-Fehler
|
||||
|
||||
## Definition of Done für die Übergabe
|
||||
|
||||
Die Übergabe an den externen Daemon gilt als vollständig, wenn:
|
||||
|
||||
1. Datenfelder und Tabellen eindeutig definiert sind
|
||||
2. Daily- und Monthly-Inputs beschrieben sind
|
||||
3. Daily- und Monthly-Outputs beschrieben sind
|
||||
4. die verbindliche Fachlogik referenziert ist
|
||||
5. Idempotenz- und Transaktionsanforderungen klar sind
|
||||
6. Kinder aus Liebschaften technisch beschrieben sind
|
||||
775
docs/FALUKANT_LOVERS_DAEMON_SPEC.md
Normal file
775
docs/FALUKANT_LOVERS_DAEMON_SPEC.md
Normal file
@@ -0,0 +1,775 @@
|
||||
# Falukant: Daemon-Spezifikation für Liebhaber, Mätressen, Ehezufriedenheit und uneheliche Kinder
|
||||
|
||||
## Zweck
|
||||
|
||||
Dieses Dokument beschreibt die konkrete Server- und Daemon-Logik für außereheliche Beziehungen im Familiensystem von Falukant. Es ergänzt das Grundkonzept in [FALUKANT_LOVERS_CONCEPT.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_CONCEPT.md) um exakte Regeln für:
|
||||
|
||||
- laufende Kosten
|
||||
- laufende Änderungen von Ansehen
|
||||
- neues System `Ehe-Zufriedenheit`
|
||||
- standesabhängige Wirkung von Ehe und Liebschaften
|
||||
- mögliche Kinder aus Liebschaften
|
||||
- gendergleiche Behandlung im Regelwerk
|
||||
|
||||
Das Dokument ist bewusst daemon-orientiert, also als Grundlage für periodische Verarbeitung im Backend.
|
||||
|
||||
## Bewusst vertagte Themen
|
||||
|
||||
Zwei Themen werden in dieser Spezifikation ausdrücklich nur vorgemerkt und nicht in die erste Umsetzung gezogen:
|
||||
|
||||
### Dienerschaft
|
||||
|
||||
`Dienerschaft` ist ein interessanter späterer Ausbau, weil sie gut zu Diskretion, Repräsentation, Hausstand und Kosten passt. Für die erste Version wird sie jedoch nicht als eigenes System modelliert.
|
||||
|
||||
Für Phase 1 gilt deshalb:
|
||||
|
||||
- Dienerschaft ist nur indirekt in den Unterhaltskosten enthalten
|
||||
- keine eigenen Diener-Slots, Rollen oder Haushaltsobjekte
|
||||
- keine gesonderte Interaktion mit Heimlichkeit oder Hofstatus
|
||||
|
||||
Später kann daraus ein eigenes Hausstands- oder Hofsystem entstehen.
|
||||
|
||||
### Balancing
|
||||
|
||||
Die in diesem Dokument genannten Zahlen sind Regelrahmen für die technische Umsetzung, nicht finale Produktionswerte.
|
||||
|
||||
Für Phase 1 gilt deshalb:
|
||||
|
||||
- Formeln und relative Verhältnisse sind wichtiger als absolute Zahlen
|
||||
- Kosten-, Ansehens- und Zufriedenheitswerte werden später nach realen Spieltests feinjustiert
|
||||
- Balancing ist eine eigene Nachphase und kein Blocker für die erste technische Integration
|
||||
|
||||
## Leitprinzipien
|
||||
|
||||
### 1. Gleichbehandlung der Geschlechter
|
||||
|
||||
Die Regeln für Ansehen, Ehezufriedenheit, Unterhalt, Skandal und soziale Bewertung gelten für männliche und weibliche Spielfiguren gleich.
|
||||
|
||||
Das heißt:
|
||||
|
||||
- dieselbe Beziehungsform erzeugt dieselben Grundkosten
|
||||
- dieselbe Sichtbarkeit erzeugt dieselben Reputationsfolgen
|
||||
- dieselbe Standeslage erzeugt dieselben Modifikatoren
|
||||
- dieselbe Untreue erzeugt dieselbe Wirkung auf die Ehe
|
||||
|
||||
Biologische Unterschiede betreffen nur die Frage, ob aus einer konkreten Paarung natürlich ein Kind entstehen kann. Die soziale und spielmechanische Behandlung bleibt gleich.
|
||||
|
||||
### 2. Stand vor Moral
|
||||
|
||||
Das System bewertet nicht abstrakt „Treue“ oder „Untreue“, sondern:
|
||||
|
||||
- wie geordnet die Situation ist
|
||||
- wie standesgemäß sie geführt wird
|
||||
- wie sichtbar sie ist
|
||||
- ob Ehe, Haus und Erbfolge destabilisiert werden
|
||||
|
||||
### 2a. Zu jung ist reputationsschädlich
|
||||
|
||||
Sehr junge Liebschaften sollen im Daemon nie neutral behandelt werden.
|
||||
|
||||
Das heißt:
|
||||
|
||||
- eine Beziehung kann technisch erlaubt sein
|
||||
- sie kann aber trotzdem das Ansehen zusätzlich belasten
|
||||
- dieser Malus kommt zusätzlich zu Sichtbarkeit, Skandal und Stand
|
||||
- der Altersmalus gilt geschlechtsunabhängig
|
||||
|
||||
### 3. Daemon statt Einmal-Effekt
|
||||
|
||||
Kosten, Ehezufriedenheit, Sichtbarkeit und Ansehen werden nicht nur beim Anlegen oder Beenden einer Beziehung verändert, sondern laufend im Daemon fortgeschrieben.
|
||||
|
||||
## Bestehende Anknüpfungspunkte
|
||||
|
||||
Das System passt auf die vorhandenen Strukturen:
|
||||
|
||||
- Beziehungen werden bereits über `Relationship` geführt in [relationship.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/relationship.js)
|
||||
- Kinder werden bereits über `ChildRelation` geführt in [child_relation.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/child_relation.js)
|
||||
- Ansehen ist bereits auf dem Charakter vorhanden in [character.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/character.js)
|
||||
- Stand ist bereits über `titleOfNobility` abbildbar in [title_of_nobility.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/type/title_of_nobility.js)
|
||||
- `lover` ist bereits ein vorhandener Beziehungstyp
|
||||
|
||||
## Neue Spielwerte
|
||||
|
||||
## A. Pro Spielfigur: Ehezufriedenheit
|
||||
|
||||
Neuer Charakter- oder Ehewert:
|
||||
|
||||
- `marriageSatisfaction`
|
||||
- Wertebereich `0..100`
|
||||
- Standardwert bei frischer Ehe: `55`
|
||||
|
||||
Interpretation:
|
||||
|
||||
- `0..19`: offene Ehekrise
|
||||
- `20..39`: stark belastet
|
||||
- `40..59`: angespannt bis normal
|
||||
- `60..79`: stabil
|
||||
- `80..100`: sehr stabil
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Ehezufriedenheit gehört zur Ehebeziehung, nicht zur Person allein.
|
||||
- Technisch ist dafür eine eigene Beziehungstabelle oder eine Erweiterung der Ehe-`Relationship` sinnvoll.
|
||||
- Für eine erste Version kann der Wert aber auf der Ehebeziehung gespeichert werden.
|
||||
|
||||
## B. Pro Liebhaber-Beziehung
|
||||
|
||||
Für jede Beziehung vom Typ `lover` werden zusätzliche Felder benötigt.
|
||||
|
||||
Pflichtfelder:
|
||||
|
||||
- `loverRole`
|
||||
- `secret_affair`
|
||||
- `lover`
|
||||
- `mistress_or_favorite`
|
||||
- `affection`
|
||||
- `0..100`
|
||||
- `visibility`
|
||||
- `0..100`
|
||||
- `discretion`
|
||||
- `0..100`
|
||||
- `maintenanceLevel`
|
||||
- `0..100`
|
||||
- `statusFit`
|
||||
- `-2..2`
|
||||
- `monthlyBaseCost`
|
||||
- integer
|
||||
- `active`
|
||||
- boolean
|
||||
- `acknowledged`
|
||||
- boolean
|
||||
- `exclusive`
|
||||
- boolean optional
|
||||
|
||||
Abgeleitete, nicht zwingend gespeicherte Werte:
|
||||
|
||||
- `monthlyTotalCost`
|
||||
- `reputationDeltaDaily`
|
||||
- `marriageDeltaDaily`
|
||||
- `scandalRiskDaily`
|
||||
- `pregnancyChanceMonthly`
|
||||
|
||||
## C. Optionaler Kinderwert
|
||||
|
||||
Für Kinder aus Liebschaften wird kein neues Kindmodell benötigt. `ChildRelation` reicht, benötigt aber zusätzlich:
|
||||
|
||||
- `legitimacy`
|
||||
- `legitimate`
|
||||
- `acknowledged_bastard`
|
||||
- `hidden_bastard`
|
||||
- `birthContext`
|
||||
- `marriage`
|
||||
- `lover`
|
||||
- `publicKnown`
|
||||
- boolean
|
||||
|
||||
## Standesgruppen
|
||||
|
||||
Für alle Daemon-Berechnungen werden Adelstitel auf vier Gruppen verdichtet:
|
||||
|
||||
### Gruppe 0: niedrige Stände
|
||||
|
||||
- `noncivil`
|
||||
- `civil`
|
||||
- `sir`
|
||||
|
||||
### Gruppe 1: wohlhabende Bürger und lokale Oberschicht
|
||||
|
||||
- `townlord`
|
||||
- `by`
|
||||
- `landlord`
|
||||
|
||||
### Gruppe 2: niederer und mittlerer Adel
|
||||
|
||||
- `knight`
|
||||
- `baron`
|
||||
- `count`
|
||||
- `palsgrave`
|
||||
- `margrave`
|
||||
- `landgrave`
|
||||
|
||||
### Gruppe 3: hoher Adel und Herrschaft
|
||||
|
||||
- `ruler`
|
||||
- `elector`
|
||||
- `imperial-prince`
|
||||
- `duke`
|
||||
- `grand-duke`
|
||||
- `prince-regent`
|
||||
- `king`
|
||||
|
||||
Diese Gruppen steuern:
|
||||
|
||||
- Toleranz gegenüber sichtbaren Liebschaften
|
||||
- erforderliches Unterhaltsniveau
|
||||
- Wirkung auf Ehezufriedenheit
|
||||
- Strafe bei Skandal
|
||||
|
||||
## Taktung im Daemon
|
||||
|
||||
Empfohlene Verarbeitung:
|
||||
|
||||
- `daily tick`: alle 24 Ingame-Stunden
|
||||
- `monthly tick`: alle 30 Ingame-Tage
|
||||
|
||||
Aufteilung:
|
||||
|
||||
### Daily Tick
|
||||
|
||||
- Sichtbarkeit und Diskretion anpassen
|
||||
- Ehezufriedenheit anpassen
|
||||
- Ansehen anpassen
|
||||
- Skandalrisiko prüfen
|
||||
- Ereignisse auslösen
|
||||
|
||||
### Monthly Tick
|
||||
|
||||
- Unterhaltskosten abbuchen
|
||||
- Beziehungskosten neu berechnen
|
||||
- Kinderchance prüfen
|
||||
- Statuswechsel prüfen
|
||||
|
||||
## Kostenmodell
|
||||
|
||||
## 1. Grundkosten pro Monat
|
||||
|
||||
### secret_affair
|
||||
|
||||
- Basis: `10`
|
||||
|
||||
### lover
|
||||
|
||||
- Basis: `30`
|
||||
|
||||
### mistress_or_favorite
|
||||
|
||||
- Basis: `80`
|
||||
|
||||
Diese Werte sind Ingame-Basiswerte und sollen relativ zu Falukant-Geldwerten noch feinjustiert werden.
|
||||
|
||||
## 2. Standesmultiplikator
|
||||
|
||||
### Gruppe 0
|
||||
|
||||
- `x 1.0`
|
||||
|
||||
### Gruppe 1
|
||||
|
||||
- `x 1.6`
|
||||
|
||||
### Gruppe 2
|
||||
|
||||
- `x 2.6`
|
||||
|
||||
### Gruppe 3
|
||||
|
||||
- `x 4.0`
|
||||
|
||||
Begründung:
|
||||
|
||||
- Höhere Stände können sich die Beziehung leisten.
|
||||
- Gleichzeitig muss sie teurer sein, weil „standesgemäß“ mehr Aufwand verlangt.
|
||||
|
||||
## 3. Unterhaltsfaktor
|
||||
|
||||
`maintenanceFactor = 0.6 + (maintenanceLevel / 100) * 1.2`
|
||||
|
||||
Beispiele:
|
||||
|
||||
- `0` => `0.6`
|
||||
- `50` => `1.2`
|
||||
- `100` => `1.8`
|
||||
|
||||
Niedrige Versorgung spart kurzfristig Geld, erhöht aber später Sichtbarkeit, Unzufriedenheit und Skandalrisiko.
|
||||
|
||||
## 4. Status-Fit-Kosten
|
||||
|
||||
Wenn `statusFit < 0`, steigen Kosten für Diskretion und Konfliktpflege:
|
||||
|
||||
- `statusFit = -1` => `+15 %`
|
||||
- `statusFit = -2` => `+35 %`
|
||||
|
||||
## 5. Monatsformel
|
||||
|
||||
`monthlyTotalCost = round(baseCost * rankMultiplier * maintenanceFactor * statusFitModifier)`
|
||||
|
||||
## 6. Folgen bei Nichtzahlung
|
||||
|
||||
Wenn der Monatsbetrag nicht vollständig gezahlt werden kann:
|
||||
|
||||
- `affection -8`
|
||||
- `discretion -6`
|
||||
- `visibility +8`
|
||||
- `marriageSatisfaction -4` falls verheiratet
|
||||
- `reputation -1` sofort, falls `visibility >= 40`
|
||||
|
||||
Bei zwei aufeinanderfolgenden Monaten Unterversorgung:
|
||||
|
||||
- tägliches Skandalrisiko zusätzlich `+2 %`
|
||||
|
||||
## Ehezufriedenheit: Grundmodell
|
||||
|
||||
## 1. Basistendenz pro Tag
|
||||
|
||||
Jede bestehende Ehe bewegt sich täglich leicht Richtung `55`, wenn keine besonderen Faktoren wirken.
|
||||
|
||||
Formel:
|
||||
|
||||
- wenn `marriageSatisfaction > 55`: `-1` alle 3 Tage
|
||||
- wenn `marriageSatisfaction < 55`: `+1` alle 5 Tage
|
||||
|
||||
So bleiben Ehen nicht dauerhaft extrem, wenn nichts passiert.
|
||||
|
||||
## 2. Modifikatoren durch Liebschaften
|
||||
|
||||
Nur wenn eine aktive Ehe und mindestens eine aktive Liebschaft existiert.
|
||||
|
||||
### Gruppe 0
|
||||
|
||||
- heimliche Liebschaft: `-1` pro Tag
|
||||
- lover: `-2` pro Tag
|
||||
- Mätresse/Favorit: `-3` pro Tag
|
||||
|
||||
### Gruppe 1
|
||||
|
||||
- heimliche Liebschaft: `-1` pro Tag
|
||||
- lover: `-1` pro Tag
|
||||
- Mätresse/Favorit: `-2` pro Tag
|
||||
|
||||
### Gruppe 2
|
||||
|
||||
- heimliche Liebschaft: `0 bis -1` pro Tag
|
||||
- lover: `-1` pro Tag
|
||||
- Mätresse/Favorit: `-1` pro Tag
|
||||
|
||||
### Gruppe 3
|
||||
|
||||
- heimliche Liebschaft: `0`
|
||||
- lover: `0`
|
||||
- Mätresse/Favorit: `+1 / 0 / -1` je nach Ordnung
|
||||
|
||||
Für Gruppe 3 gilt:
|
||||
|
||||
- `+1`, wenn:
|
||||
- `visibility <= 35`
|
||||
- `maintenanceLevel >= 65`
|
||||
- `marriageSatisfaction >= 45`
|
||||
- nur eine aktive Mätresse/Favorit vorhanden
|
||||
- `0`, wenn die Lage geordnet, aber nicht positiv ist
|
||||
- `-1`, wenn die Beziehung sichtbar Unruhe erzeugt
|
||||
|
||||
Das bildet den gewünschten Spezialfall ab:
|
||||
|
||||
- Bei einem König kann eine diskrete, geordnete Nebenbeziehung die Ehe sogar entlasten oder stabilisieren.
|
||||
- Dieselbe Lage kippt ins Negative, wenn sie chaotisch oder öffentlich demütigend wird.
|
||||
|
||||
## 3. Zusätzliche Ehemodifikatoren
|
||||
|
||||
### Positive Faktoren
|
||||
|
||||
- Ehepartner regelmäßig beschenkt: `+1` pro Tag für 5 Tage
|
||||
- großes Fest oder Hochzeitspflege: `+2..+5` einmalig
|
||||
- keine aktive Liebschaft und hohe Versorgung des Hauses: `+1` alle 4 Tage
|
||||
|
||||
### Negative Faktoren
|
||||
|
||||
- sichtbare Liebschaft `visibility >= 60`: `-2` pro Tag
|
||||
- Liebschaft mit `minAge <= 15`: zusätzlich `-1` pro Tag
|
||||
- Kind aus Liebschaft wird bekannt: `-8` einmalig
|
||||
- zwei oder mehr aktive Liebschaften: `-2` pro Tag zusätzlich
|
||||
- Unterhaltsausfall bei Mätresse/Favorit: `-1` pro Tag
|
||||
|
||||
## Ansehen: Grundmodell
|
||||
|
||||
Ansehen wird im Daily Tick pro aktiver Liebschaft angepasst.
|
||||
|
||||
## 1. Basiswert pro Beziehungsform
|
||||
|
||||
### secret_affair
|
||||
|
||||
- `-0.2` pro Tag
|
||||
|
||||
### lover
|
||||
|
||||
- `-0.4` pro Tag
|
||||
|
||||
### mistress_or_favorite
|
||||
|
||||
- `-0.6` pro Tag
|
||||
|
||||
Diese Werte sind Rohwerte vor Modifikatoren.
|
||||
|
||||
## 2. Sichtbarkeitsfaktor
|
||||
|
||||
`visibilityFactor = 0.4 + (visibility / 100) * 1.6`
|
||||
|
||||
Beispiele:
|
||||
|
||||
- `0` => `0.4`
|
||||
- `50` => `1.2`
|
||||
- `100` => `2.0`
|
||||
|
||||
## 3. Standesmodifikator auf Reputationsverlust
|
||||
|
||||
### Gruppe 0
|
||||
|
||||
- `x 1.8`
|
||||
|
||||
### Gruppe 1
|
||||
|
||||
- `x 1.3`
|
||||
|
||||
### Gruppe 2
|
||||
|
||||
- `x 1.0`
|
||||
|
||||
### Gruppe 3
|
||||
|
||||
- `x 0.7`
|
||||
|
||||
Aber:
|
||||
|
||||
- bei Gruppe 3 gilt nur dann `0.7`, wenn die Beziehung geordnet ist
|
||||
- bei Skandal, Erbfolgedruck oder offener Demütigung springt Gruppe 3 auf `1.5`
|
||||
|
||||
## 4. Ordnungsbonus
|
||||
|
||||
Wenn alle Bedingungen erfüllt sind:
|
||||
|
||||
- `maintenanceLevel >= 65`
|
||||
- `discretion >= 60`
|
||||
- `visibility <= 35`
|
||||
- maximal eine aktive Mätresse/Favorit
|
||||
|
||||
dann:
|
||||
|
||||
- Gruppe 2: `+0.1` Ansehen pro Tag statt Malus bei `mistress_or_favorite`
|
||||
- Gruppe 3: `+0.2` Ansehen pro Tag statt Malus bei `mistress_or_favorite`
|
||||
|
||||
Das repräsentiert:
|
||||
|
||||
- geordneten Überfluss
|
||||
- höfische Attraktivität
|
||||
- kontrollierte Nebenbeziehung ohne Hauschaos
|
||||
|
||||
Für `secret_affair` und normalen `lover` gibt es keinen positiven Reputationswert.
|
||||
|
||||
## 5. Tagesformel
|
||||
|
||||
`dailyReputationDelta = baseValue * visibilityFactor * rankModifier`
|
||||
|
||||
Dann:
|
||||
|
||||
- Ordnungsbonus anwenden, falls aktiv
|
||||
- auf `[-3, +1]` pro Tag je Beziehung begrenzen
|
||||
|
||||
## 5a. Altersmalus bei zu jungen Liebschaften
|
||||
|
||||
Zusätzlich zur normalen Reputationsformel wird ein eigener Altersmalus berechnet.
|
||||
|
||||
Grundlage ist immer das jüngere Alter der beiden Beteiligten:
|
||||
|
||||
- `minAge <= 13`: `ageReputationDelta = -1.5` pro Tag
|
||||
- `minAge <= 15`: `ageReputationDelta = -0.8` pro Tag
|
||||
- `minAge <= 17`: `ageReputationDelta = -0.3` pro Tag
|
||||
- `minAge >= 18`: `ageReputationDelta = 0`
|
||||
|
||||
Dann gilt:
|
||||
|
||||
`finalDailyReputationDelta = dailyReputationDelta + ageReputationDelta`
|
||||
|
||||
Zusatzregel:
|
||||
|
||||
- wenn `minAge <= 15` und `visibility >= 50`, zusätzlicher Malus `-0.5` pro Tag
|
||||
|
||||
Damit gilt dann:
|
||||
|
||||
`finalDailyReputationDelta = dailyReputationDelta + ageReputationDelta + visibilityYoungPenalty`
|
||||
|
||||
Interpretation:
|
||||
|
||||
- junge Beziehungen sind schon im privaten Bereich reputationsschädlich
|
||||
- sichtbare junge Beziehungen schaden noch stärker
|
||||
- der Malus kommt zusätzlich zu allen übrigen Skandal- und Sichtbarkeitsfolgen
|
||||
|
||||
## 6. Harte Malus-Ereignisse
|
||||
|
||||
Zusätzliche Einmal-Effekte:
|
||||
|
||||
- öffentliches Gerücht: `-3`
|
||||
- kirchlicher Tadel: `-5`
|
||||
- bekanntes Kind aus Liebschaft: `-6`
|
||||
- Erbfolgestreit durch uneheliches Kind: `-10`
|
||||
- zwei sichtbare Liebschaften gleichzeitig: `-4`
|
||||
|
||||
## Sichtbarkeit und Diskretion
|
||||
|
||||
Diese Werte verändern sich täglich.
|
||||
|
||||
## Sichtbarkeit + pro Tag
|
||||
|
||||
- `+1`, wenn `maintenanceLevel < 35`
|
||||
- `+1`, wenn `affection < 30`
|
||||
- `+2`, wenn `statusFit = -2`
|
||||
- `+1`, wenn bereits ein Ehekonflikt aktiv ist
|
||||
|
||||
## Sichtbarkeit - pro Tag
|
||||
|
||||
- `-1`, wenn `discretion >= 60`
|
||||
- `-1`, wenn `maintenanceLevel >= 70`
|
||||
|
||||
## Diskretion + pro Tag
|
||||
|
||||
- `+1`, wenn `maintenanceLevel >= 70`
|
||||
|
||||
## Diskretion - pro Tag
|
||||
|
||||
- `-1`, wenn `maintenanceLevel < 35`
|
||||
- `-1`, wenn `visibility > 60`
|
||||
|
||||
Beide Werte bleiben in `0..100`.
|
||||
|
||||
## Skandalrisiko
|
||||
|
||||
Tägliche Grundchance:
|
||||
|
||||
`baseScandalChance = 1 %`
|
||||
|
||||
Modifikatoren:
|
||||
|
||||
- `+ visibility / 25`
|
||||
- `+2 %`, wenn verheiratet
|
||||
- `+2 %`, wenn `statusFit = -2`
|
||||
- `+3 %`, wenn `maintenanceLevel < 25`
|
||||
- `+3 %`, wenn zwei oder mehr aktive Liebschaften bestehen
|
||||
- `+6 %`, wenn `minAge <= 13`
|
||||
- `+3 %`, wenn `minAge <= 15`
|
||||
- `+1 %`, wenn `minAge <= 17`
|
||||
- `-2 %`, wenn `discretion >= 75`
|
||||
- `-2 %`, wenn Gruppe 3 und Beziehung geordnet als Mätresse/Favorit geführt wird
|
||||
|
||||
Deckel:
|
||||
|
||||
- Minimum `0 %`
|
||||
- Maximum `25 %` pro Tag
|
||||
|
||||
Mögliche Ereignisse:
|
||||
|
||||
- Gerücht
|
||||
- Ehekrach
|
||||
- Forderung nach höherem Unterhalt
|
||||
- kirchlicher Tadel
|
||||
- Erpressung
|
||||
- Kind wird bekannt
|
||||
|
||||
## Kinder aus Liebschaften
|
||||
|
||||
## 1. Grundsatz
|
||||
|
||||
Kinder aus Liebschaften sind möglich.
|
||||
|
||||
Sie sollen:
|
||||
|
||||
- nicht die Ehe ersetzen
|
||||
- das Familiensystem erweitern
|
||||
- vor allem bei höheren Ständen politische und soziale Reibung erzeugen
|
||||
|
||||
## 2. Technische Bedingung
|
||||
|
||||
Im aktuellen biologischen Modell nur bei gegengeschlechtlicher Paarung.
|
||||
|
||||
Die soziale Behandlung bleibt gleich:
|
||||
|
||||
- weibliche und männliche Spielfiguren erzeugen dieselben Ansehens- und Ehefolgen
|
||||
- das System bewertet nicht unterschiedlich nach Geschlecht
|
||||
|
||||
## 3. Monatschance auf Kind
|
||||
|
||||
Nur wenn:
|
||||
|
||||
- Beziehung aktiv ist
|
||||
- beide Figuren im fruchtbaren Altersbereich sind
|
||||
- `affection >= 45`
|
||||
- `maintenanceLevel >= 30`
|
||||
- kein Sperrstatus aktiv
|
||||
|
||||
Empfohlene Monatswahrscheinlichkeit:
|
||||
|
||||
### secret_affair
|
||||
|
||||
- `2 %`
|
||||
|
||||
### lover
|
||||
|
||||
- `4 %`
|
||||
|
||||
### mistress_or_favorite
|
||||
|
||||
- `6 %`
|
||||
|
||||
Modifikatoren:
|
||||
|
||||
- `+2 %`, wenn `affection >= 75`
|
||||
- `-2 %`, wenn `visibility >= 70` und Beziehung instabil ist
|
||||
- `-3 %`, wenn eine Figur deutlich über den Fruchtbarkeitsgrenzen liegt
|
||||
|
||||
Deckel:
|
||||
|
||||
- Minimum `0 %`
|
||||
- Maximum `12 %`
|
||||
|
||||
## 4. Status des Kindes
|
||||
|
||||
Bei Geburt aus Liebschaft:
|
||||
|
||||
- `birthContext = lover`
|
||||
- `legitimacy = hidden_bastard` standardmäßig
|
||||
|
||||
Wenn die Spielfigur das Kind anerkennt:
|
||||
|
||||
- `legitimacy = acknowledged_bastard`
|
||||
- `publicKnown = true`
|
||||
|
||||
Das Kind darf nicht automatisch Erbe werden.
|
||||
|
||||
Nur explizite spätere Sonderregeln dürfen Erbfolgedruck erzeugen.
|
||||
|
||||
## 5. Folgen eines Kindes aus Liebschaft
|
||||
|
||||
### Sofortfolgen
|
||||
|
||||
- `marriageSatisfaction -8`
|
||||
- `reputation -4`
|
||||
|
||||
### Wenn öffentlich bekannt
|
||||
|
||||
- Gruppe 0: zusätzlich `-4`
|
||||
- Gruppe 1: zusätzlich `-5`
|
||||
- Gruppe 2: zusätzlich `-6`
|
||||
- Gruppe 3: zusätzlich `-8`
|
||||
|
||||
### Wenn Kind anerkannt wird
|
||||
|
||||
- laufende Monatskosten `+20..+80` je Stand
|
||||
- zusätzliche Ereignisse zu Versorgung, Namen und Status
|
||||
|
||||
## Standesabhängigkeit der Ehe
|
||||
|
||||
Die Ehe darf nicht in allen Ständen gleich funktionieren.
|
||||
|
||||
## Gruppe 0
|
||||
|
||||
- Ehe ist vor allem ökonomische Stabilität
|
||||
- Liebschaften belasten den Haushalt direkt
|
||||
- Ehezufriedenheit reagiert stark negativ
|
||||
|
||||
## Gruppe 1
|
||||
|
||||
- Ehe ist Haus- und Rufgemeinschaft
|
||||
- diskrete Affären sind denkbar, aber riskant
|
||||
- sichtbare Liebschaften schaden deutlich
|
||||
|
||||
## Gruppe 2
|
||||
|
||||
- Ehe ist Hauspolitik und Nachfolgeordnung
|
||||
- Mätressen können vorkommen, aber nur kontrolliert
|
||||
- Ehezufriedenheit reagiert weniger moralisch, stärker auf öffentliche Ordnung
|
||||
|
||||
## Gruppe 3
|
||||
|
||||
- Ehe ist Dynastie, Bündnis und Hofordnung
|
||||
- eine diskrete Mätresse/Favorit kann den ehelichen Druck senken, solange:
|
||||
- die offizielle Ehe respektiert bleibt
|
||||
- keine Erbfolge bedroht wird
|
||||
- kein öffentlicher Gesichtsverlust entsteht
|
||||
|
||||
Deshalb ist bei hohen Ständen ein positiver Effekt auf die Ehe ausdrücklich zulässig, aber nur in geordneten Fällen.
|
||||
|
||||
## Empfohlene Umsetzung im Daemon
|
||||
|
||||
Reihenfolge pro Daily Tick:
|
||||
|
||||
1. aktive Ehen laden
|
||||
2. aktive Liebschaften laden
|
||||
3. Sichtbarkeit und Diskretion fortschreiben
|
||||
4. Ehezufriedenheit pro Ehe fortschreiben
|
||||
5. Ansehen pro Charakter fortschreiben
|
||||
6. Skandalereignisse prüfen
|
||||
7. Benachrichtigungen schreiben
|
||||
|
||||
Reihenfolge pro Monthly Tick:
|
||||
|
||||
1. Monatskosten je Liebschaft berechnen
|
||||
2. Geld abbuchen
|
||||
3. Unterversorgungsfolgen anwenden
|
||||
4. Kinderchance prüfen
|
||||
5. neue Kinder aus Liebschaften anlegen
|
||||
6. Folgeereignisse erzeugen
|
||||
|
||||
## Minimale technische Erweiterungen
|
||||
|
||||
Für eine erste umsetzbare Version werden mindestens benötigt:
|
||||
|
||||
### Beziehungserweiterung
|
||||
|
||||
- Zusatzfelder an `relationship` oder neue Nebentabelle `relationship_state`
|
||||
|
||||
### Ehewert
|
||||
|
||||
- `marriage_satisfaction` an Ehebeziehung
|
||||
|
||||
### Kind-Zusatzfelder
|
||||
|
||||
- `legitimacy`
|
||||
- `birth_context`
|
||||
- `public_known`
|
||||
|
||||
### Daemon-Konfiguration
|
||||
|
||||
- täglicher Falukant-Familienjob
|
||||
- monatlicher Falukant-Familienjob
|
||||
|
||||
## MVP-Empfehlung
|
||||
|
||||
Für die erste produktive Version empfehle ich diesen Schnitt:
|
||||
|
||||
### Enthalten
|
||||
|
||||
- aktive Liebschaften
|
||||
- Monatskosten
|
||||
- tägliche Ansehensänderung
|
||||
- Ehezufriedenheit
|
||||
- standesabhängige Unterschiede
|
||||
- Kinderchance aus Liebschaften
|
||||
- sichtbare Darstellung in FamilyView
|
||||
|
||||
### Noch nicht im MVP
|
||||
|
||||
- Erpressungsketten
|
||||
- kirchliche Sonderereignisse
|
||||
- gerichtliche Konflikte
|
||||
- Sonderrechte unehelicher Kinder
|
||||
- komplexe Hofintrigen
|
||||
- Dienerschaft als eigenes System
|
||||
- finales Balancing aller Zahlenwerte
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
Die erste technische Umsetzung gilt als korrekt, wenn:
|
||||
|
||||
1. jede aktive Liebschaft monatlich Kosten erzeugt
|
||||
2. jede aktive Liebschaft täglich das Ansehen verändert
|
||||
3. verheiratete Figuren täglich Ehezufriedenheit verändern
|
||||
4. hohe Stände bei geordneter Mätresse/Favorit einen neutralen oder leicht positiven Eheeffekt haben können
|
||||
5. sichtbare oder schlecht versorgte Liebschaften zu Ansehensverlust führen
|
||||
6. Kinder aus Liebschaften entstehen können
|
||||
7. weibliche und männliche Spielfiguren regelgleich behandelt werden
|
||||
|
||||
## Offene Implementierungsfrage
|
||||
|
||||
Vor dem Coden sollte noch genau entschieden werden:
|
||||
|
||||
- ob die Zusatzwerte direkt in `relationship` landen
|
||||
- oder in eine neue Tabelle wie `falukant_data.relationship_state`
|
||||
|
||||
Fachlich ist beides möglich. Für Wartbarkeit und spätere Ereignisse ist eine eigene Zustands-/Detailtabelle sauberer.
|
||||
478
docs/FALUKANT_LOVERS_IMPLEMENTATION_BACKLOG.md
Normal file
478
docs/FALUKANT_LOVERS_IMPLEMENTATION_BACKLOG.md
Normal file
@@ -0,0 +1,478 @@
|
||||
# Falukant: Implementierungs-Backlog für Liebhaber, Mätressen, Ehezufriedenheit und Kinder aus Liebschaften
|
||||
|
||||
## Zweck
|
||||
|
||||
Dieses Backlog übersetzt die Fach- und Technikdokumente in konkrete Umsetzungspakete.
|
||||
|
||||
Grundlagen:
|
||||
|
||||
- [FALUKANT_LOVERS_CONCEPT.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_CONCEPT.md)
|
||||
- [FALUKANT_LOVERS_DAEMON_SPEC.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_SPEC.md)
|
||||
- [FALUKANT_LOVERS_TECHNICAL_CONCEPT.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_TECHNICAL_CONCEPT.md)
|
||||
|
||||
Das Backlog ist absichtlich in Reihenfolge angeordnet. Spätere Pakete bauen auf früheren auf.
|
||||
|
||||
## Rahmen
|
||||
|
||||
Nicht Teil der ersten Umsetzung:
|
||||
|
||||
- eigenes Dienerschaftssystem
|
||||
- finales Balancing
|
||||
- große Ereignisketten rund um Kirche, Gericht oder Hofintrigen
|
||||
|
||||
## Paket B1: Datenmodell vorbereiten
|
||||
|
||||
### Ziel
|
||||
|
||||
Die Datenbasis für Ehezufriedenheit, Liebschaftsstatus und Kinder aus Liebschaften anlegen.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Migration für `falukant_data.relationship_state` anlegen
|
||||
2. Modell [relationship_state.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/relationship_state.js) anlegen
|
||||
3. Associations in [associations.js](/mnt/share/torsten/Programs/YourPart3/backend/models/associations.js) ergänzen
|
||||
4. `child_relation` um `legitimacy`, `birth_context`, `public_known` erweitern
|
||||
5. Modell [child_relation.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/child_relation.js) anpassen
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [backend/models/associations.js](/mnt/share/torsten/Programs/YourPart3/backend/models/associations.js)
|
||||
- [backend/models/falukant/data/child_relation.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/child_relation.js)
|
||||
- neue Migrationen in [backend/migrations](/mnt/share/torsten/Programs/YourPart3/backend/migrations)
|
||||
- neue Datei [backend/models/falukant/data/relationship_state.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/relationship_state.js)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- keine
|
||||
|
||||
### Done
|
||||
|
||||
- Datenbank kann die neuen Felder speichern
|
||||
- Sequelize kann `Relationship` plus `state` laden
|
||||
- `ChildRelation` kennt neue Legitimitätsfelder
|
||||
|
||||
## Paket B2: Backfill und Defaults
|
||||
|
||||
### Ziel
|
||||
|
||||
Bestehende Ehen und Liebschaften mit Startwerten versorgen.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Backfill-Migration oder Reparaturskript für bestehende `married`-Beziehungen
|
||||
2. Backfill-Migration oder Reparaturskript für bestehende `lover`-Beziehungen
|
||||
3. Fallback-Logik im Backend ergänzen, falls für alte Datensätze noch kein State existiert
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- neue Migration oder Tool in [backend/migrations](/mnt/share/torsten/Programs/YourPart3/backend/migrations) oder [backend/tools](/mnt/share/torsten/Programs/YourPart3/backend/tools)
|
||||
- [backend/services/falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B1
|
||||
|
||||
### Done
|
||||
|
||||
- alle alten `married`- und `lover`-Beziehungen haben nutzbare Zustandswerte
|
||||
- Family-Lesezugriffe brechen nicht bei fehlendem State
|
||||
|
||||
## Paket B3: Family-Lesepfade erweitern
|
||||
|
||||
### Ziel
|
||||
|
||||
Die bestehenden API-Daten für Familie so erweitern, dass das Frontend sofort lesen und anzeigen kann.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. `getFamily()` in [falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js) um `state`-Daten erweitern
|
||||
2. für Ehebeziehungen `marriageSatisfaction` und `marriageState` liefern
|
||||
3. für `lovers` Rollen-, Kosten-, Sichtbarkeits- und Risikofelder liefern
|
||||
4. Hilfsmethoden für Standesgruppe und Vorschauwerte ergänzen
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [backend/services/falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B1
|
||||
- B2
|
||||
|
||||
### Done
|
||||
|
||||
- `GET /api/falukant/family` liefert die neuen Datenfelder
|
||||
- keine UI-Aktion nötig, aber Daten sind vollständig lesbar
|
||||
|
||||
## Paket B4: Family-UI lesend ausbauen
|
||||
|
||||
### Ziel
|
||||
|
||||
Die neuen Daten im Familienbereich sichtbar machen, ohne schon alle Interaktionen einzubauen.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Ehebereich in [FamilyView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/FamilyView.vue) um `Ehe-Zufriedenheit` ergänzen
|
||||
2. `lovers`-Bereich mit Rolle, Sichtbarkeit, Diskretion, Unterhalt, Reputationseffekt und Eheeffekt erweitern
|
||||
3. Kinderkennzeichnung für `legitimate`, `hidden_bastard`, `acknowledged_bastard` ergänzen
|
||||
4. I18n-Schlüssel in den Falukant-Locales ergänzen
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [frontend/src/views/falukant/FamilyView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/FamilyView.vue)
|
||||
- [frontend/src/i18n/locales/de/falukant.json](/mnt/share/torsten/Programs/YourPart3/frontend/src/i18n/locales/de/falukant.json)
|
||||
- [frontend/src/i18n/locales/en/falukant.json](/mnt/share/torsten/Programs/YourPart3/frontend/src/i18n/locales/en/falukant.json)
|
||||
- [frontend/src/i18n/locales/es/falukant.json](/mnt/share/torsten/Programs/YourPart3/frontend/src/i18n/locales/es/falukant.json)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B3
|
||||
|
||||
### Done
|
||||
|
||||
- FamilyView zeigt neue Zustände lesbar an
|
||||
- uneheliche Kinder sind UI-seitig unterscheidbar
|
||||
|
||||
## Paket B5: Berechnungslogik im Service kapseln
|
||||
|
||||
### Ziel
|
||||
|
||||
Alle Formeln in wiederverwendbare Backend-Helfer auslagern, bevor Daemon-Jobs gebaut werden.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. `getRankGroup(...)` implementieren
|
||||
2. `calculateLoverMonthlyCost(...)` implementieren
|
||||
3. `calculateMarriageDelta(...)` implementieren
|
||||
4. `calculateReputationDeltaFromLover(...)` implementieren
|
||||
5. `calculateDailyVisibilityDelta(...)` und `calculateDailyDiscretionDelta(...)` implementieren
|
||||
6. `calculateDailyScandalChance(...)` implementieren
|
||||
7. `calculateMonthlyPregnancyChance(...)` implementieren
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [backend/services/falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B1
|
||||
- B2
|
||||
|
||||
### Done
|
||||
|
||||
- Daemon-Jobs können auf zentrale Helper zugreifen
|
||||
- keine Formel liegt verstreut in mehreren Jobs
|
||||
|
||||
## Paket B6: Daily-Tick-Übergabe an externen Daemon
|
||||
|
||||
### Ziel
|
||||
|
||||
Die tägliche Spiellogik so spezifizieren und übergeben, dass der externe Daemon sie korrekt ausführen kann.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Übergabedokument für den externen Daemon erstellen
|
||||
2. Daily Input- und Output-Felder festlegen
|
||||
3. Idempotenzanforderungen für `last_daily_processed_at` festlegen
|
||||
4. Datenabhängigkeiten für Ehe, Liebschaften und Stand definieren
|
||||
5. Benachrichtigungs- und Ereignisfolgen beschreiben
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md)
|
||||
- [docs/FALUKANT_LOVERS_DAEMON_SPEC.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_SPEC.md)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B5
|
||||
|
||||
### Done
|
||||
|
||||
- der externe Daemon hat eine vollständige Daily-Tick-Übergabe
|
||||
- Daily-Logik ist ohne Rückfragen implementierbar
|
||||
|
||||
## Paket B7: Monthly-Tick-Übergabe an externen Daemon
|
||||
|
||||
### Ziel
|
||||
|
||||
Die monatliche Spiellogik so spezifizieren und übergeben, dass der externe Daemon sie korrekt ausführen kann.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Monthly Input- und Output-Felder festlegen
|
||||
2. Geldabbuchung und Moneyflow-Anforderungen beschreiben
|
||||
3. Unterversorgung und Zustandsänderungen beschreiben
|
||||
4. Kind-Erzeugung und Folgeeffekte beschreiben
|
||||
5. Idempotenzanforderungen für `last_monthly_processed_at` festlegen
|
||||
6. Transaktionsanforderungen definieren
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md)
|
||||
- [docs/FALUKANT_LOVERS_DAEMON_SPEC.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_SPEC.md)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B5
|
||||
|
||||
### Done
|
||||
|
||||
- der externe Daemon hat eine vollständige Monthly-Tick-Übergabe
|
||||
- Monatslogik ist ohne Rückfragen implementierbar
|
||||
|
||||
## Paket B8: Kinder aus Liebschaften technisch ermöglichen
|
||||
|
||||
### Ziel
|
||||
|
||||
Kinder aus aktiven Liebschaften erzeugen und korrekt markieren.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. `createChildFromLoverRelationship(...)` implementieren
|
||||
2. `processLoverBirths(...)` in den Monthly Tick integrieren
|
||||
3. `ChildRelation` korrekt mit `birthContext = lover` anlegen
|
||||
4. `legitimacy = hidden_bastard` als Startwert setzen
|
||||
5. erste Folgeeffekte auf Ansehen und Ehezufriedenheit anwenden
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [backend/services/falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js)
|
||||
- [backend/models/falukant/data/child_relation.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/child_relation.js)
|
||||
- [docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B7
|
||||
|
||||
### Done
|
||||
|
||||
- Kinder aus Liebschaften können entstehen
|
||||
- sie sind von legitimen Kindern technisch unterscheidbar
|
||||
|
||||
## Paket B9: Notifications und Folgeereignisse MVP
|
||||
|
||||
### Ziel
|
||||
|
||||
Die wichtigsten Ergebnisse für Spieler sichtbar machen.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Notifikationstypen für Kosten, Unterversorgung, Gerücht, Skandal und Kind ergänzen
|
||||
2. Benachrichtigungstexte definieren
|
||||
3. Daily- und Monthly-Tick an die Notification-Logik anbinden
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md)
|
||||
- bestehende Notification-Modelle oder Services im Backend
|
||||
- ggf. [frontend/src/views/falukant/OverviewView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/OverviewView.vue) indirekt, falls Benachrichtigungen dort auftauchen
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B6
|
||||
- B7
|
||||
- B8
|
||||
|
||||
### Done
|
||||
|
||||
- Spieler sehen relevante Familienfolgen aktiv
|
||||
|
||||
## Paket B10: Lover-Aktionen im Backend
|
||||
|
||||
### Ziel
|
||||
|
||||
Interaktive Steuerung von Liebschaften serverseitig ermöglichen.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. `setLoverMaintenance(...)`
|
||||
2. `setLoverDiscretionMode(...)`
|
||||
3. `acknowledgeLover(...)`
|
||||
4. `endLoverRelationship(...)`
|
||||
5. `giftLover(...)`
|
||||
6. Router- und Controller-Anbindung
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [backend/services/falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js)
|
||||
- [backend/controllers/falukantController.js](/mnt/share/torsten/Programs/YourPart3/backend/controllers/falukantController.js)
|
||||
- [backend/routers/falukantRouter.js](/mnt/share/torsten/Programs/YourPart3/backend/routers/falukantRouter.js)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B3
|
||||
- B5
|
||||
|
||||
### Done
|
||||
|
||||
- Backend bietet alle Kernaktionen für Lovers an
|
||||
|
||||
## Paket B11: Lover-Aktionen im Frontend
|
||||
|
||||
### Ziel
|
||||
|
||||
Die neuen Interaktionen in `FamilyView` und ggf. Dialogen bedienbar machen.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Action-Buttons in [FamilyView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/FamilyView.vue) ergänzen
|
||||
2. API-Aufrufe anbinden
|
||||
3. Feedback- und Confirm-Dialoge integrieren
|
||||
4. Zustandsänderungen direkt im UI sichtbar machen
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [frontend/src/views/falukant/FamilyView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/FamilyView.vue)
|
||||
- ggf. neue API-Helfer in `frontend/src/api`
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B10
|
||||
|
||||
### Done
|
||||
|
||||
- Unterhalt, Anerkennung, Diskretion und Beenden sind im UI nutzbar
|
||||
|
||||
## Paket B12: Anerkennung unehelicher Kinder
|
||||
|
||||
### Ziel
|
||||
|
||||
Uneheliche Kinder später sichtbar anerkennen können.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Backend-Methode `acknowledgeLoverChild(...)`
|
||||
2. Route und Controller
|
||||
3. UI-Aktion im Familienbereich
|
||||
4. direkte Folgeeffekte auf Ansehen und Ehe einbauen
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [backend/services/falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js)
|
||||
- [backend/controllers/falukantController.js](/mnt/share/torsten/Programs/YourPart3/backend/controllers/falukantController.js)
|
||||
- [backend/routers/falukantRouter.js](/mnt/share/torsten/Programs/YourPart3/backend/routers/falukantRouter.js)
|
||||
- [frontend/src/views/falukant/FamilyView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/FamilyView.vue)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B8
|
||||
- B11
|
||||
|
||||
### Done
|
||||
|
||||
- uneheliche Kinder können anerkannt werden
|
||||
- Status und Folgen ändern sich sichtbar
|
||||
|
||||
## Paket B13: Admin- und Testhilfen
|
||||
|
||||
### Ziel
|
||||
|
||||
Die neue Mechanik testbar und debugbar machen.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Admin- oder Tool-Zugriff auf `relationship_state`
|
||||
2. Debug-Skript für `30 Tage simulieren`
|
||||
3. Plausibilitätsprüfungen für fehlende States
|
||||
4. Reparaturskript für inkonsistente Kinderdaten
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [backend/tools](/mnt/share/torsten/Programs/YourPart3/backend/tools)
|
||||
- ggf. [backend/services/adminService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/adminService.js)
|
||||
- [docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md)
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B6
|
||||
- B7
|
||||
- B8
|
||||
|
||||
### Done
|
||||
|
||||
- Entwickler können Systemzustände nachvollziehen und korrigieren
|
||||
|
||||
## Paket B14: QA und Balancing-Vorbereitung
|
||||
|
||||
### Ziel
|
||||
|
||||
Noch kein finales Balancing, aber die technische Basis für spätere Feinjustierung schaffen.
|
||||
|
||||
### Aufgaben
|
||||
|
||||
1. Konfigurationspunkte für Kosten- und Reputationswerte zentralisieren
|
||||
2. Grundtests für Daily- und Monthly-Tick definieren
|
||||
3. Testfälle für Standesgruppen definieren
|
||||
4. Testfälle für weibliche und männliche Spielfiguren spiegeln
|
||||
5. Testfälle für Kinder aus Liebschaften definieren
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- [docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md)
|
||||
- ggf. Testverzeichnis im Backend
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- B6
|
||||
- B7
|
||||
- B8
|
||||
|
||||
### Done
|
||||
|
||||
- Werte sind zentral auffindbar
|
||||
- spätere Balancing-Runden können auf Testfällen aufsetzen
|
||||
|
||||
## Empfohlene Reihenfolge
|
||||
|
||||
Für eine saubere erste Lieferung:
|
||||
|
||||
1. B1
|
||||
2. B2
|
||||
3. B3
|
||||
4. B4
|
||||
5. B5
|
||||
6. B6
|
||||
7. B7
|
||||
8. B8
|
||||
9. B9
|
||||
10. B10
|
||||
11. B11
|
||||
12. B12
|
||||
13. B13
|
||||
14. B14
|
||||
|
||||
## MVP-Schnitt
|
||||
|
||||
Wenn eine erste spielbare Version schneller geliefert werden soll, reicht zunächst:
|
||||
|
||||
1. B1
|
||||
2. B2
|
||||
3. B3
|
||||
4. B4
|
||||
5. B5
|
||||
6. B6
|
||||
7. B7
|
||||
8. B8
|
||||
|
||||
Damit wären bereits vorhanden:
|
||||
|
||||
- sichtbare Liebhaber-Details
|
||||
- Ehezufriedenheit
|
||||
- laufende Kosten
|
||||
- laufende Ansehensänderung
|
||||
- Kinder aus Liebschaften
|
||||
|
||||
Noch nicht enthalten im MVP:
|
||||
|
||||
- volle Interaktionssteuerung
|
||||
- Anerkennung unehelicher Kinder
|
||||
- Admin-Tools
|
||||
- spätere Balancing-Infrastruktur
|
||||
|
||||
## Nächster konkreter Schritt
|
||||
|
||||
Wenn direkt implementiert werden soll, ist der erste technische Einstieg:
|
||||
|
||||
- B1 Datenmodell vorbereiten
|
||||
|
||||
Das ist der sauberste Startpunkt, weil danach alle weiteren Pakete darauf aufbauen können.
|
||||
503
docs/FALUKANT_LOVERS_TECHNICAL_CONCEPT.md
Normal file
503
docs/FALUKANT_LOVERS_TECHNICAL_CONCEPT.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# Falukant: Technisches Konzept für Liebhaber, Mätressen, Ehezufriedenheit und Kinder aus Liebschaften
|
||||
|
||||
## Ziel
|
||||
|
||||
Dieses Dokument beschreibt die technische Umsetzung für Backend, Daemon und UI. Es basiert auf:
|
||||
|
||||
- [FALUKANT_LOVERS_CONCEPT.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_CONCEPT.md)
|
||||
- [FALUKANT_LOVERS_DAEMON_SPEC.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_SPEC.md)
|
||||
|
||||
Es soll als direkte Arbeitsgrundlage für Migrationen, Modelle, Service-Methoden, Daemon-Jobs und Frontend-Anpassungen dienen.
|
||||
|
||||
## Umsetzungsstrategie
|
||||
|
||||
Die Umsetzung sollte in drei technischen Stufen erfolgen:
|
||||
|
||||
### Stufe 1: Datenbasis und Lesepfade
|
||||
|
||||
- neue Beziehungsdetaildaten anlegen
|
||||
- Ehezufriedenheit technisch einführen
|
||||
- Family-API erweitern
|
||||
- UI nur lesend erweitern
|
||||
|
||||
### Stufe 2: Externe Daemon-Logik
|
||||
|
||||
- tägliche und monatliche Falukant-Familienlogik an den externen Daemon übergeben
|
||||
- Kosten, Ansehen, Sichtbarkeit, Diskretion und Ehezufriedenheit laufen dort automatisch
|
||||
- Kinder aus Liebschaften werden über den externen Daemon ermöglicht
|
||||
|
||||
### Stufe 3: Interaktive Spielmechanik
|
||||
|
||||
- UI-Aktionen für Unterhalt, Diskretion, Anerkennung, Beenden
|
||||
- Ereignisse, Warnungen und Benachrichtigungen
|
||||
- spätere Vertiefung wie Skandal- oder Kirchenereignisse
|
||||
|
||||
## Datenmodell
|
||||
|
||||
## 1. Bestehende Tabellen, die genutzt werden
|
||||
|
||||
- `falukant_data.relationship`
|
||||
- `falukant_data.child_relation`
|
||||
- `falukant_data.character`
|
||||
|
||||
## 2. Empfohlene neue Tabelle
|
||||
|
||||
Empfohlen wird eine neue Detailtabelle:
|
||||
|
||||
- `falukant_data.relationship_state`
|
||||
|
||||
Begründung:
|
||||
|
||||
- `relationship` enthält aktuell nur die grobe Beziehung
|
||||
- Liebhaber-/Ehedaten sind zustandsorientiert
|
||||
- die Detailwerte wachsen voraussichtlich weiter
|
||||
- eine Nebentabelle ist sauberer als `relationship` mit vielen Spezialspalten zu überladen
|
||||
|
||||
## 3. Tabelle `relationship_state`
|
||||
|
||||
### Primärbezug
|
||||
|
||||
- `relationship_id`
|
||||
|
||||
### Spalten für Ehe
|
||||
|
||||
- `marriage_satisfaction` integer not null default `55`
|
||||
- `marriage_public_stability` integer not null default `55`
|
||||
|
||||
`marriage_public_stability` ist optional, aber sinnvoll für spätere Ereignisse. Für MVP kann er schon angelegt, aber noch wenig genutzt werden.
|
||||
|
||||
### Spalten für Liebschaften
|
||||
|
||||
- `lover_role` string nullable
|
||||
- `secret_affair`
|
||||
- `lover`
|
||||
- `mistress_or_favorite`
|
||||
- `affection` integer not null default `50`
|
||||
- `visibility` integer not null default `15`
|
||||
- `discretion` integer not null default `50`
|
||||
- `maintenance_level` integer not null default `50`
|
||||
- `status_fit` integer not null default `0`
|
||||
- `monthly_base_cost` integer not null default `0`
|
||||
- `months_underfunded` integer not null default `0`
|
||||
- `active` boolean not null default `true`
|
||||
- `acknowledged` boolean not null default `false`
|
||||
- `exclusive_flag` boolean not null default `false`
|
||||
- `last_monthly_processed_at` date nullable
|
||||
- `last_daily_processed_at` date nullable
|
||||
|
||||
### Spalten für spätere Erweiterung
|
||||
|
||||
- `notes_json` jsonb nullable
|
||||
- `flags_json` jsonb nullable
|
||||
|
||||
## 4. Erweiterung `child_relation`
|
||||
|
||||
Neue Spalten:
|
||||
|
||||
- `legitimacy` string not null default `legitimate`
|
||||
- `legitimate`
|
||||
- `acknowledged_bastard`
|
||||
- `hidden_bastard`
|
||||
- `birth_context` string not null default `marriage`
|
||||
- `marriage`
|
||||
- `lover`
|
||||
- `public_known` boolean not null default `false`
|
||||
|
||||
## 5. Optionale Erweiterung `relationship`
|
||||
|
||||
Für bessere Auswertung kann zusätzlich sinnvoll sein:
|
||||
|
||||
- `ended_at`
|
||||
- `ended_reason`
|
||||
|
||||
Das ist für MVP nicht zwingend, aber nützlich.
|
||||
|
||||
## Migrationen
|
||||
|
||||
Benötigte Migrationen:
|
||||
|
||||
### Migration 1
|
||||
|
||||
- neue Tabelle `falukant_data.relationship_state`
|
||||
|
||||
### Migration 2
|
||||
|
||||
- neue Spalten an `falukant_data.child_relation`
|
||||
|
||||
### Migration 3 optional
|
||||
|
||||
- Backfill für bestehende Beziehungen
|
||||
|
||||
Regeln für Backfill:
|
||||
|
||||
- bei `relationshipType = married`
|
||||
- `marriage_satisfaction = 55`
|
||||
- bei `relationshipType = lover`
|
||||
- `lover_role = lover`
|
||||
- `affection = 50`
|
||||
- `visibility = 20`
|
||||
- `discretion = 45`
|
||||
- `maintenance_level = 50`
|
||||
- `status_fit = 0`
|
||||
- `monthly_base_cost = 30`
|
||||
|
||||
## Sequelize-Modelle
|
||||
|
||||
## 1. Neues Modell
|
||||
|
||||
Neue Datei:
|
||||
|
||||
- [relationship_state.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/relationship_state.js)
|
||||
|
||||
## 2. Associations
|
||||
|
||||
In [associations.js](/mnt/share/torsten/Programs/YourPart3/backend/models/associations.js):
|
||||
|
||||
- `Relationship.hasOne(RelationshipState, { foreignKey: 'relationshipId', as: 'state' })`
|
||||
- `RelationshipState.belongsTo(Relationship, { foreignKey: 'relationshipId', as: 'relationship' })`
|
||||
|
||||
## 3. ChildRelation-Erweiterung
|
||||
|
||||
In [child_relation.js](/mnt/share/torsten/Programs/YourPart3/backend/models/falukant/data/child_relation.js):
|
||||
|
||||
- `legitimacy`
|
||||
- `birthContext`
|
||||
- `publicKnown`
|
||||
|
||||
## Backend-Service-Konzept
|
||||
|
||||
Hauptort:
|
||||
|
||||
- [falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js)
|
||||
|
||||
## 1. Neue interne Hilfsmethoden
|
||||
|
||||
Empfohlene neue interne Methoden:
|
||||
|
||||
- `getRankGroup(titleLabelTr)`
|
||||
- `calculateLoverMonthlyCost(relationship, state, character)`
|
||||
- `calculateMarriageDelta(relationship, state, character, spouseCharacter, context)`
|
||||
- `calculateReputationDeltaFromLover(relationship, state, character, context)`
|
||||
- `calculateDailyVisibilityDelta(state, context)`
|
||||
- `calculateDailyDiscretionDelta(state, context)`
|
||||
- `calculateDailyScandalChance(relationship, state, character, context)`
|
||||
- `calculateMonthlyPregnancyChance(relationship, state, charA, charB)`
|
||||
- `applyLoverMonthlyCosts(transactionDate?)`
|
||||
- `applyLoverDailyEffects(transactionDate?)`
|
||||
- `processLoverBirths(transactionDate?)`
|
||||
- `triggerLoverScandalEvent(...)`
|
||||
|
||||
## 2. Erweiterung `getFamily`
|
||||
|
||||
Die Familienausgabe in [falukantService.js](/mnt/share/torsten/Programs/YourPart3/backend/services/falukantService.js) muss `lovers` detaillierter liefern.
|
||||
|
||||
Zusätzliche API-Felder je Lover:
|
||||
|
||||
- `relationshipId`
|
||||
- `role`
|
||||
- `affection`
|
||||
- `visibility`
|
||||
- `discretion`
|
||||
- `maintenanceLevel`
|
||||
- `statusFit`
|
||||
- `monthlyCost`
|
||||
- `reputationEffect`
|
||||
- `marriageEffect`
|
||||
- `acknowledged`
|
||||
- `canBecomePublic`
|
||||
- `riskState`
|
||||
|
||||
Zusätzliche API-Felder für Ehe:
|
||||
|
||||
- `marriageSatisfaction`
|
||||
- `marriageState`
|
||||
- `stable`
|
||||
- `strained`
|
||||
- `crisis`
|
||||
|
||||
## 3. Neue Service-Aktionen
|
||||
|
||||
Für spätere UI-Steuerung:
|
||||
|
||||
- `setLoverMaintenance(hashedUserId, relationshipId, maintenanceLevel)`
|
||||
- `setLoverDiscretionMode(hashedUserId, relationshipId, mode)`
|
||||
- `acknowledgeLover(hashedUserId, relationshipId)`
|
||||
- `endLoverRelationship(hashedUserId, relationshipId)`
|
||||
- `giftLover(hashedUserId, relationshipId, giftType)`
|
||||
|
||||
Diese Methoden müssen in Stufe 1 noch nicht voll sichtbar sein, sollten aber im Konzept vorgesehen werden.
|
||||
|
||||
## API-Konzept
|
||||
|
||||
## 1. Bestehende Family-Route erweitern
|
||||
|
||||
Bestehender Endpunkt:
|
||||
|
||||
- `GET /api/falukant/family`
|
||||
|
||||
Erweitern um:
|
||||
|
||||
- `marriageSatisfaction`
|
||||
- `lovers` mit Detailfeldern
|
||||
- `householdTension` optional
|
||||
|
||||
## 2. Neue Endpunkte
|
||||
|
||||
Empfohlene neue Endpunkte:
|
||||
|
||||
- `POST /api/falukant/family/lover/:relationshipId/maintenance`
|
||||
- `POST /api/falukant/family/lover/:relationshipId/discretion`
|
||||
- `POST /api/falukant/family/lover/:relationshipId/acknowledge`
|
||||
- `POST /api/falukant/family/lover/:relationshipId/end`
|
||||
- `POST /api/falukant/family/lover/:relationshipId/gift`
|
||||
|
||||
## 3. Antwortschema
|
||||
|
||||
Jede mutierende Aktion sollte zurückgeben:
|
||||
|
||||
- aktualisierte Liebhaber-Daten
|
||||
- aktualisierte Ehe-Zufriedenheit
|
||||
- aktualisierte Geldmenge
|
||||
- aktualisiertes Ansehen
|
||||
- optionale Nachricht für UI
|
||||
|
||||
## Daemon-Integration
|
||||
|
||||
## 1. Tatsächliche Daemon-Lage
|
||||
|
||||
Der operative Daemon ist nicht Teil dieses Projekts. Dieses Projekt stellt daher:
|
||||
|
||||
- Datenmodell
|
||||
- Backfill
|
||||
- Family-API
|
||||
- UI-Anzeige
|
||||
- Fach- und Übergabedokumente
|
||||
|
||||
Der externe Daemon ist zuständig für:
|
||||
|
||||
- Daily Tick
|
||||
- Monthly Tick
|
||||
- Geldabbuchung
|
||||
- Ansehensänderung
|
||||
- Ehezufriedenheit
|
||||
- Kinder aus Liebschaften
|
||||
|
||||
Die operative Übergabe dafür steht in:
|
||||
|
||||
- [FALUKANT_LOVERS_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md)
|
||||
|
||||
## 2. Neue Benachrichtigungstypen
|
||||
|
||||
Es sollten neue Falukant-Benachrichtigungen eingeführt werden:
|
||||
|
||||
- `loverCostPaid`
|
||||
- `loverUnderfunded`
|
||||
- `loverRumor`
|
||||
- `loverScandal`
|
||||
- `loverChildHidden`
|
||||
- `loverChildKnown`
|
||||
- `marriageCrisis`
|
||||
|
||||
## Kindererzeugung technisch
|
||||
|
||||
## 1. Vorhandene Strukturen nutzen
|
||||
|
||||
Neue Kinder aus Liebschaften sollen dieselbe Charakter- und `ChildRelation`-Logik nutzen wie bestehende Kinder.
|
||||
|
||||
## 2. Erzeugungsort
|
||||
|
||||
Die Kind-Erzeugung soll vom externen Daemon ausgeführt oder angestoßen werden. Dieses Backend muss dafür die Zielstruktur stabil bereitstellen.
|
||||
|
||||
## 3. Anerkennung eines Kindes
|
||||
|
||||
Spätere Service-Methode:
|
||||
|
||||
- `acknowledgeLoverChild(hashedUserId, childCharacterId)`
|
||||
|
||||
Wirkung:
|
||||
|
||||
- `legitimacy = acknowledged_bastard`
|
||||
- `publicKnown = true`
|
||||
- Ansehen anpassen
|
||||
- Ehe-Zufriedenheit anpassen
|
||||
|
||||
## Frontend-Konzept
|
||||
|
||||
Hauptansicht:
|
||||
|
||||
- [FamilyView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/FamilyView.vue)
|
||||
|
||||
## 1. Ehebereich erweitern
|
||||
|
||||
Im Ehe- oder Partnerbereich anzeigen:
|
||||
|
||||
- `Ehe-Zufriedenheit`
|
||||
- textlicher Status
|
||||
- `stabil`
|
||||
- `angespannt`
|
||||
- `krisenhaft`
|
||||
- kurzer Hinweis, ob aktive Liebschaften die Ehe belasten oder stabilisieren
|
||||
|
||||
## 2. Lovers-Bereich ausbauen
|
||||
|
||||
Aktuell existiert nur Name plus Zuneigung. Neu anzeigen:
|
||||
|
||||
- Rolle
|
||||
- Zuneigung
|
||||
- Sichtbarkeit
|
||||
- Diskretion
|
||||
- Unterhaltsniveau
|
||||
- Monatskosten
|
||||
- aktueller Einfluss auf Ansehen
|
||||
- aktueller Einfluss auf Ehe-Zufriedenheit
|
||||
- Risikostatus
|
||||
|
||||
## 3. Aktionen im UI
|
||||
|
||||
Pro Liebschaft:
|
||||
|
||||
- Unterhalt erhöhen oder senken
|
||||
- Diskretion priorisieren
|
||||
- öffentlich anerkennen
|
||||
- beschenken
|
||||
- Beziehung beenden
|
||||
|
||||
## 4. Farbliche Zustände
|
||||
|
||||
### Grün
|
||||
|
||||
- geordnet
|
||||
- geringe Sichtbarkeit
|
||||
- Ehe stabil oder neutral
|
||||
|
||||
### Gelb
|
||||
|
||||
- steigende Sichtbarkeit
|
||||
- mittlere Ehebelastung
|
||||
- Unterhalt knapp
|
||||
|
||||
### Rot
|
||||
|
||||
- Skandalrisiko hoch
|
||||
- Ehekrise
|
||||
- unterfinanziert
|
||||
- Kind öffentlich geworden
|
||||
|
||||
## 5. Kinder aus Liebschaften in FamilyView
|
||||
|
||||
Im Kinderbereich kenntlich machen:
|
||||
|
||||
- legitimes Kind
|
||||
- uneheliches verborgenes Kind
|
||||
- anerkanntes uneheliches Kind
|
||||
|
||||
Es braucht keine sensationelle Darstellung, aber klare Kennzeichnung.
|
||||
|
||||
## I18n-Bedarf
|
||||
|
||||
Benötigte neue Übersetzungsbereiche in:
|
||||
|
||||
- [frontend/src/i18n/locales/de/falukant.json](/mnt/share/torsten/Programs/YourPart3/frontend/src/i18n/locales/de/falukant.json)
|
||||
- [frontend/src/i18n/locales/en/falukant.json](/mnt/share/torsten/Programs/YourPart3/frontend/src/i18n/locales/en/falukant.json)
|
||||
- [frontend/src/i18n/locales/es/falukant.json](/mnt/share/torsten/Programs/YourPart3/frontend/src/i18n/locales/es/falukant.json)
|
||||
|
||||
Neue Schlüsselgruppen:
|
||||
|
||||
- `falukant.family.marriageSatisfaction.*`
|
||||
- `falukant.family.lovers.role.*`
|
||||
- `falukant.family.lovers.visibility`
|
||||
- `falukant.family.lovers.discretion`
|
||||
- `falukant.family.lovers.maintenance`
|
||||
- `falukant.family.lovers.monthlyCost`
|
||||
- `falukant.family.lovers.reputationEffect`
|
||||
- `falukant.family.lovers.marriageEffect`
|
||||
- `falukant.family.lovers.risk.*`
|
||||
- `falukant.family.children.legitimacy.*`
|
||||
|
||||
## Admin- und Debug-Bedarf
|
||||
|
||||
Für Entwicklung und Balancing später sinnvoll:
|
||||
|
||||
- Admin-Sicht auf `relationship_state`
|
||||
- Möglichkeit, `marriageSatisfaction`, `visibility`, `discretion`, `maintenanceLevel` zu setzen
|
||||
- optionales Debug-Tool zum Simulieren von 30 Tagen
|
||||
|
||||
Das sollte nicht Teil des ersten Spieler-UI sein, aber früh mitgedacht werden.
|
||||
|
||||
## Technische Risiken
|
||||
|
||||
### 1. Tick-Duplikate
|
||||
|
||||
Wenn Daily- oder Monthly-Ticks mehrfach laufen, werden Kosten und Ansehen doppelt verrechnet.
|
||||
|
||||
Gegenmaßnahme:
|
||||
|
||||
- `last_daily_processed_at`
|
||||
- `last_monthly_processed_at`
|
||||
- idempotente Verarbeitung pro Beziehung und Tag/Monat
|
||||
|
||||
### 2. Dateninkonsistenz
|
||||
|
||||
Eine `lover`-Beziehung ohne `relationship_state` würde Berechnungen brechen.
|
||||
|
||||
Gegenmaßnahme:
|
||||
|
||||
- beim Lesen fehlende States automatisch erzeugen
|
||||
- oder beim Start ein Reparaturskript
|
||||
|
||||
### 3. Kindererzeugung doppelt
|
||||
|
||||
Bei konkurrierenden Prozessen könnte ein Kind zweimal entstehen.
|
||||
|
||||
Gegenmaßnahme:
|
||||
|
||||
- Transaktion
|
||||
- Sperre pro Beziehung und Tick
|
||||
- eindeutige Monatsverarbeitung
|
||||
|
||||
## Empfohlene Implementierungsreihenfolge
|
||||
|
||||
### Paket 1
|
||||
|
||||
- Migrationen
|
||||
- Modell `RelationshipState`
|
||||
- Associations
|
||||
- Backfill
|
||||
|
||||
### Paket 2
|
||||
|
||||
- Family-Service erweitert lesen
|
||||
- API-Felder ausliefern
|
||||
- UI in `FamilyView` lesend erweitern
|
||||
|
||||
### Paket 3
|
||||
|
||||
- Übergabe Daily Tick
|
||||
- Übergabe Monthly Tick
|
||||
- Abstimmung zu Geldabbuchung
|
||||
- Abstimmung zu Ansehen und Ehezufriedenheit
|
||||
|
||||
### Paket 4
|
||||
|
||||
- Kinder aus Liebschaften
|
||||
- Benachrichtigungen
|
||||
- UI-Aktionen
|
||||
|
||||
### Paket 5
|
||||
|
||||
- Ereignisse
|
||||
- spätere Dienerschaft
|
||||
- Balancing-Phase
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Die technische Erstumsetzung ist abgeschlossen, wenn:
|
||||
|
||||
1. `lover`-Beziehungen Detailzustand besitzen
|
||||
2. Ehezufriedenheit technisch existiert
|
||||
3. Family-API alle neuen Daten ausliefert
|
||||
4. Daily- und Monthly-Tick für den externen Daemon vollständig beschrieben sind
|
||||
5. Monatskosten- und Statuslogik extern ausführbar definiert sind
|
||||
6. Kinder aus Liebschaften technisch entstehen können
|
||||
7. `FamilyView` die neuen Daten sichtbar macht
|
||||
8. weibliche und männliche Spielfiguren regelgleich behandelt werden
|
||||
476
docs/FALUKANT_LOVERS_UNDERGROUND_DAEMON_SPEC.md
Normal file
476
docs/FALUKANT_LOVERS_UNDERGROUND_DAEMON_SPEC.md
Normal file
@@ -0,0 +1,476 @@
|
||||
# Falukant: Daemon-Spezifikation für die Beziehung zwischen Liebschaften und Untergrund
|
||||
|
||||
Dieses Dokument beschreibt die konkrete Daemon-Logik für die Verbindung zwischen:
|
||||
- aktiven Liebschaften
|
||||
- Sichtbarkeit und Diskretion
|
||||
- Untergrundaktivitäten vom Typ `investigate_affair`
|
||||
- Aufdeckung, Skandal und Erpressung
|
||||
|
||||
Es ist die technische Spezifikation für den externen Daemon und ergänzt:
|
||||
- [FALUKANT_LOVERS_DAEMON_SPEC.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_SPEC.md)
|
||||
- [FALUKANT_LOVERS_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md)
|
||||
- [FALUKANT_UNDERGROUND_AFFAIR_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_UNDERGROUND_AFFAIR_DAEMON_HANDOFF.md)
|
||||
- [FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md)
|
||||
|
||||
## 1. Ziel
|
||||
|
||||
Der Untergrund soll Liebschaften nicht nur "finden oder nicht finden", sondern auf dieselben Zustände zugreifen, die das Lovers-System ohnehin täglich verarbeitet:
|
||||
- `visibility`
|
||||
- `discretion`
|
||||
- `acknowledged`
|
||||
- `loverRole`
|
||||
- `publicKnown` unehelicher Kinder
|
||||
- Ruflage und Stand
|
||||
- Haushalts- und Dienerschaftseffekte
|
||||
|
||||
Dadurch entsteht ein gemeinsames System statt zweier getrennter Minigames.
|
||||
|
||||
## 2. Grundprinzip
|
||||
|
||||
Untergrundaktivitäten gegen Liebschaften sind keine völlig unabhängigen Zufallstests.
|
||||
|
||||
Sie hängen ab von:
|
||||
- wie sichtbar die Beziehung ohnehin schon ist
|
||||
- wie diskret sie geführt wird
|
||||
- wie groß und unruhig der Haushalt ist
|
||||
- ob Kinder oder Anerkennung die Sache bereits schwer verbergen machen
|
||||
- wie gut das Opfer sozial abgesichert ist
|
||||
|
||||
Faustregel:
|
||||
- hohe Sichtbarkeit + geringe Diskretion + schlechte Dienerschaft = gute Aufdeckungschance
|
||||
- niedrige Sichtbarkeit + hohe Diskretion + geordneter Haushalt = geringe Aufdeckungschance
|
||||
|
||||
## 3. Betroffene Aktivitäten
|
||||
|
||||
In Phase 1 betrifft diese Spezifikation nur:
|
||||
- `investigate_affair`
|
||||
|
||||
mit den Zielen:
|
||||
- `expose`
|
||||
- `blackmail`
|
||||
|
||||
## 4. Pflichtdaten für den Daemon
|
||||
|
||||
## 4.1 Aktivität
|
||||
|
||||
Aus `falukant_data.underground`:
|
||||
- `id`
|
||||
- `performer_id`
|
||||
- `victim_id`
|
||||
- `type_id`
|
||||
- `parameters.goal`
|
||||
- `result`
|
||||
- `created_at`
|
||||
|
||||
Aus `falukant_type.underground`:
|
||||
- `tr`
|
||||
|
||||
## 4.2 Opferdaten
|
||||
|
||||
Für das Opfer:
|
||||
- `falukant_user.id`
|
||||
- `user.hashed_id`
|
||||
- `character.id`
|
||||
- `character.reputation`
|
||||
- `character.birthdate`
|
||||
- `character.title_of_nobility`
|
||||
|
||||
## 4.3 Liebschaftsdaten
|
||||
|
||||
Für alle aktiven Liebschaften des Opfers:
|
||||
- `relationship.id`
|
||||
- `relationship.character1_id`
|
||||
- `relationship.character2_id`
|
||||
- `relationship_type.tr`
|
||||
- `relationship_state.lover_role`
|
||||
- `relationship_state.visibility`
|
||||
- `relationship_state.discretion`
|
||||
- `relationship_state.affection`
|
||||
- `relationship_state.acknowledged`
|
||||
- `relationship_state.status_fit`
|
||||
- `relationship_state.active`
|
||||
|
||||
## 4.4 Kinderdaten
|
||||
|
||||
Optional, aber empfohlen:
|
||||
- `child_relation.legitimacy`
|
||||
- `child_relation.birth_context`
|
||||
- `child_relation.public_known`
|
||||
|
||||
## 4.5 Haushalts-/Dienerdaten
|
||||
|
||||
Wenn das Dienersystem aktiv ist:
|
||||
- `user_house.servant_count`
|
||||
- `user_house.servant_quality`
|
||||
- `user_house.servant_pay_level`
|
||||
- `user_house.household_order`
|
||||
- daraus abgeleitete Haushalts-/Diskretionswerte
|
||||
|
||||
## 5. Auswahl des Zielobjekts
|
||||
|
||||
Wenn ein Opfer mehrere aktive Liebschaften hat, muss der Daemon eine Ziellogik verwenden.
|
||||
|
||||
Empfohlene Reihenfolge:
|
||||
|
||||
1. Nur aktive `lover`-Beziehungen betrachten
|
||||
2. je Beziehung einen `discoveryScore` berechnen
|
||||
3. die Beziehung mit dem höchsten `discoveryScore` als primäres Ziel verwenden
|
||||
4. bei fast gleichen Werten darf der Daemon zufällig zwischen den besten Kandidaten wählen
|
||||
|
||||
## 6. Discovery-Score
|
||||
|
||||
Der `discoveryScore` bestimmt, wie leicht eine konkrete Liebschaft durch den Untergrund verwertbar wird.
|
||||
|
||||
## 6.1 Formel
|
||||
|
||||
```text
|
||||
discoveryScore =
|
||||
visibility * 0.45
|
||||
+ (100 - discretion) * 0.30
|
||||
+ acknowledgedBonus
|
||||
+ childBonus
|
||||
+ ageMalusVisibilityBonus
|
||||
+ householdLeakBonus
|
||||
+ multipleAffairBonus
|
||||
+ statusMismatchBonus
|
||||
```
|
||||
|
||||
## 6.2 Teilwerte
|
||||
|
||||
### acknowledgedBonus
|
||||
|
||||
```text
|
||||
if acknowledged = true => +10
|
||||
sonst => +0
|
||||
```
|
||||
|
||||
### childBonus
|
||||
|
||||
```text
|
||||
if hidden bastard exists => +8
|
||||
if public_known bastard exists => +18
|
||||
```
|
||||
|
||||
Wenn mehrere Kinder existieren:
|
||||
- maximal `+20` in Summe
|
||||
|
||||
### ageMalusVisibilityBonus
|
||||
|
||||
Wenn die Liebschaft wegen jungen Alters bereits reputationsschädlich ist:
|
||||
|
||||
```text
|
||||
minAge <= 13 => +18
|
||||
minAge <= 15 => +12
|
||||
minAge <= 17 => +6
|
||||
sonst => +0
|
||||
```
|
||||
|
||||
### householdLeakBonus
|
||||
|
||||
Aus dem Dienersystem:
|
||||
|
||||
```text
|
||||
if no house data => +0
|
||||
if householdOrder <= 35 => +8
|
||||
if servantPayLevel = low => +5
|
||||
if servantCount > expectedMax + 1 => +4
|
||||
if servantQuality <= 35 => +6
|
||||
```
|
||||
|
||||
Deckel:
|
||||
- maximal `+15`
|
||||
|
||||
### multipleAffairBonus
|
||||
|
||||
```text
|
||||
if victim has 2 active lovers => +8
|
||||
if victim has 3 or more active lovers => +14
|
||||
```
|
||||
|
||||
### statusMismatchBonus
|
||||
|
||||
Wenn die Beziehung standesmäßig auffällig ist:
|
||||
|
||||
```text
|
||||
status_fit = -2 => +10
|
||||
status_fit = -1 => +5
|
||||
sonst => +0
|
||||
```
|
||||
|
||||
## 7. Erfolgswahrscheinlichkeit
|
||||
|
||||
## 7.1 Grundwurf
|
||||
|
||||
Auf Basis des höchsten `discoveryScore`:
|
||||
|
||||
```text
|
||||
successChance = clamp(20 + discoveryScore * 0.55, 5, 95)
|
||||
```
|
||||
|
||||
Interpretation:
|
||||
- selbst sehr diskrete Beziehungen bleiben mit kleinem Restrisiko auffindbar
|
||||
- offen geführte, chaotische Beziehungen werden fast sicher entdeckt
|
||||
|
||||
## 7.2 Ergebnisstufen
|
||||
|
||||
```text
|
||||
roll <= successChance * 0.55 => full success
|
||||
roll <= successChance => partial success
|
||||
sonst => failure
|
||||
```
|
||||
|
||||
## 8. Behandlung von `goal = expose`
|
||||
|
||||
## 8.1 Ziel
|
||||
|
||||
Die Beziehung soll öffentlich sichtbar und reputationsschädlich werden.
|
||||
|
||||
## 8.2 Wirkung bei vollem Erfolg
|
||||
|
||||
Auf Ziel-Liebschaft:
|
||||
- `visibility += 18..30`
|
||||
- `discretion -= 8..18`
|
||||
|
||||
Auf Opfer:
|
||||
- `reputationDelta = -2 .. -6`
|
||||
|
||||
Zusätzlich:
|
||||
- wenn `visibility >= 60` nach Anpassung: Skandalprüfung sofort auslösen
|
||||
- wenn `public_known` uneheliches Kind bereits existiert: zusätzlicher Rufschaden `-1`
|
||||
|
||||
## 8.3 Wirkung bei Teilerfolg
|
||||
|
||||
Auf Ziel-Liebschaft:
|
||||
- `visibility += 8..15`
|
||||
- `discretion -= 3..8`
|
||||
|
||||
Auf Opfer:
|
||||
- `reputationDelta = -1 .. -3`
|
||||
|
||||
Kein garantierter sofortiger Skandal, aber deutlich erhöhte Folgewahrscheinlichkeit.
|
||||
|
||||
## 8.4 Wirkung bei Fehlschlag
|
||||
|
||||
Keine öffentliche Wirkung, aber optional:
|
||||
- kleines Gegenrisiko für den Untergrund später
|
||||
- oder `notes` mit "no proof"
|
||||
|
||||
Für Phase 1 genügt:
|
||||
- `status = failed`
|
||||
- `outcome = failure`
|
||||
|
||||
## 9. Behandlung von `goal = blackmail`
|
||||
|
||||
## 9.1 Ziel
|
||||
|
||||
Belastendes Wissen beschaffen, ohne sofort volle Öffentlichkeit zu erzeugen.
|
||||
|
||||
## 9.2 Wirkung bei vollem Erfolg
|
||||
|
||||
Auf Ziel-Liebschaft:
|
||||
- `visibility += 4..9`
|
||||
- `discretion -= 2..6`
|
||||
|
||||
Auf Aktivität:
|
||||
- `blackmailAmount` setzen
|
||||
- `discoveries` mit verwertbaren Details befüllen
|
||||
|
||||
Auf Opfer:
|
||||
- kein großer Sofort-Rufschaden
|
||||
- optional `reputationDelta = 0 .. -1`
|
||||
|
||||
## 9.3 Wirkung bei Teilerfolg
|
||||
|
||||
Auf Ziel-Liebschaft:
|
||||
- `visibility += 2..5`
|
||||
|
||||
Auf Aktivität:
|
||||
- kleinerer `blackmailAmount`
|
||||
- `outcome = partial`
|
||||
|
||||
## 9.4 Wirkung bei Fehlschlag
|
||||
|
||||
- keine verwertbare Entdeckung
|
||||
- `status = failed`
|
||||
- `outcome = failure`
|
||||
|
||||
## 10. Berechnung der Erpressungssumme
|
||||
|
||||
Die Erpressungssumme soll aus sozialer Fallhöhe und Beweiswert entstehen.
|
||||
|
||||
## 10.1 Formel
|
||||
|
||||
```text
|
||||
base =
|
||||
500
|
||||
+ visibility * 12
|
||||
+ max(0, reputation) * 15
|
||||
+ titleGroupBonus
|
||||
+ childBlackmailBonus
|
||||
|
||||
blackmailAmount = round(base * outcomeFactor)
|
||||
```
|
||||
|
||||
## 10.2 titleGroupBonus
|
||||
|
||||
Aus der Standesgruppe des Lovers-Systems:
|
||||
|
||||
```text
|
||||
group 0 => +0
|
||||
group 1 => +600
|
||||
group 2 => +1800
|
||||
group 3 => +4200
|
||||
```
|
||||
|
||||
## 10.3 childBlackmailBonus
|
||||
|
||||
```text
|
||||
hidden_bastard exists => +900
|
||||
public_known bastard exists => +1600
|
||||
```
|
||||
|
||||
## 10.4 outcomeFactor
|
||||
|
||||
```text
|
||||
full success => 1.0
|
||||
partial success => 0.55
|
||||
failure => 0
|
||||
```
|
||||
|
||||
## 11. Sofortige Skandalprüfung
|
||||
|
||||
Bei `goal = expose` und starker Sichtbarkeitssteigerung darf der Untergrund direkt einen Skandal anstoßen.
|
||||
|
||||
## 11.1 Triggerschwelle
|
||||
|
||||
```text
|
||||
if visibility_after >= 60:
|
||||
trigger scandal check
|
||||
```
|
||||
|
||||
Zusatzbonus:
|
||||
- `+10` Punkte auf die reguläre Skandalchance bei sehr jungem Alter `<= 15`
|
||||
- `+6` Punkte bei `public_known` Kind
|
||||
- `+5` Punkte bei `householdOrder <= 35`
|
||||
|
||||
## 11.2 Socket-Events
|
||||
|
||||
Wenn daraus ein Skandal resultiert:
|
||||
- `falukant_family_scandal_hint`
|
||||
- `falukantUpdateFamily` mit `reason = scandal`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
## 12. Struktur von `underground.result`
|
||||
|
||||
Der Daemon schreibt mindestens:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "resolved",
|
||||
"outcome": "success",
|
||||
"discoveries": {
|
||||
"relationshipId": 123,
|
||||
"loverRole": "secret_affair",
|
||||
"visibility": 58,
|
||||
"acknowledged": false,
|
||||
"publicKnownChild": false,
|
||||
"householdLeak": true
|
||||
},
|
||||
"visibilityDelta": 14,
|
||||
"reputationDelta": -3,
|
||||
"blackmailAmount": 2400,
|
||||
"notes": "Servants and low discretion made the affair easy to trace."
|
||||
}
|
||||
```
|
||||
|
||||
## 13. Pflichtregeln für `discoveries`
|
||||
|
||||
`discoveries` soll mindestens enthalten:
|
||||
- `relationshipId`
|
||||
- `loverRole`
|
||||
- `visibility`
|
||||
- `acknowledged`
|
||||
|
||||
Optional, aber sehr nützlich:
|
||||
- `publicKnownChild`
|
||||
- `hiddenChild`
|
||||
- `householdLeak`
|
||||
- `minAgeBracket`
|
||||
- `multipleAffairs`
|
||||
|
||||
## 14. Interaktion mit Dienerschaft
|
||||
|
||||
Das Dienersystem ist ein eigenständiger Modifikator, kein Ersatz für Sichtbarkeit oder Diskretion.
|
||||
|
||||
Der Untergrund soll Dienerschaft nur als Verstärker oder Dämpfer nutzen:
|
||||
|
||||
Günstig für Aufdeckung:
|
||||
- niedrige Bezahlung
|
||||
- schlechte Qualität
|
||||
- chaotischer Haushalt
|
||||
- übergroße Dienerschaft
|
||||
|
||||
Ungünstig für Aufdeckung:
|
||||
- hohe Qualität
|
||||
- großzügige Bezahlung
|
||||
- geordneter Haushalt
|
||||
- passende, nicht zu große Dienerschaft
|
||||
|
||||
## 15. Interaktion mit Lover-Daily
|
||||
|
||||
Wichtig:
|
||||
- Der Untergrund darf Lovers-Zustände verändern.
|
||||
- Danach verarbeitet das normale Daily-System diese Zustände weiter.
|
||||
|
||||
Das heißt:
|
||||
- `visibility`-Erhöhungen aus dem Untergrund laufen später in Daily-Skandale und Rufdrift hinein.
|
||||
- Untergrund ersetzt nicht die Daily-Logik, sondern stößt sie an.
|
||||
|
||||
## 16. Idempotenz
|
||||
|
||||
Jede `investigate_affair`-Aktivität darf genau einmal verarbeitet werden.
|
||||
|
||||
Verarbeitbar nur wenn:
|
||||
- `underground_type.tr = investigate_affair`
|
||||
- `result.status = pending`
|
||||
|
||||
Nach Verarbeitung:
|
||||
- `result.status` auf `resolved` oder `failed`
|
||||
|
||||
Der Daemon darf keine Aktivität erneut anfassen, deren `result.status` nicht mehr `pending` ist.
|
||||
|
||||
## 17. Transaktionsgrenze
|
||||
|
||||
Folgendes soll atomar laufen:
|
||||
- Ziel-Liebschaft bestimmen
|
||||
- Erfolgswurf
|
||||
- Sichtbarkeit/Diskretion ändern
|
||||
- Rufänderung anwenden
|
||||
- `underground.result` schreiben
|
||||
- optionale Skandalereignisse vorbereiten
|
||||
|
||||
Empfehlung:
|
||||
- eine DB-Transaktion pro Aktivität
|
||||
|
||||
## 18. Definition of Done
|
||||
|
||||
Die Daemon-Umsetzung ist ausreichend, wenn:
|
||||
|
||||
1. `investigate_affair` für `expose` und `blackmail` verschieden behandelt wird
|
||||
2. nicht beliebige, sondern die plausibelste aktive Liebschaft des Opfers gewählt wird
|
||||
3. Sichtbarkeit und Diskretion aus dem Lovers-System als Eingangsgrößen verwendet werden
|
||||
4. Dienerschaft optional als Leck-/Diskretionsfaktor einfließt
|
||||
5. `expose` Sichtbarkeit und Ruf spürbar verschieben kann
|
||||
6. `blackmail` belastbare `blackmailAmount`-Werte produziert
|
||||
7. Skandale bei starken Fällen sofort ausgelöst werden können
|
||||
8. `underground.result` vollständig und UI-lesbar gefüllt wird
|
||||
|
||||
## 19. Empfehlung für die Implementierungsreihenfolge
|
||||
|
||||
1. Ziel-Liebschaft und `discoveryScore` implementieren
|
||||
2. `success / partial / failure` für `expose`
|
||||
3. `blackmailAmount` für `blackmail`
|
||||
4. `discoveries`-Füllung
|
||||
5. Sofort-Skandalprüfung
|
||||
6. Dienerschaftsmodifikator ergänzen
|
||||
7. Balancing nach ersten Tests
|
||||
569
docs/FALUKANT_MARRIAGE_HOUSEHOLD_CONTROL_SPEC.md
Normal file
569
docs/FALUKANT_MARRIAGE_HOUSEHOLD_CONTROL_SPEC.md
Normal file
@@ -0,0 +1,569 @@
|
||||
# Falukant: Steuerung von Ehezustand und Hausfrieden
|
||||
|
||||
Dieses Dokument beschreibt:
|
||||
|
||||
- wie Spieler `Ehezustand` und `Hausfrieden` direkt beeinflussen können
|
||||
- welche Werte dafür im Backend sichtbar und änderbar sein müssen
|
||||
- was der externe Daemon täglich und monatlich berechnen soll
|
||||
|
||||
Die Datei ist bewusst als gemeinsame Arbeitsgrundlage für UI, Backend und externen Daemon formuliert.
|
||||
|
||||
## 1. Zielbild
|
||||
|
||||
Es soll zwei getrennte, aber gekoppelte Systeme geben:
|
||||
|
||||
- `marriageSatisfaction`
|
||||
- numerisch `0..100`
|
||||
- individueller Kernwert der Ehe
|
||||
- `householdTension`
|
||||
- aggregierter Haushaltszustand
|
||||
- nach außen in UI als `low | medium | high`
|
||||
- intern sinnvollerweise als numerischer Spannungswert `0..100`
|
||||
|
||||
Interpretation:
|
||||
|
||||
- `marriageSatisfaction` beschreibt die Qualität der Paarbeziehung
|
||||
- `householdTension` beschreibt Spannungen im gesamten Haus
|
||||
- Liebschaften
|
||||
- Unterversorgung
|
||||
- Ordnung
|
||||
- Kinderkonflikte
|
||||
- Dienerschaft
|
||||
|
||||
## 2. Werte und Ableitungen
|
||||
|
||||
## 2.1 Ehe
|
||||
|
||||
Bestehend:
|
||||
|
||||
- `relationship_state.marriage_satisfaction`
|
||||
|
||||
Neu sinnvoll:
|
||||
|
||||
- `relationship_state.marriage_public_stability`
|
||||
- besteht bereits
|
||||
- soll aktiv genutzt werden
|
||||
- optional später:
|
||||
- `last_affection_action_at`
|
||||
- `last_conflict_action_at`
|
||||
- `last_shared_time_at`
|
||||
|
||||
UI-Ableitung:
|
||||
|
||||
- `0..19` => `broken`
|
||||
- `20..39` => `fragile`
|
||||
- `40..59` => `strained`
|
||||
- `60..79` => `stable`
|
||||
- `80..100` => `harmonious`
|
||||
|
||||
## 2.2 Hausfrieden
|
||||
|
||||
Der bisherige reine UI-Helfer
|
||||
|
||||
- `low`
|
||||
- `medium`
|
||||
- `high`
|
||||
|
||||
reicht für eine echte Steuerung nicht aus.
|
||||
|
||||
Neu sinnvoll:
|
||||
|
||||
- interner Wert `householdTensionScore`
|
||||
- Bereich `0..100`
|
||||
- `0` = sehr ruhig
|
||||
- `100` = offener Hauskonflikt
|
||||
|
||||
UI-Ableitung:
|
||||
|
||||
- `0..24` => `low`
|
||||
- `25..59` => `medium`
|
||||
- `60..100` => `high`
|
||||
|
||||
Falls kein eigener Persistenzwert angelegt werden soll, darf der Daemon den Score auch nur berechnen und als API-Feld zurückgeben.
|
||||
|
||||
## 3. Direkte Spieleraktionen
|
||||
|
||||
Es braucht direkte Spielzüge, die der Spieler bewusst auslösen kann.
|
||||
|
||||
Wichtig:
|
||||
|
||||
- nicht jede Aktion muss sofort große Werte ändern
|
||||
- direkte Aktionen sollen kleine, klare Effekte haben
|
||||
- der Daemon übernimmt Drift, Gegenkräfte und Folgewirkungen
|
||||
|
||||
## 3.1 Ehe-Aktionen
|
||||
|
||||
Diese Aktionen gehören fachlich in `FamilyView`.
|
||||
|
||||
### A. Zeit mit Ehepartner verbringen
|
||||
|
||||
Zweck:
|
||||
|
||||
- Standardaktion zur Pflege der Beziehung
|
||||
|
||||
Regel:
|
||||
|
||||
- verfügbar nur bei aktiver Ehe
|
||||
- Cooldown: `1x pro Tag`
|
||||
- Kosten: `0` oder sehr klein
|
||||
|
||||
Soforteffekt:
|
||||
|
||||
- `marriageSatisfaction +2`
|
||||
- `householdTensionScore -1`
|
||||
|
||||
Modifikatoren:
|
||||
|
||||
- wenn aktive sichtbare Liebschaft `visibility >= 45`: nur `+1`
|
||||
- wenn `householdOrder <= 35`: kein Bonus auf Hausfrieden
|
||||
- wenn `marriageSatisfaction < 25`: stattdessen nur `+1`
|
||||
|
||||
### B. Geschenk an Ehepartner
|
||||
|
||||
Zweck:
|
||||
|
||||
- Geld gegen schnellere Stabilisierung
|
||||
|
||||
Regel:
|
||||
|
||||
- verfügbar nur bei aktiver Ehe
|
||||
- Stufen: `small`, `decent`, `lavish`
|
||||
- Cooldown: `1x pro 3 Tage`
|
||||
|
||||
Soforteffekt:
|
||||
|
||||
- `small`: `marriageSatisfaction +2`
|
||||
- `decent`: `marriageSatisfaction +4`
|
||||
- `lavish`: `marriageSatisfaction +7`
|
||||
|
||||
Nebeneffekt:
|
||||
|
||||
- `marriagePublicStability +1/+2/+3`
|
||||
|
||||
Malus:
|
||||
|
||||
- bei gleichzeitig unterfinanzierter Liebschaft halbierter Effekt
|
||||
|
||||
### C. Streit schlichten
|
||||
|
||||
Zweck:
|
||||
|
||||
- gezielte Krisenintervention
|
||||
|
||||
Regel:
|
||||
|
||||
- verfügbar nur wenn `householdTensionScore >= 35` oder `marriageSatisfaction <= 50`
|
||||
- Cooldown: `1x pro 2 Tage`
|
||||
|
||||
Soforteffekt:
|
||||
|
||||
- `householdTensionScore -4`
|
||||
- `marriageSatisfaction +1`
|
||||
|
||||
Malus:
|
||||
|
||||
- wenn `visibility` einer aktiven Liebschaft `>= 60`, dann nur `householdTensionScore -2`
|
||||
|
||||
### D. Fest nur für den Haushalt
|
||||
|
||||
Zweck:
|
||||
|
||||
- Hausfrieden über Geld und Repräsentation stützen
|
||||
|
||||
Regel:
|
||||
|
||||
- verfügbar bei vorhandenem Haus
|
||||
- kleiner interner Hausakt, nicht großes Reputationsfest
|
||||
- Cooldown: `1x pro Monat`
|
||||
|
||||
Soforteffekt:
|
||||
|
||||
- `householdTensionScore -6`
|
||||
- `marriageSatisfaction +2`
|
||||
- `householdOrder +2`
|
||||
|
||||
Malus:
|
||||
|
||||
- bei unterbesetzter Dienerschaft nur halbe Wirkung
|
||||
|
||||
## 3.2 Haus-Aktionen
|
||||
|
||||
Diese Aktionen gehören fachlich in `HouseView`.
|
||||
|
||||
### A. Haus ordnen
|
||||
|
||||
Zweck:
|
||||
|
||||
- kleine direkte Ordnungsmaßnahme
|
||||
|
||||
Regel:
|
||||
|
||||
- Cooldown: `1x pro Tag`
|
||||
- Kosten: niedrig
|
||||
|
||||
Soforteffekt:
|
||||
|
||||
- `householdOrder +3`
|
||||
- wenn `householdOrder > 70`: stattdessen nur `+1`
|
||||
|
||||
Indirekter Effekt:
|
||||
|
||||
- besserer Daily-Wert für `householdTensionScore`
|
||||
|
||||
### B. Diener einstellen
|
||||
|
||||
Bereits vorhanden.
|
||||
|
||||
Neue fachliche Wirkung:
|
||||
|
||||
- wenn vorher `servantCount < expectedMin`
|
||||
- sofort `householdTensionScore -2`
|
||||
|
||||
### C. Diener entlassen
|
||||
|
||||
Bereits vorhanden.
|
||||
|
||||
Neue fachliche Wirkung:
|
||||
|
||||
- wenn danach `servantCount < expectedMin`
|
||||
- sofort `householdTensionScore +3`
|
||||
|
||||
### D. Bezahlung erhöhen
|
||||
|
||||
Bereits vorhanden.
|
||||
|
||||
Neue fachliche Wirkung:
|
||||
|
||||
- wenn von `low -> normal` oder `normal -> high`
|
||||
- sofort `householdOrder +2`
|
||||
- `householdTensionScore -1`
|
||||
|
||||
## 3.3 Familien-/Kinder-Aktionen
|
||||
|
||||
### A. Uneheliches Kind anerkennen
|
||||
|
||||
Zweck:
|
||||
|
||||
- offenere, geordnetere Lösung statt versteckter Konfliktlage
|
||||
|
||||
Soforteffekt:
|
||||
|
||||
- `publicKnown = true`
|
||||
- `legitimacy = acknowledged_bastard`
|
||||
- `householdTensionScore -2`, wenn Beziehung bereits öffentlich geordnet
|
||||
- `householdTensionScore +3`, wenn Ehe schwach und Beziehung skandalös
|
||||
|
||||
Eheeffekt:
|
||||
|
||||
- `marriageSatisfaction -2` bis `-6` je nach Sichtbarkeit und Stand
|
||||
|
||||
### B. Erbenfrage regeln
|
||||
|
||||
Wenn uneheliche Kinder sichtbar werden, kann die UI später eine Handlung
|
||||
`Erbfolge klären` bekommen.
|
||||
|
||||
Erste Version:
|
||||
|
||||
- nur vorgemerkt
|
||||
- noch keine direkte Aktion nötig
|
||||
|
||||
## 3.4 Liebschafts-Aktionen mit Einfluss auf Ehe und Haus
|
||||
|
||||
Bestehend:
|
||||
|
||||
- Unterhalt ändern
|
||||
- Beziehung anerkennen
|
||||
- Beziehung beenden
|
||||
|
||||
Diese Aktionen sollen explizit folgende Sofortwirkung haben:
|
||||
|
||||
### Unterhalt erhöhen
|
||||
|
||||
- `monthsUnderfunded` baut sich später im Daemon ab
|
||||
- sofort kein großer Ehebonus
|
||||
- aber `householdTensionScore -1`, wenn vorher Unterversorgung bestand
|
||||
|
||||
### Beziehung anerkennen
|
||||
|
||||
- `visibility` steigt nicht automatisch hart, aber öffentlicher Charakter nimmt zu
|
||||
- bei hohen Ständen geordnet eher neutral bis leicht positiv für Ehe-Stabilität
|
||||
- bei niedrigen Ständen eher negativ
|
||||
|
||||
Sofortregel:
|
||||
|
||||
- Standesgruppe `0-1`: `marriageSatisfaction -3`, `householdTensionScore +2`
|
||||
- Standesgruppe `2`: `marriageSatisfaction -1`, `householdTensionScore +1`
|
||||
- Standesgruppe `3`: `marriagePublicStability +1`, `householdTensionScore -1`, wenn Diskretion und Versorgung gut sind
|
||||
|
||||
### Beziehung beenden
|
||||
|
||||
- sofort `householdTensionScore -3`, wenn Liebschaft riskant war
|
||||
- sofort `marriageSatisfaction +1`, wenn aktive Ehe existiert
|
||||
- aber bei hoher `affection >= 70` auch möglicher Malus auf Stimmungssystem später
|
||||
|
||||
## 4. Daemon-Berechnung
|
||||
|
||||
## 4.1 Daily-Input
|
||||
|
||||
Der externe Daemon braucht pro Spielerfigur:
|
||||
|
||||
- aktive Ehebeziehung mit `marriageSatisfaction`, `marriagePublicStability`
|
||||
- aktive Liebschaften mit:
|
||||
- `loverRole`
|
||||
- `visibility`
|
||||
- `discretion`
|
||||
- `maintenanceLevel`
|
||||
- `statusFit`
|
||||
- `monthsUnderfunded`
|
||||
- `acknowledged`
|
||||
- Kinderdaten:
|
||||
- `legitimacy`
|
||||
- `birthContext`
|
||||
- `publicKnown`
|
||||
- Hausdaten:
|
||||
- `servantCount`
|
||||
- `servantQuality`
|
||||
- `servantPayLevel`
|
||||
- `householdOrder`
|
||||
- Charakterdaten:
|
||||
- `titleOfNobility`
|
||||
- `reputation`
|
||||
|
||||
## 4.2 Daily-Berechnung für Ehe
|
||||
|
||||
Grunddrift:
|
||||
|
||||
```text
|
||||
marriageDelta = 0
|
||||
|
||||
if marriageSatisfaction > 55: marriageDelta -= 1 every 3 days
|
||||
if marriageSatisfaction < 55: marriageDelta += 1 every 5 days
|
||||
```
|
||||
|
||||
Liebschaften:
|
||||
|
||||
```text
|
||||
for each active lover:
|
||||
if visibility >= 60: marriageDelta -= 2
|
||||
else if visibility >= 35: marriageDelta -= 1
|
||||
|
||||
if monthsUnderfunded >= 2: marriageDelta -= 1
|
||||
if acknowledged = true and statusGroup <= 1: marriageDelta -= 1
|
||||
if acknowledged = true and statusGroup = 3 and visibility <= 35 and maintenanceLevel >= 60:
|
||||
marriageDelta += 0 or +1 every few days
|
||||
```
|
||||
|
||||
Zu jung:
|
||||
|
||||
```text
|
||||
if minAge <= 15: marriageDelta -= 1
|
||||
if minAge <= 13: marriageDelta -= 2
|
||||
```
|
||||
|
||||
Haus:
|
||||
|
||||
```text
|
||||
if householdOrder >= 75: marriageDelta += 1
|
||||
if householdOrder <= 35: marriageDelta -= 1
|
||||
if householdTensionScore >= 60: marriageDelta -= 1
|
||||
```
|
||||
|
||||
Dienerschaft:
|
||||
|
||||
```text
|
||||
if servantCount < expectedMin: marriageDelta -= 1
|
||||
if servantPayLevel = high and servantQuality >= 70 and householdOrder >= 70:
|
||||
marriageDelta += 1 every 3 days
|
||||
```
|
||||
|
||||
Danach:
|
||||
|
||||
- clamp `0..100`
|
||||
|
||||
## 4.3 Daily-Berechnung für Hausfrieden
|
||||
|
||||
Interner Wert:
|
||||
|
||||
```text
|
||||
householdTensionScore = base
|
||||
```
|
||||
|
||||
Empfohlene Berechnung:
|
||||
|
||||
```text
|
||||
base = 10
|
||||
|
||||
for each active lover:
|
||||
if visibility >= 60: base += 18
|
||||
else if visibility >= 35: base += 10
|
||||
else: base += 4
|
||||
|
||||
if monthsUnderfunded >= 1: base += 6
|
||||
if monthsUnderfunded >= 2: base += 6
|
||||
if acknowledged = true: base += 4
|
||||
if statusFit = -1: base += 3
|
||||
if statusFit = -2: base += 6
|
||||
```
|
||||
|
||||
Kinder:
|
||||
|
||||
```text
|
||||
for each child where birthContext = 'lover':
|
||||
if publicKnown = true: base += 6
|
||||
else: base += 2
|
||||
|
||||
if legitimacy = 'acknowledged_bastard': base += 2
|
||||
if legitimacy = 'hidden_bastard': base += 4
|
||||
```
|
||||
|
||||
Haus:
|
||||
|
||||
```text
|
||||
if householdOrder >= 80: base -= 6
|
||||
else if householdOrder >= 65: base -= 3
|
||||
|
||||
if householdOrder <= 35: base += 8
|
||||
else if householdOrder <= 50: base += 4
|
||||
```
|
||||
|
||||
Dienerschaft:
|
||||
|
||||
```text
|
||||
if servantCount < expectedMin: base += 5
|
||||
if servantPayLevel = low: base += 2
|
||||
if servantQuality >= 70 and servantPayLevel = high: base -= 3
|
||||
```
|
||||
|
||||
Ehe:
|
||||
|
||||
```text
|
||||
if marriageSatisfaction <= 35: base += 6
|
||||
if marriageSatisfaction >= 75: base -= 2
|
||||
```
|
||||
|
||||
Danach:
|
||||
|
||||
- clamp `0..100`
|
||||
- UI-Ableitung auf `low/medium/high`
|
||||
|
||||
## 4.4 Monthly-Berechnung
|
||||
|
||||
Monatlich soll der Daemon zusätzlich:
|
||||
|
||||
- Dienerkosten abbuchen
|
||||
- Liebschaftskosten abbuchen
|
||||
- bei Unterversorgung `householdTensionScore` stärker erhöhen
|
||||
- langfristige Ordnungs- oder Eheboni addieren
|
||||
|
||||
Empfohlene Zusatzregeln:
|
||||
|
||||
```text
|
||||
if a lover was underfunded this month:
|
||||
householdTensionScore += 4
|
||||
|
||||
if servantCount far below expectedMin for full month:
|
||||
householdTensionScore += 3
|
||||
|
||||
if householdOrder >= 80 for full month:
|
||||
marriageSatisfaction += 1
|
||||
|
||||
if householdOrder <= 30 for full month:
|
||||
marriageSatisfaction -= 2
|
||||
```
|
||||
|
||||
## 5. UI-Anforderungen
|
||||
|
||||
## 5.1 FamilyView
|
||||
|
||||
Neu sinnvolle Aktionen:
|
||||
|
||||
- `Zeit miteinander verbringen`
|
||||
- `Geschenk machen`
|
||||
- `Streit schlichten`
|
||||
- `Liebschaft beenden`
|
||||
- `Uneheliches Kind anerkennen`
|
||||
|
||||
Zusätzlich hilfreiche Anzeige:
|
||||
|
||||
- kurze Ursachenliste für `Hausfrieden`
|
||||
- z. B. `sichtbare Liebschaft`
|
||||
- `Unruhe im Haus`
|
||||
- `zu wenig Diener`
|
||||
- `anerkanntes uneheliches Kind`
|
||||
|
||||
## 5.2 HouseView
|
||||
|
||||
Neu sinnvolle Aktionen:
|
||||
|
||||
- `Haus ordnen`
|
||||
- vorhandene Dieneraktionen mit klarer Auswirkungstextzeile
|
||||
|
||||
Anzeige:
|
||||
|
||||
- `Haushaltsordnung`
|
||||
- `erwartete Dienerzahl`
|
||||
- `Auswirkung auf Hausfrieden`
|
||||
|
||||
## 6. Backend-Anforderungen
|
||||
|
||||
## 6.1 Direktaktionen
|
||||
|
||||
Dieses Projekt sollte Endpunkte für direkte Einflussaktionen bereitstellen:
|
||||
|
||||
- `POST /api/falukant/family/marriage/spend-time`
|
||||
- `POST /api/falukant/family/marriage/gift`
|
||||
- `POST /api/falukant/family/marriage/reconcile`
|
||||
- `POST /api/falukant/houses/order`
|
||||
- später optional:
|
||||
- `POST /api/falukant/family/children/acknowledge`
|
||||
|
||||
## 6.2 API-Rückgabe
|
||||
|
||||
Family-API sollte zusätzlich liefern:
|
||||
|
||||
- `marriageSatisfaction`
|
||||
- `marriageState`
|
||||
- `marriagePublicStability`
|
||||
- `householdTension`
|
||||
- `householdTensionScore`
|
||||
- optional:
|
||||
- `householdTensionReasons[]`
|
||||
|
||||
House-API sollte zusätzlich liefern:
|
||||
|
||||
- `householdOrder`
|
||||
- `expectedServantsMin`
|
||||
- `expectedServantsMax`
|
||||
- `marriageComfortModifier`
|
||||
|
||||
## 7. Priorisierte Umsetzung
|
||||
|
||||
## Phase A
|
||||
|
||||
- `statusFit`-Fehler korrigieren
|
||||
- direkte Ehe-Aktionen `Zeit`, `Geschenk`, `Streit schlichten`
|
||||
- direkte Haus-Aktion `Haus ordnen`
|
||||
- Family-API um `householdTensionScore` erweitern
|
||||
|
||||
## Phase B
|
||||
|
||||
- externer Daemon berechnet Daily-Drift für Ehe und Hausfrieden
|
||||
- Dienerschaft fließt in Hausfrieden ein
|
||||
- Liebschaften und Unterversorgung wirken vollständig auf Hausfrieden
|
||||
|
||||
## Phase C
|
||||
|
||||
- uneheliche Kinder als aktiver Konfliktfaktor
|
||||
- Anerkennungsaktion
|
||||
- genauere Ursachenlisten in der UI
|
||||
|
||||
## 8. Offene Balancing-Punkte
|
||||
|
||||
Diese Werte sind absichtlich noch nicht final:
|
||||
|
||||
- exakte Geldkosten für Ehe-Aktionen
|
||||
- Stärke der Boni für hohe Stände
|
||||
- Stärke des Malus bei sichtbaren Liebschaften
|
||||
- Stärke der Dienerwirkung auf Ehe und Haus
|
||||
|
||||
Die Struktur sollte jetzt aber stabil genug sein, damit UI und Daemon unabhängig voneinander anfangen können.
|
||||
137
docs/FALUKANT_MARRIAGE_HOUSEHOLD_DAEMON_HANDOFF.md
Normal file
137
docs/FALUKANT_MARRIAGE_HOUSEHOLD_DAEMON_HANDOFF.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Falukant: Daemon-Handoff für Ehe und Hausfrieden
|
||||
|
||||
Dieses Dokument beschreibt den Stand nach Phase A.
|
||||
|
||||
## 1. Was im Projekt jetzt vorhanden ist
|
||||
|
||||
Backend-/API-seitig vorhanden:
|
||||
|
||||
- `relationship_state.marriage_satisfaction`
|
||||
- `relationship_state.marriage_public_stability`
|
||||
- aktive Liebschaften mit:
|
||||
- `visibility`
|
||||
- `discretion`
|
||||
- `maintenance_level`
|
||||
- `status_fit`
|
||||
- `months_underfunded`
|
||||
- `acknowledged`
|
||||
- `user_house` mit:
|
||||
- `servant_count`
|
||||
- `servant_quality`
|
||||
- `servant_pay_level`
|
||||
- `household_order`
|
||||
- `household_tension_score`
|
||||
- `household_tension_reasons_json`
|
||||
- Family-API liefert jetzt zusätzlich:
|
||||
- `householdTension`
|
||||
- `householdTensionScore`
|
||||
- `householdTensionReasons`
|
||||
|
||||
Direkte Spieleraktionen vorhanden:
|
||||
|
||||
- `POST /api/falukant/family/marriage/spend-time`
|
||||
- `POST /api/falukant/family/marriage/gift`
|
||||
- `POST /api/falukant/family/marriage/reconcile`
|
||||
- `POST /api/falukant/houses/order`
|
||||
|
||||
## 2. Daily-Input für den externen Daemon
|
||||
|
||||
Pro betroffenem Falukant-User:
|
||||
|
||||
- `falukant_user.id`
|
||||
- `user.id` / `user.hashed_id`
|
||||
- aktive Ehe-`relationship` mit `relationship_state`
|
||||
- aktive Liebschaften mit `relationship_state`
|
||||
- Kinder mit:
|
||||
- `birth_context`
|
||||
- `legitimacy`
|
||||
- `public_known`
|
||||
- Haus mit:
|
||||
- `servant_count`
|
||||
- `servant_quality`
|
||||
- `servant_pay_level`
|
||||
- `household_order`
|
||||
- Charakter mit:
|
||||
- `reputation`
|
||||
- `title_of_nobility`
|
||||
|
||||
## 3. Was der Daemon täglich berechnen soll
|
||||
|
||||
### Ehe
|
||||
|
||||
- Drift von `marriage_satisfaction`
|
||||
- Drift von `marriage_public_stability`
|
||||
- Einfluss aus:
|
||||
- sichtbaren Liebschaften
|
||||
- unterfinanzierten Liebschaften
|
||||
- Standesunterschieden
|
||||
- Dienerschaft / Haushaltsordnung
|
||||
- zu jungen Liebschaften
|
||||
|
||||
### Hausfrieden
|
||||
|
||||
Der Daemon soll intern einen numerischen Spannungswert pflegen oder berechnen:
|
||||
|
||||
- `householdTensionScore` `0..100`
|
||||
|
||||
Einflussfaktoren:
|
||||
|
||||
- sichtbare Liebschaften
|
||||
- anerkannte Liebschaften
|
||||
- unterfinanzierte Liebschaften
|
||||
- Kinder aus Liebschaften
|
||||
- Haushaltsordnung
|
||||
- Dienerschaft
|
||||
- schwache Ehe
|
||||
|
||||
UI-Ableitung:
|
||||
|
||||
- `0..24` => `low`
|
||||
- `25..59` => `medium`
|
||||
- `60..100` => `high`
|
||||
|
||||
## 4. Was der Daemon zurückschreiben soll
|
||||
|
||||
Pflicht:
|
||||
|
||||
- `relationship_state.marriage_satisfaction`
|
||||
- `relationship_state.marriage_public_stability`
|
||||
- `user_house.household_tension_score`
|
||||
- `user_house.household_tension_reasons_json`
|
||||
- lover-state-Felder bei Änderungen:
|
||||
- `visibility`
|
||||
- `discretion`
|
||||
- `months_underfunded`
|
||||
- optional `notes_json` / `flags_json`
|
||||
|
||||
## 5. Socket-/Refresh-Verhalten
|
||||
|
||||
Wenn Daily-/Monthly-Verarbeitung Ehe oder Hausfrieden betrifft:
|
||||
|
||||
- `falukantUpdateFamily` mit `reason: "daily"` oder `reason: "monthly"`
|
||||
- danach `falukantUpdateStatus`
|
||||
|
||||
Wenn ein Sonderereignis entsteht:
|
||||
|
||||
- `reason: "scandal"` zusätzlich
|
||||
|
||||
## 6. Wichtige Phase-A-Regel
|
||||
|
||||
Die neuen Direktaktionen geben nur Sofortimpulse:
|
||||
|
||||
- `spend-time`
|
||||
- `gift`
|
||||
- `reconcile`
|
||||
- `house/order`
|
||||
|
||||
Der Daemon ist weiterhin verantwortlich für:
|
||||
|
||||
- Rückdrift
|
||||
- Gegenkräfte
|
||||
- Langzeiteffekte
|
||||
- Balancing
|
||||
|
||||
Kurz:
|
||||
|
||||
- UI/Backend setzen kleine direkte Impulse
|
||||
- der Daemon bestimmt die dauerhafte Entwicklung
|
||||
431
docs/FALUKANT_NOBILITY_ADVANCEMENT_SPEC.md
Normal file
431
docs/FALUKANT_NOBILITY_ADVANCEMENT_SPEC.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# Falukant: Sozialstatus / Standesaufstieg – Erweiterte Spezifikation
|
||||
|
||||
## 1. Ziel
|
||||
|
||||
Der Aufstieg im Sozialstatus soll ab den höheren Ständen nicht mehr nur von Geld und einzelnen festen Anforderungen abhängen, sondern von einer Mischung aus gesellschaftlicher Stellung, öffentlicher Wahrnehmung und repräsentativem Lebensstandard.
|
||||
|
||||
Gleichzeitig soll das System:
|
||||
|
||||
- die frühen Aufstiege einfach halten
|
||||
- spätere Aufstiege spürbar schwieriger machen
|
||||
- mehr Geldbindung erzeugen
|
||||
- nicht bei jedem Stand dieselben Faktoren verlangen
|
||||
|
||||
## 2. Grundprinzip
|
||||
|
||||
Der Standesaufstieg bleibt ein aktiver Spielzug des Nutzers.
|
||||
|
||||
Das heißt:
|
||||
|
||||
- der Spieler beantragt den nächsten Stand weiterhin aktiv in der UI
|
||||
- das Backend prüft die Voraussetzungen
|
||||
- bei Erfolg wird der Titel erhöht und die Kosten werden abgezogen
|
||||
|
||||
Neu ist:
|
||||
|
||||
- spätere Titel haben einen größeren und variableren Anforderungskatalog
|
||||
- nicht jeder Titel prüft alle möglichen Faktoren
|
||||
- pro nächstem Titel wird nur eine Auswahl relevanter Faktoren verlangt
|
||||
- Kosten und Schwellen steigen schwach exponentiell
|
||||
|
||||
## 3. Bestehender Zustand
|
||||
|
||||
Aktuell:
|
||||
|
||||
- der nächste Titel wird über `level + 1` bestimmt
|
||||
- Anforderungen kommen aus `TitleRequirement`
|
||||
- geprüft werden bisher vor allem:
|
||||
- `money`
|
||||
- `cost`
|
||||
- `branches`
|
||||
- Aufstieg läuft manuell über `POST /api/falukant/nobility`
|
||||
- Cooldown: 7 Tage
|
||||
|
||||
Das bleibt als technisches Grundmuster erhalten.
|
||||
|
||||
## 4. Fachliche Leitentscheidung
|
||||
|
||||
### 4.1 Frühe Titel
|
||||
|
||||
- Erster Aufstieg:
|
||||
- darf weiterhin direkt kaufbar sein
|
||||
- keine komplexen sozialen Bedingungen nötig
|
||||
- Zweiter Aufstieg:
|
||||
- bleibt wie bisher
|
||||
- keine Änderung nötig
|
||||
|
||||
### 4.2 Ab dem dritten relevanten Standessprung
|
||||
|
||||
Ab dann sollen zusätzliche Faktoren einbezogen werden:
|
||||
|
||||
- höchstes bisheriges politisches Amt
|
||||
- höchstes bisheriges kirchliches Amt
|
||||
- Beliebtheit / Ansehen
|
||||
- derzeitiges Haus
|
||||
- Hauszustand
|
||||
- Anzahl Liebhaber / Mätressen
|
||||
|
||||
Wichtig:
|
||||
|
||||
- nicht jeder spätere Stand nutzt alle Faktoren gleichzeitig
|
||||
- pro Titel wird nur eine Auswahl davon aktiv
|
||||
- dadurch bleibt das System lebendig statt schematisch
|
||||
|
||||
## 5. Neue Einflussfaktoren
|
||||
|
||||
## 5.1 Höchstes bisheriges politisches Amt
|
||||
|
||||
Nicht nur das aktuelle Amt zählt, sondern das höchste jemals gehaltene politische Amt.
|
||||
|
||||
Begründung:
|
||||
|
||||
- frühere Machtstellung bleibt gesellschaftlich wirksam
|
||||
- ehemalige Amtsinhaber profitieren weiter vom Status ihrer Laufbahn
|
||||
|
||||
Empfohlene Auswertung:
|
||||
|
||||
- pro `political_office_type.name` existiert ein Rangwert
|
||||
- für den Spieler zählt das Maximum aus aktiven und historischen politischen Ämtern
|
||||
|
||||
## 5.2 Höchstes bisheriges kirchliches Amt
|
||||
|
||||
Analog zur Politik:
|
||||
|
||||
- nicht nur aktuell besetzte Kirchenämter
|
||||
- sondern höchstes jemals erreiches Kirchenamt
|
||||
|
||||
Bevorzugt anhand von:
|
||||
|
||||
- `church_office_type.hierarchy_level`
|
||||
|
||||
## 5.3 Beliebtheit / Ansehen
|
||||
|
||||
Der Stand soll nicht nur gekauft oder verwaltet, sondern auch öffentlich getragen werden.
|
||||
|
||||
Dafür wird genutzt:
|
||||
|
||||
- `character.reputation`
|
||||
|
||||
Das ist bewusst derselbe soziale Hauptwert, der auch an anderen Stellen bereits funktioniert.
|
||||
|
||||
## 5.4 Derzeitiges Haus
|
||||
|
||||
Das aktuelle Haus ist sichtbarer Ausdruck des Standes.
|
||||
|
||||
Relevanter Wert:
|
||||
|
||||
- `house.position`
|
||||
|
||||
Je höher das Haus, desto plausibler ein höherer sozialer Aufstieg.
|
||||
|
||||
## 5.5 Hauszustand
|
||||
|
||||
Nicht nur Hausgröße, auch sein Zustand zählt.
|
||||
|
||||
Zu berücksichtigen:
|
||||
|
||||
- Dach
|
||||
- Wände
|
||||
- Boden
|
||||
- Fenster
|
||||
|
||||
Empfohlener abgeleiteter Wert:
|
||||
|
||||
- `houseConditionAverage = AVG(roofCondition, wallCondition, floorCondition, windowCondition)`
|
||||
|
||||
## 5.6 Anzahl Liebhaber / Mätressen
|
||||
|
||||
Dieser Faktor ist bewusst nicht rein negativ.
|
||||
|
||||
Er soll je nach Stand unterschiedlich gewertet werden:
|
||||
|
||||
- niedrige und mittlere Stände:
|
||||
- viele offene Nebenbeziehungen schaden
|
||||
- höhere Stände:
|
||||
- eine gepflegte repräsentative Nebenbeziehung kann toleriert oder sogar sozial passend wirken
|
||||
- zu viele Beziehungen bleiben aber auch dort schädlich
|
||||
|
||||
Darum soll die Anforderung nicht als starres „je mehr, desto besser/schlechter“ funktionieren, sondern titelabhängig.
|
||||
|
||||
Empfohlene Grundlage:
|
||||
|
||||
- aktive Beziehungen mit Rollen:
|
||||
- `secret_affair`
|
||||
- `lover`
|
||||
- `mistress_or_favorite`
|
||||
|
||||
## 6. Titelabhängige Anforderungssets
|
||||
|
||||
## 6.1 Keine starre Vollprüfung
|
||||
|
||||
Ab den späteren Ständen wird pro nächstem Titel nicht alles geprüft, sondern ein Set aus:
|
||||
|
||||
- Pflichtfaktoren
|
||||
- Auswahlfaktoren
|
||||
|
||||
### Pflichtfaktoren
|
||||
|
||||
Immer:
|
||||
|
||||
- `cost`
|
||||
|
||||
Je nach Stand meistens auch:
|
||||
|
||||
- Mindestansehen oder Hausniveau
|
||||
|
||||
### Auswahlfaktoren
|
||||
|
||||
Aus einem Pool von:
|
||||
|
||||
- Politik
|
||||
- Kirche
|
||||
- Ansehen
|
||||
- Hausposition
|
||||
- Hauszustand
|
||||
- Liebhaber-/Mätressensituation
|
||||
- ggf. weiter weiterhin Niederlassungen
|
||||
|
||||
## 6.2 Titelprofil statt Zufall pro Klick
|
||||
|
||||
Die Auswahl soll nicht bei jedem Aufruf neu würfeln.
|
||||
|
||||
Stattdessen:
|
||||
|
||||
- jeder Zieltitel hat ein fest definiertes Profil
|
||||
- dieses Profil wirkt aber so, als ob nicht immer dieselben gesellschaftlichen Dinge zählen
|
||||
|
||||
Beispiel:
|
||||
|
||||
- Titel A prüft:
|
||||
- Geld
|
||||
- Ansehen
|
||||
- Hausposition
|
||||
- Titel B prüft:
|
||||
- Geld
|
||||
- politisches oder kirchliches Spitzenamt
|
||||
- Hauszustand
|
||||
- Titel C prüft:
|
||||
- Geld
|
||||
- Ansehen
|
||||
- Haus
|
||||
- kontrollierte repräsentative Nebenbeziehung
|
||||
|
||||
Das ist besser als echter Zufall, weil:
|
||||
|
||||
- nachvollziehbar
|
||||
- testbar
|
||||
- balancierbar
|
||||
|
||||
## 7. Schwellenlogik
|
||||
|
||||
## 7.1 Schwach exponentiell steigend
|
||||
|
||||
Die Anforderungen sollen nicht linear, sondern schwach exponentiell steigen.
|
||||
|
||||
Das betrifft vor allem:
|
||||
|
||||
- Kosten
|
||||
- Mindestansehen
|
||||
- Mindesthausniveau
|
||||
- Mindestwert für Amtseinfluss
|
||||
|
||||
Empfohlene Denkweise:
|
||||
|
||||
- frühe Stände: leicht erreichbar
|
||||
- mittlere Stände: merklich teurer
|
||||
- hohe Stände: deutlich selektiver, aber nicht absurd
|
||||
|
||||
## 7.2 Beispielhafte Entwicklung
|
||||
|
||||
### Früh
|
||||
|
||||
- Geld dominiert
|
||||
- kaum oder keine sozialen Zusatzbedingungen
|
||||
|
||||
### Mittel
|
||||
|
||||
- Geld plus 1 bis 2 soziale Bedingungen
|
||||
|
||||
### Hoch
|
||||
|
||||
- Geld plus 2 bis 3 soziale Bedingungen
|
||||
- mindestens eine Repräsentationsbedingung:
|
||||
- Haus oder Hauszustand
|
||||
- mindestens eine Anerkennungsbedingung:
|
||||
- Ansehen oder Amt
|
||||
|
||||
### Sehr hoch
|
||||
|
||||
- Geld plus 3 bis 4 Bedingungen
|
||||
- politische oder kirchliche Laufbahn gewinnt an Gewicht
|
||||
- Haus und Ansehen werden praktisch unverzichtbar
|
||||
|
||||
## 8. Faktorlogik im Detail
|
||||
|
||||
## 8.1 Politik / Kirche
|
||||
|
||||
Für spätere Stände genügt nicht jedes Amt.
|
||||
|
||||
Empfohlen:
|
||||
|
||||
- niedrige hohe Titel:
|
||||
- irgendein relevantes Amt reicht
|
||||
- spätere hohe Titel:
|
||||
- nur hohe Ränge zählen
|
||||
|
||||
Regel:
|
||||
|
||||
- erfüllt, wenn `maxPoliticalRank` oder `maxChurchRank` über Titel-Schwelle liegt
|
||||
|
||||
## 8.2 Beliebtheit
|
||||
|
||||
Empfohlen:
|
||||
|
||||
- frühe soziale Titel: moderate Rufschwelle
|
||||
- hohe Titel: Ruf wird Pflichtfaktor
|
||||
|
||||
## 8.3 Haus
|
||||
|
||||
Empfohlen:
|
||||
|
||||
- `house.position` wird ab mittleren Ständen wichtig
|
||||
- `houseConditionAverage` wird ab späteren Ständen zusätzlich relevant
|
||||
|
||||
Das verhindert:
|
||||
|
||||
- „großes Haus, aber verwahrlost“
|
||||
- oder „hoher Titel im ruinösen Haushalt“
|
||||
|
||||
## 8.4 Liebhaber / Mätressen
|
||||
|
||||
Dieser Faktor soll nur bei manchen Titeln überhaupt aktiv sein.
|
||||
|
||||
Mögliche Logik:
|
||||
|
||||
- niedrige/mittlere Stände:
|
||||
- `0` oder `1` toleriert
|
||||
- `>= 2` negativ
|
||||
- höhere Stände:
|
||||
- genau `1` repräsentative Nebenbeziehung kann neutral oder positiv sein
|
||||
- `0` ist ebenfalls zulässig
|
||||
- `>= 2` oder hohe Sichtbarkeit negativ
|
||||
|
||||
Das eignet sich besonders als optionaler Profilfaktor, nicht als Universalregel.
|
||||
|
||||
## 9. Vorgeschlagene Aufstiegsarchitektur
|
||||
|
||||
## 9.1 Stufe 1
|
||||
|
||||
- direkt kaufbar
|
||||
- nur Kosten
|
||||
|
||||
## 9.2 Stufe 2
|
||||
|
||||
- bleibt wie bisher
|
||||
|
||||
## 9.3 Ab Stufe 3
|
||||
|
||||
Jeder Zieltitel bekommt:
|
||||
|
||||
- `baseCost`
|
||||
- `costExponentFactor`
|
||||
- `requiredChecks`
|
||||
- `optionalCheckPool`
|
||||
- `optionalCheckCount`
|
||||
|
||||
Die Prüfung lautet dann:
|
||||
|
||||
1. Kosten erfüllt
|
||||
2. alle Pflichtchecks erfüllt
|
||||
3. aus dem Auswahlpool mindestens `optionalCheckCount` erfüllt
|
||||
|
||||
Damit bleibt das System flexibel, aber klar.
|
||||
|
||||
## 10. UI-Auswirkung
|
||||
|
||||
Die Adel-/Standesansicht sollte nicht nur „fehlend/erfüllt“ zeigen, sondern künftig:
|
||||
|
||||
- aktive Pflichtanforderungen
|
||||
- optionale Faktoren
|
||||
- wie viele davon erfüllt werden müssen
|
||||
- bereits erfüllte Faktoren optisch markieren
|
||||
|
||||
Beispiel:
|
||||
|
||||
- „Erfülle 2 von 3 gesellschaftlichen Voraussetzungen“
|
||||
|
||||
Dadurch versteht der Nutzer:
|
||||
|
||||
- warum der Aufstieg noch blockiert ist
|
||||
- wo er am effizientesten investieren kann
|
||||
|
||||
## 11. Geldbindung
|
||||
|
||||
Mehr Geldinvestition soll ausdrücklich Teil des Systems sein.
|
||||
|
||||
Darum:
|
||||
|
||||
- jeder höhere Stand hat eine klar steigende Aufstiegskostenbasis
|
||||
- Hauspflege und Hausgröße binden zusätzlich Kapital
|
||||
- politische und kirchliche Karriere kostet indirekt ebenfalls Ressourcen
|
||||
- repräsentative Liebhaber-/Mätressenführung kann bei manchen Profilen als teure, aber hilfreiche soziale Form auftreten
|
||||
|
||||
## 12. Verhältnis zu Daemon und Echtzeit
|
||||
|
||||
Der Standesaufstieg selbst bleibt weiterhin eine direkte Backend-Prüfung beim Spieler-Klick, nicht ein Daily-Daemon-Aufstieg.
|
||||
|
||||
Der Daemon kann aber vorbereitende Werte beeinflussen:
|
||||
|
||||
- Ruf
|
||||
- Hauszustand
|
||||
- aktive Amtshistorie
|
||||
- Beziehungen / Sichtbarkeit
|
||||
|
||||
Das heißt:
|
||||
|
||||
- der Daemon verändert die Voraussetzungen
|
||||
- der eigentliche Standesaufstieg bleibt ein aktiver Kauf-/Antragsvorgang
|
||||
|
||||
## 13. Empfohlene technische Erweiterung
|
||||
|
||||
Die aktuelle reine `TitleRequirement`-Logik ist für das erweiterte Modell zu schmal.
|
||||
|
||||
Empfohlen ist eine zusätzliche Titelprofil-Logik im Backend:
|
||||
|
||||
- je Titel ein Profilobjekt mit:
|
||||
- Pflichtfaktoren
|
||||
- Auswahlfaktoren
|
||||
- Mindestanzahl erfüllter Auswahlfaktoren
|
||||
- Kostenbasis
|
||||
- Progressionsfaktor
|
||||
|
||||
Dabei kann das bestehende Requirements-Modell weiterhin für einfache Titel dienen.
|
||||
|
||||
## 14. Umsetzungsreihenfolge
|
||||
|
||||
### Phase 1
|
||||
|
||||
- ersten Aufstieg unverändert kaufbar lassen
|
||||
- zweiten Aufstieg unverändert lassen
|
||||
- ab späteren Titeln Backend-Profilprüfung einführen
|
||||
|
||||
### Phase 2
|
||||
|
||||
- UI um Pflicht-/Optionsanzeige erweitern
|
||||
- soziale Faktoren sichtbar machen
|
||||
|
||||
### Phase 3
|
||||
|
||||
- Balancing
|
||||
- feinere Titelprofile
|
||||
- stärkere Verzahnung mit Politik, Kirche und Liebschaften
|
||||
|
||||
## 15. Kernaussage
|
||||
|
||||
Das System soll nicht „jeder Titel verlangt alles“ sein, sondern:
|
||||
|
||||
- frühe Aufstiege simpel
|
||||
- spätere Aufstiege gesellschaftlich glaubwürdig
|
||||
- steigende Kosten
|
||||
- wechselnde, aber definierte Faktorprofile
|
||||
- Haus, Ruf, Amt und Nebenbeziehungen werden echte Standeswerkzeuge
|
||||
487
docs/FALUKANT_PRODUCTION_CERTIFICATE_SPEC.md
Normal file
487
docs/FALUKANT_PRODUCTION_CERTIFICATE_SPEC.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# Falukant: Produktionszertifikate – Fach- und Integrationsspezifikation
|
||||
|
||||
## 1. Ziel
|
||||
|
||||
Das Produktionssystem soll stärker an den tatsächlichen gesellschaftlichen und fachlichen Fortschritt eines Spielers gebunden werden. Ein Spieler darf nur Produkte herstellen, deren Zertifikatsstufe seiner aktuellen Produktionsfreigabe entspricht.
|
||||
|
||||
Die Zertifikatsstufe steigt nicht sofort bei jeder Einzelaktion, sondern wird ausschließlich im externen Daemon einmal täglich neu berechnet.
|
||||
|
||||
Dieses Dokument beschreibt:
|
||||
|
||||
- das fachliche Modell der Produktionszertifikate
|
||||
- die Faktoren für Aufstieg und Begrenzung
|
||||
- die Daily-Berechnung im Daemon
|
||||
- die Kommunikation zwischen Daemon und UI
|
||||
- die Einbindung in die bestehende Backend-/UI-Struktur
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Die nötigen DB-Grundlagen sind bereits vorhanden.
|
||||
- Der Daemon muss keine neuen Schemaänderungen erwarten.
|
||||
- Bestehende Felder wie `falukant_data.falukant_user.certificate` und `falukant_type.product.category` bleiben die führende Basis.
|
||||
|
||||
## 2. Bestehende technische Basis
|
||||
|
||||
Bereits vorhanden:
|
||||
|
||||
- `falukant_data.falukant_user.certificate`
|
||||
- aktuelle Produktionsfreigabe des Spielers
|
||||
- `falukant_type.product.category`
|
||||
- erforderliche Zertifikatsstufe des Produkts
|
||||
- `falukant_data.knowledge`
|
||||
- Produktwissen je Charakter und Produkt
|
||||
- `falukant_data.production`
|
||||
- Produktionsvorgänge
|
||||
- `falukant_data.character.reputation`
|
||||
- Ansehen des Spielercharakters
|
||||
- `falukant_data.character.title_of_nobility`
|
||||
- Adelstitel
|
||||
- `falukant_data.user_house.house_type_id`
|
||||
- aktuelles Haus
|
||||
- politische und kirchliche Ämter
|
||||
- `falukant_data.political_office`
|
||||
- `falukant_data.church_office`
|
||||
- `falukant_log.political_office_history`
|
||||
- `falukant_type.church_office_type.hierarchy_level`
|
||||
|
||||
Bestehende Produktfreigabe im Backend:
|
||||
|
||||
- Produkte werden bereits in `falukantService.getProducts()` über `product.category <= user.certificate` gefiltert.
|
||||
|
||||
Das heißt:
|
||||
|
||||
- Zertifikatslogik muss nicht neu in die Produktionsfreigabe eingebaut werden.
|
||||
- Es muss nur die Berechnung und Fortschreibung von `falukant_user.certificate` sauber geregelt werden.
|
||||
|
||||
## 3. Fachmodell
|
||||
|
||||
### 3.1 Zertifikatsstufen
|
||||
|
||||
Die Zertifikatsstufe bleibt eine einfache ganze Zahl im Feld `falukant_user.certificate`.
|
||||
|
||||
Empfohlene Bedeutung:
|
||||
|
||||
| Stufe | Bedeutung |
|
||||
|------|-----------|
|
||||
| `1` | Grundproduktion, einfache Güter |
|
||||
| `2` | Gehobene Alltagsproduktion |
|
||||
| `3` | Fortgeschrittene Manufaktur |
|
||||
| `4` | Anspruchsvolle Qualitätsproduktion |
|
||||
| `5` | Hochwertige oder prestigegebundene Produktion |
|
||||
|
||||
Wenn im Typensystem bereits höhere `product.category`-Werte existieren, gilt dieselbe Logik entsprechend weiter.
|
||||
|
||||
### 3.2 Führungsprinzip
|
||||
|
||||
Die Zertifikatsstufe ist kein reines Wissenslevel.
|
||||
|
||||
Sie soll ausdrücken, ob ein Haushalt/Betrieb gesellschaftlich und fachlich als ausreichend etabliert gilt, um komplexere Produktion zu verantworten.
|
||||
|
||||
Darum fließen mehrere Faktoren ein:
|
||||
|
||||
- Durchschnittliches Produktwissen
|
||||
- Anzahl abgeschlossener Produktionen
|
||||
- höchstes politisches oder kirchliches Amt
|
||||
- Adelstitel
|
||||
- Ansehen
|
||||
- derzeitiges Haus
|
||||
|
||||
## 4. Berechnungslogik
|
||||
|
||||
## 4.1 Grundidee
|
||||
|
||||
Der Daemon berechnet einmal täglich einen `certificateScore`.
|
||||
|
||||
Aus diesem `certificateScore` wird eine Zielstufe `targetCertificate` abgeleitet.
|
||||
|
||||
Die gespeicherte Stufe `falukant_user.certificate` wird dann höchstens um `+1` pro Tag angehoben. Senkungen sind optional und in der ersten Version nicht vorgesehen.
|
||||
|
||||
Dadurch gilt:
|
||||
|
||||
- Aufstieg ist spürbar, aber nicht sprunghaft
|
||||
- kurzfristige Schwankungen führen nicht zu hektischen Freischaltungen
|
||||
- Balancing bleibt später leichter
|
||||
|
||||
## 4.2 Eingangsgrößen
|
||||
|
||||
Für jeden Spielercharakter mit `falukant_user`:
|
||||
|
||||
- `avgKnowledge`
|
||||
- Durchschnitt aus `falukant_data.knowledge.knowledge` des Spielercharakters
|
||||
- `completedProductions`
|
||||
- Anzahl abgeschlossener Produktionen des Spielers
|
||||
- `highestPoliticalOfficeRank`
|
||||
- höchster politischer Amtsrang
|
||||
- `highestChurchOfficeRank`
|
||||
- höchster kirchlicher Amtsrang
|
||||
- `highestOfficeRank`
|
||||
- Maximum aus politischem und kirchlichem Rang
|
||||
- `nobilityLevel`
|
||||
- aus `title_of_nobility`
|
||||
- `reputation`
|
||||
- aus `character.reputation`
|
||||
- `housePosition`
|
||||
- aus `house.position`
|
||||
|
||||
## 4.3 Normalisierung der Faktoren
|
||||
|
||||
### Produktwissen
|
||||
|
||||
`knowledgePoints`:
|
||||
|
||||
- `0`, wenn `avgKnowledge < 20`
|
||||
- `1`, wenn `avgKnowledge >= 20`
|
||||
- `2`, wenn `avgKnowledge >= 35`
|
||||
- `3`, wenn `avgKnowledge >= 50`
|
||||
- `4`, wenn `avgKnowledge >= 65`
|
||||
- `5`, wenn `avgKnowledge >= 80`
|
||||
|
||||
### Produktionsmenge
|
||||
|
||||
`productionPoints`:
|
||||
|
||||
- `0`, wenn `completedProductions < 5`
|
||||
- `1`, wenn `completedProductions >= 5`
|
||||
- `2`, wenn `completedProductions >= 20`
|
||||
- `3`, wenn `completedProductions >= 50`
|
||||
- `4`, wenn `completedProductions >= 100`
|
||||
- `5`, wenn `completedProductions >= 200`
|
||||
|
||||
### Politische / kirchliche Stellung
|
||||
|
||||
`officePoints`:
|
||||
|
||||
- politische Ämter:
|
||||
- über definierte Mapping-Tabelle im Daemon von `office_type.name -> rank`
|
||||
- kirchliche Ämter:
|
||||
- bevorzugt `church_office_type.hierarchy_level`
|
||||
- dann:
|
||||
- `highestOfficeRank = max(highestPoliticalOfficeRank, highestChurchOfficeRank)`
|
||||
- `officePoints = min(5, highestOfficeRank)`
|
||||
|
||||
Empfehlung für politische Mapping-Tabelle:
|
||||
|
||||
- einfache Kommunalämter: `1`
|
||||
- regionale Ämter: `2`
|
||||
- hohe Regionalämter: `3`
|
||||
- reichs- oder königsnahe Spitzenämter: `4` bis `5`
|
||||
|
||||
Das Mapping lebt im Daemon und kann balanciert werden, ohne DB-Änderungen.
|
||||
|
||||
### Adel
|
||||
|
||||
`nobilityPoints`:
|
||||
|
||||
- aus `title_of_nobility.level`
|
||||
- `nobilityPoints = clamp(level - 1, 0, 5)`
|
||||
|
||||
### Ansehen
|
||||
|
||||
`reputationPoints`:
|
||||
|
||||
- `0`, wenn `reputation < 20`
|
||||
- `1`, wenn `reputation >= 20`
|
||||
- `2`, wenn `reputation >= 40`
|
||||
- `3`, wenn `reputation >= 60`
|
||||
- `4`, wenn `reputation >= 75`
|
||||
- `5`, wenn `reputation >= 90`
|
||||
|
||||
### Haus
|
||||
|
||||
`housePoints`:
|
||||
|
||||
- aus `house.position`
|
||||
- Vorschlag:
|
||||
- `0`, wenn `position <= 1`
|
||||
- `1`, wenn `position >= 2`
|
||||
- `2`, wenn `position >= 4`
|
||||
- `3`, wenn `position >= 6`
|
||||
- `4`, wenn `position >= 8`
|
||||
- `5`, wenn `position >= 10`
|
||||
|
||||
Die genauen Schwellen können im Balancing später angepasst werden.
|
||||
|
||||
## 4.4 Gesamtwert
|
||||
|
||||
Der Daemon berechnet:
|
||||
|
||||
```text
|
||||
certificateScore =
|
||||
knowledgePoints * 0.45 +
|
||||
productionPoints * 0.30 +
|
||||
officePoints * 0.08 +
|
||||
nobilityPoints * 0.05 +
|
||||
reputationPoints * 0.07 +
|
||||
housePoints * 0.05
|
||||
```
|
||||
|
||||
Zusätzlich gelten Mindestanforderungen je Stufe.
|
||||
|
||||
Balancing-Grundsatz:
|
||||
|
||||
- frühe und mittlere Zertifikate sollen primär über Wissen und Produktionspraxis erreichbar sein
|
||||
- gesellschaftliche Faktoren wirken vor allem als Beschleuniger oder als Zugang zu hohen Zertifikaten
|
||||
- vorübergehende wirtschaftliche Verlustphasen blockieren den normalen Aufstieg nicht automatisch
|
||||
- ein normaler Produktionsverlust ist kein Downgrade-Grund
|
||||
|
||||
## 4.5 Mindestanforderungen je Zertifikatsstufe
|
||||
|
||||
Eine höhere Zielstufe darf nur erreicht werden, wenn neben dem `certificateScore` auch harte Mindestgrenzen erfüllt sind.
|
||||
|
||||
### Für Zertifikat 2
|
||||
|
||||
- `avgKnowledge >= 15`
|
||||
- `completedProductions >= 4`
|
||||
|
||||
### Für Zertifikat 3
|
||||
|
||||
- `avgKnowledge >= 28`
|
||||
- `completedProductions >= 15`
|
||||
- kein harter Statuszwang
|
||||
- Statusfaktoren dürfen den Score verbessern, sind hier aber noch nicht Pflicht
|
||||
|
||||
### Für Zertifikat 4
|
||||
|
||||
- `avgKnowledge >= 45`
|
||||
- `completedProductions >= 45`
|
||||
- mindestens einer der Statusfaktoren erfüllt:
|
||||
- `officePoints >= 1`
|
||||
- oder `nobilityPoints >= 1`
|
||||
- oder `reputationPoints >= 2`
|
||||
- oder `housePoints >= 2`
|
||||
|
||||
### Für Zertifikat 5
|
||||
|
||||
- `avgKnowledge >= 60`
|
||||
- `completedProductions >= 110`
|
||||
- `reputationPoints >= 2`
|
||||
- mindestens zwei der folgenden:
|
||||
- `officePoints >= 2`
|
||||
- `nobilityPoints >= 1`
|
||||
- `housePoints >= 2`
|
||||
|
||||
## 4.6 Ableitung der Zielstufe
|
||||
|
||||
Vorschlag:
|
||||
|
||||
- `targetCertificate = 1`, wenn `certificateScore < 0.9`
|
||||
- `targetCertificate = 2`, wenn `certificateScore >= 0.9`
|
||||
- `targetCertificate = 3`, wenn `certificateScore >= 1.8`
|
||||
- `targetCertificate = 4`, wenn `certificateScore >= 2.8`
|
||||
- `targetCertificate = 5`, wenn `certificateScore >= 3.8`
|
||||
|
||||
Danach werden die Mindestanforderungen geprüft.
|
||||
|
||||
Wenn eine Schwelle rechnerisch erreicht ist, die Mindestanforderungen aber fehlen, bleibt der Spieler auf der niedrigeren Zielstufe.
|
||||
|
||||
## 4.7 Fortschreibung
|
||||
|
||||
Daily-Regel:
|
||||
|
||||
- wenn `targetCertificate > currentCertificate`
|
||||
- dann `newCertificate = currentCertificate + 1`
|
||||
- sonst
|
||||
- `newCertificate = currentCertificate`
|
||||
|
||||
Für die erste Version keine automatische Herabstufung.
|
||||
|
||||
Ausnahmen, die bereits im Daemon berücksichtigt werden dürfen:
|
||||
|
||||
- `Bankrott`
|
||||
- Wenn der Spieler wirtschaftlich zusammenbricht, darf das Zertifikat gesenkt werden.
|
||||
- Die genaue Definition von Bankrott lebt im Daemon bzw. im Wirtschaftssystem.
|
||||
- `Tod ohne Kinder`
|
||||
- Stirbt der Spielercharakter und es gibt keinen erbberechtigten Nachfolger, darf das Zertifikat auf den Grundzustand des Nachfolgesystems bzw. auf eine definierte niedrige Stufe zurückgesetzt werden.
|
||||
- Dieser Fall darf bereits jetzt im Daemon umgesetzt werden.
|
||||
|
||||
Nicht vorgesehen für die erste Version:
|
||||
|
||||
- Downgrade wegen normaler Alltagsschwankungen
|
||||
- Downgrade wegen vorübergehend schlechter Werte bei Wissen, Ruf, Haus oder Amt
|
||||
|
||||
## 5. Daemon-Verhalten
|
||||
|
||||
## 5.1 Ausführungszeitpunkt
|
||||
|
||||
Die Zertifikatsprüfung läuft ausschließlich im Daily-Tick.
|
||||
|
||||
Nicht bei:
|
||||
|
||||
- Produktionsstart
|
||||
- Produktionsende
|
||||
- Wissensänderung
|
||||
- Hauswechsel
|
||||
- Amtswechsel
|
||||
|
||||
Diese Aktionen verändern nur die Eingangsgrößen. Die eigentliche Zertifikatsanpassung erfolgt erst im nächsten Daily-Lauf.
|
||||
|
||||
## 5.2 Daemon-Hinweis
|
||||
|
||||
Für den Daemon gilt ausdrücklich:
|
||||
|
||||
- die relevanten DB-Felder sind bereits vorhanden
|
||||
- es müssen für diese Funktion keine zusätzlichen Schemaänderungen mehr eingeplant werden
|
||||
- der Daemon soll direkt mit den vorhandenen Tabellen arbeiten
|
||||
|
||||
## 5.3 Empfohlener Daily-Ablauf
|
||||
|
||||
Für jeden Spieler mit `falukant_user`:
|
||||
|
||||
1. Spielercharakter laden
|
||||
2. `avgKnowledge` berechnen
|
||||
3. Anzahl abgeschlossener Produktionen laden
|
||||
4. höchstes aktives oder historisches politisches Amt ermitteln
|
||||
5. höchstes aktives kirchliches Amt ermitteln
|
||||
6. Adelstitel, Ruf und Haus laden
|
||||
7. `certificateScore` und `targetCertificate` berechnen
|
||||
8. falls Aufstieg möglich:
|
||||
- `falukant_user.certificate` um genau `+1` erhöhen
|
||||
9. Event an UI senden
|
||||
|
||||
Balancing-Hinweis für den Daemon:
|
||||
|
||||
- Wenn ein Spieler bereits regelmäßig produziert und verkauft, soll Zertifikat `2` früh stabil erreichbar sein.
|
||||
- Zertifikat `3` soll bei solider Produktionspraxis und mittlerem Wissen ebenfalls ohne hohen Adels- oder Amtsstatus erreichbar sein.
|
||||
- Hohe gesellschaftliche Faktoren sollen vor allem Zertifikat `4` und `5` deutlich erleichtern, nicht Zertifikat `2` und `3` künstlich blockieren.
|
||||
- Reine Verlustphasen in der Geldhistorie sind kein eigener Gegenfaktor, solange kein echter Bankrottfall vorliegt.
|
||||
|
||||
## 6. Event-Kommunikation zwischen Daemon und UI
|
||||
|
||||
## 6.1 Neues Event
|
||||
|
||||
Zusätzlich zum allgemeinen `falukantUpdateStatus` wird ein gezieltes Zertifikats-Event empfohlen:
|
||||
|
||||
### `falukantUpdateProductionCertificate`
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateProductionCertificate",
|
||||
"user_id": 123,
|
||||
"reason": "daily_recalculation",
|
||||
"old_certificate": 2,
|
||||
"new_certificate": 3
|
||||
}
|
||||
```
|
||||
|
||||
`reason` ist in der ersten Version fest:
|
||||
|
||||
- `daily_recalculation`
|
||||
|
||||
## 6.2 Wann senden
|
||||
|
||||
Wenn sich die Zertifikatsstufe geändert hat:
|
||||
|
||||
1. `falukantUpdateProductionCertificate`
|
||||
2. danach `falukantUpdateStatus`
|
||||
|
||||
Wenn sich die Stufe nicht geändert hat:
|
||||
|
||||
- kein spezielles Zertifikats-Event nötig
|
||||
- normales `falukantUpdateStatus` bleibt anderen Systemen vorbehalten
|
||||
|
||||
## 6.3 UI-Reaktion
|
||||
|
||||
### BranchView
|
||||
|
||||
Bei `falukantUpdateProductionCertificate`:
|
||||
|
||||
- Produkte neu laden
|
||||
- Produktionsbereich neu laden
|
||||
- optional kurzer Hinweis:
|
||||
- „Neues Handelszertifikat erreicht“
|
||||
|
||||
### OverviewView
|
||||
|
||||
Bei `falukantUpdateProductionCertificate`:
|
||||
|
||||
- Falukant-Status neu laden
|
||||
- Produktionsüberblick neu laden
|
||||
- Zertifikatsaufstieg visuell hervorheben
|
||||
|
||||
### StatusBar / DashboardWidget
|
||||
|
||||
- auf denselben Nutzer filtern
|
||||
- Zertifikat/Produktionsstatus neu laden
|
||||
|
||||
## 6.4 Deduplizierung
|
||||
|
||||
Da direkt nach dem Zertifikats-Event oft `falukantUpdateStatus` folgt, soll die UI wie bei anderen Falukant-Events entprellen:
|
||||
|
||||
- beide Events dürfen denselben kurzen Refresh-Puffer nutzen
|
||||
- ein Zertifikatsaufstieg darf keinen doppelten Reload-Sturm auslösen
|
||||
|
||||
## 7. API- und UI-Empfehlungen
|
||||
|
||||
## 7.1 Sichtbare Anzeige
|
||||
|
||||
Die UI sollte mittelfristig anzeigen:
|
||||
|
||||
- aktuelle Zertifikatsstufe
|
||||
- nächste Stufe
|
||||
- Fortschrittsfaktoren
|
||||
- Wissen
|
||||
- Produktionen
|
||||
- Amt
|
||||
- Adel
|
||||
- Ruf
|
||||
- Haus
|
||||
|
||||
## 7.2 Empfohlene Backend-Ausgabe
|
||||
|
||||
Zusätzlich zur bestehenden User-/Overview-API ist später sinnvoll:
|
||||
|
||||
- `certificate`
|
||||
- `nextCertificate`
|
||||
- `certificateFactors`
|
||||
- `avgKnowledge`
|
||||
- `completedProductions`
|
||||
- `highestOfficeRank`
|
||||
- `nobilityLevel`
|
||||
- `reputation`
|
||||
- `housePosition`
|
||||
|
||||
Das ist für die erste Daemon-Integration aber optional.
|
||||
|
||||
## 8. Balancing-Hinweis
|
||||
|
||||
Die genannten Schwellen und Gewichte sind bewusst als Spielregelrahmen zu verstehen, nicht als endgültiges Balancing.
|
||||
|
||||
Für die erste produktive Version gilt:
|
||||
|
||||
- keine zusätzlichen Stufen oder Nebensysteme
|
||||
- keine normale Herabstufung im Alltagsbetrieb
|
||||
- Herabstufung nur in Sonderfällen wie `Bankrott` oder `Tod ohne Kinder`
|
||||
- Daily-Aufstieg maximal `+1`
|
||||
|
||||
Balancing erst nach Live-Erfahrung.
|
||||
|
||||
## 9. Umsetzungsreihenfolge
|
||||
|
||||
### P1
|
||||
|
||||
- Daemon: Daily-Berechnung von `certificate`
|
||||
- Event `falukantUpdateProductionCertificate`
|
||||
- UI: gezielter Refresh in Branch/Overview
|
||||
|
||||
### P2
|
||||
|
||||
- UI: Sichtbarer Zertifikatsstatus und Aufstiegshinweis
|
||||
- Backend/API: optionale Faktor-Ausgabe
|
||||
|
||||
### P3
|
||||
|
||||
- Balancing
|
||||
- feinere Sonderfallregeln für `Bankrott`
|
||||
- feinere politische Mapping-Tabelle
|
||||
|
||||
## 10. Done-Kriterien
|
||||
|
||||
Fertig ist die erste Version, wenn:
|
||||
|
||||
- nur Produkte mit `product.category <= falukant_user.certificate` produzierbar sind
|
||||
- der Daemon die Zertifikatsprüfung genau einmal täglich ausführt
|
||||
- der Daemon bei Aufstieg das Zertifikat fortschreibt
|
||||
- die UI auf das Zertifikats-Event gezielt reagiert
|
||||
- keine neuen DB-Änderungen für diese Funktion nötig sind
|
||||
336
docs/FALUKANT_SERVANTS_CONCEPT.md
Normal file
336
docs/FALUKANT_SERVANTS_CONCEPT.md
Normal file
@@ -0,0 +1,336 @@
|
||||
# Falukant: Konzept Dienerschaft
|
||||
|
||||
Dieses Dokument beschreibt ein eigenständiges Dienerschaftssystem für Falukant. Die Dienerschaft hängt bewusst am Haus und nicht primär an Familie oder Liebschaften.
|
||||
|
||||
## 1. Grundentscheidung
|
||||
|
||||
Dienerschaft ist Teil des Hausstands.
|
||||
|
||||
Warum:
|
||||
- Diener versorgen Haushalt, Gebäude, Gäste und Repräsentation.
|
||||
- Die Größe und Qualität der Dienerschaft hängt stärker an Hausgröße und Stand als an einzelnen Familienbeziehungen.
|
||||
- Spätere Systeme wie Diskretion, Skandalabwehr, Botengänge, Schutz und Festkultur lassen sich so an einer Stelle bündeln.
|
||||
|
||||
Folgerung:
|
||||
- Hauptansicht: `HouseView`
|
||||
- Datenträger: `user_house` plus eigene Dienerstruktur
|
||||
- Familie, Liebschaften, Ruf und Untergrund nutzen die Effekte mit, besitzen das System aber nicht selbst.
|
||||
|
||||
## 2. Spielziel
|
||||
|
||||
Die Dienerschaft soll vier Dinge leisten:
|
||||
- laufende Kosten und Standesdruck erzeugen
|
||||
- Komfort und Ordnung des Haushalts darstellen
|
||||
- Repräsentation und Ansehen beeinflussen
|
||||
- Diskretion und Risiko in Familien- und Liebschaftsfragen mitsteuern
|
||||
|
||||
Die erste Ausbaustufe bleibt bewusst einfach und abstrahiert. Einzelne Namen oder tiefes Personalmanagement kommen erst später.
|
||||
|
||||
## 3. Kernmodell
|
||||
|
||||
### 3.1 Erste Ausbaustufe: abstrakte Dienerschaft
|
||||
|
||||
Der Spieler verwaltet keine einzelnen Diener, sondern einen Haushalt mit wenigen Zuständen:
|
||||
- `servantCount`
|
||||
- `servantQuality`
|
||||
- `servantPayLevel`
|
||||
- `householdOrder`
|
||||
|
||||
Empfohlene Bedeutung:
|
||||
- `servantCount`: tatsächliche Zahl der Bediensteten
|
||||
- `servantQuality`: Ausbildungs- und Verlässlichkeitsniveau
|
||||
- `servantPayLevel`: wie gut der Haushalt bezahlt und versorgt wird
|
||||
- `householdOrder`: Ergebniswert für Disziplin, Sauberkeit, Organisation
|
||||
|
||||
### 3.2 Spätere Ausbaustufe
|
||||
|
||||
Erst später werden Rollen differenziert:
|
||||
- Hausverwalter / Haushofmeister
|
||||
- Kammerdiener / Zofen
|
||||
- Küchenpersonal
|
||||
- Stallpersonal
|
||||
- Kinder- und Pflegepersonal
|
||||
- Wachen / Torpersonal
|
||||
|
||||
Diese zweite Stufe ist ausdrücklich nicht Teil des ersten Implementierungspakets.
|
||||
|
||||
## 4. Verbindung zum Haus
|
||||
|
||||
Die Dienerschaft ist an das Haus gekoppelt.
|
||||
|
||||
Das Haus bestimmt:
|
||||
- maximal sinnvolle Dienerzahl
|
||||
- erwartete Mindestzahl je nach Stand
|
||||
- Ansehenswirkung von Über- oder Unterbesetzung
|
||||
- Kostenfaktor
|
||||
|
||||
Ein kleines Haus mit zu großer Dienerschaft wirkt verschwenderisch.
|
||||
Ein großes oder nobles Haus mit zu wenig Dienern wirkt ungeordnet, geizig oder standeswidrig.
|
||||
|
||||
## 5. Haus- und Standeslogik
|
||||
|
||||
Die Zielgröße der Dienerschaft entsteht aus zwei Faktoren:
|
||||
- Hausgröße / Haustyp
|
||||
- gesellschaftlicher Stand
|
||||
|
||||
### 5.1 Erwartungswert
|
||||
|
||||
Jeder Haushalt hat einen erwarteten Bereich:
|
||||
- `expectedServantsMin`
|
||||
- `expectedServantsMax`
|
||||
|
||||
Dieser Bereich wird aus Haus und Titel abgeleitet.
|
||||
|
||||
Beispielhafte Richtung:
|
||||
- Holzhaus, niedriger Stand: 0 bis 1
|
||||
- kleines Familienhaus: 1 bis 3
|
||||
- Stadthaus oder höherer Adel: 3 bis 8
|
||||
- Hochadel und Hofnähe: deutlich darüber
|
||||
|
||||
Wichtig:
|
||||
- Das sind keine finalen Balancing-Zahlen.
|
||||
- Das Balancing bleibt eine spätere Phase.
|
||||
|
||||
## 6. Zentrale Spielwerte
|
||||
|
||||
### 6.1 Dienerzahl
|
||||
|
||||
Die Dienerzahl ist der wichtigste Primärwert.
|
||||
|
||||
Zu wenig Diener:
|
||||
- schlechtere Haushaltsordnung
|
||||
- negativer Einfluss auf Ansehen in hohen Ständen
|
||||
- höhere Spannungen im Haus
|
||||
- weniger Diskretion und schwächerer Schutz vor Gerüchten
|
||||
|
||||
Zu viele Diener:
|
||||
- unnötige Kosten
|
||||
- bei niedrigem Stand möglicher Vorwurf von Verschwendung oder Anmaßung
|
||||
- höheres Risiko für Klatsch, weil mehr Personen Wissen tragen
|
||||
|
||||
### 6.2 Qualität
|
||||
|
||||
Qualität beschreibt Verlässlichkeit und Niveau.
|
||||
|
||||
Niedrige Qualität:
|
||||
- Haus funktioniert nur grob
|
||||
- Diskretion schlecht
|
||||
- Feste und Repräsentation schwächer
|
||||
- höheres Risiko für Gerede, Unordnung, Pannen
|
||||
|
||||
Hohe Qualität:
|
||||
- besserer Hauszustand im Alltag
|
||||
- stärkere Diskretion
|
||||
- besserer Eindruck bei Gästen
|
||||
- positive Wirkung auf Ehekomfort und Familienruhe
|
||||
|
||||
### 6.3 Bezahlung
|
||||
|
||||
Die Bezahlung ist ein Steuerungswert.
|
||||
|
||||
Niedrige Bezahlung:
|
||||
- spart kurzfristig Geld
|
||||
- senkt Loyalität und Qualität
|
||||
- erhöht Gerüchte- und Diebstahlrisiko
|
||||
|
||||
Hohe Bezahlung:
|
||||
- kostet mehr
|
||||
- verbessert Loyalität, Qualität und Diskretion
|
||||
|
||||
### 6.4 Haushaltsordnung
|
||||
|
||||
`householdOrder` ist ein abgeleiteter Zustand.
|
||||
|
||||
Er hängt ab von:
|
||||
- Dienerzahl im Verhältnis zur Sollgröße
|
||||
- Qualität
|
||||
- Bezahlung
|
||||
- Hauszustand
|
||||
|
||||
Auswirkungen:
|
||||
- bessere Ordnung stabilisiert Ehe- und Familienwerte
|
||||
- schlechte Ordnung verschlechtert Komfort und Ansehen
|
||||
- sie beeinflusst spätere Fest- und Besuchssysteme
|
||||
|
||||
## 7. Systemwirkungen
|
||||
|
||||
### 7.1 Geld
|
||||
|
||||
Dienerschaft erzeugt laufende Kosten.
|
||||
|
||||
Monatliche Kosten hängen ab von:
|
||||
- Dienerzahl
|
||||
- Qualitätsniveau
|
||||
- Bezahlungsstufe
|
||||
- Hausgröße
|
||||
|
||||
Später kann darin auch Nahrung, Kleidung und Ausstattung enthalten sein.
|
||||
|
||||
### 7.2 Ansehen
|
||||
|
||||
Ansehen wird nicht direkt nur durch „mehr Diener = besser“ berechnet.
|
||||
|
||||
Stattdessen wirkt:
|
||||
- Passung zum Stand
|
||||
- Ordnung und Auftreten
|
||||
- offensichtliche Unterversorgung
|
||||
- offensichtliche Verschwendung
|
||||
|
||||
Faustregel:
|
||||
- hohe Stände werden stärker nach Hausführung beurteilt
|
||||
- niedrige Stände dürfen einfacher leben
|
||||
- extreme Abweichungen nach oben oder unten wirken negativ
|
||||
|
||||
### 7.3 Familie und Ehe
|
||||
|
||||
Die Familie nutzt die Hauswirkung mit.
|
||||
|
||||
Positive Effekte guter Dienerschaft:
|
||||
- mehr Komfort
|
||||
- geringere Alltagsbelastung
|
||||
- bessere Ehezufriedenheit
|
||||
- geringerer Haushaltsstress
|
||||
|
||||
Negative Effekte schlechter Dienerschaft:
|
||||
- Unruhe im Haus
|
||||
- Streit über Kosten und Ordnung
|
||||
- zusätzliche Spannungen bei Ehe und Kindern
|
||||
|
||||
### 7.4 Liebschaften und Skandale
|
||||
|
||||
Dienerschaft beeinflusst Diskretion.
|
||||
|
||||
Gut bezahlte, loyale und kleine, passende Dienerschaft:
|
||||
- schützt Geheimnisse besser
|
||||
- senkt Skandal- und Gerüchterisiko
|
||||
|
||||
Unzufriedene oder zu große Dienerschaft:
|
||||
- erhöht Klatsch
|
||||
- macht verdeckte Beziehungen sichtbarer
|
||||
- verbessert die Chancen von Untergrundaktivitäten, etwas aufzudecken
|
||||
|
||||
### 7.5 Untergrund / Aufdeckung
|
||||
|
||||
Das Untergrundsystem soll später auf Dienerschaft zugreifen können.
|
||||
|
||||
Beispiel:
|
||||
- unzufriedenes Personal erhöht Erfolg bei `investigate_affair`
|
||||
- sehr diskreter Haushalt erschwert Aufdeckung und Erpressung
|
||||
|
||||
## 8. Standeslogik
|
||||
|
||||
Die Bewertung der Dienerschaft ist standesabhängig.
|
||||
|
||||
### Niedrige Stände
|
||||
|
||||
Erlaubt:
|
||||
- kleine oder keine Dienerschaft
|
||||
|
||||
Negativ:
|
||||
- zu große Dienerschaft bei kleinem Haus
|
||||
- demonstrative Übertreibung
|
||||
|
||||
### Mittlere Stände
|
||||
|
||||
Erwartet:
|
||||
- geordneter kleiner Haushalt
|
||||
- passende Grundversorgung
|
||||
|
||||
Negativ:
|
||||
- sichtbare Unordnung
|
||||
- geizige Unterbesetzung
|
||||
- übertriebener Luxus
|
||||
|
||||
### Hohe Stände
|
||||
|
||||
Erwartet:
|
||||
- repräsentative, funktionierende Dienerschaft
|
||||
|
||||
Negativ:
|
||||
- zu wenig Personal
|
||||
- schlechter Hauszustand trotz Rang
|
||||
- öffentlich erkennbare Überforderung im Haushalt
|
||||
|
||||
## 9. UI-Richtung
|
||||
|
||||
Die erste Oberfläche gehört in `HouseView`.
|
||||
|
||||
Empfohlene Elemente:
|
||||
- Überblickskarte „Dienerschaft“
|
||||
- Ist-Zahl, Sollbereich, Qualität, Bezahlungsstufe, Haushaltsordnung
|
||||
- einfache Aktionen:
|
||||
- Diener einstellen
|
||||
- Diener entlassen
|
||||
- Bezahlung anheben
|
||||
- Bezahlung senken
|
||||
|
||||
Zusätzliche Anzeigen:
|
||||
- erwarteter Bereich nach Haus und Stand
|
||||
- Monatskosten
|
||||
- Haupteffekte auf Ordnung, Ansehen und Diskretion
|
||||
|
||||
Wichtig:
|
||||
- kein Mikromanagement pro Diener in der ersten Version
|
||||
- keine Personallisten im MVP
|
||||
|
||||
## 10. Daemon-/Tick-Sicht
|
||||
|
||||
Die eigentliche Veränderung der Zustände soll durch den externen Daemon laufen.
|
||||
|
||||
Daily:
|
||||
- Drift von Loyalität und Ordnung
|
||||
- kleine Folgen schlechter Versorgung
|
||||
- Diskretionswirkung auf Familien- und Liebschaftssysteme
|
||||
|
||||
Monthly:
|
||||
- Kosten abbuchen
|
||||
- Unterversorgung bewerten
|
||||
- Qualität und Loyalität nachziehen
|
||||
- Ansehenswirkung aus Passung und Ordnung anwenden
|
||||
|
||||
## 11. MVP-Schnitt
|
||||
|
||||
Erste spielbare Version:
|
||||
- Dienerschaft ist ein Hauswert
|
||||
- nur aggregierte Werte, keine Einzelrollen
|
||||
- UI in `HouseView`
|
||||
- monatliche Kosten
|
||||
- grobe Effekte auf:
|
||||
- Haushaltsordnung
|
||||
- Ansehen
|
||||
- Ehezufriedenheit
|
||||
- Diskretion bei Liebschaften
|
||||
|
||||
Noch nicht im MVP:
|
||||
- benannte Diener
|
||||
- Intrigen einzelner Bediensteter
|
||||
- eigene Dienerereignisse mit langen Ketten
|
||||
- tiefes Rollenmanagement
|
||||
|
||||
## 12. Spätere Ausbauten
|
||||
|
||||
Später interessant:
|
||||
- Dienerschaft als Voraussetzung für bestimmte Feste
|
||||
- Spezialrollen wie Amme, Leibdiener, Spion im Haushalt
|
||||
- interne Konflikte unter Dienern
|
||||
- Diebstahl, Bestechung, Illoyalität
|
||||
- Hauspersonal als Quelle für Gerüchte oder Schutz
|
||||
- Untergrund kann Personal bestechen
|
||||
|
||||
## 13. Offene Designentscheidungen
|
||||
|
||||
1. Soll die erste Version mit einer absoluten `servantCount` arbeiten oder mit Stufen wie klein / passend / groß?
|
||||
2. Soll `householdOrder` direkt gespeichert oder komplett aus anderen Werten berechnet werden?
|
||||
3. Soll Bezahlung als Prozentwert, feste Stufe oder Freitext-Enum geführt werden?
|
||||
4. Wie stark soll Dienerschaft bereits in der ersten Version auf Liebschaften und Untergrund wirken?
|
||||
5. Sollen Feste weiter ihr eigenes `servantRatio` behalten oder später an das neue System angebunden werden?
|
||||
|
||||
## 14. Empfehlung
|
||||
|
||||
Empfohlene erste Umsetzung:
|
||||
- `servantCount` als absolute Zahl
|
||||
- `servantQuality` als einfacher Wert 0 bis 100
|
||||
- `servantPayLevel` als feste Stufen `low`, `normal`, `high`
|
||||
- `householdOrder` als gespeicherter, vom Daemon gepflegter Zustand
|
||||
|
||||
Diese Variante ist einfach genug für ein erstes Spielsystem, aber stark genug, um später Familie, Ruf, Untergrund und Feste daran anzuschließen.
|
||||
636
docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md
Normal file
636
docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md
Normal file
@@ -0,0 +1,636 @@
|
||||
# Falukant: Dienerschaft – Daemon-, Technik- und Umsetzungs-Spezifikation
|
||||
|
||||
Dieses Dokument bündelt die umsetzungsreife Spezifikation für das Dienerschaftssystem in einer Datei.
|
||||
|
||||
Es ersetzt für die technische Umsetzung die sonst übliche Aufteilung in:
|
||||
- Daemon-Spec
|
||||
- Daemon-Handoff
|
||||
- technisches Konzept
|
||||
- Implementierungs-Backlog
|
||||
|
||||
Die fachliche Grundidee bleibt in [FALUKANT_SERVANTS_CONCEPT.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_SERVANTS_CONCEPT.md) beschrieben. Dieses Dokument hier ist die Arbeitsgrundlage für Implementierung und Daemon-Anbindung.
|
||||
|
||||
## 1. Zielbild
|
||||
|
||||
Die Dienerschaft ist ein Haussystem mit vier Kernwerten:
|
||||
- `servantCount`
|
||||
- `servantQuality`
|
||||
- `servantPayLevel`
|
||||
- `householdOrder`
|
||||
|
||||
Diese Werte wirken auf:
|
||||
- monatliche Kosten
|
||||
- Repräsentation und Ansehen
|
||||
- Komfort und Ordnung des Haushalts
|
||||
- Ehezufriedenheit und Haushaltsfrieden
|
||||
- Diskretion bei Liebschaften
|
||||
- spätere Untergrund-Aufdeckungen
|
||||
|
||||
## 2. Systemgrenzen
|
||||
|
||||
In Scope der ersten Version:
|
||||
- Dienerschaft hängt an `user_house`
|
||||
- House-UI zeigt und verändert Dienerwerte
|
||||
- externer Daemon verarbeitet Daily- und Monthly-Effekte
|
||||
- Familie, Liebschaften und Untergrund nutzen die resultierenden Werte mit
|
||||
|
||||
Nicht in Scope der ersten Version:
|
||||
- einzelne benannte Diener
|
||||
- eigene Dienerrollen wie Küchenpersonal, Wachen, Zofen
|
||||
- eigene Eventketten nur für Diener
|
||||
- finales Balancing
|
||||
|
||||
## 3. Datenmodell
|
||||
|
||||
### 3.1 Bereits vorhandene Hausfelder
|
||||
|
||||
In `falukant_data.user_house`:
|
||||
- `servant_count integer not null default 0`
|
||||
- `servant_quality integer not null default 50`
|
||||
- `servant_pay_level varchar(20) not null default 'normal'`
|
||||
- `household_order integer not null default 55`
|
||||
|
||||
### 3.2 Wertebereiche
|
||||
|
||||
- `servant_count`: `0..999`
|
||||
- `servant_quality`: `0..100`
|
||||
- `servant_pay_level`: `low | normal | high`
|
||||
- `household_order`: `0..100`
|
||||
|
||||
### 3.3 Abgeleitete Werte
|
||||
|
||||
Diese Werte müssen nicht persistent gespeichert werden, sondern können im Backend oder Daemon berechnet werden:
|
||||
- `expectedServantsMin`
|
||||
- `expectedServantsMax`
|
||||
- `staffingState`
|
||||
- `orderState`
|
||||
- `monthlyServantCost`
|
||||
- `discretionModifier`
|
||||
- `servantReputationModifier`
|
||||
- `marriageComfortModifier`
|
||||
|
||||
## 4. Erwartungswert der Dienerschaft
|
||||
|
||||
Die Sollgröße hängt von Haus und Stand ab.
|
||||
|
||||
### 4.1 Basis nach Hausposition
|
||||
|
||||
`house.house_type.position` ist die grobe Hausklasse.
|
||||
|
||||
Empfohlene erste Regel:
|
||||
|
||||
| Hausposition | Basis Min | Basis Max |
|
||||
|-------------|-----------|-----------|
|
||||
| `<= 2` | 0 | 1 |
|
||||
| `3` | 1 | 2 |
|
||||
| `4` | 2 | 4 |
|
||||
| `5` | 3 | 6 |
|
||||
| `>= 6` | 4 | 8 |
|
||||
|
||||
### 4.2 Standesbonus
|
||||
|
||||
Aus `character.noble_title.level`:
|
||||
|
||||
```text
|
||||
titleBonus = floor(level / 3), mindestens 0
|
||||
expectedMin = baseMin + titleBonus
|
||||
expectedMax = baseMax + titleBonus
|
||||
```
|
||||
|
||||
### 4.3 Zustandsklassen
|
||||
|
||||
```text
|
||||
if servantCount < expectedMin => understaffed
|
||||
if servantCount > expectedMax => overstaffed
|
||||
sonst => fitting
|
||||
```
|
||||
|
||||
## 5. Daily-Regeln für den externen Daemon
|
||||
|
||||
## 5.1 Daily-Input
|
||||
|
||||
Pro Falukant-User mit Haus braucht der Daemon:
|
||||
- `falukant_user.id`
|
||||
- `user.id` bzw. `user.hashed_id` für Benachrichtigung
|
||||
- `character.id`
|
||||
- `character.reputation`
|
||||
- `character.noble_title_id` und idealerweise `character.nobleTitle.level`
|
||||
- `user_house.house_type_id`
|
||||
- `house_type.position`
|
||||
- `house_type.cost`
|
||||
- `servant_count`
|
||||
- `servant_quality`
|
||||
- `servant_pay_level`
|
||||
- `household_order`
|
||||
- optional für Verknüpfungen:
|
||||
- `marriage_satisfaction` oder `relationship_state.marriage_satisfaction`
|
||||
- aktive Liebschaften mit `visibility`, `discretion`, `risk`
|
||||
|
||||
## 5.2 Daily-Hilfswerte
|
||||
|
||||
```text
|
||||
payShift(low) = -6
|
||||
payShift(normal) = 0
|
||||
payShift(high) = +6
|
||||
|
||||
missing = max(0, expectedMin - servantCount)
|
||||
excessive = max(0, servantCount - expectedMax)
|
||||
|
||||
qualityPart = round((servantQuality - 50) * 0.35)
|
||||
payPart = payShift(servantPayLevel)
|
||||
fitPenalty = missing * 10 + excessive * 4
|
||||
```
|
||||
|
||||
## 5.3 Daily-Zielwert für Haushaltsordnung
|
||||
|
||||
```text
|
||||
targetHouseholdOrder = clamp(
|
||||
55 + qualityPart + payPart - fitPenalty,
|
||||
0,
|
||||
100
|
||||
)
|
||||
```
|
||||
|
||||
## 5.4 Daily-Drift der Haushaltsordnung
|
||||
|
||||
Die Ordnung springt nicht hart, sondern driftet langsam:
|
||||
|
||||
```text
|
||||
newHouseholdOrder = oldHouseholdOrder
|
||||
|
||||
if oldHouseholdOrder < targetHouseholdOrder:
|
||||
newHouseholdOrder += min(2, targetHouseholdOrder - oldHouseholdOrder)
|
||||
|
||||
if oldHouseholdOrder > targetHouseholdOrder:
|
||||
newHouseholdOrder -= min(2, oldHouseholdOrder - targetHouseholdOrder)
|
||||
```
|
||||
|
||||
Zusatzregel:
|
||||
- bei `servantPayLevel = low` und `servantCount < expectedMin` zusätzlich `-1`
|
||||
- bei `servantPayLevel = high` und `servantQuality >= 65` zusätzlich `+1`
|
||||
|
||||
Danach clamp auf `0..100`.
|
||||
|
||||
## 5.5 Daily-Drift der Dienerqualität
|
||||
|
||||
Die Qualität ändert sich langsam:
|
||||
|
||||
```text
|
||||
qualityDelta = 0
|
||||
|
||||
if servantPayLevel = low: qualityDelta -= 1
|
||||
if servantPayLevel = high: qualityDelta += 1
|
||||
|
||||
if servantCount < expectedMin: qualityDelta -= 1
|
||||
if servantCount > expectedMax + 2: qualityDelta -= 1
|
||||
|
||||
if householdOrder >= 80: qualityDelta += 1
|
||||
if householdOrder <= 30: qualityDelta -= 1
|
||||
```
|
||||
|
||||
Danach:
|
||||
- auf `-2..+2` pro Tag begrenzen
|
||||
- `servantQuality = clamp(servantQuality + qualityDelta, 0, 100)`
|
||||
|
||||
## 5.6 Daily-Effekt auf Ansehen
|
||||
|
||||
Der Daily-Rufeffekt ist klein, damit Monats- und Ereigniseffekte wichtiger bleiben.
|
||||
|
||||
```text
|
||||
reputationDelta = 0
|
||||
|
||||
if titleLevel >= 4 and servantCount < expectedMin:
|
||||
reputationDelta -= 0.15 * missing
|
||||
|
||||
if titleLevel <= 1 and servantCount > expectedMax:
|
||||
reputationDelta -= 0.10 * excessive
|
||||
|
||||
if householdOrder >= 85 and servantCount between expectedMin and expectedMax:
|
||||
reputationDelta += 0.05
|
||||
|
||||
if householdOrder <= 25:
|
||||
reputationDelta -= 0.20
|
||||
```
|
||||
|
||||
Rundung:
|
||||
- intern als Dezimalwert möglich
|
||||
- falls nur Ganzzahlen gespeichert werden, über Tagespuffer oder Rundungsregel aggregieren
|
||||
|
||||
## 5.7 Daily-Effekt auf Ehe / Haushalt
|
||||
|
||||
Wenn ein Ehe-Zufriedenheitssystem vorhanden ist:
|
||||
|
||||
```text
|
||||
marriageDelta = 0
|
||||
|
||||
if householdOrder >= 75: marriageDelta += 0.10
|
||||
if householdOrder <= 35: marriageDelta -= 0.15
|
||||
if servantCount < expectedMin: marriageDelta -= 0.10
|
||||
```
|
||||
|
||||
Wenn noch kein eigener Wert gespeichert wird:
|
||||
- diese Regel für später vormerken
|
||||
- aktuell nur `householdTension` oder UI-Ableitungen beeinflussen
|
||||
|
||||
## 5.8 Daily-Effekt auf Liebschaften / Diskretion
|
||||
|
||||
Der Daemon berechnet einen Diskretionsmodifikator:
|
||||
|
||||
```text
|
||||
discretionModifier = 0
|
||||
|
||||
if servantQuality >= 70 and servantPayLevel = high and servantCount <= expectedMax:
|
||||
discretionModifier -= 8
|
||||
|
||||
if servantPayLevel = low:
|
||||
discretionModifier += 6
|
||||
|
||||
if servantCount > expectedMax + 1:
|
||||
discretionModifier += 4
|
||||
|
||||
if householdOrder <= 35:
|
||||
discretionModifier += 5
|
||||
```
|
||||
|
||||
Bedeutung:
|
||||
- negativer Wert verbessert Geheimhaltung
|
||||
- positiver Wert erhöht Entdeckungsrisiko
|
||||
|
||||
Anwendung:
|
||||
- bei aktiven Liebschaften auf Sichtbarkeit/Skandalchance
|
||||
- bei Untergrundaktivitäten als Erfolgsmodifikator
|
||||
|
||||
## 5.9 Daily-Notifications
|
||||
|
||||
Daily sendet nicht für jede Teildrift ein eigenes Event.
|
||||
|
||||
Wenn sich einer dieser Punkte relevant verändert:
|
||||
- `household_order`
|
||||
- `servant_quality`
|
||||
- `reputation`
|
||||
- Ehe-/Liebschaftsfolgen über Diener
|
||||
|
||||
dann:
|
||||
- `falukantUpdateFamily` mit `reason: "daily"`
|
||||
- danach `falukantUpdateStatus`
|
||||
|
||||
Es gibt keinen separaten `reason` für Dienerschaft.
|
||||
|
||||
## 6. Monthly-Regeln für den externen Daemon
|
||||
|
||||
## 6.1 Monthly-Input
|
||||
|
||||
Wie Daily, zusätzlich:
|
||||
- aktuelles Geld `falukant_user.money`
|
||||
|
||||
## 6.2 Monatskosten
|
||||
|
||||
Änderungsnotiz für den externen Daemon:
|
||||
|
||||
- Falukant verwendet einen stark komprimierten Zeitmaßstab: `1 Tag = 1 Jahr`.
|
||||
- Der Monatslauf des Daemons entspricht damit ungefähr einem 2-Stunden-Schritt.
|
||||
- Dienerkosten dürfen deshalb nicht wie ein realistischer Vollmonatslohn skaliert werden.
|
||||
- Der hier definierte Monatswert ist ein abstrahierter Unterhalts- und Bindungsbetrag pro Monatstick.
|
||||
- Die Datenbank ist für diese Änderung bereits vorbereitet; es sind keine zusätzlichen DB-Anpassungen nötig.
|
||||
|
||||
```text
|
||||
basePerServant = max(3, round((houseType.cost / 10000) + 6))
|
||||
qualityFactor = 1 + ((servantQuality - 50) / 200)
|
||||
payFactor(low) = 0.8
|
||||
payFactor(normal) = 1.0
|
||||
payFactor(high) = 1.3
|
||||
|
||||
monthlyServantCost = servantCount * basePerServant * qualityFactor * payFactor
|
||||
```
|
||||
|
||||
Auf 2 Nachkommastellen runden.
|
||||
|
||||
## 6.3 Abbuchung
|
||||
|
||||
Wenn genügend Geld vorhanden:
|
||||
- Geld abziehen
|
||||
- Aktivität z. B. `servants_monthly`
|
||||
|
||||
Wenn nicht genügend Geld vorhanden:
|
||||
- so viel wie möglich abziehen oder auf 0 fallen lassen, je nach vorhandener Gesamtlogik
|
||||
- Unterversorgung markieren
|
||||
|
||||
Empfehlung für die erste Version:
|
||||
- vollständige Abbuchung nur wenn genug Geld da
|
||||
- sonst `underfunded = true`
|
||||
|
||||
## 6.4 Folgen von Unterversorgung
|
||||
|
||||
Bei Unterversorgung im Monat:
|
||||
|
||||
```text
|
||||
servantQuality -= 4
|
||||
householdOrder -= 6
|
||||
```
|
||||
|
||||
Zusätzlich:
|
||||
- wenn `titleLevel >= 4`: `reputation -= 1`
|
||||
- wenn aktive Liebschaften vorhanden: Diskretionsmalus für den Folgemonat
|
||||
|
||||
## 6.5 Monatsbonus bei gutem Haushalt
|
||||
|
||||
Wenn gleichzeitig:
|
||||
- `servantCount` innerhalb Sollbereich
|
||||
- `servantQuality >= 70`
|
||||
- `householdOrder >= 80`
|
||||
- `servantPayLevel != low`
|
||||
|
||||
dann:
|
||||
- `reputation += 1` für hohe Stände ab `titleLevel >= 3`
|
||||
- kleiner Ehe-/Komfortbonus, falls System vorhanden
|
||||
|
||||
## 6.6 Monthly-Notifications
|
||||
|
||||
Nach Monatsverarbeitung:
|
||||
- `falukantUpdateFamily` mit `reason: "monthly"`
|
||||
- danach `falukantUpdateStatus`
|
||||
|
||||
## 7. Handoff an den externen Daemon
|
||||
|
||||
## 7.1 Der externe Daemon muss lesen
|
||||
|
||||
Aus Backend/DB:
|
||||
- `falukant_data.user_house`
|
||||
- `falukant_type.house`
|
||||
- `falukant_data.falukant_user`
|
||||
- `falukant_data.character`
|
||||
- Titel/Stand
|
||||
- optional aktive Ehe-/Liebschaftsdaten
|
||||
|
||||
## 7.2 Der externe Daemon muss schreiben
|
||||
|
||||
Mindestens:
|
||||
- `user_house.servant_quality`
|
||||
- `user_house.household_order`
|
||||
- `character.reputation` oder entsprechender Rufwert
|
||||
|
||||
Optional, falls vorhanden:
|
||||
- `relationship_state.marriage_satisfaction`
|
||||
- Hilfs-/Logtabellen für Monatskosten und Unterversorgung
|
||||
|
||||
## 7.3 Der externe Daemon muss senden
|
||||
|
||||
Bei relevanten Änderungen:
|
||||
- `falukantUpdateFamily`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
`reason` nur:
|
||||
- `daily`
|
||||
- `monthly`
|
||||
|
||||
Keine zusätzlichen Diener-Reason-Werte.
|
||||
|
||||
## 7.4 Idempotenz
|
||||
|
||||
Der Daemon muss verhindern, dass Daily/Monthly doppelt auf denselben Tick laufen.
|
||||
|
||||
Empfohlen:
|
||||
- eigene Tick-Marker außerhalb dieses Projekts
|
||||
- oder Zeitstempel in Worker-Logs
|
||||
|
||||
## 8. Backend-Aufgaben in diesem Projekt
|
||||
|
||||
## 8.1 Bereits erledigt
|
||||
|
||||
- Hausfelder in `user_house`
|
||||
- Migration
|
||||
- Produktions-SQL
|
||||
- House-API mit Dienerwerten
|
||||
- UI in `HouseView`
|
||||
- direkte Spieleraktionen:
|
||||
- einstellen
|
||||
- entlassen
|
||||
- Bezahlungsstufe ändern
|
||||
|
||||
## 8.2 Noch sinnvolle Backend-Nacharbeiten
|
||||
|
||||
- eigenes Money-Label für Monatskosten, z. B. `servants_monthly`
|
||||
- optional eigener Read-Endpunkt nur für Dienerschaft
|
||||
- optionale Validierungsgrenzen serverseitig weiter schärfen
|
||||
- später: Ableitung von `householdTension` stärker an Diener koppeln
|
||||
|
||||
## 9. UI-Anforderungen
|
||||
|
||||
Die House-UI soll anzeigen:
|
||||
- aktuelle Dienerzahl
|
||||
- Sollbereich
|
||||
- Monatskosten
|
||||
- Qualität
|
||||
- Haushaltsordnung
|
||||
- Bezahlungsstufe
|
||||
- Besetzungsstatus
|
||||
- Ordnungsstatus
|
||||
|
||||
Die UI soll direkt erlauben:
|
||||
- `+1` Diener
|
||||
- `-1` Diener
|
||||
- Pay-Level wechseln
|
||||
|
||||
Die UI braucht keine Daemon-Sonderlogik außer normalen House-/Status-Refresh.
|
||||
|
||||
## 10. API-Schnittstellen
|
||||
|
||||
Bereits vorgesehen:
|
||||
- `GET /api/falukant/houses`
|
||||
- `POST /api/falukant/houses/servants/hire`
|
||||
- `POST /api/falukant/houses/servants/dismiss`
|
||||
- `POST /api/falukant/houses/servants/pay-level`
|
||||
|
||||
### Beispiel-Response für `GET /houses`
|
||||
|
||||
```json
|
||||
{
|
||||
"roofCondition": 100,
|
||||
"wallCondition": 100,
|
||||
"floorCondition": 100,
|
||||
"windowCondition": 100,
|
||||
"servantCount": 3,
|
||||
"servantQuality": 58,
|
||||
"servantPayLevel": "normal",
|
||||
"householdOrder": 63,
|
||||
"houseType": {
|
||||
"id": 5,
|
||||
"position": 5,
|
||||
"cost": 273000,
|
||||
"labelTr": "family_house"
|
||||
},
|
||||
"servantSummary": {
|
||||
"expectedMin": 3,
|
||||
"expectedMax": 6,
|
||||
"monthlyCost": 98.1,
|
||||
"staffingState": "fitting",
|
||||
"orderState": "stable"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11. Technische Architektur
|
||||
|
||||
### 11.1 Quelle der Wahrheit
|
||||
|
||||
Quelle der Wahrheit für:
|
||||
- Stammdaten und persistente Hauswerte: dieses Backend / Datenbank
|
||||
- Tick-Ausführung: externer Daemon
|
||||
|
||||
### 11.2 Verantwortungstrennung
|
||||
|
||||
Dieses Projekt:
|
||||
- speichert Werte
|
||||
- bietet UI und API
|
||||
- berechnet einfache Hilfswerte für Anzeige
|
||||
|
||||
Externer Daemon:
|
||||
- tägliche und monatliche Veränderung
|
||||
- Kostenabbuchung
|
||||
- Reputationseffekte
|
||||
- Verknüpfung mit Familie, Liebschaften und Untergrund
|
||||
|
||||
### 11.3 Warum so
|
||||
|
||||
Damit:
|
||||
- Spiellogik nicht doppelt tickt
|
||||
- UI trotzdem schon benutzbar ist
|
||||
- der Daemon später nur auf stabile Felder aufsetzen muss
|
||||
|
||||
## 12. Implementierungs-Backlog
|
||||
|
||||
## B1 Datenbasis
|
||||
|
||||
Status: erledigt
|
||||
|
||||
Aufgaben:
|
||||
- Hausfelder in `user_house`
|
||||
- Migration
|
||||
- Produktions-SQL
|
||||
|
||||
Done:
|
||||
- Felder vorhanden
|
||||
- Model aktualisiert
|
||||
|
||||
## B2 Haus-Service
|
||||
|
||||
Status: erledigt
|
||||
|
||||
Aufgaben:
|
||||
- Sollbereich berechnen
|
||||
- Monatskosten berechnen
|
||||
- Zustandslabels ableiten
|
||||
|
||||
Done:
|
||||
- `servantSummary` wird im House-Read geliefert
|
||||
|
||||
## B3 Spieleraktionen
|
||||
|
||||
Status: erledigt
|
||||
|
||||
Aufgaben:
|
||||
- einstellen
|
||||
- entlassen
|
||||
- Bezahlung ändern
|
||||
|
||||
Done:
|
||||
- Endpunkte vorhanden
|
||||
- UI verdrahtet
|
||||
|
||||
## B4 House-UI
|
||||
|
||||
Status: erledigt
|
||||
|
||||
Aufgaben:
|
||||
- Anzeige in `HouseView`
|
||||
- Aktionen
|
||||
- Locale-Texte
|
||||
|
||||
Done:
|
||||
- HouseView zeigt den Dienerblock
|
||||
|
||||
## B5 Daemon Daily
|
||||
|
||||
Status: offen
|
||||
|
||||
Aufgaben:
|
||||
- `expectedMin/Max` im Worker berechnen
|
||||
- `householdOrder` driften
|
||||
- `servantQuality` driften
|
||||
- kleinen Reputationseffekt anwenden
|
||||
- Diskretionsmodifikator für Liebschaften ableiten
|
||||
- `daily`-Refresh senden
|
||||
|
||||
Done-Kriterien:
|
||||
- täglicher Tick verändert Hauswerte nachvollziehbar
|
||||
- keine zusätzlichen UI-Reason-Werte nötig
|
||||
|
||||
## B6 Daemon Monthly
|
||||
|
||||
Status: offen
|
||||
|
||||
Aufgaben:
|
||||
- Monatskosten berechnen
|
||||
- Geld abbuchen
|
||||
- Unterversorgung behandeln
|
||||
- Monatsrufeffekte anwenden
|
||||
- `monthly`-Refresh senden
|
||||
|
||||
Done-Kriterien:
|
||||
- Monatskosten und Unterversorgung sind im Spiel spürbar
|
||||
|
||||
## B7 Integration mit Familie / Liebschaften
|
||||
|
||||
Status: offen
|
||||
|
||||
Aufgaben:
|
||||
- `householdOrder` auf Ehekomfort mappen
|
||||
- Diskretionsmodifikator in Skandal-/Liebschaftslogik einbeziehen
|
||||
- schlechte Bezahlung oder Überbesetzung als Gerüchtefaktor nutzen
|
||||
|
||||
Done-Kriterien:
|
||||
- Dienerschaft beeinflusst Familien- und Liebschaftssystem real
|
||||
|
||||
## B8 Integration mit Untergrund
|
||||
|
||||
Status: offen
|
||||
|
||||
Aufgaben:
|
||||
- `investigate_affair` nutzt Dienerwerte
|
||||
- schlechter Haushalt erhöht Aufdeckungschance
|
||||
- guter, diskreter Haushalt senkt Erfolgswahrscheinlichkeit
|
||||
|
||||
Done-Kriterien:
|
||||
- Untergrund spürt Dienerschaft in Erfolgsmodifikatoren
|
||||
|
||||
## B9 Balancing
|
||||
|
||||
Status: offen, bewusst spätere Phase
|
||||
|
||||
Aufgaben:
|
||||
- Kosten, Rufwerte, Driftgeschwindigkeiten und Schwellwerte feinjustieren
|
||||
|
||||
## 13. Produktionshinweise
|
||||
|
||||
Wenn keine Migrationen laufen:
|
||||
- [add_servants_to_user_house.sql](/mnt/share/torsten/Programs/YourPart3/backend/sql/add_servants_to_user_house.sql) ausführen
|
||||
|
||||
Der externe Daemon muss erst danach aktiviert werden, damit die Felder sicher vorhanden sind.
|
||||
|
||||
## 14. Empfehlung für die nächste Reihenfolge
|
||||
|
||||
Empfohlene Reihenfolge ab jetzt:
|
||||
1. Produktions-SQL einspielen
|
||||
2. B5 Daily im externen Daemon
|
||||
3. B6 Monthly im externen Daemon
|
||||
4. B7 Familie/Liebschaften anbinden
|
||||
5. B8 Untergrund anbinden
|
||||
6. B9 Balancing
|
||||
|
||||
## 15. Kurzfazit
|
||||
|
||||
Die Haus- und UI-Basis ist bereits eingebaut. Für eine vollständige Spielwirkung fehlen jetzt vor allem die beiden externen Worker-Blöcke:
|
||||
- tägliche Drift
|
||||
- monatliche Kosten und Folgen
|
||||
|
||||
Mit dieser Datei sollte der externe Daemon direkt implementierbar sein, ohne weitere Konzeptdokumente zu benötigen.
|
||||
412
docs/FALUKANT_TRANSPORT_RAIDS_SPEC.md
Normal file
412
docs/FALUKANT_TRANSPORT_RAIDS_SPEC.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# Falukant: Überfälle auf Transporte und Transportwachen
|
||||
|
||||
Dieses Dokument beschreibt das Zielmodell für einen neuen Untergrundtyp **Überfälle auf Transporte** sowie das ergänzende Schutzsystem **Transportwachen**.
|
||||
|
||||
Ziel:
|
||||
|
||||
- Untergrundspieler können bewaffnete Banden anheuern
|
||||
- Banden lauern in geeigneten Regionen und überfallen dort zufällige Transporte
|
||||
- Beute wird nicht vollständig, sondern nur teilweise erlangt
|
||||
- Beute landet im nächstgelegenen Lager des Auftraggebers
|
||||
- Opfer und Auftraggeber spüren wirtschaftliche und soziale Folgen
|
||||
- Transporte können mit Wachen geschützt werden
|
||||
- Überfallserfolg hängt später im Daemon an Bandengröße, Wachzahl, Region und Zufall
|
||||
|
||||
## 1. Bestandsaufnahme
|
||||
|
||||
Bereits vorhanden:
|
||||
|
||||
- Untergrundaktivitäten im Backend und UI
|
||||
- Transportsystem mit Fahrzeugen, Routen, Start- und Zielniederlassung
|
||||
- Lager-/Bestandssystem in Niederlassungen
|
||||
- Fahrzeug- und Transportverwaltung in [BranchView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/BranchView.vue)
|
||||
- Untergrundformular in [UndergroundView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/UndergroundView.vue)
|
||||
|
||||
Noch nicht vorhanden:
|
||||
|
||||
- Untergrundtyp `raid_transport`
|
||||
- Bandengröße / Bandenkosten
|
||||
- Wachen auf Transporten
|
||||
- Kampfauflösung zwischen Überfall und Eskorte
|
||||
- Beutetransfer in Lager
|
||||
- Overworld-/Socket-Kommunikation für Überfälle
|
||||
|
||||
## 2. Kernidee
|
||||
|
||||
Ein Untergrundspieler kann eine Bande für einen Transportüberfall anheuern.
|
||||
|
||||
Die Bande:
|
||||
|
||||
- wird einer Region zugewiesen
|
||||
- darf nur in Regionen vom Typ `4` oder `5` operieren
|
||||
- darf nicht in `town` operieren
|
||||
- lauert dort auf zufällige Transporte
|
||||
|
||||
Bei einem Überfall:
|
||||
|
||||
- wird nicht der gesamte Transport geraubt
|
||||
- nur ein Teil der transportierten Ware wird erbeutet
|
||||
- nur ein Teil kann tatsächlich abtransportiert werden
|
||||
- die Beute wird in das nächstgelegene Lager des Auftraggebers eingebucht
|
||||
|
||||
Der Überfall wirkt:
|
||||
|
||||
- auf das Opfer wirtschaftlich und reputativ
|
||||
- auf den Auftraggeber als Gewinnchance, aber auch als Risiko
|
||||
|
||||
## 3. Neuer Untergrundtyp
|
||||
|
||||
Neuer Typ:
|
||||
|
||||
- `raid_transport`
|
||||
|
||||
Geplanter UI-Name:
|
||||
|
||||
- `Überfälle auf Transporte`
|
||||
|
||||
Grundparameter:
|
||||
|
||||
- `type`: `raid_transport`
|
||||
- `regionId`: Region, in der gelauert wird
|
||||
- `bandSize`: Stärke der angeheuerten Bande
|
||||
- optional später:
|
||||
- `focus`: eher Waren, eher Fahrzeuge, eher schwache Transporte
|
||||
|
||||
## 4. Regionsregeln
|
||||
|
||||
Die Aktivität darf nur in Regionen starten, die:
|
||||
|
||||
- Regionstyp `4` oder `5` haben
|
||||
- nicht `town` sind
|
||||
|
||||
Begründung:
|
||||
|
||||
- Überfälle sollen auf Wegen, Randregionen oder schlecht gesicherten Zonen stattfinden
|
||||
- nicht direkt im Stadtkern
|
||||
|
||||
UI-Regel:
|
||||
|
||||
- im Untergrundformular nur zulässige Regionen anbieten
|
||||
|
||||
Backend-Regel:
|
||||
|
||||
- Region validieren
|
||||
- unzulässige Region serverseitig ablehnen
|
||||
|
||||
## 5. Bandensystem
|
||||
|
||||
### 5.1 Bandengröße
|
||||
|
||||
Der Spieler wählt eine Bandengröße, z. B.:
|
||||
|
||||
- `small`
|
||||
- `medium`
|
||||
- `large`
|
||||
|
||||
Alternativ numerisch:
|
||||
|
||||
- `3`
|
||||
- `6`
|
||||
- `10`
|
||||
|
||||
Empfehlung für Version 1:
|
||||
|
||||
- numerischer Wert `bandSize`
|
||||
- UI zeigt zusätzlich Stufenbezeichnung
|
||||
|
||||
### 5.2 Kosten
|
||||
|
||||
Die Kosten steigen überproportional, damit große Überfälle nicht trivial werden.
|
||||
|
||||
Beispielmodell:
|
||||
|
||||
- Grundkosten: `20`
|
||||
- pro Bandit: `+12`
|
||||
- Risikozuschlag: `bandSize * 2`
|
||||
|
||||
Beispiel:
|
||||
|
||||
- `3` Banditen: `62`
|
||||
- `6` Banditen: `104`
|
||||
- `10` Banditen: `160`
|
||||
|
||||
Die finalen Werte sind Balancing und können später angepasst werden.
|
||||
|
||||
## 6. Transportwachen
|
||||
|
||||
### 6.1 Grundidee
|
||||
|
||||
Für Transporte sollen Wachen mitgeschickt werden können.
|
||||
|
||||
Wirkung:
|
||||
|
||||
- geringere Überfallchance
|
||||
- höhere Abwehrchance
|
||||
- geringere Beute bei erfolgreichem Überfall
|
||||
|
||||
### 6.2 UI-Verhalten
|
||||
|
||||
Beim Erstellen eines Transports:
|
||||
|
||||
- zusätzliches Feld `wachen`
|
||||
- nur positive ganze Zahl
|
||||
- sichtbare Zusatzkosten
|
||||
|
||||
In der Transportübersicht:
|
||||
|
||||
- Wachenanzahl anzeigen
|
||||
|
||||
### 6.3 Kosten
|
||||
|
||||
Wachen verursachen direkte Transportmehrkosten.
|
||||
|
||||
Beispiel:
|
||||
|
||||
- `guardCount * 4`
|
||||
|
||||
Optional später:
|
||||
|
||||
- bessere Wachenstufe
|
||||
- bewaffnete Eskorte
|
||||
|
||||
## 7. Überfallauflösung im Daemon
|
||||
|
||||
Der externe Daemon bleibt die führende Quelle für die eigentliche Auflösung.
|
||||
|
||||
Der Worker prüft periodisch:
|
||||
|
||||
1. aktive `raid_transport`-Aktivitäten
|
||||
2. Transporte, die gerade durch passende Regionen laufen
|
||||
3. ob eine Kollision zwischen Aktivität und Transport zustande kommt
|
||||
|
||||
### 7.1 Kandidatenprüfung
|
||||
|
||||
Ein Transport ist überfallbar, wenn:
|
||||
|
||||
- er aktiv ist
|
||||
- seine Route durch die Zielregion der Bande führt oder dort endet
|
||||
- er dem Auftraggeber nicht selbst gehört
|
||||
|
||||
### 7.2 Begegnungschance
|
||||
|
||||
Die Basiswahrscheinlichkeit hängt u. a. ab von:
|
||||
|
||||
- Bandengröße
|
||||
- Regionstyp
|
||||
- Transportfrequenz / Zufall
|
||||
- ggf. Diskretions- oder Untergrundfaktoren
|
||||
|
||||
### 7.3 Kampfwert
|
||||
|
||||
Für Version 1 reicht ein abstrahierter Vergleich:
|
||||
|
||||
- `raidPower = bandSize + random(0..bandSize)`
|
||||
- `guardPower = guardCount + random(0..guardCount)`
|
||||
|
||||
Modifikatoren:
|
||||
|
||||
- bessere Fahrzeuge können leicht entkommen
|
||||
- große Transporte sind leichter sichtbar
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- `repelled`
|
||||
- `partial_success`
|
||||
- `major_success`
|
||||
|
||||
## 8. Beute
|
||||
|
||||
Es darf niemals der komplette Transport verloren gehen.
|
||||
|
||||
### 8.1 Grundregel
|
||||
|
||||
Bei erfolgreichem Überfall:
|
||||
|
||||
- nur ein Teil der transportierten Menge wird geraubt
|
||||
- nur ein Teil dieser Menge erreicht als Beute den Auftraggeber
|
||||
|
||||
Empfohlene Formel:
|
||||
|
||||
- `baseLootShare = 0.15 bis 0.45`
|
||||
- bei `major_success` bis `0.60`
|
||||
- Wachen senken den Wert
|
||||
|
||||
Zusätzlich:
|
||||
|
||||
- Abrunden auf ganze Mengeneinheiten
|
||||
- mindestens `1`, wenn überhaupt Erfolg
|
||||
|
||||
### 8.2 Einlagerung
|
||||
|
||||
Die Beute wird in das nächstgelegene Lager des Auftraggebers eingebucht.
|
||||
|
||||
Priorität:
|
||||
|
||||
1. nächstgelegene Niederlassung des Auftraggebers
|
||||
2. nur wenn dort Lager für den Produkttyp vorhanden oder anlegbar
|
||||
3. falls kein geeignetes Lager existiert:
|
||||
- Beute teilweise verfallen lassen
|
||||
- Rest als `lost_due_to_storage`
|
||||
|
||||
Wichtig:
|
||||
|
||||
- nie stillschweigend alles gutschreiben
|
||||
- Lagerkapazität berücksichtigen
|
||||
|
||||
## 9. Folgen
|
||||
|
||||
### 9.1 Für das Opfer
|
||||
|
||||
- Warenverlust
|
||||
- optional kleiner Reputationsschaden
|
||||
- Hinweis in Geld-/Transporthistorie
|
||||
- evtl. Routenanpassung oder Absicherungsdruck
|
||||
|
||||
### 9.2 Für den Auftraggeber
|
||||
|
||||
- Kosten der Bande
|
||||
- möglicher Beutegewinn
|
||||
- optional leichter Reputations- oder Verdachtsanstieg
|
||||
- Risiko von Gegenmaßnahmen in späteren Ausbaustufen
|
||||
|
||||
## 10. Datenmodell
|
||||
|
||||
Für eine erste technische Umsetzung werden voraussichtlich neue Felder benötigt.
|
||||
|
||||
### 10.1 Underground-Aktivität
|
||||
|
||||
In `underground.result` bzw. Payload:
|
||||
|
||||
- `bandSize`
|
||||
- `attempts`
|
||||
- `successes`
|
||||
- `lastTargetTransportId`
|
||||
- `lastLoot`
|
||||
- `lastOutcome`
|
||||
|
||||
### 10.2 Transport
|
||||
|
||||
Neu empfohlen:
|
||||
|
||||
- `guardCount`
|
||||
- optional später `guardQuality`
|
||||
|
||||
### 10.3 Transport-/Überfall-Log
|
||||
|
||||
Optional, aber sinnvoll:
|
||||
|
||||
- eigener Logeintrag oder JSON-Protokoll mit:
|
||||
- Opfer
|
||||
- Auftraggeber
|
||||
- Region
|
||||
- Bandengröße
|
||||
- Wachen
|
||||
- geraubte Mengen
|
||||
- eingelagerte Mengen
|
||||
|
||||
## 11. Socket-Events
|
||||
|
||||
Empfohlene Events für die UI:
|
||||
|
||||
### 11.1 Überfall auf Opferseite
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantTransportRaid",
|
||||
"user_id": 123,
|
||||
"reason": "transport_raided"
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 Überfall auf Auftraggeberseite
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUndergroundUpdate",
|
||||
"user_id": 456,
|
||||
"reason": "raid_success"
|
||||
}
|
||||
```
|
||||
|
||||
Mögliche `reason`:
|
||||
|
||||
- `transport_raided`
|
||||
- `raid_repelled`
|
||||
- `raid_success`
|
||||
- `raid_partial_success`
|
||||
- `raid_loot_stored`
|
||||
|
||||
Begleitende Events:
|
||||
|
||||
- `falukantUpdateStatus`
|
||||
- `falukantBranchUpdate`
|
||||
- optional `falukantUpdateDebt` nicht nötig
|
||||
|
||||
## 12. UI-Anforderungen
|
||||
|
||||
### 12.1 Underground
|
||||
|
||||
In [UndergroundView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/UndergroundView.vue):
|
||||
|
||||
- neuer Typ `raid_transport`
|
||||
- Regionsauswahl mit erlaubten Regionstypen
|
||||
- Wahl der Bandengröße
|
||||
- Kostenanzeige
|
||||
- spätere Ergebnisanzeige:
|
||||
- Erfolg / Misserfolg
|
||||
- Beute
|
||||
- Zielregion
|
||||
|
||||
### 12.2 Transport
|
||||
|
||||
In [BranchView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/BranchView.vue):
|
||||
|
||||
- Wachenfeld beim Transportstart
|
||||
- Wachenanzahl in Transportliste
|
||||
- Hinweis, dass Wachen Überfälle erschweren, aber Kosten erhöhen
|
||||
|
||||
## 13. Technische Reihenfolge
|
||||
|
||||
### TRA1. Konzept und Typerweiterung
|
||||
|
||||
- `raid_transport` als Underground-Typ anlegen
|
||||
- Produktions-SQL für Bestandsdatenbank
|
||||
|
||||
### TRA2. Lokale Projektbasis
|
||||
|
||||
- API akzeptiert `bandSize`
|
||||
- UI unterstützt Bandengröße und erlaubte Regionen
|
||||
- Transporte erhalten `guardCount`
|
||||
|
||||
### TRA3. Daemon-Auflösung
|
||||
|
||||
- Worker prüft Kollisionen zwischen Aktivität und aktiven Transporten
|
||||
- Überfallausgang berechnen
|
||||
- Beute teilweise einlagern
|
||||
- Events senden
|
||||
|
||||
### TRA4. UI-Feinschliff
|
||||
|
||||
- Ergebnisflächen
|
||||
- Logs
|
||||
- klarere Rückmeldungen für Opfer und Auftraggeber
|
||||
|
||||
## 14. Hinweis für den Daemon
|
||||
|
||||
Der Daemon soll später explizit berücksichtigen:
|
||||
|
||||
- DB-Änderungen für `guardCount` und den neuen Underground-Typ werden projektseitig vorbereitet
|
||||
- Überfälle dürfen nie Totalverlust erzeugen
|
||||
- Lagerkapazität begrenzt reale Beute
|
||||
- Wachen reduzieren Erfolgsquote und Beutemenge
|
||||
|
||||
## 15. Definition of Done
|
||||
|
||||
Die erste vollständige Version gilt als fertig, wenn:
|
||||
|
||||
1. `raid_transport` im Untergrund auswählbar ist
|
||||
2. Transporte mit Wachen gestartet werden können
|
||||
3. der Daemon aktive Überfälle gegen echte Transporte auflösen kann
|
||||
4. das Opfer nie die komplette Fracht verliert
|
||||
5. Beute im nächstgelegenen Lager des Auftraggebers landet
|
||||
6. Opfer- und Auftraggeber-UI per Socket aktualisiert werden
|
||||
316
docs/FALUKANT_UI_WEBSOCKET.md
Normal file
316
docs/FALUKANT_UI_WEBSOCKET.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# Falukant: UI-Anpassung - WebSocket & Familie / Liebschaften
|
||||
|
||||
Dieses Dokument beschreibt die Nachrichten, die der externe Falukant-Daemon über den WebSocket-Broadcast sendet, damit die UI gezielt reagieren kann.
|
||||
|
||||
Transport:
|
||||
|
||||
- Alle Clients erhalten denselben Broadcast.
|
||||
- Die UI muss nach `user_id` filtern und nur Events für die eingeloggte Session verarbeiten.
|
||||
|
||||
## 1. Übersicht der Events
|
||||
|
||||
| `event` | Pflichtfelder | Typische UI-Reaktion |
|
||||
|---------|----------------|----------------------|
|
||||
| `falukantUpdateFamily` | `user_id`, `reason` | Gezielter Refresh Familie/Liebe/Geld je nach `reason` |
|
||||
| `falukantUpdateStatus` | `user_id` | Allgemeiner Status-/Spielstands-Refresh |
|
||||
| `falukantUpdateProductionCertificate` | `user_id`, `reason`, `old_certificate`, `new_certificate` | Produkte / Produktions-UI / Zertifikat neu laden |
|
||||
| `children_update` | `user_id` | Kinderliste / FamilyView aktualisieren |
|
||||
| `falukant_family_scandal_hint` | `relationship_id` | Optionaler Toast oder Log; kein `user_id` |
|
||||
| `falukantUpdateChurch` | `user_id`, `reason` | Kirchenämter, Bewerbungen, Ernennungen |
|
||||
| `falukantUpdateDebt` | `user_id`, `reason` | Schuldturm, Verzug, Pfändung, Freilassung |
|
||||
|
||||
## 2. JSON-Payloads
|
||||
|
||||
### 2.1 `falukantUpdateFamily`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateFamily",
|
||||
"user_id": 123,
|
||||
"reason": "daily"
|
||||
}
|
||||
```
|
||||
|
||||
`reason` ist immer genau einer dieser festen Strings:
|
||||
|
||||
- `daily`
|
||||
- `monthly`
|
||||
- `lover_installment`
|
||||
- `scandal`
|
||||
- `lover_birth`
|
||||
|
||||
### 2.2 `falukantUpdateChurch`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateChurch",
|
||||
"user_id": 123,
|
||||
"reason": "applications"
|
||||
}
|
||||
```
|
||||
|
||||
Mögliche `reason`:
|
||||
|
||||
- `applications`
|
||||
- `npc_decision`
|
||||
- `appointment`
|
||||
- `vacancy_fill`
|
||||
- `promotion`
|
||||
|
||||
### 2.3 `falukantUpdateStatus`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateStatus",
|
||||
"user_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
Dieses Event wird typischerweise direkt nach einem fachlichen Falukant-Event mit derselben `user_id` gesendet.
|
||||
|
||||
### 2.4 `falukantUpdateProductionCertificate`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateProductionCertificate",
|
||||
"user_id": 123,
|
||||
"reason": "daily_recalculation",
|
||||
"old_certificate": 2,
|
||||
"new_certificate": 3
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 `children_update`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "children_update",
|
||||
"user_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
Dieses Event tritt bei Geburt aus einer Liebschaft auf, meist zusammen mit:
|
||||
|
||||
- `falukantUpdateFamily` mit `reason: "lover_birth"`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
### 2.6 `falukant_family_scandal_hint`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukant_family_scandal_hint",
|
||||
"relationship_id": 456
|
||||
}
|
||||
```
|
||||
|
||||
Hinweis:
|
||||
|
||||
- Dieses Event enthält kein `user_id`.
|
||||
- Die UI kann es ignorieren oder optional nur für Log-/Toast-Zwecke verwenden.
|
||||
- Die eigentliche nutzerbezogene Aktualisierung läuft über `falukantUpdateFamily` mit `reason: "scandal"`.
|
||||
|
||||
### 2.7 `falukantUpdateDebt`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateDebt",
|
||||
"user_id": 123,
|
||||
"reason": "debtors_prison_entered"
|
||||
}
|
||||
```
|
||||
|
||||
Mögliche `reason`:
|
||||
|
||||
- `delinquency`
|
||||
- `debtors_prison_entered`
|
||||
- `asset_seizure`
|
||||
- `vehicle_liquidation`
|
||||
- `house_seizure`
|
||||
- `branch_closure`
|
||||
- `debtors_prison_released`
|
||||
|
||||
## 3. Fachliche Bedeutung von `reason`
|
||||
|
||||
### 3.1 `falukantUpdateFamily`
|
||||
|
||||
#### `reason: "daily"`
|
||||
|
||||
`daily` ist der Sammelgrund für tägliche Änderungen im Familien- und Liebschaftssystem.
|
||||
|
||||
Darunter fallen insbesondere:
|
||||
|
||||
- tägliche Drift und Änderung der Ehezufriedenheit
|
||||
- `marriage_public_stability`
|
||||
- `household_tension_score`
|
||||
- Ehe-Buffs und temporäre Zähler wie Geschenk-, Fest- oder Haus-Effekte
|
||||
- tägliche Liebschaftslogik für aktive Beziehungen
|
||||
- Rufverlust bei zwei oder mehr sichtbaren Liebschaften
|
||||
- Zufalls-Mali wie Gerücht oder Tadel
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Es gibt kein separates Event für „nur Ehe-Buff“.
|
||||
- Es gibt kein separates Event für „nur zwei sichtbare Liebschaften“.
|
||||
- Es gibt kein separates Event für „nur Gerücht/Tadel“.
|
||||
- Alles davon erscheint in der UI ausschließlich als `falukantUpdateFamily` mit `reason: "daily"`.
|
||||
|
||||
#### `reason: "monthly"`
|
||||
|
||||
`monthly` steht für monatliche Verarbeitung, insbesondere:
|
||||
|
||||
- Dienerschaftskosten
|
||||
- laufende Kosten
|
||||
- Unterversorgung
|
||||
- Geldänderungen
|
||||
|
||||
#### `reason: "lover_installment"`
|
||||
|
||||
`lover_installment` steht für die 2-Stunden-Unterhaltsbelastung von Liebschaften.
|
||||
|
||||
Die UI sollte dafür mindestens:
|
||||
|
||||
- Geld neu laden
|
||||
- Family-/Liebschaftsstatus neu laden
|
||||
|
||||
#### `reason: "scandal"`
|
||||
|
||||
`scandal` wird zusätzlich zu einem gelungenen Skandalwurf gesendet.
|
||||
|
||||
Typischer Ablauf:
|
||||
|
||||
- optional `falukant_family_scandal_hint`
|
||||
- `falukantUpdateFamily` mit `reason: "scandal"`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
Danach kann für denselben Nutzer am selben Tag zusätzlich noch `daily` folgen.
|
||||
|
||||
#### `reason: "lover_birth"`
|
||||
|
||||
`lover_birth` signalisiert ein neues Kind aus einer Liebschaft.
|
||||
|
||||
Meist folgen zusammen:
|
||||
|
||||
- `falukantUpdateFamily` mit `reason: "lover_birth"`
|
||||
- `children_update`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
### 3.2 `falukantUpdateChurch`
|
||||
|
||||
- `applications`: Spieler ist kirchlicher Vorgesetzter; offene Bewerbungen warten
|
||||
- `npc_decision`: NPC-Vorgesetzter hat entschieden
|
||||
- `appointment`: automatische Annahme älterer Bewerbung
|
||||
- `vacancy_fill`: Interimsbesetzung
|
||||
- `promotion`: reserviert / zukünftig
|
||||
|
||||
### 3.3 `falukantUpdateProductionCertificate`
|
||||
|
||||
- `daily_recalculation`: Zertifikat nach täglicher Prüfung geändert
|
||||
|
||||
### 3.4 `falukantUpdateDebt`
|
||||
|
||||
- `delinquency`: Mahnstufe oder Verzug aktualisiert
|
||||
- `debtors_prison_entered`: Eintritt in den Schuldturm
|
||||
- `asset_seizure`: Geld, Waren oder sonstige Vermögenswerte eingezogen
|
||||
- `vehicle_liquidation`: Fahrzeuge zwangsverkauft
|
||||
- `house_seizure`: Haus gepfändet
|
||||
- `branch_closure`: Niederlassung geschlossen
|
||||
- `debtors_prison_released`: Freilassung
|
||||
|
||||
## 4. Empfohlene Handler-Logik
|
||||
|
||||
```text
|
||||
onMessage(json):
|
||||
if json.user_id exists and json.user_id != currentUserId:
|
||||
return
|
||||
|
||||
switch json.event:
|
||||
case "falukantUpdateStatus":
|
||||
refreshPlayerStatus()
|
||||
return
|
||||
|
||||
case "falukantUpdateProductionCertificate":
|
||||
refreshProductsAndProductionUi()
|
||||
return
|
||||
|
||||
case "children_update":
|
||||
refreshChildrenAndFamilyView()
|
||||
return
|
||||
|
||||
case "falukantUpdateChurch":
|
||||
refreshChurchContextByReason(json.reason)
|
||||
return
|
||||
|
||||
case "falukantUpdateDebt":
|
||||
refreshDebtAndAffectedViews(json.reason)
|
||||
return
|
||||
|
||||
case "falukantUpdateFamily":
|
||||
switch json.reason:
|
||||
case "daily":
|
||||
refreshFamilyAndRelationships()
|
||||
refreshReputationIfNeeded()
|
||||
break
|
||||
case "monthly":
|
||||
refreshMoney()
|
||||
refreshFamilyAndRelationships()
|
||||
break
|
||||
case "lover_installment":
|
||||
refreshMoney()
|
||||
refreshFamilyAndRelationships()
|
||||
break
|
||||
case "scandal":
|
||||
showScandalToastOptional()
|
||||
refreshFamilyAndRelationships()
|
||||
refreshReputationIfNeeded()
|
||||
break
|
||||
case "lover_birth":
|
||||
refreshChildrenAndFamilyView()
|
||||
break
|
||||
return
|
||||
|
||||
case "falukant_family_scandal_hint":
|
||||
// optional: nur als Hinweis verarbeiten
|
||||
return
|
||||
```
|
||||
|
||||
## 5. Deduplizierung
|
||||
|
||||
Ein Nutzer kann kurz hintereinander mehrere relevante Events erhalten, zum Beispiel:
|
||||
|
||||
- `scandal`
|
||||
- danach `daily`
|
||||
- danach `falukantUpdateStatus`
|
||||
|
||||
oder:
|
||||
|
||||
- `falukantUpdateDebt`
|
||||
- direkt danach `falukantUpdateStatus`
|
||||
- zusätzlich `falukantUpdateFamily`
|
||||
|
||||
Die UI sollte deshalb:
|
||||
|
||||
- Refreshes bündeln oder entprellen
|
||||
- idempotente Reloads verwenden
|
||||
- nicht davon ausgehen, dass jeder fachliche Effekt einen eigenen Spezial-Eventpfad hat
|
||||
|
||||
## 6. Welche Daten sollten neu geladen werden?
|
||||
|
||||
| Situation | Sinnvolle Reaktion |
|
||||
|-----------|--------------------|
|
||||
| Jede `falukantUpdateFamily` | Family-/Relationship-Daten neu laden |
|
||||
| `reason: "monthly"` | Family-Daten plus Geld/Status neu laden |
|
||||
| `reason: "lover_installment"` | Geld plus Family-Daten neu laden |
|
||||
| `reason: "daily"` | Family-Daten neu laden, bei Bedarf auch Ruf-/Statusdaten |
|
||||
| `reason: "scandal"` | Family-Daten plus Ruf-/Statusdaten neu laden |
|
||||
| `children_update` / `lover_birth` | Kinderdaten und FamilyView neu laden |
|
||||
| `falukantUpdateChurch` | Kirchenämter, Bewerbungen, freie Positionen je nach `reason` |
|
||||
| `falukantUpdateProductionCertificate` | User-Status, Zertifikat, Produkte, Produktions-UI |
|
||||
| `falukantUpdateDebt` | Bank, Overview, House, Branch, ggf. Family |
|
||||
|
||||
## 7. Sonderfälle
|
||||
|
||||
| Fall | Verhalten |
|
||||
|------|-----------|
|
||||
| NPC ohne `user_id` | Keine nutzerbezogenen Socket-Events |
|
||||
| Mehrere Events kurz hintereinander | Normal; UI sollte damit robust umgehen |
|
||||
| Nur `falukantUpdateStatus` ohne Fach-Event | Kann von anderen Falukant-Workern kommen |
|
||||
180
docs/FALUKANT_UNDERGROUND_AFFAIR_DAEMON_HANDOFF.md
Normal file
180
docs/FALUKANT_UNDERGROUND_AFFAIR_DAEMON_HANDOFF.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Falukant: Daemon-Handoff für `investigate_affair`
|
||||
|
||||
## Zweck
|
||||
|
||||
Dieses Dokument beschreibt die Auswertung der Untergrundaktivität `investigate_affair` im externen Daemon.
|
||||
|
||||
Es ergänzt:
|
||||
|
||||
- [FALUKANT_UNDERGROUND_AFFAIR_PLAN.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_UNDERGROUND_AFFAIR_PLAN.md)
|
||||
- [FALUKANT_LOVERS_DAEMON_SPEC.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_SPEC.md)
|
||||
- [FALUKANT_LOVERS_DAEMON_HANDOFF.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_LOVERS_DAEMON_HANDOFF.md)
|
||||
|
||||
## Betroffene Daten
|
||||
|
||||
Der externe Daemon liest:
|
||||
|
||||
- `falukant_data.underground`
|
||||
- `falukant_type.underground`
|
||||
- `falukant_data.relationship`
|
||||
- `falukant_data.relationship_state`
|
||||
- `falukant_data.character`
|
||||
- optional `falukant_data.child_relation`
|
||||
|
||||
Relevant ist jeweils:
|
||||
|
||||
- Untergrundaktivität vom Typ `investigate_affair`
|
||||
- `performer_id`
|
||||
- `victim_id`
|
||||
- `parameters.goal`
|
||||
- `expose`
|
||||
- `blackmail`
|
||||
- `result`
|
||||
|
||||
## Erwarteter Input
|
||||
|
||||
Eine Aktivität ist für den Daemon verarbeitbar, wenn:
|
||||
|
||||
- `underground_type.tr = investigate_affair`
|
||||
- `result.status = pending`
|
||||
|
||||
Der Daemon soll dann beim Opfer prüfen:
|
||||
|
||||
- aktive Liebschaften
|
||||
- Sichtbarkeit und Diskretion dieser Liebschaften
|
||||
- evtl. bekannte uneheliche Kinder
|
||||
- Stand und Ansehen des Opfers
|
||||
|
||||
## Ergebnis-Schema in `underground.result`
|
||||
|
||||
Der Daemon schreibt nach der Auswertung ein JSON mit folgender Struktur:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "resolved",
|
||||
"outcome": "success",
|
||||
"discoveries": {
|
||||
"relationshipId": 123,
|
||||
"loverRole": "secret_affair",
|
||||
"visibility": 42,
|
||||
"acknowledged": false,
|
||||
"publicKnownChild": false
|
||||
},
|
||||
"visibilityDelta": 12,
|
||||
"reputationDelta": -3,
|
||||
"blackmailAmount": 1500,
|
||||
"notes": "Affair was uncovered and partially exposed."
|
||||
}
|
||||
```
|
||||
|
||||
Erlaubte Werte:
|
||||
|
||||
- `status`
|
||||
- `pending`
|
||||
- `resolved`
|
||||
- `failed`
|
||||
- `outcome`
|
||||
- `success`
|
||||
- `partial`
|
||||
- `failure`
|
||||
|
||||
## Auswertung: `goal = expose`
|
||||
|
||||
Ziel:
|
||||
|
||||
- Liebschaft öffentlich machen
|
||||
- Sichtbarkeit stark erhöhen
|
||||
- ggf. Skandal auslösen
|
||||
|
||||
Empfohlene Wirkung:
|
||||
|
||||
- `relationship_state.visibility +10..25`
|
||||
- optional `relationship_state.discretion -5..15`
|
||||
- sofortiger Reputationsschaden beim Opfer
|
||||
- bei sehr sichtbarer oder junger Beziehung zusätzliche Skandalchance
|
||||
|
||||
Zusätzliche Empfehlung:
|
||||
|
||||
- wenn die Beziehung bereits fast öffentlich war, darf `outcome = partial` gesetzt werden statt voller Erfolg
|
||||
|
||||
## Auswertung: `goal = blackmail`
|
||||
|
||||
Ziel:
|
||||
|
||||
- belastendes Wissen gewinnen
|
||||
- keinen sofortigen vollen öffentlichen Effekt erzeugen
|
||||
- ein Erpressungspotenzial vorbereiten
|
||||
|
||||
Empfohlene Wirkung:
|
||||
|
||||
- `relationship_state.visibility +3..10`
|
||||
- `blackmailAmount` setzen
|
||||
- kleiner oder kein sofortiger Reputationsschaden
|
||||
- optional separates Log oder spätere Forderung
|
||||
|
||||
Wenn ihr noch kein echtes Erpressungssystem habt:
|
||||
|
||||
- `blackmailAmount` trotzdem setzen
|
||||
- `notes` befüllen
|
||||
- UI zeigt den Vorgang als abgeschlossen mit Erpressungssumme
|
||||
|
||||
## Mindestregeln für Erfolg
|
||||
|
||||
Erfolgswahrscheinlichkeit sollte steigen bei:
|
||||
|
||||
- hoher vorhandener Sichtbarkeit
|
||||
- niedriger Diskretion
|
||||
- mehreren aktiven Liebschaften
|
||||
- bereits bekannten unehelichen Kindern
|
||||
|
||||
Erfolgswahrscheinlichkeit sollte sinken bei:
|
||||
|
||||
- hoher Diskretion
|
||||
- niedriger Sichtbarkeit
|
||||
- standesgemäß geführter Mätresse/Favorit auf hohem Rang
|
||||
|
||||
## Folgewirkungen auf Lovers-System
|
||||
|
||||
Bei Erfolg darf der Daemon auslösen:
|
||||
|
||||
- `falukant_family_scandal_hint`
|
||||
- `falukantUpdateFamily` mit `reason = scandal`
|
||||
- zusätzlich normale Status-Updates
|
||||
|
||||
Wenn `goal = expose`, sollte mindestens eine dieser Wirkungen eintreten:
|
||||
|
||||
- Sichtbarkeit steigt
|
||||
- Ruf sinkt
|
||||
- Skandalwahrscheinlichkeit steigt
|
||||
|
||||
Wenn `goal = blackmail`, sollte mindestens eine dieser Wirkungen eintreten:
|
||||
|
||||
- `blackmailAmount > 0`
|
||||
- kleiner Sichtbarkeitsanstieg
|
||||
- interner Merker für spätere Forderung
|
||||
|
||||
## UI-Erwartung
|
||||
|
||||
Das Frontend dieses Projekts erwartet derzeit:
|
||||
|
||||
- `type`
|
||||
- `goal`
|
||||
- `status`
|
||||
- `additionalInfo.blackmailAmount`
|
||||
|
||||
Optional nutzbar später:
|
||||
|
||||
- `discoveries`
|
||||
- `visibilityDelta`
|
||||
- `reputationDelta`
|
||||
- `notes`
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Die externe Umsetzung gilt als ausreichend, wenn:
|
||||
|
||||
1. `investigate_affair`-Einträge mit `status = pending` verarbeitet werden
|
||||
2. `result.status` danach nicht mehr `pending` ist
|
||||
3. `goal = expose` und `goal = blackmail` verschieden behandelt werden
|
||||
4. mindestens Sichtbarkeit oder Reputationswirkung zurückgeschrieben wird
|
||||
5. `blackmailAmount` bei Erpressung gesetzt werden kann
|
||||
67
docs/FALUKANT_UNDERGROUND_AFFAIR_PLAN.md
Normal file
67
docs/FALUKANT_UNDERGROUND_AFFAIR_PLAN.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Falukant: Restplan für Liebschafts-Ermittlung im Untergrund
|
||||
|
||||
## Ziel
|
||||
|
||||
Die neue Untergrundaktivität `investigate_affair` soll nicht nur auswählbar sein, sondern einen vollständigen technischen Pfad bekommen:
|
||||
|
||||
- Aktivität anlegen
|
||||
- Aktivität in der UI sichtbar machen
|
||||
- Ergebnisstruktur vorbereiten
|
||||
- externe Daemon-Auswertung eindeutig beschreiben
|
||||
|
||||
## Arbeitspakete
|
||||
|
||||
## UGA1. Aktivitätstyp im System verankern
|
||||
|
||||
Status: abgeschlossen
|
||||
|
||||
- Untergrundtyp `investigate_affair` anlegen
|
||||
- Ziele `expose` und `blackmail` definieren
|
||||
- UI-Auswahl in `UndergroundView` ergänzen
|
||||
- Produktions-SQL für Bestandsdatenbank bereitstellen
|
||||
|
||||
## UGA2. Aktivitätenliste im Frontend nutzbar machen
|
||||
|
||||
Status: abgeschlossen
|
||||
|
||||
- echten GET-Endpunkt für Untergrundaktivitäten bereitstellen
|
||||
- `UndergroundView.loadActivities()` aktivieren
|
||||
- Aktivitäten mit Typ, Ziel, Status und Zusatzinformation anzeigen
|
||||
|
||||
## UGA3. Ergebnisstruktur für spätere Auswertung definieren
|
||||
|
||||
Status: abgeschlossen
|
||||
|
||||
- Ergebnisformat für `underground.result` dokumentieren
|
||||
- Zustände `pending`, `resolved`, `failed` festlegen
|
||||
- Felder für `discoveries`, `visibilityDelta`, `reputationDelta`, `blackmailAmount` vorbereiten
|
||||
|
||||
## UGA4. Externe Daemon-Übergabe für Liebschafts-Ermittlung
|
||||
|
||||
Status: abgeschlossen
|
||||
|
||||
- Handoff-Dokument für den externen Daemon ergänzen
|
||||
- beschreiben, wie `investigate_affair` gelesen und aufgelöst wird
|
||||
- beschreiben, welche Folgewirkungen auf Liebschaften, Ansehen und Erpressung entstehen dürfen
|
||||
|
||||
## UGA5. Spätere Ausbaustufe
|
||||
|
||||
Status: bewusst offen
|
||||
|
||||
- echte Erpressungszustände im Spielmodell
|
||||
- UI für Forderungen, Schweigegeld, Gegenmaßnahmen
|
||||
- eigene WebSocket-Events für abgeschlossene Untergrund-Ergebnisse
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Der lokale Teil gilt als fertig, wenn:
|
||||
|
||||
1. `investigate_affair` im Untergrundformular auswählbar ist
|
||||
2. neue Aktivitäten in der Aktivitätenliste sichtbar sind
|
||||
3. Typ, Ziel und Status in der UI lesbar sind
|
||||
4. ein eindeutiges Result-Schema für den externen Daemon dokumentiert ist
|
||||
5. die externe Daemon-Übergabe die neue Aktivität vollständig beschreibt
|
||||
|
||||
## Restgrenze
|
||||
|
||||
Die tatsächliche Erfolgs-/Misserfolgsberechnung, das Aufdecken von Liebschaften und die Erpressungswirkung werden nicht in diesem Projekt ausgeführt, sondern im externen Daemon.
|
||||
178
docs/UMLAUT_NORMALIZATION_PLAN.md
Normal file
178
docs/UMLAUT_NORMALIZATION_PLAN.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Umlaut-Normalisierung Plan
|
||||
|
||||
## Ziel
|
||||
|
||||
Alle sichtbaren deutschsprachigen UI-Texte sollen konsistent echte Umlaute und korrektes `ß` verwenden.
|
||||
|
||||
Beispiele:
|
||||
- `ae` -> `ä`
|
||||
- `oe` -> `ö`
|
||||
- `ue` -> `ü`
|
||||
- `ss` -> `ß`, wenn orthografisch korrekt
|
||||
|
||||
Nicht Teil dieses Schritts:
|
||||
- technische Bezeichner, Dateinamen, Keys, Routen, API-Felder
|
||||
- bewusst ASCII-basierte interne Kennungen
|
||||
- englische, spanische oder backendseitige maschinennahe Werte
|
||||
- bestehende Konzept-/Audit-Dokumente, sofern nicht explizit gewünscht
|
||||
|
||||
## Leitregeln
|
||||
|
||||
- Nur sichtbare Texte anfassen.
|
||||
- Keine Übersetzungs-Keys umbenennen, wenn nur der angezeigte Wert falsch ist.
|
||||
- Keine Logikänderung mit Sprachkorrekturen vermischen.
|
||||
- `ss` nur dort zu `ß` ändern, wo es sprachlich korrekt ist.
|
||||
- Neue Texte immer direkt mit echter deutscher Schreibweise anlegen.
|
||||
|
||||
## Scope
|
||||
|
||||
### 1. Direkte UI-Texte in Vue-Dateien
|
||||
|
||||
Prüfen und korrigieren in:
|
||||
- `frontend/src/components/**/*.vue`
|
||||
- `frontend/src/views/**/*.vue`
|
||||
- `frontend/src/dialogues/**/*.vue`
|
||||
|
||||
Typische Problemfälle:
|
||||
- Überschriften
|
||||
- Buttons
|
||||
- Statushinweise
|
||||
- Hilfetexte
|
||||
- Leerzustände
|
||||
- Fehlermeldungen
|
||||
|
||||
### 2. i18n-Inhalte
|
||||
|
||||
Prüfen und korrigieren in:
|
||||
- `frontend/src/i18n/locales/de/**/*.json`
|
||||
|
||||
Besonders relevant:
|
||||
- Navigation
|
||||
- Header/Footer
|
||||
- Home
|
||||
- Blog
|
||||
- Forum
|
||||
- Vokabeltrainer
|
||||
- Minigames
|
||||
- Einstellungen
|
||||
- Admin
|
||||
|
||||
### 3. Gemeinsame Shell- und Systemtexte
|
||||
|
||||
Zuerst prüfen:
|
||||
- `frontend/src/components/AppSectionBar.vue`
|
||||
- `frontend/src/components/AppNavigation.vue`
|
||||
- `frontend/src/components/AppHeader.vue`
|
||||
- `frontend/src/components/AppFooter.vue`
|
||||
- `frontend/src/components/DialogWidget.vue`
|
||||
- `frontend/src/components/MessageboxWidget.vue`
|
||||
|
||||
### 4. Produktbereiche mit hoher Sichtbarkeit
|
||||
|
||||
Danach prüfen:
|
||||
- `frontend/src/views/home/**/*`
|
||||
- `frontend/src/views/social/**/*`
|
||||
- `frontend/src/views/falukant/**/*`
|
||||
- `frontend/src/views/minigames/**/*`
|
||||
- `frontend/src/views/settings/**/*`
|
||||
- `frontend/src/views/blog/**/*`
|
||||
- `frontend/src/views/admin/**/*`
|
||||
|
||||
## Abarbeitung
|
||||
|
||||
### Phase A: Inventur
|
||||
|
||||
1. Fundstellen mit Suchmustern sammeln.
|
||||
2. Treffer in drei Klassen sortieren:
|
||||
- `sichtbarer UI-Text`
|
||||
- `i18n-Wert`
|
||||
- `nicht anfassen` wie Variablen, Klassen, Keys, Pfade
|
||||
|
||||
Empfohlene Suchmuster:
|
||||
- `Persoen`
|
||||
- `Gaeste`
|
||||
- `Zurueck`
|
||||
- `Uebersicht`
|
||||
- `Loesch`
|
||||
- `Fuer`
|
||||
- `Oeff`
|
||||
- `Schli`
|
||||
- `groess`
|
||||
- `aend`
|
||||
- `moeg`
|
||||
- `ueber`
|
||||
- `uebrig`
|
||||
- `fuer`
|
||||
- `waehr`
|
||||
- `muess`
|
||||
- `koenn`
|
||||
|
||||
### Phase B: Shell zuerst
|
||||
|
||||
Zuerst alle global sichtbaren Texte korrigieren:
|
||||
- Bereichsleisten
|
||||
- Navigation
|
||||
- Header
|
||||
- Footer
|
||||
- Standarddialoge
|
||||
|
||||
Ziel:
|
||||
- zentrale UI sofort sprachlich konsistent
|
||||
|
||||
### Phase C: i18n-DE bereinigen
|
||||
|
||||
Danach alle deutschen Locale-Dateien durchgehen.
|
||||
|
||||
Vorgehen:
|
||||
- nur Werte ändern, nicht die Key-Namen
|
||||
- orthografische Einzelprüfung bei `ss` -> `ß`
|
||||
- HTML-haltige Texte mit prüfen, damit keine alten ASCII-Umschreibungen stehen bleiben
|
||||
|
||||
### Phase D: Direkttexte in Views und Dialogen
|
||||
|
||||
Dann alle nicht-i18n-basierten sichtbaren Texte korrigieren.
|
||||
|
||||
Priorität:
|
||||
1. Home, Navigation, Auth
|
||||
2. Social, Blog, Settings
|
||||
3. Falukant, Minigames, Admin
|
||||
|
||||
### Phase E: Konsistenzreview
|
||||
|
||||
Zum Schluss ein kompletter Review auf typische Restfehler:
|
||||
- `ue` in sichtbaren Labels
|
||||
- `oe` in Überschriften
|
||||
- `ae` in Buttons und Hinweisen
|
||||
- `ss` statt `ß` in Wörtern wie `dass`, `groß`, `außer`, `heißen`, `Fuß`, `Maß`
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
Der Schritt gilt als abgeschlossen, wenn:
|
||||
- in allen sichtbaren deutschen UI-Texten keine ASCII-Umschreibungen mehr verbleiben
|
||||
- zentrale Shell-Texte vollständig normalisiert sind
|
||||
- `de`-Locale-Dateien keine falschen Umschreibungen mehr enthalten
|
||||
- Builds weiterhin sauber laufen
|
||||
- keine technischen Keys oder internen Bezeichner versehentlich geändert wurden
|
||||
|
||||
## Risiken
|
||||
|
||||
- versehentliche Änderung von technischen Strings statt UI-Texten
|
||||
- falsche `ß`-Korrekturen in Fällen, in denen `ss` korrekt ist
|
||||
- Mischung aus i18n-Texten und hart codierten Texten kann zu doppelter Pflege führen
|
||||
|
||||
## Umsetzungsempfehlung
|
||||
|
||||
Die eigentliche Umsetzung sollte in zwei Arbeitsblöcken passieren:
|
||||
|
||||
1. `UN1`
|
||||
Shell + i18n-DE + hochsichtbare Bereiche
|
||||
|
||||
2. `UN2`
|
||||
Restliche Views/Dialoge + Abschlussreview
|
||||
|
||||
## Ergebnisdokumentation
|
||||
|
||||
Nach Abschluss sollte kurz dokumentiert werden:
|
||||
- welche Dateien geändert wurden
|
||||
- ob nur sichtbare Texte geändert wurden
|
||||
- ob noch bewusst ASCII-basierte technische Strings bestehen
|
||||
@@ -343,6 +343,36 @@ Aktueller Stand:
|
||||
- breite Tabellen auf kleinen Screens per horizontalem Scroll-Fallback abgesichert
|
||||
- globale Touch-Ziele fuer Buttons leicht vergroessert und letzte Shell-Kanten geglaettet
|
||||
|
||||
### Phase U6: Vereinfachung und Restentruempelung
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- die letzten spuerbaren Bedienhuerden aus Altmustern, Scrolllogik und funktionslastigen Ansichten werden systematisch entfernt
|
||||
|
||||
Arbeit:
|
||||
|
||||
- verbliebene `alert`-/`confirm`-Fluesse auf das zentrale Feedbacksystem umstellen
|
||||
- verschachtelte Scrollcontainer in Falukant, Admin und Minigames entfernen oder entkoppeln
|
||||
- tabellenlastige Kernansichten auf klarere Aufgabenreihenfolge pruefen
|
||||
- Debug-/Altinteraktionen aus grossen Kernviews reduzieren, wenn sie Bedienbarkeit oder Folgepflege stoeren
|
||||
- Direktwege, Rueckspruenge und Fokusverhalten in den haeufigsten Hauptpfaden nachziehen
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- `U6.1` abgeschlossen
|
||||
- `U6.2` abgeschlossen
|
||||
- `U6.3` abgeschlossen
|
||||
- `U6.4` abgeschlossen
|
||||
- `U6.5` abgeschlossen
|
||||
- aus der Review nach U5 als eigener Nachlauf identifiziert
|
||||
- Fokus bewusst nicht mehr auf Redesign, sondern auf Reibungsabbau in realen Nutzungswegen
|
||||
- priorisierte Teilpakete:
|
||||
- `U6.1 Feedback vereinheitlichen`
|
||||
- `U6.2 Scroll- und Layoutfallen entfernen`
|
||||
- `U6.3 Tabellen- und Arbeitsflaechen vereinfachen`
|
||||
- `U6.4 Interaktionsaltlasten reduzieren`
|
||||
- `U6.5 Direktwege und Ruecklogik polieren`
|
||||
|
||||
## Konkreter Arbeitskatalog
|
||||
|
||||
### 1. Shell und Navigation
|
||||
@@ -378,6 +408,13 @@ Aktueller Stand:
|
||||
- breite Inhalte auf kleine Screens pruefen
|
||||
- Dialoge und Tabellen fuer Touch pruefen
|
||||
|
||||
### 6. Vereinfachungsreview
|
||||
|
||||
- Restbestände an `alert`, `confirm` und lokalen Sonderdialogen abbauen
|
||||
- komplexe Tabellenbereiche in Aufgabenfolge statt nur Datenanzeige gliedern
|
||||
- verschachtelte Scrollbereiche konsequent entfernen
|
||||
- Debug-/Sonderlogik in Kerninteraktionen auf Bedienrelevanz pruefen
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Das Bedienbarkeitsprojekt gilt als abgeschlossen, wenn:
|
||||
@@ -386,6 +423,7 @@ Das Bedienbarkeitsprojekt gilt als abgeschlossen, wenn:
|
||||
- P1- und P2-Probleme aus dem Audit abgearbeitet sind
|
||||
- Navigation, Formulare, Dialoge und Feedback nach gemeinsamen Regeln funktionieren
|
||||
- Kernaufgaben auf Desktop und kleinem Viewport ohne strukturelle Reibung moeglich sind
|
||||
- verbleibende Altinteraktionen in Kernpfaden keine zusaetzliche Bedienlogik mehr erzwingen
|
||||
- Restpunkte nur noch P3/P4-Feinschliff sind
|
||||
|
||||
## Empfohlene Reihenfolge
|
||||
@@ -395,7 +433,8 @@ Das Bedienbarkeitsprojekt gilt als abgeschlossen, wenn:
|
||||
3. Formulare und Abschlusslogik
|
||||
4. Falukant, Vokabeltrainer, Admin, Minigames
|
||||
5. Mobile Endabnahme
|
||||
6. Vereinfachungsnachlauf ueber Feedback, Scrolllogik und tabellenlastige Restbereiche
|
||||
|
||||
## Naechster konkreter Schritt
|
||||
|
||||
Der erste sinnvolle Umsetzungsschritt ist nicht sofort Code, sondern ein kurzer UX-Audit-Durchgang ueber die wichtigsten Aufgabenfluesse. Daraus entsteht ein priorisierter Problemkatalog, auf dessen Basis die Bedienbarkeitsarbeit strukturiert umgesetzt wird.
|
||||
Der naechste sinnvolle Umsetzungsschritt ist `U6.1 Feedback vereinheitlichen`: alle verbliebenen `alert`-/`confirm`-Fluesse in Kernpfaden auf das zentrale Feedback- und Bestätigungssystem ziehen und dabei zugleich die groebsten Altinteraktionen in Falukant, Kalender, Vokabeln und Admin bereinigen.
|
||||
|
||||
@@ -9,36 +9,36 @@
|
||||
"optimize-models": "node scripts/optimize-glb.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/extension-color": "^2.14.0",
|
||||
"@tiptap/extension-image": "^2.14.0",
|
||||
"@tiptap/extension-text-align": "^2.14.0",
|
||||
"@tiptap/extension-text-style": "^2.14.0",
|
||||
"@tiptap/extension-underline": "^2.14.0",
|
||||
"@tiptap/starter-kit": "^2.14.0",
|
||||
"@tiptap/vue-3": "^2.14.0",
|
||||
"axios": "^1.7.2",
|
||||
"@tiptap/extension-color": "^2.27.2",
|
||||
"@tiptap/extension-image": "^2.27.2",
|
||||
"@tiptap/extension-text-align": "^2.27.2",
|
||||
"@tiptap/extension-text-style": "^2.27.2",
|
||||
"@tiptap/extension-underline": "^2.27.2",
|
||||
"@tiptap/starter-kit": "^2.27.2",
|
||||
"@tiptap/vue-3": "^2.27.2",
|
||||
"axios": "^1.13.6",
|
||||
"date-fns": "^3.6.0",
|
||||
"dompurify": "^3.2.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"dompurify": "^3.3.3",
|
||||
"dotenv": "^16.6.1",
|
||||
"mitt": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"three": "^0.169.0",
|
||||
"vue": "~3.4.31",
|
||||
"vue-i18n": "^10.0.0-beta.2",
|
||||
"vue-multiselect": "^3.1.0",
|
||||
"vue-router": "^4.0.13",
|
||||
"vuetify": "^3.7.4",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"three": "^0.183.2",
|
||||
"vue": "~3.5.30",
|
||||
"vue-i18n": "^10.0.8",
|
||||
"vue-multiselect": "^3.5.0",
|
||||
"vue-router": "^4.6.4",
|
||||
"vuetify": "^3.12.3",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gltf-transform/cli": "^4.3.0",
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@vitejs/plugin-vue": "^5.1.3",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"assert": "^2.1.0",
|
||||
"sass": "^1.77.8",
|
||||
"sass": "^1.98.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"util": "^0.12.5",
|
||||
"vite": "^6.3.5"
|
||||
"vite": "^6.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,16 @@
|
||||
.app-content__inner {
|
||||
max-width: var(--shell-max-width);
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 14px 18px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-content__inner > :last-child {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import { showInfo } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AppFooter',
|
||||
@@ -71,10 +72,10 @@ export default {
|
||||
},
|
||||
// Daemon WebSocket deaktiviert - diese Funktionen sind nicht mehr verfügbar
|
||||
async showFalukantDaemonStatus() {
|
||||
console.log('⚠️ Daemon WebSocket deaktiviert - Status nicht verfügbar');
|
||||
showInfo(this, 'Der Systemstatus ist in dieser Ansicht derzeit nicht direkt verfügbar.');
|
||||
},
|
||||
handleDaemonMessage(event) {
|
||||
console.log('⚠️ Daemon WebSocket deaktiviert - keine Nachrichten verarbeitet');
|
||||
handleDaemonMessage() {
|
||||
// Status-Events werden hier bewusst nicht verarbeitet.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -175,24 +175,11 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { createApp } from 'vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { EventBus } from '@/utils/eventBus.js';
|
||||
|
||||
import RandomChatDialog from '../dialogues/chat/RandomChatDialog.vue';
|
||||
import MultiChatDialog from '../dialogues/chat/MultiChatDialog.vue';
|
||||
|
||||
// Wichtig: die zentrale Instanzen importieren
|
||||
import store from '@/store';
|
||||
import router from '@/router';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export default {
|
||||
name: 'AppNavigation',
|
||||
components: {
|
||||
RandomChatDialog,
|
||||
MultiChatDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
forumList: [],
|
||||
@@ -292,7 +279,8 @@ export default {
|
||||
this.pinnedSubKey = this.pinnedSubKey === key ? null : key;
|
||||
},
|
||||
|
||||
collapseMenus() {
|
||||
collapseMenus(options = {}) {
|
||||
const { blurActiveElement = true } = options;
|
||||
this.expandedMainKey = null;
|
||||
this.expandedSubKey = null;
|
||||
this.pinnedMainKey = null;
|
||||
@@ -305,11 +293,13 @@ export default {
|
||||
this.suppressHover = false;
|
||||
this.hoverReleaseTimer = null;
|
||||
}, 180);
|
||||
this.$nextTick(() => {
|
||||
if (document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
});
|
||||
if (blurActiveElement) {
|
||||
this.$nextTick(() => {
|
||||
if (document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleDocumentClick(event) {
|
||||
@@ -317,7 +307,7 @@ export default {
|
||||
if (!root || root.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
this.collapseMenus();
|
||||
this.collapseMenus({ blurActiveElement: false });
|
||||
},
|
||||
|
||||
handleDocumentKeydown(event) {
|
||||
@@ -435,10 +425,21 @@ export default {
|
||||
},
|
||||
|
||||
openChat(userId) {
|
||||
console.log('openChat:', userId);
|
||||
// Datei erstellen und ans body anhängen
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const dialogRef = this.$root.$refs.multiChatDialog;
|
||||
const friend = this.friendsList.find((entry) => entry.id === userId);
|
||||
if (!dialogRef || typeof dialogRef.open !== 'function') {
|
||||
this.openProfile(userId);
|
||||
return;
|
||||
}
|
||||
dialogRef.open();
|
||||
if (!friend?.username) {
|
||||
return;
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
if (dialogRef.usersInRoom?.some((user) => user.name === friend.username)) {
|
||||
dialogRef.selectedTargetUser = friend.username;
|
||||
}
|
||||
}, 250);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="app-section-bar__back"
|
||||
@click="navigateBack"
|
||||
>
|
||||
Zurueck
|
||||
Zurück
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
@@ -25,13 +25,13 @@ const SECTION_LABELS = [
|
||||
{ test: (path) => path.startsWith('/settings'), label: 'Einstellungen' },
|
||||
{ test: (path) => path.startsWith('/admin'), label: 'Administration' },
|
||||
{ test: (path) => path.startsWith('/minigames'), label: 'Minispiele' },
|
||||
{ test: (path) => path.startsWith('/personal'), label: 'Persoenlich' },
|
||||
{ test: (path) => path.startsWith('/personal'), label: 'Persönlich' },
|
||||
{ test: (path) => path.startsWith('/blogs'), label: 'Blog' }
|
||||
];
|
||||
|
||||
const TITLE_MAP = {
|
||||
Friends: 'Freunde',
|
||||
Guestbook: 'Gaestebuch',
|
||||
Guestbook: 'Gästebuch',
|
||||
'Search users': 'Suche',
|
||||
Gallery: 'Galerie',
|
||||
Forum: 'Forum',
|
||||
@@ -46,7 +46,7 @@ const TITLE_MAP = {
|
||||
VocabCourse: 'Kurs',
|
||||
VocabLesson: 'Lektion',
|
||||
FalukantCreate: 'Charakter erstellen',
|
||||
FalukantOverview: 'Uebersicht',
|
||||
FalukantOverview: 'Übersicht',
|
||||
BranchView: 'Niederlassung',
|
||||
MoneyHistoryView: 'Geldverlauf',
|
||||
FalukantFamily: 'Familie',
|
||||
@@ -60,9 +60,9 @@ const TITLE_MAP = {
|
||||
HealthView: 'Gesundheit',
|
||||
PoliticsView: 'Politik',
|
||||
UndergroundView: 'Untergrund',
|
||||
'Personal settings': 'Persoenliche Daten',
|
||||
'Personal settings': 'Persönliche Daten',
|
||||
'View settings': 'Ansicht',
|
||||
'Sexuality settings': 'Sexualitaet',
|
||||
'Sexuality settings': 'Sexualität',
|
||||
'Flirt settings': 'Flirt',
|
||||
'Account settings': 'Account',
|
||||
Interests: 'Interessen',
|
||||
@@ -132,11 +132,19 @@ export default {
|
||||
return '/admin/users';
|
||||
}
|
||||
|
||||
if (window.history.length > 1) {
|
||||
return '__history_back__';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
navigateBack() {
|
||||
if (this.backTarget === '__history_back__') {
|
||||
this.$router.back();
|
||||
return;
|
||||
}
|
||||
if (this.backTarget) {
|
||||
this.$router.push(this.backTarget);
|
||||
}
|
||||
|
||||
@@ -17,23 +17,33 @@ import { getApiBaseURL } from '@/utils/axios.js';
|
||||
/** Backend-Route: GET /api/models/3d/falukant/characters/:filename (Proxy mit Draco-Optimierung) */
|
||||
const MODELS_API_PATH = '/api/models/3d/falukant/characters';
|
||||
let threeRuntimePromise = null;
|
||||
let threeLoadersPromise = null;
|
||||
let threeModelRuntimePromise = null;
|
||||
|
||||
async function loadThreeRuntime() {
|
||||
if (!threeRuntimePromise) {
|
||||
threeRuntimePromise = Promise.all([
|
||||
import('three'),
|
||||
import('three/addons/loaders/GLTFLoader.js'),
|
||||
import('three/addons/loaders/DRACOLoader.js')
|
||||
]).then(([THREE, { GLTFLoader }, { DRACOLoader }]) => ({
|
||||
THREE,
|
||||
GLTFLoader,
|
||||
DRACOLoader
|
||||
}));
|
||||
threeRuntimePromise = import('@/utils/threeRuntime.js');
|
||||
}
|
||||
|
||||
return threeRuntimePromise;
|
||||
}
|
||||
|
||||
async function loadThreeLoaders() {
|
||||
if (!threeLoadersPromise) {
|
||||
threeLoadersPromise = import('@/utils/threeLoaders.js');
|
||||
}
|
||||
|
||||
return threeLoadersPromise;
|
||||
}
|
||||
|
||||
async function loadThreeModelRuntime() {
|
||||
if (!threeModelRuntimePromise) {
|
||||
threeModelRuntimePromise = import('@/utils/threeModelRuntime.js');
|
||||
}
|
||||
|
||||
return threeModelRuntimePromise;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Character3D',
|
||||
props: {
|
||||
@@ -50,6 +60,10 @@ export default {
|
||||
noBackground: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
lightweight: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -65,7 +79,9 @@ export default {
|
||||
clock: null,
|
||||
baseYPosition: 0,
|
||||
showFallback: false,
|
||||
threeRuntime: null
|
||||
threeRuntime: null,
|
||||
threeLoaders: null,
|
||||
threeModelRuntime: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -149,49 +165,65 @@ export default {
|
||||
return this.threeRuntime;
|
||||
},
|
||||
|
||||
async ensureThreeLoaders() {
|
||||
if (!this.threeLoaders) {
|
||||
this.threeLoaders = markRaw(await loadThreeLoaders());
|
||||
}
|
||||
|
||||
return this.threeLoaders;
|
||||
},
|
||||
|
||||
async ensureThreeModelRuntime() {
|
||||
if (!this.threeModelRuntime) {
|
||||
this.threeModelRuntime = markRaw(await loadThreeModelRuntime());
|
||||
}
|
||||
|
||||
return this.threeModelRuntime;
|
||||
},
|
||||
|
||||
async init3D() {
|
||||
const container = this.$refs.container;
|
||||
if (!container) return;
|
||||
this.showFallback = false;
|
||||
const { THREE } = await this.ensureThreeRuntime();
|
||||
this.clock = markRaw(new THREE.Clock());
|
||||
const runtime = await this.ensureThreeRuntime();
|
||||
this.clock = markRaw(new runtime.Clock());
|
||||
|
||||
// Scene erstellen - markRaw verwenden, um Vue's Reactivity zu vermeiden
|
||||
this.scene = markRaw(new THREE.Scene());
|
||||
this.scene = markRaw(new runtime.Scene());
|
||||
if (!this.noBackground) {
|
||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
||||
this.scene.background = new runtime.Color(0xf0f0f0);
|
||||
await this.loadBackground();
|
||||
}
|
||||
|
||||
// Camera erstellen
|
||||
const aspect = container.clientWidth / container.clientHeight;
|
||||
this.camera = markRaw(new THREE.PerspectiveCamera(50, aspect, 0.1, 1000));
|
||||
this.camera = markRaw(new runtime.PerspectiveCamera(50, aspect, 0.1, 1000));
|
||||
this.camera.position.set(0, 1.5, 3);
|
||||
this.camera.lookAt(0, 1, 0);
|
||||
|
||||
// Renderer erstellen
|
||||
this.renderer = markRaw(new THREE.WebGLRenderer({ antialias: true, alpha: true }));
|
||||
this.renderer = markRaw(new runtime.WebGLRenderer({ antialias: true, alpha: true }));
|
||||
this.renderer.setSize(container.clientWidth, container.clientHeight);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
container.appendChild(this.renderer.domElement);
|
||||
|
||||
// Verbesserte Beleuchtung für hellere Modelle
|
||||
// Mehr ambient light für gleichmäßigere Ausleuchtung
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
|
||||
const ambientLight = new runtime.AmbientLight(0xffffff, 1.0);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
// Hauptlicht von vorne oben - stärker
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
const directionalLight = new runtime.DirectionalLight(0xffffff, 1.2);
|
||||
directionalLight.position.set(5, 10, 5);
|
||||
this.scene.add(directionalLight);
|
||||
|
||||
// Zusätzliches Licht von hinten - heller
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.75);
|
||||
const backLight = new runtime.DirectionalLight(0xffffff, 0.75);
|
||||
backLight.position.set(-5, 5, -5);
|
||||
this.scene.add(backLight);
|
||||
|
||||
// Zusätzliches Seitenlicht für mehr Tiefe
|
||||
const sideLight = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
const sideLight = new runtime.DirectionalLight(0xffffff, 0.5);
|
||||
sideLight.position.set(-5, 5, 5);
|
||||
this.scene.add(sideLight);
|
||||
|
||||
@@ -200,13 +232,13 @@ export default {
|
||||
},
|
||||
|
||||
async loadBackground() {
|
||||
const { THREE } = await this.ensureThreeRuntime();
|
||||
const runtime = await this.ensureThreeRuntime();
|
||||
// Optimierte Versionen (512×341, ~130 KB); Originale ~3 MB
|
||||
const backgrounds = ['bg1_opt.png', 'bg2_opt.png'];
|
||||
const randomBg = backgrounds[Math.floor(Math.random() * backgrounds.length)];
|
||||
const bgPath = `/images/falukant/backgrounds/${randomBg}`;
|
||||
|
||||
const loader = new THREE.TextureLoader();
|
||||
const loader = new runtime.TextureLoader();
|
||||
loader.load(
|
||||
bgPath,
|
||||
(texture) => {
|
||||
@@ -220,7 +252,7 @@ export default {
|
||||
console.warn('Fehler beim Laden des Hintergrunds:', error);
|
||||
// Fallback auf Standardfarbe bei Fehler
|
||||
if (this.scene) {
|
||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
||||
this.scene.background = new runtime.Color(0xf0f0f0);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -228,7 +260,8 @@ export default {
|
||||
|
||||
async loadModel() {
|
||||
if (!this.scene) return;
|
||||
const { THREE, GLTFLoader, DRACOLoader } = await this.ensureThreeRuntime();
|
||||
const modelRuntime = await this.ensureThreeModelRuntime();
|
||||
const loaders = await this.ensureThreeLoaders();
|
||||
|
||||
// Altes Modell entfernen
|
||||
if (this.model) {
|
||||
@@ -252,38 +285,44 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
const dracoLoader = new DRACOLoader();
|
||||
const dracoLoader = new loaders.DRACOLoader();
|
||||
dracoLoader.setDecoderPath('/draco/gltf/');
|
||||
const loader = new GLTFLoader();
|
||||
const loader = new loaders.GLTFLoader();
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
const base = getApiBaseURL();
|
||||
const prefix = base ? `${base}${MODELS_API_PATH}` : MODELS_API_PATH;
|
||||
|
||||
// Fallback-Hierarchie:
|
||||
// 1. Zuerst versuchen, Modell für genaues Alter zu laden (z.B. female_1y.glb)
|
||||
// 2. Falls nicht vorhanden, Altersbereich verwenden (z.B. female_toddler.glb)
|
||||
// 3. Falls auch nicht vorhanden, Basis-Modell verwenden (z.B. female.glb)
|
||||
// Standard:
|
||||
// 1. Exaktes Altersmodell
|
||||
// 2. Altersbereich
|
||||
// 3. Basis-Modell
|
||||
// Lightweight:
|
||||
// 1. Altersbereich
|
||||
// 2. Basis-Modell
|
||||
const exactAgePath = this.exactAgeModelPath;
|
||||
const ageGroupPath = this.modelPath;
|
||||
const fallbackPath = `${prefix}/${this.actualGender}.glb`;
|
||||
|
||||
let gltf;
|
||||
try {
|
||||
// Versuche zuerst genaues Alter
|
||||
try {
|
||||
gltf = await loader.loadAsync(exactAgePath);
|
||||
console.log(`Loaded exact age model: ${exactAgePath}`);
|
||||
} catch (exactAgeError) {
|
||||
// Falls genaues Alter nicht existiert, versuche Altersbereich
|
||||
if (this.lightweight) {
|
||||
try {
|
||||
gltf = await loader.loadAsync(ageGroupPath);
|
||||
console.log(`Loaded age group model: ${ageGroupPath}`);
|
||||
} catch (ageGroupError) {
|
||||
// Falls Altersbereich nicht existiert, verwende Basis-Modell
|
||||
console.warn(`Could not load ${ageGroupPath}, trying fallback model`);
|
||||
gltf = await loader.loadAsync(fallbackPath);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
gltf = await loader.loadAsync(exactAgePath);
|
||||
} catch (exactAgeError) {
|
||||
try {
|
||||
gltf = await loader.loadAsync(ageGroupPath);
|
||||
} catch (ageGroupError) {
|
||||
gltf = await loader.loadAsync(fallbackPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
dracoLoader.dispose();
|
||||
@@ -293,8 +332,8 @@ export default {
|
||||
this.model = markRaw(gltf.scene);
|
||||
|
||||
// Initiale Bounding Box für Größenberechnung (vor Skalierung)
|
||||
const initialBox = new THREE.Box3().setFromObject(this.model);
|
||||
const initialSize = initialBox.getSize(new THREE.Vector3());
|
||||
const initialBox = new modelRuntime.Box3().setFromObject(this.model);
|
||||
const initialSize = initialBox.getSize(new modelRuntime.Vector3());
|
||||
|
||||
// Skalierung basierend auf Alter
|
||||
const age = this.actualAge;
|
||||
@@ -318,8 +357,8 @@ export default {
|
||||
this.model.scale.set(modelScale, modelScale, modelScale);
|
||||
|
||||
// Bounding Box NACH dem Skalieren neu berechnen
|
||||
const scaledBox = new THREE.Box3().setFromObject(this.model);
|
||||
const scaledCenter = scaledBox.getCenter(new THREE.Vector3());
|
||||
const scaledBox = new modelRuntime.Box3().setFromObject(this.model);
|
||||
const scaledCenter = scaledBox.getCenter(new modelRuntime.Vector3());
|
||||
|
||||
// Modell zentrieren basierend auf der skalierten Bounding Box
|
||||
// Position direkt setzen statt zu subtrahieren, um Proxy-Probleme zu vermeiden
|
||||
@@ -331,7 +370,7 @@ export default {
|
||||
|
||||
// Animationen laden falls vorhanden
|
||||
if (gltf.animations && gltf.animations.length > 0) {
|
||||
this.mixer = markRaw(new THREE.AnimationMixer(this.model));
|
||||
this.mixer = markRaw(new modelRuntime.AnimationMixer(this.model));
|
||||
gltf.animations.forEach((clip) => {
|
||||
this.mixer.clipAction(clip).play();
|
||||
});
|
||||
|
||||
@@ -57,11 +57,12 @@ export default {
|
||||
loading: false,
|
||||
error: null,
|
||||
isDragging: false,
|
||||
_daemonMessageHandler: null
|
||||
_daemonMessageHandler: null,
|
||||
pendingFetchTimer: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket']),
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
isFalukantWidget() {
|
||||
return this.endpoint && String(this.endpoint).includes('falukant');
|
||||
},
|
||||
@@ -89,25 +90,63 @@ export default {
|
||||
if (this.isFalukantWidget) this.setupSocketListeners();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.pendingFetchTimer) {
|
||||
clearTimeout(this.pendingFetchTimer);
|
||||
this.pendingFetchTimer = null;
|
||||
}
|
||||
if (this.isFalukantWidget) this.teardownSocketListeners();
|
||||
},
|
||||
methods: {
|
||||
matchesCurrentUser(eventData) {
|
||||
if (eventData?.user_id == null) {
|
||||
return true;
|
||||
}
|
||||
const currentIds = [this.user?.id, this.user?.hashedId]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value));
|
||||
return currentIds.includes(String(eventData.user_id));
|
||||
},
|
||||
setupSocketListeners() {
|
||||
this.teardownSocketListeners();
|
||||
const daemonEvents = ['falukantUpdateStatus', 'stock_change', 'familychanged'];
|
||||
const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'falukantUpdateDebt', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'];
|
||||
if (this.daemonSocket) {
|
||||
this._daemonMessageHandler = (event) => {
|
||||
if (event.data === 'ping') return;
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (daemonEvents.includes(data.event)) this.fetchData();
|
||||
if (daemonEvents.includes(data.event) && this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
} catch (_) {}
|
||||
};
|
||||
this.daemonSocket.addEventListener('message', this._daemonMessageHandler);
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.on('falukantUpdateStatus', () => this.fetchData());
|
||||
this.socket.on('falukantBranchUpdate', () => this.fetchData());
|
||||
this._statusSocketHandler = (data) => {
|
||||
if (this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
};
|
||||
this._familySocketHandler = (data) => {
|
||||
if (this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
};
|
||||
this._churchSocketHandler = (data) => {
|
||||
if (this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
};
|
||||
this._debtSocketHandler = (data) => {
|
||||
if (this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
};
|
||||
this._childrenSocketHandler = (data) => {
|
||||
if (this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
};
|
||||
this._productionCertificateSocketHandler = (data) => {
|
||||
if (this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
};
|
||||
this._branchSocketHandler = () => this.queueFetchData();
|
||||
|
||||
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
|
||||
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
|
||||
this.socket.on('falukantUpdateChurch', this._churchSocketHandler);
|
||||
this.socket.on('falukantUpdateDebt', this._debtSocketHandler);
|
||||
this.socket.on('children_update', this._childrenSocketHandler);
|
||||
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
|
||||
this.socket.on('falukantBranchUpdate', this._branchSocketHandler);
|
||||
}
|
||||
},
|
||||
teardownSocketListeners() {
|
||||
@@ -116,10 +155,24 @@ export default {
|
||||
this._daemonMessageHandler = null;
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.off('falukantUpdateStatus');
|
||||
this.socket.off('falukantBranchUpdate');
|
||||
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
|
||||
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
|
||||
if (this._churchSocketHandler) this.socket.off('falukantUpdateChurch', this._churchSocketHandler);
|
||||
if (this._debtSocketHandler) this.socket.off('falukantUpdateDebt', this._debtSocketHandler);
|
||||
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
|
||||
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
|
||||
if (this._branchSocketHandler) this.socket.off('falukantBranchUpdate', this._branchSocketHandler);
|
||||
}
|
||||
},
|
||||
queueFetchData() {
|
||||
if (this.pendingFetchTimer) {
|
||||
clearTimeout(this.pendingFetchTimer);
|
||||
}
|
||||
this.pendingFetchTimer = setTimeout(() => {
|
||||
this.pendingFetchTimer = null;
|
||||
this.fetchData();
|
||||
}, 120);
|
||||
},
|
||||
async fetchData() {
|
||||
if (!this.endpoint || this.pauseFetch) return;
|
||||
this.loading = true;
|
||||
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
this.fetchSettings();
|
||||
} catch (err) {
|
||||
console.error('Error updating setting:', err);
|
||||
showApiError(this, err, 'Aenderung konnte nicht gespeichert werden.');
|
||||
showApiError(this, err, 'Änderung konnte nicht gespeichert werden.');
|
||||
}
|
||||
},
|
||||
languagesList() {
|
||||
|
||||
@@ -18,15 +18,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="openCreateBranchDialog">
|
||||
<button
|
||||
@click="openCreateBranchDialog"
|
||||
:disabled="blocked"
|
||||
>
|
||||
{{ $t('falukant.branch.actions.create') }}
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('upgradeBranch')"
|
||||
:disabled="!localSelectedBranch"
|
||||
:disabled="!localSelectedBranch || blocked"
|
||||
>
|
||||
{{ $t('falukant.branch.actions.upgrade') }}
|
||||
</button>
|
||||
<span v-if="blocked && blockedReason" class="blocked-hint">{{ blockedReason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,6 +55,8 @@ export default {
|
||||
props: {
|
||||
branches: { type: Array, required: true },
|
||||
selectedBranch: { type: Object, default: null },
|
||||
blocked: { type: Boolean, default: false },
|
||||
blockedReason: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -82,6 +88,7 @@ export default {
|
||||
},
|
||||
|
||||
openCreateBranchDialog() {
|
||||
if (this.blocked) return;
|
||||
this.$refs.createBranchDialog.open();
|
||||
},
|
||||
|
||||
@@ -131,4 +138,13 @@ button {
|
||||
.weather-value {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.blocked-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
color: #8b2f23;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -147,6 +147,11 @@
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
{{ $t('falukant.branch.transport.guardCount') }}
|
||||
<input v-model.number="emptyTransportForm.guardCount" type="number" min="0" max="20" @input="loadEmptyTransportRoute">
|
||||
</label>
|
||||
|
||||
<div v-if="emptyTransportForm.costLabel" class="transport-cost">
|
||||
{{ $t('falukant.branch.director.emptyTransport.cost', { cost: emptyTransportForm.costLabel }) }}
|
||||
</div>
|
||||
@@ -179,6 +184,7 @@
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
|
||||
import { showError, showInfo, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: "DirectorInfo",
|
||||
@@ -199,6 +205,7 @@ export default {
|
||||
emptyTransportForm: {
|
||||
vehicleTypeId: null,
|
||||
targetBranchId: null,
|
||||
guardCount: 0,
|
||||
distance: null,
|
||||
durationHours: null,
|
||||
eta: null,
|
||||
@@ -278,7 +285,6 @@ export default {
|
||||
},
|
||||
|
||||
openNewDirectorDialog() {
|
||||
console.log('openNewDirectorDialog');
|
||||
this.$refs.newDirectorDialog.open(this.branchId);
|
||||
},
|
||||
|
||||
@@ -307,11 +313,11 @@ export default {
|
||||
},
|
||||
|
||||
fireDirector() {
|
||||
alert(this.$t('falukant.branch.director.fireAlert'));
|
||||
showInfo(this, this.$t('falukant.branch.director.fireAlert'));
|
||||
},
|
||||
|
||||
teachDirector() {
|
||||
alert(this.$t('falukant.branch.director.teachAlert'));
|
||||
showInfo(this, this.$t('falukant.branch.director.teachAlert'));
|
||||
},
|
||||
|
||||
vehicleTypeOptions() {
|
||||
@@ -390,8 +396,8 @@ export default {
|
||||
this.emptyTransportForm.routeNames = (data.regions || []).map(r => r.name);
|
||||
}
|
||||
// Kosten für leeren Transport: 0.1
|
||||
this.emptyTransportForm.cost = 0.1;
|
||||
this.emptyTransportForm.costLabel = this.formatMoney(0.1);
|
||||
this.emptyTransportForm.cost = 0.1 + ((this.emptyTransportForm.guardCount || 0) * 4);
|
||||
this.emptyTransportForm.costLabel = this.formatMoney(this.emptyTransportForm.cost);
|
||||
} catch (error) {
|
||||
console.error('Error loading transport route:', error);
|
||||
this.emptyTransportForm.distance = null;
|
||||
@@ -426,11 +432,13 @@ export default {
|
||||
productId: null,
|
||||
quantity: 0,
|
||||
targetBranchId: this.emptyTransportForm.targetBranchId,
|
||||
guardCount: this.emptyTransportForm.guardCount || 0,
|
||||
});
|
||||
// Formular zurücksetzen
|
||||
this.emptyTransportForm = {
|
||||
vehicleTypeId: null,
|
||||
targetBranchId: null,
|
||||
guardCount: 0,
|
||||
distance: null,
|
||||
durationHours: null,
|
||||
eta: null,
|
||||
@@ -440,11 +448,11 @@ export default {
|
||||
cost: 0.1,
|
||||
costLabel: '',
|
||||
};
|
||||
alert(this.$t('falukant.branch.director.emptyTransport.success'));
|
||||
showSuccess(this, this.$t('falukant.branch.director.emptyTransport.success'));
|
||||
this.$emit('transportCreated');
|
||||
} catch (error) {
|
||||
console.error('Error creating empty transport:', error);
|
||||
alert(this.$t('falukant.branch.director.emptyTransport.error'));
|
||||
showError(this, this.$t('falukant.branch.director.emptyTransport.error'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError } from '@/utils/feedback.js';
|
||||
export default {
|
||||
name: "ProductionSection",
|
||||
props: {
|
||||
@@ -166,7 +167,7 @@
|
||||
});
|
||||
this.loadProductions();
|
||||
} catch (error) {
|
||||
alert(this.$t(`falukant.branch.production.error${error.response.data.error}`));
|
||||
showApiError(this, error, 'tr:error.network');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,17 @@
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
{{ $t('falukant.branch.transport.guardCount') }}
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="transportForm.guardCount"
|
||||
min="0"
|
||||
max="20"
|
||||
@input="recalcCost"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div v-if="transportForm.costLabel">
|
||||
{{ $t('falukant.branch.sale.transportCost', { cost: transportForm.costLabel }) }}
|
||||
</div>
|
||||
@@ -138,6 +149,7 @@
|
||||
<th>{{ $t('falukant.branch.sale.runningEta') }}</th>
|
||||
<th>{{ $t('falukant.branch.sale.runningRemaining') }}</th>
|
||||
<th>{{ $t('falukant.branch.sale.runningVehicleCount') }}</th>
|
||||
<th>{{ $t('falukant.branch.sale.runningGuards') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -164,6 +176,7 @@
|
||||
<td>{{ formatEta({ eta: group.eta }) }}</td>
|
||||
<td>{{ formatRemaining({ eta: group.eta }) }}</td>
|
||||
<td>{{ group.vehicleCount }}</td>
|
||||
<td>{{ group.totalGuards || 0 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -173,6 +186,7 @@
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||
export default {
|
||||
name: "SaleSection",
|
||||
props: {
|
||||
@@ -188,6 +202,7 @@
|
||||
vehicleTypeId: null,
|
||||
targetBranchId: null,
|
||||
quantity: 0,
|
||||
guardCount: 0,
|
||||
maxQuantity: 0,
|
||||
distance: null,
|
||||
durationHours: null,
|
||||
@@ -231,12 +246,14 @@
|
||||
eta: transport.eta,
|
||||
vehicleCount: 0,
|
||||
totalQuantity: 0,
|
||||
totalGuards: 0,
|
||||
transports: [],
|
||||
});
|
||||
}
|
||||
|
||||
const group = groups.get(key);
|
||||
group.vehicleCount += 1;
|
||||
group.totalGuards += transport.guardCount || 0;
|
||||
if (transport.product && transport.size > 0) {
|
||||
group.totalQuantity += transport.size || 0;
|
||||
}
|
||||
@@ -334,13 +351,13 @@
|
||||
quantity: quantityToSell,
|
||||
quality: item.quality,
|
||||
}).catch(() => {
|
||||
alert(this.$t('falukant.branch.sale.sellError'));
|
||||
showError(this, this.$t('falukant.branch.sale.sellError'));
|
||||
});
|
||||
},
|
||||
sellAll() {
|
||||
apiClient.post(`/api/falukant/sell/all`, { branchId: this.branchId })
|
||||
.catch(() => {
|
||||
alert(this.$t('falukant.branch.sale.sellAllError'));
|
||||
showError(this, this.$t('falukant.branch.sale.sellAllError'));
|
||||
});
|
||||
},
|
||||
inventoryOptions() {
|
||||
@@ -413,7 +430,8 @@
|
||||
}
|
||||
const unitValue = item.product.sellCost || 0;
|
||||
const totalValue = unitValue * qty;
|
||||
const cost = Math.max(0.1, totalValue * 0.01);
|
||||
const guardCost = (this.transportForm.guardCount || 0) * 4;
|
||||
const cost = Math.max(0.1, totalValue * 0.01) + guardCost;
|
||||
this.transportForm.cost = cost;
|
||||
this.transportForm.costLabel = this.formatMoney(cost);
|
||||
},
|
||||
@@ -499,14 +517,15 @@
|
||||
productId: source.productId,
|
||||
quantity: this.transportForm.quantity,
|
||||
targetBranchId: this.transportForm.targetBranchId,
|
||||
guardCount: this.transportForm.guardCount || 0,
|
||||
});
|
||||
await this.loadInventory();
|
||||
await this.loadTransports();
|
||||
alert(this.$t('falukant.branch.sale.transportStarted'));
|
||||
showSuccess(this, this.$t('falukant.branch.sale.transportStarted'));
|
||||
this.$emit('transportCreated');
|
||||
} catch (error) {
|
||||
console.error('Error creating transport:', error);
|
||||
alert(this.$t('falukant.branch.sale.transportError'));
|
||||
showError(this, this.$t('falukant.branch.sale.transportError'));
|
||||
}
|
||||
},
|
||||
async loadTransports() {
|
||||
|
||||
@@ -36,6 +36,18 @@
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="debtorsPrison.active" class="statusbar-warning" :class="{ 'is-prison': debtorsPrison.inDebtorsPrison }">
|
||||
<strong>
|
||||
{{ debtorsPrison.inDebtorsPrison
|
||||
? $t('falukant.bank.debtorsPrison.titlePrison')
|
||||
: $t('falukant.bank.debtorsPrison.titleWarning') }}
|
||||
</strong>
|
||||
<span>
|
||||
{{ debtorsPrison.inDebtorsPrison
|
||||
? $t('falukant.debtorsPrison.globalLocked')
|
||||
: $t('falukant.debtorsPrison.globalWarning') }}
|
||||
</span>
|
||||
</div>
|
||||
<MessagesDialog ref="msgs" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -60,10 +72,15 @@ export default {
|
||||
{ key: "children", icon: "👶", value: null },
|
||||
],
|
||||
unreadCount: 0,
|
||||
debtorsPrison: {
|
||||
active: false,
|
||||
inDebtorsPrison: false
|
||||
},
|
||||
pendingStatusRefresh: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["socket", "daemonSocket"]),
|
||||
...mapState(["socket", "daemonSocket", "user"]),
|
||||
...mapGetters(['menu']),
|
||||
},
|
||||
watch: {
|
||||
@@ -100,6 +117,10 @@ export default {
|
||||
beforeUnmount() {
|
||||
this.teardownSocketListeners();
|
||||
this.teardownDaemonListeners();
|
||||
if (this.pendingStatusRefresh) {
|
||||
clearTimeout(this.pendingStatusRefresh);
|
||||
this.pendingStatusRefresh = null;
|
||||
}
|
||||
EventBus.off('open-falukant-messages', this.openMessages);
|
||||
},
|
||||
methods: {
|
||||
@@ -141,6 +162,10 @@ export default {
|
||||
const childCount = Number(response.data.childrenCount) || 0;
|
||||
const unbaptisedCount = Number(response.data.unbaptisedChildrenCount) || 0;
|
||||
this.unreadCount = Number(response.data.unreadNotifications) || 0;
|
||||
this.debtorsPrison = response.data.debtorsPrison || {
|
||||
active: false,
|
||||
inDebtorsPrison: false
|
||||
};
|
||||
const childrenDisplay = `${childCount}${unbaptisedCount > 0 ? `(${unbaptisedCount})` : ''}`;
|
||||
let healthStatus = '';
|
||||
if (health > 90) {
|
||||
@@ -169,15 +194,34 @@ export default {
|
||||
setupSocketListeners() {
|
||||
this.teardownSocketListeners();
|
||||
if (!this.socket) return;
|
||||
this.socket.on('falukantUpdateStatus', (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data }));
|
||||
this.socket.on('stock_change', (data) => this.handleEvent({ event: 'stock_change', ...data }));
|
||||
this.socket.on('familychanged', (data) => this.handleEvent({ event: 'familychanged', ...data }));
|
||||
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
|
||||
this._churchSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateChurch', ...data });
|
||||
this._debtSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data });
|
||||
this._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
|
||||
this._productionCertificateSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
|
||||
this._stockSocketHandler = (data) => this.handleEvent({ event: 'stock_change', ...data });
|
||||
this._familyChangedSocketHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
|
||||
|
||||
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
|
||||
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
|
||||
this.socket.on('falukantUpdateChurch', this._churchSocketHandler);
|
||||
this.socket.on('falukantUpdateDebt', this._debtSocketHandler);
|
||||
this.socket.on('children_update', this._childrenSocketHandler);
|
||||
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
|
||||
this.socket.on('stock_change', this._stockSocketHandler);
|
||||
this.socket.on('familychanged', this._familyChangedSocketHandler);
|
||||
},
|
||||
teardownSocketListeners() {
|
||||
if (this.socket) {
|
||||
this.socket.off('falukantUpdateStatus');
|
||||
this.socket.off('stock_change');
|
||||
this.socket.off('familychanged');
|
||||
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
|
||||
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
|
||||
if (this._churchSocketHandler) this.socket.off('falukantUpdateChurch', this._churchSocketHandler);
|
||||
if (this._debtSocketHandler) this.socket.off('falukantUpdateDebt', this._debtSocketHandler);
|
||||
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
|
||||
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
|
||||
if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler);
|
||||
if (this._familyChangedSocketHandler) this.socket.off('familychanged', this._familyChangedSocketHandler);
|
||||
}
|
||||
},
|
||||
setupDaemonListeners() {
|
||||
@@ -186,13 +230,22 @@ export default {
|
||||
this._daemonHandler = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (['falukantUpdateStatus', 'stock_change', 'familychanged'].includes(data.event)) {
|
||||
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'falukantUpdateDebt', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'].includes(data.event)) {
|
||||
this.handleEvent(data);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
this.daemonSocket.addEventListener('message', this._daemonHandler);
|
||||
},
|
||||
matchesCurrentUser(eventData) {
|
||||
if (eventData?.user_id == null) {
|
||||
return true;
|
||||
}
|
||||
const currentIds = [this.user?.id, this.user?.hashedId]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value));
|
||||
return currentIds.includes(String(eventData.user_id));
|
||||
},
|
||||
teardownDaemonListeners() {
|
||||
const sock = this.daemonSocket;
|
||||
if (sock && this._daemonHandler) {
|
||||
@@ -200,12 +253,29 @@ export default {
|
||||
this._daemonHandler = null;
|
||||
}
|
||||
},
|
||||
queueStatusRefresh() {
|
||||
if (this.pendingStatusRefresh) {
|
||||
clearTimeout(this.pendingStatusRefresh);
|
||||
}
|
||||
this.pendingStatusRefresh = setTimeout(async () => {
|
||||
this.pendingStatusRefresh = null;
|
||||
await this.fetchStatus();
|
||||
}, 120);
|
||||
},
|
||||
handleEvent(eventData) {
|
||||
if (!this.matchesCurrentUser(eventData)) {
|
||||
return;
|
||||
}
|
||||
switch (eventData.event) {
|
||||
case 'falukantUpdateStatus':
|
||||
case 'falukantUpdateFamily':
|
||||
case 'falukantUpdateChurch':
|
||||
case 'falukantUpdateDebt':
|
||||
case 'children_update':
|
||||
case 'falukantUpdateProductionCertificate':
|
||||
case 'stock_change':
|
||||
case 'familychanged':
|
||||
this.fetchStatus();
|
||||
this.queueStatusRefresh();
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -248,6 +318,25 @@ export default {
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.statusbar-warning {
|
||||
flex: 1 1 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid rgba(180, 120, 40, 0.35);
|
||||
background: rgba(255, 244, 223, 0.92);
|
||||
color: #8a5411;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.statusbar-warning.is-prison {
|
||||
border-color: rgba(146, 57, 40, 0.42);
|
||||
background: rgba(255, 232, 225, 0.94);
|
||||
color: #8b2f23;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
export default {
|
||||
name: "StorageSection",
|
||||
props: { branchId: { type: Number, required: true } },
|
||||
@@ -164,12 +165,12 @@
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Error buying storage for one part of the order');
|
||||
showError(this, 'Fehler beim Kaufen eines Teils der Lagerkapazität.');
|
||||
}
|
||||
remainingAmount -= toBuy;
|
||||
}
|
||||
if (remainingAmount > 0) {
|
||||
alert(this.$t('falukant.branch.storage.notEnoughAvailable'));
|
||||
showError(this, this.$t('falukant.branch.storage.notEnoughAvailable'));
|
||||
}
|
||||
this.loadStorageData();
|
||||
},
|
||||
@@ -185,7 +186,7 @@
|
||||
.then(() => this.loadStorageData())
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert('Error selling storage');
|
||||
showError(this, 'Fehler beim Verkaufen der Lagerkapazität.');
|
||||
});
|
||||
},
|
||||
getCostOfType(labelTr) {
|
||||
|
||||
@@ -43,7 +43,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
console.log('changed to ', value)
|
||||
this.$emit("input", parseInt(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
<dd>{{ falukantData.unreadNotificationsCount }}</dd>
|
||||
<dt>{{ $t('falukant.statusbar.children') }}</dt>
|
||||
<dd>{{ falukantData.childrenCount }}</dd>
|
||||
<template v-if="falukantData.debtorsPrison?.active">
|
||||
<dt>{{ $t('falukant.bank.debtorsPrison.titlePrison') }}</dt>
|
||||
<dd class="falukant-debt" :class="{ 'falukant-debt--warning': !falukantData.debtorsPrison?.inDebtorsPrison }">
|
||||
{{ falukantData.debtorsPrison?.inDebtorsPrison
|
||||
? $t('falukant.bank.debtorsPrison.titlePrison')
|
||||
: $t('falukant.bank.debtorsPrison.titleWarning') }}
|
||||
</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
@@ -43,6 +51,7 @@ export default {
|
||||
money: pick(raw, 'money', 'money'),
|
||||
unreadNotificationsCount: pick(raw, 'unreadNotificationsCount', 'unread_notifications_count'),
|
||||
childrenCount: pick(raw, 'childrenCount', 'children_count'),
|
||||
debtorsPrison: pick(raw, 'debtorsPrison', 'debtors_prison'),
|
||||
// keep all original keys as fallback for any other usage
|
||||
...raw
|
||||
};
|
||||
@@ -173,4 +182,13 @@ export default {
|
||||
color: #198754;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-widget__falukant dd.falukant-debt {
|
||||
color: #8b2f23;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dashboard-widget__falukant dd.falukant-debt--warning {
|
||||
color: #9a5a08;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<label for="password-reset-email">{{ $t("passwordReset.email") }}</label>
|
||||
<input id="password-reset-email" type="email" v-model="email" required :class="{ 'field-error': emailTouched && !isEmailValid }" />
|
||||
<span class="form-hint">Wir senden den Link an die hinterlegte E-Mail-Adresse.</span>
|
||||
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gueltige E-Mail-Adresse eingeben.</span>
|
||||
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gültige E-Mail-Adresse eingeben.</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="form-field">
|
||||
<label for="register-email">{{ $t("register.email") }}</label>
|
||||
<input id="register-email" type="email" v-model="email" :class="{ 'field-error': emailTouched && !isEmailValid }" />
|
||||
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gueltige E-Mail-Adresse eingeben.</span>
|
||||
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gültige E-Mail-Adresse eingeben.</span>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="register-username">{{ $t("register.username") }}</label>
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="form-field">
|
||||
<label for="register-repeat-password">{{ $t("register.repeatPassword") }}</label>
|
||||
<input id="register-repeat-password" type="password" v-model="repeatPassword" :class="{ 'field-error': repeatPasswordTouched && !doPasswordsMatch }" />
|
||||
<span v-if="repeatPasswordTouched && !doPasswordsMatch" class="form-error">Die Passwoerter stimmen nicht ueberein.</span>
|
||||
<span v-if="repeatPasswordTouched && !doPasswordsMatch" class="form-error">Die Passwörter stimmen nicht überein.</span>
|
||||
</div>
|
||||
<SelectDropdownWidget labelTr="settings.personal.label.language" :v-model="language"
|
||||
tooltipTr="settings.personal.tooltip.language" :list="languages" :value="language" />
|
||||
|
||||
@@ -239,6 +239,7 @@ import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import { fetchPublicRooms, fetchRoomCreateOptions, fetchOwnRooms } from '@/api/chatApi.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getChatWsUrl, getChatWsCandidates, getChatWsProtocols } from '@/services/chatWs.js';
|
||||
import { confirmAction } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'MultiChat',
|
||||
@@ -561,7 +562,10 @@ export default {
|
||||
},
|
||||
async deleteOwnedRoom(room) {
|
||||
const title = room?.title || '';
|
||||
const confirmed = window.confirm(this.$t('chat.multichat.createRoom.ownedRooms.confirmDelete', { room: title }));
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: this.$t('chat.multichat.createRoom.ownedRooms.title'),
|
||||
message: this.$t('chat.multichat.createRoom.ownedRooms.confirmDelete', { room: title })
|
||||
});
|
||||
if (!confirmed) return;
|
||||
if (!this.transportConnected) {
|
||||
this.messages.push({
|
||||
@@ -698,7 +702,6 @@ export default {
|
||||
// Mark as closed first so any async close events won't schedule reconnect
|
||||
this.opened = false;
|
||||
this.clearPendingRoomCreateTracking();
|
||||
console.log('[Chat WS] dialog close — closing websocket');
|
||||
this.disconnectChatSocket();
|
||||
// Remove network event listeners
|
||||
window.removeEventListener('online', this.onOnline);
|
||||
@@ -715,16 +718,13 @@ export default {
|
||||
this.showOptions = false;
|
||||
},
|
||||
onOnline() {
|
||||
console.log('[Chat WS] Network online detected');
|
||||
if (this.opened && !this.chatConnected && !this.connectRacing && (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN)) {
|
||||
console.log('[Chat WS] online — attempting reconnect');
|
||||
this.reconnectAttempts = 0; // Reset attempts on network recovery
|
||||
this.reconnectIntervalMs = 3000; // Reset to base interval
|
||||
this.connectChatSocket();
|
||||
}
|
||||
},
|
||||
onOffline() {
|
||||
console.log('[Chat WS] Network offline detected');
|
||||
this.setStatus('disconnected');
|
||||
},
|
||||
async loadRooms() {
|
||||
|
||||
@@ -72,10 +72,10 @@
|
||||
({{ formatCost(computeBranchCost(type)) }})
|
||||
</option>
|
||||
</select>
|
||||
<span class="form-hint">Waehle zuerst Region und dann den Niederlassungstyp.</span>
|
||||
<span class="form-hint">Wähle zuerst die Region und dann den Niederlassungstyp.</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="form-hint">Waehle auf der Karte eine freie Region aus.</div>
|
||||
<div v-else class="form-hint">Wähle auf der Karte eine freie Region aus.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +148,7 @@
|
||||
|
||||
async onConfirm() {
|
||||
if (!this.selectedRegion || !this.selectedType) {
|
||||
showError(this, 'Bitte zuerst Region und Typ auswaehlen.');
|
||||
showError(this, 'Bitte zuerst Region und Typ auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { EventBus } from '@/utils/eventBus.js';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'CreateFolderDialog',
|
||||
@@ -93,7 +94,7 @@ export default {
|
||||
},
|
||||
async createFolder() {
|
||||
if (!this.folderTitle || !this.selectedVisibility.length) {
|
||||
alert(this.$t('socialnetwork.gallery.errors.missing_fields'));
|
||||
showError(this, this.$t('socialnetwork.gallery.errors.missing_fields'));
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
|
||||
@@ -97,6 +97,7 @@ import FolderItem from '../../components/FolderItem.vue';
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import DOMPurify from 'dompurify';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'UserProfileDialog',
|
||||
@@ -234,7 +235,7 @@ export default {
|
||||
});
|
||||
},
|
||||
async fetchImage(image) {
|
||||
const userId = localStorage.getItem('userid');
|
||||
const userId = localStorage.getItem('userid') || sessionStorage.getItem('userid');
|
||||
try {
|
||||
const response = await apiClient.get(`/api/socialnetwork/image/${image.hash}`, {
|
||||
headers: {
|
||||
@@ -265,7 +266,10 @@ export default {
|
||||
}
|
||||
},
|
||||
async submitGuestbookEntry() {
|
||||
if (!this.newEntryContent) return alert(this.$t('socialnetwork.guestbook.emptyContent'));
|
||||
if (!this.newEntryContent) {
|
||||
showError(this, this.$t('socialnetwork.guestbook.emptyContent'));
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('htmlContent', this.newEntryContent);
|
||||
formData.append('recipientName', this.userProfile.username);
|
||||
|
||||
@@ -42,12 +42,10 @@ export default {
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
confirmYes() {
|
||||
console.log('ja');
|
||||
this.resolve(true);
|
||||
this.close();
|
||||
},
|
||||
confirmNo() {
|
||||
console.log('nein');
|
||||
this.resolve(false);
|
||||
this.close();
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"activate": {
|
||||
"title": "Zugang aktivieren",
|
||||
"message": "Hallo {username}. Bitte gib hier den Code ein, den wir Dir per Email zugesendet haben.",
|
||||
"message": "Hallo {username}. Bitte gib hier den Code ein, den wir Dir per E-Mail zugesendet haben.",
|
||||
"token": "Token:",
|
||||
"submit": "Absenden",
|
||||
"failure": "Die Aktivierung war nicht erfolgreich."
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
"children": "Kinder",
|
||||
"children_unbaptised": "ungetaufte Kinder"
|
||||
},
|
||||
"debtorsPrison": {
|
||||
"actionBlocked": "Im Schuldturm kannst du diese Aktion derzeit nicht ausführen.",
|
||||
"globalWarning": "Dein Kreditverzug schränkt dein Handeln ein. Zwangsmaßnahmen können bald folgen.",
|
||||
"globalLocked": "Du bist im Schuldturm. Fast alle aktiven Falukant-Handlungen sind derzeit gesperrt."
|
||||
},
|
||||
"messages": {
|
||||
"title": "Nachrichten",
|
||||
"tooltip": "Nachrichten",
|
||||
@@ -130,7 +135,8 @@
|
||||
"years": "Jahre",
|
||||
"days": "Tage",
|
||||
"mainbranch": "Heimatstadt",
|
||||
"nobleTitle": "Stand"
|
||||
"nobleTitle": "Stand",
|
||||
"certificate": "Zertifikat"
|
||||
},
|
||||
"productions": {
|
||||
"title": "Produktionen"
|
||||
@@ -219,6 +225,12 @@
|
||||
},
|
||||
"branch": {
|
||||
"title": "Filiale",
|
||||
"debtorsPrison": {
|
||||
"branchLocked": "Im Schuldturm sind neue wirtschaftliche Schritte blockiert. Geschlossene oder gepfändete Standorte werden hier ebenfalls sichtbar.",
|
||||
"branchRisk": "Dein Kreditverzug gefährdet Niederlassungen, Fahrzeuge und Lagerbestände.",
|
||||
"selectionBlocked": "Neue Ausbauten sind im Schuldturm gesperrt."
|
||||
},
|
||||
"currentCertificate": "Derzeitiges Zertifikat",
|
||||
"tabs": {
|
||||
"director": "Direktor",
|
||||
"inventory": "Inventar",
|
||||
@@ -322,6 +334,7 @@
|
||||
"runningEta": "Ankunft",
|
||||
"runningRemaining": "Restzeit",
|
||||
"runningVehicleCount": "Fahrzeuge",
|
||||
"runningGuards": "Wachen",
|
||||
"runningDirectionOut": "Ausgehend",
|
||||
"runningDirectionIn": "Eingehend"
|
||||
},
|
||||
@@ -396,6 +409,8 @@
|
||||
"transport": {
|
||||
"title": "Transportmittel",
|
||||
"placeholder": "Hier kannst du Transportmittel für deine Region kaufen oder bauen.",
|
||||
"guardCount": "Wachen",
|
||||
"guardHint": "Zusatzkosten für Wachen: {cost}",
|
||||
"vehicleType": "Transportmittel",
|
||||
"mode": "Art",
|
||||
"modeBuy": "Kaufen (sofort verfügbar)",
|
||||
@@ -481,11 +496,17 @@
|
||||
},
|
||||
"family": {
|
||||
"title": "Familie",
|
||||
"debtorsPrison": {
|
||||
"familyWarning": "Anhaltender Kreditverzug belastet Ehe, Haushalt und Liebschaften.",
|
||||
"familyImpact": "Der Schuldturm schadet Ehe, Hausfrieden und der Stabilität von Liebschaften."
|
||||
},
|
||||
"spouse": {
|
||||
"title": "Beziehung",
|
||||
"name": "Name",
|
||||
"age": "Alter",
|
||||
"status": "Status",
|
||||
"marriageSatisfaction": "Ehe-Zufriedenheit",
|
||||
"marriageState": "Ehezustand",
|
||||
"none": "Kein Ehepartner vorhanden.",
|
||||
"search": "Ehepartner suchen",
|
||||
"found": "Ehepartner gefunden",
|
||||
@@ -517,6 +538,42 @@
|
||||
"progress": "Zuneigung",
|
||||
"jumpToPartyForm": "Hochzeitsfeier veranstalten (Nötig für Hochzeit und Kinder)"
|
||||
},
|
||||
"marriageState": {
|
||||
"stable": "Stabil",
|
||||
"strained": "Angespannt",
|
||||
"crisis": "Krise"
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Hausfrieden",
|
||||
"score": "Spannungswert",
|
||||
"reasonsLabel": "Aktuelle Ursachen",
|
||||
"low": "Ruhig",
|
||||
"medium": "Unruhig",
|
||||
"high": "Belastet",
|
||||
"reasons": {
|
||||
"visibleLover": "Sichtbare Liebschaft",
|
||||
"noticeableLover": "Auffällige Liebschaft",
|
||||
"underfundedLover": "Unterversorgte Liebschaft",
|
||||
"acknowledgedAffair": "Anerkannte Liebschaft",
|
||||
"statusMismatch": "Standesunterschied",
|
||||
"loverChild": "Kind aus Liebschaft",
|
||||
"disorder": "Unordnung im Haus",
|
||||
"tooFewServants": "Zu wenig Diener",
|
||||
"marriageCrisis": "Ehekrise"
|
||||
}
|
||||
},
|
||||
"marriageActions": {
|
||||
"title": "Ehe pflegen",
|
||||
"spendTime": "Zeit miteinander verbringen",
|
||||
"giftSmall": "Kleines Geschenk",
|
||||
"giftDecent": "Gutes Geschenk",
|
||||
"giftLavish": "Großzügiges Geschenk",
|
||||
"reconcile": "Streit schlichten",
|
||||
"spendTimeSuccess": "Die gemeinsame Zeit hat die Ehe stabilisiert.",
|
||||
"giftSuccess": "Das Geschenk hat die Ehe verbessert.",
|
||||
"reconcileSuccess": "Der Streit wurde fürs Erste geschlichtet.",
|
||||
"actionError": "Die Aktion konnte nicht ausgeführt werden."
|
||||
},
|
||||
"relationships": {
|
||||
"name": "Name"
|
||||
},
|
||||
@@ -538,14 +595,62 @@
|
||||
"baptism": "Taufen",
|
||||
"notBaptized": "Noch nicht getauft",
|
||||
"baptismNotice": "Dieses Kind wurde noch nicht getauft und hat daher noch keinen Namen.",
|
||||
"legitimacy": {
|
||||
"legitimate": "Ehelich",
|
||||
"acknowledged_bastard": "Anerkannt unehelich",
|
||||
"hidden_bastard": "Unehelich"
|
||||
},
|
||||
"details": {
|
||||
"title": "Kind-Details"
|
||||
}
|
||||
},
|
||||
"lovers": {
|
||||
"title": "Liebhaber",
|
||||
"title": "Liebhaber und Mätressen",
|
||||
"none": "Keine Liebhaber vorhanden.",
|
||||
"affection": "Zuneigung"
|
||||
"affection": "Zuneigung",
|
||||
"visibility": "Sichtbarkeit",
|
||||
"discretion": "Diskretion",
|
||||
"maintenance": "Unterhalt",
|
||||
"monthlyCost": "Monatskosten",
|
||||
"statusFit": "Standespassung",
|
||||
"acknowledged": "Anerkannt",
|
||||
"underfunded": "{count} Monate unterversorgt",
|
||||
"role": {
|
||||
"secret_affair": "Heimliche Liebschaft",
|
||||
"lover": "Geliebte Beziehung",
|
||||
"mistress_or_favorite": "Mätresse oder Favorit"
|
||||
},
|
||||
"risk": {
|
||||
"low": "Geringes Risiko",
|
||||
"medium": "Mittleres Risiko",
|
||||
"high": "Hohes Risiko"
|
||||
},
|
||||
"actions": {
|
||||
"start": "Liebschaft beginnen",
|
||||
"startSuccess": "Die neue Liebschaft wurde begonnen.",
|
||||
"startError": "Die Liebschaft konnte nicht begonnen werden.",
|
||||
"maintenanceLow": "Unterhalt 25",
|
||||
"maintenanceMedium": "Unterhalt 50",
|
||||
"maintenanceHigh": "Unterhalt 75",
|
||||
"maintenanceSuccess": "Der Unterhalt wurde angepasst.",
|
||||
"maintenanceError": "Der Unterhalt konnte nicht angepasst werden.",
|
||||
"acknowledge": "Anerkennen",
|
||||
"acknowledgeSuccess": "Die Beziehung wurde offiziell anerkannt.",
|
||||
"acknowledgeError": "Die Beziehung konnte nicht anerkannt werden.",
|
||||
"end": "Beenden",
|
||||
"endConfirm": "Soll diese Beziehung wirklich beendet werden?",
|
||||
"endSuccess": "Die Beziehung wurde beendet.",
|
||||
"endError": "Die Beziehung konnte nicht beendet werden."
|
||||
},
|
||||
"candidates": {
|
||||
"title": "Mögliche Liebschaften",
|
||||
"roleLabel": "Form der Beziehung",
|
||||
"none": "Derzeit gibt es keine passenden neuen Liebschaften."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"scandal": "Ein Familienskandal erschüttert dein Haus.",
|
||||
"loverBirth": "Aus einer Liebschaft ist ein Kind hervorgegangen."
|
||||
},
|
||||
"statuses": {
|
||||
"wooing": "In Werbung",
|
||||
@@ -651,7 +756,12 @@
|
||||
"build_vehicles": "Transportmittel gebaut",
|
||||
"transport": "Transport",
|
||||
"Marriage cost": "Heiratskosten",
|
||||
"marriage_gift": "Geschenk an Ehepartner",
|
||||
"Gift cost": "Geschenk-Kosten",
|
||||
"lover maintenance": "Unterhalt für Liebschaft",
|
||||
"servants_monthly": "Dienerschaft bezahlt",
|
||||
"servants_hired": "Diener eingestellt",
|
||||
"household_order": "Haus geordnet",
|
||||
"housebuy": "Hauskauf",
|
||||
"Baptism": "Taufe",
|
||||
"credit taken": "Kredit aufgenommen",
|
||||
@@ -757,6 +867,10 @@
|
||||
},
|
||||
"house": {
|
||||
"title": "Haus",
|
||||
"debtorsPrison": {
|
||||
"houseWarning": "Mit wachsendem Kreditverzug steigt das Risiko für Pfändung und erzwungenen Hausverlust.",
|
||||
"houseRisk": "Dein Haus ist jetzt Teil der möglichen Zwangsverwertung."
|
||||
},
|
||||
"statusreport": "Zustand des Hauses",
|
||||
"element": "Bereich",
|
||||
"state": "Zustand",
|
||||
@@ -765,8 +879,54 @@
|
||||
"price": "Kaufpreis",
|
||||
"worth": "Restwert",
|
||||
"sell": "Verkaufen",
|
||||
"sellConfirm": "Möchtest du dein Haus wirklich verkaufen?",
|
||||
"sellSuccess": "Das Haus wurde verkauft.",
|
||||
"sellError": "Das Haus konnte nicht verkauft werden.",
|
||||
"buySuccess": "Das Haus wurde gekauft.",
|
||||
"buyError": "Das Haus konnte nicht gekauft werden.",
|
||||
"renovate": "Renovieren",
|
||||
"renovateAll": "Komplett renovieren",
|
||||
"servants": {
|
||||
"title": "Dienerschaft",
|
||||
"description": "Verwalte Hauspersonal, Ordnung und laufende Kosten deines Haushalts.",
|
||||
"count": "Dienerzahl",
|
||||
"expectedRange": "Erwarteter Bereich",
|
||||
"monthlyCost": "Monatskosten",
|
||||
"quality": "Qualität",
|
||||
"householdOrder": "Haushaltsordnung",
|
||||
"payLevel": "Bezahlung",
|
||||
"payLevels": {
|
||||
"low": "Niedrig",
|
||||
"normal": "Normal",
|
||||
"high": "Großzügig"
|
||||
},
|
||||
"staffingState": {
|
||||
"label": "Besetzung",
|
||||
"understaffed": "Unterbesetzt",
|
||||
"fitting": "Passend",
|
||||
"overstaffed": "Überbesetzt"
|
||||
},
|
||||
"orderState": {
|
||||
"label": "Ordnungszustand",
|
||||
"chaotic": "Chaotisch",
|
||||
"strained": "Angespannt",
|
||||
"stable": "Stabil",
|
||||
"excellent": "Vorbildlich"
|
||||
},
|
||||
"actions": {
|
||||
"hire": "1 Diener einstellen",
|
||||
"dismiss": "1 Diener entlassen",
|
||||
"tidy": "Haus ordnen",
|
||||
"hireSuccess": "Die Dienerschaft wurde erweitert.",
|
||||
"hireError": "Die Dienerschaft konnte nicht erweitert werden.",
|
||||
"dismissSuccess": "Ein Diener wurde entlassen.",
|
||||
"dismissError": "Der Diener konnte nicht entlassen werden.",
|
||||
"payLevelSuccess": "Die Bezahlung der Dienerschaft wurde angepasst.",
|
||||
"payLevelError": "Die Bezahlung konnte nicht angepasst werden.",
|
||||
"tidySuccess": "Das Haus wurde geordnet.",
|
||||
"tidyError": "Das Haus konnte nicht geordnet werden."
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"roofCondition": "Dach",
|
||||
"wallCondition": "Wände",
|
||||
@@ -787,11 +947,21 @@
|
||||
"overview": "Übersicht",
|
||||
"advance": "Erweitern"
|
||||
},
|
||||
"highestPoliticalOffice": "Höchstes politisches Amt",
|
||||
"highestOfficeAny": "Höchstes Amt insgesamt",
|
||||
"none": "keines",
|
||||
"nextTitle": "Nächster möglicher Titel",
|
||||
"requirement": {
|
||||
"money": "Vermögen mindestens {amount}",
|
||||
"cost": "Kosten: {amount}",
|
||||
"branches": "Mindestens {amount} Niederlassungen"
|
||||
"branches": "Mindestens {amount} Niederlassungen",
|
||||
"reputation": "Beliebtheit mindestens {amount}",
|
||||
"house_position": "Hausstand mindestens Stufe {amount}",
|
||||
"house_condition": "Hauszustand mindestens {amount}",
|
||||
"office_rank_any": "Höchstes politisches oder kirchliches Amt mindestens Rang {amount}",
|
||||
"office_rank_political": "Höchstes politisches Amt mindestens Rang {amount}",
|
||||
"lover_count_min": "Mindestens {amount} Liebhaber oder Mätressen",
|
||||
"lover_count_max": "Höchstens {amount} Liebhaber oder Mätressen"
|
||||
},
|
||||
"advance": {
|
||||
"confirm": "Aufsteigen beantragen"
|
||||
@@ -880,10 +1050,18 @@
|
||||
"church": {
|
||||
"title": "Kirche",
|
||||
"tabs": {
|
||||
"baptism": "Taufen",
|
||||
"current": "Aktuelle Positionen",
|
||||
"available": "Verfügbare Positionen",
|
||||
"applications": "Bewerbungen"
|
||||
},
|
||||
"summary": {
|
||||
"highestCurrentOffice": "Höchstes aktuelles Amt",
|
||||
"availableApplications": "Mögliche Bewerbungen",
|
||||
"supervisedApplications": "Zu entscheidende Bewerbungen",
|
||||
"guidance": "Kirchenämter steigen stufenweise auf. Über Bewerbungen entscheidet in der Regel das nächsthöhere Amt; falls dort kein Spieler sitzt, kann später ein NPC entscheiden.",
|
||||
"none": "Noch kein Kirchenamt"
|
||||
},
|
||||
"current": {
|
||||
"office": "Amt",
|
||||
"region": "Region",
|
||||
@@ -895,11 +1073,25 @@
|
||||
"office": "Amt",
|
||||
"region": "Region",
|
||||
"supervisor": "Vorgesetzter",
|
||||
"decision": "Entscheidung durch",
|
||||
"decisionType": {
|
||||
"entry": "Direkter Einstieg",
|
||||
"player": "Spieler",
|
||||
"npc": "NPC",
|
||||
"interim": "Interim"
|
||||
},
|
||||
"seats": "Verfügbare Plätze",
|
||||
"action": "Aktion",
|
||||
"apply": "Bewerben",
|
||||
"applySuccess": "Bewerbung erfolgreich eingereicht.",
|
||||
"applyError": "Fehler beim Einreichen der Bewerbung.",
|
||||
"errors": {
|
||||
"characterNotFound": "Dein Charakter konnte nicht gefunden werden.",
|
||||
"officeTypeNotFound": "Das Kirchenamt wurde nicht gefunden.",
|
||||
"churchCareerTooLow": "Deine bisherige kirchliche Laufbahn reicht für dieses Amt noch nicht aus.",
|
||||
"noAvailableSeats": "Für dieses Kirchenamt sind derzeit keine Plätze frei.",
|
||||
"applicationAlreadyExists": "Für dieses Kirchenamt in dieser Region besteht bereits eine offene Bewerbung."
|
||||
},
|
||||
"none": "Keine verfügbaren Positionen."
|
||||
},
|
||||
"applications": {
|
||||
@@ -971,6 +1163,23 @@
|
||||
"maxCredit": "Maximaler Kredit",
|
||||
"availableCredit": "Verfügbarer Kredit"
|
||||
},
|
||||
"debtorsPrison": {
|
||||
"titleWarning": "Kreditverzug",
|
||||
"titlePrison": "Schuldturm",
|
||||
"descriptionWarning": "Deine Kredite sind im Verzug. Wenn du weiter nicht bedienst, drohen Zwangsmaßnahmen.",
|
||||
"descriptionPrison": "Du sitzt im Schuldturm. Neue Kredite sind gesperrt und dein Vermögen wird schrittweise verwertet.",
|
||||
"daysOverdue": "Verzugstage",
|
||||
"creditworthiness": "Kreditwürdigkeit",
|
||||
"nextForcedAction": "Nächste Zwangsmaßnahme",
|
||||
"creditBlocked": "Im Schuldturm kannst du keine neuen Kredite aufnehmen.",
|
||||
"creditError": "Der Kredit konnte nicht aufgenommen werden.",
|
||||
"actions": {
|
||||
"reminder": "Erste Mahnung",
|
||||
"final_warning": "Letzte Mahnung",
|
||||
"debtors_prison": "Einweisung in den Schuldturm",
|
||||
"asset_seizure": "Pfändung von Vermögen"
|
||||
}
|
||||
},
|
||||
"credits": {
|
||||
"title": "Kredite",
|
||||
"none": "Derzeit hast Du keinen Kredit aufgenommen.",
|
||||
@@ -1120,10 +1329,26 @@
|
||||
"type": "Aktivitätstyp",
|
||||
"victim": "Zielperson",
|
||||
"cost": "Kosten",
|
||||
"status": "Status",
|
||||
"additionalInfo": "Zusätzliche Informationen",
|
||||
"blackmailAmount": "Erpressungssumme",
|
||||
"discoveries": "Erkenntnisse",
|
||||
"visibilityDelta": "Sichtbarkeit",
|
||||
"reputationDelta": "Ansehen",
|
||||
"victimPlaceholder": "Benutzername eingeben",
|
||||
"sabotageTarget": "Sabotageziel",
|
||||
"corruptGoal": "Ziel der Korruption"
|
||||
"corruptGoal": "Ziel der Korruption",
|
||||
"affairGoal": "Ziel der Untersuchung",
|
||||
"raidRegion": "Überfallregion",
|
||||
"raidRegionPlaceholder": "Region wählen",
|
||||
"bandSize": "Bandengröße",
|
||||
"raidSummary": "Bande ({bandSize}) in {region}",
|
||||
"attempts": "Versuche",
|
||||
"successes": "Erfolge",
|
||||
"lastOutcome": "Letztes Ergebnis",
|
||||
"raidResultTitle": "Letzter Überfall",
|
||||
"lastTargetTransport": "Letzter Zieltransport",
|
||||
"loot": "Beute"
|
||||
},
|
||||
"attacks": {
|
||||
"target": "Angreifer",
|
||||
@@ -1136,7 +1361,9 @@
|
||||
"assassin": "Attentat",
|
||||
"sabotage": "Sabotage",
|
||||
"corrupt_politician": "Korruption",
|
||||
"rob": "Raub"
|
||||
"rob": "Raub",
|
||||
"investigate_affair": "Liebschaft untersuchen",
|
||||
"raid_transport": "Überfälle auf Transporte"
|
||||
},
|
||||
"targets": {
|
||||
"house": "Wohnhaus",
|
||||
@@ -1145,7 +1372,19 @@
|
||||
"goals": {
|
||||
"elect": "Amtseinsetzung",
|
||||
"taxIncrease": "Steuern erhöhen",
|
||||
"taxDecrease": "Steuern senken"
|
||||
"taxDecrease": "Steuern senken",
|
||||
"expose": "Aufdecken",
|
||||
"blackmail": "Erpressen"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Ausstehend",
|
||||
"resolved": "Abgeschlossen",
|
||||
"failed": "Gescheitert"
|
||||
},
|
||||
"raidOutcomes": {
|
||||
"repelled": "Abgewehrt",
|
||||
"partial_success": "Teilweise erfolgreich",
|
||||
"major_success": "Großer Erfolg"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
"info-title": "Information",
|
||||
"dialog": {
|
||||
"contact": {
|
||||
"email": "Email-Adresse",
|
||||
"email": "E-Mail-Adresse",
|
||||
"name": "Name",
|
||||
"message": "Deine Nachricht an uns",
|
||||
"accept": "Deine Email-Adresse wird vorübergehend in unserem System gespeichert. Nachdem Deine Anfrage bearbeitet wurde, wird die Email-Adresse wieder aus dem System gelöscht.",
|
||||
"acceptdatasave": "Ich stimme der vorübergehenden Speicherung meiner Email-Adresse zu.",
|
||||
"accept": "Deine E-Mail-Adresse wird vorübergehend in unserem System gespeichert. Nachdem Deine Anfrage bearbeitet wurde, wird die E-Mail-Adresse wieder aus dem System gelöscht.",
|
||||
"acceptdatasave": "Ich stimme der vorübergehenden Speicherung meiner E-Mail-Adresse zu.",
|
||||
"accept2": "Ohne diese Zustimmung können wir Dir leider nicht antworten."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
"login": {
|
||||
"name": "Login-Name",
|
||||
"namedescription": "Gib hier Deinen Benutzernamen ein",
|
||||
"password": "Paßwort",
|
||||
"passworddescription": "Gib hier Dein Paßwort ein",
|
||||
"lostpassword": "Paßwort vergessen",
|
||||
"password": "Passwort",
|
||||
"passworddescription": "Gib hier Dein Passwort ein",
|
||||
"lostpassword": "Passwort vergessen",
|
||||
"register": "Bei yourPart registrieren",
|
||||
"stayLoggedIn": "Eingeloggt bleiben",
|
||||
"submit": "Einloggen"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"register": {
|
||||
"title": "Bei yourPart registrieren",
|
||||
"email": "Email-Adresse",
|
||||
"email": "E-Mail-Adresse",
|
||||
"username": "Benutzername",
|
||||
"password": "Paßwort",
|
||||
"repeatPassword": "Paßwort wiederholen",
|
||||
"password": "Passwort",
|
||||
"repeatPassword": "Passwort wiederholen",
|
||||
"language": "Sprache",
|
||||
"languages": {
|
||||
"en": "Englisch",
|
||||
@@ -13,9 +13,9 @@
|
||||
"register": "Registrieren",
|
||||
"close": "Schließen",
|
||||
"failure": "Es ist ein Fehler aufgetreten.",
|
||||
"success": "Du wurdest erfolgreich registriert. Bitte schaue jetzt in Dein Email-Postfach zum aktivieren Deines Zugangs.",
|
||||
"passwordMismatch": "Die Paßwörter stimmen nicht überein.",
|
||||
"emailinuse": "Die Email-Adresse wird bereits verwendet.",
|
||||
"success": "Du wurdest erfolgreich registriert. Bitte schaue jetzt in Dein E-Mail-Postfach zum Aktivieren Deines Zugangs.",
|
||||
"passwordMismatch": "Die Passwörter stimmen nicht überein.",
|
||||
"emailinuse": "Die E-Mail-Adresse wird bereits verwendet.",
|
||||
"usernameinuse": "Der Benutzername ist nicht verfügbar."
|
||||
}
|
||||
}
|
||||
@@ -141,14 +141,14 @@
|
||||
"account": {
|
||||
"title": "Account",
|
||||
"username": "Benutzername",
|
||||
"email": "Email-Adresse",
|
||||
"email": "E-Mail-Adresse",
|
||||
"newpassword": "Passwort",
|
||||
"newpasswordretype": "Passwort wiederholen",
|
||||
"deleteAccount": "Account löschen",
|
||||
"language": "Sprache",
|
||||
"showinsearch": "In Usersuchen anzeigen",
|
||||
"changeaction": "Benutzerdaten ändern",
|
||||
"oldpassword": "Altes Paßwort (benötigt)"
|
||||
"oldpassword": "Altes Passwort (benötigt)"
|
||||
},
|
||||
"interests": {
|
||||
"title": "Interessen",
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
"windy": "Windy",
|
||||
"clear": "Clear"
|
||||
},
|
||||
"debtorsPrison": {
|
||||
"actionBlocked": "This action is blocked while you are in debtors' prison.",
|
||||
"globalWarning": "Your credit delinquency is already restricting your actions. Forced measures may follow soon.",
|
||||
"globalLocked": "You are in debtors' prison. Almost all active Falukant actions are currently blocked."
|
||||
},
|
||||
"messages": {
|
||||
"title": "Messages",
|
||||
"tooltip": "Messages",
|
||||
@@ -111,7 +116,8 @@
|
||||
"years": "Years",
|
||||
"days": "Days",
|
||||
"mainbranch": "Home city",
|
||||
"nobleTitle": "Title"
|
||||
"nobleTitle": "Title",
|
||||
"certificate": "Certificate"
|
||||
}
|
||||
},
|
||||
"health": {
|
||||
@@ -149,7 +155,7 @@
|
||||
"all": "All history"
|
||||
}
|
||||
},
|
||||
"activities": {
|
||||
"activities": {
|
||||
"Product sale": "Product sale",
|
||||
"Production cost": "Production cost",
|
||||
"Sell all products": "Sell all products",
|
||||
@@ -165,7 +171,12 @@
|
||||
"build_vehicles": "Transport vehicles built",
|
||||
"transport": "Transport",
|
||||
"Marriage cost": "Marriage cost",
|
||||
"marriage_gift": "Gift for spouse",
|
||||
"Gift cost": "Gift cost",
|
||||
"lover maintenance": "Lover maintenance",
|
||||
"servants_monthly": "Servants paid",
|
||||
"servants_hired": "Servants hired",
|
||||
"household_order": "Household ordered",
|
||||
"housebuy": "House purchase",
|
||||
"Baptism": "Baptism",
|
||||
"credit taken": "Credit taken",
|
||||
@@ -181,6 +192,82 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"house": {
|
||||
"title": "House",
|
||||
"debtorsPrison": {
|
||||
"houseWarning": "As delinquency grows, the risk of seizure and forced loss of the house increases.",
|
||||
"houseRisk": "Your house is now part of the possible forced liquidation."
|
||||
},
|
||||
"statusreport": "House condition",
|
||||
"element": "Element",
|
||||
"state": "Condition",
|
||||
"buyablehouses": "Buy a house",
|
||||
"buy": "Buy",
|
||||
"price": "Purchase price",
|
||||
"worth": "Residual value",
|
||||
"sell": "Sell",
|
||||
"sellConfirm": "Do you really want to sell your house?",
|
||||
"sellSuccess": "The house has been sold.",
|
||||
"sellError": "The house could not be sold.",
|
||||
"buySuccess": "The house has been bought.",
|
||||
"buyError": "The house could not be bought.",
|
||||
"renovate": "Renovate",
|
||||
"renovateAll": "Renovate completely",
|
||||
"servants": {
|
||||
"title": "Servants",
|
||||
"description": "Manage household staff, order and recurring costs in your home.",
|
||||
"count": "Servant count",
|
||||
"expectedRange": "Expected range",
|
||||
"monthlyCost": "Monthly cost",
|
||||
"quality": "Quality",
|
||||
"householdOrder": "Household order",
|
||||
"payLevel": "Pay level",
|
||||
"payLevels": {
|
||||
"low": "Low",
|
||||
"normal": "Normal",
|
||||
"high": "Generous"
|
||||
},
|
||||
"staffingState": {
|
||||
"label": "Staffing",
|
||||
"understaffed": "Understaffed",
|
||||
"fitting": "Fitting",
|
||||
"overstaffed": "Overstaffed"
|
||||
},
|
||||
"orderState": {
|
||||
"label": "Order state",
|
||||
"chaotic": "Chaotic",
|
||||
"strained": "Strained",
|
||||
"stable": "Stable",
|
||||
"excellent": "Excellent"
|
||||
},
|
||||
"actions": {
|
||||
"hire": "Hire 1 servant",
|
||||
"dismiss": "Dismiss 1 servant",
|
||||
"tidy": "Tidy household",
|
||||
"hireSuccess": "The household staff has been expanded.",
|
||||
"hireError": "The staff could not be expanded.",
|
||||
"dismissSuccess": "A servant has been dismissed.",
|
||||
"dismissError": "The servant could not be dismissed.",
|
||||
"payLevelSuccess": "Servant pay has been updated.",
|
||||
"payLevelError": "Servant pay could not be updated.",
|
||||
"tidySuccess": "The household has been put in order.",
|
||||
"tidyError": "The household could not be put in order."
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"roofCondition": "Roof",
|
||||
"wallCondition": "Walls",
|
||||
"floorCondition": "Floors",
|
||||
"windowCondition": "Windows"
|
||||
},
|
||||
"type": {
|
||||
"backyard_room": "Backyard room",
|
||||
"wooden_house": "Wooden house",
|
||||
"straw_hut": "Straw hut",
|
||||
"family_house": "Family house",
|
||||
"townhouse": "Townhouse"
|
||||
}
|
||||
},
|
||||
"newdirector": {
|
||||
"title": "New Director",
|
||||
"age": "Age",
|
||||
@@ -192,6 +279,12 @@
|
||||
"noProposals": "No director candidates available."
|
||||
},
|
||||
"branch": {
|
||||
"debtorsPrison": {
|
||||
"branchLocked": "While in debtors' prison, new economic steps are blocked. Closed or seized branches will also become visible here.",
|
||||
"branchRisk": "Your delinquency puts branches, vehicles and stored goods at risk.",
|
||||
"selectionBlocked": "New expansions are blocked while imprisoned for debt."
|
||||
},
|
||||
"currentCertificate": "Current certificate",
|
||||
"selection": {
|
||||
"title": "Branch Selection",
|
||||
"selected": "Selected Branch",
|
||||
@@ -201,7 +294,26 @@
|
||||
},
|
||||
"director": {
|
||||
"income": "Income",
|
||||
"incomeUpdated": "Salary has been successfully updated."
|
||||
"incomeUpdated": "Salary has been successfully updated.",
|
||||
"starttransport": "May start transports",
|
||||
"emptyTransport": {
|
||||
"title": "Transport without products",
|
||||
"description": "Move vehicles from this branch to another to use them better.",
|
||||
"vehicleType": "Vehicle type",
|
||||
"selectVehicle": "Select vehicle type",
|
||||
"targetBranch": "Target branch",
|
||||
"selectTarget": "Select target branch",
|
||||
"cost": "Cost: {cost}",
|
||||
"duration": "Duration: {duration}",
|
||||
"arrival": "Arrival: {datetime}",
|
||||
"route": "Route",
|
||||
"create": "Start transport",
|
||||
"success": "Transport started successfully.",
|
||||
"error": "Error starting the transport."
|
||||
}
|
||||
},
|
||||
"sale": {
|
||||
"runningGuards": "Guards"
|
||||
},
|
||||
"production": {
|
||||
"title": "Production",
|
||||
@@ -235,6 +347,10 @@
|
||||
"raft": "Raft",
|
||||
"sailing_ship": "Sailing ship"
|
||||
},
|
||||
"transport": {
|
||||
"guardCount": "Guards",
|
||||
"guardHint": "Additional cost for guards: {cost}"
|
||||
},
|
||||
"tabs": {
|
||||
"director": "Director",
|
||||
"inventory": "Inventory",
|
||||
@@ -257,6 +373,21 @@
|
||||
}
|
||||
},
|
||||
"nobility": {
|
||||
"highestPoliticalOffice": "Highest political office",
|
||||
"highestOfficeAny": "Highest office overall",
|
||||
"none": "none",
|
||||
"requirement": {
|
||||
"money": "Wealth at least {amount}",
|
||||
"cost": "Cost: {amount}",
|
||||
"branches": "At least {amount} branches",
|
||||
"reputation": "Popularity at least {amount}",
|
||||
"house_position": "House status at least level {amount}",
|
||||
"house_condition": "House condition at least {amount}",
|
||||
"office_rank_any": "Highest political or church office at least rank {amount}",
|
||||
"office_rank_political": "Highest political office at least rank {amount}",
|
||||
"lover_count_min": "At least {amount} lovers or favorites",
|
||||
"lover_count_max": "At most {amount} lovers or favorites"
|
||||
},
|
||||
"cooldown": "You can only advance again on {date}."
|
||||
},
|
||||
"mood": {
|
||||
@@ -416,6 +547,10 @@
|
||||
}
|
||||
},
|
||||
"family": {
|
||||
"debtorsPrison": {
|
||||
"familyWarning": "Ongoing debt delinquency puts strain on marriage, household and affairs.",
|
||||
"familyImpact": "Debtors' prison damages marriage, household peace and the stability of affairs."
|
||||
},
|
||||
"children": {
|
||||
"title": "Children",
|
||||
"name": "Name",
|
||||
@@ -434,6 +569,11 @@
|
||||
"baptism": "Baptize",
|
||||
"notBaptized": "Not yet baptized",
|
||||
"baptismNotice": "This child has not been baptized yet and therefore has no name.",
|
||||
"legitimacy": {
|
||||
"legitimate": "Legitimate",
|
||||
"acknowledged_bastard": "Acknowledged illegitimate",
|
||||
"hidden_bastard": "Illegitimate"
|
||||
},
|
||||
"details": {
|
||||
"title": "Child Details"
|
||||
}
|
||||
@@ -449,6 +589,8 @@
|
||||
}
|
||||
},
|
||||
"spouse": {
|
||||
"marriageSatisfaction": "Marriage Satisfaction",
|
||||
"marriageState": "Marriage State",
|
||||
"wooing": {
|
||||
"cancel": "Cancel wooing",
|
||||
"cancelConfirm": "Do you really want to cancel wooing? Progress will be lost.",
|
||||
@@ -457,6 +599,90 @@
|
||||
"cancelTooSoon": "You can only cancel wooing after 24 hours."
|
||||
}
|
||||
},
|
||||
"marriageState": {
|
||||
"stable": "Stable",
|
||||
"strained": "Strained",
|
||||
"crisis": "Crisis"
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Household Tension",
|
||||
"score": "Tension score",
|
||||
"reasonsLabel": "Current causes",
|
||||
"low": "Calm",
|
||||
"medium": "Uneasy",
|
||||
"high": "Strained",
|
||||
"reasons": {
|
||||
"visibleLover": "Visible affair",
|
||||
"noticeableLover": "Noticeable affair",
|
||||
"underfundedLover": "Underfunded affair",
|
||||
"acknowledgedAffair": "Acknowledged affair",
|
||||
"statusMismatch": "Status mismatch",
|
||||
"loverChild": "Child from an affair",
|
||||
"disorder": "Disorder in the house",
|
||||
"tooFewServants": "Too few servants",
|
||||
"marriageCrisis": "Marriage crisis"
|
||||
}
|
||||
},
|
||||
"marriageActions": {
|
||||
"title": "Support the marriage",
|
||||
"spendTime": "Spend time together",
|
||||
"giftSmall": "Small gift",
|
||||
"giftDecent": "Decent gift",
|
||||
"giftLavish": "Lavish gift",
|
||||
"reconcile": "Reconcile dispute",
|
||||
"spendTimeSuccess": "The time together has stabilized the marriage.",
|
||||
"giftSuccess": "The gift has improved the marriage.",
|
||||
"reconcileSuccess": "The dispute has been eased for now.",
|
||||
"actionError": "The action could not be completed."
|
||||
},
|
||||
"lovers": {
|
||||
"title": "Lovers and Mistresses",
|
||||
"none": "No lovers present.",
|
||||
"affection": "Affection",
|
||||
"visibility": "Visibility",
|
||||
"discretion": "Discretion",
|
||||
"maintenance": "Maintenance",
|
||||
"monthlyCost": "Monthly Cost",
|
||||
"statusFit": "Status Fit",
|
||||
"acknowledged": "Acknowledged",
|
||||
"underfunded": "{count} months underfunded",
|
||||
"role": {
|
||||
"secret_affair": "Secret affair",
|
||||
"lover": "Lover",
|
||||
"mistress_or_favorite": "Mistress or favorite"
|
||||
},
|
||||
"risk": {
|
||||
"low": "Low risk",
|
||||
"medium": "Medium risk",
|
||||
"high": "High risk"
|
||||
},
|
||||
"actions": {
|
||||
"start": "Start affair",
|
||||
"startSuccess": "The new affair has begun.",
|
||||
"startError": "The affair could not be started.",
|
||||
"maintenanceLow": "Maintenance 25",
|
||||
"maintenanceMedium": "Maintenance 50",
|
||||
"maintenanceHigh": "Maintenance 75",
|
||||
"maintenanceSuccess": "Maintenance has been updated.",
|
||||
"maintenanceError": "Maintenance could not be updated.",
|
||||
"acknowledge": "Acknowledge",
|
||||
"acknowledgeSuccess": "The relationship has been officially acknowledged.",
|
||||
"acknowledgeError": "The relationship could not be acknowledged.",
|
||||
"end": "End",
|
||||
"endConfirm": "Do you really want to end this relationship?",
|
||||
"endSuccess": "The relationship has been ended.",
|
||||
"endError": "The relationship could not be ended."
|
||||
},
|
||||
"candidates": {
|
||||
"title": "Possible affairs",
|
||||
"roleLabel": "Relationship form",
|
||||
"none": "There are currently no suitable new affairs."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"scandal": "A family scandal is shaking your house.",
|
||||
"loverBirth": "A child has been born from an affair."
|
||||
},
|
||||
"sendgift": {
|
||||
"error": {
|
||||
"nogiftselected": "Please select a gift.",
|
||||
@@ -471,10 +697,18 @@
|
||||
"church": {
|
||||
"title": "Church",
|
||||
"tabs": {
|
||||
"baptism": "Baptism",
|
||||
"current": "Current Positions",
|
||||
"available": "Available Positions",
|
||||
"applications": "Applications"
|
||||
},
|
||||
"summary": {
|
||||
"highestCurrentOffice": "Highest current office",
|
||||
"availableApplications": "Possible applications",
|
||||
"supervisedApplications": "Applications to decide",
|
||||
"guidance": "Church offices usually progress step by step. Applications are normally decided by the next higher office; if no player holds it, an NPC may later decide.",
|
||||
"none": "No church office yet"
|
||||
},
|
||||
"current": {
|
||||
"office": "Office",
|
||||
"region": "Region",
|
||||
@@ -486,11 +720,25 @@
|
||||
"office": "Office",
|
||||
"region": "Region",
|
||||
"supervisor": "Supervisor",
|
||||
"decision": "Decision by",
|
||||
"decisionType": {
|
||||
"entry": "Direct entry",
|
||||
"player": "Player",
|
||||
"npc": "NPC",
|
||||
"interim": "Interim"
|
||||
},
|
||||
"seats": "Available Seats",
|
||||
"action": "Action",
|
||||
"apply": "Apply",
|
||||
"applySuccess": "Application submitted successfully.",
|
||||
"applyError": "Error submitting application.",
|
||||
"errors": {
|
||||
"characterNotFound": "Your character could not be found.",
|
||||
"officeTypeNotFound": "The church office could not be found.",
|
||||
"churchCareerTooLow": "Your previous church career is not yet sufficient for this office.",
|
||||
"noAvailableSeats": "There are currently no free seats for this church office.",
|
||||
"applicationAlreadyExists": "There is already an open application for this church office in this region."
|
||||
},
|
||||
"none": "No available positions."
|
||||
},
|
||||
"applications": {
|
||||
@@ -534,6 +782,55 @@
|
||||
"error": "The child could not be baptized."
|
||||
}
|
||||
},
|
||||
"bank": {
|
||||
"title": "Bank",
|
||||
"account": {
|
||||
"title": "Account",
|
||||
"balance": "Balance",
|
||||
"totalDebt": "Total debt",
|
||||
"maxCredit": "Maximum credit",
|
||||
"availableCredit": "Available credit"
|
||||
},
|
||||
"debtorsPrison": {
|
||||
"titleWarning": "Credit delinquency",
|
||||
"titlePrison": "Debtors' prison",
|
||||
"descriptionWarning": "Your credits are overdue. If you continue to default, forced measures will follow.",
|
||||
"descriptionPrison": "You are in debtors' prison. New credits are blocked and your assets will be liquidated step by step.",
|
||||
"daysOverdue": "Days overdue",
|
||||
"creditworthiness": "Creditworthiness",
|
||||
"nextForcedAction": "Next forced action",
|
||||
"creditBlocked": "You cannot take new credits while imprisoned for debt.",
|
||||
"creditError": "The credit could not be taken.",
|
||||
"actions": {
|
||||
"reminder": "First reminder",
|
||||
"final_warning": "Final warning",
|
||||
"debtors_prison": "Commitment to debtors' prison",
|
||||
"asset_seizure": "Asset seizure"
|
||||
}
|
||||
},
|
||||
"credits": {
|
||||
"title": "Credits",
|
||||
"none": "You currently do not have any credits.",
|
||||
"amount": "Amount",
|
||||
"remaining": "Remaining",
|
||||
"interestRate": "Interest rate",
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"amount": "Amount",
|
||||
"reason": "Reason",
|
||||
"date": "Date"
|
||||
},
|
||||
"payoff": {
|
||||
"title": "Take a new credit",
|
||||
"height": "Credit amount",
|
||||
"remaining": "Remaining possible credit amount",
|
||||
"fee": "Credit interest",
|
||||
"feeHeight": "Installment (10 payments)",
|
||||
"total": "Total",
|
||||
"confirm": "Take credit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reputation": {
|
||||
"title": "Reputation",
|
||||
"overview": {
|
||||
@@ -604,6 +901,76 @@
|
||||
"cost": "Cost",
|
||||
"date": "Date"
|
||||
}
|
||||
},
|
||||
"underground": {
|
||||
"title": "Underground",
|
||||
"tabs": {
|
||||
"activities": "Activities",
|
||||
"attacks": "Attacks"
|
||||
},
|
||||
"activities": {
|
||||
"none": "No activities available.",
|
||||
"create": "Create new activity",
|
||||
"type": "Activity type",
|
||||
"victim": "Target person",
|
||||
"cost": "Cost",
|
||||
"status": "Status",
|
||||
"additionalInfo": "Additional information",
|
||||
"blackmailAmount": "Blackmail amount",
|
||||
"discoveries": "Discoveries",
|
||||
"visibilityDelta": "Visibility",
|
||||
"reputationDelta": "Reputation",
|
||||
"victimPlaceholder": "Enter username",
|
||||
"sabotageTarget": "Sabotage target",
|
||||
"corruptGoal": "Corruption goal",
|
||||
"affairGoal": "Investigation goal",
|
||||
"raidRegion": "Raid region",
|
||||
"raidRegionPlaceholder": "Select region",
|
||||
"bandSize": "Band size",
|
||||
"raidSummary": "Gang ({bandSize}) in {region}",
|
||||
"attempts": "Attempts",
|
||||
"successes": "Successes",
|
||||
"lastOutcome": "Last outcome",
|
||||
"raidResultTitle": "Latest raid",
|
||||
"lastTargetTransport": "Latest target transport",
|
||||
"loot": "Loot"
|
||||
},
|
||||
"attacks": {
|
||||
"target": "Attacker",
|
||||
"date": "Date",
|
||||
"success": "Success",
|
||||
"none": "No attacks recorded."
|
||||
},
|
||||
"types": {
|
||||
"spyin": "Espionage",
|
||||
"assassin": "Assassination",
|
||||
"sabotage": "Sabotage",
|
||||
"corrupt_politician": "Corruption",
|
||||
"rob": "Robbery",
|
||||
"investigate_affair": "Investigate affair",
|
||||
"raid_transport": "Raid transports"
|
||||
},
|
||||
"targets": {
|
||||
"house": "House",
|
||||
"storage": "Storage"
|
||||
},
|
||||
"goals": {
|
||||
"elect": "Appointment",
|
||||
"taxIncrease": "Raise taxes",
|
||||
"taxDecrease": "Lower taxes",
|
||||
"expose": "Expose",
|
||||
"blackmail": "Blackmail"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pending",
|
||||
"resolved": "Resolved",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"raidOutcomes": {
|
||||
"repelled": "Repelled",
|
||||
"partial_success": "Partial success",
|
||||
"major_success": "Major success"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,11 @@
|
||||
"children": "Hijos",
|
||||
"children_unbaptised": "hijos no bautizados"
|
||||
},
|
||||
"debtorsPrison": {
|
||||
"actionBlocked": "Esta acción está bloqueada mientras estés en la prisión por deudas.",
|
||||
"globalWarning": "Tu mora crediticia ya restringe tus acciones. Pronto pueden llegar medidas forzosas.",
|
||||
"globalLocked": "Estás en la prisión por deudas. Casi todas las acciones activas de Falukant están actualmente bloqueadas."
|
||||
},
|
||||
"messages": {
|
||||
"title": "Mensajes",
|
||||
"tooltip": "Mensajes",
|
||||
@@ -120,7 +125,8 @@
|
||||
"age": "Edad",
|
||||
"years": "años",
|
||||
"mainbranch": "Ciudad natal",
|
||||
"nobleTitle": "Rango"
|
||||
"nobleTitle": "Rango",
|
||||
"certificate": "Certificado"
|
||||
},
|
||||
"productions": {
|
||||
"title": "Producciones"
|
||||
@@ -207,6 +213,12 @@
|
||||
},
|
||||
"branch": {
|
||||
"title": "Sucursal",
|
||||
"debtorsPrison": {
|
||||
"branchLocked": "En la prisión por deudas se bloquean los nuevos pasos económicos. Las sucursales cerradas o embargadas también se reflejarán aquí.",
|
||||
"branchRisk": "Tu mora pone en peligro sucursales, vehículos y mercancías almacenadas.",
|
||||
"selectionBlocked": "Las nuevas ampliaciones están bloqueadas en la prisión por deudas."
|
||||
},
|
||||
"currentCertificate": "Certificado actual",
|
||||
"tabs": {
|
||||
"director": "Director",
|
||||
"inventory": "Inventario",
|
||||
@@ -310,6 +322,7 @@
|
||||
"runningEta": "Llegada",
|
||||
"runningRemaining": "Tiempo restante",
|
||||
"runningVehicleCount": "Vehículos",
|
||||
"runningGuards": "Guardias",
|
||||
"runningDirectionOut": "Salida",
|
||||
"runningDirectionIn": "Entrada"
|
||||
},
|
||||
@@ -381,6 +394,8 @@
|
||||
"transport": {
|
||||
"title": "Transporte",
|
||||
"placeholder": "Aquí puedes comprar o construir medios de transporte para tu región.",
|
||||
"guardCount": "Guardias",
|
||||
"guardHint": "Coste adicional por guardias: {cost}",
|
||||
"vehicleType": "Medio de transporte",
|
||||
"mode": "Tipo",
|
||||
"modeBuy": "Comprar (disponible de inmediato)",
|
||||
@@ -465,11 +480,17 @@
|
||||
},
|
||||
"family": {
|
||||
"title": "Familia",
|
||||
"debtorsPrison": {
|
||||
"familyWarning": "La mora continuada perjudica el matrimonio, el hogar y las relaciones.",
|
||||
"familyImpact": "La prisión por deudas daña el matrimonio, la paz del hogar y la estabilidad de las relaciones."
|
||||
},
|
||||
"spouse": {
|
||||
"title": "Relación",
|
||||
"name": "Nombre",
|
||||
"age": "Edad",
|
||||
"status": "Estado",
|
||||
"marriageSatisfaction": "Satisfacción matrimonial",
|
||||
"marriageState": "Estado del matrimonio",
|
||||
"none": "No hay cónyuge.",
|
||||
"search": "Buscar pareja",
|
||||
"found": "Pareja encontrada",
|
||||
@@ -501,6 +522,42 @@
|
||||
"progress": "Afecto",
|
||||
"jumpToPartyForm": "Organizar banquete de boda (necesario para boda e hijos)"
|
||||
},
|
||||
"marriageState": {
|
||||
"stable": "Estable",
|
||||
"strained": "Tenso",
|
||||
"crisis": "Crisis"
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Tensión del hogar",
|
||||
"score": "Valor de tensión",
|
||||
"reasonsLabel": "Causas actuales",
|
||||
"low": "Calmo",
|
||||
"medium": "Inquieto",
|
||||
"high": "Tenso",
|
||||
"reasons": {
|
||||
"visibleLover": "Relación visible",
|
||||
"noticeableLover": "Relación llamativa",
|
||||
"underfundedLover": "Relación infrafinanciada",
|
||||
"acknowledgedAffair": "Relación reconocida",
|
||||
"statusMismatch": "Desajuste social",
|
||||
"loverChild": "Hijo de una relación",
|
||||
"disorder": "Desorden en la casa",
|
||||
"tooFewServants": "Muy pocos sirvientes",
|
||||
"marriageCrisis": "Crisis matrimonial"
|
||||
}
|
||||
},
|
||||
"marriageActions": {
|
||||
"title": "Cuidar el matrimonio",
|
||||
"spendTime": "Pasar tiempo juntos",
|
||||
"giftSmall": "Regalo pequeño",
|
||||
"giftDecent": "Buen regalo",
|
||||
"giftLavish": "Regalo generoso",
|
||||
"reconcile": "Resolver disputa",
|
||||
"spendTimeSuccess": "El tiempo compartido ha estabilizado el matrimonio.",
|
||||
"giftSuccess": "El regalo ha mejorado el matrimonio.",
|
||||
"reconcileSuccess": "La disputa se ha calmado por ahora.",
|
||||
"actionError": "No se pudo realizar la acción."
|
||||
},
|
||||
"relationships": {
|
||||
"name": "Nombre"
|
||||
},
|
||||
@@ -522,14 +579,62 @@
|
||||
"baptism": "Bautizar",
|
||||
"notBaptized": "Aún no bautizado",
|
||||
"baptismNotice": "Este niño aún no ha sido bautizado y por lo tanto todavía no tiene nombre.",
|
||||
"legitimacy": {
|
||||
"legitimate": "Legítimo",
|
||||
"acknowledged_bastard": "Ilegítimo reconocido",
|
||||
"hidden_bastard": "Ilegítimo"
|
||||
},
|
||||
"details": {
|
||||
"title": "Detalles del hijo"
|
||||
}
|
||||
},
|
||||
"lovers": {
|
||||
"title": "Amantes",
|
||||
"title": "Amantes y favoritas",
|
||||
"none": "No hay amantes.",
|
||||
"affection": "Afecto"
|
||||
"affection": "Afecto",
|
||||
"visibility": "Visibilidad",
|
||||
"discretion": "Discreción",
|
||||
"maintenance": "Mantenimiento",
|
||||
"monthlyCost": "Coste mensual",
|
||||
"statusFit": "Adecuación social",
|
||||
"acknowledged": "Reconocido",
|
||||
"underfunded": "{count} meses con fondos insuficientes",
|
||||
"role": {
|
||||
"secret_affair": "Aventura secreta",
|
||||
"lover": "Amante",
|
||||
"mistress_or_favorite": "Favorita o favorito"
|
||||
},
|
||||
"risk": {
|
||||
"low": "Riesgo bajo",
|
||||
"medium": "Riesgo medio",
|
||||
"high": "Riesgo alto"
|
||||
},
|
||||
"actions": {
|
||||
"start": "Iniciar relación",
|
||||
"startSuccess": "La nueva relación ha comenzado.",
|
||||
"startError": "No se pudo iniciar la relación.",
|
||||
"maintenanceLow": "Mantenimiento 25",
|
||||
"maintenanceMedium": "Mantenimiento 50",
|
||||
"maintenanceHigh": "Mantenimiento 75",
|
||||
"maintenanceSuccess": "Se ha ajustado el mantenimiento.",
|
||||
"maintenanceError": "No se pudo ajustar el mantenimiento.",
|
||||
"acknowledge": "Reconocer",
|
||||
"acknowledgeSuccess": "La relación ha sido reconocida oficialmente.",
|
||||
"acknowledgeError": "No se pudo reconocer la relación.",
|
||||
"end": "Finalizar",
|
||||
"endConfirm": "¿De verdad quieres finalizar esta relación?",
|
||||
"endSuccess": "La relación ha finalizado.",
|
||||
"endError": "No se pudo finalizar la relación."
|
||||
},
|
||||
"candidates": {
|
||||
"title": "Posibles relaciones",
|
||||
"roleLabel": "Forma de la relación",
|
||||
"none": "Actualmente no hay nuevas relaciones adecuadas."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"scandal": "Un escándalo familiar sacude tu casa.",
|
||||
"loverBirth": "Ha nacido un hijo de una relación amorosa."
|
||||
},
|
||||
"statuses": {
|
||||
"wooing": "En cortejo",
|
||||
@@ -618,7 +723,12 @@
|
||||
"build_vehicles": "Medios de transporte construidos",
|
||||
"transport": "Transporte",
|
||||
"Marriage cost": "Costes de matrimonio",
|
||||
"marriage_gift": "Regalo para el cónyuge",
|
||||
"Gift cost": "Coste de regalo",
|
||||
"lover maintenance": "Manutención de amante",
|
||||
"servants_monthly": "Pago de servidumbre",
|
||||
"servants_hired": "Contratación de sirvientes",
|
||||
"household_order": "Orden del hogar",
|
||||
"housebuy": "Compra de casa",
|
||||
"Baptism": "Bautizo",
|
||||
"credit taken": "Crédito solicitado",
|
||||
@@ -723,6 +833,10 @@
|
||||
},
|
||||
"house": {
|
||||
"title": "Casa",
|
||||
"debtorsPrison": {
|
||||
"houseWarning": "A medida que aumenta la mora, crece el riesgo de embargo y pérdida forzosa de la casa.",
|
||||
"houseRisk": "Tu casa forma ahora parte de la posible liquidación forzosa."
|
||||
},
|
||||
"statusreport": "Estado de la casa",
|
||||
"element": "Elemento",
|
||||
"state": "Estado",
|
||||
@@ -731,8 +845,54 @@
|
||||
"price": "Precio de compra",
|
||||
"worth": "Valor restante",
|
||||
"sell": "Vender",
|
||||
"sellConfirm": "¿De verdad quieres vender tu casa?",
|
||||
"sellSuccess": "La casa ha sido vendida.",
|
||||
"sellError": "No se pudo vender la casa.",
|
||||
"buySuccess": "La casa ha sido comprada.",
|
||||
"buyError": "No se pudo comprar la casa.",
|
||||
"renovate": "Renovar",
|
||||
"renovateAll": "Renovar por completo",
|
||||
"servants": {
|
||||
"title": "Servicio doméstico",
|
||||
"description": "Administra el personal, el orden y los costes periódicos de tu casa.",
|
||||
"count": "Número de sirvientes",
|
||||
"expectedRange": "Rango esperado",
|
||||
"monthlyCost": "Coste mensual",
|
||||
"quality": "Calidad",
|
||||
"householdOrder": "Orden del hogar",
|
||||
"payLevel": "Pago",
|
||||
"payLevels": {
|
||||
"low": "Bajo",
|
||||
"normal": "Normal",
|
||||
"high": "Generoso"
|
||||
},
|
||||
"staffingState": {
|
||||
"label": "Dotación",
|
||||
"understaffed": "Insuficiente",
|
||||
"fitting": "Adecuada",
|
||||
"overstaffed": "Excesiva"
|
||||
},
|
||||
"orderState": {
|
||||
"label": "Estado del orden",
|
||||
"chaotic": "Caótico",
|
||||
"strained": "Tenso",
|
||||
"stable": "Estable",
|
||||
"excellent": "Excelente"
|
||||
},
|
||||
"actions": {
|
||||
"hire": "Contratar 1 sirviente",
|
||||
"dismiss": "Despedir 1 sirviente",
|
||||
"tidy": "Ordenar la casa",
|
||||
"hireSuccess": "Se ha ampliado el servicio doméstico.",
|
||||
"hireError": "No se pudo ampliar el servicio doméstico.",
|
||||
"dismissSuccess": "Se ha despedido a un sirviente.",
|
||||
"dismissError": "No se pudo despedir al sirviente.",
|
||||
"payLevelSuccess": "Se ha ajustado el pago del servicio.",
|
||||
"payLevelError": "No se pudo ajustar el pago.",
|
||||
"tidySuccess": "La casa ha sido ordenada.",
|
||||
"tidyError": "No se pudo ordenar la casa."
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"roofCondition": "Techo",
|
||||
"wallCondition": "Paredes",
|
||||
@@ -753,11 +913,21 @@
|
||||
"overview": "Resumen",
|
||||
"advance": "Ascender"
|
||||
},
|
||||
"highestPoliticalOffice": "Cargo político más alto",
|
||||
"highestOfficeAny": "Cargo más alto en total",
|
||||
"none": "ninguno",
|
||||
"nextTitle": "Siguiente título posible",
|
||||
"requirement": {
|
||||
"money": "Patrimonio mínimo {amount}",
|
||||
"cost": "Coste: {amount}",
|
||||
"branches": "Al menos {amount} sucursales"
|
||||
"branches": "Al menos {amount} sucursales",
|
||||
"reputation": "Popularidad mínima {amount}",
|
||||
"house_position": "Casa al menos nivel {amount}",
|
||||
"house_condition": "Estado de la casa al menos {amount}",
|
||||
"office_rank_any": "Cargo político o eclesiástico más alto al menos rango {amount}",
|
||||
"office_rank_political": "Cargo político más alto al menos rango {amount}",
|
||||
"lover_count_min": "Al menos {amount} amantes o favoritos",
|
||||
"lover_count_max": "Como máximo {amount} amantes o favoritos"
|
||||
},
|
||||
"advance": {
|
||||
"confirm": "Solicitar ascenso"
|
||||
@@ -823,6 +993,64 @@
|
||||
},
|
||||
"church": {
|
||||
"title": "Iglesia",
|
||||
"tabs": {
|
||||
"baptism": "Bautizos",
|
||||
"current": "Cargos actuales",
|
||||
"available": "Cargos disponibles",
|
||||
"applications": "Solicitudes"
|
||||
},
|
||||
"summary": {
|
||||
"highestCurrentOffice": "Cargo actual más alto",
|
||||
"availableApplications": "Solicitudes posibles",
|
||||
"supervisedApplications": "Solicitudes por decidir",
|
||||
"guidance": "Los cargos eclesiásticos suelen ascender paso a paso. Las solicitudes normalmente las decide el cargo inmediatamente superior; si no hay jugador en ese puesto, más adelante puede decidir un NPC.",
|
||||
"none": "Todavía sin cargo eclesiástico"
|
||||
},
|
||||
"current": {
|
||||
"office": "Cargo",
|
||||
"region": "Región",
|
||||
"holder": "Titular",
|
||||
"supervisor": "Superior",
|
||||
"none": "No hay cargos actuales."
|
||||
},
|
||||
"available": {
|
||||
"office": "Cargo",
|
||||
"region": "Región",
|
||||
"supervisor": "Superior",
|
||||
"decision": "Decide",
|
||||
"decisionType": {
|
||||
"entry": "Acceso directo",
|
||||
"player": "Jugador",
|
||||
"npc": "NPC",
|
||||
"interim": "Interino"
|
||||
},
|
||||
"seats": "Plazas disponibles",
|
||||
"action": "Acción",
|
||||
"apply": "Solicitar",
|
||||
"applySuccess": "Solicitud enviada correctamente.",
|
||||
"applyError": "Error al enviar la solicitud.",
|
||||
"errors": {
|
||||
"characterNotFound": "No se pudo encontrar tu personaje.",
|
||||
"officeTypeNotFound": "No se encontró el cargo eclesiástico.",
|
||||
"churchCareerTooLow": "Tu trayectoria eclesiástica todavía no es suficiente para este cargo.",
|
||||
"noAvailableSeats": "Actualmente no hay plazas libres para este cargo eclesiástico.",
|
||||
"applicationAlreadyExists": "Ya existe una solicitud abierta para este cargo eclesiástico en esta región."
|
||||
},
|
||||
"none": "No hay cargos disponibles."
|
||||
},
|
||||
"applications": {
|
||||
"office": "Cargo",
|
||||
"region": "Región",
|
||||
"applicant": "Solicitante",
|
||||
"date": "Fecha",
|
||||
"action": "Acción",
|
||||
"approve": "Aceptar",
|
||||
"reject": "Rechazar",
|
||||
"approveSuccess": "Solicitud aceptada.",
|
||||
"rejectSuccess": "Solicitud rechazada.",
|
||||
"decideError": "Error al tomar la decisión.",
|
||||
"none": "No hay solicitudes."
|
||||
},
|
||||
"baptism": {
|
||||
"title": "Bautizos",
|
||||
"table": {
|
||||
@@ -868,6 +1096,23 @@
|
||||
"maxCredit": "Crédito máximo",
|
||||
"availableCredit": "Crédito disponible"
|
||||
},
|
||||
"debtorsPrison": {
|
||||
"titleWarning": "Mora crediticia",
|
||||
"titlePrison": "Prisión por deudas",
|
||||
"descriptionWarning": "Tus créditos están en mora. Si sigues sin pagar, te amenazan medidas forzosas.",
|
||||
"descriptionPrison": "Estás en la prisión por deudas. Los nuevos créditos están bloqueados y tu patrimonio se liquidará gradualmente.",
|
||||
"daysOverdue": "Días de retraso",
|
||||
"creditworthiness": "Solvencia crediticia",
|
||||
"nextForcedAction": "Siguiente medida forzosa",
|
||||
"creditBlocked": "No puedes solicitar nuevos créditos mientras estés en la prisión por deudas.",
|
||||
"creditError": "No se pudo solicitar el crédito.",
|
||||
"actions": {
|
||||
"reminder": "Primer aviso",
|
||||
"final_warning": "Último aviso",
|
||||
"debtors_prison": "Ingreso en prisión por deudas",
|
||||
"asset_seizure": "Embargo de bienes"
|
||||
}
|
||||
},
|
||||
"credits": {
|
||||
"title": "Créditos",
|
||||
"none": "Actualmente no tienes ningún crédito.",
|
||||
@@ -1008,10 +1253,26 @@
|
||||
"type": "Tipo de actividad",
|
||||
"victim": "Objetivo",
|
||||
"cost": "Coste",
|
||||
"status": "Estado",
|
||||
"additionalInfo": "Información adicional",
|
||||
"blackmailAmount": "Suma del chantaje",
|
||||
"discoveries": "Hallazgos",
|
||||
"visibilityDelta": "Visibilidad",
|
||||
"reputationDelta": "Reputación",
|
||||
"victimPlaceholder": "Introduce el nombre de usuario",
|
||||
"sabotageTarget": "Objetivo del sabotaje",
|
||||
"corruptGoal": "Objetivo de la corrupción"
|
||||
"corruptGoal": "Objetivo de la corrupción",
|
||||
"affairGoal": "Objetivo de la investigación",
|
||||
"raidRegion": "Región de emboscada",
|
||||
"raidRegionPlaceholder": "Seleccionar región",
|
||||
"bandSize": "Tamaño de la banda",
|
||||
"raidSummary": "Banda ({bandSize}) en {region}",
|
||||
"attempts": "Intentos",
|
||||
"successes": "Éxitos",
|
||||
"lastOutcome": "Último resultado",
|
||||
"raidResultTitle": "Último asalto",
|
||||
"lastTargetTransport": "Último transporte objetivo",
|
||||
"loot": "Botín"
|
||||
},
|
||||
"attacks": {
|
||||
"target": "Atacante",
|
||||
@@ -1024,7 +1285,9 @@
|
||||
"assassin": "Atentado",
|
||||
"sabotage": "Sabotaje",
|
||||
"corrupt_politician": "Corrupción",
|
||||
"rob": "Robo"
|
||||
"rob": "Robo",
|
||||
"investigate_affair": "Investigar relación",
|
||||
"raid_transport": "Asaltos a transportes"
|
||||
},
|
||||
"targets": {
|
||||
"house": "Vivienda",
|
||||
@@ -1033,7 +1296,19 @@
|
||||
"goals": {
|
||||
"elect": "Nombramiento",
|
||||
"taxIncrease": "Subir impuestos",
|
||||
"taxDecrease": "Bajar impuestos"
|
||||
"taxDecrease": "Bajar impuestos",
|
||||
"expose": "Exponer",
|
||||
"blackmail": "Chantajear"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pendiente",
|
||||
"resolved": "Resuelto",
|
||||
"failed": "Fallido"
|
||||
},
|
||||
"raidOutcomes": {
|
||||
"repelled": "Rechazado",
|
||||
"partial_success": "Éxito parcial",
|
||||
"major_success": "Gran éxito"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default [
|
||||
meta: {
|
||||
seo: {
|
||||
title: 'Blogs auf YourPart',
|
||||
description: 'Oeffentliche Blogs, Beitraege und Community-Inhalte auf YourPart.',
|
||||
description: 'Öffentliche Blogs, Beiträge und Community-Inhalte auf YourPart.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -28,7 +28,7 @@ export default [
|
||||
meta: {
|
||||
seo: {
|
||||
title: 'Blogs auf YourPart',
|
||||
description: 'Oeffentliche Blogs, Beitraege und Community-Inhalte auf YourPart.',
|
||||
description: 'Öffentliche Blogs, Beiträge und Community-Inhalte auf YourPart.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -38,9 +38,9 @@ export default [
|
||||
component: BlogListView,
|
||||
meta: {
|
||||
seo: {
|
||||
title: 'Blogs auf YourPart - Community-Beitraege und Themen',
|
||||
description: 'Entdecke oeffentliche Blogs auf YourPart mit Community-Beitraegen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.',
|
||||
keywords: 'Blogs, Community Blog, Artikel, Beitraege, YourPart',
|
||||
title: 'Blogs auf YourPart - Community-Beiträge und Themen',
|
||||
description: 'Entdecke öffentliche Blogs auf YourPart mit Community-Beiträgen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.',
|
||||
keywords: 'Blogs, Community Blog, Artikel, Beiträge, YourPart',
|
||||
canonicalPath: '/blogs',
|
||||
jsonLd: [
|
||||
{
|
||||
@@ -48,7 +48,7 @@ export default [
|
||||
'@type': 'CollectionPage',
|
||||
name: 'Blogs auf YourPart',
|
||||
url: buildAbsoluteUrl('/blogs'),
|
||||
description: 'Oeffentliche Blogs und Community-Beitraege auf YourPart.',
|
||||
description: 'Öffentliche Blogs und Community-Beiträge auf YourPart.',
|
||||
inLanguage: 'de',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,12 +6,44 @@ import apiClient from '../utils/axios.js';
|
||||
import { io } from 'socket.io-client';
|
||||
import { getDaemonSocketUrl, getSocketIoUrl } from '../utils/appConfig.js';
|
||||
|
||||
const AUTH_KEYS = ['isLoggedIn', 'user', 'userid'];
|
||||
|
||||
function getStoredValue(key) {
|
||||
return localStorage.getItem(key) ?? sessionStorage.getItem(key);
|
||||
}
|
||||
|
||||
function getStoredUser() {
|
||||
const storedUser = getStoredValue('user');
|
||||
if (!storedUser) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(storedUser);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearAuthStorage() {
|
||||
AUTH_KEYS.forEach((key) => {
|
||||
localStorage.removeItem(key);
|
||||
sessionStorage.removeItem(key);
|
||||
});
|
||||
}
|
||||
|
||||
function persistAuthStorage(user, rememberMe) {
|
||||
const targetStorage = rememberMe ? localStorage : sessionStorage;
|
||||
clearAuthStorage();
|
||||
targetStorage.setItem('isLoggedIn', 'true');
|
||||
targetStorage.setItem('user', JSON.stringify(user));
|
||||
targetStorage.setItem('userid', user?.id || '');
|
||||
}
|
||||
|
||||
const store = createStore({
|
||||
state: {
|
||||
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true',
|
||||
isLoggedIn: getStoredValue('isLoggedIn') === 'true',
|
||||
connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
|
||||
daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
|
||||
user: JSON.parse(localStorage.getItem('user')) || null,
|
||||
user: getStoredUser(),
|
||||
// Reconnect state management
|
||||
backendRetryCount: 0,
|
||||
daemonRetryCount: 0,
|
||||
@@ -52,11 +84,12 @@ const store = createStore({
|
||||
menuNeedsUpdate: false,
|
||||
},
|
||||
mutations: {
|
||||
async dologin(state, user) {
|
||||
async dologin(state, payload) {
|
||||
const loginPayload = payload?.user ? payload : { user: payload, rememberMe: true };
|
||||
const { user, rememberMe = true } = loginPayload;
|
||||
state.isLoggedIn = true;
|
||||
state.user = user;
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
persistAuthStorage(user, rememberMe);
|
||||
state.menuNeedsUpdate = true;
|
||||
if (user.param.filter(param => ['birthdate', 'gender'].includes(param.name)).length < 2) {
|
||||
router.push({ path: '/settings/personal' });
|
||||
@@ -65,8 +98,7 @@ const store = createStore({
|
||||
async dologout(state) {
|
||||
state.isLoggedIn = false;
|
||||
state.user = null;
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('user');
|
||||
clearAuthStorage();
|
||||
localStorage.removeItem('menu');
|
||||
state.menuNeedsUpdate = false;
|
||||
|
||||
@@ -145,8 +177,8 @@ const store = createStore({
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async login({ commit, dispatch }, user) {
|
||||
await commit('dologin', user);
|
||||
async login({ commit, dispatch }, payload) {
|
||||
await commit('dologin', payload);
|
||||
await dispatch('initializeSocket');
|
||||
await dispatch('initializeDaemonSocket');
|
||||
const socket = this.getters.socket;
|
||||
@@ -224,11 +256,9 @@ const store = createStore({
|
||||
}
|
||||
|
||||
const maxRetries = 10;
|
||||
console.log(`Backend-Reconnect-Versuch ${state.backendRetryCount + 1}/${maxRetries}`);
|
||||
|
||||
if (state.backendRetryCount >= maxRetries) {
|
||||
// Nach maxRetries alle 5 Sekunden weiter versuchen
|
||||
console.log('Backend: Max Retries erreicht, versuche weiter alle 5 Sekunden...');
|
||||
state.backendRetryTimer = setTimeout(() => {
|
||||
state.backendRetryCount = 0; // Reset für nächsten Zyklus
|
||||
state.backendRetryTimer = null;
|
||||
@@ -241,7 +271,6 @@ const store = createStore({
|
||||
|
||||
state.backendRetryCount++;
|
||||
const delay = 5000; // Alle 5 Sekunden versuchen
|
||||
console.log(`Backend: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`);
|
||||
|
||||
state.backendRetryTimer = setTimeout(() => {
|
||||
state.backendRetryTimer = null;
|
||||
@@ -259,8 +288,6 @@ const store = createStore({
|
||||
// Vite bindet Umgebungsvariablen zur Build-Zeit ein, daher Fallback-Logik basierend auf Hostname
|
||||
let daemonUrl = getDaemonSocketUrl();
|
||||
|
||||
console.log('[Daemon] Finale Daemon-URL:', daemonUrl);
|
||||
|
||||
const connectDaemonSocket = () => {
|
||||
// Cleanup existing socket and timer
|
||||
if (state.daemonSocket) {
|
||||
|
||||
@@ -52,6 +52,11 @@ export function showApiError(context, error, fallback = 'tr:error.network') {
|
||||
showError(context, fallback, fallback);
|
||||
}
|
||||
|
||||
export async function confirmAction(context, options = {}) {
|
||||
const refs = getRootRefs(context);
|
||||
return refs.chooseDialog?.open?.(options) ?? false;
|
||||
}
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
const getAppContext = () => app._instance?.proxy;
|
||||
@@ -61,7 +66,8 @@ export default {
|
||||
showSuccess: (...args) => showSuccess(getAppContext(), ...args),
|
||||
showInfo: (...args) => showInfo(getAppContext(), ...args),
|
||||
showError: (...args) => showError(getAppContext(), ...args),
|
||||
showApiError: (...args) => showApiError(getAppContext(), ...args)
|
||||
showApiError: (...args) => showApiError(getAppContext(), ...args),
|
||||
confirmAction: (...args) => confirmAction(getAppContext(), ...args)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
7
frontend/src/utils/threeLoaders.js
Normal file
7
frontend/src/utils/threeLoaders.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
||||
|
||||
export {
|
||||
DRACOLoader,
|
||||
GLTFLoader
|
||||
};
|
||||
9
frontend/src/utils/threeModelRuntime.js
Normal file
9
frontend/src/utils/threeModelRuntime.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { AnimationMixer } from 'three/src/animation/AnimationMixer.js';
|
||||
import { Box3 } from 'three/src/math/Box3.js';
|
||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||
|
||||
export {
|
||||
AnimationMixer,
|
||||
Box3,
|
||||
Vector3
|
||||
};
|
||||
19
frontend/src/utils/threeRuntime.js
Normal file
19
frontend/src/utils/threeRuntime.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera.js';
|
||||
import { Clock } from 'three/src/core/Clock.js';
|
||||
import { AmbientLight } from 'three/src/lights/AmbientLight.js';
|
||||
import { DirectionalLight } from 'three/src/lights/DirectionalLight.js';
|
||||
import { Color } from 'three/src/math/Color.js';
|
||||
import { TextureLoader } from 'three/src/loaders/TextureLoader.js';
|
||||
import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer.js';
|
||||
import { Scene } from 'three/src/scenes/Scene.js';
|
||||
|
||||
export {
|
||||
AmbientLight,
|
||||
Clock,
|
||||
Color,
|
||||
DirectionalLight,
|
||||
PerspectiveCamera,
|
||||
Scene,
|
||||
TextureLoader,
|
||||
WebGLRenderer
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="admin-minigames-view">
|
||||
<div class="admin-header">
|
||||
<h1>{{ $t('admin.match3.title') }}</h1>
|
||||
<p>Verwalte Minigames, Level und Konfigurationen</p>
|
||||
@@ -14,7 +13,7 @@
|
||||
<div>
|
||||
<span class="workflow-hero__eyebrow">Arbeitsfluss</span>
|
||||
<h2>{{ $t('admin.match3.title') }}</h2>
|
||||
<p>Erst Level waehlen, dann Spielfeld und Ziele anpassen und erst am Ende speichern.</p>
|
||||
<p>Erst Level wählen, dann Spielfeld und Ziele anpassen und erst am Ende speichern.</p>
|
||||
</div>
|
||||
<div class="workflow-hero__meta">
|
||||
<span class="workflow-pill">{{ currentModeLabel }}</span>
|
||||
@@ -25,13 +24,13 @@
|
||||
<section class="workflow-grid">
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">1</span>
|
||||
<h3>Level waehlen</h3>
|
||||
<p>Bestehendes Level oeffnen oder sofort mit einer neuen Vorlage starten.</p>
|
||||
<h3>Level wählen</h3>
|
||||
<p>Bestehendes Level öffnen oder sofort mit einer neuen Vorlage starten.</p>
|
||||
</article>
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">2</span>
|
||||
<h3>Spielfeld bauen</h3>
|
||||
<p>Groesse, Zuege, Kacheln und Layout zuerst festziehen, bevor Ziele folgen.</p>
|
||||
<p>Größe, Züge, Kacheln und Layout zuerst festziehen, bevor Ziele folgen.</p>
|
||||
</article>
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">3</span>
|
||||
@@ -79,7 +78,7 @@
|
||||
<article class="admin-summary-card surface-card">
|
||||
<span class="admin-summary-card__label">Spielfeld</span>
|
||||
<strong>{{ levelForm.boardWidth }} x {{ levelForm.boardHeight }}</strong>
|
||||
<p>{{ levelForm.moveLimit }} Zuege, {{ levelForm.tileTypes.length }} aktive Tile-Typen.</p>
|
||||
<p>{{ levelForm.moveLimit }} Züge, {{ levelForm.tileTypes.length }} aktive Tile-Typen.</p>
|
||||
</article>
|
||||
<article class="admin-summary-card surface-card">
|
||||
<span class="admin-summary-card__label">Objectives</span>
|
||||
@@ -601,14 +600,13 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SimpleTabs from '../../components/SimpleTabs.vue';
|
||||
import apiClient from '../../utils/axios.js';
|
||||
import { showError, showSuccess } from '@/utils/feedback.js';
|
||||
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AdminMinigamesView',
|
||||
@@ -1016,15 +1014,20 @@ export default {
|
||||
},
|
||||
|
||||
async deleteLevel(levelId) {
|
||||
if (confirm('Möchtest du dieses Level wirklich löschen?')) {
|
||||
try {
|
||||
await apiClient.delete(`/api/admin/minigames/match3/levels/${levelId}`);
|
||||
this.loadLevels();
|
||||
showSuccess(this, 'Level wurde geloescht.');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Levels:', error);
|
||||
showError(this, 'Fehler beim Loeschen des Levels');
|
||||
}
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: 'Level löschen',
|
||||
message: 'Möchtest du dieses Level wirklich löschen?'
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.delete(`/api/admin/minigames/match3/levels/${levelId}`);
|
||||
this.loadLevels();
|
||||
showSuccess(this, 'Level wurde gelöscht.');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Levels:', error);
|
||||
showError(this, 'Fehler beim Löschen des Levels');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1047,14 +1050,20 @@ export default {
|
||||
},
|
||||
|
||||
removeObjective(index) {
|
||||
if (confirm('Möchtest du dieses Objective wirklich löschen?')) {
|
||||
confirmAction(this, {
|
||||
title: 'Ziel löschen',
|
||||
message: 'Möchtest du dieses Objective wirklich löschen?'
|
||||
}).then((confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.levelForm.objectives.splice(index, 1);
|
||||
|
||||
// Aktualisiere die Reihenfolge
|
||||
this.levelForm.objectives.forEach((objective, idx) => {
|
||||
objective.order = idx + 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async loadObjectivesForLevel(levelId) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="services-status-view">
|
||||
<div class="admin-header">
|
||||
<h1>{{ $t('admin.servicesStatus.title') }}</h1>
|
||||
<p>{{ $t('admin.servicesStatus.description') }}</p>
|
||||
@@ -95,7 +94,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket Log Dialog -->
|
||||
<WebSocketLogDialog ref="webSocketLogDialog" />
|
||||
@@ -481,4 +479,3 @@ export default {
|
||||
color: #c62828;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="admin-taxi-tools-view">
|
||||
<div class="admin-header">
|
||||
<h1>{{ $t('admin.taxiTools.title') }}</h1>
|
||||
<p>{{ $t('admin.taxiTools.description') }}</p>
|
||||
@@ -367,7 +366,6 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Dialog -->
|
||||
<MessageDialog ref="messageDialog" />
|
||||
@@ -378,6 +376,7 @@
|
||||
import SimpleTabs from '../../components/SimpleTabs.vue';
|
||||
import MessageDialog from '../../dialogues/standard/MessageDialog.vue';
|
||||
import apiClient from '../../utils/axios.js';
|
||||
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
// Matrix: erlaubte Haus-Tür-Richtungen je Tile-Typ und Ecke
|
||||
// Richtungen: bottom, right, top, left
|
||||
@@ -1216,10 +1215,10 @@ export default {
|
||||
|
||||
// Erfolgsmeldung anzeigen
|
||||
const message = isUpdate ? 'admin.taxiTools.mapEditor.updateSuccess' : 'admin.taxiTools.mapEditor.createSuccess';
|
||||
this.$refs.messageDialog.open(`tr:${message}`);
|
||||
showSuccess(this, `tr:${message}`);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Map:', error);
|
||||
alert('Fehler beim Speichern der Map');
|
||||
showError(this, 'Fehler beim Speichern der Map');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1247,16 +1246,20 @@ export default {
|
||||
},
|
||||
|
||||
async deleteMap(mapId) {
|
||||
if (confirm('Möchtest du diese Map wirklich löschen?')) {
|
||||
try {
|
||||
await apiClient.delete(`/api/taxi-maps/maps/${mapId}`);
|
||||
this.loadMaps();
|
||||
|
||||
// Erfolgsmeldung anzeigen
|
||||
this.$refs.messageDialog.open('tr:admin.taxiTools.mapEditor.deleteSuccess');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Map:', error);
|
||||
}
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: 'Map löschen',
|
||||
message: 'Möchtest du diese Map wirklich löschen?'
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.delete(`/api/taxi-maps/maps/${mapId}`);
|
||||
this.loadMaps();
|
||||
showSuccess(this, 'tr:admin.taxiTools.mapEditor.deleteSuccess');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Map:', error);
|
||||
showError(this, 'Fehler beim Löschen der Map');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<div class="create-npc-page">
|
||||
<div class="create-npc-view">
|
||||
<h1>{{ $t('admin.falukant.createNPC.title') }}</h1>
|
||||
|
||||
@@ -102,7 +101,6 @@
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -276,11 +276,9 @@ export default {
|
||||
}
|
||||
},
|
||||
async loadStockTypes() {
|
||||
console.log('Loading stock types...');
|
||||
this.loading.stockTypes = true;
|
||||
try {
|
||||
const stockTypesResult = await apiClient.get('/api/admin/falukant/stock-types');
|
||||
console.log('Stock types loaded:', stockTypesResult.data);
|
||||
this.stockTypes = stockTypesResult.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading stock types:', error);
|
||||
@@ -327,19 +325,13 @@ export default {
|
||||
}
|
||||
},
|
||||
canAddStock(branch) {
|
||||
console.log('canAddStock called for branch:', branch);
|
||||
console.log('stockTypes:', this.stockTypes);
|
||||
console.log('branch.stocks:', branch.stocks);
|
||||
|
||||
// Wenn keine Stock-Types geladen sind, zeige den Button nicht
|
||||
if (!this.stockTypes || this.stockTypes.length === 0) {
|
||||
console.log('No stock types loaded, returning false');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wenn keine Stocks vorhanden sind, kann immer hinzugefügt werden
|
||||
if (!branch.stocks || branch.stocks.length === 0) {
|
||||
console.log('No stocks in branch, returning true');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -350,8 +342,6 @@ export default {
|
||||
const availableStockTypes = this.stockTypes.filter(stockType =>
|
||||
!existingStockTypeIds.includes(stockType.id)
|
||||
);
|
||||
|
||||
console.log('Available stock types:', availableStockTypes);
|
||||
return availableStockTypes.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="contentscroll falukant-map-admin">
|
||||
<div class="falukant-map-admin">
|
||||
<div class="admin-header">
|
||||
<h1>{{ $t('admin.falukant.map.title') }}</h1>
|
||||
<p>{{ $t('admin.falukant.map.description') }}</p>
|
||||
@@ -212,13 +211,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||
import { confirmAction, showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AdminFalukantMapRegionsView',
|
||||
@@ -469,17 +468,21 @@ export default {
|
||||
await this.loadConnections();
|
||||
} catch (error) {
|
||||
console.error('Error saving region distance:', error);
|
||||
alert(this.$t('admin.falukant.map.errorSaveConnection'));
|
||||
showError(this, this.$t('admin.falukant.map.errorSaveConnection'));
|
||||
}
|
||||
},
|
||||
async deleteConnection(id) {
|
||||
if (!confirm(this.$t('admin.falukant.map.confirmDeleteConnection'))) return;
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: this.$t('admin.falukant.map.connectionsTitle'),
|
||||
message: this.$t('admin.falukant.map.confirmDeleteConnection')
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/admin/falukant/region-distances/${id}`);
|
||||
await this.loadConnections();
|
||||
} catch (error) {
|
||||
console.error('Error deleting region distance:', error);
|
||||
alert(this.$t('admin.falukant.map.errorDeleteConnection'));
|
||||
showError(this, this.$t('admin.falukant.map.errorDeleteConnection'));
|
||||
}
|
||||
},
|
||||
regionName(id) {
|
||||
@@ -635,5 +638,3 @@ export default {
|
||||
.btn.mini.icon {
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
<script>
|
||||
import { createBlog, updateBlog, getBlog, createPost, shareBlog } from '@/api/blogApi.js';
|
||||
import RichTextEditor from './components/RichTextEditor.vue';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
export default {
|
||||
name: 'BlogEditorView',
|
||||
components: { RichTextEditor },
|
||||
@@ -107,7 +108,7 @@ export default {
|
||||
async save() {
|
||||
if (this.form.visibility === 'logged_in') {
|
||||
if (this.form.ageMin != null && this.form.ageMax != null && this.form.ageMin > this.form.ageMax) {
|
||||
alert('Ungültiger Altersbereich');
|
||||
showError(this, 'Ungültiger Altersbereich');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
<span class="blog-list__kicker">Community-Blogs</span>
|
||||
<h1>Blogs</h1>
|
||||
<p>Artikel, Projektstaende und persoenliche Einblicke aus der YourPart-Community.</p>
|
||||
<p>Artikel, Projektstände und persönliche Einblicke aus der YourPart-Community.</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<router-link v-if="$store.getters.isLoggedIn" class="btn" to="/blogs/create">Neuen Blog erstellen</router-link>
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
return slug ? `/blogs/${encodeURIComponent(slug)}` : `/blogs/${blog.id}`;
|
||||
},
|
||||
blogExcerpt(blog) {
|
||||
const source = blog?.description || 'Oeffentliche Eintraege, Gedanken und Projektstaende aus der Community.';
|
||||
const source = blog?.description || 'Öffentliche Einträge, Gedanken und Projektstände aus der Community.';
|
||||
return source.length > 150 ? `${source.slice(0, 147)}...` : source;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<section class="posts surface-card">
|
||||
<div class="posts__header">
|
||||
<h2>{{ $t('blog.posts') }}</h2>
|
||||
<span class="posts__count">{{ total }} Eintraege</span>
|
||||
<span class="posts__count">{{ total }} Einträge</span>
|
||||
</div>
|
||||
<div v-if="!items.length" class="blog-view__state">Keine Eintraege vorhanden.</div>
|
||||
<div v-if="!items.length" class="blog-view__state">Keine Einträge vorhanden.</div>
|
||||
<article v-for="p in items" :key="p.id" class="post">
|
||||
<h3>{{ p.title }}</h3>
|
||||
<div class="content" v-html="sanitize(p.content)" />
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
.map((item) => `${item.title || ''} ${stripHtml(item.content || '')}`.trim())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const summarySource = this.blog.description || plainTextPosts || 'Oeffentlicher Community-Blog auf YourPart.';
|
||||
const summarySource = this.blog.description || plainTextPosts || 'Öffentlicher Community-Blog auf YourPart.';
|
||||
const description = truncateText(summarySource, 160);
|
||||
const canonicalPath = this.canonicalBlogPath();
|
||||
|
||||
@@ -143,7 +143,7 @@ export default {
|
||||
await this.fetchPage(1);
|
||||
this.applyBlogSeo();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error('Blog konnte nicht geladen werden:', e);
|
||||
// this.$router.replace('/blogs');
|
||||
applySeo({
|
||||
title: 'Blog nicht gefunden | YourPart',
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="bank-view">
|
||||
<StatusBar />
|
||||
<div class="contentscroll">
|
||||
<div class="bank-content">
|
||||
|
||||
<h2>{{ $t('falukant.bank.title') }}</h2>
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||
|
||||
<!-- OVERVIEW -->
|
||||
<div v-if="activeTab === 'account'">
|
||||
<div v-if="debtorsPrison.active" class="debt-status" :class="{ 'is-prison': debtorsPrison.inDebtorsPrison }">
|
||||
<h3>{{ debtStatusTitle }}</h3>
|
||||
<p>{{ debtStatusDescription }}</p>
|
||||
<div class="debt-status__meta">
|
||||
<span>{{ $t('falukant.bank.debtorsPrison.daysOverdue') }}: <strong>{{ debtorsPrison.daysOverdue }}</strong></span>
|
||||
<span>{{ $t('falukant.bank.debtorsPrison.creditworthiness') }}: <strong>{{ debtorsPrison.creditworthiness }}</strong></span>
|
||||
<span v-if="debtorsPrison.nextForcedAction">
|
||||
{{ $t('falukant.bank.debtorsPrison.nextForcedAction') }}:
|
||||
<strong>{{ $t(`falukant.bank.debtorsPrison.actions.${debtorsPrison.nextForcedAction}`) }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="account-section">
|
||||
<table>
|
||||
<tr>
|
||||
@@ -26,6 +38,10 @@
|
||||
<td>{{ $t('falukant.bank.account.availableCredit') }}</td>
|
||||
<td>{{ formatCost(bankOverview.availableCredit) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.bank.debtorsPrison.creditworthiness') }}</td>
|
||||
<td>{{ bankOverview.creditworthiness }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,9 +93,12 @@
|
||||
<p>
|
||||
<strong>{{ $t('falukant.bank.credits.payoff.total') }}: {{ formatCost(creditCost()) }}</strong>
|
||||
</p>
|
||||
<button @click="confirmPayoff" class="button" :disabled="!selectedCredit">
|
||||
<button @click="confirmPayoff" class="button" :disabled="!selectedCredit || isCreditBlocked">
|
||||
{{ $t('falukant.bank.credits.payoff.confirm') }}
|
||||
</button>
|
||||
<p v-if="isCreditBlocked" class="payoff-hint payoff-hint--error">
|
||||
{{ $t('falukant.bank.debtorsPrison.creditBlocked') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,6 +112,7 @@ import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||
import SimpleTabs from '@/components/SimpleTabs.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapState } from 'vuex';
|
||||
import { showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'BankView',
|
||||
@@ -118,34 +138,114 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket'])
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
debtorsPrison() {
|
||||
return this.bankOverview.debtorsPrison || {
|
||||
active: false,
|
||||
inDebtorsPrison: false,
|
||||
daysOverdue: 0,
|
||||
creditworthiness: 100,
|
||||
nextForcedAction: null
|
||||
};
|
||||
},
|
||||
isCreditBlocked() {
|
||||
return this.debtorsPrison.inDebtorsPrison;
|
||||
},
|
||||
debtStatusTitle() {
|
||||
return this.debtorsPrison.inDebtorsPrison
|
||||
? this.$t('falukant.bank.debtorsPrison.titlePrison')
|
||||
: this.$t('falukant.bank.debtorsPrison.titleWarning');
|
||||
},
|
||||
debtStatusDescription() {
|
||||
return this.debtorsPrison.inDebtorsPrison
|
||||
? this.$t('falukant.bank.debtorsPrison.descriptionPrison')
|
||||
: this.$t('falukant.bank.debtorsPrison.descriptionWarning');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
socket(newVal, oldVal) {
|
||||
if (oldVal) this.teardownSocketEvents();
|
||||
if (newVal) this.setupSocketEvents();
|
||||
},
|
||||
daemonSocket(newVal, oldVal) {
|
||||
if (oldVal) this.teardownSocketEvents();
|
||||
if (newVal) this.setupSocketEvents();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadBankOverview();
|
||||
this.setupSocketEvents();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.socket) {
|
||||
this.socket.off('falukantUpdateStatus', this.loadBankOverview);
|
||||
if (this._pendingRefresh) {
|
||||
clearTimeout(this._pendingRefresh);
|
||||
this._pendingRefresh = null;
|
||||
}
|
||||
this.teardownSocketEvents();
|
||||
},
|
||||
methods: {
|
||||
matchesCurrentUser(eventData) {
|
||||
if (eventData?.user_id == null) {
|
||||
return true;
|
||||
}
|
||||
const currentIds = [this.user?.id, this.user?.hashedId]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value));
|
||||
return currentIds.includes(String(eventData.user_id));
|
||||
},
|
||||
setupSocketEvents() {
|
||||
this.teardownSocketEvents();
|
||||
if (this.socket) {
|
||||
this.socket.on('falukantUpdateStatus', (data) => {
|
||||
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => this.setupSocketEvents(), 1000);
|
||||
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
|
||||
this._debtSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data });
|
||||
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
|
||||
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
|
||||
this.socket.on('falukantUpdateDebt', this._debtSocketHandler);
|
||||
}
|
||||
if (this.daemonSocket) {
|
||||
this._daemonHandler = (event) => this.handleDaemonMessage(event);
|
||||
this.daemonSocket.addEventListener('message', this._daemonHandler);
|
||||
}
|
||||
},
|
||||
teardownSocketEvents() {
|
||||
if (this.socket) {
|
||||
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
|
||||
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
|
||||
if (this._debtSocketHandler) this.socket.off('falukantUpdateDebt', this._debtSocketHandler);
|
||||
}
|
||||
if (this.daemonSocket && this._daemonHandler) {
|
||||
this.daemonSocket.removeEventListener('message', this._daemonHandler);
|
||||
this._daemonHandler = null;
|
||||
}
|
||||
},
|
||||
handleEvent(eventData) {
|
||||
if (!this.matchesCurrentUser(eventData)) {
|
||||
return;
|
||||
}
|
||||
switch (eventData.event) {
|
||||
case 'falukantUpdateStatus':
|
||||
this.loadBankOverview();
|
||||
this.queueBankRefresh();
|
||||
break;
|
||||
case 'falukantUpdateDebt':
|
||||
this.queueBankRefresh();
|
||||
break;
|
||||
case 'falukantUpdateFamily':
|
||||
if (['monthly', 'lover_installment'].includes(eventData.reason)) {
|
||||
this.queueBankRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
queueBankRefresh() {
|
||||
if (this._pendingRefresh) {
|
||||
clearTimeout(this._pendingRefresh);
|
||||
}
|
||||
this._pendingRefresh = setTimeout(() => {
|
||||
this._pendingRefresh = null;
|
||||
this.loadBankOverview();
|
||||
}, 120);
|
||||
},
|
||||
async loadBankOverview() {
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/bank/overview');
|
||||
@@ -155,6 +255,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async confirmPayoff() {
|
||||
if (this.isCreditBlocked) return;
|
||||
try {
|
||||
await apiClient.post('/api/falukant/bank/credits', {
|
||||
height: this.selectedCredit
|
||||
@@ -163,16 +264,17 @@ export default {
|
||||
this.selectedCredit = null;
|
||||
this.activeTab = 'credits';
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showError(err.response?.data?.error || this.$t('falukant.bank.debtorsPrison.creditError'));
|
||||
}
|
||||
},
|
||||
handleDaemonMessage(msg) {
|
||||
try {
|
||||
if (['falukantUpdateStatus', 'moneyChange', 'creditChange'].includes(msg.event)) {
|
||||
this.loadBankOverview();
|
||||
const data = JSON.parse(msg.data);
|
||||
if (['falukantUpdateStatus', 'falukantUpdateDebt'].includes(data.event)) {
|
||||
this.handleEvent(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(evt, err);
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
feeRate() {
|
||||
@@ -190,4 +292,38 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
h2 { padding-top: 20px; }
|
||||
|
||||
.debt-status {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem 1.1rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid rgba(180, 120, 40, 0.35);
|
||||
background: linear-gradient(180deg, rgba(255, 244, 223, 0.95), rgba(255, 249, 238, 0.98));
|
||||
}
|
||||
|
||||
.debt-status.is-prison {
|
||||
border-color: rgba(146, 57, 40, 0.45);
|
||||
background: linear-gradient(180deg, rgba(255, 232, 225, 0.96), rgba(255, 245, 241, 0.98));
|
||||
}
|
||||
|
||||
.debt-status h3 {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.debt-status__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.payoff-hint {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.payoff-hint--error {
|
||||
color: #8b2f23;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="falukant-branch-view">
|
||||
<StatusBar ref="statusBar" />
|
||||
<div class="contentscroll">
|
||||
<div class="falukant-branch">
|
||||
<div class="falukant-branch">
|
||||
<section class="branch-hero surface-card">
|
||||
<div>
|
||||
<span class="branch-kicker">Niederlassung</span>
|
||||
<h2>{{ $t('falukant.branch.title') }}</h2>
|
||||
<p>Produktion, Lager, Verkauf und Transport in einer spielweltbezogenen Steuerflaeche.</p>
|
||||
<p>Produktion, Lager, Verkauf und Transport in einer spielweltbezogenen Steuerfläche.</p>
|
||||
<div class="branch-hero__meta">
|
||||
<span class="branch-hero__badge">
|
||||
{{ $t('falukant.branch.currentCertificate') }}: {{ currentCertificate ?? '---' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="debtorsPrison.active"
|
||||
class="branch-debt-warning surface-card"
|
||||
:class="{ 'is-prison': debtorsPrison.inDebtorsPrison }"
|
||||
>
|
||||
<strong>
|
||||
{{ debtorsPrison.inDebtorsPrison
|
||||
? $t('falukant.bank.debtorsPrison.titlePrison')
|
||||
: $t('falukant.bank.debtorsPrison.titleWarning') }}
|
||||
</strong>
|
||||
<p>
|
||||
{{ debtorsPrison.inDebtorsPrison
|
||||
? $t('falukant.branch.debtorsPrison.branchLocked')
|
||||
: $t('falukant.branch.debtorsPrison.branchRisk') }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<BranchSelection
|
||||
:branches="branches"
|
||||
:selectedBranch="selectedBranch"
|
||||
:blocked="debtorsPrison.inDebtorsPrison"
|
||||
:blocked-reason="debtorsPrison.inDebtorsPrison ? $t('falukant.branch.debtorsPrison.selectionBlocked') : ''"
|
||||
@branchSelected="onBranchSelected"
|
||||
@createBranch="createBranch"
|
||||
@upgradeBranch="upgradeBranch"
|
||||
@@ -235,6 +258,13 @@
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('falukant.branch.transport.guardCount') }}
|
||||
<input v-model.number="sendVehicleDialog.guardCount" type="number" min="0" max="20" />
|
||||
</label>
|
||||
<p class="transport-guards-hint">
|
||||
{{ $t('falukant.branch.transport.guardHint', { cost: formatMoney((sendVehicleDialog.guardCount || 0) * 4) }) }}
|
||||
</p>
|
||||
<div class="modal-buttons">
|
||||
<button @click="sendVehicles" :disabled="!sendVehicleDialog.targetBranchId">
|
||||
{{ $t('falukant.branch.transport.send') }}
|
||||
@@ -315,7 +345,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -332,6 +361,7 @@ import RevenueSection from '@/components/falukant/RevenueSection.vue';
|
||||
import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapState } from 'vuex';
|
||||
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: "BranchView",
|
||||
@@ -384,6 +414,7 @@ export default {
|
||||
vehicleTypeId: null,
|
||||
targetBranchId: null,
|
||||
success: false,
|
||||
guardCount: 0,
|
||||
},
|
||||
repairVehicleDialog: {
|
||||
show: false,
|
||||
@@ -399,11 +430,17 @@ export default {
|
||||
branchTaxes: null,
|
||||
branchTaxesLoading: false,
|
||||
branchTaxesError: null,
|
||||
currentCertificate: null,
|
||||
debtorsPrison: {
|
||||
active: false,
|
||||
inDebtorsPrison: false
|
||||
},
|
||||
pendingBranchRefresh: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket']),
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
freeVehiclesByType() {
|
||||
const grouped = {};
|
||||
for (const v of this.vehicles || []) {
|
||||
@@ -437,6 +474,7 @@ export default {
|
||||
await this.loadBranches();
|
||||
|
||||
const branchId = this.$route.params.branchId;
|
||||
await this.loadCurrentCertificate();
|
||||
await this.loadProducts();
|
||||
|
||||
if (branchId) {
|
||||
@@ -455,6 +493,8 @@ export default {
|
||||
// Live-Socket-Events (Backend Socket.io)
|
||||
if (this.socket) {
|
||||
this.socket.on('falukantUpdateStatus', (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data }));
|
||||
this.socket.on('falukantUpdateDebt', (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data }));
|
||||
this.socket.on('falukantUpdateProductionCertificate', (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data }));
|
||||
this.socket.on('falukantBranchUpdate', (data) => this.handleEvent({ event: 'falukantBranchUpdate', ...data }));
|
||||
this.socket.on('transport_arrived', (data) => this.handleEvent({ event: 'transport_arrived', ...data }));
|
||||
this.socket.on('inventory_updated', (data) => this.handleEvent({ event: 'inventory_updated', ...data }));
|
||||
@@ -464,12 +504,18 @@ export default {
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
if (this.pendingBranchRefresh) {
|
||||
clearTimeout(this.pendingBranchRefresh);
|
||||
this.pendingBranchRefresh = null;
|
||||
}
|
||||
// Daemon WebSocket: Listener entfernen (der Socket selbst wird beim Logout geschlossen)
|
||||
if (this.daemonSocket) {
|
||||
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.off('falukantUpdateStatus');
|
||||
this.socket.off('falukantUpdateDebt');
|
||||
this.socket.off('falukantUpdateProductionCertificate');
|
||||
this.socket.off('falukantBranchUpdate');
|
||||
this.socket.off('transport_arrived');
|
||||
this.socket.off('inventory_updated');
|
||||
@@ -494,6 +540,34 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
matchesCurrentUser(eventData) {
|
||||
if (eventData?.user_id == null) {
|
||||
return true;
|
||||
}
|
||||
const currentIds = [this.user?.id, this.user?.hashedId]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value));
|
||||
return currentIds.includes(String(eventData.user_id));
|
||||
},
|
||||
queueBranchRefresh() {
|
||||
if (this.pendingBranchRefresh) {
|
||||
clearTimeout(this.pendingBranchRefresh);
|
||||
}
|
||||
this.pendingBranchRefresh = setTimeout(async () => {
|
||||
this.pendingBranchRefresh = null;
|
||||
this.$refs.statusBar?.fetchStatus();
|
||||
await this.loadCurrentCertificate();
|
||||
await this.loadProducts();
|
||||
this.$refs.productionSection?.loadProductions();
|
||||
this.$refs.productionSection?.loadStorage();
|
||||
this.$refs.storageSection?.loadStorageData();
|
||||
this.$refs.saleSection?.loadInventory();
|
||||
if (this.$refs.revenueSection) {
|
||||
this.$refs.revenueSection.products = this.products;
|
||||
this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
|
||||
}
|
||||
}, 120);
|
||||
},
|
||||
async loadBranches() {
|
||||
try {
|
||||
const result = await apiClient.get('/api/falukant/branches');
|
||||
@@ -513,6 +587,18 @@ export default {
|
||||
console.error('Error loading branches:', error);
|
||||
}
|
||||
},
|
||||
async loadCurrentCertificate() {
|
||||
try {
|
||||
const result = await apiClient.get('/api/falukant/user');
|
||||
this.currentCertificate = result.data?.certificate ?? null;
|
||||
this.debtorsPrison = result.data?.debtorsPrison || {
|
||||
active: false,
|
||||
inDebtorsPrison: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading certificate:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadProducts() {
|
||||
try {
|
||||
@@ -629,6 +715,10 @@ export default {
|
||||
},
|
||||
|
||||
async createBranch() {
|
||||
if (this.debtorsPrison.inDebtorsPrison) {
|
||||
showError(this, this.$t('falukant.branch.debtorsPrison.selectionBlocked'));
|
||||
return;
|
||||
}
|
||||
await this.loadBranches();
|
||||
// Nach dem Anlegen eines neuen Branches automatisch den
|
||||
// zuletzt/neu erstellten Branch auswählen.
|
||||
@@ -645,6 +735,10 @@ export default {
|
||||
|
||||
async upgradeBranch() {
|
||||
if (!this.selectedBranch) return;
|
||||
if (this.debtorsPrison.inDebtorsPrison) {
|
||||
showError(this, this.$t('falukant.branch.debtorsPrison.selectionBlocked'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.post('/api/falukant/branches/upgrade', {
|
||||
branchId: this.selectedBranch.id,
|
||||
@@ -657,7 +751,7 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error upgrading branch:', error);
|
||||
alert(this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
|
||||
showError(this, this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -772,6 +866,9 @@ export default {
|
||||
},
|
||||
|
||||
handleEvent(eventData) {
|
||||
if (!this.matchesCurrentUser(eventData)) {
|
||||
return;
|
||||
}
|
||||
switch (eventData.event) {
|
||||
case 'production_ready':
|
||||
this.$refs.productionSection?.loadProductions();
|
||||
@@ -799,30 +896,13 @@ export default {
|
||||
this.$refs.productionSection?.loadStorage();
|
||||
break;
|
||||
case 'falukantUpdateStatus':
|
||||
case 'falukantUpdateDebt':
|
||||
case 'falukantUpdateProductionCertificate':
|
||||
case 'falukantBranchUpdate':
|
||||
if (this.$refs.statusBar) {
|
||||
this.$refs.statusBar.fetchStatus();
|
||||
}
|
||||
|
||||
if (this.$refs.productionSection) {
|
||||
this.$refs.productionSection.loadProductions();
|
||||
this.$refs.productionSection.loadStorage();
|
||||
}
|
||||
|
||||
if (this.$refs.storageSection) {
|
||||
this.$refs.storageSection.loadStorageData();
|
||||
}
|
||||
|
||||
if (this.$refs.saleSection) {
|
||||
this.$refs.saleSection.loadInventory();
|
||||
}
|
||||
this.queueBranchRefresh();
|
||||
break;
|
||||
case 'knowledge_update':
|
||||
this.loadProducts();
|
||||
if (this.$refs.revenueSection) {
|
||||
this.$refs.revenueSection.products = this.products;
|
||||
this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
|
||||
}
|
||||
this.queueBranchRefresh();
|
||||
break;
|
||||
case 'transport_arrived':
|
||||
// Leerer Transport angekommen - Fahrzeug wurde zurückgeholt
|
||||
@@ -881,7 +961,7 @@ export default {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('Unhandled event:', eventData);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -919,6 +999,7 @@ export default {
|
||||
vehicleTypeId: null,
|
||||
targetBranchId: null,
|
||||
success: false,
|
||||
guardCount: 0,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -930,6 +1011,7 @@ export default {
|
||||
vehicleTypeId: vehicleTypeId,
|
||||
targetBranchId: null,
|
||||
success: false,
|
||||
guardCount: 0,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -941,6 +1023,7 @@ export default {
|
||||
vehicleTypeId: null,
|
||||
targetBranchId: null,
|
||||
success: false,
|
||||
guardCount: 0,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -956,7 +1039,7 @@ export default {
|
||||
|
||||
async sendVehicles() {
|
||||
if (!this.sendVehicleDialog.targetBranchId) {
|
||||
alert(this.$t('falukant.branch.transport.selectTargetError'));
|
||||
showError(this, this.$t('falukant.branch.transport.selectTargetError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -966,6 +1049,7 @@ export default {
|
||||
targetBranchId: this.sendVehicleDialog.targetBranchId,
|
||||
productId: null,
|
||||
quantity: 0,
|
||||
guardCount: this.sendVehicleDialog.guardCount || 0,
|
||||
};
|
||||
|
||||
if (this.sendVehicleDialog.vehicleIds && this.sendVehicleDialog.vehicleIds.length > 0) {
|
||||
@@ -975,7 +1059,7 @@ export default {
|
||||
} else if (this.sendVehicleDialog.vehicleTypeId) {
|
||||
payload.vehicleTypeId = this.sendVehicleDialog.vehicleTypeId;
|
||||
} else {
|
||||
alert(this.$t('falukant.branch.transport.noVehiclesSelected'));
|
||||
showError(this, this.$t('falukant.branch.transport.noVehiclesSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -984,7 +1068,7 @@ export default {
|
||||
this.sendVehicleDialog.success = true;
|
||||
} catch (error) {
|
||||
console.error('Error sending vehicles:', error);
|
||||
alert(this.$t('falukant.branch.transport.sendError'));
|
||||
showApiError(this, error, this.$t('falukant.branch.transport.sendError'));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1043,12 +1127,12 @@ export default {
|
||||
});
|
||||
await this.loadVehicles();
|
||||
this.closeRepairAllVehiclesDialog();
|
||||
alert(this.$t('falukant.branch.transport.repairAllSuccess'));
|
||||
showSuccess(this, this.$t('falukant.branch.transport.repairAllSuccess'));
|
||||
this.$refs.statusBar?.fetchStatus();
|
||||
} catch (error) {
|
||||
console.error('Error repairing all vehicles:', error);
|
||||
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairAllError');
|
||||
alert(errorMessage);
|
||||
showError(this, errorMessage);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1105,12 +1189,12 @@ export default {
|
||||
await apiClient.post(`/api/falukant/vehicles/${this.repairVehicleDialog.vehicle.id}/repair`);
|
||||
await this.loadVehicles();
|
||||
this.closeRepairVehicleDialog();
|
||||
alert(this.$t('falukant.branch.transport.repairSuccess'));
|
||||
showSuccess(this, this.$t('falukant.branch.transport.repairSuccess'));
|
||||
this.$refs.statusBar?.fetchStatus();
|
||||
} catch (error) {
|
||||
console.error('Error repairing vehicle:', error);
|
||||
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairError');
|
||||
alert(errorMessage);
|
||||
showError(this, errorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -1150,6 +1234,39 @@ export default {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.branch-debt-warning {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid rgba(180, 120, 40, 0.32);
|
||||
background: linear-gradient(180deg, rgba(255, 244, 223, 0.95), rgba(255, 250, 239, 0.98));
|
||||
}
|
||||
|
||||
.branch-debt-warning.is-prison {
|
||||
border-color: rgba(146, 57, 40, 0.4);
|
||||
background: linear-gradient(180deg, rgba(255, 232, 225, 0.96), rgba(255, 245, 241, 0.98));
|
||||
}
|
||||
|
||||
.branch-debt-warning p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.branch-hero__meta {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.branch-hero__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(138, 84, 17, 0.16);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: #7a4b12;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.branch-tab-content {
|
||||
margin-top: 16px;
|
||||
padding: 18px;
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="church-view">
|
||||
<StatusBar />
|
||||
<div class="contentscroll">
|
||||
<div class="church-content">
|
||||
<h2>{{ $t('falukant.church.title') }}</h2>
|
||||
<div class="church-summary">
|
||||
<div class="church-summary-card">
|
||||
<div class="church-summary-label">{{ $t('falukant.church.summary.highestCurrentOffice') }}</div>
|
||||
<div class="church-summary-value">{{ highestCurrentOfficeLabel }}</div>
|
||||
</div>
|
||||
<div class="church-summary-card">
|
||||
<div class="church-summary-label">{{ $t('falukant.church.summary.availableApplications') }}</div>
|
||||
<div class="church-summary-value">{{ availablePositions.length }}</div>
|
||||
</div>
|
||||
<div class="church-summary-card">
|
||||
<div class="church-summary-label">{{ $t('falukant.church.summary.supervisedApplications') }}</div>
|
||||
<div class="church-summary-value">{{ supervisedApplications.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="church-guidance">
|
||||
<p>{{ $t('falukant.church.summary.guidance') }}</p>
|
||||
</div>
|
||||
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -87,6 +106,7 @@
|
||||
<th>{{ $t('falukant.church.available.office') }}</th>
|
||||
<th>{{ $t('falukant.church.available.region') }}</th>
|
||||
<th>{{ $t('falukant.church.available.supervisor') }}</th>
|
||||
<th>{{ $t('falukant.church.available.decision') }}</th>
|
||||
<th>{{ $t('falukant.church.available.seats') }}</th>
|
||||
<th>{{ $t('falukant.church.available.action') }}</th>
|
||||
</tr>
|
||||
@@ -101,6 +121,7 @@
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
<td>{{ decisionModeLabel(pos.decisionMode) }}</td>
|
||||
<td>{{ pos.availableSeats }}</td>
|
||||
<td>
|
||||
<button @click="applyForPosition(pos)" :disabled="pos.availableSeats === 0">
|
||||
@@ -109,7 +130,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!availablePositions.length">
|
||||
<td colspan="5">{{ $t('falukant.church.available.none') }}</td>
|
||||
<td colspan="6">{{ $t('falukant.church.available.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -166,6 +187,7 @@ 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'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'ChurchView',
|
||||
@@ -196,9 +218,37 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
highestCurrentOffice() {
|
||||
if (!this.currentPositions.length) {
|
||||
return null;
|
||||
}
|
||||
return [...this.currentPositions]
|
||||
.filter(pos => this.isOwnPosition(pos))
|
||||
.sort((a, b) => this.officeRank(b.officeType?.name) - this.officeRank(a.officeType?.name))[0] || null;
|
||||
},
|
||||
highestCurrentOfficeLabel() {
|
||||
if (!this.highestCurrentOffice?.officeType?.name) {
|
||||
return this.$t('falukant.church.summary.none');
|
||||
}
|
||||
return this.$t(`falukant.church.offices.${this.highestCurrentOffice.officeType.name}`);
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadNotBaptisedChildren();
|
||||
await this.loadOwnCharacterId();
|
||||
await Promise.all([
|
||||
this.loadCurrentPositions(),
|
||||
this.loadAvailablePositions(),
|
||||
this.loadSupervisedApplications()
|
||||
]);
|
||||
this.setupSocketEvents();
|
||||
this.setupDaemonListeners();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.teardownSocketEvents();
|
||||
this.teardownDaemonListeners();
|
||||
},
|
||||
watch: {
|
||||
activeTab(newTab) {
|
||||
@@ -212,6 +262,90 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
matchesCurrentUser(eventData) {
|
||||
if (eventData?.user_id == null) {
|
||||
return true;
|
||||
}
|
||||
const currentIds = [this.user?.id, this.user?.hashedId]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value));
|
||||
return currentIds.includes(String(eventData.user_id));
|
||||
},
|
||||
setupSocketEvents() {
|
||||
this.teardownSocketEvents();
|
||||
if (!this.socket) {
|
||||
setTimeout(() => this.setupSocketEvents(), 1000);
|
||||
return;
|
||||
}
|
||||
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
this._churchSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateChurch', ...data });
|
||||
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
|
||||
this.socket.on('falukantUpdateChurch', this._churchSocketHandler);
|
||||
},
|
||||
teardownSocketEvents() {
|
||||
if (!this.socket) return;
|
||||
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
|
||||
if (this._churchSocketHandler) this.socket.off('falukantUpdateChurch', this._churchSocketHandler);
|
||||
},
|
||||
setupDaemonListeners() {
|
||||
this.teardownDaemonListeners();
|
||||
if (!this.daemonSocket) return;
|
||||
this._daemonChurchHandler = (event) => {
|
||||
if (event.data === 'ping') return;
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if (['falukantUpdateStatus', 'falukantUpdateChurch'].includes(message.event)) {
|
||||
this.handleEvent(message);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
this.daemonSocket.addEventListener('message', this._daemonChurchHandler);
|
||||
},
|
||||
teardownDaemonListeners() {
|
||||
if (this.daemonSocket && this._daemonChurchHandler) {
|
||||
this.daemonSocket.removeEventListener('message', this._daemonChurchHandler);
|
||||
this._daemonChurchHandler = null;
|
||||
}
|
||||
},
|
||||
queueChurchRefresh({ current = true, available = true, applications = true } = {}) {
|
||||
if (this._pendingChurchRefresh) {
|
||||
clearTimeout(this._pendingChurchRefresh);
|
||||
}
|
||||
this._pendingChurchRefresh = setTimeout(async () => {
|
||||
this._pendingChurchRefresh = null;
|
||||
const tasks = [];
|
||||
if (current) tasks.push(this.loadCurrentPositions());
|
||||
if (available) tasks.push(this.loadAvailablePositions());
|
||||
if (applications) tasks.push(this.loadSupervisedApplications());
|
||||
await Promise.all(tasks);
|
||||
}, 120);
|
||||
},
|
||||
handleEvent(eventData) {
|
||||
if (!this.matchesCurrentUser(eventData)) {
|
||||
return;
|
||||
}
|
||||
switch (eventData.event) {
|
||||
case 'falukantUpdateStatus':
|
||||
this.queueChurchRefresh();
|
||||
break;
|
||||
case 'falukantUpdateChurch':
|
||||
switch (eventData.reason) {
|
||||
case 'applications':
|
||||
this.queueChurchRefresh({ current: false, available: false, applications: true });
|
||||
break;
|
||||
case 'npc_decision':
|
||||
this.queueChurchRefresh({ current: true, available: true, applications: true });
|
||||
break;
|
||||
case 'appointment':
|
||||
case 'vacancy_fill':
|
||||
case 'promotion':
|
||||
default:
|
||||
this.queueChurchRefresh({ current: true, available: true, applications: false });
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
async loadNotBaptisedChildren() {
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/family/notbaptised')
|
||||
@@ -292,6 +426,24 @@ export default {
|
||||
}
|
||||
return pos.character.id === this.ownCharacterId;
|
||||
},
|
||||
officeRank(name) {
|
||||
const ranks = {
|
||||
'lay-preacher': 0,
|
||||
'village-priest': 1,
|
||||
'parish-priest': 2,
|
||||
dean: 3,
|
||||
archdeacon: 4,
|
||||
bishop: 5,
|
||||
archbishop: 6,
|
||||
cardinal: 7,
|
||||
pope: 8
|
||||
};
|
||||
return ranks[name] ?? -1;
|
||||
},
|
||||
decisionModeLabel(mode) {
|
||||
const key = `falukant.church.available.decisionType.${mode || 'interim'}`;
|
||||
return this.$te(key) ? this.$t(key) : mode || '—';
|
||||
},
|
||||
async applyForPosition(position) {
|
||||
try {
|
||||
const regionId = position.regionId || position.region?.id;
|
||||
@@ -310,7 +462,7 @@ export default {
|
||||
} catch (err) {
|
||||
console.error('Error applying for position', err);
|
||||
const errorMsg = err.response?.data?.message || 'falukant.church.available.applyError';
|
||||
this.$root.$refs.errorDialog?.open(`tr:${errorMsg}`);
|
||||
this.$root.$refs.errorDialog?.open(errorMsg.startsWith('falukant.') ? `tr:${errorMsg}` : `tr:falukant.church.available.applyError`);
|
||||
}
|
||||
},
|
||||
async decideOnApplication(applicationId, decision) {
|
||||
@@ -370,6 +522,35 @@ h2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.church-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0 0.75rem;
|
||||
}
|
||||
|
||||
.church-summary-card,
|
||||
.church-guidance {
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.church-summary-label {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.church-summary-value {
|
||||
margin-top: 0.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.church-guidance {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 140px;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="contenthidden">
|
||||
<div class="education-view">
|
||||
<StatusBar />
|
||||
<div class="contentscroll">
|
||||
<div class="education-content">
|
||||
<h2>{{ $t('falukant.education.title') }}</h2>
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user