diff --git a/code/datums/actions/beam_rifle.dm b/code/datums/actions/beam_rifle.dm
new file mode 100644
index 0000000000..3af5d13690
--- /dev/null
+++ b/code/datums/actions/beam_rifle.dm
@@ -0,0 +1,12 @@
+
+/datum/action/item_action/zoom_speed_action
+ name = "Toggle Zooming Speed"
+ icon_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "projectile"
+ background_icon_state = "bg_tech"
+
+/datum/action/item_action/zoom_lock_action
+ name = "Switch Zoom Mode"
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "zoom_mode"
+ background_icon_state = "bg_tech"
diff --git a/code/datums/antagonists/monkey.dm b/code/datums/antagonists/monkey.dm
new file mode 100644
index 0000000000..8d7462b567
--- /dev/null
+++ b/code/datums/antagonists/monkey.dm
@@ -0,0 +1,170 @@
+#define MONKEYS_ESCAPED 1
+#define MONKEYS_LIVED 2
+#define MONKEYS_DIED 3
+#define DISEASE_LIVED 4
+
+/datum/antagonist/monkey
+ name = "Monkey"
+ job_rank = ROLE_MONKEY
+ roundend_category = "monkeys"
+ var/datum/objective_team/monkey/monkey_team
+
+/datum/antagonist/monkey/on_gain()
+ . = ..()
+ SSticker.mode.ape_infectees += owner
+ owner.special_role = "Infected Monkey"
+
+ var/datum/disease/D = new /datum/disease/transformation/jungle_fever
+ if(!owner.current.HasDisease(D))
+ D.affected_mob = owner
+ owner.current.viruses += D
+ else
+ QDEL_NULL(D)
+
+/datum/antagonist/monkey/greet()
+ to_chat(owner, "You are a monkey now!")
+ to_chat(owner, "Bite humans to infect them, follow the orders of the monkey leaders, and help fellow monkeys!")
+ to_chat(owner, "Ensure at least one infected monkey escapes on the Emergency Shuttle!")
+ to_chat(owner, "As an intelligent monkey, you know how to use technology and how to ventcrawl while wearing things.")
+ to_chat(owner, "You can use :k to talk to fellow monkeys!")
+ SEND_SOUND(owner.current, sound('sound/ambience/antag/monkey.ogg'))
+
+/datum/antagonist/monkey/on_removal()
+ . = ..()
+ owner.special_role = null
+ SSticker.mode.ape_infectees -= owner
+
+ var/datum/disease/D = (/datum/disease/transformation/jungle_fever in owner.current.viruses)
+ if(D)
+ D.cure()
+
+/datum/antagonist/monkey/create_team(datum/objective_team/monkey/new_team)
+ if(!new_team)
+ for(var/datum/antagonist/monkey/N in get_antagonists(/datum/antagonist/monkey, TRUE))
+ if(N.monkey_team)
+ monkey_team = N.monkey_team
+ return
+ monkey_team = new /datum/objective_team/monkey
+ monkey_team.update_objectives()
+ return
+ if(!istype(new_team))
+ stack_trace("Wrong team type passed to [type] initialization.")
+ monkey_team = new_team
+
+/datum/antagonist/monkey/proc/forge_objectives()
+ if(monkey_team)
+ owner.objectives |= monkey_team.objectives
+
+/datum/antagonist/monkey/leader
+ name = "Monkey Leader"
+
+/datum/antagonist/monkey/leader/on_gain()
+ . = ..()
+ var/datum/disease/D = (/datum/disease/transformation/jungle_fever in owner.current.viruses)
+ if(D)
+ D.visibility_flags = HIDDEN_SCANNER|HIDDEN_PANDEMIC
+ var/obj/item/organ/heart/freedom/F = new
+ F.Insert(owner.current, drop_if_replaced = FALSE)
+ SSticker.mode.ape_leaders += owner
+ owner.special_role = "Monkey Leader"
+
+/datum/antagonist/monkey/leader/on_removal()
+ . = ..()
+ SSticker.mode.ape_leaders -= owner
+ var/obj/item/organ/heart/H = new
+ H.Insert(owner.current, drop_if_replaced = FALSE) //replace freedom heart with normal heart
+
+/datum/antagonist/monkey/leader/greet()
+ to_chat(owner, "You are the Jungle Fever patient zero!!")
+ to_chat(owner, "You have been planted onto this station by the Animal Rights Consortium.")
+ to_chat(owner, "Soon the disease will transform you into an ape. Afterwards, you will be able spread the infection to others with a bite.")
+ to_chat(owner, "While your infection strain is undetectable by scanners, any other infectees will show up on medical equipment.")
+ to_chat(owner, "Your mission will be deemed a success if any of the live infected monkeys reach CentCom.")
+ to_chat(owner, "As an initial infectee, you will be considered a 'leader' by your fellow monkeys.")
+ to_chat(owner, "You can use :k to talk to fellow monkeys!")
+ SEND_SOUND(owner.current, sound('sound/ambience/antag/monkey.ogg'))
+
+/datum/objective/monkey
+ explanation_text = "Ensure that infected monkeys escape on the emergency shuttle!"
+ martyr_compatible = TRUE
+ var/monkeys_to_win = 1
+ var/escaped_monkeys = 0
+
+/datum/objective/monkey/check_completion()
+ var/datum/disease/D = new /datum/disease/transformation/jungle_fever()
+ for(var/mob/living/carbon/monkey/M in GLOB.alive_mob_list)
+ if (M.HasDisease(D) && (M.onCentCom() || M.onSyndieBase()))
+ escaped_monkeys++
+ if(escaped_monkeys >= monkeys_to_win)
+ return TRUE
+ return FALSE
+
+/datum/objective_team/monkey
+ name = "Monkeys"
+
+/datum/objective_team/monkey/proc/update_objectives()
+ objectives = list()
+ var/datum/objective/monkey/O = new /datum/objective/monkey()
+ O.team = src
+ objectives += O
+ return
+
+/datum/objective_team/monkey/proc/infected_monkeys_alive()
+ var/datum/disease/D = new /datum/disease/transformation/jungle_fever()
+ for(var/mob/living/carbon/monkey/M in GLOB.alive_mob_list)
+ if(M.HasDisease(D))
+ return TRUE
+ return FALSE
+
+/datum/objective_team/monkey/proc/infected_monkeys_escaped()
+ var/datum/disease/D = new /datum/disease/transformation/jungle_fever()
+ for(var/mob/living/carbon/monkey/M in GLOB.alive_mob_list)
+ if(M.HasDisease(D) && (M.onCentCom() || M.onSyndieBase()))
+ return TRUE
+ return FALSE
+
+/datum/objective_team/monkey/proc/infected_humans_escaped()
+ var/datum/disease/D = new /datum/disease/transformation/jungle_fever()
+ for(var/mob/living/carbon/human/M in GLOB.alive_mob_list)
+ if(M.HasDisease(D) && (M.onCentCom() || M.onSyndieBase()))
+ return TRUE
+ return FALSE
+
+/datum/objective_team/monkey/proc/infected_humans_alive()
+ var/datum/disease/D = new /datum/disease/transformation/jungle_fever()
+ for(var/mob/living/carbon/human/M in GLOB.alive_mob_list)
+ if(M.HasDisease(D))
+ return TRUE
+ return FALSE
+
+/datum/objective_team/monkey/proc/get_result()
+ if(infected_monkeys_escaped())
+ return MONKEYS_ESCAPED
+ if(infected_monkeys_alive())
+ return MONKEYS_LIVED
+ if(infected_humans_alive() || infected_humans_escaped())
+ return DISEASE_LIVED
+ return MONKEYS_DIED
+
+/datum/objective_team/monkey/roundend_report()
+ var/list/parts = list()
+ switch(get_result())
+ if(MONKEYS_ESCAPED)
+ parts += "Monkey Major Victory!"
+ parts += "Central Command and [station_name()] were taken over by the monkeys! Ook ook!"
+ if(MONKEYS_LIVED)
+ parts += "Monkey Minor Victory!"
+ parts += "[station_name()] was taken over by the monkeys! Ook ook!"
+ if(DISEASE_LIVED)
+ parts += "Monkey Minor Defeat!"
+ parts += "All the monkeys died, but the disease lives on! The future is uncertain."
+ if(MONKEYS_DIED)
+ parts += "Monkey Major Defeat!"
+ parts += "All the monkeys died, and Jungle Fever was wiped out!"
+ if(LAZYLEN(SSticker.mode.ape_leaders))
+ parts += ""
+ parts += printplayerlist(SSticker.mode.ape_leaders)
+ if(LAZYLEN(SSticker.mode.ape_infectees))
+ parts += ""
+ parts += printplayerlist(SSticker.mode.ape_infectees)
+ return "
[parts.Join("
")]
"
\ No newline at end of file
diff --git a/code/datums/components/cleaning.dm b/code/datums/components/cleaning.dm
new file mode 100644
index 0000000000..5d9d5992e2
--- /dev/null
+++ b/code/datums/components/cleaning.dm
@@ -0,0 +1,40 @@
+/datum/component/cleaning
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+
+/datum/component/cleaning/Initialize()
+ if(!ismovableatom(parent))
+ . = COMPONENT_INCOMPATIBLE
+ CRASH("[type] added to a [parent.type]")
+ RegisterSignal(list(COMSIG_MOVABLE_MOVED), .proc/Clean)
+
+/datum/component/cleaning/proc/Clean()
+ var/atom/movable/AM = parent
+ var/turf/tile = AM.loc
+ if(!isturf(tile))
+ return
+
+ tile.clean_blood()
+ for(var/A in tile)
+ if(is_cleanable(A))
+ qdel(A)
+ else if(istype(A, /obj/item))
+ var/obj/item/cleaned_item = A
+ cleaned_item.clean_blood()
+ else if(ishuman(A))
+ var/mob/living/carbon/human/cleaned_human = A
+ if(cleaned_human.lying)
+ if(cleaned_human.head)
+ cleaned_human.head.clean_blood()
+ cleaned_human.update_inv_head()
+ if(cleaned_human.wear_suit)
+ cleaned_human.wear_suit.clean_blood()
+ cleaned_human.update_inv_wear_suit()
+ else if(cleaned_human.w_uniform)
+ cleaned_human.w_uniform.clean_blood()
+ cleaned_human.update_inv_w_uniform()
+ if(cleaned_human.shoes)
+ cleaned_human.shoes.clean_blood()
+ cleaned_human.update_inv_shoes()
+ cleaned_human.clean_blood()
+ cleaned_human.wash_cream()
+ to_chat(cleaned_human, "[AM] cleans your face!")
diff --git a/code/datums/position_point_vector.dm b/code/datums/position_point_vector.dm
new file mode 100644
index 0000000000..2a98d57efd
--- /dev/null
+++ b/code/datums/position_point_vector.dm
@@ -0,0 +1,239 @@
+//Designed for things that need precision trajectories like projectiles.
+//Don't use this for anything that you don't absolutely have to use this with (like projectiles!) because it isn't worth using a datum unless you need accuracy down to decimal places in pixels.
+
+#define RETURN_PRECISE_POSITION(A) new /datum/position(A)
+#define RETURN_PRECISE_POINT(A) new /datum/point(A)
+
+/datum/position //For positions with map x/y/z and pixel x/y so you don't have to return lists. Could use addition/subtraction in the future I guess.
+ var/x = 0
+ var/y = 0
+ var/z = 0
+ var/pixel_x = 0
+ var/pixel_y = 0
+
+/datum/position/proc/valid()
+ return x && y && z && !isnull(pixel_x) && !isnull(pixel_y)
+
+/datum/position/New(_x = 0, _y = 0, _z = 0, _pixel_x = 0, _pixel_y = 0) //first argument can also be a /datum/point.
+ if(istype(_x, /datum/point))
+ var/datum/point/P = _x
+ var/turf/T = P.return_turf()
+ _x = T.x
+ _y = T.y
+ _z = T.z
+ _pixel_x = P.return_px()
+ _pixel_y = P.return_py()
+ else if(isatom(_x))
+ var/atom/A = _x
+ _x = A.x
+ _y = A.y
+ _z = A.z
+ _pixel_x = A.pixel_x
+ _pixel_y = A.pixel_y
+ x = _x
+ y = _y
+ z = _z
+ pixel_x = _pixel_x
+ pixel_y = _pixel_y
+
+/datum/position/proc/return_turf()
+ return locate(x, y, z)
+
+/datum/position/proc/return_px()
+ return pixel_x
+
+/datum/position/proc/return_py()
+ return pixel_y
+
+/datum/position/proc/return_point()
+ return new /datum/point(src)
+
+/proc/point_midpoint_points(datum/point/a, datum/point/b) //Obviously will not support multiZ calculations! Same for the two below.
+ var/datum/point/P = new
+ P.x = round(a.x + (b.x - a.x) / 2, 1)
+ P.y = round(a.y + (b.y - a.y) / 2, 1)
+ P.z = a.z
+ return P
+
+/proc/pixel_length_between_points(datum/point/a, datum/point/b)
+ return sqrt(((b.x - a.x) ** 2) + ((b.y - a.y) ** 2))
+
+/proc/angle_between_points(datum/point/a, datum/point/b)
+ return ATAN2((b.y - a.y), (b.x - a.x))
+
+/datum/point //A precise point on the map in absolute pixel locations based on world.icon_size. Pixels are FROM THE EDGE OF THE MAP!
+ var/x = 0
+ var/y = 0
+ var/z = 0
+
+/datum/point/proc/valid()
+ return x && y && z
+
+/datum/point/proc/copy_to(datum/point/p = new)
+ p.x = x
+ p.y = y
+ p.z = z
+ return p
+
+/datum/point/New(_x, _y, _z, _pixel_x = 0, _pixel_y = 0) //first argument can also be a /datum/position or /atom.
+ if(istype(_x, /datum/position))
+ var/datum/position/P = _x
+ _x = P.x
+ _y = P.y
+ _z = P.z
+ _pixel_x = P.pixel_x
+ _pixel_y = P.pixel_y
+ else if(istype(_x, /atom))
+ var/atom/A = _x
+ _x = A.x
+ _y = A.y
+ _z = A.z
+ _pixel_x = A.pixel_x
+ _pixel_y = A.pixel_y
+ initialize_location(_x, _y, _z, _pixel_x, _pixel_y)
+
+/datum/point/proc/initialize_location(tile_x, tile_y, tile_z, p_x = 0, p_y = 0)
+ if(!isnull(tile_x))
+ x = ((tile_x - 1) * world.icon_size) + world.icon_size / 2 + p_x
+ if(!isnull(tile_y))
+ y = ((tile_y - 1) * world.icon_size) + world.icon_size / 2+ p_y
+ if(!isnull(tile_z))
+ z = tile_z
+
+/datum/point/proc/return_turf()
+ return locate(CEILING(x / world.icon_size, 1), CEILING(y / world.icon_size, 1), z)
+
+/datum/point/proc/return_coordinates() //[turf_x, turf_y, z]
+ return list(CEILING(x / world.icon_size, 1), CEILING(y / world.icon_size, 1), z)
+
+/datum/point/proc/return_position()
+ return new /datum/position(src)
+
+/datum/point/proc/return_px()
+ return MODULUS(x, world.icon_size) - 16
+
+/datum/point/proc/return_py()
+ return MODULUS(y, world.icon_size) - 16
+
+/datum/point/proc/mapcheck()
+ . = FALSE
+ var/maxx = world.icon_size * world.maxx
+ var/maxy = world.icon_size * world.maxy
+ var/move_zx = 0
+ var/move_zy = 0
+ if(x < 0)
+ x += maxx
+ move_zx -= 1
+ if(y < 0)
+ y += maxy
+ move_zy -= 1
+ if(x > maxx)
+ x -= maxx
+ move_zx += 1
+ if(y > maxy)
+ y -= maxy
+ move_zy += 1
+ var/datum/space_level/S = GLOB.z_levels_list["[z]"]
+ if(move_zx != 0)
+ var/datum/space_level/L = S.neigbours["[move_zx < 0? WEST : EAST]"]
+ z = L.z_value
+ . = TRUE
+ if(move_zy != 0)
+ var/datum/space_level/L = S.neigbours["[move_zy < 0? SOUTH : NORTH]"]
+ z = L.z_value
+ . = TRUE
+
+/datum/point/vector
+ var/speed = 32 //pixels per iteration
+ var/iteration = 0
+ var/angle = 0
+ var/mpx = 0 //calculated x/y movement amounts to prevent having to do trig every step.
+ var/mpy = 0
+ var/starting_x = 0 //just like before, pixels from EDGE of map! This is set in initialize_location().
+ var/starting_y = 0
+ var/starting_z = 0
+
+/datum/point/vector/New(_x, _y, _z, _pixel_x = 0, _pixel_y = 0, _angle, _speed)
+ ..()
+ initialize_trajectory(_speed, _angle)
+
+/datum/point/vector/initialize_location(tile_x, tile_y, tile_z, p_x = 0, p_y = 0)
+ . = ..()
+ starting_x = x
+ starting_y = y
+ starting_z = z
+
+/datum/point/vector/copy_to(datum/point/vector/v = new)
+ ..(v)
+ v.speed = speed
+ v.iteration = iteration
+ v.angle = angle
+ v.mpx = mpx
+ v.mpy = mpy
+ v.starting_x = starting_x
+ v.starting_y = starting_y
+ v.starting_z = starting_z
+ return v
+
+/datum/point/vector/proc/initialize_trajectory(pixel_speed, new_angle)
+ if(!isnull(pixel_speed))
+ speed = pixel_speed
+ set_angle(new_angle)
+
+/datum/point/vector/proc/set_angle(new_angle) //calculations use "byond angle" where north is 0 instead of 90, and south is 180 instead of 270.
+ if(isnull(angle))
+ return
+ angle = new_angle
+ update_offsets()
+
+/datum/point/vector/proc/update_offsets()
+ mpx = sin(angle) * speed
+ mpy = cos(angle) * speed
+
+/datum/point/vector/proc/set_speed(new_speed)
+ if(isnull(new_speed) || speed == new_speed)
+ return
+ speed = new_speed
+ update_offsets()
+
+/datum/point/vector/proc/increment(multiplier = 1)
+ iteration++
+ x += mpx * 1
+ y += mpy * 1
+ if(mapcheck())
+ on_z_change()
+
+/datum/point/vector/proc/return_vector_after_increments(amount = 7, multiplier = 1, force_simulate = FALSE)
+ var/datum/point/vector/v = copy_to()
+ if(force_simulate)
+ for(var/i in 1 to amount)
+ v.increment(multiplier)
+ else
+ v.increment(multiplier * amount)
+ return v
+
+/datum/point/vector/proc/on_z_change()
+ return
+
+/datum/point/vector/processed //pixel_speed is per decisecond.
+ var/last_process = 0
+ var/last_move = 0
+ var/paused = FALSE
+
+/datum/point/vector/processed/Destroy()
+ STOP_PROCESSING(SSprojectiles, src)
+
+/datum/point/vector/processed/proc/start()
+ last_process = world.time
+ last_move = world.time
+ START_PROCESSING(SSprojectiles, src)
+
+/datum/point/vector/processed/process()
+ if(paused)
+ last_move += world.time - last_process
+ last_process = world.time
+ return
+ var/needed_time = world.time - last_move
+ last_process = world.time
+ last_move = world.time
+ increment(needed_time)
diff --git a/code/game/objects/effects/temporary_visuals/projectile_beam.dm b/code/game/objects/effects/temporary_visuals/projectile_beam.dm
new file mode 100644
index 0000000000..af621e29da
--- /dev/null
+++ b/code/game/objects/effects/temporary_visuals/projectile_beam.dm
@@ -0,0 +1,70 @@
+/proc/generate_projectile_beam_between_points(datum/point/starting, datum/point/ending, beam_type, color, qdel_in = 5) //Do not pass z-crossing points as that will not be properly (and likely will never be properly until it's absolutely needed) supported!
+ if(!istype(starting) || !istype(ending) || !ispath(beam_type))
+ return
+ var/datum/point/midpoint = point_midpoint_points(starting, ending)
+ var/obj/effect/projectile_beam/PB = new beam_type
+ PB.apply_vars(angle_between_points(starting, ending), midpoint.return_px(), midpoint.return_py(), color, pixel_length_between_points(starting, ending) / world.icon_size, midpoint.return_turf(), 0)
+ . = PB
+ if(qdel_in)
+ QDEL_IN(PB, qdel_in)
+
+/obj/effect/projectile_beam
+ icon = 'icons/obj/projectiles.dmi'
+ layer = ABOVE_MOB_LAYER
+ anchored = TRUE
+ light_power = 1
+ light_range = 2
+ light_color = "#00ffff"
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ flags_1 = ABSTRACT_1
+ appearance_flags = 0
+
+/obj/effect/projectile_beam/singularity_pull()
+ return
+
+/obj/effect/projectile_beam/singularity_act()
+ return
+
+/obj/effect/projectile_beam/proc/scale_to(nx,ny,override=TRUE)
+ var/matrix/M
+ if(!override)
+ M = transform
+ else
+ M = new
+ M.Scale(nx,ny)
+ transform = M
+
+/obj/effect/projectile_beam/proc/turn_to(angle,override=TRUE)
+ var/matrix/M
+ if(!override)
+ M = transform
+ else
+ M = new
+ M.Turn(angle)
+ transform = M
+
+/obj/effect/projectile_beam/New(angle_override, p_x, p_y, color_override, scaling = 1)
+ if(angle_override && p_x && p_y && color_override && scaling)
+ apply_vars(angle_override, p_x, p_y, color_override, scaling)
+ return ..()
+
+/obj/effect/projectile_beam/proc/apply_vars(angle_override, p_x = 0, p_y = 0, color_override, scaling = 1, new_loc, increment = 0)
+ var/mutable_appearance/look = new(src)
+ look.pixel_x = p_x
+ look.pixel_y = p_y
+ if(color_override)
+ look.color = color_override
+ appearance = look
+ scale_to(1,scaling, FALSE)
+ turn_to(angle_override, FALSE)
+ if(!isnull(new_loc)) //If you want to null it just delete it...
+ forceMove(new_loc)
+ for(var/i in 1 to increment)
+ pixel_x += round((sin(angle_override)+16*sin(angle_override)*2), 1)
+ pixel_y += round((cos(angle_override)+16*cos(angle_override)*2), 1)
+
+/obj/effect/projectile_beam/tracer
+ icon_state = "tracer_beam"
+
+/obj/effect/projectile_beam/tracer/aiming
+ icon_state = "gbeam"
diff --git a/code/modules/keybindings/bindings_atom.dm b/code/modules/keybindings/bindings_atom.dm
new file mode 100644
index 0000000000..9738175d2d
--- /dev/null
+++ b/code/modules/keybindings/bindings_atom.dm
@@ -0,0 +1,18 @@
+// You might be wondering why this isn't client level. If focus is null, we don't want you to move.
+// Only way to do that is to tie the behavior into the focus's keyLoop().
+
+/atom/movable/keyLoop(client/user)
+ if(!user.keys_held["Ctrl"])
+ var/movement_dir = NONE
+ for(var/_key in user.keys_held)
+ movement_dir = movement_dir | GLOB.movement_keys[_key]
+ if(user.next_move_dir_add)
+ movement_dir |= user.next_move_dir_add
+ if(user.next_move_dir_sub)
+ movement_dir &= ~user.next_move_dir_sub
+ // Sanity checks in case you hold left and right and up to make sure you only go up
+ if((movement_dir & NORTH) && (movement_dir & SOUTH))
+ movement_dir &= ~(NORTH|SOUTH)
+ if((movement_dir & EAST) && (movement_dir & WEST))
+ movement_dir &= ~(EAST|WEST)
+ user.Move(get_step(src, movement_dir), movement_dir)
\ No newline at end of file
diff --git a/code/modules/mob/emote.dm b/code/modules/mob/emote.dm
new file mode 100644
index 0000000000..5487b5b284
--- /dev/null
+++ b/code/modules/mob/emote.dm
@@ -0,0 +1,48 @@
+//The code execution of the emote datum is located at code/datums/emotes.dm
+/mob/emote(act, m_type = null, message = null)
+ act = lowertext(act)
+ var/param = message
+ var/custom_param = findchar(act, " ")
+ if(custom_param)
+ param = copytext(act, custom_param + 1, length(act) + 1)
+ act = copytext(act, 1, custom_param)
+
+ var/datum/emote/E
+ E = E.emote_list[act]
+ if(!E)
+ to_chat(src, "Unusable emote '[act]'. Say *help for a list.")
+ return
+ E.run_emote(src, param, m_type)
+
+/datum/emote/flip
+ key = "flip"
+ key_third_person = "flips"
+ restraint_check = TRUE
+ mob_type_allowed_typecache = list(/mob/living, /mob/dead/observer)
+ mob_type_ignore_stat_typecache = list(/mob/dead/observer)
+
+/datum/emote/flip/run_emote(mob/user, params)
+ . = ..()
+ if(.)
+ user.SpinAnimation(7,1)
+
+/datum/emote/spin
+ key = "spin"
+ key_third_person = "spins"
+ restraint_check = TRUE
+ mob_type_allowed_typecache = list(/mob/living, /mob/dead/observer)
+ mob_type_ignore_stat_typecache = list(/mob/dead/observer)
+
+/datum/emote/spin/run_emote(mob/user)
+ . = ..()
+ if(.)
+ user.spin(20, 1)
+
+ if(iscyborg(user) && user.has_buckled_mobs())
+ var/mob/living/silicon/robot/R = user
+ GET_COMPONENT_FROM(riding_datum, /datum/component/riding, R)
+ if(riding_datum)
+ for(var/mob/M in R.buckled_mobs)
+ riding_datum.force_dismount(M)
+ else
+ R.unbuckle_all_mobs()
diff --git a/icons/mob/landmarks.dmi b/icons/mob/landmarks.dmi
new file mode 100644
index 0000000000..120745ed44
Binary files /dev/null and b/icons/mob/landmarks.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index aadf2bf1b8..224a4904cd 100755
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -306,6 +306,7 @@
#include "code\datums\mutable_appearance.dm"
#include "code\datums\mutations.dm"
#include "code\datums\outfit.dm"
+#include "code\datums\position_point_vector.dm"
#include "code\datums\profiling.dm"
#include "code\datums\progressbar.dm"
#include "code\datums\radiation_wave.dm"
@@ -318,6 +319,7 @@
#include "code\datums\verbs.dm"
#include "code\datums\weakrefs.dm"
#include "code\datums\world_topic.dm"
+#include "code\datums\actions\beam_rifle.dm"
#include "code\datums\actions\flightsuit.dm"
#include "code\datums\actions\ninja.dm"
#include "code\datums\antagonists\abductor.dm"
@@ -329,6 +331,7 @@
#include "code\datums\antagonists\datum_traitor.dm"
#include "code\datums\antagonists\devil.dm"
#include "code\datums\antagonists\internal_affairs.dm"
+#include "code\datums\antagonists\monkey.dm"
#include "code\datums\antagonists\ninja.dm"
#include "code\datums\antagonists\nukeop.dm"
#include "code\datums\antagonists\pirate.dm"
@@ -345,6 +348,7 @@
#include "code\datums\components\archaeology.dm"
#include "code\datums\components\caltrop.dm"
#include "code\datums\components\chasm.dm"
+#include "code\datums\components\cleaning.dm"
#include "code\datums\components\decal.dm"
#include "code\datums\components\infective.dm"
#include "code\datums\components\jousting.dm"
@@ -1701,6 +1705,7 @@
#include "code\modules\mob\login.dm"
#include "code\modules\mob\logout.dm"
#include "code\modules\mob\mob.dm"
+#include "code\modules\mob\mob_cleanup.dm"
#include "code\modules\mob\mob_defines.dm"
#include "code\modules\mob\mob_helpers.dm"
#include "code\modules\mob\mob_movement.dm"