Commit
the-way-out
v0.5.0
modified CHANGELOG.md
@@ -1,5 +1,43 @@
# CHANGELOG
## v0.5.0
Gives every playable character a distinct **signature ability** on a
cooldown, triggered with **Shift** — the combat differentiator for the
v1.0.0 cut. The universal dash is now Shiggy's alone.
### Gameplay
- Each character has one signature ability, fired with Shift on its
own cooldown:
- **Wizard — Slow:** bends time for every enemy, the boss and their
in-flight shots for 3s, while the Wizard keeps moving and firing at
full speed.
- **Shiggy — Dash:** the short, i-framed burst dash every character
shared before this release — now Shiggy's signature alone.
- **Penguin — Shield:** total damage immunity for 2.5s.
- **Elf — Volley:** doubled fire rate for 2s.
- **Wolf — Sprint:** a 1.5s burst of peak movement speed (no
i-frames, full steering — distinct from Shiggy's dash).
- The other four characters no longer have the dash; each is defined
by its own ability instead.
### UI
- The HUD dash ring is now an ability ring: a per-character glyph,
bright while the ability is active, a depleting arc on cooldown.
- Character select shows each character's ability — name and a
one-line description — below the four stat bars.
- Control hints updated from "Shift dash" to "Shift ability".
### Fixes
- macOS system shortcuts (Cmd-Tab, Mission Control, Spaces) no longer
steal focus mid-level. The keyboard is grabbed by the game while a
level is live, so those combos reach the game instead of the OS; the
grab is released in menus, pause and the level-end screen so the
player can always tab away. The in-game Cmd-Q quit still works.
## v0.4.0
Adds the second new built-in level for the v1.0.0 cut: **Level 5 —
modified VERSION
@@ -1 +1 @@
v0.4.0
v0.5.0
modified levels.py
@@ -6,9 +6,10 @@ from collections import deque
import pygame
from settings import (
TILE_SIZE, BOSS_TOUCH_DAMAGE, PLAYER_INVULN_TIME,
SPIKE_DAMAGE, LEVER_REACH, DASH_COOLDOWN,
SPIKE_DAMAGE, LEVER_REACH, SLOW_SCALE,
)
from units import Wizard, Boss, BOSS_ROSTER, CHARACTER_INFO, ENEMY_INFO
from units import (
Wizard, Penguin, Boss, BOSS_ROSTER, CHARACTER_INFO, ENEMY_INFO)
from static_objects import Tile, TileTextures, Prop
from interactables import Spikes, Lever, Gate, KeyItem, PressurePlate
from tiles import PROP_CHARS
@@ -544,8 +545,20 @@ class LevelManager:
if self.completed or self.failed:
return
self.entities.update(dt)
self.projectile_sprites.update(dt)
# Wizard's Slow ability scales the per-frame dt of every enemy,
# the boss and enemy projectiles. self.entities is exactly
# {player} ∪ enemy_sprites, so dispatch per actor instead of one
# group update — the player and his own shots keep raw dt.
slow = (isinstance(self.player, Wizard)
and self.player.ability_active)
dt_eff = dt * SLOW_SCALE if slow else dt
self.player.update(dt)
for enemy in self.enemy_sprites: # generic enemies + boss
enemy.update(dt_eff)
for proj in self.projectile_sprites:
proj.update(dt if proj.owner == 'player' else dt_eff)
# Spikes are a world clock — the Wizard slows enemies, not the
# dungeon — so interactables keep raw dt.
self.interactable_sprites.update(dt)
self.camera.follow(self.player, dt)
@@ -772,6 +785,12 @@ class LevelManager:
for sprite in sorted(self.entities, key=lambda s: s.hitbox.bottom):
self._draw_world_sprite(screen, sprite)
# Penguin's shield has no sprite change of its own — ring the
# player while it holds.
if (isinstance(self.player, Penguin)
and self.player.ability_active):
self._draw_shield_aura(screen)
for proj in self.projectile_sprites:
screen.blit(proj.image, self.camera.world_to_screen(proj.rect))
@@ -779,7 +798,7 @@ class LevelManager:
if self.player is not None and not self.completed:
self.draw_player_health(screen)
self.draw_dash_meter(screen)
self.draw_ability_meter(screen)
if self.needs_key:
self.draw_key_status(screen)
if self.boss is not None:
@@ -848,25 +867,28 @@ class LevelManager:
True, theme.INK)
screen.blit(label, (x, y - 36))
def draw_dash_meter(self, screen):
"""Small ring next to the HP showing dash readiness. Filled
ring = ready (Shift will fire); shrinking arc = cooldown left."""
def draw_ability_meter(self, screen):
"""Small ring next to the HP showing the character's signature
ability. Filled = ready (Shift fires it); bright = active right
now; shrinking arc = cooldown left."""
if self.player is None:
return
radius = 22
cx = 40 + 460 + 36 + radius
cy = self.height - 40 - 34 // 2 - 6
ready = (self.player.dash_cooldown_timer == 0
and self.player.dash_timer == 0)
active = self.player.ability_active
ready = (not active and self.player.ability_cooldown_timer == 0)
# Backplate
pygame.draw.circle(screen, theme.shade(theme.BG, -10),
(cx, cy), radius + 4)
pygame.draw.circle(screen, theme.LINE_C, (cx, cy), radius)
if ready:
if active:
pygame.draw.circle(screen, theme.SUCCESS, (cx, cy), radius - 4)
elif ready:
pygame.draw.circle(screen, theme.ACCENT, (cx, cy), radius - 4)
else:
ratio = 1.0 - (self.player.dash_cooldown_timer
/ max(0.001, DASH_COOLDOWN))
ratio = 1.0 - (self.player.ability_cooldown_timer
/ max(0.001, self.player.ABILITY_COOLDOWN))
# Draw filled wedge from -pi/2 sweeping clockwise.
ring = pygame.Surface((radius * 2 + 4, radius * 2 + 4),
pygame.SRCALPHA)
@@ -883,18 +905,57 @@ class LevelManager:
screen.blit(ring, (cx - radius - 2, cy - radius - 2))
pygame.draw.circle(screen, theme.shade(theme.BG, -6),
(cx, cy), radius, 2)
# Glyph: lightning-style chevron
col = theme.INK if ready else theme.MUTED
pygame.draw.polygon(screen, col, [
(cx - 6, cy - 9), (cx + 3, cy - 2),
(cx - 1, cy - 1), (cx + 6, cy + 9),
(cx - 3, cy + 2), (cx + 1, cy + 1),
])
lit = active or ready
col = theme.INK if lit else theme.MUTED
self._draw_ability_glyph(
screen, cx, cy, self.player.ABILITY_GLYPH, col)
cap = self.label_font.render(
"DASH" if ready else "...", True,
theme.INK if ready else theme.MUTED)
self.player.ABILITY_NAME if lit else "...", True, col)
screen.blit(cap, (cx + radius + 14, cy - 14))
def _draw_ability_glyph(self, screen, cx, cy, kind, col):
"""Tiny vector icon for the ability meter — one per glyph kind
used by the playable roster."""
if kind == "slow": # hourglass
pygame.draw.polygon(screen, col, [
(cx - 7, cy - 9), (cx + 7, cy - 9), (cx, cy)])
pygame.draw.polygon(screen, col, [
(cx - 7, cy + 9), (cx + 7, cy + 9), (cx, cy)])
elif kind == "shield":
pygame.draw.polygon(screen, col, [
(cx, cy - 10), (cx + 8, cy - 5), (cx + 8, cy + 2),
(cx, cy + 10), (cx - 8, cy + 2), (cx - 8, cy - 5)], 2)
elif kind == "rapid": # double chevron
for dx in (-4, 2):
pygame.draw.lines(screen, col, False, [
(cx + dx - 3, cy - 7), (cx + dx + 4, cy),
(cx + dx - 3, cy + 7)], 2)
elif kind == "sprint": # wedge + speed lines
pygame.draw.polygon(screen, col, [
(cx + 1, cy - 8), (cx + 9, cy), (cx + 1, cy + 8)])
for i, dy in enumerate((-5, 0, 5)):
pygame.draw.line(screen, col,
(cx - 9, cy + dy),
(cx - 2 - i, cy + dy), 2)
else: # "dash" — lightning bolt
pygame.draw.polygon(screen, col, [
(cx - 6, cy - 9), (cx + 3, cy - 2),
(cx - 1, cy - 1), (cx + 6, cy + 9),
(cx - 3, cy + 2), (cx + 1, cy + 1),
])
def _draw_shield_aura(self, screen):
"""Pulsing ring around the player while Penguin's shield holds."""
r = self.camera.world_to_screen(self.player.hitbox)
pulse = 0.5 + 0.5 * abs((self.time * 2.4) % 2 - 1)
radius = int(self.player.hitbox.width * 0.7 + 8 * pulse)
aura = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
pygame.draw.circle(aura, (*theme.ACCENT, int(40 + 50 * pulse)),
(radius, radius), radius)
pygame.draw.circle(aura, (*theme.ACCENT, int(120 + 80 * pulse)),
(radius, radius), radius, 3)
screen.blit(aura, aura.get_rect(center=r.center))
def draw_key_status(self, screen):
"""Small chip by the HP bar: dim when the key is still out
there, lit gold once it's in hand."""
@@ -987,7 +1048,7 @@ class LevelManager:
d = theme.HINT_DOT
hint = self.hint_font.render(
f"WASD/Arrows to move & aim {d} Space to shoot {d} "
f"Shift to dash {d} E to use",
f"Shift for ability {d} E to use",
True, theme.MUTED)
hint.set_alpha(alpha)
screen.blit(hint, hint.get_rect(center=(cx, cy + 160)))
modified main.py
@@ -335,6 +335,14 @@ while running:
and not level_manager.failed)
pygame.mouse.set_visible(not in_active_game)
# Keyboard grab while a level is live: SDL routes macOS system
# shortcuts (Cmd-Tab, Mission Control, Spaces) to the game instead
# of the OS, so they can't yank focus mid-fight. Released in menus,
# pause and the level-end screen so the player can always tab away;
# the game's own Cmd-Q handler still fires (the combo reaches the
# app, which quits cleanly).
pygame.event.set_keyboard_grab(in_active_game)
# Auto-dismiss the main-menu status toast once its TTL elapses so a
# stale "Already up to date." doesn't sit on screen forever.
if (main_menu.status_until is not None
modified menu.py
@@ -189,7 +189,7 @@ class MainMenu:
tip = theme.text_surface(
self.small_font,
f"WASD/Arrows move + aim {d} Space shoot {d} "
f"Shift dash {d} E use",
f"Shift ability {d} E use",
MUTED)
screen.blit(tip, tip.get_rect(
center=(self.width // 2, self.height - 58)))
@@ -623,6 +623,21 @@ class CharacterMenu:
screen.blit(num, num.get_rect(midleft=(bar_x + bar_w + 12, y + 5)))
y += 56
# Signature ability — a fifth line below the stat bars so the
# differentiator reads before the character is picked.
if getattr(cls, "ABILITY_NAME", ""):
pygame.draw.line(screen, LINE_C,
(left + 20, y - 6),
(left + card_w - 20, y - 6), 2)
y += 14
name = theme.text_surface(
self.stat_font, f"ABILITY {cls.ABILITY_NAME}", TITLE_C)
screen.blit(name, name.get_rect(midleft=(left + 20, y + 5)))
y += 34
desc = theme.text_surface(
self.tagline_font, cls.ABILITY_DESC, MUTED)
screen.blit(desc, desc.get_rect(midleft=(left + 20, y + 5)))
def handle_input(self, event):
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mouse_pos = pygame.mouse.get_pos()
modified settings.py
@@ -44,6 +44,12 @@ DASH_SPEED_MULT = 3.2
DASH_COOLDOWN = 1.2
DASH_INVULN_BONUS = 0.05 # extra i-frames after the dash itself ends
# --- Abilities ---
# Wizard's Slow ability scales the per-frame dt of every enemy, the
# boss and enemy projectiles by this factor while it is active; the
# Wizard himself and his own shots keep the raw dt. See levels.py.
SLOW_SCALE = 0.35
# Boss (Mr. Green) — slower than the player so it can be kited.
BOSS_SCALE = 9
BOSS_SPEED = 250
modified units.py
@@ -46,6 +46,16 @@ class Character(pygame.sprite.Sprite):
attack_damage = PROJECTILE_DAMAGE
attack_cooldown = ATTACK_COOLDOWN
# --- Signature ability ---
# Each playable subclass overrides these plus the activate / tick /
# on_ability_end hooks below; the base values keep non-playable
# units (Enemy / Boss) inert — they never reach the trigger path.
ABILITY_NAME = "" # HUD + character-menu label
ABILITY_DESC = "" # one-line tagline for the character menu
ABILITY_GLYPH = "dash" # which icon draw_ability_meter draws
ABILITY_DURATION = 0.0 # seconds the ability stays active
ABILITY_COOLDOWN = 0.0 # seconds before it can fire again
# When False, left mouse no longer triggers an attack — only Space
# fires. The main-menu's playable avatar sets this off so clicks on
# buttons don't double as shots; gameplay keeps the default.
@@ -101,13 +111,15 @@ class Character(pygame.sprite.Sprite):
# opaque pixels (so transparent padding stays transparent).
self.tint_color = None
# Dash state — the player gets a Shift burst with i-frames and
# a cooldown; enemies don't dash through this path (Boss has its
# own state-machine dash).
self.dash_timer = 0.0 # seconds remaining of active dash
self.dash_cooldown_timer = 0.0 # seconds until dash can be used
self.speed_mult = 1.0 # >1 during dash
self._dash_dir = pygame.math.Vector2()
# Ability state — the player triggers their character's
# signature ability on Shift; the base class owns only these
# timers, each playable subclass owns the actual mechanic.
# Enemies never reach the trigger path (projectile_group stays
# None), so this stays inert for them.
self.ability_timer = 0.0 # seconds left of active ability
self.ability_cooldown_timer = 0.0 # seconds until it can re-fire
self.ability_active = False
self.speed_mult = 1.0 # >1 during a dash / sprint
# Set by the level for the player only; enemies leave these None
# unless the level wires them up (boss phase 2 needs them too).
@@ -250,6 +262,11 @@ class Character(pygame.sprite.Sprite):
# --- combat -----------------------------------------------------
def current_attack_cooldown(self):
"""Seconds between shots. A hook so an ability (Elf's
rapid-fire) can shorten the cadence for its active window."""
return self.attack_cooldown
def handle_attack(self, dt):
"""Player only: fire a projectile in the facing direction on
Space or left-click.
@@ -274,52 +291,55 @@ class Character(pygame.sprite.Sprite):
spawn, aim,
self.obstacle_sprites, self.projectile_targets,
[self.projectile_group],
damage=self.attack_damage)
damage=self.attack_damage, owner='player')
audio.play("shoot")
self.attack_timer = self.attack_cooldown
self.attack_timer = self.current_attack_cooldown()
def handle_dash(self, dt):
"""Shift triggers a short, i-framed speed burst.
def handle_ability(self, dt):
"""Shift triggers this character's signature ability.
Direction is locked at dash start (current input dir, falling
back to facing) so a panic dash always commits somewhere
sensible. Only the player goes through this — the Boss has its
own dash state.
The base class owns only the timers; each playable subclass
fills in :meth:`activate_ability` (and, where the ability needs
per-frame work or teardown, :meth:`tick_ability` /
:meth:`on_ability_end`). Only the player reaches the trigger
path — enemies leave ``projectile_group`` None.
"""
# Tick timers first. Cooldown only counts once the dash itself
# has ended.
if self.dash_timer > 0:
self.dash_timer = max(0.0, self.dash_timer - dt)
if self.dash_timer == 0:
self.speed_mult = 1.0
self.dash_cooldown_timer = DASH_COOLDOWN
# A whisker of i-frames after landing helps you cross
# contact-damage windows with frame-perfect dashes.
self.invuln_timer = max(self.invuln_timer,
DASH_INVULN_BONUS)
# Tick the active window first; the cooldown only counts once
# the ability itself has ended.
if self.ability_active:
self.ability_timer = max(0.0, self.ability_timer - dt)
if self.ability_timer == 0:
self.ability_active = False
self.ability_cooldown_timer = self.ABILITY_COOLDOWN
self.on_ability_end()
else:
# Lock direction during dash so mid-burst input can't
# change course.
self.direction.update(self._dash_dir)
elif self.dash_cooldown_timer > 0:
self.dash_cooldown_timer = max(
0.0, self.dash_cooldown_timer - dt)
self.tick_ability(dt)
elif self.ability_cooldown_timer > 0:
self.ability_cooldown_timer = max(
0.0, self.ability_cooldown_timer - dt)
if self.projectile_group is None:
return # only the player dashes via input
return # only the player triggers abilities via input
keys = pygame.key.get_pressed()
shift = keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]
if (shift and self.dash_timer == 0
and self.dash_cooldown_timer == 0):
d = self.direction if self.direction.magnitude() != 0 \
else pygame.math.Vector2(*FACING_VECTORS[self.facing])
self._dash_dir = d.normalize()
self.direction.update(self._dash_dir)
self.dash_timer = DASH_DURATION
self.speed_mult = DASH_SPEED_MULT
self.invuln_timer = max(self.invuln_timer, DASH_DURATION)
audio.play("dash")
if (shift and not self.ability_active
and self.ability_cooldown_timer == 0):
self.ability_active = True
self.ability_timer = self.ABILITY_DURATION
self.activate_ability()
# Subclass hooks — no-ops on the base so Enemy / Boss stay inert
# and a character with no per-frame work only overrides what it
# actually needs.
def activate_ability(self):
"""Run once the instant the ability fires."""
def tick_ability(self, dt):
"""Run every frame the ability is active (not its final frame)."""
def on_ability_end(self):
"""Run once when the active window runs out."""
# --- animation / tick -------------------------------------------
@@ -365,8 +385,11 @@ class Character(pygame.sprite.Sprite):
self.frame_index %= len(current_animation)
frame = current_animation[int(self.frame_index)]
# Flicker while invulnerable so hits read clearly.
if self.invuln_timer > 0 and int(self.invuln_timer * 20) % 2 == 0:
# Flicker while invulnerable so hits read clearly — but not
# while an ability is active: Penguin's shield shows an aura
# instead, and a 0.18 s dash flicker barely registers anyway.
if (self.invuln_timer > 0 and not self.ability_active
and int(self.invuln_timer * 20) % 2 == 0):
frame = frame.copy()
frame.set_alpha(90)
# Boss telegraph tint (red windup / gold aim) — applied before
@@ -385,7 +408,7 @@ class Character(pygame.sprite.Sprite):
self.hit_flash_timer = max(0.0, self.hit_flash_timer - dt)
self.get_input()
self.handle_dash(dt)
self.handle_ability(dt)
self.get_status()
self.move(dt)
self.animate(dt)
@@ -398,46 +421,151 @@ class Character(pygame.sprite.Sprite):
# sync if you tune the numbers.
class Wizard(Character):
"""Balanced reference character."""
"""Balanced reference character.
Signature — *Slow*: bends time for every enemy, the boss and their
in-flight shots while the Wizard keeps moving and firing at full
speed. The slow itself lives in ``LevelManager.update``, which reads
``ability_active``; this class only triggers it.
"""
asset_folder = "wizard"
# defaults: HP 100, spd 600, cd 0.35, dmg 10
ABILITY_NAME = "SLOW"
ABILITY_DESC = "Slows enemies and their shots for 3s"
ABILITY_GLYPH = "slow"
ABILITY_DURATION = 3.0
ABILITY_COOLDOWN = 12.0
def activate_ability(self):
audio.play("slow")
class Penguin(Character):
"""Tank — slower but takes more punishment and hits a bit harder."""
"""Tank — slower but takes more punishment and hits a bit harder.
Signature — *Shield*: total damage immunity for a short window.
Implemented by keeping ``invuln_timer`` topped up, so every damage
path (boss / enemy contact, spikes, projectiles — all gated on
``invuln_timer``) is blocked for free.
"""
asset_folder = "penguin"
max_hp = 140
speed = 480
attack_damage = 12
attack_cooldown = 0.45
ABILITY_NAME = "SHIELD"
ABILITY_DESC = "Immune to all damage for 2.5s"
ABILITY_GLYPH = "shield"
ABILITY_DURATION = 2.5
ABILITY_COOLDOWN = 11.0
def activate_ability(self):
audio.play("shield")
def tick_ability(self, dt):
# A hit drops invuln_timer to PLAYER_INVULN_TIME, which can be
# shorter than the shield still has to run — so re-arm a sliver
# of i-frames every frame the shield is up.
self.invuln_timer = max(self.invuln_timer, 0.2)
class Elf(Character):
"""Archer — rapid-fire, lower damage per shot, fragile."""
"""Archer — rapid-fire, lower damage per shot, fragile.
Signature — *Volley*: doubles the fire rate for a short window by
halving the effective attack cooldown.
"""
asset_folder = "elf"
max_hp = 90
speed = 580
attack_damage = 7
attack_cooldown = 0.20
ABILITY_NAME = "VOLLEY"
ABILITY_DESC = "Doubles fire rate for 2s"
ABILITY_GLYPH = "rapid"
ABILITY_DURATION = 2.0
ABILITY_COOLDOWN = 9.0
def activate_ability(self):
audio.play("volley")
def current_attack_cooldown(self):
if self.ability_active:
return self.attack_cooldown * 0.5
return self.attack_cooldown
class Shiggy(Character):
"""Glass cannon — biggest hit, smallest health pool."""
"""Glass cannon — biggest hit, smallest health pool.
Signature — *Dash*: a short, i-framed speed burst. Direction is
locked at dash start (current input dir, falling back to facing) so
a panic dash always commits somewhere sensible. This is the dash
every character shared before v0.5.0 — now Shiggy's alone.
"""
asset_folder = "shiggy"
max_hp = 70
speed = 620
attack_damage = 20
attack_cooldown = 0.40
ABILITY_NAME = "DASH"
ABILITY_DESC = "Quick i-framed burst dash"
ABILITY_GLYPH = "dash"
ABILITY_DURATION = DASH_DURATION
ABILITY_COOLDOWN = DASH_COOLDOWN
def activate_ability(self):
d = self.direction if self.direction.magnitude() != 0 \
else pygame.math.Vector2(*FACING_VECTORS[self.facing])
self._dash_dir = d.normalize()
self.direction.update(self._dash_dir)
self.speed_mult = DASH_SPEED_MULT
self.invuln_timer = max(self.invuln_timer, DASH_DURATION)
audio.play("dash")
def tick_ability(self, dt):
# Lock direction during the dash so mid-burst input can't
# change course.
self.direction.update(self._dash_dir)
def on_ability_end(self):
self.speed_mult = 1.0
# A whisker of i-frames after landing helps cross contact-damage
# windows with frame-perfect dashes.
self.invuln_timer = max(self.invuln_timer, DASH_INVULN_BONUS)
class Wolf(Character):
"""Scout — very fast, modest combat stats."""
"""Scout — very fast, modest combat stats.
Signature — *Sprint*: a burst of peak movement speed. Unlike
Shiggy's dash it grants no i-frames and never locks the steering —
full directional control the whole time.
"""
asset_folder = "wolf"
max_hp = 85
speed = 760
attack_damage = 9
attack_cooldown = 0.40
ABILITY_NAME = "SPRINT"
ABILITY_DESC = "Peak movement speed for 1.5s"
ABILITY_GLYPH = "sprint"
ABILITY_DURATION = 1.5
ABILITY_COOLDOWN = 8.0
SPRINT_SPEED_MULT = 2.0
def activate_ability(self):
self.speed_mult = self.SPRINT_SPEED_MULT
audio.play("sprint")
def on_ability_end(self):
self.speed_mult = 1.0
# Catalogue used by the character menu to display stats and by
# levels.py to instantiate the chosen class. Order = menu order.
@@ -536,8 +664,8 @@ class Boss(Character):
# the state machine in :meth:`update`.
return
def handle_dash(self, dt):
# Boss dash is a state, not a button — skip the player path.
def handle_ability(self, dt):
# Boss has no signature ability — its dash is an FSM state.
return
def _in_phase2(self):
@@ -589,7 +717,7 @@ class Boss(Character):
[self.projectile_group],
damage=BOSS_PROJECTILE_DAMAGE,
speed=BOSS_PROJECTILE_SPEED,
color=(255, 130, 70))
color=(255, 130, 70), owner='enemy')
def _update_tint(self):
"""Red ramp during windup, gold ramp during aim — the colour
@@ -687,8 +815,8 @@ class Enemy(Character):
def handle_attack(self, dt):
return # contact-only
def handle_dash(self, dt):
return # no dash
def handle_ability(self, dt):
return # no signature ability
class Orange(Enemy):
@@ -709,17 +837,20 @@ class Projectile(pygame.sprite.Sprite):
"""A simple orb that flies straight, hurts its targets and dies on walls.
The colour and speed can be customised so the boss's volley reads
visually different from the player's shots.
visually different from the player's shots. ``owner`` ('player' /
'enemy') tags which side fired it, so the Wizard's slow-time can
spare player shots while slowing enemy ones.
"""
def __init__(self, pos, direction, obstacle_sprites, targets, groups,
damage=PROJECTILE_DAMAGE, color=(120, 230, 255),
speed=PROJECTILE_SPEED):
speed=PROJECTILE_SPEED, owner='enemy'):
super().__init__(groups)
self.obstacle_sprites = obstacle_sprites
self.targets = targets
self.damage = damage
self.speed = speed
self.owner = owner
r = PROJECTILE_RADIUS
size = (r + 4) * 2