diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm
index e74803cccda9..701ce925344c 100644
--- a/code/__DEFINES/sound.dm
+++ b/code/__DEFINES/sound.dm
@@ -17,6 +17,7 @@
#define CHANNEL_MOB_SOUNDS 1009
#define CHANNEL_Z 1008
#define CHANNEL_WALKMAN 1007 //monkestation edit
+#define CHANNEL_MASTER_VOLUME 1006
///Default range of a sound.
#define SOUND_RANGE 17
diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index 07230479d473..32a0c53c44d5 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -30,6 +30,8 @@
#define IGNORE_STASIS (1<<1)
/// If the incapacitated status effect will ignore a mob being agressively grabbed
#define IGNORE_GRAB (1<<2)
+/// If the incapacited status effect will ignore a mob in cirt
+#define IGNORE_CRIT (1<<3)
// Grouped effect sources, see also code/__DEFINES/traits.dm
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index 31168fb1657e..e80cb4bf3e93 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -18,15 +18,32 @@
// DOES NOT EFFECT THE BASE 1 DECISECOND DELAY OF NEXT_CLICK
/mob/proc/changeNext_move(num)
- next_move = world.time + ((num+next_move_adjust)*next_move_modifier)
+ var/stat_multi = 1
+ switch(stat)
+ if(SOFT_CRIT)
+ stat_multi = 8
+ if(HARD_CRIT)
+ stat_multi = 16
+ else
+ stat_multi = 1
+ next_move = world.time + ((num+next_move_adjust) * next_move_modifier * stat_multi)
/mob/living/changeNext_move(num)
var/mod = next_move_modifier
var/adj = next_move_adjust
+ var/stat_multi = 1
+ switch(stat)
+ if(SOFT_CRIT)
+ stat_multi = 4
+ if(HARD_CRIT)
+ stat_multi = 8
+ else
+ stat_multi = 1
+
for(var/datum/status_effect/effect as anything in status_effects)
mod *= effect.nextmove_modifier()
adj += effect.nextmove_adjust()
- next_move = world.time + ((num + adj)*mod)
+ next_move = world.time + ((num + adj)*mod * stat_multi)
/**
* Before anything else, defer these calls to a per-mobtype handler. This allows us to
@@ -106,7 +123,7 @@
CtrlClickOn(A)
return
- if(incapacitated(IGNORE_RESTRAINTS|IGNORE_STASIS))
+ if(incapacitated(IGNORE_RESTRAINTS|IGNORE_STASIS|IGNORE_CRIT))
return
face_atom(A)
@@ -117,7 +134,7 @@
if(!LAZYACCESS(modifiers, "catcher") && A.IsObscured())
return
- if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED))
+ if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED) && !((stat >= SOFT_CRIT && (stat != DEAD && stat != UNCONSCIOUS))))
changeNext_move(CLICK_CD_HANDCUFFED) //Doing shit in cuffs shall be vey slow
UnarmedAttack(A, FALSE)
return
diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index aa08d7889fa8..81b4d5179209 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -16,7 +16,7 @@
Otherwise pretty standard.
*/
/mob/living/carbon/human/UnarmedAttack(atom/A, proximity_flag)
- if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED))
+ if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED) && stat < SOFT_CRIT)
if(src == A)
check_self_for_injuries()
return
diff --git a/code/controllers/subsystem/ambience.dm b/code/controllers/subsystem/ambience.dm
index e138c2d6048c..75d68b1651d5 100644
--- a/code/controllers/subsystem/ambience.dm
+++ b/code/controllers/subsystem/ambience.dm
@@ -51,6 +51,10 @@ SUBSYSTEM_DEF(ambience)
///Attempts to play an ambient sound to a mob, returning the cooldown in deciseconds
/area/proc/play_ambience(mob/M, sound/override_sound, volume = 27)
var/sound/new_sound = override_sound || pick(ambientsounds)
+ if(M.client?.prefs.channel_volume)
+ volume *= M.client.prefs.channel_volume["[CHANNEL_MASTER_VOLUME]"] * 0.01
+ volume *= M.client.prefs.channel_volume["[CHANNEL_AMBIENCE]"] * 0.01
+
new_sound = sound(new_sound, repeat = 0, wait = 0, volume = volume, channel = CHANNEL_AMBIENCE)
SEND_SOUND(M, new_sound)
diff --git a/code/controllers/subsystem/processing/fastprocess.dm b/code/controllers/subsystem/processing/fastprocess.dm
index 1b30ca44c240..ce1617253ed9 100644
--- a/code/controllers/subsystem/processing/fastprocess.dm
+++ b/code/controllers/subsystem/processing/fastprocess.dm
@@ -2,3 +2,9 @@ PROCESSING_SUBSYSTEM_DEF(fastprocess)
name = "Fast Processing"
wait = 0.2 SECONDS
stat_tag = "FP"
+
+PROCESSING_SUBSYSTEM_DEF(actualfastprocess)
+ name = "Actual Fast Processing"
+ wait = 0.1 SECONDS
+ priority = FIRE_PRIORITY_TICKER
+ stat_tag = "AFP"
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index c94405a1afa3..1a7196a9eb4c 100755
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -282,7 +282,12 @@ SUBSYSTEM_DEF(ticker)
INVOKE_ASYNC(SSdbcore, TYPE_PROC_REF(/datum/controller/subsystem/dbcore,SetRoundStart))
to_chat(world, span_notice("Welcome to [station_name()], enjoy your stay!"))
- SEND_SOUND(world, sound(SSstation.announcer.get_rand_welcome_sound()))
+
+ for(var/mob/M as anything in GLOB.player_list)
+ if(!M.client)
+ SEND_SOUND(M, sound(SSstation.announcer.get_rand_welcome_sound(), volume = 100))
+ else if("[CHANNEL_VOX]" in M.client.prefs.channel_volume)
+ SEND_SOUND(M, sound(SSstation.announcer.get_rand_welcome_sound(), volume = M.client.prefs.channel_volume["[CHANNEL_VOX]"] * (M.client.prefs.channel_volume["[CHANNEL_MASTER_VOLUME]"] * 0.01)))
current_state = GAME_STATE_PLAYING
Master.SetRunLevel(RUNLEVEL_GAME)
diff --git a/code/datums/announcers/default_announcer.dm b/code/datums/announcers/default_announcer.dm
index 9db822e02fef..830471615cbb 100644
--- a/code/datums/announcers/default_announcer.dm
+++ b/code/datums/announcers/default_announcer.dm
@@ -1,5 +1,7 @@
/datum/centcom_announcer/default
- welcome_sounds = list('sound/ai/default/welcome.ogg')
+ welcome_sounds = list('monkestation/sound/ai/duke/welcome/bonus1.ogg',
+ 'monkestation/sound/ai/duke/welcome/welcome1.ogg',
+ 'monkestation/sound/ai/duke/welcome/welcome2.ogg')
alert_sounds = list('sound/ai/default/attention.ogg')
command_report_sounds = list('sound/ai/default/commandreport.ogg')
event_sounds = list(ANNOUNCER_AIMALF = 'sound/ai/default/aimalf.ogg',
diff --git a/code/datums/emotes.dm b/code/datums/emotes.dm
index 2151a384f62f..226253183825 100644
--- a/code/datums/emotes.dm
+++ b/code/datums/emotes.dm
@@ -101,7 +101,7 @@
var/tmp_sound = get_sound(user)
if(tmp_sound && should_play_sound(user, intentional) && !TIMER_COOLDOWN_CHECK(user, type))
TIMER_COOLDOWN_START(user, type, audio_cooldown)
- playsound(user, tmp_sound, 50, vary)
+ playsound(user, tmp_sound, 50, vary, mixer_channel = CHANNEL_MOB_SOUNDS)
var/user_turf = get_turf(user)
if (user.client)
diff --git a/code/datums/storage/storage.dm b/code/datums/storage/storage.dm
index ce0175849090..ace95c63b9a1 100644
--- a/code/datums/storage/storage.dm
+++ b/code/datums/storage/storage.dm
@@ -983,7 +983,7 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches)
resolve_parent.balloon_alert(to_show, "can't reach!")
return FALSE
- if(!isliving(to_show) || to_show.incapacitated())
+ if(!isliving(to_show) || to_show.incapacitated(IGNORE_CRIT))
return FALSE
if(locked)
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index c2ee234d5857..6373178fb225 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -1589,6 +1589,11 @@
pulling.remove_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), CHOKEHOLD_TRAIT)
if(. >= GRAB_NECK) // Previous state was a a neck-grab or higher.
REMOVE_TRAIT(pulling, TRAIT_FLOORED, CHOKEHOLD_TRAIT)
+ if(ismob(src))
+ var/mob/grabbed = src
+ if(grabbed.stat == SOFT_CRIT || grabbed.stat == HARD_CRIT)
+ pulling.add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), CHOKEHOLD_TRAIT)
+
if(GRAB_AGGRESSIVE)
if(. >= GRAB_NECK) // Grab got downgraded.
REMOVE_TRAIT(pulling, TRAIT_FLOORED, CHOKEHOLD_TRAIT)
diff --git a/code/game/sound.dm b/code/game/sound.dm
index 7e421372cc90..b14a9ee2155b 100644
--- a/code/game/sound.dm
+++ b/code/game/sound.dm
@@ -1,4 +1,5 @@
GLOBAL_LIST_INIT(used_sound_channels, list(
+ CHANNEL_MASTER_VOLUME,
CHANNEL_LOBBYMUSIC,
CHANNEL_ADMIN,
CHANNEL_VOX,
@@ -145,6 +146,8 @@ GLOBAL_LIST_INIT(proxy_sound_channels, list(
sound_to_use.wait = 0 //No queue
sound_to_use.channel = channel || SSsounds.random_available_channel()
sound_to_use.volume = vol
+ if("[CHANNEL_MASTER_VOLUME]" in client?.prefs?.channel_volume)
+ sound_to_use.volume *= client.prefs.channel_volume["[CHANNEL_MASTER_VOLUME]"] * 0.01
if(vary)
if(frequency)
@@ -246,6 +249,7 @@ GLOBAL_LIST_INIT(proxy_sound_channels, list(
if("[CHANNEL_LOBBYMUSIC]" in prefs.channel_volume)
if(prefs.channel_volume["[CHANNEL_LOBBYMUSIC]"] != 0)
vol *= prefs.channel_volume["[CHANNEL_LOBBYMUSIC]"] * 0.01
+ vol *= prefs.channel_volume["[CHANNEL_MASTER_VOLUME]"] * 0.01
if((prefs && (!prefs.read_preference(/datum/preference/toggle/sound_lobby))) || CONFIG_GET(flag/disallow_title_music))
return
diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm
index bbdb6bfbe22f..989e05caf600 100644
--- a/code/modules/admin/holder2.dm
+++ b/code/modules/admin/holder2.dm
@@ -157,6 +157,8 @@ GLOBAL_PROTECT(href_token)
owner.init_verbs() //re-initialize the verb list
owner.update_special_keybinds()
GLOB.admins |= client
+ if(!owner.mentor_datum)
+ owner.mentor_datum_set()
try_give_profiling()
@@ -170,6 +172,9 @@ GLOBAL_PROTECT(href_token)
GLOB.admins -= owner
owner.remove_admin_verbs()
owner.holder = null
+ GLOB.mentors -= owner
+ owner.mentor_datum.owner = null
+ owner.mentor_datum = null
owner = null
/// Returns the feedback forum thread for the admin holder's owner, as according to DB.
diff --git a/code/modules/escape_menu/home_page.dm b/code/modules/escape_menu/home_page.dm
index ad0febfcb20c..8ab6c27aa708 100644
--- a/code/modules/escape_menu/home_page.dm
+++ b/code/modules/escape_menu/home_page.dm
@@ -28,13 +28,22 @@
CALLBACK(src, PROC_REF(start_redeem)),
)
)
+ page_holder.give_screen_object(
+ new /atom/movable/screen/escape_menu/home_button(
+ null,
+ src,
+ "Open Lootbox",
+ /* offset = */ 3,
+ CALLBACK(src, PROC_REF(try_open_lootbox)),
+ )
+ )
page_holder.give_screen_object(
new /atom/movable/screen/escape_menu/home_button(
null,
src,
"Open Map",
- /* offset = */ 3,
+ /* offset = */ 4,
CALLBACK(src, PROC_REF(open_map)),
)
)
@@ -43,7 +52,7 @@
null,
src,
"Admin Help",
- /* offset = */ 4,
+ /* offset = */ 5,
)
)
@@ -52,7 +61,7 @@
null,
src,
"Leave Body",
- /* offset = */ 5,
+ /* offset = */ 6,
CALLBACK(src, PROC_REF(open_leave_body)),
)
)
@@ -63,6 +72,9 @@
/datum/escape_menu/proc/start_redeem()
client?.redeem_code()
+/datum/escape_menu/proc/try_open_lootbox()
+ client?.try_open_or_buy_lootbox()
+
/datum/escape_menu/proc/open_map()
var/redirect = ""
switch(SSmapping.config.map_name)
diff --git a/code/modules/events/brain_trauma.dm b/code/modules/events/brain_trauma.dm
index 40df052b1ecd..de94a0e9c007 100644
--- a/code/modules/events/brain_trauma.dm
+++ b/code/modules/events/brain_trauma.dm
@@ -1,7 +1,7 @@
/datum/round_event_control/brain_trauma
name = "Spontaneous Brain Trauma"
typepath = /datum/round_event/brain_trauma
- weight = 10
+ weight = 0
category = EVENT_CATEGORY_HEALTH
description = "A crewmember gains a random trauma."
min_wizard_trigger_potency = 2
diff --git a/code/modules/jobs/job_types/captain.dm b/code/modules/jobs/job_types/captain.dm
index 954d7c925e2a..c4f5d4e7e3a2 100755
--- a/code/modules/jobs/job_types/captain.dm
+++ b/code/modules/jobs/job_types/captain.dm
@@ -11,7 +11,7 @@
supervisors = "Nanotrasen officials and Space Law"
req_admin_notify = 1
minimal_player_age = 14
- exp_requirements = 180
+ exp_requirements = 1500
exp_required_type = EXP_TYPE_CREW
exp_required_type_department = EXP_TYPE_COMMAND
exp_granted_type = EXP_TYPE_CREW
diff --git a/code/modules/jobs/job_types/chief_engineer.dm b/code/modules/jobs/job_types/chief_engineer.dm
index bf511ffb4400..787db0725a75 100644
--- a/code/modules/jobs/job_types/chief_engineer.dm
+++ b/code/modules/jobs/job_types/chief_engineer.dm
@@ -11,7 +11,7 @@
supervisors = SUPERVISOR_CAPTAIN
req_admin_notify = 1
minimal_player_age = 7
- exp_requirements = 180
+ exp_requirements = 600
exp_required_type = EXP_TYPE_CREW
exp_required_type_department = EXP_TYPE_ENGINEERING
exp_granted_type = EXP_TYPE_CREW
diff --git a/code/modules/jobs/job_types/chief_medical_officer.dm b/code/modules/jobs/job_types/chief_medical_officer.dm
index 6f70c8676505..7ce73d4e7a5e 100644
--- a/code/modules/jobs/job_types/chief_medical_officer.dm
+++ b/code/modules/jobs/job_types/chief_medical_officer.dm
@@ -11,7 +11,7 @@
supervisors = SUPERVISOR_CAPTAIN
req_admin_notify = 1
minimal_player_age = 7
- exp_requirements = 180
+ exp_requirements = 300
exp_required_type = EXP_TYPE_CREW
exp_required_type_department = EXP_TYPE_MEDICAL
exp_granted_type = EXP_TYPE_CREW
diff --git a/code/modules/jobs/job_types/head_of_personnel.dm b/code/modules/jobs/job_types/head_of_personnel.dm
index 2bc31e1645f8..89c14fdfca72 100644
--- a/code/modules/jobs/job_types/head_of_personnel.dm
+++ b/code/modules/jobs/job_types/head_of_personnel.dm
@@ -11,7 +11,7 @@
supervisors = SUPERVISOR_HOP
req_admin_notify = 1
minimal_player_age = 10
- exp_requirements = 180
+ exp_requirements = 1500
exp_required_type = EXP_TYPE_CREW
exp_required_type_department = EXP_TYPE_SERVICE
exp_granted_type = EXP_TYPE_CREW
diff --git a/code/modules/jobs/job_types/head_of_security.dm b/code/modules/jobs/job_types/head_of_security.dm
index e9994ec122e9..4b0d7aaaaa39 100644
--- a/code/modules/jobs/job_types/head_of_security.dm
+++ b/code/modules/jobs/job_types/head_of_security.dm
@@ -11,7 +11,7 @@
supervisors = SUPERVISOR_CAPTAIN
req_admin_notify = 1
minimal_player_age = 14
- exp_requirements = 300
+ exp_requirements = 600
exp_required_type = EXP_TYPE_CREW
exp_required_type_department = EXP_TYPE_SECURITY
exp_granted_type = EXP_TYPE_CREW
diff --git a/code/modules/jobs/job_types/quartermaster.dm b/code/modules/jobs/job_types/quartermaster.dm
index a5d1ca7b304e..b7a8691668e2 100644
--- a/code/modules/jobs/job_types/quartermaster.dm
+++ b/code/modules/jobs/job_types/quartermaster.dm
@@ -10,6 +10,7 @@
supervisors = "the head of personnel"
minimal_player_age = 7
supervisors = SUPERVISOR_CAPTAIN
+ exp_requirements = 120
exp_required_type_department = EXP_TYPE_SUPPLY
exp_granted_type = EXP_TYPE_CREW
config_tag = "QUARTERMASTER"
diff --git a/code/modules/jobs/job_types/research_director.dm b/code/modules/jobs/job_types/research_director.dm
index 4a96bff4f791..f1a1d8135d41 100644
--- a/code/modules/jobs/job_types/research_director.dm
+++ b/code/modules/jobs/job_types/research_director.dm
@@ -13,7 +13,7 @@
req_admin_notify = 1
minimal_player_age = 7
exp_required_type_department = EXP_TYPE_SCIENCE
- exp_requirements = 180
+ exp_requirements = 900
exp_required_type = EXP_TYPE_CREW
exp_granted_type = EXP_TYPE_CREW
config_tag = "RESEARCH_DIRECTOR"
diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm
index bbf767371807..60715d8efd7b 100644
--- a/code/modules/mob/inventory.dm
+++ b/code/modules/mob/inventory.dm
@@ -187,7 +187,7 @@
return FALSE //nonliving mobs don't have hands
/mob/living/put_in_hand_check(obj/item/I)
- if(istype(I) && ((mobility_flags & MOBILITY_PICKUP) || (I.item_flags & ABSTRACT)) \
+ if(istype(I) && (((mobility_flags & MOBILITY_PICKUP) || ((stat >= SOFT_CRIT && (stat != DEAD && stat != UNCONSCIOUS)))) || (I.item_flags & ABSTRACT)) \
&& !(SEND_SIGNAL(src, COMSIG_LIVING_TRY_PUT_IN_HAND, I) & COMPONENT_LIVING_CANT_PUT_IN_HAND))
return TRUE
return FALSE
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 34155f199a1f..8f450ea48d2c 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -504,6 +504,9 @@
* * IGNORE_GRAB - mob that is agressively grabbed is not considered incapacitated
**/
/mob/living/incapacitated(flags)
+ if((flags & IGNORE_CRIT) && ((stat >= SOFT_CRIT && (stat != DEAD && stat != UNCONSCIOUS)) && !src.pulledby))
+ return FALSE
+
if(HAS_TRAIT(src, TRAIT_INCAPACITATED))
return TRUE
@@ -2117,26 +2120,26 @@ GLOBAL_LIST_EMPTY(fire_appearances)
if(CONSCIOUS)
if(. >= UNCONSCIOUS)
REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, TRAIT_KNOCKEDOUT)
- remove_traits(list(TRAIT_HANDS_BLOCKED, TRAIT_INCAPACITATED, TRAIT_FLOORED, TRAIT_CRITICAL_CONDITION), STAT_TRAIT)
+ remove_traits(list(TRAIT_HANDS_BLOCKED, TRAIT_INCAPACITATED, TRAIT_FLOORED, TRAIT_CRITICAL_CONDITION, TRAIT_POOR_AIM), STAT_TRAIT)
if(SOFT_CRIT)
if(pulledby)
ADD_TRAIT(src, TRAIT_IMMOBILIZED, PULLED_WHILE_SOFTCRIT_TRAIT) //adding trait sources should come before removing to avoid unnecessary updates
if(. >= UNCONSCIOUS)
REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, TRAIT_KNOCKEDOUT)
- ADD_TRAIT(src, TRAIT_CRITICAL_CONDITION, STAT_TRAIT)
+ add_traits(list(TRAIT_CRITICAL_CONDITION, TRAIT_POOR_AIM), STAT_TRAIT)
if(UNCONSCIOUS)
if(. != HARD_CRIT)
become_blind(UNCONSCIOUS_TRAIT)
if(health <= crit_threshold && !HAS_TRAIT(src, TRAIT_NOSOFTCRIT))
- ADD_TRAIT(src, TRAIT_CRITICAL_CONDITION, STAT_TRAIT)
+ add_traits( list(TRAIT_CRITICAL_CONDITION, TRAIT_POOR_AIM), STAT_TRAIT)
else
- REMOVE_TRAIT(src, TRAIT_CRITICAL_CONDITION, STAT_TRAIT)
+ remove_traits(list(TRAIT_CRITICAL_CONDITION, TRAIT_POOR_AIM), STAT_TRAIT)
if(HARD_CRIT)
if(. != UNCONSCIOUS)
become_blind(UNCONSCIOUS_TRAIT)
- ADD_TRAIT(src, TRAIT_CRITICAL_CONDITION, STAT_TRAIT)
+ add_traits(list(TRAIT_CRITICAL_CONDITION, TRAIT_POOR_AIM), STAT_TRAIT)
if(DEAD)
- REMOVE_TRAIT(src, TRAIT_CRITICAL_CONDITION, STAT_TRAIT)
+ remove_traits(list(TRAIT_CRITICAL_CONDITION, TRAIT_POOR_AIM), STAT_TRAIT)
remove_from_alive_mob_list()
add_to_dead_mob_list()
diff --git a/goon/icons/effects/320x320.dmi b/goon/icons/effects/320x320.dmi
new file mode 100644
index 000000000000..289584a301f9
Binary files /dev/null and b/goon/icons/effects/320x320.dmi differ
diff --git a/goon/icons/effects/particles.dmi b/goon/icons/effects/particles.dmi
new file mode 100644
index 000000000000..285cfaf7fcad
Binary files /dev/null and b/goon/icons/effects/particles.dmi differ
diff --git a/goon/icons/obj/large_storage.dmi b/goon/icons/obj/large_storage.dmi
new file mode 100644
index 000000000000..bb90d68312aa
Binary files /dev/null and b/goon/icons/obj/large_storage.dmi differ
diff --git a/goon/sounds/misc/openlootcrate.ogg b/goon/sounds/misc/openlootcrate.ogg
new file mode 100644
index 000000000000..7d197eb3c696
Binary files /dev/null and b/goon/sounds/misc/openlootcrate.ogg differ
diff --git a/goon/sounds/misc/openlootcrate2.ogg b/goon/sounds/misc/openlootcrate2.ogg
new file mode 100644
index 000000000000..fb190b610358
Binary files /dev/null and b/goon/sounds/misc/openlootcrate2.ogg differ
diff --git a/interface/skin.dmf b/interface/skin.dmf
index c6f2a29ad244..8ab3258f9a6b 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -201,15 +201,6 @@ window "infowindow"
saved-params = "is-checked"
text = "Report Issue"
command = "report-issue"
- elem "mediapanel"
- type = BROWSER
- pos = 392,25
- size = 1x1
- anchor1 = -1,-1
- anchor2 = -1,-1
- background-color = none
- is-visible = false
- saved-params = ""
window "outputwindow"
elem "outputwindow"
@@ -344,6 +335,15 @@ window "statwindow"
anchor2 = 100,100
is-visible = false
saved-params = ""
+ elem "mediapanel"
+ type = BROWSER
+ pos = 392,25
+ size = 1x1
+ anchor1 = -1,-1
+ anchor2 = -1,-1
+ background-color = none
+ is-visible = false
+ saved-params = ""
window "tgui_say"
elem "tgui_say"
diff --git a/monkestation/code/game/sound.dm b/monkestation/code/game/sound.dm
index c9309e817d9d..072ece63bd35 100644
--- a/monkestation/code/game/sound.dm
+++ b/monkestation/code/game/sound.dm
@@ -67,7 +67,7 @@
return GLOB.always_state
/datum/ui_module/volume_mixer/proc/set_channel_volume(channel, vol, mob/user)
- if(channel == CHANNEL_LOBBYMUSIC)
+ if((channel == CHANNEL_LOBBYMUSIC) || (channel == CHANNEL_MASTER_VOLUME))
if(isnewplayer(user))
user.client.media.update_volume(0.5 + (vol * 0.05))
@@ -77,6 +77,8 @@
/proc/get_channel_name(channel)
switch(channel)
+ if(CHANNEL_MASTER_VOLUME)
+ return "Master Volume"
if(CHANNEL_LOBBYMUSIC)
return "Lobby Music"
if(CHANNEL_ADMIN)
diff --git a/monkestation/code/modules/cassettes/machines/media/media_manager.dm b/monkestation/code/modules/cassettes/machines/media/media_manager.dm
index a9cd5565b9d8..3b03bbb1d155 100644
--- a/monkestation/code/modules/cassettes/machines/media/media_manager.dm
+++ b/monkestation/code/modules/cassettes/machines/media/media_manager.dm
@@ -111,7 +111,7 @@
var/client/owner // Client this is actually running in
var/forced=0 // If true, current url overrides area media sources
var/playerstyle // Choice of which player plugin to use
- var/const/WINDOW_ID = "infowindow.mediapanel" // Which elem in skin.dmf to use
+ var/const/WINDOW_ID = "statwindow.mediapanel" // Which elem in skin.dmf to use
var/balance=0 // do you know what insanity is? Value from -100 to 100 where -100 is left and 100 is right
var/signal_synced = 0 //used to check if we have our signal created
@@ -178,7 +178,7 @@
targetURL = M.media_url
targetStartTime = M.media_start_time
- targetVolume = max(0, M.volume - (dist * 0.1))
+ targetVolume = max(0, M.volume * (1 - (dist * 0.1)))
targetBalance = x_dist
//MP_DEBUG("Found audio source: [M.media_url] @ [(world.time - start_time) / 10]s.")
@@ -212,7 +212,7 @@
var/dist = get_dist(new_loc, M)
var/x_dist = -(new_loc.x - M.x) * 10
- targetVolume = max(0, M.volume - (dist * 0.1))
+ targetVolume = max(0, M.volume * (1 - (dist * 0.1)))
targetBalance = x_dist
push_volume_recalc(targetVolume, targetBalance)
diff --git a/monkestation/code/modules/client/preference_savefile.dm b/monkestation/code/modules/client/preference_savefile.dm
index 935f289a778c..fcb1b1694154 100644
--- a/monkestation/code/modules/client/preference_savefile.dm
+++ b/monkestation/code/modules/client/preference_savefile.dm
@@ -22,8 +22,17 @@
loadout = _text2path(loadout)
save_loadout[loadout] = entry
+ var/list/special_save_loadout = SANITIZE_LIST(save_data["special_loadout_list"])
+ for(var/loadout in special_save_loadout["unusual"])
+ special_save_loadout["unusual"] -= loadout
+
+ if(istext(loadout))
+ loadout = _text2num(loadout)
+ special_save_loadout["unusual"] += loadout
+
alt_job_titles = save_data["alt_job_titles"]
loadout_list = sanitize_loadout_list(save_loadout)
+ special_loadout_list = special_save_loadout
if(needs_update >= 0)
update_character_monkestation(needs_update, save_data) // needs_update == savefile_version if we need an update (positive integer)
@@ -37,6 +46,7 @@
/// Saves the modular customizations of a character on the savefile
/datum/preferences/proc/save_character_monkestation(list/save_data)
save_data["loadout_list"] = loadout_list
+ save_data["special_loadout_list"] = special_loadout_list
save_data["modular_version"] = MODULAR_SAVEFILE_VERSION_MAX
save_data["alt_job_titles"] = alt_job_titles
@@ -44,8 +54,10 @@
write_jobxp_preferences()
savefile.set_entry("channel_volume", channel_volume)
savefile.set_entry("saved_tokens", saved_tokens)
+ savefile.set_entry("extra_stat_inventory", extra_stat_inventory)
if(token_month)
savefile.set_entry("token_month", token_month)
+ savefile.set_entry("lootboxes_owned", lootboxes_owned)
/datum/preferences/proc/load_preferences_monkestation()
load_jobxp_preferences()
@@ -55,5 +67,9 @@
saved_tokens = savefile.get_entry("saved_tokens", saved_tokens)
saved_tokens = SANITIZE_LIST(saved_tokens)
+ extra_stat_inventory = savefile.get_entry("extra_stat_inventory", extra_stat_inventory)
+ extra_stat_inventory = SANITIZE_LIST(extra_stat_inventory)
+
token_month = savefile.get_entry("token_month", token_month)
+ lootboxes_owned = savefile.get_entry("lootboxes_owned", lootboxes_owned)
diff --git a/monkestation/code/modules/client/preferences.dm b/monkestation/code/modules/client/preferences.dm
index a266c0a24036..3bdc1c40ee18 100644
--- a/monkestation/code/modules/client/preferences.dm
+++ b/monkestation/code/modules/client/preferences.dm
@@ -1,7 +1,12 @@
/datum/preferences
/// Loadout prefs. Assoc list of [typepaths] to [associated list of item info].
var/list/loadout_list
-
+ ///list of specially handled loadout items as array indexes for the extra_stat_inventory
+ var/list/special_loadout_list = list(
+ "unusual" = list(),
+ "single-use" = list(),
+ "generic" = list(),
+ )
var/needs_update = TRUE
///list of all items in inventory
@@ -30,3 +35,11 @@
var/list/alt_job_titles = list()
/// the month we used our last donator token on
var/token_month = 0
+ /// these are inventory items that require external data to load correctly
+ var/list/extra_stat_inventory = list(
+ "unusual" = list(),
+ "single-use" = list(),
+ "generic" = list(),
+ )
+ ///amount of lootboxes owned
+ var/lootboxes_owned = 0
diff --git a/monkestation/code/modules/loadouts/loadout_middleware.dm b/monkestation/code/modules/loadouts/loadout_middleware.dm
index 6869478d9e61..8d1c773b241a 100644
--- a/monkestation/code/modules/loadouts/loadout_middleware.dm
+++ b/monkestation/code/modules/loadouts/loadout_middleware.dm
@@ -39,6 +39,7 @@
loadout_tabs += list(list("name" = "Toys", "title" = "Toys! ([MAX_ALLOWED_MISC_ITEMS] max)", "contents" = list_to_data(GLOB.loadout_toys)))
loadout_tabs += list(list("name" = "Other", "title" = "Backpack Items ([MAX_ALLOWED_MISC_ITEMS] max)", "contents" = list_to_data(GLOB.loadout_pocket_items)))
loadout_tabs += list(list("name" = "Effects", "title" = "Unique Effects", "contents" = list_to_data(GLOB.loadout_effects)))
+ loadout_tabs += list(list("name" = "Unusuals", "title" = "Unusual Hats", "contents" = convert_stored_unusuals_to_data()))
return list("loadout_tabs" = loadout_tabs)
@@ -49,12 +50,16 @@
var/list/all_selected_paths = list()
for(var/path in preferences.loadout_list)
all_selected_paths += path
+
+ var/list/all_selected_unusuals = list()
+ if(length(preferences.special_loadout_list["unusual"]))
+ all_selected_unusuals = preferences.special_loadout_list["unusual"]
+
data["selected_loadout"] = all_selected_paths
+ data["selected_unusuals"] = all_selected_unusuals
data["user_is_donator"] = !!(preferences.parent.patreon?.is_donator() || is_admin(preferences.parent))
data["mob_name"] = preferences.read_preference(/datum/preference/name/real_name)
data["ismoth"] = istype(preferences.parent.prefs.read_preference(/datum/preference/choiced/species), /datum/species/moth) // Moth's humanflaticcon isn't the same dimensions for some reason
- data["preivew_options"] = list(PREVIEW_PREF_JOB, PREVIEW_PREF_LOADOUT, PREVIEW_PREF_NAKED)
- data["preview_selection"] = PREVIEW_PREF_JOB
data["total_coins"] = preferences.metacoins
return data
@@ -83,6 +88,10 @@
return interacted_item
/datum/preference_middleware/loadout/proc/select_item(list/params, mob/user)
+ if(params["unusual_spawning_requirements"])
+ unusual_selection(params, user)
+ return
+
var/datum/loadout_item/interacted_item = return_item(params)
if(!interacted_item)
return
@@ -108,6 +117,20 @@
ui.send_update()
preferences.character_preview_view?.update_body()
+/datum/preference_middleware/proc/unusual_selection(list/params, mob/user)
+ if("[params["unusual_placement"]]" in preferences.special_loadout_list["unusual"])
+ preferences.special_loadout_list["unusual"] -= params["unusual_placement"]
+ preferences.save_preferences()
+ return
+
+ if(!islist(preferences.special_loadout_list["unusual"]))
+ preferences.special_loadout_list["unusual"] = list()
+
+ preferences.special_loadout_list["unusual"] += "[params["unusual_placement"]]"
+ var/datum/tgui/ui = SStgui.get_open_ui(user, preferences)
+ ui.send_update()
+ preferences.character_preview_view?.update_body()
+
/// Deselect [deselected_item].
/datum/preference_middleware/proc/deselect_item(datum/loadout_item/deselected_item, mob/user)
LAZYREMOVE(preferences.loadout_list, deselected_item.item_path)
@@ -157,6 +180,30 @@
return formatted_list
+/datum/preference_middleware/proc/convert_stored_unusuals_to_data()
+ var/list/data = preferences.extra_stat_inventory["unusual"]
+ if(!length(data))
+ return
+
+ var/list/formatted_list = new(length(data))
+
+ var/array_index = 1
+ for(var/iter as anything in data)
+ var/list/formatted_item = list()
+ formatted_item["name"] = data[array_index]["name"]
+ formatted_item["path"] = data[array_index]["unusual_type"]
+ formatted_item["unusual_placement"] = "[array_index]"
+ formatted_item["is_greyscale"] = FALSE
+ formatted_item["is_renamable"] = FALSE
+ formatted_item["is_job_restricted"] = FALSE
+ formatted_item["is_donator_only"] = FALSE
+ formatted_item["is_ckey_whitelisted"] = FALSE
+ formatted_item["unusual_spawning_requirements"] = TRUE
+
+ formatted_list[array_index++] = formatted_item
+
+ return formatted_list
+
/datum/preference_middleware/loadout/proc/set_name(list/params, mob/user)
var/datum/loadout_item/item = return_item(params)
if(!item)
@@ -251,6 +298,7 @@
/datum/preference_middleware/loadout/proc/clear_all_items()
LAZYNULL(preferences.loadout_list)
+ preferences.special_loadout_list["unusual"] = list()
preferences.character_preview_view.update_body()
/datum/preference_middleware/loadout/proc/ckey_explain(list/params, mob/user)
diff --git a/monkestation/code/modules/loadouts/loadout_outfit_helper.dm b/monkestation/code/modules/loadouts/loadout_outfit_helper.dm
index dc5eecad5abc..703955cd8e23 100644
--- a/monkestation/code/modules/loadouts/loadout_outfit_helper.dm
+++ b/monkestation/code/modules/loadouts/loadout_outfit_helper.dm
@@ -38,6 +38,7 @@
var/list/loadout_datums = loadout_list_to_datums(preference_source?.loadout_list)
+
if(override_preference == LOADOUT_OVERRIDE_CASE && !visuals_only)
var/obj/item/storage/briefcase/empty/briefcase = new(loc)
@@ -48,6 +49,15 @@
continue
new item.item_path(briefcase)
+ var/list/numbers = list()
+ for(var/num as anything in preference_source?.special_loadout_list["unusual"])
+ if(num in numbers)
+ continue
+ numbers += text2num(num)
+ var/list/data = preference_source?.extra_stat_inventory["unusual"][num]
+ var/item_path = text2path(data["unusual_type"])
+ var/obj/item/new_item = new item_path(briefcase)
+ new_item.AddComponent(/datum/component/unusual_handler, data)
briefcase.name = "[preference_source.read_preference(/datum/preference/name/real_name)]'s travel suitcase"
equipOutfit(equipped_outfit, visuals_only)
@@ -66,6 +76,16 @@
equipOutfit(equipped_outfit, visuals_only)
+
+ for(var/num as anything in preference_source?.special_loadout_list["unusual"])
+ var/list/data = preference_source?.extra_stat_inventory["unusual"][text2num(num)]
+ var/item_path = text2path(data["unusual_type"])
+ var/obj/item/new_item = new item_path
+ new_item.AddComponent(/datum/component/unusual_handler, data)
+ if(!new_item.equip_to_best_slot(src))
+ if(!put_in_hands(new_item))
+ new_item.forceMove(get_turf(src))
+
for(var/datum/loadout_item/item as anything in loadout_datums)
if(istype(item, /datum/loadout_item/effects))
continue
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/obsessed.dm b/monkestation/code/modules/storytellers/converted_events/solo/obsessed.dm
index e14da489b1e4..f2f6498560eb 100644
--- a/monkestation/code/modules/storytellers/converted_events/solo/obsessed.dm
+++ b/monkestation/code/modules/storytellers/converted_events/solo/obsessed.dm
@@ -18,7 +18,7 @@
/datum/round_event/antagonist/solo/obsessed
-/datum/round_event/antagonist/solo/clockcult/add_datum_to_mind(datum/mind/antag_mind)
+/datum/round_event/antagonist/solo/obsessed/add_datum_to_mind(datum/mind/antag_mind)
antag_mind.add_antag_datum(antag_datum)
var/mob/living/carbon/human/current = antag_mind.current
current.gain_trauma(/datum/brain_trauma/special/obsessed)
diff --git a/monkestation/code/modules/storytellers/gamemode_subsystem.dm b/monkestation/code/modules/storytellers/gamemode_subsystem.dm
index 2aa4db750c88..d615c93dcbc3 100644
--- a/monkestation/code/modules/storytellers/gamemode_subsystem.dm
+++ b/monkestation/code/modules/storytellers/gamemode_subsystem.dm
@@ -257,6 +257,8 @@ SUBSYSTEM_DEF(gamemode)
if(QDELETED(candidate) || !candidate.key || !candidate.client || (!observers && !candidate.mind))
continue
if(!observers)
+ if(!isliving(candidate))
+ continue
if(no_antags && candidate.mind.special_role)
continue
if(restricted_roles && (candidate.mind.assigned_role.title in restricted_roles))
diff --git a/monkestation/code/modules/trading/box_rolling.dm b/monkestation/code/modules/trading/box_rolling.dm
new file mode 100644
index 000000000000..53144837c7fb
--- /dev/null
+++ b/monkestation/code/modules/trading/box_rolling.dm
@@ -0,0 +1,122 @@
+/atom/movable/screen/fullscreen/lootbox_overlay
+ icon = 'goon/icons/effects/320x320.dmi'
+ icon_state = "lootb0"
+ screen_loc = "CENTER-3, CENTER-3"
+ mouse_opacity = MOUSE_OPACITY_OPAQUE
+ plane = HUD_PLANE
+ show_when_dead = TRUE
+
+/atom/movable/screen/fullscreen/lootbox_overlay/sparks
+ icon_state = "sparks"
+ layer = FULLSCREEN_LAYER + 0.2
+
+/atom/movable/screen/fullscreen/lootbox_overlay/background
+ icon_state = "background"
+ layer = FULLSCREEN_LAYER + 0.1
+
+/atom/movable/screen/fullscreen/lootbox_overlay/item_preview
+ icon_state = "nuthin" // we set this ourselves
+ layer = FULLSCREEN_LAYER + 0.3
+ screen_loc = "CENTER+1:35, CENTER+2"
+
+/atom/movable/screen/fullscreen/lootbox_overlay/duplicate
+ icon_state = "duplicate"
+ layer = FULLSCREEN_LAYER + 0.4
+ plane = ABOVE_HUD_PLANE
+
+/atom/movable/screen/fullscreen/lootbox_overlay/main
+ ///have we already opened? prevents spam clicks
+ var/opened = FALSE
+ ///are we a guarenteed roll for lootboxes.
+ var/guarentee_unusual = FALSE
+
+/atom/movable/screen/fullscreen/lootbox_overlay/main/guaranteed
+ guarentee_unusual = TRUE
+
+/atom/movable/screen/fullscreen/lootbox_overlay/main/Click(location, control, params)
+ if(opened)
+ return
+ opened = TRUE
+ playsound(usr, pick('goon/sounds/misc/openlootcrate.ogg', 'goon/sounds/misc/openlootcrate2.ogg'), 100, 0)
+ icon_state = "lootb2"
+ flick("lootb1", src)
+ addtimer(CALLBACK(src, PROC_REF(after_open), usr), 2 SECONDS)
+
+/atom/movable/screen/fullscreen/lootbox_overlay/main/proc/after_open(mob/user)
+ if(!user) // uh
+ return
+
+ //now we add
+ user.overlay_fullscreen("lb_spark", /atom/movable/screen/fullscreen/lootbox_overlay/sparks)
+ user.overlay_fullscreen("lb_bg", /atom/movable/screen/fullscreen/lootbox_overlay/background)
+ var/atom/movable/screen/fullscreen/lootbox_overlay/item_preview/preview = user.overlay_fullscreen("lb_preview", /atom/movable/screen/fullscreen/lootbox_overlay/item_preview)
+
+ var/type_rolled
+ if(!guarentee_unusual)
+ type_rolled = rand(1, 100)
+ else
+ type_rolled = 1
+
+ var/type_string
+ switch(type_rolled)
+ if(1)
+ type_string = "Unusual"
+ if(2 to 3)
+ type_string = "High Tier"
+ if(4 to 8)
+ type_string = "Medium Tier"
+ if(9 to 15)
+ type_string = "Low Tier"
+ else
+ type_string = "Loadout Item"
+
+ var/obj/item/rolled_item = return_rolled(type_string, user)
+ preview.icon_state = rolled_item.icon_state
+ preview.icon = rolled_item.icon
+ preview.appearance = rolled_item.appearance
+ preview.scale_to(10, 10)
+ user.reload_fullscreen()
+ preview.plane = ABOVE_HUD_PLANE
+
+ maptext = "[rolled_item.name]"
+ maptext_width = 360
+ maptext_x += 120 - length(rolled_item.name)
+ maptext_y += 60
+ if(user.client)
+ message_admins("[user.client.ckey] opened a lootbox and recieved [rolled_item.name]!")
+ log_game("[user.client.ckey] opened a lootbox and recieved [rolled_item.name]!")
+ preview.filters += filter(type = "drop_shadow", x = 0, y = 0, size= 5, offset = 0, color = "#F0CA85")
+ if(type_string == "Unusual")
+ to_chat(world, span_boldannounce("[user] has unboxed an [rolled_item.name]!"))
+ if(isliving(user) && !user.put_in_hands(rolled_item))
+ rolled_item.forceMove(get_turf(user))
+
+ addtimer(CALLBACK(src, PROC_REF(cleanup), user), 3 SECONDS)
+
+/atom/movable/screen/fullscreen/lootbox_overlay/main/proc/cleanup(mob/user)
+ if(!user)
+ return
+ user.clear_fullscreen("lb_spark", 1 SECONDS)
+ user.clear_fullscreen("lb_bg", 1 SECONDS)
+ user.clear_fullscreen("lb_preview", 1 SECONDS)
+ user.clear_fullscreen("lb_main", 1 SECONDS)
+ user.clear_fullscreen("lb_duplicate", 1 SECONDS)
+ qdel(src)
+
+
+/proc/testing_trigger_lootbox()
+ var/mob/user = usr
+ user.overlay_fullscreen("lb_main", /atom/movable/screen/fullscreen/lootbox_overlay/main/guaranteed)
+
+/mob/proc/trigger_lootbox_on_self()
+ src.overlay_fullscreen("lb_main", /atom/movable/screen/fullscreen/lootbox_overlay/main)
+
+/obj/item/lootbox
+ name = "lootbox"
+ icon = 'goon/icons/obj/large_storage.dmi'
+ icon_state = "attachecase-old"
+
+/obj/item/lootbox/attack_self(mob/user, modifiers)
+ . = ..()
+ user.trigger_lootbox_on_self()
+ qdel(src)
diff --git a/monkestation/code/modules/trading/icons/particles.dmi b/monkestation/code/modules/trading/icons/particles.dmi
new file mode 100644
index 000000000000..2705144d8e4f
Binary files /dev/null and b/monkestation/code/modules/trading/icons/particles.dmi differ
diff --git a/monkestation/code/modules/trading/icons/unusual_overlay.dmi b/monkestation/code/modules/trading/icons/unusual_overlay.dmi
new file mode 100644
index 000000000000..e163aa7ef88d
Binary files /dev/null and b/monkestation/code/modules/trading/icons/unusual_overlay.dmi differ
diff --git a/monkestation/code/modules/trading/lootbox_buying.dm b/monkestation/code/modules/trading/lootbox_buying.dm
new file mode 100644
index 000000000000..64ee420640bc
--- /dev/null
+++ b/monkestation/code/modules/trading/lootbox_buying.dm
@@ -0,0 +1,52 @@
+/client/proc/try_open_or_buy_lootbox()
+ if(!prefs)
+ return
+ if(!prefs.lootboxes_owned)
+ buy_lootbox()
+ if(prefs.lootboxes_owned)
+ open_lootbox()
+
+/client/proc/buy_lootbox()
+ if(!prefs)
+ return
+ if(!prefs.has_coins(5000))
+ to_chat(src, span_warning("You do not have enough Monkecoins to buy a lootbox"))
+ return
+ switch(tgui_alert(src, "Would you like to purchase a lootbox? 5K", "Buy a lootbox!", list("Yes", "No")))
+ if("Yes")
+ attempt_lootbox_buy()
+ else
+ return
+
+/client/proc/attempt_lootbox_buy()
+ if(!prefs.adjust_metacoins(ckey, -5000, donator_multipler = FALSE))
+ return
+ prefs.lootboxes_owned++
+
+/client/proc/open_lootbox()
+ message_admins("[ckey] opened a lootbox!")
+ log_game("[ckey] opened a lootbox!")
+ if(!mob)
+ return
+
+ if(isnewplayer(mob))
+ to_chat(mob, span_warning("Observe or spawn in first!"))
+ return
+
+ if(!prefs.lootboxes_owned)
+ return
+ prefs.lootboxes_owned--
+ mob.trigger_lootbox_on_self()
+
+/proc/give_lootboxes_to_randoms(amount)
+ for(var/i = 1 to amount)
+ var/mob/mob = pick(GLOB.player_list)
+ if(!mob.client)
+ continue
+ mob.client.give_lootbox(1)
+
+/client/proc/give_lootbox(amount)
+ if(!prefs)
+ return
+ prefs.lootboxes_owned += amount
+ to_chat(mob, span_notice("You have been given [amount] lootboxes! Open it using the escape menu."))
diff --git a/monkestation/code/modules/trading/lootbox_odds.dm b/monkestation/code/modules/trading/lootbox_odds.dm
new file mode 100644
index 000000000000..23103913df67
--- /dev/null
+++ b/monkestation/code/modules/trading/lootbox_odds.dm
@@ -0,0 +1,117 @@
+
+//global because its easier long term
+//also its own file to make it super easy
+//to find and adjust odds of lootbox rolls
+/proc/return_rolled(type_string, mob/user)
+ var/obj/item/temp
+ switch(type_string)
+ if("Unusual")
+ var/list/viable_hats = list(
+ /obj/item/clothing/head/caphat,
+ /obj/item/clothing/head/beanie,
+ /obj/item/clothing/head/beret,
+ )
+ viable_hats += subtypesof(/obj/item/clothing/head/hats) - typesof(/obj/item/clothing/head/hats/hos) - /obj/item/clothing/head/hats/centcom_cap - /obj/item/clothing/head/hats/hopcap - /obj/item/clothing/head/hats/centhat - /obj/item/clothing/head/hats/warden
+ viable_hats += subtypesof(/obj/item/clothing/head/costume)
+ var/path = pick(viable_hats)
+ temp = new path
+ var/list/viable_unusuals = subtypesof(/datum/component/particle_spewer) - /datum/component/particle_spewer/movement
+ var/picked_path = pick(viable_unusuals)
+ var/pulled_key = user.ckey
+ if(!pulled_key)
+ pulled_key = "MissingNo." // have fun trying to get this one lol
+ temp.AddComponent(/datum/component/unusual_handler, particle_path = picked_path, fresh_unusual = TRUE, client_ckey = pulled_key)
+
+ if(user.client?.prefs)
+ user.client.prefs.save_new_unusual(temp)
+
+ //token adding
+ if("High Tier")
+ temp = new /obj/item/coin/antagtoken
+ temp.name = "High Tier Antag Token"
+ user.client.saved_tokens.adjust_tokens(HIGH_THREAT, 1)
+ if("Medium Tier")
+ temp = new /obj/item/coin/antagtoken
+ temp.name = "Medium Tier Antag Token"
+ user.client.saved_tokens.adjust_tokens(MEDIUM_THREAT, 1)
+ if("Low Tier")
+ temp = new /obj/item/coin/antagtoken
+ temp.name = "Low Tier Antag Token"
+ user.client.saved_tokens.adjust_tokens(LOW_THREAT, 1)
+
+ if("Loadout Item")
+ var/static/list/viable_types = list()
+ if(!length(viable_types))
+ for(var/datum/loadout_item/type as anything in subtypesof(/datum/loadout_item))
+ var/datum/loadout_item/listed = new type()
+ if(!istype(listed))
+ continue
+ if(!listed.requires_purchase || listed.donator_only)
+ continue
+ if(!listed.item_path)
+ continue
+ if(length(listed.ckeywhitelist))
+ continue
+ viable_types += listed
+ var/datum/loadout_item/picked = pick(viable_types)
+ temp = new picked.item_path
+ if(picked.item_path in user.client.prefs.inventory)
+ user.client.prefs.adjust_metacoins(user.ckey, 2500, "Duplicate Loadout Item", donator_multipler = FALSE)
+ temp.color = COLOR_GRAY
+ temp.name = "Loadout Item [temp.name] (Duplicate)"
+ user.overlay_fullscreen("lb_duplicate", /atom/movable/screen/fullscreen/lootbox_overlay/duplicate)
+ else
+ picked.add_to_user(usr.client)
+ temp.name = "Loadout Item [temp.name]"
+
+ return temp
+
+
+/datum/loadout_item/proc/add_to_user(client/buyer)
+ SHOULD_CALL_PARENT(TRUE)
+ var/fail_message ="Failed to add lootbox item to database. Will reattempt until added!"
+ if(!SSdbcore.IsConnected())
+ to_chat(buyer, fail_message)
+ return FALSE
+ if(!buyer?.prefs)
+ return FALSE
+ if(!buyer.prefs.inventory[item_path])
+ buyer.prefs.inventory += item_path
+ var/datum/db_query/query_add_gear_purchase = SSdbcore.NewQuery({"
+ INSERT INTO [format_table_name("metacoin_item_purchases")] (`ckey`, `item_id`, `amount`) VALUES (:ckey, :item_id, :amount)"},
+ list("ckey" = buyer.ckey, "item_id" = item_path, "amount" = 1))
+ if(!query_add_gear_purchase.Execute())
+ to_chat(buyer, fail_message)
+ qdel(query_add_gear_purchase)
+ addtimer(CALLBACK(src, PROC_REF(add_to_user), buyer), 15 SECONDS)
+ return FALSE
+ qdel(query_add_gear_purchase)
+ else
+ buyer.prefs.inventory += item_path
+ var/datum/db_query/query_add_gear_purchase = SSdbcore.NewQuery({"
+ UPDATE [format_table_name("metacoin_item_purchases")] SET amount = :amount WHERE ckey = :ckey AND item_id = :item_id"},
+ list("ckey" = buyer.ckey, "item_id" = item_path, "amount" = 1))
+ if(!query_add_gear_purchase.Execute())
+ to_chat(buyer, fail_message)
+ qdel(query_add_gear_purchase)
+ return FALSE
+ qdel(query_add_gear_purchase)
+
+ return TRUE
+
+/proc/testing_spawn_bulk_unusuals()
+ var/turf/turf = get_turf(usr)
+
+ for(var/i = 1 to 20)
+ var/list/viable_hats = list(
+ /obj/item/clothing/head/caphat,
+ /obj/item/clothing/head/beanie,
+ /obj/item/clothing/head/beret,
+ )
+ viable_hats += subtypesof(/obj/item/clothing/head/hats) - typesof(/obj/item/clothing/head/hats/hos) - /obj/item/clothing/head/hats/centcom_cap - /obj/item/clothing/head/hats/hopcap - /obj/item/clothing/head/hats/centhat - /obj/item/clothing/head/hats/warden
+ viable_hats += subtypesof(/obj/item/clothing/head/costume) - /obj/item/clothing/head/costume/nightcap
+ var/path = pick(viable_hats)
+ var/obj/item/temp = new path(turf)
+ var/list/viable_unusuals = subtypesof(/datum/component/particle_spewer) - /datum/component/particle_spewer/movement
+ var/picked_path = pick(viable_unusuals)
+ temp.AddComponent(/datum/component/unusual_handler, particle_path = picked_path)
diff --git a/monkestation/code/modules/trading/readme.md b/monkestation/code/modules/trading/readme.md
new file mode 100644
index 000000000000..84186d0fc842
--- /dev/null
+++ b/monkestation/code/modules/trading/readme.md
@@ -0,0 +1,24 @@
+## Title:
+
+MODULE ID: TRADING
+
+### Description:
+
+Adds in auction house and surrounding systems
+
+### TG Proc Changes:
+
+### Defines:
+
+N/A
+
+### Master file additions
+
+N/A
+
+### Included files that are not contained in this module:
+
+N/A
+
+### Credits:
+Dwasint
diff --git a/monkestation/code/modules/trading/save_unusual_preference.dm b/monkestation/code/modules/trading/save_unusual_preference.dm
new file mode 100644
index 000000000000..b2818a69525a
--- /dev/null
+++ b/monkestation/code/modules/trading/save_unusual_preference.dm
@@ -0,0 +1,44 @@
+/datum/preferences/proc/save_new_unusual(obj/item/unusual)
+ var/datum/component/unusual_handler/component = unusual.GetComponent(/datum/component/unusual_handler)
+ if(!component)
+ return
+
+ var/list/data = list()
+ // These MUST be strings if you don't make them a string whatever daemon lurks inside of byond will get you.
+ data["name"] = unusual.name
+ data["type"] = "[component.particle_path]"
+ data["round"] = "[component.round_id]"
+ data["original_owner"] = component.original_owner_ckey
+ data["description"] = component.unusual_description
+ data["equipslot"] = "[component.unusual_equip_slot]"
+ data["item_overlay"] = component.unusal_overlay
+ data["unusual_type"] = "[unusual.type]"
+ data["unusual_number"] = "[component.unusual_number]"
+
+ extra_stat_inventory["unusual"] += list(data)
+ save_preferences()
+
+/datum/preferences/proc/return_unusual_data(number)
+ return extra_stat_inventory["unusual"][number]
+
+
+/datum/preferences/proc/clear_unusuals()
+ extra_stat_inventory["unusual"] = list()
+ save_preferences()
+
+/mob/proc/spawn_first_stored_unusual()
+ if(!client?.prefs)
+ return
+ var/list/data = client.prefs.return_unusual_data(1)
+ var/item_path = text2path(data["unusual_type"])
+ var/obj/item/new_item = new item_path(get_turf(src))
+
+ new_item.AddComponent(/datum/component/unusual_handler, data)
+
+/mob/proc/create_unusual()
+ if(!client?.prefs)
+ return
+ var/obj/item/clothing/head/costume/nightcap/red/created = new()
+
+ created.AddComponent(/datum/component/unusual_handler, particle_path = /datum/component/particle_spewer/fire, fresh_unusual = TRUE, client_ckey = ckey)
+ client.prefs.save_new_unusual(created)
diff --git a/monkestation/code/modules/trading/unusual_effects/_unusual_component.dm b/monkestation/code/modules/trading/unusual_effects/_unusual_component.dm
new file mode 100644
index 000000000000..c086f9421291
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/_unusual_component.dm
@@ -0,0 +1,109 @@
+GLOBAL_LIST_INIT(total_unusuals_per_type, list())
+
+
+/datum/component/unusual_handler
+ var/atom/source_object
+ ///the description added to the unusual.
+ var/unusual_description = "Not Implemented Yet Teehee"
+ ///the round the unusual was created at
+ var/round_id = 0
+ ///the particle spewer component path
+ var/particle_path = /datum/component/particle_spewer/confetti
+ /// The original owners name
+ var/original_owner_ckey = "dwasint"
+ /// the slot this item goes in used when creating the particle itself
+ var/unusual_equip_slot = ITEM_SLOT_HEAD
+ /// the icon_state of the overlay given to unusals
+ var/unusal_overlay = "none"
+ ///the unique number our unusual is in the item path
+ var/unusual_number = 0
+
+//this init is handled far differently than others. it parses data from the DB for information about the unusual itself
+//it than loads this info into the component itself, the particle_path is purely for spawning temporary ones in round
+/datum/component/unusual_handler/Initialize(list/parsed_variables = list(), particle_path = /datum/component/particle_spewer/confetti, fresh_unusual = FALSE, client_ckey = "dwasint")
+ . = ..()
+ source_object = parent
+ if(!length(GLOB.total_unusuals_per_type))
+ fetch_unusual_data()
+
+ if(!length(parsed_variables))
+ src.particle_path = particle_path
+ else
+ setup_from_list(parsed_variables)
+
+ if(fresh_unusual)
+ original_owner_ckey = client_ckey
+ round_id = text2num(GLOB.round_id)
+ GLOB.total_unusuals_per_type["[particle_path]"]++
+ unusual_number = "[GLOB.total_unusuals_per_type["[particle_path]"]]"
+
+
+ source_object.AddComponent(src.particle_path)
+
+ if(!length(parsed_variables))
+ var/datum/component/particle_spewer/created = source_object.GetComponent(/datum/component/particle_spewer)
+ unusual_description = created.unusual_description
+
+ source_object.desc += span_notice("\n Unboxed by: [original_owner_ckey]")
+ source_object.desc += span_notice("\n Unboxed on round: [round_id]")
+ source_object.desc += span_notice("\n Unusual Type: [unusual_description]")
+ source_object.desc += span_notice("\n Series Number: [unusual_number]")
+
+ if(!length(parsed_variables))
+ switch(unusual_number)
+ if(1)
+ source_object.name = span_hypnophrase("unusual [unusual_description] [source_object.name]")
+ if(2 to 5)
+ source_object.name = span_cult("unusual [unusual_description] [source_object.name]")
+ if(6 to 10)
+ source_object.name = span_clown("unusual [unusual_description] [source_object.name]")
+ if(11 to 25)
+ source_object.name = span_green("unusual [unusual_description] [source_object.name]")
+ else
+ source_object.name = "unusual [unusual_description] [source_object.name]"
+
+ RegisterSignal(source_object, COMSIG_ATOM_UPDATE_DESC, PROC_REF(append_unusual))
+ save_unusual_data()
+
+/datum/component/unusual_handler/Destroy(force, silent)
+ . = ..()
+ UnregisterSignal(source_object, COMSIG_ATOM_UPDATE_DESC)
+
+/datum/component/unusual_handler/proc/append_unusual(atom/source, updates)
+ SIGNAL_HANDLER
+ source_object.desc = initial(source_object.desc)
+ source_object.desc += span_notice("\n Unboxed by: [original_owner_ckey]")
+ source_object.desc += span_notice("\n Unboxed on: [round_id]")
+ source_object.desc += span_notice("\n Unusual Type: [unusual_description]")
+ source_object.desc += span_notice("\n Series Number: [unusual_number]")
+
+/datum/component/unusual_handler/proc/setup_from_list(list/parsed_results)
+ particle_path = text2path(parsed_results["type"])
+ round_id = text2num(parsed_results["round"])
+ original_owner_ckey = parsed_results["original_owner"]
+ unusual_description = parsed_results["description"]
+ unusual_equip_slot = text2num(parsed_results["equipslot"])
+ unusal_overlay = parsed_results["item_overlay"]
+ unusual_number = parsed_results["unusual_number"]
+ source_object.name = parsed_results["name"]
+
+/datum/component/unusual_handler/proc/fetch_unusual_data()
+ var/json_file = file("data/unusual_tracking.json")
+ if(!fexists(json_file))
+ stack_trace("We are missing the unusual JSON file, this will mess up unusual counting and unique names!")
+ var/list/json = json_decode(file2text(json_file))
+
+ if(!json)
+ return
+
+ for(var/type in json)
+ GLOB.total_unusuals_per_type[type] = json[type]
+
+/datum/component/unusual_handler/proc/save_unusual_data()
+ var/json_file = file("data/unusual_tracking.json")
+ if(!fexists(json_file))
+ stack_trace("We are missing the unusual JSON file, this will mess up unusual counting and unique names!")
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(GLOB.total_unusuals_per_type))
+
+
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/__spawning_component.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/__spawning_component.dm
new file mode 100644
index 000000000000..e59071855d02
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/__spawning_component.dm
@@ -0,0 +1,168 @@
+
+/obj/effect/abstract/particle
+ name = ""
+ plane = GAME_PLANE_FOV_HIDDEN
+ appearance_flags = RESET_ALPHA | RESET_COLOR | RESET_TRANSFORM | KEEP_APART | TILE_BOUND
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ icon = 'monkestation/code/modules/trading/icons/particles.dmi'
+ icon_state = "none"
+
+/datum/component/particle_spewer
+ var/atom/source_object
+ ///the unusual_description grabbed into the actual handler itself only needed when used as an unusual
+ var/unusual_description = "teehee"
+ //the worn mob
+ var/mob/worn_mob
+ ///the duration we last
+ var/duration = 0
+ ///the spawn intervals in game ticks
+ var/spawn_interval = 1
+ ///particles still in the process of animating
+ var/list/living_particles = list()
+ ///list of particles that finished (added only as a failsafe)
+ var/list/dead_particles = list()
+ ///x offset for source_object
+ var/offset_x = 0
+ ///y offset for source_object
+ var/offset_y = 0
+ ///the dmi location of the particle
+ var/icon_file = 'monkestation/code/modules/trading/icons/particles.dmi'
+ ///the icon_state given to the objects
+ var/particle_state = "none"
+ ///current process count
+ var/count = 0
+ ///equipped offset ie hats go to 32 if set to 32 will also reset to height changes
+ var/equipped_offset = 0
+ ///per burst spawn amount
+ var/burst_amount = 1
+ ///the actual lifetime of this component before we die [ 0 = infinite]
+ var/lifetime = 0
+ ///kept track of for removal sake
+ var/added_x = 0
+ var/added_y = 0
+ /// do we do random amounts of particle bursts?
+ var/random_bursts = FALSE
+ ///should we offset
+ var/offsets = TRUE
+ /// do we process?
+ var/processes = TRUE
+ ///the blend type we use for particles
+ var/particle_blending = BLEND_DEFAULT
+
+/datum/component/particle_spewer/Initialize(duration = 0, spawn_interval = 0, offset_x = 0, offset_y = 0, icon_file, particle_state, equipped_offset = 0, burst_amount = 0, lifetime = 0, random_bursts = 0)
+ . = ..()
+ if(icon_file)
+ src.icon_file = icon_file
+ if(particle_state)
+ src.particle_state = particle_state
+ if(offset_x)
+ src.offset_x = offset_x + rand(-8, 8)
+ if(offset_y)
+ src.offset_y = offset_y + rand(-4, 4)
+ if(spawn_interval)
+ src.spawn_interval = spawn_interval
+ if(duration)
+ src.duration = duration
+ if(equipped_offset)
+ src.equipped_offset = equipped_offset
+ if(burst_amount)
+ src.burst_amount = burst_amount
+ if(lifetime)
+ src.lifetime = lifetime
+ if(random_bursts)
+ src.random_bursts = random_bursts
+ source_object = parent
+
+ if(processes)
+ START_PROCESSING(SSactualfastprocess, src)
+ RegisterSignal(source_object, COMSIG_ITEM_EQUIPPED, PROC_REF(handle_equip_offsets))
+ RegisterSignal(source_object, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(reset_offsets))
+
+ if(lifetime)
+ addtimer(CALLBACK(src, PROC_REF(kill_it_with_fire)), lifetime)
+
+/datum/component/particle_spewer/Destroy(force, silent)
+ . = ..()
+ UnregisterSignal(source_object, list(
+ COMSIG_ITEM_EQUIPPED,
+ COMSIG_ITEM_POST_UNEQUIP,
+ ))
+
+ STOP_PROCESSING(SSactualfastprocess, src)
+ for(var/atom/listed_atom as anything in living_particles + dead_particles)
+ qdel(listed_atom)
+ living_particles = null
+ dead_particles = null
+ source_object = null
+
+/datum/component/particle_spewer/process(seconds_per_tick)
+ if(spawn_interval != 1)
+ count++
+ if(count < spawn_interval)
+ return
+ count = 0
+ spawn_particles()
+
+/datum/component/particle_spewer/proc/spawn_particles(atom/movable/mover, turf/target)
+ var/burstees = burst_amount
+ if(random_bursts)
+ burstees = rand(1, burst_amount)
+
+ for(var/i = 0 to burstees)
+ //create and assign particle its stuff
+ var/obj/effect/abstract/particle/spawned = new(get_turf(source_object))
+ if(offsets)
+ spawned.pixel_x = offset_x
+ spawned.pixel_y = offset_y
+ spawned.icon = icon_file
+ spawned.icon_state = particle_state
+ spawned.blend_mode = particle_blending
+
+ living_particles |= spawned
+
+ animate_particle(spawned)
+
+///this is the proc that gets overridden when we create new particle spewers that control its movements
+//example is animating upwards over duration and deleting
+/datum/component/particle_spewer/proc/animate_particle(obj/effect/abstract/particle/spawned)
+ animate(spawned, alpha = 75, time = duration)
+ animate(spawned, pixel_y = offset_y + 64, time = duration)
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration)
+
+/datum/component/particle_spewer/proc/delete_particle(obj/effect/abstract/particle/spawned)
+ living_particles -= spawned
+ qdel(spawned)
+
+/datum/component/particle_spewer/proc/kill_it_with_fire()
+ qdel(src)
+
+/datum/component/particle_spewer/proc/handle_equip_offsets(datum/source, mob/equipper, slot)
+ SIGNAL_HANDLER
+
+ offset_x -= added_x
+ offset_y -= added_y
+ added_x = 0
+ added_y = 0
+ worn_mob = equipper
+
+ switch(slot)
+ if(ITEM_SLOT_HEAD)
+ added_y = 16
+ else
+ added_y = 0
+ added_x = 0
+
+ offset_y += added_y
+ offset_x += added_x
+
+/datum/component/particle_spewer/proc/reset_offsets()
+ SIGNAL_HANDLER
+ offset_x -= added_x
+ offset_y -= added_y
+ added_x = 0
+ added_y = 0
+ worn_mob = null
+
+/obj/item/debug_particle_holder/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/particle_spewer, 2 SECONDS)
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/_footprint.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/_footprint.dm
new file mode 100644
index 000000000000..dec84bd40657
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/_footprint.dm
@@ -0,0 +1,29 @@
+//a unique subtype of particle spewer that only runs on equip
+/datum/component/particle_spewer/movement
+ processes = FALSE
+ var/mob/attached_signal
+
+/datum/component/particle_spewer/movement/Destroy(force, silent)
+ UnregisterSignal(source_object, COMSIG_MOVABLE_PRE_MOVE)
+ . = ..()
+ UnregisterSignal(attached_signal, COMSIG_MOVABLE_PRE_MOVE)
+ attached_signal = null
+
+/datum/component/particle_spewer/movement/Initialize(duration, spawn_interval, offset_x, offset_y, icon_file, particle_state, equipped_offset, burst_amount, lifetime, random_bursts)
+ . = ..()
+ RegisterSignal(source_object, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(spawn_particles))
+
+/datum/component/particle_spewer/movement/handle_equip_offsets(datum/source, mob/equipper, slot)
+ . = ..()
+ if(attached_signal)
+ UnregisterSignal(attached_signal, COMSIG_MOVABLE_PRE_MOVE)
+ attached_signal = null
+
+ attached_signal = equipper
+ RegisterSignal(equipper, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(spawn_particles))
+
+/datum/component/particle_spewer/movement/reset_offsets()
+ . = ..()
+ if(attached_signal)
+ UnregisterSignal(attached_signal, COMSIG_MOVABLE_PRE_MOVE)
+ attached_signal = null
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/confetti.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/confetti.dm
new file mode 100644
index 000000000000..ea7ce46d9ddc
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/confetti.dm
@@ -0,0 +1,27 @@
+/datum/component/particle_spewer/confetti
+ unusual_description = "partytime"
+ duration = 2 SECONDS
+ burst_amount = 5
+ particle_blending = BLEND_ADD
+ spawn_interval = 1 SECONDS
+
+/datum/component/particle_spewer/confetti/animate_particle(obj/effect/abstract/particle/spawned)
+ var/matrix/first = matrix()
+ var/matrix/second = matrix()
+
+ spawned.pixel_x += rand(-3,3)
+ spawned.pixel_y += rand(-3,3)
+
+ first.Turn(rand(-90, 90))
+ first.Scale(0.5,0.5)
+ second.Turn(rand(-90, 90))
+
+ spawned.color = rgb(rand(1, 255), rand(1, 255), rand(1, 255))
+
+ animate(spawned, transform = first, time = 0.4 SECONDS, pixel_y = rand(-32, 32) + spawned.pixel_y, pixel_x = rand(-32, 32) + spawned.pixel_x, easing = LINEAR_EASING)
+ animate(transform = second, time = 0.5 SECONDS, alpha = 0, pixel_y = spawned.pixel_y - 5, easing = LINEAR_EASING|EASE_OUT)
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration)
+
+/obj/item/debug_confetti/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/particle_spewer/confetti)
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/fire.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/fire.dm
new file mode 100644
index 000000000000..34f110a76ea7
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/fire.dm
@@ -0,0 +1,29 @@
+/datum/component/particle_spewer/fire
+ unusual_description = "flaming"
+ duration = 2 SECONDS
+ burst_amount = 3
+ spawn_interval = 0.2 SECONDS
+ particle_state = "1x1"
+ particle_blending = BLEND_ADD
+
+/datum/component/particle_spewer/fire/animate_particle(obj/effect/abstract/particle/spawned)
+ spawned.pixel_x += rand(-6,6)
+ spawned.pixel_y += rand(-4,4)
+
+ spawned.add_filter("outline", 1, list(type = "outline", size = 1, color = "#FF3300"))
+ spawned.add_filter("bloom", 2 , list(type = "bloom", threshold = rgb(255,128,255), size = 5, offset = 4, alpha = 255))
+
+ if(prob(35))
+ spawned.layer = ABOVE_MOB_LAYER
+
+ var/normal_x = rand(-4, 4) + spawned.pixel_x
+ var/inverse_x = 0 - normal_x
+ spawned.alpha = 130
+
+ animate(spawned, alpha = 255, time = 0.4 SECONDS, pixel_y = rand(6, 16) + spawned.pixel_y, pixel_x = normal_x, easing = LINEAR_EASING)
+ animate(time = 0.5 SECONDS, alpha = 0, inverse_x , pixel_y = rand(6, 16) + spawned.pixel_y, easing = LINEAR_EASING|EASE_OUT)
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration)
+
+/obj/item/debug_fire/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/particle_spewer/fire)
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/galaxies.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/galaxies.dm
new file mode 100644
index 000000000000..86ae92317b1a
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/galaxies.dm
@@ -0,0 +1,28 @@
+/datum/component/particle_spewer/movement/galaxies
+ unusual_description = "galactic"
+ duration = 5 SECONDS
+ spawn_interval = 0.5 SECONDS
+ burst_amount = 6
+ particle_state = "snow_small"
+
+
+/datum/component/particle_spewer/galaxies/animate_particle(obj/effect/abstract/particle/spawned)
+ var/can_be_shooting = TRUE
+ if(prob(10))
+ spawned.icon_state = "moon"
+ can_be_shooting = FALSE
+
+ if(prob(10) && can_be_shooting)
+ spawned.icon_state = "ringed_planet"
+ can_be_shooting = FALSE
+
+ spawned.pixel_x += rand(-16,16)
+ spawned.pixel_y += rand(-12,4)
+
+ if(prob(45) && can_be_shooting)
+ spawned.layer = ABOVE_MOB_LAYER
+
+ animate(spawned, alpha = 0, time = duration)
+ if(prob(33) && can_be_shooting)
+ animate(spawned, pixel_y = spawned.pixel_y + rand(-6, 6), pixel_x = spawned.pixel_x + rand(-16, 16), time = 1 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration)
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/holy_steps.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/holy_steps.dm
new file mode 100644
index 000000000000..456c920bebcf
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/holy_steps.dm
@@ -0,0 +1,25 @@
+/datum/component/particle_spewer/movement/holy_steps
+ unusual_description = "holy treads"
+ duration = 5 SECONDS
+ burst_amount = 25
+ icon_file = 'goon/icons/effects/particles.dmi'
+ particle_state = "starsmall"
+
+/datum/component/particle_spewer/movement/holy_steps/animate_particle(obj/effect/abstract/particle/spawned)
+ var/matrix/first = matrix()
+ var/matrix/second = matrix()
+
+ spawned.blend_mode = BLEND_ADD
+ spawned.pixel_x += rand(-3,3)
+ spawned.pixel_y += rand(-3,3)
+
+ first.Turn(rand(-90, 90))
+ first.Scale(0.5,0.5)
+ second.Turn(rand(-90, 90))
+
+ spawned.color = rgb(rand(1, 255), rand(1, 255), rand(1, 255))
+
+ animate(spawned, transform = first, time = 0.4 SECONDS, pixel_y = rand(-32, 32) + spawned.pixel_y, pixel_x = rand(-32, 32) + spawned.pixel_x, easing = LINEAR_EASING)
+ animate(transform = second, time = 0.5 SECONDS, pixel_y = spawned.pixel_y - 5, easing = LINEAR_EASING|EASE_OUT)
+ animate(spawned, alpha = 0, time = duration)
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration)
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/music.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/music.dm
new file mode 100644
index 000000000000..3b3280799a59
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/music.dm
@@ -0,0 +1,33 @@
+/datum/component/particle_spewer/shooting_star
+ icon_file = 'goon/icons/effects/particles.dmi'
+ particle_state = "beamed_eighth"
+
+ unusual_description = "melody"
+ duration = 2.5 SECONDS
+ burst_amount = 2
+ spawn_interval = 0.5 SECONDS
+ offsets = FALSE
+
+/datum/component/particle_spewer/shooting_star/animate_particle(obj/effect/abstract/particle/spawned)
+ var/matrix/first = matrix()
+ var/matrix/second = matrix()
+ var/matrix/default = matrix()
+
+ if(prob(30))
+ spawned.icon_state = "eighth"
+ if(prob(25))
+ spawned.icon_state = "quarter"
+
+ spawned.pixel_x += rand(-24, 24)
+ spawned.pixel_y += rand(-6, 6)
+ first.Turn(rand(-90, 90))
+ spawned.transform = first
+
+ second = first
+ second.Scale(4,4)
+ second.Turn(rand(-90, 90))
+
+ animate(spawned, transform = second, time = 1, alpha = 220)
+ animate(transform = default, time = duration + rand(-5, 5), pixel_y = spawned.pixel_y + 32, alpha = 1)
+
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration + 0.6 SECONDS)
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/shooting_stars.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/shooting_stars.dm
new file mode 100644
index 000000000000..44f2a241a8af
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/shooting_stars.dm
@@ -0,0 +1,37 @@
+/datum/component/particle_spewer/movement/shooting_star
+ icon_file = 'goon/icons/effects/particles.dmi'
+ particle_state = "starsmall"
+
+ unusual_description = "shooting star"
+ duration = 5 SECONDS
+ burst_amount = 5
+ offsets = FALSE
+ //has a chance to randomly change on animate
+ var/direction = NORTH
+
+/datum/component/particle_spewer/movement/shooting_star/spawn_particles(atom/movable/mover, turf/target)
+ . = ..()
+ var/dir = get_dir(mover, target)
+ direction = dir
+
+/datum/component/particle_spewer/movement/shooting_star/animate_particle(obj/effect/abstract/particle/spawned)
+
+ spawned.dir = direction
+ if(prob(30))
+ spawned.icon_state = "starlarge"
+ if(direction & NORTH|SOUTH)
+ spawned.pixel_x += rand(-16, 16)
+
+ if(direction & EAST|WEST)
+ spawned.pixel_y += rand(-16, 16)
+
+ switch(direction)
+ if(NORTH)
+ animate(spawned, time = rand(0.5 SECONDS, duration), pixel_y = spawned.pixel_y + 160, alpha = 25)
+ if(SOUTH)
+ animate(spawned, time = rand(0.5 SECONDS, duration), pixel_y = spawned.pixel_y - 160, alpha = 25)
+ if(EAST)
+ animate(spawned, time = rand(0.5 SECONDS, duration), pixel_x = spawned.pixel_x + 160, alpha = 25)
+ if(WEST)
+ animate(spawned, time = rand(0.5 SECONDS, duration), pixel_x = spawned.pixel_x - 160, alpha = 25)
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration)
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/skull_rain.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/skull_rain.dm
new file mode 100644
index 000000000000..a65edddd53bc
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/skull_rain.dm
@@ -0,0 +1,34 @@
+/datum/component/particle_spewer/movement/skull_rain
+ unusual_description = "spooky"
+ icon_file = 'goon/icons/effects/particles.dmi'
+ particle_state = "skull3"
+ burst_amount = 4
+ duration = 2 SECONDS
+ random_bursts = TRUE
+ spawn_interval = 0.4 SECONDS
+
+/datum/component/particle_spewer/movement/skull_rain/animate_particle(obj/effect/abstract/particle/spawned)
+ var/matrix/first = matrix(rand(1, 60), MATRIX_ROTATE)
+ var/matrix/second = matrix()
+ second.Turn(rand(-60, 60))
+
+ var/chance = rand(1, 6)
+ switch(chance)
+ if(1 to 2)
+ spawned.icon_state = "skull3"
+ if(3 to 4)
+ spawned.icon_state = "skull2"
+ if(5 to 6)
+ spawned.icon_state = "skull1"
+
+ if(prob(35))
+ spawned.layer = ABOVE_MOB_LAYER
+ spawned.pixel_x += rand(-12, 12)
+ spawned.pixel_y += rand(5, 10)
+ spawned.transform = first
+ spawned.alpha = 10
+
+ animate(spawned, transform = second, time = 20, pixel_y = rand(-16, -12), alpha = 255, easing = BOUNCE_EASING)
+ animate(time = duration, alpha = 1, easing = LINEAR_EASING)
+
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration)
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/snow.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/snow.dm
new file mode 100644
index 000000000000..41dd9aeea3cd
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/snow.dm
@@ -0,0 +1,29 @@
+/datum/component/particle_spewer/snow
+ unusual_description = "snowstorm"
+ icon_file = 'monkestation/code/modules/outdoors/icons/effects/particles/particle.dmi'
+ particle_state = "cross"
+ burst_amount = 8
+ duration = 2 SECONDS
+ random_bursts = TRUE
+ spawn_interval = 0.3 SECONDS
+
+/datum/component/particle_spewer/snow/animate_particle(obj/effect/abstract/particle/spawned)
+ var/chance = rand(1, 10)
+ switch(chance)
+ if(1 to 2)
+ spawned.icon_state = "cross"
+ if(3 to 4)
+ spawned.icon_state = "snow_2"
+ if(5 to 6)
+ spawned.icon_state = "snow_3"
+ else
+ spawned.icon_state = "snow_1"
+
+ if(prob(35))
+ spawned.layer = ABOVE_MOB_LAYER
+ spawned.pixel_x += rand(-12, 12)
+ spawned.pixel_y += rand(-5, 5)
+
+ animate(spawned, pixel_y = spawned.pixel_y - 32, time = 2 SECONDS)
+ animate(spawned, alpha = 25, time = 1.5 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration)
diff --git a/monkestation/code/modules/trading/unusual_effects/animation_housing/weh.dm b/monkestation/code/modules/trading/unusual_effects/animation_housing/weh.dm
new file mode 100644
index 000000000000..68856f587825
--- /dev/null
+++ b/monkestation/code/modules/trading/unusual_effects/animation_housing/weh.dm
@@ -0,0 +1,25 @@
+/datum/component/particle_spewer/movement/weh
+ unusual_description = "weh"
+ duration = 5 SECONDS
+ burst_amount = 3
+
+ particle_state = "map_plushie_lizard"
+ icon_file = 'icons/obj/toys/plushes.dmi'
+
+/datum/component/particle_spewer/movement/weh/animate_particle(obj/effect/abstract/particle/spawned)
+ var/matrix/first = matrix()
+ var/matrix/second = matrix()
+
+ spawned.pixel_x += rand(-3,3)
+ spawned.pixel_y += rand(-3,3)
+
+ first.Turn(rand(-90, 90))
+ first.Scale(0.5,0.5)
+ second.Turn(rand(-90, 90))
+
+ spawned.color = rgb(rand(1, 255), rand(1, 255), rand(1, 255))
+
+ animate(spawned, transform = first, time = 0.4 SECONDS, pixel_y = rand(-1, 12) + spawned.pixel_y, pixel_x = rand(-32, 32) + spawned.pixel_x, easing = JUMP_EASING)
+ animate(transform = second, time = 0.5 SECONDS, pixel_y = spawned.pixel_y - 32)
+ animate(spawned, alpha = 0, time = duration)
+ addtimer(CALLBACK(src, PROC_REF(delete_particle), spawned), duration)
diff --git a/tgstation.dme b/tgstation.dme
index a6c4a365c058..d3f1a5831b25 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -6341,6 +6341,22 @@
#include "monkestation\code\modules\surgery\organs\internal\lungs.dm"
#include "monkestation\code\modules\surgery\organs\internal\stomach.dm"
#include "monkestation\code\modules\surgery\organs\internal\tongue.dm"
+#include "monkestation\code\modules\trading\box_rolling.dm"
+#include "monkestation\code\modules\trading\lootbox_buying.dm"
+#include "monkestation\code\modules\trading\lootbox_odds.dm"
+#include "monkestation\code\modules\trading\save_unusual_preference.dm"
+#include "monkestation\code\modules\trading\unusual_effects\_unusual_component.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\__spawning_component.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\_footprint.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\confetti.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\fire.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\galaxies.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\holy_steps.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\music.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\shooting_stars.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\skull_rain.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\snow.dm"
+#include "monkestation\code\modules\trading\unusual_effects\animation_housing\weh.dm"
#include "monkestation\code\modules\twitch_bits\admin_command.dm"
#include "monkestation\code\modules\twitch_bits\twitch_system.dm"
#include "monkestation\code\modules\twitch_bits\events\amongus.dm"
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/LoadoutPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/LoadoutPage.tsx
index 8bc0457c6356..4ef557d1f5ca 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/LoadoutPage.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/LoadoutPage.tsx
@@ -44,7 +44,13 @@ const CharacterControls = (props: {
export const LoadoutManager = (props, context) => {
const { act, data } = useBackend(context);
- const { selected_loadout, loadout_tabs, user_is_donator, total_coins } = data;
+ const {
+ selected_loadout,
+ loadout_tabs,
+ user_is_donator,
+ total_coins,
+ selected_unusuals,
+ } = data;
const [multiNameInputOpen, setMultiNameInputOpen] = useLocalState(
context,
'multiNameInputOpen',
@@ -228,7 +234,13 @@ export const LoadoutManager = (props, context) => {
)}
{
onClick={() =>
act('select_item', {
path: item.path,
+ unusual_spawning_requirements:
+ item.unusual_spawning_requirements,
+ unusual_placement: item.unusual_placement,
deselect: selected_loadout.includes(
item.path
),
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts
index b648b8054644..c8004453dc09 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts
@@ -92,6 +92,7 @@ export type QuirkInfo = {
export type LoadoutInfo = {
user_is_donator: BooleanLike;
selected_loadout: string[];
+ selected_unusuals: string[];
};
export enum RandomSetting {
@@ -178,6 +179,7 @@ export type PreferencesMenuData = {
user_is_donator: BooleanLike;
selected_loadout: string[];
+ selected_unusuals: string[];
total_coins: number;
loadout_tabs: LoadoutData[];
window: Window;
@@ -191,12 +193,14 @@ type LoadoutData = {
type LoadoutItem = {
name: string;
path: string;
+ unusual_placement: number;
is_greyscale: boolean;
is_renamable: boolean;
is_job_restricted: boolean;
is_donator_only: boolean;
is_ckey_whitelisted: boolean;
tooltip_text: string;
+ unusual_spawning_requirements: boolean;
};
export type ServerData = {
jobs: {