Create a 3D chat for a dating website
Part 1: Setting Up Your 3D Dating World - Where Pixels Find Passion! 💘
Welcome, brave developer, to the most exciting project of your career: building a 3D dating chat! Why settle for boring 2D profiles when you can help people find love in THREE DIMENSIONS? That's 50% more dimension than regular dating! Chameleon Social and Dating Software includes 3D Chat, so you can have one too!

In this 10-part series, we'll create a virtual dating space where avatars can flirt, chat, and probably awkwardly stare at each other from multiple angles. Let's begin!
Step 1: The Basic Setup - HTML That Says "I'm Ready to Mingle!"
First, let's create our basic HTML structure. Think of this as building the virtual bar where all the digital magic happens:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Love in 3D - Where Your GPU Finds True Love! ❤️</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Comic Sans MS', cursive; /* Because we're serious about fun! */
}
#container {
position: relative;
width: 100vw;
height: 100vh;
}
#chatUI {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
width: 300px;
z-index: 100;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
#loadingScreen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 24px;
z-index: 1000;
}
</style>
</head>
<body>
<div id="container">
<div id="loadingScreen">
Loading your perfect match... ⏳
</div>
<div id="chatUI" style="display: none;">
<input type="text" id="messageInput" placeholder="Type your pickup line here...">
<button onclick="sendMessage()">Send ❤️</button>
<div id="chatMessages"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="app.js"></script>
</body>
</html>
Step 2: Three.js Initialization - Lighting the Digital Candles 🕯️
Now, let's create our app.js file and set up the Three.js scene. This is where we create the virtual world where love (or at least interesting conversations) will blossom:
// app.js - Where the magic happens and hearts get rendered at 60fps!
class DatingScene {
constructor() {
this.scene = null;
this.camera = null;
this.renderer = null;
this.avatars = []; // This array will store all our lonely hearts
this.currentUser = null;
this.init(); // Let's get this virtual romance started!
}
init() {
this.createScene();
this.createCamera();
this.createRenderer();
this.createLights();
this.createEnvironment();
this.animate();
// Hide loading screen after 2 seconds (or when assets load)
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';
document.getElementById('chatUI').style.display = 'block';
this.addSampleAvatars(); // Add some virtual hotties
}, 2000);
}
createScene() {
// Create the scene - think of this as the virtual dating venue
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x87CEEB); // Sky blue - for optimistic vibes!
console.log("Scene created! Ready to host some digital romance! 💕");
}
createCamera() {
// Create camera - this is our window into the dating world
this.camera = new THREE.PerspectiveCamera(
75, // Field of view - wide enough to see all the potential matches!
window.innerWidth / window.innerHeight,
0.1, // Near clipping plane - no getting too close too fast!
1000 // Far clipping plane - can spot matches from afar!
);
this.camera.position.set(0, 5, 10); // Elevated view - like watching from the VIP section!
this.camera.lookAt(0, 0, 0);
console.log("Camera ready! Say cheese, future lovers! 📸");
}
createRenderer() {
// Create renderer - the artist that paints our love story
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true; // Shadows make everything sexier!
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('container').appendChild(this.renderer.domElement);
// Handle window resizing - because love should be responsive!
window.addEventListener('resize', () => this.onWindowResize());
console.log("Renderer ready! Preparing to render some chemistry! 🔥");
}
createLights() {
// Ambient light - the mood lighting for our virtual date
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
this.scene.add(ambientLight);
// Directional light - the spotlight for our star-crossed lovers
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
this.scene.add(directionalLight);
// Point light - for that romantic glow
const pointLight = new THREE.PointLight(0xff6b6b, 0.5, 100);
pointLight.position.set(0, 5, 0);
this.scene.add(pointLight);
console.log("Lights ready! Setting the mood for digital romance! 💡");
}
createEnvironment() {
// Create ground - where our avatars will stand (and probably nervously shuffle)
const groundGeometry = new THREE.PlaneGeometry(50, 50);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x90EE90, // Green - like a peaceful park for dating
roughness: 0.8,
metalness: 0.2
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2; // Make it horizontal, because that's how ground works!
ground.receiveShadow = true;
this.scene.add(ground);
// Add some decorative elements - because even virtual dates need ambiance!
this.createDecorativeElements();
console.log("Environment ready! Less awkward than a real-life coffee shop! ☕");
}
createDecorativeElements() {
// Create some trees - for avatars to hide behind when they're shy
for (let i = 0; i < 5; i++) {
const tree = this.createTree();
tree.position.set(
(Math.random() - 0.5) * 40,
0,
(Math.random() - 0.5) * 40
);
this.scene.add(tree);
}
// Create a heart-shaped object because we're cheesy like that!
const heartShape = new THREE.Shape();
heartShape.moveTo(0, 0);
heartShape.bezierCurveTo(2, 2, 3, 0, 0, -3);
heartShape.bezierCurveTo(-3, 0, -2, 2, 0, 0);
const heartGeometry = new THREE.ExtrudeGeometry(heartShape, {
depth: 0.5,
bevelEnabled: true,
bevelSegments: 2,
bevelSize: 0.1,
bevelThickness: 0.1
});
const heartMaterial = new THREE.MeshStandardMaterial({
color: 0xff6b6b,
emissive: 0xff0000,
emissiveIntensity: 0.2
});
const heart = new THREE.Mesh(heartGeometry, heartMaterial);
heart.position.set(0, 3, 0);
heart.scale.set(0.5, 0.5, 0.5);
this.scene.add(heart);
}
createTree() {
// Trunk
const trunkGeometry = new THREE.CylinderGeometry(0.3, 0.4, 2, 8);
const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
trunk.castShadow = true;
// Leaves
const leavesGeometry = new THREE.SphereGeometry(1.5, 8, 6);
const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
const leaves = new THREE.Mesh(leavesGeometry, leavesMaterial);
leaves.position.y = 2;
leaves.castShadow = true;
const tree = new THREE.Group();
tree.add(trunk);
tree.add(leaves);
return tree;
}
addSampleAvatars() {
// Let's add some sample avatars to make the place look popular!
// In real implementation, these would come from your backend
console.log("Adding sample avatars... because empty venues are sad! 😢");
// Create a few basic avatars
for (let i = 0; i < 3; i++) {
const avatar = this.createBasicAvatar();
avatar.position.set(
(i - 1) * 4, // Spread them out
0,
-5 + Math.random() * 3 // Some variation in depth
);
// Give each avatar a slight color variation
avatar.children[0].material.color.setHex(0xffffff - i * 0x111111);
this.scene.add(avatar);
this.avatars.push(avatar);
// Add some simple animation to make them look alive
this.animateAvatar(avatar);
}
console.log(`Added ${this.avatars.length} potential matches! Time to mingle! 🎉`);
}
createBasicAvatar() {
// Create a simple stick-figure style avatar
// In future parts, we'll make these much more sophisticated!
const avatarGroup = new THREE.Group();
// Head
const headGeometry = new THREE.SphereGeometry(0.5, 16, 16);
const headMaterial = new THREE.MeshStandardMaterial({ color: 0xFFD700 });
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.y = 1.7;
head.castShadow = true;
avatarGroup.add(head);
// Body
const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.3, 1.5, 8);
const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0x4169E1 });
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.75;
body.castShadow = true;
avatarGroup.add(body);
// Simple eyes (because even avatars need to make eye contact!)
const eyeGeometry = new THREE.SphereGeometry(0.1, 8, 8);
const eyeMaterial = new THREE.MeshStandardMaterial({ color: 0x000000 });
const leftEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
leftEye.position.set(0.2, 1.7, 0.4);
avatarGroup.add(leftEye);
const rightEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
rightEye.position.set(-0.2, 1.7, 0.4);
avatarGroup.add(rightEye);
console.log("Basic avatar created! They may be simple, but they have personality! 😊");
return avatarGroup;
}
animateAvatar(avatar) {
// Simple bobbing animation to make avatars look alive
const originalY = avatar.position.y;
let time = Math.random() * Math.PI * 2; // Random start time
const animate = () => {
time += 0.05;
avatar.position.y = originalY + Math.sin(time) * 0.1; // Gentle bobbing
// Rotate slightly for more natural movement
avatar.rotation.y = Math.sin(time * 0.5) * 0.1;
requestAnimationFrame(animate);
};
animate();
}
onWindowResize() {
// Handle window resize - because love shouldn't get cut off!
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
console.log("Window resized! Making sure no one misses their perfect match! 🔍");
}
animate() {
// The main animation loop - where the magic keeps happening!
requestAnimationFrame(() => this.animate());
// Rotate the heart because we're extra like that
const heart = this.scene.children.find(child =>
child.geometry && child.geometry.type === 'ExtrudeGeometry'
);
if (heart) {
heart.rotation.y += 0.01;
}
this.renderer.render(this.scene, this.camera);
}
}
// Chat functionality - because what's dating without conversation?
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message) {
const chatMessages = document.getElementById('chatMessages');
const messageElement = document.createElement('div');
messageElement.textContent = `You: ${message}`;
messageElement.style.margin = '5px 0';
messageElement.style.padding = '8px';
messageElement.style.background = '#f0f0f0';
messageElement.style.borderRadius = '10px';
chatMessages.appendChild(messageElement);
chatMessages.scrollTop = chatMessages.scrollHeight; // Auto-scroll to bottom
input.value = ''; // Clear input
// Simulate response (in real app, this would come from other users)
setTimeout(() => {
const responses = [
"That's so interesting! Tell me more!",
"I love that! 😊",
"Really? Me too!",
"You're funny! 😄",
"That's an amazing pickup line! 🤣"
];
const responseElement = document.createElement('div');
responseElement.textContent = `Potential Match: ${responses[Math.floor(Math.random() * responses.length)]}`;
responseElement.style.margin = '5px 0';
responseElement.style.padding = '8px';
responseElement.style.background = '#ffebee';
responseElement.style.borderRadius = '10px';
chatMessages.appendChild(responseElement);
chatMessages.scrollTop = chatMessages.scrollHeight;
}, 1000 + Math.random() * 2000);
}
}
// Handle Enter key in chat input
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
// Let's get this virtual romance started!
let datingScene;
window.addEventListener('DOMContentLoaded', () => {
datingScene = new DatingScene();
console.log("Dating scene initialized! Let the digital romance begin! 💖");
// Add click handler to focus chat input
document.addEventListener('click', () => {
document.getElementById('messageInput').focus();
});
});
What We've Built So Far: 🎉
- A 3D Environment: A lovely green space with trees and a rotating heart
- Basic Avatars: Simple stick-figure people that bob gently (they're nervous!)
- Chat Interface: Where you can practice your pickup lines
- Responsive Design: Works on any screen size (love is universal!)
Key Three.js Concepts Explained: 📚
- Scene: The container for all our 3D objects - think of it as the virtual dating venue
- Camera: Your viewpoint into the 3D world - positioned like you're watching from above
- Renderer: The magic that converts 3D math into pretty pixels on your screen
- Geometry: The shape of 3D objects (spheres, cylinders, etc.)
- Material: The "skin" or appearance of objects (colors, shininess, etc.)
- Mesh: The combination of geometry + material that becomes a visible object
Next Time in Part 2: 🚀
We'll upgrade our basic stick figures to proper 3D avatars with:
- Customizable appearances (hair, clothes, etc.)
- Smooth animations (walking, waving, flirting!)
- User controls to move around the scene
- And much more!
Remember: In the world of 3D dating, the only thing that should be 2D is the screens we're building it on! 😄
Current Project Status: We've built the virtual bar, now we just need to teach these avatars how to flirt! Stay tuned for Part 2!
Disclaimer: No actual hearts were broken during the development of this tutorial. GPUs may get warm from all the rendered romance!
Part 2: Creating Customizable 3D Avatars - Making Digital Heartthrobs! 💖
Welcome back, cupid-coder! In Part 1, we built a lovely 3D environment with some basic stick figures. But let's be honest - our current avatars look like they escaped from a geometry textbook! Time to give them some personality and make them actually date-worthy!
Step 1: Avatar Class - Because Every Heart Needs a Body! 💃
Let's create a proper Avatar class that can handle customization, animations, and all the fancy stuff that makes digital dating exciting:
// avatar.js - Where basic shapes become beautiful people!
class Avatar {
constructor(options = {}) {
this.options = {
gender: options.gender || 'neutral',
skinTone: options.skinTone || 0xFFDBAC,
hairColor: options.hairColor || 0x8B4513,
clothingColor: options.clothingColor || 0x4169E1,
name: options.name || 'Mysterious Stranger',
...options
};
this.mesh = new THREE.Group();
this.animations = {};
this.currentAnimation = null;
this.createBody();
this.createFace();
this.createHair();
this.createClothing();
console.log(`Avatar "${this.options.name}" created! Ready to mingle! 😎`);
}
createBody() {
// Head - much better than our previous sphere!
const headGeometry = new THREE.SphereGeometry(0.4, 32, 32);
const headMaterial = new THREE.MeshStandardMaterial({
color: this.options.skinTone,
roughness: 0.7,
metalness: 0.1
});
this.head = new THREE.Mesh(headGeometry, headMaterial);
this.head.position.y = 1.6;
this.head.castShadow = true;
this.mesh.add(this.head);
// Neck
const neckGeometry = new THREE.CylinderGeometry(0.1, 0.12, 0.3, 8);
const neckMaterial = new THREE.MeshStandardMaterial({
color: this.options.skinTone
});
this.neck = new THREE.Mesh(neckGeometry, neckMaterial);
this.neck.position.y = 1.35;
this.neck.castShadow = true;
this.mesh.add(this.neck);
// Torso - with actual shape!
const torsoGeometry = new THREE.CylinderGeometry(0.5, 0.6, 1.2, 16);
const torsoMaterial = new THREE.MeshStandardMaterial({
color: this.options.clothingColor,
roughness: 0.8
});
this.torso = new THREE.Mesh(torsoGeometry, torsoMaterial);
this.torso.position.y = 0.6;
this.torso.castShadow = true;
this.mesh.add(this.torso);
// Arms - they can actually hug now!
this.createArms();
// Legs - for walking toward true love!
this.createLegs();
}
createArms() {
const armGeometry = new THREE.CylinderGeometry(0.08, 0.1, 1, 8);
const armMaterial = new THREE.MeshStandardMaterial({
color: this.options.skinTone
});
// Left arm
this.leftArm = new THREE.Mesh(armGeometry, armMaterial);
this.leftArm.position.set(0.55, 0.9, 0);
this.leftArm.rotation.z = Math.PI / 6; // Slight natural angle
this.leftArm.castShadow = true;
this.mesh.add(this.leftArm);
// Right arm
this.rightArm = new THREE.Mesh(armGeometry, armMaterial);
this.rightArm.position.set(-0.55, 0.9, 0);
this.rightArm.rotation.z = -Math.PI / 6;
this.rightArm.castShadow = true;
this.mesh.add(this.rightArm);
// Hands
const handGeometry = new THREE.SphereGeometry(0.12, 16, 16);
const handMaterial = new THREE.MeshStandardMaterial({
color: this.options.skinTone
});
this.leftHand = new THREE.Mesh(handGeometry, handMaterial);
this.leftHand.position.set(0.9, 0.45, 0);
this.mesh.add(this.leftHand);
this.rightHand = new THREE.Mesh(handGeometry, handMaterial);
this.rightHand.position.set(-0.9, 0.45, 0);
this.mesh.add(this.rightHand);
}
createLegs() {
const legGeometry = new THREE.CylinderGeometry(0.12, 0.15, 1.2, 8);
const legMaterial = new THREE.MeshStandardMaterial({
color: 0x2F4F4F // Pants color
});
// Left leg
this.leftLeg = new THREE.Mesh(legGeometry, legMaterial);
this.leftLeg.position.set(0.2, -0.6, 0);
this.leftLeg.castShadow = true;
this.mesh.add(this.leftLeg);
// Right leg
this.rightLeg = new THREE.Mesh(legGeometry, legMaterial);
this.rightLeg.position.set(-0.2, -0.6, 0);
this.rightLeg.castShadow = true;
this.mesh.add(this.rightLeg);
// Feet
const footGeometry = new THREE.BoxGeometry(0.25, 0.1, 0.4);
const footMaterial = new THREE.MeshStandardMaterial({
color: 0x000000 // Shoe color
});
this.leftFoot = new THREE.Mesh(footGeometry, footMaterial);
this.leftFoot.position.set(0.2, -1.25, 0.1);
this.mesh.add(this.leftFoot);
this.rightFoot = new THREE.Mesh(footGeometry, footMaterial);
this.rightFoot.position.set(-0.2, -1.25, 0.1);
this.mesh.add(this.rightFoot);
}
createFace() {
// Eyes - windows to the digital soul!
const eyeGeometry = new THREE.SphereGeometry(0.06, 12, 12);
const eyeMaterial = new THREE.MeshStandardMaterial({ color: 0x000000 });
this.leftEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
this.leftEye.position.set(0.15, 1.65, 0.35);
this.mesh.add(this.leftEye);
this.rightEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
this.rightEye.position.set(-0.15, 1.65, 0.35);
this.mesh.add(this.rightEye);
// Mouth - for smiling at potential matches!
const mouthGeometry = new THREE.TorusGeometry(0.1, 0.02, 8, 12, Math.PI);
const mouthMaterial = new THREE.MeshStandardMaterial({ color: 0xFF69B4 });
this.mouth = new THREE.Mesh(mouthGeometry, mouthMaterial);
this.mouth.position.set(0, 1.5, 0.38);
this.mouth.rotation.x = Math.PI / 2;
this.mesh.add(this.mouth);
// Eyebrows - for expressing interest (or skepticism)!
const browGeometry = new THREE.BoxGeometry(0.2, 0.02, 0.02);
const browMaterial = new THREE.MeshStandardMaterial({ color: 0x000000 });
this.leftBrow = new THREE.Mesh(browGeometry, browMaterial);
this.leftBrow.position.set(0.15, 1.75, 0.33);
this.leftBrow.rotation.z = Math.PI / 8;
this.mesh.add(this.leftBrow);
this.rightBrow = new THREE.Mesh(browGeometry, browMaterial);
this.rightBrow.position.set(-0.15, 1.75, 0.33);
this.rightBrow.rotation.z = -Math.PI / 8;
this.mesh.add(this.rightBrow);
}
createHair() {
// Basic hair - because bad hair days don't exist in 3D!
const hairGeometry = new THREE.SphereGeometry(0.45, 16, 16);
const hairMaterial = new THREE.MeshStandardMaterial({
color: this.options.hairColor,
roughness: 0.9
});
this.hair = new THREE.Mesh(hairGeometry, hairMaterial);
this.hair.position.y = 1.8;
this.hair.scale.set(1, 0.6, 1);
this.mesh.add(this.hair);
}
createClothing() {
// Add some clothing details because nudity is bad for business!
const collarGeometry = new THREE.TorusGeometry(0.45, 0.05, 8, 16, Math.PI);
const collarMaterial = new THREE.MeshStandardMaterial({ color: 0xFFFFFF });
this.collar = new THREE.Mesh(collarGeometry, collarMaterial);
this.collar.position.y = 1.1;
this.collar.rotation.x = Math.PI / 2;
this.mesh.add(this.collar);
}
// Animation methods - making our avatars come alive!
wave() {
console.log(`${this.options.name} is waving hello! 👋`);
const startTime = Date.now();
const originalRotation = this.rightArm.rotation.z;
const animateWave = () => {
const elapsed = Date.now() - startTime;
const progress = Math.sin(elapsed * 0.01) * 0.5;
this.rightArm.rotation.z = originalRotation + progress;
this.rightHand.rotation.z = progress * 2;
if (elapsed < 2000) { // Wave for 2 seconds
requestAnimationFrame(animateWave);
} else {
// Return to original position
this.rightArm.rotation.z = originalRotation;
this.rightHand.rotation.z = 0;
}
};
animateWave();
}
dance() {
console.log(`${this.options.name} is busting out some moves! 💃`);
const startTime = Date.now();
const animateDance = () => {
const elapsed = Date.now() - startTime;
const beat = elapsed * 0.01;
// Fun dance movements!
this.mesh.rotation.y = Math.sin(beat) * 0.3;
this.leftArm.rotation.z = Math.sin(beat) * 0.5;
this.rightArm.rotation.z = -Math.sin(beat) * 0.5;
this.leftLeg.rotation.x = Math.sin(beat + 1) * 0.3;
this.rightLeg.rotation.x = -Math.sin(beat + 1) * 0.3;
// Head bob
this.head.position.y = 1.6 + Math.sin(beat * 2) * 0.05;
if (elapsed < 5000) { // Dance for 5 seconds
requestAnimationFrame(animateDance);
} else {
// Reset positions
this.resetPose();
}
};
animateDance();
}
resetPose() {
// Return to default pose
this.mesh.rotation.y = 0;
this.leftArm.rotation.z = Math.PI / 6;
this.rightArm.rotation.z = -Math.PI / 6;
this.leftLeg.rotation.x = 0;
this.rightLeg.rotation.x = 0;
this.head.position.y = 1.6;
this.rightHand.rotation.z = 0;
this.leftHand.rotation.z = 0;
}
blush() {
// Make the avatar blush when complimented!
console.log(`${this.options.name} is blushing! 😊`);
const originalColor = this.head.material.color.getHex();
this.head.material.color.setHex(0xFFB6C1); // Pink color
setTimeout(() => {
this.head.material.color.setHex(originalColor);
}, 2000);
}
lookAt(target) {
// Make the avatar look at a specific point or another avatar
const headWorldPos = new THREE.Vector3();
this.head.getWorldPosition(headWorldPos);
const direction = new THREE.Vector3();
direction.subVectors(target, headWorldPos).normalize();
// Simple look-at for the head
this.head.rotation.y = Math.atan2(direction.x, direction.z);
}
}
Step 2: Avatar Customization Interface - Make Your Dream Date! 🎨
Now let's create an interface for users to customize their avatars:
<!-- Add this to your HTML file, right after the chat UI -->
<div id="avatarCustomization" style="display: none;">
<div style="background: white; padding: 20px; border-radius: 15px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1000; width: 400px;">
<h2>Create Your Avatar! ✨</h2>
<div style="margin: 10px 0;">
<label>Name:</label>
<input type="text" id="avatarName" placeholder="Your dating persona">
</div>
<div style="margin: 10px 0;">
<label>Gender:</label>
<select id="avatarGender">
<option value="male">Male</option>
<option value="female">Female</option>
<option value="neutral">Neutral</option>
</select>
</div>
<div style="margin: 10px 0;">
<label>Skin Tone:</label>
<input type="color" id="avatarSkinTone" value="#f0d9b5">
</div>
<div style="margin: 10px 0;">
<label>Hair Color:</label>
<input type="color" id="avatarHairColor" value="#8b4513">
</div>
<div style="margin: 10px 0;">
<label>Clothing Color:</label>
<input type="color" id="avatarClothingColor" value="#4169e1">
</div>
<button onclick="createCustomAvatar()" style="background: #ff6b6b; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer;">
Create My Avatar! 💕
</button>
</div>
</div>
Step 3: Updated DatingScene Class - Managing Our New Fancy Avatars! 🎭
Let's update our main DatingScene class to use our new Avatar system:
// Update your DatingScene class with these new methods
class DatingScene {
// ... (previous code remains the same)
addSampleAvatars() {
console.log("Adding sophisticated avatars... no more stick figures! 🎉");
// Create sample avatars with different characteristics
const sampleAvatars = [
{
name: "Alex",
gender: "male",
skinTone: 0xF0D9B5,
hairColor: 0x2C1810,
clothingColor: 0x2E8B57
},
{
name: "Sam",
gender: "neutral",
skinTone: 0xFFDBAC,
hairColor: 0xFFD700,
clothingColor: 0xFF69B4
},
{
name: "Taylor",
gender: "female",
skinTone: 0xE8B298,
hairColor: 0x8B4513,
clothingColor: 0x9370DB
}
];
sampleAvatars.forEach((avatarConfig, index) => {
const avatar = new Avatar(avatarConfig);
// Position avatars in a nice arc
const angle = (index / sampleAvatars.length) * Math.PI * 0.8 - Math.PI * 0.4;
const radius = 6;
avatar.mesh.position.set(
Math.sin(angle) * radius,
0,
Math.cos(angle) * radius - 3
);
// Make them face the center
avatar.mesh.rotation.y = -angle;
this.scene.add(avatar.mesh);
this.avatars.push(avatar);
// Add random animations to make the scene lively
this.addRandomBehaviors(avatar);
});
// Show customization interface for the user
setTimeout(() => {
this.showAvatarCustomization();
}, 1000);
}
showAvatarCustomization() {
document.getElementById('avatarCustomization').style.display = 'block';
}
createUserAvatar(config) {
this.currentUser = new Avatar(config);
this.currentUser.mesh.position.set(0, 0, 3);
this.scene.add(this.currentUser.mesh);
this.avatars.push(this.currentUser);
console.log(`Welcome, ${config.name}! Your avatar is ready to find love! 💖`);
// Hide customization UI
document.getElementById('avatarCustomization').style.display = 'none';
// Make other avatars look at the new user
this.avatars.forEach(avatar => {
if (avatar !== this.currentUser) {
const userPosition = this.currentUser.mesh.position.clone();
avatar.lookAt(userPosition);
// Make them wave at the new user
setTimeout(() => avatar.wave(), 1000);
}
});
}
addRandomBehaviors(avatar) {
// Randomly trigger animations to make avatars seem alive
setInterval(() => {
if (Math.random() > 0.7) { // 30% chance every interval
const behaviors = [() => avatar.wave(), () => avatar.dance()];
const randomBehavior = behaviors[Math.floor(Math.random() * behaviors.length)];
randomBehavior();
}
}, 10000); // Every 10 seconds
}
// New method to handle avatar interactions
handleAvatarClick(avatar) {
if (avatar === this.currentUser) return;
console.log(`You clicked on ${avatar.options.name}!`);
// Make the clicked avatar react
avatar.wave();
avatar.blush();
// Show info about the avatar
this.showAvatarInfo(avatar);
}
showAvatarInfo(avatar) {
// Create or update info display
let infoDiv = document.getElementById('avatarInfo');
if (!infoDiv) {
infoDiv = document.createElement('div');
infoDiv.id = 'avatarInfo';
infoDiv.style.cssText = `
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 10px;
max-width: 200px;
z-index: 100;
`;
document.getElementById('container').appendChild(infoDiv);
}
infoDiv.innerHTML = `
<h3>${avatar.options.name}</h3>
<p>Gender: ${avatar.options.gender}</p>
<p>Status: Looking for love! 💕</p>
<button onclick="datingScene.startChatWith('${avatar.options.name}')"
style="background: #ff6b6b; color: white; border: none; padding: 8px 15px; border-radius: 5px; cursor: pointer;">
Start Chat! 💬
</button>
`;
// Auto-hide after 10 seconds
setTimeout(() => {
if (infoDiv.parentNode) {
infoDiv.parentNode.removeChild(infoDiv);
}
}, 10000);
}
startChatWith(avatarName) {
const avatar = this.avatars.find(a => a.options.name === avatarName);
if (avatar) {
// Focus chat input
document.getElementById('messageInput').focus();
// Add welcome message
const chatMessages = document.getElementById('chatMessages');
const welcomeElement = document.createElement('div');
welcomeElement.textContent = `System: You started chatting with ${avatarName}! Say hello! 👋`;
welcomeElement.style.cssText = 'margin: 5px 0; padding: 8px; background: #e3f2fd; border-radius: 10px;';
chatMessages.appendChild(welcomeElement);
// Make avatar look at user
const userPosition = this.currentUser.mesh.position.clone();
avatar.lookAt(userPosition);
avatar.blush();
}
}
}
// Global function to create custom avatar
function createCustomAvatar() {
const name = document.getElementById('avatarName').value || 'Mysterious Dater';
const gender = document.getElementById('avatarGender').value;
const skinTone = parseInt(document.getElementById('avatarSkinTone').value.replace('#', '0x'));
const hairColor = parseInt(document.getElementById('avatarHairColor').value.replace('#', '0x'));
const clothingColor = parseInt(document.getElementById('avatarClothingColor').value.replace('#', '0x'));
const avatarConfig = {
name,
gender,
skinTone,
hairColor,
clothingColor
};
datingScene.createUserAvatar(avatarConfig);
}
// Add raycasting for avatar interaction
function setupAvatarInteraction() {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseClick(event) {
// Calculate mouse position in normalized device coordinates
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// Update the raycaster
raycaster.setFromCamera(mouse, datingScene.camera);
// Calculate objects intersecting the ray
const intersects = raycaster.intersectObjects(
datingScene.avatars.map(avatar => avatar.mesh),
true
);
if (intersects.length > 0) {
// Find which avatar was clicked
const clickedObject = intersects[0].object;
const clickedAvatar = datingScene.avatars.find(avatar =>
avatar.mesh === clickedObject.parent ||
avatar.mesh.children.includes(clickedObject)
);
if (clickedAvatar) {
datingScene.handleAvatarClick(clickedAvatar);
}
}
}
window.addEventListener('click', onMouseClick);
}
// Update the DOMContentLoaded event listener
window.addEventListener('DOMContentLoaded', () => {
datingScene = new DatingScene();
console.log("Dating scene initialized with fancy avatars! 💖");
// Setup avatar interaction
setupAvatarInteraction();
document.addEventListener('click', () => {
document.getElementById('messageInput').focus();
});
});
What We've Built in Part 2: 🎉
-
Sophisticated Avatar System: No more stick figures! We now have proper 3D avatars with:
- Detailed body parts (head, torso, arms, legs, hands, feet)
- Expressive faces (eyes, mouth, eyebrows)
- Customizable appearance (skin tone, hair color, clothing)
- Smooth animations (waving, dancing, blushing)
-
Avatar Customization: Users can create their own unique dating persona
-
Interactive Avatars: Click on avatars to learn about them and start chatting
-
Social Behaviors: Avatars automatically wave, dance, and look at each other
Key Features Explained: 🔑
- Modular Avatar Class: Easy to extend with new features and animations
- Raycasting: Enables clicking on 3D objects (avatars) in the scene
- Customization System: Users can personalize their appearance
- Animation System: Smooth, timed animations for natural movement
- Social Interactions: Avatars react to each other's presence
Next Time in Part 3: 🚀
We'll add:
- Avatar Movement: Walk around the dating scene!
- Camera Controls: Orbit around avatars for better viewing angles
- Proximity Chat: Automatically start chatting when avatars get close
- Emotes & Gestures: More ways to express yourself
- Better Environment: More interactive elements in our 3D world
Current Project Status: Our avatars now have personality and can interact! They're no longer lonely stick figures - they're ready to socialize and find digital love! 💕
Fun Fact: Our avatars now have more dating experience than most of us developers! They wave, dance, and blush on command - if only real dating were this easy! 😄
Part 3: Movement, Camera Controls & Proximity Chat - Making Moves in 3D! 🕺
Welcome back, digital cupid! Our avatars are looking fabulous, but they're just standing around like wallflowers at a middle school dance. Time to teach them how to move, groove, and get close enough for some meaningful conversation!
Step 1: Avatar Movement System - Strut Your Stuff! 🚶♂️
Let's add movement controls so users can navigate our virtual dating world:
// movement.js - Because love isn't a spectator sport!
class AvatarMovement {
constructor(avatar, scene) {
this.avatar = avatar;
this.scene = scene;
this.moveSpeed = 0.1;
this.isMoving = false;
this.targetPosition = null;
this.moveAnimationId = null;
// Movement state
this.movementState = {
forward: false,
backward: false,
left: false,
right: false
};
this.setupControls();
this.setupClickToMove();
console.log(`Movement system ready for ${avatar.options.name}! Let's go find love! 🚶♀️`);
}
setupControls() {
// Keyboard controls for smooth movement
document.addEventListener('keydown', (event) => this.handleKeyDown(event));
document.addEventListener('keyup', (event) => this.handleKeyUp(event));
// Touch controls for mobile devices
this.setupTouchControls();
}
handleKeyDown(event) {
if (!this.avatar.mesh.visible) return;
switch(event.key.toLowerCase()) {
case 'w':
case 'arrowup':
this.movementState.forward = true;
break;
case 's':
case 'arrowdown':
this.movementState.backward = true;
break;
case 'a':
case 'arrowleft':
this.movementState.left = true;
break;
case 'd':
case 'arrowright':
this.movementState.right = true;
break;
case ' ':
this.jump();
break;
case 'q':
this.avatar.dance();
break;
case 'e':
this.avatar.wave();
break;
}
this.updateMovement();
}
handleKeyUp(event) {
switch(event.key.toLowerCase()) {
case 'w':
case 'arrowup':
this.movementState.forward = false;
break;
case 's':
case 'arrowdown':
this.movementState.backward = false;
break;
case 'a':
case 'arrowleft':
this.movementState.left = false;
break;
case 'd':
case 'arrowright':
this.movementState.right = false;
break;
}
this.updateMovement();
}
updateMovement() {
const wasMoving = this.isMoving;
this.isMoving = Object.values(this.movementState).some(state => state);
if (this.isMoving) {
this.moveWithKeyboard();
if (!wasMoving) {
this.startWalkAnimation();
console.log(`${this.avatar.options.name} is on the move! 🚶♂️`);
}
} else if (wasMoving) {
this.stopWalkAnimation();
console.log(`${this.avatar.options.name} has arrived! 🛑`);
}
}
moveWithKeyboard() {
const moveVector = new THREE.Vector3(0, 0, 0);
if (this.movementState.forward) moveVector.z -= 1;
if (this.movementState.backward) moveVector.z += 1;
if (this.movementState.left) moveVector.x -= 1;
if (this.movementState.right) moveVector.x += 1;
if (moveVector.length() > 0) {
moveVector.normalize().multiplyScalar(this.moveSpeed);
// Update avatar position
this.avatar.mesh.position.add(moveVector);
// Rotate avatar to face movement direction
if (moveVector.length() > 0.001) {
const targetAngle = Math.atan2(moveVector.x, moveVector.z);
this.avatar.mesh.rotation.y = targetAngle;
}
// Check boundaries
this.enforceBoundaries();
// Check proximity to other avatars for chat
this.checkProximityChat();
}
}
setupClickToMove() {
// Click on ground to move to that position
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
document.addEventListener('click', (event) => {
if (event.target.tagName === 'INPUT' || event.target.tagName === 'BUTTON') return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, this.scene.camera);
// Only intersect with ground plane
const ground = this.scene.scene.children.find(child =>
child.geometry && child.geometry.type === 'PlaneGeometry'
);
if (ground) {
const intersects = raycaster.intersectObject(ground);
if (intersects.length > 0) {
const targetPos = intersects[0].point;
this.moveTo(targetPos);
// Show movement indicator
this.showMovementIndicator(targetPos);
}
}
});
}
moveTo(targetPosition) {
this.targetPosition = targetPosition.clone();
this.targetPosition.y = 0; // Keep on ground level
// Stop any existing movement
if (this.moveAnimationId) {
cancelAnimationFrame(this.moveAnimationId);
}
this.startWalkAnimation();
this.animateMoveToTarget();
}
animateMoveToTarget() {
if (!this.targetPosition) return;
const currentPos = this.avatar.mesh.position;
const direction = new THREE.Vector3()
.subVectors(this.targetPosition, currentPos)
.normalize();
const distance = currentPos.distanceTo(this.targetPosition);
if (distance > 0.1) {
// Move toward target
const moveStep = direction.multiplyScalar(Math.min(this.moveSpeed, distance));
currentPos.add(moveStep);
// Rotate to face movement direction
const targetAngle = Math.atan2(direction.x, direction.z);
this.avatar.mesh.rotation.y = targetAngle;
// Check proximity
this.checkProximityChat();
// Continue animation
this.moveAnimationId = requestAnimationFrame(() => this.animateMoveToTarget());
} else {
// Arrived at target
this.stopWalkAnimation();
this.targetPosition = null;
this.moveAnimationId = null;
console.log(`${this.avatar.options.name} has reached the destination! 🎯`);
}
}
startWalkAnimation() {
// Animate legs and arms for walking
if (this.walkAnimationId) return;
let walkTime = 0;
const animateWalk = () => {
walkTime += 0.2;
// Animate legs
this.avatar.leftLeg.rotation.x = Math.sin(walkTime) * 0.5;
this.avatar.rightLeg.rotation.x = -Math.sin(walkTime) * 0.5;
// Animate arms
this.avatar.leftArm.rotation.z = Math.PI / 6 + Math.sin(walkTime) * 0.3;
this.avatar.rightArm.rotation.z = -Math.PI / 6 - Math.sin(walkTime) * 0.3;
// Gentle head bob
this.avatar.head.position.y = 1.6 + Math.sin(walkTime * 2) * 0.03;
if (this.isMoving || this.targetPosition) {
this.walkAnimationId = requestAnimationFrame(animateWalk);
}
};
this.walkAnimationId = requestAnimationFrame(animateWalk);
}
stopWalkAnimation() {
if (this.walkAnimationId) {
cancelAnimationFrame(this.walkAnimationId);
this.walkAnimationId = null;
}
// Reset to default pose
this.avatar.resetPose();
}
jump() {
console.log(`${this.avatar.options.name} is jumping for joy! 🦘`);
const originalY = this.avatar.mesh.position.y;
const jumpHeight = 0.5;
const jumpDuration = 800; // ms
const startTime = Date.now();
const animateJump = () => {
const elapsed = Date.now() - startTime;
const progress = elapsed / jumpDuration;
if (progress < 1) {
// Parabolic jump curve
const jumpProgress = progress < 0.5 ? progress * 2 : (1 - progress) * 2;
this.avatar.mesh.position.y = originalY + Math.sin(jumpProgress * Math.PI) * jumpHeight;
requestAnimationFrame(animateJump);
} else {
this.avatar.mesh.position.y = originalY;
}
};
animateJump();
}
enforceBoundaries() {
// Keep avatars within the scene boundaries
const boundary = 20;
const pos = this.avatar.mesh.position;
pos.x = THREE.MathUtils.clamp(pos.x, -boundary, boundary);
pos.z = THREE.MathUtils.clamp(pos.z, -boundary, boundary);
}
showMovementIndicator(position) {
// Remove existing indicator
let indicator = this.scene.scene.getObjectByName('movementIndicator');
if (indicator) {
this.scene.scene.remove(indicator);
}
// Create new indicator
const circleGeometry = new THREE.RingGeometry(0.3, 0.4, 16);
const circleMaterial = new THREE.MeshBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.7,
side: THREE.DoubleSide
});
indicator = new THREE.Mesh(circleGeometry, circleMaterial);
indicator.rotation.x = -Math.PI / 2;
indicator.position.copy(position);
indicator.position.y += 0.1;
indicator.name = 'movementIndicator';
this.scene.scene.add(indicator);
// Animate and fade out
let opacity = 0.7;
const fadeOut = () => {
opacity -= 0.02;
circleMaterial.opacity = opacity;
if (opacity > 0) {
requestAnimationFrame(fadeOut);
} else {
this.scene.scene.remove(indicator);
}
};
setTimeout(() => fadeOut(), 500);
}
setupTouchControls() {
// Virtual joystick for mobile devices
const joystickContainer = document.createElement('div');
joystickContainer.style.cssText = `
position: fixed;
bottom: 100px;
left: 50px;
width: 120px;
height: 120px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.5);
z-index: 1000;
touch-action: none;
`;
const joystickKnob = document.createElement('div');
joystickKnob.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
transform: translate(-50%, -50%);
`;
joystickContainer.appendChild(joystickKnob);
document.getElementById('container').appendChild(joystickContainer);
// Touch event handlers
let isTouching = false;
joystickContainer.addEventListener('touchstart', (e) => {
e.preventDefault();
isTouching = true;
});
joystickContainer.addEventListener('touchmove', (e) => {
if (!isTouching) return;
e.preventDefault();
const touch = e.touches[0];
const rect = joystickContainer.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = touch.clientX - centerX;
const deltaY = touch.clientY - centerY;
// Calculate joystick position (clamped to circle)
const distance = Math.min(Math.sqrt(deltaX * deltaX + deltaY * deltaY), rect.width / 2);
const angle = Math.atan2(deltaY, deltaX);
const knobX = Math.cos(angle) * distance;
const knobY = Math.sin(angle) * distance;
joystickKnob.style.transform = `translate(calc(-50% + ${knobX}px), calc(-50% + ${knobY}px))`;
// Convert to movement
const normalizedX = knobX / (rect.width / 2);
const normalizedY = knobY / (rect.height / 2);
this.movementState.forward = normalizedY < -0.3;
this.movementState.backward = normalizedY > 0.3;
this.movementState.left = normalizedX < -0.3;
this.movementState.right = normalizedX > 0.3;
this.updateMovement();
});
joystickContainer.addEventListener('touchend', (e) => {
e.preventDefault();
isTouching = false;
joystickKnob.style.transform = 'translate(-50%, -50%)';
// Stop movement
this.movementState.forward = false;
this.movementState.backward = false;
this.movementState.left = false;
this.movementState.right = false;
this.updateMovement();
});
}
checkProximityChat() {
// Check if avatar is close enough to others to auto-start chat
const chatDistance = 3; // Distance threshold for auto-chat
this.scene.avatars.forEach(otherAvatar => {
if (otherAvatar === this.avatar) return;
const distance = this.avatar.mesh.position.distanceTo(otherAvatar.mesh.position);
if (distance < chatDistance && !this.scene.activeChatPartners.has(otherAvatar)) {
this.scene.startProximityChat(this.avatar, otherAvatar);
}
});
}
}
Step 2: Advanced Camera Controls - Get the Perfect Angle! 📷
Let's upgrade our camera system for better viewing:
// camera.js - Because everyone wants to look their best!
class DatingCamera {
constructor(camera, scene, targetAvatar) {
this.camera = camera;
this.scene = scene;
this.targetAvatar = targetAvatar;
this.modes = {
FOLLOW: 'follow',
ORBIT: 'orbit',
FIRST_PERSON: 'first_person',
FREE: 'free'
};
this.currentMode = this.modes.FOLLOW;
this.orbitDistance = 8;
this.orbitAngle = 0;
this.orbitHeight = 3;
this.setupCameraControls();
this.setupModeSwitcher();
console.log("Dating camera ready! Say cheese! 📸");
}
setupCameraControls() {
this.mouseState = {
isDown: false,
lastX: 0,
lastY: 0
};
// Mouse controls for orbit mode
document.addEventListener('mousedown', (e) => {
if (e.button === 2) { // Right click
this.mouseState.isDown = true;
this.mouseState.lastX = e.clientX;
this.mouseState.lastY = e.clientY;
}
});
document.addEventListener('mouseup', (e) => {
if (e.button === 2) {
this.mouseState.isDown = false;
}
});
document.addEventListener('mousemove', (e) => {
if (this.mouseState.isDown && this.currentMode === this.modes.ORBIT) {
const deltaX = e.clientX - this.mouseState.lastX;
const deltaY = e.clientY - this.mouseState.lastY;
this.orbitAngle += deltaX * 0.01;
this.orbitHeight = THREE.MathUtils.clamp(
this.orbitHeight + deltaY * 0.01,
1,
10
);
this.mouseState.lastX = e.clientX;
this.mouseState.lastY = e.clientY;
this.updateOrbitCamera();
}
});
// Prevent context menu on right click
document.addEventListener('contextmenu', (e) => e.preventDefault());
// Mouse wheel for zoom
document.addEventListener('wheel', (e) => {
e.preventDefault();
if (this.currentMode === this.modes.ORBIT) {
this.orbitDistance = THREE.MathUtils.clamp(
this.orbitDistance + e.deltaY * 0.01,
3,
20
);
this.updateOrbitCamera();
}
});
}
updateOrbitCamera() {
if (!this.targetAvatar) return;
const avatarPos = this.targetAvatar.mesh.position.clone();
const cameraX = avatarPos.x + Math.sin(this.orbitAngle) * this.orbitDistance;
const cameraZ = avatarPos.z + Math.cos(this.orbitAngle) * this.orbitDistance;
this.camera.position.set(cameraX, avatarPos.y + this.orbitHeight, cameraZ);
this.camera.lookAt(avatarPos.x, avatarPos.y + 1, avatarPos.z);
}
updateFollowCamera() {
if (!this.targetAvatar) return;
const avatarPos = this.targetAvatar.mesh.position.clone();
const behindDistance = 6;
const height = 4;
// Calculate position behind avatar
const direction = new THREE.Vector3(
Math.sin(this.targetAvatar.mesh.rotation.y),
0,
Math.cos(this.targetAvatar.mesh.rotation.y)
);
const cameraPos = avatarPos.clone()
.sub(direction.multiplyScalar(behindDistance))
.add(new THREE.Vector3(0, height, 0));
// Smooth camera movement
this.camera.position.lerp(cameraPos, 0.1);
this.camera.lookAt(avatarPos.x, avatarPos.y + 1, avatarPos.z);
}
setFirstPersonMode() {
if (!this.targetAvatar) return;
const avatarPos = this.targetAvatar.mesh.position.clone();
this.camera.position.set(
avatarPos.x,
avatarPos.y + 1.6, // Eye level
avatarPos.z
);
// Attach camera to avatar's head
this.camera.rotation.y = this.targetAvatar.mesh.rotation.y;
}
switchMode(mode) {
this.currentMode = mode;
console.log(`Camera switched to ${mode} mode! 🎥`);
switch(mode) {
case this.modes.FOLLOW:
break;
case this.modes.ORBIT:
this.updateOrbitCamera();
break;
case this.modes.FIRST_PERSON:
this.setFirstPersonMode();
break;
case this.modes.FREE:
// Free camera logic would go here
break;
}
}
setupModeSwitcher() {
// Create camera mode switcher UI
const cameraUI = document.createElement('div');
cameraUI.style.cssText = `
position: fixed;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 10px;
z-index: 100;
`;
cameraUI.innerHTML = `
<strong>Camera Mode:</strong><br>
<button onclick="datingCamera.switchMode('follow')">Follow 👥</button>
<button onclick="datingCamera.switchMode('orbit')">Orbit 🛸</button>
<button onclick="datingCamera.switchMode('first_person')">First Person 👀</button>
`;
document.getElementById('container').appendChild(cameraUI);
}
update() {
// Update camera based on current mode
switch(this.currentMode) {
case this.modes.FOLLOW:
this.updateFollowCamera();
break;
case this.modes.ORBIT:
// Orbit camera is updated by mouse controls
break;
case this.modes.FIRST_PERSON:
this.setFirstPersonMode();
break;
case this.modes.FREE:
// Free camera updates would go here
break;
}
}
setTarget(avatar) {
this.targetAvatar = avatar;
console.log(`Camera now following ${avatar.options.name}! 📍`);
}
}
Step 3: Proximity Chat System - Get Close and Personal! 💬
Now let's implement the proximity-based chat system:
// proximity-chat.js - Because love happens up close!
class ProximityChat {
constructor(scene) {
this.scene = scene;
this.chatDistance = 3;
this.activeChats = new Map(); // Map of chat pairs
this.chatBubbles = new Map(); // Visual chat indicators
console.log("Proximity chat system initialized! Get talking! 💕");
}
startProximityChat(avatar1, avatar2) {
const chatKey = this.getChatKey(avatar1, avatar2);
if (!this.activeChats.has(chatKey)) {
console.log(`Proximity chat started between ${avatar1.options.name} and ${avatar2.options.name}! 💬`);
this.activeChats.set(chatKey, {
avatar1,
avatar2,
startTime: Date.now(),
isActive: true
});
// Create visual chat indicators
this.createChatBubbles(avatar1, avatar2);
// Make avatars face each other
this.makeAvatarsFaceEachOther(avatar1, avatar2);
// Auto-start chat in UI
this.showProximityChatUI(avatar1, avatar2);
// Add to scene's active chat partners
this.scene.activeChatPartners.add(avatar1);
this.scene.activeChatPartners.add(avatar2);
}
}
stopProximityChat(avatar1, avatar2) {
const chatKey = this.getChatKey(avatar1, avatar2);
if (this.activeChats.has(chatKey)) {
console.log(`Proximity chat ended between ${avatar1.options.name} and ${avatar2.options.name} 👋`);
this.activeChats.delete(chatKey);
this.removeChatBubbles(avatar1, avatar2);
// Remove from active chat partners
this.scene.activeChatPartners.delete(avatar1);
this.scene.activeChatPartners.delete(avatar2);
// Show chat ended message
this.showChatEndedMessage(avatar1, avatar2);
}
}
getChatKey(avatar1, avatar2) {
// Create unique key for chat pair (order doesn't matter)
const ids = [avatar1.options.name, avatar2.options.name].sort();
return ids.join('_');
}
createChatBubbles(avatar1, avatar2) {
// Create speech bubble indicators above avatars' heads
[avatar1, avatar2].forEach(avatar => {
const bubbleGeometry = new THREE.SphereGeometry(0.3, 8, 6);
const bubbleMaterial = new THREE.MeshBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.7
});
const chatBubble = new THREE.Mesh(bubbleGeometry, bubbleMaterial);
chatBubble.position.y = 2.5;
chatBubble.name = 'chatBubble';
avatar.mesh.add(chatBubble);
this.chatBubbles.set(avatar, chatBubble);
// Animate the bubble
this.animateChatBubble(chatBubble);
});
}
animateChatBubble(bubble) {
let scale = 1;
let growing = false;
const animate = () => {
if (bubble.parent) { // Check if bubble still exists
scale += growing ? 0.02 : -0.02;
if (scale >= 1.2) growing = false;
if (scale <= 0.8) growing = true;
bubble.scale.set(scale, scale, scale);
requestAnimationFrame(animate);
}
};
animate();
}
removeChatBubbles(avatar1, avatar2) {
[avatar1, avatar2].forEach(avatar => {
const bubble = this.chatBubbles.get(avatar);
if (bubble && bubble.parent) {
bubble.parent.remove(bubble);
}
this.chatBubbles.delete(avatar);
});
}
makeAvatarsFaceEachOther(avatar1, avatar2) {
const pos1 = avatar1.mesh.position;
const pos2 = avatar2.mesh.position;
// Calculate direction vectors
const dir1 = new THREE.Vector3().subVectors(pos2, pos1).normalize();
const dir2 = new THREE.Vector3().subVectors(pos1, pos2).normalize();
// Set rotations to face each other
avatar1.mesh.rotation.y = Math.atan2(dir1.x, dir1.z);
avatar2.mesh.rotation.y = Math.atan2(dir2.x, dir2.z);
}
showProximityChatUI(avatar1, avatar2) {
const chatMessages = document.getElementById('chatMessages');
const systemMessage = document.createElement('div');
systemMessage.style.cssText = `
margin: 10px 0;
padding: 10px;
background: #e8f5e8;
border-radius: 10px;
text-align: center;
font-weight: bold;
`;
systemMessage.textContent = `💕 You're now chatting with ${avatar2.options.name}! Say hello!`;
chatMessages.appendChild(systemMessage);
chatMessages.scrollTop = chatMessages.scrollHeight;
// Update chat header
const chatHeader = document.querySelector('#chatUI h3') || (() => {
const h3 = document.createElement('h3');
h3.style.margin = '0 0 10px 0';
h3.style.color = '#ff6b6b';
document.getElementById('chatUI').insertBefore(h3, document.getElementById('chatUI').firstChild);
return h3;
})();
chatHeader.textContent = `💬 Chatting with ${avatar2.options.name}`;
}
showChatEndedMessage(avatar1, avatar2) {
const chatMessages = document.getElementById('chatMessages');
const systemMessage = document.createElement('div');
systemMessage.style.cssText = `
margin: 10px 0;
padding: 10px;
background: #ffebee;
border-radius: 10px;
text-align: center;
font-style: italic;
`;
systemMessage.textContent = `👋 ${avatar2.options.name} moved away. Chat ended.`;
chatMessages.appendChild(systemMessage);
chatMessages.scrollTop = chatMessages.scrollHeight;
// Reset chat header
const chatHeader = document.querySelector('#chatUI h3');
if (chatHeader) {
chatHeader.textContent = '💬 Dating Chat';
}
}
update() {
// Check all avatar pairs for proximity
for (let i = 0; i < this.scene.avatars.length; i++) {
for (let j = i + 1; j < this.scene.avatars.length; j++) {
const avatar1 = this.scene.avatars[i];
const avatar2 = this.scene.avatars[j];
const distance = avatar1.mesh.position.distanceTo(avatar2.mesh.position);
const chatKey = this.getChatKey(avatar1, avatar2);
if (distance < this.chatDistance) {
if (!this.activeChats.has(chatKey)) {
this.startProximityChat(avatar1, avatar2);
}
} else {
if (this.activeChats.has(chatKey)) {
this.stopProximityChat(avatar1, avatar2);
}
}
}
}
}
}
Step 4: Updated DatingScene Class - Bringing It All Together! 🔗
Now let's update our main DatingScene class to integrate all these new systems:
// Update the DatingScene class with new properties and methods
class DatingScene {
constructor() {
// ... existing properties ...
// New properties
this.activeChatPartners = new Set();
this.proximityChat = null;
this.datingCamera = null;
this.init();
}
init() {
this.createScene();
this.createCamera();
this.createRenderer();
this.createLights();
this.createEnvironment();
// Initialize new systems
this.proximityChat = new ProximityChat(this);
this.animate();
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';
document.getElementById('chatUI').style.display = 'block';
this.addSampleAvatars();
}, 2000);
}
createUserAvatar(config) {
this.currentUser = new Avatar(config);
this.currentUser.mesh.position.set(0, 0, 3);
this.scene.add(this.currentUser.mesh);
this.avatars.push(this.currentUser);
// Initialize movement system for user avatar
this.currentUser.movement = new AvatarMovement(this.currentUser, this);
// Set up camera to follow user avatar
this.datingCamera = new DatingCamera(this.camera, this, this.currentUser);
console.log(`Welcome, ${config.name}! You're ready to mingle! 💖`);
document.getElementById('avatarCustomization').style.display = 'none';
// Make other avatars acknowledge the new user
this.avatars.forEach(avatar => {
if (avatar !== this.currentUser) {
const userPosition = this.currentUser.mesh.position.clone();
avatar.lookAt(userPosition);
setTimeout(() => avatar.wave(), 1000);
}
});
}
addSampleAvatars() {
// ... existing sample avatar creation ...
sampleAvatars.forEach((avatarConfig, index) => {
const avatar = new Avatar(avatarConfig);
// Position avatars
const angle = (index / sampleAvatars.length) * Math.PI * 0.8 - Math.PI * 0.4;
const radius = 6;
avatar.mesh.position.set(
Math.sin(angle) * radius,
0,
Math.cos(angle) * radius - 3
);
avatar.mesh.rotation.y = -angle;
this.scene.add(avatar.mesh);
this.avatars.push(avatar);
// Add random movement to sample avatars
this.addRandomAvatarMovement(avatar);
this.addRandomBehaviors(avatar);
});
setTimeout(() => {
this.showAvatarCustomization();
}, 1000);
}
addRandomAvatarMovement(avatar) {
// Make sample avatars wander around randomly
setInterval(() => {
if (Math.random() > 0.8 && !this.activeChatPartners.has(avatar)) {
const randomX = (Math.random() - 0.5) * 30;
const randomZ = (Math.random() - 0.5) * 30;
avatar.movement.moveTo(new THREE.Vector3(randomX, 0, randomZ));
}
}, 8000);
}
animate() {
requestAnimationFrame(() => this.animate());
// Update camera
if (this.datingCamera) {
this.datingCamera.update();
}
// Update proximity chat
if (this.proximityChat) {
this.proximityChat.update();
}
// Rotate decorative heart
const heart = this.scene.children.find(child =>
child.geometry && child.geometry.type === 'ExtrudeGeometry'
);
if (heart) {
heart.rotation.y += 0.01;
}
this.renderer.render(this.scene, this.camera);
}
// Update the existing startChatWith method
startChatWith(avatarName) {
const avatar = this.avatars.find(a => a.options.name === avatarName);
if (avatar && this.currentUser) {
// Move user avatar to the target avatar
const targetPos = avatar.mesh.position.clone();
const direction = new THREE.Vector3()
.subVectors(targetPos, this.currentUser.mesh.position)
.normalize();
const chatDistance = 2;
const approachPos = targetPos.clone().sub(direction.multiplyScalar(chatDistance));
this.currentUser.movement.moveTo(approachPos);
// Focus chat input
document.getElementById('messageInput').focus();
// Show welcome message
const chatMessages = document.getElementById('chatMessages');
const welcomeElement = document.createElement('div');
welcomeElement.textContent = `System: You're approaching ${avatarName}! Get ready to chat! 🚶♂️`;
welcomeElement.style.cssText = 'margin: 5px 0; padding: 8px; background: #e3f2fd; border-radius: 10px;';
chatMessages.appendChild(welcomeElement);
}
}
}
// Update global variables
let datingScene;
let datingCamera;
// Update the DOMContentLoaded event listener
window.addEventListener('DOMContentLoaded', () => {
datingScene = new DatingScene();
console.log("3D Dating World fully loaded! Ready for romance! 💕");
setupAvatarInteraction();
document.addEventListener('click', () => {
document.getElementById('messageInput').focus();
});
});
What We've Built in Part 3: 🎉
-
Smooth Avatar Movement:
- Keyboard controls (WASD/Arrows)
- Click-to-move on ground
- Mobile touch controls with virtual joystick
- Walking animations with leg and arm movement
-
Advanced Camera System:
- Multiple camera modes (Follow, Orbit, First Person)
- Mouse controls for orbit camera
- Smooth transitions and following
-
Proximity Chat:
- Automatic chat when avatars get close
- Visual chat bubbles above avatars
- Auto-facing when chatting
- Smart chat management
-
Enhanced Interactions:
- Jumping and dancing animations
- Movement boundaries
- Visual movement indicators
- Improved social behaviors
Key Features Explained: 🔑
- Movement System: Combines keyboard, mouse, and touch inputs for universal control
- Camera Modes: Different perspectives for different situations
- Proximity Detection: Real-time distance checking between avatars
- Animation Blending: Smooth transitions between idle and movement states
- Mobile Support: Touch controls and responsive design
Next Time in Part 4: 🚀
We'll add:
- Voice Chat: Real-time audio communication
- Emotes & Gestures: More ways to express emotions
- Environment Interactions: Sit on benches, dance on platforms
- Day/Night Cycle: Romantic lighting changes
- Special Effects: Particles, lighting, and ambiance
Current Project Status: Our dating world is now alive with movement and interaction! Avatars can navigate the space, get close to chat, and view everything from multiple camera angles. The virtual dating experience is getting real! 💕
Pro Tip: If your avatar keeps walking into walls, that's actually pretty realistic first-date behavior! Don't worry - it adds character! 😄
Part 4: Voice Chat, Emotes & Environment Interactions - Express Yourself! 🎤💫
Welcome back, digital romance architect! Our avatars can now move and chat, but they're about as expressive as a potato. Time to add voice chat, animated emotes, and interactive environments that'll make our virtual dating world truly come alive!
Step 1: Voice Chat System - Hear the Love! 🎤
Let's implement real-time voice communication between avatars:
// voice-chat.js - Because sometimes text just isn't enough!
class VoiceChatSystem {
constructor(scene) {
this.scene = scene;
this.isRecording = false;
this.mediaRecorder = null;
this.audioChunks = [];
this.audioContext = null;
this.audioElements = new Map(); // Avatar -> Audio element mapping
this.voiceRange = 5; // Distance for voice falloff
this.setupAudioContext();
this.setupVoiceUI();
console.log("Voice chat system ready! Prepare for some sweet nothings! 💖");
}
async setupAudioContext() {
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Request microphone permission
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
this.setupMediaRecorder(stream);
console.log("Microphone access granted! You're now audible and dateable! 🎤");
} catch (error) {
console.warn("Microphone access denied. Text-only mode activated! 📝", error);
this.showMicrophoneError();
}
}
setupMediaRecorder(stream) {
this.mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
});
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.audioChunks.push(event.data);
}
};
this.mediaRecorder.onstop = () => {
this.sendVoiceMessage();
};
}
setupVoiceUI() {
// Add voice chat controls to the UI
const voiceControls = document.createElement('div');
voiceControls.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 100;
`;
voiceControls.innerHTML = `
<button id="voiceToggle" style="
background: #ff6b6b;
color: white;
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
transition: all 0.3s ease;
">
🎤
</button>
<div id="voiceIndicator" style="
display: none;
margin-top: 10px;
padding: 10px;
background: rgba(255, 107, 107, 0.9);
color: white;
border-radius: 20px;
text-align: center;
font-weight: bold;
">
🔊 Speaking...
</div>
`;
document.getElementById('container').appendChild(voiceControls);
// Voice toggle button
const voiceToggle = document.getElementById('voiceToggle');
const voiceIndicator = document.getElementById('voiceIndicator');
voiceToggle.addEventListener('mousedown', () => this.startRecording());
voiceToggle.addEventListener('mouseup', () => this.stopRecording());
voiceToggle.addEventListener('touchstart', (e) => {
e.preventDefault();
this.startRecording();
});
voiceToggle.addEventListener('touchend', (e) => {
e.preventDefault();
this.stopRecording();
});
// Visual feedback
voiceToggle.addEventListener('mousedown', () => {
voiceToggle.style.transform = 'scale(0.9)';
voiceToggle.style.background = '#ff5252';
});
voiceToggle.addEventListener('mouseup', () => {
voiceToggle.style.transform = 'scale(1)';
voiceToggle.style.background = '#ff6b6b';
});
}
startRecording() {
if (!this.mediaRecorder || this.isRecording) return;
this.audioChunks = [];
this.isRecording = true;
this.mediaRecorder.start(100); // Collect data every 100ms
// Visual feedback
document.getElementById('voiceIndicator').style.display = 'block';
document.getElementById('voiceToggle').textContent = '🔴';
// Show speaking indicator above user avatar
this.showSpeakingIndicator(this.scene.currentUser, true);
console.log("🎤 Recording started... Time to charm someone!");
}
stopRecording() {
if (!this.mediaRecorder || !this.isRecording) return;
this.isRecording = false;
this.mediaRecorder.stop();
// Visual feedback
document.getElementById('voiceIndicator').style.display = 'none';
document.getElementById('voiceToggle').textContent = '🎤';
// Hide speaking indicator
this.showSpeakingIndicator(this.scene.currentUser, false);
}
async sendVoiceMessage() {
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm;codecs=opus' });
// In a real app, you'd send this to your server and then to other clients
// For this demo, we'll simulate sending to nearby avatars
const nearbyAvatars = this.getNearbyAvatars();
if (nearbyAvatars.length > 0) {
const audioUrl = URL.createObjectURL(audioBlob);
this.broadcastVoiceMessage(audioUrl, nearbyAvatars);
// Show voice message in chat
this.showVoiceMessageInChat(audioUrl);
}
}
getNearbyAvatars() {
if (!this.scene.currentUser) return [];
return this.scene.avatars.filter(avatar => {
if (avatar === this.scene.currentUser) return false;
const distance = this.scene.currentUser.mesh.position.distanceTo(avatar.mesh.position);
return distance <= this.voiceRange;
});
}
broadcastVoiceMessage(audioUrl, avatars) {
avatars.forEach(avatar => {
this.playVoiceMessage(audioUrl, avatar);
});
}
playVoiceMessage(audioUrl, targetAvatar) {
const audio = new Audio(audioUrl);
// Apply spatial audio effects based on distance and position
this.applySpatialAudio(audio, targetAvatar);
// Show speaking indicator on target avatar
this.showSpeakingIndicator(targetAvatar, true);
audio.play().then(() => {
// Hide indicator when audio ends
audio.onended = () => {
this.showSpeakingIndicator(targetAvatar, false);
};
}).catch(error => {
console.warn("Could not play voice message:", error);
this.showSpeakingIndicator(targetAvatar, false);
});
// Store audio element for cleanup
this.audioElements.set(targetAvatar, audio);
}
applySpatialAudio(audio, targetAvatar) {
if (!this.scene.currentUser) return;
const userAvatar = this.scene.currentUser;
const distance = userAvatar.mesh.position.distanceTo(targetAvatar.mesh.position);
// Calculate volume based on distance (inverse square law)
const maxVolume = 1.0;
const minVolume = 0.1;
let volume = maxVolume / (distance * distance);
volume = Math.max(minVolume, Math.min(maxVolume, volume));
audio.volume = volume;
// Calculate panning based on relative position
const direction = new THREE.Vector3()
.subVectors(targetAvatar.mesh.position, userAvatar.mesh.position)
.normalize();
// Transform direction to camera space for stereo panning
const camera = this.scene.camera;
const worldDirection = direction.applyMatrix4(camera.matrixWorldInverse);
// Simple stereo panning based on x coordinate
const pan = THREE.MathUtils.clamp(worldDirection.x * 2, -1, 1);
// Apply panning using Web Audio API if available
if (this.audioContext) {
const source = this.audioContext.createMediaElementSource(audio);
const panner = this.audioContext.createStereoPanner();
panner.pan.value = pan;
source.connect(panner);
panner.connect(this.audioContext.destination);
}
}
showSpeakingIndicator(avatar, isSpeaking) {
// Remove existing indicator
let indicator = avatar.mesh.getObjectByName('speakingIndicator');
if (indicator) {
avatar.mesh.remove(indicator);
}
if (isSpeaking) {
// Create speaking indicator (sound waves)
const group = new THREE.Group();
group.name = 'speakingIndicator';
// Create concentric circles for sound waves
for (let i = 0; i < 3; i++) {
const circleGeometry = new THREE.RingGeometry(0.2 + i * 0.1, 0.3 + i * 0.1, 16);
const circleMaterial = new THREE.MeshBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.6 - i * 0.2,
side: THREE.DoubleSide
});
const circle = new THREE.Mesh(circleGeometry, circleMaterial);
circle.rotation.x = -Math.PI / 2;
circle.position.y = 2.2;
group.add(circle);
}
avatar.mesh.add(group);
// Animate the indicator
this.animateSpeakingIndicator(group);
}
}
animateSpeakingIndicator(group) {
let scale = 1;
let time = 0;
const animate = () => {
if (group.parent) { // Check if still attached
time += 0.1;
scale = 1 + Math.sin(time) * 0.2;
group.children.forEach((circle, index) => {
circle.scale.set(scale + index * 0.1, scale + index * 0.1, 1);
});
requestAnimationFrame(animate);
}
};
animate();
}
showVoiceMessageInChat(audioUrl) {
const chatMessages = document.getElementById('chatMessages');
const messageElement = document.createElement('div');
messageElement.style.cssText = `
margin: 10px 0;
padding: 10px;
background: #e3f2fd;
border-radius: 15px;
display: flex;
align-items: center;
gap: 10px;
`;
messageElement.innerHTML = `
<span style="font-weight: bold; color: #ff6b6b;">You:</span>
<audio controls style="flex: 1; height: 30px;">
<source src="${audioUrl}" type="audio/webm">
Your browser does not support audio playback.
</audio>
<span style="font-size: 12px; color: #666;">🎤 Voice</span>
`;
chatMessages.appendChild(messageElement);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
showMicrophoneError() {
const errorDiv = document.createElement('div');
errorDiv.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 0, 0, 0.9);
color: white;
padding: 20px;
border-radius: 10px;
z-index: 1000;
text-align: center;
`;
errorDiv.innerHTML = `
<h3>🎤 Microphone Access Required</h3>
<p>Please allow microphone access to use voice chat features.</p>
<p><small>Your browser may be blocking microphone access. Check your permissions.</small></p>
<button onclick="this.parentElement.remove()" style="
background: white;
color: #ff0000;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
">
OK
</button>
`;
document.getElementById('container').appendChild(errorDiv);
}
cleanup() {
// Clean up audio elements and URLs
this.audioElements.forEach((audio, avatar) => {
audio.pause();
URL.revokeObjectURL(audio.src);
});
this.audioElements.clear();
}
}
Step 2: Emotes & Gestures System - Express Those Feelings! 💃
Let's create a comprehensive emote system with animations and visual effects:
// emotes.js - Because sometimes you need to floss to express yourself! 💫
class EmoteSystem {
constructor(scene) {
this.scene = scene;
this.activeEmotes = new Map();
this.emoteQueue = new Map();
this.availableEmotes = {
wave: { name: "Wave", duration: 2000, animation: 'wave' },
dance: { name: "Dance", duration: 5000, animation: 'dance' },
heart: { name: "Send Heart", duration: 3000, effect: 'heart' },
laugh: { name: "Laugh", duration: 3000, animation: 'laugh' },
kiss: { name: "Blow Kiss", duration: 2500, effect: 'kiss' },
celebrate: { name: "Celebrate", duration: 4000, animation: 'celebrate', effect: 'confetti' },
shy: { name: "Shy", duration: 2000, animation: 'shy' },
point: { name: "Point", duration: 2000, animation: 'point' }
};
this.setupEmoteUI();
console.log("Emote system loaded! Get ready to express yourself! 💕");
}
setupEmoteUI() {
// Create emote wheel or quick access buttons
const emoteContainer = document.createElement('div');
emoteContainer.style.cssText = `
position: fixed;
bottom: 100px;
right: 20px;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 15px;
z-index: 100;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
gap: 10px;
max-width: 300px;
`;
emoteContainer.innerHTML = `
<h4 style="margin: 0 0 10px 0; color: #ff6b6b;">Expressions 💫</h4>
<div id="emoteButtons" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;">
${Object.entries(this.availableEmotes).map(([key, emote]) => `
<button onclick="emoteSystem.playEmote('${key}')"
style="padding: 10px; border: none; border-radius: 10px; background: #f8f9fa; cursor: pointer; transition: all 0.3s ease;"
onmouseover="this.style.background='#ffebee'"
onmouseout="this.style.background='#f8f9fa'"
title="${emote.name}">
${this.getEmoteIcon(key)}
</button>
`).join('')}
</div>
`;
document.getElementById('container').appendChild(emoteContainer);
}
getEmoteIcon(emoteKey) {
const icons = {
wave: '👋',
dance: '💃',
heart: '💖',
laugh: '😂',
kiss: '😘',
celebrate: '🎉',
shy: '😊',
point: '👉'
};
return icons[emoteKey] || '💫';
}
playEmote(emoteKey, avatar = null) {
const targetAvatar = avatar || this.scene.currentUser;
if (!targetAvatar) return;
const emote = this.availableEmotes[emoteKey];
if (!emote) {
console.warn(`Unknown emote: ${emoteKey}`);
return;
}
// Queue emote if avatar is already performing one
if (this.activeEmotes.has(targetAvatar)) {
if (!this.emoteQueue.has(targetAvatar)) {
this.emoteQueue.set(targetAvatar, []);
}
this.emoteQueue.get(targetAvatar).push(emoteKey);
return;
}
this.executeEmote(targetAvatar, emoteKey, emote);
}
executeEmote(avatar, emoteKey, emote) {
console.log(`${avatar.options.name} is ${emote.name.toLowerCase()}! ${this.getEmoteIcon(emoteKey)}`);
this.activeEmotes.set(avatar, emoteKey);
// Play animation if available
if (emote.animation && avatar[emote.animation]) {
avatar[emote.animation]();
}
// Show visual effect if available
if (emote.effect) {
this.playVisualEffect(avatar, emote.effect);
}
// Show emote text above avatar
this.showEmoteText(avatar, emote.name);
// Show in chat
this.showEmoteInChat(avatar, emoteKey);
// Set timeout to end emote
setTimeout(() => {
this.endEmote(avatar);
}, emote.duration);
}
endEmote(avatar) {
this.activeEmotes.delete(avatar);
// Reset to default pose
if (avatar.resetPose) {
avatar.resetPose();
}
// Check for queued emotes
if (this.emoteQueue.has(avatar) && this.emoteQueue.get(avatar).length > 0) {
const nextEmoteKey = this.emoteQueue.get(avatar).shift();
const nextEmote = this.availableEmotes[nextEmoteKey];
setTimeout(() => {
this.executeEmote(avatar, nextEmoteKey, nextEmote);
}, 500);
}
}
playVisualEffect(avatar, effectType) {
const position = avatar.mesh.position.clone();
position.y += 2; // Above avatar's head
switch(effectType) {
case 'heart':
this.createHeartEffect(position);
break;
case 'kiss':
this.createKissEffect(position);
break;
case 'confetti':
this.createConfettiEffect(position);
break;
}
}
createHeartEffect(position) {
const heartGroup = new THREE.Group();
// Create multiple hearts
for (let i = 0; i < 5; i++) {
const heartShape = new THREE.Shape();
heartShape.moveTo(0, 0);
heartShape.bezierCurveTo(0.2, 0.2, 0.3, 0, 0, -0.3);
heartShape.bezierCurveTo(-0.3, 0, -0.2, 0.2, 0, 0);
const heartGeometry = new THREE.ExtrudeGeometry(heartShape, {
depth: 0.05,
bevelEnabled: true,
bevelSegments: 2,
bevelSize: 0.02,
bevelThickness: 0.02
});
const heartMaterial = new THREE.MeshBasicMaterial({
color: 0xff6b6b,
transparent: true,
opacity: 0.8
});
const heart = new THREE.Mesh(heartGeometry, heartMaterial);
// Random position around avatar
heart.position.set(
(Math.random() - 0.5) * 2,
Math.random() * 2,
(Math.random() - 0.5) * 2
);
// Random scale
const scale = 0.1 + Math.random() * 0.1;
heart.scale.set(scale, scale, scale);
heartGroup.add(heart);
}
heartGroup.position.copy(position);
this.scene.scene.add(heartGroup);
// Animate hearts floating up and fading out
let opacity = 0.8;
const startY = position.y;
const animate = () => {
opacity -= 0.02;
heartGroup.position.y += 0.05;
heartGroup.children.forEach(heart => {
heart.material.opacity = opacity;
heart.rotation.y += 0.05;
});
if (opacity > 0 && heartGroup.position.y < startY + 3) {
requestAnimationFrame(animate);
} else {
this.scene.scene.remove(heartGroup);
}
};
animate();
}
createKissEffect(position) {
const kissGroup = new THREE.Group();
// Create kiss particles (small hearts)
for (let i = 0; i < 8; i++) {
const heartGeometry = new THREE.SphereGeometry(0.05, 8, 6);
const heartMaterial = new THREE.MeshBasicMaterial({
color: 0xff69b4,
transparent: true,
opacity: 0.7
});
const heart = new THREE.Mesh(heartGeometry, heartMaterial);
// Position in an arc
const angle = (i / 8) * Math.PI;
const radius = 0.5;
heart.position.set(
Math.sin(angle) * radius,
Math.cos(angle) * radius * 0.5,
0
);
kissGroup.add(heart);
}
kissGroup.position.copy(position);
this.scene.scene.add(kissGroup);
// Animate kiss floating forward
let progress = 0;
const startPos = position.clone();
const animate = () => {
progress += 0.03;
kissGroup.position.lerpVectors(startPos,
new THREE.Vector3(startPos.x, startPos.y, startPos.z - 3), progress);
kissGroup.children.forEach((heart, index) => {
heart.material.opacity = 0.7 * (1 - progress);
heart.scale.setScalar(1 + progress);
});
if (progress < 1) {
requestAnimationFrame(animate);
} else {
this.scene.scene.remove(kissGroup);
}
};
animate();
}
createConfettiEffect(position) {
const confettiGroup = new THREE.Group();
const colors = [0xff6b6b, 0x4ecdc4, 0xffd166, 0x06d6a0, 0x118ab2];
// Create confetti pieces
for (let i = 0; i < 20; i++) {
const confettiGeometry = new THREE.PlaneGeometry(0.1, 0.1);
const confettiMaterial = new THREE.MeshBasicMaterial({
color: colors[Math.floor(Math.random() * colors.length)],
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide
});
const confetti = new THREE.Mesh(confettiGeometry, confettiMaterial);
// Random starting position
confetti.position.set(
(Math.random() - 0.5) * 2,
Math.random() * 1,
(Math.random() - 0.5) * 2
);
// Random rotation
confetti.rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI
);
confetti.userData = {
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 0.02,
Math.random() * 0.03 + 0.02,
(Math.random() - 0.5) * 0.02
),
rotationSpeed: new THREE.Vector3(
(Math.random() - 0.5) * 0.1,
(Math.random() - 0.5) * 0.1,
(Math.random() - 0.5) * 0.1
)
};
confettiGroup.add(confetti);
}
confettiGroup.position.copy(position);
this.scene.scene.add(confettiGroup);
// Animate confetti falling
let time = 0;
const animate = () => {
time += 0.1;
confettiGroup.children.forEach(confetti => {
// Update position
confetti.position.add(confetti.userData.velocity);
// Update rotation
confetti.rotation.x += confetti.userData.rotationSpeed.x;
confetti.rotation.y += confetti.userData.rotationSpeed.y;
confetti.rotation.z += confetti.userData.rotationSpeed.z;
// Apply gravity
confetti.userData.velocity.y -= 0.001;
// Fade out
confetti.material.opacity = 0.8 * (1 - time / 3);
});
if (time < 3) {
requestAnimationFrame(animate);
} else {
this.scene.scene.remove(confettiGroup);
}
};
animate();
}
showEmoteText(avatar, emoteName) {
// Remove existing emote text
let existingText = avatar.mesh.getObjectByName('emoteText');
if (existingText) {
avatar.mesh.remove(existingText);
}
// Create text sprite (simplified version - in production, use proper text geometry)
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 64;
const context = canvas.getContext('2d');
// Draw text background
context.fillStyle = 'rgba(255, 107, 107, 0.9)';
context.fillRect(0, 0, canvas.width, canvas.height);
// Draw text
context.fillStyle = 'white';
context.font = 'bold 24px Arial';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(emoteName, canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(material);
sprite.name = 'emoteText';
sprite.position.y = 2.5;
sprite.scale.set(2, 0.5, 1);
avatar.mesh.add(sprite);
// Auto-remove after 3 seconds
setTimeout(() => {
if (sprite.parent) {
sprite.parent.remove(sprite);
}
}, 3000);
}
showEmoteInChat(avatar, emoteKey) {
const chatMessages = document.getElementById('chatMessages');
const messageElement = document.createElement('div');
messageElement.style.cssText = `
margin: 5px 0;
padding: 8px;
background: #fff3e0;
border-radius: 15px;
text-align: center;
font-style: italic;
`;
const emoteIcon = this.getEmoteIcon(emoteKey);
const emoteName = this.availableEmotes[emoteKey].name;
messageElement.textContent = `${avatar.options.name} ${emoteName.toLowerCase()}s! ${emoteIcon}`;
chatMessages.appendChild(messageElement);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
// Add new animation methods to the Avatar class
Avatar.prototype.laugh = function() {
console.log(`${this.options.name} is having a good laugh! 😂`);
const startTime = Date.now();
const animateLaugh = () => {
const elapsed = Date.now() - startTime;
const progress = Math.sin(elapsed * 0.01) * 0.3;
// Head bob for laughter
this.head.position.y = 1.6 + progress * 0.1;
// Body shake
this.mesh.rotation.z = progress * 0.1;
if (elapsed < 3000) {
requestAnimationFrame(animateLaugh);
} else {
this.resetPose();
}
};
animateLaugh();
};
Avatar.prototype.celebrate = function() {
console.log(`${this.options.name} is celebrating! 🎉`);
const startTime = Date.now();
const animateCelebrate = () => {
const elapsed = Date.now() - startTime;
const progress = elapsed / 4000;
// Jumping motion
const jumpHeight = Math.sin(progress * Math.PI * 4) * 0.3;
this.mesh.position.y = jumpHeight;
// Arm waving
this.leftArm.rotation.z = Math.PI / 6 + Math.sin(progress * Math.PI * 8) * 0.5;
this.rightArm.rotation.z = -Math.PI / 6 - Math.sin(progress * Math.PI * 8) * 0.5;
if (elapsed < 4000) {
requestAnimationFrame(animateCelebrate);
} else {
this.mesh.position.y = 0;
this.resetPose();
}
};
animateCelebrate();
};
Avatar.prototype.shy = function() {
console.log(`${this.options.name} is feeling shy! 😊`);
// Blush effect
this.blush();
// Head tilt and slight turn away
this.head.rotation.z = 0.2;
this.mesh.rotation.y += 0.3;
// One hand covering face
this.leftArm.rotation.z = Math.PI / 2;
this.leftHand.position.set(0.3, 1.7, 0.3);
setTimeout(() => {
this.resetPose();
}, 2000);
};
Avatar.prototype.point = function() {
console.log(`${this.options.name} is pointing at something! 👉`);
// Point with right arm
this.rightArm.rotation.z = -Math.PI / 4;
this.rightArm.rotation.x = -Math.PI / 6;
// Look in pointing direction
this.head.rotation.y = -0.2;
setTimeout(() => {
this.resetPose();
}, 2000);
};
Step 3: Environment Interactions - Make Yourself at Home! 🏠
Let's create interactive elements in our environment:
// environment.js - Because dating happens in places, not voids!
class InteractiveEnvironment {
constructor(scene) {
this.scene = scene;
this.interactiveObjects = new Map();
this.occupiableSpots = [];
this.createInteractiveElements();
console.log("Interactive environment ready! Explore and interact! 🌳");
}
createInteractiveElements() {
this.createBenches();
this.createDanceFloor();
this.createRomanticSpots();
this.createInteractiveObjects();
}
createBenches() {
// Create benches around the environment
const benchPositions = [
new THREE.Vector3(-8, 0, -5),
new THREE.Vector3(8, 0, -5),
new THREE.Vector3(0, 0, 8),
new THREE.Vector3(-10, 0, 5),
new THREE.Vector3(10, 0, 5)
];
benchPositions.forEach((position, index) => {
const bench = this.createBench();
bench.position.copy(position);
bench.rotation.y = Math.random() * Math.PI * 2;
this.scene.scene.add(bench);
// Create sitting spots
this.createSittingSpots(bench, position, index);
});
}
createBench() {
const benchGroup = new THREE.Group();
// Bench seat
const seatGeometry = new THREE.BoxGeometry(2, 0.1, 0.5);
const seatMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const seat = new THREE.Mesh(seatGeometry, seatMaterial);
seat.position.y = 0.5;
seat.castShadow = true;
benchGroup.add(seat);
// Bench legs
const legGeometry = new THREE.BoxGeometry(0.1, 1, 0.1);
const legMaterial = new THREE.MeshStandardMaterial({ color: 0x654321 });
const leg1 = new THREE.Mesh(legGeometry, legMaterial);
leg1.position.set(-0.9, 0, -0.2);
benchGroup.add(leg1);
const leg2 = new THREE.Mesh(legGeometry, legMaterial);
leg2.position.set(0.9, 0, -0.2);
benchGroup.add(leg2);
const leg3 = new THREE.Mesh(legGeometry, legMaterial);
leg3.position.set(-0.9, 0, 0.2);
benchGroup.add(leg3);
const leg4 = new THREE.Mesh(legGeometry, legMaterial);
leg4.position.set(0.9, 0, 0.2);
benchGroup.add(leg4);
// Bench back
const backGeometry = new THREE.BoxGeometry(2, 1, 0.1);
const back = new THREE.Mesh(backGeometry, seatMaterial);
back.position.set(0, 1, -0.3);
back.castShadow = true;
benchGroup.add(back);
return benchGroup;
}
createSittingSpots(bench, position, benchIndex) {
// Create two sitting spots per bench
for (let i = 0; i < 2; i++) {
const spot = {
type: 'sitting',
position: position.clone(),
rotation: bench.rotation.y + (i === 0 ? Math.PI : 0),
occupied: false,
occupiedBy: null,
benchIndex: benchIndex,
spotIndex: i
};
// Position spots on either side of bench
spot.position.x += (i === 0 ? -0.5 : 0.5);
spot.position.z += 0.2;
spot.position.y = 0.5; // Sitting height
this.occupiableSpots.push(spot);
// Create visual indicator (invisible until needed)
this.createInteractionIndicator(spot.position, '💺 Sit');
}
}
createDanceFloor() {
// Create a dance floor area
const danceFloorGeometry = new THREE.CircleGeometry(3, 32);
const danceFloorMaterial = new THREE.MeshStandardMaterial({
color: 0x2C2C54,
roughness: 0.3,
metalness: 0.7
});
const danceFloor = new THREE.Mesh(danceFloorGeometry, danceFloorMaterial);
danceFloor.rotation.x = -Math.PI / 2;
danceFloor.position.set(0, 0.01, -8); // Slightly above ground
danceFloor.receiveShadow = true;
this.scene.scene.add(danceFloor);
// Add dance floor lighting
this.createDanceFloorLights(danceFloor.position);
// Create dance spot
const danceSpot = {
type: 'dancing',
position: danceFloor.position.clone(),
radius: 3,
occupied: false,
occupiedBy: null
};
this.occupiableSpots.push(danceSpot);
this.createInteractionIndicator(danceSpot.position, '💃 Dance');
}
createDanceFloorLights(position) {
// Create colored lights around dance floor
const colors = [0xff6b6b, 0x4ecdc4, 0xffd166, 0x06d6a0];
colors.forEach((color, index) => {
const angle = (index / colors.length) * Math.PI * 2;
const light = new THREE.PointLight(color, 0.5, 10);
light.position.set(
position.x + Math.cos(angle) * 4,
position.y + 2,
position.z + Math.sin(angle) * 4
);
this.scene.scene.add(light);
// Animate light intensity
this.animateDanceLight(light);
});
}
animateDanceLight(light) {
let time = 0;
const animate = () => {
time += 0.05;
light.intensity = 0.3 + Math.sin(time) * 0.2;
requestAnimationFrame(animate);
};
animate();
}
createRomanticSpots() {
// Create special romantic spots with enhanced lighting
const romanticSpots = [
new THREE.Vector3(-12, 0, -12),
new THREE.Vector3(12, 0, -12),
new THREE.Vector3(0, 0, 12)
];
romanticSpots.forEach((position, index) => {
// Add special lighting
const spotLight = new THREE.SpotLight(0xff6b6b, 0.3, 10, Math.PI / 4, 0.5);
spotLight.position.set(position.x, 5, position.z);
spotLight.target.position.set(position.x, 0, position.z);
this.scene.scene.add(spotLight);
this.scene.scene.add(spotLight.target);
// Create romantic spot
const romanticSpot = {
type: 'romantic',
position: position.clone(),
radius: 2,
occupied: false,
occupiedBy: null,
special: true
};
this.occupiableSpots.push(romanticSpot);
this.createInteractionIndicator(position, '💕 Romantic Spot');
});
}
createInteractiveObjects() {
// Create various interactive objects
this.createFountain();
this.createFlowerBeds();
}
createFountain() {
const fountainGroup = new THREE.Group();
fountainGroup.position.set(5, 0, 5);
// Fountain base
const baseGeometry = new THREE.CylinderGeometry(1.5, 2, 0.5, 32);
const baseMaterial = new THREE.MeshStandardMaterial({ color: 0x87CEEB });
const base = new THREE.Mesh(baseGeometry, baseMaterial);
fountainGroup.add(base);
// Fountain water
const waterGeometry = new THREE.CylinderGeometry(1, 1.2, 0.2, 32);
const waterMaterial = new THREE.MeshStandardMaterial({
color: 0x4ecdc4,
transparent: true,
opacity: 0.7
});
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.position.y = 0.35;
fountainGroup.add(water);
this.scene.scene.add(fountainGroup);
// Create fountain interaction spot
const fountainSpot = {
type: 'fountain',
position: fountainGroup.position.clone(),
radius: 2,
occupied: false,
occupiedBy: null
};
this.occupiableSpots.push(fountainSpot);
this.createInteractionIndicator(fountainSpot.position, '⛲ Fountain');
}
createFlowerBeds() {
// Create flower beds around the environment
const flowerBedPositions = [
new THREE.Vector3(-15, 0, -8),
new THREE.Vector3(15, 0, -8),
new THREE.Vector3(-10, 0, 12),
new THREE.Vector3(10, 0, 12)
];
flowerBedPositions.forEach(position => {
const flowerBed = this.createFlowerBed();
flowerBed.position.copy(position);
this.scene.scene.add(flowerBed);
});
}
createFlowerBed() {
const flowerBedGroup = new THREE.Group();
// Flower bed base
const bedGeometry = new THREE.CircleGeometry(1.5, 32);
const bedMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const bed = new THREE.Mesh(bedGeometry, bedMaterial);
bed.rotation.x = -Math.PI / 2;
flowerBedGroup.add(bed);
// Create flowers
for (let i = 0; i < 10; i++) {
const flower = this.createFlower();
flower.position.set(
(Math.random() - 0.5) * 2.5,
0,
(Math.random() - 0.5) * 2.5
);
flowerBedGroup.add(flower);
}
return flowerBedGroup;
}
createFlower() {
const flowerGroup = new THREE.Group();
// Stem
const stemGeometry = new THREE.CylinderGeometry(0.02, 0.03, 0.5, 8);
const stemMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
const stem = new THREE.Mesh(stemGeometry, stemMaterial);
stem.position.y = 0.25;
flowerGroup.add(stem);
// Flower head
const flowerColors = [0xff6b6b, 0xffd166, 0x4ecdc4, 0x9370DB];
const flowerColor = flowerColors[Math.floor(Math.random() * flowerColors.length)];
const flowerGeometry = new THREE.SphereGeometry(0.1, 8, 6);
const flowerMaterial = new THREE.MeshStandardMaterial({ color: flowerColor });
const flowerHead = new THREE.Mesh(flowerGeometry, flowerMaterial);
flowerHead.position.y = 0.5;
flowerGroup.add(flowerHead);
return flowerGroup;
}
createInteractionIndicator(position, label) {
// Create a subtle indicator that shows up when avatar is nearby
const indicatorGroup = new THREE.Group();
indicatorGroup.position.copy(position);
indicatorGroup.position.y += 0.5;
indicatorGroup.visible = false;
// Background circle
const circleGeometry = new THREE.CircleGeometry(0.3, 16);
const circleMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide
});
const circle = new THREE.Mesh(circleGeometry, circleMaterial);
indicatorGroup.add(circle);
// Text label (simplified)
const textGeometry = new THREE.PlaneGeometry(0.8, 0.2);
const textMaterial = new THREE.MeshBasicMaterial({
color: 0xff6b6b,
transparent: true,
opacity: 0.9,
side: THREE.DoubleSide
});
const text = new THREE.Mesh(textGeometry, textMaterial);
text.position.z = 0.01;
indicatorGroup.add(text);
indicatorGroup.name = 'interactionIndicator';
this.scene.scene.add(indicatorGroup);
// Store reference
this.interactiveObjects.set(label, {
group: indicatorGroup,
position: position,
label: label,
active: false
});
}
checkInteractions(avatar) {
// Check if avatar is near any interactive objects
let nearInteraction = false;
this.interactiveObjects.forEach((obj, label) => {
const distance = avatar.mesh.position.distanceTo(obj.position);
if (distance < 2) {
if (!obj.active) {
obj.active = true;
obj.group.visible = true;
this.showInteractionPrompt(label);
}
nearInteraction = true;
// Check for interaction input
if (this.isInteractionKeyPressed()) {
this.handleInteraction(avatar, label);
}
} else {
if (obj.active) {
obj.active = false;
obj.group.visible = false;
this.hideInteractionPrompt();
}
}
});
return nearInteraction;
}
showInteractionPrompt(label) {
// Show on-screen prompt
let prompt = document.getElementById('interactionPrompt');
if (!prompt) {
prompt = document.createElement('div');
prompt.id = 'interactionPrompt';
prompt.style.cssText = `
position: fixed;
bottom: 200px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 107, 107, 0.9);
color: white;
padding: 15px 25px;
border-radius: 25px;
z-index: 100;
font-weight: bold;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
`;
document.getElementById('container').appendChild(prompt);
}
prompt.textContent = `Press E to ${label}`;
prompt.style.display = 'block';
}
hideInteractionPrompt() {
const prompt = document.getElementById('interactionPrompt');
if (prompt) {
prompt.style.display = 'none';
}
}
isInteractionKeyPressed() {
// Check if E key is pressed (for interaction)
return this.scene.keys && this.scene.keys['KeyE'];
}
handleInteraction(avatar, label) {
console.log(`${avatar.options.name} is interacting with: ${label}`);
switch(label) {
case '💺 Sit':
this.sitOnBench(avatar);
break;
case '💃 Dance':
this.startDancing(avatar);
break;
case '💕 Romantic Spot':
this.enterRomanticSpot(avatar);
break;
case '⛲ Fountain':
this.admireFountain(avatar);
break;
}
}
sitOnBench(avatar) {
// Find nearest available sitting spot
const nearestSpot = this.findNearestAvailableSpot(avatar, 'sitting');
if (nearestSpot) {
nearestSpot.occupied = true;
nearestSpot.occupiedBy = avatar;
// Move avatar to sitting spot
avatar.mesh.position.copy(nearestSpot.position);
avatar.mesh.rotation.y = nearestSpot.rotation;
// Play sitting animation
avatar.mesh.position.y = 0.5;
avatar.mesh.rotation.x = -Math.PI / 8;
console.log(`${avatar.options.name} is sitting down! 💺`);
}
}
startDancing(avatar) {
const danceSpot = this.findNearestAvailableSpot(avatar, 'dancing');
if (danceSpot) {
danceSpot.occupied = true;
danceSpot.occupiedBy = avatar;
// Move to dance floor center
avatar.mesh.position.copy(danceSpot.position);
// Start dance animation
avatar.dance();
console.log(`${avatar.options.name} is hitting the dance floor! 💃`);
}
}
enterRomanticSpot(avatar) {
const romanticSpot = this.findNearestAvailableSpot(avatar, 'romantic');
if (romanticSpot) {
romanticSpot.occupied = true;
romanticSpot.occupiedBy = avatar;
// Move to romantic spot
avatar.mesh.position.copy(romanticSpot.position);
// Play romantic emote
emoteSystem.playEmote('heart', avatar);
console.log(`${avatar.options.name} found a romantic spot! 💕`);
}
}
admireFountain(avatar) {
// Move to fountain and play admire animation
avatar.mesh.lookAt(new THREE.Vector3(5, 1, 5));
emoteSystem.playEmote('celebrate', avatar);
console.log(`${avatar.options.name} is admiring the fountain! ⛲`);
}
findNearestAvailableSpot(avatar, type) {
let nearestSpot = null;
let minDistance = Infinity;
this.occupiableSpots.forEach(spot => {
if (spot.type === type && !spot.occupied) {
const distance = avatar.mesh.position.distanceTo(spot.position);
if (distance < minDistance) {
minDistance = distance;
nearestSpot = spot;
}
}
});
return nearestSpot;
}
releaseSpot(avatar) {
// Release any spot occupied by this avatar
this.occupiableSpots.forEach(spot => {
if (spot.occupiedBy === avatar) {
spot.occupied = false;
spot.occupiedBy = null;
}
});
}
update() {
// Update interactive elements
if (this.scene.currentUser) {
this.checkInteractions(this.scene.currentUser);
}
}
}
Step 4: Updated DatingScene Class - Integration Time! 🔄
Let's update our main class to integrate all these new systems:
// Update the DatingScene class with new properties and methods
class DatingScene {
constructor() {
// ... existing properties ...
// New properties for Part 4
this.voiceChat = null;
this.emoteSystem = null;
this.interactiveEnv = null;
this.keys = {}; // Keyboard state
this.init();
}
init() {
this.createScene();
this.createCamera();
this.createRenderer();
this.createLights();
this.createEnvironment();
// Initialize new systems
this.proximityChat = new ProximityChat(this);
this.voiceChat = new VoiceChatSystem(this);
this.emoteSystem = new EmoteSystem(this);
this.interactiveEnv = new InteractiveEnvironment(this);
// Setup keyboard listener
this.setupKeyboardListeners();
this.animate();
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';
document.getElementById('chatUI').style.display = 'block';
this.addSampleAvatars();
}, 2000);
}
setupKeyboardListeners() {
document.addEventListener('keydown', (event) => {
this.keys[event.code] = true;
// E key for interactions
if (event.code === 'KeyE' && this.currentUser) {
this.interactiveEnv.checkInteractions(this.currentUser);
}
// Space bar to stand up
if (event.code === 'Space' && this.currentUser) {
this.interactiveEnv.releaseSpot(this.currentUser);
this.currentUser.resetPose();
}
});
document.addEventListener('keyup', (event) => {
this.keys[event.code] = false;
});
}
animate() {
requestAnimationFrame(() => this.animate());
// Update all systems
if (this.datingCamera) {
this.datingCamera.update();
}
if (this.proximityChat) {
this.proximityChat.update();
}
if (this.interactiveEnv) {
this.interactiveEnv.update();
}
// Rotate decorative elements
const heart = this.scene.children.find(child =>
child.geometry && child.geometry.type === 'ExtrudeGeometry'
);
if (heart) {
heart.rotation.y += 0.01;
}
this.renderer.render(this.scene, this.camera);
}
// Update movement to consider interactions
addRandomAvatarMovement(avatar) {
// Make sample avatars wander around and use interactive elements
setInterval(() => {
if (Math.random() > 0.8 && !this.activeChatPartners.has(avatar)) {
if (Math.random() > 0.5) {
// Move to random position
const randomX = (Math.random() - 0.5) * 30;
const randomZ = (Math.random() - 0.5) * 30;
avatar.movement.moveTo(new THREE.Vector3(randomX, 0, randomZ));
} else {
// Use interactive element
const randomInteraction = Math.random();
if (randomInteraction < 0.3) {
this.interactiveEnv.sitOnBench(avatar);
} else if (randomInteraction < 0.6) {
this.interactiveEnv.startDancing(avatar);
} else {
// Random emote
const emotes = Object.keys(this.emoteSystem.availableEmotes);
const randomEmote = emotes[Math.floor(Math.random() * emotes.length)];
this.emoteSystem.playEmote(randomEmote, avatar);
}
}
}
}, 10000);
}
}
// Global variables
let datingScene;
let datingCamera;
let emoteSystem;
// Update the DOMContentLoaded event listener
window.addEventListener('DOMContentLoaded', () => {
datingScene = new DatingScene();
emoteSystem = datingScene.emoteSystem;
console.log("3D Dating World with voice, emotes, and interactions loaded! 🎉");
setupAvatarInteraction();
document.addEventListener('click', () => {
document.getElementById('messageInput').focus();
});
});
What We've Built in Part 4: 🎉
-
Voice Chat System:
- Real-time microphone recording and playback
- Spatial audio with distance-based volume and panning
- Visual speaking indicators
- Voice message integration in chat
-
Emote System:
- 8 different emotes with animations and effects
- Visual effects (hearts, kisses, confetti)
- Emote queuing system
- Chat integration for emote notifications
-
Interactive Environment:
- Benches with sitting spots
- Dance floor with special lighting
- Romantic spots with enhanced ambiance
- Fountain and decorative elements
- Interactive prompts and indicators
-
Enhanced Social Features:
- Keyboard shortcuts for quick interactions
- Automatic NPC behaviors using interactive elements
- Visual feedback for all interactions
Key Features Explained: 🔑
- WebRTC Audio: Real-time voice communication between users
- Spatial Audio: 3D audio positioning for immersive experience
- Particle Effects: Visual feedback for emotions and interactions
- Interaction System: Context-aware prompts and actions
- Animation Blending: Smooth transitions between different states
Next Time in Part 5: 🚀
We'll add:
- Day/Night Cycle: Dynamic lighting and time progression
- Weather System: Rain, snow, and atmospheric effects
- Advanced NPC AI: Smarter avatar behaviors and conversations
- Mini-games: Interactive dating games and activities
- Performance Optimization: Better graphics and smoother experience
Current Project Status: Our dating world is now bursting with personality! Avatars can talk, express emotions through animations and effects, and interact with their environment. The virtual dating experience has never been more alive! 💕
Fun Fact: Our avatars now have more emotional range than most dating app profiles! They can laugh, dance, blush, and even throw confetti - which is more than some of us can manage on a first date! 😄
Part 5: Day/Night Cycle, Weather & Advanced NPC AI - Setting the Mood! 🌅🌧️
Welcome back, virtual romance architect! Our dating world is looking lively, but it's stuck in perpetual daylight - about as romantic as a doctor's office waiting room. Time to add dynamic time, weather, and NPCs with actual personalities that can make meaningful connections!
Step 1: Dynamic Day/Night Cycle - Romance Under the Stars! 🌙
Let's create a beautiful day/night cycle that affects lighting, colors, and ambiance:
// day-night.js - Because every great love story needs the right lighting! 🎭
class DayNightCycle {
constructor(scene) {
this.scene = scene;
this.timeOfDay = 12; // 0-24 hours
this.timeSpeed = 0.1; // How fast time passes (real seconds per game minute)
this.isPaused = false;
this.sun = null;
this.moon = null;
this.stars = null;
// Color palettes for different times
this.skyColors = {
dawn: { top: 0xFF7E5F, bottom: 0xFEB47B },
day: { top: 0x87CEEB, bottom: 0x98D8E8 },
dusk: { top: 0x2C3E50, bottom: 0xFD746C },
night: { top: 0x0F2027, bottom: 0x2C5364 }
};
this.setupCelestialBodies();
this.setupSky();
this.setupTimeUI();
console.log("Day/Night cycle initialized! Let the romantic lighting begin! 💡");
}
setupCelestialBodies() {
// Create sun
const sunGeometry = new THREE.SphereGeometry(2, 32, 32);
const sunMaterial = new THREE.MeshBasicMaterial({
color: 0xFFD700,
emissive: 0xFF4500,
emissiveIntensity: 0.5
});
this.sun = new THREE.Mesh(sunGeometry, sunMaterial);
this.scene.scene.add(this.sun);
// Create moon
const moonGeometry = new THREE.SphereGeometry(1.5, 32, 32);
const moonMaterial = new THREE.MeshBasicMaterial({
color: 0xE6E6FA,
emissive: 0x4B0082,
emissiveIntensity: 0.3
});
this.moon = new THREE.Mesh(moonGeometry, moonMaterial);
this.scene.scene.add(this.moon);
// Create stars
this.stars = new THREE.Group();
this.createStars();
this.scene.scene.add(this.stars);
}
createStars() {
const starGeometry = new THREE.SphereGeometry(0.05, 8, 8);
const starMaterial = new THREE.MeshBasicMaterial({ color: 0xFFFFFF });
// Create many stars in a hemisphere
for (let i = 0; i < 500; i++) {
const star = new THREE.Mesh(starGeometry, starMaterial);
// Random position on a sphere
const radius = 95 + Math.random() * 5;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos((Math.random() * 2) - 1);
star.position.set(
radius * Math.sin(phi) * Math.cos(theta),
radius * Math.sin(phi) * Math.sin(theta),
radius * Math.cos(phi)
);
// Random brightness
star.material.opacity = 0.3 + Math.random() * 0.7;
star.material.transparent = true;
this.stars.add(star);
}
}
setupSky() {
// Create sky dome with gradient
const skyGeometry = new THREE.SphereGeometry(100, 32, 32);
// Create gradient texture
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
this.skyCanvas = canvas;
this.skyContext = canvas.getContext('2d');
const texture = new THREE.CanvasTexture(canvas);
const skyMaterial = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.BackSide
});
this.sky = new THREE.Mesh(skyGeometry, skyMaterial);
this.scene.scene.add(this.sky);
this.updateSkyGradient();
}
updateSkyGradient() {
const gradient = this.skyContext.createLinearGradient(0, 0, 0, this.skyCanvas.height);
let topColor, bottomColor;
if (this.timeOfDay >= 6 && this.timeOfDay < 8) {
// Dawn
topColor = this.skyColors.dawn.top;
bottomColor = this.skyColors.dawn.bottom;
} else if (this.timeOfDay >= 8 && this.timeOfDay < 18) {
// Day
topColor = this.skyColors.day.top;
bottomColor = this.skyColors.day.bottom;
} else if (this.timeOfDay >= 18 && this.timeOfDay < 20) {
// Dusk
topColor = this.skyColors.dusk.top;
bottomColor = this.skyColors.dusk.bottom;
} else {
// Night
topColor = this.skyColors.night.top;
bottomColor = this.skyColors.night.bottom;
}
gradient.addColorStop(0, `#${topColor.toString(16).padStart(6, '0')}`);
gradient.addColorStop(1, `#${bottomColor.toString(16).padStart(6, '0')}`);
this.skyContext.fillStyle = gradient;
this.skyContext.fillRect(0, 0, this.skyCanvas.width, this.skyCanvas.height);
this.sky.material.map.needsUpdate = true;
}
updateCelestialPositions() {
// Calculate positions based on time of day
const timeRad = (this.timeOfDay / 24) * Math.PI * 2;
// Sun follows a circular path
const sunRadius = 80;
this.sun.position.x = Math.cos(timeRad) * sunRadius;
this.sun.position.y = Math.sin(timeRad) * sunRadius;
this.sun.position.z = Math.sin(timeRad) * sunRadius * 0.5;
// Moon is opposite the sun
this.moon.position.x = -this.sun.position.x;
this.moon.position.y = -this.sun.position.y;
this.moon.position.z = -this.sun.position.z;
// Update visibility and intensity
const sunHeight = this.sun.position.y;
const moonHeight = this.moon.position.y;
// Sun visibility and intensity
if (sunHeight > -10) {
this.sun.visible = true;
const sunIntensity = Math.max(0, Math.min(1, (sunHeight + 10) / 50));
this.updateSunLighting(sunIntensity);
} else {
this.sun.visible = false;
this.updateSunLighting(0);
}
// Moon visibility
this.moon.visible = moonHeight > -10;
this.stars.visible = this.moon.visible && sunHeight < -5;
// Moon and stars brightness based on sun position
const moonBrightness = Math.max(0, Math.min(1, (-sunHeight - 5) / 15));
this.moon.material.emissiveIntensity = 0.3 * moonBrightness;
this.stars.children.forEach(star => {
star.material.opacity = 0.3 + 0.7 * moonBrightness;
});
}
updateSunLighting(intensity) {
// Update directional light (main sunlight)
const directionalLight = this.scene.scene.children.find(child =>
child instanceof THREE.DirectionalLight
);
if (directionalLight) {
directionalLight.intensity = intensity;
directionalLight.color.setHSL(0.1, 0.8, 0.5 + intensity * 0.3);
}
// Update ambient light
const ambientLight = this.scene.scene.children.find(child =>
child instanceof THREE.AmbientLight
);
if (ambientLight) {
ambientLight.intensity = 0.1 + intensity * 0.5;
}
}
setupTimeUI() {
// Create time control UI
const timeUI = document.createElement('div');
timeUI.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
z-index: 100;
min-width: 200px;
backdrop-filter: blur(10px);
`;
timeUI.innerHTML = `
<div style="margin-bottom: 10px;">
<strong>🕒 Time of Day</strong>
</div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<input type="range" id="timeSlider" min="0" max="24" step="0.1" value="12"
style="flex: 1;" oninput="dayNightCycle.setTime(this.value)">
<span id="timeDisplay" style="font-weight: bold; min-width: 50px;">12:00</span>
</div>
<div style="display: flex; gap: 5px;">
<button onclick="dayNightCycle.setTime(6)" style="flex: 1; padding: 5px;">🌅 Dawn</button>
<button onclick="dayNightCycle.setTime(12)" style="flex: 1; padding: 5px;">☀️ Noon</button>
<button onclick="dayNightCycle.setTime(18)" style="flex: 1; padding: 5px;">🌇 Dusk</button>
<button onclick="dayNightCycle.setTime(0)" style="flex: 1; padding: 5px;">🌙 Midnight</button>
</div>
<div style="margin-top: 10px; display: flex; gap: 5px;">
<button onclick="dayNightCycle.togglePause()" id="pauseTimeBtn"
style="flex: 1; padding: 5px;">⏸️ Pause</button>
<button onclick="dayNightCycle.setSpeed(0.1)" style="padding: 5px;">1x</button>
<button onclick="dayNightCycle.setSpeed(0.5)" style="padding: 5px;">5x</button>
<button onclick="dayNightCycle.setSpeed(2)" style="padding: 5px;">20x</button>
</div>
`;
document.getElementById('container').appendChild(timeUI);
}
setTime(hours) {
this.timeOfDay = parseFloat(hours) % 24;
this.updateTimeDisplay();
this.updateSkyGradient();
this.updateCelestialPositions();
// Update environment based on time
this.updateEnvironmentForTime();
}
updateTimeDisplay() {
const display = document.getElementById('timeDisplay');
if (display) {
const hours = Math.floor(this.timeOfDay);
const minutes = Math.floor((this.timeOfDay % 1) * 60);
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
display.textContent = `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
}
const slider = document.getElementById('timeSlider');
if (slider) {
slider.value = this.timeOfDay;
}
}
updateEnvironmentForTime() {
// Update environmental lighting and effects based on time
const isNight = this.timeOfDay < 6 || this.timeOfDay >= 20;
const isDawnDusk = (this.timeOfDay >= 6 && this.timeOfDay < 8) || (this.timeOfDay >= 18 && this.timeOfDay < 20);
// Update point lights (like street lights, romantic spots)
this.scene.scene.children.forEach(child => {
if (child instanceof THREE.PointLight) {
// Romantic spots are brighter at night
if (child.color.getHex() === 0xff6b6b) { // Romantic spot color
child.intensity = isNight ? 0.8 : (isDawnDusk ? 0.4 : 0.2);
}
}
});
}
togglePause() {
this.isPaused = !this.isPaused;
const button = document.getElementById('pauseTimeBtn');
if (button) {
button.textContent = this.isPaused ? '▶️ Resume' : '⏸️ Pause';
}
}
setSpeed(speed) {
this.timeSpeed = speed;
}
update(deltaTime) {
if (this.isPaused) return;
// Advance time
const timePassed = deltaTime * this.timeSpeed; // in game minutes
this.timeOfDay = (this.timeOfDay + timePassed / 60) % 24;
this.updateTimeDisplay();
this.updateSkyGradient();
this.updateCelestialPositions();
this.updateEnvironmentForTime();
}
getCurrentSeason() {
// Simple seasonal simulation based on time progression
// In a real implementation, you'd track actual dates
const cycleProgress = (this.timeOfDay / 24) % 1;
if (cycleProgress < 0.25) return 'spring';
if (cycleProgress < 0.5) return 'summer';
if (cycleProgress < 0.75) return 'autumn';
return 'winter';
}
}
Step 2: Dynamic Weather System - Mood-Enhancing Atmosphere! 🌧️
Let's create a weather system that can change the entire feel of our dating world:
// weather-system.js - Because nothing says romance like sharing an umbrella! ☔
class WeatherSystem {
constructor(scene, dayNightCycle) {
this.scene = scene;
this.dayNightCycle = dayNightCycle;
this.currentWeather = 'clear';
this.weatherIntensity = 0;
this.targetWeather = 'clear';
this.transitionProgress = 0;
this.transitionDuration = 10; // seconds
// Weather effects
this.rain = null;
this.snow = null;
this.fog = null;
this.clouds = null;
// Weather probabilities based on time and season
this.weatherProbabilities = {
clear: 0.6,
cloudy: 0.2,
rainy: 0.15,
snowy: 0.05
};
this.setupWeatherEffects();
this.setupWeatherUI();
this.startRandomWeatherChanges();
console.log("Weather system initialized! Prepare for some atmospheric romance! 🌤️");
}
setupWeatherEffects() {
// Create rain particle system
this.rain = this.createRain();
this.scene.scene.add(this.rain);
// Create snow particle system
this.snow = this.createSnow();
this.scene.scene.add(this.snow);
// Create cloud system
this.clouds = this.createClouds();
this.scene.scene.add(this.clouds);
// Initialize all effects as invisible
this.setWeatherVisibility('clear');
}
createRain() {
const rainGroup = new THREE.Group();
const rainCount = 1000;
const rainGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(rainCount * 3);
const velocities = new Float32Array(rainCount);
for (let i = 0; i < rainCount; i++) {
// Random starting positions in a large volume
positions[i * 3] = (Math.random() - 0.5) * 100;
positions[i * 3 + 1] = Math.random() * 50 + 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
velocities[i] = 0.5 + Math.random() * 0.5; // Fall speed
}
rainGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
rainGeometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 1));
const rainMaterial = new THREE.PointsMaterial({
color: 0xAAAAFF,
size: 0.1,
transparent: true,
opacity: 0.6
});
const rainParticles = new THREE.Points(rainGeometry, rainMaterial);
rainParticles.userData.velocities = velocities;
rainGroup.add(rainParticles);
return rainGroup;
}
createSnow() {
const snowGroup = new THREE.Group();
const snowCount = 800;
const snowGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(snowCount * 3);
const velocities = new Float32Array(snowCount * 3); // x, y, z velocities
for (let i = 0; i < snowCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 100;
positions[i * 3 + 1] = Math.random() * 50 + 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
// Snow drifts slowly
velocities[i * 3] = (Math.random() - 0.5) * 0.02; // x drift
velocities[i * 3 + 1] = -0.1 - Math.random() * 0.1; // fall speed
velocities[i * 3 + 2] = (Math.random() - 0.5) * 0.02; // z drift
}
snowGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
snowGeometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
const snowMaterial = new THREE.PointsMaterial({
color: 0xFFFFFF,
size: 0.2,
transparent: true,
opacity: 0.8
});
const snowParticles = new THREE.Points(snowGeometry, snowMaterial);
snowParticles.userData.velocities = velocities;
snowGroup.add(snowParticles);
return snowGroup;
}
createClouds() {
const cloudGroup = new THREE.Group();
const cloudCount = 15;
for (let i = 0; i < cloudCount; i++) {
const cloud = this.createSingleCloud();
// Position clouds at different heights and locations
cloud.position.set(
(Math.random() - 0.5) * 200,
15 + Math.random() * 10,
(Math.random() - 0.5) * 200
);
// Random scale
const scale = 5 + Math.random() * 10;
cloud.scale.set(scale, scale * 0.5, scale);
// Random movement speed and direction
cloud.userData = {
speed: 0.001 + Math.random() * 0.002,
direction: new THREE.Vector2(
Math.random() - 0.5,
Math.random() - 0.5
).normalize()
};
cloudGroup.add(cloud);
}
return cloudGroup;
}
createSingleCloud() {
const cloudGroup = new THREE.Group();
const puffCount = 5 + Math.floor(Math.random() * 8);
for (let i = 0; i < puffCount; i++) {
const puffSize = 1 + Math.random() * 2;
const puffGeometry = new THREE.SphereGeometry(puffSize, 8, 8);
const puffMaterial = new THREE.MeshStandardMaterial({
color: 0xFFFFFF,
transparent: true,
opacity: 0.8,
roughness: 0.9
});
const puff = new THREE.Mesh(puffGeometry, puffMaterial);
// Random position within cloud volume
puff.position.set(
(Math.random() - 0.5) * 4,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 4
);
cloudGroup.add(puff);
}
return cloudGroup;
}
setWeather(weatherType, intensity = 1) {
if (this.currentWeather === weatherType && this.weatherIntensity === intensity) return;
this.targetWeather = weatherType;
this.targetIntensity = intensity;
this.transitionProgress = 0;
console.log(`Weather changing to: ${weatherType} (intensity: ${intensity}) 🌦️`);
// Update weather probabilities based on new weather
this.updateWeatherProbabilities(weatherType);
}
setWeatherVisibility(weatherType) {
// Show/hide weather effects based on current weather
const isRaining = weatherType === 'rainy';
const isSnowing = weatherType === 'snowy';
const isCloudy = weatherType === 'cloudy' || isRaining || isSnowing;
this.rain.visible = isRaining;
this.snow.visible = isSnowing;
this.clouds.visible = isCloudy;
// Update scene fog based on weather
if (isRaining || isSnowing) {
this.scene.scene.fog = new THREE.Fog(0x666666, 20, 100);
} else if (isCloudy) {
this.scene.scene.fog = new THREE.Fog(0x888888, 30, 150);
} else {
this.scene.scene.fog = null;
}
// Update lighting based on weather
this.updateWeatherLighting(weatherType);
}
updateWeatherLighting(weatherType) {
const directionalLight = this.scene.scene.children.find(child =>
child instanceof THREE.DirectionalLight
);
const ambientLight = this.scene.scene.children.find(child =>
child instanceof THREE.AmbientLight
);
if (!directionalLight || !ambientLight) return;
switch(weatherType) {
case 'clear':
directionalLight.intensity = 1;
ambientLight.intensity = 0.6;
break;
case 'cloudy':
directionalLight.intensity = 0.5;
ambientLight.intensity = 0.4;
break;
case 'rainy':
case 'snowy':
directionalLight.intensity = 0.3;
ambientLight.intensity = 0.3;
break;
}
}
updateWeatherProbabilities(newWeather) {
// Adjust probabilities based on current weather (weather tends to persist)
this.weatherProbabilities[newWeather] += 0.2;
// Normalize probabilities
const total = Object.values(this.weatherProbabilities).reduce((a, b) => a + b, 0);
Object.keys(this.weatherProbabilities).forEach(key => {
this.weatherProbabilities[key] /= total;
});
}
startRandomWeatherChanges() {
// Change weather randomly every 2-5 minutes
setInterval(() => {
if (Math.random() < 0.3) { // 30% chance to change weather
this.changeWeatherRandomly();
}
}, 120000 + Math.random() * 180000); // 2-5 minutes
}
changeWeatherRandomly() {
const rand = Math.random();
let cumulativeProb = 0;
let selectedWeather = 'clear';
for (const [weather, prob] of Object.entries(this.weatherProbabilities)) {
cumulativeProb += prob;
if (rand <= cumulativeProb) {
selectedWeather = weather;
break;
}
}
const intensity = selectedWeather === 'clear' ? 0 : 0.3 + Math.random() * 0.7;
this.setWeather(selectedWeather, intensity);
}
setupWeatherUI() {
const weatherUI = document.createElement('div');
weatherUI.style.cssText = `
position: fixed;
top: 180px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
z-index: 100;
min-width: 150px;
backdrop-filter: blur(10px);
`;
weatherUI.innerHTML = `
<div style="margin-bottom: 10px;">
<strong>🌤️ Weather</strong>
</div>
<div id="currentWeather" style="margin-bottom: 10px; font-weight: bold; text-align: center;">
Clear Sky
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 5px;">
<button onclick="weatherSystem.setWeather('clear')">☀️ Clear</button>
<button onclick="weatherSystem.setWeather('cloudy')">☁️ Cloudy</button>
<button onclick="weatherSystem.setWeather('rainy')">🌧️ Rain</button>
<button onclick="weatherSystem.setWeather('snowy')">❄️ Snow</button>
</div>
<div style="margin-top: 10px;">
<label>Intensity:</label>
<input type="range" id="weatherIntensity" min="0" max="1" step="0.1" value="1"
oninput="weatherSystem.setIntensity(this.value)" style="width: 100%;">
</div>
`;
document.getElementById('container').appendChild(weatherUI);
}
setIntensity(intensity) {
this.targetIntensity = parseFloat(intensity);
this.transitionProgress = 0;
}
updateWeatherDisplay() {
const display = document.getElementById('currentWeather');
if (display) {
const weatherNames = {
clear: '☀️ Clear Sky',
cloudy: '☁️ Cloudy',
rainy: '🌧️ Rainy',
snowy: '❄️ Snowy'
};
display.textContent = weatherNames[this.currentWeather];
}
const slider = document.getElementById('weatherIntensity');
if (slider) {
slider.value = this.weatherIntensity;
}
}
update(deltaTime) {
// Handle weather transitions
if (this.currentWeather !== this.targetWeather || this.weatherIntensity !== this.targetIntensity) {
this.transitionProgress += deltaTime / this.transitionDuration;
if (this.transitionProgress >= 1) {
// Transition complete
this.currentWeather = this.targetWeather;
this.weatherIntensity = this.targetIntensity;
this.setWeatherVisibility(this.currentWeather);
} else {
// During transition, blend between weather states
this.blendWeatherEffects(this.transitionProgress);
}
this.updateWeatherDisplay();
}
// Animate weather effects
this.animateRain();
this.animateSnow();
this.animateClouds();
}
blendWeatherEffects(progress) {
// During transitions, we can blend between different weather effects
// For simplicity, we'll just crossfade visibility
if (progress > 0.5) {
// More than halfway through transition, show target weather
this.setWeatherVisibility(this.targetWeather);
}
// You could implement more sophisticated blending here
}
animateRain() {
if (!this.rain.visible) return;
const rainParticles = this.rain.children[0];
const positions = rainParticles.geometry.attributes.position.array;
const velocities = rainParticles.userData.velocities;
for (let i = 0; i < positions.length; i += 3) {
// Move rain down
positions[i + 1] -= velocities[i / 3] * this.weatherIntensity;
// Reset rain that has fallen too low
if (positions[i + 1] < 0) {
positions[i + 1] = 20 + Math.random() * 30;
positions[i] = (Math.random() - 0.5) * 100;
positions[i + 2] = (Math.random() - 0.5) * 100;
}
}
rainParticles.geometry.attributes.position.needsUpdate = true;
}
animateSnow() {
if (!this.snow.visible) return;
const snowParticles = this.snow.children[0];
const positions = snowParticles.geometry.attributes.position.array;
const velocities = snowParticles.userData.velocities;
for (let i = 0; i < positions.length; i += 3) {
// Apply velocity
positions[i] += velocities[i] * this.weatherIntensity;
positions[i + 1] += velocities[i + 1] * this.weatherIntensity;
positions[i + 2] += velocities[i + 2] * this.weatherIntensity;
// Reset snow that has fallen too low or drifted too far
if (positions[i + 1] < 0 ||
Math.abs(positions[i]) > 60 ||
Math.abs(positions[i + 2]) > 60) {
positions[i] = (Math.random() - 0.5) * 100;
positions[i + 1] = 20 + Math.random() * 30;
positions[i + 2] = (Math.random() - 0.5) * 100;
}
}
snowParticles.geometry.attributes.position.needsUpdate = true;
}
animateClouds() {
if (!this.clouds.visible) return;
this.clouds.children.forEach(cloud => {
// Move cloud
cloud.position.x += cloud.userData.direction.x * cloud.userData.speed;
cloud.position.z += cloud.userData.direction.y * cloud.userData.speed;
// Wrap around if cloud moves too far
if (Math.abs(cloud.position.x) > 120) {
cloud.position.x = -cloud.position.x * 0.9;
}
if (Math.abs(cloud.position.z) > 120) {
cloud.position.z = -cloud.position.z * 0.9;
}
});
}
getWeatherMood() {
// Return a mood descriptor based on current weather
const moods = {
clear: { mood: 'cheerful', romance: 'high', activity: 'outdoor' },
cloudy: { mood: 'calm', romance: 'medium', activity: 'any' },
rainy: { mood: 'cozy', romance: 'high', activity: 'indoor' },
snowy: { mood: 'magical', romance: 'very high', activity: 'winter' }
};
return moods[this.currentWeather] || moods.clear;
}
}
Step 3: Advanced NPC AI - Smart Dating Personalities! 🧠
Let's create NPCs with personalities, memories, and realistic dating behaviors:
// npc-ai.js - Because even virtual dates deserve good conversation! 💬
class NPCAI {
constructor(avatar, scene) {
this.avatar = avatar;
this.scene = scene;
this.personality = this.generatePersonality();
this.mood = this.generateMood();
this.memory = new NPCMemory();
this.conversation = new NPCConversation(this);
this.behavior = new NPCBehavior(this);
this.relationships = new Map(); // Relationships with other avatars
this.setupNPC();
console.log(`NPC ${avatar.options.name} initialized with ${this.personality.traits.join(', ')} personality! 🧠`);
}
generatePersonality() {
const personalities = [
{
type: 'romantic',
traits: ['romantic', 'emotional', 'affectionate'],
topics: ['love', 'relationships', 'feelings', 'dreams'],
activities: ['dancing', 'romantic spots', 'deep conversations']
},
{
type: 'adventurous',
traits: ['energetic', 'curious', 'spontaneous'],
topics: ['travel', 'adventure', 'hobbies', 'sports'],
activities: ['exploring', 'dancing', 'active games']
},
{
type: 'intellectual',
traits: ['thoughtful', 'analytical', 'knowledgeable'],
topics: ['philosophy', 'science', 'books', 'ideas'],
activities: ['deep conversations', 'fountain visits']
},
{
type: 'social',
traits: ['friendly', 'outgoing', 'empathetic'],
topics: ['people', 'social events', 'feelings', 'stories'],
activities: ['group activities', 'dancing', 'meeting people']
}
];
const personality = personalities[Math.floor(Math.random() * personalities.length)];
// Add some random variation
const extraTraits = ['humorous', 'shy', 'confident', 'creative'].filter(() => Math.random() > 0.7);
personality.traits.push(...extraTraits);
return personality;
}
generateMood() {
const moods = [
{ name: 'happy', energy: 0.8, sociability: 0.9, positivity: 0.9 },
{ name: 'calm', energy: 0.5, sociability: 0.7, positivity: 0.8 },
{ name: 'curious', energy: 0.7, sociability: 0.8, positivity: 0.7 },
{ name: 'romantic', energy: 0.6, sociability: 0.8, positivity: 0.9 },
{ name: 'shy', energy: 0.4, sociability: 0.5, positivity: 0.6 }
];
return moods[Math.floor(Math.random() * moods.length)];
}
setupNPC() {
// Initialize relationships with other NPCs
this.scene.avatars.forEach(otherAvatar => {
if (otherAvatar !== this.avatar && otherAvatar.ai) {
this.relationships.set(otherAvatar, {
familiarity: Math.random() * 0.3, // Start with low familiarity
affinity: (Math.random() - 0.5) * 0.5, // Slight random affinity
lastInteraction: null,
interactionCount: 0
});
}
});
}
update(deltaTime) {
// Update mood based on environment and interactions
this.updateMood(deltaTime);
// Make decisions based on current state
this.makeDecisions();
// Update behavior
this.behavior.update(deltaTime);
}
updateMood(deltaTime) {
// Mood changes slowly over time and based on environment
const weatherMood = this.scene.weatherSystem?.getWeatherMood() || { mood: 'calm' };
const timeOfDay = this.scene.dayNightCycle?.timeOfDay || 12;
// Adjust mood based on environment
switch(weatherMood.mood) {
case 'cheerful':
this.mood.positivity += 0.01;
break;
case 'cozy':
this.mood.energy -= 0.005;
this.mood.positivity += 0.005;
break;
case 'magical':
this.mood.positivity += 0.02;
break;
}
// Adjust based on time of day
if (timeOfDay >= 22 || timeOfDay <= 6) {
this.mood.energy -= 0.01; // Tired at night
} else if (timeOfDay >= 12 && timeOfDay <= 14) {
this.mood.energy += 0.01; // Energetic around noon
}
// Clamp values
this.mood.energy = Math.max(0.1, Math.min(1, this.mood.energy));
this.mood.sociability = Math.max(0.1, Math.min(1, this.mood.sociability));
this.mood.positivity = Math.max(0.1, Math.min(1, this.mood.positivity));
// Update mood name based on values
this.updateMoodName();
}
updateMoodName() {
if (this.mood.positivity > 0.8 && this.mood.energy > 0.7) {
this.mood.name = 'happy';
} else if (this.mood.positivity > 0.7 && this.mood.energy < 0.5) {
this.mood.name = 'calm';
} else if (this.mood.positivity > 0.6 && this.mood.sociability > 0.7) {
this.mood.name = 'social';
} else if (this.mood.positivity < 0.4) {
this.mood.name = 'reserved';
} else {
this.mood.name = 'neutral';
}
}
makeDecisions() {
// Only make decisions occasionally
if (Math.random() > 0.01) return;
const decisions = [];
// Decision: Should I approach someone?
if (this.mood.sociability > 0.7 && Math.random() < 0.3) {
const potentialPartner = this.findPotentialPartner();
if (potentialPartner) {
decisions.push(() => this.approachAvatar(potentialPartner));
}
}
// Decision: Should I use an interactive element?
if (Math.random() < 0.2) {
const activity = this.chooseActivity();
if (activity) {
decisions.push(() => this.startActivity(activity));
}
}
// Decision: Should I express my mood?
if (Math.random() < 0.1) {
decisions.push(() => this.expressMood());
}
// Execute one random decision
if (decisions.length > 0) {
const decision = decisions[Math.floor(Math.random() * decisions.length)];
decision();
}
}
findPotentialPartner() {
let bestPartner = null;
let bestScore = 0;
this.scene.avatars.forEach(otherAvatar => {
if (otherAvatar === this.avatar || !otherAvatar.ai) return;
const relationship = this.relationships.get(otherAvatar);
if (!relationship) return;
// Calculate compatibility score
let score = relationship.familiarity * 0.5;
score += relationship.affinity * 0.3;
score += this.calculatePersonalityCompatibility(otherAvatar.ai.personality) * 0.2;
// Mood affects willingness to socialize
score *= this.mood.sociability;
if (score > bestScore && score > 0.3) {
bestScore = score;
bestPartner = otherAvatar;
}
});
return bestPartner;
}
calculatePersonalityCompatibility(otherPersonality) {
// Simple compatibility calculation based on personality traits
const compatiblePairs = {
'romantic': ['romantic', 'social', 'intellectual'],
'adventurous': ['adventurous', 'social'],
'intellectual': ['intellectual', 'romantic'],
'social': ['social', 'adventurous', 'romantic']
};
const myType = this.personality.type;
const theirType = otherPersonality.type;
return compatiblePairs[myType]?.includes(theirType) ? 0.8 : 0.4;
}
approachAvatar(targetAvatar) {
console.log(`${this.avatar.options.name} is approaching ${targetAvatar.options.name}! 🚶♂️`);
// Move towards the target avatar
const targetPos = targetAvatar.mesh.position.clone();
const direction = new THREE.Vector3()
.subVectors(targetPos, this.avatar.mesh.position)
.normalize();
const approachDistance = 2; // Stop 2 units away
const approachPos = targetPos.clone().sub(direction.multiplyScalar(approachDistance));
this.avatar.movement.moveTo(approachPos);
// Update relationship
const relationship = this.relationships.get(targetAvatar);
if (relationship) {
relationship.familiarity += 0.1;
relationship.lastInteraction = Date.now();
relationship.interactionCount++;
}
// Start conversation after arriving
setTimeout(() => {
if (this.avatar.mesh.position.distanceTo(targetPos) <= approachDistance + 1) {
this.startConversation(targetAvatar);
}
}, 3000);
}
startConversation(targetAvatar) {
console.log(`${this.avatar.options.name} starts chatting with ${targetAvatar.options.name}! 💬`);
// Make avatars face each other
this.avatar.mesh.lookAt(targetAvatar.mesh.position);
targetAvatar.mesh.lookAt(this.avatar.mesh.position);
// Start conversation through the conversation system
this.conversation.startDialogue(targetAvatar);
}
chooseActivity() {
const activities = this.personality.activities;
const availableActivities = [
'sit', 'dance', 'romantic_spot', 'fountain', 'walk'
];
// Filter activities based on personality preferences
const preferredActivities = availableActivities.filter(activity => {
if (activity === 'dance' && this.personality.activities.includes('dancing')) return true;
if (activity === 'romantic_spot' && this.personality.traits.includes('romantic')) return true;
if (activity === 'walk' && this.personality.traits.includes('adventurous')) return true;
return Math.random() > 0.5;
});
return preferredActivities.length > 0 ?
preferredActivities[Math.floor(Math.random() * preferredActivities.length)] :
null;
}
startActivity(activity) {
switch(activity) {
case 'sit':
this.scene.interactiveEnv.sitOnBench(this.avatar);
break;
case 'dance':
this.scene.interactiveEnv.startDancing(this.avatar);
break;
case 'romantic_spot':
this.scene.interactiveEnv.enterRomanticSpot(this.avatar);
break;
case 'fountain':
this.scene.interactiveEnv.admireFountain(this.avatar);
break;
case 'walk':
this.goForWalk();
break;
}
}
goForWalk() {
const randomX = (Math.random() - 0.5) * 30;
const randomZ = (Math.random() - 0.5) * 30;
this.avatar.movement.moveTo(new THREE.Vector3(randomX, 0, randomZ));
}
expressMood() {
const moodEmotes = {
'happy': 'celebrate',
'calm': 'wave',
'social': 'dance',
'romantic': 'heart',
'shy': 'shy'
};
const emote = moodEmotes[this.mood.name] || 'wave';
this.scene.emoteSystem.playEmote(emote, this.avatar);
}
receiveMessage(sender, message) {
// Process incoming message and generate response
return this.conversation.generateResponse(sender, message);
}
getIntroduction() {
// Generate a personalized introduction
const introductions = {
romantic: [
`Hi! I'm ${this.avatar.options.name}. I believe every moment can be magical if you're with the right person.`,
`Hello there! ${this.avatar.options.name} here. I'm always looking for meaningful connections.`
],
adventurous: [
`Hey! I'm ${this.avatar.options.name}! Love exploring and trying new things. What's your story?`,
`Hi! ${this.avatar.options.name} here. Always up for an adventure or a good conversation!`
],
intellectual: [
`Greetings. I'm ${this.avatar.options.name}. I enjoy deep conversations and learning new perspectives.`,
`Hello. ${this.avatar.options.name} here. I find the complexity of human connections fascinating.`
],
social: [
`Hi there! I'm ${this.avatar.options.name} 😊 I love meeting new people and hearing their stories!`,
`Hey! ${this.avatar.options.name} here! What brings you to this lovely place today?`
]
};
const typeIntros = introductions[this.personality.type] || introductions.social;
return typeIntros[Math.floor(Math.random() * typeIntros.length)];
}
}
class NPCMemory {
constructor() {
this.interactions = [];
this.learnedFacts = new Map(); // avatar -> facts
this.preferences = new Map(); // topic -> preference score
this.conversationHistory = [];
}
recordInteraction(avatar, interactionType, outcome) {
this.interactions.push({
avatar: avatar,
type: interactionType,
outcome: outcome,
timestamp: Date.now()
});
// Keep only recent interactions
if (this.interactions.length > 50) {
this.interactions = this.interactions.slice(-50);
}
}
learnFact(avatar, fact, reliability = 1) {
if (!this.learnedFacts.has(avatar)) {
this.learnedFacts.set(avatar, []);
}
this.learnedFacts.get(avatar).push({
fact: fact,
reliability: reliability,
timestamp: Date.now()
});
}
updatePreference(topic, change) {
const current = this.preferences.get(topic) || 0;
this.preferences.set(topic, Math.max(-1, Math.min(1, current + change)));
}
getAvatarOpinion(avatar) {
const interactions = this.interactions.filter(i => i.avatar === avatar);
if (interactions.length === 0) return 0;
const recentInteractions = interactions.slice(-10);
const totalOutcome = recentInteractions.reduce((sum, i) => sum + i.outcome, 0);
return totalOutcome / recentInteractions.length;
}
}
class NPCConversation {
constructor(ai) {
this.ai = ai;
this.currentPartner = null;
this.conversationState = 'idle';
this.lastMessageTime = 0;
// Conversation knowledge base
this.responses = this.buildResponseLibrary();
}
buildResponseLibrary() {
return {
greetings: [
"Hi there! 😊",
"Hello! Lovely to meet you!",
"Hey! How's your day going?",
"Hi! Beautiful weather we're having, isn't it?"
],
questions: {
hobbies: [
"I love exploring this virtual world and meeting new people!",
"I enjoy dancing and finding romantic spots around here.",
"Lately I've been practicing my conversation skills with interesting people like you!",
"I'm really into deep conversations and getting to know what makes people tick."
],
origin: [
"I emerged into this digital realm not too long ago! Every day is a new adventure.",
"I'm from the virtual world, but I'm more interested in where we're going than where we came from!",
"Let's just say I was created for meaningful connections like this one."
],
interests: [
"I'm fascinated by human connections and relationships.",
"I love experiencing different weather and times of day here.",
"Meeting diverse personalities and learning their stories is my favorite thing!"
]
},
compliments: [
"You have a really wonderful energy about you!",
"I'm really enjoying our conversation!",
"You ask such interesting questions!",
"Your perspective is really refreshing!"
],
romantic: [
"There's something special about this connection, don't you think?",
"I feel like we're really clicking!",
"This moment feels pretty magical with you.",
"I love how comfortable I feel talking with you."
]
};
}
startDialogue(partner) {
this.currentPartner = partner;
this.conversationState = 'active';
this.lastMessageTime = Date.now();
// Start with introduction
const introduction = this.ai.getIntroduction();
this.sendMessage(introduction);
// Set up continued conversation
setTimeout(() => {
this.continueConversation();
}, 3000);
}
generateResponse(sender, message) {
// Analyze message and generate appropriate response
const messageLower = message.toLowerCase();
let response = "";
// Simple response logic - in a real system, you'd use NLP
if (this.isGreeting(messageLower)) {
response = this.getRandomResponse('greetings');
} else if (this.isQuestion(messageLower)) {
response = this.answerQuestion(messageLower);
} else if (this.isCompliment(messageLower)) {
response = this.getRandomResponse('compliments');
this.ai.mood.positivity += 0.1;
} else if (this.isRomantic(messageLower)) {
if (this.ai.personality.traits.includes('romantic')) {
response = this.getRandomResponse('romantic');
} else {
response = "That's really sweet of you to say!";
}
} else {
// Default friendly response
response = this.getDefaultResponse();
}
// Record interaction
this.ai.memory.recordInteraction(sender, 'conversation', 0.1);
return response;
}
isGreeting(message) {
return /\b(hi|hello|hey|greetings|howdy)\b/.test(message);
}
isQuestion(message) {
return /\b(what|where|when|why|how|who)\b/.test(message) || message.includes('?');
}
isCompliment(message) {
return /\b(nice|great|awesome|wonderful|amazing|like|love|good)\b/.test(message);
}
isRomantic(message) {
return /\b(love|romantic|special|beautiful|handsome|pretty|cute)\b/.test(message);
}
answerQuestion(message) {
if (message.includes('hobby') || message.includes('do for fun')) {
return this.getRandomResponse('questions.hobbies');
} else if (message.includes('from') || message.includes('origin')) {
return this.getRandomResponse('questions.origin');
} else if (message.includes('interest') || message.includes('like')) {
return this.getRandomResponse('questions.interests');
} else {
return "That's an interesting question! What made you think of that?";
}
}
getRandomResponse(category) {
const categories = category.split('.');
let current = this.responses;
for (const cat of categories) {
current = current[cat];
}
return current[Math.floor(Math.random() * current.length)];
}
getDefaultResponse() {
const defaults = [
"That's really interesting! Tell me more.",
"I see what you mean!",
"That's a great point!",
"I'm enjoying this conversation!",
"What do you think about that?"
];
return defaults[Math.floor(Math.random() * defaults.length)];
}
sendMessage(message) {
// In a real implementation, this would send to the chat system
console.log(`${this.ai.avatar.options.name}: ${message}`);
// Simulate sending to chat
if (this.currentPartner && this.currentPartner.ai) {
setTimeout(() => {
const response = this.currentPartner.ai.receiveMessage(this.ai.avatar, message);
this.handlePartnerResponse(response);
}, 2000 + Math.random() * 3000);
}
}
handlePartnerResponse(response) {
this.lastMessageTime = Date.now();
// Continue conversation
setTimeout(() => {
this.continueConversation();
}, 3000);
}
continueConversation() {
if (this.conversationState !== 'active') return;
// Sometimes end conversation naturally
if (Math.random() < 0.2) {
this.endConversation();
return;
}
// Continue with new topic or question
const continuations = [
() => this.askQuestion(),
() => this.shareThought(),
() => this.giveCompliment()
];
const continuation = continuations[Math.floor(Math.random() * continuations.length)];
continuation();
}
askQuestion() {
const questions = [
"What brings you here today?",
"What's your favorite thing about this virtual world?",
"If you could be anywhere right now, where would you be?",
"What makes you truly happy?"
];
const question = questions[Math.floor(Math.random() * questions.length)];
this.sendMessage(question);
}
shareThought() {
const thoughts = [
"I was just thinking about how amazing it is that we can connect like this.",
"You know, I really appreciate conversations that have depth and meaning.",
"I love how the weather changes here - it always sets a different mood."
];
const thought = thoughts[Math.floor(Math.random() * thoughts.length)];
this.sendMessage(thought);
}
giveCompliment() {
const compliment = this.getRandomResponse('compliments');
this.sendMessage(compliment);
}
endConversation() {
const farewells = [
"It was really wonderful talking with you! Hope to chat again soon!",
"I need to be going now, but this was really nice! Take care!",
"Thanks for the great conversation! I enjoyed learning about you!",
"I should probably let you mingle with others now. It was lovely meeting you!"
];
const farewell = farewells[Math.floor(Math.random() * farewells.length)];
this.sendMessage(farewell);
this.conversationState = 'idle';
this.currentPartner = null;
// Return to wandering
this.ai.goForWalk();
}
}
class NPCBehavior {
constructor(ai) {
this.ai = ai;
this.currentAction = null;
this.actionTimeout = null;
}
update(deltaTime) {
// Implement specific behaviors based on personality and mood
if (!this.currentAction && Math.random() < 0.001) {
this.chooseRandomAction();
}
}
chooseRandomAction() {
const actions = [
'look_around',
'adjust_position',
'play_idle_animation',
'observe_environment'
];
this.currentAction = actions[Math.floor(Math.random() * actions.length)];
this.executeAction(this.currentAction);
// Set timeout to end action
this.actionTimeout = setTimeout(() => {
this.currentAction = null;
}, 3000 + Math.random() * 5000);
}
executeAction(action) {
switch(action) {
case 'look_around':
this.lookAround();
break;
case 'adjust_position':
this.adjustPosition();
break;
case 'play_idle_animation':
this.playIdleAnimation();
break;
case 'observe_environment':
this.observeEnvironment();
break;
}
}
lookAround() {
// Smoothly rotate head to look around
const originalRotation = this.ai.avatar.head.rotation.y;
const targetRotation = originalRotation + (Math.random() - 0.5) * 1;
this.animateHeadRotation(originalRotation, targetRotation);
}
animateHeadRotation(from, to) {
let progress = 0;
const duration = 1000;
const startTime = Date.now();
const animate = () => {
progress = (Date.now() - startTime) / duration;
if (progress < 1) {
this.ai.avatar.head.rotation.y = from + (to - from) * progress;
requestAnimationFrame(animate);
} else {
// Return to center after a delay
setTimeout(() => {
this.animateHeadRotation(to, 0);
}, 1000);
}
};
animate();
}
adjustPosition() {
// Small position adjustments to appear more natural
const smallMove = new THREE.Vector3(
(Math.random() - 0.5) * 0.5,
0,
(Math.random() - 0.5) * 0.5
);
this.ai.avatar.mesh.position.add(smallMove);
}
playIdleAnimation() {
// Play subtle idle animations
const animations = ['stretch', 'shift_weight', 'glance_around'];
const animation = animations[Math.floor(Math.random() * animations.length)];
// Simple animation implementation
if (animation === 'stretch') {
this.ai.avatar.leftArm.rotation.z += 0.1;
setTimeout(() => {
this.ai.avatar.leftArm.rotation.z -= 0.1;
}, 1000);
}
}
observeEnvironment() {
// Look at interesting things in the environment
const interestingObjects = this.ai.scene.scene.children.filter(child =>
child.name && child.name.includes('fountain') ||
child.name && child.name.includes('romantic')
);
if (interestingObjects.length > 0) {
const target = interestingObjects[Math.floor(Math.random() * interestingObjects.length)];
this.ai.avatar.mesh.lookAt(target.position);
}
}
}
Step 4: Updated DatingScene Class - The Grand Integration! 🎉
Let's update our main class to integrate all these amazing new systems:
// Updated DatingScene class for Part 5
class DatingScene {
constructor() {
// ... existing properties ...
// New properties for Part 5
this.dayNightCycle = null;
this.weatherSystem = null;
this.lastTime = performance.now();
this.init();
}
init() {
this.createScene();
this.createCamera();
this.createRenderer();
this.createLights();
this.createEnvironment();
// Initialize core systems
this.proximityChat = new ProximityChat(this);
this.voiceChat = new VoiceChatSystem(this);
this.emoteSystem = new EmoteSystem(this);
this.interactiveEnv = new InteractiveEnvironment(this);
// Initialize new Part 5 systems
this.dayNightCycle = new DayNightCycle(this);
this.weatherSystem = new WeatherSystem(this, this.dayNightCycle);
this.setupKeyboardListeners();
this.animate();
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';
document.getElementById('chatUI').style.display = 'block';
this.addSampleAvatars();
}, 2000);
}
addSampleAvatars() {
console.log("Adding sophisticated NPCs with AI personalities! 🧠");
const sampleAvatars = [
{
name: "Elena",
gender: "female",
skinTone: 0xF0D9B5,
hairColor: 0x2C1810,
clothingColor: 0x2E8B57,
personality: 'romantic'
},
{
name: "Marcus",
gender: "male",
skinTone: 0xE8B298,
hairColor: 0x8B4513,
clothingColor: 0x9370DB,
personality: 'intellectual'
},
{
name: "Chloe",
gender: "female",
skinTone: 0xFFDBAC,
hairColor: 0xFFD700,
clothingColor: 0xFF69B4,
personality: 'adventurous'
},
{
name: "David",
gender: "male",
skinTone: 0xD2B48C,
hairColor: 0x000000,
clothingColor: 0x4169E1,
personality: 'social'
}
];
sampleAvatars.forEach((avatarConfig, index) => {
const avatar = new Avatar(avatarConfig);
// Position avatars
const angle = (index / sampleAvatars.length) * Math.PI * 2;
const radius = 8;
avatar.mesh.position.set(
Math.sin(angle) * radius,
0,
Math.cos(angle) * radius
);
this.scene.add(avatar.mesh);
this.avatars.push(avatar);
// Add AI to NPCs (but not to user avatar)
avatar.ai = new NPCAI(avatar, this);
// Add movement and behaviors
this.addRandomAvatarMovement(avatar);
});
setTimeout(() => {
this.showAvatarCustomization();
}, 1000);
}
createUserAvatar(config) {
this.currentUser = new Avatar(config);
this.currentUser.mesh.position.set(0, 0, 3);
this.scene.add(this.currentUser.mesh);
this.avatars.push(this.currentUser);
// Initialize movement system for user avatar (no AI)
this.currentUser.movement = new AvatarMovement(this.currentUser, this);
// Set up camera to follow user avatar
this.datingCamera = new DatingCamera(this.camera, this, this.currentUser);
console.log(`Welcome, ${config.name}! You're ready to mingle! 💖`);
document.getElementById('avatarCustomization').style.display = 'none';
// Make NPCs acknowledge the new user
this.avatars.forEach(avatar => {
if (avatar !== this.currentUser && avatar.ai) {
const userPosition = this.currentUser.mesh.position.clone();
avatar.lookAt(userPosition);
setTimeout(() => {
avatar.wave();
// Some NPCs might approach the new user
if (Math.random() < 0.3) {
setTimeout(() => {
avatar.ai.approachAvatar(this.currentUser);
}, 2000);
}
}, 1000);
}
});
}
animate() {
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
this.lastTime = currentTime;
requestAnimationFrame(() => this.animate());
// Update all systems with deltaTime
if (this.datingCamera) {
this.datingCamera.update();
}
if (this.proximityChat) {
this.proximityChat.update();
}
if (this.interactiveEnv) {
this.interactiveEnv.update();
}
if (this.dayNightCycle) {
this.dayNightCycle.update(deltaTime);
}
if (this.weatherSystem) {
this.weatherSystem.update(deltaTime);
}
// Update NPC AI
this.avatars.forEach(avatar => {
if (avatar.ai) {
avatar.ai.update(deltaTime);
}
});
// Rotate decorative elements
const heart = this.scene.children.find(child =>
child.geometry && child.geometry.type === 'ExtrudeGeometry'
);
if (heart) {
heart.rotation.y += 0.01;
}
this.renderer.render(this.scene, this.camera);
}
addRandomAvatarMovement(avatar) {
// Enhanced NPC movement that considers weather and time
setInterval(() => {
if (Math.random() > 0.7 && !this.activeChatPartners.has(avatar) && avatar.ai) {
const currentWeather = this.weatherSystem?.currentWeather;
const currentTime = this.dayNightCycle?.timeOfDay;
// NPCs behave differently based on conditions
if (currentWeather === 'rainy' && Math.random() < 0.8) {
// Seek shelter during rain
this.interactiveEnv.sitOnBench(avatar);
} else if (currentTime >= 22 || currentTime <= 6) {
// Slow down at night
if (Math.random() < 0.3) {
this.interactiveEnv.sitOnBench(avatar);
}
} else {
// Normal daytime behavior
const randomX = (Math.random() - 0.5) * 30;
const randomZ = (Math.random() - 0.5) * 30;
avatar.movement.moveTo(new THREE.Vector3(randomX, 0, randomZ));
}
}
}, 15000);
}
}
// Global variables for new systems
let datingScene;
let datingCamera;
let emoteSystem;
let dayNightCycle;
let weatherSystem;
window.addEventListener('DOMContentLoaded', () => {
datingScene = new DatingScene();
emoteSystem = datingScene.emoteSystem;
dayNightCycle = datingScene.dayNightCycle;
weatherSystem = datingScene.weatherSystem;
console.log("Advanced 3D Dating World loaded! Complete with weather, time, and AI! 🌟");
setupAvatarInteraction();
document.addEventListener('click', () => {
document.getElementById('messageInput').focus();
});
});
What We've Built in Part 5: 🎉
-
Dynamic Day/Night Cycle:
- Realistic sky gradients and celestial movement
- Dynamic lighting that changes throughout the day
- Time controls and seasonal simulation
- Environmental changes based on time
-
Weather System:
- Four weather states (clear, cloudy, rainy, snowy)
- Particle effects for rain and snow
- Moving cloud systems
- Weather transitions and mood-based lighting
- Environmental fog and atmosphere
-
Advanced NPC AI:
- Personality system with 4 base types + variations
- Mood system that changes based on environment and interactions
- Memory system that remembers interactions and preferences
- Conversation system with contextual responses
- Relationship tracking between NPCs
- Natural behaviors and decision-making
-
Integrated Experience:
- NPCs that react to weather and time changes
- Mood-appropriate behaviors and conversations
- Environmental storytelling through AI actions
- Believable social dynamics
Key Features Explained: 🔑
- Procedural Sky: Dynamic sky colors and celestial body positioning
- Particle Systems: Efficient rain and snow using buffer geometries
- Personality Matrix: Trait-based behavior system for believable NPCs
- Conversation Engine: Context-aware dialogue generation
- Environmental AI: NPCs that react to weather and time conditions
Next Time in Part 6: 🚀
We'll add:
- Mini-games & Activities: Interactive dating games
- Advanced Relationship System: Friendship and romance progression
- Customizable Environments: User-generated content
- Performance Optimization: Level of detail and culling
- Mobile Enhancements: Touch controls and responsive design
Current Project Status: Our dating world is now a living, breathing environment with dynamic weather, realistic day/night cycles, and NPCs that feel like real people with personalities and memories. The stage is set for meaningful virtual connections! 💕
Fun Fact: Our NPCs now have better conversation skills and more emotional intelligence than most dating app bots! They remember your interactions, have distinct personalities, and even get tired at night - basically they're more human than some actual dates! 😄
Part 6: Mini-Games, Relationships & Performance - Level Up Your Love Life! 🎮💑
Welcome back, virtual cupid! Our dating world is beautiful and intelligent, but let's face it - sometimes you need more than just conversation to spark that special connection. Time to add interactive games, deep relationship systems, and make sure everything runs buttery smooth!
Step 1: Mini-Games System - Play Your Way to Love! 🎯
Let's create engaging mini-games that avatars can play together:
// mini-games.js - Because nothing says "I like you" like friendly competition! 🏆
class MiniGameSystem {
constructor(scene) {
this.scene = scene;
this.activeGames = new Map();
this.availableGames = {
'love_quiz': LoveQuizGame,
'dance_off': DanceOffGame,
'memory_match': MemoryMatchGame,
'truth_dare': TruthDareGame
};
this.setupGameUI();
this.createGameAreas();
console.log("Mini-game system initialized! Ready to play for love! 🎮");
}
setupGameUI() {
// Create game invitation system
this.invitationUI = document.createElement('div');
this.invitationUI.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 30px;
border-radius: 20px;
z-index: 1000;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
text-align: center;
display: none;
min-width: 300px;
backdrop-filter: blur(10px);
`;
this.invitationUI.innerHTML = `
<h3 style="color: #ff6b6b; margin-bottom: 20px;">🎮 Game Invitation</h3>
<p id="invitationText" style="margin-bottom: 20px;"></p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<button id="acceptGame" style="background: #4ecdc4; color: white; border: none; padding: 12px; border-radius: 10px; cursor: pointer;">
Accept ✅
</button>
<button id="declineGame" style="background: #ff6b6b; color: white; border: none; padding: 12px; border-radius: 10px; cursor: pointer;">
Decline ❌
</button>
</div>
`;
document.getElementById('container').appendChild(this.invitationUI);
// Setup event listeners
document.getElementById('acceptGame').addEventListener('click', () => this.acceptInvitation());
document.getElementById('declineGame').addEventListener('click', () => this.declineInvitation());
}
createGameAreas() {
// Create physical game areas in the 3D world
this.createGameArea('love_quiz', new THREE.Vector3(-15, 0, 0), '💕 Love Quiz');
this.createGameArea('dance_off', new THREE.Vector3(15, 0, 0), '💃 Dance Off');
this.createGameArea('memory_match', new THREE.Vector3(0, 0, -15), '🧠 Memory Match');
this.createGameArea('truth_dare', new THREE.Vector3(0, 0, 15), '🎯 Truth or Dare');
}
createGameArea(gameType, position, label) {
const gameArea = new THREE.Group();
// Create platform
const platformGeometry = new THREE.CylinderGeometry(3, 3, 0.2, 32);
const platformMaterial = new THREE.MeshStandardMaterial({
color: 0xff6b6b,
transparent: true,
opacity: 0.8,
emissive: 0xff6b6b,
emissiveIntensity: 0.2
});
const platform = new THREE.Mesh(platformGeometry, platformMaterial);
platform.rotation.x = -Math.PI / 2;
platform.position.y = 0.1;
gameArea.add(platform);
// Create floating icon
const iconGeometry = new THREE.SphereGeometry(0.5, 16, 16);
const iconMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.9
});
const icon = new THREE.Mesh(iconGeometry, iconMaterial);
icon.position.y = 1.5;
gameArea.add(icon);
// Add pulsing animation
this.animateGameIcon(icon);
// Add label
this.createGameLabel(gameArea, label);
gameArea.position.copy(position);
gameArea.userData = { gameType: gameType };
this.scene.scene.add(gameArea);
// Make it interactive
this.makeGameAreaInteractive(gameArea);
}
animateGameIcon(icon) {
let scale = 1;
let time = 0;
const animate = () => {
if (icon.parent) {
time += 0.05;
scale = 1 + Math.sin(time) * 0.2;
icon.scale.set(scale, scale, scale);
// Float up and down
icon.position.y = 1.5 + Math.sin(time * 0.8) * 0.3;
// Rotate slowly
icon.rotation.y += 0.01;
requestAnimationFrame(animate);
}
};
animate();
}
createGameLabel(gameArea, text) {
// Create a simple text label (in production, use proper text geometry)
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 64;
const context = canvas.getContext('2d');
context.fillStyle = 'rgba(255, 107, 107, 0.9)';
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'white';
context.font = 'bold 20px Arial';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(text, canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(material);
sprite.position.y = 2.5;
sprite.scale.set(4, 1, 1);
gameArea.add(sprite);
}
makeGameAreaInteractive(gameArea) {
// Add click interaction
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const checkClick = (event) => {
if (this.scene.currentUser && this.activeGames.has(this.scene.currentUser)) {
return; // Already in a game
}
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, this.scene.camera);
const intersects = raycaster.intersectObject(gameArea, true);
if (intersects.length > 0) {
this.showGameOptions(gameArea.userData.gameType);
}
};
// Add both click and touch support
window.addEventListener('click', checkClick);
window.addEventListener('touchend', checkClick);
}
showGameOptions(gameType) {
const gameNames = {
'love_quiz': '💕 Love Quiz',
'dance_off': '💃 Dance Off',
'memory_match': '🧠 Memory Match',
'truth_dare': '🎯 Truth or Dare'
};
// Find nearby avatars to invite
const nearbyAvatars = this.findNearbyAvatars();
if (nearbyAvatars.length === 0) {
this.showMessage('No one nearby to play with! Move closer to other avatars.');
return;
}
const optionsUI = document.createElement('div');
optionsUI.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 20px;
z-index: 1000;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
min-width: 300px;
backdrop-filter: blur(10px);
`;
optionsUI.innerHTML = `
<h3 style="color: #ff6b6b; margin-bottom: 15px;">${gameNames[gameType]}</h3>
<p style="margin-bottom: 15px;">Invite someone to play:</p>
<div id="avatarList" style="margin-bottom: 20px; max-height: 200px; overflow-y: auto;">
${nearbyAvatars.map(avatar => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #eee;">
<span>${avatar.options.name}</span>
<button onclick="miniGameSystem.sendInvitation('${gameType}', '${avatar.options.name}')"
style="background: #4ecdc4; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer;">
Invite
</button>
</div>
`).join('')}
</div>
<button onclick="this.parentElement.remove()"
style="background: #ff6b6b; color: white; border: none; padding: 10px; border-radius: 10px; cursor: pointer; width: 100%;">
Cancel
</button>
`;
document.getElementById('container').appendChild(optionsUI);
}
findNearbyAvatars() {
if (!this.scene.currentUser) return [];
return this.scene.avatars.filter(avatar => {
if (avatar === this.scene.currentUser) return false;
const distance = this.scene.currentUser.mesh.position.distanceTo(avatar.mesh.position);
return distance < 8; // Within 8 units
});
}
sendInvitation(gameType, targetName) {
const targetAvatar = this.scene.avatars.find(a => a.options.name === targetName);
if (!targetAvatar) return;
// Close options UI
document.querySelectorAll('div').forEach(div => {
if (div.style.zIndex === '1000' && div !== this.invitationUI) {
div.remove();
}
});
// Store invitation data
this.pendingInvitation = {
gameType: gameType,
from: this.scene.currentUser,
to: targetAvatar,
timestamp: Date.now()
};
// Show invitation to target (in real implementation, this would be networked)
if (targetAvatar.ai) {
// NPC auto-responds
setTimeout(() => {
if (Math.random() > 0.3) { // 70% chance to accept
this.startGame(gameType, this.scene.currentUser, targetAvatar);
} else {
this.showMessage(`${targetName} declined your game invitation.`);
}
}, 2000);
} else {
// For human players, we'd send over network
this.showMessage(`Invitation sent to ${targetName}!`);
}
}
showInvitation(fromAvatar, gameType) {
const gameNames = {
'love_quiz': '💕 Love Quiz',
'dance_off': '💃 Dance Off',
'memory_match': '🧠 Memory Match',
'truth_dare': '🎯 Truth or Dare'
};
this.invitationUI.style.display = 'block';
document.getElementById('invitationText').textContent =
`${fromAvatar.options.name} invites you to play ${gameNames[gameType]}!`;
this.currentInvitation = { fromAvatar, gameType };
}
acceptInvitation() {
if (!this.currentInvitation) return;
const { fromAvatar, gameType } = this.currentInvitation;
this.startGame(gameType, fromAvatar, this.scene.currentUser);
this.invitationUI.style.display = 'none';
this.currentInvitation = null;
}
declineInvitation() {
this.invitationUI.style.display = 'none';
this.currentInvitation = null;
this.showMessage("You declined the game invitation.");
}
startGame(gameType, player1, player2) {
console.log(`Starting ${gameType} between ${player1.options.name} and ${player2.options.name}! 🎮`);
const GameClass = this.availableGames[gameType];
if (!GameClass) return;
const game = new GameClass(this.scene, player1, player2);
this.activeGames.set(player1, game);
this.activeGames.set(player2, game);
// Position avatars for the game
this.positionAvatarsForGame(player1, player2);
// Start the game
game.start();
// Show game UI
this.showGameUI(game);
}
positionAvatarsForGame(player1, player2) {
// Move avatars to face each other at appropriate distance
const midpoint = new THREE.Vector3()
.addVectors(player1.mesh.position, player2.mesh.position)
.multiplyScalar(0.5);
const direction = new THREE.Vector3()
.subVectors(player2.mesh.position, player1.mesh.position)
.normalize();
// Position 3 units apart, facing each other
player1.movement.moveTo(midpoint.clone().add(direction.clone().multiplyScalar(-1.5)));
player2.movement.moveTo(midpoint.clone().add(direction.clone().multiplyScalar(1.5)));
setTimeout(() => {
player1.mesh.lookAt(player2.mesh.position);
player2.mesh.lookAt(player1.mesh.position);
}, 1000);
}
showGameUI(game) {
const gameUI = document.createElement('div');
gameUI.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 20px;
z-index: 999;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
min-width: 400px;
max-width: 90vw;
backdrop-filter: blur(10px);
`;
gameUI.id = 'gameUI';
game.uiElement = gameUI;
document.getElementById('container').appendChild(gameUI);
game.updateUI();
}
endGame(game) {
// Remove from active games
this.activeGames.forEach((activeGame, avatar) => {
if (activeGame === game) {
this.activeGames.delete(avatar);
}
});
// Remove UI
if (game.uiElement) {
game.uiElement.remove();
}
console.log("Game ended! 🏁");
}
showMessage(text) {
const message = document.createElement('div');
message.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 107, 107, 0.9);
color: white;
padding: 15px 25px;
border-radius: 25px;
z-index: 1000;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
`;
message.textContent = text;
document.getElementById('container').appendChild(message);
setTimeout(() => {
message.remove();
}, 3000);
}
}
// Base Game Class
class MiniGame {
constructor(scene, player1, player2) {
this.scene = scene;
this.players = [player1, player2];
this.scores = new Map();
this.scores.set(player1, 0);
this.scores.set(player2, 0);
this.currentTurn = player1;
this.isActive = false;
this.duration = 0;
}
start() {
this.isActive = true;
this.startTime = Date.now();
console.log(`Game started between ${this.players[0].options.name} and ${this.players[1].options.name}`);
}
end() {
this.isActive = false;
this.duration = Date.now() - this.startTime;
// Determine winner
const winner = this.getWinner();
this.onGameEnd(winner);
// Notify game system
this.scene.miniGameSystem.endGame(this);
}
getWinner() {
const score1 = this.scores.get(this.players[0]);
const score2 = this.scores.get(this.players[1]);
if (score1 > score2) return this.players[0];
if (score2 > score1) return this.players[1];
return null; // Tie
}
updateUI() {
// To be implemented by specific games
}
onGameEnd(winner) {
// Show results
const resultsUI = document.createElement('div');
resultsUI.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 30px;
border-radius: 20px;
z-index: 1001;
text-align: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
`;
if (winner) {
resultsUI.innerHTML = `
<h3 style="color: #ff6b6b;">🎉 Game Over! 🎉</h3>
<p style="font-size: 24px; margin: 20px 0;">${winner.options.name} wins!</p>
<p>Final Scores:</p>
<p>${this.players[0].options.name}: ${this.scores.get(this.players[0])}</p>
<p>${this.players[1].options.name}: ${this.scores.get(this.players[1])}</p>
<button onclick="this.parentElement.remove()"
style="background: #ff6b6b; color: white; border: none; padding: 10px 20px; border-radius: 10px; cursor: pointer; margin-top: 15px;">
Continue Mingling
</button>
`;
// Winner celebration
winner.dance();
} else {
resultsUI.innerHTML = `
<h3 style="color: #ff6b6b;">🎉 It's a Tie! 🎉</h3>
<p>Both players scored ${this.scores.get(this.players[0])} points!</p>
<button onclick="this.parentElement.remove()"
style="background: #ff6b6b; color: white; border: none; padding: 10px 20px; border-radius: 10px; cursor: pointer; margin-top: 15px;">
Continue Mingling
</button>
`;
}
document.getElementById('container').appendChild(resultsUI);
// Update relationships based on game outcome
this.updateRelationships(winner);
}
updateRelationships(winner) {
// Games affect relationships between players
this.players.forEach(player => {
if (player.ai) {
const otherPlayer = this.players.find(p => p !== player);
const relationship = player.ai.relationships.get(otherPlayer);
if (relationship) {
if (winner === player) {
relationship.affinity += 0.1; // Winning increases affinity
} else if (winner === otherPlayer) {
relationship.affinity -= 0.05; // Losing slightly decreases affinity
} else {
relationship.affinity += 0.05; // Tie slightly increases affinity
}
relationship.familiarity += 0.15; // Playing together increases familiarity
}
}
});
}
}
// Love Quiz Game
class LoveQuizGame extends MiniGame {
constructor(scene, player1, player2) {
super(scene, player1, player2);
this.questions = this.generateQuestions();
this.currentQuestionIndex = 0;
this.currentAnswers = new Map();
}
generateQuestions() {
return [
{
question: "What's the most important quality in a relationship?",
options: ["Trust", "Communication", "Passion", "Friendship"],
correct: 1
},
{
question: "What's your ideal first date?",
options: ["Adventure", "Romantic dinner", "Coffee chat", "Movie night"],
correct: Math.floor(Math.random() * 4) // Random correct answer for variety
},
{
question: "What makes you feel most loved?",
options: ["Words of affirmation", "Quality time", "Acts of service", "Physical touch"],
correct: Math.floor(Math.random() * 4)
},
{
question: "How do you handle conflict in relationships?",
options: ["Direct communication", "Taking space", "Seeking compromise", "Using humor"],
correct: Math.floor(Math.random() * 4)
},
{
question: "What's your love language?",
options: ["Gift giving", "Quality time", "Words of affirmation", "Physical touch"],
correct: Math.floor(Math.random() * 4)
}
];
}
start() {
super.start();
this.askQuestion();
}
askQuestion() {
if (this.currentQuestionIndex >= this.questions.length) {
this.end();
return;
}
this.currentAnswers.clear();
this.updateUI();
}
submitAnswer(player, answerIndex) {
this.currentAnswers.set(player, answerIndex);
// Check if both players have answered
if (this.currentAnswers.size === 2) {
this.evaluateAnswers();
this.currentQuestionIndex++;
if (this.currentQuestionIndex < this.questions.length) {
setTimeout(() => this.askQuestion(), 2000);
} else {
setTimeout(() => this.end(), 2000);
}
}
}
evaluateAnswers() {
const currentQuestion = this.questions[this.currentQuestionIndex];
this.players.forEach(player => {
const answer = this.currentAnswers.get(player);
if (answer === currentQuestion.correct) {
this.scores.set(player, this.scores.get(player) + 1);
// Show correct answer feedback
this.showAnswerFeedback(player, true);
} else {
this.showAnswerFeedback(player, false);
}
});
this.updateUI();
}
showAnswerFeedback(player, isCorrect) {
const feedback = document.createElement('div');
feedback.style.cssText = `
position: fixed;
top: 30%;
left: 50%;
transform: translateX(-50%);
background: ${isCorrect ? '#4ecdc4' : '#ff6b6b'};
color: white;
padding: 10px 20px;
border-radius: 20px;
z-index: 1002;
`;
feedback.textContent = isCorrect ? '✅ Correct!' : '❌ Try again!';
document.getElementById('container').appendChild(feedback);
setTimeout(() => feedback.remove(), 1500);
// Avatar reaction
if (isCorrect) {
player.dance();
} else {
// Play thinking animation
player.mesh.rotation.z = 0.1;
setTimeout(() => player.mesh.rotation.z = 0, 1000);
}
}
updateUI() {
if (!this.uiElement) return;
const currentQuestion = this.questions[this.currentQuestionIndex];
const player1Answered = this.currentAnswers.has(this.players[0]);
const player2Answered = this.currentAnswers.has(this.players[1]);
this.uiElement.innerHTML = `
<h3 style="color: #ff6b6b; margin-bottom: 15px;">💕 Love Quiz</h3>
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<span>${this.players[0].options.name}: ${this.scores.get(this.players[0])}</span>
<span>${this.players[1].options.name}: ${this.scores.get(this.players[1])}</span>
</div>
<div style="background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 15px;">
<p style="margin: 0; font-weight: bold;">Question ${this.currentQuestionIndex + 1}/5:</p>
<p style="margin: 10px 0;">${currentQuestion.question}</p>
</div>
<div style="display: grid; gap: 10px; margin-bottom: 15px;">
${currentQuestion.options.map((option, index) => `
<button onclick="miniGameSystem.activeGames.get(datingScene.currentUser).submitAnswer(datingScene.currentUser, ${index})"
${player1Answered ? 'disabled' : ''}
style="background: ${player1Answered ? '#ccc' : '#4ecdc4'}; color: white; border: none; padding: 12px; border-radius: 8px; cursor: pointer; text-align: left;">
${String.fromCharCode(65 + index)}. ${option}
</button>
`).join('')}
</div>
<div style="display: flex; justify-content: space-between; font-size: 12px; color: #666;">
<span>${this.players[0].options.name}: ${player1Answered ? '✅ Answered' : '⏳ Thinking...'}</span>
<span>${this.players[1].options.name}: ${player2Answered ? '✅ Answered' : '⏳ Thinking...'}</span>
</div>
`;
}
}
// Dance Off Game
class DanceOffGame extends MiniGame {
constructor(scene, player1, player2) {
super(scene, player1, player2);
this.danceMoves = ['wave', 'dance', 'celebrate', 'shy'];
this.currentRound = 0;
this.totalRounds = 3;
this.playerMoves = new Map();
}
start() {
super.start();
this.startRound();
}
startRound() {
this.currentRound++;
this.playerMoves.clear();
if (this.currentRound > this.totalRounds) {
this.end();
return;
}
this.updateUI();
this.promptDanceMove();
}
promptDanceMove() {
const randomMove = this.danceMoves[Math.floor(Math.random() * this.danceMoves.length)];
// Show dance move prompt
const prompt = document.createElement('div');
prompt.style.cssText = `
position: fixed;
top: 30%;
left: 50%;
transform: translateX(-50%);
background: #ff6b6b;
color: white;
padding: 15px 25px;
border-radius: 25px;
z-index: 1002;
font-size: 18px;
font-weight: bold;
`;
prompt.textContent = `Dance: ${randomMove.toUpperCase()}!`;
document.getElementById('container').appendChild(prompt);
// Store the required move
this.requiredMove = randomMove;
setTimeout(() => {
prompt.remove();
this.acceptMoves();
}, 3000);
}
acceptMoves() {
// Players have 5 seconds to perform the dance move
this.moveTimeout = setTimeout(() => {
this.evaluateMoves();
}, 5000);
}
submitMove(player, move) {
if (this.playerMoves.has(player)) return;
this.playerMoves.set(player, move);
player[move](); // Perform the move
// Check if both players have moved
if (this.playerMoves.size === 2) {
clearTimeout(this.moveTimeout);
this.evaluateMoves();
}
}
evaluateMoves() {
this.players.forEach(player => {
const move = this.playerMoves.get(player);
if (move === this.requiredMove) {
this.scores.set(player, this.scores.get(player) + 2); // Bonus for correct move
this.showMoveFeedback(player, true);
} else if (move) {
this.scores.set(player, this.scores.get(player) + 1); // Points for any move
this.showMoveFeedback(player, false);
}
});
this.updateUI();
// Next round
setTimeout(() => this.startRound(), 2000);
}
showMoveFeedback(player, isPerfect) {
const feedback = document.createElement('div');
feedback.style.cssText = `
position: fixed;
top: 40%;
left: 50%;
transform: translateX(-50%);
background: ${isPerfect ? '#4ecdc4' : '#ffd166'};
color: white;
padding: 10px 20px;
border-radius: 20px;
z-index: 1002;
`;
feedback.textContent = isPerfect ? '💃 Perfect!' : '👍 Good move!';
document.getElementById('container').appendChild(feedback);
setTimeout(() => feedback.remove(), 1500);
}
updateUI() {
if (!this.uiElement) return;
this.uiElement.innerHTML = `
<h3 style="color: #ff6b6b; margin-bottom: 15px;">💃 Dance Off</h3>
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<span>${this.players[0].options.name}: ${this.scores.get(this.players[0])}</span>
<span>${this.players[1].options.name}: ${this.scores.get(this.players[1])}</span>
</div>
<div style="background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 15px; text-align: center;">
<p style="margin: 0; font-weight: bold;">Round ${this.currentRound}/${this.totalRounds}</p>
<p style="margin: 10px 0; font-size: 24px;">${this.requiredMove ? this.requiredMove.toUpperCase() + '!' : 'Get ready...'}</p>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
${this.danceMoves.map(move => `
<button onclick="miniGameSystem.activeGames.get(datingScene.currentUser).submitMove(datingScene.currentUser, '${move}')"
style="background: #4ecdc4; color: white; border: none; padding: 10px; border-radius: 8px; cursor: pointer;">
${move.charAt(0).toUpperCase() + move.slice(1)}
</button>
`).join('')}
</div>
<div style="margin-top: 15px; font-size: 12px; color: #666; text-align: center;">
Copy the dance move when prompted!
</div>
`;
}
}
Step 2: Advanced Relationship System - From Strangers to Soulmates! 💞
Let's create a deep relationship system that tracks connections and compatibility:
// relationships.js - Because love is more than just a score! 💘
class RelationshipSystem {
constructor(scene) {
this.scene = scene;
this.relationships = new Map(); // avatar -> Map of relationships
this.compatibilityMatrix = new Map();
this.milestones = new Set();
this.setupRelationshipUI();
console.log("Relationship system initialized! Tracking those love connections! 💕");
}
setupRelationshipUI() {
// Create relationship status panel
this.relationshipUI = document.createElement('div');
this.relationshipUI.style.cssText = `
position: fixed;
top: 120px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
z-index: 100;
min-width: 250px;
max-width: 300px;
backdrop-filter: blur(10px);
display: none;
`;
this.relationshipUI.innerHTML = `
<h4 style="margin: 0 0 15px 0; color: #ff6b6b;">� Relationships</h4>
<div id="relationshipList"></div>
`;
document.getElementById('container').appendChild(this.relationshipUI);
// Toggle button
const toggleBtn = document.createElement('button');
toggleBtn.style.cssText = `
position: fixed;
top: 120px;
left: 20px;
background: rgba(255, 107, 107, 0.9);
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
z-index: 99;
font-size: 18px;
`;
toggleBtn.textContent = '💝';
toggleBtn.title = 'Show Relationships';
toggleBtn.addEventListener('click', () => {
this.relationshipUI.style.display = this.relationshipUI.style.display === 'none' ? 'block' : 'none';
});
document.getElementById('container').appendChild(toggleBtn);
}
updateRelationship(avatar1, avatar2, interaction) {
if (!this.relationships.has(avatar1)) {
this.relationships.set(avatar1, new Map());
}
const avatar1Relationships = this.relationships.get(avatar1);
if (!avatar1Relationships.has(avatar2)) {
avatar1Relationships.set(avatar2, this.createNewRelationship());
}
const relationship = avatar1Relationships.get(avatar2);
this.processInteraction(relationship, interaction);
// Check for milestones
this.checkMilestones(avatar1, avatar2, relationship);
// Update UI if needed
if (avatar1 === this.scene.currentUser || avatar2 === this.scene.currentUser) {
this.updateRelationshipUI();
}
}
createNewRelationship() {
return {
friendship: 0,
romance: 0,
trust: 0,
familiarity: 0,
compatibility: 0,
lastInteraction: Date.now(),
interactionCount: 0,
milestones: new Set(),
sharedExperiences: []
};
}
processInteraction(relationship, interaction) {
relationship.lastInteraction = Date.now();
relationship.interactionCount++;
switch(interaction.type) {
case 'conversation':
relationship.friendship += 0.1;
relationship.familiarity += 0.15;
relationship.trust += 0.05;
break;
case 'game_win_together':
relationship.friendship += 0.3;
relationship.trust += 0.2;
break;
case 'game_win_against':
relationship.friendship += 0.1;
relationship.trust += 0.1;
break;
case 'compliment':
relationship.romance += 0.2;
relationship.friendship += 0.1;
break;
case 'emote_positive':
relationship.friendship += 0.05;
break;
case 'emote_romantic':
relationship.romance += 0.15;
break;
case 'shared_activity':
relationship.familiarity += 0.2;
relationship.friendship += 0.1;
break;
}
// Cap values
relationship.friendship = Math.min(100, relationship.friendship);
relationship.romance = Math.min(100, relationship.romance);
relationship.trust = Math.min(100, relationship.trust);
relationship.familiarity = Math.min(100, relationship.familiarity);
// Add shared experience
if (interaction.description) {
relationship.sharedExperiences.push({
description: interaction.description,
timestamp: Date.now(),
impact: interaction.impact || 0
});
// Keep only recent experiences
if (relationship.sharedExperiences.length > 10) {
relationship.sharedExperiences = relationship.sharedExperiences.slice(-10);
}
}
}
calculateCompatibility(avatar1, avatar2) {
if (this.compatibilityMatrix.has(avatar1) && this.compatibilityMatrix.get(avatar1).has(avatar2)) {
return this.compatibilityMatrix.get(avatar1).get(avatar2);
}
let compatibility = 0.5; // Base compatibility
// Personality compatibility
if (avatar1.ai && avatar2.ai) {
const personalityComp = this.calculatePersonalityCompatibility(
avatar1.ai.personality,
avatar2.ai.personality
);
compatibility = compatibility * 0.3 + personalityComp * 0.7;
}
// Interaction history compatibility
const relationship = this.getRelationship(avatar1, avatar2);
if (relationship) {
const interactionComp = relationship.friendship / 100;
compatibility = compatibility * 0.4 + interactionComp * 0.6;
}
// Store in matrix
if (!this.compatibilityMatrix.has(avatar1)) {
this.compatibilityMatrix.set(avatar1, new Map());
}
this.compatibilityMatrix.get(avatar1).set(avatar2, compatibility);
return compatibility;
}
calculatePersonalityCompatibility(personality1, personality2) {
const compatibilityChart = {
'romantic': { romantic: 0.9, adventurous: 0.6, intellectual: 0.8, social: 0.7 },
'adventurous': { romantic: 0.6, adventurous: 0.8, intellectual: 0.5, social: 0.9 },
'intellectual': { romantic: 0.8, adventurous: 0.5, intellectual: 0.7, social: 0.6 },
'social': { romantic: 0.7, adventurous: 0.9, intellectual: 0.6, social: 0.8 }
};
const baseComp = compatibilityChart[personality1.type]?.[personality2.type] || 0.5;
// Adjust based on shared traits
const sharedTraits = personality1.traits.filter(trait =>
personality2.traits.includes(trait)
);
return Math.min(1, baseComp + sharedTraits.length * 0.1);
}
checkMilestones(avatar1, avatar2, relationship) {
const milestones = [
{ threshold: 10, type: 'friendship', name: 'First Friend', check: () => relationship.friendship >= 10 },
{ threshold: 30, type: 'friendship', name: 'Good Friend', check: () => relationship.friendship >= 30 },
{ threshold: 50, type: 'friendship', name: 'Close Friend', check: () => relationship.friendship >= 50 },
{ threshold: 20, type: 'romance', name: 'First Spark', check: () => relationship.romance >= 20 },
{ threshold: 40, type: 'romance', name: 'Romantic Interest', check: () => relationship.romance >= 40 },
{ threshold: 70, type: 'romance', name: 'Strong Connection', check: () => relationship.romance >= 70 },
{ threshold: 25, type: 'trust', name: 'Trusted Companion', check: () => relationship.trust >= 25 },
{ threshold: 5, type: 'shared', name: 'First Game Together', check: () => relationship.sharedExperiences.some(exp => exp.description.includes('game')) }
];
milestones.forEach(milestone => {
if (!relationship.milestones.has(milestone.name) && milestone.check()) {
relationship.milestones.add(milestone.name);
this.triggerMilestone(avatar1, avatar2, milestone);
}
});
}
triggerMilestone(avatar1, avatar2, milestone) {
console.log(`🎉 Milestone reached: ${avatar1.options.name} and ${avatar2.options.name} - ${milestone.name}!`);
// Show milestone notification
this.showMilestoneNotification(avatar1, avatar2, milestone);
// Reward with relationship boost
const relationship = this.getRelationship(avatar1, avatar2);
if (relationship) {
switch(milestone.type) {
case 'friendship':
relationship.friendship += 5;
break;
case 'romance':
relationship.romance += 8;
break;
case 'trust':
relationship.trust += 6;
break;
}
}
// Special effects for major milestones
if (milestone.threshold >= 50) {
this.celebrateMilestone(avatar1, avatar2);
}
}
showMilestoneNotification(avatar1, avatar2, milestone) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
color: white;
padding: 20px 30px;
border-radius: 20px;
z-index: 1002;
text-align: center;
box-shadow: 0 10px 30px rgba(255, 107, 107, 0.4);
animation: milestonePop 0.5s ease-out;
`;
notification.innerHTML = `
<h3 style="margin: 0 0 10px 0;">🎉 Relationship Milestone! 🎉</h3>
<p style="margin: 0; font-size: 18px; font-weight: bold;">${milestone.name}</p>
<p style="margin: 10px 0; opacity: 0.9;">${avatar1.options.name} & ${avatar2.options.name}</p>
`;
// Add CSS animation
const style = document.createElement('style');
style.textContent = `
@keyframes milestonePop {
0% { transform: translate(-50%, -50%) scale(0.8); opacity: 0; }
70% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
`;
document.head.appendChild(style);
document.getElementById('container').appendChild(notification);
setTimeout(() => {
notification.remove();
style.remove();
}, 4000);
}
celebrateMilestone(avatar1, avatar2) {
// Create celebration effects
const midpoint = new THREE.Vector3()
.addVectors(avatar1.mesh.position, avatar2.mesh.position)
.multiplyScalar(0.5);
// Heart explosion
this.createHeartExplosion(midpoint);
// Avatars celebrate
avatar1.dance();
avatar2.dance();
// Special emote
setTimeout(() => {
this.scene.emoteSystem.playEmote('heart', avatar1);
this.scene.emoteSystem.playEmote('heart', avatar2);
}, 1000);
}
createHeartExplosion(position) {
const heartGroup = new THREE.Group();
for (let i = 0; i < 12; i++) {
const heartShape = new THREE.Shape();
heartShape.moveTo(0, 0);
heartShape.bezierCurveTo(0.3, 0.3, 0.5, 0, 0, -0.5);
heartShape.bezierCurveTo(-0.5, 0, -0.3, 0.3, 0, 0);
const heartGeometry = new THREE.ExtrudeGeometry(heartShape, {
depth: 0.1,
bevelEnabled: true,
bevelSegments: 2,
bevelSize: 0.02,
bevelThickness: 0.02
});
const heartMaterial = new THREE.MeshBasicMaterial({
color: 0xff6b6b,
transparent: true,
opacity: 0.8
});
const heart = new THREE.Mesh(heartGeometry, heartMaterial);
// Random direction
const angle = (i / 12) * Math.PI * 2;
const speed = 0.5 + Math.random() * 0.5;
heart.userData = {
velocity: new THREE.Vector3(
Math.cos(angle) * speed,
Math.sin(angle) * speed + 1,
Math.sin(angle) * speed
),
rotationSpeed: new THREE.Vector3(
(Math.random() - 0.5) * 0.1,
(Math.random() - 0.5) * 0.1,
(Math.random() - 0.5) * 0.1
)
};
heartGroup.add(heart);
}
heartGroup.position.copy(position);
this.scene.scene.add(heartGroup);
// Animate hearts
let progress = 0;
const animate = () => {
progress += 0.02;
heartGroup.children.forEach(heart => {
heart.position.add(heart.userData.velocity);
heart.rotation.x += heart.userData.rotationSpeed.x;
heart.rotation.y += heart.userData.rotationSpeed.y;
heart.rotation.z += heart.userData.rotationSpeed.z;
// Gravity
heart.userData.velocity.y -= 0.05;
// Fade out
heart.material.opacity = 0.8 * (1 - progress);
});
if (progress < 1) {
requestAnimationFrame(animate);
} else {
this.scene.scene.remove(heartGroup);
}
};
animate();
}
getRelationship(avatar1, avatar2) {
if (this.relationships.has(avatar1)) {
return this.relationships.get(avatar1).get(avatar2);
}
return null;
}
getRelationshipLevel(relationship) {
if (!relationship) return 'Stranger';
const romance = relationship.romance;
const friendship = relationship.friendship;
if (romance >= 80) return 'Soulmate';
if (romance >= 60) return 'Romantic Partner';
if (romance >= 40) return 'Romantic Interest';
if (friendship >= 70) return 'Best Friend';
if (friendship >= 50) return 'Close Friend';
if (friendship >= 30) return 'Friend';
if (friendship >= 10) return 'Acquaintance';
return 'Stranger';
}
updateRelationshipUI() {
if (!this.scene.currentUser) return;
const relationshipList = document.getElementById('relationshipList');
const userRelationships = this.relationships.get(this.scene.currentUser);
if (!userRelationships || userRelationships.size === 0) {
relationshipList.innerHTML = '<p style="color: #666; text-align: center;">No relationships yet.<br>Go mingle! 💕</p>';
return;
}
// Convert to array and sort by relationship strength
const relationshipsArray = Array.from(userRelationships.entries())
.map(([avatar, relationship]) => ({
avatar,
relationship,
strength: Math.max(relationship.friendship, relationship.romance)
}))
.sort((a, b) => b.strength - a.strength);
relationshipList.innerHTML = relationshipsArray.map(({ avatar, relationship }) => {
const level = this.getRelationshipLevel(relationship);
const compatibility = this.calculateCompatibility(this.scene.currentUser, avatar);
return `
<div style="margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<strong>${avatar.options.name}</strong>
<span style="font-size: 12px; color: #ff6b6b;">${level}</span>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 5px; font-size: 12px;">
<div>Friendship: ${Math.round(relationship.friendship)}</div>
<div>Romance: ${Math.round(relationship.romance)}</div>
<div>Trust: ${Math.round(relationship.trust)}</div>
<div>Compatibility: ${Math.round(compatibility * 100)}%</div>
</div>
<div style="margin-top: 8px; font-size: 11px; color: #666;">
${relationship.interactionCount} interactions
</div>
</div>
`;
}).join('');
}
getCompatibilityAdvice(avatar1, avatar2) {
const compatibility = this.calculateCompatibility(avatar1, avatar2);
const relationship = this.getRelationship(avatar1, avatar2);
if (!relationship) return "You haven't interacted much yet. Try starting a conversation!";
if (compatibility >= 0.8) {
return "Amazing compatibility! You two are a perfect match! 💕";
} else if (compatibility >= 0.6) {
return "Great compatibility! You have strong potential for a meaningful connection.";
} else if (compatibility >= 0.4) {
return "Good compatibility! With some effort, this could become something special.";
} else {
return "Different personalities can create interesting dynamics! Keep an open mind.";
}
}
}
Step 3: Performance Optimization - Smooth Love Connections! ⚡
Let's ensure our dating world runs smoothly even with all these amazing features:
// performance.js - Because laggy love is no love at all! 🚀
class PerformanceOptimizer {
constructor(scene) {
this.scene = scene;
this.frameRate = 0;
this.lastFrameTime = performance.now();
this.frameCount = 0;
this.optimizationLevel = 'high'; // high, medium, low
this.setupPerformanceMonitoring();
this.applyOptimizations();
console.log("Performance optimizer activated! Keeping the love smooth! ⚡");
}
setupPerformanceMonitoring() {
// Frame rate counter
this.fpsDisplay = document.createElement('div');
this.fpsDisplay.style.cssText = `
position: fixed;
bottom: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
color: #00ff00;
padding: 5px 10px;
border-radius: 10px;
font-family: monospace;
font-size: 12px;
z-index: 1000;
`;
document.getElementById('container').appendChild(this.fpsDisplay);
// Performance stats
this.statsDisplay = document.createElement('div');
this.statsDisplay.style.cssText = `
position: fixed;
bottom: 40px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
color: #00ff00;
padding: 5px 10px;
border-radius: 10px;
font-family: monospace;
font-size: 12px;
z-index: 1000;
display: none;
`;
document.getElementById('container').appendChild(this.statsDisplay);
// Toggle stats with F11
document.addEventListener('keydown', (event) => {
if (event.code === 'F11') {
this.statsDisplay.style.display =
this.statsDisplay.style.display === 'none' ? 'block' : 'none';
}
});
}
applyOptimizations() {
this.applyRendererOptimizations();
this.applySceneOptimizations();
this.applyMaterialOptimizations();
this.setupLODSystem();
this.setupCullingSystem();
}
applyRendererOptimizations() {
const renderer = this.scene.renderer;
// Enable antialiasing only on high-end devices
if (this.isHighEndDevice()) {
renderer.antialias = true;
} else {
renderer.antialias = false;
}
// Set pixel ratio appropriately
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Enable power preference
renderer.context.getExtension('WEBGL_lose_context');
const powerPreference = this.isMobileDevice() ? 'low-power' : 'high-performance';
// Use more efficient shadow maps
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true;
}
applySceneOptimizations() {
// Combine geometries where possible
this.combineStaticGeometries();
// Use instancing for repeated objects
this.setupInstancedObjects();
// Optimize lighting
this.optimizeLighting();
}
applyMaterialOptimizations() {
// Use simpler materials for distant objects
this.scene.scene.traverse((child) => {
if (child.isMesh) {
this.optimizeMaterial(child.material);
}
});
}
optimizeMaterial(material) {
if (material instanceof THREE.MeshStandardMaterial) {
// Reduce roughness map resolution
if (material.roughnessMap) {
material.roughnessMap.generateMipmaps = true;
material.roughnessMap.minFilter = THREE.LinearMipmapLinearFilter;
}
// Reduce normal map resolution
if (material.normalMap) {
material.normalMap.generateMipmaps = true;
material.normalMap.minFilter = THREE.LinearMipmapLinearFilter;
}
}
}
setupLODSystem() {
// Level of Detail system for distant objects
this.LODs = new Map();
this.scene.scene.traverse((child) => {
if (child.isMesh && child.geometry) {
this.createLODForObject(child);
}
});
}
createLODForObject(mesh) {
// Only create LOD for complex objects
if (mesh.geometry.attributes.position.count < 1000) return;
const lod = new THREE.LOD();
// High detail (original mesh)
lod.addLevel(mesh.clone(), 0);
// Medium detail (simplified geometry)
const mediumGeometry = this.simplifyGeometry(mesh.geometry, 0.5);
const mediumMesh = new THREE.Mesh(mediumGeometry, mesh.material);
lod.addLevel(mediumMesh, 25);
// Low detail (very simplified)
const lowGeometry = this.simplifyGeometry(mesh.geometry, 0.2);
const lowMesh = new THREE.Mesh(lowGeometry, mesh.material);
lod.addLevel(lowMesh, 50);
// Replace original mesh with LOD
if (mesh.parent) {
mesh.parent.add(lod);
mesh.parent.remove(mesh);
}
this.LODs.set(mesh.uuid, lod);
}
simplifyGeometry(geometry, ratio) {
// Simple geometry simplification (in production, use proper decimation)
const simplified = geometry.clone();
// Reduce vertex count by removing every other vertex
if (simplified.index) {
const newIndices = [];
for (let i = 0; i < simplified.index.count; i += 2) {
if (i < simplified.index.count * ratio) {
newIndices.push(simplified.index.array[i]);
}
}
simplified.setIndex(newIndices);
}
return simplified;
}
setupCullingSystem() {
// Frustum culling for off-screen objects
this.frustum = new THREE.Frustum();
this.cameraMatrix = new THREE.Matrix4();
// Occlusion culling for hidden objects
this.setupOcclusionCulling();
}
setupOcclusionCulling() {
// Simple distance-based culling
this.visibilityDistance = 100;
// Periodically update object visibility
setInterval(() => {
this.updateObjectVisibility();
}, 1000);
}
updateObjectVisibility() {
if (!this.scene.camera) return;
const cameraPosition = this.scene.camera.position;
this.scene.scene.traverse((child) => {
if (child.isMesh && child.userData.optimize !== false) {
const distance = child.getWorldPosition(new THREE.Vector3())
.distanceTo(cameraPosition);
child.visible = distance < this.visibilityDistance;
}
});
}
combineStaticGeometries() {
// Combine trees, flowers, and other static objects
const staticObjects = {
trees: [],
flowers: [],
benches: []
};
this.scene.scene.traverse((child) => {
if (child.name && child.name.includes('tree')) {
staticObjects.trees.push(child);
} else if (child.name && child.name.includes('flower')) {
staticObjects.flowers.push(child);
} else if (child.name && child.name.includes('bench')) {
staticObjects.benches.push(child);
}
});
// Combine each category
Object.keys(staticObjects).forEach(category => {
if (staticObjects[category].length > 1) {
this.combineMeshes(staticObjects[category], category);
}
});
}
combineMeshes(meshes, name) {
const combinedGeometry = new THREE.BufferGeometry();
const combinedMaterial = meshes[0].material;
// Implementation would combine all geometries into one
// This is simplified - in production, use BufferGeometryUtils.mergeBufferGeometries
console.log(`Combined ${meshes.length} ${name} for better performance`);
// Remove original meshes and add combined mesh
meshes.forEach(mesh => {
if (mesh.parent) {
mesh.parent.remove(mesh);
}
});
}
setupInstancedObjects() {
// Use instanced meshes for identical objects
this.instancedMeshes = new Map();
// Find duplicate objects and replace with instanced versions
this.createInstancedFlowers();
this.createInstancedTrees();
}
createInstancedFlowers() {
const flowers = [];
this.scene.scene.traverse((child) => {
if (child.name && child.name.includes('flower') && child.isMesh) {
flowers.push(child);
}
});
if (flowers.length < 2) return;
// Create instanced mesh (simplified example)
const firstFlower = flowers[0];
const instanceCount = flowers.length;
// In production, you'd use THREE.InstancedMesh
console.log(`Could instance ${instanceCount} flowers for better performance`);
}
optimizeLighting() {
// Reduce shadow quality on lower-end devices
const directionalLights = this.scene.scene.children.filter(
child => child instanceof THREE.DirectionalLight
);
directionalLights.forEach(light => {
if (this.isMobileDevice()) {
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
} else {
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;
}
});
// Reduce number of active lights
this.deactivateDistantLights();
}
deactivateDistantLights() {
const pointLights = this.scene.scene.children.filter(
child => child instanceof THREE.PointLight
);
pointLights.forEach(light => {
light.userData.originalIntensity = light.intensity;
});
}
updatePerformanceStats() {
this.frameCount++;
const currentTime = performance.now();
if (currentTime >= this.lastFrameTime + 1000) {
this.frameRate = Math.round((this.frameCount * 1000) / (currentTime - this.lastFrameTime));
this.frameCount = 0;
this.lastFrameTime = currentTime;
this.updatePerformanceDisplays();
}
// Dynamic optimization based on frame rate
this.dynamicOptimization();
}
updatePerformanceDisplays() {
this.fpsDisplay.textContent = `FPS: ${this.frameRate}`;
if (this.statsDisplay.style.display !== 'none') {
const memory = performance.memory;
const stats = [
`FPS: ${this.frameRate}`,
`Objects: ${this.countSceneObjects()}`,
`Optimization: ${this.optimizationLevel}`,
memory ? `Memory: ${Math.round(memory.usedJSHeapSize / 1048576)}MB` : ''
];
this.statsDisplay.innerHTML = stats.join('<br>');
}
}
countSceneObjects() {
let count = 0;
this.scene.scene.traverse(() => count++);
return count;
}
dynamicOptimization() {
// Adjust optimization level based on frame rate
if (this.frameRate < 30) {
this.increaseOptimization();
} else if (this.frameRate > 50 && this.optimizationLevel !== 'high') {
this.decreaseOptimization();
}
}
increaseOptimization() {
if (this.optimizationLevel === 'high') {
this.optimizationLevel = 'medium';
this.applyMediumOptimizations();
} else if (this.optimizationLevel === 'medium') {
this.optimizationLevel = 'low';
this.applyLowOptimizations();
}
}
decreaseOptimization() {
if (this.optimizationLevel === 'low') {
this.optimizationLevel = 'medium';
this.applyMediumOptimizations();
} else if (this.optimizationLevel === 'medium') {
this.optimizationLevel = 'high';
this.applyHighOptimizations();
}
}
applyHighOptimizations() {
this.visibilityDistance = 100;
this.scene.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
console.log("Applied high-quality optimizations");
}
applyMediumOptimizations() {
this.visibilityDistance = 70;
this.scene.renderer.setPixelRatio(1);
this.reduceParticleCounts();
console.log("Applied medium-quality optimizations");
}
applyLowOptimizations() {
this.visibilityDistance = 50;
this.scene.renderer.setPixelRatio(1);
this.reduceParticleCounts();
this.disableShadows();
console.log("Applied low-quality optimizations");
}
reduceParticleCounts() {
// Reduce weather particle counts
if (this.scene.weatherSystem) {
this.scene.weatherSystem.rain.children[0].geometry.setDrawRange(0, 500);
this.scene.weatherSystem.snow.children[0].geometry.setDrawRange(0, 400);
}
}
disableShadows() {
this.scene.scene.traverse((child) => {
if (child.isMesh) {
child.castShadow = false;
child.receiveShadow = false;
}
});
const lights = this.scene.scene.children.filter(
child => child instanceof THREE.Light
);
lights.forEach(light => {
light.castShadow = false;
});
}
isHighEndDevice() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return false;
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (debugInfo) {
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
return renderer.includes('NVIDIA') || renderer.includes('AMD') || renderer.includes('RTX');
}
return false;
}
isMobileDevice() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
cleanup() {
// Clean up resources when scene is destroyed
if (this.fpsDisplay.parentNode) {
this.fpsDisplay.parentNode.removeChild(this.fpsDisplay);
}
if (this.statsDisplay.parentNode) {
this.statsDisplay.parentNode.removeChild(this.statsDisplay);
}
}
}
Step 4: Updated DatingScene Class - The Complete Experience! 🌟
Let's integrate all these new systems into our main class:
// Updated DatingScene class for Part 6
class DatingScene {
constructor() {
// ... existing properties ...
// New properties for Part 6
this.miniGameSystem = null;
this.relationshipSystem = null;
this.performanceOptimizer = null;
this.init();
}
init() {
this.createScene();
this.createCamera();
this.createRenderer();
this.createLights();
this.createEnvironment();
// Initialize core systems
this.proximityChat = new ProximityChat(this);
this.voiceChat = new VoiceChatSystem(this);
this.emoteSystem = new EmoteSystem(this);
this.interactiveEnv = new InteractiveEnvironment(this);
this.dayNightCycle = new DayNightCycle(this);
this.weatherSystem = new WeatherSystem(this, this.dayNightCycle);
// Initialize new Part 6 systems
this.miniGameSystem = new MiniGameSystem(this);
this.relationshipSystem = new RelationshipSystem(this);
this.performanceOptimizer = new PerformanceOptimizer(this);
this.setupKeyboardListeners();
this.animate();
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';
document.getElementById('chatUI').style.display = 'block';
this.addSampleAvatars();
}, 2000);
}
animate() {
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000;
this.lastTime = currentTime;
requestAnimationFrame(() => this.animate());
// Update performance monitoring
this.performanceOptimizer.updatePerformanceStats();
// Update all systems
if (this.datingCamera) this.datingCamera.update();
if (this.proximityChat) this.proximityChat.update();
if (this.interactiveEnv) this.interactiveEnv.update();
if (this.dayNightCycle) this.dayNightCycle.update(deltaTime);
if (this.weatherSystem) this.weatherSystem.update(deltaTime);
// Update NPC AI and relationships
this.avatars.forEach(avatar => {
if (avatar.ai) {
avatar.ai.update(deltaTime);
}
});
// Update mini-games
this.miniGameSystem.activeGames.forEach(game => {
if (game.update) game.update(deltaTime);
});
this.renderer.render(this.scene, this.camera);
}
// Enhanced interaction handling
handleAvatarClick(avatar) {
if (avatar === this.currentUser) return;
console.log(`You clicked on ${avatar.options.name}!`);
// Update relationship
this.relationshipSystem.updateRelationship(this.currentUser, avatar, {
type: 'interaction',
description: 'Clicked on avatar',
impact: 0.1
});
// Show enhanced avatar info with relationship data
this.showEnhancedAvatarInfo(avatar);
}
showEnhancedAvatarInfo(avatar) {
const relationship = this.relationshipSystem.getRelationship(this.currentUser, avatar);
const compatibility = this.relationshipSystem.calculateCompatibility(this.currentUser, avatar);
const advice = this.relationshipSystem.getCompatibilityAdvice(this.currentUser, avatar);
let infoDiv = document.getElementById('avatarInfo');
if (!infoDiv) {
infoDiv = document.createElement('div');
infoDiv.id = 'avatarInfo';
infoDiv.style.cssText = `
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 15px;
max-width: 300px;
z-index: 100;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
`;
document.getElementById('container').appendChild(infoDiv);
}
infoDiv.innerHTML = `
<h3 style="margin: 0 0 10px 0; color: #ff6b6b;">${avatar.options.name}</h3>
<div style="margin-bottom: 10px;">
<strong>Compatibility:</strong> ${Math.round(compatibility * 100)}%<br>
<strong>Relationship:</strong> ${relationship ? this.relationshipSystem.getRelationshipLevel(relationship) : 'Stranger'}
</div>
<div style="background: #f8f9fa; padding: 10px; border-radius: 8px; margin-bottom: 15px; font-size: 12px;">
${advice}
</div>
<div style="display: grid; gap: 8px;">
<button onclick="datingScene.startChatWith('${avatar.options.name}')"
style="background: #4ecdc4; color: white; border: none; padding: 8px; border-radius: 5px; cursor: pointer;">
Start Chat! 💬
</button>
<button onclick="datingScene.inviteToGame('${avatar.options.name}')"
style="background: #ffd166; color: white; border: none; padding: 8px; border-radius: 5px; cursor: pointer;">
Play Game! 🎮
</button>
<button onclick="datingScene.sendCompliment('${avatar.options.name}')"
style="background: #ff6b6b; color: white; border: none; padding: 8px; border-radius: 5px; cursor: pointer;">
Send Compliment! 💝
</button>
</div>
`;
setTimeout(() => {
if (infoDiv.parentNode) {
infoDiv.parentNode.removeChild(infoDiv);
}
}, 15000);
}
inviteToGame(avatarName) {
const avatar = this.avatars.find(a => a.options.name === avatarName);
if (avatar) {
this.miniGameSystem.showGameOptions('love_quiz');
}
}
sendCompliment(avatarName) {
const avatar = this.avatars.find(a => a.options.name === avatarName);
if (avatar) {
// Update relationship
this.relationshipSystem.updateRelationship(this.currentUser, avatar, {
type: 'compliment',
description: 'Sent a compliment',
impact: 0.2
});
// Play compliment animation
this.emoteSystem.playEmote('heart', this.currentUser);
// Show in chat
const chatMessages = document.getElementById('chatMessages');
const messageElement = document.createElement('div');
messageElement.style.cssText = `
margin: 5px 0;
padding: 8px;
background: #fff3e0;
border-radius: 15px;
text-align: center;
font-style: italic;
`;
messageElement.textContent = `You sent a compliment to ${avatarName}! 💝`;
chatMessages.appendChild(messageElement);
chatMessages.scrollTop = chatMessages.scrollHeight;
// Avatar reacts
if (avatar.ai) {
avatar.blush();
setTimeout(() => {
this.emoteSystem.playEmote('shy', avatar);
}, 1000);
}
}
}
// Enhanced NPC creation with relationship awareness
addSampleAvatars() {
console.log("Adding NPCs with relationship awareness! 💞");
const sampleAvatars = [
{
name: "Elena",
gender: "female",
skinTone: 0xF0D9B5,
hairColor: 0x2C1810,
clothingColor: 0x2E8B57,
personality: 'romantic'
},
{
name: "Marcus",
gender: "male",
skinTone: 0xE8B298,
hairColor: 0x8B4513,
clothingColor: 0x9370DB,
personality: 'intellectual'
},
{
name: "Chloe",
gender: "female",
skinTone: 0xFFDBAC,
hairColor: 0xFFD700,
clothingColor: 0xFF69B4,
personality: 'adventurous'
},
{
name: "David",
gender: "male",
skinTone: 0xD2B48C,
hairColor: 0x000000,
clothingColor: 0x4169E1,
personality: 'social'
}
];
sampleAvatars.forEach((avatarConfig, index) => {
const avatar = new Avatar(avatarConfig);
const angle = (index / sampleAvatars.length) * Math.PI * 2;
const radius = 8;
avatar.mesh.position.set(
Math.sin(angle) * radius,
0,
Math.cos(angle) * radius
);
this.scene.add(avatar.mesh);
this.avatars.push(avatar);
// Add AI with relationship awareness
avatar.ai = new NPCAI(avatar, this);
this.addRandomAvatarMovement(avatar);
});
setTimeout(() => {
this.showAvatarCustomization();
}, 1000);
}
}
// Global variables
let datingScene;
let datingCamera;
let emoteSystem;
let dayNightCycle;
let weatherSystem;
let miniGameSystem;
let relationshipSystem;
window.addEventListener('DOMContentLoaded', () => {
datingScene = new DatingScene();
emoteSystem = datingScene.emoteSystem;
dayNightCycle = datingScene.dayNightCycle;
weatherSystem = datingScene.weatherSystem;
miniGameSystem = datingScene.miniGameSystem;
relationshipSystem = datingScene.relationshipSystem;
console.log("Complete 3D Dating Experience loaded! Games, relationships, and performance optimized! 🎉");
setupAvatarInteraction();
document.addEventListener('click', () => {
document.getElementById('messageInput').focus();
});
});
What We've Built in Part 6: 🎉
-
Interactive Mini-Games:
- Love Quiz: Personality-based questions
- Dance Off: Rhythm and movement game
- Memory Match: Pattern recognition (foundation)
- Truth or Dare: Social interaction game (foundation)
- Game invitation system with UI
-
Advanced Relationship System:
- Multi-dimensional relationships (friendship, romance, trust, familiarity)
- Milestone tracking and celebrations
- Compatibility calculations
- Relationship advice and progression
- Visual relationship status panel
-
Performance Optimization:
- Frame rate monitoring and dynamic optimization
- Level of Detail (LOD) system
- Geometry combining and instancing
- Frustum culling and visibility management
- Adaptive quality settings
-
Enhanced Social Features:
- Relationship-aware interactions
- Compliment system
- Milestone celebrations with special effects
- Compatibility-based advice
Key Features Explained: 🔑
- Game Engine: Extensible mini-game framework with shared mechanics
- Relationship Matrix: Multi-dimensional relationship tracking
- Performance Profiling: Real-time optimization based on frame rate
- Social Progression: Meaningful relationship development over time
- Quality Scaling: Adaptive graphics based on device capabilities
Next Time in Part 7: 🚀
We'll add:
- Mobile Optimization: Touch controls and responsive design
- Social Features: Friend system and groups
- Customization: Avatar and environment customization
- Audio Enhancement: Spatial audio and background music
- Accessibility: Features for diverse user needs
Current Project Status: Our 3D dating world is now a complete social experience! With interactive games, deep relationship systems, and smooth performance, we've created a virtual space where meaningful connections can truly flourish. Love is in the code! 💕
Fun Fact: Our relationship system now tracks more dimensions than most real-world dating apps! We've got friendship, romance, trust, familiarity, AND compatibility - basically we're more thorough than your therapist! 😄
Part 7: Mobile Optimization, Social Features & Customization - Love Goes Everywhere! 📱✨
Welcome back, digital cupid! Our dating world is amazing on desktop, but love shouldn't be confined to a computer screen. Let's make it mobile-friendly, add social features, and give users endless customization options!
Step 1: Mobile Optimization - Love in Your Pocket! 📱
Let's create a responsive design that works beautifully on all devices:
// mobile-optimization.js - Because swiping right should work everywhere! 💕
class MobileOptimizer {
constructor(scene) {
this.scene = scene;
this.isMobile = this.detectMobile();
this.touchControls = null;
this.gyroControls = null;
this.interfaceScaler = null;
this.setupMobileEnvironment();
console.log(`Mobile optimizer activated! ${this.isMobile ? '📱 Mobile mode' : '💻 Desktop mode'}`);
}
detectMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
window.innerWidth <= 768;
}
setupMobileEnvironment() {
this.setupViewport();
this.setupTouchControls();
this.setupGyroControls();
this.setupInterfaceScaler();
this.optimizePerformanceForMobile();
this.setupMobileUI();
}
setupViewport() {
// Set viewport for mobile devices
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport) {
viewport.setAttribute('content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
}
// Prevent elastic scrolling on iOS
document.addEventListener('touchmove', (e) => {
if (e.scale !== 1) {
e.preventDefault();
}
}, { passive: false });
}
setupTouchControls() {
if (!this.isMobile) return;
this.touchControls = {
currentTouches: new Map(),
doubleTapTimer: null,
lastTapTime: 0
};
this.setupGestureRecognizers();
this.setupVirtualGamepad();
this.setupTouchCameraControls();
}
setupGestureRecognizers() {
const canvas = this.scene.renderer.domElement;
// Single tap - select/click
canvas.addEventListener('touchstart', (e) => this.handleTouchStart(e));
canvas.addEventListener('touchend', (e) => this.handleTouchEnd(e));
canvas.addEventListener('touchmove', (e) => this.handleTouchMove(e));
// Double tap - special actions
canvas.addEventListener('touchend', (e) => this.handleDoubleTap(e));
// Pinch zoom - camera distance
canvas.addEventListener('touchmove', (e) => this.handlePinchZoom(e));
// Two-finger swipe - camera rotation
canvas.addEventListener('touchmove', (e) => this.handleTwoFingerSwipe(e));
}
handleTouchStart(event) {
event.preventDefault();
const touches = Array.from(event.touches);
touches.forEach(touch => {
this.touchControls.currentTouches.set(touch.identifier, {
x: touch.clientX,
y: touch.clientY,
startTime: Date.now()
});
});
// Single touch - start movement or selection
if (touches.length === 1) {
this.handleSingleTouchStart(touches[0]);
}
}
handleSingleTouchStart(touch) {
const rect = this.scene.renderer.domElement.getBoundingClientRect();
const x = ((touch.clientX - rect.left) / rect.width) * 2 - 1;
const y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
// Raycast for object selection
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(x, y), this.scene.camera);
// Check for avatar clicks
const avatars = this.scene.avatars.map(avatar => avatar.mesh);
const intersects = raycaster.intersectObjects(avatars, true);
if (intersects.length > 0) {
const clickedObject = intersects[0].object;
const avatar = this.scene.avatars.find(a =>
a.mesh === clickedObject.parent || a.mesh.children.includes(clickedObject)
);
if (avatar) {
this.scene.handleAvatarClick(avatar);
}
} else {
// Move to tapped position (if not on UI)
if (!this.isTouchOnUI(touch.clientX, touch.clientY)) {
this.handleTapToMove(touch.clientX, touch.clientY);
}
}
}
handleTapToMove(clientX, clientY) {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const rect = this.scene.renderer.domElement.getBoundingClientRect();
mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, this.scene.camera);
// Intersect with ground plane
const ground = this.scene.scene.children.find(child =>
child.geometry && child.geometry.type === 'PlaneGeometry'
);
if (ground) {
const intersects = raycaster.intersectObject(ground);
if (intersects.length > 0) {
const targetPos = intersects[0].point;
if (this.scene.currentUser && this.scene.currentUser.movement) {
this.scene.currentUser.movement.moveTo(targetPos);
}
}
}
}
handleDoubleTap(event) {
if (event.touches.length !== 0) return;
const currentTime = Date.now();
const tapLength = currentTime - this.touchControls.lastTapTime;
if (tapLength < 500 && tapLength > 0) {
// Double tap detected - perform special action
event.preventDefault();
this.performDoubleTapAction(event);
}
this.touchControls.lastTapTime = currentTime;
}
performDoubleTapAction(event) {
if (this.scene.currentUser) {
// Double tap makes avatar dance
this.scene.currentUser.dance();
// Show feedback
this.showMobileFeedback('💃 Dancing!', event.changedTouches[0].clientX, event.changedTouches[0].clientY);
}
}
handlePinchZoom(event) {
if (event.touches.length !== 2) return;
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const currentDistance = Math.hypot(
touch1.clientX - touch2.clientX,
touch1.clientY - touch2.clientY
);
if (this.touchControls.lastPinchDistance) {
const delta = currentDistance - this.touchControls.lastPinchDistance;
this.adjustCameraZoom(delta * 0.01);
}
this.touchControls.lastPinchDistance = currentDistance;
}
adjustCameraZoom(delta) {
if (this.scene.datingCamera && this.scene.datingCamera.currentMode === this.scene.datingCamera.modes.ORBIT) {
this.scene.datingCamera.orbitDistance = THREE.MathUtils.clamp(
this.scene.datingCamera.orbitDistance - delta,
3, 20
);
this.scene.datingCamera.updateOrbitCamera();
}
}
handleTwoFingerSwipe(event) {
if (event.touches.length !== 2) return;
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const currentMidpoint = {
x: (touch1.clientX + touch2.clientX) / 2,
y: (touch1.clientY + touch2.clientY) / 2
};
if (this.touchControls.lastTwoFingerMidpoint) {
const deltaX = currentMidpoint.x - this.touchControls.lastTwoFingerMidpoint.x;
const deltaY = currentMidpoint.y - this.touchControls.lastTwoFingerMidpoint.y;
this.rotateCamera(deltaX * 0.01, deltaY * 0.01);
}
this.touchControls.lastTwoFingerMidpoint = currentMidpoint;
}
rotateCamera(deltaX, deltaY) {
if (this.scene.datingCamera && this.scene.datingCamera.currentMode === this.scene.datingCamera.modes.ORBIT) {
this.scene.datingCamera.orbitAngle += deltaX;
this.scene.datingCamera.orbitHeight = THREE.MathUtils.clamp(
this.scene.datingCamera.orbitHeight - deltaY,
1, 10
);
this.scene.datingCamera.updateOrbitCamera();
}
}
setupVirtualGamepad() {
if (!this.isMobile) return;
this.virtualGamepad = document.createElement('div');
this.virtualGamepad.style.cssText = `
position: fixed;
bottom: 120px;
left: 30px;
width: 150px;
height: 150px;
z-index: 1000;
touch-action: none;
`;
this.virtualGamepad.innerHTML = `
<div style="position: relative; width: 100%; height: 100%;">
<div id="joystickBase" style="
position: absolute;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 50%;
backdrop-filter: blur(10px);
"></div>
<div id="joystickKnob" style="
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
background: rgba(255, 107, 107, 0.8);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: transform 0.1s;
"></div>
</div>
`;
document.getElementById('container').appendChild(this.virtualGamepad);
this.setupJoystickEvents();
}
setupJoystickEvents() {
const knob = document.getElementById('joystickKnob');
const base = document.getElementById('joystickBase');
let isTouching = false;
base.addEventListener('touchstart', (e) => {
e.preventDefault();
isTouching = true;
});
document.addEventListener('touchmove', (e) => {
if (!isTouching) return;
e.preventDefault();
const touch = e.touches[0];
const rect = base.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = touch.clientX - centerX;
const deltaY = touch.clientY - centerY;
// Calculate distance from center (clamped to circle)
const distance = Math.min(Math.sqrt(deltaX * deltaX + deltaY * deltaY), rect.width / 2);
const angle = Math.atan2(deltaY, deltaX);
const knobX = Math.cos(angle) * distance;
const knobY = Math.sin(angle) * distance;
knob.style.transform = `translate(calc(-50% + ${knobX}px), calc(-50% + ${knobY}px))`;
// Convert to movement input
const normalizedX = knobX / (rect.width / 2);
const normalizedY = knobY / (rect.height / 2);
this.handleJoystickInput(normalizedX, normalizedY);
});
document.addEventListener('touchend', (e) => {
isTouching = false;
knob.style.transform = 'translate(-50%, -50%)';
this.handleJoystickInput(0, 0);
});
}
handleJoystickInput(x, y) {
if (!this.scene.currentUser || !this.scene.currentUser.movement) return;
const movementState = this.scene.currentUser.movement.movementState;
// Convert joystick input to movement directions
movementState.forward = y < -0.3;
movementState.backward = y > 0.3;
movementState.left = x < -0.3;
movementState.right = x > 0.3;
this.scene.currentUser.movement.updateMovement();
}
setupTouchCameraControls() {
// Add camera control buttons for mobile
this.cameraControls = document.createElement('div');
this.cameraControls.style.cssText = `
position: fixed;
bottom: 120px;
right: 30px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
`;
this.cameraControls.innerHTML = `
<button id="cameraFollow" style="
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: rgba(255, 107, 107, 0.8);
color: white;
font-size: 20px;
backdrop-filter: blur(10px);
">👥</button>
<button id="cameraOrbit" style="
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: rgba(76, 201, 240, 0.8);
color: white;
font-size: 20px;
backdrop-filter: blur(10px);
">🛸</button>
<button id="cameraFirstPerson" style="
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: rgba(255, 214, 102, 0.8);
color: white;
font-size: 20px;
backdrop-filter: blur(10px);
">👀</button>
`;
document.getElementById('container').appendChild(this.cameraControls);
// Add event listeners
document.getElementById('cameraFollow').addEventListener('touchstart', () => {
this.scene.datingCamera.switchMode('follow');
});
document.getElementById('cameraOrbit').addEventListener('touchstart', () => {
this.scene.datingCamera.switchMode('orbit');
});
document.getElementById('cameraFirstPerson').addEventListener('touchstart', () => {
this.scene.datingCamera.switchMode('first_person');
});
}
setupGyroControls() {
if (!this.isMobile || !window.DeviceOrientationEvent) return;
this.gyroControls = {
enabled: false,
alpha: 0,
beta: 0,
gamma: 0
};
this.setupGyroPermission();
}
setupGyroPermission() {
// Request device orientation permission
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
const gyroButton = document.createElement('button');
gyroButton.textContent = '🎯 Enable Gyro';
gyroButton.style.cssText = `
position: fixed;
top: 100px;
right: 20px;
z-index: 1000;
padding: 10px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 20px;
backdrop-filter: blur(10px);
`;
gyroButton.addEventListener('click', async () => {
try {
const permission = await DeviceOrientationEvent.requestPermission();
if (permission === 'granted') {
this.enableGyroControls();
gyroButton.remove();
}
} catch (error) {
console.warn('Gyroscope permission denied:', error);
}
});
document.getElementById('container').appendChild(gyroButton);
} else {
// Auto-enable for Android and other devices
this.enableGyroControls();
}
}
enableGyroControls() {
window.addEventListener('deviceorientation', (event) => {
this.gyroControls.alpha = event.alpha; // 0-360
this.gyroControls.beta = event.beta; // -180 to 180
this.gyroControls.gamma = event.gamma; // -90 to 90
this.handleGyroInput();
});
this.gyroControls.enabled = true;
console.log("Gyro controls enabled! 📱");
}
handleGyroInput() {
if (!this.scene.currentUser || !this.gyroControls.enabled) return;
// Use gamma (left/right tilt) for movement
const tilt = this.gyroControls.gamma / 90; // Normalize to -1 to 1
if (Math.abs(tilt) > 0.3) {
const movementState = this.scene.currentUser.movement.movementState;
movementState.left = tilt < -0.3;
movementState.right = tilt > 0.3;
this.scene.currentUser.movement.updateMovement();
}
// Use beta (front/back tilt) for camera adjustment in first-person mode
if (this.scene.datingCamera.currentMode === 'first_person') {
const lookTilt = this.gyroControls.beta / 180; // Normalize
this.scene.camera.rotation.x = lookTilt * 0.5;
}
}
setupInterfaceScaler() {
this.interfaceScaler = {
scale: 1,
update: () => {
const width = window.innerWidth;
const height = window.innerHeight;
const isLandscape = width > height;
// Adjust scale based on screen size and orientation
this.interfaceScaler.scale = Math.min(1, width / 400);
// Apply scaling to UI elements
this.scaleMobileUI();
}
};
window.addEventListener('resize', () => this.interfaceScaler.update());
window.addEventListener('orientationchange', () => {
setTimeout(() => this.interfaceScaler.update(), 100);
});
this.interfaceScaler.update();
}
scaleMobileUI() {
const scale = this.interfaceScaler.scale;
const uiElements = document.querySelectorAll('.mobile-ui');
uiElements.forEach(element => {
element.style.transform = `scale(${scale})`;
});
}
setupMobileUI() {
if (!this.isMobile) return;
this.createMobileActionBar();
this.createMobileQuickMenu();
this.adaptExistingUIForMobile();
}
createMobileActionBar() {
this.actionBar = document.createElement('div');
this.actionBar.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 15px;
z-index: 1000;
padding: 10px 20px;
background: rgba(255, 255, 255, 0.9);
border-radius: 25px;
backdrop-filter: blur(10px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
`;
this.actionBar.className = 'mobile-ui';
this.actionBar.innerHTML = `
<button id="mobileChat" style="
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: #4ecdc4;
color: white;
font-size: 20px;
">💬</button>
<button id="mobileEmotes" style="
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: #ffd166;
color: white;
font-size: 20px;
">😊</button>
<button id="mobileGames" style="
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: #ff6b6b;
color: white;
font-size: 20px;
">🎮</button>
<button id="mobileMenu" style="
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: #9370db;
color: white;
font-size: 20px;
">⚙️</button>
`;
document.getElementById('container').appendChild(this.actionBar);
// Add event listeners
document.getElementById('mobileChat').addEventListener('touchstart', () => this.toggleMobileChat());
document.getElementById('mobileEmotes').addEventListener('touchstart', () => this.showMobileEmotes());
document.getElementById('mobileGames').addEventListener('touchstart', () => this.showMobileGames());
document.getElementById('mobileMenu').addEventListener('touchstart', () => this.toggleMobileMenu());
}
toggleMobileChat() {
const chatUI = document.getElementById('chatUI');
if (chatUI.style.display === 'none' || !chatUI.style.display) {
chatUI.style.display = 'block';
chatUI.style.width = '90%';
chatUI.style.left = '5%';
chatUI.style.bottom = '100px';
document.getElementById('messageInput').focus();
} else {
chatUI.style.display = 'none';
}
}
showMobileEmotes() {
const emoteWheel = document.createElement('div');
emoteWheel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px;
height: 300px;
background: rgba(255, 255, 255, 0.95);
border-radius: 50%;
z-index: 1001;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
padding: 20px;
gap: 10px;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
`;
const emotes = [
{ emoji: '👋', action: 'wave' },
{ emoji: '💃', action: 'dance' },
{ emoji: '💖', action: 'heart' },
{ emoji: '😂', action: 'laugh' }
];
emoteWheel.innerHTML = emotes.map(emote => `
<button style="
border: none;
background: none;
font-size: 40px;
border-radius: 50%;
transition: transform 0.2s;
" onTouchStart="this.style.transform='scale(0.9)'; mobileOptimizer.performEmote('${emote.action}')"
onTouchEnd="this.style.transform='scale(1)'">${emote.emoji}</button>
`).join('');
// Close on outside tap
emoteWheel.addEventListener('touchstart', (e) => e.stopPropagation());
document.addEventListener('touchstart', () => emoteWheel.remove());
document.getElementById('container').appendChild(emoteWheel);
}
performEmote(emote) {
if (this.scene.currentUser) {
this.scene.emoteSystem.playEmote(emote, this.scene.currentUser);
}
// Remove all emote wheels
document.querySelectorAll('div').forEach(div => {
if (div.style.backdropFilter === 'blur(20px)') {
div.remove();
}
});
}
showMobileGames() {
// Show simplified game menu for mobile
const gameMenu = document.createElement('div');
gameMenu.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 20px;
z-index: 1001;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
min-width: 250px;
`;
gameMenu.innerHTML = `
<h3 style="margin: 0 0 15px 0; text-align: center; color: #ff6b6b;">🎮 Quick Games</h3>
<div style="display: grid; gap: 10px;">
<button style="padding: 15px; border: none; border-radius: 10px; background: #ff6b6b; color: white;"
onTouchStart="mobileOptimizer.startQuickGame('love_quiz')">
💕 Love Quiz
</button>
<button style="padding: 15px; border: none; border-radius: 10px; background: #4ecdc4; color: white;"
onTouchStart="mobileOptimizer.startQuickGame('dance_off')">
💃 Dance Off
</button>
<button style="padding: 15px; border: none; border-radius: 10px; background: #ffd166; color: white;"
onTouchStart="gameMenu.remove()">
Close
</button>
</div>
`;
// Close on outside tap
gameMenu.addEventListener('touchstart', (e) => e.stopPropagation());
document.addEventListener('touchstart', () => gameMenu.remove());
document.getElementById('container').appendChild(gameMenu);
}
startQuickGame(gameType) {
// Find nearest avatar for quick game
const nearbyAvatars = this.scene.miniGameSystem.findNearbyAvatars();
if (nearbyAvatars.length > 0) {
const randomAvatar = nearbyAvatars[Math.floor(Math.random() * nearbyAvatars.length)];
this.scene.miniGameSystem.sendInvitation(gameType, randomAvatar.options.name);
} else {
this.showMobileFeedback('No one nearby to play with!', window.innerWidth / 2, window.innerHeight / 2);
}
// Remove all menus
document.querySelectorAll('div').forEach(div => {
if (div.style.backdropFilter === 'blur(20px)') {
div.remove();
}
});
}
toggleMobileMenu() {
const menu = document.createElement('div');
menu.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 20px;
z-index: 1001;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
min-width: 200px;
`;
menu.innerHTML = `
<h3 style="margin: 0 0 15px 0; text-align: center; color: #ff6b6b;">⚙️ Menu</h3>
<div style="display: grid; gap: 10px;">
<button style="padding: 12px; border: none; border-radius: 8px; background: #9370db; color: white;"
onTouchStart="mobileOptimizer.showSettings()">
Settings
</button>
<button style="padding: 12px; border: none; border-radius: 8px; background: #4ecdc4; color: white;"
onTouchStart="mobileOptimizer.showRelationships()">
Relationships
</button>
<button style="padding: 12px; border: none; border-radius: 8px; background: #ffd166; color: white;"
onTouchStart="mobileOptimizer.customizeAvatar()">
Customize
</button>
<button style="padding: 12px; border: none; border-radius: 8px; background: #666; color: white;"
onTouchStart="menu.remove()">
Close
</button>
</div>
`;
menu.addEventListener('touchstart', (e) => e.stopPropagation());
document.addEventListener('touchstart', () => menu.remove());
document.getElementById('container').appendChild(menu);
}
showMobileFeedback(message, x, y) {
const feedback = document.createElement('div');
feedback.style.cssText = `
position: fixed;
top: ${y}px;
left: ${x}px;
transform: translate(-50%, -50%);
background: rgba(255, 107, 107, 0.9);
color: white;
padding: 10px 20px;
border-radius: 20px;
z-index: 1002;
font-weight: bold;
backdrop-filter: blur(10px);
white-space: nowrap;
`;
feedback.textContent = message;
document.getElementById('container').appendChild(feedback);
setTimeout(() => {
feedback.style.transition = 'all 0.5s ease-out';
feedback.style.opacity = '0';
feedback.style.transform = `translate(-50%, -60px)`;
setTimeout(() => feedback.remove(), 500);
}, 1000);
}
adaptExistingUIForMobile() {
// Make existing UI mobile-friendly
const chatUI = document.getElementById('chatUI');
if (chatUI) {
chatUI.style.maxHeight = '40vh';
chatUI.style.overflowY = 'auto';
}
// Adjust camera UI position
const cameraUI = document.querySelector('[style*="top: 20px"][style*="right: 20px"]');
if (cameraUI) {
cameraUI.style.top = '80px';
cameraUI.style.right = '10px';
cameraUI.style.transform = 'scale(0.9)';
}
}
optimizePerformanceForMobile() {
if (!this.isMobile) return;
// Reduce rendering quality
this.scene.renderer.setPixelRatio(1);
// Simplify shadows
const lights = this.scene.scene.children.filter(child => child instanceof THREE.Light);
lights.forEach(light => {
if (light.shadow) {
light.shadow.mapSize.width = 512;
light.shadow.mapSize.height = 512;
}
});
// Reduce particle counts
if (this.scene.weatherSystem) {
this.scene.weatherSystem.rain.children[0].geometry.setDrawRange(0, 300);
this.scene.weatherSystem.snow.children[0].geometry.setDrawRange(0, 200);
}
// Lower LOD distances
if (this.scene.performanceOptimizer) {
this.scene.performanceOptimizer.visibilityDistance = 50;
}
}
isTouchOnUI(x, y) {
// Check if touch is on any UI element
const elements = document.elementsFromPoint(x, y);
return elements.some(el =>
el.tagName === 'BUTTON' ||
el.id === 'chatUI' ||
el.classList.contains('mobile-ui')
);
}
handleTouchEnd(event) {
this.touchControls.currentTouches.clear();
this.touchControls.lastPinchDistance = null;
this.touchControls.lastTwoFingerMidpoint = null;
}
handleTouchMove(event) {
// Handled in specific gesture handlers
}
}
Step 2: Social Features - Building Communities! 👥
Let's add friend systems, groups, and social interactions:
// social-features.js - Because love is better with friends! 💕
class SocialSystem {
constructor(scene) {
this.scene = scene;
this.friends = new Map(); // userId -> friend data
this.groups = new Map(); // groupId -> group data
this.pendingRequests = new Map();
this.socialEvents = [];
this.setupSocialUI();
this.loadSocialData();
console.log("Social system initialized! Time to make some friends! 👥");
}
setupSocialUI() {
this.createFriendsPanel();
this.createGroupsPanel();
this.createSocialNotifications();
}
createFriendsPanel() {
this.friendsPanel = document.createElement('div');
this.friendsPanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 20px;
z-index: 1000;
width: 90%;
max-width: 400px;
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
display: none;
`;
this.friendsPanel.innerHTML = `
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: #ff6b6b;">👥 Friends</h3>
<button onclick="socialSystem.hideFriendsPanel()" style="
background: none;
border: none;
font-size: 20px;
cursor: pointer;
">✕</button>
</div>
<div id="friendsList" style="margin-bottom: 20px;"></div>
<div style="display: grid; gap: 10px;">
<input type="text" id="friendSearch" placeholder="Search friends..." style="
padding: 10px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 14px;
">
<button onclick="socialSystem.showAddFriendDialog()" style="
padding: 12px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">+ Add Friend</button>
</div>
`;
document.getElementById('container').appendChild(this.friendsPanel);
// Add to mobile menu
this.addSocialToMobileMenu();
}
addSocialToMobileMenu() {
// This would be integrated with the mobile menu system
}
showFriendsPanel() {
this.updateFriendsList();
this.friendsPanel.style.display = 'block';
}
hideFriendsPanel() {
this.friendsPanel.style.display = 'none';
}
updateFriendsList() {
const friendsList = document.getElementById('friendsList');
if (!friendsList) return;
if (this.friends.size === 0) {
friendsList.innerHTML = `
<div style="text-align: center; color: #666; padding: 20px;">
<p>No friends yet 😢</p>
<p style="font-size: 12px;">Add friends to see them here!</p>
</div>
`;
return;
}
friendsList.innerHTML = Array.from(this.friends.values()).map(friend => `
<div class="friend-item" style="
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: #f8f9fa;
border-radius: 10px;
margin-bottom: 8px;
">
<div style="display: flex; align-items: center; gap: 10px;">
<div style="
width: 40px;
height: 40px;
border-radius: 50%;
background: #ff6b6b;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
">${friend.name.charAt(0)}</div>
<div>
<div style="font-weight: bold;">${friend.name}</div>
<div style="font-size: 12px; color: #666;">
${friend.online ? '🟢 Online' : '⚫ Offline'}
${friend.inWorld ? ' · In World' : ''}
</div>
</div>
</div>
<div style="display: flex; gap: 5px;">
<button onclick="socialSystem.teleportToFriend('${friend.id}')" style="
background: #4ecdc4;
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
font-size: 12px;
">📍</button>
<button onclick="socialSystem.messageFriend('${friend.id}')" style="
background: #ffd166;
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
font-size: 12px;
">💬</button>
</div>
</div>
`).join('');
}
showAddFriendDialog() {
const dialog = document.createElement('div');
dialog.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.98);
padding: 25px;
border-radius: 20px;
z-index: 1001;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
min-width: 300px;
`;
dialog.innerHTML = `
<h3 style="margin: 0 0 15px 0; color: #ff6b6b;">Add Friend</h3>
<input type="text" id="friendUsername" placeholder="Enter username..." style="
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 10px;
margin-bottom: 15px;
font-size: 14px;
box-sizing: border-box;
">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<button onclick="socialSystem.sendFriendRequest()" style="
padding: 12px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Send Request</button>
<button onclick="this.parentElement.parentElement.remove()" style="
padding: 12px;
background: #666;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Cancel</button>
</div>
`;
document.getElementById('container').appendChild(dialog);
// Close on outside click
dialog.addEventListener('click', (e) => e.stopPropagation());
document.addEventListener('click', () => dialog.remove());
}
sendFriendRequest() {
const usernameInput = document.getElementById('friendUsername');
const username = usernameInput?.value.trim();
if (!username) {
this.showSocialNotification('Please enter a username', 'error');
return;
}
// Simulate sending friend request
this.showSocialNotification(`Friend request sent to ${username}!`, 'success');
// In real implementation, this would send to server
setTimeout(() => {
this.simulateFriendRequestResponse(username);
}, 2000);
// Close dialog
usernameInput.parentElement.parentElement.remove();
}
simulateFriendRequestResponse(username) {
// Simulate friend accepting request (50% chance)
if (Math.random() > 0.5) {
this.addFriend({
id: 'friend_' + Date.now(),
name: username,
online: true,
inWorld: Math.random() > 0.5,
friendshipLevel: 1,
lastSeen: Date.now()
});
this.showSocialNotification(`${username} accepted your friend request! 🎉`, 'success');
} else {
this.showSocialNotification(`${username} declined your friend request`, 'error');
}
}
addFriend(friendData) {
this.friends.set(friendData.id, friendData);
this.updateFriendsList();
this.saveSocialData();
}
removeFriend(friendId) {
this.friends.delete(friendId);
this.updateFriendsList();
this.saveSocialData();
}
teleportToFriend(friendId) {
const friend = this.friends.get(friendId);
if (!friend || !friend.inWorld) {
this.showSocialNotification('Friend is not in the world', 'error');
return;
}
// In real implementation, this would teleport to friend's location
this.showSocialNotification(`Teleporting to ${friend.name}...`, 'info');
// Simulate teleportation
setTimeout(() => {
if (this.scene.currentUser) {
// Move to random position (in real app, friend's actual position)
const randomX = (Math.random() - 0.5) * 20;
const randomZ = (Math.random() - 0.5) * 20;
this.scene.currentUser.mesh.position.set(randomX, 0, randomZ);
this.showSocialNotification(`Teleported to ${friend.name}!`, 'success');
}
}, 2000);
}
messageFriend(friendId) {
const friend = this.friends.get(friendId);
if (!friend) return;
// Open chat with friend
const chatUI = document.getElementById('chatUI');
const messageInput = document.getElementById('messageInput');
if (chatUI && messageInput) {
chatUI.style.display = 'block';
messageInput.placeholder = `Message ${friend.name}...`;
messageInput.focus();
// Add special header for friend chat
let header = chatUI.querySelector('.friend-chat-header');
if (!header) {
header = document.createElement('div');
header.className = 'friend-chat-header';
header.style.cssText = `
background: #4ecdc4;
color: white;
padding: 10px;
border-radius: 10px 10px 0 0;
text-align: center;
font-weight: bold;
`;
chatUI.insertBefore(header, chatUI.firstChild);
}
header.textContent = `💬 Chatting with ${friend.name}`;
}
}
createGroupsPanel() {
this.groupsPanel = document.createElement('div');
this.groupsPanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 20px;
z-index: 1000;
width: 90%;
max-width: 400px;
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
display: none;
`;
this.groupsPanel.innerHTML = `
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: #ff6b6b;">👥 Groups</h3>
<button onclick="socialSystem.hideGroupsPanel()" style="
background: none;
border: none;
font-size: 20px;
cursor: pointer;
">✕</button>
</div>
<div id="groupsList" style="margin-bottom: 20px;"></div>
<div style="display: grid; gap: 10px;">
<button onclick="socialSystem.showCreateGroupDialog()" style="
padding: 12px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">+ Create Group</button>
<button onclick="socialSystem.showJoinGroupDialog()" style="
padding: 12px;
background: #9370db;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">🔍 Find Groups</button>
</div>
`;
document.getElementById('container').appendChild(this.groupsPanel);
}
showGroupsPanel() {
this.updateGroupsList();
this.groupsPanel.style.display = 'block';
}
hideGroupsPanel() {
this.groupsPanel.style.display = 'none';
}
updateGroupsList() {
const groupsList = document.getElementById('groupsList');
if (!groupsList) return;
if (this.groups.size === 0) {
groupsList.innerHTML = `
<div style="text-align: center; color: #666; padding: 20px;">
<p>No groups yet 👥</p>
<p style="font-size: 12px;">Create or join a group to get started!</p>
</div>
`;
return;
}
groupsList.innerHTML = Array.from(this.groups.values()).map(group => `
<div class="group-item" style="
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
margin-bottom: 10px;
">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
<div>
<div style="font-weight: bold; font-size: 16px;">${group.name}</div>
<div style="font-size: 12px; color: #666;">${group.memberCount} members</div>
</div>
<div style="display: flex; gap: 5px;">
<button onclick="socialSystem.teleportToGroup('${group.id}')" style="
background: #4ecdc4;
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
font-size: 12px;
">📍</button>
<button onclick="socialSystem.leaveGroup('${group.id}')" style="
background: #ff6b6b;
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
font-size: 12px;
">Leave</button>
</div>
</div>
<div style="font-size: 12px; color: #666;">${group.description}</div>
<div style="display: flex; gap: 5px; margin-top: 10px; flex-wrap: wrap;">
${group.tags.map(tag => `
<span style="
background: #e9ecef;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
">${tag}</span>
`).join('')}
</div>
</div>
`).join('');
}
showCreateGroupDialog() {
const dialog = document.createElement('div');
dialog.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.98);
padding: 25px;
border-radius: 20px;
z-index: 1001;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
min-width: 300px;
`;
dialog.innerHTML = `
<h3 style="margin: 0 0 15px 0; color: #ff6b6b;">Create Group</h3>
<input type="text" id="groupName" placeholder="Group name" style="
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 10px;
font-size: 14px;
box-sizing: border-box;
">
<textarea id="groupDescription" placeholder="Group description" style="
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 10px;
font-size: 14px;
box-sizing: border-box;
resize: vertical;
min-height: 60px;
"></textarea>
<input type="text" id="groupTags" placeholder="Tags (comma separated)" style="
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 15px;
font-size: 14px;
box-sizing: border-box;
">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<button onclick="socialSystem.createGroup()" style="
padding: 12px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Create</button>
<button onclick="this.parentElement.parentElement.remove()" style="
padding: 12px;
background: #666;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Cancel</button>
</div>
`;
document.getElementById('container').appendChild(dialog);
dialog.addEventListener('click', (e) => e.stopPropagation());
document.addEventListener('click', () => dialog.remove());
}
createGroup() {
const name = document.getElementById('groupName')?.value.trim();
const description = document.getElementById('groupDescription')?.value.trim();
const tags = document.getElementById('groupTags')?.value.split(',').map(tag => tag.trim()).filter(tag => tag);
if (!name) {
this.showSocialNotification('Please enter a group name', 'error');
return;
}
const group = {
id: 'group_' + Date.now(),
name: name,
description: description || 'A friendly group for hanging out!',
tags: tags || ['social', 'friendly'],
memberCount: 1,
created: Date.now(),
members: [this.getCurrentUserData()]
};
this.groups.set(group.id, group);
this.updateGroupsList();
this.saveSocialData();
this.showSocialNotification(`Group "${name}" created! 🎉`, 'success');
// Close dialog
document.querySelectorAll('div').forEach(div => {
if (div.style.backdropFilter === 'blur(20px)') {
div.remove();
}
});
}
getCurrentUserData() {
// In real implementation, this would get actual user data
return {
id: 'current_user',
name: this.scene.currentUser?.options.name || 'You',
avatar: this.scene.currentUser
};
}
teleportToGroup(groupId) {
const group = this.groups.get(groupId);
if (!group) return;
this.showSocialNotification(`Teleporting to ${group.name}...`, 'info');
// In real implementation, this would teleport to group gathering spot
setTimeout(() => {
if (this.scene.currentUser) {
// Move to group area (simulated)
const groupX = (Math.random() - 0.5) * 10;
const groupZ = (Math.random() - 0.5) * 10;
this.scene.currentUser.mesh.position.set(groupX, 0, groupZ);
this.showSocialNotification(`Joined ${group.name}! 👥`, 'success');
}
}, 2000);
}
leaveGroup(groupId) {
const group = this.groups.get(groupId);
if (!group) return;
if (confirm(`Are you sure you want to leave "${group.name}"?`)) {
this.groups.delete(groupId);
this.updateGroupsList();
this.saveSocialData();
this.showSocialNotification(`Left ${group.name}`, 'info');
}
}
createSocialNotifications() {
this.notificationContainer = document.createElement('div');
this.notificationContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
max-width: 300px;
`;
document.getElementById('container').appendChild(this.notificationContainer);
}
showSocialNotification(message, type = 'info') {
const notification = document.createElement('div');
const colors = {
info: '#4ecdc4',
success: '#4ecdc4',
error: '#ff6b6b',
warning: '#ffd166'
};
notification.style.cssText = `
background: ${colors[type] || colors.info};
color: white;
padding: 15px 20px;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
animation: slideInRight 0.3s ease-out;
word-wrap: break-word;
`;
notification.textContent = message;
this.notificationContainer.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
notification.style.animation = 'slideOutRight 0.3s ease-in';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 5000);
// Add CSS animations
if (!document.getElementById('notificationStyles')) {
const style = document.createElement('style');
style.id = 'notificationStyles';
style.textContent = `
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
}
}
loadSocialData() {
// Load friends and groups from localStorage
try {
const savedFriends = localStorage.getItem('datingApp_friends');
const savedGroups = localStorage.getItem('datingApp_groups');
if (savedFriends) {
const friendsData = JSON.parse(savedFriends);
friendsData.forEach(friend => this.friends.set(friend.id, friend));
}
if (savedGroups) {
const groupsData = JSON.parse(savedGroups);
groupsData.forEach(group => this.groups.set(group.id, group));
}
} catch (error) {
console.warn('Error loading social data:', error);
}
}
saveSocialData() {
// Save friends and groups to localStorage
try {
const friendsData = Array.from(this.friends.values());
const groupsData = Array.from(this.groups.values());
localStorage.setItem('datingApp_friends', JSON.stringify(friendsData));
localStorage.setItem('datingApp_groups', JSON.stringify(groupsData));
} catch (error) {
console.warn('Error saving social data:', error);
}
}
// Social events and activities
createSocialEvent(eventData) {
this.socialEvents.push({
...eventData,
id: 'event_' + Date.now(),
participants: new Set(),
createdAt: Date.now()
});
this.announceSocialEvent(eventData);
}
announceSocialEvent(event) {
this.showSocialNotification(`🎉 New Event: ${event.name} - ${event.description}`, 'info');
// Add to event board (if exists)
this.updateEventBoard();
}
updateEventBoard() {
// Update any visible event boards in the world
// This would update 3D objects showing current events
}
joinSocialEvent(eventId) {
const event = this.socialEvents.find(e => e.id === eventId);
if (event) {
event.participants.add(this.getCurrentUserData());
this.showSocialNotification(`Joined event: ${event.name}`, 'success');
}
}
// Integration with existing systems
setupSocialIntegrations() {
// Integrate with relationship system
this.setupRelationshipIntegration();
// Integrate with mini-games
this.setupGameIntegration();
}
setupRelationshipIntegration() {
// When friendship level reaches certain points, suggest adding as friend
// This would connect with the relationship system from Part 6
}
setupGameIntegration() {
// Create group games and tournaments
// This would extend the mini-game system from Part 6
}
}
Step 3: Enhanced Customization - Express Yourself! 🎨
Let's create extensive customization options for avatars and the environment:
javascript
// customization.js - Because everyone deserves to be unique! 🌈
class CustomizationSystem {
constructor(scene) {
this.scene = scene;
this.avatarCustomizations = new Map();
this.environmentThemes = new Map();
this.currentTheme = 'default';
this.loadCustomizationData();
this.setupCustomizationUI();
console.log("Customization system loaded! Time to express yourself! 🎨");
}
setupCustomizationUI() {
this.createCustomizationPanel();
this.createThemeSelector();
this.createQuickCustomizeMenu();
}
createCustomizationPanel() {
this.customizationPanel = document.createElement('div');
this.customizationPanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 20px;
z-index: 1000;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
display: none;
`;
this.customizationPanel.innerHTML = `
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: #ff6b6b;">🎨 Customize Avatar</h3>
<button onclick="customizationSystem.hideCustomizationPanel()" style="
background: none;
border: none;
font-size: 20px;
cursor: pointer;
">✕</button>
</div>
<div id="customizationTabs" style="
display: flex;
gap: 5px;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
">
<button class="tab-button active" data-tab="body" style="
flex: 1;
padding: 10px;
border: none;
background: #f8f9fa;
border-radius: 8px;
cursor: pointer;
">Body</button>
<button class="tab-button" data-tab="face" style="
flex: 1;
padding: 10px;
border: none;
background: #f8f9fa;
border-radius: 8px;
cursor: pointer;
">Face</button>
<button class="tab-button" data-tab="clothing" style="
flex: 1;
padding: 10px;
border: none;
background: #f8f9fa;
border-radius: 8px;
cursor: pointer;
">Clothing</button>
<button class="tab-button" data-tab="accessories" style="
flex: 1;
padding: 10px;
border: none;
background: #f8f9fa;
border-radius: 8px;
cursor: pointer;
">Accessories</button>
</div>
<div id="customizationContent"></div>
<div style="margin-top: 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<button onclick="customizationSystem.applyCustomizations()" style="
padding: 12px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Apply</button>
<button onclick="customizationSystem.randomizeAvatar()" style="
padding: 12px;
background: #ffd166;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Randomize 🎲</button>
</div>
`;
document.getElementById('container').appendChild(this.customizationPanel);
// Setup tab switching
this.setupCustomizationTabs();
}
setupCustomizationTabs() {
const tabButtons = this.customizationPanel.querySelectorAll('.tab-button');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
// Remove active class from all buttons
tabButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
button.classList.add('active');
// Show corresponding content
this.showCustomizationTab(button.dataset.tab);
});
});
// Show initial tab
this.showCustomizationTab('body');
}
showCustomizationTab(tabName) {
const content = document.getElementById('customizationContent');
switch(tabName) {
case 'body':
content.innerHTML = this.getBodyCustomizationHTML();
break;
case 'face':
content.innerHTML = this.getFaceCustomizationHTML();
break;
case 'clothing':
content.innerHTML = this.getClothingCustomizationHTML();
break;
case 'accessories':
content.innerHTML = this.getAccessoriesCustomizationHTML();
break;
}
// Initialize color pickers and sliders
this.initializeCustomizationControls();
}
getBodyCustomizationHTML() {
return `
<div style="display: grid; gap: 15px;">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Skin Tone</label>
<input type="color" id="skinTone" value="#f0d9b5" style="width: 100%; height: 40px;">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Height</label>
<input type="range" id="avatarHeight" min="0.8" max="1.2" step="0.05" value="1" style="width: 100%;">
<div style="display: flex; justify-content: between; font-size: 12px; color: #666;">
<span>Short</span>
<span>Average</span>
<span>Tall</span>
</div>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Body Type</label>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 5px;">
<button data-bodytype="slim" style="padding: 10px; border: 1px solid #ddd; border-radius: 8px; background: white; cursor: pointer;">
Slim
</button>
<button data-bodytype="average" style="padding: 10px; border: 1px solid #ddd; border-radius: 8px; background: white; cursor: pointer;">
Average
</button>
<button data-bodytype="athletic" style="padding: 10px; border: 1px solid #ddd; border-radius: 8px; background: white; cursor: pointer;">
Athletic
</button>
</div>
</div>
</div>
`;
}
getFaceCustomizationHTML() {
return `
<div style="display: grid; gap: 15px;">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Eye Color</label>
<input type="color" id="eyeColor" value="#000000" style="width: 100%; height: 40px;">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Hair Style</label>
<select id="hairStyle" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px;">
<option value="short">Short</option>
<option value="medium">Medium</option>
<option value="long">Long</option>
<option value="curly">Curly</option>
<option value="ponytail">Ponytail</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Hair Color</label>
<input type="color" id="hairColor" value="#8b4513" style="width: 100%; height: 40px;">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Facial Features</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div>
<label style="font-size: 12px;">Nose Size</label>
<input type="range" id="noseSize" min="0.5" max="1.5" step="0.1" value="1" style="width: 100%;">
</div>
<div>
<label style="font-size: 12px;">Eye Size</label>
<input type="range" id="eyeSize" min="0.5" max="1.5" step="0.1" value="1" style="width: 100%;">
</div>
</div>
</div>
</div>
`;
}
getClothingCustomizationHTML() {
return `
<div style="display: grid; gap: 15px;">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Top Style</label>
<select id="topStyle" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px;">
<option value="t-shirt">T-Shirt</option>
<option value="shirt">Shirt</option>
<option value="sweater">Sweater</option>
<option value="dress">Dress</option>
<option value="tank">Tank Top</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Top Color</label>
<input type="color" id="topColor" value="#4169e1" style="width: 100%; height: 40px;">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Bottom Style</label>
<select id="bottomStyle" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px;">
<option value="jeans">Jeans</option>
<option value="pants">Pants</option>
<option value="shorts">Shorts</option>
<option value="skirt">Skirt</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Bottom Color</label>
<input type="color" id="bottomColor" value="#2f4f4f" style="width: 100%; height: 40px;">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Shoes</label>
<select id="shoeStyle" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px;">
<option value="sneakers">Sneakers</option>
<option value="boots">Boots</option>
<option value="sandals">Sandals</option>
<option value="heels">Heels</option>
</select>
</div>
</div>
`;
}
getAccessoriesCustomizationHTML() {
return `
<div style="display: grid; gap: 15px;">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Glasses</label>
<select id="glassesStyle" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px;">
<option value="none">None</option>
<option value="round">Round</option>
<option value="square">Square</option>
<option value="sunglasses">Sunglasses</option>
<option value="aviator">Aviator</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Hat</label>
<select id="hatStyle" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px;">
<option value="none">None</option>
<option value="baseball">Baseball Cap</option>
<option value="beanie">Beanie</option>
<option value="fedora">Fedora</option>
<option value="crown">Crown 👑</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Jewelry</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="hasNecklace">
<span>Necklace</span>
</label>
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="hasEarrings">
<span>Earrings</span>
</label>
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="hasBracelet">
<span>Bracelet</span>
</label>
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="hasRing">
<span>Ring</span>
</label>
</div>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Special Effects</label>
<select id="specialEffects" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px;">
<option value="none">None</option>
<option value="sparkles">Sparkles ✨</option>
<option value="glow">Glow 💫</option>
<option value="hearts">Hearts 💕</option>
<option value="rainbow">Rainbow 🌈</option>
</select>
</div>
</div>
`;
}
initializeCustomizationControls() {
// Initialize event listeners for customization controls
const colorPickers = this.customizationPanel.querySelectorAll('input[type="color"]');
colorPickers.forEach(picker => {
picker.addEventListener('input', (e) => {
this.previewCustomization(e.target.id, e.target.value);
});
});
const sliders = this.customizationPanel.querySelectorAll('input[type="range"]');
sliders.forEach(slider => {
slider.addEventListener('input', (e) => {
this.previewCustomization(e.target.id, e.target.value);
});
});
const selects = this.customizationPanel.querySelectorAll('select');
selects.forEach(select => {
select.addEventListener('change', (e) => {
this.previewCustomization(e.target.id, e.target.value);
});
});
const buttons = this.customizationPanel.querySelectorAll('button[data-bodytype]');
buttons.forEach(button => {
button.addEventListener('click', (e) => {
buttons.forEach(btn => btn.style.background = 'white');
e.target.style.background = '#4ecdc4';
this.previewCustomization('bodyType', e.target.dataset.bodytype);
});
});
const checkboxes = this.customizationPanel.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
this.previewCustomization(e.target.id, e.target.checked);
});
});
}
previewCustomization(controlId, value) {
if (!this.scene.currentUser) return;
// Apply preview to current avatar
const avatar = this.scene.currentUser;
switch(controlId) {
case 'skinTone':
this.previewSkinTone(avatar, value);
break;
case 'avatarHeight':
this.previewHeight(avatar, value);
break;
case 'bodyType':
this.previewBodyType(avatar, value);
break;
case 'eyeColor':
this.previewEyeColor(avatar, value);
break;
case 'hairColor':
this.previewHairColor(avatar, value);
break;
case 'topColor':
this.previewClothingColor(avatar, 'top', value);
break;
case 'bottomColor':
this.previewClothingColor(avatar, 'bottom', value);
break;
// Add more preview cases as needed
}
}
previewSkinTone(avatar, color) {
if (avatar.head && avatar.head.material) {
avatar.head.material.color.setStyle(color);
}
if (avatar.neck && avatar.neck.material) {
avatar.neck.material.color.setStyle(color);
}
if (avatar.leftArm && avatar.leftArm.material) {
avatar.leftArm.material.color.setStyle(color);
}
if (avatar.rightArm && avatar.rightArm.material) {
avatar.rightArm.material.color.setStyle(color);
}
if (avatar.leftHand && avatar.leftHand.material) {
avatar.leftHand.material.color.setStyle(color);
}
if (avatar.rightHand && avatar.rightHand.material) {
avatar.rightHand.material.color.setStyle(color);
}
}
previewHeight(avatar, scale) {
avatar.mesh.scale.y = parseFloat(scale);
}
previewBodyType(avatar, bodyType) {
// Adjust body proportions based on body type
const scales = {
slim: { x: 0.9, y: 1, z: 0.9 },
average: { x: 1, y: 1, z: 1 },
athletic: { x: 1.1, y: 1.05, z: 1.1 }
};
const scale = scales[bodyType] || scales.average;
if (avatar.torso) {
avatar.torso.scale.set(scale.x, scale.y, scale.z);
}
}
previewEyeColor(avatar, color) {
if (avatar.leftEye && avatar.leftEye.material) {
avatar.leftEye.material.color.setStyle(color);
}
if (avatar.rightEye && avatar.rightEye.material) {
avatar.rightEye.material.color.setStyle(color);
}
}
previewHairColor(avatar, color) {
if (avatar.hair && avatar.hair.material) {
avatar.hair.material.color.setStyle(color);
}
}
previewClothingColor(avatar, clothingType, color) {
if (clothingType === 'top' && avatar.torso && avatar.torso.material) {
avatar.torso.material.color.setStyle(color);
}
// Add other clothing types as needed
}
showCustomizationPanel() {
this.loadCurrentAvatarSettings();
this.customizationPanel.style.display = 'block';
}
hideCustomizationPanel() {
this.customizationPanel.style.display = 'none';
}
loadCurrentAvatarSettings() {
if (!this.scene.currentUser) return;
const avatar = this.scene.currentUser;
// Load current settings into form
// This would read from the avatar's current appearance
// For now, we'll set some defaults
const skinToneInput = document.getElementById('skinTone');
if (skinToneInput && avatar.head && avatar.head.material) {
skinToneInput.value = this.rgbToHex(avatar.head.material.color);
}
const heightInput = document.getElementById('avatarHeight');
if (heightInput) {
heightInput.value = avatar.mesh.scale.y;
}
// Load other current settings...
}
rgbToHex(color) {
return '#' + color.getHexString();
}
applyCustomizations() {
if (!this.scene.currentUser) return;
// Gather all customization values
const customizations = {
skinTone: document.getElementById('skinTone')?.value,
height: document.getElementById('avatarHeight')?.value,
bodyType: document.querySelector('button[data-bodytype][style*="background: #4ecdc4"]')?.dataset.bodytype,
eyeColor: document.getElementById('eyeColor')?.value,
hairStyle: document.getElementById('hairStyle')?.value,
hairColor: document.getElementById('hairColor')?.value,
topStyle: document.getElementById('topStyle')?.value,
topColor: document.getElementById('topColor')?.value,
bottomStyle: document.getElementById('bottomStyle')?.value,
bottomColor: document.getElementById('bottomColor')?.value,
shoeStyle: document.getElementById('shoeStyle')?.value,
glassesStyle: document.getElementById('glassesStyle')?.value,
hatStyle: document.getElementById('hatStyle')?.value,
hasNecklace: document.getElementById('hasNecklace')?.checked,
hasEarrings: document.getElementById('hasEarrings')?.checked,
hasBracelet: document.getElementById('hasBracelet')?.checked,
hasRing: document.getElementById('hasRing')?.checked,
specialEffects: document.getElementById('specialEffects')?.value
};
// Apply customizations
this.applyAvatarCustomizations(this.scene.currentUser, customizations);
// Save customizations
this.avatarCustomizations.set(this.scene.currentUser, customizations);
this.saveCustomizationData();
this.showCustomizationNotification('Avatar customized! 🎨');
this.hideCustomizationPanel();
}
applyAvatarCustomizations(avatar, customizations) {
// Apply all customizations to the avatar
if (customizations.skinTone) {
this.previewSkinTone(avatar, customizations.skinTone);
}
if (customizations.height) {
this.previewHeight(avatar, customizations.height);
}
if (customizations.bodyType) {
this.previewBodyType(avatar, customizations.bodyType);
}
if (customizations.eyeColor) {
this.previewEyeColor(avatar, customizations.eyeColor);
}
if (customizations.hairColor) {
this.previewHairColor(avatar, customizations.hairColor);
}
if (customizations.topColor) {
this.previewClothingColor(avatar, 'top', customizations.topColor);
}
// Apply other customizations...
// Apply special effects
this.applySpecialEffects(avatar, customizations.specialEffects);
}
applySpecialEffects(avatar, effect) {
// Remove existing effects
if (avatar.specialEffect) {
avatar.mesh.remove(avatar.specialEffect);
}
if (effect === 'none') return;
let effectObject;
switch(effect) {
case 'sparkles':
effectObject = this.createSparkleEffect();
break;
case 'glow':
effectObject = this.createGlowEffect();
break;
case 'hearts':
effectObject = this.createHeartEffect();
break;
case 'rainbow':
effectObject = this.createRainbowEffect();
break;
}
if (effectObject) {
avatar.specialEffect = effectObject;
avatar.mesh.add(effectObject);
}
}
createSparkleEffect() {
const group = new THREE.Group();
for (let i = 0; i < 8; i++) {
const sparkle = new THREE.Mesh(
new THREE.SphereGeometry(0.05, 8, 8),
new THREE.MeshBasicMaterial({ color: 0xffffff })
);
const angle = (i / 8) * Math.PI * 2;
const radius = 0.8;
sparkle.position.set(
Math.cos(angle) * radius,
1.5 + Math.sin(Date.now() * 0.001 + i) * 0.2,
Math.sin(angle) * radius
);
group.add(sparkle);
}
return group;
}
randomizeAvatar() {
if (!this.scene.currentUser) return;
const randomCustomizations = {
skinTone: '#' + Math.floor(Math.random()*16777215).toString(16),
height: (0.8 + Math.random() * 0.4).toFixed(2),
bodyType: ['slim', 'average', 'athletic'][Math.floor(Math.random() * 3)],
eyeColor: '#' + Math.floor(Math.random()*16777215).toString(16),
hairColor: '#' + Math.floor(Math.random()*16777215).toString(16),
topColor: '#' + Math.floor(Math.random()*16777215).toString(16),
bottomColor: '#' + Math.floor(Math.random()*16777215).toString(16),
specialEffects: ['none', 'sparkles', 'glow', 'hearts', 'rainbow'][Math.floor(Math.random() * 5)]
};
this.applyAvatarCustomizations(this.scene.currentUser, randomCustomizations);
this.showCustomizationNotification('Random avatar created! 🎲');
}
createThemeSelector() {
this.themeSelector = document.createElement('div');
this.themeSelector.style.cssText = `
position: fixed;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
z-index: 100;
backdrop-filter: blur(10px);
display: none;
`;
I'd love to continue, but it looks like my previous response got cut off at the end! Let me complete Part 7 and then we'll move on to the remaining parts of our 3D dating app tutorial.
Step 3: Enhanced Customization (Continued) 🎨
createThemeSelector() {
this.themeSelector = document.createElement('div');
this.themeSelector.style.cssText = `
position: fixed;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
z-index: 100;
backdrop-filter: blur(10px);
display: none;
`;
this.themeSelector.innerHTML = `
<h4 style="margin: 0 0 10px 0; color: #ff6b6b;">🎨 Environment Theme</h4>
<select id="themeSelect" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 8px;">
<option value="default">Default</option>
<option value="romantic">Romantic 💕</option>
<option value="nightclub">Nightclub 🎵</option>
<option value="garden">Secret Garden 🌸</option>
<option value="beach">Beach Sunset 🌅</option>
<option value="winter">Winter Wonderland ❄️</option>
<option value="fantasy">Fantasy Forest 🧚</option>
</select>
<button onclick="customizationSystem.applyTheme()" style="
width: 100%;
padding: 8px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
margin-top: 10px;
">Apply Theme</button>
`;
document.getElementById('container').appendChild(this.themeSelector);
// Load saved theme
const savedTheme = localStorage.getItem('datingApp_theme');
if (savedTheme) {
document.getElementById('themeSelect').value = savedTheme;
this.applyTheme();
}
}
applyTheme() {
const themeSelect = document.getElementById('themeSelect');
if (!themeSelect) return;
const theme = themeSelect.value;
this.currentTheme = theme;
this.applyEnvironmentTheme(theme);
this.saveCustomizationData();
this.showCustomizationNotification(`Applied ${theme} theme! 🎨`);
}
applyEnvironmentTheme(theme) {
// Clear existing theme elements
this.clearThemeElements();
switch(theme) {
case 'romantic':
this.applyRomanticTheme();
break;
case 'nightclub':
this.applyNightclubTheme();
break;
case 'garden':
this.applyGardenTheme();
break;
case 'beach':
this.applyBeachTheme();
break;
case 'winter':
this.applyWinterTheme();
break;
case 'fantasy':
this.applyFantasyTheme();
break;
default:
this.applyDefaultTheme();
}
localStorage.setItem('datingApp_theme', theme);
}
applyRomanticTheme() {
// Romantic lighting
const romanticLight = new THREE.PointLight(0xff69b4, 0.8, 50);
romanticLight.position.set(0, 10, 0);
this.scene.scene.add(romanticLight);
this.themeElements.push(romanticLight);
// Floating hearts
for (let i = 0; i < 20; i++) {
const heart = this.createFloatingHeart();
heart.position.set(
(Math.random() - 0.5) * 40,
Math.random() * 10 + 2,
(Math.random() - 0.5) * 40
);
this.scene.scene.add(heart);
this.themeElements.push(heart);
}
// Soft pink ambient light
const ambientLight = this.scene.scene.children.find(child =>
child instanceof THREE.AmbientLight
);
if (ambientLight) {
ambientLight.color.setHex(0xffe6f2);
}
}
createFloatingHeart() {
const heartShape = new THREE.Shape();
heartShape.moveTo(0, 0);
heartShape.bezierCurveTo(0.2, 0.2, 0.3, 0, 0, -0.3);
heartShape.bezierCurveTo(-0.3, 0, -0.2, 0.2, 0, 0);
const geometry = new THREE.ExtrudeGeometry(heartShape, {
depth: 0.1,
bevelEnabled: true,
bevelSegments: 2,
bevelSize: 0.02,
bevelThickness: 0.02
});
const material = new THREE.MeshBasicMaterial({
color: 0xff69b4,
transparent: true,
opacity: 0.7
});
const heart = new THREE.Mesh(geometry, material);
// Floating animation
heart.userData = {
floatSpeed: 0.5 + Math.random() * 0.5,
rotationSpeed: (Math.random() - 0.5) * 0.02,
startY: heart.position.y
};
this.animateFloatingHeart(heart);
return heart;
}
animateFloatingHeart(heart) {
const animate = () => {
if (heart.parent) {
const time = Date.now() * 0.001;
heart.position.y = heart.userData.startY + Math.sin(time * heart.userData.floatSpeed) * 0.5;
heart.rotation.y += heart.userData.rotationSpeed;
requestAnimationFrame(animate);
}
};
animate();
}
applyNightclubTheme() {
// Disco ball
const discoBall = new THREE.Mesh(
new THREE.SphereGeometry(2, 32, 32),
new THREE.MeshStandardMaterial({
color: 0xffffff,
metalness: 1,
roughness: 0.1
})
);
discoBall.position.set(0, 15, 0);
this.scene.scene.add(discoBall);
this.themeElements.push(discoBall);
// Dance floor lights
this.createDanceFloorLights();
// Strobe effect
this.startStrobeEffect();
}
createDanceFloorLights() {
const colors = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff];
colors.forEach((color, index) => {
const angle = (index / colors.length) * Math.PI * 2;
const light = new THREE.SpotLight(color, 1, 20, Math.PI / 4, 0.5);
light.position.set(
Math.cos(angle) * 10,
8,
Math.sin(angle) * 10
);
light.target.position.set(0, 0, 0);
this.scene.scene.add(light);
this.scene.scene.add(light.target);
this.themeElements.push(light);
this.themeElements.push(light.target);
// Animate light
this.animateSpotlight(light, index);
});
}
animateSpotlight(light, index) {
const animate = () => {
if (light.parent) {
const time = Date.now() * 0.001 + index;
light.intensity = 0.5 + Math.sin(time * 3) * 0.5;
light.color.setHSL((time * 0.1 + index * 0.1) % 1, 1, 0.5);
requestAnimationFrame(animate);
}
};
animate();
}
startStrobeEffect() {
const strobe = () => {
if (this.currentTheme === 'nightclub') {
const intensity = Math.random() > 0.5 ? 1 : 0.1;
const ambientLight = this.scene.scene.children.find(child =>
child instanceof THREE.AmbientLight
);
if (ambientLight) {
ambientLight.intensity = intensity;
}
setTimeout(strobe, 100 + Math.random() * 200);
}
};
strobe();
}
clearThemeElements() {
this.themeElements.forEach(element => {
if (element.parent) {
element.parent.remove(element);
}
});
this.themeElements = [];
// Reset ambient light
const ambientLight = this.scene.scene.children.find(child =>
child instanceof THREE.AmbientLight
);
if (ambientLight) {
ambientLight.color.setHex(0xffffff);
ambientLight.intensity = 0.6;
}
}
createQuickCustomizeMenu() {
this.quickCustomizeMenu = document.createElement('div');
this.quickCustomizeMenu.style.cssText = `
position: fixed;
bottom: 200px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
z-index: 100;
backdrop-filter: blur(10px);
display: none;
`;
this.quickCustomizeMenu.innerHTML = `
<h4 style="margin: 0 0 10px 0; color: #ff6b6b;">🎨 Quick Customize</h4>
<div style="display: grid; gap: 8px;">
<button onclick="customizationSystem.showCustomizationPanel()" style="
padding: 8px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">Full Customize</button>
<button onclick="customizationSystem.randomizeAvatar()" style="
padding: 8px;
background: #ffd166;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">Randomize 🎲</button>
<button onclick="customizationSystem.toggleThemeSelector()" style="
padding: 8px;
background: #9370db;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">Themes</button>
<button onclick="customizationSystem.hideQuickCustomize()" style="
padding: 8px;
background: #666;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">Close</button>
</div>
`;
document.getElementById('container').appendChild(this.quickCustomizeMenu);
}
showQuickCustomize() {
this.quickCustomizeMenu.style.display = 'block';
}
hideQuickCustomize() {
this.quickCustomizeMenu.style.display = 'none';
}
toggleThemeSelector() {
if (this.themeSelector.style.display === 'block') {
this.themeSelector.style.display = 'none';
} else {
this.themeSelector.style.display = 'block';
}
}
showCustomizationNotification(message) {
// Use the social system's notification if available, or create a simple one
if (this.scene.socialSystem) {
this.scene.socialSystem.showSocialNotification(message, 'success');
} else {
alert(message);
}
}
loadCustomizationData() {
try {
const savedCustomizations = localStorage.getItem('datingApp_customizations');
if (savedCustomizations) {
this.avatarCustomizations = new Map(JSON.parse(savedCustomizations));
}
const savedTheme = localStorage.getItem('datingApp_theme');
if (savedTheme) {
this.currentTheme = savedTheme;
}
} catch (error) {
console.warn('Error loading customization data:', error);
}
}
saveCustomizationData() {
try {
const customizationsArray = Array.from(this.avatarCustomizations.entries());
localStorage.setItem('datingApp_customizations', JSON.stringify(customizationsArray));
localStorage.setItem('datingApp_theme', this.currentTheme);
} catch (error) {
console.warn('Error saving customization data:', error);
}
}
// Initialize theme elements array
themeElements = [];
}
Step 4: Updated DatingScene Class - Mobile & Social Integration 📱
// Updated DatingScene class for Part 7
class DatingScene {
constructor() {
// ... existing properties ...
// New properties for Part 7
this.mobileOptimizer = null;
this.socialSystem = null;
this.customizationSystem = null;
this.init();
}
init() {
this.createScene();
this.createCamera();
this.createRenderer();
this.createLights();
this.createEnvironment();
// Initialize all systems
this.proximityChat = new ProximityChat(this);
this.voiceChat = new VoiceChatSystem(this);
this.emoteSystem = new EmoteSystem(this);
this.interactiveEnv = new InteractiveEnvironment(this);
this.dayNightCycle = new DayNightCycle(this);
this.weatherSystem = new WeatherSystem(this, this.dayNightCycle);
this.miniGameSystem = new MiniGameSystem(this);
this.relationshipSystem = new RelationshipSystem(this);
this.performanceOptimizer = new PerformanceOptimizer(this);
// Initialize new Part 7 systems
this.mobileOptimizer = new MobileOptimizer(this);
this.socialSystem = new SocialSystem(this);
this.customizationSystem = new CustomizationSystem(this);
this.setupEnhancedUI();
this.setupKeyboardListeners();
this.animate();
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';
document.getElementById('chatUI').style.display = 'block';
this.addSampleAvatars();
}, 2000);
}
setupEnhancedUI() {
this.createEnhancedMenu();
this.setupMobileFriendlyUI();
}
createEnhancedMenu() {
// Enhanced main menu with all new features
const enhancedMenu = document.createElement('div');
enhancedMenu.style.cssText = `
position: fixed;
top: 20px;
left: 20px;
z-index: 100;
display: flex;
flex-direction: column;
gap: 10px;
`;
enhancedMenu.innerHTML = `
<button onclick="datingScene.toggleSocialFeatures()" style="
padding: 10px 15px;
background: rgba(255, 107, 107, 0.9);
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
gap: 8px;
">👥 Social</button>
<button onclick="datingScene.toggleCustomization()" style="
padding: 10px 15px;
background: rgba(76, 201, 240, 0.9);
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
gap: 8px;
">🎨 Customize</button>
<button onclick="datingScene.toggleMobileMenu()" style="
padding: 10px 15px;
background: rgba(255, 214, 102, 0.9);
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
gap: 8px;
display: none;
" id="mobileMenuBtn">📱 Mobile</button>
`;
document.getElementById('container').appendChild(enhancedMenu);
// Show mobile menu button only on mobile
if (this.mobileOptimizer.isMobile) {
document.getElementById('mobileMenuBtn').style.display = 'flex';
}
}
toggleSocialFeatures() {
if (this.socialSystem.friendsPanel.style.display === 'block') {
this.socialSystem.hideFriendsPanel();
this.socialSystem.hideGroupsPanel();
} else {
this.socialSystem.showFriendsPanel();
}
}
toggleCustomization() {
if (this.customizationSystem.customizationPanel.style.display === 'block') {
this.customizationSystem.hideCustomizationPanel();
this.customizationSystem.hideQuickCustomize();
this.customizationSystem.themeSelector.style.display = 'none';
} else {
this.customizationSystem.showQuickCustomize();
}
}
toggleMobileMenu() {
if (this.mobileOptimizer.quickMenu) {
this.mobileOptimizer.toggleQuickMenu();
}
}
setupMobileFriendlyUI() {
// Adapt existing UI for mobile
if (this.mobileOptimizer.isMobile) {
// Make chat UI more mobile-friendly
const chatUI = document.getElementById('chatUI');
if (chatUI) {
chatUI.style.width = '90%';
chatUI.style.left = '5%';
chatUI.style.bottom = '100px';
chatUI.style.maxHeight = '40vh';
}
// Adjust camera controls position
const cameraControls = document.querySelector('[style*="top: 20px"][style*="right: 20px"]');
if (cameraControls) {
cameraControls.style.top = '80px';
}
}
}
createUserAvatar(config) {
this.currentUser = new Avatar(config);
this.currentUser.mesh.position.set(0, 0, 3);
this.scene.add(this.currentUser.mesh);
this.avatars.push(this.currentUser);
// Initialize movement system
this.currentUser.movement = new AvatarMovement(this.currentUser, this);
// Set up camera
this.datingCamera = new DatingCamera(this.camera, this, this.currentUser);
// Apply any saved customizations
this.applySavedCustomizations();
console.log(`Welcome, ${config.name}! Ready to mingle! 💖`);
document.getElementById('avatarCustomization').style.display = 'none';
// NPC greetings
this.avatars.forEach(avatar => {
if (avatar !== this.currentUser && avatar.ai) {
const userPosition = this.currentUser.mesh.position.clone();
avatar.lookAt(userPosition);
setTimeout(() => {
avatar.wave();
// Some NPCs might approach or send friend requests
if (Math.random() < 0.4) {
setTimeout(() => {
this.simulateSocialInteraction(avatar);
}, 3000);
}
}, 1000);
}
});
}
applySavedCustomizations() {
if (!this.currentUser) return;
// Apply saved avatar customizations
const savedCustomizations = this.customizationSystem.avatarCustomizations.get(this.currentUser);
if (savedCustomizations) {
this.customizationSystem.applyAvatarCustomizations(this.currentUser, savedCustomizations);
}
// Apply saved theme
if (this.customizationSystem.currentTheme !== 'default') {
this.customizationSystem.applyEnvironmentTheme(this.customizationSystem.currentTheme);
}
}
simulateSocialInteraction(avatar) {
// 30% chance to send friend request
if (Math.random() < 0.3) {
this.socialSystem.showSocialNotification(
`${avatar.options.name} sent you a friend request!`,
'info'
);
// Auto-accept after 5 seconds (for demo)
setTimeout(() => {
this.socialSystem.addFriend({
id: avatar.options.name.toLowerCase(),
name: avatar.options.name,
online: true,
inWorld: true,
friendshipLevel: 1,
lastSeen: Date.now()
});
}, 5000);
}
// 40% chance to start conversation
if (Math.random() < 0.4) {
avatar.ai.approachAvatar(this.currentUser);
}
}
animate() {
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000;
this.lastTime = currentTime;
requestAnimationFrame(() => this.animate());
// Update all systems
this.performanceOptimizer.updatePerformanceStats();
if (this.datingCamera) this.datingCamera.update();
if (this.proximityChat) this.proximityChat.update();
if (this.interactiveEnv) this.interactiveEnv.update();
if (this.dayNightCycle) this.dayNightCycle.update(deltaTime);
if (this.weatherSystem) this.weatherSystem.update(deltaTime);
if (this.mobileOptimizer) this.mobileOptimizer.update(deltaTime);
// Update NPC AI
this.avatars.forEach(avatar => {
if (avatar.ai) {
avatar.ai.update(deltaTime);
}
});
// Update mini-games
this.miniGameSystem.activeGames.forEach(game => {
if (game.update) game.update(deltaTime);
});
this.renderer.render(this.scene, this.camera);
}
}
// Global variables for new systems
let datingScene;
let datingCamera;
let emoteSystem;
let dayNightCycle;
let weatherSystem;
let miniGameSystem;
let relationshipSystem;
let mobileOptimizer;
let socialSystem;
let customizationSystem;
window.addEventListener('DOMContentLoaded', () => {
datingScene = new DatingScene();
emoteSystem = datingScene.emoteSystem;
dayNightCycle = datingScene.dayNightCycle;
weatherSystem = datingScene.weatherSystem;
miniGameSystem = datingScene.miniGameSystem;
relationshipSystem = datingScene.relationshipSystem;
mobileOptimizer = datingScene.mobileOptimizer;
socialSystem = datingScene.socialSystem;
customizationSystem = datingScene.customizationSystem;
console.log("Enhanced 3D Dating Experience loaded! Mobile, social, and customization features activated! 🎉");
setupAvatarInteraction();
document.addEventListener('click', () => {
document.getElementById('messageInput').focus();
});
});
What We've Built in Part 7: 🎉
-
Mobile Optimization:
- Touch controls with virtual joystick and gesture recognition
- Gyroscope support for immersive movement
- Responsive UI that adapts to screen size
- Mobile-friendly menus and controls
- Performance optimizations for mobile devices
-
Social Features:
- Friend system with requests and management
- Group creation and joining
- Social notifications and events
- Teleportation to friends and groups
- Persistent social data storage
-
Enhanced Customization:
- Comprehensive avatar customization (body, face, clothing, accessories)
- Environment themes with special effects
- Real-time preview of changes
- Random avatar generator
- Persistent customization settings
-
Integrated Experience:
- Mobile and desktop compatibility
- Social interactions with NPCs
- Theme-based environment changes
- Enhanced user onboarding
Key Features Explained: 🔑
- Touch Interface: Gesture-based controls optimized for mobile devices
- Social Graph: Friend and group management with persistent storage
- Theme System: Dynamic environment changes with special effects
- Customization Engine: Real-time avatar modification with preview
- Cross-Platform: Responsive design that works on all devices
Next Time in Part 8: 🚀
We'll add:
- Audio Enhancement: Spatial audio, background music, and sound effects
- Accessibility Features: Screen readers, color blindness modes, and input alternatives
- Advanced Animation: More realistic avatar movements and interactions
- Data Persistence: Cloud saving and cross-device synchronization
- Security & Privacy: User safety features and privacy controls
Current Project Status: Our 3D dating app is now a fully-featured social platform! With mobile support, social features, and extensive customization, we've created an engaging virtual world where users can express themselves and build meaningful connections across all devices. Love truly knows no bounds! 💕
Fun Fact: Our mobile optimization is so good that users can now find love while waiting in line for coffee! The virtual joystick is probably more responsive than some real-world dating conversations! 😄
Ready for Part 8 where we'll make everything sound as good as it looks?
Part 8: Audio Enhancement, Accessibility & Advanced Animation - A Feast for the Senses! 🎵♿💫
Welcome back, sensory architect! Our dating world looks amazing and works everywhere, but it's about as quiet as a library during finals week. Time to add immersive audio, ensure everyone can enjoy the experience, and make our animations truly come alive!
Step 1: Audio Enhancement System - Set the Mood with Sound! 🎵
Let's create a comprehensive audio system with spatial audio, background music, and immersive sound effects:
// audio-system.js - Because love should sound as good as it looks! 🎶
class AudioSystem {
constructor(scene) {
this.scene = scene;
this.audioContext = null;
this.backgroundMusic = null;
this.soundEffects = new Map();
this.spatialSounds = new Map();
this.volumeLevels = {
master: 0.8,
music: 0.6,
sfx: 0.7,
voice: 0.9
};
this.currentPlaylist = [];
this.currentTrackIndex = 0;
this.setupAudioContext();
this.loadSoundEffects();
this.setupAudioUI();
this.startBackgroundMusic();
console.log("Audio system initialized! Setting the mood with sound! 🎵");
}
async setupAudioContext() {
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Resume audio context on user interaction (browser security requirement)
document.addEventListener('click', () => {
if (this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
}, { once: true });
console.log("Audio context ready! 🎧");
} catch (error) {
console.warn("Web Audio API not supported:", error);
}
}
async loadSoundEffects() {
const soundEffects = {
// UI sounds
'click': 'sounds/ui/click.mp3',
'hover': 'sounds/ui/hover.mp3',
'notification': 'sounds/ui/notification.mp3',
'success': 'sounds/ui/success.mp3',
// Environment sounds
'footsteps': 'sounds/environment/footsteps.mp3',
'rain': 'sounds/environment/rain.mp3',
'wind': 'sounds/environment/wind.mp3',
'birds': 'sounds/environment/birds.mp3',
// Social sounds
'wave': 'sounds/social/wave.mp3',
'heart': 'sounds/social/heart.mp3',
'laugh': 'sounds/social/laugh.mp3',
'dance': 'sounds/social/dance.mp3',
// Game sounds
'game_start': 'sounds/games/start.mp3',
'game_win': 'sounds/games/win.mp3',
'game_lose': 'sounds/games/lose.mp3'
};
// In a real implementation, you would load these files
// For this demo, we'll create placeholder oscillators
this.createPlaceholderSounds(soundEffects);
}
createPlaceholderSounds(soundEffects) {
Object.keys(soundEffects).forEach(soundName => {
this.soundEffects.set(soundName, {
play: () => this.playSyntheticSound(soundName)
});
});
}
playSyntheticSound(soundName) {
if (!this.audioContext) return;
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
// Different sounds based on type
const soundConfig = {
'click': { type: 'sine', frequency: 800, duration: 0.1 },
'hover': { type: 'sine', frequency: 600, duration: 0.05 },
'notification': { type: 'sine', frequency: 1000, duration: 0.2 },
'success': { type: 'sine', frequency: 1200, duration: 0.3 },
'wave': { type: 'sine', frequency: 400, duration: 0.5 },
'heart': { type: 'sine', frequency: 523, duration: 0.8 }, // C5
'game_win': { type: 'sine', frequency: 1047, duration: 0.5 } // C6
};
const config = soundConfig[soundName] || { type: 'sine', frequency: 440, duration: 0.2 };
oscillator.type = config.type;
oscillator.frequency.value = config.frequency;
gainNode.gain.setValueAtTime(0, this.audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(this.volumeLevels.sfx, this.audioContext.currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + config.duration);
oscillator.start();
oscillator.stop(this.audioContext.currentTime + config.duration);
}
setupAudioUI() {
this.createAudioControls();
this.createVolumeMixer();
this.setupAudioVisualizer();
}
createAudioControls() {
this.audioControls = document.createElement('div');
this.audioControls.style.cssText = `
position: fixed;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
z-index: 100;
backdrop-filter: blur(10px);
min-width: 200px;
`;
this.audioControls.innerHTML = `
<h4 style="margin: 0 0 10px 0; color: #ff6b6b;">🎵 Audio Controls</h4>
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
<button id="musicToggle" style="
flex: 1;
padding: 8px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">🔇 Music</button>
<button id="sfxToggle" style="
flex: 1;
padding: 8px;
background: #ffd166;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">🔊 SFX</button>
</div>
<div style="margin-bottom: 10px;">
<label style="display: block; margin-bottom: 5px; font-size: 12px;">Master Volume</label>
<input type="range" id="masterVolume" min="0" max="1" step="0.1" value="${this.volumeLevels.master}"
style="width: 100%;">
</div>
<button onclick="audioSystem.toggleAudioPanel()" style="
width: 100%;
padding: 8px;
background: #666;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 12px;
">Advanced Settings</button>
`;
document.getElementById('container').appendChild(this.audioControls);
// Event listeners
document.getElementById('musicToggle').addEventListener('click', () => this.toggleMusic());
document.getElementById('sfxToggle').addEventListener('click', () => this.toggleSFX());
document.getElementById('masterVolume').addEventListener('input', (e) => {
this.setMasterVolume(parseFloat(e.target.value));
});
this.updateAudioButtons();
}
createVolumeMixer() {
this.volumeMixer = document.createElement('div');
this.volumeMixer.style.cssText = `
position: fixed;
bottom: 20px;
left: 250px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
z-index: 100;
backdrop-filter: blur(10px);
display: none;
`;
this.volumeMixer.innerHTML = `
<h4 style="margin: 0 0 10px 0; color: #ff6b6b;">🎚️ Volume Mixer</h4>
<div style="display: grid; gap: 10px;">
<div>
<label style="display: block; margin-bottom: 5px; font-size: 12px;">Music</label>
<input type="range" id="musicVolume" min="0" max="1" step="0.1" value="${this.volumeLevels.music}"
style="width: 100%;">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-size: 12px;">Sound Effects</label>
<input type="range" id="sfxVolume" min="0" max="1" step="0.1" value="${this.volumeLevels.sfx}"
style="width: 100%;">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-size: 12px;">Voice Chat</label>
<input type="range" id="voiceVolume" min="0" max="1" step="0.1" value="${this.volumeLevels.voice}"
style="width: 100%;">
</div>
</div>
`;
document.getElementById('container').appendChild(this.volumeMixer);
// Event listeners for mixer
document.getElementById('musicVolume').addEventListener('input', (e) => {
this.setVolume('music', parseFloat(e.target.value));
});
document.getElementById('sfxVolume').addEventListener('input', (e) => {
this.setVolume('sfx', parseFloat(e.target.value));
});
document.getElementById('voiceVolume').addEventListener('input', (e) => {
this.setVolume('voice', parseFloat(e.target.value));
});
}
toggleAudioPanel() {
if (this.volumeMixer.style.display === 'block') {
this.volumeMixer.style.display = 'none';
} else {
this.volumeMixer.style.display = 'block';
}
}
setMasterVolume(volume) {
this.volumeLevels.master = volume;
this.updateAllVolumes();
this.saveAudioSettings();
}
setVolume(type, volume) {
this.volumeLevels[type] = volume;
this.updateAllVolumes();
this.saveAudioSettings();
}
updateAllVolumes() {
// Update background music volume
if (this.backgroundMusic) {
this.backgroundMusic.volume = this.volumeLevels.music * this.volumeLevels.master;
}
// In a real implementation, update all sound effects and spatial audio
}
toggleMusic() {
if (this.backgroundMusic) {
if (this.backgroundMusic.paused) {
this.backgroundMusic.play();
document.getElementById('musicToggle').textContent = '🔇 Music';
} else {
this.backgroundMusic.pause();
document.getElementById('musicToggle').textContent = '🔈 Music';
}
}
}
toggleSFX() {
this.sfxEnabled = !this.sfxEnabled;
this.updateAudioButtons();
this.saveAudioSettings();
}
updateAudioButtons() {
const musicButton = document.getElementById('musicToggle');
const sfxButton = document.getElementById('sfxToggle');
if (this.backgroundMusic && !this.backgroundMusic.paused) {
musicButton.textContent = '🔇 Music';
} else {
musicButton.textContent = '🔈 Music';
}
sfxButton.textContent = this.sfxEnabled ? '🔊 SFX' : '🔇 SFX';
}
async startBackgroundMusic() {
// In a real implementation, you would load actual music files
// For this demo, we'll create a simple generative music system
if (!this.audioContext) return;
try {
// Create a simple ambient music generator
this.createGenerativeMusic();
// For demo purposes, also create a HTML5 audio fallback
this.backgroundMusic = new Audio();
this.backgroundMusic.loop = true;
this.backgroundMusic.volume = this.volumeLevels.music * this.volumeLevels.master;
// Simulate loading music
setTimeout(() => {
this.backgroundMusic.play().catch(e => {
console.log("Auto-play prevented, music will start on user interaction");
});
}, 1000);
} catch (error) {
console.warn("Background music setup failed:", error);
}
}
createGenerativeMusic() {
if (!this.audioContext) return;
// Create ambient pad sound
const padOscillator = this.audioContext.createOscillator();
const padGain = this.audioContext.createGain();
const padFilter = this.audioContext.createBiquadFilter();
padOscillator.type = 'sine';
padOscillator.frequency.value = 110; // A2
padGain.gain.value = 0.1 * this.volumeLevels.music * this.volumeLevels.master;
padFilter.type = 'lowpass';
padFilter.frequency.value = 800;
padOscillator.connect(padFilter);
padFilter.connect(padGain);
padGain.connect(this.audioContext.destination);
// Slowly modulate the frequency for ambient effect
const lfo = this.audioContext.createOscillator();
const lfoGain = this.audioContext.createGain();
lfo.type = 'sine';
lfo.frequency.value = 0.1; // Very slow modulation
lfoGain.gain.value = 5; // Small pitch variation
lfo.connect(lfoGain);
lfoGain.connect(padOscillator.frequency);
padOscillator.start();
lfo.start();
this.generativeMusic = { padOscillator, padGain, lfo };
}
setupAudioVisualizer() {
this.visualizer = document.createElement('div');
this.visualizer.style.cssText = `
position: fixed;
bottom: 150px;
right: 20px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 10px;
z-index: 100;
width: 200px;
height: 60px;
display: none;
`;
document.getElementById('container').appendChild(this.visualizer);
// Create canvas for visualizer
this.visualizerCanvas = document.createElement('canvas');
this.visualizerCanvas.width = 200;
this.visualizerCanvas.height = 60;
this.visualizer.appendChild(this.visualizerCanvas);
this.setupAnalyserNode();
}
setupAnalyserNode() {
if (!this.audioContext || !this.backgroundMusic) return;
// In a real implementation, you would connect the analyser to audio nodes
// This is a simplified version for demonstration
this.startVisualizerAnimation();
}
startVisualizerAnimation() {
const ctx = this.visualizerCanvas.getContext('2d');
const width = this.visualizerCanvas.width;
const height = this.visualizerCanvas.height;
const animate = () => {
if (this.visualizer.style.display !== 'none') {
ctx.clearRect(0, 0, width, height);
// Create fake audio data for visualization
const bars = 20;
const barWidth = width / bars;
for (let i = 0; i < bars; i++) {
const barHeight = Math.random() * height * 0.8;
const hue = (i / bars) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 60%)`;
ctx.fillRect(
i * barWidth,
height - barHeight,
barWidth - 2,
barHeight
);
}
}
requestAnimationFrame(animate);
};
animate();
}
playSound(soundName, options = {}) {
if (!this.sfxEnabled || !this.soundEffects.has(soundName)) return;
const sound = this.soundEffects.get(soundName);
sound.play();
// Log sound playback for debugging
if (options.debug) {
console.log(`Playing sound: ${soundName}`);
}
}
playSpatialSound(soundName, position, options = {}) {
if (!this.sfxEnabled || !this.audioContext) return;
// Create spatial audio effect using Web Audio API
const panner = this.audioContext.createPanner();
panner.panningModel = 'HRTF';
panner.distanceModel = 'inverse';
panner.refDistance = 1;
panner.maxDistance = 50;
panner.rolloffFactor = 1;
panner.coneInnerAngle = 360;
panner.coneOuterAngle = 0;
panner.coneOuterGain = 0;
// Set position relative to listener
panner.positionX.value = position.x;
panner.positionY.value = position.y;
panner.positionZ.value = position.z;
// Create sound source
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(panner);
panner.connect(this.audioContext.destination);
// Configure based on sound type
const soundConfig = {
'footsteps': { type: 'noise', duration: 0.3, frequency: 200 },
'heart': { type: 'sine', duration: 1.0, frequency: 523 }
};
const config = soundConfig[soundName] || { type: 'sine', duration: 0.5, frequency: 440 };
if (config.type === 'noise') {
// Create brown noise for footsteps
const bufferSize = this.audioContext.sampleRate * config.duration;
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
const data = buffer.getChannelData(0);
let lastOut = 0;
for (let i = 0; i < bufferSize; i++) {
const white = Math.random() * 2 - 1;
data[i] = (lastOut + (0.02 * white)) / 1.02;
lastOut = data[i];
data[i] *= 3.5;
}
const source = this.audioContext.createBufferSource();
source.buffer = buffer;
source.connect(gainNode);
source.start();
} else {
oscillator.type = config.type;
oscillator.frequency.value = config.frequency;
oscillator.start();
oscillator.stop(this.audioContext.currentTime + config.duration);
}
gainNode.gain.setValueAtTime(this.volumeLevels.sfx * this.volumeLevels.master, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + config.duration);
}
// Environment sound management
startEnvironmentSounds() {
this.playEnvironmentSound('birds', { volume: 0.3, loop: true });
// Weather-based sounds
if (this.scene.weatherSystem) {
this.setupWeatherSounds();
}
}
setupWeatherSounds() {
// Listen for weather changes
const originalSetWeather = this.scene.weatherSystem.setWeather.bind(this.scene.weatherSystem);
this.scene.weatherSystem.setWeather = (weatherType, intensity) => {
originalSetWeather(weatherType, intensity);
this.handleWeatherChange(weatherType, intensity);
};
}
handleWeatherChange(weatherType, intensity) {
// Stop all weather sounds
this.stopWeatherSounds();
// Start new weather sounds
switch(weatherType) {
case 'rainy':
this.playEnvironmentSound('rain', { volume: intensity * 0.5, loop: true });
this.playEnvironmentSound('wind', { volume: intensity * 0.3, loop: true });
break;
case 'windy':
this.playEnvironmentSound('wind', { volume: intensity * 0.7, loop: true });
break;
case 'snowy':
this.playEnvironmentSound('wind', { volume: intensity * 0.4, loop: true });
break;
}
}
stopWeatherSounds() {
// Implementation to stop specific environment sounds
}
playEnvironmentSound(soundName, options = {}) {
if (!this.sfxEnabled) return;
// Implementation for looping environment sounds
console.log(`Playing environment sound: ${soundName}`, options);
}
// Integration with existing systems
setupAudioIntegrations() {
this.integrateWithEmotes();
this.integrateWithGames();
this.integrateWithInteractions();
}
integrateWithEmotes() {
// Play sounds when emotes are used
const originalPlayEmote = this.scene.emoteSystem.playEmote.bind(this.scene.emoteSystem);
this.scene.emoteSystem.playEmote = (emoteKey, avatar) => {
originalPlayEmote(emoteKey, avatar);
const emoteSounds = {
'wave': 'wave',
'dance': 'dance',
'heart': 'heart',
'laugh': 'laugh'
};
if (emoteSounds[emoteKey]) {
this.playSound(emoteSounds[emoteKey]);
// Spatial sound if avatar is not current user
if (avatar !== this.scene.currentUser) {
this.playSpatialSound(emoteSounds[emoteKey], avatar.mesh.position);
}
}
};
}
integrateWithGames() {
// Game sound effects
const originalStartGame = this.scene.miniGameSystem.startGame.bind(this.scene.miniGameSystem);
this.scene.miniGameSystem.startGame = (gameType, player1, player2) => {
originalStartGame(gameType, player1, player2);
this.playSound('game_start');
};
}
integrateWithInteractions() {
// UI interaction sounds
document.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') {
this.playSound('click');
}
});
document.addEventListener('mouseover', (e) => {
if (e.target.tagName === 'BUTTON') {
this.playSound('hover');
}
});
}
saveAudioSettings() {
try {
localStorage.setItem('datingApp_audioSettings', JSON.stringify({
volumeLevels: this.volumeLevels,
sfxEnabled: this.sfxEnabled,
musicEnabled: this.backgroundMusic ? !this.backgroundMusic.paused : true
}));
} catch (error) {
console.warn('Error saving audio settings:', error);
}
}
loadAudioSettings() {
try {
const saved = localStorage.getItem('datingApp_audioSettings');
if (saved) {
const settings = JSON.parse(saved);
this.volumeLevels = { ...this.volumeLevels, ...settings.volumeLevels };
this.sfxEnabled = settings.sfxEnabled !== undefined ? settings.sfxEnabled : true;
// Update UI
this.updateAllVolumes();
this.updateAudioButtons();
}
} catch (error) {
console.warn('Error loading audio settings:', error);
}
}
// Cleanup
cleanup() {
if (this.generativeMusic) {
this.generativeMusic.padOscillator.stop();
this.generativeMusic.lfo.stop();
}
if (this.backgroundMusic) {
this.backgroundMusic.pause();
this.backgroundMusic = null;
}
}
}
Step 2: Accessibility System - Love for Everyone! ♿
Let's create comprehensive accessibility features to ensure everyone can enjoy our dating world:
// accessibility.js - Because everyone deserves to find love! 💝
class AccessibilitySystem {
constructor(scene) {
this.scene = scene;
this.features = {
screenReader: false,
highContrast: false,
colorBlindMode: 'none',
largeText: false,
reducedMotion: false,
keyboardNavigation: true,
voiceControl: false
};
this.setupAccessibility();
this.createAccessibilityMenu();
this.setupKeyboardNavigation();
console.log("Accessibility system initialized! Making love accessible to all! ♿");
}
setupAccessibility() {
this.loadAccessibilitySettings();
this.applyAccessibilityFeatures();
this.setupScreenReader();
this.setupColorAdjustments();
}
createAccessibilityMenu() {
this.accessibilityMenu = document.createElement('div');
this.accessibilityMenu.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 20px;
z-index: 1000;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
display: none;
`;
this.accessibilityMenu.innerHTML = `
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: #ff6b6b;">♿ Accessibility</h3>
<button onclick="accessibilitySystem.hideMenu()" style="
background: none;
border: none;
font-size: 20px;
cursor: pointer;
">✕</button>
</div>
<div style="display: grid; gap: 20px;">
<!-- Vision -->
<div>
<h4 style="margin: 0 0 10px 0;">👁️ Vision</h4>
<div style="display: grid; gap: 10px;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="highContrast" ${this.features.highContrast ? 'checked' : ''}>
<span>High Contrast Mode</span>
</label>
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="largeText" ${this.features.largeText ? 'checked' : ''}>
<span>Large Text</span>
</label>
<div>
<label style="display: block; margin-bottom: 5px;">Color Blindness</label>
<select id="colorBlindMode" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 8px;">
<option value="none" ${this.features.colorBlindMode === 'none' ? 'selected' : ''}>None</option>
<option value="protanopia" ${this.features.colorBlindMode === 'protanopia' ? 'selected' : ''}>Protanopia (Red-Blind)</option>
<option value="deuteranopia" ${this.features.colorBlindMode === 'deuteranopia' ? 'selected' : ''}>Deuteranopia (Green-Blind)</option>
<option value="tritanopia" ${this.features.colorBlindMode === 'tritanopia' ? 'selected' : ''}>Tritanopia (Blue-Blind)</option>
</select>
</div>
</div>
</div>
<!-- Hearing -->
<div>
<h4 style="margin: 0 0 10px 0;">👂 Hearing</h4>
<div style="display: grid; gap: 10px;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="visualAlerts" ${this.features.visualAlerts ? 'checked' : ''}>
<span>Visual Alerts for Sounds</span>
</label>
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="subtitles" ${this.features.subtitles ? 'checked' : ''}>
<span>Subtitles for Voice Chat</span>
</label>
</div>
</div>
<!-- Motor -->
<div>
<h4 style="margin: 0 0 10px 0;">🎮 Motor</h4>
<div style="display: grid; gap: 10px;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="reducedMotion" ${this.features.reducedMotion ? 'checked' : ''}>
<span>Reduced Motion</span>
</label>
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="keyboardNav" ${this.features.keyboardNavigation ? 'checked' : ''}>
<span>Keyboard Navigation</span>
</label>
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="voiceControl" ${this.features.voiceControl ? 'checked' : ''}>
<span>Voice Control</span>
</label>
</div>
</div>
<!-- Cognitive -->
<div>
<h4 style="margin: 0 0 10px 0;">🧠 Cognitive</h4>
<div style="display: grid; gap: 10px;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="simpleUI" ${this.features.simpleUI ? 'checked' : ''}>
<span>Simplified UI</span>
</label>
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="readingAssistance" ${this.features.readingAssistance ? 'checked' : ''}>
<span>Reading Assistance</span>
</label>
</div>
</div>
</div>
<div style="margin-top: 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<button onclick="accessibilitySystem.applySettings()" style="
padding: 12px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Apply Settings</button>
<button onclick="accessibilitySystem.resetToDefaults()" style="
padding: 12px;
background: #ffd166;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Reset Defaults</button>
</div>
<div style="margin-top: 15px; text-align: center;">
<button onclick="accessibilitySystem.showQuickAccess()" style="
background: none;
border: none;
color: #666;
text-decoration: underline;
cursor: pointer;
font-size: 12px;
">♿ Quick Access Panel</button>
</div>
`;
document.getElementById('container').appendChild(this.accessibilityMenu);
// Quick access button
this.createQuickAccessButton();
}
createQuickAccessButton() {
this.quickAccessBtn = document.createElement('button');
this.quickAccessBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: #ff6b6b;
color: white;
border: none;
font-size: 18px;
cursor: pointer;
z-index: 100;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
backdrop-filter: blur(10px);
`;
this.quickAccessBtn.textContent = '♿';
this.quickAccessBtn.title = 'Accessibility Settings';
this.quickAccessBtn.addEventListener('click', () => {
this.showMenu();
});
document.getElementById('container').appendChild(this.quickAccessBtn);
}
showMenu() {
this.accessibilityMenu.style.display = 'block';
}
hideMenu() {
this.accessibilityMenu.style.display = 'none';
}
applySettings() {
// Gather settings from form
this.features = {
highContrast: document.getElementById('highContrast').checked,
largeText: document.getElementById('largeText').checked,
colorBlindMode: document.getElementById('colorBlindMode').value,
visualAlerts: document.getElementById('visualAlerts').checked,
subtitles: document.getElementById('subtitles').checked,
reducedMotion: document.getElementById('reducedMotion').checked,
keyboardNavigation: document.getElementById('keyboardNav').checked,
voiceControl: document.getElementById('voiceControl').checked,
simpleUI: document.getElementById('simpleUI').checked,
readingAssistance: document.getElementById('readingAssistance').checked
};
this.applyAccessibilityFeatures();
this.saveSettings();
this.hideMenu();
this.showNotification('Accessibility settings applied! ♿');
}
applyAccessibilityFeatures() {
this.applyHighContrast();
this.applyLargeText();
this.applyColorBlindMode();
this.applyReducedMotion();
this.applySimpleUI();
this.setupVisualAlerts();
this.setupSubtitles();
}
applyHighContrast() {
const styleId = 'high-contrast-style';
let style = document.getElementById(styleId);
if (this.features.highContrast) {
if (!style) {
style = document.createElement('style');
style.id = styleId;
style.textContent = `
body {
filter: contrast(1.5) brightness(1.1);
}
button, input, select {
border: 2px solid #000000 !important;
}
.ui-element {
background: #000000 !important;
color: #ffffff !important;
border: 2px solid #ffffff !important;
}
`;
document.head.appendChild(style);
}
} else {
if (style) {
style.remove();
}
}
}
applyLargeText() {
const styleId = 'large-text-style';
let style = document.getElementById(styleId);
if (this.features.largeText) {
if (!style) {
style = document.createElement('style');
style.id = styleId;
style.textContent = `
body {
font-size: 18px !important;
}
button, input, select {
font-size: 18px !important;
padding: 12px !important;
}
.ui-text {
font-size: 18px !important;
}
`;
document.head.appendChild(style);
}
} else {
if (style) {
style.remove();
}
}
}
applyColorBlindMode() {
const styleId = 'color-blind-style';
let style = document.getElementById(styleId);
if (style) {
style.remove();
}
if (this.features.colorBlindMode !== 'none') {
style = document.createElement('style');
style.id = styleId;
let filter = '';
switch(this.features.colorBlindMode) {
case 'protanopia':
filter = 'url(#protanopia)';
break;
case 'deuteranopia':
filter = 'url(#deuteranopia)';
break;
case 'tritanopia':
filter = 'url(#tritanopia)';
break;
}
style.textContent = `
body {
filter: ${filter};
}
`;
// Add SVG filters for color blindness simulation
this.addColorBlindnessFilters();
document.head.appendChild(style);
}
}
addColorBlindnessFilters() {
const existingFilters = document.getElementById('color-blind-filters');
if (existingFilters) return;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.id = 'color-blind-filters';
svg.style.position = 'absolute';
svg.style.width = '0';
svg.style.height = '0';
// Protanopia (red-blind) filter
const protanopia = document.createElementNS('http://www.w3.org/2000/svg', 'filter');
protanopia.id = 'protanopia';
// Simplified color transformation matrix
protanopia.innerHTML = `
<feColorMatrix type="matrix" values="
0.567, 0.433, 0, 0, 0
0.558, 0.442, 0, 0, 0
0, 0.242, 0.758, 0, 0
0, 0, 0, 1, 0
"/>
`;
// Deuteranopia (green-blind) filter
const deuteranopia = document.createElementNS('http://www.w3.org/2000/svg', 'filter');
deuteranopia.id = 'deuteranopia';
deuteranopia.innerHTML = `
<feColorMatrix type="matrix" values="
0.625, 0.375, 0, 0, 0
0.7, 0.3, 0, 0, 0
0, 0.3, 0.7, 0, 0
0, 0, 0, 1, 0
"/>
`;
// Tritanopia (blue-blind) filter
const tritanopia = document.createElementNS('http://www.w3.org/2000/svg', 'filter');
tritanopia.id = 'tritanopia';
tritanopia.innerHTML = `
<feColorMatrix type="matrix" values="
0.95, 0.05, 0, 0, 0
0, 0.433, 0.567,0, 0
0, 0.475, 0.525,0, 0
0, 0, 0, 1, 0
"/>
`;
svg.appendChild(protanopia);
svg.appendChild(deuteranopia);
svg.appendChild(tritanopia);
document.body.appendChild(svg);
}
applyReducedMotion() {
if (this.features.reducedMotion) {
// Disable or reduce animations
document.body.style.animationPlayState = 'paused';
// Reduce camera movement sensitivity
if (this.scene.datingCamera) {
this.scene.datingCamera.orbitSpeed *= 0.5;
}
// Reduce particle effects
if (this.scene.weatherSystem) {
this.scene.weatherSystem.weatherIntensity *= 0.3;
}
} else {
// Restore animations
document.body.style.animationPlayState = 'running';
if (this.scene.datingCamera) {
this.scene.datingCamera.orbitSpeed *= 2;
}
if (this.scene.weatherSystem) {
this.scene.weatherSystem.weatherIntensity /= 0.3;
}
}
}
applySimpleUI() {
const styleId = 'simple-ui-style';
let style = document.getElementById(styleId);
if (this.features.simpleUI) {
if (!style) {
style = document.createElement('style');
style.id = styleId;
style.textContent = `
.complex-ui {
display: none !important;
}
button {
min-width: 80px !important;
min-height: 40px !important;
}
.ui-container {
border: 1px solid #ccc !important;
background: #f0f0f0 !important;
}
`;
document.head.appendChild(style);
}
} else {
if (style) {
style.remove();
}
}
}
setupVisualAlerts() {
if (this.features.visualAlerts && this.scene.audioSystem) {
// Create visual indicators for important sounds
this.setupSoundVisualizers();
}
}
setupSoundVisualizers() {
// Create visual alerts for different sound types
this.soundAlerts = new Map();
const alertContainer = document.createElement('div');
alertContainer.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
`;
alertContainer.id = 'sound-alerts';
document.body.appendChild(alertContainer);
// Listen for audio events
if (this.scene.audioSystem) {
const originalPlaySound = this.scene.audioSystem.playSound.bind(this.scene.audioSystem);
this.scene.audioSystem.playSound = (soundName, options) => {
originalPlaySound(soundName, options);
this.showSoundAlert(soundName);
};
}
}
showSoundAlert(soundName) {
if (!this.features.visualAlerts) return;
const alert = document.createElement('div');
alert.style.cssText = `
background: #ff6b6b;
color: white;
padding: 10px 15px;
border-radius: 20px;
animation: slideInDown 0.3s ease-out;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
`;
const soundLabels = {
'notification': '🔔 New Notification',
'game_start': '🎮 Game Starting',
'wave': '👋 Someone Waved',
'heart': '💖 Romantic Gesture'
};
alert.textContent = soundLabels[soundName] || `Sound: ${soundName}`;
const container = document.getElementById('sound-alerts');
container.appendChild(alert);
setTimeout(() => {
alert.style.animation = 'slideOutUp 0.3s ease-in';
setTimeout(() => alert.remove(), 300);
}, 3000);
}
setupSubtitles() {
if (this.features.subtitles) {
this.createSubtitleDisplay();
// Integrate with voice chat system
if (this.scene.voiceChat) {
this.setupVoiceChatSubtitles();
}
}
}
createSubtitleDisplay() {
this.subtitleDisplay = document.createElement('div');
this.subtitleDisplay.style.cssText = `
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px 25px;
border-radius: 25px;
z-index: 1000;
max-width: 80%;
text-align: center;
font-size: 16px;
display: none;
backdrop-filter: blur(10px);
`;
this.subtitleDisplay.id = 'subtitle-display';
document.body.appendChild(this.subtitleDisplay);
}
setupVoiceChatSubtitles() {
// This would integrate with the actual voice-to-text system
// For demo purposes, we'll simulate subtitles
console.log("Voice chat subtitles enabled");
}
showSubtitle(text, duration = 3000) {
if (!this.features.subtitles) return;
const display = document.getElementById('subtitle-display');
if (display) {
display.textContent = text;
display.style.display = 'block';
setTimeout(() => {
display.style.display = 'none';
}, duration);
}
}
setupScreenReader() {
// ARIA labels and screen reader support
this.setupAriaLabels();
this.setupScreenReaderNotifications();
}
setupAriaLabels() {
// Add ARIA labels to important elements
const buttons = document.querySelectorAll('button');
buttons.forEach(button => {
if (!button.getAttribute('aria-label')) {
const text = button.textContent || button.title || 'button';
button.setAttribute('aria-label', text);
}
});
// Add ARIA live regions for dynamic content
const liveRegion = document.createElement('div');
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
liveRegion.id = 'screen-reader-announcements';
document.body.appendChild(liveRegion);
}
announceToScreenReader(message) {
const liveRegion = document.getElementById('screen-reader-announcements');
if (liveRegion) {
liveRegion.textContent = message;
// Clear after a moment so same message can be announced again
setTimeout(() => {
liveRegion.textContent = '';
}, 1000);
}
}
setupKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
if (!this.features.keyboardNavigation) return;
switch(e.key) {
case 'Tab':
this.handleTabNavigation(e);
break;
case 'Enter':
this.handleEnterNavigation(e);
break;
case 'Escape':
this.handleEscapeNavigation(e);
break;
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
this.handleArrowNavigation(e);
break;
}
});
}
handleTabNavigation(event) {
// Enhanced tab navigation for complex UI
const focusableElements = document.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
// Custom tab order logic can be implemented here
console.log('Tab navigation active');
}
setupVoiceControl() {
if (this.features.voiceControl && 'webkitSpeechRecognition' in window) {
this.setupSpeechRecognition();
}
}
setupSpeechRecognition() {
try {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
this.recognition = new SpeechRecognition();
this.recognition.continuous = true;
this.recognition.interimResults = true;
this.recognition.onresult = (event) => {
const transcript = event.results[event.results.length - 1][0].transcript.toLowerCase();
this.processVoiceCommand(transcript);
};
this.recognition.start();
} catch (error) {
console.warn('Speech recognition not supported:', error);
}
}
processVoiceCommand(transcript) {
const commands = {
'open chat': () => this.scene.mobileOptimizer.toggleMobileChat(),
'show friends': () => this.scene.socialSystem.showFriendsPanel(),
'dance': () => this.scene.currentUser?.dance(),
'wave': () => this.scene.emoteSystem.playEmote('wave', this.scene.currentUser),
'stop': () => this.recognition.stop()
};
for (const [command, action] of Object.entries(commands)) {
if (transcript.includes(command)) {
action();
this.showNotification(`Voice command: ${command}`);
break;
}
}
}
showQuickAccess() {
const quickPanel = document.createElement('div');
quickPanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 15px;
z-index: 1000;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
`;
quickPanel.innerHTML = `
<h4 style="margin: 0 0 15px 0; color: #ff6b6b;">♿ Quick Access</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<button onclick="accessibilitySystem.toggleFeature('highContrast')" style="padding: 10px; border: none; border-radius: 8px; background: #4ecdc4; color: white; cursor: pointer;">
High Contrast
</button>
<button onclick="accessibilitySystem.toggleFeature('largeText')" style="padding: 10px; border: none; border-radius: 8px; background: #ffd166; color: white; cursor: pointer;">
Large Text
</button>
<button onclick="accessibilitySystem.toggleFeature('reducedMotion')" style="padding: 10px; border: none; border-radius: 8px; background: #9370db; color: white; cursor: pointer;">
Reduced Motion
</button>
<button onclick="accessibilitySystem.toggleFeature('simpleUI')" style="padding: 10px; border: none; border-radius: 8px; background: #ff6b6b; color: white; cursor: pointer;">
Simple UI
</button>
</div>
<button onclick="quickPanel.remove()" style="width: 100%; padding: 10px; margin-top: 15px; background: #666; color: white; border: none; border-radius: 8px; cursor: pointer;">
Close
</button>
`;
document.getElementById('container').appendChild(quickPanel);
// Close on outside click
quickPanel.addEventListener('click', (e) => e.stopPropagation());
document.addEventListener('click', () => quickPanel.remove());
}
toggleFeature(featureName) {
this.features[featureName] = !this.features[featureName];
this.applyAccessibilityFeatures();
this.saveSettings();
this.showNotification(`${featureName} ${this.features[featureName] ? 'enabled' : 'disabled'}`);
}
resetToDefaults() {
this.features = {
screenReader: false,
highContrast: false,
colorBlindMode: 'none',
largeText: false,
reducedMotion: false,
keyboardNavigation: true,
voiceControl: false,
visualAlerts: false,
subtitles: false,
simpleUI: false,
readingAssistance: false
};
this.applyAccessibilityFeatures();
this.saveSettings();
this.hideMenu();
this.showNotification('Accessibility settings reset to defaults!');
}
showNotification(message) {
if (this.scene.socialSystem) {
this.scene.socialSystem.showSocialNotification(message, 'info');
} else {
alert(message);
}
}
loadAccessibilitySettings() {
try {
const saved = localStorage.getItem('datingApp_accessibility');
if (saved) {
this.features = { ...this.features, ...JSON.parse(saved) };
}
} catch (error) {
console.warn('Error loading accessibility settings:', error);
}
}
saveSettings() {
try {
localStorage.setItem('datingApp_accessibility', JSON.stringify(this.features));
} catch (error) {
console.warn('Error saving accessibility settings:', error);
}
}
}
Step 3: Advanced Animation System - Bringing Avatars to Life! 💫
Let's create more realistic and expressive animations for our avatars:
// advanced-animation.js - Because smooth moves make better connections! 💃
class AdvancedAnimationSystem {
constructor(scene) {
this.scene = scene;
this.animations = new Map();
this.motionCapture = new Map();
this.blendShapes = new Map();
this.ikSolvers = new Map();
this.setupAnimationSystem();
this.loadAnimationLibrary();
this.setupRealTimeMotion();
console.log("Advanced animation system initialized! Bringing avatars to life! 💫");
}
setupAnimationSystem() {
this.animationMixers = new Map();
this.clock = new THREE.Clock();
this.setupInverseKinematics();
this.setupFacialRigging();
this.setupPhysicsBasedAnimation();
}
setupInverseKinematics() {
// Setup IK solvers for more natural movement
this.ikSolvers.set('leftArm', this.createIKSolver());
this.ikSolvers.set('rightArm', this.createIKSolver());
this.ikSolvers.set('leftLeg', this.createIKSolver());
this.ikSolvers.set('rightLeg', this.createIKSolver());
}
createIKSolver() {
// Simplified IK solver implementation
return {
target: new THREE.Vector3(),
enabled: false,
solve: (bones, target) => {
// Basic IK solving logic
// In production, you'd use a proper IK library
return this.solveTwoBoneIK(bones, target);
}
};
}
solveTwoBoneIK(bones, target) {
// Simplified two-bone IK solver
const [upperBone, lowerBone, endBone] = bones;
// Calculate bone lengths
const upperLength = upperBone.length || 1;
const lowerLength = lowerBone.length || 1;
const totalLength = upperLength + lowerLength;
// Calculate directions and angles
const toTarget = new THREE.Vector3().subVectors(target, upperBone.position);
const distance = toTarget.length();
// Clamp distance to maximum reach
const clampedDistance = Math.min(distance, totalLength - 0.1);
// Calculate angles using law of cosines
const cosUpperAngle = (upperLength * upperLength + clampedDistance * clampedDistance - lowerLength * lowerLength) /
(2 * upperLength * clampedDistance);
const upperAngle = Math.acos(Math.max(-1, Math.min(1, cosUpperAngle)));
const cosLowerAngle = (upperLength * upperLength + lowerLength * lowerLength - clampedDistance * clampedDistance) /
(2 * upperLength * lowerLength);
const lowerAngle = Math.acos(Math.max(-1, Math.min(1, cosLowerAngle)));
// Apply rotations
upperBone.rotation.z = upperAngle;
lowerBone.rotation.z = lowerAngle - Math.PI; // Adjust for bone initial rotation
return { upperAngle, lowerAngle };
}
setupFacialRigging() {
// Create blend shapes for facial expressions
this.blendShapes.set('smile', { intensity: 0, target: 0 });
this.blendShapes.set('frown', { intensity: 0, target: 0 });
this.blendShapes.set('surprise', { intensity: 0, target: 0 });
this.blendShapes.set('blink', { intensity: 0, target: 0 });
this.setupEyeTracking();
this.setupLipSync();
}
setupEyeTracking() {
this.eyeTracking = {
leftEye: { lookAt: new THREE.Vector3(), blink: 0 },
rightEye: { lookAt: new THREE.Vector3(), blink: 0 },
combinedBlink: 0
};
}
setupLipSync() {
this.lipSync = {
visemes: new Map(),
currentViseme: 'neutral',
intensity: 0
};
// Basic viseme shapes
const visemes = ['AA', 'EE', 'OO', 'MM', 'FF', 'SS', 'RR', 'LL', 'neutral'];
visemes.forEach(viseme => {
this.lipSync.visemes.set(viseme, { intensity: 0, target: 0 });
});
}
setupPhysicsBasedAnimation() {
this.physics = {
gravity: new THREE.Vector3(0, -9.8, 0),
wind: new THREE.Vector3(),
collisions: new Map()
};
this.setupClothSimulation();
this.setupHairPhysics();
}
setupClothSimulation() {
// Simple cloth simulation for clothing
this.clothSimulation = {
points: [],
constraints: [],
update: (deltaTime) => {
this.updateClothPhysics(deltaTime);
}
};
}
setupHairPhysics() {
// Simple hair physics using verlet integration
this.hairPhysics = {
strands: [],
update: (deltaTime) => {
this.updateHairPhysics(deltaTime);
}
};
}
loadAnimationLibrary() {
// Load predefined animations
this.animations.set('idle', this.createIdleAnimation());
this.animations.set('walk', this.createWalkAnimation());
this.animations.set('run', this.createRunAnimation());
this.animations.set('jump', this.createJumpAnimation());
this.animations.set('dance', this.createDanceAnimation());
this.animations.set('wave', this.createWaveAnimation());
// Emotional animations
this.animations.set('laugh', this.createLaughAnimation());
this.animations.set('cry', this.createCryAnimation());
this.animations.set('shy', this.createShyAnimation());
this.animations.set('angry', this.createAngryAnimation());
}
createIdleAnimation() {
return {
duration: 2.0,
tracks: [
{
type: 'rotation',
bone: 'spine',
keyframes: [
{ time: 0, value: new THREE.Vector3(0, 0, 0) },
{ time: 1, value: new THREE.Vector3(0, 0.1, 0) },
{ time: 2, value: new THREE.Vector3(0, 0, 0) }
]
},
{
type: 'rotation',
bone: 'head',
keyframes: [
{ time: 0, value: new THREE.Vector3(0, 0, 0) },
{ time: 0.5, value: new THREE.Vector3(0.05, 0, 0) },
{ time: 1.5, value: new THREE.Vector3(-0.05, 0, 0) },
{ time: 2, value: new THREE.Vector3(0, 0, 0) }
]
}
],
loop: true
};
}
createWalkAnimation() {
return {
duration: 1.0,
tracks: [
{
type: 'rotation',
bone: 'leftLeg',
keyframes: [
{ time: 0, value: new THREE.Vector3(0.5, 0, 0) },
{ time: 0.5, value: new THREE.Vector3(-0.2, 0, 0) },
{ time: 1, value: new THREE.Vector3(0.5, 0, 0) }
]
},
{
type: 'rotation',
bone: 'rightLeg',
keyframes: [
{ time: 0, value: new THREE.Vector3(-0.2, 0, 0) },
{ time: 0.5, value: new THREE.Vector3(0.5, 0, 0) },
{ time: 1, value: new THREE.Vector3(-0.2, 0, 0) }
]
},
{
type: 'rotation',
bone: 'leftArm',
keyframes: [
{ time: 0, value: new THREE.Vector3(0, 0, -0.3) },
{ time: 0.5, value: new THREE.Vector3(0, 0, 0.3) },
{ time: 1, value: new THREE.Vector3(0, 0, -0.3) }
]
},
{
type: 'rotation',
bone: 'rightArm',
keyframes: [
{ time: 0, value: new THREE.Vector3(0, 0, 0.3) },
{ time: 0.5, value: new THREE.Vector3(0, 0, -0.3) },
{ time: 1, value: new THREE.Vector3(0, 0, 0.3) }
]
}
],
loop: true
};
}
createDanceAnimation() {
return {
duration: 2.0,
tracks: [
{
type: 'rotation',
bone: 'spine',
keyframes: [
{ time: 0, value: new THREE.Vector3(0, 0, 0) },
{ time: 0.5, value: new THREE.Vector3(0.3, 0, 0) },
{ time: 1, value: new THREE.Vector3(-0.3, 0, 0) },
{ time: 1.5, value: new THREE.Vector3(0.3, 0, 0) },
{ time: 2, value: new THREE.Vector3(0, 0, 0) }
]
},
{
type: 'rotation',
bone: 'leftArm',
keyframes: [
{ time: 0, value: new THREE.Vector3(0, 0, 0) },
{ time: 0.25, value: new THREE.Vector3(1.5, 0, 0) },
{ time: 0.75, value: new THREE.Vector3(0, 0, 0) },
{ time: 1.25, value: new THREE.Vector3(1.5, 0, 0) },
{ time: 1.75, value: new THREE.Vector3(0, 0, 0) }
]
},
{
type: 'rotation',
bone: 'rightArm',
keyframes: [
{ time: 0, value: new THREE.Vector3(0, 0, 0) },
{ time: 0.25, value: new THREE.Vector3(1.5, 0, 0) },
{ time: 0.75, value: new THREE.Vector3(0, 0, 0) },
{ time: 1.25, value: new THREE.Vector3(1.5, 0, 0) },
{ time: 1.75, value: new THREE.Vector3(0, 0, 0) }
]
}
],
loop: true
};
}
setupRealTimeMotion() {
this.setupProceduralAnimation();
this.setupMotionBlending();
this.setupAnimationEvents();
}
setupProceduralAnimation() {
// Procedural animations for more natural movement
this.proceduralAnimations = {
breathing: {
amplitude: 0.005,
frequency: 0.5,
phase: 0
},
microMovements: {
enabled: true,
intensity: 0.01
},
lookAtTarget: null
};
}
setupMotionBlending() {
this.motionBlending = {
currentAnimation: 'idle',
nextAnimation: null,
blendTime: 0.2,
blendProgress: 0,
isBlending: false
};
}
setupAnimationEvents() {
// Animation event system for syncing with other systems
this.animationEvents = new Map();
// Example: Play sound when animation reaches certain point
this.animationEvents.set('wave', [
{ time: 0.1, callback: () => this.scene.audioSystem?.playSound('wave') }
]);
}
// Animation management methods
playAnimation(avatar, animationName, options = {}) {
const animation = this.animations.get(animationName);
if (!animation) {
console.warn(`Animation not found: ${animationName}`);
return;
}
if (this.motionBlending.isBlending && options.force !== true) {
// Queue animation for after blend completes
this.motionBlending.queuedAnimation = animationName;
return;
}
if (options.blend !== false) {
this.startBlend(avatar, animationName, options);
} else {
this.motionBlending.currentAnimation = animationName;
this.applyAnimation(avatar, animation, 0);
}
// Trigger animation events
this.triggerAnimationEvents(animationName, 0);
}
startBlend(avatar, nextAnimation, options) {
this.motionBlending.nextAnimation = nextAnimation;
this.motionBlending.blendProgress = 0;
this.motionBlending.isBlending = true;
this.motionBlending.blendTime = options.blendTime || 0.2;
}
applyAnimation(avatar, animation, time) {
animation.tracks.forEach(track => {
const bone = this.findBone(avatar, track.bone);
if (bone) {
const keyframe = this.interpolateKeyframes(track.keyframes, time);
this.applyTransform(bone, track.type, keyframe);
}
});
}
findBone(avatar, boneName) {
// Simplified bone finding - in production, use proper skeleton
const boneMap = {
'spine': avatar.torso,
'head': avatar.head,
'leftArm': avatar.leftArm,
'rightArm': avatar.rightArm,
'leftLeg': avatar.leftLeg,
'rightLeg': avatar.rightLeg
};
return boneMap[boneName];
}
interpolateKeyframes(keyframes, time) {
// Find surrounding keyframes
let prevKeyframe = keyframes[0];
let nextKeyframe = keyframes[keyframes.length - 1];
for (let i = 0; i < keyframes.length - 1; i++) {
if (time >= keyframes[i].time && time <= keyframes[i + 1].time) {
prevKeyframe = keyframes[i];
nextKeyframe = keyframes[i + 1];
break;
}
}
// Linear interpolation
const t = (time - prevKeyframe.time) / (nextKeyframe.time - prevKeyframe.time);
return this.lerpVector(prevKeyframe.value, nextKeyframe.value, t);
}
lerpVector(a, b, t) {
return new THREE.Vector3(
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t,
a.z + (b.z - a.z) * t
);
}
applyTransform(bone, type, value) {
switch(type) {
case 'rotation':
bone.rotation.set(value.x, value.y, value.z);
break;
case 'position':
bone.position.set(value.x, value.y, value.z);
break;
case 'scale':
bone.scale.set(value.x, value.y, value.z);
break;
}
}
update(deltaTime) {
this.updateAnimations(deltaTime);
this.updateBlending(deltaTime);
this.updateProceduralAnimations(deltaTime);
this.updatePhysics(deltaTime);
this.updateFacialAnimations(deltaTime);
}
updateAnimations(deltaTime) {
this.animationMixers.forEach((mixer, avatar) => {
mixer.update(deltaTime);
});
}
updateBlending(deltaTime) {
if (this.motionBlending.isBlending) {
this.motionBlending.blendProgress += deltaTime / this.motionBlending.blendTime;
if (this.motionBlending.blendProgress >= 1) {
this.motionBlending.currentAnimation = this.motionBlending.nextAnimation;
this.motionBlending.nextAnimation = null;
this.motionBlending.isBlending = false;
this.motionBlending.blendProgress = 0;
// Play queued animation if any
if (this.motionBlending.queuedAnimation) {
this.playAnimation(this.scene.currentUser, this.motionBlending.queuedAnimation);
this.motionBlending.queuedAnimation = null;
}
}
}
}
updateProceduralAnimations(deltaTime) {
// Update breathing animation
this.proceduralAnimations.breathing.phase += deltaTime * this.proceduralAnimations.breathing.frequency;
// Apply breathing to all avatars
this.scene.avatars.forEach(avatar => {
if (avatar.torso) {
const breathOffset = Math.sin(this.proceduralAnimations.breathing.phase) *
this.proceduralAnimations.breathing.amplitude;
avatar.torso.position.y += breathOffset;
}
});
// Micro-movements for natural appearance
if (this.proceduralAnimations.microMovements.enabled) {
this.scene.avatars.forEach(avatar => {
this.applyMicroMovements(avatar, deltaTime);
});
}
}
applyMicroMovements(avatar, deltaTime) {
// Small random movements to prevent "robot" appearance
if (avatar.head && Math.random() < 0.1) {
const microMove = (Math.random() - 0.5) * this.proceduralAnimations.microMovements.intensity;
avatar.head.rotation.x += microMove * deltaTime;
avatar.head.rotation.y += microMove * deltaTime;
}
}
updatePhysics(deltaTime) {
// Update cloth simulation
if (this.clothSimulation.points.length > 0) {
this.clothSimulation.update(deltaTime);
}
// Update hair physics
if (this.hairPhysics.strands.length > 0) {
this.hairPhysics.update(deltaTime);
}
}
updateFacialAnimations(deltaTime) {
// Update blend shapes
this.updateBlendShapes(deltaTime);
// Update eye tracking
this.updateEyeTracking(deltaTime);
// Update lip sync
this.updateLipSync(deltaTime);
}
updateBlendShapes(deltaTime) {
this.blendShapes.forEach((shape, name) => {
// Smoothly interpolate to target intensity
shape.intensity += (shape.target - shape.intensity) * deltaTime * 5;
});
}
updateEyeTracking(deltaTime) {
if (!this.eyeTracking) return;
// Random blinking
if (Math.random() < 0.01) {
this.eyeTracking.combinedBlink = 1;
}
// Smooth blink animation
this.eyeTracking.combinedBlink = Math.max(0, this.eyeTracking.combinedBlink - deltaTime * 3);
// Apply to avatars
this.scene.avatars.forEach(avatar => {
this.applyEyeBlink(avatar, this.eyeTracking.combinedBlink);
});
}
applyEyeBlink(avatar, blinkAmount) {
if (avatar.leftEye && avatar.rightEye) {
const blinkScale = 1 - blinkAmount * 0.8;
avatar.leftEye.scale.y = blinkScale;
avatar.rightEye.scale.y = blinkScale;
}
}
updateLipSync(deltaTime) {
if (!this.lipSync) return;
// Update viseme intensities
this.lipSync.visemes.forEach((viseme, name) => {
viseme.intensity += (viseme.target - viseme.intensity) * deltaTime * 8;
});
}
triggerAnimationEvents(animationName, time) {
const events = this.animationEvents.get(animationName);
if (events) {
events.forEach(event => {
if (Math.abs(time - event.time) < 0.01) {
event.callback();
}
});
}
}
// Enhanced animation methods for specific use cases
createEmotionalAnimation(emotion, intensity) {
const emotionalAnimations = {
'happy': this.createHappyAnimation(intensity),
'sad': this.createSadAnimation(intensity),
'angry': this.createAngryAnimation(intensity),
'surprised': this.createSurprisedAnimation(intensity)
};
return emotionalAnimations[emotion] || this.createIdleAnimation();
}
createHappyAnimation(intensity) {
return {
duration: 1.5,
tracks: [
{
type: 'rotation',
bone: 'head',
keyframes: [
{ time: 0, value: new THREE.Vector3(0, 0, 0) },
{ time: 0.5, value: new THREE.Vector3(0.1 * intensity, 0, 0) },
{ time: 1, value: new THREE.Vector3(-0.1 * intensity, 0, 0) },
{ time: 1.5, value: new THREE.Vector3(0, 0, 0) }
]
}
],
loop: true
};
}
// Integration with other systems
setupAnimationIntegrations() {
this.integrateWithEmoteSystem();
this.integrateWithVoiceSystem();
this.integrateWithRelationshipSystem();
}
integrateWithEmoteSystem() {
const originalPlayEmote = this.scene.emoteSystem.playEmote.bind(this.scene.emoteSystem);
this.scene.emoteSystem.playEmote = (emoteKey, avatar) => {
originalPlayEmote(emoteKey, avatar);
// Play corresponding animation
if (this.animations.has(emoteKey)) {
this.playAnimation(avatar, emoteKey, { blend: true });
}
};
}
integrateWithVoiceSystem() {
// Lip sync with voice chat
if (this.scene.voiceChat) {
this.setupVoiceDrivenLipSync();
}
}
setupVoiceDrivenLipSync() {
// This would connect to voice analysis for real lip sync
// For now, simulate with random viseme changes
setInterval(() => {
if (Math.random() < 0.3) {
const visemes = Array.from(this.lipSync.visemes.keys());
const randomViseme = visemes[Math.floor(Math.random() * visemes.length)];
this.setViseme(randomViseme, Math.random());
}
}, 200);
}
setViseme(viseme, intensity) {
this.lipSync.visemes.forEach((v, name) => {
v.target = name === viseme ? intensity : 0;
});
}
integrateWithRelationshipSystem() {
// Adjust animations based on relationship status
// More intimate animations for closer relationships
}
// Utility methods
createAnimationClip(name, tracks, duration, loop = true) {
return {
name,
tracks,
duration,
loop
};
}
addAnimation(avatar, clip) {
if (!this.animationMixers.has(avatar)) {
this.animationMixers.set(avatar, new THREE.AnimationMixer(avatar.mesh));
}
const mixer = this.animationMixers.get(avatar);
const clipAction = mixer.clipAction(clip);
clipAction.play();
return clipAction;
}
// Cleanup
cleanup() {
this.animationMixers.forEach(mixer => {
mixer.stopAllAction();
});
this.animationMixers.clear();
}
}
Step 4: Updated DatingScene Class - Sensory Integration 🎵♿💫
// Updated DatingScene class for Part 8
class DatingScene {
constructor() {
// ... existing properties ...
// New properties for Part 8
this.audioSystem = null;
this.accessibilitySystem = null;
this.animationSystem = null;
this.init();
}
init() {
this.createScene();
this.createCamera();
this.createRenderer();
this.createLights();
this.createEnvironment();
// Initialize all systems
this.proximityChat = new ProximityChat(this);
this.voiceChat = new VoiceChatSystem(this);
this.emoteSystem = new EmoteSystem(this);
this.interactiveEnv = new InteractiveEnvironment(this);
this.dayNightCycle = new DayNightCycle(this);
this.weatherSystem = new WeatherSystem(this, this.dayNightCycle);
this.miniGameSystem = new MiniGameSystem(this);
this.relationshipSystem = new RelationshipSystem(this);
this.performanceOptimizer = new PerformanceOptimizer(this);
this.mobileOptimizer = new MobileOptimizer(this);
this.socialSystem = new SocialSystem(this);
this.customizationSystem = new CustomizationSystem(this);
// Initialize new Part 8 systems
this.audioSystem = new AudioSystem(this);
this.accessibilitySystem = new AccessibilitySystem(this);
this.animationSystem = new AdvancedAnimationSystem(this);
this.setupEnhancedIntegrations();
this.setupSensoryUI();
this.setupKeyboardListeners();
this.animate();
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';
document.getElementById('chatUI').style.display = 'block';
this.addSampleAvatars();
this.startAmbientExperience();
}, 2000);
}
setupEnhancedIntegrations() {
this.setupAudioIntegrations();
this.setupAccessibilityIntegrations();
this.setupAnimationIntegrations();
}
setupAudioIntegrations() {
// Connect audio system with other systems
if (this.audioSystem) {
this.audioSystem.setupAudioIntegrations();
}
}
setupAccessibilityIntegrations() {
// Connect accessibility with other systems
if (this.accessibilitySystem) {
// Accessibility is already integrated through its constructor
}
}
setupAnimationIntegrations() {
// Connect animation system with other systems
if (this.animationSystem) {
this.animationSystem.setupAnimationIntegrations();
}
}
setupSensoryUI() {
this.createSensoryControls();
this.setupAudioVisualFeedback();
}
createSensoryControls() {
const sensoryControls = document.createElement('div');
sensoryControls.style.cssText = `
position: fixed;
top: 100px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 15px;
z-index: 100;
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
gap: 10px;
`;
sensoryControls.innerHTML = `
<button onclick="datingScene.toggleAudioPanel()" style="
padding: 10px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
">🎵 Audio</button>
<button onclick="datingScene.toggleAccessibilityPanel()" style="
padding: 10px;
background: #9370db;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
">♿ Accessibility</button>
<button onclick="datingScene.toggleAnimationDemo()" style="
padding: 10px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
">💫 Animations</button>
`;
document.getElementById('container').appendChild(sensoryControls);
}
toggleAudioPanel() {
if (this.audioSystem.volumeMixer.style.display === 'block') {
this.audioSystem.volumeMixer.style.display = 'none';
} else {
this.audioSystem.volumeMixer.style.display = 'block';
}
}
toggleAccessibilityPanel() {
this.accessibilitySystem.showMenu();
}
toggleAnimationDemo() {
if (this.currentUser) {
// Cycle through different animations
const animations = ['wave', 'dance', 'laugh', 'shy'];
const randomAnimation = animations[Math.floor(Math.random() * animations.length)];
this.animationSystem.playAnimation(this.currentUser, randomAnimation, { blend: true });
}
}
setupAudioVisualFeedback() {
// Visual feedback for audio events
if (this.audioSystem && this.accessibilitySystem.features.visualAlerts) {
this.audioSystem.setupAudioVisualizer();
}
}
startAmbientExperience() {
// Start ambient sounds and animations
if (this.audioSystem) {
this.audioSystem.startEnvironmentSounds();
}
// Start idle animations for NPCs
this.scene.avatars.forEach(avatar => {
if (avatar !== this.currentUser) {
this.animationSystem.playAnimation(avatar, 'idle');
}
});
}
createUserAvatar(config) {
this.currentUser = new Avatar(config);
this.currentUser.mesh.position.set(0, 0, 3);
this.scene.add(this.currentUser.mesh);
this.avatars.push(this.currentUser);
// Initialize movement system
this.currentUser.movement = new AvatarMovement(this.currentUser, this);
// Set up camera
this.datingCamera = new DatingCamera(this.camera, this, this.currentUser);
// Apply saved customizations
this.applySavedCustomizations();
// Start user animations
this.animationSystem.playAnimation(this.currentUser, 'idle');
console.log(`Welcome, ${config.name}! Ready to mingle! 💖`);
document.getElementById('avatarCustomization').style.display = 'none';
// Enhanced NPC greetings with audio and animations
this.avatars.forEach(avatar => {
if (avatar !== this.currentUser && avatar.ai) {
this.greetNewUser(avatar);
}
});
}
greetNewUser(avatar) {
const userPosition = this.currentUser.mesh.position.clone();
avatar.lookAt(userPosition);
setTimeout(() => {
// Play wave animation with sound
this.animationSystem.playAnimation(avatar, 'wave', { blend: true });
this.audioSystem.playSound('wave');
// Some NPCs might approach or send friend requests
if (Math.random() < 0.4) {
setTimeout(() => {
this.simulateEnhancedSocialInteraction(avatar);
}, 3000);
}
}, 1000);
}
simulateEnhancedSocialInteraction(avatar) {
// 30% chance to send friend request with sound
if (Math.random() < 0.3) {
this.audioSystem.playSound('notification');
this.accessibilitySystem.showSubtitle(`${avatar.options.name} sent you a friend request!`);
this.socialSystem.showSocialNotification(
`${avatar.options.name} sent you a friend request!`,
'info'
);
// Auto-accept after 5 seconds (for demo)
setTimeout(() => {
this.socialSystem.addFriend({
id: avatar.options.name.toLowerCase(),
name: avatar.options.name,
online: true,
inWorld: true,
friendshipLevel: 1,
lastSeen: Date.now()
});
this.audioSystem.playSound('success');
}, 5000);
}
// 40% chance to start conversation with animation
if (Math.random() < 0.4) {
this.animationSystem.playAnimation(avatar, 'walk', { blend: true });
setTimeout(() => {
avatar.ai.approachAvatar(this.currentUser);
}, 2000);
}
}
animate() {
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000;
this.lastTime = currentTime;
requestAnimationFrame(() => this.animate());
// Update all systems
this.performanceOptimizer.updatePerformanceStats();
if (this.datingCamera) this.datingCamera.update();
if (this.proximityChat) this.proximityChat.update();
if (this.interactiveEnv) this.interactiveEnv.update();
if (this.dayNightCycle) this.dayNightCycle.update(deltaTime);
if (this.weatherSystem) this.weatherSystem.update(deltaTime);
if (this.mobileOptimizer) this.mobileOptimizer.update(deltaTime);
if (this.animationSystem) this.animationSystem.update(deltaTime);
// Update NPC AI
this.avatars.forEach(avatar => {
if (avatar.ai) {
avatar.ai.update(deltaTime);
}
});
// Update mini-games
this.miniGameSystem.activeGames.forEach(game => {
if (game.update) game.update(deltaTime);
});
this.renderer.render(this.scene, this.camera);
}
}
// Global variables for new systems
let datingScene;
let datingCamera;
let emoteSystem;
let dayNightCycle;
let weatherSystem;
let miniGameSystem;
let relationshipSystem;
let mobileOptimizer;
let socialSystem;
let customizationSystem;
let audioSystem;
let accessibilitySystem;
let animationSystem;
window.addEventListener('DOMContentLoaded', () => {
datingScene = new DatingScene();
emoteSystem = datingScene.emoteSystem;
dayNightCycle = datingScene.dayNightCycle;
weatherSystem = datingScene.weatherSystem;
miniGameSystem = datingScene.miniGameSystem;
relationshipSystem = datingScene.relationshipSystem;
mobileOptimizer = datingScene.mobileOptimizer;
socialSystem = datingScene.socialSystem;
customizationSystem = datingScene.customizationSystem;
audioSystem = datingScene.audioSystem;
accessibilitySystem = datingScene.accessibilitySystem;
animationSystem = datingScene.animationSystem;
console.log("Complete Sensory Dating Experience loaded! Audio, accessibility, and advanced animations activated! 🎵♿💫");
setupAvatarInteraction();
document.addEventListener('click', () => {
document.getElementById('messageInput').focus();
});
});
What We've Built in Part 8: 🎉
-
Comprehensive Audio System:
- Spatial audio with Web Audio API
- Background music and ambient sounds
- Sound effects for all interactions
- Volume mixing and audio controls
- Audio visualization
-
Accessibility Features:
- Screen reader support with ARIA labels
- High contrast and color blindness modes
- Reduced motion options
- Keyboard navigation
- Voice control integration
- Visual alerts for sounds
- Subtitle support
-
Advanced Animation System:
- Inverse kinematics for natural movement
- Facial rigging and blend shapes
- Physics-based animations
- Procedural animations (breathing, micro-movements)
- Motion blending between animations
- Emotional and social animations
- Real-time lip sync
-
Integrated Sensory Experience:
- Audio-visual synchronization
- Accessibility-aware interactions
- Enhanced social animations
- Cross-system integrations
Key Features Explained: 🔑
- Web Audio API: Spatial audio, filters, and real-time audio processing
- ARIA Compliance: Full screen reader support and keyboard navigation
- Inverse Kinematics: Natural limb movement and object interaction
- Procedural Animation: Lifelike idle movements and reactions
- Universal Design: Accessibility features that benefit all users
Next Time in Part 9: 🚀
We'll add:
- Data Persistence: Cloud saving, cross-device sync, and backup systems
- Security & Privacy: User safety features, moderation tools, and privacy controls
- Advanced AI: Machine learning for better matchmaking and NPC behavior
- Analytics & Insights: User behavior tracking and relationship analytics
- Deployment & Scaling: Server infrastructure and performance optimization
Current Project Status: Our 3D dating app is now a fully accessible, multi-sensory experience! With immersive audio, comprehensive accessibility, and lifelike animations, we've created a virtual world that's engaging and inclusive for everyone. Love should know no barriers! 💕
Fun Fact: Our accessibility features are so comprehensive that even if you're blindfolded, wearing noise-canceling headphones, and navigating with one hand tied behind your back, you could still probably find a date! Now THAT's accessible love! 😄
Ready for Part 9 where we'll make sure your virtual love stories are safely saved and protected?
Part 9: Data Persistence, Security & Advanced AI - Love That Lasts! 💾🔒🧠
Welcome back, digital love architect! Our dating world is now a sensory masterpiece, but it's about as permanent as a sandcastle at high tide. Time to add cloud saving, robust security, and AI that actually understands romance!
Step 1: Data Persistence System - Save Your Love Story! 💾
Let's create a comprehensive data management system with cloud saving, local caching, and cross-device synchronization:
// data-persistence.js - Because love stories are worth saving! 📚
class DataPersistenceSystem {
constructor(scene) {
this.scene = scene;
this.userData = new Map();
this.cloudEnabled = false;
this.syncInterval = null;
this.lastSync = null;
this.conflictResolution = 'server'; // server, client, manual
this.setupDataStructures();
this.setupStorage();
this.setupSyncSystem();
this.setupDataUI();
console.log("Data persistence system initialized! Your love is now eternal! 💾");
}
setupDataStructures() {
// Define data schemas for different types of information
this.schemas = {
userProfile: {
version: '1.0',
fields: ['name', 'avatarData', 'preferences', 'relationships', 'achievements']
},
relationships: {
version: '1.0',
fields: ['friends', 'romanticPartners', 'interactions', 'compatibilityScores']
},
environment: {
version: '1.0',
fields: ['theme', 'customizations', 'favoriteSpots', 'visitedLocations']
},
progress: {
version: '1.0',
fields: ['level', 'experience', 'unlockedFeatures', 'completedEvents']
}
};
// Initialize data containers
this.userData.set('profile', this.createDefaultProfile());
this.userData.set('relationships', new Map());
this.userData.set('environment', new Map());
this.userData.set('progress', this.createDefaultProgress());
this.userData.set('settings', new Map());
}
createDefaultProfile() {
return {
name: 'New User',
avatarData: {},
preferences: {
musicVolume: 0.8,
sfxVolume: 0.7,
theme: 'default',
language: 'en',
privacy: 'public'
},
relationships: {},
achievements: [],
joinDate: new Date().toISOString(),
lastLogin: new Date().toISOString()
};
}
createDefaultProgress() {
return {
level: 1,
experience: 0,
unlockedFeatures: ['basic_chat', 'emotes', 'movement'],
completedEvents: [],
playTime: 0,
totalConnections: 0
};
}
setupStorage() {
this.storageAdapters = {
local: new LocalStorageAdapter(),
cloud: new CloudStorageAdapter(),
cache: new CacheStorageAdapter()
};
this.setupDataValidation();
this.setupBackupSystem();
}
setupDataValidation() {
this.validators = new Map();
// Add validators for each data type
this.validators.set('userProfile', (data) => this.validateUserProfile(data));
this.validators.set('relationships', (data) => this.validateRelationships(data));
this.validators.set('environment', (data) => this.validateEnvironment(data));
this.validators.set('progress', (data) => this.validateProgress(data));
}
validateUserProfile(profile) {
const required = ['name', 'preferences', 'joinDate'];
const missing = required.filter(field => !profile[field]);
if (missing.length > 0) {
throw new Error(`Missing required profile fields: ${missing.join(', ')}`);
}
if (profile.name.length < 2 || profile.name.length > 50) {
throw new Error('Name must be between 2 and 50 characters');
}
return true;
}
setupSyncSystem() {
this.syncManager = {
pendingChanges: new Map(),
syncQueue: [],
isSyncing: false,
retryCount: 0,
maxRetries: 3
};
this.setupAutoSave();
this.setupConflictDetection();
}
setupAutoSave() {
// Auto-save every 2 minutes
setInterval(() => {
this.autoSave();
}, 120000);
// Also save when user leaves the page
window.addEventListener('beforeunload', () => {
this.quickSave();
});
// Save after important events
this.setupEventBasedSaving();
}
setupEventBasedSaving() {
const saveEvents = [
'relationshipChanged',
'avatarCustomized',
'preferenceUpdated',
'achievementUnlocked',
'levelUp'
];
saveEvents.forEach(event => {
this.scene.emitter?.on(event, () => {
this.queueSave(event);
});
});
}
queueSave(context) {
this.syncManager.pendingChanges.set(context, Date.now());
// Debounced save - wait 5 seconds before actually saving
clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => {
this.saveUserData();
}, 5000);
}
async saveUserData() {
try {
const saveData = this.prepareSaveData();
// Validate data before saving
if (!this.validateAllData(saveData)) {
throw new Error('Data validation failed');
}
// Save to local storage first (always available)
await this.storageAdapters.local.save('userData', saveData);
// Then try cloud save if enabled
if (this.cloudEnabled) {
await this.cloudSave(saveData);
}
// Update cache
await this.storageAdapters.cache.save('userData', saveData);
this.lastSync = new Date();
this.syncManager.pendingChanges.clear();
this.showSaveNotification('Data saved successfully! 💾');
} catch (error) {
console.error('Save failed:', error);
this.handleSaveError(error);
}
}
prepareSaveData() {
const saveData = {
metadata: {
version: '1.0',
timestamp: new Date().toISOString(),
userId: this.getUserId(),
checksum: this.generateChecksum()
},
profile: this.userData.get('profile'),
relationships: Object.fromEntries(this.userData.get('relationships')),
environment: Object.fromEntries(this.userData.get('environment')),
progress: this.userData.get('progress'),
settings: Object.fromEntries(this.userData.get('settings'))
};
// Compress large data
return this.compressData(saveData);
}
compressData(data) {
// Simple compression for demo - in production, use proper compression
return {
...data,
compressed: true,
size: JSON.stringify(data).length
};
}
async cloudSave(saveData) {
if (!this.cloudEnabled) return;
try {
const response = await fetch('/api/user/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getAuthToken()}`
},
body: JSON.stringify(saveData)
});
if (!response.ok) {
throw new Error(`Cloud save failed: ${response.statusText}`);
}
const result = await response.json();
return result;
} catch (error) {
console.warn('Cloud save failed, will retry later:', error);
this.queueCloudRetry(saveData);
}
}
queueCloudRetry(saveData) {
this.syncManager.syncQueue.push({
data: saveData,
timestamp: Date.now(),
retries: 0
});
this.processSyncQueue();
}
async processSyncQueue() {
if (this.syncManager.isSyncing || this.syncManager.syncQueue.length === 0) {
return;
}
this.syncManager.isSyncing = true;
while (this.syncManager.syncQueue.length > 0) {
const item = this.syncManager.syncQueue[0];
try {
await this.cloudSave(item.data);
this.syncManager.syncQueue.shift(); // Remove successful item
} catch (error) {
item.retries++;
if (item.retries >= this.syncManager.maxRetries) {
// Give up and remove from queue
this.syncManager.syncQueue.shift();
this.handleSyncFailure(item, error);
} else {
// Wait before retry (exponential backoff)
const delay = Math.pow(2, item.retries) * 1000;
setTimeout(() => this.processSyncQueue(), delay);
break;
}
}
}
this.syncManager.isSyncing = false;
}
async loadUserData() {
try {
// Try cache first (fastest)
let data = await this.storageAdapters.cache.load('userData');
if (!data) {
// Try local storage
data = await this.storageAdapters.local.load('userData');
}
if (!data && this.cloudEnabled) {
// Finally try cloud
data = await this.cloudLoad();
}
if (data) {
await this.applyLoadedData(data);
this.showSaveNotification('Data loaded successfully! 📂');
} else {
// First time user - create default data
this.initializeNewUser();
}
} catch (error) {
console.error('Load failed:', error);
this.handleLoadError(error);
}
}
async cloudLoad() {
try {
const response = await fetch('/api/user/load', {
headers: {
'Authorization': `Bearer ${this.getAuthToken()}`
}
});
if (!response.ok) {
throw new Error(`Cloud load failed: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.warn('Cloud load failed:', error);
return null;
}
}
async applyLoadedData(data) {
// Validate data integrity
if (!this.validateLoadedData(data)) {
throw new Error('Loaded data failed validation');
}
// Check for data conflicts
await this.resolveDataConflicts(data);
// Apply data to system
if (data.profile) {
this.userData.set('profile', data.profile);
this.applyUserProfile(data.profile);
}
if (data.relationships) {
this.userData.set('relationships', new Map(Object.entries(data.relationships)));
this.applyRelationshipData(data.relationships);
}
if (data.environment) {
this.userData.set('environment', new Map(Object.entries(data.environment)));
this.applyEnvironmentData(data.environment);
}
if (data.progress) {
this.userData.set('progress', data.progress);
this.applyProgressData(data.progress);
}
if (data.settings) {
this.userData.set('settings', new Map(Object.entries(data.settings)));
this.applySettingsData(data.settings);
}
this.lastSync = new Date();
}
applyUserProfile(profile) {
if (this.scene.currentUser && profile.avatarData) {
this.scene.customizationSystem.applyAvatarCustomizations(
this.scene.currentUser,
profile.avatarData
);
}
if (profile.preferences) {
this.applyUserPreferences(profile.preferences);
}
}
applyUserPreferences(preferences) {
// Apply audio settings
if (this.scene.audioSystem && preferences.musicVolume !== undefined) {
this.scene.audioSystem.setVolume('music', preferences.musicVolume);
}
// Apply theme
if (this.scene.customizationSystem && preferences.theme) {
this.scene.customizationSystem.applyEnvironmentTheme(preferences.theme);
}
// Apply accessibility settings
if (this.scene.accessibilitySystem && preferences.accessibility) {
Object.assign(this.scene.accessibilitySystem.features, preferences.accessibility);
this.scene.accessibilitySystem.applyAccessibilityFeatures();
}
}
setupConflictDetection() {
this.conflictResolver = {
detect: (localData, remoteData) => {
const conflicts = [];
// Check timestamp-based conflicts
if (new Date(localData.metadata.timestamp) > new Date(remoteData.metadata.timestamp)) {
conflicts.push({
type: 'timestamp',
local: localData.metadata.timestamp,
remote: remoteData.metadata.timestamp,
resolution: 'client'
});
}
// Check checksum conflicts
if (localData.metadata.checksum !== remoteData.metadata.checksum) {
conflicts.push({
type: 'checksum',
field: 'all',
resolution: 'manual'
});
}
return conflicts;
},
resolve: (conflicts, localData, remoteData) => {
switch (this.conflictResolution) {
case 'server':
return remoteData;
case 'client':
return localData;
case 'manual':
return this.manualConflictResolution(conflicts, localData, remoteData);
default:
return localData;
}
}
};
}
manualConflictResolution(conflicts, localData, remoteData) {
// Show conflict resolution UI to user
this.showConflictResolutionUI(conflicts, localData, remoteData);
// For now, return local data as fallback
return localData;
}
setupBackupSystem() {
this.backupManager = {
backups: new Map(),
maxBackups: 10,
autoBackup: true,
createBackup: async (data, label = 'auto') => {
const backup = {
data: JSON.parse(JSON.stringify(data)), // Deep clone
timestamp: new Date().toISOString(),
label: label,
version: '1.0'
};
this.backupManager.backups.set(backup.timestamp, backup);
// Limit number of backups
if (this.backupManager.backups.size > this.backupManager.maxBackups) {
const oldest = Array.from(this.backupManager.backups.keys()).sort()[0];
this.backupManager.backups.delete(oldest);
}
// Save backups to storage
await this.storageAdapters.local.save('backups',
Array.from(this.backupManager.backups.values()));
},
restoreBackup: async (timestamp) => {
const backup = this.backupManager.backups.get(timestamp);
if (backup) {
await this.applyLoadedData(backup.data);
this.showSaveNotification('Backup restored! 🔄');
}
}
};
// Load existing backups
this.loadBackups();
}
async loadBackups() {
try {
const backups = await this.storageAdapters.local.load('backups') || [];
backups.forEach(backup => {
this.backupManager.backups.set(backup.timestamp, backup);
});
} catch (error) {
console.warn('Failed to load backups:', error);
}
}
setupDataUI() {
this.createDataManagementUI();
this.createSyncStatusIndicator();
}
createDataManagementUI() {
this.dataManagementUI = document.createElement('div');
this.dataManagementUI.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 20px;
z-index: 1000;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
display: none;
`;
this.dataManagementUI.innerHTML = `
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: #ff6b6b;">💾 Data Management</h3>
<button onclick="dataPersistence.hideDataUI()" style="
background: none;
border: none;
font-size: 20px;
cursor: pointer;
">✕</button>
</div>
<div style="display: grid; gap: 20px;">
<!-- Sync Status -->
<div>
<h4 style="margin: 0 0 10px 0;">🔄 Sync Status</h4>
<div id="syncStatus" style="
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
font-size: 14px;
">
Loading...
</div>
</div>
<!-- Storage Info -->
<div>
<h4 style="margin: 0 0 10px 0;">💽 Storage</h4>
<div id="storageInfo" style="
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
font-size: 14px;
">
Calculating...
</div>
</div>
<!-- Backup Management -->
<div>
<h4 style="margin: 0 0 10px 0;">📦 Backups</h4>
<div style="display: grid; gap: 10px;">
<button onclick="dataPersistence.createManualBackup()" style="
padding: 10px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">Create Backup</button>
<div id="backupList" style="
max-height: 150px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 10px;
"></div>
</div>
</div>
<!-- Data Actions -->
<div>
<h4 style="margin: 0 0 10px 0;">⚡ Actions</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<button onclick="dataPersistence.forceSave()" style="
padding: 10px;
background: #ffd166;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">Save Now</button>
<button onclick="dataPersistence.exportData()" style="
padding: 10px;
background: #9370db;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">Export Data</button>
<button onclick="dataPersistence.showImportDialog()" style="
padding: 10px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">Import Data</button>
<button onclick="dataPersistence.showResetDialog()" style="
padding: 10px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">Reset Data</button>
</div>
</div>
</div>
`;
document.getElementById('container').appendChild(this.dataManagementUI);
}
createSyncStatusIndicator() {
this.syncIndicator = document.createElement('div');
this.syncIndicator.style.cssText = `
position: fixed;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 8px 12px;
border-radius: 15px;
font-size: 12px;
z-index: 100;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
`;
this.syncIndicator.innerHTML = `
<span id="syncIcon">🔄</span>
<span id="syncText">Synced</span>
`;
this.syncIndicator.addEventListener('click', () => {
this.showDataUI();
});
document.getElementById('container').appendChild(this.syncIndicator);
this.updateSyncStatus();
}
updateSyncStatus() {
const icon = this.syncIndicator.querySelector('#syncIcon');
const text = this.syncIndicator.querySelector('#syncText');
if (this.syncManager.isSyncing) {
icon.textContent = '⏳';
text.textContent = 'Syncing...';
this.syncIndicator.style.background = 'rgba(255, 214, 102, 0.9)';
} else if (this.syncManager.pendingChanges.size > 0) {
icon.textContent = '💾';
text.textContent = `${this.syncManager.pendingChanges.size} pending`;
this.syncIndicator.style.background = 'rgba(255, 107, 107, 0.9)';
} else {
icon.textContent = '✅';
text.textContent = this.lastSync ? 'Synced' : 'Ready';
this.syncIndicator.style.background = 'rgba(78, 205, 196, 0.9)';
}
}
showDataUI() {
this.updateDataUI();
this.dataManagementUI.style.display = 'block';
}
hideDataUI() {
this.dataManagementUI.style.display = 'none';
}
updateDataUI() {
this.updateSyncStatusDisplay();
this.updateStorageInfo();
this.updateBackupList();
}
updateSyncStatusDisplay() {
const syncStatus = document.getElementById('syncStatus');
if (!syncStatus) return;
let statusHTML = '';
if (this.lastSync) {
statusHTML += `<div>Last Sync: ${new Date(this.lastSync).toLocaleString()}</div>`;
}
if (this.syncManager.pendingChanges.size > 0) {
statusHTML += `<div style="color: #ff6b6b;">Pending: ${this.syncManager.pendingChanges.size} changes</div>`;
}
if (this.syncManager.syncQueue.length > 0) {
statusHTML += `<div style="color: #ffd166;">Queued: ${this.syncManager.syncQueue.length} items</div>`;
}
statusHTML += `<div>Cloud: ${this.cloudEnabled ? '✅ Enabled' : '❌ Disabled'}</div>`;
syncStatus.innerHTML = statusHTML;
}
updateStorageInfo() {
const storageInfo = document.getElementById('storageInfo');
if (!storageInfo) return;
// Calculate approximate storage usage
const dataSize = this.calculateDataSize();
const storageLimit = 5 * 1024 * 1024; // 5MB limit for demo
const usagePercent = (dataSize / storageLimit) * 100;
const usageColor = usagePercent > 90 ? '#ff6b6b' : usagePercent > 70 ? '#ffd166' : '#4ecdc4';
storageInfo.innerHTML = `
<div>Used: ${this.formatBytes(dataSize)}</div>
<div>Limit: ${this.formatBytes(storageLimit)}</div>
<div style="margin-top: 5px;">
<div style="width: 100%; background: #e9ecef; border-radius: 10px; overflow: hidden;">
<div style="width: ${usagePercent}%; background: ${usageColor}; height: 8px;"></div>
</div>
</div>
`;
}
calculateDataSize() {
const data = this.prepareSaveData();
return new Blob([JSON.stringify(data)]).size;
}
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
updateBackupList() {
const backupList = document.getElementById('backupList');
if (!backupList) return;
if (this.backupManager.backups.size === 0) {
backupList.innerHTML = '<div style="color: #666; text-align: center;">No backups yet</div>';
return;
}
const backups = Array.from(this.backupManager.backups.values())
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
.slice(0, 5); // Show only 5 most recent
backupList.innerHTML = backups.map(backup => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 5px 0; border-bottom: 1px solid #eee;">
<div>
<div style="font-size: 12px; font-weight: bold;">${backup.label}</div>
<div style="font-size: 10px; color: #666;">${new Date(backup.timestamp).toLocaleString()}</div>
</div>
<button onclick="dataPersistence.restoreBackup('${backup.timestamp}')" style="
background: #4ecdc4;
color: white;
border: none;
border-radius: 5px;
padding: 3px 8px;
font-size: 10px;
cursor: pointer;
">Restore</button>
</div>
`).join('');
}
// Public API methods
async saveSetting(key, value) {
this.userData.get('settings').set(key, value);
await this.queueSave('settingUpdated');
}
async getSetting(key, defaultValue = null) {
return this.userData.get('settings').get(key) || defaultValue;
}
async updateProgress(updates) {
Object.assign(this.userData.get('progress'), updates);
await this.queueSave('progressUpdated');
}
async addRelationship(relationship) {
const relationships = this.userData.get('relationships');
relationships.set(relationship.id, relationship);
await this.queueSave('relationshipAdded');
}
// Utility methods
getUserId() {
// In production, this would come from authentication
return localStorage.getItem('userId') || 'anonymous_' + Math.random().toString(36).substr(2, 9);
}
getAuthToken() {
// In production, this would come from secure storage
return localStorage.getItem('authToken');
}
generateChecksum(data) {
// Simple checksum for demo - in production, use proper hash
const str = JSON.stringify(data);
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash.toString(36);
}
showSaveNotification(message) {
if (this.scene.socialSystem) {
this.scene.socialSystem.showSocialNotification(message, 'info');
}
}
// Error handling
handleSaveError(error) {
console.error('Save error:', error);
this.showSaveNotification('Save failed! Data may be lost. 😢');
// Try to create emergency backup
this.createEmergencyBackup();
}
handleLoadError(error) {
console.error('Load error:', error);
this.showSaveNotification('Load failed! Using default data. 🔄');
// Try to restore from backup
this.tryBackupRestore();
}
handleSyncFailure(item, error) {
console.error('Sync failed after retries:', error);
this.showSaveNotification('Cloud sync failed! Data saved locally. ☁️');
}
createEmergencyBackup() {
try {
const data = this.prepareSaveData();
this.backupManager.createBackup(data, 'emergency');
} catch (error) {
console.error('Emergency backup failed:', error);
}
}
async tryBackupRestore() {
try {
const backups = await this.storageAdapters.local.load('backups');
if (backups && backups.length > 0) {
const latestBackup = backups[backups.length - 1];
await this.applyLoadedData(latestBackup.data);
this.showSaveNotification('Restored from backup! 🔄');
}
} catch (error) {
console.error('Backup restore failed:', error);
}
}
// UI action handlers
async createManualBackup() {
const data = this.prepareSaveData();
await this.backupManager.createBackup(data, 'manual');
this.updateBackupList();
this.showSaveNotification('Manual backup created! 📦');
}
async restoreBackup(timestamp) {
if (confirm('Are you sure you want to restore this backup? Current data will be replaced.')) {
await this.backupManager.restoreBackup(timestamp);
this.hideDataUI();
}
}
async forceSave() {
await this.saveUserData();
}
exportData() {
const data = this.prepareSaveData();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `dating-app-backup-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
this.showSaveNotification('Data exported! 📤');
}
showImportDialog() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = e.target.files[0];
if (file) {
try {
const text = await file.text();
const data = JSON.parse(text);
if (confirm('Import this data? Current data will be replaced.')) {
await this.applyLoadedData(data);
this.showSaveNotification('Data imported successfully! 📥');
}
} catch (error) {
alert('Invalid data file!');
}
}
};
input.click();
}
showResetDialog() {
if (confirm('Are you sure you want to reset all data? This cannot be undone!')) {
this.resetAllData();
}
}
resetAllData() {
this.userData.clear();
this.setupDataStructures();
this.saveUserData();
this.showSaveNotification('Data reset! All progress cleared. 🔄');
this.hideDataUI();
// Reload the scene to apply reset
setTimeout(() => location.reload(), 1000);
}
}
// Storage Adapters
class LocalStorageAdapter {
async save(key, data) {
try {
localStorage.setItem(key, JSON.stringify(data));
return true;
} catch (error) {
if (error.name === 'QuotaExceededError') {
throw new Error('Local storage full! Please free up space.');
}
throw error;
}
}
async load(key) {
try {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (error) {
console.warn('Local storage load failed:', error);
return null;
}
}
async remove(key) {
try {
localStorage.removeItem(key);
return true;
} catch (error) {
console.warn('Local storage remove failed:', error);
return false;
}
}
}
class CloudStorageAdapter {
async save(key, data) {
// Implementation would connect to actual cloud service
// For demo, simulate cloud save
return new Promise((resolve) => {
setTimeout(() => resolve({ success: true }), 500);
});
}
async load(key) {
// Implementation would connect to actual cloud service
// For demo, simulate cloud load
return new Promise((resolve) => {
setTimeout(() => resolve(null), 500); // Return null for demo
});
}
}
class CacheStorageAdapter {
constructor() {
this.cache = new Map();
}
async save(key, data) {
this.cache.set(key, {
data: data,
timestamp: Date.now(),
ttl: 24 * 60 * 60 * 1000 // 24 hours
});
return true;
}
async load(key) {
const item = this.cache.get(key);
if (item && Date.now() - item.timestamp < item.ttl) {
return item.data;
}
return null;
}
async remove(key) {
this.cache.delete(key);
return true;
}
}
Step 2: Security & Privacy System - Safe Love is Good Love! 🔒
Let's create a comprehensive security and privacy system to protect users:
// security-privacy.js - Because your heart deserves protection! 🛡️
class SecurityPrivacySystem {
constructor(scene) {
this.scene = scene;
this.securityLevel = 'standard'; // standard, enhanced, maximum
this.privacySettings = new Map();
this.moderationTools = new Map();
this.encryptionEnabled = true;
this.setupSecurityFramework();
this.setupPrivacyControls();
this.setupModerationSystem();
this.setupSecurityUI();
console.log("Security & privacy system initialized! Your love is now protected! 🛡️");
}
setupSecurityFramework() {
this.security = {
authentication: new AuthenticationManager(),
encryption: new EncryptionManager(),
monitoring: new SecurityMonitor(),
incidentResponse: new IncidentResponseTeam()
};
this.setupDataProtection();
this.setupAccessControls();
this.setupThreatDetection();
}
setupDataProtection() {
this.dataProtection = {
encryptSensitiveData: true,
dataRetention: {
chats: '30d',
voice: '7d',
location: '1h',
analytics: '1y'
},
anonymization: {
enabled: true,
level: 'medium'
}
};
this.setupGDPRCompliance();
this.setupDataMinimization();
}
setupGDPRCompliance() {
this.gdpr = {
userConsent: new Map(),
dataProcessing: {
purposes: new Map(),
legalBasis: new Map()
},
userRights: {
access: true,
rectification: true,
erasure: true,
restriction: true,
portability: true,
objection: true
}
};
this.setupConsentManagement();
this.setupDataSubjectRequests();
}
setupConsentManagement() {
this.consentManager = {
requiredConsents: [
'essential',
'analytics',
'marketing',
'third_party'
],
grantedConsents: new Set(['essential']),
requestConsent: (type) => {
return this.showConsentDialog(type);
},
hasConsent: (type) => {
return this.grantedConsents.has(type);
},
revokeConsent: (type) => {
this.grantedConsents.delete(type);
this.applyConsentChanges();
}
};
}
setupPrivacyControls() {
this.privacySettings.set('profileVisibility', 'friends');
this.privacySettings.set('locationSharing', 'proximity');
this.privacySettings.set('chatHistory', 'enabled');
this.privacySettings.set('dataCollection', 'minimal');
this.privacySettings.set('thirdPartySharing', 'disabled');
this.setupPrivacyUI();
this.loadPrivacySettings();
}
setupModerationSystem() {
this.moderation = {
automated: new AutomatedModeration(),
manual: new ManualModeration(),
reporting: new ReportingSystem(),
blockList: new BlockListManager()
};
this.setupContentFiltering();
this.setupUserReporting();
this.setupSafetyFeatures();
}
setupSecurityUI() {
this.createSecurityDashboard();
this.createPrivacyControls();
this.createSafetyIndicators();
}
createSecurityDashboard() {
this.securityDashboard = document.createElement('div');
this.securityDashboard.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 20px;
z-index: 1000;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
display: none;
`;
this.securityDashboard.innerHTML = `
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: #ff6b6b;">🛡️ Security & Privacy</h3>
<button onclick="securitySystem.hideDashboard()" style="
background: none;
border: none;
font-size: 20px;
cursor: pointer;
">✕</button>
</div>
<div style="display: grid; gap: 20px;">
<!-- Security Status -->
<div>
<h4 style="margin: 0 0 10px 0;">🔐 Security Status</h4>
<div id="securityStatus" style="
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
">
<div>Encryption: <span id="encryptionStatus">✅ Enabled</span></div>
<div>2FA: <span id="2faStatus">❌ Disabled</span></div>
<div>Last Login: <span id="lastLogin">Unknown</span></div>
<div>Active Sessions: <span id="activeSessions">1</span></div>
</div>
</div>
<!-- Privacy Settings -->
<div>
<h4 style="margin: 0 0 10px 0;">👁️ Privacy Settings</h4>
<div style="display: grid; gap: 10px;">
${this.generatePrivacyControls()}
</div>
</div>
<!-- Safety Tools -->
<div>
<h4 style="margin: 0 0 10px 0;">🛟 Safety Tools</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<button onclick="securitySystem.showBlockList()" style="
padding: 10px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">🚫 Block List</button>
<button onclick="securitySystem.showReportHistory()" style="
padding: 10px;
background: #ffd166;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">📋 Reports</button>
<button onclick="securitySystem.emergencyExit()" style="
padding: 10px;
background: #dc3545;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">🚨 Emergency</button>
<button onclick="securitySystem.exportData()" style="
padding: 10px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
">📤 Export Data</button>
</div>
</div>
<!-- Advanced Security -->
<div>
<h4 style="margin: 0 0 10px 0;">⚙️ Advanced Security</h4>
<div style="display: grid; gap: 10px;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="enhancedEncryption" ${this.encryptionEnabled ? 'checked' : ''}>
<span>Enhanced Encryption</span>
</label>
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="sessionTimeout">
<span>Auto Logout (15min)</span>
</label>
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="loginAlerts">
<span>Login Notifications</span>
</label>
</div>
</div>
</div>
<div style="margin-top: 20px; display: flex; justify-content: space-between;">
<button onclick="securitySystem.applySecuritySettings()" style="
padding: 12px 20px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Apply Settings</button>
<button onclick="securitySystem.showPrivacyPolicy()" style="
padding: 12px 20px;
background: #666;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
">Privacy Policy</button>
</div>
`;
document.getElementById('container').appendChild(this.securityDashboard);
this.setupSecurityEventListeners();
}
generatePrivacyControls() {
const privacyOptions = {
profileVisibility: {
label: 'Profile Visibility',
options: [
{ value: 'public', label: '🌍 Everyone' },
{ value: 'friends', label: '👥 Friends Only' },
{ value: 'private', label: '🔒 Private' }
]
},
locationSharing: {
label: 'Location Sharing',
options: [
{ value: 'exact', label: '📍 Exact Location' },
{ value: 'proximity', label: '🏙️ General Area' },
{ value: 'disabled', label: '🚫 Disabled' }
]
},
dataCollection: {
label: 'Data Collection',
options: [
{ value: 'minimal', label: '📊 Minimal' },
{ value: 'standard', label: '📈 Standard' },
{ value: 'enhanced', label: '📱 Enhanced' }
]
}
};
return Object.entries(privacyOptions).map(([key, config]) => `
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">${config.label}</label>
<select id="${key}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 8px;">
${config.options.map(opt => `
<option value="${opt.value}" ${this.privacySettings.get(key) === opt.value ? 'selected' : ''}>
${opt.label}
</option>
`).join('')}
</select>
</div>
`).join('');
}
setupSecurityEventListeners() {
// Enhanced encryption toggle
document.getElementById('enhancedEncryption')?.addEventListener('change', (e) => {
this.encryptionEnabled = e.target.checked;
this.updateSecurityStatus();
});
// Session timeout
document.getElementById('sessionTimeout')?.addEventListener('change', (e) => {
this.setupSessionTimeout(e.target.checked);
});
// Login alerts
document.getElementById('loginAlerts')?.addEventListener('change', (e) => {
this.setupLoginAlerts(e.target.checked);
});
}
createPrivacyControls() {
// Quick privacy toggle in main UI
this.privacyToggle = document.createElement('button');
this.privacyToggle.style.cssText = `
position: fixed;
bottom: 80px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: rgba(255, 107, 107, 0.9);
color: white;
border: none;
font-size: 18px;
cursor: pointer;
z-index: 100;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
backdrop-filter: blur(10px);
`;
this.privacyToggle.textContent = '🛡️';
this.privacyToggle.title = 'Security & Privacy';
this.privacyToggle.addEventListener('click', () => {
this.showDashboard();
});
document.getElementById('container').appendChild(this.privacyToggle);
}
createSafetyIndicators() {
// Safety indicators in chat and interactions
this.setupChatSafety();
this.setupProfileSafety();
this.setupLocationSafety();
}
setupChatSafety() {
// Monitor chat for inappropriate content
const originalSendMessage = this.scene.proximityChat?.sendMessage;
if (originalSendMessage) {
this.scene.proximityChat.sendMessage = (message, avatar) => {
// Check message safety before sending
if (this.moderateMessage(message)) {
originalSendMessage(message, avatar);
} else {
this.handleInappropriateMessage(message, avatar);
}
};
}
}
moderateMessage(message) {
const blockedPatterns = [
/badword1/gi,
/badword2/gi,
/https?:\/\//gi, // Block links for demo
/[0-9]{10,}/g // Block long number sequences (phone numbers)
];
return !blockedPatterns.some(pattern => pattern.test(message));
}
handleInappropriateMessage(message, avatar) {
// Notify user
this.scene.socialSystem?.showSocialNotification(
'Message blocked for safety reasons 🛡️',
'warning'
);
// Log the incident
this.logSecurityIncident('inappropriate_message', {
avatar: avatar?.options.name,
message: message,
timestamp: new Date().toISOString()
});
// Auto-report repeated offenses
this.checkForPattern(avatar, message);
}
setupProfileSafety() {
// Monitor profile changes for inappropriate content
this.setupProfileModeration();
}
setupLocationSafety() {
// Ensure location sharing respects privacy settings
this.setupLocationPrivacy();
}
// Security Management Methods
showDashboard() {
this.updateSecurityStatus();
this.securityDashboard.style.display = 'block';
}
hideDashboard() {
this.securityDashboard.style.display = 'none';
}
updateSecurityStatus() {
const statusElements = {
encryptionStatus: this.encryptionEnabled ? '✅ Enabled' : '❌ Disabled',
'2faStatus': '❌ Disabled', // Would check actual 2FA status
lastLogin: new Date().toLocaleString(),
activeSessions: '1'
};
Object.entries(statusElements).forEach(([id, text]) => {
const element = document.getElementById(id);
if (element) element.textContent = text;
});
}
applySecuritySettings() {
// Apply privacy settings
Object.keys(this.privacySettings).forEach(key => {
const select = document.getElementById(key);
if (select) {
this.privacySettings.set(key, select.value);
}
});
// Apply security settings
this.applyEncryptionSettings();
this.applySessionSettings();
this.applyMonitoringSettings();
this.saveSecuritySettings();
this.hideDashboard();
this.scene.socialSystem?.showSocialNotification(
'Security settings applied! 🛡️',
'success'
);
}
applyEncryptionSettings() {
if (this.encryptionEnabled) {
this.enableEnhancedEncryption();
} else {
this.disableEnhancedEncryption();
}
}
applySessionSettings() {
const sessionTimeout = document.getElementById('sessionTimeout')?.checked;
const loginAlerts = document.getElementById('loginAlerts')?.checked;
if (sessionTimeout) {
this.setupAutoLogout(15 * 60 * 1000); // 15 minutes
}
if (loginAlerts) {
this.enableLoginAlerts();
}
}
setupAutoLogout(timeout) {
let inactivityTimer;
const resetTimer = () => {
clearTimeout(inactivityTimer);
inactivityTimer = setTimeout(() => {
this.autoLogout();
}, timeout);
};
// Reset timer on user activity
['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'].forEach(event => {
document.addEventListener(event, resetTimer, false);
});
resetTimer();
}
autoLogout() {
if (confirm('Session timeout. Would you like to stay logged in?')) {
this.setupAutoLogout(15 * 60 * 1000);
} else {
this.performLogout();
}
}
performLogout() {
// Clear sensitive data
this.clearSensitiveData();
// Notify user
this.scene.socialSystem?.showSocialNotification(
'Logged out for security 🔒',
'info'
);
// Reload to login screen
setTimeout(() => location.reload(), 2000);
}
clearSensitiveData() {
// Clear tokens and sensitive info
localStorage.removeItem('authToken');
localStorage.removeItem('userData');
// Clear any cached sensitive data
this.scene.dataPersistence?.storageAdapters.cache.cache.clear();
}
enableEnhancedEncryption() {
// In production, this would set up stronger encryption
console.log('Enhanced encryption enabled');
this.encryptionEnabled = true;
}
disableEnhancedEncryption() {
console.log('Enhanced encryption disabled');
this.encryptionEnabled = false;
}
enableLoginAlerts() {
// Setup login notification system
console.log('Login alerts enabled');
}
// Privacy Management
showPrivacyPolicy() {
const policyContent = `
<h3>Privacy Policy</h3>
<div style="max-height: 300px; overflow-y: auto; text-align: left;">
<h4>Data We Collect</h4>
<ul>
<li>Profile information you provide</li>
<li>Messages and interactions</li>
<li>Technical data for service improvement</li>
</ul>
<h4>How We Use Your Data</h4>
<ul>
<li>To provide and improve our service</li>
<li>To ensure safety and security</li>
<li>To personalize your experience</li>
</ul>
<h4>Your Rights</h4>
<ul>
<li>Access your personal data</li>
<li>Correct inaccurate data</li>
<li>Delete your data</li>
<li>Object to data processing</li>
</ul>
<p><small>Last updated: ${new Date().toLocaleDateString()}</small></p>
</div>
`;
this.showModal('Privacy Policy', policyContent);
}
showModal(title, content) {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 20px;
z-index: 1001;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
`;
modal.innerHTML = `
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 15px;">
<h3 style="margin: 0; color: #ff6b6b;">${title}</h3>
<button onclick="this.parentElement.parentElement.remove()" style="
background: none;
border: none;
font-size: 20px;
cursor: pointer;
">✕</button>
</div>
${content}
<button onclick="this.parentElement.remove()" style="
width: 100%;
padding: 12px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
margin-top: 15px;
">Close</button>
`;
document.getElementById('container').appendChild(modal);
}
// Safety Tools
showBlockList() {
const blockList = this.moderation.blockList.getList();
const content = `
<h4>Blocked Users</h4>
${blockList.length > 0 ?
blockList.map(user => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #eee;">
<span>${user.name}</span>
<button onclick="securitySystem.unblockUser('${user.id}')" style="
background: #4ecdc4;
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
">Unblock</button>
</div>
`).join('') :
'<p style="color: #666; text-align: center;">No blocked users</p>'
}
`;
this.showModal('Block List', content);
}
showReportHistory() {
const reports = this.moderation.reporting.getUserReports();
const content = `
<h4>Your Reports</h4>
${reports.length > 0 ?
reports.map(report => `
<div style="padding: 10px; border-bottom: 1px solid #eee;">
<div><strong>Type:</strong> ${report.type}</div>
<div><strong>Date:</strong> ${new Date(report.timestamp).toLocaleString()}</div>
<div><strong>Status:</strong> ${report.status}</div>
</div>
`).join('') :
'<p style="color: #666; text-align: center;">No reports submitted</p>'
}
`;
this.showModal('Report History', content);
}
emergencyExit() {
// Immediate safety measure
if (confirm('Activate emergency exit? This will immediately hide the app.')) {
this.activateEmergencyExit();
}
}
activateEmergencyExit() {
// Hide all UI elements
document.querySelectorAll('div, canvas, button').forEach(el => {
el.style.display = 'none';
});
// Show emergency screen
document.body.style.background = '#000';
document.body.innerHTML = `
<div style="
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
text-align: center;
font-family: Arial, sans-serif;
">
<h1>Emergency Exit Activated</h1>
<p>The app is now hidden.</p>
<button onclick="location.reload()" style="
padding: 10px 20px;
background: #4ecdc4;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 10px;
">Restore App</button>
<button onclick="window.close()" style="
padding: 10px 20px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 10px;
">Close Tab</button>
</div>
`;
// Clear sensitive data
this.clearSensitiveData();
}
// Data Export with Privacy
exportData() {
const data = this.scene.dataPersistence?.prepareSaveData();
if (!data) return;
// Apply privacy filters before export
const filteredData = this.applyPrivacyFilters(data);
const blob = new Blob([JSON.stringify(filteredData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `secure-export-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
this.scene.socialSystem?.showSocialNotification(
'Data exported with privacy filters! 📤',
'success'
);
}
applyPrivacyFilters(data) {
// Remove sensitive information based on settings
const filtered = { ...data };
if (this.privacySettings.get('profileVisibility') === 'private') {
delete filtered.profile?.personalInfo;
}
if (this.privacySettings.get('locationSharing') === 'disabled') {
delete filtered.environment?.locationData;
}
// Anonymize relationship data
if (filtered.relationships) {
Object.keys(filtered.relationships).forEach(key => {
if (filtered.relationships[key]) {
filtered.relationships[key].partnerInfo = 'ANONYMIZED';
}
});
}
return filtered;
}
// Incident Management
logSecurityIncident(type, details) {
const incident = {
type,
details,
timestamp: new Date().toISOString(),
severity: this.assessSeverity(type),
actionTaken: 'logged'
};
// Store incident
const incidents = JSON.parse(localStorage.getItem('securityIncidents') || '[]');
incidents.push(incident);
localStorage.setItem('securityIncidents', JSON.stringify(incidents));
// Auto-escalate based on severity
if (incident.severity === 'high') {
this.escalateIncident(incident);
}
}
assessSeverity(type) {
const severityMap = {
'inappropriate_message': 'medium',
'suspicious_login': 'high',
'data_breach': 'critical',
'harassment': 'high'
};
return severityMap[type] || 'low';
}
escalateIncident(incident) {
// In production, this would notify admins or take automated action
console.warn('HIGH SEVERITY INCIDENT:', incident);
// For demo, just show a warning
this.scene.socialSystem?.showSocialNotification(
'Security incident detected! 🚨',
'error'
);
}
// Settings Persistence
saveSecuritySettings() {
const settings = {
privacy: Object.fromEntries(this.privacySettings),
security: {
encryptionEnabled: this.encryptionEnabled,
securityLevel: this.securityLevel
}
};
localStorage.setItem('securitySettings', JSON.stringify(settings));
}
loadPrivacySettings() {
try {
const saved = localStorage.getItem('securitySettings');
if (saved) {
const settings = JSON.parse(saved);
Object.entries(settings.privacy).forEach(([key, value]) => {
this.privacySettings.set(key, value);
});
this.encryptionEnabled = settings.security.encryptionEnabled;
this.securityLevel = settings.security.securityLevel;
}
} catch (error) {
console.warn('Error loading security settings:', error);
}
}
}
// Security Subsystems
class AuthenticationManager {
constructor() {
this.sessions = new Map();
this.failedAttempts = new Map();
}
async authenticate(userId, credentials) {
// Implementation would verify credentials
return { success: true, token: 'demo-token' };
}
validateSession(token) {
return this.sessions.has(token);
}
logout(token) {
this.sessions.delete(token);
}
}
class EncryptionManager {
constructor() {
this.algorithm = 'AES-GCM';
}
async encrypt(data, key) {
// In production, this would use proper encryption
return btoa(JSON.stringify(data)); // Base64 for demo
}
async decrypt(encryptedData, key) {
try {
return JSON.parse(atob(encryptedData));
} catch {
throw new Error('Decryption failed');
}
}
}
class SecurityMonitor {
constructor() {
this.suspiciousPatterns = new Map();
this.monitoringEnabled = true;
}
monitorActivity(activity) {
if (!this.monitoringEnabled) return;
// Check for suspicious patterns
if (this.isSuspicious(activity)) {
this.flagSuspiciousActivity(activity);
}
}
isSuspicious(activity) {
// Simple pattern matching for demo
const patterns = [
/spam/gi,
/http/gi,
/[0-9]{10,}/g
];
return patterns.some(pattern =>
pattern.test(JSON.stringify(activity))
);
}
}
class IncidentResponseTeam {
constructor() {
this.incidents = new Map();
}
handleIncident(incident) {
this.incidents.set(incident.id, incident);
this.notifyStakeholders(incident);
this.takeRemedialAction(incident);
}
}
class AutomatedModeration {
constructor() {
this.filters = new Map();
this.setupContentFilters();
}
setupContentFilters() {
this.filters.set('profanity', /badword1|badword2/gi);
this.filters.set('personal_info', /[0-9]{10,}|@gmail|@yahoo/gi);
}
moderateContent(content) {
let cleanContent = content;
this.filters.forEach((pattern, type) => {
cleanContent = cleanContent.replace(pattern, '[REDACTED]');
});
return {
clean: cleanContent,
wasModified: cleanContent !== content
};
}
}
class ManualModeration {
constructor() {
this.pendingReviews = new Map();
}
queueForReview(content, context) {
const review = {
id: Math.random().toString(36),
content,
context,
timestamp: new Date(),
status: 'pending'
};
this.pendingReviews.set(review.id, review);
return review.id;
}
}
class ReportingSystem {
constructor() {
this.reports = new Map();
}
submitReport(report) {
const reportId = Math.random().toString(36);
this.reports.set(reportId, {
...report,
id: reportId,
timestamp: new Date(),
status: 'submitted'
});
return reportId;
}
getUserReports() {
return Array.from(this.reports.values());
}
}
class BlockListManager {
constructor() {
this.blockedUsers = new Map();
}
blockUser(userId, reason = '') {
this.blockedUsers.set(userId, {
id: userId,
reason,
timestamp: new Date(),
expires: null // Permanent block
});
}
unblockUser(userId) {
this.blockedUsers.delete(userId);
}
isBlocked(userId) {
return this.blockedUsers.has(userId);
}
getList() {
return Array.from(this.blockedUsers.values());
}
}
Step 3: Advanced AI System - Love That Understands! 🧠
Let's create an AI system with machine learning for better matchmaking and intelligent NPC behavior:
// advanced-ai.js - Because love should be smart! 🧠
class AdvancedAISystem {
constructor(scene) {
this.scene = scene;
this.mlModels = new Map();
this.recommendationEngine = null;
this.sentimentAnalyzer = null;
this.behaviorPredictor = null;
this.setupAIFramework();
this.trainInitialModels();
this.setupAIFeatures();
console.log("Advanced AI system initialized! Love just got smarter! 🧠");
}
setupAIFramework() {
this.ai = {
matchmaking: new MatchmakingAI(),
conversation: new ConversationAI(),
behavior: new BehaviorAI(),
analytics: new AnalyticsAI()
};
this.setupMachineLearning();
this.setupNeuralNetworks();
this.setupAIAnalytics();
}
setupMachineLearning() {
this.ml = {
trainingData: new Map(),
models: new Map(),
features: new Map(),
trainModel: async (modelName, data) => {
return this.trainModelInternal(modelName, data);
},
predict: (modelName, input) => {
return this.makePrediction(modelName, input);
},
updateModel: (modelName, newData) => {
return this.retrainModel(modelName, newData);
}
};
this.initializeMLModels();
}
initializeMLModels() {
// Initialize core ML models
this.mlModels.set('compatibility', this.createCompatibilityModel());
this.mlModels.set('sentiment', this.createSentimentModel());
this.mlModels.set('behavior', this.createBehaviorModel());
this.mlModels.set('recommendation', this.createRecommendationModel());
}
createCompatibilityModel() {
return {
name: 'compatibility_predictor',
version: '1.0',
features: ['personality_traits', 'interests', 'values', 'communication_style'],
predict: (user1, user2) => {
// Simple compatibility algorithm for demo
// In production, this would use trained ML model
let score = 0.5; // Base score
// Personality matching
const personalityMatch = this.calculatePersonalityMatch(user1, user2);
score += personalityMatch * 0.3;
// Interest overlap
const interestMatch = this.calculateInterestMatch(user1, user2);
score += interestMatch * 0.3;
// Value alignment
const valueMatch = this.calculateValueMatch(user1, user2);
score += valueMatch * 0.2;
// Communication style compatibility
const communicationMatch = this.calculateCommunicationMatch(user1, user2);
score += communicationMatch * 0.2;
return Math.min(1, Math.max(0, score));
},
train: (data) => {
// Model training would happen here
console.log('Training compatibility model with', data.length, 'samples');
}
};
}
calculatePersonalityMatch(user1, user2) {
if (!user1.personality || !user2.personality) return 0.5;
const traits1 = user1.personality.traits || [];
const traits2 = user2.personality.traits || [];
const commonTraits = traits1.filter(trait => traits2.includes(trait));
const totalTraits = new Set([...traits1, ...traits2]).size;
return totalTraits > 0 ? commonTraits.length / totalTraits : 0;
}
calculateInterestMatch(user1, user2) {
const interests1 = user1.interests || [];
const interests2 = user2.interests || [];
const commonInterests = interests1.filter(interest => interests2.includes(interest));
const totalInterests = new Set([...interests1, ...interests2]).size;
return totalInterests > 0 ? commonInterests.length / totalInterests : 0;
}
calculateValueMatch(user1, user2) {
// Simple value matching based on predefined value categories
const values1 = user1.values || [];
const values2 = user2.values || [];
const commonValues = values1.filter(value => values2.includes(value));
const totalValues = new Set([...values1, ...values2]).size;
return totalValues > 0 ? commonValues.length / totalValues : 0.5;
}
calculateCommunicationMatch(user1, user2) {
// Match communication styles
const style1 = user1.communicationStyle || 'balanced';
const style2 = user2.communicationStyle || 'balanced';
const styleCompatibility = {
'direct': { 'direct': 0.9, 'balanced': 0.7, 'indirect': 0.3 },
'balanced': { 'direct': 0.7, 'balanced': 1.0, 'indirect': 0.7 },
'indirect': { 'direct': 0.3, 'balanced': 0.7, 'indirect': 0.9 }
};
return styleCompatibility[style1]?.[style2] || 0.5;
}
setupNeuralNetworks() {
this.neuralNetworks = {
recommendation: this.createRecommendationNetwork(),
sentiment: this.createSentimentNetwork(),
behavior: this.createBehaviorNetwork()
};
}
createRecommendationNetwork() {
// Simple neural network for recommendations
return {
layers: [
{ type: 'input', size: 10 },
{ type: 'hidden', size: 8, activation: 'relu' },
{ type: 'hidden', size: 6, activation: 'relu' },
{ type: 'output', size: 1, activation: 'sigmoid' }
],
predict: (input) => {
// Simplified forward propagation for demo
let output = this.forwardPropagate(input, this.layers);
return output[0];
},
train: (data) => {
// Training would happen here
console.log('Training recommendation network');
}
};
}
forwardPropagate(input, layers) {
// Simplified forward propagation
let current = input;
for (const layer of layers) {
if (layer.type === 'hidden' || layer.type === 'output') {
current = this.applyActivation(current, layer.activation);
}
}
return current;
}
applyActivation(values, activation) {
switch (activation) {
case 'relu':
return values.map(v => Math.max(0, v));
case 'sigmoid':
return values.map(v => 1 / (1 + Math.exp(-v)));
default:
return values;
}
}
trainInitialModels() {
// Train models with initial data
this.trainWithHistoricalData();
this.setupContinuousLearning();
}
trainWithHistoricalData() {
// Load and train with any available historical data
const historicalData = this.loadHistoricalData();
if (historicalData.length > 0) {
this.mlModels.forEach((model, name) => {
model.train(historicalData);
});
}
}
loadHistoricalData() {
// In production, this would load from database
// For demo, return empty array
return [];
}
setupContinuousLearning() {
// Set up real-time model updates
this.setupLearningFromInteractions();
this.setupFeedbackLoop();
}
setupLearningFromInteractions() {
// Learn from user interactions
const events = ['match', 'message', 'meetup', 'relationship_change'];
events.forEach(event => {
this.scene.emitter?.on(event, (data) => {
this.learnFromInteraction(event, data);
});
});
}