Repository navigation
Expand file tree
/
Copy pathfighter.test.mjs
More file actions
31 lines (26 loc) · 7.69 KB
/
Copy pathfighter.test.mjs
File metadata and controls
31 lines (26 loc) · 7.69 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
import {test} from 'node:test';import assert from 'node:assert/strict';import {createFighter,fighters} from './fighter.mjs';
const ctx=new Proxy({globalAlpha:1,measureText:t=>({width:t.length*10})},{get:(o,k)=>k in o?o[k]:(()=>{})});
test('six fighters include three women, two men and a robot; cycling preserves all choices',()=>{assert.deepEqual(fighters.map(f=>f.gender),['man','man','woman','woman','robot','woman']);const f=createFighter();assert.deepEqual(Array.from({length:6},()=>f.cycle().name),[...fighters.slice(1),fighters[0]].map(x=>x.name))});
test('bass onsets create low/mid/high targets; targets are struck or evaded while movement stays within the patrol area',()=>{const f=createFighter(),moves=new Set();let state;for(let i=0;i<500;i++){const loud=i<300&&i%25<4;state=f.draw(ctx,800,400,i*40,Array(32).fill(loud?.75:.02));if(state.attack)moves.add(state.attack);assert.ok(state.anchorX>=800*.22&&state.anchorX<=800*.34)}assert.ok(state.spawned>=5);assert.equal(state.hits+state.dodges,state.spawned);assert.equal(state.obstacles,0);assert.deepEqual([...moves].sort(),['high','low','mid']);assert.ok(state.particles<=90)});
test('silence settles to each distinct idle, a brief gap does not, and incoming audio wakes the fighter',()=>{const idle=[];for(let id=0;id<6;id++){const f=createFighter();f.setCharacter(id);let state;for(let i=0;i<25;i++)state=f.draw(ctx,800,400,i*40,Array(32).fill(0));assert.equal(state.resting,false);for(let i=25;i<125;i++)state=f.draw(ctx,800,400,i*40,Array(32).fill(0));assert.equal(state.resting,true);idle.push(state.idleAnimation);state=f.draw(ctx,800,400,5040,Array(32).fill(.6));assert.equal(state.resting,false)}assert.equal(new Set(idle).size,6)});
test('combat pace changes target travel time without adding targets during silence',()=>{function firstHit(pace){const f=createFighter();let state;for(let i=0;i<400;i++){state=f.draw(ctx,800,400,i*40,Array(32).fill(i===0?.7:0),1,pace);if(state.hits)return i}throw Error('target was not struck')}assert.ok(firstHit(2)<firstHit(.5));const f=createFighter();let state;for(let i=0;i<300;i++)state=f.draw(ctx,800,400,i*40,Array(32).fill(0));assert.equal(state.spawned,0)});
test('expanded strikes reach their targets at contact, have distinct wind-ups, and return to guard',async()=>{
const {fighterPose}=await import('./fighter-motion.mjs');
for(const [move,height]of [['punch',55],['high-punch',94],['dragon-punch',119],['flying-kick',55],['sweep',16]]){
const hit=fighterPose(move,.48),point=move==='sweep'||move==='flying-kick'?hit.frontF:hit.hand;assert.ok(Math.abs(hit.shift+point[0]-74)<1e-8,move+' connects horizontally');assert.ok(Math.abs(hit.lift+point[1]+height)<1e-8,move+' connects at target height');assert.notDeepEqual(fighterPose(move,.16),hit);assert.deepEqual(fighterPose(move,0),fighterPose(move,1));
const frames=Array.from({length:29},(_,i)=>JSON.stringify(fighterPose(move,i/28)));assert.ok(new Set(frames).size>22,'interpolation gives a continuous move, not a static pose switch');
}
const idle1=fighterPose(null,0,100,.5),idle2=fighterPose(null,0,300,.5);assert.notDeepEqual(idle1,idle2,'guard has a rhythmic bounce');
});
test('fighter backdrop is exactly Spectrum Waves geometry with only its opacity reduced',async()=>{
const {paintFrequencyWaves}=await import('./fighter.mjs'),{createSpectrumWaves}=await import('./spectrum-waves.mjs');let paths=[],points=[];const c={save(){},restore(){},beginPath(){points=[]},moveTo:(x,y)=>points.push([x,y]),lineTo:(x,y)=>points.push([x,y]),stroke(){paths.push({color:this.strokeStyle,points})}};
const direct=createSpectrumWaves(),backdrop=createSpectrumWaves();for(const [now,band]of [[100,0],[200,.6],[300,.1],[400,0]]){const bands=Array.from({length:32},(_,i)=>band*(.5+Math.sin(i)*.5));paths=[];direct.draw(c,800,400,now,bands);const expected=paths;paths=[];paintFrequencyWaves(c,800,400,now,bands,1,backdrop);assert.deepEqual(paths,expected);assert.equal(c.globalAlpha,.50)}
});
test('all fighters have a different target graphic and theme',async()=>{const {targetThemes,paintTarget}=await import('./fighter-targets.mjs');assert.equal(new Set(targetThemes.map(t=>t.name)).size,6);const signatures=[];for(let id=0;id<6;id++)for(let variant=0;variant<3;variant++){ const marks=[];const c=new Proxy({},{get:(o,k)=>o[k]||((...args)=>marks.push([k,...args]))});paintTarget(c,{x:600,y:200,size:25,variant},id,1000);assert.ok(marks.some(([method])=>method==='stroke'));signatures.push(JSON.stringify(marks))}assert.equal(new Set(signatures).size,18)});
test('a slow last target finishes its strike before the silence animation begins',()=>{const f=createFighter();let sawFinalStrike=false,state;for(let i=0;i<140;i++){state=f.draw(ctx,800,400,i*40,Array(32).fill(i===0?.8:0),1,.5);if(i>50&&state.attack){sawFinalStrike=true;assert.equal(state.resting,false);assert.equal(state.idleAnimation,null)}}assert.ok(sawFinalStrike);assert.equal(state.resting,true)});
test('fighters stay planted and ready between targets',()=>{const f=createFighter();for(let i=0;i<300;i++){const state=f.draw(ctx,800,400,i*40,Array(32).fill(i%100===0?.75:.08));assert.equal(state.walking,false);assert.equal(state.groundTravel,0);assert.equal(state.anchorX,224.00000000000003)}});
test('the shuffled move set includes spins, ducks and jumps without dropping any targets',()=>{const f=createFighter(),moves=new Set();let state;for(let i=0;i<1800;i++){state=f.draw(ctx,800,400,i*40,Array(32).fill(i<1300&&i%25<4?.8:.03));if(state.move)moves.add(state.move)}for(const move of ['spin-kick','tornado-kick','duck','jump','backfist'])assert.ok(moves.has(move));assert.ok(state.dodges>0);assert.equal(state.hits+state.dodges,state.spawned)});
test('spinning moves turn through their wind-up and evasion poses clear high and low targets',async()=>{const {fighterPose}=await import('./fighter-motion.mjs');for(const move of ['spin-kick','tornado-kick','backfist']){assert.ok(fighterPose(move,.24).turn<-.9);assert.equal(fighterPose(move,.48).turn,1)}assert.ok(fighterPose('duck',.48).head[1]>-55);assert.ok(fighterPose('jump',.48).lift<-50)});
test('expressions react to events and blink briefly rather than staying shut',async()=>{const {faceState}=await import('./fighter-face.mjs');assert.equal(faceState(1000,0,{walking:true}).emotion,'happy');assert.equal(faceState(1000,0,{visible:true,newTarget:100}).emotion,'shock');assert.equal(faceState(1000,0,{visible:true,newTarget:500}).emotion,'anticipation');assert.equal(faceState(1000,0,{attack:{move:'punch',age:.36,impactAge:.02}}).emotion,'anger');assert.equal(faceState(1000,0,{attack:{move:'jump'}}).emotion,'surprise');assert.equal(faceState(1000,0,{resting:true}).emotion,'peaceful');let count=0;for(let now=0;now<15000;now+=25)if(faceState(now,0).blink)count++;assert.ok(count>10&&count<50)});
test('Bride uses a varied katana deck and keeps the resting caption inside compact previews',()=>{const f=createFighter();f.setCharacter(5);const moves=new Set(),captions=[];const c=new Proxy({canvas:{height:150},globalAlpha:1,measureText:t=>({width:t.length*8}),fillText:(text,x,y)=>captions.push({text,y})},{get:(o,k)=>k in o?o[k]:(()=>{})});let state;for(let i=0;i<1800;i++){state=f.draw(c,800,150,i*40,Array(32).fill(i<1500&&i%25<4?.8:0));if(state.move)moves.add(state.move)}assert.ok(moves.has('spinning-slash'));assert.ok(moves.has('rising-cut'));assert.equal(state.hits+state.dodges,state.spawned);assert.ok(captions.some(c=>c.text==='BLADE CARE'));assert.ok(captions.filter(c=>c.text==='BLADE CARE').every(c=>c.y<=142))});
test('the actual fighter scene draws live spectrum waves without static vector scenery',()=>{let calls=0;const bands=Array(32).fill(.4);const fighter=createFighter({spectrum:{draw(c,w,h,time,levels){calls++;assert.equal(w,800);assert.equal(h,400);assert.deepEqual(levels,bands)}}});fighter.draw(ctx,800,400,100,bands);assert.equal(calls,1)});