Upload files to "/"
This commit is contained in:
911
active_ragdoll-v2-weapons.html
Normal file
911
active_ragdoll-v2-weapons.html
Normal file
@@ -0,0 +1,911 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Active Ragdoll — Weapons</title>
|
||||
<style>
|
||||
html,body{margin:0;height:100%;overflow:hidden;background:#0e1116;font-family:system-ui,'Segoe UI',Roboto,sans-serif}
|
||||
canvas{display:block;cursor:crosshair}
|
||||
#ui{position:fixed;top:14px;left:14px;z-index:10;background:rgba(15,18,24,.85);backdrop-filter:blur(8px);
|
||||
border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:14px 16px;color:#e8ecf2;
|
||||
box-shadow:0 8px 30px rgba(0,0,0,.45);max-width:296px;user-select:none}
|
||||
#ui h1{font-size:15px;margin:0 0 2px;font-weight:650;letter-spacing:.2px}
|
||||
#ui .sub{font-size:11px;color:#9aa6b5;margin-bottom:8px}
|
||||
#state{display:inline-block;font-size:11px;font-weight:700;letter-spacing:.6px;padding:3px 11px;border-radius:99px;
|
||||
background:#173527;color:#7ce0a3;border:1px solid rgba(124,224,163,.3);margin-bottom:8px;transition:all .25s}
|
||||
.lbl{font-size:10px;color:#6b7a8d;text-transform:uppercase;letter-spacing:.8px;margin:8px 0 4px;font-weight:600}
|
||||
.row{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:3px}
|
||||
button{cursor:pointer;border:1px solid rgba(255,255,255,.12);background:#232a35;color:#e8ecf2;border-radius:9px;
|
||||
padding:6px 10px;font-size:12px;font-weight:550;transition:background .15s,transform .05s;white-space:nowrap}
|
||||
button:hover{background:#2e3848}button:active{transform:scale(.95)}
|
||||
button.tog.on{background:#3b82f6;border-color:#60a5fa}
|
||||
button.danger{border-color:rgba(239,68,68,.35);color:#fca5a5}button.danger:hover{background:#3b1a1a}
|
||||
button.he{border-color:rgba(251,146,60,.45);color:#fdba74}button.he:hover{background:#3b2010}
|
||||
button.ap{border-color:rgba(167,243,208,.35);color:#86efac}button.ap:hover{background:#0d2b1a}
|
||||
button.sel{outline:2px solid #60a5fa;outline-offset:1px}
|
||||
#ammo-row{display:flex;align-items:center;gap:6px;margin-top:6px;flex-wrap:wrap}
|
||||
.ammo-btn{font-size:11px;padding:4px 9px}
|
||||
.ammo-btn.active-ammo{background:#1e3a22;border-color:#4ade80;color:#86efac}
|
||||
#cam-badge{display:inline-block;font-size:10px;font-weight:700;padding:2px 9px;border-radius:99px;margin-left:6px;
|
||||
background:#1a1f2b;border:1px solid rgba(255,255,255,.15);color:#94a3b8;vertical-align:middle}
|
||||
#hint{position:fixed;bottom:12px;left:50%;transform:translateX(-50%);z-index:10;color:#aab6c6;font-size:12px;
|
||||
background:rgba(15,18,24,.65);border:1px solid rgba(255,255,255,.07);padding:6px 14px;
|
||||
border-radius:99px;white-space:nowrap;pointer-events:none}
|
||||
#hint b{color:#dfe7f1}
|
||||
#flash{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;opacity:0;
|
||||
background:radial-gradient(ellipse at center,rgba(255,160,60,.3) 0%,transparent 70%);transition:opacity .06s}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="ui">
|
||||
<h1>Active Ragdoll <span id="cam-badge">ORBIT</span></h1>
|
||||
<div class="sub">Weapons Edition · Self-balancing muscles</div>
|
||||
<div><span id="state">BALANCING</span></div>
|
||||
|
||||
<div class="lbl">Ragdoll</div>
|
||||
<div class="row">
|
||||
<button id="bShove">Shove</button>
|
||||
<button id="bKO">Knockout</button>
|
||||
<button id="bSlow" class="tog">Slow-mo</button>
|
||||
<button id="bReset">Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="lbl">Camera <span style="color:#6b7a8d;font-weight:400">(or press V)</span></div>
|
||||
<div class="row">
|
||||
<button id="camOrbit" class="sel" onclick="setCamMode('orbit')">🌐 Orbit</button>
|
||||
<button id="camFollow" onclick="setCamMode('follow')">🎯 Follow</button>
|
||||
<button id="camFP" onclick="setCamMode('fp')">👁 First-Person</button>
|
||||
</div>
|
||||
|
||||
<div class="lbl">Weapon</div>
|
||||
<div class="row">
|
||||
<button id="wBall" class="sel" onclick="selectWeapon('ball')">⚾ Ball</button>
|
||||
<button id="wGun" onclick="selectWeapon('gun')">🔫 Pistol</button>
|
||||
<button id="wCannon" onclick="selectWeapon('cannon')">💣 Cannon</button>
|
||||
<button id="wC4" onclick="selectWeapon('c4')">🧱 C4</button>
|
||||
</div>
|
||||
|
||||
<div id="ammo-row" style="display:none">
|
||||
<span style="font-size:11px;color:#9aa6b5">Ammo:</span>
|
||||
<button class="ammo-btn active-ammo" id="amNormal" onclick="selectAmmo('normal')">Normal</button>
|
||||
<button class="ammo-btn he" id="amHE" onclick="selectAmmo('HE')">HE</button>
|
||||
<button class="ammo-btn ap" id="amAP" onclick="selectAmmo('AP')">AP</button>
|
||||
</div>
|
||||
|
||||
<div class="lbl" style="margin-top:8px">C4</div>
|
||||
<div class="row">
|
||||
<button class="danger" id="bDetonate">💥 Detonate</button>
|
||||
<button class="danger" id="bClearC4">Clear C4</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hint"><b>LMB</b> fire/place · <b>RMB drag</b> grab ragdoll · <b>LMB drag</b> orbit · <b>V</b> cam · <b>scroll</b> zoom</div>
|
||||
<div id="flash"></div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/* ===================== RENDERER ===================== */
|
||||
const renderer = new THREE.WebGLRenderer({antialias:true,powerPreference:'high-performance'});
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
|
||||
renderer.setSize(innerWidth,innerHeight);
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
renderer.outputEncoding = THREE.sRGBEncoding;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.06;
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
/* ===================== SCENE ===================== */
|
||||
const scene = new THREE.Scene();
|
||||
scene.fog = new THREE.Fog(0x9fb2c2,18,72);
|
||||
{
|
||||
const c=document.createElement('canvas'); c.width=1; c.height=256;
|
||||
const g=c.getContext('2d');
|
||||
const gr=g.createLinearGradient(0,0,0,256);
|
||||
gr.addColorStop(0,'#3e6da8'); gr.addColorStop(.5,'#8aa6c0'); gr.addColorStop(1,'#c8cdce');
|
||||
g.fillStyle=gr; g.fillRect(0,0,1,256);
|
||||
const t=new THREE.CanvasTexture(c); t.encoding=THREE.sRGBEncoding; scene.background=t;
|
||||
}
|
||||
scene.add(new THREE.HemisphereLight(0xbdd3ea,0x4d463b,0.9));
|
||||
const sun = new THREE.DirectionalLight(0xfff0d8,1.5);
|
||||
sun.position.set(6,9,4); sun.castShadow=true;
|
||||
sun.shadow.mapSize.set(1024,1024); // reduced for perf
|
||||
sun.shadow.camera.near=1; sun.shadow.camera.far=30;
|
||||
sun.shadow.camera.left=-8; sun.shadow.camera.right=8;
|
||||
sun.shadow.camera.top=8; sun.shadow.camera.bottom=-8;
|
||||
sun.shadow.bias=-0.001; sun.shadow.radius=3;
|
||||
scene.add(sun);
|
||||
const fill = new THREE.DirectionalLight(0xc9d8ff,0.28);
|
||||
fill.position.set(-5,4,-6); scene.add(fill);
|
||||
|
||||
const flashPt = new THREE.PointLight(0xff8822,0,20,2); scene.add(flashPt);
|
||||
|
||||
/* ground */
|
||||
function makeGroundTex(){
|
||||
const c=document.createElement('canvas'); c.width=c.height=512;
|
||||
const g=c.getContext('2d');
|
||||
g.fillStyle='#7d8186'; g.fillRect(0,0,512,512);
|
||||
for(let i=0;i<6000;i++){
|
||||
const v=20+Math.random()*45|0;
|
||||
g.fillStyle=`rgba(${v},${v},${v+4},${.05+Math.random()*.12})`;
|
||||
g.fillRect(Math.random()*512,Math.random()*512,1.6,1.6);
|
||||
}
|
||||
const t=new THREE.CanvasTexture(c);
|
||||
t.wrapS=t.wrapT=THREE.RepeatWrapping; t.repeat.set(24,24);
|
||||
t.anisotropy=4; t.encoding=THREE.sRGBEncoding; return t;
|
||||
}
|
||||
const ground = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(220,220),
|
||||
new THREE.MeshStandardMaterial({map:makeGroundTex(),roughness:.95,metalness:0})
|
||||
);
|
||||
ground.rotation.x=-Math.PI/2; ground.receiveShadow=true; scene.add(ground);
|
||||
|
||||
/* ===================== CAMERA ===================== */
|
||||
const camera = new THREE.PerspectiveCamera(60,innerWidth/innerHeight,0.05,200);
|
||||
|
||||
// Camera modes: 'orbit' | 'follow' | 'fp'
|
||||
// orbit = standard orbit around body, lerped follow
|
||||
// follow = hard-locks orbit target to COM, player can still orbit around it
|
||||
// fp = first-person from head position, looking forward
|
||||
let camMode='orbit';
|
||||
const orbit = {yaw:.65, pitch:.18, dist:4.6, target:new THREE.Vector3(0,1,0)};
|
||||
// fp look angles (separate from orbit)
|
||||
const fp = {yaw:.65, pitch:0};
|
||||
|
||||
function updateCamera(){
|
||||
orbit.pitch = Math.max(-1.48, Math.min(1.48, orbit.pitch));
|
||||
orbit.dist = Math.max(1.0, Math.min(20, orbit.dist));
|
||||
|
||||
if(camMode==='fp'){
|
||||
// position at head, look in fp direction
|
||||
const head=parts[P.head];
|
||||
fp.pitch = Math.max(-1.4, Math.min(1.4, fp.pitch));
|
||||
const cp=Math.cos(fp.pitch);
|
||||
camera.position.copy(head.pos);
|
||||
camera.position.y += 0.06;
|
||||
const lookAt=new THREE.Vector3(
|
||||
head.pos.x + Math.sin(fp.yaw)*cp*10,
|
||||
head.pos.y + 0.06 + Math.sin(fp.pitch)*10,
|
||||
head.pos.z + Math.cos(fp.yaw)*cp*10
|
||||
);
|
||||
camera.lookAt(lookAt);
|
||||
} else {
|
||||
const cp=Math.cos(orbit.pitch);
|
||||
camera.position.set(
|
||||
orbit.target.x + Math.sin(orbit.yaw)*cp*orbit.dist,
|
||||
orbit.target.y + Math.sin(orbit.pitch)*orbit.dist,
|
||||
orbit.target.z + Math.cos(orbit.yaw)*cp*orbit.dist
|
||||
);
|
||||
camera.lookAt(orbit.target);
|
||||
}
|
||||
}
|
||||
updateCamera();
|
||||
addEventListener('resize',()=>{
|
||||
camera.aspect=innerWidth/innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(innerWidth,innerHeight);
|
||||
});
|
||||
|
||||
const camBadge=document.getElementById('cam-badge');
|
||||
const CAM_LABELS={'orbit':'ORBIT','follow':'FOLLOW','fp':'1ST PERSON'};
|
||||
function setCamMode(m){
|
||||
camMode=m;
|
||||
camBadge.textContent=CAM_LABELS[m];
|
||||
['Orbit','Follow','FP'].forEach(id=>{
|
||||
document.getElementById('cam'+id).classList.toggle('sel',id.toLowerCase()===m||(id==='FP'&&m==='fp'));
|
||||
});
|
||||
// when switching to follow, snap immediately
|
||||
if(m==='follow'||m==='fp'){
|
||||
orbit.target.copy(brain.com);
|
||||
}
|
||||
}
|
||||
// Cycle cam with V key
|
||||
addEventListener('keydown',e=>{
|
||||
if(e.key==='v'||e.key==='V'){
|
||||
const modes=['orbit','follow','fp'];
|
||||
setCamMode(modes[(modes.indexOf(camMode)+1)%3]);
|
||||
}
|
||||
});
|
||||
|
||||
/* ===================== PHYSICS ===================== */
|
||||
const GRAVITY=-9.81, AIR=0.998, PHYS_DT=1/60, SUBSTEPS=2, ITER=8;
|
||||
const parts=[], P={};
|
||||
function addPart(name,x,y,z,m,r){
|
||||
P[name]=parts.length;
|
||||
parts.push({name,
|
||||
pos:new THREE.Vector3(x,y,z),
|
||||
prev:new THREE.Vector3(x,y,z),
|
||||
force:new THREE.Vector3(),m,w:1/m,r});
|
||||
}
|
||||
const cons=[], bonded=new Set();
|
||||
function pk(i,j){return i<j?i+'_'+j:j+'_'+i;}
|
||||
function link(a,b,stiff=1,type='eq',rest=null){
|
||||
const i=P[a],j=P[b];
|
||||
const d=rest!==null?rest:parts[i].pos.distanceTo(parts[j].pos);
|
||||
cons.push({i,j,rest:d,stiff,type});
|
||||
if(type==='eq') bonded.add(pk(i,j));
|
||||
}
|
||||
// Reusable scratch vectors — never allocate in hot loops
|
||||
const _d=new THREE.Vector3(),_v=new THREE.Vector3(),
|
||||
_t=new THREE.Vector3(),_t2=new THREE.Vector3(),
|
||||
_tmp=new THREE.Vector3();
|
||||
|
||||
function projectCon(c){
|
||||
const a=parts[c.i],b=parts[c.j];
|
||||
_d.subVectors(b.pos,a.pos); const L=_d.length(); if(L<1e-9)return;
|
||||
if(c.type==='max'&&L<=c.rest)return;
|
||||
if(c.type==='min'&&L>=c.rest)return;
|
||||
const diff=(L-c.rest)/L, ws=a.w+b.w; if(!ws)return;
|
||||
const s=c.stiff;
|
||||
a.pos.addScaledVector(_d, diff*s*a.w/ws);
|
||||
b.pos.addScaledVector(_d,-diff*s*b.w/ws);
|
||||
}
|
||||
function integrate(h){
|
||||
for(const p of parts){
|
||||
_v.subVectors(p.pos,p.prev).multiplyScalar(AIR);
|
||||
p.prev.copy(p.pos); p.pos.add(_v);
|
||||
p.pos.y+=GRAVITY*h*h;
|
||||
p.pos.addScaledVector(p.force,p.w*h*h);
|
||||
}
|
||||
}
|
||||
function collideGround(){
|
||||
for(const p of parts){
|
||||
if(p.pos.y<p.r){
|
||||
const vy=p.pos.y-p.prev.y;
|
||||
p.pos.y=p.r; p.prev.y=p.r+vy*0.25;
|
||||
p.prev.x+=(p.pos.x-p.prev.x)*0.6;
|
||||
p.prev.z+=(p.pos.z-p.prev.z)*0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
function collideSelf(){
|
||||
for(let i=0;i<parts.length;i++) for(let j=i+1;j<parts.length;j++){
|
||||
if(bonded.has(i+'_'+j)) continue;
|
||||
const a=parts[i],b=parts[j],rr=a.r+b.r;
|
||||
_d.subVectors(b.pos,a.pos); const L2=_d.lengthSq();
|
||||
if(L2>rr*rr||L2<1e-10) continue;
|
||||
const L=Math.sqrt(L2),diff=(L-rr)/L,w=a.w+b.w;
|
||||
a.pos.addScaledVector(_d, diff*0.7*a.w/w);
|
||||
b.pos.addScaledVector(_d,-diff*0.7*b.w/w);
|
||||
}
|
||||
}
|
||||
function addImpulse(p,dv){ p.prev.addScaledVector(dv,-PHYS_DT/SUBSTEPS); }
|
||||
|
||||
/* ===================== PARTICLES ===================== */
|
||||
const sparks=[];
|
||||
// Shared geometry/material pool for performance
|
||||
const _sparkGeo=new THREE.SphereGeometry(.025,4,3);
|
||||
const _smokeGeo=new THREE.SphereGeometry(.1,5,4);
|
||||
const _smokeMat=new THREE.MeshBasicMaterial({color:0x555566,transparent:true,opacity:.4});
|
||||
|
||||
function spawnSparks(pos,count,color,speed,life){
|
||||
for(let i=0;i<count;i++){
|
||||
const dir=new THREE.Vector3(Math.random()-.5,Math.random()*.8+.2,Math.random()-.5).normalize();
|
||||
const m=new THREE.Mesh(_sparkGeo, new THREE.MeshBasicMaterial({color}));
|
||||
m.position.copy(pos); scene.add(m);
|
||||
sparks.push({mesh:m,vel:dir.multiplyScalar(speed*(.4+Math.random()*.8)),life,maxLife:life,grav:true});
|
||||
}
|
||||
}
|
||||
function spawnSmoke(pos,count){
|
||||
for(let i=0;i<count;i++){
|
||||
const off=new THREE.Vector3(Math.random()-.5,Math.random()*.3,Math.random()-.5).multiplyScalar(.3);
|
||||
const m=new THREE.Mesh(_smokeGeo,_smokeMat.clone());
|
||||
m.position.copy(pos).add(off); scene.add(m);
|
||||
sparks.push({mesh:m,vel:new THREE.Vector3((Math.random()-.5)*.5,.4+Math.random()*.5,(Math.random()-.5)*.5),
|
||||
life:1.2+Math.random(),maxLife:1.6,grav:false,smoke:true});
|
||||
}
|
||||
}
|
||||
function stepSparks(dt){
|
||||
for(let i=sparks.length-1;i>=0;i--){
|
||||
const s=sparks[i]; s.life-=dt;
|
||||
if(s.life<=0){scene.remove(s.mesh);sparks.splice(i,1);continue;}
|
||||
s.mesh.position.addScaledVector(s.vel,dt);
|
||||
if(s.grav) s.vel.y+=GRAVITY*dt*.3;
|
||||
const f=s.life/s.maxLife;
|
||||
if(s.smoke){s.mesh.material.opacity=.35*f;s.mesh.scale.setScalar(1+.5*(1-f));}
|
||||
else s.mesh.material.color.setHSL(.07*f,1,.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* flash overlay */
|
||||
const flashEl=document.getElementById('flash');
|
||||
let flashT=0;
|
||||
function triggerFlash(i){ flashT=.18*i; flashEl.style.opacity=String(Math.min(.85,.45*i)); setTimeout(()=>flashEl.style.opacity='0',80); }
|
||||
function stepFlash(dt){ flashT=Math.max(0,flashT-dt); flashPt.intensity=flashT*16; }
|
||||
|
||||
/* ===================== EXPLOSIONS ===================== */
|
||||
function explodeAt(pos, radius, force){
|
||||
for(const p of parts){
|
||||
_tmp.subVectors(p.pos,pos);
|
||||
const dist=_tmp.length();
|
||||
if(dist<radius){
|
||||
const fall=1-dist/radius;
|
||||
_tmp.normalize().multiplyScalar(force*fall*fall);
|
||||
_tmp.y+=force*fall*.5;
|
||||
addImpulse(p,_tmp);
|
||||
}
|
||||
}
|
||||
setState('ko'); brain.koT=Math.max(brain.koT,4.5);
|
||||
}
|
||||
|
||||
/* ===================== C4 ===================== */
|
||||
const c4List=[];
|
||||
const c4Geo=new THREE.BoxGeometry(.22,.08,.14);
|
||||
const c4Mat=new THREE.MeshStandardMaterial({color:0xd4c98a,roughness:.85});
|
||||
function placeC4(pos){
|
||||
const mesh=new THREE.Mesh(c4Geo,c4Mat);
|
||||
mesh.position.copy(pos); mesh.position.y=.041;
|
||||
mesh.rotation.y=Math.random()*Math.PI;
|
||||
mesh.castShadow=true; scene.add(mesh);
|
||||
const led=new THREE.Mesh(new THREE.SphereGeometry(.018,6,6),new THREE.MeshBasicMaterial({color:0xff2222}));
|
||||
led.position.set(.08,.055,0); mesh.add(led);
|
||||
c4List.push({mesh,pos:mesh.position.clone(),ledMesh:led,blink:0,armed:true});
|
||||
}
|
||||
function detonateC4(){
|
||||
let any=false;
|
||||
for(const c of c4List){
|
||||
if(!c.armed)continue; any=true;
|
||||
flashPt.position.copy(c.pos).setY(.5);
|
||||
triggerFlash(2.8); explodeAt(c.pos,4.2,68);
|
||||
spawnSparks(c.pos.clone().setY(.1),35,0xff6600,5,1);
|
||||
spawnSmoke(c.pos.clone().setY(.1),14);
|
||||
scene.remove(c.mesh);
|
||||
}
|
||||
c4List.length=0;
|
||||
if(any) snapCameraToBody();
|
||||
}
|
||||
function stepC4(dt){
|
||||
for(const c of c4List){
|
||||
c.blink+=dt;
|
||||
c.ledMesh.material.color.setHex(c.blink%0.6<0.1?0xff0000:0x440000);
|
||||
}
|
||||
}
|
||||
function snapCameraToBody(){
|
||||
orbit.target.copy(brain.com);
|
||||
orbit.dist=Math.max(orbit.dist,6);
|
||||
}
|
||||
|
||||
/* ===================== PROJECTILES ===================== */
|
||||
const balls=[];
|
||||
const ballGeo=new THREE.SphereGeometry(1,14,10);
|
||||
const BMATS={
|
||||
normal: new THREE.MeshStandardMaterial({color:0xd04020,roughness:.55,metalness:.1}),
|
||||
gun: new THREE.MeshStandardMaterial({color:0xd4aa44,roughness:.15,metalness:.95}),
|
||||
HE: new THREE.MeshStandardMaterial({color:0xcc3311,roughness:.4,metalness:.5,emissive:0x441100}),
|
||||
AP: new THREE.MeshStandardMaterial({color:0x44aa66,roughness:.25,metalness:.9,emissive:0x001a0a}),
|
||||
cannon: new THREE.MeshStandardMaterial({color:0x445566,roughness:.3,metalness:.8}),
|
||||
};
|
||||
|
||||
function spawnBall(pos,vel,r,m,type='normal'){
|
||||
const mesh=new THREE.Mesh(ballGeo,BMATS[type]||BMATS.normal);
|
||||
mesh.castShadow=true; mesh.scale.setScalar(r); scene.add(mesh);
|
||||
balls.push({pos:pos.clone(),vel:vel.clone(),r,m,life:12,mesh,type,hit:false});
|
||||
}
|
||||
|
||||
function onBallHitPart(ball, part){
|
||||
_d.subVectors(part.pos,ball.pos).normalize();
|
||||
const vn=ball.vel.dot(_d);
|
||||
if(vn<=0) return false;
|
||||
if(ball.type==='gun'){
|
||||
addImpulse(part, _tmp.copy(_d).multiplyScalar(vn*ball.m*3));
|
||||
ball.vel.multiplyScalar(.3);
|
||||
return false;
|
||||
} else if(ball.type==='HE'){
|
||||
return true;
|
||||
} else if(ball.type==='AP'){
|
||||
addImpulse(part, _tmp.copy(_d).multiplyScalar(vn*ball.m*4));
|
||||
spawnSparks(ball.pos.clone(),8,0x88ffaa,3,.35);
|
||||
ball.vel.multiplyScalar(.45);
|
||||
return false;
|
||||
} else {
|
||||
const share=2*ball.m/(ball.m+part.m);
|
||||
addImpulse(part, _tmp.copy(_d).multiplyScalar(vn*share*.9));
|
||||
ball.vel.addScaledVector(_d,-vn*1.4);
|
||||
ball.pos.copy(part.pos).addScaledVector(_d,-(part.r+ball.r));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function stepBalls(h){
|
||||
for(let k=balls.length-1;k>=0;k--){
|
||||
const b=balls[k]; b.life-=h;
|
||||
b.vel.y+=GRAVITY*h;
|
||||
b.pos.addScaledVector(b.vel,h);
|
||||
if(b.pos.y<b.r){
|
||||
b.pos.y=b.r;
|
||||
if(b.type==='HE'){ doExplodeBall(k); continue; }
|
||||
b.vel.y*=-.45; b.vel.x*=.86; b.vel.z*=.86;
|
||||
}
|
||||
let remove=false;
|
||||
for(const p of parts){
|
||||
_d.subVectors(p.pos,b.pos);
|
||||
if(_d.lengthSq()<(p.r+b.r)*(p.r+b.r)){
|
||||
remove=onBallHitPart(b,p);
|
||||
if(remove) break;
|
||||
}
|
||||
}
|
||||
if(remove){ doExplodeBall(k); continue; }
|
||||
b.mesh.position.copy(b.pos);
|
||||
if(b.life<=0){ scene.remove(b.mesh); balls.splice(k,1); }
|
||||
}
|
||||
}
|
||||
function doExplodeBall(k){
|
||||
const b=balls[k];
|
||||
flashPt.position.copy(b.pos);
|
||||
triggerFlash(1.8); explodeAt(b.pos,2.6,52);
|
||||
spawnSparks(b.pos.clone(),22,0xff5500,5,.7);
|
||||
spawnSmoke(b.pos.clone(),8);
|
||||
snapCameraToBody();
|
||||
scene.remove(b.mesh); balls.splice(k,1);
|
||||
}
|
||||
|
||||
function stepPhysics(){
|
||||
const h=PHYS_DT/SUBSTEPS;
|
||||
for(let s=0;s<SUBSTEPS;s++){
|
||||
integrate(h);
|
||||
for(let it=0;it<ITER;it++){
|
||||
for(const c of cons) projectCon(c);
|
||||
collideSelf(); collideGround();
|
||||
if(grab.active){ parts[grab.pi].pos.lerp(grab.target,.35); }
|
||||
}
|
||||
stepBalls(h);
|
||||
}
|
||||
for(const p of parts) p.force.set(0,0,0);
|
||||
}
|
||||
|
||||
/* ===================== SKELETON ===================== */
|
||||
addPart('pelvis', 0, .95, 0, 12, .125);
|
||||
addPart('chest', 0, 1.24, 0, 13, .14);
|
||||
addPart('neck', 0, 1.44, 0, 2, .055);
|
||||
addPart('head', 0, 1.61, 0, 5, .105);
|
||||
addPart('shoulderL', -.21, 1.38, 0, 2.5, .06);
|
||||
addPart('shoulderR', .21, 1.38, 0, 2.5, .06);
|
||||
addPart('elbowL', -.25, 1.12, .015,1.6, .05);
|
||||
addPart('elbowR', .25, 1.12, .015,1.6, .05);
|
||||
addPart('handL', -.27, .875, .03, 1.2, .047);
|
||||
addPart('handR', .27, .875, .03, 1.2, .047);
|
||||
addPart('hipL', -.115, .91, 0, 5, .075);
|
||||
addPart('hipR', .115, .91, 0, 5, .075);
|
||||
addPart('kneeL', -.125, .49, .03, 3.5, .058);
|
||||
addPart('kneeR', .125, .49, .03, 3.5, .058);
|
||||
addPart('footL', -.13, .05, -.03, 1.8, .05);
|
||||
addPart('footR', .13, .05, -.03, 1.8, .05);
|
||||
addPart('toeL', -.13, .042, .135, .6, .042);
|
||||
addPart('toeR', .13, .042, .135, .6, .042);
|
||||
|
||||
link('pelvis','chest'); link('chest','neck'); link('neck','head'); link('pelvis','neck',.9);
|
||||
link('shoulderL','shoulderR'); link('chest','shoulderL'); link('chest','shoulderR');
|
||||
link('neck','shoulderL',.9); link('neck','shoulderR',.9);
|
||||
link('pelvis','shoulderL',.55); link('pelvis','shoulderR',.55);
|
||||
link('pelvis','hipL'); link('pelvis','hipR'); link('hipL','hipR');
|
||||
link('chest','hipL',.8); link('chest','hipR',.8);
|
||||
link('shoulderL','elbowL'); link('elbowL','handL');
|
||||
link('shoulderR','elbowR'); link('elbowR','handR');
|
||||
link('hipL','kneeL'); link('kneeL','footL'); link('footL','toeL'); link('kneeL','toeL',.7);
|
||||
link('hipR','kneeR'); link('kneeR','footR'); link('footR','toeR'); link('kneeR','toeR',.7);
|
||||
link('shoulderL','handL',.8,'min',.18); link('shoulderR','handR',.8,'min',.18);
|
||||
link('hipL','footL',.8,'min',.32); link('hipR','footR',.8,'min',.32);
|
||||
link('head','chest',.5,'max',.39); link('head','chest',.5,'min',.31);
|
||||
link('kneeL','kneeR',.6,'min',.09); link('footL','footR',.6,'min',.08);
|
||||
link('head','pelvis',.6,'min',.46);
|
||||
|
||||
/* ===================== VISUALS ===================== */
|
||||
const matSuit = new THREE.MeshStandardMaterial({color:0x4b5563,roughness:.74,metalness:.05});
|
||||
const matSuit2 = new THREE.MeshStandardMaterial({color:0x39414d,roughness:.8,metalness:.05});
|
||||
const matSkin = new THREE.MeshStandardMaterial({color:0xc99c7d,roughness:.55,metalness:0});
|
||||
const matShoe = new THREE.MeshStandardMaterial({color:0x22262d,roughness:.42,metalness:.1});
|
||||
const rig=new THREE.Group(); scene.add(rig);
|
||||
const limbs=[], pickMeshes=[];
|
||||
const UP=new THREE.Vector3(0,1,0);
|
||||
function makeLimb(a,b,ra,rb,mat){
|
||||
const grp=new THREE.Group();
|
||||
const cyl=new THREE.Mesh(new THREE.CylinderGeometry(rb,ra,1,12,1,true),mat);
|
||||
cyl.castShadow=true; grp.add(cyl);
|
||||
const capA=new THREE.Mesh(new THREE.SphereGeometry(ra,12,8),mat);
|
||||
capA.castShadow=true; grp.add(capA);
|
||||
const capB=new THREE.Mesh(new THREE.SphereGeometry(rb,12,8),mat);
|
||||
capB.castShadow=true; grp.add(capB);
|
||||
rig.add(grp);
|
||||
const L={grp,cyl,capA,capB,ia:P[a],ib:P[b]};
|
||||
limbs.push(L);
|
||||
for(const m of [cyl,capA,capB]){ m.userData.pick=[P[a],P[b]]; pickMeshes.push(m); }
|
||||
return L;
|
||||
}
|
||||
makeLimb('pelvis','chest',.135,.15,matSuit);
|
||||
makeLimb('chest','neck',.1,.055,matSuit);
|
||||
makeLimb('neck','head',.05,.05,matSkin);
|
||||
makeLimb('shoulderL','shoulderR',.055,.055,matSuit);
|
||||
makeLimb('hipL','hipR',.07,.07,matSuit2);
|
||||
makeLimb('shoulderL','elbowL',.055,.045,matSuit);
|
||||
makeLimb('shoulderR','elbowR',.055,.045,matSuit);
|
||||
makeLimb('elbowL','handL',.042,.034,matSuit);
|
||||
makeLimb('elbowR','handR',.042,.034,matSuit);
|
||||
makeLimb('hipL','kneeL',.075,.055,matSuit2);
|
||||
makeLimb('hipR','kneeR',.075,.055,matSuit2);
|
||||
makeLimb('kneeL','footL',.052,.04,matSuit2);
|
||||
makeLimb('kneeR','footR',.052,.04,matSuit2);
|
||||
makeLimb('footL','toeL',.05,.044,matShoe);
|
||||
makeLimb('footR','toeR',.05,.044,matShoe);
|
||||
const headMesh=new THREE.Mesh(new THREE.SphereGeometry(.105,18,14),matSkin);
|
||||
headMesh.castShadow=true; headMesh.scale.set(.92,1.12,.98);
|
||||
headMesh.userData.pick=[P.head]; rig.add(headMesh); pickMeshes.push(headMesh);
|
||||
const handMeshes=['handL','handR'].map(n=>{
|
||||
const m=new THREE.Mesh(new THREE.SphereGeometry(.05,10,8),matSkin);
|
||||
m.castShadow=true; m.scale.set(.8,1.15,1);
|
||||
m.userData.pick=[P[n]]; rig.add(m); pickMeshes.push(m); return m;
|
||||
});
|
||||
const _q=new THREE.Quaternion();
|
||||
function syncVisuals(){
|
||||
for(const L of limbs){
|
||||
const a=parts[L.ia].pos, b=parts[L.ib].pos;
|
||||
const len=a.distanceTo(b);
|
||||
L.grp.position.copy(a).add(b).multiplyScalar(.5);
|
||||
if(len>1e-6){
|
||||
_d.subVectors(b,a).multiplyScalar(1/len);
|
||||
_q.setFromUnitVectors(UP,_d); L.grp.quaternion.copy(_q);
|
||||
}
|
||||
L.cyl.scale.set(1,Math.max(len,1e-4),1);
|
||||
L.capA.position.y=-len/2; L.capB.position.y=len/2;
|
||||
}
|
||||
headMesh.position.copy(parts[P.head].pos);
|
||||
headMesh.quaternion.copy(limbs[2].grp.quaternion);
|
||||
handMeshes[0].position.copy(parts[P.handL].pos);
|
||||
handMeshes[1].position.copy(parts[P.handR].pos);
|
||||
}
|
||||
|
||||
const POSE={};
|
||||
for(const p of parts) POSE[p.name]=p.pos.clone();
|
||||
|
||||
/* ===================== BRAIN ===================== */
|
||||
const GAIN={pelvis:[72,9],chest:[72,9],neck:[55,7],head:[48,7],shoulder:[48,7],
|
||||
elbow:[30,5],hand:[24,4],hip:[72,9],knee:[62,8],foot:[85,10],toe:[65,8]};
|
||||
const GCOMP={pelvis:.92,chest:.92,neck:.9,head:.9,shoulder:.85,
|
||||
elbow:.7,hand:.6,hip:.9,knee:.55,foot:0,toe:0};
|
||||
function groupOf(n){return n.replace(/[LR]$/,'');}
|
||||
|
||||
const brain={
|
||||
state:'balance',t:0,koT:0,strength:1,
|
||||
root:new THREE.Vector3(),
|
||||
com:new THREE.Vector3(),comPrev:new THREE.Vector3(0,1,0),comVel:new THREE.Vector3(),
|
||||
step:null,lastFoot:'R',cool:0,targets:{},totalM:0
|
||||
};
|
||||
for(const p of parts){ brain.targets[p.name]=new THREE.Vector3(); brain.totalM+=p.m; }
|
||||
const _right=new THREE.Vector3(),_fwd=new THREE.Vector3(),
|
||||
_off=new THREE.Vector3(),_feet=new THREE.Vector3(),_fall=new THREE.Vector3();
|
||||
let simTime=0;
|
||||
function setState(s){ if(brain.state!==s){brain.state=s;brain.t=0;} }
|
||||
|
||||
function brainUpdate(dt){
|
||||
simTime+=dt; brain.t+=dt; brain.cool=Math.max(0,brain.cool-dt);
|
||||
const B=brain, T=B.targets;
|
||||
_right.subVectors(parts[P.hipR].pos,parts[P.hipL].pos); _right.y=0;
|
||||
if(_right.lengthSq()<1e-6) _right.set(1,0,0); else _right.normalize();
|
||||
_fwd.set(-_right.z,0,_right.x);
|
||||
B.com.set(0,0,0);
|
||||
for(const p of parts) B.com.addScaledVector(p.pos,p.m/B.totalM);
|
||||
B.comVel.subVectors(B.com,B.comPrev).multiplyScalar(1/dt); B.comPrev.copy(B.com);
|
||||
_feet.addVectors(parts[P.footL].pos,parts[P.footR].pos).multiplyScalar(.5);
|
||||
_off.subVectors(B.com,_feet); _off.y=0;
|
||||
_off.addScaledVector(_t.copy(B.comVel).setY(0),.16);
|
||||
const offLen=_off.length();
|
||||
_t2.subVectors(parts[P.neck].pos,parts[P.pelvis].pos).normalize();
|
||||
const upr=_t2.y, comSpeed=B.comVel.length();
|
||||
|
||||
if(B.state==='balance'){
|
||||
B.strength+=(1-B.strength)*Math.min(1,dt*5);
|
||||
if(upr<.55||offLen>.34) setState('falling');
|
||||
} else if(B.state==='falling'){
|
||||
B.strength=.3;
|
||||
if(B.com.y<.55&&comSpeed<1.8) setState('down');
|
||||
else if(upr>.78&&offLen<.12&&B.com.y>.85) setState('balance');
|
||||
} else if(B.state==='down'){
|
||||
B.strength=.06;
|
||||
if(B.t>1.25) setState('getup');
|
||||
if(upr>.78&&B.com.y>.85) setState('balance');
|
||||
} else if(B.state==='getup'){
|
||||
B.strength=1.25;
|
||||
if(upr>.82&&B.com.y>.88&&offLen<.2) setState('balance');
|
||||
if(B.t>3) setState('down');
|
||||
} else if(B.state==='ko'){
|
||||
B.strength=.015; B.koT-=dt;
|
||||
if(B.koT<=0) setState('down');
|
||||
}
|
||||
const S=B.strength;
|
||||
|
||||
if(B.state==='balance'||B.state==='falling'){
|
||||
_t.copy(_feet); _t.y=0; B.root.lerp(_t,1-Math.exp(-dt*6));
|
||||
} else { B.root.set(B.com.x,0,B.com.z); }
|
||||
|
||||
for(const p of parts){
|
||||
const o=POSE[p.name];
|
||||
T[p.name].copy(B.root).addScaledVector(_right,o.x).addScaledVector(_fwd,o.z).setY(o.y);
|
||||
}
|
||||
const br=Math.sin(simTime*1.7)*.006, sw=Math.sin(simTime*.9)*.012;
|
||||
T.chest.y+=br; T.head.y+=br*1.4;
|
||||
T.head.addScaledVector(_fwd,sw); T.chest.addScaledVector(_fwd,sw*.5);
|
||||
|
||||
if(B.state==='balance'){
|
||||
T.chest.addScaledVector(_off,-.55); T.head.addScaledVector(_off,-.8); T.pelvis.addScaledVector(_off,-.25);
|
||||
const inst=Math.min(1,offLen*5);
|
||||
if(inst>.15){
|
||||
const wave=Math.sin(simTime*11)*.16*inst;
|
||||
T.handL.addScaledVector(_right,-(0.12+.26*inst)).addScaledVector(_fwd,wave).y+=.30*inst;
|
||||
T.handR.addScaledVector(_right, (0.12+.26*inst)).addScaledVector(_fwd,-wave).y+=.30*inst;
|
||||
T.elbowL.addScaledVector(_right,-.16*inst).y+=.14*inst;
|
||||
T.elbowR.addScaledVector(_right, .16*inst).y+=.14*inst;
|
||||
}
|
||||
if(!B.step&&B.cool<=0&&offLen>.12){
|
||||
const dir=_t.copy(_off).normalize();
|
||||
const foot=(B.lastFoot==='R')?'L':'R';
|
||||
const to=new THREE.Vector3(B.com.x,0,B.com.z)
|
||||
.addScaledVector(dir,.24).addScaledVector(_right,foot==='L'?-.10:.10);
|
||||
B.step={foot,from:parts[P['foot'+foot]].pos.clone(),to,t:0,dur:.22};
|
||||
B.lastFoot=foot;
|
||||
}
|
||||
}
|
||||
if(B.step){
|
||||
const st=B.step; st.t+=dt;
|
||||
const f=Math.min(1,st.t/st.dur), lift=Math.sin(f*Math.PI)*.14;
|
||||
const ft=T['foot'+st.foot];
|
||||
ft.lerpVectors(st.from,st.to,f); ft.y=.05+lift;
|
||||
T['toe'+st.foot].copy(ft).addScaledVector(_fwd,.15); T['toe'+st.foot].y=ft.y-.005;
|
||||
if(f>=1){B.step=null;B.cool=.07;}
|
||||
}
|
||||
if(B.state==='falling'){
|
||||
_fall.copy(B.comVel); _fall.y=0;
|
||||
if(_fall.lengthSq()<.01) _fall.copy(_off);
|
||||
if(_fall.lengthSq()>1e-6) _fall.normalize();
|
||||
T.handL.copy(parts[P.shoulderL].pos).addScaledVector(_fall,.42).y=Math.max(.15,parts[P.shoulderL].pos.y-.25);
|
||||
T.handR.copy(parts[P.shoulderR].pos).addScaledVector(_fall,.42).y=Math.max(.15,parts[P.shoulderR].pos.y-.25);
|
||||
T.elbowL.copy(parts[P.shoulderL].pos).addScaledVector(_fall,.2);
|
||||
T.elbowR.copy(parts[P.shoulderR].pos).addScaledVector(_fall,.2);
|
||||
T.head.copy(parts[P.neck].pos).addScaledVector(_fall,-.12).y+=.12;
|
||||
T.kneeL.y+=.15; T.kneeR.y+=.15;
|
||||
}
|
||||
const getup=B.state==='getup', ramp=getup?Math.min(1,B.t/.45):1;
|
||||
for(const p of parts){
|
||||
const g=GAIN[groupOf(p.name)];
|
||||
_t.subVectors(T[p.name],p.pos);
|
||||
const errLen=_t.length();
|
||||
if(errLen>.6) _t.multiplyScalar(.6/errLen);
|
||||
_v.subVectors(p.pos,p.prev).multiplyScalar(1/dt);
|
||||
const kp=g[0]*p.m*S*ramp, kd=g[1]*p.m*Math.min(1,S*1.5);
|
||||
p.force.addScaledVector(_t,kp).addScaledVector(_v,-kd);
|
||||
p.force.y+=-GRAVITY*p.m*GCOMP[groupOf(p.name)]*Math.min(1,S)*ramp;
|
||||
if(getup&&(p.name==='pelvis'||p.name==='chest')){
|
||||
p.force.y+=-GRAVITY*p.m*.5*ramp;
|
||||
p.prev.x+=(p.pos.x-p.prev.x)*.04;
|
||||
p.prev.z+=(p.pos.z-p.prev.z)*.04;
|
||||
}
|
||||
const fmax=p.m*260;
|
||||
if(p.force.lengthSq()>fmax*fmax) p.force.setLength(fmax);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== WEAPON STATE ===================== */
|
||||
let currentWeapon='ball';
|
||||
let currentAmmo='normal';
|
||||
let isFiring=false;
|
||||
let gunFireCooldown=0;
|
||||
|
||||
function selectWeapon(w){
|
||||
currentWeapon=w;
|
||||
['Ball','Gun','Cannon','C4'].forEach(id=>{
|
||||
const btn=document.getElementById('w'+id);
|
||||
if(btn) btn.classList.toggle('sel', id.toLowerCase()===w);
|
||||
});
|
||||
document.getElementById('ammo-row').style.display=(w==='cannon')?'flex':'none';
|
||||
}
|
||||
function selectAmmo(a){
|
||||
currentAmmo=a;
|
||||
['Normal','HE','AP'].forEach(id=>{
|
||||
document.getElementById('am'+id).classList.toggle('active-ammo',id.toLowerCase()===a);
|
||||
});
|
||||
}
|
||||
|
||||
// Pre-allocated ray/ndc to avoid per-click allocation
|
||||
const _fireRay=new THREE.Raycaster();
|
||||
const _fireNDC=new THREE.Vector2();
|
||||
|
||||
function fireWeapon(nx, ny){
|
||||
_fireNDC.set(nx,ny);
|
||||
_fireRay.setFromCamera(_fireNDC,camera);
|
||||
const origin=_fireRay.ray.origin.clone().addScaledVector(_fireRay.ray.direction,.5);
|
||||
const dir=_fireRay.ray.direction.clone();
|
||||
|
||||
if(currentWeapon==='ball'){
|
||||
spawnBall(origin, dir.multiplyScalar(15), .09, 3.5, 'normal');
|
||||
} else if(currentWeapon==='gun'){
|
||||
spawnBall(origin, dir.multiplyScalar(55), .04, 2, 'gun');
|
||||
flashPt.position.copy(origin); flashPt.intensity=8;
|
||||
setTimeout(()=>flashPt.intensity=0,35);
|
||||
spawnSparks(origin.clone(),6,0xffdd66,4,.2);
|
||||
} else if(currentWeapon==='cannon'){
|
||||
let speed,radius,mass,type,flashMult;
|
||||
if(currentAmmo==='HE'){ speed=26;radius=.17;mass=12;type='HE';flashMult=1.2;}
|
||||
else if(currentAmmo==='AP'){ speed=40;radius=.10;mass=9; type='AP';flashMult=.8;}
|
||||
else{ speed=22;radius=.15;mass=10;type='cannon';flashMult=1;}
|
||||
const spawnPt=_fireRay.ray.origin.clone().addScaledVector(dir,.8);
|
||||
spawnBall(spawnPt, dir.multiplyScalar(speed), radius, mass, type);
|
||||
triggerFlash(flashMult);
|
||||
spawnSparks(spawnPt.clone(),10,0xffaa22,3,.25);
|
||||
} else if(currentWeapon==='c4'){
|
||||
// ground intersection
|
||||
const dy=_fireRay.ray.direction.y;
|
||||
if(dy<-0.01){
|
||||
const t2=_fireRay.ray.origin.y/(-dy);
|
||||
if(t2>0){
|
||||
const gp=_fireRay.ray.origin.clone().addScaledVector(_fireRay.ray.direction,t2);
|
||||
placeC4(gp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== INPUT ===================== */
|
||||
const grab={active:false,pi:0,dist:0,target:new THREE.Vector3()};
|
||||
const _grabRay=new THREE.Raycaster();
|
||||
const _grabNDC=new THREE.Vector2();
|
||||
let dragMode=null, lastX=0, lastY=0, movedPx=0;
|
||||
|
||||
function setGrabNDC(e){ _grabNDC.set((e.clientX/innerWidth)*2-1,-(e.clientY/innerHeight)*2+1); }
|
||||
// Live ndc for held-fire (updated every mousemove)
|
||||
const _liveNDC={x:0,y:0};
|
||||
|
||||
renderer.domElement.addEventListener('pointerdown',e=>{
|
||||
e.preventDefault();
|
||||
renderer.domElement.setPointerCapture(e.pointerId);
|
||||
lastX=e.clientX; lastY=e.clientY; movedPx=0;
|
||||
_liveNDC.x=(e.clientX/innerWidth)*2-1;
|
||||
_liveNDC.y=-(e.clientY/innerHeight)*2+1;
|
||||
|
||||
if(e.button===2){
|
||||
// RIGHT CLICK — grab ragdoll only
|
||||
setGrabNDC(e); _grabRay.setFromCamera(_grabNDC,camera);
|
||||
const hits=_grabRay.intersectObjects(pickMeshes,false);
|
||||
if(hits.length){
|
||||
const h=hits[0], pk2=h.object.userData.pick;
|
||||
let best=pk2[0];
|
||||
if(pk2.length>1&&h.point.distanceToSquared(parts[pk2[1]].pos)<h.point.distanceToSquared(parts[pk2[0]].pos))
|
||||
best=pk2[1];
|
||||
grab.active=true; grab.pi=best; grab.dist=h.distance; grab.target.copy(h.point);
|
||||
dragMode='grab';
|
||||
} else {
|
||||
dragMode='orbit';
|
||||
}
|
||||
} else if(e.button===0){
|
||||
// LEFT CLICK — ONLY fire or orbit. Never grabs body.
|
||||
dragMode='mayfire'; // will become 'orbit' if dragged, or fire on up
|
||||
if(currentWeapon==='gun'||currentWeapon==='cannon'){
|
||||
isFiring=true;
|
||||
gunFireCooldown=0;
|
||||
fireWeapon(_liveNDC.x,_liveNDC.y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
renderer.domElement.addEventListener('pointermove',e=>{
|
||||
const dx=e.clientX-lastX, dy=e.clientY-lastY;
|
||||
lastX=e.clientX; lastY=e.clientY;
|
||||
movedPx+=Math.abs(dx)+Math.abs(dy);
|
||||
_liveNDC.x=(e.clientX/innerWidth)*2-1;
|
||||
_liveNDC.y=-(e.clientY/innerHeight)*2+1;
|
||||
|
||||
if(dragMode==='orbit'||dragMode==='mayfire'&&movedPx>5){
|
||||
dragMode='orbit'; isFiring=false;
|
||||
if(camMode==='fp'){
|
||||
fp.yaw-=dx*.004; fp.pitch+=dy*.004;
|
||||
} else {
|
||||
orbit.yaw-=dx*.005; orbit.pitch+=dy*.005;
|
||||
}
|
||||
updateCamera();
|
||||
} else if(dragMode==='grab'){
|
||||
setGrabNDC(e); _grabRay.setFromCamera(_grabNDC,camera);
|
||||
grab.target.copy(_grabRay.ray.origin).addScaledVector(_grabRay.ray.direction,grab.dist);
|
||||
grab.target.y=Math.max(.06,grab.target.y);
|
||||
}
|
||||
});
|
||||
|
||||
renderer.domElement.addEventListener('pointerup',e=>{
|
||||
if(dragMode==='mayfire'&&movedPx<6&&e.button===0){
|
||||
// clean click — fire once (gun/cannon already fired on down)
|
||||
if(currentWeapon!=='gun'&¤tWeapon!=='cannon'){
|
||||
fireWeapon(_liveNDC.x,_liveNDC.y);
|
||||
}
|
||||
}
|
||||
grab.active=false; dragMode=null; isFiring=false;
|
||||
});
|
||||
|
||||
renderer.domElement.addEventListener('wheel',e=>{
|
||||
e.preventDefault(); orbit.dist*=(e.deltaY>0?1.1:.9); updateCamera();
|
||||
},{passive:false});
|
||||
|
||||
addEventListener('contextmenu',e=>e.preventDefault());
|
||||
|
||||
/* ===================== UI ===================== */
|
||||
const stateEl=document.getElementById('state');
|
||||
const STATE_STYLE={
|
||||
balance:['BALANCING','#173527','#7ce0a3'],
|
||||
falling:['STUMBLING','#3a2c14','#f0b46a'],
|
||||
down:['DOWN','#2b2f36','#aab6c6'],
|
||||
getup:['GETTING UP','#15283f','#7db8f5'],
|
||||
ko:['KNOCKED OUT','#3a1717','#f08a8a']
|
||||
};
|
||||
function syncBadge(){
|
||||
const s=STATE_STYLE[brain.state];
|
||||
if(stateEl.textContent!==s[0]){
|
||||
stateEl.textContent=s[0]; stateEl.style.background=s[1];
|
||||
stateEl.style.color=s[2]; stateEl.style.borderColor=s[2]+'44';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('bShove').onclick=()=>{
|
||||
const a=Math.random()*Math.PI*2;
|
||||
_t.set(Math.cos(a)*4.2,1.2,Math.sin(a)*4.2);
|
||||
addImpulse(parts[P.chest],_t);
|
||||
addImpulse(parts[P.pelvis],_t2.copy(_t).multiplyScalar(.55));
|
||||
};
|
||||
document.getElementById('bKO').onclick=()=>{ brain.koT=3.5; setState('ko'); };
|
||||
let slow=false;
|
||||
document.getElementById('bSlow').onclick=function(){ slow=!slow; this.classList.toggle('on',slow); };
|
||||
document.getElementById('bReset').onclick=()=>{
|
||||
for(const p of parts){ p.pos.copy(POSE[p.name]); p.prev.copy(POSE[p.name]); p.force.set(0,0,0); }
|
||||
for(const b of balls) scene.remove(b.mesh); balls.length=0;
|
||||
for(const c of c4List) scene.remove(c.mesh); c4List.length=0;
|
||||
for(const s of sparks) scene.remove(s.mesh); sparks.length=0;
|
||||
brain.root.set(0,0,0); brain.comPrev.set(0,1,0);
|
||||
brain.step=null; setState('balance'); brain.strength=1;
|
||||
orbit.target.set(0,1,0); orbit.dist=4.6; updateCamera();
|
||||
};
|
||||
document.getElementById('bDetonate').onclick=detonateC4;
|
||||
document.getElementById('bClearC4').onclick=()=>{
|
||||
for(const c of c4List) scene.remove(c.mesh); c4List.length=0;
|
||||
};
|
||||
|
||||
/* ===================== MAIN LOOP ===================== */
|
||||
const clock=new THREE.Clock();
|
||||
let acc=0;
|
||||
function animate(){
|
||||
requestAnimationFrame(animate);
|
||||
let dt=Math.min(clock.getDelta(),.05);
|
||||
if(slow) dt*=.28;
|
||||
|
||||
// held-fire
|
||||
if(isFiring&&(currentWeapon==='gun'||currentWeapon==='cannon')){
|
||||
gunFireCooldown-=dt;
|
||||
if(gunFireCooldown<=0){
|
||||
gunFireCooldown=currentWeapon==='gun'?.08:.4;
|
||||
fireWeapon(_liveNDC.x,_liveNDC.y);
|
||||
}
|
||||
}
|
||||
|
||||
acc+=dt; let n=0;
|
||||
while(acc>=PHYS_DT&&n<4){
|
||||
brainUpdate(PHYS_DT); stepPhysics(); acc-=PHYS_DT; n++;
|
||||
}
|
||||
stepSparks(dt); stepC4(dt); stepFlash(dt);
|
||||
|
||||
// Camera update
|
||||
if(camMode==='follow'){
|
||||
// hard-follow: target snaps directly to COM every frame, no lerp
|
||||
orbit.target.copy(brain.com);
|
||||
updateCamera();
|
||||
} else if(camMode==='fp'){
|
||||
updateCamera(); // fp reads head pos directly
|
||||
} else {
|
||||
// orbit: smooth follow
|
||||
_t.copy(brain.com); _t.y=Math.max(.4,Math.min(1.6,brain.com.y));
|
||||
orbit.target.lerp(_t,1-Math.exp(-dt*4));
|
||||
updateCamera();
|
||||
}
|
||||
|
||||
syncVisuals(); syncBadge();
|
||||
renderer.render(scene,camera);
|
||||
}
|
||||
animate();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user