diff --git a/code/datums/storage/subtypes/others/misc.dm b/code/datums/storage/subtypes/others/misc.dm index 91d2c233a31..154ac9944ea 100644 --- a/code/datums/storage/subtypes/others/misc.dm +++ b/code/datums/storage/subtypes/others/misc.dm @@ -276,3 +276,10 @@ /datum/storage/toiletbong/New(atom/parent, max_slots, max_specific_storage, max_total_storage) . = ..() set_holdable(/obj/item/food) + +/// Carved Books +/datum/storage/carved_book + max_slots = 1 + +/datum/storage/carved_book/bible + max_specific_storage = WEIGHT_CLASS_SMALL diff --git a/code/modules/library/barcode_scanner.dm b/code/modules/library/barcode_scanner.dm index 5cacf03e013..7b650bc5f76 100644 --- a/code/modules/library/barcode_scanner.dm +++ b/code/modules/library/barcode_scanner.dm @@ -12,18 +12,77 @@ ///The current scanning mode (BARCODE_SCANNER_CHECKIN|BARCODE_SCANNER_INVENTORY) var/scan_mode = BARCODE_SCANNER_CHECKIN +/obj/item/barcodescanner/Initialize(mapload) + . = ..() + register_item_context() + register_context() + +/obj/item/barcodescanner/add_item_context(obj/item/source, list/context, atom/target, mob/living/user) + if(!istype(target, /obj/item/book)) + return NONE + + switch(scan_mode) + if(BARCODE_SCANNER_CHECKIN) + context[SCREENTIP_CONTEXT_LMB] = "Check in" + if(BARCODE_SCANNER_INVENTORY) + context[SCREENTIP_CONTEXT_LMB] = "Add to inventory" + return CONTEXTUAL_SCREENTIP_SET + +/obj/item/barcodescanner/add_context(atom/source, list/context, obj/item/held_item, mob/living/user) + if(held_item == src) + context[SCREENTIP_CONTEXT_LMB] = "Toggle scanning mode" + return CONTEXTUAL_SCREENTIP_SET + return NONE + +/obj/item/barcodescanner/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + if(istype(interacting_with, /obj/item/book)) + return interact_with_book(interacting_with, user) + return NONE + +/obj/item/barcodescanner/proc/interact_with_book(obj/item/book/target_book, mob/living/user) + var/obj/machinery/computer/libraryconsole/bookmanagement/linked_computer = computer_ref?.resolve() + if(isnull(linked_computer)) + user.balloon_alert(user, "not connected to computer!") + return ITEM_INTERACT_BLOCKING + + switch(scan_mode) + if(BARCODE_SCANNER_CHECKIN) + var/list/checkouts = linked_computer.checkouts + for(var/checkout_ref in checkouts) + var/datum/borrowbook/maybe_ours = checkouts[checkout_ref] + if(!target_book.book_data.compare(maybe_ours.book_data)) + continue + checkouts -= checkout_ref + linked_computer.checkout_update() + balloon_alert(user, "checked in") + playsound(src, 'sound/items/barcodebeep.ogg', 20, FALSE) + return ITEM_INTERACT_SUCCESS + + user.balloon_alert(user, "isn't checked out!") + return ITEM_INTERACT_BLOCKING + + if(BARCODE_SCANNER_INVENTORY) + var/datum/book_info/our_copy = target_book.book_data.return_copy() + linked_computer.inventory[ref(our_copy)] = our_copy + linked_computer.inventory_update() + balloon_alert(user, "added to inventory") + playsound(src, 'sound/items/barcodebeep.ogg', 20, FALSE) + return ITEM_INTERACT_SUCCESS + + return NONE + /obj/item/barcodescanner/attack_self(mob/user) . = ..() if(.) return if(!computer_ref?.resolve()) - user.balloon_alert(user, "not connected to computer!") + balloon_alert(user, "connect to computer!") return switch(scan_mode) if(BARCODE_SCANNER_CHECKIN) scan_mode = BARCODE_SCANNER_INVENTORY - user.balloon_alert(user, "inventory adding mode") + balloon_alert(user, "inventory adding mode") if(BARCODE_SCANNER_INVENTORY) scan_mode = BARCODE_SCANNER_CHECKIN - user.balloon_alert(user, "check-in mode") + balloon_alert(user, "check-in mode") playsound(loc, 'sound/items/click.ogg', 20, TRUE) diff --git a/code/modules/library/bibles.dm b/code/modules/library/bibles.dm index dda66b653b8..ab4fbb2873b 100644 --- a/code/modules/library/bibles.dm +++ b/code/modules/library/bibles.dm @@ -75,6 +75,8 @@ GLOBAL_LIST_INIT(bibleitemstates, list( righthand_file = 'icons/mob/inhands/items/books_righthand.dmi' force_string = "holy" unique = TRUE + carved_storage_type = /datum/storage/carved_book/bible + /// Deity this bible is related to var/deity_name = "Space Jesus" @@ -114,6 +116,10 @@ GLOBAL_LIST_INIT(bibleitemstates, list( else . += span_notice("[src] can be unpacked by hitting the floor of a holy area with it.") +/obj/item/book/bible/get_attack_self_context(mob/living/user) + if(can_set_bible_skin(user)) + return "Select bible skin" + /obj/item/book/bible/proc/curse_heathen(datum/source, mob/living/user, obj/item/burning_tool) SIGNAL_HANDLER @@ -131,19 +137,20 @@ GLOBAL_LIST_INIT(bibleitemstates, list( to_chat(user, span_userdanger("[deity_name] cast a curse upon thee!")) user.AddComponent(/datum/component/omen/bible) -/obj/item/book/bible/carve_out(obj/item/carving_item, mob/living/user) - . = ..() - atom_storage.max_specific_storage = WEIGHT_CLASS_SMALL - /obj/item/book/bible/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] is offering [user.p_them()]self to [deity_name]! It looks like [user.p_theyre()] trying to commit suicide!")) return BRUTELOSS -/obj/item/book/bible/attack_self(mob/living/carbon/human/user) +/obj/item/book/bible/proc/can_set_bible_skin(mob/living/user) if(GLOB.bible_icon_state) return FALSE if(user?.mind?.holy_role != HOLY_ROLE_HIGHPRIEST) return FALSE + return TRUE + +/obj/item/book/bible/attack_self(mob/living/carbon/human/user) + if(!can_set_bible_skin(user)) + return FALSE var/list/skins = list() for(var/i in 1 to GLOB.biblestates.len) @@ -183,13 +190,11 @@ GLOBAL_LIST_INIT(bibleitemstates, list( * * user The mob interacting with the menu */ /obj/item/book/bible/proc/check_menu(mob/living/carbon/human/user) - if(GLOB.bible_icon_state) - return FALSE if(!istype(user) || !user.is_holding(src)) return FALSE if(user.incapacitated) return FALSE - if(user.mind?.holy_role != HOLY_ROLE_HIGHPRIEST) + if(!can_set_bible_skin(user)) return FALSE return TRUE @@ -384,6 +389,10 @@ GLOBAL_LIST_INIT(bibleitemstates, list( if(owner_name) . += span_warning("The name [owner_name] is written in blood inside the cover.") +/obj/item/book/bible/syndicate/get_attack_self_context(mob/living/user) + if(uses) + return "Read" + /obj/item/book/bible/syndicate/attack(mob/living/target_mob, mob/living/carbon/human/user, params, heal_mode = TRUE) if(!user.combat_mode) return ..() diff --git a/code/modules/library/book.dm b/code/modules/library/book.dm index 8fc5139ce5d..e25935cf074 100644 --- a/code/modules/library/book.dm +++ b/code/modules/library/book.dm @@ -18,8 +18,10 @@ var/due_date = 0 /// false - Normal book, true - Should not be treated as normal book, unable to be copied, unable to be modified var/unique = FALSE - /// whether or not we have been carved out + /// Whether or not we have been carved out. var/carved = FALSE + /// The typepath for the storage datum we use when carved out. + var/carved_storage_type = /datum/storage/carved_book /// The initial title, for use in var editing and such var/starting_title @@ -36,12 +38,39 @@ AddElement(/datum/element/falling_hazard, damage = 5, wound_bonus = 0, hardhat_safety = TRUE, crushes = FALSE, impact_sound = drop_sound) AddElement(/datum/element/burn_on_item_ignition) + register_context() /obj/item/book/examine(mob/user) . = ..() if(carved) . += span_notice("[src] has been hollowed out.") +/obj/item/book/add_context(atom/source, list/context, obj/item/held_item, mob/living/user) + if(isnull(held_item)) + return NONE + + if(held_item == src) + var/attack_self_context = get_attack_self_context(user) + if(!attack_self_context) + return NONE + context[SCREENTIP_CONTEXT_LMB] = attack_self_context + return CONTEXTUAL_SCREENTIP_SET + + if(IS_WRITING_UTENSIL(held_item)) + context[SCREENTIP_CONTEXT_LMB] = "Vandalize" + if(is_carving_tool(held_item)) + context[SCREENTIP_CONTEXT_RMB] = "Carve out" + return CONTEXTUAL_SCREENTIP_SET + + if(is_carving_tool(held_item)) + context[SCREENTIP_CONTEXT_LMB] = "Carve out" + return CONTEXTUAL_SCREENTIP_SET + return NONE + +/// Gets the context to add for clicking the book inhand. Returns null if none. +/obj/item/book/proc/get_attack_self_context(mob/living/user) + return "Read" + /obj/item/book/ui_static_data(mob/user) var/list/data = list() data["author"] = book_data.get_author() @@ -98,116 +127,131 @@ user.visible_message(span_notice("[user] opens a book titled \"[book_data.title]\" and begins reading intently.")) display_content(user) -/obj/item/book/attackby(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) - if(IS_WRITING_UTENSIL(attacking_item)) - if(!user.can_perform_action(src) || !user.can_write(attacking_item)) - return - if(user.is_blind()) - to_chat(user, span_warning("As you are trying to write on the book, you suddenly feel very stupid!")) - return - if(unique) - to_chat(user, span_warning("These pages don't seem to take the ink well! Looks like you can't modify it.")) - return - if(carved) - to_chat(user, span_warning("The book has been carved out! There is nothing to be vandalized.")) - return +/obj/item/book/proc/is_carving_tool(obj/item/tool) + PRIVATE_PROC(TRUE) + if(tool.get_sharpness() & SHARP_EDGED) + return TRUE + if(tool.tool_behaviour == TOOL_WIRECUTTER) + return TRUE + return FALSE - var/choice = tgui_input_list(usr, "What would you like to change?", "Book Alteration", list("Title", "Contents", "Author", "Cancel")) - if(isnull(choice)) - return - if(!user.can_perform_action(src) || !user.can_write(attacking_item)) - return - switch(choice) - if("Title") - var/newtitle = reject_bad_text(tgui_input_text(user, "Write a new title", "Book Title", max_length = 30)) - if(!user.can_perform_action(src) || !user.can_write(attacking_item)) - return - if (length_char(newtitle) > 30) - to_chat(user, span_warning("That title won't fit on the cover!")) - return - if(!newtitle) - to_chat(user, span_warning("That title is invalid.")) - return - name = newtitle - playsound(src, SFX_WRITING_PEN, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE, SOUND_FALLOFF_EXPONENT + 3, ignore_walls = FALSE) - book_data.set_title(html_decode(newtitle)) //Don't want to double encode here - if("Contents") - var/content = tgui_input_text(user, "Write your book's contents (HTML NOT allowed)", "Book Contents", max_length = MAX_PAPER_LENGTH, multiline = TRUE) - if(!user.can_perform_action(src) || !user.can_write(attacking_item)) - return - if(!content) - to_chat(user, span_warning("The content is invalid.")) - return - book_data.set_content(html_decode(content)) - playsound(src, SFX_WRITING_PEN, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE, SOUND_FALLOFF_EXPONENT + 3, ignore_walls = FALSE) - if("Author") - var/author = tgui_input_text(user, "Write the author's name", "Author Name", max_length = MAX_NAME_LEN) - if(!user.can_perform_action(src) || !user.can_write(attacking_item)) - return - if(!author) - to_chat(user, span_warning("The name is invalid.")) - return - book_data.set_author(html_decode(author)) //Setting this encodes, don't want to double up - playsound(src, SFX_WRITING_PEN, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE, SOUND_FALLOFF_EXPONENT + 3, ignore_walls = FALSE) +/// Checks for whether we can vandalize this book, to ensure we still can after each input. +/// Uses to_chat over balloon alerts to give more detailed information as to why. +/obj/item/book/proc/can_vandalize(mob/living/user, obj/item/tool) + if(!user.can_perform_action(src) || !user.can_write(tool)) + return FALSE + if(user.is_blind()) + to_chat(user, span_warning("As you are trying to write on the book, you suddenly feel very stupid!")) + return FALSE + if(unique) + to_chat(user, span_warning("These pages don't seem to take the ink well! Looks like you can't modify it.")) + return FALSE + if(carved) + to_chat(user, span_warning("The book has been carved out! There is nothing to be vandalized.")) + return FALSE + return TRUE - else if(istype(attacking_item, /obj/item/barcodescanner)) - var/obj/item/barcodescanner/scanner = attacking_item - var/obj/machinery/computer/libraryconsole/bookmanagement/computer = scanner.computer_ref?.resolve() - if(!computer) - user.balloon_alert(user, "not connected to computer!") - return +/obj/item/book/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + // Items can both be carving tools and writing utensils. + // Because of this, we flip interaction priority on secondary. + // This means pure writing utensils have writing as their primary action, + // pure carving tools have carving as their primary action, + // but items with both have primary write secondary carve. + if(IS_WRITING_UTENSIL(tool)) + return writing_utensil_act(user, tool) + if(is_carving_tool(tool)) + return carving_act(user, tool) + return NONE - switch(scanner.scan_mode) - if(BARCODE_SCANNER_CHECKIN) - var/list/checkouts = computer.checkouts - for(var/checkout_ref in checkouts) - var/datum/borrowbook/maybe_ours = checkouts[checkout_ref] - if(!book_data.compare(maybe_ours.book_data)) - continue - checkouts -= checkout_ref - computer.checkout_update() - user.balloon_alert(user, "book checked in") - playsound(loc, 'sound/items/barcodebeep.ogg', 20, FALSE) - return +/obj/item/book/item_interaction_secondary(mob/living/user, obj/item/tool, list/modifiers) + if(is_carving_tool(tool)) + return carving_act(user, tool) + if(IS_WRITING_UTENSIL(tool)) + return writing_utensil_act(user, tool) + return NONE - user.balloon_alert(user, "book not checked out!") - return - if(BARCODE_SCANNER_INVENTORY) - var/datum/book_info/our_copy = book_data.return_copy() - computer.inventory[ref(our_copy)] = our_copy - computer.inventory_update() - user.balloon_alert(user, "book added to inventory") - playsound(loc, 'sound/items/barcodebeep.ogg', 20, FALSE) +/// Called when user clicks on the book with a writing utensil. Attempts to vandalize the book. +/obj/item/book/proc/writing_utensil_act(mob/living/user, obj/item/tool) + if(!can_vandalize(user, tool)) + return ITEM_INTERACT_BLOCKING - else if(try_carve(attacking_item, user, modifiers)) - return - return ..() + var/choice = tgui_input_list(usr, "What would you like to change?", "Book Alteration", list("Title", "Contents", "Author", "Cancel")) + if(isnull(choice)) + return ITEM_INTERACT_BLOCKING + if(!can_vandalize(user, tool)) + return ITEM_INTERACT_BLOCKING + + switch(choice) + if("Title") + return vandalize_title(user, tool) + if("Contents") + return vandalize_contents(user, tool) + if("Author") + return vandalize_author(user, tool) + + return NONE + +/obj/item/book/proc/vandalize_title(mob/living/user, obj/item/tool) + var/newtitle = reject_bad_text(tgui_input_text(user, "Write a new title", "Book Title", max_length = 30)) + if(!newtitle) + balloon_alert(user, "invalid input!") + return ITEM_INTERACT_BLOCKING + if(length_char(newtitle) > 30) + balloon_alert(user, "too long!") + return ITEM_INTERACT_BLOCKING + if(!can_vandalize(user, tool)) + return ITEM_INTERACT_BLOCKING + + name = newtitle + book_data.set_title(html_decode(newtitle)) //Don't want to double encode here + playsound(src, SFX_WRITING_PEN, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE, SOUND_FALLOFF_EXPONENT + 3, ignore_walls = FALSE) + return ITEM_INTERACT_SUCCESS + +/obj/item/book/proc/vandalize_contents(mob/living/user, obj/item/tool) + var/content = tgui_input_text(user, "Write your book's contents (HTML NOT allowed)", "Book Contents", max_length = MAX_PAPER_LENGTH, multiline = TRUE) + if(!content) + balloon_alert(user, "invalid input!") + return ITEM_INTERACT_BLOCKING + if(!can_vandalize(user, tool)) + return ITEM_INTERACT_BLOCKING + + book_data.set_content(html_decode(content)) + playsound(src, SFX_WRITING_PEN, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE, SOUND_FALLOFF_EXPONENT + 3, ignore_walls = FALSE) + return ITEM_INTERACT_SUCCESS + +/obj/item/book/proc/vandalize_author(mob/living/user, obj/item/tool) + var/author = tgui_input_text(user, "Write the author's name", "Author Name", max_length = MAX_NAME_LEN) + if(!author) + balloon_alert(user, "invalid input!") + return ITEM_INTERACT_BLOCKING + if(!can_vandalize(user, tool)) + return ITEM_INTERACT_BLOCKING + + book_data.set_author(html_decode(author)) //Setting this encodes, don't want to double up + playsound(src, SFX_WRITING_PEN, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE, SOUND_FALLOFF_EXPONENT + 3, ignore_walls = FALSE) + return ITEM_INTERACT_SUCCESS + +/// Called when user clicks on the book with a carving utensil. Attempts to carve the book. +/obj/item/book/proc/carving_act(mob/living/user, obj/item/tool) + if(carved) + balloon_alert(user, "already carved!") + return ITEM_INTERACT_BLOCKING + + balloon_alert(user, "carving out...") + if(!do_after(user, 3 SECONDS, target = src)) + balloon_alert(user, "interrupted!") + return ITEM_INTERACT_BLOCKING + + balloon_alert(user, "carved out") + playsound(src, 'sound/effects/cloth_rip.ogg', vol = 75, vary = TRUE) + carve_out() + return ITEM_INTERACT_SUCCESS + +/// Handles setting everything a carved book needs. +/obj/item/book/proc/carve_out() + carved = TRUE + create_storage(storage_type = carved_storage_type) /// Generates a random icon state for the book /obj/item/book/proc/gen_random_icon_state() icon_state = "book[rand(1, maximum_book_state)]" - -/// Called when user attempts to carve the book with an item -/obj/item/book/proc/try_carve(obj/item/carving_item, mob/living/user, list/modifiers) - if(carved) - return FALSE - if(!user.combat_mode) - return FALSE - //special check for wirecutter's because they don't have a sharp edge - if((carving_item.get_sharpness() & SHARP_EDGED) || (carving_item.tool_behaviour == TOOL_WIRECUTTER)) - balloon_alert(user, "carving out...") - if(!do_after(user, 3 SECONDS, target = src)) - balloon_alert(user, "interrupted!") - return FALSE - carve_out(carving_item, user) - return TRUE - return FALSE - -/// Called when the book gets carved successfully -/obj/item/book/proc/carve_out(obj/item/carving_item, mob/living/user) - if(user) - balloon_alert(user, "carved out") - playsound(src, 'sound/effects/cloth_rip.ogg', vol = 75, vary = TRUE) - carved = TRUE - create_storage(max_slots = 1) - return TRUE