character-selector / index.html
KBLLR's picture
threejs is failling to load
e97156e verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Character World</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r164/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r164/examples/jsm/controls/OrbitControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r164/examples/jsm/loaders/GLTFLoader.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
}
#canvas {
display: block;
}
.transition-all {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.character-card {
background: linear-gradient(145deg, rgba(30, 41, 59, 0.9), rgba(15, 23, 42, 0.9));
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.character-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
border-color: rgba(139, 92, 246, 0.5);
}
.dialog-box {
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}
.character-preview {
width: 100%;
height: 200px;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
}
.character-preview::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.05), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.character-card:hover .character-preview::before {
opacity: 1;
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.gradient-text {
background: linear-gradient(135deg, #8b5cf6 0%, #3b82f6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.floating-animation {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.pulse-glow {
animation: pulse-glow 2s ease-in-out infinite alternate;
}
@keyframes pulse-glow {
from { box-shadow: 0 0 20px rgba(139, 92, 246, 0.4); }
to { box-shadow: 0 0 30px rgba(139, 92, 246, 0.8); }
}
</style>
</head>
<body class="bg-gray-900 text-white">
<!-- Welcome Screen -->
<div id="welcome-screen" class="fixed inset-0 flex items-center justify-center bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 z-50 transition-all duration-500">
<div class="absolute inset-0 overflow-hidden">
<div class="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-conic from-transparent via-purple-500/10 to-transparent animate-spin-slow"></div>
<div class="absolute -top-1/2 -right-1/2 w-full h-full bg-gradient-conic from-transparent via-blue-500/10 to-transparent animate-spin-slow" style="animation-delay: -3s;"></div>
</div>
<div class="text-center max-w-4xl p-12 glass-effect rounded-3xl shadow-2xl relative z-10 border border-white/10">
<div class="floating-animation mb-8">
<div class="w-32 h-32 mx-auto bg-gradient-to-r from-purple-500 to-blue-500 rounded-full flex items-center justify-center shadow-2xl">
<svg class="w-16 h-16 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
</div>
</div>
<h1 class="text-6xl font-black mb-6 gradient-text tracking-tight">CharacterVerse</h1>
<p class="text-2xl mb-10 text-gray-200 leading-relaxed">Immerse yourself in an expansive 3D universe where every character has a story. Choose your avatar and embark on an unforgettable journey through dynamic worlds filled with interactive companions.</p>
<button id="start-btn" class="px-12 py-4 bg-gradient-to-r from-purple-600 to-blue-600 rounded-2xl text-white font-bold text-xl hover:from-purple-700 hover:to-blue-700 transition-all transform hover:scale-110 shadow-2xl pulse-glow border border-white/20">
<span class="flex items-center justify-center space-x-3">
<span>Begin Epic Journey</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</span>
</button>
<div class="mt-8 flex justify-center space-x-6 text-sm text-gray-400">
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Real-time 3D Graphics</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
<span>Interactive Characters</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-purple-500 rounded-full"></div>
<span>Dynamic Environments</span>
</div>
</div>
</div>
</div>
<!-- Character Selection Screen -->
<div id="character-selection" class="fixed inset-0 bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 z-40 transition-all duration-500 opacity-0 pointer-events-none">
<div class="absolute inset-0 bg-black/20 backdrop-blur-sm"></div>
<div class="container mx-auto px-6 py-8 h-full flex flex-col relative z-10">
<div class="text-center mb-12">
<h2 class="text-5xl font-black mb-4 gradient-text">Choose Your Champion</h2>
<p class="text-xl text-gray-300 max-w-2xl mx-auto">Select the perfect avatar that represents your journey. Each character comes with unique abilities and personality traits.</p>
</div>
<div class="flex-1 overflow-y-auto pb-32">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 max-w-7xl mx-auto">
<!-- Character cards will be dynamically inserted here -->
</div>
</div>
<div class="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-slate-900/95 via-slate-900/80 to-transparent py-8 px-6 z-50 backdrop-blur-lg border-t border-white/10">
<div class="container mx-auto text-center">
<button id="confirm-character" class="px-12 py-4 bg-gradient-to-r from-green-600 to-emerald-500 rounded-2xl text-white font-bold text-lg hover:from-green-700 hover:to-emerald-600 transition-all transform hover:scale-105 shadow-2xl border border-white/20 opacity-0 pulse-glow">
<span class="flex items-center justify-center space-x-3">
<span>Confirm Champion</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</span>
</button>
</div>
</div>
</div>
</div>
<!-- World Selection Screen -->
<div id="world-selection" class="fixed inset-0 bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900 z-30 transition-all duration-500 opacity-0 pointer-events-none">
<div class="absolute inset-0 bg-black/20 backdrop-blur-sm"></div>
<div class="container mx-auto px-6 py-8 h-full flex flex-col relative z-10">
<div class="text-center mb-12">
<h2 class="text-5xl font-black mb-4 gradient-text">Forge Your Universe</h2>
<p class="text-xl text-gray-300 max-w-2xl mx-auto">Select 2 unique characters to populate your world. Each will bring their own stories and interactions to your adventure.</p>
<div class="mt-4 inline-flex items-center space-x-4 bg-white/5 rounded-full px-6 py-3 border border-white/10">
<span class="text-green-400 font-semibold" id="selected-count">0</span>
<span class="text-gray-400">/</span>
<span class="text-gray-300">2 Characters Selected</span>
</div>
</div>
<div class="flex-1 overflow-y-auto pb-32">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-8 max-w-7xl mx-auto">
<!-- World character cards will be dynamically inserted here -->
</div>
</div>
<div class="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-slate-900/95 via-slate-900/80 to-transparent py-8 px-6 z-50 backdrop-blur-lg border-t border-white/10">
<div class="container mx-auto text-center">
<button id="start-world" class="px-12 py-4 bg-gradient-to-r from-purple-600 to-blue-600 rounded-2xl text-white font-bold text-lg hover:from-purple-700 hover:to-blue-700 transition-all transform hover:scale-105 shadow-2xl border border-white/20 opacity-0 pulse-glow">
<span class="flex items-center justify-center space-x-3">
<span>Generate Dynamic World</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</span>
</button>
</div>
</div>
</div>
</div>
<!-- Game UI -->
<div id="game-ui" class="fixed inset-0 pointer-events-none z-20 opacity-0 transition-all">
<div class="absolute bottom-4 left-4 bg-gray-800 bg-opacity-70 p-4 rounded-lg">
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<span>WASD: Move</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
<span>Space: Jump</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-purple-500"></div>
<span>Shift: Run</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<span>Near NPC: Talk (Space)</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<span>C: Camera Mode</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-pink-500"></div>
<span>M: Toggle Music</span>
</div>
</div>
<div class="absolute top-4 right-4 bg-gray-800 bg-opacity-70 p-4 rounded-lg">
<div class="text-white font-semibold">Camera: <span id="camera-mode-display">Follow</span></div>
</div>
<!-- Music Toggle Button -->
<div class="absolute top-4 left-4 bg-gray-800 bg-opacity-70 p-3 rounded-lg music-toggle-container">
<button id="music-toggle" class="flex items-center space-x-2 text-white hover:text-gray-300 transition-colors">
<svg id="music-icon" class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>
<span id="music-status" class="text-sm font-semibold">Music: On</span>
</button>
</div>
</div>
<!-- Dialog Box -->
<div id="dialog-box" class="fixed bottom-0 left-0 right-0 bg-gray-900 bg-opacity-90 p-6 rounded-t-2xl transform translate-y-full transition-all duration-300 z-50 max-w-4xl mx-auto">
<div class="flex items-start space-x-4">
<div id="dialog-character" class="w-16 h-16 rounded-full bg-gray-700 flex-shrink-0"></div>
<div class="flex-1">
<h3 id="dialog-name" class="text-xl font-bold mb-2">Character Name</h3>
<p id="dialog-text" class="text-gray-300">Hello there! This is a sample dialog text that will be replaced with actual dialog content.</p>
</div>
</div>
<button id="close-dialog" class="absolute top-4 right-4 text-gray-400 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Canvas for Three.js -->
<canvas id="canvas"></canvas>
<script>
// Game state
const gameState = {
currentScreen: 'welcome',
selectedCharacter: null,
selectedWorldCharacters: [],
characters: [],
worldCharacters: [],
player: null,
npcs: [],
nearbyNpc: null,
isRunning: false,
isJumping: false,
cameraMode: 'follow', // 'follow', 'orbit', 'first-person'
musicEnabled: true,
keys: {
w: false,
a: false,
s: false,
d: false,
shift: false,
space: false
}
};
// Audio context and music
let audioContext = null;
let musicSource = null;
let musicGain = null;
let isMusicPlaying = false;
// RPM Avatar Manifest Data
const characterData = [
{
id: "6637232cc6a3e0f03418f723",
name: "David",
modelUrl: "https://models.readyplayer.me/6637232cc6a3e0f03418f723.glb",
color: "#3B82F6",
dialog: ["Hello there! I'm David.", "Nice to meet you!", "Ready for an adventure?"]
},
{
id: "64e602a9b54bdcd880df8ca3",
name: "Anja",
modelUrl: "https://models.readyplayer.me/64e602a9b54bdcd880df8ca3.glb",
color: "#EC4899",
dialog: ["Hi, I'm Anja!", "Beautiful day, isn't it?", "What brings you here?"]
},
{
id: "6658826709dff701a3a2955e",
name: "Eli",
modelUrl: "https://models.readyplayer.me/6658826709dff701a3a2955e.glb",
color: "#10B981",
dialog: ["Hey, I'm Eli!", "Love exploring new places.", "What's your story?"]
},
{
id: "664e502a6ef2fae943a4e5a4",
name: "Julien",
modelUrl: "https://models.readyplayer.me/664e502a6ef2fae943a4e5a4.glb",
color: "#F59E0B",
dialog: ["Julien here!", "Always up for a challenge.", "Let's make this fun!"]
},
{
id: "680ebd7587f61ba0328013ae",
name: "Dan",
modelUrl: "https://avatars.readyplayer.me/680ebd7587f61ba0328013ae.glb",
color: "#8B5CF6",
dialog: ["Dan reporting for duty!", "Ready when you are.", "Let's do this!"]
},
{
id: "664e502a6ef2fae943a4e5a4",
name: "Lucy",
modelUrl: "https://avatars.readyplayer.me/664e502a6ef2fae943a4e5a4.glb",
color: "#EF4444",
dialog: ["Hi, I'm Lucy!", "So excited to be here!", "Ready for anything!"]
},
{
id: "665b2ed436c854537e38cdf8",
name: "Lana",
modelUrl: "https://models.readyplayer.me/665b2ed436c854537e38cdf8.glb",
color: "#6366F1",
dialog: ["Lana at your service!", "What a wonderful world!", "Let's explore together!"]
},
{
id: "665b2ed436c854537e38cdf8",
name: "Nyx",
modelUrl: "https://models.readyplayer.me/665b2ed436c854537e38cdf8.glb",
color: "#F97316",
dialog: ["I'm Nyx, nice to meet you!", "The night is my domain.", "Ready for adventure!"]
}
];
// Three.js variables
let scene, camera, renderer, controls;
let mixer, clock, loader;
let world;
// Initialize the app
document.addEventListener('DOMContentLoaded', async () => {
// Set up UI event listeners
document.getElementById('start-btn').addEventListener('click', showCharacterSelection);
document.getElementById('confirm-character').addEventListener('click', showWorldSelection);
document.getElementById('start-world').addEventListener('click', startGame);
document.getElementById('close-dialog').addEventListener('click', closeDialog);
// Keyboard event listeners
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// Camera controls listener
window.addEventListener('keydown', (event) => {
if (event.key.toLowerCase() === 'c') {
cycleCameraMode();
}
if (event.key.toLowerCase() === 'm') {
toggleMusic();
}
});
// Music toggle button listener
document.getElementById('music-toggle').addEventListener('click', toggleMusic);
// Populate character selection
populateCharacterSelection();
// Initialize audio context on user interaction
document.addEventListener('click', initializeAudio, { once: true });
// Check if Three.js modules are available
if (typeof THREE === 'undefined') {
console.error('Three.js not loaded');
// Try to load modules dynamically if needed
loadThreeJSModules();
}
});
// Function to dynamically load Three.js modules if needed
function loadThreeJSModules() {
// Check if OrbitControls is available
if (typeof THREE.OrbitControls === 'undefined') {
console.warn('OrbitControls not available, using fallback controls');
}
// Check if GLTFLoader is available
if (typeof THREE.GLTFLoader === 'undefined') {
console.warn('GLTFLoader not available, using fallback geometry');
}
}
function initializeAudio() {
if (audioContext) return;
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
musicGain = audioContext.createGain();
musicGain.connect(audioContext.destination);
musicGain.gain.value = gameState.musicEnabled ? 0.3 : 0;
// Create a simple ambient music
createAmbientMusic();
} catch (error) {
console.log('Audio not supported:', error);
}
}
function createAmbientMusic() {
if (!audioContext) return;
const oscillator1 = audioContext.createOscillator();
const oscillator2 = audioContext.createOscillator();
const lfo = audioContext.createOscillator();
const lfoGain = audioContext.createGain();
oscillator1.type = 'sine';
oscillator1.frequency.value = 220;
oscillator2.type = 'triangle';
oscillator2.frequency.value = 330;
lfo.type = 'sine';
lfo.frequency.value = 0.1;
lfoGain.gain.value = 10;
lfo.connect(lfoGain);
lfoGain.connect(oscillator1.frequency);
lfoGain.connect(oscillator2.frequency);
oscillator1.connect(musicGain);
oscillator2.connect(musicGain);
oscillator1.start();
oscillator2.start();
lfo.start();
musicSource = { oscillator1, oscillator2, lfo };
isMusicPlaying = true;
}
function toggleMusic() {
if (!audioContext || !musicGain) {
initializeAudio();
return;
}
gameState.musicEnabled = !gameState.musicEnabled;
if (musicGain) {
musicGain.gain.value = gameState.musicEnabled ? 0.3 : 0;
}
// Update UI
const musicIcon = document.getElementById('music-icon');
const musicStatus = document.getElementById('music-status');
if (gameState.musicEnabled) {
musicIcon.innerHTML = '<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>';
musicStatus.textContent = 'Music: On';
} else {
musicIcon.innerHTML = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>';
musicStatus.textContent = 'Music: Off';
}
}
function showCharacterSelection() {
document.getElementById('welcome-screen').classList.add('opacity-0', 'pointer-events-none');
document.getElementById('character-selection').classList.remove('opacity-0', 'pointer-events-none');
}
function showWorldSelection() {
if (!gameState.selectedCharacter) return;
document.getElementById('character-selection').classList.add('opacity-0', 'pointer-events-none');
document.getElementById('world-selection').classList.remove('opacity-0', 'pointer-events-none');
// Filter out the selected character from world selection
const availableCharacters = characterData.filter(char => char.id !== gameState.selectedCharacter.id);
populateWorldSelection(availableCharacters);
}
function startGame() {
if (gameState.selectedWorldCharacters.length < 2) {
alert('Please select 2 characters for your world!');
return;
}
// Ensure audio is initialized when game starts
if (!audioContext) {
initializeAudio();
}
document.getElementById('world-selection').classList.add('opacity-0', 'pointer-events-none');
document.getElementById('game-ui').classList.remove('opacity-0');
// Initialize Three.js world
initThreeJS();
}
function populateCharacterSelection() {
const container = document.querySelector('#character-selection .grid');
container.innerHTML = '';
characterData.forEach(character => {
const card = document.createElement('div');
card.className = 'character-card bg-gray-800 rounded-xl p-4 cursor-pointer transition-all shadow-lg';
card.innerHTML = `
<div class="character-preview mb-4 flex items-center justify-center">
<div class="w-24 h-24 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center text-white font-bold text-sm">
${character.name}
</div>
</div>
<h3 class="text-xl font-bold text-center mb-2 text-white">${character.name}</h3>
<p class="text-gray-300 text-center text-sm">RPM Avatar</p>
`;
card.addEventListener('click', () => {
// Deselect all cards
document.querySelectorAll('.character-card').forEach(c => {
c.classList.remove('ring-2', 'ring-purple-500');
});
// Select this card
card.classList.add('ring-2', 'ring-purple-500');
// Update selected character
gameState.selectedCharacter = character;
// Show confirm button
document.getElementById('confirm-character').classList.remove('opacity-0');
});
container.appendChild(card);
});
}
function populateWorldSelection(characters) {
const container = document.querySelector('#world-selection .grid');
container.innerHTML = '';
characters.forEach(character => {
const card = document.createElement('div');
card.className = `character-card bg-gray-800 rounded-xl p-4 cursor-pointer transition-all shadow-lg ${
gameState.selectedWorldCharacters.some(c => c.id === character.id) ? 'ring-2 ring-green-500' : ''
}`;
card.innerHTML = `
<div class="character-preview mb-4 flex items-center justify-center">
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center text-white font-bold text-xs">
${character.name}
</div>
</div>
<h3 class="text-lg font-bold text-center mb-2 text-white">${character.name}</h3>
<p class="text-gray-300 text-sm text-center">Click to ${gameState.selectedWorldCharacters.some(c => c.id === character.id) ? 'deselect' : 'select'}</p>
`;
card.addEventListener('click', () => {
// Check if character is already selected
const index = gameState.selectedWorldCharacters.findIndex(c => c.id === character.id);
if (index !== -1) {
// Deselect
gameState.selectedWorldCharacters.splice(index, 1);
card.classList.remove('ring-2', 'ring-green-500');
card.querySelector('p').textContent = 'Click to select';
} else {
// Select if we have less than 2
if (gameState.selectedWorldCharacters.length < 2) {
gameState.selectedWorldCharacters.push(character);
card.classList.add('ring-2', 'ring-green-500');
card.querySelector('p').textContent = 'Click to deselect';
}
}
// Update start world button
if (gameState.selectedWorldCharacters.length === 2) {
document.getElementById('start-world').classList.remove('opacity-0');
} else {
document.getElementById('start-world').classList.add('opacity-0');
}
});
container.appendChild(card);
});
}
function initThreeJS() {
if (typeof THREE === 'undefined') {
console.error('Three.js is not loaded properly');
// Load Three.js from a reliable CDN
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r164/three.min.js"></script>
script.onload = () => {
console.log('Three.js loaded successfully');
initThreeJS(); // Retry initialization
};
script.onerror = () => {
console.error('Failed to load Three.js');
alert('Three.js failed to load. Please check your internet connection and refresh the page.');
};
document.head.appendChild(script);
return;
}
try {
// Set up Three.js scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB); // Sky blue
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
// Renderer
const canvas = document.getElementById('canvas');
renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Clock for animations
clock = new THREE.Clock();
// Loader - check if GLTFLoader is available
if (typeof THREE.GLTFLoader !== 'undefined') {
loader = new THREE.GLTFLoader();
} else {
console.warn('GLTFLoader not available, using fallback geometry');
loader = null;
}
// Add environment map with error handling
const envMapLoader = new THREE.CubeTextureLoader();
envMapLoader.load([
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/px.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/nx.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/py.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/ny.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/pz.jpg',
'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/nz.jpg'
],
(envMap) => {
scene.background = envMap;
scene.environment = envMap;
},
undefined,
(error) => {
console.warn('Failed to load environment map:', error);
// Fallback to plain color background
scene.background = new THREE.Color(0x87CEEB);
});
// Add better lighting for RPM avatars
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1);
scene.add(hemisphereLight);
const mainLight = new THREE.DirectionalLight(0xffffff, 0.8);
mainLight.position.set(10, 20, 15);
mainLight.castShadow = true;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
mainLight.shadow.camera.near = 0.5;
mainLight.shadow.camera.far = 50;
mainLight.shadow.camera.left = -20;
mainLight.shadow.camera.right = 20;
mainLight.shadow.camera.top = 20;
mainLight.shadow.camera.bottom = -20;
scene.add(mainLight);
// Create ground
const groundGeometry = new THREE.PlaneGeometry(100, 100);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x4ade80,
roughness: 0.8,
metalness: 0.2
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Add some environment objects
addEnvironmentObjects();
// Initialize OrbitControls with fallback
if (typeof THREE.OrbitControls !== 'undefined') {
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.enabled = false; // Start with controls disabled for follow camera
} else {
console.warn('OrbitControls not available, using fallback');
controls = {
enabled: false,
update: () => {},
target: new THREE.Vector3()
};
}
// Load player character
loadPlayerCharacter();
// Load NPC characters
loadNPCCharacters();
// Start animation loop
animate();
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
} catch (error) {
console.error('Error initializing Three.js:', error);
// Show user-friendly error message
alert('Error initializing 3D graphics. Please check your browser supports WebGL.');
}
}
function addEnvironmentObjects() {
// Add some trees
const treeGeometry = new THREE.ConeGeometry(1, 3, 8);
const treeMaterial = new THREE.MeshStandardMaterial({ color: 0x2e7d32 });
for (let i = 0; i < 20; i++) {
const tree = new THREE.Mesh(treeGeometry, treeMaterial);
tree.position.x = (Math.random() - 0.5) * 80;
tree.position.z = (Math.random() - 0.5) * 80;
tree.position.y = 1.5;
tree.castShadow = true;
scene.add(tree);
// Add trunk
const trunkGeometry = new THREE.CylinderGeometry(0.3, 0.3, 1);
const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x5e4035 });
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
trunk.position.y = 0.5;
tree.add(trunk);
}
// Add some rocks
const rockGeometry = new THREE.SphereGeometry(0.5, 8, 8);
const rockMaterial = new THREE.MeshStandardMaterial({ color: 0x757575 });
for (let i = 0; i < 15; i++) {
const rock = new THREE.Mesh(rockGeometry, rockMaterial);
rock.position.x = (Math.random() - 0.5) * 80;
rock.position.z = (Math.random() - 0.5) * 80;
rock.position.y = 0.5;
rock.castShadow = true;
scene.add(rock);
}
}
function loadPlayerCharacter() {
if (loader && typeof THREE.GLTFLoader !== 'undefined') {
loader.load(gameState.selectedCharacter.modelUrl, (gltf) => {
const model = gltf.scene;
// Scale and position the RPM avatar model - adjusted for human proportions
model.scale.set(0.8, 0.8, 0.8);
model.position.set(0, 0, 0);
// Enable shadows for all children
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(model);
// Set up animations if available
const mixer = new THREE.AnimationMixer(model);
const animations = gltf.animations;
gameState.player = {
model: model,
speed: 0.08,
runSpeed: 0.15,
rotationSpeed: 0.08,
isMoving: false,
mixer: mixer,
animations: animations,
currentAnimation: null,
animationActions: {}
};
// Set up animation actions
if (animations && animations.length > 0) {
animations.forEach((clip) => {
gameState.player.animationActions[clip.name] = mixer.clipAction(clip);
});
// Play the first animation by default
if (animations[0]) {
gameState.player.animationActions[animations[0].name].play();
gameState.player.currentAnimation = animations[0].name;
}
} else {
// If no animations, create simple placeholder animations
createPlayerAnimations();
}
}, undefined, (error) => {
console.error('Error loading player model:', error);
// Fallback to placeholder
loadPlayerCharacterFallback();
});
} else {
// GLTFLoader not available, use fallback
loadPlayerCharacterFallback();
}
}
function loadPlayerCharacterFallback() {
const group = new THREE.Group();
// Body
const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 8);
const bodyMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(gameState.selectedCharacter.color),
roughness: 0.7,
metalness: 0.1
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.75;
body.castShadow = true;
group.add(body);
// Head
const headGeometry = new THREE.SphereGeometry(0.4, 8, 8);
const headMaterial = new THREE.MeshStandardMaterial({ color: 0xf5d0b5 });
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.y = 1.6;
head.castShadow = true;
group.add(head);
// Arms
const armGeometry = new THREE.CylinderGeometry(0.15, 0.15, 0.8, 6);
const leftArm = new THREE.Mesh(armGeometry, bodyMaterial);
leftArm.position.set(-0.6, 1, 0);
leftArm.rotation.z = 0.5;
leftArm.castShadow = true;
group.add(leftArm);
const rightArm = new THREE.Mesh(armGeometry, bodyMaterial);
rightArm.position.set(0.6, 1, 0);
rightArm.rotation.z = -0.5;
rightArm.castShadow = true;
group.add(rightArm);
// Legs
const legGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.8, 6);
const leftLeg = new THREE.Mesh(legGeometry, new THREE.MeshStandardMaterial({ color: 0x1e40af }));
leftLeg.position.set(-0.2, -0.4, 0);
leftLeg.castShadow = true;
group.add(leftLeg);
const rightLeg = new THREE.Mesh(legGeometry, new THREE.MeshStandardMaterial({ color: 0x1e40af }));
rightLeg.position.set(0.2, -0.4, 0);
rightLeg.castShadow = true;
group.add(rightLeg);
group.position.y = 0;
scene.add(group);
gameState.player = {
model: group,
speed: 0.08,
runSpeed: 0.15,
rotationSpeed: 0.08,
isMoving: false,
animations: {
idle: null,
walk: null,
run: null,
jump: null
},
currentAnimation: null
};
// Add a simple animation mixer for the player
mixer = new THREE.AnimationMixer(group);
createPlayerAnimations();
setPlayerAnimation('idle');
}
function createPlayerAnimations() {
// In a real app, these would come from the GLTF model
// For this example, we'll create simple animations using NumberKeyframeTrack
// Idle animation (slight bounce)
const idleTrack = new THREE.NumberKeyframeTrack(
'.position[y]',
[0, 0.5, 1],
[0, 0.1, 0]
);
const idleClip = new THREE.AnimationClip('idle', 1, [idleTrack]);
gameState.player.animations.idle = idleClip;
// Walk animation (arm and leg movement)
const leftArmTrack = new THREE.NumberKeyframeTrack(
'.children[2].rotation[z]',
[0, 0.5, 1],
[0.3, -0.3, 0.3]
);
const rightArmTrack = new THREE.NumberKeyframeTrack(
'.children[3].rotation[z]',
[0, 0.5, 1],
[-0.3, 0.3, -0.3]
);
const leftLegTrack = new THREE.NumberKeyframeTrack(
'.children[4].position[y]',
[0, 0.5, 1],
[-0.4, -0.3, -0.4]
);
const rightLegTrack = new THREE.NumberKeyframeTrack(
'.children[5].position[y]',
[0, 0.5, 1],
[-0.3, -0.4, -0.3]
);
const walkClip = new THREE.AnimationClip('walk', 0.5, [
leftArmTrack, rightArmTrack, leftLegTrack, rightLegTrack
]);
gameState.player.animations.walk = walkClip;
// Run animation (faster arm and leg movement)
const runClip = new THREE.AnimationClip('run', 0.3, [
leftArmTrack, rightArmTrack, leftLegTrack, rightLegTrack
]);
gameState.player.animations.run = runClip;
// Jump animation
const jumpTrack = new THREE.NumberKeyframeTrack(
'.position[y]',
[0, 0.2, 0.4, 0.6, 0.8, 1],
[0, 1.5, 1, 0.3, 0, 0]
);
const jumpClip = new THREE.AnimationClip('jump', 1, [jumpTrack]);
gameState.player.animations.jump = jumpClip;
}
function setPlayerAnimation(name) {
if (gameState.player.currentAnimation === name) return;
if (mixer) {
mixer.stopAllAction();
const clip = gameState.player.animations[name];
if (clip) {
const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopRepeat);
action.clampWhenFinished = false;
action.play();
}
gameState.player.currentAnimation = name;
}
}
function loadNPCCharacters() {
gameState.selectedWorldCharacters.forEach((character, index) => {
// In a real app, we would load the GLTF model from the URL
// For this example, we'll create a simple placeholder
const group = new THREE.Group();
// Body
const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 8);
const bodyMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(character.color),
roughness: 0.7,
metalness: 0.1
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.75;
body.castShadow = true;
group.add(body);
// Head
const headGeometry = new THREE.SphereGeometry(0.4, 8, 8);
const headMaterial = new THREE.MeshStandardMaterial({ color: 0xf5d0b5 });
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.y = 1.6;
head.castShadow = true;
group.add(head);
// Scale NPCs to match player size
group.scale.set(0.8, 0.8, 0.8);
// Position NPCs in a circle around the center
const angle = (index / gameState.selectedWorldCharacters.length) * Math.PI * 2;
const radius = 10 + Math.random() * 10;
group.position.x = Math.cos(angle) * radius;
group.position.z = Math.sin(angle) * radius;
group.position.y = 0;
// Make NPC face center
group.lookAt(0, 0, 0);
scene.add(group);
gameState.npcs.push({
model: group,
character: character,
dialog: character.dialog,
currentDialogIndex: 0
});
});
}
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.1); // Cap delta to prevent large jumps
// Update player animation mixer
if (mixer) {
mixer.update(delta);
}
// Handle player movement
handlePlayerMovement(delta);
// Check for nearby NPCs
checkForNearbyNPCs();
// Update camera based on mode
updateCamera(delta);
// Update OrbitControls if enabled
if (controls && controls.enabled) {
controls.update();
}
renderer.render(scene, camera);
}
function handlePlayerMovement(delta) {
if (!gameState.player) return;
const player = gameState.player;
let moving = false;
let moveDirection = new THREE.Vector3();
// Check if dialog is open - disable movement if dialog is active
const isDialogOpen = !document.getElementById('dialog-box').classList.contains('translate-y-full');
if (isDialogOpen) {
// Stop any movement and reset animation
if (player.isMoving) {
player.isMoving = false;
updatePlayerAnimation();
}
return;
}
// Forward/backward movement
if (gameState.keys.w) {
moveDirection.z = -1;
moving = true;
}
if (gameState.keys.s) {
moveDirection.z = 1;
moving = true;
}
// Left/right movement
if (gameState.keys.a) {
moveDirection.x = -1;
moving = true;
}
if (gameState.keys.d) {
moveDirection.x = 1;
moving = true;
}
// Normalize direction vector if moving diagonally
if (moving) {
moveDirection.normalize();
// Calculate speed with delta time for consistent movement
const speed = (gameState.keys.shift ? player.runSpeed : player.speed) * delta * 60;
// Apply movement relative to camera direction
const cameraForward = new THREE.Vector3();
camera.getWorldDirection(cameraForward);
cameraForward.y = 0;
cameraForward.normalize();
const cameraRight = new THREE.Vector3();
cameraRight.crossVectors(cameraForward, new THREE.Vector3(0, 1, 0));
cameraRight.normalize();
const moveVector = new THREE.Vector3();
moveVector.addScaledVector(cameraForward, moveDirection.z * speed);
moveVector.addScaledVector(cameraRight, moveDirection.x * speed);
player.model.position.add(moveVector);
// Update player rotation to face movement direction
if (moveDirection.length() > 0.1) {
const targetRotation = Math.atan2(moveDirection.x, moveDirection.z) + camera.rotation.y;
player.model.rotation.y = THREE.MathUtils.lerp(player.model.rotation.y, targetRotation, player.rotationSpeed * delta * 60);
}
}
// Jumping - only trigger when not showing dialog and not already jumping
if (gameState.keys.space && !gameState.isJumping && !isDialogOpen) {
gameState.isJumping = true;
setPlayerAnimation('jump');
// Create a more robust jump animation
let jumpStartTime = performance.now();
const jumpDuration = 1000; // 1 second
const originalY = player.model.position.y;
const jumpHeight = 1.5;
function performJump() {
const currentTime = performance.now();
const elapsed = currentTime - jumpStartTime;
const progress = Math.min(elapsed / jumpDuration, 1);
// Parabolic jump curve
const jumpProgress = 1 - Math.pow(2 * progress - 1, 2);
player.model.position.y = originalY + jumpHeight * jumpProgress;
if (progress < 1) {
requestAnimationFrame(performJump);
} else {
gameState.isJumping = false;
player.model.position.y = originalY;
updatePlayerAnimation();
}
}
performJump();
}
// Update animation based on movement
if (moving !== player.isMoving || gameState.keys.shift) {
player.isMoving = moving;
updatePlayerAnimation();
}
// Update camera position based on camera mode
updateCamera(delta);
}
function updateCamera(delta) {
if (!gameState.player) return;
const player = gameState.player;
switch (gameState.cameraMode) {
case 'follow':
// Follow camera - smooth follow behind player
const targetCameraPosition = player.model.position.clone().add(new THREE.Vector3(0, 3, 8));
camera.position.lerp(targetCameraPosition, 0.1 * delta * 60);
camera.lookAt(player.model.position);
break;
case 'orbit':
// Orbit camera - let OrbitControls handle positioning
// Set the target to player position
controls.target.copy(player.model.position);
break;
case 'first-person':
// First-person camera - attach to player's head
const headPosition = player.model.position.clone();
headPosition.y += 1.2; // Eye level
camera.position.copy(headPosition);
// Make camera face same direction as player
camera.rotation.y = player.model.rotation.y;
break;
}
}
function cycleCameraMode() {
const modes = ['follow', 'orbit', 'first-person'];
const currentIndex = modes.indexOf(gameState.cameraMode);
const nextIndex = (currentIndex + 1) % modes.length;
gameState.cameraMode = modes[nextIndex];
// Update UI display
document.getElementById('camera-mode-display').textContent =
gameState.cameraMode.charAt(0).toUpperCase() + gameState.cameraMode.slice(1);
// Enable/disable OrbitControls based on mode
if (controls && typeof controls.enabled !== 'undefined') {
controls.enabled = (gameState.cameraMode === 'orbit');
}
// Reset camera position for first-person mode
if (gameState.cameraMode === 'first-person') {
camera.rotation.set(0, 0, 0);
}
}
function updatePlayerAnimation() {
if (gameState.isJumping) return;
if (!gameState.player.isMoving) {
setPlayerAnimation('idle');
} else if (gameState.keys.shift) {
setPlayerAnimation('run');
} else {
setPlayerAnimation('walk');
}
}
function checkForNearbyNPCs() {
if (!gameState.player) return;
let closestNpc = null;
let closestDistance = Infinity;
gameState.npcs.forEach(npc => {
const distance = npc.model.position.distanceTo(gameState.player.model.position);
if (distance < 3 && distance < closestDistance) { // Reduced distance from 5 to 3
closestDistance = distance;
closestNpc = npc;
}
});
gameState.nearbyNpc = closestNpc;
// Add visual feedback when near NPC
const dialogBox = document.getElementById('dialog-box');
if (closestNpc && !dialogBox.classList.contains('translate-y-full')) {
// Dialog is already open, no need to add feedback
} else if (closestNpc) {
// Show talk prompt
const gameUI = document.getElementById('game-ui');
if (!gameUI.querySelector('.npc-talk-prompt')) {
const prompt = document.createElement('div');
prompt.className = 'npc-talk-prompt absolute bottom-20 left-4 bg-yellow-500 text-black px-3 py-2 rounded-lg font-bold';
prompt.textContent = 'Press SPACE to talk';
gameUI.appendChild(prompt);
}
} else {
// Remove talk prompt if no NPC nearby
const existingPrompt = document.getElementById('game-ui')?.querySelector('.npc-talk-prompt');
if (existingPrompt) {
existingPrompt.remove();
}
}
}
function showDialog(npc) {
if (!npc) return;
// Get next dialog line
const dialog = npc.dialog[npc.currentDialogIndex];
npc.currentDialogIndex = (npc.currentDialogIndex + 1) % npc.dialog.length;
// Update dialog UI
document.getElementById('dialog-character').style.backgroundColor = npc.character.color;
document.getElementById('dialog-name').textContent = npc.character.name;
document.getElementById('dialog-text').textContent = dialog;
// Show dialog box
document.getElementById('dialog-box').classList.remove('translate-y-full');
// Remove the talk prompt when dialog opens
const existingPrompt = document.getElementById('game-ui')?.querySelector('.npc-talk-prompt');
if (existingPrompt) {
existingPrompt.remove();
}
}
function closeDialog() {
document.getElementById('dialog-box').classList.add('translate-y-full');
// Remove the talk prompt when dialog closes
const existingPrompt = document.getElementById('game-ui')?.querySelector('.npc-talk-prompt');
if (existingPrompt) {
existingPrompt.remove();
}
}
function handleKeyDown(event) {
// Prevent default for space to avoid page scrolling
if (event.key === ' ') {
event.preventDefault();
}
// Ignore key repeats
if (event.repeat) return;
const key = event.key.toLowerCase();
switch (key) {
case 'w':
gameState.keys.w = true;
break;
case 'a':
gameState.keys.a = true;
break;
case 's':
gameState.keys.s = true;
break;
case 'd':
gameState.keys.d = true;
break;
case 'shift':
gameState.keys.shift = true;
break;
case ' ':
gameState.keys.space = true;
// Check if we're near an NPC and dialog isn't already open
const isDialogOpen = !document.getElementById('dialog-box').classList.contains('translate-y-full');
if (gameState.nearbyNpc && !gameState.isJumping && !isDialogOpen) {
showDialog(gameState.nearbyNpc);
}
break;
case 'c':
// Camera mode switching is handled separately
break;
case 'm':
// Music toggle is handled separately
break;
}
}
function handleKeyUp(event) {
// Ignore key repeats
if (event.repeat) return;
const key = event.key.toLowerCase();
switch (key) {
case 'w':
gameState.keys.w = false;
break;
case 'a':
gameState.keys.a = false;
break;
case 's':
gameState.keys.s = false;
break;
case 'd':
gameState.keys.d = false;
break;
case 'shift':
gameState.keys.shift = false;
break;
case ' ':
gameState.keys.space = false;
break;
}
// Update animation when keys are released
if (gameState.player) {
updatePlayerAnimation();
}
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=KBLLR/character-selector" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>