Compare commits
26 Commits
59869e077e
...
main
| 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 | ||
|
|
9d44a265ca | ||
|
|
4442937ebd |
@@ -36,6 +36,19 @@ const app = express();
|
||||
// - LOG_ALL_REQ=1: Logge alle Requests
|
||||
const LOG_ALL_REQ = process.env.LOG_ALL_REQ === '1';
|
||||
const LOG_SLOW_REQ_MS = Number.parseInt(process.env.LOG_SLOW_REQ_MS || '500', 10);
|
||||
const defaultCorsOrigins = [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:5173',
|
||||
'http://127.0.0.1:3000',
|
||||
'http://127.0.0.1:5173'
|
||||
];
|
||||
const corsOrigins = (process.env.CORS_ORIGINS || process.env.FRONTEND_URL || '')
|
||||
.split(',')
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean);
|
||||
const effectiveCorsOrigins = corsOrigins.length > 0 ? corsOrigins : defaultCorsOrigins;
|
||||
const corsAllowAll = process.env.CORS_ALLOW_ALL === '1';
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const reqId = req.headers['x-request-id'] || (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(8).toString('hex'));
|
||||
req.reqId = reqId;
|
||||
@@ -51,15 +64,26 @@ app.use((req, res, next) => {
|
||||
});
|
||||
|
||||
const corsOptions = {
|
||||
origin: ['http://localhost:3000', 'http://localhost:5173', 'http://127.0.0.1:3000', 'http://127.0.0.1:5173'],
|
||||
origin(origin, callback) {
|
||||
if (!origin) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
if (corsAllowAll || effectiveCorsOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
},
|
||||
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'userId', 'authCode'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'userid', 'authcode', 'userId', 'authCode'],
|
||||
credentials: true,
|
||||
preflightContinue: false,
|
||||
optionsSuccessStatus: 204
|
||||
};
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
app.options('*', cors(corsOptions));
|
||||
app.use(express.json()); // To handle JSON request bodies
|
||||
|
||||
app.use('/api/chat', chatRouter);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -13,7 +13,8 @@ import { setupWebSocket } from './utils/socket.js';
|
||||
import { syncDatabase } from './utils/syncDatabase.js';
|
||||
|
||||
// HTTP-Server für API (Port 2020, intern, über Apache-Proxy)
|
||||
const API_PORT = Number.parseInt(process.env.PORT || '2020', 10);
|
||||
const API_PORT = Number.parseInt(process.env.API_PORT || process.env.PORT || '2020', 10);
|
||||
const API_HOST = process.env.API_HOST || '127.0.0.1';
|
||||
const httpServer = http.createServer(app);
|
||||
// Socket.io wird nur auf HTTPS-Server bereitgestellt, nicht auf HTTP-Server
|
||||
// setupWebSocket(httpServer); // Entfernt: Socket.io nur über HTTPS
|
||||
@@ -25,6 +26,7 @@ const USE_TLS = process.env.SOCKET_IO_TLS === '1';
|
||||
const TLS_KEY_PATH = process.env.SOCKET_IO_TLS_KEY_PATH;
|
||||
const TLS_CERT_PATH = process.env.SOCKET_IO_TLS_CERT_PATH;
|
||||
const TLS_CA_PATH = process.env.SOCKET_IO_TLS_CA_PATH;
|
||||
const SOCKET_IO_HOST = process.env.SOCKET_IO_HOST || '0.0.0.0';
|
||||
|
||||
if (USE_TLS && TLS_KEY_PATH && TLS_CERT_PATH) {
|
||||
try {
|
||||
@@ -45,14 +47,14 @@ if (USE_TLS && TLS_KEY_PATH && TLS_CERT_PATH) {
|
||||
|
||||
syncDatabase().then(() => {
|
||||
// API-Server auf Port 2020 (intern, nur localhost)
|
||||
httpServer.listen(API_PORT, '127.0.0.1', () => {
|
||||
console.log(`[API] HTTP-Server läuft auf localhost:${API_PORT} (intern, über Apache-Proxy)`);
|
||||
httpServer.listen(API_PORT, API_HOST, () => {
|
||||
console.log(`[API] HTTP-Server läuft auf ${API_HOST}:${API_PORT}`);
|
||||
});
|
||||
|
||||
// Socket.io-Server auf Port 4443 (extern, direkt erreichbar)
|
||||
if (httpsServer) {
|
||||
httpsServer.listen(SOCKET_IO_PORT, '0.0.0.0', () => {
|
||||
console.log(`[Socket.io] HTTPS-Server läuft auf Port ${SOCKET_IO_PORT} (direkt erreichbar)`);
|
||||
httpsServer.listen(SOCKET_IO_PORT, SOCKET_IO_HOST, () => {
|
||||
console.log(`[Socket.io] HTTPS-Server läuft auf ${SOCKET_IO_HOST}:${SOCKET_IO_PORT}`);
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import amqp from 'amqplib/callback_api.js';
|
||||
import User from '../models/community/user.js';
|
||||
import Room from '../models/chat/room.js';
|
||||
|
||||
const RABBITMQ_URL = 'amqp://localhost';
|
||||
const RABBITMQ_URL = process.env.AMQP_URL || 'amqp://localhost';
|
||||
const QUEUE = 'oneToOne_messages';
|
||||
|
||||
class ChatService {
|
||||
@@ -13,11 +13,37 @@ class ChatService {
|
||||
this.users = [];
|
||||
this.randomChats = [];
|
||||
this.oneToOneChats = [];
|
||||
this.channel = null;
|
||||
this.amqpAvailable = false;
|
||||
this.initRabbitMq();
|
||||
}
|
||||
|
||||
initRabbitMq() {
|
||||
amqp.connect(RABBITMQ_URL, (err, connection) => {
|
||||
if (err) throw err;
|
||||
connection.createChannel((err, channel) => {
|
||||
if (err) throw err;
|
||||
if (err) {
|
||||
console.warn(`[chatService] RabbitMQ nicht erreichbar (${RABBITMQ_URL}) - fallback ohne Queue wird verwendet.`);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.on('error', (connectionError) => {
|
||||
console.warn('[chatService] RabbitMQ-Verbindung fehlerhaft:', connectionError.message);
|
||||
this.channel = null;
|
||||
this.amqpAvailable = false;
|
||||
});
|
||||
|
||||
connection.on('close', () => {
|
||||
console.warn('[chatService] RabbitMQ-Verbindung geschlossen.');
|
||||
this.channel = null;
|
||||
this.amqpAvailable = false;
|
||||
});
|
||||
|
||||
connection.createChannel((channelError, channel) => {
|
||||
if (channelError) {
|
||||
console.warn('[chatService] RabbitMQ-Channel konnte nicht erstellt werden:', channelError.message);
|
||||
return;
|
||||
}
|
||||
this.channel = channel;
|
||||
this.amqpAvailable = true;
|
||||
channel.assertQueue(QUEUE, { durable: false });
|
||||
});
|
||||
});
|
||||
@@ -118,8 +144,14 @@ class ChatService {
|
||||
history: [messageBundle],
|
||||
});
|
||||
}
|
||||
if (this.channel) {
|
||||
if (this.channel && this.amqpAvailable) {
|
||||
try {
|
||||
this.channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(messageBundle)));
|
||||
} catch (error) {
|
||||
console.warn('[chatService] sendToQueue fehlgeschlagen, Queue-Bridge vorübergehend deaktiviert:', error.message);
|
||||
this.channel = null;
|
||||
this.amqpAvailable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import net from 'net';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const DEFAULT_CONFIG = { host: 'localhost', port: 1235 };
|
||||
const DEFAULT_CONFIG = {
|
||||
host: process.env.CHAT_TCP_HOST || 'localhost',
|
||||
port: Number.parseInt(process.env.CHAT_TCP_PORT || '1235', 10),
|
||||
};
|
||||
|
||||
function loadBridgeConfig() {
|
||||
try {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,38 +2,110 @@
|
||||
import { Server } from 'socket.io';
|
||||
import amqp from 'amqplib/callback_api.js';
|
||||
|
||||
const RABBITMQ_URL = 'amqp://localhost';
|
||||
const RABBITMQ_URL = process.env.AMQP_URL || 'amqp://localhost';
|
||||
const QUEUE = 'chat_messages';
|
||||
const MAX_PENDING_MESSAGES = 500;
|
||||
|
||||
function routeMessage(io, message) {
|
||||
if (!message || typeof message !== 'object') return;
|
||||
|
||||
if (message.socketId) {
|
||||
io.to(message.socketId).emit('newMessage', message);
|
||||
return;
|
||||
}
|
||||
if (message.recipientSocketId) {
|
||||
io.to(message.recipientSocketId).emit('newMessage', message);
|
||||
return;
|
||||
}
|
||||
if (message.roomId) {
|
||||
io.to(String(message.roomId)).emit('newMessage', message);
|
||||
return;
|
||||
}
|
||||
if (message.room) {
|
||||
io.to(String(message.room)).emit('newMessage', message);
|
||||
return;
|
||||
}
|
||||
|
||||
io.emit('newMessage', message);
|
||||
}
|
||||
|
||||
export function setupWebSocket(server) {
|
||||
const io = new Server(server);
|
||||
let channel = null;
|
||||
let pendingMessages = [];
|
||||
|
||||
const flushPendingMessages = () => {
|
||||
if (!channel || pendingMessages.length === 0) return;
|
||||
const queued = pendingMessages;
|
||||
pendingMessages = [];
|
||||
for (const message of queued) {
|
||||
try {
|
||||
channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message)));
|
||||
} catch (err) {
|
||||
console.warn('[webSocketService] Flush fehlgeschlagen, Nachricht bleibt im Fallback:', err.message);
|
||||
pendingMessages.unshift(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
amqp.connect(RABBITMQ_URL, (err, connection) => {
|
||||
if (err) throw err;
|
||||
if (err) {
|
||||
console.warn(`[webSocketService] RabbitMQ nicht erreichbar (${RABBITMQ_URL}) - WebSocket läuft ohne Queue-Bridge.`);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.createChannel((err, channel) => {
|
||||
if (err) throw err;
|
||||
connection.on('error', (connectionError) => {
|
||||
console.warn('[webSocketService] RabbitMQ-Verbindung fehlerhaft:', connectionError.message);
|
||||
channel = null;
|
||||
});
|
||||
|
||||
connection.on('close', () => {
|
||||
console.warn('[webSocketService] RabbitMQ-Verbindung geschlossen.');
|
||||
channel = null;
|
||||
});
|
||||
|
||||
connection.createChannel((channelError, createdChannel) => {
|
||||
if (channelError) {
|
||||
console.warn('[webSocketService] RabbitMQ-Channel konnte nicht erstellt werden:', channelError.message);
|
||||
return;
|
||||
}
|
||||
|
||||
channel = createdChannel;
|
||||
channel.assertQueue(QUEUE, { durable: false });
|
||||
channel.consume(QUEUE, (msg) => {
|
||||
if (!msg) return;
|
||||
const message = JSON.parse(msg.content.toString());
|
||||
routeMessage(io, message);
|
||||
}, { noAck: true });
|
||||
flushPendingMessages();
|
||||
});
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Client connected via WebSocket');
|
||||
|
||||
// Konsumiert Nachrichten aus RabbitMQ und sendet sie an den WebSocket-Client
|
||||
channel.consume(QUEUE, (msg) => {
|
||||
const message = JSON.parse(msg.content.toString());
|
||||
io.emit('newMessage', message); // Broadcast an alle Clients
|
||||
}, { noAck: true });
|
||||
|
||||
// Empfangt eine Nachricht vom WebSocket-Client und sendet sie an die RabbitMQ-Warteschlange
|
||||
socket.on('newMessage', (message) => {
|
||||
if (channel) {
|
||||
try {
|
||||
channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message)));
|
||||
} catch (err) {
|
||||
console.warn('[webSocketService] sendToQueue fehlgeschlagen, nutze In-Memory-Fallback:', err.message);
|
||||
channel = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
pendingMessages.push(message);
|
||||
if (pendingMessages.length > MAX_PENDING_MESSAGES) {
|
||||
pendingMessages = pendingMessages.slice(-MAX_PENDING_MESSAGES);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Client disconnected');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
];
|
||||
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@ export function setupWebSocket(server) {
|
||||
|
||||
export function getIo() {
|
||||
if (!io) {
|
||||
throw new Error('Socket.io ist nicht initialisiert!');
|
||||
return null;
|
||||
}
|
||||
return io;
|
||||
}
|
||||
@@ -46,6 +46,10 @@ export function getUserSockets() {
|
||||
|
||||
export async function notifyUser(recipientHashedUserId, event, data) {
|
||||
const io = getIo();
|
||||
if (!io) {
|
||||
if (DEBUG_SOCKETS) console.warn('[socket.io] notifyUser übersprungen: Socket.io nicht initialisiert');
|
||||
return;
|
||||
}
|
||||
const userSockets = getUserSockets();
|
||||
try {
|
||||
const recipientUser = await baseService.getUserByHashedId(recipientHashedUserId);
|
||||
@@ -70,6 +74,10 @@ export async function notifyUser(recipientHashedUserId, event, data) {
|
||||
|
||||
export async function notifyAllUsers(event, data) {
|
||||
const io = getIo();
|
||||
if (!io) {
|
||||
if (DEBUG_SOCKETS) console.warn('[socket.io] notifyAllUsers übersprungen: Socket.io nicht initialisiert');
|
||||
return;
|
||||
}
|
||||
const userSockets = getUserSockets();
|
||||
|
||||
try {
|
||||
|
||||
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.
|
||||
230
docs/UI_REDESIGN_PLAN.md
Normal file
230
docs/UI_REDESIGN_PLAN.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# UI Redesign Plan
|
||||
|
||||
## Status
|
||||
|
||||
- Prioritaet 1 ist umgesetzt.
|
||||
- Prioritaet 2 ist umgesetzt.
|
||||
- Phase 3 ist abgeschlossen.
|
||||
- Phase 4 ist abgeschlossen.
|
||||
- Phase 5 ist abgeschlossen.
|
||||
- Das Redesign ist damit insgesamt abgeschlossen.
|
||||
- Optionaler technischer Nachlauf:
|
||||
- weiterer Performance-Feinschliff rund um den separaten three-Chunk
|
||||
|
||||
## Ziel
|
||||
|
||||
Das Frontend von YourPart soll visuell und strukturell modernisiert werden, ohne die bestehende Funktionsbreite zu verlieren. Der Fokus liegt auf einem klareren Designsystem, besserer Informationshierarchie, konsistenter Navigation, responsiver Nutzung und einer deutlich hochwertigeren Wahrnehmung auf Startseite, Community-Bereichen und Spiele-/Lernseiten.
|
||||
|
||||
## Ausgangslage
|
||||
|
||||
Aktueller Stand aus dem Code:
|
||||
|
||||
- Globale Styles in `frontend/src/assets/styles.scss` nutzen ein sehr einfaches Basisset mit `Arial`, wenigen Tokens und kaum skalierbarer Designsystem-Logik.
|
||||
- `AppHeader.vue`, `AppNavigation.vue` und `AppFooter.vue` sind funktional, aber visuell eher wie ein klassisches Webportal aufgebaut.
|
||||
- Farben, Abstaende, Border-Radius, Buttons und Typografie wirken nicht konsistent genug fuer ein modernes Produktbild.
|
||||
- Navigation und Fensterleiste sind stark desktop-zentriert und sollten auf mobile Nutzung und klare Priorisierung neu gedacht werden.
|
||||
- Die Landing- und Content-Bereiche haben unterschiedliche visuelle Sprachen statt eines durchgaengigen Systems.
|
||||
|
||||
## Ziele des Redesigns
|
||||
|
||||
- Moderner, eigenstaendiger Look statt generischer Standard-UI.
|
||||
- Einheitliches Designsystem mit Tokens fuer Farben, Typografie, Spacing, Schatten, Radius und States.
|
||||
- Saubere responsive Struktur fuer Desktop, Tablet und Mobile.
|
||||
- Bessere Orientierung in Community, Blog, Vokabeltrainer und Falukant.
|
||||
- Hoeherer wahrgenommener Qualitaetsstandard bei gleicher oder besserer Performance.
|
||||
- Reduktion visueller Altlasten, Inline-Anmutung und inkonsistenter Bedienelemente.
|
||||
|
||||
## Nicht-Ziele
|
||||
|
||||
- Kein kompletter Funktionsumbau im ersten Schritt.
|
||||
- Keine grossflaechige Backend-Aenderung.
|
||||
- Kein unkontrolliertes Ersetzen aller bestehenden Komponenten auf einmal.
|
||||
|
||||
## Prioritaet 1: Fundament schaffen
|
||||
|
||||
### 1. Designsystem definieren
|
||||
|
||||
- Neue Design-Tokens in einer zentralen Schicht aufbauen:
|
||||
- Farben mit klarer Primar-, Sekundar-, Surface- und Statuslogik.
|
||||
- Typografiesystem mit moderner Schriftfamilie, Skalierung und konsistenten Headline-/Body-Stilen.
|
||||
- Spacing-System, Radius-System und Schatten-System standardisieren.
|
||||
- Zustandsdefinitionen fuer Hover, Focus, Active, Disabled.
|
||||
|
||||
### 2. Globale Styling-Basis modernisieren
|
||||
|
||||
- `frontend/src/assets/styles.scss` in ein wartbares Fundament ueberfuehren.
|
||||
- Einheitliche Defaults fuer `body`, `a`, `button`, Inputs, Listen, Headlines und Fokus-Stati.
|
||||
- Gemeinsame Utility-Klassen nur dort einfuehren, wo sie echten Wiederverwendungswert haben.
|
||||
- Barrierefreiheit von Kontrasten und Fokus-Indikatoren direkt mitdenken.
|
||||
|
||||
### 3. Layout-Architektur festziehen
|
||||
|
||||
- Feste Regeln fuer Header, Navigation, Content-Flaeche, Footer und Dialog-Layer definieren.
|
||||
- Maximale Content-Breiten und Raster fuer Marketing-, Dashboard- und Formularseiten festlegen.
|
||||
- Dialoge, Window-Bar und Overlay-Logik visuell harmonisieren.
|
||||
|
||||
## Prioritaet 2: Kernnavigation neu gestalten
|
||||
|
||||
### 4. Header ueberarbeiten
|
||||
|
||||
- `frontend/src/components/AppHeader.vue` neu strukturieren.
|
||||
- Logo, Produktidentitaet, Systemstatus und optionaler Utility-Bereich klarer anordnen.
|
||||
- Die derzeitige Werbeflaeche kritisch pruefen und nur behalten, wenn sie produktseitig wirklich gewollt ist.
|
||||
- Statusindikatoren moderner, diskreter und semantisch staerker gestalten.
|
||||
|
||||
### 5. Hauptnavigation neu denken
|
||||
|
||||
- `frontend/src/components/AppNavigation.vue` vereinfachen und priorisieren.
|
||||
- Mehrstufige Menues visuell und interaction-seitig robuster aufbauen.
|
||||
- Mobile Navigation als eigenes Muster mit Drawer/Sheet oder kompaktem Menuekonzept planen.
|
||||
- Wichtige Bereiche zuerst sichtbar machen, seltene Admin- oder Tiefenfunktionen entlasten.
|
||||
- Forum, Freunde und Vokabeltrainer-Untermenues gestalterisch besser lesbar machen.
|
||||
|
||||
### 6. Footer und Fensterleiste modernisieren
|
||||
|
||||
- `frontend/src/components/AppFooter.vue` als funktionale Systemleiste neu fassen.
|
||||
- Geoeffnete Dialoge, statische Links und Systemaktionen visuell sauber trennen.
|
||||
- Footer nicht nur als Restflaeche behandeln, sondern in das Gesamtsystem integrieren.
|
||||
|
||||
## Prioritaet 3: Visuelle Produktidentitaet schaerfen
|
||||
|
||||
### 7. Startseite neu positionieren
|
||||
|
||||
- Startseite in zwei Modi denken:
|
||||
- Nicht eingeloggt: klare Landingpage mit Nutzen, Produktbereichen, Vertrauen und Handlungsaufforderungen.
|
||||
- Eingeloggt: dashboard-artiger Einstieg mit hoher Informationsdichte, aber klarer Ordnung.
|
||||
- Die bisherige Startseite braucht eine deutlich staerkere visuelle Hierarchie und bessere Inhaltsblöcke.
|
||||
|
||||
### 8. Einheitliche Oberflaechen fuer Kernbereiche
|
||||
|
||||
- Community-/Social-Bereiche: ruhiger, strukturierter, content-orientierter.
|
||||
- Blogs: lesefreundlicher, mehr Editorial-Charakter.
|
||||
- Vokabeltrainer: lernorientiert, klar, fokussiert.
|
||||
- Falukant: spielweltbezogen, aber nicht altmodisch; eigene Atmosphaere innerhalb des Systems.
|
||||
- Minispiele: kompakter, energischer, aber im selben visuellen Dach.
|
||||
|
||||
### 9. Komponentenbibliothek aufraeumen
|
||||
|
||||
- Buttons, Tabs, Cards, Inputs, Dialogtitel, Listen, Badges und Status-Chips vereinheitlichen.
|
||||
- Bestehende Komponenten auf Dopplungen und Stilbrueche pruefen.
|
||||
- Komponenten nach Rollen statt nach Einzelseiten standardisieren.
|
||||
|
||||
## Prioritaet 4: Responsivitaet und UX-Qualitaet
|
||||
|
||||
### 10. Mobile First nachziehen
|
||||
|
||||
- Brechpunkte und Layout-Verhalten klar festlegen.
|
||||
- Navigation, Dialoge, Tabellen, Formulare und Dashboard-Bloecke fuer kleine Screens neu validieren.
|
||||
- Hover-abhaengige Interaktionen fuer Touch-Nutzung absichern.
|
||||
|
||||
### 11. Bewegungen und visuelles Feedback
|
||||
|
||||
- Subtile, hochwertige Motion fuer Menues, Dialoge, Hover-Stati und Seitenwechsel einbauen.
|
||||
- Keine generischen Effekte; Animationen muessen Orientierung verbessern.
|
||||
|
||||
### 12. Accessibility und Lesbarkeit
|
||||
|
||||
- Tastaturbedienung in Navigation und Dialogen pruefen.
|
||||
- Farbkontraste, Fokus-Ringe und Textgroessen ueberarbeiten.
|
||||
- Inhaltsstruktur mit klaren Headline-Ebenen und besserer Lesefuehrung absichern.
|
||||
|
||||
## Umsetzung in Phasen
|
||||
|
||||
### Phase 1: Audit und visuelle Richtung
|
||||
|
||||
- Bestehende Screens inventarisieren.
|
||||
- Wiederkehrende UI-Muster erfassen.
|
||||
- Zielrichtung fuer Markenbild definieren: warm, modern, eigenstaendig, leicht spielerisch.
|
||||
- Moodboard bzw. 2-3 Stilrouten festlegen.
|
||||
|
||||
### Phase 2: Designsystem und Shell
|
||||
|
||||
- Tokens und globale Styles erstellen.
|
||||
- Header, Navigation, Footer und Content-Layout neu bauen.
|
||||
- Dialog- und Formular-Basis angleichen.
|
||||
|
||||
### Phase 3: Startseite und Kernseiten
|
||||
|
||||
- Home, Blog-Liste, Blog-Detail und ein zentraler Community-Bereich ueberarbeiten.
|
||||
- Danach Vokabeltrainer-Landing/Kernseiten.
|
||||
- Danach Falukant- und Minigame-Einstiege.
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- abgeschlossen
|
||||
- umgesetzt fuer Home, Blogs, zentrale Social-/Community-Flaechen, Vokabeltrainer-Kernseiten, Kalender und zentrale Falukant-Einstiege
|
||||
- Restpunkte in tieferen Vokabel-, Minigame-, Settings- und Admin-Ansichten sind nachgezogen
|
||||
|
||||
### Phase 4: Tiefere Produktbereiche
|
||||
|
||||
- Sekundaere Ansichten und Admin-Bereiche nachziehen.
|
||||
- Visuelle Altlasten in Randbereichen bereinigen.
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- abgeschlossen
|
||||
- zentrale Produktbereiche, tiefere Community-/Vokabel-/Kalender-Ansichten sowie zentrale Falukant-Einstiege und Dialog-Basis sind modernisiert
|
||||
- verbleibende Rand- und Spezialansichten aus Settings, Admin und Minigames sind visuell angeglichen
|
||||
|
||||
### Phase 5: QA und Verfeinerung
|
||||
|
||||
- Responsive Review.
|
||||
- Accessibility Review.
|
||||
- Performance-Pruefung auf unnötige visuelle Last.
|
||||
- Konsistenz-Check ueber das gesamte Produkt.
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- abgeschlossen
|
||||
- globale Bewegungsreduktion, verbesserte Fokusfuehrung und Tastaturzugang in der Hauptnavigation umgesetzt
|
||||
- Build-Chunking verbessert; Haupt-Chunk bereits reduziert
|
||||
- 3D-Runtime in Character3D auf Lazy-Loading umgestellt; verbleibende Warnung betrifft den separaten three-Chunk selbst
|
||||
|
||||
## Empfohlene technische Arbeitspakete
|
||||
|
||||
### Paket A: Design Tokens
|
||||
|
||||
- Neue CSS-Variablenstruktur aufbauen.
|
||||
- Alte Farbwerte und Ad-hoc-Stile schrittweise ersetzen.
|
||||
|
||||
### Paket B: App Shell
|
||||
|
||||
- `AppHeader.vue`
|
||||
- `AppNavigation.vue`
|
||||
- `AppFooter.vue`
|
||||
- `App.vue`
|
||||
- `frontend/src/assets/styles.scss`
|
||||
|
||||
### Paket C: Content-Komponenten
|
||||
|
||||
- Gemeinsame Card-, Section-, Button- und Dialogmuster erstellen oder konsolidieren.
|
||||
- Stark genutzte Widgets und Listen zuerst migrieren.
|
||||
|
||||
### Paket D: Seitenweise Migration
|
||||
|
||||
- Startseite
|
||||
- Blogs
|
||||
- Community
|
||||
- Vokabeltrainer
|
||||
- Falukant
|
||||
- Minispiele
|
||||
|
||||
## Reihenfolge fuer die Umsetzung
|
||||
|
||||
1. Designrichtung und Token-System festlegen.
|
||||
2. App-Shell modernisieren.
|
||||
3. Startseite und oeffentliche Einstiege erneuern.
|
||||
4. Kern-Komponentenbibliothek vereinheitlichen.
|
||||
5. Hauptbereiche seitenweise migrieren.
|
||||
6. Mobile, Accessibility und Feinschliff abschliessen.
|
||||
|
||||
## Risiken
|
||||
|
||||
- Ein rein visuelles Redesign ohne Systembasis fuehrt wieder zu inkonsistenten Einzelpatches.
|
||||
- Navigation und Dialog-Logik sind funktional verflochten; dort braucht es saubere schrittweise Migration.
|
||||
- Falukant und Community haben unterschiedliche Produktcharaktere; die Klammer muss bewusst gestaltet werden.
|
||||
- Zu viele parallele Einzelumbauten wuerden das Styling kurzfristig uneinheitlicher machen.
|
||||
|
||||
## Empfehlung
|
||||
|
||||
Das Redesign sollte nicht als einzelne Seitenkosmetik umgesetzt werden, sondern als kontrollierte Migration mit Designsystem zuerst. Der erste konkrete Umsetzungsschritt sollte daher ein kleines, aber verbindliches UI-Fundament sein: neue Tokens, neue Typografie, neue App-Shell und eine modernisierte Startseite. Danach koennen die restlichen Bereiche kontrolliert nachgezogen werden.
|
||||
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
|
||||
389
docs/USABILITY_AUDIT_U1.md
Normal file
389
docs/USABILITY_AUDIT_U1.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# UX Audit U1
|
||||
|
||||
## Ziel
|
||||
|
||||
Dieser Audit bildet Phase U1 des Bedienbarkeitskonzepts ab. Er dient als priorisierte Arbeitsgrundlage fuer die eigentliche UX-Ueberarbeitung.
|
||||
|
||||
Untersucht wurden:
|
||||
|
||||
- Shell und Navigation
|
||||
- Einstieg ohne Login
|
||||
- Registrierung/Login
|
||||
- Forum
|
||||
- Vokabeltrainer
|
||||
- Falukant
|
||||
- Admin
|
||||
- Match3/Minigames
|
||||
|
||||
## Bewertungslogik
|
||||
|
||||
- `P1`: blockiert Kernnutzung oder fuehrt sehr leicht zu Fehlbedienung
|
||||
- `P2`: verlangsamt oder verkompliziert Kernnutzung merklich
|
||||
- `P3`: Konsistenz-, Lesbarkeits- oder Komfortproblem
|
||||
- `P4`: Feinschliff
|
||||
|
||||
## Gepruefte Hauptaufgaben
|
||||
|
||||
1. Einloggen und Einstieg verstehen
|
||||
2. Registrieren
|
||||
3. Forum finden, Thema erstellen und lesen
|
||||
4. Vokabelsprache finden, anlegen, abonnieren und lernen
|
||||
5. Falukant-Status erfassen und Folgeaktion auswaehlen
|
||||
6. Admin-Nutzer oder Match3-Daten bearbeiten
|
||||
7. Match3 starten, pausieren und Kampagnenstatus verstehen
|
||||
|
||||
## Ergebnisuebersicht
|
||||
|
||||
- P1: 4 Punkte
|
||||
- P2: 11 Punkte
|
||||
- P3: 13 Punkte
|
||||
- P4: 6 Punkte
|
||||
|
||||
## P1-Probleme
|
||||
|
||||
### P1-1: Historische innere Scrollkonzepte in Teilbereichen
|
||||
|
||||
Bereiche:
|
||||
|
||||
- Falukant
|
||||
- Admin
|
||||
- Minigames
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- Mehrere Views arbeiten weiterhin mit eigenen `contenthidden/contentscroll`-Strukturen innerhalb des bereits scrollbaren App-Contents.
|
||||
- Das fuehrt zu falschen Sticky-Bezuegen, abgeschnittenen Bereichen und inkonsistenter Scrolllogik.
|
||||
|
||||
Risiko:
|
||||
|
||||
- Nutzer verlieren Orientierung.
|
||||
- statische Leisten oder Footer/Header verhalten sich unerwartet.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- alle View-internen Scrollcontainer systematisch inventarisieren
|
||||
- entscheiden, welche Bereiche echte lokale Scrollflaechen brauchen und welche komplett auf die Shell-Scrolllogik gehen
|
||||
|
||||
### P1-2: Fehler- und Erfolgsfeedback ist nicht konsistent genug
|
||||
|
||||
Bereiche:
|
||||
|
||||
- Auth
|
||||
- Settings
|
||||
- Admin
|
||||
- Vokabeltrainer
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- Mischung aus `alert`, MessageDialog, ErrorDialog, DialogWidget-internem Feedback und stillen `console.error`-Pfaden.
|
||||
- Beispiel: [RegisterDialog.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/dialogues/auth/RegisterDialog.vue) nutzt fehleranfaellig `errrorDialog` statt eines klar vereinheitlichten Feedbackpfads.
|
||||
|
||||
Risiko:
|
||||
|
||||
- Nutzer verstehen nicht sicher, ob eine Aktion fehlgeschlagen ist oder erfolgreich war.
|
||||
- Fehler werden in einzelnen Flows inkonsistent oder gar nicht sichtbar.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- gemeinsames Feedbacksystem definieren
|
||||
- `success`, `warning`, `error`, `info`, `loading` als einheitliche Muster durchziehen
|
||||
|
||||
### P1-3: Formvalidierung erfolgt oft zu spaet oder zu unsichtbar
|
||||
|
||||
Bereiche:
|
||||
|
||||
- Registrierung
|
||||
- Account-Settings
|
||||
- Admin
|
||||
- Falukant-Formulare
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- viele Formulare validieren erst beim Absenden
|
||||
- Feldfehler sitzen selten direkt am Eingabepunkt
|
||||
- Pflichtlogik ist nicht durchgaengig erkennbar
|
||||
|
||||
Risiko:
|
||||
|
||||
- hohe Reibung beim Ausfuellen
|
||||
- Wiederholschleifen und Frust bei laengeren Formularen
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Validierung naeher ans Feld bringen
|
||||
- Pflichtfelder, Eingabeformat und Fehltext systematisch sichtbar machen
|
||||
|
||||
### P1-4: Komplexe Arbeitsflaechen haben zu wenig gefuehrte Primaeraktionen
|
||||
|
||||
Bereiche:
|
||||
|
||||
- Falukant
|
||||
- Admin Match3
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- Fachflaechen zeigen viele Optionen gleichzeitig, ohne klare Priorisierung.
|
||||
- Beispiel: [MinigamesView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/admin/MinigamesView.vue) ist funktionsreich, aber als Bearbeitungsfluss kaum gefuehrt.
|
||||
|
||||
Risiko:
|
||||
|
||||
- Nutzer verstehen den naechsten sinnvollen Schritt nicht.
|
||||
- Bedienfehler in fachlich dichten Bereichen steigen.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- pro komplexer View den eigentlichen Arbeitsfluss definieren
|
||||
- Primaeraktionen, Sekundaeraktionen und Expertenaktionen sichtbarer trennen
|
||||
|
||||
## P2-Probleme
|
||||
|
||||
### P2-1: Hauptnavigation ist funktional, aber noch nicht ausreichend auf Aufgaben priorisiert
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- Navigation ist technisch konsistenter als vorher, aber die inhaltliche Priorisierung der Menuepunkte ist noch stark historisch gewachsen.
|
||||
- Der Unterschied zwischen Kernzielen und Tiefenfunktionen ist nicht deutlich genug.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Menueaufbau gegen echte Nutzungsszenarien pruefen
|
||||
- seltene Spezialpunkte staerker entlasten
|
||||
|
||||
### P2-2: Login-Einstieg ist reich an Inhalt, aber nicht maximal fokussiert
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- [NoLoginView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/home/NoLoginView.vue) erzaehlt Produkt und Zugang parallel.
|
||||
- Fuer Erstnutzer ist die Seite informativ, aber die eigentliche Primaerhandlung konkurriert mit vielen Begleitinhalten.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Zugangspfad noch klarer vom Story-Bereich trennen
|
||||
- Login, Registrierung und Passwort-Reset als zusammenhaengenden Entscheidungsraum denken
|
||||
|
||||
### P2-3: Registrierung ist bedienbar, aber noch nicht robust genug gefuehrt
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- [RegisterDialog.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/dialogues/auth/RegisterDialog.vue) ist funktional, aber nutzt wenig Hilfetext, kaum Inline-Validierung und schwer lesbare Fehlerpfade.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Feldgruppen strukturieren
|
||||
- Passwortlogik und Sprachwahl klarer erklaeren
|
||||
- Fehler am Feld statt nur global rueckmelden
|
||||
|
||||
### P2-4: Forum hat guten Einstieg, aber zu wenig Orientierung im Schreiben-und-Lesen-Wechsel
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- [ForumView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/social/ForumView.vue) mischt Themenliste und Neuerstellung in einem einfachen Toggle.
|
||||
- Es fehlt eine staerkere Fuehrung, wann man lesen und wann man schreiben soll.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Schreibmodus staerker vom Lesemodus absetzen
|
||||
- Entwurf, Abbruch und Rueckkehr klarer machen
|
||||
|
||||
### P2-5: Vokabeltrainer ist funktional breit, aber als Lernpfad noch nicht klar genug
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- Anlegen, Abonnieren, Kapitel, Kurs, Lektion und Uebung sind einzeln verbessert, bilden aber noch keinen vollkommen klaren End-to-End-Lernpfad.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Informationsarchitektur entlang von:
|
||||
- entdecken
|
||||
- beitreten
|
||||
- lernen
|
||||
- ueben
|
||||
- bearbeiten
|
||||
|
||||
### P2-6: Falukant ist visuell stark, aber in taeglichen Routineablaeufen noch nicht effizient genug
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- Statusleiste und Einstiege sind besser, aber Arbeitsablaeufe ueber mehrere Unterseiten bleiben kognitiv schwer.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Top-5-Routinen definieren
|
||||
- dafuer direkte Folgeaktionen aus Status und Uebersichten anbieten
|
||||
|
||||
### P2-7: Admin-Flaechen haben zu wenig gemeinsame Bedienlogik
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- einzelne Admin-Seiten sind moderner, aber Such-, Editier- und Speichermuster unterscheiden sich weiterhin.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- gemeinsames Admin-Muster fuer Suche, Detailansicht, Editieren und Speichern definieren
|
||||
|
||||
### P2-8: Match3 hat gute Spieloberflaeche, aber Meta-Interaktion ist noch zu verstreut
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- Spielstatus, Levelbeschreibung, Statistiken und Steuerung konkurrieren um Aufmerksamkeit.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- klare Prioritaet:
|
||||
- Spielbrett
|
||||
- aktuelles Ziel
|
||||
- verbleibende Zuege
|
||||
- Meta-Infos nur sekundär
|
||||
|
||||
### P2-9: Rueckwege sind nicht ueberall gleich gut sichtbar
|
||||
|
||||
Bereiche:
|
||||
|
||||
- Vokabel-Unterseiten
|
||||
- Forum-Themen
|
||||
- Falukant-Unterseiten
|
||||
- Admin-Details
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- gemeinsames Rueckwegmuster definieren
|
||||
- nicht nur einzelne Buttons, sondern konsistente Bereichsorientierung
|
||||
|
||||
### P2-10: Leere Zustände sind nicht systematisch genug
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- teilweise vorhanden, aber sehr uneinheitlich in Ton, Handlungsangebot und Sichtbarkeit.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Standard fuer:
|
||||
- keine Daten
|
||||
- keine Treffer
|
||||
- noch nicht gestartet
|
||||
- keine Berechtigung
|
||||
|
||||
### P2-11: Mobile Nutzbarkeit ist verbessert, aber nicht abschließend entlang echter Kernaufgaben geprüft
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- echter Geräte-/Viewport-Durchgang entlang der Kernszenarien
|
||||
|
||||
## P3-Probleme
|
||||
|
||||
### P3-1: Button-Semantik und Farblogik sind noch nicht vollkommen systematisch
|
||||
|
||||
Beobachtung:
|
||||
|
||||
- globale Buttons sind modernisiert, lokale Altvarianten bestehen weiter.
|
||||
|
||||
### P3-2: Teilweise alte Feld- und Listenmuster in Spezialbereichen
|
||||
|
||||
### P3-3: Headline-Hierarchien sind nicht in allen Ansichten gleich klar
|
||||
|
||||
### P3-4: Dialoginhalte wirken je nach Bereich unterschiedlich dicht
|
||||
|
||||
### P3-5: Tabellen haben uneinheitliche Leselogik und Aktionsplatzierung
|
||||
|
||||
### P3-6: In einigen Bereichen ist unklar, welche Aktion primaer und welche optional ist
|
||||
|
||||
### P3-7: Beta-/Systemhinweise koennen in Teilen ruhiger und weniger redundant werden
|
||||
|
||||
### P3-8: Einige Views nutzen sehr lange vertikale Inhaltsflaechen ohne Zwischenanker
|
||||
|
||||
### P3-9: Inline-Hilfen und Tooltips sind in komplexen Bereichen noch unterentwickelt
|
||||
|
||||
### P3-10: Touch-/Hover-Verhalten ist nicht in allen Spezialviews gleich robust
|
||||
|
||||
### P3-11: Message- und Error-Wording ist noch nicht konsistent
|
||||
|
||||
### P3-12: Manche Dialoge und Formulare sind auf Desktop gut, aber auf enger Breite nur ausreichend
|
||||
|
||||
### P3-13: Such- und Filterbereiche koennten staerker standardisiert werden
|
||||
|
||||
## P4-Punkte
|
||||
|
||||
### P4-1: Mikrointeraktionen in Karten, Listen und Toolbars weiter harmonisieren
|
||||
|
||||
### P4-2: Badge- und Statusdarstellungen semantisch weiter schaerfen
|
||||
|
||||
### P4-3: Fokus- und Hover-Zustaende in Spezialkomponenten weiter angleichen
|
||||
|
||||
### P4-4: Editorbereiche visuell und bedienlogisch weiter vereinheitlichen
|
||||
|
||||
### P4-5: Leichte Textkuerzungen fuer schnellere Scanbarkeit in Hero-Bereichen
|
||||
|
||||
### P4-6: Weitere Feinarbeit an Footer/Fensterleiste in langen Nutzungssessions
|
||||
|
||||
## Bereichsspezifische Kurzbewertung
|
||||
|
||||
### Shell und Navigation
|
||||
|
||||
- deutlich verbessert
|
||||
- noch offen: inhaltliche Priorisierung, Rueckwege, Endabnahme kleiner Screens
|
||||
|
||||
### Einstieg ohne Login
|
||||
|
||||
- stark verbessert
|
||||
- noch offen: Zugangspfad fokussierter gegen Story-Inhalt absetzen
|
||||
|
||||
### Registrierung/Login
|
||||
|
||||
- funktional ok
|
||||
- UX-seitig noch zu wenig gefuehrt und rueckmeldearm
|
||||
|
||||
### Forum
|
||||
|
||||
- guter Ueberblick, aber Schreib-/Lesefluss noch nicht ideal getrennt
|
||||
|
||||
### Vokabeltrainer
|
||||
|
||||
- optisch konsistent, aber noch kein vollkommen klarer End-to-End-Lernpfad
|
||||
|
||||
### Falukant
|
||||
|
||||
- visuell stark, fachlich noch der anspruchsvollste Bedienbereich
|
||||
|
||||
### Admin
|
||||
|
||||
- einzelne Ansichten verbessert, gemeinsame Admin-Bedienlogik fehlt noch
|
||||
|
||||
### Minigames
|
||||
|
||||
- Match3 solide, aber Status-/Metaebene kann bedienlogisch weiter fokussiert werden
|
||||
|
||||
## Priorisierte Umsetzung nach U1
|
||||
|
||||
### Paket U1-A
|
||||
|
||||
- Feedbacksystem vereinheitlichen
|
||||
- Formularvalidierung sichtbar machen
|
||||
- Scroll- und Sticky-Logik historischer Sonderfaelle bereinigen
|
||||
|
||||
### Paket U1-B
|
||||
|
||||
- Navigation und Rueckwege nach Aufgaben priorisieren
|
||||
- Leere Zustände und Systemzustände standardisieren
|
||||
|
||||
### Paket U1-C
|
||||
|
||||
- Vokabeltrainer-Lernpfad schärfen
|
||||
- Falukant-Routinen entschlacken
|
||||
- Admin-Bedienmuster vereinheitlichen
|
||||
|
||||
### Paket U1-D
|
||||
|
||||
- Mobile Kernaufgaben-Endabnahme
|
||||
- Konsistenz-Feinschliff auf P3/P4-Niveau
|
||||
|
||||
## Fazit
|
||||
|
||||
Die App ist gestalterisch deutlich weiter als vor dem Redesign, aber bedienlogisch noch nicht auf demselben Reifegrad. Die größten UX-Hebel liegen nicht mehr in Farben oder Layout, sondern in:
|
||||
|
||||
- konsistentem Feedback
|
||||
- klareren Aufgabenflüssen
|
||||
- sichtbarerer Validierung
|
||||
- entschlackten Fachbereichen
|
||||
- sauberer Priorisierung von Aktionen
|
||||
|
||||
Phase U1 ist damit abgeschlossen. Die naechste sinnvolle Arbeitsphase ist `U2: Shell, Navigation und Feedback`.
|
||||
440
docs/USABILITY_CONCEPT.md
Normal file
440
docs/USABILITY_CONCEPT.md
Normal file
@@ -0,0 +1,440 @@
|
||||
# Bedienbarkeitskonzept
|
||||
|
||||
## Ziel
|
||||
|
||||
Die Bedienbarkeit von YourPart soll systematisch verbessert werden, ohne die vorhandene Funktionsbreite oder den neuen UI-Stand wieder aufzubrechen. Der Fokus liegt auf Orientierung, Vorhersagbarkeit, Aufgabenfluss, Fehlertoleranz und effizienter Nutzung auf Desktop und Mobile.
|
||||
|
||||
Das Dokument ist bewusst als Arbeitsgrundlage aufgebaut:
|
||||
|
||||
- Was genau verbessert werden soll
|
||||
- nach welchen Prinzipien entschieden wird
|
||||
- in welcher Reihenfolge gearbeitet wird
|
||||
- woran ein Punkt als erledigt gilt
|
||||
|
||||
## Leitprinzipien
|
||||
|
||||
### 1. Weniger Reibung
|
||||
|
||||
- Haeufige Aufgaben muessen mit moeglichst wenig Entscheidungen und moeglichst wenig Klicks erreichbar sein.
|
||||
- Sekundaere Funktionen duerfen nicht die Kernaufgabe stoeren.
|
||||
|
||||
### 2. Klare Orientierung
|
||||
|
||||
- Nutzer muessen jederzeit erkennen:
|
||||
- wo sie sich befinden
|
||||
- was hier moeglich ist
|
||||
- was der naechste sinnvolle Schritt ist
|
||||
|
||||
### 3. Konsistente Interaktion
|
||||
|
||||
- Gleiche Interaktionsmuster muessen sich in der gesamten App gleich verhalten.
|
||||
- Buttons, Dialoge, Tabs, Listen, Formulare und Menues duerfen nicht je Bereich eigene Bedienlogiken entwickeln.
|
||||
|
||||
### 4. Fehlertoleranz statt Bestrafung
|
||||
|
||||
- Fehlbedienungen muessen auffangbar sein.
|
||||
- Kritische Aktionen brauchen klare Rueckmeldung, wo sinnvoll Bestätigung und wenn moeglich Undo oder sichere Rueckwege.
|
||||
|
||||
### 5. Geschwindigkeit fuer geuebte Nutzer
|
||||
|
||||
- Power-User sollen die App schnell nutzen koennen.
|
||||
- Haeufige Wege duerfen nicht durch uebermaessige Zwischenschritte ausgebremst werden.
|
||||
|
||||
## Nicht-Ziele
|
||||
|
||||
- Keine komplette Informationsarchitektur-Neuerfindung in einem Schritt.
|
||||
- Kein sofortiger Umbau jedes Workflows gleichzeitig.
|
||||
- Keine reine Accessibility-Checklistenarbeit ohne echten Nutzwert.
|
||||
|
||||
## Ausgangsprobleme
|
||||
|
||||
Aus dem aktuellen Projektstand und den bisherigen Umbauten ergeben sich fuer die Bedienbarkeit vor allem diese Problemklassen:
|
||||
|
||||
- Uneinheitliche Bedienmuster zwischen alten und neueren Bereichen
|
||||
- zu viele tiefe Menues und bereichsspezifische Sonderlogiken
|
||||
- einige Seiten mit hoher Funktionsdichte, aber schwacher Priorisierung
|
||||
- Mischformen aus statischen Shell-Bereichen und historischen inneren Scrollkonzepten
|
||||
- lokale Alt-Dialoge und Formmuster mit uneinheitlicher Rueckmeldung
|
||||
- Desktop-zentrierte Denkweise in Teilen von Admin, Minigames und Falukant
|
||||
|
||||
## Zielbild
|
||||
|
||||
Am Ende soll die App folgendes Nutzungsgefuehl bieten:
|
||||
|
||||
- Shell, Navigation und Dialoge verhalten sich vorhersagbar
|
||||
- jede Hauptseite zeigt klar eine Primaeraufgabe und erkennbare Sekundaeraufgaben
|
||||
- Formulare fuehren sauber durch Eingabe, Validierung und Abschluss
|
||||
- Statusaenderungen und Systemreaktionen sind sichtbar und verstaendlich
|
||||
- Falukant, Community, Blog und Lernen bleiben fachlich unterschiedlich, aber bedienbar aus einem Guss
|
||||
|
||||
## Arbeitsbereiche
|
||||
|
||||
### Bereich A: Navigation und Orientierung
|
||||
|
||||
Ziel:
|
||||
|
||||
- Nutzer finden schneller zum Ziel und verstehen Menuehierarchien besser.
|
||||
|
||||
Arbeitspunkte:
|
||||
|
||||
- Hauptnavigation auf tatsaechliche Primaerbereiche pruefen
|
||||
- Untermenues auf Redundanzen und Leerpfade pruefen
|
||||
- aktive Position und Kontext pro Bereich klarer machen
|
||||
- in tiefen Bereichen Rueckwege, Breadcrumb-artige Hinweise oder Bereichstitel konsistent gestalten
|
||||
- Schnellzugriffe nur dort einsetzen, wo sie echte Beschleunigung bringen
|
||||
|
||||
Abnahmekriterien:
|
||||
|
||||
- jeder Hauptmenuepunkt hat einen klaren Zweck
|
||||
- Menuepunkte ohne Untermenue verhalten sich direkt
|
||||
- tiefe Ansichten haben einen klaren Rueckweg
|
||||
- Nutzer muessen nicht raten, in welchem Bereich sie sich befinden
|
||||
|
||||
### Bereich B: Seitenhierarchie und Aufgabenfluss
|
||||
|
||||
Ziel:
|
||||
|
||||
- jede Seite hat einen erkennbaren Einstieg, eine Hauptaufgabe und einen lesbaren Aufbau
|
||||
|
||||
Arbeitspunkte:
|
||||
|
||||
- pro View Primaeraktion definieren
|
||||
- Informationsdichte dort reduzieren, wo gleichrangige Bloecke konkurrieren
|
||||
- visuelle Prioritaet zwischen Lesen, Auswaehlen, Bearbeiten und Absenden schaerfen
|
||||
- Tabellen, Listen und Karten jeweils auf ihren eigentlichen Einsatzzweck pruefen
|
||||
|
||||
Abnahmekriterien:
|
||||
|
||||
- Nutzer erkennen in wenigen Sekunden den Zweck einer Seite
|
||||
- Hauptaktionen sind oberhalb oder in unmittelbarer Naehe des relevanten Inhalts
|
||||
- Nebenfunktionen dominieren die Seite nicht mehr
|
||||
|
||||
### Bereich C: Formulare und Eingaben
|
||||
|
||||
Ziel:
|
||||
|
||||
- Formulare sollen verstaendlich, fehlertolerant und effizient sein
|
||||
|
||||
Arbeitspunkte:
|
||||
|
||||
- Labels, Hilfetexte, Pflichtfelder und Fehlermeldungen vereinheitlichen
|
||||
- Validierung naeher an den Eingabepunkt bringen
|
||||
- unklare Zustandswechsel vermeiden
|
||||
- Speichern, Abbrechen und gefaehrliche Aktionen konsistent platzieren
|
||||
- Checkboxen, Radios und Selects auf Lesbarkeit und Trefferflaechen pruefen
|
||||
|
||||
Abnahmekriterien:
|
||||
|
||||
- jeder Eingabefehler ist erkennbar und nachvollziehbar
|
||||
- Speichern fuehlt sich in allen Bereichen gleich an
|
||||
- keine Form wirkt wie ein historischer Sonderfall
|
||||
|
||||
### Bereich D: Dialoge, Feedback und Systemstatus
|
||||
|
||||
Ziel:
|
||||
|
||||
- Systemreaktionen muessen sichtbar, verstaendlich und nicht stoerend sein
|
||||
|
||||
Arbeitspunkte:
|
||||
|
||||
- Dialogarten unterscheiden:
|
||||
- bestaetigend
|
||||
- informierend
|
||||
- bearbeitend
|
||||
- kritisch
|
||||
- Messagebox/Dialog-Rueckmeldungen vereinheitlichen
|
||||
- Ladezustaende, Erfolg, Fehler und Leere Zustaende standardisieren
|
||||
- offene Dialoge und Fensterleiste auf Priorisierung pruefen
|
||||
|
||||
Abnahmekriterien:
|
||||
|
||||
- Nutzer verstehen, was gerade passiert ist oder noch passiert
|
||||
- kritische Aktionen sind klar markiert
|
||||
- Modale Interaktionen blockieren nur, wenn es wirklich noetig ist
|
||||
|
||||
### Bereich E: Mobile und kleine Viewports
|
||||
|
||||
Ziel:
|
||||
|
||||
- zentrale Aufgaben muessen auch auf kleineren Screens robust funktionieren
|
||||
|
||||
Arbeitspunkte:
|
||||
|
||||
- Shell mit Header, Navigation, Content und Footer auf reale Nutzungsszenarien pruefen
|
||||
- Tabellen und breite Steuermasken auf mobile Alternativen oder horizontale Strategien prüfen
|
||||
- Touch-Ziele und Abstaende angleichen
|
||||
- Hover-abhaengige Muster fuer Touch absichern
|
||||
|
||||
Abnahmekriterien:
|
||||
|
||||
- Kernaufgaben sind auf Tablet und Smartphone ohne Layoutbruch nutzbar
|
||||
- keine kritische Funktion ist nur via Hover oder Pixel-Präzision erreichbar
|
||||
|
||||
### Bereich F: Komplexe Produktbereiche
|
||||
|
||||
Ziel:
|
||||
|
||||
- Falukant, Vokabeltrainer, Minigames und Admin sollen fachlich komplex bleiben, aber leichter steuerbar werden
|
||||
|
||||
Arbeitspunkte:
|
||||
|
||||
- Falukant:
|
||||
- Schnellzugriffsleiste, Tab-Struktur, Statusfeedback und Arbeitsablaeufe priorisieren
|
||||
- Vokabeltrainer:
|
||||
- Lernpfad, Bearbeitung, Suche, Uebung und Fortschritt klarer trennen
|
||||
- Minigames:
|
||||
- Einstieg, Pause, Statusanzeige und Kampagnenfluss vereinfachen
|
||||
- Admin:
|
||||
- Such-, Editier- und Bestaetigungsfluesse entlasten
|
||||
|
||||
Abnahmekriterien:
|
||||
|
||||
- komplexe Bereiche sind ohne Einlernen nicht sofort trivial, aber deutlich besser fuehrend
|
||||
- wiederkehrende Aktionen sind schneller und sicherer bedienbar
|
||||
|
||||
## Methodik
|
||||
|
||||
### 1. Bedienbarkeits-Audit
|
||||
|
||||
Pro Hauptbereich wird ein kurzer Audit gemacht:
|
||||
|
||||
- Primaeraufgabe der Seite
|
||||
- haeufigste Nutzeraktion
|
||||
- groesste Reibung
|
||||
- groesstes Fehlerrisiko
|
||||
- mobile Schwachstelle
|
||||
|
||||
Empfohlene Cluster:
|
||||
|
||||
- Shell und Navigation
|
||||
- Home und Einstieg
|
||||
- Community/Social
|
||||
- Blog
|
||||
- Vokabeltrainer
|
||||
- Falukant
|
||||
- Admin
|
||||
- Minigames
|
||||
- Dialoge/Formulare
|
||||
|
||||
### 2. Aufgabenorientierte Review-Szenarien
|
||||
|
||||
Die App wird nicht nur nach Komponenten, sondern nach Aufgaben geprueft:
|
||||
|
||||
- registrieren und einloggen
|
||||
- Profil/Freunde finden
|
||||
- Forumsthema finden und beantworten
|
||||
- Vokabelsprache erstellen, abonnieren, lernen
|
||||
- Falukant-Status pruefen und Folgeaktion ausfuehren
|
||||
- Admin-Nutzer suchen und aendern
|
||||
- Match3 starten, pausieren, neu starten
|
||||
|
||||
### 3. Fix-Kategorien
|
||||
|
||||
Alle Probleme werden in vier Kategorien eingeordnet:
|
||||
|
||||
- P1: blockiert oder verwirrt Kernnutzung deutlich
|
||||
- P2: verlangsamt Nutzung oder erzeugt Fehlbedienung
|
||||
- P3: stoert Konsistenz oder Lesbarkeit
|
||||
- P4: Feinschliff ohne unmittelbaren Schaden
|
||||
|
||||
## Umsetzungsphasen
|
||||
|
||||
### Phase U1: Audit und Problemkatalog
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- kompakte Liste realer Bedienprobleme pro Hauptbereich
|
||||
- priorisiert nach P1 bis P4
|
||||
|
||||
Arbeit:
|
||||
|
||||
- 1 Durchgang Desktop
|
||||
- 1 Durchgang kleiner Viewport
|
||||
- 1 Durchgang fuer Tastatur-/Dialognutzung
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- abgeschlossen
|
||||
- Audit dokumentiert in `docs/USABILITY_AUDIT_U1.md`
|
||||
- priorisierte Folgephase: `U2 Shell, Navigation und Feedback`
|
||||
|
||||
### Phase U2: Shell, Navigation, Feedback
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- globale Bedienmuster sind konsistent
|
||||
|
||||
Arbeit:
|
||||
|
||||
- Navigation
|
||||
- Rueckwege
|
||||
- Fokuslogik
|
||||
- Dialog-/Feedbacksystem
|
||||
- Lade- und Leerezustaende
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- abgeschlossen
|
||||
- Shell-Kontextbereich mit Bereichstitel und Rueckweg umgesetzt
|
||||
- Navigation um klareren Seitenkontext ergaenzt
|
||||
- zentrales Feedback-API eingefuehrt
|
||||
- Standard-Feedbackdialoge visuell und technisch vereinheitlicht
|
||||
- Kernfluesse aus Auth und Settings auf das neue Feedbackmuster umgestellt
|
||||
|
||||
### Phase U3: Formulare und Abschlussfluesse
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- Eingaben, Speichern, Validierung und Rueckmeldung sind vereinheitlicht
|
||||
|
||||
Arbeit:
|
||||
|
||||
- Auth
|
||||
- Settings
|
||||
- Admin
|
||||
- Falukant-Formulare
|
||||
- Vokabel-Bearbeitungsfluesse
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- abgeschlossen
|
||||
- gemeinsames Formularmuster fuer Hinweise, Fehler und Action-Row eingefuehrt
|
||||
- Dialogbuttons respektieren Disabled-Zustaende
|
||||
- Auth-Dialoge, Account-Settings, zentrale Admin-/Falukant-Formulare und Vokabel-Bearbeitungsfluesse auf sichtbarere Validierung und konsistentere Abschlusslogik umgestellt
|
||||
|
||||
### Phase U4: Komplexe Fachbereiche
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- Falukant, Vokabeltrainer, Minigames und Admin sind auf Nutzbarkeit statt nur Funktion geprüft
|
||||
|
||||
Arbeit:
|
||||
|
||||
- Arbeitsablaeufe entlasten
|
||||
- Primaeraktionen schaerfen
|
||||
- Informationslast reduzieren
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- abgeschlossen
|
||||
- Vokabeltrainer als Aufgabenhub mit getrennten Bereichen fuer eigene und abonnierte Sprachen
|
||||
- Falukant-Uebersicht um Routinekarten, verdichtete Kennzahlen und schnellere Folgeaktionen erweitert
|
||||
- Match3-Spiel um Ziel-/Statusleiste fuer den naechsten sinnvollen Schritt ergaenzt
|
||||
- Match3-Admin um klaren 3-Schritt-Arbeitsfluss, Formzusammenfassung und sicherere Speicherlogik erweitert
|
||||
|
||||
### Phase U5: Mobile und Endabnahme
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- Kernaufgaben sind auf Standard-Viewports belastbar
|
||||
|
||||
Arbeit:
|
||||
|
||||
- letzte Layoutkorrekturen
|
||||
- Touch und Fokus
|
||||
- Abschlussreview entlang echter Nutzerszenarien
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- abgeschlossen
|
||||
- Hauptnavigation auf kleinen Viewports zu einer verlässlich aufklappbaren Mobilnavigation mit Touch-gerechten Zielgroessen umgebaut
|
||||
- Header und Footer auf kleine Breiten mit stabileren Status- und Linkblöcken angepasst
|
||||
- Dialoge fuer kleine Viewports auf sichere Maximalgroessen und mobile Button-Stacks begrenzt
|
||||
- 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
|
||||
|
||||
- Menuepunkte auf Nutzungsprioritaet sortieren
|
||||
- Untermenues auf direkte Zielerreichung pruefen
|
||||
- Bereichskontext pro Seite konsistent machen
|
||||
- globale Ruecksprunglogik definieren
|
||||
|
||||
### 2. Dialog- und Feedbacksystem
|
||||
|
||||
- Dialogtypen definieren und dokumentieren
|
||||
- Standard fuer Erfolg, Fehler, Warnung, Leere, Laden festlegen
|
||||
- Inline-Feedback vor modalem Feedback bevorzugen, wenn kein harter Block noetig ist
|
||||
|
||||
### 3. Formsystem
|
||||
|
||||
- ein gemeinsames Muster fuer Label, Hilfetext, Fehlermeldung, Pflichtfeld
|
||||
- ein gemeinsames Muster fuer Save/Cancel/Delete
|
||||
- ein gemeinsames Muster fuer Tabellenfilter und Suchformulare
|
||||
|
||||
### 4. Bereichsreviews
|
||||
|
||||
- Social/Friends/Search/Forum entlang echter Aufgaben pruefen
|
||||
- Vokabeltrainer entlang des Lernpfads pruefen
|
||||
- Falukant entlang taeglicher Routinen pruefen
|
||||
- Admin entlang Such-/Editier-Routinen pruefen
|
||||
- Minigames entlang Einstieg/Pause/Neustart pruefen
|
||||
|
||||
### 5. Mobile Review
|
||||
|
||||
- Header/Nav/Footer mit realen Hoehen pruefen
|
||||
- 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:
|
||||
|
||||
- fuer alle Hauptbereiche ein Audit stattgefunden hat
|
||||
- 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
|
||||
|
||||
1. Audit ueber Kernaufgaben
|
||||
2. Shell/Navigation/Feedback
|
||||
3. Formulare und Abschlusslogik
|
||||
4. Falukant, Vokabeltrainer, Admin, Minigames
|
||||
5. Mobile Endabnahme
|
||||
6. Vereinfachungsnachlauf ueber Feedback, Scrolllogik und tabellenlastige Restbereiche
|
||||
|
||||
## Naechster konkreter Schritt
|
||||
|
||||
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.
|
||||
@@ -1,4 +1,5 @@
|
||||
VITE_API_BASE_URL=http://localhost:3001
|
||||
VITE_API_BASE_URL=http://127.0.0.1:2020
|
||||
VITE_PUBLIC_BASE_URL=http://localhost:5173
|
||||
VITE_TINYMCE_API_KEY=xjqnfymt2wd5q95onkkwgblzexams6l6naqjs01x72ftzryg
|
||||
VITE_DAEMON_SOCKET=ws://localhost:4551
|
||||
VITE_DAEMON_SOCKET=ws://127.0.0.1:4551
|
||||
VITE_CHAT_WS_URL=ws://127.0.0.1:1235
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
VITE_API_BASE_URL=https://www.your-part.de
|
||||
VITE_PUBLIC_BASE_URL=https://www.your-part.de
|
||||
VITE_TINYMCE_API_KEY=xjqnfymt2wd5q95onkkwgblzexams6l6naqjs01x72ftzryg
|
||||
VITE_DAEMON_SOCKET=wss://www.your-part.de:4551
|
||||
VITE_CHAT_WS_URL=wss://www.your-part.de:1235
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
VITE_API_BASE_URL=https://www.your-part.de
|
||||
VITE_PUBLIC_BASE_URL=https://www.your-part.de
|
||||
VITE_TINYMCE_API_KEY=xjqnfymt2wd5q95onkkwgblzexams6l6naqjs01x72ftzryg
|
||||
VITE_DAEMON_SOCKET=wss://www.your-part.de:4551
|
||||
VITE_CHAT_WS_URL=wss://www.your-part.de:1235
|
||||
VITE_SOCKET_IO_URL=https://www.your-part.de:4443
|
||||
|
||||
|
||||
@@ -8,25 +8,25 @@
|
||||
<meta name="description" content="YourPart vereint Community, Chat, Forum, soziales Netzwerk mit Bildergalerie, Vokabeltrainer, das Aufbauspiel Falukant sowie Minispiele wie Match3 und Taxi. Die Plattform befindet sich in der Beta‑Phase und wird laufend erweitert." />
|
||||
<meta name="keywords" content="YourPart, Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer, Sprachen lernen, Falukant, Aufbauspiel, Minispiele, Match3, Taxi, Bildergalerie, Tagebuch, Freundschaften" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="https://www.your-part.de/" />
|
||||
<link rel="canonical" href="%VITE_PUBLIC_BASE_URL%/" />
|
||||
<meta name="author" content="YourPart" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="YourPart" />
|
||||
<meta property="og:title" content="YourPart – Community, Chat, Forum, Vokabeltrainer, Falukant & Minispiele" />
|
||||
<meta property="og:description" content="Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer zum Sprachen lernen, das Aufbauspiel Falukant sowie Minispiele – jetzt in der Beta testen." />
|
||||
<meta property="og:url" content="https://www.your-part.de/" />
|
||||
<meta property="og:url" content="%VITE_PUBLIC_BASE_URL%/" />
|
||||
<meta property="og:locale" content="de_DE" />
|
||||
<meta property="og:image" content="https://www.your-part.de/images/logos/logo.png" />
|
||||
<meta property="og:image" content="%VITE_PUBLIC_BASE_URL%/images/logos/logo.png" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="YourPart – Community, Chat, Forum, Vokabeltrainer, Falukant & Minispiele" />
|
||||
<meta name="twitter:description" content="Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer zum Sprachen lernen, das Aufbauspiel Falukant sowie Minispiele – jetzt in der Beta testen." />
|
||||
<meta name="twitter:image" content="https://www.your-part.de/images/logos/logo.png" />
|
||||
<meta name="twitter:image" content="%VITE_PUBLIC_BASE_URL%/images/logos/logo.png" />
|
||||
|
||||
<meta name="theme-color" content="#FF8C5A" />
|
||||
<link rel="alternate" hreflang="de" href="https://www.your-part.de/" />
|
||||
<link rel="alternate" hreflang="x-default" href="https://www.your-part.de/" />
|
||||
<link rel="alternate" hreflang="de" href="%VITE_PUBLIC_BASE_URL%/" />
|
||||
<link rel="alternate" hreflang="x-default" href="%VITE_PUBLIC_BASE_URL%/" />
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<AppHeader />
|
||||
<AppNavigation v-if="isLoggedIn && user.active" />
|
||||
<AppContent />
|
||||
<div id="app" class="app-shell">
|
||||
<AppHeader class="app-shell__header" />
|
||||
<AppNavigation v-if="isLoggedIn && user.active" class="app-shell__nav" />
|
||||
<AppContent class="app-shell__content" />
|
||||
<AppFooter />
|
||||
<AnswerContact ref="answerContactDialog" />
|
||||
<RandomChatDialog ref="randomChatDialog" />
|
||||
@@ -71,10 +71,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
.app-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,50 +1,355 @@
|
||||
:root {
|
||||
/* Moderne Farbpalette für bessere Lesbarkeit */
|
||||
--color-primary-orange: #FFB84D; /* Gelbliches, sanftes Orange */
|
||||
--color-primary-orange-hover: #FFC966; /* Noch helleres gelbliches Orange für Hover */
|
||||
--color-primary-orange-light: #FFF8E1; /* Sehr helles gelbliches Orange für Hover-Hintergründe */
|
||||
--color-primary-green: #4ADE80; /* Helles, freundliches Grün - passt zum Orange */
|
||||
--color-primary-green-hover: #6EE7B7; /* Noch helleres Grün für Hover */
|
||||
--color-text-primary: #1F2937; /* Dunkles Grau für Haupttext (bessere Lesbarkeit) */
|
||||
--color-text-secondary: #5D4037; /* Dunkles Braun für sekundären Text/Hover */
|
||||
--color-text-on-orange: #000000; /* Schwarz auf Orange */
|
||||
--color-text-on-green: #000000; /* Schwarz auf Grün */
|
||||
color-scheme: light;
|
||||
|
||||
--font-display: "Trebuchet MS", "Segoe UI", sans-serif;
|
||||
--font-body: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
|
||||
--color-bg: #f4f1ea;
|
||||
--color-bg-elevated: #faf7f1;
|
||||
--color-bg-muted: #f5eee2;
|
||||
--color-surface: rgba(255, 251, 246, 0.94);
|
||||
--color-surface-strong: #fffdfa;
|
||||
--color-surface-accent: #fff4e5;
|
||||
--color-border: rgba(93, 64, 55, 0.12);
|
||||
--color-border-strong: rgba(93, 64, 55, 0.24);
|
||||
|
||||
--color-text-primary: #211910;
|
||||
--color-text-secondary: #5f4b39;
|
||||
--color-text-muted: #7a6857;
|
||||
--color-text-on-accent: #fffaf4;
|
||||
|
||||
--color-primary: #f8a22b;
|
||||
--color-primary-hover: #ea961f;
|
||||
--color-primary-soft: rgba(248, 162, 43, 0.14);
|
||||
--color-secondary: #78c38a;
|
||||
--color-secondary-soft: rgba(120, 195, 138, 0.18);
|
||||
--color-highlight: #ffcf74;
|
||||
|
||||
--color-success: #287d5a;
|
||||
--color-warning: #c9821f;
|
||||
--color-danger: #b13b35;
|
||||
|
||||
--shell-max-width: 1440px;
|
||||
--content-max-width: 1200px;
|
||||
--header-height: 62px;
|
||||
--nav-height: 52px;
|
||||
--footer-height: 46px;
|
||||
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
|
||||
--radius-sm: 5px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-pill: 999px;
|
||||
|
||||
--shadow-soft: 0 12px 30px rgba(47, 29, 14, 0.08);
|
||||
--shadow-medium: 0 20px 50px rgba(47, 29, 14, 0.12);
|
||||
--shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
|
||||
--transition-fast: 140ms ease;
|
||||
--transition-base: 220ms ease;
|
||||
|
||||
--color-primary-orange: var(--color-primary);
|
||||
--color-primary-orange-hover: var(--color-primary-hover);
|
||||
--color-primary-orange-light: #f9ece1;
|
||||
--color-primary-green: #84c6a3;
|
||||
--color-primary-green-hover: #95d1b0;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.85), transparent 30%),
|
||||
linear-gradient(180deg, #f8f2e8 0%, #f3ebdd 100%);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-text-primary);
|
||||
background: transparent;
|
||||
line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
background: var(--color-primary-orange);
|
||||
color: var(--color-text-on-orange);
|
||||
border: 1px solid var(--color-primary-orange);
|
||||
border-radius: 4px;
|
||||
transition: background 0.05s;
|
||||
a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
.button,
|
||||
span.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
min-height: 44px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-primary);
|
||||
color: #2b1f14;
|
||||
box-shadow: 0 6px 14px rgba(248, 162, 43, 0.2);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-fast),
|
||||
background var(--transition-fast),
|
||||
border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--color-primary-orange-light);
|
||||
button:hover,
|
||||
.button:hover,
|
||||
span.button:hover {
|
||||
transform: translateY(-1px);
|
||||
background: var(--color-primary-hover);
|
||||
box-shadow: 0 10px 18px rgba(248, 162, 43, 0.24);
|
||||
}
|
||||
|
||||
button:active,
|
||||
.button:active,
|
||||
span.button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
.button:disabled,
|
||||
span.button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
textarea:focus-visible,
|
||||
a:focus-visible,
|
||||
[role="button"]:focus-visible,
|
||||
[role="menuitem"]:focus-visible,
|
||||
[tabindex]:focus-visible {
|
||||
outline: 3px solid rgba(120, 195, 138, 0.32);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
input:not([type="checkbox"]):not([type="radio"]),
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: var(--color-text-primary);
|
||||
box-shadow: var(--shadow-inset);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast);
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 120px;
|
||||
padding: 14px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input:not([type="checkbox"]):not([type="radio"]):hover,
|
||||
select:hover,
|
||||
textarea:hover {
|
||||
border-color: var(--color-border-strong);
|
||||
}
|
||||
|
||||
input:not([type="checkbox"]):not([type="radio"]):focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: rgba(120, 195, 138, 0.65);
|
||||
box-shadow: 0 0 0 4px rgba(120, 195, 138, 0.16);
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
width: auto;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
inline-size: 16px;
|
||||
block-size: 16px;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
inline-size: 16px;
|
||||
block-size: 16px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0 0 var(--space-3);
|
||||
font-family: var(--font-display);
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2rem, 3.4vw, 3.6rem);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1.5rem, 2vw, 2.4rem);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: clamp(1.15rem, 1.5vw, 1.5rem);
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
margin: 0 0 var(--space-4);
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
main,
|
||||
.contenthidden {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contentscroll {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.app-content__inner > .contenthidden {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.app-content__inner > .contenthidden > .contentscroll {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.surface-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-stack {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-field > label,
|
||||
.form-field > span:first-child {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 0.88rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.form-error {
|
||||
font-size: 0.88rem;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.field-error {
|
||||
border-color: rgba(177, 59, 53, 0.44) !important;
|
||||
box-shadow: 0 0 0 4px rgba(177, 59, 53, 0.12) !important;
|
||||
}
|
||||
|
||||
.form-actions-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.rc-system {
|
||||
@@ -52,25 +357,13 @@ button:hover {
|
||||
}
|
||||
|
||||
.rc-self {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
color: #c0412c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rc-partner {
|
||||
color: #0000ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--color-primary-orange);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0;
|
||||
display: block;
|
||||
color: #2357b5;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight,
|
||||
@@ -80,61 +373,65 @@ h3 {
|
||||
.multiselect__option--highlight[data-selected],
|
||||
.multiselect__option--highlight[data-deselect] {
|
||||
background: none;
|
||||
background-color: var(--color-primary-orange);
|
||||
color: var(--color-text-on-orange);
|
||||
}
|
||||
|
||||
span.button {
|
||||
padding: 2px 2px;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
background: var(--color-primary-orange);
|
||||
color: var(--color-text-on-orange);
|
||||
border: 1px solid var(--color-primary-orange);
|
||||
border-radius: 4px;
|
||||
transition: background 0.05s;
|
||||
border: 1px solid transparent;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
span.button:hover {
|
||||
background: var(--color-primary-orange-light);
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-text-secondary);
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-text-on-accent);
|
||||
}
|
||||
|
||||
.font-color-gender-male {
|
||||
color: #1E90FF;
|
||||
color: #1e90ff;
|
||||
}
|
||||
|
||||
.font-color-gender-female {
|
||||
color: #FF69B4;
|
||||
color: #d14682;
|
||||
}
|
||||
|
||||
.font-color-gender-transmale {
|
||||
color: #00CED1;
|
||||
color: #1f8b9b;
|
||||
}
|
||||
|
||||
.font-color-gender-transfemale {
|
||||
color: #FFB6C1;
|
||||
color: #d78398;
|
||||
}
|
||||
|
||||
.font-color-gender-nonbinary {
|
||||
color: #DAA520;
|
||||
color: #ba7c1f;
|
||||
}
|
||||
|
||||
main,
|
||||
.contenthidden {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
@media (max-width: 960px) {
|
||||
:root {
|
||||
--header-height: 56px;
|
||||
--nav-height: auto;
|
||||
--footer-height: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.8rem, 8vw, 2.8rem);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1.35rem, 5vw, 2rem);
|
||||
}
|
||||
|
||||
.contentscroll table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html:focus-within {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
.contentscroll {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -1,26 +1,59 @@
|
||||
<template>
|
||||
<main class="contenthidden">
|
||||
<div class="contentscroll">
|
||||
<main class="app-content contenthidden">
|
||||
<div class="app-content__scroll contentscroll">
|
||||
<div class="app-content__inner">
|
||||
<AppSectionBar />
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppSectionBar from './AppSectionBar.vue';
|
||||
|
||||
export default {
|
||||
name: 'AppContent'
|
||||
name: 'AppContent',
|
||||
components: {
|
||||
AppSectionBar
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
padding: 0;
|
||||
background-color: #ffffff;
|
||||
.app-content {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contentscroll {
|
||||
padding: 20px;
|
||||
.app-content__scroll {
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.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) {
|
||||
.app-content__inner {
|
||||
padding: 12px 12px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
<template>
|
||||
<footer>
|
||||
<div class="logo" @click="showFalukantDaemonStatus"><img src="/images/icons/logo_color.png"></div>
|
||||
<div class="window-bar">
|
||||
<button v-for="dialog in openDialogs" :key="dialog.dialog.name" class="dialog-button"
|
||||
@click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.localTitle">
|
||||
<footer class="app-footer">
|
||||
<div class="app-footer__inner">
|
||||
<div class="footer-system">
|
||||
<button class="footer-brand" type="button" @click="showFalukantDaemonStatus">
|
||||
<img src="/images/icons/logo_color.png" alt="YourPart" />
|
||||
<span>System</span>
|
||||
</button>
|
||||
<span class="footer-caption">
|
||||
{{ openDialogs.length === 0 ? 'Keine offenen Dialoge' : `${openDialogs.length} Fenster aktiv` }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="window-bar" :class="{ 'window-bar--empty': openDialogs.length === 0 }">
|
||||
<button
|
||||
v-for="dialog in openDialogs"
|
||||
:key="dialog.dialog.name"
|
||||
class="dialog-button"
|
||||
@click="toggleDialogMinimize(dialog.dialog.name)"
|
||||
:title="dialog.dialog.localTitle"
|
||||
>
|
||||
<img v-if="dialog.dialog.icon" :src="'/images/icons/' + dialog.dialog.icon" />
|
||||
<span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.localTitle) :
|
||||
dialog.dialog.localTitle }}</span>
|
||||
</button>
|
||||
<span v-if="openDialogs.length === 0" class="window-bar__empty">System bereit</span>
|
||||
</div>
|
||||
|
||||
<div class="static-block">
|
||||
<a href="#" @click.prevent="openImprintDialog">{{ $t('imprint.button') }}</a>
|
||||
<a href="#" @click.prevent="openDataPrivacyDialog">{{ $t('dataPrivacy.button') }}</a>
|
||||
<a href="#" @click.prevent="openContactDialog">{{ $t('contact.button') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import { showInfo } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AppFooter',
|
||||
@@ -53,28 +72,69 @@ 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.
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
footer {
|
||||
display: flex;
|
||||
background-color: var(--color-primary-green);
|
||||
height: 38px;
|
||||
width: 100%;
|
||||
color: #1F2937; /* Dunkles Grau für besseren Kontrast auf hellem Grün */
|
||||
.app-footer {
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.logo,
|
||||
.window-bar,
|
||||
.static-block {
|
||||
text-align: center;
|
||||
.app-footer__inner {
|
||||
max-width: var(--shell-max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
min-height: 44px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(242, 248, 243, 0.96) 0%, rgba(224, 238, 227, 0.98) 100%);
|
||||
border-top: 1px solid rgba(120, 195, 138, 0.28);
|
||||
box-shadow: 0 -6px 18px rgba(93, 64, 55, 0.06);
|
||||
}
|
||||
|
||||
.footer-system {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
min-height: 32px;
|
||||
padding: 0 10px 0 8px;
|
||||
background: rgba(120, 195, 138, 0.12);
|
||||
border: 1px solid rgba(120, 195, 138, 0.22);
|
||||
color: #24523a;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.footer-brand:hover {
|
||||
background: rgba(120, 195, 138, 0.18);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.footer-brand img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.footer-brand span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.footer-caption {
|
||||
font-size: 0.76rem;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.window-bar {
|
||||
@@ -83,24 +143,39 @@ footer {
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
padding-left: 10px;
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
border: 1px solid rgba(120, 195, 138, 0.16);
|
||||
}
|
||||
|
||||
.window-bar--empty {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.window-bar__empty {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
max-width: 12em;
|
||||
max-width: 15em;
|
||||
min-height: 30px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: none;
|
||||
height: 1.8em;
|
||||
border: 1px solid #0a4337;
|
||||
box-shadow: 1px 1px 2px #484949;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid rgba(120, 195, 138, 0.18);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.dialog-button>img {
|
||||
@@ -111,16 +186,71 @@ footer {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.logo>img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.static-block {
|
||||
line-height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
white-space: nowrap;
|
||||
padding-left: 8px;
|
||||
border-left: 1px solid rgba(120, 195, 138, 0.22);
|
||||
}
|
||||
|
||||
.static-block>a {
|
||||
padding-right: 1.5em;
|
||||
color: #42634e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.static-block > a:hover {
|
||||
color: #24523a;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.app-footer__inner {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-system,
|
||||
.window-bar,
|
||||
.static-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-system {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.static-block {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
border-top: 1px solid rgba(120, 195, 138, 0.2);
|
||||
padding-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-footer__inner {
|
||||
gap: 10px;
|
||||
padding: 8px 10px 10px;
|
||||
}
|
||||
|
||||
.footer-system {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.window-bar {
|
||||
border-radius: var(--radius-md);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.static-block {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,27 @@
|
||||
<template>
|
||||
<header>
|
||||
<div class="logo"><img src="/images/logos/logo.png" /></div>
|
||||
<div class="advertisement">Advertisement</div>
|
||||
<header class="app-header">
|
||||
<div class="app-header__inner">
|
||||
<div class="brand">
|
||||
<div class="logo"><img src="/images/logos/logo.png" alt="YourPart" /></div>
|
||||
<div class="brand-copy">
|
||||
<strong>YourPart</strong>
|
||||
<span>Community-Plattform</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-meta">
|
||||
<div class="header-meta__context">
|
||||
<span class="header-pill">Beta</span>
|
||||
</div>
|
||||
<div class="connection-status" v-if="isLoggedIn">
|
||||
<div class="status-indicator" :class="backendStatusClass">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">B</span>
|
||||
<span class="status-text">Backend</span>
|
||||
</div>
|
||||
<div class="status-indicator" :class="daemonStatusClass">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">D</span>
|
||||
<span class="status-text">Daemon</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -43,43 +55,118 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
.app-header {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
padding: 6px 14px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 249, 240, 0.96) 0%, rgba(246, 236, 220, 0.98) 100%);
|
||||
color: #2b1f14;
|
||||
border-bottom: 1px solid rgba(93, 64, 55, 0.12);
|
||||
box-shadow: 0 5px 14px rgba(93, 64, 55, 0.06);
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
max-width: var(--shell-max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
background-color: #f8a22b;
|
||||
gap: 16px;
|
||||
}
|
||||
.logo, .title, .advertisement {
|
||||
text-align: center;
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.advertisement {
|
||||
flex: 1;
|
||||
|
||||
.logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 5px;
|
||||
border-radius: 12px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(248, 162, 43, 0.18) 0%, rgba(255, 255, 255, 0.76) 100%);
|
||||
border: 1px solid rgba(248, 162, 43, 0.22);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.logo > img {
|
||||
max-height: 50px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-copy strong {
|
||||
font-size: 1rem;
|
||||
line-height: 1.1;
|
||||
color: #3a2a1b;
|
||||
}
|
||||
|
||||
.brand-copy span {
|
||||
font-size: 0.74rem;
|
||||
color: rgba(95, 75, 57, 0.78);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-meta__context {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-pill {
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.14);
|
||||
border: 1px solid rgba(248, 162, 43, 0.24);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #8a5411;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
gap: 5px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 6pt;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
border: 1px solid rgba(93, 64, 55, 0.1);
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@@ -100,23 +187,23 @@ header {
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
color: #2e7d32;
|
||||
background-color: rgba(76, 175, 80, 0.12);
|
||||
color: #245b2c;
|
||||
}
|
||||
|
||||
.status-connecting {
|
||||
background-color: rgba(255, 152, 0, 0.1);
|
||||
color: #f57c00;
|
||||
background-color: rgba(255, 152, 0, 0.12);
|
||||
color: #8b5e0d;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
color: #d32f2f;
|
||||
background-color: rgba(244, 67, 54, 0.12);
|
||||
color: #8f2c27;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
color: #d32f2f;
|
||||
background-color: rgba(244, 67, 54, 0.12);
|
||||
color: #8f2c27;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
@@ -124,4 +211,53 @@ header {
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.app-header {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-meta__context {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.brand-copy span {
|
||||
font-size: 0.76rem;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-header__inner {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.brand {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
min-height: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
<template>
|
||||
<nav>
|
||||
<nav
|
||||
ref="navRoot"
|
||||
class="app-navigation"
|
||||
:class="{ 'app-navigation--suppress-hover': suppressHover }"
|
||||
>
|
||||
<div class="nav-primary">
|
||||
<ul>
|
||||
<!-- Hauptmenü -->
|
||||
<li
|
||||
v-for="(item, key) in menu"
|
||||
:key="key"
|
||||
class="mainmenuitem"
|
||||
@click="handleItem(item, $event)"
|
||||
:class="{ 'mainmenuitem--active': isItemActive(item), 'mainmenuitem--expanded': isMainExpanded(key) }"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:aria-haspopup="hasTopLevelSubmenu(item) ? 'menu' : undefined"
|
||||
:aria-expanded="hasTopLevelSubmenu(item) ? String(isMainExpanded(key)) : undefined"
|
||||
@click="handleItem(item, $event, key)"
|
||||
@keydown.enter.prevent="handleItem(item, $event, key)"
|
||||
@keydown.space.prevent="handleItem(item, $event, key)"
|
||||
>
|
||||
<span
|
||||
v-if="item.icon"
|
||||
:style="`background-image:url('/images/icons/${item.icon}')`"
|
||||
class="menu-icon"
|
||||
> </span>
|
||||
<span>{{ $t(`navigation.${key}`) }}</span>
|
||||
<span class="mainmenuitem__label">{{ $t(`navigation.${key}`) }}</span>
|
||||
<span v-if="hasTopLevelSubmenu(item)" class="mainmenuitem__caret">▾</span>
|
||||
|
||||
<!-- Untermenü Ebene 1 -->
|
||||
<ul v-if="item.children" class="submenu1">
|
||||
<!-- Untermenü Ebene 1 -->
|
||||
<ul v-if="hasTopLevelSubmenu(item)" class="submenu1" :class="{ 'submenu1--open': isMainExpanded(key) }">
|
||||
<li
|
||||
v-for="(subitem, subkey) in item.children"
|
||||
:key="subkey"
|
||||
@click="handleItem(subitem, $event)"
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
:class="{ 'submenu1__item--expanded': isSubExpanded(`${key}:${subkey}`) }"
|
||||
@click="handleSubItem(subitem, subkey, key, $event)"
|
||||
@keydown.enter.prevent="handleSubItem(subitem, subkey, key, $event)"
|
||||
@keydown.space.prevent="handleSubItem(subitem, subkey, key, $event)"
|
||||
>
|
||||
<span
|
||||
v-if="subitem.icon"
|
||||
@@ -29,7 +47,7 @@
|
||||
> </span>
|
||||
<span>{{ subitem?.label || $t(`navigation.m-${key}.${subkey}`) }}</span>
|
||||
<span
|
||||
v-if="subkey === 'forum' || subkey === 'vocabtrainer' || subitem.children"
|
||||
v-if="hasSecondLevelSubmenu(subitem, subkey)"
|
||||
class="subsubmenu"
|
||||
>▶</span>
|
||||
|
||||
@@ -37,11 +55,16 @@
|
||||
<ul
|
||||
v-if="subkey === 'forum' && forumList.length"
|
||||
class="submenu2"
|
||||
:class="{ 'submenu2--open': isSubExpanded(`${key}:${subkey}`) }"
|
||||
>
|
||||
<li
|
||||
v-for="forum in forumList"
|
||||
:key="forum.id"
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
@click="handleItem({ action: 'openForum', params: forum.id }, $event)"
|
||||
@keydown.enter.prevent="handleItem({ action: 'openForum', params: forum.id }, $event)"
|
||||
@keydown.space.prevent="handleItem({ action: 'openForum', params: forum.id }, $event)"
|
||||
>
|
||||
{{ forum.name }}
|
||||
</li>
|
||||
@@ -51,16 +74,25 @@
|
||||
<ul
|
||||
v-else-if="subkey === 'vocabtrainer' && vocabLanguagesList.length"
|
||||
class="submenu2"
|
||||
:class="{ 'submenu2--open': isSubExpanded(`${key}:${subkey}`) }"
|
||||
>
|
||||
<li
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
@click="handleItem({ path: '/socialnetwork/vocab/new' }, $event)"
|
||||
@keydown.enter.prevent="handleItem({ path: '/socialnetwork/vocab/new' }, $event)"
|
||||
@keydown.space.prevent="handleItem({ path: '/socialnetwork/vocab/new' }, $event)"
|
||||
>
|
||||
{{ $t('navigation.m-sprachenlernen.m-vocabtrainer.newLanguage') }}
|
||||
</li>
|
||||
<li
|
||||
v-for="lang in vocabLanguagesList"
|
||||
:key="lang.id"
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
@click="handleItem({ path: `/socialnetwork/vocab/${lang.id}` }, $event)"
|
||||
@keydown.enter.prevent="handleItem({ path: `/socialnetwork/vocab/${lang.id}` }, $event)"
|
||||
@keydown.space.prevent="handleItem({ path: `/socialnetwork/vocab/${lang.id}` }, $event)"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</li>
|
||||
@@ -70,11 +102,16 @@
|
||||
<ul
|
||||
v-else-if="subitem.children"
|
||||
class="submenu2"
|
||||
:class="{ 'submenu2--open': isSubExpanded(`${key}:${subkey}`) }"
|
||||
>
|
||||
<li
|
||||
v-for="(subsubitem, subsubkey) in subitem.children"
|
||||
:key="subsubkey"
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
@click="handleItem(subsubitem, $event)"
|
||||
@keydown.enter.prevent="handleItem(subsubitem, $event)"
|
||||
@keydown.space.prevent="handleItem(subsubitem, $event)"
|
||||
>
|
||||
<span
|
||||
v-if="subsubitem.icon"
|
||||
@@ -91,17 +128,29 @@
|
||||
v-if="item.showLoggedinFriends === 1 && friendsList.length"
|
||||
v-for="friend in friendsList"
|
||||
:key="friend.id"
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
@click="handleItem({ action: 'openChat', params: friend.id }, $event)"
|
||||
@keydown.enter.prevent="handleItem({ action: 'openChat', params: friend.id }, $event)"
|
||||
@keydown.space.prevent="handleItem({ action: 'openChat', params: friend.id }, $event)"
|
||||
>
|
||||
{{ friend.username }}
|
||||
<ul class="submenu2">
|
||||
<li
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
@click="handleItem({ action: 'openChat', params: friend.id }, $event)"
|
||||
@keydown.enter.prevent="handleItem({ action: 'openChat', params: friend.id }, $event)"
|
||||
@keydown.space.prevent="handleItem({ action: 'openChat', params: friend.id }, $event)"
|
||||
>
|
||||
{{ $t('navigation.m-friends.chat') }}
|
||||
</li>
|
||||
<li
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
@click="handleItem({ action: 'openProfile', params: friend.id }, $event)"
|
||||
@keydown.enter.prevent="handleItem({ action: 'openProfile', params: friend.id }, $event)"
|
||||
@keydown.space.prevent="handleItem({ action: 'openProfile', params: friend.id }, $event)"
|
||||
>
|
||||
{{ $t('navigation.m-friends.profile') }}
|
||||
</li>
|
||||
@@ -110,12 +159,13 @@
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="right-block">
|
||||
<span @click="accessMailbox" class="mailbox"></span>
|
||||
<button type="button" @click="accessMailbox" class="mailbox" aria-label="Mailbox"></button>
|
||||
<span class="logoutblock">
|
||||
<span class="username">{{ user.username }}</span>
|
||||
<span @click="logout" class="menuitem">
|
||||
<span class="menuitem" @click="logout">
|
||||
{{ $t('navigation.logout') }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -125,28 +175,23 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { createApp } from 'vue';
|
||||
import apiClient from '@/utils/axios.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';
|
||||
import { EventBus } from '@/utils/eventBus.js';
|
||||
|
||||
export default {
|
||||
name: 'AppNavigation',
|
||||
components: {
|
||||
RandomChatDialog,
|
||||
MultiChatDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
forumList: [],
|
||||
friendsList: [],
|
||||
vocabLanguagesList: []
|
||||
vocabLanguagesList: [],
|
||||
expandedMainKey: null,
|
||||
expandedSubKey: null,
|
||||
pinnedMainKey: null,
|
||||
pinnedSubKey: null,
|
||||
suppressHover: false,
|
||||
hoverReleaseTimer: null,
|
||||
isMobileNav: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -156,6 +201,9 @@ export default {
|
||||
menuNeedsUpdate(newVal) {
|
||||
if (newVal) this.loadMenu();
|
||||
},
|
||||
$route() {
|
||||
this.collapseMenus();
|
||||
},
|
||||
socket(newSocket) {
|
||||
if (newSocket) {
|
||||
newSocket.on('forumschanged', this.fetchForums);
|
||||
@@ -171,6 +219,10 @@ export default {
|
||||
this.fetchFriends();
|
||||
this.fetchVocabLanguages();
|
||||
}
|
||||
this.updateViewportState();
|
||||
window.addEventListener('resize', this.updateViewportState);
|
||||
document.addEventListener('click', this.handleDocumentClick);
|
||||
document.addEventListener('keydown', this.handleDocumentKeydown);
|
||||
},
|
||||
beforeUnmount() {
|
||||
const sock = this.socket;
|
||||
@@ -179,10 +231,131 @@ export default {
|
||||
sock.off('friendloginchanged');
|
||||
sock.off('reloadmenu');
|
||||
}
|
||||
window.removeEventListener('resize', this.updateViewportState);
|
||||
document.removeEventListener('click', this.handleDocumentClick);
|
||||
document.removeEventListener('keydown', this.handleDocumentKeydown);
|
||||
if (this.hoverReleaseTimer) {
|
||||
clearTimeout(this.hoverReleaseTimer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['loadMenu', 'logout']),
|
||||
|
||||
updateViewportState() {
|
||||
this.isMobileNav = window.innerWidth <= 960;
|
||||
if (!this.isMobileNav) {
|
||||
this.expandedMainKey = null;
|
||||
this.expandedSubKey = null;
|
||||
}
|
||||
},
|
||||
|
||||
isMainExpanded(key) {
|
||||
return this.isMobileNav
|
||||
? this.expandedMainKey === key
|
||||
: this.pinnedMainKey === key;
|
||||
},
|
||||
|
||||
isSubExpanded(key) {
|
||||
return this.isMobileNav
|
||||
? this.expandedSubKey === key
|
||||
: this.pinnedSubKey === key;
|
||||
},
|
||||
|
||||
toggleMain(key) {
|
||||
this.expandedMainKey = this.expandedMainKey === key ? null : key;
|
||||
this.expandedSubKey = null;
|
||||
},
|
||||
|
||||
toggleSub(key) {
|
||||
this.expandedSubKey = this.expandedSubKey === key ? null : key;
|
||||
},
|
||||
|
||||
togglePinnedMain(key) {
|
||||
this.pinnedMainKey = this.pinnedMainKey === key ? null : key;
|
||||
this.pinnedSubKey = null;
|
||||
},
|
||||
|
||||
togglePinnedSub(key) {
|
||||
this.pinnedSubKey = this.pinnedSubKey === key ? null : key;
|
||||
},
|
||||
|
||||
collapseMenus(options = {}) {
|
||||
const { blurActiveElement = true } = options;
|
||||
this.expandedMainKey = null;
|
||||
this.expandedSubKey = null;
|
||||
this.pinnedMainKey = null;
|
||||
this.pinnedSubKey = null;
|
||||
this.suppressHover = true;
|
||||
if (this.hoverReleaseTimer) {
|
||||
clearTimeout(this.hoverReleaseTimer);
|
||||
}
|
||||
this.hoverReleaseTimer = window.setTimeout(() => {
|
||||
this.suppressHover = false;
|
||||
this.hoverReleaseTimer = null;
|
||||
}, 180);
|
||||
if (blurActiveElement) {
|
||||
this.$nextTick(() => {
|
||||
if (document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleDocumentClick(event) {
|
||||
const root = this.$refs.navRoot;
|
||||
if (!root || root.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
this.collapseMenus({ blurActiveElement: false });
|
||||
},
|
||||
|
||||
handleDocumentKeydown(event) {
|
||||
if (event.key === 'Escape') {
|
||||
this.collapseMenus();
|
||||
}
|
||||
},
|
||||
|
||||
hasChildren(item) {
|
||||
if (!item?.children) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(item.children)) {
|
||||
return item.children.length > 0;
|
||||
}
|
||||
|
||||
return Object.keys(item.children).length > 0;
|
||||
},
|
||||
|
||||
hasTopLevelSubmenu(item) {
|
||||
return this.hasChildren(item) || (item?.showLoggedinFriends === 1 && this.friendsList.length > 0);
|
||||
},
|
||||
|
||||
hasSecondLevelSubmenu(subitem, subkey) {
|
||||
if (subkey === 'forum') {
|
||||
return this.forumList.length > 0;
|
||||
}
|
||||
|
||||
if (subkey === 'vocabtrainer') {
|
||||
return this.vocabLanguagesList.length > 0;
|
||||
}
|
||||
|
||||
return this.hasChildren(subitem);
|
||||
},
|
||||
|
||||
isItemActive(item) {
|
||||
if (!item?.path || !this.$route?.path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.path === '/') {
|
||||
return this.$route.path === '/';
|
||||
}
|
||||
|
||||
return this.$route.path === item.path || this.$route.path.startsWith(`${item.path}/`);
|
||||
},
|
||||
|
||||
openMultiChat() {
|
||||
// Räume können später dynamisch geladen werden, hier als Platzhalter ein Beispiel:
|
||||
const exampleRooms = [
|
||||
@@ -199,6 +372,21 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
accessMailbox() {
|
||||
const openMessages = () => {
|
||||
EventBus.emit('open-falukant-messages');
|
||||
};
|
||||
|
||||
if (this.$route?.path?.startsWith('/falukant')) {
|
||||
openMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push({ name: 'FalukantOverview' }).then(() => {
|
||||
window.setTimeout(openMessages, 150);
|
||||
});
|
||||
},
|
||||
|
||||
async fetchForums() {
|
||||
try {
|
||||
const res = await apiClient.get('/api/forum');
|
||||
@@ -237,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);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -250,11 +449,19 @@ export default {
|
||||
* 3) Bei `action`: custom action aufrufen
|
||||
* 4) Sonst: normale Router-Navigation
|
||||
*/
|
||||
handleItem(item, event) {
|
||||
handleItem(item, event, key = null) {
|
||||
event.stopPropagation();
|
||||
|
||||
// 1) nur aufklappen, wenn es echte Untermenüs gibt (nicht bei leerem children wie bei Startseite)
|
||||
if (item.children && Object.keys(item.children).length > 0) return;
|
||||
if (key && this.hasTopLevelSubmenu(item)) {
|
||||
if (this.isMobileNav) {
|
||||
this.toggleMain(key);
|
||||
} else {
|
||||
this.togglePinnedMain(key);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasChildren(item)) return;
|
||||
|
||||
// 2) view → Dialog/Window
|
||||
if (item.view) {
|
||||
@@ -271,18 +478,38 @@ export default {
|
||||
} else {
|
||||
console.error(`Dialog '${item.class}' gefunden, aber keine open()-Methode verfügbar.`);
|
||||
}
|
||||
this.collapseMenus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) custom action (openForum, openChat, ...)
|
||||
if (item.action && typeof this[item.action] === 'function') {
|
||||
return this[item.action](item.params, event);
|
||||
this[item.action](item.params, event);
|
||||
this.collapseMenus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) Standard‑Navigation
|
||||
if (item.path) {
|
||||
this.$router.push(item.path);
|
||||
this.collapseMenus();
|
||||
}
|
||||
},
|
||||
|
||||
handleSubItem(item, subkey, parentKey, event) {
|
||||
event.stopPropagation();
|
||||
const compoundKey = `${parentKey}:${subkey}`;
|
||||
|
||||
if (this.hasSecondLevelSubmenu(item, subkey)) {
|
||||
if (this.isMobileNav) {
|
||||
this.toggleSub(compoundKey);
|
||||
} else {
|
||||
this.togglePinnedSub(compoundKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleItem(item, event);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -291,42 +518,105 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/styles.scss';
|
||||
|
||||
nav,
|
||||
nav > ul {
|
||||
.app-navigation,
|
||||
.nav-primary > ul {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #f8a22b;
|
||||
color: #000;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.app-navigation {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 12px;
|
||||
flex-wrap: wrap;
|
||||
border-radius: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(249, 236, 225, 0.98) 0%, rgba(246, 228, 212, 0.98) 100%);
|
||||
border-top: 1px solid rgba(93, 64, 55, 0.08);
|
||||
border-bottom: 1px solid rgba(93, 64, 55, 0.12);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.46);
|
||||
color: var(--color-text-primary);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.nav-primary {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-primary > ul {
|
||||
min-width: 0;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav > ul > li {
|
||||
padding: 0 1em;
|
||||
line-height: 2.5em;
|
||||
transition: background-color 0.25s;
|
||||
.mainmenuitem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
border-radius: 999px;
|
||||
border: 1px solid transparent;
|
||||
transition: background-color 0.25s, color 0.25s, transform 0.2s, border-color 0.25s, box-shadow 0.25s;
|
||||
}
|
||||
|
||||
nav > ul > li:hover {
|
||||
background-color: #f8a22b;
|
||||
.mainmenuitem:focus-visible,
|
||||
.submenu1 > li:focus-visible,
|
||||
.submenu2 > li:focus-visible,
|
||||
.mailbox:focus-visible,
|
||||
.menuitem:focus-visible {
|
||||
outline: 3px solid rgba(120, 195, 138, 0.34);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.mainmenuitem:hover {
|
||||
background-color: rgba(248, 162, 43, 0.16);
|
||||
border-color: rgba(248, 162, 43, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mainmenuitem:hover > span {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.mainmenuitem--expanded {
|
||||
background-color: rgba(248, 162, 43, 0.16);
|
||||
border-color: rgba(248, 162, 43, 0.2);
|
||||
}
|
||||
|
||||
.mainmenuitem--active {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-color: rgba(248, 162, 43, 0.22);
|
||||
box-shadow: 0 6px 14px rgba(93, 64, 55, 0.05);
|
||||
}
|
||||
|
||||
.mainmenuitem__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
nav > ul > li:hover > span {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
nav > ul > li:hover > ul {
|
||||
display: inline-block;
|
||||
.mainmenuitem__caret {
|
||||
margin-left: 6px;
|
||||
font-size: 0.7rem;
|
||||
color: rgba(95, 75, 57, 0.7);
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -335,17 +625,29 @@ a {
|
||||
|
||||
.right-block {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-left: 10px;
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
border-left: 1px solid rgba(93, 64, 55, 0.12);
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(249, 236, 225, 0.98) 0%, rgba(246, 228, 212, 0.98) 100%);
|
||||
}
|
||||
|
||||
.logoutblock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.menuitem {
|
||||
cursor: pointer;
|
||||
color: #5D4037;
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mailbox {
|
||||
@@ -353,20 +655,29 @@ a {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
padding-left: 24px;
|
||||
text-align: left;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 999px;
|
||||
background-color: rgba(120, 195, 138, 0.12);
|
||||
border: 1px solid rgba(93, 64, 55, 0.1);
|
||||
box-shadow: none;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mainmenuitem {
|
||||
position: relative;
|
||||
}
|
||||
.mainmenuitem { position: relative; font-weight: 700; }
|
||||
|
||||
.submenu1 {
|
||||
position: absolute;
|
||||
border: 1px solid #5D4037;
|
||||
background-color: #f8a22b;
|
||||
display: block;
|
||||
border: 1px solid rgba(93, 64, 55, 0.12);
|
||||
background: rgba(255, 252, 247, 0.99);
|
||||
left: 0;
|
||||
top: 2.5em;
|
||||
top: calc(100% + 10px);
|
||||
min-width: 240px;
|
||||
padding: 10px;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 18px 30px rgba(93, 64, 55, 0.14);
|
||||
max-height: 0;
|
||||
overflow: visible;
|
||||
opacity: 0;
|
||||
@@ -385,16 +696,27 @@ a {
|
||||
visibility 0s;
|
||||
}
|
||||
|
||||
.mainmenuitem--expanded .submenu1 {
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: max-height 0.25s ease-in-out,
|
||||
opacity 0.05s ease-in-out,
|
||||
visibility 0s;
|
||||
}
|
||||
|
||||
.submenu1 > li {
|
||||
padding: 0.5em;
|
||||
line-height: 1em;
|
||||
color: #5D4037;
|
||||
display: block;
|
||||
padding: 0.75em 0.9em;
|
||||
line-height: 1.1em;
|
||||
color: var(--color-text-secondary);
|
||||
position: relative;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.submenu1 > li:hover {
|
||||
color: #000;
|
||||
background-color: #f8a22b;
|
||||
color: var(--color-text-primary);
|
||||
background-color: rgba(248, 162, 43, 0.12);
|
||||
}
|
||||
|
||||
.menu-icon,
|
||||
@@ -407,7 +729,7 @@ a {
|
||||
.menu-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 3px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.submenu-icon {
|
||||
@@ -419,10 +741,15 @@ a {
|
||||
|
||||
.submenu2 {
|
||||
position: absolute;
|
||||
background-color: #f8a22b;
|
||||
left: 100%;
|
||||
display: block;
|
||||
background: rgba(255, 252, 247, 0.98);
|
||||
left: calc(100% + 8px);
|
||||
top: 0;
|
||||
border: 1px solid #5D4037;
|
||||
min-width: 230px;
|
||||
padding: 8px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid rgba(71, 52, 35, 0.12);
|
||||
box-shadow: 0 14px 24px rgba(93, 64, 55, 0.12);
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
@@ -441,15 +768,43 @@ a {
|
||||
visibility 0s;
|
||||
}
|
||||
|
||||
.submenu1__item--expanded .submenu2 {
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: max-height 0.25s ease-in-out,
|
||||
opacity 0.05s ease-in-out,
|
||||
visibility 0s;
|
||||
}
|
||||
|
||||
.app-navigation--suppress-hover .mainmenuitem:hover .submenu1,
|
||||
.app-navigation--suppress-hover .submenu1 > li:hover .submenu2 {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.submenu1__item--expanded {
|
||||
color: var(--color-text-primary);
|
||||
background-color: rgba(248, 162, 43, 0.08);
|
||||
}
|
||||
|
||||
.submenu2 > li {
|
||||
padding: 0.5em;
|
||||
padding: 0.75em 0.9em;
|
||||
line-height: 1em;
|
||||
color: #5D4037;
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.submenu2 > li:hover {
|
||||
color: #000;
|
||||
background-color: #f8a22b;
|
||||
color: var(--color-text-primary);
|
||||
background-color: rgba(120, 195, 138, 0.14);
|
||||
}
|
||||
|
||||
.submenu1 > li:focus-visible,
|
||||
.submenu2 > li:focus-visible {
|
||||
color: var(--color-text-primary);
|
||||
background-color: rgba(248, 162, 43, 0.12);
|
||||
}
|
||||
|
||||
.subsubmenu {
|
||||
@@ -457,4 +812,103 @@ a {
|
||||
font-size: 8pt;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 800;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.app-navigation {
|
||||
margin: 0;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
padding: 8px 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.nav-primary,
|
||||
.nav-primary > ul,
|
||||
.right-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-primary {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.nav-primary > ul {
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.right-block {
|
||||
justify-content: space-between;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
border-left: 0;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid rgba(93, 64, 55, 0.1);
|
||||
}
|
||||
|
||||
.logoutblock {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.mainmenuitem {
|
||||
min-height: 42px;
|
||||
width: calc(50% - 4px);
|
||||
justify-content: flex-start;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.submenu1,
|
||||
.submenu2 {
|
||||
position: static;
|
||||
min-width: 100%;
|
||||
margin-top: 8px;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.submenu1--open,
|
||||
.submenu2--open {
|
||||
max-height: 1200px;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.submenu1 > li,
|
||||
.submenu2 > li {
|
||||
min-height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mailbox {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.mainmenuitem {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right-block {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logoutblock {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
206
frontend/src/components/AppSectionBar.vue
Normal file
206
frontend/src/components/AppSectionBar.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<section v-if="isVisible" class="app-section-bar surface-card">
|
||||
<div class="app-section-bar__copy">
|
||||
<span class="app-section-bar__eyebrow">{{ sectionLabel }}</span>
|
||||
<h1 class="app-section-bar__title">{{ pageTitle }}</h1>
|
||||
</div>
|
||||
<button
|
||||
v-if="backTarget"
|
||||
type="button"
|
||||
class="app-section-bar__back"
|
||||
@click="navigateBack"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const SECTION_LABELS = [
|
||||
{ test: (path) => path.startsWith('/falukant'), label: 'Falukant' },
|
||||
{ test: (path) => path.startsWith('/socialnetwork/vocab'), label: 'Vokabeltrainer' },
|
||||
{ test: (path) => path.startsWith('/socialnetwork/forum'), label: 'Forum' },
|
||||
{ test: (path) => path.startsWith('/socialnetwork'), label: 'Community' },
|
||||
{ test: (path) => path.startsWith('/friends'), label: 'Community' },
|
||||
{ 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: 'Persönlich' },
|
||||
{ test: (path) => path.startsWith('/blogs'), label: 'Blog' }
|
||||
];
|
||||
|
||||
const TITLE_MAP = {
|
||||
Friends: 'Freunde',
|
||||
Guestbook: 'Gästebuch',
|
||||
'Search users': 'Suche',
|
||||
Gallery: 'Galerie',
|
||||
Forum: 'Forum',
|
||||
ForumTopic: 'Thema',
|
||||
Diary: 'Tagebuch',
|
||||
VocabTrainer: 'Sprachen',
|
||||
VocabNewLanguage: 'Neue Sprache',
|
||||
VocabSubscribe: 'Sprache abonnieren',
|
||||
VocabLanguage: 'Sprache',
|
||||
VocabChapter: 'Kapitel',
|
||||
VocabCourses: 'Kurse',
|
||||
VocabCourse: 'Kurs',
|
||||
VocabLesson: 'Lektion',
|
||||
FalukantCreate: 'Charakter erstellen',
|
||||
FalukantOverview: 'Übersicht',
|
||||
BranchView: 'Niederlassung',
|
||||
MoneyHistoryView: 'Geldverlauf',
|
||||
FalukantFamily: 'Familie',
|
||||
HouseView: 'Haus',
|
||||
NobilityView: 'Adel',
|
||||
ReputationView: 'Ansehen',
|
||||
ChurchView: 'Kirche',
|
||||
EducationView: 'Bildung',
|
||||
BankView: 'Bank',
|
||||
DirectorView: 'Direktoren',
|
||||
HealthView: 'Gesundheit',
|
||||
PoliticsView: 'Politik',
|
||||
UndergroundView: 'Untergrund',
|
||||
'Personal settings': 'Persönliche Daten',
|
||||
'View settings': 'Ansicht',
|
||||
'Sexuality settings': 'Sexualität',
|
||||
'Flirt settings': 'Flirt',
|
||||
'Account settings': 'Account',
|
||||
Interests: 'Interessen',
|
||||
AdminInterests: 'Interessenverwaltung',
|
||||
AdminUsers: 'Benutzer',
|
||||
AdminUserStatistics: 'Benutzerstatistik',
|
||||
AdminContacts: 'Kontaktanfragen',
|
||||
AdminUserRights: 'Rechte',
|
||||
AdminForums: 'Forumverwaltung',
|
||||
AdminChatRooms: 'Chaträume',
|
||||
AdminFalukantEditUserView: 'Falukant-Nutzer',
|
||||
AdminFalukantMapRegionsView: 'Falukant-Karte',
|
||||
AdminFalukantCreateNPCView: 'NPC erstellen',
|
||||
AdminMinigames: 'Match3-Verwaltung',
|
||||
AdminTaxiTools: 'Taxi-Tools',
|
||||
AdminServicesStatus: 'Service-Status'
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'AppSectionBar',
|
||||
computed: {
|
||||
routePath() {
|
||||
return this.$route?.path || '';
|
||||
},
|
||||
isVisible() {
|
||||
return Boolean(this.$route?.meta?.requiresAuth) && this.routePath !== '/';
|
||||
},
|
||||
sectionLabel() {
|
||||
const found = SECTION_LABELS.find((entry) => entry.test(this.routePath));
|
||||
return found?.label || 'Bereich';
|
||||
},
|
||||
pageTitle() {
|
||||
return TITLE_MAP[this.$route?.name] || this.sectionLabel;
|
||||
},
|
||||
backTarget() {
|
||||
const params = this.$route?.params || {};
|
||||
|
||||
if (this.routePath.startsWith('/socialnetwork/vocab/courses/') && params.lessonId && params.courseId) {
|
||||
return `/socialnetwork/vocab/courses/${params.courseId}`;
|
||||
}
|
||||
|
||||
if (this.routePath.startsWith('/socialnetwork/vocab/') && params.chapterId && params.languageId) {
|
||||
return `/socialnetwork/vocab/${params.languageId}`;
|
||||
}
|
||||
|
||||
if (this.routePath.startsWith('/socialnetwork/vocab/new') || this.routePath.startsWith('/socialnetwork/vocab/subscribe')) {
|
||||
return '/socialnetwork/vocab';
|
||||
}
|
||||
|
||||
if (this.routePath.startsWith('/socialnetwork/vocab/courses/') && params.courseId) {
|
||||
return '/socialnetwork/vocab/courses';
|
||||
}
|
||||
|
||||
if (this.routePath.startsWith('/admin/users/statistics')) {
|
||||
return '/admin/users';
|
||||
}
|
||||
|
||||
if (this.routePath.startsWith('/falukant/') && this.routePath !== '/falukant/home') {
|
||||
return '/falukant/home';
|
||||
}
|
||||
|
||||
if (this.routePath.startsWith('/settings/') && this.routePath !== '/settings/personal') {
|
||||
return '/settings/personal';
|
||||
}
|
||||
|
||||
if (this.routePath.startsWith('/admin/') && this.routePath !== '/admin/users') {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-section-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px 18px;
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.98), rgba(248, 240, 231, 0.94));
|
||||
}
|
||||
|
||||
.app-section-bar__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-section-bar__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-secondary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.app-section-bar__title {
|
||||
margin: 0;
|
||||
font-size: clamp(1.15rem, 1.6vw, 1.6rem);
|
||||
}
|
||||
|
||||
.app-section-bar__back {
|
||||
flex: 0 0 auto;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.app-section-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.app-section-bar__back {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +1,48 @@
|
||||
<template>
|
||||
<div ref="container" class="character-3d-container"></div>
|
||||
<div class="character-3d-shell">
|
||||
<div v-show="!showFallback" ref="container" class="character-3d-container"></div>
|
||||
<img
|
||||
v-if="showFallback"
|
||||
class="character-fallback"
|
||||
:src="fallbackImageSrc"
|
||||
:alt="`Character ${actualGender}`"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { markRaw } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
||||
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 = 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',
|
||||
@@ -28,6 +60,10 @@ export default {
|
||||
noBackground: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
lightweight: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -40,8 +76,12 @@ export default {
|
||||
model: null,
|
||||
animationId: null,
|
||||
mixer: null,
|
||||
clock: markRaw(new THREE.Clock()),
|
||||
baseYPosition: 0 // Basis-Y-Position für Animation
|
||||
clock: null,
|
||||
baseYPosition: 0,
|
||||
showFallback: false,
|
||||
threeRuntime: null,
|
||||
threeLoaders: null,
|
||||
threeModelRuntime: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -93,65 +133,97 @@ export default {
|
||||
const base = getApiBaseURL();
|
||||
const prefix = base ? `${base}${MODELS_API_PATH}` : MODELS_API_PATH;
|
||||
return `${prefix}/${this.actualGender}_${age}y.glb`;
|
||||
},
|
||||
fallbackImageSrc() {
|
||||
return this.actualGender === 'female'
|
||||
? '/images/mascot/mascot_female.png'
|
||||
: '/images/mascot/mascot_male.png';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
actualGender() {
|
||||
this.loadModel();
|
||||
async actualGender() {
|
||||
await this.loadModel();
|
||||
},
|
||||
ageGroup() {
|
||||
this.loadModel();
|
||||
async ageGroup() {
|
||||
await this.loadModel();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init3D();
|
||||
this.loadModel();
|
||||
async mounted() {
|
||||
await this.init3D();
|
||||
await this.loadModel();
|
||||
this.animate();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.cleanup();
|
||||
},
|
||||
methods: {
|
||||
init3D() {
|
||||
async ensureThreeRuntime() {
|
||||
if (!this.threeRuntime) {
|
||||
this.threeRuntime = markRaw(await loadThreeRuntime());
|
||||
}
|
||||
|
||||
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 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.loadBackground();
|
||||
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);
|
||||
|
||||
@@ -159,13 +231,14 @@ export default {
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
},
|
||||
|
||||
loadBackground() {
|
||||
async loadBackground() {
|
||||
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) => {
|
||||
@@ -179,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);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -187,6 +260,8 @@ export default {
|
||||
|
||||
async loadModel() {
|
||||
if (!this.scene) return;
|
||||
const modelRuntime = await this.ensureThreeModelRuntime();
|
||||
const loaders = await this.ensureThreeLoaders();
|
||||
|
||||
// Altes Modell entfernen
|
||||
if (this.model) {
|
||||
@@ -210,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();
|
||||
@@ -251,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;
|
||||
@@ -276,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
|
||||
@@ -289,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();
|
||||
});
|
||||
@@ -301,12 +382,17 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading 3D model:', error);
|
||||
this.showFallback = true;
|
||||
}
|
||||
},
|
||||
|
||||
animate() {
|
||||
this.animationId = requestAnimationFrame(this.animate);
|
||||
|
||||
if (!this.clock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = this.clock.getDelta();
|
||||
|
||||
// Animation-Mixer aktualisieren
|
||||
@@ -375,10 +461,25 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.character-3d-shell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.character-3d-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.character-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center bottom;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<button v-for="button in buttons" :key="button.text" @click="buttonClick(button.action)" class="dialog-button">
|
||||
<button v-for="button in buttons" :key="button.text" @click="buttonClick(button.action)" class="dialog-button" :disabled="button.disabled">
|
||||
{{ isTitleTranslated ? $t(button.text) : button.text }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -142,6 +142,9 @@ export default {
|
||||
return this.minimized;
|
||||
},
|
||||
startDragging(event) {
|
||||
if (window.innerWidth <= 760) {
|
||||
return;
|
||||
}
|
||||
this.isDragging = true;
|
||||
const dialog = this.$refs.dialog;
|
||||
this.dragOffsetX = event.clientX - dialog.offsetLeft;
|
||||
@@ -186,7 +189,8 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(24, 18, 11, 0.44);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.dialog-overlay.non-modal {
|
||||
@@ -195,14 +199,17 @@ export default {
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 252, 247, 0.98) 0%, rgba(249, 242, 232, 0.98) 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-medium);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid rgba(93, 64, 55, 0.12);
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog.minimized {
|
||||
@@ -214,64 +221,112 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 5px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background-color: var(--color-primary-orange);
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(93, 64, 55, 0.1);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(248, 162, 43, 0.16) 0%, rgba(255, 255, 255, 0.56) 100%);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.dialog-icon {
|
||||
padding: 2px 5px 0 0;
|
||||
padding: 2px 6px 0 0;
|
||||
}
|
||||
|
||||
.dialog-icon img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
flex-grow: 1;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 800;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dialog-close,
|
||||
.dialog-minimize {
|
||||
cursor: pointer;
|
||||
font-size: 1.5em;
|
||||
margin-left: 10px;
|
||||
font-size: 1.1rem;
|
||||
margin-left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
color: var(--color-text-secondary);
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.dialog-close:hover,
|
||||
.dialog-minimize:hover {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
padding: 18px 20px;
|
||||
overflow-y: auto;
|
||||
display: var(--dialog-display);
|
||||
color: var(--color-text-primary);
|
||||
&[style*="--dialog-display: flex"] {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
dialog-footer {
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
gap: 10px;
|
||||
padding: 14px 20px 18px;
|
||||
border-top: 1px solid rgba(93, 64, 55, 0.08);
|
||||
background: rgba(255, 255, 255, 0.46);
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
margin-left: 10px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
background: var(--color-primary-orange);
|
||||
color: #000000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
transition: background 0.02s;
|
||||
margin-left: 0;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
background: #FFF4F0;
|
||||
color: #5D4037;
|
||||
border: 1px solid #5D4037;
|
||||
color: #2b1f14;
|
||||
}
|
||||
|
||||
.is-active {
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.dialog {
|
||||
width: calc(100vw - 16px) !important;
|
||||
max-width: calc(100vw - 16px);
|
||||
height: auto !important;
|
||||
max-height: calc(100dvh - 16px);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
cursor: default;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -105,7 +105,8 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(24, 18, 11, 0.44);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.dialog-overlay.non-modal {
|
||||
@@ -114,12 +115,14 @@ export default {
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.98) 0%, rgba(249, 242, 232, 0.98) 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-medium);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid rgba(93, 64, 55, 0.12);
|
||||
pointer-events: all;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog.minimized {
|
||||
@@ -131,9 +134,9 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background-color: var(--color-primary-orange);
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(93, 64, 55, 0.1);
|
||||
background: linear-gradient(180deg, rgba(248, 162, 43, 0.16) 0%, rgba(255, 255, 255, 0.56) 100%);
|
||||
}
|
||||
|
||||
.dialog-icon {
|
||||
@@ -142,42 +145,46 @@ export default {
|
||||
|
||||
.dialog-title {
|
||||
flex-grow: 1;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 800;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dialog-close,
|
||||
.dialog-minimize {
|
||||
cursor: pointer;
|
||||
font-size: 1.5em;
|
||||
margin-left: 10px;
|
||||
font-size: 1.1rem;
|
||||
margin-left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
padding: 18px 20px;
|
||||
overflow-y: auto;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
padding: 14px 20px 18px;
|
||||
border-top: 1px solid rgba(93, 64, 55, 0.08);
|
||||
background: rgba(255, 255, 255, 0.46);
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
margin-left: 10px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
margin-left: 0;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
background: #0056b3;
|
||||
color: #2b1f14;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,6 +85,7 @@ import InputNumberWidget from '@/components/form/InputNumberWidget.vue';
|
||||
import FloatInputWidget from '@/components/form/FloatInputWidget.vue';
|
||||
import CheckboxWidget from '@/components/form/CheckboxWidget.vue';
|
||||
import MultiselectWidget from '@/components/form/MultiselectWidget.vue';
|
||||
import { showApiError, showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: "SettingsWidget",
|
||||
@@ -158,7 +159,7 @@ export default {
|
||||
// Prüfe ob das Setting unveränderlich ist
|
||||
const setting = this.settings.find(s => s.id === settingId);
|
||||
if (setting && setting.immutable && setting.value) {
|
||||
alert(this.$t('settings.immutable.tooltip'));
|
||||
showError(this, this.$t('settings.immutable.tooltip'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -172,9 +173,7 @@ export default {
|
||||
this.fetchSettings();
|
||||
} catch (err) {
|
||||
console.error('Error updating setting:', err);
|
||||
if (err.response && err.response.data && err.response.data.error) {
|
||||
alert(err.response.data.error);
|
||||
}
|
||||
showApiError(this, err, 'Änderung konnte nicht gespeichert werden.');
|
||||
}
|
||||
},
|
||||
languagesList() {
|
||||
@@ -208,6 +207,7 @@ export default {
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error updating visibility:', err);
|
||||
showApiError(this, err, 'Sichtbarkeit konnte nicht aktualisiert werden.');
|
||||
}
|
||||
},
|
||||
openContactDialog() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -23,11 +23,31 @@
|
||||
<img :src="'/images/icons/falukant/relationship-' + item.image + '.png'" class="relationship-icon" :title="$t(`falukant.statusbar.${item.key}`)" />
|
||||
</div>
|
||||
</template>
|
||||
<span v-if="statusItems.length > 0 && menu.falukant && menu.falukant.children">
|
||||
<div
|
||||
v-if="statusItems.length > 0 && menu.falukant && menu.falukant.children"
|
||||
class="quick-access"
|
||||
>
|
||||
<template v-for="(menuItem, key) in menu.falukant.children" :key="menuItem.id" >
|
||||
<img :src="'/images/icons/falukant/shortmap/' + key + '.png'" class="menu-icon" @click="openPage(menuItem)" :title="$t(`navigation.m-falukant.${key}`)" />
|
||||
<img
|
||||
:src="'/images/icons/falukant/shortmap/' + key + '.png'"
|
||||
class="menu-icon"
|
||||
@click="openPage(menuItem)"
|
||||
:title="$t(`navigation.m-falukant.${key}`)"
|
||||
/>
|
||||
</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>
|
||||
@@ -35,6 +55,7 @@
|
||||
<script>
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import apiClient from "@/utils/axios.js";
|
||||
import { EventBus } from '@/utils/eventBus.js';
|
||||
import MessagesDialog from './MessagesDialog.vue';
|
||||
|
||||
export default {
|
||||
@@ -51,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: {
|
||||
@@ -86,10 +112,16 @@ export default {
|
||||
// Socket.IO (Backend notifyUser) – Hauptkanal für Falukant-Events
|
||||
this.setupSocketListeners();
|
||||
this.setupDaemonListeners();
|
||||
EventBus.on('open-falukant-messages', this.openMessages);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.teardownSocketListeners();
|
||||
this.teardownDaemonListeners();
|
||||
if (this.pendingStatusRefresh) {
|
||||
clearTimeout(this.pendingStatusRefresh);
|
||||
this.pendingStatusRefresh = null;
|
||||
}
|
||||
EventBus.off('open-falukant-messages', this.openMessages);
|
||||
},
|
||||
methods: {
|
||||
preloadQuickAccessImages() {
|
||||
@@ -130,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) {
|
||||
@@ -158,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() {
|
||||
@@ -175,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) {
|
||||
@@ -189,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;
|
||||
}
|
||||
},
|
||||
@@ -220,14 +301,40 @@ export default {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f4f4f4;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
width: calc(100% + 40px);
|
||||
flex-wrap: wrap;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 251, 246, 0.96) 0%, rgba(247, 238, 224, 0.98) 100%);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
gap: 1.2em;
|
||||
margin: -21px -20px 1.5em -20px;
|
||||
position: fixed;
|
||||
padding: 0.55rem 0.9rem;
|
||||
margin: 0 0 1.5em 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
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 {
|
||||
@@ -235,6 +342,19 @@ export default {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.68);
|
||||
border: 1px solid rgba(93, 64, 55, 0.08);
|
||||
}
|
||||
|
||||
.quick-access {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.status-icon-wrapper {
|
||||
@@ -254,6 +374,8 @@ export default {
|
||||
.menu-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
padding: 4px 2px 0 0;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" title="passwordReset.title" :isTitleTranslated="true" :show-close=true :buttons="buttons" @close="closeDialog" @reset="resetPassword" name="PasswordReset">
|
||||
<div>
|
||||
<label>{{ $t("passwordReset.email") }} <input type="email" v-model="email" required /></label>
|
||||
<div class="form-stack">
|
||||
<div class="form-field">
|
||||
<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 gültige E-Mail-Adresse eingeben.</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
@@ -9,6 +14,7 @@
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'PasswordResetDialog',
|
||||
@@ -18,9 +24,21 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
buttons: [{ text: 'passwordReset.reset', action: 'reset' }]
|
||||
emailTouched: false,
|
||||
buttons: [{ text: 'passwordReset.reset', action: 'reset', disabled: true }]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isEmailValid() {
|
||||
return /\S+@\S+\.\S+/.test(this.email);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
email() {
|
||||
this.emailTouched = true;
|
||||
this.buttons[0].disabled = !this.isEmailValid;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.dialog.open();
|
||||
@@ -29,15 +47,18 @@ export default {
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
async resetPassword() {
|
||||
if (!this.isEmailValid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.post('/api/users/requestPasswordReset', {
|
||||
email: this.email
|
||||
});
|
||||
this.$refs.dialog.close();
|
||||
alert(this.$t("passwordReset.success"));
|
||||
showSuccess(this, 'tr:passwordReset.success');
|
||||
} catch (error) {
|
||||
console.error('Error resetting password:', error);
|
||||
alert(this.$t("passwordReset.failure"));
|
||||
showApiError(this, error, 'tr:passwordReset.failure');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,27 @@
|
||||
<DialogWidget ref="dialog" title="register.title" :show-close="true" :buttons="buttons" :modal="true"
|
||||
@close="closeDialog" @register="register" width="35em" height="33em" name="RegisterDialog"
|
||||
:isTitleTranslated="true">
|
||||
<div class="form-content">
|
||||
<div>
|
||||
<label>{{ $t("register.email") }}<input type="email" v-model="email" /></label>
|
||||
<div class="form-content form-stack">
|
||||
<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 gültige E-Mail-Adresse eingeben.</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>{{ $t("register.username") }}<input type="text" v-model="username" /></label>
|
||||
<div class="form-field">
|
||||
<label for="register-username">{{ $t("register.username") }}</label>
|
||||
<input id="register-username" type="text" v-model="username" :class="{ 'field-error': usernameTouched && !isUsernameValid }" />
|
||||
<span v-if="usernameTouched && !isUsernameValid" class="form-error">Der Benutzername sollte mindestens 3 Zeichen haben.</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>{{ $t("register.password") }}<input type="password" v-model="password" /></label>
|
||||
<div class="form-field">
|
||||
<label for="register-password">{{ $t("register.password") }}</label>
|
||||
<input id="register-password" type="password" v-model="password" :class="{ 'field-error': passwordTouched && !isPasswordValid }" />
|
||||
<span class="form-hint">Mindestens 8 Zeichen.</span>
|
||||
<span v-if="passwordTouched && !isPasswordValid" class="form-error">Das Passwort ist noch zu kurz.</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>{{ $t("register.repeatPassword") }}<input type="password" v-model="repeatPassword" /></label>
|
||||
<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 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" />
|
||||
@@ -26,6 +35,7 @@ import { mapActions } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue';
|
||||
import { showApiError, showError } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'RegisterDialog',
|
||||
@@ -41,6 +51,10 @@ export default {
|
||||
repeatPassword: '',
|
||||
language: null,
|
||||
languages: [],
|
||||
emailTouched: false,
|
||||
usernameTouched: false,
|
||||
passwordTouched: false,
|
||||
repeatPasswordTouched: false,
|
||||
buttons: [
|
||||
{ text: 'register.close', action: 'close' },
|
||||
{ text: 'register.register', action: 'register', disabled: !this.canRegister }
|
||||
@@ -48,11 +62,35 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isEmailValid() {
|
||||
return /\S+@\S+\.\S+/.test(this.email);
|
||||
},
|
||||
isUsernameValid() {
|
||||
return this.username.trim().length >= 3;
|
||||
},
|
||||
isPasswordValid() {
|
||||
return this.password.length >= 8;
|
||||
},
|
||||
doPasswordsMatch() {
|
||||
return Boolean(this.password) && this.password === this.repeatPassword;
|
||||
},
|
||||
canRegister() {
|
||||
return this.password && this.repeatPassword && this.password === this.repeatPassword;
|
||||
return this.isEmailValid && this.isUsernameValid && this.isPasswordValid && this.doPasswordsMatch && this.language;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
email() {
|
||||
this.emailTouched = true;
|
||||
},
|
||||
username() {
|
||||
this.usernameTouched = true;
|
||||
},
|
||||
password() {
|
||||
this.passwordTouched = true;
|
||||
},
|
||||
repeatPassword() {
|
||||
this.repeatPasswordTouched = true;
|
||||
},
|
||||
canRegister(newValue) {
|
||||
this.buttons[1].disabled = !newValue;
|
||||
}
|
||||
@@ -82,7 +120,7 @@ export default {
|
||||
},
|
||||
async register() {
|
||||
if (!this.canRegister) {
|
||||
this.$root.$refs.errrorDialog.open('tr:register.passwordMismatch');
|
||||
showError(this, 'tr:register.passwordMismatch');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,14 +137,14 @@ export default {
|
||||
this.$refs.dialog.close();
|
||||
this.$router.push('/activate');
|
||||
} else {
|
||||
this.$root.$refs.errrorDialog.open("tr:register.failure");
|
||||
showError(this, 'tr:register.failure');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 409) {
|
||||
this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error);
|
||||
showError(this, `tr:register.${error.response.data.error}`);
|
||||
} else {
|
||||
console.error('Error registering user:', error);
|
||||
this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error);
|
||||
showApiError(this, error, 'tr:register.failure');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -125,21 +163,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-content>div {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
input[type="email"],
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.5em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
{{ $t('falukant.branch.selection.selected') }}:
|
||||
<strong>{{ selectedRegion.name }}</strong>
|
||||
</div>
|
||||
<label class="form-label">
|
||||
<label class="form-label form-field">
|
||||
{{ $t('falukant.branch.columns.type') }}
|
||||
<select v-model="selectedType" class="form-control">
|
||||
<option
|
||||
@@ -72,8 +72,10 @@
|
||||
({{ formatCost(computeBranchCost(type)) }})
|
||||
</option>
|
||||
</select>
|
||||
<span class="form-hint">Wähle zuerst die Region und dann den Niederlassungstyp.</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="form-hint">Wähle auf der Karte eine freie Region aus.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,6 +85,7 @@
|
||||
<script>
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'CreateBranchDialog',
|
||||
@@ -109,7 +112,7 @@
|
||||
dialogButtons() {
|
||||
return [
|
||||
{ text: this.$t('Cancel'), action: this.close },
|
||||
{ text: this.$t('falukant.branch.actions.create'), action: this.onConfirm },
|
||||
{ text: this.$t('falukant.branch.actions.create'), action: this.onConfirm, disabled: !this.selectedRegion || !this.selectedType },
|
||||
];
|
||||
},
|
||||
},
|
||||
@@ -144,7 +147,10 @@
|
||||
},
|
||||
|
||||
async onConfirm() {
|
||||
if (!this.selectedRegion || !this.selectedType) return;
|
||||
if (!this.selectedRegion || !this.selectedType) {
|
||||
showError(this, 'Bitte zuerst Region und Typ auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post('/api/falukant/branches', {
|
||||
@@ -152,13 +158,14 @@
|
||||
branchTypeId: this.selectedType,
|
||||
});
|
||||
this.$emit('create-branch');
|
||||
showSuccess(this, 'Niederlassung erfolgreich erstellt.');
|
||||
this.close();
|
||||
} catch (e) {
|
||||
if (e?.response?.status === 412 && e?.response?.data?.error === 'insufficientFunds') {
|
||||
alert(this.$t('falukant.branch.actions.insufficientFunds'));
|
||||
showError(this, this.$t('falukant.branch.actions.insufficientFunds'));
|
||||
} else {
|
||||
console.error('Error creating branch', e);
|
||||
alert(this.$t('falukant.error.generic') || 'Fehler beim Erstellen der Niederlassung.');
|
||||
showApiError(this, e, this.$t('falukant.error.generic') || 'Fehler beim Erstellen der Niederlassung.');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,8 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" title="error.title" :show-close="true" :buttons="buttons" :modal="true" width="25em"
|
||||
height="15em" name="ErrorDialog" :isTitleTranslated=true>
|
||||
<DialogWidget ref="dialog" title="error.title" :show-close="true" :buttons="buttons" :modal="true" width="28em"
|
||||
height="16em" name="ErrorDialog" :isTitleTranslated=true>
|
||||
<div class="error-content">
|
||||
<span class="error-content__badge">Fehler</span>
|
||||
<p>{{ translatedErrorMessage }}</p>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
@@ -45,8 +46,27 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.error-content {
|
||||
padding: 1em;
|
||||
color: red;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-content__badge {
|
||||
justify-self: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(177, 59, 53, 0.12);
|
||||
color: var(--color-danger);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.error-content p {
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.55;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="translatedTitle" :show-close="true" :buttons="translatedButtons" :modal="true" width="25em"
|
||||
height="15em" name="MessageDialog" :isTitleTranslated=false>
|
||||
<DialogWidget ref="dialog" :title="translatedTitle" :show-close="true" :buttons="translatedButtons" :modal="true" width="28em"
|
||||
height="16em" name="MessageDialog" :isTitleTranslated=false>
|
||||
<div class="message-content">
|
||||
<span class="message-content__badge">Hinweis</span>
|
||||
<p>{{ translatedMessage }}</p>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
@@ -41,14 +42,6 @@ export default {
|
||||
if (this.message.startsWith('tr:')) {
|
||||
const i18nKey = this.message.substring(3);
|
||||
const translation = this.$t(i18nKey);
|
||||
console.log('translatedMessage:', {
|
||||
i18nKey: i18nKey,
|
||||
translation: translation,
|
||||
parameters: this.parameters,
|
||||
allMinigames: this.$t('minigames'),
|
||||
crashSection: this.$t('minigames.taxi.crash')
|
||||
});
|
||||
// Ersetze Parameter in der Übersetzung
|
||||
return this.interpolateParameters(translation);
|
||||
}
|
||||
return this.message;
|
||||
@@ -89,26 +82,16 @@ export default {
|
||||
}
|
||||
},
|
||||
interpolateParameters(text) {
|
||||
// Ersetze {key} Platzhalter mit den entsprechenden Werten
|
||||
let result = text;
|
||||
console.log('interpolateParameters:', {
|
||||
originalText: text,
|
||||
parameters: this.parameters
|
||||
});
|
||||
|
||||
for (const [key, value] of Object.entries(this.parameters)) {
|
||||
const placeholder = `{${key}}`;
|
||||
const regex = new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g');
|
||||
result = result.replace(regex, value);
|
||||
console.log(`Replaced ${placeholder} with ${value}:`, result);
|
||||
}
|
||||
|
||||
console.log('Final result:', result);
|
||||
return result;
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// Stelle sicher, dass Event Listener entfernt wird
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
};
|
||||
@@ -116,8 +99,27 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.message-content {
|
||||
padding: 1em;
|
||||
color: #000000;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-content__badge {
|
||||
justify-self: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(120, 195, 138, 0.16);
|
||||
color: #24523a;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.message-content p {
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.55;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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",
|
||||
@@ -497,6 +518,7 @@
|
||||
"gifts": "Werbegeschenke",
|
||||
"sendGift": "Werbegeschenk senden",
|
||||
"cancel": "Werbung abbrechen",
|
||||
"cancelConfirm": "Willst du die Werbung wirklich abbrechen? Der Fortschritt geht dabei verloren.",
|
||||
"cancelSuccess": "Die Werbung wurde abgebrochen.",
|
||||
"cancelError": "Die Werbung konnte nicht abgebrochen werden.",
|
||||
"cancelTooSoon": "Du kannst die Werbung erst nach 24 Stunden abbrechen.",
|
||||
@@ -516,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"
|
||||
},
|
||||
@@ -537,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",
|
||||
@@ -650,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",
|
||||
@@ -756,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",
|
||||
@@ -764,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",
|
||||
@@ -786,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"
|
||||
@@ -879,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",
|
||||
@@ -894,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": {
|
||||
@@ -970,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.",
|
||||
@@ -1119,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",
|
||||
@@ -1135,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",
|
||||
@@ -1144,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": {
|
||||
@@ -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,13 +589,100 @@
|
||||
}
|
||||
},
|
||||
"spouse": {
|
||||
"marriageSatisfaction": "Marriage Satisfaction",
|
||||
"marriageState": "Marriage State",
|
||||
"wooing": {
|
||||
"cancel": "Cancel wooing",
|
||||
"cancelConfirm": "Do you really want to cancel wooing? Progress will be lost.",
|
||||
"cancelSuccess": "Wooing has been cancelled.",
|
||||
"cancelError": "Wooing could not be cancelled.",
|
||||
"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.",
|
||||
@@ -470,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",
|
||||
@@ -485,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": {
|
||||
@@ -533,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": {
|
||||
@@ -603,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",
|
||||
@@ -481,6 +502,7 @@
|
||||
"gifts": "Regalos de cortejo",
|
||||
"sendGift": "Enviar regalo",
|
||||
"cancel": "Cancelar el cortejo",
|
||||
"cancelConfirm": "¿Seguro que quieres cancelar el cortejo? Se perderá el progreso.",
|
||||
"cancelSuccess": "El cortejo se ha cancelado.",
|
||||
"cancelError": "No se pudo cancelar el cortejo.",
|
||||
"cancelTooSoon": "Solo puedes cancelar el cortejo después de 24 horas.",
|
||||
@@ -500,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"
|
||||
},
|
||||
@@ -521,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",
|
||||
@@ -617,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",
|
||||
@@ -722,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",
|
||||
@@ -730,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",
|
||||
@@ -752,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"
|
||||
@@ -822,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": {
|
||||
@@ -867,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.",
|
||||
@@ -1007,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",
|
||||
@@ -1023,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",
|
||||
@@ -1032,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user