• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Wurst] ThreatHandler

Status
Not open for further replies.
Level 14
Joined
Jun 27, 2008
Messages
1,325
[WurstScript] ThreatHandler 1.1

ThreatHandler is a library for WurstScript which provides an advanced mob ai.

Based on ZTS by Zwiebelchen. This is basically a port of the whole ZTS system to WurstScript however with some changes in the API and the internal control flow. Also the internally used datastructures is based on LinkedLists instead of hashtables.

There are no requirements except for the obligatory WurstScript StandardLib.

Code:
Wurst:
package ThreatHandler
package ThreatHandler
import public ThreatHandlerConfig
// By muzzel - Version 1.1

// Based on ZTS by Zwiebelchen ([url]http://www.hiveworkshop.com/forums/spells-569/zwiebelchens-threat-system-2-6-a-156179/[/url])
//
// It is recommended to edit certain Gameplay Constants entries, to use the full potential of the system.
// The most important entries are: (with selected "Show Raw-Data")
//		CallForHelp        --> Set this to 0, if possible; the system will manage this - it isn't a problem if you don't do this, though
//		                       You don't have to do it if your threat-system controlled units are neutral hostile
//		CreepCallForHelp   --> Set this to 0, if possible; the system will manage this - it isn't a problem if you don't do this, though
//		GuardDistance      --> Set this to something higher than ReturnRange (see below)
//		MaxGuardDistance   --> Set this to something higher than ReturnRange (see below)
//		GuardReturnTime    --> Set this to something very high, so that the standard AI doesn't interfere (i.e. 60 seconds)
//
// Todo:
// - store total amount of threat in ThreatUnit to make calculation of % threat faster
// - add IsEvent function

/** 
 * Modifies the threat caused by playerUnit to threatUnit.
 * If add is true the specified value will be added/substracted.
 * This function is failsafe to use with negative values.
 */
public function modifyThreat(unit playerUnit, unit threatUnit, real value, boolean add)
	let pu = getPlayerUnit(playerUnit)
	let tu = getThreatUnit(threatUnit)
	real newValue = value
	if pu == null or tu == null
		return
	if IsUnitType(playerUnit, UNIT_TYPE_DEAD) or IsUnitType(threatUnit, UNIT_TYPE_DEAD) or playerUnit.getTypeId() == 0 or threatUnit.getTypeId() == 0
		return
	if tu.state == ThreatUnitState.RETURNING or tu.state == ThreatUnitState.RETURNED
		return
	ThreatListEntry e
	if HaveSavedInteger(ht, pu castTo int, tu castTo int)
		// ThreatListEntry for (pu x tu) already exists:
		e = ht.loadInt(pu castTo int, tu castTo int) castTo ThreatListEntry
		if add
			newValue = e.threat + value
		if newValue < 0
			newValue = 0
		e.setThreat(newValue)
	else
		// Create new ThreatListEntry:
		if value < 0
			newValue = 0
		tu.camp.addIncombatList()
		tu.camp.state = CampState.COMBAT
		e = new ThreatListEntry(pu, tu, newValue)
		var campUnit = tu.camp.dummy
		while campUnit.campNext != tu.camp.dummy
			campUnit = campUnit.campNext
			campUnit.state = ThreatUnitState.COMBAT
			if campUnit != tu
				e = new ThreatListEntry(pu, campUnit, 0)

/** 
 * Returns the combat state of a PlayerUnit or ThreatUnit.
 * Returns true if the specified unit is in combat.
 * Returns false if the specified unit is unregistered, dead or invalid.
 */
public function getCombatState(unit u) returns boolean
	if GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD)
		return false
	let pu = getPlayerUnit(u)
	if pu != null
		return pu.dummy.puNext != pu.dummy
	let tu = getThreatUnit(u)
	if tu != null
		return tu.getFirstInThreatList() != null
	return false

/**
 * Returns an iterator for the sepcified ThreatUnit which allows iterating over the ThreatUnits threat list entries.
 * Returns null if the specified ThreatUnit is invalid.
 */ 
public function getThreatUnitIterator(unit threatUnit) returns ThreatUnitIterator
	ThreatUnitIterator iter = null
	let tu = getThreatUnit(threatUnit)
	if tu != null
		iter = tu.iterator()
	return iter

/** 
 * Returns the first PlayerUnit in the specified ThreatUnits threat list.
 * Returns null if invalid ThreatUnit or if threat list is empty.
 */
public function getThreatUnitFirstSlot(unit threatUnit) returns unit
	unit u = null
	let tu = getThreatUnit(threatUnit)
	if tu != null
		u = tu.getFirstInThreatList()
	return u

hashtable ht = InitHashtable()
ThreatHandlerCamp array icList // List of Camps which are in combat
int icListLast = 0

enum CampState
	OOC
	COMBAT
	RETURNING

class ThreatHandlerCamp
	int icListId
	ThreatHandlerTU dummy
	CampState state
	int timeToPortLeft

	construct()
		icListId = 0
		dummy = new ThreatHandlerTU()
		state = CampState.OOC

	function addIncombatList()
		if icListId == 0
			icListLast++
			icListId = icListLast
			icList[icListLast] = this

	function removeIncombatList()
		if icListId != 0
			icList[icListId] = icList[icListLast]
			icListLast--
			icListId = 0

	function add(ThreatHandlerTU tu)
		if tu.camp != null
			printWarning("ThreatHandlerCamp.add(ThreatHandlerTU tu): " + tu.u.getName() + " already has a camp assigned.")
		tu.campPrev = dummy.campPrev
		tu.campNext = dummy
		dummy.campPrev.campNext = tu
		dummy.campPrev = tu
		tu.camp = this

	function returnCamp()
		state = CampState.RETURNING
		timeToPortLeft = TIME_TO_PORT*2
		var campUnit = dummy
		while campUnit.campNext != dummy
			campUnit = campUnit.campNext
			campUnit.state = ThreatUnitState.RETURNING
			// Clear threat list:
			for ThreatListEntry e in campUnit
				destroy e
			campUnit.u.issueImmediateOrderById(851972) // stop
			campUnit.u.issuePointOrder("move", campUnit.campPos)
			SetUnitInvulnerable(campUnit.u, true)
			onThreatUnitReturn(campUnit.u)

	function checkReturned()
		var ret = true
		var campUnit = dummy
		while campUnit.campNext != dummy
			campUnit = campUnit.campNext
			if campUnit.state != ThreatUnitState.RETURNED
				ret = false
				break
		if ret // all units RETURNED
			state = CampState.OOC
			removeIncombatList()
			campUnit = dummy
			while campUnit.campNext != dummy
				campUnit = campUnit.campNext
				campUnit.state = ThreatUnitState.OOC
				SetUnitInvulnerable(campUnit.u, false)
				campUnit.u.unpause()

	/** 
	 * Removes the specified ThreatHandlerTU from this camp.
	 * Destroys the camp and removes it from icList if no ThreatHandlerTU left.
	 */ 
	function remove(ThreatHandlerTU tu)
		tu.campNext.campPrev = tu.campPrev
		tu.campPrev.campNext = tu.campNext
		tu.camp = null
		if dummy.campNext == dummy // camp empty, destroy it
			destroy this

	ondestroy
		removeIncombatList()
		destroy dummy

enum ThreatUnitState
	OOC
	COMBAT
	RETURNING
	RETURNED

public class ThreatHandlerPU
	protected unit u
	// Dummy for LinkedList of ThreatListEntries:
	protected ThreatListEntry dummy

	construct(unit u)
		// TODO: Optional: check if unit is already registered or dead/null.
		this.u = u
		dummy = new ThreatListEntry()

	protected function add(ThreatListEntry e)
		e.puPrev = dummy.puPrev
		e.puNext = dummy
		dummy.puPrev.puNext = e
		dummy.puPrev = e

	protected function remove(ThreatListEntry e)
		e.puNext.puPrev = e.puPrev
		e.puPrev.puNext = e.puNext

	ondestroy
		// Clear list:
		var e = dummy
		while e.puNext != dummy // TODO puNext
			e = e.puNext // TODO puNext
			destroy e
		destroy dummy
		// Enable ==null checks:
		this.u = null

function acquireTarget()
	unit tu = GetTriggerUnit()
	unit pu
	if GetEventTargetUnit() != null
		pu = GetEventTargetUnit()
	else
		pu = GetOrderTargetUnit()
	if IsUnitEnemy(pu, GetOwningPlayer(tu))
		if getThreatUnit(tu).state == ThreatUnitState.OOC
			modifyThreat(pu, tu, 0, true) // Sets this unit in combat

public class ThreatHandlerTU
	protected ThreatUnitState state
	protected unit u
	protected vec2 campPos
	// Dummy for LinkedList of ThreatListEntries:
	protected ThreatListEntry dummy
	protected trigger targetAcquireTrigger
	// Camp linkedlist:
	protected ThreatHandlerCamp camp
	protected ThreatHandlerTU campPrev
	protected ThreatHandlerTU campNext

	private construct() // Constructs a dummy
		campPrev = this
		campNext = this
		this.camp = null

	function getUnit() returns unit
		return u

	construct(unit u)
		// TODO: Optional: Check if unit was already registered, of it unit is dead/null.
		state = ThreatUnitState.OOC
		this.u = u
		campPos = u.getPos()
		dummy = new ThreatListEntry()
		targetAcquireTrigger = CreateTrigger()
			..registerUnitEvent(u, EVENT_UNIT_ISSUED_TARGET_ORDER)
			..registerUnitEvent(u, EVENT_UNIT_ACQUIRED_TARGET)
			..addAction(function acquireTarget)
		// Look for camps:
		GroupEnumUnitsInRange(ENUM_GROUP, u.getPos().x, u.getPos().y, CAMP_RANGE, Condition(() -> begin
				let tu = getThreatUnit(GetFilterUnit())
				// Enum units which are: 1. registered ThreatUnits, 2. has camp, 3. alive, 4. not in combat 
				return tu.u != null and tu.camp != null and not IsUnitType(GetFilterUnit(), UNIT_TYPE_DEAD) and tu.state == ThreatUnitState.OOC
		end))
		let other = FirstOfGroup(ENUM_GROUP)
		if other != null
			let otherTu = getThreatUnit(other)
			// Add this to otherTus camp:
			otherTu.camp.add(this)
		else
			// Create a new camp:
			new ThreatHandlerCamp().add(this)
		ENUM_GROUP.clear()


	// TODO: Add another constructor which takes an additional unit argument.
	// It specifies to which group this unit should be added.
	// This replaces ZTS: includeCombatCamps == true.

	function iterator() returns ThreatUnitIterator
		return new ThreatUnitIterator(this)

	protected function getFirstInThreatList() returns unit
		unit ret = null
		if dummy.tuNext != dummy
			ret = dummy.tuNext.pu.u
		return ret

	protected function add(ThreatListEntry e)
		var t = dummy.tuPrev
		while e.threat > t.threat
			t = t.tuPrev
		// insert e after t:
		t.tuNext.tuPrev = e
		e.tuNext = t.tuNext
		t.tuNext = e
		e.tuPrev = t

	/** 
	 * Moves the specified entry in this list to preserve the order. 
	 * The second argument specifies if the threat was increased or decreased.
	 */
	protected function sort(ThreatListEntry e, boolean increased)
		if increased
			if e.threat > e.tuPrev.threat
				// if threat was increased and is bigger than value of previous entry:
				var t = e.tuPrev
				while e.threat > t.threat
					t = t.tuPrev
				// remove e and insert it after t:
				remove(e)
				t.tuNext.tuPrev = e
				e.tuNext = t.tuNext
				t.tuNext = e
				e.tuPrev = t
		else
			if e.threat < e.tuNext.threat
				// if threat was decreased and is smaller than the next entry:
				var t = e.tuNext
				while e.threat < t.threat
					t = t.tuNext
				// remove e and insert it before t:
				remove(e)
				t.tuPrev.tuNext = e
				e.tuPrev = t.tuPrev
				t.tuPrev = e
				e.tuNext = t

	protected function remove(ThreatListEntry e)
		e.tuNext.tuPrev = e.tuPrev
		e.tuPrev.tuNext = e.tuNext

	ondestroy
		if this.camp != null // if not a dummy
			// Clear list:
			for ThreatListEntry e in this
				destroy e
			destroy dummy
			camp.remove(this)
			targetAcquireTrigger.destr()
			if state == ThreatUnitState.RETURNING
				u.issueImmediateOrderById(851972) // stop
				SetUnitInvulnerable(u, false)
				if IsUnitPaused(u)
					u.unpause()
			// Enable ==null checks:
			this.u = null

public class ThreatUnitIterator
	ThreatHandlerTU tu
	ThreatListEntry current

	construct(ThreatHandlerTU tu)
		this.tu = tu
		current = tu.dummy

	function hasNext() returns boolean
		return current.tuNext != tu.dummy

	function next() returns ThreatListEntry
		current = current.tuNext
		return current

	function reset()
		current = tu.dummy

	function close()
		skip

public class ThreatListEntry
	protected ThreatHandlerPU pu
	protected ThreatHandlerTU tu
	protected ThreatListEntry puPrev
	protected ThreatListEntry puNext
	protected ThreatListEntry tuPrev
	protected ThreatListEntry tuNext
	protected real threat

	construct(ThreatHandlerPU pu, ThreatHandlerTU tu, real threat)
		this.pu = pu
		this.tu = tu
		this.threat = threat
		pu.add(this)
		tu.add(this)
		ht.saveInt(pu castTo int, tu castTo int, this castTo int)

	function getThreat() returns real
		return threat

	function getPlayerUnit() returns unit
		return pu.u

	function getThreatUnit() returns unit
		return tu.u

	/** Constructs a dummy for use as linkedlist warden element */
	private construct()
		tuPrev = this
		tuNext = this
		puPrev = this
		puNext = this
		threat = 100000000.
		pu = null
		tu = null

	ondestroy
		if pu != null // if not dummy element
			pu.remove(this)
			tu.remove(this)
			RemoveSavedInteger(ht, pu castTo int, tu castTo int)

	protected function setThreat(real newThreat)
		let increased = newThreat > threat // increased or decreased
		this.threat = newThreat
		tu.sort(this, increased)

function update()
	unit u
	ThreatHandlerCamp camp
	ThreatHandlerTU tu
	for int i = 1 to icListLast
		camp = icList[i]
		if camp.state == CampState.COMBAT
			tu = camp.dummy
			while tu.campNext != camp.dummy
				tu = tu.campNext
				u = tu.u
				let target = tu.getFirstInThreatList()
				if target != null and IsUnitInRangeXY(u, tu.campPos.x, tu.campPos.y, RETURN_RANGE) and IsUnitInRangeXY(target, tu.campPos.x, tu.campPos.y, ORDER_RETURN_RANGE)
					if GetUnitCurrentOrder(u) == 851983 or GetUnitCurrentOrder(u) == 0 or GetUnitCurrentOrder(u) == 851971
						// TODO: eventBool = true
						u.issueTargetOrderById(851971, target) // smart
						// TODO: eventBool = false
				else
					camp.returnCamp()
					break
		if camp.state == CampState.RETURNING
			if camp.timeToPortLeft > 0
				camp.timeToPortLeft--
				tu = camp.dummy
				while tu.campNext != camp.dummy
					tu = tu.campNext
					u = tu.u
					if tu.state == ThreatUnitState.RETURNING // Ignore RETURNED units
							if IsUnitInRangeXY(u, tu.campPos.x, tu.campPos.y, 35.)
								if not GetUnitCurrentOrder(u) == 851986 // move
									tu.state = RETURNED
									u.pause()
									camp.checkReturned()
							else
								u.issuePointOrder("move", tu.campPos)
			else
				// teleport units
				camp.state = CampState.OOC
				tu = camp.dummy
				while tu.campNext != camp.dummy
					tu = tu.campNext
					u = tu.u
					if tu.state == ThreatUnitState.RETURNING
						// teleport tu
						u.setPos(tu.campPos)
						onThreatUnitReturn(u)
					tu.state = ThreatUnitState.OOC
					SetUnitInvulnerable(u, false)
					u.unpause()

init
	CreateTimer().startPeriodic(0.5, function update)

Configuration:
Wurst:
package ThreatHandlerConfig
import initlater ThreatHandler

/** Make this return the ThreatHandlerTU assigned to the specified unit. */
public function getThreatUnit(unit u) returns ThreatHandlerTU
	return u.getUserData() castTo ThreatHandlerTU

/** Make this return the ThreatHandlerPU assigned to the specified unit. */
public function getPlayerUnit(unit u) returns ThreatHandlerPU
	return u.getUserData() castTo ThreatHandlerPU
	
/** This function is executed when units are returning. It can be used to clean them up. */
public function onThreatUnitReturn(unit u)
	u.setHP(u.getState(UNIT_STATE_MAX_LIFE))
	u.setMana(u.getState(UNIT_STATE_MAX_MANA))
	
// Maximum returning time after which ThreatUnits will get teleported back to camp:
public constant int TIME_TO_PORT = 10
// The range a ThreatUnit can move away from the original camping position, before being ordered to return:
public constant real RETURN_RANGE = 1500
// The range a ThreatUnits target can be away from the original camping position, before being ordered to return:
public constant real ORDER_RETURN_RANGE = 4000
// The range between units considered being in the same camp.
public constant real CAMP_RANGE = 400

GitHub:
https://github.com/muzzel/APT/tree/master/wurst/lib/ThreatHandler

Changelog:
1.1 - Fixed several bugs, including incorrect camp reset and a possible corruption of the internal lists
1.1 - Initial release

Credits:
- Zwiebelchen
 
Last edited:
Level 14
Joined
Jun 27, 2008
Messages
1,325
Thanks.

I still use a hashtable to map a ThreatUnit and a PlayerUnit to their common ThreatListEntry, but the ThreatList itself is a linkedlist so stuff like insertion sort, deletion, etc. is faster.

The major changes are:
1. instead of a group of "incombat" ThreaTUnits i have a list of "incombat" Camps. This can reduce the number of iterations made in the periodic function.

2. In ZTS:
- Every ThreatUnit has a Threat list with the threat values (sorted)
- Every ThreatUnit has a list of the PlayerUnits which are in its threat list. (sorted)
- Every PlayerUnit has a list (group) of ThreatUnits which it has Threat on

In my implementation there is a "ThreatListEntry" object. It references a ThreatUnit, a PlayerUnit and a threat value.
- Every ThreatUnit has a sorted linkedlist of ThreatListEntries
- Every PlayerUnit has an unsorted linkedlist of ThreatListEntries


ModifyThreat(playerunit, threatunit, threatvalue) does:
- Check if playerunit and threatunit have a common ThreatListEntry object [hashtable lookup].
1. If they have one, fetch it [hashtable lookup] and modify its threat value and move it in the ThreatUnits linkedlist so it remains sorted [O(n) linkedlist, for n slots the PlayerUnit moves up/down in ThreatUnits threatlist].
2. Otherwise, create a new one [save to hashtable].
2a. Add the new ThreatListEntry in the PlayerUnits linkedlist [O(1) LinkedList add]
2b. Add it to the ThreatUnit linkedlist in the right spot (so it remains sorted). [This is O(n) linkedlist, but when adding zero threat its O(1].

To remove a ThreatUnit or PlayerUnit only two LinkedList remove calls are required [O(1)]. The ThreatUnits linkedlist automatically remains sorted.

Iterating through a ThreatUnits list is really fast, there is one unitindexer lookup required to fetch the ThreatUnits internal object, and O(n) linkedlist reads to get the threatvalues/playerunits.

Getting the first PlayerUnit in a ThreatUnits threatlist is really fast too, only one unitindexer lookup is required.

Im not done with the whole API of ZTS but im sure i can add all the functions. Most of them should be equally fast or faster, some of them might end up being a bit slower tho (For example a "Get 5. slot in ThreatUnits threatlist" would require 5 iterations in the linkedlist). The very frequently used features should be a bit faster than the ZTSs implementation tho (modifyThreat, the perodic loop, getFirstInThreatList(ThreatUnit u).

Right now there are no "addThreatUnit", "addPlayerUnit", "removeThreatUnit"... functions. Instead you have to create instances of the classes ThreatHandlerTU and ThreatHandlerPU, manage them yourself and destroy them yourself. This allows a bit more control at the cost of a less inconvenient handling. Not sure how i am going to solve this...

€: Actually the main reason im not using the "addThreatUnit" etc. approach is that i dont like hooks and automated unit indexing.
- Hooks are (apart from their nonexistence in Wurst) in my opinion extremely bad practice as they create very intransparent code. Imagine you have 10 libraries imported which all hook some natives - you end up having no clue what the hell is really happening when you use some of the natives. The next problem is that the order in which hooks are executed is undefined, so you have to be very careful when hooking functions that your own implementations are completely independent from possible other hooks (which you usually dont know at that point). However i have to agree that sometimes hooks provide a very easy solution to an otherwise very difficult problem.
- Automated UnitIndexing is very popular. Unitindexer systems detect all units entering and leaving the map and automatically assign unique indices to them. Some indexing systems provide some way to get notified or execute some action when a unit entered or left the map (e.g. using event systems "onIndex/onDeindex" or struct modules).
The weird thing is that in many maps (especially AoS, RPGs) the user takes full control of creating and removing units by creating ALL units via trigger (in particular there are no units trained by buildings) and removing ALL units via trigger (in particular when the map uses a customized damage system which redirects all damage dealt - including autoattacks - through a single damage function). When creating and removing all units manually it is completely senseless to use a automated unitindexer to detect these events as the user could just invoke the indexing and deindexing manually. You basically abandon the controlflow just to pick it up again using some heavyweight triggers. Doing this manually is a lot faster and allows more control of those "onIndex/onDeindex" actions.
For this reason i think for many purposes a very lightweight manual indexing system which only assigns ids to units (without detecting when they enter/leave the map) is sufficient.

I should probably move these ideas to the lab, as they are very general...
 
Last edited:
The thing about unit indexers is, that they work for every possible case. It's just drop & forget. Remember that a lot of users - even if they create AoS maps - might still use ordinary WC3 spells. And a lot of them are able to create units. A manual-control Indexer might make sense when coding a tower defence map or highly customized map with only channel-based abilities, but I feel that AoS is not a good example here. Also, I've literally never seen a map without preplaced units.
However, I never understood why people made unit-indexers index units on map entry, not on the first GetUnitIndex call. It simply makes no sense to index a unit the moment it enters the map, unless you want to use UnitUserData directly instead of GetUnitIndex.


Hmm, I don't think that the idea of a user-controlled threat handler is a great idea. I think you should really write clean and simple wrappers for this, to allow the simple user interface that ZTS provides.

Just seeing this from the practical point of view here: what would you need the handler for that you could not do with the basic getters and setters of ZTS?
Even things like threat meters are simple to do with just the getters, as all the getters are pretty fast anyway.
I think the only thing that ZTS really needs to get that extra amount of flexibility are custom events:

onEnterCombat (useful for disabling buffs that should only work outside of combat like increased movement speed)
onLeaveCombat
onThreatModify (I found this to be an extremely desirable event! Extremely useful for buffs that increase/decrease the amount of threat generated)
onThreatPositionChange (useful if you want some kind of SFX to appear when a unit changes its current target)
onOrder (basicly replaces the public boolean by an event - which makes it much less GUI friendly as then you can not abuse the GUI-IssueOrder event anymore, so I might keep them both)

I'm currently thinking about updating ZTS and adding an optional event lib to provide these custom events. The drawback is that it will slow down ModifyThreat a lot, as event libraries basicly do TriggerExecutes all the time.
It might be better in the end to have a public thread additive and multiply modifier instead.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
What bothers me the most is to set up some sort of priority (threat).
The core question is what are useful parameters, without creating too much overhead.
In the basic ZTS example it adds the incoming damage.
For this operation i.e two parameters could be important, which is max hit range (not sure if there is a native to get that one) and movementspeed.
For example a slowed meele units gets incoming damage of 50 from 1200 distance range unit. This threat is definitly not worth 50 points.
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
The system doesnt cover applying threat from damage. It only provides the datastructure to manage thread values and forces the ThreatUnits to attack the units with highest threat.
If you want attacks to generate threat you need to use a damage detection system and call "modifyThreat(attacker, target, damage, true)".

€: oh now i understand what you are saying: The AI controlled units shouldnt simply attack the playerunits with highest threat value but use a heuristik to decide which unit to target.
I get the idea, but it makes the whole thing much more complex and harder to maintain high performance (at least for a clean generic solution), but i will think about it.
Another problem is that the AI gets intransparent. If you have a map which displays your current threat on a selected unit you can tell when its safe to attack without pulling aggro. In gaias everyone sees their % threat compared to #1 in list (or #2 in list when you are #1). If you cant trust those values anymore its getting complicated...
 
I think what makes more sense would be a validity checker for orders.

In other words, if the unit can not reach the target (i.e. because the pathing is blocked by units), it will not attack the first in threat, but the second (or simply the closest target).

I've been thinking about this a lot in the past and simply could never figure out a fast and good way to handle it. It would basicly require a custom pathing algorithm and those things almost never work.
A timer based approach where if a unit can not reach the target for 10 seconds will change targets obviously has two severe drawbacks:
It's not instant. Which means the unit will still have 10 seconds of "dumbness".
It also affects important gameplay mechanics like slowing a target and kiting.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I made some tests with "smart", I think you already know the results:
1. Smart changes target after a certain amount of time, if the target can't be reached.
2. Smart can't be ordered on the ground ( The function will always return false), but the unit starts moving. However the unit does not attempt to attack any nearby units.
3. "attack" is somehow the stupid version of "smart", but can be ordered on the ground.
4. A unit ordered smart will try to attack the target even if
- The target is standing on a cliff
- The path is blocked (pathing blocker)
- The path is blocked (units) --> will change target after beeing blocked.

Conclusion smart is not smart :/

It would basicly require a custom pathing algorithm and those things almost never work.
very heavy operation and difficult. May also be inaccurate.
You can't cover every aspect, but we can improve the basics which a proper setup ZTS does very nicely already.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
The most logical AI would attack whoever it can deal highest damage to at the lowest cost. The cost could be something like time wasted, or own health lost.
As such, a pure tank like in many games is utterly worthless, because he'll never be attacked anyway.
It would be an ideal approach, but definitely a very hard one, as it usually takes human intuition to know who to attack.
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
@Xonok: I disagree, for me the most intuitive behaviour would be to attack the guy who disturbs me the most (by dealing damage to me, healing the guys i try to kill, ...). Why attack someone who doesnt do anything, even if i could oneshot him?

Anyway this is not important as the system doesnt cover adding threat for specific actions. It only provides a function "modifyThreat", how you use it is up to you.


What zwiebelchen suggested is a mechanic that requires a valid threat list but offers the possibility to ignore it, e.g. if the first target cant be reached.

I agree that using custom pathing is not viable. Too slow and prone to malfunction. Detecting if a target cannot be reached at all sounds important, detecting if a target can only be reached inefficiently (like its far away and the ThreatUnit is slowed) not so much (i think kiting a unit is a valid thing, if you want to forbit that you shouldnt allow slowing down the ThreatUnit so much).
 
What came to my mind was tracking the following two variables and creating intelligent "conclusions" by tracking them:

1) the position of the AI controlled unit
2) the time the AI controlled unit did not attack the target (or cast a spell on the target)

The main idea is that, if a target can not be reached after a certain amount of seconds and the position of the AI controlled unit has not changed position during that time, the unit is "stuck" and can not reach that target. You can expect that since the pathing AI of WC3 will always move a unit to the closest point towards its target and then simply do nothing until the pathing is valid again.
Of course, there's two things that interfere with that logic: stuns and pushback effects. Stuns should result in pausing the timer. I don't really know how to handle pushback effects, though.


You could still exploit a system like that by forcing the unit to move around (for example when blocking a narrow path with a fat unit, you could slowly move that unit backwards), but at least it's getting a lot harder to exploit it, especially when the area abused to exploit the pathing is very small.


Another idea I had was applying windwalk to AI controlled units for a split-second after x seconds not reaching the target. This will allow them to move through units in case the pathing is blocked by units.
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
Sounds interesting. Lets say the ai controlled unit cannot reach #1 in its threatlist. After some time the system consideres the ai controlled unit as stuck and orders it to attack #2 in threatlist. Then there has to be a unstuck detection so the ai controlled unit switches back to #1 in threatlist when possible.
This could be done by periodically spawning dummies at the ai controlled units position and ordering them to move to the target. If they can reach it, the ai controlled unit switches back to #1 in threat list. (Of course this has to be done for all "skipped" units).
Any simpler ideas to solve this?
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Well, actually checking if a unit can be reached is bound to be impractical, so possibly that you constantly keep tabs on how much time it would take to get close enough to an enemy to harm it and also how much harm you would take while doing so.
Those 2 should be compared to find out the best compromise between attacking a harmless noob nearby and taking down that sniper far away.
 
The problem I have with an approach based on attack redirection is, that you can still abuse it, just in a different fashion:
If you use the tank to block the pathing, all damagedealers can overaggro as much as they want without fear that the unit will ever reach them. As it will redirect the attacks to targets the unit can reach, it will simply hit the tank.


Imho, it's way better to disable collision after a certain amount of time. This will make the unit "sneak through". It's kind of cheating, but so is bypassing the threat system by blocking the pathing.


The only reason we really need the collision of units is, that it makes it hard to click on units that are clumped up with a collision size of 8. Increasing the collision size but allowing the unit to bypass collision in certain cases might be the best solution.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
The problem I have with an approach based on attack redirection is, that you can still abuse it, just in a different fashion:
If you use the tank to block the pathing, all damagedealers can overaggro as much as they want without fear that the unit will ever reach them. As it will redirect the attacks to targets the unit can reach, it will simply hit the tank.


Imho, it's way better to disable collision after a certain amount of time. This will make the unit "sneak through". It's kind of cheating, but so is bypassing the threat system by blocking the pathing.

Not sure how to solve the general problem, but it would be a fun alternative to make a boss that hits much harder if it can't attack the intended target.
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
also how much harm you would take while doing so

This is impossible to calculate (and estimations are not reliable), and also not the point of a threat system.
I think threat should only be based on former actions, not on estimations of future happenings.

Removing collision is critical.. not only will players be like "wtf" but it might screw up other game mechanics :/

How about when an ai controlled unit cant reach its target it will (maybe after a small delay) increase the threat on all the units it can reach drastically (e.g. by a 20% of the threat difference each 0.1 seconds). This way the ai controlled unit will naturally lose threat on the target it cant reach.
By adding "target cannot be reached"-events this doesnt even have to be done by the system itself.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
I don't think increasing the threat is a good idea. However, it makes sense that threat simply matters more to closer units.

Think of the usual blizzard system. In some aspects it is very good, for instance, units are smart enough to focus on damaged enemies.
While in most cases I've seen threat systems being used to make AI more stupid, such as for making them attack tanks before others, even if the truly damaging enemies are easier to reach.
 
How about when an ai controlled unit cant reach its target it will (maybe after a small delay) increase the threat on all the units it can reach drastically (e.g. by a 20% of the threat difference each 0.1 seconds). This way the ai controlled unit will naturally lose threat on the target it cant reach.
By adding "target cannot be reached"-events this doesnt even have to be done by the system itself.
This would basicly render threat management pointless. If you overaggro, you can just move out of reach and your threat will magically decrease.

Actually, this will happen regardless of the implementation of the "can't reach" mechanic. I agree here. The reaction of the unit should be customizable by the user (for example by teleporting to the target, using a meathook-kind of spell or disabling pathing temporarily).
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
This is a fallback mechanic which should only activate when the target unit somehow managed to get unreachable for the ai controlled unit, which *shouldnt* happen. If you design a bossfight so that your player heroes can just step behind a rock and get unreachable for the boss while attacking him you destroy the whole point of a threatsystem.
I guess there is no solution for this which is not abusable.
 
This is a fallback mechanic which should only activate when the target unit somehow managed to get unreachable for the ai controlled unit, which *shouldnt* happen. If you design a bossfight so that your player heroes can just step behind a rock and get unreachable for the boss while attacking him you destroy the whole point of a threatsystem.
I guess there is no solution for this which is not abusable.
Well yeah, it really depends on the area design.

But take for example a huge golem boss. You will most likely want it to have a collision of 32-48 so that heroes will attack it from a distance that looks good (hit boxes unfortunately are determined by the collision size of the unit, not the model size). When you do this, you can basicly "sorround" it with units (for example spawned minions) and it will never be able to reach ranged damage dealers. This will basicly happen automaticly, even without any exploitation attempt.

The only real way around this is by deactivating pathing for all player units completely. If you do that, bigger units will always be able to reach the player units, but the player units will still hit a huge monster from a graphically pleasing distance.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
What about adding a boolean hasHitTarget or depending on you clock timeout an integer hasHit.

using boolean:
- At the end of the periodic loop you set it to false --> Your damage detection system sets it to true on damage event.

using integer:
- Damage detection resets it to i.e. 4 --> Each loop decreases the number by 1. --> if the number hits 0 the target is invalid.
- Allows to define an accuracy based on the timer interval and the reset value.

Of course this messes up in case the Threat unit is chasing a unit which does flee.

Edit: New fact: "smart" is directly connected to the acquisation range. For targets beyond this range the order will return false. It does also return false for invalid target (Invis, immun, ....)
unlike "attack" which does always return true and always forces the unit to action.
 
Last edited:
What about adding a boolean hasHitTarget or depending on you clock timeout an integer hasHit.

using boolean:
- At the end of the periodic loop you set it to false --> Your damage detection system sets it to true on damage event.

using integer:
- Damage detection resets it to i.e. 4 --> Each loop decreases the number by 1. --> if the number hits 0 the target is invalid.
- Allows to define an accuracy based on the timer interval and the reset value.

Of course this messes up in case the Threat unit is chasing a unit which does flee.
It's not about how to implement it. It's about how to make it failure-proof and possibly instant.

Edit: New fact: "smart" is directly connected to the acquisation range. For targets beyond this range the order will return false. It does also return false for invalid target (Invis, immun, ....)
unlike "attack" which does always return true and always forces the unit to action.
We know that already. It doesn't help, still, as it will not return false if the path is blocked by units.
 
Status
Not open for further replies.
Top