"""Tower module - tower placement, targeting, shooting, and rendering.""" import math import pygame as pg from game.util import tile_to_pixel class Tower(pg.sprite.Sprite): # pylint: disable=too-many-instance-attributes """A player-placed tower that targets and damages nearby enemies. Attributes: type (str): Asset key identifying the tower variant. range (float): Attack radius in world-space pixels. level (int): Current upgrade level. selected (bool): Whether the tower is selected (draws range circle). angle (float): Current facing angle in degrees. target (Enemy | None): The enemy currently being targeted, or ``None``. fire_rate (int): Frames between shots. fire_cooldown (int): Frames remaining until the next shot. damage (float): Damage dealt per shot. pos (tuple): World-space pixel centre position. tile_pos (tuple): Grid position the tower occupies. """ def __init__(self, game, pos, tile_pos, type, range, level=1): super().__init__() self.game = game self.type = type self.image = self.game.assets[type] self.rect = self.image.get_rect() self.rect.center = pos self.range = range self.selected = False self.angle = 0 self.target = None self.fire_rate = 180 # frames between shots — tune to taste self.fire_cooldown = 0 self.damage = 50 self.pos = pos self.tile_pos = tile_pos self.level = level self.action = "" self.set_action('shoot') def update(self, enemy_group): """Advance animation, pick a target, rotate toward it, and shoot if ready. Args: enemy_group (EnemyGroup -> SpriteGroup): All active enemies to consider as targets. """ self.animation.update() self.pick_target(enemy_group) self.rotate() if self.fire_cooldown > 0: self.fire_cooldown -= 1 if self.target is not None and self.fire_cooldown == 0: print("shoot fired") self.shoot() def set_action(self, action): """Switch the active animation if it differs from the current one. Args: action (str): Animation key (e.g. ``"shoot"``). """ if action != self.action: self.action = action self.animation = self.game.assets[self.type + '/' + self.action].copy() def render(self, surf, camera): """Draw the tower animation frame, rotated and scaled to the camera zoom. Also draws a semi-transparent range circle when the tower is selected. Args: surf (pg.Surface): Target surface to draw onto. camera (Camera): Active camera for zoom and coordinate conversion. """ screen_pos = camera.world_to_screen(pg.Vector2( tile_to_pixel(self.tile_pos, self.game.tilemap.tile_size, True) )) anim_img = self.animation.img() rotated_img = pg.transform.rotate(anim_img, self.angle) scaled_image = pg.transform.scale( rotated_img, ( int(rotated_img.get_width() * camera.zoom), int(rotated_img.get_height() * camera.zoom) ) ) self.rect = scaled_image.get_rect(center=screen_pos) if self.selected: #draw circle around tower if selected scaled_range = int(self.range * camera.zoom) range_surf = pg.Surface((scaled_range * 2, scaled_range * 2), pg.SRCALPHA) pg.draw.circle( range_surf, (200, 200, 200, 100), (scaled_range, scaled_range), scaled_range ) range_rect = range_surf.get_rect(center=screen_pos) surf.blit(range_surf, range_rect) surf.blit(scaled_image, self.rect) def rotate(self): """Face the tower toward its current target, or hold the last angle if no target.""" if self.target is None: # print("no target") return # keep last angle, or reset to a default — your call tower_world_pos = pg.Vector2( tile_to_pixel(self.tile_pos, self.game.tilemap.tile_size, True) ) direction = self.target.pos - tower_world_pos self.angle = math.degrees(math.atan2(-direction[1], direction[0])) -90 # print(f"angle {self.angle}") def pick_target(self, enemy_group): """Select the closest enemy within range as the current target. Sets ``self.target`` to ``None`` if no enemy is in range. Args: enemy_group (EnemyGroup): All active enemies to search through. """ closest = None closest_dist = float('inf') tower_world_pos = pg.Vector2( tile_to_pixel(self.tile_pos, self.game.tilemap.tile_size, True) ) for enemy in enemy_group: distance = tower_world_pos.distance_to(enemy.pos) if distance <= self.range and distance < closest_dist: closest = enemy closest_dist = distance self.target = closest # explicitly None if nothing in range def shoot(self): """Deal damage to the current target, play the shoot animation, and reset cooldown.""" self.target.take_damage(self.damage) self.animation.play_animation() self.fire_cooldown = self.fire_rate class TowerGroup(pg.sprite.Group): """Sprite group that delegates rendering and updates to each tower.""" def render(self, surf, camera): """Render all towers in the group. Args: surf (pg.Surface): Target surface to draw onto. camera (Camera): Active camera for zoom and coordinate conversion. """ for tower in self.sprites(): tower.render(surf, camera) def update(self, enemies): """Update all towers in the group. Args: enemies (EnemyGroup): All active enemies, passed to each tower's update. """ for tower in self.sprites(): tower.update(enemies)