feat: Füge Debug-Overlay und Alters-/Geschlechtsanpassungen für 3D-Charaktere hinzu
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
This commit is contained in:
@@ -4601,16 +4601,26 @@ class FalukantService extends BaseService {
|
|||||||
if (!marriedType) {
|
if (!marriedType) {
|
||||||
throw new Error('Relationship type "married" not found');
|
throw new Error('Relationship type "married" not found');
|
||||||
}
|
}
|
||||||
await Relationship.create({
|
const newRel = await Relationship.create({
|
||||||
character1Id: proposal.requesterCharacterId,
|
character1Id: proposal.requesterCharacterId,
|
||||||
character2Id: proposal.proposedCharacterId,
|
character2Id: proposal.proposedCharacterId,
|
||||||
relationshipTypeId: marriedType.id,
|
relationshipTypeId: marriedType.id,
|
||||||
});
|
});
|
||||||
await MarriageProposal.destroy({
|
|
||||||
where: { requesterCharacterId: character.id },
|
await MarriageProposal.destroy({ where: { requesterCharacterId: character.id } });
|
||||||
})
|
|
||||||
;
|
// Ensure both households are marked as unburdened upon marriage creation
|
||||||
return { success: true, message: 'Marriage proposal accepted' };
|
try {
|
||||||
|
const otherChar = await FalukantCharacter.findByPk(proposal.proposedCharacterId, { attributes: ['userId'] });
|
||||||
|
const otherFUserId = otherChar ? otherChar.userId : null;
|
||||||
|
const affectedUserIds = [user.id];
|
||||||
|
if (otherFUserId) affectedUserIds.push(otherFUserId);
|
||||||
|
await UserHouse.update({ householdTensionScore: 0, householdTensionReasonsJson: [] }, { where: { userId: affectedUserIds } });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to reset household tension on marriage:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: 'Marriage proposal accepted', relationshipId: newRel.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelWooing(hashedUserId) {
|
async cancelWooing(hashedUserId) {
|
||||||
|
|||||||
@@ -7,6 +7,16 @@
|
|||||||
:src="fallbackImageSrc"
|
:src="fallbackImageSrc"
|
||||||
:alt="`Character ${actualGender}`"
|
:alt="`Character ${actualGender}`"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="isDev" class="character-debug-overlay">
|
||||||
|
<div><strong>3D Debug</strong></div>
|
||||||
|
<div>model: {{ debugInfo.modelType || '—' }}</div>
|
||||||
|
<div>path: {{ debugInfo.modelPath || '—' }}</div>
|
||||||
|
<div>age: {{ debugInfo.actualAge || '—' }} ({{ debugInfo.ageGroup || '—' }})</div>
|
||||||
|
<div>camera: <span v-if="debugInfo.camera">{{ debugInfo.camera.x.toFixed(2) }}, {{ debugInfo.camera.y.toFixed(2) }}, {{ debugInfo.camera.z.toFixed(2) }}</span><span v-else>—</span></div>
|
||||||
|
<div>camDist: {{ debugInfo.cameraDistance || '—' }}</div>
|
||||||
|
<div>lightweight: {{ debugInfo.lightweight }}</div>
|
||||||
|
<div>lazy: {{ debugInfo.lazy }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -16,6 +26,46 @@ import { getApiBaseURL } from '@/utils/axios.js';
|
|||||||
|
|
||||||
/** Backend-Route: GET /api/models/3d/falukant/characters/:filename (Proxy mit Draco-Optimierung) */
|
/** Backend-Route: GET /api/models/3d/falukant/characters/:filename (Proxy mit Draco-Optimierung) */
|
||||||
const MODELS_API_PATH = '/api/models/3d/falukant/characters';
|
const MODELS_API_PATH = '/api/models/3d/falukant/characters';
|
||||||
|
// Optional: zusätzliche Namenskonventionen, die vor dem Standardpfad ausprobiert werden.
|
||||||
|
// Hier werden mögliche Alternativ-Dateinamen aufgeführt, die für bestimmte Altersstufen
|
||||||
|
// (z.B. weibliche Modelle für 1-4 Jahre) existieren könnten. Trage hier die tatsächlichen
|
||||||
|
// Dateinamen ein, falls dein Backend solche Varianten enthält.
|
||||||
|
const AGE_ALTERNATIVE_SUFFIXES = [
|
||||||
|
'_alt.glb',
|
||||||
|
'_special.glb',
|
||||||
|
'_v2.glb'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Per-age camera offsets applied after auto-fit. Values are additive (meters).
|
||||||
|
// Set `y` to raise/lower the camera, `z` to move it closer/further.
|
||||||
|
// The mapping supports per-age overrides; if an exact age key is missing the
|
||||||
|
// gender `default` entry will be used. Tune these numbers visually.
|
||||||
|
const AGE_CAMERA_OVERRIDES = {
|
||||||
|
female: {
|
||||||
|
default: { y: 0.35, z: 0.35 },
|
||||||
|
'1': { y: 0.0, z: 0.0 },
|
||||||
|
'2': { y: 0.0, z: 0.0 },
|
||||||
|
'5': { y: 0.0, z: 0.0 }
|
||||||
|
},
|
||||||
|
male: {
|
||||||
|
default: { y: 0.25, z: 0.25 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per-age model position overrides (y in meters). Positive raises the model,
|
||||||
|
// negative moves it down. Defaults move most characters slightly down to match
|
||||||
|
// the visual baseline; specific ages (e.g. female 1/2/5) keep no offset.
|
||||||
|
const AGE_MODEL_POSITION_OVERRIDES = {
|
||||||
|
female: {
|
||||||
|
default: { y: -1 },
|
||||||
|
'1': { y: 0 },
|
||||||
|
'2': { y: 0 },
|
||||||
|
'5': { y: 0 }
|
||||||
|
},
|
||||||
|
male: {
|
||||||
|
default: { y: -1 }
|
||||||
|
}
|
||||||
|
};
|
||||||
let threeRuntimePromise = null;
|
let threeRuntimePromise = null;
|
||||||
let threeLoadersPromise = null;
|
let threeLoadersPromise = null;
|
||||||
let threeModelRuntimePromise = null;
|
let threeModelRuntimePromise = null;
|
||||||
@@ -89,8 +139,23 @@ export default {
|
|||||||
,resizeObserver: null
|
,resizeObserver: null
|
||||||
,intersectionObserver: null
|
,intersectionObserver: null
|
||||||
,isVisible: false
|
,isVisible: false
|
||||||
|
,debugInfo: {
|
||||||
|
modelPath: null,
|
||||||
|
modelType: null,
|
||||||
|
camera: null,
|
||||||
|
cameraDistance: null,
|
||||||
|
ageGroup: null,
|
||||||
|
actualAge: null,
|
||||||
|
lightweight: false,
|
||||||
|
lazy: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isDev() {
|
||||||
|
return !import.meta.env.PROD;
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
actualGender() {
|
actualGender() {
|
||||||
if (this.gender) {
|
if (this.gender) {
|
||||||
@@ -157,6 +222,9 @@ export default {
|
|||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const container = this.$refs.container;
|
const container = this.$refs.container;
|
||||||
|
// expose runtime props for debug overlay
|
||||||
|
this.debugInfo.lightweight = !!this.lightweight;
|
||||||
|
this.debugInfo.lazy = !!this.lazy;
|
||||||
if (this.lazy && typeof window !== 'undefined' && 'IntersectionObserver' in window && container) {
|
if (this.lazy && typeof window !== 'undefined' && 'IntersectionObserver' in window && container) {
|
||||||
// Defer initialization until the element is in viewport
|
// Defer initialization until the element is in viewport
|
||||||
this.intersectionObserver = new window.IntersectionObserver((entries) => {
|
this.intersectionObserver = new window.IntersectionObserver((entries) => {
|
||||||
@@ -375,25 +443,49 @@ export default {
|
|||||||
const ageGroupPath = this.modelPath;
|
const ageGroupPath = this.modelPath;
|
||||||
const fallbackPath = `${prefix}/${this.actualGender}.glb`;
|
const fallbackPath = `${prefix}/${this.actualGender}.glb`;
|
||||||
|
|
||||||
|
// Build a list of candidate paths. For every age and gender we first try
|
||||||
|
// possible alternate filenames for the exact-age model (e.g. female_1y_alt.glb),
|
||||||
|
// then the exact-age file, then the age-group file, and finally the base gender
|
||||||
|
// fallback. This makes the age-specific model the primary source and the
|
||||||
|
// age-group only a later fallback.
|
||||||
|
const candidates = [];
|
||||||
|
const modelAge = this.actualAge;
|
||||||
|
|
||||||
|
// Try exact-age variants with configured suffixes first (works for any gender/age)
|
||||||
|
for (const sfx of AGE_ALTERNATIVE_SUFFIXES) {
|
||||||
|
candidates.push(`${prefix}/${this.actualGender}_${modelAge}y${sfx}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard preference order: exact age, age group, fallback
|
||||||
|
candidates.push(exactAgePath);
|
||||||
|
candidates.push(ageGroupPath);
|
||||||
|
candidates.push(fallbackPath);
|
||||||
|
|
||||||
let gltf;
|
let gltf;
|
||||||
try {
|
try {
|
||||||
if (this.lightweight) {
|
let chosenPath = null;
|
||||||
try {
|
// Try candidates in order until one loads
|
||||||
gltf = await loader.loadAsync(ageGroupPath);
|
for (const candidate of candidates) {
|
||||||
} catch (ageGroupError) {
|
|
||||||
gltf = await loader.loadAsync(fallbackPath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
gltf = await loader.loadAsync(exactAgePath);
|
|
||||||
} catch (exactAgeError) {
|
|
||||||
try {
|
try {
|
||||||
gltf = await loader.loadAsync(ageGroupPath);
|
chosenPath = candidate;
|
||||||
} catch (ageGroupError) {
|
gltf = await loader.loadAsync(candidate);
|
||||||
gltf = await loader.loadAsync(fallbackPath);
|
// loaded successfully
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
// try next candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (!gltf) {
|
||||||
|
throw new Error('No model candidate could be loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// store for debug overlay
|
||||||
|
this.debugInfo.modelPath = chosenPath;
|
||||||
|
// determine modelType
|
||||||
|
if (chosenPath === exactAgePath) this.debugInfo.modelType = 'exactAge';
|
||||||
|
else if (chosenPath === ageGroupPath) this.debugInfo.modelType = 'ageGroup';
|
||||||
|
else if (chosenPath === fallbackPath) this.debugInfo.modelType = 'fallback';
|
||||||
|
else this.debugInfo.modelType = 'alternate';
|
||||||
} finally {
|
} finally {
|
||||||
dracoLoader.dispose();
|
dracoLoader.dispose();
|
||||||
}
|
}
|
||||||
@@ -435,6 +527,19 @@ export default {
|
|||||||
const baseY = -scaledBox.min.y; // Auf Boden setzen (y=0 entspricht dem unteren Rand)
|
const baseY = -scaledBox.min.y; // Auf Boden setzen (y=0 entspricht dem unteren Rand)
|
||||||
this.model.position.set(-scaledCenter.x, baseY, -scaledCenter.z);
|
this.model.position.set(-scaledCenter.x, baseY, -scaledCenter.z);
|
||||||
this.baseYPosition = baseY; // Basisposition für Animation speichern
|
this.baseYPosition = baseY; // Basisposition für Animation speichern
|
||||||
|
// Apply per-age/gender model position overrides (vertical shift)
|
||||||
|
try {
|
||||||
|
const genderMap = AGE_MODEL_POSITION_OVERRIDES[this.actualGender] || {};
|
||||||
|
const specific = genderMap[String(this.actualAge)];
|
||||||
|
const def = genderMap.default;
|
||||||
|
const used = specific || def;
|
||||||
|
if (used && typeof used.y === 'number') {
|
||||||
|
this.model.position.y += used.y;
|
||||||
|
this.debugInfo.modelOffset = { y: used.y };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore position override errors
|
||||||
|
}
|
||||||
|
|
||||||
this.scene.add(this.model);
|
this.scene.add(this.model);
|
||||||
|
|
||||||
@@ -446,11 +551,33 @@ export default {
|
|||||||
const desiredHeight = Math.max(scaledSize.y, scaledSize.z) * 1.15; // etwas Margin
|
const desiredHeight = Math.max(scaledSize.y, scaledSize.z) * 1.15; // etwas Margin
|
||||||
let distance = desiredHeight / (2 * Math.tan(fovRad / 2));
|
let distance = desiredHeight / (2 * Math.tan(fovRad / 2));
|
||||||
if (!isFinite(distance) || distance <= 0) distance = 3;
|
if (!isFinite(distance) || distance <= 0) distance = 3;
|
||||||
const camY = scaledCenter.y + Math.max(0.5, scaledSize.y * 0.15);
|
let camY = scaledCenter.y + Math.max(0.5, scaledSize.y * 0.15);
|
||||||
const camZ = distance + 0.6; // kleiner Offset, damit etwas Raum vor dem Modell ist
|
let camZ = distance + 0.6; // kleiner Offset, damit etwas Raum vor dem Modell ist
|
||||||
|
|
||||||
|
// Apply per-age/gender camera overrides if configured
|
||||||
|
try {
|
||||||
|
const genderMap = AGE_CAMERA_OVERRIDES[this.actualGender] || {};
|
||||||
|
const specific = genderMap[String(this.actualAge)];
|
||||||
|
const def = genderMap.default;
|
||||||
|
const used = specific || def;
|
||||||
|
if (used) {
|
||||||
|
camY += (used.y || 0);
|
||||||
|
camZ += (used.z || 0);
|
||||||
|
// expose applied offsets for easier debugging
|
||||||
|
this.debugInfo.cameraOffset = { y: (used.y || 0), z: (used.z || 0) };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore override errors
|
||||||
|
}
|
||||||
|
|
||||||
this.camera.position.set(0, camY, camZ);
|
this.camera.position.set(0, camY, camZ);
|
||||||
this.camera.lookAt(scaledCenter.x, scaledCenter.y, scaledCenter.z);
|
this.camera.lookAt(scaledCenter.x, scaledCenter.y, scaledCenter.z);
|
||||||
this.onWindowResize();
|
this.onWindowResize();
|
||||||
|
// debug info
|
||||||
|
this.debugInfo.camera = { x: this.camera.position.x, y: this.camera.position.y, z: this.camera.position.z };
|
||||||
|
this.debugInfo.cameraDistance = camZ;
|
||||||
|
this.debugInfo.ageGroup = this.ageGroup;
|
||||||
|
this.debugInfo.actualAge = this.actualAge;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore camera auto-fit errors
|
// ignore camera auto-fit errors
|
||||||
@@ -582,4 +709,19 @@ export default {
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
object-position: center bottom;
|
object-position: center bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.character-debug-overlay {
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: 6px;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.2;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.character-debug-overlay div { opacity: 0.95 }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
>
|
>
|
||||||
<div v-if="child" class="child-details">
|
<div v-if="child" class="child-details">
|
||||||
<table class="details-table">
|
<table class="details-table">
|
||||||
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $t('falukant.family.children.name') }}</td>
|
<td>{{ $t('falukant.family.children.name') }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
<span v-else>{{ $t('falukant.family.children.notHeir') }}</span>
|
<span v-else>{{ $t('falukant.family.children.notHeir') }}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div v-if="!child.hasName" class="baptism-notice">
|
<div v-if="!child.hasName" class="baptism-notice">
|
||||||
<p>{{ $t('falukant.family.children.baptismNotice') }}</p>
|
<p>{{ $t('falukant.family.children.baptismNotice') }}</p>
|
||||||
|
|||||||
@@ -910,6 +910,10 @@
|
|||||||
"maintenance": "Suporta bulanan",
|
"maintenance": "Suporta bulanan",
|
||||||
"scandalExtraDailyPct": "Dugang risgo sa iskandalo/adlaw",
|
"scandalExtraDailyPct": "Dugang risgo sa iskandalo/adlaw",
|
||||||
"monthlyCost": "Gasto kada bulan",
|
"monthlyCost": "Gasto kada bulan",
|
||||||
|
"titleEffects": {
|
||||||
|
"label": "Epekto sa titulo",
|
||||||
|
"help": "Giunsa pag-usab sa usa ka titulo sa nobility sa pamatasan o visibility sa usa ka uyab."
|
||||||
|
},
|
||||||
"politicalFreeSlotsHint": "Ang mga politikal nga opisina naghatag og {count} ka affair slot nga walay bulan nga suporta (ang barato nga relasyon una).",
|
"politicalFreeSlotsHint": "Ang mga politikal nga opisina naghatag og {count} ka affair slot nga walay bulan nga suporta (ang barato nga relasyon una).",
|
||||||
"politicalFreeMaintenance": "Opisina (libre)",
|
"politicalFreeMaintenance": "Opisina (libre)",
|
||||||
"statusFit": "Angay sa kahimtang",
|
"statusFit": "Angay sa kahimtang",
|
||||||
@@ -1036,6 +1040,7 @@
|
|||||||
},
|
},
|
||||||
"householdTension": {
|
"householdTension": {
|
||||||
"label": "Balayhold Kakulba",
|
"label": "Balayhold Kakulba",
|
||||||
|
"noReasons": "Walay espesipikong hinungdan nga gi-report.",
|
||||||
"score": "Kakulba score",
|
"score": "Kakulba score",
|
||||||
"reasonsLabel": "Current causes",
|
"reasonsLabel": "Current causes",
|
||||||
"low": "Calm",
|
"low": "Calm",
|
||||||
@@ -1052,6 +1057,18 @@
|
|||||||
"tooFewServants": "Too few sulugoons",
|
"tooFewServants": "Too few sulugoons",
|
||||||
"marriageCrisis": "Ubos nga kasal satisfaction (makapasakit sa kalinaw sa balay)"
|
"marriageCrisis": "Ubos nga kasal satisfaction (makapasakit sa kalinaw sa balay)"
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
"reasonsDetail": {
|
||||||
|
"visibleLover": "Ang usa ka hayag nga relasyon makapahugno sa dungog sa balay.",
|
||||||
|
"noticeableLover": "Ang relasyon mabantayan ug mosangpot ug tsismis o panagbangi.",
|
||||||
|
"underfundedLover": "Kulang ang suporta sa relasyon, mosangpot sa kalagot ug risgo sa iskandalo.",
|
||||||
|
"acknowledgedAffair": "Ang relasyon giila sa publiko ug naghimo ug adlaw-adlaw nga tensiyon.",
|
||||||
|
"statusMismatch": "Nagkalain-lain nga sosyal nga ranggo nagdala ug panagbangi ug selos.",
|
||||||
|
"loverChild": "Ang bata gikan sa relasyon magdala ug komplikasyon ug rivalidad sa pamilya.",
|
||||||
|
"disorder": "Ang kagubot sa balay makadaut sa adlaw-adlaw nga kinabuhi.",
|
||||||
|
"tooFewServants": "Kulang ang mga sulugoon, nagdugang trabaho ug kasuko sa balay.",
|
||||||
|
"marriageCrisis": "Kahimtang sa kasal nga dili maayo nagapaubos sa kalinaw sa balay ug makapahimo ug kasuko."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"marriageActions": {
|
"marriageActions": {
|
||||||
"title": "Support ang kasal",
|
"title": "Support ang kasal",
|
||||||
|
|||||||
@@ -687,6 +687,7 @@
|
|||||||
},
|
},
|
||||||
"householdTension": {
|
"householdTension": {
|
||||||
"label": "Hausfrieden",
|
"label": "Hausfrieden",
|
||||||
|
"noReasons": "Keine spezifischen Spannungsgründe.",
|
||||||
"score": "Spannungswert",
|
"score": "Spannungswert",
|
||||||
"reasonsLabel": "Aktuelle Ursachen",
|
"reasonsLabel": "Aktuelle Ursachen",
|
||||||
"low": "Ruhig",
|
"low": "Ruhig",
|
||||||
@@ -703,6 +704,18 @@
|
|||||||
"tooFewServants": "Zu wenig Diener",
|
"tooFewServants": "Zu wenig Diener",
|
||||||
"marriageCrisis": "Niedrige Ehezufriedenheit (belastet den Hausfrieden)"
|
"marriageCrisis": "Niedrige Ehezufriedenheit (belastet den Hausfrieden)"
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
"reasonsDetail": {
|
||||||
|
"visibleLover": "Ein öffentlich sichtbares Verhältnis belastet das Ansehen im Haushalt.",
|
||||||
|
"noticeableLover": "Die Beziehung ist auffällig und führt zu Gerede oder Konflikten.",
|
||||||
|
"underfundedLover": "Die Beziehung erhält zu wenig Unterhalt, was Unzufriedenheit und Skandalisierung fördert.",
|
||||||
|
"acknowledgedAffair": "Die Beziehung ist öffentlich anerkannt und erzeugt Spannungen im Alltag.",
|
||||||
|
"statusMismatch": "Unterschiedliche soziale Stände führen zu Konflikten und Eifersucht.",
|
||||||
|
"loverChild": "Ein Kind aus einer Liebschaft verursacht familiäre Komplikationen und Rivalität.",
|
||||||
|
"disorder": "Unordnung im Haushalt verschlechtert das Zusammenleben.",
|
||||||
|
"tooFewServants": "Zu wenige Diener erhöhen Arbeitsbelastung und Unzufriedenheit im Haus.",
|
||||||
|
"marriageCrisis": "Allgemeine Unzufriedenheit in der Ehe verringert den Hausfrieden und kann Verstimmungen auslösen."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"marriageActions": {
|
"marriageActions": {
|
||||||
"title": "Ehe pflegen",
|
"title": "Ehe pflegen",
|
||||||
@@ -774,6 +787,10 @@
|
|||||||
"maintenance": "Unterhalt",
|
"maintenance": "Unterhalt",
|
||||||
"scandalExtraDailyPct": "Zusatz-Skandalrisiko/Tag",
|
"scandalExtraDailyPct": "Zusatz-Skandalrisiko/Tag",
|
||||||
"monthlyCost": "Monatskosten",
|
"monthlyCost": "Monatskosten",
|
||||||
|
"titleEffects": {
|
||||||
|
"label": "Titel-Auswirkungen",
|
||||||
|
"help": "Wie ein Adelstitel das Verhalten oder die Sichtbarkeit eines Liebhabers beeinflussen kann."
|
||||||
|
},
|
||||||
"politicalFreeSlotsHint": "Politische Ämter gewähren dir {count} Liebschaftsplatz/-plätze ohne monatlichen Unterhalt (die günstigsten Beziehungen zählen zuerst).",
|
"politicalFreeSlotsHint": "Politische Ämter gewähren dir {count} Liebschaftsplatz/-plätze ohne monatlichen Unterhalt (die günstigsten Beziehungen zählen zuerst).",
|
||||||
"politicalFreeMaintenance": "Amt (frei)",
|
"politicalFreeMaintenance": "Amt (frei)",
|
||||||
"statusFit": "Standespassung",
|
"statusFit": "Standespassung",
|
||||||
|
|||||||
@@ -927,6 +927,7 @@
|
|||||||
},
|
},
|
||||||
"householdTension": {
|
"householdTension": {
|
||||||
"label": "Household Tension",
|
"label": "Household Tension",
|
||||||
|
"noReasons": "No specific causes reported.",
|
||||||
"score": "Tension score",
|
"score": "Tension score",
|
||||||
"reasonsLabel": "Current causes",
|
"reasonsLabel": "Current causes",
|
||||||
"low": "Calm",
|
"low": "Calm",
|
||||||
@@ -943,6 +944,18 @@
|
|||||||
"tooFewServants": "Too few servants",
|
"tooFewServants": "Too few servants",
|
||||||
"marriageCrisis": "Low marriage satisfaction (strains household peace)"
|
"marriageCrisis": "Low marriage satisfaction (strains household peace)"
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
"reasonsDetail": {
|
||||||
|
"visibleLover": "A publicly visible affair affects the household's reputation.",
|
||||||
|
"noticeableLover": "The relationship is noticeable and leads to gossip or conflicts.",
|
||||||
|
"underfundedLover": "The affair receives too little maintenance, causing dissatisfaction and scandal risk.",
|
||||||
|
"acknowledgedAffair": "The relationship is acknowledged publicly and creates daily tensions.",
|
||||||
|
"statusMismatch": "Different social ranks cause conflicts and jealousy.",
|
||||||
|
"loverChild": "A child from an affair causes family complications and rivalry.",
|
||||||
|
"disorder": "Disorder in the household worsens daily life.",
|
||||||
|
"tooFewServants": "Too few servants increase workload and household dissatisfaction.",
|
||||||
|
"marriageCrisis": "General marital unhappiness reduces household peace and can cause resentments."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"marriageActions": {
|
"marriageActions": {
|
||||||
"title": "Support the marriage",
|
"title": "Support the marriage",
|
||||||
@@ -973,6 +986,10 @@
|
|||||||
"maintenance": "Maintenance",
|
"maintenance": "Maintenance",
|
||||||
"scandalExtraDailyPct": "Extra scandal risk/day",
|
"scandalExtraDailyPct": "Extra scandal risk/day",
|
||||||
"monthlyCost": "Monthly Cost",
|
"monthlyCost": "Monthly Cost",
|
||||||
|
"titleEffects": {
|
||||||
|
"label": "Title effects",
|
||||||
|
"help": "How a noble title affects a lover's behavior or visibility."
|
||||||
|
},
|
||||||
"politicalFreeSlotsHint": "Political offices grant you {count} affair slot(s) with no monthly upkeep (cheapest relationships count first).",
|
"politicalFreeSlotsHint": "Political offices grant you {count} affair slot(s) with no monthly upkeep (cheapest relationships count first).",
|
||||||
"politicalFreeMaintenance": "Office (free)",
|
"politicalFreeMaintenance": "Office (free)",
|
||||||
"statusFit": "Status Fit",
|
"statusFit": "Status Fit",
|
||||||
|
|||||||
@@ -687,6 +687,7 @@
|
|||||||
},
|
},
|
||||||
"householdTension": {
|
"householdTension": {
|
||||||
"label": "Tensión del hogar",
|
"label": "Tensión del hogar",
|
||||||
|
"noReasons": "No hay causas específicas.",
|
||||||
"score": "Valor de tensión",
|
"score": "Valor de tensión",
|
||||||
"reasonsLabel": "Causas actuales",
|
"reasonsLabel": "Causas actuales",
|
||||||
"low": "Calmo",
|
"low": "Calmo",
|
||||||
@@ -703,6 +704,18 @@
|
|||||||
"tooFewServants": "Muy pocos sirvientes",
|
"tooFewServants": "Muy pocos sirvientes",
|
||||||
"marriageCrisis": "Baja satisfacción conyugal (tensa el ambiente en casa)"
|
"marriageCrisis": "Baja satisfacción conyugal (tensa el ambiente en casa)"
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
"reasonsDetail": {
|
||||||
|
"visibleLover": "Una relación visible afecta la reputación del hogar.",
|
||||||
|
"noticeableLover": "La relación es llamativa y provoca habladurías o conflictos.",
|
||||||
|
"underfundedLover": "La relación recibe poco mantenimiento, causando descontento y riesgo de escándalo.",
|
||||||
|
"acknowledgedAffair": "La relación está reconocida públicamente y genera tensiones diarias.",
|
||||||
|
"statusMismatch": "Diferentes rangos sociales causan conflictos y celos.",
|
||||||
|
"loverChild": "Un hijo de una relación provoca complicaciones familiares y rivalidades.",
|
||||||
|
"disorder": "El desorden en la casa empeora la convivencia.",
|
||||||
|
"tooFewServants": "Muy pocos sirvientes aumentan la carga de trabajo y el descontento.",
|
||||||
|
"marriageCrisis": "Insatisfacción marital general reduce la paz del hogar y puede causar resentimientos."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"marriageActions": {
|
"marriageActions": {
|
||||||
"title": "Cuidar el matrimonio",
|
"title": "Cuidar el matrimonio",
|
||||||
@@ -774,6 +787,10 @@
|
|||||||
"maintenance": "Mantenimiento",
|
"maintenance": "Mantenimiento",
|
||||||
"scandalExtraDailyPct": "Riesgo extra de escándalo/día",
|
"scandalExtraDailyPct": "Riesgo extra de escándalo/día",
|
||||||
"monthlyCost": "Coste mensual",
|
"monthlyCost": "Coste mensual",
|
||||||
|
"titleEffects": {
|
||||||
|
"label": "Efectos del título",
|
||||||
|
"help": "Cómo un título nobiliario afecta el comportamiento o la visibilidad de un amante."
|
||||||
|
},
|
||||||
"politicalFreeSlotsHint": "Los cargos políticos te conceden {count} plaza(s) de relación sin mantenimiento mensual (primero cuentan las relaciones más baratas).",
|
"politicalFreeSlotsHint": "Los cargos políticos te conceden {count} plaza(s) de relación sin mantenimiento mensual (primero cuentan las relaciones más baratas).",
|
||||||
"politicalFreeMaintenance": "Cargo (gratis)",
|
"politicalFreeMaintenance": "Cargo (gratis)",
|
||||||
"statusFit": "Adecuación social",
|
"statusFit": "Adecuación social",
|
||||||
|
|||||||
@@ -685,6 +685,7 @@
|
|||||||
},
|
},
|
||||||
"householdTension": {
|
"householdTension": {
|
||||||
"label": "Paix dans la maison",
|
"label": "Paix dans la maison",
|
||||||
|
"noReasons": "Aucune cause spécifique signalée.",
|
||||||
"score": "Valeur de tension",
|
"score": "Valeur de tension",
|
||||||
"reasonsLabel": "Causes actuelles",
|
"reasonsLabel": "Causes actuelles",
|
||||||
"low": "Calme",
|
"low": "Calme",
|
||||||
@@ -701,6 +702,18 @@
|
|||||||
"tooFewServants": "Trop peu de serviteurs",
|
"tooFewServants": "Trop peu de serviteurs",
|
||||||
"marriageCrisis": "Faible satisfaction du couple (tension au foyer)"
|
"marriageCrisis": "Faible satisfaction du couple (tension au foyer)"
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
"reasonsDetail": {
|
||||||
|
"visibleLover": "Une relation visible porte atteinte à la réputation du foyer.",
|
||||||
|
"noticeableLover": "La relation est remarquée et provoque des commérages ou des conflits.",
|
||||||
|
"underfundedLover": "La relation reçoit trop peu d'entretien, entraînant mécontentement et risque de scandale.",
|
||||||
|
"acknowledgedAffair": "La relation est reconnue publiquement et crée des tensions quotidiennes.",
|
||||||
|
"statusMismatch": "Des rangs sociaux différents provoquent des conflits et de la jalousie.",
|
||||||
|
"loverChild": "Un enfant issu d'une relation cause des complications familiales et des rivalités.",
|
||||||
|
"disorder": "Le désordre dans le foyer dégrade la vie quotidienne.",
|
||||||
|
"tooFewServants": "Trop peu de domestiques augmentent la charge de travail et le mécontentement.",
|
||||||
|
"marriageCrisis": "Un malheur conjugal général réduit la paix du foyer et peut provoquer des ressentiments."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"marriageActions": {
|
"marriageActions": {
|
||||||
"title": "Maintenir le mariage",
|
"title": "Maintenir le mariage",
|
||||||
@@ -772,6 +785,10 @@
|
|||||||
"maintenance": "Entretien",
|
"maintenance": "Entretien",
|
||||||
"scandalExtraDailyPct": "Risque de scandale supplémentaire/jour",
|
"scandalExtraDailyPct": "Risque de scandale supplémentaire/jour",
|
||||||
"monthlyCost": "Coûts mensuels",
|
"monthlyCost": "Coûts mensuels",
|
||||||
|
"titleEffects": {
|
||||||
|
"label": "Effets du titre",
|
||||||
|
"help": "Comment un titre de noblesse influence le comportement ou la visibilité d'un amant."
|
||||||
|
},
|
||||||
"politicalFreeSlotsHint": "Les bureaux politiques vous accordent {count} intérêts amoureux sans entretien mensuel (les relations les moins chères comptent en premier).",
|
"politicalFreeSlotsHint": "Les bureaux politiques vous accordent {count} intérêts amoureux sans entretien mensuel (les relations les moins chères comptent en premier).",
|
||||||
"politicalFreeMaintenance": "Bureau (vacant)",
|
"politicalFreeMaintenance": "Bureau (vacant)",
|
||||||
"statusFit": "Ajustement de classe",
|
"statusFit": "Ajustement de classe",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const Createview = () => import('../views/falukant/CreateView.vue');
|
|||||||
const FalukantOverviewView = () => import('../views/falukant/OverviewView.vue');
|
const FalukantOverviewView = () => import('../views/falukant/OverviewView.vue');
|
||||||
const MoneyHistoryView = () => import('../views/falukant/MoneyHistoryView.vue');
|
const MoneyHistoryView = () => import('../views/falukant/MoneyHistoryView.vue');
|
||||||
const FamilyView = () => import('../views/falukant/FamilyView.vue');
|
const FamilyView = () => import('../views/falukant/FamilyView.vue');
|
||||||
|
const AllCharactersTest = () => import('../views/falukant/AllCharactersTest.vue');
|
||||||
const HouseView = () => import('../views/falukant/HouseView.vue');
|
const HouseView = () => import('../views/falukant/HouseView.vue');
|
||||||
const NobilityView = () => import('../views/falukant/NobilityView.vue');
|
const NobilityView = () => import('../views/falukant/NobilityView.vue');
|
||||||
const ReputationView = () => import('../views/falukant/ReputationView.vue');
|
const ReputationView = () => import('../views/falukant/ReputationView.vue');
|
||||||
@@ -45,6 +46,12 @@ const falukantRoutes = [
|
|||||||
component: FamilyView,
|
component: FamilyView,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/falukant/dev-all-characters',
|
||||||
|
name: 'FalukantAllCharactersTest',
|
||||||
|
component: AllCharactersTest,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/falukant/house',
|
path: '/falukant/house',
|
||||||
name: 'HouseView',
|
name: 'HouseView',
|
||||||
|
|||||||
58
frontend/src/views/falukant/AllCharactersTest.vue
Normal file
58
frontend/src/views/falukant/AllCharactersTest.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="all-characters-test">
|
||||||
|
<h1>All 3D Characters (Ages × Genders)</h1>
|
||||||
|
<div class="controls">
|
||||||
|
<label><input type="checkbox" v-model="useLightweight"> Use lightweight models</label>
|
||||||
|
<label><input type="checkbox" v-model="noBackground"> No background</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div v-for="(age, idx) in ages" :key="`age-${age}`" class="card">
|
||||||
|
<div class="label">Alter: {{ age }}</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="cell">
|
||||||
|
<div class="mini-shell">
|
||||||
|
<Character3D :gender="'female'" :age="age" :lightweight="useLightweight" :noBackground="noBackground" />
|
||||||
|
</div>
|
||||||
|
<div class="sub">weiblich</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<div class="mini-shell">
|
||||||
|
<Character3D :gender="'male'" :age="age" :lightweight="useLightweight" :noBackground="noBackground" />
|
||||||
|
</div>
|
||||||
|
<div class="sub">männlich</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Character3D from '@/components/Character3D.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AllCharactersTest',
|
||||||
|
components: { Character3D },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// Show ages 0..17 and some representative adults
|
||||||
|
ages: [...Array(18).keys()].concat([25, 40, 70]),
|
||||||
|
useLightweight: true,
|
||||||
|
noBackground: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.all-characters-test { padding: 16px }
|
||||||
|
.controls { margin-bottom: 12px }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px }
|
||||||
|
.card { border: 1px solid #e0e0e0; padding: 8px; border-radius: 6px; background: #fff }
|
||||||
|
.label { font-weight: 600; margin-bottom: 6px }
|
||||||
|
.row { display: flex; gap: 8px }
|
||||||
|
.cell { flex: 1; text-align: center }
|
||||||
|
.mini-shell { width: 100%; height: 220px }
|
||||||
|
.sub { margin-top: 6px; font-size: 12px; color: #666 }
|
||||||
|
</style>
|
||||||
@@ -275,6 +275,23 @@
|
|||||||
{{ $t('falukant.family.householdTension.reasons.' + reason) }}
|
{{ $t('falukant.family.householdTension.reasons.' + reason) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -449,7 +466,7 @@
|
|||||||
<dd>{{ lover.statusFit }}</dd>
|
<dd>{{ lover.statusFit }}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
<div class="lover-card__meta">
|
<div class="lover-card__meta">
|
||||||
<span
|
<span
|
||||||
v-if="lover.acknowledged"
|
v-if="lover.acknowledged"
|
||||||
class="lover-meta-badge"
|
class="lover-meta-badge"
|
||||||
@@ -460,6 +477,24 @@
|
|||||||
<span v-if="lover.monthsUnderfunded > 0" class="lover-meta-badge lover-meta-badge--warning">
|
<span v-if="lover.monthsUnderfunded > 0" class="lover-meta-badge lover-meta-badge--warning">
|
||||||
{{ $t('falukant.family.lovers.underfunded', { count: lover.monthsUnderfunded }) }}
|
{{ $t('falukant.family.lovers.underfunded', { count: lover.monthsUnderfunded }) }}
|
||||||
</span>
|
</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>
|
||||||
<div class="lover-card__actions">
|
<div class="lover-card__actions">
|
||||||
<button class="button button--secondary" @click="setLoverMaintenance(lover, 25)">
|
<button class="button button--secondary" @click="setLoverMaintenance(lover, 25)">
|
||||||
@@ -623,6 +658,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['socket', 'daemonSocket', 'user']),
|
...mapState(['socket', 'daemonSocket', 'user']),
|
||||||
|
isDev() { return !import.meta.env.PROD },
|
||||||
marriageGiftCosts() {
|
marriageGiftCosts() {
|
||||||
return MARRIAGE_GIFT_COSTS;
|
return MARRIAGE_GIFT_COSTS;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user