The core invention: paint lands on the blob body where projectiles hit, live
per-colour coverage % is computed from a quantized UV mask, and coverage drives
buffs per GDD §6 MVP subset (RED speed+embers, GREEN grip, BLUE waterproof,
total→mass, 20% activate / 70% super+glow).
- src/paint/skin.ts PaintSkin: Canvas-2D mask on mesh UVs → CanvasTexture;
splatAtPoint raycasts impact→face UV; dual soft-canvas
+ hard quantized-index mask so coverage is exact
integer bucketing (never getImageData); 250ms cache;
scrub-hole cleanse; U-seam wrap.
- src/paint/coverage-math.ts pure, unit-tested: countBuckets, coverage report,
buffStrength, computeModifiers.
- src/paint/cannon.ts PaintCannon: gravity-arc glob projectiles (manual
swept sphere test), target-leading ballistic solve,
fires on interval AND machine:signal; emits paint:splatted.
- src/paint/buffs.ts BuffSystem: coverage→modifiers each step + ember trail
(additive Points) + super-state emissive pulse.
- src/paint/hud.ts PaintHUD: per-colour bars + active buff kit (throttled).
- src/paint/index.ts exports + installPaint() wiring the inbound Lane C
protocol (request-splat proximity-gated / request-cleanse).
- src/demo/lane-b.ts stub ball orbiting a RED and GREEN cannon; HUD live;
debug keys for fire/signal/flood/cleanse/pause.
- src/paint/coverage-math.test.ts 32 assertions (node --experimental-strip-types).
build: tsc strict + vite pass. No edits outside owned paths; contracts frozen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 lines
18 KiB
JavaScript
2 lines
18 KiB
JavaScript
import"./modulepreload-polyfill-B5Qt9EMX.js";import{R as st,V as h,C as ot,S as it,a as nt,b as at,c as X,M as _,d as k,G as W,e as O,f as rt,g as U,B as lt,h as q,P as ct,A as ht,i as dt,j as pt,k as ut,l as mt}from"./world-KhDGgEOv.js";const f={red:"#FF3B30",orange:"#FF9500",yellow:"#FFD60A",green:"#34C759",blue:"#0A84FF",purple:"#AF52DE",pink:"#FF6EB4"},A="#F5F5F7",ft=()=>({speedMul:1,jumpMul:1,massMul:1,grip:0,size:1,waterproof:!1,glow:0}),v=["red","orange","yellow","green","blue","purple","pink"];function bt(a){return v.indexOf(a)+1}function gt(a,t){const s=new Array(t+1).fill(0);for(let e=0;e<a.length;e++)s[a[e]]++;return s}function yt(a,t){const s={};for(let o=0;o<v.length;o++)s[v[o]]=t>0?a[o+1]/t:0;return{total:t>0?(t-a[0])/t:0,byColor:s}}const C=.2,T=.7,K=.15,xt=a=>a<0?0:a>1?1:a;function E(a){if(a<C)return 0;if(a>=T)return 1;const t=(a-C)/(T-C);return K+(1-K)*t}function Y(a){let t=null,s=T;for(const e of v)a.byColor[e]>=s&&(s=a.byColor[e],t=e);return t}function wt(a){let t=null,s=0;for(const e of v)a.byColor[e]>s&&(s=a.byColor[e],t=e);return t}function Mt(a){const t=a.byColor.red,s=a.byColor.green,e=a.byColor.blue,o=1+.6*E(t),i=E(s),n=e>=C,r=1+.8*xt(a.total),l=Y(a)?1:0;return{speedMul:o,jumpMul:1,massMul:r,grip:i,size:1,waterproof:n,glow:l}}class Ct{canvas;texture;boundingRadius;mesh;ctx;size;splatScale;mask;raycaster=new st;cache=null;cacheTime=-1e9;_center=new h;_dir=new h;_origin=new h;constructor(t,s={}){this.mesh=t,this.size=s.size??256,this.splatScale=s.splatScale??.16,t.geometry.computeBoundingSphere(),this.boundingRadius=t.geometry.boundingSphere?.radius??1;const e=document.createElement("canvas");e.width=e.height=this.size;const o=e.getContext("2d");if(!o)throw new Error("PaintSkin: 2D canvas context unavailable");o.fillStyle=A,o.fillRect(0,0,this.size,this.size),this.canvas=e,this.ctx=o,this.mask=new Uint8Array(this.size*this.size);const i=new ot(e);i.colorSpace=it,i.wrapS=nt,i.wrapT=at,i.anisotropy=4,this.texture=i;const n=t.material;n.map=i,n.color&&n.color.set("#ffffff"),n.needsUpdate=!0}applySplat(t,s,e){const o=t.x*this.size,i=(1-t.y)*this.size,n=this.pixelRadius(e),r=this.makeSplatCircles(o,i,n);this.stampMask(r,bt(s)),this.stampCanvas(r,f[s]),this.texture.needsUpdate=!0}splatAtPoint(t,s,e){const o=this.uvAtWorldPoint(t);o&&this.applySplat(o,s,e)}coverage(){const t=performance.now();if(this.cache&&t-this.cacheTime<250)return this.cache;const s=gt(this.mask,v.length);return this.cache=yt(s,this.mask.length),this.cacheTime=t,this.cache}cleanse(t){const s=Math.max(0,Math.min(1,t));if(s<=0)return;if(s>=1){this.mask.fill(0),this.ctx.fillStyle=A,this.ctx.fillRect(0,0,this.size,this.size),this.invalidate();return}let e=0;for(let n=0;n<this.mask.length;n++)this.mask[n]!==0&&e++;if(e===0)return;let o=Math.floor(e*s);this.ctx.fillStyle=A;let i=0;for(;o>0&&i++<2e4;){const n=Math.random()*this.size,r=Math.random()*this.size,l=6+Math.random()*12;o-=this.eraseCircle(n,r,l)}this.invalidate()}uvAtWorldPoint(t){if(this.mesh.updateWorldMatrix(!0,!1),this.mesh.getWorldPosition(this._center),this._dir.copy(t).sub(this._center),this._dir.lengthSq()<1e-12)return null;this._dir.normalize();const s=this.worldBoundingRadius();this._origin.copy(this._center).addScaledVector(this._dir,s*2.2),this.raycaster.set(this._origin,this._dir.clone().negate()),this.raycaster.near=0,this.raycaster.far=s*4.4;const o=this.raycaster.intersectObject(this.mesh,!1).find(i=>i.uv);return o?.uv?o.uv.clone():null}worldBoundingRadius(){const t=this.mesh.getWorldScale(this._origin);return this.boundingRadius*Math.max(t.x,t.y,t.z)}pixelRadius(t){const s=t/this.boundingRadius*this.size*this.splatScale;return Math.max(4,Math.min(this.size*.4,s))}makeSplatCircles(t,s,e){const o=[{x:t,y:s,r:e*.95}],i=3+Math.floor(Math.random()*3);for(let r=0;r<i;r++){const l=Math.random()*Math.PI*2,c=e*(.35+Math.random()*.55);o.push({x:t+Math.cos(l)*c,y:s+Math.sin(l)*c,r:e*(.3+Math.random()*.35)})}const n=2+Math.floor(Math.random()*3);for(let r=0;r<n;r++){const l=Math.random()*Math.PI*2,c=e*(1+Math.random()*.7);o.push({x:t+Math.cos(l)*c,y:s+Math.sin(l)*c,r:e*(.08+Math.random()*.14)})}return o}stampMask(t,s){const e=this.size;for(const o of t){const i=this.wrapOffsets(o.x,o.r,e);for(const n of i){const r=o.x+n,l=o.r*o.r,c=Math.max(0,Math.floor(r-o.r)),d=Math.min(e-1,Math.ceil(r+o.r)),p=Math.max(0,Math.floor(o.y-o.r)),y=Math.min(e-1,Math.ceil(o.y+o.r));for(let u=p;u<=y;u++){const x=u-o.y;for(let w=c;w<=d;w++){const P=w-r;P*P+x*x<=l&&(this.mask[u*e+w]=s)}}}}}stampCanvas(t,s){const e=this.size,o=this.ctx,i=vt(s);for(const n of t){const r=this.wrapOffsets(n.x,n.r,e);for(const l of r){const c=n.x+l,d=o.createRadialGradient(c,n.y,n.r*.15,c,n.y,n.r);d.addColorStop(0,`rgba(${i},1)`),d.addColorStop(.7,`rgba(${i},0.95)`),d.addColorStop(1,`rgba(${i},0)`),o.fillStyle=d,o.beginPath(),o.arc(c,n.y,n.r,0,Math.PI*2),o.fill()}}}eraseCircle(t,s,e){const o=this.size;let i=0;const n=e*e,r=Math.max(0,Math.floor(t-e)),l=Math.min(o-1,Math.ceil(t+e)),c=Math.max(0,Math.floor(s-e)),d=Math.min(o-1,Math.ceil(s+e));for(let p=c;p<=d;p++){const y=p-s;for(let u=r;u<=l;u++){const x=u-t;if(x*x+y*y<=n){const w=p*o+u;this.mask[w]!==0&&(this.mask[w]=0,i++)}}}return this.ctx.beginPath(),this.ctx.arc(t,s,e,0,Math.PI*2),this.ctx.fill(),i}wrapOffsets(t,s,e){return t-s<0?[0,e]:t+s>e?[0,-e]:[0]}invalidate(){this.texture.needsUpdate=!0,this.cache=null,this.cacheTime=-1e9}}function vt(a){const t=a.replace("#",""),s=parseInt(t,16);return`${s>>16&255},${s>>8&255},${s&255}`}const zt=6;class J{color;cfg;scene;projectiles=[];projGeo;projMat;barrel;acc=0;unsub;_lastTarget=new h;_targetVel=new h;hasLast=!1;_tpos=new h;_aim=new h;_hit=new h;_vtmp=new h;constructor(t){this.color=t.color,this.cfg={interval:0,muzzleSpeed:16,projRadius:.12,splatRadius:.28,gravity:14,groundY:0,lead:!0,...t},this.scene=t.world.scene;const s=f[t.color];this.projGeo=new X(this.cfg.projRadius,12,10),this.projMat=new _({color:s,emissive:s,emissiveIntensity:.35,roughness:.4}),this.barrel=this.buildBarrel(s),this.barrel.position.copy(t.position),this.scene.add(this.barrel),this.unsub=t.world.events.on("machine:signal",e=>{this.cfg.triggerId&&e?.id===this.cfg.triggerId&&this.fire()})}fire(){this.target(this._tpos);const t=this.muzzleWorld();if(this._aim.copy(this._tpos),this.cfg.lead&&this.hasLast){const o=t.distanceTo(this._tpos)/this.cfg.muzzleSpeed;this._aim.addScaledVector(this._targetVel,o)}const s=St(t,this._aim,this.cfg.muzzleSpeed,this.cfg.gravity)??this._aim.clone().sub(t).normalize().multiplyScalar(this.cfg.muzzleSpeed);this.aimBarrel(s);const e=new k(this.projGeo,this.projMat);e.castShadow=!0,e.position.copy(t),this.scene.add(e),this.projectiles.push({mesh:e,vel:s.clone(),prev:t.clone(),life:0})}update(t){if(t>0){if(this.target(this._tpos),this.hasLast){const e=1/t;this._vtmp.copy(this._tpos).sub(this._lastTarget).multiplyScalar(e),this._targetVel.lerp(this._vtmp,.4)}this._lastTarget.copy(this._tpos),this.hasLast=!0}if(this.cfg.interval>0)for(this.acc+=t;this.acc>=this.cfg.interval;)this.acc-=this.cfg.interval,this.fire();if(this.projectiles.length===0)return;this.target(this._tpos);const s=this.cfg.targetRadius+this.cfg.projRadius;for(let e=this.projectiles.length-1;e>=0;e--){const o=this.projectiles[e];if(o.prev.copy(o.mesh.position),o.vel.y-=this.cfg.gravity*t,o.mesh.position.addScaledVector(o.vel,t),o.life+=t,Rt(this._tpos,o.prev,o.mesh.position,this._hit)<=s){this.cfg.paint.splatAtPoint(this._hit,this.color,this.cfg.splatRadius),this.cfg.world.events.emit("paint:splatted",{color:this.color,target:"blob"}),this.despawn(e);continue}(o.mesh.position.y<=this.cfg.groundY||o.life>zt)&&(this.cfg.world.events.emit("paint:splatted",{color:this.color,target:"world"}),this.despawn(e))}}dispose(){this.unsub();for(let t=this.projectiles.length-1;t>=0;t--)this.despawn(t);this.scene.remove(this.barrel),this.projGeo.dispose(),this.projMat.dispose()}target(t){return this.cfg.target.getWorldPosition(t)}muzzleWorld(){return this.cfg.position.clone().add(new h(0,.55,0))}despawn(t){const s=this.projectiles[t];this.scene.remove(s.mesh),this.projectiles.splice(t,1)}buildBarrel(t){const s=new W,e=new k(new O(.45,.55,.5,16),new _({color:"#3a3a44",roughness:.7}));e.castShadow=!0,s.add(e);const o=new k(new O(.22,.28,1.1,16),new _({color:t,roughness:.5}));o.castShadow=!0;const i=new W;return i.position.y=.45,o.position.y=.4,o.rotation.x=Math.PI/2,i.add(o),i.name="pivot",s.add(i),s}aimBarrel(t){const s=this.barrel.getObjectByName("pivot");if(!s)return;const e=t.clone().normalize(),o=new rt().lookAt(new h(0,0,0),e,new h(0,1,0));s.quaternion.setFromRotationMatrix(o)}}function St(a,t,s,e){const o=t.x-a.x,i=t.z-a.z,n=t.y-a.y,r=Math.hypot(o,i);if(r<.001)return new h(0,Math.sign(n||1)*s,0);const l=s*s,c=l*l-e*(e*r*r+2*n*l);if(c<0)return null;const d=Math.sqrt(c),p=(l-d)/(e*r),y=Math.atan(p),u=o/r,x=i/r,w=s*Math.cos(y),P=s*Math.sin(y);return new h(u*w,P,x*w)}function Rt(a,t,s,e){const o=s.x-t.x,i=s.y-t.y,n=s.z-t.z,r=o*o+i*i+n*n;let l=0;return r>1e-12&&(l=((a.x-t.x)*o+(a.y-t.y)*i+(a.z-t.z)*n)/r,l=l<0?0:l>1?1:l),e.set(t.x+o*l,t.y+i*l,t.z+n*l),e.distanceTo(a)}const z=90;class _t{scene;blob;embers;emberPos;emberCol;emberVel;emberLife;emberCursor=0;emberHex=new U;_spawnAt=new h;clock=0;constructor(t,s){this.scene=t.scene,this.blob=s,this.emberHex.set(f.red).lerp(new U(f.orange),.5),this.emberPos=new Float32Array(z*3),this.emberCol=new Float32Array(z*3),this.emberVel=new Float32Array(z*3),this.emberLife=new Float32Array(z);const e=new lt;e.setAttribute("position",new q(this.emberPos,3)),e.setAttribute("color",new q(this.emberCol,3));const o=new ct({size:.16,vertexColors:!0,transparent:!0,depthWrite:!1,blending:ht,sizeAttenuation:!0});this.embers=new dt(e,o),this.embers.frustumCulled=!1,this.scene.add(this.embers)}update(t){this.clock+=t;const s=this.blob.paint;if(!s)return;const e=s.coverage(),o=Mt(e),i=this.blob.modifiers;i.speedMul=o.speedMul,i.jumpMul=o.jumpMul,i.massMul=o.massMul,i.grip=o.grip,i.size=o.size,i.waterproof=o.waterproof,i.glow=o.glow,this.updateEmbers(t,e.byColor.red),this.updateGlow(o.glow,Y(e)??wt(e))}dispose(){this.scene.remove(this.embers),this.embers.geometry.dispose(),this.embers.material.dispose()}updateEmbers(t,s){const e=E(s);if(s>=C){const i=e*3;let n=Math.floor(i)+(Math.random()<i%1?1:0);for(this.blob.mesh.getWorldPosition(this._spawnAt);n-- >0;)this.spawnEmber(this._spawnAt)}const o=this.emberHex;for(let i=0;i<z;i++){if(this.emberLife[i]<=0)continue;this.emberLife[i]-=t;const n=i*3;if(this.emberLife[i]<=0){this.emberCol[n]=this.emberCol[n+1]=this.emberCol[n+2]=0;continue}this.emberVel[n+1]+=2.2*t,this.emberPos[n]+=this.emberVel[n]*t,this.emberPos[n+1]+=this.emberVel[n+1]*t,this.emberPos[n+2]+=this.emberVel[n+2]*t;const r=Math.min(1,this.emberLife[i]/.5);this.emberCol[n]=o.r*r,this.emberCol[n+1]=o.g*r,this.emberCol[n+2]=o.b*r}this.embers.geometry.attributes.position.needsUpdate=!0,this.embers.geometry.attributes.color.needsUpdate=!0}spawnEmber(t){const s=this.emberCursor;this.emberCursor=(this.emberCursor+1)%z;const e=s*3;this.emberPos[e]=t.x+(Math.random()-.5)*.5,this.emberPos[e+1]=t.y+(Math.random()-.3)*.4,this.emberPos[e+2]=t.z+(Math.random()-.5)*.5,this.emberVel[e]=(Math.random()-.5)*.8,this.emberVel[e+1]=.6+Math.random()*.8,this.emberVel[e+2]=(Math.random()-.5)*.8,this.emberLife[s]=.5+Math.random()*.4}updateGlow(t,s){const e=this.blob.mesh.material;if("emissive"in e)if(t>0&&s){const o=.55+.45*Math.sin(this.clock*8);e.emissive.set(f[s]),e.emissiveIntensity=t*o*1.4}else e.emissiveIntensity=0}}class kt{root;bars=new Map;totalPct;buffLine;lastUpdate=-1e9;constructor(t=document.body){const s=document.createElement("div");s.style.cssText=["position:fixed","top:12px","left:12px","z-index:10","font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace","color:#fff","background:rgba(18,18,24,0.72)","padding:10px 12px","border-radius:10px","min-width:210px","user-select:none","box-shadow:0 4px 18px rgba(0,0,0,0.35)","backdrop-filter:blur(4px)"].join(";");const e=document.createElement("div");e.textContent="PAINT COVERAGE",e.style.cssText="font-weight:700;letter-spacing:0.08em;opacity:0.85;margin-bottom:8px",s.appendChild(e);for(const i of v){const n=document.createElement("div");n.style.cssText="display:flex;align-items:center;gap:6px;margin:3px 0";const r=document.createElement("div");r.style.cssText=`width:10px;height:10px;border-radius:3px;background:${f[i]};flex:0 0 auto`,n.appendChild(r);const l=document.createElement("div");l.style.cssText="flex:1;height:8px;background:rgba(255,255,255,0.12);border-radius:4px;overflow:hidden";const c=document.createElement("div");c.style.cssText=`height:100%;width:0%;background:${f[i]};transition:width 0.12s linear`,l.appendChild(c),n.appendChild(l);const d=document.createElement("span");d.textContent="0%",d.style.cssText="width:34px;text-align:right;opacity:0.9",n.appendChild(d),s.appendChild(n),this.bars.set(i,{fill:c,pct:d})}const o=document.createElement("div");o.style.cssText="margin-top:8px;border-top:1px solid rgba(255,255,255,0.15);padding-top:6px",o.innerHTML="total painted: ",this.totalPct=document.createElement("span"),this.totalPct.textContent="0%",this.totalPct.style.fontWeight="700",o.appendChild(this.totalPct),s.appendChild(o),this.buffLine=document.createElement("div"),this.buffLine.style.cssText="margin-top:6px;opacity:0.95;min-height:16px",s.appendChild(this.buffLine),t.appendChild(s),this.root=s}update(t,s){const e=performance.now();if(!(e-this.lastUpdate<100)){this.lastUpdate=e;for(const o of v){const i=this.bars.get(o),n=t.byColor[o];i.fill.style.width=`${Math.min(100,n*100).toFixed(0)}%`,i.pct.textContent=`${(n*100).toFixed(0)}%`;const r=n>=C;i.pct.style.opacity=r?"1":"0.5",i.fill.style.boxShadow=n>=T?`0 0 8px ${f[o]}`:"none"}this.totalPct.textContent=`${(t.total*100).toFixed(0)}%`,this.buffLine.innerHTML=this.buffText(t,s)}}buffText(t,s){const e=[];return t.byColor.red>=C&&e.push(`<b style="color:${f.red}">BURN</b> ×${s.speedMul.toFixed(2)}`),t.byColor.green>=C&&e.push(`<b style="color:${f.green}">GRIP</b> ${s.grip.toFixed(2)}`),t.byColor.blue>=C&&e.push(`<b style="color:${f.blue}">SLICK</b> waterproof`),e.push(`mass ×${s.massMul.toFixed(2)}`),s.glow>0&&e.push('<b style="color:#fff;text-shadow:0 0 6px #fff">★ SUPER</b>'),e.length?e.join(" · "):'<span style="opacity:0.5">no buffs</span>'}dispose(){this.root.remove()}}function Pt(a,t){const s=new h,e=new h,o=()=>(t.mesh.getWorldScale(e),t.paint.boundingRadius*Math.max(e.x,e.y,e.z)),i=a.events.on("paint:request-splat",r=>{r?.point&&(t.mesh.getWorldPosition(s),s.distanceTo(r.point)<=o()+r.radius&&(t.paint.splatAtPoint(r.point,r.color,r.radius),a.events.emit("paint:splatted",{color:r.color,target:"blob"})))}),n=a.events.on("paint:request-cleanse",r=>{t.paint.cleanse(r?.fraction??1)});return()=>{i(),n()}}const B="sig-red",j="sig-green",L=3.6,S=.6,g=await pt(document.getElementById("app")),{scene:Q,camera:Z,physics:V,rapier:G,events:R}=g;Z.position.set(0,7.5,12);const D=new k(new ut(40,1,40),new _({color:"#e8d9b8"}));D.position.y=-.5;D.receiveShadow=!0;Q.add(D);V.createCollider(G.ColliderDesc.cuboid(20,.5,20).setTranslation(0,-.5,0).setFriction(1.2));const b=new k(new X(S,48,32),new _({color:"#F5F5F7",roughness:.35,metalness:0}));b.castShadow=!0;Q.add(b);const M=V.createRigidBody(G.RigidBodyDesc.dynamic().setTranslation(L,S+.2,0).setAngularDamping(.4));V.createCollider(G.ColliderDesc.ball(S).setRestitution(.2).setFriction(1.4).setDensity(2),M);const Ft=M.mass(),m={mesh:b,body:M,modifiers:ft(),paint:new Ct(b,{size:256})},$=new kt,tt=new J({world:g,position:new h(-7,0,-1.5),color:"red",target:b,paint:m.paint,targetRadius:S,triggerId:B,interval:.9,muzzleSpeed:15,splatRadius:.3}),et=new J({world:g,position:new h(7,0,1.5),color:"green",target:b,paint:m.paint,targetRadius:S,triggerId:j,interval:1,muzzleSpeed:15,splatRadius:.3}),Tt=new _t(g,m);Pt(g,{mesh:b,paint:m.paint});g.addSystem(tt);g.addSystem(et);g.addSystem(Tt);let F=!1;g.addSystem({update(a){M.setAdditionalMass(Math.max(0,Ft*(m.modifiers.massMul-1)),!0);const t=M.translation();if(!F){const o=Math.atan2(t.z,t.x)+.6,i=Math.cos(o)*L,n=Math.sin(o)*L,r=i-t.x,l=n-t.z,c=Math.hypot(r,l)||1,d=9*m.modifiers.speedMul;M.applyImpulse({x:r/c*d*a,y:0,z:l/c*d*a},!0);const p=M.linvel(),y=Math.hypot(p.x,p.z),u=4.2*m.modifiers.speedMul;if(y>u){const x=u/y;M.setLinvel({x:p.x*x,y:p.y,z:p.z*x},!0)}}b.position.set(t.x,t.y,t.z);const s=M.rotation();b.quaternion.set(s.x,s.y,s.z,s.w)}});g.onFrame(()=>{Z.lookAt(b.position.x*.3,.5,b.position.z*.3)});function H(a,t=130){for(let s=0;s<t;s++)m.paint.applySplat(new mt(Math.random(),.1+Math.random()*.8),a,.55);console.log(`[flood] ${a} — ${t} splats`)}addEventListener("keydown",a=>{switch(a.code){case"Digit1":tt.fire();break;case"Digit2":et.fire();break;case"KeyB":R.emit("machine:signal",{id:B}),console.log("[signal] machine:signal",B);break;case"KeyN":R.emit("machine:signal",{id:j}),console.log("[signal] machine:signal",j);break;case"Digit9":H("red");break;case"Digit0":H("green");break;case"KeyG":{const t=b.position.clone().add(new h(0,S,0));R.emit("paint:request-splat",{point:t,color:"blue",radius:.35}),console.log("[request-splat] blue on ball");break}case"KeyK":R.emit("paint:request-cleanse",{fraction:.5}),console.log("[request-cleanse] 0.5");break;case"KeyC":m.paint.cleanse(.5),console.log("[cleanse] 0.5 (scrub)");break;case"KeyX":m.paint.cleanse(1),console.log("[cleanse] full");break;case"KeyP":F=!F,console.log(`[orbit] ${F?"paused":"running"}`);break;case"KeyH":$.root.style.display=$.root.style.display==="none"?"":"none";break}});let I=0;R.on("paint:splatted",a=>{a.target==="blob"&&I++});let N=performance.now();g.onFrame(()=>{const a=m.paint.coverage();$.update(a,m.modifiers);const t=performance.now();if(t-N>=1e3){N=t;const s=v.filter(i=>a.byColor[i]>.005).map(i=>`${i}:${(a.byColor[i]*100).toFixed(0)}%`).join(" "),e=m.modifiers,o=[e.speedMul>1.001?`speed×${e.speedMul.toFixed(2)}`:"",e.grip>.001?`grip${e.grip.toFixed(2)}`:"",e.waterproof?"waterproof":"",`mass×${e.massMul.toFixed(2)}`,e.glow>0?"SUPER":""].filter(Boolean).join(" ");console.log(`[paint] total ${(a.total*100).toFixed(0)}% | ${s||"(clean)"} | ${o} | +${I} splats/s`),I=0}});console.log("%cBLOBBO Lane B — Paint demo",`color:${f.red};font-weight:700`);console.log("keys: 1/2 fire B/N signal 9/0 flood G request-splat K request-cleanse C/X cleanse P pause H hud");g.start();
|