Compare commits

..

27 Commits

Author SHA1 Message Date
Torsten Schulz (local)
cd739fb52e Refactor server.js for improved WebSocket and API configurations
- Updated API server to listen on a configurable port (2020) for better flexibility.
- Removed WebSocket setup from HTTP server, ensuring Socket.io is only available over HTTPS.
- Enhanced logging to clarify Socket.io availability based on TLS configuration.
2026-01-14 14:30:50 +01:00
Torsten Schulz (local)
9e845843d8 Update WebSocket and API configurations in yourpart-websocket-fixed.conf and daemonServer.js
- Adjusted WebSocket proxy settings in yourpart-websocket-fixed.conf to route traffic through port 4551 for both secure and non-secure connections.
- Enhanced daemonServer.js to listen on all interfaces (0.0.0.0) for both TLS and non-TLS WebSocket connections, improving accessibility.
2026-01-14 13:10:33 +01:00
Torsten Schulz (local)
0cc280ed55 Refactor WebSocket and API configurations in yourpart-https.conf and yourpart-websocket-fixed.conf
- Removed outdated WebSocket handling from yourpart-https.conf for improved clarity.
- Updated yourpart-websocket-fixed.conf to enable SSL and adjust WebSocket proxy settings.
- Streamlined fallback logic in frontend store to ensure direct connection to the daemon on port 4551.
- Enhanced logging for better debugging and monitoring of daemon connections.
2026-01-14 13:02:38 +01:00
Torsten Schulz (local)
b3707d21b2 Refactor yourpart-https.conf for enhanced WebSocket and API request handling
- Updated www redirect to exclude API and WebSocket paths for improved functionality.
- Organized WebSocket and API proxy settings within Location blocks for better clarity and maintainability.
- Consolidated WebSocket upgrade rules for Socket.io and daemon connections, ensuring consistent handling.
2026-01-14 12:07:04 +01:00
Torsten Schulz (local)
fbebd6c1c1 Refactor yourpart-https.conf for improved WebSocket handling and domain redirection
- Updated www redirect to exclude WebSocket paths for better functionality.
- Consolidated WebSocket upgrade rules for Socket.io and daemon connections.
- Enhanced organization of proxy settings for API and WebSocket requests, improving maintainability.
2026-01-14 12:02:49 +01:00
Torsten Schulz (local)
d7c2bda461 Enhance yourpart-https.conf with improved WebSocket and API configurations
- Added www redirect to ensure consistent domain usage.
- Consolidated WebSocket upgrade conditions for clarity.
- Streamlined API request forwarding and fallback proxy settings for better organization and maintainability.
2026-01-14 11:57:35 +01:00
Torsten Schulz (local)
2bf949513b Refactor yourpart-https.conf for improved WebSocket and API request handling
- Consolidated WebSocket upgrade conditions for clarity and consistency.
- Streamlined API request forwarding configuration.
- Removed redundant proxy settings to enhance organization and maintainability of the configuration file.
2026-01-14 10:28:23 +01:00
Torsten Schulz (local)
84619fb656 Update proxy settings in yourpart-https.conf for improved WebSocket and API handling
- Added ProxyPreserveHost and ProxyRequests directives for better request handling.
- Configured WebSocket upgrade headers for Socket.io and daemon connections.
- Established HTTP proxies for API and WebSocket requests to ensure fallback mechanisms are in place.
- Improved overall clarity and organization of proxy settings in the configuration file.
2026-01-12 16:55:09 +01:00
Torsten Schulz (local)
b600f16ecd Enhance MessagesDialog component and localization for overproduction notifications
- Updated MessagesDialog.vue to extract additional parameters (value, branch_id, region_id) for better handling of overproduction scenarios.
- Modified localization files (de/falukant.json and en/falukant.json) to reflect changes in the overproduction notification format, including branch information.
- Improved data formatting for clarity in notifications related to production levels.
2026-01-12 16:48:10 +01:00
Torsten Schulz (local)
9273066f61 Refactor trait handling in FalukantService and FamilyView for improved data consistency
- Updated trait loading in FalukantService to include trait types, enhancing the mapping of traits to characters.
- Adjusted FamilyView to utilize the new trait structure, ensuring accurate retrieval of traits associated with characters.
- Improved null checks for relationships to prevent potential errors during data access.
2026-01-12 13:48:37 +01:00
Torsten Schulz (local)
7d59dbcf84 Update mood and character traits handling in FalukantService to ensure default values are set when no data is available. This change improves robustness in data retrieval by preventing potential undefined values. 2026-01-12 13:44:29 +01:00
Torsten Schulz (local)
015d1ae95b Refactor getGiftCost method in FalukantService for improved performance
- Changed getGiftCost from an asynchronous to a synchronous method, eliminating unnecessary await for better efficiency.
- Updated comments for clarity regarding the synchronous nature of gift cost calculation.
- Adjusted related code to reflect the synchronous execution, enhancing overall performance in gift cost retrieval.
2026-01-12 12:02:46 +01:00
Torsten Schulz (local)
e2cd6e0e5e Refactor relationship retrieval in FalukantService for improved performance
- Optimized the process of finding relationships by using two separate queries for better index utilization, reducing data overhead.
- Enhanced loading of related characters and traits by implementing parallel data fetching, improving efficiency in data retrieval.
- Updated timing metrics to reflect changes in the relationship loading process, ensuring accurate performance tracking.
2026-01-12 11:57:17 +01:00
Torsten Schulz (local)
ec113058d0 Enhance getGifts method in FalukantService with detailed performance metrics and optimized data retrieval
- Implemented timing metrics to track performance across various steps of the getGifts method, improving traceability.
- Refactored data loading to optimize user, character, and relationship queries, ensuring only necessary fields are fetched.
- Enhanced error handling and logging for better debugging and performance insights during execution.
2026-01-12 11:49:49 +01:00
Torsten Schulz (local)
d2ac2bfdd8 Optimize gift retrieval in FalukantService by loading only necessary fields and implementing parallel data fetching. This change enhances performance and reduces data overhead during the gift and title of nobility retrieval process. 2026-01-12 11:46:16 +01:00
Torsten Schulz (local)
d75fe18e6a Optimize user and character loading in FalukantService by querying only necessary fields. This change enhances performance and reduces data overhead during retrieval. 2026-01-12 11:35:38 +01:00
Torsten Schulz (local)
479f222b54 Refactor character retrieval in FalukantService for improved clarity
- Changed the character retrieval method to directly access the user's character property instead of querying the database, enhancing code readability and performance.
2026-01-12 11:31:49 +01:00
Torsten Schulz (local)
013c536b47 Refactor firstNameMap creation in FalukantService for improved efficiency
- Simplified the construction of firstNameMap by removing unnecessary spread operator, enhancing performance and readability.
2026-01-12 11:28:32 +01:00
Torsten Schulz (local)
3b983a0db5 Update attribute mapping in FalukantService for mood data retrieval
- Changed mood attribute from 'labelTr' to 'tr' in the mood query to align with updated data structure.
- Adjusted mood mapping to reflect the new attribute, ensuring consistency in data handling.
2026-01-12 11:24:56 +01:00
Torsten Schulz (local)
5f9559ac8d Update FalukantService to utilize FalukantCharacterTrait for character relationships
- Added import for FalukantCharacterTrait to enhance data retrieval for character relationships.
- Refactored the relationship character query to use FalukantCharacterTrait instead of CharacterTrait, improving data accuracy and consistency.
2026-01-12 11:16:23 +01:00
Torsten Schulz (local)
f487e6d765 Enhance getFamily method in FalukantService for performance and data retrieval
- Implemented detailed timing metrics to track performance across various steps of the getFamily method.
- Refactored data retrieval to load relationships and children in parallel, improving efficiency.
- Enhanced mapping of relationship and child data, including additional attributes for characters.
- Introduced batch loading for related data to minimize database queries and optimize performance.
- Improved error handling and logging for better traceability during execution.
2026-01-12 11:09:21 +01:00
Torsten Schulz (local)
5e26422e9c Add batch price retrieval for products in region
- Implemented a new endpoint to fetch prices for multiple products in a specified region in a single request, improving efficiency.
- Added validation for input parameters to ensure proper data handling.
- Updated the FalukantService to calculate prices based on knowledge factors and worth percentages for each product.
- Modified the frontend to utilize the new batch endpoint, optimizing the loading of product prices.
2026-01-12 08:58:28 +01:00
Torsten Schulz (local)
64baebfaaa Optimize proposal generation in FalukantService using CTEs
- Replaced multiple SQL queries with a single query utilizing Common Table Expressions (CTEs) for improved performance.
- Streamlined the exclusion of existing proposals and active directors directly within the SQL query.
- Enhanced logging for SQL queries and results, providing better traceability during proposal generation.
- Simplified the process of calculating average knowledge for character proposals, ensuring more efficient data handling.
2026-01-12 08:46:54 +01:00
Torsten Schulz (local)
521dec24b2 Enhance proposal generation logic in FalukantService
- Implemented exclusion of existing proposals and active directors to avoid duplicate character selections.
- Added detailed logging for SQL queries and fallback mechanisms when insufficient older characters are found.
- Improved character selection process by combining excluded character IDs and ensuring a diverse set of proposals.
- Streamlined the batch creation of director proposals with average knowledge calculations for better income predictions.
2026-01-12 08:39:41 +01:00
Torsten Schulz (local)
36f0bd8eb9 Refactor MessagesDialog component to improve parameter interpolation and description formatting
- Enhanced the description formatting logic to ensure proper interpolation of parameters in both title and description fields.
- Updated the handling of monetary values to ensure they are correctly converted to numbers before formatting.
- Improved comments for clarity on the interpolation process and effects handling.
2026-01-09 14:44:20 +01:00
Torsten Schulz (local)
d0a2b122b2 Implement enhanced partner search and NPC creation logic in FalukantService
- Added detailed logging for partner search criteria and results, improving traceability.
- Refactored partner search logic to include a fallback mechanism for searching across all regions if no partners are found in the specified region.
- Introduced a new method for creating NPCs when no suitable partners are available, ensuring a continuous flow in the partner matching process.
- Improved the handling of character attributes such as age and title of nobility during partner searches and NPC creation.
2026-01-09 14:37:55 +01:00
Torsten Schulz (local)
c80cc8ec86 Enhance logging and error handling in FalukantService and FamilyView
- Added detailed logging for partner search and creation processes in FalukantService to improve traceability and debugging.
- Refactored the partner search logic to use a dynamic where clause for better readability and maintainability.
- Implemented error handling in FamilyView's loadGifts method to ensure an empty array is returned on API errors, enhancing user experience.
2026-01-09 14:32:27 +01:00
45 changed files with 1110 additions and 1985 deletions

View File

@@ -162,6 +162,18 @@ 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,11 +25,13 @@ 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 {
wss = new WebSocketServer({ port: PORT });
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
wss = new WebSocketServer({ port: PORT, host: '0.0.0.0' });
console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`);
}

View File

@@ -0,0 +1,13 @@
-- 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

@@ -0,0 +1,43 @@
-- 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,6 +76,7 @@ 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,19 +1,55 @@
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';
const server = http.createServer(app);
// 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
setupWebSocket(server);
// 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');
}
syncDatabase().then(() => {
const port = process.env.PORT || 3001;
server.listen(port, () => {
console.log('Server is running on port', port);
// 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)`);
});
// 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

45
check-apache-websocket.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/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"

35
debug-websocket-headers.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/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

@@ -1,143 +0,0 @@
# 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**

View File

@@ -1,171 +0,0 @@
# 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

@@ -1,159 +0,0 @@
# 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

@@ -1,334 +0,0 @@
# 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,7 +21,6 @@
"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",
@@ -2835,12 +2834,6 @@
"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,7 +21,6 @@
"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

@@ -1,40 +0,0 @@
# 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

View File

@@ -1,225 +0,0 @@
<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,6 +151,19 @@ 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
@@ -172,9 +185,12 @@ 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: this.formatDescriptionWithEffects(value.description, value.effects || [], formattedParams)
description: description
};
}
@@ -212,6 +228,10 @@ 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);
@@ -234,15 +254,27 @@ export default {
// Geldbeträge formatieren
if (params.amount !== undefined && params.amount !== null) {
formatted.amount = this.formatMoney(params.amount);
formatted.amount = this.formatMoney(Number(params.amount));
}
if (params.absolute !== undefined && params.absolute !== null) {
formatted.amount = this.formatMoney(params.absolute);
formatted.amount = this.formatMoney(Number(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

@@ -1,441 +0,0 @@
<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}% über dem Bedarf."
"overproduction": "Überproduktion: Deine Produktion liegt {value} Einheiten über dem Bedarf{branch_info}."
},
"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}% above demand."
"overproduction": "Overproduction: your production is {value} units above demand{branch_info}."
},
"transport": {
"waiting": "Transport waiting"

View File

@@ -188,18 +188,29 @@ const store = createStore({
socketIoUrl = 'http://localhost:3001';
}
// 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 {
// 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;
} catch (_) {}
}
}
const socket = io(socketIoUrl, {
@@ -284,12 +295,13 @@ const store = createStore({
// Wenn Umgebungsvariable nicht gesetzt ist oder leer, verwende Fallback-Logik
if (!daemonUrl || (typeof daemonUrl === 'string' && daemonUrl.trim() === '')) {
// Fallback: direkte Verbindung zum Daemon-Port 4551 (ohne Apache-Proxy)
// Immer direkte Verbindung zum Daemon-Port 4551 (verschlüsselt)
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
daemonUrl = `${protocol}//${hostname}:4551/`;
console.log('[Daemon] Verwende Fallback basierend auf Hostname, Protokoll und Port 4551');
console.log('[Daemon] Verwende direkte Verbindung zu Port 4551');
} else {
console.log('[Daemon] Verwende Umgebungsvariable');
// Wenn Umgebungsvariable gesetzt ist, verwende sie direkt
console.log('[Daemon] Verwende Umgebungsvariable:', daemonUrl);
}
console.log('[Daemon] Finale Daemon-URL:', daemonUrl);

View File

@@ -468,14 +468,10 @@ export default {
},
watch: {
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();
});
activeTab(newVal) {
if (newVal === 'taxes') {
this.loadBranchTaxes();
}
},
selectedBranch: {
handler(newBranch) {
@@ -541,33 +537,6 @@ 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
@@ -580,8 +549,13 @@ export default {
await this.loadVehicles();
await this.loadProductPricesForCurrentBranch();
this.$nextTick(() => {
// Alle Tabs neu laden
this.refreshActiveTab();
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();
});
// load tax info for this branch
@@ -598,27 +572,49 @@ export default {
return;
}
// 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);
}
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;
});
}
this.productPricesCache = prices;
},
formatPercent(value) {

View File

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

View File

@@ -116,26 +116,15 @@
</table>
</div>
</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 v-if="falukantUser?.character" class="imagecontainer">
<div :style="getAvatarStyle" class="avatar"></div>
<div :style="getHouseStyle" class="house"></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';
@@ -180,7 +169,6 @@ export default {
name: 'FalukantOverviewView',
components: {
StatusBar,
CharacterModel3D,
},
data() {
return {
@@ -493,27 +481,4 @@ 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,13 +50,5 @@ 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']
};
});

43
test-websocket-config.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/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

@@ -0,0 +1,74 @@
<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,28 +18,23 @@
FallbackResource /index.html
</Directory>
# API-Requests an Backend weiterleiten
ProxyPass "/api/" "http://localhost:2020/api/"
ProxyPassReverse "/api/" "http://localhost:2020/api/"
# Proxy-Einstellungen
ProxyPreserveHost On
ProxyRequests Off
RequestHeader set X-Forwarded-Proto "https"
AllowEncodedSlashes NoDecode
# 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
# www Redirect (muss zuerst kommen, aber nicht für API-Pfade)
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/socket.io/(.*)$ "ws://localhost:2020/socket.io/$1" [P,L]
RewriteCond %{SERVER_NAME} =your-part.de
RewriteCond %{REQUEST_URI} !^/api/
RewriteRule ^ https://www.%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
# 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/"
# 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>
ErrorLog /var/log/apache2/yourpart.error.log
CustomLog /var/log/apache2/yourpart.access.log combined
@@ -52,9 +47,5 @@
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

@@ -0,0 +1,39 @@
# /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>

29
yourpart-ws-fixed.conf Normal file
View File

@@ -0,0 +1,29 @@
<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>