§
    ê#¾j¤F  ã                  ó(  — d Z ddlmZ ddlZddlZddlmZ ddlZddlZ ee	¦  «         
                    ¦   «         j        j        j        Zedz  dz  Zedz  dz  Zed	z  d
z  Zg d¢Zdd„Z G d„ d¦  «        Z G d„ d¦  «        Z G d„ d¦  «        ZdS )zkUnit Test Suite for FreeExile Animation Atlas Frame Mapping, Pivot Bounding Box & Turn Inertia Snap Freeze.é    )ÚannotationsN)ÚPathÚclientÚwebappÚassetsÚ
animationsÚjsÚengine)Úchar_sword_masterÚchar_sword_maidenÚchar_feral_berserkerÚchar_wild_archerÚchar_glacial_lancerÚchar_shadow_assassinÚjs_codeÚstrÚreturnÚdictc                ó   — d| › d�}t          j        ddd|gt          t          ¦  «        dd¬¦  «        }|j        dk    sJ d	|j        › d
| › �¦   «         ‚t          j        |j         	                    ¦   «         ¦  «        S )Nag  
    globalThis.window = globalThis;
    globalThis.document = { readyState: 'complete', addEventListener: () => {}, getElementById: () => null };
    const fs = await import('node:fs');
    new Function(fs.readFileSync('./client/webapp/js/engine/animation_engine.js', 'utf-8'))();
    const ae = window.AnimationEngine;
    globalThis.ASSETS = globalThis.ASSETS || {};
    globalThis.ASSETS['hero_anim_atlas'] = { complete: true, naturalWidth: 1280, naturalHeight: 960 };
    const makeCtx = () => {
        const captured = [];
        return {
            captured, save() {}, restore() {}, translate() {}, scale() {}, rotate() {},
            fillRect() {}, strokeRect() {}, fillText() {},
            drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh) { captured.push({ sx, sy, sw, sh, dx, dy, dw, dh }); }
        };
    };
    (async () => {
        try {
            zn
        } catch (err) {
            console.error(err);
            process.exit(1);
        }
    })();
    Únodez--input-type=modulez-eT)ÚcwdÚcapture_outputÚtextr   zNode script execution failed:
z
Code:
)
Ú
subprocessÚrunr   ÚPROJECT_ROOTÚ
returncodeÚstderrÚjsonÚloadsÚstdoutÚstrip)r   ÚrunnerÚress      úLC:\Projects\FreeExile\tests\unit\test_animation_frame_mapping_and_inertia.pyÚrun_node_jsr&      s™   € ðð$ ð%ð ð €Fõ2 Œ.˜&Ð"7¸¸vÐFÍCÕP\ÑL]ÔL]ÐnrÐy}Ð
~Ñ
~Ô
~€CØŒ>˜QÒÐÐÐ `À#Ä*Ð `Ð `ÐW^Ð `Ð `ÑÔÐÝŒ:�c”j×&Ò&Ñ(Ô(Ñ)Ô)Ð)ó    c                  óú   — e Zd ZdZej                             de¦  «        ej                             dg d¢¦  «        dd„¦   «         ¦   «         Zdd	„Z	dd
„Z
dd„Zdd„Zdd„Zdd„Zdd„Zdd„Zdd„Zdd„Zdd„ZdS )ÚTestAtlasFrameMappingz>Verifies R1: Exact atlas grid frame mapping and pivot reading.Úclass_idÚ	clip_name)Úidler   Úattack_slashÚdodger   r   ÚNonec                óÖ  — t          j        t          |› d�z                       d¬¦  «        ¦  «        }d|› d|› d|› d|› d|› d	|› d
�}t	          |¦  «        d         }|d         |         d         }t          |¦  «        t          |¦  «        k    sJ ‚t          |¦  «        D ]E\  }}|d         |         }	||         d         |	d         k    r||         d         |	d         k    sJ ‚ŒFd S )Nz_anim_manifest.jsonúutf-8©Úencodingz
        globalThis.ASSETS['zŒ_anim_atlas'] = { complete: true, naturalWidth: 1280, naturalHeight: 960 };
        const ctx = makeCtx(), state = ae.createAnimationState('z!');
        state.currentClip = 'z&';
        const clip = ae.MANIFESTS['z']?.animations['z('] || ae.MANIFESTS['hero']?.animations['aL  '];
        const results = [];
        for (let f = 0; f < clip.frames.length; f++) {
            state.frameIndex = f; ctx.captured.length = 0;
            ae.drawEntityFrame(ctx, state, 100, 100, 1, 100, 125);
            results.push(ctx.captured[0] || null);
        }
        console.log(JSON.stringify({ results }));
        Úresultsr   ÚframesÚsxÚxÚsyÚy)r   r    ÚANIM_DIRÚ	read_textr&   ÚlenÚ	enumerate)
Úselfr*   r+   Úmanifest_datar   r4   Úclip_framesÚidxÚfkeyÚexps
             r%   Ú+test_all_classes_match_manifest_coordinateszATestAtlasFrameMapping.test_all_classes_match_manifest_coordinates2   sP  € õ œ
¥H°(Ð/OÐ/OÐ/OÑ$O×#ZÒ#ZÐdkÐ#ZÑ#lÔ#lÑmÔmˆðØ$ðð àAIðð ð (ðð ð %-ð	ð ð ?Hð	ð ð r{ð	ð ð ˆõ ˜gÑ&Ô& yÔ1ˆØ# LÔ1°)Ô<¸XÔFˆÝ�7‰|Œ|�s ;Ñ/Ô/Ò/Ð/Ð/Ð/Ý" ;Ñ/Ô/ð 	Uð 	U‰IˆC�Ø Ô)¨$Ô/ˆCØ˜3”< Ô%¨¨S¬Ò1Ð1°g¸c´lÀ4Ô6HÈCÐPSÌHÒ6TÐ6TÐ6TÐTÐ6Tð	Uð 	Ur'   c                óð  — d}t          |¦  «        }|d         d         dk    r|d         d         dk    sJ ‚|d         d         dk    r|d         d         dk    sJ ‚|d         d         dk    r|d         d         dk    sJ ‚|d	         d         d
k    r|d	         d         dk    sJ ‚|d         d         dk    r|d         d         dk    sJ ‚|d         d         dk    r|d         d         dk    sJ ‚d S )NaJ  
        const ctx = makeCtx(), state = ae.createAnimationState('hero');
        const checkClip = (clip) => {
            state.currentClip = clip; state.frameIndex = 0; ctx.captured.length = 0;
            ae.drawEntityFrame(ctx, state, 0, 0, 1, 100, 125);
            return ctx.captured[0];
        };
        console.log(JSON.stringify({
            thrust0: checkClip('attack_thrust'), slam0: checkClip('attack_slam'), bolt0: checkClip('attack_bolt'),
            whirlwind0: checkClip('skill_whirlwind'), dodge0: checkClip('dodge'), hurt0: checkClip('hurt')
        }));
        Úthrust0r6   i€  r8   éÀ   Úslam0Úbolt0Ú
whirlwind0i@  i€  Údodge0r   i@  Úhurt0iÀ  ©r&   ©r>   r   Úouts      r%   Ú6test_aliased_attack_clips_do_not_skew_subsequent_clipszLTestAtlasFrameMapping.test_aliased_attack_clips_do_not_skew_subsequent_clipsJ   s.  € ðˆõ ˜'Ñ"Ô"ˆØ�9Œ~˜dÔ# sÒ*Ð*¨s°9¬~¸dÔ/CÀsÒ/JÐ/JÐ/JÐJØ�7Œ|˜DÔ! SÒ(Ð(¨S°¬\¸$Ô-?À3Ò-FÐ-FÐ-FÐFØ�7Œ|˜DÔ! SÒ(Ð(¨S°¬\¸$Ô-?À3Ò-FÐ-FÐ-FÐFØ�<Ô  Ô&¨#Ò-Ð-°#°lÔ2CÀDÔ2IÈSÒ2PÐ2PÐ2PÐPØ�8Œ}˜TÔ" aÒ'Ð'¨C°¬M¸$Ô,?À3Ò,FÐ,FÐ,FÐFØ�7Œ|˜DÔ! SÒ(Ð(¨S°¬\¸$Ô-?À3Ò-FÐ-FÐ-FÐFÐ-FÐ-Fr'   c                ó¬   — d}t          |¦  «        d         }t          j        |d         dd¬¦  «        rt          j        |d         dd¬¦  «        sJ ‚d S )	Nay  
        ae.MANIFESTS['custom_test'] = {
            texture_width: 1280, texture_height: 960, frame_width: 160, frame_height: 192,
            frames: { 'idle_0': { x: 0, y: 0, w: 160, h: 192, pivot: [0.4, 0.75] } },
            animations: { idle: { frames: ['idle_0'], fps: 6, loop: true } }
        };
        globalThis.ASSETS['custom_test_anim_atlas'] = { complete: true, naturalWidth: 1280, naturalHeight: 960 };
        const ctx = makeCtx(), state = ae.createAnimationState('custom_test');
        ae.drawEntityFrame(ctx, state, 100, 100, 1, 100, 200);
        console.log(JSON.stringify({ cap: ctx.captured[0] }));
        ÚcapÚdxg      DÀçš™™™™™¹?©Úabs_tolÚdyg     ÀbÀ©r&   ÚmathÚisclose©r>   r   rR   s      r%   Ú2test_pivot_bounding_box_reads_manifest_frame_pivotzHTestAtlasFrameMapping.test_pivot_bounding_box_reads_manifest_frame_pivot_   sa   € ð
ˆõ ˜'Ñ"Ô" 5Ô)ˆÝŒ|˜C œI u°cÐ:Ñ:Ô:Ðk½t¼|ÈCÐPTÌIÐW]ÐgjÐ?kÑ?kÔ?kÐkÐkÐkÐkÐkr'   c                ól   — d}t          |¦  «        d         }|�|d         dk    r|d         dk    sJ ‚d S )Na  
        const ctx = makeCtx(), state = ae.createAnimationState('hero');
        state.currentClip = 'nonexistent_test_action';
        ae.drawEntityFrame(ctx, state, 0, 0, 1, 100, 125);
        console.log(JSON.stringify({ cap: ctx.captured[0] }));
        rR   r6   r   r8   rM   r[   s      r%   Ú=test_fallback_clip_on_unknown_animation_draws_idle_frame_zerozSTestAtlasFrameMapping.test_fallback_clip_on_unknown_animation_draws_idle_frame_zeron   sC   € ðˆõ ˜'Ñ"Ô" 5Ô)ˆØˆ 3 t¤9°¢> >°c¸$´iÀ1²n°n°nÐD°n°nr'   c                óh   — d}t          |¦  «        d         }|d         dk    r|d         dk    sJ ‚d S )Na  
        const ctx = makeCtx(), state = ae.createAnimationState('hero');
        state.currentClip = 'idle'; state.frameIndex = -1;
        ae.drawEntityFrame(ctx, state, 0, 0, 1, 100, 125);
        console.log(JSON.stringify({ cap: ctx.captured[0] }));
        rR   r6   ià  r8   r   rM   r[   s      r%   Ú.test_negative_frame_index_wraps_modulo_cleanlyzDTestAtlasFrameMapping.test_negative_frame_index_wraps_modulo_cleanlyx   sA   € ðˆõ ˜'Ñ"Ô" 5Ô)ˆØ�4Œy˜CÒÐ C¨¤I°¢N N NÐ2 N Nr'   c                ó<   — d}t          |¦  «        d         du sJ ‚d S )NzÁ
        const ctx = makeCtx(), state = ae.createAnimationState('mob_feral_hellhound');
        console.log(JSON.stringify({ drawn: ae.drawEntityFrame(ctx, state, 0, 0, 1, 80, 80) }));
        ÚdrawnFrM   ©r>   r   s     r%   Ú(test_mob_atlas_does_not_draw_hero_spritez>TestAtlasFrameMapping.test_mob_atlas_does_not_draw_hero_sprite‚   s/   € ðˆõ ˜7Ñ#Ô# GÔ,°Ð5Ð5Ð5Ð5Ð5Ð5r'   c                ó<   — d}t          |¦  «        d         du sJ ‚d S )Na  
        ae.MANIFESTS['empty_test'] = {
            texture_width: 1280, texture_height: 960, frame_width: 160, frame_height: 192,
            animations: { empty_clip: { frames: [] } }
        };
        globalThis.ASSETS['empty_test_anim_atlas'] = { complete: true, naturalWidth: 1280, naturalHeight: 960 };
        const ctx = makeCtx(), state = ae.createAnimationState('empty_test');
        state.currentClip = 'empty_clip';
        console.log(JSON.stringify({ drawn: ae.drawEntityFrame(ctx, state, 0, 0, 1, 100, 100) }));
        rb   FrM   rc   s     r%   Ú$test_empty_frames_clip_returns_falsez:TestAtlasFrameMapping.test_empty_frames_clip_returns_false‰   s/   € ð	ˆõ ˜7Ñ#Ô# GÔ,°Ð5Ð5Ð5Ð5Ð5Ð5r'   c                ó<   — d}t          |¦  «        d         du sJ ‚d S )NaÔ  
        ae.MANIFESTS['circular_cycle'] = {
            texture_width: 1280, texture_height: 960, frame_width: 160, frame_height: 192,
            animations: { loop_a: { atlas_clip: 'loop_b' }, loop_b: { atlas_clip: 'loop_a' } }
        };
        const ctx = makeCtx(), state = ae.createAnimationState('circular_cycle');
        state.currentClip = 'loop_a';
        console.log(JSON.stringify({ drawn: ae.drawEntityFrame(ctx, state, 0, 0, 1, 100, 100) }));
        rb   FrM   rc   s     r%   Ú test_circular_alias_cycle_safetyz6TestAtlasFrameMapping.test_circular_alias_cycle_safety–   s/   € ðˆõ ˜7Ñ#Ô# GÔ,°Ð5Ð5Ð5Ð5Ð5Ð5r'   c                ó°   — d}t          |¦  «        d         }|�:t          j        |d         dd¬¦  «        rt          j        |d         dd¬¦  «        sJ ‚d S )	Naw  
        ae.MANIFESTS['nan_pivot'] = {
            texture_width: 1280, texture_height: 960, frame_width: 160, frame_height: 192,
            frames: { 'idle_0': { x: 0, y: 0, w: 160, h: 192, pivot: [NaN, Infinity] } },
            animations: { idle: { frames: ['idle_0'], fps: 6, loop: true } }
        };
        globalThis.ASSETS['nan_pivot_anim_atlas'] = { complete: true, naturalWidth: 1280, naturalHeight: 960 };
        const ctx = makeCtx(), state = ae.createAnimationState('nan_pivot');
        ae.drawEntityFrame(ctx, state, 100, 100, 1, 100, 200);
        console.log(JSON.stringify({ cap: ctx.captured[0] }));
        rR   rS   g      IÀrT   rU   rW   g     €fÀrX   r[   s      r%   Ú$test_nan_and_infinite_pivot_fallbackz:TestAtlasFrameMapping.test_nan_and_infinite_pivot_fallback¢   sd   € ð
ˆõ ˜'Ñ"Ô" 5Ô)ˆØˆ¥4¤<°°D´	¸5È#Ð#NÑ#NÔ#NˆÕSWÔS_Ð`cÐdhÔ`iÐkqÐ{~ÐSÑSÔSˆˆÐˆˆr'   c                ó¬   — d}t          |¦  «        d         }t          j        |d         dd¬¦  «        rt          j        |d         dd¬¦  «        sJ ‚d	S )
zFNegative frame pivots calculate proper offset coordinates without NaN.aa  
        ae.MANIFESTS['neg_pivot'] = {
            texture_width: 1280, texture_height: 960, frame_width: 160, frame_height: 192,
            frames: { 'i0': { x: 0, y: 0, w: 160, h: 192, pivot: [-0.25, -0.5] } },
            animations: { idle: { frames: ['i0'], fps: 6 } }
        };
        globalThis.ASSETS['neg_pivot_anim_atlas'] = { complete: true, naturalWidth: 1280, naturalHeight: 960 };
        const ctx = makeCtx(), state = ae.createAnimationState('neg_pivot');
        ae.drawEntityFrame(ctx, state, 100, 100, 1, 100, 200);
        console.log(JSON.stringify({ cap: ctx.captured[0] }));
        rR   rS   g      9@rT   rU   rW   g      Y@NrX   r[   s      r%   Ú"test_negative_frame_pivot_handlingz8TestAtlasFrameMapping.test_negative_frame_pivot_handling±   s`   € ð
ˆõ ˜'Ñ"Ô" 5Ô)ˆÝŒ|˜C œI t°SÐ9Ñ9Ô9Ði½d¼lÈ3ÈtÌ9ÐV[ÐehÐ>iÑ>iÔ>iÐiÐiÐiÐiÐir'   c                óˆ   — d}t          |¦  «        }|d         du r$|d         d         dk    r|d         d         dk    sJ ‚dS )	zOFrames with non-finite or zero dimensions safely fallback to manifest defaults.a}  
        ae.MANIFESTS['malformed_frame'] = {
            texture_width: 1280, texture_height: 960, frame_width: 160, frame_height: 192,
            frames: { 'bad0': { x: NaN, y: Infinity, w: 0, h: -10 } },
            animations: { idle: { frames: ['bad0'], fps: 6 } }
        };
        globalThis.ASSETS['malformed_frame_anim_atlas'] = { complete: true, naturalWidth: 1280, naturalHeight: 960 };
        const ctx = makeCtx(), state = ae.createAnimationState('malformed_frame');
        const drawn = ae.drawEntityFrame(ctx, state, NaN, 100, 1, 100, 200);
        console.log(JSON.stringify({ drawn, cap: ctx.captured[0] }));
        rb   TrR   r6   r   r8   NrM   )r>   r   r$   s      r%   Ú(test_malformed_frame_dimensions_fallbackz>TestAtlasFrameMapping.test_malformed_frame_dimensions_fallbackÁ   sZ   € ð
ˆõ ˜'Ñ"Ô"ˆØ�7Œ|˜tÐ#Ð#¨¨E¬
°4Ô(8¸AÒ(=Ð(=À#ÀeÄ*ÈTÔBRÐVWÒBWÐBWÐBWÐWÐBWÐBWr'   c                óL   — d}t          |¦  «        }|d         €|d         �J ‚d S )NaX  
        const ctx = makeCtx();
        let errUpdate = null, errDraw = null;
        try { ae.updateAnimation(null, 0.0, 0.016); } catch (e) { errUpdate = e.message; }
        try { ae.drawEntityFrame(ctx, undefined, 0, 0, 1, 100, 100); } catch (e) { errDraw = e.message; }
        console.log(JSON.stringify({ errUpdate, errDraw }));
        Ú	errUpdateÚerrDrawrM   rN   s      r%   Ú)test_null_and_undefined_anim_state_safetyz?TestAtlasFrameMapping.test_null_and_undefined_anim_state_safetyÑ   s8   € ðˆõ ˜'Ñ"Ô"ˆØ�;ÔÐ'¨C°	¬NÐ,BÐ,BÐBÐ,BÐ,Br'   N)r*   r   r+   r   r   r/   ©r   r/   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__ÚpytestÚmarkÚparametrizeÚCLASSESrD   rP   r\   r^   r`   rd   rf   rh   rj   rl   rn   rr   © r'   r%   r)   r)   /   sn  € € € € € ØHÐHà„[×Ò˜Z¨Ñ1Ô1Ø„[×Ò˜[Ð*RÐ*RÐ*RÑSÔSðUð Uð Uñ TÔSñ 2Ô1ðUð,Gð Gð Gð Gð*lð lð lð lðEð Eð Eð Eð3ð 3ð 3ð 3ð6ð 6ð 6ð 6ð6ð 6ð 6ð 6ð
6ð 
6ð 
6ð 
6ð@ð @ð @ð @ðjð jð jð jð Xð Xð Xð Xð 	Cð 	Cð 	Cð 	Cð 	Cð 	Cr'   r)   c                  óZ   — e Zd ZdZdd„Zdd„Zdd„Zdd„Zdd„Zdd	„Z	dd
„Z
dd„Zdd„ZdS )ÚTestTurnInertiaSnapFreezezZVerifies R2: Smooth turn inertia freezes immediately on release without residual rotation.r   r/   c                ót   — d}t          |¦  «        }|d         |d         k    r|d         |d         k    sJ ‚d S )Na9  
        const state = ae.createAnimationState('hero'), moveDir = { x: 0.707, y: -0.707 };
        for (let i = 0; i < 10; i++) ae.updateAnimation(state, 4.2, 0.016, moveDir);
        const movingAngle = state.smoothAngle, stoppedFacing = { x: 1, y: 0 };
        ae.updateAnimation(state, 0.0, 0.016, stoppedFacing);
        const stoppedAngle1 = state.smoothAngle;
        for (let i = 0; i < 20; i++) ae.updateAnimation(state, 0.0, 0.016, stoppedFacing);
        console.log(JSON.stringify({ movingAngle, stoppedAngle1, stoppedAngle20: state.smoothAngle }));
        ÚstoppedAngle1ÚmovingAngleÚstoppedAngle20rM   rN   s      r%   Ú;test_smooth_angle_stops_lerp_immediately_when_velocity_zerozUTestTurnInertiaSnapFreeze.test_smooth_angle_stops_lerp_immediately_when_velocity_zeroà   sP   € ðˆõ ˜'Ñ"Ô"ˆØ�?Ô# s¨=Ô'9Ò9Ð9¸cÐBRÔ>SÐWZÐ[hÔWiÒ>iÐ>iÐ>iÐiÐ>iÐ>ir'   c                óÄ   — d}t          |¦  «        }|d         dk    r|d         dk    sJ ‚|d         dk    r|d         dk    sJ ‚|d         dk    r|d	         dk    sJ ‚d S )
NaY  
        const state = ae.createAnimationState('hero');
        state.currentClip = 'run';
        ae.updateAnimation(state, 4.2, 0.016, { x: 1, y: 0 });
        const movingClip = state.currentClip, movingPhase = state.kineticPhase;
        ae.updateAnimation(state, 0.0, 0.016, { x: 1, y: 0 });
        const stoppedClip = state.currentClip, stoppedPhase = state.kineticPhase;
        ae.updateAnimation(state, 4.2, 0.016, { x: 1, y: 0 });
        console.log(JSON.stringify({ movingClip, movingPhase, stoppedClip, stoppedPhase, resClip: state.currentClip, resPhase: state.kineticPhase }));
        Ú
movingClipr   ÚmovingPhaseÚstoppedClipr,   ÚstoppedPhaseÚresClipÚresPhaserM   rN   s      r%   Ú+test_transition_run_to_idle_in_single_framezETestTurnInertiaSnapFreeze.test_transition_run_to_idle_in_single_frameí   s‹   € ð	ˆõ ˜'Ñ"Ô"ˆØ�<Ô  EÒ)Ð)¨c°-Ô.@ÀEÒ.IÐ.IÐ.IÐIØ�=Ô! VÒ+Ð+°°NÔ0CÀvÒ0MÐ0MÐ0MÐMØ�9Œ~ Ò&Ð&¨3¨z¬?¸eÒ+CÐ+CÐ+CÐCÐ+CÐ+Cr'   c                ó\   — d}t          |¦  «        }|d         dk    r|d         dk    sJ ‚d S )NzÝ
        const state = ae.createAnimationState('hero');
        ae.updateAnimation(state, -4.2, 0.016, { x: 1, y: 0 });
        console.log(JSON.stringify({ clip: state.currentClip, phase: state.kineticPhase }));
        Úclipr   ÚphaserM   rN   s      r%   Ú)test_negative_velocity_magnitude_handlingzCTestTurnInertiaSnapFreeze.test_negative_velocity_magnitude_handlingý   sA   € ðˆõ
 ˜'Ñ"Ô"ˆØ�6Œ{˜eÒ#Ð#¨¨G¬¸Ò(=Ð(=Ð(=Ð=Ð(=Ð(=r'   c                óX   — d}t          |¦  «        }|d         dk    r
|d         du sJ ‚d S )Na  
        const state = ae.createAnimationState('hero');
        ae.playAction(state, 'attack_slash');
        ae.updateAnimation(state, 0.0, 0.016, { x: 1, y: 0 });
        console.log(JSON.stringify({ clip: state.currentClip, locked: state.isActionLocked }));
        r�   r-   ÚlockedTrM   rN   s      r%   Ú8test_action_locked_animation_not_overwritten_by_velocityzRTestTurnInertiaSnapFreeze.test_action_locked_animation_not_overwritten_by_velocity  sA   € ðˆõ ˜'Ñ"Ô"ˆØ�6Œ{˜nÒ,Ð,°°X´À$Ð1FÐ1FÐ1FÐFÐ1FÐ1Fr'   c                ót   — d}t          |¦  «        }|d         |d         k    r|d         |d         k    sJ ‚d S )Naº  
        const state = ae.createAnimationState('hero');
        const p = { wx: 0, wy: 0, speed: 4.2, anim: state, destination: { wx: 10, wy: 5 } };
        const dx = p.destination.wx - p.wx, dy = p.destination.wy - p.wy, dist = Math.hypot(dx, dy);
        ae.updateAnimation(state, p.speed, 0.016, { x: (dx/dist - dy/dist)*0.5, y: (dx/dist + dy/dist)*0.25 });
        const movingAngle = state.smoothAngle;
        ae.updateAnimation(state, 0.0, 0.016, null);
        const arrivalAngle = state.smoothAngle;
        for (let i = 0; i < 10; i++) ae.updateAnimation(state, 0.0, 0.016, null);
        console.log(JSON.stringify({ movingAngle, arrivalAngle, postAngle: state.smoothAngle }));
        ÚarrivalAngler�   Ú	postAnglerM   rN   s      r%   Ú*test_click_to_move_heading_and_snap_freezezDTestTurnInertiaSnapFreeze.test_click_to_move_heading_and_snap_freeze  sO   € ð
ˆõ ˜'Ñ"Ô"ˆØ�>Ô" c¨-Ô&8Ò8Ð8¸SÀÔ=MÐQTÐUbÔQcÒ=cÐ=cÐ=cÐcÐ=cÐ=cr'   c                ó@   — d}t          |¦  «        d         dk    sJ ‚d S )Na•  
        const state = ae.createAnimationState('hero'), dt = 1.0 / 120.0, dir = { x: 0.8, y: 0.6 };
        ae.updateAnimation(state, 4.2, dt, dir);
        const angles = [];
        for (let f = 0; f < 120; f++) {
            const mag = (f % 2 === 0) ? 0.04 : 0.06;
            ae.updateAnimation(state, (mag >= 0.05) ? 4.2 : 0, dt, (mag >= 0.05) ? dir : null);
            angles.push(state.smoothAngle);
        }
        let flutterCount = 0;
        for (let f = 1; f < angles.length; f += 2) {
            if (f + 1 < angles.length && angles[f + 1] !== angles[f]) flutterCount++;
        }
        console.log(JSON.stringify({ flutterCount }));
        ÚflutterCountr   rM   rc   s     r%   Ú+test_rapid_120hz_analog_boundary_no_flutterzETestTurnInertiaSnapFreeze.test_rapid_120hz_analog_boundary_no_flutter  s/   € ðˆõ ˜7Ñ#Ô# NÔ3°qÒ8Ð8Ð8Ð8Ð8Ð8r'   c                ó8   — d}t          |¦  «        d         �J ‚d S )Naš  
        ae.MANIFESTS['no_kin'] = { texture_width: 1280, texture_height: 960, frame_width: 160, frame_height: 192, animations: { idle: { frames: ['i0'], fps: 6 } } };
        const state = ae.createAnimationState('no_kin');
        let error = null;
        try { ae.updateAnimation(state, 2.0, 0.016, { x: 1, y: 0 }); } catch (e) { error = e.message; }
        console.log(JSON.stringify({ error }));
        ÚerrorrM   rc   s     r%   Ú%test_manifest_without_kinematics_safez?TestTurnInertiaSnapFreeze.test_manifest_without_kinematics_safe1  s*   € ðˆõ ˜7Ñ#Ô# GÔ,Ð4Ð4Ð4Ð4Ð4r'   c                óˆ   — d}t          |¦  «        }|d         du r|d         dk    sJ ‚|d         du r|d         dk    sJ ‚dS )	zJInfinity velocity or dt safely falls back without creating NaN frameIndex.a¦  
        const state1 = ae.createAnimationState('hero'), state2 = ae.createAnimationState('hero');
        ae.updateAnimation(state1, Infinity, 0.016);
        ae.updateAnimation(state2, 4.2, Infinity);
        console.log(JSON.stringify({
            idx1: state1.frameIndex, finite1: Number.isFinite(state1.frameTime),
            idx2: state2.frameIndex, finite2: Number.isFinite(state2.frameTime)
        }));
        Úfinite1TÚidx1r   Úfinite2Úidx2NrM   rN   s      r%   Ú+test_infinite_velocity_and_dt_finite_safetyzETestTurnInertiaSnapFreeze.test_infinite_velocity_and_dt_finite_safety;  sd   € ðˆõ ˜'Ñ"Ô"ˆØ�9Œ~ Ð%Ð%¨#¨f¬+¸Ò*:Ð*:Ð*:Ð:Ø�9Œ~ Ð%Ð%¨#¨f¬+¸Ò*:Ð*:Ð*:Ð:Ð*:Ð*:r'   c                ó\   — d}t          |¦  «        }|d         dk    r|d         dk    sJ ‚dS )z/Non-finite degrees safely default to S heading.ziconsole.log(JSON.stringify({ h1: ae.get8DirectionHeading(NaN), h2: ae.get8DirectionHeading(Infinity) }));Úh1ÚSÚh2NrM   rN   s      r%   Ú'test_non_finite_heading_degree_fallbackzATestTurnInertiaSnapFreeze.test_non_finite_heading_degree_fallbackJ  s=   € à}ˆÝ˜'Ñ"Ô"ˆØ�4Œy˜CÒÐ C¨¤I°Ò$4Ð$4Ð$4Ð4Ð$4Ð$4r'   Nrs   )rt   ru   rv   rw   rƒ   r‹   r�   r’   r–   r™   rœ   r¢   r§   r|   r'   r%   r~   r~   Ý   sÞ   € € € € € ØdÐdðjð jð jð jðDð Dð Dð Dð >ð >ð >ð >ðGð Gð Gð Gðdð dð dð dð9ð 9ð 9ð 9ð$5ð 5ð 5ð 5ð;ð ;ð ;ð ;ð5ð 5ð 5ð 5ð 5ð 5r'   r~   c                  ó"   — e Zd ZdZdd„Zdd„ZdS )ÚTestCodeQualityAndDebugOverlayz)Verifies R3 and Code Quality constraints.r   r/   c                ó¨   — t          t          dz                       d¬¦  «                             ¦   «         ¦  «        }|dk    sJ d|› d�¦   «         ‚d S )Nzanimation_engine.jsr1   r2   i^  zanimation_engine.js line count z exceeds soft cap of 350)r<   Ú
ENGINE_DIRr;   Ú
splitlines)r>   Úliness     r%   Ú1test_animation_engine_line_cap_strictly_under_350zPTestCodeQualityAndDebugOverlay.test_animation_engine_line_cap_strictly_under_350T  sU   € Ý•ZÐ"7Ñ7×BÒBÈGÐBÑTÔT×_Ò_ÑaÔaÑbÔbˆØ˜Š|ˆ|ˆ|Ð^¸uÐ^Ð^Ð^‰|Œ|ˆ|ˆ|ˆ|r'   c                ó<   — d}t          |¦  «        d         du sJ ‚d S )NzBconsole.log(JSON.stringify({ debugAnim: window.__DEBUG_ANIM__ }));Ú	debugAnimFrM   rc   s     r%   Ú$test_debug_overlay_hidden_by_defaultzCTestCodeQualityAndDebugOverlay.test_debug_overlay_hidden_by_defaultX  s+   € ØVˆÝ˜7Ñ#Ô# KÔ0°EÐ9Ð9Ð9Ð9Ð9Ð9r'   Nrs   )rt   ru   rv   rw   r®   r±   r|   r'   r%   r©   r©   Q  sF   € € € € € Ø3Ð3ð_ð _ð _ð _ð:ð :ð :ð :ð :ð :r'   r©   )r   r   r   r   )rw   Ú
__future__r   r   rY   Úpathlibr   r   rx   Ú__file__ÚresolveÚparentr   Ú
WEBAPP_DIRr:   r«   r{   r&   r)   r~   r©   r|   r'   r%   ú<module>r¸      si  ðØ qÐ qØ "Ð "Ð "Ð "Ð "Ð "Ø €€€Ø €€€Ø Ð Ð Ð Ð Ð Ø Ð Ð Ð Ø €€€àˆt�H‰~Œ~×%Ò%Ñ'Ô'Ô.Ô5Ô<€Ø˜HÑ$ xÑ/€
Ø˜Ñ  <Ñ/€Ø˜$Ñ Ñ)€
ð Pð  Pð  P€ð*ð *ð *ð *ð>kCð kCð kCð kCð kCñ kCô kCð kCð\q5ð q5ð q5ð q5ð q5ñ q5ô q5ð q5ðh	:ð 	:ð 	:ð 	:ð 	:ñ 	:ô 	:ð 	:ð 	:ð 	:r'   