| 04-07-2010, 12:14 PM | #1 |
Introduction: I decided to make these scripts because I was making a spell that changed the terrain temporarily, and I ran into all kinds of problems. How to store the old terrain that was there was the problem: I fixed that by making a simple array of values holding the terrain type and variation of the original. It worked great until I cast another spell overlapping the first -- by the time both spells had expired, the terrain stayed changed where they overlapped. I tried to build something into the spell, but it was futile -- it was too complicated to incorporate into a spell script. I needed something else. That's why I wrote these libraries. So what do they do? TerrainTile can change the terrain at any given point, and no matter how many are created over each other, when they are all removed, the original tile will still be there. TerrainSwap is a holder for multiple TerrainTiles. It allows you to create multiple TerrainTiles over any area, by a selection of several methods, and apply or remove them all at once. Here is the code: Requires LinkedList by Ammorth TerrainTile:/*************************************************************************** * TerrainTile by Element of Water **************************************************************************** * Requirements: * LinkedList by Ammorth **************************************************************************** * What is it? * TerrainTile provides a way to swap any single terrain tile on the map * with a different one, just like SetTerrainType. The difference is, with * this system, when the TerrainTile is destroyed, the tile that was * originally there, that the TerrainTile overwrote, will reappear. You * can also stack multiple TerrainTiles on top of one another, and * destroying the top one will reveal the next, etc, in the same order * they were applied in. Don't worry about destroying tiles that weren't * on top, either -- they will be removed from the list. **************************************************************************** * How do I use it? * Using TerrainTile couldn't be easier -- there are just a few functions * to call to control it. * 1) Create your TerrainTile: * set MyTerrainTile = TerrainTile.create(real x, real y, integer terrainType, integer variation) * real x/y -- the coordinates of your TerrainTile * integer terrainType -- the terrain tile's type id, this can be found * by converting an Environment - Change Terrain * Type GUI action into JASS and copying the value * in 'AAAA' that you see there * integer variation -- this is just the same as the "variation" when * placing tiles in the terrain editor, except * you can have a variation of -1, which means * "random variation" * 2) Apply (show) your TerrainTile: * call MyTerrainTile.apply() * 3) Change its properties: * set MyTerrainTile.terrainType = NewTerrainType * set MyTerrainTile.variation = NewVariation * 4) Reapply it, or change its location: * call MyTerrainTile.setCoords(newX, newY) * 5) When you're done with it, but you'll use it again, remove it: * call MyTerrainTile.remove() * 6) Or if you're finished with it for good, destroy it: * call MyTerrainTile.destroy() * * There is also a function called TerrainTile.coord2TileCoords, which * takes any single coordinate x, and returns its closest multiple of 128, * the size of a tile. I figured I'd make it public because some people * might want to use it. ***************************************************************************/ //! zinc library TerrainTile requires LinkedList { public struct TerrainTile { // shouldn't be changed, unless blizz changes it in a patch, // which is highly unlikely static constant real TILE_SIZE = 128.; // the hashtable to store the tiles in private static hashtable hash = InitHashtable(); // internal variable used to separate default tiles, // the ones which are there at the start of the map, // from those which are changed with this system private boolean isDefault = false; // internal variables basically there to save processing // power private integer ix = 0, iy = 0; // the coordinates of the tile -- access them with "x"/"y", // not "xx"/"xy" -- don't worry if these are // different to the ones you put into the create function // as they are rounded to the nearest multiple of 128 // can't be altered directly, as that could screw up // the whole system private real xx = 0., xy = 0.; // the rawcode of the terrain type of the tile -- you can // alter this directly but you won't see anything unless // you call apply() again integer terrainType = 0; // the variation of the tile -- again, you can alter this // but it won't be visible until apply() is called // note: a variation of -1 means random, and this means // it might change in appearance when another tile is // created over it and then removed, or even if you call // apply() again. integer variation = 0; // you can store data on the TerrainTile if you want to... integer data = 0; // the LinkedList Link used to store the place in the list // of tiles at the same place (don't worry, I don't // understand what I just typed either :P) private Link link = 0; // the LinkedList List used to store all the tiles in the // same place as this tile private List list = 0; // this method changes the position of the tile method setCoords (real newX, real newY) { // if the tile has been applied already, flag it for // reappliance boolean reapply = link != 0; // if it needs to be reapplied, temporarily remove the // tile if (reapply) remove(); // convert the new coordinates into usable ones, store them xx = coord2TileCoord(newX); xy = coord2TileCoord(newY); // also store them as integers as multiples of 128, for // optimization reasons ix = R2I(xx / TILE_SIZE); iy = R2I(xy / TILE_SIZE); // if it needs to be reapplied, apply it again if (reapply) apply(); } // these operators return the values of the readonly variables x and y method operator x () -> real {return xx;} method operator y () -> real {return xy;} // this method adds the tile to the top of the list and displays it method apply () { TerrainTile t; // I need to comment functions like these to keep my sanity... // if the tile is not already linked, link it if (link == 0) { // load the list for this xx/xy position from the hashtable list = List(LoadInteger(hash, ix, iy)); // if the list doesn't exist... if (list == 0) { // create one... list = List.create(); // ...and save it in the hashtable SaveInteger(hash, ix, iy, integer(list)); // also, this means the tile has never been changed, so we need // to store the tile that was originally there... // allocate the new tile struct... t = TerrainTile.allocate(); // store the coordinates t.ix = ix; t.iy = iy; t.xx = xx; t.xy = xy; // store the terrain type t.terrainType = GetTerrainType(xx, xy); // store the terrain variation (why does blizz call it "Variance"? O.o t.variation = GetTerrainVariance(xx, xy); // this is the default terrain, make sure the system knows so t.isDefault = true; // create a link for the new tile struct in the new list t.link = Link.create(list, integer(t)); } // create the link in the list link = Link.create(list, integer(this)); } // else, bring the link to the front of the list // if it isn't there already else if (link != list.first) { // destroy the link, ready to recreate it... link.destroy(); // recreate it at the front of the list link = Link.create(list, integer(this)); } // finally, change the actual visible terrain! SetTerrainType(xx, xy, terrainType, variation, 1, 0); } // this method removes the tile from the list, and replaces // it with the next highest if it is at the top method remove () { TerrainTile t; // again, this definitely needs commenting... // if the tile isn't linked, we don't need to remove it -- // it doesn't actually exist // otherwise, yeah, remove it... if (link != 0) { // first thing's first, destroy the link, and null the reference link.destroy(); link = 0; // now if the list contains other data, apply that terrain type if (list.first != 0) { // store the previous tile in a variable so it can be accessed easily t = TerrainTile(list.first.data); // apply the previous terrain type SetTerrainType(xx, xy, t.terrainType, t.variation, 1, 0); // if the previous tile was the default one, we can safely destroy it if (t.isDefault) { // destroy the link to the default tile t.link.destroy(); // null the reference so the destructor doesn't screw up t.link = 0; // destroy the default tile t.destroy(); // now the list should be empty, we can destroy that, too list.destroy(); // make sure the hashtable knows the list is gone RemoveSavedInteger(hash, ix, iy); } } // if the list doesn't contain other data, it is empty, destroy it // this shouldn't happen since there should always be a default tile // at the bottom, but it's just a precaution else { // destroy the list list.destroy(); // make sure the hashtable knows the list is gone RemoveSavedInteger(hash, ix, iy); } } } // this could be a useful function I guess, so I'll leave it // public... // basically, it rounds xx to the nearest multiple of 128 static method coord2TileCoord (real x) -> real { // get the modulus of the coordinate divided by 128 real mod = x - I2R(R2I(x / TILE_SIZE)) * TILE_SIZE; // make sure the modulus is in the range 0...128 if (mod < 0.) mod += TILE_SIZE; // round it down to start with x -= mod; // and if it was at the upper end of the multiple // of 128, bring it back up by 128 if (mod > TILE_SIZE / 2.) x += TILE_SIZE; // return the rounded value return x; } static method create (real x, real y, integer terrainType, integer variation) -> TerrainTile { // allocate a new TerrainTile instance TerrainTile t = TerrainTile.allocate(); // store useable coordinates (multiples of 128) // for real-space operations t.xx = coord2TileCoord(x); t.xy = coord2TileCoord(y); // store shortened integer versions of the // coordinates for hashtable operations t.ix = R2I(t.xx / TILE_SIZE); t.iy = R2I(t.xy / TILE_SIZE); // store the terrain type id and variation // for later access t.terrainType = terrainType; t.variation = variation; // return the new struct return t; } method onDestroy () { // make sure the TerrainTile is completely erased remove(); // to let users check if the TerrainTile still // exists and is associated with their data data = 0; } } } //! endzinc TerrainSwap:/*************************************************************************** * TerrainSwap by Element of Water **************************************************************************** * Requirements: * TerrainTile by Element of Water * LinkedList by Ammorth **************************************************************************** * What is it? * A TerrainSwap object is simply a container for several TerrainTiles. * It provides ways of creating several at once, arranged it circles, * squares, or contained within a certain rect, and it can also apply * and remove them all at once. **************************************************************************** * How do I use it? * The principle is basically the same as TerrainTile -- you create it, * do things to it, apply it, the remove or destroy it. * 1) Create your TerrainTile: * set MyTerrainSwap = TerrainSwap.create(integer terrainType, integer variation) * integer terrainType -- the terrain tile's type id, this can be found * by converting an Environment - Change Terrain * Type GUI action into JASS and copying the value * in 'AAAA' that you see there * integer variation -- this is just the same as the "variation" when * placing tiles in the terrain editor, except * you can have a variation of -1, which means * "random variation" * 4) Add some tiles to your TerrainSwap: * This adds a single tile using the type and variation of the whole * TerrainSwap, at the specified x/y coordinates. * call MyTerrainSwap.addTile(real x, real y) * This adds a circle just like SetTerrainType with shape 0, at * coordinates x/y with size area. * call MyTerrainSwap.addCircle(real x, real y, integer area) * This adds a square just like SetTerrainType with shape 1, at * coordinates x/y with size area. * call MyTerrainSwap.addSquare(real x, real y, integer area) * This adds a circle or a square just like SetTerrainType, at * coordinates x/y with size area. TERRAIN_SHAPE_CIRCLE is for a circle * and TERRAIN_SHAPE_SQUARE is for a square. * call MyTerrainSwap.addShape(real x, real y, integer area, integer shape) * This one takes a rect and fills it with tiles: * call MyTerrainSwap.addRect(rect r) * 3) Apply (show) your TerrainSwap: * call MyTerrainSwap.apply() * 4) When you're done with it, but you'll use it again, remove it: * call MyTerrainSwap.remove() * 5) Or if you're finished with it for good, destroy it: * call MyTerrainSwap.destroy() ***************************************************************************/ //! zinc library TerrainSwap requires TerrainTile, LinkedList { public struct TerrainSwap { // these are for use in addShape() -- I don't think they // need any explaining :P static constant integer SHAPE_CIRCLE = 0; static constant integer SHAPE_SQUARE = 1; // the list used to store all the TerrainTiles private List list = 0; // stored values of terrain type rawcode and variation -- // I might make these public and add the necessary // functionality one day, but not now private integer terrainType = 0, variation = 0; // a rather simple method that adds a tile to the // TerrainSwap, ready to be swapped method addTile (real x, real y) { // create a link in the list containing data from a // newly created TerrainTile Link.create(list, integer(TerrainTile.create(x, y, terrainType, variation))); } // a more complicated method to add several tiles in // the shape of a circle to the TerrainSwap, with // the integer area being the diameter of the circle in // number of tiles, just like the SetTerrainType // native with the shape parameter set to 0 private real xp; // these variables are used private real yp; // to pass values between addCircle private real rad; // and addCircle_child (see below) private real radsq; private real xx, yy; method addCircle (real x, real y, integer area) { // calculate the distance the method needs to // check to draw the tiles real dist = (area - 1) * TerrainTile.TILE_SIZE; // the starting x coordinate is simply the origin // minus the distance xp = - dist; // the radius is simply the distance to the starting // x/y positions, but it needs to have a small value // added to make sure the checks encompass all points rad = dist + 1.; // square the radius for efficiency in distance checking radsq = rad * rad; // make sure the child method has access to the x // and y origins of the circle xx = TerrainTile.coord2TileCoord(x); yy = TerrainTile.coord2TileCoord(y); while (xp < rad) { // tells the child method where to start its // y loop from yp = - dist; // the y loop needs to be run in a different // thread to avoid hitting the op limit addCircle_child.evaluate(); // increment the x value xp += TerrainTile.TILE_SIZE; } } // child method of addCircle runs the y loop in a // separate thread to avoid hitting the op limit private method addCircle_child () { // we need to make sure the method doesn't hit // the op limit, and that is what this variable is // for integer i = 5; // loop through y as long as we're within the radius while (yp < rad) { // if i reaches 0.. if (i == 0) { // restart the thread addCircle_child.evaluate(); // and break the current loop break; } // use Pythagoras' theorem to determine whether // the point is within the circle else if (xp * xp + yp * yp <= radsq) // if the point is within the circle, add // a tile there addTile(xx + xp, yy + yp); // decrement the op checker i -= 1; // increment the y value yp += TerrainTile.TILE_SIZE; } } // adds several tiles in the shape of a square to the // TerrainSwap -- works exactly like SetTerrainType // with shape 1 method addSquare (real x, real y, integer area) { // calculate the distance the method needs to // check to draw the tiles real dist = (area - 1) * TerrainTile.TILE_SIZE; // I'm using the same variables as for the circle // method, even if they are used for different // things -- in this case, the radius is simply // half the width of the square (plus the small // value) rad = dist + 1.; // the starting x coordinate is simply the origin // minus the distance xp = - dist; // make sure the child method has access to the x // and y origins of the square xx = TerrainTile.coord2TileCoord(x); yy = TerrainTile.coord2TileCoord(y); while (xp < rad) { // tells the child method where to start its // y loop from yp = - dist; // the y loop needs to be run in a different // thread to avoid hitting the op limit addSquare_child.evaluate(); // increment the x value xp += TerrainTile.TILE_SIZE; } } // child method of addSquare runs the y loop in a // separate thread to avoid hitting the op limit private method addSquare_child () { // we need to make sure the method doesn't hit // the op limit, and that is what this variable is // for integer i = 5; // loop through y as long as we're within the radius while (yp < rad) { // if i reaches 0.. if (i == 0) { // restart the thread addSquare_child.evaluate(); // and break the current loop break; } // no need for checks if the point is in the // circle now, it's a SQUARE! :P else addTile(xx + xp, yy + yp); // decrement the op checker i -= 1; // increment the y value yp += TerrainTile.TILE_SIZE; } } // for those who like the format of SetTerrainType, this // adds a circle or a square to the TerrainSwap // the parameters mean exactly the same as their equivelent // parameters in SetTerrainType. method addShape (real x, real y, integer area, integer shape) { // if the shape is a circle, add a circle if (shape == SHAPE_CIRCLE) addCircle(x, y, area); // or if the shape is a square, add a square else if (shape == SHAPE_SQUARE) addSquare(x, y, area); // unrecognised shape -- give an error message else debug BJDebugMsg("TerrainSwap.addShape: ERROR -- invalid shape specified (" + I2S(shape) + ")."); } // a method to swap the terrain of all tiles in a rect private real minx, maxx; private real miny, maxy; method addRect (rect r) { // store the rect as a set of coordinates // temporarily minx = TerrainTile.coord2TileCoord(GetRectMinX(r)); miny = TerrainTile.coord2TileCoord(GetRectMinY(r)); maxx = TerrainTile.coord2TileCoord(GetRectMaxX(r)); maxy = TerrainTile.coord2TileCoord(GetRectMaxY(r)); // store where to start off across the x plane xp = minx; // loop through x as long as we're within the rect while (xp < maxx) { // tell the child method where to start the y // from yp = miny; // run the y loop in a separate thread to avoid // hitting the op limit addRect_child.evaluate(); // increment the x value xp += TerrainTile.TILE_SIZE; } } // child method of addRect runs the y loop in a // separate thread to avoid hitting the op limit private method addRect_child () { // we need to make sure the method doesn't hit // the op limit, and that is what this variable is // for integer i = 5; // loop through y as long as we're within the radius while (yp < maxy) { // if i reaches 0.. if (i == 0) { // restart the thread addRect_child.evaluate(); // and break the current loop break; } // add the tile else addTile(xp, yp); // decrement the op checker i -= 1; // increment the y value yp += TerrainTile.TILE_SIZE; } } method apply () { // get the first link in the list ready to operate on Link l = list.first; // loop through the entire list while (l != 0) { // apply the TerrainTile associated with the // current link TerrainTile(l.data).apply(); // move on to the next link l = l.next; } } method remove () { // get the first link in the list ready to operate on Link l = list.first; // loop through the entire list while (l != 0) { // remove the TerrainTile associated with the // current link TerrainTile(l.data).remove(); // move on to the next link l = l.next; } } static method create (integer terrainType, integer variation) -> TerrainSwap { TerrainSwap t = TerrainSwap.allocate(); t.list = List.create(); t.terrainType = terrainType; t.variation = variation; return t; } method onDestroy () { // iterate through all the TerrainTiles and destroy them... // get the first link in the list ready to operate on Link l = list.first; // loop through the entire list while (l != 0) { // remove the TerrainTile associated with the // current link TerrainTile(l.data).destroy(); // move on to the next link l = l.next; } list.destroy(); } } } //! endzinc |
| 04-07-2010, 08:26 PM | #2 |
Important Update: Previously, TerrainSwap had no onDestroy function, causing it to leak all kinds of crap. It's fixed now. |
| 04-07-2010, 09:25 PM | #3 |
so, one can change the tile with it? across all tilesets or the current one or what? |
| 04-07-2010, 09:35 PM | #4 |
Yep, all tilesets, custom tilesets, whatever. You just need to know the rawcode of the tile, which can be obtained by going to Environment->Change Terrain Type in GUI, selecting the one you want, and converting it to Jass. |
| 04-08-2010, 01:11 PM | #5 |
Pretty cool |
| 04-09-2010, 06:02 AM | #6 |
Can you add a few examples on how to use it? I got lost with this zinc code and I have no idea what I have to do to. |
| 04-09-2010, 06:33 AM | #7 |
Ehh I guess documentation is needed then... I'll do it at some point, but right now I'm working on something else. |
| 04-11-2010, 06:56 AM | #8 |
Documentation added. |
| 04-12-2010, 05:22 PM | #9 |
| 04-12-2010, 05:35 PM | #10 |
But evaluate doesn't start a new thread... |
| 04-12-2010, 06:51 PM | #11 | |
Quote:
|
| 04-16-2010, 03:49 PM | #12 |
I think the functionality of this could be more desirable if it were done in "layers." So, say, the base tile at a point is grass, and then I put rocks over it, and then I put ice over that. If at a later point, I remove the ice, the rocks show, and if I then remove the rocks, the grass shows. If I remove the rocks, but not the ice, the ice shows - and when that's then removed, the grass shows. See what I mean? This could be extended to N different tiles over a given point (with a reasonable limit, of course). Maybe this is a bit of a pipedream, but it's worth asking. I can definitely see the use of this script, though. Why are the scripts in the first post highlighted red? Also, can you supply a rudimentary testmap that shows this script in action - preferably showing the overlap concern you had being handled appropriately? |
| 04-16-2010, 08:41 PM | #13 | ||
Quote:
Quote:
I'll try to make a little test map. Maybe a spell that changes the terrain in an area for x seconds? PS: Can you change the title to "[script] TerrainTile & TerrainSwap"? I made a little typo :P |
| 04-19-2010, 03:33 PM | #14 | |||
Quote:
Quote:
Quote:
|
| 04-19-2010, 07:34 PM | #15 | |||
Quote:
Quote:
Quote:
|
