mirror of
https://github.com/polserver/polserver
synced 2026-08-13 08:23:08 -04:00
* introduced Attackable wrapper class for opponents to be able to also attack items * changed set_opponent to use Attackable moved first logic into Attackable * added dynamic property which returns a pointer of the object instead of a copy like the current imp. needed to be able to eg store a vector * send engage/disengage events send hp status bar * send attackable item flag in world item pkt * apply damage for items + evid_damaged * react to statrequests * adapted is_attackable and do attack_effects * fixed clear_opponent_of * fixed clear_opponent_of * allow attack pkt * apply damage * items need to keep track of the opponents to be able to cleanup on destroy * updated dynproperties * updated dynprops a bit more. made the structs trivial copyable updated concept to not repeat the types * run hitscript, cleanup * listhostiles and attack_once accept now Attackable * set opponent when dblclicked * dont let hp underflow * only attack if you are in warmode * evid_opponent_moved for attackable items * if its the same opponent dont send engaged events again added test for listhostiles with two mobs * allow attackable item for setopponent * some tests npc vs item * missing files * fixed clang tidy complain * test for enter/left area * do not send hp updates while char gets created and not yet added to the world * testclient fight packets * client event constants * added client tests for attacking npc and item * increase damage * removed leftover comment * when an attackable item was destroyed the opponents where not correctly cleared * orphan check for is_attackable * moved statmsg for items * init member * moved send_hightlight back into if statement check before dereferencing * use range version of statmsg * adapted em params * comparison operator made explicit constructors for symmetry return damage in Item::apply_damage * get rid of bools * split attack method a bit * use constants for F3 flag * destroy item on hit if hp reaches 0 if no controlscript exists * only toplevel items are attackable small refactoring more tests * more tests * even more tests * docs
586 lines
18 KiB
Text
586 lines
18 KiB
Text
use uo;
|
|
use os;
|
|
use vitals;
|
|
|
|
include "testutil";
|
|
include "sysevent";
|
|
|
|
program npc_ai()
|
|
return 1;
|
|
endprogram
|
|
|
|
// checks on item listhostile, engaged-, disengage-event
|
|
exported function attack_check_listhostiles( resmgn )
|
|
var npc1;
|
|
var item;
|
|
var evs;
|
|
var res := reset( resmgn, npc1, item );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
npc1.process.sendevent( struct{ type := "enable_events" } );
|
|
|
|
// npc1 attacks item
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
|
|
// Collect attack and engaged event
|
|
if ( !( evs := collect_events( 2 ) ) )
|
|
return evs;
|
|
endif
|
|
var hit_ev := evs.filter( @( ev ) { return ev.type == "HIT"; } )[1];
|
|
var engaged_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_ENGAGED; } )[1];
|
|
if ( !hit_ev )
|
|
return ret_error( $"no hit event: {evs}" );
|
|
endif
|
|
if ( !engaged_ev )
|
|
return ret_error( $"no engaged event: {evs}" );
|
|
endif
|
|
if ( hit_ev.defender != item || hit_ev.attacker != npc1 )
|
|
return ret_error( $"wrong hit event: {hit_ev}" );
|
|
endif
|
|
if ( engaged_ev.source != npc1 )
|
|
return ret_error( $"wrong engaged event: {engaged_ev}" );
|
|
endif
|
|
|
|
var hostile := ListHostiles( item );
|
|
if ( hostile.size() != 1 )
|
|
return ret_error( $"Hostile list != 1: {hostile.size()}" );
|
|
endif
|
|
if ( hostile[1] != npc1 )
|
|
return ret_error( $"wrong opponent set: {hostile[1]}" );
|
|
endif
|
|
|
|
// stop attacking
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_DISENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
|
|
if ( !( evs := collect_events( 1 ) ) )
|
|
return evs;
|
|
endif
|
|
var disengaged_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_DISENGAGED; } )[1];
|
|
if ( !disengaged_ev )
|
|
return ret_error( $"no disengaged event: {evs}" );
|
|
endif
|
|
if ( disengaged_ev.source != npc1 )
|
|
return ret_error( $"wrong disengaged event: {disengaged_ev}" );
|
|
endif
|
|
|
|
hostile := ListHostiles( item );
|
|
if ( hostile.size() )
|
|
return ret_error( $"Hostile list != 0: {hostile}" );
|
|
endif
|
|
return 1;
|
|
endfunction
|
|
|
|
// checks on item damage event + ApplyDamage
|
|
exported function attack_check_damage( resmgn )
|
|
var npc1;
|
|
var item;
|
|
var evs;
|
|
var res := reset( resmgn, npc1, item );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
SetObjProperty( npc1, "#Damage", 10 );
|
|
npc1.process.sendevent( struct{ type := "enable_events" } );
|
|
|
|
// npc1 attacks item
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
|
|
// Collect attack, engaged and damage event
|
|
if ( !( evs := collect_events( 3 ) ) )
|
|
return evs;
|
|
endif
|
|
var hit_ev := evs.filter( @( ev ) { return ev.type == "HIT"; } )[1];
|
|
var engaged_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_ENGAGED; } )[1];
|
|
var damaged_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_DAMAGED; } )[1];
|
|
if ( !hit_ev )
|
|
return ret_error( $"no hit event: {evs}" );
|
|
endif
|
|
if ( !engaged_ev )
|
|
return ret_error( $"no engaged event: {evs}" );
|
|
endif
|
|
if ( !damaged_ev )
|
|
return ret_error( $"no damaged event: {evs}" );
|
|
endif
|
|
if ( hit_ev.defender != item || hit_ev.attacker != npc1 )
|
|
return ret_error( $"wrong hit event: {hit_ev}" );
|
|
endif
|
|
if ( engaged_ev.source != npc1 )
|
|
return ret_error( $"wrong engaged event: {engaged_ev}" );
|
|
endif
|
|
if ( damaged_ev.source != npc1 || damaged_ev.damage != 10 )
|
|
return ret_error( $"wrong damaged event: {damaged_ev}" );
|
|
endif
|
|
|
|
if ( item.hp != item.maxhp - 10 )
|
|
return ret_error( $"item not damaged: {item.hp}/{item.maxhp}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// checks on item opponent move event
|
|
exported function attack_check_moved( resmgn )
|
|
var npc1;
|
|
var item;
|
|
var evs;
|
|
var res := reset( resmgn, npc1, item );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
npc1.process.sendevent( struct{ type := "enable_events" } );
|
|
MoveObjectToLocation( npc1, npc1.x - 2, npc1.y, npc1.z );
|
|
// npc1 attacks item
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
|
|
// Collect engaged
|
|
if ( !( evs := collect_events( 1, "no event after engaged" ) ) )
|
|
return evs;
|
|
endif
|
|
var engaged_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_ENGAGED; } )[1];
|
|
if ( !engaged_ev )
|
|
return ret_error( $"no engaged event: {evs}" );
|
|
endif
|
|
if ( engaged_ev.source != npc1 )
|
|
return ret_error( $"wrong engaged event: {engaged_ev}" );
|
|
endif
|
|
MoveObjectToLocation( npc1, npc1.x - 2, npc1.y, npc1.z );
|
|
if ( !( evs := collect_events( 1, "no event after moveobject" ) ) )
|
|
return evs;
|
|
endif
|
|
var moved_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_OPPONENT_MOVED; } )[1];
|
|
if ( !moved_ev )
|
|
return ret_error( $"no moved event: {evs}" );
|
|
endif
|
|
if ( moved_ev.source != npc1 )
|
|
return ret_error( $"wrong moved event: {moved_ev}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// checks on item left/enter events
|
|
exported function check_area_events( resmgn )
|
|
var npc1;
|
|
var item;
|
|
var evs;
|
|
var res := reset( resmgn, npc1, item );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
MoveObjectToLocation( npc1, item.x - 22, npc1.y, npc1.z );
|
|
sleepms( 10 );
|
|
clear_event_queue();
|
|
var success := false;
|
|
for i := 0 to 5
|
|
npc1.process.sendevent( struct{ type := "move_to", source := item } );
|
|
if ( !( evs := collect_events( 1, "" ) ) )
|
|
continue;
|
|
endif
|
|
|
|
var enter_ev := evs.filter( @( ev ) {
|
|
return ev.type == SYSEVENT_ENTEREDAREA;
|
|
} )[1];
|
|
if ( !enter_ev )
|
|
continue;
|
|
endif
|
|
if ( enter_ev.source != npc1 )
|
|
return ret_error( $"wrong enterarea event: {enter_ev}" );
|
|
endif
|
|
if ( Distance( item, npc1 ) != 20 )
|
|
return ret_error( $"wrong enter distance: {Distance( item, npc1 )}" );
|
|
endif
|
|
success := true;
|
|
break;
|
|
endfor
|
|
if ( !success )
|
|
return ret_error( "no entered area event" );
|
|
endif
|
|
|
|
// npc1 attacks to send both oppmoved and leffarea
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
|
|
if ( !( evs := collect_events( 1, "timeout waiting for engaged" ) ) )
|
|
return evs;
|
|
endif
|
|
var engaged_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_ENGAGED; } )[1];
|
|
if ( !engaged_ev )
|
|
return ret_error( $"no engaged event: {evs}" );
|
|
endif
|
|
if ( engaged_ev.source != npc1 )
|
|
return ret_error( $"wrong engaged event: {engaged_ev}" );
|
|
endif
|
|
|
|
success := false;
|
|
for i := 0 to 5
|
|
npc1.process.sendevent( struct{ type := "move_away", source := item } );
|
|
if ( !( evs := collect_events( 1, "timeout while waiting for left area" ) ) )
|
|
continue;
|
|
endif
|
|
|
|
var enter_ev := evs.filter( @( ev ) {
|
|
return ev.type == SYSEVENT_LEFTAREA;
|
|
} )[1];
|
|
if ( !enter_ev )
|
|
continue;
|
|
endif
|
|
if ( enter_ev.source != npc1 )
|
|
return ret_error( $"wrong leftarea event: {enter_ev}" );
|
|
endif
|
|
if ( Distance( item, npc1 ) != 21 )
|
|
return ret_error( $"wrong left distance: {Distance( item, npc1 )}" );
|
|
endif
|
|
success := true;
|
|
break;
|
|
endfor
|
|
if ( !success )
|
|
return ret_error( "no left area event" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// picking an item up ends the fight for everyone attacking it
|
|
exported function attack_check_pickup( resmgn )
|
|
var npc1;
|
|
var item;
|
|
var evs;
|
|
var res := reset( resmgn, npc1, item, "MovableAttackableItem" );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
npc1.process.sendevent( struct{ type := "enable_events" } );
|
|
|
|
// npc1 attacks item
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
|
|
// Collect attack and engaged event
|
|
if ( !( evs := collect_events( 2 ) ) )
|
|
return evs;
|
|
endif
|
|
if ( !evs.filter( @( ev ) { return ev.type == SYSEVENT_ENGAGED; } )[1] )
|
|
return ret_error( $"no engaged event: {evs}" );
|
|
endif
|
|
if ( ListHostiles( item ).size() != 1 )
|
|
return ret_error( "npc1 did not engage the item" );
|
|
endif
|
|
if ( npc1.opponent != item )
|
|
return ret_error( $"wrong opponent selected: {npc1.opponent}" );
|
|
endif
|
|
|
|
var backpack := resmgn.CreateItemAtLocation( 51, 100, 0, 0xE75 );
|
|
if ( !backpack )
|
|
return ret_error( "Could not create container: " + backpack );
|
|
endif
|
|
if ( !( res := MoveItemToContainer( item, backpack ) ) )
|
|
return ret_error( "Could not pick the item up: " + res );
|
|
endif
|
|
|
|
if ( !( evs := collect_events( 1, "no event after pickup" ) ) )
|
|
return evs;
|
|
endif
|
|
var disengaged_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_DISENGAGED; } )[1];
|
|
if ( !disengaged_ev )
|
|
return ret_error( $"no disengaged event: {evs}" );
|
|
endif
|
|
if ( disengaged_ev.source != npc1 )
|
|
return ret_error( $"wrong disengaged event: {disengaged_ev}" );
|
|
endif
|
|
if ( npc1.opponent )
|
|
return ret_error( $"npc1 still attacks the picked up item: {npc1.opponent}" );
|
|
endif
|
|
|
|
// dropping it again must not resurrect the old opponents
|
|
if ( !( res := MoveObjectToLocation( item, 51, 100, 0 ) ) )
|
|
return ret_error( "Could not drop the item: " + res );
|
|
endif
|
|
var hostile := ListHostiles( item );
|
|
if ( hostile.size() )
|
|
return ret_error( $"Hostile list != 0: {hostile}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// a picked up item is no combat target at all
|
|
exported function attack_check_contained_inert( resmgn )
|
|
var npc1;
|
|
var item;
|
|
var res := reset( resmgn, npc1, item, "MovableAttackableItem" );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
|
|
var backpack := resmgn.CreateItemAtLocation( 51, 100, 0, 0xE75 );
|
|
if ( !backpack )
|
|
return ret_error( "Could not create container: " + backpack );
|
|
endif
|
|
if ( !( res := MoveItemToContainer( item, backpack ) ) )
|
|
return ret_error( "Could not pick the item up: " + res );
|
|
endif
|
|
|
|
var hp := item.hp;
|
|
var dmg := ApplyDamage( item, 10, 0, 0 );
|
|
if ( dmg )
|
|
return ret_error( $"contained item took damage: {dmg}" );
|
|
endif
|
|
if ( item.hp != hp )
|
|
return ret_error( $"contained item was damaged: {item.hp}/{hp}" );
|
|
endif
|
|
|
|
if ( ( res := ListHostiles( item ) ) )
|
|
return ret_error( $"contained item is a combat target: {res}" );
|
|
endif
|
|
|
|
// and it becomes a valid target again once it is back in the world
|
|
if ( !( res := MoveObjectToLocation( item, 51, 100, 0 ) ) )
|
|
return ret_error( "Could not drop the item: " + res );
|
|
endif
|
|
if ( !( dmg := ApplyDamage( item, 10, 0, 0 ) ) )
|
|
return ret_error( $"item in the world took no damage: {dmg}" );
|
|
endif
|
|
if ( item.hp != hp - 10 )
|
|
return ret_error( $"item not damaged: {item.hp}/{hp}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// EVMASK_ONLY_PC suppresses the area events of an npc, but the opponent move
|
|
// event has to be reported regardless of the mask
|
|
exported function attack_check_area_mask( resmgn )
|
|
var npc1;
|
|
var item;
|
|
var evs;
|
|
var res := reset( resmgn, npc1, item );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
|
|
var events := SYSEVENT_ENGAGED | SYSEVENT_DISENGAGED | SYSEVENT_OPPONENT_MOVED
|
|
| SYSEVENT_ENTEREDAREA | SYSEVENT_LEFTAREA;
|
|
if ( !( res := configure_control( item, events, 5, EVMASK_ONLY_PC ) ) )
|
|
return res;
|
|
endif
|
|
|
|
// out of the area of the item, and out of the weapon range so that no swings
|
|
// interfere with the collected events
|
|
MoveObjectToLocation( npc1, item.x + 8, item.y, item.z );
|
|
clear_event_queue();
|
|
|
|
// npc1 attacks the item
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
if ( !( evs := collect_events( 1, "no event after engaged" ) ) )
|
|
return evs;
|
|
endif
|
|
if ( !evs.filter( @( ev ) { return ev.type == SYSEVENT_ENGAGED; } )[1] )
|
|
return ret_error( $"no engaged event: {evs}" );
|
|
endif
|
|
|
|
// entering the area of the item is suppressed, the opponent move is not
|
|
MoveObjectToLocation( npc1, item.x + 3, item.y, item.z );
|
|
if ( !( evs := collect_events( 1, "no event after entering the area" ) ) )
|
|
return evs;
|
|
endif
|
|
if ( evs.filter( @( ev ) { return ev.type == SYSEVENT_ENTEREDAREA; } )[1] )
|
|
return ret_error( $"entered area event despite EVMASK_ONLY_PC: {evs}" );
|
|
endif
|
|
var moved_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_OPPONENT_MOVED; } )[1];
|
|
if ( !moved_ev )
|
|
return ret_error( $"no opponent moved event: {evs}" );
|
|
endif
|
|
if ( moved_ev.source != npc1 )
|
|
return ret_error( $"wrong opponent moved event: {moved_ev}" );
|
|
endif
|
|
|
|
// without the mask the area event is sent instead of the opponent move
|
|
if ( !( res := configure_control( item, events, 5, EVMASK_ALL ) ) )
|
|
return res;
|
|
endif
|
|
clear_event_queue();
|
|
|
|
MoveObjectToLocation( npc1, item.x + 8, item.y, item.z );
|
|
if ( !( evs := collect_events( 1, "no event after leaving the area" ) ) )
|
|
return evs;
|
|
endif
|
|
var left_ev := evs.filter( @( ev ) { return ev.type == SYSEVENT_LEFTAREA; } )[1];
|
|
if ( !left_ev )
|
|
return ret_error( $"no left area event: {evs}" );
|
|
endif
|
|
if ( left_ev.source != npc1 )
|
|
return ret_error( $"wrong left area event: {left_ev}" );
|
|
endif
|
|
if ( ( evs := collect_events( 1, "", 1 ) ) )
|
|
return ret_error( $"the area event did not replace the opponent move: {evs}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// an item can be attacked by more than one mobile, and destroying it has to
|
|
// release all of them
|
|
exported function attack_check_two_attackers( resmgn )
|
|
var npc1;
|
|
var npc2;
|
|
var item;
|
|
var res := reset( resmgn, npc1, item );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
|
|
npc2 := resmgn.CreateNPCFromTemplate( ":testattack:test_attack", 52, 100, 0 );
|
|
if ( !npc2 )
|
|
return ret_error( "Could not create NPC: " + npc2 );
|
|
endif
|
|
npc2.hitchance_mod := 1000;
|
|
SetObjProperty( npc2, "#TestPid", GetPid() );
|
|
SetName( npc2, "npc2" );
|
|
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
npc2.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
|
|
// both fights are running, so the event queue keeps filling up with hits:
|
|
// poll the hostile list instead of waiting for events
|
|
if ( !( res := wait_for_hostiles( item, 2 ) ) )
|
|
return res;
|
|
endif
|
|
if ( npc1.opponent != item || npc2.opponent != item )
|
|
return ret_error( $"wrong opponents: {npc1.opponent}, {npc2.opponent}" );
|
|
endif
|
|
|
|
if ( !( res := DestroyItem( item ) ) )
|
|
return ret_error( "Could not destroy the item: " + res );
|
|
endif
|
|
|
|
if ( npc1.opponent )
|
|
return ret_error( $"npc1 still attacks the destroyed item: {npc1.opponent}" );
|
|
endif
|
|
if ( npc2.opponent )
|
|
return ret_error( $"npc2 still attacks the destroyed item: {npc2.opponent}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// selecting the same item twice must not repeat the engaged event
|
|
exported function attack_check_reengage_same( resmgn )
|
|
var npc1;
|
|
var item;
|
|
var evs;
|
|
var res := reset( resmgn, npc1, item );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
|
|
// out of the weapon range, so that no swings interfere
|
|
MoveObjectToLocation( npc1, item.x + 3, item.y, item.z );
|
|
clear_event_queue();
|
|
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
if ( !( evs := collect_events( 1, "no event after engaged" ) ) )
|
|
return evs;
|
|
endif
|
|
if ( !evs.filter( @( ev ) { return ev.type == SYSEVENT_ENGAGED; } )[1] )
|
|
return ret_error( $"no engaged event: {evs}" );
|
|
endif
|
|
|
|
clear_event_queue();
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := item,
|
|
sleep := 0, unhide := false } );
|
|
if ( ( evs := collect_events( 1, "", 2 ) ) )
|
|
return ret_error( $"reselecting the same opponent sent an event: {evs}" );
|
|
endif
|
|
if ( npc1.opponent != item )
|
|
return ret_error( $"wrong opponent selected: {npc1.opponent}" );
|
|
endif
|
|
if ( ListHostiles( item ).size() != 1 )
|
|
return ret_error( $"Hostile list != 1: {ListHostiles( item )}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
function reset( resmgn, byref npc1, byref item, itemtype := "AttackableItem" )
|
|
npc1 := resmgn.CreateNPCFromTemplate( ":testattack:test_attack", 50, 100, 0 );
|
|
|
|
if ( !npc1 )
|
|
return ret_error( "Could not create NPC: " + npc1 );
|
|
endif
|
|
|
|
npc1.hitchance_mod := 1000;
|
|
SetObjProperty( npc1, "#TestPid", GetPid() );
|
|
SetName( npc1, "npc1" );
|
|
|
|
item := resmgn.CreateItemAtLocation( 51, 100, 0, itemtype );
|
|
|
|
if ( !item )
|
|
return ret_error( "Could not create item: " + item );
|
|
endif
|
|
SetObjProperty( item, "#TestPid", GetPid() );
|
|
clear_event_queue();
|
|
return 1;
|
|
endfunction
|
|
|
|
// Replaces the event registration of the control script of an item
|
|
function configure_control( item, events, range, evmask )
|
|
item.process.sendevent( struct{ type := "configure", events := events, range := range,
|
|
evmask := evmask } );
|
|
|
|
var deadline := ReadMillisecondClock() + 5000;
|
|
while ( ReadMillisecondClock() < deadline )
|
|
var ev := wait_for_event( 1 );
|
|
if ( ev && ev.type == "configured" )
|
|
return 1;
|
|
endif
|
|
endwhile
|
|
|
|
return ret_error( "control script did not apply the configuration" );
|
|
endfunction
|
|
|
|
// Waits until the given number of mobiles attacks the item. Deadline based,
|
|
// since a running fight keeps sending events that would have to be skipped.
|
|
function wait_for_hostiles( item, count, timeout := 5 )
|
|
var deadline := ReadMillisecondClock() + timeout * 1000;
|
|
var hostiles;
|
|
|
|
while ( ReadMillisecondClock() < deadline )
|
|
hostiles := ListHostiles( item );
|
|
if ( hostiles.size() == count )
|
|
return 1;
|
|
endif
|
|
sleepms( 100 );
|
|
endwhile
|
|
|
|
return ret_error( $"Hostile list != {count}: {hostiles}" );
|
|
endfunction
|
|
|
|
// Collect a number of events
|
|
function collect_events( count, errorMessage := "No event from NPC script received.", timeout := 5 )
|
|
var evs := array{};
|
|
|
|
while ( true )
|
|
var ev := wait_for_event( timeout );
|
|
if ( ev )
|
|
evs.append( ev );
|
|
if ( evs.size() >= count )
|
|
break;
|
|
endif
|
|
continue;
|
|
endif
|
|
|
|
return ret_error( errorMessage );
|
|
endwhile
|
|
|
|
return evs;
|
|
endfunction
|
|
|