• 🏆 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!

BasicAttack

Status
Not open for further replies.
Level 24
Joined
Aug 1, 2013
Messages
4,657
Hi all.

This is something that might be very interesting for all of you but I think it is quite a bit harder to make as I originally expected.

Goal:
Everything needs something to do right?
I want a system that simulates basic attacks. It is almost exactly the opposite of a DDS, this one will tell you when you want to apply the damage of a basic attack.

Why?
There are some features that you can have when using something like this.
First of all, you dont need the Spell Damage Reduction trick to find out if something is spell damage in a DDS because you know that all basic attack damage is done by this system.
Next to that, you can remove attack speed limitations... bonus attack speed of 2,000% proves to be very stupid cause the animations arent fast enough, and bonus attack speed may never be 0 or lower (you understand if you know the formula).
Also, attack range can simply be set using ability levels. (This does mean that there will be a lot of ability levels for this system.)
Last but not least, you can force attacks! I mean like all those people that want a proper taunt spell that you CANNOT stop by abusing the "stop" (anything else works as well ofc) order.

Code:
The code is quite simple:
It uses TimerUtils and Bribe's Unit Indexer (I modified it a bit so I can choose when it initializes for example... we will get to that later).

First of all, we have to store data for each unit (damage point, backswing point, etc)
So we need a trigger that runs when a unit is created:
JASS:
function AllocateUnitAttackData takes nothing returns boolean
    local integer id = GetUnitUserData(udg_UDexUnits[udg_UDex])
    local integer unitTypeId = GetUnitTypeId(udg_UDexUnits[id])
    
    set attackBackswingPoint[id] = GetUnitTypeBackswingPoint1(unitTypeId)
    set attackDamagePoint[id] = GetUnitTypeDamagePoint1(unitTypeId)
    set attackCooldown[id] = GetUnitTypeCooldown1(unitTypeId)
    set attackRange[id] = 100//GetUnitTypeRange1(unitTypeId)
    set attackSpeed[id] = 1.
    set isAttackForced[id] = false
    set attackSound[id] = BA_GetWeaponSound(GetUnitTypeWeaponSound1(unitTypeId))
    
    call UnitAddAbility(udg_UDexUnits[id], attackAbility)
    call SetUnitAbilityLevel(udg_UDexUnits[id], attackAbility, R2I((attackRange[id]-100.) / 5) + 1)
    return false
endfunction
We need the following information for each unit:
- Backswing Point
- Damage Point
- Cooldown
- Range
- Attack Speed (standard value)
- IsForced (will come later)
- WeaponSound

The unit also needs the ability that we use to attack units with.
And it will be set to the range of the unit.



When a unit is attacked, we re-order them to use the ability.
This will make all units unable to use their normal attack and use the ability instead with no delay.
JASS:
function ReorderAttack takes nothing returns nothing
    local integer id = GetTimerData(GetExpiredTimer())
    
    call IssueTargetOrderById(udg_UDexUnits[id], attackOrder, reorderTarget[id])
    call ReleaseTimer(reorderTimer[id])
    set reorderTarget[id] = null
    set reorderTimer[id] = null
endfunction
function AutoAttack takes nothing returns boolean
    local integer id = GetUnitUserData(GetAttacker())
    
    set reorderTarget[id] = GetTriggerUnit()
    set reorderTimer[id] = NewTimerEx(id)
    call TimerStart(reorderTimer[id], 0, false, function ReorderAttack)
    
    return false
endfunction
For some weird reason, I have to add a 0s timer because when you cast an ability with lower range than the current distance, it just stops the unit.

Anyway, we reorder the unit to cast the spel.



Now when the unit casts the spell, we have to attack in different phases.
1, We swing our weapon at the target.
2, We draw our weapon back from the target.
3, We wait until we can attack again.
(Between 1 and 2, we apply the damage.)

1: Ability Channel Event:
JASS:
function OnAttack takes nothing returns boolean
    local unit source
    local integer id
    local integer abilityId = GetSpellAbilityId()
    
    if abilityId == attackAbility or abilityId == attackForceAbility then
        set source = GetTriggerUnit()
        set id = GetUnitUserData(source)
        set newTarget[id] = GetSpellTargetUnit()
        
        if (not attackHasLanded[id]) and newTarget[id] != attackTarget[id] then
            if attackTimer[id] == null then
                set attackTimer[id] = NewTimerEx(id)
            endif
            
            set attackDamage[id] = BA_GetUnitAttackDamage(source)
            set attackTarget[id] = newTarget[id]
            set attackHasLanded[id] = false
            call SetUnitTimeScale(udg_UDexUnits[id], BA_GetUnitAttackSpeed(udg_UDexUnits[id]))
            call TimerStart(attackTimer[id], attackDamagePoint[id] / BA_GetUnitAttackSpeed(udg_UDexUnits[id]), false, function AttackOnDamagePoint)
        endif
        set source = null
        
    elseif abilityId == attackCooldownAbility then
        set newTarget[GetUnitUserData(GetTriggerUnit())] = GetSpellTargetUnit()
        
    endif
    
    return false
endfunction
If the casting ability is our attack ability, we assing the target and damage on the id of the source.
We also start a timer that runs for the Damage Point duration divided by the bonus attack speed and we set the animation speed of the source to the bonus attack speed. (You can see that this may not be 0.)

2: When that timer expires:
JASS:
function AttackOnDamagePoint takes nothing returns nothing
    local integer id = GetTimerData(GetExpiredTimer())
    
    if GetUnitCurrentOrder(udg_UDexUnits[id]) != attackOrder then
        call ReleaseTimer(attackTimer[id])
        set attackTimer[id] = null
        set attackTarget[id] = null
        return
    endif
    
    set attackHasLanded[id] = true
    call BA_OnBasicAttack(id)
    call TimerStart(attackTimer[id],  attackBackswingPoint[id] / BA_GetUnitAttackSpeed(udg_UDexUnits[id]), false, function AttackOnCooldown)
endfunction
If the order of the unit has changed in the meantime, this timer is no longer accurate any more and has to be removed.
If he unit has ordered the same ability, we have handled that in phase 1.

Here we call a configurable function called BA_OnBasicAttack() which will apply the damage for us.
We also start the timer again for the Backswing Point.

3: When that timer expires:
JASS:
function AttackOnCooldown takes nothing returns nothing
    local integer id = GetTimerData(GetExpiredTimer())
    local boolean doesAttack = GetUnitCurrentOrder(udg_UDexUnits[id]) == attackOrder
    
    call SetUnitTimeScale(udg_UDexUnits[id], 1)
    
    if GetUnitAbilityLevel(udg_UDexUnits[id], attackForceAbility) > 0 then
        call UnitRemoveAbility(udg_UDexUnits[id], attackForceAbility)
        call UnitAddAbility(udg_UDexUnits[id], attackForceCooldownAbility)
        call SetUnitAbilityLevel(udg_UDexUnits[id], attackForceCooldownAbility, R2I((attackRange[id]-100.) / 5) + 1)
    else
        call UnitRemoveAbility(udg_UDexUnits[id], attackAbility)
        call UnitAddAbility(udg_UDexUnits[id], attackCooldownAbility)
        call SetUnitAbilityLevel(udg_UDexUnits[id], attackCooldownAbility, R2I((attackRange[id]-100.) / 5) + 1)
    endif
    
    if doesAttack then
        call IssueTargetOrderById(udg_UDexUnits[id], attackOrder, newTarget[id])
    endif
    
    call TimerStart(attackTimer[id], (attackCooldown[id] - attackDamagePoint[id] - attackBackswingPoint[id]) / BA_GetUnitAttackSpeed(udg_UDexUnits[id]), false, function AttackOnCooldownReset)
endfunction
Here we replace the attack ability with the cooldown ability (stand ready).
The attack actions will now cast this ability instead and if the unit was busy casting the attack ability, he will now cast the attack cooldown ability.
Ofcourse the range has to fit.

Now the timer will run for the remaining time for the cooldown.

Cooldown reset:
JASS:
function AttackOnCooldownReset takes nothing returns nothing
    local integer id = GetTimerData(GetExpiredTimer())
    local boolean doesAttack = GetUnitCurrentOrder(udg_UDexUnits[id]) == attackOrder
    
    if GetUnitAbilityLevel(udg_UDexUnits[id], attackForceCooldownAbility) > 0 then
        call UnitRemoveAbility(udg_UDexUnits[id], attackForceCooldownAbility)
        call UnitAddAbility(udg_UDexUnits[id], attackForceAbility)
        call SetUnitAbilityLevel(udg_UDexUnits[id], attackForceAbility, R2I((attackRange[id]-100.) / 5) + 1)
    else
        call UnitRemoveAbility(udg_UDexUnits[id], attackCooldownAbility)
        call UnitAddAbility(udg_UDexUnits[id], attackAbility)
        call SetUnitAbilityLevel(udg_UDexUnits[id], attackAbility, R2I((attackRange[id]-100.) / 5) + 1)
    endif
    
    call ReleaseTimer(attackTimer[id])
    set attackTimer[id] = null
    set attackTarget[id] = null
    set attackHasLanded[id] = false
    set attackDamage[id] = 0
    
    if doesAttack then
        call IssueTargetOrderById(udg_UDexUnits[id], attackOrder, newTarget[id])
    endif
endfunction
Same as before, but now the other way around.



Other stuff:

With this, you can ofcourse have functions like SetUnitAttackRange() and SetUnitAttackSpeed() which have both been added to the system.

There are 2 drawbacks from this system.
You cannot use Object Editor damage and attack speed any more.
But we never use that anyway right?
To replace these, we have to use:
JASS:
function BA_GetUnitAttackDamage takes unit whichUnit returns real
    return 10.
endfunction
function BA_GetUnitAttackSpeed takes unit whichUnit returns real
    return attackSpeed[GetUnitUserData(whichUnit)]
endfunction
We both load those values from out stat system... and if you dont use one yet, then get started on REAL maps.

This one is slightly different:
JASS:
function BA_OnBasicAttack takes integer id returns nothing
    if IsUnitType(udg_UDexUnits[id], UNIT_TYPE_RANGED_ATTACKER) then
        //Create missile
    else
        call UnitDamageTarget(udg_UDexUnits[id], attackTarget[id], attackDamage[id], true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, weaponSound[attackSound[id]])        
    endif
endfunction
As you could see, the system would only work for melee units... wait, we always wanted that!
You as map maker now can change attack type and damage type of your basic attacks.
I have no need for that so I didnt bother to add Attack Type and Damage Type to the system.

Ranged units should have their basic attack missile be created here.
You can use a system that uses Acid Bomb as a missile if you really want to stick to WC3 standards.

Why this thread?
There is something that this system does not correctly...
Attack-Once
Attack-Move

I have added a new version: Attack-Force which is basically an attack you cannot stop.

I have to know how I can add Attack-Once and Attack-Move in this system without making it a pain or inefficient.
Ofcourse credits will be given and rep as well... and you will be a hero in my AoS :D

I thought of something like adding functions like:
function OrderAttack takes unit whichUnit, unit target returns boolean
function OrderAttackForce takes unit whichUnit, unit target returns boolean
function OrderAttackOnce takes unit whichUnit, unit target returns boolean
function OrderAttackMove takes unit whichUnit, real targetX, real targetY returns boolean

But how those would be implemented is something I dont really see.

Ofcourse any help and comments are appreciated.

Testmap:
I have added a tesmap with the system and an example to show its possibilities so far.
Type /setrange <int> to set the range to that value
Type /setspeed <int> to set the bonus attack speed to that value (remember that this is a factor of the base attack speed... it will be switched to real soon)
Type /taunt to make the attacks forced and unforced based on what it was. (try stopping the paladins attack)

(You have to have the Object Data Extractor and LUA channel ability hotfix installed (above JNGP 1.5d at least) to make you be able to modify the map.)
 

Attachments

  • BasicAttack 0.3.w3x
    58.6 KB · Views: 82
Level 24
Joined
Aug 1, 2013
Messages
4,657
Ok, after talking with WK in chat for a while, I came to the conclusion that rewriting the system would only be able to turn out good...

What is different:
Attack orders are caught and handled by the system to handle all different types of attacks:
- attacktarget
- attackmove
- attackonce
- attackground

Attack orders are stored on the unit.

Forced attacks are temporarily disabled.

Global functions (library functions) are temporarily disabled.

What are the current problems:
The unit is attacked event is one of the initiates of the BasicAttack spell. However, if the unit is not in range of the target, he will not cast the spell.
Normally, he should run towards the target until he is in range, but he doesnt do that so he comes in an infinite loop (non-crashing) of: trying to attack -> ordered to cast the spell -> stopped casting (bug?) -> acquired a target -> trying to attack -> etc
EDIT:
Solution:
In the BA_Attacked function, there is a second IssueOrder function which is a delayed one.
When you outcomment that one and comment the normal IssueOrder function, this problem is solved.

A unit who attacks a target by the attack move is ordered to attackmove to the target again when it has finished the basic attack.
However, it does not acquire the target so it does not cast the cooldown ability which results in not showing the stand ready animation.
EDIT:
Solution:
The solution to the other problem also works kind of for this one.
However, there is a small moment that the unit is showing his stand animation instead of his stand ready animation just before he cast those spells.
This is a really weird result.

Code:
I have been so kind to post the complete code this time :D
JASS:
//! LoadUnitData fields=ubs1,udp1,ua1r,ua1c,ucs1
function BA_GetUnitAttackSpeed takes unit whichUnit returns real
    return unit_AttackSpeed[GetUnitUserData(whichUnit)]
endfunction
function BA_GetUnitAttackDamage takes unit whichUnit returns real
    return 10.
endfunction
function BA_ApplyTargetDamage takes integer id returns nothing
    call UnitDamageTarget(udg_UDexUnits[id], attack_Target_Widget[id], attack_Damage[id], true, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, attack_WeaponSound[unit_WeaponSound[id]])
endfunction
function BA_ApplyPointDamage takes integer id returns nothing
    
endfunction

globals
    
    constant integer    Order_Attack                       = 851983 //OrderId("attack")
    constant integer    Order_AttackOnce                   = 851985 //OrderId("attackonce")
    constant integer    Order_AttackGround                 = 851984 //OrderId("attackground")
    constant integer    Order_Stop                         = 851972 //OrderId("stop")
    
    constant integer    attackAbilityAttack                = 'ABAA'
    constant integer    attackAbilityAttackCooldown        = 'ABAC'
    constant integer    attackAbilityForcedAttack          = 'ABAF'
    constant integer    attackAbilityForcedAttackCooldown  = 'ABAG'
    constant integer    attackAbilityOrder                 = 852529 //OrderId("absorb")
    constant integer    attackOrderType_Attack             = 1
    constant integer    attackOrderType_AttackMove         = 2
    constant integer    attackOrderType_AttackOnce         = 3
    constant integer    attackOrderType_AttackGround       = 4
    
    weapontype array    attack_WeaponSound
    
    integer array       unit_WeaponSound
    real array          unit_AttackSpeed
    real array          unit_BackswingPoint
    real array          unit_DamagePoint
    real array          unit_Cooldown
    real array          unit_Range
    
    //unit array          udg_UDexUnits
    integer array       attack_Type
    real array          attack_Damage
    boolean array       attack_HasLanded
    destructable array  attack_Target_Destructable
    item array          attack_Target_Item
    unit array          attack_Target_Unit
    widget array        attack_Target_Widget
    real array          attack_Target_X
    real array          attack_Target_Y
    timer array         attack_Timer
    
    integer array       attack_NewType
    widget array        attack_NewTarget_Widget
    real array          attack_NewTarget_X
    real array          attack_NewTarget_Y
    
endglobals

library BasicAttack2 uses DelayedOrder
    
    function SetUnitAttackRange takes unit whichUnit, real newRange returns nothing
    endfunction
    
    function SetUnitAttackSpeed takes unit whichUnit, real newAttackSpeed returns nothing
    endfunction
    
    function SetUnitForcedAttacks takes unit whichUnit, boolean forceAttacks returns nothing
    endfunction
    
    function IsUnitForcedAttacks takes unit whichUnit returns boolean
        return false
    endfunction
    //<to-do>
    
endlibrary

function BA_OnCooldownReset takes nothing returns nothing
    local integer id = GetTimerData(GetExpiredTimer())
    local boolean doesAttack = GetUnitCurrentOrder(udg_UDexUnits[id]) == attackAbilityOrder
    local integer newOrder = 0
    
    if doesAttack then
        if attack_NewType[id] == attackOrderType_Attack then
            set newOrder = 1
        elseif attack_NewType[id] == attackOrderType_AttackGround then
            set newOrder = 4
        endif
    endif
    
    if GetUnitAbilityLevel(udg_UDexUnits[id], attackAbilityForcedAttackCooldown) > 0 then
        call UnitAddAbility(udg_UDexUnits[id], attackAbilityForcedAttack)
        call UnitMakeAbilityPermanent(udg_UDexUnits[id], true, attackAbilityForcedAttack)
        call SetUnitAbilityLevel(udg_UDexUnits[id], attackAbilityForcedAttack, R2I((unit_Range[id]-100.) / 5) + 1)
        call UnitRemoveAbility(udg_UDexUnits[id], attackAbilityForcedAttackCooldown)
    else
        call UnitAddAbility(udg_UDexUnits[id], attackAbilityAttack)
        call UnitMakeAbilityPermanent(udg_UDexUnits[id], true, attackAbilityAttack)
        call SetUnitAbilityLevel(udg_UDexUnits[id], attackAbilityAttack, R2I((unit_Range[id]-100.) / 5) + 1)
        call UnitRemoveAbility(udg_UDexUnits[id], attackAbilityAttackCooldown)
    endif
    
    call ReleaseTimer(attack_Timer[id])
    set attack_Type[id] = 0
    set attack_Damage[id] = 0.
    set attack_HasLanded[id] = false
    set attack_Target_Destructable[id] = null
    set attack_Target_Item[id] = null
    set attack_Target_Unit[id] = null
    set attack_Target_Widget[id] = null
    set attack_Target_X[id] = 0.
    set attack_Target_Y[id] = 0.
    set attack_Timer[id] = null
    
    if newOrder == 1 then
        call IssueTargetOrderById(udg_UDexUnits[id], Order_Attack, attack_NewTarget_Widget[id])
    elseif newOrder == 4 then
        call IssuePointOrderById(udg_UDexUnits[id], Order_AttackGround, attack_NewTarget_X[id], attack_NewTarget_Y[id])
    endif
endfunction
function BA_OnCooldown takes nothing returns nothing
    local integer id = GetTimerData(GetExpiredTimer())
    local boolean doesAttack = GetUnitCurrentOrder(udg_UDexUnits[id]) == attackAbilityOrder
    
    call SetUnitTimeScale(udg_UDexUnits[id], 1)
    
    if GetUnitAbilityLevel(udg_UDexUnits[id], attackAbilityForcedAttack) > 0 then
        call UnitAddAbility(udg_UDexUnits[id], attackAbilityForcedAttackCooldown)
        call UnitMakeAbilityPermanent(udg_UDexUnits[id], true, attackAbilityForcedAttackCooldown)
        call SetUnitAbilityLevel(udg_UDexUnits[id], attackAbilityForcedAttackCooldown, R2I((unit_Range[id]-100.) / 5) + 1)
        call UnitRemoveAbility(udg_UDexUnits[id], attackAbilityForcedAttack)
    else
        call UnitAddAbility(udg_UDexUnits[id], attackAbilityAttackCooldown)
        call UnitMakeAbilityPermanent(udg_UDexUnits[id], true, attackAbilityAttackCooldown)
        call SetUnitAbilityLevel(udg_UDexUnits[id], attackAbilityAttackCooldown, R2I((unit_Range[id]-100.) / 5) + 1)
        call UnitRemoveAbility(udg_UDexUnits[id], attackAbilityAttack)
    endif
    
    if doesAttack then
        if attack_NewType[id] == attackOrderType_Attack then
            call IssueTargetOrderById(udg_UDexUnits[id], attackAbilityOrder, attack_NewTarget_Widget[id])
        elseif attack_NewType[id] == attackOrderType_AttackMove then
            call BJDebugMsg("works")
            call IssuePointOrderById(udg_UDexUnits[id], Order_Attack, attack_NewTarget_X[id], attack_NewTarget_Y[id])
        elseif attack_NewType[id] == attackOrderType_AttackOnce then
            call IssueImmediateOrderById(udg_UDexUnits[id], Order_Stop)
        elseif attack_NewType[id] == attackOrderType_AttackGround then
            call IssuePointOrderById(udg_UDexUnits[id], Order_AttackGround, attack_NewTarget_X[id], attack_NewTarget_Y[id])
        endif
    endif
    
    call TimerStart(attack_Timer[id], (unit_Cooldown[id] - unit_DamagePoint[id] - unit_BackswingPoint[id]) / BA_GetUnitAttackSpeed(udg_UDexUnits[id]), false, function BA_OnCooldownReset)
endfunction
function BA_OnDamagePoint takes nothing returns nothing
    local integer id = GetTimerData(GetExpiredTimer())
    
    if GetUnitCurrentOrder(udg_UDexUnits[id]) != attackAbilityOrder then
        call ReleaseTimer(attack_Timer[id])
        set attack_Type[id] = 0
        set attack_Damage[id] = 0.
        set attack_HasLanded[id] = false
        set attack_Target_Destructable[id] = null
        set attack_Target_Item[id] = null
        set attack_Target_Unit[id] = null
        set attack_Target_Widget[id] = null
        set attack_Target_X[id] = 0.
        set attack_Target_Y[id] = 0.
        set attack_Timer[id] = null
        return
    endif
    
    set attack_HasLanded[id] = true
    if attack_Target_Widget[id] != null then
        call BA_ApplyTargetDamage(id)
    else
        call BA_ApplyPointDamage(id)
    endif
    call TimerStart(attack_Timer[id], unit_BackswingPoint[id] / BA_GetUnitAttackSpeed(udg_UDexUnits[id]), false, function BA_OnCooldown)
endfunction
function BA_Channeling takes nothing returns boolean
    local integer abilityId = GetSpellAbilityId()
    local unit source
    local integer id
    
    if abilityId == attackAbilityAttack or abilityId == attackAbilityForcedAttack then
        call BJDebugMsg("Attack cast")
        set source = GetTriggerUnit()
        set id = GetUnitUserData(source)
        
        if attack_Timer[id] == null then
            set attack_Timer[id] = NewTimerEx(id)
        endif
        
        set attack_Type[id] = attack_NewType[id]
        set attack_HasLanded[id] = false
        set attack_Damage[id] = BA_GetUnitAttackDamage(source)
        set attack_Target_Destructable[id] = GetSpellTargetDestructable()
        set attack_Target_Item[id] = GetSpellTargetItem()
        set attack_Target_Unit[id] = GetSpellTargetUnit()
        if attack_Target_Destructable[id] != null then
            set attack_Target_Widget[id] = attack_Target_Destructable[id]
        elseif attack_Target_Item[id] != null then
            set attack_Target_Widget[id] = attack_Target_Item[id]
        elseif attack_Target_Unit[id] != null then
            set attack_Target_Widget[id] = attack_Target_Unit[id]
        endif
        set attack_Target_X[id] = GetSpellTargetX()
        set attack_Target_Y[id] = GetSpellTargetY()
        
        call SetUnitTimeScale(udg_UDexUnits[id], BA_GetUnitAttackSpeed(source))
        call TimerStart(attack_Timer[id], unit_DamagePoint[id] / BA_GetUnitAttackSpeed(source), false, function BA_OnDamagePoint)
    elseif abilityId == attackAbilityAttackCooldown or abilityId == attackAbilityForcedAttackCooldown then
        call BJDebugMsg("Attack Cooldown cast")
    endif
    
    return false
endfunction

function BA_Attacked takes nothing returns boolean
    local unit source = GetAttacker()
    local integer id = GetUnitUserData(source)
    local unit target = GetTriggerUnit()
    
    set attack_NewType[id] = attackOrderType_AttackMove
    set attack_NewTarget_Widget[id] = target
    call BJDebugMsg("Unit is attacked")
    call IssueTargetOrderById(source, attackAbilityOrder, target)
    //call IssueTargetOrderByIdDelayed(source, attackAbilityOrder, target, 0)
    
    set source = null
    set target = null
    return false
endfunction

function BA_IssuedOrder takes nothing returns boolean
    local integer order = GetIssuedOrderId()
    local unit source = GetTriggerUnit()
    local integer id = GetUnitUserData(source)
    
    call BJDebugMsg("unit is ordered to " + OrderId2String(order))
    if order == Order_Attack then
        set attack_NewTarget_Widget[id] = GetOrderTarget()
        if attack_NewTarget_Widget[id] == null then
            //Attack is attack-move
            set attack_NewType[id] = attackOrderType_AttackMove
            set attack_Target_X[id] = GetOrderPointX()
            set attack_Target_Y[id] = GetOrderPointY()
            call BJDebugMsg("attackmove ordered")
            
        else
            //Attack is on a unit.
            set attack_NewType[id] = attackOrderType_Attack
            if (not attack_HasLanded[id]) or attack_Target_Widget[id] != attack_NewTarget_Widget[id] then
                call IssueTargetOrderByIdDelayed(source, attackAbilityOrder, attack_NewTarget_Widget[id], 0)
            endif
            call BJDebugMsg("attack ordered")
        endif
        
    elseif order == Order_AttackOnce then
        //Attack is attack-once
        set attack_NewTarget_Widget[id] = GetOrderTarget()
        set attack_NewType[id] = attackOrderType_AttackOnce
        if (not attack_HasLanded[id]) or attack_Target_Widget[id] != attack_NewTarget_Widget[id] then
            set attack_NewTarget_Widget[id] = attack_NewTarget_Widget[id]
            call IssueTargetOrderByIdDelayed(source, attackAbilityOrder, attack_NewTarget_Widget[id], 0)
        endif
        call BJDebugMsg("attackonce ordered")
        
    elseif order == Order_AttackGround then
        //Attack is attack-ground
        set attack_NewType[id] = attackOrderType_AttackGround
        set attack_NewTarget_X[id] = GetOrderPointX()
        set attack_NewTarget_Y[id] = GetOrderPointY()
        if not attack_HasLanded[id] then
            call IssuePointOrderByIdDelayed(source, attackAbilityOrder, attack_NewTarget_X[id], attack_NewTarget_Y[id], 0)
        endif
        call BJDebugMsg("attackground ordered")
        
    elseif order != attackAbilityOrder then
        //Attack is something else
        set attack_NewType[id] = 0
        
    endif
    set source = null
    return false
endfunction

function BA_GetWeaponSound takes string weaponSound returns integer
    if weaponSound == "NONE" then
        return 0
    elseif weaponSound == "MetalLightChop" then
        return 1
    elseif weaponSound == "MetalMediumChop" then
        return 2
    elseif weaponSound == "MetalHeavyChop" then
        return 3
    elseif weaponSound == "MetalLightSlice" then
        return 4
    elseif weaponSound == "MetalMediumSlice" then
        return 5
    elseif weaponSound == "MetalHeavySlice" then
        return 6
    elseif weaponSound == "MetalMediumBash" then
        return 7
    elseif weaponSound == "MetalHeavyBash" then
        return 8
    elseif weaponSound == "MetalMediumStab" then
        return 9
    elseif weaponSound == "MetalHeavyStab" then
        return 10
    elseif weaponSound == "WoodLightSlice" then
        return 11
    elseif weaponSound == "WoodMediumSlice" then
        return 12
    elseif weaponSound == "WoodHeavySlice" then
        return 13
    elseif weaponSound == "WoodLightBash" then
        return 14
    elseif weaponSound == "WoodMediumBash" then
        return 15
    elseif weaponSound == "WoodHeavyBash" then
        return 16
    elseif weaponSound == "WoodLightStab" then
        return 17
    elseif weaponSound == "WoodMediumStab" then
        return 18
    elseif weaponSound == "ClawLightSlice" then
        return 19
    elseif weaponSound == "ClawMediumSlice" then
        return 20
    elseif weaponSound == "ClawHeavySlice" then
        return 21
    elseif weaponSound == "AxeMediumChop" then
        return 22
    elseif weaponSound == "RockHeavyBash" then
        return 23
    endif
        return 0
endfunction
function BA_AllocateUnitData takes nothing returns boolean
    local integer unitTypeId = GetUnitTypeId(udg_UDexUnits[udg_UDex])
    
    set unit_BackswingPoint[udg_UDex] = GetUnitTypeBackswingPoint1(unitTypeId)
    set unit_DamagePoint[udg_UDex] = GetUnitTypeDamagePoint1(unitTypeId)
    set unit_Cooldown[udg_UDex] = GetUnitTypeCooldown1(unitTypeId)
    set unit_Range[udg_UDex] = 1600//GetUnitTypeRange1(unitTypeId)
    set unit_AttackSpeed[udg_UDex] = 1.
    //set isAttackForced[udg_UDex] = false
    set unit_WeaponSound[udg_UDex] = BA_GetWeaponSound(GetUnitTypeWeaponSound1(unitTypeId))
    
    call UnitAddAbility(udg_UDexUnits[udg_UDex], attackAbilityAttack)
    call UnitMakeAbilityPermanent(udg_UDexUnits[udg_UDex], true, attackAbilityAttack)
    call SetUnitAbilityLevel(udg_UDexUnits[udg_UDex], attackAbilityAttack, R2I((unit_Range[udg_UDex]-100.) / 5) + 1)
    return false
endfunction

//===========================================================================
function InitTrig_BasicAttack_2 takes nothing returns nothing
    local trigger t
    local unit u
    
    set u = CreateUnit(Player(15), 'hfoo', 0, 0, 0)
    call ShowUnit(u, false)
    call UnitAddAbility(u, attackAbilityAttack)
    call UnitAddAbility(u, attackAbilityAttackCooldown)
    //call UnitAddAbility(u, attackAbilityForcedAttack)
    //call UnitAddAbility(u, attackAbilityForcedAttackCooldown)
    call SetUnitAbilityLevel(u, attackAbilityAttack, 301)
    call SetUnitAbilityLevel(u, attackAbilityAttackCooldown, 301)
    //call SetUnitAbilityLevel(u, attackAbilityForcedAttack, 301)
    //call SetUnitAbilityLevel(u, attackAbilityForcedAttackCooldown, 301)
    call RemoveUnit(u)
    set u = null
    
    set t = CreateTrigger()
    call TriggerRegisterVariableEvent(t, "udg_UnitIndexEvent", EQUAL, 1)
    call TriggerAddCondition(t, Filter(function BA_AllocateUnitData))
    
    set t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_ORDER)
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER)
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ISSUED_UNIT_ORDER)
    call TriggerAddCondition(t, Filter(function BA_IssuedOrder))
    
    set t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_ATTACKED)
    call TriggerAddCondition(t, Filter(function BA_Attacked))
    
    set t = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_CHANNEL)
    call TriggerAddCondition(t, Filter(function BA_Channeling))
    
    set attack_WeaponSound[0] = WEAPON_TYPE_WHOKNOWS
    set attack_WeaponSound[1] = WEAPON_TYPE_METAL_LIGHT_CHOP
    set attack_WeaponSound[2] = WEAPON_TYPE_METAL_MEDIUM_CHOP
    set attack_WeaponSound[3] = WEAPON_TYPE_METAL_HEAVY_CHOP
    set attack_WeaponSound[4] = WEAPON_TYPE_METAL_LIGHT_SLICE
    set attack_WeaponSound[5] = WEAPON_TYPE_METAL_MEDIUM_SLICE
    set attack_WeaponSound[6] = WEAPON_TYPE_METAL_HEAVY_SLICE
    set attack_WeaponSound[7] = WEAPON_TYPE_METAL_MEDIUM_BASH
    set attack_WeaponSound[8] = WEAPON_TYPE_METAL_HEAVY_BASH
    set attack_WeaponSound[9] = WEAPON_TYPE_METAL_MEDIUM_STAB
    set attack_WeaponSound[10] = WEAPON_TYPE_METAL_HEAVY_STAB
    set attack_WeaponSound[11] = WEAPON_TYPE_WOOD_LIGHT_SLICE
    set attack_WeaponSound[12] = WEAPON_TYPE_WOOD_MEDIUM_SLICE
    set attack_WeaponSound[13] = WEAPON_TYPE_WOOD_HEAVY_SLICE
    set attack_WeaponSound[14] = WEAPON_TYPE_WOOD_LIGHT_BASH
    set attack_WeaponSound[15] = WEAPON_TYPE_WOOD_MEDIUM_BASH
    set attack_WeaponSound[16] = WEAPON_TYPE_WOOD_HEAVY_BASH
    set attack_WeaponSound[17] = WEAPON_TYPE_WOOD_LIGHT_STAB
    set attack_WeaponSound[18] = WEAPON_TYPE_WOOD_MEDIUM_STAB
    set attack_WeaponSound[19] = WEAPON_TYPE_CLAW_LIGHT_SLICE
    set attack_WeaponSound[20] = WEAPON_TYPE_CLAW_MEDIUM_SLICE
    set attack_WeaponSound[21] = WEAPON_TYPE_CLAW_HEAVY_SLICE
    set attack_WeaponSound[22] = WEAPON_TYPE_AXE_MEDIUM_CHOP
    set attack_WeaponSound[23] = WEAPON_TYPE_ROCK_HEAVY_BASH
    
    set t = null
endfunction

Test map:
Again same installation rules.
Just download this map and try it out.
(Be aware that the chat messages dont work any more.)
 

Attachments

  • BasicAttack 0.4.w3x
    65.1 KB · Views: 85
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Wietlol, I am so glad I didn't get started on a project for the idea I had yesterday. I haven't opened your map, but it's safe to assume you're using cargo hold and a spell ability to represent the attack icon in its place. The ability plays "attack" instead of "spell" for the animation. Why I didn't see a future with it was because I didn't think as brilliantly as you by adding the ability with the attack event and removing it afterward.

Now, single-target melee units are able to use this, though as you mentioned, ranged units need a projectile system. I think you can use the AddSpellEffectTargetById and EFFECT_TYPE_MISSILE for simplifying getting the missile art. But you literally have to index all damage, range, AoE, damage-modifying items, attack speed and attack speed enhancing items/buffs/effects - of all the units and items used in a map, and that can be an enormous project.

Granted, the payoff means that you now have every attack under full control - and that is an impressive achievement any way you look at it.

I really would like to see what Nestharus has for input, here, since he's devoted so much time to attack indexing.

Oh, and you don't need to have a billion levels for range. Just cancel and refresh the cooldown of the ability if it's outside of the unit's assigned range, and give the ability a very long range so it works with all units.
 
This looks intriguing...
But we should remember what an enormous amount of overhead such a thing would add to any map that uses fully triggered attacks and missiles for everything.

We should ask ourselves the question of if there is any real practical use-case for this in popular map genres...

These are:
- tower defenses: no option at all. There are just way too many units to keep the game performance up with fully scripted attacks
- AOS style maps: probably fine performance-wise and due to skillshots we use many triggered missiles anyway. However, most AoS maps wouldn't really benefit from all the cool stuff this adds to the table, as (due to balancing reasons) many variables such as attack damage, etc. are fixed values anyway and upgrades can be used in many designs here
- Survival maps: probably performance issues with a large number of attackers, especially with many ranged units
- RPGs: could work and I see the benefits here (proc effects, armor types, triggering damage, spell reflect abilities, etc.) ... however, there are only a handful (if not less) ongoing RPG projects out there where the maker actually has the skills to use such a system. In fact, I only know 3 popular RPGs that even use JASS to begin with ... and those are so deep down in developement that it is next to impossible for the maker to make a switch


Mind you, I am not saying you should give this up. It's a great idea and I'd love to see where it goes, but keep in mind that you will probably write this for your personal use alone.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Wietlol, I am so glad I didn't get started on a project for the idea I had yesterday.
Good for you... I guess.

I haven't opened your map, but it's safe to assume you're using cargo hold and a spell ability to represent the attack icon in its place.
Uhm... no I don't.
I use the normal basic attack icon, not an icon of an ability.

The ability plays "attack" instead of "spell" for the animation. Why I didn't see a future with it was because I didn't think as brilliantly as you by adding the ability with the attack event and removing it afterward.
Well, at first I displayed the animation through triggers.
This will also be an option afterwards when the basics are finished because you know, Villager255 for example.

Now, single-target melee units are able to use this, though as you mentioned, ranged units need a projectile system. I think you can use the AddSpellEffectTargetById and EFFECT_TYPE_MISSILE for simplifying getting the missile art. But you literally have to index all damage, range, AoE, damage-modifying items, attack speed and attack speed enhancing items/buffs/effects - of all the units and items used in a map, and that can be an enormous project.
Not really.
I made this system for heavely editted maps only.
For map makers who trigger the entire thing.
For people who want things that are regulary not possible.
For me :D

I dont index damage, range, AoE, damage-modifying items, attack speed and attack speed items. At least not by this system.
I use a stat system where I can have everything stored and used and let this system load those values directly.
JASS:
function BA_GetUnitAttackDamage takes unit whichUnit returns real
    return Table_Unit_Current_Physical_Damage[GetUnitUserData(whichUnit)]
endfunction
function BA_GetUnitAttackSpeed takes unit whichUnit returns real
    return Table_Unit_Current_Attack_Speed[GetUnitUserData(whichUnit)]
endfunction

Everything else like AoE, etc are handled on the onDamage event created by my Damage System. (With the implementation of this system, I got completely rid of the damage detection part.)

Granted, the payoff means that you now have every attack under full control - and that is an impressive achievement any way you look at it.
It is not yet under complete control but yea that is the goal.
I tested it on my map but my units sometimes just stop. ("Minions" are attackmoved to the other side of the map but after they defeated the enemy wave, they simply stop moving.)

I really would like to see what Nestharus has for input, here, since he's devoted so much time to attack indexing.
I actually never heard of that thing before I made this thread... however he is quite silent in chat.

Oh, and you don't need to have a billion levels for range. Just cancel and refresh the cooldown of the ability if it's outside of the unit's assigned range, and give the ability a very long range so it works with all units.
The cooldown will never start... unless you have an attack speed that would take you 180 seconds for the attack to land.
And even then, in the current state, I order the units to cast the spell.
However when they are out of range of the target, they move towards the target until they are close enough to cast the spell.
With infinite range, that wouldnt be possible.
On the other hand, I also tested out SetUnitAcquisitionRange() and it turns out that it works perfectly on this part.
So, maybe, if you want to have acquisition range be equal to your range, then it would work... however, I dont want that.

This looks intriguing...
But we should remember what an enormous amount of overhead such a thing would add to any map that uses fully triggered attacks and missiles for everything.
Only ranged attacks cause an overheat and preloading 1204 ability levels are quite a pain. I havent tested the widgetizer yet though.
On the other hand, you dont seem to know the Ability wise way of showing the missile.
My missile system allows about 1200 missiles on the test map.
Having that amount is already talking about a game like total war.
But next to that, I think Nestharus had a system that used an actual ability to show the missile. Ofcourse this removes a lot of features that you want a missile system to have but still it is extremely efficient.
Having that system installed, these triggered basic attacks have literally 0 difference in performance.

We should ask ourselves the question of if there is any real practical use-case for this in popular map genres...
Now here you make a mistake.
You take the shot to what map genres would use this, but I am not talking about a map genre at all.
You dont even see the possibilities in what this system can do.
As you can see, all units now have the option to use attack ground, no matter what attack they use. This is something that would be extremely usefull in RPG/Dungeon maps.
In other maps such as... everything, forced attacks may still be something that you really like.

(proc effects, armor types, triggering damage, spell reflect abilities, etc.)
0.o, are you sure you posted on the right thread?
I mean these were already possible, and I even have proof of that.

that it is next to impossible for the maker to make a switch
That is also interesting.
I added this system in my AoS map to see what would happen and it turned out that everything worked immediately... with one small exception that the casting point was not 0 yet... during the casting point duration, you cannot order your unit to do something else... it is similar to casting time... I should test that out soon as well.

Mind you, I am not saying you should give this up. It's a great idea and I'd love to see where it goes, but keep in mind that you will probably write this for your personal use alone.
Wietlol said:
 
Level 12
Joined
May 20, 2009
Messages
822
Exactly what I was going to do after I got off my lazy ass and finished my movement system.

For Attack-Once, couldn't you just keep track of when something would want to only attack once and then use a variable to check if that unit is suppose to attack once? If the variable returns true, you stop the attacking and have them go about their other business that may be queued or something.

For attack move, you'll have to issue a point order (Probably through your ability) and keep track of that unit and, based on what it's acquisition range is suppose to be, you keep track of units that enter their range and pick one of those units as a target, making that it's attack-order target, perform a normal attack order, while keeping track of the distance between the unit and the point-order. Also keep track if the unit is engaged in battle or not. If it is, then it should continue attacking. If it is not, once the distance between the unit and the point order is 0 you take them out of the attack-move tracking.

Some suggestions:


Multiple periods of damage ala SC2-style. This breaks down into two things:
How many times the damage will be done.
A delay between each time the damage is done, which can be unique between each time damage is done.

So, you have 4 attack periods that do 5 damage a piece with delays of 4, 2, 1, and 0.5. This unit's normal attack speed is 6. Each time the unit does a period, the delay is cut by 50% giving off a berzerking fury. It will than wait 2 more seconds before repeating this pattern.

Alternatively, you could make their attack speed 2 which would make it crazy!

The advantage to this is the armor reduction is applied multiple times. 5-armor*4 damage is dealt.
 
Last edited:
Status
Not open for further replies.
Top