refactor(player): config a membres d'instancia (P1/P2 YAMLs independents)
This commit is contained in:
1 parent
8eba52353b
commit
2aef925286
4 files changed
+516
-591
No files matched your search
+273
-301
@@ -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; }
|
||||
|
||||
Reference in new issue
Block a user