HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Simulating Healing Spray - xecollider style

12-02-2010, 07:53 AM#1
Ignitedstar
Hey, guys. I'm trying to simulate Healing Spray using xecollider. However, I want to make sure I'm heading in the right direction triggering wise before I start anything. I would like as many questions answered as possible, because I know there are plenty of people here who have more experience either using xecollider or know more about parabolic functions that hit the ground.

Let me say something about the spell first. It would probably be easier to make the spell so that when each healing spray particle hits the ground, it will healing allies in a small AoE. This way I don't have to deal with waves and to me, the spell is more realistic.

1) I'm concerned about how triggers calculate the ground's "height". There are a lot of slopes and terrain that curves up and down in my map. What native do I use to accommodate for the dynamic changes in surface heights? Healing spray particles shouldn't wait until they hit the ground's terrain level (like how deep water is -2, shallow water is -1, etc.) to activate.

2) Parabolic functions have always made me angry in math class. I know that xecollider keeps track of an x, y, and z, but the healing spray particles activate based on whether or not they've hit a surface (or z). So far, I've only been using xecollider with method onUnitHit takes unit target returns nothing. I should be able to easily change the conditions?

I think that's it... Getting a parabolic Z should be easy. I'm only really concerned if the xecollider projectiles will detect anything once they've hit a surface (not the terrain level).
12-02-2010, 11:37 AM#2
Tyrande_ma3x
Currently there is a xemissile module that isn't approved but I think it uses both coordinates and units so it would be easier to use that and upon reaching the desired projectiles' x/y do the healing.
As for ground levels... probably move a location at the point of landing and use GetLocationZ()? I don't get how you distinguish surface and ground level.
12-02-2010, 11:48 AM#3
Anitarf
I'm not familiar with the detailed mechanics of Healing Spray, however based on what you describe (only apply the effect once the healing projectile reaches its target destination), xemissile does indeed seem more appropriate than xecollider, which was primarily intended for spells like Shockwave, Carrion Swarm and Breath of Fire; non-parabolic projectile spells that affect units as the projectile passes them, rather than on final impact.
12-02-2010, 05:12 PM#4
Bribe
You already use XE, just use XEMissile as per suggestion. XEMissile should really get approved here, because it hasn't garnered the attention it deserves.
12-02-2010, 07:32 PM#5
Won-Qu
When it comes to arc missiles and I don't want to use any systems I go like this:

Create dummy ability based of Tinker's Factory thingie (don't remember correct name but I hope you get the idea). Set summoned unit life duration to 0.01, unit type to newly created dummy unit (you can even set model of that dummy to some effect you want to play). That Factory ability has that arc projectile feature. Then just use dummy caster to use it on random point in your area, and when you want to get the moment projectile hits ground use:

Events:
Unit dies
Conditions:
Unit type of dying unit == Unit type of dummy unit that you created earlier
Actions:
Whatever you like

Use that idea if you like it, however I bet that it would be more flexible if you found some arc projectiles system.

If you didn't understand what I mean ask me, and I'll try to provide you a testmap with everything explained (try, because I've got some limited access to computer/inet).
12-03-2010, 12:09 AM#6
Ignitedstar
Haha. Thanks you guys. I tried out xemissile. Uugh... It's hard to find out how of this works, so I'm glad Anitarf made an example spell. Haha.

Anyway, I got a spell that works! It's totally awesome. Here's the code. Note: It's similar to a lot of Anitarf's code, but this how I learn... Gotta copy first.

Collapse JASS:
library HealingSpray initializer Init requires xemissile


globals
    private constant integer SPELL_ID          = 'A001'

    //Healing Spray Settings
    private constant string MAIN_MODEL         = "Abilities\\Spells\\Other\\HealingSpray\\HealBottleMissile.mdl"
    private constant real   MAIN_SCALE         = 1.0     //How big each model is.
    private constant real   MAIN_LAUNCH_OFFSET = 60.0    //The Z offset that the model is created.
    private constant real   MAIN_SPEED         = 500.0   //The projectile speed of the particles.
    private constant real   MAIN_ARC           = 0.25    //The arc of the particles.
    private constant real   TARGET_RADIUS      = 100.0   //The AoE of each particle's healing.
    private constant real   RANDOM_X           = 150.    //The X randomness of each particle's destination
    private constant real   RANDOM_Y           = 150.    //The Y randomness of each particle's destination
    private constant real   TIME_OUT           = 0.12    //The frequency of when each particle spawns
endglobals
    
private constant function Heal takes integer level returns real
    return 25. // How much each particle heals
endfunction
    
private constant function Particles takes integer level returns real
    return 8. * level // How many particles are made
endfunction
    
private function setupDamageOptions takes xedamage d returns nothing
    set d.dtype = DAMAGE_TYPE_UNIVERSAL
    set d.atype = ATTACK_TYPE_NORMAL
       
    set d.exception = UNIT_TYPE_STRUCTURE
endfunction
    
struct HealingSprayMain extends xemissile
    private unit    caster
    private integer level

    static method create takes unit caster, integer level, real tx, real ty returns HealingSprayMain
        local real x = GetUnitX(caster)
        local real y = GetUnitY(caster)
        local real z = GetUnitFlyHeight(caster)+MAIN_LAUNCH_OFFSET
        local HealingSprayMain this = HealingSprayMain.allocate(x,y,z, tx,ty,0.0)

        // Let us configure our projectile's custom data:
        set this.caster = caster
        set this.level  = level

        // Time to configure the delegate xefx properties:
        set this.fxpath = MAIN_MODEL
        set this.scale  = MAIN_SCALE

        // Launch the newly created missile:
        call this.launch(MAIN_SPEED, MAIN_ARC)

        return this
    endmethod
     
     private real launchtime = 0.0
     private static group g  = CreateGroup()
     private static HealingSprayMain current
        
        //===============================================================================
    method loopControl takes nothing returns nothing
        // Notice loopControl gets called for the missile every XE_ANIMATION_PERIOD seconds
        set launchtime = launchtime + XE_ANIMATION_PERIOD
            
        // Also, constantly re-launch the missile to get a more unique movement arc
        call this.launch(MAIN_SPEED, MAIN_ARC)
    endmethod
        
//==============================================================================

    private static method finalEnum takes nothing returns boolean
        local unit u = GetFilterUnit()
        local HealingSprayMain this = .current //Don't feel like typing "current" everywhere.
        if IsUnitAlly(u, GetOwningPlayer(.caster)) and GetWidgetLife(u) > 0.0204 then
            call SetWidgetLife(u, GetWidgetLife(u) + Heal(.level))
        endif
        set u = null
        return false
    endmethod

    method onHit takes nothing returns nothing
        set .current = this
        call GroupEnumUnitsInRange(.g, .x, .y, TARGET_RADIUS, Condition(function HealingSprayMain.finalEnum))
    endmethod

endstruct
//=====================================================================================================

private function Conditions takes nothing returns boolean
    return GetSpellAbilityId() == SPELL_ID
endfunction
    
globals
    private real x
    private real y
    private unit c
    private integer l
    private integer Count = 0
endglobals
    
private function TimerCallback takes nothing returns nothing
    local timer t = GetExpiredTimer()
    local real x2 = x + GetRandomReal(-RANDOM_X, RANDOM_X)
    local real y2 = y + GetRandomReal(-RANDOM_Y, RANDOM_Y)
    call HealingSprayMain.create(c, l, x2, y2)
    set Count = Count + 1
    if Count == Particles(l) then
        call PauseTimer(t)
        set t = null
        call DestroyTimer(t)
        set Count = 0
        set c = null   
    endif
endfunction

private function SpellActions takes nothing returns nothing
    local timer t = CreateTimer()
    set x = GetSpellTargetX()
    set y = GetSpellTargetY()
    set c = GetTriggerUnit()
    set l = GetUnitAbilityLevel(c, SPELL_ID)
    call TimerStart(t, TIME_OUT, true, function TimerCallback)
endfunction

//=====================================================================================================

private function Init takes nothing returns nothing
    local trigger HS = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(HS, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(HS, Condition(function Conditions))
    call TriggerAddAction(HS, function SpellActions)
    set HS = null
endfunction
    
endlibrary

It's very simple, but I'm very new to using structs so I'm quite proud of myself.

I have a question, though. Currently, the spell acts independently of whether or not the caster is channeling the spell. What sort condition do I make to stop the spell if the caster stops channeling it? I've never actually made a spell through xe that requires channeling before...
12-03-2010, 05:14 AM#7
Bribe
xemissile doesn't require xedamage so you should add xedamage as a requirement for this library to prevent hiccups in the future.

Collapse JASS:
        // Also, constantly re-launch the missile to get a more unique movement arc
        call this.launch(MAIN_SPEED, MAIN_ARC)

I think healing spray has a 0.40 arc. You should just try putting a higher value rather than doing this very innefficient thing.
12-03-2010, 09:30 AM#8
Anitarf
Collapse JASS:
        // Also, constantly re-launch the missile to get a more unique movement arc
        call this.launch(MAIN_SPEED, MAIN_ARC)
In the example spell, this was used to get a more unconventional movement arc for the main missile. You shouldn't do this for standard missiles. Also, you don't seem to be using the launchtime member anywhere at all, so the entire loopControl method seems unnecessary.

For making it a channeling spell, I would use SpellEvent, since you can easily piggyback on the spellEvent struct. (BTW, your spell code was horrible, not even being MUI, so I rewrote it)

Collapse JASS:
private struct spell extends array //Since we'll be piggybacking on the spellevent struct, we don't need our own constructors and destructors, so we'll make our struct into an array struct.
    private timer t
    private real x
    private real y
    private unit c
    private integer l
    private integer Count = 0

    static method timerCallback takes nothing returns nothing
        local spell this = spell(GetTimerData(GetExpiredTimer())) //Get the struct attached to the timer.
        local real a=GetRandomReal(0.0, bj_PI*2)
        local real d=SquareRoot(GetRandomReal(0.0, RANDOM_OFFSET*RANDOM_OFFSET))
        local real x2 = x + Cos(a)*d // By doing things this way, we get an even distribution of points
        local real y2 = y + Sin(a)*d // in a circular area instead of a square, which looks better.
        call HealingSprayMain.create(.c, .l, x2, y2)
        set .Count = .Count + 1
        if .Count >= Particles(l) then
            call IssueImmedaiteOrder(.c, "stop") // Stops the channeling.
        endif
    endmethod

    static method onEffect takes nothing returns nothing
        local spell this=spell(SpellEvent) // Typecast the SpellEvent struct to get a unique spell struct.
        set .x=SpellEvent.TargetX
        set .y=SpellEvent.TargetY
        set .c=SpellEvent.CastingUnit
        set .l = GetUnitAbilityLevel(.c, SPELL_ID)
        set .t=NewTimer()
        call SetTimerData(.t, integer(this)) // Attach the struct to the timer.
        call TimerStart(.t, TIME_OUT, true, function spell.timerCallback)
    endmethod

    static method onEndCast takes nothing returns nothing
        local spell this=spell(SpellEvent) // Same trick as above.
        call ReleaseTimer(.t)
    endmethod

    static method onInit takes nothing returns nothing
        call RegisterSpellEffectResponse(SPELL_ID, spell.onEffect)
        call RegisterSpellEndCastResponse(SPELL_ID, spell.onEndCast)
    endmethod
endstruct
12-04-2010, 07:57 AM#9
Ignitedstar
Haha. Thanks Anitarf. I'm still new to structs so I still kind of don't understand how they work... Like, I still don't get the difference between methods and static methods. Somebody was saying to me that it's like private functions in structs, but when I tried telling them that, they said I was wrong... so yeah. I still don't get it.

About this.launch; I didn't think that the struct was using it, but I wasn't sure. So I just left it there.

I don't know what "typecasting" is. Is the definition of it in the Jass Manual?

Wait a minute. Did you put the entire spell into the struct? I didn't know you could do that. lol

Oh, dear. Another struct to look at. I'm so confused, already.
12-04-2010, 09:24 AM#10
Anitarf
Internally, vJass structs are actually just sets of parallel JASS arrays. When you create a new struct, what the internal code does is it reserves an index for those arrays and once you destroy a struct, its index becomes available again for reuse. So for example, by creating a new struct whenever a spell is cast, we can store data for the duration of the spell without fear that other spells cast during this time will overwrite our data, like they would with your original code.

Similarly, xemissile allows for any number of missiles, since whenever we want a new missile we create a new struct so old missiles can't be overwritten. Think of structs as custom data objects, like native JASS locations except that they aren't limited to storing only two reals.

Now, methods are what we call functions inside structs. What is special about methods is that we call them to do work on a specific struct instance. method doStuff takes nothing returns nothing is equivalent to function myStruct_doStuff takes myStruct this returns nothing. To continue with the location analogy, if WC3 handles had methods, the method equivalent of call MoveLocation(l, x,y) would be call l.move(x,y).

Static methods don't have this special property of being called on a struct instance, so they work just as regular functions. static method doStaticStuff takes nothing returns nothing is equivalent to function myStruct_doStaticStuff takes nothing returns nothing.

To explain what I did in my code example, there's one more thing I need to bring up: array structs. As I said earlier, structs are just sets of parallel arrays with some autogenerated functions for reserving and releasing indexes in those arrays (the .allocate and .destroy methods). Sometimes, we have our own way of allocating indexes, though, and don't need those autogenerated .allocate and .destroy methods. In such cases, we use array structs which don't have these methods.

What I did was take advantage of internal workings of SpellEvent. What it does is create a spellEvent struct whenever a spell is cast (and then destroys it when the spell finishes casting). Each of these structs already represents a unique array index which no other spell has. Instead of using .allocate to get a unique index for my spell struct, I decided to simply inherit the index from the spellEvent struct.
12-05-2010, 11:54 PM#11
Ignitedstar
Ah, the only godsend on the forums. Bless you, Anitarf.

I would have everything working, but I have one question. I don't think SetTimerData exists? If I knew that I could attach data to timers...
12-06-2010, 01:51 AM#12
Fledermaus
SetTimerData is part of TimerUtils
12-06-2010, 07:54 AM#13
Anitarf
Heh, completely forgot to mention I was using TimerUtils (SetTimerData is not the only custom feature, so are NewTimer and ReleaseTimer), they're just so standard these days that I was half expecting your map to already have them.