Adaptative Age of Mandala




  • Admin
Wololo Kingdoms DE is the first mod to have an adaptative version of Age of Mandala. As of April 2021, Definitive Edition seems to be working on a version of it. It seems a different implementation of what I’m explaining here, judging from the screenshots I’ve seen, and it’s currently only in the beta branch.

We are going to look at the basic version of Age of Mandala and explain why it’s not possible to make it adaptative. Then we’ll see how Wololo Kingdoms DE handles the adaptative part.

Basic Age of Mandala​

The first implementation of Age of Mandala, available in the mod center and that works on every version of the game, works by modifying the shadow of buildings. Usually the graphics of buildings is separated into two parts, the building and its shadow. Age of Mandala replaces the shadow file for one with the circle.

If one looks at the code of the game, it seems clear why it’s not that simple to make the mandala grow with the range upgrades. There is nothing in the effects of the range upgrades that modifies the graphics of the buildings. Regardless of upgrades, the graphics of the buildings are the same.

Adaptative Age of Mandala​

If we want to have an adaptative version of the mandala, we need to modify the game so that we have a way of having different graphics for different range upgrades.

Easy version​

The solution at first sight is easy: have the range upgrades upgrade the buildings into a different versions of them with extra range. Since they are different units, we can have different graphics in them and thus add a different mandala in each building. This is how it’s done for Kreposts, Harbors and Donjons. For example, there are four versions of the Krepost, 1245, 1465, 1466 and 1467, each with their own graphics that include different versions of the mandala. Range upgrades such as Fletching (effect 192), upgrades one Krepost into the next version and modifies the range, so that the building still looks as having 7+1 range.

Hard version​

The previous implementation is easy to do but benefits on the detail that those buildings are unique to speciffic civilizations and therefore there is only one set of graphics we have to worry about. Castles and Towers are very hard to do in this fashion as we’d have to create different graphics for each civilization and range upgrade. At first I considered this to be too much work but, after some thought, I found a workaround that allows me not to have to modify the buildings themselves.

The idea is to use annex units that take care of the Mandala. If we look at the castle, unit 82, we see it has unit 1445, “Castle range 8”, annex. This unit only disapears if the castle itself dies, as it is attached to it. The only relevant thing it has is that it carries the mandala graphic with it. This way, when you see a castle, you are actually seing two units: the basic castle and the mandala unit attached to it. Range upgrades only have to upgrade the “range” units for the Mandala to be adaptative.

This implementation introduces a problem: since the range unit is different from the actual unit, it appears when the castle or tower is placed, before villagers even start working on it. This meant that a player could see a mandala appear as soon as the opponent placed the building but before the villagers arrived, giving him time to react.

To solve this we have to separate the building being built from the building actually standing, like Town Centers and Gates do. If we look for “Castle” units in the units tab, we can see there are actually two of them, 82 and 1460:
  • Unit 82 is where all techs and units are created but it has no Train location (which should be the villager) and has unit 1460 as Head unit.
  • Unit 1460 does have the villager as the Train location, has a different range unit from unit 82, 1416, and has unit 82 as Stack unit. The description of Stack unit reads “Second building to be placed directly on top of this building”. Moreover, one of its attributes is “Built: Vanishes”.
This means that the unit you see in the villager menu is number 1460 and this is the unit villagers build. Once the building is complete, it disapears and is replaced by unit 82, the actual castle.

Let’s look finally at the other range unit, 1416, in castle 1460. This unit is identical to unit 1445 except for the hit points, which are set to -1. Then, unit 1416 dies the moment it is created. This is done so that the mandala is visible when you are about to place the building. The game shows the standing graphics of the unit and all its annex units but without creating them. Once the building is placed, the range unit is created and dies instantly. The mandala will reappear once the building is finished and is replaced with the other version.

Code for generating the mandalas​

It is unlikely that the game will ever include a building that can not reuse one of the existing mandalas. In any case, here is the Python code I used to create the mandalas, in case it is ever needed.

Adaptative Age of Mandala code​

Code:
from PIL import Image
import math
import numpy as np

'''
This program creates mandalas given the size of the building and its reach
It differentiates between straight lines paralel to the sides of the building and
arcs around the corners, following an ellipse.

To get the distances, the program assumes that the tiles measure 96x96 pixels and that
the perspective shrinks them only vertically, by a factor of 0.5.
'''

global ang,side,horizon
#A tile measures 96 pixels horizontaly and 48 vertically
ang = math.atan(0.5) #angle of the grid lines from the horizontal, 30 degrees.
side = math.sqrt(96**2+48**2)/2 #length of a tile side in pixels
horizon = math.sqrt(2)*48 #length of a tile side when horizontal

def aprox(x):
    return list(map(round,x))

#Create empty image
def empty(base,reach):
    #base: number of tiles of the side of the building
    #reach: in tiles of the building
    global ang, side, horizon
    l = math.ceil(base*96+reach*2*horizon)+4 #length of the picture plus 4 extra pixels
    h = math.ceil(base*48+reach*2*horizon*0.5)+4 #height of the picture plus 4 extra pixels
    return Image.new("RGB", (l,h), (255,0,255)) #blank image in pink

#Paint a pixel and the 8 surounding it
def paint(img,x,color):
    i = [-1, 0, 1,-1,0,1,-1,0,1]
    j = [-1,-1,-1, 0,0,0, 1,1,1]
    for k in range(9):
        img.putpixel((x[0]+i[k],x[1]+j[k]),color)

#Draw straingt lines from point x following vector v in length long(given in tiles)
def straight_line(x,v,long,img):
    global side,trace
    step = 0.01   
    for _ in np.arange(0,long+step,step):
        x=x+v*step
        if int(trace)%2==0: #to make discontinuous line
            paint(img,aprox(x),(255,0,0)) #paint in red
        trace += step*4

#Paint arcs it distance reach from x0 starting in angle a0
#The function draws an ellipse
def arc(x0,a0,reach,img):
    global ang,trace,horizon
    step = 0.001
    #Start a bit before a0 and finish a bit after rotating ang, to ensure it connects to the straight lines
    for thet in np.arange(a0-ang*0.7,a0+2.7*ang+step,step):
        x=x0+np.array([reach*horizon*math.cos(thet),reach*horizon*0.5*math.sin(thet)])
        if int(trace)%3==0: #to make discontinuous line
            paint(img,aprox(x),(255,0,0))
        trace += step*(3*horizon/4) #increment: angle in radians times the mean radius of the ellipse

#function to create the whole mandala
def mandala(base,reach):
    #base: number of tiles of the side of the building
    #reach: in tiles of the building
    global ang,side,trace
    img = empty(base,reach)

    #vectors following the lines in the grid
    v1 = np.array([side*math.cos(ang),side*math.sin(ang),])
    v2 = np.array([-side*math.cos(ang),side*math.sin(ang),])
    (l,h) = img.size

    center = np.array([l/2-1,h/2-1])
    
    trace = 0
    #straight side NW
    x=center-v2*base/2-v1*(base/2+reach)
    straight_line(x,v2,base,img)
    
    #arc W
    x=center+v2*base/2-v1*(base/2)
    arc(x,math.pi-ang,reach,img)
    
    #straight side SW
    x=center-v1*base/2+v2*(base/2+reach)
    straight_line(x,v1,base,img)

    #arc N
    x=center-v2*base/2-v1*(base/2)
    arc(x,3*math.pi/2-ang,reach,img)

    #straight side SE
    x=center+v2*base/2+v1*(base/2+reach)
    straight_line(x,-v2,base,img)

    #arc E
    x=center-v2*base/2+v1*(base/2)
    arc(x,-ang,reach,img)

    #straight side NE
    x=center+v1*base/2-v2*(base/2+reach)
    straight_line(x,-v1,base,img)

    #arc S
    x=center+v2*base/2+v1*(base/2)
    arc(x,math.pi/2-ang,reach,img)
            
    return img

#Tower
for i in range(8,14):
    mandala(1,i).save('tower'+str(i)+'.bmp')

#Donjon
for i in range(8,12):
    mandala(2,i).save('donjon'+str(i)+'.bmp')

#Krepost/Harbor
for i in range(7,11):
    mandala(3,i).save('krepost'+str(i)+'.bmp')

#Castle
for i in range(8,15):
    mandala(4,i).save('castle'+str(i)+'.bmp')
 
Back
Top Bottom