-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathIndex.html
More file actions
451 lines (400 loc) · 19.5 KB
/
Copy pathIndex.html
File metadata and controls
451 lines (400 loc) · 19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D ARPG Open World</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
overflow: hidden; /* Hide scrollbars */
font-family: 'Inter', sans-serif;
background-color: #1a202c; /* Dark background */
color: #e2e8f0; /* Light text */
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
#game-info {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 10px 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-size: 1rem;
z-index: 10;
}
#loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
font-size: 2rem;
z-index: 100;
}
#loading-bar-container {
width: 80%;
max-width: 400px;
height: 20px;
background-color: #333;
border-radius: 10px;
overflow: hidden;
margin-top: 20px;
}
#loading-bar {
height: 100%;
width: 0%;
background-color: #4CAF50;
border-radius: 10px;
transition: width 0.1s linear;
}
</style>
</head>
<body>
<div id="loading-overlay">
<p>Loading Open World...</p>
<div id="loading-bar-container">
<div id="loading-bar"></div>
</div>
<p id="loading-percentage">0%</p>
<p id="loading-status">Initializing scene...</p>
</div>
<div id="game-info" class="rounded-lg shadow-lg">
<p><strong>Movement:</strong> Arrow Keys or W, A, S, D</p>
<p><strong>Actions:</strong></p>
<ul>
<li><strong>Q:</strong> Attack (1H_Melee_Attack_Chop)</li>
<li><strong>E:</strong> Block</li>
<li><strong>R:</strong> Cheer</li>
<li><strong>F:</strong> Pickup</li>
<li><strong>Space:</strong> Jump</li>
<li><strong>P:</strong> Hit (simulated)</li>
<li><strong>O:</strong> Death (simulated)</li>
</ul>
<p><strong>Camera:</strong> Click and Drag Mouse</p>
<p class="mt-2 text-sm text-gray-400">Note: This character is a placeholder. See console for GLB integration steps.</p>
</div>
<script type="module">
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
// GLTFLoader will be needed when you integrate your model
import { GLTFLoader } from 'https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/loaders/GLTFLoader.js';
// --- Global Variables ---
let scene, camera, renderer, character;
let mixer, actions; // These will be used when GLB is loaded
let clock = new THREE.Clock();
let keys = {
w: false, s: false, a: false, d: false,
ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false,
q: false, e: false, r: false, f: false,
p: false, o: false,
Space: false
};
const movementSpeed = 3.0;
const rotationSpeed = 0.05;
const cameraOffset = new THREE.Vector3(0, 5, -10); // Relative to character
let currentAnimationState = 'Idle'; // Tracks the desired animation state
let activeAction = null; // The currently playing animation action
// Camera mouse controls
let isDragging = false;
let previousMouseX = 0;
let previousMouseY = 0;
const cameraRotationSpeed = 0.005;
// --- Animation Names (from user's list) ---
const ANIMATIONS = {
IDLE: 'Idle',
WALKING_FORWARD: 'Walking_A',
WALKING_BACKWARD: 'Walking_Backward',
STRAFE_LEFT: 'Running_Strafe_Left',
STRAFE_RIGHT: 'Running_Strafe_Right',
RUNNING: 'Running_A',
ATTACK_1H_CHOP: '1H_Melee_Attack_Chop',
BLOCK: 'Block',
CHEER: 'Cheer',
PICKUP: 'Pickup',
JUMP: 'Jump_Full_Short',
HIT: 'Hit_A',
DEATH: 'Death_A'
// Add more as needed from your list
};
// --- Loading Manager (Simulated for placeholder) ---
const loadingOverlay = document.getElementById('loading-overlay');
const loadingBar = document.getElementById('loading-bar');
const loadingPercentage = document.getElementById('loading-percentage');
const loadingStatus = document.getElementById('loading-status');
function simulateLoading() {
let progress = 0;
const interval = setInterval(() => {
progress += 10;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
loadingOverlay.style.display = 'none';
console.log('Game loaded successfully!');
// After loading, provide instructions for GLB integration
console.log("\n--- GLB Model Integration Instructions ---");
console.log("1. Host your 'Barbarian.glb' file locally using a simple HTTP server (e.g., Python's `http.server` or Node.js's `http-server`).");
console.log(" Example (Python): Navigate to your model's directory in terminal and run `python -m http.server 8000`");
console.log("2. Get the local URL for your model (e.g., `http://localhost:8000/Barbarian.glb`).");
console.log("3. In this code, uncomment the `GLTFLoader` import at the top.");
console.log("4. Uncomment the `loadBarbarianModel()` function call in `init()`.");
console.log("5. Set the `GLB_MODEL_URL` constant to your local URL.");
console.log("6. Comment out or remove the `createPlaceholderCharacter()` call in `init()`.");
console.log("7. The animations defined in `ANIMATIONS` object will then be used by the GLB model.");
console.log("-------------------------------------------\n");
}
loadingBar.style.width = progress + '%';
loadingPercentage.textContent = progress + '%';
loadingStatus.textContent = `Loading: ${progress}%`;
}, 50); // Simulate quick loading
}
// --- Game Initialization ---
function init() {
// Scene setup
scene = new THREE.Scene();
scene.background = new THREE.Color(0x282c34);
// Camera setup
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 10, 20);
// Renderer setup
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Lighting
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; // Higher resolution shadows
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -20;
directionalLight.shadow.camera.right = 20;
directionalLight.shadow.camera.top = 20;
directionalLight.shadow.camera.bottom = -20;
scene.add(directionalLight);
// Create the "Open World" terrain
createTerrainGrid(10, 10, 10); // 10x10 grid of 10x10 units each
// Create Placeholder Character
createPlaceholderCharacter();
// --- Uncomment this when you host your GLB model locally ---
// loadBarbarianModel();
// Initial camera positioning relative to character
updateCamera();
// Event Listeners
window.addEventListener('resize', onWindowResize);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
// Mouse events for camera rotation
renderer.domElement.addEventListener('mousedown', onMouseDown);
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('mouseup', onMouseUp);
renderer.domElement.addEventListener('mouseleave', onMouseUp); // Stop dragging if mouse leaves canvas
// Start the game loop
animate();
// Simulate loading completion
simulateLoading();
}
// --- Terrain Generation ---
function createTerrainGrid(gridX, gridZ, tileSize) {
const totalWidth = gridX * tileSize;
const totalDepth = gridZ * tileSize;
const startX = -totalWidth / 2;
const startZ = -totalDepth / 2;
for (let i = 0; i < gridX; i++) {
for (let j = 0; j < gridZ; j++) {
const planeGeometry = new THREE.PlaneGeometry(tileSize, tileSize, 32, 32); // Add segments for height variation
const planeMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.3, 0.5, 0.3 + Math.random() * 0.1), // Greenish tones
side: THREE.DoubleSide,
flatShading: true // Gives a blocky, low-poly feel
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true;
// Position each tile
plane.position.x = startX + (i * tileSize) + (tileSize / 2);
plane.position.z = startZ + (j * tileSize) + (tileSize / 2);
// Add simple height variation to vertices
const positionAttribute = planeGeometry.getAttribute('position');
for (let k = 0; k < positionAttribute.count; k++) {
const x = positionAttribute.getX(k);
const y = positionAttribute.getY(k);
// Simple noise based on position (can be replaced with a real noise function)
const height = (Math.sin(x * 0.5 + i * 0.1) * Math.cos(y * 0.5 + j * 0.1)) * 0.5; // Max height 0.5
positionAttribute.setZ(k, height); // Z is up for PlaneGeometry when rotated
}
planeGeometry.attributes.position.needsUpdate = true;
planeGeometry.computeVertexNormals(); // Recalculate normals for lighting
scene.add(plane);
}
}
}
// --- Placeholder Character ---
function createPlaceholderCharacter() {
const capsuleGeometry = new THREE.CapsuleGeometry(0.5, 2, 8, 16); // radius, height, radialSegments, capSegments
const capsuleMaterial = new THREE.MeshStandardMaterial({ color: 0x007bff }); // Blue color
character = new THREE.Mesh(capsuleGeometry, capsuleMaterial);
character.position.set(0, 1, 0); // Position above ground
character.castShadow = true;
scene.add(character);
}
// --- GLB Model Loading (Uncomment and configure when ready) ---
// const GLB_MODEL_URL = 'http://localhost:8000/Barbarian.glb'; // <--- SET YOUR LOCAL URL HERE
/*
function loadBarbarianModel() {
const loader = new GLTFLoader();
loadingStatus.textContent = 'Fetching Barbarian model...';
loader.load(
GLB_MODEL_URL,
function (gltf) {
loadingStatus.textContent = 'Parsing Barbarian model...';
character = gltf.scene;
character.scale.set(1.5, 1.5, 1.5); // Adjust scale if needed
character.position.set(0, 0, 0); // Start at origin
character.castShadow = true;
scene.add(character);
// Animation Mixer
mixer = new THREE.AnimationMixer(character);
actions = {};
console.log("--- Available Animations in GLB Model ---");
if (gltf.animations.length === 0) {
console.warn("No animations found in the GLB model.");
} else {
gltf.animations.forEach((clip) => {
const action = mixer.clipAction(clip);
actions[clip.name] = action;
console.log(`- ${clip.name}`);
});
console.log("---------------------------------------");
// Set initial animation to Idle
if (actions[ANIMATIONS.IDLE]) {
actions[ANIMATIONS.IDLE].play();
activeAction = actions[ANIMATIONS.IDLE];
currentAnimationState = ANIMATIONS.IDLE;
console.log(`Defaulting to animation: ${currentAnimationState}`);
} else {
console.warn(`Idle animation "${ANIMATIONS.IDLE}" not found. Character might not animate initially.`);
if (gltf.animations.length > 0) {
activeAction = actions[gltf.animations[0].name];
activeAction.play();
currentAnimationState = gltf.animations[0].name;
console.warn(`Playing first available animation: ${currentAnimationState}`);
}
}
}
updateCamera();
},
// onProgress callback (can be used with loadingManager if you re-enable it)
undefined,
function (error) {
console.error('An error occurred while loading the GLB model:', error);
loadingOverlay.innerHTML = '<p>Error loading character model. Please check the console.</p>';
loadingStatus.textContent = 'Model load failed!';
}
);
}
*/
// --- Event Handlers ---
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onKeyDown(event) {
const key = event.key;
if (keys.hasOwnProperty(key)) {
keys[key] = true;
} else if (keys.hasOwnProperty(event.code)) { // For keys like Spacebar
keys[event.code] = true;
}
// Trigger one-shot animations immediately
if (event.key.toLowerCase() === 'q' && character) setAction(ANIMATIONS.ATTACK_1H_CHOP);
if (event.key.toLowerCase() === 'r' && character) setAction(ANIMATIONS.CHEER);
if (event.key.toLowerCase() === 'f' && character) setAction(ANIMATIONS.PICKUP);
if (event.key.toLowerCase() === 'p' && character) setAction(ANIMATIONS.HIT);
if (event.key.toLowerCase() === 'o' && character) setAction(ANIMATIONS.DEATH);
if (event.code === 'Space' && character) setAction(ANIMATIONS.JUMP);
}
function onKeyUp(event) {
const key = event.key;
if (keys.hasOwnProperty(key)) {
keys[key] = false;
} else if (keys.hasOwnProperty(event.code)) {
keys[event.code] = false;
}
}
// Mouse events for camera rotation
function onMouseDown(event) {
isDragging = true;
previousMouseX = event.clientX;
previousMouseY = event.clientY;
}
function onMouseMove(event) {
if (!isDragging) return;
const deltaX = event.clientX - previousMouseX;
const deltaY = event.clientY - previousMouseY;
// Rotate character based on mouse X movement
if (character) {
character.rotation.y -= deltaX * cameraRotationSpeed;
}
// Adjust camera's vertical angle (pitch) based on mouse Y movement
// This is a simplified approach, a proper orbital camera would adjust camera's pitch directly
// For now, we'll let the character rotation handle most of it.
// You could add a separate camera pitch control here if needed.
previousMouseX = event.clientX;
previousMouseY = event.clientY;
}
function onMouseUp(event) {
isDragging = false;
}
// --- Animation Control ---
// This function will be used by both placeholder (simulated) and actual GLB animations
function setAction(animationName, fadeDuration = 0.2) {
// If GLB model is not loaded, just log the action
if (!mixer || !actions || !actions[animationName]) {
// console.log(`Simulating action: ${animationName}`); // For placeholder
// For actual GLB, if animation not found, warn
if (mixer && actions && !actions[animationName]) {
console.warn(`Animation "${animationName}" not found. Cannot play.`);
}
currentAnimationState = animationName; // Still update state for logic
return;
}
if (animationName === currentAnimationState) return;
const newAction = actions[animationName];
const previousAction = activeAction;
if (previousAction) {
previousAction.fadeOut(fadeDuration);
}
newAction.reset().fadeIn(fadeDuration).play();
activeAction = newAction;
currentAnimationState = animationName;
}
function updateAnimationState() {
// This logic determines which animation *should* be playing
// It will be called every frame in the animate loop
if (!character) return;
// Prioritize one-shot actions
if (currentAnimationState === ANIMATIONS.ATTACK_1H_CHOP && activeAction && !activeAction.isRunning()) {
// Attack finished, revert to movement/idle
currentAnimationState = 'Idle'; // Will be overridden by movement keys
}
if (currentAnimationState === ANIMATIONS.CHEER && activeAction && !activeAction.isRunning()) {
currentAnimationState = 'Idle';
}
if (currentAnimationState === ANIMATIONS.PICKUP && activeAction &