Files
Active-Ragdoll/test_vers/deepseek_html_20260610_336ab8.html
2026-06-10 17:57:18 -04:00

909 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. 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 — Weapons Reloaded</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:296px;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}
#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">ORBIT</span></h1>
<div class="sub">Weapons Edition · Classic Explosions + Sliders</div>
<div><span id="state">BALANCING</span></div>
<div class="lbl">Ragdoll</div>
<div class="row">
<button id="bShove">Shove</button>
<button id="bKO">Knockout</button>
<button id="bSlow" class="tog">Slow-mo</button>
<button id="bReset">Reset</button>
</div>
<div class="lbl">Camera <span style="color:#6b7a8d;font-weight:400">(V to cycle)</span></div>
<div class="row">
<button id="camOrbit" class="sel" onclick="setCamMode('orbit')">🌐 Orbit</button>
<button id="camFollow" onclick="setCamMode('follow')">🎯 Follow</button>
<button id="camFP" onclick="setCamMode('fp')">👁 First-Person</button>
</div>
<div class="lbl">Weapon</div>
<div class="row" 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>
<!-- C4 Slider + HE Slider -->
<div id="c4-options" style="display:none">
<div class="slider-row">
<label>C4 Blast 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; margin-top: 6px;">
<div class="slider-row">
<label>HE Shell 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 &nbsp;·&nbsp; <b>RMB drag</b> grab &nbsp;·&nbsp; <b>LMB drag</b> orbit &nbsp;·&nbsp; <b>V</b> cam &nbsp;·&nbsp; <b>scroll</b> zoom</div>
<div id="flash"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
'use strict';
/* ===== RENDERER ===== */
const renderer = new THREE.WebGLRenderer({antialias:true,powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true; renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputEncoding=THREE.sRGBEncoding;
renderer.toneMapping=THREE.ACESFilmicToneMapping; renderer.toneMappingExposure=1.06;
document.body.appendChild(renderer.domElement);
/* ===== SCENE ===== */
const scene=new THREE.Scene();
scene.fog=new THREE.Fog(0x9fb2c2,18,72);
{
const c=document.createElement('canvas'); c.width=1; c.height=256;
const g=c.getContext('2d'), 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='orbit';
const orbit={yaw:.65,pitch:.18,dist:4.6,target:new THREE.Vector3(0,1,0)};
const fp={yaw:.65,pitch:0};
function updateCamera(){
orbit.pitch=Math.max(-1.48,Math.min(1.48,orbit.pitch));
orbit.dist=Math.max(1,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 {
const cp=Math.cos(orbit.pitch);
camera.position.set(
orbit.target.x+Math.sin(orbit.yaw)*cp*orbit.dist,
orbit.target.y+Math.sin(orbit.pitch)*orbit.dist,
orbit.target.z+Math.cos(orbit.yaw)*cp*orbit.dist
);
camera.lookAt(orbit.target);
}
}
updateCamera();
addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);});
const camBadge=document.getElementById('cam-badge');
const CAM_LABELS={orbit:'ORBIT',follow:'FOLLOW',fp:'1ST PERSON'};
function setCamMode(m){
camMode=m; camBadge.textContent=CAM_LABELS[m];
['Orbit','Follow','FP'].forEach(id=>document.getElementById('cam'+id).classList.toggle('sel',id.toLowerCase()===m||(id==='FP'&&m==='fp')));
if(m==='follow'||m==='fp') orbit.target.copy(brain.com);
}
addEventListener('keydown',e=>{if(e.key==='v'||e.key==='V'){const m=['orbit','follow','fp'];setCamMode(m[(m.indexOf(camMode)+1)%3]);}});
/* ===== PHYSICS (V2 base) ===== */
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 ===== */
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);
}
}
}
/* flash overlay */
const flashEl=document.getElementById('flash');
let flashT=0;
function triggerFlash(i){flashT=.18*i;flashEl.style.opacity=String(Math.min(.85,.45*i));setTimeout(()=>flashEl.style.opacity='0',80);}
function stepFlash(dt){flashT=Math.max(0,flashT-dt);flashPt.intensity=flashT*16;}
/* ===== FIRE STATE (molotov) ===== */
const fireState={active:false,t:0,dur:5};
function startFire(){
fireState.active=true; fireState.t=0;
setState('fire');
}
function stepFire(dt){
if(!fireState.active)return;
fireState.t+=dt;
if(Math.random()<dt*35){
const torsoPos=parts[P.chest].pos.clone().addScaledVector(
new THREE.Vector3(Math.random()-.5,Math.random()*.3,Math.random()-.5),.25);
spawnFire(torsoPos,3,.6+Math.random()*.5);
}
if(fireState.t>fireState.dur){
fireState.active=false;
setState('ko'); brain.koT=99;
}
}
// INTENSE DANCING: more chaotic + larger amplitude
function applyFireDance(dt){
if(!fireState.active)return;
const T=brain.targets, t=fireState.t;
const flail=Math.min(1.2, fireState.t*.75); // grows faster, up to 120%
const intensity = Math.sin(t*1.2)*0.5 + 0.6; // pulsing
// Wild arms with strong asymmetrical swings
T.handL.addScaledVector(_right, -(Math.sin(t*9)*0.55 + 0.2)*flail*intensity).y += Math.abs(Math.sin(t*7.2))*0.65*flail;
T.handR.addScaledVector(_right, (Math.sin(t*8.3+1.2)*0.55 + 0.2)*flail*intensity).y += Math.abs(Math.sin(t*9.1+1))*0.7*flail;
T.elbowL.addScaledVector(_right, -Math.sin(t*11)*0.38*flail).y += Math.abs(Math.sin(t*6.8))*0.42*flail;
T.elbowR.addScaledVector(_right, Math.sin(t*10+1.8)*0.38*flail).y += Math.abs(Math.sin(t*7.5))*0.44*flail;
// Thrashing legs: random stomp kicks
T.footL.y += Math.abs(Math.sin(t*5.5))*0.45*flail;
T.footR.y += Math.abs(Math.sin(t*6.2+1.2))*0.45*flail;
T.kneeL.y += Math.abs(Math.sin(t*4.8))*0.32*flail;
T.kneeR.y += Math.abs(Math.sin(t*5.6+0.9))*0.32*flail;
// Spasmodic torso twist and head shake
T.chest.addScaledVector(_right, Math.sin(t*6.7)*0.19*flail).z += Math.sin(t*5.3)*0.12*flail;
T.head.addScaledVector(_right, Math.sin(t*7.8)*0.22*flail).y += Math.sin(t*4.9)*0.18*flail;
T.head.addScaledVector(_fwd, Math.sin(t*8.2)*0.14*flail);
}
/* ===== EXPLOSIONS (V2 style: classic radial launch) ===== */
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){ setState('ko'); brain.koT=Math.max(brain.koT,4.5); }
}
// AP Skewer: fixed pushes each part strongly backward but also downward/slide
function skewerAP(shotDir, force){
for(const p of parts){
const imp = shotDir.clone().multiplyScalar(force * p.m * 0.28);
imp.y = -force * p.m * 0.15; // downward pin to prevent flying upward
addImpulse(p, imp);
}
setState('ko');
brain.koT = Math.max(brain.koT, 7);
brain.strength = 0.02;
}
/* ===== C4 (V2 classic explosion) ===== */
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; // V2 style range: up to 80
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(){orbit.target.copy(brain.com);orbit.dist=Math.max(orbit.dist,6);}
/* ===== PROJECTILES ===== */
const balls=[];
const ballGeo=new THREE.SphereGeometry(1,14,10);
const BMATS={
normal: new THREE.MeshStandardMaterial({color:0xd04020,roughness:.55,metalness:.1}),
gun: new THREE.MeshStandardMaterial({color:0xd4aa44,roughness:.15,metalness:.95}),
HE: new THREE.MeshStandardMaterial({color:0xcc3311,roughness:.4,metalness:.5,emissive:0x441100}),
AP: new THREE.MeshStandardMaterial({color:0x44aa66,roughness:.25,metalness:.9,emissive:0x001a0a}),
cannon: new THREE.MeshStandardMaterial({color:0x445566,roughness:.3,metalness:.8}),
molotov:new THREE.MeshStandardMaterial({color:0x88aa44,roughness:.4,metalness:.1}),
};
function spawnBall(pos,vel,r,m,type='normal'){
const mesh=new THREE.Mesh(ballGeo,BMATS[type]||BMATS.normal);
mesh.castShadow=true; mesh.scale.setScalar(r); scene.add(mesh);
balls.push({pos:pos.clone(),vel:vel.clone(),r,m,life:12,mesh,type,hitParts:new Set()});
}
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*1.2));
ball.vel.multiplyScalar(.55);
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*2.4));
addImpulse(part, new THREE.Vector3(0, -vn*ball.m*0.55, 0));
spawnSparks(ball.pos.clone(), 7, 0x88ffaa, 2.5, .35);
}
ball.vel.multiplyScalar(.94);
return false;
} else if(ball.type==='HE'){
return true;
} else if(ball.type==='cannon'){
const share=3*ball.m/(ball.m+part.m);
addImpulse(part,_tmp.copy(_d).multiplyScalar(vn*share));
ball.vel.addScaledVector(_d,-vn*1.6);
ball.pos.copy(part.pos).addScaledVector(_d,-(part.r+ball.r));
return false;
} else if(ball.type==='normal'){
const share=2*ball.m/(ball.m+part.m);
addImpulse(part,_tmp.copy(_d).multiplyScalar(vn*share*.8));
ball.vel.addScaledVector(_d,-vn*1.4);
ball.pos.copy(part.pos).addScaledVector(_d,-(part.r+ball.r));
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 hePowerSlider = parseFloat(document.getElementById('hePower').value);
const t = hePowerSlider/100;
const force = 12 + t*58; // V2 classic force range
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(), 22, 0xff8800, 4, .8);
spawnFire(b.pos.clone(), 28, 1.4);
spawnSmoke(b.pos.clone(), 7);
flashPt.position.copy(b.pos); flashPt.intensity=5; setTimeout(()=>flashPt.intensity=0,110);
if(b.pos.distanceTo(brain.com)<2.8) 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;}
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 & VISUALS (same as V4) ===== */
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,metalness:.05});
const matSuit2=new THREE.MeshStandardMaterial({color:0x39414d,roughness:.8, metalness:.05});
const matSkin =new THREE.MeshStandardMaterial({color:0xc99c7d,roughness:.55,metalness:0});
const matShoe =new THREE.MeshStandardMaterial({color:0x22262d,roughness:.42,metalness:.1});
const rig=new THREE.Group(); scene.add(rig);
const limbs=[],pickMeshes=[],UP=new THREE.Vector3(0,1,0);
function makeLimb(a,b,ra,rb,mat){
const grp=new THREE.Group();
const cyl=new THREE.Mesh(new THREE.CylinderGeometry(rb,ra,1,12,1,true),mat); cyl.castShadow=true; grp.add(cyl);
const capA=new THREE.Mesh(new THREE.SphereGeometry(ra,12,8),mat); capA.castShadow=true; grp.add(capA);
const capB=new THREE.Mesh(new THREE.SphereGeometry(rb,12,8),mat); capB.castShadow=true; grp.add(capB);
rig.add(grp);
const L={grp,cyl,capA,capB,ia:P[a],ib:P[b]};
limbs.push(L);
for(const m of[cyl,capA,capB]){m.userData.pick=[P[a],P[b]];pickMeshes.push(m);}
return L;
}
makeLimb('pelvis','chest',.135,.15,matSuit);
makeLimb('chest','neck',.1,.055,matSuit);
makeLimb('neck','head',.05,.05,matSkin);
makeLimb('shoulderL','shoulderR',.055,.055,matSuit);
makeLimb('hipL','hipR',.07,.07,matSuit2);
makeLimb('shoulderL','elbowL',.055,.045,matSuit);
makeLimb('shoulderR','elbowR',.055,.045,matSuit);
makeLimb('elbowL','handL',.042,.034,matSuit);
makeLimb('elbowR','handR',.042,.034,matSuit);
makeLimb('hipL','kneeL',.075,.055,matSuit2);
makeLimb('hipR','kneeR',.075,.055,matSuit2);
makeLimb('kneeL','footL',.052,.04,matSuit2);
makeLimb('kneeR','footR',.052,.04,matSuit2);
makeLimb('footL','toeL',.05,.044,matShoe);
makeLimb('footR','toeR',.05,.044,matShoe);
const headMesh=new THREE.Mesh(new THREE.SphereGeometry(.105,18,14),matSkin);
headMesh.castShadow=true; headMesh.scale.set(.92,1.12,.98);
headMesh.userData.pick=[P.head]; rig.add(headMesh); pickMeshes.push(headMesh);
const handMeshes=['handL','handR'].map(n=>{
const m=new THREE.Mesh(new THREE.SphereGeometry(.05,10,8),matSkin);
m.castShadow=true; m.scale.set(.8,1.15,1);
m.userData.pick=[P[n]]; rig.add(m); pickMeshes.push(m); return m;
});
const _q=new THREE.Quaternion();
function syncVisuals(){
for(const L of limbs){
const a=parts[L.ia].pos,b=parts[L.ib].pos,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 (simplified, includes fire dance) ===== */
const GAIN={pelvis:[72,9],chest:[72,9],neck:[55,7],head:[48,7],shoulder:[48,7],
elbow:[30,5],hand:[24,4],hip:[72,9],knee:[62,8],foot:[85,10],toe:[65,8]};
const GCOMP={pelvis:.92,chest:.92,neck:.9,head:.9,shoulder:.85,
elbow:.7,hand:.6,hip:.9,knee:.55,foot:0,toe:0};
function groupOf(n){return n.replace(/[LR]$/,'');}
const brain={
state:'balance',t:0,koT:0,strength:1,
root:new THREE.Vector3(),
com:new THREE.Vector3(),comPrev:new THREE.Vector3(0,1,0),comVel:new THREE.Vector3(),
step:null,lastFoot:'R',cool:0,targets:{},totalM:0
};
for(const p of parts){brain.targets[p.name]=new THREE.Vector3();brain.totalM+=p.m;}
const _right=new THREE.Vector3(),_fwd=new THREE.Vector3(), _off=new THREE.Vector3(),_feet=new THREE.Vector3(),_fall=new THREE.Vector3();
let simTime=0;
function setState(s){if(brain.state!==s){brain.state=s;brain.t=0;}}
function brainUpdate(dt){
simTime+=dt; brain.t+=dt; brain.cool=Math.max(0,brain.cool-dt);
const B=brain,T=B.targets;
_right.subVectors(parts[P.hipR].pos,parts[P.hipL].pos); _right.y=0;
if(_right.lengthSq()<1e-6)_right.set(1,0,0); else _right.normalize();
_fwd.set(-_right.z,0,_right.x);
B.com.set(0,0,0);
for(const p of parts)B.com.addScaledVector(p.pos,p.m/B.totalM);
B.comVel.subVectors(B.com,B.comPrev).multiplyScalar(1/dt); B.comPrev.copy(B.com);
_feet.addVectors(parts[P.footL].pos,parts[P.footR].pos).multiplyScalar(.5);
_off.subVectors(B.com,_feet); _off.y=0;
_off.addScaledVector(_t.copy(B.comVel).setY(0),.16);
const offLen=_off.length();
_t2.subVectors(parts[P.neck].pos,parts[P.pelvis].pos).normalize();
const upr=_t2.y,comSpeed=B.comVel.length();
if(B.state==='balance'){
B.strength+=(1-B.strength)*Math.min(1,dt*5);
if(upr<.55||offLen>.34)setState('falling');
} else if(B.state==='falling'){
B.strength=.3;
if(B.com.y<.55&&comSpeed<1.8)setState('down');
else if(upr>.78&&offLen<.12&&B.com.y>.85)setState('balance');
} else if(B.state==='down'){
B.strength=.06;
if(B.t>1.25)setState('getup');
if(upr>.78&&B.com.y>.85)setState('balance');
} else if(B.state==='getup'){
B.strength=1.25;
if(upr>.82&&B.com.y>.88&&offLen<.2)setState('balance');
if(B.t>3)setState('down');
} else if(B.state==='ko'){
B.strength=.015; B.koT-=dt;
if(B.koT<=0)setState('down');
} else if(B.state==='fire'){
B.strength=.55;
}
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(B.state==='fire') applyFireDance(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 STATE ===== */
let currentWeapon='ball',currentAmmo='normal',isFiring=false,gunFireCooldown=0;
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(58),.04,1.1,'gun');
flashPt.position.copy(origin); flashPt.intensity=9; setTimeout(()=>flashPt.intensity=0,40);
spawnSparks(origin.clone(),6,0xffdd66,4,.2);
} else if(currentWeapon==='cannon'){
let speed,radius,mass,type,flashMult;
if(currentAmmo==='HE'){ speed=27;radius=.18;mass=12.5;type='HE';flashMult=1.3;}
else if(currentAmmo==='AP'){speed=42;radius=.11;mass=8.2;type='AP';flashMult=.9;}
else{ speed=23;radius=.15;mass=10.5;type='cannon';flashMult=1;}
const spawnPt=_fireRay.ray.origin.clone().addScaledVector(dir,.85);
spawnBall(spawnPt,dir.multiplyScalar(speed),radius,mass,type);
triggerFlash(flashMult);
spawnSparks(spawnPt.clone(),12,0xffaa22,3.2,.28);
} 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(12.5);
throwVel.y+=4.2;
spawnBall(origin,throwVel,.078,1.55,'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(currentWeapon==='gun'||currentWeapon==='cannon'){
isFiring=true; gunFireCooldown=0; fireWeapon(_liveNDC.x,_liveNDC.y);
}
}
});
renderer.domElement.addEventListener('pointermove',e=>{
const dx=e.clientX-lastX,dy=e.clientY-lastY;
lastX=e.clientX; lastY=e.clientY; movedPx+=Math.abs(dx)+Math.abs(dy);
_liveNDC.x=(e.clientX/innerWidth)*2-1;
_liveNDC.y=-(e.clientY/innerHeight)*2+1;
if(dragMode==='orbit'||(dragMode==='mayfire'&&movedPx>5)){
dragMode='orbit'; isFiring=false;
if(camMode==='fp'){fp.yaw-=dx*.004;fp.pitch+=dy*.004;}
else{orbit.yaw-=dx*.005;orbit.pitch+=dy*.005;}
updateCamera();
} else if(dragMode==='grab'){
_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!=='gun'&&currentWeapon!=='cannon') fireWeapon(_liveNDC.x,_liveNDC.y);
}
grab.active=false; dragMode=null; isFiring=false;
});
renderer.domElement.addEventListener('wheel',e=>{e.preventDefault(); orbit.dist*=(e.deltaY>0?1.1:.9); updateCamera();},{passive:false});
addEventListener('contextmenu',e=>e.preventDefault());
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)*4.2,1.2,Math.sin(a)*4.2); addImpulse(parts[P.chest],_t); addImpulse(parts[P.pelvis],_t2.copy(_t).multiplyScalar(.55)); };
document.getElementById('bKO').onclick=()=>{brain.koT=3.5;setState('ko');};
let slow=false; document.getElementById('bSlow').onclick=function(){slow=!slow;this.classList.toggle('on',slow);};
document.getElementById('bReset').onclick=()=>{ for(const p of parts){p.pos.copy(POSE[p.name]);p.prev.copy(POSE[p.name]);p.force.set(0,0,0);} for(const b of balls)scene.remove(b.mesh); balls.length=0; for(const c of c4List)scene.remove(c.mesh); c4List.length=0; for(const s of sparks)scene.remove(s.mesh); sparks.length=0; fireState.active=false; brain.root.set(0,0,0); brain.comPrev.set(0,1,0); brain.step=null; setState('balance'); brain.strength=1; orbit.target.set(0,1,0); orbit.dist=4.6; updateCamera(); };
document.getElementById('bDetonate').onclick=detonateC4;
document.getElementById('bClearC4').onclick=()=>{for(const c of c4List)scene.remove(c.mesh);c4List.length=0;};
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==='gun'||currentWeapon==='cannon')){
gunFireCooldown-=dt;
if(gunFireCooldown<=0){ gunFireCooldown=currentWeapon==='gun'?0.14:0.48; 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); stepFire(dt);
if(camMode==='follow'){orbit.target.copy(brain.com);updateCamera();}
else if(camMode==='fp'){updateCamera();}
else{ _t.copy(brain.com); _t.y=Math.max(.4,Math.min(1.6,brain.com.y)); orbit.target.lerp(_t,1-Math.exp(-dt*4)); updateCamera();}
syncVisuals(); syncBadge();
renderer.render(scene,camera);
}
animate();
</script>
</body>
</html>