• 🏆 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 - Design Issues...

Status
Not open for further replies.
Level 14
Joined
Jun 27, 2008
Messages
1,325
Deprecated - new version is here: http://www.hiveworkshop.com/forums/lab-715/wurst-stathandler-249028/
But i still want to keep this threat alive as the problems i described here are very general and still unsolved.


This package can be used to manage stats, assign them to items/buffs/etc. and apply them to units. All stats have an absolute and a percental component which are stored in a way that ensures that the order of assigning different stats is irrelevant. This also removes numerical drifts (e.g. when you apply and unapply 31% damage over and over the value will not change).

Requires: http://www.hiveworkshop.com/forums/submissions-414/wurst-rational-247585/

Basic usage:
1. Config package: To define a stat, add it to the Enum Stat and call initStatType. The optional second parameter of initStatType is a lambda function which is called when the stat is changed.
2. For all items/buffs/etc. create a StatList and add stats to it by calling .add(Stat stat, int value) or .addPerc(Stat stat, int percent). This class is basically a linkedlist of stats.
3. For all units create a UnitStatBuffer. This class is an array which holds the units current stats.
4. To apply a StatList to a UnitStatBuffer call unitStatBuffer.applyStatList(statList). This also fires the StatChangedEvents of the stats which are changed, if existent.

Code:
Java:
package StatHandler
import public StatHandlerConfig
import LinkedList
import Rational

int numStats
int numStatsMinusOne
StatChangedEvent array statChangeEvents

public interface StatChangedEvent
	function apply(UnitStatBuffer buffer)

public function initStatType(Stat s)
	statChangeEvents[s castTo int] = null
	numStats++

public function initStatType(Stat s, StatChangedEvent e)
	statChangeEvents[s castTo int] = e
	numStats++
	
class StatAbs
	Stat stat
	int val
	
	construct(Stat stat, int val)
		this.stat = stat
		this.val = val

class StatRel
	Stat stat
	rational val
	
	construct(Stat stat, rational val)
		this.stat = stat
		this.val = val

public class StatList
	protected LinkedList<StatAbs> statListAbs
	protected LinkedList<StatRel> statListRel
	
	construct()
		statListAbs = new LinkedList<StatAbs>()
		statListRel = new LinkedList<StatRel>()
	
	/** Add ab absolute stat. */
	function add(Stat s, int val)
		statListAbs.add(new StatAbs(s, val))
	
	/** Add relative stat. The value will be considered as percental.
	 *  Example: addPerc(s, 55) will register a relative stat of 1.55 */
	function addPerc(Stat s, int val)
		statListRel.add(new StatRel(s, reduce(100+val, 100)))
	
	ondestroy
		for StatAbs s in statListAbs
			destroy s
		for StatRel s in statListRel
			destroy s
		destroy statListAbs
		destroy statListRel

public class UnitStatBuffer
	private static boolean array tmpDirty
	private static int array statsAbs
	private static rational array statsRel
	private static int size
	private int start
	
	protected static function initialize(int s)
		size = s
	
	construct()
		start = 1 + (this castTo int - 1) * size
		reset()
	
	/** Resets the absolute/relative buffer fields to 0/1. */
	function reset()
		for int n = start to start+size
			statsAbs[n] = 0
			statsRel[n] = rational(1, 1)
	
	/** Returns the stats value. */
	function getStat(Stat s) returns int
		return (statsRel[start + s castTo int] * rational(statsAbs[start + s castTo int], 1)).toInt()
	
	private function applyStatAbs(StatAbs s)
		statsAbs[start + s.stat castTo int] += s.val
	
	private function removeStatAbs(StatAbs s)
		statsAbs[start + s.stat castTo int] -= s.val
	
	private function applyStatRel(StatRel s)
		statsRel[start + s.stat castTo int] *= s.val
	
	private function removeStatRel(StatRel s)
		statsRel[start + s.stat castTo int] /= s.val
	
	private function invokeStatChangedEvents()
		for n = 0 to numStatsMinusOne
			if tmpDirty[n]
				statChangeEvents[n].apply(this)
	
	function applyStatList(StatList l)
		for n = 0 to numStatsMinusOne
			tmpDirty[n] = false
		for StatAbs s in l.statListAbs
			applyStatAbs(s)
			tmpDirty[s.stat castTo int] = true
		for StatRel s in l.statListRel
			applyStatRel(s)
			tmpDirty[s.stat castTo int] = true
		invokeStatChangedEvents()
		
	function removeStatList(StatList l)
		for n = 0 to numStatsMinusOne
			tmpDirty[n] = false
		for StatAbs s in l.statListAbs
			removeStatAbs(s)
			tmpDirty[s.stat castTo int] = true
		for StatRel s in l.statListRel
			removeStatRel(s)
			tmpDirty[s.stat castTo int] = true
		invokeStatChangedEvents()

init
	numStats = 0
	initStatTypes()
	UnitStatBuffer.initialize(numStats)
	numStatsMinusOne = numStats-1


Config (example):
Java:
package StatHandlerConfig
import StatHandler

public enum Stat
	STR
	AGI
	INT

public function initStatTypes()
	initStatType(Stat.STR, (UnitStatBuffer b) -> print("str: " + b.getStat(Stat.STR).toString()))
	initStatType(Stat.AGI, (UnitStatBuffer b) -> print("agi: " + b.getStat(Stat.AGI).toString()))
	initStatType(Stat.INT, (UnitStatBuffer b) -> print("int: " + b.getStat(Stat.INT).toString()))

Sample Application:
Java:
package Test
import StatHandler

/** Spawn some heroes/items and create their UnitStatBuffers/StatLists. */
function setup()
	for n = 0 to 3
		let b = new UnitStatBuffer()
		CreateUnit(Player(n), 'H000', 0., 0., 0.)..setUserData(b castTo int)
	for n = 1 to 5
		// Amulet
		let sl = new StatList()..add(Stat.INT, 10)
		CreateItem('I000', 100., 0.)..setUserData(sl castTo int)
	for n = 1 to 5
		// Sword
		let sl = new StatList()..add(Stat.STR, 10)..add(Stat.AGI, 5)
		CreateItem('I001', 200., 0.)..setUserData(sl castTo int)
	for n = 1 to 5
		// Helmet
		let sl = new StatList()..add(Stat.STR, 20)
		CreateItem('I002', 300., 0.)..setUserData(sl castTo int)
	for n = 1 to 5
		// Necklace
		let sl = new StatList()..add(Stat.STR, 5)..add(Stat.AGI, 5)..add(Stat.INT, 5)
		CreateItem('I003', 400., 0.)..setUserData(sl castTo int)
	for n = 1 to 5
		// Axe
		let sl = new StatList()..addPerc(Stat.STR, 100)
		CreateItem('I004', 500., 0.)..setUserData(sl castTo int)

/** Minimalist unit indexer. */
function  unitStats(unit u) returns UnitStatBuffer
	return u.getUserData() castTo UnitStatBuffer

/** Minimalist item indexer. */
function itemStats(item i) returns StatList
	return i.getUserData() castTo StatList
	
function pickupItem()
	unitStats(GetTriggerUnit()).applyStatList(itemStats(GetManipulatedItem()))

function dropItem()
	unitStats(GetTriggerUnit()).removeStatList(itemStats(GetManipulatedItem()))

init
	setup()
	CreateTrigger()
		..registerAnyUnitEvent(EVENT_PLAYER_UNIT_PICKUP_ITEM)
		..addAction(function pickupItem)
	CreateTrigger()
		..registerAnyUnitEvent(EVENT_PLAYER_UNIT_DROP_ITEM)
		..addAction(function dropItem)

This already works perfectly.

The only problem I have left is that the StatChangedEvents only have the UnitStatBuffer object passed. As the UnitStatBuffer currently has no reference to an actual unit there is no way to find out which unit belongs to the UnitStatBuffer (and the other way around).

This could easily be solved by adding a member "unit target" to the UnitStatBuffer and changing its constructor to:
Java:
	construct(unit target)
		this.target = target
		start = 1 + (this castTo int - 1) * size
		reset()

What I dont like about this solution is that some people prefer to use wrapper objects for their units, in that case it would be cleaner to give the UnitStatBuffer a member of that wrapper class. So id prefer to leave it to the user what kind of reference to the parent object he wants.
My first approach to do this was to make UnitStatBuffer generic:
Java:
public class UnitStatBuffer<T>
	// old stuff...
	private T parent
	
	construct(T parent)
		this.parent = parent
		// old stuff...
	
	function getParent() returns T
		return parent
This way the user could use UnitStatBuffer<UnitWrapper> to reference his unit wrapper or UnitStatBuffer<unit> to directly use the system with normal units.

The problem:
The apply() method of the StatChangedEvent interface has the UnitStatBuffer as an argument, so i would have to specify the generic argument here, which i dont know because i dont know how the users Wrapper class is called.
Java:
public interface StatChangedEvent
	function apply(UnitStatBuffer<?> buffer) // error, unknown type "?"

Any ideas? Of course id like suggestions/criticism about the system in general, too.
 
Last edited:

peq

peq

Level 6
Joined
May 13, 2007
Messages
171
I think the problem is, that you are using static functions and packages cannot be generic in Wurst. If you would put everything inside a class you could make that class generic. That could look somewhat like this:

Wurst:
// definition:

public class UnitStatBuffer<UnitType>
	...

plublic class StatHandler<UnitType>
	public function initStatType(Stat s)
		...
	
	public function initStatType(Stat s, StatChangedEvent<T> e)
		...

// usage:

public StatHandler<MyUnitType> statHandler = new StatHandler<MyUnitType>()

public function initStatTypes()
	statHandler
		..initStatType(Stat.STR, (UnitStatBuffer<MyUnitType> b) -> print("str: " + b.getStat(Stat.STR).toString()))
		..initStatType(Stat.AGI, (UnitStatBuffer<MyUnitType> b) -> print("agi: " + b.getStat(Stat.AGI).toString()))
		..initStatType(Stat.INT, (UnitStatBuffer<MyUnitType> b) -> print("int: " + b.getStat(Stat.INT).toString()))

Normally that would be the preferred approach in an object oriented language, but in Wurst it will probably give you performance problems, as there are no dynamic arrays in Jass.

That is a problem which comes up quite often, and Crigges always asks me about adding strange features to make it possible. Probably macros/templates are unavoidable for fast, generic code.
 
Status
Not open for further replies.
Top