Refactor OverviewView and NoLoginView to integrate Character3D component
- Replaced avatar display logic in OverviewView with a 3D character representation based on user gender and age. - Updated NoLoginView to utilize Character3D for displaying mascots, enhancing visual consistency. - Removed outdated avatar positioning logic and related computed properties for improved code clarity and maintainability. - Adjusted CSS styles for better layout and responsiveness of character displays.
This commit is contained in:
330
frontend/src/components/Character3D.vue
Normal file
330
frontend/src/components/Character3D.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="character-3d-container" :class="[`gender-${actualGender}`, `age-${ageGroup}`]">
|
||||
<div class="character-3d" :style="characterStyle">
|
||||
<!-- Kopf -->
|
||||
<div class="head">
|
||||
<div class="face">
|
||||
<div class="eye left"></div>
|
||||
<div class="eye right"></div>
|
||||
<div class="mouth"></div>
|
||||
</div>
|
||||
<!-- Haare -->
|
||||
<div class="hair" :class="actualGender"></div>
|
||||
</div>
|
||||
<!-- Körper -->
|
||||
<div class="body" :class="actualGender">
|
||||
<div class="chest"></div>
|
||||
</div>
|
||||
<!-- Arme -->
|
||||
<div class="arm left"></div>
|
||||
<div class="arm right"></div>
|
||||
<!-- Beine -->
|
||||
<div class="leg left"></div>
|
||||
<div class="leg right"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Character3D',
|
||||
props: {
|
||||
gender: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator: (value) => value === null || ['male', 'female'].includes(value)
|
||||
},
|
||||
age: {
|
||||
type: Number,
|
||||
default: null,
|
||||
validator: (value) => value === null || (value >= 0 && value <= 120)
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
randomGender: null,
|
||||
randomAge: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
actualGender() {
|
||||
if (this.gender) {
|
||||
return this.gender;
|
||||
}
|
||||
// Zufällige Auswahl beim ersten Mount, dann persistent
|
||||
if (this.randomGender === null) {
|
||||
this.randomGender = Math.random() < 0.5 ? 'male' : 'female';
|
||||
}
|
||||
return this.randomGender;
|
||||
},
|
||||
actualAge() {
|
||||
if (this.age !== null && this.age !== undefined) {
|
||||
return this.age;
|
||||
}
|
||||
// Zufällige Auswahl beim ersten Mount, dann persistent
|
||||
if (this.randomAge === null) {
|
||||
// Zufälliges Alter zwischen 18 und 65 Jahren
|
||||
this.randomAge = Math.floor(Math.random() * 47) + 18;
|
||||
}
|
||||
return this.randomAge;
|
||||
},
|
||||
ageGroup() {
|
||||
const age = this.actualAge;
|
||||
if (age <= 3) return 'toddler';
|
||||
if (age <= 12) return 'child';
|
||||
if (age <= 17) return 'teen';
|
||||
if (age <= 30) return 'young-adult';
|
||||
if (age <= 50) return 'adult';
|
||||
if (age <= 70) return 'senior';
|
||||
return 'elderly';
|
||||
},
|
||||
characterStyle() {
|
||||
const age = this.actualAge;
|
||||
let scale = 1;
|
||||
|
||||
// Skalierung basierend auf Alter
|
||||
if (age <= 3) {
|
||||
scale = 0.4; // Kleinkind
|
||||
} else if (age <= 12) {
|
||||
scale = 0.6; // Kind
|
||||
} else if (age <= 17) {
|
||||
scale = 0.85; // Teenager
|
||||
} else if (age <= 30) {
|
||||
scale = 1.0; // Junger Erwachsener
|
||||
} else if (age <= 50) {
|
||||
scale = 0.95; // Erwachsener
|
||||
} else if (age <= 70) {
|
||||
scale = 0.9; // Senior
|
||||
} else {
|
||||
scale = 0.85; // Älterer Senior
|
||||
}
|
||||
|
||||
return {
|
||||
transform: `scale(${scale})`
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.character-3d-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
perspective: 1000px;
|
||||
perspective-origin: center center;
|
||||
}
|
||||
|
||||
.character-3d {
|
||||
position: relative;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
transform-style: preserve-3d;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
/* Altersbasierte Anpassungen */
|
||||
.age-toddler .head {
|
||||
width: 35%;
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.age-child .head {
|
||||
width: 32%;
|
||||
height: 27%;
|
||||
}
|
||||
|
||||
.age-teen .head {
|
||||
width: 30%;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
.age-elderly .head {
|
||||
width: 28%;
|
||||
height: 23%;
|
||||
}
|
||||
|
||||
.age-elderly .hair {
|
||||
background: linear-gradient(135deg, #d3d3d3 0%, #a9a9a9 100%) !important;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) rotateY(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px) rotateY(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Kopf */
|
||||
.head {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
height: 25%;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(135deg, #ffdbac 0%, #f4c2a1 100%);
|
||||
border-radius: 50% 50% 45% 45%;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.face {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.eye {
|
||||
position: absolute;
|
||||
width: 8%;
|
||||
height: 8%;
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
top: 35%;
|
||||
}
|
||||
|
||||
.eye.left {
|
||||
left: 30%;
|
||||
}
|
||||
|
||||
.eye.right {
|
||||
right: 30%;
|
||||
}
|
||||
|
||||
.mouth {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
height: 8%;
|
||||
left: 50%;
|
||||
top: 60%;
|
||||
transform: translateX(-50%);
|
||||
border: 2px solid #000;
|
||||
border-top: none;
|
||||
border-radius: 0 0 50% 50%;
|
||||
}
|
||||
|
||||
/* Haare */
|
||||
.hair {
|
||||
position: absolute;
|
||||
top: -15%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
border-radius: 50% 50% 0 0;
|
||||
}
|
||||
|
||||
.hair.male {
|
||||
background: linear-gradient(135deg, #2c1810 0%, #1a0f08 100%);
|
||||
clip-path: polygon(20% 0%, 80% 0%, 100% 50%, 0% 50%);
|
||||
}
|
||||
|
||||
.hair.female {
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%);
|
||||
border-radius: 50% 50% 30% 30%;
|
||||
box-shadow: 0 -2px 10px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Körper */
|
||||
.body {
|
||||
position: absolute;
|
||||
width: 35%;
|
||||
height: 40%;
|
||||
left: 50%;
|
||||
top: 25%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 20% 20% 10% 10%;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.body.male {
|
||||
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
|
||||
}
|
||||
|
||||
.body.female {
|
||||
background: linear-gradient(135deg, #e24a90 0%, #c73a7a 100%);
|
||||
}
|
||||
|
||||
.chest {
|
||||
position: absolute;
|
||||
width: 60%;
|
||||
height: 40%;
|
||||
left: 50%;
|
||||
top: 20%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Arme */
|
||||
.arm {
|
||||
position: absolute;
|
||||
width: 12%;
|
||||
height: 35%;
|
||||
background: linear-gradient(135deg, #ffdbac 0%, #f4c2a1 100%);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.arm.left {
|
||||
left: 15%;
|
||||
top: 28%;
|
||||
transform: rotate(-20deg);
|
||||
}
|
||||
|
||||
.arm.right {
|
||||
right: 15%;
|
||||
top: 28%;
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
/* Beine */
|
||||
.leg {
|
||||
position: absolute;
|
||||
width: 15%;
|
||||
height: 30%;
|
||||
border-radius: 10px 10px 5px 5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.leg.left {
|
||||
left: 35%;
|
||||
top: 65%;
|
||||
}
|
||||
|
||||
.leg.right {
|
||||
right: 35%;
|
||||
top: 65%;
|
||||
}
|
||||
|
||||
.gender-male .leg {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
|
||||
}
|
||||
|
||||
.gender-female .leg {
|
||||
background: linear-gradient(135deg, #8b4caf 0%, #6b3a8f 100%);
|
||||
}
|
||||
|
||||
/* 3D-Effekt mit Schatten */
|
||||
.character-3d::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 60%;
|
||||
height: 10%;
|
||||
left: 50%;
|
||||
bottom: -5%;
|
||||
transform: translateX(-50%);
|
||||
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.3) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
@@ -117,7 +117,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="falukantUser?.character" class="imagecontainer">
|
||||
<div :style="getAvatarStyle" class="avatar"></div>
|
||||
<div class="character-3d-wrapper">
|
||||
<Character3D
|
||||
:gender="falukantUser.character.gender"
|
||||
:age="falukantUser.character.age"
|
||||
/>
|
||||
</div>
|
||||
<div :style="getHouseStyle" class="house"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,50 +130,16 @@
|
||||
|
||||
<script>
|
||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||
import Character3D from '@/components/Character3D.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
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: 'FalukantOverviewView',
|
||||
components: {
|
||||
StatusBar,
|
||||
Character3D,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -181,23 +152,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket']),
|
||||
getAvatarStyle() {
|
||||
if (!this.falukantUser || !this.falukantUser.character) return {};
|
||||
const { gender, age } = this.falukantUser.character;
|
||||
const imageUrl = `/images/falukant/avatar/${gender}.png`;
|
||||
const ageGroup = this.getAgeGroup(age);
|
||||
const genderData = AVATAR_POSITIONS[gender] || {};
|
||||
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
|
||||
const width = genderData.width || 100;
|
||||
const height = genderData.height || 100;
|
||||
return {
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
backgroundPosition: `-${position.x}px -${position.y}px`,
|
||||
backgroundSize: "1792px 1024px",
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
};
|
||||
},
|
||||
getHouseStyle() {
|
||||
console.log(this.falukantUser);
|
||||
if (!this.falukantUser || !this.falukantUser.userHouse?.houseType) return {};
|
||||
@@ -219,10 +173,6 @@ export default {
|
||||
imageRendering: 'crisp-edges',
|
||||
};
|
||||
},
|
||||
getAgeColor(age) {
|
||||
const ageGroup = this.getAgeGroup(age);
|
||||
return ageGroup === 'child' ? 'blue' : ageGroup === 'teen' ? 'green' : ageGroup === 'adult' ? 'red' : 'gray';
|
||||
},
|
||||
moneyValue() {
|
||||
const m = this.falukantUser?.money;
|
||||
return typeof m === 'string' ? parseFloat(m) : m;
|
||||
@@ -282,19 +232,6 @@ export default {
|
||||
break;
|
||||
}
|
||||
},
|
||||
getAgeGroup(age) {
|
||||
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+";
|
||||
},
|
||||
async fetchFalukantUser() {
|
||||
const falukantUser = await apiClient.get('/api/falukant/user');
|
||||
if (!falukantUser.data) {
|
||||
@@ -397,14 +334,18 @@ export default {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
.character-3d-wrapper {
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
image-rendering: crisp-edges;
|
||||
background-color: #fdf1db;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.house {
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<strong>{{ $t('home.betaNoticeLabel') }}</strong> {{ $t('home.betaNoticeText') }}
|
||||
</div>
|
||||
<div class="home-structure">
|
||||
<div class="mascot"><img src="/images/mascot/mascot_male.png" /></div>
|
||||
<div class="mascot">
|
||||
<Character3D gender="male" />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div>
|
||||
<h2>{{ $t('home.nologin.welcome') }}</h2>
|
||||
@@ -36,9 +38,6 @@
|
||||
|
||||
<h3>{{ $t('home.nologin.getStarted.title') }}</h3>
|
||||
<p>{{ $t('home.nologin.getStarted.text', { register: $t('home.nologin.login.register') }) }}</p>
|
||||
|
||||
<h2>{{ $t('home.nologin.randomchat') }}</h2>
|
||||
<button @click="openRandomChat">{{ $t('home.nologin.startrandomchat') }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
@@ -59,6 +58,10 @@
|
||||
<div>
|
||||
<button type="button" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ $t('home.nologin.randomchat') }}</h2>
|
||||
<button @click="openRandomChat">{{ $t('home.nologin.startrandomchat') }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<span @click="openPasswordResetDialog" class="link">{{
|
||||
$t('home.nologin.login.lostpassword') }}</span> | <span id="o1p5iry1"
|
||||
@@ -66,7 +69,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mascot"><img src="/images/mascot/mascot_female.png" /></div>
|
||||
<div class="mascot">
|
||||
<Character3D gender="female" />
|
||||
</div>
|
||||
<RandomChatDialog ref="randomChatDialog" />
|
||||
<RegisterDialog ref="registerDialog" />
|
||||
<PasswordResetDialog ref="passwordResetDialog" />
|
||||
@@ -80,6 +85,7 @@
|
||||
import RandomChatDialog from '@/dialogues/chat/RandomChatDialog.vue';
|
||||
import RegisterDialog from '@/dialogues/auth/RegisterDialog.vue';
|
||||
import PasswordResetDialog from '@/dialogues/auth/PasswordResetDialog.vue';
|
||||
import Character3D from '@/components/Character3D.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
@@ -95,6 +101,7 @@ export default {
|
||||
RandomChatDialog,
|
||||
RegisterDialog,
|
||||
PasswordResetDialog,
|
||||
Character3D,
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login']),
|
||||
@@ -154,6 +161,9 @@ export default {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #fdf1db;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
Reference in New Issue
Block a user