diff --git a/code/__DEFINES/battlepass.dm b/code/__DEFINES/battlepass.dm
new file mode 100644
index 000000000000..7a02f087dca3
--- /dev/null
+++ b/code/__DEFINES/battlepass.dm
@@ -0,0 +1,14 @@
+#define CHALLENGE_NONE "none"
+#define CHALLENGE_XENO "Xeno Challenge"
+#define CHALLENGE_HUMAN "Human Challenge"
+
+#define REWARD_CATEGORY_ARMOR "armor"
+#define REWARD_CATEGORY_HELMET_FIRE "helmet_fire"
+#define REWARD_CATEGORY_PARTICLE "particle"
+#define REWARD_CATEGORY_TOY "toy"
+#define REWARD_CATEGORY_OVERLAY "overlay"
+#define REWARD_CATEGORY_SHOTGUN_RESKIN "shotgun_reskin"
+#define REWARD_CATEGORY_M41A_RESKIN "m41a_reskin"
+
+/// How long do you need to play/stay alive for to earn the game-end points
+#define BATTLEPASS_TIME_TO_EARN_REWARD (0.1 MINUTES)
diff --git a/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm b/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm
index 2e247cdccc73..42d793a7da8c 100644
--- a/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm
+++ b/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm
@@ -70,3 +70,5 @@
/// From /obj/item/proc/dig_out_shrapnel() : ()
#define COMSIG_HUMAN_SHRAPNEL_REMOVED "human_shrapnel_removed"
+
+#define COMSIG_HUMAN_USED_DEFIB "human_used_defib"
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 e6e1e64e9c7e..bd973424f8a4 100644
--- a/code/__DEFINES/dcs/signals/atom/mob/living/signals_xeno.dm
+++ b/code/__DEFINES/dcs/signals/atom/mob/living/signals_xeno.dm
@@ -78,3 +78,13 @@
/// 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)
+
+#define COMSIG_XENO_RAGE_MAX "xeno_rage_max"
+
+#define COMSIG_XENO_PLANTED_FRUIT "xeno_planted_fruit"
+
+#define COMSIG_XENO_FACEHUGGED_HUMAN "xeno_facehugged_human"
+
+#define COMSIG_XENO_FTH_MAX_ACID "xeno_fth_max_acid"
+
+#define COMSIG_FIRER_PROJECTILE_DIRECT_HIT "firer_projectile_direct_hit"
diff --git a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
index f4df347c62db..222505284640 100644
--- a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
+++ b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
@@ -185,3 +185,5 @@
#define COMSIG_MOB_END_TUTORIAL "mob_end_tutorial"
#define COMSIG_MOB_NESTED "mob_nested"
+
+#define COMSIG_MOB_KILL_TOTAL_INCREASED "mob_kill_total_increased"
diff --git a/code/__DEFINES/dcs/signals/signals_datum.dm b/code/__DEFINES/dcs/signals/signals_datum.dm
index b798d510763e..2e3c50a93d48 100644
--- a/code/__DEFINES/dcs/signals/signals_datum.dm
+++ b/code/__DEFINES/dcs/signals/signals_datum.dm
@@ -67,3 +67,5 @@
/// Fired on the lazy template datum when the template is finished loading. (list/loaded_atom_movables, list/loaded_turfs, list/loaded_areas)
#define COMSIG_LAZY_TEMPLATE_LOADED "lazy_template_loaded"
+
+#define COMSIG_BATTLEPASS_CHALLENGE_COMPLETED "battlepass_challenge_completed"
diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index ee958d87f580..3ef58afe1f1a 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -165,6 +165,8 @@
#define FULLSCREEN_VULTURE_SCOPE_LAYER 17.21
/// in critical
#define FULLSCREEN_CRIT_LAYER 17.25
+/// tier-up battlepass
+#define FULLSCREEN_BATTLEPASS_TIERUP 18
#define HUD_LAYER 19
#define ABOVE_HUD_LAYER 20
diff --git a/code/__DEFINES/particle.dm b/code/__DEFINES/particle.dm
new file mode 100644
index 000000000000..5657566a63bb
--- /dev/null
+++ b/code/__DEFINES/particle.dm
@@ -0,0 +1,5 @@
+// /obj/effect/abstract/particle_holder/var/particle_flags
+// Flags that effect how a particle holder displays something
+
+/// If we're inside something inside a mob, display off that mob too
+#define PARTICLE_ATTACH_MOB (1<<0)
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 47aa0e732c76..cb3cfba34a59 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -149,6 +149,7 @@
#define SS_INIT_STICKY -30
#define SS_INIT_PREDSHIPS -31
#define SS_INIT_OBJECTIVES -32
+#define SS_INIT_BATTLEPASS -33
#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/__HELPERS/string_lists.dm b/code/__HELPERS/string_lists.dm
index 076bbf642756..3a5fa5256021 100644
--- a/code/__HELPERS/string_lists.dm
+++ b/code/__HELPERS/string_lists.dm
@@ -1,4 +1,5 @@
GLOBAL_LIST_EMPTY(string_lists)
+#define json_load(FILE) (json_decode(file2text(FILE)))
/**
* Caches lists with non-numeric stringify-able values (text or typepath).
diff --git a/code/_onclick/hud/fullscreen.dm b/code/_onclick/hud/fullscreen.dm
index 0bd2206091ba..d63ecf9e2dd1 100644
--- a/code/_onclick/hud/fullscreen.dm
+++ b/code/_onclick/hud/fullscreen.dm
@@ -179,6 +179,11 @@
icon_state = "vulture_scope_overlay_spotter"
should_resize = FALSE
+/atom/movable/screen/fullscreen/battlepass
+ icon_state = "battlepass_tierup"
+ layer = FULLSCREEN_BATTLEPASS_TIERUP
+ show_when_dead = TRUE
+
//Weather overlays//
/atom/movable/screen/fullscreen/weather
diff --git a/code/controllers/subsystem/battlepass.dm b/code/controllers/subsystem/battlepass.dm
new file mode 100644
index 000000000000..9f3a0c133df6
--- /dev/null
+++ b/code/controllers/subsystem/battlepass.dm
@@ -0,0 +1,160 @@
+SUBSYSTEM_DEF(battlepass)
+ name = "Battlepass"
+ flags = SS_NO_FIRE
+ init_order = SS_INIT_BATTLEPASS
+ /// The maximum tier the battlepass goes to
+ var/maximum_tier = 20
+ /// What season we're currently in
+ var/season = 1
+ /// Dict of all marine challenges to their pick weight
+ var/list/marine_challenges = list()
+ /// Dict of all xeno challenges to their pick weight
+ var/list/xeno_challenges = list()
+ /// List of paths of all battlepass rewards for the current season, in order
+ var/list/season_rewards = list()
+ /// List of all paths of all premium battlepass rewards for the current season, in order
+ var/list/premium_season_rewards = list()
+ var/list/marine_battlepass_earners = list()
+ var/list/xeno_battlepass_earners = list()
+
+/datum/controller/subsystem/battlepass/Initialize()
+ if(!fexists("config/battlepass.json"))
+ return
+
+ var/list/battlepass_data = json_load("config/battlepass.json")
+
+ maximum_tier = battlepass_data["maximum_tier"]
+ season = battlepass_data["season"]
+
+ for(var/reward_path in battlepass_data["reward_data"])
+ season_rewards += text2path(reward_path)
+
+ for(var/reward_path in battlepass_data["premium_reward_data"])
+ premium_season_rewards += text2path(reward_path)
+
+ for(var/datum/battlepass_challenge/challenge_path as anything in subtypesof(/datum/battlepass_challenge))
+ switch(initial(challenge_path.challenge_category))
+ if(CHALLENGE_NONE)
+ continue
+
+ if(CHALLENGE_HUMAN)
+ marine_challenges[challenge_path] = initial(challenge_path.pick_weight)
+
+ if(CHALLENGE_XENO)
+ xeno_challenges[challenge_path] = initial(challenge_path.pick_weight)
+
+ RegisterSignal(src, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(do_postinit))
+
+ for(var/client/client as anything in GLOB.clients)
+ if(!client.owned_battlepass)
+ continue
+
+ client.owned_battlepass.verify_rewards()
+
+ return SS_INIT_SUCCESS
+
+/datum/controller/subsystem/battlepass/proc/do_postinit(datum/source)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(src, COMSIG_SUBSYSTEM_POST_INITIALIZE)
+
+ for(var/client/client as anything in GLOB.clients)
+ client.owned_battlepass?.check_daily_challenge_reset()
+
+/// Returns a typepath of a challenge of the given category
+/datum/controller/subsystem/battlepass/proc/get_challenge(challenge_type = CHALLENGE_NONE)
+ switch(challenge_type)
+ if(CHALLENGE_NONE)
+ return
+
+ if(CHALLENGE_HUMAN)
+ return pick_weight(marine_challenges)
+
+ if(CHALLENGE_XENO)
+ return pick_weight(xeno_challenges)
+
+
+/datum/controller/subsystem/battlepass/proc/give_sides_points(marine_points = 0, xeno_points = 0)
+ if(marine_points)
+ give_side_points(marine_points, marine_battlepass_earners)
+
+ if(xeno_points)
+ give_side_points(xeno_points, xeno_battlepass_earners)
+
+/datum/controller/subsystem/battlepass/proc/save_battlepasses()
+ for(var/client/player as anything in GLOB.clients)
+ player.save_battlepass()
+
+/datum/controller/subsystem/battlepass/proc/give_side_points(point_amount = 0, ckey_list)
+ if(!islist(ckey_list))
+ CRASH("give_side_points in SSbattlepass called without giving a list of ckeys")
+
+ for(var/ckey in ckey_list)
+ if(ckey in GLOB.directory)
+ var/client/ckey_client = GLOB.directory[ckey]
+ if(ckey_client.owned_battlepass)
+ ckey_client.owned_battlepass.add_xp(point_amount)
+ else
+ if(!fexists("data/player_saves/[copytext(ckey,1,2)]/[ckey]/battlepass.sav"))
+ continue
+
+ var/savefile/ckey_save = new("data/player_saves/[copytext(ckey,1,2)]/[ckey]/battlepass.sav")
+
+ ckey_save["xp"] += point_amount // if they're >=10 XP, it'll get sorted next time they log on
+
+/// Proc meant for admin calling to see BP levels of all online players
+/datum/controller/subsystem/battlepass/proc/output_bp_levels(mob/caller)
+ var/list/levels = list(
+ "1" = 0,
+ "2" = 0,
+ "3" = 0,
+ "4" = 0,
+ "5" = 0,
+ "6" = 0,
+ "7" = 0,
+ "8" = 0,
+ "9" = 0,
+ "10" = 0,
+ "11" = 0,
+ "12" = 0,
+ "13" = 0,
+ "14" = 0,
+ "15" = 0,
+ "16" = 0,
+ "17" = 0,
+ "18" = 0,
+ "19" = 0,
+ "20" = 0,
+ )
+ for(var/client/player_client as anything in GLOB.clients)
+ if(!player_client.owned_battlepass)
+ continue
+
+ levels["[player_client.owned_battlepass.tier]"] += 1
+
+ to_chat(caller, SPAN_NOTICE(json_encode(levels)))
+
+
+/datum/controller/subsystem/battlepass/proc/get_bp_ge_to_tier(mob/caller, tiernum = 1)
+ var/i = 0
+ for(var/a in flist("data/player_saves/"))
+ for(var/ckey_str in flist("data/player_saves/[a]/"))
+ if(!fexists("data/player_saves/[a]/[ckey_str]/battlepass.sav"))
+ continue
+
+ var/savefile/save_obj = new("data/player_saves/[a]/[ckey_str]/battlepass.sav")
+ if(save_obj["tier"] >= tiernum)
+ i++
+ to_chat(caller, SPAN_NOTICE("[i]"))
+
+
+/datum/controller/subsystem/battlepass/proc/get_bp_xp_total(mob/caller)
+ var/xp = 0
+ for(var/a in flist("data/player_saves/"))
+ for(var/ckey_str in flist("data/player_saves/[a]/"))
+ if(!fexists("data/player_saves/[a]/[ckey_str]/battlepass.sav"))
+ continue
+
+ var/savefile/save_obj = new("data/player_saves/[a]/[ckey_str]/battlepass.sav")
+ xp += (((save_obj["tier"] - 1) * 10) + save_obj["xp"])
+ to_chat(caller, SPAN_NOTICE("[xp]"))
diff --git a/code/datums/statistics/entities/death_stats.dm b/code/datums/statistics/entities/death_stats.dm
index 76e3605c157f..575f84d46d20 100644
--- a/code/datums/statistics/entities/death_stats.dm
+++ b/code/datums/statistics/entities/death_stats.dm
@@ -111,6 +111,7 @@
if(cause_mob)
cause_mob.life_kills_total += life_value
+ SEND_SIGNAL(cause_mob, COMSIG_MOB_KILL_TOTAL_INCREASED, src, cause_data)
if(getBruteLoss())
new_death.total_brute = round(getBruteLoss())
diff --git a/code/game/gamemodes/cm_initialize.dm b/code/game/gamemodes/cm_initialize.dm
index 971eb8f07178..1f5ab604fdaf 100644
--- a/code/game/gamemodes/cm_initialize.dm
+++ b/code/game/gamemodes/cm_initialize.dm
@@ -510,6 +510,7 @@ Additional game mode variables.
to_chat(xeno_candidate, SPAN_WARNING("You are banished from this hive, You may not rejoin unless the Queen re-admits you or dies."))
return FALSE
if(transfer_xeno(xeno_candidate, new_xeno))
+ SSbattlepass.xeno_battlepass_earners |= new_xeno?.client?.ckey
return TRUE
to_chat(xeno_candidate, "JAS01: Something went wrong, tell a coder.")
diff --git a/code/game/gamemodes/colonialmarines/colonialmarines.dm b/code/game/gamemodes/colonialmarines/colonialmarines.dm
index a66403fc00f5..8503d2e9f535 100644
--- a/code/game/gamemodes/colonialmarines/colonialmarines.dm
+++ b/code/game/gamemodes/colonialmarines/colonialmarines.dm
@@ -359,12 +359,14 @@
if(GLOB.round_statistics && GLOB.round_statistics.current_map)
GLOB.round_statistics.current_map.total_xeno_victories++
GLOB.round_statistics.current_map.total_xeno_majors++
+ SSbattlepass.give_sides_points(3, 5)
if(MODE_INFESTATION_M_MAJOR)
musical_track = pick('sound/theme/winning_triumph1.ogg','sound/theme/winning_triumph2.ogg')
end_icon = "marine_major"
if(GLOB.round_statistics && GLOB.round_statistics.current_map)
GLOB.round_statistics.current_map.total_marine_victories++
GLOB.round_statistics.current_map.total_marine_majors++
+ SSbattlepass.give_sides_points(5, 3)
if(MODE_INFESTATION_X_MINOR)
var/list/living_player_list = count_humans_and_xenos(get_affected_zlevels())
if(living_player_list[1] && !living_player_list[2]) // If Xeno Minor but Xenos are dead and Humans are alive, see which faction is the last standing
@@ -389,16 +391,19 @@
end_icon = "xeno_minor"
if(GLOB.round_statistics && GLOB.round_statistics.current_map)
GLOB.round_statistics.current_map.total_xeno_victories++
+ SSbattlepass.give_sides_points(3, 4)
if(MODE_INFESTATION_M_MINOR)
musical_track = pick('sound/theme/neutral_hopeful1.ogg','sound/theme/neutral_hopeful2.ogg')
end_icon = "marine_minor"
if(GLOB.round_statistics && GLOB.round_statistics.current_map)
GLOB.round_statistics.current_map.total_marine_victories++
+ SSbattlepass.give_sides_points(4, 3)
if(MODE_INFESTATION_DRAW_DEATH)
end_icon = "draw"
musical_track = 'sound/theme/neutral_hopeful2.ogg'
if(GLOB.round_statistics && GLOB.round_statistics.current_map)
GLOB.round_statistics.current_map.total_draws++
+ SSbattlepass.give_sides_points(3, 3)
var/sound/S = sound(musical_track, channel = SOUND_CHANNEL_LOBBY)
S.status = SOUND_STREAM
sound_to(world, S)
@@ -418,6 +423,7 @@
declare_completion_announce_predators()
declare_completion_announce_medal_awards()
declare_fun_facts()
+ SSbattlepass.save_battlepasses()
add_current_round_status_to_end_results("Round End")
diff --git a/code/game/jobs/job/antag/xeno/xenomorph.dm b/code/game/jobs/job/antag/xeno/xenomorph.dm
index 78b6ab7e3ab2..7cea0073b0a8 100644
--- a/code/game/jobs/job/antag/xeno/xenomorph.dm
+++ b/code/game/jobs/job/antag/xeno/xenomorph.dm
@@ -11,6 +11,7 @@
// a proper limit.
spawn_positions = -1
total_positions = -1
+ xeno_sided = TRUE
/datum/job/antag/xenos/proc/calculate_extra_spawn_positions(count)
return max((round(count * XENO_TO_TOTAL_SPAWN_RATIO)), 0)
diff --git a/code/game/jobs/job/civilians/civilian.dm b/code/game/jobs/job/civilians/civilian.dm
index a9631f2ed9c1..e2169dd42d13 100644
--- a/code/game/jobs/job/civilians/civilian.dm
+++ b/code/game/jobs/job/civilians/civilian.dm
@@ -1,5 +1,6 @@
/datum/job/civilian
gear_preset = /datum/equipment_preset/colonist
+ marine_sided = TRUE
/datum/timelock/medic
name = "Medical Roles"
diff --git a/code/game/jobs/job/command/command.dm b/code/game/jobs/job/command/command.dm
index d430352d6e83..60d895d46ac5 100644
--- a/code/game/jobs/job/command/command.dm
+++ b/code/game/jobs/job/command/command.dm
@@ -3,6 +3,7 @@
supervisors = "the acting commanding officer"
total_positions = 1
spawn_positions = 1
+ marine_sided = TRUE
/datum/timelock/command
name = "Command Roles"
@@ -23,7 +24,7 @@
/datum/timelock/human/can_play(client/C)
return C.get_total_human_playtime() >= time_required
-
+
/datum/timelock/human/get_role_requirement(client/C)
return time_required - C.get_total_human_playtime()
@@ -33,4 +34,4 @@
/datum/timelock/dropship/New(name, time_required, list/roles)
. = ..()
src.roles = JOB_DROPSHIP_ROLES_LIST
-
+
diff --git a/code/game/jobs/job/job.dm b/code/game/jobs/job/job.dm
index 1a04c3cafeb5..7af3f98a4916 100644
--- a/code/game/jobs/job/job.dm
+++ b/code/game/jobs/job/job.dm
@@ -39,6 +39,8 @@
var/job_options
/// If TRUE, this job will spawn w/ a cryo emergency kit during evac/red alert
var/gets_emergency_kit = TRUE
+ var/marine_sided = FALSE
+ var/xeno_sided = FALSE
/datum/job/New()
. = ..()
@@ -239,8 +241,22 @@
setup_human(new_character, NP)
+ addtimer(CALLBACK(src, PROC_REF(add_to_battlepass_earners), new_character), BATTLEPASS_TIME_TO_EARN_REWARD)
+
return new_character
+/datum/job/proc/add_to_battlepass_earners(mob/living/carbon/human/character)
+ if(!character?.client?.ckey)
+ return
+
+ var/ckey = character.client.ckey
+
+ // You cannot double dip; marine or xeno only
+ if(marine_sided && !(ckey in SSbattlepass.xeno_battlepass_earners))
+ SSbattlepass.marine_battlepass_earners |= ckey
+ else if(xeno_sided && !(ckey in SSbattlepass.marine_battlepass_earners))
+ SSbattlepass.xeno_battlepass_earners |= ckey
+
/datum/job/proc/equip_job(mob/living/M)
if(!istype(M))
return
diff --git a/code/game/jobs/job/marine/marine.dm b/code/game/jobs/job/marine/marine.dm
index e07c1edd3138..ff1c5178b74c 100644
--- a/code/game/jobs/job/marine/marine.dm
+++ b/code/game/jobs/job/marine/marine.dm
@@ -4,6 +4,7 @@
total_positions = 8
spawn_positions = 8
allow_additional = 1
+ marine_sided = TRUE
/datum/job/marine/generate_entry_message(mob/living/carbon/human/current_human)
if(current_human.assigned_squad)
diff --git a/code/game/jobs/job/special/uscm.dm b/code/game/jobs/job/special/uscm.dm
index 2308c5af2961..74c369c63493 100644
--- a/code/game/jobs/job/special/uscm.dm
+++ b/code/game/jobs/job/special/uscm.dm
@@ -1,3 +1,6 @@
+/datum/job/special/uscm
+ marine_sided = TRUE
+
/datum/job/special/uscm/colonel
title = JOB_COLONEL
/datum/job/special/uscm/general
diff --git a/code/game/objects/effects/decals/crayon.dm b/code/game/objects/effects/decals/crayon.dm
index 35e354c121bb..85687525d54e 100644
--- a/code/game/objects/effects/decals/crayon.dm
+++ b/code/game/objects/effects/decals/crayon.dm
@@ -28,3 +28,11 @@
overlays += shadeOverlay
add_hiddenprint(usr)
+
+/obj/effect/decal/cleanable/sus_crayon
+ name = "suspicious rune"
+ desc = "A rune drawn in crayon."
+ icon = 'code/modules/battlepass/rewards/sprites/toy.dmi'
+ icon_state = "sus_crayon_rune"
+ layer = ABOVE_TURF_LAYER
+ anchored = TRUE
diff --git a/code/game/objects/items/devices/defibrillator.dm b/code/game/objects/items/devices/defibrillator.dm
index bbeb2046aff0..be39cc4e21b3 100644
--- a/code/game/objects/items/devices/defibrillator.dm
+++ b/code/game/objects/items/devices/defibrillator.dm
@@ -230,6 +230,7 @@
playsound(get_turf(src), 'sound/items/defib_success.ogg', 25, 0)
user.track_life_saved(user.job)
user.life_revives_total++
+ SEND_SIGNAL(user, COMSIG_HUMAN_USED_DEFIB, H)
H.handle_revive()
if(heart)
heart.take_damage(rand(min_heart_damage_dealt, max_heart_damage_dealt), TRUE) // Make death and revival leave lasting consequences
diff --git a/code/game/objects/items/toys/crayons.dm b/code/game/objects/items/toys/crayons.dm
index 1d9e2e1a4d54..607eb1e6ddf6 100644
--- a/code/game/objects/items/toys/crayons.dm
+++ b/code/game/objects/items/toys/crayons.dm
@@ -101,3 +101,38 @@
qdel(src)
else
..()
+
+
+/obj/item/toy/suspicious
+ name = "suspicious crayon"
+ desc = "There's something off about this crayon..."
+ icon = 'code/modules/battlepass/rewards/sprites/toy.dmi'
+ icon_state = "sus_crayon"
+ var/uses = 10
+
+/obj/item/toy/suspicious/afterattack(atom/target, mob/user, proximity)
+ if(!proximity)
+ return
+
+ if(istype(target, /turf/open/floor))
+ if(do_after(user, 5 SECONDS, INTERRUPT_ALL, BUSY_ICON_GENERIC))
+ new /obj/effect/decal/cleanable/sus_crayon(target)
+ to_chat(user, SPAN_NOTICE("You finish drawing."))
+ target.add_fingerprint(user) // Adds their fingerprints to the floor the crayon is drawn on.
+ if(uses)
+ uses--
+ if(!uses)
+ to_chat(user, SPAN_DANGER("You used up your crayon!"))
+ qdel(src)
+
+/obj/item/toy/suspicious/attack(mob/M as mob, mob/user as mob)
+ if(M == user)
+ to_chat(user, SPAN_NOTICE("You take a bite of the crayon and swallow it."))
+ user.nutrition += 5
+ if(uses)
+ uses -= 5
+ if(uses <= 0)
+ to_chat(user, SPAN_DANGER("You ate your crayon!"))
+ qdel(src)
+ else
+ ..()
diff --git a/code/game/objects/items/toys/toys.dm b/code/game/objects/items/toys/toys.dm
index 91d8164dcf38..09d81be3d637 100644
--- a/code/game/objects/items/toys/toys.dm
+++ b/code/game/objects/items/toys/toys.dm
@@ -660,3 +660,21 @@
/obj/item/toy/plush/shark/alt
icon_state = "shark_alt"
+
+/obj/item/toy/plush/runner_toy
+ name = "runner toy"
+ desc = "A squishy, shrunken rendition of an XX-121 runner caste. It feels comforting, though you're not sure why anyone would make these."
+ icon = 'code/modules/battlepass/rewards/sprites/toy.dmi'
+ icon_state = "runner_toy"
+
+/obj/item/toy/plush/warrior_toy
+ name = "warrior toy"
+ desc = "A squishy, shrunken rendition of an XX-121 warrior caste. It feels comforting, though you're not sure why anyone would make these."
+ icon = 'code/modules/battlepass/rewards/sprites/toy.dmi'
+ icon_state = "warrior_toy"
+
+/obj/item/toy/plush/queen_toy
+ name = "queen toy"
+ desc = "A squishy, shrunken rendition of an XX-121 queen. It feels comforting, though you're not sure why anyone would make these."
+ icon = 'code/modules/battlepass/rewards/sprites/toy.dmi'
+ icon_state = "queen_toy"
diff --git a/code/game/world.dm b/code/game/world.dm
index e55741ca71e5..12b41d01d6a1 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -219,6 +219,7 @@ GLOBAL_LIST_INIT(reboot_sfx, file2list("config/reboot_sfx.txt"))
return json_encode(response)
/world/Reboot(reason)
+ SSbattlepass.save_battlepasses()
Master.Shutdown()
send_reboot_sound()
var/server = CONFIG_GET(string/server)
diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm
index 0b27cf268a12..2587f0fb27f7 100644
--- a/code/modules/asset_cache/asset_list_items.dm
+++ b/code/modules/asset_cache/asset_list_items.dm
@@ -391,6 +391,24 @@
..()
+/datum/asset/spritesheet/battlepass
+ name = "battlepass"
+
+/datum/asset/spritesheet/battlepass/register()
+ var/list/iconstates_added = list()
+ for(var/datum/battlepass_reward/reward as anything in subtypesof(/datum/battlepass_reward))
+ reward = new reward
+ if(reward.icon_state in iconstates_added)
+ qdel(reward)
+ continue
+
+ var/icon/sprite = icon(reward.icon, reward.icon_state)
+ sprite.Scale(96, 96)
+ Insert(reward.icon_state, sprite)
+ iconstates_added += reward.icon_state
+ qdel(reward)
+ return ..()
+
/datum/asset/spritesheet/gun_lineart_modes
name = "gunlineartmodes"
diff --git a/code/modules/battlepass/battlepass.dm b/code/modules/battlepass/battlepass.dm
new file mode 100644
index 000000000000..ada4481c25aa
--- /dev/null
+++ b/code/modules/battlepass/battlepass.dm
@@ -0,0 +1,297 @@
+/mob/verb/battlepass()
+ set category = "OOC"
+ set name = "Battlepass"
+
+ if(!client)
+ return
+
+ if(!SSbattlepass.initialized)
+ return
+
+ client.owned_battlepass?.ui_interact(src)
+
+/mob/living/carbon/verb/claim_battlepass_reward()
+ set category = "OOC"
+ set name = "Claim Battlepass Reward"
+
+ if(!client)
+ return
+
+ var/list/acceptable_rewards = list()
+ for(var/datum/battlepass_reward/reward as anything in client.owned_battlepass.rewards)
+ if(reward.can_claim(src))
+ acceptable_rewards += reward
+
+ if(!length(acceptable_rewards))
+ to_chat(src, SPAN_WARNING("You have no rewards to claim."))
+ return
+
+ var/datum/battlepass_reward/chosen_reward = tgui_input_list(src, "Claim a battlepass reward.", "Claim Reward", acceptable_rewards)
+ if(!chosen_reward || !chosen_reward.can_claim(src))
+ return
+
+ if(chosen_reward.on_claim(src))
+ claimed_reward_categories |= chosen_reward.category
+
+/mob/var/obj/effect/abstract/particle_holder/particle_holder
+
+/// Each client possesses an instanced /datum/battlepass
+/datum/battlepass
+ /// The current battlepass tier the user is at
+ /// Max tier is stored on the master battlepass the server owns
+ var/tier = 1 as num
+
+ /// How much XP the user has in the current tier
+ var/xp = 0 as num
+
+ /// How much XP you need to go up a tier
+ var/xp_tierup = 10 as num
+
+ // If the user has paid for a premium battlepass
+ //var/premium = FALSE // (:
+
+ /// List of personal daily challenges
+ var/list/datum/battlepass_challenge/daily_challenges = list()
+
+ /// When challenges were last updated, formatted as a UNIX timestamp
+ var/daily_challenges_last_updated = 0 as num
+
+ /// Weakref to the owning client
+ var/datum/weakref/owning_client
+
+ /// All earned battlepass reward instances
+ var/list/datum/battlepass_reward/rewards = list()
+
+ /// Typepaths of all earned battlepass rewards. This isn't saved because it's populated by loading the rewards list
+ var/list/reward_paths = list()
+
+ /// The tier of the battlepass the last time on_tier_up() was called
+ var/previous_on_tier_up_tier = 0
+
+/datum/battlepass/proc/add_xp(xp_amount)
+ if(tier >= SSbattlepass.maximum_tier)
+ return
+
+ xp += xp_amount
+ check_tier_up(TRUE)
+
+/datum/battlepass/proc/check_tier_up(display_popup = TRUE)
+ if(xp >= xp_tierup)
+ var/tier_increase = round(xp / xp_tierup)
+ xp -= (tier_increase * xp_tierup)
+ tier += tier_increase
+ on_tier_up(display_popup)
+ update_static_data_for_all_viewers()
+
+/datum/battlepass/proc/on_tier_up(display_popup = TRUE)
+ if(previous_on_tier_up_tier == tier)
+ return
+
+ for(var/i in previous_on_tier_up_tier + 1 to tier)
+ var/reward_path = SSbattlepass.season_rewards[i]
+ var/datum/battlepass_reward/reward = new reward_path
+ rewards += reward
+ reward_paths += reward_path
+
+ if(display_popup)
+ display_tier_up_popup()
+
+ var/list/types_in_rewards = list()
+ for(var/datum/battlepass_reward/reward as anything in rewards)
+ if(reward.type in types_in_rewards)
+ rewards -= reward
+ reward_paths -= reward.type
+ qdel(reward)
+ continue
+
+ types_in_rewards += reward.type
+
+ previous_on_tier_up_tier = tier
+ var/client/oc = owning_client.resolve()
+ log_game("[oc.mob] ([oc.key]) has increased to battlepass tier [tier]")
+
+/datum/battlepass/proc/display_tier_up_popup()
+ if(!owning_client)
+ return
+
+ var/client/user_client = owning_client.resolve()
+ if(!user_client.mob)
+ return
+
+ playsound_client(user_client, 'sound/effects/bp_levelup.mp3', get_turf(user_client.mob), 70, FALSE) // .mp3, sue me
+ user_client.mob.overlay_fullscreen("battlepass_tierup", /atom/movable/screen/fullscreen/battlepass)
+ addtimer(CALLBACK(user_client.mob, TYPE_PROC_REF(/mob, clear_fullscreen), "battlepass_tierup", 0), 1.2 SECONDS)
+
+/// Check that the user has all the rewards they should (in case rewards shifted in config or etc).
+/// Doesn't remove ones that aren't in their tiers (in case they have some from a previous season, for example)
+/datum/battlepass/proc/verify_rewards()
+ for(var/i in 1 to tier)
+ var/reward_path = SSbattlepass.season_rewards[i]
+ if(reward_path in reward_paths)
+ continue
+
+ rewards += new reward_path
+ reward_paths += reward_path
+
+/// Check if it's been 24h since daily challenges were last assigned
+/datum/battlepass/proc/check_daily_challenge_reset()
+ // Clients can connect before the SS is initialized
+ if(!SSbattlepass?.initialized)
+ return
+
+ // 86400 seconds (24*60^2) is one day
+ if((daily_challenges_last_updated + (24 * 60 * 60)) <= rustg_unix_timestamp())
+ reset_daily_challenges()
+ return TRUE
+ return FALSE
+
+/// Give the battlepass a new set of daily challenges
+/datum/battlepass/proc/reset_daily_challenges()
+ if(!owning_client)
+ return
+
+ // We give the player 2 marine challenges and 2 xeno challenges
+ QDEL_LIST(daily_challenges)
+
+ for(var/i in 1 to 2)
+ var/gotten_path = SSbattlepass.get_challenge(CHALLENGE_HUMAN)
+ var/datum/battlepass_challenge/human_challenge = new gotten_path(owning_client.resolve())
+ RegisterSignal(human_challenge, COMSIG_BATTLEPASS_CHALLENGE_COMPLETED, PROC_REF(on_challenge_complete))
+ daily_challenges += human_challenge
+
+ for(var/i in 1 to 2)
+ var/gotten_path = SSbattlepass.get_challenge(CHALLENGE_XENO)
+ var/datum/battlepass_challenge/xeno_challenge = new gotten_path(owning_client.resolve())
+ RegisterSignal(xeno_challenge, COMSIG_BATTLEPASS_CHALLENGE_COMPLETED, PROC_REF(on_challenge_complete))
+ daily_challenges += xeno_challenge
+
+ daily_challenges_last_updated = rustg_unix_timestamp()
+
+/// Returns a list of all daily challenges formatted for a savefile
+/datum/battlepass/proc/serialize_daily_challenges()
+ . = list()
+ for(var/datum/battlepass_challenge/challenge as anything in daily_challenges)
+ . += list(challenge.serialize())
+
+/datum/battlepass/proc/serialize_rewards()
+ . = list()
+ var/list/saved_reward_paths = list()
+ for(var/datum/battlepass_reward/reward as anything in rewards)
+ if(reward.type in saved_reward_paths)
+ continue
+
+ . += reward.type
+ saved_reward_paths += reward.type
+
+/// Provided a list of lists for daily challenges, load daily challenges from the lists
+/datum/battlepass/proc/load_daily_challenges(list/challenge_data)
+ if(!owning_client)
+ return
+
+ for(var/list/entry as anything in challenge_data)
+ if(!("type" in entry))
+ continue
+
+ var/path = entry["type"]
+ var/datum/battlepass_challenge/challenge = new path(owning_client.resolve())
+ daily_challenges += challenge
+ RegisterSignal(challenge, COMSIG_BATTLEPASS_CHALLENGE_COMPLETED, PROC_REF(on_challenge_complete))
+ challenge.deserialize(entry)
+
+/datum/battlepass/proc/load_rewards(list/reward_data)
+ var/list/loaded_paths = list()
+ for(var/path in reward_data)
+ if(path in loaded_paths)
+ continue
+
+ var/datum/battlepass_reward/reward = new path
+ rewards += reward
+ reward_paths += path
+ loaded_paths += path
+
+/// Called whenever a challenge is completed
+/datum/battlepass/proc/on_challenge_complete(datum/battlepass_challenge/challenge)
+ SIGNAL_HANDLER
+
+ if(!owning_client)
+ return
+
+ var/client/resolved_client = owning_client.resolve()
+ challenge.completed = TRUE
+ add_xp(challenge.completion_xp)
+ challenge.unhook_signals(resolved_client.mob)
+
+/datum/battlepass/proc/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "Battlepass")
+ ui.open()
+
+/datum/battlepass/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/battlepass/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/spritesheet/battlepass),
+ )
+
+/datum/battlepass/ui_data(mob/user)
+ var/list/data = list()
+
+ data["tier"] = tier
+ data["xp"] = tier >= SSbattlepass.maximum_tier ? xp_tierup : xp
+ data["xp_tierup"] = xp_tierup
+
+ return data
+
+/datum/battlepass/ui_static_data(mob/user)
+ var/list/data = list()
+
+ data["season"] = SSbattlepass.season
+ data["max_tier"] = SSbattlepass.maximum_tier
+
+ data["rewards"] = list()
+
+ var/i = 1
+ for(var/datum/battlepass_reward/reward_path as anything in SSbattlepass.season_rewards)
+ data["rewards"] += list(list(
+ "name" = initial(reward_path.name),
+ "icon_state" = initial(reward_path.icon_state),
+ "tier" = i,
+ "lifeform_type" = initial(reward_path.lifeform_type),
+ ))
+ i++
+
+ data["premium_rewards"] = list()
+
+ i = 1
+ for(var/datum/battlepass_reward/reward_path as anything in SSbattlepass.premium_season_rewards)
+ data["premium_rewards"] += list(list(
+ "name" = initial(reward_path.name),
+ "icon_state" = initial(reward_path.icon_state),
+ "tier" = i,
+ "lifeform_type" = initial(reward_path.lifeform_type),
+ ))
+ i++
+
+ data["daily_challenges"] = list()
+
+ for(var/datum/battlepass_challenge/daily_challenge as anything in daily_challenges)
+ data["daily_challenges"] += list(list(
+ "name" = daily_challenge.name,
+ "desc" = daily_challenge.desc,
+ "completed" = daily_challenge.completed,
+ "category" = daily_challenge.challenge_category,
+ "completion_xp" = daily_challenge.completion_xp,
+ "completion_percent" = daily_challenge.get_completion_percent(),
+ "completion_numerator" = daily_challenge.get_completion_numerator(),
+ "completion_denominator" = daily_challenge.get_completion_denominator(),
+ ))
+
+ return data
+
+/datum/battlepass/vv_edit_var(var_name, var_value)
+ if(usr.ckey != "zonespace")
+ to_chat(usr, SPAN_BOLDWARNING("FUCK OFF"))
+ return FALSE
+ return ..()
diff --git a/code/modules/battlepass/battlepass_client.dm b/code/modules/battlepass/battlepass_client.dm
new file mode 100644
index 000000000000..13800f978fce
--- /dev/null
+++ b/code/modules/battlepass/battlepass_client.dm
@@ -0,0 +1,50 @@
+/client
+ /// Reference to the client's battlepass
+ var/datum/battlepass/owned_battlepass
+
+/client/New()
+ . = ..()
+ init_battlepass()
+
+/client/Destroy()
+ save_battlepass()
+ return ..()
+
+/client/proc/init_battlepass()
+ if(fexists("data/player_saves/[copytext(ckey,1,2)]/[ckey]/battlepass.sav"))
+ load_battlepass()
+ return
+
+ owned_battlepass = new()
+ owned_battlepass.owning_client = WEAKREF(src)
+ owned_battlepass.tier = 1
+ owned_battlepass.xp = 0
+ owned_battlepass.daily_challenges_last_updated = 0
+ owned_battlepass.check_daily_challenge_reset()
+ owned_battlepass.previous_on_tier_up_tier = owned_battlepass.tier
+
+/client/proc/load_battlepass()
+ var/savefile/battlepass_save = new("data/player_saves/[copytext(ckey,1,2)]/[ckey]/battlepass.sav")
+
+ owned_battlepass = new()
+ owned_battlepass.owning_client = WEAKREF(src)
+ owned_battlepass.tier = battlepass_save["tier"]
+ owned_battlepass.xp = battlepass_save["xp"]
+ owned_battlepass.check_tier_up(FALSE)
+ owned_battlepass.daily_challenges_last_updated = battlepass_save["daily_challenges_last_updated"]
+ owned_battlepass.load_daily_challenges(battlepass_save["daily_challenges"])
+ owned_battlepass.check_daily_challenge_reset()
+ owned_battlepass.load_rewards(battlepass_save["rewards"])
+ owned_battlepass.previous_on_tier_up_tier = battlepass_save["previous_on_tier_up_tier"]
+ if(SSbattlepass.initialized)
+ owned_battlepass.verify_rewards()
+
+/client/proc/save_battlepass()
+ var/savefile/battlepass_save = new("data/player_saves/[copytext(ckey,1,2)]/[ckey]/battlepass.sav")
+
+ battlepass_save["tier"] = owned_battlepass.tier
+ battlepass_save["xp"] = owned_battlepass.xp
+ battlepass_save["daily_challenges_last_updated"] = owned_battlepass.daily_challenges_last_updated
+ battlepass_save["daily_challenges"] = owned_battlepass.serialize_daily_challenges()
+ battlepass_save["rewards"] = owned_battlepass.serialize_rewards()
+ battlepass_save["previous_on_tier_up_tier"] = owned_battlepass.previous_on_tier_up_tier
diff --git a/code/modules/battlepass/challenges/_battlepass_challenge.dm b/code/modules/battlepass/challenges/_battlepass_challenge.dm
new file mode 100644
index 000000000000..a6b520cf865a
--- /dev/null
+++ b/code/modules/battlepass/challenges/_battlepass_challenge.dm
@@ -0,0 +1,87 @@
+/datum/battlepass_challenge
+ /// What this challenge is called
+ var/name = "" as text
+ /// The description that is given to the user on what the challenge is and how to complete it
+ var/desc = "" as text
+ /// If this challenge has been completed
+ var/completed = FALSE as num
+ /// How much XP this challenge gives on completion
+ var/completion_xp = 0 as num
+ /// If this is a xeno or marine-focused challenge
+ var/challenge_category = CHALLENGE_NONE as text
+ /// How much weight this challenge has in its category
+ var/pick_weight = 1 as num
+
+/datum/battlepass_challenge/New(client/owning_client)
+ . = ..()
+ if(!owning_client)
+ return FALSE
+
+ RegisterSignal(owning_client, COMSIG_CLIENT_MOB_LOGGED_IN, PROC_REF(on_client_login_mob))
+ return TRUE
+
+/// Override to change the desc of the challenge post-init
+/datum/battlepass_challenge/proc/regenerate_desc()
+ return
+
+/// Override this to add behavior to any mob connected to the owning client
+/datum/battlepass_challenge/proc/on_client_login_mob(datum/source, mob/logged_in_mob)
+ SIGNAL_HANDLER
+ SHOULD_CALL_PARENT(TRUE)
+
+ UnregisterSignal(logged_in_mob.client, COMSIG_CLIENT_MOB_LOGGED_IN)
+ RegisterSignal(logged_in_mob, COMSIG_MOB_LOGOUT, PROC_REF(on_client_logout_mob))
+
+/// Cleanup code from on_client_login_mob
+/datum/battlepass_challenge/proc/on_client_logout_mob(mob/source)
+ SIGNAL_HANDLER
+ SHOULD_CALL_PARENT(TRUE)
+
+ unhook_signals(source)
+ UnregisterSignal(source, COMSIG_MOB_LOGOUT)
+ if(source.logging_ckey in GLOB.directory)
+ RegisterSignal(GLOB.directory[source.logging_ckey], COMSIG_CLIENT_MOB_LOGGED_IN, PROC_REF(on_client_login_mob))
+
+/datum/battlepass_challenge/proc/unhook_signals(mob/source)
+ return
+
+/// Override to return true/false depending on the challenge's completion
+/datum/battlepass_challenge/proc/check_challenge_completed()
+ return TRUE
+
+/// Do things if the challenge is completed, will do nothing if it is not
+/datum/battlepass_challenge/proc/on_possible_challenge_completed()
+ if(!check_challenge_completed())
+ return FALSE
+ SEND_SIGNAL(src, COMSIG_BATTLEPASS_CHALLENGE_COMPLETED)
+ return TRUE
+
+/// Get how completed the challenge is as a percentage out of 1
+/datum/battlepass_challenge/proc/get_completion_percent()
+ return 0
+
+/datum/battlepass_challenge/proc/get_completion_numerator()
+ return 0
+
+/datum/battlepass_challenge/proc/get_completion_denominator()
+ return 1
+
+/// Convert data about this challenge into a list to be inserted in a savefile
+/datum/battlepass_challenge/proc/serialize()
+ SHOULD_CALL_PARENT(TRUE)
+ return list(
+ "type" = type,
+ "name" = name,
+ "desc" = desc,
+ "completion_xp" = completion_xp,
+ "completed" = completed
+ )
+
+/// Given a list, update the challenge data accordingly
+/datum/battlepass_challenge/proc/deserialize(list/save_list)
+ SHOULD_CALL_PARENT(TRUE)
+ name = save_list["name"]
+ desc = save_list["desc"]
+ completion_xp = save_list["completion_xp"]
+ completed = save_list["completed"]
+
diff --git a/code/modules/battlepass/challenges/human_misc/defib_players.dm b/code/modules/battlepass/challenges/human_misc/defib_players.dm
new file mode 100644
index 000000000000..c9513d9467f3
--- /dev/null
+++ b/code/modules/battlepass/challenges/human_misc/defib_players.dm
@@ -0,0 +1,60 @@
+/datum/battlepass_challenge/defib_players
+ name = "Defibrillate Players"
+ desc = "Successfully defibrillate AMOUNT unique marine players."
+ challenge_category = CHALLENGE_HUMAN
+ completion_xp = 5
+ var/minimum = 4 as num
+ var/maximum = 8 as num
+ var/requirement = 0 as num
+ var/list/mob_name_list = list()
+
+
+/datum/battlepass_challenge/defib_players/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ requirement = rand(minimum, maximum)
+ regenerate_desc()
+
+/datum/battlepass_challenge/defib_players/regenerate_desc()
+ desc = "Successfully defibrillate [requirement] unique marine player\s."
+
+/datum/battlepass_challenge/defib_players/on_client_login_mob(datum/source, mob/logged_in_mob)
+ . = ..()
+ if(!completed)
+ RegisterSignal(logged_in_mob, COMSIG_HUMAN_USED_DEFIB, PROC_REF(on_defib))
+
+/datum/battlepass_challenge/defib_players/unhook_signals(mob/source)
+ UnregisterSignal(source, COMSIG_HUMAN_USED_DEFIB)
+
+/datum/battlepass_challenge/defib_players/check_challenge_completed()
+ return (length(mob_name_list) >= requirement)
+
+/datum/battlepass_challenge/defib_players/get_completion_percent()
+ return (length(mob_name_list) / requirement)
+
+/datum/battlepass_challenge/defib_players/get_completion_numerator()
+ return length(mob_name_list)
+
+/datum/battlepass_challenge/defib_players/get_completion_denominator()
+ return requirement
+
+/datum/battlepass_challenge/defib_players/serialize()
+ . = ..()
+ .["requirement"] = requirement
+ .["filled"] = mob_name_list
+
+/datum/battlepass_challenge/defib_players/deserialize(list/save_list)
+ . = ..()
+ requirement = save_list["requirement"]
+ mob_name_list = save_list["filled"]
+
+/// When the xeno plants a resin node
+/datum/battlepass_challenge/defib_players/proc/on_defib(datum/source, mob/living/carbon/human/defibbed)
+ SIGNAL_HANDLER
+
+ mob_name_list |= defibbed.real_name
+ on_possible_challenge_completed()
+
+
diff --git a/code/modules/battlepass/challenges/kill/_kill_enemies.dm b/code/modules/battlepass/challenges/kill/_kill_enemies.dm
new file mode 100644
index 000000000000..fa7cba32bd07
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/_kill_enemies.dm
@@ -0,0 +1,60 @@
+/datum/battlepass_challenge/kill_enemies
+ /// How many enemies need to be killed to complete the challenge
+ var/enemy_kills_required = 0 as num
+ /// How many enemies have been killed thus far for this challenge
+ var/current_enemy_kills = 0 as num
+ /// A list of valid mob paths to count towards kills
+ var/list/valid_kill_paths = list()
+ /// The minimum amt of kills possibly required to complete this challenge
+ var/kill_requirement_lower = 0 as num
+ /// The maximum amt of kills possibly required to complete this challenge
+ var/kill_requirement_upper = 0 as num
+
+/datum/battlepass_challenge/kill_enemies/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ enemy_kills_required = rand(kill_requirement_lower, kill_requirement_upper)
+ regenerate_desc()
+
+/datum/battlepass_challenge/kill_enemies/on_client_login_mob(datum/source, mob/logged_in_mob)
+ . = ..()
+ if(!completed)
+ RegisterSignal(logged_in_mob, COMSIG_MOB_KILL_TOTAL_INCREASED, PROC_REF(on_kill))
+
+/datum/battlepass_challenge/kill_enemies/unhook_signals(mob/source)
+ UnregisterSignal(source, COMSIG_MOB_KILL_TOTAL_INCREASED)
+
+/datum/battlepass_challenge/kill_enemies/check_challenge_completed()
+ return (enemy_kills_required <= current_enemy_kills)
+
+/datum/battlepass_challenge/kill_enemies/get_completion_percent()
+ return (current_enemy_kills / enemy_kills_required)
+
+/datum/battlepass_challenge/kill_enemies/get_completion_numerator()
+ return current_enemy_kills
+
+/datum/battlepass_challenge/kill_enemies/get_completion_denominator()
+ return enemy_kills_required
+
+/datum/battlepass_challenge/kill_enemies/serialize()
+ . = ..()
+ .["enemy_kills_required"] = enemy_kills_required
+ .["current_enemy_kills"] = current_enemy_kills
+ .["valid_kill_paths"] = valid_kill_paths
+
+/datum/battlepass_challenge/kill_enemies/deserialize(list/save_list)
+ . = ..()
+ enemy_kills_required = save_list["enemy_kills_required"]
+ current_enemy_kills = save_list["current_enemy_kills"]
+ valid_kill_paths = save_list["valid_kill_paths"]
+
+/datum/battlepass_challenge/kill_enemies/proc/on_kill(mob/source, mob/killed_mob, datum/cause_data/cause_data)
+ SIGNAL_HANDLER
+
+ // Facehuggers and lessers have a life_value of 0, so they aren't counted
+ if(is_type_in_list(killed_mob, valid_kill_paths) && killed_mob.life_value)
+ current_enemy_kills++
+
+ on_possible_challenge_completed()
diff --git a/code/modules/battlepass/challenges/kill/kill_humans.dm b/code/modules/battlepass/challenges/kill/kill_humans.dm
new file mode 100644
index 000000000000..a383fb152c22
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/kill_humans.dm
@@ -0,0 +1,29 @@
+/datum/battlepass_challenge/kill_enemies/humans
+ name = "Kill Humans"
+ desc = "Kill AMOUNT humans as a Xenomorph."
+ challenge_category = CHALLENGE_XENO
+ kill_requirement_lower = 3
+ kill_requirement_upper = 5
+ valid_kill_paths = list(
+ /mob/living/carbon/human,
+ )
+ completion_xp = 6
+ pick_weight = 10
+
+/datum/battlepass_challenge/kill_enemies/humans/regenerate_desc()
+ desc = "Kill [enemy_kills_required] human\s as a Xenomorph."
+
+/datum/battlepass_challenge/kill_enemies/humans/on_kill(mob/source, mob/killed_mob, datum/cause_data/cause_data)
+ if(!isxeno(source) || (source.faction == killed_mob.faction))
+ return
+
+ if(!ishuman(killed_mob))
+ return
+
+ var/mob/living/carbon/human/killed_human = killed_mob
+
+ // Synths, preds, etc. don't count towards this
+ if(!isspecieshuman(killed_human))
+ return
+
+ return ..()
diff --git a/code/modules/battlepass/challenges/kill/kill_xenomorphs.dm b/code/modules/battlepass/challenges/kill/kill_xenomorphs.dm
new file mode 100644
index 000000000000..c72753143632
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/kill_xenomorphs.dm
@@ -0,0 +1,19 @@
+/datum/battlepass_challenge/kill_enemies/xenomorphs
+ name = "Kill Xenomorphs"
+ desc = "Kill AMOUNT Xenomorphs as a human."
+ challenge_category = CHALLENGE_HUMAN
+ kill_requirement_lower = 2
+ kill_requirement_upper = 3
+ valid_kill_paths = list(
+ /mob/living/carbon/xenomorph,
+ )
+ completion_xp = 6
+
+/datum/battlepass_challenge/kill_enemies/xenomorphs/regenerate_desc()
+ desc = "Kill [enemy_kills_required] Xenomorph\s as a human."
+
+/datum/battlepass_challenge/kill_enemies/xenomorphs/on_kill(mob/source, mob/killed_mob, datum/cause_data/cause_data)
+ if(!ishuman(source) || (source.faction == killed_mob.faction))
+ return
+
+ return ..()
diff --git a/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/_kill_caste.dm b/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/_kill_caste.dm
new file mode 100644
index 000000000000..3b8c0fff8b47
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/_kill_caste.dm
@@ -0,0 +1,18 @@
+/datum/battlepass_challenge/kill_enemies/xenomorphs/caste
+ name = "Kill Xenomorphs - Caste"
+ desc = "Kill AMOUNT CASTEs as a human."
+ challenge_category = CHALLENGE_NONE
+ /// Possible xenoes to pick from
+ var/list/possible_xeno_castes = list()
+
+/datum/battlepass_challenge/kill_enemies/xenomorphs/caste/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ valid_kill_paths = list(pick(possible_xeno_castes))
+ regenerate_desc()
+
+/datum/battlepass_challenge/kill_enemies/xenomorphs/caste/regenerate_desc()
+ var/mob/xeno_path = valid_kill_paths[1]
+ desc = "Kill [enemy_kills_required] [initial(xeno_path.name)]\s as a human."
diff --git a/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/kill_t1.dm b/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/kill_t1.dm
new file mode 100644
index 000000000000..79c87d39b471
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/kill_t1.dm
@@ -0,0 +1,9 @@
+/datum/battlepass_challenge/kill_enemies/xenomorphs/caste/t1
+ possible_xeno_castes = list(
+ /mob/living/carbon/xenomorph/drone,
+ /mob/living/carbon/xenomorph/runner,
+ /mob/living/carbon/xenomorph/defender,
+ /mob/living/carbon/xenomorph/sentinel,
+ )
+ pick_weight = 7
+ challenge_category = CHALLENGE_HUMAN
diff --git a/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/kill_t2.dm b/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/kill_t2.dm
new file mode 100644
index 000000000000..4243ece10cef
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/kill_t2.dm
@@ -0,0 +1,13 @@
+/datum/battlepass_challenge/kill_enemies/xenomorphs/caste/t2
+ possible_xeno_castes = list(
+ /mob/living/carbon/xenomorph/carrier,
+ /mob/living/carbon/xenomorph/burrower,
+ /mob/living/carbon/xenomorph/hivelord,
+ /mob/living/carbon/xenomorph/warrior,
+ /mob/living/carbon/xenomorph/spitter,
+ /mob/living/carbon/xenomorph/lurker,
+ )
+ kill_requirement_lower = 1
+ kill_requirement_upper = 2
+ pick_weight = 7
+ challenge_category = CHALLENGE_HUMAN
diff --git a/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/kill_t3.dm b/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/kill_t3.dm
new file mode 100644
index 000000000000..6830e8273714
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/specific/kill_xeno_caste/kill_t3.dm
@@ -0,0 +1,11 @@
+/datum/battlepass_challenge/kill_enemies/xenomorphs/caste/t3
+ possible_xeno_castes = list(
+ /mob/living/carbon/xenomorph/crusher,
+ /mob/living/carbon/xenomorph/praetorian,
+ /mob/living/carbon/xenomorph/ravager,
+ /mob/living/carbon/xenomorph/boiler,
+ )
+ kill_requirement_lower = 1
+ kill_requirement_upper = 1
+ pick_weight = 6
+ challenge_category = CHALLENGE_HUMAN
diff --git a/code/modules/battlepass/challenges/kill/specific/weapons/_kill_weapon.dm b/code/modules/battlepass/challenges/kill/specific/weapons/_kill_weapon.dm
new file mode 100644
index 000000000000..1757f73d7dad
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/specific/weapons/_kill_weapon.dm
@@ -0,0 +1,35 @@
+/datum/battlepass_challenge/kill_enemies/xenomorphs/weapon
+ name = "Kill Xenomorphs - Weapon"
+ desc = "Kill AMOUNT Xenomorphs using a WEAPON."
+ kill_requirement_lower = 1
+ kill_requirement_upper = 2
+ challenge_category = CHALLENGE_NONE
+ /// A list of possible weapons for this challenge to choose from. I would do it with typepaths but cause data only tracks names
+ var/list/possible_weapons = list()
+ /// The weapon chosen for this challenge
+ var/weapon_to_use = "" as text
+
+/datum/battlepass_challenge/kill_enemies/xenomorphs/weapon/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ weapon_to_use = pick(possible_weapons)
+ regenerate_desc()
+
+/datum/battlepass_challenge/kill_enemies/xenomorphs/weapon/regenerate_desc()
+ desc = "Kill [enemy_kills_required] Xenomorph\s using \an [weapon_to_use]."
+
+/datum/battlepass_challenge/kill_enemies/xenomorphs/weapon/on_kill(mob/source, mob/killed_mob, datum/cause_data/cause_data)
+ if(!findtext(cause_data.cause_name, weapon_to_use))
+ return
+
+ return ..()
+
+/datum/battlepass_challenge/kill_enemies/xenomorphs/weapon/serialize()
+ . = ..()
+ .["weapon_to_use"] = weapon_to_use
+
+/datum/battlepass_challenge/kill_enemies/xenomorphs/weapon/deserialize(list/save_list)
+ . = ..()
+ weapon_to_use = save_list["weapon_to_use"]
diff --git a/code/modules/battlepass/challenges/kill/specific/weapons/common_weapons.dm b/code/modules/battlepass/challenges/kill/specific/weapons/common_weapons.dm
new file mode 100644
index 000000000000..76e378615252
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/specific/weapons/common_weapons.dm
@@ -0,0 +1,8 @@
+/datum/battlepass_challenge/kill_enemies/xenomorphs/weapon/common
+ possible_weapons = list(
+ "M39 submachinegun",
+ "M4RA battle rifle",
+ "M41A pulse rifle MK2",
+ "M37A2 pump shotgun",
+ )
+ challenge_category = CHALLENGE_HUMAN
diff --git a/code/modules/battlepass/challenges/kill/specific/weapons/pistol_weapons.dm b/code/modules/battlepass/challenges/kill/specific/weapons/pistol_weapons.dm
new file mode 100644
index 000000000000..348ba2b81593
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/specific/weapons/pistol_weapons.dm
@@ -0,0 +1,11 @@
+/datum/battlepass_challenge/kill_enemies/xenomorphs/weapon/pistol
+ possible_weapons = list(
+ "VP78 pistol",
+ "SU-6 Smartpistol",
+ "M44 combat revolver",
+ "88 Mod 4 combat pistol",
+ "M4A3 service pistol",
+ )
+ kill_requirement_upper = 1
+ completion_xp = 7 // pistols are tough
+ challenge_category = CHALLENGE_HUMAN
diff --git a/code/modules/battlepass/challenges/kill/specific/weapons/req_weapons.dm b/code/modules/battlepass/challenges/kill/specific/weapons/req_weapons.dm
new file mode 100644
index 000000000000..0f14493abd84
--- /dev/null
+++ b/code/modules/battlepass/challenges/kill/specific/weapons/req_weapons.dm
@@ -0,0 +1,9 @@
+/datum/battlepass_challenge/kill_enemies/xenomorphs/weapon/req
+ possible_weapons = list(
+ "M240A1 incinerator unit",
+ "MOU53 break action shotgun",
+ "XM88 heavy rifle",
+ "M41A pulse rifle",
+ "M41AE2 heavy pulse rifle",
+ )
+ challenge_category = CHALLENGE_HUMAN
diff --git a/code/modules/battlepass/challenges/xeno_misc/berserker_empower.dm b/code/modules/battlepass/challenges/xeno_misc/berserker_empower.dm
new file mode 100644
index 000000000000..32f583526927
--- /dev/null
+++ b/code/modules/battlepass/challenges/xeno_misc/berserker_empower.dm
@@ -0,0 +1,62 @@
+/datum/battlepass_challenge/berserker_rage
+ name = "Max Berserker Rage"
+ desc = "As a Berserker Ravager, enter maximum berserker rage AMOUNT times."
+ challenge_category = CHALLENGE_XENO
+ completion_xp = 5
+ pick_weight = 6
+ /// The minimum possible amount of times rage needs to be entered
+ var/minimum_rages = 2 as num
+ /// The maximum
+ var/maximum_rages = 4 as num
+ var/rage_requirement = 0 as num
+ var/completed_rages = 0 as num
+
+/datum/battlepass_challenge/berserker_rage/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ rage_requirement = rand(minimum_rages, maximum_rages)
+ regenerate_desc()
+
+/datum/battlepass_challenge/berserker_rage/regenerate_desc()
+ desc = "As a Berserker Ravager, enter maximum berserker rage [rage_requirement] time\s."
+
+/datum/battlepass_challenge/berserker_rage/on_client_login_mob(datum/source, mob/logged_in_mob)
+ . = ..()
+ if(!completed)
+ RegisterSignal(logged_in_mob, COMSIG_XENO_RAGE_MAX, PROC_REF(on_rage_max))
+
+/datum/battlepass_challenge/berserker_rage/unhook_signals(mob/source)
+ UnregisterSignal(source, COMSIG_XENO_RAGE_MAX)
+
+/datum/battlepass_challenge/berserker_rage/check_challenge_completed()
+ return (completed_rages >= rage_requirement)
+
+/datum/battlepass_challenge/berserker_rage/get_completion_percent()
+ return (completed_rages / rage_requirement)
+
+/datum/battlepass_challenge/berserker_rage/get_completion_numerator()
+ return completed_rages
+
+/datum/battlepass_challenge/berserker_rage/get_completion_denominator()
+ return rage_requirement
+
+/datum/battlepass_challenge/berserker_rage/serialize()
+ . = ..()
+ .["rage_requirement"] = rage_requirement
+ .["completed_rages"] = completed_rages
+
+/datum/battlepass_challenge/berserker_rage/deserialize(list/save_list)
+ . = ..()
+ rage_requirement = save_list["rage_requirement"]
+ completed_rages = save_list["completed_rages"]
+
+/// When the xeno plants a resin node
+/datum/battlepass_challenge/berserker_rage/proc/on_rage_max(datum/source)
+ SIGNAL_HANDLER
+
+ completed_rages++
+ on_possible_challenge_completed()
+
+
diff --git a/code/modules/battlepass/challenges/xeno_misc/facehug.dm b/code/modules/battlepass/challenges/xeno_misc/facehug.dm
new file mode 100644
index 000000000000..135f35f21af7
--- /dev/null
+++ b/code/modules/battlepass/challenges/xeno_misc/facehug.dm
@@ -0,0 +1,60 @@
+/datum/battlepass_challenge/facehug
+ name = "Facehug Humans"
+ desc = "As a facehugger, facehug AMOUNT humans."
+ challenge_category = CHALLENGE_XENO
+ completion_xp = 5
+ pick_weight = 7
+ var/minimum = 1 as num
+ var/maximum = 3 as num
+ var/requirement = 0 as num
+ var/filled = 0 as num
+
+/datum/battlepass_challenge/facehug/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ requirement = rand(minimum, maximum)
+ regenerate_desc()
+
+/datum/battlepass_challenge/facehug/regenerate_desc()
+ desc = "As a facehugger, facehug [requirement] human\s."
+
+/datum/battlepass_challenge/facehug/on_client_login_mob(datum/source, mob/logged_in_mob)
+ . = ..()
+ if(!completed)
+ RegisterSignal(logged_in_mob, COMSIG_XENO_FACEHUGGED_HUMAN, PROC_REF(on_sigtrigger))
+
+/datum/battlepass_challenge/facehug/unhook_signals(mob/source)
+ UnregisterSignal(source, COMSIG_XENO_FACEHUGGED_HUMAN)
+
+/datum/battlepass_challenge/facehug/check_challenge_completed()
+ return (filled >= requirement)
+
+/datum/battlepass_challenge/facehug/get_completion_percent()
+ return (filled / requirement)
+
+/datum/battlepass_challenge/facehug/get_completion_numerator()
+ return filled
+
+/datum/battlepass_challenge/facehug/get_completion_denominator()
+ return requirement
+
+/datum/battlepass_challenge/facehug/serialize()
+ . = ..()
+ .["requirement"] = requirement
+ .["filled"] = filled
+
+/datum/battlepass_challenge/facehug/deserialize(list/save_list)
+ . = ..()
+ requirement = save_list["requirement"]
+ filled = save_list["filled"]
+
+/// When the xeno plants a resin node
+/datum/battlepass_challenge/facehug/proc/on_sigtrigger(datum/source)
+ SIGNAL_HANDLER
+
+ filled++
+ on_possible_challenge_completed()
+
+
diff --git a/code/modules/battlepass/challenges/xeno_misc/for_the_hive.dm b/code/modules/battlepass/challenges/xeno_misc/for_the_hive.dm
new file mode 100644
index 000000000000..66e47188ab65
--- /dev/null
+++ b/code/modules/battlepass/challenges/xeno_misc/for_the_hive.dm
@@ -0,0 +1,60 @@
+/datum/battlepass_challenge/for_the_hive
+ name = "For The Hive!"
+ desc = "As an Acider Runner, detonate For The Hive at maximum acid AMOUNT times."
+ challenge_category = CHALLENGE_XENO
+ completion_xp = 6
+ pick_weight = 6
+ var/minimum = 1 as num
+ var/maximum = 2 as num
+ var/requirement = 0 as num
+ var/filled = 0 as num
+
+/datum/battlepass_challenge/for_the_hive/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ requirement = rand(minimum, maximum)
+ regenerate_desc()
+
+/datum/battlepass_challenge/for_the_hive/regenerate_desc()
+ desc = "As an Acider Runner, detonate For The Hive at maximum acid [requirement] times."
+
+/datum/battlepass_challenge/for_the_hive/on_client_login_mob(datum/source, mob/logged_in_mob)
+ . = ..()
+ if(!completed)
+ RegisterSignal(logged_in_mob, COMSIG_XENO_FTH_MAX_ACID, PROC_REF(on_sigtrigger))
+
+/datum/battlepass_challenge/for_the_hive/unhook_signals(mob/source)
+ UnregisterSignal(source, COMSIG_XENO_FTH_MAX_ACID)
+
+/datum/battlepass_challenge/for_the_hive/check_challenge_completed()
+ return (filled >= requirement)
+
+/datum/battlepass_challenge/for_the_hive/get_completion_percent()
+ return (filled / requirement)
+
+/datum/battlepass_challenge/for_the_hive/get_completion_numerator()
+ return filled
+
+/datum/battlepass_challenge/for_the_hive/get_completion_denominator()
+ return requirement
+
+/datum/battlepass_challenge/for_the_hive/serialize()
+ . = ..()
+ .["requirement"] = requirement
+ .["filled"] = filled
+
+/datum/battlepass_challenge/for_the_hive/deserialize(list/save_list)
+ . = ..()
+ requirement = save_list["requirement"]
+ filled = save_list["filled"]
+
+/// When the xeno plants a resin node
+/datum/battlepass_challenge/for_the_hive/proc/on_sigtrigger(datum/source)
+ SIGNAL_HANDLER
+
+ filled++
+ on_possible_challenge_completed()
+
+
diff --git a/code/modules/battlepass/challenges/xeno_misc/glob_direct_hit.dm b/code/modules/battlepass/challenges/xeno_misc/glob_direct_hit.dm
new file mode 100644
index 000000000000..b49d5f9a99c0
--- /dev/null
+++ b/code/modules/battlepass/challenges/xeno_misc/glob_direct_hit.dm
@@ -0,0 +1,62 @@
+/datum/battlepass_challenge/glob_hits
+ name = "Direct Glob Hits"
+ desc = "Land AMOUNT direct acid glob hits as a Boiler."
+ challenge_category = CHALLENGE_XENO
+ completion_xp = 6
+ pick_weight = 6
+ var/minimum = 1 as num
+ var/maximum = 4 as num
+ var/requirement = 0 as num
+ var/filled = 0 as num
+
+/datum/battlepass_challenge/glob_hits/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ requirement = rand(minimum, maximum)
+ regenerate_desc()
+
+/datum/battlepass_challenge/glob_hits/regenerate_desc()
+ desc = "Land [requirement] direct acid glob hit\s as a Boiler."
+
+/datum/battlepass_challenge/glob_hits/on_client_login_mob(datum/source, mob/logged_in_mob)
+ . = ..()
+ if(!completed)
+ RegisterSignal(logged_in_mob, COMSIG_FIRER_PROJECTILE_DIRECT_HIT, PROC_REF(on_sigtrigger))
+
+/datum/battlepass_challenge/glob_hits/unhook_signals(mob/source)
+ UnregisterSignal(source, COMSIG_FIRER_PROJECTILE_DIRECT_HIT)
+
+/datum/battlepass_challenge/glob_hits/check_challenge_completed()
+ return (filled >= requirement)
+
+/datum/battlepass_challenge/glob_hits/get_completion_percent()
+ return (filled / requirement)
+
+/datum/battlepass_challenge/glob_hits/get_completion_numerator()
+ return filled
+
+/datum/battlepass_challenge/glob_hits/get_completion_denominator()
+ return requirement
+
+/datum/battlepass_challenge/glob_hits/serialize()
+ . = ..()
+ .["requirement"] = requirement
+ .["filled"] = filled
+
+/datum/battlepass_challenge/glob_hits/deserialize(list/save_list)
+ . = ..()
+ requirement = save_list["requirement"]
+ filled = save_list["filled"]
+
+/// When the xeno plants a resin node
+/datum/battlepass_challenge/glob_hits/proc/on_sigtrigger(datum/source, obj/projectile/hit_projectile)
+ SIGNAL_HANDLER
+ if(!istype(hit_projectile.ammo, /datum/ammo/xeno/boiler_gas))
+ return
+
+ filled++
+ on_possible_challenge_completed()
+
+
diff --git a/code/modules/battlepass/challenges/xeno_misc/plant_froot.dm b/code/modules/battlepass/challenges/xeno_misc/plant_froot.dm
new file mode 100644
index 000000000000..30caa69c091a
--- /dev/null
+++ b/code/modules/battlepass/challenges/xeno_misc/plant_froot.dm
@@ -0,0 +1,62 @@
+/datum/battlepass_challenge/plant_fruit
+ name = "Plant Resin Fruit"
+ desc = "Plant AMOUNT resin fruits."
+ challenge_category = CHALLENGE_XENO
+ completion_xp = 5
+ pick_weight = 8
+ var/minimum = 20 as num
+ var/maximum = 30 as num
+ var/requirement = 0 as num
+ var/filled = 0 as num
+
+/datum/battlepass_challenge/plant_fruit/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ requirement = rand(minimum, maximum)
+ regenerate_desc()
+
+/datum/battlepass_challenge/plant_fruit/regenerate_desc()
+ desc = "Plant [requirement] resin fruit\s."
+
+/datum/battlepass_challenge/plant_fruit/on_client_login_mob(datum/source, mob/logged_in_mob)
+ . = ..()
+ if(!completed)
+ RegisterSignal(logged_in_mob, COMSIG_XENO_PLANTED_FRUIT, PROC_REF(on_sigtrigger))
+
+/datum/battlepass_challenge/plant_fruit/unhook_signals(mob/source)
+ UnregisterSignal(source, COMSIG_XENO_PLANTED_FRUIT)
+
+/datum/battlepass_challenge/plant_fruit/check_challenge_completed()
+ return (filled >= requirement)
+
+/datum/battlepass_challenge/plant_fruit/get_completion_percent()
+ return (filled / requirement)
+
+/datum/battlepass_challenge/plant_fruit/get_completion_numerator()
+ return filled
+
+/datum/battlepass_challenge/plant_fruit/get_completion_denominator()
+ return requirement
+
+/datum/battlepass_challenge/plant_fruit/serialize()
+ . = ..()
+ .["requirement"] = requirement
+ .["filled"] = filled
+
+/datum/battlepass_challenge/plant_fruit/deserialize(list/save_list)
+ . = ..()
+ requirement = save_list["requirement"]
+ filled = save_list["filled"]
+
+/// When the xeno plants a resin node
+/datum/battlepass_challenge/plant_fruit/proc/on_sigtrigger(datum/source, mob/planter)
+ SIGNAL_HANDLER
+ if(should_block_game_interaction(planter))
+ return
+
+ filled++
+ on_possible_challenge_completed()
+
+
diff --git a/code/modules/battlepass/challenges/xeno_misc/plant_resin_nodes.dm b/code/modules/battlepass/challenges/xeno_misc/plant_resin_nodes.dm
new file mode 100644
index 000000000000..dcb14a8ea153
--- /dev/null
+++ b/code/modules/battlepass/challenges/xeno_misc/plant_resin_nodes.dm
@@ -0,0 +1,66 @@
+/datum/battlepass_challenge/plant_resin_nodes
+ name = "Plant Resin Nodes"
+ desc = "Plant AMOUNT resin nodes."
+ challenge_category = CHALLENGE_XENO
+ completion_xp = 5
+ pick_weight = 8
+ /// The minimum possible amount of nodes that need to be planted
+ var/minimum_nodes = 20 as num
+ /// The maximum
+ var/maximum_nodes = 30 as num
+ /// How many nodes need to be planted
+ var/node_requirement = 0 as num
+ /// How many nodes have been planted so far
+ var/planted_nodes = 0 as num
+
+/datum/battlepass_challenge/plant_resin_nodes/New(client/owning_client)
+ . = ..()
+ if(!.)
+ return .
+
+ node_requirement = rand(minimum_nodes, maximum_nodes)
+ regenerate_desc()
+
+/datum/battlepass_challenge/plant_resin_nodes/regenerate_desc()
+ desc = "Plant [node_requirement] resin node\s."
+
+/datum/battlepass_challenge/plant_resin_nodes/on_client_login_mob(datum/source, mob/logged_in_mob)
+ . = ..()
+ if(!completed)
+ RegisterSignal(logged_in_mob, COMSIG_XENO_PLANT_RESIN_NODE, PROC_REF(on_plant_node))
+
+/datum/battlepass_challenge/plant_resin_nodes/unhook_signals(mob/source)
+ UnregisterSignal(source, COMSIG_XENO_PLANT_RESIN_NODE)
+
+/datum/battlepass_challenge/plant_resin_nodes/check_challenge_completed()
+ return (planted_nodes >= node_requirement)
+
+/datum/battlepass_challenge/plant_resin_nodes/get_completion_percent()
+ return (planted_nodes / node_requirement)
+
+/datum/battlepass_challenge/plant_resin_nodes/get_completion_numerator()
+ return planted_nodes
+
+/datum/battlepass_challenge/plant_resin_nodes/get_completion_denominator()
+ return node_requirement
+
+/datum/battlepass_challenge/plant_resin_nodes/serialize()
+ . = ..()
+ .["node_requirement"] = node_requirement
+ .["planted_nodes"] = planted_nodes
+
+/datum/battlepass_challenge/plant_resin_nodes/deserialize(list/save_list)
+ . = ..()
+ node_requirement = save_list["node_requirement"]
+ planted_nodes = save_list["planted_nodes"]
+
+/// When the xeno plants a resin node
+/datum/battlepass_challenge/plant_resin_nodes/proc/on_plant_node(datum/source, mob/planter)
+ SIGNAL_HANDLER
+ if(should_block_game_interaction(planter))
+ return
+
+ planted_nodes++
+ on_possible_challenge_completed()
+
+
diff --git a/code/modules/battlepass/rewards/_battlepass_reward.dm b/code/modules/battlepass/rewards/_battlepass_reward.dm
new file mode 100644
index 000000000000..b691116c13a1
--- /dev/null
+++ b/code/modules/battlepass/rewards/_battlepass_reward.dm
@@ -0,0 +1,34 @@
+/mob/living/carbon
+ var/list/claimed_reward_categories = list()
+
+/datum/battlepass_reward
+ /// The name of this reward
+ var/name = "" as text
+ /// The iconfile that contains the image of this reward
+ var/icon = 'code/modules/battlepass/rewards/sprites/battlepass.dmi'
+ /// The iconstate of the image of this reward
+ var/icon_state = "coin_diamond" as text
+ /// What category this item falls under (armor, toy, etc)
+ var/category
+ /// If this item can bypass the 1-per-category limit
+ var/category_limit_bypass = FALSE
+ var/lifeform_type = "Marine"
+
+/datum/battlepass_reward/proc/can_claim(mob/target_mob)
+ if(!iscarbon(target_mob))
+ return FALSE
+
+ var/mob/living/carbon/carbon_mob = target_mob
+
+ if((category in carbon_mob.claimed_reward_categories) && !category_limit_bypass)
+ return FALSE
+
+ return TRUE
+
+/datum/battlepass_reward/proc/on_claim(mob/target_mob)
+ return
+
+/datum/battlepass_reward/test
+ name = "Debug"
+ icon = 'icons/obj/items/items.dmi'
+ icon_state = "coin_diamond"
diff --git a/code/modules/battlepass/rewards/code/particles.dm b/code/modules/battlepass/rewards/code/particles.dm
new file mode 100644
index 000000000000..a8c1411b9bec
--- /dev/null
+++ b/code/modules/battlepass/rewards/code/particles.dm
@@ -0,0 +1,277 @@
+///objects can only have one particle on them at a time, so we use these abstract effects to hold and display the effects. You know, so multiple particle effects can exist at once.
+///also because some objects do not display particles due to how their visuals are built
+/obj/effect/abstract/particle_holder
+ name = "particle holder"
+ desc = "How are you reading this? Please make a bug report :)"
+ appearance_flags = KEEP_APART|KEEP_TOGETHER|TILE_BOUND|PIXEL_SCALE|LONG_GLIDE //movable appearance_flags plus KEEP_APART and KEEP_TOGETHER
+ vis_flags = VIS_INHERIT_PLANE
+ layer = ABOVE_XENO_LAYER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ anchored = TRUE
+ /// Holds info about how this particle emitter works
+ /// See \code\__DEFINES\particles.dm
+ var/particle_flags = NONE
+
+ var/atom/parent
+
+/obj/effect/abstract/particle_holder/Initialize(mapload, particle_path_or_instance, particle_flags = NONE)
+ . = ..()
+ if(!loc)
+ stack_trace("particle holder was created with no loc!")
+ return INITIALIZE_HINT_QDEL
+ // We nullspace ourselves because some objects use their contents (e.g. storage) and some items may drop everything in their contents on deconstruct.
+ parent = loc
+ loc = null
+
+ // Mouse opacity can get set to opaque by some objects when placed into the object's contents (storage containers).
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ src.particle_flags = particle_flags
+ if(ispath(particle_path_or_instance))
+ particles = new particle_path_or_instance()
+ else
+ particles = particle_path_or_instance
+ // /atom doesn't have vis_contents, /turf and /atom/movable do
+ var/atom/movable/lie_about_areas = parent
+ lie_about_areas.vis_contents += src
+ RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(parent_deleted))
+
+ if(particle_flags & PARTICLE_ATTACH_MOB)
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
+ on_move(parent, null, NORTH)
+
+/obj/effect/abstract/particle_holder/Destroy(force)
+ QDEL_NULL(particles)
+ parent = null
+ return ..()
+
+/// Non movables don't delete contents on destroy, so we gotta do this
+/obj/effect/abstract/particle_holder/proc/parent_deleted(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/// signal called when a parent that's been hooked into this moves
+/// does a variety of checks to ensure overrides work out properly
+/obj/effect/abstract/particle_holder/proc/on_move(atom/movable/attached, atom/oldloc, direction)
+ SIGNAL_HANDLER
+
+ if(!(particle_flags & PARTICLE_ATTACH_MOB))
+ return
+
+ //remove old
+ if(ismob(oldloc))
+ var/mob/particle_mob = oldloc
+ particle_mob.vis_contents -= src
+
+ // If we're sitting in a mob, we want to emit from it too, for vibes and shit
+ if(ismob(attached.loc))
+ var/mob/particle_mob = attached.loc
+ particle_mob.vis_contents += src
+
+/// Sets the particles position to the passed coordinate list (X, Y, Z)
+/// See [https://www.byond.com/docs/ref/#/{notes}/particles] for position documentation
+/obj/effect/abstract/particle_holder/proc/set_particle_position(list/pos)
+ particles.position = pos
+
+/particles/proc/resize_pos(mob/assigned_mob)
+ return
+
+/particles/droplets
+ icon = 'icons/effects/particles/generic.dmi'
+ icon_state = list("dot"=2,"drop"=1)
+ width = 32
+ height = 36
+ count = 5
+ spawning = 0.2
+ lifespan = 1 SECONDS
+ fade = 0.5 SECONDS
+ color = "#549EFF"
+ position = generator(GEN_BOX, list(-9,-9,0), list(9,18,0), NORMAL_RAND)
+ scale = generator(GEN_VECTOR, list(0.9,0.9), list(1.1,1.1), NORMAL_RAND)
+ gravity = list(0, -0.9)
+
+/particles/droplets/resize_pos(mob/assigned_mob)
+ var/is = assigned_mob.icon_size / 32
+ position = generator(GEN_BOX, list(-9*is, -9*is, 0), list(9*is,18*is,0), NORMAL_RAND)
+
+/particles/slime
+ icon = 'icons/effects/particles/goop.dmi'
+ icon_state = list("goop_1" = 6, "goop_2" = 2, "goop_3" = 1)
+ width = 100
+ height = 100
+ count = 100
+ spawning = 0.5
+ color = "#4b4a4aa0"
+ lifespan = 1.5 SECONDS
+ fade = 1 SECONDS
+ grow = -0.025
+ gravity = list(0, -0.05)
+ position = generator(GEN_BOX, list(-8,-16,0), list(8,16,0), NORMAL_RAND)
+ spin = generator(GEN_NUM, -15, 15, NORMAL_RAND)
+ scale = list(0.75, 0.75)
+
+/particles/slime/resize_pos(mob/assigned_mob)
+ var/is = assigned_mob.icon_size / 32
+ position = generator(GEN_BOX, list(-8*is, -16*is, 0), list(8*is,16*is,0), NORMAL_RAND)
+
+/// Rainbow slime particles.
+/particles/slime/rainbow
+ gradient = list(0, "#f00a", 3, "#0ffa", 6, "#f00a", "loop", "space"=COLORSPACE_HSL)
+ color_change = 0.2
+ color = generator(GEN_NUM, 0, 6, UNIFORM_RAND)
+
+/particles/pollen
+ icon = 'icons/effects/particles/pollen.dmi'
+ icon_state = "pollen"
+ width = 100
+ height = 100
+ count = 1000
+ spawning = 4
+ lifespan = 0.7 SECONDS
+ fade = 1 SECONDS
+ grow = -0.01
+ velocity = list(0, 0)
+ position = generator(GEN_CIRCLE, 0, 16, NORMAL_RAND)
+ drift = generator(GEN_VECTOR, list(0, -0.2), list(0, 0.2))
+ gravity = list(0, 0.95)
+ scale = generator(GEN_VECTOR, list(0.3, 0.3), list(1,1), NORMAL_RAND)
+ rotation = 30
+ spin = generator(GEN_NUM, -20, 20)
+
+/particles/pollen/resize_pos(mob/assigned_mob)
+ var/is = assigned_mob.icon_size / 32
+ position = generator(GEN_CIRCLE, 0, 16*is, NORMAL_RAND)
+
+
+/particles/stink
+ icon = 'icons/effects/particles/stink.dmi'
+ icon_state = list("stink_1" = 1, "stink_2" = 2, "stink_3" = 2)
+ color = "#0BDA51"
+ width = 100
+ height = 100
+ count = 25
+ spawning = 0.25
+ lifespan = 1 SECONDS
+ fade = 1 SECONDS
+ position = generator(GEN_CIRCLE, 0, 16, UNIFORM_RAND)
+ gravity = list(0, 0.25)
+
+/particles/stink/resize_pos(mob/assigned_mob)
+ var/is = assigned_mob.icon_size / 32
+ position = generator(GEN_CIRCLE, 0, 16*is, NORMAL_RAND)
+
+/particles/musical_notes
+ icon = 'icons/effects/particles/notes/note.dmi'
+ icon_state = list(
+ "note_1" = 1,
+ "note_2" = 1,
+ "note_3" = 1,
+ "note_4" = 1,
+ "note_5" = 1,
+ "note_6" = 1,
+ "note_7" = 1,
+ "note_8" = 1,
+ )
+ width = 100
+ height = 100
+ count = 250
+ spawning = 0.3
+ lifespan = 0.7 SECONDS
+ fade = 1 SECONDS
+ grow = -0.01
+ velocity = list(0, 0)
+ position = generator(GEN_CIRCLE, 0, 16, NORMAL_RAND)
+ drift = generator(GEN_VECTOR, list(0, -0.2), list(0, 0.2))
+ gravity = list(0, 0.95)
+
+/particles/musical_notes/resize_pos(mob/assigned_mob)
+ var/is = assigned_mob.icon_size / 32
+ position = generator(GEN_CIRCLE, 0, 16*is, NORMAL_RAND)
+
+/particles/musical_notes/holy
+ icon = 'icons/effects/particles/notes/note_holy.dmi'
+ icon_state = list(
+ "holy_1" = 1,
+ "holy_2" = 1,
+ "holy_3" = 1,
+ "holy_4" = 1,
+ "holy_5" = 1,
+ "holy_6" = 1,
+ "holy_7" = 1,
+ "holy_8" = 1,
+ "holy_9" = 4, //holy theme specific
+ )
+
+/particles/musical_notes/nullwave
+ icon = 'icons/effects/particles/notes/note_null.dmi'
+ icon_state = list(
+ "null_1" = 1,
+ "null_2" = 1,
+ "null_3" = 1,
+ "null_4" = 1,
+ "null_5" = 1,
+ "null_6" = 1,
+ "null_7" = 1,
+ "null_8" = 1,
+ "null_9" = 2, //heal theme specific
+ "null_10" = 2, //heal theme specific
+ )
+
+/particles/musical_notes/harm
+ icon = 'icons/effects/particles/notes/note_harm.dmi'
+ icon_state = list(
+ "harm_1" = 1,
+ "harm_2" = 1,
+ "harm_3" = 1,
+ "harm_4" = 1,
+ "harm_5" = 1,
+ "harm_6" = 1,
+ "harm_7" = 1,
+ "harm_8" = 1,
+ "harm_9" = 2, //harm theme specific
+ "harm_10" = 2, //harm theme specific
+ )
+
+/particles/musical_notes/sleepy
+ icon = 'icons/effects/particles/notes/note_sleepy.dmi'
+ icon_state = list(
+ "sleepy_1" = 1,
+ "sleepy_2" = 1,
+ "sleepy_3" = 1,
+ "sleepy_4" = 1,
+ "sleepy_5" = 1,
+ "sleepy_6" = 1,
+ "sleepy_7" = 1,
+ "sleepy_8" = 1,
+ "sleepy_9" = 2, //sleepy theme specific
+ "sleepy_10" = 2, //sleepy theme specific
+ )
+
+/particles/musical_notes/light
+ icon = 'icons/effects/particles/notes/note_light.dmi'
+ icon_state = list(
+ "power_1" = 1,
+ "power_2" = 1,
+ "power_3" = 1,
+ "power_4" = 1,
+ "power_5" = 1,
+ "power_6" = 1,
+ "power_7" = 1,
+ "power_8" = 1,
+ "power_9" = 2, //light theme specific
+ "power_10" = 2, //light theme specific
+ )
+
+/particles/acid
+ icon = 'icons/effects/particles/goop.dmi'
+ icon_state = list("goop_1" = 6, "goop_2" = 2, "goop_3" = 1)
+ width = 100
+ height = 100
+ count = 100
+ spawning = 0.5
+ color = "#00ea2b80" //to get 96 alpha
+ lifespan = 1.5 SECONDS
+ fade = 1 SECONDS
+ grow = -0.025
+ gravity = list(0, 0.15)
+ position = generator(GEN_SPHERE, 0, 16, NORMAL_RAND)
+ spin = generator(GEN_NUM, -15, 15, NORMAL_RAND)
diff --git a/code/modules/battlepass/rewards/general_rewards.dm b/code/modules/battlepass/rewards/general_rewards.dm
new file mode 100644
index 000000000000..c3efb32e5eef
--- /dev/null
+++ b/code/modules/battlepass/rewards/general_rewards.dm
@@ -0,0 +1,49 @@
+/datum/battlepass_reward/general
+ lifeform_type = "All"
+
+
+/datum/battlepass_reward/general/particle
+ category = REWARD_CATEGORY_PARTICLE
+ var/particle_path
+
+/datum/battlepass_reward/general/particle/on_claim(mob/target_mob)
+ var/particles/new_particle = new particle_path()
+ new_particle.resize_pos(target_mob)
+ target_mob.particle_holder = new(target_mob, new_particle)
+ if(target_mob.icon_size == 48)
+ target_mob.particle_holder.pixel_x = 16
+ else if(target_mob.icon_size == 64)
+ target_mob.particle_holder.pixel_x = 16
+ target_mob.particle_holder.pixel_y = 16
+ return TRUE
+
+
+/datum/battlepass_reward/general/particle/droplets
+ name = "Water Particles"
+ icon_state = "droplets"
+ particle_path = /particles/droplets
+
+/datum/battlepass_reward/general/particle/acid
+ name = "Acid Particles"
+ icon_state = "acid"
+ particle_path = /particles/acid
+
+/datum/battlepass_reward/general/particle/music
+ name = "Musical Particles"
+ icon_state = "notes"
+ particle_path = /particles/musical_notes
+
+/datum/battlepass_reward/general/particle/slime
+ name = "Slime Particles"
+ icon_state = "slime"
+ particle_path = /particles/slime
+
+/datum/battlepass_reward/general/particle/slime_rainbow
+ name = "Rbw. Slime Particles"
+ icon_state = "rainbow_slime"
+ particle_path = /particles/slime/rainbow
+
+/datum/battlepass_reward/general/particle/pollen
+ name = "Pollen Particles"
+ icon_state = "pollen"
+ particle_path = /particles/pollen
diff --git a/code/modules/battlepass/rewards/marine_rewards.dm b/code/modules/battlepass/rewards/marine_rewards.dm
new file mode 100644
index 000000000000..11f44c58ae9f
--- /dev/null
+++ b/code/modules/battlepass/rewards/marine_rewards.dm
@@ -0,0 +1,264 @@
+/datum/battlepass_reward/marine
+ lifeform_type = "Marine"
+
+/datum/battlepass_reward/marine/can_claim(mob/target_mob)
+ . = ..()
+ if(!.)
+ return .
+
+ if(!ishuman(target_mob))
+ return FALSE
+
+ return TRUE
+
+
+/datum/battlepass_reward/marine/diamond_armor
+ name = "Diamond Armor"
+ icon_state = "diamond_armor"
+ category = REWARD_CATEGORY_ARMOR
+
+/datum/battlepass_reward/marine/diamond_armor/on_claim(mob/target_mob)
+ if(!ishuman(target_mob))
+ return FALSE
+
+ var/mob/living/carbon/human/target_human = target_mob
+ if(!istype(target_human.wear_suit, /obj/item/clothing/suit/storage/marine))
+ to_chat(target_human, SPAN_WARNING("You need to be wearing marine armor to claim this!"))
+ return FALSE
+
+ var/obj/item/clothing/suit/storage/marine/marine_armor = target_human.wear_suit
+ if(marine_armor.flags_inventory & BLOCK_KNOCKDOWN) // a bit of fairness
+ to_chat(target_human, SPAN_WARNING("Armor that blocks knockdowns cannot have a skin!"))
+ return FALSE
+
+ marine_armor.flags_marine_armor &= ~ARMOR_SQUAD_OVERLAY
+ marine_armor.flags_atom |= NO_SNOW_TYPE
+ marine_armor.armor_variation = FALSE
+ marine_armor.icon = 'code/modules/battlepass/rewards/sprites/armorobj.dmi'
+ marine_armor.icon_state = "diamond_armor"
+ marine_armor.name = "diamond [marine_armor.name]"
+ marine_armor.desc = "This suit of diamond armor looks impossible to get into. How the hell would anyone be able to afford this?"
+ marine_armor.item_icons = list(
+ WEAR_JACKET = 'code/modules/battlepass/rewards/sprites/armor.dmi'
+ )
+ marine_armor.update_icon()
+ marine_armor.update_clothing_icon()
+
+ if(target_human.shoes)
+ target_human.shoes.flags_atom |= NO_SNOW_TYPE
+ target_human.shoes.icon = 'code/modules/battlepass/rewards/sprites/armorobj.dmi'
+ target_human.shoes.icon_state = "diamond_boots"
+ target_human.shoes.item_icons = list(
+ WEAR_FEET = 'code/modules/battlepass/rewards/sprites/armor.dmi'
+ )
+ if(istype(target_human.shoes, /obj/item/clothing/shoes/marine))
+ var/obj/item/clothing/shoes/marine/marine_shoes = target_human.shoes
+ marine_shoes.base_icon_state = "diamond_boots"
+
+ target_human.shoes.name = "diamond [target_human.shoes]"
+ target_human.shoes.desc = "This pair of diamond boots look impossible to walk around in, but you think you can manage, somehow."
+ target_human.shoes.update_icon()
+ target_human.update_inv_shoes()
+
+ if(istype(target_human.head, /obj/item/clothing/head/helmet/marine))
+ var/obj/item/clothing/head/helmet/marine/marine_helmet = target_human.head
+ target_human.head.flags_atom |= NO_SNOW_TYPE
+ marine_helmet.flags_marine_helmet = NONE
+ marine_helmet.icon = 'code/modules/battlepass/rewards/sprites/armorobj.dmi'
+ marine_helmet.icon_state = "diamond_helmet"
+ marine_helmet.name = "diamond [marine_helmet.name]"
+ marine_helmet.desc = "A diamond helmet. This thing is definitely better than whatever the corps would give you, that's for sure."
+ marine_helmet.item_icons = list(
+ WEAR_HEAD = 'code/modules/battlepass/rewards/sprites/armor.dmi'
+ )
+ marine_helmet.update_icon()
+ return TRUE
+
+
+/datum/battlepass_reward/marine/toy
+ category = REWARD_CATEGORY_TOY
+ var/item_path
+
+/datum/battlepass_reward/marine/toy/on_claim(mob/target_mob)
+ if(!ishuman(target_mob) || !item_path)
+ return FALSE
+
+ var/mob/living/carbon/human/target_human = target_mob
+ var/obj/item/new_toy = new item_path
+ target_human.put_in_hands(new_toy, TRUE)
+ return TRUE
+
+
+/datum/battlepass_reward/marine/toy/sus_crayon
+ name = "Suspicious Crayon"
+ icon_state = "sus_crayon"
+ item_path = /obj/item/toy/suspicious
+
+
+/datum/battlepass_reward/marine/toy/runner_toy
+ name = "Runner Toy"
+ icon = 'code/modules/battlepass/rewards/sprites/toy.dmi'
+ icon_state = "runner_toy"
+ item_path = /obj/item/toy/plush/runner_toy
+
+
+/datum/battlepass_reward/marine/toy/warrior_toy
+ name = "Warrior Toy"
+ icon = 'code/modules/battlepass/rewards/sprites/toy.dmi'
+ icon_state = "warrior_toy"
+ item_path = /obj/item/toy/plush/warrior_toy
+
+
+/datum/battlepass_reward/marine/toy/queen_toy
+ name = "Queen Toy"
+ icon = 'code/modules/battlepass/rewards/sprites/toy.dmi'
+ icon_state = "queen_toy"
+ item_path = /obj/item/toy/plush/queen_toy
+
+
+/datum/battlepass_reward/marine/toy/bikehorn
+ name = "Bike Horn"
+ icon = 'icons/obj/items/items.dmi'
+ icon_state = "bike_horn"
+ item_path = /obj/item/toy/bikehorn
+
+
+/obj/item/clothing/head/helmet/marine
+ var/mutable_appearance/helmet_fire_overlay_icon
+ var/mutable_appearance/helmet_fire_overlay_mob_icon
+
+/datum/battlepass_reward/marine/helmet_fire
+ name = "Helmet Fire"
+ icon = 'code/modules/battlepass/rewards/sprites/helmetfire.dmi'
+ icon_state = "orange_static"
+ category = REWARD_CATEGORY_HELMET_FIRE
+ /// The iconstate to give the helmet's fire
+ var/helmet_fire_iconstate
+
+/datum/battlepass_reward/marine/helmet_fire/on_claim(mob/target_mob)
+ if(!ishuman(target_mob) || !helmet_fire_iconstate)
+ return FALSE
+
+ var/mob/living/carbon/human/target_human = target_mob
+ if(!istype(target_human.head, /obj/item/clothing/head/helmet/marine))
+ to_chat(target_human, SPAN_WARNING("You can't claim this unless you're wearing a marine helmet!"))
+ return FALSE
+
+ var/obj/item/clothing/head/helmet/marine/helmet = target_human.head
+ helmet.helmet_fire_overlay_icon = mutable_appearance('code/modules/battlepass/rewards/sprites/helmetfire.dmi', helmet_fire_iconstate)
+ helmet.helmet_fire_overlay_mob_icon = mutable_appearance('code/modules/battlepass/rewards/sprites/helmetfire.dmi', helmet_fire_iconstate)
+ helmet.helmet_fire_overlay_mob_icon.pixel_x = -16
+ helmet.helmet_fire_overlay_mob_icon.pixel_y = -16
+ helmet.helmet_fire_overlay_icon.pixel_x = -16
+ helmet.helmet_fire_overlay_icon.pixel_y = -24
+ helmet.update_icon()
+ return TRUE
+
+/datum/battlepass_reward/marine/helmet_fire/orange
+ name = "Helmet Fire (Orange)"
+ icon_state = "orange_static"
+ helmet_fire_iconstate = "orange"
+
+/datum/battlepass_reward/marine/helmet_fire/green
+ name = "Helmet Fire (Green)"
+ icon_state = "green_static"
+ helmet_fire_iconstate = "green"
+
+/datum/battlepass_reward/marine/helmet_fire/blue
+ name = "Helmet Fire (Blue)"
+ icon_state = "blue_static"
+ helmet_fire_iconstate = "blue"
+
+/datum/battlepass_reward/marine/helmet_fire/purple
+ name = "Helmet Fire (Purple)"
+ icon_state = "purple_static"
+ helmet_fire_iconstate = "purple"
+
+
+/datum/battlepass_reward/marine/m41a_reskin
+ name = "Golden M41A MK2"
+ icon_state = "m41a"
+ category = REWARD_CATEGORY_M41A_RESKIN
+
+/datum/battlepass_reward/marine/m41a_reskin/on_claim(mob/target_mob)
+ if(!ishuman(target_mob))
+ return FALSE
+
+ var/list/valid_rifles = list(
+ /obj/item/weapon/gun/rifle/m41a,
+ /obj/item/weapon/gun/rifle/m41a/stripped,
+ /obj/item/weapon/gun/rifle/m41a/tactical,
+ /obj/item/weapon/gun/rifle/m41a/training,
+ )
+
+ var/mob/living/carbon/human/target_human = target_mob
+
+ var/obj/item/weapon/gun/rifle/m41a/gotten_rifle
+ if(target_human.l_hand && (target_human.l_hand.type in valid_rifles))
+ gotten_rifle = target_human.l_hand
+
+ else if(target_human.r_hand && (target_human.r_hand.type in valid_rifles))
+ gotten_rifle = target_human.r_hand
+
+ if(!gotten_rifle)
+ to_chat(target_mob, SPAN_WARNING("You need an M41A MK2 rifle in a hand to claim this."))
+ return
+
+ gotten_rifle.base_gun_icon = "[gotten_rifle.base_gun_icon]_golden"
+ gotten_rifle.item_state = "m41a_golden"
+ gotten_rifle.map_specific_decoration = FALSE
+ gotten_rifle.icon = 'icons/obj/items/weapons/guns/guns_by_faction/uscm.dmi'
+ gotten_rifle.item_icons = list(
+ WEAR_L_HAND = 'icons/mob/humans/onmob/items_lefthand_1.dmi',
+ WEAR_R_HAND = 'icons/mob/humans/onmob/items_righthand_1.dmi',
+ WEAR_J_STORE = 'icons/mob/humans/onmob/suit_slot.dmi',
+ WEAR_BACK = 'icons/mob/humans/onmob/back.dmi',
+ )
+ gotten_rifle.item_state_slots = list(
+ WEAR_BACK = 'icons/mob/humans/onmob/back.dmi',
+ )
+ gotten_rifle.update_icon()
+ target_human.update_inv_l_hand()
+ target_human.update_inv_r_hand()
+ return TRUE
+
+
+/datum/battlepass_reward/marine/m37_reskin
+ name = "Golden M37A2"
+ icon_state = "m37"
+ category = REWARD_CATEGORY_SHOTGUN_RESKIN
+
+/datum/battlepass_reward/marine/m37_reskin/on_claim(mob/target_mob)
+ if(!ishuman(target_mob))
+ return FALSE
+
+ var/mob/living/carbon/human/target_human = target_mob
+
+ var/obj/item/weapon/gun/shotgun/pump/gotten_rifle
+ if(target_human.l_hand && (target_human.l_hand.type == /obj/item/weapon/gun/shotgun/pump))
+ gotten_rifle = target_human.l_hand
+
+ else if(target_human.r_hand && (target_human.r_hand.type == /obj/item/weapon/gun/shotgun/pump))
+ gotten_rifle = target_human.r_hand
+
+ if(!gotten_rifle)
+ to_chat(target_mob, SPAN_WARNING("You need an M37A2 shotgun in a hand to claim this."))
+ return
+
+ gotten_rifle.base_gun_icon = "[gotten_rifle.base_gun_icon]_golden"
+ gotten_rifle.item_state = "m37_golden"
+ gotten_rifle.map_specific_decoration = FALSE
+ gotten_rifle.icon = 'icons/obj/items/weapons/guns/guns_by_faction/uscm.dmi'
+ gotten_rifle.item_icons = list(
+ WEAR_L_HAND = 'icons/mob/humans/onmob/items_lefthand_1.dmi',
+ WEAR_R_HAND = 'icons/mob/humans/onmob/items_righthand_1.dmi',
+ WEAR_J_STORE = 'icons/mob/humans/onmob/suit_slot.dmi',
+ WEAR_BACK = 'icons/mob/humans/onmob/back.dmi',
+ )
+ gotten_rifle.item_state_slots = list(
+ WEAR_BACK = 'icons/mob/humans/onmob/back.dmi',
+ )
+ gotten_rifle.update_icon()
+ target_human.update_inv_l_hand()
+ target_human.update_inv_r_hand()
+ return TRUE
diff --git a/code/modules/battlepass/rewards/premium_rewards.dm b/code/modules/battlepass/rewards/premium_rewards.dm
new file mode 100644
index 000000000000..a2883e99b23e
--- /dev/null
+++ b/code/modules/battlepass/rewards/premium_rewards.dm
@@ -0,0 +1,69 @@
+/datum/battlepass_reward/spec_token
+ name = "Specialist Token"
+ icon = 'icons/obj/items/items.dmi'
+ icon_state = "coin_diamond"
+
+/datum/battlepass_reward/heap_mag
+ name = "HEAP M41A Mag"
+ icon_state = "heap_mag"
+
+/datum/battlepass_reward/cat_ears
+ name = "Cat Ears"
+ icon_state = "cat_ears"
+
+/datum/battlepass_reward/damage_boost
+ name = "Damage Increase"
+ icon_state = "damage_boost"
+ lifeform_type = "All"
+
+/datum/battlepass_reward/extra_health
+ name = "25% Health Increase"
+ icon_state = "extra_health"
+ lifeform_type = "All"
+
+/datum/battlepass_reward/stims
+ name = "NST5MST5 Stim"
+ icon_state = "stims"
+
+/datum/battlepass_reward/hoverpack
+ name = "Hoverpack"
+ icon = 'icons/obj/items/devices.dmi'
+ icon_state = "hoverpack"
+
+/datum/battlepass_reward/predalien_larva
+ name = "Predalien Larva"
+ icon_state = "predalien_larva"
+ lifeform_type = "Xeno"
+
+/datum/battlepass_reward/hugger_skin
+ name = "Facehugger Skin"
+ icon_state = "cs_source"
+ lifeform_type = "Xeno"
+
+/datum/battlepass_reward/navy_l42a
+ name = "Navy L42A"
+ icon_state = "navy_l42a"
+
+/datum/battlepass_reward/chrome_xm88
+ name = "Chrome XM88"
+ icon_state = "chrome_xm88"
+
+/datum/battlepass_reward/custom_m39
+ name = "Custom M39"
+ icon_state = "custom_m39"
+
+/datum/battlepass_reward/arctic_m41a
+ name = "Arctic M41A"
+ icon_state = "arctic_m41a"
+
+/datum/battlepass_reward/furnished_m37
+ name = "Mangrove M37A2"
+ icon_state = "furnished_shotgun"
+
+/datum/battlepass_reward/bow
+ name = "Low-Res Bow"
+ icon_state = "bow"
+
+/datum/battlepass_reward/swamp_m41a
+ name = "Swamp M41A"
+ icon_state = "swamp_m41a"
diff --git a/code/modules/battlepass/rewards/sprites/armor.dmi b/code/modules/battlepass/rewards/sprites/armor.dmi
new file mode 100644
index 000000000000..983494feffd8
Binary files /dev/null and b/code/modules/battlepass/rewards/sprites/armor.dmi differ
diff --git a/code/modules/battlepass/rewards/sprites/armorobj.dmi b/code/modules/battlepass/rewards/sprites/armorobj.dmi
new file mode 100644
index 000000000000..cf1ca3985e83
Binary files /dev/null and b/code/modules/battlepass/rewards/sprites/armorobj.dmi differ
diff --git a/code/modules/battlepass/rewards/sprites/battlepass.dmi b/code/modules/battlepass/rewards/sprites/battlepass.dmi
new file mode 100644
index 000000000000..1d5da686f4df
Binary files /dev/null and b/code/modules/battlepass/rewards/sprites/battlepass.dmi differ
diff --git a/code/modules/battlepass/rewards/sprites/halo.dmi b/code/modules/battlepass/rewards/sprites/halo.dmi
new file mode 100644
index 000000000000..a7420d2b6073
Binary files /dev/null and b/code/modules/battlepass/rewards/sprites/halo.dmi differ
diff --git a/code/modules/battlepass/rewards/sprites/halo_red.dmi b/code/modules/battlepass/rewards/sprites/halo_red.dmi
new file mode 100644
index 000000000000..f070a7798b2c
Binary files /dev/null and b/code/modules/battlepass/rewards/sprites/halo_red.dmi differ
diff --git a/code/modules/battlepass/rewards/sprites/helmetfire.dmi b/code/modules/battlepass/rewards/sprites/helmetfire.dmi
new file mode 100644
index 000000000000..692fc685b937
Binary files /dev/null and b/code/modules/battlepass/rewards/sprites/helmetfire.dmi differ
diff --git a/code/modules/battlepass/rewards/sprites/toy.dmi b/code/modules/battlepass/rewards/sprites/toy.dmi
new file mode 100644
index 000000000000..b84c6b13ccd7
Binary files /dev/null and b/code/modules/battlepass/rewards/sprites/toy.dmi differ
diff --git a/code/modules/battlepass/rewards/xeno_rewards.dm b/code/modules/battlepass/rewards/xeno_rewards.dm
new file mode 100644
index 000000000000..bd09d556da9d
--- /dev/null
+++ b/code/modules/battlepass/rewards/xeno_rewards.dm
@@ -0,0 +1,50 @@
+/datum/battlepass_reward/xeno
+ lifeform_type = "Xeno"
+
+/datum/battlepass_reward/xeno/can_claim(mob/target_mob)
+ . = ..()
+ if(!.)
+ return .
+
+ if(!isxeno(target_mob))
+ return FALSE
+
+ return TRUE
+
+/mob/living/carbon/xenomorph
+ var/has_halo = FALSE as num
+
+/mob/living/carbon/xenomorph/proc/get_halo_iconname()
+ return lowertext(caste_type)
+
+/mob/living/carbon/xenomorph/praetorian/get_halo_iconname()
+ if(!strain)
+ return "basePrae"
+
+ switch(strain.type)
+ if(/datum/xeno_strain/vanguard)
+ return "vanguardPrae"
+ if(/datum/xeno_strain/dancer)
+ return "dancerPrae"
+ if(/datum/xeno_strain/warden)
+ return "wardenPrae"
+ if(/datum/xeno_strain/oppressor)
+ return "oppPrae"
+
+/datum/battlepass_reward/xeno/halo
+ name = "Golden Halo"
+ icon_state = "golden_halo"
+ category = REWARD_CATEGORY_OVERLAY
+
+/datum/battlepass_reward/xeno/halo/on_claim(mob/living/carbon/xenomorph/target_mob)
+ target_mob.create_halo()
+ return TRUE
+
+/datum/battlepass_reward/xeno/evil_halo
+ name = "Red Halo"
+ icon_state = "red_halo"
+ category = REWARD_CATEGORY_OVERLAY
+
+/datum/battlepass_reward/xeno/evil_halo/on_claim(mob/living/carbon/xenomorph/target_mob)
+ target_mob.create_evil_halo()
+ return TRUE
diff --git a/code/modules/clothing/head/helmet.dm b/code/modules/clothing/head/helmet.dm
index 26c92f632ee2..b78e69ce1be9 100644
--- a/code/modules/clothing/head/helmet.dm
+++ b/code/modules/clothing/head/helmet.dm
@@ -557,6 +557,7 @@ GLOBAL_LIST_INIT(allowed_helmet_items, list(
// the human sprite is the only thing that reliably renders things, so
// we have to add overlays to that.
helmet_overlays = list() // Rebuild our list every time
+ overlays.Cut()
if(pockets && pockets.contents.len && (flags_marine_helmet & HELMET_GARB_OVERLAY))
var/list/above_band_layer = list()
var/list/below_band_layer = list()
@@ -584,6 +585,9 @@ GLOBAL_LIST_INIT(allowed_helmet_items, list(
if(active_visor)
helmet_overlays += active_visor.helmet_overlay
+ if(helmet_fire_overlay_icon)
+ overlays += helmet_fire_overlay_icon
+
if(ismob(loc))
var/mob/M = loc
M.update_inv_head()
diff --git a/code/modules/clothing/shoes/marine_shoes.dm b/code/modules/clothing/shoes/marine_shoes.dm
index 7855075c2fb4..9fb501f844c9 100644
--- a/code/modules/clothing/shoes/marine_shoes.dm
+++ b/code/modules/clothing/shoes/marine_shoes.dm
@@ -28,12 +28,17 @@
/obj/item/weapon/straight_razor,
)
drop_sound = "armorequip"
+ var/base_icon_state
+
+/obj/item/clothing/shoes/marine/Initialize(mapload, ...)
+ base_icon_state = initial(icon_state)
+ . = ..()
/obj/item/clothing/shoes/marine/update_icon()
if(stored_item)
- icon_state = "[initial(icon_state)]-1"
+ icon_state = "[base_icon_state]-1"
else
- icon_state = initial(icon_state)
+ icon_state = base_icon_state
/obj/item/clothing/shoes/marine/knife
spawn_item_type = /obj/item/attachable/bayonet
diff --git a/code/modules/cm_aliens/structures/special/pylon_core.dm b/code/modules/cm_aliens/structures/special/pylon_core.dm
index add9646c56ac..1cafab0655dc 100644
--- a/code/modules/cm_aliens/structures/special/pylon_core.dm
+++ b/code/modules/cm_aliens/structures/special/pylon_core.dm
@@ -309,6 +309,7 @@
if(new_xeno.client)
if(new_xeno.client.prefs.toggles_flashing & FLASH_POOLSPAWN)
window_flash(new_xeno.client)
+ SSbattlepass.xeno_battlepass_earners |= new_xeno.client.ckey
linked_hive.stored_larva--
linked_hive.hive_ui.update_burrowed_larva()
diff --git a/code/modules/mob/living/carbon/human/update_icons.dm b/code/modules/mob/living/carbon/human/update_icons.dm
index 9a0cd177e885..b2bc6e87b6bf 100644
--- a/code/modules/mob/living/carbon/human/update_icons.dm
+++ b/code/modules/mob/living/carbon/human/update_icons.dm
@@ -447,6 +447,8 @@ Applied by gun suicide and high impact bullet executions, removed by rejuvenate,
for(var/i in HEAD_GARB_LAYER to (HEAD_GARB_LAYER + MAX_HEAD_GARB_ITEMS - 1))
remove_overlay(i)
+ remove_overlay(MAX_HEAD_GARB_ITEMS + 1)
+
if(head)
if(client && hud_used && hud_used.hud_shown && hud_used.inventory_shown && hud_used.ui_datum)
@@ -484,9 +486,14 @@ Applied by gun suicide and high impact bullet executions, removed by rejuvenate,
for(var/i in num_helmet_overlays+1 to MAX_HEAD_GARB_ITEMS)
overlays_standing[HEAD_GARB_LAYER + (i-1)] = null
+ if(marine_helmet.helmet_fire_overlay_mob_icon)
+ overlays_standing[MAX_HEAD_GARB_ITEMS + 1] = marine_helmet.helmet_fire_overlay_mob_icon
+
for(var/i in HEAD_GARB_LAYER to (HEAD_GARB_LAYER + MAX_HEAD_GARB_ITEMS - 1))
apply_overlay(i)
+ apply_overlay(MAX_HEAD_GARB_ITEMS + 1)
+
#undef MAX_HEAD_GARB_ITEMS
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/facehugger/facehugger_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/facehugger/facehugger_powers.dm
index 054762b3c7d4..d14ac359913e 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/facehugger/facehugger_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/facehugger/facehugger_powers.dm
@@ -16,9 +16,12 @@
var/key_name = key_name(facehugger)
var/did_hug = FALSE
+ var/client/hugging_client = facehugger.client
if(facehugger.pounce_distance <= 1 && can_hug(L, facehugger.hivenumber))
did_hug = facehugger.handle_hug(L)
log_attack("[key_name] [did_hug ? "successfully hugged" : "tried to hug"] [key_name(L)] (Pounce Distance: [facehugger.pounce_distance]) at [get_location_in_text(L)]")
+ if(did_hug && hugging_client)
+ SEND_SIGNAL(hugging_client.mob, COMSIG_XENO_FACEHUGGED_HUMAN) //handle_hug deletes the hugger
/datum/action/xeno_action/activable/pounce/facehugger/use_ability()
for(var/obj/structure/machinery/door/airlock/current_airlock in get_turf(owner))
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm
index 05ab5d00a743..9dae27709dec 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm
@@ -78,7 +78,7 @@
playsound(xeno.loc, "alien_resin_build", 25)
apply_cooldown()
- SEND_SIGNAL(xeno, COMSIG_XENO_PLANT_RESIN_NODE)
+ SEND_SIGNAL(xeno, COMSIG_XENO_PLANT_RESIN_NODE, xeno)
return ..()
/mob/living/carbon/xenomorph/lay_down()
diff --git a/code/modules/mob/living/carbon/xenomorph/hive_status.dm b/code/modules/mob/living/carbon/xenomorph/hive_status.dm
index 22b061715892..3630f178a35b 100644
--- a/code/modules/mob/living/carbon/xenomorph/hive_status.dm
+++ b/code/modules/mob/living/carbon/xenomorph/hive_status.dm
@@ -791,6 +791,7 @@
if(new_xeno.client)
if(new_xeno.client?.prefs?.toggles_flashing & FLASH_POOLSPAWN)
window_flash(new_xeno.client)
+ SSbattlepass.xeno_battlepass_earners |= new_xeno
stored_larva--
hive_ui.update_burrowed_larva()
diff --git a/code/modules/mob/living/carbon/xenomorph/login.dm b/code/modules/mob/living/carbon/xenomorph/login.dm
index e381aea4015e..8610f31ec091 100644
--- a/code/modules/mob/living/carbon/xenomorph/login.dm
+++ b/code/modules/mob/living/carbon/xenomorph/login.dm
@@ -4,5 +4,6 @@
set_lighting_alpha_from_prefs(client)
if(client.player_data)
generate_name()
+ SSbattlepass.xeno_battlepass_earners |= client.ckey
if(SSticker.mode)
SSticker.mode.xenomorphs |= mind
diff --git a/code/modules/mob/living/carbon/xenomorph/strains/castes/drone/gardener.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/drone/gardener.dm
index d54d268f12d9..803db9424440 100644
--- a/code/modules/mob/living/carbon/xenomorph/strains/castes/drone/gardener.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/drone/gardener.dm
@@ -104,6 +104,7 @@
SPAN_XENONOTICE("We secrete a portion of our vital fluids and shape them into a fruit!"), null, 5)
var/obj/effect/alien/resin/fruit/fruit = new xeno.selected_fruit(target_weeds.loc, target_weeds, xeno)
+ SEND_SIGNAL(xeno, COMSIG_XENO_PLANTED_FRUIT, xeno)
if(!fruit)
to_chat(xeno, SPAN_XENOHIGHDANGER("Couldn't find the fruit to place! Contact a coder!"))
return
diff --git a/code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/berserker.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/berserker.dm
index c12324aa5b2a..6a528c158d0c 100644
--- a/code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/berserker.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/ravager/berserker.dm
@@ -71,6 +71,7 @@
bound_xeno.add_filter("berserker_rage", 1, list("type" = "outline", "color" = "#000000ff", "size" = 1))
rage_lock()
to_chat(bound_xeno, SPAN_XENOHIGHDANGER("We feel a euphoric rush as we reach max rage! We are LOCKED at max Rage!"))
+ SEND_SIGNAL(bound_xeno, COMSIG_XENO_RAGE_MAX)
// HP vamp
bound_xeno.gain_health((0.05*rage + hp_vamp_ratio)*((bound_xeno.melee_damage_upper - bound_xeno.melee_damage_lower)/2 + bound_xeno.melee_damage_lower))
diff --git a/code/modules/mob/living/carbon/xenomorph/strains/castes/runner/acid.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/runner/acid.dm
index 7b9bafadeb7b..a43c018c0841 100644
--- a/code/modules/mob/living/carbon/xenomorph/strains/castes/runner/acid.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/runner/acid.dm
@@ -119,6 +119,8 @@
var/acid_range = acid_amount / caboom_acid_ratio
var/max_burn_damage = acid_amount / caboom_burn_damage_ratio
var/burn_range = acid_amount / caboom_burn_range_ratio
+ if(acid_amount >= max_acid)
+ SEND_SIGNAL(bound_xeno, COMSIG_XENO_FTH_MAX_ACID)
for(var/barricades in view(bound_xeno, acid_range))
if(istype(barricades, /obj/structure/barricade))
diff --git a/code/modules/mob/living/carbon/xenomorph/update_icons.dm b/code/modules/mob/living/carbon/xenomorph/update_icons.dm
index 635bdf856208..bc9fb3b7a74e 100644
--- a/code/modules/mob/living/carbon/xenomorph/update_icons.dm
+++ b/code/modules/mob/living/carbon/xenomorph/update_icons.dm
@@ -2,6 +2,7 @@
//Abby
//Xeno Overlays Indexes//////////
+#define X_HALO_LAYER 11
#define X_BACK_LAYER 10
#define X_HEAD_LAYER 9
#define X_SUIT_LAYER 8
@@ -12,7 +13,7 @@
#define X_TARGETED_LAYER 3
#define X_LEGCUFF_LAYER 2
#define X_FIRE_LAYER 1
-#define X_TOTAL_LAYERS 10
+#define X_TOTAL_LAYERS 11
/////////////////////////////////
@@ -250,6 +251,22 @@
apply_overlay(X_SUIT_LAYER)
addtimer(CALLBACK(src, PROC_REF(remove_overlay), X_SUIT_LAYER), 20)
+/mob/living/carbon/xenomorph/proc/create_halo()
+ if(has_halo)
+ return
+
+ overlays_standing[X_HALO_LAYER] = image("icon" = 'code/modules/battlepass/rewards/sprites/halo.dmi', "icon_state" = get_halo_iconname())
+ apply_overlay(X_HALO_LAYER)
+ has_halo = TRUE
+
+/mob/living/carbon/xenomorph/proc/create_evil_halo()
+ if(has_halo)
+ return
+
+ overlays_standing[X_HALO_LAYER] = image("icon" = 'code/modules/battlepass/rewards/sprites/halo_red.dmi', "icon_state" = get_halo_iconname())
+ apply_overlay(X_HALO_LAYER)
+ has_halo = TRUE
+
/mob/living/carbon/xenomorph/proc/create_custom_empower(icolor, ialpha = 255, small_xeno = FALSE)
remove_suit_layer()
@@ -351,3 +368,4 @@
#undef X_R_HAND_LAYER
#undef X_LEGCUFF_LAYER
#undef X_FIRE_LAYER
+#undef X_HALO_LAYER
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index 7917394df830..fb6d9bc31377 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -43,6 +43,7 @@
output +="
[(client.prefs && client.prefs.real_name) ? client.prefs.real_name : client.key]"
output +="
[xeno_text]"
output += "