File size: 21,078 Bytes
b6a38d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
-- base AI behavior class
DefineClass.AIBehavior = {
	__parents = { "AIBiasObj" },
	properties = {
		{ id = "Label", editor = "text", default = "", },
		{ id = "Comment", editor = "text", default = "" },
		{ id = "Fallback", editor = "bool", default = true, help = "When enabled, this behavior will be considered the go-to fallback behavior for specific uses, like GuardArea archetype. If multiple behaviors are marked as Fallback, only the first one will be used." },
		{ id = "RequiredKeywords", editor = "string_list", default = {}, item_default = "", items = AIKeywordsCombo, arbitrary_value = true, },
		{ id = "Score", editor = "func", params = "self, unit, proto_context, debug_data", default = function(self, unit, proto_context, debug_data) return self.Weight end, },
		{ id = "turn_phase", name = "Turn Phase", editor = "choice", default = "Normal", items = function (self) return { "Early", "Normal", "Late" } end, },
		{ id = "OptLocWeight", name = "Optimal Location Weight", editor = "number", default = 100, help = "How important is moving toward optimal location", },
		{ id = "EndTurnPolicies", name = "End-of-Turn Location Policies", editor = "nested_list", default = false, base_class = "AIPositioningPolicy", class_filter = function (name, class, obj) return class.end_of_turn end, },
		{ id = "SignatureActions", name = "Signature Actions", help = "Actions specific to this behavior; if the list isn't empty the action used will be chosen from it instead of the archetype's list",
			editor = "nested_list", default = false, base_class = "AISignatureAction", class_filter = function (name, class, obj) return not class.hidden end, },
		{ id = "TargetingPolicies", name = "Targeting Policies", help = "Additoinal targeting policies that modify target score (optional)", 
			editor = "nested_list", default = false, base_class = "AITargetingPolicy", },
		{ id = "TakeCoverChance", name = "Take Cover Chance", editor = "number", min = 0, max = 100, scale = "%", default = 20, help = "chance to use Take Cover action at the end of the turn when in a cover spot" },
	},
}

function AIBehavior:MatchUnit(unit)
	for _, keyword in ipairs(self.RequiredKeywords) do
		if not table.find(unit.AIKeywords or empty_table, keyword) then
			return
		end
	end
	return true
end

function AIBehavior:GetEditorView()
	local label = self.Label ~= "" and self.Label or self.class
	local text = string.format("%s%s (%s)", self.Priority and "Priority " or "", label, self.Weight)
	
	if self.Comment ~= "" then
		text = text .. string.format(" -> %s", self.Comment)
	end
	
	return text
end

function AIBehavior:OnStart(unit)
	self:OnActivate(unit)
end

function AIBehavior:EnumDestinations(unit, context)
	AIFindDestinations(unit, context)
end

function AIBehavior:Think(unit, debug_data)
end

function AIBehavior:GetTurnPhase(unit)
	return unit:IsThreatened() and "Late" or self.turn_phase
end

function AIBehavior:BeginStep(label, debug_data)
	if not debug_data then return end	
	debug_data.thihk_steps = debug_data.thihk_steps or {}
	assert(not debug_data.thihk_steps[label])
	
	local step = { label = label, start_time = GetPreciseTicks() }
	table.insert(debug_data.thihk_steps, step)
	debug_data.thihk_steps[label] = step
end

function AIBehavior:EndStep(label, debug_data)
	if not debug_data then return end
	local step = debug_data.thihk_steps[label]
	assert(step)
	step.time = GetPreciseTicks() - step.start_time
end

function AIBehavior:TakeStance(unit)
	local context = unit.ai_context
	if not context or unit.species ~= "Human" then 
		return 
	end
	
	if context.movement_action then
		return
	end
	
	local upos = context.unit_stance_pos or stance_pos_pack(unit, unit.stance)
	local dest = context.ai_destination
	if not dest or stance_pos_dist(dest, upos) == 0 then
		-- go in pref stance if already in ai_destination
		if unit.stance ~= context.archetype.PrefStance then
			local target = context.dest_target[dest]
			local ap = Max(0, GetStanceToStanceAP(unit.stance, context.archetype.PrefStance) or 0)
			local cost = context.default_attack_cost
			local reserved = IsValidTarget(target) and cost or 0
			local uiAP = unit:GetUIActionPoints()
			if uiAP > ap + cost then
				-- check LOF for non-melee weapons first
				local max_check_range, is_melee = AIGetWeaponCheckRange(unit, context.weapon, context.default_attack)
				if not is_melee then
					local targets = context.default_attack:GetTargets({unit})
					if #targets == 0 then
						return
					end
					local targets_attack_data = GetLoFData(unit, targets, {
						obj = unit,
						action_id = context.default_attack.id,
						weapon = context.weapon,
						stance = context.archetype.PrefStance,
						range = max_check_range,
						target_spot_group = "Torso",
						prediction = true,
					})
					local any_lof = false
					for k, target in ipairs(targets) do
						local attack_data = targets_attack_data[k]
						if attack_data and not attack_data.stuck and not attack_data.best_ally_hits_count then
							any_lof = true
							break
						end
					end
					if not any_lof then return end
				end
				local target_pos = IsValidTarget(target) and target:GetPos() or nil
				AIPlayChangeStance(unit, context.archetype.PrefStance, target_pos)
			end
		end
	else
		local move_stance_idx = context.dest_combat_path[dest]
		local goto_stance = StancesList[move_stance_idx]
		if goto_stance ~= unit.stance then
			local x, y, z, stance_idx = stance_pos_unpack(dest)
			local px, py, pz = SnapToPassSlabXYZ(x, y, z)
			local cpath = context.combat_paths[move_stance_idx]
			local dest_prev_ppos = px and cpath and cpath.paths_prev_pos and cpath.paths_prev_pos[point_pack(px, py, pz)]
			if dest_prev_ppos then
				if not AIPlayChangeStance(unit, goto_stance, point(point_unpack(dest_prev_ppos))) then
					-- failed, abort movement
					assert(CanOccupy(unit, GetPassSlab(unit)))
					context.ai_destination = false
				end
			end
		end
	end
end

function AIBehavior:BeginMovement(unit, trackMove)
	local context = unit.ai_context
	local dest = context.ai_destination
	local upos = stance_pos_pack(unit, unit.stance)
	
	if not dest or (stance_pos_dist(dest, upos) == 0) then
		return "continue"
	end
	
	local x, y, z, stance_idx = stance_pos_unpack(dest)
	local move_stance_idx = context.dest_combat_path[dest]
	local cpath = context.combat_paths[move_stance_idx]
	local pt = SnapToPassSlab(x, y, z)
	local path = pt and cpath and cpath:GetCombatPathFromPos(pt)
	local goto_ap = cpath and cpath.paths_ap[point_pack(pt)] or 0

	if not context.reposition and context.movement_action then
		local retval = context.movement_action:Execute(context, context.action_states[context.movement_action])
		if retval ~= "restart" and IsKindOf(context.movement_action, "AIActionMobileShot") then
			context.max_attacks = context.max_attacks - 1
		end
		return retval
	end

	if not path then 
		return false 
	end
	local move_args = {
		goto_pos = point(point_unpack(path[1])),
		reposition = context.reposition,
		forced_run = context.forced_run,
		trackMove = trackMove,
	}
	if stance_idx ~= move_stance_idx then
		move_args.toDoStance = StancesList[stance_idx]
	end
	assert(CanOccupy(unit, move_args.goto_pos))	
	if not AIStartCombatAction("Move", unit, goto_ap, move_args) then
		return false
	end
	
	while IsValid(unit) and not unit:IsDead() and (HasCombatActionWaiting(unit) or HasCombatActionInProgress(unit)) do
		local ok, obj = WaitMsg("CombatActionStateChange", 10)
		if ok and obj == unit then
			local state = CombatActions_RunningState[unit]
			if not state or state == "PostAction" then
				break
			end
		end
	end
	local state = CombatActions_RunningState[unit]
	if (not state or state == "PostAction") and not unit:IsDead() then
		return "continue"
	end
	return false
end

function AIBehavior:EndMovement(unit)
	local context = unit.ai_context
	local dest = context.ai_destination
	if not dest or unit.species ~= "Human" or unit:IsIncapacitated() then return end
	local upos = GetPackedPosAndStance(unit)
	if stance_pos_dist(dest, upos) == 0 then
		local x, y, z, stance_idx = stance_pos_unpack(dest)
		local stance = StancesList[stance_idx]
		if unit.stance ~= stance and not unit:HasStatusEffect("StationedMachineGun") and not unit:HasStatusEffect("ManningEmplacement") then
			unit:DoChangeStance(stance)
		end
	end
end

function AIBehavior:Play(unit)
end

function AIBehavior:GetSignatureActions(context)
	return self.SignatureActions
end

----------------------------------------
-- Standard AI behavior
----------------------------------------

DefineClass.StandardAI = {
	__parents = { "AIBehavior" },
	properties = {
		{ category = "Default Attack Override", id = "override_attack_id", name = "Score Attack Id", editor = "combo", items = PresetGroupCombo("CombatAction", "WeaponAttacks"), default = "", help = "attack to use instead of the weapon's default attack to calculate damage score"},
		{ category = "Default Attack Override", id = "override_cost_id", name = "Cost Attack Id", editor = "combo", items = PresetGroupCombo("CombatAction", "WeaponAttacks"), default = "", help = "attack to use instead of the weapon's default attack to calculate attack cost"},
	},
}

function StandardAI:Think(unit, debug_data)
	self:BeginStep("think", debug_data)
		local context = unit.ai_context
	
		self:BeginStep("destinations", debug_data)
			AIFindDestinations(unit, context)
		self:EndStep("destinations", debug_data)
	
		self:BeginStep("optimal location", debug_data)
			AIFindOptimalLocation(context, debug_data and debug_data.optimal_scores)
		self:EndStep("optimal location", debug_data)
	
		self:BeginStep("end of turn location", debug_data)
			AICalcPathDistances(context)
			if self.override_attack_id ~= "" then
				context.override_attack_id = self.override_attack_id
			end
			if self.override_cost_id and CombatActions[self.override_cost_id] then
				context.override_attack_cost = CombatActions[self.override_cost_id]:GetAPCost(unit)
			end
			AIPrecalcDamageScore(context)
			context.override_attack_id = nil
			context.override_attack_cost = nil
			unit.ai_context.ai_destination = AIScoreReachableVoxels(context, self.EndTurnPolicies, self.OptLocWeight, debug_data and debug_data.reachable_scores)
		self:EndStep("end of turn location", debug_data)
		self:BeginStep("movement action", debug_data)
			context.movement_action = AIChooseMovementAction(context)
		self:EndStep("movement action", debug_data)
	self:EndStep("think", debug_data)
end

----------------------------------------
-- Retreat AI behavior
----------------------------------------

DefineClass.RetreatAI = {
	__parents = { "AIBehavior" },
	properties = {
		{ id = "DespawnAllowed", editor = "bool", default = true },
	},
}

function RetreatAI:Think(unit, debug_data)
	local context, destinations
	
	if not unit.ai_context.destinations then return end
	
	self:BeginStep("think", debug_data)
		context = unit.ai_context

		self:BeginStep("destinations", debug_data)
			AIFindDestinations(unit, context)
		self:EndStep("destinations", debug_data)
					
		context.entrance_markers = MapGetMarkers("Entrance")
		
		if not self:CanDespawn(unit) then
			self:BeginStep("optimal location", debug_data)
				AIFindOptimalLocation(context, debug_data and debug_data.optimal_scores)
			self:EndStep("optimal location", debug_data)
			
			self:BeginStep("end of turn location", debug_data)
				AICalcPathDistances(context)
				unit.ai_context.ai_destination = AIScoreReachableVoxels(context, self.EndTurnPolicies, self.OptLocWeight, debug_data and debug_data.reachable_scores)
			self:EndStep("end of turn location", debug_data)
			self:BeginStep("movement action", debug_data)
				context.movement_action = AIChooseMovementAction(context)
			self:EndStep("movement action", debug_data)
		else
			if debug_data then
				debug_data.optimal_scores[context.unit_stance_pos] = { "despawn", 100 }
			end
		end
	self:EndStep("think", debug_data)
end

function RetreatAI:CanDespawn(unit)
	if not self.DespawnAllowed then return false end	
	local context = unit.ai_context
	local pos = GetPassSlab(unit)
	local wx, wy, wz = pos:xyz()	
	local unit_stance_pos = stance_pos_pack(wx, wy, wz, StancesList[unit.stance])
	
	-- unseen?
	if not AIHasLOSToEnemyFromDest(unit_stance_pos) and unit_stance_pos == context.unit_stance_pos then
		return true
	end

	-- inside entrance marker area?
	local vx, vy = unit:GetGridCoords()
	for _, marker in ipairs(context.entrance_markers) do
		if marker:IsVoxelInsideArea(vx, vy) then
			return true
		end
	end
end

function RetreatAI:Play(unit)
	-- check for despawn conditions, despawn
	local pos = GetPassSlab(unit)
	local wx, wy, wz = pos:xyz()
	local unit_stance_pos = stance_pos_pack(wx, wy, wz, StancesList[unit.stance])
	local context = unit.ai_context
	
	if self:CanDespawn(unit) then
		AIPlayCombatAction("Despawn", unit)
	end
	return "done" -- skip attacks for this unit
end

----------------------------------------
-- Positioning AI behavior
----------------------------------------

function PositioningAIScore(self, unit, proto_context, debug_data)
	unit.ai_context = unit.ai_context or AICreateContext(unit, proto_context)
	local dest, score = AIScoreReachableVoxels(unit.ai_context, self.EndTurnPolicies, 0)
	return MulDivRound(score, self.Weight, 100)
end

DefineClass.PositioningAI = {
	__parents = { "AIBehavior" },
	properties = {
		{ id = "VoiceResponse", name = "Voice Response", editor = "text", default = "", help = "voice response to play on activation of this behavior", },
		{ id = "Score", editor = "func", params = "self, unit, proto_context, debug_data", default = PositioningAIScore, },
	},
}

function PositioningAI:Think(unit, debug_data)
	local context = unit.ai_context

	self:BeginStep("think", debug_data)	
		self:BeginStep("destinations", debug_data)
			AIFindDestinations(unit, context)
		self:EndStep("destinations", debug_data)
		self:BeginStep("positioning dest", debug_data)
			context.positioning_dest = AIScoreReachableVoxels(context, self.EndTurnPolicies, 0, debug_data and debug_data.reachable_scores)				
			context.ai_destination = context.positioning_dest
		self:EndStep("positioning dest", debug_data)
		self:BeginStep("movement action", debug_data)
			context.movement_action = AIChooseMovementAction(context)
		self:EndStep("movement action", debug_data)
	self:EndStep("think", debug_data)
end

function PositioningAI:BeginMovement(unit)
	local context = unit.ai_context
	if not context or not context.positioning_dest then
		return "restart"
	end
	if (self.VoiceResponse or "") ~= "" then
		PlayVoiceResponse(unit, self.VoiceResponse)
	end
	return AIBehavior.BeginMovement(self, unit)
end

----------------------------------------
-- HoldPosition AI behavior
----------------------------------------
DefineClass.HoldPositionAI = {
	__parents = { "AIBehavior" },
	properties = {
		{ id = "VoiceResponse", name = "Voice Response", editor = "text", default = "", help = "voice response to play on activation of this behavior", },
		{ id = "Score", editor = "func", params = "self, unit, proto_context, debug_data", default = function(self, unit) return self.Weight end, },
	},
}

function HoldPositionAI:OnStart(unit)
	AIBehavior.OnStart(self, unit)
	
	if (self.VoiceResponse or "") ~= "" then
		PlayVoiceResponse(unit, self.VoiceResponse)
	end
end

function HoldPositionAI:Think(unit, debug_data)
	local context = unit.ai_context
	self:BeginStep("think", debug_data)
		--local dest = context.voxel_to_dest[context.unit_world_voxel]		
		--local dests = dest and {dest} or nil
		local dests = { GetPackedPosAndStance(unit) }
		AIPrecalcDamageScore(context, dests)
	self:EndStep("think", debug_data)
end

----------------------------------------
-- Approach Interactable AI behavior
----------------------------------------

DefineClass.ApproachInteractableAI = {
	__parents = { "AIBehavior" },
}

function ApproachInteractableAI:Think(unit, debug_data)
	local interactable = unit.ai_context and unit.ai_context.target_interactable
	if not interactable then
		assert(false, "ApproachInteractableAI doesn't have a target_interactable set")
		return
	end
	
	self:BeginStep("think", debug_data)
		local context = unit.ai_context
	
		self:BeginStep("destinations", debug_data)
			AIFindDestinations(unit, context)
		self:EndStep("destinations", debug_data)
	
		-- skip evaluation of optimal locations, use the interactable position
		local interaction_pos = unit:GetInteractionPosWith(interactable) or interactable:GetPos()
		context.best_dest = stance_pos_pack(interaction_pos, unit.stance)
	
		self:BeginStep("end of turn location", debug_data)
			AICalcPathDistances(context)
			AIPrecalcDamageScore(context)
			unit.ai_context.ai_destination = AIScoreReachableVoxels(context, self.EndTurnPolicies, self.OptLocWeight, debug_data and debug_data.reachable_scores)
		self:EndStep("end of turn location", debug_data)
		self:BeginStep("movement action", debug_data)
			context.movement_action = AIChooseMovementAction(context)
		self:EndStep("movement action", debug_data)
	self:EndStep("think", debug_data)
end

function ApproachInteractableAI:BeginMovement(unit)
	local result = self:Play(unit)
	if result == "restart" then
		return result
	end
	return AIBehavior.BeginMovement(self, unit)
end

function ApproachInteractableAI:EndMovement()
end

function ApproachInteractableAI:Play(unit)
	local interactable = unit.ai_context and unit.ai_context.target_interactable
	
	local action = CombatActions.Interact
	local args = {target = interactable, override_ap_cost = 0 }
	
	args.goto_pos = unit:GetInteractionPosWith(interactable) or interactable:GetPos()
	args.goto_ap = args.goto_pos ~= SnapToVoxel(unit:GetPos()) and CombatActions.Move:GetAPCost(unit, { goto_pos = args.goto_pos, stance = unit.stance }) or 0
	
	local state = action:GetUIState({unit}, args)
	if state == "enabled" then
		local result = AIPlayCombatAction("Interact", unit, nil, args)
		assert(result, "AI unit wasn't able to interact")
		if result then
			return "restart"
		end
	else
		-- unassign ourselves, somebody else might have a better chance of using it		
		if g_Combat:GetEmplacementAssignment(interactable) == unit then
			g_Combat:AssignEmplacement(interactable, nil)
		end
	end	
end

----------------------------------------
-- Custom AI behavior
----------------------------------------

DefineClass.CustomAI = {
	__parents = { "AIBehavior" },
	properties = {
		{ id = "EnumDests", editor = "func", params = "self, unit, context, debug_data", default = empty_func },
		{ id = "PickEndTurnPolicies", editor = "func", params = "self, unit, context, debug_data", default = empty_func },
		{ id = "EvalDamageScore", editor = "func", params = "self, unit, context, debug_data", default = empty_func },
		{ id = "PickOptimalLoc", editor = "func", params = "self, unit, context, debug_data", default = empty_func },
		{ id = "PickEndTurnLoc", editor = "func", params = "self, unit, context, debug_data", default = empty_func },
		{ id = "SelectSignatureActions", editor = "func", params = "self, unit, context, debug_data", default = empty_func },
		{ id = "Execute", editor = "func", params = "self, unit, context, debug_data", default = empty_func },		
	},
}

function CustomAI:EnumDestinations(unit, context)
	if not self:EnumDests(unit, context) then
		AIFindDestinations(unit, context)
	end
end

function CustomAI:Think(unit, debug_data)
	self:BeginStep("think", debug_data)
		local context = unit.ai_context
	
		self:BeginStep("enum dests", debug_data)
			self:EnumDestinations(unit, context)
		self:EndStep("enum dests", debug_data)
		
		self:BeginStep("optimal location", debug_data)
			if not self:PickOptimalLoc(unit, context, debug_data) then
				AIFindOptimalLocation(context, debug_data and debug_data.optimal_scores)
			end
		self:EndStep("optimal location", debug_data)
	
		self:BeginStep("end of turn location", debug_data)
			if self.override_attack_id ~= "" then
				context.override_attack_id = self.override_attack_id
			end
			if self.override_cost_id and CombatActions[self.override_cost_id] then
				context.override_attack_cost = CombatActions[self.override_cost_id]:GetAPCost(unit)
			end
			if not self:EvalDamageScore(unit, context) then
				AIPrecalcDamageScore(context)
			end
			context.override_attack_id = nil
			context.override_attack_cost = nil
			if not self:PickEndTurnLoc(unit, context, debug_data) then
				local policies = self:PickEndTurnPolicies(unit, context) or self.EndTurnPolicies
				unit.ai_context.ai_destination = AIScoreReachableVoxels(context, policies, self.OptLocWeight, debug_data and debug_data.reachable_scores)
			end
		self:EndStep("end of turn location", debug_data)
		self:BeginStep("movement action", debug_data)
			context.movement_action = AIChooseMovementAction(context)
		self:EndStep("movement action", debug_data)
	self:EndStep("think", debug_data)
end

function CustomAI:Play(unit)
	return self:Execute(unit, unit.ai_context)
end

function CustomAI:GetSignatureActions(context)
	if context then
		return self:SelectSignatureActions(context.unit, context)
	end
	return AIBehavior.GetSignatureActions(self, context)
end