Add game source, converter pipeline, and web port
No files matched your search
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wackelpeter</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
|
||||
canvas { display: block; }
|
||||
#ui-overlay {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
pointer-events: none; font-family: monospace; z-index: 10;
|
||||
}
|
||||
#health-bar-bg {
|
||||
position: absolute; top: 16px; left: 16px; width: 250px; height: 20px;
|
||||
background: rgba(0,0,0,0.6); border: 2px solid #555;
|
||||
}
|
||||
#health-bar {
|
||||
position: absolute; top: 16px; left: 16px; width: 250px; height: 20px;
|
||||
background: #cc0000;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
#health-text {
|
||||
position: absolute; top: 16px; left: 16px; width: 250px; height: 20px;
|
||||
text-align: center; line-height: 20px; color: #fff; font-size: 12px;
|
||||
}
|
||||
#wave-label {
|
||||
position: absolute; top: 48px; left: 16px; color: #ffcc00; font-size: 18px;
|
||||
font-family: monospace; text-shadow: 2px 2px 4px #000;
|
||||
}
|
||||
#kills-label {
|
||||
position: absolute; top: 72px; left: 16px; color: #ffcc00; font-size: 16px;
|
||||
font-family: monospace; text-shadow: 2px 2px 4px #000;
|
||||
}
|
||||
.cooldown-container {
|
||||
position: absolute; left: 16px; width: 200px; height: 16px;
|
||||
}
|
||||
.cooldown-bg {
|
||||
width: 100%; height: 100%; background: rgba(0,0,0,0.6); border: 1px solid #555;
|
||||
}
|
||||
.cooldown-fill {
|
||||
height: 100%; background: #2266dd; transition: width 0.05s linear;
|
||||
}
|
||||
.cooldown-label {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
text-align: left; line-height: 16px; color: #fff; font-size: 11px;
|
||||
padding-left: 4px; font-family: monospace;
|
||||
}
|
||||
#fireball-cooldown { bottom: 72px; }
|
||||
#sword-cooldown { bottom: 48px; }
|
||||
#teleport-cooldown { bottom: 24px; }
|
||||
#wave-bar-container {
|
||||
position: absolute; top: 16px; right: 16px; width: 250px; height: 16px;
|
||||
}
|
||||
#wave-bar-bg {
|
||||
width: 100%; height: 100%; background: rgba(0,0,0,0.6); border: 1px solid #555;
|
||||
}
|
||||
#wave-bar-fill {
|
||||
height: 100%; background: #ccaa00; transition: width 0.1s linear;
|
||||
}
|
||||
#wave-bar-label {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
text-align: center; line-height: 16px; color: #fff; font-size: 11px;
|
||||
font-family: monospace;
|
||||
}
|
||||
#game-over {
|
||||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
font-size: 100px; color: #fff; font-family: monospace; opacity: 0;
|
||||
text-shadow: 4px 4px 8px #000;
|
||||
transition: opacity 2s ease-in;
|
||||
}
|
||||
#instructions {
|
||||
position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
|
||||
color: rgba(255,255,255,0.5); font-size: 11px; font-family: monospace;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="ui-overlay">
|
||||
<div id="health-bar-bg"></div>
|
||||
<div id="health-bar"></div>
|
||||
<div id="health-text">100%</div>
|
||||
<div id="wave-label">Wave 0</div>
|
||||
<div id="kills-label">Kills: 0</div>
|
||||
|
||||
<div id="fireball-cooldown" class="cooldown-container">
|
||||
<div class="cooldown-bg"></div>
|
||||
<div class="cooldown-fill"></div>
|
||||
<div class="cooldown-label">Fireball</div>
|
||||
</div>
|
||||
<div id="sword-cooldown" class="cooldown-container">
|
||||
<div class="cooldown-bg"></div>
|
||||
<div class="cooldown-fill"></div>
|
||||
<div class="cooldown-label">Sword</div>
|
||||
</div>
|
||||
<div id="teleport-cooldown" class="cooldown-container">
|
||||
<div class="cooldown-bg"></div>
|
||||
<div class="cooldown-fill"></div>
|
||||
<div class="cooldown-label">Teleport</div>
|
||||
</div>
|
||||
|
||||
<div id="wave-bar-container">
|
||||
<div id="wave-bar-bg"></div>
|
||||
<div id="wave-bar-fill"></div>
|
||||
<div id="wave-bar-label">Next Wave...</div>
|
||||
</div>
|
||||
|
||||
<div id="game-over">Verloren!</div>
|
||||
<div id="instructions">
|
||||
WASD: Move · Space: Jump · Mouse: Aim · Left Click: Fireball · Right Click: Sword · M3: Swap hands · E: Teleport
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "wackelpeter",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"three": "^0.170.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/three": "^0.170.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"name":"Material","vertexCount":86,"positions":[1.0,-1.0,-1.0,1.0,-1.0,1.0,-1.0,-1.0,1.0,-1.0,-1.0,-1.0,1.0,1.0,-0.9999989867210388,-1.0,1.0,-1.0,-1.0,2.7634100914001465,-1.0,1.0,2.7634100914001465,-0.9999989867210388,1.0,-1.0,-1.0,1.0,1.0,-0.9999989867210388,0.9999989867210388,1.0,1.0000009536743164,1.0,-1.0,1.0,1.0,-1.0,1.0,0.9999989867210388,1.0,1.0000009536743164,-1.0,1.0,1.0,-1.0,-1.0,1.0,-1.0,-1.0,1.0,-1.0,1.0,1.0,-1.0,1.0,-1.0,-1.0,-1.0,-1.0,1.0,1.0,-0.9999989867210388,1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,1.0,-1.0,0.9999989867210388,2.7634100914001465,1.0000009536743164,1.0,2.7634100914001465,-0.9999989867210388,1.0,4.550975799560547,-0.9999989867210388,0.9999989867210388,4.550975799560547,1.0000009536743164,-1.0,1.0,1.0,0.9999989867210388,1.0,1.0000009536743164,0.9999989867210388,2.7634100914001465,1.0000009536743164,-1.0,2.7634100914001465,1.0,-1.0,2.7634100914001465,-1.0,-1.0,1.0,-1.0,-2.6426289081573486,1.0,-1.0,-2.6426289081573486,2.7634100914001465,-1.0,1.0,2.7634100914001465,-0.9999989867210388,0.9999989867210388,2.7634100914001465,1.0000009536743164,2.715096950531006,2.7634100914001465,1.0000020265579224,2.7150979042053223,2.7634100914001465,-0.9999989867210388,1.0,4.550975799560547,-0.9999989867210388,-1.0,4.550975799560547,-1.0,-1.0,4.550975799560547,1.0,0.9999989867210388,4.550975799560547,1.0000009536743164,-1.0,4.550975799560547,-1.0,1.0,4.550975799560547,-0.9999989867210388,-1.0,2.7634100914001465,1.0,0.9999989867210388,2.7634100914001465,1.0000009536743164,0.9999989867210388,4.550975799560547,1.0000009536743164,-1.0,4.550975799560547,1.0,-1.0,2.7634100914001465,-1.0,-1.0,2.7634100914001465,1.0,-1.0,4.550975799560547,1.0,-1.0,4.550975799560547,-1.0,2.715096950531006,1.0,1.0000020265579224,2.7150979042053223,1.0,-0.9999989867210388,2.7150979042053223,2.7634100914001465,-0.9999989867210388,2.715096950531006,2.7634100914001465,1.0000020265579224,1.0,1.0,-0.9999989867210388,1.0,2.7634100914001465,-0.9999989867210388,2.7150979042053223,2.7634100914001465,-0.9999989867210388,2.7150979042053223,1.0,-0.9999989867210388,0.9999989867210388,2.7634100914001465,1.0000009536743164,0.9999989867210388,1.0,1.0000009536743164,2.715096950531006,1.0,1.0000020265579224,2.715096950531006,2.7634100914001465,1.0000020265579224,0.9999989867210388,1.0,1.0000009536743164,1.0,1.0,-0.9999989867210388,2.7150979042053223,1.0,-0.9999989867210388,2.715096950531006,1.0,1.0000020265579224,-2.6426289081573486,1.0,-1.0,-2.6426289081573486,1.0,0.9999989867210388,-2.6426289081573486,2.7634100914001465,0.9999989867210388,-2.6426289081573486,2.7634100914001465,-1.0,-1.0,1.0,-1.0,-1.0,1.0,1.0,-2.6426289081573486,1.0,0.9999989867210388,-2.6426289081573486,1.0,-1.0,-1.0,2.7634100914001465,1.0,-1.0,2.7634100914001465,-1.0,-2.6426289081573486,2.7634100914001465,-1.0,-2.6426289081573486,2.7634100914001465,0.9999989867210388,-1.0,1.0,1.0,-1.0,2.7634100914001465,1.0,-2.6426289081573486,2.7634100914001465,0.9999989867210388,-2.6426289081573486,1.0,0.9999989867210388],"normals":[],"indices":[0,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],"hasBones":true,"boneWeights":[1.0,-1.0,-1.0,1.0,-1.0,1.0,-1.0,-1.0,1.0,-1.0,-1.0,-1.0,1.0,1.0,-0.9999989867210388,-1.0,1.0,-1.0,-1.0,2.7634100914001465,-1.0,1.0,2.7634100914001465,-0.9999989867210388,1.0,-1.0,-1.0,1.0,1.0,-0.9999989867210388,0.9999989867210388,1.0,1.0000009536743164,1.0,-1.0,1.0,1.0,-1.0,1.0,0.9999989867210388,1.0,1.0000009536743164,-1.0,1.0,1.0,-1.0,-1.0,1.0,-1.0,-1.0,1.0,-1.0,1.0,1.0,-1.0,1.0,-1.0,-1.0,-1.0,-1.0,1.0,1.0,-0.9999989867210388,1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,1.0,-1.0,0.9999989867210388,2.7634100914001465,1.0000009536743164,1.0,2.7634100914001465,-0.9999989867210388,1.0,4.550975799560547,-0.9999989867210388,0.9999989867210388,4.550975799560547,1.0000009536743164,-1.0,1.0,1.0,0.9999989867210388,1.0,1.0000009536743164,0.9999989867210388,2.7634100914001465,1.0000009536743164,-1.0,2.7634100914001465,1.0,-1.0,2.7634100914001465,-1.0,-1.0,1.0,-1.0,-2.6426289081573486,1.0,-1.0,-2.6426289081573486,2.7634100914001465,-1.0,1.0,2.7634100914001465,-0.9999989867210388,0.9999989867210388,2.7634100914001465,1.0000009536743164,2.715096950531006,2.7634100914001465,1.0000020265579224,2.7150979042053223,2.7634100914001465,-0.9999989867210388,1.0,4.550975799560547,-0.9999989867210388,-1.0,4.550975799560547,-1.0,-1.0,4.550975799560547,1.0,0.9999989867210388,4.550975799560547,1.0000009536743164,-1.0,4.550975799560547,-1.0,1.0,4.550975799560547,-0.9999989867210388,-1.0,2.7634100914001465,1.0,0.9999989867210388,2.7634100914001465,1.0000009536743164,0.9999989867210388,4.550975799560547,1.0000009536743164,-1.0,4.550975799560547,1.0,-1.0,2.7634100914001465,-1.0,-1.0,2.7634100914001465,1.0,-1.0,4.550975799560547,1.0,-1.0,4.550975799560547,-1.0,2.715096950531006,1.0,1.0000020265579224,2.7150979042053223,1.0,-0.9999989867210388,2.7150979042053223,2.7634100914001465,-0.9999989867210388,2.715096950531006,2.7634100914001465,1.00Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"skeletons":[{"bones":[{"children":[],"rotation":[0.0,0.0,0.0,1.0],"name":"","scale":[1.0,1.0,1.0],"id":0,"position":[0.0,0.0,0.0]},{"children":["Bone.003","Bone.002"],"rotation":[0.7071066,0.0,0.0,0.7071069],"name":"Wirbel","scale":[1.0,0.99999994,0.99999994],"id":1,"position":[0.0,-0.8358549,0.0]},{"children":["rechter Arm"],"rotation":[0.9365144,-0.24383433,0.24383426,-0.06348569],"name":"Bone.002","scale":[0.99999994,1.0,1.0],"id":2,"position":[0.0,2.4753697E-7,-2.3959198]},{"children":[],"rotation":[-0.0030139268,0.70168424,0.6234812,0.34482053],"name":"rechter Arm","scale":[1.0,0.99999994,0.99999976],"id":3,"position":[5.9604645E-8,8.940697E-8,-1.2441596]},{"children":["linker Arm"],"rotation":[0.9333788,0.24936482,-0.24936469,-0.0666214],"name":"Bone.003","scale":[0.99999994,0.99999994,0.99999994],"id":4,"position":[0.0,2.4753697E-7,-2.3959198]},{"children":[],"rotation":[0.003082243,0.7017231,0.61904705,-0.35264152],"name":"linker Arm","scale":[1.0000001,0.99999994,1.0],"id":5,"position":[1.7881393E-7,-8.940697E-8,-1.2335805]}],"roots":["Wirbel",""]}],"geometries":[{"boneIndices":[1,3,0,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,2,3,0,0,2,3,0,0,1,2,3,0,1,3,0,0,1,3,0,0,1,2,3,0,1,2,3,0,1,2,4,0,1,2,4,0,1,2,4,0,1,2,4,0,1,0,0,0,1,0,0,0,1,2,3,0,1,2,3,0,1,2,4,0,1,2,4,0,1,2,3,0,1,2,3,0,2,3,0,0,2,3,0,0,1,2,3,0,1,2,3,0,2,3,0,0,2,3,0,0,1,2,4,0,1,2,4,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,3,0,0,1,2,3,0,1,2,4,0,1,0,0,0,1,2,3,0,1,2,3,0,1,2,3,0,1,0,0,0,1,2,3,0,1,2,4,0,1,2,4,0,1,0,0,0,1,2,3,0,1,2,4,0,1,2,3,0,1,2,3,4,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,4,1,2,3,4,1,2,3,0,1,2,3,4,1,2,3,0,1,2,3,0,1,0,0,0,1,2,3,0,1,2,4,0,1,2,4,0,1,0,0,0,1,2,3,0,1,2,4,0,1,0,0,0,1,0,0,0,1,2,3,0,1,2,3,0,1,2,4,0,1,2,4,0,1,2,3,0,1,2,4,0,1,2,3,0,1,2,3,4,1,2,3,0,1,2,3,4,1,3,0,0,1,2,3,4,1,2,4,0,1,2,3,0,1,0,0,0,1,2,3,4,1,2,3,4,1,0,0,0,1,2,3,4,1,2,4,0,1,2,4,0,1,0,0,0,1,2,3,4,1,2,4,0,1,2,3,0,1,2,4,0,1,2,3,0,1,2,3,4,1,2,3,4,1,2,3,0,1,2,4,0,1,2,3,4,1,2,3,0,1,2,3,4,1,0,0,0,1,2,3,4,1,0,0,0,1,2,3,4,1,2,4,0,1,2,4,0,1,0,0,0,1,2,3,4,1,2,4,0,1,0,0,0,1,0,0,0,1,2,5,4,1,2,5,4,1,2,4,0,1,2,4,0,1,2,5,4,1,2,4,0,1,2,5,4,1,2,4,0,1,0,0,0,1,2,5,4,1,0,0,0,1,2,5,4,1,2,4,0,1,4,5,0,1,0,0,0,1,2,4,5,1,2,5,4,1,0,0,0,1,2,5,4,1,2,4,0,1,2,4,0,1,0,0,0,1,2,4,5,1,2,4,0,1,2,4,5,1,2,4,0,1,4,5,0,1,2,4,5,1,4,5,0,1,4,5,0,1,2,4,0,1,2,4,5,1,4,5,0,1,2,4,5,1,0,0,0,1,2,4,5,1,0,0,0,1,2,4,5,1,2,4,0,1,2,4,0,1,0,0,0,1,2,4,5,1,2,4,0,1,2,4,0,1,2,4,0,1,0,0,0,1,0,0,0,1,2,4,5,1,4,5,0,1,2,4,0,1,4,5,0,1,2,4,5,1,4,5,0,1,2,4,5,1,4,5,0,1,2,4,5,1,5,0,0,1,2,4,5,1,2,4,5,1,2,4,5,1,4,5,0,1,4,5,0,1,4,5,0,1,0,0,0,1,4,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,4,5,0,1,2,4,0,1,4,5,0,1,2,4,5,1,4,5,0,1,4,5,0,1,4,5,0,1,2,4,0,1,2,4,5,1,4,5,0,1,4,5,0,1,4,5,0,1,4,5,0,1,4,5,0,1,4,5,0,1,0,0,0,1,4,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,4,5,0,1,5,0,0,1,4,5,0,1,4,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,4,5,0,1,2,4,0,1,4,5,0,1,4,5,0,1,4,5,0,1,4,5,0,1,4,5,0,1,4,5,0,1,4,5,0,1,4,5,0,4,5,0,0,4,5,0,0,1,4,5,0,1,4,5,0,1,5,0,0,1,4,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,4,5,0,1,2,4,0,1,4,5,0,4,5,0,0,1,4,5,0,1,4,5,0,1,2,4,0,4,5,0,0,1,4,5,0,4,5,0,0,1,4,5,0,4,5,0,0,1,5,0,0,1,4,5,0,1,5,0,0,1,4,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,5,0,0,1,5,0,0,1,4,5,0,1,4,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,4,5,0,1,2,4,0,1,4,5,0,4,5,0,0,1,4,5,0,4,5,0,0,1,4,5,0,4,5,0,0,1,4,5,0,1,4,5,0,4,5,0,0,4,5,0,0,1,5,0,0,1,4,5,0,1,5,0,0,1,4,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,4,5,0,1,2,4,5,1,4,5,0,1,4,5,0,1,4,5,0,1,4,5,0,1,2,4,5,1,2,4,5,1,4,5,0,1,4,5,0,1,5,0,0,4,5,0,0,1,5,0,0,1,4,5,0,1,5,0,0,1,4,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,5,0,0,1,5,0,0,1,5,0,0,1,4,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,4,5,0,1,2,4,5,1,4,5,0,1,2,4,5,1,5,0,0,1,2,4,5,1,5,0,0,4,5,0,0,1,2,4,5,1,2,5,4,1,3,5,0,1,5,0,0,1,3,4,5,1,5,0,0,1,3,4,5,1,0,0,0,1,3,4,5,1,2,4,0,1,2,4,0,1,0,0,0,1,3,5,0,1,2,4,5,1,3,5,0,1,3,4,5,1,3,5,0,1,2,4,0,1,2,4,5,1,3,5,0,1,2,5,4,1,3,5,0,1,2,5,4,1,3,5,0,1,5,3,4,1,5,0,0,1,3,4,5,1,0,0,0,1,2,4,0,1,0,0,0,1,3,5,0,1,3,5,0,1,5,3,4,1,5,3,4,1,0,0,0,1,3,4,5,1,2,4,0,1,2,4,0,1,0,0,0,1,3,5,0,1,2,4,5,1,3,5,0,1,2,5,4,1,3,5,0,1,2,5,4,1,3,5,0,1,3,5,0,1,2,3,4,1,2,3,4,1,3,5,0,1,5,3,4,1,3,5,0,1,5,3,4,1,0,0,0,1,3,5,0,1,2,4,0,1,2,4,0,1,0,0,0,1,3,5,0,1,2,4,0,1,0,0,0,1,0,0,0,1,3,5,0,1,3,5,0,1,2,4,0,1,2,3,4,1,3,5,0,1,2,3,4,1,3,5,0,1,2,3,4,1,3,5,0,1,2,3,5,1,3,0,0,1,2,3,5,1,2,4,0,1,3,0,0,1,3,0,0,1,2,3,5,1,2,3,5,1,3,0,0,1,2,3,5,1,2,4,0,1,2,4,0,1,0,0,0,1,3,5,0,1,2,3,4,1,3,5,0,1,2,3,4,1,3,5,0,1,2,3,4,1,3,0,0,1,3,0,0,1,2,3,4,1,2,3,4,1,3,0,0,1,2,3,0,1,3,0,0,1,2,3,0,1,3,0,0,1,2,3,5,1,2,4,0,1,2,4,0,1,0,0,0,1,3,5,0,1,2,3,4,1,3,0,0,1,0,0,0,1,2,3,0,1,2,3,0,1,2,4,0,1,2,3,4,1,3,0,0,1,2,3,4,1,3,0,0,1,2,3,4,1,3,0,0,2,3,0,0,1,3,0,0,1,3,0,0,1,2,4,0,1,3,0,0,1,3,0,0,2,3,0,0,1,2,3,0,1,3,0,0,1,2,3,0,1,2,4,0,1,2,4,0,1,0,0,0,1,2,3,0,1,2,3,4,1,2,3,0,1,2,3,4,1,2,3,0,1,2,3,0,1,2,3,4,1,2,3,0,1,2,3,0,1,2,3,0,2,3,0,0,1,2,3,0,2,3,0,0,1,3,0,0,1,2,3,0,1,3,0,0,1,2,3,0,1,2,4,0,1,2,4,0,1,0,0,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,4,0,1,2,4,0,1,3,0,0,1,0,0,0,1,2,3,0,1,2,4,0,1,2,3,0,2,3,0,0,1,2,3,0,2,3,0,0,1,2,3,0,2,3,0,0,1,3,0,0,1,2,4,0,1,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,0,0,2,0,Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"skeletons":[{"bones":[{"children":[],"rotation":[0.0,0.0,0.0,1.0],"name":"Bone","scale":[1.0,1.0,1.0],"id":0,"position":[0.0,0.0,0.0]}],"roots":["Bone"]}],"materials":[{"geometryName":"Material","materialDef":"Common/MatDefs/Misc/Unshaded.j3md","name":null,"params":[{"color":[1.0,0.0,0.0,1.0],"name":"Color","type":"Vector4"}]}],"geometries":[{"boneIndices":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"indices":[0,1,2,0,2,3,4,5,6,4,6,7,8,9,10,8,10,11,12,13,14,12,14,15,16,17,18,16,18,19,20,21,22,20,22,23,24,25,26,24,26,27,28,29,30,28,30,31,32,33,34,32,34,35,36,37,38,36,38,39,40,41,42,40,42,43,7,6,44,7,44,45,46,47,48,46,48,49,50,51,52,50,52,53,54,55,56,54,56,57,58,59,60,58,60,61,62,63,64,62,64,65,66,67,68,66,68,69,70,71,72,70,72,73,74,75,76,74,76,77,78,79,80,78,80,81,82,83,84,82,84,85],"boneWeights":[1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0],"name":"Material","elementCount":132,"positions":[1.0,-1.0,-1.0,1.0,-1.0,1.0,-1.0,-1.0,1.0,-1.0,-1.0,-1.0,1.0,1.0,-0.999999,-1.0,1.0,-1.0,-1.0,2.76341,-1.0,1.0,2.76341,-0.999999,1.0,-1.0,-1.0,1.0,1.0,-0.999999,0.999999,1.0,1.000001,1.0,-1.0,1.0,1.0,-1.0,1.0,0.999999,1.0,1.000001,-1.0,1.0,1.0,-1.0,-1.0,1.0,-1.0,-1.0,1.0,-1.0,1.0,1.0,-1.0,1.0,-1.0,-1.0,-1.0,-1.0,1.0,1.0,-0.999999,1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,1.0,-1.0,0.999999,2.76341,1.000001,1.0,2.76341,-0.999999,1.0,4.550976,-0.999999,0.999999,4.550976,1.000001,-1.0,1.0,1.0,0.999999,1.0,1.000001,0.999999,2.76341,1.000001,-1.0,2.76341,1.0,-1.0,2.76341,-1.0,-1.0,1.0,-1.0,-2.642629,1.0,-1.0,-2.642629,2.76341,-1.0,1.0,2.76341,-0.999999,0.999999,2.76341,1.000001,2.715097,2.76341,1.000002,2.715098,2.76341,-0.999999,1.0,4.550976,-0.999999,-1.0,4.550976,-1.0,-1.0,4.550976,1.0,0.999999,4.550976,1.000001,-1.0,4.550976,-1.0,1.0,4.550976,-0.999999,-1.0,2.76341,1.0,0.999999,2.76341,1.000001,0.999999,4.550976,1.000001,-1.0,4.550976,1.0,-1.0,2.76341,-1.0,-1.0,2.76341,1.0,-1.0,4.550976,1.0,-1.0,4.550976,-1.0,2.715097,1.0,1.000002,2.715098,1.0,-0.999999,2.715098,2.76341,-0.999999,2.715097,2.76341,1.000002,1.0,1.0,-0.999999,1.0,2.76341,-0.999999,2.715098,2.76341,-0.999999,2.715098,1.0,-0.999999,0.999999,2.76341,1.000001,0.999999,1.0,1.000001,2.715097,1.0,1.000002,2.715097,2.76341,1.000002,0.999999,1.0,1.000001,1.0,1.0,-0.999999,2.715098,1.0,-0.999999,2.715097,1.0,1.000002,-2.642629,1.0,-1.0,-2.642629,1.0,0.999999,-2.642629,2.76341,0.999999,-2.642629,2.76341,-1.0,-1.0,1.0,-1.0,-1.0,1.0,1.0,-2.642629,1.0,0.999999,-2.642629,1.0,-1.0,-1.0,2.76341,1.0,-1.0,2.76341,-1.0,-2.642629,2.76341,-1.0,-2.642629,2.76341,0.999999,-1.0,1.0,1.0,-1.0,2.76341,1.0,-2.642629,2.76341,0.999999,-2.642629,1.0,0.999999],"triangleCount":44,"normals":[0.0,-1.0,-0.0,0.0,-1.0,-0.0,0.0,-1.0,-0.0,0.0,-1.0,-0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,-0.0,-0.0,1.0,-0.0,-0.0,1.0,-0.0,-0.0,1.0,-0.0,-0.0,1.0,-1.0,-0.0,-0.0,-1.0,-0.0,-0.0,-1.0,-0.0,-0.0,-1.0,-0.0,-0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,1.0,0.0,1.0E-6,1.0,0.0,1.0E-6,1.0,0.0,1.0E-6,1.0,0.0Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"skeletons":[],"materials":[{"geometryName":"Sphere1","materialDef":"Common/MatDefs/Light/Lighting.j3md","name":null,"params":[{"color":[1.0,1.0,1.0,1.0],"name":"Diffuse","type":"Vector4"},{"name":"UseMaterialColors","type":"Boolean","value":"true"},{"color":[0.0,0.0,0.0,1.0],"name":"Ambient","type":"Vector4"},{"name":"ParallaxHeight","type":"Float","value":0.05},{"texture":"Models/helmet/helmet.png","name":"DiffuseMap","type":"Texture2D"},{"color":[1.0,1.0,1.0,1.0],"name":"Specular","type":"Vector4"},{"name":"Shininess","type":"Float","value":20.0}]}],"geometries":[{"texcoords":[0.14411664,0.7529861,0.12653771,0.69655085,0.13537128,0.6929093,0.14411664,0.7529861,0.13537128,0.6929093,0.14420484,0.6892678,0.14420484,0.6892678,0.17676124,0.7395287,0.771814,0.78618586,0.70640516,0.7919481,0.7025912,0.75364363,0.771814,0.78618586,0.7025912,0.75364363,0.7676858,0.74472547,0.31719738,0.5128529,0.29180443,0.42820445,0.34097123,0.40793604,0.31719738,0.5128529,0.34097123,0.40793604,0.38904735,0.4832336,0.70640516,0.7919481,0.6477201,0.80796605,0.644801,0.77864915,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.38904735,0.4832336,0.3822375,0.37407354,0.3822375,0.37407354,0.44935188,0.43374857,0.29260522,0.74052477,0.23466268,0.7158743,0.23740186,0.68654007,0.23740186,0.68654007,0.29618412,0.7021977,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.17676124,0.7395287,0.15161896,0.68318385,0.15161896,0.68318385,0.15903306,0.67709994,0.15903306,0.67709994,0.20416023,0.7170455,0.3577533,0.7490434,0.29618412,0.7021977,0.3616271,0.7075585,0.44935188,0.43374857,0.41193655,0.32962573,0.41193655,0.32962573,0.49275264,0.36879468,0.23877813,0.6526195,0.23877813,0.6526195,0.29798234,0.65787834,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.16436896,0.66911423,0.16436896,0.66911423,0.16970484,0.6611285,0.16970484,0.6611285,0.2238791,0.6875341,0.3616271,0.7075585,0.29798234,0.65787834,0.36357346,0.6595875,0.49275264,0.36879468,0.4274295,0.27854204,0.4274295,0.27854204,0.5153934,0.29414347,0.23866928,0.6171266,0.23866928,0.6171266,0.29784006,0.6115046,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.1724884,0.65195054,0.1724884,0.65195054,0.17527194,0.64277256,0.17527194,0.64277256,0.23416576,0.6536168,0.36357346,0.6595875,0.29784006,0.6115046,0.36341947,0.609393,0.5153934,0.29414347,0.4273398,0.2253615,0.4273398,0.2253615,0.51526225,0.21642795,0.23708488,0.5832151,0.23708488,0.5832151,0.29576996,0.56719714,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.17525584,0.6332179,0.17525584,0.6332179,0.1752397,0.6236632,0.1752397,0.6236632,0.23410618,0.61830723,0.36341947,0.609393,0.29576996,0.56719714,0.36117882,0.5614349,0.51526225,0.21642795,0.4116753,0.1748094,0.4116753,0.1748094,0.4923709,0.14255355,0.23416577,0.5538982,0.23416577,0.5538982,0.29195598,0.5288927,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.17242533,0.61458075,0.17242533,0.61458075,0.16961098,0.60549825,0.16961098,0.60549825,0.22370565,0.58474284,0.36117882,0.5614349,0.29195598,0.5288927,0.35705057,0.51997447,0.4923709,0.14255355,0.38182798,0.1313775,0.38182798,0.1313775,0.4487534,0.07908426,0.5738328,0.96527565,0.5158903,0.94062525,0.51862943,0.91129094,0.51862943,0.91129094,0.5774117,0.9269486,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.16424845,0.59769505,0.16424845,0.59769505,0.15888591,0.58989185,0.15888591,0.58989185,0.20388831,0.555906,0.63898087,0.97379434,0.5774117,0.9269486,0.64285463,0.9323094,0.4487534,0.07908426,0.34044978,0.09892495,0.34044978,0.09892495,0.38828534,0.03165971,0.5200057,0.8773704,0.5200057,0.8773704,0.57920986,0.8826292,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.15145169,0.58406126,0.15145169,0.58406126,0.14401749,0.5782307,0.14401749,0.5782307,0.17641504,0.53435886,0.64285463,0.9323094,0.57920986,0.8826292,0.644801,0.88433844,0.38828534,0.03165971,0.29121748,0.08033531,0.31633958,0.0044936663,0.51989686,0.8418775,0.51989686,0.8418775,0.5790676,0.83625555,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.13517214,0.57489073,0.13517214,0.57489073,0.1263268,0.57155085,0.1263268,0.57155085,0.14372692,0.52201617,0.5790676,0.83625555,0.644647,0.83414394,0.31633958,0.0044936663,0.23850545,0.0772603,0.23930879,0.0,0.51831245,0.80796605,0.51831245,0.80796605,0.5769975,0.791948,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.116856284,0.5709984,0.116856284,0.5709984,0.10738576,0.5704459,0.10738576,0.5704459,0.10872845,0.51997447,0.5769975,0.791948,0.64240634,0.7861858,0.23930879,0.0,0.18699749,0.08997318,0.18699749,0.08997318,0.16403753,0.018578041,0.5153934,0.77864915,0.5153934,0.77864915,0.57318354,0.75364363,0.0,0.0,0.0,0.0,0.0,0.0,0.09813157,0.57272995,0.09813157,0.57272995,0.08887737,0.575014,0.08887737,0.575014,0.07452942,0.5284153,0.64240634,0.7861858,0.57318354,0.75364363,0.6382781,0.74472547,0.16403753,0.018578041,0.14127024,0.11734438,0.09721393,0.05857698,0.44141868,0.77069074,0.38409,0.74030256,0.38754046,0.7180938,0.38754046,0.7180938,0.4459269,0.7416736,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.08066177,0.5799317,0.08066177,0.5799317,0.072446175,0.58484936,0.072446175,0.58484936,0.04Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"skeletons":[{"bones":[{"children":["Bone.012","Bone.001"],"rotation":[0.0,0.0,0.0,1.0],"name":"Bone","scale":[1.0,1.0,1.0],"id":0,"position":[0.0,0.0,0.0]},{"children":["Bone.002"],"rotation":[0.9703388,1.9406777E-6,-1.9406777E-6,0.24174897],"name":"Bone.001","scale":[1.0,1.0,1.0],"id":1,"position":[0.0,1.0,0.0]},{"children":["Bone.009","Bone.006","Bone.004"],"rotation":[-3.0000012E-6,0.8570753,-0.5151912,-1.6292068E-7],"name":"Bone.002","scale":[1.0,1.0,1.0],"id":2,"position":[0.0,0.611805,0.0]},{"children":["Bone.016","Bone.013","Bone.003"],"rotation":[0.0,0.0,0.0,1.0],"name":"Bone.004","scale":[1.0,1.0,1.0],"id":3,"position":[0.0,0.390448,0.0]},{"children":["Bone.025","Bone.021","Bone.019"],"rotation":[0.0,0.0,0.0,1.0],"name":"Bone.003","scale":[1.0,1.0,1.0],"id":4,"position":[0.0,0.159022,0.0]},{"children":["Bone.032","Bone.028","Bone.005"],"rotation":[0.0,0.0,0.0,1.0],"name":"Bone.019","scale":[1.0,1.0,1.0],"id":5,"position":[0.0,0.287669,0.0]},{"children":[],"rotation":[0.0,0.0,0.0,1.0],"name":"Bone.005","scale":[1.0,1.0,1.0],"id":6,"position":[0.0,0.287669,0.0]},{"children":["Bone.029"],"rotation":[-0.16956526,-0.11349029,0.56735617,0.7977936],"name":"Bone.028","scale":[1.0,1.0,1.0],"id":7,"position":[0.0,0.287669,0.0]},{"children":["Bone.030"],"rotation":[0.61746085,-0.02742953,-0.11152879,0.7781716],"name":"Bone.029","scale":[1.0,1.0,1.0],"id":8,"position":[0.0,0.431603,0.0]},{"children":[],"rotation":[-0.80308115,-0.07211706,0.20284036,0.5556219],"name":"Bone.030","scale":[1.0,1.0,1.0],"id":9,"position":[0.0,1.284803,0.0]},{"children":["Bone.033"],"rotation":[-0.17927358,0.095835894,-0.5993498,0.7742457],"name":"Bone.032","scale":[1.0,1.0,1.0],"id":10,"position":[0.0,0.287669,0.0]},{"children":["Bone.034"],"rotation":[0.599913,0.08037053,0.14626466,0.7824651],"name":"Bone.033","scale":[1.0,1.0,1.0],"id":11,"position":[0.0,0.49291,0.0]},{"children":[],"rotation":[-0.7990536,0.046228714,-0.20300917,0.5640599],"name":"Bone.034","scale":[1.0,1.0,1.0],"id":12,"position":[0.0,1.314559,0.0]},{"children":["Bone.022"],"rotation":[-0.121921144,-0.1362193,0.7325711,0.6556822],"name":"Bone.021","scale":[1.0,1.0,1.0],"id":13,"position":[0.0,0.287669,0.0]},{"children":["Bone.023"],"rotation":[0.63848716,-0.060342595,-0.17457584,0.74713874],"name":"Bone.022","scale":[1.0,1.0,1.0],"id":14,"position":[0.0,0.446815,0.0]},{"children":[],"rotation":[-0.855285,0.064981885,-0.1865495,0.47902426],"name":"Bone.023","scale":[1.0,1.0,1.0],"id":15,"position":[0.0,1.251543,0.0]},{"children":["Bone.026"],"rotation":[0.0,0.0,-0.72007334,0.69389796],"name":"Bone.025","scale":[1.0,1.0,1.0],"id":16,"position":[0.0,0.287669,0.0]},{"children":["Bone.027"],"rotation":[0.429594,0.063406214,0.14494634,0.88905525],"name":"Bone.026","scale":[1.0,1.0,1.0],"id":17,"position":[0.0,0.49762,0.0]},{"children":[],"rotation":[-0.8547853,-0.008971874,0.061981183,0.5151892],"name":"Bone.027","scale":[1.0,1.0,1.0],"id":18,"position":[0.0,1.168135,0.0]},{"children":["Bone.014"],"rotation":[-0.037134755,0.0,-0.72054154,0.6924168],"name":"Bone.013","scale":[1.0,1.0,1.0],"id":19,"position":[0.0,0.159022,0.0]},{"children":["Bone.015"],"rotation":[0.47752452,0.015384047,-0.040938295,0.8775293],"name":"Bone.014","scale":[1.0,1.0,1.0],"id":20,"position":[0.0,0.463771,0.0]},{"children":[],"rotation":[-0.8442589,0.027001427,-0.10735007,0.52437955],"name":"Bone.015","scale":[1.0,1.0,1.0],"id":21,"position":[0.0,1.269009,0.0]},{"children":["Bone.017"],"rotation":[-0.16728586,-0.12748486,0.7209073,0.6603452],"name":"Bone.016","scale":[1.0,1.0,1.0],"id":22,"position":[0.0,0.159022,0.0]},{"children":["Bone.018"],"rotation":[0.6549692,-0.029753415,0.009139916,0.75501424],"name":"Bone.017","scale":[1.0,1.0,1.0],"id":23,"position":[0.0,0.44574,0.0]},{"children":[],"rotation":[-0.8425496,-0.075653076,0.22669034,0.48269895],"name":"Bone.018","scale":[1.0,1.0,1.0],"id":24,"position":[0.0,1.359134,0.0]},{"children":["Bone.007"],"rotation":[-0.036876716,-0.054569732,0.8458627,0.52931917],"name":"Bone.006","scale":[1.0,1.0,1.0],"id":25,"position":[0.0,0.390448,0.0]},{"children":["Bone.008"],"rotation":[0.25945815,0.11208685,0.4688726,0.83682525],"name":"Bone.007","scale":[1.0,1.0,1.0],"id":26,"position":[0.0,1.047049,0.0]},{"children":[],"rotation":[-0.5501622,0.12160771,-0.1510263,0.8122341],"name":"Bone.008","scale":[1.0,1.0,1.0],"id":27,"position":[0.0,1.225283,0.0]},{"children":["Bone.010"],"rotation":[-0.049835812,0.0831541,-0.83484817,0.5418766],"name":"Bone.009","scale":[1.0,1.0,1.0],"id":28,"position":[0.0,0.390448,0.0]},{"children":["Bone.011"],"rotation":[0.34429535,-0.060006928,-0.42857024,0.8331791],"name":"Bone.010","scale":[1.0,1.0,1.0],"id":29,"position":[0.0,1.102366,0.0]},{"children":[],"rotation":[-0.56959426,0.01759631,-0.07266248,0.81851876],"name":"Bone.011","scale":[1.0,1.0,1.0],"id":30,"position":[0.0,1.253513,0.0]},{"children":[],"rotation":[-0.70707583,-0.0061754356,-0.0070402664,0.7070758],"name":"Bone.012","scale":[1.0,1.0,1.0],"id":31,"posiLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"skeletons":[{"bones":[{"children":[],"rotation":[0.0,0.0,0.0,1.0],"name":"Bone","scale":[1.0,1.0,1.0],"id":0,"position":[0.0,0.0,0.0]}],"roots":["Bone"]}],"materials":[{"geometryName":"staff","materialDef":"Common/MatDefs/Light/Lighting.j3md","name":null,"params":[{"color":[0.64000005,0.64000005,0.64000005,1.0],"name":"Diffuse","type":"Vector4"},{"name":"UseMaterialColors","type":"Boolean","value":"true"},{"color":[0.8,0.8,0.8,1.0],"name":"Ambient","type":"Vector4"},{"name":"ParallaxHeight","type":"Float","value":0.05},{"color":[0.0,0.0,0.0,1.0],"name":"GlowColor","type":"Vector4"},{"texture":"Models/staff/staff1.png","name":"DiffuseMap","type":"Texture2D"},{"color":[0.5,0.5,0.5,1.0],"name":"Specular","type":"Vector4"},{"name":"Shininess","type":"Float","value":12.5}]}],"geometries":[{"texcoords":[0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.197554,0.252954,0.232343,0.250394,0.233416,0.250426,0.233202,0.180791,0.23213,0.180759,0.251743,0.250452,0.252927,0.250443,0.253141,0.180744,0.251957,0.180753,0.247934,0.250441,0.249197,0.250451,0.249411,0.180752,0.248148,0.180742,0.209916,0.250429,0.210848,0.250422,0.210634,0.180702,0.209702,0.18071,0.206361,0.250448,0.207628,0.250443,0.207414,0.180723,0.206147,0.180728,0.202645,0.250455,0.20382,0.250454,0.203606,0.180735,0.202431,0.180735,0.224122,0.250454,0.225124,0.250455,0.225338,0.180735,0.224336,0.180734,0.220466,0.250447,0.221748,0.25045,0.221962,0.18073,0.22068,0.180726,0.216832,0.250432,0.217961,0.250437,0.218174,0.180717,0.217046,0.180712,0.238416,0.250434,0.239602,0.250406,0.239388,0.18077,0.238202,0.180799,0.234606,0.250446,0.235869,0.250455,0.235655,0.180819,0.234393,0.180811,0.23143,0.250352,0.232343,0.250394,0.23213,0.180759,0.231216,0.180717,0.250484,0.250455,0.251743,0.250452,0.251957,0.180753,0.250697,0.180756,0.246741,0.250425,0.247934,0.250441,0.248148,0.180742,0.246955,0.180727,0.208828,0.250437,0.209916,0.250429,0.209702,0.18071,0.208614,0.180717,0.205075,0.250452,0.206361,0.250448,0.206147,0.180728,0.204861,0.180732,0.201594,0.250454,0.202645,0.250455,0.202431,0.180735,0.20138,0.180734,0.222982,0.250453,0.224122,0.250454,0.224336,0.180734,0.223196,0.180733,0.219188,0.250442,0.220466,0.250447,0.22068,0.180726,0.219402,0.180722,0.215846,0.250426,0.216832,0.250432,0.217046,0.180712,0.216059,0.180706,0.237155,0.25045,0.238416,0.250434,0.238202,0.180799,0.236942,0.180815,0.233416,0.250426,0.234606,0.250446,0.234393,0.180811,0.233202,0.180791,0.252927,0.250443,0.25399,0.250428,0.254204,0.180729,0.253141,0.180744,0.249197,0.250451,0.250484,0.250455,0.250697,0.180756,0.249411,0.180752,0.245666,0.250404,0.246741,0.250425,0.246955,0.180727,0.24588,0.180706,0.207628,0.250443,0.208828,0.250437,0.208614,0.180717,0.207414,0.180723,0.20382,0.250454,0.205075,0.250452,0.204861,0.180732,0.203606,0.180735,0.200707,0.250451,0.201594,0.250454,0.20138,0.180734,0.200494,0.180732,0.221748,0.25045,0.222982,0.250453,0.223196,0.180733,0.221962,0.18073,0.217961,0.250437,0.219188,0.250442,0.219402,0.180722,0.218174,0.180717,0.239602,0.250406,0.240668,0.250367,0.240454,0.180732,0.239388,0.18077,0.235869,0.250455,0.237155,0.25045,0.236942,0.180815,0.235655,0.180819,0.235655,0.180819,0.236942,0.180815,0.236701,0.10237,0.235415,0.102374,0.23213,0.180759,0.233202,0.180791,0.232962,0.102345,0.231889,0.102313,0.253382,0.102227,0.252198,0.102236,0.249652,0.102235,0.248388,0.102225,0.209702,0.18071,0.210634,0.180702,0.210393,0.102162,0.209461,0.102169,0.206147,0.180728,0.207414,0.180723,0.207173,0.102183,0.205906,0.102188,0.202431,0.180735,0.203606,0.180735,0.203365,0.102194,0.20219,0.102195,0.224336,0.180734,0.225338,0.180735,0.225579,0.102193,0.224577,0.102193,0.22068,0.180726,0.221962,0.18073,0.222202,0.102189,0.220921,0.102185,0.217046,0.180712,0.218174,0.180717,0.218415,0.102176,0.217287,0.10217,0.239147,0.102325,0.237961,0.102353,0.235415,0.102374,0.234152,0.102366,0.231216,0.180717,0.23213,0.180759,0.231Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"skeletons":[],"geometries":[{"texcoords":[0.120075,0.879188,0.360224,0.879925,0.360961,0.639776,0.120812,0.639039,0.120812,0.158003,0.120075,0.398152,0.360224,0.398889,0.360961,0.15874,0.620443,0.399512,0.619706,0.159363,0.601111,0.15942,0.601848,0.399569,0.879188,0.399569,0.879925,0.15942,0.86133,0.159363,0.860593,0.399512,0.619706,0.879925,0.620443,0.639776,0.601848,0.639719,0.601111,0.879868,0.879188,0.879925,0.879925,0.639776,0.86133,0.639719,0.860593,0.879868],"indices":[0,1,2,0,2,3,4,5,6,4,6,7,8,9,10,8,10,11,12,13,14,12,14,15,16,17,18,16,18,19,20,21,22,20,22,23],"name":"tile-geom-1","elementCount":36,"positions":[1.0,0.0,-1.0,1.0,0.0,1.0,-1.0,0.0,1.0,-1.0,0.0,-1.0,-1.0,0.154864,1.0,1.0,0.154864,1.0,1.0,0.154864,-1.0,-1.0,0.154864,-1.0,1.0,0.0,-1.0,-1.0,0.0,-1.0,-1.0,0.154864,-1.0,1.0,0.154864,-1.0,1.0,0.0,1.0,1.0,0.0,-1.0,1.0,0.154864,-1.0,1.0,0.154864,1.0,-1.0,0.0,-1.0,-1.0,0.0,1.0,-1.0,0.154864,1.0,-1.0,0.154864,-1.0,-1.0,0.0,1.0,1.0,0.0,1.0,1.0,0.154864,1.0,-1.0,0.154864,1.0],"triangleCount":12,"normals":[0.0,-1.0,-0.0,0.0,-1.0,-0.0,0.0,-1.0,-0.0,0.0,-1.0,-0.0,0.0,1.0,-0.0,0.0,1.0,-0.0,0.0,1.0,-0.0,0.0,1.0,-0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,1.0,0.0,-0.0,1.0,0.0,-0.0,1.0,0.0,-0.0,1.0,0.0,-0.0,-1.0,-0.0,-0.0,-1.0,-0.0,-0.0,-1.0,-0.0,-0.0,-1.0,-0.0,-0.0,0.0,-0.0,1.0,0.0,-0.0,1.0,0.0,-0.0,1.0,0.0,-0.0,1.0],"vertexCount":24}],"animations":{}}
|
||||
@@ -0,0 +1 @@
|
||||
{"skeletons":[],"geometries":[{"texcoords":[0.120075,0.879188,0.360224,0.879925,0.360961,0.639776,0.120812,0.639039,0.120812,0.158003,0.120075,0.398152,0.360224,0.398889,0.360961,0.15874,0.620443,0.399512,0.619706,0.159363,0.601111,0.15942,0.601848,0.399569,0.879188,0.399569,0.879925,0.15942,0.86133,0.159363,0.860593,0.399512,0.619706,0.879925,0.620443,0.639776,0.601848,0.639719,0.601111,0.879868,0.879188,0.879925,0.879925,0.639776,0.86133,0.639719,0.860593,0.879868],"indices":[0,1,2,0,2,3,4,5,6,4,6,7,8,9,10,8,10,11,12,13,14,12,14,15,16,17,18,16,18,19,20,21,22,20,22,23],"name":"tile-geom-1","elementCount":36,"positions":[1.0,0.0,-1.0,1.0,0.0,1.0,-1.0,0.0,1.0,-1.0,0.0,-1.0,-1.0,0.154864,1.0,1.0,0.154864,1.0,1.0,0.154864,-1.0,-1.0,0.154864,-1.0,1.0,0.0,-1.0,-1.0,0.0,-1.0,-1.0,0.154864,-1.0,1.0,0.154864,-1.0,1.0,0.0,1.0,1.0,0.0,-1.0,1.0,0.154864,-1.0,1.0,0.154864,1.0,-1.0,0.0,-1.0,-1.0,0.0,1.0,-1.0,0.154864,1.0,-1.0,0.154864,-1.0,-1.0,0.0,1.0,1.0,0.0,1.0,1.0,0.154864,1.0,-1.0,0.154864,1.0],"triangleCount":12,"normals":[0.0,-1.0,-0.0,0.0,-1.0,-0.0,0.0,-1.0,-0.0,0.0,-1.0,-0.0,0.0,1.0,-0.0,0.0,1.0,-0.0,0.0,1.0,-0.0,0.0,1.0,-0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,1.0,0.0,-0.0,1.0,0.0,-0.0,1.0,0.0,-0.0,1.0,0.0,-0.0,-1.0,-0.0,-0.0,-1.0,-0.0,-0.0,-1.0,-0.0,-0.0,-1.0,-0.0,-0.0,0.0,-0.0,1.0,0.0,-0.0,1.0,0.0,-0.0,1.0,0.0,-0.0,1.0],"vertexCount":24}],"animations":{}}
|
||||
@@ -0,0 +1 @@
|
||||
{"skeletons":[],"geometries":[{"texcoords":[0.998999,0.998999,0.667668,0.998999,0.667668,0.667668,0.998999,0.667668,0.332332,0.998999,0.001001,0.998999,0.001001,0.667668,0.332332,0.667668,0.332332,0.665666,0.001001,0.665666,0.001001,0.334334,0.332332,0.334334,0.665666,0.998999,0.334334,0.998999,0.334334,0.667668,0.665666,0.667668,0.665666,0.665666,0.334334,0.665666,0.334334,0.334334,0.665666,0.334334,0.668438,0.666433,0.666901,0.335105,0.998229,0.333567,0.999766,0.664895],"indices":[0,1,2,0,2,3,4,5,6,4,6,7,8,9,10,8,10,11,12,13,14,12,14,15,16,17,18,16,18,19,20,21,22,20,22,23],"name":"wall1","elementCount":36,"positions":[-0.999999,1.991615,1.000001,-0.999999,-0.008385,1.000002,1.000001,-0.008385,0.999999,1.000001,1.991615,0.999998,-1.000002,1.991613,-0.999999,0.999999,1.991613,-1.000002,0.999999,-0.008386,-1.000001,-1.000001,-0.008387,-0.999998,-0.999999,1.991615,1.000001,-1.000002,1.991613,-0.999999,-1.000001,-0.008387,-0.999998,-0.999999,-0.008385,1.000002,-0.999999,-0.008385,1.000002,-1.000001,-0.008387,-0.999998,0.999999,-0.008386,-1.000001,1.000001,-0.008385,0.999999,1.000001,-0.008385,0.999999,0.999999,-0.008386,-1.000001,0.999999,1.991613,-1.000002,1.000001,1.991615,0.999998,-1.000002,1.991613,-0.999999,-0.999999,1.991615,1.000001,1.000001,1.991615,0.999998,0.999999,1.991613,-1.000002],"triangleCount":12,"normals":[1.0E-6,1.0E-6,1.0,1.0E-6,1.0E-6,1.0,1.0E-6,1.0E-6,1.0,1.0E-6,1.0E-6,1.0,-1.0E-6,-1.0E-6,-1.0,-1.0E-6,-1.0E-6,-1.0,-1.0E-6,-1.0E-6,-1.0,-1.0E-6,-1.0E-6,-1.0,-1.0,-0.0,1.0E-6,-1.0,-0.0,1.0E-6,-1.0,-0.0,1.0E-6,-1.0,-0.0,1.0E-6,0.0,-1.0,1.0E-6,0.0,-1.0,1.0E-6,0.0,-1.0,1.0E-6,0.0,-1.0,1.0E-6,1.0,0.0,-1.0E-6,1.0,0.0,-1.0E-6,1.0,0.0,-1.0E-6,1.0,0.0,-1.0E-6,-0.0,1.0,-1.0E-6,-0.0,1.0,-1.0E-6,-0.0,1.0,-1.0E-6,-0.0,1.0,-1.0E-6],"vertexCount":24}],"animations":{}}
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 333 KiB |
@@ -0,0 +1,50 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class Arena extends THREE.Group {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const tileSize = 10;
|
||||
const half = tileSize / 2;
|
||||
|
||||
for (let x = -45; x < 50; x += tileSize) {
|
||||
for (let z = -45; z < 50; z += tileSize) {
|
||||
const isDark = ((x / tileSize + z / tileSize) & 1) === 0;
|
||||
const color = isDark ? 0x668866 : 0x557755;
|
||||
|
||||
const tileGeom = new THREE.BoxGeometry(9.5, 0.3, 9.5);
|
||||
const tileMat = new THREE.MeshStandardMaterial({
|
||||
color,
|
||||
roughness: 0.7,
|
||||
metalness: 0.0,
|
||||
});
|
||||
|
||||
const tile = new THREE.Mesh(tileGeom, tileMat);
|
||||
tile.position.set(x, -0.15, z);
|
||||
tile.receiveShadow = true;
|
||||
this.add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
// Walls
|
||||
const wallGeom = new THREE.BoxGeometry(tileSize - 0.5, 2, 1);
|
||||
const wallMat = new THREE.MeshToonMaterial({ color: 0x776677 });
|
||||
|
||||
for (let coord = -50; coord <= 50; coord += tileSize) {
|
||||
this.createWall(coord, 49.5, wallGeom, wallMat);
|
||||
this.createWall(coord, -49.5, wallGeom, wallMat);
|
||||
}
|
||||
for (let coord = -50; coord <= 50; coord += tileSize) {
|
||||
this.createWall(49.5, coord, new THREE.BoxGeometry(1, 2, tileSize - 0.5), wallMat);
|
||||
this.createWall(-49.5, coord, new THREE.BoxGeometry(1, 2, tileSize - 0.5), wallMat);
|
||||
}
|
||||
}
|
||||
|
||||
private createWall(x: number, z: number, geom: THREE.BoxGeometry, mat: THREE.Material) {
|
||||
const wall = new THREE.Mesh(geom, mat);
|
||||
wall.position.set(x, 1, z);
|
||||
wall.castShadow = true;
|
||||
wall.receiveShadow = true;
|
||||
this.add(wall);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import * as THREE from 'three';
|
||||
import { loadGLB } from './MeshLoader';
|
||||
|
||||
let crossGeometry: THREE.BufferGeometry | null = null;
|
||||
let crossParticlesTemplate: THREE.Points | null = null;
|
||||
|
||||
export async function initCrossModel() {
|
||||
try {
|
||||
const gltf = await loadGLB('cross');
|
||||
gltf.scene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
crossGeometry = child.geometry;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
console.warn('Failed to load cross model');
|
||||
}
|
||||
}
|
||||
|
||||
function createGreenParticles(): THREE.Points {
|
||||
const count = 50;
|
||||
const tex = new THREE.TextureLoader().load('./textures/flame.png');
|
||||
tex.colorSpace = THREE.SRGBColorSpace;
|
||||
const geom = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(count * 3);
|
||||
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
const mat = new THREE.PointsMaterial({
|
||||
map: tex,
|
||||
color: 0x66ff66,
|
||||
size: 0.12,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
});
|
||||
const points = new THREE.Points(geom, mat);
|
||||
points.renderOrder = 999;
|
||||
points.userData = {
|
||||
velocities: [] as THREE.Vector3[],
|
||||
lifetimes: [] as number[],
|
||||
ages: [] as number[],
|
||||
timer: 0,
|
||||
};
|
||||
for (let i = 0; i < count; i++) {
|
||||
positions[i * 3] = (Math.random() - 0.5) * 0.8;
|
||||
positions[i * 3 + 1] = Math.random() * 1.2;
|
||||
positions[i * 3 + 2] = (Math.random() - 0.5) * 0.8;
|
||||
points.userData.velocities.push(new THREE.Vector3(
|
||||
(Math.random() - 0.5) * 0.5,
|
||||
1.5 + Math.random() * 1.5,
|
||||
(Math.random() - 0.5) * 0.5
|
||||
));
|
||||
points.userData.lifetimes.push(1 + Math.random());
|
||||
points.userData.ages.push(Math.random() * 2);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
export class Cross {
|
||||
body: THREE.Group;
|
||||
private particles: THREE.Points;
|
||||
|
||||
constructor(position: THREE.Vector3) {
|
||||
this.body = new THREE.Group();
|
||||
this.body.position.copy(position);
|
||||
this.body.position.y = 0.3;
|
||||
|
||||
if (crossGeometry) {
|
||||
// Original material: red cross (from j3o material extraction)
|
||||
const mesh = new THREE.Mesh(crossGeometry, new THREE.MeshToonMaterial({
|
||||
color: 0xff0000, emissive: 0x440000,
|
||||
}));
|
||||
mesh.castShadow = true;
|
||||
mesh.scale.set(0.25, 0.25, 0.25);
|
||||
this.body.add(mesh);
|
||||
} else {
|
||||
// Fallback cross
|
||||
const crossMaterial = new THREE.MeshToonMaterial({
|
||||
color: 0xff0000, emissive: 0x440000,
|
||||
});
|
||||
const vertical = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.8, 0.15), crossMaterial);
|
||||
vertical.position.y = 0.4;
|
||||
this.body.add(vertical);
|
||||
const horizontal = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.15, 0.15), crossMaterial);
|
||||
horizontal.position.y = 0.5;
|
||||
this.body.add(horizontal);
|
||||
}
|
||||
|
||||
// Green glow
|
||||
const glowGeom = new THREE.SphereGeometry(0.6, 16, 16);
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x00ff00, transparent: true, opacity: 0.15,
|
||||
});
|
||||
const glow = new THREE.Mesh(glowGeom, glowMat);
|
||||
this.body.add(glow);
|
||||
|
||||
// Upward-flying light green particles (like the original)
|
||||
this.particles = createGreenParticles();
|
||||
this.body.add(this.particles);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
const attr = this.particles.geometry.getAttribute('position') as THREE.BufferAttribute;
|
||||
const arr = attr.array as Float32Array;
|
||||
const ud = this.particles.userData;
|
||||
const count = ud.velocities.length;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
ud.ages[i] += dt;
|
||||
const life = ud.lifetimes[i];
|
||||
if (ud.ages[i] >= life) {
|
||||
// Respawn at bottom
|
||||
ud.ages[i] = 0;
|
||||
arr[i * 3] = (Math.random() - 0.5) * 0.8;
|
||||
arr[i * 3 + 1] = 0;
|
||||
arr[i * 3 + 2] = (Math.random() - 0.5) * 0.8;
|
||||
continue;
|
||||
}
|
||||
const v = ud.velocities[i];
|
||||
arr[i * 3] += v.x * dt;
|
||||
arr[i * 3 + 1] += v.y * dt;
|
||||
arr[i * 3 + 2] += v.z * dt;
|
||||
}
|
||||
attr.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import * as THREE from 'three';
|
||||
import type { Game } from './Game';
|
||||
import { playSound, playLoopingSound } from './SoundManager';
|
||||
import { loadGLB } from './MeshLoader';
|
||||
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { clone as cloneSkeleton } from 'three/examples/jsm/utils/SkeletonUtils.js';
|
||||
|
||||
let spiderGLTF: GLTF | null = null;
|
||||
const ANIMATIONS_ENABLED = true;
|
||||
export async function initSpiderModel() {
|
||||
try {
|
||||
spiderGLTF = await loadGLB('spider');
|
||||
} catch {
|
||||
console.warn('Failed to load spider model');
|
||||
}
|
||||
}
|
||||
|
||||
export class Enemy {
|
||||
body: THREE.Group;
|
||||
dead = false;
|
||||
deathTimer = 0;
|
||||
knockback = 0;
|
||||
health = 100;
|
||||
mixer: THREE.AnimationMixer | null = null;
|
||||
private currentAnim = '';
|
||||
private clips: Map<string, THREE.AnimationAction> = new Map();
|
||||
private stopWalkSound: (() => void) | null = null;
|
||||
|
||||
constructor(_playerPos: THREE.Vector3) {
|
||||
this.body = new THREE.Group();
|
||||
this.stopWalkSound = playLoopingSound('spiderwalking', 0.3);
|
||||
|
||||
if (spiderGLTF) {
|
||||
const model = cloneSkeleton(spiderGLTF.scene);
|
||||
// Original game scaled the spider model by 0.5 (WalkingEnemy.setupModel)
|
||||
model.scale.set(0.5, 0.5, 0.5);
|
||||
const spiderTexture = new THREE.TextureLoader().load('./textures/spider_animation2.png');
|
||||
spiderTexture.flipY = false;
|
||||
spiderTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true;
|
||||
child.material = new THREE.MeshToonMaterial({ map: spiderTexture });
|
||||
}
|
||||
});
|
||||
this.body.add(model);
|
||||
|
||||
if (spiderGLTF.animations.length > 0 && ANIMATIONS_ENABLED) {
|
||||
this.mixer = new THREE.AnimationMixer(model);
|
||||
for (const clip of spiderGLTF.animations) {
|
||||
const action = this.mixer.clipAction(clip);
|
||||
this.clips.set(clip.name, action);
|
||||
}
|
||||
this.playAnim('stand');
|
||||
}
|
||||
} else {
|
||||
// Fallback
|
||||
const abdomenGeom = new THREE.SphereGeometry(0.5, 8, 8);
|
||||
const bodyMat = new THREE.MeshToonMaterial({ color: 0x333333 });
|
||||
const abdomen = new THREE.Mesh(abdomenGeom, bodyMat);
|
||||
abdomen.scale.set(1, 0.6, 1.3);
|
||||
abdomen.position.y = 0.6;
|
||||
abdomen.castShadow = true;
|
||||
this.body.add(abdomen);
|
||||
|
||||
const cephalothoraxGeom = new THREE.SphereGeometry(0.3, 8, 8);
|
||||
const cephalothorax = new THREE.Mesh(cephalothoraxGeom, bodyMat);
|
||||
cephalothorax.position.y = 0.6;
|
||||
cephalothorax.position.z = 0.5;
|
||||
cephalothorax.castShadow = true;
|
||||
this.body.add(cephalothorax);
|
||||
|
||||
const eyeGeom = new THREE.SphereGeometry(0.06, 6, 6);
|
||||
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
|
||||
for (const sign of [-1, 1]) {
|
||||
const eye = new THREE.Mesh(eyeGeom, eyeMat);
|
||||
eye.position.set(sign * 0.12, 0.85, 0.7);
|
||||
this.body.add(eye);
|
||||
}
|
||||
|
||||
const legGeom = new THREE.CylinderGeometry(0.04, 0.04, 0.8, 4);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const leg = new THREE.Mesh(legGeom, new THREE.MeshToonMaterial({ color: 0x222222 }));
|
||||
const angle = (i / 8) * Math.PI * 2;
|
||||
const side = i < 4 ? 1 : -1;
|
||||
leg.position.set(Math.cos(angle) * 0.35, 0.3, Math.sin(angle) * 0.35);
|
||||
leg.rotation.z = side * 0.6;
|
||||
leg.rotation.x = angle;
|
||||
leg.castShadow = true;
|
||||
this.body.add(leg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playAnim(name: string, loop = true, speed = 1) {
|
||||
if (!this.clips.has(name) || this.currentAnim === name) return;
|
||||
// Stop the previous animation so it doesn't blend with the new one
|
||||
if (this.currentAnim) {
|
||||
const prev = this.clips.get(this.currentAnim);
|
||||
if (prev) prev.stop();
|
||||
}
|
||||
this.currentAnim = name;
|
||||
const action = this.clips.get(name)!;
|
||||
action.reset();
|
||||
action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, loop ? Infinity : 1);
|
||||
action.clampWhenFinished = !loop;
|
||||
action.setEffectiveTimeScale(speed);
|
||||
action.play();
|
||||
}
|
||||
|
||||
updateAnimation(dt: number) {
|
||||
if (this.mixer) this.mixer.update(dt);
|
||||
}
|
||||
|
||||
takeDamage(damage: number, game: Game) {
|
||||
if (this.dead) return;
|
||||
this.health -= damage;
|
||||
this.knockback = 0.5;
|
||||
|
||||
playSound('damage', 0.7);
|
||||
|
||||
if (this.health <= 0) {
|
||||
this.die(game);
|
||||
}
|
||||
}
|
||||
|
||||
private die(game: Game) {
|
||||
if (this.dead) return;
|
||||
this.dead = true;
|
||||
this.playAnim('die', false);
|
||||
if (this.stopWalkSound) {
|
||||
this.stopWalkSound();
|
||||
this.stopWalkSound = null;
|
||||
}
|
||||
game.removeEnemy(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import * as THREE from 'three';
|
||||
import { playSound } from './SoundManager';
|
||||
|
||||
const flameTexture = new THREE.TextureLoader().load('./textures/flame.png');
|
||||
flameTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
export class Fireball {
|
||||
body: THREE.Group;
|
||||
light: THREE.PointLight;
|
||||
direction: THREE.Vector3;
|
||||
lifetime = 1;
|
||||
exploding = false;
|
||||
timer = 0;
|
||||
damageDone = false;
|
||||
private trailParticles: THREE.Points;
|
||||
private trailVelocities: Float32Array;
|
||||
private trailLives: Float32Array;
|
||||
private trailMaxLife = 0.6;
|
||||
|
||||
constructor(start: THREE.Vector3, direction: THREE.Vector3) {
|
||||
this.direction = direction.clone();
|
||||
this.body = new THREE.Group();
|
||||
this.body.position.copy(start);
|
||||
|
||||
// Fireball core (additive glow)
|
||||
const coreGeom = new THREE.SphereGeometry(0.45, 16, 16);
|
||||
const coreMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xffcc44,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
});
|
||||
const core = new THREE.Mesh(coreGeom, coreMat);
|
||||
this.body.add(core);
|
||||
|
||||
const glowGeom = new THREE.SphereGeometry(0.75, 16, 16);
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xff6600, transparent: true, opacity: 0.45,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
});
|
||||
const glow = new THREE.Mesh(glowGeom, glowMat);
|
||||
this.body.add(glow);
|
||||
|
||||
// Sprite with flame texture
|
||||
const spriteMat = new THREE.SpriteMaterial({
|
||||
map: flameTexture,
|
||||
color: 0xffaa33,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
});
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
sprite.scale.set(2.2, 2.2, 1);
|
||||
this.body.add(sprite);
|
||||
|
||||
// Trail particles (flame sprites behind the fireball)
|
||||
const trailCount = 30;
|
||||
const trailGeom = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(trailCount * 3);
|
||||
trailGeom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
const trailMat = new THREE.PointsMaterial({
|
||||
map: flameTexture,
|
||||
color: 0xff8833,
|
||||
size: 1.0,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
});
|
||||
this.trailParticles = new THREE.Points(trailGeom, trailMat);
|
||||
this.body.add(this.trailParticles);
|
||||
this.trailVelocities = new Float32Array(trailCount * 3);
|
||||
this.trailLives = new Float32Array(trailCount).fill(0);
|
||||
|
||||
// Strong dynamic light (original: radius 100, orange, with shadow renderer)
|
||||
this.light = new THREE.PointLight(0xff7722, 80, 45, 1.5);
|
||||
this.light.position.set(0, 0.5, 0);
|
||||
this.light.castShadow = true;
|
||||
this.light.shadow.mapSize.width = 256;
|
||||
this.light.shadow.mapSize.height = 256;
|
||||
this.light.shadow.camera.near = 0.5;
|
||||
this.light.shadow.camera.far = 45;
|
||||
this.light.shadow.bias = -0.005;
|
||||
this.body.add(this.light);
|
||||
}
|
||||
|
||||
updateTrail(dt: number, pos: THREE.Vector3) {
|
||||
const attr = this.trailParticles.geometry.getAttribute('position') as THREE.BufferAttribute;
|
||||
const arr = attr.array as Float32Array;
|
||||
|
||||
for (let i = 0; i < this.trailLives.length; i++) {
|
||||
if (this.trailLives[i] > 0) {
|
||||
this.trailLives[i] -= dt;
|
||||
arr[i * 3] += this.trailVelocities[i * 3] * dt;
|
||||
arr[i * 3 + 1] += this.trailVelocities[i * 3 + 1] * dt;
|
||||
arr[i * 3 + 2] += this.trailVelocities[i * 3 + 2] * dt;
|
||||
if (this.trailLives[i] <= 0) {
|
||||
arr[i * 3] = pos.x;
|
||||
arr[i * 3 + 1] = pos.y;
|
||||
arr[i * 3 + 2] = pos.z;
|
||||
}
|
||||
} else {
|
||||
// Spawn new trail particle
|
||||
arr[i * 3] = pos.x;
|
||||
arr[i * 3 + 1] = pos.y;
|
||||
arr[i * 3 + 2] = pos.z;
|
||||
this.trailVelocities[i * 3] = (Math.random() - 0.5) * 1.5;
|
||||
this.trailVelocities[i * 3 + 1] = (Math.random() - 0.5) * 1.5;
|
||||
this.trailVelocities[i * 3 + 2] = (Math.random() - 0.5) * 1.5;
|
||||
this.trailLives[i] = this.trailMaxLife * (0.5 + Math.random());
|
||||
}
|
||||
}
|
||||
attr.needsUpdate = true;
|
||||
}
|
||||
|
||||
explode(scene: THREE.Scene) {
|
||||
if (this.exploding) return;
|
||||
this.exploding = true;
|
||||
this.timer = 0;
|
||||
this.damageDone = false;
|
||||
|
||||
playSound('explosion', 0.6);
|
||||
|
||||
// Hide the fireball core and trail
|
||||
for (const child of [...this.body.children]) {
|
||||
if (child instanceof THREE.Mesh || child instanceof THREE.Sprite || child instanceof THREE.Points) {
|
||||
child.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Bright flash
|
||||
const flashGeom = new THREE.SphereGeometry(3, 16, 16);
|
||||
const flashMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xffdd66, transparent: true, opacity: 0.9,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
});
|
||||
const flash = new THREE.Mesh(flashGeom, flashMat);
|
||||
flash.position.copy(this.body.position);
|
||||
scene.add(flash);
|
||||
|
||||
// Big explosion light pulse
|
||||
const flashLight = new THREE.PointLight(0xff8833, 200, 35, 1.5);
|
||||
flashLight.position.copy(this.body.position);
|
||||
scene.add(flashLight);
|
||||
|
||||
const startTime = performance.now();
|
||||
const updateFlash = () => {
|
||||
const elapsed = (performance.now() - startTime) / 1000;
|
||||
if (elapsed > 0.6) {
|
||||
flash.removeFromParent();
|
||||
flash.geometry.dispose();
|
||||
flashMat.dispose();
|
||||
scene.remove(flashLight);
|
||||
return;
|
||||
}
|
||||
const scale = 1 + elapsed * 8;
|
||||
flash.scale.set(scale, scale, scale);
|
||||
flashMat.opacity = 0.9 * (1 - elapsed / 0.6);
|
||||
flashLight.intensity = 200 * (1 - elapsed / 0.6);
|
||||
requestAnimationFrame(updateFlash);
|
||||
};
|
||||
updateFlash();
|
||||
|
||||
// Fire particle burst (original: 50 flame particles)
|
||||
this.spawnExplosionParticles(scene, './textures/flame.png', 0xffaa33, 60, 8, 1.5);
|
||||
|
||||
// Debris stones (original: stone particles with gravity)
|
||||
this.spawnExplosionParticles(scene, './textures/flame.png', 0x555544, 25, 6, 1.0, true);
|
||||
}
|
||||
|
||||
private spawnExplosionParticles(
|
||||
scene: THREE.Scene,
|
||||
texPath: string,
|
||||
color: number,
|
||||
count: number,
|
||||
speed: number,
|
||||
size: number,
|
||||
withGravity = false
|
||||
) {
|
||||
const tex = new THREE.TextureLoader().load(texPath);
|
||||
tex.colorSpace = THREE.SRGBColorSpace;
|
||||
const geom = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(count * 3);
|
||||
const velocities: THREE.Vector3[] = [];
|
||||
const lives: number[] = [];
|
||||
const maxLives: number[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
positions[i * 3] = this.body.position.x;
|
||||
positions[i * 3 + 1] = this.body.position.y;
|
||||
positions[i * 3 + 2] = this.body.position.z;
|
||||
velocities.push(new THREE.Vector3(
|
||||
(Math.random() - 0.5) * speed * 2,
|
||||
Math.random() * speed,
|
||||
(Math.random() - 0.5) * speed * 2
|
||||
));
|
||||
const life = 0.5 + Math.random() * 1.2;
|
||||
lives.push(life);
|
||||
maxLives.push(life);
|
||||
}
|
||||
|
||||
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
const mat = new THREE.PointsMaterial({
|
||||
map: tex,
|
||||
color,
|
||||
size,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
});
|
||||
const points = new THREE.Points(geom, mat);
|
||||
points.renderOrder = 999;
|
||||
scene.add(points);
|
||||
|
||||
const startTime = performance.now();
|
||||
const update = () => {
|
||||
const elapsed = (performance.now() - startTime) / 1000;
|
||||
if (elapsed > 2) {
|
||||
points.removeFromParent();
|
||||
geom.dispose();
|
||||
mat.dispose();
|
||||
tex.dispose();
|
||||
return;
|
||||
}
|
||||
const attr = geom.getAttribute('position') as THREE.BufferAttribute;
|
||||
const arr = attr.array as Float32Array;
|
||||
let allDead = true;
|
||||
for (let i = 0; i < count; i++) {
|
||||
lives[i] -= 0.016;
|
||||
if (lives[i] <= 0) continue;
|
||||
allDead = false;
|
||||
const v = velocities[i];
|
||||
if (withGravity) v.y -= 9.8 * 0.016;
|
||||
arr[i * 3] += v.x * 0.016;
|
||||
arr[i * 3 + 1] += v.y * 0.016;
|
||||
arr[i * 3 + 2] += v.z * 0.016;
|
||||
}
|
||||
attr.needsUpdate = true;
|
||||
mat.opacity = Math.min(1, elapsed / 2) * 0.9;
|
||||
if (allDead) {
|
||||
points.removeFromParent();
|
||||
geom.dispose();
|
||||
mat.dispose();
|
||||
tex.dispose();
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
import * as THREE from 'three';
|
||||
import { Player } from './Player';
|
||||
import { Enemy, initSpiderModel } from './Enemy';
|
||||
import { Sword } from './Sword';
|
||||
import { Staff } from './Staff';
|
||||
import { Fireball } from './Fireball';
|
||||
import { Cross, initCrossModel } from './Cross';
|
||||
import { Arena } from './Arena';
|
||||
import { initSounds, playSound, resumeContext } from './SoundManager';
|
||||
|
||||
const SPAWN_TIME = 10;
|
||||
|
||||
export class Game {
|
||||
scene: THREE.Scene;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
renderer: THREE.WebGLRenderer;
|
||||
clock: THREE.Clock;
|
||||
|
||||
player!: Player;
|
||||
enemies: Enemy[] = [];
|
||||
fireballs: Fireball[] = [];
|
||||
crosses: Cross[] = [];
|
||||
|
||||
keys: Set<string> = new Set();
|
||||
mouseButtons: Set<number> = new Set();
|
||||
private prevMouseButtons: Set<number> = new Set();
|
||||
mouseX = 0;
|
||||
mouseY = 0;
|
||||
groundPoint = new THREE.Vector3();
|
||||
|
||||
spawnTimer = 0;
|
||||
spawns = 0;
|
||||
killed = 0;
|
||||
gameOver = false;
|
||||
|
||||
// Debug hitzone visualization
|
||||
private showHitzones = false;
|
||||
private playerHitRing!: THREE.Mesh;
|
||||
private enemyHitRings: Map<Enemy, THREE.Mesh> = new Map();
|
||||
private fireballHitRings: Map<Fireball, THREE.Mesh> = new Map();
|
||||
private crossHitRings: Map<Cross, THREE.Mesh> = new Map();
|
||||
|
||||
// UI elements
|
||||
private uiHealthBar!: HTMLElement;
|
||||
private uiHealthText!: HTMLElement;
|
||||
private uiWaveLabel!: HTMLElement;
|
||||
private uiKillsLabel!: HTMLElement;
|
||||
private uiFireballCD!: HTMLElement;
|
||||
private uiSwordCD!: HTMLElement;
|
||||
private uiTeleportCD!: HTMLElement;
|
||||
private uiWaveBarFill!: HTMLElement;
|
||||
private uiWaveBarLabel!: HTMLElement;
|
||||
private uiGameOver!: HTMLElement;
|
||||
private mouseOnCanvas = false;
|
||||
|
||||
constructor() {
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x0d1117);
|
||||
this.scene.fog = new THREE.Fog(0x0d1117, 25, 70);
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
60, window.innerWidth / window.innerHeight, 1, 150
|
||||
);
|
||||
this.camera.position.set(0, 25, -15);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
this.renderer.shadowMap.enabled = true;
|
||||
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
document.body.prepend(this.renderer.domElement);
|
||||
|
||||
this.clock = new THREE.Clock();
|
||||
|
||||
this.setupUI();
|
||||
this.setupInput();
|
||||
this.setupLighting();
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.setupArena();
|
||||
this.setupPlayer();
|
||||
this.player.setGame(this);
|
||||
|
||||
initSounds();
|
||||
|
||||
await Promise.all([
|
||||
this.player.init(),
|
||||
this.player.rightHandWeapon.init(),
|
||||
this.player.leftHandWeapon.init(),
|
||||
initSpiderModel(),
|
||||
initCrossModel(),
|
||||
]);
|
||||
}
|
||||
|
||||
start() {
|
||||
window.addEventListener('resize', () => {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
this.animate();
|
||||
}
|
||||
|
||||
private setupUI() {
|
||||
this.uiHealthBar = document.getElementById('health-bar')!;
|
||||
this.uiHealthText = document.getElementById('health-text')!;
|
||||
this.uiWaveLabel = document.getElementById('wave-label')!;
|
||||
this.uiKillsLabel = document.getElementById('kills-label')!;
|
||||
this.uiFireballCD = document.querySelector('#fireball-cooldown .cooldown-fill')!;
|
||||
this.uiSwordCD = document.querySelector('#sword-cooldown .cooldown-fill')!;
|
||||
this.uiTeleportCD = document.querySelector('#teleport-cooldown .cooldown-fill')!;
|
||||
this.uiWaveBarFill = document.getElementById('wave-bar-fill')!;
|
||||
this.uiWaveBarLabel = document.getElementById('wave-bar-label')!;
|
||||
this.uiGameOver = document.getElementById('game-over')!;
|
||||
|
||||
const canvas = this.renderer.domElement;
|
||||
canvas.addEventListener('mouseenter', () => this.mouseOnCanvas = true);
|
||||
canvas.addEventListener('mouseleave', () => this.mouseOnCanvas = false);
|
||||
canvas.addEventListener('contextmenu', e => e.preventDefault());
|
||||
}
|
||||
|
||||
private setupInput() {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
this.keys.add(e.code);
|
||||
if (e.code === 'Space') {
|
||||
this.player.jump();
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.code === 'KeyH') {
|
||||
this.showHitzones = !this.showHitzones;
|
||||
}
|
||||
});
|
||||
window.addEventListener('keyup', (e) => {
|
||||
this.keys.delete(e.code);
|
||||
if (e.code === 'KeyE' && !this.gameOver) {
|
||||
this.player.leftHandWeapon.skill2(this.groundPoint, this.player);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('mousedown', (e) => {
|
||||
resumeContext();
|
||||
this.mouseButtons.add(e.button);
|
||||
this.prevMouseButtons.add(e.button);
|
||||
});
|
||||
window.addEventListener('mouseup', (e) => {
|
||||
resumeContext();
|
||||
this.mouseButtons.delete(e.button);
|
||||
this.handleMouseRelease(e.button);
|
||||
});
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
this.mouseX = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
this.mouseY = -(e.clientY / window.innerHeight) * 2 + 1;
|
||||
});
|
||||
}
|
||||
|
||||
private handleMouseRelease(button: number) {
|
||||
if (this.gameOver) return;
|
||||
if (button === 0) {
|
||||
this.player.leftHandWeapon.skill1(this.groundPoint, this.player);
|
||||
} else if (button === 1) {
|
||||
this.player.switchHands();
|
||||
} else if (button === 2) {
|
||||
this.player.rightHandWeapon.skill1(this.groundPoint, this.player);
|
||||
}
|
||||
}
|
||||
|
||||
private setupLighting() {
|
||||
const ambient = new THREE.AmbientLight(0x556688, 0.35);
|
||||
this.scene.add(ambient);
|
||||
|
||||
const dirLight = new THREE.DirectionalLight(0xccddff, 0.55);
|
||||
dirLight.position.set(30, 40, 20);
|
||||
dirLight.castShadow = true;
|
||||
dirLight.shadow.mapSize.width = 1024;
|
||||
dirLight.shadow.mapSize.height = 1024;
|
||||
dirLight.shadow.camera.near = 1;
|
||||
dirLight.shadow.camera.far = 150;
|
||||
dirLight.shadow.camera.left = -60;
|
||||
dirLight.shadow.camera.right = 60;
|
||||
dirLight.shadow.camera.top = 60;
|
||||
dirLight.shadow.camera.bottom = -60;
|
||||
this.scene.add(dirLight);
|
||||
}
|
||||
|
||||
private setupArena() {
|
||||
const arena = new Arena();
|
||||
this.scene.add(arena);
|
||||
}
|
||||
|
||||
private setupPlayer() {
|
||||
this.player = new Player();
|
||||
this.player.body.position.set(0, 0.5, 0);
|
||||
this.scene.add(this.player.body);
|
||||
|
||||
// Player hitzone ring (contact damage radius = 2)
|
||||
this.playerHitRing = this.createHitRing(2, 0xff0000);
|
||||
this.scene.add(this.playerHitRing);
|
||||
}
|
||||
|
||||
private createHitRing(radius: number, color: number): THREE.Mesh {
|
||||
const geom = new THREE.RingGeometry(radius - 0.05, radius, 48);
|
||||
const mat = new THREE.MeshBasicMaterial({
|
||||
color,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
const ring = new THREE.Mesh(geom, mat);
|
||||
ring.rotation.x = -Math.PI / 2;
|
||||
ring.position.y = 0.05;
|
||||
ring.renderOrder = 999;
|
||||
return ring;
|
||||
}
|
||||
|
||||
private updateHitzones() {
|
||||
this.playerHitRing.position.x = this.player.body.position.x;
|
||||
this.playerHitRing.position.z = this.player.body.position.z;
|
||||
this.playerHitRing.visible = this.showHitzones;
|
||||
|
||||
// Enemy rings
|
||||
for (const enemy of this.enemies) {
|
||||
let ring = this.enemyHitRings.get(enemy);
|
||||
if (!ring) {
|
||||
ring = this.createHitRing(2, 0xff8800);
|
||||
this.scene.add(ring);
|
||||
this.enemyHitRings.set(enemy, ring);
|
||||
}
|
||||
ring.position.x = enemy.body.position.x;
|
||||
ring.position.z = enemy.body.position.z;
|
||||
ring.visible = this.showHitzones && !enemy.dead;
|
||||
}
|
||||
// Clean up removed enemies
|
||||
for (const [enemy, ring] of this.enemyHitRings) {
|
||||
if (!this.enemies.includes(enemy)) {
|
||||
this.scene.remove(ring);
|
||||
ring.geometry.dispose();
|
||||
(ring.material as THREE.Material).dispose();
|
||||
this.enemyHitRings.delete(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
// Fireball rings (impact radius 1.5 / AoE 5)
|
||||
for (const fb of this.fireballs) {
|
||||
let ring = this.fireballHitRings.get(fb);
|
||||
if (!ring) {
|
||||
ring = this.createHitRing(fb.exploding ? 5 : 1.5, fb.exploding ? 0xffff00 : 0xff6600);
|
||||
this.scene.add(ring);
|
||||
this.fireballHitRings.set(fb, ring);
|
||||
}
|
||||
ring.position.x = fb.body.position.x;
|
||||
ring.position.z = fb.body.position.z;
|
||||
ring.visible = this.showHitzones;
|
||||
}
|
||||
for (const [fb, ring] of this.fireballHitRings) {
|
||||
if (!this.fireballs.includes(fb)) {
|
||||
this.scene.remove(ring);
|
||||
ring.geometry.dispose();
|
||||
(ring.material as THREE.Material).dispose();
|
||||
this.fireballHitRings.delete(fb);
|
||||
}
|
||||
}
|
||||
|
||||
// Cross rings (pickup radius 2)
|
||||
for (const cross of this.crosses) {
|
||||
let ring = this.crossHitRings.get(cross);
|
||||
if (!ring) {
|
||||
ring = this.createHitRing(2, 0x00ff00);
|
||||
this.scene.add(ring);
|
||||
this.crossHitRings.set(cross, ring);
|
||||
}
|
||||
ring.position.x = cross.body.position.x;
|
||||
ring.position.z = cross.body.position.z;
|
||||
ring.visible = this.showHitzones;
|
||||
}
|
||||
for (const [cross, ring] of this.crossHitRings) {
|
||||
if (!this.crosses.includes(cross)) {
|
||||
this.scene.remove(ring);
|
||||
ring.geometry.dispose();
|
||||
(ring.material as THREE.Material).dispose();
|
||||
this.crossHitRings.delete(cross);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private spawnEnemy() {
|
||||
const x = (Math.random() - 0.5) * 80;
|
||||
const z = (Math.random() - 0.5) * 80;
|
||||
const enemy = new Enemy(this.player.body.position);
|
||||
enemy.body.position.set(x, 0.1, z);
|
||||
this.scene.add(enemy.body);
|
||||
this.enemies.push(enemy);
|
||||
}
|
||||
|
||||
private spawnCross(position: THREE.Vector3) {
|
||||
if (Math.random() > 0.9) {
|
||||
const cross = new Cross(position.clone());
|
||||
this.scene.add(cross.body);
|
||||
this.crosses.push(cross);
|
||||
}
|
||||
}
|
||||
|
||||
private updateGroundPoint() {
|
||||
const raycaster = new THREE.Raycaster();
|
||||
raycaster.setFromCamera(new THREE.Vector2(this.mouseX, this.mouseY), this.camera);
|
||||
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
||||
const intersection = new THREE.Vector3();
|
||||
if (raycaster.ray.intersectPlane(plane, intersection)) {
|
||||
intersection.y = 0;
|
||||
intersection.clamp(
|
||||
new THREE.Vector3(-48, 0, -48),
|
||||
new THREE.Vector3(48, 0, 48)
|
||||
);
|
||||
this.groundPoint.copy(intersection);
|
||||
}
|
||||
}
|
||||
|
||||
private updatePlayerMovement(dt: number) {
|
||||
if (this.gameOver) {
|
||||
this.player.body.position.y = 0.5;
|
||||
return;
|
||||
}
|
||||
|
||||
const camFwd = new THREE.Vector3();
|
||||
this.camera.getWorldDirection(camFwd);
|
||||
camFwd.y = 0;
|
||||
camFwd.normalize();
|
||||
|
||||
const camLeft = new THREE.Vector3();
|
||||
camLeft.crossVectors(new THREE.Vector3(0, 1, 0), camFwd).normalize();
|
||||
|
||||
const moveDir = new THREE.Vector3();
|
||||
if (this.keys.has('KeyW') || this.keys.has('ArrowUp')) moveDir.add(camFwd);
|
||||
if (this.keys.has('KeyS') || this.keys.has('ArrowDown')) moveDir.sub(camFwd);
|
||||
if (this.keys.has('KeyA') || this.keys.has('ArrowLeft')) moveDir.add(camLeft);
|
||||
if (this.keys.has('KeyD') || this.keys.has('ArrowRight')) moveDir.sub(camLeft);
|
||||
|
||||
let speed = 7.5;
|
||||
let isMoving = false;
|
||||
if (moveDir.length() > 0) {
|
||||
moveDir.normalize();
|
||||
this.player.velocity.set(moveDir.x * speed, 0, moveDir.z * speed);
|
||||
isMoving = true;
|
||||
} else {
|
||||
this.player.velocity.set(0, 0, 0);
|
||||
}
|
||||
|
||||
// Play walk/stand animations
|
||||
if (isMoving && this.player.getCurrentAnimation() !== 'walk') {
|
||||
this.player.playAnimation('walk');
|
||||
} else if (!isMoving && this.player.getCurrentAnimation() !== 'stand') {
|
||||
this.player.playAnimation('stand');
|
||||
}
|
||||
|
||||
// Jump / gravity
|
||||
if (!this.player.onGround) {
|
||||
this.player.jumpVelocity -= 20 * dt;
|
||||
this.player.body.position.y += this.player.jumpVelocity * dt;
|
||||
if (this.player.body.position.y <= 0.5) {
|
||||
this.player.body.position.y = 0.5;
|
||||
this.player.jumpVelocity = 0;
|
||||
this.player.onGround = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply horizontal movement
|
||||
this.player.body.position.x += this.player.velocity.x * dt;
|
||||
this.player.body.position.z += this.player.velocity.z * dt;
|
||||
this.clampToArena(this.player.body.position);
|
||||
|
||||
// Look at ground point
|
||||
this.player.body.lookAt(
|
||||
this.groundPoint.x,
|
||||
this.player.body.position.y,
|
||||
this.groundPoint.z
|
||||
);
|
||||
}
|
||||
|
||||
private clampToArena(pos: THREE.Vector3) {
|
||||
pos.x = Math.max(-47, Math.min(47, pos.x));
|
||||
pos.z = Math.max(-47, Math.min(47, pos.z));
|
||||
}
|
||||
|
||||
private updateEnemies(dt: number) {
|
||||
for (const enemy of this.enemies) {
|
||||
if (enemy.dead) {
|
||||
enemy.updateAnimation(dt);
|
||||
continue;
|
||||
}
|
||||
|
||||
const toPlayer = new THREE.Vector3()
|
||||
.subVectors(this.player.body.position, enemy.body.position);
|
||||
toPlayer.y = 0;
|
||||
|
||||
const dist = toPlayer.length();
|
||||
if (dist > 0.1) {
|
||||
const dir = toPlayer.normalize();
|
||||
enemy.body.lookAt(
|
||||
enemy.body.position.x + dir.x,
|
||||
enemy.body.position.y,
|
||||
enemy.body.position.z + dir.z
|
||||
);
|
||||
|
||||
enemy.playAnim('walk');
|
||||
|
||||
let moveDir = dir.multiplyScalar(6);
|
||||
if (enemy.knockback > 0) {
|
||||
moveDir = dir.multiplyScalar(-6);
|
||||
enemy.knockback -= dt;
|
||||
}
|
||||
enemy.body.position.x += moveDir.x * dt;
|
||||
enemy.body.position.z += moveDir.z * dt;
|
||||
this.clampToArena(enemy.body.position);
|
||||
} else {
|
||||
enemy.playAnim('stand');
|
||||
}
|
||||
|
||||
enemy.updateAnimation(dt);
|
||||
}
|
||||
}
|
||||
|
||||
private updateFireballs(dt: number) {
|
||||
for (let i = this.fireballs.length - 1; i >= 0; i--) {
|
||||
const fb = this.fireballs[i];
|
||||
fb.lifetime -= dt;
|
||||
|
||||
if (fb.exploding) {
|
||||
fb.timer += dt;
|
||||
if (fb.timer > 0.2 && !fb.damageDone) {
|
||||
fb.damageDone = true;
|
||||
for (const enemy of this.enemies) {
|
||||
if (enemy.dead) continue;
|
||||
const dist = enemy.body.position.distanceTo(fb.body.position);
|
||||
if (dist < 8) {
|
||||
enemy.takeDamage(100, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fb.timer > 2) {
|
||||
this.scene.remove(fb.body);
|
||||
this.fireballs.splice(i, 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
fb.body.position.x += fb.direction.x * dt;
|
||||
fb.body.position.z += fb.direction.z * dt;
|
||||
fb.updateTrail(dt, fb.body.position);
|
||||
|
||||
for (const enemy of this.enemies) {
|
||||
if (enemy.dead) continue;
|
||||
const dist = enemy.body.position.distanceTo(fb.body.position);
|
||||
if (dist < 1.8) {
|
||||
fb.explode(this.scene);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fb.lifetime <= 0) {
|
||||
fb.explode(this.scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateUI() {
|
||||
const healthPct = this.player.health / this.player.MAX_HEALTH;
|
||||
this.uiHealthBar.style.width = `${Math.max(0, healthPct * 250)}px`;
|
||||
this.uiHealthText.textContent = `${Math.ceil(this.player.health)}%`;
|
||||
|
||||
this.uiWaveLabel.textContent = `Wave ${this.spawns}`;
|
||||
this.uiKillsLabel.textContent = `Kills: ${this.killed}`;
|
||||
|
||||
const sword = this.player.rightHandWeapon as Sword;
|
||||
const staff = this.player.leftHandWeapon as Staff;
|
||||
|
||||
const fbPct = Math.max(0, (staff.COOLDOWN1_TIME - staff.cooldown1) / staff.COOLDOWN1_TIME);
|
||||
this.uiFireballCD.style.width = `${fbPct * 100}%`;
|
||||
|
||||
const swordPct = Math.max(0, (sword.COOLDOWN1_TIME - sword.cooldown1) / sword.COOLDOWN1_TIME);
|
||||
this.uiSwordCD.style.width = `${swordPct * 100}%`;
|
||||
|
||||
const tpPct = Math.max(0, (staff.COOLDOWN2_TIME - staff.cooldown2) / staff.COOLDOWN2_TIME);
|
||||
this.uiTeleportCD.style.width = `${tpPct * 100}%`;
|
||||
|
||||
const wavePct = this.spawnTimer / SPAWN_TIME;
|
||||
this.uiWaveBarFill.style.width = `${wavePct * 100}%`;
|
||||
this.uiWaveBarLabel.textContent = `Next Wave: ${Math.ceil(SPAWN_TIME - this.spawnTimer)}s`;
|
||||
|
||||
if (this.player.health <= 0 && !this.gameOver) {
|
||||
this.gameOver = true;
|
||||
this.player.die();
|
||||
this.uiGameOver.style.opacity = '1';
|
||||
playSound('gameover', 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
private animate() {
|
||||
requestAnimationFrame(() => this.animate());
|
||||
|
||||
const dt = Math.min(this.clock.getDelta(), 0.1);
|
||||
|
||||
this.updateGroundPoint();
|
||||
this.updatePlayerMovement(dt);
|
||||
this.updateEnemies(dt);
|
||||
this.updateFireballs(dt);
|
||||
this.updateHitzones();
|
||||
|
||||
// Update animations
|
||||
this.player.updateAnimations(dt);
|
||||
this.player.rightHandWeapon.updateAnimation(dt);
|
||||
|
||||
// Spawn enemies in waves
|
||||
if (!this.gameOver) {
|
||||
this.spawnTimer += dt;
|
||||
if (this.spawnTimer >= SPAWN_TIME) {
|
||||
this.spawns++;
|
||||
for (let i = 0; i < this.spawns; i++) {
|
||||
this.spawnEnemy();
|
||||
}
|
||||
this.spawnTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check close encounters (spider touching player)
|
||||
for (const enemy of this.enemies) {
|
||||
if (enemy.dead) continue;
|
||||
const dist = enemy.body.position.distanceTo(this.player.body.position);
|
||||
if (dist < 2) {
|
||||
this.player.health -= 25 * dt;
|
||||
}
|
||||
}
|
||||
|
||||
// Cross collisions
|
||||
for (let i = this.crosses.length - 1; i >= 0; i--) {
|
||||
const cross = this.crosses[i];
|
||||
cross.update(dt);
|
||||
const dist = cross.body.position.distanceTo(this.player.body.position);
|
||||
if (dist < 2) {
|
||||
this.player.heal(25);
|
||||
this.scene.remove(cross.body);
|
||||
this.crosses.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Update cooldowns
|
||||
this.player.rightHandWeapon.reduceCooldowns(dt);
|
||||
this.player.leftHandWeapon.reduceCooldowns(dt);
|
||||
|
||||
// Camera follows player
|
||||
this.camera.position.set(
|
||||
this.player.body.position.x,
|
||||
25,
|
||||
this.player.body.position.z - 15
|
||||
);
|
||||
this.camera.lookAt(
|
||||
this.player.body.position.x,
|
||||
0,
|
||||
this.player.body.position.z
|
||||
);
|
||||
|
||||
// Clean dead enemies
|
||||
for (let i = this.enemies.length - 1; i >= 0; i--) {
|
||||
const enemy = this.enemies[i];
|
||||
if (enemy.dead) {
|
||||
enemy.deathTimer += dt;
|
||||
if (enemy.deathTimer > 10) {
|
||||
this.scene.remove(enemy.body);
|
||||
this.enemies.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
removeEnemy(enemy: Enemy) {
|
||||
enemy.dead = true;
|
||||
this.killed++;
|
||||
this.spawnCross(enemy.body.position);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader, type GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
|
||||
const loader = new GLTFLoader();
|
||||
|
||||
export async function loadGLB(name: string): Promise<GLTF> {
|
||||
return new Promise((resolve, reject) => {
|
||||
loader.load(
|
||||
`./models/${name}.glb`,
|
||||
(gltf) => resolve(gltf),
|
||||
undefined,
|
||||
(err) => reject(err)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import * as THREE from 'three';
|
||||
import { Sword } from './Sword';
|
||||
import { Staff } from './Staff';
|
||||
import { loadGLB } from './MeshLoader';
|
||||
import type { Game } from './Game';
|
||||
|
||||
export class Player {
|
||||
body: THREE.Group;
|
||||
leftHandWeapon: Staff;
|
||||
rightHandWeapon: Sword;
|
||||
velocity = new THREE.Vector3();
|
||||
health = 100;
|
||||
MAX_HEALTH = 100;
|
||||
game!: Game;
|
||||
jumpVelocity = 0;
|
||||
onGround = true;
|
||||
mixer!: THREE.AnimationMixer;
|
||||
private clips: Map<string, THREE.AnimationAction> = new Map();
|
||||
private currentAnim = '';
|
||||
|
||||
constructor() {
|
||||
this.body = new THREE.Group();
|
||||
|
||||
// Helmet
|
||||
const helmetGeom = new THREE.SphereGeometry(0.45, 8, 8, 0, Math.PI * 2, 0, Math.PI / 2);
|
||||
const helmetMat = new THREE.MeshToonMaterial({ color: 0x888888 });
|
||||
const helmet = new THREE.Mesh(helmetGeom, helmetMat);
|
||||
helmet.position.y = 0.9;
|
||||
helmet.castShadow = true;
|
||||
this.body.add(helmet);
|
||||
|
||||
// Try loading the real helmet model (replaces the placeholder sphere)
|
||||
loadGLB('helmet').then((gltf) => {
|
||||
const realHelmet = gltf.scene;
|
||||
const helmetTexture = new THREE.TextureLoader().load('./textures/helmet.png');
|
||||
helmetTexture.flipY = false;
|
||||
helmetTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
realHelmet.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true;
|
||||
child.material = new THREE.MeshToonMaterial({ map: helmetTexture });
|
||||
}
|
||||
});
|
||||
realHelmet.scale.set(0.35, 0.35, 0.35);
|
||||
realHelmet.position.set(0, 0.9, -0.2);
|
||||
realHelmet.rotation.set(-0.4, 0, 0);
|
||||
this.body.remove(helmet);
|
||||
this.body.add(realHelmet);
|
||||
}).catch(() => {
|
||||
// Keep placeholder helmet
|
||||
});
|
||||
|
||||
// Shadow disc
|
||||
const shadowGeom = new THREE.CircleGeometry(0.4, 16);
|
||||
const shadowMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x000000, transparent: true, opacity: 0.3,
|
||||
});
|
||||
const shadow = new THREE.Mesh(shadowGeom, shadowMat);
|
||||
shadow.rotation.x = -Math.PI / 2;
|
||||
shadow.position.y = -0.48;
|
||||
shadow.renderOrder = 999;
|
||||
this.body.add(shadow);
|
||||
|
||||
// Weapons
|
||||
this.leftHandWeapon = new Staff();
|
||||
this.rightHandWeapon = new Sword();
|
||||
this.body.add(this.leftHandWeapon.mesh);
|
||||
this.body.add(this.rightHandWeapon.mesh);
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
const gltf = await loadGLB('blob');
|
||||
const model = gltf.scene;
|
||||
model.scale.set(0.5, 0.5, 0.5);
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
const mat = new THREE.MeshToonMaterial({
|
||||
color: 0x44aa44,
|
||||
});
|
||||
child.material = mat;
|
||||
}
|
||||
});
|
||||
this.body.add(model);
|
||||
|
||||
if (gltf.animations.length > 0) {
|
||||
this.mixer = new THREE.AnimationMixer(model);
|
||||
for (const clip of gltf.animations) {
|
||||
const action = this.mixer.clipAction(clip);
|
||||
this.clips.set(clip.name, action);
|
||||
}
|
||||
this.playAnimation('stand');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load blob model, using fallback', e);
|
||||
// Fallback capsule
|
||||
const bodyGeom = new THREE.CapsuleGeometry(0.5, 0.5, 8, 16);
|
||||
const bodyMat = new THREE.MeshToonMaterial({ color: 0x44aa44 });
|
||||
const bodyMesh = new THREE.Mesh(bodyGeom, bodyMat);
|
||||
bodyMesh.position.y = 0.75;
|
||||
bodyMesh.castShadow = true;
|
||||
this.body.add(bodyMesh);
|
||||
}
|
||||
}
|
||||
|
||||
playAnimation(name: string, loop = true, speed = 1) {
|
||||
if (!this.clips.has(name)) return;
|
||||
if (this.currentAnim === name) return;
|
||||
// Stop the previous animation so it doesn't blend with the new one
|
||||
if (this.currentAnim) {
|
||||
const prev = this.clips.get(this.currentAnim);
|
||||
if (prev) prev.stop();
|
||||
}
|
||||
this.currentAnim = name;
|
||||
const action = this.clips.get(name)!;
|
||||
action.reset();
|
||||
action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, loop ? Infinity : 1);
|
||||
action.clampWhenFinished = !loop;
|
||||
action.setEffectiveTimeScale(speed);
|
||||
action.weight = 1;
|
||||
action.play();
|
||||
return action;
|
||||
}
|
||||
|
||||
stopAnimation(name: string) {
|
||||
const action = this.clips.get(name);
|
||||
if (action) action.stop();
|
||||
}
|
||||
|
||||
getCurrentAnimation(): string {
|
||||
return this.currentAnim;
|
||||
}
|
||||
|
||||
updateAnimations(dt: number) {
|
||||
if (this.mixer) this.mixer.update(dt);
|
||||
}
|
||||
|
||||
setGame(game: Game) {
|
||||
this.game = game;
|
||||
}
|
||||
|
||||
jump() {
|
||||
if (this.onGround) {
|
||||
this.jumpVelocity = 8;
|
||||
this.onGround = false;
|
||||
this.playAnimation('jump', false);
|
||||
}
|
||||
}
|
||||
|
||||
switchHands() {
|
||||
const temp = this.leftHandWeapon;
|
||||
this.leftHandWeapon = this.rightHandWeapon as unknown as Staff;
|
||||
this.rightHandWeapon = temp as unknown as Sword;
|
||||
|
||||
const leftPos = this.leftHandWeapon.mesh.position.clone();
|
||||
const rightPos = this.rightHandWeapon.mesh.position.clone();
|
||||
this.leftHandWeapon.mesh.position.copy(rightPos);
|
||||
this.rightHandWeapon.mesh.position.copy(leftPos);
|
||||
|
||||
const leftRot = this.leftHandWeapon.mesh.rotation.clone();
|
||||
const rightRot = this.rightHandWeapon.mesh.rotation.clone();
|
||||
this.leftHandWeapon.mesh.rotation.copy(rightRot);
|
||||
this.rightHandWeapon.mesh.rotation.copy(leftRot);
|
||||
}
|
||||
|
||||
heal(amount: number) {
|
||||
this.health = Math.min(this.health + amount, this.MAX_HEALTH);
|
||||
}
|
||||
|
||||
die() {
|
||||
this.playAnimation('die', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
|
||||
const sounds: Record<string, AudioBuffer> = {};
|
||||
|
||||
async function loadSound(name: string): Promise<AudioBuffer> {
|
||||
const response = await fetch(`./sounds/${name}.ogg`);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return audioCtx.decodeAudioData(arrayBuffer);
|
||||
}
|
||||
|
||||
export async function initSounds() {
|
||||
const names = ['fireball', 'explosion', 'sword_hit1', 'teleport', 'damage', 'spiderwalking', 'spawn', 'gameover'];
|
||||
for (const name of names) {
|
||||
try {
|
||||
sounds[name] = await loadSound(name);
|
||||
} catch {
|
||||
console.warn(`Sound ${name} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resumeContext() {
|
||||
if (audioCtx.state === 'suspended') {
|
||||
audioCtx.resume();
|
||||
}
|
||||
}
|
||||
|
||||
export function playSound(name: string, volume = 1) {
|
||||
const buffer = sounds[name];
|
||||
if (!buffer) return;
|
||||
if (audioCtx.state === 'suspended') return;
|
||||
const source = audioCtx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
const gain = audioCtx.createGain();
|
||||
gain.gain.value = volume;
|
||||
source.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
source.start(0);
|
||||
}
|
||||
|
||||
export function playLoopingSound(name: string, volume = 1): (() => void) | null {
|
||||
const buffer = sounds[name];
|
||||
if (!buffer) return null;
|
||||
if (audioCtx.state === 'suspended') return null;
|
||||
const source = audioCtx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
source.loop = true;
|
||||
const gain = audioCtx.createGain();
|
||||
gain.gain.value = volume;
|
||||
source.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
source.start(0);
|
||||
return () => {
|
||||
try {
|
||||
source.stop();
|
||||
} catch {
|
||||
// already stopped
|
||||
}
|
||||
source.disconnect();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as THREE from 'three';
|
||||
import { Weapon } from './Weapon';
|
||||
import { Player } from './Player';
|
||||
import { Fireball } from './Fireball';
|
||||
import { playSound } from './SoundManager';
|
||||
import { loadGLB } from './MeshLoader';
|
||||
|
||||
export class Staff extends Weapon {
|
||||
constructor() {
|
||||
super();
|
||||
this.COOLDOWN1_TIME = 5;
|
||||
this.COOLDOWN2_TIME = 50;
|
||||
|
||||
// Fallback geometry
|
||||
const shaftGeom = new THREE.CylinderGeometry(0.04, 0.05, 1.0, 8);
|
||||
const crystalGeom = new THREE.OctahedronGeometry(0.08);
|
||||
const woodMat = new THREE.MeshToonMaterial({ color: 0x8B4513 });
|
||||
const crystalMat = new THREE.MeshToonMaterial({ color: 0x4488ff });
|
||||
const shaft = new THREE.Mesh(shaftGeom, woodMat);
|
||||
shaft.position.y = 0.3;
|
||||
shaft.castShadow = true;
|
||||
const crystal = new THREE.Mesh(crystalGeom, crystalMat);
|
||||
crystal.position.y = 0.9;
|
||||
this.mesh.add(shaft);
|
||||
this.mesh.add(crystal);
|
||||
|
||||
this.mesh.position.set(0.35, 0.3, 0.35);
|
||||
this.mesh.scale.set(0.35, 0.35, 0.35);
|
||||
this.mesh.rotation.set(-0.2, 0, -0.2);
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
const gltf = await loadGLB('staff');
|
||||
const model = gltf.scene;
|
||||
const staffTexture = new THREE.TextureLoader().load('./textures/staff1.png');
|
||||
staffTexture.flipY = false;
|
||||
staffTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true;
|
||||
child.material = new THREE.MeshToonMaterial({ map: staffTexture });
|
||||
}
|
||||
});
|
||||
while (this.mesh.children.length > 0) {
|
||||
this.mesh.remove(this.mesh.children[0]);
|
||||
}
|
||||
this.mesh.add(model);
|
||||
} catch {
|
||||
// Keep fallback
|
||||
}
|
||||
}
|
||||
|
||||
skill1(groundPoint: THREE.Vector3, player: Player): void {
|
||||
if (this.cooldown1 > 0) return;
|
||||
this.cooldown1 = this.COOLDOWN1_TIME;
|
||||
|
||||
// Spawn at staff height, fly toward the cursor point
|
||||
const spawnPos = player.body.position.clone();
|
||||
spawnPos.y += 0.8;
|
||||
|
||||
const dir = new THREE.Vector3()
|
||||
.subVectors(groundPoint, spawnPos)
|
||||
.normalize()
|
||||
.multiplyScalar(25);
|
||||
dir.setY(0);
|
||||
|
||||
const fb = new Fireball(spawnPos, dir);
|
||||
player.game.scene.add(fb.body);
|
||||
player.game.fireballs.push(fb);
|
||||
playSound('fireball', 0.5);
|
||||
}
|
||||
|
||||
skill2(groundPoint: THREE.Vector3, player: Player): void {
|
||||
if (this.cooldown2 > 0) return;
|
||||
this.cooldown2 = this.COOLDOWN2_TIME;
|
||||
|
||||
const newPos = groundPoint.clone();
|
||||
newPos.y = 0.5;
|
||||
newPos.x = Math.max(-47, Math.min(47, newPos.x));
|
||||
newPos.z = Math.max(-47, Math.min(47, newPos.z));
|
||||
player.body.position.copy(newPos);
|
||||
playSound('teleport', 0.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import * as THREE from 'three';
|
||||
import { Weapon } from './Weapon';
|
||||
import { Player } from './Player';
|
||||
import { loadGLB } from './MeshLoader';
|
||||
import { playSound } from './SoundManager';
|
||||
|
||||
export class Sword extends Weapon {
|
||||
private mixer: THREE.AnimationMixer | null = null;
|
||||
private clips: Map<string, THREE.AnimationAction> = new Map();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.COOLDOWN1_TIME = 0.5;
|
||||
|
||||
this.mesh.position.set(-0.35, 0.3, 0.35);
|
||||
this.mesh.scale.set(0.2, 0.2, 0.2);
|
||||
this.mesh.rotation.set(0.3, 0, 0.3);
|
||||
|
||||
// Fallback geometry
|
||||
const bladeGeom = new THREE.BoxGeometry(0.08, 1.0, 0.15);
|
||||
const handleGeom = new THREE.CylinderGeometry(0.05, 0.05, 0.2, 8);
|
||||
const guardGeom = new THREE.BoxGeometry(0.25, 0.06, 0.06);
|
||||
const metalMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.9, roughness: 0.3 });
|
||||
const darkMat = new THREE.MeshStandardMaterial({ color: 0x444444, roughness: 0.6 });
|
||||
|
||||
const blade = new THREE.Mesh(bladeGeom, metalMat);
|
||||
blade.position.y = 0.4;
|
||||
blade.castShadow = true;
|
||||
const guard = new THREE.Mesh(guardGeom, metalMat);
|
||||
guard.position.y = -0.1;
|
||||
const handle = new THREE.Mesh(handleGeom, darkMat);
|
||||
handle.position.y = -0.25;
|
||||
this.mesh.add(blade);
|
||||
this.mesh.add(guard);
|
||||
this.mesh.add(handle);
|
||||
this._fallback = true;
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
const gltf = await loadGLB('sword');
|
||||
const model = gltf.scene;
|
||||
model.scale.set(1, 1, 1);
|
||||
const swordTexture = new THREE.TextureLoader().load('./textures/sword.png');
|
||||
swordTexture.flipY = false;
|
||||
swordTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true;
|
||||
child.material = new THREE.MeshToonMaterial({ map: swordTexture });
|
||||
}
|
||||
});
|
||||
|
||||
// Remove fallback geometry
|
||||
while (this.mesh.children.length > 0) {
|
||||
this.mesh.remove(this.mesh.children[0]);
|
||||
}
|
||||
this._fallback = false;
|
||||
this.mesh.add(model);
|
||||
|
||||
if (gltf.animations.length > 0) {
|
||||
this.mixer = new THREE.AnimationMixer(model);
|
||||
for (const clip of gltf.animations) {
|
||||
const action = this.mixer.clipAction(clip);
|
||||
this.clips.set(clip.name, action);
|
||||
}
|
||||
const normal = this.clips.get('normal');
|
||||
if (normal) {
|
||||
normal.play();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load sword model', e);
|
||||
}
|
||||
}
|
||||
|
||||
private _fallback = false;
|
||||
|
||||
updateAnimation(dt: number) {
|
||||
if (this.mixer) this.mixer.update(dt);
|
||||
}
|
||||
|
||||
skill1(_groundPoint: THREE.Vector3, player: Player): void {
|
||||
if (this.cooldown1 > 0) return;
|
||||
this.cooldown1 = this.COOLDOWN1_TIME;
|
||||
|
||||
playSound('sword_hit1', 0.5);
|
||||
|
||||
// Play skill1 animation
|
||||
const action = this.clips.get('skill1');
|
||||
if (action) {
|
||||
action.reset();
|
||||
action.setLoop(THREE.LoopOnce, 1);
|
||||
action.setEffectiveTimeScale(2);
|
||||
action.play();
|
||||
}
|
||||
|
||||
const { game } = player;
|
||||
|
||||
for (const enemy of game.enemies) {
|
||||
if (enemy.dead) continue;
|
||||
const dist = enemy.body.position.distanceTo(player.body.position);
|
||||
if (dist < 3) {
|
||||
const toEnemy = new THREE.Vector3()
|
||||
.subVectors(enemy.body.position, player.body.position).normalize();
|
||||
const playerFwd = new THREE.Vector3(0, 0, 1);
|
||||
playerFwd.applyQuaternion(player.body.quaternion);
|
||||
const dot = playerFwd.dot(toEnemy);
|
||||
if (dot > 0.3) {
|
||||
enemy.takeDamage(50, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
skill2(): void {
|
||||
// Not implemented
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as THREE from 'three';
|
||||
import { Player } from './Player';
|
||||
|
||||
export abstract class Weapon {
|
||||
mesh: THREE.Group;
|
||||
cooldown1 = 0;
|
||||
cooldown2 = 0;
|
||||
cooldown3 = 0;
|
||||
COOLDOWN1_TIME = 0;
|
||||
COOLDOWN2_TIME = 0;
|
||||
COOLDOWN3_TIME = 0;
|
||||
|
||||
constructor() {
|
||||
this.mesh = new THREE.Group();
|
||||
}
|
||||
|
||||
reduceCooldowns(tpf: number) {
|
||||
if (this.cooldown1 > 0) this.cooldown1 -= tpf;
|
||||
if (this.cooldown2 > 0) this.cooldown2 -= tpf;
|
||||
if (this.cooldown3 > 0) this.cooldown3 -= tpf;
|
||||
}
|
||||
|
||||
abstract skill1(groundPoint: THREE.Vector3, player: Player): void;
|
||||
abstract skill2(groundPoint: THREE.Vector3, player: Player): void;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Game } from './Game';
|
||||
|
||||
async function main() {
|
||||
const game = new Game();
|
||||
await game.init();
|
||||
game.start();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,8 @@
|
||||
declare module '*.glb' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.gltf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
port: 3000,
|
||||
},
|
||||
});
|
||||