# 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. ### Build Commands ```bash # Clean + compile make clean && make # Run ./orni # Individual targets make linux # Linux build make macos # macOS build make windows # Windows build (MinGW) ``` ### Build Files - **CMakeLists.txt** - CMake configuration (C++20, SDL3, project metadata) - **Makefile** - Cross-platform wrapper, extracts project info from CMakeLists.txt - **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) ### Project Metadata System **Auto-generation with CMake**: CMake generates `build/project.h` from `source/project.h.in` template on every compilation: ```cpp // build/project.h (generated automatically) namespace Project { constexpr const char* NAME = "orni"; // From project(orni ...) constexpr const char* LONG_NAME = "Orni Attack"; // From PROJECT_LONG_NAME constexpr const char* VERSION = "0.1.0"; // From VERSION constexpr const char* COPYRIGHT = "© 1999..."; // From PROJECT_COPYRIGHT constexpr const char* GIT_HASH = "abc1234"; // From git rev-parse } ``` **Window title format** (dynamic, in sdl_manager.cpp): ```cpp std::format("{} v{} ({})", Project::LONG_NAME, // "Orni Attack" Project::VERSION, // "0.1.0" Project::COPYRIGHT) // "© 1999 Visente i Sergi, 2025 Port" ``` Result: `Orni Attack v0.1.0 (© 1999 Visente i Sergi, 2025 Port)` **Single source of truth**: All project info in CMakeLists.txt, no hardcoded strings. ## 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): ```cpp 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 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) ```cpp 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: ```cpp // 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: ```cpp 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): ```cpp 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): ```cpp 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): ```cpp // 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: ```cpp 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: ```cpp 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`): ```cpp // 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: ```cpp 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: ```cpp 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: ```cpp void rota_pol(const Poligon& pol, float angul, bool dibuixar) { // Convert all polar points to Cartesian std::array 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: ```cpp 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): ```cpp 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) ```cpp // 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 ```cpp 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 ```cpp 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) ```cpp 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) ```cpp 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 } ``` ## 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): ```cpp 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:** ```cpp 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: ```cpp 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: ```cpp 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:** ```bash 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: ```cpp // 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