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; }
|
||||
|
||||
+181
-211
@@ -55,6 +55,15 @@ class Player : public AnimatedSprite {
|
||||
HOLE, // caiguda per agujero — animació `death_hole`
|
||||
};
|
||||
|
||||
// Direcció cardinal completa. Pública perquè el `WhipAttack` la passa a
|
||||
// `enterHurt` quan colpeja un altre jugador (friendly fire). Declarada
|
||||
// aquí (a sobre de `Config` perquè `TurnConfig` l'utilitza).
|
||||
enum class Cardinal : std::uint8_t { NONE,
|
||||
NORTH,
|
||||
SOUTH,
|
||||
EAST,
|
||||
WEST };
|
||||
|
||||
enum class State : std::uint8_t {
|
||||
GROUNDED,
|
||||
AIRBORNE,
|
||||
@@ -92,80 +101,172 @@ class Player : public AnimatedSprite {
|
||||
DYING,
|
||||
};
|
||||
|
||||
// Geometria carregada des del YAML del player (p.ex. `data/entities/player/sam.yaml`).
|
||||
// `sprite_*` són
|
||||
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
|
||||
// 12 píxels superiors són el cap i els 12 inferiors la base).
|
||||
// La caixa de col·lisió és un quadrat `collision_size` concèntric amb el
|
||||
// quadrat inferior del sprite (`base_size`). Els offsets els calculem
|
||||
// al load a partir d'aquests tres valors; el mapa de col·lisions i el
|
||||
// de dibuix són independents (el sprite pot sobresortir sense afectar).
|
||||
inline static int sprite_w = 0;
|
||||
inline static int sprite_h = 0;
|
||||
inline static int base_size = 0;
|
||||
inline static int collision_size = 0;
|
||||
inline static int collision_padding = 0;
|
||||
inline static int collision_x_offset = 0;
|
||||
inline static int collision_y_offset = 0;
|
||||
// Configuració per-instància del Player. Conté tot el que abans vivia
|
||||
// com a `inline static` o `static *_config`: així cada jugador (P1=sam,
|
||||
// P2=morcus) pot tenir paràmetres diferents llegits del seu YAML.
|
||||
// GameScene la carrega amb `Config::loadFromYaml` abans d'instanciar
|
||||
// cada Player i la passa al ctor (mou-la). Per refrescar-la entre
|
||||
// sales s'utilitza `Player::setConfig`.
|
||||
//
|
||||
// Els sub-structs (`InertiaConfig`, `TurnConfig`, ...) viuen dins de
|
||||
// `Config` per agrupar-los visualment amb la resta del config — no hi
|
||||
// ha cap raó tècnica per haver-los a fora.
|
||||
struct Config {
|
||||
// ---- Geometria del sprite i col·lisió --------------------------
|
||||
// `sprite_*` són la mida del frame (sprite 12×24 amb perspectiva
|
||||
// falsa). La caixa de col·lisió és un quadrat `collision_size`
|
||||
// concèntric amb el quadrat inferior del sprite (`base_size`).
|
||||
// Els offsets es calculen al load.
|
||||
int sprite_w{0};
|
||||
int sprite_h{0};
|
||||
int base_size{0};
|
||||
int collision_size{0};
|
||||
int collision_padding{0};
|
||||
int collision_x_offset{0};
|
||||
int collision_y_offset{0};
|
||||
|
||||
// Caixa de "contacte" més petita que la caixa de col·lisió: s'usa per
|
||||
// a les interaccions actor-actor (player↔player, player↔momia/trampa),
|
||||
// on volem que el contacte sigui precís i no dispari a "distància"
|
||||
// de la vora del sprite. La caixa de col·lisió gran continua usant-se
|
||||
// per xocar amb murs i sarcòfags (per mantenir el carril del
|
||||
// passadís). Sense `hit_size` al YAML, s'iguala a `collision_size` i
|
||||
// el comportament és idèntic a l'anterior.
|
||||
inline static int hit_size = 0;
|
||||
inline static int hit_x_offset = 0;
|
||||
inline static int hit_y_offset = 0;
|
||||
// Caixa de contacte actor-actor (més petita que la col·lisió).
|
||||
int hit_size{0};
|
||||
int hit_x_offset{0};
|
||||
int hit_y_offset{0};
|
||||
|
||||
// Alçada del cos del jugador en píxels (camp `collision.height` del
|
||||
// YAML, mateixa convenció que entitats com mummy o rolling_rock).
|
||||
// L'usen els filtres de col·lisió per altura: si un altre actor ens
|
||||
// creua mentre `jump_z_` és superior a aquest valor, no col·lisionem.
|
||||
inline static int body_height = 0;
|
||||
// Alçada del cos en píxels (filtre de col·lisió per altura).
|
||||
int body_height{0};
|
||||
|
||||
// Radi de la llum emesa pel jugador (en píxels del canvas), llegit
|
||||
// del bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }`
|
||||
// del YAML. 0 = no emet. Es declara estàtic com la resta de configs
|
||||
// del jugador (immunity, jump, ...): tots els jugadors del mateix YAML
|
||||
// comparteixen el valor. `light_flicker` activa la modulació "tipus
|
||||
// foc" del radi; `light_color_*` són el tint quan la sala usa mode
|
||||
// additive (en subtractive s'ignora); `light_offset_*` desplaça el
|
||||
// centre del cercle respecte el centre del sprite (Y creix avall:
|
||||
// `y: -3` puja la llum 3 px).
|
||||
inline static int light_radius = 0;
|
||||
inline static bool light_flicker = false;
|
||||
inline static std::uint8_t light_color_r = 255;
|
||||
inline static std::uint8_t light_color_g = 255;
|
||||
inline static std::uint8_t light_color_b = 255;
|
||||
inline static int light_offset_x = 0;
|
||||
inline static int light_offset_y = 0;
|
||||
// Paràmetres del flicker resolts per al jugador (combinació del
|
||||
// global + override del YAML del personatge + override de sala).
|
||||
// Només té efecte si `light_flicker` és true.
|
||||
inline static Lighting::FlickerConfig flicker_params{};
|
||||
// ---- Emissió de llum -----------------------------------------
|
||||
// Llegit del bloc opcional `light:` del YAML.
|
||||
// `radius` → 0 = no emet.
|
||||
// `flicker` → activa modulació tipus foc.
|
||||
// `color` → tint en mode additive (ignorat en subtractive).
|
||||
// `offset` → desplaçament del centre respecte el sprite (Y avall).
|
||||
int light_radius{0};
|
||||
bool light_flicker{false};
|
||||
std::uint8_t light_color_r{255};
|
||||
std::uint8_t light_color_g{255};
|
||||
std::uint8_t light_color_b{255};
|
||||
int light_offset_x{0};
|
||||
int light_offset_y{0};
|
||||
// Paràmetres del flicker resolts (global + override del YAML +
|
||||
// override de sala). Només té efecte si `light_flicker` és true.
|
||||
Lighting::FlickerConfig flicker_params{};
|
||||
|
||||
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
||||
static void loadFrom(const std::string& yaml_path);
|
||||
// ---- Sub-structs de gameplay ---------------------------------
|
||||
|
||||
// Vides amb què arrenca un jugador (camp `lives:` del YAML del
|
||||
// jugador). El consumeix el HUD per inicialitzar el comptador del
|
||||
// marcador i `Player::resetForJoin` per a re-entrades mid-partida.
|
||||
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; }
|
||||
// Inèrcia: amb `brake_time_ms == 0` el comportament és instantani.
|
||||
struct InertiaConfig {
|
||||
int brake_time_ms{0};
|
||||
int threshold_ms{0};
|
||||
};
|
||||
|
||||
// Gir de 180°: amb `duration_ms == 0` els canvis de direcció són
|
||||
// instantanis. `Cardinal` ha d'estar visible — declarada a sobre.
|
||||
struct TurnConfig {
|
||||
int duration_ms{0};
|
||||
Cardinal w_to_e{Cardinal::SOUTH};
|
||||
Cardinal e_to_w{Cardinal::NORTH};
|
||||
Cardinal n_to_s{Cardinal::EAST};
|
||||
Cardinal s_to_n{Cardinal::WEST};
|
||||
};
|
||||
|
||||
// Salt: amb `impulse_px_per_s == 0` el botó no té efecte.
|
||||
struct JumpConfig {
|
||||
float impulse_px_per_s{0.0F};
|
||||
float gravity_px_per_s2{0.0F};
|
||||
int landing_stun_ms{0};
|
||||
std::string sound_key;
|
||||
std::string land_sound_key;
|
||||
};
|
||||
|
||||
// HURT (estat post-impacte). Amb `initial_speed_px_per_s == 0`
|
||||
// entrada/sortida instantànies (sense moviment).
|
||||
struct HurtConfig {
|
||||
float initial_speed_px_per_s{0.0F};
|
||||
int brake_time_ms{0};
|
||||
std::string sound_key;
|
||||
};
|
||||
|
||||
// Látigo: hitbox `range_tiles`, swing `swing_ms`, cooldown
|
||||
// `cooldown_ms`. Per ara `damage` només es llegeix.
|
||||
struct WhipConfig {
|
||||
float range_tiles{0.0F};
|
||||
int swing_ms{0};
|
||||
int cooldown_ms{0};
|
||||
int damage{0};
|
||||
std::string sound_key;
|
||||
};
|
||||
|
||||
// Mort/respawn: `respawn_delay_ms` entre l'animació i la
|
||||
// reaparició; `initial_lives` és el comptador d'intents.
|
||||
struct DeathConfig {
|
||||
int respawn_delay_ms{0};
|
||||
int initial_lives{3};
|
||||
};
|
||||
|
||||
// Immunitat post-spawn. `duration_ms == 0` desactiva la mecànica.
|
||||
// `palette_key` (basename del .pal) és la paleta plana del blink.
|
||||
struct ImmunityConfig {
|
||||
float duration_ms{0.0F};
|
||||
int blink_period_ms{0};
|
||||
std::string palette_key;
|
||||
};
|
||||
|
||||
InertiaConfig inertia{};
|
||||
TurnConfig turn{};
|
||||
JumpConfig jump{};
|
||||
HurtConfig hurt{};
|
||||
WhipConfig whip{};
|
||||
DeathConfig death{};
|
||||
ImmunityConfig immunity{};
|
||||
|
||||
// Llig tot el YAML del personatge i retorna un `Config` poblat.
|
||||
// Substitueix l'antic `Player::loadFrom` que mutava estàtics.
|
||||
// Llança si manca el bloc `collision:` (obligatori).
|
||||
[[nodiscard]] static auto loadFromYaml(const std::string& yaml_path) -> Config;
|
||||
|
||||
// Aplica l'override de sala (bloc `lighting.emitters.player` del
|
||||
// YAML del mapa) sobre els camps de llum d'aquesta Config. Es
|
||||
// crida després de `loadFromYaml` i abans de passar la Config al
|
||||
// ctor de Player (o a `setConfig` en transicions de sala).
|
||||
void applyRoomLightOverride();
|
||||
};
|
||||
|
||||
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 = -1);
|
||||
|
||||
// Reemplaça el `Config` actual del Player. Cridat per GameScene al
|
||||
// canvi de sala per re-aplicar valors base + override de la sala nova.
|
||||
// No toca cap estat de joc (lives, posició, FSM): només la config.
|
||||
void setConfig(Config cfg);
|
||||
|
||||
// Accés a la config viva (lectura). El callsites que abans accedien
|
||||
// a estàtics fan servir `getters d'instància` (sota); aquest és per
|
||||
// codi que vol l'objecte sencer.
|
||||
[[nodiscard]] auto getConfig() const -> const Config& { return cfg_; }
|
||||
|
||||
// Vides amb què arrenca un jugador (camp `lives:` del YAML). El
|
||||
// consumeix el HUD per inicialitzar el marcador i `resetForJoin`
|
||||
// per a re-entrades mid-partida.
|
||||
[[nodiscard]] auto getInitialLives() const -> int { return cfg_.death.initial_lives; }
|
||||
|
||||
// Geometria del cos — instance getters dels camps de `cfg_` que
|
||||
// alguns callsites externs (game_scene) consulten directament.
|
||||
[[nodiscard]] auto getBodyHeight() const -> int { return cfg_.body_height; }
|
||||
[[nodiscard]] auto getSpriteW() const -> int { return cfg_.sprite_w; }
|
||||
[[nodiscard]] auto getSpriteH() const -> int { return cfg_.sprite_h; }
|
||||
[[nodiscard]] auto getCollisionSize() const -> int { return cfg_.collision_size; }
|
||||
[[nodiscard]] auto getCollisionYOffset() const -> int { return cfg_.collision_y_offset; }
|
||||
|
||||
// Emissió de llum (mateix paper que `Entity::getLightRadius` per a
|
||||
// les entitats). El centre puja amb el salt: durant `jump_z_>0` el
|
||||
// sprite es dibuixa amb `pos_.y - jump_z_`, i la llum ha de seguir-lo
|
||||
// per a que el cercle "salti" amb el jugador, no es quedi al sòl.
|
||||
[[nodiscard]] static auto getLightRadius() -> int { return light_radius; }
|
||||
[[nodiscard]] static auto getLightFlicker() -> bool { return light_flicker; }
|
||||
[[nodiscard]] static auto getLightColorR() -> std::uint8_t { return light_color_r; }
|
||||
[[nodiscard]] static auto getLightColorG() -> std::uint8_t { return light_color_g; }
|
||||
[[nodiscard]] static auto getLightColorB() -> std::uint8_t { return light_color_b; }
|
||||
[[nodiscard]] static auto getLightOffsetX() -> int { return light_offset_x; }
|
||||
[[nodiscard]] static auto getLightOffsetY() -> int { return light_offset_y; }
|
||||
[[nodiscard]] static auto getFlickerParams() -> const Lighting::FlickerConfig& { return flicker_params; }
|
||||
[[nodiscard]] auto getLightRadius() const -> int { return cfg_.light_radius; }
|
||||
[[nodiscard]] auto getLightFlicker() const -> bool { return cfg_.light_flicker; }
|
||||
[[nodiscard]] auto getLightColorR() const -> std::uint8_t { return cfg_.light_color_r; }
|
||||
[[nodiscard]] auto getLightColorG() const -> std::uint8_t { return cfg_.light_color_g; }
|
||||
[[nodiscard]] auto getLightColorB() const -> std::uint8_t { return cfg_.light_color_b; }
|
||||
[[nodiscard]] auto getLightOffsetX() const -> int { return cfg_.light_offset_x; }
|
||||
[[nodiscard]] auto getLightOffsetY() const -> int { return cfg_.light_offset_y; }
|
||||
[[nodiscard]] auto getFlickerParams() const -> const Lighting::FlickerConfig& { return cfg_.flicker_params; }
|
||||
// Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador
|
||||
// local) amb un offset gran per evitar col·lisions amb el contador
|
||||
// d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases
|
||||
@@ -173,8 +274,6 @@ class Player : public AnimatedSprite {
|
||||
[[nodiscard]] auto getLightSeed() const -> int { return id_ + 1000; }
|
||||
[[nodiscard]] auto getLightCenter() const -> SDL_FPoint;
|
||||
|
||||
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
||||
|
||||
// Update en una sola crida: manté l'API anterior per als tests i
|
||||
// escenaris amb un sol jugador. Internament és equivalent a cridar
|
||||
// `tick(dt)` seguit d'`applyMovement(dt)`.
|
||||
@@ -275,7 +374,7 @@ class Player : public AnimatedSprite {
|
||||
// que coincideix amb aquest mètode i s'usa específicament per
|
||||
// marcar la posició de la sombra a nivell de sòl.
|
||||
[[nodiscard]] auto getFeetY() const -> float {
|
||||
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
|
||||
return getPosY() + static_cast<float>(cfg_.collision_y_offset + cfg_.collision_size);
|
||||
}
|
||||
|
||||
// Alies semàntic: els peus del jugador al sòl. Coincideix amb
|
||||
@@ -484,7 +583,7 @@ class Player : public AnimatedSprite {
|
||||
// arbitrària (p. ex. la posició proposada per `tryMove` abans
|
||||
// d'aplicar-la). Evita recalcular `hit_x_offset`/`hit_y_offset` fora
|
||||
// de Player.
|
||||
[[nodiscard]] static auto hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect;
|
||||
[[nodiscard]] auto hitBoxFromBase(const SDL_FRect& base) const -> SDL_FRect;
|
||||
|
||||
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
|
||||
// caixa al callback de col·lisió. La base del Player és més petita
|
||||
@@ -568,6 +667,11 @@ class Player : public AnimatedSprite {
|
||||
// efecte immediat sense haver de tornar a entrar a la GameScene.
|
||||
void refreshControlsFromOptions();
|
||||
|
||||
// Tot el config del personatge (geometria, llum, jump, hurt, whip,
|
||||
// death, immunity, turn, inertia). Llegit del YAML per instància
|
||||
// (P1 i P2 poden tenir valors diferents) i mogut al ctor.
|
||||
Config cfg_;
|
||||
|
||||
int id_{-1};
|
||||
Controls controls_;
|
||||
|
||||
@@ -639,26 +743,6 @@ class Player : public AnimatedSprite {
|
||||
static auto facingSuffix(Facing facing) -> const char*;
|
||||
static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*;
|
||||
|
||||
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
|
||||
// Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
|
||||
// un altre jugador (friendly fire).
|
||||
public:
|
||||
enum class Cardinal : std::uint8_t { NONE,
|
||||
NORTH,
|
||||
SOUTH,
|
||||
EAST,
|
||||
WEST };
|
||||
|
||||
private:
|
||||
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
||||
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
||||
// el comportament és instantani: soltar input para el jugador en sec.
|
||||
struct InertiaConfig {
|
||||
int brake_time_ms{0}; // Temps en ms perquè la velocitat caiga de ±SPEED a 0.
|
||||
int threshold_ms{0}; // Temps mínim en moviment continu per activar la inèrcia.
|
||||
};
|
||||
static InertiaConfig inertia_config;
|
||||
|
||||
// Integra un eix. Si hi ha input en aquest eix, la velocitat física
|
||||
// salta a aquest valor (acceleració instantània). Si no, i hi ha
|
||||
// input en l'altre eix (`any_input == true`), la velocitat d'aquest
|
||||
@@ -666,127 +750,7 @@ class Player : public AnimatedSprite {
|
||||
// soltant l'input — no volem que arrossegue en diagonal. Només quan
|
||||
// l'usuari deixa anar tot l'input (`any_input == false`) s'activa la
|
||||
// inèrcia de frenada (si `motion_time_ms` ha passat el llindar).
|
||||
static void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input);
|
||||
|
||||
// Configuració del gir de 180°, llegida del bloc `turn:` del YAML del
|
||||
// player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica
|
||||
// queda desactivada i els canvis de direcció són instantanis.
|
||||
struct TurnConfig {
|
||||
int duration_ms{0};
|
||||
Cardinal w_to_e{Cardinal::SOUTH};
|
||||
Cardinal e_to_w{Cardinal::NORTH};
|
||||
Cardinal n_to_s{Cardinal::EAST};
|
||||
Cardinal s_to_n{Cardinal::WEST};
|
||||
};
|
||||
// Definida out-of-class al .cpp: dins de la classe, els initializers
|
||||
// per defecte de TurnConfig encara no són visibles per al compilador.
|
||||
static TurnConfig turn_config;
|
||||
|
||||
// Configuració del salt llegida del bloc `jump:` del YAML del player.
|
||||
// Si `impulse_px_per_s == 0` (o el bloc no existeix) la mecànica del
|
||||
// salt queda desactivada — el botó no té efecte.
|
||||
struct JumpConfig {
|
||||
float impulse_px_per_s{0.0F}; // Velocitat vertical inicial.
|
||||
float gravity_px_per_s2{0.0F}; // Acceleració descendent constant.
|
||||
int landing_stun_ms{0}; // Stun en aterrar (no es mou ni salta).
|
||||
std::string sound_key; // Clau al Resource::Cache del so de despegue (buit = silenci).
|
||||
std::string land_sound_key; // Clau del so d'aterratge (buit = silenci).
|
||||
};
|
||||
static JumpConfig jump_config;
|
||||
|
||||
// Llig el bloc `collision:` del YAML i n'omple les caixes (size/base/
|
||||
// hit) + `body_height`. Llança si el bloc no existeix. Membre estàtic
|
||||
// per accedir als estàtics privats sense exposar-los.
|
||||
static void readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path);
|
||||
|
||||
// Llig el bloc `jump:` del YAML i n'omple `jump_config`. Si el bloc
|
||||
// manca, deixa la mecànica del salt desactivada.
|
||||
static void readJumpBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració de l'estat HURT (estat post-impacte). Si
|
||||
// `initial_speed_px_per_s == 0` el jugador entra i surt
|
||||
// instantàniament (cap moviment).
|
||||
struct HurtConfig {
|
||||
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
||||
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
||||
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/audio/sounds/`).
|
||||
// Mateixa convenció que la resta de sons del jugador: el basename val
|
||||
// de clau al Resource::Cache i de path. Buit o absent = entrada
|
||||
// silenciosa.
|
||||
std::string sound_key;
|
||||
};
|
||||
static HurtConfig hurt_config;
|
||||
|
||||
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
|
||||
static void readHurtBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració del látigo del jugador llegida del bloc `whip:` del
|
||||
// YAML. `range_tiles` defineix la longitud de la hitbox abans de
|
||||
// retallar; `swing_ms` és la durada de l'animació d'atac (des
|
||||
// d'`enterAttacking` fins a desbloquejar el cooldown); `cooldown_ms`
|
||||
// afegeix una fase posterior durant la qual el jugador segueix
|
||||
// bloquejat però la hitbox ja no fa dany. `damage` queda llegit per
|
||||
// a futur; per ara els objectius reben sempre `enterHurt`.
|
||||
struct WhipConfig {
|
||||
float range_tiles{0.0F};
|
||||
int swing_ms{0};
|
||||
int cooldown_ms{0};
|
||||
int damage{0};
|
||||
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
|
||||
// `data/audio/sounds/`). Convenció: el basename val tant de clau al
|
||||
// Resource::Cache com de path. Buit o absent = swing silenciós.
|
||||
std::string sound_key;
|
||||
};
|
||||
static WhipConfig whip_config;
|
||||
|
||||
// Llig el bloc `whip:` del YAML i n'omple `whip_config`. Si manca el
|
||||
// bloc, els camps queden a 0 i el jugador no pot atacar (el flanc
|
||||
// d'input es perd silenciosament).
|
||||
static void readWhipBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració de mort/respawn llegida del bloc `death:` del YAML.
|
||||
// `respawn_delay_ms` és la pausa entre la fi de `death_spinning` i la
|
||||
// reaparició al spawn; `initial_lives` és el comptador d'intents amb
|
||||
// què naix cada Player. Si el bloc manca, defaults segurs (0/3).
|
||||
struct DeathConfig {
|
||||
int respawn_delay_ms{0};
|
||||
int initial_lives{3};
|
||||
};
|
||||
static DeathConfig death_config;
|
||||
|
||||
// Llig el bloc `death:` del YAML i n'omple `death_config`. Si manca,
|
||||
// `respawn_delay_ms` queda a 0 (el respawn és immediat al final de
|
||||
// l'animació) i `initial_lives` es manté al default (3).
|
||||
static void readDeathBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració de la immunitat post-spawn llegida del bloc `immunity:`
|
||||
// del YAML. `duration_ms` és el temps per defecte que dura cada
|
||||
// immunitat (l'invoca `respawn`). `blink_period_ms` és el mig-cicle del
|
||||
// parpadeig visual: cada `blink_period_ms` ms, el render alterna
|
||||
// entre la paleta normal i la paleta `palette_key`. Si `duration_ms`
|
||||
// és 0 (o el bloc manca) la immunitat queda desactivada i el
|
||||
// comportament és l'antic.
|
||||
struct ImmunityConfig {
|
||||
float duration_ms{0.0F};
|
||||
int blink_period_ms{0};
|
||||
// Clau al Resource::Cache de la paleta plana usada al blink (basename
|
||||
// del .pal a `data/entities/player/`). Buida = no hi ha alternança visual,
|
||||
// només la finestra lògica d'immunitat (que sense feedback visual
|
||||
// pot ser confusa, però no és incorrecte).
|
||||
std::string palette_key;
|
||||
};
|
||||
static ImmunityConfig immunity_config;
|
||||
|
||||
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
|
||||
// hi ha `palette`, registra la paleta corresponent al Resource::Cache
|
||||
// (path: `data/entities/player/<palette_key>.pal`). Mateix patró que els altres
|
||||
// readers — `loadFrom` el crida al final.
|
||||
static void readImmunityBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Llig el bloc opcional `light: { radius: N, flicker: bool }` del
|
||||
// YAML i pobla `light_radius` / `light_flicker`. Si manca, els
|
||||
// valors queden a 0 / false (jugador sense llum).
|
||||
static void readLightBlock(const fkyaml::node& yaml);
|
||||
void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) const;
|
||||
|
||||
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
|
||||
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
|
||||
@@ -992,10 +956,16 @@ class Player : public AnimatedSprite {
|
||||
// Extret de `update` per acotar la complexitat cognitiva (clang-tidy).
|
||||
void updateFacingAndTurn(float delta_time, float vx, float vy);
|
||||
static auto cardinalFromInput(float vx, float vy) -> Cardinal;
|
||||
static auto cardinalFromString(const std::string& s) -> Cardinal;
|
||||
static auto opposite(Cardinal c) -> Cardinal;
|
||||
static auto intermediateFor(Cardinal target) -> Cardinal;
|
||||
auto intermediateFor(Cardinal target) const -> Cardinal;
|
||||
|
||||
public:
|
||||
// Helper pur: tradueix un string YAML ("north", "south", ...) al
|
||||
// Cardinal corresponent. Públic perquè `Config::loadFromYaml` el crida
|
||||
// des d'un helper de la TU anònima a player.cpp.
|
||||
static auto cardinalFromString(const std::string& s) -> Cardinal;
|
||||
|
||||
private:
|
||||
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
||||
// Player només en guarda un punter per publicar events.
|
||||
EventBus* bus_{nullptr};
|
||||
|
||||
Reference in new issue
Block a user