From e6f7407318b58cdc1173c946655a1fed61b0fa77 Mon Sep 17 00:00:00 2001 From: Kamayana Date: Tue, 1 Aug 2023 08:18:06 -0500 Subject: [PATCH] Cable rework part 2: Extension cords and more (#66871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make "Plug in / Unplug" the default link_up menu text * Make link_up use default ammo_scale of 0 * cable json cleanup; Remove JUMPSTART from plug cables; change cable materials * Add clear_pockets_if, a flexible function for deleting pocket contents * Simplify links by making devices handle links themselves; split up link iuse functions; add move_cost parameter; rename charge_rate to wattage * Add ability to extend cables with other cables * Improve link display in inventory, showing current/max length and number of extensions; adjust sorting of linked items * Reopen cable menu for convenience if reeling up an already unspooled cable * Fix vehicle cables breaking off the turn AFTER they pass the threshold * Drop snapped off vehicle cables at the previous tile position * Change find_vehicle() into a public map function, use for better link vehicle pointer initialization * Fix link creation connecting to the wrong point * Immediately process cables on creation if they're linked * Remove tow cable section of shed_loose_parts - Tow cables aren't loose parts, so this never gets called * Remove UNMOUNT_ON_MOVE from cables, since it's misleading. Don't change functionality * Let shed_loose_parts selectively exclude cables * Format vehicle cable linking / tow cable linking to match each other * Split link_length funcs between current and max * Make efficiency a float to more easily handle extensions * Adjust link_up item info * Add BATTERY flag for battery vehicle parts * Astyle * Revert "wattage" back to "charge_rate", as I scrapped my plans for that change * Remove the separate link_up function - the use for it was scrapped * Remove AUTO_DELETE_CABLE, replace with NO_DROP * Show a cable's linked vehicle in the interaction menu * Add a vehicle part inspect action to disconnect cables attached to it. Replace appliance's UNPLUG action with this * Improve cable stretch message for circular distances * Only return items with the CABLE_SPOOL flag with cables(), temporary measure for 0.G * Documentation * Reopen menu on manually detaching cable, as well * Minor fixes * Improve messaging of CBM/UPS/solar backpack links * Lint JSON * Update newly added cables * Clang tidying * Set cable traits and cable part mounts immediately to avoid inaccuracies * Fix linked repair tools * Fix invoke_item_activity losing the method after cancel_activity * Remove debug code * Apply suggestions from code review Co-authored-by: Jianxiang Wang (王健翔) * More readable booleans Co-authored-by: andrei <68240139+andrei8l@users.noreply.github.com> * Update src/item.cpp Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Make cable extensions contribute volume and weight * Minor fixes * Use const for minimum link efficiency * Revamp how max_length is handled so its recalculated less often * Rework link_extend_cable's weight/volume check to be more readable and functional * Rework link length sorting, moving it to sort_compare * Make AIM sort like inventory_ui does, including using link_sort_key * Let debug cables extend debug cables * Make getting/removing remote parts two separate functions * Change find_vehicle back into a static vehicle function * Change properties_to_item into a vehicle function, part_to_item, as it now requires functions only accessible from vehicle * Make it more clear you pick up cables you disconnect * Rework shed_loose_parts to fix vehicle connections, particularly out-of-bubble ones * Cleanup * Make invalidate_towing give the cable directly to the player if they can stash it * Fix disconnecting tow cables and jumper cables * Unused variable * Make assign_t_state work with appliances * Fix jumper cable -> appliance links * Reword link_to_veh_app & link_tow_cable variables for clarity, as "target" is already used for part targets * Fix connection messages * Reword lambda for cable setting & checking * Cancel link_up if the target point is too far for the cable * Fix error when trying to link a cable immediately after loading or saving * Rename `heavy-duty cable` to `heavy-duty jumper cable` --------- Co-authored-by: Jianxiang Wang (王健翔) Co-authored-by: andrei <68240139+andrei8l@users.noreply.github.com> Co-authored-by: Maleclypse <54345792+Maleclypse@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- data/json/flags.json | 4 - .../furniture_and_terrain/appliances.json | 2 +- data/json/item_actions.json | 2 +- data/json/items/tool/cables.json | 119 +- data/json/items/tool/cooking.json | 77 +- data/json/items/tool/electronics.json | 68 +- data/json/items/tool/lighting.json | 16 +- data/json/items/tool/misc.json | 32 +- data/json/items/tool/radio_tools.json | 7 +- data/json/items/tool/science.json | 35 +- data/json/items/tool/workshop.json | 84 +- data/json/items/tool_armor.json | 16 +- data/json/obsoletion/migration_items.json | 6 + data/json/player_activities.json | 10 + data/json/vehicleparts/battery.json | 20 +- data/json/vehicleparts/vehicle_parts.json | 12 +- data/json/vehicleparts/vp_flags.json | 4 + data/mods/TEST_DATA/appliance.json | 36 +- data/raw/keybindings.json | 7 - data/raw/keybindings/vehicle.json | 9 +- doc/JSON_FLAGS.md | 1 - doc/JSON_INFO.md | 24 +- src/activity_actor.cpp | 60 +- src/activity_actor_definitions.h | 34 +- src/activity_handlers.cpp | 6 +- src/advanced_inv.cpp | 18 +- src/flag.cpp | 1 - src/flag.h | 1 - src/inventory_ui.cpp | 9 +- src/item.cpp | 470 ++++--- src/item.h | 78 +- src/item_contents.cpp | 20 +- src/item_contents.h | 5 +- src/item_factory.cpp | 24 +- src/item_pocket.cpp | 2 +- src/iuse_actor.cpp | 1137 ++++++++++------- src/iuse_actor.h | 18 +- src/map.cpp | 4 +- src/player_activity.cpp | 2 + src/savegame_json.cpp | 15 +- src/veh_appliance.cpp | 37 - src/veh_appliance.h | 6 - src/veh_interact.cpp | 18 +- src/veh_type.cpp | 1 + src/veh_type.h | 1 + src/vehicle.cpp | 143 ++- src/vehicle.h | 53 +- src/vehicle_part.cpp | 59 +- src/vehicle_use.cpp | 68 +- tests/vehicle_test.cpp | 6 +- 50 files changed, 1547 insertions(+), 1340 deletions(-) diff --git a/data/json/flags.json b/data/json/flags.json index 68568286779cb..3e512681c76a9 100644 --- a/data/json/flags.json +++ b/data/json/flags.json @@ -1241,10 +1241,6 @@ "id": "CABLE_SPOOL", "type": "json_flag" }, - { - "id": "AUTO_DELETE_CABLE", - "type": "json_flag" - }, { "id": "CAMERA_PRO", "type": "json_flag" diff --git a/data/json/furniture_and_terrain/appliances.json b/data/json/furniture_and_terrain/appliances.json index 1e716c00e4340..278209d962947 100644 --- a/data/json/furniture_and_terrain/appliances.json +++ b/data/json/furniture_and_terrain/appliances.json @@ -588,7 +588,7 @@ "durability": 120, "description": "A power cord sticking out of an appliance. You need to plug it into a powered grid for the appliance to work properly.", "item": "power_cord", - "flags": [ "NO_INSTALL_HIDDEN", "NO_UNINSTALL", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "flags": [ "NO_INSTALL_HIDDEN", "NO_UNINSTALL", "UNMOUNT_ON_DAMAGE", "POWER_TRANSFER" ], "variants": [ { "symbols": "{", "symbols_broken": "s" } ] }, { diff --git a/data/json/item_actions.json b/data/json/item_actions.json index 6814d22f92c13..1ee2f881ec480 100644 --- a/data/json/item_actions.json +++ b/data/json/item_actions.json @@ -966,7 +966,7 @@ { "type": "item_action", "id": "link_up", - "name": { "str": "Attach / Detach" } + "name": { "str": "Plug in / Manage cables" } }, { "type": "item_action", diff --git a/data/json/items/tool/cables.json b/data/json/items/tool/cables.json index 258d139e49a60..bf5e6abc56da1 100644 --- a/data/json/items/tool/cables.json +++ b/data/json/items/tool/cables.json @@ -8,7 +8,7 @@ "to_hit": 1, "color": "dark_gray", "symbol": "&", - "material": [ "steel", "plastic" ], + "material": [ "copper", "plastic" ], "volume": "500 ml", "weight": "75 g", "category": "tools", @@ -16,34 +16,8 @@ "price_postapoc": 100, "max_charges": 3, "initial_charges": 3, - "use_action": { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "is_cable_item": true, - "cable_length": 3, - "targets": [ "vehicle_port" ] - }, - "flags": [ "CABLE_SPOOL", "AUTO_DELETE_CABLE", "SINGLE_USE" ], - "melee_damage": { "bash": 2 } - }, - { - "type": "TOOL", - "id": "generic_device_cable", - "name": { "str": "device cable" }, - "description": "A cable for attaching devices to power or to other devices.", - "//": "This cable is automatically handled by devices and is used for connecting them to vehicles/appliances", - "to_hit": 1, - "color": "dark_gray", - "symbol": "&", - "material": [ "steel", "plastic" ], - "volume": "500 ml", - "weight": "75 g", - "category": "tools", - "price": 1, - "price_postapoc": 70, - "max_charges": 4, - "initial_charges": 4, - "flags": [ "CABLE_SPOOL", "AUTO_DELETE_CABLE" ], + "use_action": { "type": "link_up", "menu_text": "Plug in / Manage connections", "targets": [ "vehicle_port" ] }, + "flags": [ "CABLE_SPOOL", "NO_DROP", "SINGLE_USE" ], "melee_damage": { "bash": 2 } }, { @@ -54,7 +28,7 @@ "to_hit": 1, "color": "light_blue", "symbol": "&", - "material": [ "steel", "plastic" ], + "material": [ "copper", "plastic" ], "volume": "500 ml", "weight": "75 g", "category": "tools", @@ -64,58 +38,67 @@ "initial_charges": 4, "use_action": { "type": "link_up", - "menu_text": "Attach / Detach", - "is_cable_item": true, - "cable_length": 4, - "targets": [ "no_link", "bio_cable", "ups", "solarpack", "vehicle_battery" ] + "menu_text": "Attach / Manage connections", + "targets": [ "no_link", "bio_cable", "ups", "solarpack", "vehicle_battery" ], + "can_extend": [ "jumper_cable", "jumper_cable_heavy" ] }, "flags": [ "CABLE_SPOOL", "SINGLE_USE" ], "melee_damage": { "bash": 2 }, "qualities": [ [ "JUMPSTART", 1 ] ] }, + { + "type": "TOOL", + "id": "jumper_cable_heavy", + "name": { "str": "heavy-duty jumper cable" }, + "description": "A long, thick, heavy-duty jumper cable with power leads on either end. It looks like you could use it to hook up two vehicles to each other, though you expect the power loss would be noticeable. Can also link other electrical systems.", + "volume": "1500 ml", + "weight": "750 g", + "max_charges": 20, + "initial_charges": 20, + "copy-from": "jumper_cable" + }, { "type": "TOOL", "id": "extension_cable", "name": { "str": "extension cord" }, "description": "A long 30-foot (or about 10 m) orange extension cord for connecting appliances. Could be used on any appliance or other household electrical system.", - "copy-from": "jumper_cable", + "to_hit": 1, + "color": "light_blue", + "symbol": "&", + "material": [ "copper", "plastic" ], "volume": "1200 ml", "weight": "800 g", + "category": "tools", + "price": 1, + "price_postapoc": 100, "max_charges": 10, "initial_charges": 10, "use_action": { "type": "link_up", - "menu_text": "Plug in / Unplug", - "is_cable_item": true, - "cable_length": 10, - "targets": [ "no_link", "vehicle_port" ] + "menu_text": "Plug in / Manage connections", + "targets": [ "no_link", "vehicle_port" ], + "can_extend": [ "ELECTRICAL_DEVICES", "extension_cable", "long_extension_cable" ] }, - "flags": [ "CABLE_SPOOL", "SINGLE_USE" ] + "flags": [ "CABLE_SPOOL", "SINGLE_USE" ], + "melee_damage": { "bash": 2 } }, { "type": "TOOL", "id": "long_extension_cable", "name": { "str": "outdoor extension cord" }, "description": "An extra-long 100-foot (or about 30 m) orange extension cord for connecting outdoor appliances. Could be used on any appliance or other household electrical system.", - "copy-from": "jumper_cable", + "copy-from": "extension_cable", "volume": "3750 ml", "weight": "2500 g", "max_charges": 30, "initial_charges": 30, - "use_action": { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "is_cable_item": true, - "cable_length": 30, - "targets": [ "no_link", "vehicle_port" ] - }, "flags": [ "CABLE_SPOOL", "SINGLE_USE" ] }, { "type": "TOOL", "id": "hd_tow_cable", "name": { "str": "heavy-duty tow cable" }, - "description": "An extremely heavy-duty 75-foot (or about 24 m) tow cable made from thick steel wire. If attached to a vehicle, it could be used to pull another vehicle of any weight.", + "description": "An extremely heavy-duty 75-foot (or about 24 m) tow cable made from thick steel wire coated in plastic. If attached to a vehicle, it could be used to pull another vehicle of any weight.", "to_hit": -1, "color": "light_blue", "symbol": "&", @@ -127,46 +110,32 @@ "price_postapoc": 500, "max_charges": 6, "initial_charges": 6, - "use_action": { "type": "link_up", "is_cable_item": true, "cable_length": 6, "targets": [ "no_link", "vehicle_tow" ] }, - "qualities": [ [ "ROPE", 2 ] ], - "flags": [ "CABLE_SPOOL", "TOW_CABLE", "SINGLE_USE" ], - "melee_damage": { "bash": 6 } - }, - { - "type": "TOOL", - "id": "jumper_cable_heavy", - "name": { "str": "heavy-duty cable" }, - "description": "A long, thick, heavy-duty cable with power leads on either end. It looks like you could use it to hook up two vehicles to each other, though you expect the power loss would be noticeable. Can also link other electrical systems.", - "volume": "1500 ml", - "weight": "750 g", - "max_charges": 20, - "initial_charges": 20, "use_action": { "type": "link_up", - "menu_text": "Attach / Detach", - "is_cable_item": true, - "cable_length": 20, - "targets": [ "no_link", "bio_cable", "ups", "solarpack", "vehicle_battery" ] + "menu_text": "Attach / Manage connections", + "efficiency": 0.0, + "targets": [ "no_link", "vehicle_tow" ] }, - "copy-from": "jumper_cable" + "qualities": [ [ "ROPE", 2 ] ], + "flags": [ "CABLE_SPOOL", "TOW_CABLE", "SINGLE_USE" ], + "melee_damage": { "bash": 6 } }, { "type": "TOOL", "id": "jumper_cable_debug", "name": { "str": "shiny debug cable" }, "description": "This is the cable of the gods: 100 meters long, no power loss, light as a feather, and fits in a matchbook. You're sure this wasn't supposed to exist, and the way it shimmers makes you uneasy.", + "copy-from": "jumper_cable", "weight": "1 g", "volume": "1 ml", "max_charges": 100, "initial_charges": 100, "use_action": { "type": "link_up", - "menu_text": "Plug in / Unplug", - "is_cable_item": true, - "cable_length": 100, - "efficiency": 1000, - "targets": [ "no_link", "bio_cable", "ups", "solarpack", "vehicle_port", "vehicle_battery", "vehicle_tow" ] - }, - "copy-from": "jumper_cable" + "menu_text": "Attach / Manage connections", + "efficiency": 1.0, + "targets": [ "no_link", "bio_cable", "ups", "solarpack", "vehicle_port", "vehicle_battery", "vehicle_tow" ], + "can_extend": [ "ELECTRICAL_DEVICES", "jumper_cable_debug" ] + } } ] diff --git a/data/json/items/tool/cooking.json b/data/json/items/tool/cooking.json index 5bd80735aaf6b..c5497334c8db5 100644 --- a/data/json/items/tool/cooking.json +++ b/data/json/items/tool/cooking.json @@ -296,16 +296,7 @@ "flags": [ "WATER_BREAK" ], "charges_per_use": 25, "qualities": [ [ "BOIL", 1 ] ], - "use_action": [ - "HOTPLATE", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "1 kW" - } - ], + "use_action": [ "HOTPLATE", { "type": "link_up", "cable_length": 2, "charge_rate": "1 kW" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -331,7 +322,7 @@ "color": "blue", "ammo": [ "battery" ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK", "ELECTRONIC" ], - "use_action": { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "600 W" }, + "use_action": { "type": "link_up", "cable_length": 2, "charge_rate": "600 W" }, "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -432,7 +423,7 @@ "color": "white", "ammo": [ "battery" ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK", "ELECTRONIC" ], - "use_action": { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "450 W" }, + "use_action": { "type": "link_up", "cable_length": 2, "charge_rate": "450 W" }, "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -511,16 +502,7 @@ "flags": [ "WATER_BREAK" ], "ammo": [ "battery" ], "charges_per_use": 35, - "use_action": [ - "HOTPLATE", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "1 kW" - } - ], + "use_action": [ "HOTPLATE", { "type": "link_up", "cable_length": 3, "charge_rate": "1 kW" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -549,16 +531,7 @@ "flags": [ "WATER_BREAK", "ELECTRONIC" ], "ammo": [ "battery" ], "charges_per_use": 25, - "use_action": [ - "HOTPLATE", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "1500 W" - } - ], + "use_action": [ "HOTPLATE", { "type": "link_up", "cable_length": 3, "charge_rate": "1500 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -585,7 +558,7 @@ "color": "white", "ammo": [ "battery" ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK" ], - "use_action": { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 4, "charge_rate": "55 W" }, + "use_action": { "type": "link_up", "cable_length": 4, "charge_rate": "55 W" }, "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -692,16 +665,7 @@ "flags": [ "WATER_BREAK", "ELECTRONIC" ], "power_draw": "100 W", "qualities": [ [ "CONTAIN", 1 ] ], - "use_action": [ - "MULTICOOKER", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "1000 W" - } - ], + "use_action": [ "MULTICOOKER", { "type": "link_up", "cable_length": 2, "charge_rate": "1000 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -762,7 +726,7 @@ "symbol": ";", "color": "green", "ammo": [ "battery" ], - "use_action": { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "1 kW" }, + "use_action": { "type": "link_up", "cable_length": 2, "charge_rate": "1 kW" }, "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -1020,7 +984,7 @@ "color": "white", "ammo": [ "battery" ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK" ], - "use_action": { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "110 W" }, + "use_action": { "type": "link_up", "cable_length": 2, "charge_rate": "110 W" }, "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -1066,16 +1030,7 @@ "color": "light_blue", "ammo": [ "battery" ], "charges_per_use": 14, - "use_action": [ - "WATER_PURIFIER", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "30 W" - } - ], + "use_action": [ "WATER_PURIFIER", { "type": "link_up", "cable_length": 2, "charge_rate": "30 W" } ], "flags": [ "ALLOWS_REMOTE_USE" ], "pocket_data": [ { @@ -1143,17 +1098,7 @@ "material": [ "steel", "plastic" ], "weight": "11339 g", "volume": "9 L", - "use_action": [ - "HOTPLATE", - "HEAT_FOOD", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "1800 W" - } - ], + "use_action": [ "HOTPLATE", "HEAT_FOOD", { "type": "link_up", "cable_length": 2, "charge_rate": "1800 W" } ], "qualities": [ [ "COOK", 1 ], [ "OVEN", 1 ] ], "flags": [ "WATER_BREAK" ], "charges_per_use": 25, diff --git a/data/json/items/tool/electronics.json b/data/json/items/tool/electronics.json index f9e7239830699..713e86cf0232b 100644 --- a/data/json/items/tool/electronics.json +++ b/data/json/items/tool/electronics.json @@ -165,13 +165,7 @@ "need_charges": 1, "need_charges_msg": "The tablet batteries are dead." }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 4, - "charge_rate": "10 W" - } + { "type": "link_up", "cable_length": 4, "charge_rate": "10 W" } ], "flags": [ "WATCH", "WATER_BREAK", "ELECTRONIC" ], "pocket_data": [ @@ -209,13 +203,7 @@ "msg": "You power down the screen.", "target": "eink_tablet_pc" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 4, - "charge_rate": "10 W" - } + { "type": "link_up", "cable_length": 4, "charge_rate": "10 W" } ], "//": "LIGHT_10 is the bare minimum for reading without penalties", "flags": [ "LIGHT_10", "TRADER_AVOID", "WATCH", "WATER_BREAK", "ELECTRONIC" ] @@ -376,13 +364,7 @@ "need_charges_msg": "The laptop's batteries need more charge.", "type": "transform" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 5, - "charge_rate": "140 W" - } + { "type": "link_up", "cable_length": 5, "charge_rate": "140 W" } ], "flags": [ "WATCH", "WATER_BREAK", "ELECTRONIC" ], "pocket_data": [ @@ -428,13 +410,7 @@ "menu_text": "Turn off", "type": "transform" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 5, - "charge_rate": "140 W" - } + { "type": "link_up", "cable_length": 5, "charge_rate": "140 W" } ], "flags": [ "WATCH", "LIGHT_10", "TRADER_AVOID", "WATER_BREAK", "ELECTRONIC" ] }, @@ -452,10 +428,7 @@ "symbol": ";", "color": "dark_gray", "ammo": [ "battery" ], - "use_action": [ - "MP3", - { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 4, "charge_rate": "5 W" } - ], + "use_action": [ "MP3", { "type": "link_up", "cable_length": 4, "charge_rate": "5 W" } ], "charges_per_use": 1, "flags": [ "WATER_BREAK_ACTIVE", "ELECTRONIC" ], "pocket_data": [ @@ -475,10 +448,7 @@ "description": "This mp3 player is turned on and playing some great tunes, raising your morale steadily while on your person. It runs through batteries quickly; you can turn it off by using it. It also obscures your hearing.", "power_draw": "1 W", "revert_to": "mp3", - "use_action": [ - "MP3_ON", - { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 4, "charge_rate": "5 W" } - ], + "use_action": [ "MP3_ON", { "type": "link_up", "cable_length": 4, "charge_rate": "5 W" } ], "flags": [ "TRADER_AVOID", "WATER_BREAK", "ELECTRONIC" ] }, { @@ -590,8 +560,6 @@ "PORTABLE_GAME", { "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, "//": "Based on the Nintendo Switch's power adapter", "cable_length": 4, "charge_rate": "39 W" @@ -646,13 +614,7 @@ "need_charges_msg": "The smartphone's charge is too low.", "type": "transform" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "20 W" - } + { "type": "link_up", "cable_length": 3, "charge_rate": "20 W" } ], "flags": [ "WATCH", "ALARMCLOCK", "USE_UPS", "NO_UNLOAD", "NO_RELOAD", "WATER_BREAK", "CALORIES_INTAKE", "ELECTRONIC" ], "pocket_data": [ @@ -680,13 +642,7 @@ "EINKTABLETPC", "EBOOKSAVE", "EBOOKREAD", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "20 W" - } + { "type": "link_up", "cable_length": 3, "charge_rate": "20 W" } ], "extend": { "flags": [ "TRADER_AVOID" ] } }, @@ -716,13 +672,7 @@ "menu_text": "Turn off flashlight", "type": "transform" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "20 W" - } + { "type": "link_up", "cable_length": 3, "charge_rate": "20 W" } ], "extend": { "flags": [ "LIGHT_20", "CHARGEDIM", "TRADER_AVOID" ] } }, diff --git a/data/json/items/tool/lighting.json b/data/json/items/tool/lighting.json index a2d6f6e576b82..3af4c7ed15a66 100644 --- a/data/json/items/tool/lighting.json +++ b/data/json/items/tool/lighting.json @@ -156,13 +156,7 @@ "need_charges": 1, "need_charges_msg": "The lantern has no batteries." }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "20 W" - } + { "type": "link_up", "cable_length": 2, "charge_rate": "20 W" } ], "flags": [ "RADIO_MODABLE", "RADIO_INVOKE_PROC", "ALLOWS_REMOTE_USE", "WATER_BREAK" ], "pocket_data": [ @@ -184,13 +178,7 @@ "revert_to": "electric_lantern", "use_action": [ { "menu_text": "Turn off", "type": "transform", "target": "electric_lantern", "msg": "You turn the lamp off." }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "20 W" - } + { "type": "link_up", "cable_length": 2, "charge_rate": "20 W" } ], "flags": [ "RADIO_MODABLE", "RADIO_INVOKE_PROC", "LIGHT_15", "TRADER_AVOID", "ALLOWS_REMOTE_USE" ] }, diff --git a/data/json/items/tool/misc.json b/data/json/items/tool/misc.json index 017db90f7fe21..1f79a04071896 100644 --- a/data/json/items/tool/misc.json +++ b/data/json/items/tool/misc.json @@ -371,13 +371,7 @@ "menu_text": "Turn on", "type": "transform" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "1 kW" - } + { "type": "link_up", "cable_length": 2, "charge_rate": "1 kW" } ], "pocket_data": [ { @@ -407,13 +401,7 @@ "menu_text": "Turn off", "type": "transform" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "1 kW" - } + { "type": "link_up", "cable_length": 2, "charge_rate": "1 kW" } ] }, { @@ -589,13 +577,7 @@ "menu_text": "Turn on", "type": "transform" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "500 W" - } + { "type": "link_up", "cable_length": 2, "charge_rate": "500 W" } ], "pocket_data": [ { @@ -625,13 +607,7 @@ "menu_text": "Turn off", "type": "transform" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 2, - "charge_rate": "500 W" - } + { "type": "link_up", "cable_length": 2, "charge_rate": "500 W" } ] }, { diff --git a/data/json/items/tool/radio_tools.json b/data/json/items/tool/radio_tools.json index 1165299c5ff42..ef19b85574e84 100644 --- a/data/json/items/tool/radio_tools.json +++ b/data/json/items/tool/radio_tools.json @@ -100,10 +100,7 @@ "ammo": [ "battery" ], "flags": [ "WATER_BREAK", "ELECTRONIC" ], "charges_per_use": 1, - "use_action": [ - "RADIO_OFF", - { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 3, "charge_rate": "5 W" } - ], + "use_action": [ "RADIO_OFF", { "type": "link_up", "cable_length": 3, "charge_rate": "5 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -145,7 +142,7 @@ "ammo": [ "battery" ], "charges_per_use": 1, "flags": [ "TWO_WAY_RADIO", "WATER_BREAK", "ELECTRONIC" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 3, "charge_rate": "5 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 3, "charge_rate": "5 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", diff --git a/data/json/items/tool/science.json b/data/json/items/tool/science.json index 2aa2b7e8c6d38..32b4463cee1bc 100644 --- a/data/json/items/tool/science.json +++ b/data/json/items/tool/science.json @@ -55,16 +55,7 @@ "sub": "hotplate", "charges_per_use": 1, "qualities": [ [ "DISTILL", 1 ], [ "CHEM", 3 ], [ "BOIL", 1 ] ], - "use_action": [ - "HOTPLATE", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 7, - "charge_rate": "1 kW" - } - ], + "use_action": [ "HOTPLATE", { "type": "link_up", "cable_length": 7, "charge_rate": "1 kW" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -106,7 +97,7 @@ "symbol": ";", "ammo": [ "battery" ], "flags": [ "WATER_BREAK" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "300 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "300 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -129,7 +120,7 @@ "color": "light_gray", "ammo": [ "battery" ], "flags": [ "WATER_BREAK" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "150 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "150 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -264,7 +255,7 @@ "symbol": "E", "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK", "ELECTRONIC" ], "color": "dark_gray", - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "1200 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "1200 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -291,7 +282,7 @@ "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK" ], "ammo": [ "battery" ], "qualities": [ [ "EXTRACT", 2 ] ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "600 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "600 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -331,7 +322,7 @@ "flags": [ "ALLOWS_REMOTE_USE", "TRADER_AVOID", "WATER_BREAK", "ELECTRONIC" ], "ammo": [ "battery" ], "qualities": [ [ "EXTRACT", 1 ] ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "1800 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "1800 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -444,7 +435,7 @@ "material": [ "iron" ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK" ], "ammo": [ "battery" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "250 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "250 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -500,7 +491,7 @@ "color": "light_gray", "ammo": [ "battery" ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "150 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "150 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", "item_restriction": [ "storage_battery", "large_storage_battery" ] } ], "melee_damage": { "bash": 20 } }, @@ -520,7 +511,7 @@ "symbol": ";", "color": "light_gray", "ammo": [ "battery" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "800 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "800 W" } ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK", "ELECTRONIC" ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", "item_restriction": [ "large_storage_battery" ] } ], "melee_damage": { "bash": 20 } @@ -541,7 +532,7 @@ "symbol": ";", "color": "light_gray", "ammo": [ "battery" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "350 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "350 W" } ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK", "ELECTRONIC" ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", "item_restriction": [ "large_storage_battery" ] } ], "melee_damage": { "bash": 20 } @@ -562,7 +553,7 @@ "symbol": ";", "color": "light_gray", "ammo": [ "battery" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "1200 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "1200 W" } ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK", "ELECTRONIC" ], "pocket_data": [ { @@ -1164,7 +1155,7 @@ "color": "white", "charges_per_use": 5, "charged_qualities": [ [ "CONCENTRATE", 1 ] ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "400 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "400 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -1229,7 +1220,7 @@ "flags": [ "WATER_BREAK", "ELECTRONIC" ], "charges_per_use": 5, "charged_qualities": [ [ "CONCENTRATE", 1 ] ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "400 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "400 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", diff --git a/data/json/items/tool/workshop.json b/data/json/items/tool/workshop.json index f5eb2b52ba36c..adc5ceb0892c9 100644 --- a/data/json/items/tool/workshop.json +++ b/data/json/items/tool/workshop.json @@ -76,7 +76,7 @@ "charges_per_use": 1, "power_draw": "800 W", "flags": [ "NONCONDUCTIVE", "WATER_BREAK" ], - "use_action": { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 30, "charge_rate": "800 W" }, + "use_action": { "type": "link_up", "cable_length": 30, "charge_rate": "800 W" }, "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -168,7 +168,7 @@ "color": "white", "ammo": [ "battery" ], "flags": [ "ALLOWS_REMOTE_USE" ], - "use_action": { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "300 W" }, + "use_action": { "type": "link_up", "cable_length": 2, "charge_rate": "300 W" }, "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -195,7 +195,7 @@ "color": "white", "ammo": [ "battery" ], "flags": [ "ALLOWS_REMOTE_USE" ], - "use_action": { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "600 W" }, + "use_action": { "type": "link_up", "cable_length": 2, "charge_rate": "600 W" }, "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -276,16 +276,7 @@ "ammo": [ "battery" ], "charges_per_use": 20, "qualities": [ [ "CONTAIN", 1 ] ], - "use_action": [ - "HOTPLATE", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 15, - "charge_rate": "1500 W" - } - ], + "use_action": [ "HOTPLATE", { "type": "link_up", "cable_length": 15, "charge_rate": "1500 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -391,16 +382,7 @@ "ammo": [ "battery" ], "pocket_data": [ { "pocket_type": "MAGAZINE", "rigid": true, "ammo_restriction": { "battery": 7920 } } ], "charges_per_use": 3960, - "use_action": [ - "JACKHAMMER", - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 33, - "charge_rate": "2200 W" - } - ], + "use_action": [ "JACKHAMMER", { "type": "link_up", "cable_length": 33, "charge_rate": "2200 W" } ], "flags": [ "DIG_TOOL", "POWERED", "USE_UPS", "NO_UNLOAD", "NO_RELOAD", "WATER_BREAK", "ELECTRONIC" ], "melee_damage": { "bash": 14, "stab": 6 } }, @@ -599,7 +581,7 @@ "color": "dark_gray", "ammo": [ "battery" ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK", "ELECTRONIC" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "1500 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "1500 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -653,13 +635,7 @@ "cost_scaling": 0.1, "move_cost": 1500 }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "70 W" - } + { "type": "link_up", "cable_length": 3, "charge_rate": "70 W" } ], "flags": [ "ALLOWS_REMOTE_USE" ], "pocket_data": [ @@ -939,7 +915,7 @@ "charged_qualities": [ [ "GRIND", 2 ] ], "ammo": [ "battery" ], "flags": [ "TRADER_AVOID", "WATER_BREAK" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 5, "charge_rate": "1600 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 5, "charge_rate": "1600 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -986,7 +962,7 @@ "charges_per_use": 1, "power_draw": "800 W", "flags": [ "NONCONDUCTIVE", "WATER_BREAK" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "800 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "800 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", @@ -1144,13 +1120,7 @@ "cost_scaling": 0.1, "move_cost": 1500 }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "70 W" - } + { "type": "link_up", "cable_length": 3, "charge_rate": "70 W" } ], "flags": [ "ALLOWS_REMOTE_USE" ], "pocket_data": [ @@ -1187,13 +1157,7 @@ "cost_scaling": 0.1, "move_cost": 1500 }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "70 W" - } + { "type": "link_up", "cable_length": 3, "charge_rate": "70 W" } ], "flags": [ "SPEAR", "BELT_CLIP", "ALLOWS_REMOTE_USE", "WATER_BREAK" ], "pocket_data": [ @@ -1307,13 +1271,7 @@ "cost_scaling": 0.1, "move_cost": 500 }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 7, - "charge_rate": "3000 W" - } + { "type": "link_up", "cable_length": 7, "charge_rate": "3000 W" } ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK" ], "pocket_data": [ @@ -1385,13 +1343,7 @@ "cost_scaling": 0.1, "move_cost": 1000 }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 7, - "charge_rate": "1500 W" - } + { "type": "link_up", "cable_length": 7, "charge_rate": "1500 W" } ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK" ], "pocket_data": [ @@ -1465,13 +1417,7 @@ "cost_scaling": 0.1, "move_cost": 500 }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 7, - "charge_rate": "3000 W" - } + { "type": "link_up", "cable_length": 7, "charge_rate": "3000 W" } ], "flags": [ "ALLOWS_REMOTE_USE", "WATER_BREAK" ], "pocket_data": [ @@ -1653,7 +1599,7 @@ "symbol": ";", "color": "light_gray", "ammo": [ "battery" ], - "use_action": [ { "type": "link_up", "menu_text": "Plug in / Unplug", "ammo_scale": 0, "cable_length": 2, "charge_rate": "1500 W" } ], + "use_action": [ { "type": "link_up", "cable_length": 2, "charge_rate": "1500 W" } ], "pocket_data": [ { "pocket_type": "MAGAZINE_WELL", diff --git a/data/json/items/tool_armor.json b/data/json/items/tool_armor.json index d4d39a6fa1ec2..08603203b46da 100644 --- a/data/json/items/tool_armor.json +++ b/data/json/items/tool_armor.json @@ -3502,13 +3502,7 @@ "need_charges": 1, "need_charges_msg": "The blanket's batteries are dead." }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "110 W" - } + { "type": "link_up", "cable_length": 3, "charge_rate": "110 W" } ], "flags": [ "OVERSIZE", "OUTER", "ALLOWS_NATURAL_ATTACKS", "WATER_BREAK" ], "pocket_data": [ @@ -3543,13 +3537,7 @@ "msg": "You turn the blanket's heating elements off.", "target": "electric_blanket" }, - { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "ammo_scale": 0, - "cable_length": 3, - "charge_rate": "110 W" - } + { "type": "link_up", "cable_length": 3, "charge_rate": "110 W" } ] }, { diff --git a/data/json/obsoletion/migration_items.json b/data/json/obsoletion/migration_items.json index 9a5718f3199aa..752f7b20f6266 100644 --- a/data/json/obsoletion/migration_items.json +++ b/data/json/obsoletion/migration_items.json @@ -167,6 +167,12 @@ "replace": "UPS_ON", "flags": [ "IS_UPS" ] }, + { + "id": "generic_device_cable", + "type": "MIGRATION", + "replace": "oxygen", + "reset_item_vars": true + }, { "id": "rebreather_filter", "type": "MIGRATION", diff --git a/data/json/player_activities.json b/data/json/player_activities.json index 6ab3ca975c657..c16063fabb7a8 100644 --- a/data/json/player_activities.json +++ b/data/json/player_activities.json @@ -913,6 +913,16 @@ "based_on": "neither", "no_resume": true }, + { + "id": "ACT_INVOKE_ITEM", + "type": "activity_type", + "activity_level": "NO_EXERCISE", + "verb": "using item", + "suspendable": false, + "rooted": true, + "based_on": "neither", + "no_resume": true + }, { "id": "ACT_TREE_COMMUNION", "type": "activity_type", diff --git a/data/json/vehicleparts/battery.json b/data/json/vehicleparts/battery.json index 284b8a8efd17b..01370f3d4de8e 100644 --- a/data/json/vehicleparts/battery.json +++ b/data/json/vehicleparts/battery.json @@ -22,6 +22,7 @@ "repair": { "skills": [ [ "mechanics", 3 ] ], "time": "600 s", "using": [ [ "soldering_standard", 5 ] ] } }, "damage_reduction": { "bash": 10 }, + "flags": [ "BATTERY" ], "variants": [ { "symbols": "o", "symbols_broken": "#" } ] }, { @@ -77,7 +78,7 @@ "removal": { "skills": [ [ "mechanics", 0 ] ], "time": "10 s", "using": [ ] } }, "description": "A battery for storing electrical power, and discharging it to power electrical devices built into the vehicle. This one is mounted on a quick release framework to allow it to be easily swapped, though it still weighs so much that a lifting tool of some kind is necessary for most people.", - "flags": [ "NEEDS_BATTERY_MOUNT" ] + "flags": [ "BATTERY", "NEEDS_BATTERY_MOUNT" ] }, { "id": "medium_storage_battery", @@ -132,6 +133,7 @@ "removal": { "skills": [ [ "mechanics", 2 ] ], "time": "5 m", "using": "vehicle_weld_removal" } }, "damage_reduction": { "bash": 10 }, + "flags": [ "BATTERY" ], "variants": [ { "symbols": "O", "symbols_broken": "#" } ] }, { @@ -145,7 +147,7 @@ "removal": { "skills": [ [ "mechanics", 0 ] ], "time": "10 s", "using": [ ] } }, "description": "A battery for storing electrical power, and discharging it to power electrical devices built into the vehicle. This one is mounted on a quick release framework to allow it to be easily swapped, though it still weighs so much that a lifting tool of some kind is necessary for some people.", - "flags": [ "NEEDS_BATTERY_MOUNT" ] + "flags": [ "BATTERY", "NEEDS_BATTERY_MOUNT" ] }, { "id": "car_light_minus_battery_cell", @@ -161,7 +163,7 @@ "durability": 10, "folded_volume": "10 ml", "breaks_into": [ { "item": "e_scrap", "prob": 10 } ], - "flags": [ "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] + "flags": [ "BATTERY", "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] }, { "id": "car_light_battery_cell", @@ -177,7 +179,7 @@ "durability": 20, "folded_volume": "25ml", "breaks_into": [ { "item": "e_scrap", "prob": 10 } ], - "flags": [ "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] + "flags": [ "BATTERY", "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] }, { "id": "car_light_plus_battery_cell", @@ -193,7 +195,7 @@ "durability": 20, "folded_volume": "35ml", "breaks_into": [ { "item": "e_scrap", "prob": 10 } ], - "flags": [ "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] + "flags": [ "BATTERY", "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] }, { "id": "car_medium_battery_cell", @@ -209,7 +211,7 @@ "durability": 30, "folded_volume": "450 ml", "breaks_into": [ { "item": "light_battery_cell", "count": [ 0, 3 ] }, { "item": "scrap", "count": [ 1, 4 ] } ], - "flags": [ "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] + "flags": [ "BATTERY", "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] }, { "id": "car_medium_plus_battery_cell", @@ -225,7 +227,7 @@ "durability": 30, "folded_volume": "525 ml", "breaks_into": [ { "item": "light_plus_battery_cell", "count": [ 0, 3 ] }, { "item": "scrap", "count": [ 1, 4 ] } ], - "flags": [ "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] + "flags": [ "BATTERY", "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] }, { "id": "car_heavy_battery_cell", @@ -241,7 +243,7 @@ "durability": 40, "folded_volume": "1225 ml", "breaks_into": [ { "item": "medium_battery_cell", "count": [ 0, 1 ] }, { "item": "scrap", "count": [ 1, 4 ] } ], - "flags": [ "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] + "flags": [ "BATTERY", "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] }, { "id": "car_heavy_plus_battery_cell", @@ -257,6 +259,6 @@ "durability": 40, "folded_volume": "1500 ml", "breaks_into": [ { "item": "medium_battery_cell", "count": [ 0, 1 ] }, { "item": "scrap", "count": [ 1, 4 ] } ], - "flags": [ "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] + "flags": [ "BATTERY", "NEEDS_HANDHELD_BATTERY_MOUNT", "NO_REPAIR" ] } ] diff --git a/data/json/vehicleparts/vehicle_parts.json b/data/json/vehicleparts/vehicle_parts.json index 155036b62fc36..a1baf3b98cb83 100644 --- a/data/json/vehicleparts/vehicle_parts.json +++ b/data/json/vehicleparts/vehicle_parts.json @@ -2381,7 +2381,7 @@ "description": "Thick copper cable with leads on either end. Attach one end to one vehicle and the other to another, and you can transfer electrical power between the two.", "item": "jumper_cable", "requirements": { "removal": { "time": "5 s" } }, - "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "POWER_TRANSFER" ], "breaks_into": [ { "item": "cable", "charges": [ 1, 10 ] }, { "item": "plastic_chunk", "count": [ 1, 2 ] } ], "variants": [ { "symbols": "{", "symbols_broken": "*" } ] }, @@ -2399,7 +2399,7 @@ "description": "A long orange extension cord for connecting appliances. Currently plugged in.", "item": "extension_cable", "requirements": { "removal": { "time": "5 s" } }, - "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "POWER_TRANSFER" ], "breaks_into": [ { "item": "cable", "charges": [ 1, 10 ] }, { "item": "plastic_chunk", "count": [ 1, 2 ] } ], "variants": [ { "symbols": "{", "symbols_broken": "*" } ] }, @@ -2417,14 +2417,14 @@ "description": "An extra long 30m orange extension cord for connecting outdoor appliances. Currently plugged in.", "item": "long_extension_cable", "requirements": { "removal": { "time": "5 s" } }, - "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "POWER_TRANSFER" ], "breaks_into": [ { "item": "cable", "charges": [ 1, 10 ] }, { "item": "plastic_chunk", "count": [ 1, 2 ] } ], "variants": [ { "symbols": "{", "symbols_broken": "*" } ] }, { "type": "vehicle_part", "id": "jumper_cable_heavy", - "name": { "str": "heavy-duty cable" }, + "name": { "str": "heavy-duty jumper cable" }, "categories": [ "other" ], "color": "yellow", "broken_color": "dark_gray", @@ -2435,7 +2435,7 @@ "//": "Epower for POWER_TRANSFER stuff is how much percentage-wise loss there is in transmission", "item": "jumper_cable_heavy", "requirements": { "removal": { "time": "5 s" } }, - "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "POWER_TRANSFER" ], "breaks_into": [ { "item": "wire", "count": [ 4, 8 ] }, { "item": "plastic_chunk", "count": [ 4, 8 ] } ], "variants": [ { "symbols": "{", "symbols_broken": "*" } ] }, @@ -2467,7 +2467,7 @@ "durability": 120, "item": "jumper_cable_debug", "requirements": { "removal": { "time": "5 s" } }, - "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "POWER_TRANSFER" ], "breaks_into": [ { "item": "jumper_cable_debug" } ], "variants": [ { "symbols": "{", "symbols_broken": "*" } ] }, diff --git a/data/json/vehicleparts/vp_flags.json b/data/json/vehicleparts/vp_flags.json index ca0e5492585f6..dbe7201ba8455 100644 --- a/data/json/vehicleparts/vp_flags.json +++ b/data/json/vehicleparts/vp_flags.json @@ -273,6 +273,10 @@ "id": "AUTOPILOT", "type": "json_flag" }, + { + "id": "BATTERY", + "type": "json_flag" + }, { "id": "BATTERY_MOUNT", "type": "json_flag" diff --git a/data/mods/TEST_DATA/appliance.json b/data/mods/TEST_DATA/appliance.json index 8e961a10432a0..3e38cc7d4fd70 100644 --- a/data/mods/TEST_DATA/appliance.json +++ b/data/mods/TEST_DATA/appliance.json @@ -55,14 +55,8 @@ "price_postapoc": 100, "max_charges": 3, "initial_charges": 3, - "use_action": { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "is_cable_item": true, - "cable_length": 3, - "targets": [ "no_link", "vehicle_port", "vehicle_battery" ] - }, - "flags": [ "CABLE_SPOOL", "AUTO_DELETE_CABLE" ], + "use_action": { "type": "link_up", "targets": [ "no_link", "vehicle_port", "vehicle_battery" ] }, + "flags": [ "CABLE_SPOOL", "NO_DROP" ], "melee_damage": { "bash": 2 } }, { @@ -76,9 +70,9 @@ "epower": "0 W", "//": "Epower for POWER_TRANSFER stuff is how much percentage-wise loss there is in transmission", "durability": 120, - "description": "A power cord sticking out of an appliance. You need to plug it in a powered grid for the appliance to work properly.", + "description": "A power cord sticking out of an appliance. You need to plug it into a powered grid for the appliance to work properly.", "item": "test_power_cord", - "flags": [ "NO_INSTALL_HIDDEN", "NO_UNINSTALL", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "flags": [ "NO_INSTALL_HIDDEN", "NO_UNINSTALL", "UNMOUNT_ON_DAMAGE", "POWER_TRANSFER" ], "variants": [ { "symbols": "{", "symbols_broken": "s" } ] }, { @@ -89,13 +83,7 @@ "copy-from": "jumper_cable", "max_charges": 10, "initial_charges": 10, - "use_action": { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "is_cable_item": true, - "cable_length": 10, - "targets": [ "no_link", "vehicle_port", "vehicle_battery" ] - } + "use_action": { "type": "link_up", "targets": [ "no_link", "vehicle_port", "vehicle_battery" ] } }, { "type": "vehicle_part", @@ -111,7 +99,7 @@ "description": "A long orange extension cord for connecting appliances. Currently plugged in.", "item": "test_extension_cable", "requirements": { "removal": { "time": "5 s" } }, - "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "POWER_TRANSFER" ], "breaks_into": [ { "item": "cable", "charges": [ 1, 10 ] }, { "item": "plastic_chunk", "count": [ 1, 2 ] } ], "variants": [ { "symbols": "{", "symbols_broken": "*" } ] }, @@ -131,14 +119,8 @@ "price_postapoc": 100, "max_charges": 3, "initial_charges": 3, - "use_action": { - "type": "link_up", - "menu_text": "Plug in / Unplug", - "is_cable_item": true, - "cable_length": 3, - "targets": [ "no_link", "vehicle_port", "vehicle_battery" ] - }, - "flags": [ "CABLE_SPOOL", "AUTO_DELETE_CABLE", "SINGLE_USE" ], + "use_action": { "type": "link_up", "targets": [ "no_link", "vehicle_port", "vehicle_battery" ] }, + "flags": [ "CABLE_SPOOL", "NO_DROP", "SINGLE_USE" ], "melee_damage": { "bash": 2 } }, { @@ -155,7 +137,7 @@ "description": "A long orange extension cord for connecting appliances. Currently plugged in.", "item": "test_power_cord_25_loss", "requirements": { "removal": { "time": "5 s" } }, - "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "UNMOUNT_ON_MOVE", "POWER_TRANSFER" ], + "flags": [ "NO_INSTALL_HIDDEN", "UNMOUNT_ON_DAMAGE", "POWER_TRANSFER" ], "breaks_into": [ { "item": "cable", "charges": [ 1, 10 ] }, { "item": "plastic_chunk", "count": [ 1, 2 ] } ], "variants": [ { "symbols": "{", "symbols_broken": "*" } ] } diff --git a/data/raw/keybindings.json b/data/raw/keybindings.json index 8a3f5815852ed..60ffff140fb10 100644 --- a/data/raw/keybindings.json +++ b/data/raw/keybindings.json @@ -1659,13 +1659,6 @@ "name": "Plug in appliance", "bindings": [ { "input_method": "keyboard_any", "key": "g" } ] }, - { - "type": "keybinding", - "id": "UNPLUG", - "category": "APP_INTERACT", - "name": "Unplug power connections", - "bindings": [ { "input_method": "keyboard_any", "key": "u" } ] - }, { "type": "keybinding", "id": "INSTALL", diff --git a/data/raw/keybindings/vehicle.json b/data/raw/keybindings/vehicle.json index 4550bfc1a961a..816a91596acef 100644 --- a/data/raw/keybindings/vehicle.json +++ b/data/raw/keybindings/vehicle.json @@ -137,7 +137,7 @@ "type": "keybinding", "category": "VEHICLE", "name": "Fill container with water from faucet", - "bindings": [ { "input_method": "keyboard_any", "key": "c" } ] + "bindings": [ { "input_method": "keyboard_char", "key": "F" }, { "input_method": "keyboard_code", "key": "f", "mod": [ "shift" ] } ] }, { "id": "FAUCET_DRINK", @@ -405,6 +405,13 @@ "name": "Set turret targeting modes", "bindings": [ { "input_method": "keyboard_any", "key": "t" } ] }, + { + "id": "DISCONNECT_CABLES", + "type": "keybinding", + "category": "VEHICLE", + "name": "Disconnect attached cables", + "bindings": [ { "input_method": "keyboard_any", "key": "c" } ] + }, { "id": "ARCADE", "type": "keybinding", diff --git a/doc/JSON_FLAGS.md b/doc/JSON_FLAGS.md index c305cd3782f88..46f6753d37738 100644 --- a/doc/JSON_FLAGS.md +++ b/doc/JSON_FLAGS.md @@ -1344,7 +1344,6 @@ Melee flags are fully compatible with tool flags, and vice versa. - ```ACT_ON_RANGED_HIT``` The item should activate when thrown or fired, then immediately get processed if it spawns on the ground. - ```ALLOWS_REMOTE_USE``` This item can be activated or reloaded from adjacent tile without picking it up. -- ```AUTO_DELETE_CABLE``` This cable is automatically created and deleted by their appliance or device and the player should never be able to directly interact with it. - ```BELT_CLIP``` The item can be clipped or hooked on to a belt loop of the appropriate size (belt loops are limited by their max_volume and max_weight properties) - ```BOMB``` It can be a remote controlled bomb. - ```CABLE_SPOOL``` This item is a cable spool and must be processed as such. It should usually have a "link_up" iuse_action that describes what it can be connected to and how. diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index 026062599f106..d59333efe1f7b 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -4035,13 +4035,17 @@ The contents of use_action fields can either be a string indicating a built-in f }, "use_action": { "type": "link_up", // Connect item to a vehicle or appliance, such as plugging a chargeable device into a power source. - "cable_type": "generic_device_cable" // The item type of the cable created with this action ( Optional, defaults to "generic_device_cable" ). - "cable_length": 5 // Maximum length of the cable ( Optional, defaults to 2 ). - "charge_rate": "60 W" // Charge rate in watts. A positive value will charge the device's chargeable batteries at the expense of the connected power grid. + "cable_length": 4 // Maximum length of the cable ( Optional, defaults to the item type's maximum charges ). + // If extended by other cables, will use the sum of all cables' lengths. + "charge_rate": "60 W" // The charge rate of the plugged-in device's batteries in watts. ( Optional, defaults to "0 W" ) + // A positive value will charge the device's chargeable batteries at the expense of the connected power grid. // A negative value will charge the connected electrical grid's batteries at the expense of the device's. - // A value of 0 won't charge the device's batteries, but will still let the device operate off of the connected power grid ( Optional, defaults to "0 W" ). - "efficiency": 7 // one_in(this) chance to fail adding 1 charge every charge interval ( Optional, defaults to 7, which is around 85% efficiency ). - "menu_text": // Text displayed in the activation screen ( Optional, defaults to "Connect / Disconnect" ). + // A value of 0 won't charge the device's batteries, but will still let the device operate off of the connected power grid. + "efficiency": 0.85f // (this) out of 1.0 chance to successfully add 1 charge every charge interval ( Optional, defaults to 0.85f, AKA 85% efficiency ). + // A value less than 0.001 means the cable won't transfer any electricity at all. + // If extended by other cables, will use the product of all cable's efficiencies multiplied together. + "menu_text": // Text displayed in the activation screen ( Optional, defaults to Plug in / Manage cables" ). + "move_cost": // Move cost of attaching the cable ( Optional, defaults to 5 ). "targets": [ // Array of link_states that are valid connection points of the cable ( Optional, defaults to only allowing disconnection ). "no_link", // Must be included to allow letting the player manually disconnect the cable. "vehicle_port", // Can connect to a vehicle's cable ports / electrical controls or an appliance. @@ -4049,7 +4053,13 @@ The contents of use_action fields can either be a string indicating a built-in f "vehicle_tow", // Can be used as a tow cable between two vehicles. "bio_cable", // Can connect to a cable system bionic. "ups", // Can link to a UPS. - "solarpack", // Can link to a worn solar pack. + "solarpack" // Can link to a worn solar pack. + ], + "can_extend": [ // Array of cable items that can be extended by this one ( Optional, defaults to none ). + "extension_cable", + "long_extension_cable", + "ELECTRICAL_DEVICES" // "ELECTRICAL_DEVICES" is a special keyword that lets this cable extend all electrical devices that have link_up actions. + ] }, "use_action" : { "type" : "delayed_transform", // Like transform, but it will only transform when the item has a certain age diff --git a/src/activity_actor.cpp b/src/activity_actor.cpp index 840022e50547c..d05b37c14765e 100644 --- a/src/activity_actor.cpp +++ b/src/activity_actor.cpp @@ -117,6 +117,7 @@ static const activity_id ACT_HAIRCUT( "ACT_HAIRCUT" ); static const activity_id ACT_HARVEST( "ACT_HARVEST" ); static const activity_id ACT_HOTWIRE_CAR( "ACT_HOTWIRE_CAR" ); static const activity_id ACT_INSERT_ITEM( "ACT_INSERT_ITEM" ); +static const activity_id ACT_INVOKE_ITEM( "ACT_INVOKE_ITEM" ); static const activity_id ACT_LOCKPICK( "ACT_LOCKPICK" ); static const activity_id ACT_LONGSALVAGE( "ACT_LONGSALVAGE" ); static const activity_id ACT_MEDITATE( "ACT_MEDITATE" ); @@ -5042,18 +5043,14 @@ void reel_cable_activity_actor::start( player_activity &act, Character & ) void reel_cable_activity_actor::finish( player_activity &act, Character &who ) { - cable->active = false; - cable->charges = cable->link->max_length; - cable->link.reset(); - if( parent_item ) { - parent_item->contents_linked = false; - who.add_msg_if_player( m_info, string_format( _( "You gather the cable up with the %s." ), - parent_item->label( 1 ) ) ); - } else { - who.add_msg_if_player( m_info, string_format( _( "You reel in the %s and wind it up." ), - cable->label( 1 ) ) ); - } - if( cable->has_flag( flag_AUTO_DELETE_CABLE ) ) { + cable->link->length = 0; + cable->link->s_state = link_state::no_link; + cable->link->t_state = link_state::no_link; + cable->reset_link( &who, -2 ); + who.add_msg_if_player( m_info, + string_format( cable->has_flag( flag_CABLE_SPOOL ) ? _( "You reel in the %s and wind it up." ) : + _( "You reel in the %s's cable and wind it up." ), cable->type_name() ) ); + if( cable->has_flag( flag_NO_DROP ) ) { cable.remove_item(); } act.set_to_null(); @@ -5064,17 +5061,15 @@ void reel_cable_activity_actor::serialize( JsonOut &jsout ) const jsout.start_object(); jsout.member( "moves_total", moves_total ); jsout.member( "cable", cable ); - jsout.member( "parent_item", parent_item ); jsout.end_object(); } std::unique_ptr reel_cable_activity_actor::deserialize( JsonValue &jsin ) { - reel_cable_activity_actor actor( 0, {}, {} ); + reel_cable_activity_actor actor( 0, {} ); JsonObject data = jsin.get_object(); data.read( "moves_total", actor.moves_total ); data.read( "cable", actor.cable ); - data.read( "parent_item", actor.parent_item ); return actor.clone(); } @@ -5670,6 +5665,40 @@ std::unique_ptr wear_activity_actor::deserialize( JsonValue &jsi return actor.clone(); } +void invoke_item_activity_actor::do_turn( player_activity &, Character &who ) +{ + if( method.empty() ) { + who.cancel_activity(); + who.invoke_item( item.get_item() ); + return; + } + std::string _method = method; + who.cancel_activity(); + who.invoke_item( item.get_item(), _method ); +} + +void invoke_item_activity_actor::serialize( JsonOut &jsout ) const +{ + jsout.start_object(); + + jsout.member( "item", item ); + jsout.member( "method", method ); + + jsout.end_object(); +} + +std::unique_ptr invoke_item_activity_actor::deserialize( JsonValue &jsin ) +{ + invoke_item_activity_actor actor( {}, {} ); + + JsonObject data = jsin.get_object(); + + data.read( "item", actor.item ); + data.read( "method", actor.method ); + + return actor.clone(); +} + void pickup_menu_activity_actor::do_turn( player_activity &, Character &who ) { std::optional p( where ); @@ -7187,6 +7216,7 @@ deserialize_functions = { { ACT_HARVEST, &harvest_activity_actor::deserialize}, { ACT_HOTWIRE_CAR, &hotwire_car_activity_actor::deserialize }, { ACT_INSERT_ITEM, &insert_item_activity_actor::deserialize }, + { ACT_INVOKE_ITEM, &invoke_item_activity_actor::deserialize }, { ACT_LOCKPICK, &lockpick_activity_actor::deserialize }, { ACT_LONGSALVAGE, &longsalvage_activity_actor::deserialize }, { ACT_MEDITATE, &meditate_activity_actor::deserialize }, diff --git a/src/activity_actor_definitions.h b/src/activity_actor_definitions.h index d6d1f7204bb3f..975ccc4d0c76d 100644 --- a/src/activity_actor_definitions.h +++ b/src/activity_actor_definitions.h @@ -1480,11 +1480,9 @@ class reel_cable_activity_actor : public activity_actor private: int moves_total; item_location cable; - item_location parent_item; public: - reel_cable_activity_actor( int moves_total, const item_location &cable, - const item_location &parent_item ) : - moves_total( moves_total ), cable( cable ), parent_item( parent_item ) {} + reel_cable_activity_actor( int moves_total, const item_location &cable ) : + moves_total( moves_total ), cable( cable ) {} activity_id get_type() const override { return activity_id( "ACT_REEL_CABLE" ); } @@ -1493,7 +1491,7 @@ class reel_cable_activity_actor : public activity_actor const Character &/*who*/ ) const override { const reel_cable_activity_actor &actor = static_cast ( other ); - return actor.cable == cable && actor.parent_item == parent_item; + return actor.cable == cable; } void start( player_activity &act, Character & ) override; @@ -1699,6 +1697,32 @@ class wield_activity_actor : public activity_actor contents_change_handler handler; }; +class invoke_item_activity_actor : public activity_actor +{ + public: + invoke_item_activity_actor( item_location item, std::string method ) : + item( std::move( item ) ), + method( std::move( method ) ) {}; + activity_id get_type() const override { + return activity_id( "ACT_INVOKE_ITEM" ); + } + + void start( player_activity &, Character & ) override {}; + void do_turn( player_activity &, Character &who ) override; + void finish( player_activity &, Character & ) override {}; + + std::unique_ptr clone() const override { + return std::make_unique( *this ); + } + + void serialize( JsonOut & ) const override; + static std::unique_ptr deserialize( JsonValue & ); + + private: + item_location item; + std::string method; +}; + class pickup_menu_activity_actor : public activity_actor { public: diff --git a/src/activity_handlers.cpp b/src/activity_handlers.cpp index ca28eeff68831..6d200accc4275 100644 --- a/src/activity_handlers.cpp +++ b/src/activity_handlers.cpp @@ -2418,7 +2418,7 @@ void repair_item_finish( player_activity *act, Character *you, bool no_menu ) fix.tname() ); ammotype current_ammo; std::string ammo_name; - if( used_tool->has_flag( flag_USE_UPS ) ) { + if( used_tool->link || used_tool->has_flag( flag_USE_UPS ) ) { ammo_name = _( "battery" ); current_ammo = ammo_battery; } else if( used_tool->has_flag( flag_USES_BIONIC_POWER ) ) { @@ -2433,7 +2433,7 @@ void repair_item_finish( player_activity *act, Character *you, bool no_menu ) ammo_name = item::nname( used_tool->ammo_current() ); } - int ammo_remaining = used_tool->ammo_remaining( you ); + int ammo_remaining = used_tool->ammo_remaining( you, true ); std::set valid_entries = actor->get_valid_repair_materials( fix ); const inventory &crafting_inv = you->crafting_inventory(); @@ -2459,7 +2459,7 @@ void repair_item_finish( player_activity *act, Character *you, bool no_menu ) } title += string_format( _( "Charges: %s/%s %s (%s per use)\n" ), - ammo_remaining, used_tool->ammo_capacity( current_ammo ), + ammo_remaining, used_tool->ammo_capacity( current_ammo, true ), ammo_name, used_tool->ammo_required() ); title += string_format( _( "Materials available: %s\n" ), string_join( material_list, ", " ) ); diff --git a/src/advanced_inv.cpp b/src/advanced_inv.cpp index 5df693038559e..df7ecea6dc8a8 100644 --- a/src/advanced_inv.cpp +++ b/src/advanced_inv.cpp @@ -643,19 +643,11 @@ struct advanced_inv_sorter { } break; } - // secondary sort by name - const std::string *n1; - const std::string *n2; - if( d1.name_without_prefix == d2.name_without_prefix ) { - //if names without prefix equal, compare full name - n1 = &d1.name; - n2 = &d2.name; - } else { - //else compare name without prefix - n1 = &d1.name_without_prefix; - n2 = &d2.name_without_prefix; - } - return localized_compare( *n1, *n2 ); + // secondary sort by name and link length + auto const sort_key = []( advanced_inv_listitem const & d ) { + return std::make_tuple( d.name_without_prefix, d.name, d.items.front()->link_sort_key() ); + }; + return localized_compare( sort_key( d1 ), sort_key( d2 ) ); } }; diff --git a/src/flag.cpp b/src/flag.cpp index aabd59c6c76b4..88ddb36a1eed6 100644 --- a/src/flag.cpp +++ b/src/flag.cpp @@ -33,7 +33,6 @@ const flag_id flag_ALLOWS_REMOTE_USE( "ALLOWS_REMOTE_USE" ); const flag_id flag_ALWAYS_TWOHAND( "ALWAYS_TWOHAND" ); const flag_id flag_ANIMAL_PRODUCT( "ANIMAL_PRODUCT" ); const flag_id flag_AURA( "AURA" ); -const flag_id flag_AUTO_DELETE_CABLE( "AUTO_DELETE_CABLE" ); const flag_id flag_BAROMETER( "BAROMETER" ); const flag_id flag_BASH_IMMUNE( "BASH_IMMUNE" ); const flag_id flag_BELTED( "BELTED" ); diff --git a/src/flag.h b/src/flag.h index cf44fccde82d0..be132940a2f23 100644 --- a/src/flag.h +++ b/src/flag.h @@ -43,7 +43,6 @@ extern const flag_id flag_ALWAYS_TWOHAND; extern const flag_id flag_ANIMAL_PRODUCT; extern const flag_id flag_OLD_CURRENCY; extern const flag_id flag_AURA; -extern const flag_id flag_AUTO_DELETE_CABLE; extern const flag_id flag_BAROMETER; extern const flag_id flag_BASH_IMMUNE; extern const flag_id flag_BELTED; diff --git a/src/inventory_ui.cpp b/src/inventory_ui.cpp index fb1078c0b285e..c1fa34631eb40 100644 --- a/src/inventory_ui.cpp +++ b/src/inventory_ui.cpp @@ -659,7 +659,8 @@ bool inventory_selector_preset::sort_compare( const inventory_entry &lhs, const inventory_entry &rhs ) const { auto const sort_key = []( inventory_entry const & e ) { - return std::make_tuple( *e.cached_name, *e.cached_name_full, e.generation ); + return std::make_tuple( *e.cached_name, *e.cached_name_full, + e.any_item()->link_sort_key(), e.generation ); }; return localized_compare( sort_key( lhs ), sort_key( rhs ) ); } @@ -1274,6 +1275,8 @@ inventory_entry *inventory_column::add_entry( const inventory_entry &entry ) entry_item.position() == found_entry_item.position() && entry_item.parent_item() == found_entry_item.parent_item() && entry_item->is_collapsed() == found_entry_item->is_collapsed() && + entry_item->link_length() == found_entry_item->link_length() && + entry_item->max_link_length() == found_entry_item->max_link_length() && entry_item->display_stacked_with( *found_entry_item, preset.get_checking_components() ); } ); if( entry_with_loc != dest.end() ) { @@ -1390,8 +1393,8 @@ void inventory_column::collate() if( e->is_item() && e->get_category_ptr() == outer->get_category_ptr() && e->any_item()->is_favorite == outer->any_item()->is_favorite && e->any_item()->typeId() == outer->any_item()->typeId() && - e->any_item()->contents_linked == outer->any_item()->contents_linked && - !!e->any_item()->link == !!outer->any_item()->link && + std::min( 0, e->any_item()->link_length() ) == std::min( 0, outer->any_item()->link_length() ) && + e->any_item()->max_link_length() == outer->any_item()->max_link_length() && ( !indent_entries() || e->any_item().parent_item() == outer->any_item().parent_item() ) && ( e->is_collation_header() || !e->chevron ) && diff --git a/src/item.cpp b/src/item.cpp index 8d0db41594461..76d2a3edb688b 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -223,6 +223,8 @@ static const std::string flag_BLACKPOWDER_FOULING_DAMAGE( "BLACKPOWDER_FOULING_D // item pricing static const int PRICE_FILTHY_MALUS = 100; // cents +static constexpr float MIN_LINK_EFFICIENCY = 0.001f; + class npc_class; using npc_class_id = string_id; @@ -1515,6 +1517,12 @@ bool item::stacks_with( const item &rhs, bool check_components, bool combine_liq } } } + if( link_length() != rhs.link_length() ) { + return false; + } + if( max_link_length() != rhs.max_link_length() ) { + return false; + } const std::vector this_mods = mods(); const std::vector that_mods = rhs.mods(); if( this_mods.size() != that_mods.size() ) { @@ -6665,14 +6673,8 @@ std::string item::tname( unsigned int quantity, bool with_prefix, unsigned int t } if( active && ( has_flag( flag_WATER_EXTINGUISH ) || has_flag( flag_LITCIG ) ) ) { tagtext += _( " (lit)" ); - } else if( contents_linked || ( has_flag( flag_IS_UPS ) && get_var( "cable" ) == "plugged_in" ) ) { + } else if( has_flag( flag_IS_UPS ) && get_var( "cable" ) == "plugged_in" ) { tagtext += _( " (plugged in)" ); - } else if( link ) { - if( link->s_state == link_state::needs_reeling ) { - tagtext += _( " (unspooled)" ); - } else if( active ) { - tagtext += _( " (connected)" ); - } } else if( active && !has_temperature() && !string_ends_with( typeId().str(), "_on" ) ) { // Usually the items whose ids end in "_on" have the "active" or "on" string already contained // in their name, also food is active while it rots. @@ -6747,6 +6749,7 @@ std::string item::display_name( unsigned int quantity ) const std::string name = tname( quantity ); std::string sidetxt; std::string amt; + std::string cable; switch( get_side() ) { case side::BOTH: @@ -6857,6 +6860,30 @@ std::string item::display_name( unsigned int quantity ) const amt = " (" + ammotext + ")"; } + if( link ) { + std::string extensions = cables().empty() ? "" : string_format( "+%d", cables().size() ); + const int link_len = link_length(); + const int link_max_len = max_link_length(); + if( link_len <= -2 ) { + cable = string_format( _( " (-/%1$d cable%2$s)" ), link_max_len, extensions ); + } else if( link_len == -1 ) { + cable = string_format( _( " (%1$s cable%2$s)" ), + colorize( string_format( "×/%d", link_max_len ), c_light_red ), extensions ); + } else { + nc_color cable_color; + const double ratio = static_cast( link_len ) / static_cast( link_max_len ); + if( ratio < 1.0 / 3.0 ) { + cable_color = c_light_green; + } else if( ratio < 2.0 / 3.0 ) { + cable_color = c_yellow; + } else { + cable_color = c_red; + } + cable = string_format( " (%s)", colorize( string_format( _( "%d/%d cable%s" ), + link_max_len - link_len, link_max_len, extensions ), cable_color ) ); + } + } + // HACK: This is a hack to prevent possible crashing when displaying maps as items during character creation if( is_map() && calendar::turn != calendar::turn_zero ) { // TODO: fix point types @@ -6870,7 +6897,7 @@ std::string item::display_name( unsigned int quantity ) const } } - return string_format( "%s%s%s", name, sidetxt, amt ); + return string_format( "%s%s%s%s", name, sidetxt, amt, cable ); } bool item::is_collapsed() const @@ -10378,7 +10405,7 @@ int item::shots_remaining( const Character *carrier ) const return ret; } -int item::ammo_remaining( const Character *carrier, bool cable_links ) const +int item::ammo_remaining( const Character *carrier, const bool include_linked ) const { int ret = 0; @@ -10389,20 +10416,13 @@ int item::ammo_remaining( const Character *carrier, bool cable_links ) const } // Cable connections - if( cable_links && contents_linked ) { - for( const item *cable : contents.cables( true ) ) { - if( !cable->link ) { - continue; - } - if( cable->link->t_veh_safe ) { - ret += cable->link->t_veh_safe->connected_battery_power_level().first; - continue; - } else { - const optional_vpart_position vp = get_map().veh_at( cable->link->t_abs_pos ); - if( vp ) { - ret += vp->vehicle().connected_battery_power_level().first; - continue; - } + if( include_linked && link_length() >= 0 && link->efficiency >= MIN_LINK_EFFICIENCY ) { + if( link->t_veh_safe ) { + ret += link->t_veh_safe->connected_battery_power_level().first; + } else { + const optional_vpart_position vp = get_map().veh_at( link->t_abs_pos ); + if( vp ) { + ret += vp->vehicle().connected_battery_power_level().first; } } } @@ -10445,9 +10465,9 @@ int item::ammo_remaining( const Character *carrier, bool cable_links ) const return ret; } -int item::ammo_remaining( bool cable_links ) const +int item::ammo_remaining( const bool include_linked ) const { - return ammo_remaining( nullptr, cable_links ); + return ammo_remaining( nullptr, include_linked ); } units::energy item::energy_remaining( const Character *carrier ) const @@ -10501,10 +10521,12 @@ int item::remaining_ammo_capacity() const } } -int item::ammo_capacity( const ammotype &ammo ) const +int item::ammo_capacity( const ammotype &ammo, bool include_linked ) const { const item *mag = magazine_current(); - if( mag ) { + if( include_linked && link ) { + return link->t_veh_safe ? link->t_veh_safe->connected_battery_power_level().second : 0; + } else if( mag ) { return mag->ammo_capacity( ammo ); } else if( has_flag( flag_USES_BIONIC_POWER ) ) { return units::to_kilojoule( get_player_character().get_max_power_level() ); @@ -10581,15 +10603,13 @@ int item::ammo_consume( int qty, const tripoint &pos, Character *carrier ) const int wanted_qty = qty; // Consume power from appliances/vehicles connected with cables - if( contents_linked ) { - for( const item *cable : contents.cables( true ) ) { - if( cable->link && cable->link->t_veh_safe ) { - qty = cable->link->t_veh_safe->discharge_battery( qty, true ); - } else { - const optional_vpart_position vp = get_map().veh_at( cable->link->t_abs_pos ); - if( vp ) { - qty = vp->vehicle().discharge_battery( qty, true ); - } + if( link ) { + if( link->t_veh_safe && link->efficiency >= MIN_LINK_EFFICIENCY ) { + qty = link->t_veh_safe->discharge_battery( qty, true ); + } else { + const optional_vpart_position vp = get_map().veh_at( link->t_abs_pos ); + if( vp ) { + qty = vp->vehicle().discharge_battery( qty, true ); } } } @@ -10934,6 +10954,11 @@ std::vector item::ebooks() const return contents.ebooks(); } +std::vector item::cables() const +{ + return contents.cables(); +} + item *item::gunmod_find( const itype_id &mod ) { std::vector mods = gunmods(); @@ -12260,7 +12285,7 @@ int item::processing_speed() const return to_turns( 10_minutes ); } - if( active || ethereal || wetness || contents_linked || + if( active || ethereal || wetness || link || has_flag( flag_RADIO_ACTIVATION ) || has_relic_recharge() || has_fault_flag( flag_BLACKPOWDER_FOULING_DAMAGE ) ) { // Unless otherwise indicated, update every turn. @@ -12931,26 +12956,79 @@ bool item::process_extinguish( map &here, Character *carrier, const tripoint &po return false; } -bool item::process_cable( map &here, Character *carrier, const tripoint &pos, item *parent_item ) +void item::set_link_traits( const bool assign_t_state ) { - // Active cables need link data to process. - if( !link ) { - return reset_cable( carrier, parent_item ); + if( !link || !type->can_use( "link_up" ) ) { + return; } - // Failsafe for loose, unwound cables that are somehow still active. - if( link->s_state == link_state::needs_reeling || link->has_no_links() ) { - return reset_cable( carrier, parent_item ); + const link_up_actor *it_actor = static_cast + ( get_use( "link_up" )->get_actor_ptr() ); + link->max_length = it_actor->cable_length == -1 ? type->maximum_charges() : it_actor->cable_length; + link->efficiency = it_actor->efficiency < MIN_LINK_EFFICIENCY ? 0.0f : link->efficiency; + // Reset s_bub_pos to force the item to check the length during process_link. + link->s_bub_pos = tripoint_min; + + for( const item *cable : cables() ) { + if( !cable->type->can_use( "link_up" ) ) { + continue; + } + const link_up_actor *actor = static_cast + ( cable->get_use( "link_up" )->get_actor_ptr() ); + link->max_length += actor->cable_length == -1 ? cable->type->maximum_charges() : + actor->cable_length; + link->efficiency = link->efficiency < MIN_LINK_EFFICIENCY ? 0.0f : + link->efficiency * actor->efficiency; } - // Handle item-side links. + if( assign_t_state && link->t_veh_safe ) { + // Assign t_state based on the parts available at the connected mount point. + if( it_actor->targets.find( link_state::vehicle_port ) != it_actor->targets.end() && + ( link->t_veh_safe->avail_part_with_feature( link->t_mount, "CABLE_PORTS" ) != -1 || + link->t_veh_safe->avail_part_with_feature( link->t_mount, "APPLIANCE" ) != -1 ) ) { + link->t_state = link_state::vehicle_port; + } else if( it_actor->targets.find( link_state::vehicle_battery ) != it_actor->targets.end() && + ( link->t_veh_safe->avail_part_with_feature( link->t_mount, "BATTERY" ) != -1 || + link->t_veh_safe->avail_part_with_feature( link->t_mount, "APPLIANCE" ) != -1 ) ) { + link->t_state = link_state::vehicle_battery; + } + } +} + +int item::link_length() const +{ + return !link || link->has_no_links() ? -2 : + link->has_state( link_state::needs_reeling ) ? -1 : link->length; +} + +int item::max_link_length() const +{ + return !link ? -2 : link->max_length != -1 ? link->max_length : type->maximum_charges(); +} + +int item::link_sort_key() const +{ + const int length = link_length(); + int key = length >= 0 ? -1000000000 : length == -1 ? 0 : 1000000000; + key += max_link_length() * 100000; + return key - length; +} + +bool item::process_link( map &here, Character *carrier, const tripoint &pos ) +{ + if( link_length() < 0 ) { + return false; + } + + const bool is_cable_item = has_flag( flag_CABLE_SPOOL ); + + // Handle links to items in the inventory. if( link->s_state == link_state::solarpack ) { if( carrier == nullptr || !carrier->worn_with_flag( flag_SOLARPACK_ON ) ) { - add_msg_if_player_sees( pos, m_bad, parent_item == nullptr ? - string_format( _( "The %s has come loose from the solar pack." ), label( 1 ) ) : - string_format( _( "The %s's cable has come loose from the solar pack." ), - parent_item->label( 1 ) ) ); - reset_cable( carrier, parent_item ); + add_msg_if_player_sees( pos, m_bad, + string_format( is_cable_item ? _( "The %s has come loose from the solar pack." ) : + _( "The %s's cable has come loose from the solar pack." ), type_name() ) ); + reset_link( carrier ); return false; } } @@ -12959,48 +13037,49 @@ bool item::process_cable( map &here, Character *carrier, const tripoint &pos, it }; if( link->s_state == link_state::ups ) { if( carrier == nullptr || !carrier->has_item_with( used_ups ) ) { - add_msg_if_player_sees( pos, m_bad, parent_item == nullptr ? - string_format( _( "The %s has come loose from the UPS." ), label( 1 ) ) : - string_format( _( "The %s's cable has come loose from the UPS." ), parent_item->label( 1 ) ) ); - reset_cable( carrier, parent_item ); + add_msg_if_player_sees( pos, m_bad, + string_format( is_cable_item ? _( "The %s has come loose from the UPS." ) : + _( "The %s's cable has come loose from the UPS." ), type_name() ) ); + reset_link( carrier ); return false; } } + // Certain cable states should skip processing and also become inactive if dropped. if( ( link->t_state == link_state::no_link && link->s_state != link_state::vehicle_tow ) || link->t_state == link_state::bio_cable ) { - // Certain cable states should skip processing and also become inactive if dropped. if( carrier == nullptr ) { - return reset_cable( nullptr, parent_item, true, pos ); + return reset_link( nullptr, -1, true, pos ); } return false; } const bool last_t_abs_pos_is_oob = !here.inbounds( link->t_abs_pos ); - // Lambda function for checking if a cable's been stretched too long, resetting it if so. + // Lambda function for setting a cable's length, then checking if it's now stretched too long, resetting it if so. // @return True if the cable is disconnected. - const auto is_cable_too_long = [this, carrier, pos, parent_item, last_t_abs_pos_is_oob]() { + const auto set_length_and_check = [this, carrier, pos, last_t_abs_pos_is_oob, + is_cable_item]( float new_length ) { + link->length = new_length; if( debug_mode ) { - add_msg_debug( debugmode::DF_IUSE, "%s linked to %s%s, length %d/%d", - parent_item != nullptr ? parent_item->label( 1 ) : label( 1 ), + add_msg_debug( debugmode::DF_IUSE, "%s linked to %s%s, length %d/%d", type_name(), link->t_abs_pos.to_string_writable(), last_t_abs_pos_is_oob ? " (OoB)" : "", - link->max_length - charges, link->max_length ); + link->length, link->max_length ); } - if( charges == 0 && carrier != nullptr ) { - carrier->add_msg_if_player( m_warning, parent_item == nullptr ? - string_format( _( "Your %s is stretched to its limit!" ), label( 1 ) ) : - string_format( _( "Your %s's cable is stretched to its limit!" ), parent_item->label( 1 ) ) ); - } else if( charges < 0 ) { + if( link->length > link->max_length ) { if( carrier != nullptr ) { - carrier->add_msg_if_player( m_bad, parent_item == nullptr ? - string_format( _( "Your over-extended %s breaks loose!" ), label( 1 ) ) : - string_format( _( "Your %s's over-extended cable breaks loose!" ), parent_item->label( 1 ) ) ); + carrier->add_msg_if_player( m_bad, + string_format( is_cable_item ? _( "Your over-extended %s breaks loose!" ) : + _( "Your %s's over-extended cable breaks loose!" ), type_name() ) ); } else { - add_msg_if_player_sees( pos, m_bad, parent_item == nullptr ? - string_format( _( "The over-extended %s breaks loose!" ), label( 1 ) ) : - string_format( _( "The %s's over-extended cable breaks loose!" ), parent_item->label( 1 ) ) ); + add_msg_if_player_sees( pos, m_bad, + string_format( is_cable_item ? _( "The over-extended %s breaks loose!" ) : + _( "The %s's over-extended cable breaks loose!" ), type_name() ) ); } return true; + } else if( new_length + M_SQRT2 >= link->max_length + 1 && carrier != nullptr ) { + carrier->add_msg_if_player( m_warning, + string_format( is_cable_item ? _( "Your %s is stretched to its limit!" ) : + _( "Your %s's cable is stretched to its limit!" ), type_name() ) ); } return false; }; @@ -13012,47 +13091,36 @@ bool item::process_cable( map &here, Character *carrier, const tripoint &pos, it check_length = true; } - // If the vehicle pointer is lost... + // Re-establish vehicle pointer if it got lost or if this item just got loaded. if( !link->t_veh_safe ) { - if( last_t_abs_pos_is_oob ) { - // ... and the last recorded target point is out of bounds, just check the length if needed and exit early. - if( check_length ) { - // We can't get the exact connection point without the vehicle loaded, so fudge some forgiveness by adding the mount dimensions. - // Better to err on the side of keeping things connected. - charges = link->max_length - rl_dist( here.getabs( pos ), link->t_abs_pos.raw() ) + - link->t_mount.abs().x + link->t_mount.abs().y; - if( is_cable_too_long() ) { - return reset_cable( carrier, parent_item ); - } - } - return false; - } else { - // ... and the last recorded target point is in-bounds, try to recreate the vehicle pointer, disconnecting if it fails. - const optional_vpart_position vp = here.veh_at( link->t_abs_pos ); - if( !vp ) { - return reset_cable( carrier, parent_item, true, pos ); - } - auto vp_displayed = vp.part_displayed(); - if( vp_displayed && vp_displayed->part().has_flag( vp_flag::carried_flag ) ) { - // Connected vehicle was racked, so disconnect. - return reset_cable( carrier, parent_item, true, pos ); - } - link->t_veh_safe = vp.value().vehicle().get_safe_reference(); + vehicle *found_veh = vehicle::find_vehicle( link->t_abs_pos ); + if( !found_veh ) { + return reset_link( carrier, -2, true, pos ); } + if( debug_mode ) { + add_msg_debug( debugmode::DF_IUSE, "Re-established link of %s to %s.", type_name(), + found_veh->disp_name() ); + } + link->t_veh_safe = found_veh->get_safe_reference(); } // Regular pointers are faster, so make one now that we know the reference is valid. vehicle *t_veh = link->t_veh_safe.get(); - // We should skip processing if the last saved target point is out of bounds, but if the linked vehicle is moving fast enough, - // we should always process it to avoid erroneously skipping devices riding inside of it. + // We should skip processing if the last saved target point is out of bounds, since vehicles give innacurate absolute coordinates when out of bounds. + // However, if the linked vehicle is moving fast enough, we should always do processing to avoid erroneously skipping linked items riding inside of it. if( last_t_abs_pos_is_oob && t_veh->velocity < HALF_MAPSIZE_X * 400 ) { - if( check_length ) { - charges = link->max_length - rl_dist( here.getabs( pos ), link->t_abs_pos.raw() ) + - link->t_mount.abs().x + link->t_mount.abs().y; - if( is_cable_too_long() ) { - return reset_cable( carrier, parent_item ); + if( !check_length ) { + return false; + } + if( trigdist ) { + if( set_length_and_check( trig_dist( here.getabs( pos ), link->t_abs_pos.raw() ) + + link->t_mount.abs().x + link->t_mount.abs().y ) ) { + return reset_link( carrier ); } + } else if( set_length_and_check( square_dist( here.getabs( pos ), link->t_abs_pos.raw() ) + + link->t_mount.abs().x + link->t_mount.abs().y ) ) { + return reset_link( carrier ); } return false; } @@ -13073,13 +13141,31 @@ bool item::process_cable( map &here, Character *carrier, const tripoint &pos, it break; } } + if( link_vp_index == -1 ) { + // Check cable_ports, since that includes appliances + for( int idx : t_veh->cable_ports ) { + if( t_veh->part( idx ).mount == link->t_mount ) { + link_vp_index = idx; + break; + } + } + } } else if( link->t_state == link_state::vehicle_tow || link->s_state == link_state::vehicle_tow ) { link_vp_index = t_veh->part_at( t_veh->coord_translate( link->t_mount ) ); } if( link_vp_index == -1 ) { // The part with cable ports was lost, so disconnect the cable. - return reset_cable( carrier, parent_item, true, pos ); + return reset_link( carrier, -2, true, pos ); + } + + if( link->last_processed <= t_veh->part( link_vp_index ).last_disconnected ) { + add_msg_if_player_sees( pos, m_warning, string_format( _( "You detached the %s." ), type_name() ) ); + return reset_link( carrier, -2 ); } + t_veh->part( link_vp_index ).set_flag( vp_flag::linked_flag ); + + int turns_elapsed = to_turns( calendar::turn - link->last_processed ); + link->last_processed = calendar::turn; // Set the new absolute position to the vehicle's origin. tripoint t_veh_bub_pos = t_veh->global_pos3(); @@ -13089,35 +13175,39 @@ bool item::process_cable( map &here, Character *carrier, const tripoint &pos, it check_length = true; } - // If either the link's connected sides moved, check cable's length. + // If either of the link's connected sides moved, check the cable's length. if( check_length ) { - charges = link->max_length - rl_dist( pos, - t_veh_bub_pos + t_veh->part( link_vp_index ).precalc[0] ); - if( is_cable_too_long() ) { - return reset_cable( carrier, parent_item ); + if( trigdist ) { + if( set_length_and_check( trig_dist( pos, t_veh_bub_pos + + t_veh->part( link_vp_index ).precalc[0] ) ) ) { + return reset_link( carrier, link_vp_index ); + } + } else if( set_length_and_check( square_dist( pos, t_veh_bub_pos + + t_veh->part( link_vp_index ).precalc[0] ) ) ) { + return reset_link( carrier, link_vp_index ); } } // Extra behaviors for the cabled item. - if( parent_item != nullptr ) { - int turns_elapsed = to_turns( calendar::turn - link->last_processed ); - link->last_processed = calendar::turn; - + if( !is_cable_item ) { int power_draw = 0; + if( link->efficiency < MIN_LINK_EFFICIENCY ) { + return false; + } // Recharge or charge linked batteries - power_draw -= charge_linked_batteries( *parent_item, *t_veh, turns_elapsed ); + power_draw -= charge_linked_batteries( *t_veh, turns_elapsed ); // Tool power draw display - if( parent_item->active && parent_item->type->tool && parent_item->type->tool->power_draw > 0_W ) { - power_draw -= parent_item->type->tool->power_draw.value(); + if( active && type->tool && type->tool->power_draw > 0_W ) { + power_draw -= type->tool->power_draw.value(); } // Send total power draw to the vehicle so it can be displayed. if( power_draw != 0 ) { t_veh->linked_item_epower_this_turn += units::from_milliwatt( power_draw ); } - } else if( has_flag( flag_AUTO_DELETE_CABLE ) ) { + } else if( has_flag( flag_NO_DROP ) ) { debugmsg( "%s shouldn't exist outside of an item or vehicle part.", tname() ); return true; } @@ -13125,115 +13215,134 @@ bool item::process_cable( map &here, Character *carrier, const tripoint &pos, it return false; } -int item::charge_linked_batteries( item &linked_item, vehicle &linked_veh, int turns_elapsed ) +int item::charge_linked_batteries( vehicle &linked_veh, int turns_elapsed ) { - if( !link || link->charge_rate == 0 || turns_elapsed < 1 || link->charge_interval < 1 ) { - return 0; + if( link->charge_rate == 0 || turns_elapsed < 1 || + link->charge_interval < 1 || link->efficiency < MIN_LINK_EFFICIENCY ) { + return link->charge_rate; } - if( !linked_item.is_battery() ) { - const item *parent_mag = linked_item.magazine_current(); + if( !is_battery() ) { + const item *parent_mag = magazine_current(); if( !parent_mag || !parent_mag->has_flag( flag_RECHARGE ) ) { return 0; } } const bool power_in = link->charge_rate > 0; - if( power_in ? linked_item.ammo_remaining() >= linked_item.ammo_capacity( ammo_battery ) : - linked_item.ammo_remaining() <= 0 ) { + if( power_in ? ammo_remaining() >= ammo_capacity( ammo_battery ) : + ammo_remaining() <= 0 ) { return 0; } - // Normally efficiency is a random chance to skip a charge, but if we're catching up from time - // spent ouside the reality bubble it should be applied as a percentage of the total instead. + // Normally efficiency is the chance to get a charge every charge_interval, but if we're catching up from + // time spent ouside the reality bubble it should be applied as a percentage of the total instead. bool short_time_passed = turns_elapsed <= link->charge_interval; - if( !calendar::once_every( time_duration::from_turns( link->charge_interval ) ) && - short_time_passed ) { + if( short_time_passed && + !calendar::once_every( time_duration::from_turns( link->charge_interval ) ) ) { return link->charge_rate; } // If a long time passed, multiply the total by the efficiency rather than cancelling a charge. int transfer_total = short_time_passed ? 1 : - ( turns_elapsed * 1.0f / link->charge_interval ) * ( 1.0 - 1.0 / link->charge_efficiency ); + ( turns_elapsed * 1.0f / link->charge_interval ) * link->charge_interval; if( power_in ) { const int battery_deficit = linked_veh.discharge_battery( transfer_total, true ); // Around 85% efficient by default; a few of the discharges don't actually recharge - if( battery_deficit == 0 && !( short_time_passed && one_in( link->charge_efficiency ) ) ) { - linked_item.ammo_set( itype_battery, linked_item.ammo_remaining() + transfer_total ); + if( battery_deficit == 0 && ( !short_time_passed || rng_float( 0.0, 1.0 ) <= link->efficiency ) ) { + ammo_set( itype_battery, ammo_remaining() + transfer_total ); } } else { // Around 85% efficient by default; a few of the discharges don't actually charge - if( !( short_time_passed && one_in( link->charge_efficiency ) ) ) { + if( !short_time_passed || rng_float( 0.0, 1.0 ) <= link->efficiency ) { const int battery_surplus = linked_veh.charge_battery( transfer_total, true ); if( battery_surplus == 0 ) { - linked_item.ammo_set( itype_battery, linked_item.ammo_remaining() - transfer_total ); + ammo_set( itype_battery, ammo_remaining() - transfer_total ); } } else { const std::pair linked_levels = linked_veh.connected_battery_power_level(); if( linked_levels.first < linked_levels.second ) { - linked_item.ammo_set( itype_battery, linked_item.ammo_remaining() - transfer_total ); + ammo_set( itype_battery, ammo_remaining() - transfer_total ); } } } return link->charge_rate; } -bool item::reset_cable( Character *p, item *parent_item, const bool loose_message, - const tripoint sees_point ) +bool item::reset_link( Character *p, int vpart_index, + const bool loose_message, const tripoint cable_position ) { - active = false; if( !link ) { - if( parent_item != nullptr ) { - debugmsg( "%s's active cable lost its cable data!", parent_item->tname() ); - parent_item->contents_linked = false; - } else { - debugmsg( "Active cable %s lost its cable data!", tname() ); + return has_flag( flag_NO_DROP ); + } + // Cables that need reeling should be reset with a reel_cable_activity_actor instead. + if( link->has_state( link_state::needs_reeling ) ) { + return false; + } + + if( vpart_index != -2 && link->t_veh_safe ) { + if( vpart_index == -1 ) { + vehicle *t_veh = link->t_veh_safe.get(); + // Find the vp_part index the cable is linked to. + if( link->t_state == link_state::vehicle_port ) { + for( int idx : t_veh->cable_ports ) { + if( t_veh->part( idx ).mount == link->t_mount ) { + vpart_index = idx; + break; + } + } + } else if( link->t_state == link_state::vehicle_battery ) { + for( int idx : t_veh->batteries ) { + if( t_veh->part( idx ).mount == link->t_mount ) { + vpart_index = idx; + break; + } + } + } else if( link->t_state == link_state::vehicle_tow || link->s_state == link_state::vehicle_tow ) { + vpart_index = t_veh->part_at( t_veh->coord_translate( link->t_mount ) ); + } + } + if( vpart_index != -1 ) { + link->t_veh_safe->part( vpart_index ).remove_flag( vp_flag::linked_flag ); } - charges = type->maximum_charges(); - return has_flag( flag_AUTO_DELETE_CABLE ); } + const bool is_cable_item = has_flag( flag_CABLE_SPOOL ); + if( loose_message ) { if( p != nullptr ) { - p->add_msg_if_player( m_warning, parent_item == nullptr ? - string_format( _( "Your %s has come loose." ), label( 1 ) ) : - string_format( _( "Your %s's cable has come loose." ), parent_item->label( 1 ) ) ); + p->add_msg_if_player( m_warning, + string_format( is_cable_item ? _( "Your %s has come loose." ) : + _( "Your %s's cable has come loose." ), type_name() ) ); } else { - add_msg_if_player_sees( sees_point, m_warning, parent_item == nullptr ? - string_format( _( "The %s has come loose." ), label( 1 ) ) : - string_format( _( "The %s's cable has come loose." ), parent_item->label( 1 ) ) ); + add_msg_if_player_sees( cable_position, m_warning, + string_format( is_cable_item ? _( "The %s has come loose." ) : + _( "The %s's cable has come loose." ), type_name() ) ); } } - if( parent_item != nullptr ) { - parent_item->contents_linked = false; - } - const int respool_length = 5; - if( link->max_length - charges > respool_length ) { + + const int respool_threshold = 6; + if( link->length > respool_threshold ) { // Cables that are too long need to be manually rewound before reuse. link->s_state = link_state::needs_reeling; return false; } - charges = link->max_length; - link.reset(); - return has_flag( flag_AUTO_DELETE_CABLE ); -} - -void item::reset_cables( Character *p ) -{ - if( !contents_linked ) { - debugmsg( "Tried to reset %s's cables but it wasn't plugged in.", tname() ); - return; + if( loose_message && p ) { + p->add_msg_if_player( m_info, string_format( is_cable_item ? + _( "You reel in the %s and wind it up." ) : + _( "You reel in the %s's cable and wind it up." ), tname() ) ); } - std::vector cables = contents.cables( true ); - for( item *cable : cables ) { - if( cable->reset_cable( p, this ) ) { - remove_item( *cable ); - } + + link.reset(); + if( !cables().empty() ) { + // If there are extensions, keep link active to maintain max_length. + link = cata::make_value(); + set_link_traits(); } - contents_linked = false; + return has_flag( flag_NO_DROP ); } bool item::process_linked_item( Character *carrier, const tripoint & /*pos*/, @@ -13388,18 +13497,8 @@ bool item::process_internal( map &here, Character *carrier, const tripoint &pos, wetness -= 1; } - if( contents_linked ) { - std::vector cables = contents.cables( true ); - if( !cables.empty() ) { - for( item *cable : cables ) { - if( cable->process_cable( here, carrier, pos, this ) ) { - remove_item( *cable ); - } - } - } else { - debugmsg( "%s was labeled as plugged in but had no active cables inside.", tname() ); - contents_linked = false; - } + if( link ) { + process_link( here, carrier, pos ); } // Remaining stuff is only done for active items. @@ -13463,7 +13562,8 @@ bool item::process_internal( map &here, Character *carrier, const tripoint &pos, } if( has_flag( flag_CABLE_SPOOL ) && mark_flag() ) { // DO NOT process this as a tool! It really isn't! - return process_cable( here, carrier, pos ); + active = false; + return false; } if( has_flag( flag_IS_UPS ) && mark_flag() ) { // DO NOT process this as a tool! It really isn't! diff --git a/src/item.h b/src/item.h index 183058d4c65d9..7e63ef0b64156 100644 --- a/src/item.h +++ b/src/item.h @@ -1420,7 +1420,7 @@ class item : public visitable link_state s_state = link_state::no_link; /// State of the link's target, the end represented by t_abs_pos, @ref link_state. link_state t_state = link_state::no_link; - /// The last turn process_cable was called on this cable. Used for time the cable spends outside the bubble. + /// The last turn process_link was called on this cable. Used for time the cable spends outside the bubble. time_point last_processed = calendar::turn; /// Absolute position of the linked target vehicle/appliance. tripoint_abs_ms t_abs_pos = tripoint_abs_ms( tripoint_min ); @@ -1430,20 +1430,22 @@ class item : public visitable safe_reference t_veh_safe; // NOLINT(cata-serialize) /// The linked part's mount offset on the target vehicle. point t_mount = point_zero; + /// The current slack of the cable. + int length = 0; /// The maximum length of the cable. Set during initialization. int max_length = 2; - /// The cable's charge rate in watts. Set during initialization. + /// The cable's power capacity in watts, affects battery charge rate. Set during initialization. int charge_rate = 0; + /// (this) out of 1.0 chance to successfully add 1 charge every charge interval. + float efficiency = 0.0f; /// The turn interval between charges. Set during initialization. - int charge_interval = -1; - /// one_in(this) chance to fail adding 1 charge. Set during initialization. - int charge_efficiency = 7; + int charge_interval = 0; bool has_state( link_state state ) const { return s_state == state || t_state == state; } - bool has_states( link_state i_state_, link_state t_state_ ) const { - return s_state == i_state_ && t_state == t_state_; + bool has_states( link_state s_state_, link_state t_state_ ) const { + return s_state == s_state_ && t_state == t_state_; } bool has_no_links() const { return s_state == link_state::no_link && t_state == link_state::no_link; @@ -1452,29 +1454,54 @@ class item : public visitable void serialize( JsonOut &jsout ) const; void deserialize( const JsonObject &data ); }; - cata::value_ptr link; + /** + * @brief Sets max_length and efficiency of a link, taking cable extensions into account. + * @brief max_length is set to the sum of all cable lengths. + * @brief efficiency is set to the product of all efficiencies multiplied together. + * @param assign_t_state If true, set the t_state based on the parts at the connection point. Defaults to false. + */ + void set_link_traits( bool assign_t_state = false ); + + /** + * @return The link's current length. + * @return `-1` if the item has link_data but needs reeling. + * @return `-2` if the item has no active link. + */ + int link_length() const; + + /** + * @return The item's maximum possible link length, including extensions. Item doesn't need an active link. + * @return `-2` if the item has no active link or extensions. + */ + int max_link_length() const; + + /** + * Value used for sorting linked items in inventory lists. + */ + int link_sort_key() const; + /** * Brings a cable item back to its initial state. + * @param p Set to character that's holding the linked item, nullptr if none. + * @param vpart_index The index of the vehicle part the cable is attached to, so it can have `linked_flag` removed. + * @param * At -1, the default, this function will look up the index itself. At -2, skip modifying the part's flags entirely. + * @param loose_message If there should be a notification that the link was disconnected. + * @param cable_position Position of the linked item, used to determine if the player can see the link becoming loose. * @return True if the cable should be deleted. */ - bool reset_cable( Character *p = nullptr, item *parent_item = nullptr, - bool loose_message = false, tripoint sees_point = tripoint_zero ); - /** - * Resets all of an items cables. - */ - void reset_cables( Character *p ); + bool reset_link( Character *p = nullptr, int vpart_index = -1, + bool loose_message = false, tripoint cable_position = tripoint_zero ); /** * @brief Exchange power between an item's batteries and the vehicle/appliance it's linked to. * @brief A positive link.charge_rate will charge the item at the expense of the vehicle, * while a negative link.charge_rate will charge the vehicle at the expense of the item. * - * @param linked_item The item that contains the linking cable. * @param linked_veh The vehicle the item is connected to. * @param turns_elapsed The number of turns the link has spent outside the reality bubble. Default 1. * @return The amount of power given or taken to be displayed; ignores turns_elapsed and inefficiency. */ - int charge_linked_batteries( item &linked_item, vehicle &linked_veh, int turns_elapsed = 1 ); + int charge_linked_batteries( vehicle &linked_veh, int turns_elapsed = 1 ); /** * Whether the item should be processed (by calling @ref process). @@ -2305,15 +2332,17 @@ class item : public visitable /** * Quantity of ammunition currently loaded in tool, gun or auxiliary gunmod. * @param carrier is used for UPS and bionic power for tools - * @param cable_links Add cable-linked vehicles' ammo to the ammo count + * @param include_linked Add cable-linked vehicles' ammo to the ammo count */ - int ammo_remaining( const Character *carrier = nullptr, bool cable_links = false ) const; - int ammo_remaining( bool cable_links ) const; + int ammo_remaining( const Character *carrier = nullptr, bool include_linked = false ) const; + int ammo_remaining( bool include_linked ) const; /** * ammo capacity for a specific ammo + * @param ammo The ammo type to get the capacity for + * @param include_linked If linked up, return linked electricity grid's capacity */ - int ammo_capacity( const ammotype &ammo ) const; + int ammo_capacity( const ammotype &ammo, bool include_linked = false ) const; /** * how much more ammo can fit into this item @@ -2457,6 +2486,8 @@ class item : public visitable std::vector ebooks() const; + std::vector cables() const; + /** Get first attached gunmod matching type or nullptr if no such mod or item is not a gun */ item *gunmod_find( const itype_id &mod ); const item *gunmod_find( const itype_id &mod ) const; @@ -2911,8 +2942,7 @@ class item : public visitable // Place conditions that should remove fake smoke item in this sub-function bool process_fake_smoke( map &here, Character *carrier, const tripoint &pos ); bool process_fake_mill( map &here, Character *carrier, const tripoint &pos ); - bool process_cable( map &here, Character *carrier, const tripoint &pos, - item *parent_item = nullptr ); + bool process_link( map &here, Character *carrier, const tripoint &pos ); bool process_linked_item( Character *carrier, const tripoint &pos, link_state required_state ); bool process_blackpowder_fouling( Character *carrier ); bool process_tool( Character *carrier, const tripoint &pos ); @@ -2977,6 +3007,7 @@ class item : public visitable public: // any relic data specific to this item cata::value_ptr relic_data; + cata::value_ptr link; int charges = 0; units::energy energy = 0_mJ; // Amount of energy currently stored in a battery @@ -2999,9 +3030,6 @@ class item : public visitable bool ethereal = false; int wetness = 0; // Turns until this item is completely dry. - // This contains a cable with an active link. Is NOT true for linked cable items themselves. - bool contents_linked = false; - int seed = rng( 0, INT_MAX ); // A random seed for layering and other options harvest_drop_type_id dropped_from = diff --git a/src/item_contents.cpp b/src/item_contents.cpp index c38b76fcacad2..52ca61e065b4a 100644 --- a/src/item_contents.cpp +++ b/src/item_contents.cpp @@ -1354,6 +1354,15 @@ void item_contents::clear_magazines() } } +void item_contents::clear_pockets_if( const std::function &filter ) +{ + for( item_pocket &pocket : contents ) { + if( filter( pocket ) ) { + pocket.clear_items(); + } + } +} + void item_contents::update_open_pockets() { for( item_pocket &pocket : contents ) { @@ -1756,28 +1765,27 @@ std::vector item_contents::ebooks() const return ebooks; } -std::vector item_contents::cables( bool active_only ) +std::vector item_contents::cables() { std::vector cables; for( item_pocket &pocket : contents ) { if( pocket.is_type( item_pocket::pocket_type::CABLE ) ) { for( item *it : pocket.all_items_top() ) { - if( !active_only || it->active ) { - cables.emplace_back( it ); - } + cables.emplace_back( it ); } } } return cables; } -std::vector item_contents::cables( bool active_only ) const +std::vector item_contents::cables() const { std::vector cables; for( const item_pocket &pocket : contents ) { if( pocket.is_type( item_pocket::pocket_type::CABLE ) ) { for( const item *it : pocket.all_items_top() ) { - if( !active_only || it->active ) { + // TODO: remove flag check after 0.H + if( it->has_flag( STATIC( flag_id( "CABLE_SPOOL" ) ) ) ) { cables.emplace_back( it ); } } diff --git a/src/item_contents.h b/src/item_contents.h index e185c78d9f2c9..08ff6e7aaf9ea 100644 --- a/src/item_contents.h +++ b/src/item_contents.h @@ -132,8 +132,8 @@ class item_contents std::vector ebooks(); std::vector ebooks() const; - std::vector cables( bool active_only = false ); - std::vector cables( bool active_only = false ) const; + std::vector cables(); + std::vector cables() const; void update_modified_pockets( const std::optional &mag_or_mag_well, std::vector container_pockets ); @@ -275,6 +275,7 @@ class item_contents void clear_items(); // clears all items from magazine type pockets void clear_magazines(); + void clear_pockets_if( const std::function &filter ); void update_open_pockets(); /** diff --git a/src/item_factory.cpp b/src/item_factory.cpp index 13864fa0bcb11..81094afb97c40 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -667,6 +667,10 @@ void Item_factory::finalize_pre( itype &obj ) obj.book->martial_art = matype_id( "style_" + obj.get_id().str().substr( 7 ) ); } + if( obj.can_use( "link_up" ) ) { + obj.ammo_scale.emplace( "link_up", 0 ); + } + if( obj.longest_side == -1_mm ) { units::volume effective_volume = obj.count_by_charges() && obj.stack_size > 0 ? ( obj.volume / obj.stack_size ) : obj.volume; @@ -3723,21 +3727,11 @@ void Item_factory::add_special_pockets( itype &def ) if( !has_pocket_type( def.pockets, item_pocket::pocket_type::MIGRATION ) ) { def.pockets.emplace_back( item_pocket::pocket_type::MIGRATION ); } - if( !has_pocket_type( def.pockets, item_pocket::pocket_type::CABLE ) ) { - const use_function *iuse = def.get_use( "link_up" ); - if( iuse != nullptr ) { - const link_up_actor *actor_ptr = - static_cast( iuse->get_actor_ptr() ); - if( actor_ptr != nullptr && !actor_ptr->is_cable_item ) { - pocket_data cable_pocket( item_pocket::pocket_type::CABLE ); - cable_pocket.rigid = true; - cable_pocket.volume_capacity = units::from_milliliter( 1 ); - cable_pocket.max_contains_weight = units::from_gram( 1 ); - cable_pocket.weight_multiplier = 0.0f; - cable_pocket.volume_multiplier = 0.0f; - def.pockets.emplace_back( cable_pocket ); - } - } + if( !has_pocket_type( def.pockets, item_pocket::pocket_type::CABLE ) && + def.get_use( "link_up" ) != nullptr ) { + pocket_data cable_pocket( item_pocket::pocket_type::CABLE ); + cable_pocket.rigid = false; + def.pockets.emplace_back( cable_pocket ); } } diff --git a/src/item_pocket.cpp b/src/item_pocket.cpp index 72c826f55aed7..44a2223548b5f 100644 --- a/src/item_pocket.cpp +++ b/src/item_pocket.cpp @@ -1320,7 +1320,7 @@ ret_val item_pocket::is_compatible( const item &it ) return ret_val::make_success(); } else { return ret_val::make_failure( - contain_code::ERR_MOD, _( "only certain cables can go into cable pocket" ) ); + contain_code::ERR_MOD, _( "only cables can go into cable pocket" ) ); } } diff --git a/src/iuse_actor.cpp b/src/iuse_actor.cpp index ba16a04545e43..e04cfc17b1635 100644 --- a/src/iuse_actor.cpp +++ b/src/iuse_actor.cpp @@ -129,6 +129,7 @@ static const itype_id itype_barrel_small( "barrel_small" ); static const itype_id itype_brazier( "brazier" ); static const itype_id itype_char_smoker( "char_smoker" ); static const itype_id itype_fire( "fire" ); +static const itype_id itype_power_cord( "power_cord" ); static const itype_id itype_stock_none( "stock_none" ); static const itype_id itype_syringe( "syringe" ); @@ -4307,16 +4308,16 @@ std::unique_ptr link_up_actor::clone() const void link_up_actor::load( const JsonObject &jo ) { - jo.read( "is_cable_item", is_cable_item ); - jo.read( "cable_type", type ); jo.read( "cable_length", cable_length ); jo.read( "charge_rate", charge_rate ); - jo.read( "efficiency", charge_efficiency ); + jo.read( "efficiency", efficiency ); + jo.read( "move_cost", move_cost ); jo.read( "menu_text", menu_text ); jo.read( "targets", targets ); + jo.read( "can_extend", can_extend ); } -void link_up_actor::info( const item &, std::vector &dump ) const +void link_up_actor::info( const item &it, std::vector &dump ) const { std::vector targets_strings; bool appliance = false; @@ -4340,24 +4341,41 @@ void link_up_actor::info( const item &, std::vector &dump ) const if( targets.count( link_state::solarpack ) > 0 ) { targets_strings.emplace_back( _( "solar pack" ) ); } - + if( targets.count( link_state::vehicle_tow ) > 0 ) { + dump.emplace_back( "TOOL", _( "Can tow a vehicle." ) ); + } if( !targets_strings.empty() ) { std::string targets_string = enumerate_as_string( targets_strings, enumeration_conjunction::or_ ); dump.emplace_back( "TOOL", string_format( _( "Can be plugged into: %s." ), targets_string ) ); } - if( targets.count( link_state::vehicle_tow ) > 0 ) { - dump.emplace_back( "TOOL", _( "Can tow a vehicle." ) ); - } - dump.emplace_back( "TOOL", _( "Cable length: " ), cable_length ); + const bool no_extensions = it.cables().empty(); + item dummy( it ); + dummy.link = cata::make_value(); + dummy.set_link_traits(); + + std::string length_all_info = string_format( _( "Cable length: %d" ), + dummy.max_link_length() ); + std::string length_solo_info = no_extensions ? "" : string_format( _( " (%d without extensions)" ), + cable_length != -1 ? cable_length : dummy.type->maximum_charges() ); + dump.emplace_back( "TOOL", length_all_info, length_solo_info ); + if( charge_rate != 0_W ) { - std::string wattage = string_format( _( "%+4.1f W" ), units::to_milliwatt( charge_rate ) / 1000.f ); - if( charge_rate > 0_W ) { - dump.emplace_back( "TOOL", _( "Charge rate: " ), wattage ); - } else { - dump.emplace_back( "TOOL", _( "Discharge rate: " ), wattage ); + //~ Power in Watts. %+4.1f is a 4 digit number with 1 decimal point (ex: 4737.3 W) + dump.emplace_back( "TOOL", _( "Wattage: " ), string_format( _( "%+4.1f W" ), + units::to_milliwatt( charge_rate ) / 1000.f ) ); + } + + if( !can_extend.empty() ) { + std::vector cable_types; + cable_types.reserve( can_extend.size() ); + for( const std::string &cable_type : can_extend ) { + cable_types.emplace_back( cable_type == "ELECTRICAL_DEVICES" ? _( "electrical device cables" ) : + itype_id( cable_type )->nname( 1 ) ); } + std::string cable_type_list = enumerate_as_string( cable_types, enumeration_conjunction::or_ ); + dump.emplace_back( "TOOL", string_format( _( "Can extend: %s" ), cable_type_list ) ); } } @@ -4375,168 +4393,167 @@ std::optional link_up_actor::use( Character *p, item &it, bool t, const tri return std::nullopt; } - if( !is_cable_item && !it.has_pocket_type( item_pocket::pocket_type::CABLE ) ) { - debugmsg( "Called a link_up action on an item (%s) without a CABLE pocket or \"is_cable_item: true\" set!", - it.tname() ); - return std::nullopt; - } - - // If the item is the cable, we can assign some variables now. - // Otherwise, wait until after target selection to create the cable and assign this pointer. - item *cable = nullptr; - const int respool_length = 5; - const int respool_time_per_square = 200; - bool is_respool_length = false; - if( is_cable_item ) { - cable = ⁢ - } else { - if( !it.get_contents().cables().empty() ) { - cable = it.get_contents().cables().front(); - } - } - if( cable != nullptr ) { - if( !cable->link ) { - cable->link = cata::make_value(); - } - is_respool_length = cable->link->max_length - cable->charges > respool_length; - if( cable->link->s_state == link_state::needs_reeling ) { - if( query_yn( is_cable_item ? string_format( _( "Reel in the %s?" ), it.label( 1 ) ) : - string_format( _( "Reel in the %s's cable?" ), it.label( 1 ) ) ) ) { - p->assign_activity( player_activity( reel_cable_activity_actor( ( cable->link->max_length - - cable->charges - respool_length ) * respool_time_per_square, item_location{*p, cable}, - is_cable_item ? item_location::nowhere : item_location{*p, &it} ) ) ); - } - return 0; - } - } - - if( it.contents_linked || ( cable != nullptr && !cable->link->has_state( link_state::no_link ) ) ) { - // Cables without any free ends can only be disconnected. - if( targets.count( link_state::no_link ) > 0 ) { - if( is_cable_item ) { - if( query_yn( string_format( _( "Detach and re-spool the %s?" ), it.label( 1 ) ) ) ) { - it.reset_cable( p ); - if( cable->link && cable->link->s_state == link_state::needs_reeling ) { - p->assign_activity( player_activity( reel_cable_activity_actor( ( cable->link->max_length - - cable->charges - respool_length ) * respool_time_per_square, item_location{*p, cable}, - is_cable_item ? item_location::nowhere : item_location{*p, &it} ) ) ); - } else { - p->add_msg_if_player( m_info, string_format( _( "You detach the %s." ), it.label( 1 ) ) ); - } - return 0; - } - } else { - if( query_yn( string_format( _( "Detach and re-spool the %s's cable?" ), it.label( 1 ) ) ) ) { - it.reset_cables( p ); - if( cable->link && cable->link->s_state == link_state::needs_reeling ) { - p->assign_activity( player_activity( reel_cable_activity_actor( ( cable->link->max_length - - cable->charges - respool_length ) * respool_time_per_square, item_location{*p, cable}, - is_cable_item ? item_location::nowhere : item_location{*p, &it} ) ) ); - } else { - p->add_msg_if_player( m_info, string_format( _( "You gather the cable up with the %s." ), - it.label( 1 ) ) ); - } - return 0; - } - } - } - return std::nullopt; - } - if( targets.empty() ) { debugmsg( "Link up action for %s doesn't have any targets!", it.tname() ); return std::nullopt; } - uilist link_menu; - if( cable == nullptr || cable->link->has_no_links() ) { - // Cable doesn't have any connections, or is a device cable: + const bool is_cable_item = it.has_flag( flag_CABLE_SPOOL ); + const std::string cable_name = is_cable_item ? it.type_name() : + string_format( _( "%s's cable" ), it.type_name() ); + + const int respool_threshold = 6; + const int respool_time_per_square = 200; + const int respool_time_total = !it.link || it.link->length < respool_threshold ? 0 : + ( it.link->length - respool_threshold ) * respool_time_per_square; + const bool past_respool_threshold = it.link_length() > respool_threshold; + const bool unspooled = it.link_length() == -1; + const bool has_loose_end = !unspooled && is_cable_item ? !it.link || + it.link->has_state( link_state::no_link ) : + !it.link || it.link->has_no_links(); - link_menu.text = is_cable_item ? string_format( _( "Attaching the %s:" ), it.label( 1 ) ) : - string_format( _( "Attaching the %s's cable:" ), it.label( 1 ) ); + uilist link_menu; + if( !is_cable_item || !it.link || it.link->has_no_links() ) { + // This is either a device or a cable item without any connections. + link_menu.text = string_format( _( "What to do with the %s?%s" ), + cable_name, it.link && it.link->t_veh_safe ? + string_format( _( "\nAttached to: %s" ), it.link->t_veh_safe->name ) : "" ); if( targets.count( link_state::vehicle_port ) > 0 ) { - link_menu.addentry( 0, true, -1, _( "Attach to vehicle controls or appliance" ) ); + link_menu.addentry( 0, has_loose_end, -1, _( "Attach to vehicle controls or appliance" ) ); } if( targets.count( link_state::vehicle_battery ) > 0 ) { - link_menu.addentry( 1, true, -1, _( "Attach to vehicle battery or appliance" ) ); + link_menu.addentry( 1, has_loose_end, -1, _( "Attach to vehicle battery or appliance" ) ); } if( targets.count( link_state::vehicle_tow ) > 0 ) { - link_menu.addentry( 10, true, -1, _( "Attach tow cable to towing vehicle" ) ); - link_menu.addentry( 11, true, -1, _( "Attach tow cable to towed vehicle" ) ); + link_menu.addentry( 10, has_loose_end, -1, _( "Attach tow cable to towing vehicle" ) ); + link_menu.addentry( 11, has_loose_end, -1, _( "Attach tow cable to towed vehicle" ) ); } if( targets.count( link_state::bio_cable ) > 0 ) { if( !p->get_remote_fueled_bionic().is_empty() ) { - link_menu.addentry( 20, true, -1, _( "Attach to Cable Charger System CBM" ) ); + link_menu.addentry( 20, has_loose_end, -1, _( "Attach to Cable Charger System CBM" ) ); } } if( targets.count( link_state::ups ) > 0 ) { if( !( p->all_items_with_flag( flag_IS_UPS ) ).empty() ) { - link_menu.addentry( 21, true, -1, _( "Attach to UPS" ) ); + link_menu.addentry( 21, has_loose_end, -1, _( "Attach to UPS" ) ); } } if( targets.count( link_state::solarpack ) > 0 ) { const bool has_solar_pack_on = p->worn_with_flag( flag_SOLARPACK_ON ); if( has_solar_pack_on || p->worn_with_flag( flag_SOLARPACK ) ) { - link_menu.addentry( 22, has_solar_pack_on, -1, _( "Attach to solar pack" ) ); + link_menu.addentry( 22, has_loose_end && has_solar_pack_on, -1, _( "Attach to solar pack" ) ); } } + if( !is_cable_item || !can_extend.empty() ) { + const bool has_extensions = !unspooled && + !it.all_items_top( item_pocket::pocket_type::CABLE ).empty(); + link_menu.addentry( 30, has_loose_end, -1, + is_cable_item ? _( "Extend another cable" ) : _( "Extend with another cable" ) ); + link_menu.addentry( 31, has_extensions, -1, _( "Remove cable extensions" ) ); + } if( targets.count( link_state::no_link ) > 0 ) { - link_menu.addentry( 999, false, -1, - is_respool_length ? _( "Detach and re-spool" ) : _( "Detach" ) ); + if( unspooled ) { + link_menu.addentry( 998, true, -1, _( "Re-spool" ) ); + } else { + link_menu.addentry( 999, !!it.link && !it.link->has_no_links(), -1, + past_respool_threshold ? _( "Detach and re-spool" ) : _( "Detach" ) ); + } } - } else if( cable != nullptr && cable->link->has_state( link_state::vehicle_tow ) ) { - // Cables that started a tow can finish one or detach, nothing else. + } else if( it.link->has_state( link_state::vehicle_tow ) ) { + // Cables that started a tow can finish one or detach; nothing else. + link_menu.text = string_format( _( "What to do with the %s?%s" ), cable_name, it.link->t_veh_safe ? + string_format( _( "\nAttached to: %s" ), it.link->t_veh_safe->name ) : "" ); - link_menu.addentry( 10, cable->link->t_state == link_state::vehicle_tow, -1, + link_menu.addentry( 10, has_loose_end && it.link->t_state == link_state::vehicle_tow, -1, _( "Attach loose end to towing vehicle" ) ); - link_menu.addentry( 11, cable->link->s_state == link_state::vehicle_tow, -1, + link_menu.addentry( 11, has_loose_end && it.link->s_state == link_state::vehicle_tow, -1, _( "Attach loose end to towed vehicle" ) ); if( targets.count( link_state::no_link ) > 0 ) { - link_menu.addentry( 999, true, -1, - is_respool_length ? _( "Detach and re-spool" ) : _( "Detach" ) ); + if( unspooled ) { + link_menu.addentry( 998, true, -1, _( "Re-spool" ) ); + } else { + link_menu.addentry( 999, true, -1, + past_respool_threshold ? _( "Detach and re-spool" ) : _( "Detach" ) ); + } } - } else if( is_cable_item ) { - // Cable has one connection already: - - link_menu.text = string_format( _( "Attaching the %s's loose end:" ), it.label( 1 ) ); + } else { + // This is a cable item with at least one connection already: + std::string state_desc_lhs; + std::string state_desc_rhs; + if( it.link->has_state( link_state::no_link ) ) { + state_desc_lhs = _( "\nAttached to " ); + if( it.link->t_veh_safe ) { + state_desc_rhs = it.link->t_veh_safe->name; + } else if( it.link->has_state( link_state::bio_cable ) ) { + state_desc_rhs = _( "Cable Charger System" ); + } else if( it.link->has_state( link_state::ups ) ) { + state_desc_rhs = _( "Unified Power Supply" ); + } else if( it.link->has_state( link_state::solarpack ) ) { + state_desc_rhs = _( "solar backpack" ); + } + } else { + if( it.link->s_state == link_state::bio_cable ) { + state_desc_lhs = _( "\nConnecting Cable Charger System to " ); + } else if( it.link->s_state == link_state::ups ) { + state_desc_lhs = _( "\nConnecting UPS to " ); + } else if( it.link->s_state == link_state::solarpack ) { + state_desc_lhs = _( "\nConnecting solar backpack to " ); + } + if( it.link->t_veh_safe ) { + state_desc_rhs = it.link->t_veh_safe->name; + } else if( it.link->t_state == link_state::bio_cable ) { + state_desc_rhs = _( "Cable Charger System" ); + } + } + link_menu.text = string_format( _( "What to do with the %s?%s%s" ), cable_name, + state_desc_lhs, state_desc_rhs ); // TODO: Allow plugging UPSes and Solar Packs into more than just bionics. // There is already code to support setting up a link, but none for actual functionality. if( targets.count( link_state::vehicle_port ) > 0 ) { - link_menu.addentry( 0, !cable->link->has_state( link_state::ups ) && - !cable->link->has_state( link_state::solarpack ), + link_menu.addentry( 0, has_loose_end && !it.link->has_state( link_state::ups ) && + !it.link->has_state( link_state::solarpack ), -1, _( "Attach loose end to vehicle controls or appliance" ) ); } if( targets.count( link_state::vehicle_battery ) > 0 ) { - link_menu.addentry( 1, !cable->link->has_state( link_state::ups ) && - !cable->link->has_state( link_state::solarpack ), + link_menu.addentry( 1, has_loose_end && !it.link->has_state( link_state::ups ) && + !it.link->has_state( link_state::solarpack ), -1, _( "Attach loose end to vehicle battery or appliance" ) ); } if( targets.count( link_state::bio_cable ) > 0 && !p->get_remote_fueled_bionic().is_empty() ) { - link_menu.addentry( 20, !cable->link->has_state( link_state::bio_cable ), + link_menu.addentry( 20, has_loose_end && !it.link->has_state( link_state::bio_cable ), -1, _( "Attach loose end to Cable Charger System CBM" ) ); } if( targets.count( link_state::ups ) > 0 && !( p->all_items_with_flag( flag_IS_UPS ) ).empty() ) { - link_menu.addentry( 21, cable->link->has_state( link_state::bio_cable ), + link_menu.addentry( 21, has_loose_end && it.link->has_state( link_state::bio_cable ), -1, _( "Attach loose end to UPS" ) ); } if( targets.count( link_state::solarpack ) > 0 ) { const bool has_solar_pack_on = p->worn_with_flag( flag_SOLARPACK_ON ); if( has_solar_pack_on || p->worn_with_flag( flag_SOLARPACK ) ) { - link_menu.addentry( 22, has_solar_pack_on && cable->link->has_state( link_state::bio_cable ), + link_menu.addentry( 22, has_loose_end && has_solar_pack_on && + it.link->has_state( link_state::bio_cable ), -1, _( "Attach loose end to solar pack" ) ); } } + if( !can_extend.empty() ) { + const bool has_extensions = !unspooled && + !it.all_items_top( item_pocket::pocket_type::CABLE ).empty(); + link_menu.addentry( 30, has_loose_end, -1, _( "Extend another cable" ) ); + link_menu.addentry( 31, has_extensions, -1, _( "Remove cable extensions" ) ); + } if( targets.count( link_state::no_link ) > 0 ) { - link_menu.addentry( 999, true, -1, - is_respool_length ? _( "Detach and re-spool" ) : _( "Detach" ) ); + if( unspooled ) { + link_menu.addentry( 998, true, -1, _( "Re-spool" ) ); + } else { + link_menu.addentry( 999, true, -1, + past_respool_threshold ? _( "Detach and re-spool" ) : _( "Detach" ) ); + } } - } else { - debugmsg( "An already connected device (%s) tried to link up again!", it.tname() ); - return std::nullopt; } + int choice = -1; if( targets.size() == 1 ) { choice = link_menu.entries.begin()->retval; @@ -4545,424 +4562,638 @@ std::optional link_up_actor::use( Character *p, item &it, bool t, const tri choice = link_menu.ret; } - if( choice < 0 ) { // Cancelled selection. - - p->add_msg_if_player( _( "Never mind" ) ); + if( choice < 0 ) { + // Cancelled selection. return std::nullopt; - } else if( choice == 999 ) { // Selection: Unconnect & respool. + } else if( choice >= 998 ) { + // Selection: Unconnect & respool. - if( is_cable_item ) { - it.reset_cable( p ); - } else { - it.reset_cables( p ); - } - if( cable->link && cable->link->s_state == link_state::needs_reeling ) { - // Cables that are too long need to be manually rewound before reuse. - // 2 seconds per square - p->assign_activity( player_activity( reel_cable_activity_actor( ( cable->link->max_length - - cable->charges - respool_length ) * respool_time_per_square, item_location{*p, cable}, - is_cable_item ? item_location::nowhere : item_location{*p, &it} ) ) ); + // Reopen the menu after respooling. + p->assign_activity( invoke_item_activity_actor( item_location{*p, &it}, "link_up" ) ); + p->activity.auto_resume = true; + + it.reset_link( p ); + // Cables that are too long need to be manually rewound before reuse. + if( it.link_length() == -1 ) { + p->assign_activity( player_activity( reel_cable_activity_actor( respool_time_total, item_location{*p, &it} ) ) ); return 0; } else { - p->add_msg_if_player( m_info, is_cable_item ? string_format( _( "You detach the %s." ), - it.label( 1 ) ) : string_format( _( "You gather the cable up with the %s." ), it.label( 1 ) ) ); + p->add_msg_if_player( m_info, string_format( is_cable_item ? _( "You detach the %s." ) : + _( "You gather the %s's cable up with it." ), it.type_name() ) ); } return 0; } - map &here = get_map(); - const int move_cost = 5; - // Lambda that assigns an address to the cable pointer, creating the cable if needed. Returns false if it failed. - const auto set_cable_pointer = [this, &it, &cable]() { - if( cable == nullptr ) { - item new_cable( type ); - if( !it.put_in( new_cable, item_pocket::pocket_type::CABLE ).success() ) { - debugmsg( "Failed to add the %s inside the %s!", new_cable.tname(), it.tname() ); - return false; - } - cable = it.get_contents().cables().front(); - cable->link = cata::make_value(); - } - return true; - }; - if( choice == 0 || choice == 1 ) { // Selection: Attach electrical cable to vehicle ports / appliances, OR vehicle batteries. + return link_to_veh_app( p, it, choice == 0 ); - // You used to be able to plug cables in anywhere on a vehicle, so there's extra effort here - // to inform players that they can only plug them into dashboards or electrical controls now. - const auto can_link = [&here, &choice]( const tripoint & point ) { - const optional_vpart_position ovp = here.veh_at( point ); - if( !ovp ) { - return false; - } - if( choice == 0 ) { - return ovp.avail_part_with_feature( "CABLE_PORTS" ) || ovp.avail_part_with_feature( "APPLIANCE" ); - } else if( choice == 1 ) { - if( ovp.avail_part_with_feature( "APPLIANCE" ) ) { - return true; - } - const vehicle &veh = ovp->vehicle(); - for( const int p : veh.parts_at_relative( ovp->mount(), /* use_cache = */ false ) ) { - const vehicle_part &vp_here = veh.part( p ); - if( vp_here.is_battery() && !vp_here.is_broken() ) { - return true; - } - } - } - return false; + } else if( choice == 10 || choice == 11 ) { + // Selection: Attach tow cable to towing/towed vehicle. + return link_tow_cable( p, it, choice == 10 ); + + } else if( choice == 30 ) { + // Selection: Attach to another cable, resulting in a longer one. + return link_extend_cable( p, it ); + + } else if( choice == 31 ) { + // Selection: Remove all cable extensions and give the individual cables to the player. + return remove_extensions( p, it ); + } + + map &here = get_map(); + + if( choice == 20 ) { + // Selection: Attach electrical cable to Cable Charger System CBM. + if( !it.link ) { + it.link = cata::make_value(); + } + if( it.link->has_no_links() ) { + it.link->t_state = link_state::bio_cable; + p->add_msg_if_player( m_info, _( "You attach the cable to your Cable Charger System." ) ); + } else if( it.link->s_state == link_state::ups ) { + it.link->t_state = link_state::bio_cable; + p->add_msg_if_player( m_good, _( "You are now plugged into the UPS." ) ); + } else if( it.link->s_state == link_state::solarpack ) { + it.link->t_state = link_state::bio_cable; + p->add_msg_if_player( m_good, _( "You are now plugged into the solar backpack." ) ); + } else if( it.link->t_state == link_state::vehicle_port || + it.link->t_state == link_state::vehicle_battery ) { + it.link->s_state = link_state::bio_cable; + p->add_msg_if_player( m_good, _( "You are now plugged into the vehicle." ) ); + } + it.set_link_traits(); + it.link->last_processed = calendar::turn; + p->moves -= move_cost; + it.process( here, p, p->pos() ); + return 0; + + } else if( choice == 21 ) { + // Selection: Attach electrical cable to ups. + item_location loc; + avatar *you = p->as_avatar(); + const std::string choose_ups = _( "Choose UPS:" ); + const std::string dont_have_ups = _( "You need an active UPS." ); + auto ups_filter = [&]( const item & itm ) { + return itm.has_flag( flag_IS_UPS ); }; - const std::optional pnt_ = choose_adjacent_highlight( _( "Attach the cable where?" ), - "", can_link, false, false ); - if( !pnt_ ) { + + if( you != nullptr ) { + loc = game_menus::inv::titled_filter_menu( ups_filter, *you, choose_ups, -1, dont_have_ups ); + } + if( !loc ) { + p->add_msg_if_player( _( "Never mind." ) ); return std::nullopt; } - const tripoint &pnt = *pnt_; - const optional_vpart_position t_vp = here.veh_at( pnt ); - if( !can_link( pnt ) ) { - if( choice == 0 && t_vp && t_vp->vehicle().has_part( "CABLE_PORTS" ) ) { - p->add_msg_if_player( m_info, - _( "You can't attach it there; try the dashboard or electronics controls." ) ); - } else if( choice == 1 && t_vp && t_vp->vehicle().batteries.empty() ) { - p->add_msg_if_player( m_info, - _( "You can't attach it there; try the battery." ) ); - } else { - p->add_msg_if_player( m_info, _( "You can't attach it there." ) ); - } - return std::nullopt; + if( !it.link ) { + it.link = cata::make_value(); } + if( it.link->has_no_links() ) { + p->add_msg_if_player( m_info, _( "You attach the cable to the UPS." ) ); + } else if( it.link->t_state == link_state::bio_cable ) { + p->add_msg_if_player( m_good, _( "You are now plugged into the UPS." ) ); + } else if( it.link->s_state == link_state::solarpack ) { + p->add_msg_if_player( m_good, _( "You link up the UPS and the solar backpack." ) ); + } else if( it.link->t_state == link_state::vehicle_port || + it.link->t_state == link_state::vehicle_battery ) { + p->add_msg_if_player( m_good, _( "You link up the UPS and the vehicle." ) ); + } + it.link->s_state = link_state::ups; + loc->set_var( "cable", "plugged_in" ); + it.set_link_traits(); + it.link->last_processed = calendar::turn; + p->moves -= move_cost; + it.process( here, p, p->pos() ); + return 0; - if( !set_cable_pointer() ) { + } else if( choice == 22 ) { + // Selection: Attach electrical cable to solar pack. + item_location loc; + avatar *you = p->as_avatar(); + const std::string choose_solar = _( "Choose solar pack:" ); + const std::string dont_have_solar = _( "You need an unfolded solar pack." ); + auto solar_filter = [&]( const item & itm ) { + return itm.has_flag( flag_SOLARPACK_ON ); + }; + + if( you != nullptr ) { + loc = game_menus::inv::titled_filter_menu( solar_filter, *you, choose_solar, -1, dont_have_solar ); + } + if( !loc ) { + p->add_msg_if_player( _( "Never mind." ) ); return std::nullopt; } - if( !cable->link->has_state( link_state::vehicle_port ) && - !cable->link->has_state( link_state::vehicle_battery ) ) { - // Starting a new connection to a vehicle or connecting a cable CBM to a vehicle. + if( !it.link ) { + it.link = cata::make_value(); + } + if( it.link->has_no_links() ) { + p->add_msg_if_player( m_info, _( "You attach the cable to the solar pack." ) ); + } else if( it.link->t_state == link_state::bio_cable ) { + p->add_msg_if_player( m_good, _( "You are now plugged into the solar pack." ) ); + } else if( it.link->s_state == link_state::ups ) { + p->add_msg_if_player( m_good, _( "You link up the solar pack and the UPS." ) ); + } else if( it.link->t_state == link_state::vehicle_port || + it.link->t_state == link_state::vehicle_battery ) { + p->add_msg_if_player( m_good, _( "You link up the solar pack and the vehicle." ) ); + } + it.link->s_state = link_state::solarpack; + loc->set_var( "cable", "plugged_in" ); + it.set_link_traits(); + it.link->last_processed = calendar::turn; + p->moves -= move_cost; + it.process( here, p, p->pos() ); + return 0; + } + return std::nullopt; +} - if( cable->link->has_no_links() ) { - p->add_msg_if_player( _( "You connect the %1$s to the %2$s." ), it.label( 1 ), - t_vp->vehicle().name ); - } else if( cable->link->has_state( link_state::bio_cable ) ) { - p->add_msg_if_player( m_good, _( "You are now plugged into the %s." ), t_vp->vehicle().name ); - cable->link->s_state = link_state::bio_cable; - } else { - debugmsg( "Failed to connect the %s, it tried to make an invalid connection!", cable->tname() ); - return std::nullopt; - } - cable->link->t_state = choice == 0 ? link_state::vehicle_port : link_state::vehicle_battery; - cable->link->t_abs_pos = here.getglobal( pnt ); - cable->link->t_mount = t_vp->mount(); - cable->link->max_length = cable_length != -1 ? cable_length : type->maximum_charges(); - cable->link->charge_efficiency = charge_efficiency; - cable->link->charge_rate = charge_rate.value(); - // Convert wattage to how long it takes to charge 1 kW, the unit batteries use. - // -1 means batteries won't be charged, but it can still provide epower to devices. - cable->link->charge_interval = charge_rate == 0_W ? -1 : - std::max( 1, static_cast( std::floor( 1000000.0 / abs( charge_rate.value() ) + 0.5 ) ) ); - cable->link->last_processed = calendar::turn; - cable->active = true; - it.contents_linked = !is_cable_item; - p->moves -= move_cost; - it.process( here, p, p->pos() ); +std::optional link_up_actor::link_to_veh_app( Character *p, item &it, + const bool to_ports ) const +{ + map &here = get_map(); + // Selection: Attach electrical cable to vehicle ports / appliances, OR vehicle batteries. - } else { - // Connecting one vehicle/appliance to another. + // You used to be able to plug cables in anywhere on a vehicle, so there's extra effort here + // to inform players that they can only plug them into dashboards or electrical controls now. + const auto can_link = [&here, &to_ports]( const tripoint & point ) { + const optional_vpart_position ovp = here.veh_at( point ); + if( !ovp ) { + return false; + } + if( to_ports ) { + return ovp.avail_part_with_feature( "CABLE_PORTS" ) || ovp.avail_part_with_feature( "APPLIANCE" ); + } + return ovp.avail_part_with_feature( "BATTERY" ) || ovp.avail_part_with_feature( "APPLIANCE" ); + }; + const std::optional pnt_ = choose_adjacent_highlight( _( "Attach the cable where?" ), + "", can_link, false, false ); + if( !pnt_ ) { + return std::nullopt; + } + const tripoint &selection = *pnt_; - if( !cable->link->t_veh_safe ) { - debugmsg( "Failed to connect the %s, it lost its vehicle pointer!", cable->tname() ); - return std::nullopt; - } - vehicle *const target_veh = &t_vp->vehicle(); - vehicle *const prev_veh = cable->link->t_veh_safe.get(); - if( prev_veh == target_veh ) { - p->add_msg_if_player( m_warning, _( "You cannot connect the %s to itself." ), prev_veh->name ); - return std::nullopt; - } - const std::pair prev_target = std::make_pair( - here.getabs( prev_veh->mount_to_tripoint( cable->link->t_mount ) ), - prev_veh->global_square_location().raw() ); - for( const vpart_reference &vpr : target_veh->get_any_parts( "POWER_TRANSFER" ) ) { - if( vpr.part().target.first == prev_target.first && - vpr.part().target.second == prev_target.second ) { - p->add_msg_if_player( m_warning, _( "The %1$s and %2$s are already connected." ), - target_veh->name, prev_veh->name ); - return std::nullopt; - } - } + const optional_vpart_position s_vp = here.veh_at( selection ); + if( !can_link( selection ) ) { + if( to_ports && s_vp && s_vp->vehicle().has_part( "CABLE_PORTS" ) ) { + p->add_msg_if_player( m_info, + _( "You can't attach it there - try the dashboard or electronics controls." ) ); + } else if( !to_ports && s_vp && !s_vp->vehicle().batteries.empty() ) { + p->add_msg_if_player( m_info, + _( "You can't attach it there - try the battery." ) ); + } else { + p->add_msg_if_player( m_info, _( "You can't attach it there." ) ); + } + return std::nullopt; + } - const itype_id item_id = it.typeId(); - bool vpid_found = false; - for( const vpart_info &vpi : vehicles::parts::get_all() ) { - if( vpi.base_item == item_id ) { - vpid_found = true; - break; - } - } + if( !it.link ) { + it.link = cata::make_value(); + } + if( !it.link->has_state( link_state::vehicle_port ) && + !it.link->has_state( link_state::vehicle_battery ) ) { + // Starting a new connection to a vehicle or connecting a cable CBM to a vehicle. - if( !vpid_found ) { - debugmsg( "item %s is not base item of any vehicle part! Using jumper_cable", item_id.c_str() ); - } - const vpart_id vpid( vpid_found ? item_id.str() : "jumper_cable" ); + // Get the part name for the connection message, using the vehicle name as a fallback. + std::string s_vp_name = s_vp->vehicle().name; + std::optional s_vp_ref; + if( ( s_vp_ref = s_vp.avail_part_with_feature( "APPLIANCE" ) ) || + ( s_vp_ref = s_vp.avail_part_with_feature( "CABLE_PORTS" ) ) || + ( s_vp_ref = s_vp.avail_part_with_feature( "BATTERY" ) ) ) { + s_vp_name = s_vp_ref->part().name( false ); + } - point vcoords = cable->link->t_mount; - vehicle_part source_part( vpid, vpid_found ? item( it ) : item( "jumper_cable" ) ); - source_part.target.first = here.getabs( pnt ); - source_part.target.second = target_veh->global_square_location().raw(); - prev_veh->install_part( vcoords, std::move( source_part ) ); + if( it.link->has_no_links() ) { + p->add_msg_if_player( _( "You connect the %1$s to the %2$s." ), it.type_name(), s_vp_name ); + } else if( it.link->has_state( link_state::bio_cable ) ) { + p->add_msg_if_player( m_good, _( "You are now plugged into the %s." ), s_vp_name ); + it.link->s_state = link_state::bio_cable; + } else { + debugmsg( "Failed to connect the %s, it tried to make an invalid connection!", it.tname() ); + return std::nullopt; + } - vcoords = t_vp->mount(); - vehicle_part target_part( vpid, vpid_found ? item( it ) : item( "jumper_cable" ) ); - target_part.target.first = prev_target.first; - target_part.target.second = prev_target.second; - target_veh->install_part( vcoords, std::move( target_part ) ); + it.link->t_state = to_ports ? link_state::vehicle_port : link_state::vehicle_battery; + it.link->t_abs_pos = here.getglobal( s_vp->vehicle().global_pos3() ); + it.link->t_mount = s_vp->mount(); + it.set_link_traits(); + it.link->last_processed = calendar::turn; + p->moves -= move_cost; + it.process( here, p, p->pos() ); + return 0; - p->add_msg_if_player( m_good, _( "You link up the %1$s and the %2$s." ), - prev_veh->name, target_veh->name ); + } else { + // Connecting one vehicle/appliance to another. - return 1; // Let the cable be destroyed. + if( !it.link->t_veh_safe ) { + vehicle *found_veh = vehicle::find_vehicle( it.link->t_abs_pos ); + if( found_veh ) { + it.link->t_veh_safe = found_veh->get_safe_reference(); + } else { + debugmsg( "Failed to connect the %s, it lost its vehicle pointer!", it.tname() ); + return std::nullopt; + } } - - } else if( choice == 10 || choice == 11 ) { // Selection: Attach tow cable to towing/towed vehicle. - - if( !set_cable_pointer() ) { + vehicle *const sel_veh = &s_vp->vehicle(); + vehicle *const prev_veh = it.link->t_veh_safe.get(); + if( prev_veh == sel_veh ) { + p->add_msg_if_player( m_warning, _( "You cannot connect the %s to itself." ), prev_veh->name ); return std::nullopt; } - const auto can_link = [&here]( const tripoint & point ) { - const optional_vpart_position ovp = here.veh_at( point ); - return ovp && ovp->vehicle().is_external_part( point ); - }; + // Prepare target tripoints for the cable parts that'll be added to the selected/previous vehicles + const std::pair prev_part_target = std::make_pair( + here.getabs( selection ), + sel_veh->global_square_location().raw() ); + const std::pair sel_part_target = std::make_pair( + ( it.link->t_abs_pos + prev_veh->coord_translate( it.link->t_mount ) ).raw(), + it.link->t_abs_pos.raw() ); - const std::optional pnt_ = choose_adjacent_highlight( - choice == 10 ? _( "Attach cable to the vehicle that will do the towing." ) : - _( "Attach cable to the vehicle that will be towed." ), "", can_link, false, false ); - if( !pnt_ ) { - return std::nullopt; + for( const vpart_reference &vpr : sel_veh->get_any_parts( "POWER_TRANSFER" ) ) { + if( vpr.part().target.first == prev_part_target.first && + vpr.part().target.second == prev_part_target.second ) { + p->add_msg_if_player( m_warning, _( "The %1$s and %2$s are already connected." ), + sel_veh->name, prev_veh->name ); + return std::nullopt; + } } - const tripoint &pnt = *pnt_; - const optional_vpart_position t_vp = here.veh_at( pnt ); - if( !t_vp ) { - p->add_msg_if_player( _( "There's no vehicle there." ) ); + + if( trigdist ? trig_dist( prev_part_target.first, sel_part_target.first ) > it.link->max_length : + square_dist( prev_part_target.first, sel_part_target.first ) > it.link->max_length ) { + p->add_msg_if_player( m_warning, _( "The %1$s can't stretch that far!" ), it.type_name() ); return std::nullopt; } - vehicle *const target_veh = &t_vp->vehicle(); - if( target_veh->has_tow_attached() || target_veh->is_towed() || - target_veh->is_towing() ) { - p->add_msg_if_player( _( "That vehicle already has a tow-line attached." ) ); + const itype_id item_id = it.typeId(); + vpart_id vpid = vpart_id::NULL_ID(); + for( const vpart_info &e : vehicles::parts::get_all() ) { + if( e.base_item == item_id ) { + vpid = e.id; + break; + } + } + + if( vpid.is_null() ) { + debugmsg( "item %s is not base item of any vehicle part!", item_id.c_str() ); return std::nullopt; } - if( !target_veh->is_external_part( pnt ) ) { - p->add_msg_if_player( _( "You can't attach the tow-line to an internal part." ) ); + + const point vcoords1 = it.link->t_mount; + const point vcoords2 = s_vp->mount(); + + const ret_val can_mount1 = prev_veh->can_mount( vcoords1, *vpid ); + if( !can_mount1.success() ) { + //~ %1$s - cable name, %2$s - the reason why it failed + p->add_msg_if_player( m_bad, _( "You can't attach the %1$s: %2$s" ), + it.type_name(), can_mount1.str() ); return std::nullopt; } - if( !target_veh->part( t_vp->part_index() ).carried_stack.empty() ) { - p->add_msg_if_player( _( "You can't attach the tow-line to a racked part." ) ); + const ret_val can_mount2 = sel_veh->can_mount( vcoords2, *vpid ); + if( !can_mount2.success() ) { + //~ %1$s - cable name, %2$s - the reason why it failed + p->add_msg_if_player( m_bad, _( "You can't attach the %1$s: %2$s" ), + it.type_name(), can_mount2.str() ); return std::nullopt; } - if( cable->link->has_no_links() ) { - // Starting a new tow cable connection. + vehicle_part prev_veh_part( vpid, item( it ) ); + prev_veh_part.target.first = prev_part_target.first; + prev_veh_part.target.second = prev_part_target.second; + prev_veh->install_part( vcoords1, std::move( prev_veh_part ) ); + prev_veh->precalc_mounts( 1, prev_veh->pivot_rotation[1], prev_veh->pivot_anchor[1] ); - p->add_msg_if_player( _( "You connect the %1$s to the %2$s." ), it.label( 1 ), - t_vp->vehicle().name ); - if( choice == 10 ) { - cable->link->s_state = link_state::vehicle_tow; // Assign towing vehicle. - } else { - cable->link->t_state = link_state::vehicle_tow; // Assign towed vehicle. - } - //cable->link->t_state = link_state::vehicle_tow; - cable->link->t_abs_pos = here.getglobal( pnt ); - cable->link->t_mount = t_vp->mount(); - cable->link->max_length = cable_length != -1 ? cable_length : type->maximum_charges(); - cable->link->last_processed = calendar::turn; - cable->active = true; - it.contents_linked = !is_cable_item; - p->moves -= move_cost; - it.process( here, p, p->pos() ); + vehicle_part sel_veh_part( vpid, item( it ) ); + sel_veh_part.target.first = sel_part_target.first; + sel_veh_part.target.second = sel_part_target.second; + sel_veh->install_part( vcoords2, std::move( sel_veh_part ) ); + sel_veh->precalc_mounts( 1, sel_veh->pivot_rotation[1], sel_veh->pivot_anchor[1] ); - } else { - // Connecting two vehicles with tow cable. + if( p->has_item( it ) ) { + //~ %1$s - first vehicle name, %2$s - second vehicle name - %3$s - cable name, + p->add_msg_if_player( m_good, _( "You connect %1$s and %2$s with the %3$s." ), + prev_veh->disp_name(), sel_veh->disp_name(), it.type_name() ); + } + if( it.typeId() != itype_power_cord ) { + // Remove linked_flag from attached parts - the just-added cable vehicle parts do the same thing. + it.reset_link( p ); + } + p->moves -= move_cost; + return 1; // Let the cable be destroyed. + } +} - if( !cable->link->t_veh_safe ) { - debugmsg( "Failed to connect the %s, it lost its vehicle pointer!", cable->tname() ); - return std::nullopt; - } - vehicle *const prev_veh = cable->link->t_veh_safe.get(); - if( prev_veh == target_veh ) { - if( p->has_item( it ) ) { - p->add_msg_if_player( m_warning, _( "The %s cannot tow itself!" ), prev_veh->name ); - } - return std::nullopt; - }; - - const itype_id item_id = it.typeId(); - vpart_id vpid = vpart_id::NULL_ID(); - for( const vpart_info &e : vehicles::parts::get_all() ) { - if( e.base_item == item_id ) { - vpid = e.id; - break; - } - } +std::optional link_up_actor::link_tow_cable( Character *p, item &it, + const bool to_towing ) const +{ + map &here = get_map(); - if( vpid.is_null() ) { - debugmsg( "item %s is not base item of any vehicle part!", item_id.c_str() ); - return std::nullopt; - } + const auto can_link = [&here]( const tripoint & point ) { + const optional_vpart_position ovp = here.veh_at( point ); + return ovp && ovp->vehicle().is_external_part( point ); + }; - const point vcoords1 = cable->link->t_mount; - const point vcoords2 = t_vp->mount(); + const std::optional pnt_ = choose_adjacent_highlight( + to_towing ? _( "Attach cable to the vehicle that will do the towing." ) : + _( "Attach cable to the vehicle that will be towed." ), "", can_link, false, false ); + if( !pnt_ ) { + return std::nullopt; + } + const tripoint &selection = *pnt_; + const optional_vpart_position s_vp = here.veh_at( selection ); + if( !s_vp ) { + p->add_msg_if_player( _( "There's no vehicle there." ) ); + return std::nullopt; + } - const ret_val can_mount1 = prev_veh->can_mount( vcoords1, *vpid ); - if( !can_mount1.success() ) { - //~ %1$s - tow cable name, %2$s - the reason why it failed - p->add_msg_if_player( m_bad, _( "You can't attach the %1$s: %2$s" ), - it.type_name(), can_mount1.str() ); - return std::nullopt; - } + vehicle *const sel_veh = &s_vp->vehicle(); + if( sel_veh->has_tow_attached() || sel_veh->is_towed() || + sel_veh->is_towing() ) { + p->add_msg_if_player( _( "That vehicle already has a tow-line attached." ) ); + return std::nullopt; + } + if( !sel_veh->is_external_part( selection ) ) { + p->add_msg_if_player( _( "You can't attach the tow-line to an internal part." ) ); + return std::nullopt; + } + if( !sel_veh->part( s_vp->part_index() ).carried_stack.empty() ) { + p->add_msg_if_player( _( "You can't attach the tow-line to a racked part." ) ); + return std::nullopt; + } - const ret_val can_mount2 = target_veh->can_mount( vcoords2, *vpid ); - if( !can_mount2.success() ) { - //~ %1$s - tow cable name, %2$s - the reason why it failed - p->add_msg_if_player( m_bad, _( "You can't attach the %1$s: %2$s" ), - it.type_name(), can_mount2.str() ); - return std::nullopt; - } + if( !it.link ) { + it.link = cata::make_value(); + } + if( it.link->has_no_links() ) { + // Starting a new tow cable connection. - vehicle_part prev_part( vpid, item( it ) ); - prev_part.target.first = here.getabs( pnt ); - prev_part.target.second = target_veh->global_square_location().raw(); - prev_veh->install_part( vcoords1, std::move( prev_part ) ); + p->add_msg_if_player( _( "You connect the %1$s to the %2$s." ), it.type_name(), + s_vp->vehicle().name ); + if( to_towing ) { + it.link->s_state = link_state::vehicle_tow; // Assign towing vehicle. + } else { + it.link->t_state = link_state::vehicle_tow; // Assign towed vehicle. + } + it.link->t_abs_pos = here.getglobal( s_vp->vehicle().global_pos3() ); + it.link->t_mount = s_vp->mount(); + it.link->max_length = cable_length != -1 ? cable_length : it.type->maximum_charges(); + it.set_link_traits(); + it.link->last_processed = calendar::turn; + p->moves -= move_cost; + it.process( here, p, p->pos() ); + return 0; - vehicle_part target_part( vpid, item( it ) ); - target_part.target.first = here.getabs( prev_veh->mount_to_tripoint( cable->link->t_mount ) ); - target_part.target.second = prev_veh->global_square_location().raw(); - target_veh->install_part( vcoords2, std::move( target_part ) ); + } else { + // Connecting two vehicles with tow cable. - if( p->has_item( it ) ) { - //~ %1$s - tow cable name, %2$s - first vehicle name, %3$s - second vehicle name - p->add_msg_if_player( m_good, _( "You attach %1$s to %2$s and %3$s." ), - it.type_name(), prev_veh->disp_name(), target_veh->disp_name() ); - } - if( choice == 10 ) { - target_veh->tow_data.set_towing( target_veh, prev_veh ); + if( !it.link->t_veh_safe ) { + vehicle *found_veh = vehicle::find_vehicle( it.link->t_abs_pos ); + if( found_veh ) { + it.link->t_veh_safe = found_veh->get_safe_reference(); } else { - prev_veh->tow_data.set_towing( prev_veh, target_veh ); + debugmsg( "Failed to connect the %s, it lost its vehicle pointer!", it.tname() ); + return std::nullopt; } + } + vehicle *const prev_veh = it.link->t_veh_safe.get(); + if( prev_veh == sel_veh ) { + if( p->has_item( it ) ) { + p->add_msg_if_player( m_warning, _( "The %s cannot tow itself!" ), prev_veh->name ); + } + return std::nullopt; + }; - return 1; // Let the cable be destroyed. + // Prepare target tripoints for the cable parts that'll be added to the selected/previous vehicles + const std::pair prev_part_target = std::make_pair( + here.getabs( selection ), + sel_veh->global_square_location().raw() ); + const std::pair sel_part_target = std::make_pair( + ( it.link->t_abs_pos + prev_veh->coord_translate( it.link->t_mount ) ).raw(), + it.link->t_abs_pos.raw() ); + + if( trigdist ? trig_dist( prev_part_target.first, sel_part_target.first ) > it.link->max_length : + square_dist( prev_part_target.first, sel_part_target.first ) > it.link->max_length ) { + p->add_msg_if_player( m_warning, _( "The %1$s can't stretch that far!" ), it.type_name() ); + return std::nullopt; } - } else if( choice == 20 ) { // Selection: Attach electrical cable to Cable Charger System CBM. + const itype_id item_id = it.typeId(); + vpart_id vpid = vpart_id::NULL_ID(); + for( const vpart_info &e : vehicles::parts::get_all() ) { + if( e.base_item == item_id ) { + vpid = e.id; + break; + } + } - if( !set_cable_pointer() ) { + if( vpid.is_null() ) { + debugmsg( "item %s is not base item of any vehicle part!", item_id.c_str() ); return std::nullopt; } - if( cable->link->has_no_links() ) { - cable->link->t_state = link_state::bio_cable; - p->add_msg_if_player( m_info, _( "You attach the cable to your Cable Charger System." ) ); - } else if( cable->link->s_state == link_state::ups ) { - cable->link->t_state = link_state::bio_cable; - p->add_msg_if_player( m_good, _( "You are now plugged into the UPS." ) ); - } else if( cable->link->s_state == link_state::solarpack ) { - cable->link->t_state = link_state::bio_cable; - p->add_msg_if_player( m_good, _( "You are now plugged into the solar backpack." ) ); - } else if( cable->link->t_state == link_state::vehicle_port || - cable->link->t_state == link_state::vehicle_battery ) { - cable->link->s_state = link_state::bio_cable; - p->add_msg_if_player( m_good, _( "You are now plugged into the vehicle." ) ); - } - cable->active = true; - it.contents_linked = !is_cable_item; - p->moves -= move_cost; - it.process( here, p, p->pos() ); - return 0; - } else if( choice == 21 ) { // Selection: Attach electrical cable to ups. + const point vcoords1 = it.link->t_mount; + const point vcoords2 = s_vp->mount(); - item_location loc; - avatar *you = p->as_avatar(); - const std::string choose_ups = _( "Choose UPS:" ); - const std::string dont_have_ups = _( "You don't have any UPS." ); - auto ups_filter = [&]( const item & itm ) { - return itm.has_flag( flag_IS_UPS ); - }; - - if( you != nullptr ) { - loc = game_menus::inv::titled_filter_menu( ups_filter, *you, choose_ups, -1, dont_have_ups ); + const ret_val can_mount1 = prev_veh->can_mount( vcoords1, *vpid ); + if( !can_mount1.success() ) { + //~ %1$s - tow cable name, %2$s - the reason why it failed + p->add_msg_if_player( m_bad, _( "You can't attach the %1$s: %2$s" ), + it.type_name(), can_mount1.str() ); + return std::nullopt; } - if( !loc ) { - p->add_msg_if_player( _( "Never mind" ) ); + const ret_val can_mount2 = sel_veh->can_mount( vcoords2, *vpid ); + if( !can_mount2.success() ) { + //~ %1$s - tow cable name, %2$s - the reason why it failed + p->add_msg_if_player( m_bad, _( "You can't attach the %1$s: %2$s" ), + it.type_name(), can_mount2.str() ); return std::nullopt; } - if( !set_cable_pointer() ) { - return std::nullopt; + vehicle_part prev_veh_part( vpid, item( it ) ); + prev_veh_part.target.first = prev_part_target.first; + prev_veh_part.target.second = prev_part_target.second; + prev_veh->install_part( vcoords1, std::move( prev_veh_part ) ); + prev_veh->precalc_mounts( 1, prev_veh->pivot_rotation[1], prev_veh->pivot_anchor[1] ); + + vehicle_part sel_veh_part( vpid, item( it ) ); + sel_veh_part.target.first = sel_part_target.first; + sel_veh_part.target.second = sel_part_target.second; + sel_veh->install_part( vcoords2, std::move( sel_veh_part ) ); + sel_veh->precalc_mounts( 1, sel_veh->pivot_rotation[1], sel_veh->pivot_anchor[1] ); + + if( p->has_item( it ) ) { + //~ %1$s - first vehicle name, %2$s - second vehicle name - %3$s - tow cable name, + p->add_msg_if_player( m_good, _( "You connect the %1$s and %2$s with the %3$s." ), + prev_veh->disp_name(), sel_veh->disp_name(), it.type_name() ); } - if( cable->link->has_no_links() ) { - p->add_msg_if_player( m_info, _( "You attach the cable to the UPS." ) ); - } else if( cable->link->t_state == link_state::bio_cable ) { - p->add_msg_if_player( m_good, _( "You are now plugged into the UPS." ) ); - } else if( cable->link->s_state == link_state::solarpack ) { - p->add_msg_if_player( m_good, _( "You link up the UPS and the solar backpack." ) ); - } else if( cable->link->t_state == link_state::vehicle_port || - cable->link->t_state == link_state::vehicle_battery ) { - p->add_msg_if_player( m_good, _( "You link up the UPS and the vehicle." ) ); + if( to_towing ) { + sel_veh->tow_data.set_towing( sel_veh, prev_veh ); + } else { + prev_veh->tow_data.set_towing( prev_veh, sel_veh ); + } + if( it.typeId() != itype_power_cord ) { + // Remove linked_flag from attached parts - the just-added cable vehicle parts do the same thing. + it.reset_link( p ); } - cable->link->s_state = link_state::ups; - loc->set_var( "cable", "plugged_in" ); - loc->activate(); - cable->active = true; - it.contents_linked = !is_cable_item; p->moves -= move_cost; - it.process( here, p, p->pos() ); - return 0; + return 1; // Let the cable be destroyed. + } +} - } else if( choice == 22 ) { // Selection: Attach electrical cable to solar pack. +std::optional link_up_actor::link_extend_cable( Character *p, item &it ) const +{ + avatar *you = p->as_avatar(); + if( !you ) { + p->add_msg_if_player( m_info, _( "Never mind." ) ); + return std::nullopt; + } - item_location loc; - avatar *you = p->as_avatar(); - const std::string choose_solar = _( "Choose solar pack:" ); - const std::string dont_have_solar = _( "You need an unfolded solar pack." ); - auto solar_filter = [&]( const item & itm ) { - return itm.has_flag( flag_SOLARPACK_ON ); + const bool is_cable_item = it.has_flag( flag_CABLE_SPOOL ); + item_location selected; + if( is_cable_item ) { + const bool can_extend_devices = can_extend.find( "ELECTRICAL_DEVICES" ) != can_extend.end(); + const auto filter = [this, &it, &can_extend_devices]( const item & inv ) { + if( inv.link && ( it.link_length() >= 0 || inv.link->has_state( link_state::needs_reeling ) ) ) { + return false; + } + if( !inv.has_flag( flag_CABLE_SPOOL ) ) { + return can_extend_devices && inv.type->can_use( "link_up" ); + } + return can_extend.find( inv.typeId().c_str() ) != can_extend.end() && &inv != ⁢ }; + selected = game_menus::inv::titled_filter_menu( filter, *you, _( "Extend which cable?" ), -1, + _( "You don't have a compatible cable." ) ); + } else { + const auto filter = [&it]( const item & inv ) { + if( !inv.has_flag( flag_CABLE_SPOOL ) || !inv.type->can_use( "link_up" ) || + ( inv.link && ( it.link_length() >= 0 || inv.link->has_state( link_state::needs_reeling ) ) ) ) { + return false; + } + const link_up_actor *actor = static_cast + ( inv.get_use( "link_up" )->get_actor_ptr() ); + return actor->can_extend.find( "ELECTRICAL_DEVICES" ) != actor->can_extend.end(); + }; + selected = game_menus::inv::titled_filter_menu( filter, *you, _( "Extend with which cable?" ), -1, + _( "You don't have a compatible cable." ) ); + } + if( !selected ) { + p->add_msg_if_player( m_info, _( "Never mind." ) ); + return std::nullopt; + } - if( you != nullptr ) { - loc = game_menus::inv::titled_filter_menu( solar_filter, *you, choose_solar, -1, dont_have_solar ); - } - if( !loc ) { - p->add_msg_if_player( _( "Never mind" ) ); - return std::nullopt; - } + item_location extension = is_cable_item ? form_loc( *p, tripoint_min, it ) : selected; + item_location extended = is_cable_item ? selected : form_loc( *p, tripoint_min, it ); + std::optional extended_copy; - if( !set_cable_pointer() ) { - return std::nullopt; + // We'll make a copy of the extended item and check pocket weight/volume capacity if: + // 1. The extended item is in a container, + // 2. The extended item and extension cord(s) aren't in the same pocket, and + // 3. The extended item is in a pocket without enough remaining room for the extension cord(s). + if( extended.where() == item_location::type::container && + extended.parent_pocket() != extension.parent_pocket() && + ( extended.volume_capacity() - extension->volume() < 0_ml || + extended.weight_capacity() - extension->weight() < 0_gram ) ) { + + extended_copy = *extended; + } + + item *extended_ptr = extended_copy ? &extended_copy.value() : &*extended; + + // Put the extension cable and all of its attached cables, if any, into the extended item's CABLE pocket. + std::vector all_cables = extension->cables(); + all_cables.emplace_back( &*extension ); + for( const item *cable : all_cables ) { + item cable_copy( *cable ); + cable_copy.get_contents().clear_items(); + cable_copy.link.reset(); + if( !extended_ptr->put_in( cable_copy, item_pocket::pocket_type::CABLE ).success() ) { + debugmsg( "Failed to put %s inside %s!", cable_copy.type_name(), extended_ptr->type_name() ); } - if( cable->link->has_no_links() ) { - p->add_msg_if_player( m_info, _( "You attach the cable to the solar pack." ) ); - } else if( cable->link->t_state == link_state::bio_cable ) { - p->add_msg_if_player( m_good, _( "You are now plugged into the solar pack." ) ); - } else if( cable->link->s_state == link_state::ups ) { - p->add_msg_if_player( m_good, _( "You link up the solar pack and the UPS." ) ); - } else if( cable->link->t_state == link_state::vehicle_port || - cable->link->t_state == link_state::vehicle_battery ) { - p->add_msg_if_player( m_good, _( "You link up the solar pack and the vehicle." ) ); + } + if( !extended_ptr->link ) { + extended_ptr->link = cata::make_value(); + } + if( extension->link ) { + extended_ptr->link = extension->link; + } + extended_ptr->set_link_traits(); + + if( extended_copy ) { + // Check if there's another pocket on the same container that can hold the extended item, respecting pocket settings. + if( extended.has_parent() && + extended.parent_item()->can_contain( *extended_ptr, false, false, false, + item_location(), 10000000_ml, false ).success() ) { + if( !extended.parent_item()->put_in( *extended_ptr, + item_pocket::pocket_type::CONTAINER ).success() ) { + debugmsg( "Failed to put %s inside %s!", extended_ptr->type_name(), + extended.parent_item()->type_name() ); + return std::nullopt; + } + extended.remove_item(); + } else { + if( !query_yn( _( "The %1$s can't contain the %2$s with the %3$s attached. Continue?" ), + extended.parent_item()->type_name(), extended_ptr->type_name(), extension->type_name() ) ) { + return std::nullopt; + } + extended.parent_pocket()->add( *extended_ptr ); + extended.remove_item(); } - cable->link->s_state = link_state::solarpack; - loc->set_var( "cable", "plugged_in" ); - loc->activate(); - cable->active = true; - cable->process( here, p, p->pos() ); - it.contents_linked = !is_cable_item; - p->moves -= move_cost; - it.process( here, p, p->pos() ); + } + + p->add_msg_if_player( is_cable_item ? _( "You extend the %1$s with the %2$s." ) : + _( "You extend the %1$s's cable with the %2$s." ), + extended_ptr->type_name(), extension->type_name() ); + extension.remove_item(); + p->invalidate_inventory_validity_cache(); + p->moves -= move_cost; + return 0; +} + +std::optional link_up_actor::remove_extensions( Character *p, item &it ) const +{ + std::list all_cables = it.all_items_ptr( item_pocket::pocket_type::CABLE ); + all_cables.remove_if( []( const item * cable ) { + return !cable->has_flag( flag_CABLE_SPOOL ) || !cable->type->can_use( "link_up" ); + } ); + + if( all_cables.empty() ) { + // Delete any non-cables that somehow got into the pocket. + it.get_contents().clear_pockets_if( []( item_pocket const & pocket ) { + return pocket.is_type( item_pocket::pocket_type::CABLE ); + } ); return 0; } + item cable_main_copy( *all_cables.back() ); + all_cables.pop_back(); + if( !all_cables.empty() ) { + for( item *cable : all_cables ) { + item cable_copy( *cable ); + cable_copy.get_contents().clear_items(); + cable_copy.link.reset(); + if( !cable_main_copy.put_in( cable_copy, item_pocket::pocket_type::CABLE ).success() ) { + debugmsg( "Failed to put %s inside %s!", cable_copy.tname(), cable_main_copy.tname() ); + } + } + } + p->add_msg_if_player( _( "You disconnect the %1$s from the %2$s." ), + cable_main_copy.type_name(), it.type_name() ); + + it.get_contents().clear_pockets_if( []( item_pocket const & pocket ) { + return pocket.is_type( item_pocket::pocket_type::CABLE ); + } ); + + if( it.link ) { + // If the item was linked, keep the extension cables linked. + cable_main_copy.link = it.link; + cable_main_copy.set_link_traits(); + cable_main_copy.process( get_map(), p, p->pos() ); + it.reset_link( p ); + } + + p->i_add_or_drop( cable_main_copy ); + p->moves -= move_cost; return 0; } diff --git a/src/iuse_actor.h b/src/iuse_actor.h index 617549074d80e..ff4b9b8992354 100644 --- a/src/iuse_actor.h +++ b/src/iuse_actor.h @@ -1056,20 +1056,24 @@ class modify_gunmods_actor : public iuse_actor class link_up_actor : public iuse_actor { public: - /** True if the link_up action is called by the cable item itself, rather than by a device. */ - bool is_cable_item = false; - /** The type of cable created with this action */ - itype_id type = itype_id( "generic_device_cable" ); /** Maximum length of the cable. At -1, will use the item type's max_charges. */ int cable_length = -1; /** Charge rate in watts */ units::power charge_rate = 0_W; - /** one_in(this) chance to fail adding 1 charge */ - int charge_efficiency = 7; - /** (Optional) Text displayed in the activation screen, defaults to "Plug in / Unplug". */ + /** (this) out of 1.0 chance to successfully add 1 charge every charge interval */ + float efficiency = 0.85f; + /** (Optional) The move cost to attach the cable. */ + int move_cost = 5; + /** (Optional) Text displayed in the activation screen, defaults to "Plug in / Manage cables". */ translation menu_text; std::set targets = { link_state::no_link, link_state::vehicle_port }; + std::set can_extend = {}; + + std::optional link_to_veh_app( Character *p, item &it, bool to_ports ) const; + std::optional link_tow_cable( Character *p, item &it, bool to_towing ) const; + std::optional link_extend_cable( Character *p, item &it ) const; + std::optional remove_extensions( Character *p, item &it ) const; link_up_actor() : iuse_actor( "link_up" ) {} diff --git a/src/map.cpp b/src/map.cpp index daccb671385be..17bb7bd211e45 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -1523,7 +1523,7 @@ bool map::displace_vehicle( vehicle &veh, const tripoint &dp, const bool adjust_ } } - veh.shed_loose_parts( &src, &dst ); + veh.shed_loose_parts( trinary::SOME, &dst ); smzs = veh.advance_precalc_mounts( dst_offset, src.raw(), dp, ramp_offset, adjust_pos, parts_to_move ); veh.update_active_fakes(); @@ -5050,7 +5050,7 @@ item &map::add_item( const tripoint &p, item new_item ) // Process foods and temperature tracked items when they are added to the map, here instead of add_item_at() // to avoid double processing food during active item processing. - if( new_item.has_temperature() && !new_item.is_corpse() ) { + if( new_item.link || ( new_item.has_temperature() && !new_item.is_corpse() ) ) { new_item.process( *this, nullptr, p ); } diff --git a/src/player_activity.cpp b/src/player_activity.cpp index d42bb1648fd2c..f9363e15d8d3e 100644 --- a/src/player_activity.cpp +++ b/src/player_activity.cpp @@ -42,6 +42,7 @@ static const activity_id ACT_CONSUME_MEDS_MENU( "ACT_CONSUME_MEDS_MENU" ); static const activity_id ACT_EAT_MENU( "ACT_EAT_MENU" ); static const activity_id ACT_HACKSAW( "ACT_HACKSAW" ); static const activity_id ACT_HEATING( "ACT_HEATING" ); +static const activity_id ACT_INVOKE_ITEM( "ACT_INVOKE_ITEM" ); static const activity_id ACT_JACKHAMMER( "ACT_JACKHAMMER" ); static const activity_id ACT_MIGRATION_CANCEL( "ACT_MIGRATION_CANCEL" ); static const activity_id ACT_NULL( "ACT_NULL" ); @@ -147,6 +148,7 @@ std::optional player_activity::get_progress_message( const avatar & type == ACT_CONSUME_FOOD_MENU || type == ACT_CONSUME_MEDS_MENU || type == ACT_EAT_MENU || + type == ACT_INVOKE_ITEM || type == ACT_PICKUP_MENU || type == ACT_VIEW_RECIPE ) { return std::nullopt; diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 98c9af2b5a3c7..5914508845df7 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -2866,11 +2866,12 @@ void item::link_data::serialize( JsonOut &jsout ) const jsout.member( "link_t_state", t_state ); jsout.member( "link_t_abs_pos", t_abs_pos ); jsout.member( "link_t_mount", t_mount ); + jsout.member( "link_length", length ); jsout.member( "link_max_length", max_length ); jsout.member( "link_last_processed", last_processed ); jsout.member( "link_charge_rate", charge_rate ); jsout.member( "link_charge_interval", charge_interval ); - jsout.member( "link_charge_efficiency", charge_efficiency ); + jsout.member( "link_charge_efficiency", efficiency ); jsout.end_object(); } @@ -2882,11 +2883,12 @@ void item::link_data::deserialize( const JsonObject &data ) data.read( "link_t_state", t_state ); data.read( "link_t_abs_pos", t_abs_pos ); data.read( "link_t_mount", t_mount ); - max_length = data.get_int( "link_max_length" ); + data.read( "link_length", length ); + data.read( "link_max_length", max_length ); data.read( "link_last_processed", last_processed ); - charge_rate = data.get_int( "link_charge_rate" ); - charge_interval = data.get_int( "link_charge_interval" ); - charge_efficiency = data.get_int( "link_charge_efficiency" ); + data.read( "link_charge_rate", charge_rate ); + data.read( "link_charge_efficiency", efficiency ); + data.read( "link_charge_interval", charge_interval ); } // Template parameter because item::craft_data is private and I don't want to make it public. @@ -2977,7 +2979,6 @@ void item::io( Archive &archive ) archive.io( "item_counter", item_counter, static_cast( 0 ) ); archive.io( "countdown_point", countdown_point, calendar::turn_max ); archive.io( "wetness", wetness, 0 ); - archive.io( "contents_linked", contents_linked, false ); archive.io( "dropped_from", dropped_from, harvest_drop_type_id::NULL_ID() ); archive.io( "rot", rot, 0_turns ); archive.io( "last_temp_check", last_temp_check, calendar::start_of_cataclysm ); @@ -3357,6 +3358,7 @@ void vehicle_part::deserialize( const JsonObject &data ) data.read( "target_second_z", target.second.z ); data.read( "ammo_pref", ammo_pref ); data.read( "locked", locked ); + data.read( "last_disconnected", last_disconnected ); if( migration != nullptr ) { for( const itype_id &it : migration->add_veh_tools ) { @@ -3409,6 +3411,7 @@ void vehicle_part::serialize( JsonOut &json ) const } json.member( "ammo_pref", ammo_pref ); json.member( "locked", locked ); + json.member( "last_disconnected", last_disconnected ); json.end_object(); } diff --git a/src/veh_appliance.cpp b/src/veh_appliance.cpp index 1dfafca2830a3..fd462952f489c 100644 --- a/src/veh_appliance.cpp +++ b/src/veh_appliance.cpp @@ -172,7 +172,6 @@ veh_app_interact::veh_app_interact( vehicle &veh, const point &p ) ctxt.register_action( "SIPHON" ); ctxt.register_action( "RENAME" ); ctxt.register_action( "REMOVE" ); - ctxt.register_action( "UNPLUG" ); } // @returns true if a battery part exists on any vehicle connected to veh @@ -350,14 +349,6 @@ bool veh_app_interact::can_siphon() return false; } -bool veh_app_interact::can_unplug() -{ - vehicle_part_range vpr = veh->get_all_parts(); - return std::any_of( vpr.begin(), vpr.end(), []( const vpart_reference & ref ) { - return ref.info().has_flag( "POWER_TRANSFER" ); - } ); -} - // Helper function for selecting a part in the parts list. // If only one part is available, don't prompt the player. static vehicle_part *pick_part( const std::vector &parts, @@ -538,27 +529,6 @@ void veh_app_interact::plug() veh->plug_in( get_map().getabs( pos ) ); } -void veh_app_interact::unplug() -{ - veh->shed_loose_parts(); - int const part = veh->part_at( a_point ); - vehicle_part &vp = veh->part( part >= 0 ? part : 0 ); - act = player_activity( ACT_VEHICLE, 1, static_cast( 'u' ) ); - act.str_values.push_back( vp.info().id.str() ); - const point q = veh->coord_translate( vp.mount ); - map &here = get_map(); - for( const tripoint &p : veh->get_points( true ) ) { - act.coord_set.insert( here.getabs( p ) ); - } - act.values.push_back( here.getabs( veh->global_pos3() ).x + q.x ); - act.values.push_back( here.getabs( veh->global_pos3() ).y + q.y ); - act.values.push_back( a_point.x ); - act.values.push_back( a_point.y ); - act.values.push_back( -a_point.x ); - act.values.push_back( -a_point.y ); - act.values.push_back( veh->index_of_part( &vp ) ); -} - void veh_app_interact::populate_app_actions() { const std::string ctxt_letters = ctxt.get_available_single_char_hotkeys(); @@ -597,13 +567,6 @@ void veh_app_interact::populate_app_actions() imenu.addentry( -1, true, ctxt.keys_bound_to( "PLUG" ).front(), ctxt.get_action_name( "PLUG" ) ); - // Unplug - app_actions.emplace_back( [this]() { - unplug(); - } ); - imenu.addentry( -1, can_unplug(), ctxt.keys_bound_to( "UNPLUG" ).front(), - ctxt.get_action_name( "UNPLUG" ) ); - /*************** Get part-specific actions ***************/ veh_menu menu( veh, "IF YOU SEE THIS IT IS A BUG" ); veh->build_interact_menu( menu, veh->mount_to_tripoint( a_point ), false ); diff --git a/src/veh_appliance.h b/src/veh_appliance.h index 756f8861b0c10..ca29923a68f9d 100644 --- a/src/veh_appliance.h +++ b/src/veh_appliance.h @@ -134,12 +134,6 @@ class veh_app_interact * Connects the power cable to selected tile. */ void plug(); - /** - * Function associated with the "UNPLUG" action. - * Removes all power connections to other appliances and vehicles and drops - * any used cable items on the ground. - */ - void unplug(); /** * The main loop of the appliance UI. Redraws windows, checks for input, and * performs selected actions. The loop exits once an activity is assigned diff --git a/src/veh_interact.cpp b/src/veh_interact.cpp index 254193272187a..e198812d3d8f6 100644 --- a/src/veh_interact.cpp +++ b/src/veh_interact.cpp @@ -1757,7 +1757,7 @@ bool veh_interact::can_remove_part( int idx, const Character &you ) nmsg += string_format( _( "Removing the %1$s may yield:\n> %2$s\n" ), sel_vehicle_part->name(), enumerate_as_string( removed_names ) ); } else { - item result_of_removal = sel_vehicle_part->properties_to_item(); + item result_of_removal = veh->part_to_item( *sel_vehicle_part ); nmsg += string_format( _( "Removing the %1$s will yield:\n> %2$s\n" ), sel_vehicle_part->name(), result_of_removal.display_name() ); @@ -3293,11 +3293,6 @@ void veh_interact::complete_vehicle( Character &you ) resulting_items.insert( resulting_items.end(), contents.begin(), contents.end() ); contents.clear(); - // Power cables must remove parts from the target vehicle, too. - if( vpi.has_flag( "POWER_TRANSFER" ) ) { - veh.remove_remote_part( vp ); - } - if( broken ) { you.add_msg_if_player( _( "You remove the broken %1$s from the %2$s." ), vp.name(), veh.name ); } else if( smash_remove ) { @@ -3308,7 +3303,7 @@ void veh_interact::complete_vehicle( Character &you ) } if( wall_wire_removal ) { - vp.properties_to_item(); // what's going on here? this line isn't doing anything... + veh.part_to_item( vp ); // what's going on here? this line isn't doing anything... } else if( vpi.has_flag( "TOW_CABLE" ) ) { veh.invalidate_towing( true, &you ); } else if( broken ) { @@ -3319,7 +3314,7 @@ void veh_interact::complete_vehicle( Character &you ) item_group::ItemList pieces = vp.pieces_for_broken_part(); resulting_items.insert( resulting_items.end(), pieces.begin(), pieces.end() ); } else { - resulting_items.push_back( vp.properties_to_item() ); + resulting_items.push_back( veh.part_to_item( vp ) ); } for( const std::pair &sk : vpi.install_skills ) { // removal is half as educational as installation @@ -3327,9 +3322,14 @@ void veh_interact::complete_vehicle( Character &you ) } } + // Power cables must remove parts from the target vehicle, too. + if( vpi.has_flag( "POWER_TRANSFER" ) ) { + veh.remove_remote_part( vp ); + } + // Remove any leftover power cords from the appliance if( appliance_removal && veh.part_count() >= 2 ) { - veh.shed_loose_parts(); + veh.shed_loose_parts( trinary::ALL ); veh.part_removal_cleanup(); //always stop after removing an appliance you.activity.set_to_null(); diff --git a/src/veh_type.cpp b/src/veh_type.cpp index db70f79e38a16..df6ff5382e70f 100644 --- a/src/veh_type.cpp +++ b/src/veh_type.cpp @@ -132,6 +132,7 @@ static const std::unordered_map vpart_bitflag_map = { "TURRET_CONTROLS", VPFLAG_TURRET_CONTROLS }, { "ROOF", VPFLAG_ROOF }, { "CABLE_PORTS", VPFLAG_CABLE_PORTS }, + { "BATTERY", VPFLAG_BATTERY } }; static std::map vpart_migrations; diff --git a/src/veh_type.h b/src/veh_type.h index 27da97cfda336..9271c33d244c9 100644 --- a/src/veh_type.h +++ b/src/veh_type.h @@ -107,6 +107,7 @@ enum vpart_bitflags : int { VPFLAG_TURRET_CONTROLS, VPFLAG_ROOF, VPFLAG_CABLE_PORTS, + VPFLAG_BATTERY, NUM_VPFLAGS }; diff --git a/src/vehicle.cpp b/src/vehicle.cpp index 2cd151a059104..ecfb53ee08ee9 100644 --- a/src/vehicle.cpp +++ b/src/vehicle.cpp @@ -1836,7 +1836,7 @@ bool vehicle::remove_part( vehicle_part &vp, RemovePartHandler &handler ) return false; } vehicle_part &vp_dep = parts[dep]; - handler.add_item_or_charges( part_loc, vp_dep.properties_to_item(), false ); + handler.add_item_or_charges( part_loc, part_to_item( vp_dep ), false ); remove_part( vp_dep, handler ); return true; }; @@ -5028,22 +5028,17 @@ void vehicle::power_parts() noise_and_smoke( 0, 1_turns ); // refreshes this->vehicle_noise } } - -vehicle *vehicle::find_vehicle( const tripoint &where ) +vehicle *vehicle::find_vehicle( const tripoint_abs_ms &where ) { map &here = get_map(); // Is it in the reality bubble? - tripoint veh_local = here.getlocal( where ); - if( const optional_vpart_position vp = here.veh_at( veh_local ) ) { + if( const optional_vpart_position vp = here.veh_at( where ) ) { return &vp->vehicle(); } - // Nope. Load up its submap... point_sm_ms veh_in_sm; tripoint_abs_sm veh_sm; - // TODO: fix point types - std::tie( veh_sm, veh_in_sm ) = project_remain( tripoint_abs_ms( where ) ); - + std::tie( veh_sm, veh_in_sm ) = project_remain( where ); const submap *sm = MAPBUFFER.lookup_submap( veh_sm ); if( sm == nullptr ) { return nullptr; @@ -5084,7 +5079,7 @@ std::map vehicle::search_connected_vehicles( Vehicle *start ) continue; } - Vehicle *const v_next = vehicle::find_vehicle( vp.target.second ); + Vehicle *const v_next = find_vehicle( tripoint_abs_ms( vp.target.second ) ); if( v_next == nullptr ) { // vehicle's rolled away or off-map continue; } @@ -5958,7 +5953,7 @@ void vehicle::refresh( const bool remove_fakes ) if( vpi.has_flag( "FUNNEL" ) ) { funnels.push_back( p ); } - if( vpi.has_flag( "UNMOUNT_ON_MOVE" ) ) { + if( vpi.has_flag( "UNMOUNT_ON_MOVE" ) || vpi.has_flag( "POWER_TRANSFER" ) ) { loose_parts.push_back( p ); } if( !vpi.emissions.empty() || !vpi.exhaust.empty() ) { @@ -6488,7 +6483,7 @@ void vehicle::invalidate_towing( bool first_vehicle, Character *remover ) const int tow_cable_idx = get_tow_part(); if( tow_cable_idx > -1 ) { vehicle_part &vp = parts[tow_cable_idx]; - item drop = vp.properties_to_item(); + item drop = part_to_item( vp ); drop.set_damage( 0 ); if( other_veh != nullptr ) { if( is_towing() ) { @@ -6504,12 +6499,16 @@ void vehicle::invalidate_towing( bool first_vehicle, Character *remover ) drop.link->t_mount = other_veh->part( other_tow_cable_idx ).mount; } } else { - drop.reset_cable(); + drop.reset_link(); } if( remover != nullptr ) { - std::list drops{ drop }; - put_into_vehicle_or_drop( *remover, item_drop_reason::deliberate, drops ); + if( !drop.has_flag( flag_NO_DROP ) && remover->can_stash( drop ) ) { + remover->i_add_or_drop( drop ); + } else { + std::list drops{ drop }; + put_into_vehicle_or_drop( *remover, item_drop_reason::deliberate, drops ); + } } else { get_map().add_item_or_charges( global_part_pos3( vp ), drop ); } @@ -6594,23 +6593,34 @@ bool vehicle::no_towing_slack() const } -void vehicle::remove_remote_part( const vehicle_part &vp_local ) const +std::optional> vehicle::get_remote_part( + const vehicle_part &vp_local ) const { - vehicle *veh = find_vehicle( vp_local.target.second ); + vehicle *veh = find_vehicle( tripoint_abs_ms( vp_local.target.second ) ); // If the target vehicle is still there, ask it to remove its part if( veh != nullptr ) { const tripoint local_abs = get_map().getabs( global_part_pos3( vp_local ) ); for( const int remote_partnum : veh->loose_parts ) { vehicle_part &vp_remote = veh->parts[remote_partnum]; if( vp_remote.info().has_flag( "POWER_TRANSFER" ) && vp_remote.target.first == local_abs ) { - veh->remove_part( vp_remote ); - return; + return std::make_pair( veh, &vp_remote ); } } } + return std::nullopt; +} + +void vehicle::remove_remote_part( const vehicle_part &vp_local ) const +{ + const auto part = get_remote_part( vp_local ); + if( part ) { + part->first->remove_part( *part->second ); + // Rebuild vehicle cache to avoid creating an illusion of the remote car. + get_map().rebuild_vehicle_level_caches(); + } } -void vehicle::shed_loose_parts( const tripoint_bub_ms *src, const tripoint_bub_ms *dst ) +void vehicle::shed_loose_parts( const trinary shed_cables, const tripoint_bub_ms *dst ) { map &here = get_map(); // remove_part rebuilds the loose_parts vector, so iterate over a copy to preserve @@ -6619,42 +6629,42 @@ void vehicle::shed_loose_parts( const tripoint_bub_ms *src, const tripoint_bub_m for( const int elem : lp ) { vehicle_part &vp_loose = part( elem ); const vpart_info &vpi_loose = vp_loose.info(); + bool remove_remote = false; if( std::find( loose_parts.begin(), loose_parts.end(), elem ) == loose_parts.end() ) { // part was removed elsewhere continue; } if( vpi_loose.has_flag( "POWER_TRANSFER" ) ) { - int distance = rl_dist( here.getabs( bub_part_pos( vp_loose ) ), vp_loose.target.second ); - int max_dist = vp_loose.get_base().type->maximum_charges(); - if( src && ( max_dist - distance ) > 0 ) { - // power line still has some slack to it, so keep it attached for now - vehicle *veh = find_vehicle( vp_loose.target.second ); - if( veh != nullptr ) { - for( int remote_lp : veh->loose_parts ) { - vehicle_part &vp_remote = veh->part( remote_lp ); - if( vp_remote.info().has_flag( "POWER_TRANSFER" ) && - vp_remote.target.first == here.getabs( *src ) ) { - // update remote part's target to new position - vp_remote.target.first = here.getabs( dst ? *dst : bub_part_pos( vp_loose ) ); - vp_remote.target.second = vp_remote.target.first; - } - } + if( shed_cables == trinary::NONE ) { + // Skip cables if we're only calling shed_loose_parts to remove parts with UNMOUNT_ON_MOVE. + continue; + } + tripoint vp_loose_dst = here.getabs( dst ? *dst + vp_loose.precalc[1] : bub_part_pos( vp_loose ) ); + const int distance = rl_dist( vp_loose_dst, vp_loose.target.first ); + const int max_dist = vp_loose.get_base().max_link_length(); + + if( distance > max_dist || shed_cables == trinary::ALL ) { + add_msg_if_player_sees( global_part_pos3( vp_loose ), m_warning, + _( "The %s's %s was detached!" ), name, vp_loose.name( false ) ); + remove_remote = true; + } else { + // cable still has some slack to it, so update the remote part's target and continue. + const auto remote = get_remote_part( vp_loose ); + if( remote ) { + remote->second->target.first = vp_loose_dst; + remote->second->target.second = here.getabs( dst ? *dst : pos_bub() ); } continue; } - add_msg_if_player_sees( global_part_pos3( vp_loose ), m_warning, - _( "The %s's power connection was detached!" ), name ); - remove_remote_part( vp_loose ); } - if( vpi_loose.has_flag( "TOW_CABLE" ) ) { - invalidate_towing( true ); - continue; - } - const item drop = vp_loose.properties_to_item(); - if( !magic && !drop.has_flag( flag_AUTO_DELETE_CABLE ) ) { + const item drop = part_to_item( vp_loose ); + if( !magic ) { here.add_item_or_charges( global_part_pos3( vp_loose ), drop ); } + if( remove_remote ) { + remove_remote_part( vp_loose ); + } remove_part( vp_loose ); } } @@ -6947,7 +6957,7 @@ int vehicle::break_off( map &here, vehicle_part &vp, int dmg ) } else if( vpi_here.has_flag( "POWER_TRANSFER" ) ) { // Electrical cables - remove it in one piece and remove remote part add_msg_if_player_sees( pos, m_bad, _( "The %1$s's %2$s is disconnected!" ), name, vp_here.name() ); - here.add_item_or_charges( pos, vp_here.properties_to_item() ); + here.add_item_or_charges( pos, part_to_item( vp_here ) ); remove_remote_part( vp_here ); } else if( vp_here.is_broken() ) { // Tearing off a broken part - break it up @@ -6957,7 +6967,7 @@ int vehicle::break_off( map &here, vehicle_part &vp, int dmg ) // Intact (but possibly damaged) part - remove it in one piece add_msg_if_player_sees( pos, m_bad, _( "The %1$s's %2$s is torn off!" ), name, vp_here.name() ); if( !magic ) { - here.add_item_or_charges( pos, vp_here.properties_to_item() ); + here.add_item_or_charges( pos, part_to_item( vp_here ) ); } } remove_part( vp_here, *handler_ptr ); @@ -6975,7 +6985,7 @@ int vehicle::break_off( map &here, vehicle_part &vp, int dmg ) } else if( vpi.has_flag( "POWER_TRANSFER" ) ) { // Electrical cables - remove it in one piece and remove remote part add_msg_if_player_sees( pos, m_bad, _( "The %1$s's %2$s is disconnected!" ), name, vp.name() ); - here.add_item_or_charges( pos, vp.properties_to_item() ); + here.add_item_or_charges( pos, part_to_item( vp ) ); remove_remote_part( vp ); } else { //Just break it off @@ -7011,7 +7021,7 @@ int vehicle::break_off( map &here, vehicle_part &vp, int dmg ) if( vpi_here.has_flag( "POWER_TRANSFER" ) ) { remove_remote_part( vp_here ); } - here.add_item_or_charges( pos, vp_here.properties_to_item() ); + here.add_item_or_charges( pos, part_to_item( vp_here ) ); remove_part( vp_here, *handler_ptr ); } } @@ -7114,7 +7124,7 @@ int vehicle::damage_direct( map &here, vehicle_part &vp, int dmg, const damage_t if( vpi.has_flag( "TOW_CABLE" ) ) { invalidate_towing( true ); } else { - item part_as_item = vp.properties_to_item(); + item part_as_item = part_to_item( vp ); add_msg_if_player_sees( vppos, m_bad, _( "The %1$s's %2$s is disconnected!" ), name, vp.name() ); if( vpi.has_flag( "POWER_TRANSFER" ) ) { remove_remote_part( vp ); @@ -7122,7 +7132,7 @@ int vehicle::damage_direct( map &here, vehicle_part &vp, int dmg, const damage_t } else { part_as_item.set_damage( vpi.base_item.obj().damage_max() - 1 ); } - if( !magic && !part_as_item.has_flag( flag_AUTO_DELETE_CABLE ) ) { + if( !magic ) { here.add_item_or_charges( vppos, part_as_item ); } if( !g || &get_map() != &here ) { @@ -7808,6 +7818,39 @@ vehicle_part_with_fakes_range vehicle::get_all_parts_with_fakes( bool with_inact return vehicle_part_with_fakes_range( const_cast( *this ), with_inactive ); } +item vehicle::part_to_item( const vehicle_part &vp ) const +{ + item tmp = vp.base; + tmp.unset_flag( flag_VEHICLE ); + + // Cables get special handling: their target coordinates need to remain + // stored, and if a cable actually drops, it should be half-connected. + if( tmp.has_flag( flag_CABLE_SPOOL ) ) { + tmp.link = cata::make_value(); + + // Tow cables have these variables assigned in invalidate_towing, which calls part_to_item. + if( !tmp.has_flag( flag_TOW_CABLE ) ) { + const auto remote = get_remote_part( vp ); + if( remote ) { + tmp.link->t_veh_safe = remote->first->get_safe_reference(); + tmp.link->t_mount = remote->second->mount; + } else { + // The linked vehicle can't be found, so this part shouldn't exist. + tmp.set_flag( flag_NO_DROP ); + } + tmp.link->t_abs_pos = tripoint_abs_ms( vp.target.second ); + } + + tmp.set_link_traits( true ); + tmp.link->last_processed = calendar::turn; + } + + // quantize damage and degradation to the middle of each damage_level so that items will stack nicely + tmp.set_damage( ( tmp.damage_level() - 0.5 ) * itype::damage_scale ); + tmp.set_degradation( ( tmp.damage_level() - 0.5 ) * itype::damage_scale ); + return tmp; +} + bool vehicle::refresh_zones() { if( zones_dirty ) { diff --git a/src/vehicle.h b/src/vehicle.h index 8a4cbe132fdb4..44da02e5dfdfd 100644 --- a/src/vehicle.h +++ b/src/vehicle.h @@ -227,7 +227,8 @@ enum class vp_flag : uint32_t { animal_flag = 2, carried_flag = 4, carrying_flag = 8, - tracked_flag = 16 //carried vehicle part with tracking enabled + tracked_flag = 16, //carried vehicle part with tracking enabled + linked_flag = 32 //a cable is attached to this }; class turret_cpu @@ -511,6 +512,8 @@ struct vehicle_part { /** If it's a part with variants, which variant it is */ std::string variant; + time_point last_disconnected = calendar::before_time_starts; + private: // part type definition // note: this could be a const& but doing so would require hassle with implementing @@ -540,11 +543,6 @@ struct vehicle_part { void serialize( JsonOut &json ) const; void deserialize( const JsonObject &data ); - /** - * Generate the corresponding item from this vehicle part. It includes - * the hp (item damage), fuel charges (battery or liquids), aspect, ... - */ - item properties_to_item() const; /** * Returns an ItemList of the pieces that should arise from breaking * this part. @@ -837,14 +835,6 @@ class vehicle /** empty the contents of a tank, battery or turret spilling liquids randomly on the ground */ void leak_fuel( vehicle_part &pt ) const; - /** - * Find a possibly off-map vehicle. If necessary, loads up its submap through - * the global MAPBUFFER and pulls it from there. For this reason, you should only - * give it the coordinates of the origin tile of a target vehicle. - * @param where Location of the other vehicle's origin tile. - */ - static vehicle *find_vehicle( const tripoint &where ); - /// Returns a map of connected vehicle pointers to power loss factor: /// Keys are vehicles connected by POWER_TRANSFER parts, includes self /// Values are line loss, 0.01 corresponds to 1% charge loss to wire resistance @@ -853,6 +843,13 @@ class vehicle template static std::map search_connected_vehicles( Vehicle *start ); public: + /** + * Find a possibly off-map vehicle. If necessary, loads up its submap through + * the global MAPBUFFER and pulls it from there. For this reason, you should only + * give it the coordinates of the origin tile of a target vehicle. + * @param where Location of the other vehicle's origin tile. + */ + static vehicle *find_vehicle( const tripoint_abs_ms &where ); //! @copydoc vehicle::search_connected_vehicles( Vehicle *start ) std::map search_connected_vehicles(); //! @copydoc vehicle::search_connected_vehicles( Vehicle *start ) @@ -1090,8 +1087,14 @@ class vehicle item_location part_base( int p ); /** - * Remove a part from a targeted remote vehicle. Useful for, e.g. power cables that have - * a vehicle part on both sides. + * Get the remote vehicle and part that a part is targeting. + * Useful for, e.g. power cables that have a vehicle part on both sides. + * @param vp_local Vehicle part that is connected to the remote part. + */ + std::optional> get_remote_part( + const vehicle_part &vp_local ) const; + /** + * Remove the part on a targeted remote vehicle that a part is targeting. */ void remove_remote_part( const vehicle_part &vp_local ) const; /** @@ -1811,7 +1814,15 @@ class vehicle void shift_parts( map &here, const point &delta ); bool shift_if_needed( map &here ); - void shed_loose_parts( const tripoint_bub_ms *src = nullptr, const tripoint_bub_ms *dst = nullptr ); + /** + * Drop parts with UNMOUNT_ON_MOVE onto the ground. + * @param shed_cables If cable parts should also be dropped. + * @param - If set to trinary::NONE, the default, don't drop any cables. + * @param - If set to trinary::SOME, calculate cable length, updating remote parts, and drop if it's too long. + * @param - If set to trinary::ALL, drop all cables. + * @param dst Future vehicle position, used for calculating cable length when shed_cables == trinary::SOME. + */ + void shed_loose_parts( trinary shed_cables = trinary::NONE, const tripoint_bub_ms *dst = nullptr ); /** * @name Vehicle turrets @@ -2083,6 +2094,12 @@ class vehicle // map.cpp calls this in displace_vehicle void update_active_fakes(); + /** + * Generate the corresponding item from this vehicle part. It includes + * the hp (item damage), fuel charges (battery or liquids), aspect, ... + */ + item part_to_item( const vehicle_part &vp ) const; + // Updates the internal precalculated mount offsets after the vehicle has been displaced // used in map::displace_vehicle() std::set advance_precalc_mounts( const point &new_pos, const tripoint &src, @@ -2102,7 +2119,7 @@ class vehicle std::vector sails; // NOLINT(cata-serialize) std::vector funnels; // NOLINT(cata-serialize) std::vector emitters; // NOLINT(cata-serialize) - // Parts that will fall off the next time the vehicle moves. + // Parts that will fall off and cables that might disconnect when the vehicle moves. std::vector loose_parts; // NOLINT(cata-serialize) std::vector wheelcache; // NOLINT(cata-serialize) std::vector rotors; // NOLINT(cata-serialize) diff --git a/src/vehicle_part.cpp b/src/vehicle_part.cpp index 5d4cda7a2525e..d26fd61c9a6b8 100644 --- a/src/vehicle_part.cpp +++ b/src/vehicle_part.cpp @@ -67,62 +67,6 @@ void vehicle_part::set_base( item &&new_base ) base.set_flag( flag_VEHICLE ); } -item vehicle_part::properties_to_item() const -{ - item tmp = base; - tmp.unset_flag( flag_VEHICLE ); - - // Cables get special handling: their target coordinates need to remain - // stored, and if a cable actually drops, it should be half-connected. - if( tmp.has_flag( flag_CABLE_SPOOL ) ) { - map &here = get_map(); - tmp.link = cata::make_value(); - - // Tow cables have these variables assigned in invalidate_towing, which calls properties_to_item. - if( !tmp.has_flag( flag_TOW_CABLE ) ) { - const tripoint local_pos = here.getlocal( target.first ); - const optional_vpart_position target_vp = here.veh_at( local_pos ); - if( !target_vp ) { - // That vehicle ain't there no more. - tmp.set_flag( flag_NO_DROP ); - } else { - tmp.link->t_mount = target_vp->mount(); - } - tmp.link->t_abs_pos = tripoint_abs_ms( target.second ); - tmp.link->s_state = link_state::no_link; - tmp.link->t_state = link_state::vehicle_port; - } - - bool iuse_found = false; - const use_function *iuse = tmp.type->get_use( "link_up" ); - if( iuse != nullptr ) { - const link_up_actor *actor_ptr = - static_cast( iuse->get_actor_ptr() ); - if( actor_ptr != nullptr ) { - iuse_found = true; - tmp.link->max_length = actor_ptr->cable_length; - tmp.link->charge_efficiency = actor_ptr->charge_efficiency; - tmp.link->charge_rate = actor_ptr->charge_rate.value(); - tmp.link->charge_interval = actor_ptr->charge_rate == 0_W ? -1 : - std::max( 1, static_cast( std::floor( 1000000.0 / abs( actor_ptr->charge_rate.value() ) + - 0.5 ) ) ); - } - } - if( !iuse_found ) { - debugmsg( "Could not find link_up iuse data for %s! Using default values.", tmp.tname() ); - tmp.link->max_length = tmp.type->maximum_charges(); - } - - tmp.link->last_processed = calendar::turn; - tmp.active = true; - } - - // quantize damage and degradation to the middle of each damage_level so that items will stack nicely - tmp.set_damage( ( tmp.damage_level() - 0.5 ) * itype::damage_scale ); - tmp.set_degradation( ( tmp.damage_level() - 0.5 ) * itype::damage_scale ); - return tmp; -} - std::string vehicle_part::name( bool with_prefix ) const { std::string res; @@ -578,7 +522,8 @@ bool vehicle_part::contains_liquid() const bool vehicle_part::is_battery() const { - return base.is_magazine() && base.ammo_types().count( ammo_battery ); + return info().has_flag( VPFLAG_BATTERY ) || + ( base.is_magazine() && base.ammo_types().count( ammo_battery ) ); } bool vehicle_part::is_reactor() const diff --git a/src/vehicle_use.cpp b/src/vehicle_use.cpp index bdf59530ae075..dfe43546610f3 100644 --- a/src/vehicle_use.cpp +++ b/src/vehicle_use.cpp @@ -549,7 +549,7 @@ item vehicle::init_cord( const tripoint &pos ) cord.link->t_state = link_state::vehicle_port; cord.link->t_veh_safe = get_safe_reference(); cord.link->t_abs_pos = get_map().getglobal( pos ); - cord.active = true; + cord.set_link_traits(); return cord; } @@ -588,12 +588,14 @@ void vehicle::connect( const tripoint &source_pos, const tripoint &target_pos ) source_part.target.first = target_global; source_part.target.second = target_veh->global_square_location().raw(); source_veh->install_part( vcoords, std::move( source_part ) ); + source_veh->precalc_mounts( 1, source_veh->pivot_rotation[1], source_veh->pivot_anchor[1] ); vcoords = target_vp->mount(); vehicle_part target_part( vpid, item( cord ) ); target_part.target.first = cord.link->t_abs_pos.raw(); target_part.target.second = source_veh->global_square_location().raw(); target_veh->install_part( vcoords, std::move( target_part ) ); + target_veh->precalc_mounts( 1, target_veh->pivot_rotation[1], target_veh->pivot_anchor[1] ); } double vehicle::engine_cold_factor( const vehicle_part &vp ) const @@ -1832,9 +1834,13 @@ void vehicle::build_interact_menu( veh_menu &menu, const tripoint &p, bool with_ const vpart_position vp = *ovp; const tripoint vppos = vp.pos(); + std::vector vp_parts = get_parts_at( vppos, "", part_status_flag::working ); + // @returns true if pos contains available part with a flag - const auto has_part_here = [this, vppos]( const std::string & flag ) { - return !get_parts_at( vppos, flag, part_status_flag::working ).empty(); + const auto has_part_here = [vp_parts]( const std::string & flag ) { + return std::any_of( vp_parts.begin(), vp_parts.end(), [flag]( const vehicle_part * vp_part ) { + return vp_part->info().has_flag( flag ); + } ); }; const bool remote = g->remoteveh() == this; @@ -1846,6 +1852,13 @@ void vehicle::build_interact_menu( veh_menu &menu, const tripoint &p, bool with_ const bool player_inside = get_map().veh_at( get_player_character().pos() ) ? &get_map().veh_at( get_player_character().pos() )->vehicle() == this : false; + bool power_linked = false; + bool cable_linked = false; + for( vehicle_part *vp_part : vp_parts ) { + power_linked = power_linked ? true : vp_part->info().has_flag( "POWER_TRANSFER" ); + cable_linked = cable_linked ? true : vp_part->has_flag( vp_flag::linked_flag ) || + vp_part->info().has_flag( "TOW_CABLE" ); + } if( !is_appliance() ) { menu.add( _( "Examine vehicle" ) ) @@ -2057,6 +2070,55 @@ void vehicle::build_interact_menu( veh_menu &menu, const tripoint &p, bool with_ } } + if( power_linked ) { + menu.add( _( "Disconnect power connections" ) ) + .enable( !cable_linked ) + .desc( string_format( !cable_linked ? "" : _( "Remove other cables first" ) ) ) + .skip_locked_check() + .hotkey( "DISCONNECT_CABLES" ) + .on_submit( [this, vp_parts] { + for( vehicle_part *vp_part : vp_parts ) + { + if( vp_part->info().has_flag( "POWER_TRANSFER" ) ) { + item drop = part_to_item( *vp_part ); + if( !magic && !drop.has_flag( STATIC( flag_id( "NO_DROP" ) ) ) ) { + get_player_character().i_add_or_drop( drop ); + add_msg( _( "You detach the %s and take it." ), drop.type_name() ); + } else { + add_msg( _( "You detached the %s." ), drop.type_name() ); + } + remove_remote_part( *vp_part ); + remove_part( *vp_part ); + } + } + get_player_character().pause(); + } ); + } + if( cable_linked ) { + menu.add( _( "Disconnect cables" ) ) + .skip_locked_check() + .hotkey( "DISCONNECT_CABLES" ) + .on_submit( [this, vp_parts] { + for( vehicle_part *vp_part : vp_parts ) + { + if( vp_part->has_flag( vp_flag::linked_flag ) ) { + vp_part->last_disconnected = calendar::turn; + vp_part->remove_flag( vp_flag::linked_flag ); + add_msg( _( "You detached the %s's cables." ), vp_part->name( false ) ); + } + if( vp_part->info().has_flag( "TOW_CABLE" ) ) { + invalidate_towing( true, &get_player_character() ); + if( get_player_character().can_stash( vp_part->get_base() ) ) { + add_msg( _( "You detach the %s and take it." ), vp_part->name( false ) ); + } else { + add_msg( _( "You detached the %s." ), vp_part->name( false ) ); + } + } + } + get_player_character().pause(); + } ); + } + for( const auto&[tool_item, hk] : vp.get_tools() ) { const itype_id &tool_type = tool_item.typeId(); if( !tool_type->has_use() ) { diff --git a/tests/vehicle_test.cpp b/tests/vehicle_test.cpp index a3c4dff2d33a5..13e42a6f0173b 100644 --- a/tests/vehicle_test.cpp +++ b/tests/vehicle_test.cpp @@ -430,7 +430,7 @@ static void connect_power_line( const tripoint &src_pos, const tripoint &dst_pos cord.link = cata::make_value(); cord.link->t_state = link_state::vehicle_port; cord.link->t_abs_pos = here.getglobal( src_pos ); - cord.active = true; + cord.set_link_traits(); const optional_vpart_position target_vp = here.veh_at( dst_pos ); const optional_vpart_position source_vp = here.veh_at( src_pos ); @@ -452,12 +452,14 @@ static void connect_power_line( const tripoint &src_pos, const tripoint &dst_pos source_part.target.first = target_global; source_part.target.second = target_veh->global_square_location().raw(); source_veh->install_part( vcoords, std::move( source_part ) ); + source_veh->precalc_mounts( 1, source_veh->pivot_rotation[1], source_veh->pivot_anchor[1] ); vcoords = target_vp->mount(); vehicle_part target_part( vpid, item( cord ) ); target_part.target.first = cord.link->t_abs_pos.raw(); target_part.target.second = source_veh->global_square_location().raw(); target_veh->install_part( vcoords, std::move( target_part ) ); + target_veh->precalc_mounts( 1, target_veh->pivot_rotation[1], target_veh->pivot_anchor[1] ); } TEST_CASE( "power_cable_stretch_disconnect" ) @@ -490,6 +492,7 @@ TEST_CASE( "power_cable_stretch_disconnect" ) REQUIRE( app2.part_count() == 2 ); REQUIRE( app1.part( 1 ).get_base().type->maximum_charges() == 3 ); + REQUIRE( app1.part( 1 ).get_base().max_link_length() == 3 ); const int max_dist = app1.part( 1 ).get_base().type->maximum_charges(); @@ -530,6 +533,7 @@ TEST_CASE( "power_cable_stretch_disconnect" ) REQUIRE( app2.part_count() == 2 ); REQUIRE( app1.part( 1 ).get_base().type->maximum_charges() == 10 ); + REQUIRE( app1.part( 1 ).get_base().max_link_length() == 10 ); const int max_dist = app1.part( 1 ).get_base().type->maximum_charges();