-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex_13.txt
More file actions
84 lines (84 loc) · 27.9 KB
/
Copy pathindex_13.txt
File metadata and controls
84 lines (84 loc) · 27.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Silhouette Spin</title>
<style>
body{margin:0;background:#111;overflow:hidden;font-family:monospace;}
canvas{display:block;width:100vw;height:100vh;background:#222;image-rendering:pixelated;touch-action:none;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
'use strict';
const C={DEADZONE:.15,TICK_ANGLE_2D:2.5,TICK_ANGLE_3D:1.5,SPEED_SLOW_THRESHOLD:.33,SPEED_MEDIUM_THRESHOLD:.66,BASE_TICKS_SLOW:8,BASE_TICKS_MEDIUM:16,BASE_TICKS_FAST:28,FINE_CONTROL_MULTIPLIER:.4,TICK_AUDIO_DURATION:.03,TICK_HAPTIC_DURATION:15,TICK_HAPTIC_MAGNITUDE:.15,PULSE_MIN_INTERVAL:100,PULSE_MAX_INTERVAL:800,PULSE_DURATION:60,PULSE_INTENSITY:.5,SHAKE_DURATION:20,SHAKE_INTENSITY:12,BASE_POINTS:100,SPEED_BONUS_MAX:50,PRECISION_BONUS:25,DIFFICULTY_MIN_SPEED:.7,DIFFICULTY_MAX_SPEED:1.5,DIFFICULTY_MIN_PRECISION:.95,DIFFICULTY_MAX_PRECISION:.99,CELEBRATE_FRAMES:30,UNLOCK_FRAMES:90,TOTAL_LEVELS:50,MODE_UNLOCK_LEVEL:16,MAX_PRESTIGE:100,PI:Math.PI,HALF_PI:Math.PI/2,TWO_PI:Math.PI*2,DEG_TO_RAD:Math.PI/180,RAD_TO_DEG:180/Math.PI};
const SHAPE_2D=[
{name:"The Classic Octagon",vertices:[[0,1],[.7,.7],[1,0],[.7,-.7],[0,-1],[-.7,-.7],[-1,0],[-.7,.7]]},
{name:"Wonky Hexagon",vertices:[[0,1],[.9,.5],[.8,-.6],[0,-1],[-.9,-.4],[-.7,.6]]},
{name:"The Squashed Star",vertices:[[0,1],[.3,.3],[1,.2],[.4,-.2],[.6,-1],[0,-.4],[-.6,-1],[-.4,-.2],[-1,.2],[-.3,.3]]},
{name:"Lumpy Pentagon",vertices:[[0,1],[.95,.3],[.6,-.9],[-.6,-.9],[-.95,.3]]},
{name:"Abstract Art",vertices:[[0,1],[.5,.6],[.9,.3],[.7,-.2],[.9,-.7],[.3,-.9],[-.4,-.8],[-.8,-.3],[-.6,.4]]},
{name:"The Wobbly Square",vertices:[[.1,.95],[.95,.15],[.85,-.9],[-.1,-.95],[-.95,-.1],[-.85,.9]]},
{name:"Pointy Boi",vertices:[[0,1],[.4,.2],[.95,0],[.5,-.3],[.7,-.9],[0,-.5],[-.7,-.9],[-.5,-.3],[-.95,0],[-.4,.2]]},
{name:"The Shrug",vertices:[[0,.9],[.6,.7],[.9,.2],[.8,-.4],[.3,-.9],[-.3,-.9],[-.8,-.4],[-.9,.2],[-.6,.7]]},
{name:"Quirky Triangle",vertices:[[0,1],[.9,-.5],[-.85,-.6]]},
{name:"The Gentle Wave",vertices:[[0,1],[.5,.8],[1,.4],[.8,-.2],[.5,-.7],[0,-.9],[-.5,-.7],[-.8,-.2],[-1,.4],[-.5,.8]]},
{name:"Chonky Diamond",vertices:[[0,1],[.7,.2],[.9,-.6],[0,-.95],[-.9,-.6],[-.7,.2]]},
{name:"The Fancy Cross",vertices:[[.3,1],[.3,.3],[1,.3],[1,-.3],[.3,-.3],[.3,-1],[-.3,-1],[-.3,-.3],[-1,-.3],[-1,.3],[-.3,.3],[-.3,1]]},
{name:"Sleepy Crescent",vertices:[[.8,.5],[.9,0],[.7,-.6],[.2,-.9],[-.3,-.8],[-.7,-.4],[-.9,.1],[-.8,.6],[-.4,.9],[.1,1]]},
{name:"Confused Hexagon",vertices:[[0,1],[.6,.6],[1,.1],[.8,-.7],[-.2,-.95],[-.9,-.3]]},
{name:"The Grand Finale",vertices:[[0,1],[.2,.5],[.6,.7],[.9,.3],[1,-.2],[.7,-.7],[.3,-.95],[-.3,-.95],[-.7,-.7],[-1,-.2],[-.9,.3],[-.6,.7],[-.2,.5]]}
];
const SHAPE_3D=[
{name:"The Honest Cube",v:[[-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1],[-1,-1,1],[1,-1,1],[1,1,1],[-1,1,1]]},
{name:"Tall Boi",v:[[-.7,-1.5,-.7],[.7,-1.5,-.7],[.7,1.5,-.7],[-.7,1.5,-.7],[-.7,-1.5,.7],[.7,-1.5,.7],[.7,1.5,.7],[-.7,1.5,.7]]},
{name:"Flat Stanley",v:[[-1.2,-.4,-.4],[1.2,-.4,-.4],[1.2,.4,-.4],[-1.2,.4,-.4],[-1.2,-.4,.4],[1.2,-.4,.4],[1.2,.4,.4],[-1.2,.4,.4]]},
{name:"The Brick",v:[[-1.3,-.6,-.6],[1.3,-.6,-.6],[1.3,.6,-.6],[-1.3,.6,-.6],[-1.3,-.6,.6],[1.3,-.6,.6],[1.3,.6,.6],[-1.3,.6,.6]]},
{name:"Chonky Cube",v:[[-1.2,-1.2,-1.2],[1.2,-1.2,-1.2],[1.2,1.2,-1.2],[-1.2,1.2,-1.2],[-1.2,-1.2,1.2],[1.2,-1.2,1.2],[1.2,1.2,1.2],[-1.2,1.2,1.2]]},
{name:"Smol Cube",v:[[-.8,-.8,-.8],[.8,-.8,-.8],[.8,.8,-.8],[-.8,.8,-.8],[-.8,-.8,.8],[.8,-.8,.8],[.8,.8,.8],[-.8,.8,.8]]},
{name:"The Monolith",v:[[-.5,-1.5,-.5],[.5,-1.5,-.5],[.5,1.5,-.5],[-.5,1.5,-.5],[-.5,-1.5,.5],[.5,-1.5,.5],[.5,1.5,.5],[-.5,1.5,.5]]},
{name:"Wide Lad",v:[[-1.4,-.5,-.7],[1.4,-.5,-.7],[1.4,.5,-.7],[-1.4,.5,-.7],[-1.4,-.5,.7],[1.4,-.5,.7],[1.4,.5,.7],[-1.4,.5,.7]]},
{name:"The Domino",v:[[-1.1,-.5,-.5],[1.1,-.5,-.5],[1.1,.5,-.5],[-1.1,.5,-.5],[-1.1,-.5,.5],[1.1,-.5,.5],[1.1,.5,.5],[-1.1,.5,.5]]},
{name:"Absolute Unit",v:[[-1.3,-1.1,-1.1],[1.3,-1.1,-1.1],[1.3,1.1,-1.1],[-1.3,1.1,-1.1],[-1.3,-1.1,1.1],[1.3,-1.1,1.1],[1.3,1.1,1.1],[-1.3,1.1,1.1]]},
{name:"Slim Jim",v:[[-.4,-1.3,-.4],[.4,-1.3,-.4],[.4,1.3,-.4],[-.4,1.3,-.4],[-.4,-1.3,.4],[.4,-1.3,.4],[.4,1.3,.4],[-.4,1.3,.4]]},
{name:"The Pancake",v:[[-1.2,-.3,-1.2],[1.2,-.3,-1.2],[1.2,.3,-1.2],[-1.2,.3,-1.2],[-1.2,-.3,1.2],[1.2,-.3,1.2],[1.2,.3,1.2],[-1.2,.3,1.2]]},
{name:"The Slab",v:[[-1.4,-.4,-.9],[1.4,-.4,-.9],[1.4,.4,-.9],[-1.4,.4,-.9],[-1.4,-.4,.9],[1.4,-.4,.9],[1.4,.4,.9],[-1.4,.4,.9]]},
{name:"Stumpy",v:[[-.9,-.6,-.9],[.9,-.6,-.9],[.9,.6,-.9],[-.9,.6,-.9],[-.9,-.6,.9],[.9,-.6,.9],[.9,.6,.9],[-.9,.6,.9]]},
{name:"The Plank",v:[[-1.5,-.3,-.4],[1.5,-.3,-.4],[1.5,.3,-.4],[-1.5,.3,-.4],[-1.5,-.3,.4],[1.5,-.3,.4],[1.5,.3,.4],[-1.5,.3,.4]]},
{name:"Elongated Cube",v:[[-.6,-1.2,-.6],[.6,-1.2,-.6],[.6,1.2,-.6],[-.6,1.2,-.6],[-.6,-1.2,.6],[.6,-1.2,.6],[.6,1.2,.6],[-.6,1.2,.6]]},
{name:"The Loaf",v:[[-1.1,-.7,-.8],[1.1,-.7,-.8],[1.1,.7,-.8],[-1.1,.7,-.8],[-1.1,-.7,.8],[1.1,-.7,.8],[1.1,.7,.8],[-1.1,.7,.8]]},
{name:"Compact Boi",v:[[-.85,-.85,-.85],[.85,-.85,-.85],[.85,.85,-.85],[-.85,.85,-.85],[-.85,-.85,.85],[.85,-.85,.85],[.85,.85,.85],[-.85,.85,.85]]},
{name:"The Skyscraper",v:[[-.5,-1.6,-.5],[.5,-1.6,-.5],[.5,1.6,-.5],[-.5,1.6,-.5],[-.5,-1.6,.5],[.5,-1.6,.5],[.5,1.6,.5],[-.5,1.6,.5]]},
{name:"The Credit Card",v:[[-1.3,-.2,-.8],[1.3,-.2,-.8],[1.3,.2,-.8],[-1.3,.2,-.8],[-1.3,-.2,.8],[1.3,-.2,.8],[1.3,.2,.8],[-1.3,.2,.8]]},
{name:"Beefy Box",v:[[-1.15,-1,- 1],[1.15,-1,-1],[1.15,1,-1],[-1.15,1,-1],[-1.15,-1,1],[1.15,-1,1],[1.15,1,1],[-1.15,1,1]]},
{name:"The Stick",v:[[-.35,-1.4,-.35],[.35,-1.4,-.35],[.35,1.4,-.35],[-.35,1.4,-.35],[-.35,-1.4,.35],[.35,-1.4,.35],[.35,1.4,.35],[-.35,1.4,.35]]},
{name:"The Wafer",v:[[-1.15,-.25,-1],[1.15,-.25,-1],[1.15,.25,-1],[-1.15,.25,-1],[-1.15,-.25,1],[1.15,-.25,1],[1.15,.25,1],[-1.15,.25,1]]},
{name:"Chunky Monkey",v:[[-1.05,-1.05,-1.05],[1.05,-1.05,-1.05],[1.05,1.05,-1.05],[-1.05,1.05,-1.05],[-1.05,-1.05,1.05],[1.05,-1.05,1.05],[1.05,1.05,1.05],[-1.05,1.05,1.05]]},
{name:"The Ruler",v:[[-1.5,-.25,-.35],[1.5,-.25,-.35],[1.5,.25,-.35],[-1.5,.25,-.35],[-1.5,-.25,.35],[1.5,-.25,.35],[1.5,.25,.35],[-1.5,.25,.35]]},
{name:"Perfect Cube",v:[[-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1],[-1,-1,1],[1,-1,1],[1,1,1],[-1,1,1]]},
{name:"Rectangular Prism",v:[[-1.25,-.75,-.75],[1.25,-.75,-.75],[1.25,.75,-.75],[-1.25,.75,-.75],[-1.25,-.75,.75],[1.25,-.75,.75],[1.25,.75,.75],[-1.25,.75,.75]]},
{name:"The Tower",v:[[-.55,-1.45,-.55],[.55,-1.45,-.55],[.55,1.45,-.55],[-.55,1.45,-.55],[-.55,-1.45,.55],[.55,-1.45,.55],[.55,1.45,.55],[-.55,1.45,.55]]},
{name:"The Mattress",v:[[-1.35,-.35,-1.05],[1.35,-.35,-1.05],[1.35,.35,-1.05],[-1.35,.35,-1.05],[-1.35,-.35,1.05],[1.35,-.35,1.05],[1.35,.35,1.05],[-1.35,.35,1.05]]},
{name:"Long Boi",v:[[-1.4,-.55,-.65],[1.4,-.55,-.65],[1.4,.55,-.65],[-1.4,.55,-.65],[-1.4,-.55,.65],[1.4,-.55,.65],[1.4,.55,.65],[-1.4,.55,.65]]},
{name:"The Pillar",v:[[-.6,-1.35,-.6],[.6,-1.35,-.6],[.6,1.35,-.6],[-.6,1.35,-.6],[-.6,-1.35,.6],[.6,-1.35,.6],[.6,1.35,.6],[-.6,1.35,.6]]},
{name:"Wide Box",v:[[-1.45,-.6,-.75],[1.45,-.6,-.75],[1.45,.6,-.75],[-1.45,.6,-.75],[-1.45,-.6,.75],[1.45,-.6,.75],[1.45,.6,.75],[-1.45,.6,.75]]},
{name:"The Square Donut",v:[[-1.1,-.65,-.95],[1.1,-.65,-.95],[1.1,.65,-.95],[-1.1,.65,-.95],[-1.1,-.65,.95],[1.1,-.65,.95],[1.1,.65,.95],[-1.1,.65,.95]]},
{name:"Mega Cube",v:[[-1.25,-1.25,-1.25],[1.25,-1.25,-1.25],[1.25,1.25,-1.25],[-1.25,1.25,-1.25],[-1.25,-1.25,1.25],[1.25,-1.25,1.25],[1.25,1.25,1.25],[-1.25,1.25,1.25]]},
{name:"The Grand Finale Cube",v:[[-1.2,-1.1,-1.15],[1.2,-1.1,-1.15],[1.2,1.1,-1.15],[-1.2,1.1,-1.15],[-1.2,-1.1,1.15],[1.2,-1.1,1.15],[1.2,1.1,1.15],[-1.2,1.1,1.15]]}
];
const U={clamp(v,n,x){return Math.max(n,Math.min(x,v))},normalizeAngle(a){const n=a%C.TWO_PI;return n<0?n+C.TWO_PI:n},angleDifference(a,b,p=C.TWO_PI){const d=((a-b)%p+p)%p;return Math.min(d,p-d)},getSpeedTier(s){const a=Math.abs(s);if(a<C.DEADZONE)return'none';if(a<C.SPEED_SLOW_THRESHOLD)return'slow';if(a<C.SPEED_MEDIUM_THRESHOLD)return'medium';return'fast'}};
class Difficulty{constructor(){this.l=1}setLevel(l){this.l=l}getProgress(){return(this.l-1)/(C.TOTAL_LEVELS-1)}getSpeedMultiplier(){const p=this.getProgress();return C.DIFFICULTY_MIN_SPEED+(C.DIFFICULTY_MAX_SPEED-C.DIFFICULTY_MIN_SPEED)*p}getMatchThreshold(){const p=this.getProgress();return C.DIFFICULTY_MIN_PRECISION+(C.DIFFICULTY_MAX_PRECISION-C.DIFFICULTY_MIN_PRECISION)*p}getTicksPerSecond(t){const m=this.getSpeedMultiplier();let b=0;switch(t){case'slow':b=C.BASE_TICKS_SLOW;break;case'medium':b=C.BASE_TICKS_MEDIUM;break;case'fast':b=C.BASE_TICKS_FAST;break;default:return 0}return b*m}}
class Input{constructor(){this.gp=null;this.connected=false;this.signals={scalar:{yaw:0,pitch:0,roll:0},discrete:{yaw:0,pitch:0,roll:0},speedTier:{yaw:'none',pitch:'none',roll:'none'}};this.btns={a:false,b:false,aPrev:false,bPrev:false}}normalizeAxis(r){const a=Math.abs(r);if(a<C.DEADZONE)return{scalar:0,discrete:0,speedTier:'none'};const n=(a-C.DEADZONE)/(1-C.DEADZONE);const s=r>0?n:-n;const d=Math.round(s*10);const t=U.getSpeedTier(s);return{scalar:s,discrete:d,speedTier:t}}update(){const gps=navigator.getGamepads();this.gp=gps[0]||gps[1]||gps[2]||gps[3];this.connected=!!this.gp;if(!this.gp)return;const lx=this.normalizeAxis(this.gp.axes[0]);const ly=this.normalizeAxis(this.gp.axes[1]);const rx=this.normalizeAxis(this.gp.axes[2]||0);this.signals.scalar.yaw=lx.scalar;this.signals.discrete.yaw=lx.discrete;this.signals.speedTier.yaw=lx.speedTier;this.signals.scalar.pitch=-ly.scalar;this.signals.discrete.pitch=-ly.discrete;this.signals.speedTier.pitch=ly.speedTier;this.signals.scalar.roll=rx.scalar;this.signals.discrete.roll=rx.discrete;this.signals.speedTier.roll=rx.speedTier;this.btns.aPrev=this.btns.a;this.btns.bPrev=this.btns.b;this.btns.a=this.gp.buttons[0]?.pressed||false;this.btns.b=this.gp.buttons[1]?.pressed||false}isButtonPressed(b){return this.btns[b]&&!this.btns[b+'Prev']}getGamepad(){return this.gp}isConnected(){return this.connected}}
class Audio{constructor(){this.ctx=null;this.osc=null;this.gain=null;this.init=false}init(){if(this.init)return;this.ctx=new AudioContext();this.osc=this.ctx.createOscillator();this.osc.type='sine';this.gain=this.ctx.createGain();this.osc.connect(this.gain);this.gain.connect(this.ctx.destination);this.osc.start();this.gain.gain.value=0;this.init=true}updateFeedback(s){if(!this.init)return;this.osc.frequency.value=150+s*400;this.gain.gain.value=Math.pow(s,2)*.15}stopFeedback(){if(!this.init)return;this.gain.gain.value=0}playTick(t){if(!this.init)return;const o=this.ctx.createOscillator();o.type='sine';const g=this.ctx.createGain();let v=.08,f=300;switch(t){case'slow':v=.06;f=250;break;case'medium':v=.08;f=350;break;case'fast':v=.1;f=450;break}g.gain.setValueAtTime(0,this.ctx.currentTime);g.gain.linearRampToValueAtTime(v,this.ctx.currentTime+.005);g.gain.exponentialRampToValueAtTime(.001,this.ctx.currentTime+C.TICK_AUDIO_DURATION);o.frequency.setValueAtTime(f,this.ctx.currentTime);o.connect(g);g.connect(this.ctx.destination);o.start();o.stop(this.ctx.currentTime+C.TICK_AUDIO_DURATION)}playSuccess(){if(!this.init)return;const o=this.ctx.createOscillator();o.type='sine';const g=this.ctx.createGain();g.gain.setValueAtTime(0,this.ctx.currentTime);g.gain.linearRampToValueAtTime(.4,this.ctx.currentTime+.01);g.gain.exponentialRampToValueAtTime(.01,this.ctx.currentTime+.15);const sf=700;o.frequency.setValueAtTime(sf,this.ctx.currentTime);o.frequency.linearRampToValueAtTime(sf*1.6,this.ctx.currentTime+.15);o.connect(g);g.connect(this.ctx.destination);o.start();o.stop(this.ctx.currentTime+.15)}}
class Haptic{constructor(i){this.i=i;this.lastPulse=0}updateFeedback(s){const now=performance.now();const pi=C.PULSE_MAX_INTERVAL-(C.PULSE_MAX_INTERVAL-C.PULSE_MIN_INTERVAL)*s;if(now-this.lastPulse>=pi){this.triggerPulse();this.lastPulse=now}}triggerPulse(){const gp=this.i.getGamepad();if(!gp?.vibrationActuator||gp.vibrationActuator.type!=='dual-rumble')return;gp.vibrationActuator.playEffect('dual-rumble',{duration:C.PULSE_DURATION,weakMagnitude:C.PULSE_INTENSITY*.6,strongMagnitude:C.PULSE_INTENSITY})}stopFeedback(){const gp=this.i.getGamepad();if(!gp?.vibrationActuator||gp.vibrationActuator.type!=='dual-rumble')return;gp.vibrationActuator.reset();this.lastPulse=0}playTick(){const gp=this.i.getGamepad();if(!gp?.vibrationActuator||gp.vibrationActuator.type!=='dual-rumble')return;gp.vibrationActuator.playEffect('dual-rumble',{duration:C.TICK_HAPTIC_DURATION,weakMagnitude:C.TICK_HAPTIC_MAGNITUDE,strongMagnitude:C.TICK_HAPTIC_MAGNITUDE})}}
class TickCtrl{constructor(a,h,d){this.a=a;this.h=h;this.d=d;this.acc=0}update(t,dt,fine=false){if(t==='none'){this.acc=0;return 0}let tps=this.d.getTicksPerSecond(t);if(fine)tps*=C.FINE_CONTROL_MULTIPLIER;this.acc+=dt;const fpt=60/tps;const te=Math.floor(this.acc/fpt);if(te>0){this.acc-=te*fpt;this.a.playTick(t);this.h.playTick()}return te}reset(){this.acc=0}}
class Shape2D{constructor(a,h,d){this.idx=0;this.verts=[];this.name="";this.angle=0;this.target=0;this.leftTick=new TickCtrl(a,h,d);this.rightTick=new TickCtrl(a,h,d)}setForLevel(l){this.idx=(l-1)%SHAPE_2D.length;const s=SHAPE_2D[this.idx];this.verts=s.vertices;this.name=s.name}setRandomTarget(){this.target=Math.random()*C.TWO_PI}update(sig,dt){const lt=this.leftTick.update(sig.speedTier.yaw,dt,false);const rt=this.rightTick.update(sig.speedTier.roll,dt,true);if(lt>0){const dir=sig.scalar.yaw<0?1:-1;this.angle+=dir*lt*C.TICK_ANGLE_2D*C.DEG_TO_RAD}if(rt>0){const dir=sig.scalar.roll<0?1:-1;this.angle+=dir*rt*C.TICK_ANGLE_2D*C.DEG_TO_RAD}this.angle=U.normalizeAngle(this.angle)}getScore(){const diff=U.angleDifference(this.angle,this.target);return 1-diff/C.PI}project(sc,cx,cy){const cos=Math.cos(this.angle),sin=Math.sin(this.angle);return this.verts.map(v=>({x:(v[0]*cos-v[1]*sin)*sc+cx,y:(v[0]*sin+v[1]*cos)*sc+cy}))}projectTarget(sc,cx,cy){const cos=Math.cos(this.target),sin=Math.sin(this.target);return this.verts.map(v=>({x:(v[0]*cos-v[1]*sin)*sc+cx,y:(v[0]*sin+v[1]*cos)*sc+cy}))}reset(){this.angle=0;this.leftTick.reset();this.rightTick.reset()}}
class Shape3D{constructor(a,h,d){this.idx=0;this.name="";this.verts=[];this.faces=[[0,1,2,3],[4,5,6,7],[0,1,5,4],[1,2,6,5],[2,3,7,6],[3,0,4,7]];this.edges=[[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]];this.colors=['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c'];this.euler={x:0,y:0,z:0};this.targetEuler={x:0,y:0,z:0};this.proj=new Array(8).fill(null).map(()=>({x:0,y:0,z:0}));this.tProj=new Array(8).fill(null).map(()=>({x:0,y:0,z:0}));this.pitchTick=new TickCtrl(a,h,d);this.yawTick=new TickCtrl(a,h,d);this.rollTick=new TickCtrl(a,h,d)}setForLevel(l){this.idx=(l-C.MODE_UNLOCK_LEVEL)%SHAPE_3D.length;const s=SHAPE_3D[this.idx];this.name=s.name;this.verts=s.v}setRandomTarget(){this.targetEuler.x=(Math.random()-.5)*C.PI;this.targetEuler.y=Math.random()*C.TWO_PI;this.targetEuler.z=(Math.random()-.5)*C.PI;this.projectEuler(this.targetEuler,this.tProj)}update(sig,dt){const pt=this.pitchTick.update(sig.speedTier.pitch,dt,false);const yt=this.yawTick.update(sig.speedTier.yaw,dt,false);const rt=this.rollTick.update(sig.speedTier.roll,dt,true);if(pt>0){const dir=sig.scalar.pitch>0?1:-1;this.euler.x+=dir*pt*C.TICK_ANGLE_3D*C.DEG_TO_RAD;this.euler.x=U.clamp(this.euler.x,-C.HALF_PI,C.HALF_PI)}if(yt>0){const dir=sig.scalar.yaw<0?1:-1;this.euler.y+=dir*yt*C.TICK_ANGLE_3D*C.DEG_TO_RAD;this.euler.y=U.normalizeAngle(this.euler.y)}if(rt>0){const dir=sig.scalar.roll<0?1:-1;this.euler.z+=dir*rt*C.TICK_ANGLE_3D*C.DEG_TO_RAD;this.euler.z=U.clamp(this.euler.z,-C.HALF_PI,C.HALF_PI)}}getScore(){const dx=U.angleDifference(this.euler.x,this.targetEuler.x,C.PI)/C.HALF_PI;const dy=U.angleDifference(this.euler.y,this.targetEuler.y,C.TWO_PI)/C.PI;const dz=U.angleDifference(this.euler.z,this.targetEuler.z,C.PI)/C.HALF_PI;return 1-(dx+dy+dz)/3}projectEuler(e,out){const cx=Math.cos(e.x),sx=Math.sin(e.x),cy=Math.cos(e.y),sy=Math.sin(e.y),cz=Math.cos(e.z),sz=Math.sin(e.z);const m00=cy*cz,m01=-cy*sz,m02=sy,m10=cx*sz+sx*sy*cz,m11=cx*cz-sx*sy*sz,m12=-sx*cy,m20=sx*sz-cx*sy*cz,m21=sx*cz+cx*sy*sz,m22=cx*cy;for(let i=0;i<8;i++){const v=this.verts[i];out[i]={x:m00*v[0]+m01*v[1]+m02*v[2],y:m10*v[0]+m11*v[1]+m12*v[2],z:m20*v[0]+m21*v[1]+m22*v[2]}}}project(sc,cx,cy){this.projectEuler(this.euler,this.proj);for(let i=0;i<8;i++){this.proj[i].x=this.proj[i].x*sc+cx;this.proj[i].y=this.proj[i].y*sc+cy}return this.proj}projectTarget(sc,cx,cy){for(let i=0;i<8;i++){this.tProj[i].x=this.tProj[i].x*sc+cx;this.tProj[i].y=this.tProj[i].y*sc+cy}return this.tProj}reset(){this.euler={x:0,y:0,z:0};this.pitchTick.reset();this.yawTick.reset();this.rollTick.reset()}}
class Renderer{constructor(c){this.c=c;this.ctx=c.getContext('2d');this.w=0;this.h=0;this.cx=0;this.cy=0;this.sc=0;this.shX=0;this.shY=0;this.resize()}resize(){this.w=this.c.width=window.innerWidth|0;this.h=this.c.height=window.innerHeight|0;this.cx=this.w/2;this.cy=this.h/2;this.sc=Math.floor(Math.min((this.w-80)/4.2,(this.h-200)/4.2))}setShake(x,y){this.shX=x;this.shY=y}clear(){this.ctx.fillStyle='#111';this.ctx.fillRect(0,0,this.w,this.h)}drawShape2D(s,sc){this.ctx.save();this.ctx.translate(this.shX,this.shY);const p=s.project(this.sc,this.cx,this.cy);const t=s.projectTarget(this.sc,this.cx,this.cy);this.ctx.strokeStyle=`rgba(0,255,0,${sc*.3})`;this.ctx.lineWidth=18;this.ctx.lineCap='round';this.ctx.lineJoin='round';this.ctx.beginPath();this.ctx.moveTo(t[0].x,t[0].y);for(let i=1;i<t.length;i++)this.ctx.lineTo(t[i].x,t[i].y);this.ctx.closePath();this.ctx.stroke();this.ctx.strokeStyle='#000';this.ctx.lineWidth=4;this.ctx.beginPath();this.ctx.moveTo(t[0].x,t[0].y);for(let i=1;i<t.length;i++)this.ctx.lineTo(t[i].x,t[i].y);this.ctx.closePath();this.ctx.stroke();this.ctx.beginPath();this.ctx.moveTo(p[0].x,p[0].y);for(let i=1;i<p.length;i++)this.ctx.lineTo(p[i].x,p[i].y);this.ctx.closePath();this.ctx.fillStyle=`hsl(${180+sc*80},70%,${45+sc*25}%)`;this.ctx.fill();this.ctx.strokeStyle='rgba(0,0,0,0.4)';this.ctx.lineWidth=3;this.ctx.stroke();this.ctx.restore()}drawShape3D(s,sc){this.ctx.save();this.ctx.translate(this.shX,this.shY);const p=s.project(this.sc,this.cx-120,this.cy);const fd=s.faces.map((f,i)=>({index:i,z:f.reduce((sum,idx)=>sum+p[idx].z,0)/4}));fd.sort((a,b)=>a.z-b.z);for(let f of fd){const fc=s.faces[f.index];this.ctx.beginPath();this.ctx.moveTo(p[fc[0]].x,p[fc[0]].y);for(let k=1;k<4;k++)this.ctx.lineTo(p[fc[k]].x,p[fc[k]].y);this.ctx.closePath();this.ctx.fillStyle=s.colors[f.index];this.ctx.globalAlpha=.7+sc*.3;this.ctx.fill();this.ctx.globalAlpha=1}this.ctx.strokeStyle='rgba(0,0,0,0.6)';this.ctx.lineWidth=2;this.ctx.lineCap='round';for(let e of s.edges){const p1=p[e[0]],p2=p[e[1]];this.ctx.beginPath();this.ctx.moveTo(p1.x,p1.y);this.ctx.lineTo(p2.x,p2.y);this.ctx.stroke()}this.ctx.restore()}draw3DTarget(s){const bs=Math.min(this.sc*1.2,200);const bx=this.w-bs-40;const by=this.cy-bs/2;this.ctx.strokeStyle='#0f0';this.ctx.lineWidth=4;this.ctx.strokeRect(bx,by,bs,bs);this.ctx.fillStyle='rgba(0,255,0,0.1)';this.ctx.fillRect(bx,by,bs,bs);this.ctx.fillStyle='#0f0';this.ctx.font='bold 18px monospace';this.ctx.textAlign='center';this.ctx.fillText('MATCH THIS',bx+bs/2,by-8);const tp=s.projectTarget(bs*.35,bx+bs/2,by+bs/2);const fd=s.faces.map((f,i)=>({index:i,z:f.reduce((sum,idx)=>sum+tp[idx].z,0)/4}));fd.sort((a,b)=>a.z-b.z);for(let f of fd){const fc=s.faces[f.index];this.ctx.beginPath();this.ctx.moveTo(tp[fc[0]].x,tp[fc[0]].y);for(let k=1;k<4;k++)this.ctx.lineTo(tp[fc[k]].x,tp[fc[k]].y);this.ctx.closePath();this.ctx.fillStyle=s.colors[f.index];this.ctx.globalAlpha=.9;this.ctx.fill();this.ctx.globalAlpha=1}this.ctx.strokeStyle='#000';this.ctx.lineWidth=1.5;for(let e of s.edges){const p1=tp[e[0]],p2=tp[e[1]];this.ctx.beginPath();this.ctx.moveTo(p1.x,p1.y);this.ctx.lineTo(p2.x,p2.y);this.ctx.stroke()}}drawStars(p){if(p===0)return;this.ctx.fillStyle='#FFD700';this.ctx.font='bold 24px monospace';this.ctx.textAlign='right';const sc=Math.min(p,10);const st='★'.repeat(sc);this.ctx.fillText(st,this.w-20,85);if(p>10){this.ctx.font='18px monospace';this.ctx.fillText(`x${p}`,this.w-20,105)}}drawHUD(l,m,sc,sig,d,ts,nm,pr){this.ctx.fillStyle='rgba(0,255,0,0.95)';this.ctx.textAlign='left';this.ctx.font='bold 28px monospace';this.ctx.fillText(`Level ${l}/${C.TOTAL_LEVELS} | ${m.toUpperCase()}`,20,35);if(nm){this.ctx.font='20px monospace';this.ctx.fillStyle='#0ff';this.ctx.fillText(`"${nm}"`,20,65)}this.ctx.fillStyle='rgba(0,255,0,0.95)';this.ctx.fillRect(20,75,sc*(this.w-60),16);this.ctx.strokeStyle='#000';this.ctx.lineWidth=2;this.ctx.strokeRect(20,75,this.w-60,16);this.ctx.fillStyle='#fff';this.ctx.font='bold 32px monospace';this.ctx.textAlign='right';this.ctx.fillText(`SCORE: ${ts}`,this.w-20,50);this.drawStars(pr);const th=d.getMatchThreshold();const sm=d.getSpeedMultiplier();this.ctx.fillStyle='#888';this.ctx.font='18px monospace';this.ctx.textAlign='left';this.ctx.fillText(`Difficulty: ${(sm*100).toFixed(0)}% speed | ${(th*100).toFixed(1)}% match`,20,110)}drawStartScreen(c){this.ctx.fillStyle='#0ff';this.ctx.textAlign='center';this.ctx.font='bold 48px monospace';this.ctx.fillText(c?'Press A to Start!':'Connect Gamepad!',this.cx,this.cy-60);this.ctx.font='28px monospace';this.ctx.fillText('Match shapes with mechanical precision',this.cx,this.cy+20);this.ctx.font='24px monospace';this.ctx.fillText('Left Stick: Rotate | Right Stick: Fine Control',this.cx,this.cy+60);if(c){this.ctx.fillStyle='#0f0';this.ctx.fillText('Gamepad Ready!',this.cx,this.cy+110)}}drawUnlockScreen(){this.ctx.fillStyle='rgba(0,0,0,0.5)';this.ctx.fillRect(0,0,this.w,this.h);this.ctx.fillStyle='#0f0';this.ctx.textAlign='center';this.ctx.font='bold 52px monospace';this.ctx.fillText('3D MODE UNLOCKED!',this.cx,this.cy);this.ctx.font='bold 32px monospace';this.ctx.fillText('Match the colorful cube orientation!',this.cx,this.cy+60)}drawPauseScreen(){this.ctx.fillStyle='rgba(0,0,0,0.8)';this.ctx.fillRect(0,0,this.w,this.h);this.ctx.fillStyle='#fff';this.ctx.textAlign='center';this.ctx.font='bold 56px monospace';this.ctx.fillText('PAUSED',this.cx,this.cy);this.ctx.font='32px monospace';this.ctx.fillText('A: Resume B: Quit',this.cx,this.cy+70)}drawQuitConfirm(){this.ctx.fillStyle='rgba(0,0,0,0.9)';this.ctx.fillRect(0,0,this.w,this.h);this.ctx.fillStyle='#f00';this.ctx.textAlign='center';this.ctx.font='bold 48px monospace';this.ctx.fillText('QUIT GAME?',this.cx,this.cy-60);this.ctx.fillStyle='#fff';this.ctx.font='28px monospace';this.ctx.fillText('ALL PROGRESS WILL BE LOST',this.cx,this.cy);this.ctx.font='32px monospace';this.ctx.fillStyle='#f00';this.ctx.fillText('A: Yes, Quit B: Cancel',this.cx,this.cy+70)}drawWinScreen(l,st,ts,pr){this.ctx.fillStyle='rgba(0,0,0,0.8)';this.ctx.fillRect(0,0,this.w,this.h);this.ctx.fillStyle='#0f0';this.ctx.textAlign='center';this.ctx.font='bold 56px monospace';this.ctx.fillText('GAME COMPLETE!',this.cx,this.cy-100);this.ctx.font='bold 40px monospace';this.ctx.fillText(`Final Score: ${ts}`,this.cx,this.cy-40);this.ctx.font='32px monospace';const tt=((performance.now()-st)/1000)|0;this.ctx.fillText(`Time: ${tt}s`,this.cx,this.cy+10);if(pr<C.MAX_PRESTIGE){this.ctx.fillStyle='#FFD700';this.ctx.font='bold 36px monospace';this.ctx.fillText('GO PRESTIGE?',this.cx,this.cy+70);this.ctx.fillStyle='#fff';this.ctx.font='28px monospace';this.ctx.fillText('(Like Call of Duty, but cooler)',this.cx,this.cy+105);this.ctx.fillText(`Current Prestige: ${pr}/${C.MAX_PRESTIGE}`,this.cx,this.cy+140);this.ctx.font='32px monospace';this.ctx.fillStyle='#0f0';this.ctx.fillText('A: Prestige B: Quit',this.cx,this.cy+185)}else{this.ctx.fillStyle='#FFD700';this.ctx.font='bold 36px monospace';this.ctx.fillText('MAX PRESTIGE ACHIEVED!',this.cx,this.cy+70);this.ctx.fillStyle='#fff';this.ctx.font='28px monospace';this.ctx.fillText('You are a legend!',this.cx,this.cy+110);this.ctx.font='32px monospace';this.ctx.fillText('A: Play Again B: Quit',this.cx,this.cy+160)}}}
class Game{constructor(){this.c=document.getElementById('c');this.i=new Input();this.a=new Audio();this.h=new Haptic(this.i);this.d=new Difficulty();this.r=new Renderer(this.c);this.s2=new Shape2D(this.a,this.h,this.d);this.s3=new Shape3D(this.a,this.h,this.d);this.state='init';this.mode='2d';this.level=1;this.score=0;this.totalScore=0;this.prestige=0;this.levelStartTime=0;this.startTime=0;this.celebFrames=0;this.unlockFrames=0;this.shakeFrames=0;this.lastTime=0;this.dt=0;window.addEventListener('resize',()=>this.r.resize());document.addEventListener('pointerdown',e=>e.preventDefault(),{capture:true});document.addEventListener('keydown',e=>e.preventDefault(),{capture:true});document.body.style.touchAction='none'}init(){this.loop(0)}loop(t){this.dt=Math.min(1,(t-this.lastTime)/16.66);this.lastTime=t;this.i.update();this.updateState();this.render();requestAnimationFrame(t=>this.loop(t))}updateState(){switch(this.state){case'init':if(this.i.isConnected())this.state='start';break;case'start':if(this.i.isButtonPressed('a'))this.startGame();break;case'playing':this.updatePlaying();break;case'paused':if(this.i.isButtonPressed('a'))this.state='playing';else if(this.i.isButtonPressed('b'))this.state='quitconfirm';break;case'quitconfirm':if(this.i.isButtonPressed('a')){this.a.stopFeedback();this.h.stopFeedback();this.fullReset()}else if(this.i.isButtonPressed('b'))this.state='paused';break;case'win':if(this.i.isButtonPressed('a')){if(this.prestige<C.MAX_PRESTIGE){this.prestige++;this.reset()}else this.reset()}else if(this.i.isButtonPressed('b'))this.fullReset();break}}startGame(){this.state='playing';this.startTime=performance.now();this.levelStartTime=performance.now();this.generateTarget();this.c.requestFullscreen?.();this.a.init()}updatePlaying(){if(this.i.isButtonPressed('b')){this.a.stopFeedback();this.h.stopFeedback();this.state='paused';return}if(this.shakeFrames>0){this.shakeFrames--;const i=C.SHAKE_INTENSITY*(this.shakeFrames/C.SHAKE_DURATION);this.r.setShake((Math.random()-.5)*i*2,(Math.random()-.5)*i*2);if(this.shakeFrames===0)this.r.setShake(0,0)}if(this.celebFrames>0){this.celebFrames--;this.unlockFrames=Math.max(0,this.unlockFrames-1);const sp=.5*this.dt;if(this.mode==='2d')this.s2.angle+=sp*C.DEG_TO_RAD;else this.s3.euler.y+=sp*C.DEG_TO_RAD;return}if(this.mode==='2d'){this.s2.update(this.i.signals,this.dt);this.score=this.s2.getScore()}else{this.s3.update(this.i.signals,this.dt);this.score=this.s3.getScore()}this.a.updateFeedback(this.score);this.h.updateFeedback(this.score);const th=this.d.getMatchThreshold();if(this.score>=th&&this.celebFrames===0&&this.shakeFrames===0)this.advanceLevel()}generateTarget(){if(this.mode==='2d'){this.s2.setForLevel(this.level);this.s2.setRandomTarget()}else{this.s3.setForLevel(this.level);this.s3.setRandomTarget()}}calculatePoints(){const te=(performance.now()-this.levelStartTime)/1000;const sb=Math.max(0,C.SPEED_BONUS_MAX-Math.floor(te/2));const pb=this.score>=.995?C.PRECISION_BONUS:0;return C.BASE_POINTS+sb+pb}advanceLevel(){const pts=this.calculatePoints();this.totalScore+=pts;this.a.playSuccess();this.level++;this.d.setLevel(this.level);this.celebFrames=C.CELEBRATE_FRAMES;this.shakeFrames=C.SHAKE_DURATION;this.levelStartTime=performance.now();if(this.level===C.MODE_UNLOCK_LEVEL){this.mode='3d';this.s3.reset();this.unlockFrames=C.UNLOCK_FRAMES;this.celebFrames=60}if(this.level>C.TOTAL_LEVELS){this.state='win';return}this.generateTarget()}reset(){this.state='start';this.mode='2d';this.level=1;this.d.setLevel(1);this.score=0;this.totalScore=0;this.celebFrames=0;this.unlockFrames=0;this.shakeFrames=0;this.s2.reset();this.s3.reset();this.r.setShake(0,0)}fullReset(){this.prestige=0;this.reset()}render(){this.r.clear();switch(this.state){case'init':case'start':this.r.drawStartScreen(this.i.isConnected());break;case'playing':if(this.mode==='2d'){this.r.drawShape2D(this.s2,this.score)}else{this.r.drawShape3D(this.s3,this.score);this.r.draw3DTarget(this.s3)}this.r.drawHUD(this.level,this.mode,this.score,this.i.signals,this.d,this.totalScore,this.mode==='2d'?this.s2.name:this.s3.name,this.prestige);if(this.unlockFrames>0)this.r.drawUnlockScreen();break;case'paused':if(this.mode==='2d')this.r.drawShape2D(this.s2,0);else{this.r.drawShape3D(this.s3,0);this.r.draw3DTarget(this.s3)}this.r.drawPauseScreen();break;case'quitconfirm':this.r.drawQuitConfirm();break;case'win':this.r.drawWinScreen(this.level,this.startTime,this.totalScore,this.prestige);break}}}
const game=new Game();game.init();
</script>
</body>
</html>