• 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.

TerrainInfection v2.1

Info

TerrainInfection will start at given coordinates and infect nearby tiles. (change terrain type)
TerrainInfection also allows to undo the infection.

Furthermore it provides events for(un-)infection.

Uses


-
WorldBounds (Nestharus)

-
Table (Bribe)

-
List (Nestharus)

-
TileDefinition (IcemanBo)

Code
JASS:
library TerrainInfection/* v2.1    By IcemanBo

    TerrainInfection will start at given coordinates and infect nearby tiles. (change terrain type)
    TerrainInfection also allows to undo the infection.
    
    Furthermore it provides events for(un-)infection.
    
       */ requires /*

        */ TileDefinition /* hiveworkshop.com/forums/submissions-414/snippet-tiledefinition-259347/
        */ WorldBounds    /* github.com/nestharus/JASS/blob/master/jass/Systems/WorldBounds/script.j
        */ List           /* as Nestharus removed it check out the demo
        */ Table          /* hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
    
     API
   ¯¯¯¯¯¯
   
        function CreateTerrainInfection takes integer t, real x, real y, real chance, real interval returns TerrainInfection
            t           = infected terrain type
            x           = x of start
            y           = y of start
            chance      = chance to infect nearby tiles between 0 and 1
            interval    = interval to potentialy infect nearby tiles
        
         you also can set/read public members:
            
            real maxDistance = max distance to RootInfection
            real chance      = chance on infection between 0 and 1
            real maxX        = maxX of Infection
            real minX        = minX of Infection
            real maxY        = maxY of Infection
            real minY        = minY of Infection
            region bound     = bound region for infection
            boolean diagonal = should Infection act diagonal
            boolean enabled  = is Infection currently enabled
            
        you can read readonly members:
        
            real x                      =  x of infectionStart
            real y                      =  y of infectionStart
            integer size                =  amount of infected tiles
            integer terrainTypeInfected =  terrain type of infection
            
            
    methods (TerrainInfection)
   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
        destroy() returns nothing
        
        undoInfection() returns nothing
            Will un-infect all tiles.
            
        undoInfection_last() returns boolean
            Will un-infect the last tile.
            Returns false if there is no tile anymore.
            
            
    static methods (TerrainInfection)
   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
        enableResistor(integer Infector, integer Resistor, boolean b returns nothing
            Takes terrain types as paramater.
            Resistor will be immune against Infector. (true/false)
            
        enableInfections(boolean b) returns nothing
            Enables/disables all current running Infections.
            
        isTileInfected takes real x, real y returns boolean
            
    
    Events
   ¯¯¯¯¯¯¯¯
   You can use TriggerRegisterVariableEvent to catch following events:
   
        s__TerrainInfection_infection_state == 1 -> onInfection
        
        s__TerrainInfection_infection_state == 2 -> Infection has completly finished.
        
        s__TerrainInfection_infection_state == 3 -> onUnInfection
        
        s__TerrainInfection_infection_state == 4 -> Infection has been completly removed.
        
    onEvents you can use TerrainInfection_currrent as current instance.
    onEvents (1 and 3) you can use TerrainInfection_currentTileId to refer to operated tile.
    
    Look into "Event Example" for more info.
        
    Credits:
   ¯¯¯¯¯¯¯¯¯
        List (Nestharus)
        Table (Bribe) 
        WorldBounds (Nestharus)

*/
    globals
        private hashtable hash = InitHashtable()
        private timer tim = CreateTimer()
        private constant real TIMEOUT              = .031250000
        private constant real TILE_SIDE_LENGTH     = 128.
        private constant real TILE_DIAGONAL_LENGTH = TILE_SIDE_LENGTH*SquareRoot(2.)
    endglobals
    
    private struct InfectionData extends array
        implement List
        
        integer tileId
        integer originTerrainType
    endstruct
    
    struct TerrainInfection
        private InfectionData list
        
        private real timeCountMax               // Interval of Infecion.
        private real timeCountCurrent           // Time left until next Infection.
    
        readonly real x                         // x of RootInfection
        readonly real y                         // y of RootInfection
        readonly integer size                   // size == amount of infected tiles
        readonly integer terrainTypeInfected    // new (infected) terrain type.
        
        // public members
        real maxDistance = -1
        real chance = 1
        real maxX = WorldBounds.maxX
        real minX = WorldBounds.minX
        real maxY = WorldBounds.maxY
        real minY = WorldBounds.minY
        region bound = null
        boolean diagonal = true
        boolean enabled = true
        
        // for list
        private thistype prev
        private thistype next
        private static thistype first = 0
        
        // for events
        static real infection_state = 0
        static thistype current = 0
        static integer currentTileId = 0
        
        // to save if a tile is infected
        private static key k
        private static Table table = k
        
        /*  
        *   These methods check if a point matches all conditions,
        *   so it can be infected successfully.
        */
        
        private method coordinateCheck takes real x, real y returns boolean
            return (bound == null or IsPointInRegion(bound, x, y)) and(x <= maxX and x >= minX and y <= maxY and y >= minY) 
        endmethod
        
        private method distanceCheck takes real x, real y returns boolean
             return (maxDistance < 0 or SquareRoot((x-this.x)*(x-this.x) + (y-this.y)*(y-this.y)) <= maxDistance)
        endmethod
        
        private method terrainCheck takes real x, real y returns boolean
            return (GetTerrainType(x, y) != terrainTypeInfected) and  not(LoadBoolean(hash, terrainTypeInfected, GetTerrainType(x, y)))
        endmethod
        
        private method isPointValid takes real x, real y returns boolean
            return coordinateCheck(x,y) and distanceCheck(x,y) and terrainCheck(x,y)
        endmethod
        
        /*
        *     This method will make a circular check around given
        *     coordinates to find a terrain tile which is not
        *     infected yet. It will return boolean, if there exists
        *     a (new/old) tile around for potential infection.
        */
        
        private method checkNearbyTiles takes real x, real y returns boolean
            local real angle = 0
            local real angleChange
            local real xNew
            local real yNew
            local real offset
            local boolean diagonalCheck = true
            local InfectionData element
            
            if (this.diagonal) then
                set angleChange = bj_PI/4
            else
                set angleChange = bj_PI/2
            endif
            
            loop
                if (this.diagonal) then
                    // For 45/135/225/315 degrees the centre
                    // of the tile has an other distance, so
                    // we check if it's a digonal.
                    set diagonalCheck = not(diagonalCheck)
                    if (diagonalCheck) then
                        set offset = TILE_DIAGONAL_LENGTH
                    else
                        set offset = TILE_SIDE_LENGTH
                    endif
                else
                    set offset = TILE_SIDE_LENGTH
                endif
                
                set xNew = x + offset * Cos(angle)
                set yNew = y + offset * Sin(angle)
                
                if (this.isPointValid(xNew, yNew)) then
                    
                    if (GetRandomReal(0, 1) <= this.chance) then
                        
                        set element = this.list.enqueue()
                        set element.tileId = GetTileId(xNew,yNew)
                        set element.originTerrainType = GetTerrainType(xNew, yNew)
                        
                        call SetTerrainType(xNew, yNew, this.terrainTypeInfected, -1, 1, 1)
                        set thistype.table.boolean[element.tileId] = true
                        set this.size = (this.size + 1)
                        
                        // Fire onInfection event.
                        set thistype.current = this
                        set thistype.currentTileId = element.tileId
                        set thistype.infection_state = 1
                        set thistype.infection_state = 0
                        set thistype.current = 0
                        set thistype.currentTileId = 0
                    endif
                    return true
                endif
                
                set angle = angle + angleChange
                exitwhen (angle >= 2*bj_PI) // Full circular check was done.
            endloop
            return false
        endmethod
        
        // callback is called periodicly
    
        private static method callback takes nothing returns nothing
            local thistype this = thistype.first
            local InfectionData element
            local integer i 
            local boolean b     // Will check if infection is completly finished.
            
            // loop through all instances (root infections)
            loop
                exitwhen (this == 0)
                
                if (this.enabled) then
                
                    set this.timeCountCurrent = this.timeCountCurrent - TIMEOUT
                        if (this.timeCountCurrent <= 0) then
                            set this.timeCountCurrent = this.timeCountMax
                            set element = this.list.first
                            set i = this.size
                            set b = false
                            
                            // We use pre-loop defined integer to define
                            // the cancel condition, because the list itself
                            // is dynamic, not static. (size can be changed during loop)
                            
                            //  loop through all infected tiles of an instance
                            loop
                                exitwhen (i < 1)
                                if this.checkNearbyTiles(GetTileCenterXById(element.tileId), GetTileCenterYById(element.tileId)) then
                                    set b = true
                                endif
                                
                                set element = element.next
                                set i = (i - 1)
                            endloop
                            
                            // if there is no potential tile left for infection
                            if not(b) then
                                
                                // Fire onFinish event.
                                set thistype.current = this
                                set thistype.infection_state = 2
                                set thistype.infection_state = 0
                                set thistype.current = 0
                            endif
                        endif
                        
                    endif
                set this = this.next
            endloop
        endmethod
        
        static method create takes integer t, real x, real y, real chance, real interval returns thistype
            local thistype this = thistype.allocate()
            local InfectionData element
            
            set this.list = InfectionData.create()
            set element = this.list.enqueue()
            set element.tileId = GetTileId(x,y)
            set element.originTerrainType = GetTerrainType(x, y)
            
            set this.x = x
            set this.y = y
            set this.terrainTypeInfected = t
            set this.chance = chance
            set this. size = 1
            
            if (interval < TIMEOUT) then
                set this.timeCountMax = TIMEOUT
            else
                set this.timeCountMax = interval
            endif
            set this.timeCountCurrent = this.timeCountMax
            call SetTerrainType(x, y, t, -1, 1, 1)
            set thistype.table.boolean[element.tileId] = true
            
            // Fire onInfection event.
            set thistype.current = this
            set thistype.currentTileId = element.tileId
            set thistype.infection_state = 1
            set thistype.infection_state = 0
            set thistype.current = 0
            set thistype.currentTileId = 0
            
            if (thistype.first == 0) then
                call TimerStart(tim, TIMEOUT, true, function thistype.callback)
            endif
            set this.next = thistype.first
            set thistype.first.prev = this
            set thistype.first = this
            set this.prev = 0
            return this
        endmethod
        
        method destroy takes nothing returns nothing
            set this.bound = null
            call this.deallocate()
            call this.list.destroy()
            
            if (this == thistype.first) then
                set thistype.first = this.next
            endif
            set this.next.prev = this.prev
            set this.prev.next = this.next
            
            if (thistype.first == 0) then
                call PauseTimer(tim)
            endif

        endmethod
        
        method undoInfection_last takes nothing returns boolean
            local InfectionData element = this.list.last
            local integer tileId = element.tileId
            
            if (element == 0) then
                // Fire onRemoved event.
                set thistype.current = this
                set thistype.infection_state = 4
                set thistype.infection_state = 0
                set thistype.current = 0
                return false
            endif
            
            call SetTerrainType(GetTileCenterXById(tileId), GetTileCenterYById(tileId), element.originTerrainType, -1, 1, 1)
                
            set this.size = this.size - 1
            call thistype.table.remove(tileId)
            call element.remove()
                
            // Fire onUnInfection event.
            set thistype.current = this
            set thistype.currentTileId = tileId
            set thistype.infection_state = 3
            set thistype.infection_state = 0
            set thistype.current = 0
            set thistype.currentTileId = 0
            return true
        endmethod
        
        // loop until all infected tiles are removed
        method undoInfection takes nothing returns nothing
            loop
                exitwhen not (this.undoInfection_last())
            endloop
        endmethod
        
        static method isTileInfected takes real x, real y returns boolean
            return thistype.table.has(GetTileId(x,y))
        endmethod
        
        static method enableResistor takes integer infector, integer resistor, boolean b returns nothing
            call SaveBoolean(hash, infector, resistor, b)
        endmethod
        
        static method enableInfections takes boolean b returns nothing
            if (b) then
                call TimerStart(tim, TIMEOUT, true, function thistype.callback)
            else
                call PauseTimer(tim)
            endif
        endmethod
    endstruct
    
    function CreateTerrainInfection takes integer t, real x, real y, real chance, real interval returns TerrainInfection
        return TerrainInfection.create(t, x, y, chance, interval)
    endfunction
    
endlibrary

Changelog

v2.1
- now uses List instead of Vector
- minor changes​
v2.0a
- removed not needed method
- changed documentation a bit​
v2.0
- undoInfection is now possible
- count infected tiles is possible
- check if a tile is infected is possible​
v1.0
- release​

Keywords:
Terrain, Infection, Creep, System, Tile, Effect, IcemanBo, null
Contents

TerrainInfection (Map)

Reviews
TerrainInfection v2.1 | Reviewed by BPower | 07.07.2015 Concept[/COLOR]] Create "terrain infections" starting from a given point x/y. As time goes by the infection eventually spreads to nearby tiles. Maximum size and infection speed can be...

Moderator

M

Moderator


TerrainInfection v2.1 | Reviewed by BPower | 07.07.2015

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

Concept[/COLOR]]
126248-albums6177-picture66521.png
Create "terrain infections" starting from a given point x/y.
As time goes by the infection eventually spreads to nearby tiles.
Maximum size and infection speed can be customized.
Make use of infected tiles via isTileInfected(x, y) function.

Definitly a useful, unique system.
Due to it's innovativeness I will rate the concept with 5/5
Code[/COLOR]]
126248-albums6177-picture66521.png
  • The system is MUI, leakless and working.
126248-albums6177-picture66523.png
  • Nothing I could wish more.
Demo Map[/COLOR]]
126248-albums6177-picture66523.png
  • The demo is done very well.
Rating[/COLOR]]
CONCEPTCODEDEMORATINGSTATUS
5/5
5/5
4/5
5/5
APPROVED
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
you mention bunch of methods, but the API never states the name of the struct.

You also potentially generate TriggerEvaluation with your layout of TerrainInfection struct.

This is because method create is way above method callback, and you still start timer in create and try to call callback from it.

Also callback should be private.


Also maybe you could make it possible to have different timeouts for each infection instance, so you can have one spreading faster than another, but this is just suggestion.
 
you mention bunch of methods, but the API never states the name of the struct.
Done.

You also potentially generate TriggerEvaluation with your layout of TerrainInfection struct.

This is because method create is way above method callback, and you still start timer in create and try to call callback from it.
Thank you, I did not know that inside structs. Changed.

Also callback should be private.
Done.

Also maybe you could make it possible to have different timeouts for each infection instance, so you can have one spreading faster than another, but this is just suggestion.
It actually is already. (interval) Thank you! :csmile:
 
Level 7
Joined
Oct 11, 2008
Messages
304
OMG :O You just did the Creep Tumor from Starcraft :O I love it, definitely.

A little bit of adaptation and it become exactly how it is in Starcraft, based on range.

Nice work :)

Edit: If someone didn't know what is Creep Tumor from Starcraft, take a look at the image.

creep01.jpg
 
Ah yes, you are right. ^^ Thank you.

You could use this method, to create a limitation for a new builded structure, so it would only infect until a certain area:

JASS:
/**      method setRegion takes region returns nothing
**          Infection will be limited to this region.
Of course the user would have to define the region by himself, for each building type individually.
 
IsPointInfectedXY takes real x ,real y returns boolean
I would need to take track of each tile that was ever infected. :/
At the moment an instance gets destroyed, once there is no infectable tile around it, because it's not needed anymore.
Would you have an other idea?

But anyway I'm not very happy at the moment with not tracking them at all, because user has no controle of infected tiles and it's behaviour, once they are destroyed. So you can't post-change the behaviour, but only after creation. :S

Why do you have all the setter and getter functions?
Do you mean to change some readonly members to accessible members, instead of the setters?
What getters do you mean?
 
JASS:
method setChance takes real chance returns nothing
            set infectionChance = chance
        endmethod
        
        method setTerrainType takes integer t returns nothing
            set terrainType = t
        endmethod
        
        method setInterval takes real interv returns nothing
            set interval = interv
        endmethod
        
        method enable takes boolean b returns nothing
            set enabled = b
        endmethod
        
        method setMaxX takes real r returns nothing
            set maxX = r
        endmethod
        
        method setMinX takes real r returns nothing
            set minX = r
        endmethod

        method setMaxY takes real r returns nothing
            set maxY = r
        endmethod
These are what I'm referring to.
 
Preface: I don't understand JASS or vJASS

This looks really interesting. How would I use it to make a building spread sand in a 600 aoe around it, and then stop spreading sand once it dies/uproots?
Sorry, I completly forgot about your post and this resource. Updated and added this distance limitation. In the demo, there is an example.

I still don't infinitly keep tracking of all tiles and lose controle of the behaviour once it gets destroyed, it's kind of discouraging...

Well, but it still may be used in some scenarios. :D
 

Kazeon

Hosted Project: EC
Level 34
Joined
Oct 12, 2011
Messages
3,449
  • SquareRoot((xNew-rootX)*(xNew-rootX) + (yNew-rootY)*(yNew-rootY)) <= rootDistance
    =>
    (xNew-rootX)*(xNew-rootX) + (yNew-rootY)*(yNew-rootY) <= rootDistance*rootDistance
  • You don't need this:
    JASS:
            private constant integer INDEX_START = -1
            private integer maxIndex = INDEX_START
  • You can declare (2*bj_PI) once as "TAU". And another crap calculation like PI/2, rename it to whatever you want. :p
  • You can shorten your infect method to:
    JASS:
            private method infectNearbyPoint takes real x, real y returns boolean
                local real angle = 0
                local real xNew
                local real yNew
                @local integer counter = 0@
                local real offset
                local boolean diagonal = true
                
                loop
                    if (diagonalInfection) then
                        // For 45/135/225/315 degrees the centre
                        // of the tile has an other distance, so
                        // we check if it's a digonal.
                        set diagonal = not(diagonal)
                        if (diagonal) then
                            set offset = TILE_DIAGONAL_LENGTH
                        else
                            set offset = TILE_SIDE_LENGTH
                        endif
                        set angle = angle + bj_PI/4
                    else
                        set offset = TILE_SIDE_LENGTH
                        set angle = angle + bj_PI/2
                    endif
                    
                    set xNew = x + offset * Cos(angle)
                    set yNew = y + offset * Sin(angle)
                    
                    if (isPointValid(xNew, yNew)) then
                    
                        //  A new tile gets infected.
                        return copyData(thistype.create(terrainType, xNew, yNew, infectionChance, interval, infectionEffect))
                    endif
                    
                    exitwhen angle > (2*bj_PI) // Full circular check was done.
                endloop
                return false
            endmethod
    And it doesn't need to take another x and y, it's a non static:
    JASS:
    private method infectNearbyPoint takes @real x, real y@ returns boolean
    EDIT: and you don't need local variable "counter".
  • JASS:
                if (interv < TIMEOUT) then
                    set interval = TIMEOUT
                else
                    set interval = interv
                endif
    Ermmm......
  • You forgot to remove not null infectionRegion on destroy method.
You made this very effective. Max infection range and specific infection region was a very diligent feature, I like it.
 
Last edited:

Deleted member 219079

D

Deleted member 219079

JASS:
/*     Requires:
**    ----------
**
**       - nothing*/
Hehe :D I'm also working on vanilla JASS spell, on which I won't use any external scripts.

I think I have use for this, waiting for status change.. +rep
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Wouldn't it be better to use a second struct with a linked list to store
all terrain types and terrain ids? Something like
JASS:
private struct TerrainList extends array
    implement List// Whatever list module you prefer, or hand-written ...
    integer terrainId
    integer terrainType
endstruct

Vector<T> from the submission section does currently not even compile.
 
Level 11
Joined
Jul 4, 2016
Messages
628
Since I'm not a jasser or vjasser, I am wondering how would I go about destroying a specific tile after checking that the tile is indeed infected?
 
Level 42
Joined
Feb 27, 2007
Messages
5,330
Well, the TerrainInfection.isTileInfected method allows you to check if a certain tile is infected or not, but there's actually nothing you can do with that information. @IcemanBo has not provided a method to get the infection that 'owns' that tile or a method to clear the infection from a particular tile... and has made the Table objects used to store this information private (can't be accessed outside the library). It is possible to add such functionality to his system (as I see it) but rather than edit it and post it for you I'd simply ask that he do it himself. Bo is generally pretty good about updating his resources!

JASS:
static method isTileInfected takes real x, real y returns boolean
     return thistype.table.has(GetTileId(x,y))
endmethod
 
how would I go about destroying a specific tile after checking that the tile is indeed infected?
Yes, seems currently like Pyrogasm correctly said:
provided a method to get the infection that 'owns' that tile or a method to clear the infection from a particular tile... and has made the Table objects used to store this information private

.. iirc the plan was to provide some easy way backwards from infection, hence only the list with allowing to undo the last infection, or just all. 1 tile can be multiply infected, so allowing to uninfect 1 tile only by coordinates one still would need know, or to specify the infection that started that now has to be undone, and this would require new listing for each tile for all infections.
Long story short, we can make some sloppy approach for removing one specific tile from a infection list by coordinates:
JASS:
        method undoInfection_XY takes real x, real y returns boolean
            local InfectionData element = this.list.first
            local integer id = GetTileId(x, y)

            loop
                exitwhen element == 0
                if (element.tileId == id) then
                    call SetTerrainType(GetTileCenterXById(id), GetTileCenterYById(id), element.originTerrainType, -1, 1, 1)
                    set this.size = this.size - 1
                    call thistype.table.remove(id)
                    call element.remove()

                    set thistype.current = this
                    set thistype.currentTileId = tileId
                    set thistype.infection_state = 3
                    set thistype.infection_state = 0
                    set thistype.current = 0
                    set thistype.currentTileId = 0
                    return true
                endif

                set element = element.next
            endloop
            return false
        endmethod
^.. add this method to the main code API, and then call it like call this.undoInfection_XY(100, 100) (for coordinates x/y = 100 for example)
But as said, the list, here the this is required. You can make the list itself public, too, but I believe this is more dangerous for you in case you don't know how the list logics internally works.

I could do some update, allowing to maintain all infections for a tile in a list, or vector, providing it to the user. But for this, I would wait until new wc3 patch, as I currently don't use some external program to code, and editor is atm not too cool to code with.

In case you lean more on the "GUI" demo, there is currently not a too nice way to achive the same thing without custom script, I'm afraid. But if you need GUI, I probably can switch the system code for your map to only allow single infractions for 1 tile, and then you could maybe just define 1 tile in GUI, and then run some un-infection trigger, if you want.

===

Actually after more reading the system... the code seems not perfectly consistent with single, or multiple infections, and I maybe should by default just allow only 1 infection, and just map the infection directly to a tile. Then there can't also a problem occur with losing "original" terrain type. I will maybe do this, when editor is updated -- but still maybe something from above helps.
 
So I've been attempting to use this system and while it works as advertised, there are a few issues I'm running into. For one, with high enough numbers, new TerrainInfection instances just seem to stop working. I don't have a specific number, but in a small map like Turtle Rock, two players building a bunch of food-providing structures that also spread the Infection will quickly run into that problem with one 1 expansion each. I'm not discounting user error, but I'd mention it here just in case.

Additionally, undoing infection will cause tiles that are being infected by two or more sources will also get removed. Ideally, overlapping Infections should only clear once all contributors are destroyed. This would allow me to stop the spread when it completes so that it stops checking nearby tiles when it doesn't have to.
 
Top