Files
Bubberstation/code/modules/library/book.dm
_0Steven 3804b5fe43 Refactors book attackby into item interaction procs, adding screentips, minor qol (#91273)
## About The Pull Request

This mostly just refactors the book `attackby(...)` proc into
`item_interaction(...)`, with some side-effects and screentips.
Notable ones follow here:

We move carving away from combat mode, and instead based it on whether
our tool is a pure writing utensil, a pure carving tool, or both at once
like the captain's fountain pen. If it's a pure tool, both left click
and right click are its respective primary action. If it's both,
however, left click is vandalizing and right click is carving.
This tries to ensure the expected primary interaction is always
left-click, without blocking anything if it could do both.

We move the barcode scanner interaction to the barcode scanner instead.
There's no good reason for this to stay on the book itself.

We add `can_vandalize(...)` as a proc and move all checks in there, so
we can call it after every tgui input. We don't replace the
`to_chat(...)` calls here with balloon alerts because the additional
clarifying info is needed, and it keeps the 'soulful' blind message.

We add `get_attack_self_context(...)` as a proc for getting screentips
on hovering over it in your active hand, because both the bible subtype
and the syndicate bible _bible subtype_ want different screentips there
in different situations, and so while it's clunky it's much less clunky
than the alternatives I considered.

Making it use actual storage datums avoids subtypes needing to make
manual changes to it in the `carve_out(...)` proc, and is just good for
organization.

This doesn't add screentips to most bible-specific interactions, as I
deemed them outside of the scope of this pr.
Those are better off done atomized into another pr.
## Why It's Good For The Game

less attackby, more item interaction
screentips good

I think having the carving interaction on combat mode is unintuitive,
especially when that's the only interaction it can do.
This ensures the expected primary interaction is always left-click.
## Changelog
🆑
refactor: Refactored the book interaction chain. Please report any
issues.
fix: You can no longer vandalize carved out books if you carve it after
opening the input menu but before entering anything.
qol: Added screentips to book interactions.
qol: Added screentips to barcode scanner interactions.
qol: Carving out books no longer uses combat mode, instead using
left-click by default and right-click if your tool can also write.
Screentips account for this.
/🆑

---------

Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
2025-06-08 09:19:15 +02:00

258 lines
9.2 KiB
Plaintext

/obj/item/book
name = "book"
desc = "Crack it open, inhale the musk of its pages, and learn something new."
icon = 'icons/obj/service/library.dmi'
icon_state ="book"
worn_icon_state = "book"
throw_speed = 1
throw_range = 5
w_class = WEIGHT_CLASS_NORMAL //upped to three because books are, y'know, pretty big. (and you could hide them inside eachother recursively forever)
attack_verb_continuous = list("bashes", "whacks", "educates")
attack_verb_simple = list("bash", "whack", "educate")
resistance_flags = FLAMMABLE
drop_sound = 'sound/items/handling/book_drop.ogg'
pickup_sound = 'sound/items/handling/book_pickup.ogg'
/// Maximum icon state number
var/maximum_book_state = 8
/// Game time in 1/10th seconds
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.
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
/// The initial author, for use in var editing and such
var/starting_author
/// The initial bit of content, for use in var editing and such
var/starting_content
/// The packet of information that describes this book
var/datum/book_info/book_data
/obj/item/book/Initialize(mapload)
. = ..()
book_data = new(starting_title, starting_author, starting_content)
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()
data["title"] = book_data.get_title()
data["content"] = book_data.get_content()
return data
/obj/item/book/ui_interact(mob/living/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "MarkdownViewer", name)
ui.open()
/// Proc that handles sending the book information to the user, as well as some housekeeping stuff.
/obj/item/book/proc/display_content(mob/living/user)
credit_book_to_reader(user)
ui_interact(user)
/// Proc that checks if the user is capable of reading the book, for UI interactions and otherwise. Returns TRUE if they can, FALSE if they can't.
/obj/item/book/proc/can_read_book(mob/living/user)
if(user.is_blind())
to_chat(user, span_warning("You are blind and can't read anything!"))
return FALSE
if(!user.can_read(src))
return FALSE
if(carved)
balloon_alert(user, "book is carved out!")
return FALSE
if(!length(book_data.get_content()))
balloon_alert(user, "book is blank!")
return FALSE
return TRUE
/// Proc that adds the book to a list on the user's mind so we know what works of art they've been catching up on.
/obj/item/book/proc/credit_book_to_reader(mob/living/user)
if(!isliving(user) || isnull(user.mind))
return
LAZYINITLIST(user.mind.book_titles_read)
if(starting_title in user.mind.book_titles_read)
return
user.add_mood_event("book_nerd", /datum/mood_event/book_nerd)
user.mind.book_titles_read[starting_title] = TRUE
/obj/item/book/attack_self(mob/user)
if(!can_read_book(user))
return
user.visible_message(span_notice("[user] opens a book titled \"[book_data.title]\" and begins reading intently."))
display_content(user)
/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
/// 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
/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
/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
/// 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
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)]"