Files
Active-Ragdoll/active_ragdoll-v5-67-stickyc4-artillery.html
2026-06-10 18:00:00 -04:00

509 lines
48 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Active Ragdoll — 67 Pose & Flat Sticky C4</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,.88);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,.5);max-width:340px;user-select:none}
#ui h1{font-size:15px;margin:0 0 2px;font-weight:650}
#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.mol{border-color:rgba(251,191,36,.4);color:#fde68a}button.mol:hover{background:#2d1f06}
button.sel{outline:2px solid #60a5fa;outline-offset:1px; background:#2e4a7a}
#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 8px;border-radius:99px;margin-left:5px;
background:#1a1f2b;border:1px solid rgba(255,255,255,.15);color:#94a3b8;vertical-align:middle}
.slider-row{display:flex;align-items:center;gap:8px;margin-top:5px}
.slider-row label{font-size:11px;color:#9aa6b5;white-space:nowrap}
.slider-row input[type=range]{flex:1;accent-color:#ef4444;height:4px}
.slider-row span{font-size:11px;color:#fca5a5;width:36px;text-align:right}
#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">INDEPENDENT</span></h1>
<div class="sub">67 Pose · Flat Sticky C4 · Camera-Relative WASD</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>
<button id="b67" style="background:#4a2a1a;border-color:#f59e0b;">6⃣7⃣ Pose</button>
</div>
<div class="lbl">Camera <span style="color:#6b7a8d;font-weight:400">(V to cycle)</span></div>
<div class="row">
<button id="camOrbit" onclick="setCamMode('orbit')">🌐 Orbit</button>
<button id="camFollow" onclick="setCamMode('follow')">🎯 Follow</button>
<button id="camFP" onclick="setCamMode('fp')">👁 First-Person</button>
<button id="camIndie" class="sel" onclick="setCamMode('independent')">🎥 Independent</button>
</div>
<div class="lbl">Weapon</div>
<div class="row" id="weapon-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>
<button id="wMol" class="mol" onclick="selectWeapon('molotov')">🍾 Molotov</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 id="c4-options" style="display:none"><div class="slider-row"><label>C4 Power</label><input type="range" id="c4Power" min="5" max="100" value="45" step="1"><span id="c4PowerVal">45</span></div></div>
<div id="he-options" style="display:none"><div class="slider-row"><label>HE Power</label><input type="range" id="hePower" min="15" max="100" value="48" step="1"><span id="hePowerVal">48</span></div></div>
<div class="lbl" style="margin-top:8px">C4 / Sticky C4</div>
<div class="row">
<button class="danger" id="bDetonate">💥 Detonate C4</button>
<button class="danger" id="bClearC4">Clear C4</button>
<button id="bAddSticky" style="background:#2a3a5a;border-color:#60a5fa;">📍 Add Sticky C4</button>
<button class="danger" id="bDetonateSticky">💣 Detonate Sticky</button>
</div>
</div>
<div id="hint"><b>LMB</b> fire/place &nbsp;·&nbsp; <b>RMB drag</b> grab &nbsp;·&nbsp; <b>WASD</b> move relative to view &nbsp;·&nbsp; <b>V</b> cam</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'),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); scene.background=new THREE.CanvasTexture(c); }
scene.add(new THREE.HemisphereLight(0xbdd3ea,0x4d463b,.9));
const sun=new THREE.DirectionalLight(0xfff0d8,1.5); sun.position.set(6,9,4); sun.castShadow=true; sun.shadow.mapSize.set(1024,1024);
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,.28); fill.position.set(-5,4,-6); scene.add(fill);
const flashPt=new THREE.PointLight(0xff8822,0,20,2); scene.add(flashPt);
function makeGroundTex(){ const c=document.createElement('canvas'); c.width=c.height=512; const g=c.getContext('2d');
g.fillStyle='#7a7e83'; g.fillRect(0,0,512,512);
for(let i=0;i<5000;i++){ const v=18+Math.random()*40|0; g.fillStyle=`rgba(${v},${v},${v+3},${.04+Math.random()*.10})`; g.fillRect(Math.random()*512,Math.random()*512,1.8,1.8); }
g.strokeStyle='rgba(255,255,255,0.18)'; g.lineWidth=1.5; const cell=64;
for(let i=0;i<=512;i+=cell){ g.beginPath(); g.moveTo(i,0); g.lineTo(i,512); g.stroke(); g.beginPath(); g.moveTo(0,i); g.lineTo(512,i); g.stroke(); }
g.strokeStyle='rgba(0,0,0,0.07)'; g.lineWidth=.8;
for(let row=0;row<512/cell;row++) for(let col=0;col<512/cell;col++){ const x=col*cell,y=row*cell; g.beginPath(); g.moveTo(x,y); g.lineTo(x+cell,y+cell); g.stroke(); }
const t=new THREE.CanvasTexture(c); t.wrapS=t.wrapT=THREE.RepeatWrapping; t.repeat.set(18,18); t.anisotropy=8; return t;
}
const ground=new THREE.Mesh(new THREE.PlaneGeometry(4000,4000),new THREE.MeshStandardMaterial({map:makeGroundTex(),roughness:.95,metalness:0}));
ground.rotation.x=-Math.PI/2; ground.receiveShadow=true; scene.add(ground);
/* ========== CAMERA with RELATIVE MOVEMENT ========== */
const camera=new THREE.PerspectiveCamera(60,innerWidth/innerHeight,.05,200);
let camMode='independent';
const orbit={yaw:.65,pitch:.18,dist:4.6,target:new THREE.Vector3(0,1,0)};
const fp={yaw:.65,pitch:0};
const indie={pos:new THREE.Vector3(5,3,8), yaw:-0.8, pitch:-0.2};
const keyState={w:false,s:false,a:false,d:false,space:false,shift:false};
function getCameraDirectionVectors(){
const forward=new THREE.Vector3(0,0,-1).applyQuaternion(camera.quaternion);
forward.y=0; forward.normalize();
const right=new THREE.Vector3(1,0,0).applyQuaternion(camera.quaternion);
right.y=0; right.normalize();
return {forward,right};
}
function updateCamera(){
orbit.pitch=Math.max(-1.48,Math.min(1.48,orbit.pitch)); orbit.dist=Math.max(1,Math.min(20,orbit.dist));
if(camMode==='fp'){
const head=parts[P.head]; fp.pitch=Math.max(-1.4,Math.min(1.4,fp.pitch));
camera.position.copy(head.pos).setY(head.pos.y+.06);
camera.lookAt(head.pos.x+Math.sin(fp.yaw)*Math.cos(fp.pitch)*10, head.pos.y+.06+Math.sin(fp.pitch)*10, head.pos.z+Math.cos(fp.yaw)*Math.cos(fp.pitch)*10);
} else if(camMode==='orbit'){
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);
} else if(camMode==='follow'){
const cp=Math.cos(orbit.pitch);
const followTarget=brain.com;
camera.position.set(followTarget.x+Math.sin(orbit.yaw)*cp*orbit.dist, followTarget.y+Math.sin(orbit.pitch)*orbit.dist, followTarget.z+Math.cos(orbit.yaw)*cp*orbit.dist);
camera.lookAt(followTarget);
} else {
const {forward,right}=getCameraDirectionVectors();
let move=new THREE.Vector3(0,0,0);
if(keyState.w) move.z-=1; if(keyState.s) move.z+=1;
if(keyState.a) move.x-=1; if(keyState.d) move.x+=1;
if(move.length()>0)move.normalize();
const speed=12;
indie.pos.addScaledVector(forward, move.z*speed*0.016);
indie.pos.addScaledVector(right, move.x*speed*0.016);
if(keyState.space) indie.pos.y+=5*0.016;
if(keyState.shift) indie.pos.y-=5*0.016;
indie.pos.y=Math.max(0.5,Math.min(20,indie.pos.y));
const lookDir=new THREE.Vector3(Math.sin(indie.yaw), Math.sin(indie.pitch), Math.cos(indie.yaw)).normalize();
camera.position.copy(indie.pos);
camera.lookAt(indie.pos.clone().add(lookDir));
}
}
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',independent:'INDEPENDENT'};
function setCamMode(m){ camMode=m; camBadge.textContent=CAM_LABELS[m];
['Orbit','Follow','FP','Indie'].forEach(id=>document.getElementById('cam'+id).classList.toggle('sel',id.toLowerCase()===m||(id==='Indie'&&m==='independent')));
if(m==='follow'||m==='fp') orbit.target.copy(brain.com);
}
addEventListener('keydown',e=>{ const k=e.key.toLowerCase(); if(k==='v'){ const m=['orbit','follow','fp','independent']; setCamMode(m[(m.indexOf(camMode)+1)%4]); }
if(k==='w')keyState.w=true; if(k==='s')keyState.s=true; if(k==='a')keyState.a=true; if(k==='d')keyState.d=true;
if(k===' ') { keyState.space=true; e.preventDefault(); }
if(k==='shift') { keyState.shift=true; e.preventDefault(); }
});
addEventListener('keyup',e=>{ const k=e.key.toLowerCase(); if(k==='w')keyState.w=false; if(k==='s')keyState.s=false; if(k==='a')keyState.a=false; if(k==='d')keyState.d=false;
if(k===' ') keyState.space=false; if(k==='shift') keyState.shift=false;
});
/* ========== PHYSICS ========== */
const GRAVITY=-9.81,AIR=.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 link(a,b,stiff=1,type='eq',rest=null){ const i=P[a],j=P[b],d=rest!==null?rest:parts[i].pos.distanceTo(parts[j].pos);
cons.push({i,j,rest:d,stiff,type}); if(type==='eq')bonded.add(i<j?i+'_'+j:j+'_'+i); }
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;
a.pos.addScaledVector(_d,diff*c.stiff*a.w/ws); b.pos.addScaledVector(_d,-diff*c.stiff*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*.25;
p.prev.x+=(p.pos.x-p.prev.x)*.6; p.prev.z+=(p.pos.z-p.prev.z)*.6; } } }
function collideSelf(){ for(let i=0;i<parts.length;i++) for(let j=i+1;j<parts.length;j++){ const key=i+'_'+j; if(bonded.has(key))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*.7*a.w/w); b.pos.addScaledVector(_d,-diff*.7*b.w/w); } }
function addImpulse(p,dv){p.prev.addScaledVector(dv,-PHYS_DT/SUBSTEPS);}
/* ========== PARTICLES & FIRE ========== */
const sparks=[]; const _sparkGeo=new THREE.SphereGeometry(.024,4,3); const _smokeGeo=new THREE.SphereGeometry(.1,5,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,new THREE.MeshBasicMaterial({color:0x555566,transparent:true,opacity:.4})); 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.1+Math.random(),maxLife:1.5,grav:false,smoke:true}); } }
function spawnFire(pos,count,life){ for(let i=0;i<count;i++){ const dir=new THREE.Vector3(Math.random()-.5,Math.random()*.4+.6,Math.random()-.5).normalize();
const m=new THREE.Mesh(_sparkGeo,new THREE.MeshBasicMaterial({color:0xff6600})); m.position.copy(pos); scene.add(m);
sparks.push({mesh:m,vel:dir.multiplyScalar(.8+Math.random()*1.2),life,maxLife:life,grav:false,fire: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 if(s.fire){ const hue=.07*f*f; s.mesh.material.color.setHSL(hue,.95,.5+.15*f); s.mesh.scale.setScalar(f*.8+.2);}
else { s.mesh.material.color.setHSL(.07*f,1,.5); } } }
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;}
const fireZones=[];
function addFireZone(pos, radius, duration){ fireZones.push({ pos:pos.clone(), radius, duration, life:0 }); }
function stepFireZones(dt){
for(let i=0;i<fireZones.length;i++){ const z=fireZones[i]; z.life+=dt;
for(let p=0;p<4+Math.random()*5;p++) spawnFire(z.pos.clone().addScaledVector(new THREE.Vector3(Math.random()-.5,0,Math.random()-.5),z.radius*.9), 1, 0.4);
if(!fireState.active && brain.com.distanceTo(z.pos)<z.radius){ startFire(); applyKO(5.0); }
if(z.life>=z.duration){ fireZones.splice(i,1); i--; } }
}
/* ========== UNIFIED KNOCKOUT & FIRE STATE ========== */
let forcedGetupTimer=0;
const fireState={active:false, danceTimer:0, duration:5};
function startFire(){ if(fireState.active)return; fireState.active=true; fireState.danceTimer=0; setState('fire'); brain.strength=1.4; }
function applyKO(duration){ if(fireState.active) fireState.active=false; setState('ko'); brain.koT=duration; forcedGetupTimer=duration; brain.strength=0.02; }
function updateKOGetup(dt){ if(brain.state==='ko'){ forcedGetupTimer-=dt; if(forcedGetupTimer<=0 && !isAirborne()){ setState('getup'); brain.strength=1.2; forcedGetupTimer=0; } } }
function isAirborne(){ return (parts[P.footL].pos.y>0.12 || parts[P.footR].pos.y>0.12) && brain.comVel.y>0.5; }
function applyFireDance(dt){ if(!fireState.active)return;
fireState.danceTimer+=dt;
const t=fireState.danceTimer;
const intensity=Math.min(1.6, t*0.6);
const T=brain.targets;
T.handL.addScaledVector(_right, -(Math.sin(t*12)*0.9+0.3)*intensity).y+=Math.abs(Math.sin(t*11))*1.2*intensity;
T.handR.addScaledVector(_right, (Math.sin(t*11+1.2)*0.9+0.3)*intensity).y+=Math.abs(Math.sin(t*10.5))*1.2*intensity;
T.elbowL.addScaledVector(_right, -Math.sin(t*14)*0.65*intensity).y+=Math.abs(Math.sin(t*8.5))*0.8*intensity;
T.elbowR.addScaledVector(_right, Math.sin(t*13+1.5)*0.65*intensity).y+=Math.abs(Math.sin(t*9))*0.8*intensity;
T.footL.y+=Math.abs(Math.sin(t*10))*0.8*intensity; T.footR.y+=Math.abs(Math.sin(t*11.5))*0.8*intensity;
T.kneeL.y+=Math.abs(Math.sin(t*8))*0.6*intensity; T.kneeR.y+=Math.abs(Math.sin(t*9.2))*0.6*intensity;
T.chest.addScaledVector(_right, Math.sin(t*10)*0.45*intensity).z+=Math.sin(t*7)*0.35*intensity;
T.head.addScaledVector(_right, Math.sin(t*14)*0.55*intensity).y+=Math.sin(t*10)*0.45*intensity;
T.pelvis.y+=Math.abs(Math.sin(t*18))*0.28*intensity;
if(Math.random()<dt*0.7){ const runDir=new THREE.Vector3(Math.sin(t*5),0,Math.cos(t*4)).normalize(); addImpulse(parts[P.chest], runDir.multiplyScalar(6.5)); addImpulse(parts[P.pelvis], runDir.multiplyScalar(4)); }
if(fireState.danceTimer>=fireState.duration){ fireState.active=false; applyKO(0); setState('ko'); brain.koT=3; forcedGetupTimer=3; }
}
/* ========== C4 & FLAT STICKY C4 ========== */
const c4List=[]; const stickyC4List=[];
const c4Geo=new THREE.BoxGeometry(.22,.08,.14); const c4Mat=new THREE.MeshStandardMaterial({color:0xd4c98a,roughness:.85});
const miniC4Geo=new THREE.BoxGeometry(.14,.04,.1); const miniMat=new THREE.MeshStandardMaterial({color:0xccaa66,roughness:.7,emissive:0x331100});
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 addStickyC4(){
const bodyParts=['chest','pelvis','shoulderL','shoulderR','hipL','hipR'];
const randomPart=bodyParts[Math.floor(Math.random()*bodyParts.length)];
const partIdx=P[randomPart];
const part=parts[partIdx];
// local offset - flat against body
const localOffset=new THREE.Vector3((Math.random()-.5)*0.18, (Math.random()-.5)*0.12+0.08, (Math.random()-.5)*0.12+0.05);
const mesh=new THREE.Mesh(miniC4Geo,miniMat); mesh.castShadow=true;
mesh.position.copy(part.pos.clone().add(localOffset));
// Make it lay flat (no rotation)
mesh.rotation.set(0,0,0);
scene.add(mesh);
const led=new THREE.Mesh(new THREE.SphereGeometry(.01,4,4),new THREE.MeshBasicMaterial({color:0xff3333})); led.position.set(0.06,0.01,0.045); mesh.add(led);
stickyC4List.push({mesh, partIdx, localOffset, led, blink:0, armed:true});
}
function detonateStickyC4(){
const power=28; const radius=2.4;
for(let i=0;i<stickyC4List.length;i++){ const s=stickyC4List[i];
if(!s.armed)continue;
const part=parts[s.partIdx];
const worldPos=part.pos.clone().add(s.localOffset);
flashPt.position.copy(worldPos); triggerFlash(1.2);
explodeAt(worldPos, radius, power);
spawnSparks(worldPos.clone(),20,0xff8844,3.8,0.8);
scene.remove(s.mesh);
stickyC4List.splice(i,1); i--;
}
}
function detonateC4(){
const powerVal=parseFloat(document.getElementById('c4Power').value); const t=powerVal/100; const force=8+t*72; const radius=1.2+t*3.2;
for(const c of c4List){ if(!c.armed)continue; flashPt.position.copy(c.pos).setY(.5); triggerFlash(1+t*1.8); explodeAt(c.pos,radius,force); spawnSparks(c.pos.clone().setY(.1),20+Math.round(t*28),0xff6600,4.5+t*2.5,1); spawnSmoke(c.pos.clone().setY(.1),8+Math.round(t*12)); scene.remove(c.mesh); }
c4List.length=0; snapCameraToBody();
}
function stepStickyC4(dt){ for(const s of stickyC4List){
s.blink+=dt; s.led.material.color.setHex(s.blink%0.5<0.1?0xff3333:0x660000);
const part=parts[s.partIdx]; s.mesh.position.copy(part.pos.clone().add(s.localOffset));
// Keep flat: no rotation
} }
function stepC4(dt){ for(const c of c4List){c.blink+=dt;c.ledMesh.material.color.setHex(c.blink%0.6<.1?0xff0000:0x440000);} }
function snapCameraToBody(){ if(camMode!=='independent') orbit.target.copy(brain.com); }
function explodeAt(pos, radius, force){ let hit=false;
for(const p of parts){ _tmp.subVectors(p.pos,pos); const dist=_tmp.length(); if(dist<radius){
hit=true; const fall=1-dist/radius; _tmp.normalize().multiplyScalar(force*fall*fall); _tmp.y+=force*fall*.5; addImpulse(p,_tmp); } }
if(hit) applyKO(3.0); }
/* ========== PROJECTILES (simplified) ========== */
const balls=[]; const originalBallMat=new THREE.MeshStandardMaterial({color:0xd04020,roughness:.55,metalness:.1});
const heShellGroup=()=>{ const g=new THREE.Group(); const body=new THREE.Mesh(new THREE.CylinderGeometry(0.09,0.07,0.48,12),new THREE.MeshStandardMaterial({color:0xcc8844,metalness:.7})); body.castShadow=true; g.add(body); const nose=new THREE.Mesh(new THREE.ConeGeometry(0.07,0.16,8),new THREE.MeshStandardMaterial({color:0xeeaa66})); nose.position.y=0.27; g.add(nose); return g; };
const apShellGroup=()=>{ const g=new THREE.Group(); const body=new THREE.Mesh(new THREE.CylinderGeometry(0.085,0.075,0.52,16),new THREE.MeshStandardMaterial({color:0x99bbdd,metalness:.9})); body.castShadow=true; g.add(body); const tip=new THREE.Mesh(new THREE.ConeGeometry(0.055,0.22,12),new THREE.MeshStandardMaterial({color:0xcceeff,metalness:.95})); tip.position.y=0.3; g.add(tip); return g; };
const cannonGroup=()=>{ const g=new THREE.Group(); const body=new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.09,0.5,10),new THREE.MeshStandardMaterial({color:0xaaaaaa,metalness:.85,roughness:.3})); g.add(body); return g; };
const bottleGroup=()=>{ const g=new THREE.Group(); const body=new THREE.Mesh(new THREE.CylinderGeometry(0.065,0.048,0.21,8),new THREE.MeshStandardMaterial({color:0xaa7755,roughness:.3})); g.add(body); const cap=new THREE.Mesh(new THREE.CylinderGeometry(0.048,0.052,0.04,6),new THREE.MeshStandardMaterial({color:0xcc8844})); cap.position.y=0.12; g.add(cap); return g; };
function spawnBall(pos,vel,r,m,type='normal'){ let mesh; const scale= (type==='cannon'||type==='HE'||type==='AP')?0.7:1;
if(type==='HE') mesh=heShellGroup();
else if(type==='AP') mesh=apShellGroup();
else if(type==='cannon') mesh=cannonGroup();
else if(type==='molotov') mesh=bottleGroup();
else if(type==='normal'){ mesh=new THREE.Mesh(new THREE.SphereGeometry(r,24,24),originalBallMat); mesh.castShadow=true; }
else { mesh=new THREE.Mesh(new THREE.SphereGeometry(r,16,16),new THREE.MeshStandardMaterial({color:0xd4aa44})); mesh.castShadow=true; }
if(mesh){ mesh.scale.setScalar(mesh.scale.x*scale); mesh.position.copy(pos); scene.add(mesh); }
balls.push({pos:pos.clone(),vel:vel.clone(),r:r*(type==='molotov'?0.12:r),m,life:12,mesh,type,hitParts:new Set(), customMesh:!!mesh}); }
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*0.4)); ball.vel.multiplyScalar(.55); if(vn>4) applyKO(3.0); return false; }
else if(ball.type==='AP'){ if(!ball.hitParts.has(part)){ ball.hitParts.add(part); const shotDir=ball.vel.clone().normalize(); addImpulse(part, shotDir.clone().multiplyScalar(vn*ball.m*0.9)); addImpulse(part, new THREE.Vector3(0, -vn*ball.m*0.2, 0)); spawnSparks(ball.pos.clone(),5,0x88ffaa,2,.3); } ball.vel.multiplyScalar(.98); if(ball.hitParts.size>4) applyKO(3.0); return false; }
else if(ball.type==='HE'){ return true; }
else if(ball.type==='cannon'){ const share=2.2*ball.m/(ball.m+part.m); addImpulse(part,_tmp.copy(_d).multiplyScalar(vn*share*0.9)); ball.vel.addScaledVector(_d,-vn*1.2); ball.pos.copy(part.pos).addScaledVector(_d,-(part.r+ball.r)); if(vn>7) applyKO(3.0); return false; }
else if(ball.type==='normal'){ addImpulse(part,_tmp.copy(_d).multiplyScalar(vn*0.35*ball.m)); ball.vel.multiplyScalar(0.82); return false; }
else if(ball.type==='molotov'){ return true; } return false; }
function doExplodeBall(k){ const b=balls[k];
if(b.type==='HE'){ flashPt.position.copy(b.pos); const hePower=parseFloat(document.getElementById('hePower').value); const t=hePower/100; const force=12+t*58; const radius=1.6+t*3; triggerFlash(1.2+t*1.4); explodeAt(b.pos,radius,force); spawnSparks(b.pos.clone(),26+Math.round(t*18),0xff5500,5.5,.85); spawnSmoke(b.pos.clone(),9+Math.round(t*8)); snapCameraToBody(); }
else if(b.type==='molotov'){ spawnSparks(b.pos.clone(),28,0xff8800,4.5,1.0); for(let i=0;i<55;i++) spawnFire(b.pos.clone(),1,0.9); spawnSmoke(b.pos.clone(),12); flashPt.position.copy(b.pos); flashPt.intensity=6; setTimeout(()=>flashPt.intensity=0,150); addFireZone(b.pos.clone(), 2.6, 10.0); if(b.pos.distanceTo(brain.com)<3.5) startFire(); }
scene.remove(b.mesh); balls.splice(k,1); }
function sweptSphere(px,py,pz, cx,cy,cz, dx,dy,dz, r){ const fx=px-cx, fy=py-cy, fz=pz-cz; const a=dx*dx+dy*dy+dz*dz; if(a<1e-14)return -1; const b=2*(fx*dx+fy*dy+fz*dz); const c=fx*fx+fy*fy+fz*fz-r*r; const disc=b*b-4*a*c; if(disc<0)return -1; const t=(-b-Math.sqrt(disc))/(2*a); return (t>=0&&t<=1)?t:-1; }
function stepBalls(h){ for(let k=balls.length-1;k>=0;k--){ const b=balls[k]; b.life-=h; b.vel.y+=GRAVITY*h; const px=b.pos.x, py=b.pos.y, pz=b.pos.z; b.pos.x+=b.vel.x*h; b.pos.y+=b.vel.y*h; b.pos.z+=b.vel.z*h; const dx=b.pos.x-px, dy=b.pos.y-py, dz=b.pos.z-pz;
if(b.pos.y<b.r){ b.pos.y=b.r; if(b.type==='HE'||b.type==='molotov'){doExplodeBall(k);continue;} if(b.type==='AP'){b.vel.y*=-.1;b.vel.x*=.7;b.vel.z*=.7;} else{b.vel.y*=-.4;b.vel.x*=.85;b.vel.z*=.85;} }
let bestT=2, bestPart=null; for(const p of parts){ const r=p.r+b.r; let t=sweptSphere(px,py,pz, p.pos.x,p.pos.y,p.pos.z, dx,dy,dz, r); if(t<0){ const ox=b.pos.x-p.pos.x, oy=b.pos.y-p.pos.y, oz=b.pos.z-p.pos.z; if(ox*ox+oy*oy+oz*oz<r*r) t=0; } if(t>=0&&t<bestT){bestT=t;bestPart=p;} }
let explode=false; if(bestPart){ if(bestT>0){b.pos.x=px+dx*bestT; b.pos.y=py+dy*bestT; b.pos.z=pz+dz*bestT;} explode=onBallHitPart(b,bestPart); }
if(explode){doExplodeBall(k);continue;}
if(b.customMesh && b.mesh){ b.mesh.position.copy(b.pos); if(b.type==='AP'||b.type==='HE'||b.type==='cannon'){ const direction=b.vel.clone().normalize(); if(direction.length()>0.01){ const quat=new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0),direction); b.mesh.quaternion.copy(quat); } } else if(b.type==='molotov') b.mesh.rotation.z+=0.08; }
else if(b.mesh) b.mesh.position.copy(b.pos);
if(b.life<=0){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);
const matSuit=new THREE.MeshStandardMaterial({color:0x4b5563,roughness:.74}); const matSuit2=new THREE.MeshStandardMaterial({color:0x39414d,roughness:.8});
const matSkin=new THREE.MeshStandardMaterial({color:0xc99c7d,roughness:.55}); const matShoe=new THREE.MeshStandardMaterial({color:0x22262d});
const rig=new THREE.Group(); scene.add(rig); const limbs=[],pickMeshes=[],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.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.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,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 with 67 Pose (horizontal forward then alternate 45° up/down) ========== */
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]$/,'');}
let sixtySevenActive=false, sixtySevenTimer=0;
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 applySixtySeven(dt){ if(!sixtySevenActive)return;
sixtySevenTimer+=dt;
const T=brain.targets;
const shoulderLPos=parts[P.shoulderL].pos.clone();
const shoulderRPos=parts[P.shoulderR].pos.clone();
const forwardDir=_fwd.clone().normalize();
const upDir=new THREE.Vector3(0,1,0);
// Base: both hands straight forward horizontally
const baseOffset=forwardDir.clone().multiplyScalar(0.38);
T.handL.copy(shoulderLPos).add(baseOffset);
T.handR.copy(shoulderRPos).add(baseOffset);
T.elbowL.copy(shoulderLPos).add(baseOffset.clone().multiplyScalar(0.5));
T.elbowR.copy(shoulderRPos).add(baseOffset.clone().multiplyScalar(0.5));
// Alternating: every 0.7 seconds, swap which arm goes up 45° and which goes down 45°
const phase=Math.floor(sixtySevenTimer/0.7)%2;
const upVec=upDir.clone().multiplyScalar(0.32);
const downVec=upDir.clone().multiplyScalar(-0.32);
if(phase===0){ // right arm up, left arm down
T.handR.add(upVec); T.elbowR.add(upVec.clone().multiplyScalar(0.6));
T.handL.add(downVec); T.elbowL.add(downVec.clone().multiplyScalar(0.6));
} else { // left arm up, right arm down
T.handL.add(upVec); T.elbowL.add(upVec.clone().multiplyScalar(0.6));
T.handR.add(downVec); T.elbowR.add(downVec.clone().multiplyScalar(0.6));
}
}
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&&!isAirborne())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&&!isAirborne())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&&!isAirborne())setState('down'); }
else if(B.state==='fire'){ B.strength=1.4; }
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(),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; }
if(fireState.active) applyFireDance(dt);
if(sixtySevenActive && (B.state==='balance'||B.state==='fire')) applySixtySeven(dt);
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 UI ========== */
let currentWeapon='ball',currentAmmo='normal',isFiring=false,gunFireCooldown=0,ballFiredThisClick=false;
function selectWeapon(w){ currentWeapon=w; ['Ball','Gun','Cannon','C4','Mol'].forEach(id=>{ const btn=document.getElementById('w'+id); if(btn)btn.classList.toggle('sel',id.toLowerCase()===w||(id==='Mol'&&w==='molotov')); });
document.getElementById('ammo-row').style.display=(w==='cannon')?'flex':'none'; document.getElementById('c4-options').style.display=(w==='c4')?'block':'none'; document.getElementById('he-options').style.display=(w==='cannon' && currentAmmo==='HE')?'block':'none'; }
function selectAmmo(a){ currentAmmo=a; ['Normal','HE','AP'].forEach(id=>document.getElementById('am'+id).classList.toggle('active-ammo',id.toLowerCase()===a)); document.getElementById('he-options').style.display=(currentWeapon==='cannon' && a==='HE')?'block':'none'; }
document.getElementById('c4Power').addEventListener('input',function(){ document.getElementById('c4PowerVal').textContent=this.value; });
document.getElementById('hePower').addEventListener('input',function(){ document.getElementById('hePowerVal').textContent=this.value; });
const _fireRay=new THREE.Raycaster(),_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(70),.035,0.7,'gun'); flashPt.position.copy(origin); flashPt.intensity=7; setTimeout(()=>flashPt.intensity=0,35); spawnSparks(origin.clone(),4,0xffdd66,3.5,.2); }
else if(currentWeapon==='cannon'){ let speed,radius,mass,type; if(currentAmmo==='HE'){ speed=28;radius=.17;mass=11;type='HE';} else if(currentAmmo==='AP'){ speed=40;radius=.11;mass=7;type='AP';} else{ speed=24;radius=.14;mass=9;type='cannon';} const spawnPt=_fireRay.ray.origin.clone().addScaledVector(dir,.85); spawnBall(spawnPt,dir.multiplyScalar(speed),radius,mass,type); triggerFlash(1); spawnSparks(spawnPt.clone(),8,0xffaa22,2.8,.25); }
else if(currentWeapon==='c4'){ const dy=_fireRay.ray.direction.y; if(dy<-0.01){ const t2=_fireRay.ray.origin.y/(-dy); if(t2>0) placeC4(_fireRay.ray.origin.clone().addScaledVector(_fireRay.ray.direction,t2)); } }
else if(currentWeapon==='molotov'){ const throwVel=dir.clone().multiplyScalar(10.5); throwVel.y+=5.2; spawnBall(origin,throwVel,.12,2.2,'molotov'); } }
/* ========== INPUT ========== */
const grab={active:false,pi:0,dist:0,target:new THREE.Vector3()}; const _grabRay=new THREE.Raycaster(),_grabNDC=new THREE.Vector2();
let dragMode=null,lastX=0,lastY=0,movedPx=0; 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){ _grabNDC.set(_liveNDC.x,_liveNDC.y); _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){ dragMode='mayfire';
if(camMode==='independent'){ indieMouseDrag=true; lastMouseX=e.clientX; lastMouseY=e.clientY; dragMode='orbit'; }
if(currentWeapon==='ball' && !ballFiredThisClick){ ballFiredThisClick=true; fireWeapon(_liveNDC.x,_liveNDC.y); }
else if(currentWeapon==='gun'){ fireWeapon(_liveNDC.x,_liveNDC.y); isFiring=false; }
else if(currentWeapon==='cannon'){ isFiring=true; gunFireCooldown=0; fireWeapon(_liveNDC.x,_liveNDC.y); }
else if(currentWeapon!=='ball' && currentWeapon!=='gun'){ 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)) && !grab.active){ dragMode='orbit'; isFiring=false;
if(camMode==='fp'){fp.yaw-=dx*.004;fp.pitch+=dy*.004;}
else if(camMode==='independent'){ indie.yaw-=dx*.008; indie.pitch=Math.max(-1.2,Math.min(1.2,indie.pitch+dy*.008)); }
else{ orbit.yaw-=dx*.005; orbit.pitch+=dy*.005; }
updateCamera();
} else if(dragMode==='grab'){ _grabNDC.set(_liveNDC.x,_liveNDC.y); _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){ if(currentWeapon!=='ball' && currentWeapon!=='gun' && currentWeapon!=='cannon') fireWeapon(_liveNDC.x,_liveNDC.y); } grab.active=false; dragMode=null; isFiring=false; ballFiredThisClick=false; });
renderer.domElement.addEventListener('wheel',e=>{ e.preventDefault(); if(camMode==='independent'){ const zoomDir=new THREE.Vector3(0,0,e.deltaY>0?0.6:-0.6).applyQuaternion(camera.quaternion); indie.pos.add(zoomDir); } else orbit.dist*=(e.deltaY>0?1.1:.9); updateCamera();},{passive:false}); addEventListener('contextmenu',e=>e.preventDefault());
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'],fire:['🔥 ON FIRE 🔥','#3b1500','#fb923c'] };
function syncBadge(){ const s=STATE_STYLE[brain.state]||STATE_STYLE.down; 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)*7.5,2.2,Math.sin(a)*7.5); addImpulse(parts[P.chest],_t); addImpulse(parts[P.pelvis],_t2.copy(_t).multiplyScalar(.7)); };
document.getElementById('bKO').onclick=()=>applyKO(3.0);
let slow=false; document.getElementById('bSlow').onclick=function(){slow=!slow;this.classList.toggle('on',slow);};
document.getElementById('b67').onclick=function(){ sixtySevenActive=!sixtySevenActive; sixtySevenTimer=0; this.classList.toggle('sel',sixtySevenActive); };
document.getElementById('bAddSticky').onclick=()=>addStickyC4();
document.getElementById('bDetonateSticky').onclick=()=>detonateStickyC4();
function doReset(){ 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 stickyC4List)scene.remove(s.mesh); stickyC4List.length=0; for(const s of sparks)scene.remove(s.mesh); sparks.length=0; fireZones.length=0; fireState.active=false; fireState.danceTimer=0; sixtySevenActive=false; document.getElementById('b67').classList.remove('sel'); brain.root.set(0,0,0); brain.comPrev.set(0,1,0); brain.step=null; setState('balance'); brain.strength=1; forcedGetupTimer=0; orbit.target.set(0,1,0); updateCamera(); }
document.getElementById('bReset').onclick=doReset;
document.getElementById('bDetonate').onclick=detonateC4;
document.getElementById('bClearC4').onclick=()=>{for(const c of c4List)scene.remove(c.mesh);c4List.length=0;};
const clock=new THREE.Clock(); let acc=0;
function animate(){ requestAnimationFrame(animate); let dt=Math.min(clock.getDelta(),.05); if(slow)dt*=.28;
if(isFiring && currentWeapon==='cannon'){ gunFireCooldown-=dt; if(gunFireCooldown<=0){ gunFireCooldown=0.5; 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); stepStickyC4(dt); stepFlash(dt); stepFireZones(dt); updateKOGetup(dt);
if(camMode==='follow'){ const cp=Math.cos(orbit.pitch); const ft=brain.com; camera.position.set(ft.x+Math.sin(orbit.yaw)*cp*orbit.dist, ft.y+Math.sin(orbit.pitch)*orbit.dist, ft.z+Math.cos(orbit.yaw)*cp*orbit.dist); camera.lookAt(ft); }
else if(camMode==='orbit'){ const cp=Math.cos(orbit.pitch); orbit.target.lerp(brain.com,0.08); 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); }
else updateCamera();
syncVisuals(); syncBadge(); renderer.render(scene,camera); }
animate();
</script>
</body>
</html>