• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Townportal v.5.0

Town Portal

160036-albums4747-picture68394.png


"...and through the shadows, a circular entity emerged. I couldn't distinguish it's substance; one would say that it was a portal. What could possibly lie on the other side? Was it an entrance to another dimension or was it an exit to our dimension...?".
Zephyr Challenge #10

API
function CreateTownPortal takes unit source, real x, real y, real z, real face, real duration returns nothing
  • Creates a new portal at x/y with a facing angle "face" over passed in duration.
  • Argument "z" is the fly height of the bar dummy unit.
  • Also destroys the previously created portal from unit "source", if one exists.
function GetUnitTownPortal takes unit source returns TownPortal
  • Will return 0 if the unit doesn't have a portal.
function DestroyTownPortal takes unit source returns nothing
  • Destroys an open portal or/and cancels the porting process.
  • Does nothing on invalid unit argument ( fail safe function )
function CancelUnitTownPortalCast takes unit source returns boolean
  • Cancels an ongoing port process for unit "source".
  • Does nothing if the portal is already built or the unit argument is invalid.
  • Call this function for instance to interupt a porting process on damage event.
There is an object oriented API for each wrapper function, however unlike the wrapper functions,
those don't check for invalid method arguments. Therefore please use the wrapper functions.​


Obligatory RequirementsOptional Requirements

JASS:
library TownPortal/* v.5.0
*************************************************************************************
*
*   TownPortal provides an unique teleport behaviour.
*   After channeling a given time, the caster teleports to the desired location,
*   creating a portal to ensure a save return journey. Other unit can also use an open portal.
*   A portal closes, when the owning unit re-enters it or opens a new one. 
*   Any attempt to teleport is interrupted on order event or dislocation.
*
*************************************************************************************
*
*   */ uses /*
*
*       */ Table           /* hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
*       */ TimerUtils      /* wc3c.net/showthread.php?t=101322
*       */ MissileRecycler /* hiveworkshop.com/forums/jass-resources-412/system-missilerecycler-206086/
*       */ optional ARGB   /* wc3c.net/showthread.php?t=101858
*
************************************************************************************/
                native UnitAlive takes unit id returns boolean
//**
//*  API:
//*  ====
//! novjass
    function CreateTownPortal  takes unit source, real x, real y, real z, real face, real duration returns nothing
    //* "z" is the fly height of the bar dummy unit. 
    //* "duration" is the casting time for this instance.
    
    function GetUnitTownPortal        takes unit source returns TownPortal
    //* Returns 0 if no instance is allocated for unit source.
    
    function DestroyTownPortal        takes unit source returns nothing
    //* Destroy an instance in any phase.
    
    function CancelUnitTownPortalCast takes unit source returns boolean
    //* Only destroy an instance during the channeling phase.
//! endnovjass
//**
//*  User settings:
//*  ==============
    globals
        //*  Set the bar unit raw.
        private constant integer BAR_UNIT_ID             = 'n000'
        //*  Units in this range of a portal center enter it.
        private constant real DETECTION_RANGE            = 100.
        //*  Set timer accuracity. If unsure set it to .031250000
        private constant real ACCURATICY                 = .031250000
        //*  Set the fx displayed while porting.
        private constant string DURING_PORT_EFFECT       = "Abilities\\Spells\\Human\\MassTeleport\\MassTeleportTo.mdl"
        private constant string DURING_PORT_ATTACH_POINT = "origin"
        //*  Set the fx create on the destination coordinates.
        private constant string AFTER_JOURNEY_EFFECT     = "Abilities\\Spells\\Undead\\OrbOfDeath\\OrbOfDeathMissile.mdl"
        //*  Set the fx left behind after the portal creator disappears.
        private constant string DEPARTURE_EFFECT         = "Abilities\\Spells\\NightElf\\Blink\\BlinkCaster.mdl"
        //*  Set if a portal inherits a player color or not. ( Requires library ARGB )
        private constant boolean PORTAL_USE_PLAYER_COLOR = true
    endglobals

    //*  Set the portal model.
    private constant function GetPortalModel takes unit porter returns string
        return "doodads\\cinematic\\ShimmeringPortal\\ShimmeringPortal.mdl"
    endfunction
    //*  Set the portal model scale.
    private constant function GetPortalModelScale takes unit porter returns real
        return 0.55
    endfunction
    //*  Set the delay within any unit can't use the portal after creation.
    private constant function GetPortalBuildTime takes unit porter returns real
        return 1.
    endfunction
    //*  Filter which units can use the portal.
    private function FilterUnit takes unit porter, unit portalOwner returns boolean
        return UnitAlive(porter)// and IsUnitAlly(porter, GetOwningPlayer(portalOwner))
    endfunction
    //*  Must return true to continue a port. False cancels a port right away.
    private function UnitPositionCondition takes real prevX, real unitX, real prevY, real unitY returns boolean
        return (prevX == unitX) and (prevY == unitY)
    endfunction
//**
//*  Module initializer:
//*  ===================
    private module Inits
        private static method onInit takes nothing returns nothing
            call thistype.init()
        endmethod
    endmodule
//**
//*  Unit reference:
//*  ===============
    globals
        //*  Use unit handle id.
        private Table table = 0
    endglobals
//**
//*  Unit Indexer setup:
//*  ===================  
    private function ToogleUnitIndexer takes boolean enable returns boolean 
        local boolean prevSetup = true
        static if LIBRARY_UnitIndexer then
            set prevSetup = UnitIndexer.enabled
            set UnitIndexer.enabled = enable
        elseif LIBRARY_UnitIndexerGUI then
            set prevSetup = udg_UnitIndexerEnabled
            set udg_UnitIndexerEnabled = enable
        elseif LIBRARY_UnitDex then
            set prevSetup = UnitDex.Enabled
            set UnitDex.Enabled = enable
        endif
        return prevSetup
    endfunction
//**
//*  Sound implementation:
//*  =====================
    //*  Will fail on first play... I'm waiting for a good Sound library.
    private function NewSound takes string path, unit source returns nothing
        set bj_lastPlayedSound = null
        set bj_lastPlayedSound = CreateSound(path, false, true, true, 10, 10, "CombatSoundsEAX")
        call SetSoundChannel(bj_lastPlayedSound, 5)
        call SetSoundVolume(bj_lastPlayedSound, 127)
        call SetSoundDistances(bj_lastPlayedSound, 600, 10000)
        call SetSoundDistanceCutoff(bj_lastPlayedSound, 3000)
        call SetSoundConeAngles(bj_lastPlayedSound, 0, 0, 127)
        call SetSoundConeOrientation(bj_lastPlayedSound, 0, 0, 0)
        call AttachSoundToUnit(bj_lastPlayedSound, source)
        call StartSound(bj_lastPlayedSound)
        call KillSoundWhenDone(bj_lastPlayedSound)
    endfunction

    //*  You may delete or change the content of these 3 functions.
    private function SoundOnPortalDestroy takes unit portal returns nothing
        call NewSound("Sound\\Ambient\\DoodadEffects\\ShimmeringPortalDeath.wav", portal)
    endfunction
    
    private function SoundOnUnitEnterPortal takes unit porter returns nothing
        call NewSound("Sound\\Ambient\\DoodadEffects\\ShimmeringPortalEntrance.wav", porter)
    endfunction
    
    private function SoundOnUnitStartPort takes unit porter returns nothing
        call NewSound("Abilities\\Spells\\Human\\MassTeleport\\MassTeleportTarget.wav", porter)
    endfunction
//**
//*  Error finding:
//*  ==============
static if PORTAL_USE_PLAYER_COLOR and not LIBRARY_ARGB then
    "Error: Library TownPortal,  user setting PORTAL_USE_PLAYER_COLOR = true requires library ARGB!" 
endif
//**
//*  Struct TownPortal:
//*  ==================
    struct TownPortal
        private static boolexpr expression
    
        private unit    owner
        private unit    dummy
        private unit    bar
        private effect  dummyFx
        private effect  unitFx
        private region  reg
        private trigger trig
        private timer   clock
        private sound   audio
        //*
        private real    time
        private integer order
        private real    unitX
        private real    unitY
        
        //*  Trys to disable unit indexers.
        private method createBarUnit takes real x, real y, real z, real time returns nothing
            local boolean prev = ToogleUnitIndexer(false)
            local unit    temp = CreateUnit(GetOwningPlayer(owner), BAR_UNIT_ID, x, y, 270.)
            call ToogleUnitIndexer(prev)
            call UnitAddAbility(temp, 'Aloc')
            call UnitAddAbility(temp, 'Amrf')
            call SetUnitFlyHeight(temp, z, 0.)
            call PauseUnit(temp, true)
            call SetUnitTimeScale(temp, 1./time)
            set bar  = temp
            set temp = null
        endmethod
        
        private method createPortalRegion takes real x, real y returns nothing
            local real offset = DETECTION_RANGE*.5
            local rect tempRect = Rect(x - offset, y - offset, x + offset, y + offset)
            set reg = CreateRegion()
            set trig = CreateTrigger()
            call RegionAddRect(reg, tempRect)
            call TriggerAddCondition(trig, thistype.expression)
            call TriggerRegisterEnterRegion(trig, reg, null)
            call RemoveRect(tempRect)
            set tempRect = null
        endmethod
        
        //*  Prevent visual glitches, as MissileRecycler adjust the dummy facing a bit.
        private static method releaseDummy takes nothing returns nothing
            local unit u = bj_lastCreatedUnit
            set bj_lastCreatedUnit = null
            call TriggerSleepAction(3.)
            call RecycleMissile(u)
            set u = null
        endmethod
        
        method destroy takes nothing returns nothing
            set bj_lastCreatedUnit = dummy
            call ExecuteFunc(thistype.releaseDummy.name)
            //*  Clean rest.
            call table.remove(GetHandleId(trig))
            call table.remove(GetHandleId(owner))
            if (dummyFx != null) then
                call DestroyEffect(dummyFx)
                call SoundOnPortalDestroy(dummy)
            endif
            if (unitFx != null) then
                call DestroyEffect(unitFx)
            endif
            call DestroyEffect(unitFx)
            call RemoveRegion(reg)
            call TriggerClearConditions(trig)
            call DestroyTrigger(trig)
            if (clock != null) then
                call ReleaseTimer(clock)
            endif
            if (bar != null) then
                call RemoveUnit(bar)
            endif
            if (audio != null) then
                call StopSound(audio, true, true)
            endif
            //*
            set bar     = null
            set clock   = null
            set dummy   = null
            set owner   = null
            set dummyFx = null
            set unitFx  = null
            set reg     = null
            set trig    = null
            set audio   = null
        endmethod
        
        //*  Runs when a unit comes in portal range.
        private static method onEnter takes nothing returns boolean
            local thistype this = table[GetHandleId(GetTriggeringTrigger())]
            local unit source   = GetTriggerUnit() 
            //*  Clock is only null when the portal is ready.
            if (clock == null) and FilterUnit(source, owner) then
                call DestroyEffect(AddSpecialEffect(AFTER_JOURNEY_EFFECT, unitX, unitY))
                call SetUnitPosition(source, unitX, unitY)
                call SoundOnUnitEnterPortal(source)
                if (source == owner) then
                    call destroy()
                endif
            endif
            set source = null
            return false
        endmethod
        
        method cancel takes nothing returns boolean
            if (bar != null) then
                call destroy()
                return true
            endif
            return false
        endmethod
            
        private static method callback takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            call ReleaseTimer(clock)
            set clock = null
        endmethod
        
        private static method onPeriodic takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            local unit source = owner
            local real posX = GetUnitX(source)
            local real posY = GetUnitY(source)
            local real angle
            static if LIBRARY_ARGB and PORTAL_USE_PLAYER_COLOR then
                local ARGB color
            endif

            //*
            set time = time - ACCURATICY
            if UnitAlive(source) and (time <= 0.) then
                set unitX = posX
                set unitY = posY
                set angle = GetUnitFacing(dummy)*bj_DEGTORAD
                //*  Check for collision.
                call DestroyEffect(unitFx)
                call SetUnitPosition(source, GetUnitX(dummy), GetUnitY(dummy))
                call SetUnitFacing(source, GetUnitFacing(dummy))
                //*  Walk a few meters.
                call IssuePointOrderById(source, 851986, GetUnitX(dummy) + DETECTION_RANGE*Cos(angle), GetUnitY(dummy) + DETECTION_RANGE*Sin(angle))
                call DestroyEffect(AddSpecialEffect(DEPARTURE_EFFECT, posX, posY))
                set dummyFx = AddSpecialEffectTarget(GetPortalModel(source), dummy, "origin")
                static if PORTAL_USE_PLAYER_COLOR and LIBRARY_ARGB then
                    set color = ARGB.fromPlayerColor(GetPlayerColor(GetOwningPlayer(source)))
                    call SetUnitVertexColor(dummy, color.red, color.green, color.blue, color.alpha)
                endif
                //*
                call TimerStart(clock, GetPortalBuildTime(source), false, function thistype.callback)
                call RemoveUnit(bar)
                set bar    = null
                set unitFx = null
            //*  Check continue condition.   
            elseif not UnitAlive(source) or not UnitPositionCondition(unitX, posX, unitY, posY) or (order != GetUnitCurrentOrder(source)) then
                //*  Cancel channeling order.
                if (order == GetUnitCurrentOrder(source)) and (order != 0) then
                    call IssueImmediateOrderById(source, 851972)
                endif
                call destroy()
            //*  Adjust the last timer timeout.
            elseif (time > 0.) and (time < ACCURATICY) then
                call TimerStart(clock, time, true, function thistype.onPeriodic)
            endif
            set source = null
        endmethod

        //*  Use wrapper function CreatePortal() instead.
        static method create takes unit source, real x, real y, real z, real angle, real duration returns thistype
            local thistype this = thistype.allocate()
            //*  Assign variables.
            set owner  = source
            set clock  = NewTimerEx(this)
            set unitX  = GetUnitX(source)
            set unitY  = GetUnitY(source)
            set time   = duration
            set order  = GetUnitCurrentOrder(source)
            set dummy  = GetRecycledMissile(x, y, 0., angle)
            set unitFx = AddSpecialEffectTarget(DURING_PORT_EFFECT, source, DURING_PORT_ATTACH_POINT)
            call SetUnitScale(dummy, GetPortalModelScale(source), 0., 0.)
            call createBarUnit(GetUnitX(source), GetUnitY(source), z, duration)
            call createPortalRegion(x, y)
            //*  Pointers to instance.
            set table[GetHandleId(source)] = this
            set table[GetHandleId(trig)]   = this
            //*  Ambience.
            call SoundOnUnitStartPort(source)
            set audio = bj_lastPlayedSound 
            call TimerStart(clock, RMinBJ(duration, ACCURATICY), true, function thistype.onPeriodic)
            return this
        endmethod
        
        private static method init takes nothing returns nothing
            set table      = Table.create()
            set expression = Condition(function thistype.onEnter)
        endmethod
        implement Inits
    endstruct
//**    
//*  API:
//*  ====
    function GetUnitTownPortal takes unit source returns TownPortal
        return table[GetHandleId(source)]
    endfunction
    
    function CancelUnitTownPortalCast takes unit source returns boolean
        if table.has(GetHandleId(source)) then
            return TownPortal(table[GetHandleId(source)]).cancel()
        endif
        return false
    endfunction
    
    function DestroyTownPortal takes unit source returns nothing
        if table.has(GetHandleId(source)) then
            call TownPortal(table[GetHandleId(source)]).destroy()
        endif
    endfunction
    
    function CreateTownPortal takes unit source, real x, real y, real z, real angle, real duration returns nothing
        call DestroyTownPortal(source)
        call TownPortal.create(source, x, y, z, angle, duration)
    endfunction
    
endlibrary

Keywords:
Diablo, Teleport, Teleportation, Townportal, Portal, System, RPG, Blink, Diablo3, Waypoint
Contents

TownPortal (Map)

Reviews
Townportal System v1.0.4 | Reviewed by Maker | 1st Oct 2013 APPROVED A nice and useful system [tr] In RegisterPortalUnit you have same actions in then and else. Take those actions out of the if/then/else if DO_PAN...

Moderator

M

Moderator


Townportal System v1.0.4 | Reviewed by Maker | 1st Oct 2013
APPROVED


126248-albums6177-picture66521.png


  • A nice and useful system
126248-albums6177-picture66523.png


  • In RegisterPortalUnit you have same actions in then and else.
    Take those actions out of the if/then/else
  • if DO_PAN -> static if DO_PAN
  • Add a function where you can set hometown per player or even per unit
  • SetUnitPosition could be good to use so units won't end up
    onto of each other when teleporting to town
  • Maybe a special effect would look good when teleporting,
    the unit just disappears currently
[tr]
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
you should use UnitIndexer or hashtable for this to make it MUI coz atm

Limits:
- 1.Each player can only have one unit registered to the system

This is my fault, i've added the USE_ABILITY option at the end, because i thought it my be usefull.

When not using EVENT_PLAYER_END_CINEMATIC, it ofc makes sense to make this MUI.

I will fix that.

error messages also should use SimError library...

In case a resources is used in a map, its not helpfull to spam error message to any user, but the map or resource maker.
I think debug call fits very well.

Otherwise i just delete those lines.

EDIT: Updated to v1.0.3

reduced code by 3 Lines and updated documentation.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
updated
updated: Did everything Maker suggested, except:

Add a function where you can set hometown per player or even per unit

Good idea, but at the moment i see more cons than pros adding such a function.
For example: if you TriggerRegisterEnterRegion dynamically you also have to deregister leakless.(removing the region, destroying (local) triggers??).

I'll reconsider this at a subsequent date.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
Rewrote the whole code. Now requires UnitIndexer.
Its now shorter, better organized and slightly faster.
Also it doesn't required TimerTools anymore (That's a pro).
It uses Table instead of hashtabel hash = InitHashtable()

I considered adding such a function, but in the end I didn't(, yet).
Add a function where you can set hometown per player or even per unit
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I'm planning a huge update on my TownPortaSystem for the next few days :). The final update is still in progress. At the moment it looks like this:

For Esc: Each Player has it's own Portal region, which can be changed ingame plus a Portal Dummy unit (it really does open a portal :). Any allied player can use other players portal and will then port to the x/y coordinate of that portal (Note not your own, but the one you've just enetered). Portals only close if the owning player uses it or opens a new one.

For Ability ... I'll consider doing somthing similar.

I've also discovered a bug. When using a portal with a meele unit, It will stop if enemies come near --> order move.
Also range unit will use Autoattack since it is not detectable. I guess I will add Chargo Hold on the next update, but that means you can't attack out of porting, you have to move first and wait 0.03125 seconds.

Edit: ESC variant is finished and works, I'll upload it as soon as the ability version is written aswell.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
Here you go:
Inside the demo map the unit is registered via custom script. Just in case you are only familiar with GUI.
JASS:
/************************************************************************************
*
*    Functions
*
*       function TownPortalRegister takes unit u, real x, real y, real angle, real area returns nothing
*           -   Register an unit to the system with the following parameters:
*           -   x/y     --> coordinates of the portal
*           -   angle   --> Which angle should the portal have
*           -   area    --> Within this radius the unit cannot use teleport. Center is x/y
*
*       function TownPortalShiftInfo takes unit u, real x, real y, real angle, real area returns nothing
*           -   Shift the information of an already registered unit, uses the same parameters as TownPortalRegister
*
*       function TownPortalUnregister takes unit u returns nothing
*           -   Unregister an already registered unit and clears all corresponding members
*
************************************************************************************
*
*   struct TownPortal extends array
*
*       Description
*       -----------------------
*
*           NA
*
*       Methods
*       -----------------------
*
*           static method register takes unit u, real x, real y, real angle, real area returns nothing  
*               -   Register an unit to the system with the following parameters:
*               -   x/y     --> coordinates of the portal
*               -   angle   --> Which angle should the portal have
*               -   area    --> Within this range the unit cannot use teleport. Center is x/y  
*
*           static method shiftInfo takes unit u, real x, real y, real angle, real area returns nothing
*               -   Shift the information of an already registered unit, uses the same parameters as TownPortalRegister
*
*           static method unregister takes unit u returns nothing
*               -   Unregister an already registered unit and clears all corresponding members
*
************************************************************************************/

In case you are using the old system with one save point for the map. There is no function to change it. I've updated the whole resource and it works now very different. Sry
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Note: Don't use the ESC library, yet. Currently it leaks two handles (trigger and region) in case you re-assign the location or change the porting unit for a player. I'll fix it as soon as possible.

The Ability library is totally fine.

Edit: I fixed it, but I'll run some debug tests before uploading the new version, of the ESC library.

Update:
  • minor changes in the code.
  • made Alloc, ErrorMessage, SpellEffectEvent optional
  • far better documentation for the Ability version

(Also I think the rating should be reconsidered, because now it is a really awesome resource. :p)
 
Last edited:

Kazeon

Hosted Project: EC
Level 34
Joined
Oct 12, 2011
Messages
3,449
//* Will fail on first play... I'm waiting for a good Sound library.
For alternative solution (while waiting for that "good sound library"), you can preload the sound anytime after the map initialization (loading screen). As example use 0 second timer to call the preload function. Just like what I do in my spell:
JASS:
        // Needed so that the sound effect will appear since the first cast
        static method preloadSound takes nothing returns nothing
            
            local sound s = CreateSound(SOUND_PATH, false, false, true, 12700, 12700, "")
            
            call StartSound(s)
            call StopSound(s, true, false)
            call ReleaseTimer(GetExpiredTimer())
            set s = null
            
        endmethod
        
        static method init takes nothing returns nothing
            
            ...
            call TimerStart(NewTimer(), 0, false,  function thistype.preloadSound)
            
            ...
            
        endmethod
 

Kazeon

Hosted Project: EC
Level 34
Joined
Oct 12, 2011
Messages
3,449
I will update it.

Edit: It seems not to work for me. Maybe because those sounds are 3D sounds.
sound handles are strange ....

That's very weird. I did it all the time and always work, either 3D or not. You said it yourself that the sound doesn't appear only for the first time. The function I wrote before actually just plays the sound and stop it right away (to play the sound for the first time).

Or perhaps try to use exactly the same sound properties to preload the sound.
JASS:
        static method preloadSound takes string path returns nothing
            
            local sound s = CreateSound(path, false, true, true, 10, 10, "CombatSoundsEAX")
            
            call SetSoundChannel(s, 5)
            call SetSoundVolume(s, 127)
            call SetSoundDistances(s, 600, 10000)
            call SetSoundDistanceCutoff(s, 3000)
            call SetSoundConeAngles(s, 0, 0, 127)
            call SetSoundConeOrientation(s, 0, 0, 0)
            call AttachSoundToUnit(s, source)
            call StartSound(s)
            call StopSound(s, true, false)
            call ReleaseTimer(GetExpiredTimer())
            set s = null
            
        endmethod

I just can't accept this, it's way too weird and often become so frustrating to solve.
Once I had a very strange sound problem. I use a local sound variable and the sound wouldn't play at all. Then I changed it a global sound variable, then it worked fine. I swear I wasn't drunk. I mean, where's the sense of logic there? And I spent hours to fix that silly error. We really need to solve these problems before somebody hangs himself out of frustration. I almost became the first one that time.
 
I just can't accept this, it's way too weird and often become so frustrating to solve.
Once I had a very strange sound problem. I use a local sound variable and the sound wouldn't play at all. Then I changed it a global sound variable, then it worked fine. I swear I wasn't drunk. I mean, where's the sense of logic there? And I spent hours to fix that silly error. We really need to solve these problems before somebody hangs himself out of frustration. I almost became the first one that time.

I've had that happen before with sounds applied via triggers. I solved it by checking the Options box for "Stop when out of range" in the sound editor. It seems numerous sound files do not have that set by default.
 
Top