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:
345
docs/FALUKANT_LOVERS_CONCEPT.md
Normal file
345
docs/FALUKANT_LOVERS_CONCEPT.md
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
# Falukant: Konzept für Liebhaber, Geliebte und Mätressen
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Das Familiensystem von Falukant soll neben Ehe, Verlobung und Nachkommen auch außereheliche Bindungen abbilden. Im frühen Mittelalter sind Liebhaberinnen, Geliebte und Mätressen kein moderner Privatbereich, sondern ein sozialer, wirtschaftlicher und standesabhängiger Faktor. Das System soll deshalb:
|
||||||
|
|
||||||
|
- zur Spielwelt passen
|
||||||
|
- je nach Stand unterschiedlich bewertet werden
|
||||||
|
- Ansehen, Frömmigkeit und Familienfrieden beeinflussen
|
||||||
|
- laufende Kosten verursachen
|
||||||
|
- Stoff für Ereignisse, Skandale und Machtspiele liefern
|
||||||
|
|
||||||
|
## Grundprinzip
|
||||||
|
|
||||||
|
Eine außereheliche Beziehung ist in Falukant weder pauschal erlaubt noch pauschal verboten. Entscheidend sind:
|
||||||
|
|
||||||
|
- öffentlicher Bekanntheitsgrad
|
||||||
|
- sozialer Stand der Spielfigur
|
||||||
|
- Familienstand der Spielfigur
|
||||||
|
- gesellschaftliche Erwartung der Umgebung
|
||||||
|
- Fähigkeit, die Beziehung finanziell und politisch zu tragen
|
||||||
|
|
||||||
|
Die gleiche Beziehung kann für einen niedrigen Stand ruinös, für einen reichen Stadtadeligen unerquicklich, aber handhabbar und für einen hohen Adeligen unter Bedingungen tolerierbar sein.
|
||||||
|
|
||||||
|
## Begriffe
|
||||||
|
|
||||||
|
Für die Mechanik sollten drei Hauptformen unterschieden werden:
|
||||||
|
|
||||||
|
### Heimliche Liebschaft
|
||||||
|
|
||||||
|
- diskrete Beziehung ohne offizielle Duldung
|
||||||
|
- geringe laufende Grundkosten
|
||||||
|
- erhöhtes Skandal- und Erpressungsrisiko
|
||||||
|
- besonders gefährlich bei verheirateten Figuren
|
||||||
|
|
||||||
|
### Geliebte oder Liebhaber
|
||||||
|
|
||||||
|
- wiederkehrende, bekannte außereheliche Beziehung
|
||||||
|
- im engeren Umfeld teilweise bekannt
|
||||||
|
- mittlere Unterhaltskosten
|
||||||
|
- spürbarer Einfluss auf Ehe, Hausstand und Ansehen
|
||||||
|
|
||||||
|
### Mätresse oder Favorit
|
||||||
|
|
||||||
|
- gesellschaftlich wahrnehmbare, dauerhaft unterhaltene Beziehung
|
||||||
|
- vor allem für gehobene Stände denkbar
|
||||||
|
- hohe regelmäßige Kosten
|
||||||
|
- kann Status, Gerüchte, Neid und politische Verbindungen erzeugen
|
||||||
|
|
||||||
|
Hinweis für die Spielwelt: Für männliche und weibliche Spielfiguren soll das System symmetrisch funktionieren. Die gesellschaftliche Reaktion kann jedoch je nach Geschlecht und Stand unterschiedlich stark ausfallen.
|
||||||
|
|
||||||
|
## Standeslogik
|
||||||
|
|
||||||
|
Die Behandlung soll nicht nur von „gut oder schlecht“ abhängen, sondern vom Stand.
|
||||||
|
|
||||||
|
### Unfreie, Freie, einfache Bürger
|
||||||
|
|
||||||
|
- außereheliche Beziehungen werden schnell als Verschwendung oder Unsittlichkeit gewertet
|
||||||
|
- schon geringe Zusatzkosten können den Haushalt destabilisieren
|
||||||
|
- offenkundige Affären senken Ansehen deutlich
|
||||||
|
- Heimlichkeit ist wichtiger als Repräsentation
|
||||||
|
|
||||||
|
Typische Wirkung:
|
||||||
|
|
||||||
|
- stärkerer Ansehensverlust
|
||||||
|
- erhöhtes Risiko von Streit im Haus
|
||||||
|
- kaum gesellschaftlicher Nutzen
|
||||||
|
|
||||||
|
### Wohlhabende Bürger, Patrizier, städtische Oberschicht
|
||||||
|
|
||||||
|
- diskrete Beziehungen können geduldet werden, wenn Haushalt und Ehe nach außen stabil bleiben
|
||||||
|
- auffällige Affären schaden dem Ruf in Zünften, Rat und Nachbarschaft
|
||||||
|
- die finanzielle Belastung ist tragbar, wird aber sichtbar
|
||||||
|
|
||||||
|
Typische Wirkung:
|
||||||
|
|
||||||
|
- bei Diskretion nur mäßiger Ansehensverlust
|
||||||
|
- bei öffentlichem Bekanntwerden deutlicher Malus
|
||||||
|
- gelegentlich soziale Vorteile über Kontakte der Geliebten möglich
|
||||||
|
|
||||||
|
### Niederer Adel
|
||||||
|
|
||||||
|
- Geliebte oder Mätressen sind nicht unvorstellbar, aber müssen „standesgemäß“ geführt werden
|
||||||
|
- eine vernachlässigte Ehe oder ein niedriger sozialer Rang der Geliebten kann das Haus kompromittieren
|
||||||
|
- uneheliche Kinder oder öffentliche Kränkungen des Ehepartners schaden besonders
|
||||||
|
|
||||||
|
Typische Wirkung:
|
||||||
|
|
||||||
|
- moderate bis starke Ansehensschwankungen je nach Öffentlichkeit
|
||||||
|
- Frömmigkeit und Hausfrieden werden wichtiger
|
||||||
|
- politische Nebeneffekte möglich
|
||||||
|
|
||||||
|
### Hoher Adel
|
||||||
|
|
||||||
|
- eine diskret und kostspielig unterhaltene Mätresse kann als Ausdruck von Macht und Überfluss toleriert werden
|
||||||
|
- dieselbe Situation wird zum Skandal, wenn Haus, Kirche oder Erbfolge bedroht sind
|
||||||
|
- das Problem ist weniger die bloße Existenz als die öffentliche Unordnung
|
||||||
|
|
||||||
|
Typische Wirkung:
|
||||||
|
|
||||||
|
- geringe oder neutrale Wirkung bei geordneter Diskretion
|
||||||
|
- starker Malus bei Skandal, Erpressung, Streit mit Ehepartner oder unehelichen Erbansprüchen
|
||||||
|
- hohe Unterhaltskosten sind Pflicht, nicht Kür
|
||||||
|
|
||||||
|
## Kernwerte pro Beziehung
|
||||||
|
|
||||||
|
Jede Liebhaber-Beziehung sollte mindestens diese Werte tragen:
|
||||||
|
|
||||||
|
- `type`: heimlich, geliebt, Mätresse/Favorit
|
||||||
|
- `affection`: Zuneigung und Bindung
|
||||||
|
- `visibility`: wie bekannt die Beziehung ist
|
||||||
|
- `discretion`: wie gut sie verborgen oder kontrolliert wird
|
||||||
|
- `maintenanceLevel`: wie aufwendig die Beziehung unterhalten wird
|
||||||
|
- `monthlyCost`: laufende Kosten
|
||||||
|
- `statusFit`: passt die Beziehung zum Stand der Spielfigur
|
||||||
|
- `householdTension`: Spannungen im eigenen Haus
|
||||||
|
- `scandalRisk`: Risiko für Gerüchte, Erpressung oder Entdeckung
|
||||||
|
|
||||||
|
Optional später:
|
||||||
|
|
||||||
|
- `fertilityRisk`
|
||||||
|
- `politicalValue`
|
||||||
|
- `churchOffense`
|
||||||
|
- `favoredByCourt`
|
||||||
|
|
||||||
|
## Laufende Kosten
|
||||||
|
|
||||||
|
Eine außereheliche Beziehung muss regelmäßig Geld kosten. Sonst wird sie spielerisch zu billig.
|
||||||
|
|
||||||
|
### Basiskosten
|
||||||
|
|
||||||
|
- Geschenke
|
||||||
|
- Unterkunft oder Versorgung
|
||||||
|
- Kleidung und Schmuck
|
||||||
|
- Reisen, Botengänge, Treffen
|
||||||
|
|
||||||
|
### Zusätzliche Kosten bei gehobenen Formen
|
||||||
|
|
||||||
|
- eigenes Haus oder eigene Zimmer
|
||||||
|
- Dienerschaft
|
||||||
|
- Bewachung oder Diskretionsgeld
|
||||||
|
- Kleidung auf Standesniveau
|
||||||
|
- gesellschaftliche Geschenke
|
||||||
|
|
||||||
|
### Kostenlogik
|
||||||
|
|
||||||
|
Die Kosten sollen aus zwei Faktoren entstehen:
|
||||||
|
|
||||||
|
- Beziehungsform
|
||||||
|
- Stand der Spielfigur
|
||||||
|
|
||||||
|
Beispielhaft:
|
||||||
|
|
||||||
|
- Heimliche Liebschaft: niedrige Grundkosten, aber höheres Risiko
|
||||||
|
- Geliebte: mittlere planbare Kosten
|
||||||
|
- Mätresse/Favorit: hohe planbare Kosten plus mögliche Sonderausgaben
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- Ein hoher Adeliger darf eine Mätresse nicht billig führen.
|
||||||
|
- Wer zu wenig investiert, verliert Diskretion, Zuneigung und Ansehen.
|
||||||
|
|
||||||
|
## Wirkung auf Ansehen
|
||||||
|
|
||||||
|
Ansehen soll nicht nur einmalig sinken, sondern über Zustände beeinflusst werden.
|
||||||
|
|
||||||
|
### Positive oder neutrale Fälle
|
||||||
|
|
||||||
|
- hohe Stände
|
||||||
|
- gute Diskretion
|
||||||
|
- Ehe und Haushalt wirken stabil
|
||||||
|
- keine Erbfolgen oder offenen Kränkungen
|
||||||
|
- Geliebte steht sozial nicht völlig außerhalb des Hauses
|
||||||
|
|
||||||
|
Mögliche Wirkung:
|
||||||
|
|
||||||
|
- kein Malus
|
||||||
|
- geringer passiver Malus
|
||||||
|
- in Ausnahmefällen leichter Statusbonus als Zeichen von Überfluss und Einfluss
|
||||||
|
|
||||||
|
### Negative Fälle
|
||||||
|
|
||||||
|
- Beziehung ist öffentlich
|
||||||
|
- Spielfigur ist verheiratet
|
||||||
|
- die Ehefrau oder der Ehemann wird sichtbar gedemütigt
|
||||||
|
- die Geliebte passt nicht zum Stand
|
||||||
|
- die Kosten ruinieren den Haushalt
|
||||||
|
- die Kirche oder lokale Autoritäten greifen das Thema auf
|
||||||
|
|
||||||
|
Mögliche Wirkung:
|
||||||
|
|
||||||
|
- täglicher oder wöchentlicher Ansehensverlust
|
||||||
|
- einmalige Skandalereignisse
|
||||||
|
- höhere Kosten für Reputationspflege
|
||||||
|
- Nachteile bei Standesaufstieg
|
||||||
|
|
||||||
|
## Wirkung auf Familienleben
|
||||||
|
|
||||||
|
Das System muss spürbar mit Ehe und Haushalt verbunden sein.
|
||||||
|
|
||||||
|
### Auf die Ehe
|
||||||
|
|
||||||
|
- Ehezufriedenheit sinkt
|
||||||
|
- Streitwahrscheinlichkeit steigt
|
||||||
|
- Geschenke oder Feste für den Ehepartner können Konflikte mildern
|
||||||
|
- bei sehr hoher Spannung drohen Trennung, Rückzug oder öffentliche Kränkung
|
||||||
|
|
||||||
|
### Auf Kinder und Erbfolge
|
||||||
|
|
||||||
|
- uneheliche Kinder können später Ereignisse auslösen
|
||||||
|
- anerkannte uneheliche Kinder können Hausfrieden und Stand belasten
|
||||||
|
- je höher der Stand, desto wichtiger wird die Frage nach legitimer Erbfolge
|
||||||
|
|
||||||
|
### Auf den Familienbereich
|
||||||
|
|
||||||
|
In `FamilyView` sollte eine Liebhaber-Person nicht nur mit Name und Zuneigung erscheinen, sondern auch mit:
|
||||||
|
|
||||||
|
- Form der Beziehung
|
||||||
|
- monatlichen Kosten
|
||||||
|
- Bekanntheitsgrad
|
||||||
|
- aktuellem Einfluss auf Hausfrieden
|
||||||
|
- aktuellem Einfluss auf Ansehen
|
||||||
|
|
||||||
|
## Wirkung auf Kirche und Frömmigkeit
|
||||||
|
|
||||||
|
Für die Epoche ist die religiöse Dimension wichtig.
|
||||||
|
|
||||||
|
- Hohe Frömmigkeit plus öffentliche Affäre erzeugt stärkere Heuchelei-Strafe.
|
||||||
|
- Niedrige Frömmigkeit macht Affären sozial nicht folgenlos, kann aber kirchliche Reaktionen weniger überraschend wirken lassen.
|
||||||
|
- Kirchenspenden oder Bußhandlungen könnten später Skandale abmildern, aber nicht kostenlos neutralisieren.
|
||||||
|
|
||||||
|
## Ereignisse
|
||||||
|
|
||||||
|
Das System braucht nicht nur passive Werte, sondern Ereignisse.
|
||||||
|
|
||||||
|
### Alltägliche Ereignisse
|
||||||
|
|
||||||
|
- Wunsch nach Geschenk
|
||||||
|
- Wunsch nach besserer Unterkunft
|
||||||
|
- Streit mit Ehepartner
|
||||||
|
- Bitte um öffentliche Anerkennung
|
||||||
|
|
||||||
|
### Risikoereignisse
|
||||||
|
|
||||||
|
- Gerücht am Hof oder in der Stadt
|
||||||
|
- Erpressung durch Diener, Rivalen oder Geistliche
|
||||||
|
- Schwangerschaft oder uneheliches Kind
|
||||||
|
- Duell- oder Ehrenkonflikt
|
||||||
|
- Forderung nach Versorgung eines Kindes
|
||||||
|
|
||||||
|
### Standesereignisse
|
||||||
|
|
||||||
|
- niedrige Stände: Nachbarschaftsgerede, wirtschaftliche Belastung, häuslicher Streit
|
||||||
|
- Bürgerliche: Ratshausgerüchte, Zunftschaden, moralischer Druck
|
||||||
|
- Adel: Hofklatsch, Machtfraktionen, Belastung der Erbfolge, kirchliche Einmischung
|
||||||
|
|
||||||
|
## Spielregeln zur Balance
|
||||||
|
|
||||||
|
Damit das System interessant bleibt und nicht zur reinen Strafe oder zum Gratisbonus wird:
|
||||||
|
|
||||||
|
- maximal eine aktiv unterhaltene Mätresse/Favorit gleichzeitig
|
||||||
|
- mehrere heimliche Liebschaften sind möglich, aber das Skandalrisiko steigt stark
|
||||||
|
- hohe Kosten müssen echte Opportunitätskosten erzeugen
|
||||||
|
- Ansehen darf nicht einfach mit Geld zurückgekauft werden
|
||||||
|
- zu geringe Versorgung verschlechtert Diskretion und Beziehung
|
||||||
|
- eine Beziehung darf keinen simplen Gratisbonus auf Werte geben
|
||||||
|
|
||||||
|
## UI- und UX-Konzept
|
||||||
|
|
||||||
|
Der bestehende Bereich in [FamilyView.vue](/mnt/share/torsten/Programs/YourPart3/frontend/src/views/falukant/FamilyView.vue) kann direkt ausgebaut werden.
|
||||||
|
|
||||||
|
### Anzeige pro Person
|
||||||
|
|
||||||
|
- Name und Titel
|
||||||
|
- Rolle: heimliche Liebschaft, Geliebte, Mätresse/Favorit
|
||||||
|
- Zuneigung
|
||||||
|
- Bekanntheitsgrad
|
||||||
|
- monatliche Kosten
|
||||||
|
- Standespassung
|
||||||
|
- aktueller Effekt auf Ansehen
|
||||||
|
- aktueller Effekt auf Hausfrieden
|
||||||
|
|
||||||
|
### Aktionen
|
||||||
|
|
||||||
|
- beschenken
|
||||||
|
- besser unterbringen
|
||||||
|
- diskret halten
|
||||||
|
- öffentlich anerkennen
|
||||||
|
- Beziehung beenden
|
||||||
|
- Versorgung reduzieren
|
||||||
|
|
||||||
|
### Hinweise
|
||||||
|
|
||||||
|
- Warnung bei drohendem Skandal
|
||||||
|
- Warnung bei unpassender Standeswahl
|
||||||
|
- Warnung bei zu geringer Versorgung
|
||||||
|
- Hinweis, wenn die Beziehung die Ehe oder den Aufstieg belastet
|
||||||
|
|
||||||
|
## Umsetzungsphasen
|
||||||
|
|
||||||
|
### Phase 1: Grundsystem
|
||||||
|
|
||||||
|
- Beziehungen vom Typ `lover` im Familienbereich sauber anzeigen
|
||||||
|
- Beziehungstypen unterscheiden
|
||||||
|
- monatliche Kosten berechnen
|
||||||
|
- passiven Einfluss auf Ansehen und Hausfrieden einführen
|
||||||
|
|
||||||
|
### Phase 2: Reibung und Entscheidungen
|
||||||
|
|
||||||
|
- Sichtbarkeit und Diskretion einführen
|
||||||
|
- Ereignisse zu Streit, Geschenkforderungen und Gerüchten
|
||||||
|
- Wechselwirkungen mit Ehe und Ansehen
|
||||||
|
|
||||||
|
### Phase 3: Tiefe Systeme
|
||||||
|
|
||||||
|
- uneheliche Kinder
|
||||||
|
- Erpressung und kirchliche Reaktionen
|
||||||
|
- politische oder hofbezogene Nebeneffekte
|
||||||
|
- Standes- und Erbfolgekonflikte
|
||||||
|
|
||||||
|
## Konkrete Empfehlungsregel für Falukant
|
||||||
|
|
||||||
|
Als Startregel für die erste spielbare Version:
|
||||||
|
|
||||||
|
- jede Liebhaber-Beziehung hat laufende Monatskosten
|
||||||
|
- jede Beziehung erzeugt je nach Stand einen passiven Ansehensmodifikator
|
||||||
|
- verheiratete Figuren erhalten zusätzlich Hausfriedensverlust
|
||||||
|
- hohe Stände können eine diskrete, gut unterhaltene Mätresse mit geringem oder neutralem Ansehensmalus führen
|
||||||
|
- niedrige und mittlere Stände tragen bei öffentlicher Affäre deutlich stärkere Nachteile
|
||||||
|
- unzureichende Versorgung erhöht pro Tick Sichtbarkeit, Streit und Skandalrisiko
|
||||||
|
|
||||||
|
Damit entsteht genau das gewünschte Spannungsfeld:
|
||||||
|
|
||||||
|
- romantisch oder politisch nützlich
|
||||||
|
- aber nie kostenlos
|
||||||
|
- gesellschaftlich nie neutral
|
||||||
|
- je nach Stand anders lesbar und anders gefährlich
|
||||||
|
|
||||||
|
## Offene Designentscheidungen
|
||||||
|
|
||||||
|
Vor der technischen Umsetzung sollten noch drei Punkte festgelegt werden:
|
||||||
|
|
||||||
|
1. Soll es einen festen Wert `householdTension` geben oder soll das über bestehende Ehe-/Familienwerte laufen?
|
||||||
|
2. Soll Frömmigkeit direkt mit dem Liebhaber-System gekoppelt werden oder erst in einer späteren Kirchenphase?
|
||||||
|
3. Sollen uneheliche Kinder bereits in Phase 1 möglich sein oder erst ab Phase 3?
|
||||||
178
docs/UMLAUT_NORMALIZATION_PLAN.md
Normal file
178
docs/UMLAUT_NORMALIZATION_PLAN.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Umlaut-Normalisierung Plan
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Alle sichtbaren deutschsprachigen UI-Texte sollen konsistent echte Umlaute und korrektes `ß` verwenden.
|
||||||
|
|
||||||
|
Beispiele:
|
||||||
|
- `ae` -> `ä`
|
||||||
|
- `oe` -> `ö`
|
||||||
|
- `ue` -> `ü`
|
||||||
|
- `ss` -> `ß`, wenn orthografisch korrekt
|
||||||
|
|
||||||
|
Nicht Teil dieses Schritts:
|
||||||
|
- technische Bezeichner, Dateinamen, Keys, Routen, API-Felder
|
||||||
|
- bewusst ASCII-basierte interne Kennungen
|
||||||
|
- englische, spanische oder backendseitige maschinennahe Werte
|
||||||
|
- bestehende Konzept-/Audit-Dokumente, sofern nicht explizit gewünscht
|
||||||
|
|
||||||
|
## Leitregeln
|
||||||
|
|
||||||
|
- Nur sichtbare Texte anfassen.
|
||||||
|
- Keine Übersetzungs-Keys umbenennen, wenn nur der angezeigte Wert falsch ist.
|
||||||
|
- Keine Logikänderung mit Sprachkorrekturen vermischen.
|
||||||
|
- `ss` nur dort zu `ß` ändern, wo es sprachlich korrekt ist.
|
||||||
|
- Neue Texte immer direkt mit echter deutscher Schreibweise anlegen.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### 1. Direkte UI-Texte in Vue-Dateien
|
||||||
|
|
||||||
|
Prüfen und korrigieren in:
|
||||||
|
- `frontend/src/components/**/*.vue`
|
||||||
|
- `frontend/src/views/**/*.vue`
|
||||||
|
- `frontend/src/dialogues/**/*.vue`
|
||||||
|
|
||||||
|
Typische Problemfälle:
|
||||||
|
- Überschriften
|
||||||
|
- Buttons
|
||||||
|
- Statushinweise
|
||||||
|
- Hilfetexte
|
||||||
|
- Leerzustände
|
||||||
|
- Fehlermeldungen
|
||||||
|
|
||||||
|
### 2. i18n-Inhalte
|
||||||
|
|
||||||
|
Prüfen und korrigieren in:
|
||||||
|
- `frontend/src/i18n/locales/de/**/*.json`
|
||||||
|
|
||||||
|
Besonders relevant:
|
||||||
|
- Navigation
|
||||||
|
- Header/Footer
|
||||||
|
- Home
|
||||||
|
- Blog
|
||||||
|
- Forum
|
||||||
|
- Vokabeltrainer
|
||||||
|
- Minigames
|
||||||
|
- Einstellungen
|
||||||
|
- Admin
|
||||||
|
|
||||||
|
### 3. Gemeinsame Shell- und Systemtexte
|
||||||
|
|
||||||
|
Zuerst prüfen:
|
||||||
|
- `frontend/src/components/AppSectionBar.vue`
|
||||||
|
- `frontend/src/components/AppNavigation.vue`
|
||||||
|
- `frontend/src/components/AppHeader.vue`
|
||||||
|
- `frontend/src/components/AppFooter.vue`
|
||||||
|
- `frontend/src/components/DialogWidget.vue`
|
||||||
|
- `frontend/src/components/MessageboxWidget.vue`
|
||||||
|
|
||||||
|
### 4. Produktbereiche mit hoher Sichtbarkeit
|
||||||
|
|
||||||
|
Danach prüfen:
|
||||||
|
- `frontend/src/views/home/**/*`
|
||||||
|
- `frontend/src/views/social/**/*`
|
||||||
|
- `frontend/src/views/falukant/**/*`
|
||||||
|
- `frontend/src/views/minigames/**/*`
|
||||||
|
- `frontend/src/views/settings/**/*`
|
||||||
|
- `frontend/src/views/blog/**/*`
|
||||||
|
- `frontend/src/views/admin/**/*`
|
||||||
|
|
||||||
|
## Abarbeitung
|
||||||
|
|
||||||
|
### Phase A: Inventur
|
||||||
|
|
||||||
|
1. Fundstellen mit Suchmustern sammeln.
|
||||||
|
2. Treffer in drei Klassen sortieren:
|
||||||
|
- `sichtbarer UI-Text`
|
||||||
|
- `i18n-Wert`
|
||||||
|
- `nicht anfassen` wie Variablen, Klassen, Keys, Pfade
|
||||||
|
|
||||||
|
Empfohlene Suchmuster:
|
||||||
|
- `Persoen`
|
||||||
|
- `Gaeste`
|
||||||
|
- `Zurueck`
|
||||||
|
- `Uebersicht`
|
||||||
|
- `Loesch`
|
||||||
|
- `Fuer`
|
||||||
|
- `Oeff`
|
||||||
|
- `Schli`
|
||||||
|
- `groess`
|
||||||
|
- `aend`
|
||||||
|
- `moeg`
|
||||||
|
- `ueber`
|
||||||
|
- `uebrig`
|
||||||
|
- `fuer`
|
||||||
|
- `waehr`
|
||||||
|
- `muess`
|
||||||
|
- `koenn`
|
||||||
|
|
||||||
|
### Phase B: Shell zuerst
|
||||||
|
|
||||||
|
Zuerst alle global sichtbaren Texte korrigieren:
|
||||||
|
- Bereichsleisten
|
||||||
|
- Navigation
|
||||||
|
- Header
|
||||||
|
- Footer
|
||||||
|
- Standarddialoge
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
- zentrale UI sofort sprachlich konsistent
|
||||||
|
|
||||||
|
### Phase C: i18n-DE bereinigen
|
||||||
|
|
||||||
|
Danach alle deutschen Locale-Dateien durchgehen.
|
||||||
|
|
||||||
|
Vorgehen:
|
||||||
|
- nur Werte ändern, nicht die Key-Namen
|
||||||
|
- orthografische Einzelprüfung bei `ss` -> `ß`
|
||||||
|
- HTML-haltige Texte mit prüfen, damit keine alten ASCII-Umschreibungen stehen bleiben
|
||||||
|
|
||||||
|
### Phase D: Direkttexte in Views und Dialogen
|
||||||
|
|
||||||
|
Dann alle nicht-i18n-basierten sichtbaren Texte korrigieren.
|
||||||
|
|
||||||
|
Priorität:
|
||||||
|
1. Home, Navigation, Auth
|
||||||
|
2. Social, Blog, Settings
|
||||||
|
3. Falukant, Minigames, Admin
|
||||||
|
|
||||||
|
### Phase E: Konsistenzreview
|
||||||
|
|
||||||
|
Zum Schluss ein kompletter Review auf typische Restfehler:
|
||||||
|
- `ue` in sichtbaren Labels
|
||||||
|
- `oe` in Überschriften
|
||||||
|
- `ae` in Buttons und Hinweisen
|
||||||
|
- `ss` statt `ß` in Wörtern wie `dass`, `groß`, `außer`, `heißen`, `Fuß`, `Maß`
|
||||||
|
|
||||||
|
## Abnahmekriterien
|
||||||
|
|
||||||
|
Der Schritt gilt als abgeschlossen, wenn:
|
||||||
|
- in allen sichtbaren deutschen UI-Texten keine ASCII-Umschreibungen mehr verbleiben
|
||||||
|
- zentrale Shell-Texte vollständig normalisiert sind
|
||||||
|
- `de`-Locale-Dateien keine falschen Umschreibungen mehr enthalten
|
||||||
|
- Builds weiterhin sauber laufen
|
||||||
|
- keine technischen Keys oder internen Bezeichner versehentlich geändert wurden
|
||||||
|
|
||||||
|
## Risiken
|
||||||
|
|
||||||
|
- versehentliche Änderung von technischen Strings statt UI-Texten
|
||||||
|
- falsche `ß`-Korrekturen in Fällen, in denen `ss` korrekt ist
|
||||||
|
- Mischung aus i18n-Texten und hart codierten Texten kann zu doppelter Pflege führen
|
||||||
|
|
||||||
|
## Umsetzungsempfehlung
|
||||||
|
|
||||||
|
Die eigentliche Umsetzung sollte in zwei Arbeitsblöcken passieren:
|
||||||
|
|
||||||
|
1. `UN1`
|
||||||
|
Shell + i18n-DE + hochsichtbare Bereiche
|
||||||
|
|
||||||
|
2. `UN2`
|
||||||
|
Restliche Views/Dialoge + Abschlussreview
|
||||||
|
|
||||||
|
## Ergebnisdokumentation
|
||||||
|
|
||||||
|
Nach Abschluss sollte kurz dokumentiert werden:
|
||||||
|
- welche Dateien geändert wurden
|
||||||
|
- ob nur sichtbare Texte geändert wurden
|
||||||
|
- ob noch bewusst ASCII-basierte technische Strings bestehen
|
||||||
@@ -355,6 +355,7 @@ Arbeit:
|
|||||||
- verschachtelte Scrollcontainer in Falukant, Admin und Minigames entfernen oder entkoppeln
|
- verschachtelte Scrollcontainer in Falukant, Admin und Minigames entfernen oder entkoppeln
|
||||||
- tabellenlastige Kernansichten auf klarere Aufgabenreihenfolge pruefen
|
- tabellenlastige Kernansichten auf klarere Aufgabenreihenfolge pruefen
|
||||||
- Debug-/Altinteraktionen aus grossen Kernviews reduzieren, wenn sie Bedienbarkeit oder Folgepflege stoeren
|
- Debug-/Altinteraktionen aus grossen Kernviews reduzieren, wenn sie Bedienbarkeit oder Folgepflege stoeren
|
||||||
|
- Direktwege, Rueckspruenge und Fokusverhalten in den haeufigsten Hauptpfaden nachziehen
|
||||||
|
|
||||||
Aktueller Stand:
|
Aktueller Stand:
|
||||||
|
|
||||||
@@ -362,6 +363,7 @@ Aktueller Stand:
|
|||||||
- `U6.2` abgeschlossen
|
- `U6.2` abgeschlossen
|
||||||
- `U6.3` abgeschlossen
|
- `U6.3` abgeschlossen
|
||||||
- `U6.4` abgeschlossen
|
- `U6.4` abgeschlossen
|
||||||
|
- `U6.5` abgeschlossen
|
||||||
- aus der Review nach U5 als eigener Nachlauf identifiziert
|
- aus der Review nach U5 als eigener Nachlauf identifiziert
|
||||||
- Fokus bewusst nicht mehr auf Redesign, sondern auf Reibungsabbau in realen Nutzungswegen
|
- Fokus bewusst nicht mehr auf Redesign, sondern auf Reibungsabbau in realen Nutzungswegen
|
||||||
- priorisierte Teilpakete:
|
- priorisierte Teilpakete:
|
||||||
@@ -369,6 +371,7 @@ Aktueller Stand:
|
|||||||
- `U6.2 Scroll- und Layoutfallen entfernen`
|
- `U6.2 Scroll- und Layoutfallen entfernen`
|
||||||
- `U6.3 Tabellen- und Arbeitsflaechen vereinfachen`
|
- `U6.3 Tabellen- und Arbeitsflaechen vereinfachen`
|
||||||
- `U6.4 Interaktionsaltlasten reduzieren`
|
- `U6.4 Interaktionsaltlasten reduzieren`
|
||||||
|
- `U6.5 Direktwege und Ruecklogik polieren`
|
||||||
|
|
||||||
## Konkreter Arbeitskatalog
|
## Konkreter Arbeitskatalog
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapState } from 'vuex';
|
import { mapGetters, mapState } from 'vuex';
|
||||||
|
import { showInfo } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AppFooter',
|
name: 'AppFooter',
|
||||||
@@ -71,10 +72,10 @@ export default {
|
|||||||
},
|
},
|
||||||
// Daemon WebSocket deaktiviert - diese Funktionen sind nicht mehr verfügbar
|
// Daemon WebSocket deaktiviert - diese Funktionen sind nicht mehr verfügbar
|
||||||
async showFalukantDaemonStatus() {
|
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) {
|
handleDaemonMessage() {
|
||||||
console.log('⚠️ Daemon WebSocket deaktiviert - keine Nachrichten verarbeitet');
|
// Status-Events werden hier bewusst nicht verarbeitet.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
class="app-section-bar__back"
|
class="app-section-bar__back"
|
||||||
@click="navigateBack"
|
@click="navigateBack"
|
||||||
>
|
>
|
||||||
Zurueck
|
Zurück
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,13 +25,13 @@ const SECTION_LABELS = [
|
|||||||
{ test: (path) => path.startsWith('/settings'), label: 'Einstellungen' },
|
{ test: (path) => path.startsWith('/settings'), label: 'Einstellungen' },
|
||||||
{ test: (path) => path.startsWith('/admin'), label: 'Administration' },
|
{ test: (path) => path.startsWith('/admin'), label: 'Administration' },
|
||||||
{ test: (path) => path.startsWith('/minigames'), label: 'Minispiele' },
|
{ 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' }
|
{ test: (path) => path.startsWith('/blogs'), label: 'Blog' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const TITLE_MAP = {
|
const TITLE_MAP = {
|
||||||
Friends: 'Freunde',
|
Friends: 'Freunde',
|
||||||
Guestbook: 'Gaestebuch',
|
Guestbook: 'Gästebuch',
|
||||||
'Search users': 'Suche',
|
'Search users': 'Suche',
|
||||||
Gallery: 'Galerie',
|
Gallery: 'Galerie',
|
||||||
Forum: 'Forum',
|
Forum: 'Forum',
|
||||||
@@ -46,7 +46,7 @@ const TITLE_MAP = {
|
|||||||
VocabCourse: 'Kurs',
|
VocabCourse: 'Kurs',
|
||||||
VocabLesson: 'Lektion',
|
VocabLesson: 'Lektion',
|
||||||
FalukantCreate: 'Charakter erstellen',
|
FalukantCreate: 'Charakter erstellen',
|
||||||
FalukantOverview: 'Uebersicht',
|
FalukantOverview: 'Übersicht',
|
||||||
BranchView: 'Niederlassung',
|
BranchView: 'Niederlassung',
|
||||||
MoneyHistoryView: 'Geldverlauf',
|
MoneyHistoryView: 'Geldverlauf',
|
||||||
FalukantFamily: 'Familie',
|
FalukantFamily: 'Familie',
|
||||||
@@ -60,9 +60,9 @@ const TITLE_MAP = {
|
|||||||
HealthView: 'Gesundheit',
|
HealthView: 'Gesundheit',
|
||||||
PoliticsView: 'Politik',
|
PoliticsView: 'Politik',
|
||||||
UndergroundView: 'Untergrund',
|
UndergroundView: 'Untergrund',
|
||||||
'Personal settings': 'Persoenliche Daten',
|
'Personal settings': 'Persönliche Daten',
|
||||||
'View settings': 'Ansicht',
|
'View settings': 'Ansicht',
|
||||||
'Sexuality settings': 'Sexualitaet',
|
'Sexuality settings': 'Sexualität',
|
||||||
'Flirt settings': 'Flirt',
|
'Flirt settings': 'Flirt',
|
||||||
'Account settings': 'Account',
|
'Account settings': 'Account',
|
||||||
Interests: 'Interessen',
|
Interests: 'Interessen',
|
||||||
@@ -132,11 +132,19 @@ export default {
|
|||||||
return '/admin/users';
|
return '/admin/users';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
return '__history_back__';
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
navigateBack() {
|
navigateBack() {
|
||||||
|
if (this.backTarget === '__history_back__') {
|
||||||
|
this.$router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.backTarget) {
|
if (this.backTarget) {
|
||||||
this.$router.push(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) */
|
/** Backend-Route: GET /api/models/3d/falukant/characters/:filename (Proxy mit Draco-Optimierung) */
|
||||||
const MODELS_API_PATH = '/api/models/3d/falukant/characters';
|
const MODELS_API_PATH = '/api/models/3d/falukant/characters';
|
||||||
let threeRuntimePromise = null;
|
let threeRuntimePromise = null;
|
||||||
|
let threeLoadersPromise = null;
|
||||||
|
let threeModelRuntimePromise = null;
|
||||||
|
|
||||||
async function loadThreeRuntime() {
|
async function loadThreeRuntime() {
|
||||||
if (!threeRuntimePromise) {
|
if (!threeRuntimePromise) {
|
||||||
threeRuntimePromise = Promise.all([
|
threeRuntimePromise = import('@/utils/threeRuntime.js');
|
||||||
import('three'),
|
|
||||||
import('three/addons/loaders/GLTFLoader.js'),
|
|
||||||
import('three/addons/loaders/DRACOLoader.js')
|
|
||||||
]).then(([THREE, { GLTFLoader }, { DRACOLoader }]) => ({
|
|
||||||
THREE,
|
|
||||||
GLTFLoader,
|
|
||||||
DRACOLoader
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return threeRuntimePromise;
|
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 {
|
export default {
|
||||||
name: 'Character3D',
|
name: 'Character3D',
|
||||||
props: {
|
props: {
|
||||||
@@ -65,7 +75,9 @@ export default {
|
|||||||
clock: null,
|
clock: null,
|
||||||
baseYPosition: 0,
|
baseYPosition: 0,
|
||||||
showFallback: false,
|
showFallback: false,
|
||||||
threeRuntime: null
|
threeRuntime: null,
|
||||||
|
threeLoaders: null,
|
||||||
|
threeModelRuntime: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -149,49 +161,65 @@ export default {
|
|||||||
return this.threeRuntime;
|
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() {
|
async init3D() {
|
||||||
const container = this.$refs.container;
|
const container = this.$refs.container;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
this.showFallback = false;
|
this.showFallback = false;
|
||||||
const { THREE } = await this.ensureThreeRuntime();
|
const runtime = await this.ensureThreeRuntime();
|
||||||
this.clock = markRaw(new THREE.Clock());
|
this.clock = markRaw(new runtime.Clock());
|
||||||
|
|
||||||
// Scene erstellen - markRaw verwenden, um Vue's Reactivity zu vermeiden
|
// 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) {
|
if (!this.noBackground) {
|
||||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
this.scene.background = new runtime.Color(0xf0f0f0);
|
||||||
await this.loadBackground();
|
await this.loadBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Camera erstellen
|
// Camera erstellen
|
||||||
const aspect = container.clientWidth / container.clientHeight;
|
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.position.set(0, 1.5, 3);
|
||||||
this.camera.lookAt(0, 1, 0);
|
this.camera.lookAt(0, 1, 0);
|
||||||
|
|
||||||
// Renderer erstellen
|
// 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.setSize(container.clientWidth, container.clientHeight);
|
||||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
container.appendChild(this.renderer.domElement);
|
container.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
// Verbesserte Beleuchtung für hellere Modelle
|
// Verbesserte Beleuchtung für hellere Modelle
|
||||||
// Mehr ambient light für gleichmäßigere Ausleuchtung
|
// 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);
|
this.scene.add(ambientLight);
|
||||||
|
|
||||||
// Hauptlicht von vorne oben - stärker
|
// 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);
|
directionalLight.position.set(5, 10, 5);
|
||||||
this.scene.add(directionalLight);
|
this.scene.add(directionalLight);
|
||||||
|
|
||||||
// Zusätzliches Licht von hinten - heller
|
// 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);
|
backLight.position.set(-5, 5, -5);
|
||||||
this.scene.add(backLight);
|
this.scene.add(backLight);
|
||||||
|
|
||||||
// Zusätzliches Seitenlicht für mehr Tiefe
|
// 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);
|
sideLight.position.set(-5, 5, 5);
|
||||||
this.scene.add(sideLight);
|
this.scene.add(sideLight);
|
||||||
|
|
||||||
@@ -200,13 +228,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async loadBackground() {
|
async loadBackground() {
|
||||||
const { THREE } = await this.ensureThreeRuntime();
|
const runtime = await this.ensureThreeRuntime();
|
||||||
// Optimierte Versionen (512×341, ~130 KB); Originale ~3 MB
|
// Optimierte Versionen (512×341, ~130 KB); Originale ~3 MB
|
||||||
const backgrounds = ['bg1_opt.png', 'bg2_opt.png'];
|
const backgrounds = ['bg1_opt.png', 'bg2_opt.png'];
|
||||||
const randomBg = backgrounds[Math.floor(Math.random() * backgrounds.length)];
|
const randomBg = backgrounds[Math.floor(Math.random() * backgrounds.length)];
|
||||||
const bgPath = `/images/falukant/backgrounds/${randomBg}`;
|
const bgPath = `/images/falukant/backgrounds/${randomBg}`;
|
||||||
|
|
||||||
const loader = new THREE.TextureLoader();
|
const loader = new runtime.TextureLoader();
|
||||||
loader.load(
|
loader.load(
|
||||||
bgPath,
|
bgPath,
|
||||||
(texture) => {
|
(texture) => {
|
||||||
@@ -220,7 +248,7 @@ export default {
|
|||||||
console.warn('Fehler beim Laden des Hintergrunds:', error);
|
console.warn('Fehler beim Laden des Hintergrunds:', error);
|
||||||
// Fallback auf Standardfarbe bei Fehler
|
// Fallback auf Standardfarbe bei Fehler
|
||||||
if (this.scene) {
|
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() {
|
async loadModel() {
|
||||||
if (!this.scene) return;
|
if (!this.scene) return;
|
||||||
const { THREE, GLTFLoader, DRACOLoader } = await this.ensureThreeRuntime();
|
const modelRuntime = await this.ensureThreeModelRuntime();
|
||||||
|
const loaders = await this.ensureThreeLoaders();
|
||||||
|
|
||||||
// Altes Modell entfernen
|
// Altes Modell entfernen
|
||||||
if (this.model) {
|
if (this.model) {
|
||||||
@@ -252,9 +281,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dracoLoader = new DRACOLoader();
|
const dracoLoader = new loaders.DRACOLoader();
|
||||||
dracoLoader.setDecoderPath('/draco/gltf/');
|
dracoLoader.setDecoderPath('/draco/gltf/');
|
||||||
const loader = new GLTFLoader();
|
const loader = new loaders.GLTFLoader();
|
||||||
loader.setDRACOLoader(dracoLoader);
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
|
||||||
const base = getApiBaseURL();
|
const base = getApiBaseURL();
|
||||||
@@ -273,12 +302,12 @@ export default {
|
|||||||
// Versuche zuerst genaues Alter
|
// Versuche zuerst genaues Alter
|
||||||
try {
|
try {
|
||||||
gltf = await loader.loadAsync(exactAgePath);
|
gltf = await loader.loadAsync(exactAgePath);
|
||||||
console.log(`Loaded exact age model: ${exactAgePath}`);
|
console.debug(`Loaded exact age model: ${exactAgePath}`);
|
||||||
} catch (exactAgeError) {
|
} catch (exactAgeError) {
|
||||||
// Falls genaues Alter nicht existiert, versuche Altersbereich
|
// Falls genaues Alter nicht existiert, versuche Altersbereich
|
||||||
try {
|
try {
|
||||||
gltf = await loader.loadAsync(ageGroupPath);
|
gltf = await loader.loadAsync(ageGroupPath);
|
||||||
console.log(`Loaded age group model: ${ageGroupPath}`);
|
console.debug(`Loaded age group model: ${ageGroupPath}`);
|
||||||
} catch (ageGroupError) {
|
} catch (ageGroupError) {
|
||||||
// Falls Altersbereich nicht existiert, verwende Basis-Modell
|
// Falls Altersbereich nicht existiert, verwende Basis-Modell
|
||||||
console.warn(`Could not load ${ageGroupPath}, trying fallback model`);
|
console.warn(`Could not load ${ageGroupPath}, trying fallback model`);
|
||||||
@@ -293,8 +322,8 @@ export default {
|
|||||||
this.model = markRaw(gltf.scene);
|
this.model = markRaw(gltf.scene);
|
||||||
|
|
||||||
// Initiale Bounding Box für Größenberechnung (vor Skalierung)
|
// Initiale Bounding Box für Größenberechnung (vor Skalierung)
|
||||||
const initialBox = new THREE.Box3().setFromObject(this.model);
|
const initialBox = new modelRuntime.Box3().setFromObject(this.model);
|
||||||
const initialSize = initialBox.getSize(new THREE.Vector3());
|
const initialSize = initialBox.getSize(new modelRuntime.Vector3());
|
||||||
|
|
||||||
// Skalierung basierend auf Alter
|
// Skalierung basierend auf Alter
|
||||||
const age = this.actualAge;
|
const age = this.actualAge;
|
||||||
@@ -318,8 +347,8 @@ export default {
|
|||||||
this.model.scale.set(modelScale, modelScale, modelScale);
|
this.model.scale.set(modelScale, modelScale, modelScale);
|
||||||
|
|
||||||
// Bounding Box NACH dem Skalieren neu berechnen
|
// Bounding Box NACH dem Skalieren neu berechnen
|
||||||
const scaledBox = new THREE.Box3().setFromObject(this.model);
|
const scaledBox = new modelRuntime.Box3().setFromObject(this.model);
|
||||||
const scaledCenter = scaledBox.getCenter(new THREE.Vector3());
|
const scaledCenter = scaledBox.getCenter(new modelRuntime.Vector3());
|
||||||
|
|
||||||
// Modell zentrieren basierend auf der skalierten Bounding Box
|
// Modell zentrieren basierend auf der skalierten Bounding Box
|
||||||
// Position direkt setzen statt zu subtrahieren, um Proxy-Probleme zu vermeiden
|
// Position direkt setzen statt zu subtrahieren, um Proxy-Probleme zu vermeiden
|
||||||
@@ -331,7 +360,7 @@ export default {
|
|||||||
|
|
||||||
// Animationen laden falls vorhanden
|
// Animationen laden falls vorhanden
|
||||||
if (gltf.animations && gltf.animations.length > 0) {
|
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) => {
|
gltf.animations.forEach((clip) => {
|
||||||
this.mixer.clipAction(clip).play();
|
this.mixer.clipAction(clip).play();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export default {
|
|||||||
this.fetchSettings();
|
this.fetchSettings();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error updating setting:', err);
|
console.error('Error updating setting:', err);
|
||||||
showApiError(this, err, 'Aenderung konnte nicht gespeichert werden.');
|
showApiError(this, err, 'Änderung konnte nicht gespeichert werden.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
languagesList() {
|
languagesList() {
|
||||||
|
|||||||
@@ -279,7 +279,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openNewDirectorDialog() {
|
openNewDirectorDialog() {
|
||||||
console.log('openNewDirectorDialog');
|
|
||||||
this.$refs.newDirectorDialog.open(this.branchId);
|
this.$refs.newDirectorDialog.open(this.branchId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateValue(value) {
|
updateValue(value) {
|
||||||
console.log('changed to ', value)
|
|
||||||
this.$emit("input", parseInt(value));
|
this.$emit("input", parseInt(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<label for="password-reset-email">{{ $t("passwordReset.email") }}</label>
|
<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 }" />
|
<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 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>
|
||||||
</div>
|
</div>
|
||||||
</DialogWidget>
|
</DialogWidget>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="register-email">{{ $t("register.email") }}</label>
|
<label for="register-email">{{ $t("register.email") }}</label>
|
||||||
<input id="register-email" type="email" v-model="email" :class="{ 'field-error': emailTouched && !isEmailValid }" />
|
<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>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="register-username">{{ $t("register.username") }}</label>
|
<label for="register-username">{{ $t("register.username") }}</label>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="register-repeat-password">{{ $t("register.repeatPassword") }}</label>
|
<label for="register-repeat-password">{{ $t("register.repeatPassword") }}</label>
|
||||||
<input id="register-repeat-password" type="password" v-model="repeatPassword" :class="{ 'field-error': repeatPasswordTouched && !doPasswordsMatch }" />
|
<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>
|
</div>
|
||||||
<SelectDropdownWidget labelTr="settings.personal.label.language" :v-model="language"
|
<SelectDropdownWidget labelTr="settings.personal.label.language" :v-model="language"
|
||||||
tooltipTr="settings.personal.tooltip.language" :list="languages" :value="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
|
// Mark as closed first so any async close events won't schedule reconnect
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
this.clearPendingRoomCreateTracking();
|
this.clearPendingRoomCreateTracking();
|
||||||
console.log('[Chat WS] dialog close — closing websocket');
|
|
||||||
this.disconnectChatSocket();
|
this.disconnectChatSocket();
|
||||||
// Remove network event listeners
|
// Remove network event listeners
|
||||||
window.removeEventListener('online', this.onOnline);
|
window.removeEventListener('online', this.onOnline);
|
||||||
@@ -719,16 +718,13 @@ export default {
|
|||||||
this.showOptions = false;
|
this.showOptions = false;
|
||||||
},
|
},
|
||||||
onOnline() {
|
onOnline() {
|
||||||
console.log('[Chat WS] Network online detected');
|
|
||||||
if (this.opened && !this.chatConnected && !this.connectRacing && (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN)) {
|
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.reconnectAttempts = 0; // Reset attempts on network recovery
|
||||||
this.reconnectIntervalMs = 3000; // Reset to base interval
|
this.reconnectIntervalMs = 3000; // Reset to base interval
|
||||||
this.connectChatSocket();
|
this.connectChatSocket();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOffline() {
|
onOffline() {
|
||||||
console.log('[Chat WS] Network offline detected');
|
|
||||||
this.setStatus('disconnected');
|
this.setStatus('disconnected');
|
||||||
},
|
},
|
||||||
async loadRooms() {
|
async loadRooms() {
|
||||||
|
|||||||
@@ -72,10 +72,10 @@
|
|||||||
({{ formatCost(computeBranchCost(type)) }})
|
({{ formatCost(computeBranchCost(type)) }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</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>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
|
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
if (!this.selectedRegion || !this.selectedType) {
|
if (!this.selectedRegion || !this.selectedType) {
|
||||||
showError(this, 'Bitte zuerst Region und Typ auswaehlen.');
|
showError(this, 'Bitte zuerst Region und Typ auswählen.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"activate": {
|
"activate": {
|
||||||
"title": "Zugang aktivieren",
|
"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:",
|
"token": "Token:",
|
||||||
"submit": "Absenden",
|
"submit": "Absenden",
|
||||||
"failure": "Die Aktivierung war nicht erfolgreich."
|
"failure": "Die Aktivierung war nicht erfolgreich."
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
"info-title": "Information",
|
"info-title": "Information",
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"contact": {
|
"contact": {
|
||||||
"email": "Email-Adresse",
|
"email": "E-Mail-Adresse",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"message": "Deine Nachricht an uns",
|
"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.",
|
"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 Email-Adresse zu.",
|
"acceptdatasave": "Ich stimme der vorübergehenden Speicherung meiner E-Mail-Adresse zu.",
|
||||||
"accept2": "Ohne diese Zustimmung können wir Dir leider nicht antworten."
|
"accept2": "Ohne diese Zustimmung können wir Dir leider nicht antworten."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,9 +34,9 @@
|
|||||||
"login": {
|
"login": {
|
||||||
"name": "Login-Name",
|
"name": "Login-Name",
|
||||||
"namedescription": "Gib hier Deinen Benutzernamen ein",
|
"namedescription": "Gib hier Deinen Benutzernamen ein",
|
||||||
"password": "Paßwort",
|
"password": "Passwort",
|
||||||
"passworddescription": "Gib hier Dein Paßwort ein",
|
"passworddescription": "Gib hier Dein Passwort ein",
|
||||||
"lostpassword": "Paßwort vergessen",
|
"lostpassword": "Passwort vergessen",
|
||||||
"register": "Bei yourPart registrieren",
|
"register": "Bei yourPart registrieren",
|
||||||
"stayLoggedIn": "Eingeloggt bleiben",
|
"stayLoggedIn": "Eingeloggt bleiben",
|
||||||
"submit": "Einloggen"
|
"submit": "Einloggen"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"register": {
|
"register": {
|
||||||
"title": "Bei yourPart registrieren",
|
"title": "Bei yourPart registrieren",
|
||||||
"email": "Email-Adresse",
|
"email": "E-Mail-Adresse",
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
"password": "Paßwort",
|
"password": "Passwort",
|
||||||
"repeatPassword": "Paßwort wiederholen",
|
"repeatPassword": "Passwort wiederholen",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "Englisch",
|
"en": "Englisch",
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
"register": "Registrieren",
|
"register": "Registrieren",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"failure": "Es ist ein Fehler aufgetreten.",
|
"failure": "Es ist ein Fehler aufgetreten.",
|
||||||
"success": "Du wurdest erfolgreich registriert. Bitte schaue jetzt in Dein Email-Postfach zum aktivieren Deines Zugangs.",
|
"success": "Du wurdest erfolgreich registriert. Bitte schaue jetzt in Dein E-Mail-Postfach zum Aktivieren Deines Zugangs.",
|
||||||
"passwordMismatch": "Die Paßwörter stimmen nicht überein.",
|
"passwordMismatch": "Die Passwörter stimmen nicht überein.",
|
||||||
"emailinuse": "Die Email-Adresse wird bereits verwendet.",
|
"emailinuse": "Die E-Mail-Adresse wird bereits verwendet.",
|
||||||
"usernameinuse": "Der Benutzername ist nicht verfügbar."
|
"usernameinuse": "Der Benutzername ist nicht verfügbar."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,14 +141,14 @@
|
|||||||
"account": {
|
"account": {
|
||||||
"title": "Account",
|
"title": "Account",
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
"email": "Email-Adresse",
|
"email": "E-Mail-Adresse",
|
||||||
"newpassword": "Passwort",
|
"newpassword": "Passwort",
|
||||||
"newpasswordretype": "Passwort wiederholen",
|
"newpasswordretype": "Passwort wiederholen",
|
||||||
"deleteAccount": "Account löschen",
|
"deleteAccount": "Account löschen",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"showinsearch": "In Usersuchen anzeigen",
|
"showinsearch": "In Usersuchen anzeigen",
|
||||||
"changeaction": "Benutzerdaten ändern",
|
"changeaction": "Benutzerdaten ändern",
|
||||||
"oldpassword": "Altes Paßwort (benötigt)"
|
"oldpassword": "Altes Passwort (benötigt)"
|
||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Interessen",
|
"title": "Interessen",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default [
|
|||||||
meta: {
|
meta: {
|
||||||
seo: {
|
seo: {
|
||||||
title: 'Blogs auf YourPart',
|
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: {
|
meta: {
|
||||||
seo: {
|
seo: {
|
||||||
title: 'Blogs auf YourPart',
|
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,
|
component: BlogListView,
|
||||||
meta: {
|
meta: {
|
||||||
seo: {
|
seo: {
|
||||||
title: 'Blogs auf YourPart - Community-Beitraege und Themen',
|
title: 'Blogs auf YourPart - Community-Beiträge und Themen',
|
||||||
description: 'Entdecke oeffentliche Blogs auf YourPart mit Community-Beitraegen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.',
|
description: 'Entdecke öffentliche Blogs auf YourPart mit Community-Beiträgen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.',
|
||||||
keywords: 'Blogs, Community Blog, Artikel, Beitraege, YourPart',
|
keywords: 'Blogs, Community Blog, Artikel, Beiträge, YourPart',
|
||||||
canonicalPath: '/blogs',
|
canonicalPath: '/blogs',
|
||||||
jsonLd: [
|
jsonLd: [
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,7 @@ export default [
|
|||||||
'@type': 'CollectionPage',
|
'@type': 'CollectionPage',
|
||||||
name: 'Blogs auf YourPart',
|
name: 'Blogs auf YourPart',
|
||||||
url: buildAbsoluteUrl('/blogs'),
|
url: buildAbsoluteUrl('/blogs'),
|
||||||
description: 'Oeffentliche Blogs und Community-Beitraege auf YourPart.',
|
description: 'Öffentliche Blogs und Community-Beiträge auf YourPart.',
|
||||||
inLanguage: 'de',
|
inLanguage: 'de',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -224,11 +224,9 @@ const store = createStore({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const maxRetries = 10;
|
const maxRetries = 10;
|
||||||
console.log(`Backend-Reconnect-Versuch ${state.backendRetryCount + 1}/${maxRetries}`);
|
|
||||||
|
|
||||||
if (state.backendRetryCount >= maxRetries) {
|
if (state.backendRetryCount >= maxRetries) {
|
||||||
// Nach maxRetries alle 5 Sekunden weiter versuchen
|
// Nach maxRetries alle 5 Sekunden weiter versuchen
|
||||||
console.log('Backend: Max Retries erreicht, versuche weiter alle 5 Sekunden...');
|
|
||||||
state.backendRetryTimer = setTimeout(() => {
|
state.backendRetryTimer = setTimeout(() => {
|
||||||
state.backendRetryCount = 0; // Reset für nächsten Zyklus
|
state.backendRetryCount = 0; // Reset für nächsten Zyklus
|
||||||
state.backendRetryTimer = null;
|
state.backendRetryTimer = null;
|
||||||
@@ -241,7 +239,6 @@ const store = createStore({
|
|||||||
|
|
||||||
state.backendRetryCount++;
|
state.backendRetryCount++;
|
||||||
const delay = 5000; // Alle 5 Sekunden versuchen
|
const delay = 5000; // Alle 5 Sekunden versuchen
|
||||||
console.log(`Backend: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`);
|
|
||||||
|
|
||||||
state.backendRetryTimer = setTimeout(() => {
|
state.backendRetryTimer = setTimeout(() => {
|
||||||
state.backendRetryTimer = null;
|
state.backendRetryTimer = null;
|
||||||
@@ -259,8 +256,6 @@ const store = createStore({
|
|||||||
// Vite bindet Umgebungsvariablen zur Build-Zeit ein, daher Fallback-Logik basierend auf Hostname
|
// Vite bindet Umgebungsvariablen zur Build-Zeit ein, daher Fallback-Logik basierend auf Hostname
|
||||||
let daemonUrl = getDaemonSocketUrl();
|
let daemonUrl = getDaemonSocketUrl();
|
||||||
|
|
||||||
console.log('[Daemon] Finale Daemon-URL:', daemonUrl);
|
|
||||||
|
|
||||||
const connectDaemonSocket = () => {
|
const connectDaemonSocket = () => {
|
||||||
// Cleanup existing socket and timer
|
// Cleanup existing socket and timer
|
||||||
if (state.daemonSocket) {
|
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">
|
<section class="workflow-grid">
|
||||||
<article class="workflow-card surface-card">
|
<article class="workflow-card surface-card">
|
||||||
<span class="workflow-card__step">1</span>
|
<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>
|
<p>Bestehendes Level öffnen oder sofort mit einer neuen Vorlage starten.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="workflow-card surface-card">
|
<article class="workflow-card surface-card">
|
||||||
|
|||||||
@@ -276,11 +276,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadStockTypes() {
|
async loadStockTypes() {
|
||||||
console.log('Loading stock types...');
|
|
||||||
this.loading.stockTypes = true;
|
this.loading.stockTypes = true;
|
||||||
try {
|
try {
|
||||||
const stockTypesResult = await apiClient.get('/api/admin/falukant/stock-types');
|
const stockTypesResult = await apiClient.get('/api/admin/falukant/stock-types');
|
||||||
console.log('Stock types loaded:', stockTypesResult.data);
|
|
||||||
this.stockTypes = stockTypesResult.data;
|
this.stockTypes = stockTypesResult.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading stock types:', error);
|
console.error('Error loading stock types:', error);
|
||||||
@@ -327,19 +325,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
canAddStock(branch) {
|
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
|
// Wenn keine Stock-Types geladen sind, zeige den Button nicht
|
||||||
if (!this.stockTypes || this.stockTypes.length === 0) {
|
if (!this.stockTypes || this.stockTypes.length === 0) {
|
||||||
console.log('No stock types loaded, returning false');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn keine Stocks vorhanden sind, kann immer hinzugefügt werden
|
// Wenn keine Stocks vorhanden sind, kann immer hinzugefügt werden
|
||||||
if (!branch.stocks || branch.stocks.length === 0) {
|
if (!branch.stocks || branch.stocks.length === 0) {
|
||||||
console.log('No stocks in branch, returning true');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,8 +342,6 @@ export default {
|
|||||||
const availableStockTypes = this.stockTypes.filter(stockType =>
|
const availableStockTypes = this.stockTypes.filter(stockType =>
|
||||||
!existingStockTypeIds.includes(stockType.id)
|
!existingStockTypeIds.includes(stockType.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Available stock types:', availableStockTypes);
|
|
||||||
return availableStockTypes.length > 0;
|
return availableStockTypes.length > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="blog-list__kicker">Community-Blogs</span>
|
<span class="blog-list__kicker">Community-Blogs</span>
|
||||||
<h1>Blogs</h1>
|
<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>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<router-link v-if="$store.getters.isLoggedIn" class="btn" to="/blogs/create">Neuen Blog erstellen</router-link>
|
<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}`;
|
return slug ? `/blogs/${encodeURIComponent(slug)}` : `/blogs/${blog.id}`;
|
||||||
},
|
},
|
||||||
blogExcerpt(blog) {
|
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;
|
return source.length > 150 ? `${source.slice(0, 147)}...` : source;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
<section class="posts surface-card">
|
<section class="posts surface-card">
|
||||||
<div class="posts__header">
|
<div class="posts__header">
|
||||||
<h2>{{ $t('blog.posts') }}</h2>
|
<h2>{{ $t('blog.posts') }}</h2>
|
||||||
<span class="posts__count">{{ total }} Eintraege</span>
|
<span class="posts__count">{{ total }} Einträge</span>
|
||||||
</div>
|
</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">
|
<article v-for="p in items" :key="p.id" class="post">
|
||||||
<h3>{{ p.title }}</h3>
|
<h3>{{ p.title }}</h3>
|
||||||
<div class="content" v-html="sanitize(p.content)" />
|
<div class="content" v-html="sanitize(p.content)" />
|
||||||
@@ -89,7 +89,7 @@ export default {
|
|||||||
.map((item) => `${item.title || ''} ${stripHtml(item.content || '')}`.trim())
|
.map((item) => `${item.title || ''} ${stripHtml(item.content || '')}`.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.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 description = truncateText(summarySource, 160);
|
||||||
const canonicalPath = this.canonicalBlogPath();
|
const canonicalPath = this.canonicalBlogPath();
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ export default {
|
|||||||
await this.fetchPage(1);
|
await this.fetchPage(1);
|
||||||
this.applyBlogSeo();
|
this.applyBlogSeo();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.error('Blog konnte nicht geladen werden:', e);
|
||||||
// this.$router.replace('/blogs');
|
// this.$router.replace('/blogs');
|
||||||
applySeo({
|
applySeo({
|
||||||
title: 'Blog nicht gefunden | YourPart',
|
title: 'Blog nicht gefunden | YourPart',
|
||||||
|
|||||||
@@ -880,7 +880,7 @@ export default {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('Unhandled event:', eventData);
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -197,13 +197,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import StatusBar from '@/components/falukant/StatusBar.vue'
|
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 ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
|
||||||
import Character3D from '@/components/Character3D.vue'
|
import Character3D from '@/components/Character3D.vue'
|
||||||
|
|
||||||
import apiClient from '@/utils/axios.js'
|
import apiClient from '@/utils/axios.js'
|
||||||
import { confirmAction } from '@/utils/feedback.js'
|
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
const WOOING_PROGRESS_TARGET = 70
|
const WOOING_PROGRESS_TARGET = 70
|
||||||
@@ -212,8 +210,6 @@ export default {
|
|||||||
name: 'FamilyView',
|
name: 'FamilyView',
|
||||||
components: {
|
components: {
|
||||||
StatusBar,
|
StatusBar,
|
||||||
MessageDialog,
|
|
||||||
ErrorDialog,
|
|
||||||
ChildDetailsDialog,
|
ChildDetailsDialog,
|
||||||
Character3D
|
Character3D
|
||||||
},
|
},
|
||||||
@@ -294,7 +290,7 @@ export default {
|
|||||||
|
|
||||||
async setAsHeir(child) {
|
async setAsHeir(child) {
|
||||||
if (!child.childCharacterId) {
|
if (!child.childCharacterId) {
|
||||||
console.error('Child character ID missing');
|
showError(this, 'tr:falukant.family.children.heirSetError');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -302,10 +298,10 @@ export default {
|
|||||||
childCharacterId: child.childCharacterId
|
childCharacterId: child.childCharacterId
|
||||||
});
|
});
|
||||||
await this.loadFamilyData();
|
await this.loadFamilyData();
|
||||||
this.$root.$refs.messageDialog?.open('tr:falukant.family.children.heirSetSuccess');
|
showSuccess(this, 'tr:falukant.family.children.heirSetSuccess');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting heir:', 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',
|
route: 'BranchView',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kicker: 'Ueberblick',
|
kicker: 'Überblick',
|
||||||
title: 'Finanzen pruefen',
|
title: 'Finanzen pruefen',
|
||||||
description: 'Kontostand, Verlauf und wirtschaftliche Entwicklung ohne lange Suche.',
|
description: 'Kontostand, Verlauf und wirtschaftliche Entwicklung ohne lange Suche.',
|
||||||
cta: 'Geldhistorie',
|
cta: 'Geldhistorie',
|
||||||
|
|||||||
@@ -279,7 +279,6 @@ export default {
|
|||||||
setDropTarget(index) {
|
setDropTarget(index) {
|
||||||
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
||||||
this.dragOverIndex = index;
|
this.dragOverIndex = index;
|
||||||
console.log('[Dashboard Drag] setDropTarget:', index, '→ dragOverIndex =', index);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearDropTarget() {
|
clearDropTarget() {
|
||||||
@@ -315,16 +314,13 @@ export default {
|
|||||||
}
|
}
|
||||||
if (to == null) to = this.dragOverIndex != null ? this.dragOverIndex : this.widgets.length;
|
if (to == null) to = this.dragOverIndex != null ? this.dragOverIndex : this.widgets.length;
|
||||||
const from = this.draggedIndex;
|
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) {
|
if (from === to || to < 0 || to > this.widgets.length) {
|
||||||
console.log('[Dashboard Drag] onAnyDrop — abgebrochen');
|
|
||||||
this.draggedIndex = null;
|
this.draggedIndex = null;
|
||||||
this.dragOverIndex = null;
|
this.dragOverIndex = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = this.widgets.splice(from, 1)[0];
|
const item = this.widgets.splice(from, 1)[0];
|
||||||
this.widgets.splice(to, 0, item);
|
this.widgets.splice(to, 0, item);
|
||||||
console.log('[Dashboard Drag] onAnyDrop — erledigt. Neue Reihenfolge:', this.widgets.map(w => w.title));
|
|
||||||
this.draggedIndex = null;
|
this.draggedIndex = null;
|
||||||
this.dragOverIndex = null;
|
this.dragOverIndex = null;
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
|
|||||||
@@ -55,8 +55,13 @@
|
|||||||
<div class="login-panel">
|
<div class="login-panel">
|
||||||
<span class="panel-kicker">Direkt starten</span>
|
<span class="panel-kicker">Direkt starten</span>
|
||||||
<h2>{{ $t('home.nologin.login.submit') }}</h2>
|
<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">
|
<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">
|
:title="$t('home.nologin.login.namedescription')" @keydown.enter="focusPassword">
|
||||||
<input v-model="password" size="20" type="password"
|
<input v-model="password" size="20" type="password"
|
||||||
:placeholder="$t('home.nologin.login.password')"
|
:placeholder="$t('home.nologin.login.password')"
|
||||||
@@ -69,7 +74,6 @@
|
|||||||
<span>{{ $t('home.nologin.login.stayLoggedIn') }}</span>
|
<span>{{ $t('home.nologin.login.stayLoggedIn') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="access-split">
|
<div class="access-split">
|
||||||
@@ -150,6 +154,11 @@ export default {
|
|||||||
this.$root.$refs.errorDialog.open(`tr:error.${errorKey}`);
|
this.$root.$refs.errorDialog.open(`tr:error.${errorKey}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.usernameInput?.focus?.();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -292,6 +301,18 @@ export default {
|
|||||||
margin-bottom: 1rem;
|
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 {
|
.login-fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
@@ -302,10 +323,6 @@ export default {
|
|||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-action {
|
|
||||||
margin-top: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.access-split {
|
.access-split {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -129,7 +129,9 @@
|
|||||||
@mouseenter="onTileMouseEnter($event, index)"
|
@mouseenter="onTileMouseEnter($event, index)"
|
||||||
@mouseleave="onTileMouseLeave($event, index)"
|
@mouseleave="onTileMouseLeave($event, index)"
|
||||||
@touchstart="onTileMouseDown($event, index)"
|
@touchstart="onTileMouseDown($event, index)"
|
||||||
|
@touchmove.prevent="onTileMouseMove($event)"
|
||||||
@touchend="onTileMouseUp($event, index)"
|
@touchend="onTileMouseUp($event, index)"
|
||||||
|
@touchcancel="endDrag($event)"
|
||||||
@dblclick="handleDoubleClick(index, $event)">
|
@dblclick="handleDoubleClick(index, $event)">
|
||||||
<span v-if="tile" class="tile-symbol">{{ getTileSymbol(tile.type) }}</span>
|
<span v-if="tile" class="tile-symbol">{{ getTileSymbol(tile.type) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,7 +150,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showRocketFlight" class="power-up-animation" :style="{ left: rocketStartPos.x + 'px', top: rocketStartPos.y + 'px' }">
|
<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>
|
||||||
|
|
||||||
<div v-if="showRainbowEffect" class="power-up-animation" :style="{ left: rainbowCenter.x + 'px', top: rainbowCenter.y + 'px' }">
|
<div v-if="showRainbowEffect" class="power-up-animation" :style="{ left: rainbowCenter.x + 'px', top: rainbowCenter.y + 'px' }">
|
||||||
@@ -164,8 +168,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Raketen-Flug-Animation -->
|
<!-- 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 v-if="rocketTarget.x > 0 && rocketTarget.y > 0" class="rocket-target-marker" :style="{ left: rocketTarget.x + 'px', top: rocketTarget.y + 'px' }">
|
||||||
<div class="rocket-flight">🚀</div>
|
<div class="rocket-target-marker__icon">🎯</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fliegende Rakete -->
|
<!-- Fliegende Rakete -->
|
||||||
@@ -383,14 +387,110 @@ export default {
|
|||||||
// Füge globale Event-Listener hinzu
|
// Füge globale Event-Listener hinzu
|
||||||
document.addEventListener('mousemove', this.onGlobalMouseMove);
|
document.addEventListener('mousemove', this.onGlobalMouseMove);
|
||||||
document.addEventListener('mouseup', this.onGlobalMouseUp);
|
document.addEventListener('mouseup', this.onGlobalMouseUp);
|
||||||
|
document.addEventListener('touchmove', this.onGlobalMouseMove, { passive: false });
|
||||||
|
document.addEventListener('touchend', this.onGlobalMouseUp);
|
||||||
|
document.addEventListener('touchcancel', this.onGlobalMouseUp);
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
// Entferne globale Event-Listener
|
// Entferne globale Event-Listener
|
||||||
document.removeEventListener('mousemove', this.onGlobalMouseMove);
|
document.removeEventListener('mousemove', this.onGlobalMouseMove);
|
||||||
document.removeEventListener('mouseup', this.onGlobalMouseUp);
|
document.removeEventListener('mouseup', this.onGlobalMouseUp);
|
||||||
|
document.removeEventListener('touchmove', this.onGlobalMouseMove);
|
||||||
|
document.removeEventListener('touchend', this.onGlobalMouseUp);
|
||||||
|
document.removeEventListener('touchcancel', this.onGlobalMouseUp);
|
||||||
},
|
},
|
||||||
methods: {
|
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
|
// Initialisiere Sound-Effekte
|
||||||
initializeSounds() {
|
initializeSounds() {
|
||||||
try {
|
try {
|
||||||
@@ -1467,18 +1567,20 @@ export default {
|
|||||||
|
|
||||||
// Hilfsmethode: Prüfe ob ein Tile ein Raketen-Power-up ist
|
// Hilfsmethode: Prüfe ob ein Tile ein Raketen-Power-up ist
|
||||||
isRocketTile(tileType) {
|
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
|
// Hilfsmethode: Prüfe ob ein Tile ein Regenbogen-Power-up ist
|
||||||
isRainbowTile(tileType) {
|
isRainbowTile(tileType) {
|
||||||
return tileType === 'rainbow';
|
return this.getTileType(tileType) === 'rainbow';
|
||||||
},
|
},
|
||||||
|
|
||||||
// Hilfsmethode: Prüfe ob ein Tile ein Power-up ist
|
// Hilfsmethode: Prüfe ob ein Tile ein Power-up ist
|
||||||
isPowerUpTile(tileType) {
|
isPowerUpTile(tileType) {
|
||||||
if (!tileType) return false;
|
const normalizedType = this.getTileType(tileType);
|
||||||
return this.isRocketTile(tileType) || tileType === 'bomb' || tileType === 'rocket-horizontal' || tileType === 'rocket-vertical' || this.isRainbowTile(tileType);
|
if (!normalizedType) return false;
|
||||||
|
return this.isRocketTile(normalizedType) || normalizedType === 'bomb' || this.isRainbowTile(normalizedType);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Hilfsmethode: Debug-Ausgabe für Power-ups
|
// Hilfsmethode: Debug-Ausgabe für Power-ups
|
||||||
@@ -1932,21 +2034,11 @@ export default {
|
|||||||
|
|
||||||
// JETZT erst Power-ups erstellen, nachdem die Tiles entfernt wurden
|
// JETZT erst Power-ups erstellen, nachdem die Tiles entfernt wurden
|
||||||
debugLog('🔧 Erstelle Power-ups nach der Tile-Entfernung...');
|
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
|
// 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();
|
this.$forceUpdate();
|
||||||
|
await this.wait(120);
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug: Zeige alle Power-ups nach der Erstellung
|
// Debug: Zeige alle Power-ups nach der Erstellung
|
||||||
debugLog('🔧 Debug: Alle Power-ups nach createPowerUpsForMatches:');
|
debugLog('🔧 Debug: Alle Power-ups nach createPowerUpsForMatches:');
|
||||||
@@ -1994,37 +2086,13 @@ export default {
|
|||||||
debugLog('🔧 Debug: Alle Power-ups nach dem Füllen:');
|
debugLog('🔧 Debug: Alle Power-ups nach dem Füllen:');
|
||||||
this.debugPowerUps();
|
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();
|
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WICHTIG: Prüfe Level-Objekte nach dem Verarbeiten der Matches
|
// WICHTIG: Prüfe Level-Objekte nach dem Verarbeiten der Matches
|
||||||
if (isPlayerMove && !this.isInitializingLevel) {
|
if (isPlayerMove && !this.isInitializingLevel) {
|
||||||
debugLog('🎯 Prüfe Level-Objekte nach Match-Verarbeitung...');
|
debugLog('🎯 Prüfe Level-Objekte nach Match-Verarbeitung...');
|
||||||
this.checkLevelObjectives();
|
this.checkLevelObjectives();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fall-Down-Logik: Bewege Tiles nach unten in leere Positionen
|
// Fall-Down-Logik: Bewege Tiles nach unten in leere Positionen
|
||||||
@@ -2051,15 +2119,9 @@ export default {
|
|||||||
|
|
||||||
// Wenn Position leer ist, prüfe ob sie im Layout gültig ist
|
// Wenn Position leer ist, prüfe ob sie im Layout gültig ist
|
||||||
if (!this.board[index]) {
|
if (!this.board[index]) {
|
||||||
if (this.currentLevelData && this.currentLevelData.boardLayout) {
|
if (!this.isPlayableBoardCell(row, col)) {
|
||||||
const layoutRows = this.currentLevelData.boardLayout.split('\n');
|
debugLog(`🔧 Position [${row}, ${col}] ist ein Design-Leerfeld - überspringe`);
|
||||||
if (row < layoutRows.length && col < layoutRows[row].length) {
|
continue;
|
||||||
const targetChar = layoutRows[row][col];
|
|
||||||
if (targetChar !== 'x') {
|
|
||||||
debugLog(`🔧 Position [${row}, ${col}] ist ungültig im Layout (${targetChar}) - überspringe`);
|
|
||||||
continue; // Überspringe ungültige Positionen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog(`🔧 Leere Position gefunden: [${row}, ${col}] -> Index ${index}`);
|
debugLog(`🔧 Leere Position gefunden: [${row}, ${col}] -> Index ${index}`);
|
||||||
@@ -2069,7 +2131,7 @@ export default {
|
|||||||
for (let searchRow = row - 1; searchRow >= 0; searchRow--) {
|
for (let searchRow = row - 1; searchRow >= 0; searchRow--) {
|
||||||
const searchIndex = this.coordsToIndex(searchRow, col);
|
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}]`);
|
debugLog(`🔧 Tile ${this.board[searchIndex].type} gefunden an [${searchRow}, ${col}] -> verschiebe nach [${row}, ${col}]`);
|
||||||
|
|
||||||
// Verschiebe Tile nach unten
|
// Verschiebe Tile nach unten
|
||||||
@@ -2079,8 +2141,8 @@ export default {
|
|||||||
// Aktualisiere DOM
|
// Aktualisiere DOM
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
|
||||||
// Warte kurz für Animation
|
// Kurze Wartezeit, damit die Bewegung sichtbar bleibt ohne das Spiel zu bremsen
|
||||||
await this.wait(500);
|
await this.wait(120);
|
||||||
|
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
tileFound = true;
|
tileFound = true;
|
||||||
@@ -2104,16 +2166,10 @@ export default {
|
|||||||
|
|
||||||
// Wenn oberste Position leer ist, prüfe ob sie im Layout gültig ist
|
// Wenn oberste Position leer ist, prüfe ob sie im Layout gültig ist
|
||||||
if (!this.board[index]) {
|
if (!this.board[index]) {
|
||||||
if (this.currentLevelData && this.currentLevelData.boardLayout) {
|
if (!this.isPlayableBoardCell(0, col)) {
|
||||||
const layoutRows = this.currentLevelData.boardLayout.split('\n');
|
debugLog(`🔧 Oberste Position [0, ${col}] ist ein Design-Leerfeld - überspringe`);
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erstelle neues Tile
|
// Erstelle neues Tile
|
||||||
const newTile = this.createRandomTile();
|
const newTile = this.createRandomTile();
|
||||||
@@ -2125,7 +2181,7 @@ export default {
|
|||||||
|
|
||||||
// Aktualisiere DOM nach dem Auffüllen
|
// Aktualisiere DOM nach dem Auffüllen
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
await this.wait(100);
|
await this.wait(40);
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog(`🔧 Iteration ${iteration} abgeschlossen - Änderungen: ${hasChanges}`);
|
debugLog(`🔧 Iteration ${iteration} abgeschlossen - Änderungen: ${hasChanges}`);
|
||||||
@@ -2138,6 +2194,37 @@ export default {
|
|||||||
debugLog('🔧 Fall-Down-Logik abgeschlossen');
|
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
|
// Hilfsfunktion: Zeige den aktuellen Board-Zustand in der Konsole
|
||||||
printBoardState() {
|
printBoardState() {
|
||||||
debugLog('🔧 Board-Zustand:');
|
debugLog('🔧 Board-Zustand:');
|
||||||
@@ -2226,8 +2313,8 @@ export default {
|
|||||||
if (newTilesAdded > 0) {
|
if (newTilesAdded > 0) {
|
||||||
debugLog('🔍 Überprüfe das Brett auf Matches nach dem Füllen der leeren Positionen...');
|
debugLog('🔍 Überprüfe das Brett auf Matches nach dem Füllen der leeren Positionen...');
|
||||||
|
|
||||||
// Warte kurz, damit die neuen Tiles vollständig angezeigt werden
|
// Nur kurz warten, damit das Brett aktualisiert ist
|
||||||
await this.wait(300);
|
await this.wait(80);
|
||||||
|
|
||||||
// Prüfe auf Matches auf dem aktuellen Board
|
// Prüfe auf Matches auf dem aktuellen Board
|
||||||
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
|
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
|
||||||
@@ -2285,7 +2372,7 @@ export default {
|
|||||||
match.forEach(pos => usedPositions.add(pos));
|
match.forEach(pos => usedPositions.add(pos));
|
||||||
|
|
||||||
// Erstelle Regenbogen-Tile
|
// Erstelle Regenbogen-Tile
|
||||||
this.board[rainbowIndex] = { type: 'rainbow' };
|
this.board[rainbowIndex] = this.createTile('rainbow');
|
||||||
|
|
||||||
debugLog(`🌈 Regenbogen-Tile an Position ${rainbowIndex} erstellt`);
|
debugLog(`🌈 Regenbogen-Tile an Position ${rainbowIndex} erstellt`);
|
||||||
debugLog(`🔧 Board[${rainbowIndex}] = ${JSON.stringify(this.board[rainbowIndex])}`);
|
debugLog(`🔧 Board[${rainbowIndex}] = ${JSON.stringify(this.board[rainbowIndex])}`);
|
||||||
@@ -2309,7 +2396,7 @@ export default {
|
|||||||
usedPositions.add(match.corner);
|
usedPositions.add(match.corner);
|
||||||
|
|
||||||
// Bombe an der Ecke erstellen
|
// 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(`💣 Bombe an Position ${match.corner} erstellt`);
|
||||||
debugLog(`🔧 Board[${match.corner}] = ${JSON.stringify(this.board[match.corner])}`);
|
debugLog(`🔧 Board[${match.corner}] = ${JSON.stringify(this.board[match.corner])}`);
|
||||||
@@ -2337,7 +2424,7 @@ export default {
|
|||||||
|
|
||||||
// Erstelle Rakete basierend auf der Richtung des Matches
|
// Erstelle Rakete basierend auf der Richtung des Matches
|
||||||
const rocketType = this.determineRocketType(match);
|
const rocketType = this.determineRocketType(match);
|
||||||
this.board[rocketIndex] = { type: rocketType };
|
this.board[rocketIndex] = this.createTile(rocketType);
|
||||||
|
|
||||||
debugLog(`🚀 Rakete ${rocketType} an Position ${rocketIndex} erstellt`);
|
debugLog(`🚀 Rakete ${rocketType} an Position ${rocketIndex} erstellt`);
|
||||||
debugLog(`🔧 Board[${rocketIndex}] = ${JSON.stringify(this.board[rocketIndex])}`);
|
debugLog(`🔧 Board[${rocketIndex}] = ${JSON.stringify(this.board[rocketIndex])}`);
|
||||||
@@ -2380,53 +2467,21 @@ export default {
|
|||||||
async checkAndFillEmptyValidFields() {
|
async checkAndFillEmptyValidFields() {
|
||||||
debugLog('🔧 Prüfe alle leeren gültigen Felder...');
|
debugLog('🔧 Prüfe alle leeren gültigen Felder...');
|
||||||
|
|
||||||
let hasEmptyValidFields = false;
|
let safetyCounter = 0;
|
||||||
const emptyValidFields = [];
|
let emptyValidFields = this.collectEmptyPlayableFields();
|
||||||
|
|
||||||
// Gehe durch alle Positionen im Board
|
while (emptyValidFields.length > 0 && safetyCounter < 8) {
|
||||||
for (let row = 0; row < this.boardHeight; row++) {
|
safetyCounter++;
|
||||||
for (let col = 0; col < this.boardWidth; col++) {
|
debugLog(`🔧 ${emptyValidFields.length} leere gültige Felder gefunden - Füllrunde ${safetyCounter}`);
|
||||||
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
|
|
||||||
for (const field of emptyValidFields) {
|
for (const field of emptyValidFields) {
|
||||||
const newTile = this.createRandomTile();
|
const newTile = this.createRandomTile();
|
||||||
this.board[field.index] = newTile;
|
this.board[field.index] = newTile;
|
||||||
debugLog(`🔧 Neues Tile ${newTile.type} an Position [${field.row}, ${field.col}] hinzugefügt`);
|
debugLog(`🔧 Neues Tile ${newTile.type} an Position [${field.row}, ${field.col}] hinzugefügt`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aktualisiere die Anzeige
|
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
|
||||||
// Führe Animation für neue Tiles aus
|
|
||||||
await this.animateNewTilesAppearing(emptyValidFields.map(field => ({
|
await this.animateNewTilesAppearing(emptyValidFields.map(field => ({
|
||||||
index: field.index,
|
index: field.index,
|
||||||
row: field.row,
|
row: field.row,
|
||||||
@@ -2434,45 +2489,23 @@ export default {
|
|||||||
type: this.board[field.index].type
|
type: this.board[field.index].type
|
||||||
})));
|
})));
|
||||||
|
|
||||||
debugLog(`🔧 Alle leeren gültigen Felder gefüllt`);
|
await this.wait(40);
|
||||||
|
await this.fallTilesDown();
|
||||||
|
emptyValidFields = this.collectEmptyPlayableFields();
|
||||||
|
}
|
||||||
|
|
||||||
// WICHTIG: Nach dem Füllen der leeren gültigen Felder das Brett auf Matches überprüfen
|
if (safetyCounter >= 8 && emptyValidFields.length > 0) {
|
||||||
debugLog('🔍 Überprüfe das Brett auf Matches nach dem Füllen der leeren gültigen Felder...');
|
debugLog(`⚠️ Brett konnte nach ${safetyCounter} Füllrunden nicht vollständig stabilisiert werden`);
|
||||||
|
}
|
||||||
|
|
||||||
// Warte kurz, damit die neuen Tiles vollständig angezeigt werden
|
if (emptyValidFields.length === 0) {
|
||||||
await this.wait(300);
|
debugLog('🔧 Alle spielbaren Felder enthalten Tiles - Board ist vollständig');
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfe auf Matches auf dem aktuellen Board
|
|
||||||
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
|
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
|
||||||
|
|
||||||
if (matchesAfterFill.length > 0) {
|
if (matchesAfterFill.length > 0) {
|
||||||
debugLog(`🔍 ${matchesAfterFill.length} Match(es) nach dem Füllen der leeren gültigen Felder gefunden - starte automatische Behandlung`);
|
debugLog(`🔍 ${matchesAfterFill.length} Match(es) nach der Stabilisierung gefunden - starte automatische Behandlung`);
|
||||||
|
|
||||||
// Behandle die gefundenen Matches automatisch (kein Spieler-Move)
|
|
||||||
await this.handleMatches(matchesAfterFill, false);
|
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');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2498,8 +2531,8 @@ export default {
|
|||||||
|
|
||||||
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Animation vorbereitet`);
|
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Animation vorbereitet`);
|
||||||
|
|
||||||
// Warte auf die Animation (0,75 Sekunden)
|
// Warte auf die Animation
|
||||||
await this.wait(750);
|
await this.wait(280);
|
||||||
|
|
||||||
// Entferne die CSS-Klassen
|
// Entferne die CSS-Klassen
|
||||||
tileElements.forEach(element => {
|
tileElements.forEach(element => {
|
||||||
@@ -2525,7 +2558,7 @@ export default {
|
|||||||
|
|
||||||
// Setze das Tile an seine ursprüngliche Position (oben) mit transform
|
// Setze das Tile an seine ursprüngliche Position (oben) mit transform
|
||||||
element.style.transform = `translateY(-${fallPixels}px)`;
|
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
|
// Füge CSS-Klasse für die Fall-Animation hinzu
|
||||||
element.classList.add('falling');
|
element.classList.add('falling');
|
||||||
@@ -2553,8 +2586,8 @@ export default {
|
|||||||
// Spiele Fall-Sound ab
|
// Spiele Fall-Sound ab
|
||||||
this.playSound('falling');
|
this.playSound('falling');
|
||||||
|
|
||||||
// Warte auf die Fall-Animation (0,4 Sekunden)
|
// Warte auf die Fall-Animation
|
||||||
await this.wait(400);
|
await this.wait(180);
|
||||||
|
|
||||||
// Entferne die CSS-Klassen und transform-Eigenschaften
|
// Entferne die CSS-Klassen und transform-Eigenschaften
|
||||||
tileElements.forEach(element => {
|
tileElements.forEach(element => {
|
||||||
@@ -2588,8 +2621,8 @@ export default {
|
|||||||
|
|
||||||
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Erscheinungs-Animation vorbereitet`);
|
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Erscheinungs-Animation vorbereitet`);
|
||||||
|
|
||||||
// Warte auf die Erscheinungs-Animation (0,5 Sekunden)
|
// Warte auf die Erscheinungs-Animation
|
||||||
await this.wait(500);
|
await this.wait(180);
|
||||||
|
|
||||||
// Entferne die CSS-Klassen
|
// Entferne die CSS-Klassen
|
||||||
tileElements.forEach(element => {
|
tileElements.forEach(element => {
|
||||||
@@ -2606,12 +2639,12 @@ export default {
|
|||||||
const randomType = this.currentLevelData.tileTypes[
|
const randomType = this.currentLevelData.tileTypes[
|
||||||
Math.floor(Math.random() * this.currentLevelData.tileTypes.length)
|
Math.floor(Math.random() * this.currentLevelData.tileTypes.length)
|
||||||
];
|
];
|
||||||
return { type: randomType };
|
return this.createTile(randomType);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: Verwende Standard-Tile-Typen
|
// Fallback: Verwende Standard-Tile-Typen
|
||||||
const defaultTypes = ['red', 'blue', 'green', 'yellow', 'purple'];
|
const defaultTypes = ['red', 'blue', 'green', 'yellow', 'purple'];
|
||||||
const randomType = defaultTypes[Math.floor(Math.random() * defaultTypes.length)];
|
const randomType = defaultTypes[Math.floor(Math.random() * defaultTypes.length)];
|
||||||
return { type: randomType };
|
return this.createTile(randomType);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2802,28 +2835,15 @@ export default {
|
|||||||
async checkForCascadeMatches() {
|
async checkForCascadeMatches() {
|
||||||
debugLog('🔧 Prüfe auf Cascade-Matches...');
|
debugLog('🔧 Prüfe auf Cascade-Matches...');
|
||||||
|
|
||||||
// Warte kurz, damit alle Animationen abgeschlossen sind
|
// Nur kurze Synchronisierung nach dem Fallen
|
||||||
await this.wait(200);
|
await this.wait(60);
|
||||||
|
|
||||||
// Prüfe ob neue Matches entstanden sind
|
// Prüfe ob neue Matches entstanden sind
|
||||||
const newMatches = this.findMatchesOnBoard(this.board, false);
|
const newMatches = this.findMatchesOnBoard(this.board, false);
|
||||||
|
|
||||||
// Filtere Power-up-Matches heraus (diese sollen nicht als Cascade-Matches behandelt werden)
|
if (newMatches.length > 0) {
|
||||||
const filteredMatches = newMatches.filter(match => {
|
debugLog(`🔧 ${newMatches.length} neue Cascade-Matches gefunden`);
|
||||||
if (match.type === 'l-shape') {
|
await this.handleMatches(newMatches, false);
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
debugLog('🔧 Keine neuen Cascade-Matches gefunden');
|
debugLog('🔧 Keine neuen Cascade-Matches gefunden');
|
||||||
}
|
}
|
||||||
@@ -3412,13 +3432,18 @@ export default {
|
|||||||
|
|
||||||
debugLog(`🔧 Starte Drag für Tile ${tileIndex}`);
|
debugLog(`🔧 Starte Drag für Tile ${tileIndex}`);
|
||||||
|
|
||||||
|
const pointer = this.getPointerCoordinates(event);
|
||||||
|
if (!pointer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Setze Drag-Status
|
// Setze Drag-Status
|
||||||
this.draggedTileIndex = tileIndex;
|
this.draggedTileIndex = tileIndex;
|
||||||
this.isDragging = true;
|
this.isDragging = true;
|
||||||
|
|
||||||
// Speichere Start-Position für Drag-Offset
|
// Speichere Start-Position für Drag-Offset
|
||||||
this.dragStartX = event.clientX;
|
this.dragStartX = pointer.x;
|
||||||
this.dragStartY = event.clientY;
|
this.dragStartY = pointer.y;
|
||||||
|
|
||||||
// WICHTIG: Speichere die ursprüngliche Position des gedraggten Tiles
|
// WICHTIG: Speichere die ursprüngliche Position des gedraggten Tiles
|
||||||
const tileElement = event.target.closest('.game-tile');
|
const tileElement = event.target.closest('.game-tile');
|
||||||
@@ -3484,11 +3509,20 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog(`🔧 onTileMouseMove: clientX=${event.clientX}, clientY=${event.clientY}`);
|
const pointer = this.getPointerCoordinates(event);
|
||||||
|
if (!pointer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event?.cancelable) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog(`🔧 onTileMouseMove: clientX=${pointer.x}, clientY=${pointer.y}`);
|
||||||
|
|
||||||
// Berechne Drag-Offset
|
// Berechne Drag-Offset
|
||||||
const deltaX = event.clientX - this.dragStartX;
|
const deltaX = pointer.x - this.dragStartX;
|
||||||
const deltaY = event.clientY - this.dragStartY;
|
const deltaY = pointer.y - this.dragStartY;
|
||||||
|
|
||||||
debugLog(`🔧 Drag-Offset: deltaX=${deltaX}px, deltaY=${deltaY}px`);
|
debugLog(`🔧 Drag-Offset: deltaX=${deltaX}px, deltaY=${deltaY}px`);
|
||||||
|
|
||||||
@@ -3724,7 +3758,7 @@ export default {
|
|||||||
onGlobalMouseUp(event) {
|
onGlobalMouseUp(event) {
|
||||||
if (this.isDragging) {
|
if (this.isDragging) {
|
||||||
debugLog(`🔧 Globaler MouseUp während Drag - beende Drag`);
|
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}`);
|
debugLog(`🔧 Beende Drag: draggedTileIndex=${this.draggedTileIndex}, targetTile=${tileIndex}`);
|
||||||
|
|
||||||
// WICHTIG: Prüfe ob ein Tile tatsächlich animiert wurde
|
// WICHTIG: Prüfe ob ein Tile tatsächlich animiert wurde
|
||||||
let shouldPerformMove = false;
|
const targetTileIndex = this.resolveDragTargetIndex(event, tileIndex);
|
||||||
let targetTileIndex = null;
|
const shouldPerformMove =
|
||||||
|
targetTileIndex !== null &&
|
||||||
if (this.draggedTileIndex !== tileIndex) {
|
targetTileIndex !== this.draggedTileIndex &&
|
||||||
// Verschiedene Tiles - prüfe ob das Ziel-Tile animiert wurde
|
this.areTilesAdjacent(this.draggedTileIndex, targetTileIndex);
|
||||||
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`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setze alle Animationen zurück
|
// Setze alle Animationen zurück
|
||||||
this.resetAllTileAnimations();
|
this.resetAllTileAnimations();
|
||||||
@@ -3786,7 +3808,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Beende den Drag korrekt
|
// Beende den Drag korrekt
|
||||||
endDrag() {
|
endDrag(event = null) {
|
||||||
debugLog(`🔧 endDrag aufgerufen`);
|
debugLog(`🔧 endDrag aufgerufen`);
|
||||||
|
|
||||||
if (!this.isDragging) {
|
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
|
// Prüfe ob ein Tile tatsächlich animiert wurde UND ob es sich um ein anderes Tile handelt
|
||||||
let shouldPerformMove = false;
|
let targetTileIndex = this.resolveDragTargetIndex(event);
|
||||||
let targetTileIndex = null;
|
let shouldPerformMove =
|
||||||
|
targetTileIndex !== null &&
|
||||||
if (this.currentlyAnimatingTile !== null && this.currentlyAnimatingTile !== this.draggedTileIndex) {
|
targetTileIndex !== this.draggedTileIndex &&
|
||||||
// Ein anderes Tile ist animiert - führe Move durch
|
this.areTilesAdjacent(this.draggedTileIndex, targetTileIndex);
|
||||||
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`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zusätzliche Prüfung: Wenn das gedraggte Tile fast an seiner ursprünglichen Position ist, kein Move
|
// Zusätzliche Prüfung: Wenn das gedraggte Tile fast an seiner ursprünglichen Position ist, kein Move
|
||||||
if (shouldPerformMove && this.originalTilePosition) {
|
if (shouldPerformMove && this.originalTilePosition) {
|
||||||
@@ -4396,7 +4409,8 @@ export default {
|
|||||||
|
|
||||||
// Hilfsmethode: Prüfe ob ein Tile ein Power-Up ist
|
// Hilfsmethode: Prüfe ob ein Tile ein Power-Up ist
|
||||||
isPowerUpTile(tile) {
|
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
|
// Hilfsmethode: Prüfe ob zwei Tiles matchen können
|
||||||
@@ -4993,7 +5007,16 @@ export default {
|
|||||||
// WICHTIG: Zähle den Zug nur einmal hier, nicht in den nachfolgenden Funktionen
|
// WICHTIG: Zähle den Zug nur einmal hier, nicht in den nachfolgenden Funktionen
|
||||||
this.countPowerUpMove();
|
this.countPowerUpMove();
|
||||||
|
|
||||||
if (originalTile1.type === 'rainbow' && originalTile2.type === 'rainbow') {
|
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 (tile1IsRainbow && tile2IsRainbow) {
|
||||||
// Spiele Regenbogen-Sound
|
// Spiele Regenbogen-Sound
|
||||||
this.playSound('rainbow');
|
this.playSound('rainbow');
|
||||||
|
|
||||||
@@ -5001,29 +5024,48 @@ export default {
|
|||||||
debugLog('🌈 Zwei Regenbogen-Tiles kombiniert - entferne alle Tiles vom Board!');
|
debugLog('🌈 Zwei Regenbogen-Tiles kombiniert - entferne alle Tiles vom Board!');
|
||||||
await this.removeAllTilesFromBoardIncludingRainbows();
|
await this.removeAllTilesFromBoardIncludingRainbows();
|
||||||
return true;
|
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
|
// Spiele Regenbogen-Sound
|
||||||
this.playSound('rainbow');
|
this.playSound('rainbow');
|
||||||
|
|
||||||
// Ein Regenbogen-Tile mit normalem Tile getauscht: Entferne alle Tiles des normalen Typs
|
// Ein Regenbogen-Tile mit normalem Tile getauscht: Entferne alle Tiles des normalen Typs
|
||||||
const normalTile = originalTile1.type === 'rainbow' ? originalTile2 : originalTile1;
|
const normalTile = tile1IsRainbow ? originalTile2 : originalTile1;
|
||||||
const rainbowIndex = originalTile1.type === 'rainbow' ? this.findTileIndex(originalTile1) : this.findTileIndex(originalTile2);
|
const rainbowIndex = tile1IsRainbow ? this.findTileIndex(originalTile1) : this.findTileIndex(originalTile2);
|
||||||
|
|
||||||
if (rainbowIndex !== null) {
|
if (rainbowIndex !== null) {
|
||||||
await this.activateRainbowByType(rainbowIndex, normalTile.type);
|
await this.activateRainbowByType(rainbowIndex, normalTile.type);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else if (originalTile1.type === 'bomb' && originalTile2.type === 'bomb') {
|
} else if (tile1IsBomb && tile2IsBomb) {
|
||||||
// Zwei Bomben-Tiles getauscht: Entferne 2 Ringe (5x5 Bereich)
|
// Zwei Bomben-Tiles getauscht: Entferne 2 Ringe (5x5 Bereich)
|
||||||
const bombIndex = this.findBombIndex(originalTile1, originalTile2);
|
const bombIndex = this.findBombIndex(originalTile1, originalTile2);
|
||||||
if (bombIndex !== null) {
|
if (bombIndex !== null) {
|
||||||
this.explodeBomb(bombIndex, 2, true); // 2 Ringe = 5x5 Bereich - manuelle Aktivierung
|
this.explodeBomb(bombIndex, 2, true); // 2 Ringe = 5x5 Bereich - manuelle Aktivierung
|
||||||
}
|
}
|
||||||
return true;
|
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
|
// Ein Bomben-Tile mit normalem Tile getauscht: Entferne 9 Tiles rund um das Ziel
|
||||||
const bombTile = originalTile1.type === 'bomb' ? originalTile1 : originalTile2;
|
const bombTile = tile1IsBomb ? originalTile1 : originalTile2;
|
||||||
const targetTile = originalTile1.type === 'bomb' ? originalTile2 : originalTile1;
|
|
||||||
|
|
||||||
// Finde die neue Position der Bombe
|
// Finde die neue Position der Bombe
|
||||||
const newBombIndex = this.findTileIndex(bombTile);
|
const newBombIndex = this.findTileIndex(bombTile);
|
||||||
@@ -5031,41 +5073,10 @@ export default {
|
|||||||
this.explodeBomb(newBombIndex, 1, true); // 1 Ring = 3x3 Bereich - manuelle Aktivierung
|
this.explodeBomb(newBombIndex, 1, true); // 1 Ring = 3x3 Bereich - manuelle Aktivierung
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else if ((originalTile1.type === 'bomb' && originalTile2.type === 'rainbow') ||
|
} else if (tile1IsRocket || tile2IsRocket) {
|
||||||
(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') {
|
|
||||||
// Eine Rakete mit normalem Tile: Rakete fliegt auf zufälliges Feld
|
// Eine Rakete mit normalem Tile: Rakete fliegt auf zufälliges Feld
|
||||||
const rocketTile = originalTile1.type === 'rocket' ? originalTile1 : originalTile2;
|
const rocketTile = tile1IsRocket ? originalTile1 : originalTile2;
|
||||||
const targetTile = originalTile1.type === 'rocket' ? originalTile2 : originalTile1;
|
const targetTile = tile1IsRocket ? originalTile2 : originalTile1;
|
||||||
this.handleRocketLaunch(rocketTile, targetTile);
|
this.handleRocketLaunch(rocketTile, targetTile);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -5087,7 +5098,10 @@ export default {
|
|||||||
findTileIndex(originalTile) {
|
findTileIndex(originalTile) {
|
||||||
// Suche nach dem Tile auf dem Board
|
// Suche nach dem Tile auf dem Board
|
||||||
for (let i = 0; i < this.board.length; i++) {
|
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;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5291,7 +5305,7 @@ export default {
|
|||||||
if (powerUp.type === 'bomb') {
|
if (powerUp.type === 'bomb') {
|
||||||
// Bombe explodiert mit 1 Ring (3x3 Bereich)
|
// Bombe explodiert mit 1 Ring (3x3 Bereich)
|
||||||
this.explodeBomb(powerUp.index, 1, true);
|
this.explodeBomb(powerUp.index, 1, true);
|
||||||
} else if (powerUp.type === 'rocket') {
|
} else if (this.isRocketTile(powerUp.type)) {
|
||||||
// Rakete startet auf zufälliges Feld
|
// Rakete startet auf zufälliges Feld
|
||||||
this.launchRocketToRandomField(powerUp.index);
|
this.launchRocketToRandomField(powerUp.index);
|
||||||
}
|
}
|
||||||
@@ -5401,7 +5415,7 @@ export default {
|
|||||||
|
|
||||||
// Finde alle Raketen auf dem Board
|
// Finde alle Raketen auf dem Board
|
||||||
for (let i = 0; i < this.board.length; i++) {
|
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);
|
rocketIndices.push(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5424,7 +5438,7 @@ export default {
|
|||||||
// Neue Methode: Behandle Rakete + Bombe Kombination
|
// Neue Methode: Behandle Rakete + Bombe Kombination
|
||||||
handleRocketBombCombination(originalTile1, originalTile2) {
|
handleRocketBombCombination(originalTile1, originalTile2) {
|
||||||
// Finde die Position einer der Power-Ups (für die 4 Nachbarfelder)
|
// 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;
|
const bombTile = originalTile1.type === 'bomb' ? originalTile1 : originalTile2;
|
||||||
|
|
||||||
// Finde die Position auf dem Board
|
// Finde die Position auf dem Board
|
||||||
@@ -5563,11 +5577,17 @@ export default {
|
|||||||
y: endRect.top - boardRect.top + endRect.height / 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;
|
this.showRocketFlight = true;
|
||||||
|
|
||||||
// Verstecke Animation nach der Dauer
|
// Verstecke Animation nach der Dauer
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.showRocketFlight = false;
|
this.showRocketFlight = false;
|
||||||
|
this.rocketTarget = { x: -1, y: -1 };
|
||||||
}, 1200);
|
}, 1200);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -6410,6 +6430,7 @@ export default {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
position: relative; /* Wichtig für absolute Positionierung der animierten Tiles */
|
position: relative; /* Wichtig für absolute Positionierung der animierten Tiles */
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-tile {
|
.game-tile {
|
||||||
@@ -6427,6 +6448,7 @@ export default {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Vergrößere den klickbaren Bereich für besseres Drag&Drop */
|
/* Vergrößere den klickbaren Bereich für besseres Drag&Drop */
|
||||||
@@ -6478,7 +6500,7 @@ export default {
|
|||||||
|
|
||||||
/* Schrumpf-Animation für das Entfernen von Tiles */
|
/* Schrumpf-Animation für das Entfernen von Tiles */
|
||||||
.game-tile.removing {
|
.game-tile.removing {
|
||||||
transition: all 0.75s ease-out;
|
transition: all 0.28s ease-out;
|
||||||
transform: scale(0.1) rotate(360deg);
|
transform: scale(0.1) rotate(360deg);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
@@ -6498,12 +6520,12 @@ export default {
|
|||||||
|
|
||||||
/* Fall-Animation */
|
/* Fall-Animation */
|
||||||
.game-tile.falling {
|
.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;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-tile.new-tile {
|
.game-tile.new-tile {
|
||||||
transition: opacity 0.5s ease-in;
|
transition: opacity 0.18s ease-in;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6741,12 +6763,25 @@ export default {
|
|||||||
animation: explosion 2s ease-out forwards;
|
animation: explosion 2s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rocket-flight {
|
.rocket-flight-path {
|
||||||
width: 40px;
|
position: relative;
|
||||||
height: 40px;
|
width: 0;
|
||||||
background: linear-gradient(45deg, #ff6b6b, #ffd93d);
|
height: 0;
|
||||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
animation: rocketTravel 0.9s ease-in-out forwards;
|
||||||
animation: rocketFlight 3s 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 {
|
.rainbow-effect {
|
||||||
@@ -6895,35 +6930,51 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Raketen-Flug-Animation */
|
/* Raketen-Flug-Animation */
|
||||||
.rocket-flight-animation {
|
.rocket-target-marker {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
z-index: 2000;
|
z-index: 1400;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rocket-flight {
|
.rocket-target-marker__icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background: white;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 20px;
|
font-size: 22px;
|
||||||
animation: rocketFlight 1s ease-in-out forwards;
|
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% {
|
0% {
|
||||||
transform: scale(0.5);
|
transform: translate(0, 0) scale(0.7) rotate(-10deg);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
50% {
|
15% {
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
100% {
|
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;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -411,7 +411,6 @@ export default {
|
|||||||
}, 2 * 60 * 1000); // alle 2 Minuten
|
}, 2 * 60 * 1000); // alle 2 Minuten
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
console.log('🚪 Component unmounting, cleaning up...');
|
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -854,8 +853,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
console.log('🧹 Starting cleanup...');
|
|
||||||
|
|
||||||
// Game Loop stoppen
|
// Game Loop stoppen
|
||||||
if (this.gameLoop) {
|
if (this.gameLoop) {
|
||||||
clearTimeout(this.gameLoop);
|
clearTimeout(this.gameLoop);
|
||||||
@@ -929,20 +926,15 @@ export default {
|
|||||||
this.passengerImages = {};
|
this.passengerImages = {};
|
||||||
this.carImage = null; // Auto-Bild bereinigen
|
this.carImage = null; // Auto-Bild bereinigen
|
||||||
this.tiles = null;
|
this.tiles = null;
|
||||||
|
|
||||||
console.log('🧹 Cleanup completed');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Regelmäßige Memory-Cleanup-Methode
|
// Regelmäßige Memory-Cleanup-Methode
|
||||||
performMemoryCleanup() {
|
performMemoryCleanup() {
|
||||||
console.log('🧹 Performing memory cleanup...');
|
|
||||||
|
|
||||||
// Canvas NICHT leeren – das verursacht sichtbares Flackern / Grau
|
// Canvas NICHT leeren – das verursacht sichtbares Flackern / Grau
|
||||||
// Wir verlassen uns auf das reguläre render() zum Überschreiben des Frames
|
// Wir verlassen uns auf das reguläre render() zum Überschreiben des Frames
|
||||||
|
|
||||||
// Traffic Light States aggressiver bereinigen
|
// Traffic Light States aggressiver bereinigen
|
||||||
if (this.trafficLightStates && Object.keys(this.trafficLightStates).length > 20) {
|
if (this.trafficLightStates && Object.keys(this.trafficLightStates).length > 20) {
|
||||||
console.log('🧹 Cleaning up traffic light states');
|
|
||||||
// Nur States für aktuelle Map behalten
|
// Nur States für aktuelle Map behalten
|
||||||
if (this.currentMap && this.currentMap.tiles) {
|
if (this.currentMap && this.currentMap.tiles) {
|
||||||
const currentTileKeys = new Set();
|
const currentTileKeys = new Set();
|
||||||
@@ -966,12 +958,10 @@ export default {
|
|||||||
|
|
||||||
// Passagier-Listen aggressiver begrenzen
|
// Passagier-Listen aggressiver begrenzen
|
||||||
if (this.waitingPassengersList && this.waitingPassengersList.length > 20) {
|
if (this.waitingPassengersList && this.waitingPassengersList.length > 20) {
|
||||||
console.log('🧹 Trimming waiting passengers list');
|
|
||||||
this.waitingPassengersList = this.waitingPassengersList.slice(-10);
|
this.waitingPassengersList = this.waitingPassengersList.slice(-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.loadedPassengersList && this.loadedPassengersList.length > 20) {
|
if (this.loadedPassengersList && this.loadedPassengersList.length > 20) {
|
||||||
console.log('🧹 Trimming loaded passengers list');
|
|
||||||
this.loadedPassengersList = this.loadedPassengersList.slice(-10);
|
this.loadedPassengersList = this.loadedPassengersList.slice(-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -989,8 +979,6 @@ export default {
|
|||||||
if (window.gc) {
|
if (window.gc) {
|
||||||
window.gc();
|
window.gc();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🧹 Memory cleanup completed');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
generateLevel() {
|
generateLevel() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="calendar-kicker">Planung</span>
|
<span class="calendar-kicker">Planung</span>
|
||||||
<h2>{{ $t('personal.calendar.title') }}</h2>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
<p>Produktion, Lager, Handel und Finanzen greifen ineinander und erzeugen eine langfristige Aufbauspiel-Dynamik.</p>
|
<p>Produktion, Lager, Handel und Finanzen greifen ineinander und erzeugen eine langfristige Aufbauspiel-Dynamik.</p>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h2>Persoenliche Entwicklung</h2>
|
<h2>Persönliche Entwicklung</h2>
|
||||||
<p>Familie, Bildung, Gesundheit und gesellschaftlicher Status beeinflussen deinen Weg in Falukant.</p>
|
<p>Familie, Bildung, Gesundheit und gesellschaftlicher Status beeinflussen deinen Weg in Falukant.</p>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h2>Politik und Unterwelt</h2>
|
<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>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
<div class="cards">
|
<div class="cards">
|
||||||
<article>
|
<article>
|
||||||
<h2>Match 3</h2>
|
<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>
|
||||||
<article>
|
<article>
|
||||||
<h2>Taxi</h2>
|
<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>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<section class="marketing-page">
|
<section class="marketing-page">
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<p class="eyebrow">Sprachen online lernen</p>
|
<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">
|
<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.
|
motivierenden Lernfluss direkt im Browser.
|
||||||
</p>
|
</p>
|
||||||
<router-link class="cta" to="/">Kostenlos starten</router-link>
|
<router-link class="cta" to="/">Kostenlos starten</router-link>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<div class="features">
|
<div class="features">
|
||||||
<article>
|
<article>
|
||||||
<h2>Interaktive Kurse</h2>
|
<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>
|
||||||
<article>
|
<article>
|
||||||
<h2>Praxisorientiert</h2>
|
<h2>Praxisorientiert</h2>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h2>Teil einer Community</h2>
|
<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>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -30,14 +30,14 @@
|
|||||||
<input type="password" v-model="newpasswordretype"
|
<input type="password" v-model="newpasswordretype"
|
||||||
:placeholder="$t('settings.account.newpasswordretype')" autocomplete="new-password"
|
:placeholder="$t('settings.account.newpasswordretype')" autocomplete="new-password"
|
||||||
:class="{ 'field-error': newpasswordretype && !passwordsMatch }" />
|
: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>
|
||||||
|
|
||||||
<label class="account-settings__field account-settings__field--full">
|
<label class="account-settings__field account-settings__field--full">
|
||||||
<span>{{ $t("settings.account.oldpassword") }}</span>
|
<span>{{ $t("settings.account.oldpassword") }}</span>
|
||||||
<input type="password" v-model="oldpassword" :placeholder="$t('settings.account.oldpassword')"
|
<input type="password" v-model="oldpassword" :placeholder="$t('settings.account.oldpassword')"
|
||||||
autocomplete="current-password" :class="{ 'field-error': requiresOldPassword && !oldpassword.trim() }" />
|
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -96,13 +96,13 @@ export default {
|
|||||||
}
|
}
|
||||||
// Validiere Passwort-Wiederholung nur wenn ein neues Passwort eingegeben wurde
|
// Validiere Passwort-Wiederholung nur wenn ein neues Passwort eingegeben wurde
|
||||||
if (!this.passwordsMatch) {
|
if (!this.passwordsMatch) {
|
||||||
showError(this, 'Die Passwoerter stimmen nicht ueberein.');
|
showError(this, 'Die Passwörter stimmen nicht überein.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe ob das alte Passwort eingegeben wurde
|
// Prüfe ob das alte Passwort eingegeben wurde
|
||||||
if (!this.oldpassword || this.oldpassword.trim() === '') {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<div class="diary-view">
|
<div class="diary-view">
|
||||||
<section class="diary-hero surface-card">
|
<section class="diary-hero surface-card">
|
||||||
<div>
|
<div>
|
||||||
<span class="diary-kicker">Persoenliche Eintraege</span>
|
<span class="diary-kicker">Persönliche Einträge</span>
|
||||||
<h2>{{ $t('socialnetwork.diary.title') }}</h2>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -73,7 +73,6 @@ export default {
|
|||||||
},
|
},
|
||||||
async loadDiaryEntries(page) {
|
async loadDiaryEntries(page) {
|
||||||
try {
|
try {
|
||||||
console.log(page);
|
|
||||||
const response = await apiClient.get(`/api/socialnetwork/diary/${page}`);
|
const response = await apiClient.get(`/api/socialnetwork/diary/${page}`);
|
||||||
this.diaryEntries = response.data.entries;
|
this.diaryEntries = response.data.entries;
|
||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="forum-topic-back link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</div>
|
<div class="forum-topic-back link" @click="openForum()">{{ $t('socialnetwork.forum.title') }} {{ forumName }}</div>
|
||||||
<h2 v-if="forumTopic">{{ forumTopic }}</h2>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="forum-kicker">Community-Forum</span>
|
<span class="forum-kicker">Community-Forum</span>
|
||||||
<h2>{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
|
<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>
|
||||||
<div class="creationtoggler">
|
<div class="creationtoggler">
|
||||||
<button @click="createNewTopic">
|
<button @click="toggleCreation">
|
||||||
{{ $t(!inCreation
|
{{ $t(!inCreation
|
||||||
? 'socialnetwork.forum.showNewTopic'
|
? 'socialnetwork.forum.showNewTopic'
|
||||||
: 'socialnetwork.forum.hideNewTopic') }}
|
: 'socialnetwork.forum.hideNewTopic') }}
|
||||||
@@ -16,16 +16,26 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="inCreation" class="forum-creation surface-card">
|
<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">
|
<label class="newtitle">
|
||||||
<span>{{ $t('socialnetwork.forum.topic') }}</span>
|
<span>{{ $t('socialnetwork.forum.topic') }}</span>
|
||||||
<input type="text" v-model="newTitle" />
|
<input ref="titleInput" type="text" v-model="newTitle" />
|
||||||
</label>
|
</label>
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<EditorContent v-if="editor" :editor="editor" class="editor" />
|
<EditorContent v-if="editor" :editor="editor" class="editor" />
|
||||||
</div>
|
</div>
|
||||||
<button @click="saveNewTopic">
|
<div class="forum-creation__actions">
|
||||||
|
<button :disabled="!canSaveTopic" @click="saveNewTopic">
|
||||||
{{ $t('socialnetwork.forum.createNewTopic') }}
|
{{ $t('socialnetwork.forum.createNewTopic') }}
|
||||||
</button>
|
</button>
|
||||||
|
<span class="forum-creation__hint">Titel und Inhalt müssen beide ausgefüllt sein.</span>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-else-if="titles.length > 0" class="forum-topics surface-card">
|
<section v-else-if="titles.length > 0" class="forum-topics surface-card">
|
||||||
@@ -47,7 +57,8 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div v-else class="forum-empty surface-card">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -56,6 +67,7 @@
|
|||||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import apiClient from '../../utils/axios'
|
import apiClient from '../../utils/axios'
|
||||||
|
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ForumView',
|
name: 'ForumView',
|
||||||
@@ -78,6 +90,10 @@ export default {
|
|||||||
},
|
},
|
||||||
totalPages() {
|
totalPages() {
|
||||||
return Math.ceil(this.numberOfItems / 25)
|
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: {
|
watch: {
|
||||||
@@ -109,6 +125,24 @@ export default {
|
|||||||
if (this.editor) this.editor.destroy()
|
if (this.editor) this.editor.destroy()
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
async loadForum() {
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get(
|
const { data } = await apiClient.get(
|
||||||
@@ -121,16 +155,9 @@ export default {
|
|||||||
console.error('Fehler beim Laden des Forums', err)
|
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() {
|
async saveNewTopic() {
|
||||||
const content = this.editor ? this.editor.getHTML() : ''
|
const content = this.editor ? this.editor.getHTML() : ''
|
||||||
if (!this.newTitle.trim() || !content.trim()) return
|
if (!this.canSaveTopic) return
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.post(
|
const { data } = await apiClient.post(
|
||||||
'/api/forum/topic',
|
'/api/forum/topic',
|
||||||
@@ -147,8 +174,11 @@ export default {
|
|||||||
this.page = data.page
|
this.page = data.page
|
||||||
this.inCreation = false
|
this.inCreation = false
|
||||||
this.newTitle = ''
|
this.newTitle = ''
|
||||||
|
this.editor?.commands.setContent('')
|
||||||
|
showSuccess(this, 'Thema erfolgreich erstellt.')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Erstellen des Themas', err)
|
console.error('Fehler beim Erstellen des Themas', err)
|
||||||
|
showApiError(this, err, 'Fehler beim Erstellen des Themas')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goToPage(page) {
|
goToPage(page) {
|
||||||
@@ -211,6 +241,30 @@ export default {
|
|||||||
padding: 22px;
|
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 {
|
.newtitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -289,12 +343,20 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forum-empty p {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
@media (max-width: 960px) {
|
||||||
.forum-hero {
|
.forum-hero {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forum-creation__header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.topic-card__main {
|
.topic-card__main {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<div class="guestbook-view">
|
<div class="guestbook-view">
|
||||||
<section class="guestbook-hero surface-card">
|
<section class="guestbook-hero surface-card">
|
||||||
<div>
|
<div>
|
||||||
<span class="guestbook-kicker">Gaestebuch</span>
|
<span class="guestbook-kicker">Gästebuch</span>
|
||||||
<h2>{{ $t('socialnetwork.guestbook.title') }}</h2>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div v-if="guestbookEntries.length === 0" class="guestbook-empty surface-card">{{ $t('socialnetwork.profile.guestbook.noEntries') }}
|
<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">
|
<section class="vocab-chapter-hero surface-card">
|
||||||
<span class="vocab-chapter-hero__eyebrow">Vokabeltrainer</span>
|
<span class="vocab-chapter-hero__eyebrow">Vokabeltrainer</span>
|
||||||
<h2>{{ $t('socialnetwork.vocab.chapterTitle', { title: chapter?.title || '' }) }}</h2>
|
<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>
|
||||||
|
|
||||||
<section class="box surface-card">
|
<section class="box surface-card">
|
||||||
@@ -269,4 +269,3 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="vocab-courses-kicker">Kurse</span>
|
<span class="vocab-courses-kicker">Kurse</span>
|
||||||
<h2>{{ $t('socialnetwork.vocab.courses.title') }}</h2>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -208,12 +208,7 @@ export default {
|
|||||||
if (!this.selectedNativeLanguageId) {
|
if (!this.selectedNativeLanguageId) {
|
||||||
this.selectedNativeLanguageId = 'my';
|
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) {
|
} catch (e) {
|
||||||
console.error('Konnte Muttersprache nicht laden:', 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>
|
<option v-for="chapter in chapters" :key="chapter.id" :value="chapter.id">{{ chapter.title }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="form-actions form-actions-row">
|
||||||
<button type="submit" :disabled="!canCreateLesson">{{ $t('general.create') }}</button>
|
<button type="submit" :disabled="!canCreateLesson">{{ $t('general.create') }}</button>
|
||||||
<button type="button" @click="showAddLessonDialog = false" class="button-secondary">{{ $t('general.cancel') }}</button>
|
<button type="button" @click="showAddLessonDialog = false" class="button-secondary">{{ $t('general.cancel') }}</button>
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import apiClient from '@/utils/axios.js';
|
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 {
|
export default {
|
||||||
name: 'VocabCourseView',
|
name: 'VocabCourseView',
|
||||||
@@ -259,7 +259,7 @@ export default {
|
|||||||
showSuccess(this, 'Lektion erfolgreich angelegt.');
|
showSuccess(this, 'Lektion erfolgreich angelegt.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Hinzufügen der Lektion:', 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) {
|
async deleteLesson(lessonId) {
|
||||||
@@ -273,10 +273,10 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await apiClient.delete(`/api/vocab/lessons/${lessonId}`);
|
await apiClient.delete(`/api/vocab/lessons/${lessonId}`);
|
||||||
await this.loadCourse();
|
await this.loadCourse();
|
||||||
showSuccess(this, 'Lektion erfolgreich geloescht.');
|
showSuccess(this, 'Lektion erfolgreich gelöscht.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Löschen der Lektion:', 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) {
|
openLesson(lessonId) {
|
||||||
@@ -285,9 +285,8 @@ export default {
|
|||||||
editCourse() {
|
editCourse() {
|
||||||
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/edit`);
|
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/edit`);
|
||||||
},
|
},
|
||||||
editLesson(lessonId) {
|
editLesson() {
|
||||||
// TODO: Implement edit lesson
|
showInfo(this, 'Die Bearbeitung einzelner Lektionen folgt noch.');
|
||||||
console.log('Edit lesson', lessonId);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<span class="vocab-language-kicker">Sprache</span>
|
<span class="vocab-language-kicker">Sprache</span>
|
||||||
<h2>{{ $t('socialnetwork.vocab.languageTitle', { name: language?.name || '' }) }}</h2>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<label class="label form-field">
|
<label class="label form-field">
|
||||||
<span>{{ $t('socialnetwork.vocab.languageName') }}</span>
|
<span>{{ $t('socialnetwork.vocab.languageName') }}</span>
|
||||||
<input v-model="name" type="text" :class="{ 'field-error': nameTouched && !canSave }" />
|
<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>
|
<span v-if="nameTouched && !canSave" class="form-error">Der Name sollte mindestens 2 Zeichen haben.</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user