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) {
|
||||
throw new Error('Relationship type "married" not found');
|
||||
}
|
||||
await Relationship.create({
|
||||
const newRel = await Relationship.create({
|
||||
character1Id: proposal.requesterCharacterId,
|
||||
character2Id: proposal.proposedCharacterId,
|
||||
relationshipTypeId: marriedType.id,
|
||||
});
|
||||
await MarriageProposal.destroy({
|
||||
where: { requesterCharacterId: character.id },
|
||||
})
|
||||
;
|
||||
return { success: true, message: 'Marriage proposal accepted' };
|
||||
|
||||
await MarriageProposal.destroy({ where: { requesterCharacterId: character.id } });
|
||||
|
||||
// Ensure both households are marked as unburdened upon marriage creation
|
||||
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) {
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
:src="fallbackImageSrc"
|
||||
: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>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +26,46 @@ import { getApiBaseURL } from '@/utils/axios.js';
|
||||
|
||||
/** Backend-Route: GET /api/models/3d/falukant/characters/:filename (Proxy mit Draco-Optimierung) */
|
||||
const MODELS_API_PATH = '/api/models/3d/falukant/characters';
|
||||
// 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 threeLoadersPromise = null;
|
||||
let threeModelRuntimePromise = null;
|
||||
@@ -89,8 +139,23 @@ export default {
|
||||
,resizeObserver: null
|
||||
,intersectionObserver: null
|
||||
,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: {
|
||||
actualGender() {
|
||||
if (this.gender) {
|
||||
@@ -157,6 +222,9 @@ export default {
|
||||
},
|
||||
async mounted() {
|
||||
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) {
|
||||
// Defer initialization until the element is in viewport
|
||||
this.intersectionObserver = new window.IntersectionObserver((entries) => {
|
||||
@@ -375,25 +443,49 @@ export default {
|
||||
const ageGroupPath = this.modelPath;
|
||||
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;
|
||||
try {
|
||||
if (this.lightweight) {
|
||||
try {
|
||||
gltf = await loader.loadAsync(ageGroupPath);
|
||||
} catch (ageGroupError) {
|
||||
gltf = await loader.loadAsync(fallbackPath);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
gltf = await loader.loadAsync(exactAgePath);
|
||||
} catch (exactAgeError) {
|
||||
let chosenPath = null;
|
||||
// Try candidates in order until one loads
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
gltf = await loader.loadAsync(ageGroupPath);
|
||||
} catch (ageGroupError) {
|
||||
gltf = await loader.loadAsync(fallbackPath);
|
||||
chosenPath = candidate;
|
||||
gltf = await loader.loadAsync(candidate);
|
||||
// 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 {
|
||||
dracoLoader.dispose();
|
||||
}
|
||||
@@ -435,6 +527,19 @@ export default {
|
||||
const baseY = -scaledBox.min.y; // Auf Boden setzen (y=0 entspricht dem unteren Rand)
|
||||
this.model.position.set(-scaledCenter.x, baseY, -scaledCenter.z);
|
||||
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);
|
||||
|
||||
@@ -446,11 +551,33 @@ export default {
|
||||
const desiredHeight = Math.max(scaledSize.y, scaledSize.z) * 1.15; // etwas Margin
|
||||
let distance = desiredHeight / (2 * Math.tan(fovRad / 2));
|
||||
if (!isFinite(distance) || distance <= 0) distance = 3;
|
||||
const 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 camY = scaledCenter.y + Math.max(0.5, scaledSize.y * 0.15);
|
||||
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.lookAt(scaledCenter.x, scaledCenter.y, scaledCenter.z);
|
||||
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) {
|
||||
// ignore camera auto-fit errors
|
||||
@@ -582,4 +709,19 @@ export default {
|
||||
object-fit: contain;
|
||||
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>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
>
|
||||
<div v-if="child" class="child-details">
|
||||
<table class="details-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('falukant.family.children.name') }}</td>
|
||||
<td>
|
||||
@@ -42,6 +43,7 @@
|
||||
<span v-else>{{ $t('falukant.family.children.notHeir') }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="!child.hasName" class="baptism-notice">
|
||||
<p>{{ $t('falukant.family.children.baptismNotice') }}</p>
|
||||
|
||||
@@ -910,6 +910,10 @@
|
||||
"maintenance": "Suporta bulanan",
|
||||
"scandalExtraDailyPct": "Dugang risgo sa iskandalo/adlaw",
|
||||
"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).",
|
||||
"politicalFreeMaintenance": "Opisina (libre)",
|
||||
"statusFit": "Angay sa kahimtang",
|
||||
@@ -1036,6 +1040,7 @@
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Balayhold Kakulba",
|
||||
"noReasons": "Walay espesipikong hinungdan nga gi-report.",
|
||||
"score": "Kakulba score",
|
||||
"reasonsLabel": "Current causes",
|
||||
"low": "Calm",
|
||||
@@ -1052,6 +1057,18 @@
|
||||
"tooFewServants": "Too few sulugoons",
|
||||
"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": {
|
||||
"title": "Support ang kasal",
|
||||
|
||||
@@ -687,6 +687,7 @@
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Hausfrieden",
|
||||
"noReasons": "Keine spezifischen Spannungsgründe.",
|
||||
"score": "Spannungswert",
|
||||
"reasonsLabel": "Aktuelle Ursachen",
|
||||
"low": "Ruhig",
|
||||
@@ -703,6 +704,18 @@
|
||||
"tooFewServants": "Zu wenig Diener",
|
||||
"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": {
|
||||
"title": "Ehe pflegen",
|
||||
@@ -774,6 +787,10 @@
|
||||
"maintenance": "Unterhalt",
|
||||
"scandalExtraDailyPct": "Zusatz-Skandalrisiko/Tag",
|
||||
"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).",
|
||||
"politicalFreeMaintenance": "Amt (frei)",
|
||||
"statusFit": "Standespassung",
|
||||
|
||||
@@ -927,6 +927,7 @@
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Household Tension",
|
||||
"noReasons": "No specific causes reported.",
|
||||
"score": "Tension score",
|
||||
"reasonsLabel": "Current causes",
|
||||
"low": "Calm",
|
||||
@@ -943,6 +944,18 @@
|
||||
"tooFewServants": "Too few servants",
|
||||
"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": {
|
||||
"title": "Support the marriage",
|
||||
@@ -973,6 +986,10 @@
|
||||
"maintenance": "Maintenance",
|
||||
"scandalExtraDailyPct": "Extra scandal risk/day",
|
||||
"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).",
|
||||
"politicalFreeMaintenance": "Office (free)",
|
||||
"statusFit": "Status Fit",
|
||||
|
||||
@@ -687,6 +687,7 @@
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Tensión del hogar",
|
||||
"noReasons": "No hay causas específicas.",
|
||||
"score": "Valor de tensión",
|
||||
"reasonsLabel": "Causas actuales",
|
||||
"low": "Calmo",
|
||||
@@ -703,6 +704,18 @@
|
||||
"tooFewServants": "Muy pocos sirvientes",
|
||||
"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": {
|
||||
"title": "Cuidar el matrimonio",
|
||||
@@ -774,6 +787,10 @@
|
||||
"maintenance": "Mantenimiento",
|
||||
"scandalExtraDailyPct": "Riesgo extra de escándalo/día",
|
||||
"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).",
|
||||
"politicalFreeMaintenance": "Cargo (gratis)",
|
||||
"statusFit": "Adecuación social",
|
||||
|
||||
@@ -685,6 +685,7 @@
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Paix dans la maison",
|
||||
"noReasons": "Aucune cause spécifique signalée.",
|
||||
"score": "Valeur de tension",
|
||||
"reasonsLabel": "Causes actuelles",
|
||||
"low": "Calme",
|
||||
@@ -701,6 +702,18 @@
|
||||
"tooFewServants": "Trop peu de serviteurs",
|
||||
"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": {
|
||||
"title": "Maintenir le mariage",
|
||||
@@ -772,6 +785,10 @@
|
||||
"maintenance": "Entretien",
|
||||
"scandalExtraDailyPct": "Risque de scandale supplémentaire/jour",
|
||||
"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).",
|
||||
"politicalFreeMaintenance": "Bureau (vacant)",
|
||||
"statusFit": "Ajustement de classe",
|
||||
|
||||
@@ -3,6 +3,7 @@ const Createview = () => import('../views/falukant/CreateView.vue');
|
||||
const FalukantOverviewView = () => import('../views/falukant/OverviewView.vue');
|
||||
const MoneyHistoryView = () => import('../views/falukant/MoneyHistoryView.vue');
|
||||
const FamilyView = () => import('../views/falukant/FamilyView.vue');
|
||||
const AllCharactersTest = () => import('../views/falukant/AllCharactersTest.vue');
|
||||
const HouseView = () => import('../views/falukant/HouseView.vue');
|
||||
const NobilityView = () => import('../views/falukant/NobilityView.vue');
|
||||
const ReputationView = () => import('../views/falukant/ReputationView.vue');
|
||||
@@ -45,6 +46,12 @@ const falukantRoutes = [
|
||||
component: FamilyView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/falukant/dev-all-characters',
|
||||
name: 'FalukantAllCharactersTest',
|
||||
component: AllCharactersTest,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/falukant/house',
|
||||
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) }}
|
||||
</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>
|
||||
@@ -449,7 +466,7 @@
|
||||
<dd>{{ lover.statusFit }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="lover-card__meta">
|
||||
<div class="lover-card__meta">
|
||||
<span
|
||||
v-if="lover.acknowledged"
|
||||
class="lover-meta-badge"
|
||||
@@ -460,6 +477,24 @@
|
||||
<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)">
|
||||
@@ -623,6 +658,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
isDev() { return !import.meta.env.PROD },
|
||||
marriageGiftCosts() {
|
||||
return MARRIAGE_GIFT_COSTS;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user