class SimpleTower(AbstractTower): """A simple tower with short range that rotates towards enemies and shoots projectiles at them""" name = 'Simple Tower' colour = '#E94A1F' # Coquelicot range = CircularRange(1.5) cool_down_steps = 0 base_cost = 30 level_cost = 20 rotation_threshold = (1 / 6) * math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=5, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) def step(self, data): """Rotates toward 'target' and attacks if possible""" self.cool_down.step() target = self.get_unit_in_range(data.enemies) if target is None: return angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle if partial_angle == angle: target.damage(self.get_damage(), 'projectile')
class EnergyTower(SimpleTower): """A tower that deals energy damage""" name = "Energy Tower" range = CircularRange(1.5) cool_down_steps = 0 base_cost = 70 level_cost = 50 colour = 'yellow' rotation_threshold = (1 / 6) * math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=7, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) def step(self, data): """Rotates toward 'target' and attacks if possible""" self.cool_down.step() target = self.get_unit_in_range(data.enemies) if target is None: return angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle if partial_angle == angle: target.damage(self.get_damage(), 'energy')
class EnergyTower(AbstractTower): """Short range engergy tower""" name = 'Energy Tower' colour = 'yellow' range = CircularRange(4.0) cool_down_steps = 0 base_cost = 25 level_cost = 15 rotation_threshold = (1/10)*math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=4, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) def step(self, data): """Rotates toward 'target' and attacks if possible""" self.cool_down.step() target = self.get_unit_in_range(data.enemies) if target is None: return angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle if partial_angle == angle: target.damage(self.get_damage(), 'energy')
class IceTower(AbstractTower): """A tower that slows SimpleEnemy, kills FireEnemy, doesn't do anything to WoodEnemy. When upgraded, IceTower does damage to SimpleEnemy as well.""" name = 'Ice Tower' colour = 'cyan' cool_down_steps = 5 base_cost = 60 level_cost = 45 base_damage = 150 def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=150, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) self.upgraded = False range = CircularRange(1.3) def get_fire_targets(self, units): '''Gets all enemies in range''' target = [] count = 1 try: next_target = self.get_units_in_range(units.enemies) target.append(next_target.__next__()) while count < 15: i = len(target) new_target = next_target.__next__() if new_target not in target: target.append(new_target) if i == len(target): break count += 1 except StopIteration: return target return target def step(self, units): """Deals damage to nearby FireEnemies, slows SimpleEnemies""" self.cool_down.step() if not self.cool_down.is_done(): return target = self.get_fire_targets(units) if target == []: return for enemy in target: if(enemy.name == 'Fire Enemy' and not enemy.slowed): enemy.slowed = True enemy.damage(self.get_damage(), 'projectile') elif(enemy.name == 'Fire Enemy' and enemy.slowed): enemy.damage(self.get_damage(), 'projectile') elif (enemy.name == 'Simple Enemy' and self.upgraded): enemy.damage(15, 'projectile') elif (enemy.grid_speed == 5/60 and enemy.name == 'Simple Enemy'): enemy.grid_speed *= 0.80 self.cool_down.start()
class SlowTower(AbstractTower): """Task 3.1 (Advanced Tower): A tower that slows enemies in the vicinity""" name = 'Slow Tower' colour = '#000000' cool_down_steps = 20 # expensive because the effectiveness increases exponentially with level/number of towers base_cost = 400 level_cost = 300 rotation_threshold = (1 / 6) * math.pi range = CircularRange(1.5) def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=50, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) # contains all enemies slowed by this tower along with their speed multiplier self._enemies_slowed = [] # contains all enemies that have been restored to their normal speeds self._enemies_restored = [] # effect of multiple tower = 0.7**(number of towers) # we want effect of level up < effect of extra towers self._speed_multiplier = 0.7**(0.5 * self.level) def step(self, data): self.cool_down.step() # remove the slow speeds effect on enemies once they have left the tower's range for tup in self._enemies_slowed: enemy = tup[0] if not self.is_position_in_range( enemy.position) and enemy not in self._enemies_restored: enemy.grid_speed = enemy.grid_speed / tup[1] self._enemies_restored.append(enemy) # slow the enemies' speeds to self._speed_multiplier% of normal enemies = self.get_units_in_range(data.enemies, limit=10000) for enemy in enemies: enemies_slowed_only = [] for tup in self._enemies_slowed: enemies_slowed_only.append(tup[0]) if not enemy in enemies_slowed_only: enemy.grid_speed = enemy.grid_speed * self._speed_multiplier self._enemies_slowed.append((enemy, self._speed_multiplier))
class EnerygyTower(SimpleTower): """ A Energy Tower only attack energy enemy """ name = 'Energy Tower' range = CircularRange(2.5) cool_down_steps = 2 base_cost = 60 level_cost = 50 colour = 'orange' rotation_threshold = (1 / 3) * math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=10, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) self._target: EnergyEnemy = None def step(self, data): """Rotates toward 'target' and attacks if possible""" self.cool_down.step() target = self.get_unit_in_range(data.enemies) "only attack energy enemies" for enemy in self.get_units_in_range(data.enemies): if not enemy.name == "Energy Enemy": return else: target = enemy break if target is None: return angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle if partial_angle == angle: target.damage(self.get_damage(), 'energy')
class EnergyTower(SimpleTower): """An energy tower with short range that rotates towards enemies and shoots energy at them. Attacks custom enemy immune to projectile and missile """ #Set name and colour,range,cooldown and cost value name = 'Energy Tower' colour = '#FFFF00' #Yellow range = CircularRange(2) cool_down_steps = 5 base_cost = 40 level_cost = 50 rotation_threshold = (1 / 6) * math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=2, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) """Initialise the tower""" self._target: EnergyEnemy = None def step(self, data): """Rotates toward 'target' and attacks if possible""" self.cool_down.step() #If no enemy in range does nothing if self.get_unit_in_range(data.enemies) is None: return #If energy enemy is in range it faces and damages it target = self.get_unit_in_range(data.enemies) angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle if partial_angle == angle: if target.name == "Energy Enemy": target.damage(self.get_damage(), 'energy') else: return
class SimpleTower4(SimpleTower): """A fully upgraded simple tower with both improved range and damage that rotates towards enemies and shoots projectiles at them""" name = 'Simple Tower v4' colour = 'purple' range = CircularRange(5) cool_down_steps = 0 base_cost = 80 level_cost = 15 rotation_threshold = (1 / 6) * math.pi earn_coins = 0 def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=8, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level)
class SimpleTower3(SimpleTower): """Another variation of the upgraded simple tower with greater damage capacity that rotates towards enemies and shoots projectiles at them""" name = 'Simple Tower v3' colour = 'red' range = CircularRange(1.5) cool_down_steps = 0 base_cost = 40 level_cost = 15 rotation_threshold = (1 / 6) * math.pi earn_coins = 0 def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=8, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level)
class SimpleTower2(SimpleTower): """A variation of upgraded simple tower with improved range that rotates towards enemies and shoots projectiles at them""" name = 'Simple Tower v2' colour = 'dark red' range = CircularRange(5) # Increased range cool_down_steps = 0 base_cost = 40 level_cost = 15 rotation_threshold = (1 / 6) * math.pi earn_coins = 0 def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=1, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level)
class CustomTower(SimpleTower): """A custom tower with short range that rotates towards energy enemies.""" name = 'Energy Tower' colour = '#FEC40a' range = CircularRange(1.5) cool_down_steps = 0 base_cost = 40 level_cost = 30 rotation_threshold = (1 / 6) * math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=5, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) def step(self, data): """Rotates toward 'target' and attacks if possible""" self.cool_down.step() target = self.get_unit_in_range(data.enemies) if target is None: return # check if the enemy is custom enemy. for enemy in self.get_units_in_range(data.enemies): if not enemy.name == "Energy Enemy": return else: target = enemy break angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle # use energy to attack enemies. if partial_angle == angle: target.damage(self.get_damage(), 'energy')
class IceTower(SimpleTower): """ A spcecial tower which can slightly slow the speed of the enemy, the most expensive tower in this game, which can take significant affect """ name = 'Ice Tower' cool_down_steps = 5 base_cost = 100 level_cost = 150 range = CircularRange(2.5) colour = "#A7B6F2" rotation_threshold = (1 / 3) * math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=0, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) self._target: AbstractEnemy = None def step(self, data): """Rotates toward 'target' and attacks if possible""" self.cool_down.step() target = self.get_unit_in_range(data.enemies) if target is None: return angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle if partial_angle == angle: target.damage(self.get_damage(), 'ice')
class SlowTower(SimpleTower): """A simple tower with short range that rotates towards enemies and slows them down""" name = 'Slow Tower' colour = '#8B8D7A' #Stone Gray range = CircularRange(2) cool_down_steps = 5 base_cost = 200 level_cost = 50 rotation_threshold = (1 / 6) * math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=0, level: int = 1): super().__init__(cell_size, grid_size, rotation, base_damage, level) """Initialises the tower""" self._target: EnergyEnemy = None def step(self, data): """Rotates toward 'target' and attacks if possible""" self.cool_down.step() if self.get_unit_in_range(data.enemies) is None: return target = self.get_unit_in_range(data.enemies) angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle if partial_angle == angle: target.damage(self.get_damage(), 'slow') else: return
class LaserTower(SimpleTower): """A tower that fires lasers at a target""" name = 'Laser Tower' colour = '#6600FF' # purple-ish cool_down_steps = 1 #insanely fast base_cost = 500 level_cost = 400 range = CircularRange(4) rotation_threshold = (1 / 3) * math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=2, level: int = 1): super().__init__(cell_size, grid_size=grid_size, rotation=rotation, base_damage=base_damage, level=level) self._target: AbstractEnemy = None def _get_target(self, units) -> Union[AbstractEnemy, None]: """Returns previous target, else selects new one if previous is invalid Invalid target is one of: - dead - out-of-range - off the map Return: AbstractEnemy: Returns previous target, unless it is non-existent or invalid (see above), Otherwise, selects & returns new target if a valid one can be found, Otherwise, returns None """ if self._target is None \ or self._target.is_dead() \ or not self.is_position_in_range(self._target.position) \ or self._target.position[0] > 400: self._target = self.get_unit_in_range(units) return self._target def step(self, units): """Rotates toward 'target' and fires laser if possible""" self.cool_down.step() target = self._get_target(units.enemies) # if there's no target or if the target is out of the map if target is None: return None # Rotate toward target angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle if angle != partial_angle or not self.cool_down.is_done(): return None self.cool_down.start() # Spawn laser on tower laser = Laser(self.position, self.cell_size, target, rotation=self.rotation, damage=self.get_damage(), grid_speed=.5) # Move laser to outer edge of tower radius = self.grid_size[0] / 2 delta = polar_to_rectangular(self.cell_size * radius, partial_angle) laser.move_by(delta) return [laser]
def tower_aging(self): """Make tower lose effectiveness when the tower is aging""" self.colour = "GREY" self.base_cost = 0 self.range = CircularRange(0)
class FireTower(AbstractTower): """An advanced tower that pretty much kills everything""" name = 'Fire Tower' colour = 'Red' # Coquelicot range = CircularRange(1.5) cool_down_steps = 5 base_cost = 100 level_cost = 80 rotation_threshold = (1 / 6) * math.pi def __init__(self, cell_size: int, grid_size=(.9, .9), rotation=math.pi * .25, base_damage=150, level: int = 1): super().__init__(cell_size, grid_size=grid_size, rotation=rotation, base_damage=base_damage, level=level) self._target: AdvancedEnemy = None def _get_target(self, units) -> Union[AdvancedEnemy, None]: if self._target is None \ or self._target.is_dead() \ or not self.is_position_in_range(self._target.position): self._target = self.get_unit_in_range(units) return self._target def step(self, units): """Rotates toward 'target' and fires missile if possible""" self.cool_down.step() target = self._get_target(units.enemies) if target is None: return None # Rotate toward target angle = angle_between(self.position, target.position) partial_angle = rotate_toward(self.rotation, angle, self.rotation_threshold) self.rotation = partial_angle if angle != partial_angle or not self.cool_down.is_done(): return None self.cool_down.start() # Spawn missile on tower fire = Fire(self.position, self.cell_size, target, rotation=self.rotation, damage=self.get_damage(), grid_speed=.1) # Move missile to outer edge of tower radius = self.grid_size[0] / 2 delta = polar_to_rectangular(self.cell_size * radius, partial_angle) fire.move_by(delta) return [fire]