Skip to main content

Terminals & Enemies

game/terminals.py provides two sprite classes: Terminal (interactive level objectives) and Enemy (virus sprites that chase the player).

Terminal class

A stationary cyberpunk terminal that the player can interact with when in range. Terminals serve as level objectives in Levels 2 and 3.
class Terminal(pygame.sprite.Sprite):
    def __init__(self, x: int, y: int,
                 terminal_type: str = "scan",
                 title: str = "SYSTEM SCANNER") -> None
    def is_near(self, player) -> bool
    def update(self, player) -> None
    def draw(self, screen: pygame.Surface) -> None

Attributes

AttributeTypeDefaultDescription
terminal_typestr"scan"Semantic type tag. Used by the main loop to distinguish "network" and "quarantine" terminals.
titlestr"SYSTEM SCANNER"Display name shown in the interaction prompt.
activeboolFalseSet to True automatically when the player is within interaction_range.
interaction_rangeint80Proximity threshold in pixels.
imagepygame.Surface64×48 SRCALPHACyberpunk terminal icon with a blue border and screen lines.
rectpygame.RectCentred at (x, y)Collision and positioning rect.

__init__(x, y, terminal_type, title)

x
int
required
Horizontal centre position of the terminal sprite in screen coordinates.
y
int
required
Vertical centre position of the terminal sprite in screen coordinates.
terminal_type
str
default:"scan"
Semantic identifier for the terminal. The main game uses "network" for the Level 2 objective and "quarantine" for the Level 3 objective.
title
str
default:"SYSTEM SCANNER"
Human-readable label displayed above the terminal when the player is in range, e.g. "NETWORK NODE" or "QUARANTINE TERMINAL".
The sprite surface is 64×48 pixels with pygame.SRCALPHA. It renders a blue rounded rectangle border (0, 150, 255), a dark-blue fill (0, 50, 100), and two horizontal cyan lines (0, 200, 255) to simulate a terminal screen.

is_near(player)

Returns True if the Euclidean distance between the terminal centre and player.pos is less than interaction_range (80 px).
def is_near(self, player) -> bool:
    dist = pygame.Vector2(self.rect.center).distance_to(player.pos)
    return dist < self.interaction_range
player
Player
required
The player sprite instance. Must expose a pos attribute of type pygame.Vector2.
return
bool
True when the player is within 80 px of the terminal centre.

update(player)

Called each frame by the terminals sprite group. Sets self.active = self.is_near(player). When active becomes True, the next draw() call shows the interaction prompt and a proximity circle.
player
Player
required
The current player sprite. Passed through to is_near().

draw(screen)

Blits the terminal image and, if active, overlays:
  • A yellow "PRESS E: {title}" prompt in 16 pt Consolas, positioned 10 px above the top of the terminal rect.
  • A single-pixel cyan circle of radius interaction_range centred on the terminal to visualise the interaction zone.
screen
pygame.Surface
required
The active Pygame display surface.
In the main game loop, the interaction prompt is also re-rendered with the screen-shake offset applied. The Terminal.draw() version is kept for correctness, but the shaken version in main_game.py takes visual precedence.

Terminal instances in the game

InstancePositionterminal_typetitleActive level
network_terminal(3w/4, h/4)"network""NETWORK NODE"2
quarantine_terminal(w/2, 3h/4)"quarantine""QUARANTINE TERMINAL"3

Enemy class

A hostile virus sprite that pursues the player each frame. Contact with the player deals 15 damage and removes the enemy.
class Enemy(pygame.sprite.Sprite):
    def __init__(self, x: float, y: float, enemy_type: str = "virus") -> None
    def update(self, player_pos: pygame.Vector2) -> None
    def draw(self, screen: pygame.Surface) -> None

Attributes

AttributeTypeDefaultDescription
pospygame.Vector2(x, y)Sub-pixel position, updated each frame.
speedint2Movement speed in pixels per frame.
healthint30Hit points. Enemies are removed instantly on bullet or player contact (no health deduction mechanic is currently used).
imagepygame.Surface24×24 SRCALPHAGlowing red triangle icon.
rectpygame.RectCentred at (x, y)Collision and positioning rect.

__init__(x, y, enemy_type)

x
float
required
Horizontal spawn position. Can be any float; the rect is centred on this value.
y
float
required
Vertical spawn position.
enemy_type
str
default:"virus"
Reserved for future enemy variants. Currently only "virus" is used.
The sprite is a 24×24 surface. A red triangle (255, 50, 50) is drawn as a 2-px-stroke polygon with vertices at (12, 0), (0, 24), (24, 24). A darker red (100, 0, 0) solid inner triangle is inset by 4 px to create a glowing-border effect.

update(player_pos)

Moves the enemy toward the player at self.speed pixels per frame.
def update(self, player_pos: pygame.Vector2) -> None:
    direction = (player_pos - self.pos)
    if direction.length() > 0:
        direction = direction.normalize()
        self.pos += direction * self.speed
        self.rect.center = self.pos
player_pos
pygame.Vector2
required
The current world-space position of the player. Passed by the main loop as player.pos.
The direction vector is normalised before scaling, so the enemy always moves at exactly self.speed px/frame regardless of distance. When the enemy collides with a wall, the main loop pushes it back by 2 px in the opposite direction.

Spawning behaviour

Enemies are spawned by the main loop under two concurrent conditions:
  • General spawner — when len(enemies) < (4 + level * 2), there is a 2% spawn chance per frame at a random position at least 300 px from the player. This cap grows with level (Level 1: 6, Level 2: 8, Level 3: 10, Level 4: 12).
  • Level 1 extra spawner — while scan_progress < 100, an additional check spawns up to 6 enemies at 8% probability, at fully random screen positions with no minimum-distance check.
  • Level 4 explicit cap — an additional check caps Level 4 spawns at 8 with a 5% spawn chance.
Both conditions run every frame, so the effective enemy count in Level 1 can reach 6 from either spawner, and in Level 4 the explicit cap of 8 applies via a separate check.

Build docs developers (and LLMs) love