Spaces:
Running
Running
<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://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js"></script> | |
<style> | |
body { | |
margin: 0; | |
overflow: hidden; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
#canvas { | |
display: block; | |
} | |
.transition-all { | |
transition: all 0.3s ease; | |
} | |
.character-card:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); | |
} | |
.dialog-box { | |
background: rgba(0, 0, 0, 0.8); | |
backdrop-filter: blur(5px); | |
} | |
.character-preview { | |
width: 100%; | |
height: 200px; | |
background: rgba(255, 255, 255, 0.1); | |
border-radius: 0.5rem; | |
} | |
</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-gray-900 z-50 transition-all duration-500"> | |
<div class="text-center max-w-2xl p-8 bg-gray-800 rounded-xl shadow-2xl"> | |
<h1 class="text-5xl font-bold mb-6 bg-gradient-to-r from-purple-500 to-blue-500 bg-clip-text text-transparent">3D Character World</h1> | |
<p class="text-xl mb-8 text-gray-300">Explore a vibrant 3D world with interactive characters. Select your avatar and meet others along your journey!</p> | |
<button id="start-btn" class="px-8 py-3 bg-gradient-to-r from-purple-600 to-blue-600 rounded-full text-white font-bold text-lg hover:from-purple-700 hover:to-blue-700 transition-all transform hover:scale-105 shadow-lg"> | |
Begin Adventure | |
</button> | |
</div> | |
</div> | |
<!-- Character Selection Screen --> | |
<div id="character-selection" class="fixed inset-0 flex items-center justify-center bg-gray-900 z-40 transition-all duration-500 opacity-0 pointer-events-none"> | |
<div class="w-full max-w-6xl p-6"> | |
<h2 class="text-3xl font-bold mb-8 text-center">Choose Your Character</h2> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> | |
<!-- Character cards will be dynamically inserted here --> | |
</div> | |
<div class="text-center"> | |
<button id="confirm-character" class="px-8 py-3 bg-green-600 rounded-full text-white font-bold text-lg hover:bg-green-700 transition-all transform hover:scale-105 shadow-lg opacity-0"> | |
Confirm Selection | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- World Selection Screen --> | |
<div id="world-selection" class="fixed inset-0 flex items-center justify-center bg-gray-900 z-30 transition-all duration-500 opacity-0 pointer-events-none"> | |
<div class="w-full max-w-6xl p-6"> | |
<h2 class="text-3xl font-bold mb-8 text-center">Select 5 Characters for Your World</h2> | |
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-8"> | |
<!-- World character cards will be dynamically inserted here --> | |
</div> | |
<div class="text-center"> | |
<button id="start-world" class="px-8 py-3 bg-green-600 rounded-full text-white font-bold text-lg hover:bg-green-700 transition-all transform hover:scale-105 shadow-lg opacity-0"> | |
Generate World | |
</button> | |
</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> | |
</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, | |
keys: { | |
w: false, | |
a: false, | |
s: false, | |
d: false, | |
shift: false, | |
space: false | |
} | |
}; | |
// Sample character data (in a real app, these would be fetched from Google Cloud) | |
const characterData = [ | |
{ id: 1, name: "Warrior", modelUrl: "https://storage.googleapis.com/your-bucket-name/warrior.glb", color: "#EF4444", dialog: ["I fight for honor!", "The battlefield calls to me.", "Stay sharp!"] }, | |
{ id: 2, name: "Mage", modelUrl: "https://storage.googleapis.com/your-bucket-name/mage.glb", color: "#3B82F6", dialog: ["Magic flows through me.", "The arcane arts are limitless.", "Knowledge is power."] }, | |
{ id: 3, name: "Rogue", modelUrl: "https://storage.googleapis.com/your-bucket-name/rogue.glb", color: "#10B981", dialog: ["Shadows are my friends.", "Quick and quiet.", "Gold is always the answer."] }, | |
{ id: 4, name: "Archer", modelUrl: "https://storage.googleapis.com/your-bucket-name/archer.glb", color: "#F59E0B", dialog: ["My arrows never miss.", "The wind guides my shots.", "Aim true!"] }, | |
{ id: 5, name: "Cleric", modelUrl: "https://storage.googleapis.com/your-bucket-name/cleric.glb", color: "#8B5CF6", dialog: ["The light protects us.", "Healing is my calling.", "Have faith!"] }, | |
{ id: 6, name: "Bard", modelUrl: "https://storage.googleapis.com/your-bucket-name/bard.glb", color: "#EC4899", dialog: ["Let me sing you a tale!", "Music soothes the soul.", "Every story deserves a song."] }, | |
{ id: 7, name: "Monk", modelUrl: "https://storage.googleapis.com/your-bucket-name/monk.glb", color: "#6366F1", dialog: ["Inner peace is key.", "The body and mind are one.", "Discipline brings strength."] }, | |
{ id: 8, name: "Paladin", modelUrl: "https://storage.googleapis.com/your-bucket-name/paladin.glb", color: "#F97316", dialog: ["Justice will prevail!", "By my oath, I protect.", "Evil shall not pass."] } | |
]; | |
// Three.js variables | |
let scene, camera, renderer, controls; | |
let mixer, clock, loader; | |
let world; | |
// Initialize the app | |
document.addEventListener('DOMContentLoaded', () => { | |
// 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); | |
// Populate character selection | |
populateCharacterSelection(); | |
}); | |
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 < 5) { | |
alert('Please select 5 characters for your world!'); | |
return; | |
} | |
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" style="background-color: ${character.color};"></div> | |
</div> | |
<h3 class="text-xl font-bold text-center mb-2">${character.name}</h3> | |
<p class="text-gray-400 text-center">Click to select</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" style="background-color: ${character.color};"></div> | |
</div> | |
<h3 class="text-lg font-bold text-center mb-2">${character.name}</h3> | |
<p class="text-gray-400 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 5 | |
if (gameState.selectedWorldCharacters.length < 5) { | |
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 === 5) { | |
document.getElementById('start-world').classList.remove('opacity-0'); | |
} else { | |
document.getElementById('start-world').classList.add('opacity-0'); | |
} | |
}); | |
container.appendChild(card); | |
}); | |
} | |
function initThreeJS() { | |
// 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 | |
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
// Clock for animations | |
clock = new THREE.Clock(); | |
// Loader | |
loader = new THREE.GLTFLoader(); | |
// Add lights | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(5, 10, 7); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.mapSize.width = 2048; | |
directionalLight.shadow.mapSize.height = 2048; | |
scene.add(directionalLight); | |
// 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(); | |
// 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); | |
}); | |
} | |
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() { | |
// 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(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.1, | |
runSpeed: 0.2, | |
rotationSpeed: 0.05, | |
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); | |
// Create simple animations | |
createPlayerAnimations(); | |
// Set initial animation | |
setPlayerAnimation('idle'); | |
} | |
function createPlayerAnimations() { | |
// In a real app, these would come from the GLTF model | |
// For this example, we'll create simple animations | |
// Idle animation (slight bounce) | |
const idleTrack = new THREE.VectorKeyframeTrack( | |
'.position', | |
[0, 0.5, 1], | |
[ | |
0, 0, 0, // Start position | |
0, 0.05, 0, // Up position | |
0, 0, 0 // Back to start | |
] | |
); | |
const idleClip = new THREE.AnimationClip('idle', 1, [idleTrack]); | |
gameState.player.animations.idle = idleClip; | |
// Walk animation (arm and leg movement) | |
const leftArmTrack = new THREE.VectorKeyframeTrack( | |
'.children[2].rotation[z]', | |
[0, 0.5, 1], | |
[0.5, -0.5, 0.5] | |
); | |
const rightArmTrack = new THREE.VectorKeyframeTrack( | |
'.children[3].rotation[z]', | |
[0, 0.5, 1], | |
[-0.5, 0.5, -0.5] | |
); | |
const leftLegTrack = new THREE.VectorKeyframeTrack( | |
'.children[4].position[y]', | |
[0, 0.5, 1], | |
[-0.4, -0.2, -0.4] | |
); | |
const rightLegTrack = new THREE.VectorKeyframeTrack( | |
'.children[5].position[y]', | |
[0, 0.5, 1], | |
[-0.2, -0.4, -0.2] | |
); | |
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.VectorKeyframeTrack( | |
'.position[y]', | |
[0, 0.2, 0.4, 0.6, 0.8, 1], | |
[0, 2, 1.5, 0.5, 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.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); | |
// 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 = clock.getDelta(); | |
// Update player animation mixer | |
if (mixer) { | |
mixer.update(delta); | |
} | |
// Handle player movement | |
handlePlayerMovement(delta); | |
// Check for nearby NPCs | |
checkForNearbyNPCs(); | |
renderer.render(scene, camera); | |
} | |
function handlePlayerMovement(delta) { | |
if (!gameState.player) return; | |
const player = gameState.player; | |
let moving = false; | |
// Forward/backward movement | |
if (gameState.keys.w) { | |
player.model.translateZ(-player.speed * (gameState.keys.shift ? player.runSpeed / player.speed : 1)); | |
moving = true; | |
} | |
if (gameState.keys.s) { | |
player.model.translateZ(player.speed); | |
moving = true; | |
} | |
// Left/right movement | |
if (gameState.keys.a) { | |
player.model.translateX(-player.speed); | |
moving = true; | |
} | |
if (gameState.keys.d) { | |
player.model.translateX(player.speed); | |
moving = true; | |
} | |
// Rotation | |
if (moving) { | |
// Calculate target rotation based on movement direction | |
const targetRotation = Math.atan2( | |
(gameState.keys.a ? -1 : 0) + (gameState.keys.d ? 1 : 0), | |
(gameState.keys.w ? -1 : 0) + (gameState.keys.s ? 1 : 0) | |
); | |
// Smoothly rotate towards target | |
player.model.rotation.y = THREE.MathUtils.lerp( | |
player.model.rotation.y, | |
targetRotation, | |
player.rotationSpeed | |
); | |
} | |
// Jumping | |
if (gameState.keys.space && !gameState.isJumping) { | |
gameState.isJumping = true; | |
setPlayerAnimation('jump'); | |
setTimeout(() => { | |
gameState.isJumping = false; | |
updatePlayerAnimation(); | |
}, 1000); | |
// Check if we're near an NPC to talk | |
if (gameState.nearbyNpc) { | |
showDialog(gameState.nearbyNpc); | |
} | |
} | |
// Update animation based on movement | |
if (moving !== player.isMoving || gameState.keys.shift) { | |
player.isMoving = moving; | |
updatePlayerAnimation(); | |
} | |
// Update camera position to follow player | |
const cameraOffset = new THREE.Vector3(0, 5, 10); | |
cameraOffset.applyQuaternion(player.model.quaternion); | |
camera.position.copy(player.model.position.clone().add(cameraOffset)); | |
camera.lookAt(player.model.position); | |
} | |
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 < 5 && distance < closestDistance) { | |
closestDistance = distance; | |
closestNpc = npc; | |
} | |
}); | |
gameState.nearbyNpc = closestNpc; | |
} | |
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'); | |
} | |
function closeDialog() { | |
document.getElementById('dialog-box').classList.add('translate-y-full'); | |
} | |
function handleKeyDown(event) { | |
switch (event.key.toLowerCase()) { | |
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; | |
if (gameState.currentScreen === 'game' && gameState.nearbyNpc) { | |
showDialog(gameState.nearbyNpc); | |
} | |
break; | |
} | |
} | |
function handleKeyUp(event) { | |
switch (event.key.toLowerCase()) { | |
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; | |
} | |
} | |
</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> |