§
    ã¾j¡:  ã                  ó0  — d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddl
mZ ddlmZmZ ddlmZ  ej        e¦  «                             ¦   «         j        j        j        Zdd„Z G d„ dej        ¦  «        Zedk    r ej        ¦   «          dS dS )a  
FreeExile M5 Hybrid Unit Test Suite: Encounter Zone Architecture & Terrain-Anchored Spawning.
Covers server-side generation/purity and genuine client runtime via Node.js subprocess execution.
Strictly <= 300 lines. Zero boolean tautologies, zero mock shortcuts.
é    )ÚannotationsN)ÚTileType)ÚWildernessMapGenerator)Úserialize_map_gridÚdeserialize_map_grid)Ú
ZoneEngineÚscriptÚstrÚreturnÚdictc                óÖ   — t          j        ddd| gddt          t          ¦  «        d¬¦  «        }t	          j        d„ |j                             ¦   «         D ¦   «         d         ¦  «        S )NÚnodez--input-type=modulez-eT)Úcapture_outputÚtextÚcwdÚcheckc                ó^   — g | ]*}|                      ¦   «         ¯|                      ¦   «         ‘Œ+S © )Ústrip)Ú.0Úlns     ú8C:\Projects\FreeExile\tests\unit\test_encounter_zones.pyú
<listcomp>z_run_node.<locals>.<listcomp>   s-   € ÐSÐSÐS bÈÏÊÉ
Ì
ÐS�r—x’x‘z”zÐSÐSÐSó    éÿÿÿÿ)Ú
subprocessÚrunr
   ÚPROJECT_ROOTÚjsonÚloadsÚstdoutÚ
splitlines)r	   Úprocs     r   Ú	_run_noder$      sk   € ÝŒ>Ø	Ð&¨¨fÐ5Ø $­CµÑ,=Ô,=ÀTðñ ô €Dõ Œ:ÐSÐS¨D¬K×,BÒ,BÑ,DÔ,DÐSÑSÔSÐTVÔWÑXÔXÐXr   c                  ót   — e Zd Zd„ Zd„ Zd„ Zd„ Zd„ Zd„ Zd„ Z	d„ Z
d	„ Zd
„ Zd„ Zd„ Zd„ Zd„ Zd„ Zd„ Zd„ ZdS )ÚTestEncounterZonesc                ó†   — t          ¦   «         | _        t          j        d¦  «                             d¬¦  «        | _        d S )NÚzone_tang_kiem_nhaié*   )Úseed)r   Úenginer   Úfor_zoneÚgenerateÚmap_data)Úselfs    r   ÚsetUpzTestEncounterZones.setUp   s6   € Ý ‘l”lˆŒÝ.Ô7Ð8MÑNÔN×WÒWÐ]_ÐWÑ`Ô`ˆŒˆˆr   c                ó.  — | j         j        }|                      dt          |¦  «        cxk    odk    nc ¦  «         |D ]™}|                      d|j        cxk    o|j        cxk     o| j         j        k     nc ¦  «         |                      d|j        cxk    o|j        cxk     o| j         j	        k     nc ¦  «         |  
                    |j        d¦  «         Œšd„ | j         j        D ¦   «         }|                      t          |¦  «        dk    ¦  «         d S )Né   é   r   )é   r2   r3   c                óz   — h | ]8}|D ]3}|j         t          j        t          j        t          j        fv ¯,|j         ’Œ4Œ9S r   )Ú	tile_typer   ÚENCOUNTER_LOWÚENCOUNTER_MEDIUMÚENCOUNTER_HIGH)r   ÚrowÚcs      r   ú	<setcomp>zRTestEncounterZones.test_01_encounter_zones_generation_and_tiers.<locals>.<setcomp>*   sƒ   € ð  mð  mð  m SÈð  mð  mÀAÐPQÔP[Õ`hÔ`võ  yAô  yRõ  T\ô  Tkð  `lð  Qlð  Ql�Q”[ð  Qlð  Qlð  Qlð  Qlr   )r.   Úencounter_zonesÚ
assertTrueÚlenÚmin_xÚmax_xÚwidthÚmin_yÚmax_yÚheightÚassertInÚtierÚtiles)r/   ÚzonesÚezÚ	enc_tiless       r   Ú,test_01_encounter_zones_generation_and_tiersz?TestEncounterZones.test_01_encounter_zones_generation_and_tiers#   s<  € Ø”Ô-ˆØ�Š˜�S ™ZœZÐ,Ð,Ò,Ð,¨1Ò,Ð,Ð,Ð,Ñ-Ô-Ð-Øð 	.ð 	.ˆBØ�OŠO˜A ¤ÐJÐJÒJÐJ¨B¬HÐJÐJÒJÐJ°t´}Ô7JÒJÐJÐJÐJÑKÔKÐKØ�OŠO˜A ¤ÐKÐKÒKÐK¨B¬HÐKÐKÒKÐK°t´}Ô7KÒKÐKÐKÐKÑLÔLÐLØ�MŠM˜"œ' 9Ñ-Ô-Ð-Ð-ð mð  m¨D¬MÔ,?ð  mñ  mô  mˆ	Ø�Š�˜I™œ¨!Ò+Ñ,Ô,Ð,Ð,Ð,r   c                óè  — | j         j        d         }t          d¦  «        D ]š}t          j        |j        |j        ¦  «        }t          j        |j        |j        ¦  «        }|  	                    |j        |cxk    o
|j        k    nc ¦  «         |  	                    |j        |cxk    o
|j        k    nc ¦  «         Œ›|  
                    |j        |j        dz
  cxk    o
|j        k    nc ¦  «         d S )Nr   é   r4   )r.   r=   ÚrangeÚrandomÚrandintr@   rA   rC   rD   r>   ÚassertFalse)r/   rJ   Ú_ÚsxÚsys        r   Ú-test_02_terrain_anchored_pack_spawning_boundsz@TestEncounterZones.test_02_terrain_anchored_pack_spawning_bounds-   s÷   € ØŒ]Ô*¨1Ô-ˆÝ�r‘”ð 	8ð 	8ˆAÝ” ¤¨"¬(Ñ3Ô3ˆBÝ” ¤¨"¬(Ñ3Ô3ˆBØ�OŠO˜BœH¨Ð6Ð6Ò6Ð6¨b¬hÒ6Ð6Ð6Ð6Ñ7Ô7Ð7Ø�OŠO˜BœH¨Ð6Ð6Ò6Ð6¨b¬hÒ6Ð6Ð6Ð6Ñ7Ô7Ð7Ð7Ø×Ò˜œ R¤X°¡\Ð=Ð=Ò=Ð=°R´XÒ=Ð=Ð=Ð=Ñ>Ô>Ð>Ð>Ð>r   c           
     óÒ  ‡ ‡‡— ‰ j         j        D ]ÖŠˆˆˆ fd„t          ‰j        ‰j        dz   ¦  «        D ¦   «         }‰                      t          |¦  «        dk    ¦  «         |D ]€Š‰                      ‰j                             ¦   «         ¦  «         ‰  	                    ‰j        t          j        t          j        t          j        t          j        t          j        f¦  «         Œ�Œ×d S )Nc                ó’   •— g | ]C}t          ‰j        ‰j        d z   ¦  «        D ]#}‰j        j        |         |         xŠj        ¯!‰‘Œ$ŒDS )r4   )rO   r@   rA   r.   rH   Úwalkable)r   ÚyÚxr;   rJ   r/   s      €€€r   r   zJTestEncounterZones.test_03_pack_spawn_tile_passability.<locals>.<listcomp>8   sž   ø€ ð   ^ð   ^ð   ^ aÕRWÐXZÔX`ÐbdÔbjÐmnÑbnÑRoÔRoð   ^ð   ^ÈQÐy}ô  zGô  zMð  NOô  zPð  QRô  zSð  uSÐtuô  t]ð   ^ ð   ^ð   ^ð   ^ð   ^r   r4   r   )r.   r=   rO   rC   rD   r>   r?   r6   Úis_passableÚassertNotInr   ÚWALLÚCHASMÚWATERÚ	BOSS_GATEÚVOID)r/   Úwalkable_in_zoner;   rJ   s   ` @@r   Ú#test_03_pack_spawn_tile_passabilityz6TestEncounterZones.test_03_pack_spawn_tile_passability6   s  øøø€ Ø”-Ô/ð 	Bð 	BˆBð  ^ð   ^ð   ^ð   ^ð   ^ð   ^­5°´¸2¼8Àa¹<Ñ+HÔ+Hð   ^ñ   ^ô   ^ÐØ�OŠO�CÐ 0Ñ1Ô1°AÒ5Ñ6Ô6Ð6Ø%ð Bð B�Ø—’ ¤× 7Ò 7Ñ 9Ô 9Ñ:Ô:Ð:Ø× Ò  ¤­x¬}½h¼nÍhÌnÕ^fÔ^pÕrzÔrð  /Añ  Bô  Bð  Bð  BðBð	Bð 	Br   c                óH  — | j                              dddd¬¦  «        \  }}|                      |¦  «         |                      d|¦  «         | j                              dd¬¦  «        \  }}| j                              dd¬¦  «        \  }}|                      |p|¦  «         d S )	Nr(   Fg     Àb@)Úis_dummyr[   rZ   u   pháº¡m vi an toÃ nÚzone_boundless_sanctuary)rf   Úzone_player_hideout)r+   Úvalidate_monster_spawnrR   rF   )r/   ÚokÚmsgÚok_shrS   Úok_hos         r   Ú,test_04_waypoint_safe_radius_spawn_rejectionz?TestEncounterZones.test_04_waypoint_safe_radius_spawn_rejection>   s¯   € Ø”+×4Ò4Ð5JÐUZÐ^cÐglÐ4ÑmÔm‰ˆˆCØ×Ò˜ÑÔÐØ�ŠÐ*¨CÑ0Ô0Ð0Ø”;×5Ò5Ð6PÐ[`Ð5ÑaÔa‰ˆˆqØ”;×5Ò5Ð6KÐV[Ð5Ñ\Ô\‰ˆˆqØ×Ò˜˜ %Ñ(Ô(Ð(Ð(Ð(r   c                ó¬  — | j         j        }|                      dt          |¦  «        cxk    odk    nc ¦  «         | j         j        }|D ]ˆ\  }}}| j         j        |         |         }|                      |j        t          j	        ¦  «         |                      |j
        ¦  «         |r)|                      |                     ||¦  «        ¦  «         Œ‰d S )Nr4   r3   )r.   Ú
poi_pointsr>   r?   Ú	boss_roomrH   ÚassertEqualr6   r   ÚPOIrY   rR   Úcontains)r/   ÚpoisÚbrÚpxÚpyrS   Útiles          r   Ú)test_05_poi_placement_count_and_isolationz<TestEncounterZones.test_05_poi_placement_count_and_isolationF   sÖ   € ØŒ}Ô'ˆØ�Š˜�S ™YœYÐ+Ð+Ò+Ð+¨!Ò+Ð+Ð+Ð+Ñ,Ô,Ð,ØŒ]Ô$ˆØð 	6ð 	6‰IˆB��AØ”=Ô& rÔ*¨2Ô.ˆDØ×Ò˜Tœ^­X¬\Ñ:Ô:Ð:Ø�OŠO˜DœMÑ*Ô*Ð*Øð 6Ø× Ò  §¢¨R°Ñ!4Ô!4Ñ5Ô5Ð5øð	6ð 	6r   c                óx  — t          | j        ¦  «        }t          |¦  «        }|                      t	          |j        ¦  «        t	          | j        j        ¦  «        ¦  «         t          | j        j        |j        ¦  «        D ]7\  }}|                      |d         |d         f|d         |d         f¦  «         Œ8d S )Nr   r4   )r   r.   r   rr   r?   rp   Úzip)r/   ÚrawÚdecodedÚp1Úp2s        r   Ú*test_06_poi_binary_serialization_roundtripz=TestEncounterZones.test_06_poi_binary_serialization_roundtripQ   s¬   € Ý  ¤Ñ/Ô/ˆÝ& sÑ+Ô+ˆØ×Ò�˜WÔ/Ñ0Ô0µ#°d´mÔ6NÑ2OÔ2OÑPÔPÐPÝ˜$œ-Ô2°GÔ4FÑGÔGð 	=ð 	=‰FˆB�Ø×Ò˜b œe R¨¤U˜^¨b°¬e°R¸´U¨^Ñ<Ô<Ð<Ð<ð	=ð 	=r   c                ó  — d}t          |¦  «        }|                      |d         ¦  «         |                      |d         d¦  «         |                      |d         ¦  «         |                      d|d         cxk    odk    nc ¦  «         d S )	Naé  
        import { updateAmbushTriggers } from './client/webapp/js/engine/ambush_trigger_system.js';
        global.currentMapMetadata = { pois: [{ x: 20, y: 20, type: 15 }] };
        const activeMonsters = [];
        const factory = (cfg) => ({ ...cfg, hp: cfg.maxHp || 2500 });
        // Far player: dist > 1.0 (at wx=25.0, wy=20.5)
        global.player = { wx: 25.0, wy: 20.5, hp: 100 };
        updateAmbushTriggers(0.016, activeMonsters, factory);
        const farTriggered = global.currentMapMetadata.pois[0].isTriggered === true;
        const countFar = activeMonsters.length;
        // Close player: dist <= 1.0 (at wx=20.6, wy=20.5)
        global.player = { wx: 20.6, wy: 20.5, hp: 100 };
        updateAmbushTriggers(0.016, activeMonsters, factory);
        const nearTriggered = global.currentMapMetadata.pois[0].isTriggered === true;
        const countNear = activeMonsters.length;
        console.log(JSON.stringify({ farTriggered, countFar, nearTriggered, countNear }));
        ÚfarTriggeredÚcountFarr   ÚnearTriggeredr3   Ú	countNearé   )r$   rR   rr   r>   ©r/   r	   Úress      r   Ú(test_07_scripted_poi_wave_ambush_runtimez;TestEncounterZones.test_07_scripted_poi_wave_ambush_runtimeX   s›   € ðˆõ" ˜ÑÔˆØ×Ò˜˜^Ô,Ñ-Ô-Ð-Ø×Ò˜˜Zœ¨!Ñ,Ô,Ð,Ø�Š˜˜OÔ,Ñ-Ô-Ð-Ø�Š˜˜S Ô-Ð2Ð2Ò2Ð2°Ò2Ð2Ð2Ð2Ñ3Ô3Ð3Ð3Ð3r   c                ó¾   — d}t          |¦  «        }|                      d|d         cxk    odk    nc ¦  «         |                      |d         |d         ¦  «         d S )Na©  
        import { updateAmbushTriggers } from './client/webapp/js/engine/ambush_trigger_system.js';
        global.currentMapMetadata = { pois: [{ x: 15, y: 15, type: 15 }] };
        const activeMonsters = [];
        const factory = (cfg) => ({ ...cfg, hp: cfg.maxHp || 2500 });
        global.player = { wx: 15.5, wy: 15.5, hp: 100 };
        updateAmbushTriggers(0.016, activeMonsters, factory);
        const initialCount = activeMonsters.length;
        // Second tick at same position
        updateAmbushTriggers(0.016, activeMonsters, factory);
        const secondCount = activeMonsters.length;
        console.log(JSON.stringify({ initialCount, secondCount }));
        r3   ÚinitialCountr‡   ÚsecondCount)r$   r>   rr   rˆ   s      r   Ú0test_08_scripted_poi_once_only_execution_runtimezCTestEncounterZones.test_08_scripted_poi_once_only_execution_runtimep   sq   € ðˆõ ˜ÑÔˆØ�Š˜˜S Ô0Ð5Ð5Ò5Ð5°AÒ5Ð5Ð5Ð5Ñ6Ô6Ð6Ø×Ò˜˜]Ô+¨S°Ô-@ÑAÔAÐAÐAÐAr   c                ó   — d}t          |¦  «        }|                      |d         ¦  «         |                      |d         ¦  «         |                      |d         ¦  «         |                      |d         ¦  «         d S )NaŒ  
        import { BossGateController } from './client/webapp/js/engine/boss_gate_controller.js';
        const bgc = new BossGateController({ gateX: 50, gateY: 40 });
        const gx = 50.5, gy = 40.5;
        bgc.update(0.016, gx + 3.2, gy);
        const s1 = bgc.isProximityActive;
        bgc.update(0.016, gx + 2.8, gy);
        const s2 = bgc.isProximityActive;
        bgc.update(0.016, gx + 3.2, gy); // Hysteresis in band (3.0, 3.5]
        const s3 = bgc.isProximityActive;
        bgc.update(0.016, gx + 3.6, gy); // Exit band > 3.5
        const s4 = bgc.isProximityActive;
        console.log(JSON.stringify({ s1, s2, s3, s4 }));
        Ús1Ús2Ús3Ús4)r$   rR   r>   rˆ   s      r   Ú?test_09_boss_gate_proximity_lore_trigger_and_hysteresis_runtimezRTestEncounterZones.test_09_boss_gate_proximity_lore_trigger_and_hysteresis_runtime‚   sz   € ðˆõ ˜ÑÔˆØ×Ò˜˜TœÑ#Ô#Ð#Ø�Š˜˜Dœ	Ñ"Ô"Ð"Ø�Š˜˜Dœ	Ñ"Ô"Ð"Ø×Ò˜˜TœÑ#Ô#Ð#Ð#Ð#r   c                óx  — d}t          |¦  «        }|                      |d         d¦  «         |                      |d         d¦  «         |                      |d         d¦  «         |                      |d         d¦  «         |                      |d	         d
¦  «         |                      |d         d¦  «         d S )Na5  
        import { BossGateController } from './client/webapp/js/engine/boss_gate_controller.js';
        import { registerMonsterKill } from './client/webapp/js/engine/monster_pack_system.js';
        const z = 'zone_tang_kiem_nhai';
        global.currentMapGrid = new Uint8Array(100 * 100);
        global.currentMapWidth = 100;
        global.currentMapGrid[10 * 100 + 10] = 10; // BOSS_GATE
        global.zoneEncounterProgress = {
          [z]: { zoneId: z, totalPacks: 2, alivePacks: 2, clearedPacks: 0, totalKills: 0, requiredPacks: 2 }
        };
        const bgc = new BossGateController({ gateX: 10, gateY: 10, zoneId: z, requiredKills: 2 });
        global.bossGateController = bgc;
        const initialTile = global.currentMapGrid[10 * 100 + 10];
        const initialState = bgc.state;
        const mobs = [{ packId: 'p1', hp: 0 }, { packId: 'p2', hp: 500 }];
        registerMonsterKill(mobs[0], mobs, z);
        const stateAfterP1 = bgc.state;
        const remP1 = global.zoneEncounterProgress[z].alivePacks;
        mobs[1].hp = 0;
        registerMonsterKill(mobs[1], mobs, z);
        const stateAfterP2 = bgc.state;
        const unlockedTile = global.currentMapGrid[10 * 100 + 10];
        console.log(JSON.stringify({ initialTile, initialState, stateAfterP1, remP1, stateAfterP2, unlockedTile }));
        ÚinitialTileé
   ÚinitialStateÚLOCKEDÚstateAfterP1ÚremP1r4   ÚstateAfterP2ÚUNLOCKEDÚunlockedTile©r$   rr   rˆ   s      r   Ú<test_10_boss_gate_encounter_progress_and_auto_unlock_runtimezOTestEncounterZones.test_10_boss_gate_encounter_progress_and_auto_unlock_runtime—   s¿   € ðˆõ0 ˜ÑÔˆØ×Ò˜˜]Ô+¨RÑ0Ô0Ð0Ø×Ò˜˜^Ô,¨hÑ7Ô7Ð7Ø×Ò˜˜^Ô,¨hÑ7Ô7Ð7Ø×Ò˜˜Wœ qÑ)Ô)Ð)Ø×Ò˜˜^Ô,¨jÑ9Ô9Ð9Ø×Ò˜˜^Ô,¨aÑ0Ô0Ð0Ð0Ð0r   c                ó8  — d}t          |¦  «        }|                      |d         ¦  «         |                      |d         ¦  «         |                      |d         d¦  «         |                      |d         ¦  «         |                      |d         ¦  «         d S )NaB  
        import { findPath, hasLineOfSight, steerMonsterChase } from './client/webapp/js/engine/grid_pathfinder.js';
        global.currentMapWidth = 10;
        global.currentMapHeight = 10;
        const grid = new Uint8Array(100);
        grid.fill(1); // FLOOR
        for (let y = 2; y <= 7; y++) grid[y * 10 + 5] = 2; // WALL at x=5
        global.getTileAt = (tx, ty) => grid[ty * 10 + tx];
        const losAcross = hasLineOfSight(2.5, 5.0, 8.5, 5.0);
        const losClear = hasLineOfSight(2.5, 2.0, 2.5, 7.0);
        const outX = new Float32Array(64), outY = new Float32Array(64);
        const pathLen = findPath(2, 5, 8, 5, outX, outY);
        let hitsWall = false;
        for (let i = 0; i < pathLen; i++) {
          const tx = Math.floor(outX[i]), ty = Math.floor(outY[i]);
          if (tx === 5 && ty >= 2 && ty <= 7) hitsWall = true;
        }
        const mob = { wx: 2.5, wy: 5.0 };
        const steer = steerMonsterChase(mob, 8.5, 5.0, 0.016, 0.5);
        console.log(JSON.stringify({ losAcross, losClear, pathLen, hitsWall, hasSteer: steer.hasPath }));
        Ú	losAcrossÚlosClearÚpathLenr   ÚhitsWallÚhasSteer)r$   rR   r>   ÚassertGreaterrˆ   s      r   Ú2test_11_grid_pathfinder_navigation_and_los_runtimezETestEncounterZones.test_11_grid_pathfinder_navigation_and_los_runtime¸   s•   € ðˆõ* ˜ÑÔˆØ×Ò˜˜[Ô)Ñ*Ô*Ð*Ø�Š˜˜JœÑ(Ô(Ð(Ø×Ò˜3˜yœ>¨1Ñ-Ô-Ð-Ø×Ò˜˜ZœÑ)Ô)Ð)Ø�Š˜˜JœÑ(Ô(Ð(Ð(Ð(r   c                óª  — d}t          |¦  «        }|                      |d         d¦  «         |                      |d         d¦  «         |                      |d         d¦  «         |                      |d         ¦  «         |                      |d	         ¦  «         |                      |d
         ¦  «         |                      |d         d¦  «         d S )NaÄ  
        import { populateZonePacks, getEncounterAnchors } from './client/webapp/js/engine/monster_pack_system.js';
        global.currentMapMetadata = {
          encounterZones: [
            { minX: 10, minY: 10, maxX: 20, maxY: 20, tier: 1 },
            { minX: 30, minY: 30, maxX: 40, maxY: 40, tier: 2 }
          ]
        };
        const anchors = getEncounterAnchors('zone_tang_kiem_nhai');
        const monsters = populateZonePacks('zone_tang_kiem_nhai', (cfg) => ({ ...cfg, hp: cfg.maxHp || 2500 }));
        const pack1 = monsters.filter(m => m.packId === anchors[0].id);
        const pack2 = monsters.filter(m => m.packId === anchors[1].id);
        const leader1 = pack1.find(m => m.isPackLeader);
        const leader2 = pack2.find(m => m.isPackLeader);
        const hasAura = Boolean(leader2 && leader2.leaderAura);
        const prog = global.zoneEncounterProgress?.['zone_tang_kiem_nhai'];
        console.log(JSON.stringify({
          anchorCount: anchors.length,
          pack1Count: pack1.length,
          pack2Count: pack2.length,
          hasLeader1: Boolean(leader1),
          hasLeader2: Boolean(leader2),
          hasAura,
          totalPacks: prog?.totalPacks
        }));
        ÚanchorCountr2   Ú
pack1Counté   Ú
pack2Countr‡   Ú
hasLeader1Ú
hasLeader2ÚhasAuraÚ
totalPacks)r$   rr   r>   rˆ   s      r   Ú2test_12_monster_pack_spawning_and_cohesion_runtimezETestEncounterZones.test_12_monster_pack_spawning_and_cohesion_runtimeÕ   sÎ   € ðˆõ4 ˜ÑÔˆØ×Ò˜˜]Ô+¨QÑ/Ô/Ð/Ø×Ò˜˜\Ô*¨AÑ.Ô.Ð.Ø×Ò˜˜\Ô*¨AÑ.Ô.Ð.Ø�Š˜˜LÔ)Ñ*Ô*Ð*Ø�Š˜˜LÔ)Ñ*Ô*Ð*Ø�Š˜˜IœÑ'Ô'Ð'Ø×Ò˜˜\Ô*¨AÑ.Ô.Ð.Ð.Ð.r   c                ó~   — d}t          |¦  «        }|                      |d         |d         |d         fd¦  «         d S )Na  import { registerMonsterKill } from './client/webapp/js/engine/monster_pack_system.js';
const z = 'zone_tang_kiem_nhai'; global.currentZoneId = z;
global.zoneEncounterProgress = { [z]: { zoneId: z, totalPacks: 2, alivePacks: 2, clearedPacks: 0, totalKills: 0, requiredPacks: 2 } };
const m = [{ id: 'm1', packId: 'p1', hp: 0 }, { id: 'm2', packId: 'p1', hp: 0 }, { id: 'm3', packId: 'p1', hp: 0 }];
for (const mob of m) registerMonsterKill(mob, m, z);
console.log(JSON.stringify(global.zoneEncounterProgress[z]));ÚclearedPacksÚ
alivePacksÚ
totalKills)r4   r4   r3   rŸ   rˆ   s      r   Ú5test_13_simultaneous_aoe_death_pack_clear_idempotencyzHTestEncounterZones.test_13_simultaneous_aoe_death_pack_clear_idempotencyù   sO   € ðLð 	õ ˜ÑÔˆØ×Ò˜#˜nÔ-¨s°<Ô/@À#ÀlÔBSÐTÐV_Ñ`Ô`Ð`Ð`Ð`r   c                ó~   — d}t          |¦  «        }|                      |d         dk    o|d         dk    ¦  «         d S )Na  import { updateAmbushTriggers } from './client/webapp/js/engine/ambush_trigger_system.js';
global.currentMapMetadata = { pois: [{ x: 10, y: 10, type: 15 }] }; global.player = { wx: 10.5, wy: 10.5, hp: 100 };
global.getTileAt = (tx, ty) => (tx === 10 && ty === 10) ? 1 : 2;
const spawned = []; updateAmbushTriggers(0.016, spawned, (cfg) => ({ ...cfg, hp: 1000 }));
const walls = spawned.filter(m => global.getTileAt(Math.floor(m.wx), Math.floor(m.wy)) === 2);
console.log(JSON.stringify({ total: spawned.length, walls: walls.length }));Útotalr   Úwalls©r$   r>   rˆ   s      r   Ú2test_14_poi_ambush_adjacent_wall_fallback_passablezETestEncounterZones.test_14_poi_ambush_adjacent_wall_fallback_passable  sK   € ð[ð 	õ ˜ÑÔˆØ�Š˜˜Gœ qÒ(Ð>¨S°¬\¸QÒ->Ñ?Ô?Ð?Ð?Ð?r   c                óZ   — d}|                       t          |¦  «        d         ¦  «         d S )NaD  import { hasLineOfSight } from './client/webapp/js/engine/grid_pathfinder.js';
global.currentMapWidth = 120; global.currentMapHeight = 90;
const g = new Uint8Array(10800).fill(1); g[10 * 120 + 80] = 2; global.getTileAt = (x, y) => g[y * 120 + x];
console.log(JSON.stringify({ los: hasLineOfSight(10.5, 10.5, 85.5, 10.5) }));Úlos)rR   r$   )r/   r	   s     r   Ú7test_15_has_line_of_sight_wall_at_step_70_returns_falsezJTestEncounterZones.test_15_has_line_of_sight_wall_at_step_70_returns_false  s6   € ð\ð 	ð 	×Ò� 6Ñ*Ô*¨5Ô1Ñ2Ô2Ð2Ð2Ð2r   c                ó~   — d}t          |¦  «        }|                      |d         dk    o|d         dk    ¦  «         d S )Na¥  import { findPath } from './client/webapp/js/engine/grid_pathfinder.js';
global.currentMapWidth = 120; global.currentMapHeight = 90;
const g = new Uint8Array(10800).fill(1); global.getTileAt = (x, y) => g[y * 120 + x];
const ox = new Float32Array(64), oy = new Float32Array(64);
const len = findPath(10, 10, 90, 10, ox, oy); const dist = Math.hypot(ox[0] - 10.5, oy[0] - 10.5);
console.log(JSON.stringify({ len, dist }));r?   r   Údistg      ø?r»   rˆ   s      r   Ú;test_16_find_path_80_tile_open_line_first_waypoint_adjacentzNTestEncounterZones.test_16_find_path_80_tile_open_line_first_waypoint_adjacent  sI   € ð:ð 	õ ˜ÑÔˆØ�Š˜˜Eœ
 QšÐ=¨3¨v¬;¸#Ò+=Ñ>Ô>Ð>Ð>Ð>r   N)Ú__name__Ú
__module__Ú__qualname__r0   rL   rV   rd   rn   rz   r�   rŠ   rŽ   r”   r    r¨   r²   r·   r¼   r¿   rÂ   r   r   r   r&   r&      s$  € € € € € ðað að að-ð -ð -ð?ð ?ð ?ðBð Bð Bð)ð )ð )ð	6ð 	6ð 	6ð=ð =ð =ð4ð 4ð 4ð0Bð Bð Bð$$ð $ð $ð*1ð 1ð 1ðB)ð )ð )ð:"/ð "/ð "/ðH
að 
að 
að
@ð 
@ð 
@ð3ð 3ð 3ð
?ð 
?ð 
?ð 
?ð 
?r   r&   Ú__main__)r	   r
   r   r   )Ú__doc__Ú
__future__r   r   ÚpathlibrP   r   ÚunittestÚserver.world.map_data_typesr   Ú%server.world.wilderness_map_generatorr   Ú"server.world.map_binary_serializerr   r   Úserver.world.zone_enginer   ÚPathÚ__file__ÚresolveÚparentr   r$   ÚTestCaser&   rÃ   Úmainr   r   r   ú<module>rÕ      s;  ððð ð #Ð "Ð "Ð "Ð "Ð "Ø €€€Ø €€€Ø €€€Ø Ð Ð Ð Ø €€€à 0Ð 0Ð 0Ð 0Ð 0Ð 0Ø HÐ HÐ HÐ HÐ HÐ HØ WÐ WÐ WÐ WÐ WÐ WÐ WÐ WØ /Ð /Ð /Ð /Ð /Ð /àˆwŒ|˜HÑ%Ô%×-Ò-Ñ/Ô/Ô6Ô=ÔD€ðYð Yð Yð YðF?ð F?ð F?ð F?ð F?˜Ô*ñ F?ô F?ð F?ðR ˆzÒÐØ€H„M�O„O€O€O€Oð Ðr   