diff --git a/data/player/immune.pal b/data/player/immune.pal new file mode 100644 index 0000000..fd99122 --- /dev/null +++ b/data/player/immune.pal @@ -0,0 +1,35 @@ +JASC-PAL +0100 +32 +0 0 0 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 +217 217 217 diff --git a/data/player/morcus.yaml b/data/player/morcus.yaml index 07823bb..6d38939 100644 --- a/data/player/morcus.yaml +++ b/data/player/morcus.yaml @@ -131,3 +131,15 @@ whip: death: respawn_delay_ms: 500 lives: 3 + +# Immunitat post-spawn: durant `duration_ms` ms, els hazards no +# poden ferir ni matar el jugador (els forats sí que el maten — la +# caiguda no és dany convencional). Visualment es manifesta amb un +# parpadeig que alterna la paleta normal del sprite amb la paleta +# `palette` (basename de `data/player/.pal`) cada +# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la +# mecànica queda desactivada i el comportament és l'antic. +immunity: + duration_ms: 3000 + blink_period_ms: 100 + palette: immune diff --git a/data/player/sam.yaml b/data/player/sam.yaml index 7c33023..bfcc650 100644 --- a/data/player/sam.yaml +++ b/data/player/sam.yaml @@ -131,3 +131,15 @@ whip: death: respawn_delay_ms: 500 lives: 3 + +# Immunitat post-spawn: durant `duration_ms` ms, els hazards no +# poden ferir ni matar el jugador (els forats sí que el maten — la +# caiguda no és dany convencional). Visualment es manifesta amb un +# parpadeig que alterna la paleta normal del sprite amb la paleta +# `palette` (basename de `data/player/.pal`) cada +# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la +# mecànica queda desactivada i el comportament és l'antic. +immunity: + duration_ms: 3000 + blink_period_ms: 100 + palette: immune diff --git a/source/core/rendering/surface.hpp b/source/core/rendering/surface.hpp index b721202..4747a44 100644 --- a/source/core/rendering/surface.hpp +++ b/source/core/rendering/surface.hpp @@ -64,6 +64,10 @@ class Surface { void setPalette(const Palette& palette) { palette_ = palette; } [[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; } + // Accés de lectura a la paleta sencera. L'usen consumidors que volen + // fer save/restore (p.ex. el blink d'immunitat del Player, que aplica + // una paleta plana abans del render i restaura la normal després). + [[nodiscard]] auto getPalette() const -> const Palette& { return palette_; } static void initializeSubPalette(SubPalette& palette) { std::iota(palette.begin(), palette.end(), 0); } diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 21f6800..00b47f3 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -14,6 +14,8 @@ #include "core/input/input_state.hpp" #include "core/rendering/canvas.hpp" #include "core/rendering/screen.hpp" +#include "core/rendering/surface.hpp" +#include "core/resources/palette_loader.hpp" #include "core/resources/resource_cache.hpp" #include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" @@ -75,6 +77,7 @@ 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; void Player::loadFrom(const std::string& yaml_path) { auto bytes = Resource::Helper::loadFile(yaml_path); @@ -131,6 +134,11 @@ void Player::loadFrom(const std::string& yaml_path) { // 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); } void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) { @@ -205,6 +213,20 @@ void Player::readDeathBlock(const fkyaml::node& yaml) { if (d.contains("lives")) { death_config.initial_lives = d["lives"].get_value(); } } +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(); } + if (im.contains("blink_period_ms")) { immunity_config.blink_period_ms = im["blink_period_ms"].get_value(); } + // Paleta del blink: el basename val tant de clau al Resource::Cache + // com de path (`data/player/.pal`). Mateix patró que els sons. + if (im.contains("palette")) { + immunity_config.palette_key = im["palette"].get_value(); + Resource::Cache::get().loadPalette(immunity_config.palette_key, "data/player/" + immunity_config.palette_key + ".pal"); + } +} + void Player::readWhipBlock(const fkyaml::node& yaml) { whip_config = WhipConfig{}; if (!yaml.contains("whip")) { return; } @@ -372,6 +394,12 @@ Player::Player(const std::string& anim_key, int id, float x, float y, const Cont // GameScene el reescriu via `setSpawnPoint` quan canvia de sala. spawn_x_ = x; spawn_y_ = y; + // Cachegem la paleta normal del sprite ara que la Surface ja està + // carregada. El render del blink d'immunitat la restaura després + // d'aplicar la paleta plana — fer-ho via membre evita dependre de + // l'estat momentani de la Surface (que pot ser compartida entre + // jugadors del mateix personatge). + if (surface_ != nullptr) { normal_palette_ = surface_->getPalette(); } } void Player::update(float delta_time) { @@ -469,6 +497,16 @@ void Player::tick(float delta_time) { // que GameScene el reactiva (respawn / spawnFromAir). if (isOutOfPlay()) { return; } + // Decrement del timer d'immunitat. Independent de l'estat: si el + // jugador mor durant la immunitat (només via forat, que no es gateja), + // el `respawn` rearma una nova finestra. Si la finestra acaba mentre + // és AIRBORNE/HURT, no passa res — el gate dels hazards passa a + // quedar obert al primer frame post-zero. + if (immunity_remaining_ms_ > 0.0F) { + immunity_remaining_ms_ -= delta_time * 1000.0F; + immunity_remaining_ms_ = std::max(immunity_remaining_ms_, 0.0F); + } + // DYING: animació `death_spinning` + pausa de respawn. Ignorem tot // l'input (ni moviment, ni salt, ni atac). El moviment queda // congelat al lloc de la mort; `applyMovement` avança l'animació i @@ -886,6 +924,11 @@ auto Player::chooseHurtDirection(Cardinal preferred) const -> Cardinal { } void Player::enterHurt(Cardinal preferred) { + // Gate d'immunitat: tots els callers de dany (whip friendly fire, + // caiguda damunt, futurs hazards) passen per ací. Una sola línia + // cobreix la finestra de gràcia post-spawn. + if (isImmune()) { return; } + // 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; } @@ -1140,6 +1183,13 @@ void Player::applyMovement(float delta_time) { } void Player::enterDying(DeathCause cause) { + // Gate d'immunitat amb bypass per a HOLE: la caiguda per agujero NO + // és dany convencional sinó "sortida lògica del món", així que els + // forats han de matar fins i tot el jugador immune. Qualsevol altra + // causa (mòmia letal via `player_killed`, etc.) queda neutralitzada + // mentre dura la finestra de gràcia. + if (cause != DeathCause::HOLE && isImmune()) { return; } + // Idempotent. Un segon contacte de momia mentre el jugador ja està // en DYING no reinicia l'animació ni el comptador. Tampoc disparem // el cicle si el jugador està fora de joc (EXITED al passar la @@ -1311,6 +1361,25 @@ void Player::render() { }); } + // Blink d'immunitat: cada `blink_period_ms` ms, el render alterna + // entre la paleta normal i la paleta plana. La fase es deriva del + // timer restant — al spawn (timer = duration) toca paleta plana + // perquè el feedback sigui immediat. La Surface pot ser compartida + // entre dos jugadors del mateix personatge: com que el render del + // joc és seqüencial, el save/restore al voltant del render del + // sprite és segur — quan acaba aquesta funció, la Surface torna a + // tenir la paleta normal i la pròxima crida pot fer el seu propi + // swap independentment. + const bool BLINK_ON = + immunity_remaining_ms_ > 0.0F && + immunity_config.blink_period_ms > 0 && + !immunity_config.palette_key.empty() && + surface_ != nullptr && + (static_cast(immunity_remaining_ms_) / immunity_config.blink_period_ms) % 2 == 0; + if (BLINK_ON) { + surface_->setPalette(Resource::Cache::get().getPalette(immunity_config.palette_key)); + } + if (jump_z_ == 0.0F) { AnimatedSprite::render(); } else { @@ -1320,6 +1389,8 @@ void Player::render() { pos_.y = SAVED_Y; } + if (BLINK_ON) { surface_->setPalette(normal_palette_); } + if (CLIP_HOLE) { Rendering::popClip(); } } @@ -1486,6 +1557,12 @@ void Player::respawn(float x, float y) { setCurrentAnimation("stand_south"); setPosX(x); setPosY(y); + // Immunitat post-spawn: armem la finestra de gràcia amb la durada del + // YAML. Cobreix l'aparició inicial (`spawnFromAir` → `respawn`) i el + // respawn post-mort (`respawnFromDying` → `respawn`). Si el bloc + // `immunity:` manca al YAML, `duration_ms` és 0 i `setImmunity` és + // no-op — comportament idèntic al d'abans. + setImmunity(immunity_config.duration_ms); } void Player::disable() { diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index b47290f..db694fb 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -8,6 +8,7 @@ #include #include "core/rendering/sprite/animated_sprite.hpp" +#include "core/rendering/surface.hpp" #include "external/fkyaml_node.hpp" #include "game/map/map.hpp" @@ -311,6 +312,25 @@ class Player : public AnimatedSprite { // game over. Públic per a HUD i tests. [[nodiscard]] auto getLivesRemaining() const -> int { return lives_remaining_; } + // Activa la immunitat per `duration_ms` ms. Mentre `isImmune()` és + // cert, `enterHurt` i `enterDying(GENERIC)` són no-op — els hazards no + // poden tocar el jugador. La caiguda per forats (`enterDying(HOLE)`) + // segueix funcionant: els forats no són dany convencional, són una + // sortida lògica del món. Idempotent: una segona crida reescriu el + // timer (no acumula). Visualment, el render alterna la paleta del + // sprite amb una paleta plana cada `immunity_config.blink_period_ms` + // ms. Si `duration_ms <= 0` és no-op (manera de cancel·lar + // explícitament la immunitat seria `setImmunity(0)` — equivalent al + // comportament natural quan el timer s'esgota). + void setImmunity(float duration_ms) { + if (duration_ms > 0.0F) { immunity_remaining_ms_ = duration_ms; } + } + + // Cert mentre el jugador té immunitat activa. Comprovat per + // `enterHurt` i `enterDying` per filtrar els hazards. La causa HOLE + // ignora aquest gate (vegeu `enterDying`). + [[nodiscard]] auto isImmune() const -> bool { return immunity_remaining_ms_ > 0.0F; } + // Reset complet a l'estat inicial: posició a (x, y), velocitats a 0, // estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat // per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent @@ -664,6 +684,30 @@ class Player : public AnimatedSprite { // 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/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/player/.pal`). Mateix patró que els altres + // readers — `loadFrom` el crida al final. + static void readImmunityBlock(const fkyaml::node& yaml); + // Activa l'estat ATTACKING en la direcció cardinal indicada (la // direcció cap on mira el jugador) i publica `whip_spawn` al bus // perquè el GameScene cree el `WhipAttack` corresponent. Posa el @@ -743,6 +787,20 @@ class Player : public AnimatedSprite { // LANDING_STUN i tot l'input queda bloquejat. float landing_stun_remaining_ms_{0.0F}; + // Temps restant d'immunitat (ms). Decreix cada frame al `tick`, + // independentment de l'estat — la immunitat és ortogonal al state + // machine. Mentre > 0, `enterHurt` i `enterDying(GENERIC)` són no-op. + // L'arma `setImmunity()` (cridada per `respawn` amb la durada del + // YAML), i el render alterna paleta cada `immunity_config.blink_period_ms`. + float immunity_remaining_ms_{0.0F}; + + // Còpia de la paleta normal del sprite (extreta de `surface_->getPalette()` + // al ctor). El render del blink l'usa per restaurar la paleta després + // d'aplicar la versió plana cada mig-cicle. Cachejada perquè la + // Surface pot ser compartida entre dos jugadors del mateix personatge + // i no és segur consultar-la entre swaps. + Palette normal_palette_{}; + // Estat anterior del botó de salt per fer detecció de flanc (saltem // només a la transició no-premut → premut, no mentre es manté). Es // manté un per teclat i un per mando perquè poden conviure. diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index ddb6ca8..12488a8 100644 --- a/source/utils/defines.hpp +++ b/source/utils/defines.hpp @@ -3,7 +3,7 @@ namespace Texts { constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; - constexpr const char* VERSION = "0.32.0"; // Single source of truth — Makefile el grep + constexpr const char* VERSION = "0.33.0"; // Single source of truth — Makefile el grep } // namespace Texts namespace GameCanvas {