§
    CÍ½jç9  ã                  óÎ   — d Z ddlmZ ddlZddlZddlZddlmZ  ee¦  «         	                    ¦   «         j
        j
        j
        Zdd	„Z G d
„ d¦  «        Z G d„ d¦  «        ZdS )a  
Empirical Adversarial Challenge Suite for Milestone 2: Character Animation & Visceral Combat Feel Engine.
Author: challenger_m2_1_gen3
Roles: critic, specialist

This suite stress-tests assumptions and empirically reproduces bugs in:
1. Kinetic state machine phase synchronization across frame rates (15-240 FPS) and attack speeds (0.5-2.0x).
2. Action cancellation and clip reset in AnimationEngine.cancelAction.
3. Hit-stop timer decay in CombatFeelEngine.update.
4. Directional screen shake offset retrieval via CombatFeelEngine.getShakeOffset.
5. Hit-stop frame micro-freeze duration and DamageNumberPool zero-allocation ring buffer.
é    )ÚannotationsN)ÚPathÚcodeÚstrÚreturnÚdictc                ó¶  — d| › d�}t          j        ddd|gt          t          ¦  «        dd¬¦  «        }|j        dk    r't          d	|j        › d
|j        › d|j        › �¦  «        ‚d„ |j                             ¦   «          	                    ¦   «         D ¦   «         }d„ |D ¦   «         }|st          d|j        › �¦  «        ‚t          j        |d         ¦  «        S )z8Executes a JS snippet in Node.js and parses JSON stdout.zR
    globalThis.window = globalThis;
    (async () => {
        try {
            zn
        } catch (err) {
            console.error(err);
            process.exit(1);
        }
    })();
    Únodez--input-type=modulez-eT)ÚcwdÚcapture_outputÚtextr   zNode execution failed (code z):
STDOUT: z	
STDERR: c                ó^   — g | ]*}|                      ¦   «         ¯|                      ¦   «         ‘Œ+S © )Ústrip)Ú.0Úlines     úBC:\Projects\FreeExile\tests\unit\test_challenger_m2_adversarial.pyú
<listcomp>z$run_node_snippet.<locals>.<listcomp>-   s-   € ÐVÐVÐV˜dÈÏÊÉÌÐVˆT�ZŠZ‰\Œ\ÐVÐVÐVó    c                óf   — g | ].}|                      d ¦  «        ¯|                     d¦  «        ¯,|‘Œ/S )Ú{Ú})Ú
startswithÚendswith)r   Úls     r   r   z$run_node_snippet.<locals>.<listcomp>.   s8   € ÐLÐLÐL˜ a§l¢l°3Ñ&7Ô&7ÐL¸A¿JºJÀs¹O¼OÐL�!ÐLÐLÐLr   zNo JSON returned by script:
éÿÿÿÿ)Ú
subprocessÚrunr   ÚPROJECT_ROOTÚ
returncodeÚRuntimeErrorÚstdoutÚstderrr   Ú
splitlinesÚ
ValueErrorÚjsonÚloads)r   ÚrunnerÚresÚlinesÚ
json_liness        r   Úrun_node_snippetr,      s	  € ð
ð ð	
ð 
ð 
€Fõ Œ.Ø	Ð&¨¨fÐ5Ý•ÑÔØØð	ñ ô €Cð „~˜ÒÐÝÐx¸#¼.ÐxÐxÐVYÔV`ÐxÐxÐloÔlvÐxÐxÑyÔyÐyàVÐV c¤j×&6Ò&6Ñ&8Ô&8×&CÒ&CÑ&EÔ&EÐVÑVÔV€EØLÐL˜UÐLÑLÔL€JØð GÝÐE¸¼ÐEÐEÑFÔFÐFÝŒ:�j ”nÑ%Ô%Ð%r   c                  óô   — e Zd ZdZej                             dg d¢¦  «        ej                             dg d¢¦  «        ej                             dg d¢¦  «        dd„¦   «         ¦   «         ¦   «         Zdd„ZdS )Ú"TestAdversarialKineticStateMachinezNStress-tests 5-phase kinetic machine transitions across FPS and attack speeds.zfps,dt))é   g±?)é   g¡?)é<   g‘?)éx   g�?)éð   gq?Úaction_speed)g      à?g      ð?g      ø?g       @Úarchetype_clip)Úattack_slashÚattack_thrustÚattack_slamÚattack_boltÚfpsÚintÚdtÚfloatr   r   ÚNonec                ó>  — d|› d|› d|› d|› d|› d|› d�}t          |¦  «        }|d         d	k    sJ d
|d         › �¦   «         ‚|d         d	k    sJ d|d         › �¦   «         ‚|d         du sJ d|› d|› d|› �¦   «         ‚|d         du sJ d|› d|› d|› �¦   «         ‚dS )z\Verifies full lifecycle transitions: idle -> run -> wind_up -> (impact) -> recovery -> idle.a¹  
        await import('./client/webapp/js/engine/animation_engine.js');
        const { createAnimationState, updateAnimation, playAction, KINETIC_PHASES } = window.AnimationEngine;
        
        const state = createAnimationState('hero');
        const recordedPhases = [];
        
        // 1. Initial state
        recordedPhases.push(state.kineticPhase);
        
        // 2. Transition to Run
        updateAnimation(state, 3.2, zƒ);
        recordedPhases.push(state.kineticPhase);
        
        // 3. Stop moving -> Idle
        updateAnimation(state, 0.0, zœ);
        recordedPhases.push(state.kineticPhase);
        
        // 4. Trigger attack clip
        let hitTriggered = false;
        playAction(state, 'z', {
            actionSpeed: aB  ,
            onHit: () => { hitTriggered = true; }
        });
        recordedPhases.push(state.kineticPhase);
        
        // 5. Advance frames through the full action
        let iterations = 0;
        let visitedImpact = false;
        let visitedRecovery = false;
        
        while (state.currentClip === 'zA' && iterations < 300) {
            updateAnimation(state, 0.0, aœ  );
            if (state.kineticPhase === 'impact') visitedImpact = true;
            if (state.kineticPhase === 'recovery') visitedRecovery = true;
            recordedPhases.push(state.kineticPhase);
            iterations++;
        }
        
        // Final state should auto-recover to idle
        recordedPhases.push(state.kineticPhase);
        
        console.log(JSON.stringify({
            finalClip: state.currentClip,
            finalPhase: state.kineticPhase,
            hitTriggered: hitTriggered,
            visitedImpact: visitedImpact,
            visitedRecovery: visitedRecovery,
            totalIterations: iterations
        }));
        Ú	finalClipÚidlez$Expected auto-recovery to idle, got Ú
finalPhasezExpected final phase idle, got ÚhitTriggeredTz*Attack hit callback failed to trigger for z at z FPS, speed ÚvisitedRecoveryz!Recovery phase never reached for N©r,   )Úselfr:   r<   r4   r5   Újs_coder)   s          r   Ú*test_kinetic_transition_phases_progressionzMTestAdversarialKineticStateMachine.test_kinetic_transition_phases_progression7   s‚  € ð2ð &(ð2ð 2ð &(ð2ð 2ð( +ð)2ð 2ð* 'ð+2ð 2ð> (6ð?2ð 2ð@ *,ðA2ð 2ð 2ˆõf ˜wÑ'Ô'ˆØ�;Ô 6Ò)Ð)Ð)Ð+dÐRUÐVaÔRbÐ+dÐ+dÑ)Ô)Ð)Ø�<Ô  FÒ*Ð*Ð*Ð,aÈcÐR^ÔN_Ð,aÐ,aÑ*Ô*Ð*Ø�>Ô" dÐ*Ð*Ð*ð  -MÐYgð  -Mð  -MÐmpð  -Mð  -Mð  Kð  -Mð  -MÑ*Ô*Ð*ØÐ$Ô%¨Ð-Ð-Ð-ð  0GÐSað  0Gð  0GÐgjð  0Gð  0Gð  yEð  0Gð  0GÑ-Ô-Ð-Ð-Ð-r   c                óh   — d}t          |¦  «        }|d         dk    sJ d|d         › d�¦   «         ‚dS )zÞ
        BUG REPRODUCTION:
        Verifies whether kineticPhase is synchronized with frameIndex in the SAME tick,
        rather than lagging 1 tick behind due to premature phase calculation before frame advance.
        a°  
        await import('./client/webapp/js/engine/animation_engine.js');
        const { createAnimationState, updateAnimation, playAction } = window.AnimationEngine;
        const state = createAnimationState('hero');
        
        // attack_slash has hit_frame = 2
        playAction(state, 'attack_slash', { actionSpeed: 1.0 });
        
        let desyncCount = 0;
        // Advance 20 ticks at 60 FPS
        for (let i = 0; i < 20; i++) {
            updateAnimation(state, 0.0, 0.01667);
            if (state.currentClip === 'attack_slash') {
                if (state.frameIndex < 2 && state.kineticPhase !== 'wind_up') desyncCount++;
                if (state.frameIndex === 2 && state.kineticPhase !== 'impact') desyncCount++;
                if (state.frameIndex > 2 && state.kineticPhase !== 'recovery') desyncCount++;
            }
        }
        
        console.log(JSON.stringify({ desyncCount: desyncCount }));
        ÚdesyncCountr   z	Detected z; de-synchronized ticks between frameIndex and kineticPhase!NrE   ©rF   rG   r)   s      r   Ú.test_kinetic_phase_frame_index_synchronizationzQTestAdversarialKineticStateMachine.test_kinetic_phase_frame_index_synchronization{   sY   € ðˆõ* ˜wÑ'Ô'ˆØ�=Ô! QÒ&Ð&Ð&ð  )D°C¸Ô4Fð  )Dð  )Dð  )DÑ&Ô&Ð&Ð&Ð&r   N)
r:   r;   r<   r=   r4   r=   r5   r   r   r>   ©r   r>   )	Ú__name__Ú
__module__Ú__qualname__Ú__doc__ÚpytestÚmarkÚparametrizerH   rL   r   r   r   r.   r.   4   sÊ   € € € € € ØXÐXà„[×Ò˜Xð (ð (ð (ñ ô ð „[×Ò˜^Ð-AÐ-AÐ-AÑBÔBØ„[×ÒÐ-Ð/nÐ/nÐ/nÑoÔoð9Gð 9Gð 9Gñ pÔoñ CÔBñô ð9GðvDð Dð Dð Dð Dð Dr   r.   c                  ó€   — e Zd ZdZdd„Zdd„Zdd„Zej         	                    dg d¢¦  «        dd„¦   «         Z
dd„ZdS )ÚTestAdversarialCombatFeelEnginezCStress-tests hit-stop, camera shake, and damage number ring buffer.r   r>   c                óœ   — d}t          |¦  «        }|d         du s
J d¦   «         ‚|d         �d|d         v r
d|d         v s
J d	¦   «         ‚dS )
a"  
        BUG REPRODUCTION:
        canvas_renderer.js calls window.combatFeelEngine.getShakeOffset?.().
        CombatFeelEngine computes shakeOffsetX and shakeOffsetY but must provide getShakeOffset()
        so the renderer can actually apply the directional camera shake matrix.
        ar  
        const { CombatFeelEngine } = await import('./client/webapp/js/engine/combat_feel_engine.js');
        const cfe = new CombatFeelEngine();
        
        cfe.triggerScreenShake(5.0, 0.25, 0.785);
        cfe.update(0.016);
        
        const hasMethod = typeof cfe.getShakeOffset === 'function';
        const offset = hasMethod ? cfe.getShakeOffset() : null;
        
        console.log(JSON.stringify({
            hasMethod: hasMethod,
            offset: offset,
            hasOffsetX: typeof cfe.shakeOffsetX === 'number',
            hasOffsetY: typeof cfe.shakeOffsetY === 'number'
        }));
        Ú	hasMethodTzqCombatFeelEngine.prototype.getShakeOffset is missing! Directional screen shake is dead code in canvas_renderer.jsÚoffsetNÚxÚyz6getShakeOffset did not return {x, y} coordinate objectrE   rK   s      r   Ú$test_missing_get_shake_offset_defectzDTestAdversarialCombatFeelEngine.test_missing_get_shake_offset_defect�   sz   € ðˆõ" ˜wÑ'Ô'ˆØ�;Ô 4Ð'Ð'Ð'ð  *]Ñ'Ô'Ð'Ø�8Œ}Ð(¨S°C¸´MÐ-AÐ-AÀcÈSÐQYÌ]ÐFZÐFZÐFZð  ]UÑFZÔFZÐZÐFZÐFZr   c                óè   — d}t          |¦  «        }|d         dk    sJ d|d         › d�¦   «         ‚|d         dk    sJ d|d         › d�¦   «         ‚|d	         dk    sJ d
|d	         › d�¦   «         ‚dS )zÉ
        BUG REPRODUCTION:
        AnimationEngine.cancelAction must reset currentClip to 'idle', otherwise
        the subsequent updateAnimation call re-computes kineticPhase back to attack.
        a  
        await import('./client/webapp/js/engine/animation_engine.js');
        const { createAnimationState, updateAnimation, playAction, cancelAction } = window.AnimationEngine;
        const state = createAnimationState('hero');
        
        playAction(state, 'attack_slash');
        cancelAction(state);
        
        const clipAfterCancel = state.currentClip;
        
        // Tick updateAnimation after cancel
        updateAnimation(state, 0.0, 0.016);
        const clipAfterTick = state.currentClip;
        const phaseAfterTick = state.kineticPhase;
        
        console.log(JSON.stringify({
            clipAfterCancel: clipAfterCancel,
            clipAfterTick: clipAfterTick,
            phaseAfterTick: phaseAfterTick
        }));
        ÚclipAfterCancelrA   z"cancelAction left currentClip as 'z', expected 'idle'ÚclipAfterTickz/updateAnimation after cancel reverted clip to 'ú'ÚphaseAfterTickz0updateAnimation after cancel reverted phase to 'NrE   rK   s      r   Ú-test_cancel_action_resets_current_clip_defectzMTestAdversarialCombatFeelEngine.test_cancel_action_resets_current_clip_defect¹   sË   € ðˆõ* ˜wÑ'Ô'ˆØÐ$Ô%¨Ò/Ð/Ð/ð  2AÐVYÐZkÔVlð  2Að  2Að  2AÑ/Ô/Ð/Ø�?Ô# vÒ-Ð-Ð-Ð/xÐadÐetÔauÐ/xÐ/xÐ/xÑ-Ô-Ð-ØÐ#Ô$¨Ò.Ð.Ð.ð  1MÐcfÐgwÔcxð  1Mð  1Mð  1MÑ.Ô.Ð.Ð.Ð.r   c                ó”   — d}t          |¦  «        }|d         dk    s
J d¦   «         ‚|d         dk    sJ d|d         › d�¦   «         ‚dS )	zà
        BUG REPRODUCTION:
        CombatFeelEngine.update(dt) must decrement hitStopTimer so that the hit-stop pause
        resolves in the simulation loop without requiring an external manual isHitStopped caller.
        aY  
        const { CombatFeelEngine } = await import('./client/webapp/js/engine/combat_feel_engine.js');
        const cfe = new CombatFeelEngine();
        
        cfe.triggerHitStop(3, 0.05);
        const initialTimer = cfe.hitStopTimer;
        
        // Run update loop for 0.1s (longer than 0.05s hit-stop)
        for (let i = 0; i < 6; i++) {
            cfe.update(0.016);
        }
        const timerAfterUpdates = cfe.hitStopTimer;
        
        console.log(JSON.stringify({
            initialTimer: initialTimer,
            timerAfterUpdates: timerAfterUpdates
        }));
        ÚinitialTimerr   z hitStopTimer was not initializedÚtimerAfterUpdatesz2hitStopTimer failed to decay in update(dt)! Still z after 0.1sNrE   rK   s      r   Ú0test_hit_stop_timer_decays_in_update_loop_defectzPTestAdversarialCombatFeelEngine.test_hit_stop_timer_decays_in_update_loop_defectÙ   s{   € ðˆõ$ ˜wÑ'Ô'ˆØ�>Ô" QÒ&Ð&Ð&Ð(JÑ&Ô&Ð&ØÐ&Ô'¨1Ò,Ð,Ð,ð  /IÐcfÐgzÔc{ð  /Ið  /Ið  /IÑ,Ô,Ð,Ð,Ð,r   z0archetype_name,expected_frames,expected_duration))ÚSLASHé   gš™™™™™©?)ÚTHRUSTé   çìQ¸…ë¡?)ÚSLAMé   çL7‰A`å°?)ÚBOLTrj   rk   Úarchetype_namer   Úexpected_framesr;   Úexpected_durationr=   c                ó®   — d|› d�}t          |¦  «        }|d         |d         k    sJ ‚|d         |d         k    sJ ‚d|d         cxk    rdk    sn J ‚d	S )
zGVerifies attacker and target both halt frame advancement for 33ms-66ms.aÜ  
        await import('./client/webapp/js/engine/animation_engine.js');
        const { createAnimationState, updateAnimation, playAction } = window.AnimationEngine;
        const { CombatFeelEngine } = await import('./client/webapp/js/engine/combat_feel_engine.js');
        const { getWeaponSwingArchetype } = await import('./client/webapp/js/data/weapon_swing_catalog.js');
        
        const cfe = new CombatFeelEngine();
        const arch = getWeaponSwingArchetype('a¬  ');
        
        const attackerAnim = createAnimationState('hero');
        playAction(attackerAnim, 'attack_slash');
        attackerAnim.frameIndex = 2;
        attackerAnim.frameTime = 2.0;
        
        const targetAnim = createAnimationState('mob_skeleton_warrior');
        playAction(targetAnim, 'run');
        targetAnim.frameIndex = 1;
        targetAnim.frameTime = 1.0;
        
        const attacker = { anim: attackerAnim };
        const target = { anim: targetAnim, hitStopTimer: 0 };
        
        cfe.triggerHitStop(arch.hitStopFrames, arch.hitStopDuration, attacker, target);
        
        const dt = 0.016;
        let attackerFrozenTicks = 0;
        let targetFrozenTicks = 0;
        
        for (let tick = 0; tick < 10; tick++) {
            const prevAttackerFrame = attackerAnim.frameTime;
            const prevTargetFrame = targetAnim.frameTime;
            
            updateAnimation(attackerAnim, 0, dt);
            updateAnimation(targetAnim, 1.8, dt);
            
            if (attackerAnim.frameTime === prevAttackerFrame) attackerFrozenTicks++;
            if (targetAnim.frameTime === prevTargetFrame) targetFrozenTicks++;
        }
        
        console.log(JSON.stringify({
            expectedFrames: arch.hitStopFrames,
            attackerFrozenTicks: attackerFrozenTicks,
            targetFrozenTicks: targetFrozenTicks,
            duration: arch.hitStopDuration
        }));
        ÚattackerFrozenTicksÚexpectedFramesÚtargetFrozenTicksgL7‰A`å ?Údurationrn   NrE   )rF   rp   rq   rr   rG   r)   s         r   Ú.test_attacker_and_target_frame_freeze_durationzNTestAdversarialCombatFeelEngine.test_attacker_and_target_frame_freeze_durationõ   s–   € ð-ð 0>ð-ð -ð -ˆõ\ ˜wÑ'Ô'ˆØÐ(Ô)¨SÐ1AÔ-BÒBÐBÐBÐBØÐ&Ô'¨3Ð/?Ô+@Ò@Ð@Ð@Ð@Ø˜˜JœÐ0Ð0Ò0Ð0¨5Ò0Ð0Ð0Ð0Ð0Ð0Ð0Ð0r   c                óÄ   — d}t          |¦  «        }|d         dk    sJ ‚|d         dk    sJ ‚|d         dk    sJ ‚|d         du sJ ‚|d	         du sJ ‚|d
         du sJ ‚dS )zXSpawns 500 damage numbers; verifies strict 64-slot capacity, zero allocations, no leaks.al  
        const { CombatFeelEngine } = await import('./client/webapp/js/engine/combat_feel_engine.js');
        const cfe = new CombatFeelEngine();
        
        const initialCapacity = cfe.damagePool.length;
        
        for (let i = 0; i < 256; i++) {
            cfe.spawnDamageNumber(`-${i * 10}`, 100 + i, 200, i % 5 === 0 ? '#ef4444' : '#ffffff', i % 5 === 0);
        }
        
        const postSpawnCapacity = cfe.damagePool.length;
        const poolHead = cfe.poolHead;
        
        const allActive = cfe.damagePool.every(slot => slot.active === true);
        const allValidScales = cfe.damagePool.every(slot => typeof slot.scale === 'number' && !isNaN(slot.scale));
        
        cfe.update(0.1);
        const updatedValidScales = cfe.damagePool.every(slot => slot.scale >= 1.0);
        
        console.log(JSON.stringify({
            initialCapacity: initialCapacity,
            postSpawnCapacity: postSpawnCapacity,
            poolHead: poolHead,
            allActive: allActive,
            allValidScales: allValidScales,
            updatedValidScales: updatedValidScales
        }));
        ÚinitialCapacityé@   ÚpostSpawnCapacityÚpoolHeadr   Ú	allActiveTÚallValidScalesÚupdatedValidScalesNrE   rK   s      r   Ú6test_damage_number_pool_wraparound_and_zero_allocationzVTestAdversarialCombatFeelEngine.test_damage_number_pool_wraparound_and_zero_allocation0  s¥   € ðˆõ8 ˜wÑ'Ô'ˆØÐ$Ô%¨Ò+Ð+Ð+Ð+ØÐ&Ô'¨2Ò-Ð-Ð-Ð-Ø�:Œ !Ò#Ð#Ð#Ð#Ø�;Ô 4Ð'Ð'Ð'Ð'ØÐ#Ô$¨Ð,Ð,Ð,Ð,ØÐ'Ô(¨DÐ0Ð0Ð0Ð0Ð0Ð0r   NrM   )rp   r   rq   r;   rr   r=   r   r>   )rN   rO   rP   rQ   r\   rb   rf   rR   rS   rT   rx   r�   r   r   r   rV   rV   š   sÂ   € € € € € ØMÐMðUð Uð Uð Uð8Mð Mð Mð Mð@Ið Ið Ið Ið8 „[×ÒÐOð Rð Rð Rñ ô ð31ð 31ð 31ñô ð31ðj$1ð $1ð $1ð $1ð $1ð $1r   rV   )r   r   r   r   )rQ   Ú
__future__r   r&   r   rR   Úpathlibr   Ú__file__ÚresolveÚparentr   r,   r.   rV   r   r   r   ú<module>r‡      sþ   ððð ð #Ð "Ð "Ð "Ð "Ð "Ø €€€Ø Ð Ð Ð Ø €€€Ø Ð Ð Ð Ð Ð àˆt�H‰~Œ~×%Ò%Ñ'Ô'Ô.Ô5Ô<€ð&ð &ð &ð &ð:cDð cDð cDð cDð cDñ cDô cDð cDðLz1ð z1ð z1ð z1ð z1ñ z1ô z1ð z1ð z1ð z1r   