Files
Bubberstation/code/datums/memory/_memory.dm
san7890 c403a6eccc Wraps lowertext() to ensure proper stringification. (#82442)
## About The Pull Request

Fixes #82440

This PR just creates a new macro, `LOWER_TEXT()` (yes the irony is not
lost on me) to wrap around all calls of `lowertext()` and ensure that
whatever we input into that proc will be stringified using the `"[]"`
(or `tostring()` for the nerds) operator. very simple.

I also added a linter to enforce this (and prevent all forms of
regression) because I think that machines should do the menial work and
we shouldn't expect maintainers to remember this, let me know if you
disagree. if there is a time when it should be opted out for some
reason, the linter does respect it if you wrap your input with the
`UNLINT()` function.
2024-04-10 12:19:43 -07:00

425 lines
14 KiB
Plaintext

/**
* Little tidbits of past events generated by the player doing things.
*
* Can be used in engravings, dreams, and changeling succs.
*
* All of those things are supposed to be taken vaguely -
* Engravings crossround and should not include names, dreams and succs are memory goop.
* As such, the generated text of the memory is vague.
*
* Don't hold any references in this, it's not necessary.
*/
/datum/memory
/// The name of the MEMORY that the user sees.
/// Something like "The time the Clown did a sweet flip".
var/name
/// Job of the person memorizing the event
var/memorizer
/// Mind of who is memorizing the event
var/datum/mind/memorizer_mind
/// The value of the mood in it's worth as a story.
/// Defines how beautiful art from it can be, and whether or not it stays in persistence.
var/story_value = STORY_VALUE_NONE
/// Flags of any special behavior for the memory
var/memory_flags = NONE
/// If this memory changes based on mood, this is the verb it uses.
var/mood_verb
// Below are common infobits passed to memories, placed into strings.
// You can add your own to subtypes of memories to add more infobits.
// Just remember to override New() with your additional arguments
// (And call parent at the end of your override, not the beginning).
/// The main character of the memory.
var/protagonist_name
/// The side character of the memory.
var/deuteragonist_name
/// The main villain of the memory.
var/antagonist_name
/// Where this memory took place.
var/where
/datum/memory/New(
datum/mind/memorizer_mind,
atom/protagonist,
atom/deuteragonist,
atom/antagonist,
...
)
// If we weren't supplied a protag, use our memorizer by default.
if(isnull(protagonist))
protagonist = memorizer_mind.current
src.memorizer_mind = memorizer_mind
src.memorizer = build_story_character(memorizer_mind)
src.protagonist_name = build_story_character(protagonist)
src.deuteragonist_name = build_story_character(deuteragonist)
src.antagonist_name = build_story_character(antagonist)
if(!src.where && isatom(protagonist) && !(memory_flags & MEMORY_FLAG_NOLOCATION))
src.where = get_area_name(protagonist)
if(!(memory_flags & MEMORY_FLAG_NOMOOD))
var/story_mood = MOODLESS_MEMORY
if(isliving(protagonist))
var/mob/living/the_main_character = protagonist
story_mood = the_main_character.mob_mood?.mood_level || MOODLESS_MEMORY
select_mood_verb(story_mood)
// This happens after everything's all set, remember this for New overrides
generate_memory_name()
/datum/memory/Destroy(force)
memorizer_mind = null
return ..()
/datum/memory/serialize_list(list/options, list/semvers)
. = ..()
.["name"] = name
.["memorizer"] = memorizer
.["story_value"] = story_value
.["memory_flags"] = memory_flags
.["mood_verb"] = mood_verb
.["protagonist_name"] = protagonist_name
.["deuteragonist_name"] = deuteragonist_name
.["antagonist_name"] = antagonist_name
.["where"] = where
SET_SERIALIZATION_SEMVER(semvers, "1.0.0")
return .
/**
* Generates a name for the memory.
*/
/datum/memory/proc/generate_memory_name()
var/list/potential_names = get_names()
if(!length(potential_names))
// Someone forgot to implement get_names - it will stack trace, so we just need to return
name = "Erroneous memory - This is a bug"
story_value = STORY_VALUE_SHIT
memory_flags |= (MEMORY_FLAG_NOPERSISTENCE|MEMORY_NO_STORY)
return
name = capitalize(pick(potential_names))
/**
* Selects a mood related verb for the memory.
*
* Arguments
* * story_mood - What mood level should we use to select a verb from?
*/
/datum/memory/proc/select_mood_verb(story_mood)
if(story_mood == MOODLESS_MEMORY)
// The protagonist didn't end up having a mood, so just continue on
memory_flags |= MEMORY_FLAG_NOMOOD
return
var/list/possible_verbs
switch(story_mood)
if(MOOD_SAD4 to MOOD_SAD2)
possible_verbs = get_sad_moods()
if(MOOD_SAD2 to MOOD_HAPPY2)
possible_verbs = get_neutral_moods()
if(MOOD_HAPPY2 to MOOD_HAPPY4)
possible_verbs = get_happy_moods()
if(!length(possible_verbs))
return
mood_verb = pick(possible_verbs)
/**
* Returns a list of names for [proc/select_mood_verb] to select from.
*
* This is necessary to implement. Names should be at-a-glance summaries of what the memory entails.
*
* For example: "The time the Clown did a sweet flip.".
* You can use any information tidbits in your names to fill them out.
* Your names should be puncuated.
*/
/datum/memory/proc/get_names()
SHOULD_CALL_PARENT(FALSE)
stack_trace("[type] didn't have any names setup, these are necessary for all memories!")
return list()
/**
* Returns a list of story starts for the memory.
*
* Starts are necessary if [MEMORY_FLAG_NOSTORY] is not set. They are used in generating stories out of memories.
*
* For example: "The Clown cracks his hands and honks his horn as he prepares to do a backflip".
* You can use any information tidbits in your names to fill them out.
* If the memory is not [MEMORY_FLAG_NOMOOD], your starts should NOT be puncuated, as a mood phrase will follow.
* They should also be in the present tense.
*/
/datum/memory/proc/get_starts()
SHOULD_CALL_PARENT(FALSE)
stack_trace("[type] didn't have any starts setup, these are necessary if the MEMORY_FLAG_NOSTORY is not set!")
return list()
/**
* Returns a list of mood phrases for the memory.
*
* Mood phrases are necessary if [MEMORY_FLAG_NOMOOD] is not set. They are used in making stories out of memories.
* These are phrases that change in their verbage depending on the mood of the PROTAGONIST at the time of the memory.
*
* For example: "The clown grins (this is the mood verb) at the audience.".
* You can use any information tidbits in your names to fill them out.
* Mood phrases should be punctated, as they are their own independent clause.
* Mood phrases should always include the [mood_verb] var, as well.
*/
/datum/memory/proc/get_moods()
SHOULD_CALL_PARENT(FALSE)
stack_trace("[type] didn't have any mood phrases, these are necessary if the MEMORY_FLAG_NOMOOD is not set!")
return list()
/**
* Used to select a mood verb if the protagonist is happy for memories that do not have [MEMORY_FLAG_NOMOOD] set.
*/
/datum/memory/proc/get_happy_moods()
return list(
"chuckles",
"has a huge grin",
"has a twisted grin like a maniac",
"is silently working away like a pro",
"looks determined",
"seems cheerful about it all",
"seems confident",
"whistles to themselves",
)
/**
* Used to select a mood verb if the protagonist is neither happy or sad for memories that do not have [MEMORY_FLAG_NOMOOD] set.
*/
/datum/memory/proc/get_neutral_moods()
return list(
"appears clueless",
"is darting their eyes around",
"is impatient",
"is uninterested",
"looks around cautiously",
"seems a bit sleepy",
"seems okay",
"works diligently",
)
/**
* Used to select a mood verb if the protagonist is sad for memories that do not have [MEMORY_FLAG_NOMOOD] set.
*/
/datum/memory/proc/get_sad_moods()
return list(
"appears crushed",
"has dried tears on their face",
"is complaining loudly",
"is having a temper tantrum",
"is whiney about it all",
"looks angry",
"seems sad",
)
/**
* Returns a list of locations for use in stories which do not have [MEMORY_FLAG_NOLOCATION] set.
*/
/datum/memory/proc/get_locations()
return list(
"in [where].",
"while in [where]."
)
/**
* Generates a story based on this memory.
*
* Arguments
* * story_type - for used in grabbing phrases from memories.json involving specific types of stories.
* * story_flags - any additional flags involving the story
*/
/datum/memory/proc/generate_story(story_type, story_flags)
//entirely independent vars (not related to the action or story type)
var/static/list/something_pool = list(
/mob/living/basic/bat,
/mob/living/basic/bear,
/mob/living/basic/blob_minion/blobbernaut,
/mob/living/basic/butterfly,
/mob/living/basic/carp,
/mob/living/basic/carp/magic,
/mob/living/basic/carp/magic/chaos,
/mob/living/basic/chick,
/mob/living/basic/chicken,
/mob/living/basic/cow,
/mob/living/basic/cow/wisdom,
/mob/living/basic/crab,
/mob/living/basic/goat,
/mob/living/basic/gorilla,
/mob/living/basic/headslug,
/mob/living/basic/killer_tomato,
/mob/living/basic/lizard,
/mob/living/basic/mining/goliath,
/mob/living/basic/mining/watcher,
/mob/living/basic/morph,
/mob/living/basic/mouse,
/mob/living/basic/mushroom,
/mob/living/basic/parrot,
/mob/living/basic/pet/cat,
/mob/living/basic/pet/cat/cak,
/mob/living/basic/pet/dog/breaddog,
/mob/living/basic/pet/dog/corgi,
/mob/living/basic/pet/dog/pug,
/mob/living/basic/pet/fox,
/mob/living/basic/spider/giant,
/mob/living/basic/spider/giant/hunter,
/mob/living/basic/statue,
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
/obj/item/food/sausage/american,
/obj/item/skub,
)
// These are picked from the json
var/list/forewords = strings(MEMORY_FILE, story_type + "_forewords")
var/list/somethings = strings(MEMORY_FILE, story_type + "_somethings")
var/list/styles
if(!(story_flags & STORY_FLAG_NO_STYLE))
styles = strings(MEMORY_FILE, "styles")
if("[story_type]_styles" in GLOB.string_cache[MEMORY_FILE])
styles += strings(MEMORY_FILE, story_type + "_styles")
// These are picked from the datum
var/list/wheres = (memory_flags & MEMORY_FLAG_NOLOCATION) ? null : get_locations()
var/list/story_starts = get_starts()
var/list/story_mood_sentences = (memory_flags & MEMORY_FLAG_NOMOOD) ? null : get_moods()
var/mob/living/crew_member
var/atom/something = pick(something_pool) //Pick a something for the potential something line
// This can be signalized or something in the future
var/datum/antagonist/obsessed/creeper = memorizer_mind.has_antag_datum(/datum/antagonist/obsessed)
if(creeper && creeper.trauma.obsession)
crew_member = creeper.trauma.obsession //ALWAYS ENGRAVE MY OBSESSION!
// Get a random crewmember
else
var/list/crew_members = list()
for(var/mob/living/carbon/human/potential_crew_member as anything in GLOB.human_list)
if(potential_crew_member.mind?.assigned_role.job_flags & JOB_CREW_MEMBER)
crew_members += potential_crew_member
crew_member = length(crew_members) ? pick(crew_members) : "an unknown crewmember"
//storybuilding
var/list/story_pieces = list()
//The forewords for this specific type of story (E.g. This engraving depicts)
story_pieces += pick(forewords)
//The story start for this specific action. (E.g. The Chef carving into The Clown)
story_pieces += pick(story_starts)
//The location it happend, which isn't always included, but commonly is. (E.g. in Space, while in the Bar)
if(length(wheres))
story_pieces += pick(wheres)
//Shows how the protagonist felt about it all (E.g. The Chef is looking sad as they tear into The Clown.)
if(length(story_mood_sentences))
story_pieces += pick(story_mood_sentences)
//A nonsensical addition, using the memorizer, protagonist or even random crew / things (E.g. in the meantime, the Clown is being arrested, clutching a skub.")
if(prob(75))
var/chosen_addition = pick(somethings)
chosen_addition = replacetext(chosen_addition, "%MEMORIZER", "[memorizer]")
chosen_addition = replacetext(chosen_addition, "%PROTAGONIST", protagonist_name)
chosen_addition = replacetext(chosen_addition, "%SOMETHING", initial(something.name))
chosen_addition = replacetext(chosen_addition, "%CREWMEMBER", build_story_character(crew_member))
chosen_addition = replacetext(chosen_addition, "%STORY_TYPE", story_type)
story_pieces += chosen_addition
//Explains any unique styling the art has. e.g. (The engraving has a cubist style.)
if(length(styles) && prob(75))
story_pieces += pick(styles)
var/parsed_story = ""
var/capitalize_next_line = FALSE
for(var/line in story_pieces)
if(capitalize_next_line)
line = capitalize(line)
capitalize_next_line = FALSE
if(line[length(line)] == ".")//End of sentence, next sentence needs to start with a capital.'
capitalize_next_line = TRUE
if(line != story_pieces[story_pieces.len]) //not the last line
parsed_story += "[line] "
//after replacement section for performance
if(story_flags & STORY_FLAG_DATED)
if(memory_flags & MEMORY_FLAG_NOSTATIONNAME)
parsed_story += "This took place in [time2text(world.realtime, "Month")] of [CURRENT_STATION_YEAR]."
else
parsed_story += "This took place in [time2text(world.realtime, "Month")] of [CURRENT_STATION_YEAR] on [station_name()]."
parsed_story = trim_right(parsed_story)
return parsed_story
/**
* When passed a "character", returns the name of the character formatted for stories
*
* If character is a string, it will just return it back.
*
* Otherwise, it will try to generate a title based on the mob's assigned role.
*
* If the character has no mind or no assigned role, it'll just return their name.
*/
/datum/memory/proc/build_story_character(character)
if(isnull(character))
return
if(istext(character))
return character
if(isliving(character))
var/mob/living/living_character = character
if(living_character.mind && !is_unassigned_job(living_character.mind.assigned_role))
character = living_character.mind
else if(ishuman(character))
// This can slip into memories involving monkey humans.
return "the unfamiliar person"
if(istype(character, /datum/mind))
var/datum/mind/character_mind = character
return "\the [LOWER_TEXT(initial(character_mind.assigned_role.title))]"
// Generic result - mobs get "the guy", objs / turfs get "a thing"
return ismob(character) ? "\the [character]" : "\a [character]"
/**
* Creates a "quick copy" of the memory for another mind,
* copying just basic memory information (name, major charactecrs) over.
*
* The copied memory cannot be used for stories or anything.
* They should generally only be used to give a new mind an idea of another mind's memories.
*/
/datum/memory/proc/quick_copy_memory(datum/mind/new_memorizer)
var/datum/memory/copy/new_copy = new(new_memorizer, protagonist_name, deuteragonist_name, antagonist_name, where, memory_flags)
new_copy.name = name
new_copy.story_value = story_value
return new_copy
// To only be used by quick copies of memories
/datum/memory/copy
memory_flags = MEMORY_NO_STORY
/datum/memory/copy/New(datum/mind/memorizer_mind, atom/protagonist, atom/deuteragonist, atom/antagonist, where, new_memory_flags)
src.where = where
src.memory_flags |= new_memory_flags
return ..()
/datum/memory/copy/generate_memory_name()
// We just copy the original memory's name anyways
return