Skip to content

Commit

Permalink
[MIRROR] Adds a subtle ghost poll [MDB IGNORE] (#24503) (#220)
Browse files Browse the repository at this point in the history
* Adds a subtle ghost poll (#79105)

## About The Pull Request
This makes a new ghost poll system which doesn't give TGUI popups -
instead, users are prompted to follow the POI and one of the orbiters is
chosen. The old system remains in place, so you can still prompt if you
want to.

This gives two things:
1. A deadchat notification:

![image](https://github.com/tgstation/tgstation/assets/42397676/073fcfff-b1ed-47c3-bae0-4abf9c599144)
2. A screen alert:

![image](https://github.com/tgstation/tgstation/assets/42397676/92a4e566-614a-43ca-8680-3cb4ff86ced9)

## Why It's Good For The Game
As stated in #76507, popups are pretty annoying. This is halfway between
a screen alert with no time limit and an event with more important
pings. This is better because:
1. Less popup fatigue
2. You can SEE how many you're competing with
4. DRY
## Changelog
:cl:
add: Adds a subtle ghost poll. This pings in dead chat and gives a
screen alert, but no TGUI popup. Orbit the point of interest to be
selected for the role.
refactor: A number of ghost spawns now feature this alert. Write an
issue report if anything breaks.
/:cl:

---------



* Adds a subtle ghost poll

---------

Co-authored-by: SkyratBot <[email protected]>
Co-authored-by: Jeremiah <[email protected]>
Co-authored-by: MrMelbert <51863163+MrMelbert@ users.noreply.github.com>
  • Loading branch information
4 people authored Oct 23, 2023
1 parent 3c0919e commit 5309234
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 92 deletions.
25 changes: 18 additions & 7 deletions code/datums/brain_damage/imaginary_friend.dm
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,26 @@
/datum/brain_trauma/special/imaginary_friend/proc/make_friend()
friend = new(get_turf(owner), owner)

/// Tries an orbit poll for the imaginary friend
/datum/brain_trauma/special/imaginary_friend/proc/get_ghost()
set waitfor = FALSE
var/list/mob/dead/observer/candidates = poll_candidates_for_mob("Do you want to play as [owner.real_name]'s imaginary friend?", ROLE_PAI, null, 7.5 SECONDS, friend, POLL_IGNORE_IMAGINARYFRIEND)
if(LAZYLEN(candidates))
var/mob/dead/observer/C = pick(candidates)
friend.key = C.key
friend_initialized = TRUE
else
var/datum/callback/to_call = CALLBACK(src, PROC_REF(add_friend))
owner.AddComponent(/datum/component/orbit_poll, \
ignore_key = POLL_IGNORE_IMAGINARYFRIEND, \
job_bans = ROLE_PAI, \
title = "[owner.real_name]'s imaginary friend", \
to_call = to_call, \
)

/// Yay more friends!
/datum/brain_trauma/special/imaginary_friend/proc/add_friend(mob/dead/observer/ghost)
if(isnull(ghost))
qdel(src)
return

friend.key = ghost.key
friend_initialized = TRUE
friend.log_message("became [key_name(owner)]'s split personality.", LOG_GAME)
message_admins("[ADMIN_LOOKUPFLW(friend)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.")

/mob/camera/imaginary_friend
name = "imaginary friend"
Expand Down
27 changes: 18 additions & 9 deletions code/datums/brain_damage/split_personality.dm
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,26 @@
var/datum/action/cooldown/spell/personality_commune/owner_spell = new(src)
owner_spell.Grant(owner_backseat)


/// Attempts to get a ghost to play the personality
/datum/brain_trauma/severe/split_personality/proc/get_ghost()
set waitfor = FALSE
var/list/mob/dead/observer/candidates = poll_candidates_for_mob("Do you want to play as [owner.real_name]'s [poll_role]?", ROLE_PAI, null, 7.5 SECONDS, stranger_backseat, POLL_IGNORE_SPLITPERSONALITY)
if(LAZYLEN(candidates))
var/mob/dead/observer/C = pick(candidates)
stranger_backseat.key = C.key
stranger_backseat.log_message("became [key_name(owner)]'s split personality.", LOG_GAME)
message_admins("[ADMIN_LOOKUPFLW(stranger_backseat)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.")
else
var/datum/callback/to_call = CALLBACK(src, PROC_REF(schism))
owner.AddComponent(/datum/component/orbit_poll, \
ignore_key = POLL_IGNORE_SPLITPERSONALITY, \
job_bans = ROLE_PAI, \
title = "[owner.real_name]'s [poll_role]", \
to_call = to_call, \
)

/// Ghost poll has concluded
/datum/brain_trauma/severe/split_personality/proc/schism(mob/dead/observer/ghost)
if(isnull(ghost))
qdel(src)
return

stranger_backseat.key = ghost.key
stranger_backseat.log_message("became [key_name(owner)]'s split personality.", LOG_GAME)
message_admins("[ADMIN_LOOKUPFLW(stranger_backseat)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.")


/datum/brain_trauma/severe/split_personality/on_life(seconds_per_tick, times_fired)
if(owner.stat == DEAD)
Expand Down
108 changes: 108 additions & 0 deletions code/datums/components/orbit_poll.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* A replacement for the standard poll_ghost_candidate.
* Use this to subtly ask players to join - it picks from orbiters.
* Please use named arguments for this.
*
* @params ignore_key - Required so it doesn't spam
* @params job_bans - You can insert a list or single items here.
* @params cb - Invokes this proc and appends the poll winner as the last argument, mob/dead/observer/ghost
* @params title - Optional. Useful if the role name does not match the parent.
*
* @usage
* ```
* var/datum/callback/cb = CALLBACK(src, PROC_REF(do_stuff), arg1, arg2)
* AddComponent(/datum/component/orbit_poll, \
* ignore_key = POLL_IGNORE_EXAMPLE, \
* job_bans = ROLE_EXAMPLE or list(ROLE_EXAMPLE, ROLE_EXAMPLE2), \
* title = "Use this if you want something other than the parent name", \
* to_call = cb, \
* )
*/
/datum/component/orbit_poll
/// Prevent players with this ban from being selected
var/list/job_bans = list()
/// Title of the role to announce after it's done
var/title
/// Proc to invoke whenever the poll is complete
var/datum/callback/to_call

/datum/component/orbit_poll/Initialize( \
ignore_key, \
list/job_bans, \
datum/callback/to_call, \
title, \
header = "Ghost Poll", \
custom_message, \
timeout = 20 SECONDS \
)
. = ..()
if (!isatom(parent))
return COMPONENT_INCOMPATIBLE

var/atom/owner = parent

src.job_bans |= job_bans
src.title = title || owner.name
src.to_call = to_call

var/message = custom_message || "[capitalize(src.title)] is looking for volunteers"

notify_ghosts("[message]. An orbiter will be chosen in [DisplayTimeText(timeout)].\n", \
action = NOTIFY_ORBIT, \
enter_link = "<a href='?src=[REF(src)];ignore=[ignore_key]'>(Ignore)</a>", \
flashwindow = FALSE, \
header = "Volunteers requested", \
ignore_key = ignore_key, \
source = parent \
)

addtimer(CALLBACK(src, PROC_REF(end_poll)), timeout, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE|TIMER_DELETE_ME)

/datum/component/orbit_poll/Topic(href, list/href_list)
if(!href_list["ignore"])
return

var/mob/user = usr

var/ignore_key = href_list["ignore"]
if(tgui_alert(user, "Ignore further [title] alerts?", "Ignore Alert", list("Yes", "No"), 20 SECONDS, TRUE) != "Yes")
return

GLOB.poll_ignore[ignore_key] |= user.ckey

/// Concludes the poll, picking one of the orbiters
/datum/component/orbit_poll/proc/end_poll()
if(QDELETED(parent))
return

var/list/candidates = list()
var/atom/owner = parent

var/datum/component/orbiter/orbiter_comp = owner.GetComponent(/datum/component/orbiter)
if(isnull(orbiter_comp))
phone_home()
return

for(var/mob/dead/observer/ghost as anything in orbiter_comp.orbiter_list)
if(QDELETED(ghost) || isnull(ghost.client))
continue
if(is_banned_from(ghost.ckey, job_bans))
continue

candidates += ghost

if(!length(candidates))
phone_home()
return

var/mob/dead/observer/chosen = pick(candidates)

if(chosen)
deadchat_broadcast("[key_name(chosen, include_name = FALSE)] was selected for the role ([title]).", "Ghost Poll: ", parent)

phone_home(chosen)

/// Make sure to call your parents my dude
/datum/component/orbit_poll/proc/phone_home(mob/dead/observer/chosen)
to_call.Invoke(chosen)
qdel(src)
25 changes: 17 additions & 8 deletions code/datums/components/spirit_holding.dm
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,30 @@
attempting_awakening = TRUE
to_chat(awakener, span_notice("You attempt to wake the spirit of [parent]..."))

var/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as the spirit of [awakener.real_name]'s blade?", ROLE_PAI, FALSE, 100, POLL_IGNORE_POSSESSED_BLADE)
if(!LAZYLEN(candidates))
to_chat(awakener, span_warning("[parent] is dormant. Maybe you can try again later."))
attempting_awakening = FALSE
return
var/datum/callback/to_call = CALLBACK(src, PROC_REF(affix_spirit), awakener)
parent.AddComponent(/datum/component/orbit_poll, \
ignore_key = POLL_IGNORE_POSSESSED_BLADE, \
job_bans = ROLE_PAI, \
to_call = to_call, \
title = "Spirit of [awakener.real_name]'s blade", \
)

//Immediately unregister to prevent making a new spirit
UnregisterSignal(parent, COMSIG_ITEM_ATTACK_SELF)

var/mob/dead/observer/chosen_spirit = pick(candidates)
/// On conclusion of the ghost poll
/datum/component/spirit_holding/proc/affix_spirit(mob/awakener, mob/dead/observer/ghost)
if(isnull(ghost))
to_chat(awakener, span_warning("[parent] is dormant. Maybe you can try again later."))
attempting_awakening = FALSE
return

if(QDELETED(parent)) //if the thing that we're conjuring a spirit in has been destroyed, don't create a spirit
to_chat(chosen_spirit, span_userdanger("The new vessel for your spirit has been destroyed! You remain an unbound ghost."))
to_chat(ghost, span_userdanger("The new vessel for your spirit has been destroyed! You remain an unbound ghost."))
return

bound_spirit = new(parent)
bound_spirit.ckey = chosen_spirit.ckey
bound_spirit.ckey = ghost.ckey
bound_spirit.fully_replace_character_name(null, "The spirit of [parent]")
bound_spirit.status_flags |= GODMODE
bound_spirit.copy_languages(awakener, LANGUAGE_MASTER) //Make sure the sword can understand and communicate with the awakener.
Expand Down
25 changes: 14 additions & 11 deletions code/game/objects/items/devices/aicard_evil.dm
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,29 @@
finding_candidate = FALSE
return TRUE

/// Sets up the ghost poll
/obj/item/aicard/syndie/loaded/proc/procure_ai(mob/user)
var/datum/antagonist/nukeop/op_datum = user.mind?.has_antag_datum(/datum/antagonist/nukeop,TRUE)
if(isnull(op_datum))
balloon_alert(user, "invalid access!")
return
var/list/nuke_candidates = poll_ghost_candidates(
question = "Do you want to play as a nuclear operative MODsuit AI?",
jobban_type = ROLE_OPERATIVE,
be_special_flag = ROLE_OPERATIVE_MIDROUND,
poll_time = 15 SECONDS,
ignore_category = POLL_IGNORE_SYNDICATE,

var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), user, op_datum)
AddComponent(/datum/component/orbit_poll, \
ignore_key = POLL_IGNORE_SYNDICATE, \
job_bans = ROLE_OPERATIVE, \
to_call = to_call, \
title = "Nuclear Operative Modsuit AI" \
)
if(QDELETED(src))
return
if(!LAZYLEN(nuke_candidates))

/// Poll has concluded with a ghost, create the AI
/obj/item/aicard/syndie/loaded/proc/on_poll_concluded(mob/user, datum/antagonist/nukeop/op_datum, mob/dead/observer/ghost)
if(isnull(ghost))
to_chat(user, span_warning("Unable to connect to S.E.L.F. dispatch. Please wait and try again later or use the intelliCard on your uplink to get your points refunded."))
return

// pick ghost, create AI and transfer
var/mob/dead/observer/ghos = pick(nuke_candidates)
var/mob/living/silicon/ai/weak_syndie/new_ai = new /mob/living/silicon/ai/weak_syndie(get_turf(src), new /datum/ai_laws/syndicate_override, ghos)
var/mob/living/silicon/ai/weak_syndie/new_ai = new /mob/living/silicon/ai/weak_syndie(get_turf(src), new /datum/ai_laws/syndicate_override, ghost)
// create and apply syndie datum
var/datum/antagonist/nukeop/nuke_datum = new()
nuke_datum.send_to_spawnpoint = FALSE
Expand Down
21 changes: 14 additions & 7 deletions code/modules/antagonists/blob/powers.dm
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,22 @@
to_chat(src, span_notice("You attempt to produce a blobbernaut."))
pick_blobbernaut_candidate(factory)

/** Polls ghosts to get a blobbernaut candidate. */
/// Polls ghosts to get a blobbernaut candidate.
/mob/camera/blob/proc/pick_blobbernaut_candidate(obj/structure/blob/special/factory/factory)
if(!factory)
if(isnull(factory))
return

var/list/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as a [blobstrain.name] blobbernaut?", ROLE_BLOB, ROLE_BLOB, 50)

if(!length(candidates))
var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), factory)
factory.AddComponent(/datum/component/orbit_poll, \
ignore_key = POLL_IGNORE_BLOB, \
job_bans = ROLE_BLOB, \
to_call = to_call, \
title = "Blobbernaut", \
)

/// Called when the ghost poll concludes
/mob/camera/blob/proc/on_poll_concluded(obj/structure/blob/special/factory/factory, mob/dead/observer/ghost)
if(isnull(ghost))
to_chat(src, span_warning("You could not conjure a sentience for your blobbernaut. Your points have been refunded. Try again later."))
add_points(BLOBMOB_BLOBBERNAUT_RESOURCE_COST)
factory.assign_blobbernaut(null)
Expand All @@ -205,8 +213,7 @@
var/mob/living/basic/blob_minion/blobbernaut/minion/blobber = new(get_turf(factory))
assume_direct_control(blobber)
factory.assign_blobbernaut(blobber)
var/mob/dead/observer/player = pick(candidates)
blobber.assign_key(player.key, blobstrain)
blobber.assign_key(ghost.key, blobstrain)
RegisterSignal(blobber, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(on_blobbernaut_attacked))

/// When one of our boys attacked something, we sometimes want to perform extra effects
Expand Down
42 changes: 18 additions & 24 deletions code/modules/antagonists/wizard/equipment/soulstone.dm
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,15 @@
return TRUE

to_chat(user, "[span_userdanger("Capture failed!")]: The soul has already fled its mortal frame. You attempt to bring it back...")
INVOKE_ASYNC(src, PROC_REF(get_ghost_to_replace_shade), victim, user)

var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), user, victim)
AddComponent(/datum/component/orbit_poll, \
ignore_key = POLL_IGNORE_SHADE, \
job_bans = ROLE_CULTIST, \
to_call = to_call, \
title = "A shade" \
)

return TRUE //it'll probably get someone ;)

///captures a shade that was previously released from a soulstone.
Expand Down Expand Up @@ -435,33 +443,19 @@
shade_datum = shade.mind.add_antag_datum(/datum/antagonist/shade_minion)
shade_datum.update_master(user.real_name)

/**
* Gets a ghost from dead chat to replace a missing player when a shade is created.
*
* Gets ran if a soulstone is used on a body that has no client to take over the shade.
*
* victim - the body that's being shaded
* user - the mob shading the body
*
* Returns FALSE if no ghosts are available or the replacement fails.
* Returns TRUE otherwise.
*/
/obj/item/soulstone/proc/get_ghost_to_replace_shade(mob/living/carbon/victim, mob/user)
var/mob/dead/observer/chosen_ghost
var/list/consenting_candidates = poll_ghost_candidates("Would you like to play as a Shade?", "Cultist", ROLE_CULTIST, 5 SECONDS, POLL_IGNORE_SHADE)
if(length(consenting_candidates))
chosen_ghost = pick(consenting_candidates)

if(!victim || user.incapacitated() || !user.is_holding(src) || !user.CanReach(victim, src))
/// Called when a ghost is chosen to become a shade.
/obj/item/soulstone/proc/on_poll_concluded(mob/living/master, mob/living/victim, mob/dead/observer/ghost)
if(isnull(victim) || master.incapacitated() || !master.is_holding(src) || !master.CanReach(victim, src))
return FALSE
if(!chosen_ghost || !chosen_ghost.client)
to_chat(user, span_danger("There were no spirits willing to become a shade."))
if(isnull(ghost?.client))
to_chat(master, span_danger("There were no spirits willing to become a shade."))
return FALSE
if(contents.len) //If they used the soulstone on someone else in the meantime
if(length(contents)) //If they used the soulstone on someone else in the meantime
return FALSE
to_chat(user, "[span_info("<b>Capture successful!</b>:")] A spirit has entered [src], \
to_chat(master, "[span_info("<b>Capture successful!</b>:")] A spirit has entered [src], \
taking upon the identity of [victim].")
init_shade(victim, user, shade_controller = chosen_ghost)
init_shade(victim, master, shade_controller = ghost)

return TRUE

/proc/make_new_construct_from_class(construct_class, theme, mob/target, mob/creator, cultoverride, loc_override)
Expand Down
Loading

0 comments on commit 5309234

Please sign in to comment.