| 12-02-2010, 07:53 AM | #1 |
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 |
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 |
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 |
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 |
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 |
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. 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 |
xemissile doesn't require xedamage so you should add xedamage as a requirement for this library to prevent hiccups in the future. 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 |
JASS:// Also, constantly re-launch the missile to get a more unique movement arc call this.launch(MAIN_SPEED, MAIN_ARC) 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) 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 |
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 |
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 |
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 |
SetTimerData is part of TimerUtils |
| 12-06-2010, 07:54 AM | #13 |
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. |
