1931 lines
74 KiB
Vue
1931 lines
74 KiB
Vue
<template>
|
|
<div class="family-view">
|
|
<StatusBar />
|
|
<div class="family-layout">
|
|
<div class="family-content">
|
|
<section class="family-hero surface-card">
|
|
<div>
|
|
<span class="family-kicker">{{ $t('falukant.family.title') }}</span>
|
|
<h2>{{ $t('falukant.family.title') }}</h2>
|
|
<p>{{ $t('falukant.family.heroIntro') }}</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section
|
|
v-if="pregnancy"
|
|
class="family-pregnancy surface-card"
|
|
>
|
|
<strong>{{ $t('falukant.family.pregnancy.banner') }}</strong>
|
|
<p>{{ $t('falukant.family.pregnancy.dueHint') }}: {{ formatPregnancyDue(pregnancy.dueAt) }}</p>
|
|
</section>
|
|
|
|
<section
|
|
v-if="debtorsPrison.active"
|
|
class="family-debt-warning surface-card"
|
|
:class="{ 'is-prison': debtorsPrison.inDebtorsPrison }"
|
|
>
|
|
<strong>
|
|
{{ debtorsPrison.inDebtorsPrison
|
|
? $t('falukant.bank.debtorsPrison.titlePrison')
|
|
: $t('falukant.bank.debtorsPrison.titleWarning') }}
|
|
</strong>
|
|
<p>
|
|
{{ debtorsPrison.inDebtorsPrison
|
|
? $t('falukant.family.debtorsPrison.familyImpact')
|
|
: $t('falukant.family.debtorsPrison.familyWarning') }}
|
|
</p>
|
|
</section>
|
|
|
|
<nav
|
|
class="family-summary-strip surface-card"
|
|
role="tablist"
|
|
:aria-label="$t('falukant.family.tabsAria')"
|
|
>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
:aria-selected="familyTab === 'partner'"
|
|
class="family-summary-chip"
|
|
:class="{ 'is-active': familyTab === 'partner' }"
|
|
:title="$t('falukant.family.tabs.partner')"
|
|
@click="familyTab = 'partner'"
|
|
>
|
|
<span class="family-summary-chip__label">{{ $t('falukant.family.summary.partnerChip') }}</span>
|
|
<span class="family-summary-chip__value">{{ partnerSummaryLine }}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
:aria-selected="familyTab === 'children'"
|
|
class="family-summary-chip"
|
|
:class="{ 'is-active': familyTab === 'children' }"
|
|
:title="$t('falukant.family.tabs.children')"
|
|
@click="familyTab = 'children'"
|
|
>
|
|
<span class="family-summary-chip__label">{{ $t('falukant.family.summary.childrenChip') }}</span>
|
|
<span class="family-summary-chip__value">{{ children.length }}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
:aria-selected="familyTab === 'lovers'"
|
|
class="family-summary-chip"
|
|
:class="{ 'is-active': familyTab === 'lovers' }"
|
|
:title="$t('falukant.family.tabs.lovers')"
|
|
@click="familyTab = 'lovers'"
|
|
>
|
|
<span class="family-summary-chip__label">{{ $t('falukant.family.summary.loversChip') }}</span>
|
|
<span class="family-summary-chip__value">{{ lovers.length }}</span>
|
|
</button>
|
|
</nav>
|
|
|
|
<div
|
|
v-show="familyTab === 'partner'"
|
|
class="family-tab-panel"
|
|
role="tabpanel"
|
|
>
|
|
<div class="spouse-section">
|
|
<h3>{{ $t('falukant.family.spouse.title') }}</h3>
|
|
<div v-if="relationships.length > 0" class="relationship-container">
|
|
<div class="relationship-row">
|
|
<div class="relationship">
|
|
<dl class="spouse-card__dl">
|
|
<div>
|
|
<dt>{{ $t('falukant.family.relationships.name') }}</dt>
|
|
<dd>
|
|
{{ $t('falukant.titles.' + relationships[0].character2.gender + '.' +
|
|
relationships[0].character2.nobleTitle) }}
|
|
{{ relationships[0].character2.firstName }}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt>{{ $t('falukant.family.spouse.age') }}</dt>
|
|
<dd>{{ relationships[0].character2.age }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>{{ $t('falukant.family.spouse.mood') }}</dt>
|
|
<dd>{{ relationships[0].character2.mood?.tr ? $t(`falukant.mood.${relationships[0].character2.mood.tr}`) : '—' }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>{{ $t('falukant.family.spouse.status') }}</dt>
|
|
<dd>{{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}</dd>
|
|
</div>
|
|
<div v-if="relationships[0].relationshipType === 'married' && relationships[0].marriageSatisfaction != null">
|
|
<dt>{{ $t('falukant.family.spouse.marriageSatisfaction') }}</dt>
|
|
<dd>
|
|
{{ 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>
|
|
</dd>
|
|
</div>
|
|
<div v-if="relationships[0].relationshipType === 'wooing'">
|
|
<dt>{{ $t('falukant.family.spouse.progress') }}</dt>
|
|
<dd>
|
|
<div class="progress">
|
|
<div class="progress-inner" :style="{
|
|
width: normalizeWooingProgress(relationships[0].progress) + '%',
|
|
backgroundColor: progressColor(relationships[0].progress)
|
|
}"></div>
|
|
</div>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
<div v-if="relationships[0].relationshipType === 'engaged'" class="spouse-card__cta">
|
|
<button type="button" class="button" @click="jumpToPartyForm">{{ $t('falukant.family.spouse.jumpToPartyForm')
|
|
}}</button>
|
|
</div>
|
|
<details
|
|
v-if="relationships[0].character2.traits?.length"
|
|
class="spouse-traits"
|
|
>
|
|
<summary>{{ $t('falukant.family.spouse.traitsToggle') }}</summary>
|
|
<ul>
|
|
<li v-for="trait in relationships[0].character2.traits" :key="trait.id">
|
|
{{ $t(`falukant.character.${trait.tr}`) }}
|
|
</li>
|
|
</ul>
|
|
</details>
|
|
</div>
|
|
<div class="character-media character-media--spouse">
|
|
<div
|
|
class="character-avatar character-avatar--spouse"
|
|
:style="getCharacterAvatarStyle(relationships[0].character2, 0.72)"
|
|
></div>
|
|
<div class="character-3d-frame character-3d-frame--spouse">
|
|
<Character3D
|
|
:gender="relationships[0].character2.gender"
|
|
:age="relationships[0].character2.age"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="relationships[0].relationshipType === 'wooing'" class="gifts-section">
|
|
<h3>{{ $t('falukant.family.spouse.wooing.gifts') }}</h3>
|
|
<table class="spouse-table">
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>{{ $t('falukant.family.spouse.wooing.gift') }}</th>
|
|
<th>{{ $t('falukant.family.spouse.wooing.effect') }}</th>
|
|
<th>{{ $t('falukant.family.spouse.wooing.value') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="gift in gifts" :key="gift.id">
|
|
<td>
|
|
<input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId" />
|
|
</td>
|
|
<td>{{ $t(`falukant.gifts.${gift.name}`) }}</td>
|
|
<td>{{ getEffect(gift) }}</td>
|
|
<td>{{ formatCost(gift.cost) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div class="wooing-actions">
|
|
<button @click="sendGift" class="button">{{ $t('falukant.family.spouse.wooing.sendGift') }}</button>
|
|
<button @click="cancelWooing" class="button button-secondary">{{ $t('falukant.family.spouse.wooing.cancel') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="proposals && proposals.length > 0">
|
|
<table class="spouse-table">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t('falukant.family.spouse.select') }}</th>
|
|
<th>{{ $t('falukant.family.spouse.name') }}</th>
|
|
<th>{{ $t('falukant.family.spouse.age') }}</th>
|
|
<th>{{ $t('falukant.family.spouse.marriagecost') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="proposal in proposals" :key="proposal.id">
|
|
<td><input type="radio" name="spouse" :value="proposal.proposedCharacterId"
|
|
v-model="selectedProposalId"></td>
|
|
<td>{{
|
|
$t(`falukant.titles.${proposal.proposedCharacterGender}.${proposal.proposedCharacterNobleTitle}`)
|
|
}} {{ proposal.proposedCharacterName }}</td>
|
|
<td>{{ proposal.proposedCharacterAge }}</td>
|
|
<td>{{ formatCost(proposal.cost) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div>{{ $t('falukant.family.spouse.notice') }}</div>
|
|
<div v-if="selectedProposalId">
|
|
<button @click="acceptProposal">{{ $t('falukant.family.spouse.accept') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<section v-if="marriageSatisfaction != null || householdTension" class="marriage-overview surface-card">
|
|
<div class="marriage-overview__item" v-if="relationships.length > 0 && relationships[0].relationshipType === 'married' && 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>
|
|
<div class="marriage-overview__item" v-if="householdTensionScore != null">
|
|
<span class="marriage-overview__label">{{ $t('falukant.family.householdTension.score') }}</span>
|
|
<strong>{{ householdTensionScore }}</strong>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-if="relationships.length > 0 && relationships[0].relationshipType === 'married'" class="marriage-actions surface-card">
|
|
<h3>{{ $t('falukant.family.marriageActions.title') }}</h3>
|
|
<div class="marriage-actions__buttons">
|
|
<button class="button button--secondary" @click="spendTimeWithSpouse">
|
|
{{ $t('falukant.family.marriageActions.spendTime') }} ({{ $t('falukant.family.marriageActions.costFree') }})
|
|
</button>
|
|
<button class="button button--secondary" @click="giftToSpouse('small')">
|
|
{{ $t('falukant.family.marriageActions.giftSmall') }} ({{ formatCost(marriageGiftCosts.small) }})
|
|
</button>
|
|
<button class="button button--secondary" @click="giftToSpouse('decent')">
|
|
{{ $t('falukant.family.marriageActions.giftDecent') }} ({{ formatCost(marriageGiftCosts.decent) }})
|
|
</button>
|
|
<button class="button button--secondary" @click="giftToSpouse('lavish')">
|
|
{{ $t('falukant.family.marriageActions.giftLavish') }} ({{ formatCost(marriageGiftCosts.lavish) }})
|
|
</button>
|
|
<button class="button button--secondary" @click="reconcileMarriage">
|
|
{{ $t('falukant.family.marriageActions.reconcile') }} ({{ $t('falukant.family.marriageActions.costFree') }})
|
|
</button>
|
|
</div>
|
|
<details class="marriage-actions__help">
|
|
<summary>{{ $t('falukant.family.marriageActions.effectHintTitle') }}</summary>
|
|
<ul>
|
|
<li>{{ $t('falukant.family.marriageActions.effectSpendTime', { minutes: 15 }) }}</li>
|
|
<li>{{ $t('falukant.family.marriageActions.effectGifts', { small: formatCost(marriageGiftCosts.small), decent: formatCost(marriageGiftCosts.decent), lavish: formatCost(marriageGiftCosts.lavish) }) }}</li>
|
|
<li>{{ $t('falukant.family.marriageActions.effectReconcile') }}</li>
|
|
<li>{{ $t('falukant.family.marriageActions.effectProduction', { minutes: 15 }) }}</li>
|
|
</ul>
|
|
</details>
|
|
<div v-if="householdTensionReasons.length > 0" class="marriage-actions__reasons">
|
|
<span class="marriage-actions__reasons-label">{{ $t('falukant.family.householdTension.reasonsLabel') }}</span>
|
|
<div class="marriage-actions__reason-list">
|
|
<span v-for="reason in householdTensionReasons" :key="reason" class="lover-meta-badge lover-meta-badge--warning">
|
|
{{ $t('falukant.family.householdTension.reasons.' + reason) }}
|
|
</span>
|
|
</div>
|
|
<div class="marriage-actions__reason-details" v-if="householdTensionReasons && householdTensionReasons.length">
|
|
<ul>
|
|
<li v-for="reason in householdTensionReasons" :key="reason">
|
|
<strong>{{ $t('falukant.family.householdTension.reasons.' + reason) }}:</strong>
|
|
<span>
|
|
{{ $t('falukant.family.householdTension.reasonsDetail.' + reason) || $t('falukant.family.householdTension.reasons.' + reason) }}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div v-else class="marriage-actions__no-reasons">
|
|
<p>{{ $t('falukant.family.householdTension.noReasons') || 'Keine spezifischen Spannungsgründe angegeben.' }}</p>
|
|
<div v-if="isDev" class="marriage-actions__debug-raw">
|
|
<h4>Dev: rohdaten</h4>
|
|
<pre>{{ JSON.stringify({ householdTension, householdTensionScore, householdTensionReasons, relationships: relationships && relationships[0] ? { marriageState: relationships[0].marriageState, marriageSatisfaction: relationships[0].marriageSatisfaction, marriageFlags: relationships[0].marriageFlags } : null }, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<div
|
|
v-show="familyTab === 'children'"
|
|
class="family-tab-panel"
|
|
role="tabpanel"
|
|
>
|
|
<div class="children-section">
|
|
<h3>{{ $t('falukant.family.children.title') }}</h3>
|
|
<div v-if="children && children.length > 0" class="children-container">
|
|
<!-- 3D-Modell für ausgewähltes Kind rechts neben Liste -->
|
|
<div class="children-list">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t('falukant.family.children.name') }}</th>
|
|
<th>{{ $t('falukant.family.children.otherParent') }}</th>
|
|
<th>{{ $t('falukant.family.children.age') }}</th>
|
|
<th>{{ $t('falukant.family.children.heir') }}</th>
|
|
<th>{{ $t('falukant.family.children.actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(child, index) in children" :key="index">
|
|
<td v-if="child.hasName">
|
|
<div class="child-name-cell">
|
|
<div>
|
|
{{ child.name }}
|
|
<span v-if="child.legitimacy && child.legitimacy !== 'legitimate'" class="child-origin-badge">
|
|
{{ $t('falukant.family.children.legitimacy.' + child.legitimacy) }}
|
|
</span>
|
|
</div>
|
|
<div class="character-media character-media--compact child-character-media">
|
|
<div
|
|
class="character-avatar character-avatar--compact"
|
|
:style="getCharacterAvatarStyle(child, 0.5)"
|
|
></div>
|
|
<div class="character-3d-frame character-3d-frame--compact">
|
|
<Character3D
|
|
:gender="child.gender"
|
|
:age="child.age"
|
|
:lightweight="true"
|
|
:noBackground="true"
|
|
:lazy="true"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td v-else>
|
|
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism')
|
|
}}</button>
|
|
</td>
|
|
<td class="child-other-parent-cell">
|
|
<span class="child-other-parent-name">{{ formatOtherParentLine(child) }}</span>
|
|
<span
|
|
v-if="child.birthContext"
|
|
class="child-origin-badge child-origin-badge--context"
|
|
>
|
|
{{ birthContextShortLabel(child.birthContext) }}
|
|
</span>
|
|
</td>
|
|
<td>{{ child.age }}</td>
|
|
<td>
|
|
<span v-if="child.isHeir" class="heir-badge">{{ $t('falukant.family.children.isHeir') }}</span>
|
|
<button v-else-if="child.hasName" @click="setAsHeir(child)" class="set-heir-button">
|
|
{{ $t('falukant.family.children.setAsHeir') }}
|
|
</button>
|
|
</td>
|
|
<td>
|
|
<button @click="showChildDetails(child)">
|
|
{{ $t('falukant.family.children.detailButton') }}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<p>{{ $t('falukant.family.children.none') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-show="familyTab === 'lovers'"
|
|
class="family-tab-panel"
|
|
role="tabpanel"
|
|
>
|
|
<!-- Liebhaber / Geliebte -->
|
|
<div class="lovers-section">
|
|
<h3>{{ $t('falukant.family.lovers.title') }}</h3>
|
|
<details class="lovers-acknowledge-help">
|
|
<summary class="lovers-acknowledge-help__summary">
|
|
{{ $t('falukant.family.lovers.acknowledgeHelp.title') }}
|
|
</summary>
|
|
<p>{{ $t('falukant.family.lovers.acknowledgeHelp.meaning') }}</p>
|
|
<p>{{ $t('falukant.family.lovers.acknowledgeHelp.maintenance') }}</p>
|
|
<p>{{ $t('falukant.family.lovers.acknowledgeHelp.affection') }}</p>
|
|
<p>{{ $t('falukant.family.lovers.acknowledgeHelp.breakup') }}</p>
|
|
<p>{{ $t('falukant.family.lovers.acknowledgeHelp.tradeoff') }}</p>
|
|
</details>
|
|
<p v-if="politicalFreeLoverSlots > 0" class="lovers-political-hint">
|
|
{{ $t('falukant.family.lovers.politicalFreeSlotsHint', { count: politicalFreeLoverSlots }) }}
|
|
</p>
|
|
<div class="lovers-title-effect-card surface-card">
|
|
<h4>{{ $t('falukant.family.lovers.titleEffects.label') }}</h4>
|
|
<p>{{ $t('falukant.family.lovers.titleEffects.help') }}</p>
|
|
<ul>
|
|
<li>{{ $t('falukant.family.lovers.titleEffects.same') }}: +2</li>
|
|
<li>{{ $t('falukant.family.lovers.titleEffects.oneDiff') }}: +1</li>
|
|
<li>{{ $t('falukant.family.lovers.titleEffects.twoDiff') }}: 0</li>
|
|
<li>{{ $t('falukant.family.lovers.titleEffects.moreDiff') }}: -1..-2</li>
|
|
</ul>
|
|
<p>{{ $t('falukant.family.lovers.titleEffects.costInfo', { base: 20, perDiff: 10 }) }}</p>
|
|
</div>
|
|
<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 class="character-media character-media--compact lover-character-media">
|
|
<div
|
|
class="character-avatar character-avatar--compact"
|
|
:style="getCharacterAvatarStyle(lover, 0.5)"
|
|
></div>
|
|
<div class="character-3d-frame character-3d-frame--compact">
|
|
<Character3D
|
|
:gender="lover.gender"
|
|
:age="lover.age"
|
|
:lightweight="true"
|
|
:noBackground="true"
|
|
:lazy="true"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<strong>{{ $t('falukant.titles.' + lover.gender + '.' + lover.title) }} {{ lover.name }}</strong>
|
|
<div v-if="lover.age != null" class="lover-card__age">
|
|
{{ $t('falukant.family.lovers.age') }}: {{ lover.age }}
|
|
</div>
|
|
<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.scandalExtraDailyPct') }}</dt>
|
|
<dd>{{ Number(lover.scandalExtraDailyPct || 0).toFixed(1) }}%</dd>
|
|
</div>
|
|
<div>
|
|
<dt>{{ $t('falukant.family.lovers.monthlyCost') }}</dt>
|
|
<dd>
|
|
{{ formatCost(lover.monthlyCost || 0) }}
|
|
<span v-if="lover.politicalFreeMaintenance" class="lover-political-free">
|
|
{{ $t('falukant.family.lovers.politicalFreeMaintenance') }}
|
|
</span>
|
|
</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"
|
|
:title="$t('falukant.family.lovers.acknowledgeHelp.meaning')"
|
|
>
|
|
{{ $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>
|
|
<!-- Title effects: show which noble titles this lover influences and how -->
|
|
<div v-if="lover.titleEffects || lover.titleImpact" class="lover-title-effects">
|
|
<strong>{{ $t('falukant.family.lovers.titleEffects.label') || 'Titel-Auswirkungen' }}</strong>
|
|
<ul>
|
|
<li v-if="Array.isArray(lover.titleEffects)" v-for="(te, idx) in lover.titleEffects" :key="idx">
|
|
<span>{{ te.title || te.name || te.key }}: {{ te.effect || te.impact || JSON.stringify(te) }}</span>
|
|
</li>
|
|
<li v-else v-for="(val, key) in (lover.titleEffects || lover.titleImpact)" :key="key">
|
|
<span>{{ key }}: {{ val }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div v-if="isDev" class="lover-card__debug">
|
|
<details>
|
|
<summary>Dev: Lover raw</summary>
|
|
<pre>{{ JSON.stringify(lover, null, 2) }}</pre>
|
|
</details>
|
|
</div>
|
|
</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
|
|
class="button button--secondary"
|
|
@click="improveLoverAffection(lover)"
|
|
:title="$t('falukant.family.lovers.actions.improveAffectionHint', { cost: formatCost(loverAffectionActionCost) })"
|
|
>
|
|
{{ $t('falukant.family.lovers.actions.improveAffection', { cost: formatCost(loverAffectionActionCost) }) }}
|
|
</button>
|
|
<button
|
|
v-if="!lover.acknowledged"
|
|
class="button button--secondary"
|
|
@click="acknowledgeLover(lover)"
|
|
:title="$t('falukant.family.lovers.acknowledgeHelp.buttonHint')"
|
|
>
|
|
{{ $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.lovers.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>
|
|
</div>
|
|
<ChildDetailsDialog ref="childDetailsDialog" />
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import StatusBar from '@/components/falukant/StatusBar.vue'
|
|
import ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
|
|
import Character3D from '@/components/Character3D.vue'
|
|
|
|
import apiClient from '@/utils/axios.js'
|
|
import { confirmAction, showError, showInfo, showSuccess } from '@/utils/feedback.js'
|
|
import { mapState } from 'vuex'
|
|
|
|
const WOOING_PROGRESS_TARGET = 70
|
|
const MARRIAGE_GIFT_COSTS = {
|
|
small: 25,
|
|
decent: 80,
|
|
lavish: 180
|
|
}
|
|
const LOVER_AFFECTION_ACTION_COST = 60
|
|
const AVATAR_POSITIONS = {
|
|
male: {
|
|
width: 195,
|
|
height: 300,
|
|
positions: {
|
|
'0-1': { x: 161, y: 28 },
|
|
'2-3': { x: 802, y: 28 },
|
|
'4-6': { x: 1014, y: 28 },
|
|
'7-10': { x: 800, y: 368 },
|
|
'11-13': { x: 373, y: 368 },
|
|
'14-16': { x: 1441, y: 28 },
|
|
'17-20': { x: 1441, y: 368 },
|
|
'21-30': { x: 1014, y: 368 },
|
|
'31-45': { x: 1227, y: 368 },
|
|
'45-55': { x: 803, y: 687 },
|
|
'55+': { x: 1441, y: 687 },
|
|
},
|
|
},
|
|
female: {
|
|
width: 223,
|
|
height: 298,
|
|
positions: {
|
|
'0-1': { x: 302, y: 66 },
|
|
'2-3': { x: 792, y: 66 },
|
|
'4-6': { x: 62, y: 66 },
|
|
'7-10': { x: 1034, y: 66 },
|
|
'11-13': { x: 1278, y: 66 },
|
|
'14-16': { x: 303, y: 392 },
|
|
'17-20': { x: 1525, y: 392 },
|
|
'21-30': { x: 1278, y: 392 },
|
|
'31-45': { x: 547, y: 718 },
|
|
'45-55': { x: 1034, y: 718 },
|
|
'55+': { x: 1525, y: 718 },
|
|
},
|
|
},
|
|
}
|
|
|
|
export default {
|
|
name: 'FamilyView',
|
|
components: {
|
|
StatusBar,
|
|
ChildDetailsDialog,
|
|
Character3D
|
|
},
|
|
data() {
|
|
return {
|
|
relationships: [],
|
|
children: [],
|
|
lovers: [],
|
|
politicalFreeLoverSlots: 0,
|
|
possibleLovers: [],
|
|
candidateRoles: {},
|
|
deathPartners: [],
|
|
proposals: [],
|
|
selectedProposalId: null,
|
|
gifts: [],
|
|
selectedGiftId: null,
|
|
moodAffects: [],
|
|
characterAffects: [],
|
|
ownCharacter: null,
|
|
marriageSatisfaction: null,
|
|
marriageState: null,
|
|
householdTension: null,
|
|
householdTensionScore: null,
|
|
householdTensionReasons: [],
|
|
debtorsPrison: {
|
|
active: false,
|
|
inDebtorsPrison: false
|
|
},
|
|
pregnancy: null,
|
|
selectedChild: null,
|
|
pendingFamilyRefresh: null,
|
|
familyTab: 'partner',
|
|
}
|
|
},
|
|
computed: {
|
|
...mapState(['socket', 'daemonSocket', 'user']),
|
|
isDev() { return !import.meta.env.PROD },
|
|
marriageGiftCosts() {
|
|
return MARRIAGE_GIFT_COSTS;
|
|
},
|
|
loverAffectionActionCost() {
|
|
return LOVER_AFFECTION_ACTION_COST;
|
|
},
|
|
partnerSummaryLine() {
|
|
if (this.relationships?.length > 0) {
|
|
const r = this.relationships[0];
|
|
const c2 = r.character2;
|
|
const name = c2
|
|
? `${this.$t(`falukant.titles.${c2.gender}.${c2.nobleTitle}`)} ${c2.firstName}`.trim()
|
|
: '';
|
|
const status = this.$t(`falukant.family.statuses.${r.relationshipType}`);
|
|
return name ? `${name} · ${status}` : status;
|
|
}
|
|
if (this.proposals?.length > 0) {
|
|
return this.$t('falukant.family.summary.proposalsAvailable');
|
|
}
|
|
return this.$t('falukant.family.summary.noPartner');
|
|
},
|
|
},
|
|
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();
|
|
await this.loadFamilyData();
|
|
await this.loadGifts();
|
|
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._falukantUpdateStatusHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
|
this._falukantUpdateFamilyHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
|
|
this._falukantUpdateDebtHandler = (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...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('falukantUpdateDebt', this._falukantUpdateDebtHandler);
|
|
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._falukantUpdateDebtHandler) this.socket.off('falukantUpdateDebt', this._falukantUpdateDebtHandler);
|
|
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',
|
|
'falukantUpdateDebt',
|
|
'children_update',
|
|
'falukantUpdateChurch',
|
|
'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 'falukantUpdateDebt':
|
|
case 'familychanged':
|
|
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: ['monthly', 'daily', 'lover_installment'].includes(eventData.reason) });
|
|
break;
|
|
}
|
|
},
|
|
async loadFamilyData() {
|
|
try {
|
|
const response = await apiClient.get('/api/falukant/family');
|
|
this.relationships = response.data.relationships;
|
|
this.children = response.data.children;
|
|
this.lovers = response.data.lovers;
|
|
this.politicalFreeLoverSlots = Number(response.data.politicalFreeLoverSlots) || 0;
|
|
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;
|
|
this.householdTensionScore = response.data.householdTensionScore;
|
|
this.householdTensionReasons = response.data.householdTensionReasons || [];
|
|
this.debtorsPrison = response.data.debtorsPrison || {
|
|
active: false,
|
|
inDebtorsPrison: false
|
|
};
|
|
this.pregnancy = response.data.pregnancy || null;
|
|
} catch (error) {
|
|
console.error('Error loading family data:', error);
|
|
}
|
|
},
|
|
|
|
formatPregnancyDue(iso) {
|
|
if (!iso) return '—';
|
|
try {
|
|
return new Date(iso).toLocaleString(this.$i18n?.locale || undefined, {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
});
|
|
} catch (_) {
|
|
return String(iso);
|
|
}
|
|
},
|
|
|
|
async spendTimeWithSpouse() {
|
|
try {
|
|
const { data } = await apiClient.post('/api/falukant/family/marriage/spend-time');
|
|
await this.loadFamilyData();
|
|
const delayed = Number(data?.delayedProductions || 0);
|
|
const delayMinutes = Number(data?.productionDelayMinutes || 15);
|
|
if (delayed > 0) {
|
|
showInfo(
|
|
this,
|
|
this.$t('falukant.family.marriageActions.spendTimeSuccessWithDelay', {
|
|
count: delayed,
|
|
minutes: delayMinutes
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
showSuccess(this, this.$t('falukant.family.marriageActions.spendTimeSuccess'));
|
|
} catch (error) {
|
|
console.error('Error spending time with spouse:', error);
|
|
showError(this, this.$t('falukant.family.marriageActions.actionError'));
|
|
}
|
|
},
|
|
|
|
async giftToSpouse(giftLevel) {
|
|
try {
|
|
await apiClient.post('/api/falukant/family/marriage/gift', { giftLevel });
|
|
await this.loadFamilyData();
|
|
showSuccess(this, this.$t('falukant.family.marriageActions.giftSuccess'));
|
|
} catch (error) {
|
|
console.error('Error gifting spouse:', error);
|
|
showError(this, this.$t('falukant.family.marriageActions.actionError'));
|
|
}
|
|
},
|
|
|
|
async reconcileMarriage() {
|
|
try {
|
|
await apiClient.post('/api/falukant/family/marriage/reconcile');
|
|
await this.loadFamilyData();
|
|
showSuccess(this, this.$t('falukant.family.marriageActions.reconcileSuccess'));
|
|
} catch (error) {
|
|
console.error('Error reconciling marriage:', error);
|
|
showError(this, this.$t('falukant.family.marriageActions.actionError'));
|
|
}
|
|
},
|
|
|
|
async loadOwnCharacter() {
|
|
try {
|
|
const response = await apiClient.get('/api/falukant/user');
|
|
this.ownCharacter = response.data.character;
|
|
} catch (error) {
|
|
console.error('Error loading own character:', error);
|
|
}
|
|
},
|
|
|
|
showChildDetails(child) {
|
|
this.selectedChild = child;
|
|
this.$refs.childDetailsDialog?.open(child);
|
|
},
|
|
|
|
formatOtherParentLine(child) {
|
|
if (!child?.otherParent) {
|
|
return this.$t('falukant.family.children.otherParentUnknown');
|
|
}
|
|
const o = child.otherParent;
|
|
const title = this.$t(`falukant.titles.${o.gender}.${o.nobleTitle}`);
|
|
return `${title} ${o.firstName}`.trim();
|
|
},
|
|
|
|
birthContextShortLabel(context) {
|
|
const key = `falukant.family.children.birthContextShort.${context}`;
|
|
const t = this.$t(key);
|
|
return t === key ? context : t;
|
|
},
|
|
|
|
async setAsHeir(child) {
|
|
if (!child.childCharacterId) {
|
|
showError(this, 'tr:falukant.family.children.heirSetError');
|
|
return;
|
|
}
|
|
try {
|
|
await apiClient.post('/api/falukant/family/set-heir', {
|
|
childCharacterId: child.childCharacterId
|
|
});
|
|
await this.loadFamilyData();
|
|
showSuccess(this, 'tr:falukant.family.children.heirSetSuccess');
|
|
} catch (error) {
|
|
console.error('Error setting heir:', error);
|
|
showError(this, 'tr:falukant.family.children.heirSetError');
|
|
}
|
|
},
|
|
|
|
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 improveLoverAffection(lover) {
|
|
try {
|
|
const { data } = await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/improve-affection`);
|
|
await this.loadFamilyData();
|
|
showSuccess(
|
|
this,
|
|
this.$t('falukant.family.lovers.actions.improveAffectionSuccess', {
|
|
cost: this.formatCost(data?.cost ?? this.loverAffectionActionCost),
|
|
affection: data?.affection ?? '—',
|
|
visibility: data?.visibility ?? '—',
|
|
discretion: data?.discretion ?? '—',
|
|
})
|
|
);
|
|
} catch (error) {
|
|
console.error('Error improving lover affection:', error);
|
|
showError(this, this.$t('falukant.family.lovers.actions.improveAffectionError'));
|
|
}
|
|
},
|
|
|
|
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';
|
|
},
|
|
|
|
getAgeGroup(ageRaw) {
|
|
const age = Number(ageRaw || 0);
|
|
if (age <= 1) return '0-1';
|
|
if (age <= 3) return '2-3';
|
|
if (age <= 6) return '4-6';
|
|
if (age <= 10) return '7-10';
|
|
if (age <= 13) return '11-13';
|
|
if (age <= 16) return '14-16';
|
|
if (age <= 20) return '17-20';
|
|
if (age <= 30) return '21-30';
|
|
if (age <= 45) return '31-45';
|
|
if (age <= 55) return '45-55';
|
|
return '55+';
|
|
},
|
|
|
|
getCharacterAvatarStyle(character, scale = 1) {
|
|
const gender = String(character?.gender || 'male');
|
|
const ageGroup = this.getAgeGroup(character?.age);
|
|
const genderData = AVATAR_POSITIONS[gender] || AVATAR_POSITIONS.male;
|
|
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
|
|
const safeScale = Number(scale) > 0 ? Number(scale) : 1;
|
|
return {
|
|
backgroundImage: `url(/images/falukant/avatar/${gender}.png)`,
|
|
backgroundPosition: `-${Math.round(position.x * safeScale)}px -${Math.round(position.y * safeScale)}px`,
|
|
backgroundSize: `${Math.round(1792 * safeScale)}px ${Math.round(1024 * safeScale)}px`,
|
|
width: `${Math.round(genderData.width * safeScale)}px`,
|
|
height: `${Math.round(genderData.height * safeScale)}px`,
|
|
};
|
|
},
|
|
|
|
formatCost(value) {
|
|
return new Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
|
|
},
|
|
|
|
async acceptProposal() {
|
|
const response = await apiClient.post('/api/falukant/family/acceptmarriageproposal'
|
|
, { proposalId: this.selectedProposalId });
|
|
this.loadFamilyData();
|
|
},
|
|
|
|
async loadGifts() {
|
|
try {
|
|
const response = await apiClient.get('/api/falukant/family/gifts');
|
|
this.gifts = response.data || [];
|
|
} catch (error) {
|
|
console.error('Error loading gifts:', error);
|
|
this.gifts = []; // Leeres Array bei Fehler
|
|
}
|
|
},
|
|
|
|
async cancelWooing() {
|
|
const confirmed = await confirmAction(this, {
|
|
title: 'Werbung abbrechen',
|
|
message: this.$t('falukant.family.spouse.wooing.cancelConfirm')
|
|
});
|
|
if (!confirmed) return;
|
|
try {
|
|
await apiClient.post('/api/falukant/family/cancel-wooing');
|
|
await this.loadFamilyData();
|
|
this.$root.$refs.messageDialog?.open('tr:falukant.family.spouse.wooing.cancelSuccess');
|
|
} catch (error) {
|
|
console.error('Error cancelling wooing:', error);
|
|
if (error?.response?.status === 412) {
|
|
const retryAtIso = error.response?.data?.retryAt;
|
|
const retryStr = retryAtIso ? new Date(retryAtIso).toLocaleString(navigator.language, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
|
|
const msg = this.$t('falukant.family.spouse.wooing.cancelTooSoon');
|
|
this.$root.$refs.errorDialog?.open(retryStr ? `${msg} — ${this.$t('falukant.family.sendgift.nextGiftAt')}: ${retryStr}` : msg);
|
|
} else {
|
|
this.$root.$refs.errorDialog?.open('tr:falukant.family.spouse.wooing.cancelError');
|
|
}
|
|
}
|
|
},
|
|
|
|
async sendGift() {
|
|
if (!this.selectedGiftId) {
|
|
this.$root.$refs.errorDialog.open(`tr:falukant.family.sendgift.error.nogiftselected`);
|
|
return;
|
|
}
|
|
try {
|
|
await apiClient.post('/api/falukant/family/gift'
|
|
, { giftId: this.selectedGiftId });
|
|
this.loadFamilyData();
|
|
this.$root.$refs.messageDialog.open('tr:falukant.family.sendgift.success');
|
|
} catch (error) {
|
|
console.log(error.response);
|
|
if (error.response?.status === 412) {
|
|
const retryAtIso = error.response?.data?.retryAt;
|
|
const code = error.response?.data?.error || error.response?.data?.message;
|
|
if (retryAtIso) {
|
|
const retryStr = new Date(retryAtIso).toLocaleString(navigator.language, {
|
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit'
|
|
});
|
|
const baseMsg = this.$t(`falukant.family.sendgift.error.${code}`);
|
|
this.$root.$refs.errorDialog.open(`${baseMsg} — ${this.$t('falukant.family.sendgift.nextGiftAt')}: ${retryStr}`);
|
|
} else {
|
|
this.$root.$refs.errorDialog.open(`tr:falukant.family.sendgift.error.${code}`);
|
|
}
|
|
} else {
|
|
this.$root.$refs.errorDialog.open(`tr:falukant.family.sendgift.error.generic`);
|
|
}
|
|
}
|
|
},
|
|
|
|
async loadMoodAffects() {
|
|
try {
|
|
const response = await apiClient.get('/api/falukant/mood/affect');
|
|
this.moodAffects = response.data;
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
},
|
|
|
|
async loadCharacterAffects() {
|
|
try {
|
|
const response = await apiClient.get('/api/falukant/character/affect');
|
|
this.characterAffects = response.data;
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
},
|
|
|
|
progressColor(p) {
|
|
const pct = this.normalizeWooingProgress(p) / 100;
|
|
const red = Math.round(255 * (1 - pct));
|
|
const green = Math.round(255 * pct);
|
|
return `rgb(${red}, ${green}, 0)`;
|
|
},
|
|
normalizeWooingProgress(p) {
|
|
const raw = Number(p) || 0
|
|
const normalized = (raw / WOOING_PROGRESS_TARGET) * 100
|
|
return Math.max(0, Math.min(100, normalized))
|
|
},
|
|
|
|
jumpToPartyForm() {
|
|
this.$router.push({
|
|
name: 'ReputationView',
|
|
query: { tab: 'party' }
|
|
});
|
|
},
|
|
|
|
jumpToChurchForm() {
|
|
this.$router.push({
|
|
name: 'ChurchView',
|
|
});
|
|
},
|
|
|
|
getEffect(gift) {
|
|
// aktueller Partner
|
|
const partner = this.relationships[0].character2;
|
|
// seine aktuelle Mood-ID
|
|
const moodId = partner.mood?.id ?? partner.mood_id;
|
|
|
|
// 1) Mood-Eintrag finden
|
|
const moodEntry = gift.moodsAffects.find(ma => ma.mood_id === moodId);
|
|
const moodValue = moodEntry ? moodEntry.suitability : 0;
|
|
|
|
// 2) Trait-Einträge matchen
|
|
let highestTraitValue = 0;
|
|
for (const trait of partner.traits) {
|
|
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
|
|
if (charEntry && charEntry.suitability > highestTraitValue) {
|
|
highestTraitValue = charEntry.suitability;
|
|
}
|
|
}
|
|
|
|
// Durchschnitt, gerundet
|
|
return Math.round((moodValue + highestTraitValue) / 2);
|
|
},
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
/* Wie HouseView: AppContent setzt last-child auf flex:1 + min-height:0 — sonst können
|
|
Spalten/Karten schrumpfen und sich überlagern. */
|
|
.family-view {
|
|
flex: 0 0 auto;
|
|
min-height: min-content;
|
|
width: 100%;
|
|
}
|
|
|
|
.family-layout {
|
|
display: flex;
|
|
gap: 20px;
|
|
align-items: flex-start;
|
|
padding-top: 0;
|
|
}
|
|
|
|
.family-hero {
|
|
padding: 24px 26px;
|
|
margin-bottom: 16px;
|
|
background:
|
|
radial-gradient(circle at top right, rgba(248, 162, 43, 0.16), transparent 28%),
|
|
linear-gradient(180deg, rgba(255, 250, 243, 0.98) 0%, rgba(247, 238, 224, 0.98) 100%);
|
|
}
|
|
|
|
.family-kicker {
|
|
display: inline-block;
|
|
margin-bottom: 10px;
|
|
padding: 4px 10px;
|
|
border-radius: 999px;
|
|
background: rgba(248, 162, 43, 0.14);
|
|
color: #8a5411;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
.family-hero p {
|
|
margin: 0;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.family-pregnancy {
|
|
margin-bottom: 16px;
|
|
padding: 16px 18px;
|
|
border: 1px solid rgba(120, 140, 200, 0.35);
|
|
background: linear-gradient(180deg, rgba(235, 240, 255, 0.95), rgba(248, 250, 255, 0.98));
|
|
}
|
|
|
|
.family-pregnancy p {
|
|
margin: 6px 0 0;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.family-debt-warning {
|
|
margin-bottom: 16px;
|
|
padding: 16px 18px;
|
|
border: 1px solid rgba(180, 120, 40, 0.32);
|
|
background: linear-gradient(180deg, rgba(255, 244, 223, 0.95), rgba(255, 250, 239, 0.98));
|
|
}
|
|
|
|
.family-debt-warning.is-prison {
|
|
border-color: rgba(146, 57, 40, 0.4);
|
|
background: linear-gradient(180deg, rgba(255, 232, 225, 0.96), rgba(255, 245, 241, 0.98));
|
|
}
|
|
|
|
.family-debt-warning p {
|
|
margin: 6px 0 0;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.family-summary-strip {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
padding: 12px 14px;
|
|
}
|
|
|
|
.family-summary-chip {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 2px;
|
|
padding: 10px 14px;
|
|
border-radius: var(--radius-md);
|
|
border: 1px solid var(--color-border);
|
|
background: rgba(255, 252, 247, 0.95);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
font: inherit;
|
|
color: inherit;
|
|
min-width: 0;
|
|
flex: 1 1 140px;
|
|
max-width: 100%;
|
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
}
|
|
|
|
.family-summary-chip:hover {
|
|
border-color: rgba(248, 162, 43, 0.45);
|
|
}
|
|
|
|
.family-summary-chip.is-active {
|
|
border-color: rgba(248, 162, 43, 0.65);
|
|
box-shadow: 0 0 0 1px rgba(248, 162, 43, 0.2);
|
|
}
|
|
|
|
.family-summary-chip__label {
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.family-summary-chip__value {
|
|
font-size: 0.92rem;
|
|
font-weight: 600;
|
|
line-height: 1.35;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.family-tab-panel {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.spouse-card__dl {
|
|
margin: 0;
|
|
display: grid;
|
|
gap: 10px 16px;
|
|
}
|
|
|
|
.spouse-card__dl > div {
|
|
display: grid;
|
|
grid-template-columns: minmax(7rem, 34%) 1fr;
|
|
gap: 8px 12px;
|
|
align-items: baseline;
|
|
}
|
|
|
|
.spouse-card__dl dt {
|
|
margin: 0;
|
|
color: var(--color-text-secondary);
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.spouse-card__dl dd {
|
|
margin: 0;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.spouse-card__cta {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.spouse-traits {
|
|
margin-top: 14px;
|
|
padding: 10px 12px;
|
|
border: 1px dashed var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
background: rgba(255, 250, 243, 0.6);
|
|
}
|
|
|
|
.spouse-traits summary {
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
font-size: 0.88rem;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.spouse-traits ul {
|
|
margin: 10px 0 0;
|
|
padding-left: 1.1rem;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.spouse-card__dl > div {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.marriage-actions {
|
|
display: grid;
|
|
gap: 12px;
|
|
margin-bottom: 18px;
|
|
padding: 16px 18px;
|
|
}
|
|
|
|
.marriage-actions h3 {
|
|
margin: 0;
|
|
}
|
|
|
|
.marriage-actions__buttons {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
|
|
.marriage-actions__help {
|
|
margin: -2px 0 0;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.marriage-actions__help summary {
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.marriage-actions__help ul {
|
|
margin: 8px 0 0;
|
|
padding-left: 18px;
|
|
}
|
|
|
|
.marriage-actions__reasons {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.marriage-actions__reasons-label {
|
|
color: var(--color-text-secondary);
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.marriage-actions__reason-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.child-origin-badge--context {
|
|
margin-left: 6px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.child-other-parent-cell {
|
|
max-width: 14rem;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.child-other-parent-name {
|
|
display: block;
|
|
}
|
|
|
|
.lovers-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.lover-card {
|
|
padding: 16px 18px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.lover-card__header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: flex-start;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.lover-character-media {
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.lover-card__age {
|
|
font-size: 0.9rem;
|
|
color: rgba(0, 0, 0, 0.62);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.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;
|
|
/* Pro Karte braucht es Platz für Infospalte + Select + Button (DE-Texte); 240px war zu schmal. */
|
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 400px), 1fr));
|
|
gap: 16px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.lover-candidate-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 14px;
|
|
min-width: 0;
|
|
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;
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
.lover-candidate-card__main strong {
|
|
overflow-wrap: anywhere;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.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;
|
|
flex: 0 0 auto;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
@media (min-width: 520px) {
|
|
.lover-candidate-card {
|
|
flex-direction: row;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.lover-candidate-card__actions {
|
|
width: auto;
|
|
min-width: 200px;
|
|
max-width: min(100%, 260px);
|
|
}
|
|
}
|
|
|
|
.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;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
background-color: #fdf1db;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.family-content {
|
|
flex: 1;
|
|
max-width: var(--content-max-width);
|
|
margin: 0 auto;
|
|
padding-bottom: 24px;
|
|
}
|
|
|
|
.spouse-section,
|
|
.children-section,
|
|
.lovers-section {
|
|
border: 1px solid var(--color-border);
|
|
margin: 12px 0;
|
|
border-radius: var(--radius-lg);
|
|
padding: 16px;
|
|
background: rgba(255, 252, 247, 0.86);
|
|
box-shadow: var(--shadow-soft);
|
|
}
|
|
|
|
.relationship-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.relationship-row {
|
|
display: flex;
|
|
gap: 20px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.relationship {
|
|
flex: 0 1 auto;
|
|
}
|
|
|
|
.gifts-section {
|
|
width: 100%;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.children-container {
|
|
display: flex;
|
|
gap: 20px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.children-list {
|
|
flex: 1;
|
|
}
|
|
|
|
.character-media {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.character-media--spouse {
|
|
min-width: 260px;
|
|
}
|
|
|
|
.character-media--compact {
|
|
gap: 8px;
|
|
}
|
|
|
|
.character-avatar {
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
background-repeat: no-repeat;
|
|
background-color: #fdf1db;
|
|
box-shadow: var(--shadow-soft);
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.character-avatar--compact {
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
.character-3d-frame {
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
background-color: #fdf1db;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.character-3d-frame--spouse {
|
|
width: 150px;
|
|
height: 220px;
|
|
}
|
|
|
|
.character-3d-frame--compact {
|
|
width: 95px;
|
|
height: 140px;
|
|
}
|
|
|
|
.child-name-cell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.child-character-media {
|
|
width: fit-content;
|
|
}
|
|
|
|
.spouse-section table,
|
|
.children-section table {
|
|
margin-top: 10px;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.spouse-section th,
|
|
.spouse-section td,
|
|
.children-section th,
|
|
.children-section td {
|
|
border: 1px solid #ddd;
|
|
padding: 8px;
|
|
text-align: left;
|
|
}
|
|
|
|
.spouse-section th {
|
|
background-color: #f2f2f2;
|
|
}
|
|
|
|
.children-section th {
|
|
background-color: #f9f9f9;
|
|
}
|
|
|
|
.lovers-political-hint {
|
|
font-size: 0.92rem;
|
|
color: var(--color-text-secondary);
|
|
margin: 0 0 12px 0;
|
|
}
|
|
|
|
.lovers-acknowledge-help {
|
|
margin: 0 0 12px 0;
|
|
padding: 10px 12px;
|
|
border: 1px dashed var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
background: rgba(255, 250, 243, 0.6);
|
|
}
|
|
|
|
.lovers-acknowledge-help strong {
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.lovers-acknowledge-help__summary {
|
|
cursor: pointer;
|
|
font-weight: 700;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.lovers-acknowledge-help p {
|
|
margin: 4px 0;
|
|
color: var(--color-text-secondary);
|
|
font-size: 0.9rem;
|
|
}
|
|
.lover-political-free {
|
|
display: block;
|
|
font-size: 0.85rem;
|
|
color: var(--color-text-secondary);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.lovers-section {
|
|
ul {
|
|
list-style: none;
|
|
padding-left: 0;
|
|
}
|
|
|
|
li {
|
|
padding: 4px 0;
|
|
}
|
|
}
|
|
|
|
.spouse-table th,
|
|
.spouse-table td {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.spouse-table th:first-child,
|
|
.spouse-table td:first-child {
|
|
width: 20px;
|
|
}
|
|
|
|
.spouse-table th:nth-child(3),
|
|
.spouse-table td:nth-child(3) {
|
|
width: 30px;
|
|
}
|
|
|
|
.spouse-table th:nth-child(4),
|
|
.spouse-table td:nth-child(4) {
|
|
width: 50px;
|
|
}
|
|
|
|
.progress {
|
|
width: 100%;
|
|
background-color: #e5e7eb;
|
|
border-radius: 0.25rem;
|
|
overflow: hidden;
|
|
height: 1rem;
|
|
}
|
|
|
|
.progress-inner {
|
|
height: 100%;
|
|
transition: width 0.3s ease, background-color 0.3s ease;
|
|
}
|
|
|
|
.heir-badge {
|
|
display: inline-block;
|
|
padding: 4px 8px;
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
border-radius: 4px;
|
|
font-size: 0.9em;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.set-heir-button {
|
|
padding: 4px 8px;
|
|
background-color: #28a745;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.set-heir-button:hover {
|
|
background-color: #218838;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.relationship-row,
|
|
.children-container {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|