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
246 lines
7 KiB
Text
246 lines
7 KiB
Text
use uo;
|
|
use os;
|
|
|
|
include "testutil";
|
|
include "sysevent";
|
|
|
|
var npc1, npc2;
|
|
|
|
program npc_ai()
|
|
npc1 := CreateNPCFromTemplate( ":testattack:test_attack", 100, 100, 0 );
|
|
|
|
if ( !npc1 )
|
|
return ret_error( "Could not create NPC: " + npc1 );
|
|
endif
|
|
|
|
npc1.hitchance_mod := 1000;
|
|
SetObjProperty( npc1, "#TestPid", GetPid() );
|
|
SetName( npc1, "npc1" );
|
|
npc1.hidden := 1;
|
|
|
|
npc2 := CreateNPCFromTemplate( ":testattack:test_attack", 101, 101, 0 );
|
|
|
|
if ( !npc2 )
|
|
return ret_error( "Could not create NPC: " + npc2 );
|
|
endif
|
|
|
|
npc2.hitchance_mod := 1000;
|
|
SetObjProperty( npc2, "#TestPid", GetPid() );
|
|
SetName( npc2, "npc2" );
|
|
|
|
return 1;
|
|
endprogram
|
|
|
|
// Tests that when an attacker unhides, it will attack opponent.
|
|
// `npc1` hides, attacks `npc2`, waits 2 seconds, then unhides. Check that both
|
|
// NPCs attack each other at game clock time `t`, where `t` is the clock time where
|
|
// the NPC unhides.
|
|
exported function attack_on_unhide_attacker()
|
|
var err, evs, t;
|
|
|
|
reset_npcs();
|
|
|
|
// Hide npc1
|
|
npc1.hidden := 1;
|
|
|
|
// Send an event to npc1 to attack npc2 + unhide.
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := npc2,
|
|
sleep := 2, unhide := true } );
|
|
|
|
var initial_evs := collect_events( 1, "No event for NPC BEFORE_UNHIDE" );
|
|
if ( !initial_evs )
|
|
return initial_evs;
|
|
elseif ( initial_evs[1].type != "BEFORE_UNHIDE" )
|
|
return ret_error( "Unexpected first event type: expected 'BEFORE_UNHIDE', actual '{initial_evs[1].type}'" );
|
|
endif
|
|
|
|
// Collect two attack events
|
|
if ( !( evs := collect_events( 2 ) ) )
|
|
return evs;
|
|
endif
|
|
|
|
// The attack should happen when npc1 unhid.
|
|
t := initial_evs[1].gameclock;
|
|
|
|
if ( !validate_events( err, evs, t ) )
|
|
return err;
|
|
endif
|
|
|
|
// NPC and corpse cleanup handled in cleanup.src
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
// Tests that when an defender unhides, its attacker will still attack.
|
|
// `npc1` attacks `npc2`, `npc2` hides and clears opponent, waits 2 seconds, then unhides.
|
|
// Check that both NPCs attack each other (since `npc2` did NOT clear opponent) at game
|
|
// clock time `t`, where `t` is the clock time where the NPC unhides.
|
|
exported function attack_on_unhide_defender()
|
|
var err, evs, t;
|
|
|
|
reset_npcs();
|
|
|
|
// npc1 attacks npc2
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := npc2,
|
|
sleep := 0, unhide := false } );
|
|
// Collect two attack events
|
|
if ( !( evs := collect_events( 2 ) ) )
|
|
return evs;
|
|
endif
|
|
|
|
// Both attacks should happen at the same time
|
|
t := evs[1].gameclock;
|
|
|
|
if ( !validate_events( err, evs, t ) )
|
|
return err;
|
|
endif
|
|
|
|
// Hide npc2, clear its opponent, and sleep to reset npc1's swing timer.
|
|
npc2.hidden := 1;
|
|
npc2.process.sendevent( struct{ type := SYSEVENT_DISENGAGED } );
|
|
Sleep( 2 );
|
|
|
|
// Unhide npc2. Since npc1 is still attacking, both should get a hit.
|
|
npc2.hidden := 0;
|
|
|
|
// Both attacks should happen at the same time, when npc2 unhid.
|
|
t := ReadGameClock();
|
|
|
|
// Collect two attack events
|
|
if ( !( evs := collect_events( 2 ) ) )
|
|
return evs;
|
|
endif
|
|
|
|
if ( !validate_events( err, evs, t ) )
|
|
return err;
|
|
endif
|
|
|
|
// NPC and corpse cleanup handled in cleanup.src
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
exported function test_attack_range_mod()
|
|
var err, evs, t;
|
|
|
|
reset_npcs();
|
|
|
|
npc1.swing_speed_increase_mod := 1000;
|
|
npc2.swing_speed_increase_mod := 1000;
|
|
|
|
// Move npcs to five tiles apart
|
|
MoveObjectToLocation( npc1, 100, 100, 0 );
|
|
MoveObjectToLocation( npc2, 105, 100, 0 );
|
|
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := npc2,
|
|
sleep := 0, unhide := false } );
|
|
|
|
// Should _not_ have attack events until mod is set
|
|
Sleepms( 500 );
|
|
if ( evs := collect_events( 2, timeout := 0 ) )
|
|
return ret_error( $"Unexpectedly collected attack events when too far away: {evs}" );
|
|
endif
|
|
|
|
npc1.max_attack_range_increase_mod := 10;
|
|
// Need to re-attack...
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := npc2,
|
|
sleep := 0, unhide := false } );
|
|
|
|
// Collect two attack events
|
|
if ( !( evs := collect_events( 2 ) ) )
|
|
return evs;
|
|
endif
|
|
|
|
return 1;
|
|
endfunction
|
|
|
|
exported function attack_check_listhostiles()
|
|
var err, evs, t;
|
|
|
|
reset_npcs();
|
|
npc1.process.sendevent( struct{ type := "enable_events" } );
|
|
npc2.process.sendevent( struct{ type := "enable_events" } );
|
|
|
|
// npc1 attacks npc2
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_ENGAGED, source := npc2,
|
|
sleep := 0, unhide := false } );
|
|
|
|
// Collect two attack events
|
|
if ( !( evs := collect_events( 2 ) ) )
|
|
return evs;
|
|
endif
|
|
|
|
// Both attacks should happen at the same time
|
|
t := evs[1].gameclock;
|
|
|
|
if ( !validate_events( err, evs, t ) )
|
|
return err;
|
|
endif
|
|
|
|
var hostile1 := ListHostiles( npc1 );
|
|
var hostile2 := ListHostiles( npc2 );
|
|
if ( hostile1.size() != 1 || hostile2.size() != 1 )
|
|
return ret_error( $"Hostile list != 1: {hostile1.size()},{hostile2.size()}" );
|
|
endif
|
|
if ( hostile1[1] != npc2 || hostile2[1] != npc1 )
|
|
return ret_error( $"wrong opponent set: {hostile1[1]}, {hostile2[1]}" );
|
|
endif
|
|
return 1;
|
|
endfunction
|
|
|
|
/**
|
|
* Helper functions
|
|
*/
|
|
|
|
// Send event to both npcs to clear opponent.
|
|
function reset_npcs()
|
|
MoveObjectToLocation( npc1, 100, 100, 0 );
|
|
MoveObjectToLocation( npc2, 101, 101, 0 );
|
|
npc1.hidden := 0;
|
|
npc2.hidden := 0;
|
|
npc1.max_attack_range_increase := 0;
|
|
npc2.max_attack_range_increase := 0;
|
|
npc1.swing_speed_increase_mod := 0;
|
|
npc2.swing_speed_increase_mod := 0;
|
|
npc1.process.sendevent( struct{ type := "disable_events" } );
|
|
npc2.process.sendevent( struct{ type := "disable_events" } );
|
|
npc1.process.sendevent( struct{ type := SYSEVENT_DISENGAGED } );
|
|
npc2.process.sendevent( struct{ type := SYSEVENT_DISENGAGED } );
|
|
clear_event_queue();
|
|
endfunction
|
|
|
|
// Ensure gameclock and attacker-defender sequence of two events.
|
|
function validate_events( byref err, byref evs, t )
|
|
var failure := ( evs[1].gameclock != t ) || // First hit did not happen on unhide.
|
|
( evs[2].gameclock != t ) || // Second hit did not happen on unhide.
|
|
( evs[1].attacker != evs[2].defender ) || // First hit attacker is not second hit defender
|
|
( evs[1].defender != evs[2].attacker ); // First hit defender is not second hit attacker
|
|
|
|
if ( failure )
|
|
return err := ret_error( $"Unexpected attacker-defender sequence. Expected both hits at gameclock {t}, attacker[1] == defender[2], defender[1] == attacker[2].\n\nEvents: {PackJSON(
|
|
evs, true )}" );
|
|
endif
|
|
|
|
return 1;
|
|
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
|
|
|