diff --git a/data/json/item_actions.json b/data/json/item_actions.json index ff2b901e2ac89..898950c73ecdb 100644 --- a/data/json/item_actions.json +++ b/data/json/item_actions.json @@ -545,6 +545,11 @@ "id": "MEASURE_RESONANCE", "name": { "str": "Measure artifact resonance" } }, + { + "type": "item_action", + "id": "CHANGE_OUTFIT", + "name": { "str": "Swap outfits" } + }, { "type": "item_action", "id": "PLAY_GAME", diff --git a/data/json/items/containers/generic.json b/data/json/items/containers/generic.json index 0c1f166fcd2d2..590eceeee8ec0 100644 --- a/data/json/items/containers/generic.json +++ b/data/json/items/containers/generic.json @@ -162,6 +162,33 @@ } ] }, + { + "id": "outfit_storage", + "type": "TOOL", + "category": "container", + "name": { "str": "set of clothes", "str_pl": "sets of clothes" }, + "description": "A bundle of presses, coat hangars, and other miscellaneous items needed to store and keep a good change of clothes. You can interact with it to swap to the clothes it's currently containing, or disassemble it to get the clothes back.", + "weight": "1 kg", + "volume": "1 L", + "longest_side": "150 cm", + "price": 0, + "price_postapoc": 0, + "to_hit": -3, + "use_action": [ "CHANGE_OUTFIT" ], + "material": [ "plastic" ], + "pocket_data": [ + { + "pocket_type": "CONTAINER", + "max_contains_volume": "100 L", + "max_contains_weight": "100 kg", + "rigid": false, + "moves": 1000 + } + ], + "symbol": "#", + "color": "black", + "flags": [ "SINGLE_USE", "NO_SALVAGE", "NO_RELOAD" ] + }, { "id": "box_paper_small", "type": "GENERIC", diff --git a/data/json/player_activities.json b/data/json/player_activities.json index f8da0e7a2b8ff..5817c6268919a 100644 --- a/data/json/player_activities.json +++ b/data/json/player_activities.json @@ -730,6 +730,14 @@ "rooted": true, "based_on": "speed" }, + { + "id": "ACT_OUTFIT_SWAP", + "type": "activity_type", + "activity_level": "LIGHT_EXERCISE", + "verb": "changing clothes", + "rooted": true, + "based_on": "speed" + }, { "id": "ACT_WASH", "type": "activity_type", diff --git a/data/json/recipes/other/other.json b/data/json/recipes/other/other.json index f5603cf95291d..c837a74bb44d6 100644 --- a/data/json/recipes/other/other.json +++ b/data/json/recipes/other/other.json @@ -42,6 +42,19 @@ "tools": [ [ [ "drawing_tool", 5, "LIST" ] ] ], "components": [ [ [ "cardboard", 10 ] ] ] }, + { + "result": "outfit_storage", + "type": "recipe", + "activity_level": "NO_EXERCISE", + "category": "CC_OTHER", + "subcategory": "CSC_OTHER_TOOLS", + "skill_used": "fabrication", + "time": "1 s", + "autolearn": true, + "reversible": true, + "//": "Tying equipment or other fasteners. Maybe one day we will have coat hangars.", + "components": [ [ [ "cordage", 10, "LIST" ] ] ] + }, { "result": "tic_tac", "type": "recipe", diff --git a/src/activity_actor.cpp b/src/activity_actor.cpp index a28b78c73dfc5..37c226d912e09 100644 --- a/src/activity_actor.cpp +++ b/src/activity_actor.cpp @@ -5568,6 +5568,87 @@ std::unique_ptr reel_cable_activity_actor::deserialize( JsonValu return actor.clone(); } +void outfit_swap_actor::start( player_activity &act, Character &who ) +{ + item fake_storage( outfit_item->typeId() ); + for( const item_location &worn_item : who.get_visible_worn_items() ) { + act.moves_total += who.item_handling_cost( *worn_item ); // Cost of taking it off + act.moves_total += who.item_store_cost( *worn_item, fake_storage ); // And putting it away + } + for( const item *clothing : outfit_item->all_items_top() ) { + auto ret = who.can_wear( *clothing ); + // Note this is checking if we can put something new on, but it might conflict with our current clothing, causing a + // spurious failure. Maybe we should strip the player first? + if( ret.success() ) { + act.moves_total += who.item_wear_cost( *clothing ); + } else { + act.moves_total += who.item_retrieve_cost( *clothing, *outfit_item ); + // Dropping takes no time? So I guess that's all we need + } + } + act.moves_left = act.moves_total; +} + +void outfit_swap_actor::finish( player_activity &act, Character &who ) +{ + map &here = get_map(); + // First, make a new outfit and shove all our existing clothes into it. + item new_outfit( outfit_item->typeId() ); + item_location ground = here.add_item_ret_loc( who.pos(), new_outfit, true ); + if( !ground ) { + debugmsg( "Failed to swap outfits during outfit_swap_actor::finish" ); + act.set_to_null(); + return; + } + // Taken-off items are put in this temporary list, then naturally deleted from the world when the function returns. + std::list it_list; + for( item_location &worn_item : who.get_visible_worn_items() ) { + item outfit_component( *worn_item ); + if( who.takeoff( worn_item, &it_list ) ) { + ground->force_insert_item( outfit_component, pocket_type::CONTAINER ); + } + } + + // Now we have to take clothes out of the one we activated + for( item *component : outfit_item->all_items_top() ) { + auto ret = who.can_wear( *component ); + if( ret.success() ) { + item_location new_clothes( who, component ); + who.wear( new_clothes ); + } else { + // For some reason we couldn't wear this item. Maybe the player mutated in the meanwhile, but + // drop the item instead of deleting it. + here.add_item( who.pos(), *component ); + } + } + + who.i_rem( outfit_item.get_item() ); + + // Now we just did a whole bunch of wearing and taking off at once, but we had already paid that movecost by doing the activity + // So we reset our moves + who.set_moves( 0 ); + // TODO: Granularize this and allow resumable swapping if you were interrupted during the activity + + act.set_to_null(); +} + +void outfit_swap_actor::serialize( JsonOut &jsout ) const +{ + jsout.start_object(); + jsout.member( "outfit_item", outfit_item ); + jsout.end_object(); +} + +std::unique_ptr outfit_swap_actor::deserialize( JsonValue &jsin ) +{ + outfit_swap_actor actor( {} ); + + JsonObject data = jsin.get_object(); + + data.read( "outfit_item", actor.outfit_item ); + return actor.clone(); +} + void meditate_activity_actor::start( player_activity &act, Character & ) { act.moves_total = to_moves( 20_minutes ); diff --git a/src/activity_actor_definitions.h b/src/activity_actor_definitions.h index ef9fb1d6204a5..59c64701caeb5 100644 --- a/src/activity_actor_definitions.h +++ b/src/activity_actor_definitions.h @@ -1413,6 +1413,32 @@ class oxytorch_activity_actor : public activity_actor } }; +class outfit_swap_actor : public activity_actor +{ + public: + explicit outfit_swap_actor( const item_location &outfit_item ) : outfit_item( outfit_item ) {}; + activity_id get_type() const override { + return activity_id( "ACT_OUTFIT_SWAP" ); + } + + void start( player_activity &act, Character &who ) override; + void do_turn( player_activity &, Character & ) override {} + void finish( player_activity &act, Character &who ) 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 outfit_item; + + bool can_resume_with_internal( const activity_actor &, const Character & ) const override { + return false; + } +}; + class meditate_activity_actor : public activity_actor { public: diff --git a/src/character.cpp b/src/character.cpp index d61b10f1a0461..b7d010b3156df 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -6260,10 +6260,10 @@ std::string Character::extended_description() const ss += "\n"; ss += _( "Wearing:" ) + std::string( " " ); - const std::list visible_worn_items = get_visible_worn_items(); + const std::list visible_worn_items = get_visible_worn_items(); std::string worn_string = enumerate_as_string( visible_worn_items.begin(), visible_worn_items.end(), - []( const item & it ) { - return it.tname(); + []( const item_location & it ) { + return it.get_item()->tname(); } ); ss += !worn_string.empty() ? worn_string : _( "Nothing" ); @@ -10751,11 +10751,11 @@ std::vector Character::short_description_parts() const if( is_armed() ) { result.push_back( _( "Wielding: " ) + weapon.tname() ); } - const std::list visible_worn_items = get_visible_worn_items(); + const std::list visible_worn_items = get_visible_worn_items(); const std::string worn_str = enumerate_as_string( visible_worn_items.begin(), visible_worn_items.end(), - []( const item & it ) { - return it.tname(); + []( const item_location & it ) { + return it.get_item()->tname(); } ); if( !worn_str.empty() ) { result.push_back( _( "Wearing: " ) + worn_str ); diff --git a/src/character.h b/src/character.h index 470e13f338d0c..f6cdbde537b46 100644 --- a/src/character.h +++ b/src/character.h @@ -3418,7 +3418,7 @@ class Character : public Creature, public visitable bool is_worn_item_visible( std::list::const_iterator ) const; /** Returns all worn items visible to an outside observer */ - std::list get_visible_worn_items() const; + std::list get_visible_worn_items() const; /** Swap side on which item is worn; returns false on fail. If interactive is false, don't alert player or drain moves */ bool change_side( item &it, bool interactive = true ); diff --git a/src/character_attire.cpp b/src/character_attire.cpp index 1b99db237c0e0..895866d8fcb22 100644 --- a/src/character_attire.cpp +++ b/src/character_attire.cpp @@ -686,9 +686,9 @@ bool Character::is_worn_item_visible( std::list::const_iterator worn_item return worn.is_worn_item_visible( worn_item, worn_item_body_parts ); } -std::list Character::get_visible_worn_items() const +std::list Character::get_visible_worn_items() const { - return worn.get_visible_worn_items( *this ); + return const_cast( worn ).get_visible_worn_items( *this ); } double Character::armwear_factor() const @@ -1072,12 +1072,13 @@ item *outfit::item_worn_with_id( const itype_id &i ) return it_with_id; } -std::list outfit::get_visible_worn_items( const Character &guy ) const +std::list outfit::get_visible_worn_items( const Character &guy ) { - std::list result; - for( auto i = worn.cbegin(), end = worn.cend(); i != end; ++i ) { + std::list result; + for( auto i = worn.begin(), end = worn.end(); i != end; ++i ) { if( guy.is_worn_item_visible( i ) ) { - result.push_back( *i ); + item_location loc_here( const_cast( guy ), &*i ); + result.emplace_back( loc_here ); } } return result; diff --git a/src/character_attire.h b/src/character_attire.h index 68db0240e2a60..a3697a7f86d22 100644 --- a/src/character_attire.h +++ b/src/character_attire.h @@ -122,7 +122,7 @@ class outfit */ void item_encumb( std::map &vals, const item &new_item, const Character &guy ) const; - std::list get_visible_worn_items( const Character &guy ) const; + std::list get_visible_worn_items( const Character &guy ); int swim_modifier( int swim_skill ) const; bool natural_attack_restricted_on( const bodypart_id &bp ) const; bool natural_attack_restricted_on( const sub_bodypart_id &bp ) const; diff --git a/src/item.cpp b/src/item.cpp index 35a7871775f0d..574ef797aa492 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -1805,7 +1805,7 @@ ret_val item::put_in( const item &payload, pocket_type pk_type, void item::force_insert_item( const item &it, pocket_type pk_type ) { contents.force_insert_item( it, pk_type ); - update_inherited_flags(); + on_contents_changed(); } void item::set_var( const std::string &name, const int value ) diff --git a/src/item_factory.cpp b/src/item_factory.cpp index 15b80e0ea9d20..7c883c125bd64 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -1809,6 +1809,7 @@ void Item_factory::init() add_iuse( "MACE", &iuse::mace ); add_iuse( "MAGIC_8_BALL", &iuse::magic_8_ball ); add_iuse( "MEASURE_RESONANCE", &iuse::measure_resonance ); + add_iuse( "CHANGE_OUTFIT", &iuse::change_outfit ); add_iuse( "PLAY_GAME", &iuse::play_game ); add_iuse( "MAKEMOUND", &iuse::makemound ); add_iuse( "DIG_CHANNEL", &iuse::dig_channel ); diff --git a/src/iuse.cpp b/src/iuse.cpp index 1e0618f73401f..cb76d9e43c11a 100644 --- a/src/iuse.cpp +++ b/src/iuse.cpp @@ -8786,6 +8786,19 @@ std::optional iuse::measure_resonance( Character *p, item *it, const tripoi return 0; } +std::optional iuse::change_outfit( Character *p, item *it, const tripoint & ) +{ + if( !p->is_avatar() ) { + debugmsg( "NPC %s tried to swap outfit", p->get_name() ); + return std::nullopt; + } + + p->assign_activity( outfit_swap_actor( item_location{*p, it} ) ); + + // Deleting the item we activated is handled in outfit_swap_actor::finish + return std::nullopt; +} + std::optional iuse::electricstorage( Character *p, item *it, const tripoint & ) { // From item processing diff --git a/src/iuse.h b/src/iuse.h index 8dfe49d962716..727345569b9a1 100644 --- a/src/iuse.h +++ b/src/iuse.h @@ -138,6 +138,7 @@ std::optional lumber( Character *, item *, const tripoint & ); std::optional ma_manual( Character *, item *, const tripoint & ); std::optional magic_8_ball( Character *, item *, const tripoint & ); std::optional measure_resonance( Character *, item *, const tripoint & ); +std::optional change_outfit( Character *, item *, const tripoint & ); std::optional electricstorage( Character *, item *, const tripoint & ); std::optional ebooksave( Character *, item *, const tripoint & ); std::optional ebookread( Character *, item *, const tripoint & ); diff --git a/src/npc.cpp b/src/npc.cpp index 693c3001b198b..e9a70f6ee5e9c 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -2612,10 +2612,10 @@ int npc::print_info( const catacurses::window &w, int line, int vLines, int colu } // Worn gear list on following lines. - const std::list visible_worn_items = get_visible_worn_items(); + const std::list visible_worn_items = get_visible_worn_items(); const std::string worn_str = enumerate_as_string( visible_worn_items.begin(), - visible_worn_items.end(), []( const item & it ) { - return it.tname(); + visible_worn_items.end(), []( const item_location & it ) { + return it.get_item()->tname(); } ); if( !worn_str.empty() ) { std::vector worn_lines = foldstring( _( "Wearing: " ) + worn_str, iWidth );