Enhance usability and localization across components: Update USABILITY_CONCEPT.md with new focus areas, improve user feedback in AppFooter and FamilyView components, and refine text in various UI elements for better clarity and consistency. Replace console logs with user-friendly messages, correct German translations, and streamline interaction logic in multiple components.
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import { showInfo } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AppFooter',
|
||||
@@ -71,10 +72,10 @@ export default {
|
||||
},
|
||||
// Daemon WebSocket deaktiviert - diese Funktionen sind nicht mehr verfügbar
|
||||
async showFalukantDaemonStatus() {
|
||||
console.log('⚠️ Daemon WebSocket deaktiviert - Status nicht verfügbar');
|
||||
showInfo(this, 'Der Systemstatus ist in dieser Ansicht derzeit nicht direkt verfügbar.');
|
||||
},
|
||||
handleDaemonMessage(event) {
|
||||
console.log('⚠️ Daemon WebSocket deaktiviert - keine Nachrichten verarbeitet');
|
||||
handleDaemonMessage() {
|
||||
// Status-Events werden hier bewusst nicht verarbeitet.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="app-section-bar__back"
|
||||
@click="navigateBack"
|
||||
>
|
||||
Zurueck
|
||||
Zurück
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
@@ -25,13 +25,13 @@ const SECTION_LABELS = [
|
||||
{ test: (path) => path.startsWith('/settings'), label: 'Einstellungen' },
|
||||
{ test: (path) => path.startsWith('/admin'), label: 'Administration' },
|
||||
{ test: (path) => path.startsWith('/minigames'), label: 'Minispiele' },
|
||||
{ test: (path) => path.startsWith('/personal'), label: 'Persoenlich' },
|
||||
{ test: (path) => path.startsWith('/personal'), label: 'Persönlich' },
|
||||
{ test: (path) => path.startsWith('/blogs'), label: 'Blog' }
|
||||
];
|
||||
|
||||
const TITLE_MAP = {
|
||||
Friends: 'Freunde',
|
||||
Guestbook: 'Gaestebuch',
|
||||
Guestbook: 'Gästebuch',
|
||||
'Search users': 'Suche',
|
||||
Gallery: 'Galerie',
|
||||
Forum: 'Forum',
|
||||
@@ -46,7 +46,7 @@ const TITLE_MAP = {
|
||||
VocabCourse: 'Kurs',
|
||||
VocabLesson: 'Lektion',
|
||||
FalukantCreate: 'Charakter erstellen',
|
||||
FalukantOverview: 'Uebersicht',
|
||||
FalukantOverview: 'Übersicht',
|
||||
BranchView: 'Niederlassung',
|
||||
MoneyHistoryView: 'Geldverlauf',
|
||||
FalukantFamily: 'Familie',
|
||||
@@ -60,9 +60,9 @@ const TITLE_MAP = {
|
||||
HealthView: 'Gesundheit',
|
||||
PoliticsView: 'Politik',
|
||||
UndergroundView: 'Untergrund',
|
||||
'Personal settings': 'Persoenliche Daten',
|
||||
'Personal settings': 'Persönliche Daten',
|
||||
'View settings': 'Ansicht',
|
||||
'Sexuality settings': 'Sexualitaet',
|
||||
'Sexuality settings': 'Sexualität',
|
||||
'Flirt settings': 'Flirt',
|
||||
'Account settings': 'Account',
|
||||
Interests: 'Interessen',
|
||||
@@ -132,11 +132,19 @@ export default {
|
||||
return '/admin/users';
|
||||
}
|
||||
|
||||
if (window.history.length > 1) {
|
||||
return '__history_back__';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
navigateBack() {
|
||||
if (this.backTarget === '__history_back__') {
|
||||
this.$router.back();
|
||||
return;
|
||||
}
|
||||
if (this.backTarget) {
|
||||
this.$router.push(this.backTarget);
|
||||
}
|
||||
|
||||
@@ -17,23 +17,33 @@ 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';
|
||||
let threeRuntimePromise = null;
|
||||
let threeLoadersPromise = null;
|
||||
let threeModelRuntimePromise = null;
|
||||
|
||||
async function loadThreeRuntime() {
|
||||
if (!threeRuntimePromise) {
|
||||
threeRuntimePromise = Promise.all([
|
||||
import('three'),
|
||||
import('three/addons/loaders/GLTFLoader.js'),
|
||||
import('three/addons/loaders/DRACOLoader.js')
|
||||
]).then(([THREE, { GLTFLoader }, { DRACOLoader }]) => ({
|
||||
THREE,
|
||||
GLTFLoader,
|
||||
DRACOLoader
|
||||
}));
|
||||
threeRuntimePromise = import('@/utils/threeRuntime.js');
|
||||
}
|
||||
|
||||
return threeRuntimePromise;
|
||||
}
|
||||
|
||||
async function loadThreeLoaders() {
|
||||
if (!threeLoadersPromise) {
|
||||
threeLoadersPromise = import('@/utils/threeLoaders.js');
|
||||
}
|
||||
|
||||
return threeLoadersPromise;
|
||||
}
|
||||
|
||||
async function loadThreeModelRuntime() {
|
||||
if (!threeModelRuntimePromise) {
|
||||
threeModelRuntimePromise = import('@/utils/threeModelRuntime.js');
|
||||
}
|
||||
|
||||
return threeModelRuntimePromise;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Character3D',
|
||||
props: {
|
||||
@@ -65,7 +75,9 @@ export default {
|
||||
clock: null,
|
||||
baseYPosition: 0,
|
||||
showFallback: false,
|
||||
threeRuntime: null
|
||||
threeRuntime: null,
|
||||
threeLoaders: null,
|
||||
threeModelRuntime: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -149,49 +161,65 @@ export default {
|
||||
return this.threeRuntime;
|
||||
},
|
||||
|
||||
async ensureThreeLoaders() {
|
||||
if (!this.threeLoaders) {
|
||||
this.threeLoaders = markRaw(await loadThreeLoaders());
|
||||
}
|
||||
|
||||
return this.threeLoaders;
|
||||
},
|
||||
|
||||
async ensureThreeModelRuntime() {
|
||||
if (!this.threeModelRuntime) {
|
||||
this.threeModelRuntime = markRaw(await loadThreeModelRuntime());
|
||||
}
|
||||
|
||||
return this.threeModelRuntime;
|
||||
},
|
||||
|
||||
async init3D() {
|
||||
const container = this.$refs.container;
|
||||
if (!container) return;
|
||||
this.showFallback = false;
|
||||
const { THREE } = await this.ensureThreeRuntime();
|
||||
this.clock = markRaw(new THREE.Clock());
|
||||
const runtime = await this.ensureThreeRuntime();
|
||||
this.clock = markRaw(new runtime.Clock());
|
||||
|
||||
// Scene erstellen - markRaw verwenden, um Vue's Reactivity zu vermeiden
|
||||
this.scene = markRaw(new THREE.Scene());
|
||||
this.scene = markRaw(new runtime.Scene());
|
||||
if (!this.noBackground) {
|
||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
||||
this.scene.background = new runtime.Color(0xf0f0f0);
|
||||
await this.loadBackground();
|
||||
}
|
||||
|
||||
// Camera erstellen
|
||||
const aspect = container.clientWidth / container.clientHeight;
|
||||
this.camera = markRaw(new THREE.PerspectiveCamera(50, aspect, 0.1, 1000));
|
||||
this.camera = markRaw(new runtime.PerspectiveCamera(50, aspect, 0.1, 1000));
|
||||
this.camera.position.set(0, 1.5, 3);
|
||||
this.camera.lookAt(0, 1, 0);
|
||||
|
||||
// Renderer erstellen
|
||||
this.renderer = markRaw(new THREE.WebGLRenderer({ antialias: true, alpha: true }));
|
||||
this.renderer = markRaw(new runtime.WebGLRenderer({ antialias: true, alpha: true }));
|
||||
this.renderer.setSize(container.clientWidth, container.clientHeight);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
container.appendChild(this.renderer.domElement);
|
||||
|
||||
// Verbesserte Beleuchtung für hellere Modelle
|
||||
// Mehr ambient light für gleichmäßigere Ausleuchtung
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
|
||||
const ambientLight = new runtime.AmbientLight(0xffffff, 1.0);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
// Hauptlicht von vorne oben - stärker
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
const directionalLight = new runtime.DirectionalLight(0xffffff, 1.2);
|
||||
directionalLight.position.set(5, 10, 5);
|
||||
this.scene.add(directionalLight);
|
||||
|
||||
// Zusätzliches Licht von hinten - heller
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.75);
|
||||
const backLight = new runtime.DirectionalLight(0xffffff, 0.75);
|
||||
backLight.position.set(-5, 5, -5);
|
||||
this.scene.add(backLight);
|
||||
|
||||
// Zusätzliches Seitenlicht für mehr Tiefe
|
||||
const sideLight = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
const sideLight = new runtime.DirectionalLight(0xffffff, 0.5);
|
||||
sideLight.position.set(-5, 5, 5);
|
||||
this.scene.add(sideLight);
|
||||
|
||||
@@ -200,13 +228,13 @@ export default {
|
||||
},
|
||||
|
||||
async loadBackground() {
|
||||
const { THREE } = await this.ensureThreeRuntime();
|
||||
const runtime = await this.ensureThreeRuntime();
|
||||
// Optimierte Versionen (512×341, ~130 KB); Originale ~3 MB
|
||||
const backgrounds = ['bg1_opt.png', 'bg2_opt.png'];
|
||||
const randomBg = backgrounds[Math.floor(Math.random() * backgrounds.length)];
|
||||
const bgPath = `/images/falukant/backgrounds/${randomBg}`;
|
||||
|
||||
const loader = new THREE.TextureLoader();
|
||||
const loader = new runtime.TextureLoader();
|
||||
loader.load(
|
||||
bgPath,
|
||||
(texture) => {
|
||||
@@ -220,7 +248,7 @@ export default {
|
||||
console.warn('Fehler beim Laden des Hintergrunds:', error);
|
||||
// Fallback auf Standardfarbe bei Fehler
|
||||
if (this.scene) {
|
||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
||||
this.scene.background = new runtime.Color(0xf0f0f0);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -228,7 +256,8 @@ export default {
|
||||
|
||||
async loadModel() {
|
||||
if (!this.scene) return;
|
||||
const { THREE, GLTFLoader, DRACOLoader } = await this.ensureThreeRuntime();
|
||||
const modelRuntime = await this.ensureThreeModelRuntime();
|
||||
const loaders = await this.ensureThreeLoaders();
|
||||
|
||||
// Altes Modell entfernen
|
||||
if (this.model) {
|
||||
@@ -252,9 +281,9 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
const dracoLoader = new DRACOLoader();
|
||||
const dracoLoader = new loaders.DRACOLoader();
|
||||
dracoLoader.setDecoderPath('/draco/gltf/');
|
||||
const loader = new GLTFLoader();
|
||||
const loader = new loaders.GLTFLoader();
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
const base = getApiBaseURL();
|
||||
@@ -273,12 +302,12 @@ export default {
|
||||
// Versuche zuerst genaues Alter
|
||||
try {
|
||||
gltf = await loader.loadAsync(exactAgePath);
|
||||
console.log(`Loaded exact age model: ${exactAgePath}`);
|
||||
console.debug(`Loaded exact age model: ${exactAgePath}`);
|
||||
} catch (exactAgeError) {
|
||||
// Falls genaues Alter nicht existiert, versuche Altersbereich
|
||||
try {
|
||||
gltf = await loader.loadAsync(ageGroupPath);
|
||||
console.log(`Loaded age group model: ${ageGroupPath}`);
|
||||
console.debug(`Loaded age group model: ${ageGroupPath}`);
|
||||
} catch (ageGroupError) {
|
||||
// Falls Altersbereich nicht existiert, verwende Basis-Modell
|
||||
console.warn(`Could not load ${ageGroupPath}, trying fallback model`);
|
||||
@@ -293,8 +322,8 @@ export default {
|
||||
this.model = markRaw(gltf.scene);
|
||||
|
||||
// Initiale Bounding Box für Größenberechnung (vor Skalierung)
|
||||
const initialBox = new THREE.Box3().setFromObject(this.model);
|
||||
const initialSize = initialBox.getSize(new THREE.Vector3());
|
||||
const initialBox = new modelRuntime.Box3().setFromObject(this.model);
|
||||
const initialSize = initialBox.getSize(new modelRuntime.Vector3());
|
||||
|
||||
// Skalierung basierend auf Alter
|
||||
const age = this.actualAge;
|
||||
@@ -318,8 +347,8 @@ export default {
|
||||
this.model.scale.set(modelScale, modelScale, modelScale);
|
||||
|
||||
// Bounding Box NACH dem Skalieren neu berechnen
|
||||
const scaledBox = new THREE.Box3().setFromObject(this.model);
|
||||
const scaledCenter = scaledBox.getCenter(new THREE.Vector3());
|
||||
const scaledBox = new modelRuntime.Box3().setFromObject(this.model);
|
||||
const scaledCenter = scaledBox.getCenter(new modelRuntime.Vector3());
|
||||
|
||||
// Modell zentrieren basierend auf der skalierten Bounding Box
|
||||
// Position direkt setzen statt zu subtrahieren, um Proxy-Probleme zu vermeiden
|
||||
@@ -331,7 +360,7 @@ export default {
|
||||
|
||||
// Animationen laden falls vorhanden
|
||||
if (gltf.animations && gltf.animations.length > 0) {
|
||||
this.mixer = markRaw(new THREE.AnimationMixer(this.model));
|
||||
this.mixer = markRaw(new modelRuntime.AnimationMixer(this.model));
|
||||
gltf.animations.forEach((clip) => {
|
||||
this.mixer.clipAction(clip).play();
|
||||
});
|
||||
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
this.fetchSettings();
|
||||
} catch (err) {
|
||||
console.error('Error updating setting:', err);
|
||||
showApiError(this, err, 'Aenderung konnte nicht gespeichert werden.');
|
||||
showApiError(this, err, 'Änderung konnte nicht gespeichert werden.');
|
||||
}
|
||||
},
|
||||
languagesList() {
|
||||
|
||||
@@ -279,7 +279,6 @@ export default {
|
||||
},
|
||||
|
||||
openNewDirectorDialog() {
|
||||
console.log('openNewDirectorDialog');
|
||||
this.$refs.newDirectorDialog.open(this.branchId);
|
||||
},
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
console.log('changed to ', value)
|
||||
this.$emit("input", parseInt(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<label for="password-reset-email">{{ $t("passwordReset.email") }}</label>
|
||||
<input id="password-reset-email" type="email" v-model="email" required :class="{ 'field-error': emailTouched && !isEmailValid }" />
|
||||
<span class="form-hint">Wir senden den Link an die hinterlegte E-Mail-Adresse.</span>
|
||||
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gueltige E-Mail-Adresse eingeben.</span>
|
||||
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gültige E-Mail-Adresse eingeben.</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="form-field">
|
||||
<label for="register-email">{{ $t("register.email") }}</label>
|
||||
<input id="register-email" type="email" v-model="email" :class="{ 'field-error': emailTouched && !isEmailValid }" />
|
||||
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gueltige E-Mail-Adresse eingeben.</span>
|
||||
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gültige E-Mail-Adresse eingeben.</span>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="register-username">{{ $t("register.username") }}</label>
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="form-field">
|
||||
<label for="register-repeat-password">{{ $t("register.repeatPassword") }}</label>
|
||||
<input id="register-repeat-password" type="password" v-model="repeatPassword" :class="{ 'field-error': repeatPasswordTouched && !doPasswordsMatch }" />
|
||||
<span v-if="repeatPasswordTouched && !doPasswordsMatch" class="form-error">Die Passwoerter stimmen nicht ueberein.</span>
|
||||
<span v-if="repeatPasswordTouched && !doPasswordsMatch" class="form-error">Die Passwörter stimmen nicht überein.</span>
|
||||
</div>
|
||||
<SelectDropdownWidget labelTr="settings.personal.label.language" :v-model="language"
|
||||
tooltipTr="settings.personal.tooltip.language" :list="languages" :value="language" />
|
||||
|
||||
@@ -702,7 +702,6 @@ export default {
|
||||
// Mark as closed first so any async close events won't schedule reconnect
|
||||
this.opened = false;
|
||||
this.clearPendingRoomCreateTracking();
|
||||
console.log('[Chat WS] dialog close — closing websocket');
|
||||
this.disconnectChatSocket();
|
||||
// Remove network event listeners
|
||||
window.removeEventListener('online', this.onOnline);
|
||||
@@ -719,16 +718,13 @@ export default {
|
||||
this.showOptions = false;
|
||||
},
|
||||
onOnline() {
|
||||
console.log('[Chat WS] Network online detected');
|
||||
if (this.opened && !this.chatConnected && !this.connectRacing && (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN)) {
|
||||
console.log('[Chat WS] online — attempting reconnect');
|
||||
this.reconnectAttempts = 0; // Reset attempts on network recovery
|
||||
this.reconnectIntervalMs = 3000; // Reset to base interval
|
||||
this.connectChatSocket();
|
||||
}
|
||||
},
|
||||
onOffline() {
|
||||
console.log('[Chat WS] Network offline detected');
|
||||
this.setStatus('disconnected');
|
||||
},
|
||||
async loadRooms() {
|
||||
|
||||
@@ -72,10 +72,10 @@
|
||||
({{ formatCost(computeBranchCost(type)) }})
|
||||
</option>
|
||||
</select>
|
||||
<span class="form-hint">Waehle zuerst Region und dann den Niederlassungstyp.</span>
|
||||
<span class="form-hint">Wähle zuerst die Region und dann den Niederlassungstyp.</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="form-hint">Waehle auf der Karte eine freie Region aus.</div>
|
||||
<div v-else class="form-hint">Wähle auf der Karte eine freie Region aus.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +148,7 @@
|
||||
|
||||
async onConfirm() {
|
||||
if (!this.selectedRegion || !this.selectedType) {
|
||||
showError(this, 'Bitte zuerst Region und Typ auswaehlen.');
|
||||
showError(this, 'Bitte zuerst Region und Typ auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"activate": {
|
||||
"title": "Zugang aktivieren",
|
||||
"message": "Hallo {username}. Bitte gib hier den Code ein, den wir Dir per Email zugesendet haben.",
|
||||
"message": "Hallo {username}. Bitte gib hier den Code ein, den wir Dir per E-Mail zugesendet haben.",
|
||||
"token": "Token:",
|
||||
"submit": "Absenden",
|
||||
"failure": "Die Aktivierung war nicht erfolgreich."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
"info-title": "Information",
|
||||
"dialog": {
|
||||
"contact": {
|
||||
"email": "Email-Adresse",
|
||||
"email": "E-Mail-Adresse",
|
||||
"name": "Name",
|
||||
"message": "Deine Nachricht an uns",
|
||||
"accept": "Deine Email-Adresse wird vorübergehend in unserem System gespeichert. Nachdem Deine Anfrage bearbeitet wurde, wird die Email-Adresse wieder aus dem System gelöscht.",
|
||||
"acceptdatasave": "Ich stimme der vorübergehenden Speicherung meiner Email-Adresse zu.",
|
||||
"accept": "Deine E-Mail-Adresse wird vorübergehend in unserem System gespeichert. Nachdem Deine Anfrage bearbeitet wurde, wird die E-Mail-Adresse wieder aus dem System gelöscht.",
|
||||
"acceptdatasave": "Ich stimme der vorübergehenden Speicherung meiner E-Mail-Adresse zu.",
|
||||
"accept2": "Ohne diese Zustimmung können wir Dir leider nicht antworten."
|
||||
}
|
||||
},
|
||||
@@ -58,4 +58,4 @@
|
||||
"yes": "Ja",
|
||||
"no": "Nein"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
"login": {
|
||||
"name": "Login-Name",
|
||||
"namedescription": "Gib hier Deinen Benutzernamen ein",
|
||||
"password": "Paßwort",
|
||||
"passworddescription": "Gib hier Dein Paßwort ein",
|
||||
"lostpassword": "Paßwort vergessen",
|
||||
"password": "Passwort",
|
||||
"passworddescription": "Gib hier Dein Passwort ein",
|
||||
"lostpassword": "Passwort vergessen",
|
||||
"register": "Bei yourPart registrieren",
|
||||
"stayLoggedIn": "Eingeloggt bleiben",
|
||||
"submit": "Einloggen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"register": {
|
||||
"title": "Bei yourPart registrieren",
|
||||
"email": "Email-Adresse",
|
||||
"email": "E-Mail-Adresse",
|
||||
"username": "Benutzername",
|
||||
"password": "Paßwort",
|
||||
"repeatPassword": "Paßwort wiederholen",
|
||||
"password": "Passwort",
|
||||
"repeatPassword": "Passwort wiederholen",
|
||||
"language": "Sprache",
|
||||
"languages": {
|
||||
"en": "Englisch",
|
||||
@@ -13,9 +13,9 @@
|
||||
"register": "Registrieren",
|
||||
"close": "Schließen",
|
||||
"failure": "Es ist ein Fehler aufgetreten.",
|
||||
"success": "Du wurdest erfolgreich registriert. Bitte schaue jetzt in Dein Email-Postfach zum aktivieren Deines Zugangs.",
|
||||
"passwordMismatch": "Die Paßwörter stimmen nicht überein.",
|
||||
"emailinuse": "Die Email-Adresse wird bereits verwendet.",
|
||||
"success": "Du wurdest erfolgreich registriert. Bitte schaue jetzt in Dein E-Mail-Postfach zum Aktivieren Deines Zugangs.",
|
||||
"passwordMismatch": "Die Passwörter stimmen nicht überein.",
|
||||
"emailinuse": "Die E-Mail-Adresse wird bereits verwendet.",
|
||||
"usernameinuse": "Der Benutzername ist nicht verfügbar."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,14 +141,14 @@
|
||||
"account": {
|
||||
"title": "Account",
|
||||
"username": "Benutzername",
|
||||
"email": "Email-Adresse",
|
||||
"email": "E-Mail-Adresse",
|
||||
"newpassword": "Passwort",
|
||||
"newpasswordretype": "Passwort wiederholen",
|
||||
"deleteAccount": "Account löschen",
|
||||
"language": "Sprache",
|
||||
"showinsearch": "In Usersuchen anzeigen",
|
||||
"changeaction": "Benutzerdaten ändern",
|
||||
"oldpassword": "Altes Paßwort (benötigt)"
|
||||
"oldpassword": "Altes Passwort (benötigt)"
|
||||
},
|
||||
"interests": {
|
||||
"title": "Interessen",
|
||||
@@ -177,4 +177,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default [
|
||||
meta: {
|
||||
seo: {
|
||||
title: 'Blogs auf YourPart',
|
||||
description: 'Oeffentliche Blogs, Beitraege und Community-Inhalte auf YourPart.',
|
||||
description: 'Öffentliche Blogs, Beiträge und Community-Inhalte auf YourPart.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -28,7 +28,7 @@ export default [
|
||||
meta: {
|
||||
seo: {
|
||||
title: 'Blogs auf YourPart',
|
||||
description: 'Oeffentliche Blogs, Beitraege und Community-Inhalte auf YourPart.',
|
||||
description: 'Öffentliche Blogs, Beiträge und Community-Inhalte auf YourPart.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -38,9 +38,9 @@ export default [
|
||||
component: BlogListView,
|
||||
meta: {
|
||||
seo: {
|
||||
title: 'Blogs auf YourPart - Community-Beitraege und Themen',
|
||||
description: 'Entdecke oeffentliche Blogs auf YourPart mit Community-Beitraegen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.',
|
||||
keywords: 'Blogs, Community Blog, Artikel, Beitraege, YourPart',
|
||||
title: 'Blogs auf YourPart - Community-Beiträge und Themen',
|
||||
description: 'Entdecke öffentliche Blogs auf YourPart mit Community-Beiträgen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.',
|
||||
keywords: 'Blogs, Community Blog, Artikel, Beiträge, YourPart',
|
||||
canonicalPath: '/blogs',
|
||||
jsonLd: [
|
||||
{
|
||||
@@ -48,7 +48,7 @@ export default [
|
||||
'@type': 'CollectionPage',
|
||||
name: 'Blogs auf YourPart',
|
||||
url: buildAbsoluteUrl('/blogs'),
|
||||
description: 'Oeffentliche Blogs und Community-Beitraege auf YourPart.',
|
||||
description: 'Öffentliche Blogs und Community-Beiträge auf YourPart.',
|
||||
inLanguage: 'de',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -224,11 +224,9 @@ const store = createStore({
|
||||
}
|
||||
|
||||
const maxRetries = 10;
|
||||
console.log(`Backend-Reconnect-Versuch ${state.backendRetryCount + 1}/${maxRetries}`);
|
||||
|
||||
if (state.backendRetryCount >= maxRetries) {
|
||||
// Nach maxRetries alle 5 Sekunden weiter versuchen
|
||||
console.log('Backend: Max Retries erreicht, versuche weiter alle 5 Sekunden...');
|
||||
state.backendRetryTimer = setTimeout(() => {
|
||||
state.backendRetryCount = 0; // Reset für nächsten Zyklus
|
||||
state.backendRetryTimer = null;
|
||||
@@ -241,7 +239,6 @@ const store = createStore({
|
||||
|
||||
state.backendRetryCount++;
|
||||
const delay = 5000; // Alle 5 Sekunden versuchen
|
||||
console.log(`Backend: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`);
|
||||
|
||||
state.backendRetryTimer = setTimeout(() => {
|
||||
state.backendRetryTimer = null;
|
||||
@@ -259,8 +256,6 @@ const store = createStore({
|
||||
// Vite bindet Umgebungsvariablen zur Build-Zeit ein, daher Fallback-Logik basierend auf Hostname
|
||||
let daemonUrl = getDaemonSocketUrl();
|
||||
|
||||
console.log('[Daemon] Finale Daemon-URL:', daemonUrl);
|
||||
|
||||
const connectDaemonSocket = () => {
|
||||
// Cleanup existing socket and timer
|
||||
if (state.daemonSocket) {
|
||||
|
||||
7
frontend/src/utils/threeLoaders.js
Normal file
7
frontend/src/utils/threeLoaders.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
||||
|
||||
export {
|
||||
DRACOLoader,
|
||||
GLTFLoader
|
||||
};
|
||||
9
frontend/src/utils/threeModelRuntime.js
Normal file
9
frontend/src/utils/threeModelRuntime.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { AnimationMixer } from 'three/src/animation/AnimationMixer.js';
|
||||
import { Box3 } from 'three/src/math/Box3.js';
|
||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||
|
||||
export {
|
||||
AnimationMixer,
|
||||
Box3,
|
||||
Vector3
|
||||
};
|
||||
19
frontend/src/utils/threeRuntime.js
Normal file
19
frontend/src/utils/threeRuntime.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera.js';
|
||||
import { Clock } from 'three/src/core/Clock.js';
|
||||
import { AmbientLight } from 'three/src/lights/AmbientLight.js';
|
||||
import { DirectionalLight } from 'three/src/lights/DirectionalLight.js';
|
||||
import { Color } from 'three/src/math/Color.js';
|
||||
import { TextureLoader } from 'three/src/loaders/TextureLoader.js';
|
||||
import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer.js';
|
||||
import { Scene } from 'three/src/scenes/Scene.js';
|
||||
|
||||
export {
|
||||
AmbientLight,
|
||||
Clock,
|
||||
Color,
|
||||
DirectionalLight,
|
||||
PerspectiveCamera,
|
||||
Scene,
|
||||
TextureLoader,
|
||||
WebGLRenderer
|
||||
};
|
||||
@@ -24,7 +24,7 @@
|
||||
<section class="workflow-grid">
|
||||
<article class="workflow-card surface-card">
|
||||
<span class="workflow-card__step">1</span>
|
||||
<h3>Level waehlen</h3>
|
||||
<h3>Level wählen</h3>
|
||||
<p>Bestehendes Level öffnen oder sofort mit einer neuen Vorlage starten.</p>
|
||||
</article>
|
||||
<article class="workflow-card surface-card">
|
||||
|
||||
@@ -276,11 +276,9 @@ export default {
|
||||
}
|
||||
},
|
||||
async loadStockTypes() {
|
||||
console.log('Loading stock types...');
|
||||
this.loading.stockTypes = true;
|
||||
try {
|
||||
const stockTypesResult = await apiClient.get('/api/admin/falukant/stock-types');
|
||||
console.log('Stock types loaded:', stockTypesResult.data);
|
||||
this.stockTypes = stockTypesResult.data;
|
||||
} catch (error) {
|
||||
console.error('Error loading stock types:', error);
|
||||
@@ -327,19 +325,13 @@ export default {
|
||||
}
|
||||
},
|
||||
canAddStock(branch) {
|
||||
console.log('canAddStock called for branch:', branch);
|
||||
console.log('stockTypes:', this.stockTypes);
|
||||
console.log('branch.stocks:', branch.stocks);
|
||||
|
||||
// Wenn keine Stock-Types geladen sind, zeige den Button nicht
|
||||
if (!this.stockTypes || this.stockTypes.length === 0) {
|
||||
console.log('No stock types loaded, returning false');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wenn keine Stocks vorhanden sind, kann immer hinzugefügt werden
|
||||
if (!branch.stocks || branch.stocks.length === 0) {
|
||||
console.log('No stocks in branch, returning true');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -350,8 +342,6 @@ export default {
|
||||
const availableStockTypes = this.stockTypes.filter(stockType =>
|
||||
!existingStockTypeIds.includes(stockType.id)
|
||||
);
|
||||
|
||||
console.log('Available stock types:', availableStockTypes);
|
||||
return availableStockTypes.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
<span class="blog-list__kicker">Community-Blogs</span>
|
||||
<h1>Blogs</h1>
|
||||
<p>Artikel, Projektstaende und persoenliche Einblicke aus der YourPart-Community.</p>
|
||||
<p>Artikel, Projektstände und persönliche Einblicke aus der YourPart-Community.</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<router-link v-if="$store.getters.isLoggedIn" class="btn" to="/blogs/create">Neuen Blog erstellen</router-link>
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
return slug ? `/blogs/${encodeURIComponent(slug)}` : `/blogs/${blog.id}`;
|
||||
},
|
||||
blogExcerpt(blog) {
|
||||
const source = blog?.description || 'Oeffentliche Eintraege, Gedanken und Projektstaende aus der Community.';
|
||||
const source = blog?.description || 'Öffentliche Einträge, Gedanken und Projektstände aus der Community.';
|
||||
return source.length > 150 ? `${source.slice(0, 147)}...` : source;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<section class="posts surface-card">
|
||||
<div class="posts__header">
|
||||
<h2>{{ $t('blog.posts') }}</h2>
|
||||
<span class="posts__count">{{ total }} Eintraege</span>
|
||||
<span class="posts__count">{{ total }} Einträge</span>
|
||||
</div>
|
||||
<div v-if="!items.length" class="blog-view__state">Keine Eintraege vorhanden.</div>
|
||||
<div v-if="!items.length" class="blog-view__state">Keine Einträge vorhanden.</div>
|
||||
<article v-for="p in items" :key="p.id" class="post">
|
||||
<h3>{{ p.title }}</h3>
|
||||
<div class="content" v-html="sanitize(p.content)" />
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
.map((item) => `${item.title || ''} ${stripHtml(item.content || '')}`.trim())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const summarySource = this.blog.description || plainTextPosts || 'Oeffentlicher Community-Blog auf YourPart.';
|
||||
const summarySource = this.blog.description || plainTextPosts || 'Öffentlicher Community-Blog auf YourPart.';
|
||||
const description = truncateText(summarySource, 160);
|
||||
const canonicalPath = this.canonicalBlogPath();
|
||||
|
||||
@@ -143,7 +143,7 @@ export default {
|
||||
await this.fetchPage(1);
|
||||
this.applyBlogSeo();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error('Blog konnte nicht geladen werden:', e);
|
||||
// this.$router.replace('/blogs');
|
||||
applySeo({
|
||||
title: 'Blog nicht gefunden | YourPart',
|
||||
|
||||
@@ -880,7 +880,7 @@ export default {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('Unhandled event:', eventData);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -197,13 +197,11 @@
|
||||
|
||||
<script>
|
||||
import StatusBar from '@/components/falukant/StatusBar.vue'
|
||||
import MessageDialog from '@/dialogues/standard/MessageDialog.vue'
|
||||
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue'
|
||||
import ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
|
||||
import Character3D from '@/components/Character3D.vue'
|
||||
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import { confirmAction } from '@/utils/feedback.js'
|
||||
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const WOOING_PROGRESS_TARGET = 70
|
||||
@@ -212,8 +210,6 @@ export default {
|
||||
name: 'FamilyView',
|
||||
components: {
|
||||
StatusBar,
|
||||
MessageDialog,
|
||||
ErrorDialog,
|
||||
ChildDetailsDialog,
|
||||
Character3D
|
||||
},
|
||||
@@ -294,7 +290,7 @@ export default {
|
||||
|
||||
async setAsHeir(child) {
|
||||
if (!child.childCharacterId) {
|
||||
console.error('Child character ID missing');
|
||||
showError(this, 'tr:falukant.family.children.heirSetError');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -302,10 +298,10 @@ export default {
|
||||
childCharacterId: child.childCharacterId
|
||||
});
|
||||
await this.loadFamilyData();
|
||||
this.$root.$refs.messageDialog?.open('tr:falukant.family.children.heirSetSuccess');
|
||||
showSuccess(this, 'tr:falukant.family.children.heirSetSuccess');
|
||||
} catch (error) {
|
||||
console.error('Error setting heir:', error);
|
||||
this.$root.$refs.errorDialog?.open('tr:falukant.family.children.heirSetError');
|
||||
showError(this, 'tr:falukant.family.children.heirSetError');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ export default {
|
||||
route: 'BranchView',
|
||||
},
|
||||
{
|
||||
kicker: 'Ueberblick',
|
||||
kicker: 'Überblick',
|
||||
title: 'Finanzen pruefen',
|
||||
description: 'Kontostand, Verlauf und wirtschaftliche Entwicklung ohne lange Suche.',
|
||||
cta: 'Geldhistorie',
|
||||
|
||||
@@ -279,7 +279,6 @@ export default {
|
||||
setDropTarget(index) {
|
||||
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
||||
this.dragOverIndex = index;
|
||||
console.log('[Dashboard Drag] setDropTarget:', index, '→ dragOverIndex =', index);
|
||||
}
|
||||
},
|
||||
clearDropTarget() {
|
||||
@@ -315,16 +314,13 @@ export default {
|
||||
}
|
||||
if (to == null) to = this.dragOverIndex != null ? this.dragOverIndex : this.widgets.length;
|
||||
const from = this.draggedIndex;
|
||||
console.log('[Dashboard Drag] onAnyDrop — Maus:', x, y, '→ to:', to, 'from:', from, 'event.target:', e?.target?.className);
|
||||
if (from === to || to < 0 || to > this.widgets.length) {
|
||||
console.log('[Dashboard Drag] onAnyDrop — abgebrochen');
|
||||
this.draggedIndex = null;
|
||||
this.dragOverIndex = null;
|
||||
return;
|
||||
}
|
||||
const item = this.widgets.splice(from, 1)[0];
|
||||
this.widgets.splice(to, 0, item);
|
||||
console.log('[Dashboard Drag] onAnyDrop — erledigt. Neue Reihenfolge:', this.widgets.map(w => w.title));
|
||||
this.draggedIndex = null;
|
||||
this.dragOverIndex = null;
|
||||
await this.saveConfig();
|
||||
|
||||
@@ -55,8 +55,13 @@
|
||||
<div class="login-panel">
|
||||
<span class="panel-kicker">Direkt starten</span>
|
||||
<h2>{{ $t('home.nologin.login.submit') }}</h2>
|
||||
<p class="login-panel__hint">Mit bestehendem Konto direkt einloggen oder alternativ ohne Konto den Random-Chat testen.</p>
|
||||
<div class="quick-access-actions">
|
||||
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
|
||||
<button type="button" class="secondary-action" @click="openRegisterDialog">{{ $t('home.nologin.login.register') }}</button>
|
||||
</div>
|
||||
<div class="login-fields">
|
||||
<input v-model="username" size="20" type="text" :placeholder="$t('home.nologin.login.name')"
|
||||
<input ref="usernameInput" v-model="username" size="20" type="text" :placeholder="$t('home.nologin.login.name')"
|
||||
:title="$t('home.nologin.login.namedescription')" @keydown.enter="focusPassword">
|
||||
<input v-model="password" size="20" type="password"
|
||||
:placeholder="$t('home.nologin.login.password')"
|
||||
@@ -69,7 +74,6 @@
|
||||
<span>{{ $t('home.nologin.login.stayLoggedIn') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="access-split">
|
||||
@@ -150,6 +154,11 @@ export default {
|
||||
this.$root.$refs.errorDialog.open(`tr:error.${errorKey}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.usernameInput?.focus?.();
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -292,6 +301,18 @@ export default {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-panel__hint {
|
||||
margin: 0 0 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.quick-access-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.login-fields {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
@@ -302,10 +323,6 @@ export default {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.access-split {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -129,7 +129,9 @@
|
||||
@mouseenter="onTileMouseEnter($event, index)"
|
||||
@mouseleave="onTileMouseLeave($event, index)"
|
||||
@touchstart="onTileMouseDown($event, index)"
|
||||
@touchmove.prevent="onTileMouseMove($event)"
|
||||
@touchend="onTileMouseUp($event, index)"
|
||||
@touchcancel="endDrag($event)"
|
||||
@dblclick="handleDoubleClick(index, $event)">
|
||||
<span v-if="tile" class="tile-symbol">{{ getTileSymbol(tile.type) }}</span>
|
||||
</div>
|
||||
@@ -148,7 +150,9 @@
|
||||
</div>
|
||||
|
||||
<div v-if="showRocketFlight" class="power-up-animation" :style="{ left: rocketStartPos.x + 'px', top: rocketStartPos.y + 'px' }">
|
||||
<div class="rocket-flight" :style="{ '--dx': (rocketEndPos.x - rocketStartPos.x) + 'px', '--dy': (rocketEndPos.y - rocketStartPos.y) + 'px' }"></div>
|
||||
<div class="rocket-flight-path" :style="{ '--dx': (rocketEndPos.x - rocketStartPos.x) + 'px', '--dy': (rocketEndPos.y - rocketStartPos.y) + 'px' }">
|
||||
<div class="rocket-flight-icon">🚀</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showRainbowEffect" class="power-up-animation" :style="{ left: rainbowCenter.x + 'px', top: rainbowCenter.y + 'px' }">
|
||||
@@ -164,8 +168,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Raketen-Flug-Animation -->
|
||||
<div v-if="rocketTarget.x > 0 && rocketTarget.y > 0" class="rocket-flight-animation" :style="{ left: rocketTarget.x + 'px', top: rocketTarget.y + 'px' }">
|
||||
<div class="rocket-flight">🚀</div>
|
||||
<div v-if="rocketTarget.x > 0 && rocketTarget.y > 0" class="rocket-target-marker" :style="{ left: rocketTarget.x + 'px', top: rocketTarget.y + 'px' }">
|
||||
<div class="rocket-target-marker__icon">🎯</div>
|
||||
</div>
|
||||
|
||||
<!-- Fliegende Rakete -->
|
||||
@@ -383,14 +387,110 @@ export default {
|
||||
// Füge globale Event-Listener hinzu
|
||||
document.addEventListener('mousemove', this.onGlobalMouseMove);
|
||||
document.addEventListener('mouseup', this.onGlobalMouseUp);
|
||||
document.addEventListener('touchmove', this.onGlobalMouseMove, { passive: false });
|
||||
document.addEventListener('touchend', this.onGlobalMouseUp);
|
||||
document.addEventListener('touchcancel', this.onGlobalMouseUp);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// Entferne globale Event-Listener
|
||||
document.removeEventListener('mousemove', this.onGlobalMouseMove);
|
||||
document.removeEventListener('mouseup', this.onGlobalMouseUp);
|
||||
document.removeEventListener('touchmove', this.onGlobalMouseMove);
|
||||
document.removeEventListener('touchend', this.onGlobalMouseUp);
|
||||
document.removeEventListener('touchcancel', this.onGlobalMouseUp);
|
||||
},
|
||||
methods: {
|
||||
createTile(type, extra = {}) {
|
||||
return {
|
||||
type,
|
||||
id: extra.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
||||
...extra
|
||||
};
|
||||
},
|
||||
|
||||
getTileType(tileOrType) {
|
||||
if (!tileOrType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return typeof tileOrType === 'string' ? tileOrType : tileOrType.type;
|
||||
},
|
||||
|
||||
getPowerUpKind(tileOrType) {
|
||||
const tileType = this.getTileType(tileOrType);
|
||||
|
||||
if (this.isRocketTile(tileType)) {
|
||||
return 'rocket';
|
||||
}
|
||||
|
||||
if (tileType === 'bomb' || tileType === 'rainbow') {
|
||||
return tileType;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
getPointerCoordinates(event) {
|
||||
const pointer = event?.touches?.[0] || event?.changedTouches?.[0];
|
||||
const x = pointer?.clientX ?? event?.clientX ?? null;
|
||||
const y = pointer?.clientY ?? event?.clientY ?? null;
|
||||
|
||||
if (x === null || y === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { x, y };
|
||||
},
|
||||
|
||||
getSwipeDirection(clientX, clientY, threshold = 18) {
|
||||
if (this.dragStartX === null || this.dragStartY === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deltaX = clientX - this.dragStartX;
|
||||
const deltaY = clientY - this.dragStartY;
|
||||
|
||||
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.abs(deltaX) >= Math.abs(deltaY)
|
||||
? (deltaX > 0 ? 'right' : 'left')
|
||||
: (deltaY > 0 ? 'down' : 'up');
|
||||
},
|
||||
|
||||
resolveDragTargetIndex(event, fallbackTileIndex = null) {
|
||||
if (this.currentlyAnimatingTile !== null && this.currentlyAnimatingTile !== this.draggedTileIndex) {
|
||||
return this.currentlyAnimatingTile;
|
||||
}
|
||||
|
||||
if (
|
||||
fallbackTileIndex !== null &&
|
||||
fallbackTileIndex !== this.draggedTileIndex &&
|
||||
this.areTilesAdjacent(this.draggedTileIndex, fallbackTileIndex)
|
||||
) {
|
||||
return fallbackTileIndex;
|
||||
}
|
||||
|
||||
const pointer = this.getPointerCoordinates(event);
|
||||
if (!pointer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pointerTargetIndex = this.findTileAtPosition(pointer.x, pointer.y);
|
||||
if (
|
||||
pointerTargetIndex !== null &&
|
||||
pointerTargetIndex !== this.draggedTileIndex &&
|
||||
this.areTilesAdjacent(this.draggedTileIndex, pointerTargetIndex)
|
||||
) {
|
||||
return pointerTargetIndex;
|
||||
}
|
||||
|
||||
const swipeDirection = this.getSwipeDirection(pointer.x, pointer.y);
|
||||
return swipeDirection ? this.getAdjacentIndex(this.draggedTileIndex, swipeDirection) : null;
|
||||
},
|
||||
|
||||
// Initialisiere Sound-Effekte
|
||||
initializeSounds() {
|
||||
try {
|
||||
@@ -1467,18 +1567,20 @@ export default {
|
||||
|
||||
// Hilfsmethode: Prüfe ob ein Tile ein Raketen-Power-up ist
|
||||
isRocketTile(tileType) {
|
||||
return tileType === 'rocket' || tileType === 'rocket-horizontal' || tileType === 'rocket-vertical';
|
||||
const normalizedType = this.getTileType(tileType);
|
||||
return normalizedType === 'rocket' || normalizedType === 'rocket-horizontal' || normalizedType === 'rocket-vertical';
|
||||
},
|
||||
|
||||
// Hilfsmethode: Prüfe ob ein Tile ein Regenbogen-Power-up ist
|
||||
isRainbowTile(tileType) {
|
||||
return tileType === 'rainbow';
|
||||
return this.getTileType(tileType) === 'rainbow';
|
||||
},
|
||||
|
||||
// Hilfsmethode: Prüfe ob ein Tile ein Power-up ist
|
||||
isPowerUpTile(tileType) {
|
||||
if (!tileType) return false;
|
||||
return this.isRocketTile(tileType) || tileType === 'bomb' || tileType === 'rocket-horizontal' || tileType === 'rocket-vertical' || this.isRainbowTile(tileType);
|
||||
const normalizedType = this.getTileType(tileType);
|
||||
if (!normalizedType) return false;
|
||||
return this.isRocketTile(normalizedType) || normalizedType === 'bomb' || this.isRainbowTile(normalizedType);
|
||||
},
|
||||
|
||||
// Hilfsmethode: Debug-Ausgabe für Power-ups
|
||||
@@ -1932,21 +2034,11 @@ export default {
|
||||
|
||||
// JETZT erst Power-ups erstellen, nachdem die Tiles entfernt wurden
|
||||
debugLog('🔧 Erstelle Power-ups nach der Tile-Entfernung...');
|
||||
const powerUpsCreated = await this.createPowerUpsForMatches(matches);
|
||||
await this.createPowerUpsForMatches(matches);
|
||||
|
||||
// Wenn Raketen erstellt wurden, lass sie im nächsten Zug starten
|
||||
if (powerUpsCreated && powerUpsCreated.rockets && powerUpsCreated.rockets.length > 0) {
|
||||
debugLog(`🚀 ${powerUpsCreated.rockets.length} Raketen erstellt - werden im nächsten Zug aktiviert`);
|
||||
|
||||
// Aktualisiere die Anzeige
|
||||
this.$forceUpdate();
|
||||
|
||||
// Warte kurz, damit die Rakete sichtbar wird
|
||||
await this.wait(300);
|
||||
|
||||
// KEINE automatische Aktivierung - Raketen bleiben auf dem Board
|
||||
// und werden erst durch Spieler-Aktionen aktiviert
|
||||
}
|
||||
this.$forceUpdate();
|
||||
await this.wait(120);
|
||||
|
||||
// Debug: Zeige alle Power-ups nach der Erstellung
|
||||
debugLog('🔧 Debug: Alle Power-ups nach createPowerUpsForMatches:');
|
||||
@@ -1994,36 +2086,12 @@ export default {
|
||||
debugLog('🔧 Debug: Alle Power-ups nach dem Füllen:');
|
||||
this.debugPowerUps();
|
||||
|
||||
// Prüfe ob Power-ups erstellt wurden - wenn ja, keine Cascade-Matches prüfen
|
||||
// Verwende den Rückgabewert von createPowerUpsForMatches
|
||||
if (!powerUpsCreated || powerUpsCreated.count === 0) {
|
||||
// Nur Cascade-Matches prüfen, wenn keine Power-ups erstellt wurden
|
||||
await this.checkForCascadeMatches();
|
||||
} else {
|
||||
debugLog(`🔧 ${powerUpsCreated.count} Power-ups erstellt - überspringe Cascade-Match-Prüfung`);
|
||||
|
||||
// Debug: Zeige alle Power-ups nach der Verarbeitung
|
||||
debugLog('🔧 Debug: Alle Power-ups nach Power-up-Verarbeitung:');
|
||||
this.debugPowerUps();
|
||||
|
||||
// Debug: Zeige alle Power-ups im Template nach der Verarbeitung
|
||||
debugLog('🔧 Debug: Power-ups im Template nach Verarbeitung:');
|
||||
for (let i = 0; i < this.board.length; i++) {
|
||||
if (this.board[i]) {
|
||||
const isPowerUp = this.isPowerUpTile(this.board[i].type);
|
||||
debugLog(`🔧 Position ${i}: Tile ${this.board[i].type}, isPowerUpTile: ${isPowerUp}`);
|
||||
if (isPowerUp) {
|
||||
debugLog(`🔧 ✅ Power-up im Template: ${this.board[i].type} an Position ${i}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.checkForCascadeMatches();
|
||||
|
||||
// WICHTIG: Prüfe Level-Objekte nach dem Verarbeiten der Matches
|
||||
if (isPlayerMove && !this.isInitializingLevel) {
|
||||
debugLog('🎯 Prüfe Level-Objekte nach Match-Verarbeitung...');
|
||||
this.checkLevelObjectives();
|
||||
}
|
||||
|
||||
this.checkLevelObjectives();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2051,15 +2119,9 @@ export default {
|
||||
|
||||
// Wenn Position leer ist, prüfe ob sie im Layout gültig ist
|
||||
if (!this.board[index]) {
|
||||
if (this.currentLevelData && this.currentLevelData.boardLayout) {
|
||||
const layoutRows = this.currentLevelData.boardLayout.split('\n');
|
||||
if (row < layoutRows.length && col < layoutRows[row].length) {
|
||||
const targetChar = layoutRows[row][col];
|
||||
if (targetChar !== 'x') {
|
||||
debugLog(`🔧 Position [${row}, ${col}] ist ungültig im Layout (${targetChar}) - überspringe`);
|
||||
continue; // Überspringe ungültige Positionen
|
||||
}
|
||||
}
|
||||
if (!this.isPlayableBoardCell(row, col)) {
|
||||
debugLog(`🔧 Position [${row}, ${col}] ist ein Design-Leerfeld - überspringe`);
|
||||
continue;
|
||||
}
|
||||
|
||||
debugLog(`🔧 Leere Position gefunden: [${row}, ${col}] -> Index ${index}`);
|
||||
@@ -2069,7 +2131,7 @@ export default {
|
||||
for (let searchRow = row - 1; searchRow >= 0; searchRow--) {
|
||||
const searchIndex = this.coordsToIndex(searchRow, col);
|
||||
|
||||
if (this.board[searchIndex] && !this.isPowerUpTile(this.board[searchIndex].type)) {
|
||||
if (this.board[searchIndex]) {
|
||||
debugLog(`🔧 Tile ${this.board[searchIndex].type} gefunden an [${searchRow}, ${col}] -> verschiebe nach [${row}, ${col}]`);
|
||||
|
||||
// Verschiebe Tile nach unten
|
||||
@@ -2079,8 +2141,8 @@ export default {
|
||||
// Aktualisiere DOM
|
||||
this.$forceUpdate();
|
||||
|
||||
// Warte kurz für Animation
|
||||
await this.wait(500);
|
||||
// Kurze Wartezeit, damit die Bewegung sichtbar bleibt ohne das Spiel zu bremsen
|
||||
await this.wait(120);
|
||||
|
||||
hasChanges = true;
|
||||
tileFound = true;
|
||||
@@ -2104,15 +2166,9 @@ export default {
|
||||
|
||||
// Wenn oberste Position leer ist, prüfe ob sie im Layout gültig ist
|
||||
if (!this.board[index]) {
|
||||
if (this.currentLevelData && this.currentLevelData.boardLayout) {
|
||||
const layoutRows = this.currentLevelData.boardLayout.split('\n');
|
||||
if (0 < layoutRows.length && col < layoutRows[0].length) {
|
||||
const targetChar = layoutRows[0][col];
|
||||
if (targetChar !== 'x') {
|
||||
debugLog(`🔧 Oberste Position [0, ${col}] ist ungültig im Layout (${targetChar}) - überspringe`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!this.isPlayableBoardCell(0, col)) {
|
||||
debugLog(`🔧 Oberste Position [0, ${col}] ist ein Design-Leerfeld - überspringe`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Erstelle neues Tile
|
||||
@@ -2125,7 +2181,7 @@ export default {
|
||||
|
||||
// Aktualisiere DOM nach dem Auffüllen
|
||||
this.$forceUpdate();
|
||||
await this.wait(100);
|
||||
await this.wait(40);
|
||||
}
|
||||
|
||||
debugLog(`🔧 Iteration ${iteration} abgeschlossen - Änderungen: ${hasChanges}`);
|
||||
@@ -2137,6 +2193,37 @@ export default {
|
||||
|
||||
debugLog('🔧 Fall-Down-Logik abgeschlossen');
|
||||
},
|
||||
|
||||
isPlayableBoardCell(row, col) {
|
||||
if (row < 0 || row >= this.boardHeight || col < 0 || col >= this.boardWidth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.boardLayout?.[row]?.[col]) {
|
||||
return this.boardLayout[row][col].type !== 'empty';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
collectEmptyPlayableFields() {
|
||||
const emptyPlayableFields = [];
|
||||
|
||||
for (let row = 0; row < this.boardHeight; row++) {
|
||||
for (let col = 0; col < this.boardWidth; col++) {
|
||||
if (!this.isPlayableBoardCell(row, col)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = this.coordsToIndex(row, col);
|
||||
if (index !== null && !this.board[index]) {
|
||||
emptyPlayableFields.push({ index, row, col });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return emptyPlayableFields;
|
||||
},
|
||||
|
||||
// Hilfsfunktion: Zeige den aktuellen Board-Zustand in der Konsole
|
||||
printBoardState() {
|
||||
@@ -2226,8 +2313,8 @@ export default {
|
||||
if (newTilesAdded > 0) {
|
||||
debugLog('🔍 Überprüfe das Brett auf Matches nach dem Füllen der leeren Positionen...');
|
||||
|
||||
// Warte kurz, damit die neuen Tiles vollständig angezeigt werden
|
||||
await this.wait(300);
|
||||
// Nur kurz warten, damit das Brett aktualisiert ist
|
||||
await this.wait(80);
|
||||
|
||||
// Prüfe auf Matches auf dem aktuellen Board
|
||||
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
|
||||
@@ -2285,7 +2372,7 @@ export default {
|
||||
match.forEach(pos => usedPositions.add(pos));
|
||||
|
||||
// Erstelle Regenbogen-Tile
|
||||
this.board[rainbowIndex] = { type: 'rainbow' };
|
||||
this.board[rainbowIndex] = this.createTile('rainbow');
|
||||
|
||||
debugLog(`🌈 Regenbogen-Tile an Position ${rainbowIndex} erstellt`);
|
||||
debugLog(`🔧 Board[${rainbowIndex}] = ${JSON.stringify(this.board[rainbowIndex])}`);
|
||||
@@ -2309,7 +2396,7 @@ export default {
|
||||
usedPositions.add(match.corner);
|
||||
|
||||
// Bombe an der Ecke erstellen
|
||||
this.board[match.corner] = { type: 'bomb' };
|
||||
this.board[match.corner] = this.createTile('bomb');
|
||||
|
||||
debugLog(`💣 Bombe an Position ${match.corner} erstellt`);
|
||||
debugLog(`🔧 Board[${match.corner}] = ${JSON.stringify(this.board[match.corner])}`);
|
||||
@@ -2337,7 +2424,7 @@ export default {
|
||||
|
||||
// Erstelle Rakete basierend auf der Richtung des Matches
|
||||
const rocketType = this.determineRocketType(match);
|
||||
this.board[rocketIndex] = { type: rocketType };
|
||||
this.board[rocketIndex] = this.createTile(rocketType);
|
||||
|
||||
debugLog(`🚀 Rakete ${rocketType} an Position ${rocketIndex} erstellt`);
|
||||
debugLog(`🔧 Board[${rocketIndex}] = ${JSON.stringify(this.board[rocketIndex])}`);
|
||||
@@ -2380,99 +2467,45 @@ export default {
|
||||
async checkAndFillEmptyValidFields() {
|
||||
debugLog('🔧 Prüfe alle leeren gültigen Felder...');
|
||||
|
||||
let hasEmptyValidFields = false;
|
||||
const emptyValidFields = [];
|
||||
|
||||
// Gehe durch alle Positionen im Board
|
||||
for (let row = 0; row < this.boardHeight; row++) {
|
||||
for (let col = 0; col < this.boardWidth; col++) {
|
||||
const index = this.coordsToIndex(row, col);
|
||||
|
||||
// Wenn Position leer ist, prüfe ob sie im Layout gültig ist
|
||||
if (!this.board[index]) {
|
||||
if (this.currentLevelData && this.currentLevelData.boardLayout) {
|
||||
const layout = this.currentLevelData.boardLayout;
|
||||
const layoutRows = layout.split('\n');
|
||||
|
||||
// Prüfe, ob das Feld im Layout gültig ist (nicht 'o')
|
||||
if (row < layoutRows.length && col < layoutRows[row].length) {
|
||||
const targetChar = layoutRows[row][col];
|
||||
|
||||
if (targetChar === 'x') {
|
||||
// Gültiges Feld ist leer - muss gefüllt werden
|
||||
emptyValidFields.push({ index, row, col });
|
||||
hasEmptyValidFields = true;
|
||||
debugLog(`🔧 Gültiges Feld [${row}, ${col}] ist leer und muss gefüllt werden`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (this.board[index] && this.isPowerUpTile(this.board[index].type)) {
|
||||
// Position enthält bereits ein Power-up - nicht überschreiben
|
||||
debugLog(`🔧 Position [${row}, ${col}] enthält bereits Power-up ${this.board[index].type} - wird nicht überschrieben`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEmptyValidFields) {
|
||||
debugLog(`🔧 ${emptyValidFields.length} leere gültige Felder gefunden - starte erneuten Fall-Prozess`);
|
||||
|
||||
// Fülle alle leeren gültigen Felder mit neuen Tiles
|
||||
let safetyCounter = 0;
|
||||
let emptyValidFields = this.collectEmptyPlayableFields();
|
||||
|
||||
while (emptyValidFields.length > 0 && safetyCounter < 8) {
|
||||
safetyCounter++;
|
||||
debugLog(`🔧 ${emptyValidFields.length} leere gültige Felder gefunden - Füllrunde ${safetyCounter}`);
|
||||
|
||||
for (const field of emptyValidFields) {
|
||||
const newTile = this.createRandomTile();
|
||||
this.board[field.index] = newTile;
|
||||
debugLog(`🔧 Neues Tile ${newTile.type} an Position [${field.row}, ${field.col}] hinzugefügt`);
|
||||
}
|
||||
|
||||
// Aktualisiere die Anzeige
|
||||
|
||||
this.$forceUpdate();
|
||||
|
||||
// Führe Animation für neue Tiles aus
|
||||
await this.animateNewTilesAppearing(emptyValidFields.map(field => ({
|
||||
index: field.index,
|
||||
row: field.row,
|
||||
col: field.col,
|
||||
type: this.board[field.index].type
|
||||
|
||||
await this.animateNewTilesAppearing(emptyValidFields.map(field => ({
|
||||
index: field.index,
|
||||
row: field.row,
|
||||
col: field.col,
|
||||
type: this.board[field.index].type
|
||||
})));
|
||||
|
||||
debugLog(`🔧 Alle leeren gültigen Felder gefüllt`);
|
||||
|
||||
// WICHTIG: Nach dem Füllen der leeren gültigen Felder das Brett auf Matches überprüfen
|
||||
debugLog('🔍 Überprüfe das Brett auf Matches nach dem Füllen der leeren gültigen Felder...');
|
||||
|
||||
// Warte kurz, damit die neuen Tiles vollständig angezeigt werden
|
||||
await this.wait(300);
|
||||
|
||||
// Prüfe auf Matches auf dem aktuellen Board
|
||||
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
|
||||
|
||||
if (matchesAfterFill.length > 0) {
|
||||
debugLog(`🔍 ${matchesAfterFill.length} Match(es) nach dem Füllen der leeren gültigen Felder gefunden - starte automatische Behandlung`);
|
||||
|
||||
// Behandle die gefundenen Matches automatisch (kein Spieler-Move)
|
||||
await this.handleMatches(matchesAfterFill, false);
|
||||
|
||||
// WICHTIG: Rekursiver Aufruf, falls durch die Matches neue leere Positionen entstehen
|
||||
// Das verhindert Endlosschleifen durch max. 3 Rekursionen
|
||||
if (this.recursionDepth === undefined) {
|
||||
this.recursionDepth = 0;
|
||||
}
|
||||
|
||||
if (this.recursionDepth < 3) {
|
||||
this.recursionDepth++;
|
||||
debugLog(`🔄 Rekursiver Aufruf ${this.recursionDepth}/3 - prüfe auf weitere leere gültige Felder`);
|
||||
|
||||
// Prüfe erneut auf leere gültige Felder und fülle sie auf
|
||||
await this.checkAndFillEmptyValidFields();
|
||||
|
||||
this.recursionDepth--;
|
||||
} else {
|
||||
debugLog('⚠️ Maximale Rekursionstiefe erreicht - stoppe automatische Match-Behandlung');
|
||||
}
|
||||
} else {
|
||||
debugLog('✅ Keine Matches nach dem Füllen der leeren gültigen Felder gefunden - Board ist bereit');
|
||||
}
|
||||
} else {
|
||||
debugLog('🔧 Alle gültigen Felder enthalten Tiles - Board ist vollständig');
|
||||
|
||||
await this.wait(40);
|
||||
await this.fallTilesDown();
|
||||
emptyValidFields = this.collectEmptyPlayableFields();
|
||||
}
|
||||
|
||||
if (safetyCounter >= 8 && emptyValidFields.length > 0) {
|
||||
debugLog(`⚠️ Brett konnte nach ${safetyCounter} Füllrunden nicht vollständig stabilisiert werden`);
|
||||
}
|
||||
|
||||
if (emptyValidFields.length === 0) {
|
||||
debugLog('🔧 Alle spielbaren Felder enthalten Tiles - Board ist vollständig');
|
||||
}
|
||||
|
||||
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
|
||||
if (matchesAfterFill.length > 0) {
|
||||
debugLog(`🔍 ${matchesAfterFill.length} Match(es) nach der Stabilisierung gefunden - starte automatische Behandlung`);
|
||||
await this.handleMatches(matchesAfterFill, false);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2498,8 +2531,8 @@ export default {
|
||||
|
||||
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Animation vorbereitet`);
|
||||
|
||||
// Warte auf die Animation (0,75 Sekunden)
|
||||
await this.wait(750);
|
||||
// Warte auf die Animation
|
||||
await this.wait(280);
|
||||
|
||||
// Entferne die CSS-Klassen
|
||||
tileElements.forEach(element => {
|
||||
@@ -2525,7 +2558,7 @@ export default {
|
||||
|
||||
// Setze das Tile an seine ursprüngliche Position (oben) mit transform
|
||||
element.style.transform = `translateY(-${fallPixels}px)`;
|
||||
element.style.transition = 'transform 0.4s ease-out';
|
||||
element.style.transition = 'transform 0.18s ease-out';
|
||||
|
||||
// Füge CSS-Klasse für die Fall-Animation hinzu
|
||||
element.classList.add('falling');
|
||||
@@ -2553,8 +2586,8 @@ export default {
|
||||
// Spiele Fall-Sound ab
|
||||
this.playSound('falling');
|
||||
|
||||
// Warte auf die Fall-Animation (0,4 Sekunden)
|
||||
await this.wait(400);
|
||||
// Warte auf die Fall-Animation
|
||||
await this.wait(180);
|
||||
|
||||
// Entferne die CSS-Klassen und transform-Eigenschaften
|
||||
tileElements.forEach(element => {
|
||||
@@ -2588,8 +2621,8 @@ export default {
|
||||
|
||||
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Erscheinungs-Animation vorbereitet`);
|
||||
|
||||
// Warte auf die Erscheinungs-Animation (0,5 Sekunden)
|
||||
await this.wait(500);
|
||||
// Warte auf die Erscheinungs-Animation
|
||||
await this.wait(180);
|
||||
|
||||
// Entferne die CSS-Klassen
|
||||
tileElements.forEach(element => {
|
||||
@@ -2606,12 +2639,12 @@ export default {
|
||||
const randomType = this.currentLevelData.tileTypes[
|
||||
Math.floor(Math.random() * this.currentLevelData.tileTypes.length)
|
||||
];
|
||||
return { type: randomType };
|
||||
return this.createTile(randomType);
|
||||
} else {
|
||||
// Fallback: Verwende Standard-Tile-Typen
|
||||
const defaultTypes = ['red', 'blue', 'green', 'yellow', 'purple'];
|
||||
const randomType = defaultTypes[Math.floor(Math.random() * defaultTypes.length)];
|
||||
return { type: randomType };
|
||||
return this.createTile(randomType);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2802,28 +2835,15 @@ export default {
|
||||
async checkForCascadeMatches() {
|
||||
debugLog('🔧 Prüfe auf Cascade-Matches...');
|
||||
|
||||
// Warte kurz, damit alle Animationen abgeschlossen sind
|
||||
await this.wait(200);
|
||||
// Nur kurze Synchronisierung nach dem Fallen
|
||||
await this.wait(60);
|
||||
|
||||
// Prüfe ob neue Matches entstanden sind
|
||||
const newMatches = this.findMatchesOnBoard(this.board, false);
|
||||
|
||||
// Filtere Power-up-Matches heraus (diese sollen nicht als Cascade-Matches behandelt werden)
|
||||
const filteredMatches = newMatches.filter(match => {
|
||||
if (match.type === 'l-shape') {
|
||||
debugLog('🔧 L-Form Match in Cascade gefunden - überspringe');
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(match) && match.length === 4) {
|
||||
debugLog('🔧 4er-Match in Cascade gefunden - überspringe');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredMatches.length > 0) {
|
||||
debugLog(`🔧 ${filteredMatches.length} neue Cascade-Matches gefunden`);
|
||||
await this.handleMatches(filteredMatches, false);
|
||||
if (newMatches.length > 0) {
|
||||
debugLog(`🔧 ${newMatches.length} neue Cascade-Matches gefunden`);
|
||||
await this.handleMatches(newMatches, false);
|
||||
} else {
|
||||
debugLog('🔧 Keine neuen Cascade-Matches gefunden');
|
||||
}
|
||||
@@ -3411,14 +3431,19 @@ export default {
|
||||
}
|
||||
|
||||
debugLog(`🔧 Starte Drag für Tile ${tileIndex}`);
|
||||
|
||||
const pointer = this.getPointerCoordinates(event);
|
||||
if (!pointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setze Drag-Status
|
||||
this.draggedTileIndex = tileIndex;
|
||||
this.isDragging = true;
|
||||
|
||||
// Speichere Start-Position für Drag-Offset
|
||||
this.dragStartX = event.clientX;
|
||||
this.dragStartY = event.clientY;
|
||||
this.dragStartX = pointer.x;
|
||||
this.dragStartY = pointer.y;
|
||||
|
||||
// WICHTIG: Speichere die ursprüngliche Position des gedraggten Tiles
|
||||
const tileElement = event.target.closest('.game-tile');
|
||||
@@ -3483,12 +3508,21 @@ export default {
|
||||
if (!this.isDragging || this.draggedTileIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointer = this.getPointerCoordinates(event);
|
||||
if (!pointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event?.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
debugLog(`🔧 onTileMouseMove: clientX=${event.clientX}, clientY=${event.clientY}`);
|
||||
debugLog(`🔧 onTileMouseMove: clientX=${pointer.x}, clientY=${pointer.y}`);
|
||||
|
||||
// Berechne Drag-Offset
|
||||
const deltaX = event.clientX - this.dragStartX;
|
||||
const deltaY = event.clientY - this.dragStartY;
|
||||
const deltaX = pointer.x - this.dragStartX;
|
||||
const deltaY = pointer.y - this.dragStartY;
|
||||
|
||||
debugLog(`🔧 Drag-Offset: deltaX=${deltaX}px, deltaY=${deltaY}px`);
|
||||
|
||||
@@ -3724,7 +3758,7 @@ export default {
|
||||
onGlobalMouseUp(event) {
|
||||
if (this.isDragging) {
|
||||
debugLog(`🔧 Globaler MouseUp während Drag - beende Drag`);
|
||||
this.endDrag();
|
||||
this.endDrag(event);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3744,23 +3778,11 @@ export default {
|
||||
debugLog(`🔧 Beende Drag: draggedTileIndex=${this.draggedTileIndex}, targetTile=${tileIndex}`);
|
||||
|
||||
// WICHTIG: Prüfe ob ein Tile tatsächlich animiert wurde
|
||||
let shouldPerformMove = false;
|
||||
let targetTileIndex = null;
|
||||
|
||||
if (this.draggedTileIndex !== tileIndex) {
|
||||
// Verschiedene Tiles - prüfe ob das Ziel-Tile animiert wurde
|
||||
if (this.currentlyAnimatingTile === tileIndex) {
|
||||
// Das Ziel-Tile ist animiert - führe Move durch
|
||||
shouldPerformMove = true;
|
||||
targetTileIndex = tileIndex;
|
||||
debugLog(`🔧 Ziel-Tile ${tileIndex} ist animiert - führe Move durch`);
|
||||
} else {
|
||||
// Das Ziel-Tile ist nicht animiert - kein Move
|
||||
debugLog(`🔧 Ziel-Tile ${tileIndex} ist nicht animiert - kein Move`);
|
||||
}
|
||||
} else {
|
||||
debugLog(`🔧 Gleiches Tile, kein Move erforderlich`);
|
||||
}
|
||||
const targetTileIndex = this.resolveDragTargetIndex(event, tileIndex);
|
||||
const shouldPerformMove =
|
||||
targetTileIndex !== null &&
|
||||
targetTileIndex !== this.draggedTileIndex &&
|
||||
this.areTilesAdjacent(this.draggedTileIndex, targetTileIndex);
|
||||
|
||||
// Setze alle Animationen zurück
|
||||
this.resetAllTileAnimations();
|
||||
@@ -3786,7 +3808,7 @@ export default {
|
||||
},
|
||||
|
||||
// Beende den Drag korrekt
|
||||
endDrag() {
|
||||
endDrag(event = null) {
|
||||
debugLog(`🔧 endDrag aufgerufen`);
|
||||
|
||||
if (!this.isDragging) {
|
||||
@@ -3795,20 +3817,11 @@ export default {
|
||||
}
|
||||
|
||||
// Prüfe ob ein Tile tatsächlich animiert wurde UND ob es sich um ein anderes Tile handelt
|
||||
let shouldPerformMove = false;
|
||||
let targetTileIndex = null;
|
||||
|
||||
if (this.currentlyAnimatingTile !== null && this.currentlyAnimatingTile !== this.draggedTileIndex) {
|
||||
// Ein anderes Tile ist animiert - führe Move durch
|
||||
shouldPerformMove = true;
|
||||
targetTileIndex = this.currentlyAnimatingTile;
|
||||
debugLog(`🔧 Anderes Tile ${targetTileIndex} ist animiert - führe Move durch`);
|
||||
} else if (this.currentlyAnimatingTile === this.draggedTileIndex) {
|
||||
// Das gedraggte Tile ist auf sich selbst animiert - kein Move
|
||||
debugLog(`🔧 Gedraggtes Tile ist auf sich selbst animiert - kein Move`);
|
||||
} else {
|
||||
debugLog(`🔧 Kein Tile animiert - kein Move`);
|
||||
}
|
||||
let targetTileIndex = this.resolveDragTargetIndex(event);
|
||||
let shouldPerformMove =
|
||||
targetTileIndex !== null &&
|
||||
targetTileIndex !== this.draggedTileIndex &&
|
||||
this.areTilesAdjacent(this.draggedTileIndex, targetTileIndex);
|
||||
|
||||
// Zusätzliche Prüfung: Wenn das gedraggte Tile fast an seiner ursprünglichen Position ist, kein Move
|
||||
if (shouldPerformMove && this.originalTilePosition) {
|
||||
@@ -4396,7 +4409,8 @@ export default {
|
||||
|
||||
// Hilfsmethode: Prüfe ob ein Tile ein Power-Up ist
|
||||
isPowerUpTile(tile) {
|
||||
return tile && this.powerUpTypes.includes(tile.type);
|
||||
const tileType = this.getTileType(tile);
|
||||
return this.getPowerUpKind(tileType) !== null;
|
||||
},
|
||||
|
||||
// Hilfsmethode: Prüfe ob zwei Tiles matchen können
|
||||
@@ -4992,8 +5006,17 @@ export default {
|
||||
// Power-Up Tausch als Zug zählen (wird auch über swapTiles aufgerufen)
|
||||
// WICHTIG: Zähle den Zug nur einmal hier, nicht in den nachfolgenden Funktionen
|
||||
this.countPowerUpMove();
|
||||
|
||||
const tile1Type = this.getTileType(originalTile1);
|
||||
const tile2Type = this.getTileType(originalTile2);
|
||||
const tile1IsRocket = this.isRocketTile(tile1Type);
|
||||
const tile2IsRocket = this.isRocketTile(tile2Type);
|
||||
const tile1IsBomb = tile1Type === 'bomb';
|
||||
const tile2IsBomb = tile2Type === 'bomb';
|
||||
const tile1IsRainbow = tile1Type === 'rainbow';
|
||||
const tile2IsRainbow = tile2Type === 'rainbow';
|
||||
|
||||
if (originalTile1.type === 'rainbow' && originalTile2.type === 'rainbow') {
|
||||
if (tile1IsRainbow && tile2IsRainbow) {
|
||||
// Spiele Regenbogen-Sound
|
||||
this.playSound('rainbow');
|
||||
|
||||
@@ -5001,29 +5024,48 @@ export default {
|
||||
debugLog('🌈 Zwei Regenbogen-Tiles kombiniert - entferne alle Tiles vom Board!');
|
||||
await this.removeAllTilesFromBoardIncludingRainbows();
|
||||
return true;
|
||||
} else if (originalTile1.type === 'rainbow' || originalTile2.type === 'rainbow') {
|
||||
} else if ((tile1IsBomb && tile2IsRainbow) || (tile1IsRainbow && tile2IsBomb)) {
|
||||
this.playSound('rainbow');
|
||||
this.createRandomBombs(20);
|
||||
setTimeout(() => {
|
||||
this.detonateAllBombs();
|
||||
}, 500);
|
||||
return true;
|
||||
} else if ((tile1IsRocket && tile2IsRainbow) || (tile1IsRainbow && tile2IsRocket)) {
|
||||
this.playSound('rainbow');
|
||||
this.createRandomRockets(10);
|
||||
setTimeout(() => {
|
||||
this.launchAllRockets();
|
||||
}, 500);
|
||||
return true;
|
||||
} else if (tile1IsRainbow || tile2IsRainbow) {
|
||||
// Spiele Regenbogen-Sound
|
||||
this.playSound('rainbow');
|
||||
|
||||
// Ein Regenbogen-Tile mit normalem Tile getauscht: Entferne alle Tiles des normalen Typs
|
||||
const normalTile = originalTile1.type === 'rainbow' ? originalTile2 : originalTile1;
|
||||
const rainbowIndex = originalTile1.type === 'rainbow' ? this.findTileIndex(originalTile1) : this.findTileIndex(originalTile2);
|
||||
const normalTile = tile1IsRainbow ? originalTile2 : originalTile1;
|
||||
const rainbowIndex = tile1IsRainbow ? this.findTileIndex(originalTile1) : this.findTileIndex(originalTile2);
|
||||
|
||||
if (rainbowIndex !== null) {
|
||||
await this.activateRainbowByType(rainbowIndex, normalTile.type);
|
||||
}
|
||||
return true;
|
||||
} else if (originalTile1.type === 'bomb' && originalTile2.type === 'bomb') {
|
||||
} else if (tile1IsBomb && tile2IsBomb) {
|
||||
// Zwei Bomben-Tiles getauscht: Entferne 2 Ringe (5x5 Bereich)
|
||||
const bombIndex = this.findBombIndex(originalTile1, originalTile2);
|
||||
if (bombIndex !== null) {
|
||||
this.explodeBomb(bombIndex, 2, true); // 2 Ringe = 5x5 Bereich - manuelle Aktivierung
|
||||
}
|
||||
return true;
|
||||
} else if (originalTile1.type === 'bomb' || originalTile2.type === 'bomb') {
|
||||
} else if ((tile1IsRocket && tile2IsBomb) || (tile1IsBomb && tile2IsRocket)) {
|
||||
this.handleRocketBombCombination(originalTile1, originalTile2);
|
||||
return true;
|
||||
} else if (tile1IsRocket && tile2IsRocket) {
|
||||
this.handleRocketConnection(originalTile1, originalTile2);
|
||||
return true;
|
||||
} else if (tile1IsBomb || tile2IsBomb) {
|
||||
// Ein Bomben-Tile mit normalem Tile getauscht: Entferne 9 Tiles rund um das Ziel
|
||||
const bombTile = originalTile1.type === 'bomb' ? originalTile1 : originalTile2;
|
||||
const targetTile = originalTile1.type === 'bomb' ? originalTile2 : originalTile1;
|
||||
const bombTile = tile1IsBomb ? originalTile1 : originalTile2;
|
||||
|
||||
// Finde die neue Position der Bombe
|
||||
const newBombIndex = this.findTileIndex(bombTile);
|
||||
@@ -5031,41 +5073,10 @@ export default {
|
||||
this.explodeBomb(newBombIndex, 1, true); // 1 Ring = 3x3 Bereich - manuelle Aktivierung
|
||||
}
|
||||
return true;
|
||||
} else if ((originalTile1.type === 'bomb' && originalTile2.type === 'rainbow') ||
|
||||
(originalTile1.type === 'rainbow' && originalTile2.type === 'bomb')) {
|
||||
// Spiele Regenbogen-Sound
|
||||
this.playSound('rainbow');
|
||||
|
||||
// Bombe + Regenbogen: Erstelle zufällige Bomben und löse sie aus
|
||||
this.createRandomBombs(20);
|
||||
setTimeout(() => {
|
||||
this.detonateAllBombs();
|
||||
}, 500); // Kurze Verzögerung für visuellen Effekt
|
||||
return true;
|
||||
} else if (originalTile1.type === 'rocket' && originalTile2.type === 'rocket') {
|
||||
// Zwei Raketen verbunden: Lösche Nachbarfelder beider Raketen und starte 3 Raketen
|
||||
this.handleRocketConnection(originalTile1, originalTile2);
|
||||
return true;
|
||||
} else if ((originalTile1.type === 'rocket' && originalTile2.type === 'rainbow') ||
|
||||
(originalTile1.type === 'rainbow' && originalTile2.type === 'rocket')) {
|
||||
// Spiele Regenbogen-Sound
|
||||
this.playSound('rainbow');
|
||||
|
||||
// Rakete + Regenbogen: Erstelle zufällige Raketen und starte sie
|
||||
this.createRandomRockets(10);
|
||||
setTimeout(() => {
|
||||
this.launchAllRockets();
|
||||
}, 500); // Kurze Verzögerung für visuellen Effekt
|
||||
return true;
|
||||
} else if ((originalTile1.type === 'rocket' && originalTile2.type === 'bomb') ||
|
||||
(originalTile1.type === 'bomb' && originalTile2.type === 'rocket')) {
|
||||
// Rakete + Bombe: Lösche 4 Nachbarfelder und starte Bomben-Rakete
|
||||
this.handleRocketBombCombination(originalTile1, originalTile2);
|
||||
return true;
|
||||
} else if (originalTile1.type === 'rocket' || originalTile2.type === 'rocket') {
|
||||
} else if (tile1IsRocket || tile2IsRocket) {
|
||||
// Eine Rakete mit normalem Tile: Rakete fliegt auf zufälliges Feld
|
||||
const rocketTile = originalTile1.type === 'rocket' ? originalTile1 : originalTile2;
|
||||
const targetTile = originalTile1.type === 'rocket' ? originalTile2 : originalTile1;
|
||||
const rocketTile = tile1IsRocket ? originalTile1 : originalTile2;
|
||||
const targetTile = tile1IsRocket ? originalTile2 : originalTile1;
|
||||
this.handleRocketLaunch(rocketTile, targetTile);
|
||||
return true;
|
||||
}
|
||||
@@ -5087,7 +5098,10 @@ export default {
|
||||
findTileIndex(originalTile) {
|
||||
// Suche nach dem Tile auf dem Board
|
||||
for (let i = 0; i < this.board.length; i++) {
|
||||
if (this.board[i] && this.board[i].id === originalTile.id) {
|
||||
if (this.board[i] === originalTile) {
|
||||
return i;
|
||||
}
|
||||
if (this.board[i] && originalTile && this.board[i].id && originalTile.id && this.board[i].id === originalTile.id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -5291,7 +5305,7 @@ export default {
|
||||
if (powerUp.type === 'bomb') {
|
||||
// Bombe explodiert mit 1 Ring (3x3 Bereich)
|
||||
this.explodeBomb(powerUp.index, 1, true);
|
||||
} else if (powerUp.type === 'rocket') {
|
||||
} else if (this.isRocketTile(powerUp.type)) {
|
||||
// Rakete startet auf zufälliges Feld
|
||||
this.launchRocketToRandomField(powerUp.index);
|
||||
}
|
||||
@@ -5401,7 +5415,7 @@ export default {
|
||||
|
||||
// Finde alle Raketen auf dem Board
|
||||
for (let i = 0; i < this.board.length; i++) {
|
||||
if (this.board[i] && this.board[i].type === 'rocket') {
|
||||
if (this.board[i] && this.isRocketTile(this.board[i].type)) {
|
||||
rocketIndices.push(i);
|
||||
}
|
||||
}
|
||||
@@ -5424,7 +5438,7 @@ export default {
|
||||
// Neue Methode: Behandle Rakete + Bombe Kombination
|
||||
handleRocketBombCombination(originalTile1, originalTile2) {
|
||||
// Finde die Position einer der Power-Ups (für die 4 Nachbarfelder)
|
||||
const rocketTile = originalTile1.type === 'rocket' ? originalTile1 : originalTile2;
|
||||
const rocketTile = this.isRocketTile(originalTile1.type) ? originalTile1 : originalTile2;
|
||||
const bombTile = originalTile1.type === 'bomb' ? originalTile1 : originalTile2;
|
||||
|
||||
// Finde die Position auf dem Board
|
||||
@@ -5562,12 +5576,18 @@ export default {
|
||||
x: endRect.left - boardRect.left + endRect.width / 2 - 20,
|
||||
y: endRect.top - boardRect.top + endRect.height / 2 - 20
|
||||
};
|
||||
|
||||
this.rocketTarget = {
|
||||
x: endRect.left - boardRect.left + endRect.width / 2,
|
||||
y: endRect.top - boardRect.top + endRect.height / 2
|
||||
};
|
||||
|
||||
this.showRocketFlight = true;
|
||||
|
||||
// Verstecke Animation nach der Dauer
|
||||
setTimeout(() => {
|
||||
this.showRocketFlight = false;
|
||||
this.rocketTarget = { x: -1, y: -1 };
|
||||
}, 1200);
|
||||
}
|
||||
});
|
||||
@@ -6410,6 +6430,7 @@ export default {
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
position: relative; /* Wichtig für absolute Positionierung der animierten Tiles */
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.game-tile {
|
||||
@@ -6427,6 +6448,7 @@ export default {
|
||||
pointer-events: auto;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* Vergrößere den klickbaren Bereich für besseres Drag&Drop */
|
||||
@@ -6478,7 +6500,7 @@ export default {
|
||||
|
||||
/* Schrumpf-Animation für das Entfernen von Tiles */
|
||||
.game-tile.removing {
|
||||
transition: all 0.75s ease-out;
|
||||
transition: all 0.28s ease-out;
|
||||
transform: scale(0.1) rotate(360deg);
|
||||
opacity: 0;
|
||||
z-index: 200;
|
||||
@@ -6498,12 +6520,12 @@ export default {
|
||||
|
||||
/* Fall-Animation */
|
||||
.game-tile.falling {
|
||||
transition: transform 0.5s ease-in, opacity 0.3s ease-out;
|
||||
transition: transform 0.18s ease-in, opacity 0.18s ease-out;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.game-tile.new-tile {
|
||||
transition: opacity 0.5s ease-in;
|
||||
transition: opacity 0.18s ease-in;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@@ -6741,12 +6763,25 @@ export default {
|
||||
animation: explosion 2s ease-out forwards;
|
||||
}
|
||||
|
||||
.rocket-flight {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(45deg, #ff6b6b, #ffd93d);
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
animation: rocketFlight 3s ease-in-out forwards;
|
||||
.rocket-flight-path {
|
||||
position: relative;
|
||||
width: 0;
|
||||
height: 0;
|
||||
animation: rocketTravel 0.9s ease-in-out forwards;
|
||||
}
|
||||
|
||||
.rocket-flight-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-left: -22px;
|
||||
margin-top: -22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
background: linear-gradient(135deg, #fff7e8, #ffd7a8);
|
||||
box-shadow: 0 10px 24px rgba(181, 94, 21, 0.28);
|
||||
}
|
||||
|
||||
.rainbow-effect {
|
||||
@@ -6895,35 +6930,51 @@ export default {
|
||||
}
|
||||
|
||||
/* Raketen-Flug-Animation */
|
||||
.rocket-flight-animation {
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
.rocket-target-marker {
|
||||
position: absolute;
|
||||
z-index: 1400;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.rocket-flight {
|
||||
.rocket-target-marker__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
animation: rocketFlight 1s ease-in-out forwards;
|
||||
font-size: 22px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 0 0 6px rgba(255, 107, 107, 0.18);
|
||||
animation: rocketTargetPulse 1.2s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes rocketFlight {
|
||||
@keyframes rocketTravel {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
transform: translate(0, 0) scale(0.7) rotate(-10deg);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
15% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
transform: translate(var(--dx), var(--dy)) scale(1) rotate(8deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rocketTargetPulse {
|
||||
0% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0;
|
||||
}
|
||||
35% {
|
||||
transform: scale(1.08);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +411,6 @@ export default {
|
||||
}, 2 * 60 * 1000); // alle 2 Minuten
|
||||
},
|
||||
beforeUnmount() {
|
||||
console.log('🚪 Component unmounting, cleaning up...');
|
||||
this.cleanup();
|
||||
},
|
||||
methods: {
|
||||
@@ -854,8 +853,6 @@ export default {
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
console.log('🧹 Starting cleanup...');
|
||||
|
||||
// Game Loop stoppen
|
||||
if (this.gameLoop) {
|
||||
clearTimeout(this.gameLoop);
|
||||
@@ -929,20 +926,15 @@ export default {
|
||||
this.passengerImages = {};
|
||||
this.carImage = null; // Auto-Bild bereinigen
|
||||
this.tiles = null;
|
||||
|
||||
console.log('🧹 Cleanup completed');
|
||||
},
|
||||
|
||||
// Regelmäßige Memory-Cleanup-Methode
|
||||
performMemoryCleanup() {
|
||||
console.log('🧹 Performing memory cleanup...');
|
||||
|
||||
// Canvas NICHT leeren – das verursacht sichtbares Flackern / Grau
|
||||
// Wir verlassen uns auf das reguläre render() zum Überschreiben des Frames
|
||||
|
||||
// Traffic Light States aggressiver bereinigen
|
||||
if (this.trafficLightStates && Object.keys(this.trafficLightStates).length > 20) {
|
||||
console.log('🧹 Cleaning up traffic light states');
|
||||
// Nur States für aktuelle Map behalten
|
||||
if (this.currentMap && this.currentMap.tiles) {
|
||||
const currentTileKeys = new Set();
|
||||
@@ -966,12 +958,10 @@ export default {
|
||||
|
||||
// Passagier-Listen aggressiver begrenzen
|
||||
if (this.waitingPassengersList && this.waitingPassengersList.length > 20) {
|
||||
console.log('🧹 Trimming waiting passengers list');
|
||||
this.waitingPassengersList = this.waitingPassengersList.slice(-10);
|
||||
}
|
||||
|
||||
if (this.loadedPassengersList && this.loadedPassengersList.length > 20) {
|
||||
console.log('🧹 Trimming loaded passengers list');
|
||||
this.loadedPassengersList = this.loadedPassengersList.slice(-10);
|
||||
}
|
||||
|
||||
@@ -989,8 +979,6 @@ export default {
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
}
|
||||
|
||||
console.log('🧹 Memory cleanup completed');
|
||||
},
|
||||
|
||||
generateLevel() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
<span class="calendar-kicker">Planung</span>
|
||||
<h2>{{ $t('personal.calendar.title') }}</h2>
|
||||
<p>Termine, Geburtstage und eigene Eintraege in einer strukturierten Uebersicht.</p>
|
||||
<p>Termine, Geburtstage und eigene Einträge in einer strukturierten Übersicht.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
<p>Produktion, Lager, Handel und Finanzen greifen ineinander und erzeugen eine langfristige Aufbauspiel-Dynamik.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Persoenliche Entwicklung</h2>
|
||||
<h2>Persönliche Entwicklung</h2>
|
||||
<p>Familie, Bildung, Gesundheit und gesellschaftlicher Status beeinflussen deinen Weg in Falukant.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Politik und Unterwelt</h2>
|
||||
<p>Zwischen Kirche, Reputation, Adel und dunklen Netzwerken entstehen Entscheidungen mit spuerbaren Folgen.</p>
|
||||
<p>Zwischen Kirche, Reputation, Adel und dunklen Netzwerken entstehen Entscheidungen mit spürbaren Folgen.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
<div class="cards">
|
||||
<article>
|
||||
<h2>Match 3</h2>
|
||||
<p>Das klassische Puzzle-Prinzip mit Kampagnenstruktur fuer Spielerinnen und Spieler, die kurze Sessions lieben.</p>
|
||||
<p>Das klassische Puzzle-Prinzip mit Kampagnenstruktur für Spielerinnen und Spieler, die kurze Sessions lieben.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Taxi</h2>
|
||||
<p>Fahre Passagiere effizient ans Ziel und verbessere deine Kontrolle, Streckenwahl und Reaktionsfaehigkeit.</p>
|
||||
<p>Fahre Passagiere effizient ans Ziel und verbessere deine Kontrolle, Streckenwahl und Reaktionsfähigkeit.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<section class="marketing-page">
|
||||
<div class="hero">
|
||||
<p class="eyebrow">Sprachen online lernen</p>
|
||||
<h1>Der Vokabeltrainer auf YourPart kombiniert Lernen, Kurse und Uebungen in einer Plattform.</h1>
|
||||
<h1>Der Vokabeltrainer auf YourPart kombiniert Lernen, Kurse und Übungen in einer Plattform.</h1>
|
||||
<p class="lead">
|
||||
Arbeite mit interaktiven Lektionen, erweitere deinen Wortschatz und nutze strukturierte Inhalte fuer einen
|
||||
Arbeite mit interaktiven Lektionen, erweitere deinen Wortschatz und nutze strukturierte Inhalte für einen
|
||||
motivierenden Lernfluss direkt im Browser.
|
||||
</p>
|
||||
<router-link class="cta" to="/">Kostenlos starten</router-link>
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="features">
|
||||
<article>
|
||||
<h2>Interaktive Kurse</h2>
|
||||
<p>Kurse, Lektionen und Uebungen helfen beim systematischen Aufbau neuer Sprachkenntnisse.</p>
|
||||
<p>Kurse, Lektionen und Übungen helfen beim systematischen Aufbau neuer Sprachkenntnisse.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Praxisorientiert</h2>
|
||||
@@ -21,7 +21,7 @@
|
||||
</article>
|
||||
<article>
|
||||
<h2>Teil einer Community</h2>
|
||||
<p>Der Sprachbereich ist in eine groessere Community-Plattform mit Blogs, Forum und Chat eingebettet.</p>
|
||||
<p>Der Sprachbereich ist in eine größere Community-Plattform mit Blogs, Forum und Chat eingebettet.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
<input type="password" v-model="newpasswordretype"
|
||||
:placeholder="$t('settings.account.newpasswordretype')" autocomplete="new-password"
|
||||
:class="{ 'field-error': newpasswordretype && !passwordsMatch }" />
|
||||
<span v-if="newpasswordretype && !passwordsMatch" class="form-error">Die Passwoerter stimmen nicht ueberein.</span>
|
||||
<span v-if="newpasswordretype && !passwordsMatch" class="form-error">Die Passwörter stimmen nicht überein.</span>
|
||||
</label>
|
||||
|
||||
<label class="account-settings__field account-settings__field--full">
|
||||
<span>{{ $t("settings.account.oldpassword") }}</span>
|
||||
<input type="password" v-model="oldpassword" :placeholder="$t('settings.account.oldpassword')"
|
||||
autocomplete="current-password" :class="{ 'field-error': requiresOldPassword && !oldpassword.trim() }" />
|
||||
<span v-if="requiresOldPassword && !oldpassword.trim()" class="form-error">Zum Passwortwechsel wird das aktuelle Passwort benoetigt.</span>
|
||||
<span v-if="requiresOldPassword && !oldpassword.trim()" class="form-error">Zum Passwortwechsel wird das aktuelle Passwort benötigt.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -96,13 +96,13 @@ export default {
|
||||
}
|
||||
// Validiere Passwort-Wiederholung nur wenn ein neues Passwort eingegeben wurde
|
||||
if (!this.passwordsMatch) {
|
||||
showError(this, 'Die Passwoerter stimmen nicht ueberein.');
|
||||
showError(this, 'Die Passwörter stimmen nicht überein.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob das alte Passwort eingegeben wurde
|
||||
if (!this.oldpassword || this.oldpassword.trim() === '') {
|
||||
showError(this, 'Bitte geben Sie Ihr aktuelles Passwort ein, um das Passwort zu aendern.');
|
||||
showError(this, 'Bitte geben Sie Ihr aktuelles Passwort ein, um das Passwort zu ändern.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div class="diary-view">
|
||||
<section class="diary-hero surface-card">
|
||||
<div>
|
||||
<span class="diary-kicker">Persoenliche Eintraege</span>
|
||||
<span class="diary-kicker">Persönliche Einträge</span>
|
||||
<h2>{{ $t('socialnetwork.diary.title') }}</h2>
|
||||
<p>Gedanken, Notizen und kurze Updates in einer ruhigen, persoenlichen Ansicht.</p>
|
||||
<p>Gedanken, Notizen und kurze Updates in einer ruhigen, persönlichen Ansicht.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -66,14 +66,13 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sanitizedText(entry) {
|
||||
return DOMPurify.sanitize(entry.text);
|
||||
},
|
||||
async loadDiaryEntries(page) {
|
||||
try {
|
||||
console.log(page);
|
||||
const response = await apiClient.get(`/api/socialnetwork/diary/${page}`);
|
||||
this.diaryEntries = response.data.entries;
|
||||
this.currentPage = page;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
<div class="forum-topic-back link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</div>
|
||||
<h2 v-if="forumTopic">{{ forumTopic }}</h2>
|
||||
<p>Diskussionen, Antworten und neue Beitraege in einer fokussierten Leseflaeche.</p>
|
||||
<p>Diskussionen, Antworten und neue Beiträge in einer fokussierten Lesefläche.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<div>
|
||||
<span class="forum-kicker">Community-Forum</span>
|
||||
<h2>{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
|
||||
<p>Themen, Diskussionen und neue Beitraege an einem strukturierten Ort.</p>
|
||||
<p>Themen, Diskussionen und neue Beiträge an einem strukturierten Ort.</p>
|
||||
</div>
|
||||
<div class="creationtoggler">
|
||||
<button @click="createNewTopic">
|
||||
<button @click="toggleCreation">
|
||||
{{ $t(!inCreation
|
||||
? 'socialnetwork.forum.showNewTopic'
|
||||
: 'socialnetwork.forum.hideNewTopic') }}
|
||||
@@ -16,16 +16,26 @@
|
||||
</section>
|
||||
|
||||
<section v-if="inCreation" class="forum-creation surface-card">
|
||||
<div class="forum-creation__header">
|
||||
<div>
|
||||
<h3>Neues Thema verfassen</h3>
|
||||
<p>Erst Titel setzen, dann den Beitrag schreiben und anschließend direkt veröffentlichen.</p>
|
||||
</div>
|
||||
<button type="button" class="button-secondary" @click="cancelCreation">Abbrechen</button>
|
||||
</div>
|
||||
<label class="newtitle">
|
||||
<span>{{ $t('socialnetwork.forum.topic') }}</span>
|
||||
<input type="text" v-model="newTitle" />
|
||||
<input ref="titleInput" type="text" v-model="newTitle" />
|
||||
</label>
|
||||
<div class="editor-container">
|
||||
<EditorContent v-if="editor" :editor="editor" class="editor" />
|
||||
</div>
|
||||
<button @click="saveNewTopic">
|
||||
{{ $t('socialnetwork.forum.createNewTopic') }}
|
||||
</button>
|
||||
<div class="forum-creation__actions">
|
||||
<button :disabled="!canSaveTopic" @click="saveNewTopic">
|
||||
{{ $t('socialnetwork.forum.createNewTopic') }}
|
||||
</button>
|
||||
<span class="forum-creation__hint">Titel und Inhalt müssen beide ausgefüllt sein.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="titles.length > 0" class="forum-topics surface-card">
|
||||
@@ -47,7 +57,8 @@
|
||||
</section>
|
||||
|
||||
<div v-else class="forum-empty surface-card">
|
||||
{{ $t('socialnetwork.forum.noTitles') }}
|
||||
<p>{{ $t('socialnetwork.forum.noTitles') }}</p>
|
||||
<button type="button" @click="toggleCreation">{{ $t('socialnetwork.forum.createNewTopic') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -56,6 +67,7 @@
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import apiClient from '../../utils/axios'
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'ForumView',
|
||||
@@ -78,6 +90,10 @@ export default {
|
||||
},
|
||||
totalPages() {
|
||||
return Math.ceil(this.numberOfItems / 25)
|
||||
},
|
||||
canSaveTopic() {
|
||||
const content = this.editor ? this.editor.getText().trim() : '';
|
||||
return this.newTitle.trim().length >= 3 && content.length > 0;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -109,6 +125,24 @@ export default {
|
||||
if (this.editor) this.editor.destroy()
|
||||
},
|
||||
methods: {
|
||||
focusTitleInput() {
|
||||
this.$nextTick(() => this.$refs.titleInput?.focus?.());
|
||||
},
|
||||
toggleCreation() {
|
||||
this.inCreation = !this.inCreation;
|
||||
if (this.inCreation && this.editor) {
|
||||
this.editor.commands.setContent('');
|
||||
this.newTitle = '';
|
||||
this.focusTitleInput();
|
||||
}
|
||||
},
|
||||
cancelCreation() {
|
||||
this.inCreation = false;
|
||||
this.newTitle = '';
|
||||
if (this.editor) {
|
||||
this.editor.commands.setContent('');
|
||||
}
|
||||
},
|
||||
async loadForum() {
|
||||
try {
|
||||
const { data } = await apiClient.get(
|
||||
@@ -121,16 +155,9 @@ export default {
|
||||
console.error('Fehler beim Laden des Forums', err)
|
||||
}
|
||||
},
|
||||
createNewTopic() {
|
||||
this.inCreation = !this.inCreation
|
||||
if (this.inCreation && this.editor) {
|
||||
this.editor.commands.setContent('')
|
||||
this.$nextTick(() => this.editor?.commands.focus('end'))
|
||||
}
|
||||
},
|
||||
async saveNewTopic() {
|
||||
const content = this.editor ? this.editor.getHTML() : ''
|
||||
if (!this.newTitle.trim() || !content.trim()) return
|
||||
if (!this.canSaveTopic) return
|
||||
try {
|
||||
const { data } = await apiClient.post(
|
||||
'/api/forum/topic',
|
||||
@@ -147,8 +174,11 @@ export default {
|
||||
this.page = data.page
|
||||
this.inCreation = false
|
||||
this.newTitle = ''
|
||||
this.editor?.commands.setContent('')
|
||||
showSuccess(this, 'Thema erfolgreich erstellt.')
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Erstellen des Themas', err)
|
||||
showApiError(this, err, 'Fehler beim Erstellen des Themas')
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
@@ -211,6 +241,30 @@ export default {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.forum-creation__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.forum-creation__header h3 {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.forum-creation__header p,
|
||||
.forum-creation__hint {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.forum-creation__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.newtitle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -289,12 +343,20 @@ export default {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forum-empty p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.forum-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.forum-creation__header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topic-card__main {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div class="guestbook-view">
|
||||
<section class="guestbook-hero surface-card">
|
||||
<div>
|
||||
<span class="guestbook-kicker">Gaestebuch</span>
|
||||
<span class="guestbook-kicker">Gästebuch</span>
|
||||
<h2>{{ $t('socialnetwork.guestbook.title') }}</h2>
|
||||
<p>Nachrichten, Rueckmeldungen und kleine Einblicke aus deinem Netzwerk.</p>
|
||||
<p>Nachrichten, Rückmeldungen und kleine Einblicke aus deinem Netzwerk.</p>
|
||||
</div>
|
||||
</section>
|
||||
<div v-if="guestbookEntries.length === 0" class="guestbook-empty surface-card">{{ $t('socialnetwork.profile.guestbook.noEntries') }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<section class="vocab-chapter-hero surface-card">
|
||||
<span class="vocab-chapter-hero__eyebrow">Vokabeltrainer</span>
|
||||
<h2>{{ $t('socialnetwork.vocab.chapterTitle', { title: chapter?.title || '' }) }}</h2>
|
||||
<p>Kapitelinhalt durchsuchen, Vokabeln pflegen und direkt in die Uebung wechseln.</p>
|
||||
<p>Kapitelinhalt durchsuchen, Vokabeln pflegen und direkt in die Übung wechseln.</p>
|
||||
</section>
|
||||
|
||||
<section class="box surface-card">
|
||||
@@ -269,4 +269,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
<span class="vocab-courses-kicker">Kurse</span>
|
||||
<h2>{{ $t('socialnetwork.vocab.courses.title') }}</h2>
|
||||
<p>Oeffentliche und eigene Lernkurse filtern, finden und direkt weiterlernen.</p>
|
||||
<p>Öffentliche und eigene Lernkurse filtern, finden und direkt weiterlernen.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -208,12 +208,7 @@ export default {
|
||||
if (!this.selectedNativeLanguageId) {
|
||||
this.selectedNativeLanguageId = 'my';
|
||||
}
|
||||
console.log(`[loadMyNativeLanguageId] Gefunden: ${nativeLanguageName} (ID: ${nativeLang.id})`);
|
||||
} else {
|
||||
console.warn(`[loadMyNativeLanguageId] Sprache "${nativeLanguageName}" nicht in languages-Liste gefunden. Verfügbare Sprachen:`, this.languages.map(l => l.name).join(', '));
|
||||
}
|
||||
} else {
|
||||
console.warn(`[loadMyNativeLanguageId] languages-Liste ist leer.`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Konnte Muttersprache nicht laden:', e);
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<option v-for="chapter in chapters" :key="chapter.id" :value="chapter.id">{{ chapter.title }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<span v-if="lessonFormTouched && !canCreateLesson" class="form-error">Bitte Nummer, Titel und Kapitel vollstaendig angeben.</span>
|
||||
<span v-if="lessonFormTouched && !canCreateLesson" class="form-error">Bitte Nummer, Titel und Kapitel vollständig angeben.</span>
|
||||
<div class="form-actions form-actions-row">
|
||||
<button type="submit" :disabled="!canCreateLesson">{{ $t('general.create') }}</button>
|
||||
<button type="button" @click="showAddLessonDialog = false" class="button-secondary">{{ $t('general.cancel') }}</button>
|
||||
@@ -111,7 +111,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { confirmAction, showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
import { confirmAction, showApiError, showInfo, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'VocabCourseView',
|
||||
@@ -259,7 +259,7 @@ export default {
|
||||
showSuccess(this, 'Lektion erfolgreich angelegt.');
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Hinzufügen der Lektion:', e);
|
||||
showApiError(this, e, 'Fehler beim Hinzufuegen der Lektion');
|
||||
showApiError(this, e, 'Fehler beim Hinzufügen der Lektion');
|
||||
}
|
||||
},
|
||||
async deleteLesson(lessonId) {
|
||||
@@ -273,10 +273,10 @@ export default {
|
||||
try {
|
||||
await apiClient.delete(`/api/vocab/lessons/${lessonId}`);
|
||||
await this.loadCourse();
|
||||
showSuccess(this, 'Lektion erfolgreich geloescht.');
|
||||
showSuccess(this, 'Lektion erfolgreich gelöscht.');
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Löschen der Lektion:', e);
|
||||
showApiError(this, e, 'Fehler beim Loeschen der Lektion');
|
||||
showApiError(this, e, 'Fehler beim Löschen der Lektion');
|
||||
}
|
||||
},
|
||||
openLesson(lessonId) {
|
||||
@@ -285,9 +285,8 @@ export default {
|
||||
editCourse() {
|
||||
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/edit`);
|
||||
},
|
||||
editLesson(lessonId) {
|
||||
// TODO: Implement edit lesson
|
||||
console.log('Edit lesson', lessonId);
|
||||
editLesson() {
|
||||
showInfo(this, 'Die Bearbeitung einzelner Lektionen folgt noch.');
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div v-else>
|
||||
<span class="vocab-language-kicker">Sprache</span>
|
||||
<h2>{{ $t('socialnetwork.vocab.languageTitle', { name: language?.name || '' }) }}</h2>
|
||||
<p>Kapitel, Suchfunktionen und Freigaben fuer diese Sprache an einem Ort.</p>
|
||||
<p>Kapitel, Suchfunktionen und Freigaben für diese Sprache an einem Ort.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<label class="label form-field">
|
||||
<span>{{ $t('socialnetwork.vocab.languageName') }}</span>
|
||||
<input v-model="name" type="text" :class="{ 'field-error': nameTouched && !canSave }" />
|
||||
<span class="form-hint">Ein kurzer, klarer Sprachname reicht fuer den Start.</span>
|
||||
<span class="form-hint">Ein kurzer, klarer Sprachname reicht für den Start.</span>
|
||||
<span v-if="nameTouched && !canSave" class="form-error">Der Name sollte mindestens 2 Zeichen haben.</span>
|
||||
</label>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user