1096 lines
34 KiB
Markdown
1096 lines
34 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
make # Compile (delegates to CMake)
|
|
make debug # Debug build
|
|
make clean # Clean artifacts
|
|
./orni # Run
|
|
```
|
|
|
|
### Release Packaging
|
|
|
|
```bash
|
|
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 (`make` → `cmake --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`:
|
|
|
|
```cmake
|
|
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`:
|
|
|
|
```cpp
|
|
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:
|
|
|
|
```cmake
|
|
# 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):
|
|
|
|
```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<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)
|
|
|
|
```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<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:
|
|
|
|
```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
|
|
}
|
|
```
|
|
|
|
## 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)
|
|
|
|
```cpp
|
|
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)
|
|
|
|
```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)
|
|
|
|
```cpp
|
|
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)
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// 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):
|
|
|
|
```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
|