Files
orni_attack/CLAUDE.md

34 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

This is Orni Attack, an Asteroids-style game originally written in Turbo Pascal 7 for DOS (1999), now being migrated to modern C++20 with SDL3. The game features a spaceship that must avoid and destroy enemies (pentagonal "ORNIs"). This is a phased migration preserving the original game feel.

Language: All code, comments, and variable names are in Catalan/Valencian (preserved from original).

Current Status: BETA 3.0 - Modernized architecture with dynamic windows, modular code organization, viewport scaling, and auto-generated project metadata.

Build System

Based on /home/sergio/gitea/pollo project structure, now with CMake as build authority and automatic file discovery.

Basic Build Commands

make           # Compile (delegates to CMake)
make debug     # Debug build
make clean     # Clean artifacts
./orni         # Run

Release Packaging

make macos_release     # macOS .app bundle + .dmg (Apple Silicon)
make linux_release     # Linux .tar.gz
make windows_release   # Windows .zip (requires MinGW on Windows)
make windows_cross     # Cross-compile Windows from Linux/macOS
make rpi_release       # Raspberry Pi ARM64 cross-compile

Build Files

  • CMakeLists.txt - CMake configuration (C++20, SDL3, auto-discovers .cpp files)
  • Makefile - Wrapper for compilation + complex packaging recipes
  • source/project.h.in - Template for auto-generated project.h
  • build/project.h - Auto-generated (by CMake) with project constants
  • release/ - Platform-specific resources (icons, .rc, .plist, frameworks, DLLs)

Architecture: Hybrid CMake + Makefile

CMake handles: Compilation (simple, standard, IDE-friendly)

  • Auto-discovers all .cpp files in source/core/ and source/game/
  • Excludes source/legacy/ automatically
  • Generates build/project.h from template
  • Links SDL3

Makefile handles: Packaging (complex bash scripts)

  • Delegates compilation to CMake (makecmake --build build)
  • Contains 5 release packaging targets (macOS, Linux, Windows, RPI, Windows-cross)
  • Includes: code signing, framework symlinks, DMG creation, cross-compilation

Project Metadata System

Single source of truth in CMakeLists.txt:

project(orni VERSION 0.3.0)
set(PROJECT_LONG_NAME "Orni Attack")
set(PROJECT_COPYRIGHT "© 1999 Visente i Sergi, 2025 Port")

Auto-generated build/project.h:

namespace Project {
    constexpr const char* NAME = "orni";
    constexpr const char* LONG_NAME = "Orni Attack";
    constexpr const char* VERSION = "0.3.0";
    constexpr const char* COPYRIGHT = "© 1999 Visente i Sergi, 2025 Port";
    constexpr const char* GIT_HASH = "abc1234";  // From git rev-parse
}

Window title (dynamic): Orni Attack v0.3.0 (© 1999 Visente i Sergi, 2025 Port)

File Discovery

Automatic - no manual maintenance needed:

# CMakeLists.txt automatically finds:
file(GLOB_RECURSE CORE_SOURCES "source/core/*.cpp")
file(GLOB_RECURSE GAME_SOURCES "source/game/*.cpp")
# + source/main.cpp
# - source/legacy/* (excluded)

When you create a new file like source/game/entities/asteroide.cpp:

  1. Just create it in the appropriate directory
  2. Run make
  3. CMake automatically detects and compiles it

No need to edit Makefile or CMakeLists.txt!

Cross-Platform Notes

  • macOS: Requires create-dmg (auto-installed via Homebrew)
  • Windows: Compile natively with MinGW or use make windows_cross on Linux/macOS
  • Windows cross: Requires x86_64-w64-mingw32-g++ toolchain
  • RPI cross: Requires aarch64-linux-gnu-g++ toolchain
  • Frameworks: macOS release includes SDL3.xcframework with symlink recreation

Architecture

File Structure (BETA 3.0)

source/
├── core/                          - Reusable engine layer
│   ├── defaults.hpp               - Configuration constants (SINGLE SOURCE OF TRUTH)
│   ├── types.hpp                  - Data structures (IPunt, Punt, Triangle, Poligon)
│   └── rendering/
│       ├── sdl_manager.hpp/cpp    - SDL3 window management + viewport scaling
│       └── primitives.hpp/cpp     - Pure geometric functions
├── game/                          - Asteroids-specific game logic
│   ├── constants.hpp              - Legacy constant aliases
│   └── joc_asteroides.hpp/cpp     - Game loop, physics, rendering
├── utils/                         - Shared utilities (empty for now)
├── main.cpp                       - Entry point, F1/F2/F3 window controls
└── legacy/
    └── asteroids.cpp              - Original Pascal code (reference only)

Key architectural decisions:

  • core/ contains reusable, game-agnostic code
  • game/ contains Asteroids-specific logic
  • All constants centralized in core/defaults.hpp
  • Backward compatibility via game/constants.hpp aliases

Core Data Structures

The game uses polar coordinates for all geometric objects (preserved from Pascal original):

struct IPunt {
    float r;      // Radius
    float angle;  // Angle in radians
};

struct Punt {
    int x, y;     // Cartesian coordinates
};

struct Triangle {           // Player's ship (nau_)
    IPunt p1, p2, p3;      // 3 polar points
    Punt centre;           // Center position
    float angle;           // Orientation
    float velocitat;       // Speed (px/s)
};

struct Poligon {           // Enemies (orni_) and bullets (bales_)
    std::array<IPunt, MAX_IPUNTS> ipuntx;  // Polar points
    Punt centre;
    float angle;           // Movement direction
    float velocitat;       // Speed (units/frame)
    uint8_t n;            // Number of sides
    float drotacio;       // Rotation delta per frame
    float rotacio;        // Current rotation angle
    bool esta;            // Is active?
};

Constants (joc_asteroides.hpp)

namespace Constants {
    constexpr int MARGE_DALT = 20;      // Top margin
    constexpr int MARGE_BAIX = 460;     // Bottom margin
    constexpr int MARGE_ESQ = 20;       // Left margin
    constexpr int MARGE_DRET = 620;     // Right margin
    constexpr int MAX_IPUNTS = 30;      // Max polygon points
    constexpr int MAX_ORNIS = 15;       // Max enemies
    constexpr int MAX_BALES = 3;        // Max bullets
    constexpr int VELOCITAT = 2;        // Base velocity
    constexpr int VELOCITAT_MAX = 6;    // Max velocity
    constexpr float PI = 3.14159265359f;
}

Game Loop (main.cpp)

Time-based physics with real delta_time:

// Lines 21-56
Uint64 last_time = SDL_GetTicks();
while (running) {
    // Calculate real delta_time
    Uint64 current_time = SDL_GetTicks();
    float delta_time = (current_time - last_time) / 1000.0f;  // ms → s
    last_time = current_time;

    // Cap at 50ms (20 FPS minimum)
    if (delta_time > 0.05f) delta_time = 0.05f;

    // Process events
    while (SDL_PollEvent(&event)) {
        joc.processar_input(event);
        // Handle quit/ESC
    }

    // Update + render
    joc.actualitzar(delta_time);
    sdl.neteja(0, 0, 0);
    joc.dibuixar();
    sdl.presenta();
}

Critical: Delta_time is real and variable, not fixed at 0.016f. All physics must multiply by delta_time.

SDL3 API Notes

SDL3 has breaking changes from SDL2:

  • SDL_CreateRenderer(window, nullptr) - no flags parameter
  • event.key.key instead of event.key.keysym.sym
  • SDL_EVENT_KEY_DOWN instead of SDL_KEYDOWN
  • SDL_EVENT_QUIT instead of SDL_QUIT
  • SDL_GetKeyboardState(nullptr) - state-based input for continuous keys

Physics System

Ship Movement (joc_asteroides.cpp:67-155)

State-based input (not event-based) for smooth controls:

const bool* keyboard_state = SDL_GetKeyboardState(nullptr);

if (keyboard_state[SDL_SCANCODE_RIGHT])
    nau_.angle += ROTATION_SPEED * delta_time;
if (keyboard_state[SDL_SCANCODE_LEFT])
    nau_.angle -= ROTATION_SPEED * delta_time;
if (keyboard_state[SDL_SCANCODE_UP])
    nau_.velocitat += ACCELERATION * delta_time;

Physics constants (calibrated for ~20 FPS feel):

constexpr float ROTATION_SPEED = 2.5f;      // rad/s (~143°/s)
constexpr float ACCELERATION = 100.0f;      // px/s²
constexpr float MAX_VELOCITY = 200.0f;      // px/s
constexpr float FRICTION = 6.0f;            // px/s²

Position calculation (angle-PI/2 because angle=0 points up, not right):

float dy = (nau_.velocitat * delta_time) * std::sin(nau_.angle - PI/2.0f);
float dx = (nau_.velocitat * delta_time) * std::cos(nau_.angle - PI/2.0f);
nau_.centre.y += round(dy);
nau_.centre.x += round(dx);

Visual velocity effect: Ship triangle grows when moving (joc_asteroides.cpp:162-164):

// Scale 200 px/s → 6 px visual effect (like original)
float velocitat_visual = nau_.velocitat / 33.33f;
rota_tri(nau_, nau_.angle, velocitat_visual, true);

Enemy Movement (joc_asteroides.cpp:367-405) - FASE 8

Autonomous movement with random direction changes:

void mou_orni(Poligon& orni, float delta_time) {
    // 5% probability to change direction
    if (rand() < 0.05f * RAND_MAX)
        orni.angle = random() * 2*PI;

    // Move (2 px/frame * 20 FPS = 40 px/s)
    float velocitat_efectiva = orni.velocitat * 20.0f * delta_time;
    float dy = velocitat_efectiva * sin(orni.angle - PI/2.0f);
    float dx = velocitat_efectiva * cos(orni.angle - PI/2.0f);
    orni.centre.y += round(dy);
    orni.centre.x += round(dx);

    // Bounce on walls
    if (x < MARGE_ESQ || x > MARGE_DRET)
        orni.angle = PI - orni.angle;  // Horizontal reflection
    if (y < MARGE_DALT || y > MARGE_BAIX)
        orni.angle = 2*PI - orni.angle;  // Vertical reflection
}

Bullet Movement (joc_asteroides.cpp:444-465) - FASE 9

Straight-line movement, deactivates when leaving screen:

void mou_bales(Poligon& bala, float delta_time) {
    // Fast movement (6 px/frame * 20 FPS = 120 px/s)
    float velocitat_efectiva = bala.velocitat * 20.0f * delta_time;
    float dy = velocitat_efectiva * sin(bala.angle - PI/2.0f);
    float dx = velocitat_efectiva * cos(bala.angle - PI/2.0f);
    bala.centre.y += round(dy);
    bala.centre.x += round(dx);

    // Deactivate if out of bounds
    if (x < MARGE_ESQ || x > MARGE_DRET ||
        y < MARGE_DALT || y > MARGE_BAIX)
        bala.esta = false;
}

Rendering System

Coordinate Conversion

Polar → Cartesian with rotation (used in rota_tri and rota_pol):

// For each polar point
int x = round((r + velocitat) * cos(angle_punt + angle_object)) + centre.x;
int y = round((r + velocitat) * sin(angle_punt + angle_object)) + centre.y;

Line Drawing (joc_asteroides.cpp:230-298)

Currently uses SDL_RenderLine for efficiency:

bool linea(int x1, int y1, int x2, int y2, bool dibuixar) {
    if (dibuixar && renderer_) {
        SDL_SetRenderDrawColor(renderer_, 255, 255, 255, 255);  // White
        SDL_RenderLine(renderer_, x1, y1, x2, y2);
    }
    return false;  // Collision detection TODO (Phase 10)
}

Note: Original Bresenham algorithm preserved in comments for Phase 10 (pixel-perfect collision detection).

Ship Rendering (joc_asteroides.cpp:300-337)

Triangle with 3 lines:

void rota_tri(const Triangle& tri, float angul, float velocitat, bool dibuixar) {
    // Convert 3 polar points to Cartesian
    int x1 = round((tri.p1.r + velocitat) * cos(tri.p1.angle + angul)) + tri.centre.x;
    int y1 = round((tri.p1.r + velocitat) * sin(tri.p1.angle + angul)) + tri.centre.y;
    // ... same for p2, p3

    // Draw 3 lines
    linea(x1, y1, x2, y2, dibuixar);
    linea(x1, y1, x3, y3, dibuixar);
    linea(x3, y3, x2, y2, dibuixar);
}

Polygon Rendering (joc_asteroides.cpp:339-365)

Enemies and bullets:

void rota_pol(const Poligon& pol, float angul, bool dibuixar) {
    // Convert all polar points to Cartesian
    std::array<Punt, MAX_IPUNTS> xy;
    for (int i = 0; i < pol.n; i++) {
        xy[i].x = round(pol.ipuntx[i].r * cos(pol.ipuntx[i].angle + angul)) + pol.centre.x;
        xy[i].y = round(pol.ipuntx[i].r * sin(pol.ipuntx[i].angle + angul)) + pol.centre.y;
    }

    // Draw lines between consecutive points
    for (int i = 0; i < pol.n - 1; i++)
        linea(xy[i].x, xy[i].y, xy[i+1].x, xy[i+1].y, dibuixar);

    // Close polygon
    linea(xy[pol.n-1].x, xy[pol.n-1].y, xy[0].x, xy[0].y, dibuixar);
}

Input System - FASE 9

Continuous Input (actualitzar)

Arrow keys use state-based polling:

const bool* keyboard_state = SDL_GetKeyboardState(nullptr);
if (keyboard_state[SDL_SCANCODE_UP]) { /* accelerate */ }

Event-Based Input (processar_input)

SPACE bar for shooting (joc_asteroides.cpp:174-212):

void processar_input(const SDL_Event& event) {
    if (event.type == SDL_EVENT_KEY_DOWN) {
        if (event.key.key == SDLK_SPACE) {
            // Find first inactive bullet
            for (auto& bala : bales_) {
                if (!bala.esta) {
                    bala.esta = true;
                    bala.centre = nau_.centre;  // Spawn at ship
                    bala.angle = nau_.angle;    // Fire in ship direction
                    bala.velocitat = 6.0f;      // High speed
                    break;  // Only one bullet at a time
                }
            }
        }
    }
}

Initialization (joc_asteroides.cpp:15-65)

Ship (lines 20-34)

// Triangle with 3 polar points (r=12, angles at 270°, 45°, 135°)
nau_.p1.r = 12.0f;
nau_.p1.angle = 3.0f * PI / 2.0f;  // Points up
nau_.p2.r = 12.0f;
nau_.p2.angle = PI / 4.0f;         // Back-right
nau_.p3.r = 12.0f;
nau_.p3.angle = 3.0f * PI / 4.0f;  // Back-left
nau_.centre = {320, 240};
nau_.angle = 0.0f;
nau_.velocitat = 0.0f;

Enemies (lines 39-54) - FASE 7

for (int i = 0; i < MAX_ORNIS; i++) {
    crear_poligon_regular(orni_[i], 5, 20.0f);  // Pentagon, r=20
    orni_[i].centre.x = rand(30, 610);
    orni_[i].centre.y = rand(30, 450);
    orni_[i].angle = rand(0, 360) * PI/180;
    orni_[i].esta = true;
}

Bullets (lines 56-64) - FASE 9

for (int i = 0; i < MAX_BALES; i++) {
    crear_poligon_regular(bales_[i], 5, 5.0f);  // Small pentagon, r=5
    bales_[i].esta = false;  // Initially inactive
}

Update Loop (joc_asteroides.cpp:67-155)

void actualitzar(float delta_time) {
    // 1. Ship input + physics (lines 68-125)
    //    - Keyboard state polling
    //    - Rotation, acceleration, friction
    //    - Position update with boundary checking

    // 2. Enemy movement (lines 137-147) - FASE 8
    for (auto& enemy : orni_) {
        if (enemy.esta) {
            mou_orni(enemy, delta_time);
            enemy.rotacio += enemy.drotacio;  // Visual rotation
        }
    }

    // 3. Bullet movement (lines 149-154) - FASE 9
    for (auto& bala : bales_) {
        if (bala.esta)
            mou_bales(bala, delta_time);
    }

    // TODO Phase 10: Collision detection
}

Draw Loop (joc_asteroides.cpp:157-184)

void dibuixar() {
    // 1. Ship (if alive)
    if (itocado_ == 0) {
        float velocitat_visual = nau_.velocitat / 33.33f;
        rota_tri(nau_, nau_.angle, velocitat_visual, true);
    }

    // 2. Enemies (FASE 7)
    for (const auto& enemy : orni_) {
        if (enemy.esta)
            rota_pol(enemy, enemy.rotacio, true);
    }

    // 3. Bullets (FASE 9)
    for (const auto& bala : bales_) {
        if (bala.esta)
            rota_pol(bala, 0.0f, true);  // No visual rotation
    }

    // TODO Phase 11: Draw borders
}

Title Screen Ship System (BETA 3.0)

The title screen features two 3D ships floating on the starfield with perspective rendering, entry/exit animations, and subtle floating motion.

Architecture Overview

Files:

  • source/game/title/ship_animator.hpp/cpp - Ship animation state machine
  • source/core/rendering/shape_renderer.hpp/cpp - 3D rotation + perspective projection
  • source/core/defaults.hpp - Title::Ships namespace with all constants
  • source/game/escenes/escena_titol.hpp/cpp - Integration with title scene

Design Philosophy:

  • Static 3D rotation: Ships have fixed pitch/yaw/roll angles (not recalculated per frame)
  • Simple Z-axis simulation: Scale changes simulate depth, not full perspective recalculation
  • State machine: ENTERING → FLOATING → EXITING states
  • Easing functions: Smooth transitions with ease_out_quad (entry) and ease_in_quad (exit)
  • Sinusoidal floating: Organic motion using X/Y oscillation with phase offset

3D Rendering System

Rotation3D Struct (shape_renderer.hpp)

struct Rotation3D {
    float pitch;  // X-axis rotation (nose up/down)
    float yaw;    // Y-axis rotation (turn left/right)
    float roll;   // Z-axis rotation (bank left/right)

    Rotation3D() : pitch(0.0f), yaw(0.0f), roll(0.0f) {}
    Rotation3D(float p, float y, float r) : pitch(p), yaw(y), roll(r) {}

    bool has_rotation() const {
        return pitch != 0.0f || yaw != 0.0f || roll != 0.0f;
    }
};

3D Transformation Pipeline (shape_renderer.cpp)

static Punt apply_3d_rotation(float x, float y, const Rotation3D& rot) {
    float z = 0.0f;  // All 2D points start at Z=0

    // 1. Pitch (X-axis): Rotate around horizontal axis
    float cos_pitch = std::cos(rot.pitch);
    float sin_pitch = std::sin(rot.pitch);
    float y1 = y * cos_pitch - z * sin_pitch;
    float z1 = y * sin_pitch + z * cos_pitch;

    // 2. Yaw (Y-axis): Rotate around vertical axis
    float cos_yaw = std::cos(rot.yaw);
    float sin_yaw = std::sin(rot.yaw);
    float x2 = x * cos_yaw + z1 * sin_yaw;
    float z2 = -x * sin_yaw + z1 * cos_yaw;

    // 3. Roll (Z-axis): Rotate around depth axis
    float cos_roll = std::cos(rot.roll);
    float sin_roll = std::sin(rot.roll);
    float x3 = x2 * cos_roll - y1 * sin_roll;
    float y3 = x2 * sin_roll + y1 * cos_roll;

    // 4. Perspective projection (Z-divide)
    constexpr float perspective_factor = 500.0f;
    float scale_factor = perspective_factor / (perspective_factor + z2);

    return {x3 * scale_factor, y3 * scale_factor};
}

Rendering order: 3D rotation → perspective → 2D scale → 2D rotation → translation

Backward compatibility: Optional rotation_3d parameter (default nullptr) - existing code unaffected

Ship Animation State Machine

States (ship_animator.hpp)

enum class EstatNau {
    ENTERING,  // Entering from off-screen
    FLOATING,  // Floating at target position
    EXITING    // Flying towards vanishing point
};

struct NauTitol {
    int jugador_id;              // 1 or 2
    EstatNau estat;              // Current state
    float temps_estat;           // Time in current state

    Punt posicio_inicial;        // Start position
    Punt posicio_objectiu;       // Target position
    Punt posicio_actual;         // Current interpolated position

    float escala_inicial;        // Start scale
    float escala_objectiu;       // Target scale
    float escala_actual;         // Current interpolated scale

    Rotation3D rotacio_3d;       // STATIC 3D rotation (never changes)
    float fase_oscilacio;        // Oscillation phase accumulator

    std::shared_ptr<Graphics::Shape> forma;
    bool visible;
};

State Transitions

ENTERING (2.0s):

  • Ships appear from beyond screen edges (calculated radially from clock positions)
  • Lerp position: off-screen → target (clock 8 / clock 4)
  • Lerp scale: 1.0 → 0.6 (perspective effect)
  • Easing: ease_out_quad (smooth deceleration)
  • Transition: → FLOATING when complete

FLOATING (indefinite):

  • Sinusoidal oscillation on X/Y axes
  • Different frequencies (0.5 Hz / 0.7 Hz) with phase offset (π/2)
  • Creates organic circular/elliptical motion
  • Scale constant at 0.6
  • Transition: → EXITING when START pressed

EXITING (1.0s):

  • Ships fly towards vanishing point (center: 320, 240)
  • Lerp position: current → vanishing point
  • Lerp scale: current → 0.0 (simulates Z → infinity)
  • Easing: ease_in_quad (acceleration)
  • Edge case: If START pressed during ENTERING, ships fly from mid-animation position
  • Marks invisible when complete

Configuration (defaults.hpp)

namespace Title {
namespace Ships {
    // Clock positions (polar coordinates from center 320, 240)
    constexpr float CLOCK_8_ANGLE = 150.0f * Math::PI / 180.0f;  // Bottom-left
    constexpr float CLOCK_4_ANGLE = 30.0f * Math::PI / 180.0f;   // Bottom-right
    constexpr float CLOCK_RADIUS = 150.0f;

    // Target positions (pre-calculated)
    constexpr float P1_TARGET_X = 190.0f;  // Clock 8
    constexpr float P1_TARGET_Y = 315.0f;
    constexpr float P2_TARGET_X = 450.0f;  // Clock 4
    constexpr float P2_TARGET_Y = 315.0f;

    // 3D rotations (STATIC - tuned for subtle effect)
    constexpr float P1_PITCH = 0.1f;   // ~6° nose-up
    constexpr float P1_YAW = -0.15f;   // ~9° turn left
    constexpr float P1_ROLL = -0.05f;  // ~3° bank left

    constexpr float P2_PITCH = 0.1f;   // ~6° nose-up
    constexpr float P2_YAW = 0.15f;    // ~9° turn right
    constexpr float P2_ROLL = 0.05f;   // ~3° bank right

    // Scales
    constexpr float ENTRY_SCALE_START = 1.0f;
    constexpr float FLOATING_SCALE = 0.6f;

    // Animation durations
    constexpr float ENTRY_DURATION = 2.0f;
    constexpr float EXIT_DURATION = 1.0f;
    constexpr float ENTRY_OFFSET = 200.0f;  // Distance beyond screen edge

    // Floating oscillation
    constexpr float FLOAT_AMPLITUDE_X = 6.0f;
    constexpr float FLOAT_AMPLITUDE_Y = 4.0f;
    constexpr float FLOAT_FREQUENCY_X = 0.5f;
    constexpr float FLOAT_FREQUENCY_Y = 0.7f;
    constexpr float FLOAT_PHASE_OFFSET = 1.57f;  // π/2 (90°)

    // Vanishing point
    constexpr float VANISHING_POINT_X = 320.0f;
    constexpr float VANISHING_POINT_Y = 240.0f;
}
}

Integration with EscenaTitol

Constructor

// Initialize ships after starfield
ship_animator_ = std::make_unique<Title::ShipAnimator>(sdl_.obte_renderer());
ship_animator_->inicialitzar();

if (estat_actual_ == EstatTitol::MAIN) {
    // Jump to MAIN: ships already in position (no entry animation)
    ship_animator_->set_visible(true);
} else {
    // Normal flow: ships enter during STARFIELD_FADE_IN
    ship_animator_->set_visible(true);
    ship_animator_->start_entry_animation();
}

Update Loop

// Update ships in visible states
if (ship_animator_ &&
    (estat_actual_ == EstatTitol::STARFIELD_FADE_IN ||
     estat_actual_ == EstatTitol::STARFIELD ||
     estat_actual_ == EstatTitol::MAIN ||
     estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) {
    ship_animator_->actualitzar(delta_time);
}

// Trigger exit when START pressed
if (checkStartGameButtonPressed()) {
    estat_actual_ = EstatTitol::PLAYER_JOIN_PHASE;
    ship_animator_->trigger_exit_animation();  // Edge case: handles mid-ENTERING
    Audio::get()->fadeOutMusic(MUSIC_FADE);
}

Draw Loop

// Draw order: starfield → ships → logo → text
if (starfield_) starfield_->dibuixar();

if (ship_animator_ &&
    (estat_actual_ == EstatTitol::STARFIELD_FADE_IN ||
     estat_actual_ == EstatTitol::STARFIELD ||
     estat_actual_ == EstatTitol::MAIN ||
     estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) {
    ship_animator_->dibuixar();
}

// Logo and text drawn after ships (foreground)

Timing & Visibility

Timeline:

  1. STARFIELD_FADE_IN (3.0s): Ships enter from off-screen
  2. STARFIELD (4.0s): Ships floating
  3. MAIN (indefinite): Ships floating + logo + text visible
  4. PLAYER_JOIN_PHASE (2.5s): Ships exit (1.0s) + text blink
  5. BLACK_SCREEN (2.0s): Ships already invisible (exit completed at 1.0s)

Automatic visibility management:

  • Ships marked visible = false when exit animation completes (actualitzar_exiting)
  • No manual hiding needed - state machine handles it

Tuning Notes

If ships look distorted:

  • Reduce rotation angles (P1_PITCH, P1_YAW, P1_ROLL, P2_*)
  • Current values (0.1, 0.15, 0.05) are tuned for subtle 3D effect
  • Angles in radians: 0.1 rad ≈ 6°, 0.15 rad ≈ 9°

If ships are too large/small:

  • Adjust FLOATING_SCALE (currently 0.6)
  • Adjust ENTRY_SCALE_START (currently 1.0)

If floating motion is too jerky/smooth:

  • Adjust FLOAT_AMPLITUDE_X/Y (currently 6.0/4.0 pixels)
  • Adjust FLOAT_FREQUENCY_X/Y (currently 0.5/0.7 Hz)

If entry/exit animations are too fast/slow:

  • Adjust ENTRY_DURATION (currently 2.0s)
  • Adjust EXIT_DURATION (currently 1.0s)

Implementation Phases (Completed)

Phase 1: 3D infrastructure (Rotation3D, render_shape extension) Phase 2: Foundation (ship_animator files, constants) Phase 3: Configuration & loading (shape loading, initialization) Phase 4: Floating animation (sinusoidal oscillation) Phase 5: Entry animation (off-screen → position with easing) Phase 6: Exit animation (position → vanishing point) Phase 7: EscenaTitol integration (constructor, update, draw) Phase 8: Polish & tuning (angles, scales, edge cases) Phase 9: Documentation (CLAUDE.md, code comments)

Migration Progress

Phase 0: Project Setup

  • CMakeLists.txt + Makefile (based on pollo)
  • Stub files created

Phase 1: SDL Manager

  • SDLManager class (sdl_manager.hpp/cpp)
  • Window + renderer initialization
  • Fixed SDL3 API differences

Phase 2: Data Structures

  • IPunt, Punt, Triangle, Poligon defined
  • Constants namespace with constexpr

Phase 3: Geometry Functions

  • modul(), diferencia(), distancia(), angle_punt()
  • crear_poligon_regular()

Phase 4: Line Drawing

  • linea() with SDL_RenderLine
  • Bresenham preserved in comments for Phase 10

Phase 5: Ship Rendering

  • rota_tri() polar→Cartesian conversion
  • Ship initialization

Phase 6: Ship Movement

  • Critical fix: Event-based → State-based input (SDL_GetKeyboardState)
  • Critical fix: Fixed delta_time (0.016f) → Real delta_time calculation
  • Critical fix: Visual velocity scaling (200 px/s → 6 px visual)
  • Time-based physics (all values in px/s)
  • Rotation, acceleration, friction, boundary checking

Phase 7: Enemy Rendering

  • rota_pol() for polygons
  • 15 random pentagons initialized
  • Visual rotation (enemy.rotacio)

Phase 8: Enemy AI & Movement

  • mou_orni() (joc_asteroides.cpp:367-405)
    • 5% random direction change
    • Polar movement (40 px/s)
    • Boundary bounce (angle reflection)
  • Integrated in actualitzar() (lines 137-147)

Phase 9: Bullet System

  • Bullet initialization (joc_asteroides.cpp:56-64)
    • 3 bullets, initially inactive
    • Small pentagons (r=5)
  • Shooting with SPACE (joc_asteroides.cpp:174-212)
    • Spawns at ship position
    • Fires in ship direction
    • Only one bullet at a time
  • mou_bales() (joc_asteroides.cpp:444-465)
    • Fast rectlinear movement (120 px/s)
    • Deactivates when out of bounds
  • Drawing (joc_asteroides.cpp:175-181)
    • No visual rotation

🔲 Phase 10: Collision Detection & Death (NEXT)

  • Collision detection:
    • Restore Bresenham pixel-perfect algorithm
    • Detect ship-enemy collision → tocado()
    • Detect bullet-enemy collision → destroy enemy
  • Death sequence (tocado):
    • Explosion animation (itocado_ counter)
    • Ship shrinking
    • Debris particles (chatarra_cosmica)
    • Respawn after delay
  • Important: Original Pascal used bit-packed framebuffer (llig() function)
    • Need to adapt to SDL3 rendering pipeline
    • Options: render to texture, software buffer, or geometric collision

🔲 Phase 11: Polish & Refinements

  • Draw borders (marges)
  • Text rendering with SDL_ttf (TODO for later)
  • Sound effects (optional)
  • Score system (optional)

🔲 Phase 12: Cross-Platform Testing

  • Test on Linux, macOS, Windows
  • Create release builds
  • Package with resources

Known Issues & Tuning Needed

  1. Ship physics constants: User mentioned "sigue sin ir fino" - may need adjustment:

    • ROTATION_SPEED (currently 2.5 rad/s)
    • ACCELERATION (currently 100.0 px/s²)
    • MAX_VELOCITY (currently 200.0 px/s)
    • FRICTION (currently 6.0 px/s²)
  2. Enemy movement: May need speed/bounce angle tuning

    • VELOCITAT_SCALE (currently 20.0)
    • Reflection angles (PI - angle, 2*PI - angle)
  3. Bullet speed: May need adjustment

    • velocitat = 6.0f (120 px/s)

Important Pascal References (Original Code)

The original Pascal game is in source/ASTEROID.PAS (if available). Key procedures:

  • teclapuls - Keyboard handler (converted to SDL_GetKeyboardState)
  • mou_nau - Ship movement (now actualitzar ship section)
  • mou_orni - Enemy movement (joc_asteroides.cpp:367-405)
  • mou_bales - Bullet movement (joc_asteroides.cpp:444-465)
  • rota_tri - Ship rendering (joc_asteroides.cpp:300-337)
  • rota_pol - Polygon rendering (joc_asteroides.cpp:339-365)
  • linea - Bresenham line (joc_asteroides.cpp:230-298)
  • tocado - Death sequence (TODO Phase 10)

Controls

  • Arrow Keys (UP/DOWN/LEFT/RIGHT): Ship movement (continuous)
  • SPACE: Shoot (event-based)
  • ESC: Quit

Debug Output

Ship debug info printed every second (joc_asteroides.cpp:115-125):

static float time_accumulator = 0.0f;
time_accumulator += delta_time;
if (time_accumulator >= 1.0f) {
    std::cout << "Nau: pos(" << nau_.centre.x << "," << nau_.centre.y
              << ") vel=" << (int)nau_.velocitat << " px/s"
              << " angle=" << (int)(nau_.angle * 180/PI) << "°"
              << " dt=" << (int)(delta_time * 1000) << "ms" << std::endl;
    time_accumulator -= 1.0f;
}

Next Session Priorities

  1. Phase 10: Collision Detection

    • Most complex phase
    • Need to decide: geometric vs pixel-perfect collision
    • Implement tocado() death sequence
    • Bullet-enemy collision
  2. Phase 11: Polish

    • Draw borders
    • Consider text rendering (score, lives)
  3. Phase 12: Release

    • Cross-platform testing
    • Final physics tuning

IMPORTANT: Modernization Architecture (BETA 3.0)

Starting from BETA 3.0, the project has evolved into a professional modular architecture:

Structural Changes

Before (BETA 2.2):

source/
├── main.cpp
├── sdl_manager.hpp/cpp
├── joc_asteroides.hpp/cpp
└── asteroids.cpp (Pascal)

Now (BETA 3.0):

source/
├── core/                    - Reusable engine
│   ├── defaults.hpp         - SINGLE SOURCE OF TRUTH for constants
│   ├── types.hpp            - Data structures
│   └── rendering/
│       ├── sdl_manager      - Dynamic windows + viewport
│       └── primitives       - Pure geometric functions
├── game/                    - Asteroids-specific logic
│   ├── constants.hpp        - Aliases for backward compatibility
│   └── joc_asteroides       - Game loop
├── utils/                   - Shared utilities
├── main.cpp                 - Entry point
└── legacy/
    └── asteroids.cpp        - Original Pascal code (reference)

Dynamic Window System

Controls:

  • F1: Decrease window (-100px width/height)
  • F2: Increase window (+100px width/height)
  • F3: Toggle fullscreen
  • ESC: Exit (unchanged)

Behavior:

  • Window starts at 640x480 centered on screen
  • Each resize keeps window centered on itself
  • Minimum size: 320x240
  • Maximum size: Calculated from display resolution (limit -100px)

Viewport Scaling (SDL3):

  • Game ALWAYS renders in logical coordinates 640x480
  • SDL3 automatically scales to any physical window size
  • Aspect ratio preserved (4:3 with letterboxing)
  • Vectors look SHARPER in larger windows (higher resolution)
  • Game physics UNCHANGED (still px/s relative to 640x480 logical)

Implementation:

SDL_SetRenderLogicalPresentation(
    renderer_,
    640, 480,  // Fixed logical size
    SDL_LOGICAL_PRESENTATION_LETTERBOX  // Maintain aspect ratio
);

Configuration System

core/defaults.hpp - Only place to change constants:

namespace Defaults {
    namespace Window {
        constexpr int WIDTH = 640;
        constexpr int HEIGHT = 480;
        constexpr int SIZE_INCREMENT = 100;  // F1/F2
    }

    namespace Game {
        constexpr int WIDTH = 640;           // Logical
        constexpr int HEIGHT = 480;
        constexpr int MARGIN_LEFT = 20;      // MARGE_ESQ
        constexpr int MARGIN_RIGHT = 620;    // MARGE_DRET
        // ...
    }

    namespace Physics {
        constexpr float ROTATION_SPEED = 2.5f;
        constexpr float ACCELERATION = 100.0f;
        // ...
    }
}

game/constants.hpp - Backward compatibility:

using Defaults::Game::MARGIN_LEFT;  // For legacy code using MARGE_ESQ
// ...

Important Reminders

1. Logical vs Physical Coordinates

  • ALL game code uses logical coordinates (640x480)
  • NO need to adjust physics or collision calculations
  • SDL3 handles conversion automatically

2. Rendering

  • linea(), rota_tri(), rota_pol() still use direct coords
  • NO manual transformation, SDL does it internally

3. Configuration

  • NEVER use magic numbers in new code
  • Always reference Defaults::*
  • For game values, create aliases in game/constants.hpp if needed

4. Future OpenGL

  • Current system allows migrating to OpenGL without changing game code
  • Would only require changing SDLManager and rendering function implementations
  • Postponed until needing >50 enemies or complex effects

Compilation

No changes:

make clean && make
./asteroids

Files modified by CMake:

  • Updated to include subdirectories core/, game/
  • Include path: ${CMAKE_SOURCE_DIR}/source (for relative includes)

Migration for Future Sessions

If you find code using magic numbers:

  1. Add constant in core/defaults.hpp in the appropriate namespace
  2. If it's a frequently used game value, create alias in game/constants.hpp
  3. Replace the number with the constant
  4. Compile and verify

Example:

// Before (bad):
if (enemy.centre.x < 20 || enemy.centre.x > 620) { ... }

// After (good):
if (enemy.centre.x < MARGIN_LEFT || enemy.centre.x > MARGIN_RIGHT) { ... }

Tips for Future Claude Code Sessions

  • Always read this file first before making changes
  • Preserve Valencian naming: nau, orni, bales, centre, velocitat, etc.
  • Time-based physics: All movement must multiply by delta_time
  • Polar coordinates: Core to the game, don't change to Cartesian
  • Test compilation after each change: make clean && make
  • Visual velocity scaling: Remember to scale velocitat before passing to rota_tri
  • Angle convention: angle=0 points UP (not right), hence angle - PI/2 in calculations
  • One bullet at a time: Original game limitation, preserve it
  • Simple code style: Avoid over-engineering, keep "small DOS program" feel
  • Use defaults.hpp: Never hardcode constants, always use Defaults namespace