• 🏆 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] StatHandler

Status
Not open for further replies.
Level 14
Joined
Jun 27, 2008
Messages
1,325
StatHandler

This package can be used to manage custom stats, assign them to items/buffs/etc. and apply them to units. The way how multiple stat values accumulate can be selected for each stat individually, it is also possible to link stats to give them an absolute and a percental component. By using caches and the appropriate data structures this generic system offers a similar or even higher performance than the usual hardcoded approaches.
This replaces http://www.hiveworkshop.com/forums/lab-715/wurst-stathandler-design-issues-247795/

GitHub: https://github.com/muzzel/WurstDemo/tree/master/wurst/lib/StatHandler

Requires: Rational
Optional: A unit indexing system.

Code:
Wurst:
package StatHandler
import public StatHandlerConfig
import Rational
import HashMap
import LinkedList
// By muzzel - Version 1.1

/** Creates a new RawStatBuffer initialized with default base stats for the specified unitType. */
public function createBaseStatBuffer(int unitTypeId) returns UnitBaseStatBuffer
	return new UnitBaseStatBuffer(unitTypeId)
	
/** Assigns a new UnitStatBuffer to the specified unit. */
public function createUnitStatBuffer(unit u)
	new UnitStatBuffer(u)
	
/** Destroys this units UnitStatBuffer, if any assigned. */
public function destroyUnitStatBuffer(unit u)
	let buffer = mapUnitStatBuffers[getUnitIndex(u)]
	if buffer castTo int != 0
		destroy buffer

/** 
 * Returns the specified units UnitStatBuffer.
 * Returns null if no UnitStatBuffer assigned.
 * Requires getUnitIndex(). Returns null if not available.
 */
public function getUnitStatBuffer(unit u) returns UnitStatBuffer
	return mapUnitStatBuffers[getUnitIndex(u)]

/**
 * Returns a units stat.
 * Warning if no UnitStatBuffer assigned.
 * Requires getUnitIndex(). Warning if not available.
 */
public function unit.getStat(Stat s) returns int
	let buffer = mapUnitStatBuffers[getUnitIndex(this)]
	if buffer castTo int == 0
		printWarning("StatHandlerConfig: A called function requires a valid implementation of getUnitIndex(unit u).")
	return buffer.get(s)
	
/**
 * Returns a units base stat.
 * Warning if no UnitStatBuffer assigned.
 * Requires getUnitIndex(). Warning if not available.
 */
public function unit.getBaseStat(Stat s) returns int
	let buffer = mapUnitStatBuffers[getUnitIndex(this)]
	if buffer castTo int == 0
		printWarning("StatHandlerConfig: A called function requires a valid implementation of getUnitIndex(unit u).")
	return buffer.getBaseStat(s)

/**
 * Applies the specified StatList to the unit.
 * Warning if no UnitStatBuffer assigned.
 * Requires getUnitIndex()! Warning if not available.
 */
public function unit.applyStatList(StatList statList)
	let buffer = mapUnitStatBuffers[getUnitIndex(this)]
	if buffer castTo int == 0
		printWarning("StatHandlerConfig: A called function requires a valid implementation of getUnitIndex(unit u).")
	buffer.apply(statList)
	
/**
 * Removes the specified StatList from the unit.
 * Warning if no UnitStatBuffer assigned.
 * Requires getUnitIndex()! Warning if not available.
 */
public function unit.removeStatList(StatList statList)
	let buffer = mapUnitStatBuffers[getUnitIndex(this)]
	if buffer castTo int == 0
		printWarning("StatHandlerConfig: A called function requires a valid implementation of getUnitIndex(unit u).")
	buffer.remove(statList)

/** Registers the specified stat to grow linearly. */
public function registerStatLin(Stat stat)
	statType[stat castTo int] = StatType.LINEAR
	statSubstat[stat castTo int] = -1
	parentStat[stat castTo int] = -1
	numStats++
	
/** Registers the specified stat to grow linearly and adds a StatChangedEvent. */
public function registerStatLin(Stat stat, StatChangedEvent statChangedEvent)
	statChangedEvents[stat castTo int] = statChangedEvent
	registerStatLin(stat)
	
/** Registers the specified stat to grow exponentially. */
public function registerStatExp(Stat stat)
	statType[stat castTo int] = StatType.EXPONENTIAL
	statSubstat[stat castTo int] = -1
	parentStat[stat castTo int] = -1
	numStats++
	
/** Registers the specified stat to grow exponentially and adds a StatChangedEvent. */
public function registerStatExp(Stat stat, StatChangedEvent statChangedEvent)
	statChangedEvents[stat castTo int] = statChangedEvent
	registerStatExp(stat)

/** Registers the specified stat to grow logistically. */
public function registerStatLog(Stat stat)
	statType[stat castTo int] = StatType.LOGISTIC
	statSubstat[stat castTo int] = -1
	parentStat[stat castTo int] = -1
	numStats++
	
/** Registers the specified stat to grow logistically and adds a StatChangedEvent. */
public function registerStatLog(Stat stat, StatChangedEvent statChangedEvent)
	statChangedEvents[stat castTo int] = statChangedEvent
	registerStatLog(stat)

/** Registers the specified stat to grow linearly and adds a linear substat. */
public function registerStatLinLin(Stat stat, Stat substat)
	registerStatLin(stat)
	registerStatLin(substat)
	statSubstat[stat castTo int] = substat castTo int
	parentStat[substat castTo int] = stat castTo int
	
/** Registers the specified stat to grow linearly, adds a linear substat and a StatChangedEvent. */
public function registerStatLinLin(Stat stat, Stat substat, StatChangedEvent statChangedEvent)
	statChangedEvents[stat castTo int] = statChangedEvent
	registerStatLinLin(stat, substat)
	
/** Registers the specified stat to grow linearly and adds an exponential substat. */
public function registerStatLinExp(Stat stat, Stat substat)
	registerStatLin(stat)
	registerStatExp(substat)
	statSubstat[stat castTo int] = substat castTo int
	parentStat[substat castTo int] = stat castTo int
	
/** Registers the specified stat to grow linearly, adds a exponential substat and a StatChangedEvent. */
public function registerStatLinExp(Stat stat, Stat substat, StatChangedEvent statChangedEvent)
	statChangedEvents[stat castTo int] = statChangedEvent
	registerStatLinExp(stat, substat)

StatType array statType
int array statSubstat // references the stats substat, -1 if none
int array parentStat // references the substats stat, -1 if none
StatChangedEvent array statChangedEvents
int numStats = 0 // number of stats total
HashMap<int, UnitBaseStatBuffer> mapBaseStatBuffers = new HashMap<int, UnitBaseStatBuffer>()
UnitStatBuffer array mapUnitStatBuffers
UnitBaseStatBuffer DEFAULT_BASE_STAT_BUFFER

public interface StatChangedEvent
	function statChanged(unit u, Stat s)

enum StatType
	LINEAR
	EXPONENTIAL
	LOGISTIC

class StatEntity
	Stat stat
	int val
	
	construct(Stat stat, int val)
		this.stat = stat
		this.val = val

/** A dynamic list which can hold an arbitrary amount of stats. */
public class StatList
	protected LinkedList<StatEntity> statList
	
	construct()
		statList = new LinkedList<StatEntity>()
		
	/**
	 * Add a stat with the specified value.
	 * For exponential/logistic stats use percental values.
	 */
	function add(Stat s, int value)
		switch statType[s castTo int]
			case LINEAR
				statList.add(new StatEntity(s, value))
			case EXPONENTIAL
				statList.add(new StatEntity(s, value+100))
			case LOGISTIC
				statList.add(new StatEntity(s, 100-value))
		
	ondestroy
		for StatEntity s in statList
			destroy s
		destroy statList

/**
 * A sized list which buffers a units base stats.
 * This is used to set default values for UnitStatBuffers.
 */
public class UnitBaseStatBuffer
	private static int array stats
	private int arrayStart
	
	/** Creates a new RawStatBuffer initialized with default base stats for the specified unitType. */
	construct(int unitType)
		mapBaseStatBuffers.put(unitType, this)
		arrayStart = 1 + (this castTo int - 1) * numStats
		for n = 0 to numStats
			if statType[n] == StatType.LINEAR
				stats[arrayStart + n] = 0
			else // EXPONENTIAL, LOGISTIC
				stats[arrayStart + n] = 100
		
	/** Sets the specified stat to the specified value. */
	function set(Stat s, int value)
		switch statType[s castTo int]
			case LINEAR
				stats[s2i(s)] = value
			case EXPONENTIAL
				stats[s2i(s)] = value+100
			case LOGISTIC
				stats[s2i(s)] = 100-value
	
	/** Returns the array index for the specified stat. */
	protected function s2i(Stat s) returns int
		return arrayStart + s castTo int
	
	/** Returns the base value (in the internal representation) of the specified stat. */
	protected function getStatRaw(Stat s) returns int
		return stats[arrayStart + s castTo int]

/**
 * A sized list which buffers a units stats.
 * Allows adding and removing StatLists and cached retrieval of single stats.
 */
public class UnitStatBuffer
	private static int array statsAbs
	private static rational array statsPerc
	// Cache:
	private static int array cache
	private static boolean array cacheDirty
	// Stat changed events:
	private static Stat array eventsToInvoke
	private static int eventsToInvokeLast
	private static boolean array isEventAdded
	private int arrayStart
	private unit u
	private UnitBaseStatBuffer baseStats
	
	/**
	 * Creates a new UnitStatBuffer for the specified unit.
	 * Initializes it with values from the UnitBaseStatBuffer assigned to the units unittype.
	 */
	construct(unit u)
		mapUnitStatBuffers[getUnitIndex(u)] = this
		reset(u, mapBaseStatBuffers.get(u.getTypeId()))
	
	/**
	 * Creates a new UnitStatBuffer for the specified unit.
	 * Initializes it with values from the specified UnitBaseStatBuffer.
	 * Uses a default UnitBaseStatBuffer if null.
	 */
	construct(unit u, UnitBaseStatBuffer baseStats)
		mapUnitStatBuffers[getUnitIndex(u)] = this
		reset(u, baseStats)
	
	ondestroy
		mapUnitStatBuffers[getUnitIndex(u)] = null
	
	private function reset(unit u, UnitBaseStatBuffer baseStatBuffer)
		this.u = u
		if baseStatBuffer == null
			this.baseStats = DEFAULT_BASE_STAT_BUFFER
		else
			this.baseStats = baseStatBuffer
		arrayStart = 1 + (this castTo int - 1) * numStats
		for n = 0 to numStats
			cacheDirty[arrayStart + n] = true
			// Copy UnitBaseStatBuffer:
			if statType[n] == StatType.LINEAR
				statsAbs[arrayStart + n] = baseStats.getStatRaw(n castTo Stat)
			else // EXPONENTIAL, LOGISTIC
				statsPerc[arrayStart + n] = rational(baseStats.getStatRaw(n castTo Stat), 100)
		 
	
	/** Retrieves the specified stats value, reads it from cache if available. */
	function get(Stat s) returns int
		int result = 0 
		if cacheDirty[arrayStart + s castTo int]
			// cache miss
			result = getStatRecalc(s castTo int)
			cache[arrayStart + s castTo int] = result
			cacheDirty[arrayStart + s castTo int] = false
		else
			// cache hit
			result = cache[arrayStart + s castTo int]
		return result
	
	/** Retrieves the specified stats base value. */
	function getBaseStat(Stat s) returns int
		int res = 0
		switch statType[s castTo int]
			case LINEAR
				res = baseStats.getStatRaw(s)
			case EXPONENTIAL
				res = baseStats.getStatRaw(s)-100
			case LOGISTIC
				res = 100-baseStats.getStatRaw(s)
		return res
		
	
	/** Calculates the specified stats value. */
	private function getStatRecalc(int statId) returns int
		int result = 0
		switch statType[statId]
			case LINEAR
				result = statsAbs[arrayStart + statId]
			case EXPONENTIAL
				result = (statsPerc[arrayStart + statId] * rational(100, 1)).toInt() - 100
			case LOGISTIC
				result = 100 - (statsPerc[arrayStart + statId] * rational(100, 1)).toInt()
		if statSubstat[statId] != -1
			// TODO: read substat value from cache, problem: cyclic dependency
//			result = (result*(100+getStat(statSubstat[statId] castTo Stat))) div 100
			result = (result*(100+getStatRecalc(statSubstat[statId]))) div 100
		return result
	
	/** Adds the specified stats event to the event queue, if not added yet. */
	private static function eventQueueAdd(Stat s)
		if not isEventAdded[s castTo int] and statChangedEvents[s castTo int] != null
			isEventAdded[s castTo int] = true
			eventsToInvokeLast++
			eventsToInvoke[eventsToInvokeLast] = s
	
	/** Resets the event queue. */
	private static function eventQueueReset()
		eventsToInvokeLast = -1
		
	/** Fires all events in the event queue. */
	function eventQueueInvoke()
		for n = 0 to eventsToInvokeLast
			isEventAdded[eventsToInvoke[n] castTo int] = false
			statChangedEvents[eventsToInvoke[n] castTo int].statChanged(u, eventsToInvoke[n])
	
	/** Applies all stat values from the specified StatList to this UnitStatBuffer. */
	function apply(StatList l)
		eventQueueReset()
		for StatEntity s in l.statList
			// Mark cache dirty:
			cacheDirty[arrayStart + s.stat castTo int] = true
			if parentStat[s.stat castTo int] != -1
				cacheDirty[arrayStart + parentStat[s.stat castTo int]] = true
				eventQueueAdd(parentStat[s.stat castTo int] castTo Stat)
			else
				eventQueueAdd(s.stat)
			// Accumulate values:
			if statType[s.stat castTo int] == StatType.LINEAR
				statsAbs[arrayStart + s.stat castTo int] += s.val
			else // EXPONENTIAL, LOGISTIC
				statsPerc[arrayStart + s.stat castTo int] *= rational(s.val, 100)
		eventQueueInvoke()
		
	/** Removes all stat values from the specified StatList from this UnitStatBuffer. */
	function remove(StatList l)
		eventQueueReset()
		for StatEntity s in l.statList
			// Mark cache dirty:
			cacheDirty[arrayStart + s.stat castTo int] = true
			if parentStat[s.stat castTo int] != -1
				cacheDirty[arrayStart + parentStat[s.stat castTo int]] = true
				eventQueueAdd(parentStat[s.stat castTo int] castTo Stat)
			else
				eventQueueAdd(s.stat)
			// Accumulate values:
			cacheDirty[arrayStart + s.stat castTo int] = true
			if statType[s.stat castTo int] == StatType.LINEAR
				statsAbs[arrayStart + s.stat castTo int] -= s.val
			else // EXPONENTIAL, LOGISTIC
				statsPerc[arrayStart + s.stat castTo int] /= rational(s.val, 100)
		eventQueueInvoke()

init
	statRegistry()
	DEFAULT_BASE_STAT_BUFFER = new UnitBaseStatBuffer(0)


Config (example):
Wurst:
package StatHandlerConfig
import StatHandler

/**
 * Make this function return a unique index if you want to use the unit extension functions.
 * You can also comment it out and "import public" a package which provides this function.
 */
public function getUnitIndex(unit u) returns int
	return 0

/** List your stats here. */
public enum Stat
	DAMAGE
	DAMAGE_PERC
	ARMOR
	EVASION

/**
 * Register all your stats here. Available functions:
 * - Linear stat:
 *		registerStatLin(Stat stat)
 * - Exponential stat:
 *		registerStatExp(Stat stat)
 * - Logistic stat:
 *	registerStatLog(Stat stat)
 * - Linear stat with linear substat:
 *		registerStatLinLin(Stat stat, Stat substat)
 * - Linear stat with exponential substat:
 *		registerStatLinExp(Stat stat, Stat substat)
 */
public function statRegistry()
	registerStatLinLin(Stat.DAMAGE, Stat.DAMAGE_PERC, (unit u, Stat s) -> print("Stat changed: Damage"))
	registerStatLin(Stat.ARMOR, (unit u, Stat s) -> print("Stat changed: Armor"))
	registerStatLog(Stat.EVASION)


Defining Stats:
To define a stat you first have to add a new constant to the enum "Stat" in StatHandlerConfig. Then you have to register the stat by calling one of the following functions. These functions determine how multiple stat values are accumulated.

1. Linear - registerStatLin(Stat.ARMOR):
Simply adds the values. This should be used for most absolute values like Damage, Armor, Strength, ...
Example: (20 Armor) and (20 Armor) results in (40 Armor).

2. Exponential - registerStatExp(Stat.SPELLPOWER):
Treats values as percental and multiplies them. This is usually used for percental stats which increase an absolute value.
Example: (50% Spellpower) and (50% Spellpower) result in (125% Spellpower).
Consider your spell deals 100 Damage. If you add a 50% Spellpower increase it does 150 Damage. If you increase those 150 Damage again by 50% you end up at 225 Damage, which is equal to 100 Damage with 125% Bonus damage.

3. Logistic - registerStatLog(Stat.EVASION):
Treats values as percental and multiplies their inverse. This is usually used for chances that rolled after each other.
Example: (10% Evasion) and (10% Evasion) results in (19% Evasion).
Consider you have 10% evasion and you get attacked 100 times. You will evade 10 times and get hit 90 times. Consider you get another chance of 10% to evade if your first try failed. Then you will evade another 9 times (10% of 90) and get hit 81 times. So overall you get hit 81 times and evade 19 times, which is equal to 19% evasion.
A sideeffect of this is that this automatically caps at 100% as you can only get very close to 100%, but never reach it.

In addition to these three types you can use combined types which will add an additional substat to the registered stat. The substat will be considered percental and increase the stat in the respective way. The substat will automatically be taken into account when retrieving the stat!

4. Linear stat, linear substat - registerStatLinLin(Stat.DAMAGE, Stat.DAMAGE_PERCENT)
Both stat and substat accumulate separately in a linear manner, when retrieving the stat the substat is interpreted as percental and multiplied to the stat.
Example: (35 Damage), (65 Damage), (40% Damage), (40% Damage) results in (100 Damage), (80% Damage) which results in (180 Damage).

5. Linear stat, exponential substat - registerStatLinExp(Stat.DAMAGE, Stat.DAMAGE_PERCENT)
Stat values accumulate in a linear manner while substats accumulate exponentially. When retrieving the stat the substat is multiplied to the stat.
Example: (35 Damage), (65 Damage), (40% Damage), (40% Damage) results in (100 Damage), (96% Damage) which results in (196 Damage).


Usage:
1. UnitBaseStatBuffer
To define the base stats for a unittype create a new object of UnitBaseStatBuffer and pass the desired base stats to it. Stats which get no base value assigned will use a default one (zero).

Wurst:
let buffer = new UnitBaseStatBuffer('hfoo')
buffer.add(Stat.DAMAGE, 5)
buffer.add(Stat.ARMOR, 2)

2. UnitStatBuffer
Creating a UnitStatBuffer for a unit allows it to apply stats to it.

Wurst:
let u = CreateUnit(Player(0), 'hfoo', 0., 0., 0.)
let buffer = new UnitStatBuffer(u)

The UnitStatBuffer will automatically look for UnitBaseStatBuffers assigned to the unittype of u. It is also possible to pass a UnitBaseStatBuffer directly as second argument (useful if different unittypes are sharing a UnitBaseStatBuffer).

3. StatList
A StatList is a dynamic list which can hold an arbitrary amoint of stats. When a StatList is applied to a UnitStatBuffer all its stats will be added to the UnitStatBuffers unit. StatList are perfect for storing stats of items or buffs.

Wurst:
let u = CreateUnit(Player(0), 'hfoo', 0., 0., 0.)
let buffer = new UnitStatBuffer(u)
// Create a new StatList for item1:
let item1 = new StatList()
item1.add(Stat.ATTACKSPEED, 10)
item1.add(Stat.AGILITY, 6)
// Create a new StatList for item2 (cascade operator syntax):
let item2 = new StatList()..add(Stat.DAMAGE, 4)..add(Stat.DAMAGE_PERCENTAL, 15)..add(Stat.EVASION, 10)
// Apply item1 to the StatBuffer of u:
buffer.apply(item1)
// Apply item2:
buffer.apply(item2)
// Remove item1:
buffer.remove(item1)

4. Retrieving stats
To get a units current stats call its UnitStatBuffers getStat function. Getting a stat which has a substat will return the combined value of stat and substat!

Wurst:
let u = CreateUnit(Player(0), 'hfoo', 0., 0., 0.)
let buffer = new UnitStatBuffer(u)
let item1 = new StatList()..add(Stat.DAMAGE, 8)..add(Stat.DAMAGE_PERCENTAL, 50)
buffer.apply(item1)
int damage = buffer.get(Stat.DAMAGE) // will return 8+50% = 12


Optional UnitIndexer:
If a UnitIndexing system is available the "getUnitIndex(...)" function in StatHandlerConfig can be overwritten to return the correct values. Alternatively the function can be removed and a package which provides the same function can be imported ("import public") to StatHandlerConfig.
Providing a UnitIndexer will enable additional functions, which are basically more comfortable wrappers for the normal functions.

Wurst:
public function createUnitStatBuffer(unit u)
public function destroyUnitStatBuffer(unit u)
public function getUnitStatBuffer(unit u) returns UnitStatBuffer
public function unit.getStat(Stat s) returns int
public function unit.applyStatList(StatList statList)
public function unit.removeStatList(StatList statList)


Todo list:
- Left to decide: Make unitindexer a requirement, remove most of the normal functions and only provide those mentioned in "Optional UnitIndexer". -> Smaller API?
- provide a way to reset a units stats?

Changelog:
1.1 Lots of fixes, did intensive testing
1.0 (stable) UnitStatBuffer constructor allows "null" UnitBaseStatBuffer argument and uses a default (empty) one, Added StatChangedEvents,
0.9 Initial unstable release
 
Last edited:
Status
Not open for further replies.
Top