Files
Active-Ragdoll/test_vers/deepseek_html_20260610_1f471a.html
2026-06-10 17:56:29 -04:00

456 lines
45 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 & Fire Dance</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:320px;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 · Fire Dance 5s</div>
<div><span id="state">BALANCING</span></div>
<div class="lbl">Ragdoll</div>
<div class="row">
<button id="bShove">Shove (strong)</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</div>
<div class="row"><button class="danger" id="bDetonate">💥 Detonate</button><button class="danger" id="bClearC4">Clear</button></div>
</div>
<div id="hint"><b>LMB</b> fire/place (ball single shot) &nbsp;·&nbsp; <b>RMB drag</b> grab &nbsp;·&nbsp; <b>LMB drag</b> rotate &nbsp;·&nbsp; <b>WASD</b> move &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 ========== */
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 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 camDir=new THREE.Vector3(0,0,-1).applyQuaternion(camera.quaternion);
const camRight=new THREE.Vector3(1,0,0).applyQuaternion(camera.quaternion);
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(camDir, move.z*speed*0.016);
indie.pos.addScaledVector(camRight, 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 (5 sec dance then fall) ========== */
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; }
// Violent fire dance for 5 seconds then KO
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;
// Random running impulses
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 & EXPLOSIONS ========== */
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(){ const powerVal=parseFloat(document.getElementById('c4Power').value); const t=powerVal/100; const force=8+t*72; const radius=1.2+t*3.2; let any=false;
for(const c of c4List){ if(!c.armed)continue; any=true; 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; if(any)snapCameraToBody(); }
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 ========== */
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 ========== */
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;
// Alternate raising left and right hand every 0.6 seconds
const phase=Math.floor(sixtySevenTimer/0.6)%2;
if(phase===0){ // raise left hand
T.handL.y=POSE.handL.y+0.55;
T.handR.y=POSE.handR.y;
T.elbowL.y=POSE.elbowL.y+0.35;
T.elbowR.y=POSE.elbowR.y;
} else { // raise right hand
T.handL.y=POSE.handL.y;
T.handR.y=POSE.handR.y+0.55;
T.elbowL.y=POSE.elbowL.y;
T.elbowR.y=POSE.elbowR.y+0.35;
}
}
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();
// State machine
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 & FIRING (ball single shot) ========== */
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); };
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 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); 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>