From c38c80753af72f34a9aa5f17835a44d4a4707593 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 25 Apr 2026 15:29:48 +0200 Subject: [PATCH] =?UTF-8?q?feat(player):=20direcci=C3=B3=20preferida=20al?= =?UTF-8?q?=20HURT=20segons=20l'offset=20d'impacte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/entities/player.cpp | 141 +++++++++++++++++++------------- source/game/entities/player.hpp | 31 +++++-- 2 files changed, 108 insertions(+), 64 deletions(-) diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 1f1e23b..5762236 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -584,74 +584,81 @@ void Player::startJump() { // tocaran fins a aterrar. } -void Player::enterHurt() { +auto Player::deltasFor(Cardinal c) -> std::pair { + switch (c) { + case Cardinal::EAST: + return {1.0F, 0.0F}; + case Cardinal::WEST: + return {-1.0F, 0.0F}; + case Cardinal::NORTH: + return {0.0F, -1.0F}; + case Cardinal::SOUTH: + return {0.0F, 1.0F}; + case Cardinal::NONE: + break; + } + return {0.0F, 0.0F}; +} + +auto Player::reachInDirection(float dx, float dy, int max_px) const -> int { + const auto BASE = getBaseBox(); + for (int i = 1; i <= max_px; ++i) { + SDL_FRect probe = BASE; + probe.x += static_cast(i) * dx; + probe.y += static_cast(i) * dy; + if (map_.collides(probe) || !map_.inside_playfield(probe)) { return i - 1; } + } + return max_px; +} + +auto Player::chooseHurtDirection(Cardinal preferred) const -> Cardinal { + constexpr int MAX_PX = 32; // 2 tiles, prou per a la cerca; la inèrcia limita el recorregut real. + + // Recorregut natural del HURT (distància que recórreria sota + // decaiment lineal pur). Si la direcció preferida cabe sense topar + // abans d'aquesta distància, l'usem; d'altra manera, busquem la + // millor. + const auto REQUIRED = static_cast( + 0.5F * hurt_config.initial_speed_px_per_s * (static_cast(hurt_config.brake_time_ms) / 1000.0F)); + + if (preferred != Cardinal::NONE) { + const auto [DX, DY] = deltasFor(preferred); + if (reachInDirection(DX, DY, MAX_PX) >= REQUIRED) { return preferred; } + } + + constexpr std::array ALL{Cardinal::EAST, Cardinal::WEST, Cardinal::NORTH, Cardinal::SOUTH}; + int best_reach = -1; + Cardinal best_dir = Cardinal::EAST; + for (Cardinal c : ALL) { + const auto [DX, DY] = deltasFor(c); + const int R = reachInDirection(DX, DY, MAX_PX); + if (R > best_reach) { + best_reach = R; + best_dir = c; + } + } + return best_dir; +} + +void Player::enterHurt(Cardinal preferred) { // Si la mecànica no està configurada (speed = 0), no fem res — el // comportament queda equivalent al d'abans, sense empenta. if (hurt_config.initial_speed_px_per_s <= 0.0F || hurt_config.brake_time_ms <= 0) { return; } - // Triem la direcció amb més recorregut lliure: provem els 4 - // cardinals i ens quedem el de major distància contra muros/sarcs. - // El test no comprova jugadors (durant HURT el jugador és - // intangible al PvP), només geometria del mapa. - constexpr int MAX_PX = 32; // 2 tiles, prou per a la cerca; la inèrcia limita el recorregut real. - struct Candidate { - Cardinal dir; - float dx; - float dy; - }; - constexpr std::array ALL{{ - {.dir = Cardinal::EAST, .dx = 1.0F, .dy = 0.0F}, - {.dir = Cardinal::WEST, .dx = -1.0F, .dy = 0.0F}, - {.dir = Cardinal::NORTH, .dx = 0.0F, .dy = -1.0F}, - {.dir = Cardinal::SOUTH, .dx = 0.0F, .dy = 1.0F}, - }}; - const auto BASE = getBaseBox(); - int best_reach = -1; - Cardinal best_dir = Cardinal::EAST; - for (const auto& c : ALL) { - int reach = MAX_PX; - for (int i = 1; i <= MAX_PX; ++i) { - SDL_FRect probe = BASE; - probe.x += static_cast(i) * c.dx; - probe.y += static_cast(i) * c.dy; - if (map_.collides(probe) || !map_.inside_playfield(probe)) { - reach = i - 1; - break; - } - } - if (reach > best_reach) { - best_reach = reach; - best_dir = c.dir; - } - } + const Cardinal CHOSEN = chooseHurtDirection(preferred); // Activem l'estat i fixem la velocitat segons la direcció triada. state_ = State::HURT; - applyCardinal(best_dir); - last_input_cardinal_ = best_dir; + applyCardinal(CHOSEN); + last_input_cardinal_ = CHOSEN; turning_remaining_ms_ = 0.0F; pending_cardinal_ = Cardinal::NONE; intermediate_cardinal_ = Cardinal::NONE; being_pushed_ = false; const float V = hurt_config.initial_speed_px_per_s; - intended_vx_ = 0.0F; - intended_vy_ = 0.0F; - switch (best_dir) { - case Cardinal::EAST: - intended_vx_ = V; - break; - case Cardinal::WEST: - intended_vx_ = -V; - break; - case Cardinal::NORTH: - intended_vy_ = -V; - break; - case Cardinal::SOUTH: - intended_vy_ = V; - break; - case Cardinal::NONE: - break; - } + const auto [DX, DY] = deltasFor(CHOSEN); + intended_vx_ = DX * V; + intended_vy_ = DY * V; input_vx_ = 0.0F; input_vy_ = 0.0F; motion_time_ms_ = 0.0F; @@ -692,7 +699,27 @@ void Player::resolveLandingOverlap() { const SDL_FRect MY_HIT = getHitBox(); const SDL_FRect OT_HIT = other->getHitBox(); if (!SDL_HasRectIntersectionFloat(&MY_HIT, &OT_HIT)) { continue; } - other->enterHurt(); + + // Direcció preferida del HURT: cap on l'altre està respecte a + // nosaltres (cardinal dominant del vector centre→centre this → + // other). Si caem a la dreta del seu cos, ell està a l'esquerra + // nostra → preferred = WEST → surt cap a l'esquerra. Si caem + // exactament al damunt (centres alineats), preferred queda + // NONE i `enterHurt` busca la millor direcció lliure. + const float MCX = MY_HIT.x + (MY_HIT.w * 0.5F); + const float MCY = MY_HIT.y + (MY_HIT.h * 0.5F); + const float OCX = OT_HIT.x + (OT_HIT.w * 0.5F); + const float OCY = OT_HIT.y + (OT_HIT.h * 0.5F); + const float DX = OCX - MCX; + const float DY = OCY - MCY; + Cardinal preferred = Cardinal::NONE; + if (std::fabs(DX) >= std::fabs(DY) && DX != 0.0F) { + preferred = (DX > 0.0F) ? Cardinal::EAST : Cardinal::WEST; + } else if (DY != 0.0F) { + preferred = (DY > 0.0F) ? Cardinal::SOUTH : Cardinal::NORTH; + } + + other->enterHurt(preferred); } } diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 40bbdae..b5ebc55 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -411,13 +411,30 @@ class Player : public AnimatedSprite { static void readHurtBlock(const fkyaml::node& yaml); // Activa l'estat HURT: el jugador queda projectat amb velocitat - // inicial `hurt_config.initial_speed_px_per_s` cap a la direcció que - // doni més recorregut lliure (provem els 4 cardinals i triem el de - // major distància fins a topar amb un mur). Decelera linealment fins - // a parar i, en eixe instant, torna a GROUNDED. Sense col·lisió amb - // altres jugadors mentre dura — ja que es travessa l'eventual - // solapament inicial. - void enterHurt(); + // inicial `hurt_config.initial_speed_px_per_s`. La direcció es tria + // així: + // - Si `preferred != NONE` i hi cap el recorregut natural complet + // (sense topar amb mur abans d'esgotar la inèrcia), s'usa + // `preferred`. + // - Altrament (o si `preferred == NONE`), prova els 4 cardinals + // i tria el que dóna més recorregut lliure. + // Decelera linealment fins a parar i, en eixe instant, torna a + // GROUNDED. Sense col·lisió amb altres jugadors mentre dura. + void enterHurt(Cardinal preferred = Cardinal::NONE); + + // Tria la direcció definitiva del HURT. Si `preferred` cabe el + // recorregut natural complet, la retorna; d'altra manera tria la + // direcció amb més recorregut lliure entre els 4 cardinals. + [[nodiscard]] auto chooseHurtDirection(Cardinal preferred) const -> Cardinal; + + // Recorregut lliure (en píxels, fins a `max_px`) en la direcció + // (dx, dy) abans de topar amb un mur o eixir del playfield. La + // base-box es projecta `i` píxels i es comprova; retorna `i-1` al + // primer xoc. + [[nodiscard]] auto reachInDirection(float dx, float dy, int max_px) const -> int; + + // Vectors unitaris dels 4 cardinals. `NONE` retorna (0, 0). + [[nodiscard]] static auto deltasFor(Cardinal c) -> std::pair; // Estat de la màquina d'estats. Mutat per `tick`/`applyMovement`. Per // defecte GROUNDED al spawn.