In this massive feature request, I propose a method of making the game's maps more interactive by combining features of maps and scenarios. This will make the game more dynamic and make a larger portion of the game modable (and modding is the lifeline of MegaGlest).
Maps are scenarios
As it stands, we can currently play scenarios, which have set factions and goals. Meh, good for one play through and they lose their thrill. Now consider a volcanic level where the center of the map has a large volcano with lava pouring out of the ground in various locations. Units that touch the lava are hurt. Prowling the level are creatures that thrive on the hellish landscape. They don't belong to any faction, and are wild beasts who attack on sight.
This image is a lot different than the games we currently play. We can't add these models to a map. Tilesets are too limited. We can't have damaging lava. We can't add faction-less enemies. Traps? Ha! Dynamic landscapes? No way. This proposal seeks to change that by adding a lua script to maps. This is *like* scenarios, but only a subset of the commands in scenarios are useful in maps, so a large variety of other commands are necessary to make these maps dynamic. The map format also changes, as we need to "attach" models to maps, which are independent of tilesets.
The focus is making maps versatile. I believe maps are the area which would allow the biggest change to MegaGlest at the moment.
The remainder of this document is highly technical and assumes experience in Lua scripting in MegaGlest.Maps are more than just tileset objects
As it stands, our maps are really quite simple. A grid of heights with textures and objects placed. Water and cliffs are just dependent on the height. The tileset is the life of the map. This proposal does not change that. The tileset is still being used.
However, we also add the ability to position independent models on the map. To do this, we specify the position that the geometric center of the model is placed. The center is the natural choice, since it allows models to hang off edges easily. We then specify the size (diameter) of the model, in tiles. So if I place a model at {100, 100} and specify the size as "1", it is expected that the model will take up just the tile at {100, 100}. However if the size is 2, then the model will be placed so that it is 2 * 2. Models are thus assumed to be a perfect square. Since we're specifying the center, it is necessary for models to be shifted for even numbers, and will always shift right and down. So for a 2 * 2 model, the model will take up {100,100}, {100, 101}, {101, 100}, and {101, 101}.
It is necessary that we can specify the walkability of models. This will be done with cellmaps in the same way as a unit.
There can be any number of models, and models can overlap. Most importantly, models can be placed on top of one another. Thus, I can have two different models on {100, 100}. When the walkability of models overlaps, we treat the walkability like a logical OR. The final cell is only "walkable" (a zero in the cellmap) if both overlapping cells are walkable (0 OR 0 = 0). Models can also be placed over top of tileset objects. The base height of the model is assumed to be the *highest* height tile in the model's square. This is for ease of positioning on non-flat surfaces. The highest tile is a better choice than the lowest, as it would make models such as bridges significantly easier.
For animated models, the size is relative to the first frame only.
Syntactically, maps are formatted with an XML similar to that of scenarios. Here's what the model placement portion would look like:
<map>
<models>
<model name="modelName" file="model.g3d" x="100" y="100" size="2" active="true">
<cellmap value="true">
<row value="01" />
<row value="10" />
</cellmap>
</model>
</models>
</map>
- name is an identifier for that model. This will be used later to toggle the active and inactive state of models.
- file is the path to the G3D model to use.
- x and y are the x and y coordinates of the model's geometric center. In other words, where the object will be placed.
- size specifies the diameter of the model in tiles.
- active is used to toggle whether the model is displayed. This is used later, where we can enabled or disable a model. Enabled models are visible. You can see them and are impacted by the cellmap. Disabled models are pretty much not there at all. Later code will allow them to be toggled as enabled. This allows models to appear and disappear. Disabled models do not apply their cellmap. This attribute is optional and defaults to "true".
- cellmap works the same way as for units. If value is "false", it is assumed that the entire model grid is not walkable.
Thus, all models are listed in the map XML, and can be preloaded with the game.
Maps can be split into regions
The concept of regions exist in scenarios, but maps demand precision. A region is simply a zone on the map. A group of tiles that can do *something* when units enter it. A region exists as a group of selections. For example, a region might be the rectangular selection from {100, 100} to {105, 105} (that is, a 5 * 5 square). A more complicated region would be a combination of two rectangular selections, such as the the rectangular selection from {95, 100} to {105, 100} and the rectangular selection from {100, 95} to {100, 105} (a cross). By combining selections, we can select any set of tiles.
Like models, regions are declared statically in the XML file, giving a name and a collection of selections:
<map>
<regions>
<region name="regionName">
<rectangle-selection x1="100" y1="100" x2="105" y2="105" />
<single-selection x="100" y="100" />
</region>
</regions>
</map>
There may be any number of regions with any number of selections. Tiles can belong to multiple regions. The region
name is a unique identifier, which is used so that Lua can modify regions and capture activity within them.
rectangle-selection is straight forward enough. x1 and y1 refer to the coordinates of the top left corner, while x2 and y2 refer to the coordinates of the bottom left corner.
single-selection is purely a syntactical convenience, and means the same thing as a rectangular selection where x1 = x2 and y1=y2 (a single tile rectangle).
Dynamic, baby!
First of all, we need to discuss how Lua code is triggered. Like scenarios, the map XML has a startup tag, where Lua code is executed once when the map is loaded.
Beyond that, all Lua code is triggers, just like scenarios. I expect one of the most useful triggers will be the existing timer triggers, which should function exactly the same way in maps as they would in scenarios. Timers can be used to make models move, to trigger damage in regions, etc.
Further, the
<regionTrigger value="regionName"> is called when a unit enters "regionName". This trigger is only useful for events that occur upon entering a region. For repetitive damage inside a region, a timer trigger is best used, combined with functions that return units inside of a given region (more on these functions later).
Models
Models are dynamic in how they can modify their coordinates ("move"), toggle their active state ("appear" and "disappear"), and modify their cellmap (create walkability). Note there is no way to change a model to use a different animation, since all models are pre-loaded. To use a different animation, you'd want to disable a model and enable another model (with the desired animation) in its place.
void setModelPosition(string modelName, int[] coords, float damage)Moves a model instantaneously to the given coordinates (in the format of
{x, y}). The
damage is used to determine how to deal with units that are in the way of the movement. If
damage is -1, units in walkable cells in which the model appears in will be instantly killed. Otherwise, units are pushed out of the way and delivered the damage. If the
damage is a floating point value between zero and one (
damage > 0 && damage < 1), then the damage dealt is a percentage. If there is absolutely no way to push the unit aside, the unit is instantly killed.
void moveModelPosition(string modelName, int[] coords, int damage, int animSpeed)The same as above, but moves the model gradually instead of instantaneously. The
animSpeed is used to determine how many world cycles it takes to move the model (recall that by default, 40 cycles = 1 second). Note that the animation is purely aesthetical. The model is considered to have instantly moved (so damage works the same way as the previous function). When moving a damaging model multiple tiles, it would be best to move it one tile at a time (using a timer). This would ensure that damage is dealt on every tile the object passes over.
int[] getModelPosition(string modelName)Returns an array of the model's position. Works like
unitPosition(). First index contains both the x and y coordinate, second index just the x coordinate, and third index just the y coordinate.
int getModelSize(string modelName)Returns the size of the model.
void setModelActiveState(string modelName, bool activeState)Sets the specified model to enabled (true) or disabled (false).
bool getModelActiveState(string modelName)Returns whether or not the model is enabled.
void setModelCellmap(string modelName, bool[] cellmap)Sets the cellmap of a specified model. The cellmap is simply an array of boolean values. So for a 2 * 2 object, a cell map might look like
{1, 1, 0, 0}, which is the same as the first row being all ones (unwalkable) and the second row being all zeroes (walkable).
bool[] getModelCellmap(string modelName)Returns the cellmap in the format specified in the previous function.
Regions
The latter two functions listed here are not directly related to regions, but are a necessity to make regions useful.
int[] getUnitsInRegion(string regionName)Returns an array of unit IDs for units inside the specified region.
void changeUnitStats(int unitID, string statName, float relativeChange, int duration)This allows stats of a specified unit to be changed. The modifiable stats are "hp" (current), "maxHp", "ep" (current), "maxEp", "sight", "attackStrength", "attackRange", "armour", "moveSpeed", "productionSpeed", "hpRegen", and "epRegen". That is, all the stats that can be modified by an update as well as the current HP/EP and regeneration. This allows maps to provide mini-upgrades (or downgrades) for units as well as damaging/healing units.
All values are relative to the current stats (just like in upgrades). For values inbetween zero and one (non-exclusive), treat the change as a multiplier.
The effects last for
duration seconds. If the duration is -1, the stat change lasts indefinitely.
void applyAttackBoost(int unitID, ???)It is currently unclear how to proceed with this function. Attack boosts have a very large number of parameters, so applying one purely with a function would create a function from hell. Alternatives include passing an array with the attack boost parameters or defining the attack boost in the map XML with an identifier. Regardless of the Lua implementation, attack boosts provide powerful ways to modify units. A unit could emerge from a cave as a hero which boosts the combat skills of those who fight by his side.
Particles
You didn't really think I forgot about particles, did you? Particles can be applied to a model or region. Thus, we could have a planes region with a windy appearance or a volcano model that smokes real good. To facilitate loading of particle effects, all particle files are loaded in at startup just like models and regions:
<map>
<particle-files>
<particle-file name="particleName" file="particleSystem.xml" />
</particle-files>
</map>
Thus, particles also have unique identifiers. We can then apply particle systems to models and regions by default with an additional tag:
<map>
<models>
<model name="modelName" file="model.g3d" x="100" y="100" size="2" active="true">
<particle value="particleName" />
</model>
</models>
<regions>
<region name="regionName">
<single-selection x="100" y="100" />
<particle value="particleName" />
</region>
</regions>
</map>
Finally, we can apply particles dynamically, allowing us to change the particles on a model or region. A volcano can explode or a field can catch ablaze!
void setModelParticleSystem(string modelName, string particleName)Sets the particle system for a model.
void setRegionParticleSystem(string regionName, string particleName)Sets the particle system for a region.
It should be noted that models are always perfect squares, which means that their particles are too. Thus, it may often be beneficial to use regions for setting models on a particle system. Thus, we can have a large volcano model where only the top of the volcano billows smoke (the area of the top would be a region with a particle system).
Neutral factions
Neutral factions are already supported by scenarios. In maps, neutral factions are more versatile. In particular, there may be up to eight different neutral factions. Teams are assigned to them the same way as they are assigned to players (which means there may be up to 16 teams in total; 8 player teams and 8 neutral faction teams). This allows neutral factions to war with each other or ally with a player. Neutral factions are assigned a level of aggressiveness ranging from zero to four.
- 0: The faction is benevolent to everyone, and never attacks. In the future, they could use beneficial abilities on players.
- 1: The faction is non-aggressive and will not attack unless attacked first. They will never attack allies.
- 2: The faction is semi-aggressive and will not attack unless threatened (get too close) or attacked first. They will counter-attack allies.
- 3: The faction is aggressive and will attack on sight. They will counter-attack allies.
- 4: The faction is very aggressive and will attack on sight. They will also go out of their way to attack enemies (for example, they will traverse the entire map to attack an enemy base)
Neutral factions are not meant to be large or very powerful, and are meant to be produced entirely through Lua commands. Neutral factions may range from nomads to animals to magical auras. If capturing buildings becomes a possibility in the future, neutral factions consisting solely of abandoned buildings may be very interesting.
The XML code is very similar to that of in scenarios:
<map>
<players>
<player faction="civilians" aggressiveness="1" team="9"/>
<player faction="glestimals" aggressiveness="3" team="10"/>
</players>
</map>
To allow factions that are unique to maps (and not part of the player's tech tree), it should be possible to load "external" factions. Presumably we could just create a faction folder in the map folder and dump factions in there. If the game cannot find the faction in the techtree, it checks this folder.
Other functions
There's also a few other miscellaneous functions that would be highly beneficial for this.
void changeTileWalkability(int[] coords, bool walkable)Overrides the walkability of a tile specified by the coordinates (in the form of
{x, y}). If
walkable is true, the tile is walkable. Otherwise, the tile becomes non-walkable. This overrides any model or object that may be on this tile. This would be useful for making bridges over water.
void resetTileWalkability(int[] coords)Removes an override of the specified tile's walkability. The walkability is recalculated from the objects and models that overlap that tile. This would be useful for blowing up said bridges.
void regionAIHint(string regionName, int hint)Many regions are either beneficial or harmful for players. For example, a pit of lava is harmful while a healing aura might be beneficial. Thus, it's a good idea to give the AI hints over how they should treat regions. The
hint is a number from 10 to -10, where -10 is most harmful and +10 is most beneficial. The default is zero (neutral). Thus, an AI may go out of its way to enter beneficial areas, and should avoid harmful areas unless absolutely necessary. For example, the pathfinder should treat harmful areas as unwalkable unless there's no alternative. Minor harmful areas might be worth the risk.
This command is optional and intended to prevent the AI from being *too* retarded.
It is not intended to be a long term solution. Ideally, the AI would start out oblivious and learn for itself if a region is good or bad. However, the AI has more pressing concerns, so for the mean time, such a command forms a nice AI hint.
<suggested-tileset value="tilesetName" />This XML element is completely optional and goes under the top level map element. It suggests a tileset to use. This is a good idea, since maps with custom models are likely meant for specific tilesets and may look out of place with others. Heck, you could even use suggested tilesets on a vanilla map. This is just a suggestion, and would make the tileset in the game's set-up default to the value. If the tileset does not exist here and does exist in the download center, the game could suggest downloading the tileset.
Grand "we're-at-the-end-of-the-freaking-post" summary
Yes, that was very long. For modders, it's not as complex as you'd think. Rather, I am extremely verbose and this is very complete. Many maps would not need to know everything here. In fact, if you just want to put a big mountain model in your map, that's very easy. You just have to know the model XML in the second section, no Lua necessary. It's the developers who have the hard job. However, I believe this can be heavily compartmentalized. It is possible to implement features in baby steps while maintaining a working game. For example, we could implement the model XML first. That alone is highly useful, in my opinion. Once that's working correctly, we could add particles, then regions, and finally the Lua functions are all modular and can be added one by one. However, in order for the features to be truly useful, most of the features must be implemented.
In my opinion, the model functions are fairly low priority. Moving models can allow for things like spike traps, avalanches, moving boulders, and collapsing buildings. However, getting models displayed is more important, and regions would be much easier to use than the model funtions.
Regions are almost useless without the
getUnitsInRegion() function, which is necessary to loop through units to damage.
changeUnitStats() is much more useful than
applyAttackBoost() (which needs additional input on how to implement the feature).
The particles are particularly useful and thankfully very simple. The most difficult aspect of the particles is that regions are not a circle or square. The engine will probably require changes to be able to spread particles over an arbitrary shape.
Neutral factions are particularly useful. In early stages, we can make all the factions aggressive and simply not give attack commands to units that are supposed to be non-aggressive (the AI runs away from attackers if there is no attack command).
The
regionAIHint() can also be saved for later. The AI would likely be fairly dumb without it (depending on the map's design), but it's not a priority.
Why do we want/need this?
This change has two main benefits:
- You now have to play the map. We have a large number of maps, but for the most part, they're very similar. Players are usually on the corners or edges, generally surrounded by trees and stones and the bases are connected by paths. With dynamic maps, you have to play against the map as well as the enemy players. You can't go waltzing off into lava. Going out of your way to explore can be beneficial. Scouting is more important. Maps can hold surprises that aren't just "yay, more resources".
Maps also look better. All our maps are different combinations of trees, water, stones, and textures. The tileset does more than the map could. This reverses that. You can have glorious mountains, true cliffs, towering buildings, massive ruins, giant trees, portals to another world, animal dens, and more. This removes the limitations of tilesets and creates endless possibilities.
- Modding forever. Endless possibilities is great for gamers, and even better for modders. Those ideas you imagined are no longer "technically impossible". You can create the map exactly as you wish, with the only limitation being your modeling skills (and it's easy to find static models for maps under free licenses).
Comments? Suggestions? Post below!I hope you weren't waiting up for me, Ishmaru. 20k characters takes a while.