24 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.
Build Commands
# 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:
// 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):
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.hppaliases
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 parameterevent.key.keyinstead ofevent.key.keysym.symSDL_EVENT_KEY_DOWNinstead ofSDL_KEYDOWNSDL_EVENT_QUITinstead ofSDL_QUITSDL_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
}
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
-
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²)
-
Enemy movement: May need speed/bounce angle tuning
VELOCITAT_SCALE(currently 20.0)- Reflection angles (PI - angle, 2*PI - angle)
-
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
-
Phase 10: Collision Detection
- Most complex phase
- Need to decide: geometric vs pixel-perfect collision
- Implement tocado() death sequence
- Bullet-enemy collision
-
Phase 11: Polish
- Draw borders
- Consider text rendering (score, lives)
-
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.hppif 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:
- Add constant in
core/defaults.hppin the appropriate namespace - If it's a frequently used game value, create alias in
game/constants.hpp - Replace the number with the constant
- 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/2in 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