polserver/testsuite/pol/testpkgs/attack/test_npc_attack.src
turleypol e6abc8fcfe
Attackable item (#884)
* 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
2026-07-30 20:27:36 +02:00

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