-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpattern-lab.html
More file actions
178 lines (165 loc) · 24.3 KB
/
Copy pathpattern-lab.html
File metadata and controls
178 lines (165 loc) · 24.3 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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover" />
<meta name="theme-color" content="#091116" />
<title>招式试验场|七种空战轨迹</title>
<style>
:root{color-scheme:dark;--bg:#091116;--panel:#151d23;--line:#34434d;--ink:#edf4f6;--muted:#9fb0b9;--cyan:#48dce8;--gold:#ffd166;--red:#ff565c;--green:#62e6b1}
*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:var(--bg);color:var(--ink);font-family:ui-sans-serif,system-ui,-apple-system,"PingFang SC",sans-serif;overscroll-behavior:none}
.app{min-height:100svh;display:grid;grid-template-columns:minmax(300px,540px) minmax(330px,470px);gap:18px;justify-content:center;align-items:start;padding:16px}
.game{position:relative;width:min(540px,calc((100svh - 32px)*.5625),calc(100vw - 32px));aspect-ratio:9/16;margin:auto;border:1px solid #4a5a64;background:#050b0f;overflow:hidden;touch-action:none;box-shadow:0 18px 60px #000b}
canvas{display:block;width:100%;height:100%;touch-action:none;cursor:none}
.panel{max-height:calc(100svh - 32px);overflow:auto;padding:16px;background:var(--panel);border:1px solid var(--line)}
h1{margin:0 0 8px;font-size:24px}h2{margin:18px 0 8px;color:var(--gold);font-size:16px}p,li{color:var(--muted);font-size:14px;line-height:1.55}.question{padding:10px 12px;background:#0e161b;border-left:4px solid var(--cyan);color:var(--ink)}
.tabs{display:grid;grid-template-columns:repeat(2,1fr);gap:7px}.tabs button,.actions button,.feedback button,select{min-height:40px;border:1px solid #546570;background:#222e36;color:var(--ink);font:inherit}.tabs button.active{border-color:var(--cyan);background:#153640;color:#bffaff}.tabs button small{display:block;color:#91a5af;font-size:10px}
.rule{min-height:70px;margin-top:10px;padding:10px;background:#0e161b;border:1px solid #2b3c45;color:#d9e4e8;font-size:13px;line-height:1.5}.settings{display:flex;align-items:center;justify-content:space-between;gap:12px}.actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.actions .primary{background:#963338;border-color:#e06367;font-weight:700}
.state{display:grid;grid-template-columns:1fr 1fr;gap:7px}.metric{padding:8px;background:#0e161b;border:1px solid #2b3c45;color:var(--muted);font-size:11px}.metric b{display:block;margin-top:2px;color:var(--ink);font-size:16px}
.feedback{display:grid;grid-template-columns:1fr;gap:7px}.feedback button.on{background:#184737;border-color:var(--green);color:#caffeb}.summary{width:100%;border-collapse:collapse;font-size:11px}.summary th,.summary td{padding:6px 4px;border-bottom:1px solid #2b3c45;text-align:center}.summary th:first-child,.summary td:first-child{text-align:left}.muted{color:var(--muted)}
.mobile-panel{display:none;position:absolute;right:8px;bottom:8px;z-index:5;border:1px solid #61727c;border-radius:99px;background:#0c151bdc;color:#fff;padding:6px 10px}
@media(max-width:900px){.app{display:block;padding:0}.game{width:min(100%,calc(100svh*.5625));border:0}.panel{max-height:none;border-width:1px 0 0}.mobile-panel{display:block}}
</style>
</head>
<body>
<main class="app">
<section class="game" aria-label="空战招式试验画布">
<canvas id="arena" width="540" height="960"></canvas>
<button id="jump" class="mobile-panel">切换招式</button>
</section>
<aside id="panel" class="panel">
<h1>招式试验场</h1>
<p class="question"><strong>只回答一个问题:</strong>哪种运动规律能让人第一眼看懂、中弹时知道原因、第三次明显进步,而且还想再来一次?每局15秒,第一次中弹即结束。</p>
<h2>七种候选招式</h2>
<div id="tabs" class="tabs"></div>
<div id="rule" class="rule"></div>
<h2>试验设置</h2>
<div class="settings"><label for="difficulty">密度</label><select id="difficulty"><option value="normal">普通:先学会规则</option><option value="hard">困难:组合更快</option></select></div>
<div class="actions"><button id="start" class="primary">开始当前招式</button><button id="next">下一种招式</button></div>
<h2>当前状态</h2>
<div id="state" class="state"></div>
<h2>试玩后点三下</h2>
<div class="feedback">
<button data-feedback="clear">① 规则一眼懂</button>
<button data-feedback="fair">② 死得明白</button>
<button data-feedback="retry">③ 还想再来</button>
</div>
<h2>本次比较</h2>
<table class="summary"><thead><tr><th>招式</th><th>次数</th><th>首次</th><th>最佳</th><th>通关</th><th>反馈</th></tr></thead><tbody id="summary"></tbody></table>
<p class="muted">手机按住拖动;电脑可用鼠标或WASD。试验场不保存数据、不计热度,也不会改动主游戏。</p>
</aside>
</main>
<script>
(() => {
'use strict';
const canvas=document.querySelector('#arena'),ctx=canvas.getContext('2d'),W=canvas.width,H=canvas.height,TAU=Math.PI*2,TRIAL=15;
const clamp=(n,a,b)=>Math.max(a,Math.min(b,n)),lerp=(a,b,t)=>a+(b-a)*t;
const patternOrder=['homing','ring','orbit','boomerang','copy','freeze','walls'];
const patterns={
homing:{name:'律师函锁定',short:'锁点后追踪',color:'#ff6b70',rule:'红色准星先记录你当时的位置;律师函只向那个快照飞,不会无限拐弯。看到准星后诱导,再横移。'},
ring:{name:'旋转圆环',short:'转动缺口',color:'#48dce8',rule:'Boss发出不断扩张的圆环;每环有两枚宽的缺口,缺口逐轮旋转。先找缺口,再决定是否穿环。'},
orbit:{name:'冠冕轨道',short:'环绕后放射',color:'#ffd166',rule:'皇冠先绕Boss旋转,金色闪光后沿当时角度向外释放。观察轨道,不要只盯当前空位。'},
boomerang:{name:'潮退回旋',short:'下落后返回',color:'#ff9f43',rule:'障碍先向下压,触及各自转向线后反向上升。第一次躲过不代表结束,避免回到原路线。'},
copy:{name:'新概念复制',short:'延迟复刻',color:'#bd78ff',rule:'第一排纸张落下后,第二排会复制原布局并整体错位。记住第一排缺口,再对付偏移后的副本。'},
freeze:{name:'冻币爆裂',short:'停住后环爆',color:'#91d9ff',rule:'冰币先在场内冻结并扩大警戒圈,随后向八个方向碎裂。远离重叠警戒圈,不要等爆裂才移动。'},
walls:{name:'衣柜合围',short:'移动窄门',color:'#f47cc3',rule:'左右衣柜组成向下移动的墙,只留一个窄门;下一道门会提前显示。先对准门心,别在最后一刻横穿。'}
};
function mulberry32(seed){return()=>{seed|=0;seed=seed+0x6D2B79F5|0;let t=Math.imul(seed^seed>>>15,1|seed);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
const results=Object.fromEntries(patternOrder.map(k=>[k,{attempts:0,first:null,best:0,clears:0,last:'—',feedback:{clear:false,fair:false,retry:false}}]));
let selected='homing',difficulty='normal',rng=mulberry32(1),last=performance.now(),keys=new Set(),pointer={active:false,id:null,x:W/2,y:H*.8};
let s=makeState('ready');
function makeState(mode='ready'){
return{mode,time:TRIAL,elapsed:0,player:{x:W/2,y:H*.8,tx:W/2,ty:H*.8,r:8,trail:[]},objects:[],warnings:[],particles:[],spawnT:.6,step:0,phase:0,hitCause:'',message:'',messageT:0,stars:Array.from({length:55},(_,i)=>({x:(i*97)%W,y:(i*173)%H,v:70+(i%7)*14}))};
}
function seedFor(){return 1701+patternOrder.indexOf(selected)*977+(difficulty==='hard'?4999:0);}
function startTrial(){difficulty=document.querySelector('#difficulty').value;rng=mulberry32(seedFor());s=makeState('play');results[selected].attempts++;renderPanel();}
function endTrial(clear,cause=''){
if(s.mode!=='play')return;s.mode=clear?'clear':'hit';s.hitCause=cause;const survived=clear?TRIAL:Math.min(TRIAL,s.elapsed),r=results[selected];if(r.first===null)r.first=survived;r.best=Math.max(r.best,survived);if(clear)r.clears++;r.last=clear?'15秒通关':cause;renderPanel();
}
function selectPattern(key){selected=key;s=makeState('ready');rng=mulberry32(seedFor());renderPanel();}
function hard(){return difficulty==='hard';}
function rand(a,b){return a+rng()*(b-a);}
function addObject(o){s.objects.push({dead:false,age:0,...o});}
function warn(kind,data,duration,spawn){s.warnings.push({kind,data,t:duration,max:duration,spawn,dead:false});}
function burst(x,y,color,n=18){for(let i=0;i<n;i++){const a=rng()*TAU,v=rand(70,220);s.particles.push({x,y,vx:Math.cos(a)*v,vy:Math.sin(a)*v,t:rand(.25,.7),color});}}
function spawnHoming(){
const target={x:s.player.x,y:s.player.y};warn('target',target,.62,()=>{const dx=target.x-W/2,dy=target.y-145,d=Math.hypot(dx,dy)||1;addObject({kind:'missile',x:W/2,y:145,vx:dx/d*(hard()?470:400),vy:dy/d*(hard()?470:400),r:11,color:patterns.homing.color,cause:'被锁定律师函追上'});});
}
function spawnRing(){
const count=hard()?20:16,gap=hard()?2:3,angle=s.phase*.58+Math.atan2(s.player.y-160,s.player.x-W/2);s.phase++;warn('ring',{angle,count,gap},.48,()=>{const speed=hard()?250:215;for(let i=0;i<count;i++){const d=i-count/2;if(Math.abs(d)<gap/2)continue;const a=angle+d*TAU/count;addObject({kind:'bullet',x:W/2,y:160,vx:Math.cos(a)*speed,vy:Math.sin(a)*speed,r:7,color:patterns.ring.color,cause:'撞上旋转圆环'});}});
}
function spawnOrbit(){
const count=hard()?10:8,omega=(s.phase++%2?1:-1)*(hard()?2.7:2.15);for(let i=0;i<count;i++)addObject({kind:'orbit',x:W/2,y:160,cx:W/2,cy:160,angle:i*TAU/count,radius:62,omega,release:hard()?1.05:1.3,r:9,color:patterns.orbit.color,cause:'被轨道皇冠放射击中'});
}
function spawnBoomerang(){
const count=hard()?6:5,gap=(s.step++*2+1)%count;for(let i=0;i<count;i++){if(i===gap)continue;const x=50+i*(440/(count-1));addObject({kind:'boomerang',x,y:145-i%2*24,vx:(i-(count-1)/2)*10,vy:hard()?315:275,turnY:hard()?rand(590,710):rand(650,760),turned:false,r:12,color:patterns.boomerang.color,cause:'被回头的潮退障碍击中'});}
}
function spawnCopy(){
const lanes=[55,150,245,340,435],gap=s.step++%lanes.length,shift=s.step%2?42:-42,first=lanes.filter((_,i)=>i!==gap);for(const x of first)addObject({kind:'copy',x,y:145,vx:0,vy:hard()?300:255,r:12,color:'#8c6ad9',cause:'撞上原稿'});warn('copy',{xs:first.map(x=>clamp(x+shift,28,W-28)),shift},hard()?.72:.9,()=>{for(const x of first.map(x=>clamp(x+shift,28,W-28)))addObject({kind:'copy',x,y:145,vx:0,vy:hard()?325:275,r:12,color:patterns.copy.color,cause:'被延迟复制件击中'});});
}
function spawnFreeze(){
const count=hard()?4:3;for(let i=0;i<count;i++){const x=75+(i+.5)*(390/count)+rand(-24,24),y=280+(i%2)*170+rand(-35,35);warn('mine',{x,y,r:hard()?80:70},hard()?.58:.78,()=>{const n=hard()?10:8,speed=hard()?285:245;burst(x,y,patterns.freeze.color,20);for(let j=0;j<n;j++){const a=j*TAU/n+(s.phase%2?TAU/n/2:0);addObject({kind:'shard',x,y,vx:Math.cos(a)*speed,vy:Math.sin(a)*speed,r:7,color:patterns.freeze.color,cause:'进入冻币爆裂范围'});}s.phase++;});}
}
function spawnWalls(){
const centers=hard()?[120,420,205,350,155,390]:[145,390,250,330,180],center=centers[s.step++%centers.length],gapW=hard()?112:150;warn('wall',{center,gapW},.62,()=>{const leftW=Math.max(0,center-gapW/2),rightX=center+gapW/2;addObject({kind:'rect',x:0,y:170,w:leftW,h:56,vy:hard()?290:245,color:patterns.walls.color,cause:'被合拢衣柜夹住'});addObject({kind:'rect',x:rightX,y:170,w:W-rightX,h:56,vy:hard()?290:245,color:patterns.walls.color,cause:'被合拢衣柜夹住'});});
}
function updatePattern(dt){
s.spawnT-=dt;if(s.spawnT>0)return;
if(selected==='homing'){spawnHoming();s.spawnT=hard()?1.05:1.4;}
else if(selected==='ring'){spawnRing();s.spawnT=hard()?1.05:1.35;}
else if(selected==='orbit'){spawnOrbit();s.spawnT=hard()?2.15:2.7;}
else if(selected==='boomerang'){spawnBoomerang();s.spawnT=hard()?1.35:1.75;}
else if(selected==='copy'){spawnCopy();s.spawnT=hard()?1.8:2.25;}
else if(selected==='freeze'){spawnFreeze();s.spawnT=hard()?2.05:2.65;}
else{spawnWalls();s.spawnT=hard()?1.55:2.05;}
}
function circleHit(p,o){return Math.hypot(p.x-o.x,p.y-o.y)<p.r+o.r;}
function rectHit(p,o){const nx=clamp(p.x,o.x,o.x+o.w),ny=clamp(p.y,o.y,o.y+o.h);return Math.hypot(p.x-nx,p.y-ny)<p.r;}
function updateObjects(dt){
for(const w of s.warnings){w.t-=dt;if(w.t<=0&&!w.dead){w.dead=true;w.spawn();}}s.warnings=s.warnings.filter(w=>!w.dead);
for(const o of s.objects){o.age+=dt;if(o.kind==='orbit'&&o.age<o.release){o.angle+=o.omega*dt;o.x=o.cx+Math.cos(o.angle)*o.radius;o.y=o.cy+Math.sin(o.angle)*o.radius;}else if(o.kind==='orbit'){const a=o.angle;o.kind='bullet';o.vx=Math.cos(a)*(hard()?300:250);o.vy=Math.sin(a)*(hard()?300:250);o.color='#ffb94e';burst(o.x,o.y,o.color,5);}else if(o.kind==='boomerang'){o.x+=o.vx*dt;o.y+=o.vy*dt;if(!o.turned&&o.y>=o.turnY){o.turned=true;o.vy=-(hard()?365:325);o.vx*=-1.7;o.color='#ffd166';burst(o.x,o.y,o.color,6);}}else if(o.kind==='rect')o.y+=o.vy*dt;else{o.x+=o.vx*dt;o.y+=o.vy*dt;}
if((o.kind==='rect'?rectHit(s.player,o):circleHit(s.player,o))){endTrial(false,o.cause);return;}if(o.kind==='rect'){if(o.y>H+80)o.dead=true;}else if(o.x<-90||o.x>W+90||o.y<-100||o.y>H+100)o.dead=true;
}s.objects=s.objects.filter(o=>!o.dead);
}
function update(dt){
for(const star of s.stars){star.y+=star.v*dt;if(star.y>H){star.y=-4;star.x=rand(0,W);}}
if(s.mode!=='play'){updateFx(dt);return;}s.elapsed+=dt;s.time=Math.max(0,TRIAL-s.elapsed);if(s.time<=0){endTrial(true);return;}
let dx=0,dy=0;if(keys.has('ArrowLeft')||keys.has('KeyA'))dx--;if(keys.has('ArrowRight')||keys.has('KeyD'))dx++;if(keys.has('ArrowUp')||keys.has('KeyW'))dy--;if(keys.has('ArrowDown')||keys.has('KeyS'))dy++;if(dx||dy){s.player.tx=clamp(s.player.tx+dx*380*dt,18,W-18);s.player.ty=clamp(s.player.ty+dy*380*dt,115,H-28);}
s.player.x=lerp(s.player.x,s.player.tx,1-Math.pow(.001,dt));s.player.y=lerp(s.player.y,s.player.ty,1-Math.pow(.001,dt));s.player.trail.unshift({x:s.player.x,y:s.player.y,t:.25});if(s.player.trail.length>13)s.player.trail.pop();for(const t of s.player.trail)t.t-=dt;
updatePattern(dt);updateObjects(dt);updateFx(dt);renderState();
}
function updateFx(dt){for(const p of s.particles){p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=.97;p.vy*=.97;p.t-=dt;}s.particles=s.particles.filter(p=>p.t>0);s.messageT=Math.max(0,s.messageT-dt);}
function drawWarning(w){
const a=clamp(w.t/w.max,0,1),pulse=.55+.35*Math.sin(performance.now()/55);ctx.save();ctx.globalAlpha=.45+.4*(1-a);
if(w.kind==='target'){const q=w.data;ctx.strokeStyle='#ff6b70';ctx.lineWidth=4;ctx.beginPath();ctx.arc(q.x,q.y,18+20*a,0,TAU);ctx.stroke();ctx.beginPath();ctx.moveTo(q.x-30,q.y);ctx.lineTo(q.x+30,q.y);ctx.moveTo(q.x,q.y-30);ctx.lineTo(q.x,q.y+30);ctx.stroke();ctx.fillStyle='#ffd1d2';ctx.font='bold 12px sans-serif';ctx.textAlign='center';ctx.fillText('锁定快照',q.x,q.y-38);}
else if(w.kind==='ring'){const q=w.data;ctx.strokeStyle=patterns.ring.color;ctx.lineWidth=5;ctx.setLineDash([10,7]);ctx.beginPath();ctx.arc(W/2,160,45+55*(1-a),q.angle+q.gap*TAU/q.count/2,q.angle+TAU-q.gap*TAU/q.count/2);ctx.stroke();ctx.setLineDash([]);ctx.fillStyle='#bdfaff';ctx.font='bold 12px sans-serif';ctx.textAlign='center';ctx.fillText('缺口会旋转',W/2,235);}
else if(w.kind==='copy'){ctx.fillStyle='#bd78ff';ctx.font='bold 13px sans-serif';ctx.textAlign='center';for(const x of w.data.xs){ctx.strokeStyle='#bd78ff';ctx.strokeRect(x-13,142,26,26);ctx.fillText('复',x,161);}ctx.fillText(`延迟副本 ${w.data.shift>0?'→':'←'}`,W/2,205);}
else if(w.kind==='mine'){const q=w.data;ctx.strokeStyle=patterns.freeze.color;ctx.lineWidth=5;ctx.globalAlpha=pulse;ctx.beginPath();ctx.arc(q.x,q.y,q.r*(1-a*.55),0,TAU);ctx.stroke();ctx.fillStyle='#d8f6ff';ctx.font='bold 12px sans-serif';ctx.textAlign='center';ctx.fillText('即将爆裂',q.x,q.y+5);}
else if(w.kind==='wall'){const q=w.data;ctx.fillStyle='#f47cc344';ctx.fillRect(0,170,q.center-q.gapW/2,56);ctx.fillRect(q.center+q.gapW/2,170,W-q.center-q.gapW/2,56);ctx.strokeStyle='#ffb5e1';ctx.lineWidth=4;ctx.strokeRect(q.center-q.gapW/2,166,q.gapW,64);ctx.fillStyle='#ffd4eb';ctx.font='bold 12px sans-serif';ctx.textAlign='center';ctx.fillText('门',q.center,202);}
ctx.restore();
}
function drawObject(o){ctx.save();ctx.translate(o.x,o.y);ctx.fillStyle=o.color;ctx.strokeStyle='#fff';ctx.lineWidth=2;if(o.kind==='rect'){ctx.translate(-o.x,-o.y);ctx.fillRect(o.x,o.y,o.w,o.h);ctx.strokeRect(o.x,o.y,o.w,o.h);ctx.fillStyle='#48152f';ctx.font='bold 15px sans-serif';ctx.textAlign='center';ctx.fillText('衣柜',o.x+o.w/2,o.y+34);}else if(o.kind==='missile'){const a=Math.atan2(o.vy,o.vx);ctx.rotate(a+Math.PI/2);ctx.beginPath();ctx.moveTo(0,-16);ctx.lineTo(10,11);ctx.lineTo(0,7);ctx.lineTo(-10,11);ctx.closePath();ctx.fill();ctx.stroke();}else if(o.kind==='boomerang'){ctx.beginPath();ctx.arc(0,0,o.r,0,TAU);ctx.fill();ctx.stroke();ctx.fillStyle='#5d3611';ctx.font='bold 16px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('↩',0,1);}else if(o.kind==='copy'){ctx.rotate(o.age*2);ctx.fillRect(-o.r,-o.r,o.r*2,o.r*2);ctx.strokeRect(-o.r,-o.r,o.r*2,o.r*2);ctx.rotate(-o.age*2);ctx.fillStyle='#25143a';ctx.font='bold 12px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('复',0,1);}else if(o.kind==='shard'){ctx.rotate(Math.atan2(o.vy,o.vx));ctx.beginPath();ctx.moveTo(14,0);ctx.lineTo(-7,-6);ctx.lineTo(-7,6);ctx.closePath();ctx.fill();ctx.stroke();}else{ctx.beginPath();ctx.arc(0,0,o.r,0,TAU);ctx.fill();ctx.stroke();}ctx.restore();}
function draw(){
const meta=patterns[selected],g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'#10212a');g.addColorStop(1,'#04080b');ctx.fillStyle=g;ctx.fillRect(0,0,W,H);ctx.fillStyle='#d9edf3';for(const star of s.stars){ctx.globalAlpha=.22;ctx.fillRect(star.x,star.y,2,5);}ctx.globalAlpha=1;ctx.strokeStyle='#1d3540';for(let y=(s.elapsed*120)%82;y<H;y+=82){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y+22);ctx.stroke();}
ctx.fillStyle='#071016e8';ctx.fillRect(0,0,W,102);ctx.textAlign='left';ctx.fillStyle=meta.color;ctx.font='bold 21px sans-serif';ctx.fillText(meta.name,16,31);ctx.fillStyle='#b8c8cf';ctx.font='13px sans-serif';ctx.fillText(meta.short,16,58);ctx.fillText(hard()?'困难密度':'普通密度',16,83);ctx.textAlign='right';ctx.fillStyle='#fff';ctx.font='bold 25px ui-monospace,monospace';ctx.fillText(`${s.time.toFixed(1)}s`,W-16,34);ctx.fillStyle='#aab8bf';ctx.font='13px sans-serif';ctx.fillText(`第 ${results[selected].attempts||1} 次`,W-16,62);ctx.fillText('中弹即结束',W-16,84);
ctx.save();ctx.translate(W/2,145);ctx.fillStyle='#ecf1f3';ctx.strokeStyle=meta.color;ctx.lineWidth=5;ctx.beginPath();ctx.moveTo(0,-30);ctx.lineTo(30,21);ctx.lineTo(0,12);ctx.lineTo(-30,21);ctx.closePath();ctx.fill();ctx.stroke();ctx.fillStyle='#1b252a';ctx.font='bold 12px sans-serif';ctx.textAlign='center';ctx.fillText('BOSS',0,3);ctx.restore();
for(const w of s.warnings)drawWarning(w);for(const o of s.objects)drawObject(o);for(const p of s.particles){ctx.globalAlpha=clamp(p.t*2,0,1);ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,3,0,TAU);ctx.fill();}ctx.globalAlpha=1;
for(const t of s.player.trail){ctx.globalAlpha=clamp(t.t*2,0,.45);ctx.fillStyle='#62e6b1';ctx.beginPath();ctx.arc(t.x,t.y,5,0,TAU);ctx.fill();}ctx.globalAlpha=1;ctx.save();ctx.translate(s.player.x,s.player.y);ctx.fillStyle='#62e6b1';ctx.strokeStyle='#dffff2';ctx.lineWidth=3;ctx.beginPath();ctx.moveTo(0,-21);ctx.lineTo(16,16);ctx.lineTo(0,9);ctx.lineTo(-16,16);ctx.closePath();ctx.fill();ctx.stroke();ctx.fillStyle='#14372e';ctx.font='bold 12px sans-serif';ctx.textAlign='center';ctx.fillText('试',0,6);ctx.globalAlpha=.45;ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.beginPath();ctx.arc(0,0,s.player.r,0,TAU);ctx.stroke();ctx.restore();
if(s.mode!=='play'){ctx.fillStyle='#000d';ctx.fillRect(0,0,W,H);ctx.textAlign='center';ctx.fillStyle=meta.color;ctx.font='bold 31px sans-serif';ctx.fillText(s.mode==='ready'?meta.name:s.mode==='clear'?'试炼通过':'试炼结束',W/2,260);ctx.fillStyle='#e7eef1';ctx.font='17px sans-serif';if(s.mode==='ready'){wrapText(meta.rule,W/2,310,410,27);ctx.fillStyle='#9b3439';ctx.fillRect(W/2-120,445,240,58);ctx.fillStyle='#fff';ctx.font='bold 21px sans-serif';ctx.fillText('点击开始',W/2,482);}else{ctx.fillStyle=s.mode==='clear'?'#77f2d5':'#ff8589';ctx.font='bold 22px sans-serif';ctx.fillText(s.mode==='clear'?'15秒未中弹':s.hitCause,W/2,315);ctx.fillStyle='#fff';ctx.font='18px sans-serif';ctx.fillText(`存活 ${Math.min(TRIAL,s.elapsed).toFixed(1)} 秒`,W/2,354);ctx.fillStyle='#9b3439';ctx.fillRect(W/2-120,420,240,58);ctx.fillStyle='#fff';ctx.font='bold 21px sans-serif';ctx.fillText('再试一次',W/2,457);}}
}
function wrapText(text,x,y,maxWidth,lineHeight){const chars=[...text];let line='';for(const ch of chars){const test=line+ch;if(ctx.measureText(test).width>maxWidth&&line){ctx.fillText(line,x,y);line=ch;y+=lineHeight;}else line=test;}if(line)ctx.fillText(line,x,y);}
function renderTabs(){document.querySelector('#tabs').innerHTML=patternOrder.map(k=>`<button data-pattern="${k}" class="${k===selected?'active':''}">${patterns[k].name}<small>${patterns[k].short}</small></button>`).join('');document.querySelectorAll('[data-pattern]').forEach(b=>b.addEventListener('click',()=>selectPattern(b.dataset.pattern)));}
function renderState(){const r=results[selected],entries=[['阶段',s.mode==='play'?'试炼中':s.mode==='ready'?'待开始':s.mode==='clear'?'已通过':'已中弹'],['剩余',`${s.time.toFixed(1)}秒`],['屏内威胁',s.objects.length],['预警',s.warnings.length],['首次',r.first===null?'—':`${r.first.toFixed(1)}秒`],['最佳',r.best?`${r.best.toFixed(1)}秒`:'—']];document.querySelector('#state').innerHTML=entries.map(([k,v])=>`<div class="metric">${k}<b>${v}</b></div>`).join('');}
function renderSummary(){document.querySelector('#summary').innerHTML=patternOrder.map(k=>{const r=results[k],marks=[r.feedback.clear?'懂':'',r.feedback.fair?'明':'',r.feedback.retry?'想':''].filter(Boolean).join('/')||'—';return`<tr><td>${patterns[k].name}</td><td>${r.attempts}</td><td>${r.first===null?'—':r.first.toFixed(1)}</td><td>${r.best?r.best.toFixed(1):'—'}</td><td>${r.clears}</td><td>${marks}</td></tr>`;}).join('');}
function renderPanel(){renderTabs();document.querySelector('#rule').innerHTML=`<strong style="color:${patterns[selected].color}">${patterns[selected].name}</strong><br>${patterns[selected].rule}`;document.querySelectorAll('[data-feedback]').forEach(b=>b.classList.toggle('on',results[selected].feedback[b.dataset.feedback]));renderState();renderSummary();}
function loop(now){const dt=Math.min(.033,(now-last)/1000||0);last=now;update(dt);draw();requestAnimationFrame(loop);}requestAnimationFrame(loop);renderPanel();
function point(e){const r=canvas.getBoundingClientRect();return{x:(e.clientX-r.left)*W/r.width,y:(e.clientY-r.top)*H/r.height};}
canvas.addEventListener('pointerdown',e=>{e.preventDefault();if(s.mode!=='play'){startTrial();return;}const p=point(e);pointer.active=true;pointer.id=e.pointerId;pointer.x=p.x;pointer.y=p.y;canvas.setPointerCapture?.(e.pointerId);});
canvas.addEventListener('pointermove',e=>{if(!pointer.active&&e.pointerType!=='mouse')return;const p=point(e),offset=e.pointerType==='touch'?58:0;pointer.x=p.x;pointer.y=p.y;s.player.tx=clamp(p.x,18,W-18);s.player.ty=clamp(p.y-offset,115,H-28);});
const release=e=>{pointer.active=false;pointer.id=null;try{canvas.releasePointerCapture?.(e.pointerId);}catch{}};canvas.addEventListener('pointerup',release);canvas.addEventListener('pointercancel',release);
window.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','KeyA','KeyD','KeyW','KeyS','Space','Enter'].includes(e.code))e.preventDefault();keys.add(e.code);if((e.code==='Space'||e.code==='Enter')&&!e.repeat&&s.mode!=='play')startTrial();});window.addEventListener('keyup',e=>keys.delete(e.code));
document.querySelector('#difficulty').addEventListener('change',e=>{difficulty=e.target.value;s=makeState('ready');renderPanel();});document.querySelector('#start').addEventListener('click',startTrial);document.querySelector('#next').addEventListener('click',()=>selectPattern(patternOrder[(patternOrder.indexOf(selected)+1)%patternOrder.length]));document.querySelector('#jump').addEventListener('click',()=>document.querySelector('#panel').scrollIntoView({behavior:'smooth'}));document.querySelectorAll('[data-feedback]').forEach(b=>b.addEventListener('click',()=>{const f=b.dataset.feedback;results[selected].feedback[f]=!results[selected].feedback[f];renderPanel();}));
})();
</script>
</body>
</html>