From 932b4577256c1312f72fa0a765862ebc67054930 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 8 May 2026 13:17:28 +0200 Subject: [PATCH 1/6] feat(config): afegeix GameplayConfig persistent amb versionatge defensiu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Singleton paral·lel a UiConfig però escrivible: viu a la carpeta de sistema (gameplay.yaml). Defaults compilats: friendly_fire, players_collide i whip_damage_pierces_walls a true. Davant qualsevol irregularitat (fitxer absent/buit, parse error, version absent o desconeguda, clau requerida mal tipada o absent), reseteja a defaults i reescriu el fitxer en lloc de preservar estat parcial. Cablejat al Director::start després de Options::loadFromFile. Encara sense consumidors — els flags es cablejaran a WhipAttack i al sistema de col·lisió de jugadors en commits posteriors. --- CMakeLists.txt | 1 + source/core/system/director.cpp | 6 ++ source/game/gameplay_config.cpp | 163 ++++++++++++++++++++++++++++++++ source/game/gameplay_config.hpp | 71 ++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 source/game/gameplay_config.cpp create mode 100644 source/game/gameplay_config.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 537e2be..602c69f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,7 @@ set(APP_SOURCES source/utils/log.cpp source/utils/utils.cpp source/game/debug_config.cpp + source/game/gameplay_config.cpp source/game/options.cpp source/game/options_apply.cpp source/game/ui/service_menu.cpp diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 29f5fe8..e0a3c3d 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -29,6 +29,7 @@ #include "core/system/event_buffer.hpp" #include "core/ui/ui_config.hpp" #include "game/debug_config.hpp" +#include "game/gameplay_config.hpp" #ifdef _DEBUG #include "game/editor/editor_config.hpp" #endif @@ -93,6 +94,11 @@ Director::Director() { // el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`). GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml"); + // Tweaks de jugabilitat (látigo travessa sarcs, friendly fire, col·lisió + // entre jugadors). Fitxer escrivible des del menú de servei (F9). Si + // no existeix, GameplayConfig::load el crea amb els defaults. + GameplayConfig::get().load(system_folder_ + "gameplay.yaml"); + // Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix // es crea amb presets per defecte (off, crt, ntsc). Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE); diff --git a/source/game/gameplay_config.cpp b/source/game/gameplay_config.cpp new file mode 100644 index 0000000..1a4ea07 --- /dev/null +++ b/source/game/gameplay_config.cpp @@ -0,0 +1,163 @@ +#include "game/gameplay_config.hpp" + +#include +#include +#include +#include +#include + +#include "external/fkyaml_node.hpp" +#include "utils/log.hpp" + +namespace { + + auto readFile(const std::string& path) -> std::string { + std::ifstream file(path); + if (!file) { return {}; } + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + } + + // Llig una subclau booleana obligatòria (`section.key`). Retorna + // false si la secció o la clau no hi són o el tipus no és bool — + // qualsevol d'aquests casos és una irregularitat que dispara + // reescriptura del fitxer. + auto readRequiredBool(const fkyaml::node& root, const char* section, const char* key, bool& out) + -> bool { + if (!root.contains(section)) { return false; } + const auto& sec = root[section]; + if (!sec.contains(key)) { return false; } + try { + out = sec[key].get_value(); + return true; + } catch (const std::exception&) { + return false; + } + } + + // Escriu el fitxer amb els valors actuals + comentaris d'ajuda. + // Format estable perquè l'usuari pot editar-lo a mà. + void writeFile(const std::string& path, + int version, + bool whip_pierces, + bool friendly_fire, + bool players_collide) { + std::ofstream file(path); + if (!file) { + Log::error("gameplay.yaml", "no s'ha pogut escriure {}", path); + return; + } + file << "# gameplay.yaml — tweaks de jugabilitat persistents.\n"; + file << "# Edita des del menú de servei (F9 → Tweaks) o a mà.\n"; + file << "# Si esborres el fitxer (o aquesta capçalera de versió), es regenerarà\n"; + file << "# amb els defaults a la propera arrencada.\n"; + file << "\n"; + file << "version: " << version << "\n"; + file << "\n"; + file << "whip:\n"; + file << " damage_pierces_walls: " << (whip_pierces ? "true" : "false") << "\n"; + file << "\n"; + file << "combat:\n"; + file << " friendly_fire: " << (friendly_fire ? "true" : "false") << "\n"; + file << "\n"; + file << "players:\n"; + file << " collide_with_each_other: " << (players_collide ? "true" : "false") << "\n"; + } + +} // namespace + +auto GameplayConfig::get() -> GameplayConfig& { + static GameplayConfig instance_; + return instance_; +} + +void GameplayConfig::load(const std::string& file_path) { + file_path_ = file_path; + + std::error_code error; + const bool EXISTS = std::filesystem::exists(file_path, error); + const auto CONTENT = EXISTS ? readFile(file_path) : std::string{}; + + // Tots els valors van a temporaris; només ens els quedem si la + // validació completa passa. Així una validació parcial (p. ex. dues + // claus bones i una de dolenta) no deixa l'estat a mig camí. + bool whip_pierces = whip_damage_pierces_walls_; + bool ff = friendly_fire_; + bool players_collide = players_collide_; + + auto rewrite_with_defaults = [&](const char* reason) { + // Resetem els camps als defaults compilats abans de reescriure. + // Necessari si load() es crida diverses vegades amb fitxers + // distints; el primer pot haver mutat l'estat. + whip_damage_pierces_walls_ = true; + friendly_fire_ = true; + players_collide_ = true; + writeFile(file_path, + CURRENT_VERSION, + whip_damage_pierces_walls_, + friendly_fire_, + players_collide_); + Log::info("gameplay.yaml", "{} — reescrit a {} amb defaults", reason, file_path); + }; + + if (!EXISTS) { + rewrite_with_defaults("fitxer absent"); + return; + } + if (CONTENT.empty()) { + rewrite_with_defaults("fitxer buit"); + return; + } + + fkyaml::node yaml; + try { + yaml = fkyaml::node::deserialize(CONTENT); + } catch (const std::exception& e) { + rewrite_with_defaults((std::string("parse error: ") + e.what()).c_str()); + return; + } + + // `version` ha de ser un enter present i igual a CURRENT_VERSION. + // Qualsevol altra cosa (absent, tipus no enter, valor diferent) és + // una irregularitat: el fitxer ve d'una versió incompatible o ha + // estat manipulat de manera inesperada. + if (!yaml.contains("version")) { + rewrite_with_defaults("version absent"); + return; + } + int version = 0; + try { + version = yaml["version"].get_value(); + } catch (const std::exception&) { + rewrite_with_defaults("version no és enter"); + return; + } + if (version != CURRENT_VERSION) { + rewrite_with_defaults("version desconeguda"); + return; + } + + if (!readRequiredBool(yaml, "whip", "damage_pierces_walls", whip_pierces) || !readRequiredBool(yaml, "combat", "friendly_fire", ff) || !readRequiredBool(yaml, "players", "collide_with_each_other", players_collide)) { + rewrite_with_defaults("clau requerida absent o de tipus incorrecte"); + return; + } + + // Tot validat: ens quedem els valors llegits. + whip_damage_pierces_walls_ = whip_pierces; + friendly_fire_ = ff; + players_collide_ = players_collide; +} + +void GameplayConfig::save() const { + if (file_path_.empty()) { + // Sense path establert: cap consumidor s'ha registrat encara. + // Pot passar en tests que no carreguen el fitxer; silenciem el cas. + return; + } + writeFile(file_path_, + CURRENT_VERSION, + whip_damage_pierces_walls_, + friendly_fire_, + players_collide_); +} diff --git a/source/game/gameplay_config.hpp b/source/game/gameplay_config.hpp new file mode 100644 index 0000000..1608937 --- /dev/null +++ b/source/game/gameplay_config.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include + +// Tweaks de jugabilitat persistents per usuari. Carregat de +// `gameplay.yaml` dins la carpeta de sistema (vegeu +// `Director::createSystemFolder` per la ubicació real per plataforma). +// +// Diferent d'UiConfig (data/config/ui.yaml, només lectura): aquest fitxer +// és escrivible i el ServiceMenu en modifica els valors. Els defaults +// compilats són els que veuen els nous usuaris fins que toquen el menú. +// +// Política d'integritat: davant qualsevol irregularitat (fitxer absent, +// parse error, `version` absent o diferent, clau requerida absent o de +// tipus incorrecte), `load()` reseteja a defaults i reescriu el fitxer. +// No es preserven fitxers parcialment vàlids — així evitem dragar +// versions antigues incoherents quan l'esquema evolucioni. +class GameplayConfig { + public: + // Esquema actual del fitxer. Incrementar quan canviï l'estructura + // (renomenar/treure claus o canviar semàntica). Els fitxers d'usuari + // amb una `version` distinta es reescriuen amb els defaults nous. + // Afegir noves claus opcionals no requereix bump si els defaults + // compilats són equivalents al comportament anterior. + static constexpr int CURRENT_VERSION = 1; + + static auto get() -> GameplayConfig&; + + GameplayConfig(const GameplayConfig&) = delete; + GameplayConfig(GameplayConfig&&) = delete; + auto operator=(const GameplayConfig&) -> GameplayConfig& = delete; + auto operator=(GameplayConfig&&) -> GameplayConfig& = delete; + + // Llig el fitxer i guarda la ruta per a `save()`. Si no existeix, + // l'escriu amb els defaults compilats. Idempotent: pot cridar-se + // múltiples vegades (l'última crida fixa el path actiu). + void load(const std::string& file_path); + + // Persisteix l'estat actual al path passat a `load()`. Cridat pel + // ServiceMenu a cada canvi de toggle. No-op si `load()` no s'ha + // cridat encara (sense path no podem escriure enlloc útil). + void save() const; + + // Tweaks. Defaults: tots true. + // + // - whip_damage_pierces_walls: el dany del látigo travessa sarcòfags + // (NO el mur extern del playfield, que retalla igualment per + // evitar que el rect surti de la sala visible). + // - friendly_fire: un latigazo pot impactar l'altre jugador. + // - players_collide: els jugadors col·lisionen físicament entre si + // en lloc de travessar-se. + [[nodiscard]] auto whipDamagePiercesWalls() const -> bool { return whip_damage_pierces_walls_; } + [[nodiscard]] auto friendlyFire() const -> bool { return friendly_fire_; } + [[nodiscard]] auto playersCollide() const -> bool { return players_collide_; } + + void setWhipDamagePiercesWalls(bool v) { whip_damage_pierces_walls_ = v; } + void setFriendlyFire(bool v) { friendly_fire_ = v; } + void setPlayersCollide(bool v) { players_collide_ = v; } + + private: + GameplayConfig() = default; + ~GameplayConfig() = default; + + // Ruta absoluta al fitxer persistit. Buida fins que algú crida + // `load()` per primera vegada; mentre estigui buida, `save()` és no-op. + std::string file_path_; + + bool whip_damage_pierces_walls_{true}; + bool friendly_fire_{true}; + bool players_collide_{true}; +}; From 7871d3930e13623310fa0c620dc826c00dd11b3b Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 8 May 2026 13:22:52 +0200 Subject: [PATCH 2/6] feat(whip): cabla flag whip.damage_pierces_walls a computeDamageRect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quan el flag està actiu (default), l'iteració píxel a píxel ignora `Map::collides` i només respecta `insidePlayfield`. El mur extern, doncs, sempre retalla — fora del playfield no hi ha res a colpejar. Els tests existents passen amb la nova lògica perquè els seus setups posen el damage_rect a corredors lliures (sense sarcs a la trajectòria), i el test W2 segueix verificant el retall del playfield. --- CMakeLists.txt | 1 + source/game/entities/concrete/whip_attack.cpp | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 602c69f..280d3fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -398,6 +398,7 @@ if(BUILD_TESTS) tests/audio_cache_stubs.cpp source/core/resources/resource_helper.cpp source/core/resources/resource_pack.cpp + source/game/gameplay_config.cpp source/game/map/map.cpp source/utils/log.cpp ${ENTITY_SOURCES_BASE} diff --git a/source/game/entities/concrete/whip_attack.cpp b/source/game/entities/concrete/whip_attack.cpp index b638eb0..794f4df 100644 --- a/source/game/entities/concrete/whip_attack.cpp +++ b/source/game/entities/concrete/whip_attack.cpp @@ -9,6 +9,7 @@ #include "game/entities/entity_factory.hpp" #include "game/entities/entity_manager.hpp" #include "game/entities/player.hpp" +#include "game/gameplay_config.hpp" #include "game/map/map.hpp" namespace { @@ -174,9 +175,16 @@ void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y, // d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`. // L'amplada perpendicular és `tile_size` (els passadissos del joc // tenen exactament aquesta mida i l'atac és lateral). + // + // Si el tweak `whip.damage_pierces_walls` està actiu, la hitbox + // ignora els sarcòfags (passa a través del bloc complet) però + // continua respectant el `insidePlayfield`: fora de la sala no hi + // ha res a colpejar i ampliar-ho generaria visuals/edge cases + // estranys. El mur extern, doncs, sempre retalla. const auto TILE = static_cast(Map::tile_size); const int MAX_PX = static_cast(std::max(0.0F, range_px)); const auto MAX = static_cast(MAX_PX); + const bool PIERCES = GameplayConfig::get().whipDamagePiercesWalls(); float length = 0.0F; auto try_probe = [&](float l) -> bool { @@ -198,7 +206,9 @@ void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y, probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l}; break; } - return Map::insidePlayfield(probe) && !Map::collides(probe); + if (!Map::insidePlayfield(probe)) { return false; } + if (PIERCES) { return true; } + return !Map::collides(probe); }; for (int i = 1; i <= MAX_PX; ++i) { From cfbcdc236bc20c47cbd187cebe655e1413ca807e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 8 May 2026 13:24:33 +0200 Subject: [PATCH 3/6] feat(whip): cabla flag combat.friendly_fire a hitPlayerIfApplicable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quan el flag està desactivat, el látigo no fa dany a altres jugadors (l'owner ja quedava exclòs per la guarda existent). Default true: manté el comportament actual. Sense canvis als tests del whip — friendly fire ja no es cobreix allà perquè els stubs de Player no porten estat real (vegeu el comentari del top de test_whip_attack.cpp). --- source/game/entities/concrete/whip_attack.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/game/entities/concrete/whip_attack.cpp b/source/game/entities/concrete/whip_attack.cpp index 794f4df..e75b16e 100644 --- a/source/game/entities/concrete/whip_attack.cpp +++ b/source/game/entities/concrete/whip_attack.cpp @@ -321,6 +321,10 @@ namespace { WhipAttack::Direction dir, std::unordered_set& already_hit) { if (p == owner || !playerIsTargetable(p)) { return; } + // Tweak `combat.friendly_fire`: si està desactivat, els altres + // jugadors són immunes al látigo aliat. L'owner ja queda exclòs + // per la primera guarda; aquest filtre cobreix només cross-player. + if (!GameplayConfig::get().friendlyFire()) { return; } if (already_hit.contains(static_cast(p))) { return; } const auto BOX = p->getBaseBox(); if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; } From 3635cebf256cffb28e463b72c9e8b10a3a00443b Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 8 May 2026 13:26:47 +0200 Subject: [PATCH 4/6] feat(player): cabla flag players.collide_with_each_other MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `collidesWithWorld` saltem la branca actor↔actor quan el flag està desactivat: els jugadors travessen els altres lliurement però segueixen topant amb murs i sarcs. A `tryPushSnapBlockers` retornem early per la mateixa raó: si no hi ha col·lisió entre jugadors, no hi ha ningú a empentar quan el snap del corredor s'aplica. --- source/game/entities/player.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index f009bf2..4f47b04 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -22,6 +22,7 @@ #include "game/entities/entity.hpp" #include "game/entities/entity_manager.hpp" #include "game/entities/event_bus.hpp" +#include "game/gameplay_config.hpp" #include "game/options.hpp" #include "utils/defaults.hpp" @@ -414,6 +415,10 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool { // al check actor↔actor més avall via el predicat `isOutOfPlay`. if (isOutOfPlay()) { return false; } if (map_.collides(box)) { return true; } + // Tweak `players.collide_with_each_other`: si està desactivat, els + // jugadors es travessen lliurement. La col·lisió contra el món + // (murs/sarcs) ja s'ha avaluat dalt; saltem només la part actor↔actor. + if (!GameplayConfig::get().playersCollide()) { return false; } if (all_players_ == nullptr) { return false; } // Per al contacte actor-actor usem la hit-box petita a banda i banda: // projectem la hit-box des de la posició base proposada i la comparem @@ -1737,6 +1742,10 @@ auto Player::tryPushSnapBlockers( float (Player::*get_pos)() const, void (Player::*set_pos)(float)) -> bool { if (all_players_ == nullptr) { return false; } + // Si el tweak de col·lisió entre jugadors està desactivat, mai hi + // ha res a empentar — els jugadors són intangibles entre si i el + // snap pot procedir lliurement sense desplaçar ningú. + if (!GameplayConfig::get().playersCollide()) { return false; } // Blockers = jugadors que ara mateix solapen la nostra hit-box (ja // amb el snap aplicat). Mateix criteri actor-actor que `collidesWithWorld`. From e25ccd02fd40abc763028c323d7a3041fb6ebbc9 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 8 May 2026 13:29:04 +0200 Subject: [PATCH 5/6] =?UTF-8?q?feat(menu):=20submen=C3=BA=20"Jugabilitat"?= =?UTF-8?q?=20al=20men=C3=BA=20de=20servei=20amb=20tres=20toggles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nou apartat al ServiceMenu (entre Controls i Sistema) amb tres toggles ON/OFF connectats a GameplayConfig: látec travessa sarcs, foc amic, col·lisió entre jugadors. Cada canvi crida save() immediatament, així l'estat persisteix entre sessions sense necessitat de "aplicar". Locales ca/en: claus `tweaks` + tres `menu.items.*` corresponents. --- data/locale/ca.yaml | 5 +++++ data/locale/en.yaml | 5 +++++ source/game/ui/service_menu.cpp | 39 +++++++++++++++++++++++++++++++++ source/game/ui/service_menu.hpp | 1 + 4 files changed, 50 insertions(+) diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index d7965ae..ca17f35 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -62,6 +62,7 @@ menu: controls: "Controls" keyboard: "Teclat" gamepad: "Comandament" + tweaks: "Jugabilitat" system: "Sistema" labels: player_1: "Jugador 1" @@ -73,6 +74,7 @@ menu: controls_submenu: "Controls" keyboard_submenu: "Teclat" gamepad_submenu: "Comandament" + tweaks_submenu: "Jugabilitat" system_submenu: "Sistema" move_up: "Amunt" move_down: "Avall" @@ -101,6 +103,9 @@ menu: music_volume: "Volum música" sounds: "Sons" sounds_volume: "Volum sons" + whip_pierces_walls: "Látec travessa sarcs" + friendly_fire: "Foc amic" + players_collide: "Col·lisió entre jugadors" restart: "Reiniciar" exit_game: "Eixir del joc" values: diff --git a/data/locale/en.yaml b/data/locale/en.yaml index f2af055..32fc53a 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -61,6 +61,7 @@ menu: controls: "Controls" keyboard: "Keyboard" gamepad: "Gamepad" + tweaks: "Gameplay" system: "System" labels: player_1: "Player 1" @@ -72,6 +73,7 @@ menu: controls_submenu: "Controls" keyboard_submenu: "Keyboard" gamepad_submenu: "Gamepad" + tweaks_submenu: "Gameplay" system_submenu: "System" move_up: "Up" move_down: "Down" @@ -100,6 +102,9 @@ menu: music_volume: "Music volume" sounds: "Sounds" sounds_volume: "Sound volume" + whip_pierces_walls: "Whip pierces sarcs" + friendly_fire: "Friendly fire" + players_collide: "Player collision" restart: "Restart" exit_game: "Exit game" values: diff --git a/source/game/ui/service_menu.cpp b/source/game/ui/service_menu.cpp index f459fbb..80d9376 100644 --- a/source/game/ui/service_menu.cpp +++ b/source/game/ui/service_menu.cpp @@ -17,6 +17,7 @@ #include "core/rendering/text.hpp" #include "core/system/event_buffer.hpp" #include "core/ui/ui_config.hpp" +#include "game/gameplay_config.hpp" #include "game/options.hpp" #include "game/options_apply.hpp" #include "utils/defaults.hpp" @@ -692,6 +693,8 @@ auto ServiceMenu::buildRoot() -> Page { []() { ServiceMenu::get().pushPage(buildAudio()); })); page.items.push_back(makeSubmenu("menu.items.controls_submenu", []() { ServiceMenu::get().pushPage(buildControls()); })); + page.items.push_back(makeSubmenu("menu.items.tweaks_submenu", + []() { ServiceMenu::get().pushPage(buildTweaks()); })); page.items.push_back(makeSubmenu("menu.items.system_submenu", []() { ServiceMenu::get().pushPage(buildSystem()); })); return page; @@ -1155,6 +1158,42 @@ void ServiceMenu::startDefineButtons(SDL_Gamepad* pad, const std::string& name, define_buttons_.enable(pad, name, path); } +auto ServiceMenu::buildTweaks() -> Page { + // Submenú "Tweaks" — flags de jugabilitat persistits a `gameplay.yaml` + // dins la carpeta de sistema. Cada toggle muta `GameplayConfig` i + // n'invoca `save()` immediatament: l'usuari veu el canvi reflectit al + // joc sense esperar a un "aplicar" i, si tanca el joc, torna en el + // mateix estat. Tres toggles ON/OFF, sense estat addicional. + Page page; + page.title_key = "menu.titles.tweaks"; + + page.items.push_back(makeToggle( + "menu.items.whip_pierces_walls", + []() { return GameplayConfig::get().whipDamagePiercesWalls(); }, + [](bool v) { + GameplayConfig::get().setWhipDamagePiercesWalls(v); + GameplayConfig::get().save(); + })); + + page.items.push_back(makeToggle( + "menu.items.friendly_fire", + []() { return GameplayConfig::get().friendlyFire(); }, + [](bool v) { + GameplayConfig::get().setFriendlyFire(v); + GameplayConfig::get().save(); + })); + + page.items.push_back(makeToggle( + "menu.items.players_collide", + []() { return GameplayConfig::get().playersCollide(); }, + [](bool v) { + GameplayConfig::get().setPlayersCollide(v); + GameplayConfig::get().save(); + })); + + return page; +} + auto ServiceMenu::buildSystem() -> Page { Page page; page.title_key = "menu.titles.system"; diff --git a/source/game/ui/service_menu.hpp b/source/game/ui/service_menu.hpp index 4496a2c..76d94d9 100644 --- a/source/game/ui/service_menu.hpp +++ b/source/game/ui/service_menu.hpp @@ -253,6 +253,7 @@ class ServiceMenu { [[nodiscard]] static auto buildKeyboardControls() -> Page; [[nodiscard]] static auto buildGamepadControls() -> Page; [[nodiscard]] static auto buildSystem() -> Page; + [[nodiscard]] static auto buildTweaks() -> Page; // Helpers privats d'items de vídeo: aïllen la lògica de // `shader_type` i `preset` fora de `buildVideo` perquè no exploti la From d0d5cb0e662be4f58e3e63d3603ff3d30c002f42 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 8 May 2026 13:40:30 +0200 Subject: [PATCH 6/6] =?UTF-8?q?fix(player):=20players=5Fcollide=20tamb?= =?UTF-8?q?=C3=A9=20anul=C2=B7la=20resolver=20i=20landing=20overlap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/entities/player.cpp | 6 ++++++ source/game/scenes/game_scene.cpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 4f47b04..746eae6 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -1032,6 +1032,12 @@ void Player::resolveLandingOverlap() { // canvi, qualsevol jugador amb el qual solape entra en HURT i // surt projectat. Així el solapament inicial es resol per moviment // del solapat, no per desplaçament forçat al lloc. + // + // Tweak `players.collide_with_each_other`: si està desactivat, el + // saltador travessa l'altre net en lloc de projectar-lo. Aquest + // camí és independent de `collidesWithWorld`, així que cal aturar-lo + // explícitament aquí. + if (!GameplayConfig::get().playersCollide()) { return; } if (all_players_ == nullptr) { return; } for (Player* other : *all_players_) { if (other == nullptr || other == this) { continue; } diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index e96b37b..d3981b9 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -32,6 +32,7 @@ #include "game/entities/entity_loader.hpp" #include "game/entities/event_bus.hpp" #include "game/entities/player.hpp" +#include "game/gameplay_config.hpp" #include "game/levels/level_manager.hpp" #include "game/map/map.hpp" #include "game/options.hpp" @@ -108,6 +109,11 @@ namespace { auto* a = ps[0]; auto* b = ps[1]; if (a == nullptr || b == nullptr) { return; } + // Tweak `players.collide_with_each_other`: amb el flag desactivat + // saltem TOT el resolver — head-on, push i bounce aeri queden + // anul·lats. La col·lisió contra el món continua intacta perquè + // viu a `Player::collidesWithWorld`/`tryMove`, no aquí. + if (!GameplayConfig::get().playersCollide()) { return; } const bool A_AIR = a->isAirborne(); const bool B_AIR = b->isAirborne();