///Max Writeable Content Pages per book, players really don't need more than this
#define MAX_PAGES 5
/**
* # Standard Book
*
* Game Object which stores pages of text usually written by players, has other editable information such as the book's
* title, author, summary, and categories. Has other values that are generated when books are acquired through the library
* computer.
*
* Like other User Interfaces that heavily rely on player input, using newer tools such as TGUI presents sanitization issues
* so books will remain using BrowserUI until further notice.
*/
/obj/item/book
name = "book"
icon = 'icons/obj/library.dmi'
icon_state = "book"
throw_speed = 1
throw_range = 5
force = 2
w_class = WEIGHT_CLASS_NORMAL
attack_verb = list("bashed", "whacked")
resistance_flags = FLAMMABLE
drop_sound = 'sound/items/handling/book_drop.ogg'
pickup_sound = 'sound/items/handling/book_pickup.ogg'
///Title & Real name of the book
var/title
///Who wrote the book, can be changed by pen or PC
var/author
///Short summary of the contents of the book, can be changed by pen or PC
var/summary
///Book Rating - Assigned by library computer based on how many/how players have rated this book
var/rating
///Book Categories - used for differentiating types of books, set by players upon upload, viewable upon examining book
var/categories = list()
///The background color of the book, useful for themed programmatic books, must be in #FFFFFF hex color format
var/book_bgcolor = "#FFF2E5"
///Content Pages of the books, this variable is a list of strings containting the HTML + Text of each page
var/pages = list()
///What page is the book currently opened to? Page 0 - Intro Page | Page 1-5 - Content Pages
var/current_page = 0
///Book UI Popup Height
var/book_height = 400
///Book UI Popup Width
var/book_width = 400
///Prevents book from being uploaded - For all printed books
var/copyright = FALSE
///Prevents book contents from being edited
var/protected = FALSE
///Book's id within the library system, unique to each book object, should not be declared manually
var/libraryid
///Indicates whether or not a books pages have been carved out
var/carved = FALSE
///Item that is stored inside the book
var/obj/item/store
/// Cooldown for brain damage loss when reading
COOLDOWN_DECLARE(brain_damage_cooldown)
/obj/item/book/Initialize(mapload, datum/cachedbook/CB, _copyright = FALSE, _protected = FALSE)
. = ..()
if(!CB)
return
author = CB.author
title = CB.title
pages = CB.content
summary = CB.summary
categories = CB.categories
copyright = _copyright
protected = _protected
rating = CB.rating
name = "Book: [CB.title]"
icon_state = "book[rand(1,8)]"
/obj/item/book/attack(mob/M, mob/living/user)
if(user.a_intent == INTENT_HELP)
force = 0
attack_verb = list("educated")
else
force = initial(force)
attack_verb = list("bashed", "whacked")
..()
/obj/item/book/attack_self(mob/user)
if(carved)
//Attempt to remove inserted object, if none found, remind user that someone vandalized their book (Bastards)!
if(!remove_stored_item(user, TRUE))
to_chat(user, "The pages of [title] have been cut out!")
return
user.visible_message("[user] opens a book titled \"[title]\" and begins reading intently.")
read_book(user)
/obj/item/book/attackby(obj/item/I, mob/user, params)
if(is_pen(I))
edit_book(user)
else if(istype(I, /obj/item/barcodescanner))
var/obj/item/barcodescanner/scanner = I
scanner.scanBook(src, user) //abstraction and proper scoping ftw | did you know barcode scanner code used to be here?
return
else if(I.sharp && !carved) //don't use sharp objects on your books if you don't want to carve out all of its pages kids!
carve_book(user, I)
else if(store_item(I, user))
return
else
..()
/obj/item/book/examine(mob/user)
. = ..()
if(isobserver(user))
read_book(user)
/**
* Internal Checker Proc
*
* Gives free pass to observers to read, ensures that all other mobs attempting to read book are A) literated and
* B) are within range to actually read the book.
*/
/obj/item/book/proc/can_read(mob/user)
if(isobserver(user)) //We check this first because ghosts should be able to read any book they can see no matter what
return TRUE
if(!in_range(src, user))
return FALSE
if(!user.is_literate()) //this person was 2 cool 4 school and cannot READ
to_chat(user, "You attempt to the read the book but remember that you don't actually know how to read.")
return FALSE
return TRUE
/**
* Read Book Proc
*
* Checks if players is able to read book and that book is readable before calling the neccesary procs to open up UI
*/
/obj/item/book/proc/read_book(mob/user)
if(!length(pages)) //You can't read a book with no pages in it
to_chat(user, "This book is completely blank!")
return
if(!can_read(user))
return
if(isliving(user))
var/mob/living/L = user
// Books can be read every BRAIN_DAMAGE_BOOK_TIME, and has a minumum delay of BRAIN_DAMAGE_MOB_TIME between seperate book reads.
if(!L.has_status_effect(STATUS_BOOKWYRM) && COOLDOWN_FINISHED(src, brain_damage_cooldown))
if(prob(10))
to_chat(L, "You feel a bit smarter!")
L.adjustBrainLoss(-1)
COOLDOWN_START(src, brain_damage_cooldown, BRAIN_DAMAGE_BOOK_TIME)
L.apply_status_effect(STATUS_BOOKWYRM)
show_content(user) //where all the magic happens
onclose(user, "book")
/**
* Show Content Proc
*
* Builds the browserUI html to show to the player then open up the User interface. It first build the header navigation
* buttons and then builds the rest of the UI based on what page the player is turned to.
*/
/obj/item/book/proc/show_content(mob/user)
var/dat = ""
//First, we're going to choose/generate our header buttons for switching pages and store it in var/dat
var/header_left = "
"
var/header_right = ""
if(length(pages)) //No need to have page switching buttons if there's no pages
if(current_page < length(pages))
header_right = "
"
if(current_page)
header_left = ""
dat += header_left + header_right
//Now we're going to display the header buttons + the current page selected, if it's page 0, we display the cover_page instead
if(!current_page)
var/cover_page = {"[title]
Written by: [author]
Summary: [summary]"}
user << browse("[dat]
" + "[cover_page]", "window=book[UID()];size=400x400")
return
else
user << browse("[dat]
" + "[pages[current_page]]", "window=book[UID()]")
/obj/item/book/Topic(href, href_list)
if(..() || isobserver(usr))
return
if(href_list["next_page"])
if(current_page > length(pages)) //should never be false, but just in-case
current_page = length(pages)
return
current_page++
playsound(loc, "pageturn", 50, 1)
read_book(usr) //scuffed but this is how you update the UI
updateUsrDialog()
if(href_list["prev_page"])
if(current_page < 0) //should never be false, but just in-case
current_page = 0
return
current_page--
playsound(loc, "pageturn", 50, 1)
read_book(usr) //scuffed but this is how you update the UI
updateUsrDialog()
/**
* Edit Book Proc
*
* This is where a lot of the magic happens, upon interacting with the book with a pen, this proc will open up options
* for the player to edit the book. Most importantly, this is where we account for player stupidity and maliciousness
* any input must strip/reject bad text and HTML from user input, additionally we account for players trying to screw
* with the Database. This will also limit the max characters players can upload at one time to prevent spamming.
*/
/obj/item/book/proc/edit_book(mob/user)
if(protected) //we don't want people touching "special" books, especially ones that use iframes
to_chat(user, "These pages don't seem to take the ink well. Looks like you can't modify it.")
return
var/choice = tgui_input_list(user, "What would you like to edit?", "Book Edit", list("Title", "Edit Current Page", "Author", "Summary", "Add Page", "Remove Page"))
switch(choice)
if("Title")
var/newtitle = reject_bad_text(stripped_input(user, "Write a new title:"))
if(!newtitle)
to_chat(user, "You change your mind.")
return
//Like with paper, the name (not title) of the book should indicate that THIS IS A BOOK when actions are performed with it
//this is to prevent players from naming it "Nuclear Authentification Disk" or "Energy Sword" to fuck with security
name = "Book: " + newtitle
title = newtitle
if("Author")
var/newauthor = stripped_input(user, "Write the author's name:")
if(!newauthor)
to_chat(user, "You change your mind.")
return
author = newauthor
if("Summary")
var/newsummary = strip_html(input(user, "Write the new summary:") as message|null, MAX_SUMMARY_LEN)
if(!newsummary)
to_chat(user, "You change your mind.")
return
summary = newsummary
if("Edit Current Page")
if(carved)
to_chat(user, "The pages of [title] have been cut out!")
return
if(!current_page)
to_chat(user, "You need to turn to a page before writing in the book.")
return
var/character_space_remaining = MAX_CHARACTERS_PER_BOOKPAGE - length(pages[current_page])
if(character_space_remaining <= 0)
to_chat(user, "There's not enough space left on this page to write anything!")
return
var/content = strip_html(input(user, "Add Text to this page, you have [character_space_remaining] characters of space left:") as message|null, MAX_CHARACTERS_PER_BOOKPAGE)
if(!content)
to_chat(user, "You change your mind.")
return
//check if length of current text content + what player is adding is larger than our character limit
else if((length(content) + length(pages[current_page])) > MAX_CHARACTERS_PER_BOOKPAGE)
//if true, let's cut down the text to fit perfectly into our character limit, player is only half-pissed!
pages[current_page] += dd_limittext(content, (MAX_CHARACTERS_PER_BOOKPAGE - length(pages[current_page])))
else
pages[current_page] += content
if("Add Page")
if(carved)
to_chat(user, "You can't add anymore pages, the pages of [title] have been cut out and the book is ruined!")
return
if(length(pages) >= MAX_PAGES)
to_chat(user, "You can't fit anymore pages in this book!")
return
to_chat(user, "You add another page to the book!")
pages += " "
if("Remove Page")
if(!length(pages))
to_chat(user, "There aren't any pages in this book!")
return
var/page_choice = input(user, "There are [length(pages)] pages, which page number would you like to remove?", "Input Page Number", null) as num|null
if(!page_choice)
to_chat(user, "You change your mind.")
return
if(page_choice <= 0 || page_choice > length(pages))
to_chat(user, "That is not an acceptable value.= MAX_PAGES)
return FALSE
pages += text
current_page = length(pages) //open to newely added page so player can edit it
return TRUE
/obj/item/book/proc/remove_page(page_number)
pages -= pages[page_number]
current_page = min(current_page, length(pages)) //if page_number is somehow at a value it shouldn't be we fix it here aswell
return TRUE //we want to make sure whatever is calling this proc knows the operation was succesful
/obj/item/book/proc/carve_book(mob/user, obj/item/I)
if(carved)
to_chat(user, "[title] has already been carved out!")
return
if(!I.sharp)
to_chat(user, "You can't carve [title] using that!")
return
to_chat(user, "You begin to carve out [title].")
if(I.use_tool(src, user, 30, volume = I.tool_volume))
user.visible_message("[user] appears to carve out the pages inside of [title]!",\
"You carve out [title]!")
carved = TRUE
return TRUE
/obj/item/book/proc/store_item(obj/item/I, mob/user)
if(!carved)
return
if(store)
to_chat(user, "There is already something in [src]!")
return
//does it exist, if so is it an abstract item?
if(!istype(I) || (I.flags & ABSTRACT))
return
if(I.flags & NODROP)
to_chat(user, "[I] stays stuck to your hand when you try and hide it in the book!.")
return
//Checking to make sure the item we're storing isn't larger than/equal to size of the book, prevents recursive storing aswell
if(I.w_class >= w_class)
to_chat(user, "[I] is to large to fit in [src].")
return
user.drop_item()
I.forceMove(src)
RegisterSignal(I, COMSIG_PARENT_QDELETING, PROC_REF(clear_stored_item)) //ensure proper GC'ing
store = I
to_chat(user, "You hide [I] in [name].")
return TRUE
///needed for proper GC'ing
/obj/item/book/proc/clear_stored_item()
store = null
/obj/item/book/proc/remove_stored_item(mob/user, display_message = TRUE)
if(!store)
if(display_message) //we don't wanna display this message in certain cases if there's not a user removing it
to_chat(user, "You search [name] but there is nothing in it!")
return FALSE
if(display_message)
to_chat(user, "You carefully remove [store] from [name]!")
store.forceMove(get_turf(store.loc))
clear_stored_item()
UnregisterSignal(store, COMSIG_PARENT_QDELETING)
return TRUE
//* Book Spawners n'stuff *//
/obj/item/book/random
icon_state = "random_book"
var/amount = 1
/obj/item/book/random/Initialize()
..()
var/list/books = GLOB.library_catalog.get_random_book(amount)
for(var/datum/cachedbook/book as anything in books)
new /obj/item/book(loc, book, TRUE, FALSE)
return INITIALIZE_HINT_QDEL
/obj/item/book/random/triple
amount = 3
//* Codex Gigas *//
//This book used to have its own dm file, due to devil code removal, it is now only a cosmetic item for the time being
//this will be its resting place until it is used for something else eventually.
/obj/item/book/codex_gigas
name = "\improper Codex Gigas"
desc = "A book documenting the nature of devils, it seems whatever magic that once possessed this codex is long gone."
icon_state = "demonomicon"
throw_speed = 1
throw_range = 10
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
author = "Forces beyond your comprehension"
protected = TRUE
title = "The codex gigas"
copyright = TRUE
#undef MAX_PAGES