547 lines
23 KiB
C++
547 lines
23 KiB
C++
#include "game/entities/player.hpp"
|
||
|
||
#include <SDL3/SDL.h>
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <stdexcept>
|
||
#include <utility>
|
||
|
||
#include "core/input/gamepad_manager.hpp"
|
||
#include "core/input/input_state.hpp"
|
||
#include "core/resources/resource_cache.hpp"
|
||
#include "core/resources/resource_helper.hpp"
|
||
#include "external/fkyaml_node.hpp"
|
||
#include "game/entities/event_bus.hpp"
|
||
#include "utils/defaults.hpp"
|
||
|
||
namespace {
|
||
|
||
// Alias local per reduir soroll; la constant viu a Defaults::Player.
|
||
constexpr float SPEED_PX_PER_S = Defaults::Player::SPEED_PX_PER_S;
|
||
|
||
// Dead-zone del stick analògic esquerre (rang Sint16: -32768..32767).
|
||
// Converteix eix analògic en direcció digital; per sota, es considera
|
||
// repòs i no contribueix a la velocitat.
|
||
constexpr Sint16 STICK_DEADZONE = 12000;
|
||
|
||
// Acumula la contribució del mando assignat al jugador (cruceta +
|
||
// stick esquerre) sobre els acumuladors de velocitat. Extracció
|
||
// d'`update()` per a mantenir la complexitat cognitiva sota llindar
|
||
// (threshold 25). L'assignació mando↔jugador viu al `GamepadManager`
|
||
// i la persisteix Options; si aquest jugador no en té cap assignat,
|
||
// la funció és un no-op (sols es mou amb el teclat).
|
||
void accumulateGamepadMovement(int player_id, float& vx, float& vy) {
|
||
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(player_id);
|
||
if (pad == nullptr) { return; }
|
||
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT)) { vx -= SPEED_PX_PER_S; }
|
||
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) { vx += SPEED_PX_PER_S; }
|
||
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP)) { vy -= SPEED_PX_PER_S; }
|
||
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN)) { vy += SPEED_PX_PER_S; }
|
||
const Sint16 LX = InputState::getGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
|
||
const Sint16 LY = InputState::getGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
|
||
if (LX < -STICK_DEADZONE) { vx -= SPEED_PX_PER_S; }
|
||
if (LX > STICK_DEADZONE) { vx += SPEED_PX_PER_S; }
|
||
if (LY < -STICK_DEADZONE) { vy -= SPEED_PX_PER_S; }
|
||
if (LY > STICK_DEADZONE) { vy += SPEED_PX_PER_S; }
|
||
}
|
||
|
||
} // namespace
|
||
|
||
Player::TurnConfig Player::turn_config;
|
||
Player::InertiaConfig Player::inertia_config;
|
||
|
||
void Player::loadFrom(const std::string& yaml_path) {
|
||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||
if (bytes.empty()) {
|
||
throw std::runtime_error("Player: no s'ha pogut obrir " + yaml_path);
|
||
}
|
||
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||
const auto* end = begin + bytes.size();
|
||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||
|
||
sprite_w = yaml["frameWidth"].get_value<int>();
|
||
sprite_h = yaml["frameHeight"].get_value<int>();
|
||
|
||
if (!yaml.contains("collision")) {
|
||
throw std::runtime_error("Player: manca bloc `collision:` a " + yaml_path);
|
||
}
|
||
const auto& c = yaml["collision"];
|
||
collision_size = c["size"].get_value<int>();
|
||
base_size = c["base_size"].get_value<int>();
|
||
|
||
// Col·lisió centrada en el quadrat inferior del sprite. Si la caixa és
|
||
// més gran que la base (cas habitual, 16 vs 12) el padding és simètric
|
||
// a cada costat; si són iguals, padding=0 i la caixa coincideix amb la
|
||
// base sense offsets.
|
||
collision_padding = (collision_size - base_size) / 2;
|
||
collision_x_offset = -collision_padding;
|
||
collision_y_offset = (sprite_h - base_size) - collision_padding;
|
||
|
||
// Hit-box opcional (actor-actor). Si no hi és, s'iguala a la base
|
||
// per preservar compatibilitat amb YAMLs antics. Concèntrica amb la
|
||
// caixa de col·lisió: offset interior = (collision_size - hit_size) / 2.
|
||
hit_size = c.contains("hit_size") ? c["hit_size"].get_value<int>() : collision_size;
|
||
const int HIT_INSET = (collision_size - hit_size) / 2;
|
||
hit_x_offset = collision_x_offset + HIT_INSET;
|
||
hit_y_offset = collision_y_offset + HIT_INSET;
|
||
|
||
// Bloc opcional `inertia:` — si manca, `brake_time_ms` queda a 0 i
|
||
// la inèrcia de frenada no s'activa (parada instantània com abans).
|
||
inertia_config = InertiaConfig{};
|
||
if (yaml.contains("inertia")) {
|
||
const auto& i = yaml["inertia"];
|
||
if (i.contains("brake_time_ms")) { inertia_config.brake_time_ms = i["brake_time_ms"].get_value<int>(); }
|
||
if (i.contains("threshold_ms")) { inertia_config.threshold_ms = i["threshold_ms"].get_value<int>(); }
|
||
}
|
||
|
||
// Bloc opcional `turn:` — si manca, `duration_ms` queda a 0 i la
|
||
// mecànica del gir de 180° no s'activa (canvis de direcció instantanis).
|
||
turn_config = TurnConfig{};
|
||
if (yaml.contains("turn")) {
|
||
const auto& t = yaml["turn"];
|
||
if (t.contains("duration_ms")) { turn_config.duration_ms = t["duration_ms"].get_value<int>(); }
|
||
if (t.contains("transitions")) {
|
||
const auto& tr = t["transitions"];
|
||
if (tr.contains("west_to_east")) { turn_config.w_to_e = cardinalFromString(tr["west_to_east"].get_value<std::string>()); }
|
||
if (tr.contains("east_to_west")) { turn_config.e_to_w = cardinalFromString(tr["east_to_west"].get_value<std::string>()); }
|
||
if (tr.contains("north_to_south")) { turn_config.n_to_s = cardinalFromString(tr["north_to_south"].get_value<std::string>()); }
|
||
if (tr.contains("south_to_north")) { turn_config.s_to_n = cardinalFromString(tr["south_to_north"].get_value<std::string>()); }
|
||
}
|
||
}
|
||
}
|
||
|
||
void Player::applyCardinal(Cardinal c) {
|
||
switch (c) {
|
||
case Cardinal::NORTH:
|
||
facing_ = Facing::UP;
|
||
setFlip(SDL_FLIP_NONE);
|
||
break;
|
||
case Cardinal::SOUTH:
|
||
facing_ = Facing::DOWN;
|
||
setFlip(SDL_FLIP_NONE);
|
||
break;
|
||
case Cardinal::EAST:
|
||
facing_ = Facing::SIDE;
|
||
setFlip(SDL_FLIP_NONE);
|
||
break;
|
||
case Cardinal::WEST:
|
||
facing_ = Facing::SIDE;
|
||
setFlip(SDL_FLIP_HORIZONTAL);
|
||
break;
|
||
case Cardinal::NONE:
|
||
break;
|
||
}
|
||
}
|
||
|
||
auto Player::cardinalFromInput(float vx, float vy) -> Cardinal {
|
||
// Mateixa precedència que la lògica original: l'horitzontal preval
|
||
// sobre el vertical (els diagonals s'interpreten com a laterals).
|
||
if (vx > 0.0F) { return Cardinal::EAST; }
|
||
if (vx < 0.0F) { return Cardinal::WEST; }
|
||
if (vy < 0.0F) { return Cardinal::NORTH; }
|
||
if (vy > 0.0F) { return Cardinal::SOUTH; }
|
||
return Cardinal::NONE;
|
||
}
|
||
|
||
auto Player::cardinalFromString(const std::string& s) -> Cardinal {
|
||
if (s == "north" || s == "up") { return Cardinal::NORTH; }
|
||
if (s == "south" || s == "down") { return Cardinal::SOUTH; }
|
||
if (s == "east" || s == "right") { return Cardinal::EAST; }
|
||
if (s == "west" || s == "left") { return Cardinal::WEST; }
|
||
throw std::runtime_error("Player: cardinal desconegut '" + s + "' al bloc turn.transitions");
|
||
}
|
||
|
||
auto Player::opposite(Cardinal c) -> Cardinal {
|
||
switch (c) {
|
||
case Cardinal::NORTH:
|
||
return Cardinal::SOUTH;
|
||
case Cardinal::SOUTH:
|
||
return Cardinal::NORTH;
|
||
case Cardinal::EAST:
|
||
return Cardinal::WEST;
|
||
case Cardinal::WEST:
|
||
return Cardinal::EAST;
|
||
case Cardinal::NONE:
|
||
return Cardinal::NONE;
|
||
}
|
||
return Cardinal::NONE;
|
||
}
|
||
|
||
auto Player::intermediateFor(Cardinal target) -> Cardinal {
|
||
switch (target) {
|
||
case Cardinal::EAST:
|
||
return turn_config.w_to_e;
|
||
case Cardinal::WEST:
|
||
return turn_config.e_to_w;
|
||
case Cardinal::SOUTH:
|
||
return turn_config.n_to_s;
|
||
case Cardinal::NORTH:
|
||
return turn_config.s_to_n;
|
||
case Cardinal::NONE:
|
||
return Cardinal::NONE;
|
||
}
|
||
return Cardinal::NONE;
|
||
}
|
||
|
||
void Player::updateFacingAndTurn(float delta_time, float vx, float vy) {
|
||
// Cardinal d'INPUT cru. Important: la detecció del gir de 180° s'ha
|
||
// de basar en l'input (intent del jugador), no en la velocitat
|
||
// resultant. Mentre no hi haja inèrcia, vx/vy són directament l'input
|
||
// i serveixen com a tal; quan s'introduïsca, cal preservar aquesta
|
||
// separació (calcular el cardinal a partir de les tecles/gamepad
|
||
// crus, no de la velocitat suavitzada).
|
||
const Cardinal INPUT_CARD = cardinalFromInput(vx, vy);
|
||
|
||
// Si hi ha un gir en curs, descomptem el timer i mantenim el facing
|
||
// forçat al cardinal intermedi. Quan s'esgota, apliquem el cardinal
|
||
// pendent. El moviment físic continua sense alterar-se (vx/vy ja
|
||
// s'han enviat a setVel al caller).
|
||
if (turning_remaining_ms_ > 0.0F) {
|
||
turning_remaining_ms_ -= delta_time * 1000.0F;
|
||
if (turning_remaining_ms_ <= 0.0F) {
|
||
turning_remaining_ms_ = 0.0F;
|
||
applyCardinal(pending_cardinal_);
|
||
pending_cardinal_ = Cardinal::NONE;
|
||
intermediate_cardinal_ = Cardinal::NONE;
|
||
} else {
|
||
applyCardinal(intermediate_cardinal_);
|
||
}
|
||
} else {
|
||
// Detecció del gir: `last_input_cardinal_` és l'últim cardinal
|
||
// d'input no nul (es preserva entre releases). Així, el primer
|
||
// input des de l'estat idle no dispara cap gir — només dispara
|
||
// quan el jugador "canvia d'opinió" prement la direcció oposada
|
||
// a la prèvia. Requereix també que el bloc `turn:` del YAML
|
||
// estiga activat (`duration_ms > 0`).
|
||
const bool TRIGGER_TURN =
|
||
(turn_config.duration_ms > 0) && INPUT_CARD != Cardinal::NONE && last_input_cardinal_ != Cardinal::NONE && INPUT_CARD == opposite(last_input_cardinal_);
|
||
if (TRIGGER_TURN) {
|
||
pending_cardinal_ = INPUT_CARD;
|
||
intermediate_cardinal_ = intermediateFor(INPUT_CARD);
|
||
turning_remaining_ms_ = static_cast<float>(turn_config.duration_ms);
|
||
applyCardinal(intermediate_cardinal_);
|
||
} else if (vx != 0.0F) {
|
||
// Tria de facing original: prevalença lateral, flip segons el
|
||
// signe de vx; verticals purs van a UP/DOWN amb flip NONE; en
|
||
// aturar-se conservem `facing_` per a l'animació stand.
|
||
facing_ = Facing::SIDE;
|
||
setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
|
||
} else if (vy < 0.0F) {
|
||
facing_ = Facing::UP;
|
||
setFlip(SDL_FLIP_NONE);
|
||
} else if (vy > 0.0F) {
|
||
facing_ = Facing::DOWN;
|
||
setFlip(SDL_FLIP_NONE);
|
||
}
|
||
}
|
||
|
||
// Memoritzem l'últim cardinal d'input no nul per a la propera detecció
|
||
// de gir (ignorant els zeros: l'estat idle no reinicia la memòria).
|
||
if (INPUT_CARD != Cardinal::NONE) {
|
||
last_input_cardinal_ = INPUT_CARD;
|
||
}
|
||
}
|
||
|
||
Player::Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index)
|
||
: AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)),
|
||
id_(id),
|
||
controls_(controls),
|
||
map_(map_info),
|
||
paint_tile_index_(paint_tile_index) {
|
||
setPos(x, y);
|
||
// Postura inicial mirant cap al jugador (cara avall), no de costat:
|
||
// forcem facing_ a DOWN i l'animació `stand_down` abans del primer
|
||
// frame d'input — el primer `update` la preservarà si l'usuari no
|
||
// prem cap tecla.
|
||
facing_ = Facing::DOWN;
|
||
setCurrentAnimation("stand_down");
|
||
}
|
||
|
||
void Player::update(float delta_time) {
|
||
tick(delta_time);
|
||
applyMovement(delta_time);
|
||
}
|
||
|
||
auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||
if (map_.collides(box)) { return true; }
|
||
if (all_players_ == nullptr) { return false; }
|
||
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
|
||
// projectem la hit-box des de la posició base proposada i la comparem
|
||
// amb la hit-box actual de l'altre jugador. Així la caixa de moviment
|
||
// (16×16) segueix topant amb murs/sarcs però els jugadors poden
|
||
// acostar-se fins al contacte real.
|
||
const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box);
|
||
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT](const Player* p) {
|
||
if (p == nullptr || p == this) { return false; }
|
||
const auto OTHER = p->getHitBox();
|
||
return SDL_HasRectIntersectionFloat(&PROPOSED_HIT, &OTHER);
|
||
});
|
||
}
|
||
|
||
void Player::integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) {
|
||
// Acceleració instantània: mentre hi ha input, la velocitat salta a
|
||
// ±SPEED sense transició. Cobreix també els canvis de direcció (un
|
||
// coast cap a la dreta passa a -SPEED immediatament si es prem esquerra).
|
||
if (input != 0.0F) {
|
||
phys = input;
|
||
return;
|
||
}
|
||
if (phys == 0.0F) { return; }
|
||
// Hi ha velocitat residual i aquest eix no té input. Tres camins:
|
||
// · Input a l'altre eix (gir de 90°): parada en sec — no arrosseguem
|
||
// en diagonal mentre l'usuari està premant una altra direcció.
|
||
// · Input solta del tot i llindar superat: inèrcia (decaiment lineal).
|
||
// · Input solta del tot i llindar NO superat: parada en sec (passets).
|
||
if (any_input) {
|
||
phys = 0.0F;
|
||
return;
|
||
}
|
||
const bool INERTIA_ACTIVE = (inertia_config.brake_time_ms > 0) && (motion_time_ms >= static_cast<float>(inertia_config.threshold_ms));
|
||
if (!INERTIA_ACTIVE) {
|
||
phys = 0.0F;
|
||
return;
|
||
}
|
||
const float DECEL = SPEED_PX_PER_S / (static_cast<float>(inertia_config.brake_time_ms) / 1000.0F);
|
||
const float STEP = DECEL * delta_time;
|
||
if (std::fabs(phys) <= STEP) {
|
||
phys = 0.0F;
|
||
} else {
|
||
phys -= std::copysign(STEP, phys);
|
||
}
|
||
}
|
||
|
||
void Player::tick(float delta_time) {
|
||
// Reset per frame: el resolver del GameScene el tornarà a posar a
|
||
// true si aquest frame hi ha push actiu. Així quan el push acaba
|
||
// (un deixa d'empentar, l'altre comença a caminar), l'animació torna
|
||
// a governar-se per la velocitat real sense estat romanent.
|
||
being_pushed_ = false;
|
||
// Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el
|
||
// service menu (o qualsevol UI modal) està obert, el wrapper retorna
|
||
// un buffer tot-a-zero perquè el jugador no es mogui mentre l'usuari
|
||
// navega el menú. Polling-blocked; veure `core/input/input_state.hpp`.
|
||
const bool* keys = InputState::getKeyboardState();
|
||
float vx = 0.0F;
|
||
float vy = 0.0F;
|
||
if (keys[controls_.left]) { vx -= SPEED_PX_PER_S; }
|
||
if (keys[controls_.right]) { vx += SPEED_PX_PER_S; }
|
||
if (keys[controls_.up]) { vy -= SPEED_PX_PER_S; }
|
||
if (keys[controls_.down]) { vy += SPEED_PX_PER_S; }
|
||
|
||
// Cruceta i stick esquerre del mando assignat (si n'hi ha). També
|
||
// respecten el bloqueig d'`InputState`, així que amb el menú obert
|
||
// els botons tornen false/0 i no contribueixen a la velocitat.
|
||
accumulateGamepadMovement(id_, vx, vy);
|
||
|
||
input_vx_ = vx;
|
||
input_vy_ = vy;
|
||
|
||
// Integració física per eix. `intended_v*` fa de memòria entre frames:
|
||
// si l'usuari té input, se sobreescriu a ±SPEED; si no, decau per
|
||
// inèrcia o es posa a 0. El `motion_time_ms_` que passem és el del
|
||
// final del frame anterior — serveix per decidir si la parada d'aquest
|
||
// frame és suau (llindar superat) o en sec. HAS_INPUT bloca la inèrcia
|
||
// en girs de 90° (un eix a 0 amb l'altre actiu no ha de "patinar").
|
||
const bool HAS_INPUT = (input_vx_ != 0.0F) || (input_vy_ != 0.0F);
|
||
integrateAxis(intended_vx_, input_vx_, delta_time, motion_time_ms_, HAS_INPUT);
|
||
integrateAxis(intended_vy_, input_vy_, delta_time, motion_time_ms_, HAS_INPUT);
|
||
|
||
// Actualitzem l'acumulador DESPRÉS de la integració. Creix mentre hi
|
||
// haja intenció (input) o moviment residual (coast); només es buida
|
||
// quan el jugador està genuïnament aturat (ni input ni velocitat). Així
|
||
// un canvi de direcció enmig del coast o un toc curt contra el coast
|
||
// no rearma mai el llindar.
|
||
const bool HAS_VELOCITY = (intended_vx_ != 0.0F) || (intended_vy_ != 0.0F);
|
||
if (HAS_INPUT || HAS_VELOCITY) {
|
||
motion_time_ms_ += delta_time * 1000.0F;
|
||
} else {
|
||
motion_time_ms_ = 0.0F;
|
||
}
|
||
|
||
setVelX(intended_vx_);
|
||
setVelY(intended_vy_);
|
||
|
||
// Important: el facing i la detecció de gir de 180° s'alimenten amb
|
||
// input CRU, no amb la velocitat física. Un coast sense input NO
|
||
// canvia el facing (la darrera orientació es preserva), i els girs
|
||
// només disparen amb intenció explícita del jugador.
|
||
updateFacingAndTurn(delta_time, input_vx_, input_vy_);
|
||
}
|
||
|
||
void Player::applyMovement(float delta_time) {
|
||
// El resolver del GameScene pot haver escalat `intended_vx_/vy_`
|
||
// (cas push: velocitat * push_factor) o haver-les anul·lat (cas
|
||
// head-on). Consumim-les aquí com a velocitat definitiva del frame.
|
||
const float VX = intended_vx_;
|
||
const float VY = intended_vy_;
|
||
|
||
// Snap al passadís: si només hi ha moviment horitzontal (o vertical) i
|
||
// la caixa està prou prop d'una lane perpendicular (≤ mitja caixa), la
|
||
// Y (o X) es corregeix gradualment per enfilar el passadís.
|
||
const bool ONLY_H = (VX != 0.0F) && (VY == 0.0F);
|
||
const bool ONLY_V = (VY != 0.0F) && (VX == 0.0F);
|
||
|
||
const float FRAME_START_X = getPosX();
|
||
const float FRAME_START_Y = getPosY();
|
||
|
||
if (ONLY_H) {
|
||
snapToHorizontalLane(delta_time);
|
||
} else if (ONLY_V) {
|
||
snapToVerticalLane(delta_time);
|
||
}
|
||
|
||
// Animació del frame + moviment col·lisió-aware. `tryMove` aplica els
|
||
// deltes per eix i reverteix l'eix que col·lisiona; així el jugador
|
||
// llisca contra sarcòfags, murs i altres jugadors en compte de quedar
|
||
// bloquejat en sec. La callback tracta els altres jugadors com a
|
||
// sòlids per evitar overlaps (el resolver del GameScene deixa les
|
||
// velocitats "segures", i aquesta comprovació és la xarxa de
|
||
// seguretat davant de casos d'ordre d'aplicació).
|
||
animate(delta_time);
|
||
tryMove(VX * delta_time, VY * delta_time, [this](const SDL_FRect& box) {
|
||
return collidesWithWorld(box);
|
||
});
|
||
|
||
clampToPlayfield();
|
||
|
||
const bool MOVED = (getPosX() != FRAME_START_X) || (getPosY() != FRAME_START_Y);
|
||
// `being_pushed_` força `stand_*`: el jugador es desplaça sense input,
|
||
// animar-lo com a caminant faria creure que està caminant quan de
|
||
// fet és arrossegat per l'altre.
|
||
const bool ANIM_AS_MOVING = MOVED && (turning_remaining_ms_ <= 0.0F) && !being_pushed_;
|
||
setCurrentAnimation(animationName(ANIM_AS_MOVING));
|
||
|
||
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
|
||
// col·lisions perquè la posició aquí ja és la definitiva del frame;
|
||
// així el PaintLayer no rep falsos positius mentre el Player rellisca
|
||
// contra una paret. Si el tile ha canviat, publiquem `on_tile_entered`
|
||
// al bus amb l'índex de pintat d'aquest jugador; el PaintLayer filtra
|
||
// pintabilitat i re-publica `tile_painted` als sarcòfags.
|
||
if (bus_ != nullptr) {
|
||
const auto [COL, ROW] = getCurrentTile();
|
||
if (COL != last_tile_col_ || ROW != last_tile_row_) {
|
||
last_tile_col_ = COL;
|
||
last_tile_row_ = ROW;
|
||
bus_->publish(Event{
|
||
.name = "on_tile_entered",
|
||
.payload = PaintedTile{.col = COL, .row = ROW, .tile_index = paint_tile_index_},
|
||
.player_id = id_,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
auto Player::animationName(bool moving) const -> const char* {
|
||
switch (facing_) {
|
||
case Facing::UP:
|
||
return moving ? "walk_up" : "stand_up";
|
||
case Facing::DOWN:
|
||
return moving ? "walk_down" : "stand_down";
|
||
case Facing::SIDE:
|
||
break;
|
||
}
|
||
return moving ? "walk" : "stand";
|
||
}
|
||
|
||
auto Player::getCurrentTile() const -> std::pair<int, int> {
|
||
// El grid del tilemap és de `tile_size` píxels i està 16-alineat
|
||
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
|
||
// centre de la caixa de col·lisió ens dóna el tile on "està" el Player.
|
||
const auto BOX = baseBox();
|
||
const float CX = BOX.x + (BOX.w * 0.5F);
|
||
const float CY = BOX.y + (BOX.h * 0.5F);
|
||
const int COL = static_cast<int>(std::floor(
|
||
(CX - static_cast<float>(map_.tilemap_origin_x)) / static_cast<float>(map_.tile_size)));
|
||
const int ROW = static_cast<int>(std::floor(
|
||
(CY - static_cast<float>(map_.tilemap_origin_y)) / static_cast<float>(map_.tile_size)));
|
||
return {COL, ROW};
|
||
}
|
||
|
||
void Player::clampToPlayfield() {
|
||
// Límits de la zona jugable: la caixa de col·lisió no pot eixir del
|
||
// playfield (muros). El cap del sprite sí pot sobresortir per damunt.
|
||
const auto MIN_X = static_cast<float>(map_.playfield_x - collision_x_offset);
|
||
const auto MAX_X = static_cast<float>(
|
||
map_.playfield_x + map_.playfield_w - collision_x_offset - collision_size);
|
||
const auto MIN_Y = static_cast<float>(map_.playfield_y - collision_y_offset);
|
||
const auto MAX_Y = static_cast<float>(
|
||
map_.playfield_y + map_.playfield_h - collision_y_offset - collision_size);
|
||
setPosX(std::clamp(getPosX(), MIN_X, MAX_X));
|
||
setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y));
|
||
}
|
||
|
||
auto Player::getBaseBox() const -> SDL_FRect {
|
||
return {
|
||
.x = getPosX() + static_cast<float>(collision_x_offset),
|
||
.y = getPosY() + static_cast<float>(collision_y_offset),
|
||
.w = static_cast<float>(collision_size),
|
||
.h = static_cast<float>(collision_size),
|
||
};
|
||
}
|
||
|
||
auto Player::getHitBox() const -> SDL_FRect {
|
||
return {
|
||
.x = getPosX() + static_cast<float>(hit_x_offset),
|
||
.y = getPosY() + static_cast<float>(hit_y_offset),
|
||
.w = static_cast<float>(hit_size),
|
||
.h = static_cast<float>(hit_size),
|
||
};
|
||
}
|
||
|
||
auto Player::hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect {
|
||
// Desplaçament intern respecte al cantó de la caixa base: mateixos
|
||
// offsets que `getHitBox()` però partint d'una caixa arbitrària.
|
||
const int INSET = (collision_size - hit_size) / 2;
|
||
return {
|
||
.x = base.x + static_cast<float>(INSET),
|
||
.y = base.y + static_cast<float>(INSET),
|
||
.w = static_cast<float>(hit_size),
|
||
.h = static_cast<float>(hit_size),
|
||
};
|
||
}
|
||
|
||
void Player::snapToLane(
|
||
float delta_time,
|
||
float base,
|
||
float pitch,
|
||
int max_idx,
|
||
float (Player::*get_pos)() const,
|
||
void (Player::*set_pos)(float)) {
|
||
// Lanes en pos_axis: base + i·pitch per a i ∈ [0, max_idx]. Correspon a
|
||
// la caixa de col·lisió ocupant exactament un passadís perpendicular.
|
||
const auto THRESHOLD = static_cast<float>(collision_size) / 2.0F;
|
||
|
||
const auto IDX = std::clamp(
|
||
static_cast<int>(std::round(((this->*get_pos)() - base) / pitch)),
|
||
0,
|
||
max_idx);
|
||
const float LANE = base + (static_cast<float>(IDX) * pitch);
|
||
const float DELTA = LANE - (this->*get_pos)();
|
||
|
||
if (std::abs(DELTA) > 0.0F && std::abs(DELTA) <= THRESHOLD) {
|
||
const float STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time);
|
||
const float SAVED = (this->*get_pos)();
|
||
(this->*set_pos)(SAVED + std::copysign(STEP, DELTA));
|
||
if (map_.collides(baseBox())) { (this->*set_pos)(SAVED); }
|
||
}
|
||
}
|
||
|
||
void Player::snapToHorizontalLane(float delta_time) {
|
||
snapToLane(delta_time,
|
||
static_cast<float>(map_.playfield_y - collision_y_offset),
|
||
static_cast<float>(map_.pitch_y),
|
||
map_.grid_rows,
|
||
&Player::getPosY,
|
||
&Player::setPosY);
|
||
}
|
||
|
||
void Player::snapToVerticalLane(float delta_time) {
|
||
snapToLane(delta_time,
|
||
static_cast<float>(map_.playfield_x - collision_x_offset),
|
||
static_cast<float>(map_.pitch_x),
|
||
map_.grid_cols,
|
||
&Player::getPosX,
|
||
&Player::setPosX);
|
||
}
|