diff --git a/scripts/api/entity/spaceobject.lua b/scripts/api/entity/spaceobject.lua index 78c6e497fb..c9747cc349 100644 --- a/scripts/api/entity/spaceobject.lua +++ b/scripts/api/entity/spaceobject.lua @@ -387,6 +387,70 @@ function Entity:scanningChannelDepth() if self.components.scan_state then return self.components.scan_state.depth end return 0 end +--- Sets a per-entity hacking difficulty override for this entity, replacing the global hacking difficulty for this target. +--- Valid values are between 0 (easiest) and 3 (hardest), matching the global setting range. +--- Pass -1 to clear the override and use the global hacking difficulty instead. +--- Example: entity:setHackingDifficulty(3) +function Entity:setHackingDifficulty(difficulty) + if difficulty < 0 then + local games = self.components.hacking_target and self.components.hacking_target.games or -1 + -- If neither difficulty nor game type is customized, remove the HackingTarget component. + if games < 0 then + self.components.hacking_target = nil + else + self.components.hacking_target = {difficulty=-1} + end + else + self.components.hacking_target = {difficulty=difficulty} + end + return self +end +--- Returns the per-entity hacking difficulty override for this entity. +--- Returns a number between 0 (easiest) and 3 (hardest), or -1 if no override is set and the global hacking difficulty applies. +--- Example: +--- entity:setHackingDifficulty(3) +--- entity:getHackingDifficulty() -- returns 3 +function Entity:getHackingDifficulty() + if self.components.hacking_target then return self.components.hacking_target.difficulty end + return -1 +end +--- Sets a per-entity hacking game override for this entity, replacing the global hacking game setting for this target. +--- Valid values are "mines", "lights", or "all". Pass nil to clear the override. +--- Example: entity:setHackingGame("mines") +function Entity:setHackingGame(game) + local games = -1 + if game == "mines" then games = 0 + elseif game == "lights" then games = 1 + elseif game == "all" then games = 2 + end + -- If neither difficulty nor game type is customized, remove the HackingTarget component. + if games < 0 then + local difficulty = self.components.hacking_target and self.components.hacking_target.difficulty or -1 + if difficulty < 0 then + self.components.hacking_target = nil + else + self.components.hacking_target = {games=-1} + end + else + self.components.hacking_target = {games=games} + end + return self +end +--- Returns the per-entity hacking game override for this entity. +--- Returns "mines", "lights", or "all", or nil if no override is set. +--- Example: +--- entity:setHackingGame("mines") +--- entity:getHackingGame() -- returns "mines" +function Entity:getHackingGame() + if self.components.hacking_target then + local g = self.components.hacking_target.games + if g == 0 then return "mines" + elseif g == 1 then return "lights" + elseif g == 2 then return "all" + end + end + return nil +end --- Defines whether all factions consider this entity as having been scanned. --- Only ship entities are created in an unscanned state. Other entities are created as fully scanned. --- If false, all factions treat this entity as unscanned. diff --git a/src/components/hacking.h b/src/components/hacking.h index ebabd23a6b..9d0b800140 100644 --- a/src/components/hacking.h +++ b/src/components/hacking.h @@ -1,8 +1,21 @@ #pragma once -// Component to indicate that we can hack other ships. +// Component to indicate that this entity can hack other entities. class HackingDevice { public: float effectiveness = 0.5f; +}; + +// Component on hackable entities to store any per-entity overrides for +// difficulty and hacking minigame type. +class HackingTarget +{ +public: + // Difficulty values map to the global range (0-3). A value of -1 uses the + // global hacking_difficulty instead. + int difficulty = -1; + // Maps to an EHackingGames value. Value of -1 uses global hacking_games + // instead. + int games = -1; }; \ No newline at end of file diff --git a/src/init/ecs.cpp b/src/init/ecs.cpp index 294db64331..b329517aea 100644 --- a/src/init/ecs.cpp +++ b/src/init/ecs.cpp @@ -85,6 +85,7 @@ void initSystemsAndComponents() sp::ecs::MultiplayerReplication::registerComponentReplication(); sp::ecs::MultiplayerReplication::registerComponentReplication(); sp::ecs::MultiplayerReplication::registerComponentReplication(); + sp::ecs::MultiplayerReplication::registerComponentReplication(); sp::ecs::MultiplayerReplication::registerComponentReplication(); sp::ecs::MultiplayerReplication::registerComponentReplication(); sp::ecs::MultiplayerReplication::registerComponentReplication(); diff --git a/src/multiplayer/hacking.cpp b/src/multiplayer/hacking.cpp index 905e552179..b0caf08cca 100644 --- a/src/multiplayer/hacking.cpp +++ b/src/multiplayer/hacking.cpp @@ -4,4 +4,9 @@ BASIC_REPLICATION_IMPL(HackingDeviceReplication, HackingDevice) BASIC_REPLICATION_FIELD(effectiveness); +} + +BASIC_REPLICATION_IMPL(HackingTargetReplication, HackingTarget) + BASIC_REPLICATION_FIELD(difficulty); + BASIC_REPLICATION_FIELD(games); } \ No newline at end of file diff --git a/src/multiplayer/hacking.h b/src/multiplayer/hacking.h index b357e6838c..5e8c5a139c 100644 --- a/src/multiplayer/hacking.h +++ b/src/multiplayer/hacking.h @@ -4,3 +4,4 @@ #include "components/hacking.h" BASIC_REPLICATION_CLASS(HackingDeviceReplication, HackingDevice); +BASIC_REPLICATION_CLASS(HackingTargetReplication, HackingTarget); diff --git a/src/screenComponents/hackingDialog.cpp b/src/screenComponents/hackingDialog.cpp index 1d2ec793d3..9cbad214c1 100644 --- a/src/screenComponents/hackingDialog.cpp +++ b/src/screenComponents/hackingDialog.cpp @@ -10,6 +10,8 @@ #include #include +#include "components/hacking.h" + #include "gui/gui2_panel.h" #include "gui/gui2_label.h" #include "gui/gui2_listbox.h" @@ -158,14 +160,27 @@ void GuiHackingDialog::onMiniGameComplete(bool success) status_label->setText(success ? tr("Hacking SUCCESS!") : tr("Hacking FAILURE!")); } -void GuiHackingDialog::getNewGame() { +void GuiHackingDialog::getNewGame() +{ + // Apply difficulty and game type settings in this priority order: + // - Per-entity overrides via HackingTarget component + // - If none, use GameGlobalInfo settings + // - If none, use global defaults int difficulty = 2; EHackingGames games = HG_All; - if (gameGlobalInfo) { + + if (gameGlobalInfo) + { difficulty = gameGlobalInfo->hacking_difficulty; games = gameGlobalInfo->hacking_games; } + if (auto ht = target.getComponent()) + { + if (ht->difficulty >= 0) difficulty = ht->difficulty; + if (ht->games >= 0) games = EHackingGames(ht->games); + } + const string lights_help = tr("To successfully hack this system, you must fully illuminate all binary countermeasure nodes.\n\nSelect a node in the grid to toggle its state between off and on. Doing so also toggles the state of adjacent nodes immediately above, below, and to the selected node's sides. Continue toggling nodes until every node is illuminated.\n\nYou can make an unlimited number of moves, but seek efficient solutions to best aid your crewmates. Inexperienced hackers might chase dark nodes from top row to the bottom row by selecting the node immediately below each dark node. More experienced intrusion specialists might identify more efficient solutions.\n\nClick the Reset button to reset the field, or select a system to attempt a different intrusion method."); const string mine_help = tr("To successfully hack this system, you must apply a systematic process of elimination to identify sensitive data nodes within a grid without disturbing them.\n\nSelect a node in the grid to reveal it. If you reveal a sensitive node, the hacking interface marks it with an X. You can safely reveal one sensitive node, but revealing a second sensitive node alerts the system being hacked and disconnects your intrusion attempt.\n\nAn empty node lights up, and if no sensitive nodes surround it, any other adjacent nodes that also lack a nearby sensitive node automatically reveal themselves.\n\nIf a sensitive node is immediately adjacent to or diagonal from the revealed node, a number indicates how many of the surrounding nodes are sensitive. Seek patterns in the numbers that surround unrevealed nodes to determine which unrevealed nodes are sensitive.\n\nClick the Reset button to reset the field, or select a system to attempt a different intrusion method."); diff --git a/src/screens/gm/tweak.cpp b/src/screens/gm/tweak.cpp index 6b5dd803fc..4676561760 100644 --- a/src/screens/gm/tweak.cpp +++ b/src/screens/gm/tweak.cpp @@ -511,6 +511,63 @@ GuiEntityTweak::GuiEntityTweak(GuiContainer* owner) ADD_PAGE(tr("tweak-tab", "Hacking"), HackingDevice); ADD_NUM_TEXT_TWEAK(tr("tweak-text", "Effectiveness:"), HackingDevice, effectiveness); + ADD_PAGE(tr("tweak-tab", "Hacking target"), HackingTarget); + { + auto row = new GuiElement(new_page->tweaks, ""); + row + ->setSize(GuiElement::GuiSizeMax, 30.0f) + ->setAttribute("layout", "horizontal"); + + (new GuiLabel(row, "", tr("tweak-text", "Difficulty:"), 20.0f)) + ->setAlignment(sp::Alignment::CenterRight) + ->setSize(GuiElement::GuiSizeMax, 30.0f); + + auto ui = new GuiSelectorTweak(row, "", + [this](int index, string value) + { + if (auto v = entity.getComponent()) + v->difficulty = index - 1; + } + ); + ui->addEntry(tr("hacking", "Global default"), ""); + ui->addEntry(tr("hacking", "Simple"), ""); + ui->addEntry(tr("hacking", "Normal"), ""); + ui->addEntry(tr("hacking", "Difficult"), ""); + ui->addEntry(tr("hacking", "Fiendish"), ""); + ui->update_func = [this]() -> int { + if (auto v = entity.getComponent()) + return v->difficulty + 1; + return 0; + }; + } + { + auto row = new GuiElement(new_page->tweaks, ""); + row + ->setSize(GuiElement::GuiSizeMax, 30.0f) + ->setAttribute("layout", "horizontal"); + + (new GuiLabel(row, "", tr("tweak-text", "Game:"), 20.0f)) + ->setAlignment(sp::Alignment::CenterRight) + ->setSize(GuiElement::GuiSizeMax, 30.0f); + + auto ui = new GuiSelectorTweak(row, "", + [this](int index, string value) + { + if (auto v = entity.getComponent()) + v->games = index - 1; + } + ); + ui->addEntry(tr("hacking", "Global default"), ""); + ui->addEntry(tr("hacking", "Mine"), ""); + ui->addEntry(tr("hacking", "Lights"), ""); + ui->addEntry(tr("hacking", "All"), ""); + ui->update_func = [this]() -> int { + if (auto v = entity.getComponent()) + return v->games + 1; + return 0; + }; + } + ADD_PAGE(tr("tweak-tab", "Self-destruct"), SelfDestruct); ADD_BOOL_TWEAK(tr("tweak-text", "Active:"), SelfDestruct, active); ADD_NUM_TEXT_TWEAK(tr("tweak-text", "Countdown:"), SelfDestruct, countdown); diff --git a/src/script/components.cpp b/src/script/components.cpp index c11385cfec..4ae77d6c4f 100644 --- a/src/script/components.cpp +++ b/src/script/components.cpp @@ -658,6 +658,9 @@ void initComponentScriptBindings() BIND_MEMBER(PlayerControl, allowed_positions); sp::script::ComponentHandler::name("hacking_device"); BIND_MEMBER(HackingDevice, effectiveness); + sp::script::ComponentHandler::name("hacking_target"); + BIND_MEMBER(HackingTarget, difficulty); + BIND_MEMBER(HackingTarget, games); sp::script::ComponentHandler::name("ship_log"); sp::script::ComponentHandler::name("move_to");