Implement lover relationship management features: Add endpoints for creating, acknowledging, and managing lover relationships in the FalukantController. Enhance backend models with RelationshipState for tracking relationship statuses. Update frontend components to display and manage lover details, including marriage satisfaction and household tension. Improve localization for new features in multiple languages.
This commit is contained in:
@@ -116,6 +116,14 @@ class FalukantController {
|
||||
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));
|
||||
this.createLoverRelationship = this._wrapWithUser((userId, req) =>
|
||||
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201 });
|
||||
this.acknowledgeLover = this._wrapWithUser((userId, req) =>
|
||||
this.service.acknowledgeLover(userId, req.params.relationshipId));
|
||||
this.endLoverRelationship = this._wrapWithUser((userId, req) =>
|
||||
this.service.endLoverRelationship(userId, req.params.relationshipId));
|
||||
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
|
||||
this.sendGift = this._wrapWithUser(async (userId, req) => {
|
||||
try {
|
||||
@@ -234,6 +242,7 @@ class FalukantController {
|
||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||
|
||||
this.getUndergroundTypes = this._wrapWithUser((userId) => this.service.getUndergroundTypes(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 });
|
||||
|
||||
@@ -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');
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
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;
|
||||
@@ -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,
|
||||
|
||||
@@ -47,6 +47,10 @@ 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/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);
|
||||
@@ -101,6 +105,7 @@ 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/activities', falukantController.getUndergroundActivities);
|
||||
router.get('/notifications', falukantController.getNotifications);
|
||||
router.get('/notifications/all', falukantController.getAllNotifications);
|
||||
router.post('/notifications/mark-shown', falukantController.markNotificationsShown);
|
||||
|
||||
@@ -29,6 +29,7 @@ import DaySell from '../models/falukant/log/daysell.js';
|
||||
import MarriageProposal from '../models/falukant/data/marriage_proposal.js';
|
||||
import RelationshipType from '../models/falukant/type/relationship.js';
|
||||
import Relationship from '../models/falukant/data/relationship.js';
|
||||
import RelationshipState from '../models/falukant/data/relationship_state.js';
|
||||
import PromotionalGift from '../models/falukant/type/promotional_gift.js';
|
||||
import PromotionalGiftCharacterTrait from '../models/falukant/predefine/promotional_gift_character_trait.js';
|
||||
import PromotionalGiftMood from '../models/falukant/predefine/promotional_gift_mood.js';
|
||||
@@ -419,6 +420,158 @@ class FalukantService extends BaseService {
|
||||
FROM ancestors;
|
||||
`;
|
||||
|
||||
buildDefaultRelationshipState(typeTr) {
|
||||
const base = {
|
||||
marriageSatisfaction: 55,
|
||||
marriagePublicStability: 55,
|
||||
loverRole: null,
|
||||
affection: 50,
|
||||
visibility: 15,
|
||||
discretion: 50,
|
||||
maintenanceLevel: 50,
|
||||
statusFit: 0,
|
||||
monthlyBaseCost: 0,
|
||||
monthsUnderfunded: 0,
|
||||
active: true,
|
||||
acknowledged: false,
|
||||
exclusiveFlag: false,
|
||||
};
|
||||
|
||||
if (typeTr === 'lover') {
|
||||
return {
|
||||
...base,
|
||||
loverRole: 'lover',
|
||||
visibility: 20,
|
||||
discretion: 45,
|
||||
monthlyBaseCost: 30,
|
||||
};
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
async ensureRelationshipStates(relRows, typeMap) {
|
||||
if (!relRows?.length) return new Map();
|
||||
|
||||
const relationshipIds = relRows.map((r) => r.id).filter(Boolean);
|
||||
if (relationshipIds.length === 0) return new Map();
|
||||
|
||||
const existingStates = await RelationshipState.findAll({
|
||||
where: { relationshipId: relationshipIds }
|
||||
});
|
||||
const stateMap = new Map(existingStates.map((state) => [state.relationshipId, state]));
|
||||
|
||||
const missingPayloads = relRows
|
||||
.filter((r) => !stateMap.has(r.id))
|
||||
.map((r) => ({
|
||||
relationshipId: r.id,
|
||||
...this.buildDefaultRelationshipState(typeMap[r.relationshipTypeId]?.tr || null)
|
||||
}));
|
||||
|
||||
if (missingPayloads.length > 0) {
|
||||
await RelationshipState.bulkCreate(missingPayloads, { ignoreDuplicates: true });
|
||||
const reloadedStates = await RelationshipState.findAll({
|
||||
where: { relationshipId: relationshipIds }
|
||||
});
|
||||
return new Map(reloadedStates.map((state) => [state.relationshipId, state]));
|
||||
}
|
||||
|
||||
return stateMap;
|
||||
}
|
||||
|
||||
async getOwnedLoverRelationState(hashedUserId, relationshipId) {
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
if (!user?.character?.id) throw new Error('User or character not found');
|
||||
|
||||
const relationship = await Relationship.findOne({
|
||||
where: {
|
||||
id: relationshipId,
|
||||
character1Id: user.character.id
|
||||
},
|
||||
include: [{
|
||||
model: RelationshipType,
|
||||
as: 'relationshipType',
|
||||
where: { tr: 'lover' }
|
||||
}]
|
||||
});
|
||||
|
||||
if (!relationship) {
|
||||
throw { status: 404, message: 'Lover relationship not found' };
|
||||
}
|
||||
|
||||
let state = await RelationshipState.findOne({ where: { relationshipId: relationship.id } });
|
||||
if (!state) {
|
||||
state = await RelationshipState.create({
|
||||
relationshipId: relationship.id,
|
||||
...this.buildDefaultRelationshipState('lover')
|
||||
});
|
||||
}
|
||||
|
||||
return { user, relationship, state };
|
||||
}
|
||||
|
||||
getMarriageStateLabel(satisfaction) {
|
||||
if (satisfaction == null) return null;
|
||||
if (satisfaction < 40) return 'crisis';
|
||||
if (satisfaction < 60) return 'strained';
|
||||
return 'stable';
|
||||
}
|
||||
|
||||
getLoverRiskState(state) {
|
||||
if (!state) return 'low';
|
||||
if ((state.visibility ?? 0) >= 60 || (state.monthsUnderfunded ?? 0) >= 2) return 'high';
|
||||
if ((state.visibility ?? 0) >= 35 || (state.monthsUnderfunded ?? 0) >= 1) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
calculateLoverStatusFit(ownTitleId, targetTitleId) {
|
||||
const diff = Math.abs(Number(ownTitleId || 0) - Number(targetTitleId || 0));
|
||||
return Math.max(0, 100 - diff * 20);
|
||||
}
|
||||
|
||||
calculateLoverBaseCost(ownTitleId, targetTitleId) {
|
||||
const diff = Math.abs(Number(ownTitleId || 0) - Number(targetTitleId || 0));
|
||||
return 20 + diff * 10;
|
||||
}
|
||||
|
||||
getLoverRoleConfig(loverRole, ownTitleId, targetTitleId) {
|
||||
const normalizedRole = ['secret_affair', 'lover', 'mistress_or_favorite'].includes(loverRole)
|
||||
? loverRole
|
||||
: 'secret_affair';
|
||||
const baseCost = this.calculateLoverBaseCost(ownTitleId, targetTitleId);
|
||||
|
||||
switch (normalizedRole) {
|
||||
case 'lover':
|
||||
return {
|
||||
loverRole: normalizedRole,
|
||||
affection: 50,
|
||||
visibility: 30,
|
||||
discretion: 45,
|
||||
acknowledged: true,
|
||||
monthlyBaseCost: baseCost + 10
|
||||
};
|
||||
case 'mistress_or_favorite':
|
||||
return {
|
||||
loverRole: normalizedRole,
|
||||
affection: 55,
|
||||
visibility: 45,
|
||||
discretion: 35,
|
||||
acknowledged: true,
|
||||
monthlyBaseCost: baseCost + 25
|
||||
};
|
||||
case 'secret_affair':
|
||||
default:
|
||||
return {
|
||||
loverRole: 'secret_affair',
|
||||
affection: 45,
|
||||
visibility: 10,
|
||||
discretion: 55,
|
||||
acknowledged: false,
|
||||
monthlyBaseCost: baseCost
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getFalukantUserByHashedId(hashedId) {
|
||||
const user = await FalukantUser.findOne({
|
||||
include: [
|
||||
@@ -2748,7 +2901,7 @@ class FalukantService extends BaseService {
|
||||
// Load relationships without includes to avoid EagerLoadingError
|
||||
const relRows = await Relationship.findAll({
|
||||
where: { character1Id: character.id },
|
||||
attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress', 'character2Id', 'relationshipTypeId']
|
||||
attributes: ['id', 'createdAt', 'widowFirstName2', 'nextStepProgress', 'character2Id', 'relationshipTypeId']
|
||||
});
|
||||
let relationships;
|
||||
if (relRows.length === 0) {
|
||||
@@ -2770,6 +2923,7 @@ class FalukantService extends BaseService {
|
||||
]);
|
||||
const typeMap = Object.fromEntries(types.map(t => [t.id, t]));
|
||||
const char2Map = Object.fromEntries(character2s.map(c => [c.id, c]));
|
||||
const relationshipStateMap = await this.ensureRelationshipStates(relRows, typeMap);
|
||||
const ctRows = await FalukantCharacterTrait.findAll({
|
||||
where: { characterId: char2Ids },
|
||||
attributes: ['characterId', 'traitId']
|
||||
@@ -2791,7 +2945,9 @@ class FalukantService extends BaseService {
|
||||
relationships = relRows.map(r => {
|
||||
const c2 = char2Map[r.character2Id];
|
||||
const type = typeMap[r.relationshipTypeId];
|
||||
const state = relationshipStateMap.get(r.id);
|
||||
return {
|
||||
id: r.id,
|
||||
createdAt: r.createdAt,
|
||||
widowFirstName2: r.widowFirstName2,
|
||||
progress: r.nextStepProgress,
|
||||
@@ -2804,7 +2960,22 @@ class FalukantService extends BaseService {
|
||||
mood: c2.mood,
|
||||
traits: c2.traits || []
|
||||
} : null,
|
||||
relationshipType: type ? type.tr : ''
|
||||
relationshipType: type ? type.tr : '',
|
||||
state: state ? {
|
||||
marriageSatisfaction: state.marriageSatisfaction,
|
||||
marriagePublicStability: state.marriagePublicStability,
|
||||
loverRole: state.loverRole,
|
||||
affection: state.affection,
|
||||
visibility: state.visibility,
|
||||
discretion: state.discretion,
|
||||
maintenanceLevel: state.maintenanceLevel,
|
||||
statusFit: state.statusFit,
|
||||
monthlyBaseCost: state.monthlyBaseCost,
|
||||
monthsUnderfunded: state.monthsUnderfunded,
|
||||
active: state.active,
|
||||
acknowledged: state.acknowledged,
|
||||
exclusiveFlag: state.exclusiveFlag,
|
||||
} : this.buildDefaultRelationshipState(type ? type.tr : null)
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2821,7 +2992,7 @@ class FalukantService extends BaseService {
|
||||
{ motherCharacterId: { [Op.in]: userCharacterIds } }
|
||||
]
|
||||
},
|
||||
attributes: ['childCharacterId', 'nameSet', 'isHeir', 'createdAt']
|
||||
attributes: ['childCharacterId', 'nameSet', 'isHeir', 'legitimacy', 'birthContext', 'publicKnown', 'createdAt']
|
||||
})
|
||||
: [];
|
||||
const childCharIds = [...new Set(childRels.map(r => r.childCharacterId))];
|
||||
@@ -2842,20 +3013,69 @@ class FalukantService extends BaseService {
|
||||
age: kid?.birthdate ? calcAge(kid.birthdate) : null,
|
||||
hasName: rel.nameSet,
|
||||
isHeir: rel.isHeir || false,
|
||||
legitimacy: rel.legitimacy || 'legitimate',
|
||||
birthContext: rel.birthContext || 'marriage',
|
||||
publicKnown: !!rel.publicKnown,
|
||||
_createdAt: rel.createdAt,
|
||||
};
|
||||
});
|
||||
// Sort children globally by relation createdAt ascending (older first)
|
||||
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
|
||||
const inProgress = ['wooing', 'engaged', 'married'];
|
||||
const activeRelationships = relationships.filter(r => inProgress.includes(r.relationshipType));
|
||||
const activeMarriage = activeRelationships.find(r => r.relationshipType === 'married') || activeRelationships[0] || null;
|
||||
const marriageSatisfaction = activeMarriage?.state?.marriageSatisfaction ?? null;
|
||||
const marriageState = this.getMarriageStateLabel(marriageSatisfaction);
|
||||
const lovers = relationships
|
||||
.filter(r => r.relationshipType === 'lover')
|
||||
.filter(r => (r.state?.active ?? true) !== false)
|
||||
.map((r) => {
|
||||
const state = r.state || this.buildDefaultRelationshipState('lover');
|
||||
const partner = r.character2 || {};
|
||||
const monthlyCost = Number(state.monthlyBaseCost || 0);
|
||||
return {
|
||||
relationshipId: r.id,
|
||||
name: partner.firstName || 'Unknown',
|
||||
gender: partner.gender || null,
|
||||
title: partner.nobleTitle || '',
|
||||
role: state.loverRole || 'lover',
|
||||
affection: state.affection,
|
||||
visibility: state.visibility,
|
||||
discretion: state.discretion,
|
||||
maintenanceLevel: state.maintenanceLevel,
|
||||
statusFit: state.statusFit,
|
||||
monthlyBaseCost: monthlyCost,
|
||||
monthlyCost,
|
||||
acknowledged: state.acknowledged,
|
||||
active: state.active,
|
||||
monthsUnderfunded: state.monthsUnderfunded,
|
||||
riskState: this.getLoverRiskState(state),
|
||||
reputationEffect: null,
|
||||
marriageEffect: null,
|
||||
canBecomePublic: true,
|
||||
character2: partner,
|
||||
state,
|
||||
};
|
||||
});
|
||||
const family = {
|
||||
relationships: relationships.filter(r => inProgress.includes(r.relationshipType)),
|
||||
lovers: relationships.filter(r => r.relationshipType === 'lover'),
|
||||
relationships: activeRelationships.map((r) => ({
|
||||
...r,
|
||||
marriageSatisfaction: r.state?.marriageSatisfaction ?? null,
|
||||
marriageState: this.getMarriageStateLabel(r.state?.marriageSatisfaction ?? null),
|
||||
})),
|
||||
marriageSatisfaction,
|
||||
marriageState,
|
||||
householdTension: lovers.some(l => l.riskState === 'high') ? 'high' : lovers.some(l => l.riskState === 'medium') ? 'medium' : 'low',
|
||||
lovers,
|
||||
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
|
||||
children: children.map(({ _createdAt, ...rest }) => rest),
|
||||
possiblePartners: []
|
||||
possiblePartners: [],
|
||||
possibleLovers: []
|
||||
};
|
||||
const ownAge = calcAge(character.birthdate);
|
||||
if (ownAge >= 12) {
|
||||
family.possibleLovers = await this.getPossibleLovers(character.id);
|
||||
}
|
||||
if (ownAge >= 12 && family.relationships.length === 0) {
|
||||
family.possiblePartners = await this.getPossiblePartners(character.id);
|
||||
if (family.possiblePartners.length === 0) {
|
||||
@@ -2872,6 +3092,185 @@ class FalukantService extends BaseService {
|
||||
return family;
|
||||
}
|
||||
|
||||
async getPossibleLovers(requestingCharacterId) {
|
||||
const requester = await FalukantCharacter.findOne({
|
||||
where: { id: requestingCharacterId },
|
||||
attributes: ['id', 'regionId', 'birthdate', 'titleOfNobility']
|
||||
});
|
||||
if (!requester?.id) return [];
|
||||
if (calcAge(requester.birthdate) < 12) return [];
|
||||
|
||||
const existingRelationships = await Relationship.findAll({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ character1Id: requestingCharacterId },
|
||||
{ character2Id: requestingCharacterId }
|
||||
]
|
||||
},
|
||||
include: [
|
||||
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'] },
|
||||
{ model: RelationshipState, as: 'state', required: false }
|
||||
],
|
||||
attributes: ['character1Id', 'character2Id']
|
||||
});
|
||||
|
||||
const excludedCharacterIds = new Set([requestingCharacterId]);
|
||||
for (const rel of existingRelationships) {
|
||||
const relationType = rel.relationshipType?.tr;
|
||||
const isActiveLover = relationType === 'lover' ? ((rel.state?.active ?? true) !== false) : true;
|
||||
if (relationType !== 'widowed' && isActiveLover) {
|
||||
excludedCharacterIds.add(rel.character1Id === requestingCharacterId ? rel.character2Id : rel.character1Id);
|
||||
}
|
||||
}
|
||||
|
||||
const ownTitle = Number(requester.titleOfNobility || 0);
|
||||
const ownAge = calcAge(requester.birthdate);
|
||||
const candidates = await FalukantCharacter.findAll({
|
||||
where: {
|
||||
id: { [Op.notIn]: Array.from(excludedCharacterIds) },
|
||||
regionId: requester.regionId,
|
||||
health: { [Op.gt]: 0 },
|
||||
birthdate: { [Op.lte]: new Date(Date.now() - 12 * 24 * 60 * 60 * 1000) },
|
||||
titleOfNobility: { [Op.between]: [Math.max(1, ownTitle - 2), ownTitle + 2] }
|
||||
},
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
|
||||
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'id'] }
|
||||
],
|
||||
order: [
|
||||
[Sequelize.literal(`ABS("title_of_nobility" - ${ownTitle})`), 'ASC'],
|
||||
[Sequelize.literal(`ABS((EXTRACT(EPOCH FROM (NOW() - "birthdate")) / 86400) - ${ownAge})`), 'ASC']
|
||||
],
|
||||
limit: 6
|
||||
});
|
||||
|
||||
return candidates.map((candidate) => {
|
||||
const age = calcAge(candidate.birthdate);
|
||||
return {
|
||||
characterId: candidate.id,
|
||||
name: `${candidate.definedFirstName?.name || ''} ${candidate.definedLastName?.name || ''}`.trim(),
|
||||
gender: candidate.gender,
|
||||
age,
|
||||
title: candidate.nobleTitle?.labelTr || 'noncivil',
|
||||
titleId: candidate.nobleTitle?.id || candidate.titleOfNobility || null,
|
||||
statusFit: this.calculateLoverStatusFit(ownTitle, candidate.nobleTitle?.id || candidate.titleOfNobility),
|
||||
estimatedMonthlyCost: this.calculateLoverBaseCost(ownTitle, candidate.nobleTitle?.id || candidate.titleOfNobility)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async createLoverRelationship(hashedUserId, targetCharacterId, loverRole) {
|
||||
const parsedTargetCharacterId = Number.parseInt(targetCharacterId, 10);
|
||||
if (Number.isNaN(parsedTargetCharacterId)) {
|
||||
throw { status: 400, message: 'targetCharacterId is required' };
|
||||
}
|
||||
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
if (!user?.character?.id) throw new Error('User or character not found');
|
||||
if (user.character.id === parsedTargetCharacterId) {
|
||||
throw { status: 400, message: 'Cannot create relationship with self' };
|
||||
}
|
||||
|
||||
const possibleLovers = await this.getPossibleLovers(user.character.id);
|
||||
const target = possibleLovers.find((candidate) => candidate.characterId === parsedTargetCharacterId);
|
||||
if (!target) {
|
||||
throw { status: 409, message: 'Target character is not available for a lover relationship' };
|
||||
}
|
||||
|
||||
const loverType = await RelationshipType.findOne({ where: { tr: 'lover' }, attributes: ['id'] });
|
||||
if (!loverType?.id) {
|
||||
throw new Error('Relationship type "lover" not found');
|
||||
}
|
||||
|
||||
const relationship = await Relationship.create({
|
||||
character1Id: user.character.id,
|
||||
character2Id: parsedTargetCharacterId,
|
||||
relationshipTypeId: loverType.id
|
||||
});
|
||||
|
||||
const roleConfig = this.getLoverRoleConfig(
|
||||
loverRole,
|
||||
user.character.titleOfNobility,
|
||||
target.titleId
|
||||
);
|
||||
|
||||
await RelationshipState.create({
|
||||
relationshipId: relationship.id,
|
||||
...this.buildDefaultRelationshipState('lover'),
|
||||
...roleConfig,
|
||||
maintenanceLevel: 50,
|
||||
statusFit: target.statusFit,
|
||||
active: true
|
||||
});
|
||||
|
||||
await notifyUser(hashedUserId, 'falukantUpdateFamily', { reason: 'daily' });
|
||||
await notifyUser(hashedUserId, 'falukantUpdateStatus', {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
relationshipId: relationship.id,
|
||||
targetCharacterId: parsedTargetCharacterId
|
||||
};
|
||||
}
|
||||
|
||||
async setLoverMaintenance(hashedUserId, relationshipId, maintenanceLevel) {
|
||||
const parsedRelationshipId = Number.parseInt(relationshipId, 10);
|
||||
const parsedMaintenance = Number.parseInt(maintenanceLevel, 10);
|
||||
if (Number.isNaN(parsedRelationshipId) || Number.isNaN(parsedMaintenance)) {
|
||||
throw { status: 400, message: 'relationshipId and maintenanceLevel are required' };
|
||||
}
|
||||
if (parsedMaintenance < 0 || parsedMaintenance > 100) {
|
||||
throw { status: 400, message: 'maintenanceLevel must be between 0 and 100' };
|
||||
}
|
||||
|
||||
const { state } = await this.getOwnedLoverRelationState(hashedUserId, parsedRelationshipId);
|
||||
await state.update({ maintenanceLevel: parsedMaintenance });
|
||||
return {
|
||||
success: true,
|
||||
relationshipId: parsedRelationshipId,
|
||||
maintenanceLevel: state.maintenanceLevel
|
||||
};
|
||||
}
|
||||
|
||||
async acknowledgeLover(hashedUserId, relationshipId) {
|
||||
const parsedRelationshipId = Number.parseInt(relationshipId, 10);
|
||||
if (Number.isNaN(parsedRelationshipId)) {
|
||||
throw { status: 400, message: 'relationshipId is required' };
|
||||
}
|
||||
|
||||
const { state } = await this.getOwnedLoverRelationState(hashedUserId, parsedRelationshipId);
|
||||
const updateData = { acknowledged: true };
|
||||
if (state.loverRole === 'secret_affair' || !state.loverRole) {
|
||||
updateData.loverRole = 'lover';
|
||||
}
|
||||
await state.update(updateData);
|
||||
return {
|
||||
success: true,
|
||||
relationshipId: parsedRelationshipId,
|
||||
acknowledged: true,
|
||||
role: state.loverRole
|
||||
};
|
||||
}
|
||||
|
||||
async endLoverRelationship(hashedUserId, relationshipId) {
|
||||
const parsedRelationshipId = Number.parseInt(relationshipId, 10);
|
||||
if (Number.isNaN(parsedRelationshipId)) {
|
||||
throw { status: 400, message: 'relationshipId is required' };
|
||||
}
|
||||
|
||||
const { state } = await this.getOwnedLoverRelationState(hashedUserId, parsedRelationshipId);
|
||||
await state.update({
|
||||
active: false,
|
||||
acknowledged: false
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
relationshipId: parsedRelationshipId,
|
||||
active: false
|
||||
};
|
||||
}
|
||||
|
||||
async setHeir(hashedUserId, childCharacterId) {
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
if (!user) throw new Error('User not found');
|
||||
@@ -5471,12 +5870,25 @@ ORDER BY r.id`,
|
||||
// politicalTargets kann optional sein, falls benötigt prüfen
|
||||
}
|
||||
|
||||
if (undergroundType.tr === 'investigate_affair') {
|
||||
if (!goal || !['expose', 'blackmail'].includes(goal)) {
|
||||
throw new PreconditionError('Affair investigation goal missing');
|
||||
}
|
||||
}
|
||||
|
||||
// 5) Eintrag anlegen (optional: in Transaction)
|
||||
const newEntry = await Underground.create({
|
||||
undergroundTypeId: typeId,
|
||||
performerId: performerChar.id,
|
||||
victimId: victimChar.id,
|
||||
result: null,
|
||||
result: {
|
||||
status: 'pending',
|
||||
outcome: null,
|
||||
discoveries: null,
|
||||
visibilityDelta: 0,
|
||||
reputationDelta: 0,
|
||||
blackmailAmount: 0
|
||||
},
|
||||
parameters: {
|
||||
target: target || null,
|
||||
goal: goal || null,
|
||||
@@ -5487,6 +5899,57 @@ ORDER BY r.id`,
|
||||
return newEntry;
|
||||
}
|
||||
|
||||
async getUndergroundActivities(hashedUserId) {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
const character = await FalukantCharacter.findOne({
|
||||
where: { userId: falukantUser.id }
|
||||
});
|
||||
if (!character) throw new Error('Character not found');
|
||||
|
||||
const activities = await Underground.findAll({
|
||||
where: { performerId: character.id },
|
||||
include: [
|
||||
{
|
||||
model: FalukantCharacter,
|
||||
as: 'victim',
|
||||
include: [
|
||||
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
|
||||
{ model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] }
|
||||
],
|
||||
attributes: ['id', 'gender']
|
||||
},
|
||||
{
|
||||
model: UndergroundType,
|
||||
as: 'undergroundType',
|
||||
attributes: ['tr', 'cost']
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
return activities.map((activity) => {
|
||||
const result = activity.result || {};
|
||||
const status = result.status || (result.outcome ? 'resolved' : 'pending');
|
||||
return {
|
||||
id: activity.id,
|
||||
type: activity.undergroundType?.tr || null,
|
||||
cost: activity.undergroundType?.cost || null,
|
||||
victimName: `${activity.victim?.definedFirstName?.name || ''} ${activity.victim?.definedLastName?.name || ''}`.trim() || '—',
|
||||
createdAt: activity.createdAt,
|
||||
status,
|
||||
success: result.outcome === 'success',
|
||||
target: activity.parameters?.target || null,
|
||||
goal: activity.parameters?.goal || null,
|
||||
additionalInfo: {
|
||||
discoveries: result.discoveries || null,
|
||||
visibilityDelta: result.visibilityDelta ?? null,
|
||||
reputationDelta: result.reputationDelta ?? null,
|
||||
blackmailAmount: result.blackmailAmount ?? null
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async getUndergroundAttacks(hashedUserId) {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
@@ -659,6 +659,10 @@ const undergroundTypes = [
|
||||
"tr": "rob",
|
||||
"cost": 500
|
||||
},
|
||||
{
|
||||
"tr": "investigate_affair",
|
||||
"cost": 7000
|
||||
},
|
||||
];
|
||||
|
||||
{
|
||||
|
||||
@@ -115,9 +115,6 @@ Jede Liebhaber-Beziehung sollte mindestens diese Werte tragen:
|
||||
- `statusFit`: passt die Beziehung zum Stand der Spielfigur
|
||||
- `householdTension`: Spannungen im eigenen Haus
|
||||
- `scandalRisk`: Risiko für Gerüchte, Erpressung oder Entdeckung
|
||||
|
||||
Optional später:
|
||||
|
||||
- `fertilityRisk`
|
||||
- `politicalValue`
|
||||
- `churchOffense`
|
||||
|
||||
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
|
||||
192
docs/FALUKANT_UI_WEBSOCKET.md
Normal file
192
docs/FALUKANT_UI_WEBSOCKET.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Falukant: UI-Anpassung – WebSocket & Familie / Liebschaften
|
||||
|
||||
Dieses Dokument beschreibt die Nachrichten, die der externe Falukant-Daemon für Familien-, Ehe- und Liebschaftsänderungen 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 von Familie, Liebschaften und je nach `reason` auch Geld oder Ruf |
|
||||
| `falukantUpdateStatus` | `user_id` | Allgemeiner Falukant-Status-/Spielstands-Refresh |
|
||||
| `children_update` | `user_id` | Kinderliste und Familienansicht aktualisieren |
|
||||
| `falukant_family_scandal_hint` | `relationship_id` | Optionaler Hinweis oder Logeintrag; kein `user_id` |
|
||||
|
||||
## 2. JSON-Payloads
|
||||
|
||||
### 2.1 `falukantUpdateFamily`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateFamily",
|
||||
"user_id": 123,
|
||||
"reason": "daily"
|
||||
}
|
||||
```
|
||||
|
||||
`reason` ist immer genau einer dieser festen Strings:
|
||||
- `daily`
|
||||
- `monthly`
|
||||
- `scandal`
|
||||
- `lover_birth`
|
||||
|
||||
Es gibt keine weiteren `reason`-Werte.
|
||||
|
||||
### 2.2 `falukantUpdateStatus`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateStatus",
|
||||
"user_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
Dieses Event wird typischerweise direkt nach `falukantUpdateFamily` mit derselben `user_id` gesendet.
|
||||
|
||||
### 2.3 `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.4 `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"`.
|
||||
|
||||
## 3. Fachliche Bedeutung von `reason`
|
||||
|
||||
### `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
|
||||
- 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: "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: "monthly"`
|
||||
|
||||
`monthly` steht für Monatsverarbeitung, insbesondere:
|
||||
- laufende Kosten
|
||||
- Unterversorgung
|
||||
- Geldänderungen
|
||||
|
||||
### `reason: "lover_birth"`
|
||||
|
||||
`lover_birth` signalisiert ein neues Kind aus einer Liebschaft.
|
||||
|
||||
Meist folgen zusammen:
|
||||
- `falukantUpdateFamily` mit `reason: "lover_birth"`
|
||||
- `children_update`
|
||||
- `falukantUpdateStatus`
|
||||
|
||||
## 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 "children_update":
|
||||
refreshChildrenAndFamilyView()
|
||||
return
|
||||
|
||||
case "falukantUpdateFamily":
|
||||
switch json.reason:
|
||||
case "daily":
|
||||
refreshFamilyAndRelationships()
|
||||
refreshReputationIfNeeded()
|
||||
break
|
||||
case "monthly":
|
||||
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
|
||||
|
||||
Am selben Tag kann ein Nutzer mehrere relevante Events erhalten, zum Beispiel:
|
||||
- `scandal`
|
||||
- danach `daily`
|
||||
- danach `falukantUpdateStatus`
|
||||
|
||||
Die UI sollte deshalb:
|
||||
- Refreshes bündeln oder entprellen
|
||||
- idempotente Reloads verwenden
|
||||
- nicht davon ausgehen, dass jeder fachliche Effekt einen eigenen `reason` 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: "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 |
|
||||
|
||||
## 7. Sonderfälle
|
||||
|
||||
| Fall | Verhalten |
|
||||
|------|-----------|
|
||||
| NPC ohne `user_id` | Keine nutzerbezogenen Family-Socket-Events |
|
||||
| Mehrere Events kurz hintereinander | Normal; UI sollte damit robust umgehen |
|
||||
| Nur `falukantUpdateStatus` ohne Family-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.
|
||||
@@ -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,51 @@ 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', 'children_update', '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._childrenSocketHandler = (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('children_update', this._childrenSocketHandler);
|
||||
this.socket.on('falukantBranchUpdate', this._branchSocketHandler);
|
||||
}
|
||||
},
|
||||
teardownSocketListeners() {
|
||||
@@ -116,10 +143,21 @@ 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._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
|
||||
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;
|
||||
|
||||
@@ -60,10 +60,11 @@ export default {
|
||||
{ key: "children", icon: "👶", value: null },
|
||||
],
|
||||
unreadCount: 0,
|
||||
pendingStatusRefresh: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["socket", "daemonSocket"]),
|
||||
...mapState(["socket", "daemonSocket", "user"]),
|
||||
...mapGetters(['menu']),
|
||||
},
|
||||
watch: {
|
||||
@@ -100,6 +101,10 @@ export default {
|
||||
beforeUnmount() {
|
||||
this.teardownSocketListeners();
|
||||
this.teardownDaemonListeners();
|
||||
if (this.pendingStatusRefresh) {
|
||||
clearTimeout(this.pendingStatusRefresh);
|
||||
this.pendingStatusRefresh = null;
|
||||
}
|
||||
EventBus.off('open-falukant-messages', this.openMessages);
|
||||
},
|
||||
methods: {
|
||||
@@ -169,15 +174,25 @@ 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._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...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('children_update', this._childrenSocketHandler);
|
||||
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._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
|
||||
if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler);
|
||||
if (this._familyChangedSocketHandler) this.socket.off('familychanged', this._familyChangedSocketHandler);
|
||||
}
|
||||
},
|
||||
setupDaemonListeners() {
|
||||
@@ -186,13 +201,22 @@ export default {
|
||||
this._daemonHandler = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (['falukantUpdateStatus', 'stock_change', 'familychanged'].includes(data.event)) {
|
||||
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'stock_change', 'familychanged'].includes(data.event)) {
|
||||
this.handleEvent(data);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
this.daemonSocket.addEventListener('message', this._daemonHandler);
|
||||
},
|
||||
matchesCurrentUser(eventData) {
|
||||
if (eventData?.user_id == null) {
|
||||
return true;
|
||||
}
|
||||
const currentIds = [this.user?.id, this.user?.hashedId]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value));
|
||||
return currentIds.includes(String(eventData.user_id));
|
||||
},
|
||||
teardownDaemonListeners() {
|
||||
const sock = this.daemonSocket;
|
||||
if (sock && this._daemonHandler) {
|
||||
@@ -200,12 +224,26 @@ 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 'children_update':
|
||||
case 'stock_change':
|
||||
case 'familychanged':
|
||||
this.fetchStatus();
|
||||
this.queueStatusRefresh();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -486,6 +486,8 @@
|
||||
"name": "Name",
|
||||
"age": "Alter",
|
||||
"status": "Status",
|
||||
"marriageSatisfaction": "Ehe-Zufriedenheit",
|
||||
"marriageState": "Ehezustand",
|
||||
"none": "Kein Ehepartner vorhanden.",
|
||||
"search": "Ehepartner suchen",
|
||||
"found": "Ehepartner gefunden",
|
||||
@@ -517,6 +519,17 @@
|
||||
"progress": "Zuneigung",
|
||||
"jumpToPartyForm": "Hochzeitsfeier veranstalten (Nötig für Hochzeit und Kinder)"
|
||||
},
|
||||
"marriageState": {
|
||||
"stable": "Stabil",
|
||||
"strained": "Angespannt",
|
||||
"crisis": "Krise"
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Hausfrieden",
|
||||
"low": "Ruhig",
|
||||
"medium": "Unruhig",
|
||||
"high": "Belastet"
|
||||
},
|
||||
"relationships": {
|
||||
"name": "Name"
|
||||
},
|
||||
@@ -538,14 +551,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",
|
||||
@@ -1120,10 +1181,16 @@
|
||||
"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"
|
||||
},
|
||||
"attacks": {
|
||||
"target": "Angreifer",
|
||||
@@ -1136,7 +1203,8 @@
|
||||
"assassin": "Attentat",
|
||||
"sabotage": "Sabotage",
|
||||
"corrupt_politician": "Korruption",
|
||||
"rob": "Raub"
|
||||
"rob": "Raub",
|
||||
"investigate_affair": "Liebschaft untersuchen"
|
||||
},
|
||||
"targets": {
|
||||
"house": "Wohnhaus",
|
||||
@@ -1145,7 +1213,14 @@
|
||||
"goals": {
|
||||
"elect": "Amtseinsetzung",
|
||||
"taxIncrease": "Steuern erhöhen",
|
||||
"taxDecrease": "Steuern senken"
|
||||
"taxDecrease": "Steuern senken",
|
||||
"expose": "Aufdecken",
|
||||
"blackmail": "Erpressen"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Ausstehend",
|
||||
"resolved": "Abgeschlossen",
|
||||
"failed": "Gescheitert"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,6 +434,11 @@
|
||||
"baptism": "Baptize",
|
||||
"notBaptized": "Not yet baptized",
|
||||
"baptismNotice": "This child has not been baptized yet and therefore has no name.",
|
||||
"legitimacy": {
|
||||
"legitimate": "Legitimate",
|
||||
"acknowledged_bastard": "Acknowledged illegitimate",
|
||||
"hidden_bastard": "Illegitimate"
|
||||
},
|
||||
"details": {
|
||||
"title": "Child Details"
|
||||
}
|
||||
@@ -449,6 +454,8 @@
|
||||
}
|
||||
},
|
||||
"spouse": {
|
||||
"marriageSatisfaction": "Marriage Satisfaction",
|
||||
"marriageState": "Marriage State",
|
||||
"wooing": {
|
||||
"cancel": "Cancel wooing",
|
||||
"cancelConfirm": "Do you really want to cancel wooing? Progress will be lost.",
|
||||
@@ -457,6 +464,65 @@
|
||||
"cancelTooSoon": "You can only cancel wooing after 24 hours."
|
||||
}
|
||||
},
|
||||
"marriageState": {
|
||||
"stable": "Stable",
|
||||
"strained": "Strained",
|
||||
"crisis": "Crisis"
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Household Tension",
|
||||
"low": "Calm",
|
||||
"medium": "Uneasy",
|
||||
"high": "Strained"
|
||||
},
|
||||
"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.",
|
||||
@@ -604,6 +670,60 @@
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,6 +470,8 @@
|
||||
"name": "Nombre",
|
||||
"age": "Edad",
|
||||
"status": "Estado",
|
||||
"marriageSatisfaction": "Satisfacción matrimonial",
|
||||
"marriageState": "Estado del matrimonio",
|
||||
"none": "No hay cónyuge.",
|
||||
"search": "Buscar pareja",
|
||||
"found": "Pareja encontrada",
|
||||
@@ -501,6 +503,17 @@
|
||||
"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",
|
||||
"low": "Calmo",
|
||||
"medium": "Inquieto",
|
||||
"high": "Tenso"
|
||||
},
|
||||
"relationships": {
|
||||
"name": "Nombre"
|
||||
},
|
||||
@@ -522,14 +535,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",
|
||||
@@ -1008,10 +1069,16 @@
|
||||
"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"
|
||||
},
|
||||
"attacks": {
|
||||
"target": "Atacante",
|
||||
@@ -1024,7 +1091,8 @@
|
||||
"assassin": "Atentado",
|
||||
"sabotage": "Sabotaje",
|
||||
"corrupt_politician": "Corrupción",
|
||||
"rob": "Robo"
|
||||
"rob": "Robo",
|
||||
"investigate_affair": "Investigar relación"
|
||||
},
|
||||
"targets": {
|
||||
"house": "Vivienda",
|
||||
@@ -1033,7 +1101,14 @@
|
||||
"goals": {
|
||||
"elect": "Nombramiento",
|
||||
"taxIncrease": "Subir impuestos",
|
||||
"taxDecrease": "Bajar impuestos"
|
||||
"taxDecrease": "Bajar impuestos",
|
||||
"expose": "Exponer",
|
||||
"blackmail": "Chantajear"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pendiente",
|
||||
"resolved": "Resuelto",
|
||||
"failed": "Fallido"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,15 @@
|
||||
<td>{{ $t('falukant.family.spouse.status') }}</td>
|
||||
<td>{{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}</td>
|
||||
</tr>
|
||||
<tr v-if="relationships[0].marriageSatisfaction != null">
|
||||
<td>{{ $t('falukant.family.spouse.marriageSatisfaction') }}</td>
|
||||
<td>
|
||||
{{ relationships[0].marriageSatisfaction }}
|
||||
<span class="inline-status-pill" :class="`inline-status-pill--${relationships[0].marriageState || 'stable'}`">
|
||||
{{ $t('falukant.family.marriageState.' + (relationships[0].marriageState || 'stable')) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="relationships[0].relationshipType === 'wooing'">
|
||||
<td>{{ $t('falukant.family.spouse.progress') }}</td>
|
||||
<td>
|
||||
@@ -123,6 +132,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="marriageSatisfaction != null || householdTension" class="marriage-overview surface-card">
|
||||
<div class="marriage-overview__item" v-if="marriageSatisfaction != null">
|
||||
<span class="marriage-overview__label">{{ $t('falukant.family.spouse.marriageSatisfaction') }}</span>
|
||||
<strong>{{ marriageSatisfaction }}</strong>
|
||||
</div>
|
||||
<div class="marriage-overview__item" v-if="marriageState">
|
||||
<span class="marriage-overview__label">{{ $t('falukant.family.spouse.marriageState') }}</span>
|
||||
<span class="inline-status-pill" :class="`inline-status-pill--${marriageState}`">
|
||||
{{ $t('falukant.family.marriageState.' + marriageState) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="marriage-overview__item" v-if="householdTension">
|
||||
<span class="marriage-overview__label">{{ $t('falukant.family.householdTension.label') }}</span>
|
||||
<span class="inline-status-pill" :class="`inline-status-pill--${householdTension}`">
|
||||
{{ $t('falukant.family.householdTension.' + householdTension) }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="children-section">
|
||||
<h3>{{ $t('falukant.family.children.title') }}</h3>
|
||||
<div v-if="children && children.length > 0" class="children-container">
|
||||
@@ -141,6 +169,9 @@
|
||||
<tr v-for="(child, index) in children" :key="index">
|
||||
<td v-if="child.hasName">
|
||||
{{ child.name }}
|
||||
<span v-if="child.legitimacy && child.legitimacy !== 'legitimate'" class="child-origin-badge">
|
||||
{{ $t('falukant.family.children.legitimacy.' + child.legitimacy) }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-else>
|
||||
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism')
|
||||
@@ -177,17 +208,106 @@
|
||||
<!-- Liebhaber / Geliebte -->
|
||||
<div class="lovers-section">
|
||||
<h3>{{ $t('falukant.family.lovers.title') }}</h3>
|
||||
<div v-if="lovers && lovers.length > 0">
|
||||
<ul>
|
||||
<li v-for="(lover, idx) in lovers" :key="idx">
|
||||
{{ $t('falukant.titles.' + lover.gender + '.' + lover.title) }} {{ lover.name }}
|
||||
({{ $t('falukant.family.lovers.affection') }}: {{ lover.affection }})
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="lovers && lovers.length > 0" class="lovers-grid">
|
||||
<article v-for="lover in lovers" :key="lover.relationshipId" class="lover-card surface-card">
|
||||
<div class="lover-card__header">
|
||||
<div>
|
||||
<strong>{{ $t('falukant.titles.' + lover.gender + '.' + lover.title) }} {{ lover.name }}</strong>
|
||||
<div class="lover-card__role">
|
||||
{{ $t('falukant.family.lovers.role.' + (lover.role || 'lover')) }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="inline-status-pill" :class="`inline-status-pill--${lover.riskState || 'low'}`">
|
||||
{{ $t('falukant.family.lovers.risk.' + (lover.riskState || 'low')) }}
|
||||
</span>
|
||||
</div>
|
||||
<dl class="lover-card__stats">
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.affection') }}</dt>
|
||||
<dd>{{ lover.affection }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.visibility') }}</dt>
|
||||
<dd>{{ lover.visibility }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.discretion') }}</dt>
|
||||
<dd>{{ lover.discretion }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.maintenance') }}</dt>
|
||||
<dd>{{ lover.maintenanceLevel }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.monthlyCost') }}</dt>
|
||||
<dd>{{ formatCost(lover.monthlyCost || 0) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.statusFit') }}</dt>
|
||||
<dd>{{ lover.statusFit }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="lover-card__meta">
|
||||
<span v-if="lover.acknowledged" class="lover-meta-badge">
|
||||
{{ $t('falukant.family.lovers.acknowledged') }}
|
||||
</span>
|
||||
<span v-if="lover.monthsUnderfunded > 0" class="lover-meta-badge lover-meta-badge--warning">
|
||||
{{ $t('falukant.family.lovers.underfunded', { count: lover.monthsUnderfunded }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="lover-card__actions">
|
||||
<button class="button button--secondary" @click="setLoverMaintenance(lover, 25)">
|
||||
{{ $t('falukant.family.lovers.actions.maintenanceLow') }}
|
||||
</button>
|
||||
<button class="button button--secondary" @click="setLoverMaintenance(lover, 50)">
|
||||
{{ $t('falukant.family.lovers.actions.maintenanceMedium') }}
|
||||
</button>
|
||||
<button class="button button--secondary" @click="setLoverMaintenance(lover, 75)">
|
||||
{{ $t('falukant.family.lovers.actions.maintenanceHigh') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!lover.acknowledged"
|
||||
class="button button--secondary"
|
||||
@click="acknowledgeLover(lover)"
|
||||
>
|
||||
{{ $t('falukant.family.lovers.actions.acknowledge') }}
|
||||
</button>
|
||||
<button class="button button--danger" @click="endLoverRelationship(lover)">
|
||||
{{ $t('falukant.family.lovers.actions.end') }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $t('falukant.family.lovers.none') }}</p>
|
||||
</div>
|
||||
<div class="lover-candidates surface-card">
|
||||
<h4>{{ $t('falukant.family.lovers.candidates.title') }}</h4>
|
||||
<div v-if="possibleLovers && possibleLovers.length > 0" class="lover-candidates__grid">
|
||||
<article v-for="candidate in possibleLovers" :key="candidate.characterId" class="lover-candidate-card">
|
||||
<div class="lover-candidate-card__main">
|
||||
<strong>{{ $t('falukant.titles.' + candidate.gender + '.' + candidate.title) }} {{ candidate.name }}</strong>
|
||||
<span>{{ $t('falukant.family.spouse.age') }}: {{ candidate.age }}</span>
|
||||
<span>{{ $t('falukant.family.lovers.statusFit') }}: {{ candidate.statusFit }}</span>
|
||||
<span>{{ $t('falukant.family.lovers.monthlyCost') }}: {{ formatCost(candidate.estimatedMonthlyCost || 0) }}</span>
|
||||
</div>
|
||||
<div class="lover-candidate-card__actions">
|
||||
<label class="lover-candidate-card__label">
|
||||
{{ $t('falukant.family.lovers.candidates.roleLabel') }}
|
||||
</label>
|
||||
<select class="lover-candidate-card__select" v-model="candidateRoles[candidate.characterId]">
|
||||
<option value="secret_affair">{{ $t('falukant.family.lovers.role.secret_affair') }}</option>
|
||||
<option value="lover">{{ $t('falukant.family.lovers.role.lover') }}</option>
|
||||
<option value="mistress_or_favorite">{{ $t('falukant.family.lovers.role.mistress_or_favorite') }}</option>
|
||||
</select>
|
||||
<button class="button button--secondary" @click="createLoverRelationship(candidate)">
|
||||
{{ $t('falukant.family.lovers.actions.start') }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else>{{ $t('falukant.family.lovers.candidates.none') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,7 +321,7 @@ import ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
|
||||
import Character3D from '@/components/Character3D.vue'
|
||||
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js'
|
||||
import { confirmAction, showError, showInfo, showSuccess } from '@/utils/feedback.js'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const WOOING_PROGRESS_TARGET = 70
|
||||
@@ -218,6 +338,8 @@ export default {
|
||||
relationships: [],
|
||||
children: [],
|
||||
lovers: [],
|
||||
possibleLovers: [],
|
||||
candidateRoles: {},
|
||||
deathPartners: [],
|
||||
proposals: [],
|
||||
selectedProposalId: null,
|
||||
@@ -226,11 +348,25 @@ export default {
|
||||
moodAffects: [],
|
||||
characterAffects: [],
|
||||
ownCharacter: null,
|
||||
selectedChild: null
|
||||
marriageSatisfaction: null,
|
||||
marriageState: null,
|
||||
householdTension: null,
|
||||
selectedChild: null,
|
||||
pendingFamilyRefresh: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket'])
|
||||
...mapState(['socket', 'daemonSocket', 'user'])
|
||||
},
|
||||
watch: {
|
||||
socket(newVal, oldVal) {
|
||||
if (oldVal) this.teardownSocketEvents();
|
||||
if (newVal) this.setupSocketEvents();
|
||||
},
|
||||
daemonSocket(newVal, oldVal) {
|
||||
if (oldVal) this.teardownDaemonListeners();
|
||||
if (newVal) this.setupDaemonListeners();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadOwnCharacter();
|
||||
@@ -239,25 +375,110 @@ export default {
|
||||
await this.loadMoodAffects();
|
||||
await this.loadCharacterAffects();
|
||||
this.setupSocketEvents();
|
||||
this.setupDaemonListeners();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.teardownSocketEvents();
|
||||
this.teardownDaemonListeners();
|
||||
if (this.pendingFamilyRefresh) {
|
||||
clearTimeout(this.pendingFamilyRefresh);
|
||||
this.pendingFamilyRefresh = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setupSocketEvents() {
|
||||
this.teardownSocketEvents();
|
||||
if (this.socket) {
|
||||
this.socket.on('falukantUpdateStatus', (data) => {
|
||||
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
});
|
||||
this.socket.on('familychanged', (data) => {
|
||||
this.handleEvent({ event: 'familychanged', ...data });
|
||||
});
|
||||
this._falukantUpdateStatusHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
this._falukantUpdateFamilyHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
|
||||
this._childrenUpdateHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
|
||||
this._familyChangedHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
|
||||
|
||||
this.socket.on('falukantUpdateStatus', this._falukantUpdateStatusHandler);
|
||||
this.socket.on('falukantUpdateFamily', this._falukantUpdateFamilyHandler);
|
||||
this.socket.on('children_update', this._childrenUpdateHandler);
|
||||
this.socket.on('familychanged', this._familyChangedHandler);
|
||||
} else {
|
||||
setTimeout(() => this.setupSocketEvents(), 1000);
|
||||
}
|
||||
},
|
||||
teardownSocketEvents() {
|
||||
if (!this.socket) return;
|
||||
if (this._falukantUpdateStatusHandler) this.socket.off('falukantUpdateStatus', this._falukantUpdateStatusHandler);
|
||||
if (this._falukantUpdateFamilyHandler) this.socket.off('falukantUpdateFamily', this._falukantUpdateFamilyHandler);
|
||||
if (this._childrenUpdateHandler) this.socket.off('children_update', this._childrenUpdateHandler);
|
||||
if (this._familyChangedHandler) this.socket.off('familychanged', this._familyChangedHandler);
|
||||
},
|
||||
setupDaemonListeners() {
|
||||
this.teardownDaemonListeners();
|
||||
if (!this.daemonSocket) return;
|
||||
this._daemonFamilyHandler = (event) => {
|
||||
if (event.data === 'ping') return;
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if ([
|
||||
'falukantUpdateStatus',
|
||||
'falukantUpdateFamily',
|
||||
'children_update',
|
||||
'familychanged',
|
||||
'falukant_family_scandal_hint'
|
||||
].includes(message.event)) {
|
||||
this.handleEvent(message);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
this.daemonSocket.addEventListener('message', this._daemonFamilyHandler);
|
||||
},
|
||||
teardownDaemonListeners() {
|
||||
if (this.daemonSocket && this._daemonFamilyHandler) {
|
||||
this.daemonSocket.removeEventListener('message', this._daemonFamilyHandler);
|
||||
this._daemonFamilyHandler = null;
|
||||
}
|
||||
},
|
||||
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));
|
||||
},
|
||||
queueFamilyRefresh({ reloadCharacter = false } = {}) {
|
||||
if (this.pendingFamilyRefresh) {
|
||||
clearTimeout(this.pendingFamilyRefresh);
|
||||
}
|
||||
|
||||
this.pendingFamilyRefresh = setTimeout(async () => {
|
||||
this.pendingFamilyRefresh = null;
|
||||
await this.loadFamilyData();
|
||||
if (reloadCharacter) {
|
||||
await this.loadOwnCharacter();
|
||||
}
|
||||
}, 120);
|
||||
},
|
||||
handleEvent(eventData) {
|
||||
if (!this.matchesCurrentUser(eventData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (eventData.event) {
|
||||
case 'falukantUpdateStatus':
|
||||
case 'familychanged':
|
||||
this.loadFamilyData();
|
||||
this.queueFamilyRefresh({ reloadCharacter: true });
|
||||
break;
|
||||
case 'children_update':
|
||||
this.queueFamilyRefresh({ reloadCharacter: false });
|
||||
break;
|
||||
case 'falukantUpdateFamily':
|
||||
if (eventData.reason === 'scandal') {
|
||||
showInfo(this, this.$t('falukant.family.notifications.scandal'));
|
||||
} else if (eventData.reason === 'lover_birth') {
|
||||
showInfo(this, this.$t('falukant.family.notifications.loverBirth'));
|
||||
}
|
||||
this.queueFamilyRefresh({ reloadCharacter: eventData.reason === 'monthly' || eventData.reason === 'daily' });
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -267,8 +488,13 @@ export default {
|
||||
this.relationships = response.data.relationships;
|
||||
this.children = response.data.children;
|
||||
this.lovers = response.data.lovers;
|
||||
this.possibleLovers = response.data.possibleLovers || [];
|
||||
this.syncCandidateRoles();
|
||||
this.proposals = response.data.possiblePartners;
|
||||
this.deathPartners = response.data.deathPartners;
|
||||
this.marriageSatisfaction = response.data.marriageSatisfaction;
|
||||
this.marriageState = response.data.marriageState;
|
||||
this.householdTension = response.data.householdTension;
|
||||
} catch (error) {
|
||||
console.error('Error loading family data:', error);
|
||||
}
|
||||
@@ -305,6 +531,73 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async setLoverMaintenance(lover, maintenanceLevel) {
|
||||
try {
|
||||
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/maintenance`, {
|
||||
maintenanceLevel
|
||||
});
|
||||
await this.loadFamilyData();
|
||||
showSuccess(this, this.$t('falukant.family.lovers.actions.maintenanceSuccess'));
|
||||
} catch (error) {
|
||||
console.error('Error updating lover maintenance:', error);
|
||||
showError(this, this.$t('falukant.family.lovers.actions.maintenanceError'));
|
||||
}
|
||||
},
|
||||
|
||||
async acknowledgeLover(lover) {
|
||||
try {
|
||||
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/acknowledge`);
|
||||
await this.loadFamilyData();
|
||||
showSuccess(this, this.$t('falukant.family.lovers.actions.acknowledgeSuccess'));
|
||||
} catch (error) {
|
||||
console.error('Error acknowledging lover:', error);
|
||||
showError(this, this.$t('falukant.family.lovers.actions.acknowledgeError'));
|
||||
}
|
||||
},
|
||||
|
||||
async endLoverRelationship(lover) {
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: this.$t('falukant.family.lovers.actions.end'),
|
||||
message: this.$t('falukant.family.lovers.actions.endConfirm')
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/end`);
|
||||
await this.loadFamilyData();
|
||||
showSuccess(this, this.$t('falukant.family.lovers.actions.endSuccess'));
|
||||
} catch (error) {
|
||||
console.error('Error ending lover relationship:', error);
|
||||
showError(this, this.$t('falukant.family.lovers.actions.endError'));
|
||||
}
|
||||
},
|
||||
|
||||
async createLoverRelationship(candidate) {
|
||||
try {
|
||||
await apiClient.post('/api/falukant/family/lover', {
|
||||
targetCharacterId: candidate.characterId,
|
||||
loverRole: this.getCandidateRole(candidate.characterId)
|
||||
});
|
||||
await this.loadFamilyData();
|
||||
showSuccess(this, this.$t('falukant.family.lovers.actions.startSuccess'));
|
||||
} catch (error) {
|
||||
console.error('Error creating lover relationship:', error);
|
||||
showError(this, this.$t('falukant.family.lovers.actions.startError'));
|
||||
}
|
||||
},
|
||||
|
||||
syncCandidateRoles() {
|
||||
const nextRoles = {};
|
||||
for (const candidate of this.possibleLovers) {
|
||||
nextRoles[candidate.characterId] = this.candidateRoles[candidate.characterId] || 'secret_affair';
|
||||
}
|
||||
this.candidateRoles = nextRoles;
|
||||
},
|
||||
|
||||
getCandidateRole(characterId) {
|
||||
return this.candidateRoles[characterId] || 'secret_affair';
|
||||
},
|
||||
|
||||
formatCost(value) {
|
||||
return new Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
|
||||
},
|
||||
@@ -442,16 +735,6 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
handleDaemonMessage(event) {
|
||||
if (event.data === 'ping') {
|
||||
return;
|
||||
}
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.event === 'children_update') {
|
||||
this.loadFamilyData();
|
||||
}
|
||||
},
|
||||
|
||||
getEffect(gift) {
|
||||
// aktueller Partner
|
||||
const partner = this.relationships[0].character2;
|
||||
@@ -512,6 +795,196 @@ export default {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.marriage-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.marriage-overview__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.marriage-overview__label {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.inline-status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.inline-status-pill--stable,
|
||||
.inline-status-pill--low {
|
||||
background: rgba(66, 140, 87, 0.16);
|
||||
color: #2e6b42;
|
||||
}
|
||||
|
||||
.inline-status-pill--strained,
|
||||
.inline-status-pill--medium {
|
||||
background: rgba(230, 172, 52, 0.18);
|
||||
color: #875e08;
|
||||
}
|
||||
|
||||
.inline-status-pill--crisis,
|
||||
.inline-status-pill--high {
|
||||
background: rgba(188, 84, 61, 0.16);
|
||||
color: #9a3c26;
|
||||
}
|
||||
|
||||
.child-origin-badge {
|
||||
margin-left: 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(131, 104, 73, 0.14);
|
||||
color: #6a4d2f;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lovers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.lover-card {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.lover-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.lover-card__role {
|
||||
margin-top: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.lover-card__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lover-card__stats div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.lover-card__stats dt {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.lover-card__stats dd {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lover-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.lover-card__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.lover-card__actions .button {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lover-candidates {
|
||||
margin-top: 18px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.lover-candidates__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.lover-candidate-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 250, 243, 0.88);
|
||||
}
|
||||
|
||||
.lover-candidate-card__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.lover-candidate-card__main span {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.lover-candidate-card__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.lover-candidate-card__label {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lover-candidate-card__select {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.lover-meta-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
background: rgba(66, 140, 87, 0.14);
|
||||
color: #2e6b42;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lover-meta-badge--warning {
|
||||
background: rgba(188, 84, 61, 0.14);
|
||||
color: #9a3c26;
|
||||
}
|
||||
|
||||
.self-character-3d {
|
||||
width: 250px;
|
||||
height: 350px;
|
||||
|
||||
@@ -213,10 +213,11 @@ export default {
|
||||
productions: [],
|
||||
potentialHeirs: [],
|
||||
loadingHeirs: false,
|
||||
pendingOverviewRefresh: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket']),
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
getAvatarStyle() {
|
||||
if (!this.falukantUser || !this.falukantUser.character) return {};
|
||||
const { gender, age } = this.falukantUser.character;
|
||||
@@ -335,12 +336,18 @@ export default {
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.pendingOverviewRefresh) {
|
||||
clearTimeout(this.pendingOverviewRefresh);
|
||||
this.pendingOverviewRefresh = null;
|
||||
}
|
||||
if (this.daemonSocket) {
|
||||
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.off("falukantUserUpdated", this.fetchFalukantUser);
|
||||
this.socket.off("falukantUpdateStatus");
|
||||
this.socket.off("falukantUpdateFamily");
|
||||
this.socket.off("children_update");
|
||||
this.socket.off("falukantBranchUpdate");
|
||||
this.socket.off("stock_change");
|
||||
}
|
||||
@@ -352,6 +359,12 @@ export default {
|
||||
this.socket.on("falukantUpdateStatus", (data) => {
|
||||
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
});
|
||||
this.socket.on("falukantUpdateFamily", (data) => {
|
||||
this.handleEvent({ event: 'falukantUpdateFamily', ...data });
|
||||
});
|
||||
this.socket.on("children_update", (data) => {
|
||||
this.handleEvent({ event: 'children_update', ...data });
|
||||
});
|
||||
this.socket.on("falukantBranchUpdate", (data) => {
|
||||
this.handleEvent({ event: 'falukantBranchUpdate', ...data });
|
||||
});
|
||||
@@ -387,16 +400,37 @@ export default {
|
||||
console.error('Overview: Error processing daemon message:', err);
|
||||
}
|
||||
},
|
||||
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));
|
||||
},
|
||||
queueOverviewRefresh() {
|
||||
if (this.pendingOverviewRefresh) {
|
||||
clearTimeout(this.pendingOverviewRefresh);
|
||||
}
|
||||
this.pendingOverviewRefresh = setTimeout(async () => {
|
||||
this.pendingOverviewRefresh = null;
|
||||
await this.fetchFalukantUser();
|
||||
if (this.falukantUser?.character) {
|
||||
await this.fetchProductions();
|
||||
await this.fetchAllStock();
|
||||
}
|
||||
}, 120);
|
||||
},
|
||||
async handleEvent(eventData) {
|
||||
if (!this.falukantUser?.character) return;
|
||||
if (!this.matchesCurrentUser(eventData)) return;
|
||||
switch (eventData.event) {
|
||||
case 'falukantUpdateStatus':
|
||||
case 'falukantUpdateFamily':
|
||||
case 'children_update':
|
||||
case 'falukantBranchUpdate':
|
||||
await this.fetchFalukantUser();
|
||||
if (this.falukantUser?.character) {
|
||||
await this.fetchProductions();
|
||||
await this.fetchAllStock();
|
||||
}
|
||||
this.queueOverviewRefresh();
|
||||
break;
|
||||
case 'production_ready':
|
||||
case 'production_started':
|
||||
|
||||
@@ -80,6 +80,18 @@
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label v-if="selectedType && selectedType.tr === 'investigate_affair'" class="form-label">
|
||||
{{ $t('falukant.underground.activities.affairGoal') }}
|
||||
<select v-model="newAffairGoal" class="form-control">
|
||||
<option value="expose">
|
||||
{{ $t('falukant.underground.goals.expose') }}
|
||||
</option>
|
||||
<option value="blackmail">
|
||||
{{ $t('falukant.underground.goals.blackmail') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button class="btn-create-activity" :disabled="!canCreate" @click="createActivity">
|
||||
{{ $t('falukant.underground.activities.create') }}
|
||||
</button>
|
||||
@@ -96,6 +108,7 @@
|
||||
<th>{{ $t('falukant.underground.activities.type') }}</th>
|
||||
<th>{{ $t('falukant.underground.activities.victim') }}</th>
|
||||
<th>{{ $t('falukant.underground.activities.cost') }}</th>
|
||||
<th>{{ $t('falukant.underground.activities.status') }}</th>
|
||||
<th>{{ $t('falukant.underground.activities.additionalInfo') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -105,16 +118,73 @@
|
||||
<td>{{ act.victimName }}</td>
|
||||
<td>{{ formatCost(act.cost) }}</td>
|
||||
<td>
|
||||
<template v-if="act.type === 'sabotage'">
|
||||
{{ $t(`falukant.underground.targets.${act.target}`) }}
|
||||
</template>
|
||||
<template v-else-if="act.type === 'corrupt_politician'">
|
||||
{{ $t(`falukant.underground.goals.${act.goal}`) }}
|
||||
</template>
|
||||
<div class="activity-status">
|
||||
<span class="activity-status__badge" :class="`is-${act.status}`">
|
||||
{{ $t(`falukant.underground.status.${act.status}`) }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="activity-details">
|
||||
<div class="activity-details__summary">
|
||||
<template v-if="act.type === 'sabotage'">
|
||||
{{ $t(`falukant.underground.targets.${act.target}`) }}
|
||||
</template>
|
||||
<template v-else-if="act.type === 'corrupt_politician'">
|
||||
{{ $t(`falukant.underground.goals.${act.goal}`) }}
|
||||
</template>
|
||||
<template v-else-if="act.type === 'investigate_affair'">
|
||||
{{ $t(`falukant.underground.goals.${act.goal}`) }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="act.type === 'investigate_affair' && hasAffairDetails(act)">
|
||||
<div
|
||||
v-if="getAffairDiscoveries(act).length"
|
||||
class="activity-details__block"
|
||||
>
|
||||
<div class="activity-details__label">
|
||||
{{ $t('falukant.underground.activities.discoveries') }}
|
||||
</div>
|
||||
<ul class="activity-details__list">
|
||||
<li v-for="entry in getAffairDiscoveries(act)" :key="entry">
|
||||
{{ entry }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasAffairImpact(act)"
|
||||
class="activity-details__block activity-details__metrics"
|
||||
>
|
||||
<span
|
||||
v-if="hasNumericValue(act.additionalInfo?.visibilityDelta)"
|
||||
class="activity-metric"
|
||||
>
|
||||
{{ $t('falukant.underground.activities.visibilityDelta') }}:
|
||||
{{ formatSignedNumber(act.additionalInfo.visibilityDelta) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="hasNumericValue(act.additionalInfo?.reputationDelta)"
|
||||
class="activity-metric"
|
||||
>
|
||||
{{ $t('falukant.underground.activities.reputationDelta') }}:
|
||||
{{ formatSignedNumber(act.additionalInfo.reputationDelta) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="act.additionalInfo?.blackmailAmount > 0"
|
||||
class="activity-metric"
|
||||
>
|
||||
{{ $t('falukant.underground.activities.blackmailAmount') }}:
|
||||
{{ formatCost(act.additionalInfo.blackmailAmount) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!activities.length">
|
||||
<td colspan="4">{{ $t('falukant.underground.activities.none') }}</td>
|
||||
<td colspan="5">{{ $t('falukant.underground.activities.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -179,7 +249,8 @@ export default {
|
||||
victimSearchTimeout: null,
|
||||
newPoliticalTargets: [],
|
||||
newSabotageTarget: 'house',
|
||||
newCorruptGoal: 'elect'
|
||||
newCorruptGoal: 'elect',
|
||||
newAffairGoal: 'expose'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -202,6 +273,12 @@ export default {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selectedType?.tr === 'investigate_affair' &&
|
||||
!this.newAffairGoal
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
@@ -234,7 +311,6 @@ export default {
|
||||
}
|
||||
},
|
||||
async searchVictims(q) {
|
||||
console.log('Searching victims for:', q);
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/users/search', {
|
||||
params: { q }
|
||||
@@ -264,6 +340,9 @@ export default {
|
||||
payload.politicalTargets = this.newPoliticalTargets;
|
||||
}
|
||||
}
|
||||
if (this.selectedType.tr === 'investigate_affair') {
|
||||
payload.goal = this.newAffairGoal;
|
||||
}
|
||||
try {
|
||||
await apiClient.post(
|
||||
'/api/falukant/underground/activities',
|
||||
@@ -273,6 +352,7 @@ export default {
|
||||
this.newPoliticalTargets = [];
|
||||
this.newSabotageTarget = 'house';
|
||||
this.newCorruptGoal = 'elect';
|
||||
this.newAffairGoal = 'expose';
|
||||
await this.loadActivities();
|
||||
} catch (err) {
|
||||
console.error('Error creating activity', err);
|
||||
@@ -287,8 +367,6 @@ export default {
|
||||
},
|
||||
|
||||
async loadActivities() {
|
||||
return; // TODO: Aktivierung der Methode geplant
|
||||
/* Temporär deaktiviert:
|
||||
this.loading.activities = true;
|
||||
try {
|
||||
const { data } = await apiClient.get(
|
||||
@@ -298,7 +376,6 @@ export default {
|
||||
} finally {
|
||||
this.loading.activities = false;
|
||||
}
|
||||
*/
|
||||
},
|
||||
|
||||
async loadAttacks() {
|
||||
@@ -328,6 +405,48 @@ export default {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(v);
|
||||
},
|
||||
|
||||
hasNumericValue(value) {
|
||||
return typeof value === 'number' && !Number.isNaN(value);
|
||||
},
|
||||
|
||||
formatSignedNumber(value) {
|
||||
if (!this.hasNumericValue(value)) return '0';
|
||||
return new Intl.NumberFormat(navigator.language, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
signDisplay: 'always'
|
||||
}).format(value);
|
||||
},
|
||||
|
||||
getAffairDiscoveries(activity) {
|
||||
const discoveries = activity?.additionalInfo?.discoveries;
|
||||
if (Array.isArray(discoveries)) {
|
||||
return discoveries.filter(Boolean).map(entry => String(entry));
|
||||
}
|
||||
if (discoveries && typeof discoveries === 'object') {
|
||||
return Object.entries(discoveries)
|
||||
.filter(([, value]) => value !== null && value !== undefined && value !== '')
|
||||
.map(([key, value]) => `${key}: ${value}`);
|
||||
}
|
||||
if (typeof discoveries === 'string' && discoveries.trim()) {
|
||||
return [discoveries.trim()];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
hasAffairImpact(activity) {
|
||||
const info = activity?.additionalInfo || {};
|
||||
return (
|
||||
this.hasNumericValue(info.visibilityDelta) ||
|
||||
this.hasNumericValue(info.reputationDelta) ||
|
||||
(typeof info.blackmailAmount === 'number' && info.blackmailAmount > 0)
|
||||
);
|
||||
},
|
||||
|
||||
hasAffairDetails(activity) {
|
||||
return this.getAffairDiscoveries(activity).length > 0 || this.hasAffairImpact(activity);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -407,6 +526,7 @@ h2 {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
@@ -432,4 +552,72 @@ h2 {
|
||||
.suggestions li:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.activity-status__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
background: #ececec;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.activity-status__badge.is-pending {
|
||||
background: #fff2cc;
|
||||
color: #7a5600;
|
||||
}
|
||||
|
||||
.activity-status__badge.is-resolved {
|
||||
background: #dff3e2;
|
||||
color: #25613a;
|
||||
}
|
||||
|
||||
.activity-status__badge.is-failed {
|
||||
background: #f8d7da;
|
||||
color: #8a2632;
|
||||
}
|
||||
|
||||
.activity-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.activity-details__summary {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity-details__block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.activity-details__label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.activity-details__list {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
|
||||
.activity-details__metrics {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.activity-metric {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: #f0f0f0;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user