HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Charging... What a pain.

08-06-2008, 04:37 PM#1
EquippedChaos
Darkwulfv again. It's always me who has the problems, isn't it?

Anyways, I'm working on a new spell; ignition. It's a bunch of stuff, but long story short the caster is supposed to charge a target. Sounds easy, right?

Weeelll.. Not for me. This is entirely new to me, so of course, it screws up.

Now the caster moves, oh yeah. Looks great, loving it. Problem is, he moves in the wrong direction. I believe this has everything to do with my trigonometry, but I don't know how to do it properly for this situation.

Here's the whole ugly code (it's unfinished. I'm more concerned with it working than it working well). I'll pick out parts that I believe are causing the issues, but you can pick through the whole code if you so wish.
Collapse JASS:
scope Ignition initializer Init

//add constants for SFX units + comment others

globals
  private constant integer ABILITY_ID = 'A000'
  private constant integer IMMOLATE_ID = 'A001'
  private constant integer TRAIL_DUMMY = 'e000'
//How many timer ticks per created flame
  private constant integer FLAME_INTERVAL = 2
//How many flame bursts created
  private constant integer FLAME_BURSTS = 1
//How many gut explosions created
  private constant integer GUT_EXPLOSIONS = 5
  private constant integer FLAME_BURST_ID = 'e002'
  private constant integer GUT_EXPLOSION_ID = 'e001'
//Red value of text
  private constant integer TEXT_RED = 255
//Green value of text
  private constant integer TEXT_GREEN = 64
//Blue value of text
  private constant integer TEXT_BLUE = 0
//Size of text
  private constant integer TEXT_SIZE = 12
  
  private constant real TIMER_INTERVAL = .035
//The speed at which the run animation plays. 
//It's in %, so 150 is 50% faster, and so on.
  private constant real ANIMATION_SPEED = 150.
//Base damage, modified later in "Damage" function.
//How far the caster moves per timer tick
  private constant real SPEED = 5.
//Heh, it rhymes. Anyways, this is the precise duration of ATTACK_ANIMATION.
  private constant real ANIMATION_DURATION = 1.133
//How long the trail flames last
  private constant real TRAIL_DURATION = 3
//Duration of text
  private constant real TEXT_DURATION = 4.
  
  private constant string RUN_ANIMATION = "Walk"
  private constant string ATTACK_ANIMATION = "Attack Slam"
//Fire attached to the caster
  private constant string MOVE_FIRE = "Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl"
//Fire left behind the caster
  private constant string TRAIL_FIRE = "Abilities\\Spells\\Other\\BreathOfFire\\BreathOfFireDamage.mdl"
//Fire made when target explodes
  private constant string BURST_FIRE = "Abilities\\Spells\\Other\\Incinerate\\FireLordDeathExplode.mdl"
//Guts made when target explodes
  private constant string BURST_GUTS = "Objects\\Spawnmodels\\Orc\\OrcSmallDeathExplode\\OrcSmallDeathExplode.mdl"
//Attachment points
  private constant string ATTACH_1 = "hand left"
  private constant string ATTACH_2 = "hand right"
  private constant string ATTACH_3 = "chest"
  private constant string ATTACH_4 = "weapon"
  private constant string ATTACH_5 = "origin"
//These are the messages shown during the spell. Set them to "" if you don't want any messages shown.
  private constant string CAST_MESSAGE = "Ignite..."
  private constant string MIDDLE_MESSAGE = "And..."
  private constant string END_MESSAGE = "BURN!!"
  
  private constant attacktype ATTACK_TYPE = ATTACK_TYPE_CHAOS
  private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_UNIVERSAL
  private constant weapontype WEAPON_TYPE = null
endglobals

private constant function Damage takes integer level returns real
  return 200. * level
endfunction

private constant function AOEDamage takes integer level returns real
  return 50. + (level * 50)
endfunction

private constant function Radius takes integer level returns real
  return 200. + (level * 25)
endfunction


//No touching from here on out!
globals
  private gamecache GCache = null
endglobals
struct IgnitionData
  unit caster = null
  unit target = null
  integer level = 0
  integer flametick = 0
  integer ticks = 0
  integer maxticks = 0
  real angle = 0
  real movedistanceX = 0
  real movedistanceY = 0
  effect array SFX[8191]
  
  static method create takes unit caster, unit target, integer level, real angle, real distance returns IgnitionData
    local IgnitionData D = IgnitionData.allocate()
    
    set D.caster = caster
    set D.target = target
    set D.level = level
    set D.angle = angle
    set D.movedistanceX = SPEED * Cos(angle)
    set D.movedistanceY = SPEED * Sin(angle)
    set D.maxticks = R2I(distance / SPEED)
    
    return D
  endmethod
endstruct


private function Knockback takes unit target, real angle returns nothing
  call SetUnitPosition(target, GetUnitX(target) + 50 * Cos(angle), GetUnitY(target) + 50 * Sin(angle))
endfunction

private function knockfilter takes nothing returns boolean
  local unit u = GetFilterUnit()
  local boolean b1 = IsUnitType(u, UNIT_TYPE_STRUCTURE) == false
  local boolean b2 = IsUnitType(u, UNIT_TYPE_FLYING) == false
  local boolean b3 = IsUnitType(u, UNIT_TYPE_DEAD) == false
  
  set u = null
  return b1 and b2 and b3
endfunction

private function filter takes nothing returns boolean
  local unit u = GetFilterUnit()
  local boolean b1 = IsUnitType(u, UNIT_TYPE_STRUCTURE) == false
  local boolean b2 = IsUnitType(u, UNIT_TYPE_FLYING) == false
  local boolean b3 = IsUnitType(u, UNIT_TYPE_DEAD) == false
  local boolean b4 = IsUnitEnemy(u, GetOwningPlayer(GetTriggerUnit()))
  local boolean b5 = GetUnitAbilityLevel(u, 'Avul') < 1
  
  set u = null
  return b1 and b2 and b3 and b4 and b5
endfunction

private function DamageFunc takes nothing returns nothing
  local timer t = GetExpiredTimer()
  local IgnitionData Data = GetStoredInteger(GCache, "GCache", "IgnitionDat")
  local unit u = Data.caster
  local unit u2 = Data.target
  local integer lvl = Data.level
  local real x = GetUnitX(u2)
  local real y = GetUnitY(u2)
  local group g = CreateGroup()
  local integer i = 1
  local unit f
  local player p = GetOwningPlayer(u)
  
  call SetUnitAnimation(u, "stand")
  
  call UnitDamageTarget(u, u2, Damage(lvl), false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
  if Damage(lvl) > GetWidgetLife(u2) then
    loop
    exitwhen i > FLAME_BURSTS
      call UnitApplyTimedLife(CreateUnit(p, FLAME_BURST_ID, x, y, 270.), 'BTLF', i * .3)
      set i = i + 1
    endloop
    
    set i = 1
    
    loop
    exitwhen i > GUT_EXPLOSIONS
      call UnitApplyTimedLife(CreateUnit(p, GUT_EXPLOSION_ID, x, y, 270.), 'BTLF', i * .3)
      set i = i + 1
    endloop
    
    call GroupEnumUnitsInRange(g, x, y, Radius(lvl), Condition(function filter))
    loop
    set f = FirstOfGroup(g)
      exitwhen f == null
      call UnitDamageTarget(u, f, AOEDamage(lvl), false, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
      call GroupRemoveUnit(g, f)
    endloop
  endif
  
  call PauseTimer(t)
  call DestroyTimer(t)
  call DestroyGroup(g)
  call Data.destroy()
  set t = null
  set u = null
  set u2 = null
  set g = null
  set f = null
  set p = null
endfunction
  
private function Final takes IgnitionData dat returns nothing
  local unit caster = dat.caster
  local integer i = 1
  
  call SetUnitAnimation(caster, ATTACK_ANIMATION)
  call SetUnitTimeScale(caster, 100 * 0.01)
  
  call TimerStart(CreateTimer(), ANIMATION_DURATION, false, function DamageFunc)
  
  loop
  exitwhen i > 6
    call DestroyEffect(dat.SFX[i])
    set i = i + 1
  endloop
  
  call PauseUnit(caster, false)
  call SetUnitInvulnerable(caster, false)
  if GetLocalPlayer() == GetOwningPlayer(caster) then
    call SelectUnit(caster, true)
  endif
  
  set caster = null
endfunction

private function Callback takes nothing returns nothing
  local timer t = GetExpiredTimer()
  local group g = CreateGroup()
  local unit f
  local IgnitionData Data = GetStoredInteger(GCache, "GCache", "IgnitionDat")
  local unit u = Data.caster
  local unit u2 = Data.target
  local real x = GetUnitX(u)
  local real y = GetUnitY(u)
  local real x2 = GetUnitX(u2)
  local real y2 = GetUnitY(u2)
  local real newx = 0
  local real newy = 0
  local real angle = Atan2(y2 - y, x2 - x)
  local integer lvl = Data.level
  
  
  //Make Flames
  if Data.flametick == FLAME_INTERVAL then
    call UnitApplyTimedLife(CreateUnit(GetOwningPlayer(u), TRAIL_DUMMY, x, y, 270.), 'BTLF', TRAIL_DURATION)
    set Data.flametick = 0
  endif
  
  //Middle Text
  if Data.ticks == Data.maxticks / 2 then
    if MIDDLE_MESSAGE != "" then
      call CreateTextTagEX(MIDDLE_MESSAGE, TEXT_SIZE, u, TEXT_DURATION, TEXT_RED, TEXT_GREEN, TEXT_BLUE) 
    endif  
  endif
  
  //Checking if moving still possible
  if Data.ticks < Data.maxticks then
    set newx = x + (SPEED * Cos(angle))
    set newy = y + (SPEED * Sin(angle))
    call SetUnitPosition(u, newx, newy )
    call GroupEnumUnitsInRange(g, newx, newy, 35., Condition(function knockfilter))    
    
    //Knockback units in way
    loop
    set f = FirstOfGroup(g)
      exitwhen f == null
      
      if Data.angle >= Atan2(GetUnitY(f) - newy, GetUnitX(f) - newx) then
        call Knockback(f, Data.angle - (bj_DEGTORAD * 90))
      else
        call Knockback(f, Data.angle + (bj_DEGTORAD * 90))
      endif
      
      call GroupRemoveUnit(g, f)
    endloop
  
    set Data.ticks = Data.ticks + 1  
    set Data.flametick = Data.flametick + 1
    
  //End of spell 
  else
  if END_MESSAGE != "" then
    call CreateTextTagEX(END_MESSAGE, TEXT_SIZE, u, TEXT_DURATION, TEXT_RED, TEXT_GREEN, TEXT_BLUE) 
  endif
    call PauseTimer(t)
    call DestroyTimer(t)
    call Final(Data)
  endif  
  
  call DestroyGroup(g)
  set t = null
  set u = null
  set g = null
  set f = null
endfunction

private function Conditions takes nothing returns boolean
  return ( GetSpellAbilityId() == ABILITY_ID )
endfunction

private function Actions takes nothing returns nothing
  local unit u = GetTriggerUnit()
  local unit u2 = GetSpellTargetUnit()
  local integer lvl = GetUnitAbilityLevel(u, ABILITY_ID)
  local timer t = CreateTimer() 
  local real x = GetUnitX(u)
  local real y = GetUnitY(u)
  local real x2 = GetUnitX(u2)
  local real y2 = GetUnitY(u2)
  local real dx = x2 - x
  local real dy = y2 - y
  local real distance = SquareRoot(dx * dx + dy * dy)
  local IgnitionData data = IgnitionData.create(u, u2, lvl, Atan2(y2 - y, x2 - x), distance)
  
  if CAST_MESSAGE != "" then
    call CreateTextTagEX(CAST_MESSAGE, TEXT_SIZE, u, TEXT_DURATION, TEXT_RED, TEXT_GREEN, TEXT_BLUE) 
  endif
  
  set data.SFX[1] = AddSpecialEffectTarget(MOVE_FIRE, u, ATTACH_1)
  set data.SFX[2] = AddSpecialEffectTarget(MOVE_FIRE, u, ATTACH_2)
  set data.SFX[3] = AddSpecialEffectTarget(MOVE_FIRE, u, ATTACH_3)
  set data.SFX[4] = AddSpecialEffectTarget(MOVE_FIRE, u, ATTACH_4)
  set data.SFX[5] = AddSpecialEffectTarget(MOVE_FIRE, u, ATTACH_5)
  
  call UnitAddAbility(u, IMMOLATE_ID)
  call SetUnitAbilityLevel(u, IMMOLATE_ID, lvl)
  
  call PauseUnit(u, true)
  call SetUnitInvulnerable(u, true)
  if GetLocalPlayer() == GetOwningPlayer(u) then
    call ClearSelection()
  endif
  
  call PolledWait(.75)
  
  call SetUnitAnimation(u, RUN_ANIMATION)
  call SetUnitTimeScale(u, ANIMATION_SPEED * 0.01)
  call StoreInteger(GCache, "GCache", "IgnitionDat", data)
  call TimerStart(t, TIMER_INTERVAL, true, function Callback)
  
  set u = null
  set u2 = null
  set t = null
endfunction

//===========================================================================
private function Init takes nothing returns nothing
  local trigger t = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition(t, Condition( function Conditions ) )
    call TriggerAddAction(t, function Actions )
    call Preload(TRAIL_FIRE)
    call Preload(BURST_FIRE)
    call Preload(BURST_GUTS)
    call FlushGameCache(InitGameCache("GCache"))
    set GCache = InitGameCache("GCache")
  set t = null  
endfunction
endscope

And now the areas I deem problematic:
Collapse JASS:
  local real x = GetUnitX(u)
  local real y = GetUnitY(u)
  local real x2 = GetUnitX(u2)
  local real y2 = GetUnitY(u2)
  local real angle = Atan2(y2 - y, x2 - x)
   set newx = x + (SPEED * Cos(angle))
   set newy = y + (SPEED * Sin(angle))

  local real distance = SquareRoot(dx * dx + dy * dy)
  local IgnitionData data = IgnitionData.create(u, u2, lvl, Atan2(y2 - y, x2 - x), distance)

I hope you get what I mean. Now, in my test map, the caster targets a unit below him (not directly; about 45 degrees lower), and goes flying upwards at that angle instead of downwards. He also goes like, 15x longer than he should but I can probably figure that out on my own.

I also have a question; What is the best way to determine that the caster is in the middle of the distance he is supposed to travel, and that he is at the point he is supposed to end at? Right now, I just divide the distance by the units-moved-per-interval and check it in "ticks" (eg: 100 distance / 5 speed = 20 ticks so 10 ticks would be middle, 20 would be end), but right now he moves like all over the map instead of just ~600 distance. He moves about 2000+.
(My example probably doesn't work, which is probably why my actual situation doesn't either)

I hope I've given enough information and that it's decently clear enough to work with. I know my code is disgusting, but I'm going to clean it and make it look pretty after I get the spell itself working.

Note: I suck at trigonometry since I've been learning it crash-course style, so if this is something as simple as putting a - where a + is, let me know. Of course, let me know how to check that it should be a - instead of a + in accordance to the situation.

Thanks for any help I get, and bear with me... this isn't my strong point.
08-07-2008, 03:55 AM#2
Pyrogasm
Quote:
Originally Posted by EquippedChaos
call StoreInteger(GCache, "GCache", "IgnitionDat", data)
If you're just going to do that... why have a gamecache at all?! Were you going for MUI or not?

A few other things:
  • If the angle may or may not change every tick, why store the angle and the X/Y offsets each time? It's pointless to do so/
  • I would say using SetUnitPosition is a bit, well, "meh" for this spell. He's on fire and charging but he's not going to ram through trees and/or other units?
  • Collapse JASS:
    local unit u = Data.caster
    local unit u2 = Data.target
    Why?
  • Collapse JASS:
    call SetUnitPosition(u, newx, newy )
    call GroupEnumUnitsInRange(g, newx, newy, 35., Condition(function knockfilter)) //Assume this line represents all the other knockback stuff too
    I would switch the order of these two lines because (in theory) using SetUnitPosition the unit could end up not at (newx, newy) but rather somewhere else. If you knocked back before moving the unit, other units that are in the way might be moved so that the unit is free to go where you want him to.

    If you don't want to switch the order, I would at least do this (though it is a bit more computation)
    Collapse JASS:
    call SetUnitPosition(u, newx, newy )
    call GroupEnumUnitsInRange(g, GetUnitX(u), GetUnitY(u), 35., Condition(function knockfilter)) //Assume this line represents all the other knockback stuff too
    And then if you stored those values instead of just calculating them, you wouldn't have to calculate them on the next iteration of the callback!
  • call SetUnitTimeScale(caster, 100 * 0.01) ...
  • Instead of this:
    Collapse JASS:
      if GetLocalPlayer() == GetOwningPlayer(caster) then
        call ClearSelection()
      endif
    I'd do this:
    Collapse JASS:
      if GetLocalPlayer() == GetOwningPlayer(caster) then
        call SelectUnit(caster, false)
      endif
  • Instead of all the groups you keep creating and then destroying, it would make much more sense to just use 1 global group and never destroy it.
Quote:
Originally Posted by EquippedChaos
I also have a question; What is the best way to determine that the caster is in the middle of the distance he is supposed to travel, and that he is at the point he is supposed to end at? Right now, I just divide the distance by the units-moved-per-interval and check it in "ticks"
That's pretty much the best way I can think of, though it might be off by 1 tick if MaxTicks is an odd number. Meh.


Dunno what's up with the angle. Posting the map would be a good idea.
08-07-2008, 04:24 AM#3
EquippedChaos
Wow, stuff to change.

Uhh, yeah a few things are iffy (like the 100 * .01), but like i said I wanted this to work before it works well.

Quote:
If you're just going to do that... why have a gamecache at all?! Were you going for MUI or not?
Uhh.. In that thread for Word of Power, Dusk got at the spell-submitter for using Handle Vars, and told him to use his own gamecahce and StoreInteger instead.

Sooo... That's exactly what I'm doing. I thought it was MUI though, guess not. Maybe I'll just stick to Handle Vars.

Quote:
If the angle may or may not change every tick, why store the angle and the X/Y offsets each time? It's pointless to do so/
It only stores those angles once, doesn't it? And the X/Y distance was for less calculation needed (since it'd know exactly what distance to move every time), but I don't actually use them yet (because I thought they were causing issues).

Quote:
I would say using SetUnitPosition is a bit, well, "meh" for this spell. He's on fire and charging but he's not going to ram through trees and/or other units?
SetUnitPosition saved my ass; when this spell bugged he ran right up to the map bounds and would've kept going until the game crashed. Besides, he does ram through other units (the knockback), and I was considering blowing up destructables as well.

Quote:
Why?
Habit... Uhh, less calls to the struct? Will fix.

Quote:
I would switch the order of these two lines because (in theory) using SetUnitPosition the unit could end up not at (newx, newy) but rather somewhere else. If you knocked back before moving the unit, other units that are in the way might be moved so that the unit is free to go where you want him to.
Good point. Will do.

Quote:
Instead of all the groups you keep creating and then destroying, it would make much more sense to just use 1 global group and never destroy it.
I suppose it would.

Quote:
Dunno what's up with the angle. Posting the map would be a good idea.
I'm not sure why the map would change anything, but uh... I will when I make those changes above.
08-07-2008, 04:38 AM#4
Pyrogasm
Quote:
Originally Posted by EquippedChaos
Uhh.. In that thread for Word of Power, Dusk got at the spell-submitter for using Handle Vars, and told him to use his own gamecahce and StoreInteger instead.

Sooo... That's exactly what I'm doing. I thought it was MUI though, guess not. Maybe I'll just stick to Handle Vars.
You have the right idea, you're just not storing things properly. Instead of the missionkey (last argument) being "IgnitionDat", it should be I2S(H2I(t)).