aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_mortar_titans.gnut
blob: 08598808aca0051559efff23f2d2a7bb7428c48e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
untyped

global function MortarTitanThink
global function MortarTitans_Init

global function MortarTitanDeathCleanup
global function MortarMissileFiredCallback
global function MoveToMortarPosition

global function MortarTitanKneelToAttack

global function MortarTitanAttack

global function MortarTitanStopAttack

//global function MortarAIWaitToEngage

const float MORTAR_TITAN_ABORT_ATTACK_HEALTH_FRAC			= 0.90	// will stop mortar attack if he's health gets below 90% of his current health.
const float MORTAR_TITAN_POSITION_SEARCH_RANGE			= 1024 //3072	// How far away from his spawn point a mortar titan will look for positions to mortar from.
const float MORTAR_TITAN_ENGAGE_DELAY						= 3.0	// How long before a mortar titan start to attack the generator if he's taken damage getting to his mortar position.
const float MORTAR_TITAN_REENGAGE_DELAY					= 7.0	// How long before a mortar titan goes back to attacking the generator after breaking of an attack.

// --------------------------------------------------------------------
// MORTAR TITAN LOGIC
// --------------------------------------------------------------------

function MortarTitans_Init()
{
	RegisterSignal( "InterruptMortarAttack" )
	RegisterSignal( "BeginMortarAttack" )
}

void function MortarTitanDeathCleanup( entity titan )
{
	titan.EndSignal( "OnSyncedMeleeVictim" )
	titan.EndSignal( "OnDeath" )
	titan.EndSignal( "OnDestroy" )

	OnThreadEnd(
		function() : ( titan )
		{
			entity animEnt = titan.ai.carryBarrel

			if ( IsValid( animEnt ) )
				animEnt.Destroy()

			if ( IsAlive( titan ) )
			{
				titan.Signal( "InterruptMortarAttack" )
				titan.Anim_Stop()
			}
		}
	)

	WaitForever()
}

void function MortarMissileFiredCallback( entity missile, entity weaponOwner )
{
	thread MortarMissileThink( missile, weaponOwner )
}

void function MortarMissileThink( entity missile, entity weaponOwner )
{
	Assert( IsValid( missile ) )

	missile.EndSignal( "OnDestroy" )
	missile.EndSignal( "OnDeath" )

	if ( !IsValid( weaponOwner.ai.mortarTarget ) )
		return

	entity targetEnt = weaponOwner.ai.mortarTarget

	missile.DamageAliveOnly( true )
	missile.kv.lifetime = 6.0
	missile.s.mortar <- true
	vector startPos = missile.GetOrigin()

	// made a hacky way to get the mortar arc to go higher and still have it hit it's target.

	float dist = Distance( startPos, targetEnt.GetOrigin() )

	// radius tightens over time
	float radius = GraphCapped( Time() - weaponOwner.ai.spawnTime, 60.0, 180.0, 220, 100 )
	missile.SetMissileTarget( targetEnt, < RandomFloatRange( -radius, radius ), RandomFloatRange( -radius, radius ), 0 > )

	string sound = "weapon_spectremortar_projectile"
	if ( weaponOwner.IsTitan() )
		sound = "Weapon_FlightCore_Incoming_Projectile"

	EmitSoundAtPosition( weaponOwner.GetTeam(), targetEnt.GetOrigin(), sound )

	float homingSpeedMin = 10.0
	float homingSpeedMax = Graph( dist, 2500, 7000, 400, 200 )
	float estTravelTime = GraphCapped( dist, 0, 7000, 0, 5 )

	float startTime = Time()
	while( true )
	{
		float frac = min( 1, pow( ( Time() - startTime ) / estTravelTime, 2.0 ) )

	 	if ( frac > 1.0 )
	 		break

		float homingSpeed = GraphCapped( frac, 0, 1, homingSpeedMin, homingSpeedMax )

	 	missile.SetHomingSpeeds( homingSpeed, 0 )

	 	wait 0.25
	}

	missile.ClearMissileTargetPosition()
}

void function MoveToMortarPosition( entity titan, vector origin, entity target )
{
	titan.EndSignal( "OnSyncedMeleeVictim" )
	titan.EndSignal( "OnDeath" )
	titan.EndSignal( "OnDestroy" )

	titan.SetLookDistOverride( 320 )
	titan.SetHearingSensitivity( 0 )
	titan.EnableNPCMoveFlag( NPCMF_PREFER_SPRINT )

	local animEnt = titan.ai.carryBarrel

	local dir = target.GetOrigin() - origin
	local dist = dir.Norm()
	local angles = VectorToAngles( dir )
	angles.x = 0
	angles.z = 0

	float frac = TraceLineSimple( origin + < 0, 0, 32 >, origin + < 0, 0, -32 >, titan )
	if ( frac > 0 && frac < 1 )
		origin = origin + < 0, 0, 32 > - < 0, 0, 64 * frac >

	animEnt.SetOrigin( origin )
	animEnt.SetAngles( angles )

	float goalRadius = titan.GetMinGoalRadius()

	OnThreadEnd(
		function() : ( titan )
		{
			if ( !IsValid( titan ) )
				return

			local classname = titan.GetClassName()
			titan.DisableLookDistOverride()
			titan.SetHearingSensitivity( 1 )
			titan.DisableNPCMoveFlag( NPCMF_PREFER_SPRINT )
		}
	)

	local tries = 0
	while( true )
	{
		local dist = Distance( titan.GetOrigin(), origin )
		if ( dist <= goalRadius * 2 )
			break

		printt( "Mortar titan moving toward his goal", dist, tries++ )
		titan.AssaultPoint( origin )
		titan.AssaultSetGoalRadius( goalRadius )

		local result = WaitSignal( titan, "OnFinishedAssault", "OnEnterGoalRadius" )
	}
}

void function MortarTitanKneelToAttack( entity titan )
{
	titan.EndSignal( "OnSyncedMeleeVictim" )
	titan.EndSignal( "OnDeath" )
	titan.EndSignal( "OnDestroy" )

	entity animEnt = titan.ai.carryBarrel
	waitthread PlayAnim( titan, "at_mortar_stand2knee", animEnt )
}

function MortarTitanAttack( entity titan, entity target )
{
	titan.EndSignal( "OnSyncedMeleeVictim" )
	titan.EndSignal( "OnDeath" )
	titan.EndSignal( "OnDestroy" )
	titan.EndSignal( "InterruptMortarAttack" )

	OnThreadEnd(
		function() : ( titan )
		{
			if ( !IsValid( titan ) )
				return

			if ( "selectedPosition" in titan.s )
			{
				titan.s.selectedPosition.inUse = false
				delete titan.s.selectedPosition
			}

			if ( IsAlive( titan ) )
				thread MortarTitanAttackEnd( titan )
		}
	)

	titan.ai.mortarTarget = target
	entity animEnt = titan.ai.carryBarrel

	entity weapon = titan.GetActiveWeapon()

	while ( weapon.IsWeaponOffhand() )
	{
		WaitFrame()
		weapon = titan.GetActiveWeapon()
	}

	weapon.SetMods(  [ "coop_mortar_titan" ] )

	while( true )
	{
		waitthread PlayAnim( titan, "at_mortar_knee", animEnt )
	}
}

function MortarTitanAttackEnd( entity titan )
{
	titan.EndSignal( "OnDeath" )
	titan.EndSignal( "OnDestroy" )

	entity animEnt = titan.ai.carryBarrel

	// remove the mortar mod, we do this so that we don't get mortar sound and fx when firing normal
	entity weapon = titan.GetActiveWeapon()

	while ( weapon.IsWeaponOffhand() )
	{
		WaitFrame()
		weapon = titan.GetActiveWeapon()
	}

	weapon.SetMods( [] )

	WaitEndFrame() // if I didn't add this PlayAnim, below, would return immediately for some unknown reason.

	if ( IsValid( animEnt ) && IsAlive( titan ) )
		waitthread PlayAnim( titan, "at_mortar_knee2stand", animEnt )
}

function MortarTitanStopAttack( titan )
{
	titan.Signal( "InterruptMortarAttack" )
}

function MortarTitanStopAttack_Internal( titan )
{
	titan.Signal( "InterruptMortarAttack" )
	titan.Anim_Stop()
}

void function MortarAIWaitToEngage( entity titan, float timeFrame, int minDamage = 75 )
{
	entity soul = titan.GetTitanSoul()
	float endtime = Time() + timeFrame
	int lastHealth = titan.GetHealth() + soul.GetShieldHealth()
 	float tickTime = 1.0

	while ( Time() < endtime )
	{
		wait tickTime

		int currentHealth = titan.GetHealth() + soul.GetShieldHealth()
		if ( lastHealth > ( currentHealth + minDamage ) ) // add minDamage so that we ignore low amounts of damage.
		{
			lastHealth = currentHealth
			endtime = Time() + timeFrame
		}
	}
}


/*******************************************************************\
	MORTAR TITANS
\*******************************************************************/
//Function assumes that given Titan is spawned as npc_titan_atlas_tracker_mortar. Changing the Titan's AISettings post-spawn
//disrupts the Titan's titanfall animations and can result in the Titan landing outside the level.
void function MortarTitanThink( entity titan, entity generator )
{
	titan.EndSignal( "OnSyncedMeleeVictim" )
	titan.EndSignal( "OnDeath" )
	titan.EndSignal( "OnDestroy" )

	entity soul = titan.GetTitanSoul()
	soul.EndSignal( "OnDestroy" )

	titan.ai.carryBarrel = CreateScriptRef()
	titan.TakeWeaponNow( titan.GetActiveWeapon().GetWeaponClassName() )
	titan.GiveWeapon( "mp_titanweapon_rocketeer_rocketstream" )
	titan.SetActiveWeaponByName( "mp_titanweapon_rocketeer_rocketstream" )
	titan.SetScriptName( "mortar_titan" )

	entity weapon = titan.GetActiveWeapon()
	weapon.w.missileFiredCallback = MortarMissileFiredCallback
	thread MortarTitanDeathCleanup( titan )

	WaitTillHotDropComplete( titan )

	float minEngagementDuration = 5
	StationaryAIPosition ornull mortarPosition = GetRandomStationaryPosition( titan.GetOrigin(), MORTAR_TITAN_POSITION_SEARCH_RANGE, eStationaryAIPositionTypes.MORTAR_TITAN )
	while ( mortarPosition == null )
	{
		// incase all stationary titan positions are in use wait for one to become available
		wait 5
		mortarPosition = GetRandomStationaryPosition( titan.GetOrigin(), MORTAR_TITAN_POSITION_SEARCH_RANGE, eStationaryAIPositionTypes.MORTAR_TITAN )
	}

	expect StationaryAIPosition( mortarPosition )

	ClaimStationaryAIPosition( mortarPosition )

	OnThreadEnd(
		function() : ( mortarPosition )
		{
			// release mortar position when dead
			ReleaseStationaryAIPosition( mortarPosition )
		}
	)

	float minDamage = 75 // so that the titan doesn't care about small amounts of damage.

	while( true )
	{
		vector origin = mortarPosition.origin

		float startHealth = float( titan.GetHealth() + soul.GetShieldHealth() )
		waitthread MoveToMortarPosition( titan, origin, generator )

		if ( startHealth > ( ( titan.GetHealth() + soul.GetShieldHealth() ) + minDamage ) || !titan.IsInterruptable() )
		{
			// we took damage getting to the mortar location lets wait until we stop taking damage
			waitthread MortarAIWaitToEngage( titan, MORTAR_TITAN_ENGAGE_DELAY )
			continue
		}

		waitthread MortarTitanKneelToAttack( titan )
		thread MortarTitanAttack( titan, generator )

		wait minEngagementDuration	// aways mortar the target for a while before potentially breaking out

		// wait for interruption
		waitthread WaitForInteruption( titan )

		MortarTitanStopAttack_Internal( titan )

		// lets wait until we stop taking damage before going back to attacking the generator
		waitthread MortarAIWaitToEngage( titan, MORTAR_TITAN_REENGAGE_DELAY )
	}
}

void function WaitForInteruption( entity titan )
{
	Assert( IsNewThread(), "Must be threaded off" )

	titan.EndSignal( "OnSyncedMeleeVictim" )
	titan.EndSignal( "OnDeath" )
	titan.EndSignal( "OnDestroy" )
	titan.EndSignal( "InterruptMortarAttack" )

	entity soul = titan.GetTitanSoul()
	soul.EndSignal( "OnDestroy" )

	float playerProximityDistSqr = pow( 256, 2 )
	float healthBreakOff = ( titan.GetHealth() + soul.GetShieldHealth() ) * MORTAR_TITAN_ABORT_ATTACK_HEALTH_FRAC

	while( true )
	{
		if ( IsEnemyWithinDist( titan, playerProximityDistSqr ) )
			break
		if ( ( titan.GetHealth() + soul.GetShieldHealth() ) < healthBreakOff )
			break
		wait 1
	}
}

bool function IsEnemyWithinDist( entity titan, float dist )
{
	vector origin = titan.GetOrigin()
	array<entity> players = GetPlayerArrayOfEnemies_Alive( titan.GetTeam() )

	foreach( player in players )
	{
		if ( DistanceSqr( player.GetOrigin(), origin ) < dist )
			return true
	}

	return false
}