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
589 lines
19 KiB
Text
589 lines
19 KiB
Text
// script level api of attackable items: the module functions that were widened
|
|
// from "character" to "attackable", and the item flag semantics that come with
|
|
// the Attackable itemdesc entry.
|
|
use uo;
|
|
use os;
|
|
use polsys;
|
|
use vitals;
|
|
|
|
include "testutil";
|
|
include "sysevent";
|
|
|
|
program npc_ai()
|
|
return 1;
|
|
endprogram
|
|
|
|
// attack_once accepts an item, damages it and reports the new error cases
|
|
exported function api_attack_once( resmgn )
|
|
var npc := make_npc( resmgn );
|
|
if ( !npc )
|
|
return npc;
|
|
endif
|
|
var item := resmgn.CreateItemAtLocation( 51, 100, 0, "AttackableNoScript" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
|
|
// nothing is selected and nobody attacks the npc
|
|
var res := npc.attack_once();
|
|
if ( res != error )
|
|
return ret_error( $"attack_once without an opponent: {res}" );
|
|
elseif ( res.errortext != "No opponent" )
|
|
return ret_error( $"wrong error without an opponent: {res.errortext}" );
|
|
endif
|
|
|
|
SetObjProperty( npc, "#Damage", 10 );
|
|
clear_event_queue();
|
|
|
|
if ( ( res := npc.attack_once( item ) ) != 1 )
|
|
return ret_error( $"attack_once( item ) failed: {res}" );
|
|
endif
|
|
|
|
// the swing runs the attack hit script with the item as defender
|
|
var ev := wait_for_type( "HIT", "no hit event after attack_once" );
|
|
if ( !ev )
|
|
return ev;
|
|
elseif ( ev.attacker != npc || ev.defender != item )
|
|
return ret_error( $"wrong hit event: {ev}" );
|
|
endif
|
|
if ( item.hp != 90 )
|
|
return ret_error( $"item not damaged: {item.hp}" );
|
|
endif
|
|
|
|
// a single swing does not select an opponent
|
|
if ( npc.opponent )
|
|
return ret_error( $"attack_once selected an opponent: {npc.opponent}" );
|
|
endif
|
|
|
|
// an item without the flag is no target
|
|
var plain := resmgn.CreateItemAtLocation( 52, 100, 0, "NonAttackableItem" );
|
|
if ( !plain )
|
|
return ret_error( $"Could not create item: {plain}" );
|
|
endif
|
|
res := npc.attack_once( plain );
|
|
if ( res != error )
|
|
return ret_error( $"attack_once( plain item ): {res}" );
|
|
elseif ( res.errortext != "Opponent is not attackable" )
|
|
return ret_error( $"wrong error for a plain item: {res.errortext}" );
|
|
endif
|
|
|
|
// and neither is an attackable one inside a container
|
|
var backpack := resmgn.CreateItemAtLocation( 52, 100, 0, 0xE75 );
|
|
if ( !backpack )
|
|
return ret_error( $"Could not create container: {backpack}" );
|
|
endif
|
|
var contained := resmgn.CreateItemInContainer( backpack, "AttackableNoScript" );
|
|
if ( !contained )
|
|
return ret_error( $"Could not create item: {contained}" );
|
|
endif
|
|
res := npc.attack_once( contained );
|
|
if ( res != error )
|
|
return ret_error( $"attack_once( contained item ): {res}" );
|
|
elseif ( res.errortext != "Opponent is not attackable" )
|
|
return ret_error( $"wrong error for a contained item: {res.errortext}" );
|
|
endif
|
|
|
|
// the exact text is not checked here: a failing getUObjectParam leaves its
|
|
// own error behind, which overrides the "Invalid parameter type" of the
|
|
// method itself
|
|
res := npc.attack_once( 5 );
|
|
if ( res != error )
|
|
return ret_error( $"attack_once( 5 ): {res}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// SetOpponent takes attackable items and rejects everything else
|
|
exported function api_setopponent( resmgn )
|
|
var npc := make_npc( resmgn );
|
|
if ( !npc )
|
|
return npc;
|
|
endif
|
|
var item := resmgn.CreateItemAtLocation( 51, 100, 0, "AttackableNoScript" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
var plain := resmgn.CreateItemAtLocation( 52, 100, 0, "NonAttackableItem" );
|
|
if ( !plain )
|
|
return ret_error( $"Could not create item: {plain}" );
|
|
endif
|
|
|
|
var res := set_opponent( npc, plain );
|
|
if ( !res )
|
|
return res;
|
|
elseif ( res.result != 0 )
|
|
return ret_error( $"SetOpponent( plain item ) succeeded: {res.result}" );
|
|
elseif ( res.errortext != "Invalid parameter" )
|
|
return ret_error( $"wrong error for a plain item: '{res.errortext}'" );
|
|
endif
|
|
if ( npc.opponent )
|
|
return ret_error( $"a rejected object was selected: {npc.opponent}" );
|
|
endif
|
|
|
|
res := set_opponent( npc, item );
|
|
if ( !res )
|
|
return res;
|
|
elseif ( res.result != 1 )
|
|
return ret_error( $"SetOpponent( item ) failed: {res.errortext}" );
|
|
endif
|
|
if ( npc.opponent != item )
|
|
return ret_error( $"wrong opponent selected: {npc.opponent}" );
|
|
endif
|
|
|
|
// SetOpponent( 0 ) clears the opponent. Only the effect is checked: the
|
|
// failing getUObjectParam leaves its error behind, so the documented return
|
|
// value of 0 never reaches the script.
|
|
res := set_opponent( npc, 0 );
|
|
if ( !res )
|
|
return res;
|
|
endif
|
|
if ( npc.opponent )
|
|
return ret_error( $"opponent was not cleared: {npc.opponent}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// only attackable toplevel items can be damaged
|
|
exported function api_applydamage_errors( resmgn )
|
|
var plain := resmgn.CreateItemAtLocation( 51, 100, 0, "NonAttackableItem" );
|
|
if ( !plain )
|
|
return ret_error( $"Could not create item: {plain}" );
|
|
endif
|
|
|
|
var res := ApplyDamage( plain, 10 );
|
|
if ( res != error )
|
|
return ret_error( $"ApplyDamage( plain item ): {res}" );
|
|
elseif ( res.errortext != "Object is not damageable" )
|
|
return ret_error( $"wrong error for a plain item: {res.errortext}" );
|
|
endif
|
|
if ( plain.hp != 100 )
|
|
return ret_error( $"plain item was damaged: {plain.hp}" );
|
|
endif
|
|
|
|
res := ApplyRawDamage( plain, 10 );
|
|
if ( res != error )
|
|
return ret_error( $"ApplyRawDamage( plain item ): {res}" );
|
|
elseif ( res.errortext != "Object is not damageable" )
|
|
return ret_error( $"wrong raw error for a plain item: {res.errortext}" );
|
|
endif
|
|
|
|
// an attackable item inside a container is no target either
|
|
var backpack := resmgn.CreateItemAtLocation( 51, 100, 0, 0xE75 );
|
|
if ( !backpack )
|
|
return ret_error( $"Could not create container: {backpack}" );
|
|
endif
|
|
var contained := resmgn.CreateItemInContainer( backpack, "AttackableNoScript" );
|
|
if ( !contained )
|
|
return ret_error( $"Could not create item: {contained}" );
|
|
endif
|
|
res := ApplyDamage( contained, 10 );
|
|
if ( res != error )
|
|
return ret_error( $"ApplyDamage( contained item ): {res}" );
|
|
elseif ( res.errortext != "Object is not damageable" )
|
|
return ret_error( $"wrong error for a contained item: {res.errortext}" );
|
|
endif
|
|
if ( contained.hp != 100 )
|
|
return ret_error( $"contained item was damaged: {contained.hp}" );
|
|
endif
|
|
|
|
// the damage range check still applies to items
|
|
var item := resmgn.CreateItemAtLocation( 52, 100, 0, "AttackableNoScript" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
res := ApplyDamage( item, 30001 );
|
|
if ( res != error )
|
|
return ret_error( $"ApplyDamage( item, 30001 ): {res}" );
|
|
elseif ( res.errortext != "Damage is out of range" )
|
|
return ret_error( $"wrong error for too much damage: {res.errortext}" );
|
|
endif
|
|
if ( item.hp != 100 )
|
|
return ret_error( $"item was damaged: {item.hp}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// ApplyRawDamage subtracts the hitpoints of an item one to one, it is not
|
|
// scaled by 100 the way the hundredths of a mobile are
|
|
exported function api_applyrawdamage( resmgn )
|
|
var item := resmgn.CreateItemAtLocation( 51, 100, 0, "AttackableNoScript" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
|
|
var res := ApplyRawDamage( item, 10 );
|
|
if ( res != 1 )
|
|
return ret_error( $"ApplyRawDamage failed: {res}" );
|
|
endif
|
|
if ( item.hp != 90 )
|
|
return ret_error( $"item not damaged: {item.hp}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// more damage than hitpoints destroys an item that has no control script
|
|
exported function api_applydamage_overkill( resmgn )
|
|
var item := resmgn.CreateItemAtLocation( 51, 100, 0, "AttackableNoScript" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
|
|
// the requested damage is reported, not the amount of hitpoints that were left
|
|
var res := ApplyDamage( item, 500 );
|
|
if ( res == error )
|
|
return ret_error( $"ApplyDamage failed: {res.errortext}" );
|
|
elseif ( res != 500 )
|
|
return ret_error( $"unexpected damage reported: {res}" );
|
|
endif
|
|
if ( item )
|
|
return ret_error( $"item survived with {item.hp} hp" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// ListHostiles on items: rejections, range and the hidden/los flags
|
|
exported function api_listhostiles( resmgn )
|
|
var npc := make_npc( resmgn );
|
|
if ( !npc )
|
|
return npc;
|
|
endif
|
|
var item := resmgn.CreateItemAtLocation( 51, 100, 0, "AttackableNoScript" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
var plain := resmgn.CreateItemAtLocation( 52, 100, 0, "NonAttackableItem" );
|
|
if ( !plain )
|
|
return ret_error( $"Could not create item: {plain}" );
|
|
endif
|
|
|
|
var res := ListHostiles( plain );
|
|
if ( res != error )
|
|
return ret_error( $"ListHostiles( plain item ): {res}" );
|
|
elseif ( res.errortext != "Invalid parameter" )
|
|
return ret_error( $"wrong error for a plain item: {res.errortext}" );
|
|
endif
|
|
|
|
// an attackable item nobody attacks has an empty, but valid, list
|
|
res := ListHostiles( item );
|
|
if ( res == error )
|
|
return ret_error( $"ListHostiles( item ) failed: {res.errortext}" );
|
|
elseif ( res.size() )
|
|
return ret_error( $"unexpected hostiles: {res}" );
|
|
endif
|
|
|
|
if ( !( res := set_opponent( npc, item ) ) )
|
|
return res;
|
|
elseif ( res.result != 1 )
|
|
return ret_error( $"SetOpponent( item ) failed: {res.errortext}" );
|
|
endif
|
|
|
|
if ( ( res := ListHostiles( item ) ).size() != 1 )
|
|
return ret_error( $"Hostile list != 1: {res}" );
|
|
elseif ( res[1] != npc )
|
|
return ret_error( $"wrong hostile: {res[1]}" );
|
|
endif
|
|
|
|
// the npc stands one tile away, so it drops out of a range of 0
|
|
if ( ( res := ListHostiles( item, 0 ) ).size() )
|
|
return ret_error( $"Hostile list of range 0 != 0: {res}" );
|
|
endif
|
|
|
|
// both stand in the open, so the los filter keeps the npc
|
|
if ( ( res := ListHostiles( item, 20, LH_FLAG_LOS ) ).size() != 1 )
|
|
return ret_error( $"Hostile list with LH_FLAG_LOS != 1: {res}" );
|
|
endif
|
|
|
|
npc.hidden := 1;
|
|
if ( ( res := ListHostiles( item ) ).size() )
|
|
return ret_error( $"hidden hostile was listed: {res}" );
|
|
endif
|
|
if ( ( res := ListHostiles( item, 20, LH_FLAG_INCLUDE_HIDDEN ) ).size() != 1 )
|
|
return ret_error( $"Hostile list with LH_FLAG_INCLUDE_HIDDEN != 1: {res}" );
|
|
endif
|
|
npc.hidden := 0;
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// the flag shows up in the itemdesc struct
|
|
exported function api_itemdesc_attackable()
|
|
var desc := GetItemDescriptor( "AttackableItem" );
|
|
if ( !desc )
|
|
return ret_error( $"Could not get itemdesc: {desc}" );
|
|
elseif ( desc.Attackable != 1 )
|
|
return ret_error( $"Attackable != 1: {desc.Attackable}" );
|
|
endif
|
|
|
|
desc := GetItemDescriptor( "NonAttackableItem" );
|
|
if ( !desc )
|
|
return ret_error( $"Could not get itemdesc: {desc}" );
|
|
elseif ( desc.Attackable != 0 )
|
|
return ret_error( $"Attackable != 0: {desc.Attackable}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// attackable items never stack, even if their graphic says otherwise
|
|
exported function api_not_stackable( resmgn )
|
|
var backpack := resmgn.CreateItemAtLocation( 51, 100, 0, 0xE75 );
|
|
if ( !backpack )
|
|
return ret_error( $"Could not create container: {backpack}" );
|
|
endif
|
|
|
|
var attackable1 := resmgn.CreateItemInContainer( backpack, "AttackableStackable" );
|
|
var attackable2 := resmgn.CreateItemInContainer( backpack, "AttackableStackable" );
|
|
if ( !attackable1 || !attackable2 )
|
|
return ret_error( $"Could not create items: {attackable1}, {attackable2}" );
|
|
endif
|
|
|
|
var res := IsStackable( attackable1, attackable2 );
|
|
if ( res != error )
|
|
return ret_error( $"attackable items are stackable: {res}" );
|
|
elseif ( res.errortext != "That item type is not stackable." )
|
|
return ret_error( $"wrong error: {res.errortext}" );
|
|
endif
|
|
|
|
// the same graphic without the flag stacks as usual
|
|
var normal1 := resmgn.CreateItemInContainer( backpack, "NormalStackable" );
|
|
var normal2 := resmgn.CreateItemInContainer( backpack, "NormalStackable" );
|
|
if ( !normal1 || !normal2 )
|
|
return ret_error( $"Could not create items: {normal1}, {normal2}" );
|
|
endif
|
|
|
|
res := IsStackable( normal1, normal2 );
|
|
if ( res != 1 )
|
|
return ret_error( $"the same graphic without the flag does not stack: {res}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// a copy of an attackable item stays attackable
|
|
exported function api_clone_keeps_flag( resmgn )
|
|
var item := resmgn.CreateItemAtLocation( 51, 100, 0, "AttackableNoScript" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
|
|
var copy := CreateItemCopyAtLocation( 52, 100, 0, item );
|
|
if ( !copy )
|
|
return ret_error( $"Could not copy item: {copy}" );
|
|
endif
|
|
|
|
var res := check_copy( copy );
|
|
DestroyItem( copy );
|
|
return res;
|
|
endfunction
|
|
|
|
// an equipped item is no combat target, but becomes one again once dropped
|
|
exported function api_equipped_inert( resmgn )
|
|
var npc := make_npc( resmgn );
|
|
if ( !npc )
|
|
return npc;
|
|
endif
|
|
var shirt := resmgn.CreateItemAtLocation( 50, 100, 0, "AttackableShirt" );
|
|
if ( !shirt )
|
|
return ret_error( $"Could not create item: {shirt}" );
|
|
endif
|
|
|
|
// on the ground it is a valid target
|
|
var res := ApplyDamage( shirt, 10 );
|
|
if ( res == error )
|
|
return ret_error( $"item on the ground took no damage: {res.errortext}" );
|
|
elseif ( shirt.hp != 90 )
|
|
return ret_error( $"item not damaged: {shirt.hp}" );
|
|
endif
|
|
|
|
if ( !( res := EquipItem( npc, shirt ) ) )
|
|
return ret_error( $"Could not equip the item: {res}" );
|
|
endif
|
|
|
|
res := ApplyDamage( shirt, 10 );
|
|
if ( res != error )
|
|
return ret_error( $"equipped item took damage: {res}" );
|
|
elseif ( res.errortext != "Object is not damageable" )
|
|
return ret_error( $"wrong error for an equipped item: {res.errortext}" );
|
|
endif
|
|
if ( shirt.hp != 90 )
|
|
return ret_error( $"equipped item was damaged: {shirt.hp}" );
|
|
endif
|
|
|
|
res := ListHostiles( shirt );
|
|
if ( res != error )
|
|
return ret_error( $"equipped item is a combat target: {res}" );
|
|
endif
|
|
|
|
// and back on the ground it is attackable again
|
|
if ( !( res := MoveObjectToLocation( shirt, 50, 100, 0 ) ) )
|
|
return ret_error( $"Could not drop the item: {res}" );
|
|
endif
|
|
res := ApplyDamage( shirt, 10 );
|
|
if ( res == error )
|
|
return ret_error( $"dropped item took no damage: {res.errortext}" );
|
|
elseif ( shirt.hp != 80 )
|
|
return ret_error( $"dropped item not damaged: {shirt.hp}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// moving an item into another realm ends every fight it was part of
|
|
exported function api_realm_change_clears_opps( resmgn )
|
|
var npc := make_npc( resmgn );
|
|
if ( !npc )
|
|
return npc;
|
|
endif
|
|
var item := resmgn.CreateItemAtLocation( 51, 100, 0, "AttackableNoScript" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
|
|
var res := set_opponent( npc, item );
|
|
if ( !res )
|
|
return res;
|
|
elseif ( res.result != 1 )
|
|
return ret_error( $"SetOpponent( item ) failed: {res.errortext}" );
|
|
endif
|
|
if ( ListHostiles( item ).size() != 1 )
|
|
return ret_error( "npc did not engage the item" );
|
|
endif
|
|
|
|
if ( !( res := MoveObjectToLocation( item, 10, 11, -5, "britannia2",
|
|
MOVEOBJECT_FORCELOCATION ) ) )
|
|
return ret_error( $"Could not move the item: {res}" );
|
|
endif
|
|
|
|
if ( ( res := ListHostiles( item ) ).size() )
|
|
return ret_error( $"Hostile list != 0 after the realm change: {res}" );
|
|
endif
|
|
if ( npc.opponent )
|
|
return ret_error( $"npc still attacks the item in another realm: {npc.opponent}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// a refusing destroy script keeps an item alive, both when its hitpoints run
|
|
// out and when DestroyItem is called
|
|
exported function api_destroy_script_refusal( resmgn )
|
|
var item := resmgn.CreateItemAtLocation( 51, 100, 0, "AttackableGuardedItem" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
SetObjProperty( item, "#NoDestroy", 1 );
|
|
|
|
var res := ApplyDamage( item, 200 );
|
|
if ( res == error )
|
|
return ret_error( $"ApplyDamage failed: {res.errortext}" );
|
|
endif
|
|
if ( !item )
|
|
return ret_error( "item was destroyed although its destroy script refused" );
|
|
elseif ( item.hp != 0 )
|
|
return ret_error( $"item hp != 0: {item.hp}" );
|
|
endif
|
|
|
|
res := DestroyItem( item );
|
|
if ( res == error )
|
|
return ret_error( $"DestroyItem returned an error instead of 0: {res.errortext}" );
|
|
elseif ( res != 0 )
|
|
return ret_error( $"DestroyItem did not report the refusal: {res}" );
|
|
endif
|
|
if ( !item )
|
|
return ret_error( "item was destroyed against the refusal" );
|
|
endif
|
|
|
|
// without the guard it can be destroyed again
|
|
EraseObjProperty( item, "#NoDestroy" );
|
|
res := DestroyItem( item );
|
|
if ( res != 1 )
|
|
return ret_error( $"DestroyItem failed: {res}" );
|
|
endif
|
|
if ( item )
|
|
return ret_error( "item still exists" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// assigning hitpoints clamps into the storage range and never destroys the item
|
|
exported function api_hp_member( resmgn )
|
|
var item := resmgn.CreateItemAtLocation( 51, 100, 0, "AttackableNoScript" );
|
|
if ( !item )
|
|
return ret_error( $"Could not create item: {item}" );
|
|
endif
|
|
|
|
item.hp := -5;
|
|
if ( item.hp != 0 )
|
|
return ret_error( $"negative hp not clamped: {item.hp}" );
|
|
endif
|
|
if ( !item )
|
|
return ret_error( "assigning 0 hp destroyed the item" );
|
|
endif
|
|
|
|
item.hp := 100000;
|
|
if ( item.hp != 65535 )
|
|
return ret_error( $"hp not clamped to the storage range: {item.hp}" );
|
|
endif
|
|
|
|
item.hp := 100;
|
|
if ( item.hp != 100 )
|
|
return ret_error( $"hp not restored: {item.hp}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
/**
|
|
* Helper functions
|
|
*/
|
|
|
|
function make_npc( resmgn )
|
|
var npc := resmgn.CreateNPCFromTemplate( ":testattack:test_attack", 50, 100, 0 );
|
|
if ( !npc )
|
|
return ret_error( $"Could not create NPC: {npc}" );
|
|
endif
|
|
|
|
npc.hitchance_mod := 1000;
|
|
SetObjProperty( npc, "#TestPid", GetPid() );
|
|
SetName( npc, "npc1" );
|
|
clear_event_queue();
|
|
return npc;
|
|
endfunction
|
|
|
|
// lets the ai script call SetOpponent and returns its result
|
|
function set_opponent( npc, opponent )
|
|
npc.process.sendevent( struct{ type := "set_opponent", source := opponent } );
|
|
return wait_for_type( "set_opponent_result", "no result from SetOpponent" );
|
|
endfunction
|
|
|
|
// waits for an event of the given type. deadline based, since a running fight
|
|
// keeps sending hit events that have to be skipped.
|
|
function wait_for_type( evtype, errormessage, timeout := 5 )
|
|
var deadline := ReadMillisecondClock() + timeout * 1000;
|
|
|
|
while ( ReadMillisecondClock() < deadline )
|
|
var ev := wait_for_event( 1 );
|
|
if ( ev && ev.type == evtype )
|
|
return ev;
|
|
endif
|
|
endwhile
|
|
|
|
return ret_error( errormessage );
|
|
endfunction
|
|
|
|
function check_copy( copy )
|
|
var res := ApplyDamage( copy, 10 );
|
|
if ( res == error )
|
|
return ret_error( $"the copy is not attackable: {res.errortext}" );
|
|
elseif ( copy.hp != 90 )
|
|
return ret_error( $"the copy was not damaged: {copy.hp}" );
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|