refactor(player): config a membres d'instancia (P1/P2 YAMLs independents)

This commit is contained in:
JailDesigner committed 2026-05-13 21:40:02 +02:00
1 parent 8eba52353b
commit 2aef925286
4 files changed
+516 -591

No files matched your search

+273 -301
View File
@@ -37,19 +37,6 @@ namespace {
// repòs i no contribueix a la velocitat.
constexpr Sint16 STICK_DEADZONE = 12000;
// Llegeix la mida del frame des del bloc `sprite: { path, size: [w, h] }`
// (mateix format que la resta de plantilles d'entitat, vegeu
// AnimatedSprite). Extracció per mantenir `Player::loadFrom` sota el
// llindar de complexitat cognitiva (25).
void readSpriteSize(const fkyaml::node& yaml, const std::string& yaml_path, int& w, int& h) {
if (!yaml.contains("sprite") || !yaml["sprite"].contains("size")) {
throw std::runtime_error("Player: manca `sprite.size` a " + yaml_path);
}
const auto& size_node = yaml["sprite"]["size"];
w = size_node[0].get_value<int>();
h = size_node[1].get_value<int>();
}
// 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
@@ -73,15 +60,156 @@ namespace {
} // namespace
Player::TurnConfig Player::turn_config;
Player::InertiaConfig Player::inertia_config;
Player::JumpConfig Player::jump_config;
Player::HurtConfig Player::hurt_config;
Player::WhipConfig Player::whip_config;
Player::DeathConfig Player::death_config;
Player::ImmunityConfig Player::immunity_config;
namespace {
void Player::loadFrom(const std::string& yaml_path) {
// ---- Helpers de lectura per sub-blocs del YAML del personatge ----
// Cada helper rep el bloc YAML root i un `Player::Config&` a poblar.
// Camps absents respecten els defaults del Config (no reinicialitzen
// a 0 — la Config ja ve construïda amb els valors default abans).
void readSpriteSizeInto(const fkyaml::node& yaml, const std::string& yaml_path, Player::Config& out) {
if (!yaml.contains("sprite") || !yaml["sprite"].contains("size")) {
throw std::runtime_error("Player: manca `sprite.size` a " + yaml_path);
}
const auto& size_node = yaml["sprite"]["size"];
out.sprite_w = size_node[0].get_value<int>();
out.sprite_h = size_node[1].get_value<int>();
}
void readCollisionInto(const fkyaml::node& yaml, const std::string& yaml_path, Player::Config& out) {
if (!yaml.contains("collision")) {
throw std::runtime_error("Player: manca bloc `collision:` a " + yaml_path);
}
const auto& c = yaml["collision"];
out.collision_size = c["size"].get_value<int>();
out.base_size = c["base_size"].get_value<int>();
// Col·lisió centrada en el quadrat inferior del sprite.
out.collision_padding = (out.collision_size - out.base_size) / 2;
out.collision_x_offset = -out.collision_padding;
out.collision_y_offset = (out.sprite_h - out.base_size) - out.collision_padding;
// Hit-box opcional (actor-actor): default = caixa de col·lisió.
out.hit_size = c.contains("hit_size") ? c["hit_size"].get_value<int>() : out.collision_size;
const int HIT_INSET = (out.collision_size - out.hit_size) / 2;
out.hit_x_offset = out.collision_x_offset + HIT_INSET;
out.hit_y_offset = out.collision_y_offset + HIT_INSET;
out.body_height = c.contains("height") ? c["height"].get_value<int>() : 0;
}
void readInertiaInto(const fkyaml::node& yaml, Player::Config& out) {
if (!yaml.contains("inertia")) { return; }
const auto& i = yaml["inertia"];
if (i.contains("brake_time_ms")) { out.inertia.brake_time_ms = i["brake_time_ms"].get_value<int>(); }
if (i.contains("threshold_ms")) { out.inertia.threshold_ms = i["threshold_ms"].get_value<int>(); }
}
void readTurnInto(const fkyaml::node& yaml, Player::Config& out) {
if (!yaml.contains("turn")) { return; }
const auto& t = yaml["turn"];
if (t.contains("duration_ms")) { out.turn.duration_ms = t["duration_ms"].get_value<int>(); }
if (!t.contains("transitions")) { return; }
const auto& tr = t["transitions"];
if (tr.contains("west_to_east")) { out.turn.w_to_e = Player::cardinalFromString(tr["west_to_east"].get_value<std::string>()); }
if (tr.contains("east_to_west")) { out.turn.e_to_w = Player::cardinalFromString(tr["east_to_west"].get_value<std::string>()); }
if (tr.contains("north_to_south")) { out.turn.n_to_s = Player::cardinalFromString(tr["north_to_south"].get_value<std::string>()); }
if (tr.contains("south_to_north")) { out.turn.s_to_n = Player::cardinalFromString(tr["south_to_north"].get_value<std::string>()); }
}
void readJumpInto(const fkyaml::node& yaml, Player::Config& out) {
if (!yaml.contains("jump")) { return; }
const auto& j = yaml["jump"];
if (j.contains("impulse")) { out.jump.impulse_px_per_s = j["impulse"].get_value<float>(); }
if (j.contains("gravity")) { out.jump.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
if (j.contains("landing_stun_ms")) { out.jump.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
if (j.contains("sound")) {
out.jump.sound_key = j["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(out.jump.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + out.jump.sound_key);
}
if (j.contains("land_sound")) {
out.jump.land_sound_key = j["land_sound"].get_value<std::string>();
Resource::Cache::get().loadSound(out.jump.land_sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + out.jump.land_sound_key);
}
}
void readHurtInto(const fkyaml::node& yaml, Player::Config& out) {
if (!yaml.contains("hurt")) { return; }
const auto& h = yaml["hurt"];
if (h.contains("speed")) { out.hurt.initial_speed_px_per_s = h["speed"].get_value<float>(); }
if (h.contains("brake_time_ms")) { out.hurt.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
if (h.contains("sound")) {
out.hurt.sound_key = h["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(out.hurt.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + out.hurt.sound_key);
}
}
void readWhipInto(const fkyaml::node& yaml, Player::Config& out) {
if (!yaml.contains("whip")) { return; }
const auto& w = yaml["whip"];
if (w.contains("range_tiles")) { out.whip.range_tiles = w["range_tiles"].get_value<float>(); }
if (w.contains("swing_ms")) { out.whip.swing_ms = w["swing_ms"].get_value<int>(); }
if (w.contains("cooldown_ms")) { out.whip.cooldown_ms = w["cooldown_ms"].get_value<int>(); }
if (w.contains("damage")) { out.whip.damage = w["damage"].get_value<int>(); }
if (w.contains("sound")) {
out.whip.sound_key = w["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(out.whip.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + out.whip.sound_key);
}
}
void readDeathInto(const fkyaml::node& yaml, Player::Config& out) {
if (!yaml.contains("death")) { return; }
const auto& d = yaml["death"];
if (d.contains("respawn_delay_ms")) { out.death.respawn_delay_ms = d["respawn_delay_ms"].get_value<int>(); }
if (d.contains("lives")) { out.death.initial_lives = d["lives"].get_value<int>(); }
}
void readImmunityInto(const fkyaml::node& yaml, Player::Config& out) {
if (!yaml.contains("immunity")) { return; }
const auto& im = yaml["immunity"];
if (im.contains("duration_ms")) { out.immunity.duration_ms = im["duration_ms"].get_value<float>(); }
if (im.contains("blink_period_ms")) { out.immunity.blink_period_ms = im["blink_period_ms"].get_value<int>(); }
if (im.contains("palette")) {
out.immunity.palette_key = im["palette"].get_value<std::string>();
Resource::Cache::get().loadPalette(out.immunity.palette_key, std::string{Defaults::Paths::PLAYER_DIR} + out.immunity.palette_key + ".pal");
}
}
void readLightInto(const fkyaml::node& yaml, Player::Config& out) {
// Punt de partida del flicker: el global de lighting.yaml. El bloc
// opcional `flicker_params:` del personatge sobreescriu camps concrets.
out.flicker_params = Lighting::flickerConfig();
if (!yaml.contains("light")) { return; }
const auto& l = yaml["light"];
if (l.contains("radius")) { out.light_radius = l["radius"].get_value<int>(); }
if (l.contains("flicker")) { out.light_flicker = l["flicker"].get_value<bool>(); }
if (l.contains("color")) {
const auto& c = l["color"];
if (c.is_sequence() && c.size() == 3) {
out.light_color_r = static_cast<std::uint8_t>(std::clamp(c[0].get_value<int>(), 0, 255));
out.light_color_g = static_cast<std::uint8_t>(std::clamp(c[1].get_value<int>(), 0, 255));
out.light_color_b = static_cast<std::uint8_t>(std::clamp(c[2].get_value<int>(), 0, 255));
}
}
if (l.contains("offset")) {
const auto& o = l["offset"];
if (o.is_sequence() && o.size() == 2) {
out.light_offset_x = o[0].get_value<int>();
out.light_offset_y = o[1].get_value<int>();
}
}
if (l.contains("flicker_params")) {
const auto& fp = l["flicker_params"];
if (fp.contains("wave_amplitude_ratio")) { out.flicker_params.wave_amplitude_ratio = fp["wave_amplitude_ratio"].get_value<float>(); }
if (fp.contains("wave_frequency_hz")) { out.flicker_params.wave_frequency_hz = fp["wave_frequency_hz"].get_value<float>(); }
if (fp.contains("noise_amplitude_ratio")) { out.flicker_params.noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value<float>(); }
if (fp.contains("noise_frequency_hz")) { out.flicker_params.noise_frequency_hz = fp["noise_frequency_hz"].get_value<float>(); }
}
}
} // namespace
auto Player::Config::loadFromYaml(const std::string& yaml_path) -> Player::Config {
auto bytes = Resource::Helper::loadFile(yaml_path);
if (bytes.empty()) {
throw std::runtime_error("Player: no s'ha pogut obrir " + yaml_path);
@@ -90,208 +218,51 @@ void Player::loadFrom(const std::string& yaml_path) {
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
readSpriteSize(yaml, yaml_path, sprite_w, sprite_h);
readCollisionBlock(yaml, yaml_path);
// 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>()); }
}
}
// Bloc opcional `jump:` — si manca, `impulse_px_per_s` queda a 0 i el
// botó de salt no fa res. Extret a un mètode estàtic per acotar la
// complexitat cognitiva de `loadFrom` sota el llindar de clang-tidy.
readJumpBlock(yaml);
// Bloc opcional `hurt:` — estat post-impacte. Si manca o el speed
// és 0, l'estat HURT mai s'activa (cas degenerat).
readHurtBlock(yaml);
// Bloc opcional `whip:` — paràmetres del látigo. Si manca, el
// jugador no atacarà efectivament (ATTACKING dura 0 ms i el flanc
// d'input es perd).
readWhipBlock(yaml);
// Bloc opcional `death:` — pausa de respawn i comptador inicial de
// vides. Si manca, el respawn és immediat i les vides queden a 3
// per defecte (no és un valor crític: avui no hi ha game over).
readDeathBlock(yaml);
// Bloc opcional `immunity:` — finestra de gràcia post-spawn. Si
// `duration_ms` és 0 (o el bloc manca) la mecànica queda
// desactivada i els hazards toquen al primer frame, com abans.
readImmunityBlock(yaml);
// Bloc opcional `light:` — radi i flicker. Extret a un mètode per
// mantenir `loadFrom` sota el llindar de complexitat de clang-tidy.
readLightBlock(yaml);
Player::Config out;
readSpriteSizeInto(yaml, yaml_path, out);
readCollisionInto(yaml, yaml_path, out);
readInertiaInto(yaml, out);
readTurnInto(yaml, out);
readJumpInto(yaml, out);
readHurtInto(yaml, out);
readWhipInto(yaml, out);
readDeathInto(yaml, out);
readImmunityInto(yaml, out);
readLightInto(yaml, out);
return out;
}
void Player::readLightBlock(const fkyaml::node& yaml) {
light_radius = 0;
light_flicker = false;
light_color_r = 255;
light_color_g = 255;
light_color_b = 255;
light_offset_x = 0;
light_offset_y = 0;
// Punt de partida del flicker: el global de lighting.yaml. El bloc
// opcional `flicker_params:` del personatge sobreescriu camps concrets.
flicker_params = Lighting::flickerConfig();
if (!yaml.contains("light")) { return; }
const auto& l = yaml["light"];
if (l.contains("radius")) { light_radius = l["radius"].get_value<int>(); }
if (l.contains("flicker")) { light_flicker = l["flicker"].get_value<bool>(); }
// `color: [r, g, b]` opcional, bytes 0..255. Si manca, blanc (sense
// tint) — comportament idèntic al mode subtractive de hui.
if (l.contains("color")) {
const auto& c = l["color"];
if (c.is_sequence() && c.size() == 3) {
light_color_r = static_cast<std::uint8_t>(std::clamp(c[0].get_value<int>(), 0, 255));
light_color_g = static_cast<std::uint8_t>(std::clamp(c[1].get_value<int>(), 0, 255));
light_color_b = static_cast<std::uint8_t>(std::clamp(c[2].get_value<int>(), 0, 255));
}
void Player::Config::applyRoomLightOverride() {
// Override de sala per al jugador (bloc `lighting.emitters.player`
// del YAML del mapa). Si la sala no en declara, els valors base del
// YAML del personatge es conserven. Si la sala desactiva
// (`players: false`), `light_radius = 0` (no emet).
const auto* room = Map::getRoomEmitterOverride("player");
if (room == nullptr) { return; }
if (!room->enabled) {
light_radius = 0;
light_flicker = false;
return;
}
// `offset: [x, y]` opcional, píxels enters. Default (0, 0) → centre
// del sprite. Y creix cap avall, així que `y: -3` puja la llum 3 px.
if (l.contains("offset")) {
const auto& o = l["offset"];
if (o.is_sequence() && o.size() == 2) {
light_offset_x = o[0].get_value<int>();
light_offset_y = o[1].get_value<int>();
}
if (room->radius.has_value()) { light_radius = *room->radius; }
if (room->flicker.has_value()) { light_flicker = *room->flicker; }
if (room->color.has_value()) {
light_color_r = (*room->color)[0];
light_color_g = (*room->color)[1];
light_color_b = (*room->color)[2];
}
// `flicker_params: { ... }` opcional. Qualsevol dels 4 camps absent
// conserva el valor heretat del global (vegeu inicialització a sobre).
if (l.contains("flicker_params")) {
const auto& fp = l["flicker_params"];
if (fp.contains("wave_amplitude_ratio")) { flicker_params.wave_amplitude_ratio = fp["wave_amplitude_ratio"].get_value<float>(); }
if (fp.contains("wave_frequency_hz")) { flicker_params.wave_frequency_hz = fp["wave_frequency_hz"].get_value<float>(); }
if (fp.contains("noise_amplitude_ratio")) { flicker_params.noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value<float>(); }
if (fp.contains("noise_frequency_hz")) { flicker_params.noise_frequency_hz = fp["noise_frequency_hz"].get_value<float>(); }
if (room->offset.has_value()) {
light_offset_x = (*room->offset)[0];
light_offset_y = (*room->offset)[1];
}
if (room->flicker_wave_amplitude_ratio.has_value()) { flicker_params.wave_amplitude_ratio = *room->flicker_wave_amplitude_ratio; }
if (room->flicker_wave_frequency_hz.has_value()) { flicker_params.wave_frequency_hz = *room->flicker_wave_frequency_hz; }
if (room->flicker_noise_amplitude_ratio.has_value()) { flicker_params.noise_amplitude_ratio = *room->flicker_noise_amplitude_ratio; }
if (room->flicker_noise_frequency_hz.has_value()) { flicker_params.noise_frequency_hz = *room->flicker_noise_frequency_hz; }
}
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
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;
// Alçada del cos, llegida del mateix bloc per coherència amb la
// resta d'entitats (mummy, sarcophagus, etc.). 0 = sense filtre per
// altura — el salt no esquiva l'altre jugador.
body_height = c.contains("height") ? c["height"].get_value<int>() : 0;
}
void Player::readJumpBlock(const fkyaml::node& yaml) {
jump_config = JumpConfig{};
if (!yaml.contains("jump")) { return; }
const auto& j = yaml["jump"];
if (j.contains("impulse")) { jump_config.impulse_px_per_s = j["impulse"].get_value<float>(); }
if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
// Sons opcionals: el nom del fitxer (relatiu a Defaults::Paths::AUDIO_SOUNDS_DIR)
// val tant de clau al Resource::Cache com de basename per a la càrrega.
// Si el camp manca, la clau queda buida i `playSound` no es crida.
if (j.contains("sound")) {
jump_config.sound_key = j["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(jump_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + jump_config.sound_key);
}
if (j.contains("land_sound")) {
jump_config.land_sound_key = j["land_sound"].get_value<std::string>();
Resource::Cache::get().loadSound(jump_config.land_sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + jump_config.land_sound_key);
}
}
void Player::readHurtBlock(const fkyaml::node& yaml) {
hurt_config = HurtConfig{};
if (!yaml.contains("hurt")) { return; }
const auto& h = yaml["hurt"];
if (h.contains("speed")) { hurt_config.initial_speed_px_per_s = h["speed"].get_value<float>(); }
if (h.contains("brake_time_ms")) { hurt_config.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
// So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`.
if (h.contains("sound")) {
hurt_config.sound_key = h["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(hurt_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + hurt_config.sound_key);
}
}
void Player::readDeathBlock(const fkyaml::node& yaml) {
death_config = DeathConfig{};
if (!yaml.contains("death")) { return; }
const auto& d = yaml["death"];
if (d.contains("respawn_delay_ms")) { death_config.respawn_delay_ms = d["respawn_delay_ms"].get_value<int>(); }
if (d.contains("lives")) { death_config.initial_lives = d["lives"].get_value<int>(); }
}
void Player::readImmunityBlock(const fkyaml::node& yaml) {
immunity_config = ImmunityConfig{};
if (!yaml.contains("immunity")) { return; }
const auto& im = yaml["immunity"];
if (im.contains("duration_ms")) { immunity_config.duration_ms = im["duration_ms"].get_value<float>(); }
if (im.contains("blink_period_ms")) { immunity_config.blink_period_ms = im["blink_period_ms"].get_value<int>(); }
// Paleta del blink: el basename val tant de clau al Resource::Cache
// com de path (`Defaults::Paths::PLAYER_DIR + key + .pal`). Mateix patró
// que els sons.
if (im.contains("palette")) {
immunity_config.palette_key = im["palette"].get_value<std::string>();
Resource::Cache::get().loadPalette(immunity_config.palette_key, std::string{Defaults::Paths::PLAYER_DIR} + immunity_config.palette_key + ".pal");
}
}
void Player::readWhipBlock(const fkyaml::node& yaml) {
whip_config = WhipConfig{};
if (!yaml.contains("whip")) { return; }
const auto& w = yaml["whip"];
if (w.contains("range_tiles")) { whip_config.range_tiles = w["range_tiles"].get_value<float>(); }
if (w.contains("swing_ms")) { whip_config.swing_ms = w["swing_ms"].get_value<int>(); }
if (w.contains("cooldown_ms")) { whip_config.cooldown_ms = w["cooldown_ms"].get_value<int>(); }
if (w.contains("damage")) { whip_config.damage = w["damage"].get_value<int>(); }
// So del swing: mateix patró que `jump.sound` — basename val de
// clau al cache i de path. Si manca, queda buit i no es dispara.
if (w.contains("sound")) {
whip_config.sound_key = w["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(whip_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + whip_config.sound_key);
}
void Player::setConfig(Config cfg) {
cfg_ = std::move(cfg);
}
void Player::applyCardinal(Cardinal c) {
@@ -350,16 +321,16 @@ auto Player::opposite(Cardinal c) -> Cardinal {
return Cardinal::NONE;
}
auto Player::intermediateFor(Cardinal target) -> Cardinal {
auto Player::intermediateFor(Cardinal target) const -> Cardinal {
switch (target) {
case Cardinal::EAST:
return turn_config.w_to_e;
return cfg_.turn.w_to_e;
case Cardinal::WEST:
return turn_config.e_to_w;
return cfg_.turn.e_to_w;
case Cardinal::SOUTH:
return turn_config.n_to_s;
return cfg_.turn.n_to_s;
case Cardinal::NORTH:
return turn_config.s_to_n;
return cfg_.turn.s_to_n;
case Cardinal::NONE:
return Cardinal::NONE;
}
@@ -397,11 +368,11 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) {
// 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_);
(cfg_.turn.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);
turning_remaining_ms_ = static_cast<float>(cfg_.turn.duration_ms);
applyCardinal(intermediate_cardinal_);
} else if (vy < 0.0F) {
// Tria de facing amb prevalença vertical: si hi ha component
@@ -425,8 +396,9 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) {
}
}
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)
Player::Player(Config cfg, 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)),
cfg_(std::move(cfg)),
id_(id),
controls_(controls),
map_(map_info),
@@ -440,7 +412,7 @@ Player::Player(const std::string& anim_key, int id, float x, float y, const Cont
setCurrentAnimation("stand_south");
// Comptador d'intents inicialitzat amb el valor del YAML (default 3
// si el bloc `death:` manca). Fora del cicle DYING no toca a ningú.
lives_remaining_ = death_config.initial_lives;
lives_remaining_ = cfg_.death.initial_lives;
// Spawn point inicial: la posició del ctor val per defecte; el
// GameScene el reescriu via `setSpawnPoint` quan canvia de sala.
spawn_x_ = x;
@@ -483,7 +455,7 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
// travessa net del caminant; ningú actua de "muro" durant el
// vol, ni a la pujada ni a la baixada.
// - Tots dos en l'aire → col·lisió només si els rangs verticals
// [jump_z, jump_z + body_height] solapen. Així permetem el xoc
// [jump_z, jump_z + cfg_.body_height] solapen. Així permetem el xoc
// mid-aire quan tots dos van un contra l'altre i estan a la
// mateixa franja d'alçada.
const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box);
@@ -502,7 +474,7 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
// per altura mira que els rangs verticals encara solapin; si no,
// estan a prou distància en Y "real" i no es toquen.
if (MY_AIR && body_height > 0 && std::fabs(MY_Z - p->jump_z_) >= static_cast<float>(body_height)) {
if (MY_AIR && cfg_.body_height > 0 && std::fabs(MY_Z - p->jump_z_) >= static_cast<float>(cfg_.body_height)) {
return false;
}
const auto OTHER = p->getHitBox();
@@ -510,7 +482,7 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
});
}
void Player::integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) {
void Player::integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) const {
// 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).
@@ -528,12 +500,12 @@ void Player::integrateAxis(float& phys, float input, float delta_time, float mot
phys = 0.0F;
return;
}
const bool INERTIA_ACTIVE = (inertia_config.brake_time_ms > 0) && (motion_time_ms >= static_cast<float>(inertia_config.threshold_ms));
const bool INERTIA_ACTIVE = (cfg_.inertia.brake_time_ms > 0) && (motion_time_ms >= static_cast<float>(cfg_.inertia.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 DECEL = SPEED_PX_PER_S / (static_cast<float>(cfg_.inertia.brake_time_ms) / 1000.0F);
const float STEP = DECEL * delta_time;
if (std::fabs(phys) <= STEP) {
phys = 0.0F;
@@ -656,8 +628,8 @@ void Player::tick(float delta_time) {
if (state_ == State::HURT) {
(void)consumeJumpEdge();
(void)consumeAttackEdge();
const float DECEL = (hurt_config.brake_time_ms > 0)
? hurt_config.initial_speed_px_per_s / (static_cast<float>(hurt_config.brake_time_ms) / 1000.0F)
const float DECEL = (cfg_.hurt.brake_time_ms > 0)
? cfg_.hurt.initial_speed_px_per_s / (static_cast<float>(cfg_.hurt.brake_time_ms) / 1000.0F)
: 0.0F;
const float STEP = DECEL * delta_time;
auto decay = [STEP](float& v) {
@@ -742,7 +714,7 @@ void Player::tick(float delta_time) {
// doble per excés que perdre l'atac per un toc accidental al salt).
// En entrar a ATTACKING, descartem el flanc del salt per no que
// queda penjat per a quan es desbloque.
if (consumeAttackEdge() && whip_config.swing_ms > 0) {
if (consumeAttackEdge() && cfg_.whip.swing_ms > 0) {
enterAttacking(currentFacingCardinal());
(void)consumeJumpEdge();
return;
@@ -755,7 +727,7 @@ void Player::tick(float delta_time) {
// ho gestionem deixant que el resolver vinga, i si en el moment del
// despegue (consumeJumpEdge → startJump) ja estem AIRBORNE, l'altre
// braç del switch no canvia res.
if (consumeJumpEdge() && jump_config.impulse_px_per_s > 0.0F) {
if (consumeJumpEdge() && cfg_.jump.impulse_px_per_s > 0.0F) {
startJump();
}
}
@@ -831,7 +803,7 @@ auto Player::currentFacingCardinal() const -> Cardinal {
void Player::enterAttacking(Cardinal facing) {
state_ = State::ATTACKING;
attacking_remaining_ms_ = static_cast<float>(whip_config.swing_ms + whip_config.cooldown_ms);
attacking_remaining_ms_ = static_cast<float>(cfg_.whip.swing_ms + cfg_.whip.cooldown_ms);
// Garantim que facing/flip i cardinal queden coherents amb la
// direcció triada (per si l'atac s'ha disparat amb facing residual,
@@ -843,8 +815,8 @@ void Player::enterAttacking(Cardinal facing) {
// So del swing — disparat ací (no des del WhipAttack) perquè és el
// jugador qui executa l'acció; si demà el spawn falla per qualsevol
// motiu, el so del gest segueix sonant.
if (!whip_config.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(whip_config.sound_key, Audio::Group::GAME);
if (!cfg_.whip.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(cfg_.whip.sound_key, Audio::Group::GAME);
}
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
@@ -870,7 +842,7 @@ void Player::enterAttacking(Cardinal facing) {
// en deriva tant la draw_rect (sprite del látigo, més gran) com la
// damage_rect (al nivell dels peus, retallada per bloquejadors).
if (bus_ != nullptr) {
const float RANGE_PX = whip_config.range_tiles * static_cast<float>(Map::tile_size);
const float RANGE_PX = cfg_.whip.range_tiles * static_cast<float>(Map::tile_size);
bus_->publish("whip_spawn", WhipSpawnPayload{
.owner = this,
.direction = dir_int,
@@ -904,16 +876,16 @@ void Player::startJump() {
state_ = State::AIRBORNE;
jump_z_ = 0.0F;
jump_vz_ = jump_config.impulse_px_per_s;
if (!jump_config.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(jump_config.sound_key, Audio::Group::GAME);
jump_vz_ = cfg_.jump.impulse_px_per_s;
if (!cfg_.jump.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(cfg_.jump.sound_key, Audio::Group::GAME);
}
// Avisem al bus perquè el GameScene puga spawnejar el polvet de
// despegue al peu del jugador.
if (bus_ != nullptr) {
bus_->publish(Event{
.name = "player_jumped",
.payload = WorldPos{.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F), .y = getGroundFeetY()},
.payload = WorldPos{.x = getPosX() + (static_cast<float>(cfg_.sprite_w) * 0.5F), .y = getGroundFeetY()},
.player_id = id_,
});
}
@@ -957,7 +929,7 @@ auto Player::chooseHurtDirection(Cardinal preferred) const -> Cardinal {
// abans d'aquesta distància, l'usem; d'altra manera, busquem la
// millor.
const auto REQUIRED = static_cast<int>(
0.5F * hurt_config.initial_speed_px_per_s * (static_cast<float>(hurt_config.brake_time_ms) / 1000.0F));
0.5F * cfg_.hurt.initial_speed_px_per_s * (static_cast<float>(cfg_.hurt.brake_time_ms) / 1000.0F));
if (preferred != Cardinal::NONE) {
const auto [DX, DY] = deltasFor(preferred);
@@ -986,14 +958,14 @@ 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; }
if (cfg_.hurt.initial_speed_px_per_s <= 0.0F || cfg_.hurt.brake_time_ms <= 0) { return; }
const Cardinal CHOSEN = chooseHurtDirection(preferred);
// Activem l'estat i fixem la velocitat segons la direcció triada.
state_ = State::HURT;
if (!hurt_config.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(hurt_config.sound_key, Audio::Group::GAME);
if (!cfg_.hurt.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(cfg_.hurt.sound_key, Audio::Group::GAME);
}
applyCardinal(CHOSEN);
last_input_cardinal_ = CHOSEN;
@@ -1001,7 +973,7 @@ void Player::enterHurt(Cardinal preferred) {
pending_cardinal_ = Cardinal::NONE;
intermediate_cardinal_ = Cardinal::NONE;
being_pushed_ = false;
const float V = hurt_config.initial_speed_px_per_s;
const float V = cfg_.hurt.initial_speed_px_per_s;
const auto [DX, DY] = deltasFor(CHOSEN);
intended_vx_ = DX * V;
intended_vy_ = DY * V;
@@ -1012,7 +984,7 @@ void Player::enterHurt(Cardinal preferred) {
auto Player::integrateJump(float delta_time) -> bool {
if (state_ != State::AIRBORNE) { return false; }
jump_vz_ -= jump_config.gravity_px_per_s2 * delta_time;
jump_vz_ -= cfg_.jump.gravity_px_per_s2 * delta_time;
jump_z_ += jump_vz_ * delta_time;
// Caiguda per agujero: el sòl ja no és present sota el jugador, així
// que NO clampem `jump_z_` a 0. Deixem baixar a negatius (visualment
@@ -1051,22 +1023,22 @@ auto Player::integrateJump(float delta_time) -> bool {
jump_z_ = 0.0F;
jump_vz_ = 0.0F;
state_ = State::LANDING_STUN;
landing_stun_remaining_ms_ = static_cast<float>(jump_config.landing_stun_ms);
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
landing_stun_remaining_ms_ = static_cast<float>(cfg_.jump.landing_stun_ms);
if (!cfg_.jump.land_sound_key.empty() && Audio::get() != nullptr) {
// Consumeix el preset armat per GameScene al spawn aeri
// d'INTRO (single-shot): el primer LAND el sona reverberat,
// els salts següents cauen al camí sec.
if (next_land_reverb_preset_.empty()) {
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
Audio::get()->playSound(cfg_.jump.land_sound_key, Audio::Group::GAME);
} else {
Audio::get()->playSoundWithReverb(jump_config.land_sound_key, next_land_reverb_preset_, Audio::Group::GAME);
Audio::get()->playSoundWithReverb(cfg_.jump.land_sound_key, next_land_reverb_preset_, Audio::Group::GAME);
next_land_reverb_preset_.clear();
}
}
if (bus_ != nullptr) {
bus_->publish(Event{
.name = "player_landed",
.payload = WorldPos{.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F), .y = getGroundFeetY()},
.payload = WorldPos{.x = getPosX() + (static_cast<float>(cfg_.sprite_w) * 0.5F), .y = getGroundFeetY()},
.player_id = id_,
});
}
@@ -1295,8 +1267,8 @@ void Player::enterDying(DeathCause cause) {
// So del cop letal: reaprofitem `hurt.wav` (ja precarregat per
// `readHurtBlock`). El bloc `death:` no té camp de so propi —
// semànticament l'impacte mortal és la versió forta de `hurt`.
if (!hurt_config.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(hurt_config.sound_key, Audio::Group::GAME);
if (!cfg_.hurt.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(cfg_.hurt.sound_key, Audio::Group::GAME);
}
}
@@ -1309,7 +1281,7 @@ void Player::applyDyingMovement(float delta_time) {
animate(delta_time);
if (!death_animation_complete_ && animationIsCompleted()) {
death_animation_complete_ = true;
respawn_remaining_ms_ = static_cast<float>(death_config.respawn_delay_ms);
respawn_remaining_ms_ = static_cast<float>(cfg_.death.respawn_delay_ms);
}
if (death_animation_complete_) {
respawn_remaining_ms_ -= delta_time * 1000.0F;
@@ -1320,7 +1292,7 @@ void Player::applyDyingMovement(float delta_time) {
}
void Player::resetForJoin() {
lives_remaining_ = death_config.initial_lives;
lives_remaining_ = cfg_.death.initial_lives;
score_ = 0;
treasures_ = 0;
// Publica l'estat inicial perquè el HUD reompla el bloc en el moment
@@ -1489,12 +1461,12 @@ void Player::render() {
// swap independentment.
const bool BLINK_ON =
immunity_remaining_ms_ > 0.0F &&
immunity_config.blink_period_ms > 0 &&
!immunity_config.palette_key.empty() &&
cfg_.immunity.blink_period_ms > 0 &&
!cfg_.immunity.palette_key.empty() &&
surface_ != nullptr &&
(static_cast<int>(immunity_remaining_ms_) / immunity_config.blink_period_ms) % 2 == 0;
(static_cast<int>(immunity_remaining_ms_) / cfg_.immunity.blink_period_ms) % 2 == 0;
if (BLINK_ON) {
surface_->setPalette(Resource::Cache::get().getPalette(immunity_config.palette_key));
surface_->setPalette(Resource::Cache::get().getPalette(cfg_.immunity.palette_key));
}
if (jump_z_ == 0.0F) {
@@ -1517,8 +1489,8 @@ auto Player::getLightCenter() const -> SDL_FPoint {
// que la llum "salti" amb el jugador en lloc de quedar-se al sòl.
// Sumem també l'offset estàtic configurat al YAML del personatge.
return {
.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F) + static_cast<float>(light_offset_x),
.y = getPosY() - jump_z_ + (static_cast<float>(sprite_h) * 0.5F) + static_cast<float>(light_offset_y),
.x = getPosX() + (static_cast<float>(cfg_.sprite_w) * 0.5F) + static_cast<float>(cfg_.light_offset_x),
.y = getPosY() - jump_z_ + (static_cast<float>(cfg_.sprite_h) * 0.5F) + static_cast<float>(cfg_.light_offset_y),
};
}
@@ -1533,14 +1505,14 @@ void Player::renderShadow() const {
if (falling_through_hole_) { return; }
// `height_factor`: 0 al terra → fotograma més gran, 1 al pic → més
// petit. Si el `jump_config` té un divisor zero (cas degenerat) caem
// petit. Si el `cfg_.jump` té un divisor zero (cas degenerat) caem
// a 0 per no propagar nan al renderer.
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)
const auto PEAK = (cfg_.jump.gravity_px_per_s2 > 0.0F)
? (cfg_.jump.impulse_px_per_s * cfg_.jump.impulse_px_per_s) / (2.0F * cfg_.jump.gravity_px_per_s2)
: 0.0F;
const float HEIGHT_FACTOR = (PEAK > 0.0F) ? std::clamp(jump_z_ / PEAK, 0.0F, 1.0F) : 0.0F;
const auto CENTER_X = static_cast<int>(getPosX() + (static_cast<float>(sprite_w) * 0.5F));
const auto CENTER_X = static_cast<int>(getPosX() + (static_cast<float>(cfg_.sprite_w) * 0.5F));
const auto FEET_Y = static_cast<int>(getGroundFeetY());
Effects::Shadow::render(CENTER_X, FEET_Y, HEIGHT_FACTOR);
}
@@ -1590,7 +1562,7 @@ auto Player::statePrefix(State state, bool moving, bool in_attack_swing) -> cons
}
auto Player::animationName(bool moving) const -> std::string {
const bool IN_ATTACK_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
const bool IN_ATTACK_SWING = (attacking_remaining_ms_ > static_cast<float>(cfg_.whip.cooldown_ms));
return std::string(statePrefix(state_, moving, IN_ATTACK_SWING)) + facingSuffix(facing_);
}
@@ -1611,12 +1583,12 @@ auto Player::getCurrentTile() const -> std::pair<int, int> {
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 MIN_X = static_cast<float>(map_.playfield_x - cfg_.collision_x_offset);
const auto MAX_X = static_cast<float>(
map_.playfield_x + map_.playfield_w - collision_x_offset - collision_size);
auto min_y = static_cast<float>(map_.playfield_y - collision_y_offset);
map_.playfield_x + map_.playfield_w - cfg_.collision_x_offset - cfg_.collision_size);
auto min_y = static_cast<float>(map_.playfield_y - cfg_.collision_y_offset);
const auto MAX_Y = static_cast<float>(
map_.playfield_y + map_.playfield_h - collision_y_offset - collision_size);
map_.playfield_y + map_.playfield_h - cfg_.collision_y_offset - cfg_.collision_size);
// Passadís de sortida: si la base box queda enterament dins del rang
// X del passadís de la porta, deixem que pugi al strip del mur trasser
@@ -1624,12 +1596,12 @@ void Player::clampToPlayfield() {
// playfield. La regla "enterament dins" exigeix alinear-se al passadís
// abans de pujar — la base box (12 px) hi cap amb 4 px de marge.
if (map_.door_corridor_w > 0) {
const float BASE_X = getPosX() + static_cast<float>(collision_x_offset);
const float BASE_RIGHT = BASE_X + static_cast<float>(collision_size);
const float BASE_X = getPosX() + static_cast<float>(cfg_.collision_x_offset);
const float BASE_RIGHT = BASE_X + static_cast<float>(cfg_.collision_size);
const auto CORRIDOR_LEFT = static_cast<float>(map_.door_corridor_x);
const auto CORRIDOR_RIGHT = static_cast<float>(map_.door_corridor_x + map_.door_corridor_w);
if (BASE_X >= CORRIDOR_LEFT && BASE_RIGHT <= CORRIDOR_RIGHT) {
min_y = static_cast<float>(map_.door_corridor_y - collision_y_offset);
min_y = static_cast<float>(map_.door_corridor_y - cfg_.collision_y_offset);
}
}
@@ -1674,7 +1646,7 @@ void Player::respawn(float x, float y) {
// enemics ja estan apareixent i el jugador no ha de tindre gràcia).
// Si el bloc `immunity:` manca al YAML, `duration_ms` és 0 i el timer
// queda a 0 — sense gràcia, comportament idèntic al d'abans.
setImmunity(immunity_config.duration_ms);
setImmunity(cfg_.immunity.duration_ms);
}
void Player::disable() {
@@ -1705,14 +1677,14 @@ void Player::enterHoleFall(float center_x, float center_y, float floor_y) {
// cas i obligaria a hacks tipus "forçar GROUNDED abans de cridar".
// Snap del centre de la hit-box a la coordenada central del forat.
// Resolem `pos = center - (hit_offset + hit_size/2)` per a cada eix:
// Resolem `pos = center - (hit_offset + cfg_.hit_size/2)` per a cada eix:
// així el centre exacte de la caja de contacte queda alineat amb
// (center_x, center_y), independentment de la geometria interna del
// sprite. `setPosX/Y` és l'API canònica per teleportar el jugador
// (la usen `respawn` i `spawnFromAir`).
const float HIT_HALF = static_cast<float>(hit_size) * 0.5F;
setPosX(center_x - static_cast<float>(hit_x_offset) - HIT_HALF);
setPosY(center_y - static_cast<float>(hit_y_offset) - HIT_HALF);
const float HIT_HALF = static_cast<float>(cfg_.hit_size) * 0.5F;
setPosX(center_x - static_cast<float>(cfg_.hit_x_offset) - HIT_HALF);
setPosY(center_y - static_cast<float>(cfg_.hit_y_offset) - HIT_HALF);
// Frenem qualsevol inèrcia prèvia: la caiguda ha de ser estàtica al
// centre del forat.
@@ -1758,7 +1730,7 @@ void Player::spawnFromAir(float x, float y) {
// visualment es pinta a `pos_.y - jump_z_`). Afegim un xicotet marge
// perquè el primer frame ja siga clarament off-screen, no just al ras.
constexpr float OFFSCREEN_MARGIN_PX = 8.0F;
const float SPAWN_HEIGHT = y + static_cast<float>(sprite_h) + OFFSCREEN_MARGIN_PX;
const float SPAWN_HEIGHT = y + static_cast<float>(cfg_.sprite_h) + OFFSCREEN_MARGIN_PX;
state_ = State::AIRBORNE;
jump_z_ = SPAWN_HEIGHT;
@@ -1778,31 +1750,31 @@ void Player::spawnFromAir(float x, float 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),
.x = getPosX() + static_cast<float>(cfg_.collision_x_offset),
.y = getPosY() + static_cast<float>(cfg_.collision_y_offset),
.w = static_cast<float>(cfg_.collision_size),
.h = static_cast<float>(cfg_.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),
.x = getPosX() + static_cast<float>(cfg_.hit_x_offset),
.y = getPosY() + static_cast<float>(cfg_.hit_y_offset),
.w = static_cast<float>(cfg_.hit_size),
.h = static_cast<float>(cfg_.hit_size),
};
}
auto Player::hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect {
auto Player::hitBoxFromBase(const SDL_FRect& base) const -> 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;
const int INSET = (cfg_.collision_size - cfg_.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),
.w = static_cast<float>(cfg_.hit_size),
.h = static_cast<float>(cfg_.hit_size),
};
}
@@ -1811,7 +1783,7 @@ void Player::snapTowardsLane(
float target_lane,
float (Player::*get_pos)() const,
void (Player::*set_pos)(float)) {
const auto THRESHOLD = static_cast<float>(collision_size) / 2.0F;
const auto THRESHOLD = static_cast<float>(cfg_.collision_size) / 2.0F;
const float DELTA = target_lane - (this->*get_pos)();
if (std::abs(DELTA) <= 0.0F || std::abs(DELTA) > THRESHOLD) { return; }
@@ -1911,10 +1883,10 @@ auto Player::wouldHitOuterWall(float dx, float dy) const -> bool {
// corridor — encara no ho està, perquè el snap és justament qui
// ha d'aliniar-la. En lloc d'això comprovem si la base box està
// prou prop del centre del corridor com per a que el snap
// (mateix THRESHOLD = collision_size/2) puga completar-lo.
// (mateix THRESHOLD = cfg_.collision_size/2) puga completar-lo.
if (Map::door_open && dy < 0.0F && map_.door_corridor_w > 0) {
const float CORRIDOR_TARGET = static_cast<float>(map_.door_corridor_x) + (static_cast<float>(map_.door_corridor_w - collision_size) / 2.0F);
const auto THRESHOLD = static_cast<float>(collision_size) / 2.0F;
const float CORRIDOR_TARGET = static_cast<float>(map_.door_corridor_x) + (static_cast<float>(map_.door_corridor_w - cfg_.collision_size) / 2.0F);
const auto THRESHOLD = static_cast<float>(cfg_.collision_size) / 2.0F;
if (std::abs(CORRIDOR_TARGET - probe.x) <= THRESHOLD) { return false; }
}
return true;
@@ -1928,7 +1900,7 @@ void Player::snapToHorizontalLane(float delta_time) {
return;
}
const auto BASE_Y = static_cast<float>(map_.playfield_y - collision_y_offset);
const auto BASE_Y = static_cast<float>(map_.playfield_y - cfg_.collision_y_offset);
const auto PITCH_Y = static_cast<float>(map_.pitch_y);
const int IDX = std::clamp(
static_cast<int>(std::round((getPosY() - BASE_Y) / PITCH_Y)),
@@ -1948,7 +1920,7 @@ void Player::snapToVerticalLane(float delta_time) {
}
// Candidat 1: lane regular del laberint.
const auto BASE_X = static_cast<float>(map_.playfield_x - collision_x_offset);
const auto BASE_X = static_cast<float>(map_.playfield_x - cfg_.collision_x_offset);
const auto PITCH_X = static_cast<float>(map_.pitch_x);
const int IDX = std::clamp(
static_cast<int>(std::round((getPosX() - BASE_X) / PITCH_X)),
@@ -1961,8 +1933,8 @@ void Player::snapToVerticalLane(float delta_time) {
// forat per on no pot passar.
float best_lane = LANE_REG;
if (Map::door_open && map_.door_corridor_w > 0) {
const float CORRIDOR_CENTER = static_cast<float>(map_.door_corridor_x) + (static_cast<float>(map_.door_corridor_w - collision_size) / 2.0F);
const float LANE_DOOR = CORRIDOR_CENTER - static_cast<float>(collision_x_offset);
const float CORRIDOR_CENTER = static_cast<float>(map_.door_corridor_x) + (static_cast<float>(map_.door_corridor_w - cfg_.collision_size) / 2.0F);
const float LANE_DOOR = CORRIDOR_CENTER - static_cast<float>(cfg_.collision_x_offset);
const float DELTA_REG = LANE_REG - getPosX();
const float DELTA_DOOR = LANE_DOOR - getPosX();
if (std::abs(DELTA_DOOR) < std::abs(DELTA_REG)) { best_lane = LANE_DOOR; }