diff --git a/data/json/items/book/abstract.json b/data/json/items/book/abstract.json index ce0207731d2df..591bb68e7446f 100644 --- a/data/json/items/book/abstract.json +++ b/data/json/items/book/abstract.json @@ -52,6 +52,14 @@ "copy-from": "book_fict_tpl", "melee_damage": { "bash": 1 } }, + { + "abstract": "book_fict_soft_collection_tpl", + "type": "BOOK", + "copy-from": "book_fict_soft_tpl", + "name": { "str": "paperback novel", "str_pl": "paperbacks" }, + "description": "Paperback fiction novel generic collection", + "generic": true + }, { "abstract": "book_nonf_hard_tpl", "type": "BOOK", diff --git a/data/json/items/book/misc.json b/data/json/items/book/misc.json index 3d967eb00c776..1d327505691a0 100644 --- a/data/json/items/book/misc.json +++ b/data/json/items/book/misc.json @@ -88,6 +88,7 @@ "intelligence": 10, "time": "26 m", "chapters": 40, + "generic": true, "fun": 3 }, { @@ -170,6 +171,7 @@ "color": "pink", "time": "10 m", "chapters": 4, + "generic": true, "fun": 1 }, { @@ -194,7 +196,7 @@ "type": "BOOK", "name": { "str": "adventure novel" }, "description": "The stirring tale of a race against time, in search of a lost city located in the heart of the African continent.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "price": "8 USD 50 cent", "price_postapoc": "50 cent", "time": "20 m", @@ -205,7 +207,7 @@ "type": "BOOK", "name": { "str": "buddy novel" }, "description": "A gripping tale of two friends struggling to survive on the streets of New York City.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "weight": "244 g", "volume": "500 ml", "price": "6 USD 50 cent", @@ -215,7 +217,7 @@ "id": "novel_crime", "type": "BOOK", "name": { "str": "crime novel" }, - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "description": "After their diamond heist goes wrong, the surviving criminals begin to suspect that one of them is a police informant.", "intelligence": 6, "time": "20 m", @@ -252,7 +254,7 @@ "type": "BOOK", "name": { "str": "drama novel" }, "description": "A real book for real adults.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "intelligence": 7, "time": "25 m", "chapters": 28, @@ -263,7 +265,7 @@ "type": "BOOK", "name": { "str": "erotic novel" }, "description": "A hackneyed fictional narrative concealing low-grade literary smut.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "weight": "200 g", "volume": "500 ml", "time": "18 m", @@ -274,7 +276,7 @@ "type": "BOOK", "name": { "str": "experimental novel" }, "description": "A bizarre play about the philosophy of existential absurdity. Or maybe it's about two guys waiting for their friend to show up. It's confusing.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "weight": "142 g", "volume": "500 ml", "intelligence": 7, @@ -286,7 +288,7 @@ "type": "BOOK", "name": { "str": "fantasy novel" }, "description": "Basic sword & sorcery.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "weight": "227 g", "volume": "1 L", "intelligence": 7, @@ -300,7 +302,7 @@ "type": "BOOK", "name": { "str": "horror novel" }, "description": "Maybe not the best reading material considering the situation.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "weight": "227 g", "intelligence": 7, "time": "18 m", @@ -312,7 +314,7 @@ "type": "BOOK", "name": { "str": "mystery novel" }, "description": "A detective investigates an unusual murder in a secluded location.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "intelligence": 7, "time": "18 m", "chapters": 28, @@ -323,7 +325,7 @@ "type": "BOOK", "name": { "str": "road novel" }, "description": "A tale about a group of friends who wander the USA in the 1960s against a backdrop of jazz, poetry and drug use.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "weight": "244 g", "volume": "500 ml", "time": "20 m", @@ -345,7 +347,7 @@ "type": "BOOK", "name": { "str": "romance novel" }, "description": "Drama and mild smut.", - "copy-from": "book_fict_soft_tpl" + "copy-from": "book_fict_soft_collection_tpl" }, { "id": "paperback_romance_circuses", @@ -497,7 +499,7 @@ "type": "BOOK", "name": { "str": "spy novel" }, "description": "A tale of intrigue and espionage among Nazis, no, Commies, no, Iraqis!", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "intelligence": 5, "time": "18 m", "chapters": 20, @@ -524,7 +526,7 @@ "description": "An exciting seventeenth century tale of how an enslaved Irish doctor and his comrades-in-chains escape and become heroic pirates of the Robin Hood variety.", "weight": "582 g", "volume": "750 ml", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "intelligence": 7, "time": "20 m", "chapters": 28, @@ -582,7 +584,7 @@ "type": "BOOK", "name": { "str": "war novel" }, "description": "A thrilling narrative of survival in a prisoner of war camp during the Second World War, filled with riveting subplots about rat farming and dysentery.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "weight": "686 g", "price_postapoc": "50 cent", "intelligence": 7, @@ -604,7 +606,7 @@ "type": "BOOK", "name": { "str": "western novel" }, "description": "The classic tale of a gunfighting stranger who comes to a small settlement and is hired to help the townsfolk defend themselves from a band of marauding outlaws.", - "copy-from": "book_fict_soft_tpl", + "copy-from": "book_fict_soft_collection_tpl", "intelligence": 5, "time": "20 m", "chapters": 28, @@ -680,6 +682,7 @@ "intelligence": 4, "time": "1 m", "chapters": 200, + "generic": true, "fun": -5, "melee_damage": { "bash": 2 } }, @@ -698,6 +701,7 @@ "color": "light_gray", "time": "10 m", "chapters": 4, + "generic": true, "fun": 1, "flags": [ "INSPIRATIONAL" ] }, @@ -717,6 +721,7 @@ "intelligence": 9, "time": "18 m", "chapters": 36, + "generic": true, "fun": 2 }, { @@ -735,6 +740,7 @@ "intelligence": 9, "time": "18 m", "chapters": 36, + "generic": true, "fun": 2 }, { @@ -825,6 +831,7 @@ "intelligence": 7, "time": "48 m", "chapters": 28, + "generic": true, "fun": 5 }, { @@ -843,6 +850,7 @@ "intelligence": 6, "time": "18 m", "chapters": 24, + "generic": true, "fun": 3 }, { @@ -978,6 +986,7 @@ "intelligence": 7, "time": "28 m", "chapters": 40, + "generic": true, "fun": 3 }, { @@ -1073,6 +1082,7 @@ "intelligence": 7, "time": "28 m", "chapters": 40, + "generic": true, "fun": 4 }, { diff --git a/doc/JSON/JSON_INFO.md b/doc/JSON/JSON_INFO.md index e7cb1b9171f69..7701187108b37 100644 --- a/doc/JSON/JSON_INFO.md +++ b/doc/JSON/JSON_INFO.md @@ -3905,6 +3905,7 @@ Books can be defined like this: "fun" : -2, // Morale bonus/penalty for reading "skill" : "computer", // Skill raised "chapters" : 4, // Number of chapters (for fun only books), each reading "consumes" a chapter. Books with no chapters left are less fun (because the content is already known to the character). +"generic": false, // This book counts chapters by item instance instead of by type (this book represents a generic variety of books, like "book of essays") "required_level" : 2, // Minimum skill level required to learn "martial_art": "style_mma", // Martial art learned from this book; incompatible with `skill` "proficiencies": [ // Having this book mitigate lack of proficiency, required for crafting diff --git a/src/character.h b/src/character.h index e7a0ed0d1c108..ebb334b08c925 100644 --- a/src/character.h +++ b/src/character.h @@ -2313,6 +2313,8 @@ class Character : public Creature, public visitable /** Calculates the total fun bonus relative to this character's traits and chapter progress */ bool fun_to_read( const item &book ) const; int book_fun_for( const item &book, const Character &p ) const; + /** The number of chapters remaining for each book itype */ + std::map book_chapters; bool can_pickVolume( const item &it, bool safe = false, const item *avoid = nullptr, bool ignore_pkt_settings = true ) const; diff --git a/src/item.cpp b/src/item.cpp index 099b22a6a494d..d8e3720db5698 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -8857,7 +8857,7 @@ void item::set_browsed( bool browsed ) bool item::is_ecopiable() const { - return has_flag( flag_E_COPIABLE ) || ( is_book() && get_chapters() == 0 ); + return has_flag( flag_E_COPIABLE ) || ( is_book() && !type->book->generic ); } bool item::efiles_all_browsed() const @@ -10776,22 +10776,34 @@ int item::get_chapters() const int item::get_remaining_chapters( const Character &u ) const { - // NOLINTNEXTLINE(cata-translate-string-literal) - const std::string var = string_format( "remaining-chapters-%d", u.getID().get_value() ); - return get_var( var, get_chapters() ); + const itype_id &type = typeId(); + if( is_book() && type->book->generic ) { + // NOLINTNEXTLINE(cata-translate-string-literal) + const std::string var = string_format( "remaining-chapters-%d", u.getID().get_value() ); + return get_var( var, get_chapters() ); + } + const std::map &book_chapters = u.book_chapters; + auto find_chapters = book_chapters.find( type ); + if( find_chapters == book_chapters.end() ) { + return get_chapters(); + } + return find_chapters->second; } -void item::mark_chapter_as_read( const Character &u ) +void item::mark_chapter_as_read( Character &u ) { - // NOLINTNEXTLINE(cata-translate-string-literal) - const std::string var = string_format( "remaining-chapters-%d", u.getID().get_value() ); - if( type->book && type->book->chapters == 0 ) { - // books without chapters will always have remaining chapters == 0, so we don't need to store them - erase_var( var ); + // books without chapters will always have remaining chapters == 0, so we don't need to store them + if( is_book() && get_chapters() == 0 ) { + return; + } + if( is_book() && type->book->generic ) { + // NOLINTNEXTLINE(cata-translate-string-literal) + const std::string var = string_format( "remaining-chapters-%d", u.getID().get_value() ); + const int remain = std::max( 0, get_remaining_chapters( u ) - 1 ); + set_var( var, remain ); return; } - const int remain = std::max( 0, get_remaining_chapters( u ) - 1 ); - set_var( var, remain ); + u.book_chapters[typeId()] = std::max( 0, get_remaining_chapters( u ) - 1 ); } std::set item::get_saved_recipes() const diff --git a/src/item.h b/src/item.h index b1dc6c0304aae..c576af582a857 100644 --- a/src/item.h +++ b/src/item.h @@ -2409,7 +2409,7 @@ class item : public visitable * Mark one chapter of the book as read by the given player. May do nothing if the book has * no unread chapters. This is a per-character setting, see @ref get_remaining_chapters. */ - void mark_chapter_as_read( const Character &u ); + void mark_chapter_as_read( Character &u ); /** * Returns recipes stored on the item (laptops, smartphones, sd cards etc) * Filters out !is_valid() recipes diff --git a/src/item_factory.cpp b/src/item_factory.cpp index ecc7d985c7534..2710e0b97bea1 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -3264,6 +3264,7 @@ void islot_book::load( const JsonObject &jo ) optional( jo, was_loaded, "skill", skill, skill_id::NULL_ID() ); optional( jo, was_loaded, "martial_art", martial_art, matype_id::NULL_ID() ); optional( jo, was_loaded, "chapters", chapters, 0 ); + optional( jo, was_loaded, "generic", generic, false ); optional( jo, was_loaded, "proficiencies", proficiencies ); optional( jo, was_loaded, "scannable", is_scannable, true ); } diff --git a/src/itype.h b/src/itype.h index 49a26619a51df..8fe63a89ab308 100644 --- a/src/itype.h +++ b/src/itype.h @@ -563,6 +563,11 @@ struct islot_book { * "To read" means getting 1 skill point, not all of them. */ time_duration time = 0_turns; + /** + * This book counts chapters by item instance instead of by type + * (i.e. this book represents a generic variety of books, like "book of essays") + */ + bool generic = false; /** * Fun books have chapters; after all are read, the book is less fun. */ diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index a760b18388883..bc19b8cc8fc54 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -1112,6 +1112,7 @@ void Character::load( const JsonObject &data ) data.read( "male", male ); data.read( "cash", cash ); data.read( "recoil", recoil ); + data.read( "book_chapters", book_chapters ); data.read( "in_vehicle", in_vehicle ); data.read( "last_sleep_check", last_sleep_check ); if( data.read( "id", tmpid ) && tmpid.is_valid() ) { @@ -1489,6 +1490,7 @@ void Character::store( JsonOut &json ) const json.member( "cash", cash ); json.member( "recoil", recoil ); + json.member( "book_chapters", book_chapters ); json.member( "in_vehicle", in_vehicle ); json.member( "id", getID() );