Files
aee-arcade/source/game/entities/player.cpp
T

159 lines
6.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "game/entities/player.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/map/map.hpp"
namespace {
// Velocitat constant. Si més endavant volem sprint, slow zones, etc.,
// es pot convertir en un membre modificable.
constexpr float SPEED_PX_PER_S = 60.0F;
// Color de debug per a la caixa de col·lisió (ARGB opac). S'elimina
// quan el joc estiga afinat.
constexpr std::uint32_t DEBUG_COLLISION_ARGB = 0xFF00FFFFU;
} // namespace
Player::Player(float x, float y, const Controls& controls)
: AnimatedSprite(Resource::Cache::get().getAnimationData("player.yaml")),
controls_(controls) {
setPos(x, y);
setCurrentAnimation("stand");
}
void Player::update(float delta_time) {
const bool* keys = SDL_GetKeyboardState(nullptr);
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; }
setVelX(vx);
setVelY(vy);
// El sprite original mira a la dreta; flipem només si es mou a l'esquerra
// i restaurem si torna a la dreta. Moviments purament verticals conserven
// l'últim flip perquè el jugador no "gire" sense motiu.
if (vx < 0.0F) {
setFlip(SDL_FLIP_HORIZONTAL);
} else if (vx > 0.0F) {
setFlip(SDL_FLIP_NONE);
}
const bool MOVING = vx != 0.0F || vy != 0.0F;
setCurrentAnimation(MOVING ? "walk" : "stand");
// 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. Amb
// moviment diagonal no es fa snap — l'usuari pilota manualment.
const bool ONLY_H = (vx != 0.0F) && (vy == 0.0F);
const bool ONLY_V = (vy != 0.0F) && (vx == 0.0F);
if (ONLY_H) {
snapToHorizontalLane(delta_time);
} else if (ONLY_V) {
snapToVerticalLane(delta_time);
}
// Guardem la posició prèvia i deixem que AnimatedSprite::update aplique
// la velocitat com sempre; així no dupliquem la integració temporal ni
// l'animació. Després, axis-separated collision: revertim i re-apliquem
// cada eix comprovant el mapa de colisions, de manera que el jugador
// llisca contra els sarcòfags en compte de quedar-se bloquejat en sec.
const float OLD_X = getPosX();
const float OLD_Y = getPosY();
AnimatedSprite::update(delta_time);
const float NEW_X = getPosX();
const float NEW_Y = getPosY();
setPos(OLD_X, OLD_Y);
setPosX(NEW_X);
if (Map::collides(baseBox())) { setPosX(OLD_X); }
setPosY(NEW_Y);
if (Map::collides(baseBox())) { setPosY(OLD_Y); }
// 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.
constexpr auto MIN_X = static_cast<float>(Map::PLAYFIELD_X - COLLISION_X_OFFSET);
constexpr auto MAX_X = static_cast<float>(
Map::PLAYFIELD_X + Map::PLAYFIELD_W - COLLISION_X_OFFSET - COLLISION_SIZE);
constexpr auto MIN_Y = static_cast<float>(Map::PLAYFIELD_Y - COLLISION_Y_OFFSET);
constexpr 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));
}
void Player::render() {
// Debug: pinta la caixa de col·lisió (16×16) per sota del sprite perquè
// es vegen els límits del mapa de colisions alineats amb els passadissos.
Screen::get().fillRect(
static_cast<int>(getPosX()) + COLLISION_X_OFFSET,
static_cast<int>(getPosY()) + COLLISION_Y_OFFSET,
COLLISION_SIZE,
COLLISION_SIZE,
DEBUG_COLLISION_ARGB);
AnimatedSprite::render();
}
auto Player::baseBox() 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),
};
}
void Player::snapToHorizontalLane(float delta_time) {
// Lanes en posY: base + i·PITCH_Y per a i ∈ [0, GRID_ROWS]. Correspon a
// la caixa de col·lisió ocupant exactament un passadís horitzontal.
constexpr auto BASE = static_cast<float>(Map::PLAYFIELD_Y - COLLISION_Y_OFFSET);
constexpr auto PITCH = static_cast<float>(Map::PITCH_Y);
constexpr auto THRESHOLD = static_cast<float>(COLLISION_SIZE) / 2.0F;
const auto IDX = std::clamp(
static_cast<int>(std::round((getPosY() - BASE) / PITCH)),
0,
Map::GRID_ROWS);
const float LANE = BASE + (static_cast<float>(IDX) * PITCH);
const float DY = LANE - getPosY();
if (std::abs(DY) > 0.0F && std::abs(DY) <= THRESHOLD) {
const float STEP = std::min(std::abs(DY), SPEED_PX_PER_S * delta_time);
const float SAVED = getPosY();
setPosY(getPosY() + std::copysign(STEP, DY));
if (Map::collides(baseBox())) { setPosY(SAVED); }
}
}
void Player::snapToVerticalLane(float delta_time) {
constexpr auto BASE = static_cast<float>(Map::PLAYFIELD_X - COLLISION_X_OFFSET);
constexpr auto PITCH = static_cast<float>(Map::PITCH_X);
constexpr auto THRESHOLD = static_cast<float>(COLLISION_SIZE) / 2.0F;
const auto IDX = std::clamp(
static_cast<int>(std::round((getPosX() - BASE) / PITCH)),
0,
Map::GRID_COLS);
const float LANE = BASE + (static_cast<float>(IDX) * PITCH);
const float DX = LANE - getPosX();
if (std::abs(DX) > 0.0F && std::abs(DX) <= THRESHOLD) {
const float STEP = std::min(std::abs(DX), SPEED_PX_PER_S * delta_time);
const float SAVED = getPosX();
setPosX(getPosX() + std::copysign(STEP, DX));
if (Map::collides(baseBox())) { setPosX(SAVED); }
}
}