forums | blogs | polls | tutorials | downloads | rules | help

Error message

Deprecated function: The each() function is deprecated. This message will be suppressed on further calls in remember_me_form_alter() (line 78 of /var/www/siegetheday.org/sites/all/modules/contrib/remember_me/remember_me.module).

Attack classes, item mana/ammo/fuel, and punching arrows.

I've been modding the templates to re-balance combat in a way that reduces weapon redundancy, while widening the options for increasing each skill (melee and ranged), making mage staves useful for increasing magic skills, etc.

So far, changing dagger class weapons to improve ranged skill worked flawlessly. If i get to higher levels, I can likely see the blunt daggers' stronger, magical versions drop, etc. etc.

The next step would be reworking bow use. At Best in Slot levels for levels 0, 1, and 2, both melee and ranged weapons have the same average DPS, all the while ranged characters don't have to worry about pesky retaliation and miss chances. To amend this and introduce a form of resource management for both skills, I've decided to copy the mana/ammo/fuel system of goblin guns, and this way make ranged weapons into "burst" damage options for both classes (the plan is to make crossbows work with melee skill, as of now)

I've tested with the peasant short bow, and it works behind the scenes, just didn't show the ammo. After several tries, I changed it's attack class to ac_minigun, and voila, success.

Except instead of shooting the arrows, my test girl punches them out of her fists.
And she shoots all her mana.
And it looks like a shotgun.

the templates so far:

[t:template,n:bw_g_d_s_s_c_avg]
{
	category_name = "weapon";
	doc = "Peasant Short Bow";
	specializes = base_bow;
	[aspect]
	{
		mana = 100;
		mana_recovery_period = 1;
		mana_recovery_unit = 10;
		max_mana = 100;
		model = m_w_bow_001;
	}
	[attack]
	{
		attack_class = ac_minigun;
		attack_range = 10;
		damage_max = 6;
		damage_min = 3;
		reload_delay = 0;
	}
	[common]
	{
        	allow_modifiers = false;
		screen_name = "Peasant Short Bow";
	}
	[gui]
	{
		inventory_height = 3;
		inventory_icon = b_gui_ig_i_w_bow_051;
		inventory_width = 1;
	}
	[minigun_magic]
	{
		mana_per_shot = 20.0;
		use_mana = true;
	}
}

edited reload delay: seems to solve the shotgun problem, though still punching too much. Can I make a custom attack class?

boromonokli wrote:
edited reload delay: seems to solve the shotgun problem, though still punching too much. Can I make a custom attack class?

Not really sure to be honest.

However your punching problem is due to the game thinking you are using a crossbow type weapon, miniguns also fall into this category. If you examine wpn_bases.gas found in the interactive folder in Logic.ds2res, you'll see that bows have an attack_class of ac_bow while crossbows use ac_minigun. This is what calls the particular weapon animation that the character will display according to this list;

fs0 = barehanded/bite/claw, etc
fs1 = single handed weapon
fs2 = single handed weapon plus a shield
fs3 = shaft handled 2-handed weapon (battle-axe, halberd, etc)
fs4 = short handled 2-handed weapon (swords, etc)
fs5 = staff
fs6 = bow
fs7 = crossbows (miniguns, etc)
fs8 = shield only
fs9 = dual wielding single handed weapons
fs10 = throwing weapons

So your new template is calling fs7 when you want it to call fs6 animations.

Equip_slot is also important for the weapon to look natural, bows uses es_shield_hand while crossbows use es_weapon_hand.

Have you tried using minigun_magic while keeping the attack_class at ac_bow? Maybe that might work? Possibly you can make melee weapons that use mana as well for extra effects? Such as staffs?

That was the default idea. It worked, but it didn't display the yellow ammo/fuel/mana bar next to the weapon's active icon, or the fuel tooltip in inventory rollover.

So far, it looks like most of the minigun_magic is designed to work with ac_minigun, which defaults to ac_beastfu/FS0, ignoring attack speed. Additionally, using it with ac_bow makes a mess of the regular animation (desyncs after first shot, needs delay or does funny shotbow-gun stuff)

I had more success with crossbows


You're shooting the wrong way!!!


finally

(oh yep, shock-wave explosion added as break effect. looks good)

trying to upload new results. It's still somewhat buggy. I'll try to consolidate it with spell_multiple_hit some more tomorrow.


/////////////////////////////////////////////////////////////////////////////
//
// File     : weapon_attack_area.skrit
// Author(s): Boro (and Witness, and Eric Tams too)
// Purpose  : checks if there are enemies in the area, and hits them too!!!
//
// Version  : blah
//
//----------------------------------------------------------------------------
//  Version: too low             Date: January, 2015
//----------------------------------------------------------------------------
//////////////////////////////////////////////////////////////////////////////

property int		enchantments$		= 0		doc = "";
//property float		hit_multiplier$ 	= 1.0		doc = "";
property string		effect_script$	= ""		doc = "";
property string		effect_params$	= ""		doc = "";
property string 	hit_script$		= ""	doc = "Name of the SiegeFx script that will be providing the visual target explodes.";
property int		maxhits$		= 2		doc = "";
property string description$		= ""	doc = "Description of enchantment being applied";
property bool		attack_unconscious$	= true		doc = "";
property float  	max_range$		= 5.0	doc = "Maximum distance between target for hit to occur";

property eWorldEvent	msg_hit_begin$	= WE_REQ_ACTIVATE	doc = "";
property eWorldEvent	msg_hit$	= WE_REQ_CAST		doc = "";
property eWorldEvent	msg_hit_end$	= WE_REQ_DEACTIVATE	doc = "";

owner = GoSkritComponent;

//////////////////////////////////////////////////////////////////////////////
// GLOBALS
//////////////////////////////////////////////////////////////////////////////

Goid wielder$, target$;
SFxSID	active_effect$ = 0;

void ToggleEnchantments$ {
	int index$ = enchantments$;
	while ( index$ > 0 ) {
		owner.Go.Magic.SApplyEnchantmentsByName( wielder$, wielder$, MakeStringF( "%s_%d", owner.Go.Magic.StateName, index$ ) );
		index$ -= 1;
	}
}

//////////////////////////////////////////////////////////////////////////////
// STATES
//////////////////////////////////////////////////////////////////////////////

startup State Wait$ {

	event OnEnterState$
	{
			report.generic("Something\n");
			if ( owner.Go.IsEquipped ) {
			PostWorldMessage( WE_EQUIPPED, owner.Goid, owner.Goid, 0.0 );
			return;
		}
	}
	event OnGoHandleMessage$( eWorldEvent e$, WorldMessage msg$ ) {
		 if ( e$ == msg_hit_begin$) {
			wielder$ = msg$.GetSendFrom();
			target$ = wielder$.Go.Mind.EngagedObject;
			//report.reportf("Kinda Tired\n");
			
			if ( !wielder$.IsValid || !target$.IsValid ) {
				return;
			}
			if( effect_script$ != "")
			{
//				report.generic("removing effect!!\n");
				active_effect$ = SiegeFx.SRunMpScript( effect_script$, wielder$, wielder$, effect_params$, owner.Goid, e$ );
			}
			SetState Attack$;
		}
		else if ( e$ == WE_EQUIPPED )
		{
			if( effect_script$ != "")
			{
//				report.generic("removing effect!!\n");
				active_effect$ = SiegeFx.SRunMpScript( effect_script$, wielder$, wielder$, effect_params$, owner.Goid, e$ );
			}
		}
	}
}

State Attack$ {
	float target_life$;

	event OnEnterState$ {
		target_life$ = target$.Go.Aspect.CurrentLife;
		
		//apply enchantments
		//ToggleEnchantments$;
	}
	
	
	event OnGoHandleMessage$( eWorldEvent e$, WorldMessage ) {
		if ( e$ == msg_hit$ ) {
			if ( !wielder$.IsValid || !target$.IsValid ) {
				SetState Wait$;
				return;
			}

			Goid Weapon$ = owner.Goid;
			Goid HitWeapon$;
			wielder$.Go.Mind.TempGopColl1.Clear;
			wielder$.Go.Mind.GetEnemiesInSphere( max_range$, wielder$.Go.Mind.TempGopColl1 );
			int i$ = 0;
							int num_hit$ = 0;
							Go temp$;
							while(i$ < wielder$.Go.Mind.TempGopColl1.Size())//&& (num_hit$ < maxhits$))
							{
								report.generic("found someone to kill.\n");
								temp$ = wielder$.Go.Mind.TempGopColl1.Get( i$ );
								if( temp$.Goid.IsValidMp && ( temp$.Goid != target$ ) )
								{
									HitWeapon$ = Goid.InvalidGoid;
									
									if ( temp$.HasInventory ) {	
										Go temp_weapon$ = temp$.Inventory.SelectedWeapon;
										if( temp_weapon$ != NULL )
										{
											HitWeapon$ = temp$.Inventory.SelectedWeapon.goid;
										}
									}
									
									if (!HitWeapon$.IsValid) {
										HitWeapon$ = temp$.goid;
									}
									
									//if( Rules.CanHit( wielder$, temp$.goid, Weapon$, HitWeapon$ ) )
									//{
										SiegeFx.SRunScript( hit_script$, temp$.goid, wielder$, effect_params$, owner.Goid, e$ );
										Rules.DamageGoMelee( wielder$, Weapon$, temp$.goid );
										//report.generic("hit.\n");
									//} else {
										//report.generic("miss.\n");
									//}
									num_hit$ += 1;
								}

						        i$ += 1;
						    }	
		
							if ( e$ == msg_hit_end$ ) {
								SetState Wait$;
							}
		}
	}
	event OnExitState$ {
		//remove enchantments
		//ToggleEnchantments$;
			if (active_effect$ != 0)
			{
//				report.generic("removing effect!!\n");
				SiegeFx.SStopScript( active_effect$ );
			}
	}
}

boromonokli wrote:
trying to upload new results. It's still somewhat buggy. I'll try to consolidate it with spell_multiple_hit some more tomorrow.

Very nice work. Shows there's still a lot of untapped potential for modding the game.

This would go great with the Siege of the Three Kingdoms weapon pack. If you're unfamiliar with that mod, it was originally made by theresnothinglft for DS1.
It included a lot of oversize melee weapons that had AOE properties. I was able to port those weapons to DS2 but not unfortunately the AOE properties.

... but trying to figure out which bit will start an effect when the object is equipped (like weapon effects from the base game, sparkles and the like)

and semester started: Hooray for 6 times as much coding as I did before (in c++).

and maths. I gotta love the maths. (*faints*)

edit: can't find siege of the three kingdoms, the last link I saw on SN is broken... Will look into it whenever I can get to the university, as there's likely to be no net at home for the coming month.

Edit2: Okay, script on equip works, and on top of that I have hooked it up to the godb's watching (and unequipping works the same way). I wonder if I can use it to fix the shields adding their bonuses (block chance & magical alterations) when using 2h weapons.

And breakthrough: Hit detection works flawlessly.

Last time I made it, I used Lisah's weapon_attack_notify.skrit, which was a litte bit buggy when it came to detecting actual hits done to the target. Changing it around (adding a timer and checking health difference between a weapon swing's start and end) seems to work well. I can detect misses (when no damage is done), and enemy deaths just as well.

All there is to do is to cut in the damage code from spell_multiple_hit.skrit and it'll be perfect (actually, I can do anything: delayed damage, damage stretched out in time (physics.createdamagevolume), and instant damage)

and as always: here's the code.


/////////////////////////////////////////////////////////////////////////////
//
// File     : weapon_attack_area.skrit
// Author(s): Boro (Imre Szendrői)
// Purpose  : checks if there are enemies in the area, and hits them too!!!
//
// Version  : 0.3 [January 10, 2015]
//				- Re-implemented a version of Witness' weapon_attack_notify_melee.skrit that actually detects if the wielder hits something.
//				- detects whether the enemy is dead, damaged, or undamaged after the hit.
//			  0.2 [January 9, 2015]
//				- Weapon does not add additional active effects when re-equipped.
//			  0.1 [January 9, 2015]
//				- Implemented Witness' equip_joint script's part that detects if the weapon is equipped or starts as equipped,
//				- Weapon starts an effect script if equipped.
//	     
//
// Notes:	: Script based on work by Witness (Lisa Hui) and Eric Tams.
//
//----------------------------------------------------------------------------
//  Version: 1.2              Date: October 25, 2004
//----------------------------------------------------------------------------
//////////////////////////////////////////////////////////////////////////////

property string		effect_script$	= ""		doc = "";
property string		effect_params$	= ""		doc = "";
property string 	hit_script$		= ""	doc = "Name of the SiegeFx script that will be providing the visual on hit.";
property int		maxhits$		= 2		doc = "";
property float  	max_range$		= 5.0	doc = "Maximum distance between target for hit to occur";

property string		description$	= "" doc = "";

owner = GoSkritComponent;

//////////////////////////////////////////////////////////////////////////////
// GLOBALS
//////////////////////////////////////////////////////////////////////////////

Goid wielder$, target$;
SFxSID	active_effect$ = 0;
bool active$ = false;
float target_life$;

//////////////////////////////////////////////////////////////////////////////
// STATES
//////////////////////////////////////////////////////////////////////////////

startup State Init$ {
	event OnEnterState$ {
		if ( IsInGame( WorldState.CurrentState ) ) {
			SetState Listen$;
		}
	}
	
	transition -> Init$ : OnGoHandleMessage( WE_WORLD_STATE_TRANSITION_DONE );
}

State Listen$ {
	event OnEnterState$ {
		if ( IsServerLocal() && owner.Go.IsEquipped() && !active$  ) {
			PostWorldMessage( WE_EQUIPPED, owner.Goid, owner.Goid, 0.0 );
		}
	}
	trigger OnGoHandleMessage$( WE_EQUIPPED ) {
		if ( owner.Go.Parent == NULL ) {
			return;
		}
		wielder$ = owner.Go.Parent.Goid;
		if ( !active$ )
		{
			GoDb.StartWatching( owner.Goid, wielder$);
			active_effect$ = Siegefx.SRunScript(effect_script$, owner.goid, wielder$, "", owner.goid, WE_EQUIPPED);
			active$ = true;
		}
		
	}
	trigger OnGoHandleMessage$( WE_UNEQUIPPED ) {
		Siegefx.SStopScript( active_effect$ );
		GoDb.StopWatching( owner.Goid, wielder$);
		active$ = false;
		//SetState Init$;
	}
	
	//adding handler for attacking
	//eWorldEvent e$;
	event OnGoHandleCCMessage$( eWorldEvent e$, WorldMessage msg$ ) {
		if ( e$ == WE_MCP_FACING_LOCKEDON ) {
			//if wielder is about to attack, the mind should have a valid engaged object for ProcessHitEngaged
			target$ = wielder$.Go.Mind.EngagedObject;
			
			if ( target$.IsValid ) {
				//fetch target$'s life
				target_life$ = target$.Go.Aspect.CurrentLife;
			}
		}
		else if ( e$ == WE_ENGAGED_HIT_KILLED ) {
			if ( msg$.GetSendTo() == wielder$ ) {
				//SiegeFx.SRunScript( "shock_wave", target$, target$, effect_params$, owner.Goid, WE_REQ_ACTIVATE );
			}
		}
		else if ( e$ == WE_ANIM_WEAPON_FIRE ) {
			if ( !target$.IsValid ) {
				return;
			}
			//start a short timer
			this.CreateTimer( 1, 0.1 );
			//SiegeFx.SRunScript( "shock_wave", target$, target$, effect_params$, owner.Goid, WE_REQ_ACTIVATE );
		}
	}
	trigger OnTimer$( 1 ) {
		//check if the target didn't die since swinging. if it died, the WE_ENGAGED_HIT_KILLED will be the part to run.
		if ( !target$.IsValid ) {
			return;
		}
		//check if the target wasn't damaged, because that's another case to handle
		if(target_life$ == target$.Go.Aspect.CurrentLife)
		{
				return;
		}
		//and now we are pretty sure that the target is hit, so run the script.
		else
		{
			SiegeFx.SRunScript( "shock_wave", target$, target$, effect_params$, owner.Goid, WE_REQ_ACTIVATE );
			target_life$ = target$.Go.Aspect.CurrentLife;
		}
	}
	

}

Araknuum's picture

MasterArcher1.0 by jjliaw applies an attack speed bonus to ranged attacks based on dexterity and ranged skill:
(the edited section is parsed by the author under {state RequestAction$})

/*
  ============================================================================
  ----------------------------------------------------------------------------

	File		: job_attack_object_ranged.skrit

	Author(s)	: Bartosz Kijanka

	Purpose		: Get close to a target (within weapon range) and shoot at it.

	[Expected Behavior]
	{
		// $$$ fill out
	}

	(C)opyright 2000 Gas Powered Games, Inc.

  ----------------------------------------------------------------------------
  ============================================================================
*/

property	float	persistence$					= 1.0	doc = "chance that this monster will continue to try to attack even if the target is beyond range.";

Go		m_Go$;
GoMind	m_Mind$;
GoBody  m_Body$;
Job		m_Job$;

Goid	m_Ammo$;
Goid	m_Target$;
float	m_RangeToTarget$;
Goid	m_Weapon$;
bool	trying_new_spot$;

////////////////////////////////////////////////////////////////////////////////

#include "k_job_c_mcp_attack_utils"
 
////////////////////////////////////////////////////////////////////////////////
 
startup state STARTUP$
{
}


////////////////////////////////////////////////////////////////////////////////
//	init
event OnJobInitPointers$( Job job$ )
{
	m_Job$			= job$;
	m_Go$			= job$.Go;
	m_Mind$			= job$.Go.Mind;
	m_Body$			= job$.Go.Body;
}

event OnJobInit$( Job job$ )
{
	OnJobInitPointers$( job$ );

	m_Target$		= job$.GoalObject;
	m_Weapon$		= job$.GoalModifier;
	m_Ammo$			= goid.InvalidGoid;

	if( m_Target$.Go.IsActor && !IsAlive( m_Target$.Go.Aspect.LifeState ) )
	{
		SetState Exiting$;
		return;
	}

	ResetPathFindingParameters$();

	SetState Begin$;
}


////////////////////////////////////////////////////////////////////////////////
//	global message handler
trigger OnWorldMessage$( WE_KILLED             ) { SetState Exiting$ ; }
trigger OnWorldMessage$( WE_ENGAGED_INVALID    ) { SetState Exiting$ ; }
trigger OnWorldMessage$( WE_ENGAGED_KILLED     ) { SetState Exiting$ ; }
trigger OnWorldMessage$( WE_ENGAGED_LOST       ) { SetState Exiting$ ; }
trigger OnWorldMessage$( WE_ENGAGED_HIT_KILLED ) { SetState Exiting$ ; }
trigger OnWorldMessage$( WE_DESTRUCTED         ) { SetState Exiting$ ; }
trigger OnWorldMessage$( WE_JOB_DESTRUCTED     ) { SetState Exiting$ ; }


////////////////////////////////////////////////////////////////////////////////
//	
state Begin$
{
	////////////////////////////////////////////////////////////////////////////////
	//	check standing orders

	event OnEnterState$
	{
		if( m_Target$.Go == NULL )
		{
			SetState Exiting$;
			return;
		}

		bool exit$ = true;

		if( m_Mind$.MovementOrders == MO_HOLD && !m_Job$.IsUserAssigned() )
		{
			if( m_Mind$.IsInRange( m_Target$.Go, m_Weapon$.Go, RL_EFFECTIVE_ATTACK_RANGE ) )
			{
				if( m_Mind$.IsLosClear( m_Target$.Go ) )
				{
					exit$ = false;
					SetState RequestAction$;
				}
			}
		}
		else if ( m_Mind$.CombatOrders == CO_HOLD && !m_Job$.IsUserAssigned() )
		{
		}
		else
		{
			exit$ = false;
			SetState ApproachingLOSPoint$;
		}

		if( exit$ )
		{
			SetState Exiting$;
		}
	}
}


////////////////////////////////////////////////////////////////////////////////
//	
state ApproachingLOSPoint$
{
	event OnEnterState$
	{
		if( m_Mind$.IsLosClear( m_Target$.Go ) )
		{
			report.reportf( "aiskrit", "'%s' - found LOS to target..\n", m_Go$.TemplateName );
			SetState( RequestAction$ );
		}
		else
		{
			report.reportf( "aiskrit", "'%s' - couldn't find LOS to target, calling for search.\n", m_Go$.TemplateName );
			if( AIQuery.FindClearLosPoint( m_Go$, m_Target$.Go, 25, Math.PiHalf*0.5, m_Mind$.TempPos1 ) )
			{
				m_RangeToTarget$ = 0;
				eReqRetCode ret$ = MCPManager.MakeRequest( m_Go$.Goid, PL_APPROACH, m_Mind$.TempPos1, m_RangeToTarget$ );

				report.ReportF(	"aimove","[%s] ApproachingLOSPoint (attack object ranged) [%s] returned [%s]\n",
							 	m_Go$.TemplateName,
								MakeSiegePosString(m_Mind$.TempPos1),
								ToString(ret$) );

				if (RequestFailed(ret$))
				{
					m_Job$.MarkForDeletion( JR_FAILED_NO_PATH );
					SetState( Exiting$ );
				}
				else
				{
					SetState( RequestAction$ );
				}
			}
			else
			{
				SetState( Exiting$ );
			}
		}
	}
}


////////////////////////////////////////////////////////////////////////////////
//	
state RequestAction$
{
	transition
	{
		-> TargetMoved$:		OnWorldMessage( WE_MCP_DEPENDANCY_BROKEN );
		-> HeadToHead$:			OnWorldMessage( WE_MCP_MUTUAL_DEPENDANCY );
		-> FireWithoutLoad$:	OnWorldMessage( WE_ANIM_WEAPON_FIRE );
		-> Exiting$:			OnWorldMessage( WE_MCP_INVALIDATED );
		-> RequestAction$:		OnWorldMessage( WE_JOB_TIMER_DONE );
	}
	trigger OnWorldMessage$( WE_ANIM_ATTACH_AMMO )
	{
		if( !trying_new_spot$ )
		{
			SetState Loading$;
		}
	}
	
	event OnEnterState$
	{
		if( m_Job$.EndRequested() )
		{
			SetState( Exiting$ );
			return;
		}

		trying_new_spot$ = false;
		
		// $$$ Need to revisit this because the animation should cause the loading of the arrow, but this is totally safe - Rick
		if( !m_Weapon$.Go.Attack.AmmoAppearsJIT )
		{
			m_Ammo$ = m_Weapon$.Go.Attack.SPrepareAmmo;
		}

//		eAnimStance stance$ = m_Go$.inventory.animstance;
// 		float attack_loop_duration$ = m_Weapon$.Go.Attack.ReloadDelay;
//		attack_loop_duration$ += m_Go$.Aspect.AspectHandle.Blender.GetBaseDuration(CHORE_ATTACK,stance$);

        //////////////////////////////////////////////////////////////
        // jjliaw modify begin: customize player's attack speed

        eAnimStance stance$ = m_Go$.inventory.animstance;
        float attack_loop_duration$ = m_Go$.Aspect.AspectHandle.Blender.GetBaseDuration(CHORE_ATTACK, stance$)
        attack_loop_duration$ += m_Weapon$.Go.Attack.ReloadDelay;

        float dex$ = m_Go$.Actor.GetSkillLevel("dexterity") - 10;
        float ranged$ = m_Go$.Actor.GetSkillLevel("ranged");
        float delaybase$ = 0.2;
        float spdup$ = 1 + dex$ * 0.008 + ranged$ * 0.002;
        spdup$ *= spdup$;
        
        attack_loop_duration$ = (attack_loop_duration$ - delaybase$) / spdup$ + delaybase$;

        // jjliaw modify end
        //////////////////////////////////////////////////////////////
                
  		m_Job$.EnterAtomicState( attack_loop_duration$ );
  
  		MCPManager.MakeRequest( m_Go$.Goid, PL_FACE, m_Target$ );

		eReqFlag reqFlags$ = ( m_Mind$.MovementOrders == MO_HOLD && !m_Job$.IsUserAssigned() ) ? REQFLAG_NOMOVE : REQFLAG_DEFAULT;
		
//		report.genericf("Approach Time: %g\n",m_MaxApproachTime$);
		
		m_RangeToTarget$ = m_Mind$.WeaponRange;
		eReqRetCode ret$ = MCPManager.MakeRequest(	m_Go$.Goid,
													PL_ATTACK_OBJECT_RANGED,
													attack_loop_duration$,
													m_Target$,
													m_LookAhead$, m_MaxApproachTime$, // path args
													m_RangeToTarget$,
													reqFlags$ );

		report.ReportF(	"aimove","[%s] PL_ATTACK_OBJECT_RANGED [%s] returned [%s]\n",
					 	m_Go$.TemplateName,
					 	m_Target$.go.TemplateName,
						ToString(ret$) );

		
		if( RequestFailed(ret$) )
		{
			if (ret$ == REQUEST_FAILED_OVERCROWDED)
			{
				SetState( OverCrowdedExit$ );
			}
			else
			{
				SetState( Exiting$ );
			}
		}
		else if (ret$ == REQUEST_OK_BEYOND_RANGE)
		{
//			report.generic("Beyond Range.\n");
			if( persistence$ < Math.RandomFloat(1) )
			{
				report.reportf( "aiskrit", "%s: giving up magic attack to find a new target.\n",m_Go$.TemplateName );
				m_Job$.MarkForDeletion( JR_OK );
				return;;
			}
			
			SetState( MovingCloser$ );
			return;
		}
		else if (ret$ == REQUEST_OK_CROWDED)
		{
//			report.generic("I'm trying to find a new point.\n");
				
			// bail if this is a player, the player should be able to attack, not run around trying to look pretty. -ET
			if( m_Go$.Player.Controller == PC_HUMAN )
			{
				SetState( ContinueAction$ );
				return;
			}
			
			// finding a new point.
			
			if( AIQuery.FindSpotRelativeToSource(	m_Go$,
													m_Target$.Go,
													false,
													2.5,
													4.5,
													70,
													70,
													2.0,
													m_Mind$.TempPos1,
													false ) )
			{
			  	m_RangeToTarget$ = 1.0;
				MCPManager.Flush( m_Go$.Goid, .5 );
				MCPManager.MakeRequest( m_Go$.Goid, PL_APPROACH, m_Mind$.TempPos1, m_RangeToTarget$ );
				m_Job$.SetTimer( .2 );
				trying_new_spot$ = true;
			}
			else
			{
				SetState( ContinueAction$ );
				return;
			}
		}		
		else // ret$ == OK
		{
//			report.generic("In Range.\n");
			SetState( ContinueAction$ );
		}
	}
}

////////////////////////////////////////////////////////////////////////////////
//	
state ContinueAction$
{
	// We arrive at this state after we receive a 'WE_MCP_MUTUAL_DEPENDANCY' message
	// or if the characters is within range and we want to ignore any WE_MCP_MUTUAL_DEPENDANCY

	transition
	{
		-> TargetMoved$:		OnWorldMessage( WE_MCP_DEPENDANCY_BROKEN );
		-> Loading$:			OnWorldMessage( WE_ANIM_ATTACH_AMMO );
		-> Begin$:				OnWorldMessage( WE_ANIM_DONE );  // no fire event found
		-> FireWithoutLoad$:	OnWorldMessage( WE_ANIM_WEAPON_FIRE );
		-> Exiting$:			OnWorldMessage( WE_MCP_INVALIDATED );
	}
}



////////////////////////////////////////////////////////////////////////////////
//	
state Loading$
{
	transition
	{
		-> Shooting$: OnWorldMessage( WE_ANIM_WEAPON_FIRE );
		-> RequestAction$: OnWorldMessage( WE_ANIM_DONE );
	}

	event OnEnterState$
	{
		if( !m_Weapon$.Go.Attack.AmmoAppearsJIT )
		{
			m_Ammo$ = m_Weapon$.Go.Attack.SPrepareAmmo;
		}
	}
}

////////////////////////////////////////////////////////////////////////////////
//	
state FireWithoutLoad$
{
	event OnEnterState$
	{
		//report.warningf("$$$$ The projectile attack for [%s] has no ATTACH AMMO event\n",m_Go$.TemplateName);

		if( !m_Weapon$.Go.Attack.AmmoAppearsJIT )	
		{
			m_Ammo$ = m_Weapon$.Go.Attack.SPrepareAmmo;
		}

		SetState(Shooting$);
	}
}

////////////////////////////////////////////////////////////////////////////////
//	
state Shooting$
{
	transition
	{
		-> Begin$:			OnWorldMessage( WE_ANIM_DONE );
	}

	event OnEnterState$
	{
		if( m_Weapon$.Go == NULL )
		{
			SetState Exiting$;
			return;
		}
		
		// launch arrow at engaged object

		if (m_Weapon$.Go.Aspect.MaxMana > 0.0)
		{
			SendWorldMessage( WE_REQ_ACTIVATE, m_Go$.Goid, m_Weapon$, MakeInt( m_Target$ ) );
		}
		else
		{
			// If JIT ammo creation then make it here otherwise it has already been created
			if( m_Weapon$.Go.Attack.AmmoAppearsJIT )
			{
				m_Ammo$ = m_Weapon$.Go.Attack.SPrepareAmmo;
			}

			if( !m_Ammo$.IsValidMp || !m_Target$.IsValidMp )
			{
//				report.errorf("%s is trying to attack ranged, but his %s INVALID!!\n", 
//					( ( m_Go$ == NULL ) ? "NULL ACTOR" : m_Go$.templatename ),
//					( ( !m_Target$.IsValidMp ) ? 
//							( ( !m_Ammo$.IsValidMp ) ? 
//									"Target and Ammo are" : 
//									"Target is" ) : 
//							( ( !m_Ammo$.IsValidMp ) ? 
//									"Ammo is" :
//									"Ammo or Target is" ) ) );
				SetState Exiting$;
				return;					
			}

			int firing_pos$ = SiegeFx.AddVariable( m_Weapon$.Go.Attack.ComputeFiringPos, m_Ammo$ );
			int target_pos$ = SiegeFx.AddVariable( m_Weapon$.Go.Attack.ComputeTargetPos( m_Target$ ), m_Ammo$ );
	
			m_Weapon$.Go.Attack.ComputeAimingError;
			float x_error$ = m_Weapon$.Go.Attack.GetAimingErrorX;
			float y_error$ = m_Weapon$.Go.Attack.GetAimingErrorY;
	
			float velocity$ = m_Weapon$.Go.Physics.GetVelocity;
	
			float aiming_angle$ = m_Weapon$.Go.Attack.ComputeAimingAngle( SiegeFx.GetVariable( firing_pos$, m_Ammo$ ), SiegeFx.GetVariable( target_pos$, m_Ammo$ ), velocity$ );
	
			m_Weapon$.Go.Attack.SLaunchAmmo( velocity$, SiegeFx.GetVariable( firing_pos$, m_Ammo$ ), SiegeFx.GetVariable( target_pos$, m_Ammo$ ), x_error$, y_error$, m_Target$ );
			
			m_Weapon$.Go.Attack.AlertRangedAttack( m_Target$ );
		}
	}
}


////////////////////////////////////////////////////////////////////////////////
//	
state CleaningUpAndExiting$
{
	transition
	{
		-> Exiting$:	OnWorldMessage( WE_ANIM_DONE );
	}
	
	event OnEnterState$
	{
		m_Job$.MarkCleaningUp();
	}
}


////////////////////////////////////////////////////////////////////////////////
//	
state Exiting$
{
	event OnEnterState$
	{
		m_Job$.LeaveAtomicState();

		if( m_Weapon$.Go != NULL )
		{
			MCPManager.MakeRequest( m_Go$.Goid, PL_FACE );
	
			// Need to make sure that the ranged weapon is still equipped if we are going to make another ammo			
			if( m_Go$.Inventory.IsRangedWeaponEquipped() )
			{	
				Go temp_weapon$ = m_Go$.Inventory.GetSelectedRangedWeapon();
				
				if( temp_weapon$ != NULL )
				{ 
					if ( m_Weapon$ == temp_weapon$.Goid )
					{
						if( !m_Weapon$.Go.Attack.AmmoAppearsJIT )
						{
							m_Ammo$ = m_Weapon$.Go.Attack.SPrepareAmmo;
						}
					}
				}
			}
	
			if (m_Weapon$.Go.Aspect.MaxMana > 0.0)
			{
				SendWorldMessage( WE_REQ_DEACTIVATE, m_Go$.Goid, m_Weapon$ );
			}
		}
		m_Job$.MarkForDeletion();
	}
}
	
state OverCrowdedExit$
{
	event OnEnterState$
	{
		m_Job$.LeaveAtomicState();

		if( m_Weapon$.Go != NULL )
		{
			MCPManager.MakeRequest( m_Go$.Goid, PL_FACE );
	
			// Need to make sure that the ranged weapon is still equipped if we are going to make another ammo
			if( m_Go$.Inventory.IsRangedWeaponEquipped() )
			{
				if( !m_Weapon$.Go.Attack.AmmoAppearsJIT )
				{
					m_Ammo$ = m_Weapon$.Go.Attack.SPrepareAmmo;
				}
			}
	
			if (m_Weapon$.Go.Aspect.MaxMana > 0.0)
			{
				PostWorldMessage( WE_REQ_DEACTIVATE, m_Go$.Goid, m_Weapon$, 0 );
			}
		}
		m_Job$.MarkForDeletion(JR_OK);
	}
}

The attribute/s and values the speed bonus is based on are maleable and I think possibly similar to how Crusader's MoreRange_Mod handles Ranged Attack Range increase based on Ranged Skill.

The Master Archer Mod also adds a new heroes.gas that simply tells the Master Archer char to look for the new job under Jobs:

[t:template,n:hero]
{
    doc = "This is a hero that the actor can choose at the start of game";
    specializes = actor_good;
    [actor]
    {
        can_level_up = true;
        race = human;
        [skills]
        {
            strength    =   0, 0, 10;
            intelligence=   0, 0, 10;
            dexterity   =   0, 0, 10;
        }
    }
    [aspect]
    {
        life_recovery_unit   = 1;
        life_recovery_period = 4;

        mana_recovery_unit   = 1;
        mana_recovery_period = 3;

    }
    [attack]
    {
        ammo_attach_bone = weapon_grip;
        attack_range = 0.5;
        reload_delay = 0;
        damage_max = 0;
        damage_min = 0;

    }
    [guts_manager]
    {
        effect_name = melee_hit_2;
    }
    [common]
    {
        membership = hero;
        auto_expiration_class = never;
        forced_expiration_class = never;
    }
    [inventory]
    {
        grid_height = 13;
        grid_width = 4;
    }
    [mind]
    {
        /*
            In my mind's eye, I see... a bunch of skrit code.
        */

        ////////////////////////////////////////
        //  jobs

        jat_brain   = world\ai\jobs\common\brain_hero.skrit;
        jat_listen  = world\ai\jobs\common\job_listen.skrit;

        jat_attack_object_ranged = world\ai\jobs\common\job_attack_object_MasterArcher.skrit;

        ////////////////////////////////////////
        //  params

        melee_engage_range              = 0.9;
        ranged_engage_range             = 5.5;
        sensor_scan_period              = 0.25;
        sight_range                     = 12.0;

        actor_life_ratio_low_threshold  = 0.5;
        actor_life_ratio_high_threshold = 0.8;
        actor_mana_ratio_low_threshold  = 0.5;
        actor_mana_ratio_high_threshold = 0.8;

        actor_weapon_preference         = WP_NONE;

        ////////////////////////////////////////
        //  permissions

        combat_orders                              = CO_LIMITED;
        movement_orders                            = MO_LIMITED;

        actor_auto_switches_to_magic               = false;
        actor_auto_defends_others                  = true;
        actor_auto_heals_others_life               = true;
        actor_auto_switches_to_karate              = false;
        actor_auto_switches_to_melee               = false;

        on_enemy_entered_icz_switch_to_melee       = false;
        on_engaged_lost_consciousness_abort_attack = false;
    }

    [physics]
    {
        fire_effect = human_physics_fire;
        fire_resistance = 0.0;
        fire_burn_threshold = 999;
        elasticity = 0.2;
    }
}

I had been looking for more info on attack_class and how to scale or outright change attack speeds for every non-ammo weapon, it seems that attack class names are well hidden and that animation time, and thus, attack speed are locked to the class without this kind of work-around.

I'm still barely getting my feet wet again, so, not sure at all if this pertains. But I saw this thread and wanted very much to try and continue this discussion.

Maybe the melee skrit can help me with the melee accuracies.