ajhahn.de
← the-way-out
Python 107 lines
"""Attack + ability cooldown bookkeeping on the Character base class
and the per-character ABILITY_COOLDOWN catalogue.

The Character constructor wants an obstacle_sprites group and a real
asset folder. ``Wizard`` is the simplest playable (defaults across the
board); the units module's ``load_assets`` tolerates missing PNGs by
returning an empty frame list (``_placeholder_frame`` covers the
draw)."""
import pygame

from settings import ATTACK_COOLDOWN, DASH_COOLDOWN
from units import Elf, Penguin, Shiggy, Wizard, Wolf


def _wizard():
    return Wizard(0, 0, obstacle_sprites=pygame.sprite.Group())


def _elf():
    return Elf(0, 0, obstacle_sprites=pygame.sprite.Group())


# --- Per-character ABILITY_COOLDOWN catalogue ----------------------------

def test_default_ability_cooldown_constants():
    # Numbers are the contract the HUD's ability-ring draws against
    # — bumping one without bumping the CHANGELOG is a regression.
    assert Wizard.ABILITY_COOLDOWN == 12.0
    assert Penguin.ABILITY_COOLDOWN == 11.0
    assert Elf.ABILITY_COOLDOWN == 9.0
    assert Wolf.ABILITY_COOLDOWN == 8.0
    assert Shiggy.ABILITY_COOLDOWN == DASH_COOLDOWN


def test_default_attack_cooldown_is_module_constant():
    # Wizard does not override attack_cooldown, so the class attribute
    # is the module-wide ATTACK_COOLDOWN.
    assert Wizard.attack_cooldown == ATTACK_COOLDOWN


# --- attack_timer tick + clamp ------------------------------------------

def test_attack_timer_starts_at_zero():
    w = _wizard()
    assert w.attack_timer == 0.0


def test_attack_timer_clamps_at_zero_after_overrun():
    w = _wizard()
    w.attack_timer = 0.05
    w.attack_timer = max(0.0, w.attack_timer - 0.5)
    assert w.attack_timer == 0.0


def test_current_attack_cooldown_default_equals_class_attr():
    w = _wizard()
    assert w.current_attack_cooldown() == Wizard.attack_cooldown


# --- Elf VOLLEY halves the cadence while active --------------------------

def test_elf_cooldown_halved_during_active_ability():
    e = _elf()
    base = e.current_attack_cooldown()
    assert base == Elf.attack_cooldown
    e.ability_active = True
    assert e.current_attack_cooldown() == Elf.attack_cooldown * 0.5
    e.ability_active = False
    assert e.current_attack_cooldown() == base


# --- ability_cooldown_timer tick + clamp ---------------------------------

def test_ability_cooldown_timer_starts_at_zero():
    w = _wizard()
    assert w.ability_cooldown_timer == 0.0


def test_ability_cooldown_timer_ticks_down_and_clamps():
    w = _wizard()
    w.ability_cooldown_timer = 0.4
    # Mirror the per-frame countdown handle_ability runs.
    w.ability_cooldown_timer = max(0.0, w.ability_cooldown_timer - 0.1)
    assert abs(w.ability_cooldown_timer - 0.3) < 1e-9
    w.ability_cooldown_timer = max(0.0, w.ability_cooldown_timer - 5.0)
    assert w.ability_cooldown_timer == 0.0


def test_ability_gating_uses_cooldown_timer_and_active_flag():
    # Mirror the gate handle_ability checks: ready only when not active
    # and cooldown timer is zero. No keyboard polling here — the trigger
    # check is just (not active) and (timer == 0).
    w = _wizard()
    assert not w.ability_active
    assert w.ability_cooldown_timer == 0.0
    ready = (not w.ability_active and w.ability_cooldown_timer == 0)
    assert ready is True

    w.ability_cooldown_timer = 0.5
    ready = (not w.ability_active and w.ability_cooldown_timer == 0)
    assert ready is False

    w.ability_cooldown_timer = 0.0
    w.ability_active = True
    ready = (not w.ability_active and w.ability_cooldown_timer == 0)
    assert ready is False