diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 8d7b4133840c..e2390c9fe817 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -89,6 +89,19 @@
],
"group": "build",
"label": "tgui: sonar"
+ },
+ {
+ "type": "shell",
+ "command": "bin/tgfont",
+ "windows": {
+ "command": ".\\bin\\tgfont.cmd"
+ },
+ "problemMatcher": [
+ "$tsc",
+ "$eslint-stylish"
+ ],
+ "group": "build",
+ "label": "tgui: rebuild tgfont"
}
]
}
diff --git a/bin/tgfont.cmd b/bin/tgfont.cmd
new file mode 100644
index 000000000000..b768c81d653a
--- /dev/null
+++ b/bin/tgfont.cmd
@@ -0,0 +1,2 @@
+@echo off
+call "%~dp0\..\tools\build\build.bat" --wait-on-error tg-font %*
diff --git a/code/__DEFINES/dcs/signals/atom/mob/living/signals_xeno.dm b/code/__DEFINES/dcs/signals/atom/mob/living/signals_xeno.dm
index 73b10d700247..e6e1e64e9c7e 100644
--- a/code/__DEFINES/dcs/signals/atom/mob/living/signals_xeno.dm
+++ b/code/__DEFINES/dcs/signals/atom/mob/living/signals_xeno.dm
@@ -74,3 +74,7 @@
/// From /mob/living/carbon/xenomorph/proc/handle_crit()
#define COMSIG_XENO_ENTER_CRIT "xeno_entering_critical"
+
+/// From /mob/living/carbon/xenomorph/proc/hivemind_talk(): (message)
+#define COMSIG_XENO_TRY_HIVEMIND_TALK "xeno_try_hivemind_talk"
+ #define COMPONENT_OVERRIDE_HIVEMIND_TALK (1<<0)
diff --git a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
index 710e4d9ae20a..61eb757e9c4d 100644
--- a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
+++ b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
@@ -109,6 +109,12 @@
#define COMSIG_MOB_EMOTED(emote_key) "mob_emoted_[emote_key]"
+#define COMSIG_MOB_TRY_EMOTE "mob_try_emote"
+ #define COMPONENT_OVERRIDE_EMOTE (1<<0)
+
+#define COMSIG_MOB_TRY_POINT "mob_try_point"
+ #define COMPONENT_OVERRIDE_POINT (1<<0)
+
//from /mob/living/set_stat()
#define COMSIG_MOB_STAT_SET_ALIVE "mob_stat_set_alive"
//from /mob/living/set_stat()
diff --git a/code/__DEFINES/keybinding.dm b/code/__DEFINES/keybinding.dm
index f4503aeea5d5..4b8fd9bd58b3 100644
--- a/code/__DEFINES/keybinding.dm
+++ b/code/__DEFINES/keybinding.dm
@@ -165,6 +165,8 @@
#define COMSIG_KB_XENO_HIVE_STATUS "keybinding_hive_status"
#define COMSIG_KB_XENO_HIDE "keybinding_hide"
#define COMSIG_KB_XENO_EVOLVE "keybinding_evolve"
+#define COMSIG_KB_XENO_PURCHASE_STRAIN "keybinding_purchase_strain"
+
// Yautja
#define COMSIG_KB_YAUTJA_BUTCHER "keybinding_yautja_butcher"
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index 7ec2ab8b975e..e2bd868f9a80 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -309,77 +309,6 @@
#define CAN_HOLD_TWO_HANDS 1
#define CAN_HOLD_ONE_HAND 2
-// ------------ //
-// STRAIN FLAGS //
-// ------------ //
-
-// Queen strain flags
-#define QUEEN_NORMAL "Normal"
-
-// Facehugger strain flags
-#define FACEHUGGER_NORMAL "Normal"
-#define FACEHUGGER_WATCHER "Watcher"
-
-// Drone strain flags
-#define DRONE_NORMAL "Normal"
-#define DRONE_HEALER "Healer"
-#define DRONE_GARDENER "Gardener"
-
-// Hivelord strain flags
-#define HIVELORD_NORMAL "Normal"
-#define HIVELORD_RESIN_WHISPERER "Resin Whisperer"
-
-// Carrier strain flags
-#define CARRIER_NORMAL "Normal"
-#define CARRIER_EGGSAC "Eggsac"
-
-// Burrower strain flags
-#define BURROWER_NORMAL "Normal"
-#define BURROWER_TREMOR "Tremor"
-
-// Sentinel strain flags
-#define SENTINEL_NORMAL "Normal"
-
-// Spitter strain flags
-#define SPITTER_NORMAL "Normal"
-
-// Boiler strain flags
-#define BOILER_NORMAL "Normal"
-#define BOILER_TRAPPER "Trapper"
-
-// Runner strain flags
-#define RUNNER_NORMAL "Normal"
-#define RUNNER_ACIDER "Acider"
-
-// Lurker strain flags
-#define LURKER_NORMAL "Normal"
-#define LURKER_VAMPIRE "Vampire"
-// Ravager strain flags
-#define RAVAGER_NORMAL "Normal"
-#define RAVAGER_HEDGEHOG "Hedgehog"
-#define RAVAGER_BERSERKER "Berserker"
-
-// Defender strain flags
-#define DEFENDER_NORMAL "Normal"
-#define DEFENDER_STEELCREST "Steelcrest"
-
-// Warrior strain flags
-#define WARRIOR_NORMAL "Normal"
-
-// Crusher strain flags
-#define CRUSHER_NORMAL "Normal"
-#define CRUSHER_CHARGER "Charger"
-
-// Praetorian strain flags
-#define PRAETORIAN_NORMAL "Normal"
-#define PRAETORIAN_VANGUARD "Vanguard"
-#define PRAETORIAN_DANCER "Dancer"
-#define PRAETORIAN_WARDEN "Warden"
-#define PRAETORIAN_OPPRESSOR "Oppressor"
-
-// Hellhound strain flags
-#define HELLHOUND_NORMAL "Normal"
-
GLOBAL_LIST_INIT(default_onmob_icons, list(
WEAR_L_HAND = 'icons/mob/humans/onmob/items_lefthand_0.dmi',
WEAR_R_HAND = 'icons/mob/humans/onmob/items_righthand_0.dmi',
@@ -452,4 +381,3 @@ GLOBAL_LIST_INIT(default_xeno_onmob_icons, list(
#define MOBILITY_FLAGS_DEFAULT (MOBILITY_MOVE | MOBILITY_STAND)
#define MOBILITY_FLAGS_CARBON_DEFAULT (MOBILITY_MOVE | MOBILITY_STAND | MOBILITY_REST | MOBILITY_LIEDOWN)
#define MOBILITY_FLAGS_REST_CAPABLE_DEFAULT (MOBILITY_MOVE | MOBILITY_STAND | MOBILITY_REST | MOBILITY_LIEDOWN)
-
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 96335a3c1acf..47aa0e732c76 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -146,8 +146,9 @@
#define SS_INIT_DATABASE -27
#define SS_INIT_ENTITYMANAGER -28
#define SS_INIT_PLAYTIME -29
-#define SS_INIT_PREDSHIPS -30
-#define SS_INIT_OBJECTIVES -31
+#define SS_INIT_STICKY -30
+#define SS_INIT_PREDSHIPS -31
+#define SS_INIT_OBJECTIVES -32
#define SS_INIT_MINIMAP -34
#define SS_INIT_STATPANELS -98
#define SS_INIT_CHAT -100 //Should be last to ensure chat remains smooth during init.
diff --git a/code/__DEFINES/xeno.dm b/code/__DEFINES/xeno.dm
index 2761bc12acf0..6f6e4eef7c20 100644
--- a/code/__DEFINES/xeno.dm
+++ b/code/__DEFINES/xeno.dm
@@ -361,6 +361,48 @@
#define RESIN_CONSTRUCTION_NO_MAX -1
+// -------------- //
+// STRAIN DEFINES //
+// -------------- //
+
+// Facehugger strain flags
+#define FACEHUGGER_WATCHER "Watcher"
+
+// Drone strain flags
+#define DRONE_HEALER "Healer"
+#define DRONE_GARDENER "Gardener"
+
+// Hivelord strain flags
+#define HIVELORD_RESIN_WHISPERER "Resin Whisperer"
+
+// Carrier strain flags
+#define CARRIER_EGGSAC "Eggsac"
+
+// Boiler strain flags
+#define BOILER_TRAPPER "Trapper"
+
+// Runner strain flags
+#define RUNNER_ACIDER "Acider"
+
+// Lurker strain flags
+#define LURKER_VAMPIRE "Vampire"
+
+// Ravager strain flags
+#define RAVAGER_HEDGEHOG "Hedgehog"
+#define RAVAGER_BERSERKER "Berserker"
+
+// Defender strain flags
+#define DEFENDER_STEELCREST "Steelcrest"
+
+// Crusher strain flags
+#define CRUSHER_CHARGER "Charger"
+
+// Praetorian strain flags
+#define PRAETORIAN_VANGUARD "Vanguard"
+#define PRAETORIAN_DANCER "Dancer"
+#define PRAETORIAN_WARDEN "Warden"
+#define PRAETORIAN_OPPRESSOR "Oppressor"
+
/////////////////////////////////////////////////////////////////////////////////////
//
// Modifiers
diff --git a/code/__HELPERS/logging.dm b/code/__HELPERS/logging.dm
index c20db3da303f..d6c18c8a93be 100644
--- a/code/__HELPERS/logging.dm
+++ b/code/__HELPERS/logging.dm
@@ -209,10 +209,10 @@ GLOBAL_VAR_INIT(log_end, world.system_type == UNIX ? ascii2text(13) : "")
WRITE_LOG(GLOB.world_game_log, "MISC: [text]")
GLOB.STUI?.debug.Add("\[[time]]MISC: [text]")
-/proc/log_mutator(text)
- if(!GLOB.mutator_logs)
+/proc/log_strain(text)
+ if(!GLOB.strain_logs)
return
- WRITE_LOG(GLOB.mutator_logs, "[text]")
+ WRITE_LOG(GLOB.strain_logs, "[text]")
/proc/log_hiveorder(text)
var/time = time_stamp()
diff --git a/code/_globalvars/global_lists.dm b/code/_globalvars/global_lists.dm
index 1f3404f403f2..7d9cd3324067 100644
--- a/code/_globalvars/global_lists.dm
+++ b/code/_globalvars/global_lists.dm
@@ -166,9 +166,6 @@ GLOBAL_LIST_INIT(language_keys, setup_language_keys()) //table of say codes for
GLOBAL_REFERENCE_LIST_INDEXED(origins, /datum/origin, name)
GLOBAL_LIST_INIT(player_origins, USCM_ORIGINS)
-//Xeno mutators
-GLOBAL_REFERENCE_LIST_INDEXED_SORTED(xeno_mutator_list, /datum/xeno_mutator, name)
-
//Xeno hives
GLOBAL_LIST_INIT_TYPED(hive_datum, /datum/hive_status, list(
XENO_HIVE_NORMAL = new /datum/hive_status(),
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index eb1b0540fb54..83929ecf8803 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -531,6 +531,8 @@ This maintains a list of ip addresses that are able to bypass topic filtering.
/datum/config_entry/string/round_results_webhook_url
+/datum/config_entry/string/important_log_channel
+
/// InfluxDB v2 Host to connect to for sending statistics (over HTTP API)
/datum/config_entry/string/influxdb_host
/// InfluxDB v2 Bucket to send staistics to
diff --git a/code/controllers/subsystem/stickyban.dm b/code/controllers/subsystem/stickyban.dm
new file mode 100644
index 000000000000..48e934addc1a
--- /dev/null
+++ b/code/controllers/subsystem/stickyban.dm
@@ -0,0 +1,284 @@
+SUBSYSTEM_DEF(stickyban)
+ name = "Sticky Ban"
+ init_order = SS_INIT_STICKY
+ flags = SS_NO_FIRE
+
+/datum/controller/subsystem/stickyban/Initialize()
+ var/list/all_bans = world.GetConfig("ban")
+
+ for(var/existing_ban in all_bans)
+ var/list/ban_data = params2list(world.GetConfig("ban", existing_ban))
+ INVOKE_ASYNC(src, PROC_REF(import_sticky), existing_ban, ban_data)
+
+ return SS_INIT_SUCCESS
+
+/**
+ * Returns a list of [/datum/view_record/stickyban]s, or null, if no stickybans are found. All arguments are optional, but you should pass at least one if you want any results.
+ */
+/datum/controller/subsystem/stickyban/proc/check_for_sticky_ban(ckey, address, computer_id)
+ var/list/stickyban_ids = list()
+
+ for(var/datum/view_record/stickyban_matched_ckey/matched_ckey as anything in get_impacted_ckey_records(ckey))
+ stickyban_ids += matched_ckey.linked_stickyban
+
+ for(var/datum/view_record/stickyban_matched_cid/matched_cid as anything in get_impacted_cid_records(computer_id))
+ stickyban_ids += matched_cid.linked_stickyban
+
+ for(var/datum/view_record/stickyban_matched_ip/matched_ip as anything in get_impacted_ip_records(address))
+ stickyban_ids += matched_ip.linked_stickyban
+
+ if(!length(stickyban_ids))
+ return FALSE
+
+ var/list/datum/view_record/stickyban/stickies = DB_VIEW(/datum/view_record/stickyban,
+ DB_AND(
+ DB_COMP("id", DB_IN, stickyban_ids),
+ DB_COMP("active", DB_EQUALS, TRUE)
+ )
+ )
+
+ for(var/datum/view_record/stickyban/current_sticky in stickies)
+ if(length(get_whitelisted_ckey_records(current_sticky.id, ckey)))
+ stickies -= current_sticky
+
+ if(!length(stickies))
+ return FALSE
+
+ return stickies
+
+/**
+ * Associates an existing stickyban with a new match, either of a ckey, address, or computer_id. Or all three.
+ *
+ * Arguments:
+ * - existing_ban_id, int, required
+ * - ckey, string, optional
+ * - address, string, optional
+ * - computer_id, string, optional
+ */
+/datum/controller/subsystem/stickyban/proc/match_sticky(existing_ban_id, ckey, address, computer_id)
+ if(!existing_ban_id)
+ return
+
+ if(ckey)
+ add_matched_ckey(existing_ban_id, ckey)
+
+ if(address)
+ add_matched_ip(existing_ban_id, address)
+
+ if(computer_id)
+ add_matched_cid(existing_ban_id, computer_id)
+
+/**
+ * Adds a new tracked stickyban, and returns a [/datum/entity/stickyban] if it was successful. Blocking, sleeps.
+ */
+/datum/controller/subsystem/stickyban/proc/add_stickyban(identifier, reason, message, datum/entity/player/banning_admin, override_date)
+ var/datum/entity/stickyban/new_sticky = DB_ENTITY(/datum/entity/stickyban)
+ new_sticky.identifier = identifier
+ new_sticky.reason = reason
+ new_sticky.message = message
+
+ if(banning_admin)
+ new_sticky.adminid = banning_admin.id
+
+ new_sticky.date = override_date ? override_date : "[time2text(world.realtime, "YYYY-MM-DD hh:mm:ss")]"
+ new_sticky.save()
+ new_sticky.sync()
+
+ return new_sticky
+
+/// Adds a ckey match to the specified sticky ban.
+/datum/controller/subsystem/stickyban/proc/add_matched_ckey(existing_ban_id, key)
+ key = ckey(key)
+
+ if(length(DB_VIEW(/datum/view_record/stickyban_matched_ckey,
+ DB_AND(
+ DB_COMP("linked_stickyban", DB_EQUALS, existing_ban_id),
+ DB_COMP("ckey", DB_EQUALS, key)
+ )
+ )))
+ return
+
+ var/datum/entity/stickyban_matched_ckey/matched_ckey = DB_ENTITY(/datum/entity/stickyban_matched_ckey)
+
+ matched_ckey.ckey = key
+ matched_ckey.linked_stickyban = existing_ban_id
+
+ matched_ckey.save()
+
+/// Adds an IP match to the specified stickyban.
+/datum/controller/subsystem/stickyban/proc/add_matched_ip(existing_ban_id, ip)
+ if(length(DB_VIEW(/datum/view_record/stickyban_matched_ip,
+ DB_AND(
+ DB_COMP("linked_stickyban", DB_EQUALS, existing_ban_id),
+ DB_COMP("ip", DB_EQUALS, ip)
+ )
+ )))
+ return
+
+ var/datum/entity/stickyban_matched_ip/matched_ip = DB_ENTITY(/datum/entity/stickyban_matched_ip)
+
+ matched_ip.ip = ip
+ matched_ip.linked_stickyban = existing_ban_id
+
+ matched_ip.save()
+
+/// Adds a CID match to the specified stickyban.
+/datum/controller/subsystem/stickyban/proc/add_matched_cid(existing_ban_id, cid)
+ if(length(DB_VIEW(/datum/view_record/stickyban_matched_cid,
+ DB_AND(
+ DB_COMP("linked_stickyban", DB_EQUALS, existing_ban_id),
+ DB_COMP("cid", DB_EQUALS, cid)
+ )
+ )))
+ return
+
+
+ var/datum/entity/stickyban_matched_cid/matched_cid = DB_ENTITY(/datum/entity/stickyban_matched_cid)
+
+ matched_cid.cid = cid
+ matched_cid.linked_stickyban = existing_ban_id
+
+ matched_cid.save()
+
+/// Whitelists a specific CKEY to the specified stickyban, which will allow connection, even with matching CIDs and IPs.
+/datum/controller/subsystem/stickyban/proc/whitelist_ckey(existing_ban_id, key)
+ key = ckey(key)
+
+ if(!key)
+ return
+
+ var/id_to_select
+
+ var/list/datum/view_record/stickyban_matched_ckey/existing_matches = DB_VIEW(/datum/view_record/stickyban_matched_ckey,
+ DB_AND(
+ DB_COMP("linked_stickyban", DB_EQUALS, existing_ban_id),
+ DB_COMP("ckey", DB_EQUALS, key)
+ )
+ )
+
+ if(length(existing_matches))
+ var/datum/view_record/stickyban_matched_ckey/match = existing_matches[1]
+ id_to_select = match.id
+
+ var/datum/entity/stickyban_matched_ckey/whitelisted_ckey = DB_ENTITY(/datum/entity/stickyban_matched_ckey, id_to_select)
+
+ whitelisted_ckey.ckey = key
+ whitelisted_ckey.linked_stickyban = existing_ban_id
+ whitelisted_ckey.whitelisted = TRUE
+
+ whitelisted_ckey.save()
+
+/**
+ * Returns a [/list] of [/datum/view_record/stickyban_matched_ckey] where the ckey provided has not been
+ * whitelisted from the stickyban, and would be prevented from joining - provided that the stickyban itself
+ * remains active.
+ */
+/datum/controller/subsystem/stickyban/proc/get_impacted_ckey_records(key)
+ key = ckey(key)
+
+ return DB_VIEW(/datum/view_record/stickyban_matched_ckey,
+ DB_AND(
+ DB_COMP("ckey", DB_EQUALS, key),
+ DB_COMP("whitelisted", DB_EQUALS, FALSE)
+ )
+ )
+
+/**
+ * Returns a [/list] of [/datum/view_record/stickyban_matched_ckey] which have been manually whitelisted by an admin and matches the provided existing_ban_id and key.
+ */
+/datum/controller/subsystem/stickyban/proc/get_whitelisted_ckey_records(existing_ban_id, key)
+ key = ckey(key)
+
+ return DB_VIEW(/datum/view_record/stickyban_matched_ckey,
+ DB_AND(
+ DB_COMP("linked_stickyban", DB_EQUALS, existing_ban_id),
+ DB_COMP("ckey", DB_EQUALS, key),
+ DB_COMP("whitelisted", DB_EQUALS, TRUE),
+ )
+ )
+
+/**
+ * Returns a [/list] of [/datum/view_record/stickyban_matched_cid] where the impacted CID matches the CID provided.
+ * Connections matching this CID will be blocked - provided the linked stickyban is active.
+ */
+/datum/controller/subsystem/stickyban/proc/get_impacted_cid_records(cid)
+ return DB_VIEW(/datum/view_record/stickyban_matched_cid,
+ DB_COMP("cid", DB_EQUALS, cid)
+ )
+
+/**
+ * Returns a [/list] of [/datum/view_record/stickyban_matched_ip] where the impacted IP matches the IP provided.
+ * Connections matchin this IP will be blocked - provided the linked stickyban is active.
+ */
+/datum/controller/subsystem/stickyban/proc/get_impacted_ip_records(ip)
+ return DB_VIEW(/datum/view_record/stickyban_matched_ip,
+ DB_COMP("ip", DB_EQUALS, ip)
+ )
+
+/// Legacy import from pager bans to database bans.
+/datum/controller/subsystem/stickyban/proc/import_sticky(identifier, list/ban_data)
+ WAIT_DB_READY
+
+ if(ban_data["type"] != "sticky")
+ handle_old_perma(identifier, ban_data)
+ return
+
+ if(!ban_data["message"])
+ ban_data["message"] = "Evasion"
+
+ add_stickyban(identifier, ban_data["reason"], ban_data["message"], override_date = "LEGACY")
+
+/**
+ * We abuse the on_insert from ndatabase here to ensure we have the synced ID of the new stickyban when applying a *lot* of associated bans. If we don't have a matching pager ban with the new sticky's identifier, we stop.
+ */
+/datum/entity_meta/stickyban/on_insert(datum/entity/stickyban/new_sticky)
+ var/list/ban_data = params2list(world.GetConfig("ban", new_sticky.identifier))
+
+ if(!length(ban_data))
+ return
+
+ var/list/whitelisted = list()
+ if(ban_data["whitelist"])
+ whitelisted = splittext(ban_data["whitelist"], ",")
+ for(var/key in whitelisted)
+ SSstickyban.whitelist_ckey(new_sticky.id, key)
+
+ if(ban_data["keys"])
+ var/list/keys = splittext(ban_data["keys"], ",")
+ keys -= whitelisted
+ for(var/key in keys)
+ SSstickyban.add_matched_ckey(new_sticky.id, key)
+
+ if(ban_data["computer_id"])
+ var/list/cids = splittext(ban_data["computer_id"], ",")
+ for(var/cid in cids)
+ SSstickyban.add_matched_cid(new_sticky.id, cid)
+
+ if(ban_data["IP"])
+ var/list/ips = splittext(ban_data["IP"], ",")
+ for(var/ip in ips)
+ SSstickyban.add_matched_ip(new_sticky.id, ip)
+
+ world.SetConfig("ban", new_sticky.identifier, null)
+
+/// Imports permabans from the old ban.txt, and does *not* ban people that have been whitelisted.
+/datum/controller/subsystem/stickyban/proc/handle_old_perma(identifier, list/ban_data)
+ var/list/keys_to_ban = list()
+
+ keys_to_ban += splittext(ban_data["keys"], ",")
+
+ for(var/x in 1 to length(keys_to_ban))
+ keys_to_ban[x] = ckey(keys_to_ban[x])
+
+ var/list/keys = splittext(ban_data["whitelist"], ",")
+ for(var/key in keys)
+ keys_to_ban -= ckey(key)
+
+ for(var/key in keys_to_ban)
+ var/datum/entity/player/player_entity = get_player_from_key(key)
+ if(!player_entity)
+ continue
+
+ INVOKE_ASYNC(player_entity, TYPE_PROC_REF(/datum/entity/player, add_perma_ban), ban_data["message"])
+
+ world.SetConfig("ban", identifier, null)
diff --git a/code/datums/ammo/bullet/rifle.dm b/code/datums/ammo/bullet/rifle.dm
index 0be6f1db8ff4..ab30599eeb12 100644
--- a/code/datums/ammo/bullet/rifle.dm
+++ b/code/datums/ammo/bullet/rifle.dm
@@ -169,16 +169,16 @@
shell_speed = AMMO_SPEED_TIER_6
/datum/ammo/bullet/rifle/m4ra/impact/on_hit_mob(mob/M, obj/projectile/P)
- knockback(M, P, 32) // Can knockback basically at max range
+ knockback(M, P, 32) // Can knockback basically at max range max range is 24 tiles...
/datum/ammo/bullet/rifle/m4ra/impact/knockback_effects(mob/living/living_mob, obj/projectile/fired_projectile)
if(iscarbonsizexeno(living_mob))
var/mob/living/carbon/xenomorph/target = living_mob
to_chat(target, SPAN_XENODANGER("You are shaken and slowed by the sudden impact!"))
- target.KnockDown(0.5) // purely for visual effect, noone actually cares
- target.Stun(0.5)
- target.apply_effect(2, SUPERSLOW)
- target.apply_effect(5, SLOW)
+ target.KnockDown(0.5-fired_projectile.distance_travelled/100) // purely for visual effect, noone actually cares
+ target.Stun(0.5-fired_projectile.distance_travelled/100)
+ target.apply_effect(2-fired_projectile.distance_travelled/20, SUPERSLOW)
+ target.apply_effect(5-fired_projectile.distance_travelled/10, SLOW)
else
if(!isyautja(living_mob)) //Not predators.
living_mob.apply_effect(1, SUPERSLOW)
diff --git a/code/datums/components/temporary_mute.dm b/code/datums/components/temporary_mute.dm
new file mode 100644
index 000000000000..0fed49c4778e
--- /dev/null
+++ b/code/datums/components/temporary_mute.dm
@@ -0,0 +1,90 @@
+/datum/component/temporary_mute
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+ /// A message to tell the user when they attempt to speak, if any
+ var/on_speak_message = ""
+ /// A message to tell the user when they attempt to emote, if any
+ var/on_emote_message = ""
+ /// A message to tell the user when they become no longer mute, if any
+ var/on_unmute_message = ""
+ /// How long after the component's initialization it should be deleted. -1 means it will never delete
+ var/time_until_unmute = 3 MINUTES
+
+/datum/component/temporary_mute/Initialize(on_speak_message = "", on_emote_message = "", on_unmute_message = "", time_until_unmute = 3 MINUTES)
+ . = ..()
+ if(!ismob(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.on_speak_message = on_speak_message
+ src.on_emote_message = on_emote_message
+ src.on_unmute_message = on_unmute_message
+ src.time_until_unmute = time_until_unmute
+ if(time_until_unmute != -1)
+ QDEL_IN(src, time_until_unmute)
+
+/datum/component/temporary_mute/RegisterWithParent()
+ ..()
+ RegisterSignal(parent, COMSIG_LIVING_SPEAK, PROC_REF(on_speak))
+ RegisterSignal(parent, COMSIG_XENO_TRY_HIVEMIND_TALK, PROC_REF(on_hivemind))
+ RegisterSignal(parent, COMSIG_MOB_TRY_EMOTE, PROC_REF(on_emote))
+ RegisterSignal(parent, COMSIG_MOB_TRY_POINT, PROC_REF(on_point))
+
+/datum/component/temporary_mute/UnregisterFromParent()
+ ..()
+ if(parent)
+ UnregisterSignal(parent, COMSIG_LIVING_SPEAK)
+ UnregisterSignal(parent, COMSIG_XENO_TRY_HIVEMIND_TALK)
+ UnregisterSignal(parent, COMSIG_MOB_TRY_EMOTE)
+ UnregisterSignal(parent, COMSIG_MOB_TRY_POINT)
+ if(on_unmute_message)
+ to_chat(parent, SPAN_NOTICE(on_unmute_message))
+
+/datum/component/temporary_mute/proc/on_speak(
+ mob/user,
+ message,
+ datum/language/speaking = null,
+ verb = "says",
+ alt_name = "",
+ italics = FALSE,
+ message_range = GLOB.world_view_size,
+ sound/speech_sound,
+ sound_vol,
+ nolog = FALSE,
+ message_mode = null
+)
+ SIGNAL_HANDLER
+
+ if(!nolog)
+ log_say("[user.name != "Unknown" ? user.name : "([user.real_name])"] attempted to say the following before their spawn mute ended: [message] (CKEY: [user.key]) (JOB: [user.job])")
+ if(on_speak_message)
+ to_chat(parent, SPAN_BOLDNOTICE(on_speak_message))
+ return COMPONENT_OVERRIDE_SPEAK
+
+/datum/component/temporary_mute/proc/on_hivemind(mob/user, message)
+ SIGNAL_HANDLER
+
+ log_say("[user.name != "Unknown" ? user.name : "([user.real_name])"] attempted to hivemind the following before their spawn mute ended: [message] (CKEY: [user.key]) (JOB: [user.job])")
+ if(on_speak_message)
+ to_chat(parent, SPAN_BOLDNOTICE(on_speak_message))
+ return COMPONENT_OVERRIDE_HIVEMIND_TALK
+
+/datum/component/temporary_mute/proc/on_emote(mob/user, datum/emote/current_emote, act, m_type, param, intentional)
+ SIGNAL_HANDLER
+
+ // Allow involuntary emotes or non-custom emotes
+ if(!intentional)
+ return
+ if(!param && !istype(current_emote, /datum/emote/custom))
+ return
+
+ log_say("[user.name != "Unknown" ? user.name : "([user.real_name])"] attempted to emote the following before their spawn mute ended: [param] (CKEY: [user.key]) (JOB: [user.job])")
+ if(on_emote_message)
+ to_chat(parent, SPAN_BOLDNOTICE(on_emote_message))
+ return COMPONENT_OVERRIDE_EMOTE
+
+/datum/component/temporary_mute/proc/on_point(mob/user, atom/target)
+ SIGNAL_HANDLER
+
+ log_say("[user.name != "Unknown" ? user.name : "([user.real_name])"] attempted to point at the following before their spawn mute ended: [target] (CKEY: [user.key]) (JOB: [user.job])")
+ if(on_emote_message)
+ to_chat(parent, SPAN_BOLDNOTICE(on_emote_message))
+ return COMPONENT_OVERRIDE_POINT
diff --git a/code/datums/emergency_calls/goons.dm b/code/datums/emergency_calls/goons.dm
index 8a0b00968807..fb491a2f43c4 100644
--- a/code/datums/emergency_calls/goons.dm
+++ b/code/datums/emergency_calls/goons.dm
@@ -96,6 +96,45 @@
to_chat(backstory_human, SPAN_BOLD("You heard about the original distress signal ages ago, but you have only just gotten permission from corporate to enter the area."))
to_chat(backstory_human, SPAN_BOLD("Ensure no damage is incurred against Weyland-Yutani. Make sure the researcher is kept safe and follow their instructions."))
+/datum/emergency_call/goon/bodyguard
+ name = "Weyland-Yutani Goon (Executive Bodyguard Detail)"
+ mob_max = 1
+ mob_min = 1
+
+/datum/emergency_call/goon/bodyguard/New()
+ ..()
+ dispatch_message = "[MAIN_SHIP_NAME], this is a Weyland-Yutani Corporate Security Protection Detail shuttle inbound to the Liaison's Beacon."
+ objectives = "Protect the Corporate Liaison and follow his commands, unless it goes against Company policy. Do not damage Wey-Yu property."
+
+/datum/emergency_call/goon/bodyguard/create_member(datum/mind/M, turf/override_spawn_loc)
+ var/turf/spawn_loc = override_spawn_loc ? override_spawn_loc : get_spawn_point()
+
+ if(!istype(spawn_loc))
+ return //Didn't find a useable spawn point.
+
+ var/mob/living/carbon/human/mob = new(spawn_loc)
+ M.transfer_to(mob, TRUE)
+
+ if(!leader && HAS_FLAG(mob.client.prefs.toggles_ert, PLAY_LEADER) && check_timelock(mob.client, JOB_SQUAD_LEADER, time_required_for_job))
+ leader = mob
+ to_chat(mob, SPAN_ROLE_HEADER("You are a Weyland-Yutani Corporate Security Lead!"))
+ arm_equipment(mob, /datum/equipment_preset/goon/lead, TRUE, TRUE)
+ else
+ to_chat(mob, SPAN_ROLE_HEADER("You are a Weyland-Yutani Corporate Security Officer!"))
+ arm_equipment(mob, /datum/equipment_preset/goon/standard, TRUE, TRUE)
+
+ print_backstory(mob)
+
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(to_chat), mob, SPAN_BOLD("Objectives: [objectives]")), 1 SECONDS)
+
+/datum/emergency_call/goon/bodyguard/print_backstory(mob/living/carbon/human/M)
+ to_chat(M, SPAN_BOLD("You were born [pick(75;"in Europe", 15;"in Asia", 10;"on Mars")] to a poor family."))
+ to_chat(M, SPAN_BOLD("Joining the ranks of Weyland-Yutani was all you could do to keep yourself and your loved ones fed."))
+ to_chat(M, SPAN_BOLD("You have no idea what a xenomorph is."))
+ to_chat(M, SPAN_BOLD("You are a simple security officer employed by Weyland-Yutani to guard their Executives from all Divisions alike."))
+ to_chat(M, SPAN_BOLD("You were sent to act as the Executives bodyguard on the [MAIN_SHIP_NAME], you have gotten permission from corporate to enter the area."))
+ to_chat(M, SPAN_BOLD("Ensure no damage is incurred against Weyland-Yutani. Make sure the CL is safe."))
+
/datum/emergency_call/goon/platoon
name = "Weyland-Yutani Corporate Security (Platoon)"
mob_min = 8
diff --git a/code/datums/emergency_calls/inspection.dm b/code/datums/emergency_calls/inspection.dm
index f0400528ac31..d2efcf5fe275 100644
--- a/code/datums/emergency_calls/inspection.dm
+++ b/code/datums/emergency_calls/inspection.dm
@@ -164,6 +164,42 @@
new /obj/item/storage/box/handcuffs(drop_spawn)
new /obj/item/storage/box/handcuffs(drop_spawn)
+/datum/emergency_call/inspection_wy/lawyer
+ name = "Lawyers - Corporate"
+ mob_max = 2
+ mob_min = 1
+ name_of_spawn = /obj/effect/landmark/ert_spawns/distress_pmc
+ item_spawn = /obj/effect/landmark/ert_spawns/distress_pmc/item
+ probability = 0
+
+/datum/emergency_call/inspection_wy/lawyer/New()
+ ..()
+ objectives = "Make sure the crew of the [MAIN_SHIP_NAME] is aware of your presence. Investigate who the Corporate Liaison reported for breaking their contract and any review other Company assets and make sure they remain loyal to the Company. Make a detailed report back to Corporate."
+
+/datum/emergency_call/inspection_wy/lawyer/create_member(datum/mind/M, turf/override_spawn_loc)
+ var/turf/T = override_spawn_loc ? override_spawn_loc : get_spawn_point()
+
+ if(!istype(T))
+ return FALSE
+
+ var/mob/living/carbon/human/H = new(T)
+ M.transfer_to(H, TRUE)
+
+ if(!leader && HAS_FLAG(H.client.prefs.toggles_ert, PLAY_LEADER) && check_timelock(H.client, list(JOB_SQUAD_LEADER), time_required_for_job))
+ leader = H
+ arm_equipment(H, /datum/equipment_preset/wy/exec_supervisor/lawyer, TRUE, TRUE)
+ to_chat(H, SPAN_ROLE_HEADER("You are a Weyland-Yutani Lead Corporate Attorney!"))
+ to_chat(H, SPAN_ROLE_BODY("While officially the Corporate Affairs Division does mundane paperwork for Weyland-Yutani, in practice you serve as both official and unofficial investigators into conduct of Company and non-Company personnel. You are being dispatched to the [MAIN_SHIP_NAME] to make sure that the USCM abides by it's signed contracts provided by the local Liaison and that they have not forgotten the real hand that feeds them."))
+ to_chat(H, SPAN_ROLE_BODY("Remember the USCM personnel on the ship may not appreciate your presence there. Should the Liaison be in jail, you are to act as legal counsel in any way. Your basic duty is to make a detailed report of anything involving the Liaison, any other WY personnel and of course any contract violations on board the ship."))
+ to_chat(H, SPAN_WARNING("You are to avoid open conflict with the Marines. Retreat and make a report if they are outright hostile. Ahelp if you have any more questions or wish to release this character for other players."))
+ else
+ arm_equipment(H, /datum/equipment_preset/wy/exec_spec/lawyer, TRUE, TRUE)
+ to_chat(H, SPAN_ROLE_HEADER("You are a Weyland-Yutani Corporate Attorney!"))
+ to_chat(H, SPAN_ROLE_BODY("While officially the Corporate Affairs Division does mundane paperwork for Weyland-Yutani, in practice you serve as both official and unofficial investigators into conduct of Company and non-Company personnel. The Lead Attorney is in charge, your duty is to provide counsel and any other form of assistance you can render to make sure your mission is a success."))
+ to_chat(H, SPAN_ROLE_BODY("Remember that the USCM, or at least some parts of it, may be hostile towards your presence on the ship. You and the Lead Attorney are to avoid open conflict with the Marines. Your main priority is making sure that you both survive to write the report the Company is due."))
+ to_chat(H, SPAN_WARNING("You are to avoid open conflict with the Marines. Retreat and make a report if they are outright hostile. Ahelp if you have any more questions or wish to release this character for other players."))
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(to_chat), H, SPAN_BOLD("Objectives: [objectives]")), 1 SECONDS)
+
// Colonial Marshals - UA Law Enforcement / Investigative Federal Agents which usually watch over Colonies. Also a good option for prisoner transfers, investigating corporate corruption, survivor rescues, or illict trade practices(black market).
/datum/emergency_call/inspection_cmb
diff --git a/code/datums/entities/player.dm b/code/datums/entities/player.dm
index 2973a174858f..f0b601652fe0 100644
--- a/code/datums/entities/player.dm
+++ b/code/datums/entities/player.dm
@@ -305,6 +305,36 @@ BSQL_PROTECT_DATUM(/datum/entity/player)
return TRUE
+/// Permanently bans this user, with the provided reason. The banner ([/datum/entity/player]) argument is optional, as this can be done without admin intervention.
+/datum/entity/player/proc/add_perma_ban(reason, internal_reason, datum/entity/player/banner)
+ if(is_permabanned)
+ return FALSE
+
+ is_permabanned = TRUE
+ permaban_date = "[time2text(world.realtime, "YYYY-MM-DD hh:mm:ss")]"
+ permaban_reason = reason
+
+ if(banner)
+ permaban_admin_id = banner.id
+ message_admins("[key_name_admin(banner.owning_client)] has permanently banned [ckey] for '[reason]'.")
+ var/datum/tgs_chat_embed/field/reason_embed
+ if(internal_reason)
+ reason_embed = new("Permaban Reason", internal_reason)
+ important_message_external("[banner.owning_client] has permanently banned [ckey] for '[reason]'.", "Permaban Placed", reason_embed ? list(reason_embed) : null)
+
+ add_note("Permanently banned | [reason]", FALSE, NOTE_ADMIN, TRUE)
+ if(internal_reason)
+ add_note("Internal reason: [internal_reason]", TRUE, NOTE_ADMIN)
+
+ if(owning_client)
+ to_chat_forced(owning_client, SPAN_LARGE("You have been permanently banned by [banner.ckey].\nReason: [reason]."))
+ to_chat_forced(owning_client, SPAN_LARGE("This is a permanent ban. It will not be removed."))
+ QDEL_NULL(owning_client)
+
+ save()
+
+ return TRUE
+
/datum/entity/player/proc/auto_unban()
if(!is_time_banned)
return
@@ -447,54 +477,45 @@ BSQL_PROTECT_DATUM(/datum/entity/player)
record_login_triplet(player.ckey, address, computer_id)
player_data.sync()
-/datum/entity/player/proc/check_ban(computer_id, address)
+/datum/entity/player/proc/check_ban(computer_id, address, is_telemetry)
. = list()
- var/list/linked_bans = check_for_sticky_ban(address, computer_id)
- if(islist(linked_bans))
- var/datum/view_record/stickyban_list_view/SLW = LAZYACCESS(linked_bans, 1)
- if(SLW)
- var/reason = ""
-
- if(SLW.address == address)
- reason += "IP Address Matches; "
- if(SLW.computer_id == computer_id)
- reason += "CID Matches; "
- if(SLW.ckey == ckey)
- reason += "Ckey Matches; "
-
- var/source_id = SLW.linked_stickyban
- var/source_reason = SLW.linked_reason
- var/source_ckey = SLW.linked_ckey
- if(!source_id)
- source_id = "[SLW.entry_id]"
- source_reason = SLW.reason
- source_ckey = SLW.ckey
-
- log_access("Failed Login: [ckey] [last_known_cid] [last_known_ip] - Stickybanned (Linked to [source_ckey]; Reason: [source_reason])")
- message_admins("Failed Login: [ckey] (IP: [last_known_ip], CID: [last_known_cid]) - Stickybanned (Linked to ckey [source_ckey]; Reason: [source_reason])")
-
- DB_FILTER(/datum/entity/player_sticky_ban,
- DB_AND(
- DB_COMP("ckey", DB_EQUALS, ckey),
- DB_COMP("address", DB_EQUALS, address),
- DB_COMP("computer_id", DB_EQUALS, computer_id)
- ), CALLBACK(src, PROC_REF(process_stickyban), address, computer_id, source_id, reason, null))
-
- .["desc"] = "\nReason: Stickybanned\nExpires: PERMANENT"
- .["reason"] = "ckey/id"
- return .
+ var/list/datum/view_record/stickyban/all_stickies = SSstickyban.check_for_sticky_ban(ckey, address, computer_id)
+
+ if(length(all_stickies))
+ var/datum/view_record/stickyban/sticky = all_stickies[1]
+
+ if(!is_telemetry)
+ log_access("Failed Login: [ckey] [last_known_cid] [last_known_ip] - Stickybanned (Reason: [sticky.reason])")
+ message_admins("Failed Login: [ckey] (IP: [last_known_ip], CID: [last_known_cid]) - Stickybanned (Reason: [sticky.reason])")
+
+ var/appeal
+ if(CONFIG_GET(string/banappeals))
+ appeal = "\nFor more information on your ban, or to appeal, head to [CONFIG_GET(string/banappeals)]"
+
+ .["desc"] = "\nReason: Stickybanned - [sticky.message] Identifier: [sticky.identifier]\n[appeal]"
+ .["reason"] = "ckey/id"
+
+ if(!is_telemetry)
+ SSstickyban.match_sticky(sticky.id, ckey, address, computer_id)
+ return
+
if(!is_time_banned && !is_permabanned)
return null
+
var/appeal
if(CONFIG_GET(string/banappeals))
appeal = "\nFor more information on your ban, or to appeal, head to [CONFIG_GET(string/banappeals)]"
if(is_permabanned)
- permaban_admin.sync()
- log_access("Failed Login: [ckey] [last_known_cid] [last_known_ip] - Banned [permaban_reason]")
- message_admins("Failed Login: [ckey] id:[last_known_cid] ip:[last_known_ip] - Banned [permaban_reason]")
- .["desc"] = "\nReason: [permaban_reason]\nExpires: PERMANENT\nBy: [permaban_admin.ckey][appeal]"
+ var/banner = "Host"
+ if(permaban_admin_id)
+ var/datum/view_record/players/banning_admin = locate() in DB_VIEW(/datum/view_record/players, DB_COMP("id", DB_EQUALS, permaban_admin_id))
+ banner = banning_admin.ckey
+ if(!is_telemetry)
+ log_access("Failed Login: [ckey] [last_known_cid] [last_known_ip] - Banned [permaban_reason]")
+ message_admins("Failed Login: [ckey] id:[last_known_cid] ip:[last_known_ip] - Banned [permaban_reason]")
+ .["desc"] = "\nReason: [permaban_reason]\nExpires: PERMANENT\nBy: [banner][appeal]"
.["reason"] = "ckey/id"
return .
if(is_time_banned)
@@ -509,8 +530,9 @@ BSQL_PROTECT_DATUM(/datum/entity/player)
timeleftstring = "[round(time_left / 60, 0.1)] Hours"
else
timeleftstring = "[time_left] Minutes"
- log_access("Failed Login: [ckey] [last_known_cid] [last_known_ip] - Banned [time_ban_reason]")
- message_admins("Failed Login: [ckey] id:[last_known_cid] ip:[last_known_ip] - Banned [time_ban_reason]")
+ if(!is_telemetry)
+ log_access("Failed Login: [ckey] [last_known_cid] [last_known_ip] - Banned [time_ban_reason]")
+ message_admins("Failed Login: [ckey] id:[last_known_cid] ip:[last_known_ip] - Banned [time_ban_reason]")
.["desc"] = "\nReason: [time_ban_reason]\nExpires: [timeleftstring]\nBy: [time_ban_admin.ckey][appeal]"
.["reason"] = "ckey/id"
return .
@@ -681,7 +703,6 @@ BSQL_PROTECT_DATUM(/datum/entity/player)
parent_entity = /datum/entity/player
child_entity = /datum/entity/player
child_field = "permaban_admin_id"
-
parent_name = "permabanning_admin"
/datum/view_record/players
diff --git a/code/datums/entities/player_sticky_ban.dm b/code/datums/entities/player_sticky_ban.dm
index d79befddb04e..752334e8e001 100644
--- a/code/datums/entities/player_sticky_ban.dm
+++ b/code/datums/entities/player_sticky_ban.dm
@@ -1,94 +1,133 @@
-/datum/entity/player_sticky_ban
- var/player_id
- var/admin_id
+BSQL_PROTECT_DATUM(/datum/entity/stickyban)
+
+/datum/entity/stickyban
+ var/identifier
var/reason
+ var/message
var/date
- var/ckey
- var/address
- var/computer_id
-
- var/linked_stickyban
-
-BSQL_PROTECT_DATUM(/datum/entity/player_sticky_ban)
+ var/active = TRUE
+ var/adminid
-/datum/entity_meta/player_sticky_ban
- entity_type = /datum/entity/player_sticky_ban
- table_name = "player_sticky_bans"
+/datum/entity_meta/stickyban
+ entity_type = /datum/entity/stickyban
+ table_name = "stickyban"
field_types = list(
- "player_id"=DB_FIELDTYPE_BIGINT,
- "admin_id"=DB_FIELDTYPE_BIGINT,
- "reason"=DB_FIELDTYPE_STRING_MAX,
- "date"=DB_FIELDTYPE_STRING_LARGE,
- "address"=DB_FIELDTYPE_STRING_LARGE,
- "ckey" = DB_FIELDTYPE_STRING_LARGE,
- "computer_id"=DB_FIELDTYPE_STRING_LARGE,
- "linked_stickyban"=DB_FIELDTYPE_BIGINT,
+ "identifier" = DB_FIELDTYPE_STRING_LARGE,
+ "reason" = DB_FIELDTYPE_STRING_LARGE,
+ "message" = DB_FIELDTYPE_STRING_LARGE,
+ "date" = DB_FIELDTYPE_STRING_LARGE,
+ "active" = DB_FIELDTYPE_INT,
+ "adminid" = DB_FIELDTYPE_BIGINT,
)
+/datum/view_record/stickyban
+ var/id
+ var/identifier
+ var/reason
+ var/message
+ var/date
+ var/active
+ var/admin
-/datum/entity_link/linked_sticky_bans
- parent_entity = /datum/entity/player_sticky_ban
- child_entity = /datum/entity/player_sticky_ban
- child_field = "linked_stickyban"
-
- parent_name = "linked_ban"
- child_name = "linked_bans"
-
-/datum/entity_link/player_to_player_sticky_bans
- parent_entity = /datum/entity/player
- child_entity = /datum/entity/player_sticky_ban
- child_field = "player_id"
-
- parent_name = "player"
- child_name = "stickybans"
+/datum/entity_view_meta/stickyban
+ root_record_type = /datum/entity/stickyban
+ destination_entity = /datum/view_record/stickyban
+ fields = list(
+ "id",
+ "identifier",
+ "reason",
+ "message",
+ "date",
+ "active",
+ "admin" = DB_CASE(DB_COMP("adminid", DB_ISNOT), "stickybanning_admin.ckey", DB_CONST("AdminBot"))
+ )
-/datum/entity_link/admin_to_player_sticky_bans
+/datum/entity_link/stickyban_to_banning_admin
parent_entity = /datum/entity/player
- child_entity = /datum/entity/player_sticky_ban
- child_field = "admin_id"
+ child_entity = /datum/entity/stickyban
+ child_field = "adminid"
+ parent_name = "stickybanning_admin"
- parent_name = "admin"
+/datum/entity/stickyban_matched_ckey
+ var/ckey
+ var/linked_stickyban
+ var/whitelisted = FALSE
-/datum/view_record/stickyban_list_view
- var/entry_id
- var/player_id
- var/admin_id
+/datum/entity_meta/stickyban_matched_ckey
+ entity_type = /datum/entity/stickyban_matched_ckey
+ table_name = "stickyban_matched_ckey"
+ field_types = list(
+ "ckey" = DB_FIELDTYPE_STRING_LARGE,
+ "linked_stickyban" = DB_FIELDTYPE_BIGINT,
+ "whitelisted" = DB_FIELDTYPE_INT,
+ )
- var/reason
- var/date
- var/address
- var/computer_id
+/datum/view_record/stickyban_matched_ckey
+ var/id
var/ckey
+ var/linked_stickyban
var/whitelisted
+/datum/entity_view_meta/stickyban_matched_ckey
+ root_record_type = /datum/entity/stickyban_matched_ckey
+ destination_entity = /datum/view_record/stickyban_matched_ckey
+ fields = list(
+ "id",
+ "ckey",
+ "linked_stickyban",
+ "whitelisted",
+ )
+
+
+/datum/entity/stickyban_matched_cid
+ var/cid
var/linked_stickyban
- var/linked_ckey
- var/linked_reason
- var/admin_ckey
- var/linked_admin_ckey
+/datum/entity_meta/stickyban_matched_cid
+ entity_type = /datum/entity/stickyban_matched_cid
+ table_name = "stickyban_matched_cid"
+ field_types = list(
+ "cid" = DB_FIELDTYPE_STRING_LARGE,
+ "linked_stickyban" = DB_FIELDTYPE_BIGINT,
+ )
+/datum/view_record/stickyban_matched_cid
+ var/id
+ var/cid
+ var/linked_stickyban
-/datum/entity_view_meta/stickyban_list_view
- root_record_type = /datum/entity/player_sticky_ban
- destination_entity = /datum/view_record/stickyban_list_view
+/datum/entity_view_meta/stickyban_matched_cid
+ root_record_type = /datum/entity/stickyban_matched_cid
+ destination_entity = /datum/view_record/stickyban_matched_cid
fields = list(
- "entry_id" = "id",
- "player_id",
- "admin_id",
+ "id",
+ "cid",
+ "linked_stickyban",
+ )
- "reason",
- "date",
- "address",
- "computer_id",
- "ckey" = "player.ckey",
- "whitelisted" = "player.stickyban_whitelisted",
- "linked_stickyban",
- "linked_ckey" = "linked_ban.player.ckey",
- "linked_reason" = "linked_ban.reason",
+/datum/entity/stickyban_matched_ip
+ var/ip
+ var/linked_stickyban
- "admin_ckey" = "admin.ckey",
- "linked_admin_ckey" = "linked_ban.admin.ckey"
+/datum/entity_meta/stickyban_matched_ip
+ entity_type = /datum/entity/stickyban_matched_ip
+ table_name = "stickyban_matched_ip"
+ field_types = list(
+ "ip" = DB_FIELDTYPE_STRING_LARGE,
+ "linked_stickyban" = DB_FIELDTYPE_BIGINT,
+ )
+
+/datum/view_record/stickyban_matched_ip
+ var/id
+ var/ip
+ var/linked_stickyban
+
+/datum/entity_view_meta/stickyban_matched_ip
+ root_record_type = /datum/entity/stickyban_matched_ip
+ destination_entity = /datum/view_record/stickyban_matched_ip
+ fields = list(
+ "id",
+ "ip",
+ "linked_stickyban",
)
- order_by = list("entry_id" = DB_ORDER_BY_DESC)
diff --git a/code/datums/keybinding/xenomorph.dm b/code/datums/keybinding/xenomorph.dm
index a3996b4b2a1b..64acd876b49f 100644
--- a/code/datums/keybinding/xenomorph.dm
+++ b/code/datums/keybinding/xenomorph.dm
@@ -206,3 +206,18 @@
name = "evolve"
full_name = "Evolve"
keybind_signal = COMSIG_KB_XENO_EVOLVE
+
+/datum/keybinding/xenomorph/purchase_strain
+ hotkey_keys = list("Unbound")
+ classic_keys = list("Unbound")
+ name = "purchase_strain"
+ full_name = "Purchase Strain"
+ keybind_signal = COMSIG_KB_XENO_PURCHASE_STRAIN
+
+/datum/keybinding/xenomorph/purchase_strain/down(client/user)
+ . = ..()
+ if(.)
+ return
+
+ var/mob/living/carbon/xenomorph/current_xeno = user?.mob
+ current_xeno.purchase_strain()
diff --git a/code/datums/tutorial/_tutorial_menu.dm b/code/datums/tutorial/_tutorial_menu.dm
index 951b9654ef0e..3c7a28e77b92 100644
--- a/code/datums/tutorial/_tutorial_menu.dm
+++ b/code/datums/tutorial/_tutorial_menu.dm
@@ -71,7 +71,7 @@
path = text2path(params["tutorial_path"])
- if(!path || !isnewplayer(usr))
+ if(!ispath(path, /datum/tutorial) || !isnewplayer(usr))
return
if(HAS_TRAIT(usr, TRAIT_IN_TUTORIAL) || istype(get_area(usr), /area/misc/tutorial))
diff --git a/code/datums/tutorial/xenomorph/xenomorph_basic.dm b/code/datums/tutorial/xenomorph/xenomorph_basic.dm
index e91c85e1e1e4..965f7b55d3c0 100644
--- a/code/datums/tutorial/xenomorph/xenomorph_basic.dm
+++ b/code/datums/tutorial/xenomorph/xenomorph_basic.dm
@@ -21,6 +21,7 @@
xeno.plasma_max = 0
xeno.melee_damage_lower = 40
xeno.melee_damage_upper = 40
+ xeno.lock_evolve = TRUE
message_to_player("Welcome to the Xenomorph basic tutorial. You are [xeno.name], a drone, the workhorse of the hive.")
diff --git a/code/datums/xeno_shields/shield_types/vanguard_shield.dm b/code/datums/xeno_shields/shield_types/vanguard_shield.dm
index 5b9eebc04ab8..21d9fb12cfd7 100644
--- a/code/datums/xeno_shields/shield_types/vanguard_shield.dm
+++ b/code/datums/xeno_shields/shield_types/vanguard_shield.dm
@@ -50,7 +50,6 @@
if (!istype(linked_xeno))
return
- if (linked_xeno.mutation_type == PRAETORIAN_VANGUARD)
- var/datum/behavior_delegate/praetorian_vanguard/BD = linked_xeno.behavior_delegate
- if (istype(BD))
- BD.last_combat_time = world.time
+ var/datum/behavior_delegate/praetorian_vanguard/behavior = linked_xeno.behavior_delegate
+ if (istype(behavior))
+ behavior.last_combat_time = world.time
diff --git a/code/defines/procs/admin.dm b/code/defines/procs/admin.dm
index 1e4f02e95cc0..d48c02127ea9 100644
--- a/code/defines/procs/admin.dm
+++ b/code/defines/procs/admin.dm
@@ -1,3 +1,15 @@
-/proc/log_and_message_admins(message as text)
- log_admin("[key_name(usr)] [message]")
- message_admins("[key_name(usr)] [message]")
+/proc/important_message_external(message, title, list/datum/tgs_chat_embed/field/fields)
+ if(CONFIG_GET(string/important_log_channel))
+ var/datum/tgs_message_content/to_send = new("")
+
+ var/datum/tgs_chat_embed/structure/embed = new()
+ embed.title = title ? title : "Important Log"
+ embed.description = message
+ embed.timestamp = time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")
+ embed.colour = "#ED2939"
+ if(length(fields))
+ embed.fields = fields
+
+ to_send.embed = embed
+
+ send2chat(to_send, CONFIG_GET(string/important_log_channel))
diff --git a/code/game/machinery/medical_pod/sleeper.dm b/code/game/machinery/medical_pod/sleeper.dm
index 84ef2f579ba1..5868df901c6a 100644
--- a/code/game/machinery/medical_pod/sleeper.dm
+++ b/code/game/machinery/medical_pod/sleeper.dm
@@ -208,6 +208,14 @@
var/amount = text2num(params["amount"])
if(!length(chemical) || amount <= 0)
return
+ if(!(amount in connected.amounts))
+ log_debug("[amount] is an invalid amount to inject in [src]!")
+ return
+ if(!(chemical in connected.available_chemicals))
+ log_debug("[chemical] is not available to inject in [src]!")
+ return
+ if(connected.occupant.reagents && connected.occupant.reagents.get_reagent_amount(chemical) + amount > connected.max_chem)
+ return
if(connected.occupant.health > connected.min_health || (chemical in connected.emergency_chems))
connected.inject_chemical(usr, chemical, amount)
else
diff --git a/code/game/machinery/vending/cm_vending.dm b/code/game/machinery/vending/cm_vending.dm
index 2ba1ac089826..4f825030d627 100644
--- a/code/game/machinery/vending/cm_vending.dm
+++ b/code/game/machinery/vending/cm_vending.dm
@@ -900,7 +900,7 @@ GLOBAL_LIST_EMPTY(vending_products)
for(var/list/vendspec in listed_products)
var/multiplier = vendspec[2]
if(multiplier > 0)
- var/awarded = round(vendspec[2] * scale) // Starting amount
+ var/awarded = round(vendspec[2] * scale, 1) // Starting amount
//Record the multiplier and how many have actually been given out
dynamic_stock_multipliers[vendspec] = list(vendspec[2], awarded)
vendspec[2] = awarded // Override starting amount
@@ -920,8 +920,8 @@ GLOBAL_LIST_EMPTY(vending_products)
var/list/metadata = dynamic_stock_multipliers[vendspec]
var/multiplier = metadata[1] // How much do we multiply scales by
var/previous_max_amount = metadata[2] // How many we already handed out at old scale
- var/projected_max_amount = round(new_scale * multiplier) // How much we would have had total now in total
- var/amount_to_add = round(projected_max_amount - previous_max_amount) // Rounding just in case
+ var/projected_max_amount = round(new_scale * multiplier, 1) // How much we would have had total now in total
+ var/amount_to_add = round(projected_max_amount - previous_max_amount, 1) // Rounding just in case
if(amount_to_add > 0)
metadata[2] += amount_to_add
vendspec[2] += amount_to_add
diff --git a/code/game/objects/items/devices/motion_detector.dm b/code/game/objects/items/devices/motion_detector.dm
index c26db692f082..7db2825deedf 100644
--- a/code/game/objects/items/devices/motion_detector.dm
+++ b/code/game/objects/items/devices/motion_detector.dm
@@ -191,6 +191,14 @@
if(ishuman(A.loc))
return A.loc
+/obj/item/device/motiondetector/xm4
+
+///Forces the blue blip to appear around the detected mob
+/obj/item/device/motiondetector/xm4/get_user()
+ var/atom/holder = loc
+ if(ishuman(holder.loc))
+ return holder.loc
+
/obj/item/device/motiondetector/proc/apply_debuff(mob/M)
return
diff --git a/code/game/objects/items/devices/portable_vendor.dm b/code/game/objects/items/devices/portable_vendor.dm
index da399192b713..8e7c8df1d9a7 100644
--- a/code/game/objects/items/devices/portable_vendor.dm
+++ b/code/game/objects/items/devices/portable_vendor.dm
@@ -267,8 +267,11 @@
req_role = JOB_CORPORATE_LIAISON
listed_products = list(
list("INCENTIVES", 0, null, null, null),
+ list("Corporate Security Bodyguard", 50, /obj/item/handheld_distress_beacon/bodyguard, "white", "A beacon which sends the Corporate Security Division an encoded message informing them of your request for a Corporate Security Bodyguard."),
+ list("Corporate Lawyer Team", 50, /obj/item/handheld_distress_beacon/lawyer, "white", "A beacon which sends the Corporate Affairs Division an encoded message informing them of your request for a Corporate Lawyer, required when a contract signee breaks one of their clauses."),
list("Neurostimulator Implant", 30, /obj/item/implanter/neurostim, "white", "Implant which regulates nociception and sensory function. Benefits include pain reduction, improved balance, and improved resistance to overstimulation and disorientation. To encourage compliance, negative stimulus is applied if the implant hears a (non-radio) spoken codephrase. Implant will be degraded by the body's immune system over time, and thus malfunction with gradually increasing frequency. Personal use not recommended."),
list("Ultrazine Injector", 25, /obj/item/reagent_container/hypospray/autoinjector/ultrazine/liaison, "white", "Highly-addictive stimulant. Enhances short-term physical performance, particularly running speed. Effects last approximately 10 minutes per injection. More than two injections at a time will result in overdose. Withdrawal causes extreme discomfort and hallucinations. Long-term use results in halluciations and organ failure. Conditional distribution secures subject compliance. Not for personal use."),
+ list("Cyanide Pill", 20, /obj/item/reagent_container/pill/cyanide, "white", "A cyanide pill, also known as a suicide pill. For the easy way out."),
list("Ceramic Plate", 10, /obj/item/trash/ceramic_plate, "white", "A ceramic plate, useful in a variety of situations."),
list("Cash", 5, /obj/item/spacecash/c1000/counterfeit, "white", "$1000 USD, unmarked bills"),
list("WY Encryption Key", 5, /obj/item/device/encryptionkey/WY, "white", "WY private comms encryption key, for conducting private business."),
@@ -282,6 +285,7 @@
list("Sake", 5, /obj/item/reagent_container/food/drinks/bottle/sake, "white", "Weyland-Yutani Sake, for a proper business dinner."),
list("Beer", 5, /obj/item/reagent_container/food/drinks/cans/aspen, "white", "Weyland-Yutani Aspen Beer, for a more casual night."),
list("Drinking Glass", 1, /obj/item/reagent_container/food/drinks/drinkingglass, "white", "A Drinking Glass, because you have class."),
+ list("Weyland-Yutani Coffee Mug", 1, /obj/item/reagent_container/food/drinks/coffeecup/wy, "white", "A Weyland-Yutani coffee mug, for any Marines who want a Company souvenir."),
list("STATIONARY", 0, null, null, null),
list("WY pen, black", 1, /obj/item/tool/pen/clicky, "white", "A WY pen, for writing formally on the go."),
diff --git a/code/game/objects/items/handheld_distress_beacon.dm b/code/game/objects/items/handheld_distress_beacon.dm
index c11a7a57c350..91d6a6aa945f 100644
--- a/code/game/objects/items/handheld_distress_beacon.dm
+++ b/code/game/objects/items/handheld_distress_beacon.dm
@@ -64,3 +64,23 @@
recipient = "Anchorpoint Station"
ert_full_name = list("CMB - Patrol Team - Marshals in Distress (Friendly)", "CMB - Anchorpoint Station Colonial Marine QRF (Friendly)")
ert_short_name = list("SEND CMB", "SEND QRF")
+
+// Corporate Lawyer beacon available for 50 points at the CLs briefcase
+/obj/item/handheld_distress_beacon/lawyer
+ name = "\improper Corporate Affairs Division handheld beacon"
+ desc = "An encoded beacon. This one is branded with the Weyland-Yutani slogan, 'Building Better Worlds since 2099'. Etched in stencil on the side is 'FOR CONTRACT BREAKERS ONLY'. This one is branded with the Corporate Affairs Division symbol and provided to most Executives situated in UA or TWE space."
+
+ beacon_type = "Lawyer beacon"
+ recipient = "the Corporate Affairs Division"
+ ert_full_name = list("Lawyers - Corporate")
+ ert_short_name = list("SEND LAWYERS")
+
+// Corporate Security Bodyguard beacon available for 50 points at the CLs briefcase
+/obj/item/handheld_distress_beacon/bodyguard
+ name = "\improper Corporate Security Division handheld beacon"
+ desc = "An standard Corporate Security beacon. This one is branded with the Weyland-Yutani slogan, 'Building Better Worlds since 2099'. This one is branded with the Corporate Security Division symbol and provided to Executives stationed in very dangerous sites across the entire Galaxy."
+
+ beacon_type = "Bodyguard beacon"
+ recipient = "the Corporate Security Division"
+ ert_full_name = list("Weyland-Yutani Goon (Executive Bodyguard Detail)")
+ ert_short_name = list("SEND BODYGUARD")
diff --git a/code/game/world.dm b/code/game/world.dm
index 627e245bc4c1..2b7dacee373a 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -125,7 +125,7 @@ GLOBAL_LIST_INIT(reboot_sfx, file2list("config/reboot_sfx.txt"))
GLOB.world_runtime_log = "[GLOB.log_directory]/runtime.log"
GLOB.round_stats = "[GLOB.log_directory]/round_stats.log"
GLOB.scheduler_stats = "[GLOB.log_directory]/round_scheduler_stats.log"
- GLOB.mutator_logs = "[GLOB.log_directory]/mutator_logs.log"
+ GLOB.strain_logs = "[GLOB.log_directory]/strain_logs.log"
start_log(GLOB.tgui_log)
start_log(GLOB.world_href_log)
@@ -134,7 +134,7 @@ GLOBAL_LIST_INIT(reboot_sfx, file2list("config/reboot_sfx.txt"))
start_log(GLOB.world_runtime_log)
start_log(GLOB.round_stats)
start_log(GLOB.scheduler_stats)
- start_log(GLOB.mutator_logs)
+ start_log(GLOB.strain_logs)
if(fexists(GLOB.config_error_log))
fcopy(GLOB.config_error_log, "[GLOB.log_directory]/config_error.log")
diff --git a/code/modules/admin/IsBanned.dm b/code/modules/admin/IsBanned.dm
index 4a7307b247a5..85a1028c2296 100644
--- a/code/modules/admin/IsBanned.dm
+++ b/code/modules/admin/IsBanned.dm
@@ -1,6 +1,6 @@
#ifndef OVERRIDE_BAN_SYSTEM
//Blocks an attempt to connect before even creating our client datum thing.
-/world/IsBanned(key,address,computer_id, type, real_bans_only=FALSE)
+/world/IsBanned(key,address,computer_id, type, real_bans_only=FALSE, is_telemetry = FALSE)
var/ckey = ckey(key)
// This is added siliently. Thanks to MSO for this fix. You will see it when/if we go OS
@@ -28,11 +28,7 @@
var/datum/entity/player/P = get_player_from_key(ckey)
- . = P.check_ban(computer_id, address)
- if(.)
- return .
-
- return ..() //default pager ban stuff
+ . = P.check_ban(computer_id, address, is_telemetry)
#endif
diff --git a/code/modules/admin/NewBan.dm b/code/modules/admin/NewBan.dm
index 7dca354129ff..f4394738fb2d 100644
--- a/code/modules/admin/NewBan.dm
+++ b/code/modules/admin/NewBan.dm
@@ -180,15 +180,44 @@ GLOBAL_DATUM(Banlist, /savefile)
expiry = "Removal Pending"
else
expiry = "Permaban"
- var/unban_link = "(U)"
+ var/unban_link
+ if(ban.is_permabanned)
+ unban_link = "(UP)"
+ else
+ unban_link = "(UT)"
- dat += "
[unban_link] Key: [ban.ckey] | ComputerID: [ban.last_known_cid] | IP: [ban.last_known_ip] | [expiry] | (By: [ban.admin]) | (Reason: [ban.reason]) |
"
+ dat += "[unban_link] Key: [ban.ckey] | ComputerID: [ban.last_known_cid] | IP: [ban.last_known_ip] | [expiry] | (By: [ban.admin ? ban.admin : "AdminBot"]) | (Reason: [ban.reason]) |
"
dat += ""
- var/dat_header = "
Bans: (U) = Unban"
+ var/dat_header = "
Bans: (UP) = Unban Perma (UT) = Unban Timed"
dat_header += " - Ban Listing
[dat]"
show_browser(usr, dat_header, "Unban Panel", "unbanp", "size=875x400")
+/datum/admins/proc/stickypanel()
+ var/add_sticky = "Add Sticky Ban"
+ var/find_sticky = "Find Sticky Ban"
+
+ var/data = "
Sticky Bans: [add_sticky] [find_sticky] "
+
+ var/list/datum/view_record/stickyban/stickies = DB_VIEW(/datum/view_record/stickyban,
+ DB_COMP("active", DB_EQUALS, TRUE)
+ )
+
+ for(var/datum/view_record/stickyban/current_sticky in stickies)
+ var/whitelist_link = "(WHITELIST)"
+ var/remove_sticky_link = "(REMOVE)"
+ var/add_to_sticky_link = "(ADD)"
+
+ var/impacted_ckey_link = "CKEYs"
+ var/impacted_ip_link = "IPs"
+ var/impacted_cid_link = "CIDs"
+
+ data += "[whitelist_link][remove_sticky_link][add_to_sticky_link] | Identifier: [current_sticky.identifier] | Reason: [current_sticky.reason] | Message: [current_sticky.message] | Admin: [current_sticky.admin] | View: [impacted_ckey_link][impacted_ip_link][impacted_cid_link] |
"
+
+ data += "
"
+
+ show_browser(owner, data, "Stickyban Panel", "sticky", "size=875x400")
+
//////////////////////////////////// DEBUG ////////////////////////////////////
/proc/CreateBans()
@@ -251,3 +280,48 @@ GLOBAL_DATUM(Banlist, /savefile)
if(P.is_time_banned && alert(usr, "Ban already exists. Proceed?", "Confirmation", "Yes", "No") != "Yes")
return
P.add_timed_ban(reason, mins)
+
+/client/proc/cmd_admin_do_stickyban(identifier, reason, message, list/impacted_ckeys, list/impacted_cids, list/impacted_ips)
+ if(!identifier)
+ identifier = tgui_input_text(src, "Name of the primary CKEY you are adding a stickyban to.", "BuildABan")
+ if(!identifier)
+ return
+
+ if(!message)
+ message = tgui_input_text(src, "What message should be given to the impacted users?", "BuildABan", encode = FALSE)
+ if(!message)
+ return
+
+ if(!reason)
+ reason = tgui_input_text(src, "What's the reason for the ban? This is shown internally, and not displayed in public notes and ban messages. Include as much detail as necessary.", "BuildABan", multiline = TRUE, encode = FALSE)
+ if(!reason)
+ return
+
+ if(!length(impacted_ckeys))
+ impacted_ckeys = splittext(tgui_input_text(src, "Which CKEYs should be impacted by this ban? Include the primary ckey, separated by semicolons.", "BuildABan", "player1;player2;player3"), ";")
+
+ if(!length(impacted_cids))
+ impacted_cids = splittext(tgui_input_text(src, "Which CIDs should be impacted by this ban? Separate with semicolons.", "BuildABan", "12345678;87654321"), ";")
+
+ if(!length(impacted_ips))
+ impacted_ips = splittext(tgui_input_text(src, "Which IPs should be impacted by this ban? Separate with semicolons.", "BuildABan", "1.1.1.1;8.8.8.8"), ";")
+
+ var/datum/entity/stickyban/new_sticky = SSstickyban.add_stickyban(identifier, reason, message, player_data)
+
+ if(!new_sticky)
+ to_chat(src, SPAN_ADMIN("Failed to apply stickyban."))
+ return
+
+ for(var/ckey in impacted_ckeys)
+ SSstickyban.add_matched_ckey(new_sticky.id, ckey)
+
+ for(var/cid in impacted_cids)
+ SSstickyban.add_matched_cid(new_sticky.id, cid)
+
+ for(var/ip in impacted_ips)
+ SSstickyban.add_matched_ip(new_sticky.id, ip)
+
+ log_admin("STICKYBAN: Identifier: [identifier] Reason: [reason] Message: [message] CKEYs: [english_list(impacted_ckeys)] IPs: [english_list(impacted_ips)] CIDs: [english_list(impacted_cids)]")
+ message_admins("[key_name_admin(src)] has added a new stickyban with the identifier '[identifier]'.")
+ var/datum/tgs_chat_embed/field/reason_embed = new("Stickyban Reason", reason)
+ important_message_external("[src] has added a new stickyban with the identifier '[identifier]'.", "Stickyban Placed", list(reason_embed))
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 43b06b6d80bc..f3eae1447ba0 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -96,8 +96,9 @@ GLOBAL_LIST_INIT(admin_verbs_admin, list(
))
GLOBAL_LIST_INIT(admin_verbs_ban, list(
- /client/proc/unban_panel
- // /client/proc/jobbans // Disabled temporarily due to 15-30 second lag spikes. Don't forget the comma in the line above when uncommenting this!
+ /client/proc/unban_panel,
+ /client/proc/stickyban_panel,
+ // /client/proc/jobbans // Disabled temporarily due to 15-30 second lag spikes.
))
GLOBAL_LIST_INIT(admin_verbs_sounds, list(
diff --git a/code/modules/admin/player_panel/actions/punish.dm b/code/modules/admin/player_panel/actions/punish.dm
index 576de30ae7ff..eb80632d8afe 100644
--- a/code/modules/admin/player_panel/actions/punish.dm
+++ b/code/modules/admin/player_panel/actions/punish.dm
@@ -45,6 +45,58 @@
return TRUE
+/datum/player_action/permanent_ban
+ action_tag = "permanent_ban"
+ name = "Permanent Ban"
+ permissions_required = R_BAN
+
+/datum/player_action/permanent_ban/act(client/user, mob/target, list/params)
+ var/reason = tgui_input_text(user, "What message should be given to the permabanned user?", "Permanent Ban", encode = FALSE)
+ if(!reason)
+ return
+
+ var/internal_reason = tgui_input_text(user, "What's the reason for the ban? This is shown internally, and not displayed in public notes and ban messages. Include as much detail as necessary.", "Permanent Ban", multiline = TRUE, encode = FALSE)
+ if(!internal_reason)
+ return
+
+ var/datum/entity/player/target_entity = target.client?.player_data
+ if(!target_entity)
+ target_entity = get_player_from_key(target.ckey || target.persistent_ckey)
+
+ if(!target_entity)
+ return
+
+ if(!target_entity.add_perma_ban(reason, internal_reason, user.player_data))
+ to_chat(user, SPAN_ADMIN("The user is already permabanned! If necessary, you can remove the permaban, and place a new one."))
+
+/datum/player_action/sticky_ban
+ action_tag = "sticky_ban"
+ name = "Sticky Ban"
+ permissions_required = R_BAN
+
+/datum/player_action/sticky_ban/act(client/user, mob/target, list/params)
+ var/datum/entity/player/player = get_player_from_key(target.ckey || target.persistent_ckey)
+ if(!player)
+ return
+
+ var/persistent_ip = target.client?.address || player.last_known_ip
+ var/persistent_cid = target.client?.computer_id || player.last_known_cid
+
+ var/message = tgui_input_text(user, "What message should be given to the impacted users?", "BuildABan", encode = FALSE)
+ if(!message)
+ return
+
+ var/reason = tgui_input_text(user, "What's the reason for the ban? This is shown internally, and not displayed in public notes and ban messages. Include as much detail as necessary.", "BuildABan", multiline = TRUE, encode = FALSE)
+ if(!reason)
+ return
+
+ user.cmd_admin_do_stickyban(target.ckey, reason, message, impacted_ckeys = list(target.ckey), impacted_cids = list(persistent_cid), impacted_ips = list(persistent_ip))
+ player.add_note("Stickybanned | [message]", FALSE, NOTE_ADMIN, TRUE)
+ player.add_note("Internal reason: [reason]", TRUE, NOTE_ADMIN)
+
+ if(target.client)
+ qdel(target.client)
+
/datum/player_action/mute
action_tag = "mob_mute"
name = "Mute"
diff --git a/code/modules/admin/stickyban.dm b/code/modules/admin/stickyban.dm
deleted file mode 100644
index 69793a599596..000000000000
--- a/code/modules/admin/stickyban.dm
+++ /dev/null
@@ -1,66 +0,0 @@
-// BLOCKING PROC, RUN ASYNC
-
-/proc/stickyban_internal(ckey, address, cid, reason, linked_stickyban, datum/entity/player/banning_admin)
-
- if(!ckey || !address || !cid)
- CRASH("Incorrect data passed to stickyban_internal ([ckey], [address], [cid])")
-
- var/datum/entity/player/P = get_player_from_key(ckey)
-
- if(!P)
- message_admins("Tried stickybanning ckey \"[ckey]\", player entity was unable to be found. Please try again later.")
- return
-
- var/datum/entity/player_sticky_ban/PSB = DB_ENTITY(/datum/entity/player_sticky_ban)
- PSB.player_id = P.id
- if(reason)
- PSB.reason = reason
- PSB.address = address
- PSB.computer_id = cid
- PSB.ckey = P.ckey
- PSB.date = "[time2text(world.realtime, "YYYY-MM-DD hh:mm:ss")]"
-
- if(linked_stickyban)
- PSB.linked_stickyban = linked_stickyban
-
- if(!reason)
- reason = "No reason given."
-
- if(banning_admin)
- PSB.admin_id = banning_admin.id
- if(banning_admin.owning_client)
- message_admins("[banning_admin.owning_client.ckey] has stickybanned [ckey].")
-
- message_admins("[ckey] (IP: [address], CID: [cid]) has been stickybanned for: \"[reason]\".")
-
- if(P.owning_client)
- to_chat_forced(P.owning_client, SPAN_WARNING("You have been sticky banned by [banning_admin? banning_admin.ckey : "Host"].\nReason: [sanitize(reason)]."))
- to_chat_forced(P.owning_client, SPAN_WARNING("This is a permanent ban"))
- QDEL_NULL(P.owning_client)
-
- PSB.save()
-
-/datum/entity/player/proc/process_stickyban(address, computer_id, source_id, reason, datum/entity/player/banning_admin, list/PSB)
- if(length(PSB) > 0) // sticky ban with identical data already exists, no need for another copy
- if(banning_admin)
- to_chat(banning_admin, SPAN_WARNING("Failed to add stickyban to [ckey]. Reason: Stickyban already exists."))
- return
-
- stickyban_internal(ckey, address, computer_id, reason, source_id, banning_admin)
-
-
-/datum/entity/player/proc/check_for_sticky_ban(address, computer_id)
- var/list/datum/view_record/stickyban_list_view/SBLW = DB_VIEW(/datum/view_record/stickyban_list_view,
- DB_OR(
- DB_COMP("ckey", DB_EQUALS, ckey),
- DB_COMP("address", DB_EQUALS, address),
- DB_COMP("computer_id", DB_EQUALS, computer_id)
- ))
-
- if(length(SBLW) == 0)
- return
-
- if(stickyban_whitelisted)
- return
-
- return SBLW
diff --git a/code/modules/admin/tabs/admin_tab.dm b/code/modules/admin/tabs/admin_tab.dm
index 8dce41ac8235..356762b5edd7 100644
--- a/code/modules/admin/tabs/admin_tab.dm
+++ b/code/modules/admin/tabs/admin_tab.dm
@@ -53,6 +53,12 @@
admin_holder.unbanpanel()
return
+/client/proc/stickyban_panel()
+ set name = "Stickyban Panel"
+ set category = "Admin.Panels"
+
+ admin_holder?.stickypanel()
+
/client/proc/player_panel_new()
set name = "Player Panel"
set category = "Admin.Panels"
diff --git a/code/modules/admin/topic/topic.dm b/code/modules/admin/topic/topic.dm
index 825f7c3cdbf4..ecef2627ed3c 100644
--- a/code/modules/admin/topic/topic.dm
+++ b/code/modules/admin/topic/topic.dm
@@ -229,6 +229,200 @@
alert(usr, "This ban has already been lifted / does not exist.", "Error", "Ok")
unbanpanel()
+ else if(href_list["unban_perma"])
+ var/datum/entity/player/unban_player = get_player_from_key(href_list["unban_perma"])
+ if(!(tgui_alert(owner, "Do you want to unban [unban_player.ckey]? They are currently permabanned for: [unban_player.permaban_reason], since [unban_player.permaban_date].", "Unban Player", list("Yes", "No")) == "Yes"))
+ return
+
+ if(!unban_player.is_permabanned)
+ to_chat(owner, "The player is not currently permabanned.")
+
+ unban_player.is_permabanned = FALSE
+ unban_player.permaban_admin_id = null
+ unban_player.permaban_date = null
+ unban_player.permaban_reason = null
+
+ unban_player.save()
+
+ message_admins("[key_name_admin(owner)] has removed the permanent ban on [unban_player.ckey].")
+ important_message_external("[owner] has removed the permanent ban on [unban_player.ckey].", "Permaban Removed")
+
+ else if(href_list["sticky"])
+ if(href_list["view_all_ckeys"])
+ var/list/datum/view_record/stickyban_matched_ckey/all_ckeys = DB_VIEW(/datum/view_record/stickyban_matched_ckey,
+ DB_COMP("linked_stickyban", DB_EQUALS, href_list["sticky"])
+ )
+
+ var/list/keys = list()
+ var/list/whitelisted = list()
+ for(var/datum/view_record/stickyban_matched_ckey/match as anything in all_ckeys)
+ if(match.whitelisted)
+ whitelisted += match.ckey
+ else
+ keys += match.ckey
+
+ show_browser(owner, "Impacted: [english_list(keys)]
Whitelisted: [english_list(whitelisted)]", "Stickyban Keys", "stickykeys")
+ return
+
+ if(href_list["view_all_cids"])
+ var/list/datum/view_record/stickyban_matched_cid/all_cids = DB_VIEW(/datum/view_record/stickyban_matched_cid,
+ DB_COMP("linked_stickyban", DB_EQUALS, href_list["sticky"])
+ )
+
+ var/list/cids = list()
+ for(var/datum/view_record/stickyban_matched_cid/match as anything in all_cids)
+ cids += match.cid
+
+ show_browser(owner, english_list(cids), "Stickyban CIDs", "stickycids")
+ return
+
+ if(href_list["view_all_ips"])
+ var/list/datum/view_record/stickyban_matched_ip/all_ips = DB_VIEW(/datum/view_record/stickyban_matched_ip,
+ DB_COMP("linked_stickyban", DB_EQUALS, href_list["sticky"])
+ )
+
+ var/list/ips = list()
+ for(var/datum/view_record/stickyban_matched_ip/match as anything in all_ips)
+ ips += match.ip
+
+ show_browser(owner, english_list(ips), "Stickyban IPs", "stickycips")
+ return
+
+ if(href_list["find_sticky"])
+ var/ckey = ckey(tgui_input_text(owner, "Which CKEY should we attempt to find stickybans for?", "FindABan"))
+ if(!ckey)
+ return
+
+ var/list/datum/view_record/stickyban/stickies = SSstickyban.check_for_sticky_ban(ckey)
+ if(!stickies)
+ to_chat(owner, SPAN_ADMIN("Could not locate any stickbans impacting [ckey]."))
+ return
+
+ var/list/impacting_stickies = list()
+
+ for(var/datum/view_record/stickyban/sticky as anything in stickies)
+ impacting_stickies += sticky.identifier
+
+ to_chat(owner, SPAN_ADMIN("Found the following stickybans for [ckey]: [english_list(impacting_stickies)]"))
+
+ if(!check_rights_for(owner, R_BAN))
+ return
+
+ if(href_list["new_sticky"])
+ owner.cmd_admin_do_stickyban()
+ return
+
+ var/datum/entity/stickyban/sticky = DB_ENTITY(/datum/entity/stickyban, href_list["sticky"])
+ if(!sticky)
+ return
+
+ sticky.sync()
+
+ if(href_list["whitelist_ckey"])
+ var/ckey_to_whitelist = ckey(tgui_input_text(owner, "What CKEY should be whitelisted? Editing stickyban: [sticky.identifier]"))
+ if(!ckey_to_whitelist)
+ return
+
+ SSstickyban.whitelist_ckey(sticky.id, ckey_to_whitelist)
+ message_admins("[key_name_admin(owner)] has whitelisted [ckey_to_whitelist] against stickyban '[sticky.identifier]'.")
+ important_message_external("[owner] has whitelisted [ckey_to_whitelist] against stickyban '[sticky.identifier]'.", "CKEY Whitelisted")
+
+ if(href_list["add"])
+ var/option = tgui_input_list(owner, "What do you want to add?", "AddABan", list("CID", "CKEY", "IP"))
+ if(!option)
+ return
+
+ var/to_add = tgui_input_text(owner, "Provide the [option] to add to the stickyban.", "AddABan")
+ if(!to_add)
+ return
+
+ switch(option)
+ if("CID")
+ SSstickyban.add_matched_cid(sticky.id, to_add)
+ if("CKEY")
+ SSstickyban.add_matched_ckey(sticky.id, to_add)
+ if("IP")
+ SSstickyban.add_matched_ip(sticky.id, to_add)
+
+ message_admins("[key_name_admin(owner)] has added a [option] ([to_add]) to stickyban '[sticky.identifier]'.")
+ important_message_external("[owner] has added a [option] ([to_add]) to stickyban '[sticky.identifier]'.", "[option] Added to Stickyban")
+
+ if(href_list["remove"])
+ var/option = tgui_input_list(owner, "What do you want to remove?", "DelABan", list("Entire Stickyban", "CID", "CKEY", "IP"))
+ switch(option)
+ if("Entire Stickyban")
+ if(!(tgui_alert(owner, "Are you sure you want to remove this stickyban? Identifier: [sticky.identifier] Reason: [sticky.reason]", "Confirm", list("Yes", "No")) == "Yes"))
+ return
+
+ sticky.active = FALSE
+ sticky.save()
+
+ message_admins("[key_name_admin(owner)] has deactivated stickyban '[sticky.identifier]'.")
+ important_message_external("[owner] has deactivated stickyban '[sticky.identifier]'.", "Stickyban Deactivated")
+
+ if("CID")
+ var/list/datum/view_record/stickyban_matched_cid/all_cids = DB_VIEW(/datum/view_record/stickyban_matched_cid,
+ DB_COMP("linked_stickyban", DB_EQUALS, sticky.id)
+ )
+
+ var/list/cid_to_record_id = list()
+ for(var/datum/view_record/stickyban_matched_cid/match in all_cids)
+ cid_to_record_id["[match.cid]"] = match.id
+
+ var/picked = tgui_input_list(owner, "Which CID to remove?", "DelABan", cid_to_record_id)
+ if(!picked)
+ return
+
+ var/selected = cid_to_record_id[picked]
+
+ var/datum/entity/stickyban_matched_cid/sticky_cid = DB_ENTITY(/datum/entity/stickyban_matched_cid, selected)
+ sticky_cid.delete()
+
+ message_admins("[key_name_admin(owner)] has removed a CID ([picked]) from stickyban '[sticky.identifier]'.")
+ important_message_external("[owner] has removed a CID ([picked]) from stickyban '[sticky.identifier]'.", "CID Removed from Stickyban")
+
+ if("CKEY")
+ var/list/datum/view_record/stickyban_matched_ckey/all_ckeys = DB_VIEW(/datum/view_record/stickyban_matched_ckey,
+ DB_COMP("linked_stickyban", DB_EQUALS, sticky.id)
+ )
+
+ var/list/ckey_to_record_id = list()
+ for(var/datum/view_record/stickyban_matched_ckey/match in all_ckeys)
+ ckey_to_record_id["[match.ckey]"] = match.id
+
+ var/picked = tgui_input_list(owner, "Which CKEY to remove?", "DelABan", ckey_to_record_id)
+ if(!picked)
+ return
+
+ var/selected = ckey_to_record_id[picked]
+
+ var/datum/entity/stickyban_matched_ckey/sticky_ckey = DB_ENTITY(/datum/entity/stickyban_matched_ckey, selected)
+ sticky_ckey.delete()
+
+ message_admins("[key_name_admin(owner)] has removed a CKEY ([picked]) from stickyban '[sticky.identifier]'.")
+ important_message_external("[owner] has removed a CKEY ([picked]) from stickyban '[sticky.identifier]'.", "CKEY Removed from Stickyban")
+
+ if("IP")
+ var/list/datum/view_record/stickyban_matched_ip/all_ips = DB_VIEW(/datum/view_record/stickyban_matched_ip,
+ DB_COMP("linked_stickyban", DB_EQUALS, sticky.id)
+ )
+
+ var/list/ip_to_record_id = list()
+ for(var/datum/view_record/stickyban_matched_ip/match in all_ips)
+ ip_to_record_id["[match.ip]"] = match.id
+
+ var/picked = tgui_input_list(owner, "Which IP to remove?", "DelABan", ip_to_record_id)
+ if(!picked)
+ return
+
+ var/selected = ip_to_record_id[picked]
+
+ var/datum/entity/stickyban_matched_ip/sticky_ip = DB_ENTITY(/datum/entity/stickyban_matched_ip, selected)
+ sticky_ip.delete()
+
+ message_admins("[key_name_admin(owner)] has removed an IP ([picked]) from stickyban [sticky.identifier].")
+ important_message_external("[owner] has removed an IP ([picked]) from stickyban '[sticky.identifier].", "IP Removed from Stickyban")
+
else if(href_list["warn"])
usr.client.warn(href_list["warn"])
diff --git a/code/modules/admin/verbs/autoreplace.dm b/code/modules/admin/verbs/autoreplace.dm
index b2fe04cfb4a3..a896c751f5ed 100644
--- a/code/modules/admin/verbs/autoreplace.dm
+++ b/code/modules/admin/verbs/autoreplace.dm
@@ -32,7 +32,7 @@ GLOBAL_LIST_INIT_TYPED(admin_runtime_decorators, /datum/decorator/manual/admin_r
GLOB.admin_runtime_decorators.Add(SSdecorator.add_decorator(/datum/decorator/manual/admin_runtime, types, subtypes, field, value))
- log_and_message_admins("[src] activated new decorator id: [GLOB.admin_runtime_decorators.len] set for [hint_text] `[types]` for field `[field]` set value `[value]`")
+ message_admins("[src] activated new decorator id: [GLOB.admin_runtime_decorators.len] set for [hint_text] `[types]` for field `[field]` set value `[value]`")
/client/proc/deactivate_autoreplacer()
set category = "Admin.Events"
@@ -49,7 +49,7 @@ GLOBAL_LIST_INIT_TYPED(admin_runtime_decorators, /datum/decorator/manual/admin_r
GLOB.admin_runtime_decorators[num_value].enabled = FALSE
- log_and_message_admins("[src] deactivated decorator id: [num_value]")
+ message_admins("[src] deactivated decorator id: [num_value]")
/client/proc/rerun_decorators()
set category = "Admin.Events"
@@ -65,4 +65,4 @@ GLOBAL_LIST_INIT_TYPED(admin_runtime_decorators, /datum/decorator/manual/admin_r
SSdecorator.force_update()
- log_and_message_admins("[src] rerun all decorators.")
+ message_admins("[src] rerun all decorators.")
diff --git a/code/modules/clans/client.dm b/code/modules/clans/client.dm
index c4948b2a6923..3450b7553c51 100644
--- a/code/modules/clans/client.dm
+++ b/code/modules/clans/client.dm
@@ -234,7 +234,7 @@
return
- log_and_message_admins("[key_name_admin(src)] has set the name of [target_clan.name] to [input].")
+ message_admins("[key_name_admin(src)] has set the name of [target_clan.name] to [input].")
to_chat(src, SPAN_NOTICE("Set the name of [target_clan.name] to [input]."))
target_clan.name = trim(input)
@@ -247,7 +247,7 @@
if(!input || input == target_clan.description)
return
- log_and_message_admins("[key_name_admin(src)] has set the description of [target_clan.name].")
+ message_admins("[key_name_admin(src)] has set the description of [target_clan.name].")
to_chat(src, SPAN_NOTICE("Set the description of [target_clan.name]."))
target_clan.description = trim(input)
@@ -261,7 +261,7 @@
return
target_clan.color = color
- log_and_message_admins("[key_name_admin(src)] has set the color of [target_clan.name] to [color].")
+ message_admins("[key_name_admin(src)] has set the color of [target_clan.name] to [color].")
to_chat(src, SPAN_NOTICE("Set the name of [target_clan.name] to [color]."))
if(CLAN_ACTION_CLAN_SETHONOR)
if(!has_clan_permission(CLAN_PERMISSION_ADMIN_MANAGER))
@@ -272,7 +272,7 @@
if((!input && input != 0) || input == target_clan.honor)
return
- log_and_message_admins("[key_name_admin(src)] has set the honor of clan [target_clan.name] from [target_clan.honor] to [input].")
+ message_admins("[key_name_admin(src)] has set the honor of clan [target_clan.name] from [target_clan.honor] to [input].")
to_chat(src, SPAN_NOTICE("Set the honor of [target_clan.name] from [target_clan.honor] to [input]."))
target_clan.honor = input
@@ -286,7 +286,7 @@
to_chat(src, "You have decided not to delete [target_clan.name].")
return
- log_and_message_admins("[key_name_admin(src)] has deleted the clan [target_clan.name].")
+ message_admins("[key_name_admin(src)] has deleted the clan [target_clan.name].")
to_chat(src, SPAN_NOTICE("You have deleted [target_clan.name]."))
var/list/datum/view_record/clan_playerbase_view/CPV = DB_VIEW(/datum/view_record/clan_playerbase_view, DB_COMP("clan_id", DB_EQUALS, target_clan.id))
@@ -339,7 +339,7 @@
return
var/target_clan = target.clan_id
- log_and_message_admins("[key_name_admin(src)] has purged [player_name]'s clan profile.")
+ message_admins("[key_name_admin(src)] has purged [player_name]'s clan profile.")
to_chat(src, SPAN_NOTICE("You have purged [player_name]'s clan profile."))
target.delete()
@@ -379,20 +379,20 @@
target.clan_id = null
target.clan_rank = GLOB.clan_ranks_ordered[CLAN_RANK_YOUNG]
to_chat(src, SPAN_NOTICE("Removed [player_name] from their clan."))
- log_and_message_admins("[key_name_admin(src)] has removed [player_name] from their current clan.")
+ message_admins("[key_name_admin(src)] has removed [player_name] from their current clan.")
else if(input == "Remove from Ancient")
target.clan_rank = GLOB.clan_ranks_ordered[CLAN_RANK_YOUNG]
target.permissions = GLOB.clan_ranks[CLAN_RANK_YOUNG].permissions
to_chat(src, SPAN_NOTICE("Removed [player_name] from ancient."))
- log_and_message_admins("[key_name_admin(src)] has removed [player_name] from ancient.")
+ message_admins("[key_name_admin(src)] has removed [player_name] from ancient.")
else if(input == "Make Ancient" && is_clan_manager)
target.clan_rank = GLOB.clan_ranks_ordered[CLAN_RANK_ADMIN]
target.permissions = CLAN_PERMISSION_ADMIN_ANCIENT
to_chat(src, SPAN_NOTICE("Made [player_name] an ancient."))
- log_and_message_admins("[key_name_admin(src)] has made [player_name] an ancient.")
+ message_admins("[key_name_admin(src)] has made [player_name] an ancient.")
else
to_chat(src, SPAN_NOTICE("Moved [player_name] to [input]."))
- log_and_message_admins("[key_name_admin(src)] has moved [player_name] to clan [input].")
+ message_admins("[key_name_admin(src)] has moved [player_name] to clan [input].")
target.clan_id = clans[input]
@@ -455,7 +455,7 @@
target.clan_rank = GLOB.clan_ranks_ordered[chosen_rank.name]
target.permissions = chosen_rank.permissions
- log_and_message_admins("[key_name_admin(src)] has set the rank of [player_name] to [chosen_rank.name] for their clan.")
+ message_admins("[key_name_admin(src)] has set the rank of [player_name] to [chosen_rank.name] for their clan.")
to_chat(src, SPAN_NOTICE("Set [player_name]'s rank to [chosen_rank.name]"))
target.save()
diff --git a/code/modules/client/tgui_macro.dm b/code/modules/client/tgui_macro.dm
index f245f1d657d4..5159a8b7414c 100644
--- a/code/modules/client/tgui_macro.dm
+++ b/code/modules/client/tgui_macro.dm
@@ -101,6 +101,7 @@ GLOBAL_LIST_EMPTY(ui_data_keybindings)
prefs.save_preferences()
INVOKE_ASYNC(owner, /client/proc/set_macros)
return TRUE
+
if("clear_keybind")
var/list/kbinds = prefs.key_bindings
var/kb_name = params["keybinding"]
@@ -111,13 +112,15 @@ GLOBAL_LIST_EMPTY(ui_data_keybindings)
for(var/key in keys)
if(kbinds[key])
kbinds[key] -= kb_name
- kbinds["Unbound"] += kb_name
if(!length(kbinds[key]))
kbinds -= key
+ // Add the keybind name to the 'unbound' list if it's not already in there.
+ kbinds["Unbound"] |= kb_name
prefs.save_preferences()
INVOKE_ASYNC(owner, /client/proc/set_macros)
return TRUE
+
if("clear_all_keybinds")
var/choice = tgui_alert(owner, "Would you prefer 'hotkey' or 'classic' defaults?", "Setup keybindings", list("Hotkey", "Classic", "Cancel"))
if(choice == "Cancel")
diff --git a/code/modules/clothing/suits/marine_armor.dm b/code/modules/clothing/suits/marine_armor.dm
index 1be37df150a8..bc29b0a33570 100644
--- a/code/modules/clothing/suits/marine_armor.dm
+++ b/code/modules/clothing/suits/marine_armor.dm
@@ -281,8 +281,122 @@
/obj/item/clothing/suit/storage/marine/medium/rto/intel
name = "\improper XM4 pattern intelligence officer armor"
- uniform_restricted = list(/obj/item/clothing/under/marine/officer, /obj/item/clothing/under/rank/qm_suit, /obj/item/clothing/under/marine/officer/intel)
+ uniform_restricted = list(/obj/item/clothing/under/marine/officer/intel)
specialty = "XM4 pattern intel"
+ desc = "Tougher than steel, quieter than whispers, the XM4 Intel Armor provides capable protection combined with an experimental integrated motion tracker. It took an R&D team a weekend to develop and costs more than the Chinook Station... probably. When worn, uniform accessories such as webbing cannot be attached due to the motion sensors occupying the clips."
+ desc_lore = "ARMAT Perfection. The XM4 Soldier Awareness System mixes M4-style hard armor and a distributed series of motion sensors clipped onto the breastplate. When connected to any HUD optic, it replicates the effects of an M314 Motion Detector unit, increasing user situational awareness. It is currently undergoing field trials by intelligence operatives."
+ storage_slots = 5
+ /// XM4 Integral Motion Detector Ability
+ actions_types = list(/datum/action/item_action/toggle, /datum/action/item_action/intel/toggle_motion_detector)
+ var/motion_detector = FALSE
+ var/obj/item/device/motiondetector/xm4/proximity
+ var/long_range_cooldown = 2
+ var/recycletime = 120
+
+/obj/item/clothing/suit/storage/marine/medium/rto/intel/Initialize(mapload, ...)
+ . = ..()
+ proximity = new(src)
+ update_icon()
+
+/datum/action/item_action/intel/action_activate()
+ if(!ishuman(owner))
+ return
+
+/datum/action/item_action/intel/update_button_icon()
+ return
+
+/datum/action/item_action/intel/toggle_motion_detector/New(Target, obj/item/holder)
+ . = ..()
+ name = "Toggle Motion Detector"
+ action_icon_state = "motion_detector"
+ button.name = name
+ button.overlays.Cut()
+ button.overlays += image('icons/mob/hud/actions.dmi', button, action_icon_state)
+
+/datum/action/item_action/intel/toggle_motion_detector/action_activate()
+ . = ..()
+ var/obj/item/clothing/suit/storage/marine/medium/rto/intel/recon = holder_item
+ recon.toggle_motion_detector(owner)
+
+/datum/action/item_action/intel/toggle_motion_detector/proc/update_icon()
+ if(!holder_item)
+ return
+ var/obj/item/clothing/suit/storage/marine/medium/rto/intel/recon = holder_item
+ if(recon.motion_detector)
+ button.icon_state = "template_on"
+ else
+ button.icon_state = "template"
+
+/obj/item/clothing/suit/storage/marine/medium/rto/intel/process()
+ if(!motion_detector)
+ STOP_PROCESSING(SSobj, src)
+ if(motion_detector)
+ recycletime--
+ if(!recycletime)
+ recycletime = initial(recycletime)
+ proximity.refresh_blip_pool()
+ long_range_cooldown--
+ if(long_range_cooldown)
+ return
+ long_range_cooldown = initial(long_range_cooldown)
+ proximity.scan()
+
+/obj/item/clothing/suit/storage/marine/medium/rto/intel/proc/toggle_motion_detector(mob/user)
+ to_chat(user,SPAN_NOTICE("You [motion_detector? "disable" : "enable"] \the [src]'s motion detector."))
+ if(!motion_detector)
+ playsound(loc,'sound/items/detector_turn_on.ogg', 25, 1)
+ else
+ playsound(loc,'sound/items/detector_turn_off.ogg', 25, 1)
+ motion_detector = !motion_detector
+ var/datum/action/item_action/intel/toggle_motion_detector/TMD = locate(/datum/action/item_action/intel/toggle_motion_detector) in actions
+ TMD.update_icon()
+ motion_detector()
+
+/obj/item/clothing/suit/storage/marine/medium/rto/intel/proc/motion_detector()
+ if(motion_detector)
+ START_PROCESSING(SSobj, src)
+ else
+ STOP_PROCESSING(SSobj, src)
+
+/obj/item/clothing/suit/storage/marine/medium/rto/intel/mob_can_equip(mob/living/carbon/human/user, slot, disable_warning) //Thanks to Drathek for the help on this part!
+ if(!..())
+ return FALSE
+
+ // Only equip if uniform doesn't already have a utility accessory slot equipped
+ var/obj/item/clothing/under/uniform = user.w_uniform
+ var/accessory = locate(/obj/item/clothing/accessory/storage) in uniform.accessories
+ if(accessory)
+ to_chat(user, SPAN_WARNING("[src] can't be worn with [accessory]."))
+ return FALSE
+ // Only equip if user has expert intel skill level
+ if(!skillcheck(user, SKILL_INTEL, SKILL_INTEL_EXPERT))
+ to_chat(user, SPAN_WARNING("You don't seem to know how to use [src]..."))
+ return FALSE
+ return TRUE
+
+/obj/item/clothing/suit/storage/marine/medium/rto/intel/equipped(mob/user, slot, silent) //When XM4 is equipped this removes ACCESSORY_SLOT_UTILITY as a valid accessory for the uniform
+ . = ..()
+ if(slot == WEAR_JACKET)
+ var/mob/living/carbon/human/human = user
+ var/obj/item/clothing/under/uniform = human.w_uniform
+ if(uniform?.valid_accessory_slots)
+ uniform?.valid_accessory_slots -= ACCESSORY_SLOT_UTILITY
+
+/obj/item/clothing/suit/storage/marine/medium/rto/intel/unequipped(mob/user, slot) //When unequipped this adds the ACCESSORY_SLOT_UTILITY back as a valid accessory
+ . = ..()
+ if(slot == WEAR_JACKET)
+ var/mob/living/carbon/human/human = user
+ var/obj/item/clothing/under/uniform = human.w_uniform
+ if(uniform)
+ // Figure out if the uniform originally allowed ACCESSORY_SLOT_UTILITY
+ var/obj/item/clothing/under/temp_uniform = new uniform.type
+ if(temp_uniform.valid_accessory_slots)
+ for(var/allowed in temp_uniform.valid_accessory_slots)
+ if(allowed == ACCESSORY_SLOT_UTILITY)
+ // It was allowed previously, now add it back
+ uniform.valid_accessory_slots += ACCESSORY_SLOT_UTILITY
+ break
+ qdel(temp_uniform)
/obj/item/clothing/suit/storage/marine/MP
name = "\improper M2 pattern MP armor"
diff --git a/code/modules/dropships/attach_points/attach_point.dm b/code/modules/dropships/attach_points/attach_point.dm
index 7230e67e0af1..6fef4d58e785 100644
--- a/code/modules/dropships/attach_points/attach_point.dm
+++ b/code/modules/dropships/attach_points/attach_point.dm
@@ -27,44 +27,45 @@
/obj/effect/attach_point/attackby(obj/item/I, mob/user)
if(istype(I, /obj/item/powerloader_clamp))
- var/obj/item/powerloader_clamp/PC = I
- install_equipment(PC, user)
+ var/obj/item/powerloader_clamp/clamp = I
+ install_equipment(clamp, user)
return TRUE
return ..()
/// Called when a real user with a powerloader attempts to install an equipment on the attach point
-/obj/effect/attach_point/proc/install_equipment(obj/item/powerloader_clamp/PC, mob/living/user)
- if(!istype(PC.loaded, /obj/structure/dropship_equipment))
+/obj/effect/attach_point/proc/install_equipment(obj/item/powerloader_clamp/clamp, mob/living/user)
+ if(!istype(clamp.loaded, /obj/structure/dropship_equipment))
+ return
+ var/obj/structure/dropship_equipment/ds_equipment = clamp.loaded
+ if(!(base_category in ds_equipment.equip_categories))
+ to_chat(user, SPAN_WARNING("[ds_equipment] doesn't fit on [src]."))
return
- var/obj/structure/dropship_equipment/SE = PC.loaded
- if(!(base_category in SE.equip_categories))
- to_chat(user, SPAN_WARNING("[SE] doesn't fit on [src]."))
- return TRUE
if(installed_equipment)
- return TRUE
- playsound(loc, 'sound/machines/hydraulics_1.ogg', 40, 1)
+ return
+ playsound(loc, 'sound/machines/hydraulics_1.ogg', 40, TRUE)
var/point_loc = loc
if(!user || !do_after(user, (7 SECONDS) * user.get_skill_duration_multiplier(SKILL_ENGINEER), INTERRUPT_NO_NEEDHAND|BEHAVIOR_IMMOBILE, BUSY_ICON_BUILD))
- return TRUE
+ return
if(loc != point_loc)//dropship flew away
- return TRUE
- if(installed_equipment || PC.loaded != SE)
- return TRUE
- to_chat(user, SPAN_NOTICE("You install [SE] on [src]."))
- SE.forceMove(loc)
- PC.loaded = null
- playsound(loc, 'sound/machines/hydraulics_2.ogg', 40, 1)
- PC.update_icon()
- installed_equipment = SE
- SE.ship_base = src
+ return
+ if(installed_equipment || clamp.loaded != ds_equipment)
+ return
+ to_chat(user, SPAN_NOTICE("You install [ds_equipment] on [src]."))
+ ds_equipment.forceMove(loc)
+ clamp.loaded = null
+ playsound(loc, 'sound/machines/hydraulics_2.ogg', 40, TRUE)
+ clamp.update_icon()
+ installed_equipment = ds_equipment
+ ds_equipment.ship_base = src
+ ds_equipment.plane = plane
for(var/obj/docking_port/mobile/marine_dropship/shuttle in SSshuttle.mobile)
if(shuttle.id == ship_tag)
- SE.linked_shuttle = shuttle
- SEND_SIGNAL(shuttle, COMSIG_DROPSHIP_ADD_EQUIPMENT, SE)
+ ds_equipment.linked_shuttle = shuttle
+ SEND_SIGNAL(shuttle, COMSIG_DROPSHIP_ADD_EQUIPMENT, ds_equipment)
break
- SE.update_equipment()
+ ds_equipment.update_equipment()
/// Weapon specific attachment point
/obj/effect/attach_point/weapon
diff --git a/code/modules/gear_presets/uscm.dm b/code/modules/gear_presets/uscm.dm
index 8f5243edf84d..5402a5ed7305 100644
--- a/code/modules/gear_presets/uscm.dm
+++ b/code/modules/gear_presets/uscm.dm
@@ -251,8 +251,6 @@
/datum/equipment_preset/uscm/intel/full/load_gear(mob/living/carbon/human/new_human)
var/obj/item/clothing/under/marine/officer/intel/U = new(new_human)
- var/obj/item/clothing/accessory/storage/webbing/W = new()
- U.attach_accessory(new_human, W)
new_human.equip_to_slot_or_del(U, WEAR_BODY)
new_human.equip_to_slot_or_del(new /obj/item/device/radio/headset/almayer/intel(new_human), WEAR_L_EAR)
new_human.equip_to_slot_or_del(new /obj/item/clothing/shoes/marine/knife(new_human), WEAR_FEET)
diff --git a/code/modules/gear_presets/wy.dm b/code/modules/gear_presets/wy.dm
index ac8aefb6238d..d492f9573b57 100644
--- a/code/modules/gear_presets/wy.dm
+++ b/code/modules/gear_presets/wy.dm
@@ -20,7 +20,7 @@
/datum/equipment_preset/wy/load_gear(mob/living/carbon/human/new_human)
new_human.equip_to_slot_or_del(new headset_type(new_human), WEAR_L_EAR)
new_human.equip_to_slot_or_del(new /obj/item/clothing/under/liaison_suit/ivy(new_human), WEAR_BODY)
- new_human.equip_to_slot_or_del(new /obj/item/clothing/shoes/centcom(new_human), WEAR_FEET)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/shoes/laceup(new_human), WEAR_FEET)
new_human.equip_to_slot_or_del(new /obj/item/storage/backpack/satchel(new_human), WEAR_BACK)
. = ..()
@@ -63,6 +63,20 @@
rank = JOB_EXECUTIVE_SPECIALIST
paygrade = PAY_SHORT_WYC5
+/datum/equipment_preset/wy/exec_spec/lawyer
+ name = "Corporate - E - Lawyer"
+
+/datum/equipment_preset/wy/exec_spec/lawyer/load_gear(mob/living/carbon/human/new_human)
+ new_human.equip_to_slot_or_del(new /obj/item/device/radio/headset/distress/WY(new_human), WEAR_L_EAR)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/under/liaison_suit/blue(new_human), WEAR_BODY)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/suit/storage/jacket/marine/corporate/blue(new_human), WEAR_JACKET)
+ new_human.equip_to_slot_or_del(new /obj/item/tool/pen/clicky(new_human), WEAR_IN_BACK)
+ new_human.equip_to_slot_or_del(new /obj/item/clipboard(new_human), WEAR_IN_BACK)
+ new_human.equip_to_slot_or_del(new /obj/item/notepad/blue(new_human), WEAR_R_STORE)
+ new_human.equip_to_slot_or_del(new /obj/item/device/taperecorder(new_human), WEAR_L_STORE)
+
+ ..()
+
/datum/equipment_preset/wy/exec_supervisor
name = "Corporate - F - Executive Supervisor"
flags = EQUIPMENT_PRESET_EXTRA
@@ -70,6 +84,21 @@
rank = JOB_EXECUTIVE_SUPERVISOR
paygrade = PAY_SHORT_WYC6
+/datum/equipment_preset/wy/exec_supervisor/lawyer
+ name = "Corporate - F - Lawyer"
+
+/datum/equipment_preset/wy/exec_supervisor/lawyer/load_gear(mob/living/carbon/human/new_human)
+ new_human.equip_to_slot_or_del(new /obj/item/device/radio/headset/distress/WY(new_human), WEAR_L_EAR)
+ new_human.equip_to_slot_or_del(new /obj/item/storage/secure/briefcase(new_human), WEAR_R_HAND)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/under/liaison_suit/black(new_human), WEAR_BODY)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/suit/storage/jacket/marine/corporate/black(new_human), WEAR_JACKET)
+ new_human.equip_to_slot_or_del(new /obj/item/tool/pen/clicky(new_human), WEAR_IN_BACK)
+ new_human.equip_to_slot_or_del(new /obj/item/clipboard(new_human), WEAR_IN_BACK)
+ new_human.equip_to_slot_or_del(new /obj/item/spacecash/c1000/counterfeit(new_human), WEAR_IN_BACK)
+ new_human.equip_to_slot_or_del(new /obj/item/notepad/black(new_human), WEAR_R_STORE)
+ new_human.equip_to_slot_or_del(new /obj/item/device/taperecorder(new_human), WEAR_L_STORE)
+ ..()
+
/datum/equipment_preset/wy/manager
skills = /datum/skills/civilian/manager
idtype = /obj/item/card/id/silver/clearance_badge/manager
diff --git a/code/modules/logging/global_logs.dm b/code/modules/logging/global_logs.dm
index ff384fe537c0..e0055907d67a 100644
--- a/code/modules/logging/global_logs.dm
+++ b/code/modules/logging/global_logs.dm
@@ -25,8 +25,8 @@ GLOBAL_PROTECT(world_runtime_log)
GLOBAL_VAR(scheduler_stats)
GLOBAL_PROTECT(scheduler_stats)
-GLOBAL_VAR(mutator_logs)
-GLOBAL_PROTECT(mutator_logs)
+GLOBAL_VAR(strain_logs)
+GLOBAL_PROTECT(strain_logs)
GLOBAL_VAR(round_stats)
GLOBAL_PROTECT(round_stats)
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index cb16480fcfaa..ae86518a640f 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -316,7 +316,10 @@
hud_used.show_hud(hud_used.hud_version)
/mob/dead/observer/Login()
- ..()
+ ..() // This calls signals which might have resulted in our client getting deleted
+
+ if(!client)
+ return
if(client.check_whitelist_status(WHITELIST_PREDATOR))
RegisterSignal(SSdcs, COMSIG_GLOB_PREDATOR_ROUND_TOGGLED, PROC_REF(toggle_predator_action))
diff --git a/code/modules/mob/emote.dm b/code/modules/mob/emote.dm
index f1b600179450..fa7c7ec3176e 100644
--- a/code/modules/mob/emote.dm
+++ b/code/modules/mob/emote.dm
@@ -14,13 +14,15 @@
to_chat(src, SPAN_NOTICE("'[act]' emote does not exist. Say *help for a list."))
return FALSE
var/silenced = FALSE
- for(var/datum/emote/P in key_emotes)
- if(!P.check_cooldown(src, intentional))
+ for(var/datum/emote/current_emote in key_emotes)
+ if(!current_emote.check_cooldown(src, intentional))
silenced = TRUE
continue
- if(P.run_emote(src, param, m_type, intentional))
- SEND_SIGNAL(src, COMSIG_MOB_EMOTE, P, act, m_type, message, intentional)
- SEND_SIGNAL(src, COMSIG_MOB_EMOTED(P.key))
+ if(SEND_SIGNAL(src, COMSIG_MOB_TRY_EMOTE, current_emote, act, m_type, param, intentional) & COMPONENT_OVERRIDE_EMOTE)
+ silenced = TRUE
+ continue
+ if(current_emote.run_emote(src, param, m_type, intentional))
+ SEND_SIGNAL(src, COMSIG_MOB_EMOTE, current_emote, act, m_type, message, intentional)
return TRUE
if(intentional && !silenced && !force_silence)
to_chat(src, SPAN_NOTICE("Unusable emote '[act]'. Say *help for a list."))
diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm
index 08ddd11da5b3..c38ddbcd552c 100644
--- a/code/modules/mob/living/carbon/human/human_helpers.dm
+++ b/code/modules/mob/living/carbon/human/human_helpers.dm
@@ -247,6 +247,13 @@
var/datum/action/item_action/smartgun/toggle_motion_detector/TMD = locate(/datum/action/item_action/smartgun/toggle_motion_detector) in sg.actions
TMD.update_icon()
sg.motion_detector()
+ if(istype(i, /obj/item/clothing/suit/storage/marine/medium/rto/intel))
+ var/obj/item/clothing/suit/storage/marine/medium/rto/intel/xm4 = i
+ if(xm4.motion_detector)
+ xm4.motion_detector = FALSE
+ var/datum/action/item_action/intel/toggle_motion_detector/TMD = locate(/datum/action/item_action/intel/toggle_motion_detector) in xm4.actions
+ TMD.update_icon()
+ xm4.motion_detector()
/mob/living/carbon/human/proc/disable_headsets()
//Disable all radios to reduce radio spam for dead people
diff --git a/code/modules/mob/living/carbon/xenomorph/Evolution.dm b/code/modules/mob/living/carbon/xenomorph/Evolution.dm
index d6f963747e33..b6576b764b51 100644
--- a/code/modules/mob/living/carbon/xenomorph/Evolution.dm
+++ b/code/modules/mob/living/carbon/xenomorph/Evolution.dm
@@ -211,7 +211,10 @@
return FALSE
if(lock_evolve)
- to_chat(src, SPAN_WARNING("You are banished and cannot reach the hivemind."))
+ if(banished)
+ to_chat(src, SPAN_WARNING("We are banished and cannot reach the hivemind."))
+ else
+ to_chat(src, SPAN_WARNING("We can't evolve."))
return FALSE
if(jobban_isbanned(src, JOB_XENOMORPH))//~who so genius to do this is?
@@ -269,7 +272,10 @@
return
if(lock_evolve)
- to_chat(src, SPAN_WARNING("We are banished and cannot reach the hivemind."))
+ if(banished)
+ to_chat(src, SPAN_WARNING("We are banished and cannot reach the hivemind."))
+ else
+ to_chat(src, SPAN_WARNING("We can't deevolve."))
return FALSE
diff --git a/code/modules/mob/living/carbon/xenomorph/XenoMutatorSets.dm b/code/modules/mob/living/carbon/xenomorph/XenoMutatorSets.dm
deleted file mode 100644
index fc5532955ff9..000000000000
--- a/code/modules/mob/living/carbon/xenomorph/XenoMutatorSets.dm
+++ /dev/null
@@ -1,264 +0,0 @@
-#define MUTATOR_GAIN_PER_QUEEN_LEVEL 6
-#define MUTATOR_GAIN_PER_XENO_LEVEL 3
-
-//A class that holds mutators for a given Xeno hive
-//Each time a Queen matures, the hive gets more points
-//Each time a Queen dies, the mutators are reset
-
-//The class contains a lot of variables that are applied to various xenos' stats and actions
-/datum/mutator_set
- var/remaining_points = 1 //How many points the xeno / hive still has to spend on mutators
- var/list/purchased_mutators = list() //List of purchased mutators
- var/user_level = 0 //Level of the Queen for Hive or the individual xeno. Starting at -1 so at tier 0 you'd get some mutators to play with
-
- var/tackle_strength_bonus = 0
-
-//Functions to be overloaded to call for when something gets updated on the xenos
-/datum/mutator_set/proc/recalculate_everything(description)
-/datum/mutator_set/proc/recalculate_stats(description)
-/datum/mutator_set/proc/recalculate_actions(description)
-/datum/mutator_set/proc/recalculate_pheromones(description)
-/datum/mutator_set/proc/give_feedback(description)
-
-
-/datum/mutator_set/proc/purchase_mutator(name)
- return FALSE
-
-/datum/mutator_set/proc/list_and_purchase_mutators()
- var/list/mutators_for_purchase = available_mutators()
- var/mob/living/carbon/xenomorph/Xeno = usr
- if(mutators_for_purchase.len == 0)
- to_chat(usr, "There are no available strains.")
- var/pick = tgui_input_list(usr, "Which strain would you like to purchase?", "Purchase strain", mutators_for_purchase, theme="hive_status")
- if(!pick)
- return FALSE
- if(alert(usr, "[GLOB.xeno_mutator_list[pick].description]\n\nConfirm mutation?", "Strain purchase", "Yes", "No") != "Yes") return
- if(!Xeno.strain_checks())
- return
- if(GLOB.xeno_mutator_list[pick].apply_mutator(src))
- to_chat(usr, "Mutation complete!")
- return TRUE
- else
- to_chat(usr, "Mutation failed!")
- return FALSE
-
-/datum/mutator_set/proc/can_purchase_mutator(mutator_name)
- var/datum/xeno_mutator/XM = GLOB.xeno_mutator_list[mutator_name]
- if(user_level < XM.required_level)
- return FALSE //xeno doesn't meet the level requirements
- if(remaining_points < XM.cost)
- return FALSE //mutator is too expensive
- if(XM.unique)
- if(XM.name in purchased_mutators)
- return FALSE //unique mutator already purchased
- if(XM.keystone)
- for(var/name in purchased_mutators)
- if(GLOB.xeno_mutator_list[name].keystone)
- return FALSE //We already have a keystone mutator
- if(XM.flaw)
- for(var/name in purchased_mutators)
- if(GLOB.xeno_mutator_list[name].flaw)
- return FALSE //We already have a flaw mutator
- return TRUE
-
-//Lists mutators available for purchase
-/datum/mutator_set/proc/available_mutators()
- var/list/can_purchase = list()
-
- for(var/str in GLOB.xeno_mutator_list)
- if (can_purchase_mutator(str))
- can_purchase += str //can purchase!
-
- return can_purchase
-
-//Mutators applying to the Hive as a whole
-/datum/mutator_set/hive_mutators
- var/datum/hive_status/hive //Which hive do these mutators apply to. Need this to affect variables there
- var/leader_count_boost = 0
- var/maturation_multiplier = 1
- var/tier_slot_multiplier = 1
- var/larva_gestation_multiplier = 1
- var/bonus_larva_spawn_chance = 0
-
-/datum/mutator_set/hive_mutators/list_and_purchase_mutators()
- if(!hive || !hive.living_xeno_queen)
- return //somehow Queen is not set but this function was called...
- if(hive.living_xeno_queen.is_dead())
- return //Dead xenos can't mutate!
- if(hive.living_xeno_queen.hardcore)
- to_chat(usr, SPAN_WARNING("No time for that, must KILL!"))
- return
- if(!hive.living_xeno_queen.ovipositor)
- to_chat(usr, "You must be in Ovipositor to purchase Hive Mutators.")
- return
- . = ..()
- if (. == TRUE && purchased_mutators.len)
- var/m = purchased_mutators[purchased_mutators.len]
- log_mutator("[hive.living_xeno_queen.name] purchased Hive Mutator '[m]'")
-
-/datum/mutator_set/hive_mutators/can_purchase_mutator(mutator_name)
- if (..() == FALSE)
- return FALSE //Can't buy it regardless
- var/datum/xeno_mutator/XM = GLOB.xeno_mutator_list[mutator_name]
- if(XM.individual_only)
- return FALSE //We can't buy individual mutators on a Hive level
- return TRUE
-
-//Called when the Queen dies
-// This isn't currently used, but if anyone wants to, expect it to be broken because
-// I haven't made any effort to integrate it into the new system (Fourkhan, 5/11/19)
-/datum/mutator_set/hive_mutators/proc/reset_mutators()
- if(purchased_mutators.len == 0)
- //No mutators purchased, nothing to reset!
- return
-
- var/depowered = FALSE
- for(var/name in purchased_mutators)
- if(!GLOB.xeno_mutator_list[name].death_persistent)
- purchased_mutators -= name
- depowered = TRUE
-
- if(!depowered)
- return //We haven't lost anything
-
- tackle_strength_bonus = 0
-
- leader_count_boost = 0
- maturation_multiplier = 1
- tier_slot_multiplier = 1
- larva_gestation_multiplier = 1
- bonus_larva_spawn_chance = 0
-
- for(var/mob/living/carbon/xenomorph/X in GLOB.living_xeno_list)
- if(X.hivenumber == hive.hivenumber)
- X.recalculate_everything()
- to_chat(X, SPAN_XENOANNOUNCE("Queen's influence wanes. You feel weak!"))
- playsound(X.loc, "alien_help", 25)
- X.xeno_jitter(15)
-
-/datum/mutator_set/hive_mutators/recalculate_everything(description)
- for(var/mob/living/carbon/xenomorph/X in GLOB.living_xeno_list)
- if(X.hivenumber == hive.hivenumber)
- X.recalculate_everything()
- to_chat(X, SPAN_XENOANNOUNCE("Queen has granted the Hive a boon! [description]"))
- X.xeno_jitter(15)
-/datum/mutator_set/hive_mutators/recalculate_stats(description)
- for(var/mob/living/carbon/xenomorph/X in GLOB.living_xeno_list)
- if(X.hivenumber == hive.hivenumber)
- X.recalculate_stats()
- to_chat(X, SPAN_XENOANNOUNCE("Queen has granted the Hive a boon! [description]"))
- X.xeno_jitter(15)
-/datum/mutator_set/hive_mutators/recalculate_actions(description)
- for(var/mob/living/carbon/xenomorph/X in GLOB.living_xeno_list)
- if(X.hivenumber == hive.hivenumber)
- X.recalculate_actions()
- to_chat(X, SPAN_XENOANNOUNCE("Queen has granted the Hive a boon! [description]"))
- X.xeno_jitter(15)
-/datum/mutator_set/hive_mutators/recalculate_pheromones(description)
- for(var/mob/living/carbon/xenomorph/X in GLOB.living_xeno_list)
- if(X.hivenumber == hive.hivenumber)
- X.recalculate_pheromones()
- to_chat(X, SPAN_XENOANNOUNCE("Queen has granted the Hive a boon! [description]"))
- X.xeno_jitter(15)
-/datum/mutator_set/hive_mutators/proc/recalculate_maturation(description)
- for(var/mob/living/carbon/xenomorph/X in GLOB.living_xeno_list)
- if(X.hivenumber == hive.hivenumber)
- X.recalculate_maturation()
- to_chat(X, SPAN_XENOANNOUNCE("Queen has granted the Hive a boon! [description]"))
- X.xeno_jitter(15)
-/datum/mutator_set/hive_mutators/proc/recalculate_hive(description)
- hive.recalculate_hive()
- give_feedback(description)
-/datum/mutator_set/hive_mutators/give_feedback(description)
- for(var/mob/living/carbon/xenomorph/X in GLOB.living_xeno_list)
- if(X.hivenumber == hive.hivenumber)
- to_chat(X, SPAN_XENOANNOUNCE("Queen has granted the Hive a boon! [description]"))
- X.xeno_jitter(15)
-
-//Mutators applying to an individual xeno
-/datum/mutator_set/individual_mutators
- var/mob/living/carbon/xenomorph/xeno
- var/pull_multiplier = 1
- var/egg_laying_multiplier = 1
- var/need_weeds = TRUE
- //Strains Below
- remaining_points = 6
-
-
-/datum/mutator_set/individual_mutators/Destroy()
- if(xeno)
- xeno.mutators = null
- xeno = null
- . = ..()
-
-/datum/mutator_set/individual_mutators/list_and_purchase_mutators()
- . = ..()
- if (. == TRUE && purchased_mutators.len)
- var/m = purchased_mutators[purchased_mutators.len]
- log_mutator("[xeno.name] purchased Mutator '[m]'")
-
-/datum/mutator_set/individual_mutators/can_purchase_mutator(mutator_name)
- if (..() == FALSE)
- return FALSE //Can't buy it regardless
- var/datum/xeno_mutator/XM = GLOB.xeno_mutator_list[mutator_name]
- if(XM.hive_only)
- return FALSE //We can't buy Hive mutators on an individual level
- if(XM.caste_whitelist && (XM.caste_whitelist.len > 0) && !(xeno.caste_type in XM.caste_whitelist))
- return FALSE //We are not on the whitelist
- return TRUE
-
-/datum/mutator_set/individual_mutators/recalculate_actions(description, flavor_description = null)
- xeno.recalculate_actions()
- to_chat(xeno, SPAN_XENOANNOUNCE("[description]"))
- if (flavor_description != null)
- to_chat(xeno, SPAN_XENOLEADER("[flavor_description]"))
- xeno.xeno_jitter(15)
-
-
-/mob/living/carbon/xenomorph/queen/verb/purchase_hive_mutators()
- set name = "Purchase Hive Mutators"
- set desc = "Purchase Mutators affecting the entire Hive."
- set category = "Alien"
- if(hardcore)
- to_chat(usr, SPAN_WARNING("No time for that, must KILL!"))
- return
- if(!src.hive || !src.hive.mutators)
- return //For some reason we don't have mutators
- src.hive.mutators.list_and_purchase_mutators()
-
-/mob/living/carbon/xenomorph/verb/purchase_strains()
- set name = "Purchase Strains"
- set desc = "Purchase Strains for yourself."
- set category = "Alien"
-
- if(!strain_checks())
- return
- if(!src.mutators)
- return //For some reason we don't have mutators
- src.mutators.list_and_purchase_mutators()
-
-/mob/living/carbon/xenomorph/proc/strain_checks()
- if(!check_state(TRUE))
- return FALSE
-
- if(is_ventcrawling)
- to_chat(src, SPAN_WARNING("This place is too constraining to take a strain."))
- return FALSE
-
- if(!isturf(loc))
- to_chat(src, SPAN_WARNING("We can't take a strain here."))
- return FALSE
-
- if(handcuffed || legcuffed)
- to_chat(src, SPAN_WARNING("The restraints are too restricting to allow us to take a strain."))
- return FALSE
-
- if(health < maxHealth)
- to_chat(src, SPAN_WARNING("We must be at full health to take a strain."))
- return FALSE
-
- if(agility || fortify || crest_defense || stealth)
- to_chat(src, SPAN_WARNING("We cannot take a strain while in this stance."))
- return FALSE
-
- return TRUE
diff --git a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm
index fc25c80e795f..c8d3df91c975 100644
--- a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm
+++ b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm
@@ -130,10 +130,8 @@
var/weed_level = WEED_LEVEL_STANDARD
var/acid_level = 0
- // Mutator-related and other important vars
- var/mutation_icon_state = null
- var/mutation_type = null
- var/datum/mutator_set/individual_mutators/mutators = new
+ /// The xeno's strain, if they've taken one.
+ var/datum/xeno_strain/strain = null
// Hive-related vars
var/datum/hive_status/hive
@@ -389,8 +387,6 @@
for(var/trait in hive.hive_inherant_traits)
ADD_TRAIT(src, trait, TRAIT_SOURCE_HIVE)
- mutators.xeno = src
-
//Set caste stuff
if(caste_type && GLOB.xeno_datum_list[caste_type])
caste = GLOB.xeno_datum_list[caste_type]
@@ -661,8 +657,8 @@
. += "It appears to belong to [hive?.name ? "the [hive.name]" : "a different hive"]."
if(isxeno(user) || isobserver(user))
- if(mutation_type != "Normal")
- . += "It has specialized into a [mutation_type]."
+ if(strain)
+ . += "It has specialized into a [strain.name]."
if(iff_tag)
. += SPAN_NOTICE("It has an IFF tag sticking out of its carapace.")
@@ -693,7 +689,7 @@
selected_ability = null
queued_action = null
- QDEL_NULL(mutators)
+ QDEL_NULL(strain)
QDEL_NULL(behavior_delegate)
built_structures = null
@@ -709,18 +705,11 @@
if(hardcore)
attack_log?.Cut() // Completely clear out attack_log to limit mem usage if we fail to delete
- . = ..()
-
- // Everything below fits the "we have to clear by principle it but i dont wanna break stuff" bill
- mutators = null
-
-
+ return ..()
/mob/living/carbon/xenomorph/slip(slip_source_name, stun_level, weaken_level, run_only, override_noslip, slide_steps)
return FALSE
-
-
/mob/living/carbon/xenomorph/start_pulling(atom/movable/AM, lunge, no_msg)
if(SEND_SIGNAL(AM, COMSIG_MOVABLE_XENO_START_PULLING, src) & COMPONENT_ALLOW_PULL)
return do_pull(AM, lunge, no_msg)
@@ -823,10 +812,10 @@
//*********************************************************//
-//********************Mutator functions********************//
+// ******************** Strain Procs **********************//
//*********************************************************//
-//Call this function when major changes happen - evolutions, upgrades, mutators getting removed
+//Call this proc when major changes happen - evolutions, upgrades, mutators getting removed
/mob/living/carbon/xenomorph/proc/recalculate_everything()
recalculate_stats()
recalculate_actions()
@@ -850,8 +839,6 @@
tackle_min = caste.tackle_min
tackle_max = caste.tackle_max
tackle_chance = caste.tackle_chance + tackle_chance_modifier
- tacklestrength_min = caste.tacklestrength_min + mutators.tackle_strength_bonus + hive.mutators.tackle_strength_bonus
- tacklestrength_max = caste.tacklestrength_max + mutators.tackle_strength_bonus + hive.mutators.tackle_strength_bonus
/mob/living/carbon/xenomorph/proc/recalculate_health()
var/new_max_health = nocrit ? health_modifier + maxHealth : health_modifier + caste.max_health
@@ -907,18 +894,8 @@
/mob/living/carbon/xenomorph/proc/recalculate_actions()
recalculate_acid()
recalculate_weeds()
- pull_multiplier = mutators.pull_multiplier
- if(isrunner(src))
- //Xeno runners need a small nerf to dragging speed mutator
- pull_multiplier = 1 - (1 - mutators.pull_multiplier) * 0.85
- if(is_zoomed)
- zoom_out()
- if(iscarrier(src))
- var/mob/living/carbon/xenomorph/carrier/carrier = src
- carrier.huggers_max = caste.huggers_max
- carrier.eggs_max = caste.eggs_max
- need_weeds = mutators.need_weeds
-
+ // Modified on subtypes
+ pull_multiplier = initial(pull_multiplier)
/mob/living/carbon/xenomorph/proc/recalculate_acid()
if(caste)
@@ -1043,7 +1020,7 @@
handle_ghost_message()
/mob/living/carbon/xenomorph/proc/handle_ghost_message()
- notify_ghosts("[src] ([mutation_type] [caste_type]) has ghosted and their body is up for grabs!", source = src)
+ notify_ghosts("[src] ([get_strain_name()] [caste_type]) has ghosted and their body is up for grabs!", source = src)
/mob/living/carbon/xenomorph/larva/handle_ghost_message()
if(locate(/obj/effect/alien/resin/special/pylon/core) in range(2, get_turf(src)))
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/crusher/crusher_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/crusher/crusher_powers.dm
index 4ac166c58c69..e1af5e36a40f 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/crusher/crusher_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/crusher/crusher_powers.dm
@@ -37,7 +37,7 @@
RegisterSignal(owner, COMSIG_XENO_PRE_CALCULATE_ARMOURED_DAMAGE_PROJECTILE, PROC_REF(check_directional_armor))
var/mob/living/carbon/xenomorph/xeno_owner = owner
- if(!istype(xeno_owner) || xeno_owner.mutation_type != CRUSHER_NORMAL)
+ if(!istype(xeno_owner))
return
var/datum/behavior_delegate/crusher_base/crusher_delegate = xeno_owner.behavior_delegate
@@ -51,7 +51,7 @@
..()
UnregisterSignal(owner, COMSIG_XENO_PRE_CALCULATE_ARMOURED_DAMAGE_PROJECTILE)
var/mob/living/carbon/xenomorph/xeno_owner = owner
- if(!istype(xeno_owner) || xeno_owner.mutation_type != CRUSHER_NORMAL)
+ if(!istype(xeno_owner))
return
var/datum/behavior_delegate/crusher_base/crusher_delegate = xeno_owner.behavior_delegate
@@ -62,7 +62,7 @@
/datum/action/xeno_action/activable/pounce/crusher_charge/proc/undo_charging_icon()
var/mob/living/carbon/xenomorph/xeno_owner = owner
- if(!istype(xeno_owner) || xeno_owner.mutation_type != CRUSHER_NORMAL)
+ if(!istype(xeno_owner))
return
var/datum/behavior_delegate/crusher_base/crusher_delegate = xeno_owner.behavior_delegate
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/defender/defender_abilities.dm b/code/modules/mob/living/carbon/xenomorph/abilities/defender/defender_abilities.dm
index 22d5f4b57aa2..d28a4bb67978 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/defender/defender_abilities.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/defender/defender_abilities.dm
@@ -20,6 +20,13 @@
ability_primacy = XENO_PRIMARY_ACTION_2
xeno_cooldown = 4 SECONDS
+ var/base_damage = 30
+ var/usable_while_fortified = FALSE
+
+/datum/action/xeno_action/activable/headbutt/steel_crest
+ base_damage = 37.5
+ usable_while_fortified = TRUE
+
/datum/action/xeno_action/onclick/tail_sweep
name = "Tail Sweep"
action_icon_state = "tail_sweep"
@@ -41,8 +48,9 @@
/// Extra armor when fortified and facing bullets.
var/frontal_armor = 5
- /// Extra armor when steelcrest, fortified, and facing bullets.
- var/steelcrest_frontal_armor = 15
+
+/datum/action/xeno_action/activable/fortify/steel_crest
+ frontal_armor = 15
/datum/action/xeno_action/activable/tail_stab/slam
name = "Tail Slam"
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/defender/defender_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/defender/defender_powers.dm
index 9326ae78bf9d..98c0c19f68f7 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/defender/defender_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/defender/defender_powers.dm
@@ -40,7 +40,7 @@
// Defender Headbutt
/datum/action/xeno_action/activable/headbutt/use_ability(atom/target_atom)
var/mob/living/carbon/xenomorph/fendy = owner
- if (!istype(fendy))
+ if(!istype(fendy))
return
if(!isxeno_human(target_atom) || fendy.can_not_harm(target_atom))
@@ -49,13 +49,13 @@
if(!fendy.check_state())
return
- if (!action_cooldown_check())
+ if(!action_cooldown_check())
return
if(!check_and_use_plasma_owner())
return
- if(fendy.fortify && !(fendy.mutation_type == DEFENDER_STEELCREST))
+ if(fendy.fortify && !usable_while_fortified)
to_chat(fendy, SPAN_XENOWARNING("We cannot use headbutt while fortified."))
return
@@ -81,11 +81,10 @@
fendy.visible_message(SPAN_XENOWARNING("[fendy] rams [carbone] with its armored crest!"), \
SPAN_XENOWARNING("We ram [carbone] with our armored crest!"))
- if(carbone.stat != DEAD && (!(carbone.status_flags & XENO_HOST) || !HAS_TRAIT(carbone, TRAIT_NESTED)) )
- var/h_damage = 30 - (fendy.crest_defense * 10)
- if(fendy.mutation_type == DEFENDER_STEELCREST)
- h_damage += 7.5
- carbone.apply_armoured_damage(get_xeno_damage_slash(carbone, h_damage), ARMOR_MELEE, BRUTE, "chest", 5)
+ if(carbone.stat != DEAD && (!(carbone.status_flags & XENO_HOST) || !HAS_TRAIT(carbone, TRAIT_NESTED)))
+ // -10 damage if their crest is down.
+ var/damage = base_damage - (fendy.crest_defense * 10)
+ carbone.apply_armoured_damage(get_xeno_damage_slash(carbone, damage), ARMOR_MELEE, BRUTE, "chest", 5)
var/facing = get_dir(fendy, carbone)
var/headbutt_distance = 1 + (fendy.crest_defense * 2) + (fendy.fortify * 2)
@@ -157,10 +156,6 @@
if (!istype(xeno))
return
- if(xeno.crest_defense && xeno.mutation_type == DEFENDER_STEELCREST)
- to_chat(src, SPAN_XENOWARNING("We cannot fortify while our crest is already down!"))
- return
-
if(xeno.crest_defense)
to_chat(src, SPAN_XENOWARNING("We cannot use fortify with our crest lowered."))
return
@@ -201,55 +196,59 @@
if(xeno.fortify)
button.icon_state = "template_active"
-/datum/action/xeno_action/activable/fortify/proc/fortify_switch(mob/living/carbon/xenomorph/X, fortify_state)
- if(X.fortify == fortify_state)
+/datum/action/xeno_action/activable/fortify/proc/fortify_switch(mob/living/carbon/xenomorph/xeno, fortify_state)
+ if(xeno.fortify == fortify_state)
return
if(fortify_state)
- to_chat(X, SPAN_XENOWARNING("We tuck ourself into a defensive stance."))
- if(X.mutation_type == DEFENDER_STEELCREST)
- X.armor_deflection_buff += 10
- X.armor_explosive_buff += 60
- X.ability_speed_modifier += 3
- X.damage_modifier -= XENO_DAMAGE_MOD_SMALL
- else
- X.armor_deflection_buff += 30
- X.armor_explosive_buff += 60
- ADD_TRAIT(X, TRAIT_IMMOBILIZED, TRAIT_SOURCE_ABILITY("Fortify"))
- X.anchored = TRUE
- X.small_explosives_stun = FALSE
+ to_chat(xeno, SPAN_XENOWARNING("We tuck ourself into a defensive stance."))
RegisterSignal(owner, COMSIG_XENO_PRE_CALCULATE_ARMOURED_DAMAGE_PROJECTILE, PROC_REF(check_directional_armor))
- X.mob_size = MOB_SIZE_IMMOBILE //knockback immune
- X.mob_flags &= ~SQUEEZE_UNDER_VEHICLES
- X.update_icons()
- X.fortify = TRUE
+ xeno.mob_size = MOB_SIZE_IMMOBILE //knockback immune
+ xeno.mob_flags &= ~SQUEEZE_UNDER_VEHICLES
+ xeno.fortify = TRUE
else
- to_chat(X, SPAN_XENOWARNING("We resume our normal stance."))
- REMOVE_TRAIT(X, TRAIT_IMMOBILIZED, TRAIT_SOURCE_ABILITY("Fortify"))
- X.anchored = FALSE
- if(X.mutation_type == DEFENDER_STEELCREST)
- X.armor_deflection_buff -= 10
- X.armor_explosive_buff -= 60
- X.ability_speed_modifier -= 3
- X.damage_modifier += XENO_DAMAGE_MOD_SMALL
- else
- X.armor_deflection_buff -= 30
- X.armor_explosive_buff -= 60
- X.small_explosives_stun = TRUE
+ to_chat(xeno, SPAN_XENOWARNING("We resume our normal stance."))
+ REMOVE_TRAIT(xeno, TRAIT_IMMOBILIZED, TRAIT_SOURCE_ABILITY("Fortify"))
+ xeno.anchored = FALSE
UnregisterSignal(owner, COMSIG_XENO_PRE_CALCULATE_ARMOURED_DAMAGE_PROJECTILE)
- X.mob_size = MOB_SIZE_XENO //no longer knockback immune
- X.mob_flags |= SQUEEZE_UNDER_VEHICLES
- X.update_icons()
- X.fortify = FALSE
+ xeno.mob_size = MOB_SIZE_XENO //no longer knockback immune
+ xeno.mob_flags |= SQUEEZE_UNDER_VEHICLES
+ xeno.fortify = FALSE
+
+ apply_modifiers(xeno, fortify_state)
+ xeno.update_icons()
+
+/datum/action/xeno_action/activable/fortify/proc/apply_modifiers(mob/living/carbon/xenomorph/xeno, fortify_state)
+ if(fortify_state)
+ xeno.armor_deflection_buff += 30
+ xeno.armor_explosive_buff += 60
+ ADD_TRAIT(xeno, TRAIT_IMMOBILIZED, TRAIT_SOURCE_ABILITY("Fortify"))
+ xeno.anchored = TRUE
+ xeno.small_explosives_stun = FALSE
+ else
+ xeno.armor_deflection_buff -= 30
+ xeno.armor_explosive_buff -= 60
+ xeno.small_explosives_stun = TRUE
+
+// Steel crest override
+/datum/action/xeno_action/activable/fortify/steel_crest/apply_modifiers(mob/living/carbon/xenomorph/xeno, fortify_state)
+ if(fortify_state)
+ xeno.armor_deflection_buff += 10
+ xeno.armor_explosive_buff += 60
+ xeno.ability_speed_modifier += 3
+ xeno.damage_modifier -= XENO_DAMAGE_MOD_SMALL
+ else
+ xeno.armor_deflection_buff -= 10
+ xeno.armor_explosive_buff -= 60
+ xeno.ability_speed_modifier -= 3
+ xeno.damage_modifier += XENO_DAMAGE_MOD_SMALL
/datum/action/xeno_action/activable/fortify/proc/check_directional_armor(mob/living/carbon/xenomorph/defendy, list/damagedata)
SIGNAL_HANDLER
var/projectile_direction = damagedata["direction"]
+ // If the defender is facing the projectile.
if(defendy.dir & REVERSE_DIR(projectile_direction))
- if(defendy.mutation_type == DEFENDER_STEELCREST)
- damagedata["armor"] += steelcrest_frontal_armor
- else
- damagedata["armor"] += frontal_armor
+ damagedata["armor"] += frontal_armor
/datum/action/xeno_action/activable/fortify/proc/unconscious_check()
SIGNAL_HANDLER
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/lurker/lurker_abilities.dm b/code/modules/mob/living/carbon/xenomorph/abilities/lurker/lurker_abilities.dm
index fd525701b12d..8a829d8d6bc0 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/lurker/lurker_abilities.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/lurker/lurker_abilities.dm
@@ -6,46 +6,11 @@
plasma_cost = 20
// Config options
- distance = 6
knockdown = FALSE
knockdown_duration = 2.5
- freeze_self = TRUE
freeze_time = 15
can_be_shield_blocked = TRUE
-/datum/action/xeno_action/activable/pounce/lurker/additional_effects_always()
- var/mob/living/carbon/xenomorph/xeno = owner
- if (!istype(xeno))
- return
- if (xeno.mutation_type == LURKER_NORMAL)
- var/found = FALSE
- for (var/mob/living/carbon/human/human in get_turf(xeno))
- if(human.stat == DEAD)
- continue
- found = TRUE
- break
-
- if (found)
- var/datum/action/xeno_action/onclick/lurker_invisibility/lurker_invis = get_xeno_action_by_type(xeno, /datum/action/xeno_action/onclick/lurker_invisibility)
- if (istype(lurker_invis))
- lurker_invis.invisibility_off()
-
-/datum/action/xeno_action/activable/pounce/lurker/additional_effects(mob/living/living_mob)
- var/mob/living/carbon/xenomorph/xeno = owner
- if (!istype(xeno))
- return
-
- if (xeno.mutation_type == LURKER_NORMAL)
- RegisterSignal(xeno, COMSIG_XENO_SLASH_ADDITIONAL_EFFECTS_SELF, PROC_REF(remove_freeze), TRUE) // Suppresses runtime ever we pounce again before slashing
-
-/datum/action/xeno_action/activable/pounce/lurker/proc/remove_freeze(mob/living/carbon/xenomorph/xeno)
- SIGNAL_HANDLER
-
- var/datum/behavior_delegate/lurker_base/behaviour_del = xeno.behavior_delegate
- if (istype(behaviour_del))
- UnregisterSignal(xeno, COMSIG_XENO_SLASH_ADDITIONAL_EFFECTS_SELF)
- end_pounce_freeze()
-
/datum/action/xeno_action/onclick/lurker_invisibility
name = "Turn Invisible"
action_icon_state = "lurker_invisibility"
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/lurker/lurker_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/lurker/lurker_powers.dm
index 24aa94727ca9..373c2701ce37 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/lurker/lurker_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/lurker/lurker_powers.dm
@@ -1,3 +1,34 @@
+/datum/action/xeno_action/activable/pounce/lurker/additional_effects_always()
+ var/mob/living/carbon/xenomorph/xeno = owner
+ if(!istype(xeno))
+ return
+
+ var/found = FALSE
+ for(var/mob/living/carbon/human/human in get_turf(xeno))
+ if(human.stat == DEAD)
+ continue
+ found = TRUE
+ break
+
+ if(found)
+ var/datum/action/xeno_action/onclick/lurker_invisibility/lurker_invis = get_xeno_action_by_type(xeno, /datum/action/xeno_action/onclick/lurker_invisibility)
+ lurker_invis.invisibility_off()
+
+/datum/action/xeno_action/activable/pounce/lurker/additional_effects(mob/living/living_mob)
+ var/mob/living/carbon/xenomorph/xeno = owner
+ if(!istype(xeno))
+ return
+
+ RegisterSignal(xeno, COMSIG_XENO_SLASH_ADDITIONAL_EFFECTS_SELF, PROC_REF(remove_freeze), TRUE) // Suppresses runtime ever we pounce again before slashing
+
+/datum/action/xeno_action/activable/pounce/lurker/proc/remove_freeze(mob/living/carbon/xenomorph/xeno)
+ SIGNAL_HANDLER
+
+ var/datum/behavior_delegate/lurker_base/behaviour_del = xeno.behavior_delegate
+ if(istype(behaviour_del))
+ UnregisterSignal(xeno, COMSIG_XENO_SLASH_ADDITIONAL_EFFECTS_SELF)
+ end_pounce_freeze()
+
/datum/action/xeno_action/onclick/lurker_invisibility/use_ability(atom/targeted_atom)
var/mob/living/carbon/xenomorph/xeno = owner
@@ -16,9 +47,8 @@
xeno.speed_modifier -= speed_buff
xeno.recalculate_speed()
- if (xeno.mutation_type == LURKER_NORMAL)
- var/datum/behavior_delegate/lurker_base/behavior = xeno.behavior_delegate
- behavior.on_invisibility()
+ var/datum/behavior_delegate/lurker_base/behavior = xeno.behavior_delegate
+ behavior.on_invisibility()
// if we go off early, this also works fine.
invis_timer_id = addtimer(CALLBACK(src, PROC_REF(invisibility_off)), duration, TIMER_STOPPABLE)
@@ -45,10 +75,9 @@
xeno.speed_modifier += speed_buff
xeno.recalculate_speed()
- if (xeno.mutation_type == LURKER_NORMAL)
- var/datum/behavior_delegate/lurker_base/behavior = xeno.behavior_delegate
- if (istype(behavior))
- behavior.on_invisibility_off()
+ var/datum/behavior_delegate/lurker_base/behavior = xeno.behavior_delegate
+ if (istype(behavior))
+ behavior.on_invisibility_off()
/datum/action/xeno_action/onclick/lurker_invisibility/ability_cooldown_over()
to_chat(owner, SPAN_XENOHIGHDANGER("We are ready to use our invisibility again!"))
@@ -66,9 +95,6 @@
if (!check_and_use_plasma_owner())
return
- if (xeno.mutation_type != LURKER_NORMAL)
- return
-
var/datum/behavior_delegate/lurker_base/behavior = xeno.behavior_delegate
if (istype(behavior))
behavior.next_slash_buffed = TRUE
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/praetorian/praetorian_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/praetorian/praetorian_powers.dm
index 605fe290b468..647bf7936195 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/praetorian/praetorian_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/praetorian/praetorian_powers.dm
@@ -70,10 +70,9 @@
playsound(current_mob, 'sound/weapons/alien_tail_attack.ogg', 30, TRUE)
if (target_mobs.len >= shield_regen_threshold)
- if (source_xeno.mutation_type == PRAETORIAN_VANGUARD)
- var/datum/behavior_delegate/praetorian_vanguard/BD = source_xeno.behavior_delegate
- if (istype(BD))
- BD.regen_shield()
+ var/datum/behavior_delegate/praetorian_vanguard/behavior = source_xeno.behavior_delegate
+ if (istype(behavior))
+ behavior.regen_shield()
apply_cooldown()
return ..()
@@ -135,10 +134,9 @@
playsound(get_turf(H), "alien_claw_flesh", 30, 1)
if (target_mobs.len >= shield_regen_threshold)
- if (X.mutation_type == PRAETORIAN_VANGUARD)
- var/datum/behavior_delegate/praetorian_vanguard/BD = X.behavior_delegate
- if (istype(BD))
- BD.regen_shield()
+ var/datum/behavior_delegate/praetorian_vanguard/behavior = X.behavior_delegate
+ if (istype(behavior))
+ behavior.regen_shield()
/datum/action/xeno_action/activable/cleave/use_ability(atom/target_atom)
var/mob/living/carbon/xenomorph/vanguard_user = owner
@@ -545,16 +543,12 @@
if (!check_and_use_plasma_owner())
return
- var/buffed = FALSE
apply_cooldown()
- if (dancer_user.mutation_type == PRAETORIAN_DANCER)
- var/found = FALSE
- for (var/datum/effects/dancer_tag/dancer_tag_effect in target_carbon.effects_list)
- found = TRUE
- qdel(dancer_tag_effect)
- break
-
- buffed = found
+ var/buffed = FALSE
+ for (var/datum/effects/dancer_tag/dancer_tag_effect in target_carbon.effects_list)
+ buffed = TRUE
+ qdel(dancer_tag_effect)
+ break
if(ishuman(target_carbon))
var/mob/living/carbon/human/Hu = target_carbon
@@ -601,9 +595,6 @@
if (!check_and_use_plasma_owner())
return
- if (xeno.mutation_type != PRAETORIAN_DANCER)
- return
-
var/datum/behavior_delegate/praetorian_dancer/behavior = xeno.behavior_delegate
if (!istype(behavior))
return
@@ -626,9 +617,6 @@
if (!istype(xeno))
return
- if (xeno.mutation_type != PRAETORIAN_DANCER)
- return
-
var/datum/behavior_delegate/praetorian_dancer/behavior = xeno.behavior_delegate
if (!istype(behavior))
return
@@ -806,17 +794,16 @@
var/bonus_shield = 0
- if (X.mutation_type == PRAETORIAN_WARDEN)
- var/datum/behavior_delegate/praetorian_warden/BD = X.behavior_delegate
- if (!istype(BD))
- return
+ var/datum/behavior_delegate/praetorian_warden/behavior = X.behavior_delegate
+ if (!istype(behavior))
+ return
- if (!BD.use_internal_hp_ability(shield_cost))
- return
+ if (!behavior.use_internal_hp_ability(shield_cost))
+ return
- bonus_shield = BD.internal_hitpoints*0.5
- if (!BD.use_internal_hp_ability(bonus_shield))
- bonus_shield = 0
+ bonus_shield = behavior.internal_hitpoints*0.5
+ if (!behavior.use_internal_hp_ability(bonus_shield))
+ bonus_shield = 0
var/total_shield_amount = shield_amount + bonus_shield
@@ -841,7 +828,7 @@
if (!X.Adjacent(A))
to_chat(X, SPAN_XENODANGER("We must be within touching distance of [targetXeno]!"))
return
- if (targetXeno.mutation_type == PRAETORIAN_WARDEN)
+ if(istype(targetXeno.strain, /datum/xeno_strain/warden))
to_chat(X, SPAN_XENODANGER("We cannot heal a sister of the same strain!"))
return
if (SEND_SIGNAL(targetXeno, COMSIG_XENO_PRE_HEAL) & COMPONENT_CANCEL_XENO_HEAL)
@@ -849,18 +836,16 @@
return
var/bonus_heal = 0
+ var/datum/behavior_delegate/praetorian_warden/behavior = X.behavior_delegate
+ if (!istype(behavior))
+ return
- if (X.mutation_type == PRAETORIAN_WARDEN)
- var/datum/behavior_delegate/praetorian_warden/BD = X.behavior_delegate
- if (!istype(BD))
- return
-
- if (!BD.use_internal_hp_ability(heal_cost))
- return
+ if (!behavior.use_internal_hp_ability(heal_cost))
+ return
- bonus_heal = BD.internal_hitpoints*0.5
- if (!BD.use_internal_hp_ability(bonus_heal))
- bonus_heal = 0
+ bonus_heal = behavior.internal_hitpoints*0.5
+ if (!behavior.use_internal_hp_ability(bonus_heal))
+ bonus_heal = 0
to_chat(X, SPAN_XENODANGER("We heal [targetXeno]!"))
to_chat(targetXeno, SPAN_XENOHIGHDANGER("We are healed by [X]!"))
@@ -868,9 +853,7 @@
targetXeno.visible_message(SPAN_BOLDNOTICE("[X] places its claws on [targetXeno], and its wounds are quickly sealed!")) //marines probably should know if a xeno gets healed
X.gain_health(heal_amount*0.5 + bonus_heal*0.5)
X.flick_heal_overlay(3 SECONDS, "#00B800")
- if(X.mutation_type == PRAETORIAN_WARDEN)
- var/datum/behavior_delegate/praetorian_warden/warden_delegate = X.behavior_delegate
- warden_delegate.transferred_healing += heal_amount
+ behavior.transferred_healing += heal_amount
use_plasma = TRUE //it's already hard enough to gauge health without hp showing on the mob
targetXeno.flick_heal_overlay(3 SECONDS, "#00B800")//so the visible_message and recovery overlay will warn marines and possibly predators that the xenomorph has been healed!
@@ -879,13 +862,12 @@
to_chat(X, SPAN_XENOHIGHDANGER("We cannot rejuvenate targets through overwatch!"))
return
- if (X.mutation_type == PRAETORIAN_WARDEN)
- var/datum/behavior_delegate/praetorian_warden/BD = X.behavior_delegate
- if (!istype(BD))
- return
+ var/datum/behavior_delegate/praetorian_warden/behavior = X.behavior_delegate
+ if (!istype(behavior))
+ return
- if (!BD.use_internal_hp_ability(debuff_cost))
- return
+ if (!behavior.use_internal_hp_ability(debuff_cost))
+ return
to_chat(X, SPAN_XENODANGER("We rejuvenate [targetXeno]!"))
to_chat(targetXeno, SPAN_XENOHIGHDANGER("We are rejuvenated by [X]!"))
@@ -910,8 +892,8 @@
if(!istype(X))
return
- var/datum/behavior_delegate/praetorian_warden/BD = X.behavior_delegate
- if(!istype(BD))
+ var/datum/behavior_delegate/praetorian_warden/behavior = X.behavior_delegate
+ if(!istype(behavior))
return
if(X.observed_xeno != null)
@@ -954,7 +936,7 @@
if(!check_plasma_owner())
return
- if(!BD.use_internal_hp_ability(retrieve_cost))
+ if(!behavior.use_internal_hp_ability(retrieve_cost))
return
if(!check_and_use_plasma_owner())
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_abilities.dm b/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_abilities.dm
index d245449fa2cf..9ec61b4c95ca 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_abilities.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_abilities.dm
@@ -62,6 +62,10 @@
. = ..()
SSticker.OnRoundstart(CALLBACK(src, PROC_REF(apply_queen_build_boost)))
+// queenos don't need weeds under them to build on ovi
+/datum/action/xeno_action/activable/secrete_resin/remote/queen/can_remote_build()
+ return TRUE
+
/datum/action/xeno_action/activable/secrete_resin/remote/queen/proc/apply_queen_build_boost()
var/boost_duration = 30 MINUTES
// In the event secrete_resin is given after round start
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/ravager/ravager_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/ravager/ravager_powers.dm
index daad0362e91e..8fe101a08dfa 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/ravager/ravager_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/ravager/ravager_powers.dm
@@ -6,9 +6,6 @@
if(!xeno.check_state())
return
- if(xeno.mutation_type != RAVAGER_NORMAL)
- return
-
if(!action_cooldown_check())
return
@@ -122,8 +119,6 @@
var/mob/living/carbon/human/human = living
var/mob/living/carbon/xenomorph/xeno = owner
- if(xeno.mutation_type != RAVAGER_NORMAL)
- return
var/datum/behavior_delegate/ravager_base/behavior = xeno.behavior_delegate
if(behavior.empower_targets < behavior.super_empower_threshold)
return
@@ -146,8 +141,6 @@
// Determine whether or not we should daze here
var/should_sslow = FALSE
- if(ravager_user.mutation_type != RAVAGER_NORMAL)
- return
var/datum/behavior_delegate/ravager_base/ravager_delegate = ravager_user.behavior_delegate
if(ravager_delegate.empower_targets >= ravager_delegate.super_empower_threshold)
should_sslow = TRUE
@@ -302,17 +295,14 @@
if (carbon.stat == DEAD)
return
- // All strain-specific behavior
- if (xeno.mutation_type == RAVAGER_BERSERKER)
- var/datum/behavior_delegate/ravager_berserker/behavior = xeno.behavior_delegate
-
- if (behavior.rage >= 2)
- behavior.decrement_rage()
- heal_amount += additional_healing_enraged
- else
- to_chat(xeno, SPAN_XENOWARNING("Our rejuvenation was weaker without rage!"))
- debilitate = FALSE
- fling_distance--
+ var/datum/behavior_delegate/ravager_berserker/behavior = xeno.behavior_delegate
+ if (behavior.rage >= 2)
+ behavior.decrement_rage()
+ heal_amount += additional_healing_enraged
+ else
+ to_chat(xeno, SPAN_XENOWARNING("Our rejuvenation was weaker without rage!"))
+ debilitate = FALSE
+ fling_distance--
// Damage
var/obj/limb/head/head = carbon.get_limb("head")
@@ -361,17 +351,16 @@
var/max_lifesteal = 250
var/lifesteal_range = 1
- if (xeno.mutation_type == RAVAGER_BERSERKER)
- var/datum/behavior_delegate/ravager_berserker/behavior = xeno.behavior_delegate
- if (behavior.rage == 0)
- to_chat(xeno, SPAN_XENODANGER("We cannot eviscerate when we have 0 rage!"))
- return
- damage = damage_at_rage_levels[clamp(behavior.rage, 1, behavior.max_rage)]
- range = range_at_rage_levels[clamp(behavior.rage, 1, behavior.max_rage)]
- windup_reduction = windup_reduction_at_rage_levels[clamp(behavior.rage, 1, behavior.max_rage)]
- behavior.decrement_rage(behavior.rage)
+ var/datum/behavior_delegate/ravager_berserker/behavior = xeno.behavior_delegate
+ if (behavior.rage == 0)
+ to_chat(xeno, SPAN_XENODANGER("We cannot eviscerate when we have 0 rage!"))
+ return
+ damage = damage_at_rage_levels[clamp(behavior.rage, 1, behavior.max_rage)]
+ range = range_at_rage_levels[clamp(behavior.rage, 1, behavior.max_rage)]
+ windup_reduction = windup_reduction_at_rage_levels[clamp(behavior.rage, 1, behavior.max_rage)]
+ behavior.decrement_rage(behavior.rage)
- apply_cooldown()
+ apply_cooldown()
if (range > 1)
xeno.visible_message(SPAN_XENOHIGHDANGER("[xeno] begins digging in for a massive strike!"), SPAN_XENOHIGHDANGER("We begin digging in for a massive strike!"))
@@ -439,12 +428,11 @@
if (!xeno.check_state())
return
- if (xeno.mutation_type == RAVAGER_HEDGEHOG)
- var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
- if (!behavior.check_shards(shard_cost))
- to_chat(xeno, SPAN_DANGER("Not enough shards! We need [shard_cost - behavior.shards] more!"))
- return
- behavior.use_shards(shard_cost)
+ var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
+ if (!behavior.check_shards(shard_cost))
+ to_chat(xeno, SPAN_DANGER("Not enough shards! We need [shard_cost - behavior.shards] more!"))
+ return
+ behavior.use_shards(shard_cost)
xeno.visible_message(SPAN_XENODANGER("[xeno] ruffles its bone-shard quills, forming a defensive shell!"), SPAN_XENODANGER("We ruffle our bone-shard quills, forming a defensive shell!"))
@@ -468,10 +456,8 @@
return FALSE
else if (cooldown_timer_id == TIMER_ID_NULL)
var/mob/living/carbon/xenomorph/xeno = owner
- if (xeno.mutation_type == RAVAGER_HEDGEHOG)
- var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
- return behavior.check_shards(shard_cost)
- return TRUE
+ var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
+ return behavior.check_shards(shard_cost)
return FALSE
/datum/action/xeno_action/onclick/spike_shield/proc/remove_shield()
@@ -502,12 +488,11 @@
if(!affected_atom || affected_atom.layer >= FLY_LAYER || !isturf(xeno.loc) || !xeno.check_state())
return
- if (xeno.mutation_type == RAVAGER_HEDGEHOG)
- var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
- if (!behavior.check_shards(shard_cost))
- to_chat(xeno, SPAN_DANGER("Not enough shards! We need [shard_cost - behavior.shards] more!"))
- return
- behavior.use_shards(shard_cost)
+ var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
+ if (!behavior.check_shards(shard_cost))
+ to_chat(xeno, SPAN_DANGER("Not enough shards! We need [shard_cost - behavior.shards] more!"))
+ return
+ behavior.use_shards(shard_cost)
xeno.visible_message(SPAN_XENOWARNING("[xeno] fires their spikes at [affected_atom]!"), SPAN_XENOWARNING("We fire our spikes at [affected_atom]!"))
@@ -531,11 +516,8 @@
var/mob/living/carbon/xenomorph/xeno = owner
if(!istype(xeno))
return FALSE
- if (xeno.mutation_type == RAVAGER_HEDGEHOG)
- var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
- return behavior.check_shards(shard_cost)
-
- return TRUE
+ var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
+ return behavior.check_shards(shard_cost)
else
return FALSE
@@ -548,13 +530,12 @@
if (!xeno.check_state())
return
- if (xeno.mutation_type == RAVAGER_HEDGEHOG)
- var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
- if (!behavior.check_shards(shard_cost))
- to_chat(xeno, SPAN_DANGER("Not enough shards! We need [shard_cost - behavior.shards] more!"))
- return
- behavior.use_shards(shard_cost)
- behavior.lock_shards()
+ var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
+ if (!behavior.check_shards(shard_cost))
+ to_chat(xeno, SPAN_DANGER("Not enough shards! We need [shard_cost - behavior.shards] more!"))
+ return
+ behavior.use_shards(shard_cost)
+ behavior.lock_shards()
xeno.visible_message(SPAN_XENOWARNING("[xeno] sheds their spikes, firing them in all directions!"), SPAN_XENOWARNING("We shed our spikes, firing them in all directions!!"))
xeno.spin_circle()
@@ -567,10 +548,7 @@
/datum/action/xeno_action/onclick/spike_shed/action_cooldown_check()
if (cooldown_timer_id == TIMER_ID_NULL)
var/mob/living/carbon/xenomorph/xeno = owner
- if (xeno.mutation_type == RAVAGER_HEDGEHOG)
- var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
- return behavior.check_shards(shard_cost)
-
- return TRUE
+ var/datum/behavior_delegate/ravager_hedgehog/behavior = xeno.behavior_delegate
+ return behavior.check_shards(shard_cost)
else
return FALSE
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/runner/runner_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/runner/runner_powers.dm
index b907a382dda1..f5e090bc7e4b 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/runner/runner_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/runner/runner_powers.dm
@@ -45,7 +45,7 @@
return ..()
/mob/living/carbon/xenomorph/runner/corrosive_acid(atom/affected_atom, acid_type, plasma_cost)
- if (mutation_type != RUNNER_ACIDER)
+ if(!istype(strain, /datum/xeno_strain/acider))
return ..()
if(!affected_atom.Adjacent(src))
if(istype(affected_atom,/obj/item/explosive/plastic))
@@ -159,9 +159,6 @@
if(!action_cooldown_check())
return
- if(xeno.mutation_type != RUNNER_ACIDER)
- return
-
var/datum/behavior_delegate/runner_acider/behavior_delegate = xeno.behavior_delegate
if(!istype(behavior_delegate))
return
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/sentinel/sentinel_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/sentinel/sentinel_powers.dm
index 1ddec771d075..1ed8863c231a 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/sentinel/sentinel_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/sentinel/sentinel_powers.dm
@@ -35,9 +35,6 @@
if(!xeno.check_state())
return
- if(xeno.mutation_type != SENTINEL_NORMAL)
- return
-
if(!action_cooldown_check())
to_chat(src, SPAN_WARNING("We must wait for your spit glands to refill."))
return
@@ -77,9 +74,6 @@
if (!check_and_use_plasma_owner())
return
- if (xeno.mutation_type != SENTINEL_NORMAL)
- return
-
var/datum/behavior_delegate/sentinel_base/behavior = xeno.behavior_delegate
if (istype(behavior))
behavior.next_slash_buffed = TRUE
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Boiler.dm b/code/modules/mob/living/carbon/xenomorph/castes/Boiler.dm
index 074af92d69fa..f7e906a82b28 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Boiler.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Boiler.dm
@@ -13,6 +13,7 @@
evasion = XENO_EVASION_NONE
speed = XENO_SPEED_TIER_3
+ available_strains = list(/datum/xeno_strain/trapper)
behavior_delegate_type = /datum/behavior_delegate/boiler_base
evolution_allowed = FALSE
@@ -50,7 +51,6 @@
tier = 3
gib_chance = 100
drag_delay = 6 //pulling a big dead xeno is hard
- mutation_type = BOILER_NORMAL
spit_delay = 30 SECONDS
tileoffset = 3
viewsize = 7
@@ -98,4 +98,3 @@
// No special behavior for boilers
/datum/behavior_delegate/boiler_base
name = "Base Boiler Behavior Delegate"
-
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Burrower.dm b/code/modules/mob/living/carbon/xenomorph/castes/Burrower.dm
index 886ffcfe1ef2..bec41a87521e 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Burrower.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Burrower.dm
@@ -74,8 +74,6 @@
/mob/living/carbon/xenomorph/proc/set_hugger_reserve_for_morpher,
)
- mutation_type = BURROWER_NORMAL
-
icon_xeno = 'icons/mob/xenos/burrower.dmi'
icon_xenonid = 'icons/mob/xenonids/burrower.dmi'
@@ -109,11 +107,3 @@
/datum/behavior_delegate/burrower_base
name = "Base Burrower Behavior Delegate"
-
-/datum/behavior_delegate/burrower_base/on_update_icons()
- if(bound_xeno.stat == DEAD)
- return
-
- if(HAS_TRAIT(bound_xeno, TRAIT_ABILITY_BURROWED))
- bound_xeno.icon_state = "[bound_xeno.mutation_icon_state] Burrower Burrowed"
- return TRUE
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Carrier.dm b/code/modules/mob/living/carbon/xenomorph/castes/Carrier.dm
index 2e106743514a..d290b917e945 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Carrier.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Carrier.dm
@@ -14,6 +14,9 @@
evasion = XENO_EVASION_NONE
speed = XENO_SPEED_TIER_4
+ available_strains = list(/datum/xeno_strain/eggsac)
+ behavior_delegate_type = /datum/behavior_delegate/carrier_base
+
evolution_allowed = FALSE
deevolves_to = list(XENO_CASTE_DRONE)
throwspeed = SPEED_AVERAGE
@@ -75,7 +78,6 @@
/mob/living/carbon/xenomorph/proc/rename_tunnel,
/mob/living/carbon/xenomorph/proc/set_hugger_reserve_for_morpher,
)
- mutation_type = CARRIER_NORMAL
icon_xenonid = 'icons/mob/xenonids/carrier.dmi'
@@ -95,18 +97,9 @@
var/eggs_max = 0
var/laid_egg = 0
-/mob/living/carbon/xenomorph/carrier/update_icons()
- . = ..()
- if (mutation_type == CARRIER_NORMAL)
- update_hugger_overlays()
- if (mutation_type == CARRIER_EGGSAC)
- update_eggsac_overlays()
-
/mob/living/carbon/xenomorph/carrier/proc/update_hugger_overlays()
if(!hugger_overlays_icon)
return
- if(mutation_type != CARRIER_NORMAL)
- return
overlays -= hugger_overlays_icon
hugger_overlays_icon.overlays.Cut()
@@ -146,8 +139,6 @@
/mob/living/carbon/xenomorph/carrier/proc/update_eggsac_overlays()
if(!eggsac_overlays_icon)
return
- if(mutation_type != CARRIER_EGGSAC)
- return
overlays -= eggsac_overlays_icon
eggsac_overlays_icon.overlays.Cut()
@@ -184,9 +175,6 @@
. = ..(cause, gibbed)
if(.)
var/chance = 75 //75% to drop an egg or hugger.
- if(mutation_type == CARRIER_EGGSAC)
- visible_message(SPAN_XENOWARNING("[src] throes as its eggsac bursts into a mess of acid!"))
- playsound(src.loc, 'sound/effects/alien_egg_burst.ogg', 25, 1)
if(huggers_cur)
//Hugger explosion, like an egg morpher
@@ -207,6 +195,11 @@
if(eggs_dropped) //Checks whether or not to announce egg drop.
xeno_message(SPAN_XENOANNOUNCE("[src] has dropped some precious eggs!"), 2, hive.hivenumber)
+/mob/living/carbon/xenomorph/carrier/recalculate_actions()
+ . = ..()
+ huggers_max = caste.huggers_max
+ eggs_max = caste.eggs_max
+
/mob/living/carbon/xenomorph/carrier/get_status_tab_items()
. = ..()
if(huggers_max > 0)
@@ -397,3 +390,10 @@
return
GLOB.hive_datum[hivenumber].spawn_as_hugger(user, src)
huggers_cur--
+
+/datum/behavior_delegate/carrier_base
+ name = "Base Carrier Behavior Delegate"
+
+/datum/behavior_delegate/carrier_base/on_update_icons()
+ var/mob/living/carbon/xenomorph/carrier/bound_carrier = bound_xeno
+ bound_carrier.update_hugger_overlays()
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Crusher.dm b/code/modules/mob/living/carbon/xenomorph/castes/Crusher.dm
index d2c0b0e40e59..38b1e5816ffe 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Crusher.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Crusher.dm
@@ -14,6 +14,7 @@
speed = XENO_SPEED_TIER_2
heal_standing = 0.66
+ available_strains = list(/datum/xeno_strain/charger)
behavior_delegate_type = /datum/behavior_delegate/crusher_base
minimum_evolve_time = 15 MINUTES
@@ -63,8 +64,6 @@
)
claw_type = CLAW_TYPE_VERY_SHARP
- mutation_icon_state = CRUSHER_NORMAL
- mutation_type = CRUSHER_NORMAL
icon_xeno = 'icons/mob/xenos/crusher.dmi'
icon_xenonid = 'icons/mob/xenonids/crusher.dmi'
@@ -281,5 +280,5 @@
/datum/behavior_delegate/crusher_base/on_update_icons()
if(bound_xeno.throwing || is_charging) //Let it build up a bit so we're not changing icons every single turf
- bound_xeno.icon_state = "[bound_xeno.mutation_icon_state || bound_xeno.mutation_type] Crusher Charging"
+ bound_xeno.icon_state = "[bound_xeno.get_strain_icon()] Crusher Charging"
return TRUE
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Defender.dm b/code/modules/mob/living/carbon/xenomorph/castes/Defender.dm
index 8ec53d51046e..a63bafb5d2b0 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Defender.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Defender.dm
@@ -18,6 +18,7 @@
deevolves_to = list("Larva")
can_vent_crawl = 0
+ available_strains = list(/datum/xeno_strain/steel_crest)
behavior_delegate_type = /datum/behavior_delegate/defender_base
tackle_min = 2
@@ -51,9 +52,6 @@
/datum/action/xeno_action/onclick/tacmap,
)
- mutation_icon_state = DEFENDER_NORMAL
- mutation_type = DEFENDER_NORMAL
-
icon_xeno = 'icons/mob/xenos/defender.dmi'
icon_xenonid = 'icons/mob/xenonids/defender.dmi'
@@ -90,8 +88,8 @@
return
if(bound_xeno.fortify && bound_xeno.health > 0)
- bound_xeno.icon_state = "[bound_xeno.mutation_icon_state || bound_xeno.mutation_type] Defender Fortify"
+ bound_xeno.icon_state = "[bound_xeno.get_strain_icon()] Defender Fortify"
return TRUE
if(bound_xeno.crest_defense && bound_xeno.health > 0)
- bound_xeno.icon_state = "[bound_xeno.mutation_icon_state || bound_xeno.mutation_type] Defender Crest"
+ bound_xeno.icon_state = "[bound_xeno.get_strain_icon()] Defender Crest"
return TRUE
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Drone.dm b/code/modules/mob/living/carbon/xenomorph/castes/Drone.dm
index c4c9b11b37e4..a0ce70316eb8 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Drone.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Drone.dm
@@ -12,6 +12,11 @@
evasion = XENO_EVASION_MEDIUM
speed = XENO_SPEED_TIER_7
+ available_strains = list(
+ /datum/xeno_strain/gardener,
+ /datum/xeno_strain/healer,
+ )
+
build_time_mult = BUILD_TIME_MULT_BUILDER
caste_desc = "A builder of hives. Only drones may evolve into Queens."
@@ -69,7 +74,6 @@
/mob/living/carbon/xenomorph/proc/rename_tunnel,
/mob/living/carbon/xenomorph/proc/set_hugger_reserve_for_morpher,
)
- mutation_type = DRONE_NORMAL
icon_xeno = 'icons/mob/xenos/drone.dmi'
icon_xenonid = 'icons/mob/xenonids/drone.dmi'
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Facehugger.dm b/code/modules/mob/living/carbon/xenomorph/castes/Facehugger.dm
index 7ce3a8750568..d7702b9d8ee1 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Facehugger.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Facehugger.dm
@@ -9,6 +9,9 @@
caste_desc = "Ewwww, that's disgusting!"
speed = XENO_SPEED_TIER_8
+ available_strains = list(/datum/xeno_strain/watcher)
+ behavior_delegate_type = /datum/behavior_delegate/facehugger_base
+
evolution_allowed = FALSE
can_be_revived = FALSE
@@ -55,7 +58,6 @@
inherent_verbs = list(
/mob/living/carbon/xenomorph/proc/vent_crawl,
)
- mutation_type = "Normal"
icon_xeno = 'icons/mob/xenos/facehugger.dmi'
icon_xenonid = 'icons/mob/xenonids/facehugger.dmi'
@@ -64,6 +66,20 @@
weed_food_states = list("Facehugger_1","Facehugger_2","Facehugger_3")
weed_food_states_flipped = list("Facehugger_1","Facehugger_2","Facehugger_3")
+/mob/living/carbon/xenomorph/facehugger/Login()
+ var/last_ckey_inhabited = persistent_ckey
+ . = ..()
+ if(ckey == last_ckey_inhabited)
+ return
+
+ AddComponent(\
+ /datum/component/temporary_mute,\
+ "We aren't old enough to vocalize anything yet.",\
+ "We aren't old enough to communicate like this yet.",\
+ "We feel old enough to be able to vocalize and speak to the hivemind.",\
+ 3 MINUTES,\
+ )
+
/mob/living/carbon/xenomorph/facehugger/initialize_pass_flags(datum/pass_flags_container/PF)
..()
if (PF)
@@ -74,34 +90,16 @@
if(stat == DEAD)
return ..()
- if(body_position == STANDING_UP && !(mutation_type == FACEHUGGER_WATCHER) && !(locate(/obj/effect/alien/weeds) in get_turf(src)))
- adjustBruteLoss(1)
- return ..()
-
if(!client && !aghosted && away_timer > XENO_FACEHUGGER_LEAVE_TIMER)
// Become a npc once again
new /obj/item/clothing/mask/facehugger(loc, hivenumber)
qdel(src)
return ..()
-/mob/living/carbon/xenomorph/facehugger/update_icons(is_pouncing)
- if(!caste)
- return
-
- if(stat == DEAD)
- icon_state = "[mutation_type] [caste.caste_type] Dead"
- else if(body_position == LYING_DOWN)
- if(!HAS_TRAIT(src, TRAIT_INCAPACITATED) && !HAS_TRAIT(src, TRAIT_FLOORED))
- icon_state = "[mutation_type] [caste.caste_type] Sleeping"
- else
- icon_state = "[mutation_type] [caste.caste_type] Knocked Down"
- else if(is_pouncing)
- icon_state = "[mutation_type] [caste.caste_type] Thrown"
- else
- icon_state = "[mutation_type] [caste.caste_type] Running"
-
- update_fire() //the fire overlay depends on the xeno's stance, so we must update it.
- update_wounds()
+/mob/living/carbon/xenomorph/facehugger/update_icons()
+ . = ..()
+ if(throwing)
+ icon_state = "[get_strain_icon()] [caste.caste_type] Thrown"
/mob/living/carbon/xenomorph/facehugger/start_pulling(atom/movable/AM)
return
@@ -235,34 +233,9 @@
else
. += "Lifetime Hugs: [total_facehugs]"
+/datum/behavior_delegate/facehugger_base
+ name = "Base Facehugger Behavior Delegate"
-/datum/xeno_mutator/watcher
- name = "STRAIN: Facehugger - Watcher"
- description = "You lose your ability to hide in exchange to see further and the ability to no longer take damage outside of weeds. This enables you to stalk your host from a distance and wait for the perfect oppertunity to strike."
- flavor_description = "No need to hide when you can see the danger."
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_FACEHUGGER)
- mutator_actions_to_remove = list(
- /datum/action/xeno_action/onclick/xenohide,
- )
- mutator_actions_to_add = list(
- /datum/action/xeno_action/onclick/toggle_long_range/runner,
- )
-
- cost = 1
-
- keystone = TRUE
-
-/datum/xeno_mutator/watcher/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if(!.)
- return
-
- var/mob/living/carbon/xenomorph/facehugger/facehugger = mutator_set.xeno
-
- facehugger.viewsize = 10
- facehugger.layer = MOB_LAYER
-
- facehugger.mutation_type = FACEHUGGER_WATCHER
- mutator_update_actions(facehugger)
- mutator_set.recalculate_actions(description, flavor_description)
+/datum/behavior_delegate/facehugger_base/on_life()
+ if(bound_xeno.body_position == STANDING_UP && !(locate(/obj/effect/alien/weeds) in get_turf(bound_xeno)))
+ bound_xeno.adjustBruteLoss(1)
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Hellhound.dm b/code/modules/mob/living/carbon/xenomorph/castes/Hellhound.dm
index 6868fd5ac589..93d40820bf7b 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Hellhound.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Hellhound.dm
@@ -64,7 +64,6 @@
inherent_verbs = list(
/mob/living/carbon/xenomorph/proc/vent_crawl,
)
- mutation_type = HELLHOUND_NORMAL
icon_xeno = 'icons/mob/xenos/hellhound.dmi'
icon_xenonid = 'icons/mob/xenos/hellhound.dmi'
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Hivelord.dm b/code/modules/mob/living/carbon/xenomorph/castes/Hivelord.dm
index b00ec2a9c1eb..ee4157a67d84 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Hivelord.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Hivelord.dm
@@ -13,6 +13,8 @@
evasion = XENO_EVASION_NONE
speed = XENO_SPEED_TIER_2
+ available_strains = list(/datum/xeno_strain/resin_whisperer)
+
evolution_allowed = FALSE
caste_desc = "A builder of really big hives."
deevolves_to = list(XENO_CASTE_DRONE)
@@ -76,8 +78,6 @@
/mob/living/carbon/xenomorph/proc/set_hugger_reserve_for_morpher,
)
- mutation_type = HIVELORD_NORMAL
-
icon_xeno = 'icons/mob/xenos/hivelord.dmi'
icon_xenonid = 'icons/mob/xenonids/hivelord.dmi'
@@ -121,10 +121,3 @@
toggle_resin_walker()
to_chat(bound_xeno, SPAN_WARNING("You feel dizzy as the world slows down."))
bound_xeno.recalculate_move_delay = TRUE
-
-/// This check mainly exists because of the new resin node ability for resin whisperer.
-/mob/living/carbon/xenomorph/hivelord/proc/on_weeds()
- var/turf/turf = get_turf(src)
- if(locate(/obj/effect/alien/weeds) in turf)
- return TRUE
- return FALSE
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Larva.dm b/code/modules/mob/living/carbon/xenomorph/castes/Larva.dm
index f1c77e7fb757..c06b8c43839d 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Larva.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Larva.dm
@@ -46,7 +46,6 @@
inherent_verbs = list(
/mob/living/carbon/xenomorph/proc/vent_crawl,
)
- mutation_type = "Normal"
var/burrowable = TRUE //Can it be safely burrowed if it has no player?
var/state_override
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Lurker.dm b/code/modules/mob/living/carbon/xenomorph/castes/Lurker.dm
index 469b9a78d063..b9bde4c78992 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Lurker.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Lurker.dm
@@ -15,6 +15,7 @@
attack_delay = 2 // VERY high slash damage, but attacks relatively slowly
+ available_strains = list(/datum/xeno_strain/vampire)
behavior_delegate_type = /datum/behavior_delegate/lurker_base
deevolves_to = list(XENO_CASTE_RUNNER)
@@ -50,7 +51,6 @@
inherent_verbs = list(
/mob/living/carbon/xenomorph/proc/vent_crawl,
)
- mutation_type = LURKER_NORMAL
claw_type = CLAW_TYPE_SHARP
tackle_min = 2
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Praetorian.dm b/code/modules/mob/living/carbon/xenomorph/castes/Praetorian.dm
index 9536ef1553e9..69b679573352 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Praetorian.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Praetorian.dm
@@ -26,6 +26,12 @@
tackle_max = 5
tackle_chance = 45
+ available_strains = list(
+ /datum/xeno_strain/dancer,
+ /datum/xeno_strain/oppressor,
+ /datum/xeno_strain/vanguard,
+ /datum/xeno_strain/warden,
+ )
behavior_delegate_type = /datum/behavior_delegate/praetorian_base
minimum_evolve_time = 15 MINUTES
@@ -46,7 +52,6 @@
mob_size = MOB_SIZE_BIG
drag_delay = 6 //pulling a big dead xeno is hard
tier = 3
- mutation_type = PRAETORIAN_NORMAL
base_actions = list(
/datum/action/xeno_action/onclick/xeno_resting,
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Predalien.dm b/code/modules/mob/living/carbon/xenomorph/castes/Predalien.dm
index bcf47386fefc..ebae6f56830f 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Predalien.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Predalien.dm
@@ -63,8 +63,6 @@
/datum/action/xeno_action/onclick/tacmap,
)
- mutation_type = "Normal"
-
weed_food_icon = 'icons/mob/xenos/weeds_64x64.dmi'
weed_food_states = list("Predalien_1","Predalien_2","Predalien_3")
weed_food_states_flipped = list("Predalien_1","Predalien_2","Predalien_3")
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Queen.dm b/code/modules/mob/living/carbon/xenomorph/castes/Queen.dm
index 2e5c6a9112a3..f5207e6abf30 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Queen.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Queen.dm
@@ -350,8 +350,6 @@
/datum/action/xeno_action/activable/xeno_spit/queen_macro, //third macro
/datum/action/xeno_action/onclick/shift_spits, //second macro
)
- mutation_icon_state = QUEEN_NORMAL
- mutation_type = QUEEN_NORMAL
claw_type = CLAW_TYPE_VERY_SHARP
var/queen_aged = FALSE
@@ -519,7 +517,7 @@
overwatch(observed_xeno, TRUE)
if(ovipositor && !is_mob_incapacitated(TRUE))
- egg_amount += 0.07 * mutators.egg_laying_multiplier //one egg approximately every 30 seconds
+ egg_amount += 0.07 //one egg approximately every 30 seconds
if(egg_amount >= 1)
if(isturf(loc))
var/turf/T = loc
@@ -595,7 +593,7 @@
xeno_announcement(input, hivenumber, "The words of the [name] reverberate in our head...")
- log_and_message_admins("[key_name_admin(src)] has created a Word of the Queen report:")
+ message_admins("[key_name_admin(src)] has created a Word of the Queen report:")
log_admin("[key_name_admin(src)] Word of the Queen: [input]")
return TRUE
@@ -936,7 +934,7 @@
var/mob/living/carbon/xenomorph/queen/Queen = bound_xeno
if(Queen.ovipositor)
Queen.icon = Queen.queen_ovipositor_icon
- Queen.icon_state = "[Queen.mutation_icon_state || Queen.mutation_type] Queen Ovipositor"
+ Queen.icon_state = "[Queen.get_strain_name()] Queen Ovipositor"
return TRUE
// Switch icon back and then let normal icon behavior happen
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Ravager.dm b/code/modules/mob/living/carbon/xenomorph/castes/Ravager.dm
index 6d19a1b8feec..e50a0f026d65 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Ravager.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Ravager.dm
@@ -26,6 +26,10 @@
fire_immunity = FIRE_IMMUNITY_NO_DAMAGE|FIRE_IMMUNITY_XENO_FRENZY
attack_delay = -1
+ available_strains = list(
+ /datum/xeno_strain/berserker,
+ /datum/xeno_strain/hedgehog,
+ )
behavior_delegate_type = /datum/behavior_delegate/ravager_base
minimum_evolve_time = 15 MINUTES
@@ -45,7 +49,6 @@
tier = 3
pixel_x = -16
old_x = -16
- mutation_type = RAVAGER_NORMAL
claw_type = CLAW_TYPE_VERY_SHARP
base_actions = list(
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Runner.dm b/code/modules/mob/living/carbon/xenomorph/castes/Runner.dm
index 3a23afc145d1..400195f21de0 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Runner.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Runner.dm
@@ -13,6 +13,8 @@
evasion = XENO_EVASION_NONE
speed = XENO_SPEED_RUNNER
attack_delay = -4
+
+ available_strains = list(/datum/xeno_strain/acider)
behavior_delegate_type = /datum/behavior_delegate/runner_base
evolves_to = list(XENO_CASTE_LURKER)
deevolves_to = list("Larva")
@@ -62,7 +64,6 @@
inherent_verbs = list(
/mob/living/carbon/xenomorph/proc/vent_crawl,
)
- mutation_type = RUNNER_NORMAL
icon_xeno = 'icons/mob/xenos/runner.dmi'
icon_xenonid = 'icons/mob/xenonids/runner.dmi'
@@ -77,6 +78,12 @@
if (pass_flags_container)
pass_flags_container.flags_pass |= PASS_FLAGS_CRAWLER
+/mob/living/carbon/xenomorph/runner/recalculate_actions()
+ . = ..()
+ pull_multiplier *= 0.85
+ if(is_zoomed)
+ zoom_out()
+
/datum/behavior_delegate/runner_base
name = "Base Runner Behavior Delegate"
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Sentinel.dm b/code/modules/mob/living/carbon/xenomorph/castes/Sentinel.dm
index a568a093b3a4..2e53f97e297b 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Sentinel.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Sentinel.dm
@@ -53,7 +53,6 @@
inherent_verbs = list(
/mob/living/carbon/xenomorph/proc/vent_crawl,
)
- mutation_type = SENTINEL_NORMAL
icon_xeno = 'icons/mob/xenos/sentinel.dmi'
icon_xenonid = 'icons/mob/xenonids/sentinel.dmi'
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Spitter.dm b/code/modules/mob/living/carbon/xenomorph/castes/Spitter.dm
index 9ad2f4909fb8..984a2d08bb75 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Spitter.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Spitter.dm
@@ -56,7 +56,6 @@
inherent_verbs = list(
/mob/living/carbon/xenomorph/proc/vent_crawl,
)
- mutation_type = SPITTER_NORMAL
icon_xeno = 'icons/mob/xenos/spitter.dmi'
icon_xenonid = 'icons/mob/xenonids/spitter.dmi'
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Warrior.dm b/code/modules/mob/living/carbon/xenomorph/castes/Warrior.dm
index b19978a33766..1c329c8b9e82 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Warrior.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Warrior.dm
@@ -55,7 +55,6 @@
/datum/action/xeno_action/onclick/tacmap,
)
- mutation_type = WARRIOR_NORMAL
claw_type = CLAW_TYPE_SHARP
icon_xeno = 'icons/mob/xenos/warrior.dmi'
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/caste_datum.dm b/code/modules/mob/living/carbon/xenomorph/castes/caste_datum.dm
index cfaedf013a44..feee2edecb67 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/caste_datum.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/caste_datum.dm
@@ -66,6 +66,9 @@
var/agility_speed_increase = 0 // this opens up possibilities for balancing
+ /// A list of strain typepaths that are able to be chosen by this caste.
+ var/list/available_strains = list()
+
// The type of mutator delegate to instantiate on the base caste. Will
// be replaced when the Xeno chooses a strain.
var/behavior_delegate_type = /datum/behavior_delegate
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/lesser_drone.dm b/code/modules/mob/living/carbon/xenomorph/castes/lesser_drone.dm
index f050a0dcfe8a..8b268ebfce62 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/lesser_drone.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/lesser_drone.dm
@@ -74,8 +74,6 @@
/mob/living/carbon/xenomorph/proc/set_hugger_reserve_for_morpher,
)
- mutation_type = DRONE_NORMAL
-
icon_xeno = 'icons/mob/xenos/lesser_drone.dmi'
icon_xenonid = 'icons/mob/xenonids/lesser_drone.dmi'
@@ -83,6 +81,20 @@
weed_food_states = list("Lesser_Drone_1","Lesser_Drone_2","Lesser_Drone_3")
weed_food_states_flipped = list("Lesser_Drone_1","Lesser_Drone_2","Lesser_Drone_3")
+/mob/living/carbon/xenomorph/lesser_drone/Login()
+ var/last_ckey_inhabited = persistent_ckey
+ . = ..()
+ if(ckey == last_ckey_inhabited)
+ return
+
+ AddComponent(\
+ /datum/component/temporary_mute,\
+ "We aren't old enough to vocalize anything yet.",\
+ "We aren't old enough to communicate like this yet.",\
+ "We feel old enough to be able to vocalize and speak to the hivemind.",\
+ 3 MINUTES,\
+ )
+
/mob/living/carbon/xenomorph/lesser_drone/age_xeno()
if(stat == DEAD || !caste || QDELETED(src) || !client)
return
diff --git a/code/modules/mob/living/carbon/xenomorph/egg_item.dm b/code/modules/mob/living/carbon/xenomorph/egg_item.dm
index 1bc41b881129..f349b8acfb8d 100644
--- a/code/modules/mob/living/carbon/xenomorph/egg_item.dm
+++ b/code/modules/mob/living/carbon/xenomorph/egg_item.dm
@@ -104,12 +104,15 @@
if(weed.weed_strength >= WEED_LEVEL_WEAK && weed.linked_hive.hivenumber == hivenumber) //check for ANY weeds
any_weeds = weed
+ // If the user isn't an eggsac carrier, then they can only plant eggs on hive weeds.
+ var/needs_hive_weeds = !istype(user.strain, /datum/xeno_strain/eggsac)
+
var/datum/hive_status/hive = GLOB.hive_datum[hivenumber]
if(!any_weeds && !hive_weeds) //you need at least some weeds to plant on.
to_chat(user, SPAN_XENOWARNING("[src] must be planted on [lowertext(hive.prefix)]weeds."))
return
- if(!hive_weeds && user.mutation_type != CARRIER_EGGSAC)
+ if(!hive_weeds && needs_hive_weeds)
to_chat(user, SPAN_XENOWARNING("[src] can only be planted on [lowertext(hive.prefix)]hive weeds."))
return
@@ -138,7 +141,7 @@
return
for(var/obj/effect/alien/weeds/weed in T)
- if(weed.weed_strength >= WEED_LEVEL_HIVE || user.mutation_type == CARRIER_EGGSAC)
+ if(weed.weed_strength >= WEED_LEVEL_HIVE || !needs_hive_weeds)
user.use_plasma(30)
var/obj/effect/alien/egg/newegg
if(weed.weed_strength >= WEED_LEVEL_HIVE)
diff --git a/code/modules/mob/living/carbon/xenomorph/hive_status.dm b/code/modules/mob/living/carbon/xenomorph/hive_status.dm
index bb67eaa055a8..4b6c0f4b279e 100644
--- a/code/modules/mob/living/carbon/xenomorph/hive_status.dm
+++ b/code/modules/mob/living/carbon/xenomorph/hive_status.dm
@@ -46,7 +46,6 @@
var/allowed_nest_distance = 15 //How far away do we allow nests from an ovied Queen. Default 15 tiles.
var/obj/effect/alien/resin/special/pylon/core/hive_location = null //Set to ref every time a core is built, for defining the hive location
- var/datum/mutator_set/hive_mutators/mutators = new
var/tier_slot_multiplier = 1
var/larva_gestation_multiplier = 1
var/bonus_larva_spawn_chance = 1
@@ -143,7 +142,6 @@
var/static/list/evolution_menu_images
/datum/hive_status/New()
- mutators.hive = src
hive_ui = new(src)
mark_ui = new(src)
faction_ui = new(src)
@@ -279,7 +277,6 @@
/datum/hive_status/proc/set_living_xeno_queen(mob/living/carbon/xenomorph/queen/queen)
if(!queen)
- mutators.reset_mutators()
SStracking.delete_leader("hive_[hivenumber]")
SStracking.stop_tracking("hive_[hivenumber]", living_xeno_queen)
SShive_status.wait = 10 SECONDS
@@ -293,10 +290,8 @@
recalculate_hive()
/datum/hive_status/proc/recalculate_hive()
- if (!living_xeno_queen)
- queen_leader_limit = 0 //No leaders for a Hive without a Queen!
- else
- queen_leader_limit = 4 + mutators.leader_count_boost
+ //No leaders for a Hive without a Queen!
+ queen_leader_limit = living_xeno_queen ? 4 : 0
if (xeno_leader_list.len > queen_leader_limit)
var/diff = 0
@@ -310,11 +305,6 @@
open_xeno_leader_positions += i
xeno_leader_list.len++
-
- tier_slot_multiplier = mutators.tier_slot_multiplier
- larva_gestation_multiplier = mutators.larva_gestation_multiplier
- bonus_larva_spawn_chance = mutators.bonus_larva_spawn_chance
-
hive_ui.update_all_data()
/datum/hive_status/proc/add_hive_leader(mob/living/carbon/xenomorph/xeno)
@@ -524,7 +514,7 @@
xeno_name = "Larva ([X.nicknumber])"
xenos["[X.nicknumber]"] = list(
"name" = xeno_name,
- "strain" = X.mutation_type,
+ "strain" = X.get_strain_name(),
"ref" = "\ref[X]"
)
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/mutator.dm b/code/modules/mob/living/carbon/xenomorph/mutators/mutator.dm
deleted file mode 100644
index 6736bdb0419b..000000000000
--- a/code/modules/mob/living/carbon/xenomorph/mutators/mutator.dm
+++ /dev/null
@@ -1,103 +0,0 @@
-#define MUTATOR_COST_CHEAP 2
-#define MUTATOR_COST_MODERATE 3
-#define MUTATOR_COST_EXPENSIVE 1
-
-//Individual mutator
-/datum/xeno_mutator
- var/name = "Mutator name" //Name of the mutator, should be short but informative
- var/description = "Mutator description" //Description to be displayed on purchase
- var/flavor_description = null // Optional flavor text to be shown. Semi-OOC
- var/cost = MUTATOR_COST_CHEAP //How expensive the mutator is
- var/required_level = 0 //Level of xeno upgrade required to unlock
- var/unique = TRUE //True if you can only buy it once
- var/death_persistent = FALSE //True if the mutators persists after Queen death (aka, mostly for "once ever" mutators)
- var/hive_only = FALSE //Hive-only mutators
- var/individual_only = FALSE //Individual-only mutators
- var/keystone = FALSE //Xeno can only take one Keystone mutator
- var/flaw = FALSE //Flaws give you points back, but you can only take one of them
- var/list/caste_whitelist = list() //List of the only castes that can buy this mutator
-
- // Rework by Fourkhan - 4/26/19, redone again c. 2/2020
- // HOW TO ADD A NEW MUTATOR
- // Step 0: Write an action(s)
- // the "handler" procs go in the appropriate caste's subfolder under the "ABILITIES" file.
- // the actual ACTION procs go in the appropriate caste's subfolder under the "POWERS" file.
- // Any constants you need to access for your strain should be in the behavior holder and
- // accessed using a cast to it using the mutator_type variable as defined below. (Or using an istype of the behavior holder)
- // vars that absolutely must be held on the xenos themselves can be added to the Xenomorph class itself.
- // Be sure to follow the spec in xeno_action.dm as far as setting up xeno_cooldown is concerned.
- //
- // Step 1: Write the Behavior Delegate datum IF NECESSARY
- // the "behavior holder" datum defines all unique behavior and state for each xeno/strain. It works by embedding a number of 'hooks'
- // for example, if you want to store bonus damage and apply it on slashes, behavior delegates are the way to do it.
- // in common procs that call back to Xeno features. See other behavior delegates for examples. Afterward, set the behavior_delegate_type
- // var on the strain datum to indicate which behavior holder to apply to your strain.
- //
- // Step 1: Copy/paste another datum definiton and edit it for your strain
- // make sure to populate each of the variables listed above (at least as much as other strains)
- //
- // Step 2: Write the apply_mutator proc.
- // FIRST: populate mutator_actions_to_add and mutator_actions_to_remove according to that documentation.
- // THEN: write the body of the apply_mutator method according to your speficiations
- // THEN: call mutator_update_actions on your xeno
- // call recalculate actions on your mutator set (this should be auto populated)
- // You should probably also call recalculate_everything() on the host Xeno to make sure you don't end up with any
- // strange transient values.
- // THEN: Set the mutation_type var on the host xeno to "name" the strain.
- // FINALLY: Call apply_behavior_holder() to add the behavior datum to the new Xeno.
- //
- // You're done!
-
- // Both should be set to null when their use is not necessary.
- /// A list of PATHS of actions that need to be removed when a xeno takes the mutator.
- var/list/mutator_actions_to_remove //Actions to remove when the mutator is added
- /// A list of PATHS of actions to be ADDED when the Xeno takes the mutator.
- var/list/mutator_actions_to_add //Actions to add when the mutator is added
-
- // Type of the behavior datum to add
- var/behavior_delegate_type = null // Specify this on subtypes
-
-/datum/xeno_mutator/New()
- . = ..()
- name = "[name]"
-
-
-/datum/xeno_mutator/proc/apply_mutator(datum/mutator_set/MS)
- if(!MS.can_purchase_mutator(name))
- return FALSE
- if(MS.remaining_points < cost)
- return FALSE
- MS.remaining_points -= cost
- MS.purchased_mutators += name
-
- if(istype(MS, /datum/mutator_set/individual_mutators))
- var/datum/mutator_set/individual_mutators/IS = MS
- if(IS.xeno)
- IS.xeno.hive.hive_ui.update_xeno_info()
-
- return TRUE
-
-// Sets up actions for when a mutator is taken
-// Must be called at the end of any mutator that changes available actions
-// (read: Strains) apply_mutator proc for the mutator to work correctly.
-/datum/xeno_mutator/proc/mutator_update_actions(mob/living/carbon/xenomorph/X)
- if(mutator_actions_to_remove)
- for(var/action_path in mutator_actions_to_remove)
- remove_action(X, action_path)
- if(mutator_actions_to_add)
- for(var/action_path in mutator_actions_to_add)
- give_action(X, action_path)
-
-// Substitutes the existing behavior delegate for the strain-defined one.
-/datum/xeno_mutator/proc/apply_behavior_holder(mob/living/carbon/xenomorph/X)
- if (!istype(X))
- log_debug("Null mob handed to apply_behavior_holder. Tell the devs.")
- log_admin("Null mob handed to apply_behavior_holder. Tell the devs.")
- message_admins("Null mob handed to apply_behavior_holder. Tell the devs.")
-
- if (behavior_delegate_type)
- if(X.behavior_delegate)
- qdel(X.behavior_delegate)
- X.behavior_delegate = new behavior_delegate_type()
- X.behavior_delegate.bound_xeno = X
- X.behavior_delegate.add_to_xeno()
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/defender/steel_crest.dm b/code/modules/mob/living/carbon/xenomorph/mutators/strains/defender/steel_crest.dm
deleted file mode 100644
index 957e7f1b8926..000000000000
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/defender/steel_crest.dm
+++ /dev/null
@@ -1,44 +0,0 @@
-/datum/xeno_mutator/steel_crest
- name = "STRAIN: Defender - Steel Crest"
- description = "You trade your tail sweep and a small amount of your slash damage for slightly increased headbutt knockback and damage and the ability to slowly move and headbutt while fortified. Along with this, you gain a unique ability to accumulate damage, and use it to recover a slight amount of health and refresh your tail slam."
- flavor_description = "To handle yourself, use your head. To handle others, use your head."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_DEFENDER)
- mutator_actions_to_remove = list(
- /datum/action/xeno_action/onclick/tail_sweep,
- )
- mutator_actions_to_add = list(
- /datum/action/xeno_action/onclick/soak,
- )
- behavior_delegate_type = /datum/behavior_delegate/defender_steel_crest
- keystone = TRUE
-
-/datum/xeno_mutator/steel_crest/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
-
- var/mob/living/carbon/xenomorph/defender/defender = mutator_set.xeno
- defender.mutation_type = DEFENDER_STEELCREST
- defender.mutation_icon_state = DEFENDER_STEELCREST
- defender.damage_modifier -= XENO_DAMAGE_MOD_VERY_SMALL
- if(defender.fortify)
- defender.ability_speed_modifier += 2.5
- mutator_update_actions(defender)
- mutator_set.recalculate_actions(description, flavor_description)
- defender.recalculate_stats()
-
-/datum/behavior_delegate/defender_steel_crest
- name = "Steel Crest Defender Behavior Delegate"
-
-/datum/behavior_delegate/defender_steel_crest/on_update_icons()
- if(bound_xeno.stat == DEAD)
- return
-
- if(bound_xeno.fortify)
- bound_xeno.icon_state = "[bound_xeno.mutation_icon_state || bound_xeno.mutation_type] Steelcrest Defender Fortify"
- return TRUE
- if(bound_xeno.crest_defense)
- bound_xeno.icon_state = "[bound_xeno.mutation_icon_state || bound_xeno.mutation_type] Steelcrest Defender Crest"
- return TRUE
diff --git a/code/modules/mob/living/carbon/xenomorph/say.dm b/code/modules/mob/living/carbon/xenomorph/say.dm
index 5b8ce1ecd292..c40a50ce7523 100644
--- a/code/modules/mob/living/carbon/xenomorph/say.dm
+++ b/code/modules/mob/living/carbon/xenomorph/say.dm
@@ -98,6 +98,9 @@
to_chat(src, SPAN_WARNING("A headhunter temporarily cut off your psychic connection!"))
return
+ if(SEND_SIGNAL(src, COMSIG_XENO_TRY_HIVEMIND_TALK, message) & COMPONENT_OVERRIDE_HIVEMIND_TALK)
+ return
+
hivemind_broadcast(message, hive)
/mob/living/carbon/proc/hivemind_broadcast(message, datum/hive_status/hive)
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/behavior_delegate.dm b/code/modules/mob/living/carbon/xenomorph/strains/behavior_delegate.dm
similarity index 100%
rename from code/modules/mob/living/carbon/xenomorph/mutators/behavior_delegate.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/behavior_delegate.dm
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/boiler/trapper.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/boiler/trapper.dm
similarity index 87%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/boiler/trapper.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/boiler/trapper.dm
index c14d2c6773cf..e6c8061bd0fe 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/boiler/trapper.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/boiler/trapper.dm
@@ -1,40 +1,33 @@
-
-/datum/xeno_mutator/trapper
- name = "STRAIN: Boiler - Trapper"
+/datum/xeno_strain/trapper
+ name = BOILER_TRAPPER
description = "You trade your ability to bombard, lance, and dump your acid in order to gain some speed and the ability to create acid explosions and restrain talls within them. With your longer-range vision, set up traps that immobilize your opponents and place acid mines which deal damage to talls and barricades and reduce the cooldown of your trap deployment for every tall hit. Finally, hit talls with your Acid Shotgun ability which adds a stack of insight to empower the next trap you place once you reach a maximum of ten insight. A point-blank shot or a shot on a stunned target will instantly apply ten stacks."
flavor_description = "Hsss, I love the smell of burnin' tallhost flesh in the mornin'."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_BOILER) //Only boiler.
- mutator_actions_to_remove = list(
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/xeno_spit/bombard,
/datum/action/xeno_action/onclick/shift_spits/boiler,
/datum/action/xeno_action/activable/spray_acid/boiler,
/datum/action/xeno_action/onclick/toggle_long_range/boiler,
/datum/action/xeno_action/onclick/acid_shroud,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/activable/boiler_trap,
/datum/action/xeno_action/activable/acid_mine,
/datum/action/xeno_action/activable/acid_shotgun,
/datum/action/xeno_action/onclick/toggle_long_range/trapper,
)
- keystone = TRUE
behavior_delegate_type = /datum/behavior_delegate/boiler_trapper
-/datum/xeno_mutator/trapper/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if(. == 0)
- return
+/datum/xeno_strain/trapper/apply_strain(mob/living/carbon/xenomorph/boiler/boiler)
+ if(!istype(boiler))
+ return FALSE
- var/mob/living/carbon/xenomorph/boiler/boiler = mutator_set.xeno
if(boiler.is_zoomed)
boiler.zoom_out()
boiler.tileoffset = 0
boiler.viewsize = TRAPPER_VIEWRANGE
- boiler.mutation_type = BOILER_TRAPPER
boiler.plasma_types -= PLASMA_NEUROTOXIN
boiler.armor_modifier -= XENO_ARMOR_MOD_LARGE // no armor
boiler.health_modifier -= XENO_HEALTH_MOD_MED
@@ -42,12 +35,6 @@
boiler.speed_modifier += XENO_SPEED_SLOWMOD_TIER_5 // compensating for base buffs
boiler.recalculate_everything()
- apply_behavior_holder(boiler)
-
- mutator_update_actions(boiler)
- mutator_set.recalculate_actions(description, flavor_description)
-
-
/datum/behavior_delegate/boiler_trapper
name = "Boiler Trapper Behavior Delegate"
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/carrier/eggsac.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/carrier/eggsac.dm
similarity index 86%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/carrier/eggsac.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/carrier/eggsac.dm
index 10cbc29f51b0..a73a7082fbdb 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/carrier/eggsac.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/carrier/eggsac.dm
@@ -1,48 +1,36 @@
-/datum/xeno_mutator/eggsac
- name = "STRAIN: Carrier - Eggsac"
+/datum/xeno_strain/eggsac
+ name = CARRIER_EGGSAC
description = "In exchange for your ability to store huggers and place traps, you gain larger plasma stores, strong pheromones, and the ability to lay eggs by using your plasma stores. In addition, you can now carry twelve eggs at once and can place eggs one pace further than normal. \n\nYou can also place a small number of fragile eggs on normal weeds. These eggs have a lifetime of five minutes while you remain within 14 tiles. Or one minute if you leave this range."
flavor_description = "An egg is always an adventure; the next one may be different."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_CARRIER)
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Eggsac"
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/throw_hugger,
/datum/action/xeno_action/onclick/place_trap,
/datum/action/xeno_action/activable/retrieve_egg, // readding it so it gets at the end of the ability list
/datum/action/xeno_action/onclick/set_hugger_reserve,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/active_toggle/generate_egg,
/datum/action/xeno_action/activable/retrieve_egg, // readding it so it gets at the end of the ability list
)
+
behavior_delegate_type = /datum/behavior_delegate/carrier_eggsac
- keystone = TRUE
-/datum/xeno_mutator/eggsac/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (!.)
- return
- var/mob/living/carbon/xenomorph/carrier/carrier = mutator_set.xeno
- if(!istype(carrier))
- return FALSE
+/datum/xeno_strain/eggsac/apply_strain(mob/living/carbon/xenomorph/carrier/carrier)
carrier.plasma_types = list(PLASMA_EGG)
carrier.phero_modifier += XENO_PHERO_MOD_LARGE // praetorian level pheremones
- carrier.plasmapool_modifier = 1.2
- mutator_update_actions(carrier)
- mutator_set.recalculate_actions(description, flavor_description)
- carrier.recalculate_pheromones()
carrier.recalculate_plasma()
- if(carrier.huggers_cur > 0)
- playsound(carrier.loc, 'sound/voice/alien_facehugger_dies.ogg', 25, 1)
+ carrier.recalculate_pheromones()
+
+ if(carrier.huggers_cur)
+ playsound(carrier.loc, 'sound/voice/alien_facehugger_dies.ogg', 25, TRUE)
carrier.huggers_cur = 0
carrier.huggers_max = 0
carrier.update_hugger_overlays()
- carrier.mutation_type = CARRIER_EGGSAC
- carrier.update_eggsac_overlays()
carrier.eggs_max = 12
carrier.egg_planting_range = 2
- apply_behavior_holder(carrier)
- return TRUE
+ carrier.update_eggsac_overlays()
#define EGGSAC_OFF_WEED_EGGCAP 4
#define EGGSAC_EGG_SUSTAIN_DISTANCE 14
@@ -60,6 +48,10 @@
. = list()
. += "Eggs sustained: [length(eggs_sustained)] / [egg_sustain_cap]"
+/datum/behavior_delegate/carrier_eggsac/on_update_icons()
+ var/mob/living/carbon/xenomorph/carrier/bound_carrier = bound_xeno
+ bound_carrier.update_eggsac_overlays()
+
/datum/behavior_delegate/carrier_eggsac/on_life()
if(length(eggs_sustained) > egg_sustain_cap)
var/obj/effect/alien/egg/carrier_egg/my_egg = eggs_sustained[1]
@@ -87,6 +79,9 @@
remove_egg_owner(my_egg)
my_egg.start_unstoppable_decay()
+ M.visible_message(SPAN_XENOWARNING("[M] throes as its eggsac bursts into a mess of acid!"))
+ playsound(M.loc, 'sound/effects/alien_egg_burst.ogg', 25, TRUE)
+
///Remove all references to src in eggs_sustained
/datum/behavior_delegate/carrier_eggsac/Destroy()
for(var/obj/effect/alien/egg/carrier_egg/my_egg as anything in eggs_sustained)
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/crusher/charger.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/crusher/charger.dm
similarity index 96%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/crusher/charger.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/crusher/charger.dm
index 17b5cf62052c..84877b43571e 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/crusher/charger.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/crusher/charger.dm
@@ -1,10 +1,8 @@
-//
// Specific momentum based damage defines
#define CHARGER_DESTROY charger_ability.momentum * 40
#define CHARGER_DAMAGE_CADE charger_ability.momentum * 22
#define CHARGER_DAMAGE_SENTRY charger_ability.momentum * 9
-#define CHARGER_DAMAGE_MG charger_ability.momentum * 15
// Momentum loss defines. 8 is maximum momentum
@@ -14,34 +12,26 @@
#define CCA_MOMENTUM_LOSS_MIN 1
-/datum/xeno_mutator/charger
- name = "STRAIN: Crusher - Charger"
+/datum/xeno_strain/charger
+ name = CRUSHER_CHARGER
description = "In exchange for your shield, a little bit of your armor and damage, your slowdown resist from autospitters, your influence under frenzy pheromones, your stomp no longer knocking down talls, and your ability to lock your direction, you gain a considerable amount of health, some speed, your stomp does extra damage when stomping over a grounded tall, and your charge is now manually-controlled and momentum-based; the further you go, the more damage and speed you will gain until you achieve maximum momentum, indicated by your roar. In addition, your armor is now directional, being the toughest on the front, weaker on the sides, and weakest from the back. In return, you gain an ability to tumble to pass through talls and avoid enemy fire, and an ability to forcefully move enemies via ramming into them."
flavor_description = "We're just getting started. Nothing stops this train. Nothing."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_CRUSHER)
- mutator_actions_to_remove = list (
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/pounce/crusher_charge,
/datum/action/xeno_action/onclick/crusher_stomp,
/datum/action/xeno_action/onclick/crusher_shield,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/onclick/charger_charge,
/datum/action/xeno_action/activable/tumble,
/datum/action/xeno_action/onclick/crusher_stomp/charger,
/datum/action/xeno_action/activable/fling/charger,
)
- keystone = TRUE
- behavior_delegate_type = /datum/behavior_delegate/crusher_charger
-/datum/xeno_mutator/charger/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
+ behavior_delegate_type = /datum/behavior_delegate/crusher_charger
- var/mob/living/carbon/xenomorph/crusher/crusher = mutator_set.xeno
- crusher.mutation_type = CRUSHER_CHARGER
+/datum/xeno_strain/charger/apply_strain(mob/living/carbon/xenomorph/crusher/crusher)
crusher.small_explosives_stun = FALSE
crusher.health_modifier += XENO_HEALTH_MOD_LARGE
crusher.speed_modifier += XENO_SPEED_FASTMOD_TIER_3
@@ -49,10 +39,6 @@
crusher.damage_modifier -= XENO_DAMAGE_MOD_SMALL
crusher.ignore_aura = "frenzy" // no funny crushers going 7 morbillion kilometers per second
crusher.phero_modifier = -crusher.caste.aura_strength
- crusher.recalculate_pheromones()
- mutator_update_actions(crusher)
- mutator_set.recalculate_actions(description, flavor_description)
- apply_behavior_holder(crusher)
crusher.recalculate_everything()
/datum/behavior_delegate/crusher_charger
@@ -83,7 +69,7 @@
/datum/behavior_delegate/crusher_charger/on_update_icons()
if(HAS_TRAIT(bound_xeno, TRAIT_CHARGING) && bound_xeno.body_position == STANDING_UP)
- bound_xeno.icon_state = "[bound_xeno.mutation_icon_state || bound_xeno.mutation_type] Crusher Charging"
+ bound_xeno.icon_state = "[bound_xeno.get_strain_icon()] Crusher Charging"
return TRUE
// Fallback proc for shit that doesn't have a collision def
@@ -643,3 +629,12 @@
return XENO_CHARGE_TRY_MOVE
charger_ability.stop_momentum()
+
+
+#undef CHARGER_DESTROY
+#undef CHARGER_DAMAGE_CADE
+#undef CHARGER_DAMAGE_SENTRY
+#undef CCA_MOMENTUM_LOSS_HALF
+#undef CCA_MOMENTUM_LOSS_THIRD
+#undef CCA_MOMENTUM_LOSS_QUARTER
+#undef CCA_MOMENTUM_LOSS_MIN
diff --git a/code/modules/mob/living/carbon/xenomorph/strains/castes/defender/steel_crest.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/defender/steel_crest.dm
new file mode 100644
index 000000000000..f84566e9c841
--- /dev/null
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/defender/steel_crest.dm
@@ -0,0 +1,22 @@
+/datum/xeno_strain/steel_crest
+ name = DEFENDER_STEELCREST
+ description = "You trade your tail sweep and a small amount of your slash damage for slightly increased headbutt knockback and damage and the ability to slowly move and headbutt while fortified. Along with this, you gain a unique ability to accumulate damage, and use it to recover a slight amount of health and refresh your tail slam."
+ flavor_description = "To handle yourself, use your head. To handle others, use your head."
+ icon_state_prefix = "Steelcrest"
+
+ actions_to_remove = list(
+ /datum/action/xeno_action/activable/headbutt,
+ /datum/action/xeno_action/activable/fortify,
+ /datum/action/xeno_action/onclick/tail_sweep,
+ )
+ actions_to_add = list(
+ /datum/action/xeno_action/activable/headbutt/steel_crest,
+ /datum/action/xeno_action/activable/fortify/steel_crest,
+ /datum/action/xeno_action/onclick/soak,
+ )
+
+/datum/xeno_strain/steel_crest/apply_strain(mob/living/carbon/xenomorph/defender/defender)
+ defender.damage_modifier -= XENO_DAMAGE_MOD_VERY_SMALL
+ if(defender.fortify)
+ defender.ability_speed_modifier += 2.5
+ defender.recalculate_stats()
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/drone/gardener.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/drone/gardener.dm
similarity index 93%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/drone/gardener.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/drone/gardener.dm
index da354b465331..d54d268f12d9 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/drone/gardener.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/drone/gardener.dm
@@ -1,45 +1,41 @@
-/datum/xeno_mutator/gardener
- name = "STRAIN: Drone - Gardener"
+/datum/xeno_strain/gardener
+ name = DRONE_GARDENER
description = "You trade your choice of resin secretions, your corrosive acid, and your ability to transfer plasma for a tiny bit of extra health regeneration on weeds and several new abilities, including the ability to plant hardier weeds, temporarily reinforce structures with your plasma, and to plant up to six potent resin fruits for your sisters by secreting your vital fluids at the cost of a bit of your health for each fruit you shape. You can use Resin Surge to speed up the growth of your fruits."
flavor_description = "The glory of gardening: hands in the weeds, head in the dark, heart with resin."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_DRONE) //Only drone.
- mutator_actions_to_remove = list(
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/secrete_resin,
/datum/action/xeno_action/onclick/choose_resin,
/datum/action/xeno_action/activable/corrosive_acid/weak,
/datum/action/xeno_action/activable/transfer_plasma,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/onclick/plant_weeds/gardener, // second macro
/datum/action/xeno_action/activable/resin_surge, // third macro
/datum/action/xeno_action/onclick/plant_resin_fruit/greater, // fourth macro
/datum/action/xeno_action/onclick/change_fruit,
/datum/action/xeno_action/activable/transfer_plasma,
)
- keystone = TRUE
- behavior_delegate_type = /datum/behavior_delegate/drone_gardener
-/datum/xeno_mutator/gardener/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
+ behavior_delegate_type = /datum/behavior_delegate/drone_gardener
- var/mob/living/carbon/xenomorph/drone/drone = mutator_set.xeno
- drone.mutation_type = DRONE_GARDENER
- drone.available_fruits = list(/obj/effect/alien/resin/fruit/greater, /obj/effect/alien/resin/fruit/unstable, /obj/effect/alien/resin/fruit/spore, /obj/effect/alien/resin/fruit/speed, /obj/effect/alien/resin/fruit/plasma)
+/datum/xeno_strain/gardener/apply_strain(mob/living/carbon/xenomorph/drone/drone)
+ drone.available_fruits = list(
+ /obj/effect/alien/resin/fruit/greater,
+ /obj/effect/alien/resin/fruit/unstable,
+ /obj/effect/alien/resin/fruit/spore,
+ /obj/effect/alien/resin/fruit/speed,
+ /obj/effect/alien/resin/fruit/plasma
+ )
drone.selected_fruit = /obj/effect/alien/resin/fruit/greater
drone.max_placeable = 6
drone.regeneration_multiplier = XENO_REGEN_MULTIPLIER_TIER_1
- mutator_update_actions(drone)
- apply_behavior_holder(drone)
+
// Also change the primacy value for our place construction ability (because we want it in the same place but have another primacy ability)
for(var/datum/action/xeno_action/action in drone.actions)
if(istype(action, /datum/action/xeno_action/activable/place_construction))
action.ability_primacy = XENO_NOT_PRIMARY_ACTION
break // Don't need to keep looking
- mutator_set.recalculate_actions(description, flavor_description)
/datum/action/xeno_action/onclick/plant_resin_fruit
name = "Plant Resin Fruit (50)"
@@ -368,9 +364,6 @@
var/mutable_appearance/fruit_sac_overlay_icon
-/datum/behavior_delegate/drone_gardener/add_to_xeno()
- on_update_icons()
-
/datum/behavior_delegate/drone_gardener/on_update_icons()
if(!fruit_sac_overlay_icon)
fruit_sac_overlay_icon = mutable_appearance('icons/mob/xenos/drone_strain_overlays.dmi', "Gardener Drone Walking")
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/drone/healer.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/drone/healer.dm
similarity index 90%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/drone/healer.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/drone/healer.dm
index 86f50b47e080..0fcbb2ecf09a 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/drone/healer.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/drone/healer.dm
@@ -1,18 +1,17 @@
-/datum/xeno_mutator/healer
- name = "STRAIN: Drone - Healer"
+/datum/xeno_strain/healer
+ name = DRONE_HEALER
description = "You lose your choice of resin secretions, a chunk of your slash damage, and you will experience a slighty-increased difficulty in tackling tallhosts in exchange for strong pheromones, the ability to use a bit of your health to plant a maximum of three lesser resin fruits, and the ability to heal your sisters' wounds by secreting a regenerative resin salve by using your vital fluids and a fifth of your plasma. Be wary, this is a dangerous process; overexert yourself and you may exhaust yourself to unconsciousness, or die..."
flavor_description = "To the very last drop, your blood belongs to The Hive; share it with your sisters to keep them fighting."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_DRONE) //Only drone.
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Healer"
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/secrete_resin,
/datum/action/xeno_action/onclick/choose_resin,
/datum/action/xeno_action/activable/transfer_plasma,
/datum/action/xeno_action/activable/place_construction, // so it doesn't use fifth macro
/datum/action/xeno_action/onclick/plant_weeds, // so it doesn't break order
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/activable/place_construction/not_primary, // so it doesn't use fifth macro
/datum/action/xeno_action/onclick/plant_weeds, // so it doesn't break order
/datum/action/xeno_action/onclick/plant_resin_fruit, // Second macro. Resin fruits belong to Gardener, but Healer has a minor variant.
@@ -20,32 +19,20 @@
/datum/action/xeno_action/activable/transfer_plasma/healer, //Fourth macro, an improved plasma transfer.
/datum/action/xeno_action/activable/healer_sacrifice, //Fifth macro, the ultimate ability to sacrifice yourself
)
- keystone = TRUE
- behavior_delegate_type = /datum/behavior_delegate/drone_healer
-
-/datum/xeno_mutator/healer/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
+ behavior_delegate_type = /datum/behavior_delegate/drone_healer
- var/mob/living/carbon/xenomorph/drone/drone = mutator_set.xeno
- drone.mutation_type = DRONE_HEALER
+/datum/xeno_strain/healer/apply_strain(mob/living/carbon/xenomorph/drone/drone)
drone.phero_modifier += XENO_PHERO_MOD_LARGE
drone.plasma_types += PLASMA_PHEROMONE
drone.damage_modifier -= XENO_DAMAGE_MOD_VERY_SMALL
+ drone.tackle_chance_modifier -= 5
drone.max_placeable = 3
drone.available_fruits = list(/obj/effect/alien/resin/fruit)
drone.selected_fruit = /obj/effect/alien/resin/fruit
- drone.tackle_chance_modifier -= 5
- mutator_update_actions(drone)
- apply_behavior_holder(drone)
- mutator_set.recalculate_actions(description, flavor_description)
- drone.recalculate_health()
- drone.recalculate_damage()
- drone.recalculate_pheromones()
- drone.recalculate_tackle()
+
+ drone.recalculate_everything()
/*
Improved Plasma Transfer
@@ -123,13 +110,14 @@
to_chat(src, SPAN_XENOWARNING("[target_xeno] is already at max health!"))
return
-///Tiny xenos (Larva and Facehuggers), don't need as much health so don't cost as much.
- if(target_xeno.mob_size == 0)
+ //Tiny xenos (Larva and Facehuggers), don't need as much health so don't cost as much.
+ if(target_xeno.mob_size == MOB_SIZE_SMALL)
amount = amount * 0.15
damage_taken_mod = 1
-//Forces an equivalent exchange of health between healers so they do not spam heal each other to full health.
- if(target_xeno.mutation_type == DRONE_HEALER)
+ //Forces an equivalent exchange of health between healers so they do not spam heal each other to full health.
+ var/target_is_healer = istype(target_xeno.strain, /datum/xeno_strain/healer)
+ if(target_is_healer)
damage_taken_mod = 1
face_atom(target_xeno)
@@ -144,7 +132,7 @@
playsound(src, "alien_drool", 25)
var/datum/behavior_delegate/drone_healer/healer_delegate = behavior_delegate
healer_delegate.salve_applied_recently = TRUE
- if(target_xeno.mutation_type != DRONE_HEALER && !isfacehugger(target_xeno)) // no cheap grinding
+ if(!target_is_healer && !isfacehugger(target_xeno)) // no cheap grinding
healer_delegate.modify_transferred(amount * damage_taken_mod)
update_icons()
addtimer(CALLBACK(healer_delegate, /datum/behavior_delegate/drone_healer/proc/un_salve), 10 SECONDS, TIMER_OVERRIDE|TIMER_UNIQUE)
@@ -301,4 +289,3 @@
to_chat(xeno, SPAN_HIGHDANGER("Warning: [name] is a last measure skill. Using it will kill us."))
else
to_chat(xeno, SPAN_HIGHDANGER("Warning: [name] is a last measure skill. Using it will kill us, but new life will be granted for our hard work for the hive."))
-
diff --git a/code/modules/mob/living/carbon/xenomorph/strains/castes/facehugger/watcher.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/facehugger/watcher.dm
new file mode 100644
index 000000000000..7fba30b6f352
--- /dev/null
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/facehugger/watcher.dm
@@ -0,0 +1,21 @@
+/datum/xeno_strain/watcher
+ name = FACEHUGGER_WATCHER
+ description = "You lose your ability to hide in exchange to see further and the ability to no longer take damage outside of weeds. This enables you to stalk your host from a distance and wait for the perfect oppertunity to strike."
+ flavor_description = "No need to hide when you can see the danger."
+
+ actions_to_remove = list(
+ /datum/action/xeno_action/onclick/xenohide,
+ )
+ actions_to_add = list(
+ /datum/action/xeno_action/onclick/toggle_long_range/runner,
+ )
+
+ behavior_delegate_type = /datum/behavior_delegate/facehugger_watcher
+
+/datum/xeno_strain/watcher/apply_strain(mob/living/carbon/xenomorph/facehugger/huggy)
+ huggy.viewsize = 10
+ huggy.layer = initial(huggy.layer)
+
+// This has no special effects, it's just here to skip `/datum/behavior_delegate/facehugger_base/on_life()`.
+/datum/behavior_delegate/facehugger_watcher
+ name = "Watcher Facehugger Behavior Delegate"
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/hivelord/resin_whisperer.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/hivelord/resin_whisperer.dm
similarity index 80%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/hivelord/resin_whisperer.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/hivelord/resin_whisperer.dm
index 3653209b78f2..8bdcd1311f94 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/hivelord/resin_whisperer.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/hivelord/resin_whisperer.dm
@@ -1,41 +1,30 @@
-/datum/xeno_mutator/resinwhisperer
- name = "STRAIN: Hivelord - Resin Whisperer"
+/datum/xeno_strain/resin_whisperer
+ name = HIVELORD_RESIN_WHISPERER
description = "You lose your corrosive acid, your ability to secrete thick resin, your ability to reinforce resin secretions, sacrifice your ability to plant resin nodes outside of weeds, and you sacrifice a fifth of your plasma reserves to enhance your vision and gain a stronger connection to the resin. You can now remotely place resin secretions including resin nodes up to a distance of twelve paces!"
flavor_description = "Let the resin guide you. It whispers, so listen closely."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_HIVELORD)
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Resin Whisperer"
+
+ actions_to_remove = list(
/datum/action/xeno_action/onclick/plant_weeds,
/datum/action/xeno_action/activable/secrete_resin/hivelord,
/datum/action/xeno_action/activable/corrosive_acid,
/datum/action/xeno_action/activable/transfer_plasma/hivelord,
/datum/action/xeno_action/active_toggle/toggle_speed,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/activable/secrete_resin/remote, //third macro
/datum/action/xeno_action/activable/transfer_plasma/hivelord, // readding it so it gets at the end of the ability list
/datum/action/xeno_action/active_toggle/toggle_speed, // readding it so it gets at the end of the ability list
)
- keystone = TRUE
-
-/datum/xeno_mutator/resinwhisperer/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if(!.)
- return
- var/mob/living/carbon/xenomorph/hivelord/hivelord = mutator_set.xeno
+/datum/xeno_strain/resin_whisperer/apply_strain(mob/living/carbon/xenomorph/hivelord/hivelord)
hivelord.plasmapool_modifier = 0.8 // -20% plasma pool
hivelord.extra_build_dist = 12 // 1 + 12 = 13 tile build range
hivelord.can_stack_builds = TRUE
+ hivelord.recalculate_plasma()
hivelord.client.change_view(10, src)
- hivelord.mutation_type = HIVELORD_RESIN_WHISPERER
- mutator_update_actions(hivelord)
- mutator_set.recalculate_actions(description, flavor_description)
- hivelord.recalculate_plasma()
-
hivelord.set_resin_build_order(GLOB.resin_build_order_hivelord_whisperer)
for(var/datum/action/xeno_action/action in hivelord.actions)
// Also update the choose_resin icon since it resets
@@ -68,12 +57,9 @@
action_type = XENO_ACTION_CLICK
/datum/action/xeno_action/activable/secrete_resin/remote/use_ability(atom/target_atom, mods)
- var/mob/living/carbon/xenomorph/xeno_owner = owner
- if(xeno_owner.mutation_type == HIVELORD_RESIN_WHISPERER)
- var/mob/living/carbon/xenomorph/hivelord/hivelord_mob = owner
- if(!hivelord_mob.on_weeds()) // There is a chance that queen can't place down buildings in ovi build view so we place the rein whisperer check here.
- to_chat(owner, SPAN_XENONOTICE("We must be standing on weeds to establish a connection to the resin."))
- return
+ if(!can_remote_build())
+ to_chat(owner, SPAN_XENONOTICE("We must be standing on weeds to establish a connection to the resin."))
+ return
if(!action_cooldown_check())
return
@@ -115,6 +101,12 @@
playsound(target_turf, "alien_resin_build", 25)
return TRUE
+// By default, the xeno must be on a weed tile in order to build from a distance.
+/datum/action/xeno_action/activable/secrete_resin/remote/proc/can_remote_build()
+ if(!locate(/obj/effect/alien/weeds) in get_turf(owner))
+ return FALSE
+ return TRUE
+
/datum/action/xeno_action/verb/verb_coerce_resin()
set category = "Alien"
set name = "Coerce Resin"
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/lurker/vampire.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/lurker/vampire.dm
similarity index 71%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/lurker/vampire.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/lurker/vampire.dm
index 72214d06332e..1df49092e783 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/lurker/vampire.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/lurker/vampire.dm
@@ -1,31 +1,22 @@
-
-/datum/xeno_mutator/Vampire
- name = "STRAIN: Lurker - Vampire"
+/datum/xeno_strain/vampire
+ name = LURKER_VAMPIRE
description = "You lose all of your abilities and you forefeit a chunk of your health and damage in exchange for a large amount of armor, a little bit of movement speed, increased attack speed, and brand new abilities that make you an assassin. Rush on your opponent to disorient them and Flurry to unleash a forward cleave that can hit and slow three talls and heal you for every tall you hit. Use your special AoE Tail Jab to knock talls away, doing more damage with direct hits and even more damage and a stun if they smack into walls. Finally, execute unconscious talls with a headbite that bypasses armor and heals you for a grand amount of health."
flavor_description = "Your thirst for tallhost blood surpasses even mine, child. Show no mercy! Slaughter them all!"
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_LURKER)
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Vampire"
+
+ actions_to_remove = list(
/datum/action/xeno_action/onclick/lurker_invisibility,
/datum/action/xeno_action/onclick/lurker_assassinate,
/datum/action/xeno_action/activable/pounce/lurker,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/activable/pounce/rush,
/datum/action/xeno_action/activable/flurry,
/datum/action/xeno_action/activable/tail_jab,
/datum/action/xeno_action/activable/headbite,
)
- keystone = TRUE
-
-/datum/xeno_mutator/Vampire/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == FALSE)
- return
-
- var/mob/living/carbon/xenomorph/lurker/lurker = mutator_set.xeno
+/datum/xeno_strain/vampire/apply_strain(mob/living/carbon/xenomorph/lurker/lurker)
lurker.plasmapool_modifier = 0
lurker.health_modifier -= XENO_HEALTH_MOD_MED
lurker.speed_modifier += XENO_SPEED_FASTMOD_TIER_1
@@ -34,7 +25,4 @@
lurker.melee_damage_upper = XENO_DAMAGE_TIER_3
lurker.attack_speed_modifier -= 2
- mutator_update_actions(lurker)
- mutator_set.recalculate_actions(description, flavor_description)
lurker.recalculate_everything()
- lurker.mutation_type = LURKER_VAMPIRE
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/dancer.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/dancer.dm
similarity index 68%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/dancer.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/dancer.dm
index a21f549ea8cd..f9a5dbedb614 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/dancer.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/dancer.dm
@@ -1,44 +1,31 @@
-/datum/xeno_mutator/praetorian_dancer
+/datum/xeno_strain/dancer
// My name is Cuban Pete, I'm the King of the Rumba Beat
- name = "STRAIN: Praetorian - Dancer"
+ name = PRAETORIAN_DANCER
description = "You lose all of your acid-based abilities and a small amount of your armor in exchange for increased movement speed, evasion, and unparalleled agility that gives you an ability to move even more quickly, dodge bullets, and phase through tallhosts. By slashing tallhosts, you temporarily increase your movement speed and you also you apply a tag that changes how your two new tail abilities function. By tagging hosts, you will make Impale hit twice instead of once and make Tail Trip knock hosts down instead of stunning them."
flavor_description = "Demonstrate to the talls what 'there is beauty in death' truly symbolizes, then dance upon their graves!"
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_PRAETORIAN) // Only bae
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Dancer"
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/xeno_spit,
/datum/action/xeno_action/activable/pounce/base_prae_dash,
/datum/action/xeno_action/activable/prae_acid_ball,
/datum/action/xeno_action/activable/spray_acid/base_prae_spray_acid,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/activable/prae_impale,
/datum/action/xeno_action/onclick/prae_dodge,
/datum/action/xeno_action/activable/prae_tail_trip,
)
- behavior_delegate_type = /datum/behavior_delegate/praetorian_dancer
- keystone = TRUE
-
-/datum/xeno_mutator/praetorian_dancer/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
-
- var/mob/living/carbon/xenomorph/praetorian/praetorian = mutator_set.xeno
- praetorian.armor_modifier -= XENO_ARMOR_MOD_VERY_SMALL
- praetorian.speed_modifier += XENO_SPEED_FASTMOD_TIER_5
- praetorian.plasma_types = list(PLASMA_CATECHOLAMINE)
- praetorian.claw_type = CLAW_TYPE_SHARP
- mutator_update_actions(praetorian)
- mutator_set.recalculate_actions(description, flavor_description)
+ behavior_delegate_type = /datum/behavior_delegate/praetorian_dancer
- praetorian.recalculate_everything()
+/datum/xeno_strain/dancer/apply_strain(mob/living/carbon/xenomorph/praetorian/prae)
+ prae.armor_modifier -= XENO_ARMOR_MOD_VERY_SMALL
+ prae.speed_modifier += XENO_SPEED_FASTMOD_TIER_5
+ prae.plasma_types = list(PLASMA_CATECHOLAMINE)
+ prae.claw_type = CLAW_TYPE_SHARP
- apply_behavior_holder(praetorian)
- praetorian.mutation_icon_state = PRAETORIAN_DANCER
- praetorian.mutation_type = PRAETORIAN_DANCER
+ prae.recalculate_everything()
/datum/behavior_delegate/praetorian_dancer
name = "Praetorian Dancer Behavior Delegate"
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/oppressor.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/oppressor.dm
similarity index 67%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/oppressor.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/oppressor.dm
index 4beaedf8d6a8..91ea59661462 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/oppressor.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/oppressor.dm
@@ -1,12 +1,11 @@
-/datum/xeno_mutator/praetorian_oppressor
+/datum/xeno_strain/oppressor
// Dread it, run from it, destiny still arrives... or should I say, I do
- name = "STRAIN: Praetorian - Oppressor"
+ name = PRAETORIAN_OPPRESSOR
description = "You abandon all of your acid-based abilities, your dash, some speed, and a bit of your slash damage for some resistance against small explosives, slashes that deal extra damage to prone targets, and a powerful hook ability that pulls up to three talls towards you, slows them, and has varying effects depending on how many talls you pull. You also gain a powerful punch that reduces your other abilities' cooldowns, pierces through armor, and does double damage in addition to rooting slowed targets. You can also knock talls back and slow them with your new Tail Lash and quickly grab a tall, slow it, and pull it towards you with your unique Tail Stab."
flavor_description = "Dread it. Run from it. The Hive arrives all the same, or, more accurately, you do."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_PRAETORIAN)
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Oppressor"
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/tail_stab,
/datum/action/xeno_action/activable/xeno_spit,
/datum/action/xeno_action/activable/pounce/base_prae_dash,
@@ -14,38 +13,24 @@
/datum/action/xeno_action/activable/spray_acid/base_prae_spray_acid,
/datum/action/xeno_action/activable/corrosive_acid,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/activable/tail_stab/tail_seize,
/datum/action/xeno_action/activable/prae_abduct,
/datum/action/xeno_action/activable/oppressor_punch,
/datum/action/xeno_action/activable/tail_lash,
)
- behavior_delegate_type = /datum/behavior_delegate/oppressor_praetorian
- keystone = TRUE
-
-/datum/xeno_mutator/praetorian_oppressor/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
-
- var/mob/living/carbon/xenomorph/praetorian/praetorian = mutator_set.xeno
- praetorian.damage_modifier -= XENO_DAMAGE_MOD_SMALL
- praetorian.explosivearmor_modifier += XENO_EXPOSIVEARMOR_MOD_SMALL
- praetorian.small_explosives_stun = FALSE
- praetorian.speed_modifier += XENO_SPEED_SLOWMOD_TIER_5
- praetorian.plasma_types = list(PLASMA_NEUROTOXIN, PLASMA_CHITIN)
- praetorian.claw_type = CLAW_TYPE_SHARP
-
- mutator_update_actions(praetorian)
-
- mutator_set.recalculate_actions(description, flavor_description)
+ behavior_delegate_type = /datum/behavior_delegate/oppressor_praetorian
- apply_behavior_holder(praetorian)
+/datum/xeno_strain/oppressor/apply_strain(mob/living/carbon/xenomorph/praetorian/prae)
+ prae.damage_modifier -= XENO_DAMAGE_MOD_SMALL
+ prae.explosivearmor_modifier += XENO_EXPOSIVEARMOR_MOD_SMALL
+ prae.small_explosives_stun = FALSE
+ prae.speed_modifier += XENO_SPEED_SLOWMOD_TIER_5
+ prae.plasma_types = list(PLASMA_NEUROTOXIN, PLASMA_CHITIN)
+ prae.claw_type = CLAW_TYPE_SHARP
- praetorian.recalculate_everything()
- praetorian.mutation_icon_state = PRAETORIAN_OPPRESSOR
- praetorian.mutation_type = PRAETORIAN_OPPRESSOR
+ prae.recalculate_everything()
/datum/behavior_delegate/oppressor_praetorian
name = "Oppressor Praetorian Behavior Delegate"
@@ -60,4 +45,3 @@
target_carbon.apply_armoured_damage(get_xeno_damage_slash(target_carbon, tearing_damage), ARMOR_MELEE, BRUTE, bound_xeno.zone_selected ? bound_xeno.zone_selected : "chest")
target_carbon.visible_message(SPAN_DANGER("[bound_xeno] tears into [target_carbon]!"))
playsound(bound_xeno, 'sound/weapons/alien_tail_attack.ogg', 25, TRUE)
-
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/vanguard.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/vanguard.dm
similarity index 80%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/vanguard.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/vanguard.dm
index d5ca8c4d6aad..2a344523e974 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/vanguard.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/vanguard.dm
@@ -1,43 +1,31 @@
-/datum/xeno_mutator/vanguard
- name = "STRAIN: Praetorian - Vanguard"
+/datum/xeno_strain/vanguard
+ name = PRAETORIAN_VANGUARD
description = "You forfeit all of your acid-based abilities and some health for some extra speed and a rechargable shield that can block one attack. Use your Pierce from up to three paces away to stab through talls, while stabbing through several will completely recharge your shield. Use your charge to plow through enemies and use it again to unleash a powerful AoE slash that reaches up to three paces. You also have a Cleave ability, amplified by your shield, which you can toggle to either immobilize or fling a target away."
flavor_description = "They are my bulwark against the tallhosts. They are my Vanguard and they shall know no fear."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_PRAETORIAN) //Only praetorian.
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Vanguard"
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/xeno_spit,
/datum/action/xeno_action/activable/pounce/base_prae_dash,
/datum/action/xeno_action/activable/prae_acid_ball,
/datum/action/xeno_action/activable/spray_acid/base_prae_spray_acid,
/datum/action/xeno_action/activable/corrosive_acid,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/activable/pierce,
/datum/action/xeno_action/activable/pounce/prae_dash,
/datum/action/xeno_action/activable/cleave,
/datum/action/xeno_action/onclick/toggle_cleave,
)
- behavior_delegate_type = /datum/behavior_delegate/praetorian_vanguard
- keystone = TRUE
-
-/datum/xeno_mutator/vanguard/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
- var/mob/living/carbon/xenomorph/praetorian/praetorian = mutator_set.xeno
- praetorian.speed_modifier += XENO_SPEED_FASTMOD_TIER_3
- praetorian.health_modifier -= XENO_HEALTH_MOD_MED
- praetorian.claw_type = CLAW_TYPE_SHARP
- mutator_update_actions(praetorian)
- mutator_set.recalculate_actions(description, flavor_description)
- praetorian.recalculate_everything()
+ behavior_delegate_type = /datum/behavior_delegate/praetorian_vanguard
- praetorian.mutation_icon_state = PRAETORIAN_VANGUARD
- praetorian.mutation_type = PRAETORIAN_VANGUARD
+/datum/xeno_strain/vanguard/apply_strain(mob/living/carbon/xenomorph/praetorian/prae)
+ prae.speed_modifier += XENO_SPEED_FASTMOD_TIER_3
+ prae.health_modifier -= XENO_HEALTH_MOD_MED
+ prae.claw_type = CLAW_TYPE_SHARP
- apply_behavior_holder(praetorian)
+ prae.recalculate_everything()
/datum/behavior_delegate/praetorian_vanguard
name = "Praetorian Vanguard Behavior Delegate"
@@ -103,8 +91,3 @@
var/datum/action/xeno_action/activable/cleave/caction = get_xeno_action_by_type(bound_xeno, /datum/action/xeno_action/activable/cleave)
if (istype(caction))
caction.buffed = TRUE
-
-
-
-
-
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/warden.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/warden.dm
similarity index 80%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/warden.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/warden.dm
index d9946fb9ec64..313778baf038 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/praetorian/warden.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/praetorian/warden.dm
@@ -1,19 +1,18 @@
-/datum/xeno_mutator/praetorian_warden
+/datum/xeno_strain/warden
// i mean so basically im braum
- name = "STRAIN: Praetorian - Warden"
+ name = PRAETORIAN_WARDEN
description = "You trade your acid ball, acid spray, dash, and a small bit of your slash damage and speed to become an effective medic. You gain the ability to emit strong pheromones, an ability that retrieves endangered, knocked-down or sitting allies and pulls them to your location, and you gain an internal hitpoint pool that fills with every slash against your enemies, which can be spent to aid your allies and yourself by healing them or curing their ailments."
flavor_description = "Only in death does your sisters' service to the Queen end. They will be untouched by plague or disease; no sickness will blight them."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_PRAETORIAN) // Only bae
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Warden"
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/xeno_spit,
/datum/action/xeno_action/activable/pounce/base_prae_dash,
/datum/action/xeno_action/activable/prae_acid_ball,
/datum/action/xeno_action/activable/spray_acid/base_prae_spray_acid,
/datum/action/xeno_action/onclick/tacmap,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/onclick/emit_pheromones,
/datum/action/xeno_action/activable/xeno_spit,
/datum/action/xeno_action/activable/spray_acid/prae_warden,
@@ -22,28 +21,15 @@
/datum/action/xeno_action/onclick/prae_switch_heal_type,
/datum/action/xeno_action/onclick/tacmap,
)
- behavior_delegate_type = /datum/behavior_delegate/praetorian_warden
- keystone = TRUE
-
-/datum/xeno_mutator/praetorian_warden/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
- var/mob/living/carbon/xenomorph/praetorian/praetorian = mutator_set.xeno
+ behavior_delegate_type = /datum/behavior_delegate/praetorian_warden
+/datum/xeno_strain/warden/apply_strain(mob/living/carbon/xenomorph/praetorian/prae)
// Make a 'halftank'
- praetorian.speed_modifier += XENO_SPEED_SLOWMOD_TIER_5
- praetorian.damage_modifier -= XENO_DAMAGE_MOD_SMALL
-
- mutator_update_actions(praetorian)
- mutator_set.recalculate_actions(description, flavor_description)
+ prae.speed_modifier += XENO_SPEED_SLOWMOD_TIER_5
+ prae.damage_modifier -= XENO_DAMAGE_MOD_SMALL
- praetorian.recalculate_everything()
-
- apply_behavior_holder(praetorian)
- praetorian.mutation_icon_state = PRAETORIAN_WARDEN
- praetorian.mutation_type = PRAETORIAN_WARDEN
+ prae.recalculate_everything()
/datum/behavior_delegate/praetorian_warden
name = "Praetorian Warden Behavior Delegate"
@@ -58,7 +44,6 @@
var/internal_hitpoints = 0
var/transferred_healing = 0
-
/datum/behavior_delegate/praetorian_warden/append_to_stat()
. = list()
. += "Energy Reserves: [internal_hitpoints]/[internal_hitpoints_max]"
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/ravager/berserker.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/berserker.dm
similarity index 90%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/ravager/berserker.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/berserker.dm
index 7881c9aa75f1..c12324aa5b2a 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/ravager/berserker.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/berserker.dm
@@ -1,39 +1,28 @@
-/datum/xeno_mutator/berserker
- name = "STRAIN: Ravager - Berserker"
+/datum/xeno_strain/berserker
+ name = RAVAGER_BERSERKER
description = "You lose your empower, charge, and scissor cut, decrease your health, and sacrifice a bit of your influence under frenzy pheromones to increase your movement speed, slightly increase your armor, and gain a new set of abilities that make you a terrifying melee monster. By slashing, you heal yourself and gain a stack of rage that increases your armor, movement speed, attack speed, and your heals per slash, to a maximum of six rage. Use your new Appehend ability to increase your movement speed and apply a slow on the next target you slash and use your Clothesline ability to fling your target to heal yourself, even more-so if you have a rage stack that will be used up. Finally, use your Eviscerate to unleash a devastating windmill attack that heals you for every host you hit after an immobilizing wind-up."
flavor_description = "They shall be my finest warriors. They will rend and tear, crush and butcher, and maim and rage until every tallhost falls."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_RAVAGER)
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Berserker"
+
+ actions_to_remove = list(
/datum/action/xeno_action/onclick/empower,
/datum/action/xeno_action/activable/pounce/charge,
/datum/action/xeno_action/activable/scissor_cut,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/onclick/apprehend,
/datum/action/xeno_action/activable/clothesline,
/datum/action/xeno_action/activable/eviscerate,
)
- keystone = TRUE
- behavior_delegate_type = /datum/behavior_delegate/ravager_berserker
-/datum/xeno_mutator/berserker/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
+ behavior_delegate_type = /datum/behavior_delegate/ravager_berserker
- var/mob/living/carbon/xenomorph/ravager/ravager = mutator_set.xeno
- ravager.mutation_type = RAVAGER_BERSERKER
+/datum/xeno_strain/berserker/apply_strain(mob/living/carbon/xenomorph/ravager/ravager)
ravager.plasma_max = 0
ravager.health_modifier -= XENO_HEALTH_MOD_MED
ravager.armor_modifier += XENO_ARMOR_MOD_VERY_SMALL
ravager.speed_modifier += XENO_SPEED_FASTMOD_TIER_3
ravager.received_phero_caps["frenzy"] = 2.9 // Moderate
- mutator_update_actions(ravager)
- mutator_set.recalculate_actions(description, flavor_description)
-
- apply_behavior_holder(ravager)
ravager.recalculate_everything()
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/ravager/hedgehog.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/hedgehog.dm
similarity index 87%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/ravager/hedgehog.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/hedgehog.dm
index 913883549fba..e1d6dc583416 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/ravager/hedgehog.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/hedgehog.dm
@@ -1,41 +1,28 @@
-/datum/xeno_mutator/hedgehog
- name = "STRAIN: Ravager - Hedgehog"
+/datum/xeno_strain/hedgehog
+ name = RAVAGER_HEDGEHOG
description = "You lose your empower, charge, scissor cut and some slash damage, for a bit more explosive resistance, immunity to small explosions, and you gain several new abilities that allow you to become a spiky tank. You build up shards internally over time and also when taking damage that increase your armor's resilience. You can use these shards to power three new abilities: Spike Shield, which gives you a temporary shield that spits bone shards around you when damaged, Fire Spikes, which launches spikes at your target that slows them and does extra damage if they move, and finally, Spike Shed, which launches spikes all around yourself and gives you a temporary speed boost as an escape plan at the cost of all your stored shards and being unable to gain shards for thirty seconds."
flavor_description = "They will be of iron will and steely muscle. In great armor shall they be clad, and with the mightiest spikes will they be armed."
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_RAVAGER) // Only Ravager.
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Hedgehog"
+
+ actions_to_remove = list(
/datum/action/xeno_action/onclick/empower,
/datum/action/xeno_action/activable/pounce/charge,
/datum/action/xeno_action/activable/scissor_cut,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/onclick/spike_shield,
/datum/action/xeno_action/activable/rav_spikes,
/datum/action/xeno_action/onclick/spike_shed,
)
- behavior_delegate_type = /datum/behavior_delegate/ravager_hedgehog
- keystone = TRUE
-
-/datum/xeno_mutator/hedgehog/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
- var/mob/living/carbon/xenomorph/ravager/ravager = mutator_set.xeno
+ behavior_delegate_type = /datum/behavior_delegate/ravager_hedgehog
- ravager.mutation_type = RAVAGER_HEDGEHOG
+/datum/xeno_strain/hedgehog/apply_strain(mob/living/carbon/xenomorph/ravager/ravager)
ravager.plasma_max = 0
ravager.small_explosives_stun = FALSE
ravager.explosivearmor_modifier += XENO_EXPOSIVEARMOR_MOD_SMALL
ravager.damage_modifier -= XENO_DAMAGE_MOD_SMALL
- apply_behavior_holder(ravager)
-
- mutator_update_actions(ravager)
- mutator_set.recalculate_actions(description, flavor_description)
-
ravager.recalculate_everything()
/datum/behavior_delegate/ravager_hedgehog
diff --git a/code/modules/mob/living/carbon/xenomorph/mutators/strains/runner/acid.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/runner/acid.dm
similarity index 90%
rename from code/modules/mob/living/carbon/xenomorph/mutators/strains/runner/acid.dm
rename to code/modules/mob/living/carbon/xenomorph/strains/castes/runner/acid.dm
index 490e5ca36cba..7b9bafadeb7b 100644
--- a/code/modules/mob/living/carbon/xenomorph/mutators/strains/runner/acid.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/runner/acid.dm
@@ -1,37 +1,27 @@
-/datum/xeno_mutator/acider
- name = "STRAIN: Runner - Acider"
+/datum/xeno_strain/acider
+ name = RUNNER_ACIDER
description = "At the cost of a little bit of your speed and all of your current abilities, you gain a considerable amount of health, some armor, and a new organ that fills with volatile acid over time. Your Tail Stab and slashes apply acid to living lifeforms that slowly burns them, and slashes against targets with acid stacks fill your acid glands. You also gain Corrosive Acid equivalent to that of a boiler that you can deploy more quickly than any other caste, at the cost of a chunk of your acid reserves with each use. Finally, after a twenty second windup, you can force your body to explode, covering everything near you with acid. The more acid you have stored, the more devastating the explosion will be, but during those twenty seconds before detonation you are slowed and give off several warning signals which give talls an opportunity to end you before you can detonate. If you successfully explode, you will reincarnate as a larva again!"
flavor_description = "Burn their walls, maim their faces! Your life, for The Hive!"
- cost = MUTATOR_COST_EXPENSIVE
- individual_only = TRUE
- caste_whitelist = list(XENO_CASTE_RUNNER)
- keystone = TRUE
- behavior_delegate_type = /datum/behavior_delegate/runner_acider
- mutator_actions_to_remove = list(
+ icon_state_prefix = "Acider"
+
+ actions_to_remove = list(
/datum/action/xeno_action/activable/pounce/runner,
/datum/action/xeno_action/activable/runner_skillshot,
/datum/action/xeno_action/onclick/toggle_long_range/runner,
)
- mutator_actions_to_add = list(
+ actions_to_add = list(
/datum/action/xeno_action/activable/acider_acid,
/datum/action/xeno_action/activable/acider_for_the_hive,
)
-/datum/xeno_mutator/acider/apply_mutator(datum/mutator_set/individual_mutators/mutator_set)
- . = ..()
- if (. == 0)
- return
+ behavior_delegate_type = /datum/behavior_delegate/runner_acider
- var/mob/living/carbon/xenomorph/runner/runner = mutator_set.xeno
- runner.mutation_icon_state = RUNNER_ACIDER
- runner.mutation_type = RUNNER_ACIDER
+/datum/xeno_strain/acider/apply_strain(mob/living/carbon/xenomorph/runner/runner)
runner.speed_modifier += XENO_SPEED_SLOWMOD_TIER_5
runner.armor_modifier += XENO_ARMOR_MOD_MED
runner.health_modifier += XENO_HEALTH_MOD_ACIDER
- apply_behavior_holder(runner)
- mutator_update_actions(runner)
+
runner.recalculate_everything()
- mutator_set.recalculate_actions(description, flavor_description)
/datum/behavior_delegate/runner_acider
var/acid_amount = 0
diff --git a/code/modules/mob/living/carbon/xenomorph/strains/xeno_strain.dm b/code/modules/mob/living/carbon/xenomorph/strains/xeno_strain.dm
new file mode 100644
index 000000000000..18f1f892ddfa
--- /dev/null
+++ b/code/modules/mob/living/carbon/xenomorph/strains/xeno_strain.dm
@@ -0,0 +1,131 @@
+/datum/xeno_strain
+ /// The name of the strain. Should be short but informative.
+ var/name
+ /// Description to be displayed on purchase.
+ var/description
+ /// (OPTIONAL) Flavor text to be shown on purchase. Semi-OOC
+ var/flavor_description
+ /// (OPTIONAL) A custom icon state prefix for xenos who have taken the strain.
+ var/icon_state_prefix
+
+ /// A list of action typepaths which should be removed when a xeno takes the strain.
+ var/list/actions_to_remove
+ /// A list of action typepaths which should be added when a xeno takes the strain.
+ var/list/actions_to_add
+
+ /// Typepath of the [/datum/behavior_delegate] to add.
+ var/behavior_delegate_type
+
+/**
+ * Add this strain to `xeno`, replacing their actions and behavior holder.
+ *
+ * Returns a bool indicating if the strain was successfully applied.
+ * **Override [/datum/xeno_strain/proc/apply_strain], not this! (Unless you know what you're doing.)**
+ */
+/datum/xeno_strain/proc/_add_to_xeno(mob/living/carbon/xenomorph/xeno)
+ SHOULD_NOT_OVERRIDE(TRUE)
+
+ xeno.strain = src
+
+ // Update the xeno's actions.
+ for(var/action_path in actions_to_remove)
+ remove_action(xeno, action_path)
+ for(var/action_path in actions_to_add)
+ give_action(xeno, action_path)
+
+ // Update the xeno's behavior delegate.
+ if(behavior_delegate_type)
+ if(xeno.behavior_delegate)
+ qdel(xeno.behavior_delegate)
+ xeno.behavior_delegate = new behavior_delegate_type()
+ xeno.behavior_delegate.bound_xeno = xeno
+ xeno.behavior_delegate.add_to_xeno()
+
+ apply_strain(xeno)
+
+ xeno.update_icons()
+ xeno.hive.hive_ui.update_xeno_info()
+
+ // Give them all of the info about the strain.
+ to_chat(xeno, SPAN_XENOANNOUNCE(description))
+ if(flavor_description)
+ to_chat(xeno, SPAN_XENOLEADER(flavor_description))
+ return TRUE
+
+/**
+ * Adds any special modifiers/changes from this strain to `xeno`.
+ *
+ * Called when the strain is first added to the player.
+ */
+/datum/xeno_strain/proc/apply_strain(mob/living/carbon/xenomorph/xeno)
+ // Override with custom behaviour.
+ return
+
+
+/mob/living/carbon/xenomorph/verb/purchase_strain()
+ set name = "Purchase Strain"
+ set desc = "Purchase a strain for yourself"
+ set category = "Alien"
+
+ // Firstly, make sure the xeno is actually able to take a strain.
+ if(!can_take_strain())
+ return
+
+ // Make an assoc list of {name: typepath} from the strains available to the xeno's caste.
+ var/list/strain_list = list()
+ for(var/datum/xeno_strain/strain_type as anything in caste.available_strains)
+ strain_list[initial(strain_type.name)] = strain_type
+
+ // Ask the user which strain they want.
+ var/strain_choice = tgui_input_list(usr, "Which strain would you like to take?", "Choose Strain", strain_list, theme = "hive_status")
+ if(!strain_choice)
+ return
+ var/datum/xeno_strain/chosen_strain = strain_list[strain_choice]
+
+ // Check again after the user picks one, in case anything changed.
+ if(!can_take_strain())
+ return
+ // Show the user the strain's description, and double check that they want it.
+ if(alert(usr, "[initial(chosen_strain.description)]\n\nConfirm mutation?", "Choose Strain", "Yes", "No") != "Yes")
+ return
+ // One more time after they confirm.
+ if(!can_take_strain())
+ return
+
+ // Create the strain datum and apply it to the xeno.
+ var/datum/xeno_strain/strain_instance = new chosen_strain()
+ if(strain_instance._add_to_xeno(src))
+ xeno_jitter(1.5 SECONDS)
+ // If it applied successfully, add it to the logs.
+ log_strain("[name] purchased strain '[strain_instance.type]'")
+
+/// Is this xeno currently able to take a strain?
+/mob/living/carbon/xenomorph/proc/can_take_strain()
+ if(!length(caste.available_strains) || !check_state(TRUE))
+ return FALSE
+
+ if(strain)
+ to_chat(src, SPAN_WARNING("We have already chosen a strain."))
+ return FALSE
+
+ if(is_ventcrawling)
+ to_chat(src, SPAN_WARNING("This place is too constraining to take a strain."))
+ return FALSE
+
+ if(!isturf(loc))
+ to_chat(src, SPAN_WARNING("We can't take a strain here."))
+ return FALSE
+
+ if(handcuffed || legcuffed)
+ to_chat(src, SPAN_WARNING("The restraints are too restricting to allow us to take a strain."))
+ return FALSE
+
+ if(health < maxHealth)
+ to_chat(src, SPAN_WARNING("We must be at full health to take a strain."))
+ return FALSE
+
+ if(agility || fortify || crest_defense || stealth)
+ to_chat(src, SPAN_WARNING("We cannot take a strain while in this stance."))
+ return FALSE
+
+ return TRUE
diff --git a/code/modules/mob/living/carbon/xenomorph/update_icons.dm b/code/modules/mob/living/carbon/xenomorph/update_icons.dm
index 55995ec0b264..571f261ab981 100644
--- a/code/modules/mob/living/carbon/xenomorph/update_icons.dm
+++ b/code/modules/mob/living/carbon/xenomorph/update_icons.dm
@@ -41,7 +41,7 @@
Q.queen_standing_icon = icon_xeno
Q.queen_ovipositor_icon = 'icons/mob/xenos/ovipositor.dmi'
- var/mutation_caste_state = "[mutation_type] [caste.caste_type]"
+ var/mutation_caste_state = "[get_strain_icon()] [caste.caste_type]"
if(!walking_state_cache[mutation_caste_state])
var/cache_walking_state = FALSE
for(var/state in icon_states(icon))
@@ -64,7 +64,7 @@
if(behavior_delegate?.on_update_icons())
return
- var/mutation_caste_state = "[mutation_icon_state || mutation_type] [caste.caste_type]"
+ var/mutation_caste_state = "[get_strain_icon()] [caste.caste_type]"
if(stat == DEAD)
icon_state = "[mutation_caste_state] Dead"
if(!(icon_state in icon_states(icon_xeno)))
diff --git a/code/modules/mob/living/carbon/xenomorph/xeno_helpers.dm b/code/modules/mob/living/carbon/xenomorph/xeno_helpers.dm
index 7324a3af6892..2e4b968d5a59 100644
--- a/code/modules/mob/living/carbon/xenomorph/xeno_helpers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/xeno_helpers.dm
@@ -34,6 +34,23 @@
return 100
return round(armor_integrity * 100 / armor_integrity_max)
+/**
+ * Returns the name of the xeno's strain, if it has one.
+ *
+ * If that can't be found, returns "Normal".
+ */
+/mob/living/carbon/xenomorph/proc/get_strain_name()
+ return strain?.name || "Normal"
+
+/**
+ * Returns the custom icon state from the xeno's strain, if it has one.
+ *
+ * If that can't be found, returns "Normal"
+ */
+/mob/living/carbon/xenomorph/proc/get_strain_icon()
+ return strain?.icon_state_prefix || "Normal"
+ // TODO: Go through xeno/xenoid sprites and remove "Normal", so that this isn't needed.
+
//These don't do much currently. Or anything? Only around for legacy code.
/mob/living/carbon/xenomorph/is_mob_restrained()
return 0
@@ -58,4 +75,4 @@
return caste.fire_intensity_resistance
/mob/living/carbon/xenomorph/alter_ghost(mob/dead/observer/ghost)
- ghost.icon_state = "[mutation_type] [caste.caste_type] Running"
+ ghost.icon_state = "[get_strain_icon()] [caste.caste_type] Running"
diff --git a/code/modules/mob/mob_verbs.dm b/code/modules/mob/mob_verbs.dm
index 1ba8985d56bd..f12d00cc0988 100644
--- a/code/modules/mob/mob_verbs.dm
+++ b/code/modules/mob/mob_verbs.dm
@@ -50,34 +50,33 @@
to_chat(usr, SPAN_DANGER("This mob type cannot throw items."))
return
-/mob/proc/point_to(atom/A in view())
+/mob/proc/point_to(atom/target in view())
//set name = "Point To"
//set category = "Object"
- if(!isturf(src.loc) || !(A in view(src)))//target is no longer visible to us
- return 0
+ if(!isturf(src.loc) || !(target in view(src)))//target is no longer visible to us
+ return FALSE
- if(!A.mouse_opacity)//can't click it? can't point at it.
- return 0
+ if(!target.mouse_opacity)//can't click it? can't point at it.
+ return FALSE
if(is_mob_incapacitated() || (status_flags & FAKEDEATH)) //incapacitated, can't point
- return 0
+ return FALSE
- var/tile = get_turf(A)
- if (!tile)
- return 0
+ var/tile = get_turf(target)
+ if(!tile)
+ return FALSE
if(recently_pointed_to > world.time)
- return 0
-
- next_move = world.time + 2
-
- point_to_atom(A, tile)
- return 1
-
+ return FALSE
+ if(SEND_SIGNAL(src, COMSIG_MOB_TRY_POINT, target) & COMPONENT_OVERRIDE_POINT)
+ return FALSE
+ next_move = world.time + 2
+ point_to_atom(target, tile)
+ return TRUE
/mob/verb/memory()
set name = "Notes"
diff --git a/code/modules/projectiles/gun_attachables.dm b/code/modules/projectiles/gun_attachables.dm
index 1807213f6226..37094bb12775 100644
--- a/code/modules/projectiles/gun_attachables.dm
+++ b/code/modules/projectiles/gun_attachables.dm
@@ -3301,6 +3301,10 @@ Defined in conflicts.dm of the #defines folder.
var/bipod_deployed = FALSE
/// If this should anchor the user while in use
var/heavy_bipod = FALSE
+ // Are switching to full auto when deploying the bipod
+ var/full_auto_switch = FALSE
+ // Store our old firemode so we can switch to it when undeploying the bipod
+ var/old_firemode = null
/obj/item/attachable/bipod/New()
..()
@@ -3311,16 +3315,33 @@ Defined in conflicts.dm of the #defines folder.
scatter_mod = SCATTER_AMOUNT_TIER_9
recoil_mod = RECOIL_AMOUNT_TIER_5
-/obj/item/attachable/bipod/Attach(obj/item/weapon/gun/G)
+/obj/item/attachable/bipod/Attach(obj/item/weapon/gun/gun, mob/user)
..()
- RegisterSignal(G, COMSIG_ITEM_DROPPED, PROC_REF(handle_drop))
+ if((GUN_FIREMODE_AUTOMATIC in gun.gun_firemode_list) || (gun.flags_gun_features & GUN_SUPPORT_PLATFORM))
+ var/given_action = FALSE
+ if(user && (gun == user.l_hand || gun == user.r_hand))
+ give_action(user, /datum/action/item_action/bipod/toggle_full_auto_switch, src, gun)
+ given_action = TRUE
+ if(!given_action)
+ new /datum/action/item_action/bipod/toggle_full_auto_switch(src, gun)
+
+ RegisterSignal(gun, COMSIG_ITEM_DROPPED, PROC_REF(handle_drop))
/obj/item/attachable/bipod/Detach(mob/user, obj/item/weapon/gun/detaching_gub)
UnregisterSignal(detaching_gub, COMSIG_ITEM_DROPPED)
+ //clear out anything related to full auto switching
+ full_auto_switch = FALSE
+ old_firemode = null
+ for(var/item_action in detaching_gub.actions)
+ var/datum/action/item_action/bipod/toggle_full_auto_switch/target_action = item_action
+ if(target_action.target == src)
+ qdel(item_action)
+ break
+
if(bipod_deployed)
- undeploy_bipod(detaching_gub)
+ undeploy_bipod(detaching_gub, user)
..()
/obj/item/attachable/bipod/update_icon()
@@ -3334,60 +3355,62 @@ Defined in conflicts.dm of the #defines folder.
if(istype(loc, /obj/item/weapon/gun))
var/obj/item/weapon/gun/gun = loc
gun.update_attachable(slot)
- for(var/datum/action/A as anything in gun.actions)
- A.update_button_icon()
+ for(var/datum/action/item_action as anything in gun.actions)
+ if(!istype(item_action, /datum/action/item_action/bipod/toggle_full_auto_switch))
+ item_action.update_button_icon()
-/obj/item/attachable/bipod/proc/handle_drop(obj/item/weapon/gun/G, mob/living/carbon/human/user)
+/obj/item/attachable/bipod/proc/handle_drop(obj/item/weapon/gun/gun, mob/living/carbon/human/user)
SIGNAL_HANDLER
UnregisterSignal(user, COMSIG_MOB_MOVE_OR_LOOK)
if(bipod_deployed)
- undeploy_bipod(G)
+ undeploy_bipod(gun, user)
user.apply_effect(1, SUPERSLOW)
user.apply_effect(2, SLOW)
-/obj/item/attachable/bipod/proc/undeploy_bipod(obj/item/weapon/gun/G)
- REMOVE_TRAIT(G, TRAIT_GUN_BIPODDED, "attached_bipod")
+/obj/item/attachable/bipod/proc/undeploy_bipod(obj/item/weapon/gun/gun, mob/user)
+ REMOVE_TRAIT(gun, TRAIT_GUN_BIPODDED, "attached_bipod")
bipod_deployed = FALSE
accuracy_mod = -HIT_ACCURACY_MULT_TIER_5
scatter_mod = SCATTER_AMOUNT_TIER_9
recoil_mod = RECOIL_AMOUNT_TIER_5
burst_scatter_mod = 0
delay_mod = FIRE_DELAY_TIER_12
- G.recalculate_attachment_bonuses()
- G.stop_fire()
- var/mob/living/user
- if(isliving(G.loc))
- user = G.loc
- SEND_SIGNAL(user, COMSIG_MOB_UNDEPLOYED_BIPOD)
- UnregisterSignal(user, COMSIG_MOB_MOVE_OR_LOOK)
+ //if we are no longer on full auto, don't bother switching back to the old firemode
+ if(full_auto_switch && gun.gun_firemode == GUN_FIREMODE_AUTOMATIC && gun.gun_firemode != old_firemode)
+ gun.do_toggle_firemode(user, null, old_firemode)
- if(G.flags_gun_features & GUN_SUPPORT_PLATFORM)
- G.remove_firemode(GUN_FIREMODE_AUTOMATIC)
+ gun.recalculate_attachment_bonuses()
+ gun.stop_fire()
+ SEND_SIGNAL(user, COMSIG_MOB_UNDEPLOYED_BIPOD)
+ UnregisterSignal(user, COMSIG_MOB_MOVE_OR_LOOK)
+
+ if(gun.flags_gun_features & GUN_SUPPORT_PLATFORM)
+ gun.remove_firemode(GUN_FIREMODE_AUTOMATIC)
if(heavy_bipod)
user.anchored = FALSE
- if(!QDELETED(G))
+ if(!QDELETED(gun))
playsound(user,'sound/items/m56dauto_rotate.ogg', 55, 1)
update_icon()
-/obj/item/attachable/bipod/activate_attachment(obj/item/weapon/gun/G,mob/living/user, turn_off)
+/obj/item/attachable/bipod/activate_attachment(obj/item/weapon/gun/gun, mob/living/user, turn_off)
if(turn_off)
if(bipod_deployed)
- undeploy_bipod(G)
+ undeploy_bipod(gun, user)
else
- var/obj/support = check_bipod_support(G, user)
+ var/obj/support = check_bipod_support(gun, user)
if(!support&&!bipod_deployed)
to_chat(user, SPAN_NOTICE("You start deploying [src] on the ground."))
- if(!do_after(user, 15, INTERRUPT_ALL, BUSY_ICON_HOSTILE, G,INTERRUPT_DIFF_LOC))
+ if(!do_after(user, 15, INTERRUPT_ALL, BUSY_ICON_HOSTILE, gun, INTERRUPT_DIFF_LOC))
return FALSE
bipod_deployed = !bipod_deployed
if(user)
if(bipod_deployed)
- ADD_TRAIT(G, TRAIT_GUN_BIPODDED, "attached_bipod")
+ ADD_TRAIT(gun, TRAIT_GUN_BIPODDED, "attached_bipod")
to_chat(user, SPAN_NOTICE("You deploy [src] [support ? "on [support]" : "on the ground"]."))
SEND_SIGNAL(user, COMSIG_MOB_DEPLOYED_BIPOD)
playsound(user,'sound/items/m56dauto_rotate.ogg', 55, 1)
@@ -3395,25 +3418,29 @@ Defined in conflicts.dm of the #defines folder.
scatter_mod = -SCATTER_AMOUNT_TIER_10
recoil_mod = -RECOIL_AMOUNT_TIER_4
burst_scatter_mod = -SCATTER_AMOUNT_TIER_8
- if(istype(G,/obj/item/weapon/gun/rifle/sniper/M42A))
+ if(istype(gun, /obj/item/weapon/gun/rifle/sniper/M42A))
delay_mod = -FIRE_DELAY_TIER_7
else
delay_mod = -FIRE_DELAY_TIER_12
- G.recalculate_attachment_bonuses()
- G.stop_fire()
+ gun.recalculate_attachment_bonuses()
+ gun.stop_fire()
initial_mob_dir = user.dir
RegisterSignal(user, COMSIG_MOB_MOVE_OR_LOOK, PROC_REF(handle_mob_move_or_look))
- if(G.flags_gun_features & GUN_SUPPORT_PLATFORM)
- G.add_firemode(GUN_FIREMODE_AUTOMATIC)
+ if(gun.flags_gun_features & GUN_SUPPORT_PLATFORM)
+ gun.add_firemode(GUN_FIREMODE_AUTOMATIC)
if(heavy_bipod)
user.anchored = TRUE
+ old_firemode = gun.gun_firemode
+ if(full_auto_switch && gun.gun_firemode != GUN_FIREMODE_AUTOMATIC)
+ gun.do_toggle_firemode(user, null, GUN_FIREMODE_AUTOMATIC)
+
else
to_chat(user, SPAN_NOTICE("You retract [src]."))
- undeploy_bipod(G)
+ undeploy_bipod(gun, user)
update_icon()
@@ -3430,10 +3457,10 @@ Defined in conflicts.dm of the #defines folder.
//when user fires the gun, we check if they have something to support the gun's bipod.
-/obj/item/attachable/proc/check_bipod_support(obj/item/weapon/gun/G, mob/living/user)
+/obj/item/attachable/proc/check_bipod_support(obj/item/weapon/gun/gun, mob/living/user)
return 0
-/obj/item/attachable/bipod/check_bipod_support(obj/item/weapon/gun/G, mob/living/user)
+/obj/item/attachable/bipod/check_bipod_support(obj/item/weapon/gun/gun, mob/living/user)
var/turf/T = get_turf(user)
for(var/obj/O in T)
if(O.throwpass && O.density && O.dir == user.dir && O.flags_atom & ON_BORDER)
@@ -3445,6 +3472,31 @@ Defined in conflicts.dm of the #defines folder.
return O2
return 0
+//item actions for handling deployment to full auto.
+/datum/action/item_action/bipod/toggle_full_auto_switch/New(Target, obj/item/holder)
+ . = ..()
+ name = "Toggle Full Auto Switch"
+ action_icon_state = "full_auto_switch"
+ button.name = name
+ button.overlays.Cut()
+ button.overlays += image('icons/mob/hud/actions.dmi', button, action_icon_state)
+
+/datum/action/item_action/bipod/toggle_full_auto_switch/action_activate()
+ var/obj/item/weapon/gun/holder_gun = holder_item
+ var/obj/item/attachable/bipod/attached_bipod = holder_gun.attachments["under"]
+
+ attached_bipod.full_auto_switch = !attached_bipod.full_auto_switch
+ to_chat(owner, SPAN_NOTICE("[icon2html(holder_gun, owner)] You will [attached_bipod.full_auto_switch? "start" : "stop"] switching to full auto when deploying the bipod."))
+ playsound(owner, 'sound/weapons/handling/gun_burst_toggle.ogg', 15, 1)
+
+ if(attached_bipod.full_auto_switch)
+ button.icon_state = "template_on"
+ else
+ button.icon_state = "template"
+
+ button.overlays.Cut()
+ button.overlays += image('icons/mob/hud/actions.dmi', button, action_icon_state)
+
/obj/item/attachable/bipod/m60
name = "bipod"
diff --git a/code/modules/projectiles/gun_helpers.dm b/code/modules/projectiles/gun_helpers.dm
index 66e3a65f2f77..781db8fc1222 100644
--- a/code/modules/projectiles/gun_helpers.dm
+++ b/code/modules/projectiles/gun_helpers.dm
@@ -390,7 +390,7 @@ DEFINES in setup.dm, referenced here.
user.visible_message(SPAN_NOTICE("[user] attaches [attachment] to [src]."),
SPAN_NOTICE("You attach [attachment] to [src]."), null, 4)
user.temp_drop_inv_item(attachment)
- attachment.Attach(src)
+ attachment.Attach(src, user)
update_attachable(attachment.slot)
playsound(user, 'sound/handling/attachment_add.ogg', 15, 1, 4)
return TRUE
diff --git a/code/modules/projectiles/guns/rifles.dm b/code/modules/projectiles/guns/rifles.dm
index 29b2c55b7ec0..b631f093bafe 100644
--- a/code/modules/projectiles/guns/rifles.dm
+++ b/code/modules/projectiles/guns/rifles.dm
@@ -1763,9 +1763,9 @@
/obj/item/weapon/gun/rifle/rmc_f90/set_gun_config_values()
..()
- fire_delay = FIRE_DELAY_TIER_8
+ fire_delay = FIRE_DELAY_TIER_11
burst_amount = BURST_AMOUNT_TIER_3
- burst_delay = FIRE_DELAY_TIER_8
+ burst_delay = FIRE_DELAY_TIER_11
accuracy_mult = BASE_ACCURACY_MULT + HIT_ACCURACY_MULT_TIER_4 + 2*HIT_ACCURACY_MULT_TIER_1
accuracy_mult_unwielded = BASE_ACCURACY_MULT - HIT_ACCURACY_MULT_TIER_7
scatter = SCATTER_AMOUNT_TIER_8
@@ -1804,7 +1804,7 @@
/obj/item/weapon/gun/rifle/rmc_f90/scope/set_gun_config_values()
..()
- fire_delay = FIRE_DELAY_TIER_7
+ fire_delay = FIRE_DELAY_TIER_11
burst_amount = 0
accuracy_mult = BASE_ACCURACY_MULT + HIT_ACCURACY_MULT_TIER_4 + 2*HIT_ACCURACY_MULT_TIER_1
accuracy_mult_unwielded = BASE_ACCURACY_MULT - HIT_ACCURACY_MULT_TIER_7
@@ -1844,9 +1844,9 @@
/obj/item/weapon/gun/rifle/rmc_f90/shotgun/set_gun_config_values()
..()
- fire_delay = FIRE_DELAY_TIER_8
+ fire_delay = FIRE_DELAY_TIER_11
burst_amount = BURST_AMOUNT_TIER_3
- burst_delay = FIRE_DELAY_TIER_8
+ burst_delay = FIRE_DELAY_TIER_11
accuracy_mult = BASE_ACCURACY_MULT + HIT_ACCURACY_MULT_TIER_4 + 2*HIT_ACCURACY_MULT_TIER_1
accuracy_mult_unwielded = BASE_ACCURACY_MULT - HIT_ACCURACY_MULT_TIER_2
scatter = SCATTER_AMOUNT_TIER_8
diff --git a/code/modules/tgui/tgui_number_input.dm b/code/modules/tgui/tgui_number_input.dm
index 4c06b5333b6d..356448853db3 100644
--- a/code/modules/tgui/tgui_number_input.dm
+++ b/code/modules/tgui/tgui_number_input.dm
@@ -145,6 +145,7 @@
"min_value" = min_value,
"preferences" = list(),
"title" = title,
+ "integer_only" = integer_only
)
/datum/tgui_input_number/ui_data(mob/user)
diff --git a/code/modules/tgui_panel/telemetry.dm b/code/modules/tgui_panel/telemetry.dm
index eb5c7d96a4d9..4ef1f06bfac0 100644
--- a/code/modules/tgui_panel/telemetry.dm
+++ b/code/modules/tgui_panel/telemetry.dm
@@ -86,7 +86,7 @@
// Check for a malformed history object
if (!row || row.len < 3 || (!row["ckey"] || !row["address"] || !row["computer_id"]))
- return
+ continue
/* TODO - Reintroduce this when we get a proper round ID tracking,
and we want to log it to database
@@ -103,7 +103,7 @@
continue
*/
- if (world.IsBanned(row["ckey"], row["address"], row["computer_id"], real_bans_only = TRUE))
+ if (world.IsBanned(row["ckey"], row["address"], row["computer_id"], real_bans_only = TRUE, is_telemetry = TRUE))
found = row
break
diff --git a/colonialmarines.dme b/colonialmarines.dme
index 64760c4f2bc9..f3dc6f1f81d9 100644
--- a/colonialmarines.dme
+++ b/colonialmarines.dme
@@ -298,6 +298,7 @@
#include "code\controllers\subsystem\sound_loops.dm"
#include "code\controllers\subsystem\soundscape.dm"
#include "code\controllers\subsystem\statpanel.dm"
+#include "code\controllers\subsystem\stickyban.dm"
#include "code\controllers\subsystem\techtree.dm"
#include "code\controllers\subsystem\tgui.dm"
#include "code\controllers\subsystem\ticker.dm"
@@ -406,6 +407,7 @@
#include "code\datums\components\overlay_lighting.dm"
#include "code\datums\components\rename.dm"
#include "code\datums\components\speed_modifier.dm"
+#include "code\datums\components\temporary_mute.dm"
#include "code\datums\components\toxin_buildup.dm"
#include "code\datums\components\tutorial_status.dm"
#include "code\datums\components\weed_damage_reduction.dm"
@@ -1397,7 +1399,6 @@
#include "code\modules\admin\NewBan.dm"
#include "code\modules\admin\player_notes.dm"
#include "code\modules\admin\server_verbs.dm"
-#include "code\modules\admin\stickyban.dm"
#include "code\modules\admin\STUI.dm"
#include "code\modules\admin\tag.dm"
#include "code\modules\admin\medal_panel\medals_panel.dm"
@@ -1967,7 +1968,6 @@
#include "code\modules\mob\living\carbon\xenomorph\xeno_verbs.dm"
#include "code\modules\mob\living\carbon\xenomorph\XenoAttacks.dm"
#include "code\modules\mob\living\carbon\xenomorph\Xenomorph.dm"
-#include "code\modules\mob\living\carbon\xenomorph\XenoMutatorSets.dm"
#include "code\modules\mob\living\carbon\xenomorph\XenoOverwatch.dm"
#include "code\modules\mob\living\carbon\xenomorph\XenoProcs.dm"
#include "code\modules\mob\living\carbon\xenomorph\XenoUpgrade.dm"
@@ -2047,23 +2047,24 @@
#include "code\modules\mob\living\carbon\xenomorph\castes\Spitter.dm"
#include "code\modules\mob\living\carbon\xenomorph\castes\Warrior.dm"
#include "code\modules\mob\living\carbon\xenomorph\items\iff_tag.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\behavior_delegate.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\mutator.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\boiler\trapper.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\carrier\eggsac.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\crusher\charger.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\defender\steel_crest.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\drone\gardener.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\drone\healer.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\hivelord\resin_whisperer.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\lurker\vampire.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\praetorian\dancer.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\praetorian\oppressor.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\praetorian\vanguard.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\praetorian\warden.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\ravager\berserker.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\ravager\hedgehog.dm"
-#include "code\modules\mob\living\carbon\xenomorph\mutators\strains\runner\acid.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\behavior_delegate.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\xeno_strain.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\boiler\trapper.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\carrier\eggsac.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\crusher\charger.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\defender\steel_crest.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\drone\gardener.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\drone\healer.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\facehugger\watcher.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\hivelord\resin_whisperer.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\lurker\vampire.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\praetorian\dancer.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\praetorian\oppressor.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\praetorian\vanguard.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\praetorian\warden.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\ravager\berserker.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\ravager\hedgehog.dm"
+#include "code\modules\mob\living\carbon\xenomorph\strains\castes\runner\acid.dm"
#include "code\modules\mob\living\silicon\death.dm"
#include "code\modules\mob\living\silicon\login.dm"
#include "code\modules\mob\living\silicon\say.dm"
diff --git a/html/changelogs/archive/2024-02.yml b/html/changelogs/archive/2024-02.yml
index f8d85764a2a3..16fd2fd70475 100644
--- a/html/changelogs/archive/2024-02.yml
+++ b/html/changelogs/archive/2024-02.yml
@@ -164,3 +164,51 @@
- ui: Made the Tutorial Menu automatically close when a tutorial is started.
- code_imp: Made the tutorial Xenomorph not inherit the user's 'age' prefix.
- code_imp: Added a basic unit test for tutorials.
+2024-02-14:
+ SabreML:
+ - bugfix: Fixed crates and similar objects layering below the dropship's fulton
+ recovery system.
+ Zonespace Drathek:
+ - balance: Lesser drones and Facehuggers cannot speak, custom emote, or point for
+ 3 minutes after spawning
+2024-02-15:
+ InsaneRed:
+ - balance: Drop pouch now scales the same as normal webbing
+ SabreML:
+ - bugfix: Fixed some changelog icons being smaller than others.
+ Vicacrov:
+ - maptweak: LV-624 tfort's floodlights are now colony floodlights.
+ VileBeggar:
+ - qol: Attaching the bipod to weapons that have full auto (HPR included) will give
+ you a button toggle for immediately switching to full auto when deploying the
+ bipod.
+2024-02-16:
+ BadAtThisGame302:
+ - rscadd: Added two new beacons for the CL, Lawyer and Bodyguard.
+ - rscadd: Added an Executive Specialist and Executive Supervisor Lawyer preset.
+ - rscadd: Added a cyanide pill in the CLs briefcase for an easy way out.
+ - rscadd: Added a Wey-Yu Coffee Mug in the CLs briefcase as a souvenir sale.
+ Drathek:
+ - bugfix: Fixed xeno tutorial allowing evolution/de-evolution
+ - bugfix: Fixed some runtimes with tutorials
+ SabreML:
+ - bugfix: Fixed the 'Clear' button not resetting the number of keybind assignment
+ slots. (Keybinds menu)
+ - refactor: Refactored xeno strains.
+ - rscadd: Added a keybind setting to open the 'Purchase Strain' window. (Also renamed
+ the macro)
+ cuberound:
+ - balance: m4ra ammo looses about 1/5 of initial effect duration per 10 tiles traveled
+ harryob:
+ - admin: permanent and sticky bans are now handled in game, instead of being managed
+ externally
+2024-02-17:
+ MobiusWon:
+ - rscadd: Adds integral Motion Detector to XM4 IO Armor
+ - balance: Uniform utility accessories (I.E. Webbing) no longer can be equipped
+ while the XM4 is worn. Armor slots increased to 5 to compensate.
+ Steelpoint:
+ - balance: Royal Marine Commando's firearms now have a higher rate of fire, comparable
+ to the M41A.
+ mullenpaul:
+ - code_imp: rework logic of restrictedinput component to reduce checks
diff --git a/icons/mob/hud/actions.dmi b/icons/mob/hud/actions.dmi
index 820da22ea5b1..3c3e6dd9de3d 100644
Binary files a/icons/mob/hud/actions.dmi and b/icons/mob/hud/actions.dmi differ
diff --git a/icons/mob/xenos/burrower.dmi b/icons/mob/xenos/burrower.dmi
index f546776e6b27..c54684e61bc9 100644
Binary files a/icons/mob/xenos/burrower.dmi and b/icons/mob/xenos/burrower.dmi differ
diff --git a/maps/map_files/LV624/LV624.dmm b/maps/map_files/LV624/LV624.dmm
index cd28e7341917..0e1948deadf3 100644
--- a/maps/map_files/LV624/LV624.dmm
+++ b/maps/map_files/LV624/LV624.dmm
@@ -1126,9 +1126,7 @@
/area/lv624/ground/barrens/containers)
"afu" = (
/obj/item/ammo_casing,
-/obj/structure/machinery/floodlight/landing{
- name = "bolted floodlight"
- },
+/obj/structure/machinery/colony_floodlight,
/turf/open/floor/plating{
dir = 9;
icon_state = "warnplate"
@@ -1162,9 +1160,7 @@
},
/area/lv624/ground/barrens/central_barrens)
"afy" = (
-/obj/structure/machinery/floodlight/landing{
- name = "bolted floodlight"
- },
+/obj/structure/machinery/colony_floodlight,
/turf/open/floor/plating{
dir = 5;
icon_state = "warnplate"
@@ -1409,9 +1405,7 @@
/area/lv624/ground/barrens/central_barrens)
"agF" = (
/obj/item/ammo_casing,
-/obj/structure/machinery/floodlight/landing{
- name = "bolted floodlight"
- },
+/obj/structure/machinery/colony_floodlight,
/turf/open/floor/plating,
/area/lv624/ground/barrens/central_barrens)
"agG" = (
@@ -1588,9 +1582,7 @@
},
/area/lv624/ground/barrens/west_barrens/ceiling)
"ahM" = (
-/obj/structure/machinery/floodlight/landing{
- name = "bolted floodlight"
- },
+/obj/structure/machinery/colony_floodlight,
/turf/open/floor/plating{
dir = 10;
icon_state = "warnplate"
@@ -1630,9 +1622,7 @@
},
/area/lv624/ground/barrens/central_barrens)
"ahT" = (
-/obj/structure/machinery/floodlight/landing{
- name = "bolted floodlight"
- },
+/obj/structure/machinery/colony_floodlight,
/turf/open/floor/plating{
dir = 6;
icon_state = "warnplate"
diff --git a/tgui/packages/tgfont/config.cjs b/tgui/packages/tgfont/config.cjs
index 4f6b58f1061e..73d96ac6ce90 100644
--- a/tgui/packages/tgfont/config.cjs
+++ b/tgui/packages/tgfont/config.cjs
@@ -7,8 +7,12 @@
module.exports = {
name: 'tgfont',
inputDir: './icons',
+ normalize: true,
outputDir: './dist',
fontTypes: ['woff2', 'eot'],
assetTypes: ['css'],
prefix: 'tg',
+ formatOptions: {
+ preserveAspectRatio: true,
+ },
};
diff --git a/tgui/packages/tgfont/icons/image-minus.svg b/tgui/packages/tgfont/icons/image-minus.svg
new file mode 100644
index 000000000000..8c3231917ff9
--- /dev/null
+++ b/tgui/packages/tgfont/icons/image-minus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tgui/packages/tgfont/icons/image-plus.svg b/tgui/packages/tgfont/icons/image-plus.svg
new file mode 100644
index 000000000000..1658509429e3
--- /dev/null
+++ b/tgui/packages/tgfont/icons/image-plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tgui/packages/tgfont/icons/sound-minus.svg b/tgui/packages/tgfont/icons/sound-minus.svg
new file mode 100644
index 000000000000..df51179d4b53
--- /dev/null
+++ b/tgui/packages/tgfont/icons/sound-minus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tgui/packages/tgfont/icons/sound-plus.svg b/tgui/packages/tgfont/icons/sound-plus.svg
new file mode 100644
index 000000000000..c5f40d53b560
--- /dev/null
+++ b/tgui/packages/tgfont/icons/sound-plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tgui/packages/tgfont/static/tgfont.css b/tgui/packages/tgfont/static/tgfont.css
index 992f088913c4..49e59cdd6959 100644
--- a/tgui/packages/tgfont/static/tgfont.css
+++ b/tgui/packages/tgfont/static/tgfont.css
@@ -1,7 +1,7 @@
@font-face {
font-family: "tgfont";
- src: url("./tgfont.woff2?b809fa4bdd4aff7084540f130e4e524b") format("woff2"),
-url("./tgfont.eot?b809fa4bdd4aff7084540f130e4e524b#iefix") format("embedded-opentype");
+ src: url("./tgfont.woff2?0843e2c3dacddd319720859dcaeda508") format("woff2"),
+url("./tgfont.eot?0843e2c3dacddd319720859dcaeda508#iefix") format("embedded-opentype");
}
i[class^="tg-"]:before, i[class*=" tg-"]:before {
@@ -15,39 +15,15 @@ i[class^="tg-"]:before, i[class*=" tg-"]:before {
-moz-osx-font-smoothing: grayscale;
}
-.tg-air-tank-slash:before {
- content: "\f101";
-}
-.tg-air-tank:before {
- content: "\f102";
-}
-.tg-bad-touch:before {
- content: "\f103";
-}
.tg-image-minus:before {
- content: "\f104";
+ content: "\f101";
}
.tg-image-plus:before {
- content: "\f105";
-}
-.tg-nanotrasen-logo:before {
- content: "\f106";
-}
-.tg-non-binary:before {
- content: "\f107";
-}
-.tg-prosthetic-full:before {
- content: "\f108";
-}
-.tg-prosthetic-leg:before {
- content: "\f109";
+ content: "\f102";
}
.tg-sound-minus:before {
- content: "\f10a";
+ content: "\f103";
}
.tg-sound-plus:before {
- content: "\f10b";
-}
-.tg-syndicate-logo:before {
- content: "\f10c";
+ content: "\f104";
}
diff --git a/tgui/packages/tgfont/static/tgfont.eot b/tgui/packages/tgfont/static/tgfont.eot
index e879c327d3f1..17e3790d026c 100644
Binary files a/tgui/packages/tgfont/static/tgfont.eot and b/tgui/packages/tgfont/static/tgfont.eot differ
diff --git a/tgui/packages/tgfont/static/tgfont.woff2 b/tgui/packages/tgfont/static/tgfont.woff2
index 6a3be3d2cb40..a4cc01a984a1 100644
Binary files a/tgui/packages/tgfont/static/tgfont.woff2 and b/tgui/packages/tgfont/static/tgfont.woff2 differ
diff --git a/tgui/packages/tgui/components/RestrictedInput.jsx b/tgui/packages/tgui/components/RestrictedInput.jsx
index 082fc606d998..f61025039f34 100644
--- a/tgui/packages/tgui/components/RestrictedInput.jsx
+++ b/tgui/packages/tgui/components/RestrictedInput.jsx
@@ -1,48 +1,56 @@
-import { classes } from 'common/react';
+import { KEY_ENTER, KEY_ESCAPE } from 'common/keycodes';
import { clamp } from 'common/math';
+import { classes } from 'common/react';
import { Component, createRef } from 'react';
+
import { Box } from './Box';
-import { KEY_ESCAPE, KEY_ENTER } from 'common/keycodes';
-const DEFAULT_MIN = -16777216;
-const DEFAULT_MAX = 16777216;
+const DEFAULT_MIN = 0;
+const DEFAULT_MAX = 10000;
/**
- * Takes a string input and parses integers from it.
+ * Takes a string input and parses integers or floats from it.
* If none: Minimum is set.
* Else: Clamps it to the given range.
*/
-const getClampedNumber = (value, minValue, maxValue) => {
+const getClampedNumber = (value, minValue, maxValue, allowFloats) => {
const minimum = minValue || DEFAULT_MIN;
const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX;
- const defaultValue = maximum < 0 ? minimum : minimum > 0 ? minimum : 0;
if (!value || !value.length) {
- return String(defaultValue);
+ return String(minimum);
}
- let parsedValue = parseFloat(value.replace(/[^\-.\d]/g, ''), 10);
+ let parsedValue = allowFloats
+ ? parseFloat(value.replace(/[^\-\d.]/g, ''))
+ : parseInt(value.replace(/[^\-\d]/g, ''), 10);
if (isNaN(parsedValue)) {
- return String(defaultValue);
+ return String(minimum);
} else {
return String(clamp(parsedValue, minimum, maximum));
}
};
export class RestrictedInput extends Component {
- constructor() {
- super();
+ constructor(props) {
+ super(props);
this.inputRef = createRef();
this.state = {
editing: false,
};
this.handleBlur = (e) => {
+ const { maxValue, minValue, allowFloats } = this.props;
const { editing } = this.state;
if (editing) {
+ e.target.value = getClampedNumber(
+ e.target.value,
+ minValue,
+ maxValue,
+ allowFloats
+ );
this.setEditing(false);
}
};
this.handleChange = (e) => {
- const { maxValue, minValue, onChange } = this.props;
- e.target.value = getClampedNumber(e.target.value, minValue, maxValue);
+ const { onChange } = this.props;
if (onChange) {
onChange(e, +e.target.value);
}
@@ -64,9 +72,14 @@ export class RestrictedInput extends Component {
}
};
this.handleKeyDown = (e) => {
- const { maxValue, minValue, onChange, onEnter } = this.props;
+ const { maxValue, minValue, onChange, onEnter, allowFloats } = this.props;
if (e.keyCode === KEY_ENTER) {
- const safeNum = getClampedNumber(e.target.value, minValue, maxValue);
+ const safeNum = getClampedNumber(
+ e.target.value,
+ minValue,
+ maxValue,
+ allowFloats
+ );
this.setEditing(false);
if (onChange) {
onChange(e, +safeNum);
@@ -91,11 +104,16 @@ export class RestrictedInput extends Component {
}
componentDidMount() {
- const { maxValue, minValue } = this.props;
+ const { maxValue, minValue, allowFloats } = this.props;
const nextValue = this.props.value?.toString();
const input = this.inputRef.current;
if (input) {
- input.value = getClampedNumber(nextValue, minValue, maxValue);
+ input.value = getClampedNumber(
+ nextValue,
+ minValue,
+ maxValue,
+ allowFloats
+ );
}
if (this.props.autoFocus || this.props.autoSelect) {
setTimeout(() => {
@@ -109,14 +127,19 @@ export class RestrictedInput extends Component {
}
componentDidUpdate(prevProps, _) {
- const { maxValue, minValue } = this.props;
+ const { maxValue, minValue, allowFloats } = this.props;
const { editing } = this.state;
const prevValue = prevProps.value?.toString();
const nextValue = this.props.value?.toString();
const input = this.inputRef.current;
if (input && !editing) {
if (nextValue !== prevValue && nextValue !== input.value) {
- input.value = getClampedNumber(nextValue, minValue, maxValue);
+ input.value = getClampedNumber(
+ nextValue,
+ minValue,
+ maxValue,
+ allowFloats
+ );
}
}
}
diff --git a/tgui/packages/tgui/interfaces/NumberInputModal.tsx b/tgui/packages/tgui/interfaces/NumberInputModal.tsx
index eff7acb62a1b..32d7161d8faf 100644
--- a/tgui/packages/tgui/interfaces/NumberInputModal.tsx
+++ b/tgui/packages/tgui/interfaces/NumberInputModal.tsx
@@ -13,11 +13,19 @@ type NumberInputData = {
min_value: number | null;
timeout: number;
title: string;
+ integer_only: 0 | 1;
};
-export const NumberInputModal = (props) => {
+export const NumberInputModal = () => {
const { act, data } = useBackend();
- const { init_value, large_buttons, message = '', timeout, title } = data;
+ const {
+ init_value,
+ large_buttons,
+ message = '',
+ timeout,
+ title,
+ integer_only,
+ } = data;
const [input, setInput] = useLocalState('input', init_value);
const onChange = (value: number) => {
if (value === input) {
@@ -56,7 +64,12 @@ export const NumberInputModal = (props) => {
{message}
-
+
@@ -72,7 +85,7 @@ export const NumberInputModal = (props) => {
const InputArea = (props) => {
const { act, data } = useBackend();
const { min_value, max_value, init_value } = data;
- const { input, onClick, onChange } = props;
+ const { input, onClick, onChange, integer_only } = props;
return (
@@ -94,6 +107,7 @@ const InputArea = (props) => {
onChange={(_, value) => onChange(value)}
onEnter={(_, value) => act('submit', { entry: value })}
value={input}
+ allowFloats={integer_only === 1 ? false : true}
/>
diff --git a/tgui/packages/tgui/interfaces/PlayerPanel.jsx b/tgui/packages/tgui/interfaces/PlayerPanel.jsx
index 85f73b581c67..88b30aa51802 100644
--- a/tgui/packages/tgui/interfaces/PlayerPanel.jsx
+++ b/tgui/packages/tgui/interfaces/PlayerPanel.jsx
@@ -236,7 +236,6 @@ const GeneralActions = (props) => {
/>
{
/>