diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 1cccecd..71099a5 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -9,6 +9,7 @@ #include "core/input/gamepad_manager.hpp" #include "core/input/input_state.hpp" +#include "core/rendering/screen.hpp" #include "core/resources/resource_cache.hpp" #include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" @@ -689,6 +690,39 @@ void Player::render() { pos_.y = SAVED_Y; } +void Player::renderShadow() const { + // Sense salt actiu, no hi ha sombra: això simplifica els callsites, + // que poden cridar `renderShadow` per a tots els jugadors sense + // discriminar. + if (!isAirborne()) { return; } + + // Sombra centrada al sprite, recolzada a la línia dels peus reals + // (ignorant `jump_z_`). L'amplada es redueix proporcional a l'altura + // del salt — al pic queda més estreta, donant la sensació de + // "elevació" sense necessitat de coordenades 3D reals. Si el + // jump_config té un divisor zero (cas degenerat), només dibuixem + // la sombra a mida plena per evitar nan. + const auto PEAK = (jump_config.gravity_px_per_s2 > 0.0F) + ? (jump_config.impulse_px_per_s * jump_config.impulse_px_per_s) / (2.0F * jump_config.gravity_px_per_s2) + : 0.0F; + const float NORM_Z = (PEAK > 0.0F) ? std::clamp(jump_z_ / PEAK, 0.0F, 1.0F) : 0.0F; + constexpr float MAX_SHRINK = 0.4F; // Fins a un 40% més estreta al pic. + const float SHRINK = 1.0F - (NORM_Z * MAX_SHRINK); + + constexpr int SHADOW_H = 3; + const auto SHADOW_W = static_cast(static_cast(base_size) * SHRINK); + const float CX = getPosX() + (static_cast(sprite_w) * 0.5F); + const float GROUND_Y = getGroundFeetY(); + const auto SX = static_cast(CX - (static_cast(SHADOW_W) * 0.5F)); + const auto SY = static_cast(GROUND_Y) - SHADOW_H; + + // ARGB negre amb alpha ~40%. `blendRect` mescla sobre el canvas; així + // la sombra fosqueja el sòl (pintat o no) sense tapar-lo del tot. + constexpr std::uint32_t SHADOW_ARGB = 0x00000000U; + constexpr std::uint8_t SHADOW_ALPHA = 100; + Screen::get().blendRect(SX, SY, SHADOW_W, SHADOW_H, SHADOW_ARGB, SHADOW_ALPHA); +} + auto Player::animationName(bool moving) const -> const char* { // En l'aire forcem la variant `jump_*` (per direcció), independent de // si hi ha velocitat horitzontal: per al jugador, l'estat visual del diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index b7a7386..5ef685f 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -212,6 +212,12 @@ class Player : public AnimatedSprite { // la posició al món no s'alteren. void render() override; + // Pinta una sombra translúcida sota el jugador a nivell de sòl + // (sense aplicar `jump_z_`). El GameScene la crida en una passada + // pre-actors, així queda per sota dels actors i del propi jugador. + // No-op si el jugador no està en l'aire. + void renderShadow() const; + private: // Alias privat històric. Mantingut perquè viu dins de l'implementació // (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`. diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 23fdbca..a4278c2 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -532,6 +532,12 @@ void GameScene::render() { // explícita: l'editor edita el tilemap, no la capa de pintat). if (!SKIP_ACTORS && paint_layer_) { paint_layer_->render(); } if (!SKIP_ACTORS) { + // Passada de sombres dels jugadors saltant. Va per davant del + // pintat (al damunt) però per darrere de TOTS els actors — + // entitats i jugadors, inclòs el saltador mateix— així la sombra + // queda al sòl sense tapar res. `renderShadow` és no-op si el + // jugador està a terra, així que no cal filtrar al callsite. + for (const auto& p : players_) { p->renderShadow(); } // Z-order interleaving entitats ↔ players per `getFeetY()` ascendent: // els actors amb els peus més amunt queden dibuixats primer (al fons), // els més avall al davant. Així un sarc situat per darrere d'un player