[GLOB.configuration.general.server_name] is now starting up. The map is [SSmapping.map_datum.fluff_name] ([SSmapping.map_datum.technical_name]). You can connect with the Switch Server verb."
+ message_all_peers(startup_msg)
+ return ..()
+
+/datum/controller/subsystem/instancing/fire(resumed)
+ update_heartbeat()
+ update_playercache()
+
+/**
+ * Playercache updater
+ *
+ * Updates the player cache in the DB. Different from heartbeat so we can force invoke it on player operations
+ */
+/datum/controller/subsystem/instancing/proc/update_playercache(optional_ckey)
+ // You may be wondering, why the fuck is an "optional ckey" variable here
+ // Well, this is invoked in client/New(), and needs to read from GLOB.clients
+ // However, this proc sleeps, and if you sleep during client/New() once the client is in GLOB.clients, stuff breaks bad
+ // (See my comment rambling in client/New())
+ // By passing the ckey through, we can sleep in this proc and still get the data
+ if(!SSdbcore.IsConnected())
+ return
+ // First iterate clients to get ckeys
+ var/list/ckeys = list()
+ for(var/client/C in GLOB.clients) // No code review. I am not doing the `as anything` bullshit, because we *do* need the type checks here to avoid null clients which do happen sometimes
+ ckeys += C.ckey
+ // Add our optional
+ if(optional_ckey)
+ ckeys += optional_ckey
+ // Note: We dont have to sort the list here. The only time this is read for is a search,
+ // and order doesnt matter for that.
+ var/ckey_json = json_encode(ckeys)
+
+ // Yes I care about performance savings this much here to mass execute this shit
+ var/list/datum/db_query/queries = list()
+ queries += SSdbcore.NewQuery("UPDATE instance_data_cache SET key_value=:json WHERE key_name='playerlist' AND server_id=:sid", list(
+ "json" = ckey_json,
+ "sid" = GLOB.configuration.system.instance_id
+ ))
+ queries += SSdbcore.NewQuery("UPDATE instance_data_cache SET key_value=:count WHERE key_name='playercount' AND server_id=:sid", list(
+ "count" = length(ckeys),
+ "sid" = GLOB.configuration.system.instance_id
+ ))
+
+ SSdbcore.MassExecute(queries, TRUE, TRUE, FALSE, FALSE)
+
+/**
+ * Heartbeat updater
+ *
+ * Updates the heartbeat in the DB. Used so other servers can see when this one was alive
+ */
+/datum/controller/subsystem/instancing/proc/update_heartbeat()
+ // this could probably just go in fire() but clean code and profiler ease who cares
+ var/datum/db_query/dbq = SSdbcore.NewQuery("UPDATE instance_data_cache SET key_value=NOW() WHERE key_name='heartbeat' AND server_id=:sid", list(
+ "sid" = GLOB.configuration.system.instance_id
+ ))
+ dbq.warn_execute()
+ qdel(dbq)
+
+/**
+ * Seed data
+ *
+ * Seeds all our data into the DB for other servers to discover from.
+ * This is called during world/New() instead of on initialize so it can be done *instantly*
+ */
+/datum/controller/subsystem/instancing/proc/seed_data()
+ // We need to seed a lot of keys, so lets just use a key-value-pair-map to do this easily
+ var/list/kvp_map = list()
+ kvp_map["server_name"] = GLOB.configuration.general.server_name // Name of the server
+ kvp_map["server_port"] = world.port // Server port (used for redirection and topics)
+ kvp_map["topic_key"] = GLOB.configuration.system.topic_key // Server topic key (used for topics)
+ kvp_map["internal_ip"] = GLOB.configuration.system.internal_ip // Server internal IP (used for topics)
+ kvp_map["playercount"] = length(GLOB.clients) // Server client count (used for status info)
+ kvp_map["playerlist"] = json_encode(list()) // Server client list. Used for dupe login checks. This gets filled in later
+ kvp_map["heartbeat"] = SQLtime() // SQL timestamp for heartbeat purposes. Any server without a heartbeat in the last 60 seconds can be considered dead
+ // Also note for above. You may say "But AA you dont need to JSON encode it, just use "\[]"."
+ // Well to that I say, no. This is meant to be JSON regardless, and it should represent that. This proc is ran once during world/New()
+ // An extra nanosecond of load will make zero difference.
+
+ for(var/key in kvp_map)
+ var/datum/db_query/dbq = SSdbcore.NewQuery("INSERT INTO instance_data_cache (server_id, key_name, key_value) VALUES (:sid, :kn, :kv) ON DUPLICATE KEY UPDATE key_value=:kv2", // Is this necessary? Who knows!
+ list(
+ "sid" = GLOB.configuration.system.instance_id,
+ "kn" = key,
+ "kv" = "[kvp_map[key]]", // String encoding IS necessary since these tables use strings, not ints
+ "kv2" = "[kvp_map[key]]", // Dont know if I need the second but better to be safe
+ )
+ )
+ dbq.warn_execute(FALSE) // Do NOT async execute here because world/New() shouldnt sleep. EVER. You get issues if you do.
+ qdel(dbq)
+
+
+/**
+ * Message all peers
+ *
+ * Wrapper for [topic_all_peers] to format the input into a message topic. Will send a server-wide announcement to the other servers
+ *
+ * Arguments:
+ * * message - Message to send to the other servers
+ */
+/datum/controller/subsystem/instancing/proc/message_all_peers(message)
+ if(!SSdbcore.IsConnected())
+ return
+ var/topic_string = "instance_announce&msg=[url_encode(message)]"
+ topic_all_peers(topic_string)
+
+/**
+ * Sends a topic to all peers
+ *
+ * Sends a raw topic to the other servers. WILL APPEND &key=[commskey] ON THE END. PLEASE ACCOUNT FOR THIS.
+ *
+ * Arguments:
+ * * raw_topic - The raw topic to send to the other servers
+ */
+/datum/controller/subsystem/instancing/proc/topic_all_peers(raw_topic)
+ // Someone here is going to say "AA you shouldnt put load on the DB server you can do sorting in BYOND"
+ // Well let me put it this way. The DB server is an entirely different machine to BYOND, with this entire dataset being stored in its RAM, not even on disk
+ // By making the DB server do the work, we can offload from BYOND, which is already strained
+ var/datum/db_query/dbq1 = SSdbcore.NewQuery({"
+ SELECT server_id, key_name, key_value FROM instance_data_cache WHERE server_id IN
+ (SELECT server_id FROM instance_data_cache WHERE server_id !=:sid AND
+ key_name='heartbeat' AND last_updated BETWEEN NOW() - INTERVAL 60 SECOND AND NOW())
+ AND key_name IN ("topic_key", "internal_ip", "server_port")"}, list(
+ "sid" = GLOB.configuration.system.instance_id
+ ))
+ if(!dbq1.warn_execute())
+ qdel(dbq1)
+ return
+
+ var/servers_outer = list()
+ while(dbq1.NextRow())
+ if(!servers_outer[dbq1.item[1]])
+ servers_outer[dbq1.item[1]] = list()
+
+ servers_outer[dbq1.item[1]][dbq1.item[2]] = dbq1.item[3] // This should assoc load our data
+
+ qdel(dbq1)
+
+ for(var/server in servers_outer)
+ var/server_data = servers_outer[server]
+ world.Export("byond://[server_data["internal_ip"]]:[server_data["server_port"]]?[raw_topic]&key=[server_data["topic_key"]]")
+
+
+/**
+ * Player checker
+ *
+ * Check all connected peers to see if a player exists in the player cache
+ * This is used to make sure players dont log into 2 servers at once
+ * Arguments:
+ * ckey - The ckey to check if they are logged into another server
+ */
+/datum/controller/subsystem/instancing/proc/check_player(ckey)
+ // Please see above rant on L127
+ var/datum/db_query/dbq1 = SSdbcore.NewQuery({"
+ SELECT server_id, key_value FROM instance_data_cache WHERE server_id IN
+ (SELECT server_id FROM instance_data_cache WHERE server_id != :sid AND
+ key_name='heartbeat' AND last_updated BETWEEN NOW() - INTERVAL 60 SECOND AND NOW())
+ AND key_name IN ("playerlist")"}, list(
+ "sid" = GLOB.configuration.system.instance_id
+ ))
+ if(!dbq1.warn_execute())
+ qdel(dbq1)
+ return
+
+ while(dbq1.NextRow())
+ var/list/other_server_cache = json_decode(dbq1.item[2])
+ if(ckey in other_server_cache)
+ var/target_server = dbq1.item[1] // Yes. This var is necessary.
+ qdel(dbq1)
+ return target_server
+
+ qdel(dbq1)
+ return null // If we are here, it means we didnt find our player on another server
diff --git a/code/controllers/subsystem/metrics.dm b/code/controllers/subsystem/metrics.dm
index 95055f99216..181f24cfb08 100644
--- a/code/controllers/subsystem/metrics.dm
+++ b/code/controllers/subsystem/metrics.dm
@@ -28,6 +28,7 @@ SUBSYSTEM_DEF(metrics)
out["elapsed_real"] = (REALTIMEOFDAY - world_init_time)
out["client_count"] = length(GLOB.clients)
out["round_id"] = text2num(GLOB.round_id) // This is so we can filter the metrics by a single round ID
+ out["server_id"] = GLOB.configuration.system.instance_id // And this is so we can filter by instance ID
// Funnel in all SS metrics
var/list/ss_data = list()
diff --git a/code/controllers/subsystem/statistics.dm b/code/controllers/subsystem/statistics.dm
index 63c20742904..01ebaf215f7 100644
--- a/code/controllers/subsystem/statistics.dm
+++ b/code/controllers/subsystem/statistics.dm
@@ -18,10 +18,11 @@ SUBSYSTEM_DEF(statistics)
return
else
var/datum/db_query/statquery = SSdbcore.NewQuery(
- "INSERT INTO legacy_population (playercount, admincount, time) VALUES (:playercount, :admincount, NOW())",
+ "INSERT INTO legacy_population (playercount, admincount, time, server_id) VALUES (:playercount, :admincount, NOW(), :server_id)",
list(
"playercount" = length(GLOB.clients),
- "admincount" = length(GLOB.admins)
+ "admincount" = length(GLOB.admins),
+ "server_id" = GLOB.configuration.system.instance_id
)
)
statquery.warn_execute()
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 28f69ba1795..5bb1d0eb69f 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -408,12 +408,65 @@ SUBSYSTEM_DEF(ticker)
if(player.mind.assigned_role != player.mind.special_role)
SSjobs.AssignRank(player, player.mind.assigned_role, FALSE)
SSjobs.EquipRank(player, player.mind.assigned_role, FALSE)
- EquipCustomItems(player)
+ equip_cuis(player)
+
if(captainless)
for(var/mob/M in GLOB.player_list)
if(!isnewplayer(M))
to_chat(M, "Captainship not forced on anyone.")
+/datum/controller/subsystem/ticker/proc/equip_cuis(mob/living/carbon/human/H)
+ if(!H.client)
+ return // If they are spawning without a client (somehow), they *cant* have a CUI list
+ for(var/datum/custom_user_item/cui in H.client.cui_entries)
+ // Skip items with invalid character names
+ if((cui.characer_name != H.real_name) && !cui.all_characters_allowed)
+ continue
+
+ var/ok = FALSE
+
+ if(!cui.all_jobs_allowed)
+ var/alt_blocked = FALSE
+ if(H.mind.role_alt_title)
+ if(!(H.mind.role_alt_title in cui.allowed_jobs))
+ alt_blocked = TRUE
+ if(!(H.mind.assigned_role in cui.allowed_jobs) || alt_blocked)
+ continue
+
+ var/obj/item/I = new cui.object_typepath()
+ var/name_override = cui.item_name_override
+ var/desc_override = cui.item_desc_override
+
+ if(name_override)
+ I.name = name_override
+ if(desc_override)
+ I.desc = desc_override
+
+ if(istype(H.back, /obj/item/storage)) // Try to place it in something on the mob's back
+ var/obj/item/storage/S = H.back
+ if(length(S.contents) < S.storage_slots)
+ I.forceMove(H.back)
+ ok = TRUE
+ to_chat(H, "Your [I.name] has been added to your [H.back.name].")
+
+ if(!ok)
+ for(var/obj/item/storage/S in H.contents) // Try to place it in any item that can store stuff, on the mob.
+ if(length(S.contents) < S.storage_slots)
+ I.forceMove(S)
+ ok = TRUE
+ to_chat(H, "Your [I.name] has been added to your [S.name].")
+ break
+
+ if(!ok) // Finally, since everything else failed, place it on the ground
+ var/turf/T = get_turf(H)
+ if(T)
+ I.forceMove(T)
+ to_chat(H, "Your [I.name] is on the [T.name] below you.")
+ else
+ to_chat(H, "Your [I.name] couldnt spawn anywhere on you or even on the floor below you. Please file a bug report.")
+ qdel(I)
+
+
/datum/controller/subsystem/ticker/proc/send_tip_of_the_round()
var/m
if(selected_tip)
diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm
index 0185aa5931b..a93d7e443c8 100644
--- a/code/controllers/subsystem/vote.dm
+++ b/code/controllers/subsystem/vote.dm
@@ -266,8 +266,12 @@ SUBSYSTEM_DEF(vote)
Click here or type vote to place your vote.
You have [GLOB.configuration.vote.vote_time / 10] seconds to vote."})
switch(vote_type)
- if("crew transfer", "gamemode", "custom", "map")
+ if("crew transfer", "gamemode", "custom")
SEND_SOUND(world, sound('sound/ambience/alarm4.ogg'))
+ if("map")
+ SEND_SOUND(world, sound('sound/ambience/alarm4.ogg'))
+ for(var/mob/M in GLOB.player_list)
+ M.throw_alert("Map Vote", /obj/screen/alert/notify_mapvote, timeout_override = GLOB.configuration.vote.vote_time)
if(mode == "gamemode" && SSticker.ticker_going)
SSticker.ticker_going = FALSE
to_chat(world, "Round start has been delayed.")
diff --git a/code/datums/action.dm b/code/datums/action.dm
index be7f82494f9..4ba27fb037d 100644
--- a/code/datums/action.dm
+++ b/code/datums/action.dm
@@ -423,6 +423,13 @@
/datum/action/item_action/remove_badge
name = "Remove Holobadge"
+
+// Clown Acrobat Shoes
+/datum/action/item_action/slipping
+ name = "Tactical Slip"
+ desc = "Activates the clown shoes' ankle-stimulating module, allowing the user to do a short slip forward going under anyone."
+ button_icon_state = "clown"
+
// Jump boots
/datum/action/item_action/bhop
name = "Activate Jump Boots"
diff --git a/code/datums/custom_user_item.dm b/code/datums/custom_user_item.dm
new file mode 100644
index 00000000000..9680a990488
--- /dev/null
+++ b/code/datums/custom_user_item.dm
@@ -0,0 +1,66 @@
+/**
+ * # Custom User Item
+ *
+ * Holder for CUIs
+ *
+ * This datum is a holder that is essentially a "model" of the `customuseritems`
+ * database table, and is used for giving people their CUIs on spawn.
+ * It is instanced as part of the client data loading framework on the client.
+ *
+ */
+/datum/custom_user_item
+ /// Can this be used on all characters?
+ var/all_characters_allowed = FALSE
+ /// Name of the character that can have this item.
+ var/characer_name
+ /// Are all jobs allowed?
+ var/all_jobs_allowed = FALSE
+ /// List of allowed jobs
+ var/list/allowed_jobs = list()
+ /// Custom item typepath
+ var/object_typepath
+ /// Custom item name override
+ var/item_name_override
+ /// Custom item description override
+ var/item_desc_override
+ /// Raw job mask
+ var/raw_job_mask
+
+
+/**
+ * CUI Info Parser
+ *
+ * Parses all the raw info into usable stuff, and also does validity checks
+ * Returns TRUE if its a valid item, and FALSE if not
+ *
+ * Arguments:
+ * * owning_ckey - Player who owns this item. Used for logging purposes.
+ */
+/datum/custom_user_item/proc/parse_info(owning_ckey)
+ . = FALSE // Setting this here means it will return false even if it runtimes
+
+ // Sort path
+ if(!object_typepath || !ispath(object_typepath))
+ stack_trace("Incorrect database entry found in table 'customuseritems' path value is [object_typepath ? object_typepath : "(NULL)"], which doesnt exist. Ask the host to look at CUI entries for [owning_ckey]")
+ return
+
+ // Sort job mask
+ if(raw_job_mask == "*")
+ all_jobs_allowed = TRUE
+ else
+ var/list/local_allowed_jobs = splittext(raw_job_mask, ",")
+ for(var/i in 1 to length(local_allowed_jobs))
+ if(istext(local_allowed_jobs[i]))
+ local_allowed_jobs[i] = trim(local_allowed_jobs[i])
+
+ allowed_jobs = local_allowed_jobs
+
+ // Sort character name
+ if(characer_name == "*")
+ all_characters_allowed = TRUE
+
+ return TRUE
+
+
+/datum/custom_user_item/vv_edit_var(var_name, var_value)
+ return FALSE // fuck off
diff --git a/code/datums/diseases/advance/symptoms/damage_converter.dm b/code/datums/diseases/advance/symptoms/damage_converter.dm
index b7ea81f04af..922a0ee2819 100644
--- a/code/datums/diseases/advance/symptoms/damage_converter.dm
+++ b/code/datums/diseases/advance/symptoms/damage_converter.dm
@@ -49,6 +49,7 @@ Bonus
healed += min(E.brute_dam, get_damage) + min(E.burn_dam, get_damage)
E.heal_damage(get_damage, get_damage, 0, 0)
M.adjustToxLoss(healed)
+ M.UpdateAppearance()
else
@@ -56,6 +57,7 @@ Bonus
M.adjustFireLoss(-get_damage)
M.adjustBruteLoss(-get_damage)
M.adjustToxLoss(get_damage)
+ M.UpdateAppearance()
else
return
diff --git a/code/datums/http.dm b/code/datums/http.dm
index e7ba1620854..6679b859c65 100644
--- a/code/datums/http.dm
+++ b/code/datums/http.dm
@@ -19,6 +19,8 @@
var/headers
/// URL that the request is being sent to
var/url
+ /// If present, response body will be saved to this file.
+ var/output_file
/// The raw response, which will be decoeded into a [/datum/http_response]
var/_raw_response
/// Callback for executing after async requests. Will be called with an argument of [/datum/http_response] as first argument
@@ -42,7 +44,7 @@ THE METHODS IN THIS FILE ARE TO BE USED BY THE SUBSYSTEM AS A MANGEMENT HUB
* * _body - The body of the request, if applicable
* * _headers - Associative list of HTTP headers to send, if applicab;e
*/
-/datum/http_request/proc/prepare(_method, _url, _body = "", list/_headers)
+/datum/http_request/proc/prepare(_method, _url, _body = "", list/_headers, _output_file)
if(!length(_headers))
headers = ""
else
@@ -51,6 +53,7 @@ THE METHODS IN THIS FILE ARE TO BE USED BY THE SUBSYSTEM AS A MANGEMENT HUB
method = _method
url = _url
body = _body
+ output_file = _output_file
/**
* Blocking executor
@@ -60,7 +63,7 @@ THE METHODS IN THIS FILE ARE TO BE USED BY THE SUBSYSTEM AS A MANGEMENT HUB
*/
/datum/http_request/proc/execute_blocking()
CRASH("Attempted to execute a blocking HTTP request")
- // _raw_response = rustg_http_request_blocking(method, url, body, headers)
+ // _raw_response = rustg_http_request_blocking(method, url, body, headers, build_options())
/**
* Async execution starter
@@ -73,7 +76,7 @@ THE METHODS IN THIS FILE ARE TO BE USED BY THE SUBSYSTEM AS A MANGEMENT HUB
if(in_progress)
CRASH("Attempted to re-use a request object.")
- id = rustg_http_request_async(method, url, body, headers)
+ id = rustg_http_request_async(method, url, body, headers, build_options())
if(isnull(text2num(id)))
_raw_response = "Proc error: [id]"
@@ -81,6 +84,16 @@ THE METHODS IN THIS FILE ARE TO BE USED BY THE SUBSYSTEM AS A MANGEMENT HUB
else
in_progress = TRUE
+/**
+ * Options builder
+ *
+ * Builds options for if we want to download files with SShttp
+ */
+/datum/http_request/proc/build_options()
+ if(output_file)
+ return json_encode(list("output_filename" = output_file, "body_filename" = null))
+ return null
+
/**
* Async completion checker
*
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 1fbb7a34f59..1d802c2966c 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -86,7 +86,7 @@
return ..()
/datum/mind/proc/get_display_key()
- var/clientKey = current?.client.get_display_key()
+ var/clientKey = current?.client?.get_display_key()
return clientKey ? clientKey : key
/datum/mind/proc/transfer_to(mob/living/new_character)
@@ -107,6 +107,8 @@
for(var/a in antag_datums) //Makes sure all antag datums effects are applied in the new body
var/datum/antagonist/A = a
A.on_body_transfer(old_current, current)
+ if(vampire)
+ vampire.update_owner(new_character)
transfer_antag_huds(hud_to_transfer) //inherit the antag HUD
transfer_actions(new_character)
if(martial_art)
@@ -1544,14 +1546,15 @@
SSticker.mode.equip_syndicate(current)
-/datum/mind/proc/make_Vampire()
+/datum/mind/proc/make_vampire(ancient_vampire = FALSE)
if(!(src in SSticker.mode.vampires))
SSticker.mode.vampires += src
SSticker.mode.grant_vampire_powers(current)
special_role = SPECIAL_ROLE_VAMPIRE
- SSticker.mode.forge_vampire_objectives(src)
SSticker.mode.greet_vampire(src)
SSticker.mode.update_vampire_icons_added(src)
+ if(!ancient_vampire)
+ SSticker.mode.forge_vampire_objectives(src)
/datum/mind/proc/make_Changeling()
if(!(src in SSticker.mode.changelings))
diff --git a/code/datums/outfits/outfit_admin.dm b/code/datums/outfits/outfit_admin.dm
index 18175766871..b76d92dc527 100644
--- a/code/datums/outfits/outfit_admin.dm
+++ b/code/datums/outfits/outfit_admin.dm
@@ -1103,21 +1103,15 @@
if(H.mind)
if(!H.mind.vampire)
- H.make_vampire()
- if(H.mind.vampire)
- H.mind.vampire.bloodusable = 9999
- H.mind.vampire.bloodtotal = 9999
- H.mind.vampire.check_vampire_upgrade(0)
- H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shapeshift/bats)
- to_chat(H, "You have gained the ability to shapeshift into bat form. This is a weak form with no abilities, only useful for stealth.")
- H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shapeshift/hellhound)
- to_chat(H, "You have gained the ability to shapeshift into lesser hellhound form. This is a combat form with different abilities, tough but not invincible. It can regenerate itself over time by resting.")
- H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/raise_vampires)
- to_chat(H, "You have gained the ability to Raise Vampires. This extremely powerful AOE ability affects all humans near you. Vampires/thralls are healed. Corpses are raised as vampires. Others are stunned, then brain damaged, then killed.")
- H.dna.SetSEState(GLOB.jumpblock, 1)
- singlemutcheck(H, GLOB.jumpblock, MUTCHK_FORCED)
- H.update_mutations()
- H.gene_stability = 100
+ H.mind.make_vampire(TRUE)
+ H.mind.vampire.bloodusable = 9999
+ H.mind.vampire.bloodtotal = 9999
+ H.mind.offstation_role = TRUE
+ H.mind.vampire.add_subclass(SUBCLASS_ANCIENT, FALSE)
+ H.dna.SetSEState(GLOB.jumpblock, TRUE)
+ singlemutcheck(H, GLOB.jumpblock, MUTCHK_FORCED)
+ H.update_mutations()
+ H.gene_stability = 100
/datum/outfit/admin/wizard
name = "Blue Wizard"
diff --git a/code/datums/spell.dm b/code/datums/spell.dm
index 23b2241da03..00c807687cd 100644
--- a/code/datums/spell.dm
+++ b/code/datums/spell.dm
@@ -131,6 +131,11 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
var/special_availability_check = 0//Whether the spell needs to bypass the action button's IsAvailable()
var/sound = null //The sound the spell makes when it is cast
+ /// If the ability is for vampires
+ var/vampire_ability = FALSE
+ var/required_blood = 0
+ var/gain_desc = null
+ var/deduct_blood_on_cast = TRUE
/* Checks if the user can cast the spell
* @param charge_check If the proc should do the cooldown check
@@ -190,6 +195,8 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
charge_counter = charge_max
else
start_recharge()
+ if(!gain_desc)
+ gain_desc = "You can now use [src]."
/obj/effect/proc_holder/spell/Destroy()
QDEL_NULL(action)
@@ -238,6 +245,9 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
action.UpdateButtonIcon()
/obj/effect/proc_holder/spell/proc/before_cast(list/targets)
+ if(vampire_ability)
+ if(!before_cast_vampire(targets))
+ return
if(overlay)
for(var/atom/target in targets)
var/location
@@ -594,25 +604,55 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
var/mob/living/carbon/human/H = user
var/clothcheck = locate(/obj/effect/proc_holder/spell/noclothes) in user.mob_spell_list
var/clothcheck2 = user.mind && (locate(/obj/effect/proc_holder/spell/noclothes) in user.mind.spell_list)
- if(clothes_req && !clothcheck && !clothcheck2) //clothes check
+ if(clothes_req && !clothcheck && !clothcheck2 && !vampire_ability) //clothes check
var/obj/item/clothing/robe = H.wear_suit
var/obj/item/clothing/hat = H.head
var/obj/item/clothing/shoes = H.shoes
if(!robe || !hat || !shoes)
if(show_message)
to_chat(user, "Your outfit isn't complete, you should put on your robe and wizard hat, as well as sandals.")
- return 0
+ return FALSE
if(!robe.magical || !hat.magical || !shoes.magical)
if(show_message)
to_chat(user, "Your outfit isn't magical enough, you should put on your robe and wizard hat, as well as your sandals.")
- return 0
+ return FALSE
else
- if(clothes_req || human_req)
+ if(clothes_req || human_req || vampire_ability)
if(show_message)
to_chat(user, "This spell can only be cast by humans!")
- return 0
+ return FALSE
if(nonabstract_req && (isbrain(user) || ispAI(user)))
if(show_message)
to_chat(user, "This spell can only be cast by physical beings!")
- return 0
- return 1
+ return FALSE
+
+ if(vampire_ability)
+
+ var/datum/vampire/vampire = user.mind.vampire
+
+ if(!vampire)
+ return FALSE
+
+ var/fullpower = vampire.get_ability(/datum/vampire_passive/full)
+
+ if(user.stat >= DEAD)
+ if(show_message)
+ to_chat(user, "Not while you're dead!")
+ return FALSE
+
+ if(vampire.nullified >= VAMPIRE_COMPLETE_NULLIFICATION && !fullpower) // above 100 nullification vampire powers are useless
+ if(show_message)
+ to_chat(user, "Something is blocking your powers!")
+ return FALSE
+ if(vampire.bloodusable < required_blood)
+ if(show_message)
+ to_chat(user, "You require at least [required_blood] units of usable blood to do that!")
+ return FALSE
+ //chapel check
+ if(istype(get_area(user), /area/chapel) && !fullpower)
+ if(show_message)
+ to_chat(user, "Your powers are useless on this holy ground.")
+ return FALSE
+
+ return TRUE
+
diff --git a/code/datums/spells/ethereal_jaunt.dm b/code/datums/spells/ethereal_jaunt.dm
index 4c7c6b76b36..12b258fca64 100644
--- a/code/datums/spells/ethereal_jaunt.dm
+++ b/code/datums/spells/ethereal_jaunt.dm
@@ -12,16 +12,18 @@
include_user = 1
nonabstract_req = 1
centcom_cancast = 0 //Prevent people from getting to centcom
-
+ var/sound1 = 'sound/magic/ethereal_enter.ogg'
var/jaunt_duration = 50 //in deciseconds
var/jaunt_in_time = 5
var/jaunt_in_type = /obj/effect/temp_visual/wizard
var/jaunt_out_type = /obj/effect/temp_visual/wizard/out
+ var/jaunt_type_path = /obj/effect/dummy/spell_jaunt
+ var/jaunt_water_effect = TRUE
action_icon_state = "jaunt"
/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/cast(list/targets, mob/user = usr) //magnets, so mostly hardcoded
- playsound(get_turf(user), 'sound/magic/ethereal_enter.ogg', 50, 1, -1)
+ playsound(get_turf(user), sound1, 50, 1, -1)
for(var/mob/living/target in targets)
if(!target.can_safely_leave_loc()) // No more brainmobs hopping out of their brains
to_chat(target, "You are somehow too bound to your current location to abandon it.")
@@ -31,13 +33,14 @@
/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/do_jaunt(mob/living/target)
target.notransform = 1
var/turf/mobloc = get_turf(target)
- var/obj/effect/dummy/spell_jaunt/holder = new /obj/effect/dummy/spell_jaunt(mobloc)
+ var/obj/effect/dummy/spell_jaunt/holder = new jaunt_type_path(mobloc)
new jaunt_out_type(mobloc, target.dir)
target.ExtinguishMob()
target.forceMove(holder)
target.reset_perspective(holder)
target.notransform = 0 //mob is safely inside holder now, no need for protection.
- jaunt_steam(mobloc)
+ if(jaunt_water_effect)
+ jaunt_steam(mobloc)
sleep(jaunt_duration)
@@ -45,22 +48,28 @@
qdel(holder)
return
mobloc = get_turf(target.loc)
- jaunt_steam(mobloc)
+ if(jaunt_water_effect)
+ jaunt_steam(mobloc)
target.canmove = 0
holder.reappearing = 1
playsound(get_turf(target), 'sound/magic/ethereal_exit.ogg', 50, 1, -1)
- sleep(25 - jaunt_in_time)
+ sleep(jaunt_in_time * 4)
new jaunt_in_type(mobloc, holder.dir)
target.setDir(holder.dir)
sleep(jaunt_in_time)
qdel(holder)
if(!QDELETED(target))
- if(mobloc.density)
+ if(is_blocked_turf(mobloc, TRUE))
for(var/turf/T in orange(7))
- if(T)
- if(target.Move(T))
- break
- target.canmove = 1
+ if(isspaceturf(T))
+ continue
+ if(target.Move(T))
+ target.remove_CC()
+ return
+ for(var/turf/space/S in orange(7))
+ if(target.Move(S))
+ break
+ target.remove_CC()
/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/jaunt_steam(mobloc)
var/datum/effect_system/steam_spread/steam = new /datum/effect_system/steam_spread()
@@ -90,14 +99,34 @@
return
var/turf/newLoc = get_step(src,direction)
setDir(direction)
- if(!(newLoc.flags & NOJAUNT))
+ if(can_move(newLoc))
forceMove(newLoc)
else
- to_chat(user, "Some strange aura is blocking the way!")
+ to_chat(user, "Something is blocking the way!")
movedelay = world.time + movespeed
+/obj/effect/dummy/spell_jaunt/proc/can_move(turf/T)
+ if(T.flags & NOJAUNT)
+ return FALSE
+ return TRUE
+
/obj/effect/dummy/spell_jaunt/ex_act(blah)
return
/obj/effect/dummy/spell_jaunt/bullet_act(blah)
return
+
+/obj/effect/dummy/spell_jaunt/blood_pool
+ name = "sanguine pool"
+ desc = "a pool of living blood."
+ movespeed = 1.5
+
+/obj/effect/dummy/spell_jaunt/blood_pool/relaymove(mob/user, direction)
+ ..()
+ new /obj/effect/decal/cleanable/blood(loc)
+
+
+/obj/effect/dummy/spell_jaunt/blood_pool/can_move(turf/T)
+ if(isspaceturf(T) || T.density)
+ return FALSE
+ return TRUE
diff --git a/code/datums/spells/mime.dm b/code/datums/spells/mime.dm
index 79c4e796aa1..2ef54e6076f 100644
--- a/code/datums/spells/mime.dm
+++ b/code/datums/spells/mime.dm
@@ -93,11 +93,11 @@
/obj/effect/proc_holder/spell/targeted/mime/fingergun
name = "Finger Gun"
- desc = "Shoot stunning, invisible bullets out of your fingers! 6 bullets available per cast. Use your fingers to holster them manually."
+ desc = "Shoot stunning, invisible bullets out of your fingers! 3 bullets available per cast. Use your fingers to holster them manually."
school = "mime"
panel = "Mime"
clothes_req = 0
- charge_max = 600
+ charge_max = 300
range = -1
include_user = 1
human_req = 1
@@ -117,7 +117,7 @@
revert_cast(user)
/obj/effect/proc_holder/spell/targeted/mime/fingergun/fake
- desc = "Pretend you're shooting bullets out of your fingers! 6 bullets available per cast. Use your fingers to holster them manually."
+ desc = "Pretend you're shooting bullets out of your fingers! 3 bullets available per cast. Use your fingers to holster them manually."
gun = /obj/item/gun/projectile/revolver/fingergun/fake
// Mime Spellbooks
diff --git a/code/datums/spells/shapeshift.dm b/code/datums/spells/shapeshift.dm
index 41bf7f299d6..52aa5a266c3 100644
--- a/code/datums/spells/shapeshift.dm
+++ b/code/datums/spells/shapeshift.dm
@@ -95,6 +95,7 @@
invocation = "none"
invocation_type = "none"
action_icon_state = "vampire_bats"
+ gain_desc = "You have gained the ability to shapeshift into bat form. This is a weak form with no abilities, only useful for stealth."
shapeshift_type = /mob/living/simple_animal/hostile/scarybat/adminvampire
current_shapes = list(/mob/living/simple_animal/hostile/scarybat/adminvampire)
@@ -108,6 +109,7 @@
invocation_type = "none"
action_background_icon_state = "bg_demon"
action_icon_state = "glare"
+ gain_desc = "You have gained the ability to shapeshift into lesser hellhound form. This is a combat form with different abilities, tough but not invincible. It can regenerate itself over time by resting."
shapeshift_type = /mob/living/simple_animal/hostile/hellhound
current_shapes = list(/mob/living/simple_animal/hostile/hellhound)
diff --git a/code/datums/spells/turf_teleport.dm b/code/datums/spells/turf_teleport.dm
index adaa4afc94c..53568db8940 100644
--- a/code/datums/spells/turf_teleport.dm
+++ b/code/datums/spells/turf_teleport.dm
@@ -8,12 +8,16 @@
var/include_space = 0 //whether it includes space tiles in possible teleport locations
var/include_dense = 0 //whether it includes dense tiles in possible teleport locations
+ /// Whether the spell can teleport to light locations
+ var/include_light_turfs = TRUE
var/sound1 = 'sound/weapons/zapbang.ogg'
var/sound2 = 'sound/weapons/zapbang.ogg'
/obj/effect/proc_holder/spell/targeted/turf_teleport/cast(list/targets,mob/living/user = usr)
- playsound(get_turf(user), sound1, 50,1)
+ if(sound1)
+ playsound(get_turf(user), sound1, 50,1)
+
for(var/mob/living/target in targets)
var/list/turfs = new/list()
for(var/turf/T in range(target,outer_tele_radius))
@@ -22,6 +26,10 @@
if(T.density && !include_dense) continue
if(T.x>world.maxx-outer_tele_radius || T.x[server] - [players] player[players == 1 ? "" : "s"] online.")
+ to_chat(usr, "Offline instances are not reported")
diff --git a/code/modules/awaymissions/corpse.dm b/code/modules/awaymissions/corpse.dm
index 9ed88fc1c4e..ef3732988a7 100644
--- a/code/modules/awaymissions/corpse.dm
+++ b/code/modules/awaymissions/corpse.dm
@@ -111,9 +111,6 @@
/obj/effect/mob_spawn/proc/create(ckey, flavour = TRUE, name)
var/mob/living/M = new mob_type(get_turf(src)) //living mobs only
- var/mob/living/carbon/human/H = M
- if(H && !H.dna)
- H.Initialize(null)
if(!random)
M.real_name = mob_name ? mob_name : M.name
if(!mob_gender)
diff --git a/code/modules/awaymissions/mission_code/UO71-terrorspiders.dm b/code/modules/awaymissions/mission_code/UO71-terrorspiders.dm
index 37924f36064..d291e1c15ab 100644
--- a/code/modules/awaymissions/mission_code/UO71-terrorspiders.dm
+++ b/code/modules/awaymissions/mission_code/UO71-terrorspiders.dm
@@ -174,7 +174,7 @@
"}
/obj/item/gun/energy/laser/awaymission_aeg
- name = "Wireless Energy Gun"
+ name = "wireless energy gun"
desc = "An energy gun that recharges wirelessly during away missions. Does not work on the main station."
force = 10
origin_tech = null
diff --git a/code/modules/awaymissions/mission_code/challenge.dm b/code/modules/awaymissions/mission_code/challenge.dm
index 15dac122c54..a32f5e5eec1 100644
--- a/code/modules/awaymissions/mission_code/challenge.dm
+++ b/code/modules/awaymissions/mission_code/challenge.dm
@@ -23,7 +23,7 @@
/obj/machinery/power/emitter/energycannon
- name = "Energy Cannon"
+ name = "energy cannon"
desc = "A heavy duty industrial laser"
icon = 'icons/obj/singularity.dmi'
icon_state = "emitter"
diff --git a/code/modules/awaymissions/mission_code/ruins/snowbiodome.dm b/code/modules/awaymissions/mission_code/ruins/snowbiodome.dm
new file mode 100644
index 00000000000..787190cfdac
--- /dev/null
+++ b/code/modules/awaymissions/mission_code/ruins/snowbiodome.dm
@@ -0,0 +1,48 @@
+//Paper Fluff
+/obj/item/paper/fluff/ruins/snowbiodome/entrance
+ name = "Day 11 Entry"
+ info = "Day 11 EntryVampiric claws: Unlocked at 150 blood, allows you to summon a robust pair of claws that attack rapidly and drain a targets blood.
+Blood tendrils: Unlocked at 250 blood, allows you to slow everyone in a targeted 3x3 area after a short delay.
+Sanguine pool: Unlocked at 400 blood, allows you to travel at high speeds for a short duration. Doing this leaves behind blood splatters. You can move through anything but walls and space when doing this.
+Blood eruption: Unlocked at 600 blood, allows you to manipulate all nearby blood splatters, in 4 tiles around you, into spikes that impale anyone stood ontop of them.
+Full power
+
Cloak of darkness: Unlocked at 150 blood, when toggled, allows you to become nearly invisible and move rapidly when in dark regions. While active, burn damage is more effective against you.
+Shadow snare: Unlocked at 250 blood, allows you to summon a trap that when crossed blinds and ensares the victim. This trap is hard to see, but withers in the light.
+Dark passage: Unlocked at 400 blood, allows you to target a turf on screen, you will then teleport to that turf.
+Extinguish: Unlocked at 600 blood, allows you to snuff out nearby electronic light sources and glowshrooms.
+Full power
+
In addition, you also gain permament X-ray vision.
+Rejuvenate: Will heal you at an increased rate based on how much damage you have taken.
+Blood swell: Unlocked at 150 blood, increases your resistance to physical damage, stuns and stamina for 30 seconds. While it is active you cannot fire guns.
+Blood rush: Unlocked at 250 blood, gives you a short speed boost when cast.
+Blood swell II: Unlocked at 400 blood, increases all melee damage by 10.
+Overwhelming force: Unlocked at 600 blood, when toggled, if you bump into a door that you dont have access to, it will force it open. In addition, you cannot be pushed or pulled while it is active.
+Full Power
+