resolveBodyPair afegeix early-out per a parells on a.radius<=0 o b.radius<=0.
Honra el comentari de bullet.cpp:30 ("radius=0 → sin colisión física,
cinemática pura") que abans no s'aplicava: amb bala radius=0 + enemic
radius=ENEMY_RADIUS, SUM_R era enemic radius i el body-body disparava
si la bala (a 700 px/s) penetrava el cos l'enemic entre frames.
Símptomes corregits:
- Pentagon: la bala "rebotava espectacularment" en lloc d'impactar.
- Quadrat: rebut un impulse double del cantó de la física que es
sumava (o cancel·lava, segons l'angle) al manual, fent l'efecte
inconsistent.
Ara la gameplay collision (Physics::checkCollision amb entity radius,
que ja és més generós) és l'única que tracta el parell bala-enemic.
A més: IMPACT_MOMENTUM_FACTOR 2.0 → 3.0 per compensar la pèrdua del
rebot físic i donar més empenta:
- Pentagon (m=5) Δv = 210 px/s
- Quadrat (m=8) Δv = 131 px/s
- Molinillo (m=4) Δv = 262 px/s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- IMPACT_MOMENTUM_FACTOR: 1.0 → 2.0 (doble del moment de la bala).
Pentagon Δv = 140 px/s (≈4× la seva velocity base), prou clar.
- Enemy::update: salta el switch de behavior (Pentagon zigzag,
Quadrat tracking, Molinillo proximity-spin) mentre wounded_timer_>0.
El enemic herit és un "cos mort" inert: només respon a la inèrcia
del impulse rebut i a les col·lisions físiques resoltes per
PhysicsWorld. Abans, el Quadrat renormalitzava la velocity cada 1s
cap al ship, esborrant la inèrcia.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sustitueix IMPACT_IMPULSE (magnitud arbitrària radial) per
IMPACT_MOMENTUM_FACTOR (factor de transferència del moment de la bala).
El impulse ara és bullet.body.velocity * (bullet.body.mass * factor),
és a dir el moment lineal real de la bala, dirigit cap a on viatjava.
Amb factor=1.0 i la bala (m=0.5, v=700 px/s):
- Pentagon (m=5) → Δv = 70 px/s (doble de la seva velocity base)
- Quadrat (m=8) → Δv = 44 px/s
- Molinillo (m=4) → Δv = 88 px/s
Visiblement notable durant el segon de "ferit" abans de l'explosió.
El factor és tunable per pujar/baixar segons gusts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Systems::Collision::detectWoundedChain itera parells d'enemics: si
exactament un està herit i toquen (Physics::checkCollision), el sa entra
en estat herit propagant last_hit_by_ → la cascada de morts segueix
acreditant el shooter original. El rebot físic ja el gestiona
PhysicsWorld; aquí només propaguem l'estat.
Hook a detectAll just després de detectBulletEnemy: les balles tenen
prioritat sobre la cadena del mateix frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enemy::draw() ara, si wounded_timer_ > 0, alterna entre el color del
tipus i Defaults::Palette::WOUNDED (dorat) a Wounded::BLINK_HZ usant
fmod sobre el periode del cicle — patró reutilitzat del Ship::draw()
d'invulnerabilitat però aplicat a color en lloc de visibilitat.
A 10 Hz amb DURATION=1s dóna ~10 parpadeigs visibles abans d'explotar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE (placeholder 1.0).
- Enemy::herir(shooter_id) emmagatzema last_hit_by_ per a atribució posterior.
- collision_system: helper anònim explodeNow(ctx, enemy, shooter_id) que
llegeix velocity/dades ABANS de destruir() (corregeix bug latent: el codi
anterior llegia getVelocityVector() després de destruir, que zera velocity
→ l'explosió mai heretava inèrcia).
- detectBulletEnemy: primer impacte aplica impulse + herir(); segon impacte
sobre enemy ferit dispara explodeNow immediata.
- processWoundedDeaths: explota enemics amb wound timer expirat aquest frame.
- detectAll: processWoundedDeaths abans de detectBulletEnemy (les expiracions
maten primer; les bales del mateix frame ja no toquen el cos destruït).
Puntos s'atribueixen a la mort real, no a l'impacte inicial.
Build neta i smoke test xvfb OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Defaults::Physics::Bullet::IMPACT_IMPULSE (50 px·s placeholder)
- detectBulletEnemy: calcula normal bullet→enemy, normalitza
(fallback a direcció de bala o (0,-1) si estan solapats) i crida
enemy.applyImpulse(normal * IMPACT_IMPULSE) abans de destruir.
El destruir() immediat encara zera la velocity, així que l'efecte
visual no es nota: serà visible quan la Fase 3 difereixi la mort.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Defaults::Palette::WOUNDED ({255,215,0}) dorat per a parpadeig
- Defaults::Enemies::Wounded::{DURATION, BLINK_HZ}
- Enemy: wounded_timer_, wound_expired_this_frame_
- API: herir(), isWounded(), getWoundedTimer(),
woundExpiredThisFrame(), consumeWoundExpired(), applyImpulse()
- update() decrementa timer i marca expiració al creuar 0
- destruir() reseteja l'estat wounded
Sense efectes visuals ni canvis de comportament: cap callsite invoca
encara herir() ni applyImpulse(). Build verda i smoke test xvfb OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reemplaça core/defaults.hpp pels subheaders concrets a director.cpp i
config_yaml.cpp (silencia unused-includes de clangd). Marca el umbrella
amb IWYU pragma: begin_exports/end_exports per evitar falsos positius
als consumidors transitius.
Separa el tamany lògic (1280×720) del render target offscreen. Llista
tancada de 5 presets 16:9 (720p/900p/1080p/1440p/2160p) llegida de
rendering.render_{width,height} amb fallback a 1280×720 si invàlida.
Inclou API resizeRenderTarget() preparada per al menú de servei futur.
- COLOR_INFO passa de blanc neutre (230,230,230) a cian (80,230,255)
per a diferenciar més els toasts informatius dels d'avís/error.
- TEXT_SCALE de 0.4 → 0.55 perquè el text sigui més llegible amb
l'aspect-fit del viewport.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
La primera ESC ja no tanca el joc directament: dispara un toast
"PREMEU ESC UN ALTRE COP PER EIXIR" en vermell. Mentre el toast està
entrant o aguantant (Notifier::isActiveWindow()), una segona ESC
confirma i tanca. Si l'usuari espera a que el toast comenci a sortir
o desaparegui, ESC torna a obrir la finestra de confirmació sense
tancar — només una doble pulsació consecutiva tanca.
Si el Notifier no existeix (no hauria de passar dins runFrameLoop),
ESC manté el comportament antic de tancar directament.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Substitueixen els std::cout dels handlers per crides a notifyInfo() del
Notifier:
- F1/F2: ZOOM: X.YX (amb el valor actual)
- F3: PANTALLA COMPLETA / MODE FINESTRA
- F4: VSYNC ACTIU / VSYNC INACTIU
- F5: AA ACTIU / AA INACTIU
Tots els missatges en majúscules perquè la font vectorial actual només
té glyphs A-Z. Es manté la lògica de toggle i de persistència de cfg;
únicament canvia el canal de feedback (consola → toast HUD).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Notifier singleton (System::init/get/destroy) que dibuixa un cuadre
centrat al centre-superior amb fons semitransparent (derivat oscur del
color del text) i bordes en línies.
- Màquina d'estats HIDDEN → ENTERING → HOLDING → EXITING amb easing
outCubic (entrada) i inCubic (sortida), slide de 300 ms.
- pushRect() afegit a GpuFrameRenderer (2 triangles, edge_dist=0) per
poder pintar el fons opac/semitransparent reutilitzant el pipeline de
línies — sense afegir cap pipeline nou.
- VectorText::render/renderCentered admeten color RGBA explícit
(default {0,0,0,0} preserva el comportament previ amb oscil·lador
global de color).
- Easing header-only a core/utils/easing.hpp (outCubic, inCubic).
- Director crea Notifier just després del DebugOverlay i el draweja com
a última capa per damunt de l'escena i el debug.
Encara cap consumer el crida; els F1-F5 i la doble pulsació d'ESC
arriben en commits posteriors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Permet alternar l'AA geomètric en runtime:
- Action::TOGGLE_ANTIALIAS bound a F5.
- GlobalEvents::handle reacciona al scancode F5 cridant sdl.toggleAntialias().
- SDLManager::toggleAntialias muta cfg_->rendering.antialias i propaga a
gpu_renderer_.setAntialias().
- GpuFrameRenderer manté l'estat antialias_enabled_ (true per defecte) i
pushLine adapta extrusió i edge_dist en funció del flag — geometria nua
quan està OFF, fade als bords quan està ON.
- RenderingConfig guanya el camp `antialias{1}` per coherència amb vsync;
l'estat NO es persisteix al YAML de moment (decisió volgudament conservadora,
podem afegir-ho en un commit a part si cal).
- DebugOverlay (F11) mostra una tercera línia "AA: ON/OFF" sota VSYNC per
poder comparar a temps real.
Afegim antialias geomètric (sense MSAA) al pipeline de línies aprofitant
que la línia ja es construeix com a quad extruït a CPU:
- LineVertex: nou camp edge_dist (±1 als laterals del quad, 0 al centre).
- pushLine: extrudeix 0.5px extra per banda (AA_PADDING) per allotjar el
fade sense menjar gruix nominal.
- line.vert: passa l'edge_dist al fragment com a varying.
- line.frag: alpha *= 1 - smoothstep(0.7, 1.0, |edge_dist|) — fade Hermite
C¹ als bords, sense banding.
AA actiu per defecte. El toggle a runtime (F5) ve en el commit següent.
setVSync demanava SDL_GPU_PRESENTMODE_IMMEDIATE sense comprovar suport.
A SDL_GPU només VSYNC està garantit; IMMEDIATE i MAILBOX són opcionals.
Si no estaven suportats (típicament Wayland/X11 amb compositor), SDL
retornava error i la swapchain es quedava en VSYNC sense que ho sabéssim.
Ara:
- Consultem SDL_WindowSupportsGPUPresentMode abans de fer la crida.
- En VSync OFF: provem IMMEDIATE → fallback a MAILBOX → si cap, ens
quedem en VSYNC i avisem (driver/compositor força VSync).
- Loggejem sempre el mode efectiu (no només els errors), perquè ara mateix
no hi havia forma de saber des de fora si el toggle havia tingut efecte.
Quan arribava un SDL_EVENT_WINDOW_RESIZED en fullscreen, recalculàvem
zoom_factor_ a partir de la mida física i el clampàvem a max_zoom_. Això
"consumia" el zoom_factor_ que tenia l'usuari en windowed, així que en
tornar a windowed (F3) la mida quedava la del clamp, no la prèvia.
Ara, en fullscreen, zoom_factor_ i windowed_*_ es deixen intactes (no
participen del càlcul del viewport, que ja és aspect-fit sobre la mida
física). En windowed, comportament inalterat.
A SDL3, SDL_SetWindowFullscreen(true) sol hereta el SDL_DisplayMode que tingués
la finestra. Sense una crida prèvia a SDL_SetWindowFullscreenMode(win, nullptr),
el comportament és no determinista entre invocacions (i pot acabar en mode
exclusiu si abans hi havia hagut un mode setejat).
Afegim la crida amb mode=nullptr just abans d'activar el fullscreen perquè
sempre entrem en "borderless desktop" (cobrint el monitor on viu la finestra).
El viewport del pase final es calculava com `Game::WIDTH * zoom_factor_`. Però
`zoom_factor_` està capat a `max_zoom_`, que es deriva de `display - 100px`
(marge per a decoracions). En fullscreen això deixa marc negre als 4 costats:
amb display 1920×1080 max_zoom_≈1.25 → viewport 1600×900 dins de 1920×1080.
Ara l'escala es calcula directament de la finestra física actual com a
aspect-fit (`min(curW/1280, curH/720)`), de manera que el viewport sempre
omple un eix i lletraboxeja l'altre, independentment del zoom_factor_. El
zoom_factor_ continua dimensionant la finestra en mode windowed (F1/F2).
Pas 7 final del hallazgo #28. La capa de game/options havia esdevingut
exclusivament una capa de persistència YAML que llegia i escrivia
Config::EngineConfig. El nom "Options" no reflectia bé aquest rol.
Canvis:
- Renomenats fitxers: game/options.{hpp,cpp} → game/config_yaml.{hpp,cpp}
(preservant la història de git via mv).
- Renomenat namespace: Options → ConfigYaml.
- Esborrades del .hpp les referències-alias inline (window, rendering,
player1, player2, keyboard_controls, gamepad_controls, console) que
ja no tenien call-sites externs (només existien per a la transició).
El .hpp ara només exposa engine_config + version + path + funcions.
- A config_yaml.cpp s'introdueixen aliases internes (anonymous namespace)
per mantenir llegible el codi de la implementació, sense exposar-les.
- Actualitzat main.cpp per a usar ConfigYaml::*.
- Actualitzats els comentaris stale a sdl_manager.hpp, director.hpp,
engine_config.hpp i audio.hpp.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pas 5/N del hallazgo #28.
Director deixa d'incloure game/options.hpp i les seves crides a
Options::*. El seu ctor accepta ara:
- Config::EngineConfig& cfg → la struct runtime (window, console, ...).
- Config::ConfigPersistence → 4 lambdes (init/set_path/load/save)
que delegen la persistència a la capa concreta (game/Options::*).
Cap més referència a Options:: ni a "game/..." dins del Director:
- cfg_->* substitueix tot Options::* (window, console, player1/2,
rendering, engine_config).
- persistence_.{init,save,load,set_path} substitueix les funcions
d'I/O de YAML.
run() i checkProgramArguments deixen de ser estàtics (necessiten
accés a cfg_ i persistence_). Això també desfà el smell del
hallazgo #37 (Director::run estàtic que llegia estat d'instància).
main.cpp queda com a orquestrador: construeix la struct
ConfigPersistence amb lambdes que enllacen amb Options::* i la
injecta al Director.
Afegit: Config::ConfigPersistence a engine_config.hpp.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pas 4/N del hallazgo #28.
SDLManager deixa d'incloure game/options.hpp. El ctor accepta ara una
Config::EngineConfig& (per llegir/mutar window i rendering) i un
opcional std::function<void()> on_persist callback.
Canvis funcionals:
- Es mantenen les mutacions de window.{width,height,zoom_factor,fullscreen}
però ara sobre cfg_->window en lloc d'Options::window. Comportament
idèntic perquè Options::window és un alias a engine_config.window.
- toggleVSync deixa de cridar Options::saveToFile() directament i
invoca on_persist_ si està connectat. El Director li passa una lambda
que fa la persistència (mantenint sdl_manager agnòstic).
- initWindowAndGpu (free function) rep el vsync inicial per paràmetre.
- Eliminat el ctor per defecte (SDLManager()) que no era cridat des de
cap call-site del projecte.
Cleanup preexistent surfat per clang-tidy en treure el ctor default:
- finestra_, max_width_, max_height_, max_zoom_ passen a tindre
default member initializers; eliminat el seu ctor mem-init redundant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pas 3/N del hallazgo #28.
Input deixa d'incloure game/options.hpp. Els antics applyPlayerXFromOptions
es renombren a applyPlayerXBindings(const Config::PlayerBindings&) i
reben els bindings per paràmetre en lloc de llegir-los del global
Options::*. El Director hi passa Options::player1/2 als call-sites.
Esborrats applyKeyboardBindingsFromOptions i applyGamepadBindingsFromOptions
que no eren cridats per ningú (dead code aprofitat).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pas 2/N del hallazgo #28.
DebugOverlay deixa d'incloure game/options.hpp i passa a rebre un
const Config::RenderingConfig& en el seu constructor. El Director li
passa Options::rendering (que ja és un alias d'engine_config.rendering).
Eliminat: include "game/options.hpp" des de core/system/debug_overlay.cpp.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pas 1/N del hallazgo #28. Sense canvi de comportament:
Nou: source/core/config/engine_config.hpp
- Defineix Config::EngineConfig (POD) amb les sub-structs WindowConfig,
RenderingConfig, KeyboardBindings, GamepadBindings, PlayerBindings i
el flag console.
- Sense singletons ni virtuals; la inversió real es fa en commits
posteriors injectant Config::EngineConfig& per constructor.
Modificat: source/game/options.hpp
- Elimina les struct definitions locals (Window, Rendering, ...).
- Afegeix Options::engine_config (única font de veritat).
- Conserva Options::window, Options::rendering, player1, player2,
keyboard_controls, gamepad_controls i console com a referències
inline a camps d'engine_config. Cost runtime zero, callsites
existents no requereixen cap canvi.
Hallazgo #28 de CODE_REVIEW.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
defaults.hpp tenia 527 línies amb 17 namespaces de dominis distints
(Window, Game, Zones, Entities, Palette, Ship, Physics, Math,
Brightness, Rendering, Audio, Music, Sound, Controls, Enemies, Title,
FloatingScore). 22 .cpp/.hpp l'incloïen, així que tocar una constant
forçava recompilar pràcticament tot.
Es divideix en 15 subfitxers (un per namespace, fusionant Music/Sound
a audio.hpp i unificant els dos blocs Game duplicats en un sol):
defaults/window.hpp defaults/audio.hpp
defaults/game.hpp defaults/controls.hpp
defaults/zones.hpp defaults/enemies.hpp
defaults/entities.hpp defaults/title.hpp
defaults/palette.hpp defaults/floating_score.hpp
defaults/ship.hpp defaults/math.hpp
defaults/physics.hpp defaults/brightness.hpp
defaults/rendering.hpp
Cross-deps explícites (#include en lloc d'order-of-declaration):
zones.hpp -> game.hpp (per Game::WIDTH/HEIGHT)
enemies.hpp -> entities.hpp (per SHIP_RADIUS)
title.hpp -> game.hpp, math.hpp + <cmath>
defaults.hpp queda com a umbrella que inclou els 15 subfitxers. Els
22 includers existents no requereixen cap canvi. Codi nou pot
incloure el subfitxer concret per millorar la compilació incremental.
Hallazgos #22 i #30 de CODE_REVIEW.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Constants legacy heretades del Pascal, en unitats/frame, que la
migració a SDL3 va deixar sense ús real:
- ENEMY_SPEED i BULLET_SPEED només es llegien des d'Options::physics
(esborrat al #21) i des de Constants::VELOCITAT/VELOCITAT_MAX
(esborrat al #24). Ara amb zero callers.
- VELOCITY_SCALE no tenia callers (les velocitats efectives es
calculen a Bullet::BULLET_SPEED = 140 px/s i a
Defaults::Enemies::{Pentagon,Cuadrado,Molinillo}::VELOCITAT).
S'ajusta el comentari del namespace per reflectir que ara conté
només la física del control de la nau.
Hallazgo #25 de CODE_REVIEW.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>