• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] TilesInRange

This is not a system to provide data structure of tiles in range, but to enumerate through them.

Code
JASS:
library TilesInRange  /* v1.1 -- hiveworkshop.com/threads/tilesinrange.277289/


*/ requires /*

        */ WorldBounds    /* github.com/nestharus/JASS/blob/master/jass/Systems/WorldBounds/script.j
        */ TileDefinition /* hiveworkshop.com/forums/jass-resources-412/snippet-tiledefinition-259347/

   
    Information
    ¯¯¯¯¯¯¯¯¯¯¯
   
            Allows to enumerate through tiles in range.
            There is currently no code for resetting OP limit,
            so I don't know if for example too much tiles in a huge map can be added.
       
       
       
    Applied method
    ¯¯¯¯¯¯¯¯¯¯¯¯¯¯
            Rules for tile inclusion:
 
                1. A tile gets included when it's TileMinimum is included.
                2. A tile gets NOT included when only it's TileMaximum is included.
                    (because it's maximum is also the minimum of the very next tile, and so comes Rule 1)
                3. If any cooridnate from the tile is in range, the tile will be included. If the center is in range does not matter.
                    (exception is Rule 2, of course)
                4. Tiles outside the WorldBounds will be excluded.
   
            Example:
   
                Tile1: minX=-64   maxX= 64   minY =-64   maxY=64
                Tile2: minX= 64   maxX=192   minY =-64   maxY=64
       
            Now we add Tile(64/60) to the list, but which Tile will be included?
                Tile1 will not be included, because 64 is it's X maximum.
                Tile2 will be included, because 64 is it's X minimum.
           
           
            These rules are based on how TileDefinition works.
           
            In case you want a stricter filtering, for example only include tiles which's TileCenter is in range
            you have the possibility to do so with custom filters. See API.
 
*/
//! novjass
 //=================== --------- API --------- ====================


    function GetTilesInRange takes real x, real y, real range, boolexpr filter returns TileData
   
        // returns the first element to start your loop. (see later)
        // if the returned TileData equals "0" , the list is empty.
   
        // If a list already exist, the system will automaticaly clear and destroy it on this function call.
        // This means you can only have one list at a time, but also don't have
        // to care about desotroying it yourself/leaks.
   
 
    struct TileData
 
        integer id
        real range
 
//      You are able to enumerate though the List like this:
   
            local TileData this = GetTilesInRange(x, y, 100, null) // null filter
            loop
                exitwhen(this == 0)
                    // this.id      // TileId of this
                    // this.range   // Range of the tile to x/y
                set this = this.next
            endloop
       
        static thistype first    // you can start loops with it
        static integer  Size     // Size of the list.
        static integer  TileId   // The current Tile Id which you refer to inside the filter.
        static integer  Range    // The current Tile Range which you refer to inside the filter.
 
//      Example:
   
            function MyFilter takes nothing returns boolean
                call BJDebugMsg("Current Tile id: " + I2S(TileData.TileId) + " | Range: " + R2S(TileData.Range))
            endfunction


 //=================== ---------------------- ====================
//! endnovjass

globals
    private trigger handler = CreateTrigger()
    private triggercondition tc = null
endglobals

// only need a very basic structure, which is fast
private module List
 
    readonly thistype next
    readonly static thistype first = 0
    readonly static integer Size = 0
 
    method add takes nothing returns nothing
        if(thistype.Size > 8190) then
            debug call BJDebugMsg("TilesInRange - Module List: Max amount of elements has been reached \"8191\".")
            return
        endif
        set this.next = thistype.first
        set thistype.first = this
        set thistype.Size = thistype.Size + 1
    endmethod
 
    static method destroyList takes nothing returns nothing
        local thistype this = thistype.first
        loop
            exitwhen (this == 0)
            call this.destroy()
            set this = this.next
        endloop
        set thistype.first = 0
        set thistype.Size = 0
        if tc != null then
            call TriggerRemoveCondition(handler, tc)
            set tc = null
        endif
    endmethod
 
endmodule

struct TileData
    implement List
    real range
    integer id
 
    static integer TileId = 0
    static real Range = 0
 
    static method create takes nothing returns thistype
        local thistype this = thistype.allocate()
        call this.add()
        return this
    endmethod
endstruct

private function IsXInWorld takes real x returns boolean
    return x < WorldBounds.maxX and x > WorldBounds.minX
endfunction

private function IsYInWorld takes real y returns boolean
    return y < WorldBounds.maxY and y > WorldBounds.minY
endfunction

private function IsXYInWorld takes real x, real y returns boolean
    return x < WorldBounds.maxX and x > WorldBounds.minX and y < WorldBounds.maxY and y > WorldBounds.minY
endfunction

private function AddTile takes real x, real y, real r returns TileData
    local TileData this
    set TileData.TileId = GetTileId(x, y)
    set TileData.Range = r
    if IsXYInWorld(x, y) and TriggerEvaluate(handler)then
        set this = TileData.create()
        set this.id = TileData.TileId
        set this.range = TileData.Range
        return this
    endif
    return 0
endfunction

function GetTilesInRange takes real x, real y, real range, boolexpr filter  returns TileData
 
    local real tempX
    local real tempY
    local real maxX
    local real maxY
    local real yRange
    local real rangeSquare
    local real currentRange
    local real currentRangeTwo
    local TileData this
 
    if range < 0 then
        debug call BJDebugMsg("LIBRARY TilesInRange: Invalid input \"range\". Must be positive.")
        return 0
    endif
    if  not IsXYInWorld(x, y) then
        debug call BJDebugMsg("LIBRARY TilesInRange: Invalid input \"x\" or \"y\". Must be in world")
        return 0
    endif
 
    // So user never has to care about destroying lists
    if (TileData.first != 0) then
        call TileData.destroyList()
    endif
 
    if filter != null then
        set tc = TriggerAddCondition(handler, filter)
    endif
 
//     We will operate though tiles in range in 3 steps.
//     ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
 
//  Step 1
 
//      We directly add tile at start location.
//      We go up and downwards from it, and add all tiles in range.
   
   
    call AddTile(x, y, 0)
 
    // Add all tiles above start tile
    set maxY = GetTileMax(y + range)
    set tempY = y + 128
    set currentRange = GetTileMax(y) - y
    loop
        exitwhen tempY >= maxY or not IsYInWorld(tempY)
        call AddTile(x, tempY, currentRange)
        set currentRange = currentRange + 128
        set tempY = tempY + 128
    endloop
   
    // Add all tiles under start tile
    set maxY = GetTileMin(y - range)
    set tempY =  y - 128
    set currentRange = y - GetTileMin(y)
    loop
        exitwhen tempY < maxY  or not IsYInWorld(tempY)
        call AddTile(x, tempY, currentRange)
        set currentRange = currentRange + 128
        set tempY = tempY - 128
    endloop
 
    set rangeSquare = range*range
 
//  Step 2
 
//      We go right and add all tiles in range.
//      We go up and downwards from all of them, and also add the tiles in range.
 
    set tempX = GetTileMax(x)
    set maxX = GetTileMax(x + range)
    set currentRange = GetTileMax(x) - x
    loop
        exitwhen tempX >= maxX  or not IsXInWorld(tempX)
        call AddTile(tempX, y, currentRange)
   
        set yRange = SquareRoot( rangeSquare - (tempX-x)*(tempX-x) )
   
        // Add all tiles above current tile
        set maxY = GetTileMax(y + yRange)
        set tempY = y + 128
        set currentRangeTwo = GetTileMax(y) - y
        loop
            exitwhen tempY >= maxY  or not IsXYInWorld(tempX, tempY)
            call AddTile(tempX, tempY, SquareRoot(currentRange*currentRange + currentRangeTwo*currentRangeTwo))
            set currentRangeTwo = currentRangeTwo + 128
            set tempY = tempY + 128
        endloop
   
        // Add all tiles under current tile
        set maxY = GetTileMin(y - yRange)
        set tempY =  y - 128
        set currentRangeTwo = y - GetTileMin(y)
        loop
            exitwhen tempY < maxY  or not IsXYInWorld(tempX, tempY)
            call AddTile(tempX, tempY, SquareRoot(currentRange*currentRange + currentRangeTwo*currentRangeTwo))
            set currentRangeTwo = currentRangeTwo + 128
            set tempY = tempY - 128
        endloop
   
        set currentRange = currentRange + 128
        set tempX = tempX + 128
    endloop
 
// Step 3
 
//     We go left and add all tiles in range.
//     We go up and downwards from all of them, and also add the tiles in range.
   
    set maxX = GetTileMin(x - range)
    set tempX = GetTileMin(x) - 128
    set currentRange = x - GetTileMin(x)
    loop
        exitwhen tempX < maxX  or not IsXInWorld(tempX)
        call AddTile(tempX, y, currentRange)
   
        set yRange = SquareRoot( rangeSquare - (tempX-x+128)*(tempX-x+128) )
   
        // Add all tiles above current tile
        set maxY = GetTileMax(y + yRange)
        set tempY = y + 128
        set currentRangeTwo = GetTileMax(y) - y
        loop
            exitwhen tempY >= maxY  or not IsXYInWorld(tempX, tempY)
            call AddTile(tempX, tempY, SquareRoot(currentRange*currentRange + currentRangeTwo*currentRangeTwo))
            set currentRangeTwo = currentRangeTwo + 128
            set tempY = tempY + 128
        endloop
   
        // Add all tiles under current tile
        set maxY = GetTileMin(y - yRange)
        set tempY =  y - 128
        set currentRangeTwo = y - GetTileMin(y)
        loop
            exitwhen tempY < maxY  or not IsXYInWorld(tempX, tempY)
            call AddTile(tempX, tempY, SquareRoot(currentRange*currentRange + currentRangeTwo*currentRangeTwo))
            set currentRangeTwo = currentRangeTwo + 128
            set tempY = tempY -128
        endloop
   
        set currentRange = currentRange + 128
        set tempX = tempX - 128
   
    endloop
    return TileData.first
endfunction

endlibrary

Addon

JASS:
library TilesInRangeUtils  /* v1.1 -- hiveworkshop.com/threads/tilesinrange.277289/


*/ requires /*

        */ TilesInRange    /* hiveworkshop.com/threads/tilesinrange.277289/
        */ QuickSort       /* http://www.hiveworkshop.com/threads/quicksort.274447/

     
    Information
    ¯¯¯¯¯¯¯¯¯¯¯
     
            Addon library to TilesInRange.

*/

//! novjass
 //=================== --------- API --------- ====================


    function GetTilesInRangeSorted takes real x, real y, real range, boolexpr filter, boolean ascending returns TileData
     
        // The Tile-List is sorted by range/distance from origin.
        // ascending defines if it's sorted ascending or descending.
        // For more usage information read TilesInRange library.
     
     
    function GetClosestTileInRange takes real x, real y, real range, boolexpr filter returns TileData
    function GetFarestTileInRange takes real x, real y, real range, boolexpr filter returns TileData
 
        // Returns only one TileData, so you direclty can use .id and .range from returned TileData.


 //=================== ---------------------- ====================
//! endnovjass

    function GetTilesInRangeSorted takes real x, real y, real range, boolexpr filter, boolean ascending returns TileData
        local TileData this = GetTilesInRange(x, y, range, filter)
     
        loop
            exitwhen this == 0
            set QuickSortValue[this] = this.range
            set QuickSortIndex[this] = this.id
            set this = this.next
        endloop
     
        set QuickSortAscending = ascending
        call QuickSort(1, TileData.Size)
     
        set this = TileData.first
        loop
            exitwhen this == 0
            set this.range = QuickSortValue[this]
            set this.id    = QuickSortIndex[this]
            set this = this.next
        endloop
     
        return TileData.first
    endfunction
 
    globals
        private boolexpr exprClosest
        private boolexpr exprFarest
        private boolexpr exprTemp
        private integer tempId
        private real tempRange
    endglobals
 
    private function filterFarest takes nothing returns boolean
        if TileData.Range > tempRange then
            set tempRange = TileData.Range
            set tempId    = TileData.TileId
        endif
        return true
    endfunction
 
    private function filterClosest takes nothing returns boolean
        if TileData.Range < tempRange then
            set tempRange = TileData.Range
            set tempId    = TileData.TileId
        endif
        return true
    endfunction
 
    private module Init
        private static method onInit takes nothing returns nothing
            set exprClosest = Filter(function filterClosest)
            set exprFarest  = Filter(function filterFarest)
        endmethod
    endmodule
    private struct S extends array
        implement Init
    endstruct
 
    function GetClosestTileInRange takes real x, real y, real range, boolexpr filter returns integer
        set tempId = 0
        set tempRange = 123456789
        set exprTemp = And(filter, exprClosest)
        call GetTilesInRange(x, y, range, exprTemp)
        set TileData.first.id = tempId
        set TileData.first.range = tempRange
        call DestroyBoolExpr(exprTemp)
        set exprTemp = null
        return TileData.first
    endfunction
 
    function GetFarestTileInRange takes real x, real y, real range, boolexpr filter returns integer
        set tempId = 0
        set tempRange = -1
        set exprTemp = And(filter, exprFarest)
        call GetTilesInRange(x, y, range, exprTemp)
        set TileData.first.id = tempId
        set TileData.first.range = tempRange
        call DestroyBoolExpr(exprTemp)
        set exprTemp = null
        return TileData.first
    endfunction
 
endlibrary
 
Last edited:
Oh, true, that can be combined. Will remember it and change it on next update!

Sure, I try.

In an ice escape map I had a special unit which moves around and changes the terrain type in it's range.
I used Change Terrain Type with Shape "Circle" of size... for doing the job.
But in the function you purely can define the tile-size of a circle, but not the exact range. Range will be more accurate.
Basicy you can do Actions with TerrainTilesInRange over using this sized circle.

You also very dynamicly can check or also count certain terrain tile with special conditions is in range. For example TerrainType/TerrainHeight/TerrainVariance/Cliff Level.

Are they mostly useful? Probably not. But for special systems/spells some might be used. :)

Edit: Updated.
 
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
JASS:
// only need a very basic structure, which is fast
private module List

JASS:
    // So user never has to care about destroying lists
    if (TileData.first != 0) then
        call TileData.destroyList()
    endif

Well if there's always only 1 list, why not use a global array instead, for which you can set the size to 0 and call it "destroyed"?

TileDefinition's math seems a bit off (see the attached test map).

Also it's a bit ironic for a library called TileDefinition not have a definition of what a tile is =). (on page 2 in the thread there's something about it being 128x128).

Could also merge these two into a single function that returns both X and Y.
JASS:
    function GetTileCenterXById takes integer id returns real
        if ((id < 0) or (id >= WorldTilesX * WorldTilesY)) then
            return 0.
        endif

        return (WorldBounds.minX + ModuloInteger(id, WorldTilesX) * 128.)
    endfunction

    function GetTileCenterYById takes integer id returns real
        if ((id < 0) or (id >= WorldTilesX * WorldTilesY)) then
            return 0.
        endif

        return (WorldBounds.minY + id / WorldTilesX * 128.)
    endfunction

These seem unnecessary:
JASS:
    function GetTileMin takes real a returns real
        return GetTileCenterCoordinate(a) - 64.
    endfunction

    function GetTileMax takes real a returns real
        return GetTileCenterCoordinate(a) + 64.
    endfunction

because when one has the center point of a tile (and they know that a tile is 128x128 units) they can compute the 4 corners of the tile:
JASS:
call GetTileCenterFromXY(x, y)
// or GetTileCenterFromId(GetTileId(x, y))

// can't have multiple return values so we use globals instead
//
set tcx = Tile.cx // or TileDefinition.centerX
set tcy = Tile.cy // or TileDefinition.centerY

// bottom left corner of the tile
set blx = tcx - 64.0
set bly = tcy - 64.0

// bottom right corner of the tile
set brx = tcx + 64.0
set bry = tcy - 64.0

// top right corner of the tile
set trx = tcx + 64.0
set try = tcy + 64.0

// top left corner of the tile
set tlx = tcx - 64.0
set trx = tcy + 64.0


// can also compute the middle points of the tile's sides
//

// left side middle point
set lsmx = tcx - 64.0
set lsmy = tcxy

// right side middle point
set rsmx = tcx + 64.0
set rsmy = tcy

// top side middle x
set tsmx = tcx
set tsmy = tcy + 64.0

// bottom side middle x
set bsmx = tcx
set bsmy = tcy - 64.0

// tcx <3 =)


I could also imagine having a tile struct that can change/retrive the terrain type of the tile using enum constants:
JASS:
set tile = Tile.from_id(GetTileId(x, y))
call title.set_type(Tile.LORDAERON_WINTER_GRASSY_SNOW /*'Wsng'*/, Tile.VARIATION_RANDOM)
set tile_type = tile.get_type()
set tile_variation = tile.get_variation()
 

Attachments

  • td-test.w3x
    16.3 KB · Views: 93
Well if there's always only 1 list, why not use a global array instead, for which you can set the size to 0 and call it "destroyed"?
I guess it would be some faster, but I also like the .next syntax. I think about it.

Also it's a bit ironic for a library called TileDefinition not have a definition of what a tile is =). (on page 2 in the thread there's something about it being 128x128).
Info

A specific terrain tile is defined by it's terrain type, but also through coordinates.
While there exists a native GetTerrainType, there is no native to get the geographical data of it.

TileDefinition can give these geographical information about the location of a terrain tile.
It can also provide an unique index for each terrain tile which can be used as reference.
So it's about terrain tiles, which size is always 128x128. gras/snow/dirt/... -- I'm not sure what to add.

Could also merge these two into a single function that returns both X and Y.
What would be the sense of merging? I think it's positive to have them seperated. The same reason why elemtal moving is SetX/Y, and GetX/Y. Or what do you mean?

These seem unnecessary:
JASS:
function GetTileMin takes real a returns real
        return GetTileCenterCoordinate(a) - 64.
    endfunction
 
    function GetTileMax takes real a returns real
        return GetTileCenterCoordinate(a) + 64.
    endfunction

because when one has the center point of a tile (and they know that a tile is 128x128 units) they can compute the 4 corners of the tile:
Why? It seems like a useful function to get the maximum/minimum x or y coordinate of a tile.
One self does not always use/have the Center x/y, so the function does it all internaly, and only returns the needed max/min x/y value.
Like in the GetTilesInRange:

JASS:
// Add all tiles above start tile
    set maxY = GetTileMax(y + range)
    set tempY = y + 128
    set currentRange = GetTileMax(y) - y
    loop
        exitwhen tempY >= maxY or not IsYInWorld(tempY)
        call AddTile(x, tempY, currentRange)
        set currentRange = currentRange + 128
        set tempY = tempY + 128
    endloop
^x/y is not the center, and I actually don't need it, so I only use GetTileMax function.

I could also imagine having a tile struct that can change/retrive the terrain type
But there are already straight forward natives about TerrainType
JASS:
native GetTerrainType takes real x, real y returns integer
native GetTerrainVariance takes real x, real y returns integer

native SetTerrainType takes real x, real y, integer terrainType, integer variation, integer area, integer shape returns nothing
and the user just needs random x/y from the tile to use them, so not even the center or so. So I don't know what I would add useful.

TileDefinition's math seems a bit off (see the attached test map).
Could you specifiy? It seems okay to me, but maybe I miss something. I'm a bit confused why x/y of unit is printed. (and the decimal dot is hardly visible)

Thanks for feedback.
 
Level 13
Joined
Nov 7, 2014
Messages
571
Could you specifiy? It seems okay to me, but maybe I miss something.
The first tile's id (bottom left corner) should be 1 (or 0), but it's 34? I guess it doesn't matter...

Hm... I think of tiles as the squares between the grid lines (in the demo map a "tile" has 4 goblin land mines in each of its corners), but for you (I think) tiles are squares with dimensions 128x128 and center points where the grid lines intersect, i.e where one can place a terrain texture in the terrain editor (the mouse pointer snaps on those points).

What would be the sense of merging? I think it's positive to have them seperated. The same reason why elemtal moving is SetX/Y, and GetX/Y. Or what do you mean?
Less function calls?
 
The terrain tiles with id 0,1,2,.. seem to be directly at border, so

m... I think of tiles as the squares between the grid lines (in the demo map a "tile" has 4 goblin land mines in each of its corners), but for you (I think) tiles are squares with dimensions 128x128 and center points where the grid lines intersect, i.e where one can place a terrain texture in the terrain editor (the mouse pointer snaps on those points).
... you are right here, I guess, so (for "my" tiles) the tile's center of my TileId(0) tile is at the very left bottom, while for "your" tiles it is, +64/+64 offset.
So yus, only the nodes where the lines intersect are maybe relevant for (terrain tiles). I haven't meant to calculate for the others.
Less function calls?
Should I add a struct syntax maybe?
JASS:
local real x = 0
local real y = 0
local TerrainTile tile = TerrainTile[GetTildeId(x, y)]

call SetUnitX(unit, tile.centerX)
call SetUnitX(unit, tile.centerY)
 
Level 13
Joined
Nov 7, 2014
Messages
571
Should I add a struct syntax maybe?
JASS:
local real x = 0
local real y = 0
local TerrainTile tile = TerrainTile[GetTildeId(x, y)]

call SetUnitX(unit, tile.centerX)
call SetUnitX(unit, tile.centerY)

I guess you could but one would not be able to have a reference to more than 1 TerrainTile at a time (using structs):
JASS:
local TerrainTile t1 = TerrainTile[GetTildeId(x1, y1)]
local TerrainTile t2 =  TerrainTile[GetTildeId(x2, y2)]

// TerrainTile[] can't (in general) return a different instance because
// even for a map with dimensions 96x96 one would need arrays of size 9216,
// jass has only 8191, i.e 91x90 but these are not multiple of 32

You either have to make the restriction of only 1 TerrainTile instance (i.e a static struct/global variables) at a time or the different struct instances would have to be implemented via a hashtable.
 
Level 13
Joined
Nov 7, 2014
Messages
571
I thought about hashtables, but I haven't thought far enough. method operators are proabably needed then, which makes it slower again.
You either have to make the restriction of only 1 TerrainTile instance (i.e a static struct/global variables) at a time or the different struct instances would have to be implemented via a hashtable.

I guess you could also make the TerrainTile instances be the tile ids (the ones from GetTileId), i.e similar to how the ARGB struct instances work, i.e they don't store instance data it gets computed on the fly.
 
Anyways, I wanted to note, that if someone wants to write a standalone library for finding the nearest or the farest Tile X (for example snow),
he is still welcome to write something when using more effective algotithms than I am using here, maybe some quad tree. Because my goal of this
library is primaly to enumerate ALL tiles in certain range and effectivly. Intuitivly I do not care about something like going from close to far, or something similar.
So the addon library is still useful, but for getting Closest or Farest tile X, one could maybe write a better standalone way. The sort function is okay, though.
 
Top