def ShootToConstruction(self, target, frompos, targetpos): # Shoots to a construction target. Usually by a cannon # The targetpos parameter is where attacker points. But the precision factor can modify it # In addition, the final shooting point can hit any other construction part (see NOTE bellow) # Returns a dictionary with hit result -> {"Hit": True/False, "Intersection": Point3D()} ret = {"Hit": False, "Intersection": Point3D()} # Aiming consideration: We are going to calculate a shoot in a direction sampled by a cosinus distribution # From the aiming point of view, the shooter points to a far point, and if accuracy factor is 100%, it should hit the target. # So, the sphere used to sample the cosinus distribution should has the target distance as radius. # Then, we can use the accuracy factor to get a percentage of shooting distance. With this consideration, bad accuracy factors means # sampling on small spheres, so the shoot can fail dist = frompos.Distance(targetpos) accuracy = dist * self.__accuracyFactor / 100.0 direction = Vector3D() direction.CreateFrom2Points(frompos, targetpos) sph = Sphere(position=frompos, radius=accuracy) v = sph.GetRayCosine(direction) # Check hit on the castle # NOTE : The intersection test over whole castle geometry has a high cost. The process is simplified considering only target. # If shoot doesn't hits target, the shoot fails ray = Ray(origin=frompos, direction=v, energy=self.__attack) hitpoint = target.RecieveImpact(ray) if (hitpoint != None): ret["Hit"] = True ret["Intersection"] = hitpoint return ret
def __init__(self): Construction.Construction.__init__(self) self._thickness = Battles.Utils.Settings.SETTINGS.Get_F('Castle', 'Wall', 'Thickness') self._height = Battles.Utils.Settings.SETTINGS.Get_F('Castle', 'Wall', 'InnerHeight') self._defenseIncrease = Battles.Utils.Settings.SETTINGS.Get_I('Castle', 'Wall', 'DefenseIncrease') self.__slope = None self.__joins = [None, None] self.__walkway = { "altitude": (self._height - Battles.Utils.Settings.SETTINGS.Get_F('Castle', 'Wall', 'MerlonHeight')), "width": Battles.Utils.Settings.SETTINGS.Get_F('Castle', 'Wall', 'WalkwayWidth')} self._defendingLines = BattalionConstruction.BattalionConstruction(height=self.__walkway["altitude"], cellsize=Battles.Utils.Settings.SETTINGS.Get_F( 'Castle', 'Wall', 'BattalionGridCellSize')) self.__tilesManager = TilesManager.TilesManager(self) self._label = 'Wall_' + str(Wall.wallCounter) Wall.wallCounter += 1 self.__rectangle3D = [Point3D(), Point3D(), Point3D(), Point3D()] self.__boundingRectangle3D = BoundingBox() self.__normalVector = Vector3D() self.__heightCanvasObjs = [] self.__climberStairs = [] self.__climbedAttacker = {"Battalion": None, "SoldierPosition": Point3D()} self.__attachedSiegeTower = {"SiegeTower": None, "Position": Point3D()}
def CalculateNormalVector(self, index=0): # Returns the wall exterior normal angle # Due walls are ever perpendicular to terrain, we can calculate it in 2D v = Vector2D() v.CreateFrom2Points(self.GetStartPosition(), self.GetEndPosition()) normal = Vector2D(v.val[1], -v.val[0]) normal.Normalize() # Redundant self.__normalVector = Vector3D(normal.val[0], normal.val[1], 0.0)
def GetBestTurtleBattalion(self, target, siegetowerpos, castle): # Returns the best battalion to be converted to a siege tower turtle # Return the closest battalion to given moat target position and siegetower position. The later condition helps to get a good battalion placement if the avaiable # battallions are too far from the siege tower (helping to get a more straight path to the goal) # Only those battalions that the path from their positions and target dont intersect with the castle, are considered minDist = -1 closest = None b = self.__battalions["Infantry"] if (b == None): return None else: for bb in b.battalions: if ( not bb.IsDefeated() ): # Be aware. Due this method can be called meanwhile a battalion is killed, we have to check if they are defeated com = bb.GetCommand() if (com.IsMove() and not com.IsCoverMoat()): center = bb.GetCenterPosition() dist = center.Distance(target) # Check if turtle path to target intersects the castle (so the turtle is stupid and cannot take a good path avoiding obstacles ..., yeah, another one TODO) ray = Ray(origin=center, direction=Vector3D().CreateFrom2Points( center, target)) if (not castle.RayHitTest_Closest( ray=ray, exclude=None, distance=dist)): dist2 = center.Distance(siegetowerpos) d = dist + dist2 if ((closest == None) or (d < minDist)): minDist = d closest = bb return closest
def JoinShape(self, obj, invert=False): # Creates and join current shape with given object's shape # Note the order of objects: "Current wall is going to be joined with given object". This means that only is calculated the ending wall join # The invert flag changes this behaviour (only for towers -> TODO: Also for walls) factory = ConstructionFactory() if obj == None: # None object to join. Constructs a simple rectangular wall shape del self._shape2D[0:len(self._shape2D)] l = self.GetLength() vector = Vector2D() vector.CreateFrom2Points(self.GetStartPosition(), self.GetEndPosition()) tvector = vector.Copy() tvector.Rotate(-90) p = self.GetStartPosition().Copy() p.Move(tvector, self._thickness / 2) self._shape2D.append(p.Copy()) p.Move(vector, l) self._shape2D.append(p.Copy()) p.Move(tvector, -self._thickness) self._shape2D.append(p.Copy()) p.Move(vector, -l) self._shape2D.append(p.Copy()) return else: # Updates the adjacencies if invert: self._adjacentConstructions[0] = obj obj._adjacentConstructions[1] = self else: self._adjacentConstructions[1] = obj obj._adjacentConstructions[0] = self if factory.IsWall(obj): # Calculate the bisector between both walls bisecang = self.GetBisector(obj) # The join point will be along the bisector d = (self._thickness / 2.0) / math.cos( math.radians(bisecang["angle"] / 2.0)) # WARNING: We consider all walls of same thickness p = self.GetEndPosition().Copy() p.Move(bisecang["bisector"], d) self._shape2D[1] = p.Copy() obj._shape2D[0] = p.Copy() p.Move(bisecang["bisector"], -(d * 2.0)) self._shape2D[2] = p.Copy() obj._shape2D[3] = p.Copy() self._adjacentConstructions[1] = obj obj._adjacentConstructions[0] = self elif factory.IsSquaredTower(obj): # Calculate the intersection points of wall shape with tower # We must consider the invert flag. If it is true, the joined part will be the starting wall. Otherwise, the ending wall if invert: v = Vector3D().SetFrom2D(self.GetWallVector()) v.Invert() ray1 = Ray(origin=Point3D().SetFrom2D(self._shape2D[1]), direction=v) ray2 = Ray(origin=Point3D().SetFrom2D(self._shape2D[2]), direction=v) else: v = Vector3D().SetFrom2D(self.GetWallVector()) ray1 = Ray(origin=Point3D().SetFrom2D(self._shape2D[0]), direction=v) ray2 = Ray(origin=Point3D().SetFrom2D(self._shape2D[3]), direction=v) int1 = obj.RecieveImpact(ray1) int2 = obj.RecieveImpact(ray2) if int1 != None: if invert: self._shape2D[0] = Point2D().SetFrom3D(int1) else: self._shape2D[1] = Point2D().SetFrom3D(int1) if int2 != None: if invert: self._shape2D[3] = Point2D().SetFrom3D(int2) else: self._shape2D[2] = Point2D().SetFrom3D(int2) # Reshape the wall axis (and all dependant data). Get the medium points of side shape segments mp1 = Segment2D(self._shape2D[0], self._shape2D[3]).GetMidPoint() mp2 = Segment2D(self._shape2D[1], self._shape2D[2]).GetMidPoint() self.SetPosition(mp1, mp2, reshape=False) elif factory.IsRoundedTower(obj): # Calculate the intersection points of wall shape with tower # We must consider the invert flag. If it is true, the joined part will be the starting wall. Otherwise, the ending wall if invert: v = Vector3D().SetFrom2D(self.GetWallVector()) v.Invert() ray1 = Ray(origin=Point3D().SetFrom2D(self._shape2D[1]), direction=v) ray2 = Ray(origin=Point3D().SetFrom2D(self._shape2D[2]), direction=v) else: v = Vector3D().SetFrom2D(self.GetWallVector()) ray1 = Ray(origin=Point3D().SetFrom2D(self._shape2D[0]), direction=v) ray2 = Ray(origin=Point3D().SetFrom2D(self._shape2D[3]), direction=v) int1 = obj.RecieveImpact(ray1) int2 = obj.RecieveImpact(ray2) if int1 != None: if invert: self._shape2D[0] = Point2D().SetFrom3D(int1) else: self._shape2D[1] = Point2D().SetFrom3D(int1) if int2 != None: if invert: self._shape2D[3] = Point2D().SetFrom3D(int2) else: self._shape2D[2] = Point2D().SetFrom3D(int2) # Reshape the wall axis (and all dependant data). Get the medium points of side shape segments mp1 = Segment2D(self._shape2D[0], self._shape2D[3]).GetMidPoint() mp2 = Segment2D(self._shape2D[1], self._shape2D[2]).GetMidPoint() self.SetPosition(mp1, mp2, reshape=False) elif factory.IsBastion(obj): # Fit the wall to the bastion if invert: self._shape2D[3] = obj.GetEndPosition(exterior=False) self._shape2D[0] = obj.GetEndPosition(exterior=True) else: self._shape2D[1] = obj.GetStartPosition(exterior=True) self._shape2D[2] = obj.GetStartPosition(exterior=False) # Reshape the wall axis (and all dependant data). Get the medium points of side shape segments mp1 = Segment2D(self._shape2D[0], self._shape2D[3]).GetMidPoint() mp2 = Segment2D(self._shape2D[1], self._shape2D[2]).GetMidPoint() self.SetPosition(mp1, mp2, reshape=False)
def __GetVector3D(self): return Vector3D().CreateFrom2Points(self.__GetStartLine3D(), self.__GetEndLine3D())
def GetBestTileToShoot(self, frompos): # Returns the tile that is the best suitable tile to shoot from given position # Due the goal is to create a gateway for the soldiers, the cannons try to shoot at higher wall positions. Due all tiles are placed at same xy pos, get the best # tile "column", and then the highest one pos2D = Point2D().SetFrom3D(frompos) ret = {"row": -1, "column": -1} maxD = -1.0 normal = self.__construction.GetNormalVector() # Do a two pass search. First, search for the best tile to shoot without any hole at left or right. Second, just search the best tile to shoot if previous search failed # This avoid launching rubble to left and right of already holes. Otherwise, the rubble could cover the hole, and make it higer end = False firstpass = True while (not end): j = 0 while (j < self.__tilesSize["columns"]): # Check for rubble status nrubbletiles = self.GetNRubbleCoveredTiles(j) # Check if there are any tile to shoot i = nrubbletiles found = False if (firstpass): # Check for left and right hole condition while (not found and (i < self.__tilesSize["rows"])): if (i > 0): left = self.__tiles[i - 1][j].IsHole() else: left = False center = self.__tiles[i][j].IsHole() if (i < (self.__tilesSize["rows"] - 1)): right = self.__tiles[i + 1][j].IsHole() else: right = False if (left or right or center): i += 1 else: found = True else: while (not found and (i < self.__tilesSize["rows"])): if (not self.__tiles[i][j].IsHole()): found = True else: i += 1 if (found): center = self.GetTileCenter(self.__tiles[i][j]) # Because we don't worry about the cannon height, calculate the distances only in 2D # TODO: Calculate it in 3D space to be more accurate center2D = Point2D().SetFrom3D(center) dist = center2D.Distance(pos2D) invec = Vector3D() invec.CreateFrom2Points(Point3D().SetFrom2D(center2D), Point3D().SetFrom2D(pos2D)) anglefactor = invec.DotProd(normal) # Greater distance -> less effective # Near to wall normal -> more effective factor = anglefactor / dist if (factor > maxD): maxD = factor ret["column"] = j j += 1 if (maxD != -1): end = True # Found the best one (first or second pass) else: if (firstpass): firstpass = False # Not found. Launch the second pass else: end = True # Not found. End of second pass # Check another amazing case (no wall!) if (maxD == -1): return None else: # Get the tallest row. Remember to consider the holes and the rubble nrubbletiles = self.GetNRubbleCoveredTiles(ret["column"]) i = int(self.__tilesSize["rows"] - 1) while ((i >= nrubbletiles) and self.__tiles[i][ret["column"]].IsHole()): i -= 1 ret["row"] = i return self.__tiles[ret["row"]][ret["column"]]
def ShootToBattlefield(self, battlefield, frompos, targetpos): # Shoots to battlefield. Usually, by a cannon # The targetpos parameter is where attacker points. But the precision factor can modify it # Returns a dictionary with the hit result and final cell -> {"Hit": True/False, "Intersection": Point3D(), "Cell": GroundCell} ret = {"Hit": False, "Intersection": Point3D(), "Cell": None} # Aiming consideration: We are going to calculate a shoot in a direction sampled by a cosinus distribution # From the aiming point of view, the shooter points to a far point, and if accuracy factor is 100%, it should hit the target. # So, the sphere used to sample the cosinus distribution should has the target distance as radius. # Then, we can use the accuracy factor to get a percentage of shooting distance. With this consideration, bad accuracy factors means # sampling on small spheres, so the shoot can fail dist = frompos.Distance(targetpos) accuracy = dist * self.__accuracyFactor / 100.0 direction = Vector3D() direction.CreateFrom2Points(frompos, targetpos) sph = Sphere(position=frompos, radius=accuracy) v = sph.GetRayCosine(direction) # Check hit on the battlefield ray = Ray(origin=frompos, direction=v, energy=self.__attack) hitpoint = battlefield.RayIntersects(ray) if (hitpoint != None): ret["Intersection"] = hitpoint.Copy() ret["Cell"] = battlefield.GetCellFromPoint(hitpoint) ret["Battalion"] = None if (not ret["Cell"].HasBattalion()): ret["Hit"] = False else: battalion = ret["Cell"].GetBattalion() ret["Battalion"] = battalion # If battlefield battalion has only 1 unit (cannons, siege towers), the method will be the classic one (attack against defense) # Otherwise, we have to calculate the number of killed units into the battalion if (battalion.GetNumber() == 1): if (self.ShootDuel(battalion)): ret["Hit"] = True battalion.Kill(1) else: ret["Hit"] = False else: # Shooting against battlefield is usually done by cannons. The main difference is that a cannon ball can kill more than one unit. To know how many units are killed # we follow the next algorithm. # When a cannon shoot hits on a cell, and considering that a cannon ball doesnt explode, the ball go trough the cell until it touch the ground. If any soldier is in its # path, dies. But we dont have any predefined soldier position inside the battalion. To solve it, we are going to work with densities. # We can get the cell soldiers density with # celldensity = battalion_size / cell_volume # The cannon ball path inside the cell can be modelled as a cylinder. So, # nhits = celldensity * cylinder_volume # But this has a problem, so we are considering that each unit has the same volume in the battlefield cell, and this is not true (think on a cannon) # So we need to consider the occupied volume ratio. Then # cellratio = (battalion_size * soldier_volume) / cell_volume # Because we want to keep the same ratio for cylinder, we get # (nkills * soldier_volume) = cellratio * cylinder_volume # nkills = battalion_size * cylinder_volume / cell_volume bbox = ret["Cell"].GetBoundingBox() ray.Reset() if (ray.HitBox(bbox)): clength = ret["Intersection"].Distance( ray.GetHitPoint()) bvol = bbox.GetVolume() if (bvol == 0): ret["Hit"] = False # Some battalions can have 0 height, such are the siege towers when are in construction phase else: svolume = battalion.GetSoldierBoxVolume() cellratio = (battalion.GetNumber() * svolume) / bvol cvolume = Battles.Utils.Settings.SETTINGS.Get_F( 'Army', 'Cannons', 'BallRadius' ) * Battles.Utils.Settings.SETTINGS.Get_F( 'Army', 'Cannons', 'BallRadius') * math.pi * clength nhits = (cellratio * cvolume) / svolume if (nhits >= 1): battalion.Kill(math.ceil(nhits)) ret["Hit"] = True else: ret["Hit"] = False else: ret["Hit"] = False return ret
def InAttackRange(self, currPos, targetPos, castle, constructionTarget, excludeConstruction=None): # Returns true if targetPos is in attack range from currPos # Given castle is used to check if shoot should intersect with any castle part, returning False. # If the attacker aim to any castle battalion, constructionTarget object is used to discard the target's construction from intersection test # If the attacker shoots from a construction (its a defender), excludeConstruction is used to avoid the castle self intersection check # Check the distance #dist = currPos.Distance(targetPos) #dist = math.sqrt(((currPos.x - targetPos.x)**2) + ((currPos.y - targetPos.y)**2)) #dist = math.hypot(currPos.x - targetPos.x, currPos.y - targetPos.y) dist = math.sqrt(((currPos.x - targetPos.x)**2) + ((currPos.y - targetPos.y)**2) + ((currPos.z - targetPos.z)**2)) if (dist > self.__distance): return False else: # Check the attack direction and angle if (not self.__attackVector.IsNull()): if (not self.__attackAngle): return False # Check horizontal angle vec = Vector2D() vec.val[0] = targetPos.x - currPos.x vec.val[1] = targetPos.y - currPos.y l = math.hypot(vec.val[0], vec.val[1]) if (l == 0): vec.val[0] = 0 vec.val[1] = 0 else: vec.val[0] /= l vec.val[1] /= l dotprod = (self.__attackVector.val[0] * vec.val[0]) + ( self.__attackVector.val[1] * vec.val[1]) if (dotprod > 1): dotprod = 1 if (dotprod < -1): dotprod = -1 ang = math.degrees(math.acos(dotprod)) if (ang > (self.__attackAngle['H'] / 2.0)): return False else: # Check the vertical angle vec2 = Vector2D() vec2.val[0] = math.fabs(targetPos.x - currPos.x) vec2.val[1] = targetPos.z - currPos.z l2 = math.hypot(vec2.val[0], vec2.val[1]) if (l2 == 0): vec2.val[0] = 0 vec2.val[1] = 0 else: vec2.val[0] /= l2 vec2.val[1] /= l2 dotprod2 = vec2.val[ 0] # The reference vector is a ground parallel vector, that is <1,0>, so the dotprod can be simplified if (dotprod > 1): dotprod = 1 if (dotprod < -1): dotprod = -1 ang2 = math.degrees(math.acos(dotprod2)) if (((targetPos.z <= currPos.z) and (ang2 > self.__attackAngle['V']['bottom'])) or ((targetPos.z > currPos.z) and (ang2 > self.__attackAngle['V']['top']))): return False attackvec = Vector3D().CreateFrom2Points(currPos, targetPos) if (constructionTarget != None): # Check the castle intersection when the attacker aims to the castle r = Ray(origin=currPos, direction=attackvec) # Check the ray intersection on all castle parts and get the closest (the first intersection) # If the closest is not the target, means that is occluded if (not constructionTarget.RayHitTest(r)): return False else: distr = r.GetLength() r.Reset( ) # Resets the previous hitpoint calculated in constructionTarget.RayHitTest(r) if (castle.RayHitTest_Closest(ray=r, exclude=constructionTarget, distance=distr) != None): return False else: return True if (excludeConstruction != None): # Check the castle intersection when the attacker shoots from the castle r = Ray(origin=currPos, direction=attackvec) if (castle.RayHitTest(r, excludeConstruction)): distr = r.GetLength() if (distr < dist): return False else: return True else: return True """ constr = castle.RayHitTest_Closest(ray = r, exclude = excludeConstruction, distance = r.GetLength()) if (constr != None): # We have to check if the intersection is done against an exterior wall or not # The battalion placement could be under or behind the wall factory = ConstructionFactory() if (factory.IsWall(constr)): # The most easy way to check it is to compare the wall normal vector and attack vector to check if both are visible wnorm = constr.GetNormalVector() if (wnorm.DotProd(attackvec) > 0): # Not visible. This should be the wall back return False else: return True # Front wall hit else: return False # Tower hit else: return True # None castle part occludes the shot """ return True