Compare commits

..

5 Commits

Author SHA1 Message Date
Torsten Schulz (local)
d74f7b852b Refactor proposal generation in FalukantService to improve character selection logic
- Removed the tracking of used character IDs and streamlined the exclusion of characters already proposed or currently active as directors.
- Enhanced logging for SQL queries and fallback mechanisms to ensure better visibility during character selection.
- Implemented a more efficient approach to gather and process character knowledge for proposal creation, ensuring accurate average calculations.
- Improved error handling to provide clearer feedback when no eligible characters are found.
2026-01-12 08:33:26 +01:00
Torsten Schulz (local)
92d6b15c3f Enhance proposal generation logic in FalukantService to prevent duplicate character usage
- Introduced a mechanism to track used character IDs, ensuring that previously proposed characters are excluded from future proposals.
- Added error handling and logging for scenarios where no eligible characters are found, improving traceability and user feedback.
- Implemented a fallback to include newer characters if older ones are unavailable, enhancing the robustness of the proposal generation process.
2026-01-12 08:24:00 +01:00
Torsten Schulz (local)
91f59062f5 Update BranchView to refresh active tab data on tab change and modify 3D model for female toddler character
- Enhanced the activeTab watcher to refresh data only when the selected branch changes and the tab is switched.
- Introduced a new refreshActiveTab method to load data for the currently active tab, improving data management and user experience.
- Updated the female toddler 3D model file for better integration in the application.
2026-01-12 08:07:50 +01:00
Torsten Schulz (local)
1674086c73 Enhance partner search and gift loading functionality in FalukantService and FamilyView
- Added detailed logging for partner search criteria and results in FalukantService to improve debugging and traceability.
- Refactored partner search logic to use a dynamic where clause for better readability and maintainability.
- Implemented error handling in FamilyView for gift loading, ensuring an empty array is set on failure to load gifts, enhancing user experience.
2026-01-09 14:28:01 +01:00
Torsten Schulz (local)
5ddb099f5a Add 3D character model integration and update dependencies
- Introduced a new CharacterModel3D component for rendering 3D character models in OverviewView.
- Updated package.json and package-lock.json to include 'three' library for 3D graphics support.
- Enhanced Vite configuration to allow access to external files and ensure proper handling of GLB/GLTF assets.
- Improved layout and styling in OverviewView for better visualization of character and avatar.
2026-01-09 13:29:32 +01:00
45 changed files with 1992 additions and 1117 deletions

View File

@@ -162,18 +162,6 @@ class FalukantController {
}
return this.service.getProductPriceInRegion(userId, productId, regionId);
});
this.getProductPricesInRegionBatch = this._wrapWithUser((userId, req) => {
const productIds = req.query.productIds;
const regionId = parseInt(req.query.regionId, 10);
if (!productIds || Number.isNaN(regionId)) {
throw new Error('productIds (comma-separated) and regionId are required');
}
const productIdArray = productIds.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !Number.isNaN(id));
if (productIdArray.length === 0) {
throw new Error('At least one valid productId is required');
}
return this.service.getProductPricesInRegionBatch(userId, productIdArray, regionId);
});
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10);
const currentPrice = parseFloat(req.query.currentPrice);

View File

@@ -25,13 +25,11 @@ function createServer() {
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
});
wss = new WebSocketServer({ server: httpsServer });
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
httpsServer.listen(PORT, '0.0.0.0', () => {
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
});
} else {
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
wss = new WebSocketServer({ port: PORT, host: '0.0.0.0' });
wss = new WebSocketServer({ port: PORT });
console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`);
}

View File

@@ -1,13 +0,0 @@
-- Rollback: Remove indexes for director proposals and character queries
-- Created: 2026-01-12
DROP INDEX IF EXISTS falukant_data.idx_character_region_user_created;
DROP INDEX IF EXISTS falukant_data.idx_character_region_user;
DROP INDEX IF EXISTS falukant_data.idx_character_user_id;
DROP INDEX IF EXISTS falukant_data.idx_director_proposal_employer_character;
DROP INDEX IF EXISTS falukant_data.idx_director_character_id;
DROP INDEX IF EXISTS falukant_data.idx_director_employer_user_id;
DROP INDEX IF EXISTS falukant_data.idx_knowledge_character_id;
DROP INDEX IF EXISTS falukant_data.idx_relationship_character1_id;
DROP INDEX IF EXISTS falukant_data.idx_child_relation_father_id;
DROP INDEX IF EXISTS falukant_data.idx_child_relation_mother_id;

View File

@@ -1,43 +0,0 @@
-- Migration: Add indexes for director proposals and character queries
-- Created: 2026-01-12
-- Index für schnelle Suche nach NPCs in einer Region (mit Altersbeschränkung)
CREATE INDEX IF NOT EXISTS idx_character_region_user_created
ON falukant_data.character (region_id, user_id, created_at)
WHERE user_id IS NULL;
-- Index für schnelle Suche nach NPCs ohne Altersbeschränkung
CREATE INDEX IF NOT EXISTS idx_character_region_user
ON falukant_data.character (region_id, user_id)
WHERE user_id IS NULL;
-- Index für Character-Suche nach user_id (wichtig für getFamily, getDirectorForBranch)
CREATE INDEX IF NOT EXISTS idx_character_user_id
ON falukant_data.character (user_id);
-- Index für Director-Proposals
CREATE INDEX IF NOT EXISTS idx_director_proposal_employer_character
ON falukant_data.director_proposal (employer_user_id, director_character_id);
-- Index für aktive Direktoren
CREATE INDEX IF NOT EXISTS idx_director_character_id
ON falukant_data.director (director_character_id);
-- Index für Director-Suche nach employer_user_id
CREATE INDEX IF NOT EXISTS idx_director_employer_user_id
ON falukant_data.director (employer_user_id);
-- Index für Knowledge-Berechnung
CREATE INDEX IF NOT EXISTS idx_knowledge_character_id
ON falukant_data.knowledge (character_id);
-- Index für Relationships (getFamily)
CREATE INDEX IF NOT EXISTS idx_relationship_character1_id
ON falukant_data.relationship (character1_id);
-- Index für ChildRelations (getFamily)
CREATE INDEX IF NOT EXISTS idx_child_relation_father_id
ON falukant_data.child_relation (father_id);
CREATE INDEX IF NOT EXISTS idx_child_relation_mother_id
ON falukant_data.child_relation (mother_id);

View File

@@ -76,7 +76,6 @@ router.get('/politics/open', falukantController.getOpenPolitics);
router.post('/politics/open', falukantController.applyForElections);
router.get('/cities', falukantController.getRegions);
router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
router.get('/products/prices-in-region-batch', falukantController.getProductPricesInRegionBatch);
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
router.get('/vehicles/types', falukantController.getVehicleTypes);

View File

@@ -1,55 +1,19 @@
import './config/loadEnv.js'; // .env deterministisch laden
import http from 'http';
import https from 'https';
import fs from 'fs';
import app from './app.js';
import { setupWebSocket } from './utils/socket.js';
import { syncDatabase } from './utils/syncDatabase.js';
// HTTP-Server für API (Port 2020, intern, über Apache-Proxy)
const API_PORT = Number.parseInt(process.env.PORT || '2020', 10);
const httpServer = http.createServer(app);
// Socket.io wird nur auf HTTPS-Server bereitgestellt, nicht auf HTTP-Server
// setupWebSocket(httpServer); // Entfernt: Socket.io nur über HTTPS
const server = http.createServer(app);
// HTTPS-Server für Socket.io (Port 4443, direkt erreichbar)
let httpsServer = null;
const SOCKET_IO_PORT = Number.parseInt(process.env.SOCKET_IO_PORT || '4443', 10);
const USE_TLS = process.env.SOCKET_IO_TLS === '1';
const TLS_KEY_PATH = process.env.SOCKET_IO_TLS_KEY_PATH;
const TLS_CERT_PATH = process.env.SOCKET_IO_TLS_CERT_PATH;
const TLS_CA_PATH = process.env.SOCKET_IO_TLS_CA_PATH;
if (USE_TLS && TLS_KEY_PATH && TLS_CERT_PATH) {
try {
httpsServer = https.createServer({
key: fs.readFileSync(TLS_KEY_PATH),
cert: fs.readFileSync(TLS_CERT_PATH),
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
}, app);
setupWebSocket(httpsServer);
console.log(`[Socket.io] HTTPS-Server für Socket.io konfiguriert auf Port ${SOCKET_IO_PORT}`);
} catch (err) {
console.error('[Socket.io] Fehler beim Laden der TLS-Zertifikate:', err.message);
console.error('[Socket.io] Socket.io wird nicht verfügbar sein');
}
} else {
console.warn('[Socket.io] TLS nicht konfiguriert - Socket.io wird nicht verfügbar sein');
}
setupWebSocket(server);
syncDatabase().then(() => {
// API-Server auf Port 2020 (intern, nur localhost)
httpServer.listen(API_PORT, '127.0.0.1', () => {
console.log(`[API] HTTP-Server läuft auf localhost:${API_PORT} (intern, über Apache-Proxy)`);
const port = process.env.PORT || 3001;
server.listen(port, () => {
console.log('Server is running on port', port);
});
// Socket.io-Server auf Port 4443 (extern, direkt erreichbar)
if (httpsServer) {
httpsServer.listen(SOCKET_IO_PORT, '0.0.0.0', () => {
console.log(`[Socket.io] HTTPS-Server läuft auf Port ${SOCKET_IO_PORT} (direkt erreichbar)`);
});
}
}).catch(err => {
console.error('Failed to sync database:', err);
process.exit(1);

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +0,0 @@
#!/bin/bash
echo "=== Apache WebSocket-Konfiguration prüfen ==="
echo ""
# Prüfe, welche Module aktiviert sind
echo "Aktivierte Apache-Module:"
apache2ctl -M 2>/dev/null | grep -E "(proxy|rewrite|ssl|headers)" || echo "Keine relevanten Module gefunden"
echo ""
# Prüfe, ob die benötigten Module aktiviert sind
REQUIRED_MODULES=("proxy" "proxy_http" "proxy_wstunnel" "rewrite" "ssl" "headers")
MISSING_MODULES=()
for module in "${REQUIRED_MODULES[@]}"; do
if ! apache2ctl -M 2>/dev/null | grep -q "${module}_module"; then
MISSING_MODULES+=("$module")
fi
done
if [ ${#MISSING_MODULES[@]} -eq 0 ]; then
echo "✅ Alle benötigten Module sind aktiviert"
else
echo "❌ Fehlende Module:"
for module in "${MISSING_MODULES[@]}"; do
echo " - $module"
done
echo ""
echo "Aktivieren mit:"
for module in "${MISSING_MODULES[@]}"; do
echo " sudo a2enmod $module"
done
fi
echo ""
echo "=== Apache-Konfiguration testen ==="
if sudo apache2ctl configtest 2>&1; then
echo "✅ Apache-Konfiguration ist gültig"
else
echo "❌ Apache-Konfiguration hat Fehler"
fi
echo ""
echo "=== Aktive VirtualHosts ==="
apache2ctl -S 2>/dev/null | grep -E "(443|4443|4551)" || echo "Keine relevanten VirtualHosts gefunden"

View File

@@ -1,35 +0,0 @@
#!/bin/bash
echo "=== WebSocket-Header Debug ==="
echo ""
echo "Prüfe Apache-Logs für WebSocket-Upgrade-Header..."
echo ""
# Prüfe die letzten 50 Zeilen des Access-Logs für /ws/ oder /socket.io/
echo "Access-Log Einträge für /ws/ und /socket.io/:"
sudo tail -50 /var/log/apache2/yourpart.access.log | grep -E "(/ws/|/socket.io/)" | tail -10
echo ""
echo "Prüfe Error-Log für WebSocket-Fehler:"
sudo tail -50 /var/log/apache2/yourpart.error.log | grep -iE "(websocket|upgrade|proxy)" | tail -10
echo ""
echo "=== Test mit curl ==="
echo ""
echo "Teste WebSocket-Upgrade für /ws/:"
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
https://www.your-part.de/ws/ 2>&1 | head -20
echo ""
echo "=== Prüfe Apache-Konfiguration ==="
echo ""
echo "Aktive Rewrite-Regeln für WebSocket:"
sudo apache2ctl -S 2>/dev/null | grep -A 5 "your-part.de:443" || echo "VirtualHost nicht gefunden"
echo ""
echo "Prüfe, ob mod_proxy_wstunnel aktiviert ist:"
apache2ctl -M 2>/dev/null | grep proxy_wstunnel || echo "mod_proxy_wstunnel NICHT aktiviert!"

View File

@@ -0,0 +1,143 @@
# 3D-Animationen im Falukant-Bereich
## Benötigte Dependencies
### Three.js (Empfohlen)
```bash
npm install three
npm install @types/three --save-dev # Für TypeScript-Support
```
**Alternative Optionen:**
- **Babylon.js**: Mächtiger, aber größer (~500KB vs ~600KB)
- **A-Frame**: WebVR-fokussiert, einfacher für VR/AR
- **React Three Fiber**: Falls React verwendet wird (hier Vue)
**Empfehlung: Three.js** - am weitesten verbreitet, beste Dokumentation, große Community
### Optional: Vue-Three.js Wrapper
```bash
npm install vue-threejs # Oder troika-three-text für Text-Rendering
```
## Sinnvolle Seiten für 3D-Animationen
### 1. **OverviewView** (Hauptübersicht)
**Sinnvoll:** ⭐⭐⭐⭐⭐
- **3D-Charakter-Modell**: Rotierendes 3D-Modell des eigenen Charakters
- **Statussymbole**: 3D-Icons für Geld, Gesundheit, Reputation (schwebend/rotierend)
- **Hintergrund**: Subtile 3D-Szene (z.B. mittelalterliche Stadt im Hintergrund)
### 2. **HouseView** (Haus)
**Sinnvoll:** ⭐⭐⭐⭐⭐
- **3D-Haus-Modell**: Interaktives 3D-Modell des eigenen Hauses
- **Upgrade-Visualisierung**: Animation beim Haus-Upgrade
- **Zustand-Anzeige**: 3D-Visualisierung von Dach, Wänden, Boden, Fenstern
### 3. **BranchView** (Niederlassungen)
**Sinnvoll:** ⭐⭐⭐⭐
- **3D-Fabrik/Gebäude**: 3D-Modell der Niederlassung
- **Produktions-Animation**: 3D-Animationen für laufende Produktionen
- **Transport-Visualisierung**: 3D-Wagen/Karren für Transporte
### 4. **FamilyView** (Familie)
**Sinnvoll:** ⭐⭐⭐⭐
- **3D-Charaktere**: 3D-Modelle von Partner und Kindern
- **Beziehungs-Visualisierung**: 3D-Animationen für Beziehungsstatus
- **Geschenk-Animation**: 3D-Animation beim Verschenken
### 5. **HealthView** (Gesundheit)
**Sinnvoll:** ⭐⭐⭐
- **3D-Körper-Modell**: 3D-Visualisierung des Gesundheitszustands
- **Aktivitäts-Animationen**: 3D-Animationen für Gesundheitsaktivitäten
### 6. **NobilityView** (Sozialstatus)
**Sinnvoll:** ⭐⭐⭐
- **3D-Wappen**: Rotierendes 3D-Wappen
- **Insignien**: 3D-Krone, Schwert, etc. je nach Titel
### 7. **ChurchView** (Kirche)
**Sinnvoll:** ⭐⭐⭐
- **3D-Kirche**: 3D-Modell der Kirche
- **Taufe-Animation**: 3D-Animation bei der Taufe
### 8. **BankView** (Bank)
**Sinnvoll:** ⭐⭐
- **3D-Bankgebäude**: 3D-Modell der Bank
- **Geld-Animation**: 3D-Münzen/Geldstapel
### 9. **UndergroundView** (Untergrund)
**Sinnvoll:** ⭐⭐⭐⭐
- **3D-Dungeon**: 3D-Untergrund-Visualisierung
- **Aktivitäts-Animationen**: 3D-Animationen für Untergrund-Aktivitäten
### 10. **ReputationView** (Reputation)
**Sinnvoll:** ⭐⭐⭐
- **3D-Party-Szene**: 3D-Visualisierung von Festen
- **Reputation-Visualisierung**: 3D-Effekte für Reputationsänderungen
## Implementierungs-Strategie
### Phase 1: Basis-Setup
1. Three.js installieren
2. Basis-Komponente `ThreeScene.vue` erstellen
3. Erste einfache Animation (z.B. rotierender Würfel) auf OverviewView
### Phase 2: Charakter-Modell
1. 3D-Charakter-Modell erstellen/laden (GLTF/GLB)
2. Auf OverviewView integrieren
3. Interaktionen (Klick, Hover)
### Phase 3: Gebäude-Modelle
1. Haus-Modell für HouseView
2. Fabrik-Modell für BranchView
3. Kirche-Modell für ChurchView
### Phase 4: Animationen
1. Upgrade-Animationen
2. Status-Änderungs-Animationen
3. Interaktive Elemente
## Technische Überlegungen
### Performance
- **Lazy Loading**: 3D-Szenen nur laden, wenn Seite aktiv ist
- **Level of Detail (LOD)**: Einfache Modelle für schwächere Geräte
- **WebGL-Detection**: Fallback auf 2D, wenn WebGL nicht unterstützt wird
### Asset-Management
- **GLTF/GLB**: Kompaktes Format für 3D-Modelle
- **Texturen**: Optimiert für Web (WebP, komprimiert)
- **CDN**: Assets über CDN laden für bessere Performance
### Browser-Kompatibilität
- **WebGL 1.0**: Mindestanforderung (95%+ Browser)
- **WebGL 2.0**: Optional für bessere Features
- **Fallback**: 2D-Versionen für ältere Browser
## Beispiel-Struktur
```
frontend/src/
components/
falukant/
ThreeScene.vue # Basis-3D-Szene-Komponente
CharacterModel.vue # 3D-Charakter-Komponente
BuildingModel.vue # 3D-Gebäude-Komponente
assets/
3d/
models/
character.glb
house.glb
factory.glb
textures/
...
```
## Nächste Schritte
1. **Three.js installieren**
2. **Basis-Komponente erstellen**
3. **Erste Animation auf OverviewView testen**
4. **3D-Modelle erstellen/beschaffen** (Blender, Sketchfab, etc.)
5. **Schrittweise auf weitere Seiten ausweiten**

171
docs/3D_ASSETS_STRUCTURE.md Normal file
View File

@@ -0,0 +1,171 @@
# 3D-Assets Struktur für Falukant
## Verzeichnisstruktur
```
frontend/public/
models/
3d/
falukant/
characters/
male.glb # Basis-Modell männlich
female.glb # Basis-Modell weiblich
male_child.glb # Männlich, Kind (0-9 Jahre)
male_teen.glb # Männlich, Teenager (10-17 Jahre)
male_adult.glb # Männlich, Erwachsen (18-39 Jahre)
male_middle.glb # Männlich, Mittelalter (40-59 Jahre)
male_elder.glb # Männlich, Älter (60+ Jahre)
female_child.glb # Weiblich, Kind
female_teen.glb # Weiblich, Teenager
female_adult.glb # Weiblich, Erwachsen
female_middle.glb # Weiblich, Mittelalter
female_elder.glb # Weiblich, Älter
buildings/
house/
house_small.glb # Kleines Haus
house_medium.glb # Mittleres Haus
house_large.glb # Großes Haus
factory/
factory_basic.glb # Basis-Fabrik
factory_advanced.glb # Erweiterte Fabrik
church/
church.glb # Kirche
bank/
bank.glb # Bank
objects/
weapons/
sword.glb
shield.glb
items/
coin.glb
gift.glb
effects/
particles/
money.glb # Geld-Effekt
health.glb # Gesundheits-Effekt
```
## Namenskonventionen
### Charaktere
- Format: `{gender}[_{ageRange}].glb`
- Beispiele:
- `male.glb` - Basis-Modell männlich (Fallback)
- `female.glb` - Basis-Modell weiblich (Fallback)
- `male_adult.glb` - Männlich, Erwachsen
- `female_teen.glb` - Weiblich, Teenager
### Gebäude
- Format: `{buildingType}_{variant}.glb`
- Beispiele:
- `house_small.glb`
- `factory_basic.glb`
- `church.glb`
### Objekte
- Format: `{category}/{item}.glb`
- Beispiele:
- `weapons/sword.glb`
- `items/coin.glb`
## Altersbereiche
Die Altersbereiche werden automatisch bestimmt:
```javascript
// In CharacterModel3D.vue
getAgeRange(age) {
if (age < 10) return 'child';
if (age < 18) return 'teen';
if (age < 40) return 'adult';
if (age < 60) return 'middle';
return 'elder';
}
```
**Fallback-Verhalten:**
- Wenn kein spezifisches Modell für den Altersbereich existiert, wird das Basis-Modell (`male.glb` / `female.glb`) verwendet
- Dies ermöglicht schrittweise Erweiterung ohne Breaking Changes
## Dateigrößen-Empfehlungen
- **Charaktere**: 100KB - 500KB (komprimiert)
- **Gebäude**: 200KB - 1MB (komprimiert)
- **Objekte**: 10KB - 100KB (komprimiert)
## Optimierung
### Vor dem Hochladen:
1. **Blender** öffnen
2. **Decimate Modifier** anwenden (falls nötig)
3. **Texturen komprimieren** (WebP, max 1024x1024)
4. **GLB Export** mit:
- Compression aktiviert
- Texturen eingebettet
- Unnötige Animationen entfernt
### Komprimierung:
- Verwende `gltf-pipeline` oder `gltf-transform` für weitere Komprimierung
- Ziel: < 500KB pro Modell
## Verwendung im Code
```vue
<!-- CharacterModel3D.vue -->
<CharacterModel3D
:gender="character.gender"
:age="character.age"
/>
<!-- Automatisch wird geladen: -->
<!-- /models/3d/falukant/characters/male_adult.glb -->
<!-- Falls nicht vorhanden: male.glb -->
```
## Erweiterte Struktur (Optional)
Für komplexere Szenarien:
```
frontend/public/
models/
3d/
falukant/
characters/
{gender}/
base/
{gender}.glb # Basis-Modell
ages/
{gender}_{ageRange}.glb
variants/
{gender}_{variant}.glb # Z.B. verschiedene Outfits
```
## Wartung
### Neue Modelle hinzufügen:
1. GLB-Datei in entsprechendes Verzeichnis kopieren
2. Namenskonvention beachten
3. Dateigröße prüfen (< 500KB empfohlen)
4. Im Browser testen
### Modelle aktualisieren:
1. Alte Datei ersetzen
2. Browser-Cache leeren (oder Versionierung verwenden)
3. Testen
### Versionierung (Optional):
```
characters/
v1/
male.glb
v2/
male.glb
```
## Performance-Tipps
1. **Lazy Loading**: Modelle nur laden, wenn benötigt
2. **Preloading**: Wichtige Modelle vorladen
3. **Caching**: Browser-Cache nutzen
4. **CDN**: Für Produktion CDN verwenden

View File

@@ -0,0 +1,159 @@
# 3D-Modell-Erstellung für Falukant
## KI-basierte Tools (Empfohlen)
### 1. **Rodin** ⭐⭐⭐⭐⭐
- **URL**: https://rodin.io/
- **Preis**: Kostenlos (mit Limits), Premium verfügbar
- **Features**:
- Text-zu-3D (z.B. "medieval character", "house")
- Sehr gute Qualität
- Export als GLB/GLTF
- **Gut für**: Charaktere, Gebäude, Objekte
### 2. **Meshy** ⭐⭐⭐⭐⭐
- **URL**: https://www.meshy.ai/
- **Preis**: Kostenlos (mit Limits), ab $9/monat
- **Features**:
- Text-zu-3D
- Bild-zu-3D
- Textur-Generierung
- Export als GLB/OBJ/FBX
- **Gut für**: Alle Arten von Modellen
### 3. **Luma AI Genie** ⭐⭐⭐⭐
- **URL**: https://lumalabs.ai/genie
- **Preis**: Kostenlos (Beta)
- **Features**:
- Text-zu-3D
- Sehr schnell
- Export als GLB
- **Gut für**: Schnelle Prototypen
### 4. **CSM (Common Sense Machines)** ⭐⭐⭐⭐
- **URL**: https://csm.ai/
- **Preis**: Kostenlos (mit Limits)
- **Features**:
- Text-zu-3D
- Bild-zu-3D
- Export als GLB/USD
- **Gut für**: Verschiedene Objekte
### 5. **Tripo AI** ⭐⭐⭐⭐
- **URL**: https://www.tripo3d.ai/
- **Preis**: Kostenlos (mit Limits), Premium verfügbar
- **Features**:
- Text-zu-3D
- Bild-zu-3D
- Export als GLB/FBX/OBJ
- **Gut für**: Charaktere und Objekte
### 6. **Masterpiece Studio** ⭐⭐⭐
- **URL**: https://masterpiecestudio.com/
- **Preis**: Ab $9/monat
- **Features**:
- Text-zu-3D
- VR-Unterstützung
- Export als GLB/FBX
- **Gut für**: Professionelle Modelle
## Traditionelle Tools (Für Nachbearbeitung)
### 1. **Blender** (Kostenlos) ⭐⭐⭐⭐⭐
- **URL**: https://www.blender.org/
- **Features**:
- Vollständige 3D-Suite
- GLB/GLTF Export
- Optimierung von KI-generierten Modellen
- **Gut für**: Nachbearbeitung, Optimierung, Animationen
### 2. **Sketchfab** (Modelle kaufen/laden)
- **URL**: https://sketchfab.com/
- **Preis**: Kostenlos (CC0 Modelle), Premium Modelle kostenpflichtig
- **Features**:
- Millionen von 3D-Modellen
- Viele kostenlose CC0 Modelle
- GLB/GLTF Download
- **Gut für**: Vorgefertigte Modelle, Inspiration
## Empfohlener Workflow
### Für Falukant-Charaktere:
1. **Rodin** oder **Meshy** verwenden
2. Prompt: "medieval character, male/female, simple style, low poly, game ready"
3. Export als GLB
4. In **Blender** optimieren (falls nötig)
5. Texturen anpassen
### Für Gebäude:
1. **Meshy** oder **Tripo AI** verwenden
2. Prompt: "medieval house, simple, low poly, game ready, front view"
3. Export als GLB
4. In **Blender** optimieren
5. Mehrere Varianten erstellen (Haus, Fabrik, Kirche)
### Für Objekte:
1. **Sketchfab** durchsuchen (kostenlose CC0 Modelle)
2. Oder **Meshy** für spezifische Objekte
3. Export als GLB
4. Optimieren falls nötig
## Prompt-Beispiele für Falukant
### Charakter:
```
"medieval character, [male/female], simple low poly style,
game ready, neutral pose, front view, no background,
GLB format, optimized for web"
```
### Haus:
```
"medieval house, simple low poly style, game ready,
front view, no background, GLB format, optimized for web"
```
### Fabrik:
```
"medieval factory building, simple low poly style,
game ready, front view, no background, GLB format"
```
### Wappen:
```
"medieval coat of arms shield, simple low poly style,
game ready, front view, no background, GLB format"
```
## Optimierung für Web
### Nach der Erstellung:
1. **Blender** öffnen
2. **Decimate Modifier** anwenden (weniger Polygone)
3. **Texture** komprimieren (WebP, 512x512 oder 1024x1024)
4. **GLB Export** mit:
- Compression aktiviert
- Texturen eingebettet
- Normals und Tangents berechnet
### Größen-Richtlinien:
- **Charaktere**: 2000-5000 Polygone
- **Gebäude**: 1000-3000 Polygone
- **Objekte**: 100-1000 Polygone
- **Texturen**: 512x512 oder 1024x1024 (nicht größer)
## Kostenlose Alternativen
### Wenn KI-Tools Limits haben:
1. **Sketchfab** durchsuchen (CC0 Modelle)
2. **Poly Haven** (https://polyhaven.com/) - kostenlose Assets
3. **Kenney.nl** - kostenlose Game Assets
4. **OpenGameArt.org** - kostenlose Game Assets
## Nächste Schritte
1. **Rodin** oder **Meshy** testen
2. Ersten Charakter erstellen
3. Als GLB exportieren
4. In Three.js testen
5. Bei Bedarf optimieren

View File

@@ -0,0 +1,334 @@
# Blender Rigging-Anleitung für Falukant-Charaktere
Diese Anleitung erklärt, wie du Bones/Gelenke zu deinen 3D-Modellen in Blender hinzufügst, damit sie animiert werden können.
## Voraussetzungen
- Blender (kostenlos, https://www.blender.org/)
- GLB-Modell von meshy.ai oder anderen Quellen
## Schritt-für-Schritt Anleitung
### 1. Modell in Blender importieren
1. Öffne Blender
2. Gehe zu `File``Import``glTF 2.0 (.glb/.gltf)`
3. Wähle dein Modell aus
4. Das Modell sollte jetzt in der Szene erscheinen
### 2. Modell vorbereiten
1. Stelle sicher, dass das Modell im **Object Mode** ist (Tab drücken, falls im Edit Mode)
2. Wähle das Modell aus (Linksklick)
3. Drücke `Alt + G` um die Position auf (0, 0, 0) zu setzen
4. Drücke `Alt + R` um die Rotation zurückzusetzen
5. Drücke `Alt + S` um die Skalierung auf 1 zu setzen
### 3. Rigging (Bones hinzufügen)
#### Option A: Automatisches Rigging mit Rigify (Empfohlen)
1. **Rigify aktivieren:**
- Gehe zu `Edit``Preferences` (oder `Blender``Preferences` auf Mac)
- Klicke auf den Tab **"Add-ons"** (links im Fenster)
- Im Suchfeld oben rechts tippe: **"rigify"** (ohne Anführungszeichen)
- Du solltest "Rigify: Auto-rigging system" sehen
- Aktiviere das **Häkchen** neben "Rigify"
- Das Add-on ist jetzt aktiviert
- Schließe das Preferences-Fenster
**Alternative Wege zu Preferences:**
- Windows/Linux: `Edit``Preferences`
- Mac: `Blender``Preferences`
- Oder: `Ctrl + ,` (Strg + Komma)
2. **Rigify-Rig hinzufügen:**
- Stelle sicher, dass du im **Object Mode** bist (Tab drücken, falls im Edit Mode)
- Wähle das Modell aus (oder nichts, das Rig wird separat erstellt)
- Drücke `Shift + A` (Add Menu)
- Wähle **`Armature`** aus
- In der Liste siehst du jetzt **`Human (Meta-Rig)`** - klicke darauf
- Ein Basis-Rig wird in der Szene erstellt
**Falls "Human (Meta-Rig)" nicht erscheint:**
- Stelle sicher, dass Rigify aktiviert ist (siehe Schritt 1)
- Starte Blender neu, falls nötig
- Prüfe, ob du die neueste Blender-Version hast (Rigify ist ab Version 2.8+ verfügbar)
3. **Rig positionieren und anpassen:**
**Schritt 1: Rig zum Modell bewegen**
- Stelle sicher, dass du im **Object Mode** bist (Tab drücken)
- Wähle das **Armature** aus (nicht das Modell)
- Drücke `G` (Grab/Move) und bewege das Rig zum Modell
- Oder: Drücke `Alt + G` um die Position zurückzusetzen, dann `G` + `X`, `Y` oder `Z` für eine Achse
**Schritt 2: Rig skalieren (falls zu groß/klein)**
- Wähle das Armature aus
- Drücke `S` (Scale) und skaliere das Rig
- Oder: `S` + `X`, `Y` oder `Z` für eine Achse
- Tipp: Drücke `Shift + X` (oder Y/Z) um diese Achse auszuschließen
**Schritt 3: Einzelne Bones anpassen**
- Wähle das Armature aus
- Wechsle in den **Edit Mode** (Tab)
- Wähle einen Bone aus (Linksklick)
- Drücke `G` um ihn zu bewegen
- Drücke `E` um einen neuen Bone zu extrudieren
- Drücke `R` um einen Bone zu rotieren
- Drücke `S` um einen Bone zu skalieren
**Wichtige Bones zum Anpassen:**
- **Root/Spine** - Sollte in der Mitte des Körpers sein (Hüfthöhe)
- **Spine1/Spine2** - Entlang der Wirbelsäule
- **Neck/Head** - Am Hals und Kopf
- **Shoulders** - An den Schultern
- **Arms** - Entlang der Arme
- **Legs** - Entlang der Beine
**Tipp:** Nutze die Zahlenansicht (Numpad) um die Positionen genau zu sehen
4. **Rig generieren:**
- Wechsle zurück in den **Object Mode** (Tab drücken)
- Wähle das **Meta-Rig (Armature)** aus (nicht das Modell!) - sollte im Outliner blau markiert sein
**Methode 1: Rigify-Button in der Toolbar (Einfachste Methode)**
- Oben in der Toolbar siehst du den Button **"Rigify"** (neben "Object")
- Klicke auf **"Rigify"** → **"Generate Rig"**
- Ein vollständiges Rig wird erstellt (dies kann einen Moment dauern)
**Methode 2: Properties-Panel (Alternative)**
- Im **Properties-Panel** (rechts):
- Klicke auf das **Wrench-Icon** (Modifier Properties) in der linken Toolbar
- Oder: Klicke auf das **Bone-Icon** (Armature Properties)
- Scrolle durch die Tabs, bis du **"Rigify"** oder **"Rigify Generation"** siehst
- In diesem Tab findest du den Button **"Generate Rig"**
- Klicke auf **"Generate Rig"**
**Wichtig:** Nach dem Generieren kannst du das Rig weiter anpassen, aber du musst es im **Pose Mode** tun (nicht Edit Mode)
**Die richtigen Tabs im Properties-Panel (von oben nach unten):**
- 📐 **Object Properties** (Würfel-Icon) - hier findest du Transform, etc.
- 🦴 **Armature Properties** (Bone-Icon) - hier findest du Armature-Einstellungen
- 🔧 **Modifier Properties** (Wrench-Icon) - hier sollte der **Rigify-Tab** sein!
- 🌍 **World Properties** (Globus-Icon) - NICHT hier suchen!
**Falls du den Rigify-Tab nicht siehst:**
- Stelle sicher, dass das **Meta-Rig** (nicht ein bereits generiertes Rig) ausgewählt ist
- Klicke auf das **Wrench-Icon** (Modifier Properties) in der linken Toolbar
- Der Rigify-Tab sollte dort erscheinen
#### Option B: Manuelles Rigging
1. **Armature erstellen:**
- Drücke `Shift + A``Armature`
- Ein Bone wird erstellt
2. **Bones hinzufügen:**
- Wechsle in den **Edit Mode** (Tab)
- Wähle den Root-Bone aus
- Drücke `E` um einen neuen Bone zu extrudieren
- Erstelle die wichtigsten Bones:
- **Spine/Spine1/Spine2** - Wirbelsäule
- **Neck/Head** - Hals und Kopf
- **LeftArm/LeftForeArm/LeftHand** - Linker Arm
- **RightArm/RightForeArm/RightHand** - Rechter Arm
- **LeftUpLeg/LeftLeg/LeftFoot** - Linkes Bein
- **RightUpLeg/RightLeg/RightFoot** - Rechtes Bein
3. **Bone-Namen vergeben:**
- Wähle jeden Bone aus
- Im Properties-Panel (rechts) unter "Bone" kannst du den Namen ändern
- **Wichtig:** Verwende diese Namen für die Animation:
- `LeftArm`, `RightArm`
- `LeftForeArm`, `RightForeArm`
- `LeftHand`, `RightHand`
- `LeftUpLeg`, `RightUpLeg`
- `LeftLeg`, `RightLeg`
- `LeftFoot`, `RightFoot`
- `Neck`, `Head`
- `Spine`, `Spine1`, `Spine2`
### 4. Modell an Bones binden (Skinning)
1. **Beide Objekte auswählen:**
- Wähle zuerst das **Mesh** aus
- Dann wähle das **Armature** aus (Shift + Linksklick)
- Drücke `Ctrl + P``With Automatic Weights`
- Blender berechnet automatisch, welche Vertices zu welchen Bones gehören
2. **Weights überprüfen:**
- Wähle das Mesh aus
- Wechsle in den **Weight Paint Mode** (Dropdown oben)
- Wähle einen Bone aus (rechts im Properties-Panel)
- Rot = vollständig gebunden, Blau = nicht gebunden
- Falls nötig, kannst du die Weights manuell anpassen
### 5. Test-Animation erstellen (Optional)
1. **Pose Mode aktivieren:**
- Wähle das **generierte Rig** aus (nicht das Meta-Rig!)
- Wechsle in den **Pose Mode** (Dropdown oben: "Object Mode" → "Pose Mode")
- Oder: `Ctrl + Tab` → "Pose Mode"
2. **Bone auswählen:**
- **Wichtig:** Arbeite im **3D-Viewport** (Hauptfenster), nicht nur im Outliner!
- **Rigify-Bone-Namen** (nach dem Generieren):
- Für **Knie beugen**: `Leg.L (IK)` oder `Leg.L (FK)` (nicht "Tweak"!)
- Für **Hand anheben**: `Arm.L (IK)` oder `Arm.L (FK)`
- Für **Fuß bewegen**: `Leg.L (IK)` (der Fuß-Controller)
- **IK** = Inverse Kinematics (einfacher, empfohlen für Anfänger)
- **FK** = Forward Kinematics (mehr Kontrolle)
- **Tweak** = Feinabstimmungen (für später, nicht für Hauptanimationen)
- Klicke auf einen **Bone** im **3D-Viewport** (nicht im Outliner!)
- Der Bone sollte orange/ausgewählt sein und im Viewport sichtbar sein
- **Tipp:** Nutze `X-Ray Mode` (Button oben im Viewport) um Bones besser zu sehen
- **Tipp:** Im Outliner kannst du Bones finden, aber die Animation machst du im Viewport
3. **Bone animieren:**
- Wähle z.B. `hand.L` (linke Hand) aus
- Drücke `R` (Rotate) und rotiere den Bone
- Oder: `R` + `Z` (um Z-Achse rotieren)
- Oder: `R` + `X` (um X-Achse rotieren)
- Bewege die Maus → Linksklick zum Bestätigen
- **Beispiel für Hand anheben:** `hand.L``R``Z` → nach oben bewegen
4. **Animation aufnehmen (Timeline):**
- Unten siehst du die **Timeline** (falls nicht sichtbar: `Shift + F12` oder `Window``Animation``Timeline`)
- Stelle den Frame auf **1** (Anfang)
- Wähle den Bone aus und positioniere ihn in der **Ausgangsposition**
- Drücke `I` (Insert Keyframe) → wähle **"Rotation"** (oder "Location" falls bewegt)
- Ein Keyframe wird erstellt (gelber Punkt in der Timeline)
- Stelle den Frame auf **30** (oder einen anderen Frame)
- Rotiere/Bewege den Bone in die **Zielposition** (z.B. Hand nach oben)
- Drücke wieder `I`**"Rotation"** (oder "Location")
- Stelle den Frame auf **60** (Rückkehr zur Ausgangsposition)
- Rotiere den Bone zurück zur Ausgangsposition
- Drücke `I`**"Rotation"**
- Drücke **Play** (Leertaste) um die Animation zu sehen
5. **Animation testen:**
- Die Animation sollte jetzt in einer Schleife abgespielt werden
- Du kannst weitere Keyframes hinzufügen (Frame 90, 120, etc.)
- **Tipp:** Nutze `Alt + A` um die Animation zu stoppen
### 6. Modell exportieren
1. **Beide Objekte auswählen:**
- Wähle das **Mesh** aus
- Shift + Linksklick auf das **generierte Rig** (nicht das Meta-Rig!)
2. **Exportieren:**
- Gehe zu `File``Export``glTF 2.0 (.glb/.gltf)`
- Wähle `.glb` Format
- Stelle sicher, dass folgende Optionen aktiviert sind:
-**Include****Selected Objects**
-**Transform****+Y Up**
-**Geometry****Apply Modifiers**
-**Animation****Bake Animation** (wichtig für Animationen!)
-**Animation****Always Sample Animations** (falls Animationen nicht korrekt exportiert werden)
- Klicke auf "Export glTF 2.0"
### 7. Modell testen
1. Kopiere die exportierte `.glb` Datei nach:
```
frontend/public/models/3d/falukant/characters/
```
2. Lade die Seite neu
3. Die Bones sollten jetzt automatisch erkannt und animiert werden
4. **Animationen testen:**
- Öffne die Browser-Konsole (F12)
- Du solltest sehen: `[ThreeScene] Found X animation(s)`
- Die Animationen sollten automatisch abgespielt werden
- Falls keine Animationen vorhanden sind, werden die Bones trotzdem mit Idle-Animationen bewegt
## Rig anpassen - Detaillierte Anleitung
### Rig nach dem Generieren anpassen
Wenn das Rigify-Rig generiert wurde, aber nicht perfekt passt:
1. **Pose Mode verwenden:**
- Wähle das generierte Armature aus
- Wechsle in den **Pose Mode** (Dropdown oben, oder Strg+Tab → Pose Mode)
- Hier kannst du die Bones bewegen, ohne die Struktur zu zerstören
2. **Rig neu generieren (falls nötig):**
- Falls das Rig komplett neu positioniert werden muss:
- Lösche das generierte Rig (X → Delete)
- Gehe zurück zum Meta-Rig
- Passe das Meta-Rig im Edit Mode an
- Generiere das Rig erneut
3. **Snap to Mesh (Hilfsmittel):**
- Im Edit Mode: `Shift + Tab` um Snap zu aktivieren
- Oder: Rechtsklick auf das Snap-Symbol (Magnet) oben
- Wähle "Face" oder "Vertex" als Snap-Target
- Jetzt werden Bones automatisch am Mesh ausgerichtet
### Häufige Probleme und Lösungen
**Problem: Rig ist zu groß/klein**
- Lösung: Im Object Mode das Armature auswählen und mit `S` skalieren
**Problem: Rig ist an falscher Position**
- Lösung: Im Object Mode mit `G` bewegen, oder `Alt + G` zurücksetzen
**Problem: Einzelne Bones passen nicht**
- Lösung: Im Edit Mode die Bones einzeln anpassen (`G` zum Bewegen)
**Problem: Nach dem Generieren passt es nicht mehr**
- Lösung: Passe das Meta-Rig an und generiere neu, oder verwende Pose Mode
## Tipps und Tricks
### Bone-Namen für automatische Erkennung
Die Komponente erkennt Bones anhand ihrer Namen. Verwende diese Keywords:
- `arm` - für Arme
- `hand` oder `wrist` - für Hände
- `leg` oder `knee` - für Beine
- `foot` oder `ankle` - für Füße
- `shoulder` - für Schultern
- `elbow` - für Ellbogen
### Einfacheres Rigging mit Mixamo
Alternativ kannst du:
1. Dein Modell auf [Mixamo](https://www.mixamo.com/) hochladen
2. Automatisches Rigging durchführen lassen
3. Das geriggte Modell herunterladen
4. In Blender importieren und anpassen
### Performance-Optimierung
- Verwende nicht zu viele Bones (max. 50-100 für Charaktere)
- Entferne unnötige Bones vor dem Export
- Teste die Animation im Browser, bevor du das finale Modell exportierst
## Troubleshooting
### Bones werden nicht erkannt
- Prüfe die Bone-Namen (müssen `arm`, `hand`, `leg`, etc. enthalten)
- Stelle sicher, dass das Modell korrekt an die Bones gebunden ist
- Öffne die Browser-Konsole und prüfe die Logs: `[ThreeScene] Found X bones for animation`
### Modell verformt sich falsch
- Überprüfe die Weights im Weight Paint Mode
- Passe die Bone-Positionen an
- Stelle sicher, dass alle Vertices korrekt zugewiesen sind
### Export schlägt fehl
- Stelle sicher, dass beide Objekte (Mesh + Armature) ausgewählt sind
- Prüfe, ob das Modell im Object Mode ist
- Versuche es mit einem anderen Export-Format (.gltf statt .glb)
## Weitere Ressourcen
- [Blender Rigging Tutorial](https://www.youtube.com/results?search_query=blender+rigging+tutorial)
- [Mixamo Auto-Rigging](https://www.mixamo.com/)
- [Three.js GLTF Animation Guide](https://threejs.org/docs/#manual/en/introduction/Animation-system)

View File

@@ -21,6 +21,7 @@
"dotenv": "^16.4.5",
"mitt": "^3.0.1",
"socket.io-client": "^4.8.1",
"three": "^0.182.0",
"vue": "~3.4.31",
"vue-i18n": "^10.0.0-beta.2",
"vue-multiselect": "^3.1.0",
@@ -2834,6 +2835,12 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/three": {
"version": "0.182.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",

View File

@@ -21,6 +21,7 @@
"dotenv": "^16.4.5",
"mitt": "^3.0.1",
"socket.io-client": "^4.8.1",
"three": "^0.182.0",
"vue": "~3.4.31",
"vue-i18n": "^10.0.0-beta.2",
"vue-multiselect": "^3.1.0",

View File

@@ -0,0 +1,40 @@
# 3D-Charakter-Modelle
## Verzeichnisstruktur
Dieses Verzeichnis enthält die 3D-Modelle für Falukant-Charaktere.
## Dateinamen-Konvention
### Basis-Modelle (Fallback)
- `male.glb` - Basis-Modell männlich
- `female.glb` - Basis-Modell weiblich
### Altersspezifische Modelle
- `male_toddler.glb` - Männlich, Kleinkind (0-3 Jahre)
- `male_child.glb` - Männlich, Kind (4-7 Jahre)
- `male_preteen.glb` - Männlich, Vor-Teenager (8-12 Jahre)
- `male_teen.glb` - Männlich, Teenager (13-17 Jahre)
- `male_adult.glb` - Männlich, Erwachsen (18+ Jahre)
- `female_toddler.glb` - Weiblich, Kleinkind (0-3 Jahre)
- `female_child.glb` - Weiblich, Kind (4-7 Jahre)
- `female_preteen.glb` - Weiblich, Vor-Teenager (8-12 Jahre)
- `female_teen.glb` - Weiblich, Teenager (13-17 Jahre)
- `female_adult.glb` - Weiblich, Erwachsen (18+ Jahre)
## Fallback-Verhalten
Wenn kein spezifisches Modell für den Altersbereich existiert, wird automatisch das Basis-Modell (`male.glb` / `female.glb`) verwendet.
## Dateigröße
- Empfohlen: < 500KB pro Modell
- Maximal: 1MB pro Modell
## Optimierung
Vor dem Hochladen:
1. In Blender öffnen
2. Decimate Modifier anwenden (falls nötig)
3. Texturen komprimieren (WebP, max 1024x1024)
4. GLB Export mit Compression aktiviert

Binary file not shown.

View File

@@ -0,0 +1,225 @@
<template>
<div class="character-model-3d">
<ThreeScene
v-if="currentModelPath"
:key="currentModelPath"
:modelPath="currentModelPath"
:autoRotate="autoRotate"
:rotationSpeed="rotationSpeed"
:cameraPosition="cameraPosition"
:backgroundColor="backgroundColor"
@model-loaded="onModelLoaded"
@model-error="onModelError"
@loading-progress="onLoadingProgress"
/>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<p v-if="loadingProgress > 0">{{ Math.round(loadingProgress) }}%</p>
</div>
<div v-if="error" class="error-overlay">
<p>{{ error }}</p>
</div>
</div>
</template>
<script>
import ThreeScene from './ThreeScene.vue';
export default {
name: 'CharacterModel3D',
components: {
ThreeScene
},
props: {
gender: {
type: String,
required: true,
validator: (value) => ['male', 'female'].includes(value)
},
age: {
type: Number,
default: null
},
autoRotate: {
type: Boolean,
default: false
},
rotationSpeed: {
type: Number,
default: 0.5
},
cameraPosition: {
type: Object,
default: () => ({ x: 0, y: 1, z: 3 })
},
backgroundColor: {
type: String,
default: '#f0f0f0'
}
},
data() {
return {
loading: true,
loadingProgress: 0,
error: null,
currentModelPath: null
};
},
computed: {
baseModelPath() {
const basePath = '/models/3d/falukant/characters';
return `${basePath}/${this.gender}.glb`;
},
ageSpecificModelPath() {
const ageRange = this.getAgeRange(this.age);
if (!ageRange) return null;
const basePath = '/models/3d/falukant/characters';
return `${basePath}/${this.gender}_${ageRange}.glb`;
}
},
watch: {
gender() {
this.findAndLoadModel();
},
age() {
this.findAndLoadModel();
}
},
mounted() {
this.findAndLoadModel();
},
methods: {
getAgeRange(age) {
if (age === null || age === undefined) return null;
// Verfügbare Altersbereiche: toddler, child, preteen, teen, adult
// Alter ist in Tagen gespeichert (1 Tag = 1 Jahr)
if (age < 4) return 'toddler'; // 0-3 Jahre
if (age < 10) return 'child'; // 4-7 Jahre
if (age < 13) return 'preteen'; // 8-12 Jahre
if (age < 18) return 'teen'; // 13-17 Jahre
return 'adult'; // 18+ Jahre
},
async findAndLoadModel() {
this.loading = true;
this.error = null;
// Versuche zuerst altersspezifisches Modell, dann Basis-Modell
const pathsToTry = [];
if (this.ageSpecificModelPath) {
pathsToTry.push(this.ageSpecificModelPath);
}
pathsToTry.push(this.baseModelPath);
// Prüfe welche Datei existiert
for (const path of pathsToTry) {
const exists = await this.checkFileExists(path);
if (exists) {
this.currentModelPath = path;
console.log(`[CharacterModel3D] Using model: ${path}`);
return;
}
}
// Fallback: Verwende Basis-Modell auch wenn Prüfung fehlschlägt
this.currentModelPath = this.baseModelPath;
console.warn(`[CharacterModel3D] Using fallback model: ${this.baseModelPath}`);
},
async checkFileExists(path) {
try {
const response = await fetch(path, { method: 'HEAD' });
if (!response.ok) {
return false;
}
// Prüfe Content-Type - sollte nicht HTML sein
const contentType = response.headers.get('content-type') || '';
const isHTML = contentType.includes('text/html') || contentType.includes('text/plain');
if (isHTML) {
console.warn(`[CharacterModel3D] File ${path} returns HTML, probably doesn't exist`);
return false;
}
// GLB-Dateien können verschiedene Content-Types haben
return true;
} catch (error) {
console.warn(`[CharacterModel3D] Error checking file ${path}:`, error);
return false;
}
},
onModelLoaded(model) {
this.loading = false;
this.error = null;
this.$emit('model-loaded', model);
},
onModelError(error) {
// Wenn ein Fehler auftritt und wir noch nicht das Basis-Modell verwenden
if (this.currentModelPath !== this.baseModelPath) {
console.warn('[CharacterModel3D] Model failed, trying fallback...');
this.currentModelPath = this.baseModelPath;
// Der Watch-Handler wird das Modell neu laden
return;
}
this.loading = false;
this.error = 'Fehler beim Laden des 3D-Modells';
console.error('Character model error:', error);
this.$emit('model-error', error);
},
onLoadingProgress(progress) {
this.loadingProgress = progress;
}
}
};
</script>
<style scoped>
.character-model-3d {
width: 100%;
height: 100%;
position: relative;
min-height: 400px;
}
.loading-overlay,
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
z-index: 10;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #F9A22C;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-overlay p {
color: #d32f2f;
font-weight: bold;
}
</style>

View File

@@ -151,19 +151,6 @@ export default {
// Extrahiere Parameter aus value und effects
params = this.extractParamsFromValue(value, n);
// Wenn value eine einfache Zahl ist (z.B. für overproduction), als value-Parameter verwenden
if (typeof parsed.value === 'number') {
params.value = parsed.value;
}
// Weitere Parameter aus parsed extrahieren (z.B. branch_id)
if (parsed.branch_id !== undefined) {
params.branch_id = parsed.branch_id;
}
if (parsed.region_id !== undefined) {
params.region_id = parsed.region_id;
}
}
} catch (e) {
// Bei Parse-Fehler: Alte Struktur unterstützen
@@ -185,12 +172,9 @@ export default {
if (value && value.title && value.description) {
// Parameter aus effects extrahieren und formatieren
const formattedParams = this.formatParams(params);
// Zuerst Description interpolieren (für {amount} etc.), dann Effects hinzufügen
let description = this.interpolateString(value.description, formattedParams);
description = this.formatDescriptionWithEffects(description, value.effects || [], formattedParams);
return {
title: this.interpolateString(value.title, formattedParams),
description: description
description: this.formatDescriptionWithEffects(value.description, value.effects || [], formattedParams)
};
}
@@ -228,10 +212,6 @@ export default {
const title = this.$t(titleKey, formattedParams);
let description = this.$t(descKey, formattedParams);
// Stelle sicher, dass auch hier die Parameter interpoliert werden (für {amount} etc.)
// Vue i18n interpoliert bereits, aber wir müssen sicherstellen, dass formatParams korrekt formatiert
description = this.interpolateString(description, formattedParams);
// Füge Effect-Details hinzu, falls vorhanden
if (value && value.effects) {
description = this.formatDescriptionWithEffects(description, value.effects, formattedParams);
@@ -254,27 +234,15 @@ export default {
// Geldbeträge formatieren
if (params.amount !== undefined && params.amount !== null) {
formatted.amount = this.formatMoney(Number(params.amount));
formatted.amount = this.formatMoney(params.amount);
}
if (params.absolute !== undefined && params.absolute !== null) {
formatted.amount = this.formatMoney(Number(params.absolute));
formatted.amount = this.formatMoney(params.absolute);
}
if (params.percent !== undefined && params.percent !== null) {
formatted.percent = `${params.percent > 0 ? '+' : ''}${params.percent.toFixed(1)}%`;
}
// Einfache Werte (z.B. für overproduction)
if (params.value !== undefined && params.value !== null) {
formatted.value = Number(params.value);
}
// Filiale-Information
if (params.branch_id !== undefined && params.branch_id !== null) {
formatted.branch_info = ` (Filiale #${params.branch_id})`;
} else {
formatted.branch_info = '';
}
// Gesundheit formatieren
if (params.change !== undefined && params.change !== null) {
formatted.healthChange = params.change > 0 ? `+${params.change}` : `${params.change}`;

View File

@@ -0,0 +1,441 @@
<template>
<div ref="container" class="three-scene-container"></div>
</template>
<script>
import { markRaw } from 'vue';
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
export default {
name: 'ThreeScene',
props: {
modelPath: {
type: String,
required: true
},
autoRotate: {
type: Boolean,
default: true
},
rotationSpeed: {
type: Number,
default: 0.5
},
cameraPosition: {
type: Object,
default: () => ({ x: 0, y: 1, z: 3 })
},
backgroundColor: {
type: String,
default: '#f0f0f0'
}
},
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
model: null,
animationId: null,
mixer: null,
clock: null,
animationStartTime: 0,
baseY: 0, // Basis-Y-Position für Bewegungsanimation
bones: [] // Gespeicherte Bones für manuelle Animation
};
},
mounted() {
this.initScene();
this.loadModel();
this.animate();
window.addEventListener('resize', this.onWindowResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.onWindowResize);
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
if (this.mixer) {
this.mixer.stopAllAction();
}
if (this.renderer) {
this.renderer.dispose();
}
if (this.model) {
this.disposeModel(this.model);
}
},
watch: {
modelPath() {
if (this.model) {
this.disposeModel(this.model);
this.model = null;
}
this.loadModel();
},
autoRotate(newVal) {
if (this.controls) {
this.controls.autoRotate = newVal;
}
}
},
methods: {
initScene() {
// Szene erstellen - markRaw verhindert Vue-Reaktivität
this.scene = markRaw(new THREE.Scene());
this.scene.background = new THREE.Color(this.backgroundColor);
// Kamera erstellen - markRaw verhindert Vue-Reaktivität
this.camera = markRaw(new THREE.PerspectiveCamera(
50,
this.$refs.container.clientWidth / this.$refs.container.clientHeight,
0.1,
1000
));
this.camera.position.set(
this.cameraPosition.x,
this.cameraPosition.y,
this.cameraPosition.z
);
// Renderer erstellen - markRaw verhindert Vue-Reaktivität
this.renderer = markRaw(new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: 'high-performance'
}));
this.renderer.setSize(
this.$refs.container.clientWidth,
this.$refs.container.clientHeight
);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Begrenzt für Performance
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.2; // Leicht erhöhte Helligkeit
this.$refs.container.appendChild(this.renderer.domElement);
// Controls erstellen - markRaw verhindert Vue-Reaktivität
this.controls = markRaw(new OrbitControls(this.camera, this.renderer.domElement));
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.autoRotate = false; // Rotation deaktiviert
this.controls.enableRotate = false; // Manuelle Rotation deaktiviert
this.controls.enableZoom = true;
this.controls.enablePan = false;
this.controls.minDistance = 2;
this.controls.maxDistance = 5;
// Clock für Animationen
this.clock = markRaw(new THREE.Clock());
// Verbesserte Beleuchtung
// Umgebungslicht - heller für bessere Sichtbarkeit
const ambientLight = markRaw(new THREE.AmbientLight(0xffffff, 1.0));
this.scene.add(ambientLight);
// Hauptlicht von vorne oben (Key Light)
const mainLight = markRaw(new THREE.DirectionalLight(0xffffff, 1.2));
mainLight.position.set(3, 8, 4);
mainLight.castShadow = true;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
mainLight.shadow.camera.near = 0.5;
mainLight.shadow.camera.far = 50;
this.scene.add(mainLight);
// Fülllicht von links (Fill Light)
const fillLight = markRaw(new THREE.DirectionalLight(0xffffff, 0.6));
fillLight.position.set(-4, 5, 3);
this.scene.add(fillLight);
// Zusätzliches Licht von rechts (Rim Light)
const rimLight = markRaw(new THREE.DirectionalLight(0xffffff, 0.5));
rimLight.position.set(4, 3, -3);
this.scene.add(rimLight);
// Punktlicht von oben für zusätzliche Helligkeit
const pointLight = markRaw(new THREE.PointLight(0xffffff, 0.8, 20));
pointLight.position.set(0, 6, 0);
this.scene.add(pointLight);
},
loadModel() {
const loader = new GLTFLoader();
// Optional: DRACO-Loader für komprimierte Modelle
// const dracoLoader = new DRACOLoader();
// dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
// loader.setDRACOLoader(dracoLoader);
console.log('[ThreeScene] Loading model from:', this.modelPath);
console.log('[ThreeScene] Full URL:', window.location.origin + this.modelPath);
loader.load(
this.modelPath,
(gltf) => {
console.log('[ThreeScene] Model loaded successfully:', gltf);
// Altes Modell entfernen
if (this.model) {
this.scene.remove(this.model);
this.disposeModel(this.model);
}
// Modell als nicht-reaktiv markieren - verhindert Vue-Proxy-Konflikte
this.model = markRaw(gltf.scene);
// Modell zentrieren und skalieren
const box = new THREE.Box3().setFromObject(this.model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
console.log('[ThreeScene] Model bounds:', { center, size });
// Modell zentrieren (X und Z)
this.model.position.x = -center.x;
this.model.position.z = -center.z;
// Modell skalieren (größer für bessere Sichtbarkeit)
const maxSize = Math.max(size.x, size.y, size.z);
const scale = maxSize > 0 ? 3.0 / maxSize : 1;
this.model.scale.multiplyScalar(scale);
// Modell auf Boden setzen und Basis-Y-Position speichern
this.baseY = -size.y * scale / 2;
this.model.position.y = this.baseY;
// Schatten aktivieren
this.model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
this.scene.add(this.model);
// Kamera auf Modell ausrichten
this.centerCameraOnModel();
// Bones für manuelle Animation finden
this.findAndStoreBones(this.model);
// Falls keine Bones gefunden, Hinweis in der Konsole
if (this.bones.length === 0) {
console.warn('[ThreeScene] No bones found in model. To enable limb animations, add bones in Blender. See docs/BLENDER_RIGGING_GUIDE.md');
}
// Animationen aus GLTF laden (falls vorhanden)
if (gltf.animations && gltf.animations.length > 0) {
console.log(`[ThreeScene] Found ${gltf.animations.length} animation(s):`, gltf.animations.map(a => a.name));
this.mixer = markRaw(new THREE.AnimationMixer(this.model));
gltf.animations.forEach((clip) => {
const action = this.mixer.clipAction(clip);
action.play();
console.log(`[ThreeScene] Playing animation: "${clip.name}" (duration: ${clip.duration.toFixed(2)}s)`);
});
} else {
console.log('[ThreeScene] No animations found in model');
}
this.animationStartTime = this.clock.getElapsedTime();
this.$emit('model-loaded', this.model);
},
(progress) => {
// Loading-Progress
if (progress.lengthComputable) {
const percent = (progress.loaded / progress.total) * 100;
this.$emit('loading-progress', percent);
} else {
// Fallback für nicht-computable progress
this.$emit('loading-progress', 50);
}
},
(error) => {
console.error('[ThreeScene] Error loading model:', error);
console.error('[ThreeScene] Model path was:', this.modelPath);
console.error('[ThreeScene] Full URL:', window.location.origin + this.modelPath);
console.error('[ThreeScene] Error details:', {
message: error?.message,
stack: error?.stack,
type: error?.constructor?.name
});
// Prüfe ob es ein 404-Fehler ist (JSON-Parse-Fehler deutet auf HTML-Fehlerseite hin)
if (error?.message && (error.message.includes('JSON') || error.message.includes('Unexpected'))) {
console.error('[ThreeScene] Possible 404 error - file not found or wrong path');
console.error('[ThreeScene] Please check:');
console.error(' 1. File exists at:', this.modelPath);
console.error(' 2. Vite dev server is running');
console.error(' 3. File is in public/ directory');
// Versuche die Datei direkt zu fetchen um den Fehler zu sehen
fetch(this.modelPath)
.then(response => {
console.error('[ThreeScene] Fetch response:', {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries())
});
return response.text();
})
.then(text => {
console.error('[ThreeScene] Response preview:', text.substring(0, 200));
})
.catch(fetchError => {
console.error('[ThreeScene] Fetch error:', fetchError);
});
}
this.$emit('model-error', error);
}
);
},
disposeModel(model) {
model.traverse((child) => {
if (child.isMesh) {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach((mat) => mat.dispose());
} else {
child.material.dispose();
}
}
}
});
},
findAndStoreBones(object) {
this.bones = [];
object.traverse((child) => {
if (child.isBone || (child.type === 'Bone')) {
// Speichere Bones mit ihren Namen für einfachen Zugriff
const boneName = child.name.toLowerCase();
// Typische Bone-Namen für Gliedmaßen
if (boneName.includes('arm') ||
boneName.includes('hand') ||
boneName.includes('leg') ||
boneName.includes('foot') ||
boneName.includes('shoulder') ||
boneName.includes('elbow') ||
boneName.includes('knee') ||
boneName.includes('wrist') ||
boneName.includes('ankle')) {
this.bones.push({
bone: child,
name: boneName,
originalRotation: child.rotation.clone()
});
}
}
});
console.log(`[ThreeScene] Found ${this.bones.length} bones for animation`);
},
animateLimbs(time) {
// Sanfte Idle-Animation für Gliedmaßen
const animationSpeed = 1.5; // Geschwindigkeit
const maxRotation = 0.15; // Maximale Rotation in Radianten (ca. 8.6 Grad)
this.bones.forEach((boneData, index) => {
const bone = boneData.bone;
const boneName = boneData.name;
// Unterschiedliche Animationen basierend auf Bone-Typ
if (boneName.includes('arm') || boneName.includes('shoulder')) {
// Arme: Sanftes Vor- und Zurückschwingen
const phase = time * animationSpeed + (index * 0.5);
bone.rotation.x = boneData.originalRotation.x + Math.sin(phase) * maxRotation * 0.3;
bone.rotation.z = boneData.originalRotation.z + Math.cos(phase * 0.7) * maxRotation * 0.2;
} else if (boneName.includes('hand') || boneName.includes('wrist')) {
// Hände: Leichtes Wackeln
const phase = time * animationSpeed * 1.5 + (index * 0.3);
bone.rotation.y = boneData.originalRotation.y + Math.sin(phase) * maxRotation * 0.4;
} else if (boneName.includes('leg') || boneName.includes('knee')) {
// Beine: Leichtes Vor- und Zurückbewegen
const phase = time * animationSpeed * 0.8 + (index * 0.4);
bone.rotation.x = boneData.originalRotation.x + Math.sin(phase) * maxRotation * 0.2;
} else if (boneName.includes('foot') || boneName.includes('ankle')) {
// Füße: Minimales Wackeln
const phase = time * animationSpeed * 1.2 + (index * 0.2);
bone.rotation.x = boneData.originalRotation.x + Math.sin(phase) * maxRotation * 0.15;
}
});
},
centerCameraOnModel() {
if (!this.model || !this.camera) return;
// Kamera-Position für gute Ansicht des zentrierten Modells
this.camera.position.set(0, this.baseY + 1, 3);
this.camera.lookAt(0, this.baseY + 0.5, 0);
if (this.controls) {
this.controls.target.set(0, this.baseY + 0.5, 0);
this.controls.update();
}
},
animate() {
this.animationId = requestAnimationFrame(this.animate);
const delta = this.clock ? this.clock.getDelta() : 0;
// GLTF-Animationen aktualisieren (falls vorhanden)
if (this.mixer) {
this.mixer.update(delta);
}
// Gliedmaßen-Animationen
if (this.bones.length > 0) {
const time = this.clock ? this.clock.getElapsedTime() : 0;
this.animateLimbs(time);
}
if (this.controls) {
this.controls.update();
}
if (this.renderer && this.scene && this.camera) {
this.renderer.render(this.scene, this.camera);
}
},
onWindowResize() {
if (!this.$refs.container || !this.camera || !this.renderer) return;
const width = this.$refs.container.clientWidth;
const height = this.$refs.container.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
}
}
};
</script>
<style scoped>
.three-scene-container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.three-scene-container canvas {
display: block;
width: 100%;
height: 100%;
}
</style>

View File

@@ -33,7 +33,7 @@
"notifications": {
"notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.",
"production": {
"overproduction": "Überproduktion: Deine Produktion liegt {value} Einheiten über dem Bedarf{branch_info}."
"overproduction": "Überproduktion: Deine Produktion liegt {value}% über dem Bedarf."
},
"transport": {
"waiting": "Transport wartet"

View File

@@ -19,7 +19,7 @@
"notifications": {
"notify_election_created": "A new election has been scheduled.",
"production": {
"overproduction": "Overproduction: your production is {value} units above demand{branch_info}."
"overproduction": "Overproduction: your production is {value}% above demand."
},
"transport": {
"waiting": "Transport waiting"

View File

@@ -188,29 +188,18 @@ const store = createStore({
socketIoUrl = 'http://localhost:3001';
}
// Direkte Verbindung zu Socket.io (ohne Apache-Proxy)
// In Produktion: direkte Verbindung zu Port 4443 (verschlüsselt)
const hostname = window.location.hostname;
const isProduction = hostname === 'www.your-part.de' || hostname.includes('your-part.de');
if (isProduction) {
// Produktion: direkte Verbindung zu Port 4443 (verschlüsselt)
const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
socketIoUrl = `${protocol}//${hostname}:4443`;
} else {
// Lokale Entwicklung: direkte Backend-Verbindung
if (!socketIoUrl && (import.meta.env.DEV || hostname === 'localhost' || hostname === '127.0.0.1')) {
socketIoUrl = 'http://localhost:3001';
} else if (socketIoUrl) {
try {
const parsed = new URL(socketIoUrl, window.location.origin);
socketIoUrl = parsed.origin;
} catch (e) {
socketIoUrl = window.location.origin;
}
} else {
socketIoUrl = window.location.origin;
// Normalisiere URL (Env-Variablen enthalten teils Pfade wie /api; Port kann absichtlich gesetzt sein, z.B. :4443)
try {
if (socketIoUrl) {
const parsed = new URL(socketIoUrl, window.location.origin);
// Falls /api oder ähnliche Pfade enthalten sind → auf Origin reduzieren (inkl. Port!)
socketIoUrl = parsed.origin;
}
} catch (e) {
// Wenn Parsing fehlschlägt: letzte Rettung ist der aktuelle Origin
try {
socketIoUrl = window.location.origin;
} catch (_) {}
}
const socket = io(socketIoUrl, {
@@ -295,13 +284,12 @@ const store = createStore({
// Wenn Umgebungsvariable nicht gesetzt ist oder leer, verwende Fallback-Logik
if (!daemonUrl || (typeof daemonUrl === 'string' && daemonUrl.trim() === '')) {
// Immer direkte Verbindung zum Daemon-Port 4551 (verschlüsselt)
// Fallback: direkte Verbindung zum Daemon-Port 4551 (ohne Apache-Proxy)
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
daemonUrl = `${protocol}//${hostname}:4551/`;
console.log('[Daemon] Verwende direkte Verbindung zu Port 4551');
console.log('[Daemon] Verwende Fallback basierend auf Hostname, Protokoll und Port 4551');
} else {
// Wenn Umgebungsvariable gesetzt ist, verwende sie direkt
console.log('[Daemon] Verwende Umgebungsvariable:', daemonUrl);
console.log('[Daemon] Verwende Umgebungsvariable');
}
console.log('[Daemon] Finale Daemon-URL:', daemonUrl);

View File

@@ -468,10 +468,14 @@ export default {
},
watch: {
activeTab(newVal) {
if (newVal === 'taxes') {
this.loadBranchTaxes();
}
activeTab(newVal, oldVal) {
// Nur neu laden, wenn der Tab wirklich gewechselt wurde und ein Branch ausgewählt ist
if (!this.selectedBranch || newVal === oldVal) return;
// Alle Tabs neu laden, wenn gewechselt wird
this.$nextTick(() => {
this.refreshActiveTab();
});
},
selectedBranch: {
handler(newBranch) {
@@ -537,6 +541,33 @@ export default {
}
},
refreshActiveTab() {
// Lade die Daten für den aktiven Tab neu
switch (this.activeTab) {
case 'director':
this.$refs.directorInfo?.refresh();
break;
case 'inventory':
this.$refs.saleSection?.loadInventory();
this.$refs.saleSection?.loadTransports();
break;
case 'production':
this.$refs.productionSection?.loadProductions();
this.$refs.productionSection?.loadStorage();
this.$refs.revenueSection?.refresh && this.$refs.revenueSection.refresh();
break;
case 'taxes':
this.loadBranchTaxes();
break;
case 'storage':
this.$refs.storageSection?.loadStorageData();
break;
case 'transport':
this.loadVehicles();
break;
}
},
async onBranchSelected(newBranch) {
this.selectedBranch = newBranch;
// Branches neu laden, um das Wetter zu aktualisieren
@@ -549,13 +580,8 @@ export default {
await this.loadVehicles();
await this.loadProductPricesForCurrentBranch();
this.$nextTick(() => {
this.$refs.directorInfo?.refresh();
this.$refs.saleSection?.loadInventory();
this.$refs.saleSection?.loadTransports();
this.$refs.productionSection?.loadProductions();
this.$refs.productionSection?.loadStorage();
this.$refs.storageSection?.loadStorageData();
this.$refs.revenueSection?.refresh && this.$refs.revenueSection.refresh();
// Alle Tabs neu laden
this.refreshActiveTab();
});
// load tax info for this branch
@@ -572,49 +598,27 @@ export default {
return;
}
if (!this.products || this.products.length === 0) {
this.productPricesCache = {};
return;
}
// OPTIMIERUNG: Lade alle Preise in einem Batch-Request
try {
const productIds = this.products.map(p => p.id).join(',');
const { data } = await apiClient.get('/api/falukant/products/prices-in-region-batch', {
params: {
productIds: productIds,
regionId: this.selectedBranch.regionId
}
});
this.productPricesCache = data || {};
} catch (error) {
console.error('Error loading prices in batch:', error);
// Fallback: Lade Preise einzeln (aber parallel)
const pricePromises = this.products.map(async (product) => {
try {
const { data } = await apiClient.get('/api/falukant/products/price-in-region', {
params: {
productId: product.id,
regionId: this.selectedBranch.regionId
}
});
return { productId: product.id, price: data.price };
} catch (err) {
console.error(`Error loading price for product ${product.id}:`, err);
// Fallback auf Standard-Berechnung
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
return { productId: product.id, price: minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100) };
}
});
const results = await Promise.all(pricePromises);
this.productPricesCache = {};
results.forEach(({ productId, price }) => {
this.productPricesCache[productId] = price;
});
// Lade Preise für alle Produkte in der aktuellen Region
const prices = {};
for (const product of this.products) {
try {
const { data } = await apiClient.get('/api/falukant/products/price-in-region', {
params: {
productId: product.id,
regionId: this.selectedBranch.regionId
}
});
prices[product.id] = data.price;
} catch (error) {
console.error(`Error loading price for product ${product.id}:`, error);
// Fallback auf Standard-Berechnung
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
prices[product.id] = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
}
}
this.productPricesCache = prices;
},
formatPercent(value) {

View File

@@ -274,16 +274,12 @@ export default {
getEffect(gift) {
const relationship = this.relationships[0];
if (!relationship || !relationship.character2) {
return 0;
}
const partner = relationship.character2;
const currentMoodId = partner.moodId;
const currentMoodId = partner.mood?.id ?? partner.mood_id;
const moodEntry = gift.moodsAffects.find(ma => ma.mood_id === currentMoodId);
const moodValue = moodEntry ? moodEntry.suitability : 0;
let highestCharacterValue = 0;
// traits ist ein Array von Trait-Objekten mit id und tr
for (const trait of partner.traits || []) {
for (const trait of partner.characterTrait) {
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
if (charEntry && charEntry.suitability > highestCharacterValue) {
highestCharacterValue = charEntry.suitability;

View File

@@ -116,15 +116,26 @@
</table>
</div>
</div>
<div v-if="falukantUser?.character" class="imagecontainer">
<div :style="getAvatarStyle" class="avatar"></div>
<div :style="getHouseStyle" class="house"></div>
<div v-if="falukantUser?.character" class="overview-visualization">
<div class="character-3d-container">
<CharacterModel3D
:gender="falukantUser.character.gender"
:age="falukantUser.character.age"
:autoRotate="true"
:rotationSpeed="0.5"
/>
</div>
<div class="imagecontainer">
<div :style="getAvatarStyle" class="avatar"></div>
<div :style="getHouseStyle" class="house"></div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import CharacterModel3D from '@/components/falukant/CharacterModel3D.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
@@ -169,6 +180,7 @@ export default {
name: 'FalukantOverviewView',
components: {
StatusBar,
CharacterModel3D,
},
data() {
return {
@@ -481,4 +493,27 @@ h2 {
padding: 20px;
color: #666;
}
.overview-visualization {
display: flex;
gap: 20px;
margin-top: 20px;
flex-wrap: wrap;
}
.character-3d-container {
flex: 1;
min-width: 300px;
max-width: 500px;
height: 400px;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
background: #f9f9f9;
}
.imagecontainer {
flex: 1;
min-width: 300px;
}
</style>

View File

@@ -50,5 +50,13 @@ export default defineConfig(({ mode }) => {
assert: 'assert',
}
},
server: {
fs: {
// Erlaube Zugriff auf Dateien außerhalb des Projektverzeichnisses
strict: false
}
},
// Stelle sicher, dass GLB/GLTF-Dateien als Assets behandelt werden
assetsInclude: ['**/*.glb', '**/*.gltf']
};
});

View File

@@ -1,43 +0,0 @@
#!/bin/bash
echo "=== WebSocket-Konfiguration testen ==="
echo ""
# Prüfe Apache-Module
echo "1. Prüfe Apache-Module:"
REQUIRED_MODULES=("proxy" "proxy_http" "proxy_wstunnel" "rewrite" "ssl" "headers")
for module in "${REQUIRED_MODULES[@]}"; do
if apache2ctl -M 2>/dev/null | grep -q "${module}_module"; then
echo "$module ist aktiviert"
else
echo "$module ist NICHT aktiviert"
fi
done
echo ""
echo "2. Prüfe Apache-Konfiguration:"
if sudo apache2ctl configtest 2>&1 | grep -q "Syntax OK"; then
echo " ✅ Konfiguration ist gültig"
else
echo " ❌ Konfiguration hat Fehler:"
sudo apache2ctl configtest 2>&1
fi
echo ""
echo "3. Prüfe aktive VirtualHosts:"
apache2ctl -S 2>/dev/null | grep -E "(443|4443|4551)" || echo " Keine relevanten VirtualHosts gefunden"
echo ""
echo "4. Prüfe Apache-Logs (letzte 20 Zeilen):"
echo " Error-Log:"
sudo tail -20 /var/log/apache2/yourpart.error.log 2>/dev/null || echo " Keine Fehler gefunden"
echo ""
echo " Access-Log (letzte 10 Zeilen mit /ws/ oder /socket.io/):"
sudo tail -100 /var/log/apache2/yourpart.access.log 2>/dev/null | grep -E "(/ws/|/socket.io/)" | tail -10 || echo " Keine relevanten Einträge gefunden"
echo ""
echo "5. Teste WebSocket-Verbindungen:"
echo " Socket.io: wss://www.your-part.de/socket.io/"
echo " Daemon: wss://www.your-part.de/ws/"
echo ""
echo " Bitte im Browser testen und dann die Logs prüfen."

View File

@@ -1,74 +0,0 @@
<IfModule mod_ssl.c>
<VirtualHost your-part.de:443>
ServerAdmin webmaster@your-part.de
ServerName your-part.de
ServerAlias www.your-part.de
DocumentRoot /opt/yourpart/frontend/dist
DirectoryIndex index.html
# Frontend statische Dateien
<Directory "/opt/yourpart/frontend/dist">
AllowOverride None
Options -Indexes +FollowSymLinks
Require all granted
# Fallback für Vue Router
FallbackResource /index.html
</Directory>
# www Redirect (muss zuerst kommen)
RewriteEngine on
RewriteCond %{SERVER_NAME} =your-part.de
RewriteRule ^ https://www.%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
# Proxy-Einstellungen
ProxyPreserveHost On
ProxyRequests Off
RequestHeader set X-Forwarded-Proto "https"
AllowEncodedSlashes NoDecode
# API-Requests an Backend weiterleiten
ProxyPass "/api/" "http://localhost:2020/api/"
ProxyPassReverse "/api/" "http://localhost:2020/api/"
# Socket.io: WebSocket und HTTP-Polling mit Location-Blöcken
<LocationMatch "^/socket.io/">
# WebSocket-Upgrade
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule .* "ws://localhost:2020%{REQUEST_URI}" [P,L]
# HTTP-Fallback für Polling
ProxyPass "http://localhost:2020/socket.io/"
ProxyPassReverse "http://localhost:2020/socket.io/"
</LocationMatch>
# Daemon: WebSocket mit Location-Block
<LocationMatch "^/ws/">
# WebSocket-Upgrade
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule .* "ws://localhost:4551%{REQUEST_URI}" [P,L]
# HTTP-Fallback (sollte eigentlich nicht benötigt werden)
ProxyPass "http://localhost:4551/"
ProxyPassReverse "http://localhost:4551/"
</LocationMatch>
ErrorLog /var/log/apache2/yourpart.error.log
CustomLog /var/log/apache2/yourpart.access.log combined
HostnameLookups Off
UseCanonicalName Off
ServerSignature On
# SSL-Konfiguration
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.your-part.de/privkey.pem
</VirtualHost>
</IfModule>

View File

@@ -18,23 +18,28 @@
FallbackResource /index.html
</Directory>
# Proxy-Einstellungen
ProxyPreserveHost On
ProxyRequests Off
RequestHeader set X-Forwarded-Proto "https"
AllowEncodedSlashes NoDecode
# API-Requests an Backend weiterleiten
ProxyPass "/api/" "http://localhost:2020/api/"
ProxyPassReverse "/api/" "http://localhost:2020/api/"
# www Redirect (muss zuerst kommen, aber nicht für API-Pfade)
# WebSocket-Requests an Backend weiterleiten
ProxyPass "/socket.io/" "http://localhost:2020/socket.io/"
ProxyPassReverse "/socket.io/" "http://localhost:2020/socket.io/"
# WebSocket-Upgrade-Header für Socket.io
RewriteEngine on
RewriteCond %{SERVER_NAME} =your-part.de
RewriteCond %{REQUEST_URI} !^/api/
RewriteRule ^ https://www.%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/socket.io/(.*)$ "ws://localhost:2020/socket.io/$1" [P,L]
# API-Requests an Backend weiterleiten (Location-Block hat höhere Priorität)
<Location "/api/">
ProxyPass "http://localhost:2020/api/"
ProxyPassReverse "http://localhost:2020/api/"
</Location>
# WebSocket-Upgrade-Header für Daemon-Verbindungen
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/ws/(.*)$ "ws://localhost:4551/$1" [P,L]
# WebSocket-Proxy für Daemon-Verbindungen mit benutzerdefiniertem Protokoll
ProxyPass "/ws/" "ws://localhost:4551/" upgrade=websocket
ProxyPassReverse "/ws/" "ws://localhost:4551/"
ErrorLog /var/log/apache2/yourpart.error.log
CustomLog /var/log/apache2/yourpart.access.log combined
@@ -47,5 +52,9 @@
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.your-part.de/privkey.pem
# www Redirect
RewriteCond %{SERVER_NAME} =your-part.de
RewriteRule ^ https://www.%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
</IfModule>

View File

@@ -1,39 +0,0 @@
# /etc/apache2/sites-available/yourpart-websocket.conf
<IfModule mod_ssl.c>
<VirtualHost *:4551>
ServerName www.your-part.de
# SSL aktivieren
SSLEngine on
Protocols http/1.1
# SSL-Konfiguration
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.your-part.de/privkey.pem
# Proxy-Einstellungen
ProxyPreserveHost On
ProxyRequests Off
AllowEncodedSlashes NoDecode
# WebSocket-Upgrade (muss VOR ProxyPass stehen)
# Apache lauscht auf Port 4551 (extern, verschlüsselt) und leitet an Daemon auf Port 4551 weiter (intern, unverschlüsselt)
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://localhost:4551/$1" [P,L]
# Fallback für normale HTTP-Requests (falls nötig)
ProxyPass / http://localhost:4551/
ProxyPassReverse / http://localhost:4551/
# CORS-Headers
Header always set Access-Control-Allow-Origin "https://www.your-part.de"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization"
ErrorLog /var/log/apache2/yourpart-websocket.error.log
CustomLog /var/log/apache2/yourpart-websocket.access.log combined
</VirtualHost>
</IfModule>

View File

@@ -1,29 +0,0 @@
<IfModule mod_ssl.c>
<VirtualHost *:4443>
ServerName www.your-part.de
SSLEngine on
Protocols http/1.1
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/www.your-part.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.your-part.de/privkey.pem
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
AllowEncodedSlashes NoDecode
# WebSocket-Upgrade für Socket.io (muss VOR ProxyPass stehen)
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/socket.io/(.*)$ "ws://127.0.0.1:2020/socket.io/$1" [P,L]
# HTTP-Proxy für Socket.io (Fallback für Polling)
ProxyPass "/socket.io/" "http://127.0.0.1:2020/socket.io/" retry=0
ProxyPassReverse "/socket.io/" "http://127.0.0.1:2020/socket.io/"
ErrorLog /var/log/apache2/yourpart-ws.error.log
CustomLog /var/log/apache2/yourpart-ws.access.log combined
</VirtualHost>
</IfModule>