Here you will find Blog posts about atavism
Most up-to-date version: AGIS Combat Traces (Google Drive)
Contents: AutoAttackTrace AbilityTrace EffectTrace CoordEffectTrace StatTrace EquipTrace
I'm fairly new to Atavism and, like many of you, am developing an MMO starting from scratch. This document started as my attempt to understand how auto-attack works but has evolved somewhat into a decent reference for the combat system. It is not complete, but it covers the basics. I define a trace as a logical chain of events in the code from start to finish. The traces involve files along the chain from player to server to database so they may refer to Java or CSharp files and include Unity, Server, and AGIS code references (AGIS requires a Veteran License). Keep in mind that many of the function references are not written verbatim from the code and are sometimes altered to make it easier to remember what certain variables represent. For example, an ability’s caster is referred to as ‘caster’, ‘obj’, ‘attacker’, and ‘source’ in different areas of the code and I simply wrote ‘attacker’ every time to keep it easy to remember. Sometimes I’ve directly copied comments from the code when they are descriptive. Furthermore, the language is often short-hand and not in complete sentences because I was moving fast. I encourage other developers to expand upon this and other plugin systems so that we can have basic references for everything.
Credits: to the Atavism Online team (Jacques and Andrew) for building the system and providing me permission to post this.
Version: Atavism 2.1.1 (will update if there are major changes, see Google Drive link at the top for most up-to-date version)
Note: This document is too long to fit into a blog entry so you'll only find part of it here and the rest on Google Drive.
This trace aims to document what happens from the moment a player clicks on a mob to the moment that mob’s hp is decremented. The journey through the code is a long one, but it is due to the level of sophistication of the intervening code that provides many contingencies for failure, result possibilities, and innumerable opportunities for customization.
Mouseover object click on ‘attackable’ object
NetworkAPI sendAttackMessage with object ID (Oid), attacktype (strike), attackstatus (true)
Message client “sent strike attack for” Oid (number displayed is not damage amount, it is object id
SendAttackMesssage (Oid, attackType, attackStatus)
AutoAttackMessage sent via Client.Instance.NetworkHelper.SendMessage
AutoAttackHook intercepts msg
Obtain attacker id and target id and attackstatus
Load CombatInfo for both
Lock player and target CombatInfo objects and only unlock when attack is over
if status is false or obj dead or no target or target dead, stopAutoAttack() and set COMBAT_TAG_OWNER to next aggressive mob, else obj.setAutoAttack(targetOid)
obj.setAutoAttack(targetOid), lock first and unlock when done
if newtarget is same as oldtarget, return (we are already attacking)
if not, we need to start a new attack, so setCombatState (true) and if not scheduled, schedule the auto attack with schedule(getAttackDelay() which checks attack_speed) as the update interval for CombatInfo object run() method
remove the old attacker from our list and if we have a null new target, we want to turn off combat by setCombatState(false), but if we do have a new attacker we want to addAttacker(target, getOwnerOid()) to the list
setCombatState sets COMBAT_PROP_COMBATSTATE, and broadcasts message that we are in combat and that weapons are unsheathed.
addAttacker (OID target, OID attacker)
lock combatplugin to do this
Add player’s OID as one of the attackers to this mob’s attacker hash map or create a new attacker map if it is null
make sure this target is tagOwner (current target?)
run() method for both attacker and target is scheduled according to their attack_speed (getAttackDelay())
Lock the CombatInfo object for attacker and target
Get the attack ability id from COMBAT_PROP_AUTOATTACK_ABILITY and continue if successful
if target is null or the entity it points to is null, stop scheduling
Otherwise CombatPlugin.resolveAutoAttack(this CombatInfo) for both attacker and target and schedule next run() method with getAttackDelay()
Get CombatInfo objects from attacker and target
if target is null or attack ability not valid, return
Some temp AI code here but the running code does the following: if the attacker isn’t the player (isUser(), COMBAT_PROP_USERFLAG false) then set the maxHealth, curHealth to health-max and health stats and healthPercent calculated as float, but these numbers aren’t used for anything locally currently.
Get AgisAbility object ability from abilityID
More temp AI code here: If the abilityID is no longer the autoattack (currently not possible), the previous AI code wants the mob to cast a spell, so a message is sent to the client that the mob begins casting the ability name.
Duel code: if both attacker and target are players get duelIDs for both and if they are in the same duel, then setDuelID on the autoattack ability (which does nothing as of now ?)
AgisAbility.startAbility(ability, info, target, null) finally executes the autoattack for attacker and target (as earlier, delay is based on attack_speed) --> See AbilityTrace
Abilities can range from auto attack to heals to sophisticated high level damage spells and may even be general enough to be completely unrelated to combat if creativity is used.
startAbility(AgisAbility ability, CombatInfo source, CombatInfo target, AgisItem item, Point loc)
For this ability, generateState(source, target, item, loc) which returns AgisAbilityState obj (see AgisAbilityState constructor for details--if the ability is TargetType.SELF it sets the target to the source=attacker and a message hook is setup in case the attacker is moving so the ability can be interrupted) and updateState() called
source (attacker) and target are already defined from the generateState earlier, so this first locks the attacker and the target and all potentialTargets which may change throughout the course of the ability
ActivationState state is INIT by default, enum defined in AgisAbility. Ability can be in INIT, ACTIVATING, CHANNELING, ACTIVATED, COMPLETED, CANCELLED, INTERRUPTED, or FAILED states
Depending on state, switch will perform the correct type of update
For all updates, a series of checks is performed on the ability to determine whether to go forward. if the checks succeed, AbilityResult.SUCCESS is returned and if they fail, a different AbilityResult is returned and AgisAbility.interruptAbility is called with the result, which resets the states (if fails during init runs init coordinated effects of ability? --> See CoordEffectTrace ) and sends a message.
checkAbility(source, target, state)
The checks for INIT are if the attacker is ready (not performing another action), if the target type makes sense depending on the ability target type, if the target species makes sense depending on the ability type, if the ability only works on specifically named targets and that target is selected (allows object interaction ?), if the player/target is alive, or if the target should be dead (resurrection), if the target is within range for starting the ability (too close or too far), and finally if the ability has any effect requirements that are active like just having dealt a crit, having last attack dodged, having used a certain ability within the past 5 seconds, etc (works by storing all effects player gets into a list and then listing what numeric effect values an ability requires to activate--can be >1effect required).
The checks for other states (including INIT) are if the attacker has/knows the ability, if the ability is a passive ability that shouldn’t be activated, if the attacker requires a tool or reagent in their inventory and if they have it, if the attacker has enough ‘costProp’ (the property required by the ability, usually mana) or vigor (which is like rage in that some abilities add vigor and some need vigor) to use the ability, if the target is within range for continuing the ability (too close or too far), and finally if they are in the correct combat stance (not fully coded).
For CombatMeleeAbility, which is the default player auto attack ability, it extends AgisAbility, so retains the same properties as AgisAbility and adds the following: checkAbility does the default checks and then sendAbilityFailMessage with result if failure. Otherwise, checkPosition (not coded yet), then checkEquip to see which weapon type is required and if it is equipped.
If ACTIVATING, reset params to default, call changeCoordinatedEffect(“success”) to reset the result parameters (which may become block/partial resist later).
The next piece of code is confusing in regards to player vs target, especially the in-line comments. Checks target’s defenses: sets defensiveType to ‘dodged’ (default) and weapType to target’s weaponType (possible error--shouldn’t this be the weapon of the attacker?). Checks if target has a parry effect for this weapon type and if so, does the target also have the skillType required. If so, set defensiveType to ‘parried’.
hitChance = CombatHelper.CalcPhysicalHitChance(attacker, target, skillType)
CalcPhysicalHitChance(attacker, target, skillType)
dexterity taken from CombatPlugin.PHYSICAL_ACCURACY_STAT, targetPerception = 20 (arbitrary, temp?), targetLevel taken, attacker skillLevel set to attacker level * 10 if attacker doesn’t have skill (arbitrary?), and to attacker’s skill if he has it.
hitChance = Math.atan((dexterity * skillLevel) - ((targetPerception * targetLevel)/1400) * 0.3) + 0.7
This calculation is arbitrary and there is a ‘New Formula’ above it commented out.
Next get a random value [0,1] and hitRoll = rand * 100
If the rand > 0.95, the ability has missed. Set attackerResult in params to missed = 3, changeCoordinatedEffect(“missed”), get casterResultEffect and targetResultEffect for the miss, which are effects that are later applied to both parties in the event of the miss.
If the rand > hitChance, the ability was dodged or parried if the target defensiveType was set to parried earlier. Set attackerResult in params to dodged = 5, changeCoordinatedEffect(“dodged”), get caster/target result effects, similar for parry.
There are some other commented-out possibilities including blockChance, critChance
Else, the ability has hit successfully and attackerResult = 1. Even if the ability didn’t hit, AbilityResult.SUCCESS is set to continue updating the ability state.
In updateState(), If AbilityResult.SUCCESS
If INIT, setCurrentAction for attacker to AgisAbilityState.INIT, then move to nextState(), run coordinated effects for this state using effect.invoke(getSourceOid(), getTargetOid(), location, this) -->See CoordEffectTrace and then another switch statement controls the new state
If ACTIVATING, which is next state, check the activation time and if there is an animation for it, run that (and also check the castingAffinity and send that to the attacker as well ?), then send the casting started message, set the duration of the activating state (getActivationTime()), and then schedule the run() function of AgisAbilityState to execute once that time has elapsed, which simply calls updateState() again to check the state and advance it. This then calls ability.completeActivation.
Gets attacker, target CombatInfo objs
Checks if reagents are required and if consumeReagents is active before removing reagents from inventory via AgisInventoryClient.removeGenericItem(OID, itemID, removeStack?, numToRemove)
Checks for a costProp (like mana) and subtracts from that property using CombatInfo.statModifyBaseValue(costProp, -activationCost), does similar with vigor stat except adds or removes (vigor is like building rage with abilities), then sendStatusUpdate() which is not coded.
Checks for effects that must be consumed by attacker and target (required effects or debuffs) and removes them with AgisEffect.removeEffect(EffectState existingState).
If attacker is player, send message to client that ability was used
Activate cooldowns in CooldownMap with Cooldown.activateCooldowns(cooldown collection, CombatInfo obj, quickness value = 100)
Smoo only? decrementWeaponUses message broadcasted ?
For CombatMeleeAbility, it retains the same properties from completeActivation and adds the following: also setCombatState(true) on the target, (if AREA_ENEMY =AoE ability gets a list of attackable targets in radius with getAoETargets(attacker’s CombatInfo)).
For every activation effect, put attackerResult, skillType, hitRoll into params. Get the target of this effect, and next condition checks all AoE targets and finally calls AgisEffect.applyEffect(activationEffect(i), attacker, target, params), (-->See EffectTrace) to all targets. If not AoE, checks if the target is attacker (self) and then applies the effect with the function above before clearing the params.
Last, the casterResultEffect and targetResultEffect are applied (-->See EffectTrace) in the same way.
Next it moves to nextState() which is CHANNELLING or ACTIVATED or (COMPLETED if not persistent) depending on ability and coordinated effects are run for that state -->See CoordEffectTrace.
If CHANNELLING, duration of the CHANNELLING state is set to the number of pulses * pulse time and it is scheduled to run again after one pulse time. ability.pulseChannelling(this) is called (which subtracts from costProp the cost per channel pulse) and nextPulse is incremented and scheduled to run after pulse time and updateState() returns so it can repeat until the number of pulses is up before moving to nextState() which is ACTIVATED or COMPLETED depending on ability persistence. For CombatMeleeAbility, AgisEffect.applyEffect(channelEffect, attacker, target) is called, -->See EffectTrace
If ACTIVATED, the current action is set to null since the attacker is no longer busy activating/channeling, the ability is added to active abilities via addActiveAbility(this) and update is scheduled for ActivePulseTime.
After return pulseActivated(this) is called (which subtracts from costProp the cost for an activePulse) and nextPulse incremented. Then the function schedules for ActivePulseTime and returns indefinitely without progressing to the next state (is this return intentional?). The only way for it to move to completed is for the state to be changed to COMPLETED elsewhere. For CombatMeleeAbility, AgisEffect.applyEffect(activeEffect, attacker, target) is called, -->See EffectTrace
If COMPLETED, setCurrentAction to null since we are no longer busy, terminate the casting animation and castingAffinity (?)
Finally unlockAll because the ability is finished
...Continued at AGIS Combat Traces (Google Drive)