@reldens/skills Execution Flow
Step by step walkthrough of how a @reldens/skills skill is validated and executed: phases, events, type-specific logic, critical hits, cooldowns, range, uses limit and common pitfalls.
Entry Points
- skill.validate() - checks if the skill can be used now and starts the cooldown. Call it before executing (or enable autoValidation so execute() calls it).
- await skill.execute(target) - runs the skill against the target (or the fixed target prop).
- await skill.executeOnHit(target) - physical skills only, called by your physics system when the projectile collides.
if(skill.validate()){
let result = await skill.execute(target);
}
Phase 1: Validation
validate() is synchronous and returns true or false:
- If the skill is not ready (isReady false because of missing required props), it returns false.
- Sets isValid = true and fires VALIDATE_BEFORE.
- If canActivate is false (cooldown) or owner.isCasting is true, sets lastState to CAN_NOT_ACTIVATE and returns false.
- Validates every ownerConditions entry against the owner; the first failure fires VALIDATE_FAIL with the failed condition and returns false.
- If usesLimit is greater than 0 and uses reached it, returns false.
- If skillDelay is greater than 0, sets canActivate = false and starts skillActivationTimer to restore it.
- Fires VALIDATE_SUCCESS (a synchronous listener can set isValid = false) and returns isValid.
Phase 2: Pre-execution
execute(target) starts with:
- Returns false if the skill is not ready.
- Fires SKILL_BEFORE_EXECUTE with (skill, target).
- Sets this.target when a target is passed; returns false if there is no target at all.
- Returns false when any of these fail:
- onExecuteConditions() - your custom validation hook.
- The automatic range validation - only when rangeAutomaticValidation, rangePropertyX and rangePropertyY are set (pass the target to execute() in this case, since the check uses the argument).
- validate() - only when autoValidation is true.
Phase 3: Owner Effects
- Applies the ownerEffects modifiers to the owner with applyModifiers(ownerEffects, owner, true); the last argument skips the critical bonus.
- Fires SKILL_APPLY_OWNER_EFFECTS with (skill, target).
Use owner effects for costs and self buffs, for example consuming mana.
Phase 4: Cast Time
When castTime is greater than 0:
- Fires SKILL_BEFORE_CAST with (skill, target).
- Sets owner.isCasting = true and starts owner.castingTimer.
- Returns false right away: the rest of execute() (uses counter, rewards, after execute event) continues without waiting.
- When the timer ends: runs the skill logic (phase 5), sets owner.isCasting = false and fires SKILL_AFTER_CAST with (skill, target, skillLogicResult).
While the owner is casting, validate() rejects any other skill of the same owner. Without cast time, the logic runs immediately.
Phase 5: Skill Logic
- Fires SKILL_BEFORE_RUN_LOGIC with (skill, target).
- Runs await this.runSkillLogic() - the type-specific behavior.
- Fires SKILL_AFTER_RUN_LOGIC with (skill, target).
Phase 6: Post-execution
- Increments uses.
- Runs await this.onExecuteRewards() - your custom rewards hook.
- Fires SKILL_AFTER_EXECUTE with (skill, target).
- Returns the skill logic result (false when a cast time is used).
Type-specific Logic
Base Skill
runSkillLogic() does nothing and returns true. Extend Skill and override it to implement custom behavior.
Attack
- Resets lastState. Without target or owner: TARGET_NOT_AVAILABLE, returns false.
- Checks the range with the current owner.getPosition() and target.getPosition() (the target or owner may have moved): OUT_OF_RANGE, returns false.
- Sets APPLYING_DAMAGE and runs applyDamageTo(target):
- Calculates the owner aim and the target dodge totals (returns false if a property cannot be read).
- Full dodge: with dodgeFullEnabled and dodge > aim * dodgeOverAimSuccess, sets DODGED and returns false.
- Without allowEffectBelowZero, returns false when the affected property is already 0 or lower.
- Damage: hitDamage directly, or adjusted by attack vs defense (and by dodge vs aim with damageAffected).
- Adds the critical bonus (reduced by dodge vs aim with criticalAffected).
- Writes the new value of the affected property (clamped to 0 unless allowEffectBelowZero), sets APPLIED_DAMAGE.
- Fires SKILL_ATTACK_APPLY_DAMAGE with (skill, target, damage, newValue) and returns true.
Effect
- Same target and range checks as Attack.
- Sets APPLYING_EFFECTS and applies targetEffects to the target with applyModifiers(targetEffects, target) (critical allowed).
- Sets APPLIED_EFFECTS, fires SKILL_EFFECT_TARGET_MODIFIERS with (skill) and returns true.
PhysicalAttack and PhysicalEffect
When executed:
- Validates the range with validateRange(target), which reads the positions through rangePropertyX / rangePropertyY (required for physical skills): OUT_OF_RANGE, returns false.
- Sets EXECUTE_PHYSICAL_ATTACK and calls await owner.executePhysicalSkill(target, skill) - your physics system creates the body using objectWidth, objectHeight and magnitude.
- Returns false: the result happens later, on collision.
On collision, your physics system calls await skill.executeOnHit(collidedObject):
- Fires SKILL_PHYSICAL_ATTACK_HIT or SKILL_PHYSICAL_EFFECT_HIT with (skill, collidedObject).
- With validateTargetOnHit, if the collided object is not skill.target, sets PHYSICAL_SKILL_INVALID_TARGET and returns false.
- Sets PHYSICAL_SKILL_RUN_LOGIC and runs the parent Attack or Effect logic, which works with skill.target.
class Archer
{
async executePhysicalSkill(target, skill)
{
let projectile = this.physics.createBody({
width: skill.objectWidth,
height: skill.objectHeight,
position: this.getPosition()
});
projectile.applyForce(skill.magnitude, this.directionTo(target));
projectile.onCollision(async (collidedObject) => {
await skill.executeOnHit(collidedObject);
});
}
}
Modifier Application
applyModifiers(modifiersObjectList, target, avoidCritical = false):
- Resets lastAppliedModifiers.
- For each modifier: sets modifier.target = target and calculates modifier.getModifiedValue() (current value plus the operation, with the modifier limits).
- Unless avoidCritical, adds getCriticalDiff(modifier.value): the critical bonus is calculated on the modifier value only, not on the resulting total, and it is added after the limits.
- Writes the value with modifier.setOwnerProperty() and stores it in lastAppliedModifiers[propertyKey].
Example: current value 80, modifier INC 10, critical multiplier 2. getModifiedValue() returns 90, getCriticalDiff(10) returns 10 (20 - 10), the final value is 100.
Modifier conditions are not evaluated here; use ownerConditions or onExecuteConditions() for that.
Critical Hit System
- criticalChance - percentage from 0 to 100 (default 0, disabled).
- criticalMultiplier - multiplier applied on a critical (default 1).
- criticalFixedValue - value added on a critical, after the multiplier (default 0).
isCritical()
{
if(this.criticalChance <= 0){
return false;
}
return sc.randomInteger(1, 100) <= this.criticalChance;
}
applyCriticalValue(normalValue)
{
if(!this.isCritical()){
return normalValue;
}
if(sc.isNumber(this.criticalMultiplier)){
normalValue = normalValue * this.criticalMultiplier;
}
if(sc.isNumber(this.criticalFixedValue)){
normalValue = normalValue + this.criticalFixedValue;
}
return normalValue;
}
getCriticalDiff(normalValue)
{
return this.applyCriticalValue(normalValue) - normalValue;
}
- Attack: damage = damage + criticalBonus, where the bonus is getCriticalDiff(damage), reduced by the dodge over aim proportion when criticalAffected is enabled and dodge is higher than aim.
- Effect: the bonus is getCriticalDiff(modifier.value), rolled for each modifier.
- Owner effects never get a critical bonus.
Cooldown System
if(0 < this.skillDelay){
this.canActivate = false;
this.skillActivationTimer = setTimeout(() => {
this.canActivate = true;
}, this.skillDelay);
}
- The cooldown starts in validate(), not in execute().
- skillDelay is in milliseconds; 0 (default) means no cooldown.
- While canActivate is false, validate() returns false with the CAN_NOT_ACTIVATE state.
- Each skill instance has its own timer.
Range Validation
- range - maximum distance; 0 (default) means infinite range.
- isInRange(ownerPosition, targetPosition) - uses an InteractionArea around the target: the owner x and y must both be within the target position plus or minus the range (a square area, not a circle). Fires the range events.
- Automatic validation in execute() - enabled with rangeAutomaticValidation and the position paths rangePropertyX / rangePropertyY (for example state/x and state/y); use rangeTargetPropertyX / rangeTargetPropertyY when the target stores its position under different paths.
- Attack and Effect logic always check the range again with getPosition(), so the target must implement it too.
let skill = new Attack({
key: 'arrow',
owner: player,
affectedProperty: 'stats/hp',
range: 250,
rangeAutomaticValidation: true,
rangePropertyX: 'state/x',
rangePropertyY: 'state/y'
});
Target Validation
- The target can be fixed (target prop) or passed to execute(target); without any target the execution returns false.
- allowSelfTarget is stored but not enforced by the base class; enforce it in your hook:
onExecuteConditions()
{
if(!this.allowSelfTarget && this.target === this.owner){
return false;
}
return true;
}
Uses Limit
- usesLimit - maximum number of executions (0, the default, means unlimited).
- uses - incremented on every execution that reaches phase 6.
- The limit is enforced by validate(), so it only applies when you validate before executing (or use autoValidation).
Skill States
skill.lastState holds one of the SkillConst.SKILL_STATES values after validation or execution:
- CAN_NOT_ACTIVATE - cooldown active or owner casting.
- TARGET_NOT_AVAILABLE - target or owner missing.
- OUT_OF_RANGE - target out of range.
- DODGED - attack fully dodged.
- APPLYING_DAMAGE, APPLIED_DAMAGE, APPLIED_CRITICAL_DAMAGE - attack progress.
- APPLYING_EFFECTS, APPLIED_EFFECTS - effect progress.
- EXECUTE_PHYSICAL_ATTACK, PHYSICAL_SKILL_RUN_LOGIC, PHYSICAL_SKILL_INVALID_TARGET - physical skills progress.
Debugging Skill Execution
- Check skill.isReady first: false means a required prop is missing (the reason is logged).
- Check skill.lastState after a failed validation or execution.
- Listen to the execution events to trace the flow:
let traceEvents = [
SkillsEvents.SKILL_BEFORE_EXECUTE,
SkillsEvents.SKILL_BEFORE_RUN_LOGIC,
SkillsEvents.SKILL_AFTER_RUN_LOGIC,
SkillsEvents.SKILL_AFTER_EXECUTE
];
for(let eventName of traceEvents){
skill.listenEvent(
eventName,
(skill) => console.log('[SKILL]', eventName, skill.key, skill.lastState),
skill.getOwnerUniqueEventKey('trace.'+eventName)
);
}
Common Pitfalls
1. Not awaiting execute()
// WRONG - the code continues before the skill finished
skill.execute(target);
// RIGHT
await skill.execute(target);
2. Executing without validating
execute() does not check the cooldown, the casting state, the owner conditions or the uses limit unless autoValidation is enabled. Call validate() first.
3. Forgetting to override runSkillLogic()
A class that extends Skill without overriding runSkillLogic() runs the events but does nothing.
4. Applying the critical to the wrong value
// WRONG - doubles the whole value (current + modifier)
newValue = modifier.getModifiedValue() * 2;
// RIGHT - only the modifier value gets the critical bonus
newValue = modifier.getModifiedValue() + this.getCriticalDiff(modifier.value);
5. Expecting automatic range validation without the range properties
rangeAutomaticValidation alone does nothing in execute(); it also needs rangePropertyX and rangePropertyY.
6. Physical skills without the owner method or range properties
Without owner.executePhysicalSkill() the skill is not ready and execute() returns false. Without rangePropertyX / rangePropertyY the launch fails with OUT_OF_RANGE.
7. Reading the result of a skill with cast time
With a cast time, execute() returns false before the logic runs. Listen to SKILL_AFTER_CAST, which receives the logic result.
Related Documentation
- @reldens/skills Architecture - Package overview and damage calculation.
- Skills Event System - Events catalog and listeners.
- Skills Class Hierarchy - Skill properties and methods.
- Skills Testing Guide - Testing skills.
- Battle System - How Reldens executes skills in combat.
- Skill Entity - Skills configuration.
- Target Options Entity - Skill target configuration.
reldens