Server rebooting in:\n\ [DisplayTimeText(timeleft(SSticker.reboot_timer), 1)]
")
+/datum/controller/subsystem/ticker/vv_edit_var(var_name, var_value)
+ if(var_name == NAMEOF(src, login_music))
+ set_lobby_music(var_value, override = TRUE)
+ return ..()
+
/// Checks if the round should be ending, called every ticker tick
/datum/controller/subsystem/ticker/proc/check_finished()
if(!setup_done)
@@ -905,6 +910,10 @@ SUBSYSTEM_DEF(ticker)
return
login_music = new_music
+ //we just overrode the song, let's update everyone.
+ if(override)
+ for(var/mob/dead/new_player/new_player as anything in GLOB.new_player_list)
+ new_player?.client.playtitlemusic()
#undef ROUND_START_MUSIC_LIST
#undef SS_TICKER_TRAIT
diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm
index 0d5678a4abe4..bbc9d168d52e 100644
--- a/code/controllers/subsystem/vote.dm
+++ b/code/controllers/subsystem/vote.dm
@@ -284,6 +284,7 @@ SUBSYSTEM_DEF(vote)
log_admin("[key_name(toggle_initiator)] [text_verb] Dead Vote.")
message_admins("[key_name_admin(toggle_initiator)] [text_verb] Dead Vote.")
SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Dead Vote", text_verb))
+ update_static_data_for_all_viewers()
/datum/controller/subsystem/vote/ui_state()
return GLOB.always_state
@@ -312,8 +313,6 @@ SUBSYSTEM_DEF(vote)
"multiSelection" = current_vote?.choices_by_ckey,
)
- data["voting"]= is_lower_admin ? voting : list()
-
var/list/all_vote_data = list()
for(var/vote_name in possible_votes)
var/datum/vote/vote = possible_votes[vote_name]
@@ -356,6 +355,7 @@ SUBSYSTEM_DEF(vote)
/datum/controller/subsystem/vote/ui_static_data(mob/user)
var/list/data = list()
data["VoteCD"] = CONFIG_GET(number/vote_delay)
+ data["deadVoteEnabled"] = CONFIG_GET(flag/no_dead_vote)
return data
/datum/controller/subsystem/vote/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
@@ -374,7 +374,6 @@ SUBSYSTEM_DEF(vote)
voter.log_message("cancelled a vote.", LOG_ADMIN)
message_admins("[key_name_admin(voter)] has cancelled the current vote.")
- SStgui.close_uis(src)
reset()
return TRUE
diff --git a/code/controllers/subsystem/weather.dm b/code/controllers/subsystem/weather.dm
index 7a89236e08a2..d50013baceed 100644
--- a/code/controllers/subsystem/weather.dm
+++ b/code/controllers/subsystem/weather.dm
@@ -8,13 +8,26 @@ SUBSYSTEM_DEF(weather)
wait = 10
runlevels = RUNLEVEL_GAME
var/list/processing = list()
+ /// Z levels on which weather can occur -> weather that can occur -> probability of said weather occuring
var/list/eligible_zlevels = list()
- var/list/next_hit_by_zlevel = list() //Used by barometers to know when the next storm is coming
+ /// Used by barometers to know when the next storm is coming
+ var/list/next_hit_by_zlevel = list()
+ /// Alist of all particle holders per Z-stack offset for particle weather to be shown to clients
+ var/alist/particle_holders = alist()
+ /// List of all RENDER_PLANE_PARTICLE_WEATHER and RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER planes
+ var/list/particle_planemasters = list()
/datum/controller/subsystem/weather/fire(resumed = FALSE)
// process active weather
for(var/datum/weather/weather_event as anything in processing)
- if(!length(weather_event.subsystem_tasks) || weather_event.stage != MAIN_STAGE)
+ if(!length(weather_event.subsystem_tasks))
+ continue
+
+ if(istype(weather_event, /datum/weather/particle))
+ var/datum/weather/particle/particle_event = weather_event
+ particle_event.process_particles()
+
+ if(weather_event.stage != MAIN_STAGE)
continue
if(weather_event.subsystem_tasks[weather_event.task_index] == SSWEATHER_MOBS)
@@ -69,21 +82,42 @@ SUBSYSTEM_DEF(weather)
next_hit_by_zlevel["[z]"] = addtimer(CALLBACK(src, PROC_REF(make_eligible), z, possible_weather), randTime + initial(weather_event.weather_duration_upper), TIMER_UNIQUE|TIMER_STOPPABLE)
/datum/controller/subsystem/weather/Initialize()
- for(var/V in subtypesof(/datum/weather))
- var/datum/weather/W = V
- var/probability = initial(W.probability)
- var/target_trait = initial(W.target_trait)
+ for(var/datum/weather/weather as anything in valid_subtypesof(/datum/weather))
+ var/probability = initial(weather.probability)
+ var/target_trait = initial(weather.target_trait)
// any weather with a probability set may occur at random
if (probability)
for(var/z in SSmapping.levels_by_trait(target_trait))
LAZYINITLIST(eligible_zlevels["[z]"])
- eligible_zlevels["[z]"][W] = probability
+ eligible_zlevels["[z]"][weather] = probability
return SS_INIT_SUCCESS
+/datum/controller/subsystem/weather/proc/add_weather_objects(list/new_holders, z_level)
+ for (var/offset in 1 to length(new_holders))
+ var/list/holder_list = new_holders[offset]
+ if (isnull(particle_holders[offset]))
+ particle_holders[offset] = list()
+ particle_holders[offset] += holder_list
+
+ // We add it to vis_contents of planemasters rather than client screen as planemasters already
+ // manage their own visibility based on owner's z level
+ for (var/atom/movable/screen/plane_master/plane_master as anything in particle_planemasters)
+ for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list)
+ if (holder.plane == plane_master.plane)
+ plane_master.vis_contents |= holder
+
+/datum/controller/subsystem/weather/proc/remove_weather_objects(list/old_holders)
+ for (var/offset in 1 to length(old_holders))
+ var/list/holder_list = old_holders[offset]
+ particle_holders[offset] -= holder_list
+
+ for (var/atom/movable/screen/plane_master/plane_master as anything in particle_planemasters)
+ plane_master.vis_contents -= holder_list
+
/datum/controller/subsystem/weather/proc/update_z_level(datum/space_level/level)
var/z = level.z_value
- for(var/datum/weather/weather as anything in subtypesof(/datum/weather))
+ for(var/datum/weather/weather as anything in valid_subtypesof(/datum/weather))
var/probability = initial(weather.probability)
var/target_trait = initial(weather.target_trait)
if(probability && level.traits[target_trait])
@@ -92,10 +126,9 @@ SUBSYSTEM_DEF(weather)
/datum/controller/subsystem/weather/proc/run_weather(datum/weather/weather_datum_type, z_levels, list/weather_data)
if (istext(weather_datum_type))
- for (var/V in subtypesof(/datum/weather))
- var/datum/weather/W = V
- if (initial(W.name) == weather_datum_type)
- weather_datum_type = V
+ for (var/datum/weather/weather as anything in valid_subtypesof(/datum/weather))
+ if (initial(weather.name) == weather_datum_type)
+ weather_datum_type = weather
break
if (!ispath(weather_datum_type, /datum/weather))
CRASH("run_weather called with invalid weather_datum_type: [weather_datum_type || "null"]")
@@ -107,10 +140,9 @@ SUBSYSTEM_DEF(weather)
else if (!islist(z_levels))
CRASH("run_weather called with invalid z_levels: [z_levels || "null"]")
-
- var/datum/weather/W = new weather_datum_type(z_levels, weather_data)
- W.telegraph(weather_data)
- return W
+ var/datum/weather/weather = new weather_datum_type(z_levels, weather_data)
+ weather.telegraph(weather_data)
+ return weather
/datum/controller/subsystem/weather/proc/make_eligible(z, possible_weather)
eligible_zlevels[z] = possible_weather
diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm
index 317f6f0f139a..d453b15e5e66 100644
--- a/code/datums/actions/action.dm
+++ b/code/datums/actions/action.dm
@@ -181,7 +181,13 @@
return FALSE
if((check_flags & AB_CHECK_CONSCIOUS) && owner.stat != CONSCIOUS)
if (feedback)
- owner.balloon_alert(owner, "[owner.stat == DEAD ? "dead" : "unconscious"]!")
+ switch(owner.stat)
+ if(SOFT_CRIT)
+ owner.balloon_alert(owner, "downed!")
+ if(DEAD)
+ owner.balloon_alert(owner, "dead!")
+ else
+ owner.balloon_alert(owner, "unconscious!")
return FALSE
if((check_flags & AB_CHECK_HANDS_BLOCKED) && HAS_TRAIT(owner, TRAIT_HANDS_BLOCKED))
if (feedback)
diff --git a/code/datums/actions/mobs/charge.dm b/code/datums/actions/mobs/charge.dm
index 4dc016a66e67..dd80650065e0 100644
--- a/code/datums/actions/mobs/charge.dm
+++ b/code/datums/actions/mobs/charge.dm
@@ -212,18 +212,21 @@
if(!isliving(target))
source.visible_message(span_danger("[source] smashes into [target]!"))
- living_source?.Stun(recoil_duration, ignore_canstun = TRUE)
+ if (recoil_duration >= 0) // Because 0 stun/knockdown is still a valid value
+ living_source?.Stun(recoil_duration, ignore_canstun = TRUE)
return
var/mob/living/living_target = target
if(ishuman(living_target))
var/mob/living/carbon/human/human_target = living_target
if(human_target.check_block(source, 0, "\the [source]", attack_type = LEAP_ATTACK) && living_source)
- living_source.Stun(recoil_duration, ignore_canstun = TRUE)
+ if (recoil_duration >= 0)
+ living_source.Stun(recoil_duration, ignore_canstun = TRUE)
return
living_target.visible_message(span_danger("[source] charges into [living_target]!"), span_userdanger("[source] charges into you!"))
- living_target.Knockdown(knockdown_duration)
+ if (knockdown_duration >= 0)
+ living_target.Knockdown(knockdown_duration)
/datum/status_effect/tired_post_charge
id = "tired_post_charge"
diff --git a/code/datums/actions/mobs/dash.dm b/code/datums/actions/mobs/dash.dm
index ad87ab93f9a7..36db85531c1e 100644
--- a/code/datums/actions/mobs/dash.dm
+++ b/code/datums/actions/mobs/dash.dm
@@ -6,8 +6,6 @@
cooldown_time = 1.5 SECONDS
/// The range of the dash
var/dash_range = 4
- /// The distance you will be from the target after you dash
- var/pick_range = 5
/datum/action/cooldown/mob_cooldown/dash/Activate(atom/target_atom)
disable_cooldown_actions()
diff --git a/code/datums/actions/mobs/projectileattack.dm b/code/datums/actions/mobs/projectileattack.dm
index bd7425cc8e71..bdbea403e2fe 100644
--- a/code/datums/actions/mobs/projectileattack.dm
+++ b/code/datums/actions/mobs/projectileattack.dm
@@ -5,7 +5,7 @@
desc = "Fires a set of projectiles at a selected target."
cooldown_time = 1.5 SECONDS
/// The type of the projectile to be fired
- var/projectile_type
+ var/obj/projectile/projectile_type
/// The sound played when a projectile is fired
var/projectile_sound
/// If the projectile should home in on its target
@@ -58,7 +58,7 @@
our_projectile.set_homing_target(target)
if(isnum(set_angle))
our_projectile.fire(set_angle)
- return
+ return our_projectile
our_projectile.fire()
return our_projectile
@@ -78,7 +78,7 @@
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/attack_sequence(mob/living/firer, atom/target)
for(var/i in 1 to shot_count)
shoot_projectile(firer, target, null, firer, rand(-default_projectile_spread, default_projectile_spread), null)
- SLEEP_CHECK_DEATH(shot_delay, src)
+ SLEEP_CHECK_DEATH(shot_delay, firer)
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/direct
shot_count = 40
@@ -331,7 +331,7 @@
var/mob/living/simple_animal/hostile/megafauna/colossus/colossus
if(istype(firer, /mob/living/simple_animal/hostile/megafauna/colossus))
colossus = firer
- colossus.say("Perish.", spans = list("colossus", "yell"))
+ colossus.say("Perish.", spans = list(SPAN_COLOSSUS, SPAN_YELL))
SLEEP_CHECK_DEATH(1.5 SECONDS, firer) //gives dumbasses in melee range a slim chance to retreat
var/finale_counter = 10
diff --git a/code/datums/actions/mobs/sequences/dash_attack.dm b/code/datums/actions/mobs/sequences/dash_attack.dm
index b5fff4cb4922..42486f8d9f37 100644
--- a/code/datums/actions/mobs/sequences/dash_attack.dm
+++ b/code/datums/actions/mobs/sequences/dash_attack.dm
@@ -6,6 +6,12 @@
cooldown_time = 3 SECONDS
shared_cooldown = MOB_SHARED_COOLDOWN_2
sequence_actions = list(
- /datum/action/cooldown/mob_cooldown/dash = 0.1 SECONDS,
- /datum/action/cooldown/mob_cooldown/projectile_attack/kinetic_accelerator = 0,
+ /datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner = 0.22 SECONDS, // 0.1s windup + 0.12s max dash
+ /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator = 0,
+ )
+
+/datum/action/cooldown/mob_cooldown/dash_attack/long_burst
+ sequence_actions = list(
+ /datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner = 0.22 SECONDS,
+ /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/long_burst = 0,
)
diff --git a/code/datums/actions/mobs/transform_weapon.dm b/code/datums/actions/mobs/transform_weapon.dm
deleted file mode 100644
index 6cb97e0c635c..000000000000
--- a/code/datums/actions/mobs/transform_weapon.dm
+++ /dev/null
@@ -1,26 +0,0 @@
-/datum/action/cooldown/mob_cooldown/transform_weapon
- name = "Transform Weapon"
- button_icon = 'icons/obj/mining_zones/artefacts.dmi'
- button_icon_state = "cleaving_saw"
- desc = "Transform weapon into a different state."
- cooldown_time = 5 SECONDS
- shared_cooldown = MOB_SHARED_COOLDOWN_2
- /// The max possible cooldown, cooldown is random between the default cooldown time and this
- var/max_cooldown_time = 10 SECONDS
-
-/datum/action/cooldown/mob_cooldown/transform_weapon/Activate(atom/target_atom)
- disable_cooldown_actions()
- do_transform()
- StartCooldown(rand(cooldown_time, max_cooldown_time), 0)
- enable_cooldown_actions()
- return TRUE
-
-/datum/action/cooldown/mob_cooldown/transform_weapon/proc/do_transform()
- if(!istype(owner, /mob/living/basic/boss/blood_drunk_miner))
- return
- var/mob/living/basic/boss/blood_drunk_miner/blood_drunk_miner = owner
- blood_drunk_miner.miner_saw.attack_self(owner)
- var/saw_open = HAS_TRAIT(blood_drunk_miner.miner_saw, TRAIT_TRANSFORM_ACTIVE)
- blood_drunk_miner.rapid_melee_hits = saw_open ? 3 : 5
- blood_drunk_miner.icon_state = "miner[saw_open ? "_transformed":""]"
- blood_drunk_miner.icon_living = "miner[saw_open ? "_transformed":""]"
diff --git a/code/datums/ai/_ai_behavior.dm b/code/datums/ai/_ai_behavior.dm
index 4a277c0e8611..a73ca1672147 100644
--- a/code/datums/ai/_ai_behavior.dm
+++ b/code/datums/ai/_ai_behavior.dm
@@ -33,6 +33,7 @@
return
clear_movement_target(controller)
controller.ai_movement.stop_moving_towards(controller)
+ EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_BEHAVIORS, "[controller.pawn] has [succeeded ? "succeeded" : "failed"] at performing [src]", get_turf(controller.pawn), "Behavior finished: [succeeded ? "Success" : "Failure"]")
/// Helper proc to ensure consistency in setting the source of the movement target
/datum/ai_behavior/proc/set_movement_target(datum/ai_controller/controller, atom/target, datum/ai_movement/new_movement)
diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm
index 5536477253fb..dc14b7633cd6 100644
--- a/code/datums/ai/_ai_controller.dm
+++ b/code/datums/ai/_ai_controller.dm
@@ -70,6 +70,7 @@ multiple modular subtrees with behaviors
/// are we currently on failed planning timeout?
var/on_failed_planning_timeout = FALSE
+
/datum/ai_controller/New(atom/new_pawn)
change_ai_movement_type(ai_movement)
init_subtrees()
@@ -156,6 +157,8 @@ multiple modular subtrees with behaviors
RegisterSignal(pawn, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed))
RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained))
RegisterSignal(pawn, COMSIG_QDELETING, PROC_REF(on_pawn_qdeleted))
+ RegisterSignal(pawn, COMSIG_EVLOGGING_ENABLED, PROC_REF(on_pawn_evlogging_enabled))
+ RegisterSignal(pawn, COMSIG_EVLOGGING_DISABLED, PROC_REF(on_pawn_evlogging_disabled))
update_able_to_run()
setup_able_to_run()
@@ -305,7 +308,7 @@ multiple modular subtrees with behaviors
SEND_SIGNAL(src, COMSIG_AI_CONTROLLER_UNPOSSESSED_PAWN)
set_ai_status(AI_STATUS_OFF)
- UnregisterSignal(pawn, list(COMSIG_MOVABLE_Z_CHANGED, COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT, COMSIG_MOB_STATCHANGE, COMSIG_QDELETING))
+ UnregisterSignal(pawn, list(COMSIG_MOVABLE_Z_CHANGED, COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT, COMSIG_MOB_STATCHANGE, COMSIG_QDELETING, COMSIG_EVLOGGING_ENABLED))
clear_able_to_run()
if(ai_movement.moving_controllers[src])
ai_movement.stop_moving_towards(src)
@@ -427,6 +430,15 @@ multiple modular subtrees with behaviors
arguments += stored_arguments
forgotten_behavior.finish_action(arglist(arguments))
+ if(IS_EVLOGGING)
+ var/list/event_text = list()
+ event_text += "New plan starting!"
+
+ for(var/datum/ai_behavior/behavior in planned_behaviors)
+ event_text += "Queued behavior [behavior.type]"
+
+ EVLOG_TEXT(src, EVLOG_CATEGORY_AI_DECISIONMAKING, jointext(event_text, "\n"))
+
///This proc handles changing ai status, and starts/stops processing if required.
/datum/ai_controller/proc/set_ai_status(new_ai_status, additional_flags = NONE)
if(ai_status == new_ai_status)
@@ -563,6 +575,7 @@ multiple modular subtrees with behaviors
return
for(var/datum/ai_behavior/current_behavior as anything in current_behaviors)
fail_behavior(current_behavior)
+ EVLOG_TEXT(src, EVLOG_CATEGORY_AI_DECISIONMAKING, "Actions were cancelled!")
/datum/ai_controller/proc/fail_behavior(datum/ai_behavior/current_behavior)
var/list/arguments = list(src, FALSE)
@@ -874,6 +887,8 @@ multiple modular subtrees with behaviors
*/
/datum/ai_controller/proc/remove_thing_from_blackboard_key(key, thing)
var/associated_value = blackboard[key]
+ if(isnull(associated_value))
+ return
if(thing == associated_value)
stack_trace("remove_thing_from_blackboard_key was called un-necessarily in a situation where clear_blackboard_key would suffice. ")
clear_blackboard_key(key)
@@ -942,6 +957,55 @@ multiple modular subtrees with behaviors
index += 1
+/// When the pawn gets DF_EVLOGGING, propagate it to this controller too.
+/datum/ai_controller/proc/on_pawn_evlogging_enabled(datum/source)
+ SIGNAL_HANDLER
+ enable_evlogging(pawn)
+
+/// When the pawn gets DF_EVLOGGING disabled, propagate it to this controller too.
+/datum/ai_controller/proc/on_pawn_evlogging_disabled(datum/source)
+ SIGNAL_HANDLER
+ disable_evlogging(pawn)
+
+///Register for an event being added so we can update track info
+/datum/ai_controller/enable_evlogging()
+ . = ..()
+ RegisterSignal(src, COMSIG_EVLOG_EVENT_ADDED, PROC_REF(on_evlog_event_added))
+
+///Unregister the evlog event added event, as we're no longer updating track info
+/datum/ai_controller/disable_evlogging()
+ . = ..()
+ UnregisterSignal(src, COMSIG_EVLOG_EVENT_ADDED)
+
+/// Called whenever an event is logged for this controller. Attaches a snapshot of current behaviors and blackboard state to the event via track_info.
+/datum/ai_controller/proc/on_evlog_event_added(datum/source, datum/event_logger_track/track, list/event_data)
+ SIGNAL_HANDLER
+ var/list/track_info = list()
+
+ var/list/current_behavior_info = list()
+ for(var/datum/ai_behavior/behavior in planned_behaviors)
+ if(behavior in current_behaviors) //Not really ideal; we should find a better way to do this.
+ current_behavior_info += "ACTIVE: [span_bold("[behavior.type]")]"
+ else
+ current_behavior_info += "[behavior.type]"
+ EVLOG_TRACK_INFO_ENTRY(track_info, "Behaviors", "Current Behavior", jointext(current_behavior_info, "\n"))
+
+ for(var/blackboard_key_name, blackboard_value in blackboard)
+ var/value_string
+ if(isatom(blackboard_value))
+ value_string = "[blackboard_value]"
+ else if(islist(blackboard_value))
+ var/list/blackboard_list = blackboard_value
+ value_string = length(blackboard_list) ? jointext(blackboard_list, "\n") : "Empty List"
+ else if(isnull(blackboard_value))
+ value_string = "null"
+ else // I think I covered all cases?
+ value_string = "[blackboard_value]"
+ EVLOG_TRACK_INFO_ENTRY(track_info, "Blackboard", blackboard_key_name, value_string)
+
+ event_data["track_info"] = track_info
+
+
#undef TRACK_AI_DATUM_TARGET
#undef CLEAR_AI_DATUM_TARGET
#undef TRAIT_AI_TRACKING
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm
index 9d9f995e17fd..5b41ae5254d2 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm
@@ -24,17 +24,8 @@
var/atom/target = controller.blackboard[target_key]
if (isnull(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
- if (!target.IsReachableBy(controller.pawn))
- controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER)
- return AI_BEHAVIOR_INSTANT
- var/can_attack_time = controller.blackboard[BB_BASIC_MOB_MELEE_COOLDOWN_TIMER]
- if (isnull(can_attack_time))
- var/blackboard_delay = controller.blackboard[BB_BASIC_MOB_MELEE_DELAY]
- var/attack_delay = isnull(blackboard_delay) ? DEFAULT_ATTACK_DELAY : blackboard_delay
- controller.set_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER, world.time + attack_delay)
- return AI_BEHAVIOR_INSTANT
- if (can_attack_time > world.time)
+ if (!can_attack(controller, target))
return AI_BEHAVIOR_INSTANT
if (isliving(controller.pawn))
@@ -56,6 +47,23 @@
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY
+/datum/ai_behavior/basic_melee_attack/proc/can_attack(datum/ai_controller/controller, atom/target)
+ if (!target.IsReachableBy(controller.pawn))
+ controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER)
+ return FALSE
+
+ var/can_attack_time = controller.blackboard[BB_BASIC_MOB_MELEE_COOLDOWN_TIMER]
+ if (isnull(can_attack_time))
+ var/blackboard_delay = controller.blackboard[BB_BASIC_MOB_MELEE_DELAY]
+ var/attack_delay = isnull(blackboard_delay) ? DEFAULT_ATTACK_DELAY : blackboard_delay
+ controller.set_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER, world.time + attack_delay)
+ return FALSE
+
+ if (can_attack_time > world.time)
+ return FALSE
+
+ return TRUE
+
/datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER)
@@ -78,7 +86,7 @@
/// range we will try chasing the target before giving up
var/chase_range = 9
///do we care about avoiding friendly fire?
- var/avoid_friendly_fire = FALSE
+ var/avoid_friendly_fire = FALSE
/datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
index 9305c3986113..ce46d4c1ab98 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
@@ -12,6 +12,12 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
var/vision_range = 9
/// Blackboard key for aggro range, uses vision range if not specified
var/aggro_range_key = BB_AGGRO_RANGE
+ /// Range in which we can acquire a new target
+ var/aggro_grab_range_key = BB_AGGRO_GRAB_RANGE
+ /// Blackboard key for the target priority strategy
+ var/priority_strategy_key = BB_TARGET_PRIORITY_STRATEGY
+ /// If we have a priority strategy set, how often do we refresh our target search?
+ var/priority_refresh_cooldown = 6 SECONDS
/datum/ai_behavior/find_potential_targets/get_cooldown(datum/ai_controller/cooldown_for)
if(cooldown_for.blackboard[BB_FIND_TARGETS_FIELD(type)])
@@ -26,10 +32,15 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
var/atom/current_target = controller.blackboard[target_key]
- if (targeting_strategy.can_attack(living_mob, current_target, vision_range))
+ var/datum/target_priority_strategy/priority_strategy = GET_TARGET_PRIORITY_STRATEGY(controller.blackboard[priority_strategy_key])
+ if((!priority_strategy || controller.blackboard[BB_BASIC_MOB_TARGET_REFRESH_COOLDOWN] > world.time) && current_target && targeting_strategy.can_attack(living_mob, current_target, vision_range))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
- var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
+ var/aggro_range = vision_range
+ if(isnull(current_target) && !isnull(controller.blackboard[aggro_grab_range_key]))
+ aggro_range = controller.blackboard[aggro_grab_range_key]
+ else if(!isnull(controller.blackboard[aggro_range_key]))
+ aggro_range = controller.blackboard[aggro_range_key]
controller.clear_blackboard_key(target_key)
@@ -46,22 +57,34 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
potential_targets += hostile_machine
if(!potential_targets.len)
- failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key)
+ if(!current_target)
+ failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/list/filtered_targets = list()
+ var/current_priority = 0
+ if(priority_strategy)
+ current_priority = priority_strategy.get_target_priority(controller, current_target)
for(var/atom/pot_target in potential_targets)
- if(targeting_strategy.can_attack(living_mob, pot_target))//Can we attack it?
- filtered_targets += pot_target
+ if(!targeting_strategy.can_attack(living_mob, pot_target))
continue
+ if (priority_strategy && priority_strategy.get_target_priority(controller, pot_target) < current_priority)
+ continue
+ filtered_targets += pot_target
if(!filtered_targets.len)
- failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key)
+ if(!current_target)
+ failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/atom/target = pick_final_target(controller, filtered_targets)
+
+ EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_TARGETING, "[controller.pawn] has selected [target] as a target for blackboard key [target_key]! Behavior: [src]", get_turf(target), "Target: [target]")
+ EVLOG_LINES(controller, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(controller.pawn), get_turf(target))
+
controller.set_blackboard_key(target_key, target)
+ controller.set_blackboard_key(BB_BASIC_MOB_TARGET_REFRESH_COOLDOWN, world.time + priority_refresh_cooldown)
var/atom/potential_hiding_location = targeting_strategy.find_hidden_mobs(living_mob, target)
@@ -71,7 +94,12 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/find_potential_targets/proc/failed_to_find_anyone(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
- var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
+ var/aggro_range = vision_range
+ if(!isnull(controller.blackboard[aggro_grab_range_key]))
+ aggro_range = controller.blackboard[aggro_grab_range_key]
+ else if(!isnull(controller.blackboard[aggro_range_key]))
+ aggro_range = controller.blackboard[aggro_range_key]
+
// takes the larger between our range() input and our implicit hearers() input (world.view)
aggro_range = max(aggro_range, ROUND_UP(max(getviewsize(world.view)) / 2))
// Alright, here's the interesting bit
@@ -134,6 +162,8 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
// Alright, we found something acceptable, let's use it yeah?
var/atom/target = pick_final_target(controller, accepted_targets)
+ EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_TARGETING, "[controller.pawn] has selected [target] as a target for blackboard key [target_key]! Behavior: [src]", get_turf(target), "Target: [target]")
+ EVLOG_LINES(controller, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(controller.pawn), get_turf(target))
controller.set_blackboard_key(target_key, target)
var/atom/potential_hiding_location = strategy.find_hidden_mobs(pawn, target)
@@ -153,7 +183,10 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
/// Returns the desired final target from the filtered list of targets
/datum/ai_behavior/find_potential_targets/proc/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
- return pick(filtered_targets)
+ var/datum/target_priority_strategy/priority_strategy = GET_TARGET_PRIORITY_STRATEGY(controller.blackboard[priority_strategy_key])
+ if (!priority_strategy)
+ return pick(filtered_targets)
+ return priority_strategy.select_target(controller, filtered_targets)
/// Targets with the trait specified by the BB_TARGET_PRIORITY_TRAIT blackboard key will be prioritized over the rest.
/datum/ai_behavior/find_potential_targets/prioritize_trait
@@ -164,8 +197,5 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
if(HAS_TRAIT(target, controller.blackboard[BB_TARGET_PRIORITY_TRAIT]))
priority_targets += target
if(length(priority_targets))
- return pick(priority_targets)
+ return ..(controller, priority_targets)
return ..()
-
-/datum/ai_behavior/find_potential_targets/bigger_range
- vision_range = 16
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm b/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm
index f78697b2b813..46886680d528 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm
@@ -1,5 +1,3 @@
-#define REINFORCEMENTS_COOLDOWN (30 SECONDS)
-
/// Calls all nearby mobs that share a faction to give backup in combat
/datum/ai_planning_subtree/call_reinforcements
/// Blackboard key containing something to say when calling reinforcements (takes precedence over emotes)
@@ -21,8 +19,6 @@
controller.queue_behavior(/datum/ai_behavior/perform_speech, call_say)
else if(!isnull(call_emote))
controller.queue_behavior(/datum/ai_behavior/perform_emote, call_emote)
- else
- controller.queue_behavior(/datum/ai_behavior/perform_emote, "cries for help!")
controller.queue_behavior(call_type)
@@ -30,20 +26,43 @@
/datum/ai_planning_subtree/call_reinforcements/proc/decide_to_call(datum/ai_controller/controller)
return controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET) && istype(controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET], /mob)
+/datum/ai_planning_subtree/call_reinforcements/mining
+ call_type = /datum/ai_behavior/call_reinforcements/mining
+
/// Call out to all mobs in the specified range for help
/datum/ai_behavior/call_reinforcements
+ /// How frequently can we call for reinforcements?
+ var/cooldown = 30 SECONDS
/// Range to call reinforcements from
var/reinforcements_range = 15
/datum/ai_behavior/call_reinforcements/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/pawn_mob = controller.pawn
for(var/mob/other_mob in oview(reinforcements_range, pawn_mob))
- if(pawn_mob.faction_check_atom(other_mob) && !isnull(other_mob.ai_controller))
- // Add our current target to their retaliate list so that they'll attack our aggressor
- other_mob.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
- other_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENT_TARGET, pawn_mob)
+ if(!pawn_mob.faction_check_atom(other_mob) || isnull(other_mob.ai_controller))
+ continue
+ // Add our current target to their retaliate list so that they'll attack our aggressor
+ other_mob.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET], world.time)
+ other_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENT_TARGET, pawn_mob)
- controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN, world.time + REINFORCEMENTS_COOLDOWN)
+ controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN, world.time + cooldown)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
-#undef REINFORCEMENTS_COOLDOWN
+/// Does not force retaliation, but increases targeting priority instead
+/datum/ai_behavior/call_reinforcements/mining
+ cooldown = 1 SECONDS
+ reinforcements_range = 7
+
+/datum/ai_behavior/call_reinforcements/mining/perform(seconds_per_tick, datum/ai_controller/controller)
+ var/mob/pawn_mob = controller.pawn
+ var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ for(var/mob/other_mob in oview(reinforcements_range, pawn_mob))
+ if(!pawn_mob.faction_check_atom(other_mob) || isnull(other_mob.ai_controller))
+ continue
+ var/list/existing_requests = other_mob.ai_controller.blackboard[BB_MINING_MOB_REINFORCEMENTS_REQUESTS]
+ if (!existing_requests || !existing_requests[target])
+ other_mob.ai_controller.set_blackboard_key_assoc_lazylist(BB_MINING_MOB_REINFORCEMENTS_REQUESTS, target, list())
+ other_mob.ai_controller.add_blackboard_key_assoc(BB_MINING_MOB_REINFORCEMENTS_REQUESTS, target, world.time)
+
+ controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN, world.time + cooldown)
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm
index 52b19036b9a4..211e01be1f28 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm
@@ -49,7 +49,7 @@
failed_targeting(pawn)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
- controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, final_target)
+ controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, final_target, world.time)
pawn.visible_message(span_warning("[pawn] glares grumpily at [final_target]!"))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/enrage.dm b/code/datums/ai/basic_mobs/basic_subtrees/enrage.dm
new file mode 100644
index 000000000000..cf3a922a5a44
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/enrage.dm
@@ -0,0 +1,44 @@
+// Performs the enrage behavior when health is below given threshold, and calm down behavior if above that value afterwards
+/datum/ai_planning_subtree/enrage
+ var/health_threshold = 0.5
+ var/enrage_behavior = /datum/ai_behavior/enrage
+
+/datum/ai_planning_subtree/enrage/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!isbasicmob(controller.pawn))
+ return
+ var/mob/living/basic/basic_pawn = controller.pawn
+ var/low_health = (basic_pawn.health / basic_pawn.maxHealth) <= health_threshold
+
+ var/is_enraged = controller.blackboard_key_exists(BB_BASIC_MOB_ENRAGE)
+ if(low_health && !is_enraged)
+ controller.queue_behavior(enrage_behavior, FALSE)
+ else if(!low_health && is_enraged)
+ controller.queue_behavior(enrage_behavior, TRUE)
+
+
+/// Cuts down basic mob's melee attack cooldown in half
+/datum/ai_behavior/enrage
+
+/datum/ai_behavior/enrage/perform(seconds_per_tick, datum/ai_controller/controller, calm_down)
+ var/mob/living/basic/basic_pawn = controller.pawn
+ if(calm_down)
+ var/previous_delay = controller.blackboard[BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN]
+ // Technically something else could have modified the cooldown before/after but that requires further consideration so don't use this behavior in these scenarios
+ basic_pawn.melee_attack_cooldown = previous_delay
+ controller.clear_blackboard_key(BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN)
+ controller.clear_blackboard_key(BB_BASIC_MOB_ENRAGE)
+ return AI_BEHAVIOR_SUCCEEDED
+
+ var/current_cooldown = basic_pawn.melee_attack_cooldown
+ var/new_attack_cooldown = current_cooldown / 2
+
+ controller.set_blackboard_key(BB_BASIC_MOB_ENRAGE, TRUE)
+ controller.set_blackboard_key(BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN, current_cooldown)
+ basic_pawn.melee_attack_cooldown = new_attack_cooldown
+
+ if(controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
+ var/current_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ controller.pawn.visible_message(span_danger("\The [controller.pawn] gets an enraged look at [current_target]!"))
+ else
+ controller.pawn.visible_message(span_danger("\The [controller.pawn] gets an enraged look!"))
+ return AI_BEHAVIOR_SUCCEEDED
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm b/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm
index 12875f9a3f34..6edc631c7a15 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm
@@ -21,6 +21,7 @@
set_movement_target(controller, target)
/datum/ai_behavior/mine_wall/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
var/mob/living/basic/living_pawn = controller.pawn
var/turf/closed/mineral/target = controller.blackboard[target_key]
var/is_gibtonite_turf = istype(target, /turf/closed/mineral/gibtonite)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm
index 525248529615..57fb22b4a956 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm
@@ -3,10 +3,12 @@
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// Targeting strategy key to use
var/strategy_key = BB_TARGETING_STRATEGY
+ /// Behavior to use to find targets
+ var/target_behavior = /datum/ai_behavior/find_potential_targets
/datum/ai_planning_subtree/simple_find_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- controller.queue_behavior(/datum/ai_behavior/find_potential_targets, target_key, strategy_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ controller.queue_behavior(target_behavior, target_key, strategy_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
// Prevents finding a target if a human is nearby
/datum/ai_planning_subtree/simple_find_target/not_while_observed
@@ -20,10 +22,5 @@
/datum/ai_planning_subtree/simple_find_target/to_flee
target_key = BB_BASIC_MOB_FLEE_TARGET
-/datum/ai_planning_subtree/simple_find_target/increased_range
-
-/datum/ai_planning_subtree/simple_find_target/increased_range/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- controller.queue_behavior(/datum/ai_behavior/find_potential_targets/bigger_range, target_key, strategy_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
-
/datum/ai_planning_subtree/simple_find_target/hunt
strategy_key = BB_HUNT_TARGETING_STRATEGY
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
index 6893bad6f6a7..c86994054613 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
@@ -9,9 +9,11 @@
var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION
/// do we check for faction?
var/check_faction = FALSE
+ /// Behavior to use to select our target
+ var/target_behavior = /datum/ai_behavior/target_from_retaliate_list
/datum/ai_planning_subtree/target_retaliate/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_strategy_key, hiding_place_key, check_faction)
+ controller.queue_behavior(target_behavior, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_strategy_key, hiding_place_key, check_faction)
/datum/ai_planning_subtree/target_retaliate/check_faction
check_faction = TRUE
@@ -43,6 +45,12 @@
var/list/shitlist = controller.blackboard[shitlist_key]
var/atom/existing_target = controller.blackboard[target_key]
+ var/datum/target_priority_strategy/priority_strategy = GET_TARGET_PRIORITY_STRATEGY(controller.blackboard[BB_TARGET_PRIORITY_STRATEGY])
+ var/existing_priority = 0
+ // If we have an existing target and its priority is higher than our new target's, don't switch focus
+ if (priority_strategy && existing_target)
+ existing_priority = priority_strategy.get_target_priority(controller, existing_target)
+
if (!check_faction)
controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE)
@@ -53,13 +61,12 @@
for(var/mob/living/potential_target as anything in shitlist)
if(!targeting_strategy.can_attack(living_mob, potential_target, vision_range))
continue
+ // Strict comparasion because priority strategies might not care about retaliation, so this makes existing targets not override potential retaliates
+ if (priority_strategy && priority_strategy.get_target_priority(controller, potential_target) < existing_priority)
+ continue
enemies_list += potential_target
if(!length(enemies_list))
-
- if(existing_target)
- controller.clear_blackboard_key(target_key)
-
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/atom/new_target = pick_final_target(controller, enemies_list)
@@ -74,7 +81,10 @@
/// Returns the desired final target from the filtered list of enemies
/datum/ai_behavior/target_from_retaliate_list/proc/pick_final_target(datum/ai_controller/controller, list/enemies_list)
- return pick(enemies_list)
+ var/datum/target_priority_strategy/priority_strategy = GET_TARGET_PRIORITY_STRATEGY(controller.blackboard[BB_TARGET_PRIORITY_STRATEGY])
+ if (!priority_strategy)
+ return pick(enemies_list)
+ return priority_strategy.select_target(controller, enemies_list)
/datum/ai_behavior/target_from_retaliate_list/finish_action(datum/ai_controller/controller, succeeded, shitlist_key, target_key, targeting_strategy_key, hiding_location_key, check_faction)
. = ..()
diff --git a/code/datums/ai/basic_mobs/target_priority_strategies/_target_priority_strategy.dm b/code/datums/ai/basic_mobs/target_priority_strategies/_target_priority_strategy.dm
new file mode 100644
index 000000000000..3cb58ba086c8
--- /dev/null
+++ b/code/datums/ai/basic_mobs/target_priority_strategies/_target_priority_strategy.dm
@@ -0,0 +1,13 @@
+/// Target priority datum, exists to allow mobs to have custom targeting priorities. Singleton.
+/datum/target_priority_strategy
+
+/// Returns a number representing the priority of a target, higher -> more likely to attack
+/datum/target_priority_strategy/proc/get_target_priority(datum/ai_controller/controller, atom/target)
+ return 1
+
+/// Returns a single atom from the list of passed targets
+/datum/target_priority_strategy/proc/select_target(datum/ai_controller/controller, list/atom/targets)
+ var/list/target_priorities = list()
+ for (var/atom/target as anything in targets)
+ target_priorities[target] = get_target_priority(controller, target)
+ return pick_weight(target_priorities)
diff --git a/code/datums/ai/basic_mobs/target_priority_strategies/mining_strategies.dm b/code/datums/ai/basic_mobs/target_priority_strategies/mining_strategies.dm
new file mode 100644
index 000000000000..569f23fff187
--- /dev/null
+++ b/code/datums/ai/basic_mobs/target_priority_strategies/mining_strategies.dm
@@ -0,0 +1,89 @@
+/// Ashies get lower priorities than humans
+#define AGGRO_PRIORITY_ASHWALKER 1
+/// Basicmobs have slightly less priority so that miners are targeted over their raptors
+#define AGGRO_PRIORITY_BASICMOB 3
+/// Everyone by default
+#define AGGRO_PRIORITY_HUMAN 5
+/// Mobs who have attacked someone in our view recently
+#define AGGRO_PRIORITY_MINER 8
+/// NODE drone priority
+#define AGGRO_PRIORITY_NODE 10
+/// NODE drone priority for legion broods and brimdemons
+#define AGGRO_PRIORITY_NODE_LOW_PRIO 3
+/// Priority for mobs we're retaliating against
+#define AGGRO_PRIORITY_RETALIATE 15
+
+/// Prioritizes NODE drones until attacked, then swaps back onto miners
+/datum/target_priority_strategy/mining
+ /// For how long do we keep aggro on mobs over NODE drones?
+ var/retaliate_aggro_memory = 25 SECONDS
+ /// Priority for NODE drones
+ var/node_priority = AGGRO_PRIORITY_NODE
+
+/datum/target_priority_strategy/mining/get_target_priority(datum/ai_controller/controller, mob/living/target)
+ if (!isliving(target))
+ return ..()
+
+ // If a mob recently attacked us, it has higher priority than NODE drones, otherwise we care about NODE drones more
+ var/list/shitlist = controller.blackboard[BB_BASIC_MOB_RETALIATE_LIST]
+ if (shitlist?[target] && (shitlist[target] + retaliate_aggro_memory > world.time))
+ // Does not decay until it passes the retaliate memory threshold to avoid constant target swapping
+ return AGGRO_PRIORITY_RETALIATE
+
+ if (istype(target, /mob/living/basic/node_drone))
+ return node_priority
+
+ // Check if they've recently attacked any of our friends, if so - increase their priority by 1 for each attack in the past [retaliate_aggro_memory] seconds
+ var/list/allies_shitlist = controller.blackboard[BB_MINING_MOB_REINFORCEMENTS_REQUESTS]
+ var/list/target_requests = allies_shitlist?[target]
+ var/aggro_boost = 0
+ if (target_requests)
+ var/total_requests = length(target_requests)
+ // Goes end-to-start because we do not care about requests older than [retaliate_aggro_memory]
+ for (var/i in 1 to total_requests)
+ var/request_time = target_requests[total_requests - i + 1]
+ if (request_time + retaliate_aggro_memory < world.time)
+ break
+ aggro_boost += 1
+
+ if (aggro_boost || HAS_TRAIT(target, TRAIT_MINING_AGGRO))
+ return AGGRO_PRIORITY_MINER + aggro_boost
+
+ if (isbasicmob(target))
+ return AGGRO_PRIORITY_BASICMOB
+
+ return target.has_faction(FACTION_ASHWALKER) ? AGGRO_PRIORITY_ASHWALKER : AGGRO_PRIORITY_HUMAN
+
+/datum/target_priority_strategy/mining/select_target(datum/ai_controller/controller, list/atom/targets)
+ var/max_priority = 0
+ var/min_distance = INFINITY
+ var/lucky_fella = null
+
+ // Selects highest priority targets, then picks the closest
+ for (var/atom/target as anything in targets)
+ var/target_prio = get_target_priority(controller, target)
+ if (target_prio < max_priority)
+ continue
+
+ if (target_prio > max_priority)
+ max_priority = target_prio
+ min_distance = INFINITY
+
+ var/target_dist = get_dist(controller.pawn, target)
+ if (target_dist < min_distance)
+ lucky_fella = target
+ min_distance = target_dist
+
+ return pick(lucky_fella)
+
+/datum/target_priority_strategy/mining/low_node_priority
+ // Higher than normal humans but will instantly pivot towards anyone who attacks or attacked mobs in our vicinity
+ node_priority = AGGRO_PRIORITY_NODE_LOW_PRIO
+
+#undef AGGRO_PRIORITY_ASHWALKER
+#undef AGGRO_PRIORITY_BASICMOB
+#undef AGGRO_PRIORITY_HUMAN
+#undef AGGRO_PRIORITY_MINER
+#undef AGGRO_PRIORITY_NODE
+#undef AGGRO_PRIORITY_NODE_LOW_PRIO
+#undef AGGRO_PRIORITY_RETALIATE
diff --git a/code/datums/ai/generic/find_and_set.dm b/code/datums/ai/generic/find_and_set.dm
index 9ec033e04026..ced56912c91c 100644
--- a/code/datums/ai/generic/find_and_set.dm
+++ b/code/datums/ai/generic/find_and_set.dm
@@ -14,6 +14,9 @@
var/find_this_thing = search_tactic(controller, locate_path, search_range)
if(isnull(find_this_thing))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_TARGETING, "[controller.pawn] has selected [find_this_thing] as a target for blackboard key [set_key]! Behavior: [src]", get_turf(find_this_thing), "Target: [find_this_thing]")
+ EVLOG_LINES(controller, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(controller.pawn), get_turf(find_this_thing))
controller.set_blackboard_key(set_key, find_this_thing)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm
index da338fa45cbd..d0e8c25c9d77 100644
--- a/code/datums/ai/generic/generic_behaviors.dm
+++ b/code/datums/ai/generic/generic_behaviors.dm
@@ -301,6 +301,7 @@
/datum/ai_behavior/perform_emote
/datum/ai_behavior/perform_emote/perform(seconds_per_tick, datum/ai_controller/controller, emote, speech_sound)
+ . = ..()
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn))
return AI_BEHAVIOR_INSTANT
diff --git a/code/datums/ai/hunting_behavior/hunting_behaviors.dm b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
index db684921281a..1095919c058b 100644
--- a/code/datums/ai/hunting_behavior/hunting_behaviors.dm
+++ b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
@@ -55,6 +55,8 @@
if(!valid_dinner(living_mob, possible_dinner, hunt_range, controller, seconds_per_tick))
continue
controller.set_blackboard_key(hunting_target_key, possible_dinner)
+ EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_TARGETING, "[controller.pawn] has selected [possible_dinner] as a target for blackboard key [hunting_target_key]! Behavior: [src]", get_turf(possible_dinner), "Target: [possible_dinner]")
+ EVLOG_LINES(controller, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(controller.pawn), get_turf(possible_dinner))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
diff --git a/code/datums/ai/monkey/monkey_behaviors.dm b/code/datums/ai/monkey/monkey_behaviors.dm
index c21d15e8a19d..865f6a069eaa 100644
--- a/code/datums/ai/monkey/monkey_behaviors.dm
+++ b/code/datums/ai/monkey/monkey_behaviors.dm
@@ -296,5 +296,10 @@
if(!length(valids))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
- controller.set_blackboard_key(set_key, pick_weight(valids))
+ var/mob/living/target = pick_weight(valids)
+
+ EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_TARGETING, "[controller.pawn] has selected [target] as a target for blackboard key [set_key]! Behavior: [src]", get_turf(target), "Target: [target]")
+ EVLOG_LINES(controller, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(controller.pawn), get_turf(target))
+
+ controller.set_blackboard_key(set_key, target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
diff --git a/code/datums/ai/telegraph_effects.dm b/code/datums/ai/telegraph_effects.dm
index 62bbe9a0397e..b658c651988a 100644
--- a/code/datums/ai/telegraph_effects.dm
+++ b/code/datums/ai/telegraph_effects.dm
@@ -6,6 +6,14 @@
light_range = 1
duration = 2 SECONDS
+/obj/effect/temp_visual/telegraphing/Initialize(mapload)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/effect/temp_visual/telegraphing/update_overlays()
+ . = ..()
+ . += emissive_appearance(icon, icon_state, src, alpha = 90)
+
/obj/effect/temp_visual/telegraphing/vending_machine_tilt
duration = 1 SECONDS
@@ -15,7 +23,18 @@
src.duration = duration
return ..()
-/obj/effect/temp_visual/telegraphing/thunderbolt
+/obj/effect/temp_visual/telegraphing/circle
icon = 'icons/mob/telegraphing/telegraph.dmi'
icon_state = "target_circle"
duration = 2 SECONDS
+
+/obj/effect/temp_visual/telegraphing/circle/short
+ duration = 1 SECONDS
+
+/obj/effect/temp_visual/telegraphing/line
+ icon = 'icons/mob/telegraphing/telegraph.dmi'
+ icon_state = "line"
+ duration = 0.8 SECONDS
+
+/obj/effect/temp_visual/telegraphing/line/short
+ duration = 0.5 SECONDS
diff --git a/code/datums/bodypart_overlays/bodypart_overlay.dm b/code/datums/bodypart_overlays/bodypart_overlay.dm
index 19bf44f39a32..164c6908cb2e 100644
--- a/code/datums/bodypart_overlays/bodypart_overlay.dm
+++ b/code/datums/bodypart_overlays/bodypart_overlay.dm
@@ -90,7 +90,7 @@
CRASH("External organ color set to override with no override proc.")
///Generate a unique identifier to cache with. If you change something about the image, but the icon cache stays the same, it'll simply pull the unchanged image out of the cache
-/datum/bodypart_overlay/proc/generate_icon_cache()
+/datum/bodypart_overlay/proc/generate_icon_cache(obj/item/bodypart/limb)
return list()
/// Additionally color or texture the limb
diff --git a/code/datums/bodypart_overlays/markings_bodypart_overlay.dm b/code/datums/bodypart_overlays/markings_bodypart_overlay.dm
index 0cc780794f11..efa407c57e36 100644
--- a/code/datums/bodypart_overlays/markings_bodypart_overlay.dm
+++ b/code/datums/bodypart_overlays/markings_bodypart_overlay.dm
@@ -28,9 +28,8 @@
icon_state = accessory.icon_state
use_gender = accessory.gender_specific
draw_color = accessory.color_src ? set_color : null
- cache_key = jointext(generate_icon_cache(), "_")
-/datum/bodypart_overlay/simple/body_marking/generate_icon_cache()
+/datum/bodypart_overlay/simple/body_marking/generate_icon_cache(obj/item/bodypart/limb)
. = ..()
. += use_gender
. += draw_color
diff --git a/code/datums/bodypart_overlays/mutant_bodypart_overlay.dm b/code/datums/bodypart_overlays/mutant_bodypart_overlay.dm
index 40b303b1a4f0..40b770d87bf9 100644
--- a/code/datums/bodypart_overlays/mutant_bodypart_overlay.dm
+++ b/code/datums/bodypart_overlays/mutant_bodypart_overlay.dm
@@ -72,22 +72,20 @@
/datum/bodypart_overlay/mutant/proc/get_base_icon_state()
return sprite_datum.icon_state
+///Used to build the final incon state for the sprite
+/datum/bodypart_overlay/mutant/proc/build_icon_state(image_layer, obj/item/bodypart/limb)
+ PROTECTED_PROC(TRUE)
+ var/gender_key = (sprite_datum.gender_specific && limb?.limb_gender) || "m" // Male is default because sprite accessories are so ancient they predate the concept of not hardcoding gender
+ var/base_state = get_base_icon_state()
+ var/layer_key = mutant_bodyparts_layertext(image_layer)
+ return "[gender_key]_[feature_key]_[base_state]_[layer_key]"
+
///Get the image we need to draw on the person. Called from get_overlay() which is called from _bodyparts.dm. Limb can be null
/datum/bodypart_overlay/mutant/get_image(image_layer, obj/item/bodypart/limb)
if(!sprite_datum)
CRASH("Trying to call get_image() on [type] while it didn't have a sprite_datum. This shouldn't happen, report it as soon as possible.")
- var/gender = limb?.limb_gender || "m"
- var/list/icon_state_builder = list()
- icon_state_builder += sprite_datum.gender_specific ? gender : "m" //Male is default because sprite accessories are so ancient they predate the concept of not hardcoding gender
- icon_state_builder += feature_key
- icon_state_builder += get_base_icon_state()
- icon_state_builder += mutant_bodyparts_layertext(image_layer)
-
- var/finished_icon_state = icon_state_builder.Join("_")
-
- var/mutable_appearance/appearance = mutable_appearance(sprite_datum.icon, finished_icon_state, layer = image_layer)
-
+ var/mutable_appearance/appearance = mutable_appearance(sprite_datum.icon, build_icon_state(image_layer, limb), layer = image_layer)
if(sprite_datum.center)
center_image(appearance, sprite_datum.dimension_x, sprite_datum.dimension_y)
@@ -102,20 +100,17 @@
///Change our accessory sprite, using the accesssory type. If you need to change the sprite for something, use simple_change_sprite()
/datum/bodypart_overlay/mutant/set_appearance(accessory_type)
sprite_datum = fetch_sprite_datum(accessory_type)
- cache_key = jointext(generate_icon_cache(), "_")
///In a lot of cases, appearances are stored in DNA as the Name, instead of the path. Use set_appearance instead of possible
/datum/bodypart_overlay/mutant/proc/set_appearance_from_name(accessory_name)
sprite_datum = fetch_sprite_datum_from_name(accessory_name)
- cache_key = jointext(generate_icon_cache(), "_")
///Generate a unique key based on our sprites. So that if we've aleady drawn these sprites, they can be found in the cache and wont have to be drawn again (blessing and curse, but mostly curse)
-/datum/bodypart_overlay/mutant/generate_icon_cache()
+/datum/bodypart_overlay/mutant/generate_icon_cache(obj/item/bodypart/limb)
. = list()
. += "[get_base_icon_state()]"
. += "[feature_key]"
. += "[dye_color || draw_color]"
- return .
///Return a dumb glob list for this specific feature (called from parse_sprite)
/datum/bodypart_overlay/mutant/proc/get_global_feature_list()
diff --git a/code/datums/bodypart_overlays/simple_bodypart_overlay.dm b/code/datums/bodypart_overlays/simple_bodypart_overlay.dm
index a8004811b1a1..fa5222f5adf1 100644
--- a/code/datums/bodypart_overlays/simple_bodypart_overlay.dm
+++ b/code/datums/bodypart_overlays/simple_bodypart_overlay.dm
@@ -15,10 +15,9 @@
overlay.color = draw_color
-/datum/bodypart_overlay/simple/generate_icon_cache()
+/datum/bodypart_overlay/simple/generate_icon_cache(obj/item/bodypart/limb)
. = ..()
-
- . += "[icon_state]"
+ . += icon_state
///A sixpack drawn on the chest
/datum/bodypart_overlay/simple/sixpack
diff --git a/code/datums/bodypart_overlays/texture_bodypart_overlay.dm b/code/datums/bodypart_overlays/texture_bodypart_overlay.dm
index 920c2973ac8d..23b8aa6b780b 100644
--- a/code/datums/bodypart_overlays/texture_bodypart_overlay.dm
+++ b/code/datums/bodypart_overlays/texture_bodypart_overlay.dm
@@ -16,9 +16,9 @@
/datum/bodypart_overlay/texture/modify_bodypart_appearance(datum/appearance)
appearance.add_filter("bodypart_texture_[texture_icon_state]", 1, layering_filter(icon = cached_texture_icon, blend_mode = BLEND_INSET_OVERLAY))
-/datum/bodypart_overlay/texture/generate_icon_cache()
+/datum/bodypart_overlay/texture/generate_icon_cache(obj/item/bodypart/limb)
. = ..()
- . += "[type]"
+ . += type
/datum/bodypart_overlay/texture/can_draw_on_bodypart(obj/item/bodypart/bodypart_owner, mob/living/carbon/owner, is_husked = FALSE)
if (!..())
diff --git a/code/datums/brain_damage/special.dm b/code/datums/brain_damage/special.dm
index 469a5b90562e..b4792fbd27c0 100644
--- a/code/datums/brain_damage/special.dm
+++ b/code/datums/brain_damage/special.dm
@@ -439,7 +439,7 @@
var/beepskys_cry = "Level 10 infraction alert!"
to_chat(victim, "[span_name("[name]")] exclaims, \"[span_robot("[beepskys_cry]")]")
if(victim.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat))
- victim.create_chat_message(src, raw_message = beepskys_cry, spans = list("robotic"))
+ victim.create_chat_message(src, raw_message = beepskys_cry, spans = list(SPAN_ROBOT))
// Used by Veteran Security Advisor job.
/datum/brain_trauma/special/ptsd
diff --git a/code/datums/browser.dm b/code/datums/browser.dm
index a143be585013..d16b2f93cabc 100644
--- a/code/datums/browser.dm
+++ b/code/datums/browser.dm
@@ -260,7 +260,7 @@
if (focused_window)
winset(user, focused_window, "focus=true")
else
- winset(user, "mapwindow", "focus=true")
+ winset(user, SKIN_MAPWINDOW, "focus=true")
break
if (timeout)
addtimer(CALLBACK(src, PROC_REF(close)), timeout)
diff --git a/code/datums/components/_component.dm b/code/datums/components/_component.dm
index 5c93b9833f8f..ec60916b8acb 100644
--- a/code/datums/components/_component.dm
+++ b/code/datums/components/_component.dm
@@ -237,11 +237,11 @@
/datum/component/proc/_GetInverseTypeList(our_type = type)
//we can do this one simple trick
. = list(our_type)
- var/current_type = parent_type
+ var/datum/current_type = parent_type
//and since most components are root level + 1, this won't even have to run
while (current_type != /datum/component)
. += current_type
- current_type = type2parent(current_type)
+ current_type = current_type::parent_type
// The type arg is casted so initial works, you shouldn't be passing a real instance into this
/**
@@ -370,9 +370,12 @@
if((source in old_component.sources) && !old_component.allow_source_update(source))
return old_component // source already registered, no work to do
- if(old_component.on_source_add(arglist(list(source) + raw_args.Copy(2))) == COMPONENT_INCOMPATIBLE)
+ var/source_result = old_component.on_source_add(arglist(list(source) + raw_args.Copy(2)))
+ if(source_result == COMPONENT_INCOMPATIBLE)
stack_trace("incompatible source added to a [old_component.type]. Args: [json_encode(raw_args)]")
return null
+ if(source_result == COMPONENT_REDUNDANT)
+ return null
else if(!new_component)
new_component = new component_type(raw_args) // There's a valid dupe mode but there's no old component, act like normal
diff --git a/code/datums/components/ai_listen_to_weather.dm b/code/datums/components/ai_listen_to_weather.dm
index a7bb95ee8c13..29bcda3839b5 100644
--- a/code/datums/components/ai_listen_to_weather.dm
+++ b/code/datums/components/ai_listen_to_weather.dm
@@ -7,7 +7,7 @@
///what blackboard key are we setting
var/weather_key
-/datum/component/ai_listen_to_weather/Initialize(weather_type = /datum/weather/ash_storm, weather_key = BB_STORM_APPROACHING)
+/datum/component/ai_listen_to_weather/Initialize(weather_type = /datum/weather/particle/ash_storm, weather_key = BB_STORM_APPROACHING)
if(!isliving(parent))
return COMPONENT_INCOMPATIBLE
src.weather_type = weather_type
diff --git a/code/datums/components/ai_retaliate_advanced.dm b/code/datums/components/ai_retaliate_advanced.dm
index d734fa92b3cb..12e22c3d3956 100644
--- a/code/datums/components/ai_retaliate_advanced.dm
+++ b/code/datums/components/ai_retaliate_advanced.dm
@@ -33,5 +33,5 @@
if (!victim.ai_controller)
return
- victim.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
+ victim.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)
post_retaliate_callback?.InvokeAsync(attacker)
diff --git a/code/datums/components/bane.dm b/code/datums/components/bane.dm
new file mode 100644
index 000000000000..08d3feac17be
--- /dev/null
+++ b/code/datums/components/bane.dm
@@ -0,0 +1,199 @@
+/**
+ * ## Bane component
+ *
+ * Applied to items and projectiles that modifies damage dealt to certain things.
+ * For example a sword that deals 2x damage to specifically skeletons.
+ *
+ * For items, you are not limited to only dealing more damage. Supports smaller multipliers or removing flat damage.
+ * For projectiles, you are limited to only dealing more damage, at least until further refactors are done.
+ */
+/datum/component/bane
+ dupe_mode = COMPONENT_DUPE_ALLOWED
+ /// Callback invoked when checking if a target is valid to be baned (arguments: target)
+ /// Return a boolean, FALSE to stop the bane effect or TRUE to allow it
+ VAR_FINAL/datum/callback/should_bane_callback
+ /// Callback invoked before bane damage is applied, allows for modifying the damage or applying other effects. (arguments: target, attacker, damage_modifiers)
+ /// Return value doesn't matter, but you can modify the damage_modifiers list to change the attack's values
+ VAR_FINAL/datum/callback/pre_bane_callback
+ /// Callback invoked after bane damage is applied, allows for applying other effects. (arguments: target, attacker)
+ /// Return value doesn't matter
+ VAR_FINAL/datum/callback/on_bane_callback
+ /// A bitfield of mob biotypes that this bane component applies to.
+ ///If NONE, applies to all biotypes. Defaults to NONE.
+ VAR_FINAL/affected_biotypes = NONE
+ /// Multiplier applied to damage when the bane effect applies. Defaults to 1 (no change).
+ VAR_FINAL/damage_multiplier = 1
+ /// Flat damage added when the bane effect applies. Defaults to 0 (no change).
+ VAR_FINAL/added_damage = 0
+ /// Optional text to show in the weapon label readout, if not set it will generate a generic one based on the biotypes
+ VAR_FINAL/label_text = ""
+
+/datum/component/bane/Initialize(
+ damage_multiplier = 1,
+ added_damage = 0,
+ affected_biotypes = NONE,
+ datum/callback/should_bane_callback,
+ datum/callback/pre_bane_callback,
+ datum/callback/on_bane_callback,
+ label_text = "",
+)
+ if(!isitem(parent) && !isprojectile(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.damage_multiplier = damage_multiplier
+ src.added_damage = added_damage
+ src.affected_biotypes = affected_biotypes
+ src.should_bane_callback = should_bane_callback
+ src.pre_bane_callback = pre_bane_callback
+ src.on_bane_callback = on_bane_callback
+ src.label_text = label_text
+
+/datum/component/bane/RegisterWithParent()
+ if(isitem(parent))
+ RegisterSignal(parent, COMSIG_ITEM_WEAPON_LABEL_READOUT, PROC_REF(label_readout))
+ RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, PROC_REF(pre_attack))
+ RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, PROC_REF(after_attack))
+ RegisterSignal(parent, COMSIG_MOVABLE_IMPACT_ZONE, PROC_REF(on_thrown_hit))
+ if(isprojectile(parent))
+ RegisterSignal(parent, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(projectile_hit))
+
+/datum/component/bane/UnregisterFromParent()
+ if(isitem(parent))
+ UnregisterSignal(parent, COMSIG_ITEM_WEAPON_LABEL_READOUT)
+ UnregisterSignal(parent, COMSIG_ITEM_PRE_ATTACK)
+ UnregisterSignal(parent, COMSIG_ITEM_AFTERATTACK)
+ UnregisterSignal(parent, COMSIG_MOVABLE_IMPACT_ZONE)
+ if(isprojectile(parent))
+ UnregisterSignal(parent, COMSIG_PROJECTILE_SELF_ON_HIT)
+
+/datum/component/bane/proc/is_bane_target(atom/target)
+ if(!isliving(target))
+ return FALSE
+
+ var/mob/living/living_target = target
+ if(affected_biotypes && !(living_target.mob_biotypes & affected_biotypes))
+ return FALSE
+
+ return isnull(should_bane_callback) ? TRUE : should_bane_callback.Invoke(target)
+
+/datum/component/bane/proc/label_readout(obj/item/source, list/readout)
+ SIGNAL_HANDLER
+
+ if(damage_multiplier == 1 && added_damage == 0 && !label_text)
+ return
+
+ var/label_line = ""
+ if(damage_multiplier > 1 || added_damage > 0)
+ label_line += "It is especially effective against"
+ else if(damage_multiplier < 1 || added_damage < 0)
+ label_line += "It is less effective against"
+ else
+ label_line += "It interacts uniquely with" // for custom behaviors?
+
+ label_line += " "
+ if(label_text)
+ label_line += label_text
+ else if(affected_biotypes)
+ var/list/affected_biotypes_readable = bitfield_to_list(affected_biotypes, MOB_BIOTYPES_READABLE)
+ label_line += "[english_list(affected_biotypes_readable)] enemies"
+ else
+ label_line += "certain enemies"
+
+ if(damage_multiplier > 1 || added_damage > 0)
+ var/magnitude = ""
+ switch(max(damage_multiplier, added_damage / 10))
+ if(3 to INFINITY)
+ magnitude = "massively increased"
+ if(2 to 3)
+ magnitude = "greatly increased"
+ if(1.5 to 2)
+ magnitude = "significantly increased"
+ if(1 to 1.5)
+ magnitude = "slightly increased"
+
+ label_line += ", dealing [span_warning(magnitude)] damage per hit"
+
+ else if(damage_multiplier < 1 || added_damage < 0)
+ var/magnitude = ""
+ switch(min(damage_multiplier, 10 / abs(added_damage || 1)))
+ if(-INFINITY to 0)
+ magnitude = "no"
+ if(0 to 0.3)
+ magnitude = "massively reduced"
+ if(0.3 to 0.6)
+ magnitude = "greatly reduced"
+ if(0.6 to 0.9)
+ magnitude = "significantly reduced"
+ if(0.9 to 1)
+ magnitude = "slightly reduced"
+
+ label_line += ", dealing [span_warning(magnitude)] damage per hit"
+
+ label_line += "."
+ readout += label_line
+
+// Item attack handling
+/datum/component/bane/proc/pre_attack(datum/source, atom/target, mob/living/attacker, list/modifiers, list/attack_modifiers)
+ SIGNAL_HANDLER
+
+ if(!is_bane_target(target))
+ return
+
+ if(added_damage != 0)
+ MODIFY_ATTACK_FORCE(attack_modifiers, added_damage)
+ if(damage_multiplier != 1)
+ MODIFY_ATTACK_FORCE_MULTIPLIER(attack_modifiers, damage_multiplier)
+
+ pre_bane_callback?.Invoke(target, attacker, attack_modifiers)
+
+/datum/component/bane/proc/after_attack(datum/source, atom/target, mob/living/attacker, ...)
+ SIGNAL_HANDLER
+
+ if(!is_bane_target(target))
+ return
+
+ SEND_SIGNAL(target, COMSIG_LIVING_BANED, source, attacker)
+ on_bane_callback?.Invoke(target, attacker)
+
+// Throw impact handling
+/datum/component/bane/proc/on_thrown_hit(datum/source, atom/hit_atom, hit_zone, blocked, datum/thrownthing/throwingdatum)
+ SIGNAL_HANDLER
+
+ if(!is_bane_target(hit_atom))
+ return
+
+ var/mob/thrower = throwingdatum?.thrower?.resolve()
+ var/list/damage_modifiers = list("[FORCE_MULTIPLIER]" = damage_multiplier, "[FORCE_MODIFIER]" = added_damage)
+ pre_bane_callback?.Invoke(hit_atom, thrower, damage_modifiers)
+
+ // We're not modifying the throwforce of the item we're just applying more damage as a separate damage event
+ // That's why we do damage_multiplier - 1 (so that a 1.5x multiplier would apply 0.5x damage here for a total of 1.5x)
+ var/obj/item/throwing_item = parent
+ var/extra_damage = (throwing_item.throwforce * max(0, damage_modifiers[FORCE_MULTIPLIER] - 1)) + damage_modifiers[FORCE_MODIFIER]
+ if(extra_damage > 0)
+ var/mob/living/living_target = hit_atom // safe assertion from is_bane_target
+ living_target.apply_damage(extra_damage, throwing_item.damtype, hit_zone, blocked)
+
+ SEND_SIGNAL(hit_atom, COMSIG_LIVING_BANED, thrower, hit_atom)
+ on_bane_callback?.Invoke(hit_atom, thrower)
+
+// Projectile hit handling
+/datum/component/bane/proc/projectile_hit(datum/source, atom/firer, atom/target, angle, hit_zone, blocked, ...)
+ SIGNAL_HANDLER
+
+ if(!is_bane_target(target))
+ return
+
+ var/list/damage_modifiers = list("[FORCE_MULTIPLIER]" = damage_multiplier, "[FORCE_MODIFIER]" = added_damage)
+ pre_bane_callback?.Invoke(target, firer, damage_modifiers)
+
+ // We're not modifying the projectile damage we're just applying more damage as a separate damage event
+ // That's why we do damage_multiplier - 1 (so that a 1.5x multiplier would apply 0.5x damage here for a total of 1.5x)
+ var/obj/projectile/projectile_owner = parent
+ var/extra_damage = (projectile_owner.damage * max(0, damage_modifiers[FORCE_MULTIPLIER] - 1)) + damage_modifiers[FORCE_MODIFIER]
+ if(extra_damage > 0)
+ var/mob/living/living_target = target // safe assertion from is_bane_target
+ living_target.apply_damage(extra_damage, projectile_owner.damage_type, hit_zone, blocked)
+
+ SEND_SIGNAL(target, COMSIG_LIVING_BANED, firer, target)
+ on_bane_callback?.Invoke(target, firer)
diff --git a/code/datums/components/blob_minion.dm b/code/datums/components/blob_minion.dm
index 8e8f796d3ae1..6549410f4a4b 100644
--- a/code/datums/components/blob_minion.dm
+++ b/code/datums/components/blob_minion.dm
@@ -74,7 +74,6 @@
var/mob/living/living_parent = parent
living_parent.add_faction(ROLE_BLOB)
ADD_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
- remove_verb(parent, /mob/living/verb/pulled) // No dragging people into the blob
RegisterSignal(parent, COMSIG_MOB_MIND_INITIALIZED, PROC_REF(on_mind_init))
RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON, PROC_REF(on_update_appearance))
RegisterSignal(parent, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(on_update_status_tab))
@@ -98,7 +97,6 @@
living_parent.pass_flags &= ~PASSBLOB
living_parent.remove_faction(ROLE_BLOB)
REMOVE_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
- add_verb(parent, /mob/living/verb/pulled)
UnregisterSignal(parent, list(
COMSIG_ATOM_BLOB_ACT,
COMSIG_ATOM_FIRE_ACT,
diff --git a/code/datums/components/butchering.dm b/code/datums/components/butchering.dm
index bdc41329a744..0ccaa013943d 100644
--- a/code/datums/components/butchering.dm
+++ b/code/datums/components/butchering.dm
@@ -318,6 +318,7 @@
wound.remove_wound()
wound.apply_wound(replacement, silent = TRUE)
+ SEND_SIGNAL(target, COMSIG_BODYPART_BUTCHERED, replacement)
return replacement
/datum/component/butchering/proc/start_butcher(obj/item/source, mob/living/target, mob/living/user)
diff --git a/code/datums/components/crafting/ranged_weapon.dm b/code/datums/components/crafting/ranged_weapon.dm
index 9c7c65351cc8..d25fca338a74 100644
--- a/code/datums/components/crafting/ranged_weapon.dm
+++ b/code/datums/components/crafting/ranged_weapon.dm
@@ -302,8 +302,8 @@
return ..()
/datum/crafting_recipe/deagle_prime_mag
- name = "Regal Condor Magazine (10mm Reaper)"
- result = /obj/item/ammo_box/magazine/r10mm
+ name = "Regal Condor Magazine (.45 Reaper)"
+ result = /obj/item/ammo_box/magazine/r45
reqs = list(
/obj/item/stack/sheet/iron = 10,
/obj/item/stack/sheet/mineral/gold = 10,
diff --git a/code/datums/components/crafting/tools.dm b/code/datums/components/crafting/tools.dm
index 964a2be5d556..460da3101487 100644
--- a/code/datums/components/crafting/tools.dm
+++ b/code/datums/components/crafting/tools.dm
@@ -132,6 +132,10 @@
)
category = CAT_TOOLS
+/datum/crafting_recipe/jaws_of_recovery/New()
+ LAZYADD(blacklist, typecacheof(/obj/item/crowbar/power/paramedic, ignore_root_path = TRUE))
+ return ..()
+
/datum/crafting_recipe/lantern
name = "Lantern"
result = /obj/item/flashlight/lantern
diff --git a/code/datums/components/digitigrade_linb.dm b/code/datums/components/digitigrade_linb.dm
new file mode 100644
index 000000000000..6b7776474b5c
--- /dev/null
+++ b/code/datums/components/digitigrade_linb.dm
@@ -0,0 +1,142 @@
+/// Updates the limb id of a bodypart if the mob is wearing digitigrade squishing clothing
+/datum/component/digitigrade_limb
+ /// Id when wearing digitigrade squishing clothing
+ var/squashed_id
+ /// Id when not wearing digitigrade squishing clothing
+ var/free_id
+
+ /// Lazylist of refs that are squishing this limb
+ VAR_PRIVATE/list/squashing_us
+
+/datum/component/digitigrade_limb/Initialize(squashed_id, free_id)
+ if(!istype(parent, /obj/item/bodypart/leg))
+ return COMPONENT_INCOMPATIBLE
+
+ var/obj/item/bodypart/limb = parent
+ limb.bodytype |= BODYTYPE_DIGITIGRADE
+
+ src.squashed_id = squashed_id
+ src.free_id = free_id || initial(limb.limb_id)
+
+ RegisterSignal(parent, COMSIG_BODYPART_UPDATED, PROC_REF(update_limb_id_comsig))
+ RegisterSignal(parent, COMSIG_BODYPART_ATTACHED, PROC_REF(on_attach))
+ RegisterSignal(parent, COMSIG_BODYPART_REMOVED, PROC_REF(on_remove))
+ RegisterSignal(parent, COMSIG_BODYPART_BUTCHERED, PROC_REF(on_butchered))
+
+ if(ishuman(limb.owner))
+ on_attach(limb, limb.owner)
+
+/datum/component/digitigrade_limb/Destroy()
+ UnregisterSignal(parent, COMSIG_BODYPART_UPDATED)
+ UnregisterSignal(parent, COMSIG_BODYPART_ATTACHED)
+ UnregisterSignal(parent, COMSIG_BODYPART_REMOVED)
+
+ UnregisterSignal(parent, COMSIG_BODYPART_BUTCHERED)
+ var/obj/item/bodypart/limb = parent
+ if(!QDELING(parent) && ishuman(limb.owner))
+ on_remove(limb, limb.owner)
+ limb.bodytype &= ~BODYTYPE_DIGITIGRADE
+ return ..()
+
+/datum/component/digitigrade_limb/proc/on_attach(obj/item/bodypart/limb, mob/living/carbon/new_limb_owner)
+ SIGNAL_HANDLER
+
+ RegisterSignal(new_limb_owner, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(equipped_item))
+ RegisterSignal(new_limb_owner, COMSIG_MOB_DROPPED_ITEM, PROC_REF(unequipped_item))
+ RegisterSignal(new_limb_owner, COMSIG_CARBON_ITEM_COVERAGE_CHANGED, PROC_REF(coverage_changed))
+ for(var/obj/item/equipped as anything in new_limb_owner.get_equipped_items())
+ if(equipped_item(new_limb_owner, equipped, new_limb_owner.get_slot_by_item(equipped)))
+ LAZYOR(squashing_us, REF(equipped))
+
+/datum/component/digitigrade_limb/proc/on_remove(obj/item/bodypart/limb, mob/living/carbon/old_limb_owner)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(old_limb_owner, COMSIG_MOB_EQUIPPED_ITEM)
+ UnregisterSignal(old_limb_owner, COMSIG_MOB_DROPPED_ITEM)
+ UnregisterSignal(old_limb_owner, COMSIG_CARBON_ITEM_COVERAGE_CHANGED)
+ LAZYNULL(squashing_us)
+ update_limb_id()
+
+/datum/component/digitigrade_limb/proc/equipped_item(mob/living/carbon/equipper, obj/item/equipped_item, slot)
+ SIGNAL_HANDLER
+
+ if((slot & equipped_item.slot_flags) && item_squishes_limb(equipped_item, equipper))
+ LAZYOR(squashing_us, REF(equipped_item))
+ update_limb_id()
+
+/datum/component/digitigrade_limb/proc/unequipped_item(mob/living/carbon/equipper, obj/item/equipped_item)
+ SIGNAL_HANDLER
+
+ LAZYREMOVE(squashing_us, REF(equipped_item))
+ update_limb_id()
+
+// Snowflake case for updating whether our jumpsuit should squish us
+/datum/component/digitigrade_limb/proc/coverage_changed(mob/living/carbon/equipper, added_slots, removed_slots)
+ SIGNAL_HANDLER
+
+ if(!((added_slots|removed_slots) & HIDEJUMPSUIT))
+ return
+
+ var/obj/item/clothing/uniform = equipper.get_item_by_slot(ITEM_SLOT_ICLOTHING)
+ if(isnull(uniform))
+ return
+
+ var/uniform_ref = REF(uniform)
+ if(uniform_ref in squashing_us)
+ if(added_slots & HIDEJUMPSUIT)
+ LAZYREMOVE(squashing_us, uniform_ref)
+ update_limb_id()
+
+ else
+ if((removed_slots & HIDEJUMPSUIT) && item_squishes_limb(uniform, equipper))
+ LAZYOR(squashing_us, uniform_ref)
+ update_limb_id()
+
+/datum/component/digitigrade_limb/proc/item_squishes_limb(obj/item/equipped_item, mob/living/carbon/equipper)
+ if(!(equipped_item.body_parts_covered & (LEGS|FEET)))
+ return FALSE
+
+ switch(equipper.get_slot_by_item(equipped_item))
+ if(ITEM_SLOT_FEET, ITEM_SLOT_OCLOTHING)
+ return !(equipped_item.supports_variations_flags & DIGITIGRADE_VARIATIONS)
+ if(ITEM_SLOT_ICLOTHING) // If the jumpsuit is obscured, it shouldn't contribute to squishing
+ return !(equipped_item.supports_variations_flags & DIGITIGRADE_VARIATIONS) && !(equipper.obscured_slots & HIDEJUMPSUIT)
+
+ return FALSE
+
+/// This is just ran on update_limb() to ensure we always have the correct ID
+/datum/component/digitigrade_limb/proc/update_limb_id_comsig()
+ SIGNAL_HANDLER
+ update_limb_id(sprite_update = FALSE)
+
+/// Digitigrade limbs that are butchered add the component to the replacement limb
+/datum/component/digitigrade_limb/proc/on_butchered(datum/source, obj/item/bodypart/replacement)
+ SIGNAL_HANDLER
+ squashed_id = "[initial(replacement.limb_id)]_[BODYPART_ID_DIGITIGRADE]"
+ free_id = initial(replacement.limb_id)
+ replacement.TakeComponent(src)
+
+/datum/component/digitigrade_limb/proc/update_limb_id(sprite_update = TRUE)
+ var/obj/item/bodypart/limb = parent
+ var/old_id = limb.limb_id
+ if(LAZYLEN(squashing_us))
+ limb.limb_id = squashed_id
+ limb.remove_bodyshape(BODYSHAPE_DIGITIGRADE)
+
+ else
+ limb.limb_id = free_id
+ limb.add_bodyshape(BODYSHAPE_DIGITIGRADE)
+
+ if(!sprite_update || old_id == limb.limb_id)
+ return
+
+ if(isnull(limb.owner))
+ limb.update_icon_dropped()
+ return
+
+ // Ensures any items that with a variation are updated
+ for(var/obj/item/thing as anything in limb.owner.get_equipped_items(INCLUDE_PROSTHETICS|INCLUDE_ABSTRACT))
+ if(thing.supports_variations_flags & DIGITIGRADE_VARIATIONS)
+ thing.update_slot_icon()
+ // Updates underwear and mob sprites
+ limb.owner.update_body()
diff --git a/code/datums/components/fantasy/suffixes.dm b/code/datums/components/fantasy/suffixes.dm
index 2b25e442b236..20a55c04e29c 100644
--- a/code/datums/components/fantasy/suffixes.dm
+++ b/code/datums/components/fantasy/suffixes.dm
@@ -58,7 +58,6 @@
name = "of slaying (random species, carbon or simple animal)"
placement = AFFIX_SUFFIX
alignment = AFFIX_GOOD
- var/list/target_types_by_comp = list()
/datum/fantasy_affix/bane/apply(datum/component/fantasy/comp, newName)
. = ..()
@@ -83,16 +82,9 @@
// This works even with the species picks since we're only accessing the name
var/obj/item/master = comp.parent
- master.AddElement(/datum/element/bane, target_type = picked_mobtype)
- target_types_by_comp[comp] = picked_mobtype
+ comp.appliedComponents += master.AddComponent(/datum/component/bane, affected_biotypes = picked_mobtype, damage_multiplier = 2)
return "[newName] of [initial(picked_mobtype.name)] slaying"
-/datum/fantasy_affix/bane/remove(datum/component/fantasy/comp)
- var/picked_mobtype = target_types_by_comp[comp]
- var/obj/item/master = comp.parent
- master.RemoveElement(/datum/element/bane, picked_mobtype)
- target_types_by_comp -= comp
-
/datum/fantasy_affix/summoning
name = "of summoning (dangerous, can pick all but megafauna tier stuff)"
placement = AFFIX_SUFFIX
diff --git a/code/datums/components/force_move.dm b/code/datums/components/force_move.dm
index 62fe26d71f7d..c69fa0d6c5e1 100644
--- a/code/datums/components/force_move.dm
+++ b/code/datums/components/force_move.dm
@@ -10,6 +10,8 @@
var/mob/mob_parent = parent
var/dist = get_dist(mob_parent, target)
var/datum/move_loop/loop = GLOB.move_manager.move_towards(mob_parent, target, delay = 1, timeout = dist)
+ if(!loop)
+ return COMPONENT_INCOMPATIBLE
RegisterSignal(mob_parent, COMSIG_MOB_CLIENT_PRE_LIVING_MOVE, PROC_REF(stop_move))
RegisterSignal(mob_parent, COMSIG_ATOM_PRE_PRESSURE_PUSH, PROC_REF(stop_pressure))
if(spin)
diff --git a/code/datums/components/ghostrole_on_revive.dm b/code/datums/components/ghostrole_on_revive.dm
index 52ea1d0bd073..ef64f42fb74c 100644
--- a/code/datums/components/ghostrole_on_revive.dm
+++ b/code/datums/components/ghostrole_on_revive.dm
@@ -87,11 +87,13 @@
/datum/component/ghostrole_on_revive/proc/prepare_brain(obj/item/organ/source)
ADD_TRAIT(source, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
RegisterSignal(source, COMSIG_ORGAN_IMPLANTED, PROC_REF(prepare_mob_from_brain))
+ RegisterSignal(source, COMSIG_ATOM_EXAMINE, PROC_REF(brain_examine))
UnregisterSignal(source, COMSIG_ORGAN_REMOVED)
/datum/component/ghostrole_on_revive/proc/unprepare_brain(obj/item/organ/source)
REMOVE_TRAIT(source, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
UnregisterSignal(source, COMSIG_ORGAN_IMPLANTED)
+ UnregisterSignal(source, COMSIG_ATOM_EXAMINE)
RegisterSignal(source, COMSIG_ORGAN_REMOVED, PROC_REF(prepare_brain))
source.owner?.med_hud_set_status()
@@ -102,6 +104,11 @@
UnregisterSignal(source, COMSIG_ORGAN_IMPLANTED)
prepare_mob(owner)
+/datum/component/ghostrole_on_revive/proc/brain_examine(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+
+ examine_list += span_info("Another soul may take [source.p_their()] place if put in a body...")
+
/datum/component/ghostrole_on_revive/proc/on_revive(mob/living/source)
SIGNAL_HANDLER
diff --git a/code/datums/components/hide_weather_planes.dm b/code/datums/components/hide_weather_planes.dm
index 5b7addb199e7..7a7cb3655b8f 100644
--- a/code/datums/components/hide_weather_planes.dm
+++ b/code/datums/components/hide_weather_planes.dm
@@ -8,17 +8,20 @@
dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
var/list/datum/weather/active_weather = list()
var/list/atom/movable/screen/plane_master/plane_masters = list()
+ /// Do we care about all weather or only particle weather?
+ var/particle_only = FALSE
-/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about)
+/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about, particle_only = FALSE)
if(!istype(parent, /datum/plane_master_group))
return COMPONENT_INCOMPATIBLE
+ src.particle_only = particle_only
var/datum/plane_master_group/home = parent
plane_masters += care_about
RegisterSignal(care_about, COMSIG_QDELETING, PROC_REF(plane_master_deleted))
var/list/starting_signals = list()
var/list/ending_signals = list()
- for(var/datum/weather/weather_type as anything in typesof(/datum/weather))
+ for(var/datum/weather/weather_type as anything in valid_subtypesof(particle_only ? /datum/weather/particle : /datum/weather))
starting_signals += COMSIG_WEATHER_TELEGRAPH(weather_type)
ending_signals += COMSIG_WEATHER_END(weather_type)
@@ -103,6 +106,8 @@
var/list/connected_levels = SSmapping.get_connected_levels(new_z)
for(var/datum/weather/active as anything in SSweather.processing)
+ if(particle_only && !istype(active, /datum/weather/particle))
+ continue
if(length(connected_levels & active.impacted_z_levels))
active_weather += WEAKREF(active)
diff --git a/code/datums/components/hitsplat.dm b/code/datums/components/hitsplat.dm
new file mode 100644
index 000000000000..89de3d1fa65a
--- /dev/null
+++ b/code/datums/components/hitsplat.dm
@@ -0,0 +1,177 @@
+/**
+ * Component to display hitsplats
+ */
+/datum/component/hitsplat
+ ///positions of all hitsplats
+ var/list/hitsplat_positions = list(
+ list(0, -2) = null,
+ list(0, 10) = null,
+ list(10, 0) = null,
+ list(-10, 0) = null,
+ list(10, 12) = null,
+ list(-10, 12) = null,
+ list(0, 22) = null,
+ )
+
+ var/obj/effect/overlay/vis/hitsplat/hitsplat_type = /obj/effect/overlay/vis/hitsplat/debugging
+ // Alot less spammy and more useable as a smite or ingame feature. Off for ALL the health adjustments
+ var/only_attacks = FALSE
+
+/datum/component/hitsplat/Initialize(hitsplat_type, only_attacks)
+ if(!ismob(parent))
+ return COMPONENT_INCOMPATIBLE
+ if(hitsplat_type)
+ src.hitsplat_type = hitsplat_type
+ src.only_attacks = only_attacks
+
+/datum/component/hitsplat/RegisterWithParent()
+ if(only_attacks)
+ RegisterSignal(parent, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(on_attacked))
+ RegisterSignal(parent, COMSIG_ATOM_AFTER_ATTACKEDBY, PROC_REF(after_attackby))
+ else
+ RegisterSignals(parent, COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES, PROC_REF(on_damage_adjusted))
+ RegisterSignal(parent, COMSIG_CARBON_LIMB_DAMAGED, PROC_REF(on_limb_damage))
+
+/datum/component/hitsplat/UnregisterFromParent()
+ if(only_attacks)
+ UnregisterSignal(parent, list(COMSIG_MOB_APPLY_DAMAGE, COMSIG_ATOM_AFTER_ATTACKEDBY))
+ else
+ UnregisterSignal(parent, list(COMSIG_CARBON_LIMB_DAMAGED) + COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES)
+
+
+/datum/component/hitsplat/proc/after_attackby(atom/target, obj/item/weapon)
+ SIGNAL_HANDLER
+ if(weapon.force) //will be handled by on_attacked
+ return NONE
+ spawn_hitsplat(0)
+
+/datum/component/hitsplat/proc/on_attacked(mob/source, damage_amount, damagetype, def_zone, blocked)
+ SIGNAL_HANDLER
+
+ if(damagetype == STAMINA || damage_amount < 0)
+ return NONE
+ spawn_hitsplat(damage_amount, damagetype)
+
+/datum/component/hitsplat/proc/on_damage_adjusted(mob/source, type, amount)
+ SIGNAL_HANDLER
+
+ if(type == STAMINA)
+ return NONE
+ spawn_hitsplat(amount, type)
+
+/datum/component/hitsplat/proc/on_limb_damage(mob/living/our_mob, limb, brute, burn)
+ SIGNAL_HANDLER
+
+ if(brute)
+ spawn_hitsplat(brute, BRUTE)
+ if(burn)
+ spawn_hitsplat(burn, BURN)
+
+/datum/component/hitsplat/proc/spawn_hitsplat(amount, type)
+ var/obj/effect/overlay/vis/hitsplat/new_hitsplat = new hitsplat_type()
+ new_hitsplat.set_damage_amount(amount, type)
+ add_hitsplat(new_hitsplat)
+
+/datum/component/hitsplat/proc/add_hitsplat(obj/effect/new_hitsplat)
+
+ RegisterSignal(new_hitsplat, COMSIG_QDELETING, PROC_REF(on_hitsplat_delete))
+ var/mob/living_parent = parent
+ living_parent.vis_contents += new_hitsplat
+
+ for(var/list/hitsplat in hitsplat_positions)
+ if(!isnull(hitsplat_positions[hitsplat]))
+ continue
+ hitsplat_positions[hitsplat] = new_hitsplat
+ new_hitsplat.pixel_w = hitsplat[1]
+ new_hitsplat.pixel_z = hitsplat[2]
+ return
+
+ var/list/first_hitsplat = hitsplat_positions[1]
+ qdel(hitsplat_positions[first_hitsplat])
+ hitsplat_positions[first_hitsplat] = new_hitsplat
+ new_hitsplat.pixel_w = first_hitsplat[1]
+ new_hitsplat.pixel_z = first_hitsplat[2]
+
+/datum/component/hitsplat/proc/on_hitsplat_delete(datum/source)
+ SIGNAL_HANDLER
+
+ for(var/list/hitsplat in hitsplat_positions)
+ if(hitsplat_positions[hitsplat] == source)
+ hitsplat_positions[hitsplat] = null
+
+
+/obj/effect/overlay/vis/hitsplat
+ name = "hitsplat"
+ icon = 'icons/effects/hitsplats.dmi'
+ icon_state = "hitsplat_default"
+ base_icon_state = "hitsplat"
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ layer = ABOVE_MOB_LAYER
+ vis_flags = VIS_INHERIT_PLANE
+ ///the damage amount we're displaying
+ var/damage_amount = 0
+ /// Percision of rounding for damage number
+ var/rounding = 1
+ /// Controls how accurate we try and be to runescape visuals, otherwise we go for consise debugging information
+ var/lore_accurate = TRUE
+ /// Color of text of image
+ var/text_color
+
+/obj/effect/overlay/vis/hitsplat/Initialize(mapload)
+ . = ..()
+ QDEL_IN(src, 2 SECONDS)
+
+/obj/effect/overlay/vis/hitsplat/proc/set_damage_amount(damage_number, damage_type)
+ damage_amount = damage_number
+
+ if(lore_accurate)
+ if(damage_amount < 0)
+ icon_state = "[base_icon_state]_heal"
+ else if(damage_amount == 0)
+ icon_state = "[base_icon_state]_blocked"
+ else if(damage_type == TOX)
+ icon_state = "[base_icon_state]_poison"
+ else if(damage_type == BURN)
+ icon_state = "[base_icon_state]_burn"
+ else
+ switch(damage_type)
+ if(BRUTE)
+ text_color = COLOR_RED
+ if(BURN)
+ text_color = COLOR_ORANGE
+ if(TOX)
+ text_color = COLOR_GREEN
+ if(OXY)
+ text_color = COLOR_BLUE
+ if(STAMINA)
+ text_color = COLOR_YELLOW
+
+ update_appearance(UPDATE_OVERLAYS)
+
+
+/obj/effect/overlay/vis/hitsplat/lore_accurate
+
+/obj/effect/overlay/vis/hitsplat/lore_accurate/update_overlays()
+ . = ..()
+ var/hitsplat_num = CEILING(abs(damage_amount), rounding)
+ var/image/hitsplat_text = image(loc = src, layer = layer + 0.1)
+ hitsplat_text.pixel_w = 1
+ hitsplat_text.pixel_z = 10
+ hitsplat_text.maptext = MAPTEXT("[hitsplat_num]")
+ . += hitsplat_text
+
+
+/obj/effect/overlay/vis/hitsplat/debugging
+ icon_state = null // Just the maptext
+ base_icon_state = null
+ rounding = 0.01
+ lore_accurate = FALSE
+
+/obj/effect/overlay/vis/hitsplat/debugging/update_overlays()
+ . = ..()
+ var/hitsplat_num = CEILING(damage_amount, rounding)
+ var/image/hitsplat_text = image(loc = src, layer = layer + 0.1)
+ hitsplat_text.pixel_w = 1
+ hitsplat_text.pixel_z = 10
+ hitsplat_text.maptext = MAPTEXT("[hitsplat_num]")
+ . += hitsplat_text
diff --git a/code/datums/components/ingredients_holder.dm b/code/datums/components/ingredients_holder.dm
index afa52f3fc538..e5c46d2896f5 100644
--- a/code/datums/components/ingredients_holder.dm
+++ b/code/datums/components/ingredients_holder.dm
@@ -72,6 +72,7 @@
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
RegisterSignal(parent, COMSIG_ATOM_EXITED, PROC_REF(food_exited))
RegisterSignal(parent, COMSIG_ATOM_PROCESSED, PROC_REF(on_processed))
+ RegisterSignal(parent, COMSIG_PIZZA_SLICE_TAKEN, PROC_REF(on_slice_taken))
RegisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_requesting_context_from_item))
ADD_TRAIT(parent, TRAIT_INGREDIENTS_HOLDER, INNATE_TRAIT)
@@ -81,6 +82,7 @@
COMSIG_ATOM_EXAMINE,
COMSIG_ATOM_EXITED,
COMSIG_ATOM_PROCESSED,
+ COMSIG_PIZZA_SLICE_TAKEN,
COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM,
))
REMOVE_TRAIT(parent, TRAIT_INGREDIENTS_HOLDER, INNATE_TRAIT)
@@ -287,6 +289,11 @@
for (var/atom/result as anything in results)
result.AddComponent(/datum/component/ingredients_holder, null, fill_type, ingredient_type = ingredient_type, max_ingredients = max_ingredients, processed_holder = src)
+/// Pizzas have unique slicing interaction so we need to do this
+/datum/component/ingredients_holder/proc/on_slice_taken(datum/source, mob/living/user, obj/item/slice)
+ SIGNAL_HANDLER
+ slice.AddComponent(/datum/component/ingredients_holder, null, fill_type, ingredient_type = ingredient_type, max_ingredients = max_ingredients, processed_holder = src)
+
/**
* Adds context sensitivy directly to the customizable reagent holder file for screentips
* Arguments:
diff --git a/code/datums/components/jetpack.dm b/code/datums/components/jetpack.dm
index c49c7d12154c..1ca91a9cb2ea 100644
--- a/code/datums/components/jetpack.dm
+++ b/code/datums/components/jetpack.dm
@@ -109,7 +109,7 @@
RegisterSignal(new_user, COMSIG_MOVABLE_SPACEMOVE, PROC_REF(stabilize))
if (effect_type)
setup_trail(new_user)
- new_user.inertia_move_multiplier /= drift_force // lower multiplier = faster drifting
+ new_user.inertia_move_multiplier_active /= drift_force // lower multiplier = faster drifting
/datum/component/jetpack/proc/deactivate(datum/source, mob/old_user)
SIGNAL_HANDLER
@@ -126,7 +126,7 @@
COMSIG_MOVABLE_SPACEMOVE,
))
QDEL_NULL(trail)
- old_user.inertia_move_multiplier *= drift_force
+ old_user.inertia_move_multiplier_active *= drift_force
/datum/component/jetpack/proc/move_react(mob/source)
SIGNAL_HANDLER
@@ -136,13 +136,22 @@
if(source.client.intended_direction && check_on_move.Invoke(TRUE)) //You use jet when press keys. yes.
trail?.generate_effect()
+/// Handles all active 0g movement, including both manual (trying to move a direction in 0g) and automatic (drifting idly in 0g)
/datum/component/jetpack/proc/stabilize(mob/source, movement_dir, continuous_move, backup)
SIGNAL_HANDLER
- if(!continuous_move && movement_dir)
- return COMSIG_MOVABLE_STOP_SPACEMOVE
- // Check if we have the fuel to stop this. Do NOT consume any fuel, just check
- // This is done because things other then us can use our fuel
- if(stabilize && check_on_move.Invoke(FALSE))
+ /*
+ * Checks if we should stop any active movement
+ *
+ * Obviously we stop all forms of drifting if we have stabilizers active (allowing free space movement)
+ * Less obviously, we stop need to stop drift if we are trying to move *while passively drifting*
+ * (Without checking the latter, jetpacks will act very weird and jank. As you move in one direction,
+ * you will simultaneously drift in that direction, causing you to jump/skip a tile every so often.)
+ *
+ * Either way, we need to check that we have the "fuel" to stop this
+ * DO NOT CONSUME FUEL HERE, just check if we have it
+ * This is done because things other then us can use our fuel
+ */
+ if((stabilize || (!continuous_move && movement_dir)) && check_on_move.Invoke(FALSE))
return COMSIG_MOVABLE_STOP_SPACEMOVE
return NONE
diff --git a/code/datums/components/leash.dm b/code/datums/components/leash.dm
index ef0b278c7992..2047d3ffb872 100644
--- a/code/datums/components/leash.dm
+++ b/code/datums/components/leash.dm
@@ -2,17 +2,17 @@
/// but teleporting if necessary.
/datum/component/leash
/// The owner of the leash. If this is qdeleted, the leash is as well.
- var/atom/movable/owner
-
+ var/atom/owner
/// The maximum distance you can move from your owner
var/distance
-
/// The object type to create on the old turf when forcibly teleporting out
var/force_teleport_out_effect
-
/// The object type to create on the new turf when forcibly teleporting out
var/force_teleport_in_effect
-
+ /// Avoid sending out "too far" bubbles
+ var/silent = FALSE
+ /// Should we snap instead of teleporting back?
+ var/snap_on_teleport = FALSE
VAR_PRIVATE
// Pathfinding can yield, so only move us closer if this is the best one
current_path_tick = 0
@@ -21,10 +21,12 @@
performing_path_move = FALSE
/datum/component/leash/Initialize(
- atom/movable/owner,
+ atom/owner,
distance = 3,
force_teleport_out_effect,
force_teleport_in_effect,
+ silent = FALSE,
+ snap_on_teleport = FALSE,
)
. = ..()
@@ -32,8 +34,8 @@
stack_trace("Parent must be a movable")
return COMPONENT_INCOMPATIBLE
- if (!ismovable(owner))
- stack_trace("[owner] (owner) is not a movable")
+ if (!isatom(owner))
+ stack_trace("[owner] (owner) is not an atom")
return COMPONENT_INCOMPATIBLE
if (!isnum(distance))
@@ -52,18 +54,22 @@
src.distance = distance
src.force_teleport_out_effect = force_teleport_out_effect
src.force_teleport_in_effect = force_teleport_in_effect
+ src.silent = silent
+ src.snap_on_teleport = snap_on_teleport
RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(on_owner_qdel))
+ RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_parent_pre_move))
+ check_distance()
+
+ if (!ismovable(owner))
+ return
+
var/static/list/container_connections = list(
COMSIG_MOVABLE_MOVED = PROC_REF(on_owner_moved),
)
-
AddComponent(/datum/component/connect_containers, owner, container_connections)
RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_owner_moved))
- RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_parent_pre_move))
-
- check_distance()
/datum/component/leash/Destroy()
owner = null
@@ -97,7 +103,7 @@
if (get_dist(new_location_turf, owner) <= distance)
return NONE
- if (ismob(source))
+ if (ismob(source) && !silent)
source.balloon_alert(source, "too far!")
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
@@ -124,7 +130,7 @@
var/list/path = get_path_to(parent, owner, mintargetdist = distance)
- if (last_completed_path_tick > our_path_tick)
+ if (last_completed_path_tick > our_path_tick || QDELETED(src))
return
last_completed_path_tick = our_path_tick
@@ -158,9 +164,11 @@
/datum/component/leash/proc/force_teleport_back(reason)
PRIVATE_PROC(TRUE)
- var/atom/movable/movable_parent = parent
+ if (snap_on_teleport)
+ qdel(src)
+ return
- SSblackbox.record_feedback("tally", "leash_force_teleport_back", 1, reason)
+ var/atom/movable/movable_parent = parent
if (force_teleport_out_effect)
new force_teleport_out_effect(movable_parent.loc)
@@ -171,7 +179,9 @@
new force_teleport_in_effect(movable_parent.loc)
if (ismob(movable_parent))
- movable_parent.balloon_alert(movable_parent, "moved out of range!")
+ SSblackbox.record_feedback("tally", "leash_force_teleport_back", 1, reason)
+ if(!silent)
+ movable_parent.balloon_alert(movable_parent, "moved out of range!")
SEND_SIGNAL(parent, COMSIG_LEASH_FORCE_TELEPORT)
diff --git a/code/datums/components/object_possession.dm b/code/datums/components/object_possession.dm
index 256f29526707..f13acee56c28 100644
--- a/code/datums/components/object_possession.dm
+++ b/code/datums/components/object_possession.dm
@@ -69,9 +69,9 @@
user.name = target.name
user.reset_perspective(target)
- target.AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
+ target.AddElement(/datum/element/weather_listener, /datum/weather/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
target.AddElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds)
- target.AddElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
+ target.AddElement(/datum/element/weather_listener, /datum/weather/particle/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
target.AddElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds)
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(end_possession))
@@ -84,8 +84,8 @@
var/mob/poltergeist = parent
- possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
- possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
+ possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
+ possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/particle/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds)
possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds)
UnregisterSignal(possessed, COMSIG_QDELETING)
diff --git a/code/datums/components/plumbing/_plumbing.dm b/code/datums/components/plumbing/_plumbing.dm
index 3f3af944f68b..cad8d5b935fd 100644
--- a/code/datums/components/plumbing/_plumbing.dm
+++ b/code/datums/components/plumbing/_plumbing.dm
@@ -20,11 +20,6 @@
/// Ex - if this was set to "3", our component would only request the first 3 reagents found, even if more are available
var/distinct_reagent_cap = INFINITY
- ///Extra offset on supply pipe.
- var/supply_offset = 0
- ///Extra offset on demand pipe.
- var/demand_offset = 0
-
/datum/component/plumbing/Initialize(ducting_layer)
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
@@ -105,7 +100,7 @@
if(istype(duct))
if(duct.neighbours && (duct.duct_layer & ducting_layer))
duct.neighbours[parent] = opposite_dir
- duct.update_appearance(UPDATE_ICON_STATE)
+ duct.update_appearance(UPDATE_ICON)
duct.net.add_plumber(src, direction)
continue
@@ -128,7 +123,7 @@
for(var/obj/machinery/duct/pipe as anything in net.ducts)
if(pipe.neighbours[parent])
pipe.neighbours -= parent
- pipe.update_appearance(UPDATE_ICON_STATE)
+ pipe.update_appearance(UPDATE_ICON)
//remove ourself from this network and delete it if emtpy
if(net.remove_plumber(src))
@@ -194,28 +189,8 @@
var/duct_y = offset - parent_movable.pixel_y - parent_movable.pixel_z
if(direction & initial(demand_connects))
color = demand_color
- if(demand_offset)
- switch(parent_movable.dir)
- if(NORTH)
- duct_y -= demand_offset
- if(SOUTH)
- duct_y += demand_offset
- if(EAST)
- duct_x -= demand_offset
- if(WEST)
- duct_x += demand_offset
else if(direction & initial(supply_connects))
color = supply_color
- if(supply_offset)
- switch(parent_movable.dir)
- if(NORTH)
- duct_y += supply_offset
- if(SOUTH)
- duct_y -= supply_offset
- if(EAST)
- duct_x += supply_offset
- if(WEST)
- duct_x -= supply_offset
else
continue
diff --git a/code/datums/components/plumbing/boulder_reactions.dm b/code/datums/components/plumbing/boulder_reactions.dm
index 56496ea803c4..982a7839d7a8 100644
--- a/code/datums/components/plumbing/boulder_reactions.dm
+++ b/code/datums/components/plumbing/boulder_reactions.dm
@@ -1,30 +1,32 @@
-/**
- * The boulder machines take in many types of chems, but should only ever eject "waste" chems,
- */
+/// The boulder machines take in many types of chems, but should only ever eject "waste" chems,
/datum/component/plumbing/boulder_reactions
- demand_connects = NORTH
- supply_connects = SOUTH
- supply_offset = 4
- demand_offset = 4
+ demand_connects = WEST
+ supply_connects = EAST
/datum/component/plumbing/boulder_reactions/Initialize(ducting_layer)
if(!istype(parent, /obj/machinery/bouldertech/refinery))
return COMPONENT_INCOMPATIBLE
return ..()
+/datum/component/plumbing/boulder_reactions/send_request(dir)
+ var/obj/machinery/bouldertech/refinery/the_refinery = parent
+ var/list/datum/reagents/boosters = the_refinery.get_booster_reagents()
+
+ for(var/datum/reagent/booster as anything in boosters)
+ process_request(MACHINE_REAGENT_TRANSFER, booster, dir, TRUE)
+
/datum/component/plumbing/boulder_reactions/can_give(amount, reagent, datum/ductnet/net)
- if(amount <= 0 || !reagents.total_volume || !reagent)
+ if(!reagents.total_volume || amount <= 0)
return FALSE
var/obj/machinery/bouldertech/refinery/the_refinery = parent
- var/list/datum/reagents/boosters = the_refinery.booster_list
+ if(reagent)
+ if(reagent != the_refinery.waste_chemical) //Always allow waste chemicals to enter.
+ return FALSE
- if(istype(amount, the_refinery.waste_chemical))
- return TRUE //Always allow waste chemicals to leave.
+ return !!reagents.has_reagent(the_refinery.waste_chemical)
- if(!length(boosters))
- return ..()
- for(var/datum/chem as anything in boosters)
- if(chem.type == reagent) // Need to check strict subtype since most acids are subtypes of eachother.
- return FALSE
- return ..()
+/datum/component/plumbing/boulder_reactions/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net, round_robin)
+ var/obj/machinery/bouldertech/refinery/the_refinery = parent
+
+ reagents.trans_to(target.recipient_reagents_holder(), amount, target_id = the_refinery.waste_chemical, methods = round_robin ? LINEAR : NONE)
diff --git a/code/datums/components/plumbing/chemical_acclimator.dm b/code/datums/components/plumbing/chemical_acclimator.dm
index a8bb511c374b..4f3b1dcc4d91 100644
--- a/code/datums/components/plumbing/chemical_acclimator.dm
+++ b/code/datums/components/plumbing/chemical_acclimator.dm
@@ -1,7 +1,6 @@
/datum/component/plumbing/acclimator
demand_connects = WEST
supply_connects = EAST
- var/obj/machinery/plumbing/acclimator/myacclimator
/datum/component/plumbing/acclimator/Initialize(ducting_layer)
if(!istype(parent, /obj/machinery/plumbing/acclimator))
diff --git a/code/datums/components/reflection.dm b/code/datums/components/reflection.dm
index 5e786b0c4acd..bd24a4e4f822 100644
--- a/code/datums/components/reflection.dm
+++ b/code/datums/components/reflection.dm
@@ -204,6 +204,7 @@
reflection.name = "[target.name]'s reflection"
SEND_SIGNAL(target, COMSIG_REFLECTION_UPDATED, parent, reflection)
+ SEND_SIGNAL(parent, COMSIG_REFLECTED_IMAGE_UPDATED, reflection)
///Called when the target movable changes its appearance or dir.
/datum/component/reflection/proc/update_reflection(atom/movable/source)
diff --git a/code/datums/components/revenant_prison.dm b/code/datums/components/revenant_prison.dm
new file mode 100644
index 000000000000..d5ba6529a626
--- /dev/null
+++ b/code/datums/components/revenant_prison.dm
@@ -0,0 +1,67 @@
+/datum/component/revenant_prison
+ // Whether a revenant should be created upon release
+ var/create_on_release = FALSE
+ // The revenant which is currently imprisoned
+ var/mob/living/basic/revenant/revenant
+ // ckey of the player who controlled it when it was imprisoned
+ var/old_ckey
+
+/datum/component/revenant_prison/Initialize(mob/living/basic/revenant/revenant, create_on_release = FALSE)
+ if(create_on_release)
+ return ..()
+ if(!istype(revenant) || !isobj(parent))
+ return COMPONENT_INCOMPATIBLE
+ . = ..()
+
+ src.revenant = revenant
+ revenant.dormant = TRUE
+ old_ckey = revenant.client?.ckey
+ revenant.forceMove(parent)
+
+/datum/component/revenant_prison/Destroy()
+ if(revenant?.client)
+ revenant.ghostize(can_reenter_corpse = FALSE)
+ QDEL_NULL(revenant)
+ return ..()
+
+/datum/component/revenant_prison/proc/on_parent_break(obj/source, damage_flags)
+ SIGNAL_HANDLER
+ source.visible_message(span_revenwarning("The revenant cackles as it escapes from [source]!"))
+ playsound(source.loc, 'sound/effects/chemistry/ahaha.ogg', 100, TRUE)
+ release_revenant(source, cause = "[parent] breaking")
+
+/datum/component/revenant_prison/proc/release_revenant(obj/source, cause)
+ SIGNAL_HANDLER
+ if(create_on_release)
+ revenant = new(get_turf(parent))
+ if(!revenant)
+ qdel(src)
+ return
+ message_admins("[revenant] [ADMIN_FLW(revenant)] has been released from [source] [ADMIN_JMP(source)]. Cause: [cause]")
+ if(!revenant.reform(old_ckey))
+ message_admins("Couldn't reform revenant upon release.")
+ revenant = null
+ qdel(src)
+
+/datum/component/revenant_prison/proc/shift_reflection(datum/source, obj/effect/abstract/reflection)
+ SIGNAL_HANDLER
+ apply_wibbly_filters(reflection)
+
+/datum/component/revenant_prison/proc/on_parent_examine(datum/source, mob/user, list/examine_list)
+ if(istype(parent, /obj/structure/mirror))
+ examine_list += span_revenwarning("The reflection is shifting and distorted.")
+
+/datum/component/revenant_prison/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_parent_examine))
+ RegisterSignal(parent, COMSIG_ATOM_BREAK, PROC_REF(on_parent_break))
+ RegisterSignal(parent, COMSIG_REVENANT_RELEASE, PROC_REF(release_revenant))
+ RegisterSignal(parent, COMSIG_REFLECTED_IMAGE_UPDATED, PROC_REF(shift_reflection))
+
+/datum/component/revenant_prison/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_ATOM_EXAMINE)
+ UnregisterSignal(parent, COMSIG_ATOM_BREAK)
+ UnregisterSignal(parent, COMSIG_REVENANT_RELEASE)
+ UnregisterSignal(parent, COMSIG_REFLECTED_IMAGE_UPDATED)
+
+/datum/component/revenant_prison/PostTransfer(datum/new_parent)
+ revenant.forceMove(new_parent)
diff --git a/code/datums/components/riding/riding.dm b/code/datums/components/riding/riding.dm
index 56faa2982d39..5b094e65e57b 100644
--- a/code/datums/components/riding/riding.dm
+++ b/code/datums/components/riding/riding.dm
@@ -68,7 +68,7 @@
RegisterSignals(parent, GLOB.movement_type_removetrait_signals, PROC_REF(on_movement_type_trait_loss))
RegisterSignal(parent, COMSIG_SUPERMATTER_CONSUMED, PROC_REF(on_entered_supermatter))
switch(other_unbuckle)
- if(CAN_FORCE_UNBUCKLE)
+ if(CANNOT_FORCE_UNBUCKLE)
RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(block_unbuckle))
if(CAN_DISARM_UNBUCKLE)
RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(block_unbuckle))
diff --git a/code/datums/components/storm_hating.dm b/code/datums/components/storm_hating.dm
index 62a59a0ad71a..0b4edbe84e78 100644
--- a/code/datums/components/storm_hating.dm
+++ b/code/datums/components/storm_hating.dm
@@ -4,7 +4,7 @@
/datum/component/storm_hating
/// Types of weather which trigger the effect
var/static/list/stormy_weather = list(
- /datum/weather/ash_storm,
+ /datum/weather/particle/ash_storm,
/datum/weather/snow_storm,
/datum/weather/void_storm,
)
diff --git a/code/datums/components/temporary_body.dm b/code/datums/components/temporary_body.dm
index d069116cf0ab..bf1dc618d482 100644
--- a/code/datums/components/temporary_body.dm
+++ b/code/datums/components/temporary_body.dm
@@ -79,7 +79,7 @@
SIGNAL_HANDLER
var/mob/new_body = parent
- var/mob/dead/observer/ghost = new_body.get_ghost() || new_body.ghostize()
+ var/mob/dead/observer/ghost = new_body.get_ghost() || new_body.ghostize(forced = TRUE)
if(QDELETED(ghost))
stack_trace("[src] belonging to [parent] was completely unable to find a ghost to put back into a body!")
qdel(src) // i guess this is useless now
@@ -90,7 +90,7 @@
if(old_mind.current != old_body)
stack_trace("Temporary body returning mind to old body, but the mind's current body doesn't match the old body!")
old_mind.set_current(old_body)
- if(old_body.stat != DEAD)
+ if(old_body.stat != DEAD && !IS_FAKE_KEY(old_body.key))
ghost.reenter_corpse()
qdel(src) // we're done here
diff --git a/code/datums/components/tug_towards.dm b/code/datums/components/tug_towards.dm
index 6b1a094b6d1e..338d27660ee7 100644
--- a/code/datums/components/tug_towards.dm
+++ b/code/datums/components/tug_towards.dm
@@ -40,13 +40,18 @@
/datum/component/tug_towards/Destroy(force)
tugging_to_targets.Cut()
- animate(
- parent,
- pixel_x = -current_tug_offset_x,
- pixel_y = -current_tug_offset_y,
- time = 0.2 SECONDS,
- flags = ANIMATION_RELATIVE
- )
+ if(isliving(parent))
+ var/mob/living/living_parent = parent
+ living_parent.remove_offsets(REF(src))
+
+ else
+ animate(
+ parent,
+ pixel_x = -current_tug_offset_x,
+ pixel_y = -current_tug_offset_y,
+ time = 0.2 SECONDS,
+ flags = ANIMATION_RELATIVE
+ )
return ..()
@@ -93,6 +98,9 @@
SIGNAL_HANDLER
PRIVATE_PROC(TRUE)
+ if(QDELETED(src))
+ return // another movement call could have deleted us
+
var/atom/atom_parent = parent
var/mob/mob_parent = parent
@@ -123,13 +131,18 @@
if (total_tug_x == current_tug_offset_x && total_tug_y == current_tug_offset_y)
return
- animate(
- atom_parent,
- pixel_x = -current_tug_offset_x + total_tug_x,
- pixel_y = -current_tug_offset_y + total_tug_y,
- time = 0.2 SECONDS,
- flags = ANIMATION_RELATIVE
- )
+ if(isliving(mob_parent))
+ var/mob/living/living_parent = mob_parent
+ living_parent.add_offsets(REF(src), x_add = total_tug_x, y_add = total_tug_y)
+
+ else
+ animate(
+ atom_parent,
+ pixel_x = -current_tug_offset_x + total_tug_x,
+ pixel_y = -current_tug_offset_y + total_tug_y,
+ time = 0.2 SECONDS,
+ flags = ANIMATION_RELATIVE
+ )
current_tug_offset_x = total_tug_x
current_tug_offset_y = total_tug_y
diff --git a/code/datums/components/twohanded.dm b/code/datums/components/twohanded.dm
index 0db3642eb60f..afcc8305fb53 100644
--- a/code/datums/components/twohanded.dm
+++ b/code/datums/components/twohanded.dm
@@ -119,7 +119,7 @@
src.force_multiplier = force_multiplier
if(!isnull(force_wielded))
src.force_wielded = force_wielded
- if(isnull(force_unwielded))
+ if(!isnull(force_unwielded))
src.force_unwielded = force_unwielded
if(icon_wielded)
src.icon_wielded = icon_wielded
diff --git a/code/datums/datum.dm b/code/datums/datum.dm
index 39c0f9ab284c..0b46fe66554b 100644
--- a/code/datums/datum.dm
+++ b/code/datums/datum.dm
@@ -87,7 +87,7 @@
/**
* Parent types.
- *
+ *
* Abstract-ness is a meta-property of a class that is used to indicate
* that the class is intended to be used as a base class for others, and
* should not (or cannot) be instantiated.
@@ -123,6 +123,7 @@
* Returns [QDEL_HINT_QUEUE]
*/
/datum/proc/Destroy(force = FALSE)
+ PROTECTED_PROC(TRUE)
SHOULD_CALL_PARENT(TRUE)
SHOULD_NOT_SLEEP(TRUE)
tag = null
@@ -160,9 +161,16 @@
//END: ECS SHIT
#ifndef DISABLE_DREAMLUAU
+ var/list/to_remove = list()
+ if(ismovable(src))
+ var/atom/movable/src_movable = src
+ to_remove += list(src_movable.vis_contents, src_movable.vis_locs)
if(!(datum_flags & DF_STATIC_OBJECT))
- DREAMLUAU_CLEAR_REF_USERDATA(vars) // vars ceases existing when src does, so we need to clear any lua refs to it that exist.
- DREAMLUAU_CLEAR_REF_USERDATA(src)
+ if(isatom(src))
+ var/atom/src_atom = src
+ to_remove += list(src_atom.contents, src_atom.filters, src_atom.underlays, src_atom.overlays)
+ to_remove += list(vars, src) // vars ceases existing when src does, so we need to clear any lua refs to it that exist.
+ DREAMLUAU_CLEAR_REF_USERDATA(arglist(to_remove))
#endif
return QDEL_HINT_QUEUE
diff --git a/code/datums/diseases/_disease.dm b/code/datums/diseases/_disease.dm
index 3c7ede4f1a60..5bd335eca33d 100644
--- a/code/datums/diseases/_disease.dm
+++ b/code/datums/diseases/_disease.dm
@@ -77,8 +77,8 @@
SSdisease.active_diseases += D //Add it to the active diseases list, now that it's actually in a mob and being processed.
D.after_add()
+ D.register_disease_signals()
infectee.med_hud_set_status()
- register_disease_signals()
var/turf/source_turf = get_turf(infectee)
log_virus("[key_name(infectee)] was infected by virus: [src.admin_details()] at [loc_name(source_turf)]")
@@ -96,6 +96,7 @@
/datum/disease/proc/register_disease_signals()
if(isnull(affected_mob))
return
+ RegisterSignal(affected_mob, COMSIG_LIVING_LIFE, PROC_REF(on_life))
if(spread_flags & DISEASE_SPREAD_AIRBORNE)
RegisterSignal(affected_mob, COMSIG_CARBON_PRE_BREATHE, PROC_REF(on_breath))
@@ -103,12 +104,21 @@
/datum/disease/proc/unregister_disease_signals()
if(isnull(affected_mob))
return
- UnregisterSignal(affected_mob, COMSIG_CARBON_PRE_BREATHE)
+ UnregisterSignal(affected_mob, list(COMSIG_LIVING_LIFE, COMSIG_CARBON_PRE_BREATHE))
// Proc to determine if the virus can resist natural recovery
/datum/disease/proc/get_recovery_failure_chance()
return 0
+/datum/disease/proc/on_life(datum/source, seconds_per_tick)
+ SIGNAL_HANDLER
+ PRIVATE_PROC(TRUE)
+
+ if(HAS_TRAIT(affected_mob, TRAIT_STASIS) || QDELETED(src) || (affected_mob.stat == DEAD && !process_dead))
+ return
+
+ stage_act(seconds_per_tick)
+
///Proc to process the disease and decide on whether to advance, cure or make the symptoms appear. Returns a boolean on whether to continue acting on the symptoms or not.
/datum/disease/proc/stage_act(seconds_per_tick)
var/slowdown = HAS_TRAIT(affected_mob, TRAIT_VIRUS_RESISTANCE) ? 0.5 : 1 // spaceacillin slows stage speed by 50%
@@ -147,9 +157,11 @@
if(!(disease_flags & CHRONIC) && disease_flags & CURABLE && bypasses_immunity != TRUE)
switch(severity)
- if(DISEASE_SEVERITY_POSITIVE) //good viruses don't go anywhere after hitting max stage - you can try to get rid of them by sleeping earlier
- cycles_to_beat = max(DISEASE_RECOVERY_SCALING, DISEASE_CYCLES_POSITIVE) //because of the way we later check for recovery_prob, we need to floor this at least equal to the scaling to avoid infinitely getting less likely to cure
- if(((HAS_TRAIT(affected_mob, TRAIT_NOHUNGER)) || ((affected_mob.nutrition > NUTRITION_LEVEL_STARVING) && (affected_mob.satiety >= 0))) && slowdown == 1) //any sort of malnourishment/immunosuppressant opens you to losing a good virus
+ if(DISEASE_SEVERITY_POSITIVE)
+ if(slowdown < 1 || (!(HAS_TRAIT(affected_mob, TRAIT_NOHUNGER)) && (affected_mob.satiety < DISEASE_SATIETY_THRESHOLD || affected_mob.nutrition < NUTRITION_LEVEL_STARVING)))
+ cycles_to_beat = max(DISEASE_RECOVERY_SCALING, DISEASE_CYCLES_POSITIVE)
+ else
+ recovery_prob = 0
return TRUE
if(DISEASE_SEVERITY_NONTHREAT)
cycles_to_beat = max(DISEASE_RECOVERY_SCALING, DISEASE_CYCLES_NONTHREAT)
@@ -169,14 +181,14 @@
recovery_prob += DISEASE_RECOVERY_CONSTANT + (peaked_cycles / (cycles_to_beat / DISEASE_RECOVERY_SCALING)) //more severe viruses are beaten back more aggressively after the peak
if(stage_peaked)
recovery_prob *= DISEASE_PEAKED_RECOVERY_MULTIPLIER
- if(slowdown != 1) //using spaceacillin can help get them over the finish line to kill a virus with decreasing effect over time
+ if(slowdown < 1) //using spaceacillin can help get them over the finish line to kill a virus with decreasing effect over time
recovery_prob += clamp((((1 - slowdown)*(DISEASE_SLOWDOWN_RECOVERY_BONUS * 2)) * ((DISEASE_SLOWDOWN_RECOVERY_BONUS_DURATION - chemical_offsets) / DISEASE_SLOWDOWN_RECOVERY_BONUS_DURATION)), 0, DISEASE_SLOWDOWN_RECOVERY_BONUS)
chemical_offsets = min(chemical_offsets + 1, DISEASE_SLOWDOWN_RECOVERY_BONUS_DURATION)
if(!HAS_TRAIT(affected_mob, TRAIT_NOHUNGER))
- if(affected_mob.satiety < 0 || affected_mob.nutrition < NUTRITION_LEVEL_STARVING) //being malnourished makes it a lot harder to defeat your illness
+ if(affected_mob.satiety < DISEASE_SATIETY_THRESHOLD || affected_mob.nutrition < NUTRITION_LEVEL_STARVING) //being malnourished makes it a lot harder to defeat your illness
recovery_prob -= DISEASE_MALNUTRITION_RECOVERY_PENALTY
else
- if(affected_mob.satiety >= 0)
+ if(affected_mob.satiety > 0)
recovery_prob += round((DISEASE_SATIETY_RECOVERY_MULTIPLIER * (affected_mob.satiety/MAX_SATIETY)), 0.1)
if(affected_mob.mob_mood) // this and most other modifiers below a shameless rip from sleeping healing buffs, but feeling good helps make it go away quicker
@@ -227,7 +239,7 @@
var/failure_chance = (1 - get_recovery_failure_chance() / 100)
if(SPT_PROB(recovery_prob * failure_chance, seconds_per_tick))
if(stage == 1 && prob(cure_chance * DISEASE_FINAL_CURE_CHANCE_MULTIPLIER)) //if we reduce FROM stage == 1, cure the virus - after defeating its cure_chance in a final battle
- if(!HAS_TRAIT(affected_mob, TRAIT_NOHUNGER) && (affected_mob.satiety < 0 || affected_mob.nutrition < NUTRITION_LEVEL_STARVING))
+ if(!HAS_TRAIT(affected_mob, TRAIT_NOHUNGER) && (affected_mob.satiety < DISEASE_SATIETY_THRESHOLD || affected_mob.nutrition < NUTRITION_LEVEL_STARVING))
if(stage_peaked == FALSE) //if you didn't ride out the virus from its peak, if you're malnourished when it cures, you don't get resistance
cure(add_resistance = FALSE)
return FALSE
diff --git a/code/datums/dna/dna.dm b/code/datums/dna/dna.dm
index 7262271af1b4..87e99b754ab2 100644
--- a/code/datums/dna/dna.dm
+++ b/code/datums/dna/dna.dm
@@ -4,7 +4,7 @@
*/
GLOBAL_LIST_INIT(total_ui_len_by_block, populate_total_ui_len_by_block())
-GLOBAL_LIST_INIT(standard_mutation_sources, list(MUTATION_SOURCE_ACTIVATED, MUTATION_SOURCE_MUTATOR, MUTATION_SOURCE_TIMED_INJECTOR))
+GLOBAL_LIST_INIT(standard_mutation_sources, list(MUTATION_SOURCE_ACTIVATED, MUTATION_SOURCE_MUTATOR))
/proc/populate_total_ui_len_by_block()
. = list()
@@ -44,10 +44,6 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block())
var/real_name
///All mutations are from now on here
var/list/mutations
- ///Temporary changes to the UE
- var/list/temporary_mutations
- ///For temporary name/ui/ue/blood_type modifications
- var/list/previous
var/mob/living/holder
///List of which mutations this carbon has and its assigned block
var/mutation_index[DNA_MUTATION_BLOCKS]
@@ -77,8 +73,6 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block())
QDEL_NULL(species)
LAZYNULL(mutations) //This only references mutations, just dereference.
- LAZYNULL(temporary_mutations) //^
- LAZYNULL(previous) //^
return ..()
@@ -89,9 +83,9 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block())
new_dna.unique_features = unique_features
new_dna.features = features.Copy()
new_dna.real_name = real_name
- new_dna.temporary_mutations = LAZYLISTDUPLICATE(temporary_mutations)
- new_dna.mutation_index = mutation_index
- new_dna.default_mutation_genes = default_mutation_genes
+ if(transfer_flags & COPY_DNA_SE)
+ new_dna.mutation_index = mutation_index
+ new_dna.default_mutation_genes = default_mutation_genes
//if the new DNA has a holder, transform them immediately, otherwise save it
if(new_dna.holder)
if (iscarbon(new_dna.holder) && (transfer_flags & COPY_DNA_BLOOD_TYPE)) // DARKPACK EDIT CHANGE - added & COPY_DNA_BLOOD_TYPE
@@ -158,7 +152,7 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block())
var/datum/mutation/actual_mutation = get_mutation(mutation_to_remove)
if(!actual_mutation || !(sources & actual_mutation.sources))
- return
+ return FALSE
actual_mutation.sources -= sources
@@ -172,6 +166,7 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block())
qdel(actual_mutation)
update_instability(FALSE)
+ return TRUE
/datum/dna/proc/check_mutation(mutation_type)
return get_mutation(mutation_type)
diff --git a/code/datums/drift_handler.dm b/code/datums/drift_handler.dm
index 9eef89f7ee58..1d4bee6708a1 100644
--- a/code/datums/drift_handler.dm
+++ b/code/datums/drift_handler.dm
@@ -223,4 +223,4 @@
return COMSIG_MOB_CLIENT_BLOCK_PRE_MOVE
/datum/drift_handler/proc/get_loop_delay(atom/movable/movable)
- return (DEFAULT_INERTIA_SPEED / ((1 - INERTIA_SPEED_COEF) + drift_force * INERTIA_SPEED_COEF)) * movable.inertia_move_multiplier
+ return (DEFAULT_INERTIA_SPEED / ((1 - INERTIA_SPEED_COEF) + drift_force * INERTIA_SPEED_COEF)) * min(movable.inertia_move_multiplier_passive, movable.inertia_move_multiplier_active)
diff --git a/code/datums/elements/ai_retaliate.dm b/code/datums/elements/ai_retaliate.dm
index 03465d3c2464..4fcb51581438 100644
--- a/code/datums/elements/ai_retaliate.dm
+++ b/code/datums/elements/ai_retaliate.dm
@@ -24,4 +24,4 @@
if (victim == attacker)
return
- victim.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
+ victim.ai_controller?.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)
diff --git a/code/datums/elements/bane.dm b/code/datums/elements/bane.dm
deleted file mode 100644
index 769cf8d361da..000000000000
--- a/code/datums/elements/bane.dm
+++ /dev/null
@@ -1,93 +0,0 @@
-/// Deals extra damage to mobs of a certain type, species, or biotype.
-/// This doesn't directly modify the normal damage of the weapon, instead it applies its own damage separately ON TOP of normal damage
-/// ie. a sword that does 10 damage with a bane element attached that has a 0.5 damage_multiplier will do:
-/// 10 damage from the swords normal attack + 5 damage (50%) from the bane element
-/datum/element/bane
- element_flags = ELEMENT_BESPOKE
- argument_hash_start_idx = 2
- /// can be a mob or a species.
- var/target_type
- /// multiplier of the extra damage based on the force of the item.
- var/damage_multiplier
- /// Added after the above.
- var/added_damage
- /// If it requires combat mode on to deal the extra damage or not.
- var/requires_combat_mode
- /// if we want it to only affect a certain mob biotype
- var/mob_biotypes
-
-/datum/element/bane/Attach(datum/target, target_type = /mob/living, mob_biotypes = NONE, damage_multiplier=1, added_damage = 0, requires_combat_mode = TRUE)
- . = ..()
-
- if(!ispath(target_type, /mob/living) && !ispath(target_type, /datum/species))
- return ELEMENT_INCOMPATIBLE
-
- src.target_type = target_type
- src.damage_multiplier = damage_multiplier
- src.added_damage = added_damage
- src.requires_combat_mode = requires_combat_mode
- src.mob_biotypes = mob_biotypes
- target.AddElementTrait(TRAIT_ON_HIT_EFFECT, REF(src), /datum/element/on_hit_effect)
- RegisterSignal(target, COMSIG_ON_HIT_EFFECT, PROC_REF(do_bane))
-
-/datum/element/bane/Detach(datum/source)
- UnregisterSignal(source, COMSIG_ON_HIT_EFFECT)
- REMOVE_TRAIT(source, TRAIT_ON_HIT_EFFECT, REF(src))
- return ..()
-
-/datum/element/bane/proc/do_bane(datum/element_owner, mob/living/bane_applier, mob/living/baned_target, hit_zone, throw_hit)
- if(!check_biotype_path(bane_applier, baned_target))
- return
- if(SEND_SIGNAL(element_owner, COMSIG_OBJECT_PRE_BANING, baned_target) & COMPONENT_CANCEL_BANING)
- return
-
- var/force_boosted
- var/applied_dam_type
-
- if(isitem(element_owner))
- var/obj/item/item_owner = element_owner
- force_boosted = item_owner.force
- applied_dam_type = item_owner.damtype
- else if(isprojectile(element_owner))
- var/obj/projectile/projectile_owner = element_owner
- force_boosted = projectile_owner.damage
- applied_dam_type = projectile_owner.damage_type
- else if (isliving(element_owner))
- var/mob/living/living_owner = element_owner
- force_boosted = (living_owner.melee_damage_lower + living_owner.melee_damage_upper) / 2
- //commence crying. yes, these really are the same check. FUCK.
- if(isbasicmob(living_owner))
- var/mob/living/basic/basic_owner = living_owner
- applied_dam_type = basic_owner.melee_damage_type
- else if(isanimal(living_owner))
- var/mob/living/simple_animal/simple_owner = living_owner
- applied_dam_type = simple_owner.melee_damage_type
- else
- return
- else
- return
-
- var/extra_damage = max(0, (force_boosted * damage_multiplier) + added_damage)
- baned_target.apply_damage(extra_damage, applied_dam_type, hit_zone)
- SEND_SIGNAL(baned_target, COMSIG_LIVING_BANED, bane_applier, baned_target) // for extra effects when baned.
- SEND_SIGNAL(element_owner, COMSIG_OBJECT_ON_BANING, baned_target)
-
-/**
- * Checks typepaths and the mob's biotype, returning TRUE if correct and FALSE if wrong.
- * Additionally checks if combat mode is required, and if so whether it's enabled or not.
- */
-/datum/element/bane/proc/check_biotype_path(atom/bane_applier, atom/target)
- if(!isliving(target))
- return FALSE
- var/mob/living/living_target = target
- if(isliving(bane_applier) && bane_applier)
- var/mob/living/living_bane_applier = bane_applier
- if(requires_combat_mode && !living_bane_applier.combat_mode)
- return FALSE
- var/is_correct_biotype = living_target.mob_biotypes & mob_biotypes
- if(mob_biotypes && !(is_correct_biotype))
- return FALSE
- if(ispath(target_type, /mob/living))
- return istype(living_target, target_type)
- else //species type
- return is_species(living_target, target_type)
diff --git a/code/datums/elements/block_area_power_fail.dm b/code/datums/elements/block_area_power_fail.dm
new file mode 100644
index 000000000000..8de9ed241189
--- /dev/null
+++ b/code/datums/elements/block_area_power_fail.dm
@@ -0,0 +1,67 @@
+// Used to differentiate what is protecting an area
+#define AREA_TRAIT_SOURCE(the_source) "[type]_[the_source.type]"
+#define MOVABLE_TRAIT_SOURCE(the_source) "[type]_[REF(the_source)]"
+#define TURF_TRAIT_SOURCE(the_source) "[type]_[the_source.x]-[the_source.y]-[the_source.z]"
+
+/**
+ * ## block_area_power_fail
+ *
+ * Element that interacts with the grid check event (and similar effects) to protect certain rooms
+ *
+ * * Attach to an area to prevent arbitrary power outages from affecting it
+ * * Attach to a movable to prevent arbitrary power outages from affecting the area it's in
+ * * Attach to a turf to prevent arbitrary power outages from affecting its area
+ */
+/datum/element/block_area_power_fail
+ element_flags = ELEMENT_DETACH_ON_HOST_DESTROY
+
+/datum/element/block_area_power_fail/Attach(datum/target)
+ . = ..()
+ if(isarea(target)) // practically just a complicated way to do add trait
+ ADD_TRAIT(target, TRAIT_AREA_BLOCK_POWER_FAIL, AREA_TRAIT_SOURCE(target))
+
+ else if(ismovable(target)) // manages adding and removing the trait as the movable moves
+ var/atom/movable/movable_target = target
+ movable_target.become_area_sensitive(type)
+ RegisterSignal(target, COMSIG_ENTER_AREA, PROC_REF(on_movable_entered_area))
+ RegisterSignal(target, COMSIG_EXIT_AREA, PROC_REF(on_movable_exited_area))
+ on_movable_entered_area(target, get_area(movable_target))
+
+ else if(isturf(target)) // turfs don't move so it just adds the trait to the turf's area
+ var/turf/turf_target = target
+ ADD_TRAIT(turf_target.loc, TRAIT_AREA_BLOCK_POWER_FAIL, TURF_TRAIT_SOURCE(turf_target))
+
+ else
+ return ELEMENT_INCOMPATIBLE
+
+/datum/element/block_area_power_fail/Detach(datum/source)
+ . = ..()
+ if(isarea(source))
+ REMOVE_TRAIT(source, TRAIT_AREA_BLOCK_POWER_FAIL, AREA_TRAIT_SOURCE(source))
+
+ else if(ismovable(source))
+ var/atom/movable/movable_source = source
+ movable_source.lose_area_sensitivity(type)
+ UnregisterSignal(source, COMSIG_ENTER_AREA)
+ UnregisterSignal(source, COMSIG_EXIT_AREA)
+ on_movable_exited_area(source, get_area(movable_source))
+
+ else if(isturf(source))
+ var/turf/turf_source = source
+ REMOVE_TRAIT(turf_source.loc, TRAIT_AREA_BLOCK_POWER_FAIL, TURF_TRAIT_SOURCE(turf_source))
+
+/datum/element/block_area_power_fail/proc/on_movable_entered_area(atom/movable/source, area/new_area)
+ SIGNAL_HANDLER
+
+ if(new_area)
+ ADD_TRAIT(new_area, TRAIT_AREA_BLOCK_POWER_FAIL, MOVABLE_TRAIT_SOURCE(source))
+
+/datum/element/block_area_power_fail/proc/on_movable_exited_area(atom/movable/source, area/old_area)
+ SIGNAL_HANDLER
+
+ if(old_area)
+ REMOVE_TRAIT(old_area, TRAIT_AREA_BLOCK_POWER_FAIL, MOVABLE_TRAIT_SOURCE(source))
+
+#undef AREA_TRAIT_SOURCE
+#undef MOVABLE_TRAIT_SOURCE
+#undef TURF_TRAIT_SOURCE
diff --git a/code/datums/elements/cuffable_item.dm b/code/datums/elements/cuffable_item.dm
index b81068d2361f..6939681099c9 100644
--- a/code/datums/elements/cuffable_item.dm
+++ b/code/datums/elements/cuffable_item.dm
@@ -47,13 +47,24 @@
if(cuffs.used || DOING_INTERACTION_WITH_TARGET(user, source))
return
- if(HAS_TRAIT_FROM(source, TRAIT_NODROP, CUFFED_ITEM_TRAIT))
- to_chat(user, span_warning("[source] is already cuffed to your wrist!"))
+ for(var/datum/status_effect/cuffed_item/effect in user.status_effects)
+ if(effect.cuffed == source)
+ to_chat(user, span_warning("[source] is already cuffed to your wrist!"))
+ return
+ if(effect.cuffed_to == user.get_inactive_hand())
+ to_chat(user, span_warning("You already have something cuffed to your opposite wrist!"))
+ return
+
+ if(!user.get_inactive_hand())
+ to_chat(user, span_warning("You don't have another hand to cuff [source] to!"))
return
if(cuffs.handcuffs_clumsiness_check(user))
return
+ if(SEND_SIGNAL(source, COMSIG_ITEM_PRE_CUFFED_TO_MOB, user, cuffs) & BLOCK_ITEM_CUFF)
+ return
+
source.balloon_alert(user, "cuffing item...")
playsound(source, cuffs.cuffsound, 30, TRUE, -2)
if(!do_after(user, cuffs.get_handcuff_time(user), source))
diff --git a/code/datums/elements/move_force_on_death.dm b/code/datums/elements/move_force_on_death.dm
index af2560d000c3..481f93581337 100644
--- a/code/datums/elements/move_force_on_death.dm
+++ b/code/datums/elements/move_force_on_death.dm
@@ -39,11 +39,11 @@
if(!isnull(move_resist))
source.move_resist = move_resist
if(!isnull(pull_force))
- source.pull_force = pull_force
+ source.set_pull_force(pull_force)
/datum/element/change_force_on_death/proc/on_revive(mob/living/source)
SIGNAL_HANDLER
source.move_force = initial(source.move_force)
source.move_resist = initial(source.move_resist)
- source.pull_force = initial(source.pull_force)
+ source.set_pull_force(initial(source.pull_force))
diff --git a/code/datums/elements/moving_randomly.dm b/code/datums/elements/moving_randomly.dm
new file mode 100644
index 000000000000..5677ab2c7b9a
--- /dev/null
+++ b/code/datums/elements/moving_randomly.dm
@@ -0,0 +1,26 @@
+/// Just move something around in the simplest way possible
+/datum/element/moving_randomly
+ element_flags = ELEMENT_DETACH_ON_HOST_DESTROY
+
+ /// Movables that we are moving around
+ var/list/movers = list()
+
+/datum/element/moving_randomly/Attach(datum/target)
+ . = ..()
+
+ if(!ismovable(target))
+ return ELEMENT_INCOMPATIBLE
+
+ movers += target
+
+/datum/element/moving_randomly/Detach(datum/source, ...)
+ . = ..()
+
+ movers -= source
+
+/datum/element/moving_randomly/New()
+ START_PROCESSING(SSdcs, src)
+
+/datum/element/moving_randomly/process(seconds_per_tick)
+ for(var/atom/movable/mover as anything in movers)
+ mover.Move(get_step(mover, pick(GLOB.alldirs)))
diff --git a/code/datums/elements/nullrod_core.dm b/code/datums/elements/nullrod_core.dm
index fa48e421f8e1..6b4a309ba359 100644
--- a/code/datums/elements/nullrod_core.dm
+++ b/code/datums/elements/nullrod_core.dm
@@ -19,7 +19,7 @@
effects_we_clear = list(/obj/effect/rune, /obj/effect/heretic_rune, /obj/effect/cosmic_rune), \
)
target.AddComponent(/datum/component/cult_kill_tracker)
- target.AddElement(/datum/element/bane, mob_biotypes = MOB_SPIRIT, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
+ target.AddComponent(/datum/component/bane, affected_biotypes = MOB_SPIRIT, added_damage = 25)
ADD_TRAIT(target, TRAIT_NULLROD_ITEM, ELEMENT_TRAIT(type))
if(!PERFORM_ALL_TESTS(focus_only/nullrod_variants) || !chaplain_spawnable)
diff --git a/code/datums/elements/relay_attackers.dm b/code/datums/elements/relay_attackers.dm
index 5dfef67a1191..348f0e083547 100644
--- a/code/datums/elements/relay_attackers.dm
+++ b/code/datums/elements/relay_attackers.dm
@@ -39,25 +39,25 @@
/datum/element/relay_attackers/proc/after_attackby(atom/target, obj/item/weapon, mob/attacker, list/modifiers)
SIGNAL_HANDLER
if(weapon.force)
- relay_attacker(target, attacker, weapon.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK)
+ relay_attacker(target, attacker, weapon.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK, get_dir(target, attacker))
/datum/element/relay_attackers/proc/on_attack_generic(atom/target, mob/living/attacker, list/modifiers)
SIGNAL_HANDLER
// Check for a shove.
if(LAZYACCESS(modifiers, RIGHT_CLICK))
- relay_attacker(target, attacker, ATTACKER_SHOVING)
+ relay_attacker(target, attacker, ATTACKER_SHOVING, get_dir(target, attacker))
return
// Else check for combat mode.
if(attacker.combat_mode)
- relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK, get_dir(target, attacker))
return
/datum/element/relay_attackers/proc/on_attack_npc(atom/target, mob/living/attacker)
SIGNAL_HANDLER
if(attacker.melee_damage_upper > 0)
- relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK, get_dir(target, attacker))
/// Even if another component blocked this hit, someone still shot at us
/datum/element/relay_attackers/proc/on_bullet_act(atom/target, obj/projectile/hit_projectile)
@@ -66,7 +66,7 @@
return
if(!ismob(hit_projectile.firer))
return
- relay_attacker(target, hit_projectile.firer, hit_projectile.damage_type == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK)
+ relay_attacker(target, hit_projectile.firer, ATTACK_RANGED | hit_projectile.damage_type == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK, get_dir(target, hit_projectile))
/// Even if another component blocked this hit, someone still threw something
/datum/element/relay_attackers/proc/on_hitby(atom/target, atom/movable/hit_atom, datum/thrownthing/throwingdatum)
@@ -79,15 +79,15 @@
var/atom/thrown_by = throwingdatum?.get_thrower()
if(!istype(thrown_by))
return
- relay_attacker(target, thrown_by, hit_item.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK)
+ relay_attacker(target, thrown_by, ATTACK_RANGED | hit_item.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK, get_dir(target, hit_atom))
/datum/element/relay_attackers/proc/on_attack_hulk(atom/target, mob/attacker)
SIGNAL_HANDLER
- relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK, get_dir(target, attacker))
/datum/element/relay_attackers/proc/on_attack_mech(atom/target, obj/vehicle/sealed/mecha/mecha_attacker, mob/living/pilot, mecha_attack_cooldown)
SIGNAL_HANDLER
- relay_attacker(target, mecha_attacker, ATTACKER_DAMAGING_ATTACK)
+ relay_attacker(target, mecha_attacker, ATTACKER_DAMAGING_ATTACK, get_dir(target, mecha_attacker))
// DARKPACK EDIT ADD START
/datum/element/relay_attackers/proc/on_power_use(atom/target, datum/discipline_power/used_power, hostile_usage)
@@ -98,5 +98,5 @@
// DARKPACK EDIT ADD END
/// Send out a signal identifying whoever just attacked us (usually a mob but sometimes a mech or turret)
-/datum/element/relay_attackers/proc/relay_attacker(atom/victim, atom/attacker, attack_flags)
- SEND_SIGNAL(victim, COMSIG_ATOM_WAS_ATTACKED, attacker, attack_flags)
+/datum/element/relay_attackers/proc/relay_attacker(atom/victim, atom/attacker, attack_flags, direction)
+ SEND_SIGNAL(victim, COMSIG_ATOM_WAS_ATTACKED, attacker, attack_flags, direction)
diff --git a/code/datums/elements/weapon_description.dm b/code/datums/elements/weapon_description.dm
index 022318abfcf0..23acb72960c0 100644
--- a/code/datums/elements/weapon_description.dm
+++ b/code/datums/elements/weapon_description.dm
@@ -97,6 +97,8 @@
if(attached_proc)
readout += call(source, attached_proc)()
+ SEND_SIGNAL(source, COMSIG_ITEM_WEAPON_LABEL_READOUT, readout)
+
// Finally bringing the fields together
return readout.Join("\n")
diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm
index d4154408fa3f..c6dcbc200df2 100644
--- a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm
+++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm
@@ -926,6 +926,15 @@
name = "Gi (Worn)"
icon_file = 'icons/mob/clothing/under/costume.dmi'
+/datum/greyscale_config/jester_hat
+ name = "Jester Hat"
+ icon_file = 'icons/obj/clothing/head/costume.dmi'
+ json_config = 'code/datums/greyscale/json_configs/jester_hat.json'
+
+/datum/greyscale_config/jester_hat/worn
+ name = "Jester Hat (Worn)"
+ icon_file = 'icons/mob/clothing/head/costume.dmi'
+
/datum/greyscale_config/jester_suit
name = "Jester Suit"
icon_file = 'icons/obj/clothing/under/civilian.dmi'
@@ -944,15 +953,24 @@
name = "Jester Shoes (Worn)"
icon_file = 'icons/mob/clothing/feet.dmi'
-/datum/greyscale_config/jester_hat
- name = "Jester Hat"
+/datum/greyscale_config/jester_hat_alt
+ name = "Jester Hat (Alt)"
icon_file = 'icons/obj/clothing/head/costume.dmi'
- json_config = 'code/datums/greyscale/json_configs/jester_hat.json'
+ json_config = 'code/datums/greyscale/json_configs/jester_hat_alt.json'
-/datum/greyscale_config/jester_hat/worn
- name = "Jester Hat (Worn)"
+/datum/greyscale_config/jester_hat_alt/worn
+ name = "Jester Hat (Alt, Worn)"
icon_file = 'icons/mob/clothing/head/costume.dmi'
+/datum/greyscale_config/jester_suit_alt
+ name = "Jester Suit (Alt)"
+ icon_file = 'icons/obj/clothing/under/civilian.dmi'
+ json_config = 'code/datums/greyscale/json_configs/jester_suit_alt.json'
+
+/datum/greyscale_config/jester_suit_alt/worn
+ name = "Jester Suit (Alt, Worn)"
+ icon_file = 'icons/mob/clothing/under/civilian.dmi'
+
/datum/greyscale_config/lizard_hat
name = "Lizardskin Cloche Hat"
icon_file = 'icons/obj/clothing/head/costume.dmi'
diff --git a/code/datums/greyscale/json_configs/jester_hat_alt.json b/code/datums/greyscale/json_configs/jester_hat_alt.json
new file mode 100644
index 000000000000..d791644a6f33
--- /dev/null
+++ b/code/datums/greyscale/json_configs/jester_hat_alt.json
@@ -0,0 +1,21 @@
+{
+ "jester_alt": [
+ {
+ "type": "icon_state",
+ "icon_state": "jester_alt_a",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "jester_alt_b",
+ "blend_mode": "overlay",
+ "color_ids": [2]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "jester_alt_bells",
+ "blend_mode": "overlay"
+ }
+ ]
+}
diff --git a/code/datums/greyscale/json_configs/jester_suit_alt.json b/code/datums/greyscale/json_configs/jester_suit_alt.json
new file mode 100644
index 000000000000..d791644a6f33
--- /dev/null
+++ b/code/datums/greyscale/json_configs/jester_suit_alt.json
@@ -0,0 +1,21 @@
+{
+ "jester_alt": [
+ {
+ "type": "icon_state",
+ "icon_state": "jester_alt_a",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "jester_alt_b",
+ "blend_mode": "overlay",
+ "color_ids": [2]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "jester_alt_bells",
+ "blend_mode": "overlay"
+ }
+ ]
+}
diff --git a/code/datums/helper_datums/teleport.dm b/code/datums/helper_datums/teleport.dm
index 02e133b57181..e8cef1fd5ff9 100644
--- a/code/datums/helper_datums/teleport.dm
+++ b/code/datums/helper_datums/teleport.dm
@@ -28,22 +28,16 @@
switch(channel)
if(TELEPORT_CHANNEL_BLUESPACE)
- if(istype(teleatom, /obj/item/storage/backpack/holding))
- precision = rand(1,100)
-
- var/static/list/bag_cache = typecacheof(/obj/item/storage/backpack/holding, /obj/item/mod/control, /obj/item/mod/module/storage)
- var/list/bagholding = typecache_filter_list(teleatom.get_all_contents(), bag_cache)
- for(var/obj/item/mod/modsuit_or_module in bagholding)
- var/datum/storage/storage = modsuit_or_module.atom_storage
- if(istype(storage, /datum/storage/bag_of_holding) && storage.real_location == storage.parent)
- continue
- bagholding -= modsuit_or_module
- if(bagholding.len)
- precision = max(rand(1,100)*bagholding.len,100)
+ var/interference = 0
+ for(var/obj/item/check as anything in teleatom.get_all_contents_type(/obj/item))
+ if(check.item_flags & BLUESPACE_INTERFERENCE)
+ interference += 1
+ if(interference)
+ precision = max(rand(1,100)*interference,100)
if(isliving(teleatom))
var/mob/living/MM = teleatom
- to_chat(MM, span_warning("The bluespace interface on your bag of holding interferes with the teleport!"))
-
+ to_chat(MM, span_warning("The clashing pulls of bluespace interfere with your teleport!"))
+
// if effects are not specified and not explicitly disabled, sparks
if((!effectin || !effectout) && !no_effects)
var/datum/effect_system/basic/spark_spread/sparks = new(teleatom, 5, TRUE)
diff --git a/code/datums/hotkeys_help.dm b/code/datums/hotkeys_help.dm
index 0e90c7dffe5d..5fe826f3682b 100644
--- a/code/datums/hotkeys_help.dm
+++ b/code/datums/hotkeys_help.dm
@@ -10,6 +10,15 @@
ui = new(user, src, "HotkeysHelp")
ui.open()
+/datum/hotkeys_help/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ var/mob/user = ui.user
+ if(action == "open_keybindings")
+ user.client.prefs.current_window = PREFERENCE_TAB_KEYBINDINGS
+ user.client.prefs.ui_interact(usr)
+ user.client.uiclose(ui.window.id)
+ return TRUE
+
// Not static data since user could rebind keys.
/datum/hotkeys_help/ui_data(mob/user)
// List every keybind to chat.
diff --git a/code/datums/keybinding/communication.dm b/code/datums/keybinding/communication.dm
index a5aa69812433..767bc897cbd3 100644
--- a/code/datums/keybinding/communication.dm
+++ b/code/datums/keybinding/communication.dm
@@ -15,7 +15,7 @@
winset(user, null, "command=[VERB_SAY]")
return TRUE
winset(user, null, "command=[user.tgui_say_create_open_command(SAY_CHANNEL)];")
- winset(user, "tgui_say.browser", "focus=true")
+ winset(user, SKIN_TGUISAY_BROWSER, "focus=true")
return TRUE
/datum/keybinding/client/communication/radio
@@ -32,7 +32,7 @@
winset(user, null, "command=[VERB_SAY]")
return TRUE
winset(user, null, "command=[user.tgui_say_create_open_command(RADIO_CHANNEL)]")
- winset(user, "tgui_say.browser", "focus=true")
+ winset(user, SKIN_TGUISAY_BROWSER, "focus=true")
return TRUE
/datum/keybinding/client/communication/ooc
@@ -49,7 +49,7 @@
winset(user, null, "command=[VERB_OOC]")
return TRUE
winset(user, null, "command=[user.tgui_say_create_open_command(OOC_CHANNEL)]")
- winset(user, "tgui_say.browser", "focus=true")
+ winset(user, SKIN_TGUISAY_BROWSER, "focus=true")
return TRUE
/datum/keybinding/client/communication/me
@@ -66,7 +66,7 @@
winset(user, null, "command=[VERB_ME]")
return TRUE
winset(user, null, "command=[user.tgui_say_create_open_command(ME_CHANNEL)]")
- winset(user, "tgui_say.browser", "focus=true")
+ winset(user, SKIN_TGUISAY_BROWSER, "focus=true")
return TRUE
/datum/keybinding/client/communication/pray
diff --git a/code/datums/keybinding/living.dm b/code/datums/keybinding/living.dm
index 7a8b28cad2f9..ee3285dda40f 100644
--- a/code/datums/keybinding/living.dm
+++ b/code/datums/keybinding/living.dm
@@ -227,3 +227,27 @@
if(!HAS_TRAIT(living_user, TRAIT_CAN_HOLD_ITEMS))
return
living_user.give()
+
+/datum/keybinding/living/view_pet_data
+ hotkey_keys = list("Shift")
+ name = "view_pet_commands"
+ full_name = "View Pet Commands"
+ description = "Hold down to see all the commands you can give your pets!"
+ keybind_signal = COMSIG_KB_LIVING_VIEW_PET_COMMANDS
+
+/datum/keybinding/living/cancel_interactions
+ name = "stop_interactions"
+ full_name = "Cancel Interactions"
+ description = "Cancels any ongoing interactions (such as using a tool, performing surgery, or climbing). \
+ Note that some interactions cannot be interrupted, and you can't cancel other player's interaction with this hotkey."
+ keybind_signal = COMSIG_KB_LIVING_STOP_INTERACTIONS_DOWN
+
+/datum/keybinding/living/cancel_interactions/down(client/user, turf/target, mousepos_x, mousepos_y)
+ . = ..()
+ if(.)
+ return
+ var/mob/living/mob_user = user.mob
+ if(!LAZYLEN(mob_user.do_afters) || HAS_TRAIT(mob_user, TRAIT_INCAPACITATED))
+ return
+ // this is currently the best way to stop all ongoing doafters
+ mob_user.incapacitate(0.1 SECONDS, ignore_canstun = TRUE)
diff --git a/code/datums/keybinding/mob.dm b/code/datums/keybinding/mob.dm
index 86bf3ab9933f..a35937fe5559 100644
--- a/code/datums/keybinding/mob.dm
+++ b/code/datums/keybinding/mob.dm
@@ -161,7 +161,7 @@
/datum/keybinding/mob/target/head_cycle
hotkey_keys = list("Numpad8")
name = "target_head_cycle"
- full_name = "Target: Cycle Head"
+ full_name = "Target: Cycle head"
description = "Pressing this key targets the head, and continued presses will cycle to the eyes and mouth. This will impact where you hit people, and can be used for surgery."
keybind_signal = COMSIG_KB_MOB_TARGETCYCLEHEAD_DOWN
@@ -189,7 +189,7 @@
/datum/keybinding/mob/target/r_arm
hotkey_keys = list("Numpad4")
name = "target_r_arm"
- full_name = "Target: right arm"
+ full_name = "Target: Right arm"
description = "Pressing this key targets the right arm. This will impact where you hit people, and can be used for surgery."
keybind_signal = COMSIG_KB_MOB_TARGETRIGHTARM_DOWN
@@ -203,7 +203,7 @@
/datum/keybinding/mob/target/left_arm
hotkey_keys = list("Numpad6")
name = "target_left_arm"
- full_name = "Target: left arm"
+ full_name = "Target: Left arm"
description = "Pressing this key targets the body. This will impact where you hit people, and can be used for surgery."
keybind_signal = COMSIG_KB_MOB_TARGETLEFTARM_DOWN
@@ -224,7 +224,7 @@
/datum/keybinding/mob/target/left_leg
hotkey_keys = list("Numpad3")
name = "target_left_leg"
- full_name = "Target: left leg"
+ full_name = "Target: Left leg"
description = "Pressing this key targets the left leg. This will impact where you hit people, and can be used for surgery."
keybind_signal = COMSIG_KB_MOB_TARGETLEFTLEG_DOWN
@@ -246,10 +246,3 @@
if(.)
return
user.movement_locked = FALSE
-
-/datum/keybinding/living/view_pet_data
- hotkey_keys = list("Shift")
- name = "view_pet_commands"
- full_name = "View Pet Commands"
- description = "Hold down to see all the commands you can give your pets!"
- keybind_signal = COMSIG_KB_LIVING_VIEW_PET_COMMANDS
diff --git a/code/datums/lazy_template.dm b/code/datums/lazy_template.dm
index 9fe85cbcbcd2..d527d46b6d65 100644
--- a/code/datums/lazy_template.dm
+++ b/code/datums/lazy_template.dm
@@ -112,6 +112,10 @@
reservations += reservation
return reservation
+/datum/lazy_template/nukie_elevator
+ key = LAZY_TEMPLATE_KEY_NUKIEELEVATOR
+ map_name = "nukie_elevator"
+
/datum/lazy_template/nukie_base
key = LAZY_TEMPLATE_KEY_NUKIEBASE
map_name = "nukie_base"
diff --git a/code/datums/looping_sounds/_looping_sound.dm b/code/datums/looping_sounds/_looping_sound.dm
index 7080033c63f3..d4bd2f0ddf94 100644
--- a/code/datums/looping_sounds/_looping_sound.dm
+++ b/code/datums/looping_sounds/_looping_sound.dm
@@ -67,6 +67,10 @@
var/reserve_random_channel = FALSE
//If we reserve a random sound channel, store the channel number here so we can clean it up later.
var/reserved_channel
+ ///Whether this looping sound uses sound tokens. This should only be true for sounds that need to update as the source or listeners move. (Generally long or important sounds like grav-gen)
+ var/use_sound_tokens = FALSE
+ ///The sound token instance for this looping sound.
+ var/datum/sound_token/sound_token_instance
/datum/looping_sound/New(
_parent,
@@ -104,12 +108,12 @@
if(timer_id)
return
- if(!sound_channel && reserve_random_channel)
- sound_channel = SSsounds.reserve_sound_channel_datumless()
+ if(!use_sound_tokens && !sound_channel && reserve_random_channel)
+ sound_channel = SSsounds.reserve_sound_channel()
reserved_channel = sound_channel
-
on_start()
+
/**
* The proc to call to stop the sound loop.
*
@@ -171,6 +175,14 @@
* * volume_override - The volume we want to play the sound at, overriding the `volume` variable.
*/
/datum/looping_sound/proc/play(soundfile, volume_override)
+
+ if(use_sound_tokens)
+ if(sound_token_instance)
+ sound_token_instance.set_volume(volume_override || volume, FALSE) // Don't update, we'll do that after
+ sound_token_instance.update_sound(soundfile, TRUE)
+ else
+ sound_token_instance = new /datum/sound_token(parent, soundfile, SOUND_RANGE + extra_range, volume_override || volume, falloff_exponent, falloff_distance)
+ return
var/sound/sound_to_play = sound(soundfile)
sound_to_play.channel = sound_channel || SSsounds.random_available_channel()
sound_to_play.volume = volume_override || volume //Use volume as fallback if theres no override
@@ -248,6 +260,7 @@
/// Stops sound playing on current channel, if specified
/datum/looping_sound/proc/stop_current()
+ QDEL_NULL(sound_token_instance)
if(!sound_channel || !ismob(parent))
return
var/mob/mob_parent = parent
diff --git a/code/datums/looping_sounds/burning.dm b/code/datums/looping_sounds/burning.dm
index 191ae88db892..8673a9ab1dec 100644
--- a/code/datums/looping_sounds/burning.dm
+++ b/code/datums/looping_sounds/burning.dm
@@ -7,3 +7,4 @@
volume = 50
vary = TRUE
extra_range = MEDIUM_RANGE_SOUND_EXTRARANGE
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/changeling_absorb.dm b/code/datums/looping_sounds/changeling_absorb.dm
index 418c2a8dacf5..6b28c56ee47c 100644
--- a/code/datums/looping_sounds/changeling_absorb.dm
+++ b/code/datums/looping_sounds/changeling_absorb.dm
@@ -12,3 +12,4 @@
mid_length = 3 SECONDS
volume = 80
ignore_walls = FALSE
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/choking.dm b/code/datums/looping_sounds/choking.dm
index 6d337b1c7d64..bf0222b038ec 100644
--- a/code/datums/looping_sounds/choking.dm
+++ b/code/datums/looping_sounds/choking.dm
@@ -10,3 +10,4 @@
vary = TRUE
// Same as above
ignore_walls = FALSE
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/cyborg.dm b/code/datums/looping_sounds/cyborg.dm
index 0a01a4c7116b..1b23271ee331 100644
--- a/code/datums/looping_sounds/cyborg.dm
+++ b/code/datums/looping_sounds/cyborg.dm
@@ -8,3 +8,4 @@
end_sound = 'sound/mobs/non-humanoids/cyborg/wash_end.ogg'
vary = TRUE
extra_range = 5
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/drip.dm b/code/datums/looping_sounds/drip.dm
index 224d7850fc29..2c6d21e273a7 100644
--- a/code/datums/looping_sounds/drip.dm
+++ b/code/datums/looping_sounds/drip.dm
@@ -7,3 +7,4 @@
vary = TRUE
ignore_walls = FALSE
falloff_distance = 5
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/item_sounds.dm b/code/datums/looping_sounds/item_sounds.dm
index e5950566d4f3..517dc752944d 100644
--- a/code/datums/looping_sounds/item_sounds.dm
+++ b/code/datums/looping_sounds/item_sounds.dm
@@ -2,16 +2,19 @@
mid_sounds = list('sound/effects/clock_tick.ogg' = 1)
mid_length = 0.35 SECONDS
volume = 25
+ use_sound_tokens = TRUE
/datum/looping_sound/reverse_bear_trap_beep
mid_sounds = list('sound/machines/beep/beep.ogg' = 1)
mid_length = 6 SECONDS
volume = 10
+ use_sound_tokens = TRUE
/datum/looping_sound/siren
mid_sounds = list('sound/items/weeoo1.ogg' = 1)
mid_length = 1.5 SECONDS
volume = 20
+ use_sound_tokens = TRUE
/datum/looping_sound/tape_recorder_hiss
mid_sounds = list('sound/items/taperecorder/taperecorder_hiss_mid.ogg' = 1)
@@ -29,6 +32,7 @@
falloff_exponent = 10
falloff_distance = 1
volume = 5
+ use_sound_tokens = TRUE
/datum/looping_sound/chainsaw
start_sound = list('sound/items/weapons/chainsaw_start.ogg' = 1)
@@ -39,6 +43,7 @@
end_volume = 35
volume = 40
ignore_walls = FALSE
+ use_sound_tokens = TRUE
/datum/looping_sound/beesmoke
mid_sounds = list('sound/items/weapons/beesmoke.ogg' = 1)
@@ -59,3 +64,4 @@
end_volume = 15
ignore_walls = FALSE
reserve_random_channel = TRUE
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm
index 871a5ca7bf90..a8549ab1418b 100644
--- a/code/datums/looping_sounds/machinery_sounds.dm
+++ b/code/datums/looping_sounds/machinery_sounds.dm
@@ -18,13 +18,15 @@
falloff_exponent = 10
falloff_distance = 5
vary = TRUE
+ use_sound_tokens = TRUE
/datum/looping_sound/destabilized_crystal
mid_sounds = list('sound/machines/sm/loops/delamming.ogg')
mid_length = 6 SECONDS
volume = 55
- extra_range = 15
+ extra_range = 35
vary = TRUE
+ use_sound_tokens = TRUE
/datum/looping_sound/hypertorus
mid_sounds = list('sound/machines/hypertorus/loops/hypertorus_nominal.ogg')
@@ -32,6 +34,7 @@
volume = 55
extra_range = 15
vary = TRUE
+ use_sound_tokens = TRUE
/datum/looping_sound/generator
start_sound = 'sound/machines/generator/generator_start.ogg'
@@ -52,9 +55,10 @@
'sound/machines/fryer/deep_fryer_1.ogg',
'sound/machines/fryer/deep_fryer_2.ogg',
)
- mid_length = 0.2 SECONDS
+ mid_length = 1 SECONDS
end_sound = 'sound/machines/fryer/deep_fryer_emerge.ogg'
volume = 15
+ use_sound_tokens = TRUE
/datum/looping_sound/clock
mid_sounds = list('sound/ambience/misc/ticking_clock.ogg')
@@ -66,6 +70,7 @@
mid_sounds = list('sound/machines/grill/grillsizzle.ogg')
mid_length = 18
volume = 50
+ use_sound_tokens = TRUE
/datum/looping_sound/oven
start_sound = 'sound/machines/oven/oven_loop_start.ogg' //my immersions
@@ -75,14 +80,7 @@
end_sound = 'sound/machines/oven/oven_loop_end.ogg'
volume = 100
falloff_exponent = 4
-
-/datum/looping_sound/deep_fryer
- mid_length = 0.2 SECONDS
- mid_sounds = list(
- 'sound/machines/fryer/deep_fryer_1.ogg',
- 'sound/machines/fryer/deep_fryer_2.ogg',
- )
- volume = 30
+ use_sound_tokens = TRUE
/datum/looping_sound/microwave
start_sound = 'sound/machines/microwave/microwave-start.ogg'
@@ -94,6 +92,7 @@
mid_length = 1 SECONDS
end_sound = 'sound/machines/microwave/microwave-end.ogg'
volume = 90
+ use_sound_tokens = TRUE
/datum/looping_sound/lathe_print
mid_sounds = list('sound/machines/lathe/lathe_print.ogg')
@@ -103,12 +102,14 @@
ignore_walls = FALSE
falloff_distance = 1
mid_length_vary = 1 SECONDS
+ use_sound_tokens = TRUE
/datum/looping_sound/jackpot
mid_length = 1.1 SECONDS
mid_sounds = list('sound/machines/roulette/roulettejackpot.ogg')
volume = 85
vary = TRUE
+ use_sound_tokens = TRUE
/datum/looping_sound/server
mid_sounds = list(
@@ -139,10 +140,11 @@
mid_length = 1.8 SECONDS
end_sound = 'sound/machines/computer/computer_end.ogg'
end_volume = 1 SECONDS
- volume = SOUND_AUDIBLE_VOLUME_MIN
- falloff_exponent = 5 //Ultra quiet very fast
+ volume = 3
+ falloff_exponent = 4 //Ultra quiet very fast
extra_range = -12
- falloff_distance = 1 //Instant falloff after initial tile
+ falloff_distance = 0 //Instant falloff after initial tile
+ use_sound_tokens = TRUE
/datum/looping_sound/gravgen
start_sound = 'sound/machines/gravgen/grav_gen_start.ogg'
@@ -157,7 +159,8 @@
vary = TRUE
volume = 70
falloff_distance = 5
- falloff_exponent = 20
+ falloff_exponent = 10
+ use_sound_tokens = TRUE
/datum/looping_sound/firealarm
mid_sounds = list(
@@ -168,16 +171,19 @@
)
mid_length = 2.4 SECONDS
volume = 30
+ use_sound_tokens = TRUE
/datum/looping_sound/gravgen/kinesis
volume = 20
falloff_distance = 2
falloff_exponent = 5
+ use_sound_tokens = TRUE
/datum/looping_sound/boiling
mid_sounds = list('sound/effects/bubbles/bubbles2.ogg')
mid_length = 7 SECONDS
volume = 25
+ use_sound_tokens = TRUE
/datum/looping_sound/typing
mid_sounds = list(
@@ -205,7 +211,8 @@
end_sound = 'sound/effects/soup_boil/soup_boil_end.ogg'
end_volume = 60
extra_range = MEDIUM_RANGE_SOUND_EXTRARANGE
- falloff_exponent = 4
+ falloff_exponent = 3
+ use_sound_tokens = TRUE
/datum/looping_sound/soup/toxic
volume = 40
@@ -225,3 +232,4 @@
)
mid_length = 5 SECONDS
volume = 50
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/projectiles.dm b/code/datums/looping_sounds/projectiles.dm
index ca96df698e92..1c2dc9b19b72 100644
--- a/code/datums/looping_sounds/projectiles.dm
+++ b/code/datums/looping_sounds/projectiles.dm
@@ -2,3 +2,4 @@
mid_sounds = list('sound/effects/moon_parade_soundloop.ogg' = 1)
mid_length = 2 SECONDS
volume = 20
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/vents.dm b/code/datums/looping_sounds/vents.dm
index 183c337ef981..04e0246a4ee5 100644
--- a/code/datums/looping_sounds/vents.dm
+++ b/code/datums/looping_sounds/vents.dm
@@ -2,5 +2,6 @@
start_sound = 'sound/machines/fan/fan_start.ogg'
start_length = 1.5 SECONDS
end_sound = 'sound/machines/fan/fan_stop.ogg'
- end_sound = 1.5 SECONDS
mid_sounds = 'sound/machines/fan/fan_loop.ogg'
+ mid_length = 1.9 SECONDS
+ use_sound_tokens = TRUE
diff --git a/code/datums/mapgen/CaveGenerator.dm b/code/datums/mapgen/CaveGenerator.dm
index 8d78ce25c47f..316f9659e91d 100644
--- a/code/datums/mapgen/CaveGenerator.dm
+++ b/code/datums/mapgen/CaveGenerator.dm
@@ -41,6 +41,8 @@
/// of biome-related operations. Is populated through
/// `generate_terrain_with_biomes()`.
var/list/generated_turfs_per_biome = list()
+ /// Same as generated_turfs_per_biome, but additionally indexed by area
+ var/list/generated_turfs_per_area_biome = list()
/// 2D list of all biomes based on heat and humidity combos. Associative by
/// `BIOME_X_HEAT` and then by `BIOME_X_HUMIDITY` (i.e.
/// `possible_biomes[BIOME_LOW_HEAT][BIOME_LOWMEDIUM_HUMIDITY]`).
@@ -65,14 +67,16 @@
var/shared_seed = TRUE
/// Stamp size for DBP noise, aka frequency
var/biome_stamp_size = 75
+ /// Stored noise maps per z level if we use a shared seed
+ var/static_biome_maps = list()
- ///Base chance of spawning a mob
+ /// Base chance of spawning a mob
var/mob_spawn_chance = 6
- ///Base chance of spawning flora
+ /// Base chance of spawning flora
var/flora_spawn_chance = 2
- ///Base chance of spawning features
+ /// Base chance of spawning features
var/feature_spawn_chance = 0.25
- ///Unique ID for this spawner
+ /// Unique ID for this spawner
var/string_gen
/// Radius around features within which we avoid spawning other features
@@ -82,15 +86,31 @@
/// Radius around megafauna within which we avoid spawning tendrils
var/megafauna_exclusion_radius = 7
- ///Chance of cells starting closed
- var/initial_closed_chance = 45
- ///Amount of smoothing iterations
- var/smoothing_iterations = 20
- ///How much neighbours does a dead cell need to become alive
- var/birth_limit = 4
- ///How little neighbours does a alive cell need to die
- var/death_limit = 3
+ ///Cave gen settings below!!
+
+ /// Minimum dimension of a BSP leaf in the generator. Raising this creates larger pockets but can end up making for big corridors
+ var/min_bsp_size = 25
+ /// Maximum aspect ratio for BSP splits for lavaland generator
+ var/max_ratio = 1.5
+ /// Room edge padding within BSP leaf for lavaland generator
+ var/padding = 1
+ /// How much of each BSP leaf is considered untouchable by the cellular automata. Raising this generally means bigger pockets
+ var/room_fill_percent = 30
+ /// Width of corridors between rooms for lavaland generator. Raising this just means corridors are AT LEAST this wide. but cellular automata can make them bigger
+ var/corridor_width = 1
+ /// Chance to add extra MST edges for loops for lavaland generator. This basically results in more corridors / mazier generation
+ var/loop_percent = 15
+ /// Initial random floor density for lavaland generator
+ var/noise_percent = 51
+ /// Cellular Automata smoothing iterations for lavaland generator
+ var/ca_steps = 8
+ /// Neighbors to create floor (>=) for lavaland generator
+ var/birth_limit = 6
+ /// Neighbors to survive as floor (>=) for lavaland generator
+ var/survival_limit = 4
+ ///Whether out-of-boudns counts as being alive. Setting this to FALSE results in the edges of the generator generating more closed. Default behavior tends to open up tunnels outside.
+ var/edges_are_alive = TRUE
/datum/map_generator/cave_generator/New()
. = ..()
@@ -134,10 +154,11 @@
return generate_terrain_with_biomes(turfs, generate_in)
var/start_time = REALTIMEOFDAY
- string_gen = rustg_cnoise_generate("[initial_closed_chance]", "[smoothing_iterations]", "[birth_limit]", "[death_limit]", "[world.maxx]", "[world.maxy]") //Generate the raw CA data
+
+ string_gen = generate_cave(generate_in)
for(var/turf/gen_turf as anything in turfs) //Go through all the turfs and generate them
- var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "0"
+ var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "1"
var/turf/new_turf = pick(closed ? closed_turf_types : open_turf_types)
// The assumption is this will be faster then changeturf, and changeturf isn't required since by this point
@@ -186,7 +207,7 @@
heat_seed = rand(0, 50000)
var/start_time = REALTIMEOFDAY
- string_gen = rustg_cnoise_generate("[initial_closed_chance]", "[smoothing_iterations]", "[birth_limit]", "[death_limit]", "[world.maxx]", "[world.maxy]") //Generate the raw CA data
+ string_gen = generate_cave(generate_in)
var/humidity_gen = list()
humidity_gen[BIOME_HIGH_HUMIDITY] = rustg_dbp_generate("[humidity_seed]", "60", "[biome_stamp_size]", "[world.maxx]", "[high_heat_threshold]", "1.1")
@@ -196,9 +217,17 @@
heat_gen[BIOME_HIGH_HEAT] = rustg_dbp_generate("[heat_seed]", "60", "[biome_stamp_size]", "[world.maxx]", "[high_heat_threshold]", "1.1")
heat_gen[BIOME_MEDIUM_HEAT] = rustg_dbp_generate("[heat_seed]", "60", "[biome_stamp_size]", "[world.maxx]", "[medium_heat_threshold]", "[high_heat_threshold]")
+ if (shared_seed)
+ static_biome_maps["[turfs[1].z]"] = list(
+ BIOME_HIGH_HUMIDITY = humidity_gen[BIOME_HIGH_HUMIDITY],
+ BIOME_MEDIUM_HUMIDITY = humidity_gen[BIOME_MEDIUM_HUMIDITY],
+ BIOME_HIGH_HEAT = heat_gen[BIOME_HIGH_HEAT],
+ BIOME_MEDIUM_HEAT = heat_gen[BIOME_MEDIUM_HEAT],
+ )
+
var/list/to_generate = list()
for(var/turf/gen_turf as anything in turfs) //Go through all the turfs and generate them
- var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "0"
+ var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "1"
var/datum/biome/selected_biome
// Here comes the meat of the biome code.
@@ -222,12 +251,40 @@
for(var/biome in to_generate)
var/datum/biome/generating_biome = SSmapping.biomes[biome]
var/list/turf/generated_turfs = generating_biome.generate_turfs_for_terrain(to_generate[biome])
- generated_turfs_per_biome[biome] = generated_turfs
+ generated_turfs_per_biome[biome] = (generated_turfs_per_biome[biome] || list()) + generated_turfs
+ var/list/area_list = generated_turfs_per_area_biome[biome]
+ if (!area_list)
+ area_list = list()
+ generated_turfs_per_area_biome[biome] = area_list
+ area_list[generate_in] = generated_turfs
var/message = "[name] terrain generation finished in [(REALTIMEOFDAY - start_time)/10]s!"
to_chat(world, span_boldannounce("[message]"), MESSAGE_TYPE_DEBUG)
log_world(message)
+/// Returns a biome datum that the turf was initialized with, or would be if it is present on our Z level and we use a consistent shared seed
+/// Not consistent between calls by itself due to using RNG in perlin zoom, needs a static coordinate-based formula
+/datum/map_generator/cave_generator/proc/get_biome_for_turf(turf/target)
+ for (var/biome in generated_turfs_per_biome)
+ var/list/generated_turfs = generated_turfs_per_biome[biome]
+ if (generated_turfs[target])
+ return biome
+
+ var/list/biome_map = static_biome_maps["[target.z]"]
+ if (!shared_seed || !biome_map)
+ return null
+
+ var/drift_x = clamp((target.x + rand(-BIOME_RANDOM_SQUARE_DRIFT, BIOME_RANDOM_SQUARE_DRIFT)), 1, world.maxx)
+ var/drift_y = clamp((target.y + rand(-BIOME_RANDOM_SQUARE_DRIFT, BIOME_RANDOM_SQUARE_DRIFT)), 2, world.maxy)
+ var/coordinate = world.maxx * (drift_y - 1) + drift_x
+
+ var/humidity_level = text2num(biome_map[BIOME_HIGH_HUMIDITY][coordinate]) ? \
+ BIOME_HIGH_HUMIDITY : text2num(biome_map[BIOME_MEDIUM_HUMIDITY][coordinate]) ? BIOME_MEDIUM_HUMIDITY : BIOME_LOW_HUMIDITY
+ var/heat_level = text2num(biome_map[BIOME_HIGH_HEAT][coordinate]) ? \
+ BIOME_HIGH_HEAT : text2num(biome_map[BIOME_MEDIUM_HEAT][coordinate]) ? BIOME_MEDIUM_HEAT : BIOME_LOW_HEAT
+
+ return possible_biomes[heat_level][humidity_level]
+
/datum/map_generator/cave_generator/populate_terrain(list/turfs, area/generate_in)
if (biome_population && length(possible_biomes))
return populate_terrain_with_biomes(turfs, generate_in)
@@ -285,7 +342,7 @@
is_megafauna = TRUE
var/can_spawn = TRUE
- if(ispath(picked_mob, /obj/structure/spawner/lavaland))
+ if(ispath(picked_mob, /mob/living/basic/mining/tendril))
// Prevents tendrils spawning in each other's collapse range
for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_TENDRIL])
if (get_dist(spawn_turf, target_turf) <= 2)
@@ -312,7 +369,7 @@
if (!can_spawn)
continue
- if (ispath(picked_mob, /obj/structure/spawner/lavaland))
+ if (ispath(picked_mob, /mob/living/basic/mining/tendril))
spawn_data[CAVE_SPAWN_TENDRIL] += target_turf
else
if (is_megafauna)
@@ -354,9 +411,10 @@
log_world(message)
return
- for(var/biome in generated_turfs_per_biome)
+ for(var/biome in generated_turfs_per_area_biome)
var/datum/biome/generating_biome = SSmapping.biomes[biome]
- generating_biome.populate_turfs(generated_turfs_per_biome[biome], flora_allowed, features_allowed, fauna_allowed)
+ var/list/areas_list = generated_turfs_per_area_biome[biome]
+ generating_biome.populate_turfs(areas_list[generate_in], flora_allowed, features_allowed, fauna_allowed)
CHECK_TICK
@@ -364,6 +422,32 @@
to_chat(world, span_boldannounce("[message]"), MESSAGE_TYPE_DEBUG)
log_world(message)
+
+///Generates the cave shape using Rust-G
+/datum/map_generator/cave_generator/proc/generate_cave(area/generate_in)
+
+ ///Loop through all the active ruins for this z-level and make a json format out of it so we can send it to the generator
+ var/list/active_ruins_list = list()
+
+ for(var/turf/bottom_left_turf in SSmapping.active_ruins)
+ var/datum/map_template/ruin/active_ruin = SSmapping.active_ruins[bottom_left_turf]
+ if(bottom_left_turf.z != generate_in.z)
+ continue
+ active_ruins_list += list(list(
+ "x" = max(1, bottom_left_turf.x - active_ruin.terrain_padding),
+ "y" = max(1, bottom_left_turf.y - active_ruin.terrain_padding),
+ "w" = active_ruin.width + active_ruin.terrain_padding * 2,
+ "h" = active_ruin.height + active_ruin.terrain_padding * 2,
+ "isEnclosed" = active_ruin.enclosed_for_terrain,
+ ))
+
+ var/active_ruin_string = json_encode(active_ruins_list)
+
+ var/string_gen = rustg_cave_system_generator_generate("[world.maxx]", "[world.maxy]", active_ruin_string, "[min_bsp_size]","[max_ratio]", "[padding]", "[room_fill_percent]", "[corridor_width]","[loop_percent]", "[noise_percent]", "[ca_steps]", "[birth_limit]", "[survival_limit]", "[edges_are_alive]")
+
+ return string_gen
+
+
/datum/map_generator/cave_generator/jungle
possible_biomes = list(
BIOME_LOW_HEAT = list(
diff --git a/code/datums/mapgen/Cavegens/IcemoonCaves.dm b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
index 153b0c22e804..45f429c0fe80 100644
--- a/code/datums/mapgen/Cavegens/IcemoonCaves.dm
+++ b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
@@ -11,7 +11,7 @@
/mob/living/basic/mining/lobstrosity = 15,
/mob/living/basic/mining/wolf = 50,
/obj/effect/spawner/random/lavaland_mob/raptor = 15,
- /mob/living/simple_animal/hostile/asteroid/polarbear = 30,
+ /mob/living/basic/mining/polarbear = 30,
/obj/structure/spawner/ice_moon = 3,
/obj/structure/spawner/ice_moon/polarbear = 3,
)
@@ -39,10 +39,7 @@
weighted_open_turf_types = list(/turf/open/misc/asteroid/snow/icemoon = 1)
flora_spawn_chance = 60
weighted_mob_spawn_list = null
- initial_closed_chance = 0
- birth_limit = 5
- death_limit = 4
- smoothing_iterations = 10
+ noise_percent = 100 // Full floor
feature_spawn_chance = 0.15
weighted_feature_spawn_list = list(
@@ -62,7 +59,7 @@
/// Surface snow generator variant for forested station trait, WITH FORESTSSSS
/datum/map_generator/cave_generator/icemoon/surface/forested
- initial_closed_chance = 10
+ noise_percent = 65 //Few small rocks, but mostly open floor for the trees to spawn on
flora_spawn_chance = 80
weighted_flora_spawn_list = list(
@@ -80,7 +77,6 @@
weighted_mob_spawn_list = list(/mob/living/basic/deer/ice = 99, /mob/living/basic/tree = 1, /obj/effect/spawner/random/lavaland_mob/raptor = 15)
/datum/map_generator/cave_generator/icemoon/surface/rocky
- initial_closed_chance = 53
mob_spawn_chance = 0.5
/datum/map_generator/cave_generator/icemoon/surface/noruins //use this for when you don't want ruins to spawn in a certain area
diff --git a/code/datums/mapgen/Cavegens/LavalandGenerator.dm b/code/datums/mapgen/Cavegens/LavalandGenerator.dm
index e464c62dc33d..b40ea70beda8 100644
--- a/code/datums/mapgen/Cavegens/LavalandGenerator.dm
+++ b/code/datums/mapgen/Cavegens/LavalandGenerator.dm
@@ -20,50 +20,11 @@
),
)
- biome_population = FALSE
-
high_heat_threshold = 0.15
high_humidity_threshold = 0.15
biome_stamp_size = 60
-
- weighted_mob_spawn_list = list(
- SPAWN_MEGAFAUNA = 2,
- /obj/effect/spawner/random/lavaland_mob/goliath = 50,
- /obj/effect/spawner/random/lavaland_mob/legion = 30,
- /obj/effect/spawner/random/lavaland_mob/watcher = 40,
- /mob/living/basic/mining/bileworm = 20,
- /mob/living/basic/mining/brimdemon = 20,
- /mob/living/basic/mining/lobstrosity/lava = 20,
- /obj/effect/spawner/random/lavaland_mob/raptor = 15,
- /mob/living/basic/mining/goldgrub = 15,
- /obj/structure/spawner/lavaland = 2,
- /obj/structure/spawner/lavaland/goliath = 3,
- /obj/structure/spawner/lavaland/legion = 3,
- )
-
- weighted_flora_spawn_list = list(
- /obj/structure/flora/ash/cacti = 1,
- /obj/structure/flora/ash/cap_shroom = 2,
- /obj/structure/flora/ash/fireblossom = 2,
- /obj/structure/flora/ash/leaf_shroom = 2,
- /obj/structure/flora/ash/seraka = 2,
- /obj/structure/flora/ash/stem_shroom = 2,
- /obj/structure/flora/ash/tall_shroom = 2,
- )
-
- ///Note that this spawn list is also in the icemoon generator
- weighted_feature_spawn_list = list(
- /obj/structure/geyser/hollowwater = 8,
- /obj/structure/geyser/plasma_oxide = 8,
- /obj/structure/geyser/protozine = 8,
- /obj/structure/geyser/random = 2,
- /obj/structure/geyser/wittel = 8,
- /obj/structure/geyser/chiral_buffer = 8,
- /obj/structure/ore_vent/boss = 1,
- )
-
- smoothing_iterations = 50
-
+
/datum/map_generator/cave_generator/lavaland/ruin_version
+ biome_population = FALSE
weighted_open_turf_types = list(/turf/open/misc/asteroid/basalt/lava_land_surface/no_ruins = 1)
weighted_closed_turf_types = list(/turf/closed/mineral/volcanic/lava_land_surface/do_not_chasm = 1)
diff --git a/code/datums/mapgen/biomes/_biome.dm b/code/datums/mapgen/biomes/_biome.dm
index d671f50e1f8e..dc2a17563b37 100644
--- a/code/datums/mapgen/biomes/_biome.dm
+++ b/code/datums/mapgen/biomes/_biome.dm
@@ -1,3 +1,8 @@
+#define CAVE_SPAWN_MOB "mob"
+#define CAVE_SPAWN_FEATURE "feature"
+#define CAVE_SPAWN_TENDRIL "tendril"
+#define CAVE_SPAWN_MEGAFAUNA "megafauna"
+
///This datum handles the transitioning from a turf to a specific biome, and handles spawning decorative structures and mobs.
/datum/biome
/// Type of turf this biome creates for open turfs
@@ -19,7 +24,17 @@
/// Weighted list of type paths of fauna that can be spawned when the
/// turf spawns fauna.
var/list/fauna_types = list()
-
+ /// Weighted list of megafauna that can spawn from SPAWN_MEGAFAUNA fauna entry
+ /// Defaults to GLOB.megafauna_spawn_list
+ var/list/megafauna_types = null
+ /// Radius around features within which we avoid spawning other features
+ var/feature_exclusion_radius = 7
+ /// Radius around mobs which we avoid spawning other mobs
+ var/mob_exclusion_radius = 12
+ /// Radius around megafauna within which we avoid spawning tendrils
+ var/megafauna_exclusion_radius = 7
+ /// Minimum distance between tendril spawns
+ var/tendril_exclusion_radius = 12
/datum/biome/New()
. = ..()
@@ -32,6 +47,8 @@
if(length(feature_types))
feature_types = expand_weights(feature_types)
+ if(isnull(megafauna_types))
+ megafauna_types = GLOB.megafauna_spawn_list
///This proc handles the creation of a turf of a specific biome type
/datum/biome/proc/generate_turf(turf/gen_turf, closed)
@@ -107,57 +124,95 @@
if(!has_flora && !has_features && !has_fauna)
return
+ // Assoc list of spawned categories to
+ // Static as to be shared between all biomes
+ var/static/list/spawn_data
+ if (isnull(spawn_data))
+ spawn_data = list(
+ CAVE_SPAWN_FEATURE = list(),
+ CAVE_SPAWN_TENDRIL = list(),
+ CAVE_SPAWN_MOB = list(),
+ CAVE_SPAWN_MEGAFAUNA = list(),
+ )
+
for(var/turf/target_turf as anything in target_turfs)
- // We do the CHECK_TICK here because there's a bunch of continue calls
- // in this.
+ // Only put stuff on open turfs we generated, so closed walls and rivers and stuff are skipped
+ if(!istype(target_turf, open_turf_type))
+ continue
+
CHECK_TICK
+ if (!(target_turf.turf_flags & TURF_BLOCKS_POPULATE_TERRAIN_FLORAFEATURES))
+ if (flora_allowed && prob(flora_density))
+ var/flora_type = pick(flora_types)
+ new flora_type(target_turf)
+ continue
- if(istype(target_turf, closed_turf_type))
- continue
+ if (features_allowed && prob(feature_density))
+ var/can_spawn = TRUE
+ for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_FEATURE])
+ if (get_dist(spawn_turf, target_turf) <= feature_exclusion_radius)
+ can_spawn = FALSE
+ break
- if(has_flora && prob(flora_density))
- var/obj/structure/flora = pick(flora_types)
- new flora(target_turf)
- continue
+ if (can_spawn)
+ var/picked_feature = pick(feature_types)
+ new picked_feature(target_turf)
+ spawn_data[CAVE_SPAWN_FEATURE] += target_turf
+ continue
- if(has_features && prob(feature_density))
- var/can_spawn = TRUE
+ if (!fauna_allowed || !prob(fauna_density))
+ continue
- var/atom/picked_feature = pick(feature_types)
+ var/picked_mob = pick(fauna_types)
+ var/is_megafauna = FALSE
+ if (picked_mob == SPAWN_MEGAFAUNA)
+ picked_mob = pick_weight(megafauna_types)
+ is_megafauna = TRUE
- var/list/features_in_range = range(7, target_turf)
- for(var/obj/structure/existing_feature in features_in_range)
- if(istype(existing_feature, picked_feature))
+ var/can_spawn = TRUE
+ if(ispath(picked_mob, /mob/living/basic/mining/tendril))
+ for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_TENDRIL])
+ if (get_dist(spawn_turf, target_turf) <= tendril_exclusion_radius)
can_spawn = FALSE
break
- if(can_spawn)
- new picked_feature(target_turf)
+ if (!can_spawn)
continue
- if(has_fauna && prob(fauna_density))
- var/mob/picked_mob = pick(fauna_types)
+ // Also avoid spawning them next to megafauna
+ for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MEGAFAUNA])
+ if (get_dist(spawn_turf, target_turf) <= megafauna_exclusion_radius)
+ can_spawn = FALSE
+ break
- // prevents tendrils spawning in each other's collapse range
- if(ispath(picked_mob, /obj/structure/spawner/lavaland))
- var/blocked = FALSE
- for(var/obj/structure/spawner/lavaland/spawn_blocker in range(2, target_turf))
- blocked = TRUE
+ else if (is_megafauna)
+ // Megafauna can spawn wherever it wants as long as its not next to another mega
+ for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MEGAFAUNA])
+ if (get_dist(spawn_turf, target_turf) <= megafauna_exclusion_radius)
+ can_spawn = FALSE
+ break
+ else
+ for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MOB])
+ if (get_dist(spawn_turf, target_turf) <= mob_exclusion_radius)
+ can_spawn = FALSE
break
- if(blocked)
- continue
+ if (!can_spawn)
+ continue
- // if the random is not a tendril (hopefully meaning it is a mob), avoid spawning if there's another one within 12 tiles
- else
- var/list/things_in_range = range(12, target_turf)
- var/blocked = FALSE
- for(var/mob/living/mob_blocker in things_in_range)
- if(ismining(mob_blocker))
- blocked = TRUE
- break
+ if (ispath(picked_mob, /mob/living/basic/mining/tendril))
+ spawn_data[CAVE_SPAWN_TENDRIL] += target_turf
+ else if (is_megafauna)
+ spawn_data[CAVE_SPAWN_MEGAFAUNA] += target_turf
+ spawn_data[CAVE_SPAWN_MOB] += target_turf
- if(blocked)
- continue
+ new picked_mob(target_turf)
+
+ // There can be only one Bubblegum, so don't waste spawns on it
+ if(ispath(picked_mob, /mob/living/simple_animal/hostile/megafauna/bubblegum))
+ megafauna_types.Remove(picked_mob)
- new picked_mob(target_turf)
+#undef CAVE_SPAWN_MOB
+#undef CAVE_SPAWN_FEATURE
+#undef CAVE_SPAWN_TENDRIL
+#undef CAVE_SPAWN_MEGAFAUNA
diff --git a/code/datums/mapgen/biomes/lavaland.dm b/code/datums/mapgen/biomes/lavaland.dm
index 9ebcd2d73eb0..88c907dc23fe 100644
--- a/code/datums/mapgen/biomes/lavaland.dm
+++ b/code/datums/mapgen/biomes/lavaland.dm
@@ -1,12 +1,113 @@
/datum/biome/lavaland
open_turf_type = /turf/open/misc/asteroid/basalt/lava_land_surface
closed_turf_type = /turf/closed/mineral/volcanic
+ mob_exclusion_radius = 7
/datum/biome/lavaland/basalt
closed_turf_type = /turf/closed/mineral/random/volcanic
+ fauna_density = 6
+ flora_density = 2.5
+ feature_density = 0.25
+
+ // Legions and goliaths, but fewer watchers and no demons
+ fauna_types = list(
+ SPAWN_MEGAFAUNA = 2,
+ /obj/effect/spawner/random/lavaland_mob/goliath = 50,
+ /obj/effect/spawner/random/lavaland_mob/legion = 40,
+ /obj/effect/spawner/random/lavaland_mob/watcher = 20,
+ /mob/living/basic/mining/bileworm = 10,
+ /mob/living/basic/mining/lobstrosity/lava = 20,
+ /obj/effect/spawner/random/lavaland_mob/raptor = 15,
+ /mob/living/basic/mining/goldgrub = 15,
+ /mob/living/basic/mining/tendril = 7,
+ )
+
+ flora_types = list(
+ /obj/structure/flora/ash/cap_shroom = 3,
+ /obj/structure/flora/ash/fireblossom = 1,
+ /obj/structure/flora/ash/stem_shroom = 1,
+ /obj/structure/flora/rock/style_random = 1,
+ /obj/structure/flora/rock/pile/style_random = 3,
+ /obj/structure/flora/rock/volcano = 1,
+ )
+
+ feature_types = list(
+ /obj/structure/geyser/plasma_oxide = 8,
+ /obj/structure/geyser/protozine = 8,
+ /obj/structure/geyser/wittel = 8,
+ /obj/structure/geyser/random = 3,
+ /obj/structure/ore_vent/boss = 1,
+ )
+
/datum/biome/lavaland/shale
+ open_turf_type = /turf/open/misc/asteroid/basalt/smooth/shale/lava_land_surface
closed_turf_type = /turf/closed/mineral/random/volcanic/shale
+ fauna_density = 6
+ flora_density = 5
+ feature_density = 0.25
+
+ // Higher chance of lobstrocities, goldgrubs and brimdemons, but no bileworms
+ fauna_types = list(
+ SPAWN_MEGAFAUNA = 2,
+ /obj/effect/spawner/random/lavaland_mob/goliath = 40,
+ /obj/effect/spawner/random/lavaland_mob/legion = 20,
+ /obj/effect/spawner/random/lavaland_mob/watcher = 30,
+ /mob/living/basic/mining/brimdemon = 40,
+ /mob/living/basic/mining/lobstrosity/lava = 40,
+ /obj/effect/spawner/random/lavaland_mob/raptor = 15,
+ /mob/living/basic/mining/goldgrub = 35,
+ /mob/living/basic/mining/tendril = 8,
+ )
+
+ flora_types = list(
+ /obj/structure/flora/ash/fireblossom = 2,
+ /obj/structure/flora/ash/leaf_shroom = 2,
+ /obj/structure/flora/ash/seraka = 2,
+ /obj/structure/flora/ash/glowgrowth = 2,
+ /obj/structure/flora/rock/pile/shale/style_random = 5,
+ )
+
+ feature_types = list(
+ /obj/structure/geyser/hollowwater = 12,
+ /obj/structure/geyser/plasma_oxide = 12,
+ /obj/structure/geyser/random = 3,
+ /obj/structure/ore_vent/boss = 1,
+ )
/datum/biome/lavaland/red_rock
+ open_turf_type = /turf/open/misc/asteroid/basalt/smooth/siderite/lava_land_surface
closed_turf_type = /turf/closed/mineral/random/volcanic/red_rock
+ fauna_density = 6
+ flora_density = 4
+ feature_density = 0.25
+
+ // Bileworms, raptors and watchers, but few goliaths
+ fauna_types = list(
+ SPAWN_MEGAFAUNA = 2,
+ /obj/effect/spawner/random/lavaland_mob/goliath = 20,
+ /obj/effect/spawner/random/lavaland_mob/legion = 25,
+ /obj/effect/spawner/random/lavaland_mob/watcher = 55,
+ /mob/living/basic/mining/bileworm = 35,
+ /mob/living/basic/mining/brimdemon = 15,
+ /mob/living/basic/mining/lobstrosity/lava = 25,
+ /obj/effect/spawner/random/lavaland_mob/raptor = 20,
+ /mob/living/basic/mining/goldgrub = 15,
+ /mob/living/basic/mining/tendril = 6,
+ )
+
+ flora_types = list(
+ /obj/structure/flora/ash/cacti = 2,
+ /obj/structure/flora/ash/cap_shroom = 1,
+ /obj/structure/flora/ash/tall_shroom = 2,
+ /obj/structure/flora/ash/stem_shroom = 2,
+ /obj/structure/flora/rock/pile/siderite/style_random = 4,
+ /obj/structure/flora/rock/siderite_growth = 1,
+ )
+
+ feature_types = list(
+ /obj/structure/geyser/protozine = 12,
+ /obj/structure/geyser/chiral_buffer = 12,
+ /obj/structure/geyser/random = 3,
+ /obj/structure/ore_vent/boss = 1,
+ )
diff --git a/code/datums/martial/_action.dm b/code/datums/martial/_action.dm
new file mode 100644
index 000000000000..72d3dadc39b6
--- /dev/null
+++ b/code/datums/martial/_action.dm
@@ -0,0 +1,39 @@
+/datum/action/swap_arts
+ name = "Remember the Basics" //this is dynamic, uses `update_button_name`
+ desc = "LMB: See your movelist. RMB: Swap artstyle, if you have more than one."
+ background_icon_state = "bg_martial_arts"
+ button_icon_state = "martial"
+
+ ///The martial arts currently used for the name & help information.
+ var/datum/martial_art/current_used_art
+
+/datum/action/swap_arts/Destroy()
+ current_used_art = null
+ return ..()
+
+/datum/action/swap_arts/New(Target, datum/martial_art/starting_style)
+ current_used_art = starting_style
+ return ..()
+
+/datum/action/swap_arts/update_button_name(atom/movable/screen/movable/action_button/button, force, datum/martial_art/new_art)
+ if(new_art)
+ current_used_art = new_art
+ name = "[current_used_art.help_verb]"
+ var/mob/living/living_owner = owner
+ if(LAZYLEN(living_owner.martial_arts) >= 2)
+ name += "/Swap Style"
+ return ..()
+
+/datum/action/swap_arts/Trigger(mob/living/clicker, trigger_flags)
+ . = ..()
+ var/mob/living/living_owner = owner
+ if(trigger_flags & TRIGGER_SECONDARY_ACTION && (LAZYLEN(living_owner.martial_arts) >= 2))
+ clicker.cycle_style()
+ return TRUE
+ var/help_information = current_used_art.get_style_help()
+ if(isnull(help_information))
+ to_chat(clicker, span_notice("[current_used_art.name] doesn't have any combat information!"))
+ return TRUE
+ for(var/info in help_information)
+ to_chat(clicker, info)
+ return TRUE
diff --git a/code/datums/martial/_martial.dm b/code/datums/martial/_martial.dm
index 1281d028b221..53dbaae321db 100644
--- a/code/datums/martial/_martial.dm
+++ b/code/datums/martial/_martial.dm
@@ -19,7 +19,7 @@
VAR_PRIVATE/datum/weakref/current_target
/// Path to verb to display help text for this martial art.
- var/help_verb
+ var/help_verb = "Remember the Basics"
/// If TRUE, this martial art smashes tables when performing table slams and head smashes
var/smashes_tables = FALSE
/// If TRUE, a combo meter will be displayed on the HUD for the current streak
@@ -327,9 +327,11 @@
LAZYINSERT(new_holder.martial_arts, 2, src)
else
LAZYADD(new_holder.martial_arts, src)
+
+ if(LAZYLEN(new_holder.martial_arts) == 1 ? get_style_help() : !(locate(/datum/action/swap_arts) in new_holder.actions))
+ var/datum/action/swap_arts/new_action = new(new_holder, src)
+ new_action.Grant(new_holder)
if(LAZYLEN(new_holder.martial_arts) >= 2)
- // newly learned martials are preferred to be the active one
- add_verb(new_holder, /mob/living/proc/verb_switch_style)
// if the active one is locked, this will no-op, which is fine
new_holder.switch_style(GET_ACTIVE_MARTIAL_ART(new_holder), src)
else if(!active)
@@ -369,8 +371,9 @@
UnregisterSignal(old_holder, COMSIG_QDELETING)
LAZYREMOVE(old_holder.martial_arts, src)
holder = null
- if(LAZYLEN(old_holder.martial_arts) <= 1)
- remove_verb(old_holder, /mob/living/proc/verb_switch_style)
+ if(LAZYLEN(old_holder.martial_arts) < 1) //no more arts, we check above to switch style already.
+ var/datum/action/swap_arts/swap_button = locate() in old_holder.actions
+ qdel(swap_button)
return TRUE
/**
@@ -379,8 +382,6 @@
/datum/martial_art/proc/activate_style(mob/living/new_holder)
SHOULD_CALL_PARENT(TRUE)
active = TRUE
- if(help_verb)
- add_verb(new_holder, help_verb)
RegisterSignal(new_holder, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(unarmed_strike))
RegisterSignal(new_holder, COMSIG_LIVING_GRAB, PROC_REF(attempt_grab))
RegisterSignals(new_holder, list(COMSIG_LIVING_TABLE_SLAMMING, COMSIG_LIVING_TABLE_LIMB_SLAMMING), PROC_REF(smash_table))
@@ -395,8 +396,6 @@
/datum/martial_art/proc/deactivate_style(mob/living/remove_from)
SHOULD_CALL_PARENT(TRUE)
active = FALSE
- if(help_verb)
- remove_verb(remove_from, help_verb)
UnregisterSignal(remove_from, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_LIVING_GRAB, COMSIG_LIVING_TABLE_SLAMMING, COMSIG_LIVING_TABLE_LIMB_SLAMMING))
remove_from.hud_used?.remove_screen_object(HUD_MOB_COMBO)
@@ -405,11 +404,7 @@
SIGNAL_HANDLER
source.hud_used.add_screen_object(/atom/movable/screen/combo, HUD_MOB_COMBO, HUD_GROUP_INFO, update_screen = TRUE)
-/mob/living/proc/verb_switch_style()
- set name = "Swap Style"
- set desc = "Switch to a different martial arts style."
- set category = "IC"
-
+/mob/living/proc/cycle_style()
var/datum/martial_art/current = GET_ACTIVE_MARTIAL_ART(src)
var/datum/martial_art/next = GET_NEXT_MARTIAL_ART(src)
@@ -417,7 +412,7 @@
to_chat(src, span_warning("You can't stop practicing [current]! It's too ingrained in your muscle memory."))
return
- switch_style(GET_ACTIVE_MARTIAL_ART(src), GET_NEXT_MARTIAL_ART(src))
+ switch_style(current, next)
to_chat(src, span_notice("You stop practicing [current] and start practicing [next]."))
/// Deactivates the current martial art and activates the next one.
@@ -428,6 +423,9 @@
if(!current_martial.active || next_martial.active)
return
+ var/datum/action/swap_arts/swap_button = locate() in actions
+ swap_button.current_used_art = next_martial
+
current_martial.deactivate_style(src)
next_martial.activate_style(src)
// front of the list with ye
@@ -436,3 +434,7 @@
// back of the list with ye
LAZYREMOVE(martial_arts, current_martial)
LAZYADD(martial_arts, current_martial)
+
+///To be overwritten for artstyle help.
+/datum/martial_art/proc/get_style_help()
+ return FALSE
diff --git a/code/datums/martial/boxing.dm b/code/datums/martial/boxing.dm
index 58c4ba853652..75420fcd8952 100644
--- a/code/datums/martial/boxing.dm
+++ b/code/datums/martial/boxing.dm
@@ -17,7 +17,7 @@
name = "Boxing"
id = MARTIALART_BOXING
pacifist_style = TRUE
- help_verb = /mob/living/proc/boxing_help
+ help_verb = "Focus on your Form"
/// Boolean on whether we are sportsmanlike in our tussling; TRUE means we have restrictions
var/honorable_boxer = TRUE
/// Can we perform grabs even if it would be dishonorable?
@@ -379,22 +379,20 @@
return FALSE
return ..()
-/mob/living/proc/boxing_help()
- set name = "Focus on your Form"
- set desc = "You focus on how to make the most of your boxing form."
- set category = "Boxing"
- to_chat(usr, "You focus on your form, visualizing how best to throw a punch.")
+/datum/martial_art/boxing/get_style_help()
+ . = list()
- to_chat(usr, "What moves you perform depend on what mouse buttons you click, and whether the last button clicked matches which hand you have selected when you throw the last punch.")
+ . += "You focus on your form, visualizing how best to throw a punch."
+ . += "What moves you perform depend on what mouse buttons you click, and whether the last button clicked matches which hand you have selected when you throw the last punch."
- to_chat(usr, "[span_notice("Straight Punch")]: Left Left/Right Right with the matching hand. Regular damage.")
- to_chat(usr, "[span_notice("Jab")]: Left Left/Right Right with the opposite hand. Regular damage. If you're blind, you'll make a blind jab instead.")
- to_chat(usr, "[span_notice("Left/Right Hook")]: Left Right/Right Left with the matching hand. Does extra damage, but slows your next hit.")
- to_chat(usr, "[span_notice("Uppercut")]: Left Right/Right Left with the opposite hand. Has a higher probability to knock out the target, but slows your next hit.")
+ . += "[span_notice("Straight Punch")]: Left Left/Right Right with the matching hand. Regular damage."
+ . += "[span_notice("Jab")]: Left Left/Right Right with the opposite hand. Regular damage. If you're blind, you'll make a blind jab instead."
+ . += "[span_notice("Left/Right Hook")]: Left Right/Right Left with the matching hand. Does extra damage, but slows your next hit."
+ . += "[span_notice("Uppercut")]: Left Right/Right Left with the opposite hand. Has a higher probability to knock out the target, but slows your next hit."
- to_chat(usr, "While in Throw Mode, you can block incoming punches and return a bit of damage back to an attacker. Blocking attacks this way causes you to lose some stamina damage.")
-
- to_chat(usr, "Your boxing abilities are only able to be used on other boxers.")
+ . += "While in Throw Mode, you can block incoming punches and return a bit of damage back to an attacker. Blocking attacks this way causes you to lose some stamina damage."
+ . += "Your boxing abilities are only able to be used on other boxers."
+ return .
// Boxing Variants!
@@ -405,25 +403,24 @@
name = "Evil Boxing"
id = MARTIALART_EVIL_BOXING
pacifist_style = FALSE
- help_verb = /mob/living/proc/evil_boxing_help
+ help_verb = "Focus on Brawling"
honorable_boxer = FALSE
boxing_traits = list(TRAIT_BOXING_READY, TRAIT_STRENGTH, TRAIT_STIMMED)
-/mob/living/proc/evil_boxing_help()
- set name = "Focus on Brawling"
- set desc = "You ponder how best to rearrange the faces of your enemies."
- set category = "Evil Boxing"
- to_chat(usr, "You contemplate on the violence ahead, visualizing how best to throw a punch.")
+/datum/martial_art/boxing/evil/get_style_help()
+ . = list()
- to_chat(usr, "What moves you perform depend on what mouse buttons you click, and whether the last button clicked matches which hand you have selected when you throw the last punch.")
+ . += "You contemplate on the violence ahead, visualizing how best to throw a punch."
+ . += "What moves you perform depend on what mouse buttons you click, and whether the last button clicked matches which hand you have selected when you throw the last punch."
- to_chat(usr, "[span_notice("Straight Punch")]: Left Left/Right Right with the matching hand. Regular damage.")
- to_chat(usr, "[span_notice("Jab")]: Left Left/Right Right with the opposite hand. Regular damage. If you're blind, you'll make a blind jab instead.")
- to_chat(usr, "[span_notice("Left/Right Hook")]: Left Right/Right Left with the matching hand. Does extra damage, but slows your next hit.")
- to_chat(usr, "[span_notice("Uppercut")]: Left Right/Right Left with the opposite hand. Has a higher probability to knock out the target, but slows your next hit.")
- to_chat(usr, "[span_notice("Sucker Punch")]: Any combination done to a vulnerable target becomes a sucker punch. This could knock them out in one!.")
+ . += "[span_notice("Straight Punch")]: Left Left/Right Right with the matching hand. Regular damage."
+ . += "[span_notice("Jab")]: Left Left/Right Right with the opposite hand. Regular damage. If you're blind, you'll make a blind jab instead."
+ . += "[span_notice("Left/Right Hook")]: Left Right/Right Left with the matching hand. Does extra damage, but slows your next hit."
+ . += "[span_notice("Uppercut")]: Left Right/Right Left with the opposite hand. Has a higher probability to knock out the target, but slows your next hit."
+ . += "[span_notice("Sucker Punch")]: Any combination done to a vulnerable target becomes a sucker punch. This could knock them out in one!."
- to_chat(usr, "While in Throw Mode, you can block incoming punches and return a bit of damage back to an attacker. Blocking attacks this way causes you to lose some stamina damage.")
+ . += "While in Throw Mode, you can block incoming punches and return a bit of damage back to an attacker. Blocking attacks this way causes you to lose some stamina damage."
+ return .
/// Hunter Boxing: for the uncaring, completely deranged one-spacer ecological disaster.
/// The honor check accepts boxing ready targets, OR various biotypes as valid targets. Uses a special crit effect rather than the standard one (against monsters).
@@ -432,7 +429,7 @@
name = "Hunter Boxing"
id = MARTIALART_HUNTER_BOXING
pacifist_style = FALSE
- help_verb = /mob/living/proc/hunter_boxing_help
+ help_verb = "Focus on the Hunt"
default_damage_type = BRUTE
boxing_traits = list(TRAIT_BOXING_READY)
ignore_grab_restriction = TRUE
@@ -442,23 +439,22 @@
var/list/first_word_strike = list("Extinction", "Brutalization", "Explosion", "Adventure", "Thunder", "Lightning", "Sonic", "Atomizing", "Whirlwind", "Tornado", "Shark", "Falcon")
var/list/second_word_strike = list(" Punch", " Pawnch", "-punch", " Jab", " Hook", " Fist", " Uppercut", " Straight", " Strike", " Lunge")
-/mob/living/proc/hunter_boxing_help()
- set name = "Focus on the Hunt"
- set desc = "You focus on how to most effectively punch the hell out of another endangered species."
- set category = "Hunter Boxing"
- to_chat(usr, "You focus on your Fists. You focus on Adventure. You focus on the Hunt.")
+/datum/martial_art/boxing/hunter/get_style_help()
+ . = list()
- to_chat(usr, "What moves you perform depend on what mouse buttons you click, and whether the last button clicked matches which hand you have selected when you throw the last punch.")
+ . += "You focus on your Fists. You focus on Adventure. You focus on the Hunt."
+ . += "What moves you perform depend on what mouse buttons you click, and whether the last button clicked matches which hand you have selected when you throw the last punch."
- to_chat(usr, "[span_notice("Straight Punch")]: Left Left/Right Right with the matching hand. Regular damage.")
- to_chat(usr, "[span_notice("Jab")]: Left Left/Right Right with the opposite hand. Regular damage. If you're blind, you'll make a blind jab instead.")
- to_chat(usr, "[span_notice("Left/Right Hook")]: Left Right/Right Left with the matching hand. Does extra damage, but slows your next hit.")
- to_chat(usr, "[span_notice("Uppercut")]: Left Right/Right Left with the opposite hand. Has a higher probability to critically hit the target, but slows your next hit.")
+ . += "[span_notice("Straight Punch")]: Left Left/Right Right with the matching hand. Regular damage."
+ . += "[span_notice("Jab")]: Left Left/Right Right with the opposite hand. Regular damage. If you're blind, you'll make a blind jab instead."
+ . += "[span_notice("Left/Right Hook")]: Left Right/Right Left with the matching hand. Does extra damage, but slows your next hit."
+ . += "[span_notice("Uppercut")]: Left Right/Right Left with the opposite hand. Has a higher probability to critically hit the target, but slows your next hit."
- to_chat(usr, "While in Throw Mode, you can block incoming punches and return a bit of damage back to an attacker. Blocking attacks this way causes you to lose some stamina damage.")
- to_chat(usr, "Stringing together effective combos restores some of your health and deals even more damage.")
+ . += "While in Throw Mode, you can block incoming punches and return a bit of damage back to an attacker. Blocking attacks this way causes you to lose some stamina damage."
+ . += "Stringing together effective combos restores some of your health and deals even more damage."
- to_chat(usr, "Your hunter boxing abilities are only able to be used on the various flora, fauna and unnatural creatures that reside in this universe. Against normal humanoids, you are just a boxer.")
+ . += "Your hunter boxing abilities are only able to be used on the various flora, fauna and unnatural creatures that reside in this universe. Against normal humanoids, you are just a boxer."
+ return .
/datum/martial_art/boxing/hunter/honor_check(mob/living/possible_boxer)
if(HAS_TRAIT(possible_boxer, TRAIT_BOXING_READY))
diff --git a/code/datums/martial/cqc.dm b/code/datums/martial/cqc.dm
index 4e3815a1e5dd..46993c93e02e 100644
--- a/code/datums/martial/cqc.dm
+++ b/code/datums/martial/cqc.dm
@@ -7,7 +7,7 @@
/datum/martial_art/cqc
name = "CQC"
id = MARTIALART_CQC
- help_verb = /mob/living/proc/CQC_help
+ help_verb = "Remember The Basics"
smashes_tables = TRUE
display_combos = TRUE
/// Weakref to a mob we're currently restraining (with grab-grab combo)
@@ -342,7 +342,11 @@
if(prob(65) && (defender.stat == CONSCIOUS || !defender.IsParalyzed() || !restraining_mob?.resolve()))
var/obj/item/disarmed_item = defender.get_active_held_item()
if(disarmed_item && defender.temporarilyRemoveItemFromInventory(disarmed_item))
- attacker.put_in_hands(disarmed_item)
+ defender.dropItemToGround(disarmed_item)
+ if(isturf(disarmed_item.loc)) //If it fell on the ground we can take it, otherwise assume it's attached to something.
+ attacker.put_in_hands(disarmed_item)
+ else
+ disarmed_item = null
else
disarmed_item = null
@@ -373,19 +377,19 @@
return MARTIAL_ATTACK_FAIL
-/mob/living/proc/CQC_help()
- set name = "Remember The Basics"
- set desc = "You try to remember some of the basics of CQC."
- set category = "CQC"
- to_chat(usr, "You try to remember some of the basics of CQC.")
+/datum/martial_art/cqc/get_style_help()
+ . = list()
- to_chat(usr, "[span_notice("Slam")]: Grab Punch. Slam opponent into the ground, knocking them down.")
- to_chat(usr, "[span_notice("CQC Kick")]: Punch Punch. Knocks opponent away. Knocks out stunned opponents and does stamina damage.")
- to_chat(usr, "[span_notice("Restrain")]: Grab Grab. Locks opponents into a restraining position, disarm to knock them out with a chokehold.")
- to_chat(usr, "[span_notice("Pressure")]: Shove Grab. Decent stamina damage.")
- to_chat(usr, "[span_notice("Consecutive CQC")]: Shove Shove Punch. Mainly offensive move, huge damage and decent stamina damage.")
+ . += "You try to remember some of the basics of CQC."
- to_chat(usr, "In addition, by having your throw mode on when being attacked, you enter an active defense mode where you have a chance to block and sometimes even counter attacks done to you.")
+ . += "[span_notice("Slam")]: Grab Punch. Slam opponent into the ground, knocking them down."
+ . += "[span_notice("CQC Kick")]: Punch Punch. Knocks opponent away. Knocks out stunned opponents and does stamina damage."
+ . += "[span_notice("Restrain")]: Grab Grab. Locks opponents into a restraining position, disarm to knock them out with a chokehold."
+ . += "[span_notice("Pressure")]: Shove Grab. Decent stamina damage."
+ . += "[span_notice("Consecutive CQC")]: Shove Shove Punch. Mainly offensive move, huge damage and decent stamina damage."
+
+ . += "In addition, by having your throw mode on when being attacked, you enter an active defense mode where you have a chance to block and sometimes even counter attacks done to you."
+ return .
///Subtype of CQC. Only used for the chef.
/datum/martial_art/cqc/under_siege
diff --git a/code/datums/martial/plasma_fist.dm b/code/datums/martial/plasma_fist.dm
index 2a9597895291..eb8357abdc6b 100644
--- a/code/datums/martial/plasma_fist.dm
+++ b/code/datums/martial/plasma_fist.dm
@@ -5,7 +5,7 @@
/datum/martial_art/plasma_fist
name = "Plasma Fist"
id = MARTIALART_PLASMAFIST
- help_verb = /mob/living/proc/plasma_fist_help
+ help_verb = "Recall Teachings"
var/nobomb = FALSE
var/plasma_power = 1 //starts at a 1, 2, 4 explosion.
var/plasma_increment = 1 //how much explosion power gets added per kill (1 = 1, 2, 4. 2 = 2, 4, 8 and so on)
@@ -162,18 +162,17 @@
add_to_streak("G", defender)
return check_streak(attacker, defender) ? MARTIAL_ATTACK_SUCCESS : MARTIAL_ATTACK_INVALID
-/mob/living/proc/plasma_fist_help()
- set name = "Recall Teachings"
- set desc = "Remember the martial techniques of the Plasma Fist."
- set category = "Plasma Fist"
+/datum/martial_art/plasma_fist/get_style_help()
+ . = list()
- var/datum/martial_art/plasma_fist/martial = GET_ACTIVE_MARTIAL_ART(src)
- to_chat(usr, "You clench your fists and have a flashback of knowledge...")
- to_chat(usr, "[span_notice("Tornado Sweep")]: Punch Punch Shove. Repulses opponent and everyone back.")
- to_chat(usr, "[span_notice("Throwback")]: Shove Punch Shove. Throws the opponent and an item at them.")
- to_chat(usr, "[span_notice("The Plasma Fist")]: Punch Shove Shove Shove Punch. Instantly gibs an opponent.[martial.nobomb ? "" : " Each kill with this grows your [span_notice("Apotheosis")] explosion size."]")
+ var/datum/martial_art/plasma_fist/martial = GET_ACTIVE_MARTIAL_ART(holder)
+ . += "You clench your fists and have a flashback of knowledge..."
+ . += "[span_notice("Tornado Sweep")]: Punch Punch Shove. Repulses opponent and everyone back."
+ . += "[span_notice("Throwback")]: Shove Punch Shove. Throws the opponent and an item at them."
+ . += "[span_notice("The Plasma Fist")]: Punch Shove Shove Shove Punch. Instantly gibs an opponent.[martial.nobomb ? "" : " Each kill with this grows your [span_notice("Apotheosis")] explosion size."]"
if(!martial.nobomb)
- to_chat(usr, "[span_notice("Apotheosis")]: Use [span_notice("The Plasma Fist")] on yourself. Sends you away in a glorious explosion.")
+ . += "[span_notice("Apotheosis")]: Use [span_notice("The Plasma Fist")] on yourself. Sends you away in a glorious explosion."
+ return .
/obj/effect/temp_visual/plasma_soul
diff --git a/code/datums/martial/sleeping_carp.dm b/code/datums/martial/sleeping_carp.dm
index b1cb83c88467..589a6975f209 100644
--- a/code/datums/martial/sleeping_carp.dm
+++ b/code/datums/martial/sleeping_carp.dm
@@ -6,7 +6,7 @@
/datum/martial_art/the_sleeping_carp
name = "The Sleeping Carp"
id = MARTIALART_SLEEPINGCARP
- help_verb = /mob/living/proc/sleeping_carp_help
+ help_verb = "Recall Teachings"
display_combos = TRUE
grab_state_modifier = 1
/// List of traits applied to users of this martial art.
@@ -374,24 +374,23 @@
return style_factor_points
/// Verb added to humans who learn the art of the sleeping carp.
-/mob/living/proc/sleeping_carp_help()
- set name = "Recall Teachings"
- set desc = "Remember the martial techniques of the Sleeping Carp clan."
- set category = "Sleeping Carp"
-
- to_chat(usr, span_info("You retreat inward and recall the teachings of the Sleeping Carp...\n\
- [span_notice("Gnashing Teeth")]: Punch Grab. Violently twists your opponent's arm, dislocating or even shattering bone and forcing them to drop their held items.\n\
- [span_notice("Crashing Wave Kick")]: Punch Shove. Launch your opponent away from you with incredible force!\n\
- [span_notice("Keelhaul")]: Shove Shove. Nonlethally kick an opponent to the floor, knocking them down, discombobulating them and dealing substantial stamina damage. If they're already prone, disarm them as well.\n\
- [span_notice("Kraken Wrack")]: Grab Punch. Deliver a knee jab into the opponent, dealing high stamina damage, as well as briefly stunning them, winding them and making it difficult for them to speak.\n\
- [span_notice("Grabs and Shoves")]: While in combat mode, your typical grab and shove do decent stamina damage, and your grabs harder to break. If you grab someone who has substantial amounts of stamina damage, you knock them out!\n\
- While in combat mode (and not stunned, not a hulk, and not in a mech), you can reflect all projectiles that come your way, sending them back at the people who fired them! \n\
- However, your ability to avoid projectiles is negatively affected when your are burdened by armor, or whenever you are carrying normal-sized or heavier objects in your hands. \n\
- But if you commmit fully to the martial arts lifestyle by wearing martial arts or carp-related regalia, you will feel empowered enough to potentially avoid attacks even from melee weapons or other unarmed combatants. \n\
- Some melee weapons, such as bo starves, spears, short blades, knives, toolboxes, baseball bats and non-blocking small objects are safe to carry without affecting your ability to defend yourself. Exploit this for a tactical advantage. \n\
- Also, you are more resilient against suffering wounds in combat, and your limbs cannot be dismembered. This grants you extra staying power during extended combat, especially against slashing and other bleeding weapons. \n\
- You are not invincible, however- while you may not suffer debilitating wounds often, you must still watch your health and should have appropriate medical supplies for use during downtime. \n\
- In addition, your training has imbued you with a loathing of guns, and you can no longer use them."))
+/datum/martial_art/the_sleeping_carp/get_style_help()
+ . = list()
+
+ . += span_info("You retreat inward and recall the teachings of the Sleeping Carp...\n\
+ [span_notice("Gnashing Teeth")]: Punch Grab. Violently twists your opponent's arm, dislocating or even shattering bone and forcing them to drop their held items.\n\
+ [span_notice("Crashing Wave Kick")]: Punch Shove. Launch your opponent away from you with incredible force!\n\
+ [span_notice("Keelhaul")]: Shove Shove. Nonlethally kick an opponent to the floor, knocking them down, discombobulating them and dealing substantial stamina damage. If they're already prone, disarm them as well.\n\
+ [span_notice("Kraken Wrack")]: Grab Punch. Deliver a knee jab into the opponent, dealing high stamina damage, as well as briefly stunning them, winding them and making it difficult for them to speak.\n\
+ [span_notice("Grabs and Shoves")]: While in combat mode, your typical grab and shove do decent stamina damage, and your grabs harder to break. If you grab someone who has substantial amounts of stamina damage, you knock them out!\n\
+ While in combat mode (and not stunned, not a hulk, and not in a mech), you can reflect all projectiles that come your way, sending them back at the people who fired them! \n\
+ However, your ability to avoid projectiles is negatively affected when your are burdened by armor, or whenever you are carrying normal-sized or heavier objects in your hands. \n\
+ But if you commmit fully to the martial arts lifestyle by wearing martial arts or carp-related regalia, you will feel empowered enough to potentially avoid attacks even from melee weapons or other unarmed combatants. \n\
+ Some melee weapons, such as bo starves, spears, short blades, knives, toolboxes, baseball bats and non-blocking small objects are safe to carry without affecting your ability to defend yourself. Exploit this for a tactical advantage. \n\
+ Also, you are more resilient against suffering wounds in combat, and your limbs cannot be dismembered. This grants you extra staying power during extended combat, especially against slashing and other bleeding weapons. \n\
+ You are not invincible, however- while you may not suffer debilitating wounds often, you must still watch your health and should have appropriate medical supplies for use during downtime. \n\
+ In addition, your training has imbued you with a loathing of guns, and you can no longer use them.")
+ return .
/obj/item/staff/bostaff
name = "bo staff"
diff --git a/code/datums/martial/spiders_bite.dm b/code/datums/martial/spiders_bite.dm
index 3ace8bdcb1ac..a5d6ddaa369c 100644
--- a/code/datums/martial/spiders_bite.dm
+++ b/code/datums/martial/spiders_bite.dm
@@ -1,7 +1,7 @@
/datum/martial_art/spiders_bite
name = "Spider's Bite"
id = MARTIALART_SPIDERSBITE
- help_verb = /mob/living/proc/spiders_bite_help
+ help_verb = "Recall Teachings"
grab_damage_modifier = 10
grab_escape_chance_modifier = -20
/// REF() to the last mob we kicked
@@ -60,13 +60,12 @@
return IS_LEFT_INDEX(martial_artist.active_hand_index) ? BODY_ZONE_L_LEG : BODY_ZONE_R_LEG
-/mob/living/proc/spiders_bite_help()
- set name = "Recall Teachings"
- set desc = "Remember the Spider Bite technique used by the Spider Clan."
- set category = "Spider's Bite"
+/datum/martial_art/spiders_bite/get_style_help()
+ . = list()
- to_chat(usr, span_info("You retreat inward and recall the Spider Clan's techniques...\n\
+ . += span_info("You retreat inward and recall the Spider Clan's techniques...\n\
• Remember, Many Legged Spider: Unarmed attacks against staggered opponents will always be kicks - granting you greater accuracy and damage.\n\
• Remember, Jump and Climb: Right clicking on throw mode will perform a tackle which is far far less likely to fail.\n\
• Remember, Flow of Gravity: Kicking opponents will have a chance to knock their weapons to the floor. The chance increases for each sequential kick.\n\
- • Remember, Wrap in Web: Your grabs will be harder to escape from."))
+ • Remember, Wrap in Web: Your grabs will be harder to escape from.")
+ return .
diff --git a/code/datums/material/material_container.dm b/code/datums/material/material_container.dm
index 256c1ea4717a..927765b0a818 100644
--- a/code/datums/material/material_container.dm
+++ b/code/datums/material/material_container.dm
@@ -616,7 +616,6 @@
for(var/x in mats) //Loop through all required materials
var/wanted = OPTIMAL_COST(mats[x] * coefficient) * multiplier
if(!has_enough_of_material(x, wanted))//Not a category, so just check the normal way
- testing("didn't have: [x] wanted: [wanted]")
return FALSE
return TRUE
diff --git a/code/datums/materials/_material.dm b/code/datums/materials/_material.dm
index 2c7f8093c74c..98186d0709ff 100644
--- a/code/datums/materials/_material.dm
+++ b/code/datums/materials/_material.dm
@@ -220,9 +220,9 @@ Simple datum which is instanced once per type and is used for every object of sa
/datum/material/proc/on_throw_impact(obj/item/source, atom/hit_atom, datum/thrownthing/throwing_datum, caught)
SIGNAL_HANDLER
if (caught)
- impact_affect_touch(source, hit_atom, astype(throwing_datum.thrower.resolve(), /mob/living))
+ impact_affect_touch(source, hit_atom, astype(throwing_datum.thrower?.resolve(), /mob/living))
else if (!isliving(hit_atom)) // Hit mobs have armor checking
- impact_affect_throw_impact(source, hit_atom, astype(throwing_datum.thrower.resolve(), /mob/living))
+ impact_affect_throw_impact(source, hit_atom, astype(throwing_datum.thrower?.resolve(), /mob/living))
/datum/material/proc/on_throw_impact_living(obj/item/source, mob/living/target, def_zone, blocked, datum/thrownthing/throwing_datum)
SIGNAL_HANDLER
@@ -233,7 +233,7 @@ Simple datum which is instanced once per type and is used for every object of sa
if (!skin_contact)
break
- impact_affect_throw_impact(source, target, astype(throwing_datum.thrower.resolve(), /mob/living), def_zone, !!skin_contact)
+ impact_affect_throw_impact(source, target, astype(throwing_datum.thrower?.resolve(), /mob/living), def_zone, !!skin_contact)
/datum/material/proc/impact_affect_touch(obj/item/source, mob/living/user, mob/living/initiator)
var/arm_dir = IS_LEFT_INDEX(user.active_hand_index) ? BODY_ZONE_L_ARM : BODY_ZONE_R_ARM
diff --git a/code/datums/materials/material_slots/generic.dm b/code/datums/materials/material_slots/generic.dm
index 971ae28b6122..cae828555e9e 100644
--- a/code/datums/materials/material_slots/generic.dm
+++ b/code/datums/materials/material_slots/generic.dm
@@ -61,7 +61,7 @@
/datum/material_slot/weapon_head/proc/on_throw_impact(obj/item/source, atom/hit_atom, datum/thrownthing/throwing_datum, caught)
SIGNAL_HANDLER
if (!caught && !isliving(hit_atom))
- affect_throw_impact(source, hit_atom, astype(throwing_datum.thrower.resolve(), /mob/living))
+ affect_throw_impact(source, hit_atom, astype(throwing_datum.thrower?.resolve(), /mob/living))
/datum/material_slot/weapon_head/proc/on_item_attack(obj/item/source, atom/movable/target, mob/living/user)
SIGNAL_HANDLER
@@ -89,7 +89,7 @@
if (!skin_contact)
break
- affect_throw_impact(source, target, astype(throwing_datum.thrower.resolve(), /mob/living), def_zone, !!skin_contact)
+ affect_throw_impact(source, target, astype(throwing_datum.thrower?.resolve(), /mob/living), def_zone, !!skin_contact)
/datum/material_slot/weapon_head/proc/affect_target(obj/item/source, atom/target, mob/living/user, def_zone, skin_contact = TRUE)
var/datum/material/source_mat = source.get_material_from_slot(type)
@@ -185,7 +185,7 @@
/datum/material_slot/handle/proc/on_throw_impact(obj/item/source, atom/hit_atom, datum/thrownthing/throwing_datum, caught)
SIGNAL_HANDLER
if (caught)
- affect_user(source, hit_atom, astype(throwing_datum.thrower.resolve(), /mob/living))
+ affect_user(source, hit_atom, astype(throwing_datum.thrower?.resolve(), /mob/living))
/datum/material_slot/handle/proc/affect_user(obj/item/source, mob/living/user, mob/living/initiator)
var/datum/material/source_mat = source.get_material_from_slot(type)
diff --git a/code/datums/materials/requirements/_requirement.dm b/code/datums/materials/requirements/_requirement.dm
index d23e11c3fb98..3d953098927e 100644
--- a/code/datums/materials/requirements/_requirement.dm
+++ b/code/datums/materials/requirements/_requirement.dm
@@ -3,6 +3,8 @@
abstract_type = /datum/material_requirement
/// Flags which materials need to have to pass
var/required_flags = NONE
+ /// Flags which materials need to not have in order to pass
+ var/blacklisted_flags = NONE
/// Minimum property values that materials need to have to pass
var/list/property_minimums = null
/// Maximum property values that materials need to have to pass
@@ -13,6 +15,8 @@
var/list/flag_strings = list()
for (var/flag in bitfield_to_list(required_flags))
flag_strings += GLOB.material_flags_to_string[flag]
+ for (var/flag in bitfield_to_list(blacklisted_flags))
+ flag_strings += "not [GLOB.material_flags_to_string[flag]]"
if (!length(property_minimums) && !length(property_maximums))
return "[capitalize(english_list(flag_strings, and_text = " or "))] material"
@@ -29,37 +33,28 @@
return "[capitalize(english_list(flag_strings, and_text = " or "))] material with [english_list(prop_reqs)]"
/datum/material_requirement/proc/valid_material(datum/material/material)
- if (required_flags > 0 && !(material.mat_flags & required_flags))
+ if (required_flags && !(material.mat_flags & required_flags))
return FALSE
- if (required_flags < 0 && (material.mat_flags & (-required_flags)))
+ if (blacklisted_flags && (material.mat_flags & blacklisted_flags))
return FALSE
for (var/prop_id, min_val in property_minimums)
if (material.get_property(prop_id) < min_val)
return FALSE
+
for (var/prop_id, max_val in property_maximums)
if (material.get_property(prop_id) > max_val)
return FALSE
+
return TRUE
// Actual requirement datums
/datum/material_requirement/armor_material
required_flags = ITEM_MATERIAL_CLASSES
- property_minimums = list(
- MATERIAL_HARDNESS = 2,
- )
- property_maximums = list(
- MATERIAL_FLEXIBILITY = 5,
- )
/datum/material_requirement/solid_material
- property_minimums = list(
- MATERIAL_HARDNESS = 2,
- )
- property_maximums = list(
- MATERIAL_FLEXIBILITY = 5,
- )
+ blacklisted_flags = MATERIAL_CLASS_AMORPHOUS
/datum/material_requirement/rigid_material
required_flags = MATERIAL_CLASS_RIGID
diff --git a/code/datums/mind/_mind.dm b/code/datums/mind/_mind.dm
index 52631401fe3b..45ba1464e899 100644
--- a/code/datums/mind/_mind.dm
+++ b/code/datums/mind/_mind.dm
@@ -472,6 +472,7 @@
if(G.can_reenter_corpse || even_if_they_cant_reenter)
return G
break
+ return null
/datum/mind/proc/grab_ghost(force)
var/mob/dead/observer/G = get_ghost(even_if_they_cant_reenter = force)
diff --git a/code/datums/mind/antag.dm b/code/datums/mind/antag.dm
index 007acd4c3b34..6422ee8c3d19 100644
--- a/code/datums/mind/antag.dm
+++ b/code/datums/mind/antag.dm
@@ -111,18 +111,21 @@
* * antag_datum: the antag datum of the uplink owner, for storing it in antag memory. optional!
*/
/datum/mind/proc/give_uplink(silent = FALSE, datum/antagonist/antag_datum)
- if(isnull(current))
+ if(!isliving(current))
return
var/mob/living/carbon/human/traitor_mob = current
if (!istype(traitor_mob))
- return
+ var/datum/status_effect/shapechange_mob/shapeshift = current.has_status_effect(/datum/status_effect/shapechange_mob)
+ if (!ishuman(shapeshift?.caster_mob))
+ return
+ traitor_mob = shapeshift.caster_mob
var/obj/item/uplink_loc
- var/uplink_spawn_location = traitor_mob.client?.prefs?.read_preference(/datum/preference/choiced/uplink_location)
+ var/uplink_spawn_location = current.client?.prefs?.read_preference(/datum/preference/choiced/uplink_location)
var/cant_speak = (HAS_TRAIT(traitor_mob, TRAIT_MUTE) || is_mime_job(assigned_role))
if(uplink_spawn_location == UPLINK_RADIO && cant_speak)
if(!silent)
- to_chat(traitor_mob, span_warning("You have been deemed ineligible for a radio uplink. Supplying standard uplink instead."))
+ to_chat(current, span_warning("You have been deemed ineligible for a radio uplink. Supplying standard uplink instead."))
uplink_spawn_location = UPLINK_PDA
if(uplink_spawn_location != UPLINK_IMPLANT)
@@ -134,7 +137,7 @@
var/obj/item/implant/uplink/starting/new_implant = new(traitor_mob)
new_implant.implant(traitor_mob, null, silent = TRUE)
if(!silent)
- to_chat(traitor_mob, span_boldnotice("Your Syndicate Uplink has been cunningly implanted in you, for a small TC fee. Simply trigger the uplink to access it."))
+ to_chat(current, span_boldnotice("Your Syndicate Uplink has been cunningly implanted in you, for a small TC fee. Simply trigger the uplink to access it."))
add_memory(/datum/memory/key/traitor_uplink/implant, uplink_loc = "implant")
return new_implant
@@ -144,7 +147,7 @@
if(!new_uplink)
CRASH("Uplink creation failed.")
new_uplink.setup_unlock_code()
- new_uplink.uplink_handler.owner = traitor_mob.mind
+ new_uplink.uplink_handler.owner = src
new_uplink.uplink_handler.assigned_role = traitor_mob.mind.assigned_role.title
new_uplink.uplink_handler.assigned_species = traitor_mob.dna.species.id
@@ -164,7 +167,7 @@
new_uplink.unlock_text = unlock_text
if(!silent)
- to_chat(traitor_mob, span_boldnotice(unlock_text))
+ to_chat(current, span_boldnotice(unlock_text))
if(antag_datum)
antag_datum.antag_memory += new_uplink.unlock_note + " "
return .
@@ -182,6 +185,7 @@
var/datum/antagonist/nukeop/converter = creator.mind.has_antag_datum(/datum/antagonist/nukeop,TRUE)
var/datum/antagonist/nukeop/N = new()
N.send_to_spawnpoint = FALSE
+ N.give_bonus_tc = FALSE
N.nukeop_outfit = null
add_antag_datum(N,converter.nuke_team)
diff --git a/code/datums/mocking/client.dm b/code/datums/mocking/client.dm
index 5c684f085212..79611ef0e9d0 100644
--- a/code/datums/mocking/client.dm
+++ b/code/datums/mocking/client.dm
@@ -83,7 +83,7 @@
/datum/client_interface/proc/get_award_status(achievement_type, mob/user, value = 1)
return FALSE
-/datum/client_interface/proc/set_fullscreen(logging_in = FALSE)
+/datum/client_interface/proc/set_fullscreen()
return TRUE
/datum/client_interface/proc/check_drag_proximity(atom/dragging, atom/over, src_location, over_location, src_control, over_control, params)
diff --git a/code/datums/mood.dm b/code/datums/mood.dm
index afc7824380f8..d9c031151671 100644
--- a/code/datums/mood.dm
+++ b/code/datums/mood.dm
@@ -495,6 +495,9 @@
else
msg += "• [span_grey("I don't have much of a reaction to anything right now.")] "
+ if(LAZYLEN(mob_parent.personalities))
+ msg += span_notice("You know yourself to be [mob_parent.get_parsonality_string()]. ")
+
if(LAZYLEN(mob_parent.quirks))
msg += span_notice("You have these quirks: [mob_parent.get_quirk_string(FALSE, CAT_QUIRK_ALL)].")
diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm
index 665857195f19..60f6b09aed12 100644
--- a/code/datums/mood_events/generic_negative_events.dm
+++ b/code/datums/mood_events/generic_negative_events.dm
@@ -655,3 +655,8 @@
description = "Man, getting jabbed with that thing really sucked."
mood_change = -4
timeout = 5 MINUTES
+
+/datum/mood_event/gizmo_negative
+ description = "I hear a voice whispering, and I don't like what it says."
+ mood_change = -3
+ timeout = 30 SECONDS
diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm
index ab581040bf9f..7364493f853d 100644
--- a/code/datums/mood_events/generic_positive_events.dm
+++ b/code/datums/mood_events/generic_positive_events.dm
@@ -701,3 +701,8 @@
description = "What a night! I can't wait to do it all again!"
mood_change = 2
timeout = 10 MINUTES
+
+/datum/mood_event/gizmo_positive
+ description = "I hear a voice whispering kind words in my ear!"
+ mood_change = 3
+ timeout = 30 SECONDS
diff --git a/code/datums/move_manager.dm b/code/datums/move_manager.dm
index 23ddcbecdb70..8b74eb1fe14f 100644
--- a/code/datums/move_manager.dm
+++ b/code/datums/move_manager.dm
@@ -88,7 +88,7 @@ GLOBAL_DATUM_INIT(move_manager, /datum/move_manager, new)
return //Give up
if(existing_loop?.compare_loops(arglist(args.Copy(2))))
- return //it already exists stop trying to make the same moveloop
+ return existing_loop //it already exists stop trying to make the same moveloop
var/datum/move_loop/new_loop = new loop_type(src, subsystem, parent, priority, flags, extra_info) //Pass the mob to move and ourselves in via new
var/list/arguments = args.Copy(6) //Just send the args we've not already dealt with
diff --git a/code/datums/mutations/_mutations.dm b/code/datums/mutations/_mutations.dm
index 8c8b30f98bb5..7f88a696b5ba 100644
--- a/code/datums/mutations/_mutations.dm
+++ b/code/datums/mutations/_mutations.dm
@@ -89,9 +89,8 @@
var/list/valid_chrom_list = list()
/// List of traits that are added or removed by the mutation with GENETIC_TRAIT source.
var/list/mutation_traits
-
-/datum/mutation/New()
- . = ..()
+ /// if TRUE admins get alerted when someone force-injects someone else with this mutation
+ var/warn_admins_on_inject = FALSE
/datum/mutation/Destroy()
power_path = null
diff --git a/code/datums/mutations/body.dm b/code/datums/mutations/body.dm
index c20046d3fe2f..efdab6a92d2a 100644
--- a/code/datums/mutations/body.dm
+++ b/code/datums/mutations/body.dm
@@ -246,6 +246,7 @@
instability = NEGATIVE_STABILITY_MAJOR // mmmonky
remove_on_aheal = FALSE
locked = TRUE //Species specific, keep out of actual gene pool
+ warn_admins_on_inject = TRUE
var/datum/species/original_species = /datum/species/human
var/original_name
@@ -533,6 +534,7 @@
difficulty = 12 //pretty good for traitors
quality = NEGATIVE //holy shit no eyes or tongue or ears
text_gain_indication = span_warning("Something feels off.")
+ warn_admins_on_inject = TRUE
/datum/mutation/headless/on_acquiring()
. = ..()
diff --git a/code/datums/mutations/touch.dm b/code/datums/mutations/touch.dm
index 7a91cc390e8f..8ce75e746c58 100644
--- a/code/datums/mutations/touch.dm
+++ b/code/datums/mutations/touch.dm
@@ -88,8 +88,8 @@
quality = POSITIVE
locked = FALSE
difficulty = 16
- text_gain_indication = span_notice("Your hand feels blessed!")
- text_lose_indication = span_notice("Your hand feels secular once more.")
+ text_gain_indication = span_notice("Your hands feel blessed!")
+ text_lose_indication = span_notice("Your hands no longer feel blessed.")
power_path = /datum/action/cooldown/spell/touch/lay_on_hands
instability = POSITIVE_INSTABILITY_MAJOR
energy_coeff = 1
@@ -124,7 +124,7 @@
hand_path = /obj/item/melee/touch_attack/lay_on_hands
draw_message = span_notice("You ready your hand to transfer injuries to yourself.")
drop_message = span_notice("You lower your hand.")
- /// Multiplies the amount healed.
+ /// Multiplies the amount healed (or damage dealt, in the case of a smite).
var/heal_multiplier = 1
/// Multiplies the incoming pain from healing. (Halved with synchronizer chromosome)
var/pain_multiplier = 1
@@ -140,7 +140,7 @@
if(!.)
return .
var/obj/item/bodypart/transfer_limb = cast_on.get_active_hand()
- if(IS_ROBOTIC_LIMB(transfer_limb))
+ if(!IS_ORGANIC_LIMB(transfer_limb))
to_chat(cast_on, span_notice("You fail to channel your mending powers through your inorganic hand."))
return FALSE
@@ -255,43 +255,45 @@
// Did the transfer work?
. = FALSE
- // Get the hurtguy's limbs and the mendicant's limbs to attempt a 1-1 transfer.
- var/list/hurt_limbs = hurtguy.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC) + hurtguy.get_wounded_bodyparts(BODYTYPE_ORGANIC)
- var/list/mendicant_organic_limbs = list()
- for(var/obj/item/bodypart/possible_limb in mendicant.get_bodyparts())
- if(IS_ORGANIC_LIMB(possible_limb))
- mendicant_organic_limbs += possible_limb
-
- // If we have no organic available limbs just give up.
- if(!length(mendicant_organic_limbs))
- mendicant.balloon_alert(mendicant, "no organic limbs!")
- return .
- if(!length(hurt_limbs))
- hurtguy.balloon_alert(mendicant, "no damaged organic limbs!")
- return .
// Counter to make sure we don't take too much from separate limbs
- var/total_damage_healed = 0
- // Transfer damage from one limb to the mendicant's counterpart.
- for(var/obj/item/bodypart/affected_limb as anything in hurt_limbs)
+ var/healing_budget = 35 * heal_multiplier
+ // Transfer damage from each limb to the mendicant's counterpart.
+ for(var/obj/item/bodypart/affected_limb as anything in hurtguy.get_bodyparts())
+ if(!(affected_limb.bodytype & BODYTYPE_ORGANIC))
+ continue
var/obj/item/bodypart/mendicant_transfer_limb = mendicant.get_bodypart(affected_limb.body_zone)
- // If the compared limb isn't organic, skip it and pick a random one.
- if(!(mendicant_transfer_limb in mendicant_organic_limbs))
- mendicant_transfer_limb = pick(mendicant_organic_limbs)
-
- // Transfer at most 35 damage, by default.
- var/brute_damage = min(affected_limb.brute_dam, 35 * heal_multiplier)
- // no double dipping
- var/burn_damage = min(affected_limb.burn_dam, (35 * heal_multiplier) - brute_damage)
- if((brute_damage || burn_damage) && total_damage_healed < (35 * heal_multiplier))
- total_damage_healed += brute_damage + burn_damage
+ var/list/mendicant_organic_limbs = list()
+ for(var/obj/item/bodypart/possible_limb in mendicant.get_bodyparts(include_stumps = TRUE)) // Keeping this here in case stumps can store damage in the future
+ if(IS_ORGANIC_LIMB(possible_limb) && !IS_STUMP(possible_limb)) // No stumps are currently organic, this is futureproofing.
+ mendicant_organic_limbs += possible_limb
+ // Spread the healing between brute and burn
+ var/affected_limb_damage = affected_limb.brute_dam + affected_limb.burn_dam
+ var/brute_damage = 0
+ var/burn_damage = 0
+ if(healing_budget >= affected_limb_damage)
+ healing_budget -= affected_limb_damage
+ brute_damage = affected_limb.brute_dam
+ burn_damage = affected_limb.burn_dam
+ else
+ if(affected_limb.brute_dam > affected_limb.burn_dam)
+ burn_damage = min(affected_limb.burn_dam, healing_budget/2)
+ brute_damage = healing_budget - burn_damage
+ else
+ brute_damage = min(affected_limb.burn_dam, healing_budget/2)
+ burn_damage = healing_budget - brute_damage
+ healing_budget = 0
+ if((brute_damage || burn_damage))
. = TRUE
var/brute_taken = brute_damage * pain_multiplier
var/burn_taken = burn_damage * pain_multiplier
// Heal!
affected_limb.heal_damage(brute_damage, burn_damage, required_bodytype = BODYTYPE_ORGANIC)
// Hurt!
- mendicant_transfer_limb.receive_damage(brute_taken, burn_taken, forced = TRUE, wound_bonus = CANT_WOUND)
+ if((mendicant_transfer_limb in mendicant_organic_limbs))
+ mendicant_transfer_limb.receive_damage(brute_taken, burn_taken, forced = TRUE, wound_bonus = CANT_WOUND)
+ else // spread damage if we're trying to heal a bodypart we have no organic parallel of
+ mendicant.take_overall_damage(brute_damage, burn_damage, required_bodytype = BODYTYPE_ORGANIC)
// Force light wounds onto you.
for(var/datum/wound/iter_wound as anything in affected_limb.wounds)
@@ -300,21 +302,26 @@
if(prob(50 * heal_multiplier))
continue
if(WOUND_SEVERITY_CRITICAL)
- if(heal_multiplier < 1.5) // need buffs to transfer crit wounds
+ if(heal_multiplier < 1.5 && prob(30 * heal_multiplier)) // need buffs to transfer crit wounds
continue
. = TRUE
iter_wound.remove_wound()
iter_wound.apply_wound(mendicant_transfer_limb)
+ if(!healing_budget)
+ break
+
if(!CAN_HAVE_BLOOD(mendicant) || !CAN_HAVE_BLOOD(hurtguy))
return .
// 10% base
var/max_blood_transfer = (BLOOD_VOLUME_NORMAL * 0.10) * heal_multiplier
+ var/hurtguy_blood = hurtguy.get_blood_volume()
// Too little blood
- if(hurtguy.get_blood_volume() < BLOOD_VOLUME_NORMAL)
+ if(hurtguy_blood < BLOOD_VOLUME_NORMAL)
+ var/amount_to_transfer = min(max_blood_transfer, BLOOD_VOLUME_NORMAL - hurtguy_blood)
// We ignore incompatibility here.
- var/blood_transferred = mendicant.transfer_blood_to(hurtguy, max_blood_transfer, ignore_low_blood = TRUE, ignore_incompatibility = TRUE)
+ var/blood_transferred = mendicant.transfer_blood_to(hurtguy, amount_to_transfer, ignore_low_blood = TRUE, ignore_incompatibility = TRUE)
if(!blood_transferred)
return
@@ -329,11 +336,12 @@
to_chat(hurtguy, span_notice("Your veins feel thicker!"))
return
- if(hurtguy.get_blood_volume() < BLOOD_VOLUME_EXCESS)
+ if(hurtguy_blood < BLOOD_VOLUME_MAXIMUM)
return
+ var/amount_to_receive = min(max_blood_transfer, hurtguy_blood - BLOOD_VOLUME_MAXIMUM)
// We ignore incompatibility here.
- var/blood_received = hurtguy.transfer_blood_to(mendicant, hurtguy.get_blood_volume() - BLOOD_VOLUME_EXCESS, ignore_incompatibility = TRUE)
+ var/blood_received = hurtguy.transfer_blood_to(mendicant, amount_to_receive, ignore_incompatibility = TRUE)
if(!blood_received)
return
@@ -346,6 +354,8 @@
to_chat(mendicant, span_notice("Your veins swell and itch!"))
else
to_chat(mendicant, span_notice("Your veins swell!"))
+ if(!.)
+ mendicant.balloon_alert(hurtguy, "no damaged organic limbs!")
/datum/action/cooldown/spell/touch/lay_on_hands/proc/determine_if_this_hurts_instead(mob/living/carbon/mendicant, mob/living/hurtguy)
diff --git a/code/datums/mutations/void_magnet.dm b/code/datums/mutations/void_magnet.dm
index 122fab4e198f..aef24d19629d 100644
--- a/code/datums/mutations/void_magnet.dm
+++ b/code/datums/mutations/void_magnet.dm
@@ -60,7 +60,7 @@
/datum/action/cooldown/spell/void/cursed/proc/on_life(mob/living/source, seconds_per_tick)
SIGNAL_HANDLER
- if(!isliving(source) || HAS_TRAIT(source, TRAIT_STASIS) || source.stat == DEAD || HAS_TRAIT(source, TRAIT_NO_TRANSFORM))
+ if(HAS_TRAIT(source, TRAIT_STASIS) || source.stat == DEAD)
return
if(!is_valid_target(source))
diff --git a/code/datums/quirks/_quirk.dm b/code/datums/quirks/_quirk.dm
index ac0b01684dd0..d1546c56c8b1 100644
--- a/code/datums/quirks/_quirk.dm
+++ b/code/datums/quirks/_quirk.dm
@@ -258,45 +258,3 @@
to_chat(quirk_holder, chat_string)
where_items_spawned = null
-
-/**
- * get_quirk_string() is used to get a printable string of all the quirk traits someone has for certain criteria
- *
- * Arguments:
- * * Medical- If we want the long, fancy descriptions that show up in medical records, or if not, just the name
- * * Category- Which types of quirks we want to print out. Defaults to everything
- * * from_scan- If the source of this call is like a health analyzer or HUD, in which case QUIRK_HIDE_FROM_MEDICAL hides the quirk.
- */
-/mob/living/proc/get_quirk_string(medical = FALSE, category = CAT_QUIRK_ALL, from_scan = FALSE)
- var/list/dat = list()
- for(var/datum/quirk/candidate as anything in quirks)
- if(from_scan && (candidate.quirk_flags & QUIRK_HIDE_FROM_SCAN))
- continue
- switch(category)
- if(CAT_QUIRK_MAJOR_DISABILITY)
- if(candidate.value >= -4)
- continue
- if(CAT_QUIRK_MINOR_DISABILITY)
- if(!ISINRANGE(candidate.value, -4, -1))
- continue
- if(CAT_QUIRK_NOTES)
- if(candidate.value < 0)
- continue
- dat += medical ? candidate.medical_record_text : candidate.name
-
- if(!length(dat))
- return medical ? "No issues have been declared." : "None"
- return medical ? dat.Join(" ") : dat.Join(", ")
-
-/mob/living/proc/cleanse_quirk_datums() //removes all trait datums
- QDEL_LAZYLIST(quirks)
-
-/mob/living/proc/transfer_quirk_datums(mob/living/to_mob)
- // We could be done before the client was moved or after the client was moved
- var/datum/preferences/to_pass = client || to_mob.client
-
- for(var/datum/quirk/quirk as anything in quirks)
- if(quirk.quirk_flags & QUIRK_NO_TRANSFER)
- continue
- quirk.remove_from_current_holder(quirk_transfer = TRUE)
- quirk.add_to_holder(to_mob, quirk_transfer = TRUE, client_source = to_pass)
diff --git a/code/datums/quirks/negative_quirks/addict.dm b/code/datums/quirks/negative_quirks/addict.dm
index 8f2dcc2590c2..556452bc3607 100644
--- a/code/datums/quirks/negative_quirks/addict.dm
+++ b/code/datums/quirks/negative_quirks/addict.dm
@@ -95,11 +95,11 @@
associated_typepath = /datum/quirk/item_quirk/addict/junkie
customization_options = list(/datum/preference/choiced/junkie)
-/datum/quirk/item_quirk/addict/junkie/add_unique(client/client_source)
-
- var/addiction = client_source?.prefs.read_preference(/datum/preference/choiced/junkie)
- if(addiction && (addiction != "Random"))
- reagent_type = GLOB.possible_junkie_addictions[addiction]
+/datum/quirk/item_quirk/addict/junkie/add_to_holder(mob/living/new_holder, quirk_transfer = FALSE, client/client_source, unique = TRUE, announce = TRUE)
+ if(!quirk_transfer)
+ var/addiction = client_source?.prefs.read_preference(/datum/preference/choiced/junkie)
+ if(addiction && (addiction != "Random"))
+ reagent_type = GLOB.possible_junkie_addictions[addiction]
return ..()
/datum/quirk/item_quirk/addict/remove()
diff --git a/code/datums/quirks/negative_quirks/asthma.dm b/code/datums/quirks/negative_quirks/asthma.dm
index 1c4e53640f93..31532b2a3fe1 100644
--- a/code/datums/quirks/negative_quirks/asthma.dm
+++ b/code/datums/quirks/negative_quirks/asthma.dm
@@ -84,10 +84,7 @@
/datum/quirk/item_quirk/asthma/proc/on_life(mob/living/source, seconds_per_tick)
SIGNAL_HANDLER
- if (quirk_holder.stat == DEAD)
- return
-
- if (HAS_TRAIT(quirk_holder, TRAIT_STASIS) || HAS_TRAIT(quirk_holder, TRAIT_NO_TRANSFORM))
+ if (quirk_holder.stat == DEAD || HAS_TRAIT(quirk_holder, TRAIT_STASIS))
return
var/obj/item/organ/lungs/holder_lungs = quirk_holder.get_organ_slot(ORGAN_SLOT_LUNGS)
diff --git a/code/datums/quirks/negative_quirks/chronic_illness.dm b/code/datums/quirks/negative_quirks/chronic_illness.dm
index 49438160c489..a96c787e37dc 100644
--- a/code/datums/quirks/negative_quirks/chronic_illness.dm
+++ b/code/datums/quirks/negative_quirks/chronic_illness.dm
@@ -10,8 +10,8 @@
mail_goodies = list(/obj/item/storage/pill_bottle/sansufentanyl)
/datum/quirk/item_quirk/chronic_illness/add(client/client_source)
- var/datum/disease/chronic_illness/hms = new /datum/disease/chronic_illness()
- quirk_holder.ForceContractDisease(hms)
+ var/datum/disease/chronic_illness/hms = new()
+ quirk_holder.ForceContractDisease(hms, make_copy = FALSE, del_on_fail = TRUE)
/datum/quirk/item_quirk/chronic_illness/add_unique(client/client_source)
give_item_to_holder(/obj/item/storage/pill_bottle/sansufentanyl, list(LOCATION_BACKPACK), flavour_text = "You've been provided with medication to help manage your condition. Take it regularly to avoid complications.", notify_player = TRUE)
diff --git a/code/datums/quirks/negative_quirks/limper.dm b/code/datums/quirks/negative_quirks/limper.dm
new file mode 100644
index 000000000000..e272bd2c7129
--- /dev/null
+++ b/code/datums/quirks/negative_quirks/limper.dm
@@ -0,0 +1,30 @@
+/datum/quirk/item_quirk/limper
+ name = "Limper"
+ desc = "You have a pronounced limp when you walk. This will slow you down considerably. Good thing you brought your cane."
+ icon = FA_ICON_PERSON_CANE
+ gain_text = span_danger("Your leg feels a bit weak.")
+ lose_text = span_notice("Your legs feel normal again.")
+ medical_record_text = "Patient appears to suffer from a weakness in the leg."
+ value = -6
+ hardcore_value = 3
+ quirk_flags = QUIRK_HUMAN_ONLY
+
+ mail_goodies = list(
+ /obj/item/cane,
+ /obj/item/cane/crutch,
+ /obj/item/cane/white,
+ )
+
+/datum/quirk/item_quirk/limper/add_unique(client/client_source)
+ give_item_to_holder(new /obj/item/cane(get_turf(quirk_holder)), list(
+ LOCATION_HANDS,
+ LOCATION_BACKPACK,
+ ))
+ return
+
+/datum/quirk/item_quirk/limper/add(client/client_source)
+ quirk_holder.apply_status_effect(/datum/status_effect/limp/quirk)
+
+/datum/quirk/item_quirk/limper/remove(client/client_source)
+ quirk_holder.remove_status_effect(/datum/status_effect/limp/quirk)
+
diff --git a/code/datums/quirks/positive_quirks/spacer.dm b/code/datums/quirks/positive_quirks/spacer.dm
index 24d392985de6..7c07aaffd6f8 100644
--- a/code/datums/quirks/positive_quirks/spacer.dm
+++ b/code/datums/quirks/positive_quirks/spacer.dm
@@ -18,7 +18,7 @@
/obj/item/reagent_containers/applicator/pill/gravitum,
)
/// How high spacers get bumped up to
- var/modded_height = HUMAN_HEIGHT_TALLER
+ var/modded_height = HUMAN_HEIGHT_TALLEST
/// How long on a planet before we get averse effects
var/planet_period = 3 MINUTES
/// TimerID for time spend on a planet
@@ -30,6 +30,11 @@
/// Determines the last state we were in ([LAST_STATE_PLANET], [LAST_STATE_SPACE], or [LAST_STATE_NOGRAV])
VAR_FINAL/last_state
+ /// Modifier to damage taken from pressure/cold
+ VAR_FINAL/damage_mod = 0.66
+ /// Modifier to drift speed in zero G
+ VAR_FINAL/drift_mod = 0.75
+
/datum/quirk/spacer_born/add(client/client_source)
if(isdummy(quirk_holder))
return
@@ -45,12 +50,12 @@
update_effects(quirk_holder, skip_timers = TRUE)
// drift slightly faster through zero G
- quirk_holder.inertia_move_multiplier *= 0.8
+ quirk_holder.inertia_move_multiplier_passive *= drift_mod
var/mob/living/carbon/human/human_quirker = quirk_holder
human_quirker.set_mob_height(modded_height)
- human_quirker.physiology.pressure_mod *= 0.8
- human_quirker.physiology.cold_mod *= 0.8
+ human_quirker.physiology.pressure_mod *= damage_mod
+ human_quirker.physiology.cold_mod *= damage_mod
/datum/quirk/spacer_born/post_add()
var/on_a_planet = SSmapping.is_planetary()
@@ -76,15 +81,15 @@
if(QDELING(quirk_holder))
return
- quirk_holder.inertia_move_multiplier /= 0.8
+ quirk_holder.inertia_move_multiplier_passive /= drift_mod
quirk_holder.clear_mood_event("spacer")
quirk_holder.remove_movespeed_modifier(/datum/movespeed_modifier/spacer)
quirk_holder.remove_status_effect(/datum/status_effect/spacer)
var/mob/living/carbon/human/human_quirker = quirk_holder
human_quirker.set_mob_height(HUMAN_HEIGHT_MEDIUM)
- human_quirker.physiology.pressure_mod /= 0.8
- human_quirker.physiology.cold_mod /= 0.8
+ human_quirker.physiology.pressure_mod /= damage_mod
+ human_quirker.physiology.cold_mod /= damage_mod
/// Check on Z change whether we should start or stop timers
/datum/quirk/spacer_born/proc/spacer_moved(mob/living/source, turf/old_turf, turf/new_turf, same_z_layer)
diff --git a/code/datums/ruins.dm b/code/datums/ruins.dm
index 703991f596e9..5741904e4906 100644
--- a/code/datums/ruins.dm
+++ b/code/datums/ruins.dm
@@ -28,6 +28,10 @@
var/suffix = null
///What flavor or ruin is this? eg ZTRAIT_SPACE_RUINS
var/ruin_type = null
+ ///is this ruin "enclosed" by walls. This is relevant for terrain gen with cellular automata to know whether this ruin will spawn inside of walls, or should spawn in the open.
+ var/enclosed_for_terrain = FALSE
+ ///Padding to be used to ensure extra space around the ruin for terrain gen purposes. If a ruin is NOT enclosed and this is set to 1; there will be at least one layer of open terrain around the ruin. If a ruin IS enclosed and this is set to 1; there will be at least one layer of wall terrain around the ruin.
+ var/terrain_padding = 0
/datum/map_template/ruin/New()
if(!name && id)
diff --git a/code/datums/ruins/icemoon.dm b/code/datums/ruins/icemoon.dm
index 5e293a70747a..35d1856d8ace 100644
--- a/code/datums/ruins/icemoon.dm
+++ b/code/datums/ruins/icemoon.dm
@@ -23,6 +23,7 @@
id = "lust"
description = "Not exactly what you expected."
suffix = "icemoon_surface_lust.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/asteroid
name = "Ice-Ruin Asteroid Site"
@@ -103,6 +104,7 @@
suffix = "icemoon_surface_mining_site.dmm"
always_place = TRUE
always_spawn_with = list(/datum/map_template/ruin/icemoon/underground/mining_site_below = PLACE_BELOW)
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/mining_site_below
name = "Ice-Ruin Mining Site Underground"
@@ -111,6 +113,7 @@
suffix = "icemoon_underground_mining_site.dmm"
has_ceiling = FALSE
unpickable = TRUE
+ enclosed_for_terrain = TRUE
// below ground only
@@ -124,18 +127,21 @@
id = "abandonedvillage"
description = "Who knows what lies within?"
suffix = "icemoon_underground_abandoned_village.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/library
name = "Ice-Ruin Buried Library"
id = "buriedlibrary"
description = "A once grand library, now lost to the confines of the Ice Moon."
suffix = "icemoon_underground_library.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/wrath
name = "Ice-Ruin Ruin of Wrath"
id = "wrath"
description = "You'll fight and fight and just keep fighting."
suffix = "icemoon_underground_wrath.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/hermit
name = "Ice-Ruin Frozen Shack"
@@ -148,6 +154,7 @@
id = "lavalandsite"
description = "I guess we never really left you huh?"
suffix = "icemoon_underground_lavaland.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/puzzle
name = "Ice-Ruin Ancient Puzzle"
@@ -180,6 +187,7 @@
id = "mailroom"
description = "This is where all of your paychecks went. Signed, the management."
suffix = "icemoon_underground_mailroom.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/biodome
name = "Ice-Ruin Syndicate Bio-Dome"
@@ -204,6 +212,7 @@
id = "syndie_lab"
description = "A small laboratory and living space for Syndicate agents."
suffix = "icemoon_underground_syndielab.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/o31
name = "Ice-Ruin Outpost 31"
@@ -224,6 +233,7 @@
id = "hotsprings"
description = "Just relax and take a dip, nothing will go wrong, I swear!"
suffix = "icemoon_underground_hotsprings.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/vent
name = "Ice-Ruin Icemoon Ore Vent"
diff --git a/code/datums/ruins/lavaland.dm b/code/datums/ruins/lavaland.dm
index cd3c1065ec4d..206158c15d1f 100644
--- a/code/datums/ruins/lavaland.dm
+++ b/code/datums/ruins/lavaland.dm
@@ -15,6 +15,7 @@
description = "Seemingly plucked from a tropical destination, this beach is calm and cool, with the salty waves roaring softly in the background. \
Comes with a rustic wooden bar and suicidal bartender."
suffix = "lavaland_biodome_beach.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/biodome/winter
name = "Lava-Ruin Biodome Winter"
@@ -43,6 +44,7 @@
suffix = "lavaland_surface_cube.dmm"
cost = 10
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/seed_vault
name = "Lava-Ruin Seed Vault"
@@ -52,6 +54,7 @@
suffix = "lavaland_surface_seed_vault.dmm"
cost = 10
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/ash_walker
name = "Lava-Ruin Ash Walker Nest"
@@ -61,6 +64,7 @@
suffix = "lavaland_surface_ash_walker1.dmm"
cost = 20
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/syndicate_base
name = "Lava-Ruin Syndicate Lava Base"
@@ -87,6 +91,7 @@
cost = 5
suffix = "lavaland_surface_gaia.dmm"
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/sin
cost = 10
@@ -124,6 +129,7 @@
suffix = "lavaland_surface_sloth.dmm"
// Generates nothing but atmos runtimes and salt
cost = 0
+ terrain_padding = 2
/datum/map_template/ruin/lavaland/ratvar
name = "Lava-Ruin Dead God"
@@ -140,6 +146,7 @@
suffix = "lavaland_surface_hierophant.dmm"
always_place = TRUE
allow_duplicates = FALSE
+ terrain_padding = 2
/datum/map_template/ruin/lavaland/blood_drunk_miner
name = "Lava-Ruin Blood-Drunk Miner"
@@ -148,14 +155,17 @@
suffix = "lavaland_surface_blooddrunk1.dmm"
cost = 0
allow_duplicates = FALSE //will only spawn one variant of the ruin
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/blood_drunk_miner/guidance
name = "Lava-Ruin Blood-Drunk Miner (Guidance)"
suffix = "lavaland_surface_blooddrunk2.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/blood_drunk_miner/hunter
name = "Lava-Ruin Blood-Drunk Miner (Hunter)"
suffix = "lavaland_surface_blooddrunk3.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/blood_drunk_miner/random
name = "Lava-Ruin Blood-Drunk Miner (Random)"
@@ -172,6 +182,7 @@
description = "Turns out that keeping your abductees unconscious is really important. Who knew?"
suffix = "lavaland_surface_ufo_crash.dmm"
cost = 5
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/xeno_nest
name = "Lava-Ruin Xenomorph Nest"
@@ -180,6 +191,7 @@
Quality memes."
suffix = "lavaland_surface_xeno_nest.dmm"
cost = 20
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/fountain
name = "Lava-Ruin Fountain Hall"
@@ -227,6 +239,7 @@
suffix = "lavaland_surface_random_ripley.dmm"
allow_duplicates = FALSE
cost = 5
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/dark_wizards
name = "Lava-Ruin Dark Wizard Altar"
@@ -234,6 +247,7 @@
description = "A ruin with dark wizards. What secret do they guard?"
suffix = "lavaland_surface_wizard.dmm"
cost = 5
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/strong_stone
name = "Lava-Ruin Strong Stone"
@@ -266,6 +280,7 @@
suffix = "lavaland_surface_elephant_graveyard.dmm"
allow_duplicates = FALSE
cost = 10
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/bileworm_nest
name = "Lava-Ruin Bileworm Nest"
@@ -274,6 +289,7 @@
cost = 5
suffix = "lavaland_surface_bileworm_nest.dmm"
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/lava_phonebooth
name = "Lava-Ruin Phonebooth"
@@ -323,6 +339,8 @@
description = "Not every shuttle makes it back to CentCom."
suffix = "lavaland_surface_shuttle_wreckage.dmm"
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
+
/datum/map_template/ruin/lavaland/crashsite
name = "Lava-Ruin Pod Crashsite"
@@ -330,6 +348,7 @@
description = "They launched too early"
suffix = "lavaland_surface_crashsite.dmm"
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/shoe_facotry
name = "Lava-Ruin Shoe Factory"
diff --git a/code/datums/sound_token.dm b/code/datums/sound_token.dm
new file mode 100644
index 000000000000..93fdfb8213bb
--- /dev/null
+++ b/code/datums/sound_token.dm
@@ -0,0 +1,284 @@
+// Sound tokens, a datumized handler for spatial sound.
+// Uses the spatial grid to track clients in range and add them as listeners
+// Updated by the SSsound_tokens subsystem every tick when requested by client so that if the source or listener moves, the sound updates accordingly.
+/datum/sound_token
+ /// The atom playing the sound.
+ var/atom/source
+ /// k:v list of mob : sound status
+ var/list/listeners = list()
+
+ /// Sound maximum range
+ var/range
+ /// Sound volume
+ var/volume
+ /// Sound falloff
+ var/falloff_exponent
+ /// Sound falloff distance
+ var/falloff_distance
+
+ /// The master copy of the playing sound.
+ var/sound/sound
+ /// Null sound for cancelling the sound entirely.
+ var/sound/null_sound
+
+ /// Status of the playing sound
+ var/sound_status = NONE
+ /// The channel being used.
+ var/sound_channel
+ /// world.time when the sound started (or when the sound file was last changed). Used to calculate playback offset for new listeners.
+ var/start_time
+ /// Duration of the current sound file in deciseconds. Used to wrap offset for looping sounds.
+ var/sound_duration
+ /// Cell tracker managing spatial grid cells within range of the source. The wizards say this is the fastest.
+ var/datum/cell_tracker/cell_tracker
+
+/datum/sound_token/New(atom/_source, _sound, _range = 10, _volume = 50, _falloff_exponent = SOUND_FALLOFF_EXPONENT, _falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE)
+ source = _source
+ RegisterSignal(source, COMSIG_QDELETING, PROC_REF(source_deleted))
+ RegisterSignal(source, COMSIG_MOVABLE_MOVED, PROC_REF(source_moved))
+ RegisterSignal(source, COMSIG_ENTER_AREA, PROC_REF(on_enter_area))
+
+ range = _range
+ volume = _volume
+ falloff_exponent = _falloff_exponent
+ falloff_distance = _falloff_distance
+
+ update_sound(_sound)
+
+ null_sound = sound(channel = sound_channel)
+
+ cell_tracker = new /datum/cell_tracker(range, range)
+ update_tracked_cells()
+
+ RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGIN, PROC_REF(player_login))
+ RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGOUT, PROC_REF(player_logout))
+
+/datum/sound_token/Destroy(force, ...)
+ for(var/listener in listeners)
+ remove_listener(listener)
+
+ listeners = null
+ source = null
+ return ..()
+
+///Lets us update the sound to a new one.
+/datum/sound_token/proc/update_sound(_sound, start_playing = FALSE)
+ sound = sound(_sound)
+ if(!sound_channel)
+ sound_channel = SSsounds.reserve_sound_channel_for_datum(src)
+ sound.channel = sound_channel
+ sound_duration = SSsounds.get_sound_length(_sound)
+ start_time = REALTIMEOFDAY
+ if(start_playing)
+ force_update_all_listeners(FALSE)
+
+/// Updates the data of a listener, or adds them if they are not present.
+/datum/sound_token/proc/add_or_update_listener(mob/listener_mob)
+ if(isnull(listeners[listener_mob]))
+ add_listener(listener_mob)
+ else
+ update_listener(listener_mob)
+
+/// Adds a listener to the sound.
+/datum/sound_token/proc/add_listener(mob/listener_mob)
+ if(!isnull(listeners[listener_mob]))
+ return FALSE
+
+ if(!listener_mob.client || isnewplayer(listener_mob))
+ return
+
+ listeners[listener_mob] = NONE
+ listener_mob.client.sound_tokens += src
+ RegisterSignal(listener_mob, COMSIG_QDELETING, PROC_REF(listener_deleted))
+ RegisterSignals(listener_mob, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(listener_deafness_update))
+ update_listener(listener_mob, FALSE)
+ return TRUE
+
+/// Remove a listener from the sound.
+/datum/sound_token/proc/remove_listener(mob/listener_mob)
+
+ listeners -= listener_mob
+
+ if(listener_mob.client)
+ listener_mob.client.sound_tokens -= src
+
+ UnregisterSignal(listener_mob, list(COMSIG_QDELETING, SIGNAL_ADDTRAIT(TRAIT_DEAF),SIGNAL_REMOVETRAIT(TRAIT_DEAF)))
+ SEND_SOUND(listener_mob, null_sound)
+
+/datum/sound_token/proc/update_listener(mob/listener_mob, update_sound = TRUE)
+ if(QDELETED(src))
+ return
+ if(isnull(listeners[listener_mob]))
+ return
+
+ var/turf/source_turf = get_turf(source)
+ var/turf/listener_turf = get_turf(listener_mob)
+
+ if(!source_turf || !listener_turf)
+ return
+
+ var/is_muted = listeners[listener_mob] & SOUND_MUTE
+ var/should_be_muted = FALSE
+
+ if(source_turf.z != listener_turf.z)
+ should_be_muted = TRUE
+
+ var/distance = get_dist_euclidean(source_turf, listener_turf)
+ if(distance > range)
+ should_be_muted = TRUE
+ if(should_be_muted && is_muted)
+ return
+
+ should_be_muted ||= HAS_TRAIT(listener_mob, TRAIT_DEAF)
+ if(should_be_muted && is_muted)
+ return
+
+ set_listener_status(listener_mob, should_be_muted ? SOUND_MUTE : NONE)
+ send_listener_sound(listener_mob, update_sound)
+
+/datum/sound_token/proc/send_listener_sound(mob/listener_mob, update_sound)
+ PRIVATE_PROC(TRUE)
+
+ sound.status = SOUND_STREAM|sound_status|listeners[listener_mob]
+ if(update_sound)
+ sound.status |= SOUND_UPDATE
+ else
+ sound.offset = calculate_offset()
+
+ if(sound.status & SOUND_MUTE)
+ SEND_SOUND(listener_mob, sound)
+ return
+
+ if(!listener_mob.playsound_local(get_turf(source), vol = volume, falloff_exponent = falloff_exponent, channel = sound_channel, sound_to_use = sound, max_distance = range, falloff_distance = falloff_distance, use_reverb = TRUE))
+ sound.status = SOUND_UPDATE|SOUND_MUTE
+ SEND_SOUND(listener_mob, sound)
+ sound.offset = null
+
+/datum/sound_token/proc/update_all_listeners()
+ for(var/mob/listener_mob in listeners)
+ if(listener_mob.client)
+ SSsound_tokens.clients_needing_update[listener_mob.client] = TRUE
+
+/datum/sound_token/proc/force_update_all_listeners(update_sound = TRUE)
+ for(var/mob/listener_mob in listeners)
+ if(listener_mob.client)
+ update_listener(listener_mob, update_sound)
+
+/// Setter for volume
+/datum/sound_token/proc/set_volume(new_volume, update_listeners = TRUE)
+ volume = new_volume
+ if(update_listeners)
+ update_all_listeners()
+
+/// Set the status of a listener. Does not update the sound.
+/datum/sound_token/proc/set_listener_status(mob/listener_mob, new_status)
+ if(isnull(listeners[listener_mob]))
+ return
+
+ listeners[listener_mob] = new_status
+
+/// Respond to TRAIT_DEAF addition/removal
+/datum/sound_token/proc/listener_deafness_update(atom/movable/source)
+ SIGNAL_HANDLER
+ update_listener(source)
+
+/datum/sound_token/proc/listener_deleted(datum/source)
+ SIGNAL_HANDLER
+ remove_listener(source)
+
+/// Respond to any mob in the world being logged into. Only adds if the mob is within range.
+/datum/sound_token/proc/player_login(datum/source, mob/player)
+ SIGNAL_HANDLER
+ var/turf/player_turf = get_turf(player)
+ var/turf/source_turf = get_turf(src.source)
+ if(!player_turf || !source_turf)
+ return
+ if(player_turf.z != source_turf.z)
+ return
+ if(get_dist_euclidean(source_turf, player_turf) > range)
+ return
+ add_or_update_listener(player)
+
+/// Respond to any cliented mob becoming uncliented
+/datum/sound_token/proc/player_logout(datum/source, mob/player)
+ SIGNAL_HANDLER
+ remove_listener(player)
+
+/// If the sound source moves, update tracked cells then refresh all listener positions.
+/datum/sound_token/proc/source_moved()
+ SIGNAL_HANDLER
+ update_tracked_cells()
+ update_all_listeners()
+
+/datum/sound_token/proc/source_deleted()
+ SIGNAL_HANDLER
+
+ qdel(src)
+
+///Update env when source is entering new area
+/datum/sound_token/proc/on_enter_area(datum/source, area/area_to_register)
+ SIGNAL_HANDLER
+ set_new_environment(area_to_register.sound_environment || SOUND_ENVIRONMENT_NONE)
+
+/datum/sound_token/proc/set_new_environment(new_env)
+ if(sound.environment == new_env)
+ return
+ sound.environment = new_env
+ update_all_listeners()
+
+///Calculates the offset to give the sound for people who start hearing it mid-play
+/datum/sound_token/proc/calculate_offset()
+ var/elapsed = REALTIMEOFDAY - start_time
+ var/freq_factor = (sound.frequency || 100) / 100
+ var/pitch_factor = (sound.pitch || 100) / 100
+ var/offset = elapsed * freq_factor * pitch_factor
+ return offset
+
+///Update tracked cells; happens on movement. We need to check if anyone is now out of cell range and kick them out.
+/datum/sound_token/proc/update_tracked_cells()
+ if(!get_turf(source))
+ return
+
+ var/list/new_and_old = cell_tracker.recalculate_cells(get_turf(source))
+ var/list/datum/spatial_grid_cell/added_cells = new_and_old[1]
+ var/list/datum/spatial_grid_cell/removed_cells = new_and_old[2]
+
+ for(var/datum/spatial_grid_cell/cell as anything in removed_cells)
+ UnregisterSignal(cell, list(SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS),))
+
+ // Remove listeners whose mob is no longer in any remaining member cell
+ if(removed_cells.len)
+ for(var/mob/listener_mob as anything in listeners)
+ var/still_in_range = FALSE
+ for(var/datum/spatial_grid_cell/cell as anything in cell_tracker.member_cells)
+ if(listener_mob in cell.client_contents)
+ still_in_range = TRUE
+ break
+ if(!still_in_range)
+ remove_listener(listener_mob)
+
+ for(var/datum/spatial_grid_cell/cell as anything in added_cells)
+ RegisterSignal(cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_cell_client_entered))
+ RegisterSignal(cell, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_cell_client_exited))
+ for(var/mob/listener_mob as anything in cell.client_contents)
+ add_or_update_listener(listener_mob)
+
+/// Signal handler for SPATIAL_GRID_CELL_ENTERED on tracked cells. Adds newly arriving mobs as listeners.
+/datum/sound_token/proc/on_cell_client_entered(datum/source, list/entering_mobs)
+ SIGNAL_HANDLER
+
+ for(var/mob/listener_mob as anything in entering_mobs)
+ if(!isnull(listeners[listener_mob])) // already added
+ continue
+ add_or_update_listener(listener_mob)
+
+/// Signal handler for SPATIAL_GRID_CELL_EXITED on tracked cells. Removes mobs who have left all member cells.
+/datum/sound_token/proc/on_cell_client_exited(datum/source, list/exiting_mobs)
+ SIGNAL_HANDLER
+ for(var/mob/listener_mob as anything in exiting_mobs)
+ var/still_in_range = FALSE
+ if(SSspatial_grid.get_cell_of(listener_mob) in cell_tracker.member_cells)
+ still_in_range = TRUE
+
+ if(!still_in_range)
+ remove_listener(listener_mob)
diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm
index c3734662080b..b8730620c2d0 100644
--- a/code/datums/status_effects/buffs.dm
+++ b/code/datums/status_effects/buffs.dm
@@ -732,3 +732,24 @@
/datum/status_effect/rev_resilience/on_remove()
to_chat(owner, span_notice("You feel your surge of revolutionary zeal fade. You hope you don't get shot in the foot..."))
owner.remove_traits(list(TRAIT_HARDLY_WOUNDED,TRAIT_ANALGESIA,TRAIT_FEARLESS), TRAIT_STATUS_EFFECT(id))
+
+//status effect granted when taking attack damage while metabolizing synthpax
+/datum/status_effect/synthpax_immunity
+ id = "synthpax_immune"
+ duration = 5 SECONDS
+ status_type = STATUS_EFFECT_REFRESH
+ alert_type = null
+
+/datum/status_effect/synthpax_immunity/on_creation(mob/living/new_owner, duration = 5 SECONDS)
+ src.duration = duration
+ return ..()
+
+/datum/status_effect/synthpax_immunity/on_apply()
+ ADD_TRAIT(owner, TRAIT_SYNTHPAX_IMMUNE, TRAIT_STATUS_EFFECT(id))
+ REMOVE_TRAIT(owner, TRAIT_PACIFISM, METABOLIZATION_TRAIT(/datum/reagent/pax/peaceborg))
+ return TRUE
+
+/datum/status_effect/synthpax_immunity/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_SYNTHPAX_IMMUNE, TRAIT_STATUS_EFFECT(id))
+ if(owner.reagents.has_reagent(/datum/reagent/pax/peaceborg))
+ ADD_TRAIT(owner, TRAIT_PACIFISM, METABOLIZATION_TRAIT(/datum/reagent/pax/peaceborg))
diff --git a/code/datums/status_effects/cuffed_item.dm b/code/datums/status_effects/cuffed_item.dm
index 4711af1cb01d..297666efc75a 100644
--- a/code/datums/status_effects/cuffed_item.dm
+++ b/code/datums/status_effects/cuffed_item.dm
@@ -14,47 +14,80 @@
var/obj/item/cuffed
///Reference to the pair of handcuffs used to bind the item
var/obj/item/restraints/handcuffs/cuffs
+ ///Reference to the bodypart we're cuffed to
+ var/obj/item/bodypart/arm/cuffed_to
+ ///Reference to the component managing the link between the mob and the item
+ VAR_PRIVATE/datum/component/chained_together/link_effect
/datum/status_effect/cuffed_item/on_creation(mob/living/new_owner, obj/item/cuffed, obj/item/restraints/handcuffs/cuffs)
src.cuffed = cuffed
src.cuffs = cuffs
. = ..() //throws the alert and all
+ if(QDELETED(src))
+ return
linked_alert.update_appearance(UPDATE_OVERLAYS)
/datum/status_effect/cuffed_item/on_apply()
- if(HAS_TRAIT_FROM(cuffed, TRAIT_NODROP, CUFFED_ITEM_TRAIT))
- qdel(src)
- return FALSE
owner.temporarilyRemoveItemFromInventory(cuffs, force = TRUE)
- if(!owner.is_holding(cuffed) && !owner.put_in_hands(cuffed))
+ cuffed_to = owner.get_inactive_hand()
+ if(isnull(cuffed_to) || !update_link())
owner.put_in_hands(cuffs)
qdel(src)
return FALSE
- ADD_TRAIT(cuffed, TRAIT_NODROP, CUFFED_ITEM_TRAIT)
-
- RegisterSignals(cuffed, list(COMSIG_ITEM_DROPPED, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(on_displaced))
+ RegisterSignals(cuffed, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED, COMSIG_MOVABLE_MOVED), PROC_REF(check_for_link))
+ RegisterSignal(cuffed, COMSIG_QDELETING, PROC_REF(cleanup_effect))
RegisterSignal(cuffed, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(on_item_update_appearance))
RegisterSignal(cuffed, COMSIG_ATOM_EXAMINE, PROC_REF(cuffed_reminder))
RegisterSignal(cuffed, COMSIG_TOPIC, PROC_REF(topic_handler))
RegisterSignal(cuffed, COMSIG_ITEM_GET_STRIPPABLE_ALT_ACTIONS, PROC_REF(get_strippable_action))
RegisterSignal(cuffed, COMSIG_ITEM_STRIPPABLE_ALT_ACTION, PROC_REF(do_strippable_action))
+ RegisterSignal(cuffed, COMSIG_ITEM_PRE_STORAGE_INSERTION, PROC_REF(block_storage_insert))
+ RegisterSignal(cuffed, COMSIG_ITEM_PRE_CUFFED_TO_MOB, PROC_REF(block_item_cuff))
- RegisterSignals(cuffs, list(COMSIG_ITEM_EQUIPPED, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(on_displaced))
+ RegisterSignals(cuffs, list(COMSIG_ITEM_EQUIPPED, COMSIG_QDELETING, COMSIG_MOVABLE_MOVED), PROC_REF(cleanup_effect))
RegisterSignal(cuffs, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(on_item_update_appearance))
+ cuffed_to.set_speed_modifiers(cuffed_to.interaction_modifier + 0.25, cuffed_to.click_cd_modifier + 0.25)
+ RegisterSignal(cuffed_to, COMSIG_QDELETING, PROC_REF(cleanup_effect))
+ RegisterSignal(cuffed_to, COMSIG_BODYPART_REMOVED, PROC_REF(cuffed_to_removed))
+
RegisterSignal(owner, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(on_examine_more))
- owner.log_message("bound [src] to themselves with restraints", LOG_GAME)
+ owner.log_message("bound [cuffed] to [owner.p_themselves()] with restraints", LOG_GAME)
+ SSblackbox.record_feedback("tally", "cuffed_item", 1, cuffed.type)
return TRUE
/datum/status_effect/cuffed_item/on_remove()
//Prevent possible recursions from these signals
- UnregisterSignal(cuffed, list(COMSIG_ITEM_DROPPED, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
- UnregisterSignal(cuffs, list(COMSIG_ITEM_EQUIPPED, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
-
- REMOVE_TRAIT(cuffed, TRAIT_NODROP, CUFFED_ITEM_TRAIT)
+ UnregisterSignal(cuffed, list(
+ COMSIG_ATOM_EXAMINE,
+ COMSIG_ATOM_UPDATE_APPEARANCE,
+ COMSIG_ITEM_PRE_CUFFED_TO_MOB,
+ COMSIG_ITEM_DROPPED,
+ COMSIG_ITEM_EQUIPPED,
+ COMSIG_ITEM_GET_STRIPPABLE_ALT_ACTIONS,
+ COMSIG_ITEM_PRE_STORAGE_INSERTION,
+ COMSIG_ITEM_STRIPPABLE_ALT_ACTION,
+ COMSIG_MOVABLE_MOVED,
+ COMSIG_QDELETING,
+ COMSIG_TOPIC,
+ ))
+ UnregisterSignal(cuffs, list(
+ COMSIG_ATOM_UPDATE_APPEARANCE,
+ COMSIG_ITEM_EQUIPPED,
+ COMSIG_MOVABLE_MOVED,
+ COMSIG_QDELETING,
+ ))
+ UnregisterSignal(cuffed_to, list(
+ COMSIG_BODYPART_REMOVED,
+ COMSIG_QDELETING,
+ ))
+ UnregisterSignal(owner, list(
+ COMSIG_ATOM_EXAMINE_MORE,
+ COMSIG_CARBON_POST_ATTACH_LIMB,
+ ))
cuffed = null
if(!QDELETED(cuffs))
@@ -63,14 +96,117 @@
cuffs.forceMove(owner.drop_location())
cuffs = null
+ cuffed_to.set_speed_modifiers(cuffed_to.interaction_modifier - 0.25, cuffed_to.click_cd_modifier - 0.25)
+ cuffed_to = null
+
+ break_leash()
+
///Called when someone examines the owner twice, so they can know if someone has a cuffed item
/datum/status_effect/cuffed_item/proc/on_examine_more(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
- examine_list += span_warning("[cuffed.examine_title(user)] is bound to [owner.p_their()] [owner.get_held_index_name(owner.get_held_index_of_item(cuffed))] by [cuffs.examine_title(user)]")
+ examine_list += span_warning("There's [cuffed.examine_title(user)] bound to [owner.p_their()] \
+ [cuffed_to.plaintext_zone] by [cuffs.examine_title(user)].")
+
+/// What happens if the limb we're cuffed to is removed?
+/datum/status_effect/cuffed_item/proc/cuffed_to_removed(datum/source, mob/living/carbon/owner, special)
+ SIGNAL_HANDLER
+ // if special we will just wait for the new limb
+ if(special)
+ UnregisterSignal(cuffed_to, list(COMSIG_QDELETING, COMSIG_BODYPART_REMOVED))
+ cuffed_to.set_speed_modifiers(cuffed_to.interaction_modifier - 0.25, cuffed_to.click_cd_modifier - 0.25)
+ cuffed_to = null
+ RegisterSignal(owner, COMSIG_CARBON_POST_ATTACH_LIMB, PROC_REF(new_cuffed_to_attached))
+ return
+ // otherwise we wipe the effect
+ qdel(src)
+
+/// Specifically if our cuffed limb is removed "specially", change it to the newly applied arm
+/datum/status_effect/cuffed_item/proc/new_cuffed_to_attached(datum/source, obj/item/bodypart/limb, special)
+ SIGNAL_HANDLER
+
+ if(!istype(limb, /obj/item/bodypart/arm))
+ return
+
+ cuffed_to = limb
+ cuffed_to.set_speed_modifiers(cuffed_to.interaction_modifier + 0.25, cuffed_to.click_cd_modifier + 0.25)
+ RegisterSignal(cuffed_to, COMSIG_QDELETING, PROC_REF(cleanup_effect))
+ RegisterSignal(cuffed_to, COMSIG_BODYPART_REMOVED, PROC_REF(cuffed_to_removed))
+ UnregisterSignal(owner, COMSIG_CARBON_POST_ATTACH_LIMB)
+
+/// Check if we need to spawn the tether effect or not
+/datum/status_effect/cuffed_item/proc/check_for_link(...)
+ SIGNAL_HANDLER
+ if(!update_link())
+ qdel(src)
+
+/// Updates our link and beam effect based on our state
+/// Returns TRUE if we are in a valid link state, FALSE otherwise
+/datum/status_effect/cuffed_item/proc/update_link()
+ // when held, we need no tether
+ if(cuffed.loc == owner)
+ return break_leash()
+
+ // when on the ground, init a tether between item <-> owner
+ if(isturf(cuffed.loc))
+ return init_leash(cuffed)
+
+ // when being picked up by something else, init a tether between grabber <-> owner
+ if(ismovable(cuffed.loc) && isturf(cuffed.loc.loc))
+ return init_leash(cuffed.loc)
+
+ // we have no idea where it is...
+ return FALSE
+
+/// Inits the leash and beam effect to the given target, cleaning up old ones if necessary
+/datum/status_effect/cuffed_item/proc/init_leash(atom/movable/leash_to)
+ if(link_effect)
+ if(link_effect.parent == leash_to)
+ return TRUE
+ break_leash()
+
+ link_effect = leash_to.AddComponentFrom(REF(src), /datum/component/chained_together, chained_to = owner)
+ if(!QDELETED(link_effect))
+ return TRUE
+
+ // chain component failed to apply
+ if(ismob(leash_to))
+ var/mob/leash_to_mob = leash_to
+ addtimer(CALLBACK(src, PROC_REF(eject_item), leash_to_mob), 1)
+ return TRUE
+
+ return FALSE
+
+/datum/status_effect/cuffed_item/proc/break_leash()
+ link_effect?.parent.RemoveComponentSource(REF(src), /datum/component/chained_together)
+ if(QDELETED(link_effect))
+ link_effect = null
+ return TRUE
+
+// Delayed unequip after an invalid pickup. This sucks but I can't think of a better way around due to move order shenanigans
+/datum/status_effect/cuffed_item/proc/eject_item(mob/leash_to_mob)
+ if(QDELETED(src) || cuffed.loc != leash_to_mob)
+ return
+ if(!leash_to_mob.dropItemToGround(cuffed))
+ qdel(src)
+ return
+ to_chat(leash_to_mob, span_warning("[cuffs] binding [cuffed] to [owner] tugs it out of your grasp!"))
+
+/// Stops it from being stored anywhere
+/datum/status_effect/cuffed_item/proc/block_storage_insert(obj/item/source, atom/target_storage, mob/user, force, messages)
+ SIGNAL_HANDLER
+ if(messages)
+ target_storage.balloon_alert(user, "can't store [source.name] while cuffed!")
+ return BLOCK_STORAGE_INSERT
+
+/// Stops double cuff
+/datum/status_effect/cuffed_item/proc/block_item_cuff(obj/item/source, mob/cuffer, obj/item/cuffs)
+ SIGNAL_HANDLER
+ source.balloon_alert(cuffer, "cuffed to someone else!")
+ return BLOCK_ITEM_CUFF
///What happens if one of the items is moved away from the mob
-/datum/status_effect/cuffed_item/proc/on_displaced(datum/source)
+/datum/status_effect/cuffed_item/proc/cleanup_effect(datum/source)
SIGNAL_HANDLER
qdel(src)
@@ -128,9 +264,10 @@
log_combat(user, owner, "removed restraints binding [cuffed] to")
var/obj/item/restraints/handcuffs/ref_cuffs = cuffs
+ var/mob/living/ref_owner = owner
ref_cuffs.forceMove(owner.drop_location()) //This will cause the status effect to delete itself, which unsets the 'cuffs' var
user.put_in_hands(ref_cuffs)
- owner.balloon_alert(user, "cuffs removed from item")
+ ref_owner.balloon_alert(user, "cuffs removed from item")
return TRUE
@@ -162,3 +299,55 @@
if(.)
var/datum/status_effect/cuffed_item/effect = attached_effect
effect?.try_remove_cuffs(owner)
+
+// Chains two movable to be adjacent to each other. YMMV using this
+/datum/component/chained_together
+ dupe_mode = COMPONENT_DUPE_SOURCES
+ /// Weakref to the thing we're chained to
+ var/datum/weakref/chained_to_weakref
+ // Tracks the various things we apply to whatever we are cuffed to for cleanup
+ VAR_PRIVATE/datum/component/leash/link_effect
+ VAR_PRIVATE/datum/component/tug_towards/tug_effect
+ VAR_PRIVATE/datum/beam/beam_effect
+
+/datum/component/chained_together/on_source_add(source, atom/movable/chained_to)
+ // more sources are only allowed if they're chaining to the same thing
+ // having something linked to two different things is not supported now, and will break horribly
+ return chained_to_weakref?.resolve() == chained_to ? ..() : COMPONENT_REDUNDANT
+
+/datum/component/chained_together/Initialize(atom/movable/chained_to)
+ if(!ismovable(parent) || !ismovable(chained_to))
+ return COMPONENT_INCOMPATIBLE
+
+ chained_to_weakref = WEAKREF(chained_to)
+ var/atom/movable/movable_parent = parent
+ link_effect = movable_parent.AddComponent(/datum/component/leash, owner = chained_to, distance = 1)
+ tug_effect = movable_parent.AddComponent(/datum/component/tug_towards, tugging_to = chained_to, strength = 0.66)
+ beam_effect = movable_parent.Beam(chained_to, "chain")
+ RegisterSignal(link_effect, COMSIG_QDELETING, PROC_REF(delete_self))
+ RegisterSignal(tug_effect, COMSIG_QDELETING, PROC_REF(delete_self))
+ RegisterSignal(beam_effect, COMSIG_QDELETING, PROC_REF(recreate_beam))
+
+/datum/component/chained_together/Destroy()
+ UnregisterSignal(beam_effect, COMSIG_QDELETING)
+ UnregisterSignal(link_effect, COMSIG_QDELETING)
+ UnregisterSignal(tug_effect, COMSIG_QDELETING)
+ if(!QDELETED(link_effect))
+ QDEL_NULL(link_effect)
+ if(!QDELETED(tug_effect))
+ QDEL_NULL(tug_effect)
+ if(!QDELETED(beam_effect))
+ QDEL_NULL(beam_effect)
+ return ..()
+
+/datum/component/chained_together/proc/recreate_beam(datum/beam/source)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(beam_effect, COMSIG_QDELETING)
+ var/atom/movable/movable_parent = parent
+ beam_effect = movable_parent.Beam(chained_to_weakref.resolve(), "chain")
+ RegisterSignal(beam_effect, COMSIG_QDELETING, PROC_REF(recreate_beam))
+
+/datum/component/chained_together/proc/delete_self(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
diff --git a/code/datums/status_effects/debuffs/dna_transformation.dm b/code/datums/status_effects/debuffs/dna_transformation.dm
index bd2a079d6f8b..15a96bca42dc 100644
--- a/code/datums/status_effects/debuffs/dna_transformation.dm
+++ b/code/datums/status_effects/debuffs/dna_transformation.dm
@@ -5,10 +5,12 @@
tick_interval = STATUS_EFFECT_NO_TICK
duration = 1 MINUTES // set in on creation, this just needs to be any value to process
alert_type = null
+ /// Flags used to determine what all we're copying over
+ VAR_PROTECTED/copy_dna_flags = COPY_DNA_SPECIES
/// A reference to a COPY of the DNA that the mob will be transformed into.
- var/datum/dna/new_dna
+ VAR_PRIVATE/datum/dna/new_dna
/// A reference to a COPY of the DNA of the mob prior to transformation.
- var/datum/dna/old_dna
+ VAR_PRIVATE/datum/dna/old_dna
/datum/status_effect/temporary_transformation/Destroy()
. = ..() // parent must be called first, so we clear DNA refs AFTER transforming back... yeah i know
@@ -16,10 +18,14 @@
QDEL_NULL(old_dna)
/datum/status_effect/temporary_transformation/on_creation(mob/living/new_owner, new_duration = 1 MINUTES, datum/dna/dna_to_copy)
+ if(!iscarbon(new_owner) || isnull(dna_to_copy))
+ qdel(src)
+ return
+
src.duration = new_duration
src.new_dna = new()
src.old_dna = new()
- dna_to_copy.copy_dna(new_dna)
+ init_dna(new_owner, dna_to_copy)
return ..()
/datum/status_effect/temporary_transformation/on_apply()
@@ -30,27 +36,39 @@
if(!transforming.has_dna())
return FALSE
- // Save the old DNA
- transforming.dna.copy_dna(old_dna)
- // Makes them into the new DNA
- new_dna.copy_dna(transforming.dna, COPY_DNA_SPECIES)
- transforming.real_name = new_dna.real_name
- transforming.name = transforming.get_visible_name()
- transforming.updateappearance(mutcolor_update = TRUE)
- transforming.domutcheck()
+ save_dna()
+ apply_dna()
return TRUE
/datum/status_effect/temporary_transformation/on_remove()
var/mob/living/carbon/transforming = owner
if(!QDELING(owner)) // Don't really need to do appearance stuff if we're being deleted
- old_dna.copy_dna(transforming.dna, COPY_DNA_SPECIES)
+ old_dna.copy_dna(transforming.dna, copy_dna_flags)
transforming.updateappearance(mutcolor_update = TRUE)
transforming.domutcheck()
transforming.real_name = old_dna.real_name // Name is fine though
transforming.name = transforming.get_visible_name()
+/// Called when initializing the DNA that the mob is transforming into
+/datum/status_effect/temporary_transformation/proc/init_dna(mob/living/carbon/new_owner, datum/dna/dna_to_copy)
+ dna_to_copy.copy_dna(new_dna, copy_dna_flags)
+
+/// Called when saving the mob's DNA before transformation
+/datum/status_effect/temporary_transformation/proc/save_dna()
+ var/mob/living/carbon/transforming = owner
+ transforming.dna.copy_dna(old_dna, copy_dna_flags)
+
+/// Applies the DNA to the mob
+/datum/status_effect/temporary_transformation/proc/apply_dna()
+ var/mob/living/carbon/transforming = owner
+ new_dna.copy_dna(transforming.dna, copy_dna_flags)
+ transforming.real_name = new_dna.real_name
+ transforming.name = transforming.get_visible_name()
+ transforming.updateappearance(mutcolor_update = TRUE)
+ transforming.domutcheck()
+
/datum/status_effect/temporary_transformation/trans_sting
/// Tracks the time left on the effect when the owner last died. Used to pause the effect.
var/time_before_pause = -1
@@ -89,3 +107,35 @@
else if(time_before_pause != -1)
duration = time_before_pause
time_before_pause = -1
+
+/datum/status_effect/temporary_transformation/dna_injector
+ id = "temp_dna_injector_transformation"
+ status_type = STATUS_EFFECT_MULTIPLE
+ copy_dna_flags = NONE // no touching species or mutations
+
+// when initting dna, any unset fields are copied from the mob's dna (so nothing changes effectively)
+/datum/status_effect/temporary_transformation/dna_injector/init_dna(mob/living/carbon/new_owner, datum/dna/dna_to_copy)
+ . = ..()
+ new_dna.real_name ||= new_owner.dna.real_name
+ new_dna.unique_enzymes ||= new_owner.dna.unique_enzymes
+ new_dna.unique_features ||= new_owner.dna.unique_features
+ new_dna.unique_identity ||= new_owner.dna.unique_identity
+ new_dna.blood_type ||= new_owner.dna.blood_type
+ // just to put something there, it'll get updated if UF does anyways
+ new_dna.features = new_owner.dna.features.Copy()
+
+// ensure secondary transformation make a copy of the original dna (to prevent latter effects that expire earlier from returning to the wrong dna)
+/datum/status_effect/temporary_transformation/dna_injector/save_dna()
+ for(var/datum/status_effect/temporary_transformation/dna_injector/other_effect in owner.status_effects)
+ other_effect.old_dna.copy_dna(src.old_dna, copy_dna_flags)
+ return
+
+ return ..()
+
+// when the effect ends, see if there's any other active effects, and re-apply them if necessary
+/datum/status_effect/temporary_transformation/dna_injector/on_remove()
+ . = ..()
+ if(QDELING(owner))
+ return
+ for(var/datum/status_effect/temporary_transformation/dna_injector/other_effect in owner.status_effects)
+ other_effect.apply_dna()
diff --git a/code/datums/status_effects/debuffs/slime/slime_leech.dm b/code/datums/status_effects/debuffs/slime/slime_leech.dm
index 0df86b15130d..5c7b7567d553 100644
--- a/code/datums/status_effects/debuffs/slime/slime_leech.dm
+++ b/code/datums/status_effects/debuffs/slime/slime_leech.dm
@@ -41,7 +41,10 @@
our_slime = null
/datum/status_effect/slime_leech/tick(seconds_between_ticks)
- if(our_slime.stat != CONSCIOUS)
+ if(QDELETED(our_slime))
+ qdel(src)
+ return
+ if(our_slime.stat != CONSCIOUS || !owner)
our_slime.stop_feeding(silent = TRUE)
return
diff --git a/code/datums/status_effects/debuffs/spacer.dm b/code/datums/status_effects/debuffs/spacer.dm
index 99b724b1d356..7fa342370e2c 100644
--- a/code/datums/status_effects/debuffs/spacer.dm
+++ b/code/datums/status_effects/debuffs/spacer.dm
@@ -18,33 +18,58 @@
// The good side (being in space)
/datum/status_effect/spacer/gravity_wellness
alert_type = null
- tick_interval = 3 SECONDS
+ tick_interval = 1 SECONDS
/// How much disgust to heal per tick
- var/disgust_healing_per_tick = 1.5
- /// How much of stamina damage to heal per tick when we've been in nograv for a while
- var/stamina_heal_per_tick = 3
+ var/disgust_healing_per_second = 0.5
+ /// How much of stamina damage to heal per tick when we've been in nograv for a while.
+ var/stamina_heal_per_second = 4
/// How many seconds of stuns to reduce per tick when we've been in nograv for a while
- var/stun_heal_per_tick = 3 SECONDS
+ var/stun_heal_per_second = 2 SECONDS
+ /// How much to reduce incoming knockdown effects while in nograv for a while
+ var/knockdown_multiplier = 0.75
/// Tracks how long we've been in no gravity
VAR_FINAL/seconds_in_nograv = 0 SECONDS
+ /// Tracks if we've applied knockdown modifier reduction to the mob
+ VAR_FINAL/knockdown_mod_applied = FALSE
/datum/status_effect/spacer/gravity_wellness/tick(seconds_between_ticks)
var/in_nograv = !owner.has_gravity()
- var/nograv_mod = in_nograv ? 1 : 0.5
- owner.adjust_disgust(-1 * disgust_healing_per_tick * nograv_mod)
+ var/nograv_mod = in_nograv ? 2 : 0.5
+ owner.adjust_disgust(-1 * nograv_mod * disgust_healing_per_second * seconds_between_ticks)
if(!in_nograv)
seconds_in_nograv = 0 SECONDS
+ if(knockdown_mod_applied)
+ remove_knockdown_mod()
return
seconds_in_nograv += (seconds_between_ticks * 0.1)
- if(seconds_in_nograv >= 3 MINUTES)
- // This has some interesting side effects with gravitum or similar negating effects that may be worth nothing
- owner.adjust_stamina_loss(-1 * stamina_heal_per_tick)
- owner.AdjustAllImmobility(-1 * stun_heal_per_tick)
- // For comparison: Ephedrine heals 4 stamina per tick / 2 per second
- // and Nicotine heals 5 seconds of stun per tick / 2.5 per second
+ if(seconds_in_nograv >= 1 MINUTES)
+ // With 4 stamina healing per second you'd get out of stamcrit in 6 seconds instead of the usual 10
+ owner.adjust_stamina_loss(-1 * stamina_heal_per_second * seconds_between_ticks)
+ if(seconds_in_nograv >= 10 SECONDS)
+ owner.AdjustAllImmobility(-1 * stun_heal_per_second * seconds_between_ticks)
+ if(!knockdown_mod_applied)
+ apply_knockdown_mod()
+
+/datum/status_effect/spacer/gravity_wellness/on_remove()
+ . = ..()
+ remove_knockdown_mod()
+
+/datum/status_effect/spacer/gravity_wellness/proc/apply_knockdown_mod()
+ if(knockdown_mod_applied || !ishuman(owner))
+ return
+ var/mob/living/carbon/human/the_spacer = owner
+ the_spacer.physiology.knockdown_mod *= knockdown_multiplier
+ knockdown_mod_applied = TRUE
+
+/datum/status_effect/spacer/gravity_wellness/proc/remove_knockdown_mod()
+ if(!knockdown_mod_applied || !ishuman(owner))
+ return
+ var/mob/living/carbon/human/the_spacer = owner
+ the_spacer.physiology.knockdown_mod /= knockdown_multiplier
+ knockdown_mod_applied = FALSE
// The bad side (being on a planet)
/datum/status_effect/spacer/gravity_sickness
@@ -125,7 +150,7 @@
/datum/movespeed_modifier/spacer/in_space
movetypes = FLOATING
blacklisted_movetypes = FLYING
- multiplicative_slowdown = -0.1
+ multiplicative_slowdown = -0.15
/datum/movespeed_modifier/spacer/on_planet
movetypes = GROUND|FLYING
@@ -136,4 +161,4 @@
multiplicative_slowdown = 0.5
/datum/movespeed_modifier/spacer/on_planet/nerfed
- multiplicative_slowdown = 0.25
+ multiplicative_slowdown = 0.15
diff --git a/code/datums/status_effects/wound_effects.dm b/code/datums/status_effects/wound_effects.dm
index 1fd6c53e8c12..5ea0be0b256d 100644
--- a/code/datums/status_effects/wound_effects.dm
+++ b/code/datums/status_effects/wound_effects.dm
@@ -79,18 +79,23 @@
// less limping while we have determination still
var/determined_mod = owner.has_status_effect(/datum/status_effect/determined) ? 0.5 : 1
- if(SEND_SIGNAL(owner, COMSIG_CARBON_LIMPING) & COMPONENT_CANCEL_LIMP)
- return
-
+ var/limp_chance
+ var/limp_slowdown
if(next_leg == left)
- if(prob(limp_chance_left * determined_mod))
- owner.client.move_delay += slowdown_left * determined_mod
+ limp_chance = limp_chance_left
+ limp_slowdown = slowdown_left
next_leg = right
else
- if(prob(limp_chance_right * determined_mod))
- owner.client.move_delay += slowdown_right * determined_mod
+ limp_chance = limp_chance_right
+ limp_slowdown = slowdown_right
next_leg = left
+ if(SEND_SIGNAL(owner, COMSIG_CARBON_LIMPING) & COMPONENT_CANCEL_LIMP)
+ return
+
+ if(prob(limp_chance * determined_mod))
+ owner.client.move_delay += limp_slowdown * determined_mod
+
/// We need to make sure that we properly clear these refs if one of the owner's limbs gets deleted
/datum/status_effect/limp/proc/on_limb_removed(datum/source, obj/item/bodypart/limb_lost, special, dismembered)
SIGNAL_HANDLER
@@ -137,6 +142,31 @@
carbon_mob.remove_status_effect(src)
return
+//Quirk variant of limping. Will always be applied as long as you have a leg to stand on.
+/datum/status_effect/limp/quirk
+ id = "limp_quirk"
+ alert_type = null
+
+/datum/status_effect/limp/quirk/update_limp(datum/source)
+ var/mob/living/carbon/carbon_mob = owner
+ left = carbon_mob.get_bodypart(BODY_ZONE_L_LEG)
+ right = carbon_mob.get_bodypart(BODY_ZONE_R_LEG)
+
+ slowdown_left = 0
+ slowdown_right = 0
+ limp_chance_left = 0
+ limp_chance_right = 0
+
+
+ if(left)
+ slowdown_left = 7 //Same as compound fracture
+ limp_chance_left = 70
+
+ else if(right)
+ slowdown_right = 7
+ limp_chance_right = 70
+
+
/////////////////////////
//////// WOUNDS /////////
/////////////////////////
diff --git a/code/datums/storage/storage.dm b/code/datums/storage/storage.dm
index 20cfc49b737f..c478de045f5d 100644
--- a/code/datums/storage/storage.dm
+++ b/code/datums/storage/storage.dm
@@ -489,6 +489,8 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches)
return FALSE
if(SEND_SIGNAL(parent, COMSIG_ATOM_PRE_STORED_ITEM, to_insert, user, force, messages) & BLOCK_STORAGE_INSERT)
return FALSE
+ if(SEND_SIGNAL(to_insert, COMSIG_ITEM_PRE_STORAGE_INSERTION, parent, user, force, messages) & BLOCK_STORAGE_INSERT)
+ return FALSE
SEND_SIGNAL(parent, COMSIG_ATOM_STORED_ITEM, to_insert, user, force)
SEND_SIGNAL(src, COMSIG_STORAGE_STORED_ITEM, to_insert, user, force)
diff --git a/code/datums/storage/storage_interface.dm b/code/datums/storage/storage_interface.dm
index 1f358af00efb..2e42e34a6ea0 100644
--- a/code/datums/storage/storage_interface.dm
+++ b/code/datums/storage/storage_interface.dm
@@ -112,7 +112,7 @@
for(var/obj/item as anything in storage_contents)
item.mouse_opacity = MOUSE_OPACITY_OPAQUE
- item.screen_loc = "[current_x]:[screen_pixel_x],[current_y]:[screen_pixel_y]"
+ item.screen_loc = "[current_x]:[screen_pixel_x + item.base_pixel_x],[current_y]:[screen_pixel_y + item.base_pixel_y]"
if(parent_storage.numerical_stacking)
item.maptext = storage_contents[item]
SET_PLANE(item, ABOVE_HUD_PLANE, our_turf)
diff --git a/code/datums/verbs.dm b/code/datums/verbs.dm
deleted file mode 100644
index 9fed14c20c81..000000000000
--- a/code/datums/verbs.dm
+++ /dev/null
@@ -1,102 +0,0 @@
-/datum/verbs
- var/name
- var/list/children
- var/datum/verbs/parent
- var/list/verblist
- var/abstract = FALSE
-
-//returns the master list for verbs of a type
-/datum/verbs/proc/GetList()
- CRASH("Abstract verblist for [type]")
-
-//do things for each entry in Generate_list
-//return value sets Generate_list[verbpath]
-/datum/verbs/proc/HandleVerb(list/entry, procpath/verbpath, ...)
- return entry
-
-/datum/verbs/New()
- var/mainlist = GetList()
- var/ourentry = mainlist[type]
- children = list()
- verblist = list()
- if (ourentry)
- if (!islist(ourentry)) //some of our childern already loaded
- qdel(src)
- CRASH("Verb double load: [type]")
- Add_children(ourentry)
-
- mainlist[type] = src
-
- Load_verbs(type, typesof("[type]/verb"))
-
- var/datum/verbs/parent = mainlist[parent_type]
- if (!parent)
- mainlist[parent_type] = list(src)
- else if (islist(parent))
- parent += src
- else
- parent.Add_children(list(src))
-
-/datum/verbs/proc/Set_parent(datum/verbs/_parent)
- parent = _parent
- if (abstract)
- parent.Add_children(children)
- var/list/verblistoftypes = list()
- for(var/thing in verblist)
- LAZYADD(verblistoftypes[verblist[thing]], thing)
-
- for(var/verbparenttype in verblistoftypes)
- parent.Load_verbs(verbparenttype, verblistoftypes[verbparenttype])
-
-/datum/verbs/proc/Add_children(list/kids)
- if (abstract && parent)
- parent.Add_children(kids)
- return
-
- for(var/thing in kids)
- var/datum/verbs/item = thing
- item.Set_parent(src)
- if (!item.abstract)
- children += item
-
-/datum/verbs/proc/Load_verbs(verb_parent_type, list/verbs)
- if (abstract && parent)
- parent.Load_verbs(verb_parent_type, verbs)
- return
-
- for (var/verbpath in verbs)
- verblist[verbpath] = verb_parent_type
-
-/datum/verbs/proc/Generate_list(...)
- . = list()
- if (length(children))
- for (var/thing in children)
- var/datum/verbs/child = thing
- var/list/childlist = child.Generate_list(arglist(args))
- if (childlist)
- var/childname = "[child]"
- if (childname == "[child.type]")
- var/list/tree = splittext(childname, "/")
- childname = tree[tree.len]
- .[child.type] = "parent=[url_encode("[type]")];name=[childname]"
- . += childlist
-
- for (var/thing in verblist)
- var/procpath/verbpath = thing
- if (!verbpath)
- stack_trace("Bad VERB in [type] verblist: [english_list(verblist)]")
- var/list/entry = list()
- entry["parent"] = "[type]"
- entry["name"] = verbpath.desc
- if (verbpath.name[1] == "@")
- entry["command"] = copytext(verbpath.name, length(verbpath.name[1]) + 1)
- else
- entry["command"] = replacetext(verbpath.name, " ", "-")
-
- .[verbpath] = HandleVerb(arglist(list(entry, verbpath) + args))
-
-/world/proc/LoadVerbs(verb_type)
- if(!ispath(verb_type, /datum/verbs) || verb_type == /datum/verbs)
- CRASH("Invalid verb_type: [verb_type]")
- for (var/typepath in subtypesof(verb_type))
- new typepath()
diff --git a/code/datums/view.dm b/code/datums/view.dm
index b9a15f0cb05e..4c2ccfc46ae5 100644
--- a/code/datums/view.dm
+++ b/code/datums/view.dm
@@ -45,16 +45,16 @@
SEND_SIGNAL(chief.mob, COMSIG_VIEWDATA_UPDATE, getView())
/datum/view_data/proc/assertFormat()//T-Pose
- winset(chief, "mapwindow.map", "zoom=0")
+ winset(chief, SKIN_MAPWINDOW_MAP, "zoom=0")
zoom = 0
/datum/view_data/proc/resetFormat()//Cuck
zoom = chief?.prefs.read_preference(/datum/preference/numeric/pixel_size)
- winset(chief, "mapwindow.map", "zoom=[zoom]")
+ winset(chief, SKIN_MAPWINDOW_MAP, "zoom=[zoom]")
chief?.attempt_auto_fit_viewport() // If you change zoom mode, fit the viewport
/datum/view_data/proc/setZoomMode()
- winset(chief, "mapwindow.map", "zoom-mode=[chief?.prefs.read_preference(/datum/preference/choiced/scaling_method)]")
+ winset(chief, SKIN_MAPWINDOW_MAP, "zoom-mode=[chief?.prefs.read_preference(/datum/preference/choiced/scaling_method)]")
/datum/view_data/proc/isZooming()
return (width || height)
diff --git a/code/datums/weather/particle_weather.dm b/code/datums/weather/particle_weather.dm
new file mode 100644
index 000000000000..b8e1a5ac3fe2
--- /dev/null
+++ b/code/datums/weather/particle_weather.dm
@@ -0,0 +1,145 @@
+/// Subtype which uses particle systems instead of overlays to display its effects
+/datum/weather/particle
+ abstract_type = /datum/weather/particle
+ /// Particles used to display the weather visuals
+ var/particles/weather/particle_type = null
+ /// Secondary (layered ontop) particle type which is rendered as emissive in case you want embers or whatever
+ var/particles/weather/emissive_type = null
+ /// Minimum possible severity for the weather, (0~100]
+ var/min_severity = 1
+ /// Maximum possible severity for the weather, (0~100]
+ var/max_severity = MAXIMUM_WEATHER_SEVERITY
+ /// Maximum variation in severity each weather frame
+ var/severity_variation = 5
+ /// Optimal point towards which severity will try to gravitate by influencing random values
+ var/optimal_severity = 70
+ /// How often can we change our severity?
+ /// Don't set this too low or it'll look jank
+ var/severity_cooldown = 5 SECONDS
+
+ /// Current weather severity
+ var/severity = 0
+ /// Last tick during which we've changed our visual severity
+ var/last_severity_tick = 0
+ /// List of weather object lists (as we can have multiple if emissives are involved) by plane offset
+ var/list/weather_objects = list()
+ /// Direction of our wind
+ var/wind_sign = 0
+
+/datum/weather/particle/New(z_levels, list/weather_data)
+ . = ..()
+ if (isnull(particle_type) && isnull(emissive_type))
+ CRASH("[src] ([type]) attempted to initialize without normal or emissive particle types!")
+
+ for (var/offset in 0 to SSmapping.max_plane_offset)
+ var/list/object_list = list()
+ if (particle_type)
+ var/obj/effect/abstract/weather_holder/holder = new()
+ SET_PLANE_W_SCALAR(holder, RENDER_PLANE_PARTICLE_WEATHER, offset)
+ holder.particles = new particle_type()
+ object_list += holder
+
+ if (emissive_type)
+ var/obj/effect/abstract/weather_holder/holder = new()
+ holder.particles = new emissive_type()
+ SET_PLANE_W_SCALAR(holder, RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER, offset)
+ object_list += holder
+
+ weather_objects += list(object_list)
+
+ SSweather.add_weather_objects(weather_objects)
+
+/datum/weather/particle/Destroy()
+ SSweather.remove_weather_objects(weather_objects)
+ for(var/list/object_list as anything in weather_objects)
+ QDEL_LIST(object_list)
+ weather_objects = null
+ return ..()
+
+/datum/weather/particle/telegraph(list/weather_data)
+ . = ..()
+ if (!.)
+ return
+ animate_severity(0)
+
+/datum/weather/particle/end()
+ . = ..()
+ if (!.)
+ return
+ animate_severity(0)
+
+/// Adjust our severity by a random number based on our stage
+/datum/weather/particle/proc/process_particles()
+ if (last_severity_tick + severity_cooldown > world.time)
+ return
+
+ last_severity_tick = world.time
+ var/new_severity = severity
+ switch (stage)
+ if (STARTUP_STAGE)
+ // Aims at minimum severity, and can only go up
+ new_severity += rand() * severity_variation * (1 - severity / min_severity)
+ if (MAIN_STAGE)
+ // Tries to stay close to optimal severity
+ new_severity += LERP(-severity_variation * clamp(INVERSE_LERP(min_severity, optimal_severity, severity), 0, 1), severity_variation * clamp(INVERSE_LERP(max_severity, optimal_severity, severity), 0, 1), rand())
+ new_severity = clamp(new_severity, min(new_severity, min_severity), max_severity)
+ if (WIND_DOWN_STAGE)
+ // Slowly goes down to zero
+ new_severity += rand() * -severity_variation * (severity / min_severity)
+
+ animate_severity(new_severity)
+
+/datum/weather/particle/proc/animate_severity(new_severity)
+ if (!wind_sign)
+ wind_sign = pick(-1, 1)
+
+ severity = new_severity
+ for (var/list/holder_list as anything in weather_objects)
+ for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list)
+ var/particles/weather/particle_effect = holder.particles
+ particle_effect.animate_severity(severity / MAXIMUM_WEATHER_SEVERITY, wind_sign)
+
+/datum/weather/particle/generate_overlay_cache()
+ . = ..()
+ for (var/offset in 0 to SSmapping.max_plane_offset)
+ . += mutable_appearance('icons/effects/weather_overlay.dmi', "weather_overlay", overlay_layer, null, WEATHER_MASK_PLANE, offset_const = offset)
+
+/obj/effect/abstract/weather_holder
+ icon = null
+ appearance_flags = TILE_BOUND | PIXEL_SCALE
+ blocks_emissive = EMISSIVE_BLOCK_NONE
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+
+/particles/weather
+ spawning = 0
+ // Wider than our view due to weird BYOND jank
+ width = 800
+ height = 800
+ count = 8000
+
+ lifespan = 30 SECONDS
+ fade = 1 SECONDS
+ fadein = 0.5 SECONDS
+
+ /// Increase in speed per tick
+ var/wind_strength = 0
+ /// Minimum number of spawned particles per tick for easing
+ var/min_spawn = 0
+ /// Maximum amount of spawned particles at full strength
+ var/max_spawn = 0
+
+/// Changes the strength of the weather visual effect, severity should be between 0 and 1
+/particles/weather/proc/animate_severity(severity, wind_sign)
+ // Stop spawning if severity is zero or negative
+ if (severity <= 0)
+ spawning = 0
+ return
+
+ var/wind = wind_strength * severity * wind_sign
+ spawning = LERP(min_spawn, max_spawn, severity)
+ // Might already be set on init, in which case we preserve y and z components
+ if (islist(gravity) && length(gravity))
+ gravity[1] = wind
+ else
+ gravity = list(wind)
+
diff --git a/code/datums/weather/weather.dm b/code/datums/weather/weather.dm
index df380a58ad05..2829d15523ae 100644
--- a/code/datums/weather/weather.dm
+++ b/code/datums/weather/weather.dm
@@ -13,9 +13,10 @@
*/
/datum/weather
- /// name of weather
+ abstract_type = /datum/weather
+ /// Name of weather
var/name = "space wind"
- /// description of weather
+ /// Description of weather
var/desc = "Heavy gusts of wind blanket the area, periodically knocking down anyone caught in the open."
/// The message displayed in chat to foreshadow the weather's beginning
var/telegraph_message = span_warning("The wind begins to pick up.")
@@ -42,6 +43,8 @@
var/weather_overlay
/// Color to apply to the area while weather is occuring
var/weather_color = null
+ /// Alpha of the weather overlay
+ var/weather_alpha = 255
/// Displayed once the weather is over
var/end_message = span_danger("The wind relents its assault.")
@@ -326,7 +329,7 @@
*/
/datum/weather/proc/start()
if(stage >= MAIN_STAGE)
- return
+ return FALSE
SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_START(type), src)
stage = MAIN_STAGE
update_areas()
@@ -335,6 +338,7 @@
addtimer(CALLBACK(src, PROC_REF(wind_down)), weather_duration, TIMER_UNIQUE)
for(var/area/impacted_area as anything in impacted_areas)
SEND_SIGNAL(impacted_area, COMSIG_WEATHER_BEGAN_IN_AREA(type), src)
+ return TRUE
/**
* Weather enters the winding down phase, stops effects
@@ -345,12 +349,13 @@
*/
/datum/weather/proc/wind_down()
if(stage >= WIND_DOWN_STAGE)
- return
+ return FALSE
SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_WINDDOWN(type), src)
stage = WIND_DOWN_STAGE
update_areas()
send_alert(end_message, end_sound, end_sound_vol)
addtimer(CALLBACK(src, PROC_REF(end)), end_duration, TIMER_UNIQUE)
+ return TRUE
/**
* Fully ends the weather
@@ -361,7 +366,7 @@
*/
/datum/weather/proc/end()
if(stage == END_STAGE)
- return
+ return FALSE
SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_END(type), src)
UnregisterSignal(SSdcs, COMSIG_GLOB_MOB_CREATED)
stage = END_STAGE
@@ -373,6 +378,7 @@
if(target_trait)
for(var/mob/living/affected as anything in GLOB.mob_living_list | GLOB.dead_mob_list)
UnregisterSignal(affected, COMSIG_MOB_LOGIN)
+ return TRUE
// handles sending all alerts
/datum/weather/proc/send_alert(alert_msg, alert_sfx, alert_sfx_vol = 100)
@@ -576,11 +582,11 @@
// I prefer it to creating 2 extra plane masters however, so it's a cost I'm willing to pay
// LU
if(use_glow)
- var/mutable_appearance/glow_overlay = mutable_appearance('icons/effects/glow_weather.dmi', weather_state, overlay_layer, null, WEATHER_GLOW_PLANE, 100, offset_const = offset)
+ var/mutable_appearance/glow_overlay = mutable_appearance('icons/effects/glow_weather.dmi', weather_state, overlay_layer, null, WEATHER_GLOW_PLANE, 100 / 255 * weather_alpha, offset_const = offset)
glow_overlay.color = weather_color
gen_overlay_cache += glow_overlay
- var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, offset_const = offset)
+ var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, alpha = weather_alpha, offset_const = offset)
new_weather_overlay.color = weather_color
gen_overlay_cache += new_weather_overlay
diff --git a/code/datums/weather/weather_types/ash_storm.dm b/code/datums/weather/weather_types/ash_storm.dm
index 367977a90bca..ae12115e367f 100644
--- a/code/datums/weather/weather_types/ash_storm.dm
+++ b/code/datums/weather/weather_types/ash_storm.dm
@@ -1,8 +1,15 @@
-//Ash storms happen frequently on lavaland. They heavily obscure vision, and cause high fire damage to anyone caught outside.
-/datum/weather/ash_storm
+/// Ash storms happen frequently on lavaland. They heavily obscure vision, and cause high fire damage to anyone caught outside.
+/datum/weather/particle/ash_storm
name = "ash storm"
desc = "An intense atmospheric storm lifts ash off of the planet's surface and billows it down across the area, dealing intense fire damage to the unprotected."
+ particle_type = /particles/weather/ash_storm
+ emissive_type = /particles/weather/ash_storm/embers
+ min_severity = 60
+ optimal_severity = 80
+ weather_alpha = 100
+ wind_sign = -1 // Always blows left to sync with the animated overlays
+
telegraph_message = span_boldwarning("An eerie moan rises on the wind. Sheets of burning ash blacken the horizon. Seek shelter.")
telegraph_duration = 30 SECONDS
telegraph_overlay = "light_ash"
@@ -28,10 +35,51 @@
var/list/weak_sounds = list()
var/list/strong_sounds = list()
-/datum/weather/ash_storm/get_playlist_ref()
+/particles/weather/ash_storm
+ icon = 'icons/effects/particles/smoke.dmi'
+ icon_state = list("chill_1" = 4, "chill_2" = 3, "chill_3" = 2)
+ position = generator(GEN_BOX, list(-510, -256, 0), list(400, 512, 0))
+ grow = list(-0.01, -0.01)
+ gravity = list(0, -7, 0.5)
+ drift = generator(GEN_BOX, list(-1, -1, 0), list(1, 0, 0))
+ friction = 0.3
+ min_spawn = 20
+ max_spawn = 400
+ wind_strength = 16
+ spin = generator(GEN_NUM, -5, 5, NORMAL_RAND)
+ /// Y-coordinate of our gravity
+ var/gravity_power = -12
+
+/particles/weather/ash_storm/animate_severity(severity)
+ . = ..()
+ if (length(gravity) > 1)
+ gravity[2] = gravity_power * severity
+ else
+ gravity = list(gravity[1], gravity_power * severity, 0.5)
+
+/particles/weather/ash_storm/embers
+ icon = 'icons/effects/particles/generic.dmi'
+ icon_state = list("dot" = 4, "cross" = 2, "curl" = 1)
+ color = LIGHT_COLOR_FIRE
+ min_spawn = 10
+ max_spawn = 80
+
+/particles/weather/ash_storm/emberfall
+ min_spawn = 20
+ max_spawn = 200
+ wind_strength = 3
+ gravity_power = -2
+
+/particles/weather/ash_storm/embers/emberfall
+ min_spawn = 20
+ max_spawn = 200
+ wind_strength = 3
+ gravity_power = -2
+
+/datum/weather/particle/ash_storm/get_playlist_ref()
return GLOB.ash_storm_sounds
-/datum/weather/ash_storm/telegraph()
+/datum/weather/particle/ash_storm/telegraph()
for(var/area/impacted_area as anything in impacted_areas)
if(impacted_area.outdoors)
weak_sounds[impacted_area] = /datum/looping_sound/weak_outside_ashstorm
@@ -45,17 +93,17 @@
GLOB.ash_storm_sounds += weak_sounds
return ..()
-/datum/weather/ash_storm/start()
+/datum/weather/particle/ash_storm/start()
GLOB.ash_storm_sounds -= weak_sounds
GLOB.ash_storm_sounds += strong_sounds
return ..()
-/datum/weather/ash_storm/wind_down()
+/datum/weather/particle/ash_storm/wind_down()
GLOB.ash_storm_sounds -= strong_sounds
GLOB.ash_storm_sounds += weak_sounds
return ..()
-/datum/weather/ash_storm/recursive_weather_protection_check(atom/to_check)
+/datum/weather/particle/ash_storm/recursive_weather_protection_check(atom/to_check)
. = ..()
if(. || !ishuman(to_check))
return
@@ -63,12 +111,13 @@
if(human_to_check.get_thermal_protection() >= FIRE_IMMUNITY_MAX_TEMP_PROTECT)
return TRUE
-/datum/weather/ash_storm/weather_act_mob(mob/living/victim)
+/datum/weather/particle/ash_storm/weather_act_mob(mob/living/victim)
victim.adjust_fire_loss(4, required_bodytype = BODYTYPE_ORGANIC)
return ..()
-/datum/weather/ash_storm/end()
+/datum/weather/particle/ash_storm/end()
GLOB.ash_storm_sounds -= weak_sounds
+ GLOB.ash_storm_sounds -= strong_sounds
for(var/turf/open/misc/asteroid/basalt/basalt as anything in GLOB.dug_up_basalt)
if(!(basalt.loc in impacted_areas) || !(basalt.z in impacted_z_levels))
continue
@@ -76,7 +125,7 @@
return ..()
//Emberfalls are the result of an ash storm passing by close to the playable area of lavaland. They have a 10% chance to trigger in place of an ash storm.
-/datum/weather/ash_storm/emberfall
+/datum/weather/particle/ash_storm/emberfall
name = "emberfall"
desc = "A passing ash storm blankets the area in harmless embers."
@@ -89,3 +138,6 @@
weather_flags = parent_type::weather_flags & ~(WEATHER_MOBS|WEATHER_THUNDER)
probability = 10
+
+ particle_type = /particles/weather/ash_storm/emberfall
+ emissive_type = /particles/weather/ash_storm/embers/emberfall
diff --git a/code/datums/weather/weather_types/rain_storm.dm b/code/datums/weather/weather_types/rain_storm.dm
index 4516112824c4..2b2998fff727 100644
--- a/code/datums/weather/weather_types/rain_storm.dm
+++ b/code/datums/weather/weather_types/rain_storm.dm
@@ -1,16 +1,16 @@
-/datum/weather/rain_storm
+/datum/weather/particle/rain_storm
name = "rain"
desc = "Heavy thunderstorms rain down below, drenching anyone caught in it."
+ particle_type = /particles/weather/rain_storm
+ min_severity = 30
+
telegraph_message = span_danger("Thunder rumbles far above. You hear droplets drumming against the canopy.")
- telegraph_overlay = "rain_low"
telegraph_duration = 30 SECONDS
weather_message = span_userdanger("Rain pours down around you!")
- weather_overlay = "rain_high"
end_message = span_bolddanger("The downpour gradually slows to a light shower.")
- end_overlay = "rain_low"
end_duration = 30 SECONDS
weather_duration_lower = 3 MINUTES
@@ -27,10 +27,20 @@
weather_flags = (WEATHER_TURFS | WEATHER_MOBS | WEATHER_THUNDER | WEATHER_BAROMETER)
whitelist_weather_reagents = list(/datum/reagent/water)
-/datum/weather/rain_storm/get_playlist_ref()
+/datum/weather/particle/rain_storm/New(z_levels, list/weather_data)
+ . = ..()
+ if (isnull(weather_reagent) || istype(weather_reagent, /datum/reagent/water) || !weather_color)
+ return
+
+ // Non-water rain gets colored into their reagent's color
+ for (var/list/holder_list as anything in weather_objects)
+ for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list)
+ holder.particles.color = weather_color
+
+/datum/weather/particle/rain_storm/get_playlist_ref()
return GLOB.rain_storm_sounds
-/datum/weather/rain_storm/telegraph()
+/datum/weather/particle/rain_storm/telegraph()
GLOB.rain_storm_sounds.Cut()
for(var/area/impacted_area as anything in impacted_areas)
GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/start
@@ -43,39 +53,53 @@
return ..()
-/datum/weather/rain_storm/start()
+/datum/weather/particle/rain_storm/start()
GLOB.rain_storm_sounds.Cut()
for(var/area/impacted_area as anything in impacted_areas)
GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/middle
return ..()
-/datum/weather/rain_storm/wind_down()
+/datum/weather/particle/rain_storm/wind_down()
GLOB.rain_storm_sounds.Cut()
for(var/area/impacted_area as anything in impacted_areas)
GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/end
return ..()
-/datum/weather/rain_storm/end()
+/datum/weather/particle/rain_storm/end()
GLOB.rain_storm_sounds.Cut()
return ..()
-/datum/weather/rain_storm/blood
+/particles/weather/rain_storm
+ icon = 'icons/effects/particles/generic.dmi'
+ icon_state = "drop"
+ color = "#ccffff"
+ position = generator(GEN_BOX, list(-510, -256, 0), list(400, 512, 0))
+ grow = list(-0.01, -0.01)
+ gravity = list(0, -10, 0.5)
+ drift = generator(GEN_CIRCLE, 0, 1)
+ friction = 0.3
+ min_spawn = 50
+ max_spawn = 300
+ wind_strength = 5
+ spin = 0
+
+/datum/weather/particle/rain_storm/blood
whitelist_weather_reagents = list(/datum/reagent/blood)
probability = 0 // admeme event
// Fun fact - if you increase the weather_temperature higher than LIQUID_PLASMA_BP
// the plasma rain will vaporize into a gas on whichever turf it lands on
-/datum/weather/rain_storm/plasma
+/datum/weather/particle/rain_storm/plasma
whitelist_weather_reagents = list(/datum/reagent/toxin/plasma)
probability = 0 // maybe for icebox maps one day?
-/datum/weather/rain_storm/deep_fried
+/datum/weather/particle/rain_storm/deep_fried
weather_temperature = 455 // just hot enough to apply the fried effect
whitelist_weather_reagents = list(/datum/reagent/consumable/nutriment/fat/oil)
weather_flags = (WEATHER_TURFS | WEATHER_INDOORS)
probability = 0 // admeme event
-/datum/weather/rain_storm/acid
+/datum/weather/particle/rain_storm/acid
desc = "The planet's thunderstorms are by nature acidic, and will incinerate anyone standing beneath them without protection."
telegraph_duration = 40 SECONDS
@@ -97,7 +121,7 @@
)
probability = 0
-/datum/weather/rain_storm/wizard
+/datum/weather/particle/rain_storm/wizard
name = "magical rain"
desc = "A magical thunderstorm rains down below, drenching anyone caught in it with mysterious rain."
@@ -116,7 +140,7 @@
probability = 0 // shouldn't spawn normally
weather_flags = (WEATHER_TURFS | WEATHER_MOBS | WEATHER_INDOORS | WEATHER_BAROMETER)
-/datum/weather/rain_storm/wizard/New(z_levels, list/weather_data)
+/datum/weather/particle/rain_storm/wizard/New(z_levels, list/weather_data)
if(length(GLOB.wizard_rain_reagents)) // the wizard event has already been run once and setup the whitelist
whitelist_weather_reagents = GLOB.wizard_rain_reagents
return ..()
diff --git a/code/datums/wires/big_manipulator.dm b/code/datums/wires/big_manipulator.dm
index 4d56f5470e18..9ad7dbeb3a2e 100644
--- a/code/datums/wires/big_manipulator.dm
+++ b/code/datums/wires/big_manipulator.dm
@@ -23,8 +23,7 @@
var/list/status = list()
status += "The big light bulb [holder_manipulator.power_access_wire_cut ? "is off" : "is glowing [holder_manipulator.on ? "green" : "red"]"]."
status += "The small light bulb [holder_manipulator.held_object ? "is glowing bright green" : "is off"]."
- status += "The green number on the display shows [length(holder_manipulator.pickup_points)]."
- status += "The red number on the display shows [length(holder_manipulator.dropoff_points)]."
+ status += "The number on the display shows [length(holder_manipulator.tasks)]."
return status
/datum/wires/big_manipulator/on_pulse(wire)
diff --git a/code/datums/wounds/_wounds.dm b/code/datums/wounds/_wounds.dm
index 1654670e5045..9b03a3283827 100644
--- a/code/datums/wounds/_wounds.dm
+++ b/code/datums/wounds/_wounds.dm
@@ -405,6 +405,9 @@
var/obj/item/bodypart/cached_limb = limb // remove_wound() nulls limb so we have to track it locally
remove_wound(replaced=TRUE)
new_wound.apply_wound(cached_limb, old_wound = src, smited = smited, attack_direction = attack_direction, wound_source = wound_source, replacing = TRUE)
+ if(HAS_TRAIT(src, TRAIT_WOUND_SCANNED) && severity > new_wound.severity)
+ for(var/trait_source in GET_TRAIT_SOURCES(src, TRAIT_WOUND_SCANNED))
+ ADD_TRAIT(new_wound, TRAIT_WOUND_SCANNED, trait_source)
. = new_wound
qdel(src)
diff --git a/code/datums/wounds/pierce.dm b/code/datums/wounds/pierce.dm
index 28a9286a7752..e13fd79e9353 100644
--- a/code/datums/wounds/pierce.dm
+++ b/code/datums/wounds/pierce.dm
@@ -169,7 +169,7 @@
playsound(user, 'sound/items/handling/surgery/cautery2.ogg', 75, TRUE)
- var/bleeding_wording = (!limb.can_bleed() ? "holes" : "bleeding")
+ var/bleeding_wording = (limb.can_bleed() ? "bleeding" : "holes")
user.visible_message(span_green("[user] cauterizes some of the [bleeding_wording] on [victim]."), span_green("You cauterize some of the [bleeding_wording] on [victim]."))
victim.apply_damage(2 + severity, BURN, limb, wound_bonus = CANT_WOUND)
if(prob(30))
diff --git a/code/datums/wounds/slash.dm b/code/datums/wounds/slash.dm
index 856a54f86314..e0ac7b30ddca 100644
--- a/code/datums/wounds/slash.dm
+++ b/code/datums/wounds/slash.dm
@@ -276,7 +276,7 @@
playsound(user, 'sound/items/handling/surgery/cautery2.ogg', 75, TRUE)
- var/bleeding_wording = (!limb.can_bleed() ? "cuts" : "bleeding")
+ var/bleeding_wording = (limb.can_bleed() ? "bleeding" : "cuts")
user.visible_message(span_green("[user] cauterizes some of the [bleeding_wording] on [victim]."), span_green("You cauterize some of the [bleeding_wording] on [victim]."))
victim.apply_damage(2 + severity, BURN, limb, wound_bonus = CANT_WOUND)
if(prob(30))
diff --git a/code/game/area/areas/ruins/icemoon.dm b/code/game/area/areas/ruins/icemoon.dm
index 80f8f6bb33ae..d44ab31c59b6 100644
--- a/code/game/area/areas/ruins/icemoon.dm
+++ b/code/game/area/areas/ruins/icemoon.dm
@@ -27,6 +27,10 @@
name = "\improper Listening Post"
sound_environment = SOUND_ENVIRONMENT_CITY
+/area/ruin/comms_agent/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/block_area_power_fail)
+
/area/ruin/comms_agent/maint
name = "\improper Listening Post Maintenance"
sound_environment = SOUND_AREA_TUNNEL_ENCLOSED
diff --git a/code/game/area/areas/station/ai.dm b/code/game/area/areas/station/ai.dm
index ff12a728f282..faa717845b3a 100644
--- a/code/game/area/areas/station/ai.dm
+++ b/code/game/area/areas/station/ai.dm
@@ -45,6 +45,10 @@
icon_state = "ai_chamber"
annoying_ambience = null
+/area/station/ai/satellite/chamber/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/block_area_power_fail)
+
/area/station/ai/satellite/exterior
name = "\improper AI Satellite Exterior"
secure = FALSE
diff --git a/code/game/area/areas/station/engineering.dm b/code/game/area/areas/station/engineering.dm
index 1193404c975c..3f0464453857 100644
--- a/code/game/area/areas/station/engineering.dm
+++ b/code/game/area/areas/station/engineering.dm
@@ -16,6 +16,10 @@
name = "Engineering"
icon_state = "engine"
+/area/station/engineering/main/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/block_area_power_fail)
+
/area/station/engineering/hallway
name = "Engineering Hallway"
icon_state = "engine_hallway"
@@ -69,16 +73,28 @@
icon_state = "atmos_engine"
area_flags = BLOBS_ALLOWED | CULT_PERMITTED
+/area/station/engineering/atmospherics_engine/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/block_area_power_fail)
+
/area/station/engineering/lobby
name = "\improper Engineering Lobby"
icon_state = "engi_lobby"
/area/station/engineering/supermatter
- name = "\improper Supermatter Engine"
+ name = "\improper Place Somewhere Around the Supermatter" // don't use this type
icon_state = "engine_sm"
area_flags = BLOBS_ALLOWED | CULT_PERMITTED
+
+/area/station/engineering/supermatter/engine
+ name = "\improper Supermatter Engine"
+ icon_state = "engine_sm"
sound_environment = SOUND_AREA_SMALL_ENCLOSED
+/area/station/engineering/supermatter/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/block_area_power_fail)
+
/area/station/engineering/supermatter/waste
name = "\improper Supermatter Waste Chamber"
icon_state = "engine_sm_waste"
diff --git a/code/game/area/areas/station/telecomm.dm b/code/game/area/areas/station/telecomm.dm
index 02101c28c1a9..adb4670b44b3 100644
--- a/code/game/area/areas/station/telecomm.dm
+++ b/code/game/area/areas/station/telecomm.dm
@@ -16,6 +16,9 @@
)
airlock_wires = /datum/wires/airlock/engineering
+/area/station/tcommsat/maints
+ name = "\improper Telecomms Maintenance Room"
+
/area/station/tcommsat/computer
name = "\improper Telecomms Control Room"
icon_state = "tcomsatcomp"
diff --git a/code/game/atom/_atom.dm b/code/game/atom/_atom.dm
index 76c5744c0f98..ad246f5ff825 100644
--- a/code/game/atom/_atom.dm
+++ b/code/game/atom/_atom.dm
@@ -192,15 +192,6 @@
if(smoothing_flags & SMOOTH_QUEUED)
SSicon_smooth.remove_from_queues(src)
-#ifndef DISABLE_DREAMLUAU
- // These lists cease existing when src does, so we need to clear any lua refs to them that exist.
- if(!(datum_flags & DF_STATIC_OBJECT))
- DREAMLUAU_CLEAR_REF_USERDATA(contents)
- DREAMLUAU_CLEAR_REF_USERDATA(filters)
- DREAMLUAU_CLEAR_REF_USERDATA(overlays)
- DREAMLUAU_CLEAR_REF_USERDATA(underlays)
-#endif
-
return ..()
/atom/proc/handle_ricochet(obj/projectile/ricocheting_projectile)
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 874575bb765f..bba7df382899 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -36,9 +36,13 @@
var/speech_span
///Are we moving with inertia? Mostly used as an optimization
var/inertia_moving = FALSE
- ///Multiplier for inertia based movement in space
- var/inertia_move_multiplier = 1
- ///Object "weight", higher weight reduces acceleration applied to the object
+ /// Multiplies speed the movable drifts when unaffected by gravity.
+ /// "Passive" is used for referring "base drift speed" - only the smaller of the two are used.
+ var/inertia_move_multiplier_passive = 1
+ /// Multiplies speed the movable drifts when unaffected by gravity.
+ /// "Active" is used for referring to things boosting our drift speed, like jetpacks - only the smaller of the two are used.
+ var/inertia_move_multiplier_active = 1
+ /// Object "weight", higher weight reduces acceleration applied to the object
var/inertia_force_weight = 1
///The last time we pushed off something
///This is a hack to get around dumb him him me scenarios
@@ -242,12 +246,6 @@
LAZYNULL(client_mobs_in_contents)
-#ifndef DISABLE_DREAMLUAU
- // These lists cease existing when src does, so we need to clear any lua refs to them that exist.
- DREAMLUAU_CLEAR_REF_USERDATA(vis_contents)
- DREAMLUAU_CLEAR_REF_USERDATA(vis_locs)
-#endif
-
. = ..()
for(var/movable_content in contents)
diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm
index 887a2cb4c7b9..f56633befce7 100644
--- a/code/game/data_huds.dm
+++ b/code/game/data_huds.dm
@@ -44,7 +44,7 @@
hud_icons = list(FAN_HUD)
/datum/atom_hud/data/diagnostic
- hud_icons = list(DIAG_HUD, DIAG_STAT_HUD, DIAG_BATT_HUD, DIAG_MECH_HUD, DIAG_BOT_HUD, DIAG_TRACK_HUD, DIAG_CAMERA_HUD, DIAG_AIRLOCK_HUD, DIAG_LAUNCHPAD_HUD, BIG_MANIP_HUD)
+ hud_icons = list(DIAG_HUD, DIAG_STAT_HUD, DIAG_BATT_HUD, DIAG_MECH_HUD, DIAG_BOT_HUD, DIAG_TRACK_HUD, DIAG_CAMERA_HUD, DIAG_AIRLOCK_HUD, DIAG_LAUNCHPAD_HUD)
/datum/atom_hud/data/bot_path
hud_icons = list(DIAG_PATH_HUD)
@@ -62,7 +62,7 @@
. = ..()
if(!new_viewer || hud_users_all_z_levels.len != 1)
return
- for(var/mob/eye/camera/ai/eye as anything in GLOB.camera_eyes)
+ for(var/mob/eye/camera/ai/eye in GLOB.camera_eyes)
eye.update_ai_detect_hud()
/datum/atom_hud/data/malf_apc
diff --git a/code/game/gamemodes/events.dm b/code/game/gamemodes/events.dm
index 171086b06b65..002ec4787a6e 100644
--- a/code/game/gamemodes/events.dm
+++ b/code/game/gamemodes/events.dm
@@ -23,7 +23,7 @@
continue
if(!station_area.requires_power || station_area.always_unpowered )
continue
- if(GLOB.typecache_powerfailure_safe_areas[station_area.type])
+ if(HAS_TRAIT(station_area, TRAIT_AREA_BLOCK_POWER_FAIL))
continue
station_area.power_light = FALSE
@@ -34,7 +34,7 @@
for(var/obj/machinery/power/apc/C as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/power/apc))
if(C.cell && is_station_level(C.z))
var/area/A = C.area
- if(GLOB.typecache_powerfailure_safe_areas[A.type])
+ if(HAS_TRAIT(A, TRAIT_AREA_BLOCK_POWER_FAIL))
continue
C.cell.charge = 0
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index d6d8ff1b6463..b4cfe34cf497 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -193,12 +193,15 @@
if(occupant_typecache)
occupant_typecache = typecacheof(occupant_typecache)
- if((resistance_flags & INDESTRUCTIBLE) && component_parts){ // This is needed to prevent indestructible machinery still blowing up. If an explosion occurs on the same tile as the indestructible machinery without the PREVENT_CONTENTS_EXPLOSION_1 flag, /datum/controller/subsystem/explosions/proc/propagate_blastwave will call ex_act on all movable atoms inside the machine, including the circuit board and component parts. However, if those parts get deleted, the entire machine gets deleted, allowing for INDESTRUCTIBLE machines to be destroyed. (See #62164 for more info)
+ if((resistance_flags & INDESTRUCTIBLE) && component_parts) // This is needed to prevent indestructible machinery still blowing up. If an explosion occurs on the same tile as the indestructible machinery without the PREVENT_CONTENTS_EXPLOSION_1 flag, /datum/controller/subsystem/explosions/proc/propagate_blastwave will call ex_act on all movable atoms inside the machine, including the circuit board and component parts. However, if those parts get deleted, the entire machine gets deleted, allowing for INDESTRUCTIBLE machines to be destroyed. (See #62164 for more info)
flags_1 |= PREVENT_CONTENTS_EXPLOSION_1
- }
+
+ if(critical_machine)
+ AddElement(/datum/element/block_area_power_fail)
if(HAS_TRAIT(SSstation, STATION_TRAIT_MACHINES_GLITCHED) && mapload)
randomize_language_if_on_station()
+
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NEW_MACHINE, src)
return INITIALIZE_HINT_LATELOAD
@@ -1323,8 +1326,18 @@
set_machine_stat(vval)
datum_flags |= DF_VAR_EDITED
return TRUE
+ if(vname == NAMEOF(src, critical_machine))
+ if(critical_machine == !!vval) // boolean cast in case a badmin tries to set it to 2 for some reason
+ return FALSE
+ critical_machine = !!vval
+ if(critical_machine)
+ AddElement(/datum/element/block_area_power_fail)
+ else
+ RemoveElement(/datum/element/block_area_power_fail)
+ datum_flags |= DF_VAR_EDITED
+ return TRUE
- return ..()
+ . = ..()
/**
* Alerts the AI that a hack is in progress.
diff --git a/code/game/machinery/big_manipulator/_defines.dm b/code/game/machinery/big_manipulator/_defines.dm
index 362831bd8e7b..b04d1b90bb60 100644
--- a/code/game/machinery/big_manipulator/_defines.dm
+++ b/code/game/machinery/big_manipulator/_defines.dm
@@ -18,32 +18,17 @@
#define MAX_SPEED_MULTIPLIER_TIER_3 5
#define MAX_SPEED_MULTIPLIER_TIER_4 6
-#define MAX_INTERACTION_POINTS_TIER_1 2
-#define MAX_INTERACTION_POINTS_TIER_2 3
-#define MAX_INTERACTION_POINTS_TIER_3 4
-#define MAX_INTERACTION_POINTS_TIER_4 6
+#define MAX_TASKS_TIER_1 6
+#define MAX_TASKS_TIER_2 12
+#define MAX_TASKS_TIER_3 24
+#define MAX_TASKS_TIER_4 32
-#define CURRENT_TASK_NONE "NO TASK" // manipulator is off
-#define CURRENT_TASK_IDLE "IDLE" // manipulator is skipping a cycle because it has nothing to do
-#define CURRENT_TASK_MOVING_PICKUP "MOVING TO PICKUP POINT"
-#define CURRENT_TASK_MOVING_DROPOFF "MOVING TO DROPOFF POINT"
-#define CURRENT_TASK_INTERACTING "INTERACTING"
-#define CURRENT_TASK_STOPPING "STOPPING"
// How should the worker interact with the point
#define WORKER_SINGLE_USE "SINGLE TIME"
#define WORKER_EMPTY_USE "EMPTY HAND"
#define WORKER_NORMAL_USE "NORMAL"
-// The tasking schedules the manipulator uses to iterate through points
-#define TASKING_ROUND_ROBIN "Round Robin" // 1 - 2 - 3 - 2 - 3
-#define TASKING_STRICT_ROBIN "Strict Robin" // 1 - 2 - 3 - (waiting for 1) - 1 - 2
-#define TASKING_PREFER_FIRST "Prefer First" // 1 - 2 - 1 - 2 - 3 - 2 - 1 - 3 (first availiable)
-
-// Defines if this point is a pickup or a dropoff point
-#define TRANSFER_TYPE_PICKUP "PICK UP"
-#define TRANSFER_TYPE_DROPOFF "DROP OFF"
-
#define BASE_POWER_USAGE 0.2
#define BASE_INTERACTION_TIME 0.3 SECONDS
@@ -62,6 +47,16 @@
#define POST_INTERACTION_DROP_NEXT_FITTING "AT ANY FITTING"
#define POST_INTERACTION_WAIT "CONTINUE"
-// Some macros for interaction checks
-#define IS_STOPPING (current_task == CURRENT_TASK_STOPPING)
-#define IS_BUSY (current_task != CURRENT_TASK_NONE)
+
+#define PICKUP_EAGER "Always Pick Up"
+#define PICKUP_CAN_WAIT "Wait For Suiting"
+
+#define TASK_TYPE_PICKUP "pickup"
+#define TASK_TYPE_DROP "drop"
+#define TASK_TYPE_THROW "throw"
+#define TASK_TYPE_USE "use"
+#define TASK_TYPE_INTERACT "interact"
+#define TASK_TYPE_WAIT "wait"
+
+#define TASKING_SEQUENTIAL "Sequential"
+#define TASKING_STRICT "Strict order"
diff --git a/code/game/machinery/big_manipulator/big_manipulator.dm b/code/game/machinery/big_manipulator/big_manipulator.dm
index 3a9e9d83d356..83ab74f9a034 100644
--- a/code/game/machinery/big_manipulator/big_manipulator.dm
+++ b/code/game/machinery/big_manipulator/big_manipulator.dm
@@ -8,7 +8,6 @@
circuit = /obj/item/circuitboard/machine/big_manipulator
greyscale_colors = "#d8ce13"
greyscale_config = /datum/greyscale_config/big_manipulator
- hud_possible = list(BIG_MANIP_HUD)
/// Is the manipulator turned on?
var/on = FALSE
@@ -17,13 +16,8 @@
/// How quickly the manipulator will process it's actions.
var/speed_multiplier = 1
- var/min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_1
- var/max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_1
-
- /// The current task.
- var/current_task = CURRENT_TASK_NONE
- var/current_task_start_time = 0
- var/current_task_duration = 0
+ var/min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_1
+ var/max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_1
/// The object inside the manipulator.
var/datum/weakref/held_object = null
@@ -31,112 +25,88 @@
var/datum/weakref/monkey_worker = null
/// Weakref to the ID that locked this manipulator.
var/datum/weakref/id_lock = null
+ /// Inserted manipulator task disk.
+ var/obj/item/disk/manipulator/task_disk = null
/// The manipulator's arm.
var/obj/effect/big_manipulator_arm/manipulator_arm = null
/// Is the power access wire cut? Disables the power button if `TRUE`.
var/power_access_wire_cut = FALSE
- /// How many interaction points of each kind can we have?
- var/interaction_point_limit = MAX_INTERACTION_POINTS_TIER_1
- /// List of pickup points.
- var/list/pickup_points = list()
- /// List of dropoff points.
- var/list/dropoff_points = list()
-
- /// Which tasking scenario we use for pickup points?
- var/pickup_tasking = TASKING_ROUND_ROBIN
- /// Which tasking scenario we use for dropoff points?
- var/dropoff_tasking = TASKING_ROUND_ROBIN
-
- /// List of all HUD icons resembling interaction points.
- var/list/hud_points = list()
+ /// How many tasks total we can have.
+ var/interaction_point_limit = MAX_TASKS_TIER_1
- /// Pickup strategy for tasking.
- var/datum/tasking_strategy/pickup_strategy
- /// Dropoff strategy for tasking.
- var/datum/tasking_strategy/dropoff_strategy
+ /// A list of tasks for the manipulator.
+ var/list/tasks = list()
+ /// The task we're currently working on.
+ var/datum/manipulator_task/current_task
-/// Re-creates hud images for the points
-/obj/machinery/big_manipulator/proc/update_hud()
- LAZYCLEARLIST(hud_points)
+ /// Is the manipulator in the process of stopping?
+ var/stopping = FALSE
+ /// Is the manipulator waiting for a turf signal to retry?
+ var/waiting_for_signal = FALSE
+ /// Turfs we registered enter/exit signals on while waiting.
+ var/list/signal_turfs = list()
- var/image/main_hud = hud_list[BIG_MANIP_HUD]
- if(!main_hud)
- return
+ /// Which tasking scenario we use for iterating tasks.
+ var/tasking_strategy = TASKING_SEQUENTIAL
+ /// Tasking strategy instance.
+ var/datum/tasking_strategy/master_tasking
- main_hud.loc = get_turf(src)
- main_hud.appearance = mutable_appearance('icons/effects/interaction_points.dmi', null, ABOVE_ALL_MOB_LAYER, src, GAME_PLANE)
-
- main_hud.overlays.Cut()
- var/list/point_overlays = list()
-
- for(var/i in 1 to length(pickup_points))
- var/datum/interaction_point/point = pickup_points[i]
- var/turf/target_turf = point.interaction_turf
- if(target_turf)
- var/mutable_appearance/point_appearance = mutable_appearance('icons/effects/interaction_points.dmi', "pickup_[i]", ABOVE_ALL_MOB_LAYER, src, GAME_PLANE)
- var/turf/manip_turf = get_turf(src)
- point_appearance.pixel_x = (target_turf.x - manip_turf.x) * 32
- point_appearance.pixel_y = (target_turf.y - manip_turf.y) * 32
- point_overlays += point_appearance
-
- for(var/i in 1 to length(dropoff_points))
- var/datum/interaction_point/point = dropoff_points[i]
- var/turf/target_turf = point.interaction_turf
- if(target_turf)
- var/mutable_appearance/point_appearance = mutable_appearance('icons/effects/interaction_points.dmi', "dropoff_[i]", ABOVE_ALL_MOB_LAYER, src, GAME_PLANE)
- var/turf/manip_turf = get_turf(src)
- point_appearance.pixel_x = (target_turf.x - manip_turf.x) * 32
- point_appearance.pixel_y = (target_turf.y - manip_turf.y) * 32
- point_overlays += point_appearance
-
- main_hud.overlays += point_overlays
- hud_points += main_hud
- set_hud_image_active(BIG_MANIP_HUD)
-
-/// Attempts to find a suitable turf near the manipulator
+/// Attempts to find a suitable turf near the manipulator for creating a cargo task.
/obj/machinery/big_manipulator/proc/find_suitable_turf()
- var/turf/center = get_turf(src)
-
- for(var/turf/checked_turf in orange(center, 1))
+ var/turf/base = get_turf(src)
+ for(var/turf/checked_turf in orange(base, 1))
if(!isclosedturf(checked_turf))
return checked_turf
-
- // didn't find any :boowomp:
return null
-/// Attempts to create a new interaction point and assign it to the correct list.
-/obj/machinery/big_manipulator/proc/create_new_interaction_point(mob/user, turf/new_turf, list/new_filters, new_filters_status, new_interaction_mode, transfer_type)
- if(!new_turf || !isturf(new_turf))
- new_turf = find_suitable_turf()
- if(!new_turf)
- balloon_alert(user, "no suitable turfs found!")
- return FALSE
-
- var/list/current_points = (transfer_type == TRANSFER_TYPE_PICKUP) ? pickup_points : dropoff_points
- var/point_type = (transfer_type == TRANSFER_TYPE_PICKUP) ? "pickup" : "dropoff"
-
- if(length(current_points) + 1 > interaction_point_limit)
- balloon_alert(user, "[point_type] point limit reached!")
+/// Attempts to create a new task and assign it to the list.
+/obj/machinery/big_manipulator/proc/create_new_task(mob/user, task_type, turf/new_turf)
+ if(length(tasks) >= interaction_point_limit)
+ balloon_alert(user, "task limit reached!")
return FALSE
var/datum/stock_part/servo/locate_servo = locate() in component_parts
var/manipulator_tier = locate_servo ? locate_servo.tier : 1
- var/datum/interaction_point/new_interaction_point = new(new_turf, new_filters, new_filters_status, new_interaction_mode, manipulator_tier)
+ var/datum/manipulator_task/new_task
+ var/needs_turf = task_type in list(TASK_TYPE_PICKUP, TASK_TYPE_DROP, TASK_TYPE_THROW, TASK_TYPE_USE, TASK_TYPE_INTERACT)
+
+ if(needs_turf)
+ if(!new_turf)
+ new_turf = find_suitable_turf()
+ if(!new_turf)
+ return FALSE
- if(QDELETED(new_interaction_point)) // if something STILL somehow went wrong
+ switch(task_type)
+ if(TASK_TYPE_PICKUP)
+ new_task = new /datum/manipulator_task/cargo/pickup(new_turf, manipulator_tier)
+ if(TASK_TYPE_DROP)
+ new_task = new /datum/manipulator_task/cargo/dropoff_base/drop(new_turf, manipulator_tier)
+ if(TASK_TYPE_THROW)
+ new_task = new /datum/manipulator_task/cargo/dropoff_base/throw(new_turf, manipulator_tier)
+ if(TASK_TYPE_USE)
+ new_task = new /datum/manipulator_task/cargo/dropoff_base/use(new_turf, manipulator_tier)
+ if(TASK_TYPE_INTERACT)
+ new_task = new /datum/manipulator_task/cargo/interact(new_turf, manipulator_tier)
+ if(TASK_TYPE_WAIT)
+ new_task = new /datum/manipulator_task/simple/wait()
+
+ if(QDELETED(new_task))
return FALSE
- current_points += new_interaction_point
+ tasks += new_task
- if(obj_flags & EMAGGED)
- new_interaction_point.type_filters += /mob/living
+ if(istype(new_task, /datum/manipulator_task/cargo))
+ var/datum/manipulator_task/cargo/cargo_task = new_task
+ cargo_task.offset_dx = new_turf.x - x
+ cargo_task.offset_dy = new_turf.y - y
- if(is_operational)
- update_hud()
+ if((obj_flags & EMAGGED) && istype(new_task, /datum/manipulator_task/cargo))
+ var/datum/manipulator_task/cargo/cargo_task = new_task
+ cargo_task.type_filters += /mob/living
- return new_interaction_point
+ return new_task
/obj/machinery/big_manipulator/Initialize(mapload)
. = ..()
@@ -147,11 +117,6 @@
set_wires(new /datum/wires/big_manipulator(src))
register_context()
- for(var/datum/atom_hud/data/diagnostic/diag_hud in GLOB.huds)
- diag_hud.add_atom_to_hud(src)
-
- prepare_huds()
- update_hud()
update_strategies()
/// Checks the component tiers, adjusting the properties of the manipulator.
@@ -165,35 +130,32 @@
if(-INFINITY to 1)
min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_1
max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_1
- interaction_point_limit = MAX_INTERACTION_POINTS_TIER_1
+ interaction_point_limit = MAX_TASKS_TIER_1
set_greyscale(COLOR_YELLOW)
manipulator_arm?.set_greyscale(COLOR_YELLOW)
if(2)
min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_2
max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_2
- interaction_point_limit = MAX_INTERACTION_POINTS_TIER_2
+ interaction_point_limit = MAX_TASKS_TIER_2
set_greyscale(COLOR_ORANGE)
manipulator_arm?.set_greyscale(COLOR_ORANGE)
if(3)
min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_3
max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_3
- interaction_point_limit = MAX_INTERACTION_POINTS_TIER_3
+ interaction_point_limit = MAX_TASKS_TIER_3
set_greyscale(COLOR_RED)
manipulator_arm?.set_greyscale(COLOR_RED)
if(4 to INFINITY)
min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_4
max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_4
- interaction_point_limit = MAX_INTERACTION_POINTS_TIER_4
+ interaction_point_limit = MAX_TASKS_TIER_4
set_greyscale(COLOR_PURPLE)
manipulator_arm?.set_greyscale(COLOR_PURPLE)
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * BASE_POWER_USAGE * manipulator_tier
- for(var/datum/interaction_point/each_point in pickup_points)
- each_point.interaction_priorities = each_point.fill_priority_list(manipulator_tier)
-
- for(var/datum/interaction_point/each_point in dropoff_points)
- each_point.interaction_priorities = each_point.fill_priority_list(manipulator_tier)
+ for(var/datum/manipulator_task/cargo/cargo_task in tasks)
+ cargo_task.interaction_priorities = cargo_task.fill_priority_list(manipulator_tier)
/obj/machinery/big_manipulator/examine(mob/user)
. = ..()
@@ -205,10 +167,15 @@
try_press_on(user)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+/obj/machinery/big_manipulator/click_alt(mob/user)
+ eject_task_disk(user)
+ return CLICK_ACTION_SUCCESS
+
/obj/machinery/big_manipulator/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
context[SCREENTIP_CONTEXT_RMB] = "Toggle"
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Eject disk"
if(isnull(held_item))
context[SCREENTIP_CONTEXT_LMB] = panel_open ? "Interact with wires" : "Open UI"
@@ -228,16 +195,23 @@
context[SCREENTIP_CONTEXT_LMB] = "Interact with wires"
return CONTEXTUAL_SCREENTIP_SET
-/obj/machinery/big_manipulator/Destroy(force)
- remove_all_huds()
- for(var/datum/atom_hud/data/diagnostic/diag_hud in GLOB.huds)
- diag_hud.remove_atom_from_hud(src)
+/obj/machinery/big_manipulator/atom_deconstruct(disassembled)
+ if(task_disk)
+ task_disk.forceMove(drop_location())
+ task_disk = null
+ unregister_task_turf_signals()
+ QDEL_NULL(manipulator_arm)
+ QDEL_LIST(tasks)
+ id_lock = null
+ return ..()
+/obj/machinery/big_manipulator/Destroy(force)
+ unregister_task_turf_signals()
+ QDEL_NULL(task_disk)
QDEL_NULL(manipulator_arm)
- // QDEL_NULL(monkey_worker.resolve())
- // QDEL_NULL(held_object.resolve())
+ QDEL_LIST(tasks)
id_lock = null
- . = ..()
+ return ..()
/obj/machinery/big_manipulator/Exited(atom/movable/gone, direction)
. = ..()
@@ -252,90 +226,13 @@
poor_monkey.remove_offsets(type)
monkey_worker = null
-/obj/machinery/big_manipulator/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
- . = ..()
-
- if(!old_loc || !isturf(old_loc))
- return
-
- var/turf/old_turf = old_loc
- var/turf/new_turf = get_turf(src)
- if(!new_turf || old_turf == new_turf)
- return
-
- var/dx = new_turf.x - old_turf.x
- var/dy = new_turf.y - old_turf.y
-
- if(dx == 0 && dy == 0) // if we rotated for instance
- return
-
- for(var/datum/interaction_point/point in pickup_points)
- update_point_position(point, dx, dy)
-
- for(var/datum/interaction_point/point in dropoff_points)
- update_point_position(point, dx, dy)
-
- if(is_operational)
- update_hud()
-/// Updates a single interaction point's position by the given offset
-/obj/machinery/big_manipulator/proc/update_point_position(datum/interaction_point/point, dx, dy)
- if(!point || !point.interaction_turf)
+/// Removes an invalid task from the list.
+/obj/machinery/big_manipulator/proc/remove_invalid_task(datum/manipulator_task/task)
+ if(!task)
return
-
- var/turf/old_turf = point.interaction_turf
- if(!old_turf)
- return
-
- var/turf/manipulator_turf = get_turf(src)
- if(!manipulator_turf)
- return
-
- var/turf/new_turf = locate(old_turf.x + dx, old_turf.y + dy, manipulator_turf.z)
-
- // if manipulator is not anchored, allow points to be anywhere (even in walls) to not mess up your stuff when moving it
- if(!anchored)
- if(new_turf)
- point.interaction_turf = new_turf
- return
-
- if(!new_turf || isclosedturf(new_turf))
- new_turf = find_suitable_turf_near(new_turf || old_turf)
- if(!new_turf)
- remove_invalid_point(point)
- return
-
- if(new_turf == old_turf)
- return
-
- point.interaction_turf = new_turf
-
-/// Finds a suitable turf near the given location
-/obj/machinery/big_manipulator/proc/find_suitable_turf_near(turf/center)
- if(!center)
- return null
-
- var/turf/manipulator_turf = get_turf(src)
- if(!manipulator_turf)
- return null
-
- for(var/turf/each in orange(1, src))
- if(!isclosedturf(each))
- return each
-
- return null
-
-/// Removes an invalid interaction point from the lists.
-/obj/machinery/big_manipulator/proc/remove_invalid_point(datum/interaction_point/point)
- if(!point)
- return
-
- pickup_points.Remove(point)
- dropoff_points.Remove(point)
-
- qdel(point)
- if(is_operational)
- update_hud()
+ tasks -= task
+ qdel(task)
/obj/machinery/big_manipulator/emag_act(mob/user, obj/item/card/emag/emag_card)
. = ..()
@@ -345,10 +242,8 @@
balloon_alert(user, "overloaded")
obj_flags |= EMAGGED
- for(var/datum/interaction_point/pickup_point in pickup_points)
- pickup_point.type_filters += /mob/living
- for(var/datum/interaction_point/dropoff_point in dropoff_points)
- dropoff_point.type_filters += /mob/living
+ for(var/datum/manipulator_task/cargo/cargo_task in tasks)
+ cargo_task.type_filters += /mob/living
return TRUE
@@ -358,7 +253,7 @@
return ITEM_INTERACT_SUCCESS
/obj/machinery/big_manipulator/can_be_unfasten_wrench(mob/user, silent)
- if(current_task != CURRENT_TASK_NONE || on)
+ if(on || stopping)
to_chat(user, span_warning("[src] is activated!"))
return FAILED_UNFASTEN
return ..()
@@ -366,11 +261,8 @@
/obj/machinery/big_manipulator/default_unfasten_wrench(mob/user, obj/item/wrench, time)
. = ..()
if(. == SUCCESSFUL_UNFASTEN)
- if(anchored) // on anchoring, validate all points and remove invalid ones
- validate_all_points()
- update_hud()
- else
- remove_all_huds()
+ if(anchored)
+ validate_all_tasks()
/obj/machinery/big_manipulator/screwdriver_act(mob/living/user, obj/item/tool)
return default_deconstruction_screwdriver(user, tool)
@@ -381,6 +273,21 @@
/obj/machinery/big_manipulator/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
if(user.combat_mode)
return NONE
+
+ if(istype(tool, /obj/item/disk/manipulator))
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return ITEM_INTERACT_BLOCKING
+ if(task_disk)
+ task_disk.forceMove(drop_location())
+ task_disk = null
+ if(!user.transferItemToLoc(tool, src))
+ return ITEM_INTERACT_BLOCKING
+ task_disk = tool
+ balloon_alert(user, "disk inserted")
+ SStgui.update_uis(src)
+ return ITEM_INTERACT_SUCCESS
+
if(!panel_open || !is_wire_tool(tool))
return NONE
wires.interact(user)
@@ -391,7 +298,7 @@
process_upgrades()
/obj/machinery/big_manipulator/mouse_drop_dragged(atom/drop_point, mob/user, src_location, over_location, params)
- if(current_task != CURRENT_TASK_NONE)
+ if(on || stopping)
balloon_alert(user, "turn it off first!")
return
@@ -409,7 +316,7 @@
poor_monkey.forceMove(drop_point)
/obj/machinery/big_manipulator/mouse_drop_receive(atom/monkey, mob/user, params)
- if(current_task != CURRENT_TASK_NONE)
+ if(on || stopping)
balloon_alert(user, "turn it off first!")
return
@@ -470,8 +377,6 @@
if(!user)
on = newly_on
- if(!on)
- remove_all_huds()
return
if(newly_on)
@@ -483,7 +388,7 @@
balloon_alert(user, "anchor first!")
return
- validate_all_points()
+ validate_all_tasks()
on = newly_on
SStgui.update_uis(src)
@@ -493,28 +398,21 @@
drop_held_atom()
on = newly_on
next_cycle_scheduled = FALSE
- // Set stopping task instead of ending current task immediately
- if(current_task != CURRENT_TASK_NONE && current_task != CURRENT_TASK_STOPPING)
- start_task(CURRENT_TASK_STOPPING, 0)
- // Schedule automatic completion of stopping task
+ if(current_task != null && !stopping)
+ stopping = TRUE
addtimer(CALLBACK(src, PROC_REF(complete_stopping_task)), 1 SECONDS)
else
- end_current_task()
+ stopping = FALSE
+ unregister_task_turf_signals()
+ waiting_for_signal = FALSE
SStgui.update_uis(src)
-/obj/machinery/big_manipulator/proc/validate_all_points()
- for(var/datum/interaction_point/point in pickup_points)
- if(!point.is_valid())
- pickup_points -= point
- qdel(point)
-
- for(var/datum/interaction_point/point in dropoff_points)
- if(!point.is_valid())
- dropoff_points -= point
- qdel(point)
-
- if(is_operational)
- update_hud()
+/// Validates all cargo tasks, removing those on closed turfs.
+/obj/machinery/big_manipulator/proc/validate_all_tasks()
+ for(var/datum/manipulator_task/cargo/cargo_task in tasks)
+ if(!cargo_task.is_valid())
+ tasks -= cargo_task
+ qdel(cargo_task)
/// Attempts to press the power button.
/obj/machinery/big_manipulator/proc/try_press_on(mob/living/carbon/human/user)
@@ -522,8 +420,7 @@
balloon_alert(user, "unresponsive!")
return
- // Reject activation during stopping task
- if(current_task == CURRENT_TASK_STOPPING)
+ if(stopping)
balloon_alert(user, "stopping in progress!")
return
@@ -533,14 +430,6 @@
else
balloon_alert(user, "deactivated")
-/// Drop the held atom.
-/obj/machinery/big_manipulator/proc/drop_held_atom()
- if(isnull(held_object))
- return
- var/obj/obj_resolve = held_object?.resolve()
- obj_resolve?.forceMove(get_turf(obj_resolve))
- finish_manipulation(TRANSFER_TYPE_DROPOFF) // MCBALAAM TODO
-
/obj/machinery/big_manipulator/ui_interact(mob/user, datum/tgui/ui)
if(id_lock)
to_chat(user, span_warning("[src] is locked behind ID authentication!"))
@@ -558,78 +447,105 @@
/obj/machinery/big_manipulator/ui_data(mob/user)
var/list/data = list()
data["active"] = on
- data["current_task"] = current_task
- data["current_task_duration"] = current_task_duration
+ data["stopping"] = stopping
+ data["current_task"] = current_task ? REF(current_task) : null
data["speed_multiplier"] = speed_multiplier
data["min_speed_multiplier"] = min_speed_multiplier
data["max_speed_multiplier"] = max_speed_multiplier
data["manipulator_position"] = "[x],[y]"
- data["pickup_tasking"] = pickup_tasking
- data["dropoff_tasking"] = dropoff_tasking
-
- var/list/pickup_points_data = list()
- for(var/datum/interaction_point/point in pickup_points)
- var/list/point_data = list()
- point_data["name"] = point.name
- point_data["id"] = REF(point)
- var/turf/resolved_turf = point.interaction_turf
- point_data["turf"] = resolved_turf ? "[resolved_turf.x],[resolved_turf.y]" : "0,0"
- point_data["mode"] = "PICK"
- var/list/filter_names = list()
- for(var/obj/item/some_path as anything in point.atom_filters)
- filter_names += some_path::name
- point_data["item_filters"] = filter_names
- point_data["filters_status"] = point.should_use_filters
- point_data["filtering_mode"] = point.filtering_mode
- point_data["worker_interaction"] = point.worker_interaction
- point_data["overflow_status"] = point.overflow_status
- point_data["worker_use_rmb"] = point.worker_use_rmb
- point_data["worker_combat_mode"] = point.worker_combat_mode
- point_data["throw_range"] = point.throw_range
-
- var/list/settings_list_pick = list()
- for(var/datum/manipulator_priority/pr_pick in point.interaction_priorities)
- var/list/entry_pick = list()
- entry_pick["name"] = pr_pick.name
- entry_pick["active"] = pr_pick.active
- settings_list_pick += list(entry_pick)
- point_data["settings_list"] = settings_list_pick
- pickup_points_data += list(point_data)
- data["pickup_points"] = pickup_points_data
-
- var/list/dropoff_points_data = list()
- for(var/datum/interaction_point/point in dropoff_points)
- var/list/point_data = list()
- point_data["name"] = point.name
- point_data["id"] = REF(point)
- var/turf/resolved_turf = point.interaction_turf
- point_data["turf"] = resolved_turf ? "[resolved_turf.x],[resolved_turf.y]" : "0,0"
- point_data["mode"] = point.interaction_mode
- var/list/filter_names = list()
- for(var/obj/item/some_path as anything in point.atom_filters)
- filter_names += some_path::name
- point_data["item_filters"] = filter_names
- point_data["filters_status"] = point.should_use_filters
- point_data["filtering_mode"] = point.filtering_mode
- point_data["worker_interaction"] = point.worker_interaction
- point_data["overflow_status"] = point.overflow_status
- point_data["worker_use_rmb"] = point.worker_use_rmb
- point_data["worker_combat_mode"] = point.worker_combat_mode
- point_data["throw_range"] = point.throw_range
- point_data["use_post_interaction"] = point.use_post_interaction
-
- var/list/settings_list_drop = list()
- for(var/datum/manipulator_priority/pr_drop in point.interaction_priorities)
- var/list/entry_drop = list()
- entry_drop["name"] = pr_drop.name
- entry_drop["active"] = pr_drop.active
- settings_list_drop += list(entry_drop)
- point_data["settings_list"] = settings_list_drop
- dropoff_points_data += list(point_data)
- data["dropoff_points"] = dropoff_points_data
-
+ data["tasking_strategy"] = tasking_strategy
+ data["has_monkey"] = !isnull(monkey_worker?.resolve())
+ data["disk_inserted"] = !isnull(task_disk)
+ data["disk_read_only"] = task_disk?.read_only
+ data["disk_task_count"] = length(task_disk?.tasks_data)
+
+ var/list/tasks_data = list()
+ for(var/datum/manipulator_task/task in tasks)
+ var/list/td = list()
+ td["name"] = task.name
+ td["id"] = REF(task)
+
+ if(istype(task, /datum/manipulator_task/cargo/pickup))
+ td["task_type"] = TASK_TYPE_PICKUP
+ var/datum/manipulator_task/cargo/pickup/t = task
+ td["turf"] = "[t.offset_dx],[t.offset_dy]"
+ td["filters_status"] = t.should_use_filters
+ td["filtering_mode"] = t.filtering_mode
+ td["item_filters"] = _collect_filter_names(t.atom_filters)
+ td["settings_list"] = _collect_priorities(t.interaction_priorities)
+ td["pickup_eagerness"] = t.pickup_eagerness
+
+ else if(istype(task, /datum/manipulator_task/cargo/dropoff_base/drop))
+ td["task_type"] = TASK_TYPE_DROP
+ var/datum/manipulator_task/cargo/dropoff_base/drop/t = task
+ td["turf"] = "[t.offset_dx],[t.offset_dy]"
+ td["filters_status"] = t.should_use_filters
+ td["filtering_mode"] = t.filtering_mode
+ td["item_filters"] = _collect_filter_names(t.atom_filters)
+ td["settings_list"] = _collect_priorities(t.interaction_priorities)
+ td["overflow_status"] = t.overflow_status
+
+ else if(istype(task, /datum/manipulator_task/cargo/dropoff_base/throw))
+ td["task_type"] = TASK_TYPE_THROW
+ var/datum/manipulator_task/cargo/dropoff_base/throw/t = task
+ td["turf"] = "[t.offset_dx],[t.offset_dy]"
+ td["filters_status"] = t.should_use_filters
+ td["filtering_mode"] = t.filtering_mode
+ td["item_filters"] = _collect_filter_names(t.atom_filters)
+ td["settings_list"] = _collect_priorities(t.interaction_priorities)
+ td["throw_range"] = t.throw_range
+
+ else if(istype(task, /datum/manipulator_task/cargo/dropoff_base/use))
+ td["task_type"] = TASK_TYPE_USE
+ var/datum/manipulator_task/cargo/dropoff_base/use/t = task
+ td["turf"] = "[t.offset_dx],[t.offset_dy]"
+ td["filters_status"] = t.should_use_filters
+ td["filtering_mode"] = t.filtering_mode
+ td["item_filters"] = _collect_filter_names(t.atom_filters)
+ td["settings_list"] = _collect_priorities(t.interaction_priorities)
+ td["worker_interaction"] = t.worker_interaction
+ td["use_post_interaction"] = t.use_post_interaction
+ td["worker_use_rmb"] = t.worker_use_rmb
+ td["worker_combat_mode"] = t.worker_combat_mode
+
+ else if(istype(task, /datum/manipulator_task/cargo/interact))
+ td["task_type"] = TASK_TYPE_INTERACT
+ var/datum/manipulator_task/cargo/interact/t = task
+ td["turf"] = "[t.offset_dx],[t.offset_dy]"
+ td["filters_status"] = t.should_use_filters
+ td["filtering_mode"] = t.filtering_mode
+ td["item_filters"] = _collect_filter_names(t.atom_filters)
+ td["settings_list"] = _collect_priorities(t.interaction_priorities)
+ td["worker_interaction"] = t.worker_interaction
+ td["use_post_interaction"] = t.use_post_interaction
+ td["worker_use_rmb"] = t.worker_use_rmb
+ td["worker_combat_mode"] = t.worker_combat_mode
+
+ else if(istype(task, /datum/manipulator_task/simple/wait))
+ td["task_type"] = TASK_TYPE_WAIT
+ var/datum/manipulator_task/simple/wait/t = task
+ td["time"] = t.time_seconds
+
+ tasks_data += list(td)
+
+ data["tasks_data"] = tasks_data
return data
+/obj/machinery/big_manipulator/proc/_collect_filter_names(list/filters)
+ var/list/names = list()
+ for(var/atom/f as anything in filters)
+ names += initial(f.name)
+ return names
+
+/obj/machinery/big_manipulator/proc/_collect_priorities(list/priorities)
+ var/list/out = list()
+ for(var/datum/manipulator_priority/pr in priorities)
+ var/list/entry = list()
+ entry["name"] = pr.name
+ entry["active"] = pr.active
+ out += list(entry)
+ return out
+
/obj/machinery/big_manipulator/ui_act(action, params, datum/tgui/ui)
. = ..()
if(.)
@@ -644,41 +560,29 @@
drop_held_atom()
return TRUE
- if("create_pickup_point")
- create_new_interaction_point(ui.user, null, list(), FALSE, null, TRANSFER_TYPE_PICKUP)
+ if("create_task")
+ create_new_task(ui.user, params["task_type"])
+ maybe_wake()
return TRUE
- if("create_dropoff_point")
- create_new_interaction_point(ui.user, null, list(), FALSE, INTERACT_DROP, TRANSFER_TYPE_DROPOFF)
+ if("reset_tasking_index")
+ master_tasking.current_index = 1
+ balloon_alert(ui.user, "tasking index reset")
+ maybe_wake()
return TRUE
- if("reset_tasking_Pickup Points")
- pickup_strategy.current_index = 1
- balloon_alert(ui.user, "index reset")
- return TRUE
-
- if("reset_tasking_Dropoff Points")
- dropoff_strategy.current_index = 1
- balloon_alert(ui.user, "index reset")
- return TRUE
-
- if("cycle_tasking_schedule")
- var/new_schedule = params["new_schedule"]
-
- if(new_schedule in list(TASKING_ROUND_ROBIN, TASKING_STRICT_ROBIN, TASKING_PREFER_FIRST))
- var/is_pickup = params["is_pickup"]
- if(is_pickup)
- pickup_tasking = new_schedule
- else
- dropoff_tasking = new_schedule
+ if("cycle_tasking_strategy")
+ var/new_strategy = params["new_strategy"]
+ if(new_strategy in list(TASKING_SEQUENTIAL, TASKING_STRICT))
+ tasking_strategy = new_strategy
update_strategies()
+ maybe_wake()
return TRUE
if("adjust_interaction_speed")
var/new_speed = text2num(params["new_speed"])
if(isnull(new_speed))
return FALSE
-
speed_multiplier = clamp(new_speed, min_speed_multiplier, max_speed_multiplier)
return TRUE
@@ -687,163 +591,331 @@
monkey_worker = null
return TRUE
- if("adjust_point_param")
- return adjust_param_for_point(params["pointId"], params["param"], params["value"], ui.user)
+ if("adjust_task_param")
+ var/success = adjust_param_for_task(params["taskId"], params["param"], params["value"], ui.user)
+ if(success)
+ maybe_wake()
+ return success
+
+ if("disk_eject")
+ return eject_task_disk(ui.user)
+
+ if("disk_read")
+ if(read_disk_tasks(ui.user))
+ maybe_wake()
+ return TRUE
+
+ if("disk_write")
+ return write_disk_tasks(ui.user)
+
+ if("disk_clear")
+ return clear_disk_tasks(ui.user)
+
-/obj/machinery/big_manipulator/proc/adjust_param_for_point(point_ref, param, value, mob/user)
- if(!param) // there may be no value if we're resetting stuff
+/obj/machinery/big_manipulator/proc/eject_task_disk(mob/user)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return FALSE
+ if(!task_disk)
return FALSE
+ var/obj/item/disk/manipulator/ejectable_disk = task_disk
+ task_disk = null
+ if(istype(user) && user.put_in_hands(ejectable_disk))
+ balloon_alert(user, "disk ejected")
+ else
+ ejectable_disk.forceMove(drop_location())
+ balloon_alert(user, "disk dropped")
+ SStgui.update_uis(src)
+ return TRUE
- var/datum/interaction_point/target_point = locate(point_ref) in (pickup_points + dropoff_points)
- if(!target_point)
+/obj/machinery/big_manipulator/proc/clear_disk_tasks(mob/user)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
return FALSE
+ if(!task_disk)
+ return FALSE
+ if(task_disk.read_only)
+ balloon_alert(user, "disk protected")
+ return FALSE
+ task_disk.set_tasks(list())
+ balloon_alert(user, "cleared")
+ SStgui.update_uis(src)
+ return TRUE
- switch(param)
- if("set_name")
- target_point.name = sanitize_name(value, allow_numbers = TRUE)
- return TRUE
+/obj/machinery/big_manipulator/proc/write_disk_tasks(mob/user)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return FALSE
+ if(!task_disk)
+ return FALSE
+ if(task_disk.read_only)
+ balloon_alert(user, "disk protected")
+ return FALSE
- if("toggle_priority")
- return target_point.tick_priority_by_index(value)
+ var/list/out = list()
+ for(var/datum/manipulator_task/task as anything in tasks)
+ out += list(task.serialize())
- if("remove_point")
- pickup_points.Remove(target_point)
- dropoff_points.Remove(target_point) // one'll hit for sure
- update_hud()
- qdel(target_point)
- return TRUE
+ task_disk.set_tasks(out)
+ balloon_alert(user, "written")
+ SStgui.update_uis(src)
+ return TRUE
- if("reset_atom_filters")
- target_point.atom_filters = list()
+/obj/machinery/big_manipulator/proc/read_disk_tasks(mob/user)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return FALSE
+ if(!task_disk)
+ return FALSE
+
+ QDEL_LIST(tasks)
+ tasks = list()
+ current_task = null
+
+ var/turf/base = get_turf(src)
+ var/datum/stock_part/servo/locate_servo = locate() in component_parts
+ var/manipulator_tier = locate_servo ? locate_servo.tier : 1
+
+ for(var/list/task_data as anything in task_disk.tasks_data)
+ if(length(tasks) >= interaction_point_limit)
+ break
+ if(!islist(task_data))
+ continue
+ var/task_type = task_data["type"]
+ if(!ispath(task_type, /datum/manipulator_task))
+ continue
+ var/datum/manipulator_task/new_task
+ if(ispath(task_type, /datum/manipulator_task/cargo))
+ if(!base)
+ continue
+ var/list/offset = task_data["offset"]
+ if(!islist(offset))
+ continue
+ var/dx = offset["dx"]
+ var/dy = offset["dy"]
+ if(!isnum(dx) || !isnum(dy))
+ continue
+ if(dx < -1 || dx > 1 || dy < -1 || dy > 1)
+ continue
+ if(dx == 0 && dy == 0)
+ continue
+ var/turf/target_turf = locate(base.x + dx, base.y + dy, base.z)
+ if(!target_turf || isclosedturf(target_turf))
+ continue
+ new_task = new task_type(target_turf, manipulator_tier, serialized_data = task_data)
+ if(istype(new_task, /datum/manipulator_task/cargo))
+ var/datum/manipulator_task/cargo/c = new_task
+ c.offset_dx = dx
+ c.offset_dy = dy
+ else
+ new_task = new task_type(serialized_data = task_data)
+ if(!new_task || QDELETED(new_task))
+ continue
+ tasks += new_task
+
+ process_upgrades()
+ validate_all_tasks()
+ balloon_alert(user, "loaded")
+ SStgui.update_uis(src)
+ return TRUE
+
+/obj/machinery/big_manipulator/proc/adjust_param_for_task(task_ref, param, value, mob/user)
+ if(!param)
+ return FALSE
+
+ var/datum/manipulator_task/target_task = locate(task_ref) in tasks
+ if(!target_task)
+ return FALSE
+
+ switch(param)
+ if("set_name")
+ if(!value)
+ return FALSE
+ target_task.name = sanitize_name(value, allow_numbers = TRUE)
return TRUE
- if("cycle_dropoff_point_interaction")
- target_point.interaction_mode = cycle_value(target_point.interaction_mode, monkey_worker ? list(INTERACT_DROP, INTERACT_THROW, INTERACT_USE) : list(INTERACT_DROP, INTERACT_THROW))
- var/datum/stock_part/servo/locate_servo = locate() in component_parts
- var/manipulator_tier = locate_servo ? locate_servo.tier : 1
- target_point.interaction_priorities = target_point.fill_priority_list(manipulator_tier)
+ if("set_wait_time")
+ if(!istype(target_task, /datum/manipulator_task/simple/wait))
+ return FALSE
+ var/datum/manipulator_task/simple/wait/t = target_task
+ t.time_seconds = clamp(text2num(value), 1, 60)
return TRUE
- if("toggle_filter_skip")
- target_point.should_use_filters = !target_point.should_use_filters
+ if("remove_task")
+ tasks.Remove(target_task)
+ qdel(target_task)
return TRUE
- if("cycle_pickup_point_type")
- target_point.filtering_mode = cycle_value(target_point.filtering_mode, obj_flags & EMAGGED ? list(TAKE_ITEMS, TAKE_CLOSETS, TAKE_HUMANS) : list(TAKE_ITEMS, TAKE_CLOSETS))
+ if("move_up")
+ var/idx = tasks.Find(target_task)
+ if(idx <= 1)
+ return FALSE
+ tasks.Swap(idx, idx - 1)
return TRUE
- if("cycle_worker_interaction")
- target_point.worker_interaction = cycle_value(target_point.worker_interaction, list(WORKER_NORMAL_USE, WORKER_SINGLE_USE, WORKER_EMPTY_USE))
+ if("move_down")
+ var/idx = tasks.Find(target_task)
+ if(idx >= length(tasks))
+ return FALSE
+ tasks.Swap(idx, idx + 1)
return TRUE
- if("cycle_overflow_status")
- target_point.overflow_status = cycle_value(target_point.overflow_status, list(POINT_OVERFLOW_ALLOWED, POINT_OVERFLOW_FILTERS, POINT_OVERFLOW_HELD, POINT_OVERFLOW_FORBIDDEN))
+ if("move_to")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/cargo_task = target_task
+ var/button_number = text2num(value["buttonNumber"])
+ if(button_number < 1 || button_number > 9)
+ return
+ var/dx = ((button_number - 1) % 3) - 1
+ var/dy = 1 - round((button_number - 1) / 3)
+ var/turf/new_turf = locate(x + dx, y + dy, z)
+ if(!new_turf || isclosedturf(new_turf))
+ return FALSE
+ cargo_task.interaction_turf = new_turf
+ cargo_task.offset_dx = dx
+ cargo_task.offset_dy = dy
return TRUE
- if("cycle_throw_range")
- target_point.throw_range = cycle_value(target_point.throw_range, list(1, 2, 3, 4, 5, 6, 7))
+ if("toggle_filter_skip")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
+ ct.should_use_filters = !ct.should_use_filters
return TRUE
- if("cycle_post_interaction")
- target_point.use_post_interaction = cycle_value(target_point.use_post_interaction, list(POST_INTERACTION_DROP_AT_POINT, POST_INTERACTION_DROP_AT_MACHINE, POST_INTERACTION_DROP_NEXT_FITTING, POST_INTERACTION_WAIT))
+ if("reset_atom_filters")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
+ ct.atom_filters = list()
return TRUE
if("add_atom_filter_from_held")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
var/obj/item/held_item = user.get_active_held_item()
-
if(!held_item)
return FALSE
-
- for(var/filter_path in target_point.atom_filters)
+ for(var/filter_path in ct.atom_filters)
if(istype(held_item, filter_path))
return FALSE
-
- target_point.atom_filters += held_item.type
+ ct.atom_filters += held_item.type
return TRUE
if("delete_filter")
- target_point.atom_filters.Cut(value, value + 1)
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
+ ct.atom_filters.Cut(value, value + 1)
return TRUE
- if("toggle_worker_rmb")
- target_point.worker_use_rmb = !target_point.worker_use_rmb
+ if("cycle_filtering_mode")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
+ ct.filtering_mode = cycle_value(ct.filtering_mode, obj_flags & EMAGGED ? list(TAKE_ITEMS, TAKE_CLOSETS, TAKE_HUMANS) : list(TAKE_ITEMS, TAKE_CLOSETS))
return TRUE
- if("toggle_worker_combat")
- target_point.worker_combat_mode = !target_point.worker_combat_mode
- return TRUE
+ if("toggle_priority")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/current_task = target_task
+ return current_task.tick_priority_by_index(value)
if("priority_move_up")
- return target_point.move_priority_up_by_index(value)
-
- if("move_to")
- var/button_number = text2num(value["buttonNumber"])
-
- var/dx = ((button_number - 1) % 3) - 1
- var/dy = 1 - round((button_number - 1) / 3)
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/current_task = target_task
+ return current_task.move_priority_up_by_index(value)
- var/turf/new_turf = locate(x + dx, y + dy, z)
- if(!new_turf || isclosedturf(new_turf))
+ if("cycle_pickup_eagerness")
+ if(!istype(target_task, /datum/manipulator_task/cargo/pickup))
return FALSE
+ var/datum/manipulator_task/cargo/pickup/cycle_target_task = target_task
+ cycle_target_task.pickup_eagerness = cycle_value(cycle_target_task.pickup_eagerness, list(PICKUP_CAN_WAIT, PICKUP_EAGER))
+ return TRUE
- target_point.interaction_turf = new_turf
- update_hud()
+ if("cycle_overflow_status")
+ if(!istype(target_task, /datum/manipulator_task/cargo/dropoff_base/drop))
+ return FALSE
+ var/datum/manipulator_task/cargo/dropoff_base/drop/cycle_target_task = target_task
+ cycle_target_task.overflow_status = cycle_value(cycle_target_task.overflow_status, list(POINT_OVERFLOW_ALLOWED, POINT_OVERFLOW_FILTERS, POINT_OVERFLOW_HELD, POINT_OVERFLOW_FORBIDDEN))
return TRUE
-/// Cycles the given value in the given list. Retuns the next value in the list, or the first one if the list isn't long enough.
-/obj/machinery/big_manipulator/proc/cycle_value(current_value, list/possible_values)
- var/current_index = possible_values.Find(current_value)
- if(current_index == 0)
- return possible_values[1]
+ if("cycle_throw_range")
+ if(!istype(target_task, /datum/manipulator_task/cargo/dropoff_base/throw))
+ return FALSE
+ var/datum/manipulator_task/cargo/dropoff_base/throw/cycle_target_task = target_task
+ cycle_target_task.throw_range = cycle_value(cycle_target_task.throw_range, list(1, 2, 3, 4, 5, 6, 7))
+ return TRUE
- var/next_index = (current_index % length(possible_values)) + 1
- return possible_values[next_index]
+ if("cycle_worker_interaction")
+ var/list/vals = list(WORKER_NORMAL_USE, WORKER_SINGLE_USE, WORKER_EMPTY_USE)
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/cycle_target_task = target_task
+ cycle_target_task.worker_interaction = cycle_value(cycle_target_task.worker_interaction, vals)
+ return TRUE
+ if(istype(target_task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/cycle_target_task = target_task
+ cycle_target_task.worker_interaction = cycle_value(cycle_target_task.worker_interaction, vals)
+ return TRUE
+ return FALSE
-/// Begins a new task with the specified type and duration
-/obj/machinery/big_manipulator/proc/start_task(task_type, duration)
- if(current_task == CURRENT_TASK_STOPPING)
- return
+ if("cycle_post_interaction")
+ var/list/vals = list(POST_INTERACTION_DROP_AT_POINT, POST_INTERACTION_DROP_AT_MACHINE, POST_INTERACTION_DROP_NEXT_FITTING, POST_INTERACTION_WAIT)
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/cycle_target_task = target_task
+ cycle_target_task.use_post_interaction = cycle_value(cycle_target_task.use_post_interaction, vals)
+ return TRUE
+ if(istype(target_task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/cycle_target_task = target_task
+ cycle_target_task.use_post_interaction = cycle_value(cycle_target_task.use_post_interaction, vals)
+ return TRUE
+ return FALSE
- end_current_task() // ends any previous task first (momentarily sets IDLE)
- current_task_start_time = world.time
- current_task_duration = duration
- current_task = task_type
- SStgui.update_uis(src)
+ if("toggle_worker_rmb")
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/cycle_target_task = target_task
+ cycle_target_task.worker_use_rmb = !cycle_target_task.worker_use_rmb
+ return TRUE
+ if(istype(target_task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/cycle_target_task = target_task
+ cycle_target_task.worker_use_rmb = !cycle_target_task.worker_use_rmb
+ return TRUE
+ return FALSE
-/// Ends the current task
-/obj/machinery/big_manipulator/proc/end_current_task()
- current_task_start_time = 0
- current_task_duration = 0
- if(current_task == CURRENT_TASK_STOPPING)
- current_task = CURRENT_TASK_NONE
- SStgui.update_uis(src) // Update UI immediately
-
-/// Completes the stopping task and transitions to TASK_NONE
-/obj/machinery/big_manipulator/proc/complete_stopping_task()
- on = FALSE
- next_cycle_scheduled = FALSE
- end_current_task()
- SStgui.update_uis(src)
+ if("toggle_worker_combat")
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/cycle_target_task = target_task
+ cycle_target_task.worker_combat_mode = !cycle_target_task.worker_combat_mode
+ return TRUE
+ if(istype(target_task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/cycle_target_task = target_task
+ cycle_target_task.worker_combat_mode = !cycle_target_task.worker_combat_mode
+ return TRUE
+ return FALSE
-/obj/machinery/big_manipulator/proc/remove_all_huds()
- var/image/main_hud = hud_list[BIG_MANIP_HUD]
- if(main_hud)
- main_hud.overlays.Cut()
- main_hud.loc = null
+/// Cycles the given value in the given list.
+/obj/machinery/big_manipulator/proc/cycle_value(current_value, list/possible_values)
+ var/current_index = possible_values.Find(current_value)
+ if(current_index == 0)
+ return possible_values[1]
+ return possible_values[(current_index % length(possible_values)) + 1]
- hud_points.Cut()
- set_hud_image_inactive(BIG_MANIP_HUD)
+/// Retries the task loop if we're waiting for a signal and the machine is on.
+/obj/machinery/big_manipulator/proc/maybe_wake()
+ if(on && !stopping && waiting_for_signal)
+ something_happened()
/obj/machinery/big_manipulator/proc/update_strategies()
- pickup_strategy = create_strategy(pickup_tasking)
- dropoff_strategy = create_strategy(dropoff_tasking)
-
-/obj/machinery/big_manipulator/proc/create_strategy(tasking_mode)
- switch(tasking_mode)
- if(TASKING_PREFER_FIRST)
- return new /datum/tasking_strategy/prefer_first()
- if(TASKING_ROUND_ROBIN)
- return new /datum/tasking_strategy/round_robin()
- if(TASKING_STRICT_ROBIN)
- return new /datum/tasking_strategy/strict_robin()
- return new /datum/tasking_strategy/prefer_first()
+ master_tasking = create_strategy(tasking_strategy)
+
+/obj/machinery/big_manipulator/proc/create_strategy(strategy_mode)
+ switch(strategy_mode)
+ if(TASKING_SEQUENTIAL)
+ return new /datum/tasking_strategy/sequential()
+ if(TASKING_STRICT)
+ return new /datum/tasking_strategy/strict()
+ return new /datum/tasking_strategy/sequential()
diff --git a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
index 900f659c5ac5..01500efdd5aa 100644
--- a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
+++ b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
@@ -1,55 +1,42 @@
-/// Selects which atom to pick up from this point for interaction with available dropoff points based on the dropoff points.
-/obj/machinery/big_manipulator/proc/find_pickup_candidate_for_pickup_point(datum/interaction_point/pickup_point)
- if(!pickup_point)
- return null
-
- var/turf/pickup_turf = pickup_point.interaction_turf
- if(!pickup_turf)
- return null
-
- var/list/candidates = list()
- for(var/atom/movable/candidate as anything in pickup_turf.contents)
- if(candidate.anchored || HAS_TRAIT(candidate, TRAIT_NODROP) || move_resist > MOVE_FORCE_STRONG)
- continue
-
- if(!pickup_point.check_filters_for_atom(candidate))
- continue
-
- for(var/datum/interaction_point/dest_point as anything in dropoff_points)
- if(!dest_point || !dest_point.is_valid())
- continue
- if(!dest_point.check_filters_for_atom(candidate))
- continue
- if(dest_point.is_available(TRANSFER_TYPE_DROPOFF, candidate))
- candidates += candidate
- break
-
- if(!length(candidates))
- return null
-
- return pickup_strategy.get_next_candidate(candidates)
-
-/// Calculates the next interaction point the manipulator should transfer the item to or pick up it from.
-/obj/machinery/big_manipulator/proc/find_next_point(transfer_type)
- if(!transfer_type)
- return NONE
+/// We have no tasks to execute for some reason. Waits for a turf signal to retry.
+/obj/machinery/big_manipulator/proc/nothing_ever_happens()
+ if(stopping)
+ complete_stopping_task()
+ return FALSE
- var/atom/movable/target = held_object?.resolve()
- if(isnull(target) && transfer_type == TRANSFER_TYPE_DROPOFF)
- return NONE
+ current_task = null
+ waiting_for_signal = TRUE
+ register_task_turf_signals()
- var/list/interaction_points = transfer_type == TRANSFER_TYPE_DROPOFF ? dropoff_points : pickup_points
- if(!length(interaction_points))
- return NONE
+ return FALSE
- var/datum/tasking_strategy/strategy = transfer_type == TRANSFER_TYPE_DROPOFF ? dropoff_strategy : pickup_strategy
- var/datum/callback/availability_callback = transfer_type == TRANSFER_TYPE_DROPOFF ? CALLBACK(src, PROC_REF(check_dropoff_availability)) : CALLBACK(src, PROC_REF(check_pickup_availability))
+/// A signal ran or some settings changed; checking if we can run the tasks now.
+/obj/machinery/big_manipulator/proc/something_happened()
+ next_cycle_scheduled = FALSE
+ step_tasks()
- return strategy.get_next_available(interaction_points, target, transfer_type, availability_callback)
+/// Runs the next task. Or doesn't.
+/obj/machinery/big_manipulator/proc/step_tasks()
+ if(!on || stopping)
+ return
+ next_cycle_scheduled = FALSE
+ if(waiting_for_signal)
+ unregister_task_turf_signals()
+ waiting_for_signal = FALSE
+ if(!length(tasks))
+ nothing_ever_happens()
+ return
+ var/datum/manipulator_task/next_task = master_tasking.get_next_task(tasks, src)
+ if(!next_task)
+ nothing_ever_happens()
+ return
+ current_task = next_task
+ SStgui.update_uis(src)
+ next_task.run_task(src)
/// Attempts to launch the work cycle. Should only be ran on pressing the "Run" button.
/obj/machinery/big_manipulator/proc/try_kickstart(mob/user)
- if(!on || !anchored || IS_BUSY)
+ if(!on || !anchored || stopping || current_task != null)
return FALSE
if(!use_energy(active_power_usage, force = FALSE))
@@ -58,208 +45,62 @@
return FALSE
next_cycle_scheduled = FALSE
- run_pickup_phase()
+ step_tasks()
-/// Safely schedules the next cycle attempt to prevent overlapping.
-/obj/machinery/big_manipulator/proc/schedule_next_cycle()
- if(next_cycle_scheduled || IS_STOPPING)
- return
-
- // Allow scheduling if manipulator is idle, none, or if we have a held object (need to drop it off)
- if(current_task != CURRENT_TASK_IDLE && current_task != CURRENT_TASK_NONE && !held_object)
+/// Safely schedules the next step to prevent overlapping.
+/obj/machinery/big_manipulator/proc/schedule_next_cycle(time_seconds = BASE_INTERACTION_TIME)
+ if(next_cycle_scheduled || stopping)
return
next_cycle_scheduled = TRUE
- if(held_object)
- run_dropoff_phase()
- else
- run_pickup_phase()
+ addtimer(CALLBACK(src, PROC_REF(step_tasks)), time_seconds)
-/// Handles the common pattern of waiting and scheduling next cycle when no work can be done.
-/obj/machinery/big_manipulator/proc/handle_no_work_available()
- // If we're stopping, don't schedule next cycle
- if(IS_STOPPING)
- complete_stopping_task()
- return FALSE
-
- current_task = CURRENT_TASK_IDLE
-
- addtimer(CALLBACK(src, PROC_REF(schedule_next_cycle)), CYCLE_SKIP_TIMEOUT)
- return FALSE
-
-/obj/machinery/big_manipulator/proc/check_pickup_availability(datum/interaction_point/point, atom/movable/target, transfer_type)
- if(!point)
- return FALSE
-
- var/turf/pickup_turf = point.interaction_turf
- if(!pickup_turf)
- return FALSE
-
- for(var/atom/movable/candidate as anything in pickup_turf.contents)
- if(!candidate.anchored && !HAS_TRAIT(candidate, TRAIT_NODROP))
- return TRUE
-
- return FALSE
-
-/obj/machinery/big_manipulator/proc/check_dropoff_availability(datum/interaction_point/point, atom/movable/target, transfer_type)
- return point.is_available(transfer_type, target)
-
-/// Attempts to run the pickup phase. Selects the next origin point and attempts to pick up an item from it.
-/obj/machinery/big_manipulator/proc/run_pickup_phase()
- if(!on || IS_STOPPING)
- return
-
- next_cycle_scheduled = FALSE
-
- var/datum/interaction_point/origin_point = find_next_point(TRANSFER_TYPE_PICKUP)
-
- if(!origin_point)
- return handle_no_work_available()
-
- rotate_to_point(origin_point, PROC_REF(try_interact_with_origin_point), CURRENT_TASK_MOVING_PICKUP)
- return TRUE
-
-/// Attempts to interact with the origin point (pick up the object)
-/obj/machinery/big_manipulator/proc/try_interact_with_origin_point(datum/interaction_point/origin_point, hand_is_empty = FALSE)
- // If we're stopping, just finish the task and shut down
- if(IS_STOPPING)
- complete_stopping_task()
+/// Rotates the manipulator arm to face the target task's turf.
+/obj/machinery/big_manipulator/proc/rotate_to_point(datum/manipulator_task/cargo/target_task, callback_object, callback)
+ if(stopping)
return
- if(!origin_point.interaction_turf)
- return handle_no_work_available()
-
- var/atom/movable/selected = find_pickup_candidate_for_pickup_point(origin_point) // find a suitable item that matches available destinations
- if(!selected)
- return handle_no_work_available()
-
- if(selected.anchored || HAS_TRAIT(selected, TRAIT_NODROP))
- return handle_no_work_available()
-
- if(isitem(selected))
- var/obj/item/selected_item = selected
- if(selected_item.item_flags & (ABSTRACT|DROPDEL))
- return handle_no_work_available()
-
- start_task(CURRENT_TASK_INTERACTING, 0.2 SECONDS)
- interact_with_origin_point(selected, hand_is_empty)
- return TRUE
-
-/// Attempts to start a work cycle (pick up the object)
-/obj/machinery/big_manipulator/proc/interact_with_origin_point(atom/movable/target, hand_is_empty = FALSE)
- if(!hand_is_empty)
- target.forceMove(src)
- held_object = WEAKREF(target)
- manipulator_arm.update_claw(held_object)
-
- // Schedule the dropoff phase after a successful pickup to avoid overlapping tasks
- if(!hand_is_empty)
- schedule_next_cycle()
-
-/obj/machinery/big_manipulator/proc/run_dropoff_phase()
- // Find the next available destination point that can accept the held item
- var/datum/interaction_point/destination_point = find_next_point(TRANSFER_TYPE_DROPOFF)
-
- next_cycle_scheduled = FALSE
-
- if(!destination_point)
- return handle_no_work_available()
-
- rotate_to_point(destination_point, PROC_REF(try_interact_with_destination_point), CURRENT_TASK_MOVING_DROPOFF)
- return TRUE
-
-/// Attempts to interact with the destination point (drop/use/throw the object)
-/obj/machinery/big_manipulator/proc/try_interact_with_destination_point(datum/interaction_point/destination_point, hand_is_empty = FALSE)
- // If we're stopping, just finish the task and shut down
- if(IS_STOPPING)
- complete_stopping_task()
- return FALSE
-
- start_task(CURRENT_TASK_INTERACTING, 0.2 SECONDS)
-
- if(hand_is_empty)
- use_thing_with_empty_hand(destination_point)
- return TRUE
-
- var/obj/actual_held_object = held_object?.resolve()
- if(actual_held_object.loc != src)
- handle_no_work_available()
- return FALSE
-
- switch(destination_point.interaction_mode)
- if(INTERACT_DROP)
- try_drop_thing(destination_point)
- if(INTERACT_USE)
- try_use_thing(destination_point)
- if(INTERACT_THROW)
- throw_thing(destination_point)
-
- return TRUE
-
-/// Rotates the manipulator arm to face the target point.
-/obj/machinery/big_manipulator/proc/rotate_to_point(datum/interaction_point/target_point, callback, type)
- if(IS_STOPPING)
- return
-
- if(!target_point)
+ if(!target_task)
return FALSE
- var/target_dir = get_dir(get_turf(src), target_point.interaction_turf)
+ var/target_dir = get_dir(get_turf(src), target_task.interaction_turf)
var/target_angle = dir2angle(target_dir)
var/current_angle = manipulator_arm.transform.get_angle()
var/angle_diff = closer_angle_difference(current_angle, target_angle)
var/num_rotations = round(abs(angle_diff) / 45)
- var/total_rotation_time = num_rotations * BASE_INTERACTION_TIME / speed_multiplier
-
- start_task(type == CURRENT_TASK_MOVING_PICKUP ? CURRENT_TASK_MOVING_PICKUP : CURRENT_TASK_MOVING_DROPOFF, total_rotation_time)
- // If the next point is on the same tile, we don't need to rotate at all
if(!num_rotations)
- addtimer(CALLBACK(src, callback, target_point), BASE_INTERACTION_TIME)
+ var/datum/callback/cb = CALLBACK(callback_object, callback, src)
+ cb.Invoke()
return TRUE
- // Breaking the angle up into 45 degree steps
var/rotation_step = 45 * sign(angle_diff)
- do_step_rotation(target_point, callback, current_angle, target_angle, rotation_step, 0, total_rotation_time)
-
+ do_step_rotation(target_task, callback_object, callback, current_angle, target_angle, rotation_step)
return TRUE
/// Does a 45 degree step, animating the claw
-/obj/machinery/big_manipulator/proc/do_step_rotation(datum/interaction_point/target_point, callback, current_angle, target_angle, rotation_step, elapsed_time, total_time)
- if(IS_STOPPING)
+/obj/machinery/big_manipulator/proc/do_step_rotation(datum/manipulator_task/cargo/target_task, callback_object, callback, current_angle, target_angle, rotation_step)
+ if(stopping)
return
- // Just making sure we're not there already
var/angle_diff = closer_angle_difference(current_angle, target_angle)
if(abs(angle_diff) < abs(rotation_step))
- // If this is the last step, doing a precise degree turn
var/matrix/final_matrix = matrix()
final_matrix.Turn(target_angle)
animate(manipulator_arm, transform = final_matrix, time = BASE_INTERACTION_TIME / speed_multiplier)
- addtimer(CALLBACK(src, callback, target_point), BASE_INTERACTION_TIME / speed_multiplier)
+ addtimer(CALLBACK(callback_object, callback, src), BASE_INTERACTION_TIME / speed_multiplier)
return
- // Animating a single rotation step
-
- // YES, this has to be done like that because byond or whatever is stupid and `animate`ing a 180+ degree turn
- // fucking fails and squashes the icon vertically (or horizontally, whichever it feels like) instead
-
var/next_angle = current_angle + rotation_step
var/matrix/next_matrix = matrix()
next_matrix.Turn(next_angle)
animate(manipulator_arm, transform = next_matrix, time = BASE_INTERACTION_TIME / speed_multiplier)
- // Recursively planning the next step (yay recursion :yuppie: call me a madman I LOVE recursion)
- elapsed_time += BASE_INTERACTION_TIME / speed_multiplier
- addtimer(CALLBACK(src, PROC_REF(do_step_rotation), target_point, callback, next_angle, target_angle, rotation_step, elapsed_time, total_time), BASE_INTERACTION_TIME / speed_multiplier)
+ addtimer(CALLBACK(src, PROC_REF(do_step_rotation), target_task, callback_object, callback, next_angle, target_angle, rotation_step), BASE_INTERACTION_TIME / speed_multiplier)
-/// Moves the item onto the turf.
-///
-/// If the turf has an atom with fitting `atom_storage` that corresponds to the
-/// priority settings, it will attempt to insert the held item.
-/obj/machinery/big_manipulator/proc/try_drop_thing(datum/interaction_point/destination_point)
- var/drop_endpoint = destination_point.find_type_priority()
+/obj/machinery/big_manipulator/proc/try_drop_thing(datum/manipulator_task/cargo/dropoff_base/drop/destination_task)
+ var/drop_endpoint = destination_task.find_type_priority()
var/obj/actual_held_object = held_object?.resolve()
if(isnull(drop_endpoint))
@@ -268,46 +109,42 @@
var/atom/drop_target = drop_endpoint
if(drop_target.atom_storage && actual_held_object && (!drop_target.atom_storage.attempt_insert(actual_held_object, override = TRUE, messages = FALSE)))
actual_held_object.forceMove(drop_target.drop_location())
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return TRUE
actual_held_object?.forceMove(drop_endpoint)
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return TRUE
-/// Attempts to use the held object on the atoms of the interaction turf.
-///
-/// If the interaction turf has an atom that corresponds to the priority settings,
-/// it will attempt to use the held item. If it doesn't, it will simply drop the item.
-/obj/machinery/big_manipulator/proc/try_use_thing(datum/interaction_point/destination_point, work_done_at_point = FALSE)
- if(IS_STOPPING)
+/obj/machinery/big_manipulator/proc/try_use_thing(datum/manipulator_task/cargo/interact/destination_task, work_done_at_point = FALSE)
+ if(stopping)
return
var/obj/obj_resolve = held_object?.resolve()
var/mob/living/carbon/human/species/monkey/monkey_resolve = monkey_worker?.resolve()
- var/destination_turf = destination_point.interaction_turf
+ var/destination_turf = destination_task.interaction_turf
if(!obj_resolve || QDELETED(obj_resolve) || obj_resolve.loc != src)
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return FALSE
if(!monkey_resolve || !destination_turf)
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return FALSE
- if(!(monkey_resolve.loc == src))
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ if(monkey_resolve.loc != src)
+ finish_manipulation()
return FALSE
var/obj/item/held_item = obj_resolve
- var/atom/type_to_use = destination_point.find_type_priority()
+ var/atom/type_to_use = destination_task.find_type_priority()
if(isnull(type_to_use))
- check_for_cycle_end_drop(destination_point, FALSE, work_done_at_point)
+ check_for_cycle_end_drop(destination_task, FALSE, work_done_at_point)
return FALSE
- if(isitem(type_to_use) && !destination_point.check_filters_for_atom(type_to_use))
- check_for_cycle_end_drop(destination_point, FALSE, work_done_at_point)
+ if(isitem(type_to_use) && !destination_task.check_filters_for_atom(type_to_use))
+ check_for_cycle_end_drop(destination_task, FALSE, work_done_at_point)
return FALSE
var/original_loc = held_item.loc
@@ -316,109 +153,99 @@
if(held_item.GetComponent(/datum/component/two_handed))
held_item.attack_self(monkey_resolve)
- var/use_rmb = destination_point.worker_use_rmb
- var/use_combat = destination_point.worker_combat_mode
-
- monkey_resolve.combat_mode = use_combat
- held_item.melee_attack_chain(monkey_resolve, type_to_use, list(RIGHT_CLICK = use_rmb ? TRUE : FALSE))
+ monkey_resolve.combat_mode = destination_task.worker_combat_mode
+ held_item.melee_attack_chain(monkey_resolve, type_to_use, list(RIGHT_CLICK = destination_task.worker_use_rmb ? TRUE : FALSE))
monkey_resolve.combat_mode = FALSE
do_attack_animation(destination_turf)
manipulator_arm.do_attack_animation(destination_turf)
- // if we destroyed the item while using it OR something else uncanny happened to it and it's GONE
if(QDELETED(held_item) || !held_item || (held_item.loc != monkey_resolve && held_item.loc != original_loc))
held_object = null
manipulator_arm.update_claw(null)
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return TRUE
if(held_item.loc == monkey_resolve)
held_item.forceMove(original_loc)
- check_for_cycle_end_drop(destination_point, TRUE, TRUE)
+ check_for_cycle_end_drop(destination_task, TRUE, TRUE)
-/// Checks what should we do with the `held_object` after `USE`-ing it.
-/obj/machinery/big_manipulator/proc/check_for_cycle_end_drop(datum/interaction_point/drop_point, item_used_this_iteration, work_done_at_point = FALSE)
+/obj/machinery/big_manipulator/proc/check_for_cycle_end_drop(datum/manipulator_task/cargo/interact/destination_task, item_used_this_iteration, work_done_at_point = FALSE)
var/obj/obj_resolve = held_object?.resolve()
- var/turf/drop_turf = drop_point.interaction_turf
+ var/turf/drop_turf = destination_task.interaction_turf
if(!obj_resolve || obj_resolve.loc != src || QDELETED(obj_resolve))
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return
- if(drop_point.worker_interaction == WORKER_SINGLE_USE && item_used_this_iteration)
+ if(destination_task.worker_interaction == WORKER_SINGLE_USE && item_used_this_iteration)
obj_resolve.forceMove(drop_turf)
obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src))
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return
- if(!on || drop_point.interaction_mode != INTERACT_USE)
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ if(!on)
+ finish_manipulation()
return
if(item_used_this_iteration)
- addtimer(CALLBACK(src, PROC_REF(try_use_thing), drop_point, TRUE), BASE_INTERACTION_TIME * 2)
+ addtimer(CALLBACK(src, PROC_REF(try_use_thing), destination_task, TRUE), BASE_INTERACTION_TIME * 2)
return
- switch(drop_point.use_post_interaction)
+ drop_held_after_use(destination_task)
+
+/obj/machinery/big_manipulator/proc/drop_held_after_use(datum/manipulator_task/cargo/interact/destination_task)
+ var/obj/obj_resolve = held_object?.resolve()
+ var/turf/drop_turf = destination_task.interaction_turf
+
+ switch(destination_task.use_post_interaction)
if(POST_INTERACTION_DROP_AT_POINT)
obj_resolve.forceMove(drop_turf)
obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src))
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
- return
+ finish_manipulation()
if(POST_INTERACTION_DROP_AT_MACHINE)
obj_resolve.forceMove(get_turf(src))
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
- return
+ finish_manipulation()
if(POST_INTERACTION_DROP_NEXT_FITTING)
- var/datum/interaction_point/next = find_next_point(TRANSFER_TYPE_DROPOFF)
- if(next)
- rotate_to_point(next, PROC_REF(try_interact_with_destination_point), CURRENT_TASK_MOVING_DROPOFF)
+ var/datum/manipulator_task/next = master_tasking.get_next_task(tasks, src)
+ if(istype(next, /datum/manipulator_task/cargo/dropoff_base))
+ rotate_to_point(next, next, TYPE_PROC_REF(/datum/manipulator_task/cargo/dropoff_base, try_dropoff))
return
obj_resolve.forceMove(drop_turf)
obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src))
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
- return
-
- if(POST_INTERACTION_WAIT)
+ finish_manipulation()
+ else
schedule_next_cycle()
- return
- schedule_next_cycle()
-
-/// Throws the held object in the direction of the interaction point.
-/obj/machinery/big_manipulator/proc/throw_thing(datum/interaction_point/drop_point)
- var/drop_turf = drop_point.interaction_turf
- var/item_throw_range = drop_point.throw_range
+/obj/machinery/big_manipulator/proc/throw_thing(datum/manipulator_task/cargo/dropoff_base/throw/throw_task)
+ var/drop_turf = throw_task.interaction_turf
var/atom/movable/held_atom = held_object?.resolve()
held_atom.forceMove(drop_turf)
do_attack_animation(drop_turf)
manipulator_arm.do_attack_animation(drop_turf)
- if(((isliving(held_atom))) && !(obj_flags & EMAGGED))
+ if(isliving(held_atom) && !(obj_flags & EMAGGED))
held_atom.dir = get_dir(get_turf(held_atom), get_turf(src))
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return
- held_atom.throw_at(get_edge_target_turf(get_turf(src), get_dir(get_turf(src), get_turf(held_atom))), item_throw_range, 2)
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ held_atom.throw_at(get_edge_target_turf(get_turf(src), get_dir(get_turf(src), get_turf(held_atom))), throw_task.throw_range, 2)
+ finish_manipulation()
-/// Uses the empty hand to interact with objects
-/obj/machinery/big_manipulator/proc/use_thing_with_empty_hand(datum/interaction_point/destination_point)
+/obj/machinery/big_manipulator/proc/use_thing_with_empty_hand(datum/manipulator_task/cargo/interact/destination_task)
var/mob/living/carbon/human/species/monkey/monkey_resolve = monkey_worker?.resolve()
if(isnull(monkey_resolve))
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return
- var/atom/type_to_use = destination_point.find_type_priority()
+ var/atom/type_to_use = destination_task.find_type_priority()
if(isnull(type_to_use))
- check_end_of_use_for_use_with_empty_hand(destination_point, FALSE)
+ check_end_of_use_for_use_with_empty_hand(destination_task, FALSE)
return
- // We don't perform an unarmed attack on items because we pick them up duh
if(isitem(type_to_use))
var/obj/item/interact_with_item = type_to_use
var/resolve_loc = interact_with_item.loc
@@ -426,37 +253,78 @@
interact_with_item.attack_self(monkey_resolve)
interact_with_item.forceMove(resolve_loc)
else
+ monkey_resolve.combat_mode = destination_task.worker_combat_mode
monkey_resolve.UnarmedAttack(type_to_use)
+ monkey_resolve.combat_mode = FALSE
- var/turf/dest_turf = destination_point.interaction_turf
+ var/turf/dest_turf = destination_task.interaction_turf
if(dest_turf)
do_attack_animation(dest_turf)
manipulator_arm.do_attack_animation(dest_turf)
- check_end_of_use_for_use_with_empty_hand(destination_point, TRUE)
-/// Checks if we should continue using the empty hand after interaction
-/obj/machinery/big_manipulator/proc/check_end_of_use_for_use_with_empty_hand(datum/interaction_point/destination_point, item_was_used = TRUE)
- if(!on || (destination_point.worker_interaction != WORKER_EMPTY_USE && destination_point.interaction_mode == INTERACT_USE))
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ check_end_of_use_for_use_with_empty_hand(destination_task, TRUE)
+
+/obj/machinery/big_manipulator/proc/check_end_of_use_for_use_with_empty_hand(datum/manipulator_task/cargo/interact/destination_task, item_was_used = TRUE)
+ if(!on || destination_task.worker_interaction != WORKER_EMPTY_USE)
+ finish_manipulation()
return
if(!item_was_used)
- finish_manipulation(TRANSFER_TYPE_DROPOFF)
+ finish_manipulation()
return
- addtimer(CALLBACK(src, PROC_REF(use_thing_with_empty_hand), destination_point), BASE_INTERACTION_TIME)
+ addtimer(CALLBACK(src, PROC_REF(use_thing_with_empty_hand), destination_task), BASE_INTERACTION_TIME)
-/// Completes the current manipulation action
-/obj/machinery/big_manipulator/proc/finish_manipulation(transfer_type = TRANSFER_TYPE_DROPOFF)
+/// Completes the current manipulation action and schedules the next step.
+/obj/machinery/big_manipulator/proc/finish_manipulation()
held_object = null
manipulator_arm.update_claw(null)
+ current_task = null
- end_current_task()
+ SStgui.update_uis(src)
- if(IS_STOPPING)
+ if(stopping)
complete_stopping_task()
return
- current_task = CURRENT_TASK_IDLE
-
schedule_next_cycle()
+
+/// Completes the stopping task and transitions to idle
+/obj/machinery/big_manipulator/proc/complete_stopping_task()
+ on = FALSE
+ stopping = FALSE
+ next_cycle_scheduled = FALSE
+ current_task = null
+ unregister_task_turf_signals()
+ waiting_for_signal = FALSE
+ SStgui.update_uis(src)
+
+/// Registers enter/exit signals on all unique cargo task turfs.
+/obj/machinery/big_manipulator/proc/register_task_turf_signals()
+ unregister_task_turf_signals()
+ for(var/datum/manipulator_task/cargo/task in tasks)
+ if(!task.interaction_turf || (task.interaction_turf in signal_turfs))
+ continue
+ signal_turfs += task.interaction_turf
+ RegisterSignals(task.interaction_turf, list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED), PROC_REF(on_task_turf_changed))
+
+/// Unregisters all previously registered turf signals.
+/obj/machinery/big_manipulator/proc/unregister_task_turf_signals()
+ for(var/turf/t in signal_turfs)
+ UnregisterSignal(t, list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED))
+ signal_turfs = list()
+
+/// Fires when something enters or leaves a watched task turf.
+/obj/machinery/big_manipulator/proc/on_task_turf_changed(datum/source)
+ SIGNAL_HANDLER
+ if(!on || stopping || !waiting_for_signal)
+ return
+ something_happened()
+
+/// Drop the held atom.
+/obj/machinery/big_manipulator/proc/drop_held_atom()
+ if(isnull(held_object))
+ return
+ var/obj/obj_resolve = held_object?.resolve()
+ obj_resolve?.forceMove(get_turf(obj_resolve))
+ finish_manipulation()
diff --git a/code/game/machinery/big_manipulator/interaction_points.dm b/code/game/machinery/big_manipulator/interaction_points.dm
deleted file mode 100644
index 3d3a8c4b8fb2..000000000000
--- a/code/game/machinery/big_manipulator/interaction_points.dm
+++ /dev/null
@@ -1,239 +0,0 @@
-/datum/interaction_point
- var/name = "interaction point"
-
- /// The turf this interaction point represents.
- var/turf/interaction_turf
- /// Should we check our filters while interacting with this point?
- var/should_use_filters = FALSE
- /// How should this point be interacted with?
- var/interaction_mode = INTERACT_DROP
- /// How should the monkey worker (if there is one) interact with the target point?
- var/worker_interaction = WORKER_NORMAL_USE
- /// How far should the manipulator throw the object?
- var/throw_range = 1
- /// Which items are supposed to be picked up from `interaction_turf` if this is a pickup point
- /// or looked for in the `interaction_turf` if this is a dropoff point.
- var/list/atom_filters = list()
- /// If this is a dropoff point, influences which interaction endpoints are preferred over which
- /// by the manipulator.
- var/list/interaction_priorities = list()
- /// Should the manipulator put items on this point if there are already such items on the turf?
- var/overflow_status = POINT_OVERFLOW_ALLOWED
- /// Which object category should the filters be looking out for.
- var/filtering_mode = TAKE_ITEMS
- /// Whether the worker will use combat mode while interacting with this point.
- var/worker_combat_mode = FALSE
- /// Whether the worker will simulate RMB instead of LMB on interaction.
- var/worker_use_rmb = FALSE
- /// List of types that can be picked up from this point
- var/list/type_filters = list(
- /obj/item,
- /obj/structure/closet,
- )
- /// What should the manipulator do when there's nothing to "USE" the held item on anymore?
- var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT
-
-/datum/interaction_point/New(turf/new_turf, list/new_filters, new_should_use_filters, new_interaction_mode, new_allowed_types, new_overflow_status, manipulator_tier)
- if(!new_turf)
- stack_trace("New manipulator interaction point created with no valid turf references passed.")
- qdel(src)
- return
-
- if(isclosedturf(new_turf))
- qdel(src)
- return
-
- interaction_turf = new_turf
-
- if(length(new_filters))
- atom_filters = new_filters
-
- if(new_should_use_filters)
- should_use_filters = new_should_use_filters
-
- if(new_interaction_mode)
- interaction_mode = new_interaction_mode
-
- if(new_allowed_types)
- type_filters = new_allowed_types
-
- if(new_overflow_status)
- overflow_status = new_overflow_status
-
- interaction_priorities = fill_priority_list(manipulator_tier)
-
-/// Finds the type priority of the interaction point.
-/datum/interaction_point/proc/find_type_priority()
- var/list/turf_contents = interaction_turf.contents
-
- var/atom/movable/best_candidate = null
- var/best_priority_index = INFINITY
-
- for(var/atom/movable/thing as anything in turf_contents)
- for(var/i in 1 to length(interaction_priorities))
- if(i >= best_priority_index)
- break
-
- var/datum/manipulator_priority/prio = interaction_priorities[i]
-
- if(!prio.active)
- continue
-
- if(prio.atom_typepath == /turf)
- if(i < best_priority_index)
- best_candidate = interaction_turf
- best_priority_index = i
- continue
-
- if(!istype(thing, prio.atom_typepath))
- continue
-
- if(isliving(thing))
- var/mob/living/L = thing
- if(L.stat == DEAD)
- continue
-
- best_candidate = thing
- best_priority_index = i
-
- if(best_priority_index == 1)
- return best_candidate
- break
-
- return best_candidate
-
-/// Checks if the interaction point is available - if it has items that can be interacted with.
-/datum/interaction_point/proc/is_available(transfer_type, atom/movable/target)
- if(!is_valid())
- return FALSE
-
- // All atoms on the turf that can be interacted with.
- var/list/atoms_on_the_turf = interaction_turf?.contents
-
- // For pickup points, we want points that have atoms to pick up
- if(transfer_type == TRANSFER_TYPE_PICKUP)
-
- if(!length(atoms_on_the_turf))
- return FALSE // nothing to pick up
-
- // If the atom filters are required, we need to check if any atom on the turf fits the filters. If not, the check will only determine whether it fits the category
- for(var/atom/movable/movable_atom as anything in atoms_on_the_turf)
- if(check_filters_for_atom(movable_atom))
- return TRUE
-
- // No suitable atoms to pick up - the pickup point is unavailable.
- return FALSE
-
- if(transfer_type == TRANSFER_TYPE_DROPOFF)
- // If filters are enabled, the held item itself must match them for any overflow mode
- if(!check_filters_for_atom(target) && should_use_filters)
- return FALSE
-
- if(interaction_mode != INTERACT_DROP) // you don't check for overflow if you're not actually putting anything on the turf silly
- return TRUE
-
- switch(overflow_status)
- if(POINT_OVERFLOW_ALLOWED)
- // If we don't care if there are already things on this turf, then we just check for filters
- // Hence if the atom filters are skipped, the point is available for dropoff
- if(!should_use_filters)
- return TRUE
-
- if(POINT_OVERFLOW_FILTERS)
- // We need to check if there are already items matching the filters on the turf
- for(var/atom/movable/movable_atom as anything in atoms_on_the_turf)
- if(check_filters_for_atom(movable_atom))
- return FALSE // the item on the turf was in the filters, hence the turf is considered overflowed
-
- if(POINT_OVERFLOW_HELD)
- // We need to check if any of the items on the turf match the item we're holding
- for(var/atom/movable/movable_atom as anything in atoms_on_the_turf)
- if(istype(movable_atom, target?.type))
- return FALSE // one of the items on the turf was the same as the one we're holding
-
- if(POINT_OVERFLOW_FORBIDDEN)
- // We need to check if there are already ANY items on the turf
- if(locate(/obj/item) in atoms_on_the_turf)
- return FALSE
-
- return TRUE
-
- // Ambiguous interaction type — no interaction is possible, point is unavailable
- return FALSE
-
-/// Checks if the interaction point is valid — is not located on a closed turf.
-/datum/interaction_point/proc/is_valid()
- if(!interaction_turf)
- return FALSE
-
- if(isclosedturf(interaction_turf))
- return FALSE
- return TRUE
-
-/// Checks if the passed movable `atom` fits the filters.
-/datum/interaction_point/proc/check_filters_for_atom(atom/movable/target)
- if(!target || target.anchored || HAS_TRAIT(target, TRAIT_NODROP))
- return FALSE
-
- switch(filtering_mode)
- if(TAKE_CLOSETS)
- return iscloset(target)
-
- if(TAKE_HUMANS)
- return ishuman(target)
-
- if(TAKE_ITEMS)
- if(!should_use_filters)
- return(isitem(target))
-
- for(var/filter_path in atom_filters)
- if(istype(target, filter_path))
- return TRUE
- return FALSE
-
- return FALSE
-
-/// Fills the interaction endpoint priority list for the current interaction mode.
-/datum/interaction_point/proc/fill_priority_list(manipulator_tier)
- var/list/priorities_to_set = new /list((manipulator_tier == 4 ? 5 : 4))
-
- switch(interaction_mode)
- if(INTERACT_DROP)
- priorities_to_set[1] = new /datum/manipulator_priority/drop/in_storage
- priorities_to_set[2] = new /datum/manipulator_priority/drop/on_floor
-
- if(INTERACT_USE)
- priorities_to_set[1] = new /datum/manipulator_priority/interact/with_living
- priorities_to_set[2] = new /datum/manipulator_priority/interact/with_structure
- priorities_to_set[3] = new /datum/manipulator_priority/interact/with_machinery
- priorities_to_set[4] = new /datum/manipulator_priority/interact/with_items
-
- if(manipulator_tier == 4)
- priorities_to_set[5] = new /datum/manipulator_priority/interact/with_vehicles
-
- return priorities_to_set
-
-/// Moves the priority for a given index 1 step higher.
-/datum/interaction_point/proc/move_priority_up_by_index(index)
- if(!index) // also handles index being 0
- return FALSE
-
- interaction_priorities.Swap(index, index + 1)
-
- return TRUE
-
-/// Toggles the priority's `active` param. Sets to TRUE if `reset` is TRUE.
-/datum/interaction_point/proc/tick_priority_by_index(index, reset = FALSE)
- var/datum/manipulator_priority/target_priority = interaction_priorities[index + 1]
-
- if(reset)
- target_priority.active = TRUE
- else
- target_priority.active = !target_priority.active
-
- return TRUE
-
-/datum/interaction_point/Destroy()
- interaction_turf = null
- QDEL_LIST(interaction_priorities)
- return ..()
diff --git a/code/game/machinery/big_manipulator/manipulator_tasks.dm b/code/game/machinery/big_manipulator/manipulator_tasks.dm
new file mode 100644
index 000000000000..48a136ff255f
--- /dev/null
+++ b/code/game/machinery/big_manipulator/manipulator_tasks.dm
@@ -0,0 +1,501 @@
+/datum/manipulator_task
+ var/name = "task"
+
+/datum/manipulator_task/proc/can_run(obj/machinery/big_manipulator/manipulator)
+ return FALSE
+
+/datum/manipulator_task/proc/run_task(obj/machinery/big_manipulator/manipulator)
+ return
+
+/datum/manipulator_task/proc/serialize()
+ return list("type" = type)
+
+/datum/manipulator_task/New(...)
+ ..()
+ return
+
+// ===== WAIT =====
+
+/datum/manipulator_task/simple/wait
+ name = "wait"
+ var/time_seconds = 1
+
+/datum/manipulator_task/simple/wait/can_run(obj/machinery/big_manipulator/manipulator)
+ for(var/datum/manipulator_task/cargo/task in manipulator.tasks)
+ if(task.can_run(manipulator))
+ return TRUE
+ return FALSE
+
+/datum/manipulator_task/simple/wait/run_task(obj/machinery/big_manipulator/manipulator)
+ manipulator.schedule_next_cycle(time_seconds SECONDS)
+
+/datum/manipulator_task/simple/wait/serialize()
+ var/list/data = ..()
+ data["time_seconds"] = time_seconds
+ return data
+
+/datum/manipulator_task/simple/wait/New(..., serialized_data)
+ ..()
+ if(serialized_data)
+ time_seconds = serialized_data["time_seconds"]
+ return
+
+// ===== BASE CARGO =====
+
+/datum/manipulator_task/cargo
+ var/turf/interaction_turf
+ var/offset_dx
+ var/offset_dy
+ var/should_use_filters = FALSE
+ var/list/atom_filters = list()
+ var/filtering_mode = TAKE_ITEMS
+ var/list/type_filters = list(
+ /obj/item,
+ /obj/structure/closet,
+ )
+ var/list/interaction_priorities = list()
+
+/datum/manipulator_task/cargo/New(turf/new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ var/list/offset = serialized_data["offset"]
+ if(islist(offset))
+ offset_dx = offset["dx"]
+ offset_dy = offset["dy"]
+ if(new_turf)
+ interaction_turf = new_turf
+
+ should_use_filters = !!serialized_data["should_use_filters"]
+ atom_filters = serialized_data["atom_filters"] || list()
+ filtering_mode = serialized_data["filtering_mode"]
+ type_filters = serialized_data["type_filters"] || list()
+
+ var/list/prios_data = serialized_data["interaction_priorities"]
+ if(islist(prios_data))
+ interaction_priorities = list()
+ for(var/list/prio_data as anything in prios_data)
+ if(!islist(prio_data))
+ continue
+ var/prio_type = prio_data["type"]
+ if(!ispath(prio_type, /datum/manipulator_priority))
+ continue
+ var/datum/manipulator_priority/prio = new prio_type
+ prio.active = !!prio_data["active"]
+ interaction_priorities += prio
+
+ return ..()
+
+ if(!new_turf)
+ stack_trace("New manipulator task created with no valid turf reference passed.")
+ qdel(src)
+ return
+
+ if(isclosedturf(new_turf))
+ qdel(src)
+ return
+
+ interaction_turf = new_turf
+ interaction_priorities = fill_priority_list(manipulator_tier)
+ return ..()
+
+/datum/manipulator_task/cargo/proc/fill_priority_list(manipulator_tier)
+ return list()
+
+/datum/manipulator_task/cargo/proc/find_type_priority()
+ var/atom/movable/best_candidate = null
+ var/best_priority_index = INFINITY
+
+ for(var/atom/movable/thing as anything in interaction_turf.contents)
+ for(var/i in 1 to length(interaction_priorities))
+ if(i >= best_priority_index)
+ break
+
+ var/datum/manipulator_priority/prio = interaction_priorities[i]
+
+ if(!prio.active || ispath(prio, /turf))
+ continue
+
+ if(!istype(thing, prio.atom_typepath))
+ continue
+
+ if(isliving(thing))
+ var/mob/living/living_mob = thing
+ if(living_mob.stat == DEAD)
+ continue
+
+ best_candidate = thing
+ best_priority_index = i
+
+ if(best_priority_index == 1)
+ return best_candidate
+ break
+
+ for(var/i in 1 to length(interaction_priorities))
+ if(i >= best_priority_index)
+ break
+ var/datum/manipulator_priority/prio = interaction_priorities[i]
+ if(prio.active && prio.atom_typepath == /turf)
+ best_candidate = interaction_turf
+ best_priority_index = i
+ break
+
+ return best_candidate
+
+/datum/manipulator_task/cargo/proc/move_priority_up_by_index(index)
+ if(!index)
+ return FALSE
+ interaction_priorities.Swap(index, index + 1)
+ return TRUE
+
+/datum/manipulator_task/cargo/proc/tick_priority_by_index(index, reset = FALSE)
+ var/datum/manipulator_priority/target_priority = interaction_priorities[index + 1]
+ if(reset)
+ target_priority.active = TRUE
+ else
+ target_priority.active = !target_priority.active
+ return TRUE
+
+/datum/manipulator_task/cargo/proc/is_valid()
+ if(!interaction_turf)
+ return FALSE
+ return !isclosedturf(interaction_turf)
+
+/datum/manipulator_task/cargo/proc/check_filters_for_atom(atom/movable/target)
+ if(!target || target.anchored || HAS_TRAIT(target, TRAIT_NODROP))
+ return FALSE
+
+ switch(filtering_mode)
+ if(TAKE_CLOSETS)
+ return iscloset(target)
+ if(TAKE_HUMANS)
+ return ishuman(target)
+ if(TAKE_ITEMS)
+ if(!should_use_filters)
+ return isitem(target)
+ for(var/filter_path in atom_filters)
+ if(istype(target, filter_path))
+ return TRUE
+ return FALSE
+
+ return FALSE
+
+/datum/manipulator_task/cargo/can_run(obj/machinery/big_manipulator/manipulator)
+ return is_valid()
+
+/datum/manipulator_task/cargo/serialize()
+ var/list/data = ..()
+ data["offset"] = list(
+ "dx" = offset_dx,
+ "dy" = offset_dy,
+ )
+ data["should_use_filters"] = should_use_filters
+ data["atom_filters"] = atom_filters
+ data["filtering_mode"] = filtering_mode
+ data["type_filters"] = type_filters
+ data["interaction_priorities"] = list()
+ for(var/datum/manipulator_priority/prio as anything in interaction_priorities)
+ data["interaction_priorities"] += list(list(
+ "type" = prio.type,
+ "active" = prio.active,
+ ))
+ return data
+
+
+/datum/manipulator_task/cargo/Destroy()
+ interaction_turf = null
+ QDEL_LIST(interaction_priorities)
+ return ..()
+
+// ===== PICKUP =====
+
+/datum/manipulator_task/cargo/pickup
+ name = "pickup"
+ var/pickup_eagerness = PICKUP_CAN_WAIT
+
+/datum/manipulator_task/cargo/pickup/fill_priority_list(manipulator_tier)
+ return list()
+
+/datum/manipulator_task/cargo/pickup/can_run(obj/machinery/big_manipulator/manipulator)
+ if(!..())
+ return FALSE
+ if(manipulator.held_object)
+ return FALSE
+ for(var/atom/movable/candidate as anything in interaction_turf.contents)
+ if(!check_filters_for_atom(candidate))
+ continue
+ if(pickup_eagerness == PICKUP_EAGER)
+ return TRUE
+ for(var/datum/manipulator_task/cargo/dropoff_base/dest in manipulator.tasks)
+ if(dest.can_accept(candidate))
+ return TRUE
+ return FALSE
+
+/datum/manipulator_task/cargo/pickup/run_task(obj/machinery/big_manipulator/manipulator)
+ manipulator.rotate_to_point(src, src, PROC_REF(try_pickup))
+
+/datum/manipulator_task/cargo/pickup/proc/try_pickup(obj/machinery/big_manipulator/manipulator)
+ var/atom/movable/selected = find_pickup_candidate(manipulator)
+ if(!selected)
+ manipulator.nothing_ever_happens()
+ return
+
+ if(selected.anchored || HAS_TRAIT(selected, TRAIT_NODROP))
+ manipulator.nothing_ever_happens()
+ return
+
+ if(isitem(selected))
+ var/obj/item/selected_item = selected
+ if(selected_item.item_flags & (ABSTRACT|DROPDEL))
+ manipulator.nothing_ever_happens()
+ return
+
+ selected.forceMove(manipulator)
+ manipulator.held_object = WEAKREF(selected)
+ manipulator.manipulator_arm.update_claw(manipulator.held_object)
+ manipulator.schedule_next_cycle()
+
+/datum/manipulator_task/cargo/pickup/serialize()
+ var/list/data = ..()
+ data["pickup_eagerness"] = pickup_eagerness
+ return data
+
+/datum/manipulator_task/cargo/pickup/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ pickup_eagerness = serialized_data["pickup_eagerness"]
+ return
+
+/datum/manipulator_task/cargo/pickup/proc/find_pickup_candidate(obj/machinery/big_manipulator/manipulator)
+ var/list/candidates = list()
+
+ for(var/atom/movable/candidate as anything in interaction_turf.contents)
+ if(candidate.anchored || HAS_TRAIT(candidate, TRAIT_NODROP))
+ continue
+ if(!check_filters_for_atom(candidate))
+ continue
+ if(pickup_eagerness == PICKUP_EAGER)
+ candidates += candidate
+ continue
+ for(var/datum/manipulator_task/cargo/dropoff_base/dest in manipulator.tasks)
+ if(dest.can_accept(candidate))
+ candidates += candidate
+ break
+
+ if(!length(candidates))
+ return null
+
+ return manipulator.master_tasking.get_next_candidate(candidates)
+
+// ===== BASE DROPOFF =====
+// Base type for anything that accepts a `held_object`: drop, throw, use.
+// Pickup iterates by this type to find a target point.
+
+/datum/manipulator_task/cargo/dropoff_base
+ name = "dropoff"
+
+/datum/manipulator_task/cargo/dropoff_base/proc/can_accept(atom/movable/target)
+ if(!is_valid())
+ return FALSE
+ if(should_use_filters && !check_filters_for_atom(target))
+ return FALSE
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/can_run(obj/machinery/big_manipulator/manipulator)
+ if(!..())
+ return FALSE
+ var/atom/movable/target = manipulator.held_object?.resolve()
+ if(!target)
+ return FALSE
+ return can_accept(target)
+
+/datum/manipulator_task/cargo/dropoff_base/run_task(obj/machinery/big_manipulator/manipulator)
+ manipulator.rotate_to_point(src, src, PROC_REF(try_dropoff))
+
+/datum/manipulator_task/cargo/dropoff_base/proc/try_dropoff(obj/machinery/big_manipulator/manipulator)
+ var/obj/actual_held_object = manipulator.held_object?.resolve()
+ if(!actual_held_object || actual_held_object.loc != manipulator)
+ manipulator.nothing_ever_happens()
+ return FALSE
+ do_dropoff(manipulator)
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/serialize()
+ var/list/data = ..()
+ return data
+
+
+/datum/manipulator_task/cargo/dropoff_base/proc/do_dropoff(obj/machinery/big_manipulator/manipulator)
+ return
+
+// ===== DROP =====
+
+/datum/manipulator_task/cargo/dropoff_base/drop
+ name = "drop"
+ var/overflow_status = POINT_OVERFLOW_ALLOWED
+
+/datum/manipulator_task/cargo/dropoff_base/drop/fill_priority_list(manipulator_tier)
+ return list(
+ new /datum/manipulator_priority/drop/in_storage,
+ new /datum/manipulator_priority/drop/on_floor
+ )
+
+/datum/manipulator_task/cargo/dropoff_base/drop/can_accept(atom/movable/target)
+ if(!..())
+ return FALSE
+
+ var/list/atoms_on_the_turf = interaction_turf.contents
+ switch(overflow_status)
+ if(POINT_OVERFLOW_ALLOWED)
+ return TRUE
+ if(POINT_OVERFLOW_FILTERS)
+ for(var/atom/movable/movable_atom as anything in atoms_on_the_turf)
+ if(check_filters_for_atom(movable_atom))
+ return FALSE
+ if(POINT_OVERFLOW_HELD)
+ for(var/atom/movable/movable_atom as anything in atoms_on_the_turf)
+ if(istype(movable_atom, target?.type))
+ return FALSE
+ if(POINT_OVERFLOW_FORBIDDEN)
+ if(locate(/obj/item) in atoms_on_the_turf)
+ return FALSE
+
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/drop/serialize()
+ var/list/data = ..()
+ data["overflow_status"] = overflow_status
+ return data
+
+/datum/manipulator_task/cargo/dropoff_base/drop/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ overflow_status = serialized_data["overflow_status"]
+ return
+
+/datum/manipulator_task/cargo/dropoff_base/drop/do_dropoff(obj/machinery/big_manipulator/manipulator)
+ manipulator.try_drop_thing(src)
+
+// ===== THROW =====
+
+/datum/manipulator_task/cargo/dropoff_base/throw
+ name = "throw"
+ var/throw_range = 1
+
+/datum/manipulator_task/cargo/dropoff_base/throw/can_accept(atom/movable/target)
+ if(!is_valid())
+ return FALSE
+ if(should_use_filters && !check_filters_for_atom(target))
+ return FALSE
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/throw/serialize()
+ var/list/data = ..()
+ data["throw_range"] = throw_range
+ return data
+
+/datum/manipulator_task/cargo/dropoff_base/throw/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ throw_range = serialized_data["throw_range"]
+ return
+
+/datum/manipulator_task/cargo/dropoff_base/throw/do_dropoff(obj/machinery/big_manipulator/manipulator)
+ manipulator.throw_thing(src)
+
+// ===== USE =====
+
+/datum/manipulator_task/cargo/dropoff_base/use
+ name = "use"
+ var/worker_interaction = WORKER_NORMAL_USE
+ var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT
+ var/worker_combat_mode = FALSE
+ var/worker_use_rmb = FALSE
+
+/datum/manipulator_task/cargo/dropoff_base/use/fill_priority_list(manipulator_tier)
+ var/list/priorities = list(
+ new /datum/manipulator_priority/interact/with_living,
+ new /datum/manipulator_priority/interact/with_structure,
+ new /datum/manipulator_priority/interact/with_machinery,
+ new /datum/manipulator_priority/interact/with_items,
+ )
+ if(manipulator_tier == 4)
+ priorities += new /datum/manipulator_priority/interact/with_vehicles
+ return priorities
+
+/datum/manipulator_task/cargo/dropoff_base/use/can_accept(atom/movable/target)
+ if(!is_valid())
+ return FALSE
+ if(should_use_filters && !check_filters_for_atom(target))
+ return FALSE
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/use/serialize()
+ var/list/data = ..()
+ data["worker_interaction"] = worker_interaction
+ data["use_post_interaction"] = use_post_interaction
+ data["worker_combat_mode"] = worker_combat_mode
+ data["worker_use_rmb"] = worker_use_rmb
+ return data
+
+/datum/manipulator_task/cargo/dropoff_base/use/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ worker_interaction = serialized_data["worker_interaction"]
+ use_post_interaction = serialized_data["use_post_interaction"]
+ worker_combat_mode = !!serialized_data["worker_combat_mode"]
+ worker_use_rmb = !!serialized_data["worker_use_rmb"]
+ return
+
+/datum/manipulator_task/cargo/dropoff_base/use/do_dropoff(obj/machinery/big_manipulator/manipulator)
+ manipulator.try_use_thing(src)
+
+// ===== INTERACT (empty hand) =====
+
+/datum/manipulator_task/cargo/interact
+ name = "interact"
+ var/worker_interaction = WORKER_EMPTY_USE
+ var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT
+ var/worker_combat_mode = FALSE
+ var/worker_use_rmb = FALSE
+
+/datum/manipulator_task/cargo/interact/fill_priority_list(manipulator_tier)
+ var/list/priorities = list(
+ new /datum/manipulator_priority/interact/with_living,
+ new /datum/manipulator_priority/interact/with_structure,
+ new /datum/manipulator_priority/interact/with_machinery,
+ new /datum/manipulator_priority/interact/with_items,
+ )
+ if(manipulator_tier == 4)
+ priorities += new /datum/manipulator_priority/interact/with_vehicles
+ return priorities
+
+/datum/manipulator_task/cargo/interact/can_run(obj/machinery/big_manipulator/manipulator)
+ if(!..())
+ return FALSE
+ return find_type_priority() != null
+
+/datum/manipulator_task/cargo/interact/run_task(obj/machinery/big_manipulator/manipulator)
+ manipulator.rotate_to_point(src, src, PROC_REF(try_interact))
+
+/datum/manipulator_task/cargo/interact/serialize()
+ var/list/data = ..()
+ data["worker_interaction"] = worker_interaction
+ data["use_post_interaction"] = use_post_interaction
+ data["worker_combat_mode"] = worker_combat_mode
+ data["worker_use_rmb"] = worker_use_rmb
+ return data
+
+/datum/manipulator_task/cargo/interact/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ worker_interaction = serialized_data["worker_interaction"]
+ use_post_interaction = serialized_data["use_post_interaction"]
+ worker_combat_mode = !!serialized_data["worker_combat_mode"]
+ worker_use_rmb = !!serialized_data["worker_use_rmb"]
+ return
+
+/datum/manipulator_task/cargo/interact/proc/try_interact(obj/machinery/big_manipulator/manipulator)
+ var/atom/movable/held = manipulator.held_object?.resolve()
+ if(held)
+ manipulator.try_use_thing(src)
+ else
+ manipulator.use_thing_with_empty_hand(src)
diff --git a/code/game/machinery/big_manipulator/tasking.dm b/code/game/machinery/big_manipulator/tasking.dm
index 90db115d1f60..cc91434363e5 100644
--- a/code/game/machinery/big_manipulator/tasking.dm
+++ b/code/game/machinery/big_manipulator/tasking.dm
@@ -1,109 +1,69 @@
-// Handles indexing for the tasking strategy used for the manipulator's point iteraction
/datum/tasking_strategy
var/current_index = 1
-/// Get the next available point for this tasking strategy
-/datum/tasking_strategy/proc/get_next_available(list/points, atom/movable/target, transfer_type, availability_check)
+/// Returns the next task to run, or null if nothing is available.
+/datum/tasking_strategy/proc/get_next_task(list/tasks)
return null
-// Prefers the first point avaliable
-/datum/tasking_strategy/prefer_first
-
-/datum/tasking_strategy/prefer_first/get_next_available(list/points, atom/movable/target, transfer_type, datum/callback/availability_check)
- if(!length(points))
+/// Picks a next candidate from a list of eligible atoms.
+/datum/tasking_strategy/proc/get_next_candidate(list/candidates)
+ if(!length(candidates))
return null
+ return candidates[1]
- for(var/datum/interaction_point/point in points)
- if(availability_check.Invoke(point, target, transfer_type))
- return point
-
- return null
-
-/datum/tasking_strategy/round_robin
+// Moves through the list, skipping tasks that can't run.
+/datum/tasking_strategy/sequential
-/datum/tasking_strategy/round_robin/get_next_available(list/points, atom/movable/target, transfer_type, datum/callback/availability_check)
- if(!length(points))
+/datum/tasking_strategy/sequential/get_next_task(list/tasks, obj/machinery/big_manipulator/manipulator)
+ if(!length(tasks))
return null
-
- if(current_index < 1 || current_index > length(points))
+ if(current_index < 1 || current_index > length(tasks))
current_index = 1
-
- var/starting_index = current_index
-
+ var/start = current_index
while(TRUE)
- var/datum/interaction_point/point = points[current_index]
-
- if(point && availability_check.Invoke(point, target, transfer_type))
- advance_index(length(points))
- return point
-
- advance_index(length(points))
-
- if(current_index == starting_index)
- break
-
- return null
-
-/// Advances the index of the last point the manipulator interacted with to properly iterate in the correct order
-/datum/tasking_strategy/round_robin/proc/advance_index(list_length)
- current_index++
- if(current_index > list_length)
- current_index = 1
-
-/datum/tasking_strategy/strict_robin
-
-/datum/tasking_strategy/strict_robin/get_next_available(list/points, atom/movable/target, transfer_type, datum/callback/availability_check)
- if(!length(points))
- return null
-
- if(current_index < 1 || current_index > length(points))
- current_index = 1
-
- var/datum/interaction_point/point = points[current_index]
-
- if(point && availability_check.Invoke(point, target, transfer_type))
+ var/datum/manipulator_task/task = tasks[current_index]
current_index++
- if(current_index > length(points))
+ if(current_index > length(tasks))
current_index = 1
- return point
+ if(task.can_run(manipulator))
+ return task
+ if(current_index == start)
+ return null
- return null
-
-/// Picks a next candidate to run the checks for
-/datum/tasking_strategy/proc/get_next_candidate(list/candidates)
- if(!length(candidates))
- return null
- return candidates[1]
-
-/datum/tasking_strategy/prefer_first/get_next_candidate(list/candidates)
- if(!length(candidates))
- return null
- return candidates[1]
-
-/datum/tasking_strategy/round_robin/get_next_candidate(list/candidates)
+/datum/tasking_strategy/sequential/get_next_candidate(list/candidates)
if(!length(candidates))
return null
-
if(current_index < 1 || current_index > length(candidates))
current_index = 1
-
var/candidate = candidates[current_index]
current_index++
if(current_index > length(candidates))
current_index = 1
-
return candidate
-/datum/tasking_strategy/strict_robin/get_next_candidate(list/candidates)
- if(!length(candidates))
+// Stays on the current task until it can run.
+/datum/tasking_strategy/strict
+
+/datum/tasking_strategy/strict/get_next_task(list/tasks, obj/machinery/big_manipulator/manipulator)
+ if(!length(tasks))
+ return null
+ if(current_index < 1 || current_index > length(tasks))
+ current_index = 1
+ var/datum/manipulator_task/task = tasks[current_index]
+ if(!task.can_run(manipulator))
return null
+ current_index++
+ if(current_index > length(tasks))
+ current_index = 1
+ return task
+/datum/tasking_strategy/strict/get_next_candidate(list/candidates)
+ if(!length(candidates))
+ return null
if(current_index < 1 || current_index > length(candidates))
current_index = 1
-
var/candidate = candidates[current_index]
current_index++
if(current_index > length(candidates))
current_index = 1
-
return candidate
diff --git a/code/game/machinery/camera/camera.dm b/code/game/machinery/camera/camera.dm
index a35180969a4f..f234c8873531 100644
--- a/code/game/machinery/camera/camera.dm
+++ b/code/game/machinery/camera/camera.dm
@@ -334,20 +334,28 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
if(proximity_monitor)
drop_upgrade(proximity_monitor)
-/obj/machinery/camera/update_icon_state() //TO-DO: Make panel open states, xray camera, and indicator lights overlays instead.
- var/xray_module
- if(isXRay(TRUE))
- xray_module = "xray"
-
- if(!camera_enabled)
+/obj/machinery/camera/update_icon_state()
+ var/xray_module = isXRay(TRUE) ? "xray" : ""
+ if(!camera_enabled || (machine_stat & EMPED))
icon_state = "[xray_module][base_icon_state]_off"
return ..()
- if(machine_stat & EMPED)
- icon_state = "[xray_module][base_icon_state]_emp"
- return ..()
- icon_state = "[xray_module][base_icon_state][in_use_lights ? "_in_use" : ""]"
+ icon_state = "[xray_module][base_icon_state]"
return ..()
+/obj/machinery/camera/update_overlays()
+ . = ..()
+ if(panel_open)
+ . += "[base_icon_state]_panel"
+
+ var/xray_module = isXRay(TRUE) ? "xray" : ""
+ if(machine_stat & EMPED)
+ . += "[xray_module][base_icon_state]_emp"
+ . += emissive_appearance(icon, "[xray_module][base_icon_state]_emp", src, alpha = src.alpha)
+ return
+ if(camera_enabled)
+ . += "[xray_module][base_icon_state]_[in_use_lights ? "in_use" : "on"]"
+ . += emissive_appearance(icon, "[xray_module][base_icon_state]_[in_use_lights ? "in_use" : "on"]", src, alpha = src.alpha)
+
/obj/machinery/camera/proc/toggle_cam(mob/user, displaymessage = TRUE)
camera_enabled = !camera_enabled
if(can_use())
diff --git a/code/game/machinery/computer/dna_console.dm b/code/game/machinery/computer/dna_console.dm
index d336c2fc9f82..86cb2acec3de 100644
--- a/code/game/machinery/computer/dna_console.dm
+++ b/code/game/machinery/computer/dna_console.dm
@@ -829,7 +829,7 @@
// Create a new DNA Injector and add the appropriate mutations to it
var/obj/item/dnainjector/activator/injector = new /obj/item/dnainjector/activator(loc)
- injector.add_mutations += mutation.make_copy()
+ LAZYADD(injector.add_mutations, mutation.make_copy())
var/is_activator = text2num(params["is_activator"])
@@ -1303,93 +1303,19 @@
// number later
// params["type"] - Type of injector to create
// Expected results:
- // "ue" - Unique Enzyme, changes name and blood type
+ // "ue" - Unique Enzyme, changes name and blood type
// "ui" - Unique Identity, changes looks
// "uf" - Unique Features, changes mutant bodyparts and mutcolors
// "mixed" - Combination of both ue and ui
if("makeup_injector")
if(!COOLDOWN_FINISHED(src, enzyme_copy_timer))
return
- // Convert the index to a number and clamp within the array range, then
- // copy the data from the disk to that buffer
- var/buffer_index = text2num(params["index"])
- buffer_index = clamp(buffer_index, 1, NUMBER_OF_BUFFERS)
- var/list/buffer_slot = genetic_makeup_buffer[buffer_index]
-
- // GUARD CHECK - This shouldn't be possible to execute this on a null
- // buffer. Unexpected resut
- if(!istype(buffer_slot))
+ // Convert the index to a number and clamp within the array range, then copy the data from the disk to that buffer
+ var/buffer_index = clamp(text2num(params["index"]), 1, NUMBER_OF_BUFFERS)
+ if(!make_cosmetic_dna_injector(dna_injector_type_to_flag(params["type"]), genetic_makeup_buffer[buffer_index]))
+ to_chat(usr, span_warning("Genetic data corrupted, unable to create injector."))
return
-
- var/type = params["type"]
- var/obj/item/dnainjector/timed/I
-
- switch(type)
- if("ui")
- // GUARD CHECK - There's currently no way to save partial genetic data.
- // However, if this is the case, we can't make a complete injector and
- // this catches that edge case
- if(!buffer_slot["UI"])
- to_chat(usr,span_warning("Genetic data corrupted, unable to create injector."))
- return
-
- I = new /obj/item/dnainjector/timed(loc)
- I.fields = list("UI"=buffer_slot["UI"])
-
- // If there is a connected scanner, we can use its upgrades to reduce
- // the genetic damage generated by this injector
- if(scanner_operational())
- I.damage_coeff = connected_scanner.damage_coeff
- if("ue")
- // GUARD CHECK - There's currently no way to save partial genetic data.
- // However, if this is the case, we can't make a complete injector and
- // this catches that edge case
- if(!buffer_slot["name"] || !buffer_slot["UE"] || !buffer_slot["blood_type"])
- to_chat(usr,span_warning("Genetic data corrupted, unable to create injector."))
- return
-
- I = new /obj/item/dnainjector/timed(loc)
- I.fields = list("name"=buffer_slot["name"], "UE"=buffer_slot["UE"], "blood_type"=buffer_slot["blood_type"])
-
- // If there is a connected scanner, we can use its upgrades to reduce
- // the genetic damage generated by this injector
- if(scanner_operational())
- I.damage_coeff = connected_scanner.damage_coeff
- if("uf")
- // GUARD CHECK - There's currently no way to save partial genetic data.
- // However, if this is the case, we can't make a complete injector and
- // this catches that edge case
- if(!buffer_slot["name"] || !buffer_slot["UF"] || !buffer_slot["blood_type"])
- to_chat(usr,span_warning("Genetic data corrupted, unable to create injector."))
- return
-
- I = new /obj/item/dnainjector/timed(loc)
- I.fields = list("name"=buffer_slot["name"], "UF"=buffer_slot["UF"])
-
- // If there is a connected scanner, we can use its upgrades to reduce
- // the genetic damage generated by this injector
- if(scanner_operational())
- I.damage_coeff = connected_scanner.damage_coeff
- if("mixed")
- // GUARD CHECK - There's currently no way to save partial genetic data.
- // However, if this is the case, we can't make a complete injector and
- // this catches that edge case
- if(!buffer_slot["UI"] || !buffer_slot["name"] || !buffer_slot["UE"] || !buffer_slot["UF"] || !buffer_slot["blood_type"])
- to_chat(usr,span_warning("Genetic data corrupted, unable to create injector."))
- return
-
- I = new /obj/item/dnainjector/timed(loc)
- I.fields = list("UI"=buffer_slot["UI"],"name"=buffer_slot["name"], "UE"=buffer_slot["UE"], "UF"=buffer_slot["UF"], "blood_type"=buffer_slot["blood_type"])
-
- // If there is a connected scanner, we can use its upgrades to reduce
- // the genetic damage generated by this injector
- if(scanner_operational())
- I.damage_coeff = connected_scanner.damage_coeff
-
- // If we successfully created an injector, don't forget to set the new
- // ready timer.
- if(I)
- injector_ready = world.time + MISC_INJECTOR_TIMEOUT
+ injector_ready = world.time + MISC_INJECTOR_TIMEOUT
if(connected_scanner)
connected_scanner.use_energy(connected_scanner.active_power_usage)
else
@@ -1579,7 +1505,7 @@
// new injector
var/total_stability = 0
for(var/datum/mutation/mutation as anything in injector_selection[inj_name])
- injector.add_mutations += mutation.make_copy()
+ LAZYADD(injector.add_mutations, mutation.make_copy())
total_stability += mutation.instability
// Force apply any mutations, this is functionality similar to mutators
@@ -1785,12 +1711,90 @@
return TRUE
return FALSE
+
/**
* Checks if there is a connected DNA Scanner that is operational
*/
/obj/machinery/computer/dna_console/proc/scanner_operational()
return connected_scanner?.is_operational
+/**
+ * Gets the damage coefficient of the connected DNA Scanner, or 1 if there isn't an operational one
+ */
+/obj/machinery/computer/dna_console/proc/get_injector_damage_coeff()
+ if(scanner_operational())
+ return connected_scanner.damage_coeff
+ return 1
+
+/// Copy UI to the dna injector
+#define DNA_INJECTOR_FLAG_UI (1<<0)
+/// Copy UE to the dna injector
+#define DNA_INJECTOR_FLAG_UE (1<<1)
+/// Copy UF to the dna injector
+#define DNA_INJECTOR_FLAG_UF (1<<2)
+/// Copy name to the dna injector
+#define DNA_INJECTOR_FLAG_NAME (1<<3)
+/// Copy blood type to the dna injector
+#define DNA_INJECTOR_FLAG_BLOOD (1<<4)
+
+/**
+ * Converts a string (from tgui) to a series of flags determine what we should put in a DNA Injector
+ */
+/obj/machinery/computer/dna_console/proc/dna_injector_type_to_flag(injector_type)
+ switch(injector_type)
+ if("ui")
+ return DNA_INJECTOR_FLAG_UI
+ if("ue")
+ return DNA_INJECTOR_FLAG_UE | DNA_INJECTOR_FLAG_NAME | DNA_INJECTOR_FLAG_BLOOD
+ if("uf")
+ return DNA_INJECTOR_FLAG_UF | DNA_INJECTOR_FLAG_NAME
+ if("mixed")
+ return ALL
+ return NONE
+
+/**
+ * Pass an injector flag and a genetic makeup buffer slot to create a DNA Injector
+ */
+/obj/machinery/computer/dna_console/proc/make_cosmetic_dna_injector(dna_flag, list/buffer_slot = list())
+ if(!dna_flag || !length(buffer_slot))
+ return FALSE
+
+ var/datum/dna/stored_dna = new()
+
+ if(dna_flag & DNA_INJECTOR_FLAG_NAME)
+ if(!buffer_slot["name"])
+ return FALSE
+ stored_dna.real_name = buffer_slot["name"]
+
+ if(dna_flag & DNA_INJECTOR_FLAG_BLOOD)
+ if(!buffer_slot["blood_type"])
+ return FALSE
+ stored_dna.blood_type = buffer_slot["blood_type"]
+
+ if(dna_flag & DNA_INJECTOR_FLAG_UI)
+ if(!buffer_slot["UI"])
+ return FALSE
+ stored_dna.unique_identity = buffer_slot["UI"]
+
+ if(dna_flag & DNA_INJECTOR_FLAG_UE)
+ if(!buffer_slot["UE"])
+ return FALSE
+ stored_dna.unique_enzymes = buffer_slot["UE"]
+
+ if(dna_flag & DNA_INJECTOR_FLAG_UF)
+ if(!buffer_slot["UF"])
+ return FALSE
+ stored_dna.unique_features = buffer_slot["UF"]
+
+ new /obj/item/dnainjector/timed(loc, stored_dna, get_injector_damage_coeff())
+ return TRUE
+
+#undef DNA_INJECTOR_FLAG_UI
+#undef DNA_INJECTOR_FLAG_UE
+#undef DNA_INJECTOR_FLAG_UF
+#undef DNA_INJECTOR_FLAG_NAME
+#undef DNA_INJECTOR_FLAG_BLOOD
+
/**
* Checks if there is a valid DNA Scanner occupant for genetic modification
*
diff --git a/code/game/machinery/dna_infuser/organ_sets/gondola_organs.dm b/code/game/machinery/dna_infuser/organ_sets/gondola_organs.dm
index b3d400ac39d6..d8c0ee95f535 100644
--- a/code/game/machinery/dna_infuser/organ_sets/gondola_organs.dm
+++ b/code/game/machinery/dna_infuser/organ_sets/gondola_organs.dm
@@ -57,6 +57,7 @@ Fluoride Stare: After someone says 5 words, blah blah blah...
post_init_icon_state = "tongue"
greyscale_config = /datum/greyscale_config/mutant_organ
greyscale_colors = GONDOLA_COLORS
+ speakable_with = FALSE
organ_traits = list(TRAIT_MUTE)
/obj/item/organ/tongue/gondola/Initialize(mapload)
diff --git a/code/game/machinery/dna_infuser/organ_sets/stoat_organs.dm b/code/game/machinery/dna_infuser/organ_sets/stoat_organs.dm
index 623f8607e9a0..90c5c2c1f4f5 100644
--- a/code/game/machinery/dna_infuser/organ_sets/stoat_organs.dm
+++ b/code/game/machinery/dna_infuser/organ_sets/stoat_organs.dm
@@ -139,7 +139,7 @@
return
var/mob/living/carbon/human/human_owner = organ_owner
mob_base_height = human_owner.get_base_mob_height()
- human_owner.set_mob_height(HUMAN_HEIGHT_TALLER, update_dna = FALSE)
+ human_owner.set_mob_height(HUMAN_HEIGHT_TALLEST, update_dna = FALSE)
/obj/item/organ/heart/stoat/on_mob_remove(mob/living/carbon/organ_owner, special, movement_flags)
. = ..()
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 31ea87918487..57dffa377e5f 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -322,6 +322,10 @@
return
return ..()
+/obj/machinery/door/allowed(mob/accessor)
+ return ..() || emergency
+
+/// A mob is trying to open or close the door
/obj/machinery/door/proc/try_to_activate_door(mob/user, access_bypass = FALSE, bumped = FALSE)
add_fingerprint(user)
if(operating || (obj_flags & EMAGGED))
@@ -333,24 +337,8 @@
if(elevator_mode && elevator_status != LIFT_PLATFORM_UNLOCKED)
return
- var/access_check = access_bypass
- if(emergency)
- access_check = TRUE
- else if(unrestricted_side(user) && !delayed_unres_open)
- access_check = TRUE
- else if(!requiresID())
- access_check = TRUE
- else if(allowed(user)) // You
- access_check = TRUE
- else for(var/mob/living/human_backpack in user.buckled_mobs)
- if(allowed(human_backpack)) // Your partner in crime
- access_check = TRUE
- break
-
- if(!access_check && unrestricted_side(user) && attempt_delayed_unres_open(user))
- access_check = TRUE
-
- if(access_check)
+ // note: if the ID wire is cut no ID cards are checked at all! (This is intentional!)
+ if(access_bypass || (requiresID() && user_can_activate_door(user)))
if(density)
open()
else
@@ -360,6 +348,18 @@
else if(!operating && density)
run_animation(DOOR_DENY_ANIMATION)
+/// Used in try_to_activate_door
+/obj/machinery/door/proc/user_can_activate_door(mob/user)
+ PRIVATE_PROC(TRUE)
+ if(allowed(user))
+ return TRUE
+ for(var/mob/living/human_backpack in user.buckled_mobs)
+ if(allowed(human_backpack))
+ return TRUE
+ if(unrestricted_side(user))
+ return !delayed_unres_open || attempt_delayed_unres_open(user)
+ return FALSE
+
/// Allows for specific side of airlocks to be unrestricted (IE, can exit maint freely, but need access to enter)
/obj/machinery/door/proc/unrestricted_side(mob/opener)
return get_dir(src, opener) & unres_sides
diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm
index ee4158538b35..01ec6e7b66cb 100644
--- a/code/game/machinery/doors/windowdoor.dm
+++ b/code/game/machinery/doors/windowdoor.dm
@@ -535,6 +535,18 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/brigdoor/security/holding
icon_state = "rightsecure"
base_state = "rightsecure"
+/obj/machinery/door/window/brigdoor/security/holodeck
+ name = "cell door"
+ desc = "For keeping in criminal scum."
+ req_one_access = COMMON_ACCESS
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/brigdoor/security/holodeck/left, 0)
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/brigdoor/security/holodeck/right, 0)
+
+/obj/machinery/door/window/brigdoor/security/holodeck/right
+ icon_state = "rightsecure"
+ base_state = "rightsecure"
+
/*
* Subtype used in unit tests to ensure instant windoor open/close
*/
diff --git a/code/game/machinery/experimental_cloner/experimental_cloner_pod.dm b/code/game/machinery/experimental_cloner/experimental_cloner_pod.dm
index 460786c9ea87..617d013fab34 100644
--- a/code/game/machinery/experimental_cloner/experimental_cloner_pod.dm
+++ b/code/game/machinery/experimental_cloner/experimental_cloner_pod.dm
@@ -163,7 +163,7 @@ GLOBAL_VAR_INIT(experimental_cloner_fuckup_chance, 50)
if (prob(75))
var/static/list/permitted_heights = list(HUMAN_HEIGHT_SHORTEST, HUMAN_HEIGHT_SHORT, HUMAN_HEIGHT_MEDIUM, HUMAN_HEIGHT_TALL, HUMAN_HEIGHT_TALLER, HUMAN_HEIGHT_TALLEST)
- new_clone.dna.remove_mutation(/datum/mutation/dwarfism, list(MUTATION_SOURCE_ACTIVATED, MUTATION_SOURCE_MUTATOR))
+ new_clone.dna.remove_mutation(/datum/mutation/dwarfism, GLOB.standard_mutation_sources)
new_clone.set_mob_height(pick(permitted_heights - loaded_record.height)) // To differentiate the clones
return new_clone
diff --git a/code/game/machinery/launch_pad.dm b/code/game/machinery/launch_pad.dm
index 34a52615623c..ed60bd83539a 100644
--- a/code/game/machinery/launch_pad.dm
+++ b/code/game/machinery/launch_pad.dm
@@ -91,7 +91,12 @@
/obj/machinery/launchpad/update_icon_state()
. = ..()
- icon_state = panel_open ? "[base_icon_state]-open" : base_icon_state
+ if(machine_stat & (BROKEN|NOPOWER))
+ icon_state = "[base_icon_state]-off"
+ else if(state_open)
+ icon_state = "[base_icon_state]-open"
+ else
+ icon_state = "[base_icon_state]-idle"
/obj/machinery/launchpad/attack_ghost(mob/dead/observer/ghost)
. = ..()
@@ -399,11 +404,18 @@
if("move_pos")
var/plus_x = text2num(params["x"])
var/plus_y = text2num(params["y"])
- // sanitizes our ranges for us
- our_pad.set_offset(
- x = our_pad.x_offset + plus_x,
- y = our_pad.y_offset + plus_y
- )
+ if(plus_x || plus_y)
+ // sanitizes our ranges for us
+ our_pad.set_offset(
+ x = our_pad.x_offset + plus_x,
+ y = our_pad.y_offset + plus_y,
+ )
+ else
+ // for resetting
+ our_pad.set_offset(
+ x = 0,
+ y = 0,
+ )
. = TRUE
if("rename")
. = TRUE
diff --git a/code/game/machinery/mining_weather_monitor.dm b/code/game/machinery/mining_weather_monitor.dm
index c057b44f56cd..402a40dd8f16 100644
--- a/code/game/machinery/mining_weather_monitor.dm
+++ b/code/game/machinery/mining_weather_monitor.dm
@@ -346,8 +346,8 @@ GLOBAL_LIST_EMPTY(weather_towers)
/// Return a list of weather typepaths that this tower can summon when given a weather core.
/obj/machinery/power/weather_tower/proc/get_summonable_weather_types()
. = list(
- /datum/weather/ash_storm,
- /datum/weather/rain_storm,
+ /datum/weather/particle/ash_storm,
+ /datum/weather/particle/rain_storm,
/datum/weather/sand_storm,
/datum/weather/snow_storm,
)
diff --git a/code/game/machinery/prisongate.dm b/code/game/machinery/prisongate.dm
index 88cb40dd50f7..688ddc087022 100644
--- a/code/game/machinery/prisongate.dm
+++ b/code/game/machinery/prisongate.dm
@@ -1,4 +1,7 @@
#define SPAM_CD (3 SECONDS)
+#define ALLOW_EXIT "allow_exit"
+#define BLOCK_EXIT "block_exit"
+#define SKIP_EXIT null
/obj/machinery/prisongate
name = "prison gate scanner"
@@ -14,6 +17,7 @@
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.05
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 0.03
anchored = TRUE
+ req_one_access = list(ACCESS_BRIG)
/// dictates whether the gate barrier is up or not
var/gate_active = TRUE
COOLDOWN_DECLARE(spam_cooldown_time)
@@ -55,39 +59,74 @@
COOLDOWN_START(src, spam_cooldown_time, SPAM_CD)
return FALSE
var/mob/living/carbon/the_toucher = gate_toucher
- if(gate_active == FALSE)
+ if(!gate_active)
return TRUE
- for(var/obj/item/card/id/regular_id in the_toucher.get_all_contents())
- var/list/id_access = regular_id.GetAccess()
- if(ACCESS_BRIG in id_access)
- if(COOLDOWN_FINISHED(src, spam_cooldown_time))
- say("Brig clearance detected. Access granted.")
- playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
- COOLDOWN_START(src, spam_cooldown_time, SPAM_CD)
- return TRUE
- for(var/obj/item/card/id/advanced/prisoner/prison_id in the_toucher.get_all_contents())
- if(!prison_id.timed)
- continue
- if(prison_id.time_to_assign)
- say("Prison ID with active sentence detected. Please enjoy your stay in our corporate rehabilitation center, [prison_id.registered_name]!")
- playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
- prison_id.time_left = prison_id.time_to_assign
- prison_id.time_to_assign = initial(prison_id.time_to_assign)
- prison_id.start_timer()
- return TRUE
- if(prison_id.time_left <= 0)
- say("Prison ID with served sentence detected. Access granted.")
- prison_id.timed = FALSE //disables the id check from earlier so you can't just throw it back into perma for mass escapes
+ if(allowed(the_toucher))
+ if(COOLDOWN_FINISHED(src, spam_cooldown_time))
+ say("Brig clearance detected. Access granted.")
playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
- return TRUE
+ COOLDOWN_START(src, spam_cooldown_time, SPAM_CD)
+ return TRUE
+
+ else if(the_toucher.pulledby && allowed(the_toucher.pulledby))
if(COOLDOWN_FINISHED(src, spam_cooldown_time))
- say("Prison ID with ongoing sentence detected. Access denied.")
- playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, FALSE)
+ say("Brig escort detected. Access granted.")
+ playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
COOLDOWN_START(src, spam_cooldown_time, SPAM_CD)
- return FALSE
+ return TRUE
+
+ for(var/obj/item/card/id/advanced/prisoner/prison_id in the_toucher.get_all_contents())
+ switch(allow_prisoner_id(prison_id))
+ if(ALLOW_EXIT)
+ return TRUE
+ if(BLOCK_EXIT)
+ return FALSE
+
if(COOLDOWN_FINISHED(src, spam_cooldown_time))
to_chat(the_toucher, span_warning("You try to push through the hardlight barrier with little effect."))
COOLDOWN_START(src, spam_cooldown_time, SPAM_CD)
return FALSE
+/obj/machinery/prisongate/proc/allow_prisoner_id(obj/item/card/id/advanced/prisoner/prison_id)
+ if(!prison_id.timed)
+ return SKIP_EXIT
+ if(prison_id.time_to_assign)
+ say("Prison ID with active sentence detected. Please enjoy your stay in our corporate rehabilitation center, [prison_id.registered_name]!")
+ playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
+ prison_id.time_left = prison_id.time_to_assign
+ prison_id.time_to_assign = initial(prison_id.time_to_assign)
+ prison_id.start_timer()
+ return ALLOW_EXIT
+ if(prison_id.time_left <= 0)
+ say("Prison ID with served sentence detected. Access granted.")
+ prison_id.timed = FALSE //disables the id check from earlier so you can't just throw it back into perma for mass escapes
+ playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
+ return ALLOW_EXIT
+ if(COOLDOWN_FINISHED(src, spam_cooldown_time))
+ say("Prison ID with ongoing sentence detected. Access denied.")
+ playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, FALSE)
+ COOLDOWN_START(src, spam_cooldown_time, SPAM_CD)
+ return BLOCK_EXIT
+
+/obj/machinery/prisongate/labour
+ name = "labor camp gate scanner"
+
+/obj/machinery/prisongate/labour/allow_prisoner_id(obj/item/card/id/advanced/prisoner/prison_id)
+ if(!prison_id.goal)
+ return SKIP_EXIT
+ if(prison_id.points >= prison_id.goal)
+ if(COOLDOWN_FINISHED(src, spam_cooldown_time))
+ say("All points served. Access granted.")
+ playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
+ COOLDOWN_START(src, spam_cooldown_time, SPAM_CD)
+ return ALLOW_EXIT
+ if(COOLDOWN_FINISHED(src, spam_cooldown_time))
+ say("[prison_id.goal - prison_id.points] point\s remaining on sentence. Access denied.")
+ playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, FALSE)
+ COOLDOWN_START(src, spam_cooldown_time, SPAM_CD)
+ return BLOCK_EXIT
+
#undef SPAM_CD
+#undef ALLOW_EXIT
+#undef BLOCK_EXIT
+#undef SKIP_EXIT
diff --git a/code/game/machinery/quantum_pad.dm b/code/game/machinery/quantum_pad.dm
index 1bdeb615f4bb..3892a16cb056 100644
--- a/code/game/machinery/quantum_pad.dm
+++ b/code/game/machinery/quantum_pad.dm
@@ -94,7 +94,10 @@
/obj/machinery/quantumpad/update_icon_state()
. = ..()
- icon_state = panel_open ? "[base_icon_state]-idle-open" : "[base_icon_state]-idle"
+ if(machine_stat & (BROKEN|NOPOWER) || state_open)
+ icon_state = "[base_icon_state]-idle-open"
+ else
+ icon_state = "[base_icon_state]-idle"
/obj/machinery/quantumpad/interact(mob/user, obj/machinery/quantumpad/target_pad = linked_pad)
if(QDELETED(target_pad))
diff --git a/code/game/machinery/requests_console.dm b/code/game/machinery/requests_console.dm
index 018bfd65e500..1bc8c9a4fdab 100644
--- a/code/game/machinery/requests_console.dm
+++ b/code/game/machinery/requests_console.dm
@@ -389,7 +389,7 @@ GLOBAL_LIST_EMPTY(req_console_ckey_departments)
/obj/machinery/requests_console/ui_data(mob/user)
var/list/data = list()
- data["is_admin_ghost_ai"] = isAdminGhostAI()
+ data["is_admin_ghost_ai"] = isAdminGhostAI(user)
data["can_send_announcements"] = can_send_announcements
data["department"] = department
data["emergency"] = emergency
diff --git a/code/game/machinery/syndicatebeacon.dm b/code/game/machinery/syndicatebeacon.dm
index 97ac006bcd50..7eec0b16f40c 100644
--- a/code/game/machinery/syndicatebeacon.dm
+++ b/code/game/machinery/syndicatebeacon.dm
@@ -24,10 +24,6 @@
if(user)
to_chat(user, span_notice("The connected wire doesn't have enough current."))
return
- for (var/datum/component/singularity/singulo as anything in GLOB.singularities)
- var/atom/singulo_atom = singulo.parent
- if(singulo_atom.z == z)
- singulo.target = src
icon_state = "[icontype]1"
active = TRUE
if(user)
@@ -35,8 +31,7 @@
/obj/machinery/power/singularity_beacon/proc/Deactivate(mob/user = null)
- for(var/_singulo in GLOB.singularities)
- var/datum/component/singularity/singulo = _singulo
+ for(var/datum/component/singularity/singulo as anything in GLOB.singularities)
if(singulo.target == src)
singulo.target = null
icon_state = "[icontype]0"
@@ -97,10 +92,10 @@
add_load(energy_used)
if(COOLDOWN_FINISHED(src, singularity_beacon_cd))
COOLDOWN_START(src, singularity_beacon_cd, 8 SECONDS)
- for(var/_singulo_component in GLOB.singularities)
- var/datum/component/singularity/singulo_component = _singulo_component
+ for(var/datum/component/singularity/singulo_component as anything in GLOB.singularities)
var/atom/singulo = singulo_component.parent
if(singulo.z == z)
+ singulo_component.target = src
say("[singulo] is now [get_dist(src,singulo)] standard lengths away to the [dir2text(get_dir(src,singulo))]")
else
Deactivate()
diff --git a/code/game/machinery/syndicatebomb.dm b/code/game/machinery/syndicatebomb.dm
index c09b74cab85e..942344a30d62 100644
--- a/code/game/machinery/syndicatebomb.dm
+++ b/code/game/machinery/syndicatebomb.dm
@@ -20,7 +20,7 @@
/// What is the lowest amount of time we can set the timer to?
var/minimum_timer = SYNDIEBOMB_MIN_TIMER_SECONDS
/// What is the highest amount of time we can set the timer to?
- var/maximum_timer = 100 MINUTES
+ var/maximum_timer = 100*60 // 100 MINUTES // not using the MINUTES define because those are for deciseconds
/// What is the default amount of time we set the timer to?
var/timer_set = SYNDIEBOMB_MIN_TIMER_SECONDS
/// Can we be unanchored?
diff --git a/code/game/objects/effects/anomalies/anomalies_weather.dm b/code/game/objects/effects/anomalies/anomalies_weather.dm
index ea407acfa3af..17e6f415af58 100644
--- a/code/game/objects/effects/anomalies/anomalies_weather.dm
+++ b/code/game/objects/effects/anomalies/anomalies_weather.dm
@@ -55,7 +55,7 @@
/obj/effect/anomaly/weather/proc/select_weather()
return pick(
- /datum/weather/rain_storm,
+ /datum/weather/particle/rain_storm,
/datum/weather/snow_storm,
/datum/weather/sand_storm,
)
@@ -97,4 +97,4 @@
// maybe we can put acid rain in this later?
// though it'd feel unfair if it showed up and immediately dumped acid on people.
// we would need an even longer telegraphing time for that
- weather_type = /datum/weather/rain_storm
+ weather_type = /datum/weather/particle/rain_storm
diff --git a/code/game/objects/effects/decals/cleanable.dm b/code/game/objects/effects/decals/cleanable.dm
index 4e7ca61d0dd0..91e858273ec2 100644
--- a/code/game/objects/effects/decals/cleanable.dm
+++ b/code/game/objects/effects/decals/cleanable.dm
@@ -82,12 +82,16 @@
RETURN_TYPE(/datum/reagents)
if (reagents)
return reagents
+ return init_reagents(decal_reagent, reagent_amount)
- if (!decal_reagent)
+/obj/effect/decal/cleanable/proc/init_reagents(reagent = null, amount = null)
+ RETURN_TYPE(/datum/reagents)
+
+ if (!reagent)
return
- create_reagents(reagent_amount)
- reagents.add_reagent(decal_reagent, reagent_amount)
+ create_reagents(amount)
+ reagents.add_reagent(reagent, amount)
return reagents
/obj/effect/decal/cleanable/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
diff --git a/code/game/objects/effects/decals/cleanable/blood.dm b/code/game/objects/effects/decals/cleanable/blood.dm
index 7dcef673bd9d..82253bfabf1a 100644
--- a/code/game/objects/effects/decals/cleanable/blood.dm
+++ b/code/game/objects/effects/decals/cleanable/blood.dm
@@ -134,7 +134,9 @@
create_reagents(round(bloodiness * BLOOD_TO_UNITS_MULTIPLIER, CHEMICAL_VOLUME_ROUNDING))
var/num_reagents = length(reagents_to_add)
for(var/reagent_type in reagents_to_add)
- reagents.add_reagent(reagent_type, round(bloodiness * BLOOD_TO_UNITS_MULTIPLIER / num_reagents, CHEMICAL_VOLUME_ROUNDING))
+ reagents.add_reagent(reagent_type = reagent_type,
+ amount = round(bloodiness * BLOOD_TO_UNITS_MULTIPLIER / num_reagents, CHEMICAL_VOLUME_ROUNDING),
+ data = ispath(reagent_type, /datum/reagent/blood) ? list("blood_DNA" = pick(blood_DNA)) : null)
return reagents
/obj/effect/decal/cleanable/blood/replace_decal(obj/effect/decal/cleanable/blood/merger)
@@ -540,13 +542,7 @@
/obj/effect/decal/cleanable/blood/gibs/lazy_init_reagents()
if (reagents)
return reagents
-
- if (!decal_reagent)
- return
-
- create_reagents(reagent_amount)
- reagents.add_reagent(decal_reagent, reagent_amount)
- return reagents
+ return init_reagents(decal_reagent, reagent_amount)
/obj/effect/decal/cleanable/blood/gibs/update_overlays()
. = ..()
diff --git a/code/game/objects/effects/decals/cleanable/mess.dm b/code/game/objects/effects/decals/cleanable/mess.dm
index bcb485e56f48..2a2c80405e3a 100644
--- a/code/game/objects/effects/decals/cleanable/mess.dm
+++ b/code/game/objects/effects/decals/cleanable/mess.dm
@@ -291,7 +291,7 @@ GLOBAL_LIST_EMPTY(nebula_vomits)
pixel_x = rand(-10, 10)
pixel_y = rand(-10, 10)
if(!isnull(oldname))
- desc = "The sad remains of what used to be [oldname]"
+ desc = "The sad remains of what used to be \a [oldname]."
. = ..()
/obj/effect/decal/cleanable/glitter
diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
index 468654f4d5a0..cea1190aa66d 100644
--- a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
@@ -31,12 +31,17 @@
var/allow_duplicate_results = TRUE
/// The amount of time this foam stick around for before it dissipates.
var/lifetime = 8 SECONDS
+ /// The orignal value of lifetime
+ var/original_lifetime = 8 SECONDS
/// Whether or not this foam should be slippery.
var/slippery_foam = TRUE
-/obj/effect/particle_effect/fluid/foam/Initialize(mapload)
+/obj/effect/particle_effect/fluid/foam/Initialize(mapload, fluid_group, source, lifetime, slippery)
. = ..()
+ src.lifetime = lifetime ? lifetime : src.lifetime
+ src.original_lifetime = src.lifetime
+ src.slippery_foam = !isnull(slippery) ? slippery : slippery_foam
if(slippery_foam)
AddComponent(/datum/component/slippery, 100, can_slip_callback = CALLBACK(src, PROC_REF(try_slip)))
if(HAS_TRAIT(loc, TRAIT_ELEVATED_TURF))
@@ -165,7 +170,7 @@
continue
foam_mob(foaming, seconds_per_tick)
- var/obj/effect/particle_effect/fluid/foam/spread_foam = new type(spread_turf, group, src)
+ var/obj/effect/particle_effect/fluid/foam/spread_foam = new type(spread_turf, group, src, src.original_lifetime, src.slippery_foam)
reagents.trans_to(spread_foam, reagents.total_volume, copy_only = TRUE)
spread_foam.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
spread_foam.result_type = result_type
@@ -219,7 +224,7 @@
/// What type of thing the foam should leave behind when it dissipates.
var/atom/movable/result_type = null
-/datum/effect_system/fluid_spread/foam/New(turf/location, range = 1, amount = null, atom/holder = null, datum/reagents/carry = null, result_type = null, stop_reactions = FALSE, reagent_scale = FOAM_REAGENT_SCALE)
+/datum/effect_system/fluid_spread/foam/New(turf/location, range = 1, amount = null, atom/holder = null, datum/reagents/carry = null, result_type = null, stop_reactions = FALSE, reagent_scale = FOAM_REAGENT_SCALE,)
. = ..()
chemholder = new(1000, NO_REACT)
carry?.trans_to(chemholder, carry.total_volume, no_react = stop_reactions, copy_only = TRUE)
@@ -231,8 +236,8 @@
QDEL_NULL(chemholder)
return ..()
-/datum/effect_system/fluid_spread/foam/start(log = FALSE)
- var/obj/effect/particle_effect/fluid/foam/foam = new effect_type(location, new /datum/fluid_group(amount))
+/datum/effect_system/fluid_spread/foam/start(log = FALSE, lifetime, slippery)
+ var/obj/effect/particle_effect/fluid/foam/foam = new effect_type(location, new /datum/fluid_group(amount), null, lifetime, slippery)
var/foamcolor = mix_color_from_reagents(chemholder.reagent_list)
if(reagent_scale > 1) // Make room in case we were created by a particularly stuffed payload.
foam.reagents.maximum_volume *= reagent_scale
diff --git a/code/game/objects/effects/landmarks.dm b/code/game/objects/effects/landmarks.dm
index 77ae51ea07ca..db968042c6d9 100644
--- a/code/game/objects/effects/landmarks.dm
+++ b/code/game/objects/effects/landmarks.dm
@@ -264,36 +264,64 @@ INITIALIZE_IMMEDIATE(/obj/effect/landmark)
GLOB.wizardstart += loc
return INITIALIZE_HINT_QDEL
-/obj/effect/landmark/start/nukeop
+/// Places to put midround nukeops (normal)
+GLOBAL_LIST_EMPTY(nukeop_base_start)
+/// Places to put midround nukeops (leader)
+GLOBAL_LIST_EMPTY(nukeop_base_leader_start)
+/// Places to put nukeops overwatch agents
+GLOBAL_LIST_EMPTY(nukeop_base_overwatch_start)
+/// Places to spawn nukeops on roundstart
+GLOBAL_LIST_EMPTY(nukeop_elevator_start)
+
+/obj/effect/landmark/start/nukeop_base
name = "nukeop"
icon = 'icons/effects/landmarks_static.dmi'
icon_state = "snukeop_spawn"
-/obj/effect/landmark/start/nukeop/Initialize(mapload)
+/obj/effect/landmark/start/nukeop_base/Initialize(mapload)
..()
- GLOB.nukeop_start += loc
+ var/list/add_to = get_global_list()
+ add_to += loc
return INITIALIZE_HINT_QDEL
-/obj/effect/landmark/start/nukeop_leader
+/obj/effect/landmark/start/nukeop_base/proc/get_global_list()
+ return GLOB.nukeop_base_start
+
+/obj/effect/landmark/start/nukeop_base/leader
name = "nukeop leader"
icon = 'icons/effects/landmarks_static.dmi'
icon_state = "snukeop_leader_spawn"
-/obj/effect/landmark/start/nukeop_leader/Initialize(mapload)
- ..()
- GLOB.nukeop_leader_start += loc
- return INITIALIZE_HINT_QDEL
+/obj/effect/landmark/start/nukeop_base/leader/get_global_list()
+ return GLOB.nukeop_base_leader_start
-/obj/effect/landmark/start/nukeop_overwatch
+/obj/effect/landmark/start/nukeop_base/overwatch
name = "nukeop overwatch"
icon = 'icons/effects/landmarks_static.dmi'
icon_state = "snukeop_leader_spawn"
-/obj/effect/landmark/start/nukeop_overwatch/Initialize(mapload)
- ..()
- GLOB.nukeop_overwatch_start += loc
+/obj/effect/landmark/start/nukeop_base/overwatch/get_global_list()
+ return GLOB.nukeop_base_overwatch_start
+
+/obj/effect/landmark/start/nukeop_elevator
+ name = "nukeop (elevator)"
+ icon = /obj/effect/landmark/start/nukeop_base::icon
+ icon_state = /obj/effect/landmark/start/nukeop_base::icon_state
+
+/obj/effect/landmark/start/nukeop_elevator/Initialize(mapload)
+ . = ..()
+ GLOB.nukeop_elevator_start += loc
return INITIALIZE_HINT_QDEL
+/obj/effect/landmark/nukeop_elevator
+ icon_state = "x"
+
+/obj/effect/landmark/nukeop_elevator/interior
+ name = "nukeop elevator interior top right corner"
+
+/obj/effect/landmark/nukeop_elevator/exterior
+ name = "nukeop elevator exterior top right corner"
+
// Must be immediate because players will
// join before SSatom initializes everything.
INITIALIZE_IMMEDIATE(/obj/effect/landmark/start/new_player)
diff --git a/code/game/objects/effects/landmarks/atmospherics_sanity_landmarks.dm b/code/game/objects/effects/landmarks/atmospherics_sanity_landmarks.dm
index 6a9df18bab3b..a021caa1edd4 100644
--- a/code/game/objects/effects/landmarks/atmospherics_sanity_landmarks.dm
+++ b/code/game/objects/effects/landmarks/atmospherics_sanity_landmarks.dm
@@ -21,12 +21,19 @@
icon_state = "atmos_sanity_start"
/**
- * Marks an area as a goal for atmospheric connectivity; ignored if the map contains the mark all station areas landmark!
+ * Marks an area as a goal for atmospheric connectivity; ignored if the map contains the mark all station areas landmark
*/
/obj/effect/landmark/atmospheric_sanity/goal_area
name = "Atmospheric Sanity Goal"
icon_state = "atmos_sanity_goal"
+/**
+ * Marks an area as forbidden from atmospheric connectivity; ignored if the map contains the "mark all station areas" landmark
+ */
+/obj/effect/landmark/atmospheric_sanity/forbidden_area
+ name = "Atmospheric Sanity Forbidden"
+ icon_state = "atmos_sanity_forbidden"
+
/**
* Marks an area as ignored for purposes of default station connectivity.
*/
diff --git a/code/game/objects/effects/particles/fire.dm b/code/game/objects/effects/particles/fire.dm
index 481849c00eb1..c35b1d645489 100644
--- a/code/game/objects/effects/particles/fire.dm
+++ b/code/game/objects/effects/particles/fire.dm
@@ -31,7 +31,7 @@
gradient = list("#FBAF4D", "#FCE6B6", "#FD481C")
position = generator(GEN_BOX, list(-12,-16,0), list(12,16,0), NORMAL_RAND)
drift = generator(GEN_VECTOR, list(-0.1,0), list(0.1,0.025), UNIFORM_RAND)
- spin = generator(GEN_NUM, list(-15,15), NORMAL_RAND)
+ spin = generator(GEN_NUM, -15, 15, NORMAL_RAND)
scale = generator(GEN_VECTOR, list(0.5,0.5), list(2,2), NORMAL_RAND)
/particles/embers/minor
diff --git a/code/game/objects/effects/particles/smoke.dm b/code/game/objects/effects/particles/smoke.dm
index c5626ef646c6..cd7766ccff7e 100644
--- a/code/game/objects/effects/particles/smoke.dm
+++ b/code/game/objects/effects/particles/smoke.dm
@@ -128,7 +128,7 @@
position = generator(GEN_BOX, list(-17,-15,0), list(24,15,0), NORMAL_RAND)
scale = generator(GEN_VECTOR, list(0.9,0.9), list(1.1,1.1), NORMAL_RAND)
drift = generator(GEN_SPHERE, list(-0.01,0), list(0.01,0.01), UNIFORM_RAND)
- spin = generator(GEN_NUM, list(-2,2), NORMAL_RAND)
+ spin = generator(GEN_NUM, -2, 2, NORMAL_RAND)
gravity = list(0.05, 0.28)
friction = 0.3
grow = 0.037
diff --git a/code/game/objects/effects/spawners/random/medical.dm b/code/game/objects/effects/spawners/random/medical.dm
index 57b180a02b43..5c2017e23b29 100644
--- a/code/game/objects/effects/spawners/random/medical.dm
+++ b/code/game/objects/effects/spawners/random/medical.dm
@@ -75,7 +75,7 @@
/obj/effect/spawner/random/medical/surgery_tool
name = "Surgery tool spawner"
- icon_state = "scapel"
+ icon_state = "scalpel"
loot = list(
/obj/item/scalpel,
/obj/item/hemostat,
@@ -88,7 +88,7 @@
/obj/effect/spawner/random/medical/surgery_tool_advanced
name = "Advanced surgery tool spawner"
- icon_state = "scapel"
+ icon_state = "scalpel"
loot = list( // Mail loot spawner. Drop pool of advanced medical tools typically from research. Not endgame content.
/obj/item/scalpel/advanced,
/obj/item/retractor/advanced,
@@ -97,7 +97,7 @@
/obj/effect/spawner/random/medical/surgery_tool_alien
name = "Rare surgery tool spawner"
- icon_state = "scapel"
+ icon_state = "scalpel"
loot = list( // Mail loot spawner. Some sort of random and rare surgical tool. Alien tech found here.
/obj/item/scalpel/alien,
/obj/item/hemostat/alien,
diff --git a/code/game/objects/effects/spawners/random/structure.dm b/code/game/objects/effects/spawners/random/structure.dm
index bfebd92e01f5..38f49d611cb4 100644
--- a/code/game/objects/effects/spawners/random/structure.dm
+++ b/code/game/objects/effects/spawners/random/structure.dm
@@ -22,6 +22,7 @@
/obj/structure/closet/mini_fridge = 35,
/obj/effect/spawner/random/trash/mess = 30,
/obj/item/kirbyplants/fern = 20,
+ /obj/effect/spawner/random/gizmo = 20,
/obj/structure/closet/crate/decorations = 15,
/obj/effect/decal/remains/human/smokey/maintenance = 7,
/obj/structure/destructible/cult/pants_altar = 1,
diff --git a/code/game/objects/effects/temporary_visuals/effect_trail.dm b/code/game/objects/effects/temporary_visuals/effect_trail.dm
index 5eed9462dfdf..e8ab9526f519 100644
--- a/code/game/objects/effects/temporary_visuals/effect_trail.dm
+++ b/code/game/objects/effects/temporary_visuals/effect_trail.dm
@@ -27,7 +27,7 @@
return INITIALIZE_HINT_QDEL
AddElement(/datum/element/floor_loving)
- AddComponent(/datum/component/spawner, spawn_types = list(spawned_effect), max_spawned = max_spawned, spawn_time = spawn_interval)
+ add_spawner()
src.target = target
movement = GLOB.move_manager.move_towards(src, chasing = target, delay = move_speed, home = homing, timeout = duration, flags = MOVEMENT_LOOP_START_FAST)
@@ -35,6 +35,9 @@
if (isliving(target))
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_invalid))
+/obj/effect/temp_visual/effect_trail/proc/add_spawner()
+ AddComponent(/datum/component/spawner, spawn_types = list(spawned_effect), max_spawned = max_spawned, spawn_time = spawn_interval)
+
/// Destroy ourselves if the target is no longer valid
/obj/effect/temp_visual/effect_trail/proc/on_target_invalid()
SIGNAL_HANDLER
diff --git a/code/game/objects/effects/temporary_visuals/miscellaneous.dm b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
index ec8c398f335b..ab74022a640c 100644
--- a/code/game/objects/effects/temporary_visuals/miscellaneous.dm
+++ b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
@@ -274,6 +274,10 @@
light_color = LIGHT_COLOR_FIRE
duration = 10
+/obj/effect/temp_visual/fire/light
+ icon_state = "light"
+ color = COLOR_DARK_ORANGE
+
/obj/effect/temp_visual/revenant
name = "spooky lights"
icon_state = "purplesparkles"
@@ -781,17 +785,17 @@
pixel_x = -16
pixel_y = -16
duration = 0.5 SECONDS
- color = COLOR_LIME
var/max_alpha = 255
///How far the effect would scale in size
var/amount_to_scale = 2
-/obj/effect/temp_visual/circle_wave/Initialize(mapload)
+/obj/effect/temp_visual/circle_wave/Initialize(mapload, color)
transform = matrix().Scale(0.1)
animate(src, transform = matrix().Scale(amount_to_scale), time = duration, flags = ANIMATION_PARALLEL)
animate(src, alpha = max_alpha, time = duration * 0.6, flags = ANIMATION_PARALLEL)
animate(alpha = 0, time = duration * 0.4)
apply_wibbly_filters(src)
+ src.color ||= color
return ..()
/obj/effect/temp_visual/circle_wave/bioscrambler
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 92c2f448609c..4285f07a7093 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -369,7 +369,7 @@
return TRUE
/obj/item/blob_act(obj/structure/blob/B)
- if(B && B.loc == loc)
+ if(B && B.loc == loc && !(resistance_flags & INDESTRUCTIBLE))
atom_destruction(MELEE)
/**Makes cool stuff happen when you suicide with an item
@@ -434,7 +434,8 @@
if(item_flags & CRUEL_IMPLEMENT)
.[span_red("morbid")] = "It seems quite practical for particularly morbid procedures and experiments."
-
+ if(item_flags & BLUESPACE_INTERFERENCE)
+ .["bluespace-active"] = "It is highly active in bluespace and will cause malfunctions in teleporters."
if (siemens_coefficient == 0)
.["insulated"] = "It is made from a robust electrical insulator and will block any electricity passing through it!"
else if (siemens_coefficient <= 0.5)
diff --git a/code/game/objects/items/cards_ids.dm b/code/game/objects/items/cards_ids.dm
index 18cc77200918..8e2ae5c689f9 100644
--- a/code/game/objects/items/cards_ids.dm
+++ b/code/game/objects/items/cards_ids.dm
@@ -824,7 +824,7 @@
/obj/item/card/id/click_alt(mob/living/user)
if(!alt_click_can_use_id(user))
return NONE
- if (registered_account.being_dumped)
+ if (LAZYLEN(registered_account.being_dumped))
registered_account.bank_card_talk(span_warning("内部服务器错误"), TRUE)
return CLICK_ACTION_SUCCESS
if(registered_account.account_debt)
diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
index 1a8695c75d3d..060da2c649d4 100644
--- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
@@ -1316,6 +1316,26 @@
/obj/item/stack/sheet/glass = 1)
needs_anchored = FALSE
+/obj/item/circuitboard/machine/hydroponics/proc/changeindicators(mob/living/user, obj/item/I)
+ if(build_path == /obj/machinery/hydroponics/constructable/oldstyle)
+ name = "Hydroponics Tray [name_extension]"
+ build_path = /obj/machinery/hydroponics/constructable
+ balloon_alert(user, "defaulting indicator location")
+ else
+ name = "Hydroponics Tray (Alt) [name_extension]"
+ build_path = /obj/machinery/hydroponics/constructable/oldstyle
+ balloon_alert(user, "moved indicators location")
+
+/obj/item/circuitboard/machine/hydroponics/item_interaction(mob/living/user, obj/item/I, list/modifiers)
+ if(istype(I, /obj/item/plant_analyzer))
+ changeindicators(user)
+ return ITEM_INTERACT_SUCCESS
+ return ..()
+
+/obj/item/circuitboard/machine/hydroponics/screwdriver_act(mob/living/user, obj/item/tool)
+ changeindicators(user)
+ return ITEM_INTERACT_SUCCESS
+
/obj/item/circuitboard/machine/hydroponics/fullupgrade
build_path = /obj/machinery/hydroponics/constructable/fullupgrade
specific_parts = TRUE
@@ -1325,6 +1345,11 @@
/obj/item/stack/sheet/glass = 1
)
+// DARKPACK EDIT ADD START
+/obj/item/circuitboard/machine/hydroponics/tainted
+ build_path = /obj/machinery/hydroponics/constructable/tainted
+// DARKPACK EDIT ADD END
+
/obj/item/circuitboard/machine/microwave
name = "Microwave"
greyscale_colors = CIRCUIT_COLOR_SERVICE
diff --git a/code/game/objects/items/crab17.dm b/code/game/objects/items/crab17.dm
index 77407bd79461..fdb179ba55af 100644
--- a/code/game/objects/items/crab17.dm
+++ b/code/game/objects/items/crab17.dm
@@ -27,9 +27,9 @@
if(isliving(user))
L = user
accounts_to_rob -= L.get_bank_account()
+ var/obj/effect/dumpeet_target/dump_machine = new /obj/effect/dumpeet_target(targetturf, L)
for(var/datum/bank_account/B as anything in accounts_to_rob)
- B.dumpeet()
- new /obj/effect/dumpeet_target(targetturf, L)
+ B.dumpeet(dump_machine.dump)
to_chat(user, span_notice("You have activated Protocol CRAB-17."))
user.log_message("activated Protocol CRAB-17.", LOG_GAME)
@@ -67,7 +67,7 @@
*/
/obj/structure/checkoutmachine/proc/check_if_finished()
for(var/datum/bank_account/B as anything in accounts_to_rob)
- if (B.being_dumped)
+ if(LAZYFIND(B.being_dumped, src))
return FALSE
return TRUE
@@ -94,13 +94,13 @@
balloon_alert(user, "card has no registered account!")
return
- if(!card.registered_account.being_dumped)
+ if(!LAZYFIND(card.registered_account.being_dumped, src))
balloon_alert(user, "funds are already safe!")
return
to_chat(user, span_warning("You quickly cash out your funds to a more secure banking location. Funds are safu.")) // This is a reference and not a typo
accounts_to_rob -= card.registered_account
- card.registered_account.stop_dump()
+ card.registered_account.stop_dump(src)
if(check_if_finished())
qdel(src)
@@ -112,6 +112,8 @@
return
bogdanoff = user
internal_account = new /datum/bank_account/remote("CRAB-17", 0, player_account = FALSE)
+
+/obj/structure/checkoutmachine/proc/setup_siphoning()
add_overlay("flaps")
add_overlay("hatch")
add_overlay("legs_retracted")
@@ -232,7 +234,7 @@
*/
/obj/structure/checkoutmachine/proc/stop_dumping()
for(var/datum/bank_account/B as anything in accounts_to_rob)
- B.stop_dump()
+ B.stop_dump(src)
/**
* Splits the balance of the internal_account into several smaller piles of cash and scatters them around the area.
@@ -274,6 +276,7 @@
/obj/effect/dumpeet_target/Initialize(mapload, user)
. = ..()
bogdanoff = user
+ dump = new /obj/structure/checkoutmachine(null, bogdanoff)
addtimer(CALLBACK(src, PROC_REF(startLaunch)), 10 SECONDS)
sound_to_playing_players('sound/items/dump_it.ogg', 20)
deadchat_broadcast("Protocol CRAB-17 has been activated. A space-coin market has been launched at the station!", turf_target = get_turf(src), message_type=DEADCHAT_ANNOUNCEMENT)
@@ -283,7 +286,7 @@
*/
/obj/effect/dumpeet_target/proc/startLaunch()
DF = new /obj/effect/dumpeet_fall(drop_location())
- dump = new /obj/structure/checkoutmachine(null, bogdanoff)
+ dump.setup_siphoning()
priority_announce("The spacecoin bubble has popped! Get to the credit deposit machine at [get_area(src)] and cash out before you lose all of your funds!", sender_override = "CRAB-17 Protocol")
animate(DF, pixel_z = -8, time = 5, , easing = LINEAR_EASING)
playsound(src, 'sound/items/weapons/mortar_whistle.ogg', 70, TRUE, 6)
diff --git a/code/game/objects/items/devices/aicard_evil.dm b/code/game/objects/items/devices/aicard_evil.dm
index d5146c0a54ac..307748172db5 100644
--- a/code/game/objects/items/devices/aicard_evil.dm
+++ b/code/game/objects/items/devices/aicard_evil.dm
@@ -76,6 +76,7 @@
// create and apply syndie datum
var/datum/antagonist/nukeop/nuke_datum = new()
nuke_datum.send_to_spawnpoint = FALSE
+ nuke_datum.give_bonus_tc = FALSE
new_ai.mind.add_antag_datum(nuke_datum, op_datum.nuke_team)
LAZYADD(new_ai.mind.special_roles, "Syndicate AI")
new_ai.add_faction(ROLE_SYNDICATE)
diff --git a/code/game/objects/items/devices/broadcast_camera.dm b/code/game/objects/items/devices/broadcast_camera.dm
index 81b5b0ea344d..bbc94accca08 100644
--- a/code/game/objects/items/devices/broadcast_camera.dm
+++ b/code/game/objects/items/devices/broadcast_camera.dm
@@ -28,6 +28,8 @@
var/broadcast_name = "Curator News"
/// The networks it broadcasts to, default is CAMERANET_NETWORK_CURATOR
var/list/camera_networks = list(CAMERANET_NETWORK_CURATOR)
+ /// Range of the camera
+ var/camera_range = 7
/// The "virtual" security camera inside of the physical camera
var/obj/machinery/camera/internal_camera
/// The "virtual" radio inside of the the physical camera, a la microphone
@@ -86,6 +88,7 @@
// INTERNAL CAMERA
internal_camera = new(wielding_carbon) // Cameras for some reason do not work inside of obj's
internal_camera.internal_light = FALSE
+ internal_camera.view_range = camera_range
internal_camera.network = camera_networks
internal_camera.c_tag = "LIVE: [broadcast_name]"
start_broadcasting_network(camera_networks, "[broadcast_name] is now LIVE!")
@@ -126,3 +129,16 @@
/obj/item/broadcast_camera/proc/set_microphone_state()
internal_radio.set_broadcasting(active_microphone)
+
+// Orderable from cargo
+/obj/item/broadcast_camera/cargo
+ slowdown = 0.3
+ item_flags = parent_type::item_flags | SLOWS_WHILE_IN_HAND
+ broadcast_name = "Camera Broadcast"
+ camera_range = 5
+
+/obj/item/broadcast_camera/cargo/Initialize(mapload)
+ . = ..()
+ // Gives each cargo camera a unique network id
+ var/static/cargo_camera_network_id = 0
+ camera_networks = list("cargo_camera_id_[cargo_camera_network_id++]")
diff --git a/code/game/objects/items/devices/earthcracker.dm b/code/game/objects/items/devices/earthcracker.dm
new file mode 100644
index 000000000000..455be3168f26
--- /dev/null
+++ b/code/game/objects/items/devices/earthcracker.dm
@@ -0,0 +1,178 @@
+#define EARTHCRACKER_READY "ready"
+#define EARTHCRACKER_ACTIVE "active"
+#define EARTHCRACKER_SPENT "spent"
+
+/**
+ * The E-2 Earthcracker, a traitor device that creates intentional weakpoints on the station after use, which can be triggered via explosions.
+ * See weakpoint.dm for more specifics on their effects.
+ */
+/obj/item/earthcracker
+ name = "E-2 Earthcracker"
+ desc = "A nasty automated pilebunker can be used to create a massive weakpoint in flooring,\
+ which can be triggered afterwards by a sufficiently strong enough explosion."
+ icon = 'icons/obj/devices/tool.dmi'
+ icon_state = "earthcracker"
+ base_icon_state = "earthcracker"
+ /// Is the earthcracker ready to arm, arming, activating, or spent?
+ var/status = EARTHCRACKER_READY
+ /// What kind of weakpoint shall you spawn?
+ var/obj/weakpoint_type = /obj/effect/weakpoint/big
+ /// The timer for the strike_the_earth activation
+ var/activation_timer
+
+/obj/item/earthcracker/Initialize(mapload)
+ . = ..()
+ if(!weakpoint_type)
+ CRASH("An earthcracker spawned without a designated weakpoint!")
+ register_context()
+
+/obj/item/earthcracker/attack_self(mob/user, modifiers)
+ . = ..()
+ if(status == EARTHCRACKER_READY)
+ handle_arming(user)
+
+/obj/item/earthcracker/attack_hand(mob/user, list/modifiers)
+ . = ..()
+ if(!anchored)
+ return NONE
+ switch(status)
+ if(EARTHCRACKER_ACTIVE)
+ if(activation_timer)
+ return FALSE
+ var/response = tgui_alert(user, "Activate the earthcracker?", "Activate?", list("Yes", "No")) == "Yes"
+ if(!response)
+ return FALSE
+ if(!user.Adjacent(src))
+ return FALSE
+ flick("[base_icon_state]-active", src)
+ if(is_mining_level(z))
+ activation_timer = addtimer(CALLBACK(src, PROC_REF(mining_act), user), 1.2 SECONDS)
+ return TRUE
+ activation_timer = addtimer(CALLBACK(src, PROC_REF(strike_the_earth)), 1.2 SECONDS)
+ return TRUE
+ if(EARTHCRACKER_SPENT)
+ balloon_alert(user, "used up!")
+ return FALSE
+
+/obj/item/earthcracker/update_icon_state()
+ . = ..()
+ switch(status)
+ if(EARTHCRACKER_READY)
+ icon_state = "[base_icon_state]"
+ if(EARTHCRACKER_ACTIVE)
+ icon_state = "[base_icon_state]-armed"
+ if(EARTHCRACKER_SPENT)
+ icon_state = "[base_icon_state]-spent"
+
+/obj/item/earthcracker/wrench_act(mob/living/user, obj/item/tool)
+ if(anchored && status == EARTHCRACKER_SPENT)
+ balloon_alert(user, "it falls apart")
+ animate(src, 0.6 SECONDS, alpha = 0, easing = CIRCULAR_EASING|EASE_IN)
+ addtimer(CALLBACK(src, PROC_REF(post_break)), 0.6 SECONDS)
+ return ITEM_INTERACT_SUCCESS
+ if(!anchored && status == EARTHCRACKER_READY)
+ balloon_alert(user, "arm in hands first!")
+ return ITEM_INTERACT_SUCCESS
+ if(anchored && status == EARTHCRACKER_ACTIVE)
+ balloon_alert(user, "unfastening from the floor...")
+ if(!tool.use_tool(src, user, 8 SECONDS, volume = 50))
+ return ITEM_INTERACT_FAILURE
+ anchored = FALSE
+ status = EARTHCRACKER_READY
+ update_appearance(UPDATE_ICON)
+ return NONE
+
+/obj/item/earthcracker/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ switch(status)
+ if(EARTHCRACKER_ACTIVE)
+ context[SCREENTIP_CONTEXT_LMB] = "Activate device"
+ if(EARTHCRACKER_SPENT)
+ if(held_item?.tool_behaviour == TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_LMB] = "Disassemble device"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/item/earthcracker/examine(mob/user)
+ . = ..()
+ if(status == EARTHCRACKER_SPENT)
+ . += span_warning("This device is toast. You could disassemble the remains using a [EXAMINE_HINT("Wrench")].")
+ else
+ . += span_info("This device can be unanchored using a [EXAMINE_HINT("Wrench")].")
+
+/obj/item/earthcracker/proc/handle_arming(mob/user)
+ var/turf/arm_location = get_turf(user)
+ if(!arm_location)
+ return FALSE
+ if(isgroundlessturf(arm_location))
+ balloon_alert(user, "can't deploy here")
+ return FALSE
+
+ if(status == EARTHCRACKER_SPENT)
+ balloon_alert(user, "used up!")
+ return FALSE
+ balloon_alert(user, "arming...")
+ if(!do_after(user, 3 SECONDS, src))
+ balloon_alert(user, "failed to arm")
+ return FALSE
+
+ forceMove(arm_location)
+ anchored = TRUE
+ flick("[base_icon_state]-arm", src)
+ playsound(src, 'sound/items/barcodebeep.ogg', 50, FALSE)
+ status = EARTHCRACKER_ACTIVE
+ update_appearance(UPDATE_ICON)
+ return TRUE
+
+/// The fun part. We spawn a huge weakpoint here.
+/obj/item/earthcracker/proc/strike_the_earth()
+ if(QDELETED(src))
+ return
+ playsound(src, 'sound/items/weapons/earthcracker_bang.mp3', 75, FALSE, 3)
+ var/turf/cracked_hull = drop_location()
+ new weakpoint_type(cracked_hull)
+ handle_after_activation(cracked_hull)
+
+/// Cleanup after an earthcracker is activated either for sabotage or mining.
+/obj/item/earthcracker/proc/handle_after_activation(turf/cracked_hull)
+ do_sparks(2, FALSE, src)
+ cracked_hull.levelupdate()
+
+ status = EARTHCRACKER_SPENT
+ update_appearance(UPDATE_ICON)
+ particles = new /particles/smoke/burning/small
+ activation_timer = null
+
+/// Called after the earthcracker activates.
+/obj/item/earthcracker/proc/post_break()
+ deconstruct(TRUE)
+
+/// When this item is used on a mining Z, we perform an action that breaks all rocks in a radius around us, the same as starting an ore vent wave.
+/obj/item/earthcracker/proc/mining_act(mob/user)
+ for(var/i in 1 to 5)
+ for(var/turf/rock in oview(i)) // This collects a list of rings of turfs (in a growing radius of i) that we'll applying logic to "drill" below.
+
+ if(istype(rock, /turf/closed/mineral))
+ if(prob(50 + (i * 8)))
+ continue
+ var/turf/closed/mineral/drillable = rock
+ drillable.gets_drilled(user)
+ if(prob(50))
+ new /obj/effect/decal/cleanable/rubble(rock)
+ continue
+
+ if(istype(rock, /turf/open/misc/asteroid) && prob(35))
+ new /obj/effect/decal/cleanable/rubble(rock)
+ continue
+ sleep(0.6 SECONDS)
+ handle_after_activation()
+
+/// Small subtype for shenanigans.
+/obj/item/earthcracker/small
+ name = "E-1 Earthcracker"
+ desc = "A rusty automated pilebunker can be used to create a weakpoint in flooring,\
+ which can be triggered afterwards by a sufficiently strong enough explosion.\
+ You're pretty sure the mining company that used to make these got bought by Nanotrasen ages ago."
+ weakpoint_type = /obj/effect/weakpoint
+
+#undef EARTHCRACKER_READY
+#undef EARTHCRACKER_ACTIVE
+#undef EARTHCRACKER_SPENT
diff --git a/code/game/objects/items/devices/transfer_valve.dm b/code/game/objects/items/devices/transfer_valve.dm
index 0b5574de31c0..c1e7f296846a 100644
--- a/code/game/objects/items/devices/transfer_valve.dm
+++ b/code/game/objects/items/devices/transfer_valve.dm
@@ -33,6 +33,20 @@
attached_device = null
return ..()
+/obj/item/transfer_valve/examine(mob/user)
+ . = ..()
+ if(tank_one)
+ . += span_notice("A [tank_one] is attached to the primary port.")
+ if(tank_two)
+ . += span_notice("A [tank_two] is attached to the secondary port.")
+ if(!tank_one || !tank_two)
+ . += span_notice("You could attach a gas tank onto the open port.")
+ if(attached_device)
+ . += span_notice("A [attached_device] is attached to the fuse input.")
+ if(wired)
+ . += span_notice("It looks like some nutjob added makeshift backpack straps with [EXAMINE_HINT("wires")]...")
+ . += span_notice("You could cut off the straps with [EXAMINE_HINT(TOOL_WIRECUTTER)].")
+
/obj/item/transfer_valve/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
@@ -79,58 +93,37 @@
/obj/item/transfer_valve/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
. = ..()
- if (!istype(interacting_with, /obj/vehicle/ridden/wheelchair))
- return NONE
- var/obj/vehicle/ridden/wheelchair/chair = interacting_with
-
- if (chair.bomb_attached)
- user.balloon_alert(user, "already has a TTV!")
- return ITEM_INTERACT_FAILURE
- user.balloon_alert(user, "attaching TTV...")
- if (!do_after(user, 0.5 SECONDS, chair))
- return ITEM_INTERACT_FAILURE
-
- chair.attach_bomb(src)
- user.log_message("attached [src] to [chair]", LOG_GAME)
- return ITEM_INTERACT_SUCCESS
+ if(istype(interacting_with, /obj/vehicle/ridden/wheelchair))
+ var/obj/vehicle/ridden/wheelchair/chair = interacting_with
+
+ if (chair.bomb_attached)
+ user.balloon_alert(user, "already has a TTV!")
+ return ITEM_INTERACT_FAILURE
+ user.balloon_alert(user, "attaching TTV...")
+ if (!do_after(user, 0.5 SECONDS, chair))
+ return ITEM_INTERACT_FAILURE
+
+ chair.attach_bomb(src)
+ user.log_message("attached [src] to [chair]", LOG_GAME)
+ return ITEM_INTERACT_SUCCESS
+ else if(istype(interacting_with, /obj/item/tank))
+ if(try_attach_tank(interacting_with))
+ return ITEM_INTERACT_SUCCESS
+ else
+ return ITEM_INTERACT_FAILURE
+ else if(isassembly(interacting_with))
+ if(try_attach_assembly(interacting_with))
+ return ITEM_INTERACT_SUCCESS
+ else
+ return ITEM_INTERACT_FAILURE
+ return NONE
/obj/item/transfer_valve/attackby(obj/item/item, mob/user, params)
if(istype(item, /obj/item/tank))
- if(tank_one && tank_two)
- to_chat(user, span_warning("There are already two tanks attached, remove one first!"))
- return
-
- if(!tank_one)
- if(!user.transferItemToLoc(item, src))
- return
- tank_one = item
- to_chat(user, span_notice("You attach the tank to the transfer valve."))
- else if(!tank_two)
- if(!user.transferItemToLoc(item, src))
- return
- tank_two = item
- to_chat(user, span_notice("You attach the tank to the transfer valve."))
-
- update_appearance()
+ try_attach_tank(item, user)
//TODO: Have this take an assemblyholder
else if(isassembly(item))
- var/obj/item/assembly/A = item
- if(A.secured)
- to_chat(user, span_notice("The device is secured."))
- return
- if(attached_device)
- to_chat(user, span_warning("There is already a device attached to the valve, remove it first!"))
- return
- if(!user.transferItemToLoc(item, src))
- return
- attached_device = A
- to_chat(user, span_notice("You attach the [item] to the valve controls and secure it."))
- A.holder = src
- A.on_attach()
- A.toggle_secure() //this calls update_icon(), which calls update_icon() on the holder (i.e. the bomb).
- log_bomber(user, "attached a [item.name] to a ttv -", src, null, FALSE)
- attacher = user
-
+ try_attach_assembly(item, user)
else if(istype(item, /obj/item/stack/cable_coil) && !wired)
var/obj/item/stack/cable_coil/coil = item
if (coil.get_amount() < 15)
@@ -153,6 +146,42 @@
return
+/obj/item/transfer_valve/proc/try_attach_tank(obj/item/tank/new_tank, mob/user)
+ if(!tank_one)
+ if(!user.transferItemToLoc(new_tank, src))
+ return FALSE
+ tank_one = new_tank
+ to_chat(user, span_notice("You attach the [new_tank] to the transfer valve's primary port."))
+ else if(!tank_two)
+ if(!user.transferItemToLoc(new_tank, src))
+ return FALSE
+ tank_two = new_tank
+ to_chat(user, span_notice("You attach the [new_tank] to the transfer valve's secondary port."))
+ else
+ to_chat(user, span_warning("There are already two tanks attached, remove one first!"))
+ return FALSE
+
+ update_appearance()
+ return TRUE
+
+/obj/item/transfer_valve/proc/try_attach_assembly(obj/item/assembly/A, mob/user)
+ if(A.secured)
+ to_chat(user, span_notice("The [A] is secured; unsecure it first."))
+ return FALSE
+ if(attached_device)
+ to_chat(user, span_warning("There is already a device attached to the valve, remove it first!"))
+ return FALSE
+ if(!user.transferItemToLoc(A, src))
+ return FALSE
+ attached_device = A
+ to_chat(user, span_notice("You attach the [A] to the valve controls and secure it."))
+ A.holder = src
+ A.on_attach()
+ A.toggle_secure() //this calls update_icon(), which calls update_icon() on the holder (i.e. the bomb).
+ log_bomber(user, "attached a [A.name] to a ttv -", src, null, FALSE)
+ attacher = user
+ return TRUE
+
//These keep attached devices synced up, for example a TTV with a mouse trap being found in a bag so it's triggered, or moving the TTV with an infrared beam sensor to update the beam's direction.
/obj/item/transfer_valve/Move()
. = ..()
diff --git a/code/game/objects/items/dna_injector.dm b/code/game/objects/items/dna_injector.dm
index baf4bb85e626..178f67c0b008 100644
--- a/code/game/objects/items/dna_injector.dm
+++ b/code/game/objects/items/dna_injector.dm
@@ -11,18 +11,34 @@
throw_range = 5
w_class = WEIGHT_CLASS_TINY
+ /// Modified to damage caused from injection, currently unused though
var/damage_coeff = 1
- var/list/fields
- var/list/add_mutations = list()
- var/list/remove_mutations = list()
-
- var/used = FALSE
-
-/obj/item/dnainjector/Initialize(mapload)
+ /// Adds all mutations in this list to the injected mob, activating it rather than mutating it if possible.
+ var/list/add_mutations
+ /// If the injected mob has any mutations in this list activated or mutated, they will be removed.
+ var/list/remove_mutations
+ /// Tracks if it's been used
+ VAR_FINAL/used = FALSE
+ /// Duration of the mutations added/activated or DNA changes. Does not affect removed mutations.
+ var/duration = INFINITY
+ /// A DNA datum that has its fields copied to the target on injection.
+ var/datum/dna/stored_dna
+
+/obj/item/dnainjector/Initialize(mapload, datum/dna/stored_dna, damage_coeff = 1)
. = ..()
AddElement(/datum/element/update_icon_updates_onmob)
if(used)
update_appearance()
+ src.stored_dna = stored_dna
+ src.damage_coeff = damage_coeff
+
+/obj/item/dnainjector/Destroy()
+ if(stored_dna?.holder)
+ stack_trace("DNA injector got owned DNA somehow")
+ stored_dna = null
+ else
+ QDEL_NULL(stored_dna)
+ return ..()
/obj/item/dnainjector/vv_edit_var(vname, vval)
. = ..()
@@ -43,27 +59,26 @@
/obj/item/dnainjector/proc/inject(mob/living/carbon/target, mob/user)
if(!target.can_mutate())
return FALSE
+ if(target.stat == DEAD) //prevents dead people from having their DNA changed
+ to_chat(user, span_notice("You can't modify [target]'s DNA while [target.p_theyre()] dead."))
+ return FALSE
for(var/removed_mutation in remove_mutations)
- target.dna.remove_mutation(removed_mutation, list(MUTATION_SOURCE_ACTIVATED, MUTATION_SOURCE_MUTATOR))
- for(var/added_mutation in add_mutations)
- if(added_mutation == /datum/mutation/race)
- message_admins("[ADMIN_LOOKUPFLW(user)] injected [key_name_admin(target)] with \the [src] [span_danger("(MONKEY)")]")
+ target.dna.remove_mutation(removed_mutation, GLOB.standard_mutation_sources)
+
+ for(var/datum/mutation/added_mutation as anything in add_mutations)
+ if(added_mutation::warn_admins_on_inject && target != user && !ismonkey(target))
+ message_admins("[ADMIN_LOOKUPFLW(user)] injected [key_name_admin(target)] with [src] containing [added_mutation::name].")
if(target.dna.mutation_in_sequence(added_mutation))
target.dna.activate_mutation(added_mutation)
+ if(duration != INFINITY)
+ addtimer(CALLBACK(target.dna, TYPE_PROC_REF(/datum/dna, remove_mutation), added_mutation, MUTATION_SOURCE_ACTIVATED), duration)
else
target.dna.add_mutation(added_mutation, MUTATION_SOURCE_MUTATOR)
- if(fields)
- if(fields["name"] && fields["UE"] && fields["blood_type"])
- target.real_name = fields["name"]
- target.dna.unique_enzymes = fields["UE"]
- target.name = target.real_name
- target.set_blood_type(fields["blood_type"])
- if(fields["UI"]) //UI+UE
- target.dna.unique_identity = merge_text(target.dna.unique_identity, fields["UI"])
- if(fields["UF"])
- target.dna.unique_features = merge_text(target.dna.unique_features, fields["UF"])
- if(fields["UI"] || fields["UF"])
- target.updateappearance(mutcolor_update = TRUE, mutations_overlay_update = TRUE)
+ if(duration != INFINITY)
+ addtimer(CALLBACK(target.dna, TYPE_PROC_REF(/datum/dna, remove_mutation), added_mutation, MUTATION_SOURCE_MUTATOR), duration)
+
+ if(stored_dna)
+ target.apply_status_effect(/datum/status_effect/temporary_transformation/dna_injector, duration, stored_dna)
return TRUE
/obj/item/dnainjector/attack(mob/target, mob/user)
@@ -100,53 +115,7 @@
update_appearance()
/obj/item/dnainjector/timed
- var/duration = 60 SECONDS
-
-/obj/item/dnainjector/timed/inject(mob/living/carbon/target, mob/user)
- if(target.stat == DEAD) //prevents dead people from having their DNA changed
- to_chat(user, span_notice("You can't modify [target]'s DNA while [target.p_theyre()] dead."))
- return FALSE
- if(!target.can_mutate())
- return FALSE
- var/endtime = world.time + duration
- for(var/mutation in remove_mutations)
- target.dna.remove_mutation(mutation, list(MUTATION_SOURCE_ACTIVATED, MUTATION_SOURCE_MUTATOR))
- for(var/mutation in add_mutations)
- if(target.dna.get_mutation(mutation))
- continue //Skip permanent mutations we already have.
- if(mutation == /datum/mutation/race && !ismonkey(target))
- message_admins("[ADMIN_LOOKUPFLW(user)] injected [key_name_admin(target)] with \the [src] [span_danger("(MONKEY)")]")
- target.dna.add_mutation(mutation, MUTATION_SOURCE_TIMED_INJECTOR)
- addtimer(CALLBACK(target.dna, TYPE_PROC_REF(/datum/dna, remove_mutation), mutation, MUTATION_SOURCE_TIMED_INJECTOR), duration)
- if(fields)
- if(fields["name"] && fields["UE"] && fields["blood_type"])
- LAZYINITLIST(target.dna.previous)
- if(!target.dna.previous["name"])
- target.dna.previous["name"] = target.real_name
- if(!target.dna.previous["UE"])
- target.dna.previous["UE"] = target.dna.unique_enzymes
- if(!target.dna.previous["blood_type"])
- target.dna.previous["blood_type"] = target.get_bloodtype()
- target.real_name = fields["name"]
- target.dna.unique_enzymes = fields["UE"]
- target.name = target.real_name
- target.set_blood_type(fields["blood_type"])
- LAZYSET(target.dna.temporary_mutations, UE_CHANGED, endtime)
- if(fields["UI"]) //UI+UE
- LAZYINITLIST(target.dna.previous)
- if(!target.dna.previous["UI"])
- target.dna.previous["UI"] = target.dna.unique_identity
- target.dna.unique_identity = merge_text(target.dna.unique_identity, fields["UI"])
- LAZYSET(target.dna.temporary_mutations, UI_CHANGED, endtime)
- if(fields["UF"]) //UI+UE
- LAZYINITLIST(target.dna.previous)
- if(!target.dna.previous["UF"])
- target.dna.previous["UF"] = target.dna.unique_features
- target.dna.unique_features = merge_text(target.dna.unique_features, fields["UF"])
- LAZYSET(target.dna.temporary_mutations, UF_CHANGED, endtime)
- if(fields["UI"] || fields["UF"])
- target.updateappearance(mutcolor_update = TRUE, mutations_overlay_update = TRUE)
- return TRUE
+ duration = 60 SECONDS
/obj/item/dnainjector/timed/hulk
name = "\improper DNA injector (Hulk)"
diff --git a/code/game/objects/items/floppy_disk.dm b/code/game/objects/items/floppy_disk.dm
index 83a5a6d2dca9..3b7a75f1fa63 100644
--- a/code/game/objects/items/floppy_disk.dm
+++ b/code/game/objects/items/floppy_disk.dm
@@ -358,6 +358,20 @@
. = ..()
new /obj/item/disk/data(src)
+/obj/item/disk/manipulator
+ name = "manipulator task disk"
+ desc = "A floppy disk containing manipulator tasks."
+ var/list/tasks_data = list()
+
+/obj/item/disk/manipulator/proc/set_tasks(list/new_tasks_data)
+ if(read_only)
+ return FALSE
+ tasks_data = islist(new_tasks_data) ? new_tasks_data : list()
+ return TRUE
+
+/obj/item/disk/manipulator/proc/get_tasks()
+ return tasks_data?.Copy() || list()
+
#undef STARTING_STICKER
#undef MAX_DISK_STACK_SIZE
#undef STACK_PIXEL_STEP
diff --git a/code/game/objects/items/food/dough.dm b/code/game/objects/items/food/dough.dm
index 75fda88fc203..3266f9658c37 100644
--- a/code/game/objects/items/food/dough.dm
+++ b/code/game/objects/items/food/dough.dm
@@ -46,7 +46,7 @@
/obj/item/food/pizzabread/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/ingredients_holder, /obj/item/food/pizza, CUSTOM_INGREDIENT_ICON_SCATTER, max_ingredients = 12)
+ AddComponent(/datum/component/ingredients_holder, /obj/item/food/pizza/custom, CUSTOM_INGREDIENT_ICON_SCATTER, max_ingredients = 12)
/obj/item/food/doughslice
name = "dough slice"
diff --git a/code/game/objects/items/food/pastries.dm b/code/game/objects/items/food/pastries.dm
index 986aa1ea8e0a..4834586176eb 100644
--- a/code/game/objects/items/food/pastries.dm
+++ b/code/game/objects/items/food/pastries.dm
@@ -602,3 +602,18 @@
food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_SMALL
crafting_complexity = FOOD_COMPLEXITY_3
+
+/obj/item/food/apple_fritter
+ name = "apple fritter"
+ desc = "For something that looks like a pile of glazed dirt, it's suprisingly tart. It smells sweet enough to knock you unconscious."
+ icon_state = "apple_fritter"
+ food_reagents = list(
+ /datum/reagent/consumable/nutriment = 3,
+ /datum/reagent/consumable/nutriment/vitamin = 1,
+ /datum/reagent/consumable/sugar = 1,
+ /datum/reagent/consumable/applejuice = 1,
+ )
+ tastes = list("apple" = 1, "glaze" = 1)
+ foodtypes = GRAIN|FRUIT|FRIED|DAIRY|BREAKFAST
+ w_class = WEIGHT_CLASS_SMALL
+ crafting_complexity = FOOD_COMPLEXITY_3
diff --git a/code/game/objects/items/food/pizza.dm b/code/game/objects/items/food/pizza.dm
index 70d71459e15c..cc1c4b79ce8e 100644
--- a/code/game/objects/items/food/pizza.dm
+++ b/code/game/objects/items/food/pizza.dm
@@ -106,6 +106,7 @@
slice.reagents.remove_all()
reagents.trans_to(slice, amount = reagents.total_volume / slices_left, no_react = TRUE)
+ SEND_SIGNAL(src, COMSIG_PIZZA_SLICE_TAKEN, user, slice)
slices_left--
if(slices_left <= 0)
@@ -116,15 +117,6 @@
remove_filter("pizzaslices")
add_filter("pizzaslices", 1, get_slices_filter())
-// Raw Pizza
-/obj/item/food/pizza/raw
- foodtypes = GRAIN | RAW
- slice_type = null
- crafting_complexity = FOOD_COMPLEXITY_2
-
-/obj/item/food/pizza/raw/make_bakeable()
- AddComponent(/datum/component/bakeable, /obj/item/food/pizza, rand(70 SECONDS, 80 SECONDS), TRUE, TRUE)
-
// Pizza Slice
/obj/item/food/pizzaslice
name = "bugged pizza slice"
@@ -140,6 +132,17 @@
/obj/item/food/pizzaslice/make_processable()
AddElement(/datum/element/processable, TOOL_ROLLINGPIN, /obj/item/stack/sheet/pizza, 1, 1 SECONDS, table_required = TRUE, screentip_verb = "Flatten", sound_to_play = SFX_ROLLING_PIN_ROLLING)
+/obj/item/food/pizza/custom
+ name = "pizza"
+ desc = "A fancy custom pizza."
+ icon_state = "pizzamargherita" // Colored by the ingredient_holder component
+ slice_type = /obj/item/food/pizzaslice/custom
+
+/obj/item/food/pizzaslice/custom
+ name = "pizza slice"
+ desc = "A slice of custom fancy pizza."
+ icon_state = "pizzamargheritaslice"
+
/obj/item/food/pizza/margherita
name = "pizza margherita"
desc = "The most cheezy pizza in galaxy."
diff --git a/code/game/objects/items/food/sweets.dm b/code/game/objects/items/food/sweets.dm
index 4be75cf07fc9..e429976657f2 100644
--- a/code/game/objects/items/food/sweets.dm
+++ b/code/game/objects/items/food/sweets.dm
@@ -258,7 +258,7 @@
to_chat(victim, span_warning("[pick("You hear faint whispers.", "You smell ash.", "You feel hot.", "You hear a roar in the distance.")]"))
/obj/item/food/bubblegum/bubblegum/suicide_act(mob/living/user)
- user.say(";[pick(BUBBLEGUM_HALLUCINATION_LINES)]")
+ user.say(";[pick(BUBBLEGUM_HALLUCINATION_LINES)]", spans = list(SPAN_COLOSSUS))
return ..()
/obj/item/food/gumball
diff --git a/code/game/objects/items/grenades/_grenade.dm b/code/game/objects/items/grenades/_grenade.dm
index 11e7f3683e28..0d715d8e6d9f 100644
--- a/code/game/objects/items/grenades/_grenade.dm
+++ b/code/game/objects/items/grenades/_grenade.dm
@@ -145,7 +145,7 @@
/obj/item/grenade/proc/log_grenade(mob/user)
if(!type_cluster)
- log_bomber(user, "has primed a", src, "for detonation", message_admins = dud_flags != NONE)
+ log_bomber(user, "has primed a", src, "for detonation", message_admins = dud_flags == NONE)
/**
* arm_grenade (formerly preprime) refers to when a grenade with a standard time fuze is activated, making it go beepbeepbeep and then detonate a few seconds later.
diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm
index 6fe265d6b56b..cc8889b3b12b 100644
--- a/code/game/objects/items/handcuffs.dm
+++ b/code/game/objects/items/handcuffs.dm
@@ -430,6 +430,8 @@
slowdown = 7
breakouttime = 30 SECONDS
slot_flags = ITEM_SLOT_LEGCUFFED
+ /// Icon state for the legcuff overlay
+ var/legcuff_state = "legcuff"
/**
* # Bear trap
diff --git a/code/game/objects/items/inspector.dm b/code/game/objects/items/inspector.dm
index bc82fa85b948..4ca2320caa2e 100644
--- a/code/game/objects/items/inspector.dm
+++ b/code/game/objects/items/inspector.dm
@@ -77,7 +77,6 @@
/obj/item/inspector/examine(mob/user)
. = ..()
- . += span_info("Use in-hand to scan the local area, creating an encrypted security inspection.")
. += span_info("Use on an item to scan if it contains, or is, contraband.")
if(!cell_cover_open)
. += span_notice("Its cell cover is closed. It looks like it could be pried out, but doing so would require an appropriate tool.")
diff --git a/code/game/objects/items/pinpointer.dm b/code/game/objects/items/pinpointer.dm
index 2e23ffdfbb77..db79f9388df7 100644
--- a/code/game/objects/items/pinpointer.dm
+++ b/code/game/objects/items/pinpointer.dm
@@ -17,12 +17,23 @@
sound_vary = TRUE
pickup_sound = SFX_GENERIC_DEVICE_PICKUP
drop_sound = SFX_GENERIC_DEVICE_DROP
+ /// Is the pinpointer on?
var/active = FALSE
- var/atom/movable/target //The thing we're searching for
- var/minimum_range = 0 //at what range the pinpointer declares you to be at your destination
- var/alert = FALSE // TRUE to display things more seriously
- var/process_scan = TRUE // some pinpointers change target every time they scan, which means we can't have it change very process but instead when it turns on.
- var/icon_suffix = "" // for special pinpointer icons
+ ///The thing we're searching for
+ var/atom/movable/target
+ /// TRUE to display things more seriously
+ var/alert = FALSE
+ /// Some pinpointers change target every time they scan, which means we can't have it change every process() but instead when it turns on.
+ var/process_scan = TRUE
+ /// Icon_state suffix for special pinpointer icons
+ var/icon_suffix = ""
+
+ /// At what range the pinpointer declares you to be at your destination. Use to hide the exact location of your target.
+ var/minimum_range = 0
+ /// From 1 to this value, the sprite will display as though you're close.
+ var/close_range = 8
+ /// From close_range + 1 to this value, the sprite will display as though you're medium distance away. Past this value, we'll display as though you're far.
+ var/medium_range = 16
/obj/item/pinpointer/Initialize(mapload)
. = ..()
@@ -85,13 +96,13 @@
return "pinon[alert ? "alert" : ""]direct[icon_suffix]"
else
setDir(get_dir(here, there))
- switch(get_dist(here, there))
- if(1 to 8)
- return "pinon[alert ? "alert" : "close"][icon_suffix]"
- if(9 to 16)
- return "pinon[alert ? "alert" : "medium"][icon_suffix]"
- if(16 to INFINITY)
- return "pinon[alert ? "alert" : "far"][icon_suffix]"
+ var/current_distance = get_dist(here, there)
+ if(current_distance >= 1 && current_distance <= close_range)
+ return "pinon[alert ? "alert" : "close"][icon_suffix]"
+ else if(current_distance > (close_range + 1) && current_distance <= medium_range)
+ return "pinon[alert ? "alert" : "medium"][icon_suffix]"
+ else if(current_distance > medium_range)
+ return "pinon[alert ? "alert" : "far"][icon_suffix]"
/obj/item/pinpointer/crew // A replacement for the old crew monitoring consoles
name = "crew pinpointer"
@@ -100,9 +111,14 @@
worn_icon_state = "pinpointer_crew"
custom_price = PAYCHECK_CREW * 6
custom_premium_price = PAYCHECK_CREW * 6
- var/has_owner = FALSE
+ /// The mob that the pinpointer is owned by.
var/pinpointer_owner = null
- var/ignore_suit_sensor_level = FALSE /// Do we find people even if their suit sensors are turned off
+ /// Do we find people even if their suit sensors are turned off
+ var/ignore_suit_sensor_level = FALSE
+
+/obj/item/pinpointer/crew/Destroy()
+ . = ..()
+ pinpointer_owner = null
/obj/item/pinpointer/crew/proc/trackable(mob/living/carbon/human/H)
var/turf/here = get_turf(src)
@@ -120,7 +136,7 @@
user.visible_message(span_notice("[user] deactivates [user.p_their()] pinpointer."), span_notice("You deactivate your pinpointer."))
return
- if (has_owner && !pinpointer_owner)
+ if (!pinpointer_owner)
pinpointer_owner = user
if (pinpointer_owner && pinpointer_owner != user)
@@ -173,6 +189,7 @@
/obj/item/pinpointer/pair
name = "pair pinpointer"
desc = "A handheld tracking device that locks onto its other half of the matching pair."
+ /// Reference to the other, specific pinpointer that it's bought with. Assigned on /obj/item/storage/box/pinpointer_pairs.
var/other_pair
/obj/item/pinpointer/pair/Destroy()
@@ -197,6 +214,7 @@
icon_state = "pinpointer_hunter"
worn_icon_state = "pinpointer_black"
icon_suffix = "_hunter"
+ /// Reference to the bounty hunter shuttle's docking port.
var/obj/docking_port/mobile/shuttleport
/obj/item/pinpointer/shuttle/Initialize(mapload)
diff --git a/code/game/objects/items/robot/items/hypo.dm b/code/game/objects/items/robot/items/hypo.dm
index 2a759a275a8b..3119037f0153 100644
--- a/code/game/objects/items/robot/items/hypo.dm
+++ b/code/game/objects/items/robot/items/hypo.dm
@@ -50,6 +50,7 @@
/datum/reagent/medicine/morphine,\
/datum/reagent/medicine/potass_iodide,\
/datum/reagent/medicine/syndicate_nanites,\
+ /datum/reagent/medicine/atropine,\
)
#define BASE_SERVICE_REAGENTS list(/datum/reagent/consumable/applejuice, /datum/reagent/consumable/banana,\
/datum/reagent/consumable/berryjuice, /datum/reagent/consumable/cherryjelly, /datum/reagent/consumable/coffee,\
diff --git a/code/game/objects/items/robot/items/tools.dm b/code/game/objects/items/robot/items/tools.dm
index 41e73a1bf79d..351949715451 100644
--- a/code/game/objects/items/robot/items/tools.dm
+++ b/code/game/objects/items/robot/items/tools.dm
@@ -187,13 +187,20 @@
return ..()
/obj/item/borg/cyborg_omnitool/add_context(atom/source, list/context, obj/item/held_item, mob/user)
- . = ..()
+ . = NONE
if (!issilicon(user))
return
+
var/mob/living/silicon/robot/as_cyborg = user
if (!(src in as_cyborg.held_items))
context[SCREENTIP_CONTEXT_RMB] = "Select Tool"
- return CONTEXTUAL_SCREENTIP_SET
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/item/borg/cyborg_omnitool/examine(mob/user)
+ . = ..()
+ if(reference)
+ var/obj/item/tool = get_proxy_attacker_for(src, usr)
+ . += tool.examine(user)
/**
* Sets the new internal tool to be used
@@ -202,8 +209,6 @@
* * obj/item/ref - typepath for the new internal omnitool
*/
/obj/item/borg/cyborg_omnitool/proc/set_internal_tool(obj/item/tool)
- SHOULD_NOT_OVERRIDE(TRUE)
-
for(var/obj/item/internal_tool as anything in omni_toolkit)
if(internal_tool == tool)
reference = internal_tool
@@ -237,14 +242,17 @@
//if all else fails just make a new one from scratch
tool = new reference(user)
+ //assign the upgraded toolspeed, if engi omnitool upgrade was applied.
+ tool.toolspeed = initial(tool.toolspeed) - upgraded * 0.3
//the internal tool is considered part of the tool itself, so don't let it be dropped.
tool.item_flags |= ABSTRACT
ADD_TRAIT(tool, TRAIT_NODROP, INNATE_TRAIT)
+ //store tool for future use
atoms[reference] = tool
- tool.toolspeed = initial(tool.toolspeed) - upgraded * 0.3 //and finally assign the upgraded toolspeed, if any.
+
return tool
-/obj/item/borg/cyborg_omnitool/attack_self(mob/user)
+/obj/item/borg/cyborg_omnitool/attack_self(mob/user, modifiers)
//build the radial menu options
var/list/radial_menu_options = list()
var/list/tool_map = list()
@@ -269,7 +277,7 @@
return ..()
var/mob/living/silicon/robot/user = usr
if (!(src in user.held_items))
- attack_self(user)
+ attack_self(user, modifiers)
return ..()
/obj/item/borg/cyborg_omnitool/update_icon_state()
@@ -318,15 +326,60 @@
/obj/item/screwdriver/cyborg,
/obj/item/crowbar/cyborg,
/obj/item/multitool/cyborg,
+ /obj/item/weldingtool/largetank/cyborg,
)
-/obj/item/borg/cyborg_omnitool/engineering/examine(mob/user)
+/obj/item/borg/cyborg_omnitool/engineering/Initialize(mapload)
. = ..()
+ RegisterSignal(src, COMSIG_SILICON_MODULE_ACTIVATION, PROC_REF(welder_toggle))
- if(tool_behaviour == TOOL_MULTITOOL)
- for(var/obj/item/multitool/tool in atoms)
- . += "Its multitool buffer contains [tool.buffer]"
- break
+/obj/item/borg/cyborg_omnitool/engineering/update_overlays()
+ . = ..()
+ if(tool_behaviour == TOOL_WELDER)
+ var/obj/item/weldingtool/tool = atoms[/obj/item/weldingtool/largetank/cyborg]
+ if(tool?.welding)
+ . |= tool.update_overlays()
+
+/obj/item/borg/cyborg_omnitool/engineering/attack_self(mob/user, modifiers)
+ if(tool_behaviour == TOOL_WELDER && LAZYACCESS(modifiers, LEFT_CLICK))
+ welder_toggle(src, null, user)
+
+ return NONE
+
+ return ..()
+
+/obj/item/borg/cyborg_omnitool/engineering/set_internal_tool(obj/item/tool)
+ if(tool_behaviour == TOOL_WELDER)
+ welder_toggle(src, FALSE)
+
+ return ..()
+
+///Reflects internal welder icon onto the omnitool
+/obj/item/borg/cyborg_omnitool/engineering/proc/welder_update(source)
+ PRIVATE_PROC(TRUE)
+ SIGNAL_HANDLER
+
+ update_appearance(UPDATE_OVERLAYS)
+
+///Toggles welder on/off when module slot is selected/deselected
+/obj/item/borg/cyborg_omnitool/engineering/proc/welder_toggle(datum/omnitool, state, mob/self_user)
+ PRIVATE_PROC(TRUE)
+ SIGNAL_HANDLER
+
+ if(tool_behaviour == TOOL_WELDER)
+ var/obj/item/weldingtool/tool = get_proxy_attacker_for(src, usr)
+ if(isnull(state))
+ state = !tool.welding
+ if(state == tool.welding)
+ return
+
+ if(state)
+ RegisterSignal(tool, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(welder_update), override = TRUE)
+ if(self_user)
+ tool.switched_on(self_user)
+ else
+ tool.switched_off()
+ UnregisterSignal(tool, COMSIG_ATOM_UPDATE_APPEARANCE)
/obj/item/borg/cyborg_omnitool/botany
name = "botanical omni-toolset"
diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm
index b5f2d7b78ce8..3d9e29418a10 100644
--- a/code/game/objects/items/robot/robot_upgrades.dm
+++ b/code/game/objects/items/robot/robot_upgrades.dm
@@ -459,10 +459,6 @@
if(!.)
return .
ADD_TRAIT(cyborg, TRAIT_FASTMED, REF(src))
- for(var/obj/item/borg/cyborg_omnitool/medical/omnitool_upgrade in cyborg.model.modules)
- if(omnitool_upgrade.upgraded)
- to_chat(user, span_warning("This unit is already equipped with an omnitool upgrade!"))
- return FALSE
for(var/obj/item/borg/cyborg_omnitool/medical/omnitool in cyborg.model.modules)
omnitool.set_upgraded(TRUE)
@@ -471,7 +467,7 @@
if(!.)
return .
REMOVE_TRAIT(cyborg, TRAIT_FASTMED, REF(src))
- for(var/obj/item/borg/cyborg_omnitool/omnitool in cyborg.model.modules)
+ for(var/obj/item/borg/cyborg_omnitool/medical/omnitool in cyborg.model.modules)
omnitool.set_upgraded(FALSE)
/obj/item/borg/upgrade/engineering_omnitool
@@ -486,24 +482,16 @@
/obj/item/borg/upgrade/engineering_omnitool/action(mob/living/silicon/robot/cyborg, mob/living/user = usr)
. = ..()
if(!.)
- return .
- for(var/obj/item/borg/cyborg_omnitool/engineering/omnitool_upgrade in cyborg.model.modules)
- if(omnitool_upgrade.upgraded)
- to_chat(user, span_warning("This unit is already equipped with an omnitool upgrade!"))
- return FALSE
+ return
for(var/obj/item/borg/cyborg_omnitool/engineering/omnitool in cyborg.model.modules)
omnitool.set_upgraded(TRUE)
- for(var/obj/item/weldingtool/largetank/cyborg/welder in cyborg.model.modules)
- welder.toolspeed = initial(welder.toolspeed) - 0.3
/obj/item/borg/upgrade/engineering_omnitool/deactivate(mob/living/silicon/robot/cyborg, mob/living/user = usr)
. = ..()
if(!.)
- return .
- for(var/obj/item/borg/cyborg_omnitool/omnitool in cyborg.model.modules)
+ return
+ for(var/obj/item/borg/cyborg_omnitool/engineering/omnitool in cyborg.model.modules)
omnitool.set_upgraded(FALSE)
- for(var/obj/item/weldingtool/largetank/cyborg/welder in cyborg.model.modules)
- welder.toolspeed = initial(welder.toolspeed)
/obj/item/borg/upgrade/defib
name = "medical cyborg defibrillator"
diff --git a/code/game/objects/items/stacks/sheets/runed_metal.dm b/code/game/objects/items/stacks/sheets/runed_metal.dm
index 674dea391c1e..e68a1356d0fb 100644
--- a/code/game/objects/items/stacks/sheets/runed_metal.dm
+++ b/code/game/objects/items/stacks/sheets/runed_metal.dm
@@ -39,8 +39,7 @@ GLOBAL_LIST_INIT(runed_metal_recipes, list( \
req_amount = 3, \
time = 4 SECONDS, \
crafting_flags = CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND, \
- desc = span_cult_bold("Daemon Forge: Can make Nar'Sien Hardened Armor, Flagellant's Robes, \
- and Eldritch Longswords. Emits Light."), \
+ desc = span_cult_bold("Daemon Forge: Can make Nar'Sien Hardened Armor and Eldritch Longswords. Emits Light."), \
required_noun = "runed metal sheet", \
category = CAT_CULT, \
), \
diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm
index 11f5de5190bc..8d617c291178 100644
--- a/code/game/objects/items/stacks/stack.dm
+++ b/code/game/objects/items/stacks/stack.dm
@@ -475,8 +475,12 @@
if(isstack(created))
var/obj/item/stack/crafted_stack = created
+ if(recipe.res_amount > 0 && recipe.req_amount != recipe.res_amount)
+ var/scale = recipe.req_amount / recipe.res_amount
+ for(var/mat in result_mats)
+ result_mats[mat] *= scale
crafted_stack.mats_per_unit = SSmaterials.get_material_set_cache(result_mats)
- update_custom_materials()
+ crafted_stack.update_custom_materials()
else
created.set_custom_materials(result_mats, recipe.req_amount * multiplier)
diff --git a/code/game/objects/items/storage/backpack.dm b/code/game/objects/items/storage/backpack.dm
index 82e689e64cf8..ea60130b4fb7 100644
--- a/code/game/objects/items/storage/backpack.dm
+++ b/code/game/objects/items/storage/backpack.dm
@@ -60,7 +60,7 @@
icon_state = "bag_of_holding"
inhand_icon_state = "holdingpack"
resistance_flags = FIRE_PROOF
- item_flags = NO_MAT_REDEMPTION
+ item_flags = NO_MAT_REDEMPTION | BLUESPACE_INTERFERENCE
armor_type = /datum/armor/backpack_holding
storage_type = /datum/storage/bag_of_holding
pickup_sound = null
diff --git a/code/game/objects/items/storage/garment.dm b/code/game/objects/items/storage/garment.dm
index 93b4684db4a7..babc579a95e9 100644
--- a/code/game/objects/items/storage/garment.dm
+++ b/code/game/objects/items/storage/garment.dm
@@ -47,6 +47,7 @@
new /obj/item/clothing/under/rank/captain(src)
new /obj/item/clothing/under/rank/captain/skirt(src)
new /obj/item/clothing/under/rank/captain/parade(src)
+ new /obj/item/clothing/under/rank/captain/royal(src)
new /obj/item/clothing/suit/armor/vest/capcarapace(src)
new /obj/item/clothing/suit/armor/vest/capcarapace/captains_formal(src)
new /obj/item/clothing/suit/hooded/wintercoat/captain(src)
@@ -56,6 +57,7 @@
new /obj/item/clothing/head/costume/crown/fancy(src)
new /obj/item/clothing/head/hats/caphat(src)
new /obj/item/clothing/head/hats/caphat/parade(src)
+ new /obj/item/clothing/head/hats/caphat/bicorne(src)
new /obj/item/clothing/neck/cloak/cap(src)
new /obj/item/clothing/shoes/laceup(src)
new /obj/item/storage/backpack/captain(src)
diff --git a/code/game/objects/items/storage/toolboxes/medical.dm b/code/game/objects/items/storage/toolboxes/medical.dm
index 0c9323212b6b..26630831aef1 100644
--- a/code/game/objects/items/storage/toolboxes/medical.dm
+++ b/code/game/objects/items/storage/toolboxes/medical.dm
@@ -41,4 +41,4 @@
/obj/item/storage/toolbox/medical/coroner/Initialize(mapload)
. = ..()
- AddElement(/datum/element/bane, mob_biotypes = MOB_UNDEAD, damage_multiplier = 1) //Just in case one of the tennants get uppity
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_UNDEAD, damage_multiplier = 2) //Just in case one of the tennants get uppity
diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm
index a022243f9f2a..aff0ccc4e800 100644
--- a/code/game/objects/items/storage/uplink_kits.dm
+++ b/code/game/objects/items/storage/uplink_kits.dm
@@ -491,6 +491,11 @@
new /obj/item/reagent_containers/cup/bottle/amanitin(src)
new /obj/item/reagent_containers/syringe(src)
+/obj/item/storage/box/syndie_kit/carnivorous_blood/PopulateContents()
+ new /obj/item/reagent_containers/cup/bottle/carnivorous_blood(src)
+ new /obj/item/reagent_containers/syringe(src)
+ new /obj/item/food/meat/slab/human(src)
+
/obj/item/storage/box/syndie_kit/nuke
name = "nuke core extraction kit"
desc = "A box containing the equipment and instructions for extracting the plutonium cores of most Nanotrasen nuclear explosives."
@@ -798,6 +803,7 @@
var/datum/antagonist/nukeop/nuke_datum = new()
nuke_datum.send_to_spawnpoint = FALSE
+ nuke_datum.give_bonus_tc = FALSE
nuke_datum.nukeop_outfit = null
human_target.mind?.add_antag_datum(nuke_datum)
human_target.add_faction(ROLE_SYNDICATE)
diff --git a/code/game/objects/items/teleportation.dm b/code/game/objects/items/teleportation.dm
index cc212fe91c7e..6c33c1d2ed0a 100644
--- a/code/game/objects/items/teleportation.dm
+++ b/code/game/objects/items/teleportation.dm
@@ -401,11 +401,9 @@
var/teleport_distance = rand(minimum_teleport_distance, maximum_teleport_distance)
var/turf/destination = get_teleport_loc(current_location, user, teleport_distance)
var/bagholdingcheck = FALSE
- if(iscarbon(user))
- var/mob/living/carbon/teleporting_guy = user
- if(locate(/obj/item/storage/backpack/holding) in teleporting_guy.get_all_gear(INCLUDE_PROSTHETICS|INCLUDE_ABSTRACT|INCLUDE_ACCESSORIES))
+ for(var/obj/item/check as anything in user.get_all_contents_type(/obj/item))
+ if(check.item_flags & BLUESPACE_INTERFERENCE)
bagholdingcheck = TRUE
-
if(isclosedturf(destination))
if(!triggered_by_emp && !bagholdingcheck)
panic_teleport(user, destination) //We're in a wall, engage emergency parallel teleport.
@@ -496,7 +494,7 @@
///Bleed and make blood splatters at tele start and end points
/obj/item/syndicate_teleporter/proc/make_bloods(turf/old_location, turf/new_location, mob/living/user)
- if(!user.can_bleed(BLOOD_COVER_TURFS) != BLEED_SPLATTER)
+ if(user.can_bleed(BLOOD_COVER_TURFS) != BLEED_SPLATTER)
return FALSE
user.add_splatter_floor(old_location)
user.add_splatter_floor(new_location)
diff --git a/code/game/objects/items/tools/engineering/weldingtool.dm b/code/game/objects/items/tools/engineering/weldingtool.dm
index b2803ba409fe..d75700a4cee8 100644
--- a/code/game/objects/items/tools/engineering/weldingtool.dm
+++ b/code/game/objects/items/tools/engineering/weldingtool.dm
@@ -262,13 +262,13 @@
START_PROCESSING(SSobj, src)
else
balloon_alert(user, "no fuel!")
- switched_off(user)
+ switched_off()
else
playsound(loc, deactivation_sound, 50, TRUE)
- switched_off(user)
+ switched_off()
/// Switches the welder off
-/obj/item/weldingtool/proc/switched_off(mob/user)
+/obj/item/weldingtool/proc/switched_off()
set_welding(FALSE)
force = 3
diff --git a/code/game/objects/items/weaponry/melee/baton.dm b/code/game/objects/items/weaponry/melee/baton.dm
index 3c5f0a4991ac..a4ceccaa4ebb 100644
--- a/code/game/objects/items/weaponry/melee/baton.dm
+++ b/code/game/objects/items/weaponry/melee/baton.dm
@@ -1009,22 +1009,19 @@
AddElement(/datum/element/examine_lore, \
lore_hint = span_notice("You can [EXAMINE_HINT("look closer")] to learn a little more about [src]."), \
lore = "The Secure Apprehension Device (sometimes referred to as the SAD in the officer training manuals) is \
- the unholy union of a mace and a cattleprod. This nonlethal device was designed to put a stop to ruffians, \
- scoundrels, ne'er-do-wells and criminals wherever they may rear their ugly heads. \
+ an unholy union of mace and cattleprod. Designed to stop criminals in their tracks, Nanotrasen security members \
+ are rarely without their trusty stun baton. Assuming they haven't lost it somewhere. \
\
- A symbol of Nanotrasen security forces, the stun baton is the primary tool officers employ against the \
- unlawful scum and villainy of the Spinward and abroad. Trained to 'baton first, interrogate later', \
- Nanotrasen security has long since earned itself a mixed reputation. Able to rapidly shut down the \
- central nervous system of a criminal with only a few direct applications of the conductive striking head \
- of the device, few would-be troublemakers want to find themselves on the wrong end of an officer brandishing \
- this baton. \
+ Trained to 'baton first, interrogate later', Nanotrasen security has long since earned itself a mixed reputation. \
+ The device is able to rapidly shut down the central nervous system of a criminal with only a few direct applications \
+ of the conductive striking head. \
\
TerraGov law enforcement has avoided the adoption of stun batons due to various ethical dilemmas posed by \
- their usage, largely because of the longterm physical and mental ramifications of being struck by a human cattleprod. \
- Citizens' rights advocacy groups protest against the proliferation of stun batons as a policing tool, \
- arguing that they are 'inhumane' and 'authoritarian'. Nanotrasen, on the other hand, has had no such qualms \
- when deploying stun batons as a compliance measure across all of their existing stations and facilities against \
- unruly members of staff." \
+ their utilization. Studies of their usage have shown numerous longterm physical and mental ramifications caused by \
+ being struck by a human cattleprod. Citizens' rights advocacy groups protest against the proliferation of stun \
+ batons as a policing tool, arguing that they are 'inhumane' and 'authoritarian'. Nanotrasen, on the other hand, \
+ has had no such qualms when deploying stun batons as a compliance measure across all of their existing stations \
+ and facilities against their own unruly members of staff." \
)
// Contractor Baton
@@ -1033,25 +1030,14 @@
AddElement(/datum/element/examine_lore, \
lore_hint = span_notice("You can [EXAMINE_HINT("look closer")] to learn a little more about [src]."), \
lore = "The Contract Acquisition Device (sometimes referred to as the CAD in encrypted correspondence) is \
- one of the more frequently encountered examples of Cybersun Industries weaponry. Extremely similar to Nanotrasen's \
- own Secure Apprehension Device (also simply known as the stun baton), the contractor baton is able to induce \
- CNS disruption in a target to render them helpless. It is also capable of devastating blunt force trauma if \
- used as a bludgeon. The contractor baton is also capable of telescopic deployment, allowing for discretion while \
- making an approach towards a target. \
+ the most frequently encountered example of Cybersun Industries weaponry. Similar in purpose to Nanotrasen's \
+ own Secure Apprehension Device, the baton is capable of inducing rapid CNS disruption in a target to render them \
+ helpless. It also makes for an effective bludgeon, another quality it shares with the stun baton. To maximize \
+ ease of concealment, the baton is also able to be telescopically collapsed, to then be rapidly deployed at the \
+ pull of a trigger. \
\
- The contractor baton is famously associated with contractors, elite Cybersun field agents. While the standard \
- agent would often be tasked with sabotage, terrorism, murder or theft, contractors have the critical task of \
- kidnapping high value personnel. Anyone with the potential to possess classified or sensitive data about Nanotrasen \
- security systems and devices could be a target for Cybersun. \
- \
- Extracting this information is most easily performed on living subjects. As such, the contractor baton was designed \
- with nonlethal incapacitation in mind. However, Cybersun Industries has long since found workarounds for extracting \
- data from the recently deceased, should the contractor find themselves with only a corpse left to send back. Death \
- may not spare you from the machinations of Cybersun Industries if they deem you a valuable asset towards their goals. \
- \
- Nanotrasen utilizes a number of countermeasures to contractor insurgencies, such as employing selective memory wiping \
- or falsified memory injection, the establishment of 'dummy' command staff through the artificial acceleration \
- of otherwise incompetent but useful crewmembers (whose incompetence will often result in an acceptable degree \
- of operational disruption), which provides convenient scapegoats in the event of a security breach as well as \
- frequent staff turnover and reassignment." \
+ The contractor baton is famously associated with contractors, elite Cybersun field agents sent to kidnap and extract \
+ high value enemy personnel for interrogation. Anyone with the potential to possess classified or sensitive data about \
+ Nanotrasen could find themselves a target for Cybersun. The company relentlessly employs contractors to probe Nanotrasen \
+ for vulnerabilities, starting with their employees." \
)
diff --git a/code/game/objects/items/weaponry/melee/energy.dm b/code/game/objects/items/weaponry/melee/energy.dm
index ca7f75e93a90..93571d4d710a 100644
--- a/code/game/objects/items/weaponry/melee/energy.dm
+++ b/code/game/objects/items/weaponry/melee/energy.dm
@@ -222,48 +222,6 @@
return ..()
-/obj/item/melee/energy/sword/cyborg
- name = "cyborg energy sword"
- sword_color_icon = "red"
- /// The cell cost of hitting something.
- var/hitcost = 0.05 * STANDARD_CELL_CHARGE
-
-/obj/item/melee/energy/sword/cyborg/attack(mob/target, mob/living/silicon/robot/user)
- if(!user.cell)
- return
-
- var/obj/item/stock_parts/power_store/our_cell = user.cell
- if(HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) && !(our_cell.use(hitcost)))
- attack_self(user)
- to_chat(user, span_notice("It's out of charge!"))
- return
- return ..()
-
-/obj/item/melee/energy/sword/cyborg/cyborg_unequip(mob/user)
- if(!HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE))
- return
- attack_self(user)
-
-/obj/item/melee/energy/sword/cyborg/saw //Used by medical Syndicate cyborgs
- name = "energy saw"
- desc = "For heavy duty cutting. It has a carbon-fiber blade in addition to a toggleable hard-light edge to dramatically increase sharpness."
- icon = 'icons/obj/medical/surgery_tools.dmi'
- icon_state = "esaw"
- hitsound = 'sound/items/weapons/circsawhit.ogg'
- force = 18
- hitcost = 0.075 * STANDARD_CELL_CHARGE // Costs more than a standard cyborg esword.
- w_class = WEIGHT_CLASS_NORMAL
- sharpness = SHARP_EDGED
- light_color = LIGHT_COLOR_LIGHT_CYAN
- tool_behaviour = TOOL_SAW
- toolspeed = 0.7 // Faster than a normal saw.
-
- active_force = 30
- sword_color_icon = null // Stops icon from breaking when turned on.
-
-/obj/item/melee/energy/sword/cyborg/saw/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
- return FALSE
-
// The colored energy swords we all know and love.
/obj/item/melee/energy/sword/saber
/// Assoc list of all possible saber colors to color define. If you add a new color, make sure to update /obj/item/toy/sword too!
@@ -325,6 +283,51 @@
to_chat(user, span_warning("RNBW_ENGAGE"))
update_appearance(UPDATE_ICON_STATE)
+/obj/item/melee/energy/sword/saber/cyborg
+ name = "cyborg energy sword"
+ hacked = TRUE
+ sword_color_icon = "rainbow"
+ /// The cell cost of hitting something.
+ var/hitcost = 0.05 * STANDARD_CELL_CHARGE
+
+/obj/item/melee/energy/sword/saber/cyborg/Initialize(mapload)
+ . = ..()
+ set_light_range(5) //Cyborgs don't have inhand sprites, so we compensate by making it glow brightly.
+
+/obj/item/melee/energy/sword/saber/cyborg/attack(mob/target, mob/living/silicon/robot/user)
+ if(!user.cell)
+ return
+
+ var/obj/item/stock_parts/power_store/our_cell = user.cell
+ if(HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) && !(our_cell.use(hitcost)))
+ attack_self(user)
+ to_chat(user, span_notice("It's out of charge!"))
+ return
+ return ..()
+
+/obj/item/melee/energy/sword/saber/cyborg/cyborg_unequip(mob/user)
+ if(!HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE))
+ return
+ attack_self(user)
+
+/obj/item/melee/energy/sword/saber/cyborg/saw //Used by medical Syndicate cyborgs
+ name = "energy saw"
+ desc = "For heavy duty cutting. It has a carbon-fiber blade in addition to a toggleable hard-light edge to dramatically increase sharpness."
+ icon = 'icons/obj/medical/surgery_tools.dmi'
+ icon_state = "esaw"
+ hitsound = 'sound/items/weapons/circsawhit.ogg'
+ force = 18
+ hitcost = 0.075 * STANDARD_CELL_CHARGE // Costs more than a standard cyborg esword.
+ w_class = WEIGHT_CLASS_NORMAL
+ sharpness = SHARP_EDGED
+ light_color = LIGHT_COLOR_LIGHT_CYAN
+ tool_behaviour = TOOL_SAW
+ toolspeed = 0.7 // Faster than a normal saw.
+ hacked = FALSE
+ active_force = 30
+ block_chance = 0 //Unlike assault cyborgs, syndicate medical cyborgs don't get any blocking capabilities
+ sword_color_icon = null // Stops icon from breaking when turned on.
+
/obj/item/melee/energy/sword/pirate
name = "energy cutlass"
desc = "Arrrr matey."
diff --git a/code/game/objects/items/weaponry/melee/misc.dm b/code/game/objects/items/weaponry/melee/misc.dm
index 938af1f1f22a..b4297882ec55 100644
--- a/code/game/objects/items/weaponry/melee/misc.dm
+++ b/code/game/objects/items/weaponry/melee/misc.dm
@@ -488,7 +488,18 @@
/obj/effect/decal/cleanable/ants,
/obj/item/queen_bee,
))
- AddElement(/datum/element/bane, mob_biotypes = MOB_BUG, target_type = /mob/living/basic, damage_multiplier = 0, added_damage = 24, requires_combat_mode = FALSE)
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_BUG, pre_bane_callback = CALLBACK(src, PROC_REF(bane_check)) )
+
+// Different type of bug mobs get different amounts of damage multipliers
+/obj/item/melee/flyswatter/proc/bane_check(mob/living/target, mob/living/attacker, list/attack_modifiers)
+ if(isanimal_or_basicmob(target))
+ MODIFY_ATTACK_FORCE(attack_modifiers, 24)
+ else if(isflyperson(target))
+ MODIFY_ATTACK_FORCE(attack_modifiers, 29)
+ else if(ismoth(target))
+ MODIFY_ATTACK_FORCE(attack_modifiers, 9)
+ else // ?? Whatever
+ MODIFY_ATTACK_FORCE(attack_modifiers, 14)
/obj/item/melee/flyswatter/afterattack(atom/target, mob/user, list/modifiers, list/attack_modifiers)
if(is_type_in_typecache(target, splattable))
diff --git a/code/game/objects/items/weaponry/melee/sabre.dm b/code/game/objects/items/weaponry/melee/sabre.dm
index 0cc900208005..1507193e3e81 100644
--- a/code/game/objects/items/weaponry/melee/sabre.dm
+++ b/code/game/objects/items/weaponry/melee/sabre.dm
@@ -34,29 +34,23 @@
bonus_modifier = 5, \
)
// The weight of authority comes down on the tider's crimes.
- AddElement(/datum/element/bane, target_type = /mob/living/carbon/human, damage_multiplier = 0.35)
- RegisterSignal(src, COMSIG_OBJECT_PRE_BANING, PROC_REF(attempt_bane))
- RegisterSignal(src, COMSIG_OBJECT_ON_BANING, PROC_REF(bane_effects))
-
-/**
- * If the target reeks of maintenance, the blade can tear through their body with a total of 20 damage.
- */
-/obj/item/melee/sabre/proc/attempt_bane(element_owner, mob/living/carbon/criminal)
- SIGNAL_HANDLER
- var/obj/item/organ/liver/liver = criminal.get_organ_slot(ORGAN_SLOT_LIVER)
- if(isnull(liver) || !HAS_TRAIT(liver, TRAIT_MAINTENANCE_METABOLISM))
- return COMPONENT_CANCEL_BANING
-
-/**
- * Assistants should fear this weapon.
- */
-/obj/item/melee/sabre/proc/bane_effects(element_owner, mob/living/carbon/human/baned_target)
- SIGNAL_HANDLER
- baned_target.visible_message(
- span_warning("[src] tears through [baned_target] with unnatural ease!"),
- span_userdanger("As [src] tears into your body, you feel the weight of authority collapse into your wounds!"),
+ AddComponent(/datum/component/bane, \
+ damage_multiplier = 1.35, \
+ should_bane_callback = CALLBACK(src, PROC_REF(bane_check)), \
+ on_bane_callback = CALLBACK(src, PROC_REF(bane_message)), \
+ label_text = "assistants", \
)
- INVOKE_ASYNC(baned_target, TYPE_PROC_REF(/mob/living/carbon/human, emote), "scream")
+
+/obj/item/melee/sabre/proc/bane_check(mob/living/target)
+ var/obj/item/organ/liver/liver = target.get_organ_slot(ORGAN_SLOT_LIVER)
+ return !isnull(liver) && HAS_TRAIT(liver, TRAIT_MAINTENANCE_METABOLISM)
+
+/obj/item/melee/sabre/proc/bane_message(mob/living/target, mob/living/attacker)
+ target.visible_message(
+ span_warning("[src] tears through [target] with unnatural ease!"),
+ span_boldwarning("As [src] tears into your body, you feel the weight of authority collapse into your wounds!"),
+ )
+ INVOKE_ASYNC(target, TYPE_PROC_REF(/mob, emote), "scream")
/obj/item/melee/sabre/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
if(attack_type == PROJECTILE_ATTACK || attack_type == LEAP_ATTACK || attack_type == OVERWHELMING_ATTACK)
diff --git a/code/game/objects/items/weaponry/melee/soulscythe.dm b/code/game/objects/items/weaponry/melee/soulscythe.dm
index 35b27736e133..596166d14e02 100644
--- a/code/game/objects/items/weaponry/melee/soulscythe.dm
+++ b/code/game/objects/items/weaponry/melee/soulscythe.dm
@@ -38,7 +38,7 @@
RegisterSignal(soul, COMSIG_MOB_ATTACK_RANGED, PROC_REF(on_attack))
RegisterSignal(soul, COMSIG_MOB_ATTACK_RANGED_SECONDARY, PROC_REF(on_secondary_attack))
RegisterSignal(src, COMSIG_ATOM_INTEGRITY_CHANGED, PROC_REF(on_integrity_change))
- AddElement(/datum/element/bane, mob_biotypes = MOB_PLANT, damage_multiplier = 0.5, requires_combat_mode = FALSE)
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5)
/obj/item/soulscythe/examine(mob/user)
. = ..()
@@ -263,12 +263,10 @@
/mob/living/basic/soulscythe/Initialize(mapload)
. = ..()
add_traits(list(TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE, TRAIT_LAVA_IMMUNE), INNATE_TRAIT)
- RegisterSignal(src, COMSIG_LIVING_LIFE, PROC_REF(on_life))
-/mob/living/basic/soulscythe/proc/on_life(datum/source, seconds_per_tick) // done like this because there's no need to go through all of life since the item does the work anyways
+/mob/living/basic/soulscythe/Life(seconds_per_tick)
if(stat == CONSCIOUS)
adjust_blood_volume(round(1 * seconds_per_tick), maximum = MAX_BLOOD_LEVEL)
- return COMPONENT_LIVING_CANCEL_LIFE_PROCESSING
/// Special projectile for the soulscythe.
/obj/projectile/soulscythe
diff --git a/code/game/objects/items/weaponry/melee/spear.dm b/code/game/objects/items/weaponry/melee/spear.dm
index c3a87f4f0d63..39ca04c50173 100644
--- a/code/game/objects/items/weaponry/melee/spear.dm
+++ b/code/game/objects/items/weaponry/melee/spear.dm
@@ -462,7 +462,7 @@
/obj/item/spear/dragonator/Initialize(mapload)
. = ..()
- AddElement(/datum/element/bane, mob_biotypes = MOB_MINING, damage_multiplier = 0, added_damage = 80, requires_combat_mode = FALSE) //For killing really big monsters.
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_MINING, added_damage = 80) //For killing really big monsters.
/*
* Untreated Giantslayer , needs to be thrown into lava
diff --git a/code/game/objects/items/weaponry/shields.dm b/code/game/objects/items/weaponry/shields.dm
index 167480f4467b..68c848f3fc2b 100644
--- a/code/game/objects/items/weaponry/shields.dm
+++ b/code/game/objects/items/weaponry/shields.dm
@@ -43,6 +43,7 @@
/obj/item/shield/Initialize(mapload)
. = ..()
AddElement(/datum/element/disarm_attack)
+ AddElement(/datum/element/cuffable_item)
/obj/item/shield/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
var/effective_block_chance = final_block_chance
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index 167ca4221090..3db7bc1c0791 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -357,6 +357,8 @@ GLOBAL_LIST_EMPTY(objects_by_id_tag)
/// Returns modifier to how much damage this object does to a target considered vulnerable to "demolition" (other objects, robots, etc)
/obj/proc/get_demolition_modifier(obj/target)
+ if(HAS_TRAIT(target, TRAIT_IGNORE_DEMOLITION))
+ return 1
if(HAS_TRAIT(target, TRAIT_INVERTED_DEMOLITION))
return (1 / demolition_mod)
return demolition_mod
diff --git a/code/game/objects/structures/crates_lockers/closets/bodybag.dm b/code/game/objects/structures/crates_lockers/closets/bodybag.dm
index b911705b0102..3da2b67101e3 100644
--- a/code/game/objects/structures/crates_lockers/closets/bodybag.dm
+++ b/code/game/objects/structures/crates_lockers/closets/bodybag.dm
@@ -317,15 +317,15 @@
icon_state = "prisonerenvirobag"
foldedbag_path = /obj/item/bodybag/environmental/prisoner
breakout_time = 4 MINUTES // because it's probably about as hard to get out of this as it is to get out of a straightjacket.
- /// How long it takes to sinch the bag.
- var/sinch_time = 10 SECONDS
- /// Whether or not the bag is sinched. Starts unsinched.
- var/sinched = FALSE
- /// The sound that plays when the bag is done sinching.
- var/sinch_sound = 'sound/items/equip/toolbelt_equip.ogg'
+ /// How long it takes to cinch the bag.
+ var/cinch_time = 10 SECONDS
+ /// Whether or not the bag is cinched. Starts uncinched.
+ var/cinched = FALSE
+ /// The sound that plays when the bag is done cinching.
+ var/cinch_sound = 'sound/items/equip/toolbelt_equip.ogg'
/obj/structure/closet/body_bag/environmental/prisoner/attempt_fold(mob/living/carbon/human/the_folder)
- if(sinched)
+ if(cinched)
to_chat(the_folder, span_warning("You wrestle with [src], but it won't fold while its straps are fastened."))
return FALSE
return ..()
@@ -335,23 +335,23 @@
if(!.)
return FALSE
- if(sinched && !force)
- to_chat(user, span_danger("The buckles on [src] are sinched down, preventing it from opening."))
+ if(cinched && !force)
+ to_chat(user, span_danger("The buckles on [src] are cinched down, preventing it from opening."))
return FALSE
- sinched = FALSE //in case it was forced open unsinch it
+ cinched = FALSE //in case it was forced open uncinch it
return TRUE
/obj/structure/closet/body_bag/environmental/prisoner/update_icon()
. = ..()
- if(sinched)
- icon_state = initial(icon_state) + "_sinched"
+ if(cinched)
+ icon_state = initial(icon_state) + "_cinched"
else
icon_state = initial(icon_state)
/obj/structure/closet/body_bag/environmental/prisoner/container_resist_act(mob/living/user, loc_required = TRUE)
// copy-pasted with changes because flavor text as well as some other misc stuff
- if(opened || ismovable(loc) || !sinched)
+ if(opened || ismovable(loc) || !cinched)
return ..()
user.changeNext_move(CLICK_CD_BREAKOUT)
@@ -360,7 +360,7 @@
span_notice("You start wriggling, attempting to loosen [src]'s buckles... (this will take about [DisplayTimeText(breakout_time)].)"), \
span_hear("You hear straining cloth from [src]."))
if(do_after(user,(breakout_time), target = src))
- if(!user || user.stat != CONSCIOUS || user.loc != src || opened || !sinched )
+ if(!user || user.stat != CONSCIOUS || user.loc != src || opened || !cinched )
return
//we check after a while whether there is a point of resisting anymore and whether the user is capable of resisting
user.visible_message(span_danger("[user] successfully broke out of [src]!"),
@@ -374,7 +374,7 @@
/obj/structure/closet/body_bag/environmental/prisoner/bust_open()
- sinched = FALSE
+ cinched = FALSE
// We don't break the bag, because the buckles were backed out as opposed to fully broken.
open()
@@ -393,19 +393,19 @@
return
if(iscarbon(user))
add_fingerprint(user)
- if(!sinched)
+ if(!cinched)
for(var/mob/living/target in contents)
to_chat(target, span_userdanger("You feel the lining of [src] tighten around you! Soon, you won't be able to escape!"))
- user.visible_message(span_notice("[user] begins sinching down the buckles on [src]."))
- if(!(do_after(user,(sinch_time),target = src)))
+ user.visible_message(span_notice("[user] begins cinching down the buckles on [src]."))
+ if(!(do_after(user,(cinch_time),target = src)))
return
- sinched = !sinched
- if(sinched)
- playsound(loc, sinch_sound, 15, TRUE, -2)
- user.visible_message(span_notice("[user] [sinched ? null : "un"]sinches [src]."),
- span_notice("You [sinched ? null : "un"]sinch [src]."),
+ cinched = !cinched
+ if(cinched)
+ playsound(loc, cinch_sound, 15, TRUE, -2)
+ user.visible_message(span_notice("[user] [cinched ? null : "un"]cinches [src]."),
+ span_notice("You [cinched ? null : "un"]cinch [src]."),
span_hear("You hear stretching followed by metal clicking from [src]."))
- user.log_message("[sinched ? "sinched":"unsinched"] secure environmental bag [src]", LOG_GAME)
+ user.log_message("[cinched ? "cinched":"uncinched"] secure environmental bag [src]", LOG_GAME)
update_appearance()
/obj/structure/closet/body_bag/environmental/prisoner/syndicate
@@ -418,7 +418,7 @@
foldedbag_path = /obj/item/bodybag/environmental/prisoner/syndicate
weather_protection = list(TRAIT_WEATHER_IMMUNE)
breakout_time = 8 MINUTES
- sinch_time = 20 SECONDS
+ cinch_time = 20 SECONDS
/obj/structure/closet/body_bag/environmental/prisoner/pressurized/syndicate/refresh_air()
air_contents = null
@@ -430,7 +430,7 @@
air_contents.adjust_gas(/datum/gas/nitrous_oxide, (ONE_ATMOSPHERE*50)/(R_IDEAL_GAS_EQUATION*T20C) * N2STANDARD)
/obj/structure/closet/body_bag/environmental/hardlight
- name = "hardlight bodybag"
+ name = "hardlight body bag"
desc = "A hardlight bag for storing bodies. Resistant to space."
icon_state = "holobag_med"
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
@@ -443,8 +443,8 @@
playsound(src, 'sound/items/weapons/egloves.ogg', 80, TRUE)
/obj/structure/closet/body_bag/environmental/prisoner/hardlight
- name = "hardlight prisoner bodybag"
- desc = "A hardlight bag for storing bodies. Resistant to space, can be sinched to prevent escape."
+ name = "hardlight prisoner body bag"
+ desc = "A hardlight bag for storing bodies. Resistant to space, can be cinched to prevent escape."
icon_state = "holobag_sec"
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
foldedbag_path = null
@@ -456,7 +456,7 @@
playsound(src, 'sound/items/weapons/egloves.ogg', 80, TRUE)
/obj/structure/closet/body_bag/environmental/stasis
- name = "stasis bodybag"
+ name = "stasis body bag"
desc = "A disposable bodybag designed to keep its contents in stasis, preventing decay and further injury. \
The bag itself cannot maintain stasis for long, and will eventually fall apart."
max_integrity = 300
@@ -577,6 +577,7 @@
take_damage(max_integrity * 0.004 * seconds_per_tick, sound_effect = FALSE)
/obj/structure/closet/body_bag/environmental/stasis/after_open(mob/living/user, force = FALSE)
+ . = ..()
if(COOLDOWN_FINISHED(src, freeze_sound_cd) && (locate(/mob/living) in loc))
playsound(src, 'sound/effects/spray.ogg', 25, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE, frequency = 0.4)
COOLDOWN_START(src, freeze_sound_cd, 2 SECONDS)
@@ -588,6 +589,7 @@
RegisterSignal(target, COMSIG_LIVING_EARLY_UNARMED_ATTACK, PROC_REF(skip_to_attack_hand))
/obj/structure/closet/body_bag/environmental/stasis/after_close(mob/living/user)
+ . = ..()
if(COOLDOWN_FINISHED(src, freeze_sound_cd) && (locate(/mob/living) in src))
playsound(src, 'sound/effects/spray.ogg', 25, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE, frequency = 0.5)
COOLDOWN_START(src, freeze_sound_cd, 2 SECONDS)
diff --git a/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm b/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm
index 11f23fb0cecf..11bae7645ddb 100644
--- a/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm
+++ b/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm
@@ -99,18 +99,20 @@
playsound(loc, 'sound/machines/chime.ogg', 50, FALSE, -5)
/// Does the MGS ! animation
-/atom/proc/do_alert_animation()
+/atom/proc/do_alert_animation(duration = 1 SECONDS)
var/mutable_appearance/alert = mutable_appearance('icons/obj/storage/closet.dmi', "cardboard_special")
SET_PLANE_EXPLICIT(alert, ABOVE_LIGHTING_PLANE, src)
- var/atom/movable/flick_visual/exclamation = flick_overlay_view(alert, 1 SECONDS)
+ var/atom/movable/flick_visual/exclamation = flick_overlay_view(alert, duration)
exclamation.alpha = 0
exclamation.pixel_x = -pixel_x
- animate(exclamation, pixel_z = 32, alpha = 255, time = 0.5 SECONDS, easing = ELASTIC_EASING)
+ animate(exclamation, pixel_z = 32, alpha = 255, time = duration * 0.5, easing = ELASTIC_EASING)
+ animate(time = duration * 0.35)
+ animate(pixel_z = 64, alpha = 0, time = duration * 0.15, easing = SINE_EASING)
// We use this list to update plane values on parent z change, which is why we need the timer too
// I'm sorry :(
LAZYADD(update_on_z, exclamation)
// Intentionally less time then the flick so we don't get weird shit
- addtimer(CALLBACK(src, PROC_REF(forget_alert), exclamation), 0.8 SECONDS, TIMER_CLIENT_TIME)
+ addtimer(CALLBACK(src, PROC_REF(forget_alert), exclamation), max(0.05 SECONDS, duration - 0.1 SECONDS), TIMER_CLIENT_TIME)
/atom/proc/forget_alert(atom/movable/flick_visual/exclamation)
LAZYREMOVE(update_on_z, exclamation)
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/cargo.dm b/code/game/objects/structures/crates_lockers/closets/secure/cargo.dm
index 3bfee4282c27..66fa776f403a 100644
--- a/code/game/objects/structures/crates_lockers/closets/secure/cargo.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/cargo.dm
@@ -9,6 +9,7 @@
new /obj/item/radio/weather_monitor (src)
new /obj/item/radio/headset/heads/qm(src)
new /obj/item/megaphone/cargo(src)
+ new /obj/item/assembly/flash/handheld(src)
new /obj/item/tank/internals/emergency_oxygen(src)
new /obj/item/universal_scanner(src)
new /obj/item/door_remote/quartermaster(src)
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/security.dm b/code/game/objects/structures/crates_lockers/closets/secure/security.dm
index d17dd0665ef6..56c61bb39c56 100644
--- a/code/game/objects/structures/crates_lockers/closets/secure/security.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/security.dm
@@ -189,6 +189,9 @@
req_one_access = list(ACCESS_BRIG)
var/id = null
+/obj/structure/closet/secure_closet/brig/holodeck
+ req_one_access = COMMON_ACCESS
+
/obj/structure/closet/secure_closet/brig/genpop
name = "genpop storage locker"
desc = "Used for storing the belongings of genpop's tourists visiting the locals."
diff --git a/code/game/objects/structures/crates_lockers/crates/abandoned_crates/abandoned_crates.dm b/code/game/objects/structures/crates_lockers/crates/abandoned_crates/abandoned_crates.dm
index 6ab6b9ce437a..61034b9f0176 100644
--- a/code/game/objects/structures/crates_lockers/crates/abandoned_crates/abandoned_crates.dm
+++ b/code/game/objects/structures/crates_lockers/crates/abandoned_crates/abandoned_crates.dm
@@ -188,6 +188,12 @@
return ITEM_INTERACT_SUCCESS
+/obj/structure/closet/crate/secure/loot/multitool_act_secondary(mob/living/user, obj/item/tool)
+ if(!locked)
+ return
+ attack_hand(user)
+ return ITEM_INTERACT_SUCCESS
+
/// Implements bulls and cows algorithm to compare guess against actual code
/obj/structure/closet/crate/secure/loot/proc/bulls_and_cows(guess)
var/bulls = 0
diff --git a/code/game/objects/structures/displaycase.dm b/code/game/objects/structures/displaycase.dm
index 3f3d06534a01..efc44e195dd6 100644
--- a/code/game/objects/structures/displaycase.dm
+++ b/code/game/objects/structures/displaycase.dm
@@ -388,7 +388,7 @@
/obj/structure/displaycase/trophy/proc/toggle_historian_mode(mob/user)
historian_mode = !historian_mode
balloon_alert(user, "[historian_mode ? "enabled" : "disabled"] historian mode.")
- playsound(src, 'sound/machines/beep/twobeep.ogg', vary = 50)
+ playsound(src, 'sound/machines/beep/twobeep.ogg', 10, vary = 50)
SStgui.update_uis(src)
/obj/structure/displaycase/trophy/toggle_lock(mob/user)
diff --git a/code/game/objects/structures/electricchair.dm b/code/game/objects/structures/electricchair.dm
index d68457f9baec..ffe4252f6ef3 100644
--- a/code/game/objects/structures/electricchair.dm
+++ b/code/game/objects/structures/electricchair.dm
@@ -8,10 +8,14 @@
/obj/structure/chair/e_chair/Initialize(mapload)
. = ..()
- var/obj/item/assembly/shock_kit/stored_kit = new(contents)
var/mutable_appearance/export_to_component = mutable_appearance('icons/obj/chairs.dmi', "echair_over", OBJ_LAYER, src, appearance_flags = KEEP_APART)
export_to_component = color_atom_overlay(export_to_component)
- AddComponent(/datum/component/electrified_buckle, (SHOCK_REQUIREMENT_ITEM | SHOCK_REQUIREMENT_LIVE_CABLE | SHOCK_REQUIREMENT_SIGNAL_RECEIVED_TOGGLE), stored_kit, list(export_to_component))
+
+ AddComponent(\
+ /datum/component/electrified_buckle,\
+ overlays_to_add = list(export_to_component),\
+ shock_flags = (SHOCK_NOGLOVES)\
+ )
/obj/structure/chair/e_chair/attackby(obj/item/W, mob/user, list/modifiers, list/attack_modifiers)
if(W.tool_behaviour == TOOL_WRENCH)
diff --git a/code/game/objects/structures/flora.dm b/code/game/objects/structures/flora.dm
index 989ebe024f72..3717ccc637f2 100644
--- a/code/game/objects/structures/flora.dm
+++ b/code/game/objects/structures/flora.dm
@@ -308,7 +308,8 @@
/obj/structure/flora/tree/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/seethrough, get_seethrough_map())
+ if(get_seethrough_map())
+ AddComponent(/datum/component/seethrough, get_seethrough_map())
/obj/structure/flora/tree/get_potential_products()
return list(/obj/item/grown/log/tree = 1)
@@ -330,7 +331,7 @@
/obj/structure/flora/tree/stump
name = "stump"
- desc = "This represents our promise to the crew, and the station itself, to cut down as many trees as possible." //running naked through the trees
+ desc = "The remains of a once great tree. A deeply rooted stump." // DARKPACK EDIT CHANGE - (tg lore cruft)
icon = 'icons/obj/fluff/flora/pinetrees.dmi'
icon_state = "tree_stump"
density = FALSE
@@ -345,6 +346,9 @@
to_chat(user, span_notice("You manage to remove [src]."))
qdel(src)
+/obj/structure/flora/tree/stump/get_seethrough_map()
+ return FALSE
+
/obj/structure/flora/tree/dead
icon = 'icons/obj/fluff/flora/deadtrees.dmi'
desc = "A dead tree. How it died, you know not."
@@ -1077,8 +1081,81 @@
/obj/structure/flora/rock/pile/style_random/Initialize(mapload)
. = ..()
- icon_state = "lavarocks[rand(1, 3)]"
+ icon_state = "lavarocks[pick(3;1,3;2,1;3)]"
+ update_appearance()
+
+/obj/structure/flora/rock/pile/siderite
+ icon_state = "lavarocks_siderite1"
+
+/obj/structure/flora/rock/pile/siderite/get_potential_products()
+ return list(/obj/item/stack/ore/glass/siderite = 1)
+
+/obj/structure/flora/rock/pile/siderite/style_2
+ icon_state = "lavarocks_siderite2"
+
+/obj/structure/flora/rock/pile/siderite/style_3
+ icon_state = "lavarocks_siderite3"
+
+/obj/structure/flora/rock/pile/siderite/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "lavarocks_siderite[pick(3;1,3;2,1;3)]"
+ update_appearance()
+
+/obj/structure/flora/rock/pile/shale
+ icon_state = "lavarocks_shale1"
+
+/obj/structure/flora/rock/pile/shale/style_2
+ icon_state = "lavarocks_shale2"
+
+/obj/structure/flora/rock/pile/shale/style_3
+ icon_state = "lavarocks_shale3"
+
+/obj/structure/flora/rock/pile/shale/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "lavarocks_shale[pick(3;1,3;2,1;3)]"
+ update_appearance()
+
+/obj/structure/flora/rock/siderite_growth
+ name = "siderite growth"
+ desc = "A natural stalagmite formed of siderite, with thin pointy strands of dirty metal sticking out of the top."
+ icon_state = "siderite_growth1"
+ base_icon_state = "siderite_growth"
+ icon = 'icons/obj/mining_zones/terrain.dmi'
+ harvest_message_med = "You finish mining the growth."
+
+/obj/structure/flora/rock/siderite_growth/Initialize(mapload)
+ . = ..()
+ icon_state = "[base_icon_state][pick(3;1,3;2,1;3)]"
update_appearance()
+ AddComponent(/datum/component/seethrough, SEE_THROUGH_MAP_DEFAULT)
+
+/obj/structure/flora/rock/siderite_growth/get_potential_products()
+ return list(/obj/item/stack/ore/glass/siderite = 1)
+
+/obj/structure/flora/rock/volcano
+ name = "volcanic pore"
+ desc = "A miniature volcano-like rock formed of the lava slowly pouring from it."
+ icon_state = "volcano_1"
+ base_icon_state = "volcano"
+ harvest_message_med = "You finish mining the pore buildup."
+ light_range = 3
+ light_power = 2.5
+ light_color = LIGHT_COLOR_LAVA
+
+/obj/structure/flora/rock/volcano/Initialize(mapload)
+ . = ..()
+ icon_state = "[base_icon_state]_[rand(1, 4)]"
+ update_appearance()
+
+/obj/structure/flora/rock/volcano/update_overlays()
+ . = ..()
+ . += emissive_appearance(icon, "[icon_state]_e", src, alpha = 200)
+
+/obj/structure/flora/rock/volcano/after_harvest(user)
+ var/turf/open/our_turf = loc
+ if (istype(our_turf))
+ our_turf.ChangeTurf(/turf/open/lava/smooth, flags = CHANGETURF_INHERIT_AIR)
+ return ..()
/obj/structure/flora/rock/pile/jungle
icon_state = "rock1"
diff --git a/code/game/objects/structures/grille.dm b/code/game/objects/structures/grille.dm
index ec618d608314..77e80e95dc46 100644
--- a/code/game/objects/structures/grille.dm
+++ b/code/game/objects/structures/grille.dm
@@ -31,6 +31,7 @@
/obj/structure/grille/Initialize(mapload)
. = ..()
AddElement(/datum/element/atmos_sensitive, mapload)
+ register_context()
/obj/structure/grille/Destroy()
update_cable_icons_on_turf(get_turf(src))
@@ -59,9 +60,19 @@
if(resistance_flags & INDESTRUCTIBLE)
return
if(anchored)
- . += span_notice("It's secured in place with screws. The rods look like they could be cut through.")
+ . += span_notice("It's secured in place with [EXAMINE_HINT("screws")]. The rods look like they could be [EXAMINE_HINT("cut")] through.")
else
- . += span_notice("The anchoring screws are unscrewed. The rods look like they could be cut through.")
+ . += span_notice("The anchoring screws are [EXAMINE_HINT("unscrewed")]. The rods look like they could be [EXAMINE_HINT("cut")] through.")
+
+/obj/structure/grille/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(held_item?.tool_behaviour == TOOL_WIRECUTTER)
+ context[SCREENTIP_CONTEXT_RMB] = "Deconstruct"
+ return CONTEXTUAL_SCREENTIP_SET
+ if(held_item?.tool_behaviour == TOOL_SCREWDRIVER)
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unanchor" : "Anchor"]"
+ return CONTEXTUAL_SCREENTIP_SET
+ return .
/obj/structure/grille/rcd_vals(mob/user, obj/item/construction/rcd/the_rcd)
switch(the_rcd.mode)
@@ -199,7 +210,7 @@
return TRUE
return FALSE
-/obj/structure/grille/wirecutter_act(mob/living/user, obj/item/tool)
+/obj/structure/grille/wirecutter_act_secondary(mob/living/user, obj/item/tool)
add_fingerprint(user)
if(shock(user, 100))
return
diff --git a/code/game/objects/structures/icemoon/cave_entrance.dm b/code/game/objects/structures/icemoon/cave_entrance.dm
index 408e409e5571..b58fff6bb705 100644
--- a/code/game/objects/structures/icemoon/cave_entrance.dm
+++ b/code/game/objects/structures/icemoon/cave_entrance.dm
@@ -66,7 +66,7 @@ GLOBAL_LIST_INIT(ore_probability, list(
/obj/structure/spawner/ice_moon/polarbear
max_mobs = 1
spawn_time = 60 SECONDS
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/polarbear)
+ mob_types = list(/mob/living/basic/mining/polarbear)
mob_gps_id = "BR" // bear
/obj/structure/spawner/ice_moon/polarbear/clear_rock()
diff --git a/code/game/objects/structures/lavaland/necropolis_tendril.dm b/code/game/objects/structures/lavaland/necropolis_tendril.dm
deleted file mode 100644
index e6ca59016ed5..000000000000
--- a/code/game/objects/structures/lavaland/necropolis_tendril.dm
+++ /dev/null
@@ -1,141 +0,0 @@
-//Necropolis Tendrils, which spawn lavaland monsters and break into a chasm when killed
-/obj/structure/spawner/lavaland
- name = "necropolis tendril"
- desc = "A vile tendril of corruption, originating deep underground. Terrible monsters are pouring out of it."
-
- icon = 'icons/mob/simple/lavaland/nest.dmi'
- icon_state = "tendril"
-
- faction = list(FACTION_MINING, FACTION_ASHWALKER)
- max_mobs = 3
- max_integrity = 250
- mob_types = list(/mob/living/basic/mining/watcher)
-
- move_resist=INFINITY // just killing it tears a massive hole in the ground, let's not move it
- anchored = TRUE
- resistance_flags = FIRE_PROOF | LAVA_PROOF
- var/obj/effect/light_emitter/tendril/emitted_light
- scanner_taggable = TRUE
- mob_gps_id = "WT"
- spawner_gps_id = "Necropolis Tendril"
-
-/obj/structure/spawner/lavaland/goliath
- mob_types = list(/mob/living/basic/mining/goliath)
- mob_gps_id = "GL"
-
-/obj/structure/spawner/lavaland/legion
- mob_types = list(/mob/living/basic/mining/legion/spawner_made)
- mob_gps_id = "LG"
-
-/obj/structure/spawner/lavaland/icewatcher
- mob_types = list(/mob/living/basic/mining/watcher/icewing)
- mob_gps_id = "WT|I" // icewing
-
-GLOBAL_LIST_INIT(tendrils, list())
-/obj/structure/spawner/lavaland/Initialize(mapload)
- . = ..()
- emitted_light = new(loc)
- for(var/F in RANGE_TURFS(1, src))
- if(ismineralturf(F))
- var/turf/closed/mineral/M = F
- M.ScrapeAway(null, CHANGETURF_IGNORE_AIR)
- AddComponent(/datum/component/gps, "Eerie Signal")
- GLOB.tendrils += src
-
-/obj/structure/spawner/lavaland/atom_deconstruct(disassembled)
- new /obj/effect/collapse(loc)
-
-/obj/structure/spawner/lavaland/examine(mob/user)
- var/list/examine_messages = ..()
- examine_messages += span_notice("Once this thing gets hurts enough, it triggers a violent final retaliation.")
- examine_messages += span_notice("You'll only have a few moments to run up, grab some loot with an open hand, and get out with it.")
- return examine_messages
-
-/obj/structure/spawner/lavaland/Destroy()
- var/last_tendril = TRUE
- if(GLOB.tendrils.len>1)
- last_tendril = FALSE
-
- if(last_tendril && !(flags_1 & ADMIN_SPAWNED_1))
- if(SSachievements.achievements_enabled)
- for(var/mob/living/L in view(7,src))
- if(L.stat || !L.client)
- continue
- L.client.give_award(/datum/award/achievement/boss/tendril_exterminator, L)
- L.client.give_award(/datum/award/score/tendril_score, L) //Progresses score by one
- GLOB.tendrils -= src
- QDEL_NULL(emitted_light)
- return ..()
-
-/obj/effect/light_emitter/tendril
- set_luminosity = 4
- set_cap = 2.5
- light_color = LIGHT_COLOR_LAVA
-
-/obj/effect/collapse
- name = "collapsing necropolis tendril"
- desc = "Get your loot and get clear!"
- layer = TABLE_LAYER
- icon = 'icons/mob/simple/lavaland/nest.dmi'
- icon_state = "tendril"
- anchored = TRUE
- density = TRUE
- /// weakref list of which mobs have gotten their loot from this effect.
- var/list/collected = list()
- /// a bit of light as to make less unfair deaths from the chasm
- var/obj/effect/light_emitter/tendril/emitted_light
-
-/obj/effect/collapse/Initialize(mapload)
- . = ..()
- emitted_light = new(loc)
- visible_message(span_bolddanger("The tendril writhes in fury as the earth around it begins to crack and break apart! Get back!"))
- balloon_alert_to_viewers("interact to grab loot before collapse!", vision_distance = 7)
- playsound(loc,'sound/effects/tendril_destroyed.ogg', 200, FALSE, 50, TRUE, TRUE)
- addtimer(CALLBACK(src, PROC_REF(collapse)), 5 SECONDS)
-
-/obj/effect/collapse/examine(mob/user)
- var/list/examine_messages = ..()
- if(isliving(user))
- if(has_collected(user))
- examine_messages += span_boldnotice("You've grabbed what you can, now get out!")
- else
- examine_messages += span_boldnotice("You might have some time to grab some goodies with an open hand before it collapses!")
- return examine_messages
-
-/obj/effect/collapse/attack_hand(mob/living/collector, list/modifiers)
- . = ..()
- if(has_collected(collector))
- to_chat(collector, span_danger("You've already gotten some loot, just get out of there with it!"))
- return
- visible_message(span_warning("Something falls free of the tendril!"))
- var/obj/structure/closet/crate/necropolis/tendril/loot = new /obj/structure/closet/crate/necropolis/tendril(loc)
- collector.start_pulling(loot)
- collected += WEAKREF(collector)
-
-/obj/effect/collapse/Destroy()
- collected.Cut()
- QDEL_NULL(emitted_light)
- return ..()
-
-///Helper proc that resolves weakrefs to determine if collector is in collected list, returning a boolean.
-/obj/effect/collapse/proc/has_collected(mob/collector)
- for(var/datum/weakref/weakref as anything in collected)
- var/mob/living/resolved = weakref.resolve()
- //it could have been collector, it could not have been, we don't care
- if(!resolved)
- continue
- if(resolved == collector)
- return TRUE
- return FALSE
-
-/obj/effect/collapse/proc/collapse()
- for(var/mob/M in range(7,src))
- shake_camera(M, 15, 1)
- playsound(get_turf(src),'sound/effects/explosion/explosionfar.ogg', 200, TRUE)
- visible_message(span_bolddanger("The tendril falls inward, the ground around it widening into a yawning chasm!"))
- for(var/turf/T in RANGE_TURFS(2,src))
- if(HAS_TRAIT(T, TRAIT_NO_TERRAFORM))
- continue
- if(!T.density)
- T.TerraformTurf(/turf/open/chasm/lavaland, /turf/open/chasm/lavaland, flags = CHANGETURF_INHERIT_AIR)
- qdel(src)
diff --git a/code/game/objects/structures/lavaland/ore_vent.dm b/code/game/objects/structures/lavaland/ore_vent.dm
index 38e5ac835371..3ab9542f6301 100644
--- a/code/game/objects/structures/lavaland/ore_vent.dm
+++ b/code/game/objects/structures/lavaland/ore_vent.dm
@@ -1,4 +1,4 @@
-#define MAX_ARTIFACT_ROLL_CHANCE 10
+#define ARTIFACT_ROLL_CHANCE 7
#define MINERAL_TYPE_OPTIONS_RANDOM 4
#define OVERLAY_OFFSET_START 0
#define OVERLAY_OFFSET_EACH 5
@@ -72,7 +72,7 @@
/// What base icon_state do we use for this vent's boulders?
var/boulder_icon_state = "boulder"
/// Percent chance that this vent will produce an artifact boulder.
- var/artifact_chance = 0
+ var/artifact_chance = ARTIFACT_ROLL_CHANCE
/// We use a cooldown to prevent the wave defense from being started multiple times.
COOLDOWN_DECLARE(wave_cooldown)
/// We use a cooldown to prevent players from tapping boulders rapidly from vents.
@@ -161,6 +161,8 @@
. += span_notice("This vent produces [span_bold("large")] boulders containing [ore_string]")
else
. += span_notice("This vent can be scanned with a [span_bold("Mining Scanner")].")
+ if(artifact_chance)
+ . += span_notice("This vent has a low chance to produce an [span_bold("artifact boulder.")] These may contain rare minerals or strange artifacts.")
/obj/structure/ore_vent/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
if(is_type_in_list(held_item, scanning_equipment))
@@ -526,6 +528,11 @@
var/atom/movable/flick_visual/visual = flick_overlay_view(mutable_appearance('icons/effects/vent_overlays.dmi', selected_mat.name), 4.5 SECONDS)
animate(visual, alpha = 0, time = 4.5 SECONDS, easing = CIRCULAR_EASING|EASE_IN)
+ if(artifact_chance)
+ var/atom/movable/flick_visual/rare = flick_overlay_view(mutable_appearance('icons/effects/vent_overlays.dmi', "rare_ore"), 4.5 SECONDS)
+ animate(rare, alpha = 0, time = 4.5 SECONDS, easing = CIRCULAR_EASING|EASE_IN)
+
+
/**
* Here is where we handle producing a new boulder, based on the qualities of this ore vent.
* Returns the boulder produced.
@@ -541,7 +548,8 @@
//produce the boulder
var/obj/item/boulder/new_rock
if(prob(artifact_chance))
- new_rock = new /obj/item/boulder/artifact(loc)
+ var/picked_artifact = pick(typesof(/obj/item/boulder/artifact))
+ new_rock = new picked_artifact(loc)
else
new_rock = new /obj/item/boulder(loc)
Shake(duration = 1.5 SECONDS)
@@ -646,6 +654,10 @@
/datum/material/glass = 1,
)
+/obj/structure/ore_vent/starter_resources/Initialize(mapload)
+ . = ..()
+ generate_description()
+
/obj/structure/ore_vent/random
// Todo: determine if we need a boulder_size default thats unique from the override performed in vent_size_setup.
@@ -654,7 +666,6 @@
if(!unique_vent && !mapload)
generate_mineral_breakdown(map_loading = mapload) //Default to random mineral breakdowns, unless this is a unique vent or we're still setting up default vent distribution.
generate_description()
- artifact_chance = rand(0, MAX_ARTIFACT_ROLL_CHANCE)
if(!mapload)
vent_size_setup(random = TRUE) // We only do this here specific to random distribution ore vents, and within mapload we handle this manually within SSore_generation.
@@ -666,7 +677,7 @@
/mob/living/basic/mining/lobstrosity,
/mob/living/basic/mining/legion/snow/spawner_made,
/mob/living/basic/mining/wolf,
- /mob/living/simple_animal/hostile/asteroid/polarbear,
+ /mob/living/basic/mining/polarbear,
)
ore_vent_options = list(
SMALL_VENT_TYPE,
@@ -679,7 +690,7 @@
/mob/living/basic/mining/legion/snow/spawner_made,
/mob/living/basic/mining/ice_demon,
/mob/living/basic/mining/wolf,
- /mob/living/simple_animal/hostile/asteroid/polarbear,
+ /mob/living/basic/mining/polarbear,
)
ore_vent_options = list(
SMALL_VENT_TYPE = 3,
@@ -700,9 +711,9 @@
/datum/material/titanium = 1,
/datum/material/silver = 1,
/datum/material/gold = 1,
- /datum/material/diamond = 1,
+ /datum/material/diamond = 0.1,
/datum/material/uranium = 1,
- /datum/material/bluespace = 1,
+ /datum/material/bluespace = 0.1,
/datum/material/plastic = 1,
)
defending_mobs = list(
@@ -785,6 +796,7 @@
var/value = tgui_input_number(user, "What weight should it have?", "ore pickweight", 1, 100, 1)
mineral_breakdown[choice] = value
balloon_alert_to_viewers("weighting of [value] added")
+ generate_description()
/obj/structure/ore_vent/debug/attack_hand_secondary(mob/user, list/modifiers)
. = ..()
@@ -807,7 +819,7 @@
GLOB.mining_center += loc
return INITIALIZE_HINT_QDEL
-#undef MAX_ARTIFACT_ROLL_CHANCE
+#undef ARTIFACT_ROLL_CHANCE
#undef MINERAL_TYPE_OPTIONS_RANDOM
#undef OVERLAY_OFFSET_START
#undef OVERLAY_OFFSET_EACH
diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm
index 718a527a85cf..ee4b16dc10d6 100644
--- a/code/game/objects/structures/mirror.dm
+++ b/code/game/objects/structures/mirror.dm
@@ -15,6 +15,9 @@
#define PRIDE_MIRROR_OPTIONS list(CHANGE_HAIR, CHANGE_BEARD, CHANGE_RACE, CHANGE_SEX, CHANGE_EYES)
#define MAGIC_MIRROR_OPTIONS list(CHANGE_HAIR, CHANGE_BEARD, CHANGE_RACE, CHANGE_SEX, CHANGE_EYES, CHANGE_NAME)
+// Chance for the mirror to be haunted at creation
+#define ROUNDSTART_CURSED_CHANCE 0.2
+
/obj/structure/mirror
name = "mirror"
desc = "Mirror mirror on the wall, who's the most robust of them all?"
@@ -29,7 +32,8 @@
///Can this mirror be removed from walls with tools?
var/deconstructable = TRUE
var/list/mirror_options = INERT_MIRROR_OPTIONS
-
+ // Can a revenant be imprisoned in this mirror?
+ var/cursable = TRUE
///Flags this race must have to be selectable with this type of mirror.
var/race_flags = MIRROR_MAGIC
///List of all Races that can be chosen, decided by its Initialize.
@@ -57,6 +61,8 @@
)
if(mapload)
find_and_mount_on_atom()
+ if(prob(ROUNDSTART_CURSED_CHANCE) && cursable)
+ AddComponent(/datum/component/revenant_prison, create_on_release = TRUE)
update_choices()
register_context()
@@ -72,10 +78,12 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror, 28)
/obj/structure/mirror/broken
icon_state = "mirror_broke"
+ cursable = FALSE
+ broken = TRUE
+ desc = "Oh no, seven years of bad luck!"
/obj/structure/mirror/broken/Initialize(mapload)
. = ..()
- atom_break(null, mapload)
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
@@ -302,13 +310,12 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
to_chat(unlucky_dude, span_warning("A chill runs down your spine as [src] shatters..."))
unlucky_dude.AddComponent(/datum/component/omen, incidents_left = 7)
-/obj/structure/mirror/atom_break(damage_flag, mapload)
+/obj/structure/mirror/atom_break(damage_flag)
. = ..()
if(broken)
return
icon_state = "mirror_broke"
- if(!mapload)
- playsound(src, SFX_SHATTER, 70, TRUE)
+ playsound(src, SFX_SHATTER, 70, TRUE)
if(desc == initial(desc))
desc = "Oh no, seven years of bad luck!"
broken = TRUE
@@ -361,6 +368,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
icon_state = "magic_mirror"
mirror_options = MAGIC_MIRROR_OPTIONS
deconstructable = FALSE
+ cursable = FALSE
/obj/structure/mirror/magic/Initialize(mapload)
. = ..()
@@ -463,3 +471,5 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
#undef INERT_MIRROR_OPTIONS
#undef PRIDE_MIRROR_OPTIONS
#undef MAGIC_MIRROR_OPTIONS
+
+#undef ROUNDSTART_CURSED_CHANCE
diff --git a/code/game/objects/structures/morgue.dm b/code/game/objects/structures/morgue.dm
index 72159ef326e4..3fb3ff29410a 100644
--- a/code/game/objects/structures/morgue.dm
+++ b/code/game/objects/structures/morgue.dm
@@ -462,50 +462,55 @@ GLOBAL_LIST_EMPTY(crematoriums)
/obj/structure/bodycontainer/crematorium/proc/cremate(mob/user)
if(locked)
return //don't let you cremate something twice or w/e
+
// Make sure we don't delete the actual morgue and its tray
var/list/conts = get_all_contents() - src - connected
-
- if(!conts.len)
+ if(!length(conts))
audible_message(span_hear("You hear a hollow crackle."))
return
- else
- audible_message(span_hear("You hear a roar as the crematorium activates."))
+ audible_message(span_hear("You hear a roar as the crematorium activates."))
+ locked = TRUE
+ update_appearance()
- locked = TRUE
- update_appearance()
+ for(var/mob/living/victim in conts)
+ if(victim.incorporeal_move) //can't cook revenants!
+ continue
- for(var/mob/living/M in conts)
- if(M.incorporeal_move) //can't cook revenants!
- continue
- if (M.stat != DEAD)
- M.emote("scream")
- if(user)
- log_combat(user, M, "cremated")
- else
- M.log_message("was cremated", LOG_ATTACK)
-
- if(user.stat != DEAD)
- user.investigate_log("has died from being cremated.", INVESTIGATE_DEATHS)
- M.death(TRUE)
- if(!QDELETED(M)) //some animals get automatically deleted on death.
- M.ghostize()
- qdel(M)
-
- for(var/obj/O in conts) //conts defined above, ignores crematorium and tray
- if(istype(O, /obj/effect/dummy/phased_mob)) //they're not physical, don't burn em.
- continue
- qdel(O)
+ if (victim.stat != DEAD)
+ victim.emote("scream")
+
+ if(user)
+ log_combat(user, victim, "cremated")
+ else
+ victim.log_message("was cremated", LOG_ATTACK)
- if(!locate(/obj/effect/decal/cleanable/ash) in get_step(src, dir))//prevent pile-up
- new/obj/effect/decal/cleanable/ash(src)
+ if(user.stat != DEAD)
+ user.investigate_log("has died from being cremated.", INVESTIGATE_DEATHS)
- sleep(3 SECONDS)
+ victim.death(TRUE)
+ if(!QDELETED(victim)) //some animals get automatically deleted on death.
+ victim.ghostize()
+ qdel(victim)
- if(!QDELETED(src))
- locked = FALSE
- update_appearance()
- playsound(src.loc, 'sound/machines/ding.ogg', 50, TRUE) //you horrible people
+ for(var/obj/to_destroy in conts) // conts defined above, ignores crematorium and tray
+ // Indestructible atoms should not be destroyed
+ if(istype(to_destroy, /obj/effect/dummy/phased_mob) || (to_destroy.resistance_flags & INDESTRUCTIBLE))
+ continue
+ qdel(to_destroy)
+
+ addtimer(CALLBACK(src, PROC_REF(unlock)), 3 SECONDS)
+
+/obj/structure/bodycontainer/crematorium/proc/unlock()
+ if(QDELETED(src))
+ return
+
+ if(!locate(/obj/effect/decal/cleanable/ash) in get_step(src, dir)) // As to prevent pile-up
+ new /obj/effect/decal/cleanable/ash(src)
+
+ locked = FALSE
+ update_appearance()
+ playsound(src.loc, 'sound/machines/ding.ogg', 50, TRUE) // You horrible people
/obj/structure/bodycontainer/crematorium/creamatorium
name = "creamatorium"
@@ -579,8 +584,8 @@ GLOBAL_LIST_EMPTY(crematoriums)
if(!istype(O, /obj/structure/closet/body_bag))
return
else
- var/mob/M = O
- if(M.buckled)
+ var/mob/victim = O
+ if(victim.buckled)
return
O.forceMove(src.loc)
if (user != O)
diff --git a/code/game/objects/structures/mystery_box.dm b/code/game/objects/structures/mystery_box.dm
index 28700113381e..0e153b328131 100644
--- a/code/game/objects/structures/mystery_box.dm
+++ b/code/game/objects/structures/mystery_box.dm
@@ -189,7 +189,7 @@ GLOBAL_LIST_INIT(mystery_fishing, list(
presented_item.vis_flags = VIS_INHERIT_PLANE
vis_contents += presented_item
presented_item.start_animation(src)
- current_sound_channel = SSsounds.reserve_sound_channel(src)
+ current_sound_channel = SSsounds.reserve_sound_channel_for_datum(src)
playsound(src, open_sound, 70, FALSE, channel = current_sound_channel, falloff_exponent = 10)
playsound(src, crate_open_sound, 80)
if(user.mind)
diff --git a/code/game/sound/sound.dm b/code/game/sound/sound.dm
index f8af564b25a3..122b29f8ae17 100644
--- a/code/game/sound/sound.dm
+++ b/code/game/sound/sound.dm
@@ -14,8 +14,9 @@
* * ignore_walls - Whether or not the sound can pass through walls.
* * falloff_distance - Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range.
* * volume_preference - Optional: Will be checked to modify the volume of the sound for each listener.
+ * * min_volume - minimum volume the sound can reach at max_range.
*/
-/proc/playsound(atom/source, soundin, vol as num, vary, extrarange as num, falloff_exponent = SOUND_FALLOFF_EXPONENT, frequency = null, channel = 0, pressure_affected = TRUE, ignore_walls = TRUE, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, use_reverb = TRUE, datum/preference/numeric/volume/volume_preference = null)
+/proc/playsound(atom/source, soundin, vol as num, vary, extrarange as num, falloff_exponent = SOUND_FALLOFF_EXPONENT, frequency = null, channel = 0, pressure_affected = TRUE, ignore_walls = TRUE, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, use_reverb = TRUE, datum/preference/numeric/volume/volume_preference = null, min_volume = 3)
if(isarea(source))
CRASH("playsound(): source is an area")
@@ -25,8 +26,6 @@
if(!soundin)
CRASH("playsound(): no soundin passed")
- if(vol < SOUND_AUDIBLE_VOLUME_MIN) // never let sound go below SOUND_AUDIBLE_VOLUME_MIN or bad things will happen
- CRASH("playsound(): volume below SOUND_AUDIBLE_VOLUME_MIN. [vol] < [SOUND_AUDIBLE_VOLUME_MIN]")
var/turf/turf_source = get_turf(source)
if (!turf_source)
@@ -62,30 +61,28 @@
var/turf/above_turf = GET_TURF_ABOVE(turf_source)
var/turf/below_turf = GET_TURF_BELOW(turf_source)
- var/audible_distance = CALCULATE_MAX_SOUND_AUDIBLE_DISTANCE(vol, maxdistance, falloff_distance, falloff_exponent)
-
if(ignore_walls)
- listeners = get_hearers_in_range(audible_distance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners = get_hearers_in_range(maxdistance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
if(above_turf && istransparentturf(above_turf))
- listeners += get_hearers_in_range(audible_distance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners += get_hearers_in_range(maxdistance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
if(below_turf && istransparentturf(turf_source))
- listeners += get_hearers_in_range(audible_distance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners += get_hearers_in_range(maxdistance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
else //these sounds don't carry through walls
- listeners = get_hearers_in_view(audible_distance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners = get_hearers_in_view(maxdistance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
if(above_turf && istransparentturf(above_turf))
- listeners += get_hearers_in_view(audible_distance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners += get_hearers_in_view(maxdistance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
if(below_turf && istransparentturf(turf_source))
- listeners += get_hearers_in_view(audible_distance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners += get_hearers_in_view(maxdistance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
for(var/mob/listening_ghost as anything in SSmobs.dead_players_by_zlevel[source_z])
- if(get_dist(listening_ghost, turf_source) <= audible_distance)
- listeners += listening_ghost
+ listeners += listening_ghost
for(var/mob/listening_mob in listeners)//had nulls sneak in here, hence the typecheck
- listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb, volume_preference)
+ if(get_dist_euclidean(listening_mob, turf_source) <= maxdistance)
+ listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb, volume_preference)
return listeners
@@ -108,8 +105,9 @@
* * distance_multiplier - Default 1, multiplies the maximum distance of our sound
* * use_reverb - bool default TRUE, determines if our sound has reverb
* * volume_preference - Optional: Will be checked to modify the volume of the sound.
+ * * min_volume - minimum volume the sound can reach at max_range.
*/
-/mob/proc/playsound_local(turf/turf_source, soundin, vol as num, vary, frequency, falloff_exponent = SOUND_FALLOFF_EXPONENT, channel = 0, pressure_affected = TRUE, sound/sound_to_use, max_distance, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, distance_multiplier = 1, use_reverb = TRUE, datum/preference/numeric/volume/volume_preference = null)
+/mob/proc/playsound_local(turf/turf_source, soundin, vol as num, vary, frequency, falloff_exponent = SOUND_FALLOFF_EXPONENT, channel = 0, pressure_affected = TRUE, sound/sound_to_use, max_distance, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, distance_multiplier = 1, use_reverb = TRUE, datum/preference/numeric/volume/volume_preference = null, min_volume = 5)
if(!client || HAS_TRAIT(src, TRAIT_DEAF))
return
@@ -132,10 +130,10 @@
var/turf/turf_loc = get_turf(src)
//sound volume falloff with distance
- distance = get_dist(turf_loc, turf_source) * distance_multiplier
+ distance = get_dist_euclidean(turf_loc, turf_source) * distance_multiplier
if(max_distance) //If theres no max_distance we're not a 3D sound, so no falloff.
- sound_to_use.volume -= CALCULATE_SOUND_VOLUME(vol, distance, max_distance, falloff_distance, falloff_exponent)
+ sound_to_use.volume -= CALCULATE_SOUND_VOLUME_RATIO(vol, distance, max_distance, falloff_distance, falloff_exponent) * (vol - min_volume)
if(pressure_affected)
//Atmosphere affects sound
@@ -156,9 +154,6 @@
sound_to_use.volume *= pressure_factor
//End Atmosphere affecting sound
- if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN)
- return //No sound
-
var/dx = turf_source.x - turf_loc.x // Hearing from the right/left
sound_to_use.x = dx * distance_multiplier
var/dz = turf_source.y - turf_loc.y // Hearing from infront/behind
@@ -186,13 +181,14 @@
if(ispath(volume_preference) && client.prefs)
var/client_volume_modifier = client.prefs.read_preference(volume_preference)
sound_to_use.volume *= (client_volume_modifier / 100)
- if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN)
- return
+ if(sound_to_use.volume < 0.1)
+ return FALSE
if(HAS_TRAIT(src, TRAIT_SOUND_DEBUGGED))
to_chat(src, span_admin("Max Range-[max_distance] Distance-[distance] Vol-[round(sound_to_use.volume, 0.01)] Sound-[sound_to_use.file]"))
SEND_SOUND(src, sound_to_use)
+ return TRUE
/proc/sound_to_playing_players(soundin, volume = 100, vary = FALSE, frequency = 0, channel = 0, pressure_affected = FALSE, sound/S, datum/preference/numeric/volume/volume_preference = null)
if(!S)
diff --git a/code/game/turfs/closed/minerals.dm b/code/game/turfs/closed/minerals.dm
index d21d2377be45..37575ccffa85 100644
--- a/code/game/turfs/closed/minerals.dm
+++ b/code/game/turfs/closed/minerals.dm
@@ -47,6 +47,8 @@
var/hand_mine_speed = 15 SECONDS
/// Distance to the nearest open turf
var/open_turf_distance = DEFAULT_BORDER_DISTANCE
+ /// Wall plane overlay icon state
+ var/wall_icon_state = "rock"
/turf/closed/mineral/Initialize(mapload)
. = ..()
@@ -55,7 +57,11 @@
// So we draw them as if they were on the game plane, and then overlay a copy onto
// The wall plane (so emissives/light masks behave)
// I am so sorry
- var/static/mutable_appearance/wall_overlay = mutable_appearance('icons/turf/mining.dmi', "rock", appearance_flags = RESET_TRANSFORM)
+ var/static/list/mutable_appearance/wall_overlays = list()
+ var/mutable_appearance/wall_overlay = wall_overlays[wall_icon_state]
+ if (!wall_overlay)
+ wall_overlay = mutable_appearance('icons/turf/mining.dmi', wall_icon_state, appearance_flags = RESET_TRANSFORM)
+ wall_overlays[wall_icon_state] = wall_overlay
wall_overlay.plane = MUTATE_PLANE(WALL_PLANE, src)
overlays += wall_overlay
@@ -608,6 +614,9 @@
tool_mine_speed = 5 SECONDS // 25% harder than basalt
hand_mine_speed = 17 SECONDS
mineral_chance = 8 // N% functionally, 6.67% default, accounts for ~22% turfs
+ wall_icon_state = "red_rock"
+ turf_type = /turf/open/misc/asteroid/basalt/smooth/siderite/lava_land_surface
+ baseturfs = /turf/open/misc/asteroid/basalt/smooth/siderite/lava_land_surface
/turf/closed/mineral/random/volcanic/red_rock/mineral_chances()
return list(
@@ -634,6 +643,9 @@
tool_mine_speed = 7 SECONDS // 75% harder than basalt
hand_mine_speed = 20 SECONDS
mineral_chance = 8 // N% functionally, 7.01% default, accounts for ~13% turfs
+ wall_icon_state = "shale"
+ turf_type = /turf/open/misc/asteroid/basalt/smooth/shale/lava_land_surface
+ baseturfs = /turf/open/misc/asteroid/basalt/smooth/shale/lava_land_surface
/turf/closed/mineral/random/volcanic/shale/mineral_chances()
return list(
@@ -929,28 +941,34 @@
icon = 'icons/turf/mining.dmi'
icon_state = "volcanic_biome"
smoothing_flags = NONE
+ /// Area type to whose generator we try to default to
+ var/default_area = /area/lavaland/surface/outdoors/unexplored/danger
/turf/closed/mineral/volcanic/lava_land_surface/biome_replace/Initialize(mapload)
. = ..()
- var/area/cur_area = loc
- if (!cur_area) // what
- return
-
// Just spawn a normal lavaland rock if we fail to get a mapgen, such as being spawned over lava
var/supposed_type = /turf/closed/mineral/volcanic/lava_land_surface
- var/datum/map_generator/cave_generator/map_generator = cur_area.get_generator()
+ var/area/cur_area = loc
+ var/datum/map_generator/cave_generator/map_generator = cur_area?.get_generator()
+ if (!map_generator)
+ cur_area = GLOB.areas_by_type[default_area]
+ map_generator = cur_area?.get_generator()
+
if (istype(map_generator))
- for (var/datum/biome/biome as anything in map_generator.generated_turfs_per_biome)
- var/list/gen_turfs = map_generator.generated_turfs_per_biome[biome]
- if (gen_turfs[src])
- // Ignore what we were supposed to spawn as in favor of the closed turf
- supposed_type = biome.closed_turf_type
- break
-
- var/turf/new_turf = new supposed_type(src)
- if(turf_flags & NO_RUINS)
+ var/biome = map_generator.get_biome_for_turf(src)
+ if (biome)
+ var/datum/biome/generating_biome = SSmapping.biomes[biome]
+ supposed_type = generating_biome.closed_turf_type
+
+ var/cur_flags = turf_flags
+ var/turf/new_turf = ChangeTurf(supposed_type, flags = CHANGETURF_FORCEOP | CHANGETURF_INHERIT_AIR)
+ if(cur_flags & NO_RUINS)
new_turf.turf_flags |= NO_RUINS
+/// A turf that can't we can't build openspace chasms on or spawn ruins in.
+/turf/closed/mineral/volcanic/lava_land_surface/do_not_chasm
+ turf_flags = NO_RUINS
+
/// Wall piece
/turf/closed/mineral/ash_rock
name = "rock"
@@ -1020,6 +1038,7 @@
transform = MAP_SWITCH(TRANSLATE_MATRIX(-8, -8), matrix())
smoothing_groups = SMOOTH_GROUP_CLOSED_TURFS + SMOOTH_GROUP_RED_ROCK_WALLS
canSmoothWith = SMOOTH_GROUP_RED_ROCK_WALLS
+ wall_icon_state = "red_rock"
/turf/closed/mineral/random/stationside/asteroid
name = "iron rock"
@@ -1029,6 +1048,7 @@
transform = MAP_SWITCH(TRANSLATE_MATRIX(-8, -8), matrix())
smoothing_groups = SMOOTH_GROUP_CLOSED_TURFS + SMOOTH_GROUP_RED_ROCK_WALLS
canSmoothWith = SMOOTH_GROUP_RED_ROCK_WALLS
+ wall_icon_state = "red_rock"
/turf/closed/mineral/random/stationside/asteroid/porus
name = "porous iron rock"
@@ -1169,6 +1189,7 @@
canSmoothWith = SMOOTH_GROUP_RED_ROCK_WALLS
tool_mine_speed = 5 SECONDS // 25% harder than basalt
hand_mine_speed = 17 SECONDS
+ wall_icon_state = "red_rock"
/turf/closed/mineral/gibtonite/volcanic/shale
name = "shale"
@@ -1180,6 +1201,7 @@
canSmoothWith = SMOOTH_GROUP_SHALE_WALLS
tool_mine_speed = 7 SECONDS // 75% harder than basalt
hand_mine_speed = 20 SECONDS
+ wall_icon_state = "shale"
/turf/closed/mineral/gibtonite/volcanic/airless
turf_type = /turf/open/misc/asteroid/basalt
diff --git a/code/game/turfs/open/asteroid.dm b/code/game/turfs/open/asteroid.dm
index 9675f0c45d3d..c441871caa82 100644
--- a/code/game/turfs/open/asteroid.dm
+++ b/code/game/turfs/open/asteroid.dm
@@ -140,6 +140,7 @@ GLOBAL_LIST_EMPTY(dug_up_basalt)
base_icon_state = "basalt"
floor_variance = 15
dig_result = /obj/item/stack/ore/glass/basalt
+ smoothing_groups = SMOOTH_GROUP_FLOOR_BASALT
/turf/open/misc/asteroid/basalt/getDug()
. = ..()
@@ -186,9 +187,34 @@ GLOBAL_LIST_EMPTY(dug_up_basalt)
/turf/open/misc/asteroid/basalt/lava_land_surface/no_ruins
turf_flags = NO_RUINS
-/// A turf that can't we can't build openspace chasms on or spawn ruins in.
-/turf/closed/mineral/volcanic/lava_land_surface/do_not_chasm
- turf_flags = NO_RUINS
+/// Variant for ruins which replaces itself with the floor of a biome it generates in
+/turf/open/misc/asteroid/basalt/lava_land_surface/biome_replace
+ icon = 'icons/turf/mining.dmi'
+ icon_state = "basalt_biome"
+ smoothing_flags = NONE
+ /// Area type to whose generator we try to default to
+ var/default_area = /area/lavaland/surface/outdoors/unexplored/danger
+
+/turf/open/misc/asteroid/basalt/lava_land_surface/biome_replace/Initialize(mapload)
+ . = ..()
+ // Just spawn a normal lavaland rock if we fail to get a mapgen, such as being spawned over lava
+ var/supposed_type = /turf/open/misc/asteroid/basalt/lava_land_surface
+ var/area/cur_area = loc
+ var/datum/map_generator/cave_generator/map_generator = cur_area?.get_generator()
+ if (!map_generator)
+ cur_area = GLOB.areas_by_type[default_area]
+ map_generator = cur_area?.get_generator()
+
+ if (istype(map_generator))
+ var/biome = map_generator.get_biome_for_turf(src)
+ if (biome)
+ var/datum/biome/generating_biome = SSmapping.biomes[biome]
+ supposed_type = generating_biome.open_turf_type
+
+ var/cur_flags = turf_flags
+ var/turf/new_turf = ChangeTurf(supposed_type, flags = CHANGETURF_FORCEOP | CHANGETURF_INHERIT_AIR)
+ if(cur_flags & NO_RUINS)
+ new_turf.turf_flags |= NO_RUINS
/turf/open/misc/asteroid/lowpressure
initial_gas_mix = OPENTURF_LOW_PRESSURE
@@ -201,6 +227,77 @@ GLOBAL_LIST_EMPTY(dug_up_basalt)
turf_type = /turf/open/misc/asteroid/airless
worm_chance = 0
+/turf/open/misc/asteroid/basalt/smooth
+ smoothing_flags = SMOOTH_BITMASK
+ layer = MID_TURF_LAYER
+ floor_variance = 0
+ transform = MAP_SWITCH(TRANSLATE_MATRIX(-8, -8), matrix())
+ /// DMI used by unsmoothed turfs for variance
+ var/variant_dmi = null
+ /// Amount of variants this turf has
+ var/variant_num = 8
+
+/turf/open/misc/asteroid/basalt/smooth/set_smoothed_icon_state(new_junction)
+ . = ..()
+ if (new_junction == 255 && variant_dmi)
+ icon = variant_dmi
+ icon_state = "[base_icon_state][rand(1, variant_num)]"
+ else
+ icon = initial(icon)
+
+/turf/open/misc/asteroid/basalt/smooth/update_overlays()
+ . = ..()
+ if (smoothing_junction != 255 && variant_dmi)
+ . = list(mutable_appearance(variant_dmi, "[base_icon_state][rand(1, variant_num)]")) + .
+
+/turf/open/misc/asteroid/basalt/smooth/get_smooth_underlay_icon(mutable_appearance/underlay_appearance, turf/asking_turf, adjacency_dir)
+ . = ..()
+ if (!.)
+ return
+ if(!smoothing_flags)
+ return
+ underlay_appearance.transform = transform
+
+/turf/open/misc/asteroid/basalt/smooth/siderite
+ name = "siderite floor"
+ baseturfs = /turf/open/misc/asteroid/basalt/smooth/siderite
+ icon = 'icons/turf/floors/siderite.dmi'
+ damaged_dmi = 'icons/turf/floors/siderite_variants.dmi'
+ variant_dmi = 'icons/turf/floors/siderite_variants.dmi'
+ icon_state = "siderite-255"
+ base_icon_state = "siderite"
+ layer = HIGH_TURF_LAYER
+ smoothing_groups = SMOOTH_GROUP_TURF_OPEN + SMOOTH_GROUP_FLOOR_SIDERITE
+ canSmoothWith = SMOOTH_GROUP_FLOOR_LAVA + SMOOTH_GROUP_FLOOR_WATER_LAVALAND + SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_CLOSED_TURFS
+ dig_result = /obj/item/stack/ore/glass/siderite
+
+/turf/open/misc/asteroid/basalt/smooth/siderite/lava_land_surface
+ initial_gas_mix = LAVALAND_DEFAULT_ATMOS
+ planetary_atmos = TRUE
+ baseturfs = /turf/open/lava/smooth/lava_land_surface
+
+/turf/open/misc/asteroid/basalt/smooth/siderite/lava_land_surface/no_ruins
+ turf_flags = NO_RUINS
+
+/turf/open/misc/asteroid/basalt/smooth/shale
+ name = "shale floor"
+ baseturfs = /turf/open/misc/asteroid/basalt/smooth/shale
+ icon = 'icons/turf/floors/shale.dmi'
+ damaged_dmi = 'icons/turf/floors/shale_variants.dmi'
+ variant_dmi = 'icons/turf/floors/shale_variants.dmi'
+ icon_state = "shale-255"
+ base_icon_state = "shale"
+ smoothing_groups = SMOOTH_GROUP_TURF_OPEN + SMOOTH_GROUP_FLOOR_SHALE
+ canSmoothWith = SMOOTH_GROUP_FLOOR_LAVA + SMOOTH_GROUP_FLOOR_WATER_LAVALAND + SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_CLOSED_TURFS
+
+/turf/open/misc/asteroid/basalt/smooth/shale/lava_land_surface
+ initial_gas_mix = LAVALAND_DEFAULT_ATMOS
+ planetary_atmos = TRUE
+ baseturfs = /turf/open/lava/smooth/lava_land_surface
+
+/turf/open/misc/asteroid/basalt/smooth/shale/lava_land_surface/no_ruins
+ turf_flags = NO_RUINS
+
/turf/open/misc/asteroid/snow
gender = PLURAL
name = "snow"
diff --git a/code/game/turfs/open/lava.dm b/code/game/turfs/open/lava.dm
index a931748a577c..63a4e1e5045d 100644
--- a/code/game/turfs/open/lava.dm
+++ b/code/game/turfs/open/lava.dm
@@ -147,11 +147,6 @@
return result
-/turf/open/lava/smooth_icon()
- . = ..()
- mask_state = icon_state
- update_appearance(~UPDATE_SMOOTHING)
-
/turf/open/lava/ex_act(severity, target)
if(fish_source)
GLOB.preset_fish_sources[fish_source].spawn_reward_from_explosion(src, severity)
@@ -375,10 +370,59 @@
base_icon_state = "lava"
smoothing_flags = SMOOTH_BITMASK | SMOOTH_BORDER
smoothing_groups = SMOOTH_GROUP_TURF_OPEN + SMOOTH_GROUP_FLOOR_LAVA
- canSmoothWith = SMOOTH_GROUP_FLOOR_LAVA
- underfloor_accessibility = 2 //This avoids strangeness when routing pipes / wires along catwalks over lava
+ canSmoothWith = SMOOTH_GROUP_FLOOR_LAVA + SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_FLOOR_BASALT + SMOOTH_GROUP_MINERAL_WALLS + SMOOTH_GROUP_RED_ROCK_WALLS + SMOOTH_GROUP_SHALE_WALLS
+ underfloor_accessibility = 2 // This avoids strangeness when routing pipes / wires along catwalks over lava
+ /// *Inverse* smoothing bitflag for basalt overlays
+ var/basalt_junction = NONE
+ /// *Inverse* smoothing bitflag for siderite overlays
+ var/siderite_junction = NONE
+ /// *Inverse* smoothing bitflag for shale overlays
+ var/shale_junction = NONE
+
+/turf/open/lava/smooth/bitmask_smooth()
+ . = ..()
+ basalt_junction = ALL_SMOOTHING_JUNCTIONS
+ siderite_junction = ALL_SMOOTHING_JUNCTIONS
+ shale_junction = ALL_SMOOTHING_JUNCTIONS
+ // We need to convert basalt/siderite/shale groups into a readable format
+ var/static/basalt_group = null
+ var/static/siderite_group = null
+ var/static/shale_group = null
+ if (isnull(basalt_group))
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_BASALT + SMOOTH_GROUP_MINERAL_WALLS, basalt_group)
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_RED_ROCK_WALLS, siderite_group)
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_SHALE_WALLS, shale_group)
+ // After smoothing normally we can check our smoothed directions for possible basalt/siderite/shale tiles
+ for (var/check_dir in GLOB.alldirs)
+ var/junction = dir_to_junction(check_dir) | all_junctions_of_dir(check_dir)
+ if (!(junction & smoothing_junction))
+ continue
+ var/turf/to_smooth = get_step(src, check_dir)
+ if (!istype(to_smooth) || !to_smooth.smoothing_groups)
+ continue
+ for(var/key, group in to_smooth.smoothing_groups)
+ if (group & basalt_group[key])
+ basalt_junction &= ~junction
+ else if (group & siderite_group[key])
+ siderite_junction &= ~junction
+ else if (group & shale_group[key])
+ shale_junction &= ~junction
+
+/turf/open/lava/smooth/smooth_icon()
+ . = ..()
+ mask_state = "lava-[smoothing_junction & (basalt_junction & siderite_junction & shale_junction)]"
+ update_appearance(~UPDATE_SMOOTHING)
-/// Smooth lava needs to take after basalt in order to blend better. If you make a /turf/open/lava/smooth subtype for an area NOT surrounded by basalt; you should override this proc.
+/turf/open/lava/smooth/update_overlays()
+ . = ..()
+ if (basalt_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/basalt_outline.dmi', "basalt_outline-[basalt_junction]")
+ if (siderite_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/siderite_outline.dmi', "siderite_outline-[siderite_junction]")
+ if (shale_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/shale_outline.dmi', "shale_outline-[shale_junction]")
+
+/// Smooth lava needs to take after basalt in order to blend better.
/turf/open/lava/smooth/get_smooth_underlay_icon(mutable_appearance/underlay_appearance, turf/asking_turf, adjacency_dir)
underlay_appearance.icon = /turf/open/misc/asteroid/basalt::icon
underlay_appearance.icon_state = /turf/open/misc/asteroid/basalt::icon_state
diff --git a/code/game/turfs/open/water.dm b/code/game/turfs/open/water.dm
index ecf75011189c..21fe6b5b01b6 100644
--- a/code/game/turfs/open/water.dm
+++ b/code/game/turfs/open/water.dm
@@ -144,8 +144,56 @@
base_icon_state = "water_lavaland"
smoothing_flags = SMOOTH_BITMASK | SMOOTH_BORDER
smoothing_groups = SMOOTH_GROUP_TURF_OPEN + SMOOTH_GROUP_FLOOR_WATER_LAVALAND
- canSmoothWith = SMOOTH_GROUP_FLOOR_WATER_LAVALAND
+ canSmoothWith = SMOOTH_GROUP_FLOOR_WATER_LAVALAND + SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_FLOOR_BASALT + SMOOTH_GROUP_MINERAL_WALLS + SMOOTH_GROUP_RED_ROCK_WALLS + SMOOTH_GROUP_SHALE_WALLS
fishing_datum = /datum/fish_source/ocean
+ /// *Inverse* smoothing bitflag for basalt overlays
+ var/basalt_junction = NONE
+ /// *Inverse* smoothing bitflag for siderite overlays
+ var/siderite_junction = NONE
+ /// *Inverse* smoothing bitflag for shale overlays
+ var/shale_junction = NONE
+
+/turf/open/water/lavaland_atmos/basalt/bitmask_smooth()
+ . = ..()
+ basalt_junction = ALL_SMOOTHING_JUNCTIONS
+ siderite_junction = ALL_SMOOTHING_JUNCTIONS
+ shale_junction = ALL_SMOOTHING_JUNCTIONS
+ // We need to convert basalt/siderite/shale groups into a readable format
+ var/static/basalt_group = null
+ var/static/siderite_group = null
+ var/static/shale_group = null
+ if (isnull(basalt_group))
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_BASALT + SMOOTH_GROUP_MINERAL_WALLS, basalt_group)
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_RED_ROCK_WALLS, siderite_group)
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_SHALE_WALLS, shale_group)
+ // After smoothing normally we can check our smoothed directions for possible basalt/siderite/shale tiles
+ for (var/check_dir in GLOB.alldirs)
+ var/junction = dir_to_junction(check_dir) | all_junctions_of_dir(check_dir)
+ if (!(junction & smoothing_junction))
+ continue
+ var/turf/to_smooth = get_step(src, check_dir)
+ if (!istype(to_smooth) || !to_smooth.smoothing_groups)
+ continue
+ for(var/key, group in to_smooth.smoothing_groups)
+ if (group & basalt_group[key])
+ basalt_junction &= ~junction
+ else if (group & siderite_group[key])
+ siderite_junction &= ~junction
+ else if (group & shale_group[key])
+ shale_junction &= ~junction
+
+/turf/open/water/lavaland_atmos/basalt/smooth_icon()
+ . = ..()
+ update_appearance(~UPDATE_SMOOTHING)
+
+/turf/open/water/lavaland_atmos/basalt/update_overlays()
+ . = ..()
+ if (basalt_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/basalt_outline.dmi', "basalt_outline-[basalt_junction]")
+ if (siderite_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/siderite_outline.dmi', "siderite_outline-[siderite_junction]")
+ if (shale_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/shale_outline.dmi', "shale_outline-[shale_junction]")
/turf/open/water/lavaland_atmos/basalt/get_smooth_underlay_icon(mutable_appearance/underlay_appearance, turf/asking_turf, adjacency_dir)
underlay_appearance.icon = /turf/open/misc/asteroid/basalt::icon
diff --git a/code/game/world.dm b/code/game/world.dm
index cdb6006de052..78d6046b83d3 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -172,8 +172,6 @@ GLOBAL_VAR(restart_counter)
// Initialize RETA system - code/modules/reta/reta_system.dm
reta_init_config()
- LoadVerbs(/datum/verbs/menu)
-
if(fexists(RESTART_COUNTER_PATH))
GLOB.restart_counter = text2num(trim(file2text(RESTART_COUNTER_PATH)))
fdel(RESTART_COUNTER_PATH)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 161b39ca10d3..19853b998edf 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -870,6 +870,9 @@ ADMIN_VERB(give_ai_speech, R_FUN, "Give Random AI Speech", ADMIN_VERB_NO_DESCRIP
return
our_controller.planning_subtrees = list(GLOB.ai_subtrees[/datum/ai_planning_subtree/random_speech/blackboard]) + our_controller.planning_subtrees
+ADMIN_VERB(open_event_logger, R_DEBUG, "Open Event Logger", "Open the event logger interface.", ADMIN_CATEGORY_DEBUG)
+ GLOB.event_logger.ui_interact(user.mob)
+
ADMIN_VERB(new_blackmarket_item, R_BUILD, "Create Black Market Item", "Add an item to the black market for purchase.", ADMIN_CATEGORY_EVENTS, object as text)
if(!object)
to_chat(user, span_boldwarning("Failed! Provide a full or partial typepath!"))
@@ -917,4 +920,5 @@ ADMIN_VERB(new_blackmarket_item, R_BUILD, "Create Black Market Item", "Add an it
BLACKBOX_LOG_ADMIN_VERB("Create Black Market Item")
+
#undef STEALTH_MODE_TRAIT
diff --git a/code/modules/admin/event_logger.dm b/code/modules/admin/event_logger.dm
new file mode 100644
index 000000000000..cd943db8d30d
--- /dev/null
+++ b/code/modules/admin/event_logger.dm
@@ -0,0 +1,551 @@
+/// Global event logger datum, accessible anywhere for debug logging.
+GLOBAL_DATUM_INIT(event_logger, /datum/event_logger, new())
+
+/// Enables event logging for a datum. If display_on is supplied then it will report the events as if theyre happening on that datum
+/datum/proc/enable_evlogging(datum/display_on = null)
+ if(display_on)
+ GLOB.event_logger.display_map[REF(src)] = REF(display_on)
+ if(datum_flags & DF_EVLOGGING)
+ return
+ datum_flags |= DF_EVLOGGING
+ SEND_SIGNAL(src, COMSIG_EVLOGGING_ENABLED)
+
+/// Disables event logging for a datum. If display_on was supplied during enable_evlogging, it will also remove the display mapping.
+/datum/proc/disable_evlogging()
+ GLOB.event_logger.display_map -= REF(src)
+ if(!(datum_flags & DF_EVLOGGING))
+ return
+ datum_flags &= ~DF_EVLOGGING
+ SEND_SIGNAL(src, COMSIG_EVLOGGING_DISABLED)
+
+
+///Datum that holds info for a single track of events in the logger. Each track represents a datum that is being tracked. This track is shown in the timeline and when selected shows information on the track.
+/datum/event_logger_track
+ /// Display name for this track row.
+ var/name
+ /// String ref key used to identify this track.
+ var/ref
+ /// List of event assoc lists: id, tick, category, kind, info, + kind-specific fields.
+ var/list/events = list()
+ /// Assoc list of string (Title) -> string (Info) displayed in the info panel when this track is selected.
+ var/list/info = list()
+
+/datum/event_logger_track/New(track_ref, track_name, list/info_data = list())
+ ref = track_ref
+ name = track_name
+ info = info_data
+
+/datum/event_logger_track/Destroy()
+ events = null
+ info = null
+ return ..()
+
+///Datum for the event logger. One of these is made, and everyone shares it. So be nice. This keeps tracks of everything related to the logger
+/datum/event_logger
+ /// Whether the logger is actively recording.
+ var/running = FALSE
+ /// world.time at which logging first started (null until first start).
+ var/time_start = null
+ /// Assoc list: category name -> enabled (TRUE/FALSE).
+ var/list/categories = list()
+ /// Assoc list: category name -> hex color, assigned when a category is first seen.
+ var/list/category_colors = list()
+
+ /// List of colors we use for categories. We loop around once we run out
+ var/list/category_palette = list(
+ "#4fc3f7", "#81c784", "#ffb74d", "#e57373", "#ba68c8",
+ "#4dd0e1", "#fff176", "#f06292", "#a1887f", "#90a4ae",
+ )
+ /// Assoc list: ref string -> /datum/event_logger_track
+ var/list/tracks = list()
+ /// Assoc list: string event id -> event assoc list, for quick look-up when selecting events
+ var/list/events_by_id = list()
+ /// Incrementing id assigned to each logged event.
+ var/next_event_id = 0
+ /// Ref string of the currently selected track (for the info panel).
+ var/selected_ref = null
+ /// Assoc list: mob -> list of /image, for overlay cleanup.
+ var/list/user_overlays = list()
+ /// The mob currently waiting to click a target for pick-target mode, or null.
+ var/mob/awaiting_pick_user = null
+ /// Assoc list: REF(source datum) -> REF(display datum). Events from the source appear under the display datum's track.
+ var/list/display_map = list()
+
+/datum/event_logger/Destroy()
+ _clear_all_overlays()
+ QDEL_LIST_ASSOC_VAL(tracks)
+ tracks = null
+ categories = null
+ user_overlays = null
+ display_map = null
+ return ..()
+
+/datum/event_logger/proc/start()
+ if(running)
+ return
+ running = TRUE
+ if(isnull(time_start))
+ time_start = world.time
+
+/datum/event_logger/proc/stop()
+ running = FALSE
+
+/// Enter pick-target mode: the next atom the user clicks gets DF_EVLOGGING set.
+/datum/event_logger/proc/toggle_pick_target(mob/user)
+ if(awaiting_pick_user)
+ _end_pick_target()
+ else
+ awaiting_pick_user = user
+ RegisterSignal(user, COMSIG_MOB_CLICKON, PROC_REF(on_pick_target_click))
+
+/// Signal handler: fired when the user clicks something while in pick-target mode.
+/datum/event_logger/proc/on_pick_target_click(mob/source, atom/clicked, list/modifiers)
+ SIGNAL_HANDLER
+ clicked.enable_evlogging()
+ _end_pick_target()
+ return NONE
+
+/// Ends pick-target mode, unregistering the click signal and clearing the awaiting_pick_user var.
+/datum/event_logger/proc/_end_pick_target()
+ if(isnull(awaiting_pick_user))
+ return
+ UnregisterSignal(awaiting_pick_user, COMSIG_MOB_CLICKON)
+ awaiting_pick_user = null
+
+/// Ensures a track exists for ref_string. Safe to call multiple times.
+/datum/event_logger/proc/add_track(ref_string, track_name, list/info_data)
+ if(tracks[ref_string])
+ return
+ tracks[ref_string] = new /datum/event_logger_track(ref_string, track_name, info_data)
+
+/// Internal: Sets the event up, making a track if necesary. Also turns the instance into a REF() at this point
+/datum/event_logger/proc/_add_event(datum/source, list/event_data)
+ if(!running)
+ return
+ // Resolve display routing: if source has a display_on target, log under that datum's track instead
+ var/datum/display_target
+ var/display_ref = display_map[REF(source)]
+ if(display_ref)
+ display_target = locate(display_ref)
+ if(!display_target) // display_on was deleted, remove the stale entry. Yeah this is a bit lame, but I felt like registering QDEL might be more of a hassle if we're using REF() anyway. Feel free to @ me over it
+ display_map -= REF(source)
+
+ var/datum/track_datum = display_target || source
+ var/ref_string = REF(track_datum)
+ if(!tracks[ref_string])
+ var/track_name
+ if(isatom(track_datum))
+ var/atom/resolved_atom = track_datum
+ track_name = "[ref_string] [resolved_atom] [track_datum.type]"
+ else
+ track_name = "[ref_string] [track_datum.type]"
+ add_track(ref_string, track_name, null)
+ var/datum/event_logger_track/track = tracks[ref_string]
+ event_data["id"] = ++next_event_id
+ event_data["tick"] = world.time
+ var/category = event_data["category"]
+ if(!isnull(category) && isnull(categories[category]))
+ categories[category] = TRUE
+
+ var/selected_index = (length(category_colors) + 1) % (length(category_palette) + 1)
+
+ category_colors[category] = category_palette[selected_index]
+ track.events += list(event_data)
+ events_by_id["[event_data["id"]]"] = event_data
+ SEND_SIGNAL(source, COMSIG_EVLOG_EVENT_ADDED, track, event_data)
+ if(display_target)
+ SEND_SIGNAL(display_target, COMSIG_EVLOG_EVENT_ADDED, track, event_data)
+
+/// Log a plain text event. Has no world-visuals, just puts text into the menu
+/datum/event_logger/proc/log_event_text(datum/source, category, info_string)
+ _add_event(source, list(
+ "log_type" = EVLOG_TYPE_TEXT,
+ "category" = category,
+ "info" = info_string,
+ ))
+
+/// Log a location event (highlights a single tile). I should remove this one as turfs does the same thing essentially
+/datum/event_logger/proc/log_event_location(datum/source, category, info_string, turf/T)
+ _add_event(source, list(
+ "log_type" = EVLOG_TYPE_LOCATION,
+ "category" = category,
+ "info" = info_string,
+ "x" = T.x,
+ "y" = T.y,
+ "z" = T.z,
+ ))
+
+/// Log a turfs event (highlights a set of tiles).
+/datum/event_logger/proc/log_event_turfs(datum/source, category, info_string, list/turfs)
+ var/list/coords = list()
+ for(var/turf/T as anything in turfs)
+ coords += list(list("x" = T.x, "y" = T.y, "z" = T.z))
+ _add_event(source, list(
+ "log_type" = EVLOG_TYPE_TURFS,
+ "category" = category,
+ "info" = info_string,
+ "coords" = coords,
+ ))
+
+/// Log a line event (draws a line between 2 turfs).
+/datum/event_logger/proc/log_event_lines(datum/source, category, info_string, turf/A, turf/B)
+ _add_event(source, list(
+ "log_type" = EVLOG_TYPE_LINES,
+ "category" = category,
+ "info" = info_string,
+ "x1" = A.x,
+ "y1" = A.y,
+ "z1" = A.z,
+ "x2" = B.x,
+ "y2" = B.y,
+ "z2" = B.z,
+ ))
+
+/// Log a path event (renders directional arrows + start/end markers for a list of turfs in order from start to finish).
+/datum/event_logger/proc/log_event_path(datum/source, category, info_string, list/turfs)
+ var/list/coords = list()
+ for(var/turf/T as anything in turfs)
+ coords += list(list("x" = T.x, "y" = T.y, "z" = T.z))
+ _add_event(source, list(
+ "log_type" = EVLOG_TYPE_PATH,
+ "category" = category,
+ "info" = info_string,
+ "coords" = coords,
+ ))
+
+/// Log a maptext event (renders a floating text label at the turf when selected). text_string is the string to display at the turf.
+/datum/event_logger/proc/log_event_maptext(datum/source, category, info_string, turf/T, text_string)
+ _add_event(source, list(
+ "log_type" = EVLOG_TYPE_MAPTEXT,
+ "category" = category,
+ "info" = info_string,
+ "x" = T.x,
+ "y" = T.y,
+ "z" = T.z,
+ "text" = text_string,
+ ))
+
+/// Remove all overlays for a specific user.
+/datum/event_logger/proc/_clear_user_overlays(mob/user)
+ if(!user_overlays[user])
+ return
+ if(user.client)
+ user.client.images -= user_overlays[user]
+ user_overlays -= user
+
+/// Remove all overlays for all users.
+/datum/event_logger/proc/_clear_all_overlays()
+ for(var/mob/user as anything in user_overlays)
+ _clear_user_overlays(user)
+
+/// Push world-space highlight overlays to the user's client for the given events.
+/datum/event_logger/proc/_apply_event_overlays(mob/user, list/events_to_show)
+ _clear_user_overlays(user)
+ if(!user.client || !length(events_to_show))
+ return
+
+ var/list/images = list()
+
+ for(var/list/evt as anything in events_to_show)
+ var/color = get_category_color(evt["category"])
+ var/log_type = evt["log_type"]
+
+ if(log_type == EVLOG_TYPE_LOCATION)
+ var/turf/found_turf = locate(evt["x"], evt["y"], evt["z"])
+ if(found_turf)
+ images += _make_tile_image(found_turf, "box_overlay", color, 0.3)
+
+ else if(log_type == EVLOG_TYPE_TURFS)
+ var/list/coords = evt["coords"]
+ for(var/list/coord as anything in coords)
+ var/turf/found_turf = locate(coord["x"], coord["y"], coord["z"])
+ if(found_turf)
+ images += _make_tile_image(found_turf, "box_overlay", color, 0.3)
+
+ else if(log_type == EVLOG_TYPE_LINES)
+ var/turf/turf_A = locate(evt["x1"], evt["y1"], evt["z1"])
+ var/turf/turf_B = locate(evt["x2"], evt["y2"], evt["z2"])
+ if(turf_A && turf_B && turf_A != turf_B)
+ images += _make_line_images(turf_A, turf_B, color)
+
+ //Use SSPathfinder to render the full path
+ else if(log_type == EVLOG_TYPE_PATH)
+ var/list/path_turfs = list()
+ var/list/coords = evt["coords"]
+ for(var/list/coord as anything in coords)
+ var/turf/found_turf = locate(coord["x"], coord["y"], coord["z"])
+ if(found_turf)
+ path_turfs += found_turf
+ images += SSpathfinder.render_path_images_full(path_turfs)
+
+ //Draw maptext on a turf which lets us show important events in-world
+ else if(log_type == EVLOG_TYPE_MAPTEXT)
+ var/turf/found_turf = locate(evt["x"], evt["y"], evt["z"])
+ if(found_turf)
+ var/image/img = image('icons/turf/debug.dmi', found_turf, "circle", PATH_DEBUG_LAYER)
+ SET_PLANE_EXPLICIT(img, BALLOON_CHAT_PLANE, found_turf)
+ img.color = color
+ img.maptext = MAPTEXT(evt["text"])
+ img.maptext_width = 200
+ img.maptext_height = 64
+ img.maptext_x = 0
+ img.maptext_y = 32
+ images += img
+
+ if(length(images))
+ user_overlays[user] = images
+ user.client.images += images
+
+///Draw a line of images similar to beams but client-side. I couldn't find anything like this yet so here we are. Maybe making this a global is a good idea.
+/datum/event_logger/proc/_make_line_images(turf/turf_A, turf/turf_B, color)
+ var/list/images = list()
+ var/beam_icon = 'icons/turf/debug.dmi'
+ var/beam_icon_state = "beam"
+
+ var/origin_px = turf_A.pixel_x + turf_A.pixel_w
+ var/origin_py = turf_A.pixel_y + turf_A.pixel_z
+ var/target_px = turf_B.pixel_x + turf_B.pixel_w
+ var/target_py = turf_B.pixel_y + turf_B.pixel_z
+
+ var/Angle = get_angle_raw(turf_A.x, turf_A.y, origin_px, origin_py, turf_B.x, turf_B.y, target_px, target_py)
+ var/matrix/rot_matrix = matrix()
+ rot_matrix.Turn(Angle)
+
+ var/DX = (ICON_SIZE_X * turf_B.x + target_px) - (ICON_SIZE_X * turf_A.x + origin_px)
+ var/DY = (ICON_SIZE_Y * turf_B.y + target_py) - (ICON_SIZE_Y * turf_A.y + origin_py)
+ var/line_length = round(sqrt(DX ** 2 + DY ** 2))
+
+ for(var/N in 0 to line_length - 1 step 32)
+ var/Pixel_x
+ var/Pixel_y
+ if(DX == 0)
+ Pixel_x = 0
+ else
+ Pixel_x = round(sin(Angle) + ICON_SIZE_X * sin(Angle) * (N + 16) / 32)
+ if(DY == 0)
+ Pixel_y = 0
+ else
+ Pixel_y = round(cos(Angle) + ICON_SIZE_Y * cos(Angle) * (N + 16) / 32)
+
+ var/final_x = turf_A.x
+ var/final_y = turf_A.y
+ if(abs(Pixel_x) > ICON_SIZE_X)
+ final_x += Pixel_x > 0 ? round(Pixel_x / ICON_SIZE_X) : ceil(Pixel_x / ICON_SIZE_X)
+ Pixel_x %= ICON_SIZE_X
+ if(abs(Pixel_y) > ICON_SIZE_Y)
+ final_y += Pixel_y > 0 ? round(Pixel_y / ICON_SIZE_Y) : ceil(Pixel_y / ICON_SIZE_Y)
+ Pixel_y %= ICON_SIZE_Y
+
+ var/turf/seg_turf = locate(final_x, final_y, turf_A.z)
+ if(!seg_turf)
+ continue
+
+ var/image/img = image(beam_icon, seg_turf, beam_icon_state, PATH_DEBUG_LAYER)
+ SET_PLANE_EXPLICIT(img, BALLOON_CHAT_PLANE, seg_turf)
+ if(N + 32 > line_length)
+ // Terminal segment: crop the icon to avoid overshooting the target
+ var/icon/clipped = new(beam_icon, beam_icon_state)
+ clipped.DrawBox(null, 1, (line_length - N), 32, 32)
+ img.icon = clipped
+ img.color = color
+ img.transform = rot_matrix
+ img.pixel_x = origin_px + Pixel_x
+ img.pixel_y = origin_py + Pixel_y
+ images += img
+
+ return images
+
+
+/// Creates a single coloured tile overlay image.
+/datum/event_logger/proc/_make_tile_image(turf/selected_turf, icon_state, color, alpha_fraction)
+ var/image/img = image('icons/turf/debug.dmi', selected_turf, icon_state, PATH_DEBUG_LAYER)
+ SET_PLANE_EXPLICIT(img, BALLOON_CHAT_PLANE, selected_turf)
+ img.color = color
+ img.alpha = round(alpha_fraction * 255)
+ return img
+
+/// Returns the hex color string for a category, or white if unknown.
+/datum/event_logger/proc/get_category_color(category)
+ return category_colors[category] || "#ffffff"
+
+/// Clears all tracks, categories and overlays.
+/datum/event_logger/proc/clear()
+ _clear_all_overlays()
+ QDEL_LIST_ASSOC_VAL(tracks)
+ tracks = list()
+ events_by_id = list()
+ categories = list()
+ category_colors = list()
+ next_event_id = 0
+ selected_ref = null
+ time_start = null
+ running = FALSE
+
+///Tgui stuff below!!
+
+
+/datum/event_logger/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/simple/chat_dark),
+ )
+
+/datum/event_logger/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "EventLogger", "Event Logger")
+ ui.open()
+
+/datum/event_logger/ui_close(mob/user)
+ . = ..()
+ _clear_user_overlays(user)
+ if(awaiting_pick_user == user)
+ _end_pick_target()
+
+/datum/event_logger/ui_state(mob/user)
+ return ADMIN_STATE(R_DEBUG)
+
+/datum/event_logger/ui_data(mob/user)
+ var/list/data = list()
+
+ data["running"] = running
+ data["time_start"] = time_start
+ data["time_current"] = world.time
+ data["selected_ref"] = selected_ref
+ data["awaiting_pick"] = !isnull(awaiting_pick_user)
+
+ // Categories
+ var/list/cats = list() //meow
+ for(var/cat, enabled in categories)
+ cats += list(list("name" = cat, "enabled" = enabled))
+ data["categories"] = cats
+
+ // Tracks
+ var/list/track_list = list()
+ for(var/ref, track_val in tracks)
+ var/datum/event_logger_track/track = track_val
+
+ // Converts track info to a list for tgui
+ var/list/info_pairs = list()
+ for(var/title, entry in track.info)
+ info_pairs += list(list("title" = title, "entry" = entry))
+
+ track_list += list(list(
+ "name" = track.name,
+ "ref" = track.ref,
+ "events" = track.events,
+ "info" = info_pairs,
+ ))
+ data["tracks"] = track_list
+
+ return data
+
+/datum/event_logger/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ var/mob/user = ui.user
+
+ switch(action)
+ if("toggle_running") //Start or stop the logger
+ if(running)
+ stop()
+ else
+ start()
+ return TRUE
+
+ if("toggle_category") //Enable categories
+ var/cat = params["name"]
+ if(!isnull(categories[cat]))
+ categories[cat] = !categories[cat]
+ return TRUE
+
+ if("select_track") //Select a track on the timeline
+ selected_ref = params["ref"]
+ return TRUE
+
+ if("select_events") ///We selected events on the timeline, clear old overlays, add new ones.
+ var/list/ids = params["ids"]
+ if(!islist(ids) || !length(ids))
+ _clear_user_overlays(user)
+ return TRUE
+
+ var/list/matched = list()
+ for(var/id in ids)
+ var/list/evt = events_by_id["[id]"]
+ if(evt)
+ matched += list(evt)
+
+ _apply_event_overlays(user, matched)
+ return TRUE
+
+ if("clear")
+ clear()
+ return TRUE
+
+ if("start_pick_target")
+ toggle_pick_target(user)
+ return TRUE
+
+ if("disable_evlogging")
+ var/track_ref = params["ref"]
+ var/datum/target = locate(track_ref)
+ if(!target)
+ return FALSE
+ if(tgui_alert(user, "Stop tracking [target]?", "Confirm", list("Stop Tracking", "Cancel")) != "Stop Tracking")
+ return FALSE
+ target.disable_evlogging()
+ return TRUE
+
+ if("remove_track") // Removes a track and all its events from the logger entirely, and stops tracking the datum
+ var/track_ref = params["ref"]
+ var/datum/event_logger_track/track = tracks[track_ref]
+ if(!track)
+ return FALSE
+ // Remove all events from events_by_id
+ for(var/list/evt as anything in track.events)
+ events_by_id -= "[evt["id"]]"
+ // Disable evlogging on the datum if it still exists
+ var/datum/target = locate(track_ref)
+ if(target)
+ target.disable_evlogging()
+ // Clear overlays and deselect if this was the selected track
+ _clear_user_overlays(user)
+ if(selected_ref == track_ref)
+ selected_ref = null
+ qdel(track)
+ tracks -= track_ref
+ return TRUE
+
+ if("teleport_to_event")
+ var/target_id = params["id"]
+ var/list/found_evt = events_by_id["[target_id]"]
+ if(!found_evt)
+ return FALSE
+ var/dest_x
+ var/dest_y
+ var/dest_z
+ switch(found_evt["log_type"])
+ if(EVLOG_TYPE_LOCATION, EVLOG_TYPE_MAPTEXT)
+ dest_x = found_evt["x"]
+ dest_y = found_evt["y"]
+ dest_z = found_evt["z"]
+ if(EVLOG_TYPE_TURFS, EVLOG_TYPE_PATH)
+ var/list/coords = found_evt["coords"]
+ if(!length(coords))
+ return FALSE
+ var/list/first = coords[1]
+ dest_x = first["x"]
+ dest_y = first["y"]
+ dest_z = first["z"]
+ if(EVLOG_TYPE_LINES)
+ dest_x = found_evt["x1"]
+ dest_y = found_evt["y1"]
+ dest_z = found_evt["z1"]
+ else
+ return FALSE
+ var/turf/dest = locate(dest_x, dest_y, dest_z)
+ if(!dest)
+ return FALSE
+ user.forceMove(dest)
+ return TRUE
diff --git a/code/modules/admin/greyscale_modify_menu.dm b/code/modules/admin/greyscale_modify_menu.dm
index 7d3710ffb08f..a64f490cbbc8 100644
--- a/code/modules/admin/greyscale_modify_menu.dm
+++ b/code/modules/admin/greyscale_modify_menu.dm
@@ -346,7 +346,7 @@ This is highly likely to cause massive amounts of lag as every object in the gam
return
while(initial(current.greyscale_config) == initial(parent.greyscale_config))
current = parent
- parent = type2parent(current)
+ parent = current::parent_type
config_owner_type = current
/// Used for spray painting items in the gags_recolorable component
diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm
index 9db5efee0dbd..3b78e5527401 100644
--- a/code/modules/admin/holder2.dm
+++ b/code/modules/admin/holder2.dm
@@ -122,6 +122,7 @@ GLOBAL_PROTECT(href_token)
add_verb(client, /client/proc/readmin)
client.disable_combo_hud()
client.update_special_keybinds()
+ client.set_stat_panel()
/datum/admins/proc/associate(client/client)
if(IsAdminAdvancedProcCall())
@@ -162,6 +163,7 @@ GLOBAL_PROTECT(href_token)
owner.init_verbs() //re-initialize the verb list
owner.update_special_keybinds()
GLOB.admins |= client
+ client.set_stat_panel()
try_give_profiling()
diff --git a/code/modules/admin/smites/hitsplat.dm b/code/modules/admin/smites/hitsplat.dm
new file mode 100644
index 000000000000..dd4fcacb18a5
--- /dev/null
+++ b/code/modules/admin/smites/hitsplat.dm
@@ -0,0 +1,20 @@
+/datum/smite/hitsplat
+ name = "Hitsplat"
+
+/datum/smite/hitsplat/effect(client/user, mob/living/target)
+ . = ..()
+ target.AddComponent(/datum/component/hitsplat, /obj/effect/overlay/vis/hitsplat/lore_accurate)
+
+
+// Dragon dagger spec spam.
+/datum/smite/hitsplat/stackout
+ name = "Stackout (Hitsplat)"
+
+/datum/smite/hitsplat/stackout/effect(client/user, mob/living/target)
+ . = ..()
+ for(var/attack in 1 to 4)
+ playsound(target, SFX_SWING_HIT, 50, TRUE)
+ target.apply_damage(rand(0, 50))
+ target.apply_damage(rand(0, 50))
+ // dragon dagger has a 4 tick attack
+ sleep(2.4 SECONDS)
diff --git a/code/modules/admin/spawn_panel/spawn_handling.dm b/code/modules/admin/spawn_panel/spawn_handling.dm
index 265dd6c38dad..46ba9534f1ff 100644
--- a/code/modules/admin/spawn_panel/spawn_handling.dm
+++ b/code/modules/admin/spawn_panel/spawn_handling.dm
@@ -96,8 +96,8 @@
pod = new()
for(var/i in 1 to amount)
- if(istype(atom_to_spawn, /turf))
- var/turf/original_turf = target
+ if(ispath(atom_to_spawn, /turf))
+ var/turf/original_turf = get_turf(target) //Get_turf is required here, because if target is not a turf we are cooked chat.
var/turf/created_turf = original_turf.ChangeTurf(atom_to_spawn.type)
if(created_turf && atom_name)
created_turf.name = atom_name
diff --git a/code/modules/admin/spawn_panel/spawn_panel.dm b/code/modules/admin/spawn_panel/spawn_panel.dm
index a5cc42a43919..449fb8c0469a 100644
--- a/code/modules/admin/spawn_panel/spawn_panel.dm
+++ b/code/modules/admin/spawn_panel/spawn_panel.dm
@@ -131,7 +131,7 @@
var/list/spawn_params = list(
"selected_atom" = selected_atom,
"offset" = params["offset"],
- "atom_dir" = text2num(params["dir"]) || 1,
+ "atom_dir" = text2num(params["atom_dir"]) || 1,
"atom_amount" = text2num(params["atom_amount"]) || 1,
"atom_name" = params["atom_name"],
"where_target_type" = params["where_target_type"] || WHERE_FLOOR_BELOW_MOB,
@@ -196,9 +196,9 @@
admin_client.mouse_pointer_icon = admin_client.mouse_override_icon
admin_client.click_intercept = src
- winset(admin_client, "mapwindow.map", "right-click=true")
+ winset(admin_client, SKIN_MAPWINDOW_MAP, "right-click=true")
else
- winset(admin_client, "mapwindow.map", "right-click=false")
+ winset(admin_client, SKIN_MAPWINDOW_MAP, "right-click=false")
var/mob/holder_mob = admin_client.mob
holder_mob?.update_mouse_pointer()
diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm
index 1683025574fc..54fd02796a82 100644
--- a/code/modules/admin/sql_ban_system.dm
+++ b/code/modules/admin/sql_ban_system.dm
@@ -391,6 +391,7 @@
ROLE_MALF,
ROLE_NINJA,
ROLE_OPERATIVE,
+ ROLE_CLOWN_OPERATIVE,
ROLE_OVERTHROW,
ROLE_PARADOX_CLONE,
ROLE_REV,
diff --git a/code/modules/admin/verbs/adminweather.dm b/code/modules/admin/verbs/adminweather.dm
index 8721a1aabb25..290e52e5ee7c 100644
--- a/code/modules/admin/verbs/adminweather.dm
+++ b/code/modules/admin/verbs/adminweather.dm
@@ -2,7 +2,7 @@ ADMIN_VERB(run_weather, R_ADMIN|R_FUN, "Run Weather", "Triggers specific weather
var/list/weather_choices = list()
if(!length(weather_choices))
- for(var/datum/weather/weather_type as anything in subtypesof(/datum/weather))
+ for(var/datum/weather/weather_type as anything in valid_subtypesof(/datum/weather))
weather_choices[initial(weather_type.type)] = weather_type
var/datum/weather/weather_choice = tgui_input_list(user, "Choose a weather to run", "Weather", weather_choices)
diff --git a/code/modules/admin/verbs/path_debugger.dm b/code/modules/admin/verbs/path_debugger.dm
index bd6b66208281..98bed7a0f344 100644
--- a/code/modules/admin/verbs/path_debugger.dm
+++ b/code/modules/admin/verbs/path_debugger.dm
@@ -103,22 +103,10 @@ GLOBAL_DATUM_INIT(pathfind_dude, /obj/pathfind_guy, new())
return
/datum/action/innate/path_debug/proc/render_path(list/turf/draw_list)
- if(!length(draw_list))
- return list()
-
- var/list/image/turf_images = list()
- // Render everything but the first and last
- for(var/i in 1 to (length(draw_list) - 1))
- var/turf/problem_child = draw_list[i]
- var/turf/next = draw_list[i + 1]
- turf_images += render_turf(problem_child, get_dir(problem_child, next))
-
- return turf_images
+ return SSpathfinder.render_path_images(draw_list)
/datum/action/innate/path_debug/proc/render_turf(turf/draw, direction)
- var/image/arrow = image('icons/turf/debug.dmi', draw, "arrow", PATH_ARROW_DEBUG_LAYER, direction)
- SET_PLANE_EXPLICIT(arrow, BALLOON_CHAT_PLANE, draw)
- return arrow
+ return SSpathfinder.render_path_arrow(draw, direction)
/datum/action/innate/path_debug/jps
name = "JPS Test"
diff --git a/code/modules/admin/view_variables/debug_variables.dm b/code/modules/admin/view_variables/debug_variables.dm
index ded5890cba8c..bdd45073f193 100644
--- a/code/modules/admin/view_variables/debug_variables.dm
+++ b/code/modules/admin/view_variables/debug_variables.dm
@@ -103,20 +103,14 @@
return "/list ([list_value.len])"
// if it's a number, is it a bitflag?
- var/list/valid_bitflags
- if(!isnum(name))
- valid_bitflags = get_valid_bitflags(name)
-
- if(!length(valid_bitflags))
- return "[VV_HTML_ENCODE(value)]"
-
- var/list/flags = list()
- for (var/bit_name in valid_bitflags)
- if (value & valid_bitflags[bit_name])
- flags += bit_name
- if(length(flags))
- return "[VV_HTML_ENCODE(flags.Join(", "))]"
- return "NONE"
+ var/list/matching_bitflags = get_matching_bitflags(name, value)
+
+ if(!isnull(matching_bitflags))
+ if(length(matching_bitflags))
+ return "[VV_HTML_ENCODE(matching_bitflags.Join(", "))]"
+ return "NONE"
+
+ return "[VV_HTML_ENCODE(value)]"
/datum/proc/debug_variable_value(name, level, datum/owner, sanitize, display_flags)
if("[src]" != "[type]") // If we have a name var, let's use it.
diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm
index f708ec696e0a..ed7f8ffeec24 100644
--- a/code/modules/antagonists/_common/antag_spawner.dm
+++ b/code/modules/antagonists/_common/antag_spawner.dm
@@ -63,7 +63,7 @@
/obj/item/antag_spawner/contract/proc/poll_for_student(mob/living/carbon/human/teacher, apprentice_school)
balloon_alert(teacher, "contacting apprentice...")
polling = TRUE
- var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [span_danger("[teacher]'s")] [span_notice("[apprentice_school] apprentice")]?", check_jobban = ROLE_WIZARD, role = ROLE_WIZARD, poll_time = 15 SECONDS, checked_target = src, alert_pic = /obj/item/clothing/head/wizard/red, jump_target = src, role_name_text = "wizard apprentice", chat_text_border_icon = /obj/item/clothing/head/wizard/red)
+ var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to play as [span_danger("[teacher]'s")] [span_notice("[apprentice_school] apprentice")]?", check_jobban = ROLE_WIZARD_MIDROUND, role = ROLE_WIZARD_MIDROUND, poll_time = 15 SECONDS, checked_target = src, alert_pic = /obj/item/clothing/head/wizard/red, jump_target = src, role_name_text = "wizard apprentice", chat_text_border_icon = /obj/item/clothing/head/wizard/red)
polling = FALSE
if(isnull(chosen_one))
to_chat(teacher, span_warning("Unable to reach your apprentice! You can either attack the spellbook with the contract to refund your points, or wait and try again later."))
@@ -139,7 +139,7 @@
return
to_chat(user, span_notice("You activate [src] and wait for confirmation."))
- var/mob/chosen_one = SSpolling.poll_ghost_candidates("Do you want to play as a reinforcement [special_role_name]?", check_jobban = ROLE_OPERATIVE, role = ROLE_OPERATIVE, poll_time = 15 SECONDS, ignore_category = POLL_IGNORE_SYNDICATE, alert_pic = src, role_name_text = special_role_name, amount_to_pick = 1)
+ var/mob/chosen_one = SSpolling.poll_ghost_candidates("Do you want to play as a reinforcement [special_role_name]?", check_jobban = ROLE_OPERATIVE_MIDROUND, role = ROLE_OPERATIVE_MIDROUND, poll_time = 15 SECONDS, ignore_category = POLL_IGNORE_SYNDICATE, alert_pic = src, role_name_text = special_role_name, amount_to_pick = 1)
if(chosen_one)
if(QDELETED(src) || !check_usability(user))
return
@@ -185,8 +185,8 @@
/obj/item/antag_spawner/nuke_ops/overwatch/Initialize(mapload)
. = ..()
- if(length(GLOB.nukeop_overwatch_start)) //Otherwise, it will default to the datum's spawn point anyways
- spawn_location = pick(GLOB.nukeop_overwatch_start)
+ if(length(GLOB.nukeop_base_overwatch_start)) //Otherwise, it will default to the datum's spawn point anyways
+ spawn_location = pick(GLOB.nukeop_base_overwatch_start)
//////CLOWN OP
/obj/item/antag_spawner/nuke_ops/clown
@@ -272,7 +272,7 @@
return
if(used)
return
- var/mob/chosen_one = SSpolling.poll_ghosts_for_target(check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, poll_time = 5 SECONDS, checked_target = src, alert_pic = demon_type, jump_target = src, role_name_text = initial(demon_type.name))
+ var/mob/chosen_one = SSpolling.poll_ghosts_for_target(check_jobban = ROLE_SENTIENCE, role = ROLE_SENTIENCE, poll_time = 5 SECONDS, checked_target = src, alert_pic = demon_type, jump_target = src, role_name_text = initial(demon_type.name))
if(chosen_one)
if(used || QDELETED(src))
return
@@ -323,8 +323,6 @@
var/pod_style = /datum/pod_style/syndicate
/// Do we use a random subtype of the outfit?
var/use_subtypes = TRUE
- /// The antag role we check if the ghosts have enabled to get the poll.
- var/poll_role_check = ROLE_TRAITOR
/// The mind's special role.
var/role_to_play = ROLE_SYNDICATE_MONKEY
/// What category to ignore the poll
@@ -351,8 +349,7 @@
to_chat(user, span_notice("You activate [src] and wait for confirmation."))
var/mob/chosen_one = SSpolling.poll_ghost_candidates(
- check_jobban = poll_role_check,
- role = poll_role_check,
+ check_jobban = role_to_play,
poll_time = 10 SECONDS,
ignore_category = poll_ignore_category,
alert_pic = src,
@@ -409,7 +406,7 @@
outfit = /datum/outfit/contractor_partner
use_subtypes = FALSE
antag_datum = /datum/antagonist/traitor/contractor_support
- poll_ignore_category = ROLE_TRAITOR
+ poll_ignore_category = POLL_IGNORE_CONTRACTOR_SUPPORT
role_to_play = ROLE_CONTRACTOR_SUPPORT
/obj/item/antag_spawner/loadout/contractor/do_special_things(mob/living/carbon/human/contractor_support, mob/user)
@@ -426,9 +423,8 @@
outfit = /datum/outfit/syndicate_monkey
antag_datum = /datum/antagonist/syndicate_monkey
use_subtypes = FALSE
- poll_role_check = ROLE_TRAITOR
role_to_play = ROLE_SYNDICATE_MONKEY
- poll_ignore_category = POLL_IGNORE_SYNDICATE
+ poll_ignore_category = POLL_IGNORE_SYNDICATE_MONKEY
fail_text = "Unable to connect to the Animal Rights Consortium's Banana Ops. Please wait and try again later or use the beacon on your uplink to get your points refunded."
/obj/item/antag_spawner/loadout/monkey_man/do_special_things(mob/living/carbon/human/monkey_man, mob/user)
diff --git a/code/modules/antagonists/_common/antag_team.dm b/code/modules/antagonists/_common/antag_team.dm
index 527196c51c3e..178ec2d59a16 100644
--- a/code/modules/antagonists/_common/antag_team.dm
+++ b/code/modules/antagonists/_common/antag_team.dm
@@ -29,6 +29,9 @@ GLOBAL_LIST_EMPTY(antagonist_teams)
/datum/team/Destroy(force)
GLOB.antagonist_teams -= src
members = null
+ for(var/datum/objective/objective as anything in objectives)
+ if(objective.team == src)
+ objective.team = null
objectives = null
return ..()
diff --git a/code/modules/antagonists/blob/overmind.dm b/code/modules/antagonists/blob/overmind.dm
index dcfc2a184f43..a5a9ff6a694c 100644
--- a/code/modules/antagonists/blob/overmind.dm
+++ b/code/modules/antagonists/blob/overmind.dm
@@ -207,7 +207,6 @@ GLOBAL_LIST_EMPTY(blob_nodes)
blob_points = INFINITY
else
to_chat(src, span_blob("You've reached critical mass, but something feels terribly wrong, stopping you from expanding further. All you can do now is fight as long as you can..."))
- balloon_alert(src, "victory - expansion over critical mass disabled")
addtimer(CALLBACK(src, PROC_REF(victory)), 45 SECONDS)
/// Actually *do* the blob's victory: give them their greentext and, depending on the end_round_on_victory variable, decide if everyone dies or if it's just a jumpscare.
diff --git a/code/modules/antagonists/blob/structures/_blob.dm b/code/modules/antagonists/blob/structures/_blob.dm
index 819840fe458c..8bde78251ff5 100644
--- a/code/modules/antagonists/blob/structures/_blob.dm
+++ b/code/modules/antagonists/blob/structures/_blob.dm
@@ -164,6 +164,7 @@
return TRUE
// If it's not supposed to end the round and it's at the win count, don't make more. (400 tiles is still a lot to fight through...)
if(!controller.end_round_on_victory && (controller.blobs_legit.len >= controller.blobwincount))
+ balloon_alert(controller, "max tiles reached!")
return FALSE
// Otherwise, it's probably fine.
return TRUE
diff --git a/code/modules/antagonists/blood_worm/actions/cocoon.dm b/code/modules/antagonists/blood_worm/actions/cocoon.dm
index d84a6681912b..f976a4fe5598 100644
--- a/code/modules/antagonists/blood_worm/actions/cocoon.dm
+++ b/code/modules/antagonists/blood_worm/actions/cocoon.dm
@@ -407,3 +407,9 @@
/obj/structure/blood_worm_cocoon/adult/examine(mob/user)
return ..() + span_warning("It can be broken to prevent the blood worm from reproducing, but it looks extremely tough.")
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/hatchling/polymorph
+ new_worm_type = /mob/living/basic/blood_worm/juvenile/polymorph
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/juvenile/polymorph
+ new_worm_type = /mob/living/basic/blood_worm/adult/polymorph
diff --git a/code/modules/antagonists/blood_worm/blood_worm_host_handling.dm b/code/modules/antagonists/blood_worm/blood_worm_host_handling.dm
index 2b87fd4c9f61..ebfeed40a646 100644
--- a/code/modules/antagonists/blood_worm/blood_worm_host_handling.dm
+++ b/code/modules/antagonists/blood_worm/blood_worm_host_handling.dm
@@ -22,6 +22,7 @@
RegisterSignal(host, COMSIG_LIVING_ADJUST_OXY_DAMAGE, PROC_REF(on_host_adjust_oxy_damage))
RegisterSignal(host, COMSIG_LIVING_PRE_UPDATE_BLOOD_STATUS, PROC_REF(on_host_pre_update_blood_status))
RegisterSignal(host, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(on_host_get_status_tab_items))
+ client?.set_stat_panel()
RegisterSignal(host, COMSIG_MOB_EXAMINING, PROC_REF(on_host_examining))
START_PROCESSING(SSfastprocess, src)
@@ -118,6 +119,7 @@
COMSIG_MOB_EXAMINING,
COMSIG_MOB_HUD_CREATED,
))
+ client?.set_stat_panel()
STOP_PROCESSING(SSfastprocess, src)
diff --git a/code/modules/antagonists/blood_worm/blood_worm_mob.dm b/code/modules/antagonists/blood_worm/blood_worm_mob.dm
index 6fe2a41391f9..7ac058417869 100644
--- a/code/modules/antagonists/blood_worm/blood_worm_mob.dm
+++ b/code/modules/antagonists/blood_worm/blood_worm_mob.dm
@@ -398,3 +398,12 @@
transfuse_action = /datum/action/cooldown/mob_cooldown/blood_worm/inject/adult
regen_rate = 0.5 // 360 seconds to recover from 0 to 180, or exactly 6 minutes.
+
+/mob/living/basic/blood_worm/hatchling/polymorph
+ cocoon_action = /datum/action/cooldown/mob_cooldown/blood_worm/cocoon/hatchling/polymorph
+
+/mob/living/basic/blood_worm/juvenile/polymorph
+ cocoon_action = /datum/action/cooldown/mob_cooldown/blood_worm/cocoon/juvenile/polymorph
+
+/mob/living/basic/blood_worm/adult/polymorph
+ cocoon_action = null
diff --git a/code/modules/antagonists/changeling/cellular_emporium.dm b/code/modules/antagonists/changeling/cellular_emporium.dm
index 1a661c86caea..08e3bfe726bd 100644
--- a/code/modules/antagonists/changeling/cellular_emporium.dm
+++ b/code/modules/antagonists/changeling/cellular_emporium.dm
@@ -42,6 +42,7 @@
var/list/ability_data = list(
"name" = initial(ability_path.name),
"desc" = initial(ability_path.desc),
+ "icon" = initial(ability_path.button_icon_state),
"helptext" = initial(ability_path.helptext),
"path" = ability_path,
"genetic_point_required" = dna_cost,
diff --git a/code/modules/antagonists/changeling/powers/absorb.dm b/code/modules/antagonists/changeling/powers/absorb.dm
index 23dad3450c34..ec28fc2866f1 100644
--- a/code/modules/antagonists/changeling/powers/absorb.dm
+++ b/code/modules/antagonists/changeling/powers/absorb.dm
@@ -216,20 +216,16 @@
if(href_list["exit_hivemind"] && !QDELETED(src))
exit_hivemind()
-/mob/eye/imaginary_friend/hivemind/verb/exit_hivemind()
- set category = "IC"
- set name = "Exit Hivemind"
- set desc = "Leave the hivemind behind and enter the land of the dead."
-
+/mob/eye/imaginary_friend/hivemind/proc/exit_hivemind()
var/response = tgui_alert(src, "Are you sure you want to exit the hivemind? \
You can't re-enter it, though you can still be revived.", "Confirm Exit", list("Exit", "Stay"))
if(response != "Exit" || QDELETED(src))
return
ghostize(TRUE)
-/mob/eye/imaginary_friend/hivemind/ghostize(can_reenter_corpse, admin_ghost)
+/mob/eye/imaginary_friend/hivemind/ghostize(can_reenter_corpse, forced)
. = ..()
- if(admin_ghost)
+ if(forced)
return
alert_hivemind("You sense the presence of [real_name] disappear from the hivemind.")
diff --git a/code/modules/antagonists/changeling/powers/mutations.dm b/code/modules/antagonists/changeling/powers/mutations.dm
index 20c4b7e220de..3dc6e81a6ef0 100644
--- a/code/modules/antagonists/changeling/powers/mutations.dm
+++ b/code/modules/antagonists/changeling/powers/mutations.dm
@@ -478,7 +478,7 @@
return BULLET_ACT_HIT
/obj/projectile/tentacle/Destroy()
- qdel(chain)
+ QDEL_NULL(chain)
source = null
return ..()
diff --git a/code/modules/antagonists/changeling/powers/tiny_prick.dm b/code/modules/antagonists/changeling/powers/tiny_prick.dm
index 5767a87ca55a..06c360aba079 100644
--- a/code/modules/antagonists/changeling/powers/tiny_prick.dm
+++ b/code/modules/antagonists/changeling/powers/tiny_prick.dm
@@ -1,5 +1,5 @@
/datum/action/changeling/sting//parent path, not meant for users afaik
- name = "Tiny Prick" //cellularemporium uses `nameToIconState` to button icon state must match this, on top of matching the hud below.
+ name = "Tiny Prick"
desc = "Stabby stabby"
category = "stings"
button_icon_state = "sting_null" //This must be equal to the icon state for `/atom/movable/screen/ling/sting`
diff --git a/code/modules/antagonists/cult/cult_structure_forge.dm b/code/modules/antagonists/cult/cult_structure_forge.dm
index 3fe9a884369d..d3532d2c6363 100644
--- a/code/modules/antagonists/cult/cult_structure_forge.dm
+++ b/code/modules/antagonists/cult/cult_structure_forge.dm
@@ -1,6 +1,5 @@
/// Some defines for items the daemon forge can create.
#define NARSIE_ARMOR "Nar'Sien Hardened Armor"
-#define FLAGELLANT_ARMOR "Flagellant's Robe"
#define ELDRITCH_SWORD "Eldritch Longsword"
#define CURSED_BLADE "Cursed Ritual Blade"
@@ -8,7 +7,7 @@
/obj/structure/destructible/cult/item_dispenser/forge
name = "daemon forge"
desc = "A forge used in crafting the unholy weapons used by the armies of Nar'Sie."
- cult_examine_tip = "Can be used to create Nar'Sien hardened armor, flagellant's robes, and eldritch longswords."
+ cult_examine_tip = "Can be used to create Nar'Sien hardened armor and eldritch longswords."
icon_state = "forge"
light_range = 2
light_color = LIGHT_COLOR_LAVA
@@ -56,6 +55,5 @@
debris = list()
#undef NARSIE_ARMOR
-#undef FLAGELLANT_ARMOR
#undef ELDRITCH_SWORD
#undef CURSED_BLADE
diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm
index efddddaa4adb..56fbb918952c 100644
--- a/code/modules/antagonists/heretic/heretic_antag.dm
+++ b/code/modules/antagonists/heretic/heretic_antag.dm
@@ -417,10 +417,12 @@
COMSIG_LIVING_POST_FULLY_HEAL,
COMSIG_LIVING_CULT_SACRIFICED,
COMSIG_ATOM_EXAMINE,
+ COMSIG_ATOM_UPDATE_OVERLAYS,
SIGNAL_ADDTRAIT(TRAIT_HERETIC_AURA_HIDDEN),
- SIGNAL_REMOVETRAIT(TRAIT_HERETIC_AURA_HIDDEN)
+ SIGNAL_REMOVETRAIT(TRAIT_HERETIC_AURA_HIDDEN),
)
)
+ our_mob.update_appearance(UPDATE_OVERLAYS)
/// Removes the ability to blade break, removes cloak of shadows and removes the cap on how many blades you can craft
/datum/antagonist/heretic/proc/disable_blade_breaking()
diff --git a/code/modules/antagonists/heretic/influences.dm b/code/modules/antagonists/heretic/influences.dm
index ad7478fc6fd9..b74d120d86b9 100644
--- a/code/modules/antagonists/heretic/influences.dm
+++ b/code/modules/antagonists/heretic/influences.dm
@@ -147,8 +147,7 @@
else
human_user.gib(DROP_ALL_REMAINS)
human_user.investigate_log("has died from using telekinesis on a heretic influence.", INVESTIGATE_DEATHS)
- var/datum/effect_system/reagents_explosion/explosion = new(get_turf(human_user), 1, 1, 1)
- explosion.start(src)
+ dyn_explosion(get_turf(human_user), 1, flash_range = 1, flame_range = 1)
/obj/effect/visible_heretic_influence/examine(mob/living/user)
. = ..()
diff --git a/code/modules/antagonists/heretic/knowledge/_heretic_paths.dm b/code/modules/antagonists/heretic/knowledge/_heretic_paths.dm
index 6bdda2d115c6..ed4c7b97dd35 100644
--- a/code/modules/antagonists/heretic/knowledge/_heretic_paths.dm
+++ b/code/modules/antagonists/heretic/knowledge/_heretic_paths.dm
@@ -62,6 +62,8 @@ GLOBAL_LIST_INIT(heretic_path_datums, init_heretic_path_datums())
var/guaranteed_side_tier2
/// Knowledge guaranteed to show up in the third draft
var/guaranteed_side_tier3
+ /// Discount applied to shop knowledge costs for this path (subtracted from each tier cost, minimum 1)
+ var/shop_cost_discount = 0
/datum/heretic_knowledge_tree_column/proc/get_ui_data(datum/antagonist/heretic/our_heretic, category)
@@ -232,6 +234,10 @@ GLOBAL_LIST_INIT(heretic_path_datums, init_heretic_path_datums())
/// costs by index mapped to depth
var/list/shop_costs = list(1, 2, 2, 2, 3)
+ var/discount = heretic_path.shop_cost_discount
+ if(discount)
+ for(var/i in 1 to length(shop_costs))
+ shop_costs[i] = max(1, shop_costs[i] - discount)
// Relevant variables that we pull from the path
var/knowledge_tier1 = heretic_path.knowledge_tier1
diff --git a/code/modules/antagonists/heretic/knowledge/lock_lore.dm b/code/modules/antagonists/heretic/knowledge/lock_lore.dm
index 76f2dfd27763..f83bfbdb4b2f 100644
--- a/code/modules/antagonists/heretic/knowledge/lock_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/lock_lore.dm
@@ -3,6 +3,7 @@
ui_bgr = "node_lock"
complexity = "Medium"
complexity_color = COLOR_YELLOW
+ shop_cost_discount = 1
icon = list(
"icon" = 'icons/obj/weapons/khopesh.dmi',
"state" = "key_blade",
diff --git a/code/modules/antagonists/heretic/knowledge/moon_lore.dm b/code/modules/antagonists/heretic/knowledge/moon_lore.dm
index d8c093ac4059..5c611079f8cf 100644
--- a/code/modules/antagonists/heretic/knowledge/moon_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/moon_lore.dm
@@ -119,8 +119,9 @@
research_tree_icon_frame = 9
/datum/heretic_knowledge/armor/moon
- desc = "Allows you to transmute a table (or a suit), a mask and two sheets of glass to create a Resplendant Regalia, this robe will render the user fully immune to disabling effects and convert all forms of damage into brain damage, while also pacifying the user and render him unable to use ranged weapons (Moon blade will bypass pacifism). \
- Acts as a focus while hooded."
+ desc = "Allows you to transmute a table (or a suit), a mask and two sheets of glass to create a Resplendant Regalia. \
+ This robe will render the user fully immune to disabling effects and convert all forms of damage into brain damage, while also pacifying the user and rendering them unable to use ranged weapons. \
+ A Moonlight Amulet will be necessary to use blades while wearing it."
gain_text = "Trails of light and mirth flowed from every arm of this magnificent attire. \
The troupe twirled in irridescent cascades, dazzling onlookers with the truth they sought. \
I observed, basking in the light, to find my self."
diff --git a/code/modules/antagonists/heretic/moon_lunatic.dm b/code/modules/antagonists/heretic/moon_lunatic.dm
index d33ad281a83a..27aaef81f15a 100644
--- a/code/modules/antagonists/heretic/moon_lunatic.dm
+++ b/code/modules/antagonists/heretic/moon_lunatic.dm
@@ -15,9 +15,14 @@
var/mob/living/carbon/human/ascended_body
// Our objective
var/datum/objective/lunatic/lunatic_obj
+ // Actions granted by this datum, tracked for proper cleanup on body transfer
+ var/datum/action/cooldown/lunatic_track/moon_track
+ var/datum/action/cooldown/spell/touch/mansus_grasp/mad_touch
/datum/antagonist/lunatic/on_gain()
+ owner.current.log_message("has become a Lunatic!", LOG_ATTACK, color="red")
// Masters gain an objective before so we dont want duplicates
+ . = ..()
for(var/objective in objectives)
if(!istype(objective, /datum/objective/lunatic))
continue
@@ -25,6 +30,18 @@
var/datum/objective/lunatic/loony = new()
objectives += loony
lunatic_obj = loony
+ moon_track = new(owner)
+ mad_touch = new(owner)
+ mad_touch.Grant(owner.current)
+ moon_track.Grant(owner.current)
+
+/datum/antagonist/lunatic/on_removal()
+ QDEL_NULL(moon_track)
+ QDEL_NULL(mad_touch)
+ return ..()
+
+/datum/antagonist/lunatic/on_removal()
+ owner?.current?.log_message("lost their Lunatic status!", LOG_ATTACK, color="red")
return ..()
/// Runs when the moon heretic creates us, used to give the lunatic a master
@@ -44,15 +61,11 @@
add_team_hud(our_mob, /datum/antagonist/lunatic)
ADD_TRAIT(our_mob, TRAIT_MADNESS_IMMUNE, REF(src))
- var/datum/action/cooldown/lunatic_track/moon_track = new /datum/action/cooldown/lunatic_track()
- var/datum/action/cooldown/spell/touch/mansus_grasp/mad_touch = new /datum/action/cooldown/spell/touch/mansus_grasp()
- mad_touch.Grant(our_mob)
- moon_track.Grant(our_mob)
-
/datum/antagonist/lunatic/remove_innate_effects(mob/living/mob_override)
var/mob/living/our_mob = mob_override || owner.current
handle_clown_mutation(our_mob, removing = FALSE)
our_mob.remove_faction(FACTION_HERETIC)
+ REMOVE_TRAIT(our_mob, TRAIT_MADNESS_IMMUNE, REF(src))
// Mood event given to moon acolytes
/datum/mood_event/heretics/lunatic
@@ -87,3 +100,6 @@
/datum/antagonist/lunatic/master/apply_innate_effects(mob/living/mob_override)
var/mob/living/our_mob = mob_override || owner.current
add_team_hud(our_mob, /datum/antagonist/lunatic)
+
+/datum/antagonist/lunatic/master/remove_innate_effects(mob/living/mob_override)
+ return
diff --git a/code/modules/antagonists/nightmare/nightmare_organs.dm b/code/modules/antagonists/nightmare/nightmare_organs.dm
index 6ac1148a2224..03ecc0eeb30d 100644
--- a/code/modules/antagonists/nightmare/nightmare_organs.dm
+++ b/code/modules/antagonists/nightmare/nightmare_organs.dm
@@ -93,6 +93,10 @@
/// The armblade granted to the host of this heart.
var/obj/item/light_eater/blade
+/obj/item/organ/heart/nightmare/Destroy()
+ QDEL_NULL(blade)
+ return ..()
+
/obj/item/organ/heart/nightmare/attack(mob/M, mob/living/carbon/user, obj/target)
if(M != user)
return ..()
@@ -111,9 +115,11 @@
/obj/item/organ/heart/nightmare/on_mob_insert(mob/living/carbon/heart_owner, special, movement_flags)
. = ..()
- if(special != HEART_SPECIAL_SHADOWIFY)
- blade = new/obj/item/light_eater
- heart_owner.put_in_hands(blade)
+ if(special == HEART_SPECIAL_SHADOWIFY)
+ return
+ blade = new /obj/item/light_eater
+ heart_owner.put_in_hands(blade)
+ RegisterSignal(blade, COMSIG_QDELETING, PROC_REF(on_blade_deleted))
/obj/item/organ/heart/nightmare/on_mob_remove(mob/living/carbon/heart_owner, special, movement_flags)
. = ..()
@@ -125,6 +131,11 @@
/obj/item/organ/heart/nightmare/Stop()
return FALSE
+// Happens if the blade was deleted before we were during mob destruction
+/obj/item/organ/heart/nightmare/proc/on_blade_deleted(datum/source)
+ SIGNAL_HANDLER
+ blade = null
+
/obj/item/organ/heart/nightmare/on_death(seconds_per_tick)
if(!owner)
return
diff --git a/code/modules/antagonists/nukeop/datums/operative.dm b/code/modules/antagonists/nukeop/datums/operative.dm
index dccd7450f978..ae3d3c9d777f 100644
--- a/code/modules/antagonists/nukeop/datums/operative.dm
+++ b/code/modules/antagonists/nukeop/datums/operative.dm
@@ -15,6 +15,8 @@
var/datum/team/nuclear/nuke_team
/// Should the user be moved to default spawnpoint after being granted this datum.
var/send_to_spawnpoint = TRUE
+ /// Should the nukie get a little bonus tc depending on how many players there are
+ var/give_bonus_tc = TRUE
var/job_type = /datum/job/nuclear_operative
/// The DEFAULT outfit we will give to players granted this datum
@@ -49,11 +51,11 @@
equip_op()
if(send_to_spawnpoint)
move_to_spawnpoint()
- // grant extra TC for the people who start in the nukie base ie. not the lone op
- var/extra_tc = CEILING(GLOB.joined_player_list.len/5, 5)
+ if(give_bonus_tc)
+ var/extra_tc = CEILING(GLOB.joined_player_list.len / 5, 5)
var/datum/component/uplink/uplink = owner.find_syndicate_uplink()
- if (uplink)
- uplink.uplink_handler.add_telecrystals(extra_tc)
+ uplink?.uplink_handler.add_telecrystals(extra_tc)
+
var/datum/component/uplink/uplink = owner.find_syndicate_uplink()
if(uplink)
var/datum/team/nuclear/nuke_team = get_team()
@@ -81,7 +83,7 @@
objectives |= nuke_team.objectives
/datum/antagonist/nukeop/leader/get_spawnpoint()
- return pick(GLOB.nukeop_leader_start)
+ return pick(GLOB.nukeop_base_leader_start)
/datum/antagonist/nukeop/create_team(datum/team/nuclear/new_team)
if(!new_team)
@@ -146,7 +148,7 @@
return TRUE
/datum/antagonist/nukeop/proc/admin_send_to_base(mob/admin)
- owner.current.forceMove(pick(GLOB.nukeop_start))
+ owner.current.forceMove(pick(GLOB.nukeop_base_start))
/datum/antagonist/nukeop/proc/admin_tell_code(mob/admin)
var/code
@@ -192,7 +194,7 @@
if(nuke_team)
team_number = nuke_team.members.Find(owner)
- return GLOB.nukeop_start[((team_number - 1) % GLOB.nukeop_start.len) + 1]
+ return GLOB.nukeop_base_start[((team_number - 1) % GLOB.nukeop_base_start.len) + 1]
/datum/antagonist/nukeop/proc/spawn_infiltrator()
var/datum/map_template/shuttle/infiltrator/ship = SSmapping.shuttle_templates[infiltrator_id]
@@ -221,6 +223,6 @@
mobile_port.setTimer(mobile_port.ignitionTime)
/datum/antagonist/nukeop/on_respawn(mob/new_character)
- new_character.forceMove(pick(GLOB.nukeop_start))
+ new_character.forceMove(pick(GLOB.nukeop_base_start))
equip_op()
return TRUE
diff --git a/code/modules/antagonists/nukeop/datums/operative_leader.dm b/code/modules/antagonists/nukeop/datums/operative_leader.dm
index 7815c8c8631c..3f3aaaf31852 100644
--- a/code/modules/antagonists/nukeop/datums/operative_leader.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_leader.dm
@@ -1,6 +1,8 @@
/datum/antagonist/nukeop/leader
name = "Nuclear Operative Leader"
nukeop_outfit = /datum/outfit/syndicate/leader
+ /// Whether to spawn the infiltrator
+ var/spawn_ship = TRUE
/// Randomly chosen honorific, for distinction
var/title
/// The nuclear challenge remote we will spawn this player with.
@@ -63,7 +65,8 @@
return capitalize(newname)
/datum/antagonist/nukeop/leader/create_team(datum/team/nuclear/new_team)
- spawn_infiltrator()
+ if(spawn_ship)
+ spawn_infiltrator()
if(new_team)
return ..()
// Leaders always make new teams
diff --git a/code/modules/antagonists/nukeop/datums/operative_lone.dm b/code/modules/antagonists/nukeop/datums/operative_lone.dm
index b074016890aa..ba6d001ff807 100644
--- a/code/modules/antagonists/nukeop/datums/operative_lone.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_lone.dm
@@ -1,6 +1,7 @@
/datum/antagonist/nukeop/lone
name = "Lone Operative"
send_to_spawnpoint = FALSE //Handled by event
+ give_bonus_tc = FALSE
nukeop_outfit = /datum/outfit/syndicate/full/loneop
preview_outfit = /datum/outfit/nuclear_operative
preview_outfit_behind = null
diff --git a/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm b/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm
index eb386342c09a..9cbad2572f87 100644
--- a/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm
@@ -2,6 +2,7 @@
name = "Nuclear Operative Reinforcement"
show_in_antagpanel = FALSE
send_to_spawnpoint = FALSE
+ give_bonus_tc = FALSE
nukeop_outfit = /datum/outfit/syndicate/reinforcement
/datum/antagonist/nukeop/reinforcement/cyborg
diff --git a/code/modules/antagonists/nukeop/datums/operative_support.dm b/code/modules/antagonists/nukeop/datums/operative_support.dm
index 1b1ea923903d..b10bb4a369c0 100644
--- a/code/modules/antagonists/nukeop/datums/operative_support.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_support.dm
@@ -32,7 +32,7 @@
owner.current.grant_language(/datum/language/codespeak)
/datum/antagonist/nukeop/support/get_spawnpoint()
- return pick(GLOB.nukeop_overwatch_start)
+ return pick(GLOB.nukeop_base_overwatch_start)
/datum/antagonist/nukeop/support/forge_objectives()
var/datum/objective/overwatch/objective = new
diff --git a/code/modules/antagonists/nukeop/datums/operative_team.dm b/code/modules/antagonists/nukeop/datums/operative_team.dm
index 4eab9341d48c..98ffe695e47c 100644
--- a/code/modules/antagonists/nukeop/datums/operative_team.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_team.dm
@@ -18,7 +18,7 @@
objectives += maingoal
// when a nuke team is created, the infiltrator has not loaded in yet - it takes some time. so no nuke, we have to wait
- addtimer(CALLBACK(src, PROC_REF(assign_nuke_delayed)), 4 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(assign_nuke_delayed)), 5 SECONDS)
/datum/team/nuclear/roundend_report()
var/list/parts = list()
@@ -192,7 +192,7 @@
infil_or_nukebase = SPAWN_AT_BASE
if(infil_or_nukebase == SPAWN_AT_BASE)
- spawn_loc = pick(GLOB.nukeop_start)
+ spawn_loc = pick(GLOB.nukeop_base_start)
var/mob/living/carbon/human/nukie = new(spawn_loc)
chosen_one.client.prefs.safe_transfer_prefs_to(nukie, is_antag = TRUE)
@@ -200,6 +200,7 @@
var/datum/antagonist/nukeop/antag_datum = new()
antag_datum.send_to_spawnpoint = FALSE
+ antag_datum.give_bonus_tc = FALSE
antag_datum.nukeop_outfit = /datum/outfit/syndicate/reinforcement
nukie.mind.add_antag_datum(antag_datum, src)
@@ -313,20 +314,25 @@
..()
SEND_SIGNAL(src, COMSIG_NUKE_TEAM_ADDITION, new_member.current)
-/datum/team/nuclear/proc/assign_nuke_delayed()
- assign_nuke()
- if(tracked_nuke && memorized_code)
- for(var/datum/mind/synd_mind in members)
- var/datum/antagonist/nukeop/synd_datum = synd_mind.has_antag_datum(/datum/antagonist/nukeop)
- synd_datum?.memorize_code()
+/datum/team/nuclear/proc/assign_nuke_delayed(attempts = 0)
+ if(!assign_nuke())
+ if(attempts > 5)
+ stack_trace("Failed to assign nuke to team after multiple attempts. \
+ This likely means the nuke was not found. Please investigate.")
+ else
+ addtimer(CALLBACK(src, PROC_REF(assign_nuke_delayed), attempts + 1), 5 SECONDS)
+ return
+
+ for(var/datum/mind/synd_mind as anything in members)
+ var/datum/antagonist/nukeop/synd_datum = synd_mind.has_antag_datum(/datum/antagonist/nukeop)
+ synd_datum?.memorize_code()
/datum/team/nuclear/proc/assign_nuke()
- memorized_code = random_nukecode()
var/obj/machinery/nuclearbomb/syndicate/nuke = locate() in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/syndicate)
if(!nuke)
- stack_trace("Syndicate nuke not found during nuke team creation.")
- memorized_code = null
- return
+ return FALSE
+
+ memorized_code = random_nukecode()
tracked_nuke = nuke
if(nuke.r_code == NUKE_CODE_UNSET)
nuke.r_code = memorized_code
@@ -334,6 +340,7 @@
memorized_code = nuke.r_code
for(var/obj/machinery/nuclearbomb/beer/beernuke as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/beer))
beernuke.r_code = memorized_code
+ return TRUE
#undef SPAWN_AT_BASE
#undef SPAWN_AT_INFILTRATOR
@@ -341,14 +348,14 @@
/datum/team/nuclear/loneop
/datum/team/nuclear/loneop/assign_nuke()
- memorized_code = random_nukecode()
var/obj/machinery/nuclearbomb/selfdestruct/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/selfdestruct)
- if(nuke)
- tracked_nuke = nuke
- if(nuke.r_code == NUKE_CODE_UNSET)
- nuke.r_code = memorized_code
- else //Already set by admins/something else?
- memorized_code = nuke.r_code
- else
- stack_trace("Station self-destruct not found during lone op team creation.")
- memorized_code = null
+ if(!nuke)
+ return FALSE
+
+ memorized_code = random_nukecode()
+ tracked_nuke = nuke
+ if(nuke.r_code == NUKE_CODE_UNSET)
+ nuke.r_code = memorized_code
+ else //Already set by admins/something else?
+ memorized_code = nuke.r_code
+ return TRUE
diff --git a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
index 05187e7f0672..084723b1e2ae 100644
--- a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
+++ b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
@@ -173,7 +173,7 @@
/obj/machinery/piratepad
name = "cargo hold pad"
icon = 'icons/obj/machines/telepad.dmi'
- icon_state = "lpad-idle-off"
+ icon_state = "lpad-off"
base_icon_state = "lpad"
/// Determines what icon is being shown
VAR_PRIVATE/is_sending = FALSE
@@ -213,11 +213,11 @@
/obj/machinery/piratepad/update_icon_state()
. = ..()
if(panel_open)
- icon_state = "[base_icon_state]-idle-open"
+ icon_state = "[base_icon_state]-open"
else if(is_sending)
- icon_state = "[base_icon_state]-idle"
+ icon_state = "[base_icon_state]"
else
- icon_state = "[base_icon_state]-idle-off"
+ icon_state = "[base_icon_state]-off"
/obj/machinery/computer/piratepad_control
name = "cargo hold control terminal"
diff --git a/code/modules/antagonists/space_dragon/carp_rift.dm b/code/modules/antagonists/space_dragon/carp_rift.dm
index ac1b2967dfd4..3d640b8b57a2 100644
--- a/code/modules/antagonists/space_dragon/carp_rift.dm
+++ b/code/modules/antagonists/space_dragon/carp_rift.dm
@@ -340,7 +340,7 @@
/obj/structure/carp_rift/hitby(atom/movable/hit_by, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
if(HAS_TRAIT(hit_by, TRAIT_TELEKINESIS_CONTROLLED))
- var/mob/thrower = throwingdatum.thrower.resolve()
+ var/mob/thrower = throwingdatum.thrower?.resolve()
if(thrower && ismob(thrower))
to_chat(thrower, span_warning("The gravitational field of [src] interferes with the telekenetic control of [hit_by], nullifying the hit!"))
return
diff --git a/code/modules/antagonists/traitor/contractor/contractor_items.dm b/code/modules/antagonists/traitor/contractor/contractor_items.dm
index 86bbaf4168c5..7375660f969d 100644
--- a/code/modules/antagonists/traitor/contractor/contractor_items.dm
+++ b/code/modules/antagonists/traitor/contractor/contractor_items.dm
@@ -4,7 +4,6 @@
icon_state = "pinpointer_syndicate"
worn_icon_state = "pinpointer_black"
minimum_range = 25
- has_owner = TRUE
ignore_suit_sensor_level = TRUE
/obj/item/paper/contractor_guide
diff --git a/code/modules/antagonists/voidwalker/voidwalker_mob.dm b/code/modules/antagonists/voidwalker/voidwalker_mob.dm
index 2cd3ff136bbc..1300c7b6d378 100644
--- a/code/modules/antagonists/voidwalker/voidwalker_mob.dm
+++ b/code/modules/antagonists/voidwalker/voidwalker_mob.dm
@@ -40,6 +40,11 @@
hud_possible = list(ANTAG_HUD)
sight = SEE_TURFS | SEE_MOBS
+ //purplish tint night vision because the voidwalker is purple
+ lighting_cutoff_red = 30
+ lighting_cutoff_green = 15
+ lighting_cutoff_blue = 30
+
/// Color of our regen outline
var/regenerate_colour = COLOR_GRAY
diff --git a/code/modules/antagonists/voidwalker/voidwalker_window.dm b/code/modules/antagonists/voidwalker/voidwalker_window.dm
index b6dad0722dad..c62dbf68c528 100644
--- a/code/modules/antagonists/voidwalker/voidwalker_window.dm
+++ b/code/modules/antagonists/voidwalker/voidwalker_window.dm
@@ -26,7 +26,7 @@
velocity = list(0, 0.2, 0)
position = generator(GEN_BOX, list(-8,-16,0), list(8,16,0), NORMAL_RAND)
drift = generator(GEN_BOX, list(-8,-16,0), list(8,16,0), NORMAL_RAND)
- spin = generator(GEN_NUM, list(-5,5), NORMAL_RAND)
+ spin = generator(GEN_NUM, -5, 5, NORMAL_RAND)
friction = 0.5
gravity = list(0, 0)
grow = 0.05
diff --git a/code/modules/antagonists/wizard/equipment/soulstone.dm b/code/modules/antagonists/wizard/equipment/soulstone.dm
index dbf4d3c43615..c822f7022206 100644
--- a/code/modules/antagonists/wizard/equipment/soulstone.dm
+++ b/code/modules/antagonists/wizard/equipment/soulstone.dm
@@ -205,6 +205,11 @@
log_combat(user, M, "captured [M.name]'s soul", src)
capture_soul(M, user)
+/obj/item/soulstone/suicide_act(mob/living/user)
+ . = ..()
+ user.visible_message(span_suicide("[user] is capturing [user.p_their()] own soul with [src]! It looks like [user.p_theyre()] trying to commit suicide!"))
+ return capture_soul(user, null, TRUE) ? MANUAL_SUICIDE : BRUTELOSS
+
///////////////////Options for using captured souls///////////////////////////////////////
/obj/item/soulstone/attack_self(mob/living/user)
diff --git a/code/modules/antagonists/wizard/slaughter_antag.dm b/code/modules/antagonists/wizard/slaughter_antag.dm
index effd5d2abf63..bce27599c8cd 100644
--- a/code/modules/antagonists/wizard/slaughter_antag.dm
+++ b/code/modules/antagonists/wizard/slaughter_antag.dm
@@ -3,7 +3,7 @@
roundend_category = "demons"
show_name_in_check_antagonists = TRUE
ui_name = "AntagInfoDemon"
- pref_flag = ROLE_ALIEN
+ pref_flag = ROLE_SENTIENCE
show_in_antagpanel = FALSE
show_to_ghosts = TRUE
antagpanel_category = ANTAG_GROUP_WIZARDS
diff --git a/code/modules/art/statues.dm b/code/modules/art/statues.dm
index 2bb491227472..e86b53d68ea2 100644
--- a/code/modules/art/statues.dm
+++ b/code/modules/art/statues.dm
@@ -332,14 +332,17 @@
. = ..()
AddElement(/datum/element/eyestab)
AddElement(/datum/element/wall_engraver)
- //deals 200 damage to statues, meaning you can actually kill one in ~250 hits
- AddElement(/datum/element/bane, target_type = /mob/living/basic/statue, damage_multiplier = 40)
+ AddComponent(/datum/component/bane, damage_multiplier = 40, should_bane_callback = CALLBACK(src, PROC_REF(bane_check)), label_text = "statues")
/obj/item/chisel/Destroy()
prepared_block = null
tracked_user = null
return ..()
+/// Bane component callback
+/obj/item/chisel/proc/bane_check(mob/living/target)
+ return istype(target, /mob/living/basic/statue)
+
/*
Hit the block to start
Point with the chisel at the target to choose what to sculpt or hit block to choose from preset statue types.
diff --git a/code/modules/asset_cache/assets/tgui.dm b/code/modules/asset_cache/assets/tgui.dm
index 4b31d93e037f..89556bc4487e 100644
--- a/code/modules/asset_cache/assets/tgui.dm
+++ b/code/modules/asset_cache/assets/tgui.dm
@@ -33,3 +33,9 @@
)
#endif
+
+/datum/asset/simple/chat_dark
+ keep_local_name = FALSE
+ assets = list(
+ "tgui-chat-dark.bundle.css" = file("tgui/public/tgui-chat-dark.bundle.css"),
+ )
diff --git a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm
index b48dbd53c0c3..5f24e9397463 100644
--- a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm
+++ b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm
@@ -55,12 +55,15 @@
if(TOOL_SCREWDRIVER)
context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel"
if(TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_RMB] = "Rotate"
+ context[SCREENTIP_CONTEXT_LMB] = "Rotate"
return CONTEXTUAL_SCREENTIP_SET
/obj/machinery/atmospherics/components/binary/crystallizer/screwdriver_act(mob/living/user, obj/item/tool)
return on ? NONE : default_deconstruction_screwdriver(user, tool)
+/obj/machinery/atmospherics/components/binary/crystallizer/wrench_act(mob/living/user, obj/item/tool)
+ return default_change_direction_wrench(user, tool)
+
/obj/machinery/atmospherics/components/binary/crystallizer/crowbar_act(mob/living/user, obj/item/tool)
return crowbar_deconstruction_act(user, tool, internal.return_pressure())
diff --git a/code/modules/autowiki/pages/vending.dm b/code/modules/autowiki/pages/vending.dm
index e110afa760ea..eb10e0549e4f 100644
--- a/code/modules/autowiki/pages/vending.dm
+++ b/code/modules/autowiki/pages/vending.dm
@@ -11,7 +11,7 @@
var/obj/parent = new
for (var/obj/machinery/vending/vending_type as anything in sort_list(subtypesof(/obj/machinery/vending), GLOBAL_PROC_REF(cmp_typepaths_asc)))
- var/obj/machinery/vending/parent_machine = type2parent(vending_type)
+ var/obj/machinery/vending/parent_machine = vending_type::parent_type
if(initial(parent_machine.name) == initial(vending_type.name))
continue //Same name, likely just a slightly touched up subtype for specific maps.
var/obj/machinery/vending/vending_machine = new vending_type(parent)
diff --git a/code/modules/bitrunning/components/avatar_connection.dm b/code/modules/bitrunning/components/avatar_connection.dm
index 5c3844dbf9d0..3e1c9e18b231 100644
--- a/code/modules/bitrunning/components/avatar_connection.dm
+++ b/code/modules/bitrunning/components/avatar_connection.dm
@@ -187,7 +187,11 @@
if(damage > 30 && prob(30))
INVOKE_ASYNC(old_body, TYPE_PROC_REF(/mob/living, emote), "scream")
- old_body.apply_damage(damage, damage_type, def_zone, blocked, wound_bonus = CANT_WOUND)
+ var/zone = def_zone
+ if(isbodypart(def_zone)) // If the defined zone is a bodypart, then it is the bodypart of the domain avatar. We don't want that bodypart, we want the real body's bodypart, so we go back to zone.
+ zone = astype(def_zone, /obj/item/bodypart).body_zone
+
+ old_body.apply_damage(damage, damage_type, zone, blocked, wound_bonus = CANT_WOUND)
if(old_body.stat > SOFT_CRIT) // KO!
full_avatar_disconnect(cause_damage = TRUE)
diff --git a/code/modules/bitrunning/components/bitrunning_points.dm b/code/modules/bitrunning/components/bitrunning_points.dm
deleted file mode 100644
index 039f3a9d3384..000000000000
--- a/code/modules/bitrunning/components/bitrunning_points.dm
+++ /dev/null
@@ -1,36 +0,0 @@
-/// Attaches to a turf so it spawns a crate when a certain amount of points are added to it.
-/datum/component/bitrunning_points
- /// The amount required to spawn a crate
- var/points_goal = 10
- /// A special condition limits this from spawning a crate
- var/points_received = 0
-
-
-/datum/component/bitrunning_points/Initialize(datum/lazy_template/virtual_domain/domain)
- if(!isturf(parent))
- return COMPONENT_INCOMPATIBLE
-
- RegisterSignal(domain, COMSIG_BITRUNNER_GOAL_POINT, PROC_REF(on_add_points))
-
-
-/// Listens for points to be added which will eventually spawn a crate.
-/datum/component/bitrunning_points/proc/on_add_points(datum/source, points_to_add)
- SIGNAL_HANDLER
-
- points_received += points_to_add
-
- if(points_received < points_goal)
- return
-
- reveal()
-
-
-/// Spawns the crate with some effects
-/datum/component/bitrunning_points/proc/reveal()
- playsound(src, 'sound/effects/magic/blink.ogg', 50, TRUE)
-
- var/turf/tile = parent
- var/obj/structure/closet/crate/secure/bitrunning/encrypted/crate = new()
- crate.forceMove(tile) // Triggers any on-move effects on that turf
- do_sparks(5, FALSE, tile, spark_type = /datum/effect_system/basic/spark_spread/quantum)
- qdel(src)
diff --git a/code/modules/bitrunning/objects/hololadder.dm b/code/modules/bitrunning/objects/hololadder.dm
index 3df41a403e73..1335ef8b848b 100644
--- a/code/modules/bitrunning/objects/hololadder.dm
+++ b/code/modules/bitrunning/objects/hololadder.dm
@@ -64,6 +64,14 @@
if(isnull(user.mind))
return
+ var/obj/machinery/quantum_server/our_server = server_ref.resolve()
+ if(!our_server.domain_complete)
+ for(var/datum/weakref/ghostrole_weakref as anything in our_server.spawned_threat_refs)
+ var/mob/living/ghostrole = ghostrole_weakref.resolve()
+ if(ghostrole?.stat == CONSCIOUS && ghostrole.client && ghostrole.mind.has_antag_datum(/datum/antagonist/bitrunning_glitch))
+ to_chat(user, span_danger("A being in the simulation is preventing your retreat. You must either complete your mission or remove the obstacle before safe exit will be possible."))
+ return
+
if(!HAS_TRAIT(user, TRAIT_TEMPORARY_BODY))
balloon_alert(user, "no connection detected")
return
diff --git a/code/modules/bitrunning/server/_parent.dm b/code/modules/bitrunning/server/_parent.dm
index 89525514e471..a0e60892326b 100644
--- a/code/modules/bitrunning/server/_parent.dm
+++ b/code/modules/bitrunning/server/_parent.dm
@@ -16,6 +16,8 @@
var/datum/lazy_template/virtual_domain/generated_domain
/// If the current domain was a random selection
var/domain_randomized = FALSE
+ /// Whether the domain is finished, so the bitrunners can leave despite glitches
+ var/domain_complete = FALSE
/// Prevents multiple user actions. Handled by loading domains and cooldowns
var/is_ready = TRUE
/// Chance multipled by threat to spawn a glitch
diff --git a/code/modules/bitrunning/server/loot.dm b/code/modules/bitrunning/server/loot.dm
index 9fdbcebbca4a..2d9db6c5a7d0 100644
--- a/code/modules/bitrunning/server/loot.dm
+++ b/code/modules/bitrunning/server/loot.dm
@@ -80,6 +80,8 @@
generated_domain.disk_reward_spawned = TRUE
chosen_forge.start_to_spawn(reward_cache)
+
+ domain_complete = TRUE
return TRUE
diff --git a/code/modules/bitrunning/server/map_handling.dm b/code/modules/bitrunning/server/map_handling.dm
index 96a10d60bbf5..9076f30a73a1 100644
--- a/code/modules/bitrunning/server/map_handling.dm
+++ b/code/modules/bitrunning/server/map_handling.dm
@@ -53,12 +53,12 @@
scrub_vdom()
is_ready = TRUE
return FALSE
-
-
+
+
//We will want to record how the domain was selected. Either entirely randomly, with the name redacted, or with full information.
//Without this, it is difficult to determine what domains are selected more often intentionallly, vs unintentionally.
var/selection_type
-
+
if(was_random_selection)
selection_type = "random selection"
else
@@ -168,8 +168,7 @@
continue
if(istype(thing, /obj/effect/landmark/bitrunning/loot_signal))
- var/turf/signaler_turf = get_turf(thing)
- signaler_turf.AddComponent(/datum/component/bitrunning_points, generated_domain)
+ generated_domain.main_crate_loc = thing.loc
qdel(thing)
continue
@@ -200,6 +199,7 @@
/// Stops the current virtual domain and disconnects all users
/obj/machinery/quantum_server/proc/reset(fast = FALSE)
is_ready = FALSE
+ domain_complete = FALSE
sever_connections()
diff --git a/code/modules/bitrunning/server/threats.dm b/code/modules/bitrunning/server/threats.dm
index 34583aa358de..c9c33c49f44b 100644
--- a/code/modules/bitrunning/server/threats.dm
+++ b/code/modules/bitrunning/server/threats.dm
@@ -142,6 +142,22 @@
aas.broadcast("QUANTUM SERVER ALERT: Fabrication protocols have crashed unexpectedly. Please evacuate the area.", list(RADIO_CHANNEL_SUPPLY))
timeout = 10 SECONDS
+ var/bitrunners_alive = 0
+ var/island_brawl_exception = istype(generated_domain, /datum/lazy_template/virtual_domain/island_brawl)
+ for(var/datum/weakref/bitrunner_ref in avatar_connection_refs)
+ var/mob/living/bitrunner = astype(bitrunner_ref.resolve(), /datum/component/avatar_connection)?.parent
+ if(!bitrunner)
+ continue
+ if((bitrunner.stat > CONSCIOUS) || !bitrunner.client)
+ continue
+ if(island_brawl_exception)
+ timeout *= max(5 - generated_domain.main_crate_points, 1)
+ continue
+ bitrunners_alive++
+ timeout *= 5
+ if(bitrunners_alive)
+ to_chat(antag, span_warning("[bitrunners_alive] criminals still remain here, pilfering your domain. It will be more difficult to leave until they are handled."))
+
if(!do_after(antag, timeout) || QDELETED(chosen_forge) || QDELETED(antag) || QDELETED(src) || !is_ready || !is_operational)
chosen_forge.setup_particles()
return
diff --git a/code/modules/bitrunning/virtual_domain/virtual_domain.dm b/code/modules/bitrunning/virtual_domain/virtual_domain.dm
index 8fc3173df951..139e46d5508a 100644
--- a/code/modules/bitrunning/virtual_domain/virtual_domain.dm
+++ b/code/modules/bitrunning/virtual_domain/virtual_domain.dm
@@ -52,6 +52,12 @@
var/secondary_loot_generated
/// Has this domain been beaten with high enough score to spawn a tech disk?
var/disk_reward_spawned = FALSE
+ /// The amount of points towards the spawning of the main crate, on maps using points.
+ var/main_crate_points = 0
+ /// The amount of points required to spawn the main crate, on maps using points.
+ var/main_crate_point_goal = 10
+ /// The location the crate will spawn when enough points are accumulated, on maps using points.
+ var/main_crate_loc
/**
* Modularity
@@ -99,7 +105,19 @@
/// Sends a point to any loot signals on the map
/datum/lazy_template/virtual_domain/proc/add_points(points_to_add = 1)
- SEND_SIGNAL(src, COMSIG_BITRUNNER_GOAL_POINT, points_to_add)
+ main_crate_points += points_to_add
+ if(main_crate_points >= main_crate_point_goal)
+ reveal()
+
+/datum/lazy_template/virtual_domain/proc/reveal()
+ if(!main_crate_loc)
+ return
+ var/turf/spawn_loc = get_turf(main_crate_loc)
+ playsound(spawn_loc, 'sound/effects/magic/blink.ogg', 50, TRUE)
+ var/obj/structure/closet/crate/secure/bitrunning/encrypted/crate = new()
+ crate.forceMove(spawn_loc) // Triggers any on-move effects on that turf
+ do_sparks(5, FALSE, spawn_loc, spark_type = /datum/effect_system/basic/spark_spread/quantum)
+ main_crate_loc = null
/// Loads the ghost candidates.
/datum/lazy_template/virtual_domain/proc/load_advanced_npcs(list/mob/lucky_ghosts)
diff --git a/code/modules/cargo/bounties/security.dm b/code/modules/cargo/bounties/security.dm
index f94c1973cbb8..aad196c3b5e7 100644
--- a/code/modules/cargo/bounties/security.dm
+++ b/code/modules/cargo/bounties/security.dm
@@ -68,6 +68,7 @@
start_tracking(id_card)
/datum/bounty/patrol/proc/start_tracking(obj/item/card/id/id_card)
+ contribution |= id_card.registered_account
tracker = AddComponent(/datum/component/connect_containers, id_card, list(COMSIG_MOVABLE_MOVED = PROC_REF(on_card_moved)))
RegisterSignal(id_card, COMSIG_MOVABLE_MOVED, PROC_REF(on_card_moved))
diff --git a/code/modules/cargo/exports/lavaland.dm b/code/modules/cargo/exports/lavaland.dm
index 78f98cb04da6..576ca4d8c95a 100644
--- a/code/modules/cargo/exports/lavaland.dm
+++ b/code/modules/cargo/exports/lavaland.dm
@@ -45,6 +45,7 @@
/obj/item/jacobs_ladder,
/obj/item/borg/upgrade/modkit/lifesteal,
/obj/item/clockwork_alloy,
+ /obj/item/gun/energy/recharge/kinetic_accelerator/bdm,
)
/datum/export/lavaland/major //valuable chest/ruin loot, minor megafauna loot
diff --git a/code/modules/cargo/goodies.dm b/code/modules/cargo/goodies.dm
index e7cec6e0c613..c6cfa0cf2aab 100644
--- a/code/modules/cargo/goodies.dm
+++ b/code/modules/cargo/goodies.dm
@@ -203,6 +203,12 @@
cost = PAYCHECK_CREW * 3
contains = list(/obj/item/storage/toolbox/mechanical)
+/datum/supply_pack/goody/autolatheboard
+ name = "Autolathe Circuit Board"
+ desc = "A single autolathe circuit board for your construction needs."
+ cost = PAYCHECK_CREW * 2
+ contains = list(/obj/item/circuitboard/machine/autolathe)
+
/datum/supply_pack/goody/valentine
name = "Valentine Card"
desc = "Make an impression on that special someone! Comes with one valentine card and a free candy heart!"
@@ -441,12 +447,17 @@
cost = PAYCHECK_CREW * 5
contains = list(/obj/item/key/golfcart)
-
/datum/supply_pack/goody/handheld_crew_monitor
name = "Handheld Crew Monitor"
- desc = "A crate containing a handheld crew monitor"
+ desc = "A crate containing a handheld crew monitor."
cost = /obj/item/sensor_device::custom_premium_price * 1.25 // 1.25X base vending machine value
contains = list(
/obj/item/sensor_device,
)
crate_name = "handheld crew monitor crate"
+
+/datum/supply_pack/goody/camera
+ name = "Broadcast Camera"
+ desc = "A single broadcast camera which broadcasts to the station's entertainment monitors, for all your theatrical needs."
+ cost = PAYCHECK_COMMAND * 8
+ contains = list(/obj/item/broadcast_camera/cargo)
diff --git a/code/modules/cargo/markets/market_items/weapons.dm b/code/modules/cargo/markets/market_items/weapons.dm
index c9326807cc5e..ce2c16735fe3 100644
--- a/code/modules/cargo/markets/market_items/weapons.dm
+++ b/code/modules/cargo/markets/market_items/weapons.dm
@@ -153,3 +153,13 @@
price_max = CARGO_CRATE_VALUE * 5
stock_max = 2
availability_prob = 80
+
+/datum/market_item/weapon/earthcracker
+ name = "E-1 Earthcracker"
+ desc = "Surplus mining equipment from one of the many, nameless asteroid mining companies bought out by Nanotrasen in this sector.\
+ While they were decent enough at mining asteroids, these things really shine at leaving large cracks on station-class hulls when deployed these days."
+ item = /obj/item/earthcracker/small
+ price_min = CARGO_CRATE_VALUE
+ price_max = CARGO_CRATE_VALUE * 3
+ stock_max = 1
+ availability_prob = 30
diff --git a/code/modules/cargo/packs/organic.dm b/code/modules/cargo/packs/organic.dm
index 0967f66f030d..dce618895d9b 100644
--- a/code/modules/cargo/packs/organic.dm
+++ b/code/modules/cargo/packs/organic.dm
@@ -88,6 +88,7 @@
/obj/item/food/grown/cucumber,
)
+/* // DARKPACK EDIT REMOVAL
/datum/supply_pack/organic/exoticseeds
name = "Exotic Seeds Crate"
desc = "Any entrepreneuring botanist's dream. Contains twelve different seeds, \
@@ -110,6 +111,7 @@
)
crate_name = "exotic seeds crate"
crate_type = /obj/structure/closet/crate/hydroponics
+*/
/datum/supply_pack/organic/food
name = "Food Crate"
diff --git a/code/modules/cargo/packs/science.dm b/code/modules/cargo/packs/science.dm
index e35601e244f7..a81db2ee0e2e 100644
--- a/code/modules/cargo/packs/science.dm
+++ b/code/modules/cargo/packs/science.dm
@@ -122,4 +122,16 @@
contains = list(/obj/item/mod/core/standard = 3)
crate_name = "\improper MOD core crate"
crate_type = /obj/structure/closet/crate/nakamura
+
+/datum/supply_pack/science/gizmo
+ name = "Gizmo research crate"
+ desc = "Three weird science gizmo thinga-majiggers? We don't know what they do."
+ cost = CARGO_CRATE_VALUE * 5
+ access = ACCESS_SCIENCE
+ access_view = ACCESS_SCIENCE
+ contains = list(
+ /obj/machinery/gizmo = 1,
+ /obj/effect/spawner/random/gizmo = 2,
+ )
+ crate_name = "\improper Gizmo research crate"
*/
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index 989ff3efa1e2..84dd50428ef1 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -79,6 +79,9 @@
//SOUND STUFF//
///////////////
+ /// Sound tokens currently playing for this client. Managed by /datum/sound_token and the soundtoken subsystem!! SOUND TOKENS 2026
+ var/list/datum/sound_token/sound_tokens = list()
+
////////////
//SECURITY//
////////////
@@ -256,5 +259,7 @@
/// The DPI scale of the client. 1 is equivalent to 100% window scaling, 2 will be 200% window scaling
var/window_scaling
+ var/datum/tgui_window/stat_panel
+
/// OOC colour of the clients messages.
var/ooc_colour = null
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 1da97852223c..f04bd580c639 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -562,8 +562,6 @@ GLOBAL_LIST_INIT(unrecommended_builds, list(
to_chat(src, span_info("You have unread updates in the changelog."))
if(CONFIG_GET(flag/aggressive_changelog))
changelog()
- else
- winset(src, "infobuttons.changelog", "font-style=bold")
if(ckey in GLOB.clientmessages)
for(var/message in GLOB.clientmessages[ckey])
@@ -584,19 +582,16 @@ GLOBAL_LIST_INIT(unrecommended_builds, list(
if(!tooltips)
tooltips = new /datum/tooltip(src)
- if (!interviewee)
- initialize_menus()
-
loot_panel = new(src)
view_size = new(src)
- set_fullscreen(logging_in = TRUE)
view_size.resetFormat()
view_size.setZoomMode()
view_size.apply()
Master.UpdateTickRate()
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CLIENT_CONNECT, src)
fully_created = TRUE
+ set_fullscreen()
//////////////
//DISCONNECT//
@@ -646,6 +641,8 @@ GLOBAL_LIST_INIT(unrecommended_builds, list(
SSambience.remove_ambience_client(src)
SSmouse_entered.hovers -= src
SSping.currentrun -= src
+ SSsound_tokens.clients_needing_update -= src
+ SSsound_tokens.currentrun -= src
QDEL_NULL(view_size)
QDEL_NULL(void)
QDEL_NULL(tooltips)
@@ -1124,29 +1121,6 @@ GLOBAL_LIST_INIT(unrecommended_builds, list(
return
to_chat(src, span_userdanger("Statpanel failed to load, click here to reload the panel "))
-/**
- * Initializes dropdown menus on client
- */
-/client/proc/initialize_menus()
- var/list/topmenus = GLOB.menulist[/datum/verbs/menu]
- for (var/thing in topmenus)
- var/datum/verbs/menu/topmenu = thing
- var/topmenuname = "[topmenu]"
- if (topmenuname == "[topmenu.type]")
- var/list/tree = splittext(topmenuname, "/")
- topmenuname = tree[tree.len]
- winset(src, "[topmenu.type]", "parent=menu;name=[url_encode(topmenuname)]")
- var/list/entries = topmenu.Generate_list(src)
- for (var/child in entries)
- winset(src, "[child]", "[entries[child]]")
- if (!ispath(child, /datum/verbs/menu))
- var/procpath/verbpath = child
- if (verbpath.name[1] != "@")
- new child(src)
-
- // Place Help back at the end.
- winset(src, "help-menu", "index=1000")
-
/client/proc/open_filter_editor(atom/in_atom)
if(holder)
holder.filterrific = new /datum/filter_editor(in_atom)
@@ -1160,13 +1134,13 @@ GLOBAL_LIST_INIT(unrecommended_builds, list(
/client/proc/set_right_click_menu_mode(shift_only)
if(shift_only)
- winset(src, "mapwindow.map", "right-click=true")
- winset(src, "ShiftUp", "is-disabled=false")
- winset(src, "Shift", "is-disabled=false")
+ winset(src, SKIN_MAPWINDOW_MAP, "right-click=true")
+ winset(src, SKIN_DEFAULT_SHIFTUP, "is-disabled=false")
+ winset(src, SKIN_DEFAULT_SHIFT, "is-disabled=false")
else
- winset(src, "mapwindow.map", "right-click=false")
- winset(src, "default.Shift", "is-disabled=true")
- winset(src, "default.ShiftUp", "is-disabled=true")
+ winset(src, SKIN_MAPWINDOW_MAP, "right-click=false")
+ winset(src, SKIN_DEFAULT_SHIFT, "is-disabled=true")
+ winset(src, SKIN_DEFAULT_SHIFTUP, "is-disabled=true")
/client/proc/update_ambience_pref(value)
if(value)
@@ -1235,15 +1209,8 @@ GLOBAL_LIST_INIT(unrecommended_builds, list(
prefs.write_preference(GLOB.preference_entries[/datum/preference/toggle/fullscreen_mode], !is_on)
set_fullscreen()
-/client/proc/set_fullscreen(logging_in = FALSE)
- var/fullscreen = prefs?.read_preference(/datum/preference/toggle/fullscreen_mode)
- //no need to set every login to not fullscreen, they already aren't.
- //we also dont need to call attempt_auto_fit_viewport, Login does that for us.
- if(logging_in)
- if(fullscreen)
- winset(src, "mainwindow", "menu=;is-fullscreen=[fullscreen ? "true" : "false"]")
- return
- winset(src, "mainwindow", "menu=;is-fullscreen=[fullscreen ? "true" : "false"]")
+/client/proc/set_fullscreen()
+ winset(src, SKIN_MAINWINDOW, "is-fullscreen=[prefs?.read_preference(/datum/preference/toggle/fullscreen_mode) ? "true" : "false"]")
attempt_auto_fit_viewport()
/// Clears the client's screen, aside from ones that opt out
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index c8e24a15f9d9..f686126bfcc2 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -260,6 +260,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
for(var/datum/preference_middleware/preference_middleware as anything in middleware)
preference_middleware.post_set_preference(ui.user, requested_preference_key, value)
+ requested_preference.post_set_preference(ui.user, value) // DARKPACK EDIT ADD - SPLATS - (lore primers)
return TRUE
if ("set_color_preference")
var/requested_preference_key = params["preference"]
diff --git a/code/modules/client/preferences/names.dm b/code/modules/client/preferences/names.dm
index 338fc17c9ca0..fee5edb04248 100644
--- a/code/modules/client/preferences/names.dm
+++ b/code/modules/client/preferences/names.dm
@@ -174,7 +174,7 @@
return FALSE
// If one of the roles is ticked in the antag prefs menu, this option will show.
- var/static/list/ops_roles = list(ROLE_OPERATIVE, ROLE_LONE_OPERATIVE, ROLE_OPERATIVE_MIDROUND, ROLE_CLOWN_OPERATIVE)
+ var/static/list/ops_roles = list(ROLE_OPERATIVE, ROLE_LONE_OPERATIVE, ROLE_OPERATIVE_MIDROUND, ROLE_CLOWN_OPERATIVE, ROLE_CLOWN_OPERATIVE_MIDROUND)
if(length(ops_roles & preferences.be_special))
return TRUE
diff --git a/code/modules/client/preferences/operative_species.dm b/code/modules/client/preferences/operative_species.dm
index 5bee923c4153..e67ffff75682 100644
--- a/code/modules/client/preferences/operative_species.dm
+++ b/code/modules/client/preferences/operative_species.dm
@@ -14,7 +14,7 @@
return FALSE
// If one of the roles is ticked in the antag prefs menu, this option will show.
- var/static/list/ops_roles = list(ROLE_OPERATIVE, ROLE_LONE_OPERATIVE, ROLE_OPERATIVE_MIDROUND, ROLE_CLOWN_OPERATIVE)
+ var/static/list/ops_roles = list(ROLE_OPERATIVE, ROLE_LONE_OPERATIVE, ROLE_OPERATIVE_MIDROUND, ROLE_CLOWN_OPERATIVE, ROLE_CLOWN_OPERATIVE_MIDROUND)
if(length(ops_roles & preferences.be_special))
return TRUE
diff --git a/code/modules/client/preferences/statpanel.dm b/code/modules/client/preferences/statpanel.dm
index 4c9e3c51b262..3924e0743fec 100644
--- a/code/modules/client/preferences/statpanel.dm
+++ b/code/modules/client/preferences/statpanel.dm
@@ -9,3 +9,8 @@
return FALSE
return is_admin(preferences.parent)
+
+/datum/preference/toggle/statpanel
+ savefile_key = "statpanel_open"
+ savefile_identifier = PREFERENCE_PLAYER
+ default_value = TRUE
diff --git a/code/modules/client/preferences/status_bar.dm b/code/modules/client/preferences/status_bar.dm
index 1f1724ff49e0..853280cd7fc9 100644
--- a/code/modules/client/preferences/status_bar.dm
+++ b/code/modules/client/preferences/status_bar.dm
@@ -7,4 +7,4 @@
/datum/preference/toggle/status_bar/apply_to_client(client/client, value)
if(isnull(client) || istype(client, /datum/client_interface)) //no winset on mock clients.
return
- winset(client, "mapwindow.status_bar", "is-visible=[value]")
+ winset(client, SKIN_MAPWINDOW_STATUS_BAR, "is-visible=[value]")
diff --git a/code/modules/client/preferences_menu.dm b/code/modules/client/preferences_menu.dm
index 887816a0a0af..30c658d45410 100644
--- a/code/modules/client/preferences_menu.dm
+++ b/code/modules/client/preferences_menu.dm
@@ -1,26 +1,22 @@
-/datum/verbs/menu/Preferences/verb/open_character_preferences()
+/client/verb/open_character_preferences()
set category = "OOC"
set name = "Open Character Preferences"
set desc = "Open Character Preferences"
- var/datum/preferences/preferences = usr?.client?.prefs
- if (!preferences)
+ if(!prefs)
return
+ prefs.current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES
+ prefs.update_static_data(usr)
+ prefs.ui_interact(usr)
- preferences.current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES
- preferences.update_static_data(usr)
- preferences.ui_interact(usr)
-
-/datum/verbs/menu/Preferences/verb/open_game_preferences()
+/client/verb/open_game_preferences()
set category = "OOC"
set name = "Open Game Preferences"
set desc = "Open Game Preferences"
- var/datum/preferences/preferences = usr?.client?.prefs
- if (!preferences)
+ if(!prefs)
return
-
- preferences.current_window = PREFERENCE_TAB_GAME_PREFERENCES
- preferences.update_static_data(usr)
- preferences.ui_interact(usr)
+ prefs.current_window = PREFERENCE_TAB_GAME_PREFERENCES
+ prefs.update_static_data(usr)
+ prefs.ui_interact(usr)
diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm
index ce33331c6e4d..7aa9ffd8fb85 100644
--- a/code/modules/client/verbs/ooc.dm
+++ b/code/modules/client/verbs/ooc.dm
@@ -351,16 +351,16 @@ ADMIN_VERB(reset_ooc_color, R_FUN, "Reset Player OOC Color", "Returns player OOC
var/aspect_ratio = view_size[1] / view_size[2]
// Calculate desired pixel width using window size and aspect ratio
- var/list/sizes = params2list(winget(src, "mainwindow.split;mapwindow", "size"))
+ var/list/sizes = params2list(winget(src, "[SKIN_MAINWINDOW_SPLIT];[SKIN_MAPWINDOW]", "size"))
// Client closed the window? Some other error? This is unexpected behaviour, let's
// CRASH with some info.
- if(!sizes["mapwindow.size"])
+ if(!sizes["[SKIN_MAPWINDOW].size"])
CRASH("sizes does not contain mapwindow.size key. This means a winget failed to return what we wanted. --- sizes var: [sizes] --- sizes length: [length(sizes)]")
- var/list/map_size = splittext(sizes["mapwindow.size"], "x")
+ var/list/map_size = splittext(sizes["[SKIN_MAPWINDOW].size"], "x")
- var/split_size = splittext(sizes["mainwindow.split.size"], "x")
+ var/split_size = splittext(sizes["[SKIN_MAINWINDOW_SPLIT].size"], "x")
var/split_width = text2num(split_size[1])
// Window is minimized, we can't get proper data so return to avoid division by 0
@@ -394,12 +394,12 @@ ADMIN_VERB(reset_ooc_color, R_FUN, "Reset Player OOC Color", "Returns player OOC
// Calculate and apply a best estimate
// +4 pixels are for the width of the splitter's handle
var/pct = 100 * (desired_width + 4) / split_width
- winset(src, "mainwindow.split", "splitter=[pct]")
+ winset(src, SKIN_MAINWINDOW_SPLIT, "splitter=[pct]")
// Apply an ever-lowering offset until we finish or fail
var/delta
for(var/safety in 1 to 10)
- var/after_size = winget(src, "mapwindow", "size")
+ var/after_size = winget(src, SKIN_MAPWINDOW, "size")
map_size = splittext(after_size, "x")
var/got_width = text2num(map_size[1])
@@ -414,7 +414,7 @@ ADMIN_VERB(reset_ooc_color, R_FUN, "Reset Player OOC Color", "Returns player OOC
delta = -delta/2
pct += delta
- winset(src, "mainwindow.split", "splitter=[pct]")
+ winset(src, SKIN_MAINWINDOW_SPLIT, "splitter=[pct]")
/// Attempt to automatically fit the viewport, assuming the user wants it
/client/proc/attempt_auto_fit_viewport()
diff --git a/code/modules/client/verbs/ping.dm b/code/modules/client/verbs/ping.dm
deleted file mode 100644
index 03603cea8952..000000000000
--- a/code/modules/client/verbs/ping.dm
+++ /dev/null
@@ -1,22 +0,0 @@
-/client/verb/update_ping(time as num)
- set instant = TRUE
- set name = ".update_ping"
- var/ping = pingfromtime(time)
- lastping = ping
- if (!avgping)
- avgping = ping
- else
- avgping = MC_AVERAGE_SLOW(avgping, ping)
-
-/client/proc/pingfromtime(time)
- return ((world.time+world.tick_lag*TICK_USAGE_REAL/100)-time)*100
-
-/client/verb/display_ping(time as num)
- set instant = TRUE
- set name = ".display_ping"
- to_chat(src, span_notice("Round trip ping took [round(pingfromtime(time),1)]ms"))
-
-/client/verb/ping()
- set name = "Ping"
- set category = "OOC"
- winset(src, null, "command=.display_ping+[world.time+world.tick_lag*TICK_USAGE_REAL/100]")
diff --git a/code/modules/client/verbs/stat_panel.dm b/code/modules/client/verbs/stat_panel.dm
new file mode 100644
index 000000000000..b350c4ee8879
--- /dev/null
+++ b/code/modules/client/verbs/stat_panel.dm
@@ -0,0 +1,21 @@
+/client/verb/toggle_stat_panel()
+ set name = "Toggle Stat Panel"
+ set hidden = TRUE
+
+ //Flip it
+ prefs.write_preference(GLOB.preference_entries[/datum/preference/toggle/statpanel], !prefs.read_preference(/datum/preference/toggle/statpanel))
+ set_stat_panel()
+
+///Sets the stat panel's visibility to the player, depending on whether they need it/have it enabled or not.
+/client/proc/set_stat_panel()
+ if(prefs.read_preference(/datum/preference/toggle/statpanel) || needs_stat_panel())
+ winset(src, SKIN_INFOWINDOW_CHILD, "left=statwindow")
+ else
+ winset(src, SKIN_INFOWINDOW_CHILD, "left=null")
+
+///Returns TRUE if the player has something that necessitates the stat panel.
+/client/proc/needs_stat_panel()
+ if(holder || interviewee)
+ return TRUE
+ . = mob.get_status_tab_items()
+ return length(.) >= 2
diff --git a/code/modules/clothing/head/hat.dm b/code/modules/clothing/head/hat.dm
index 5449496e467e..84c4238f867d 100644
--- a/code/modules/clothing/head/hat.dm
+++ b/code/modules/clothing/head/hat.dm
@@ -227,7 +227,13 @@
/obj/item/clothing/head/costume/jesteralt
name = "jester hat"
desc = "A hat with bells, to add some merriness to the suit."
- icon_state = "jester2"
+ icon = 'icons/map_icons/clothing/head/_head.dmi'
+ icon_state = "/obj/item/clothing/head/costume/jesteralt"
+ post_init_icon_state = "jester_alt"
+ greyscale_config = /datum/greyscale_config/jester_hat_alt
+ greyscale_config_worn = /datum/greyscale_config/jester_hat_alt/worn
+ greyscale_colors = "#E10000#E1E100"
+ flags_1 = IS_PLAYER_COLORABLE_1
/obj/item/clothing/head/costume/rice_hat
name = "rice hat"
diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm
index fc65e2f2e63c..0f1324aba924 100644
--- a/code/modules/clothing/head/jobs.dm
+++ b/code/modules/clothing/head/jobs.dm
@@ -124,6 +124,12 @@
icon_state = "capcap"
dog_fashion = null
+/obj/item/clothing/head/hats/caphat/bicorne
+ name = "captain's bicorne"
+ desc = "Why be king when you can be Emperor?"
+ icon_state = "capbicorne"
+ dog_fashion = null
+
/obj/item/clothing/head/caphat/beret
name = "captain's beret"
desc = "For the Captains known for their sense of fashion."
diff --git a/code/modules/clothing/outfits/standard.dm b/code/modules/clothing/outfits/standard.dm
index 9dcb90b0e3d1..ba6fae4d6aa6 100644
--- a/code/modules/clothing/outfits/standard.dm
+++ b/code/modules/clothing/outfits/standard.dm
@@ -421,7 +421,7 @@
back = /obj/item/mod/control/pre_equipped/debug
backpack_contents = list(
/obj/item/melee/energy/axe = 1,
- /obj/item/storage/part_replacer/bluespace/tier4 = 1,
+ /obj/item/storage/part_replacer/bluespace/AdminDebug = 1,
/obj/item/gun/magic/wand/resurrection/debug = 1,
/obj/item/gun/magic/wand/death/debug = 1,
/obj/item/debug/human_spawner = 1,
@@ -452,7 +452,7 @@
back = /obj/item/mod/control/pre_equipped/administrative
backpack_contents = list(
/obj/item/melee/energy/axe = 1,
- /obj/item/storage/part_replacer/bluespace/tier4 = 1,
+ /obj/item/storage/part_replacer/bluespace/AdminDebug = 1,
/obj/item/gun/magic/wand/resurrection/debug = 1,
/obj/item/gun/magic/wand/death/debug = 1,
/obj/item/debug/human_spawner = 1,
diff --git a/code/modules/clothing/shoes/clown.dm b/code/modules/clothing/shoes/clown.dm
index 29e635f695b9..cf3317db2bd5 100644
--- a/code/modules/clothing/shoes/clown.dm
+++ b/code/modules/clothing/shoes/clown.dm
@@ -49,7 +49,13 @@
/obj/item/clothing/shoes/clown_shoes/jester
name = "jester shoes"
desc = "A court jester's shoes, updated with modern squeaking technology."
- icon_state = "jester_shoes"
+ icon = 'icons/map_icons/clothing/shoes.dmi'
+ icon_state = "/obj/item/clothing/shoes/clown_shoes/jester"
+ post_init_icon_state = "jester_map"
+ greyscale_config = /datum/greyscale_config/jester_shoes
+ greyscale_config_worn = /datum/greyscale_config/jester_shoes/worn
+ greyscale_colors = "#E10000#E1E100"
+ flags_1 = IS_PLAYER_COLORABLE_1
/obj/item/clothing/shoes/clown_shoes/meown_shoes
name = "meown shoes"
diff --git a/code/modules/clothing/suits/reactive_armour.dm b/code/modules/clothing/suits/reactive_armour.dm
index 2f5249158204..a38ffbadce84 100644
--- a/code/modules/clothing/suits/reactive_armour.dm
+++ b/code/modules/clothing/suits/reactive_armour.dm
@@ -534,7 +534,7 @@
shock_turf_windup(owner.loc)
/obj/item/clothing/suit/armor/reactive/weather/proc/shock_turf_windup(turf/target)
- new /obj/effect/temp_visual/telegraphing/thunderbolt(target)
+ new /obj/effect/temp_visual/telegraphing/circle(target)
addtimer(CALLBACK(src, PROC_REF(shock_turf), target), 1 SECONDS)
/obj/item/clothing/suit/armor/reactive/weather/proc/shock_turf(turf/target)
diff --git a/code/modules/clothing/under/jobs/civilian/clown_mime.dm b/code/modules/clothing/under/jobs/civilian/clown_mime.dm
index a452c6c98e04..1ed0b8608cf8 100644
--- a/code/modules/clothing/under/jobs/civilian/clown_mime.dm
+++ b/code/modules/clothing/under/jobs/civilian/clown_mime.dm
@@ -88,7 +88,13 @@
/obj/item/clothing/under/rank/civilian/clown/jesteralt
name = "jester suit"
desc = "A jolly dress, well suited to entertain your master, nuncle."
- icon_state = "jester2"
+ icon = 'icons/map_icons/clothing/under/_under.dmi'
+ icon_state = "/obj/item/clothing/under/rank/civilian/clown/jesteralt"
+ post_init_icon_state = "jester_alt"
+ greyscale_config = /datum/greyscale_config/jester_suit_alt
+ greyscale_config_worn = /datum/greyscale_config/jester_suit_alt/worn
+ greyscale_colors = "#E10000#E1E100"
+ flags_1 = IS_PLAYER_COLORABLE_1
/obj/item/clothing/under/rank/civilian/clown/sexy
name = "sexy-clown suit"
diff --git a/code/modules/clothing/under/jobs/command.dm b/code/modules/clothing/under/jobs/command.dm
index 3016eb9fa2c7..0df4a925fe11 100644
--- a/code/modules/clothing/under/jobs/command.dm
+++ b/code/modules/clothing/under/jobs/command.dm
@@ -28,3 +28,8 @@
icon_state = "captain_parade"
inhand_icon_state = null
can_adjust = FALSE
+
+/obj/item/clothing/under/rank/captain/royal
+ name = "captain's royal uniform"
+ desc = "A captain's royal uniform. Fit for commanding your Officers in the midst of a nuclear incursion."
+ icon_state = "caproyal"
diff --git a/code/modules/deathmatch/deathmatch_loadouts.dm b/code/modules/deathmatch/deathmatch_loadouts.dm
index 5601623e94e1..92ac46acfa6c 100644
--- a/code/modules/deathmatch/deathmatch_loadouts.dm
+++ b/code/modules/deathmatch/deathmatch_loadouts.dm
@@ -1144,3 +1144,107 @@
shoes = /obj/item/clothing/shoes/bronze
l_pocket = /obj/item/reagent_containers/cup/beaker/synthflesh/named // they used to turn their dmg into tox with a spell. close enough
r_pocket = /obj/item/reagent_containers/cup/beaker/synthflesh/named
+
+//syndicate spaceman
+
+/datum/outfit/deathmatch_loadout/syndicate_spaceman
+ name = "DM: Syndicate Spaceman"
+ display_name = "Syndicate Spaceman"
+ desc = "A syndicate operative suited up for some space reconnaissance."
+
+ uniform = /obj/item/clothing/under/syndicate
+ belt = /obj/item/storage/belt/holster
+ r_pocket = /obj/item/tank/internals/emergency_oxygen/double
+ l_pocket = /obj/item/knife/combat/survival
+ internals_slot = ITEM_SLOT_RPOCKET
+ shoes = /obj/item/clothing/shoes/combat
+ gloves = /obj/item/clothing/gloves/combat
+ back = /obj/item/tank/jetpack/harness
+ id = /obj/item/card/id/advanced/black/syndicate_command/crew_id
+
+/datum/outfit/deathmatch_loadout/syndicate_spaceman/pre_equip(mob/living/carbon/human/user, visualsOnly = FALSE)
+ if(user.jumpsuit_style == PREF_SKIRT)
+ uniform = /obj/item/clothing/under/syndicate/skirt
+ // pick a random syndicate spess suit
+ suit = pick(GLOB.syndicate_space_suits_to_helmets)
+ head = GLOB.syndicate_space_suits_to_helmets[suit]
+
+/datum/outfit/deathmatch_loadout/syndicate_spaceman/post_equip(mob/living/carbon/human/syndicate_spaceman, visuals_only)
+ . = ..()
+ var/obj/item/card/id/id_card = syndicate_spaceman.get_item_by_slot(ITEM_SLOT_ID)
+ var/obj/item/storage/belt/belt = syndicate_spaceman.get_item_by_slot(ITEM_SLOT_BELT)
+ if(belt)
+ new /obj/item/gun/ballistic/automatic/pistol/m1911(belt)
+ if(id_card)
+ SSid_access.apply_trim_to_card(id_card, /datum/id_trim/syndicom/crew)
+ id_card.registered_name = syndicate_spaceman.real_name
+ id_card.update_label()
+ id_card.update_appearance()
+
+//cargo spaceman
+
+/datum/outfit/deathmatch_loadout/cargo_spaceman
+ name = "DM: Spaceman"
+ display_name = "Spaceman"
+ desc = "A spaceman from spacestation 13 equipped for space."
+
+ uniform = /obj/item/clothing/under/rank/cargo/tech
+ belt = /obj/item/storage/belt/utility/full
+ suit = /obj/item/clothing/suit/space
+ head = /obj/item/clothing/head/helmet/space
+ internals_slot = ITEM_SLOT_SUITSTORE
+ r_pocket = /obj/item/ammo_casing/strilka310
+ suit_store = /obj/item/tank/internals/oxygen/yellow
+ shoes = /obj/item/clothing/shoes/sneakers
+ gloves = /obj/item/clothing/gloves/fingerless
+ back = /obj/item/gun/ballistic/rifle/boltaction
+ id = /obj/item/card/id/advanced
+
+/datum/outfit/deathmatch_loadout/cargo_spaceman/pre_equip(mob/living/carbon/human/cargo_spaceman, visualsOnly = FALSE)
+ if(cargo_spaceman.jumpsuit_style == PREF_SKIRT)
+ uniform = /obj/item/clothing/under/rank/cargo/tech/skirt
+
+/datum/outfit/deathmatch_loadout/cargo_spaceman/post_equip(mob/living/carbon/human/cargo_spaceman, visuals_only)
+ . = ..()
+ var/obj/item/card/id/id_card = cargo_spaceman.get_item_by_slot(ITEM_SLOT_ID)
+ if(id_card)
+ SSid_access.apply_trim_to_card(id_card, /datum/id_trim/job/cargo_technician)
+ id_card.registered_name = cargo_spaceman.real_name
+ id_card.update_label()
+ id_card.update_appearance()
+
+//spacetider
+
+/datum/outfit/deathmatch_loadout/spacetider
+ name = "DM: Assistant (Spaceworthy)"
+ display_name = "Assistant (Spaceworthy)"
+ desc = "A spacetiding assistant."
+
+ uniform = /obj/item/clothing/under/color/grey
+ mask = /obj/item/clothing/mask/breath
+ belt = /obj/item/gun/energy/disabler/smoothbore
+ suit = /obj/item/clothing/suit/utility/fire/firefighter
+ head = /obj/item/clothing/head/utility/hardhat/red
+ r_pocket = /obj/item/reagent_containers/cup/glass/coffee
+ l_pocket = /obj/item/knife
+ internals_slot = ITEM_SLOT_SUITSTORE
+ suit_store = /obj/item/tank/internals/oxygen/red
+ shoes = /obj/item/clothing/shoes/sneakers
+ gloves = /obj/item/clothing/gloves/color/grey/protects_cold
+ back = /obj/item/gun/energy/laser/musket
+ id = /obj/item/card/id/advanced
+
+/datum/outfit/deathmatch_loadout/spacetider/pre_equip(mob/living/carbon/human/spacetider, visualsOnly = FALSE)
+ if(spacetider.jumpsuit_style == PREF_SKIRT)
+ uniform = /obj/item/clothing/under/color/jumpskirt/grey
+
+/datum/outfit/deathmatch_loadout/spacetider/post_equip(mob/living/carbon/human/spacetider, visuals_only)
+ . = ..()
+ spacetider.reagents.add_reagent(/datum/reagent/consumable/coffee, 30) //pre prime the coffee
+ var/obj/item/card/id/id_card = spacetider.get_item_by_slot(ITEM_SLOT_ID)
+ if(!id_card)
+ return
+ SSid_access.apply_trim_to_card(id_card, /datum/id_trim/job/assistant)
+ id_card.registered_name = spacetider.real_name
+ id_card.update_label()
+ id_card.update_appearance()
diff --git a/code/modules/deathmatch/deathmatch_maps.dm b/code/modules/deathmatch/deathmatch_maps.dm
index f2822cf331af..ff0372c6c63b 100644
--- a/code/modules/deathmatch/deathmatch_maps.dm
+++ b/code/modules/deathmatch/deathmatch_maps.dm
@@ -246,3 +246,12 @@
/datum/turf_reservation/indestructible_plating
turf_type = /turf/open/indestructible/plating //a little hacky but i guess it has to be done
+
+/datum/lazy_template/deathmatch/deep_space
+ name = "Deep Space"
+ desc = "A deep-space cargo shipping station has fallen under attack by a Syndicate boarding party."
+ max_players = 8
+ automatic_gameend_time = 15 MINUTES //its a pretty big map
+ allowed_loadouts = list(/datum/outfit/deathmatch_loadout/cargo_spaceman, /datum/outfit/deathmatch_loadout/syndicate_spaceman, /datum/outfit/deathmatch_loadout/spacetider)
+ map_name = "deep_space"
+ key = "deep_space"
diff --git a/code/modules/economy/account.dm b/code/modules/economy/account.dm
index 80a97caa1d69..6b34477b4e38 100644
--- a/code/modules/economy/account.dm
+++ b/code/modules/economy/account.dm
@@ -25,8 +25,8 @@
var/account_id
///Amount of money that's been crabbed, if you lose enough from one series of CRAB-17's, you get a negative moodlet.
var/money_crabbed
- ///Is there a CRAB 17 on the station draining funds? Prevents manual fund transfer. pink levels are rising
- var/being_dumped = FALSE
+ ///Lazylist of CRAB 17s on the station draining funds. Prevents manual fund transfer. pink levels are rising
+ var/list/being_dumped
///Reference to the current civilian bounty that the account is working on.
var/datum/bounty/civilian_bounty
///If player is currently picking a civilian bounty to do, these options are held here to prevent soft-resetting through the UI.
@@ -117,15 +117,15 @@
/**
* Sets the bank_account to behave as though a CRAB-17 event is happening.
*/
-/datum/bank_account/proc/dumpeet()
- being_dumped = TRUE
+/datum/bank_account/proc/dumpeet(obj/structure/checkoutmachine/dump_machine)
+ LAZYADD(being_dumped, dump_machine)
money_crabbed = 0
/**
* Stops the dumping of the bank account.
*/
-/datum/bank_account/proc/stop_dump()
- being_dumped = FALSE
+/datum/bank_account/proc/stop_dump(obj/structure/checkoutmachine/dump_machine)
+ LAZYREMOVE(being_dumped, dump_machine)
if(money_crabbed < NO_MY_MONEY)
return
for(var/obj/card as anything in bank_cards)
diff --git a/code/modules/emote_panel/emote_panel.dm b/code/modules/emote_panel/emote_panel.dm
index d064161c2324..c39631fbe3ce 100644
--- a/code/modules/emote_panel/emote_panel.dm
+++ b/code/modules/emote_panel/emote_panel.dm
@@ -53,12 +53,3 @@
/datum/emote_panel/ui_state(mob/user)
return GLOB.always_state
-
-/mob/living/verb/emote_panel()
- set name = "Emote Panel"
- set category = "IC"
-
- var/static/datum/emote_panel/emote_panel
- if(isnull(emote_panel))
- emote_panel = new
- emote_panel.ui_interact(src)
diff --git a/code/modules/events/grid_check.dm b/code/modules/events/grid_check.dm
index 8fd3b86f14bd..d68cdb72f145 100644
--- a/code/modules/events/grid_check.dm
+++ b/code/modules/events/grid_check.dm
@@ -27,4 +27,4 @@
COOLDOWN_START(controller, announcement_spam_protection, 30 SECONDS)
/datum/round_event/grid_check/start()
- power_fail(30, 120)
+ power_fail(60, 240) // 1 to 4 minutes
diff --git a/code/modules/events/heart_attack.dm b/code/modules/events/heart_attack.dm
index e3c6ca2b91b8..d8a1ea54f5d0 100644
--- a/code/modules/events/heart_attack.dm
+++ b/code/modules/events/heart_attack.dm
@@ -64,7 +64,10 @@
*/
/datum/round_event/heart_attack/proc/attack_heart()
var/mob/living/carbon/human/winner = pick_weight(victims)
- if(winner.has_status_effect(/datum/status_effect/exercised)) //Stuff that should "block" a heart attack rather than just deny eligibility for one goes here.
+ // Our fitness level can potentially block a heart attack outright.
+ var/fitness_protection_probability = winner.mind?.get_skill_modifier(/datum/skill/athletics, SKILL_RANDS_MODIFIER)
+
+ if(prob(fitness_protection_probability * 2)) //Stuff that should "block" a heart attack rather than just deny eligibility for one goes here.
winner.visible_message(span_warning("[winner] grunts and clutches their chest for a moment, catching [winner.p_their()] breath."), span_medal("Your chest lurches in pain for a brief moment, which quickly fades. \
You feel like you've just avoided a serious health disaster."), span_hear("You hear someone's breathing sharpen for a moment, followed by a sigh of relief."), 4)
winner.playsound_local(get_turf(winner), 'sound/effects/health/slowbeat.ogg', 40, 0, channel = CHANNEL_HEARTBEAT, use_reverb = FALSE)
diff --git a/code/modules/events/immovable_rod/immovable_rod.dm b/code/modules/events/immovable_rod/immovable_rod.dm
index e2404bbc0906..9306fa7939a9 100644
--- a/code/modules/events/immovable_rod/immovable_rod.dm
+++ b/code/modules/events/immovable_rod/immovable_rod.dm
@@ -245,6 +245,7 @@
span_boldwarning("[strongman] suplexes [src] into the ground!"),
span_warning("As you suplex [src] into the ground, your body ripples with power!")
)
+ sound_to_playing_players('sound/items/handling/lead_pipe/lead_pipe_drop.ogg')
new /obj/structure/festivus/anchored(drop_location())
new /obj/effect/anomaly/flux(drop_location())
diff --git a/code/modules/events/space_vines/vine_mutations.dm b/code/modules/events/space_vines/vine_mutations.dm
index a0308fb317da..542e8f092c94 100644
--- a/code/modules/events/space_vines/vine_mutations.dm
+++ b/code/modules/events/space_vines/vine_mutations.dm
@@ -268,6 +268,7 @@
/datum/spacevine_mutation/transparency/on_birth(obj/structure/spacevine/holder)
holder.light_state = PASS_LIGHT
holder.alpha = 125
+ holder.unblock_sunlight()
/datum/spacevine_mutation/gas_eater
abstract_type = /datum/spacevine_mutation/gas_eater
diff --git a/code/modules/events/space_vines/vine_structure.dm b/code/modules/events/space_vines/vine_structure.dm
index 50ce10154a17..a2abaee3394c 100644
--- a/code/modules/events/space_vines/vine_structure.dm
+++ b/code/modules/events/space_vines/vine_structure.dm
@@ -38,6 +38,7 @@
AddElement(/datum/element/connect_loc, loc_connections)
AddElement(/datum/element/atmos_sensitive, mapload)
AddComponent(/datum/component/storm_hating)
+ block_sunlight()
/obj/structure/spacevine/examine(mob/user)
. = ..()
@@ -211,3 +212,9 @@
. = ..()
if(isvineimmune(mover))
return TRUE
+
+/obj/structure/spacevine/proc/block_sunlight()
+ AddElement(/datum/element/give_turf_traits, string_list(list(TRAIT_TURF_SUN_BLOCKED)))
+
+/obj/structure/spacevine/proc/unblock_sunlight()
+ RemoveElement(/datum/element/give_turf_traits, string_list(list(TRAIT_TURF_SUN_BLOCKED)))
diff --git a/code/modules/events/wizard/magical_rain.dm b/code/modules/events/wizard/magical_rain.dm
index 203a960d36e6..0885c9830648 100644
--- a/code/modules/events/wizard/magical_rain.dm
+++ b/code/modules/events/wizard/magical_rain.dm
@@ -21,7 +21,7 @@
if(!started)
started = TRUE
- SSweather.run_weather(/datum/weather/rain_storm/wizard)
+ SSweather.run_weather(/datum/weather/particle/rain_storm/wizard)
/datum/round_event/wizard/magical_rain/end()
for(var/mob/living/wizard in GLOB.alive_mob_list)
diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm
index 9052a8e39835..ec6ba632f442 100644
--- a/code/modules/fishing/fish/_fish.dm
+++ b/code/modules/fishing/fish/_fish.dm
@@ -275,7 +275,11 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
user.set_combat_mode(TRUE)
ADD_TRAIT(user, TRAIT_COMBAT_MODE_LOCK, REF(src))
slapperoni(user, iteration = 1)
- return MANUAL_SUICIDE
+ REMOVE_TRAIT(user, TRAIT_COMBAT_MODE_LOCK, REF(src))
+ if (user.stat == DEAD)
+ return MANUAL_SUICIDE
+ user.visible_message(span_suicide("[user] slaps [user.p_them()]self with [src], but fails to go through with it!"))
+ return SHAME
/obj/item/fish/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
if(attack_type == OVERWHELMING_ATTACK)
@@ -816,14 +820,14 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
/obj/item/fish/apply_material_effects()
. = ..()
//Either effects aren't applied of he materials are simply being increased/decreased along with the weight. Avoids recursion.
- if(!(material_flags & MATERIAL_EFFECTS) || (fish_flags & FISH_FLAG_UPDATING_SIZE_AND_WEIGHT) || material_weight_mult == 1)
+ if(!(material_flags & MATERIAL_EFFECTS) || (fish_flags & FISH_FLAG_UPDATING_SIZE_AND_WEIGHT) )
return
maximum_weight *= material_weight_mult
update_size_and_weight(size, (temp_weight || weight) * material_weight_mult, update_materials = FALSE)
/obj/item/fish/remove_material_effects(replace_mats = TRUE)
. = ..()
- if(replace_mats || !(material_flags & MATERIAL_EFFECTS) || material_weight_mult == 1)
+ if(replace_mats || !(material_flags & MATERIAL_EFFECTS) )
return
maximum_weight /= material_weight_mult
update_size_and_weight(size, weight / material_weight_mult)
diff --git a/code/modules/fishing/fishing_rod.dm b/code/modules/fishing/fishing_rod.dm
index 0ba7596ab554..b2cb3270da90 100644
--- a/code/modules/fishing/fishing_rod.dm
+++ b/code/modules/fishing/fishing_rod.dm
@@ -93,11 +93,12 @@
set_slot(new line(src), ROD_SLOT_LINE)
update_appearance()
-
- //Bane effect that make it extra-effective against mobs with water adaptation (read: fish infusion)
- AddElement(/datum/element/bane, target_type = /mob/living, damage_multiplier = 1.25)
- RegisterSignal(src, COMSIG_OBJECT_PRE_BANING, PROC_REF(attempt_bane))
- RegisterSignal(src, COMSIG_OBJECT_ON_BANING, PROC_REF(bane_effects))
+ AddComponent(/datum/component/bane, \
+ damage_multiplier = 2.25, \
+ should_bane_callback = CALLBACK(src, PROC_REF(should_bane_fish_infusions)), \
+ on_bane_callback = CALLBACK(src, PROC_REF(on_bane_fish_infusions)), \
+ label_text = "fishpeople", \
+ )
/obj/item/fishing_rod/add_context(atom/source, list/context, obj/item/held_item, mob/user)
if(src == held_item)
@@ -261,18 +262,13 @@
material_chance += user.mind?.get_skill_level(/datum/skill/fishing) * 1.5
return material_chance
-///Fishing rodss should only bane fish DNA-infused spessman
-/obj/item/fishing_rod/proc/attempt_bane(datum/source, mob/living/fish)
- SIGNAL_HANDLER
- if(!force || !HAS_TRAIT(fish, TRAIT_WATER_ADAPTATION))
- return COMPONENT_CANCEL_BANING
+/obj/item/fishing_rod/proc/should_bane_fish_infusions(mob/living/target)
+ return force > 0 && HAS_TRAIT(target, TRAIT_WATER_ADAPTATION)
-///Fishing rods should hard-counter fish DNA-infused spessman
-/obj/item/fishing_rod/proc/bane_effects(datum/source, mob/living/fish)
- SIGNAL_HANDLER
- fish.adjust_staggered_up_to(STAGGERED_SLOWDOWN_LENGTH, 4 SECONDS)
- fish.adjust_confusion_up_to(1.5 SECONDS, 3 SECONDS)
- fish.adjust_wet_stacks(-4)
+/obj/item/fishing_rod/proc/on_bane_fish_infusions(mob/living/target, mob/living/attacker)
+ target.adjust_staggered_up_to(STAGGERED_SLOWDOWN_LENGTH, 4 SECONDS)
+ target.adjust_confusion_up_to(1.5 SECONDS, 3 SECONDS)
+ target.adjust_wet_stacks(-4)
/obj/item/fishing_rod/interact(mob/user)
if(currently_hooked)
diff --git a/code/modules/fishing/sources/subtypes/surgery.dm b/code/modules/fishing/sources/subtypes/surgery.dm
index 2192d2ed5c87..de1fc0092e8c 100644
--- a/code/modules/fishing/sources/subtypes/surgery.dm
+++ b/code/modules/fishing/sources/subtypes/surgery.dm
@@ -10,6 +10,7 @@
fishing_difficulty = -10
//The range for waiting is also a bit narrower, so it cannot take as few as 3 seconds or as many as 25 to snatch an organ.
wait_time_range = list(6 SECONDS, 12 SECONDS)
+ fish_source_flags = FISH_SOURCE_FLAG_EXPLOSIVE_NONE
/datum/fish_source/surgery/spawn_reward(reward_path, atom/spawn_location, atom/fishing_spot, obj/item/fishing_rod/used_rod)
if(reward_path != FISHING_RANDOM_ORGAN)
diff --git a/code/modules/flufftext/dreaming.dm b/code/modules/flufftext/dreaming.dm
index 73520a1625c2..a97054affe09 100644
--- a/code/modules/flufftext/dreaming.dm
+++ b/code/modules/flufftext/dreaming.dm
@@ -183,7 +183,7 @@ GLOBAL_LIST_INIT(dreams, populate_dream_list())
addtimer(CALLBACK(src, PROC_REF(StopSound), dreamer), 5 SECONDS)
/datum/dream/hear_something/proc/ReserveSoundChannel()
- reserved_sound_channel = SSsounds.reserve_sound_channel(src)
+ reserved_sound_channel = SSsounds.reserve_sound_channel_for_datum(src)
UnregisterSignal(SSsounds, COMSIG_SUBSYSTEM_POST_INITIALIZE)
/datum/dream/hear_something/proc/PlayRandomSound(mob/living/carbon/dreamer)
diff --git a/code/modules/food_and_drinks/machinery/smartfridge.dm b/code/modules/food_and_drinks/machinery/smartfridge.dm
index 31a6e85075d1..601188a30fa2 100644
--- a/code/modules/food_and_drinks/machinery/smartfridge.dm
+++ b/code/modules/food_and_drinks/machinery/smartfridge.dm
@@ -14,6 +14,7 @@
light_range = MINIMUM_USEFUL_LIGHT_RANGE
integrity_failure = 0.5
can_atmos_pass = ATMOS_PASS_NO
+ pass_flags_self = PASSCLOSEDTURF
/// Icon state part for contents display
var/contents_overlay_icon = "plant"
/// What path boards used to construct it should build into when dropped. Needed so we don't accidentally have them build variants with items preloaded in them.
@@ -131,8 +132,7 @@
. = ..()
if(!anchored && welded_down) //make sure they're keep in sync in case it was forcibly unanchored by badmins or by a megafauna.
welded_down = FALSE
- can_atmos_pass = anchorvalue ? ATMOS_PASS_NO : ATMOS_PASS_YES
- air_update_turf(TRUE, anchorvalue)
+ recheck_atmos_passing()
/obj/machinery/smartfridge/wrench_act(mob/living/user, obj/item/tool)
if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN)
@@ -189,6 +189,14 @@
. += structure_examine()
+/obj/machinery/smartfridge/on_set_machine_stat(old_value)
+ . = ..()
+ recheck_atmos_passing()
+ if(machine_stat & BROKEN)
+ pass_flags_self = PASSMACHINE
+ return
+ pass_flags_self = PASSCLOSEDTURF
+
/// Returns details related to the fridge structure
/obj/machinery/smartfridge/proc/structure_examine()
. = list()
@@ -422,6 +430,13 @@
return FALSE
+/obj/machinery/smartfridge/proc/recheck_atmos_passing()
+ if(machine_stat & BROKEN)
+ can_atmos_pass = ATMOS_PASS_YES
+ else
+ can_atmos_pass = anchored ? ATMOS_PASS_NO : ATMOS_PASS_YES
+ air_update_turf(TRUE, anchored)
+
// ----------------------------
// Drying 'smartfridge'
// ----------------------------
@@ -506,7 +521,7 @@
use_energy(active_power_usage)
/obj/machinery/smartfridge/drying/accept_check(obj/item/O)
- return HAS_TRAIT(O, TRAIT_DRYABLE)
+ return HAS_TRAIT(O, TRAIT_DRYABLE) && !HAS_TRAIT(O, TRAIT_DRIED)
/**
* Toggles drying on or off
diff --git a/code/modules/food_and_drinks/recipes/soup_mixtures.dm b/code/modules/food_and_drinks/recipes/soup_mixtures.dm
index eef072eb1929..ab9a2175a89a 100644
--- a/code/modules/food_and_drinks/recipes/soup_mixtures.dm
+++ b/code/modules/food_and_drinks/recipes/soup_mixtures.dm
@@ -66,7 +66,7 @@
//number of ingredients who's requested amounts has been satisfied
var/completed_ingredients = 0
for(var/obj/item/ingredient as anything in pot.added_ingredients)
- var/ingredient_type = ingredient.type
+ var/datum/ingredient_type = ingredient.type
do
{
var/ingredient_count = reqs_copy[ingredient_type]
@@ -91,7 +91,7 @@
//means we have to look for subtypes
else if(isnull(ingredient_count))
- ingredient_type = type2parent(ingredient_type)
+ ingredient_type = ingredient_type::parent_type
//means we have no more remaining ingredients so bail, can happen if multiple ingredients of the same type/subtype are in the pot
else
diff --git a/code/modules/food_and_drinks/recipes/tablecraft/recipes_pastry.dm b/code/modules/food_and_drinks/recipes/tablecraft/recipes_pastry.dm
index ced52c61149c..20c00e0d2eb0 100644
--- a/code/modules/food_and_drinks/recipes/tablecraft/recipes_pastry.dm
+++ b/code/modules/food_and_drinks/recipes/tablecraft/recipes_pastry.dm
@@ -764,3 +764,14 @@
)
result = /obj/item/food/cookie/macaron
dish_category = DISH_COOKIE
+
+/datum/crafting_recipe/food/apple_fritter
+ name = "Apple fritter"
+ reqs = list(
+ /obj/item/food/pastrybase = 1,
+ /obj/item/food/appleslice = 1,
+ )
+ result = /obj/item/food/apple_fritter
+ added_foodtypes = GRAIN|FRUIT|FRIED|BREAKFAST
+ dish_category = DISH_PASTRY
+ meal_category = MEAL_BREAKFAST
diff --git a/code/modules/hallucination/bubblegum_attack.dm b/code/modules/hallucination/bubblegum_attack.dm
index 64a4f0e2036b..b1f735798e44 100644
--- a/code/modules/hallucination/bubblegum_attack.dm
+++ b/code/modules/hallucination/bubblegum_attack.dm
@@ -49,7 +49,7 @@
hallucinator.playsound_local(wall_source, 'sound/effects/meteorimpact.ogg', 150, TRUE)
if(haunt_them)
- to_chat(hallucinator, pick(hallucination_lines))
+ to_chat(hallucinator, span_colossus(pick(hallucination_lines)))
var/obj/effect/client_image_holder/hallucination/bubblegum/fake_bubbles = new(wall_source, hallucinator, src)
addtimer(CALLBACK(src, PROC_REF(charge_loop), fake_bubbles, target_landing_turf), 1 SECONDS)
diff --git a/code/modules/hallucination/mother.dm b/code/modules/hallucination/mother.dm
index 77ed2cfb6390..2d9a7ad6b593 100644
--- a/code/modules/hallucination/mother.dm
+++ b/code/modules/hallucination/mother.dm
@@ -81,11 +81,16 @@
/obj/effect/client_image_holder/hallucination/your_mother/Initialize(mapload, list/mobs_which_see_us, datum/hallucination/parent)
var/mob/living/hallucinator = parent.hallucinator
- if (ishuman(hallucinator))
+ if (ishuman(hallucinator) && !isplasmaman(hallucinator)) //Plasmapeople don't have parents in a traditional sense, so their mother is different.
var/mob/living/carbon/dna_haver = hallucinator
image_icon = image(get_dynamic_human_appearance(/datum/outfit/yourmother, dna_haver.dna.species.type))
return ..()
+ if (isplasmaman(hallucinator))
+ image_icon = 'icons/turf/floors.dmi'
+ image_state = "liquidplasma"
+ return ..()
+
if (istype(hallucinator, /mob/living/basic/pet/dog/corgi/ian))
image_icon = getFlatIcon(get_dynamic_human_appearance(/datum/outfit/job/hop))
name = "Head of Personnel"
diff --git a/code/modules/holiday/holidays.dm b/code/modules/holiday/holidays.dm
index 26438316b250..4e728969aec0 100644
--- a/code/modules/holiday/holidays.dm
+++ b/code/modules/holiday/holidays.dm
@@ -300,10 +300,6 @@ GLOBAL_LIST_INIT(holiday_mail, list())
. = ..()
SSjob.set_overflow_role(/datum/job/clown)
SSticker.set_lobby_music('sound/music/lobby_music/clown.ogg', override = TRUE)
- for(var/i in GLOB.new_player_list)
- var/mob/dead/new_player/P = i
- if(P.client)
- P.client.playtitlemusic()
/datum/holiday/april_fools/get_holiday_colors(atom/thing_to_color)
return "#[random_short_color()]"
diff --git a/code/modules/holodeck/holo_effect.dm b/code/modules/holodeck/holo_effect.dm
index 55535d4b5707..7a035b94a6d2 100644
--- a/code/modules/holodeck/holo_effect.dm
+++ b/code/modules/holodeck/holo_effect.dm
@@ -55,18 +55,42 @@
var/mob/our_mob = null
/obj/effect/holodeck_effect/mobspawner/activate(obj/machinery/computer/holodeck/HC)
+ . = list()
+
if(islist(mobtype))
mobtype = pick(mobtype)
our_mob = new mobtype(loc)
our_mob.flags_1 |= HOLOGRAM_1
+ . += our_mob
+
+ for(var/atom/holo_atom as anything in our_mob.contents)
+ holo_atom.flags_1 |= HOLOGRAM_1
+ . += holo_atom
+
+ if(iscarbon(our_mob))
+ var/mob/living/carbon/holo_carbon_mob = our_mob
+ for(var/obj/item/organ/holo_organ as anything in holo_carbon_mob.organs)
+ holo_organ.flags_1 |= HOLOGRAM_1
+ . += holo_organ
// these vars are not really standardized but all would theoretically create stuff on death
our_mob.add_traits(list(TRAIT_PERMANENTLY_MORTAL, TRAIT_NO_BLOOD_OVERLAY, TRAIT_NOBLOOD, TRAIT_NOHUNGER, TRAIT_SPAWNED_MOB), INNATE_TRAIT)
RegisterSignal(our_mob, COMSIG_QDELETING, PROC_REF(handle_mob_delete))
- return our_mob
+
+ return .
/obj/effect/holodeck_effect/mobspawner/deactivate(obj/machinery/computer/holodeck/HC)
if(our_mob)
+ for(var/atom/holo_atom as anything in our_mob.contents)
+ if(holo_atom.flags_1 & HOLOGRAM_1)
+ HC.derez(holo_atom)
+
+ if(iscarbon(our_mob))
+ var/mob/living/carbon/holo_carbon_mob = our_mob
+ for(var/obj/item/organ/holo_organ as anything in holo_carbon_mob.organs)
+ if(holo_organ.flags_1 & HOLOGRAM_1)
+ HC.derez(holo_organ)
+
HC.derez(our_mob)
qdel(src)
@@ -100,13 +124,6 @@
/obj/effect/holodeck_effect/mobspawner/monkey
mobtype = /mob/living/carbon/human/species/monkey
-/obj/effect/holodeck_effect/mobspawner/monkey/activate(obj/machinery/computer/holodeck/computer)
- var/mob/living/carbon/human/monkey = ..()
- . = list() + monkey
-
- for(var/atom/atom as anything in monkey.contents + monkey.organs)
- . += atom
-
/obj/effect/holodeck_effect/mobspawner/penguin
mobtype = /mob/living/basic/pet/penguin/emperor/neuter
diff --git a/code/modules/holodeck/holodeck_map_templates.dm b/code/modules/holodeck/holodeck_map_templates.dm
index 3e79de6ea43e..06cb3004ca15 100644
--- a/code/modules/holodeck/holodeck_map_templates.dm
+++ b/code/modules/holodeck/holodeck_map_templates.dm
@@ -157,3 +157,9 @@
template_id = "holodeck_refuelingstation"
mappath = "_maps/templates/holodeck_refuelingstation.dmm"
restricted = TRUE
+
+/datum/map_template/holodeck/prison
+ name = "Holodeck - Prison Block"
+ template_id = "holodeck_prison"
+ mappath = "_maps/templates/holodeck_prison.dmm"
+ restricted = TRUE
diff --git a/code/modules/hydroponics/hydroitemdefines.dm b/code/modules/hydroponics/hydroitemdefines.dm
index 511002675725..a35b938fcf66 100644
--- a/code/modules/hydroponics/hydroitemdefines.dm
+++ b/code/modules/hydroponics/hydroitemdefines.dm
@@ -172,11 +172,8 @@
/obj/item/scythe/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/butchering, \
- speed = 9 SECONDS, \
- effectiveness = 105, \
- )
- AddElement(/datum/element/bane, mob_biotypes = MOB_PLANT, damage_multiplier = 0.5, requires_combat_mode = FALSE)
+ AddComponent(/datum/component/butchering, speed = 9 SECONDS, effectiveness = 105)
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5)
/obj/item/scythe/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] is beheading [user.p_them()]self with [src]! It looks like [user.p_theyre()] trying to commit suicide!"))
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index d3d672174b26..34368182823d 100644
--- a/code/modules/hydroponics/hydroponics.dm
+++ b/code/modules/hydroponics/hydroponics.dm
@@ -1,4 +1,3 @@
-
/obj/machinery/hydroponics
name = "hydroponics tray"
desc = "A basin used to grow plants in."
@@ -65,7 +64,11 @@
var/tray_flags = HYDROPONIC
///How many extra px to offset the plant sprite on the y axis, gets passed to the seed and added to the seeds offset
var/plant_offset_y = 0
-
+ ///Suffix things
+ var/alt_tray = FALSE
+ ///Soil things
+ var/obj/machinery/hydroponics/soil/current_soil
+ var/mutating_tray = FALSE // DARKPACK EDIT ADD
/obj/machinery/hydroponics/Initialize(mapload)
//ALRIGHT YOU DEGENERATES. YOU HAD REAGENT HOLDERS FOR AT LEAST 4 YEARS AND NONE OF YOU MADE HYDROPONICS TRAYS HOLD NUTRIENT CHEMS INSTEAD OF USING "Points".
@@ -163,15 +166,27 @@
return NONE
/obj/machinery/hydroponics/constructable
- name = "hydroponics tray"
icon = 'icons/obj/service/hydroponics/equipment.dmi'
icon_state = "hydrotray3"
+/obj/machinery/hydroponics/constructable/oldstyle
+ icon = 'icons/obj/service/hydroponics/equipment.dmi'
+ icon_state = "hydrotray3-alt"
+ alt_tray = TRUE
+
/obj/machinery/hydroponics/constructable/fullupgrade
name = "deluxe hydroponics tray"
desc = "A basin used to grown plants in, packed full of cutting-edge technology."
circuit = /obj/item/circuitboard/machine/hydroponics/fullupgrade
+// DARKPACK EDIT ADD START
+/obj/machinery/hydroponics/constructable/tainted
+ name = "strange hydroponics tray"
+ desc = "A strange modified basic used to grow plants. It has many nozzles and tanks full of bubbling liquid you cant understand."
+ circuit = /obj/item/circuitboard/machine/hydroponics/tainted
+ mutating_tray = TRUE
+// DARKPACK EDIT ADD END
+
/obj/machinery/hydroponics/constructable/Initialize(mapload)
. = ..()
AddElement(/datum/element/simple_rotation)
@@ -186,8 +201,12 @@
tmp_capacity += matter_bin.tier
for (var/datum/stock_part/servo/servo in component_parts)
rating = servo.tier
- maxwater = tmp_capacity * 50 // Up to 300
- maxnutri = (tmp_capacity * 5) + STATIC_NUTRIENT_CAPACITY // Up to 50 Maximum
+ if(current_soil)
+ maxwater = current_soil.maxwater + ((tmp_capacity - 2)*50)
+ maxnutri = current_soil.maxnutri + ((tmp_capacity - 2)*5)
+ else
+ maxwater = tmp_capacity * 50
+ maxnutri = (tmp_capacity * 5) + STATIC_NUTRIENT_CAPACITY
reagents.maximum_volume = maxnutri
nutridrain = 1/rating
@@ -228,21 +247,27 @@
if(myseed)
QDEL_NULL(myseed)
remove_shared_particles(/particles/pollen)
+ QDEL_NULL(current_soil)
return ..()
/obj/machinery/hydroponics/Exited(atom/movable/gone)
. = ..()
if(!QDELETED(src) && gone == myseed)
set_seed(null, FALSE)
- if(!istype(gone, /obj/item/mob_holder/snail))
return
- var/obj/item/mob_holder/snail_object = gone
- if(snail_object.held_mob)
- UnregisterSignal(snail_object.held_mob, list(
- COMSIG_LIVING_DEATH,
- COMSIG_MOVABLE_ATTEMPTED_MOVE,
- ))
- QDEL_NULL(our_snail)
+ if(gone == current_soil)
+ current_soil = null
+ if(!QDELETED(src))
+ update_appearance()
+ return
+ if(istype(gone, /obj/item/mob_holder/snail))
+ var/obj/item/mob_holder/snail_object = gone
+ if(snail_object.held_mob)
+ UnregisterSignal(snail_object.held_mob, list(
+ COMSIG_LIVING_DEATH,
+ COMSIG_MOVABLE_ATTEMPTED_MOVE,
+ ))
+ QDEL_NULL(our_snail)
/obj/machinery/hydroponics/constructable/screwdriver_act(mob/living/user, obj/item/tool)
return default_deconstruction_screwdriver(user, tool)
@@ -391,17 +416,22 @@
pollinate()
//This is where stability mutations exist now.
- if(myseed.instability >= 80)
- traitmutate(myseed.instability - 75) //Scaling odds of a random trait or chemical
- if(myseed.instability >= 60)
- if(prob((myseed.instability)/2) && !self_sustaining && LAZYLEN(myseed.mutatelist) && !myseed.get_gene(/datum/plant_gene/trait/never_mutate)) //Minimum 30%, Maximum 50% chance of mutating every age tick when not on autogrow or having Prosophobic Inclination trait.
- mutatespecie()
- myseed.set_instability(myseed.instability/2)
- if(myseed.instability >= 20 && prob(myseed.instability) && !myseed.get_gene(/datum/plant_gene/trait/stable_stats)) //No hardmutation if Symbiotic Resilience trait is present.
- if(myseed.instability >= 40)
- hardmutate(stabmut = myseed.instability >= 80 ? 5 : 0)
- else
- mutate(stabmut = 0)
+ // DARKPACK EDIT CHANGE START - (Locks mutating through process to a specifc type of tray)
+ if(mutating_tray)
+ if(myseed.instability >= 80)
+ traitmutate(myseed.instability - 75) //Scaling odds of a random trait or chemical
+ if(myseed.instability >= 60)
+ if(prob((myseed.instability)/2) && !self_sustaining && LAZYLEN(myseed.mutatelist) && !myseed.get_gene(/datum/plant_gene/trait/never_mutate)) //Minimum 30%, Maximum 50% chance of mutating every age tick when not on autogrow or having Prosophobic Inclination trait.
+ mutatespecie()
+ myseed.set_instability(myseed.instability/2)
+ if(myseed.instability >= 20 && prob(myseed.instability) && !myseed.get_gene(/datum/plant_gene/trait/stable_stats)) //No hardmutation if Symbiotic Resilience trait is present.
+ if(myseed.instability >= 40)
+ hardmutate(stabmut = myseed.instability >= 80 ? 5 : 0)
+ else
+ mutate(stabmut = 0)
+ else if(myseed.instability >= 20 && prob(myseed.instability) && !myseed.get_gene(/datum/plant_gene/trait/stable_stats))
+ mutate(stabmut = 0)
+ // DARKPACK EDIT CHANGE END
//Health & Age///////////////////////////////////////////////////////////
@@ -455,8 +485,19 @@
/obj/machinery/hydroponics/update_name(updates)
. = ..()
- if(!GetComponent(/datum/component/rename) && myseed)
- name = "[initial(name)] ([myseed.plantname])"
+ if(GetComponent(/datum/component/rename))
+ return
+ name = current_soil ? "botanic tray" : initial(name)
+ if(myseed)
+ name += " ([myseed.plantname])"
+
+/obj/machinery/hydroponics/update_desc(updates)
+ . = ..()
+ if(GetComponent(/datum/component/rename))
+ return
+ desc = initial(desc)
+ if(current_soil)
+ desc += " Filled with [current_soil.name]."
/obj/machinery/hydroponics/update_overlays()
. = ..()
@@ -464,21 +505,32 @@
. += myseed.get_tray_overlay(age, plant_status, plant_offset_y)
. += update_status_light_overlays()
+ if(current_soil)
+ var/soil_overlay = "[current_soil.icon_state]_tray"
+ . += mutable_appearance(icon, soil_overlay, OBJ_LAYER + 0.001)
+
if(self_sustaining && self_sustaining_overlay_icon_state)
- . += mutable_appearance(icon, self_sustaining_overlay_icon_state)
+ . += mutable_appearance(icon, self_sustaining_overlay_icon_state, OBJ_LAYER + 0.002)
+ . += emissive_appearance(icon, self_sustaining_overlay_icon_state, src, OBJ_LAYER + 0.002)
/obj/machinery/hydroponics/proc/update_status_light_overlays()
. = list()
+ var/indicatorsuffix = alt_tray ? "-alt" : ""
if(waterlevel <= 10)
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowwater3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowwater3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_lowwater3[indicatorsuffix]", src, alpha = src.alpha)
if(reagents.total_volume <= 2)
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lownutri3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lownutri3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_lownutri3[indicatorsuffix]", src, alpha = src.alpha)
if(plant_health <= (myseed.endurance / 2))
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowhealth3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowhealth3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_lowhealth3[indicatorsuffix]", src, alpha = src.alpha)
if(weedlevel >= 5 || pestlevel >= 5 || toxic >= 40)
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_alert3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_alert3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_alert3[indicatorsuffix]", src, alpha = src.alpha)
if(plant_status == HYDROTRAY_PLANT_HARVESTABLE)
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_harvest3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_harvest3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_harvest3[indicatorsuffix]", src, alpha = src.alpha)
///Sets a new value for the myseed variable, which is the seed of the plant that's growing inside the tray.
/obj/machinery/hydroponics/proc/set_seed(obj/item/seeds/new_seed, delete_old_seed = TRUE)
@@ -795,6 +847,7 @@
set_pestlevel(0, update_icon = FALSE) // Pests die
lastproduce = 0
SEND_SIGNAL(src, COMSIG_HYDROTRAY_PLANT_DEATH)
+ remove_shared_particles(/particles/pollen) //We shouldn't look like we're pollenating if we're dead.
if(update_icon)
update_appearance()
@@ -1063,6 +1116,30 @@
flowergun.update_appearance()
to_chat(user, span_notice("[myseed.plantname]'s mutation was set to [locked_mutation], depleting [flowergun]'s cell!"))
return
+ else if(istype(O, /obj/item/soil_sack))
+ var/obj/item/soil_sack/oursoil = O
+
+ if(plant_status != HYDROTRAY_NO_PLANT)
+ balloon_alert(user, "remove the plants first!")
+ return
+
+ if(!isnull(current_soil))
+ balloon_alert(user, "tray is full!")
+ return
+
+ balloon_alert(user, "filling the tray...")
+ if(!do_after(user, 2 SECONDS, src))
+ return
+
+ oursoil.transfer_soil(src, inside_tray = TRUE)
+
+ RefreshParts()
+ tray_flags = current_soil.tray_flags
+
+ qdel(oursoil)
+ update_appearance()
+ return TRUE
+
else
return ..()
@@ -1169,6 +1246,9 @@
if(myseed)
to_chat(user, span_warning("[src] already has a plant growing in it!"))
return
+ if(young_plant.seed_flags & NO_PLANTING)
+ to_chat(user, span_warning("[young_plant] cannot be planted in [src]!"))
+ return
if(istype(young_plant, /obj/item/seeds/kudzu))
investigate_log("had Kudzu planted in it by [key_name(user)] at [AREACOORD(src)].", INVESTIGATE_BOTANY)
if(!user.transferItemToLoc(young_plant, src))
diff --git a/code/modules/hydroponics/soil.dm b/code/modules/hydroponics/soil.dm
index ef403fe4db15..29f26fdd408c 100644
--- a/code/modules/hydroponics/soil.dm
+++ b/code/modules/hydroponics/soil.dm
@@ -163,6 +163,12 @@
animate(time = 100 MILLISECONDS, pixel_z = 0, easing = QUAD_EASING | EASE_IN)
animate(time = 250 MILLISECONDS, pixel_x = rand(-6, 6), pixel_y = rand(-4, 4), flags = ANIMATION_PARALLEL)
+/obj/item/soil_sack/Exited(atom/movable/gone)
+ . = ..()
+ if(gone == stored_soil)
+ stored_soil = null
+ qdel(src)
+
/obj/item/soil_sack/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(!isopenturf(interacting_with) || isgroundlessturf(interacting_with))
return ..()
@@ -174,19 +180,24 @@
if(!do_after(user, 1 SECONDS, interacting_with))
return ITEM_INTERACT_BLOCKING
+ transfer_soil(interacting_with)
+ return ITEM_INTERACT_SUCCESS
+
+//Proc responsible for placing the soil inside track onto the turf or inside a hydroponic tray
+/obj/item/soil_sack/proc/transfer_soil(atom/target, inside_tray = FALSE)
if(ispath(stored_soil))
stored_soil = new stored_soil(src)
+ if(inside_tray)
+ STOP_PROCESSING(SSmachines, stored_soil)
stored_soil.reagents.add_reagent(/datum/reagent/plantnutriment/eznutriment, stored_soil.maxnutri / 2)
stored_soil.waterlevel = stored_soil.maxwater
- else
+ else if(!inside_tray)
START_PROCESSING(SSmachines, stored_soil)
-
- stored_soil.forceMove(interacting_with)
- playsound(stored_soil, placement_sound, 65, vary = TRUE)
- stored_soil.on_place()
- qdel(src)
- return ITEM_INTERACT_SUCCESS
+ playsound(target, placement_sound, 65, vary = TRUE)
+ if(!inside_tray)
+ stored_soil.on_place()
+ stored_soil.forceMove(target) //stored_soil is set to null at this point, and the soil sack is deleted when that happens
/obj/item/soil_sack/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
if(attack_type == OVERWHELMING_ATTACK)
diff --git a/code/modules/instruments/songs/_song.dm b/code/modules/instruments/songs/_song.dm
index f0df050b5c46..04786785bde6 100644
--- a/code/modules/instruments/songs/_song.dm
+++ b/code/modules/instruments/songs/_song.dm
@@ -120,6 +120,10 @@
var/cached_exponential_dropoff = 1.045
/////////////////////////////////////////////////////////////////////////
+
+ ///Rate at which volume goes down to 0. Not controlled in menu.
+ var/exponential_falloff = 4
+
/datum/song/New(atom/parent, list/instrument_ids, new_range)
SSinstruments.on_song_new(src)
lines = list()
diff --git a/code/modules/instruments/songs/editor.dm b/code/modules/instruments/songs/editor.dm
index aef02858c037..93c84f5c74ba 100644
--- a/code/modules/instruments/songs/editor.dm
+++ b/code/modules/instruments/songs/editor.dm
@@ -7,6 +7,11 @@
/datum/song/ui_host(mob/user)
return parent
+/datum/song/ui_state(mob/user)
+ if(ispAI(user))
+ return GLOB.always_state
+ return ..()
+
/datum/song/ui_data(mob/user)
var/list/data = ..()
data["id"] = id
diff --git a/code/modules/instruments/songs/play_legacy.dm b/code/modules/instruments/songs/play_legacy.dm
index bb60d63c3b54..8513b8582170 100644
--- a/code/modules/instruments/songs/play_legacy.dm
+++ b/code/modules/instruments/songs/play_legacy.dm
@@ -88,5 +88,5 @@
var/pref_volume = M?.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_instruments)
if(!pref_volume)
continue
- M.playsound_local(source, null, volume * using_instrument.volume_multiplier * (pref_volume/100), sound_to_use = music_played)
+ M.playsound_local(source, null, volume * using_instrument.volume_multiplier * (pref_volume/100), sound_to_use = music_played, falloff_exponent = exponential_falloff)
// Could do environment and echo later but not for now
diff --git a/code/modules/instruments/songs/play_synthesized.dm b/code/modules/instruments/songs/play_synthesized.dm
index 6f9cf2280b28..92ae4d369c58 100644
--- a/code/modules/instruments/songs/play_synthesized.dm
+++ b/code/modules/instruments/songs/play_synthesized.dm
@@ -67,7 +67,7 @@
var/pref_volume = M?.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_instruments)
if(!pref_volume)
continue
- M.playsound_local(get_turf(parent), null, volume * (pref_volume/100), FALSE, K.frequency, null, channel, null, copy)
+ M.playsound_local(get_turf(parent), null, volume * (pref_volume/100), FALSE, K.frequency, exponential_falloff, channel, null, copy)
// Could do environment and echo later but not for now
/**
diff --git a/code/modules/jobs/job_types/antagonists/nuclear_operative.dm b/code/modules/jobs/job_types/antagonists/nuclear_operative.dm
index f2e38bf27d74..ac4b7be3435b 100644
--- a/code/modules/jobs/job_types/antagonists/nuclear_operative.dm
+++ b/code/modules/jobs/job_types/antagonists/nuclear_operative.dm
@@ -2,26 +2,20 @@
title = ROLE_OPERATIVE
/datum/job/nuclear_operative/get_roundstart_spawn_point()
- return pick(GLOB.nukeop_start)
+ return pick(GLOB.nukeop_elevator_start)
/datum/job/nuclear_operative/get_latejoin_spawn_point()
- return pick(GLOB.nukeop_start)
+ return pick(GLOB.nukeop_base_start)
/datum/job/nuclear_operative/leader
-/datum/job/nuclear_operative/leader/get_roundstart_spawn_point()
- return pick(GLOB.nukeop_leader_start)
-
/datum/job/nuclear_operative/leader/get_latejoin_spawn_point()
- return pick(GLOB.nukeop_leader_start)
+ return pick(GLOB.nukeop_base_leader_start)
/datum/job/nuclear_operative/clown_operative
title = ROLE_CLOWN_OPERATIVE
/datum/job/nuclear_operative/clown_operative/leader
-/datum/job/nuclear_operative/clown_operative/leader/get_roundstart_spawn_point()
- return pick(GLOB.nukeop_leader_start)
-
/datum/job/nuclear_operative/clown_operative/leader/get_latejoin_spawn_point()
- return pick(GLOB.nukeop_leader_start)
+ return pick(GLOB.nukeop_base_leader_start)
diff --git a/code/modules/jobs/job_types/assistant/gimmick_assistants.dm b/code/modules/jobs/job_types/assistant/gimmick_assistants.dm
index 2c3133ed8d86..1a9b62956904 100644
--- a/code/modules/jobs/job_types/assistant/gimmick_assistants.dm
+++ b/code/modules/jobs/job_types/assistant/gimmick_assistants.dm
@@ -168,6 +168,7 @@
head = /obj/item/clothing/head/utility/hardhat
uniform = /obj/item/clothing/under/color/yellow
l_pocket = /obj/item/modular_computer/pda/assistant
+ pda_slot = ITEM_SLOT_LPOCKET
outfit_weight = 6
diff --git a/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm b/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm
index 91823a073c8b..c06f8993b53f 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm
@@ -79,12 +79,8 @@ If the scythe isn't empowered when you sheath it, you take a heap of damage and
/obj/item/vorpalscythe/Initialize(mapload)
. = ..()
AddElement(/datum/element/nullrod_core, chaplain_spawnable = FALSE, rune_remove_line = "TO DUST WITH YE!! AWAY!!") // The implant is the actual item the chappie can select
- AddComponent(
- /datum/component/butchering, \
- speed = 3 SECONDS, \
- effectiveness = 125, \
- )
- AddElement(/datum/element/bane, mob_biotypes = MOB_PLANT, damage_multiplier = 0.5, requires_combat_mode = FALSE) //also good at killing plants
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5) //also good at killing plants
+ AddComponent(/datum/component/butchering, speed = 3 SECONDS, effectiveness = 125)
/obj/item/vorpalscythe/attack(mob/living/target, mob/living/user, list/modifiers, list/attack_modifiers)
if(ismonkey(target) && !target.mind) //Don't empower from hitting monkeys. Hit a corgi or something, I don't know.
diff --git a/code/modules/jobs/job_types/janitor.dm b/code/modules/jobs/job_types/janitor.dm
index d7c52c8255b7..52735aad579d 100644
--- a/code/modules/jobs/job_types/janitor.dm
+++ b/code/modules/jobs/job_types/janitor.dm
@@ -39,7 +39,7 @@
uniform = /obj/item/clothing/under/rank/civilian/janitor
belt = /obj/item/modular_computer/pda/janitor
ears = /obj/item/radio/headset/headset_srv
- skillchips = list(/obj/item/skillchip/job/janitor)
+ skillchips = list(/obj/item/skillchip/job/janitor, /obj/item/skillchip/disposals)
backpack_contents = list(/obj/item/access_key)
/datum/outfit/job/janitor/pre_equip(mob/living/carbon/human/human_equipper, visuals_only)
diff --git a/code/modules/jobs/job_types/prisoner.dm b/code/modules/jobs/job_types/prisoner.dm
index 99b3eab21550..c025a900c801 100644
--- a/code/modules/jobs/job_types/prisoner.dm
+++ b/code/modules/jobs/job_types/prisoner.dm
@@ -3,7 +3,7 @@
description = "Keep yourself occupied in permabrig."
faction = FACTION_STATION
total_positions = 0
- spawn_positions = 2
+ spawn_positions = 4
supervisors = "the security team"
exp_granted_type = EXP_TYPE_CREW
paycheck = PAYCHECK_LOWER
diff --git a/code/modules/jobs/job_types/shaft_miner.dm b/code/modules/jobs/job_types/shaft_miner.dm
index 6f166c35d169..d2c7502c5105 100644
--- a/code/modules/jobs/job_types/shaft_miner.dm
+++ b/code/modules/jobs/job_types/shaft_miner.dm
@@ -96,6 +96,7 @@
l_pocket = /obj/item/modular_computer/pda/shaftminer
r_pocket = /obj/item/extinguisher/mini
belt = /obj/item/storage/belt/mining/healing
+ pda_slot = ITEM_SLOT_LPOCKET
/datum/outfit/job/miner/equipped/combat/post_equip(mob/living/carbon/human/miner, visuals_only = FALSE)
. = ..()
diff --git a/code/modules/library/bibles.dm b/code/modules/library/bibles.dm
index 07a83912f003..597c245a7c3e 100644
--- a/code/modules/library/bibles.dm
+++ b/code/modules/library/bibles.dm
@@ -365,13 +365,13 @@ GLOBAL_LIST_INIT(bibleitemstates, list(
/obj/item/book/bible/syndicate/Initialize(mapload)
. = ..()
AddComponent(/datum/component/anti_magic, MAGIC_RESISTANCE|MAGIC_RESISTANCE_HOLY)
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_SPIRIT, added_damage = 25)
AddComponent(/datum/component/effect_remover, \
success_feedback = "You disrupt the magic of %THEEFFECT with %THEWEAPON.", \
success_forcesay = "BEGONE FOUL MAGIKS!!", \
tip_text = "Clear rune", \
effects_we_clear = list(/obj/effect/rune, /obj/effect/heretic_rune, /obj/effect/cosmic_rune), \
)
- AddElement(/datum/element/bane, mob_biotypes = MOB_SPIRIT, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
/obj/item/book/bible/syndicate/attack_self(mob/living/carbon/human/user, modifiers)
if(!uses || !istype(user))
diff --git a/code/modules/library/skill_learning/generic_skillchips/misc.dm b/code/modules/library/skill_learning/generic_skillchips/misc.dm
index 288b164bc63b..253d411ab87e 100644
--- a/code/modules/library/skill_learning/generic_skillchips/misc.dm
+++ b/code/modules/library/skill_learning/generic_skillchips/misc.dm
@@ -154,3 +154,13 @@
/datum/action/cooldown/fishing_tip/Activate(atom/target_atom)
. = ..()
send_tip_of_the_round(owner, pick(GLOB.fishing_tips), source = "Ancient fishing wisdom")
+
+/obj/item/skillchip/disposals
+ name = "T4RG3T.bin skillchip"
+ desc = "Become a dauntless disposaler, target trash right where it belongs."
+ auto_traits = list(TRAIT_THROWINGARM)
+ skill_name = "Dauntless Disposaler"
+ skill_description = "You have an uncanny ability to perfectly land every toss into disposal units."
+ skill_icon = "trash-can"
+ activate_message = span_notice("You seem laser focused on the nearby disposal unit.")
+ deactivate_message = span_notice("The nearby disposal unit fades into the background of your vision.")
diff --git a/code/modules/logging/log_holder.dm b/code/modules/logging/log_holder.dm
index fa295c5e2923..666390613ecf 100644
--- a/code/modules/logging/log_holder.dm
+++ b/code/modules/logging/log_holder.dm
@@ -32,13 +32,10 @@ GLOBAL_REAL(logger, /datum/log_holder)
GENERAL_PROTECT_DATUM(/datum/log_holder)
-ADMIN_VERB(log_viewer_new, R_ADMIN|R_DEBUG, "View Round Logs", "View the rounds logs.", ADMIN_CATEGORY_MAIN)
+ADMIN_VERB(log_viewer_new, R_ADMIN, "View Round Logs", "View the rounds logs.", ADMIN_CATEGORY_MAIN)
logger.ui_interact(user.mob)
/datum/log_holder/ui_interact(mob/user, datum/tgui/ui)
- if(!check_rights_for(user.client, R_ADMIN))
- return
-
ui = SStgui.try_update_ui(user, src, ui)
if(isnull(ui))
ui = new(user, src, "LogViewer")
@@ -46,7 +43,7 @@ ADMIN_VERB(log_viewer_new, R_ADMIN|R_DEBUG, "View Round Logs", "View the rounds
ui.open()
/datum/log_holder/ui_state(mob/user)
- return ADMIN_STATE(R_ADMIN | R_DEBUG)
+ return ADMIN_STATE(R_ADMIN)
/datum/log_holder/ui_static_data(mob/user)
var/list/data = list(
diff --git a/code/modules/lootpanel/_lootpanel.dm b/code/modules/lootpanel/_lootpanel.dm
index f7cfab8d8cd4..cc67696d6f22 100644
--- a/code/modules/lootpanel/_lootpanel.dm
+++ b/code/modules/lootpanel/_lootpanel.dm
@@ -23,6 +23,8 @@
/datum/lootpanel/Destroy(force)
+ SSlooting.backlog -= src
+ SSlooting.processing -= src
reset_contents()
owner = null
source_turf = null
diff --git a/code/modules/lootpanel/ss_looting.dm b/code/modules/lootpanel/ss_looting.dm
index eb8cff36e696..6f2481844a16 100644
--- a/code/modules/lootpanel/ss_looting.dm
+++ b/code/modules/lootpanel/ss_looting.dm
@@ -18,7 +18,7 @@ SUBSYSTEM_DEF(looting)
/datum/controller/subsystem/looting/fire(resumed)
- if(!length(backlog))
+ if(!length(backlog) && !length(processing))
return
if(!resumed)
diff --git a/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm b/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm
index ce57035a7661..b18e740f8cb4 100644
--- a/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm
+++ b/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm
@@ -123,4 +123,77 @@
resistance_flags = FIRE_PROOF | LAVA_PROOF
max_integrity = 200
+/obj/effect/light_emitter/tendril
+ set_luminosity = 4
+ set_cap = 2.5
+ light_color = LIGHT_COLOR_LAVA
+
+/obj/effect/collapse
+ name = "collapsing necropolis tendril"
+ desc = "Get your loot and get clear!"
+ layer = TABLE_LAYER
+ icon = 'icons/mob/simple/lavaland/nest.dmi'
+ icon_state = "tendril"
+ anchored = TRUE
+ density = TRUE
+ /// weakref list of which mobs have gotten their loot from this effect.
+ var/list/collected = list()
+ /// a bit of light as to make less unfair deaths from the chasm
+ var/obj/effect/light_emitter/tendril/emitted_light
+
+/obj/effect/collapse/Initialize(mapload)
+ . = ..()
+ emitted_light = new(loc)
+ visible_message(span_bolddanger("The tendril writhes in fury as the earth around it begins to crack and break apart! Get back!"))
+ balloon_alert_to_viewers("interact to grab loot before collapse!", vision_distance = 7)
+ playsound(loc,'sound/effects/tendril_destroyed.ogg', 200, FALSE, 50, TRUE, TRUE)
+ addtimer(CALLBACK(src, PROC_REF(collapse)), 5 SECONDS)
+
+/obj/effect/collapse/examine(mob/user)
+ var/list/examine_messages = ..()
+ if(isliving(user))
+ if(has_collected(user))
+ examine_messages += span_boldnotice("You've grabbed what you can, now get out!")
+ else
+ examine_messages += span_boldnotice("You might have some time to grab some goodies with an open hand before it collapses!")
+ return examine_messages
+
+/obj/effect/collapse/attack_hand(mob/living/collector, list/modifiers)
+ . = ..()
+ if(has_collected(collector))
+ to_chat(collector, span_danger("You've already gotten some loot, just get out of there with it!"))
+ return
+ visible_message(span_warning("Something falls free of the tendril!"))
+ var/obj/structure/closet/crate/necropolis/tendril/loot = new /obj/structure/closet/crate/necropolis/tendril(loc)
+ collector.start_pulling(loot)
+ collected += WEAKREF(collector)
+
+/obj/effect/collapse/Destroy()
+ collected.Cut()
+ QDEL_NULL(emitted_light)
+ return ..()
+
+///Helper proc that resolves weakrefs to determine if collector is in collected list, returning a boolean.
+/obj/effect/collapse/proc/has_collected(mob/collector)
+ for(var/datum/weakref/weakref as anything in collected)
+ var/mob/living/resolved = weakref.resolve()
+ //it could have been collector, it could not have been, we don't care
+ if(!resolved)
+ continue
+ if(resolved == collector)
+ return TRUE
+ return FALSE
+
+/obj/effect/collapse/proc/collapse()
+ for(var/mob/viewer in range(7, src))
+ shake_camera(viewer, 15, 1)
+ playsound(get_turf(src),'sound/effects/explosion/explosionfar.ogg', 200, TRUE)
+ visible_message(span_bolddanger("The tendril falls inward, the ground around it widening into a yawning chasm!"))
+ for(var/turf/ground in RANGE_TURFS(2, src))
+ if(HAS_TRAIT(ground, TRAIT_NO_TERRAFORM))
+ continue
+ if(!ground.density)
+ ground.TerraformTurf(/turf/open/chasm/lavaland, /turf/open/chasm/lavaland, flags = CHANGETURF_INHERIT_AIR)
+ qdel(src)
+
#undef ASH_WALKER_SPAWN_THRESHOLD
diff --git a/code/modules/mapping/ruins.dm b/code/modules/mapping/ruins.dm
index 94d5dd6195c2..c6ce163abf52 100644
--- a/code/modules/mapping/ruins.dm
+++ b/code/modules/mapping/ruins.dm
@@ -212,6 +212,9 @@
var/top_right_y = bottom_left_y + current_pick.height - 1
log_mapping("Successfully placed [current_pick.name] ruin ([bottom_left_x],[bottom_left_y],[placed_turf.z] to [top_right_x],[top_right_y],[placed_turf.z]).")
+ ///Keep track of the active ruins so we can take it in account for map generation. Using bottom lef turf as key
+ SSmapping.active_ruins[locate(bottom_left_x, bottom_left_y, placed_turf.z)] = current_pick
+
//Update the available list
for(var/datum/map_template/ruin/R in ruins_available)
if(R.cost > budget || R.mineral_cost > mineral_budget)
diff --git a/code/modules/mining/aux_base.dm b/code/modules/mining/aux_base.dm
index 82cdaffa7146..ff4bf581cd83 100644
--- a/code/modules/mining/aux_base.dm
+++ b/code/modules/mining/aux_base.dm
@@ -147,7 +147,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/computer/auxiliary_base, 32)
return
var/shuttle_error = SSshuttle.moveShuttle(shuttleId, params["shuttle_id"], 1)
if(launch_warning)
- say("Launch sequence activated! Prepare for drop!!", spans = list("danger"))
+ say("Launch sequence activated! Prepare for drop!!", spans = list(SPAN_DANGER))
playsound(loc, 'sound/machines/warning-buzzer.ogg', 70, FALSE)
launch_warning = FALSE
blind_drop_ready = FALSE
diff --git a/code/modules/mining/boulder_processing/_boulder_processing.dm b/code/modules/mining/boulder_processing/_boulder_processing.dm
index 6d11c876bf36..324c616ae84e 100644
--- a/code/modules/mining/boulder_processing/_boulder_processing.dm
+++ b/code/modules/mining/boulder_processing/_boulder_processing.dm
@@ -22,10 +22,6 @@
var/points_held = 0
///The action verb to display to players
var/action = "processing"
-
-
- /// What list of reagents should we look at when we boost the effectiveness of this machinery? Assign a value to a chem as well, eg: /datum/reagent/water = 1 is a 10% boost
- var/list/booster_list = list()
/// What reagent should be produced when a boost chemical is replaced by the booster_reagent?
var/datum/reagent/waste_chemical = /datum/reagent/water
@@ -107,6 +103,7 @@
/obj/machinery/bouldertech/examine_more(mob/user)
. = ..()
+ var/list/datum/reagents/booster_list = get_booster_reagents()
if(length(booster_list))
. += span_notice("This machine's output is boosted by chemical intake: ")
for(var/datum/reagent/increment as anything in booster_list)
@@ -121,7 +118,7 @@
icon_state ="[base_icon_state][suffix]"
/obj/machinery/bouldertech/CanAllowThrough(atom/movable/mover, border_dir)
- if(!anchored)
+ if(!anchored || !(dir == border_dir || dir == REVERSE_DIR(border_dir)))
return FALSE
if(istype(mover, /obj/item/stack/sheet))
return TRUE
@@ -254,6 +251,12 @@
refining_efficiency = initial(refining_efficiency) //Reset refining efficiency to 100%.
+///Returns a map of reagent -> boost amount to increase this machines efficiency
+/obj/machinery/bouldertech/proc/get_booster_reagents()
+ RETURN_TYPE(/list/datum/reagents)
+
+ return list()
+
/**
* Checks if this machine can process this material
* Arguments
@@ -380,7 +383,7 @@
if(istype(chosen_boulder, /obj/item/boulder/artifact))
points_held = round((points_held + MINER_POINT_MULTIPLIER)) /// Artifacts give bonus points!
chosen_boulder.break_apart()
- return//We've processed all the materials in the boulder, so we can just destroy it in break_apart.
+ return //We've processed all the materials in the boulder, so we can just destroy it in break_apart.
chosen_boulder.processed_by = src
diff --git a/code/modules/mining/boulder_processing/boulder.dm b/code/modules/mining/boulder_processing/boulder.dm
index 96ed8878e71a..3969c5551808 100644
--- a/code/modules/mining/boulder_processing/boulder.dm
+++ b/code/modules/mining/boulder_processing/boulder.dm
@@ -34,7 +34,7 @@
register_context()
AddComponent(/datum/component/two_handed, require_twohands = TRUE, force_unwielded = 0, force_wielded = 5) //Heavy as all hell, it's a boulder, dude.
AddComponent(/datum/component/sisyphus_awarder)
- AddElement(/datum/element/bane, mob_biotypes = MOB_SPECIAL, added_damage = 20, requires_combat_mode = FALSE)
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_SPECIAL, added_damage = 20)
/obj/item/boulder/Destroy(force)
SSore_generation.available_boulders -= src
diff --git a/code/modules/mining/boulder_processing/boulder_types.dm b/code/modules/mining/boulder_processing/boulder_types.dm
index ec67648fe2a3..1a6979f38852 100644
--- a/code/modules/mining/boulder_processing/boulder_types.dm
+++ b/code/modules/mining/boulder_processing/boulder_types.dm
@@ -1,3 +1,6 @@
+#define BONUS_MATS_MINIMUM 1
+#define BONUS_MATS_MAXIMUM 5
+
///Boulders with special artificats that can give higher mining points
/obj/item/boulder/artifact
name = "artifact boulder"
@@ -7,10 +10,14 @@
var/artifact_type = /obj/item/relic/lavaland
/// References to the relic inside the boulder, if any.
var/obj/item/artifact_inside
+ /// Bonus materials to add to this boulder, in addition to existing materials created by the ore vent.
+ var/datum/material/bonus_mat
/obj/item/boulder/artifact/Initialize(mapload)
. = ..()
artifact_inside = new artifact_type(src) /// This could be poggers for archaeology in the future.
+ if(bonus_mat)
+ add_bonus_mats()
/obj/item/boulder/artifact/Destroy(force)
QDEL_NULL(artifact_inside)
@@ -27,7 +34,24 @@
/obj/item/boulder/artifact/update_icon_state()
. = ..()
- icon_state = "boulder_artifact" // Hardset to artifact sprites for consistency
+ icon_state = initial(icon_state) // Hardset to artifact sprites for consistency
+
+/// Adds a random amount of material to an artifact boulder, determined by BONUS_MAT defines and of the type bonus_mat defined on the boulder.
+/obj/item/boulder/artifact/proc/add_bonus_mats()
+ var/list/bonus_mats = list()
+ if(custom_materials)
+ bonus_mats = custom_materials.Copy()
+ bonus_mats[bonus_mat] += rand(BONUS_MATS_MINIMUM, BONUS_MATS_MAXIMUM) * SHEET_MATERIAL_AMOUNT
+ set_custom_materials(bonus_mats)
+
+
+/obj/item/boulder/artifact/bluespace
+ icon_state = "boulder_artifact_BS"
+ bonus_mat = /datum/material/bluespace
+
+/obj/item/boulder/artifact/diamond
+ icon_state = "boulder_artifact_diamond"
+ bonus_mat = /datum/material/diamond
///Boulders usually spawned in lavaland labour camp area
/obj/item/boulder/gulag
@@ -77,3 +101,6 @@
desc = "A bizarre, twisted boulder. Wait, wait no, it's just a rock."
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 1.1, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 1.1)
durability = 1
+
+#undef BONUS_MATS_MINIMUM
+#undef BONUS_MATS_MAXIMUM
diff --git a/code/modules/mining/boulder_processing/refinery.dm b/code/modules/mining/boulder_processing/refinery.dm
index 52cf7189062a..f65669d3b401 100644
--- a/code/modules/mining/boulder_processing/refinery.dm
+++ b/code/modules/mining/boulder_processing/refinery.dm
@@ -12,14 +12,33 @@
usage_sound = 'sound/machines/mining/refinery.ogg'
action = "crushing"
waste_chemical = /datum/reagent/toxin/acid/industrial_waste
+ pixel_y = 1
- /// What list of reagents should we look at when we boost the effectiveness of this machinery?
- booster_list = list(
- /datum/reagent/toxin/acid = 1,
- /datum/reagent/toxin/acid/fluacid = 2,
- /datum/reagent/toxin/acid/nitracid = 3,
- /datum/reagent/teslium = 5,
- )
+/obj/machinery/bouldertech/refinery/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/plumbing/boulder_reactions)
+ AddElement(/datum/element/simple_rotation)
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/machinery/bouldertech/refinery/update_icon_state()
+ . = ..()
+ set_light_on(anchored && is_operational && !panel_open)
+
+/obj/machinery/bouldertech/refinery/create_reagents(max_vol, flags)
+ QDEL_NULL(reagents)
+ reagents = new /datum/reagents/plumbing(max_vol, flags)
+ reagents.my_atom = src
+
+/obj/machinery/bouldertech/refinery/get_booster_reagents()
+ var/static/list/booster_reagents
+ if(!length(booster_reagents))
+ booster_reagents = list(
+ /datum/reagent/toxin/acid = 1,
+ /datum/reagent/toxin/acid/fluacid = 2,
+ /datum/reagent/toxin/acid/nitracid = 3,
+ /datum/reagent/teslium = 5,
+ )
+ return booster_reagents
/obj/machinery/bouldertech/refinery/can_process_material(datum/material/possible_mat)
var/static/list/processable_materials
@@ -54,24 +73,19 @@
reagents.maximum_volume = new_volume
-/obj/machinery/bouldertech/refinery/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/plumbing/boulder_reactions)
- AddElement(/datum/element/simple_rotation)
- update_appearance(UPDATE_OVERLAYS)
-
/obj/machinery/bouldertech/refinery/check_for_boosts()
. = ..() //resets to 1.00 efficiency in the parent
+
var/highest_boost = 0
var/datum/reagent/biggest_booster
- for(var/datum/reagent/chem in reagents.reagent_list)
- if(!booster_list[chem.type])
+ var/list/datum/reagents/booster_list = get_booster_reagents()
+ for(var/datum/reagent/booster as anything in booster_list)
+ var/booster_volume = booster_list[booster]
+ if(!reagents.has_reagent(booster, booster_volume)) //check that we have the associated quantity of the chem in order to perform the boost.
continue
- if(!reagents.has_reagent(chem.type, booster_list[chem.type])) //check that we have the associated quantity of the chem in order to perform the boost.
- continue
- if(booster_list[chem.type] > highest_boost)
- highest_boost = booster_list[chem.type]
- biggest_booster = chem.type
+ if(booster_list[booster] > highest_boost)
+ highest_boost = booster_volume
+ biggest_booster = booster
if(!biggest_booster)
return
@@ -80,14 +94,6 @@
refining_efficiency = 1 + (highest_boost / 10) //Results in a boost from 10-30%
reagents.add_reagent(waste_chemical, highest_boost)
-/obj/machinery/bouldertech/refinery/default_deconstruction_screwdriver(mob/user, icon_state_open, icon_state_closed, obj/item/screwdriver)
- . = ..()
- set_light_on(TRUE)
-
-/obj/machinery/bouldertech/refinery/default_unfasten_wrench(mob/user, obj/item/wrench, time)
- . = ..()
- set_light_on(TRUE)
-
/obj/machinery/bouldertech/refinery/plunger_act(obj/item/plunger/attacking_plunger, mob/living/user, reinforced)
. = ..()
balloon_alert(user, "emptying...")
@@ -113,17 +119,24 @@
circuit = /obj/item/circuitboard/machine/smelter
usage_sound = 'sound/machines/mining/smelter.ogg'
action = "smelting"
- booster_list = list(
- /datum/reagent/fuel = 1,
- /datum/reagent/thermite = 2,
- /datum/reagent/gunpowder = 3,
- /datum/reagent/liquid_dark_matter = 5,
- )
+ pixel_x = -1
/obj/machinery/bouldertech/refinery/smelter/Initialize(mapload)
. = ..()
update_light_value()
+
+/obj/machinery/bouldertech/refinery/smelter/get_booster_reagents()
+ var/static/list/booster_reagents
+ if(!length(booster_reagents))
+ booster_reagents = list(
+ /datum/reagent/fuel = 1,
+ /datum/reagent/thermite = 2,
+ /datum/reagent/gunpowder = 3,
+ /datum/reagent/liquid_dark_matter = 5,
+ )
+ return booster_reagents
+
/obj/machinery/bouldertech/refinery/smelter/can_process_material(datum/material/possible_mat)
var/static/list/processable_materials
if(!length(processable_materials))
diff --git a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm
index 55533341d895..9f72047fcdea 100644
--- a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm
+++ b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm
@@ -218,7 +218,7 @@
var/backstabbed = FALSE
var/def_check = target.getarmor(type = BOMB)
// Backstab bonus
- if(check_behind(user, target) || boosted_mark)
+ if(check_behind(user, target) && !HAS_TRAIT(target, TRAIT_BACKSTAB_IMMUNE) || boosted_mark)
backstabbed = TRUE
combined_damage += backstab_bonus
playsound(user, backstab_sound, 100, TRUE) //Seriously who spelled it wrong
@@ -336,6 +336,9 @@
fired_from = null
return ..()
+/obj/projectile/destabilizer/is_hostile_projectile()
+ return TRUE
+
/obj/projectile/destabilizer/proc/on_parry(mob/user)
SIGNAL_HANDLER
boosted = TRUE
diff --git a/code/modules/mining/equipment/kinetic_crusher/trophies_fauna.dm b/code/modules/mining/equipment/kinetic_crusher/trophies_fauna.dm
index 16290710871d..df40bf4f05b3 100644
--- a/code/modules/mining/equipment/kinetic_crusher/trophies_fauna.dm
+++ b/code/modules/mining/equipment/kinetic_crusher/trophies_fauna.dm
@@ -144,7 +144,7 @@
// Bileworm
/obj/item/crusher_trophy/bileworm_spewlet
name = "bileworm spewlet"
- icon = 'icons/mob/simple/lavaland/bileworm.dmi'
+ icon = 'icons/obj/mining_zones/artefacts.dmi'
icon_state = "bileworm_spewlet"
desc = "A baby bileworm. Suitable as a trophy for a kinetic crusher."
denied_type = /obj/item/crusher_trophy/bileworm_spewlet
@@ -186,17 +186,31 @@
check_flags = NONE
owner_has_control = FALSE
cooldown_time = 10 SECONDS
- projectile_type = /obj/projectile/bileworm_acid/crusher
+ projectile_type = /obj/projectile/bileworm_acid
projectile_sound = 'sound/mobs/non-humanoids/bileworm/bileworm_spit.ogg'
/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/spewlet/New(Target)
firing_directions = GLOB.cardinals.Copy()
return ..()
-/obj/projectile/bileworm_acid/crusher
+/obj/projectile/bileworm_acid // basically only used by the crusher trophy
+ name = "acidic bile"
+ damage = 20
+ speed = 0.5
+ range = 20
+ hitsound = 'sound/items/weapons/sear.ogg'
+ pass_flags = PASSTABLE
+ icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ icon_state = "bile_glob"
+ layer = ABOVE_ALL_MOB_LAYER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
damage_type = BRUTE // Otherwise the mobs take heavily reduced damage
-/obj/projectile/bileworm_acid/crusher/prehit_pierce(atom/target)
+/obj/projectile/bileworm_acid/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/parriable_projectile)
+
+/obj/projectile/bileworm_acid/prehit_pierce(atom/target)
if (!isliving(target))
return ..()
var/mob/living/as_living = target
diff --git a/code/modules/mining/equipment/kinetic_crusher/trophies_megafauna.dm b/code/modules/mining/equipment/kinetic_crusher/trophies_megafauna.dm
index 5c69886c3711..09d15bef6ba2 100644
--- a/code/modules/mining/equipment/kinetic_crusher/trophies_megafauna.dm
+++ b/code/modules/mining/equipment/kinetic_crusher/trophies_megafauna.dm
@@ -38,7 +38,7 @@
living_target.adjust_fire_loss(bonus_value, forced = TRUE)
/obj/item/crusher_trophy/tail_spike/proc/pushback(mob/living/target, mob/living/user)
- if(!QDELETED(target) && !QDELETED(user) && (!target.anchored || ismegafauna(target))) //megafauna will always be pushed
+ if(!QDELETED(target) && !QDELETED(user) && (!target.anchored || ismegafauna(target)) && target.move_resist < INFINITY) //megafauna will always be pushed
step(target, get_dir(user, target))
//bubblegum
diff --git a/code/modules/mining/equipment/mining_tools.dm b/code/modules/mining/equipment/mining_tools.dm
index 73ebdf10a71c..d16be7e4b4c1 100644
--- a/code/modules/mining/equipment/mining_tools.dm
+++ b/code/modules/mining/equipment/mining_tools.dm
@@ -185,7 +185,7 @@
/obj/item/shovel/serrated/Initialize(mapload)
. = ..()
- AddElement(/datum/element/bane, mob_biotypes = MOB_ORGANIC, damage_multiplier = 1) //You may be horridly cursed now, but at least you kill the living a whole lot more easily!
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_ORGANIC, damage_multiplier = 2) //You may be horridly cursed now, but at least you kill the living a whole lot more easily!
/obj/item/shovel/serrated/examine(mob/user)
. = ..()
diff --git a/code/modules/mining/equipment/monster_organs/brimdust_sac.dm b/code/modules/mining/equipment/monster_organs/brimdust_sac.dm
index 5ea9730daca4..9c741ae06808 100644
--- a/code/modules/mining/equipment/monster_organs/brimdust_sac.dm
+++ b/code/modules/mining/equipment/monster_organs/brimdust_sac.dm
@@ -169,6 +169,7 @@
dust_overlay.alpha = stacks * BRIMDUST_ALPHA_PER_STACK
dust_overlay.color = COLOR_RED_LIGHT
dust_overlay.blend_mode = BLEND_INSET_OVERLAY
+ ADD_KEEP_TOGETHER(owner, REF(src))
owner.add_overlay(dust_overlay)
var/obj/effect/holder = owner.add_shared_particles(/particles/brimdust, "brimdust_coating-[owner.base_pixel_w]")
holder.pixel_w = -owner.base_pixel_w
@@ -178,6 +179,7 @@
/datum/status_effect/stacking/brimdust_coating/on_remove()
. = ..()
owner.cut_overlay(dust_overlay)
+ REMOVE_KEEP_TOGETHER(owner, REF(src))
owner.remove_shared_particles("brimdust_coating-[owner.base_pixel_w]")
UnregisterSignal(owner, list(COMSIG_MOB_APPLY_DAMAGE, COMSIG_COMPONENT_CLEAN_ACT))
diff --git a/code/modules/mining/equipment/monster_organs/rush_gland.dm b/code/modules/mining/equipment/monster_organs/rush_gland.dm
index 894a9dbe0db6..f22bf029a8c7 100644
--- a/code/modules/mining/equipment/monster_organs/rush_gland.dm
+++ b/code/modules/mining/equipment/monster_organs/rush_gland.dm
@@ -24,6 +24,7 @@
/obj/item/organ/monster_core/rush_gland/on_mob_insert(mob/living/carbon/organ_owner)
. = ..()
RegisterSignal(organ_owner, COMSIG_GOLIATH_TENTACLED_GRABBED, PROC_REF(trigger_organ_action_on_sig))
+ RegisterSignal(organ_owner, COMSIG_TENDRIL_TENTACLED_GRABBED, PROC_REF(trigger_organ_action_on_sig))
/obj/item/organ/monster_core/rush_gland/on_mob_remove(mob/living/carbon/organ_owner, special, movement_flags)
. = ..()
@@ -34,7 +35,9 @@
/obj/item/organ/monster_core/rush_gland/proc/trigger_organ_action_on_sig(datum/source)
SIGNAL_HANDLER
+
INVOKE_ASYNC(src, PROC_REF(trigger_organ_action))
+ return COMPONENT_GOLIATH_CANCEL_TENTACLE_GRAB
/**
* Status effect: Makes you run faster and ignore damage speed penalties for a short duration.
diff --git a/code/modules/mining/equipment/resonator.dm b/code/modules/mining/equipment/resonator.dm
index 8c5ffb873a75..dcd980e71cdd 100644
--- a/code/modules/mining/equipment/resonator.dm
+++ b/code/modules/mining/equipment/resonator.dm
@@ -131,8 +131,9 @@
SEND_SIGNAL(creator, COMSIG_LIVING_RESONATOR_BURST, creator, attacked_living)
to_chat(attacked_living, span_userdanger("[src] ruptured with you in it!"))
attacked_living.apply_damage(resonance_damage, BRUTE)
- attacked_living.add_movespeed_modifier(/datum/movespeed_modifier/resonance)
- addtimer(CALLBACK(attacked_living, TYPE_PROC_REF(/mob, remove_movespeed_modifier), /datum/movespeed_modifier/resonance), 10 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE)
+ if(!QDELETED(attacked_living))
+ attacked_living.add_movespeed_modifier(/datum/movespeed_modifier/resonance)
+ addtimer(CALLBACK(attacked_living, TYPE_PROC_REF(/mob, remove_movespeed_modifier), /datum/movespeed_modifier/resonance), 10 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE)
for(var/obj/effect/temp_visual/resonance/field in orange(1, src))
if(field.rupturing)
continue
diff --git a/code/modules/mining/equipment/vent_pointer.dm b/code/modules/mining/equipment/vent_pointer.dm
index 4edab185597b..5c276b746ee0 100644
--- a/code/modules/mining/equipment/vent_pointer.dm
+++ b/code/modules/mining/equipment/vent_pointer.dm
@@ -2,7 +2,9 @@
name = "ventpointer"
desc = "A handheld tracking device. It will locate and point to nearby vents. A bit unreliable though."
icon_state = "pinpointer_vent"
- minimum_range = 14 //gotta use them eyes
+ minimum_range = 8 //gotta use them eyes
+ close_range = 12
+ medium_range = 20
/obj/item/pinpointer/vent/scan_for_target()
var/closest_dist = INFINITY
diff --git a/code/modules/mining/laborcamp/laborstacker.dm b/code/modules/mining/laborcamp/laborstacker.dm
index 2478b84b7f73..170baf58a3db 100644
--- a/code/modules/mining/laborcamp/laborstacker.dm
+++ b/code/modules/mining/laborcamp/laborstacker.dm
@@ -29,7 +29,8 @@
/obj/machinery/mineral/labor_claim_console/proc/register_shuttle_signal()
SIGNAL_HANDLER
var/obj/docking_port/mobile/laborshuttle = SSshuttle.getShuttle("laborcamp")
- RegisterSignal(laborshuttle, COMSIG_SHUTTLE_SHOULD_MOVE, PROC_REF(on_laborshuttle_can_move))
+ if(laborshuttle)
+ RegisterSignal(laborshuttle, COMSIG_SHUTTLE_SHOULD_MOVE, PROC_REF(on_laborshuttle_can_move))
UnregisterSignal(SSshuttle, COMSIG_SUBSYSTEM_POST_INITIALIZE)
/obj/machinery/mineral/labor_claim_console/Destroy()
@@ -77,6 +78,11 @@
data["can_go_home"] = can_go_home
return data
+/obj/machinery/mineral/labor_claim_console/ui_static_data(mob/user)
+ var/list/data = list()
+ data["shuttle_exists"] = !isnull(SSshuttle.getShuttle("laborcamp"))
+ return data
+
/obj/machinery/mineral/labor_claim_console/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
@@ -103,6 +109,12 @@
COOLDOWN_START(src, say_cooldown, 2 SECONDS)
if("move_shuttle")
+ if(isnull(SSshuttle.getShuttle("laborcamp")))
+ if(COOLDOWN_FINISHED(src, say_cooldown))
+ say("Shuttle not found.")
+ COOLDOWN_START(src, say_cooldown, 2 SECONDS)
+ return
+
var/list/labor_shuttle_mobs = find_labor_shuttle_mobs()
if(length(labor_shuttle_mobs) > 1 || labor_shuttle_mobs[1] != user_mob)
if(COOLDOWN_FINISHED(src, say_cooldown))
@@ -171,6 +183,7 @@
/**********************Prisoner Collection Unit**************************/
/obj/machinery/mineral/stacking_machine/laborstacker
+ name = "labor camp collection unit"
force_connect = TRUE
damage_deflection = 21 //otherwise prisoners will destroy it
///Idle points sitting in the machine left to be claimed.
diff --git a/code/modules/mining/lavaland/ash_flora.dm b/code/modules/mining/lavaland/ash_flora.dm
index 7f8c16d805e9..c9da29aad6ee 100644
--- a/code/modules/mining/lavaland/ash_flora.dm
+++ b/code/modules/mining/lavaland/ash_flora.dm
@@ -40,7 +40,7 @@
icon_state = base_icon_state
/obj/structure/flora/ash/tall_shroom //exists only so that the spawning check doesn't allow these spawning near other things
- regrowth_time_low = 4200
+ regrowth_time_low = 7 MINUTES
/obj/structure/flora/ash/leaf_shroom
name = "leafy mushrooms"
@@ -54,8 +54,8 @@
harvest_message_med = "You pluck a number of leaves, leaving a few unsuitable ones."
harvest_message_high = "You pluck quite a lot of suitable leaves."
harvest_time = 20
- regrowth_time_low = 2400
- regrowth_time_high = 6000
+ regrowth_time_low = 4 MINUTES
+ regrowth_time_high = 10 MINUTES
/obj/structure/flora/ash/leaf_shroom/get_potential_products()
return list(/obj/item/food/grown/ash_flora/mushroom_leaf = 1)
@@ -72,8 +72,8 @@
harvest_message_med = "You slice off a few conks from the larger mushrooms."
harvest_message_high = "You slice off a number of caps and conks from these mushrooms."
harvest_time = 50
- regrowth_time_low = 3000
- regrowth_time_high = 5400
+ regrowth_time_low = 5 MINUTES
+ regrowth_time_high = 9 MINUTES
/obj/structure/flora/ash/cap_shroom/get_potential_products()
return list(/obj/item/food/grown/ash_flora/mushroom_cap = 1)
@@ -92,8 +92,8 @@
harvest_message_med = "You pick and decapitate several mushrooms for their stems."
harvest_message_high = "You acquire a number of stems from these mushrooms."
harvest_time = 40
- regrowth_time_low = 3000
- regrowth_time_high = 6000
+ regrowth_time_low = 5 MINUTES
+ regrowth_time_high = 10 MINUTES
/obj/structure/flora/ash/stem_shroom/get_potential_products()
return list(/obj/item/food/grown/ash_flora/mushroom_stem = 1)
@@ -110,8 +110,8 @@
harvest_message_med = "You pick several cactus fruit." //shouldn't show up, because you can't get more than two
harvest_message_high = "You pick a pair of cactus fruit."
harvest_time = 10
- regrowth_time_low = 4800
- regrowth_time_high = 7200
+ regrowth_time_low = 8 MINUTES
+ regrowth_time_high = 12 MINUTES
can_uproot = FALSE //Don't want 50 in one tile to decimate whoever dare step on the mass of cacti
/obj/structure/flora/ash/cacti/Initialize(mapload)
@@ -133,8 +133,8 @@
harvest_message_med = "You grab a good haul of mushrooms."
harvest_message_high = "You hit the mushroom motherlode and make off with a bunch of tasty mushrooms."
harvest_time = 25
- regrowth_time_low = 3000
- regrowth_time_high = 5400
+ regrowth_time_low = 5 MINUTES
+ regrowth_time_high = 9 MINUTES
number_of_variants = 2
harvest_message_true_thresholds = FALSE
@@ -155,8 +155,8 @@
harvest_message_low = "You pluck a single, suitable flower."
harvest_message_med = "You pluck a number of flowers, leaving a few unsuitable ones."
harvest_message_high = "You pluck quite a lot of suitable flowers."
- regrowth_time_low = 2500
- regrowth_time_high = 4000
+ regrowth_time_low = 4 MINUTES
+ regrowth_time_high = 7 MINUTES
number_of_variants = 2
/obj/structure/flora/ash/fireblossom/get_potential_products()
@@ -174,6 +174,50 @@
update_light()
return ..()
+/obj/structure/flora/ash/glowgrowth
+ name = "glowgrowth colony"
+ desc = "A colony of bioluminescent fungi growing on a hot air vent, feeding off mineral particulate blowing through it."
+ icon_state = "glowgrowth1"
+ base_icon_state = "glowgrowth"
+ density = TRUE // Large rock formations with plants ontop
+ light_range = 1.7
+ light_power = 1.2
+ light_color = "#67b6a5"
+ harvested_name = "hot air vent"
+ harvested_desc = "A ridged porous rock formation exuming hot air from the depths of the planet."
+ harvest_amount_high = 4
+ harvest_verb = "scrape"
+ harvest_message_low = "You scrape off a thin layer of glowing fungi."
+ harvest_message_med = "You pick a sizeable patch of glowing fungi."
+ harvest_message_high = "You grab a large fistful of glowing fungi."
+ regrowth_time_low = 8 MINUTES
+ regrowth_time_high = 16 MINUTES
+ number_of_variants = 3
+
+/obj/structure/flora/ash/glowgrowth/Initialize(mapload)
+ . = ..()
+ update_appearance()
+
+/obj/structure/flora/ash/glowgrowth/get_potential_products()
+ return list(/obj/item/food/grown/ash_flora/glowgrowth = 1)
+
+/obj/structure/flora/ash/glowgrowth/after_harvest()
+ set_light_on(FALSE)
+ update_light()
+ update_appearance()
+ return ..()
+
+/obj/structure/flora/ash/glowgrowth/regrow()
+ set_light_on(TRUE)
+ update_light()
+ update_appearance()
+ return ..()
+
+/obj/structure/flora/ash/glowgrowth/update_overlays()
+ . = ..()
+ if (!harvested)
+ . += emissive_appearance(icon, "[icon_state]e", src, alpha = 120)
+
///Snow flora to exist on icebox.
/obj/structure/flora/ash/chilly
name = "springy grassy fruit"
@@ -274,6 +318,13 @@
// Fire flowers make fireproof raptors
AddElement(/datum/element/raptor_food, color_chances = string_list(list(/datum/raptor_color/blue = 5)))
+/obj/item/food/grown/ash_flora/glowgrowth
+ name = "glowgrowth sheet"
+ desc = "A thick sheet of glowing fungi."
+ icon_state = "glowgrowth"
+ seed = /obj/item/seeds/lavaland/glowgrowth // Cannot be grown in hydroponics as it feeds off mineral air
+ wine_power = 50
+
// SEEDS
/obj/item/seeds/lavaland
@@ -401,6 +452,17 @@
growing_icon = 'icons/obj/service/hydroponics/growing_flowers.dmi'
reagents_add = list(/datum/reagent/consumable/tinlux = 0.04, /datum/reagent/consumable/nutriment = 0.03, /datum/reagent/carbon = 0.05)
+/obj/item/seeds/lavaland/glowgrowth
+ name = "glowgrowth mycelium pack"
+ desc = "This mycelium grows into glowgrowth fungi."
+ plantname = "Glowgrowth Fungi"
+ icon_state = "mycelium-glowgrowth"
+ species = "glowgrowth"
+ product = /obj/item/food/grown/ash_flora/glowgrowth
+ genes = list(/datum/plant_gene/trait/plant_type/fungal_metabolism, /datum/plant_gene/trait/fire_resistance, /datum/plant_gene/trait/glow/blue) // Fungal metab doesn't do anything (cause it can't be planted) but it shows up in analyzers
+ reagents_add = list(/datum/reagent/luminescent_fluid/cyan = 0.06, /datum/reagent/consumable/nutriment = 0.01, /datum/reagent/silicon = 0.03)
+ seed_flags = parent_type::seed_flags | NO_PLANTING
+
//CRAFTING
/datum/crafting_recipe/mushroom_bowl
diff --git a/code/modules/mining/lavaland/mining_loot/megafauna/colossus.dm b/code/modules/mining/lavaland/mining_loot/megafauna/colossus.dm
index 7d4d87d13bd7..05f1c614b9bc 100644
--- a/code/modules/mining/lavaland/mining_loot/megafauna/colossus.dm
+++ b/code/modules/mining/lavaland/mining_loot/megafauna/colossus.dm
@@ -383,7 +383,6 @@
possessor.mind.transfer_to(holder_animal)
var/datum/action/exit_possession/escape = new(holder_animal)
escape.Grant(holder_animal)
- remove_verb(holder_animal, /mob/living/verb/pulled)
/obj/structure/closet/stasis/dump_contents(kill = TRUE)
for(var/mob/living/possessor in src)
diff --git a/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm b/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm
index cb8fa5532718..4ec0dbfe49e3 100644
--- a/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm
+++ b/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm
@@ -95,7 +95,7 @@
playsound(src, 'sound/effects/magic/lightningshock.ogg', 10, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0)
targeted_turfs += target_turf
balloon_alert(user, "you aim at [target_turf]...")
- new /obj/effect/temp_visual/telegraphing/thunderbolt(target_turf)
+ new /obj/effect/temp_visual/telegraphing/circle(target_turf)
addtimer(CALLBACK(src, PROC_REF(throw_thunderbolt), target_turf, power_boosted), 1.5 SECONDS)
thunder_charges--
addtimer(CALLBACK(src, PROC_REF(recharge)), thunder_charge_time)
diff --git a/code/modules/mining/ores_coins.dm b/code/modules/mining/ores_coins.dm
index 5857738adfc2..8f9658445583 100644
--- a/code/modules/mining/ores_coins.dm
+++ b/code/modules/mining/ores_coins.dm
@@ -182,6 +182,13 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
mine_experience = 0
merge_type = /obj/item/stack/ore/glass/basalt
+/obj/item/stack/ore/glass/siderite
+ name = "siderite dust"
+ icon_state = "siderite_sand"
+ singular_name = "siderite dust pile"
+ mine_experience = 0
+ merge_type = /obj/item/stack/ore/glass/siderite
+
/obj/item/stack/ore/plasma
name = "plasma ore"
icon_state = "plasma"
diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm
index 9d8977e9cf9f..ed32f7256ca4 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -365,13 +365,11 @@
*/
/mob/dead/new_player/proc/register_for_interview()
// First we detain them by removing all the verbs they have on client
- for (var/v in client.verbs)
- var/procpath/verb_path = v
+ for (var/procpath/verb_path as anything in client.verbs)
remove_verb(client, verb_path)
// Then remove those on their mob as well
- for (var/v in verbs)
- var/procpath/verb_path = v
+ for (var/procpath/verb_path as anything in verbs)
remove_verb(src, verb_path)
// Then we create the interview form and show it to the client
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 5d38c421e100..a8b9e7bd24eb 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -254,18 +254,22 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER)
lum = max(read_color[3], 80)
return rgb(read_color[1], sat, lum, space = COLORSPACE_HSL)
-/*
-Transfer_mind is there to check if mob is being deleted/not going to have a body.
-Works together with spawning an observer, noted above.
-*/
-
-/mob/proc/ghostize(can_reenter_corpse = TRUE, admin_ghost = FALSE)
+/**
+ * # Ghostize
+ *
+ * Creates a /mob/dead/observer and moves the player's key into it (among other handling for player->observer)
+ * Ignores things like adminghosts and corpselocked (ethereal) players.
+ * Args:
+ * can_reenter_corse: Whether the new Ghost will be able to click "Re-enter body", TRUE by default.
+ * forced: Whether we are forcing this player to be ghosted, ignoring things like corpselocking, FALSE by default.
+ */
+/mob/proc/ghostize(can_reenter_corpse = TRUE, forced = FALSE)
if(!key)
return
if(IS_FAKE_KEY(key)) // Skip aghosts.
return
- if(HAS_TRAIT(src, TRAIT_CORPSELOCKED) && !admin_ghost)
+ if(HAS_TRAIT(src, TRAIT_CORPSELOCKED) && !forced)
if(can_reenter_corpse) //If you can re-enter the corpse you can't leave when corpselocked
return
if(ishuman(usr)) //following code only applies to those capable of having an ethereal heart, ie humans
@@ -293,7 +297,7 @@ Works together with spawning an observer, noted above.
SEND_SIGNAL(src, COMSIG_MOB_GHOSTIZED)
return ghost
-/mob/living/ghostize(can_reenter_corpse = TRUE)
+/mob/living/ghostize(can_reenter_corpse = TRUE, forced = FALSE)
. = ..()
if(. && can_reenter_corpse)
var/mob/dead/observer/ghost = .
@@ -380,8 +384,8 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
return
client.view_size.resetToDefault()//Let's reset so people can't become allseeing gods
SStgui.on_transfer(src, mind.current) // Transfer NanoUIs.
- if(mind.current.stat == DEAD && SSlag_switch.measures[DISABLE_DEAD_KEYLOOP])
- to_chat(src, span_warning("To leave your body again use the Ghost verb."))
+ if(mind.current.stat == DEAD && SSlag_switch.measures[DISABLE_DEAD_KEYLOOP] && !client.holder)
+ to_chat(src, span_warning("To leave your body again use the 'Ghost verb' (in the command bar)."))
mind.current.PossessByPlayer(key)
mind.current.client.init_verbs()
return TRUE
diff --git a/code/modules/mob/dead/observer/observer_movement.dm b/code/modules/mob/dead/observer/observer_movement.dm
index 6972d3b265ff..cfe505fb0f79 100644
--- a/code/modules/mob/dead/observer/observer_movement.dm
+++ b/code/modules/mob/dead/observer/observer_movement.dm
@@ -1,14 +1,8 @@
/mob/dead/observer/down()
- set name = "Move Down"
- set category = "IC"
-
if(zMove(DOWN, z_move_flags = ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move down."))
/mob/dead/observer/up()
- set name = "Move Upwards"
- set category = "IC"
-
if(zMove(UP, z_move_flags = ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move upwards."))
diff --git a/code/modules/mob/eye/eye.dm b/code/modules/mob/eye/eye.dm
index fb3c2a4fab73..2b79ab2a19cc 100644
--- a/code/modules/mob/eye/eye.dm
+++ b/code/modules/mob/eye/eye.dm
@@ -27,16 +27,10 @@
return FALSE
/mob/eye/up()
- set name = "Move Upwards"
- set category = "IC"
-
if(zMove(UP, z_move_flags = ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move upwards."))
/mob/eye/down()
- set name = "Move Down"
- set category = "IC"
-
if(zMove(DOWN, z_move_flags = ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move down."))
diff --git a/code/modules/mob/living/basic/basic.dm b/code/modules/mob/living/basic/basic.dm
index 6378daf25599..641e2b7999ab 100644
--- a/code/modules/mob/living/basic/basic.dm
+++ b/code/modules/mob/living/basic/basic.dm
@@ -308,17 +308,6 @@
if (.)
update_held_items()
-/mob/living/basic/update_held_items()
- . = ..()
- if(isnull(client) || isnull(hud_used) || hud_used.hud_version == HUD_STYLE_NOHUD)
- return
- var/turf/our_turf = get_turf(src)
- for(var/obj/item/held in held_items)
- var/index = get_held_index_of_item(held)
- SET_PLANE(held, ABOVE_HUD_PLANE, our_turf)
- held.screen_loc = ui_hand_position(index)
- client.screen |= held
-
/mob/living/basic/get_body_temp_heat_damage_limit()
return maximum_survivable_temperature
diff --git a/code/modules/mob/living/basic/blob_minions/blob_mob.dm b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
index 8d24dccd7dfa..b13e9f693451 100644
--- a/code/modules/mob/living/basic/blob_minions/blob_mob.dm
+++ b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
@@ -21,6 +21,7 @@
initial_language_holder = /datum/language_holder/empty
can_buckle_to = FALSE
damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1)
+ pull_force = MOVE_FORCE_NONE
/// Size of cloud produced from a dying spore
var/death_cloud_size = BLOBMOB_CLOUD_NONE
var/loot = /obj/item/food/spore_sack
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm
index 7462974381e0..0e9e734a21de 100644
--- a/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm
@@ -12,16 +12,17 @@ Difficulty: Medium
/mob/living/basic/boss/blood_drunk_miner
name = "blood-drunk miner"
desc = "A miner destined to wander forever, engaged in an endless hunt."
- health = 900
- maxHealth = 900
+ health = 1300
+ maxHealth = 1300
icon_state = "miner"
icon_living = "miner"
+ base_icon_state = "miner"
icon = 'icons/mob/simple/broadMobs.dmi'
health_doll_icon = "miner"
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID|MOB_SPECIAL|MOB_MINING
light_color = COLOR_LIGHT_GRAYISH_RED
speak_emote = list("roars")
- speed = 3
+ speed = 2.5
pixel_x = -16
base_pixel_x = -16
basic_mob_flags = DEL_ON_DEATH
@@ -43,7 +44,7 @@ Difficulty: Medium
victor_memory_type = /datum/memory/megafauna_slayer
crusher_loot = list(/obj/item/crusher_trophy/miner_eye, /obj/item/knife/hunting/wildhunter)
- regular_loot = list(/obj/item/melee/cleaving_saw, /obj/item/gun/energy/recharge/kinetic_accelerator)
+ regular_loot = list(/obj/item/melee/cleaving_saw, /obj/item/gun/energy/recharge/kinetic_accelerator/bdm)
/// Their little saw
var/obj/item/melee/cleaving_saw/miner/miner_saw
@@ -87,9 +88,9 @@ Difficulty: Medium
/// Returns a list of innate actions for the blood-drunk miner.
/mob/living/basic/boss/blood_drunk_miner/proc/get_innate_actions()
- var/static/list/innate_abilities = list(
- /datum/action/cooldown/mob_cooldown/dash = BB_BDM_DASH_ABILITY,
- /datum/action/cooldown/mob_cooldown/projectile_attack/kinetic_accelerator = BB_BDM_KINETIC_ACCELERATOR_ABILITY,
+ var/list/innate_abilities = list(
+ /datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner = BB_BDM_DASH_ABILITY,
+ /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator = BB_BDM_KINETIC_ACCELERATOR_ABILITY,
/datum/action/cooldown/mob_cooldown/dash_attack = BB_BDM_DASH_ATTACK_ABILITY,
/datum/action/cooldown/mob_cooldown/transform_weapon = BB_BDM_TRANSFORM_WEAPON_ABILITY,
)
@@ -101,6 +102,13 @@ Difficulty: Medium
INVOKE_ASYNC(ai_controller.blackboard[BB_BDM_TRANSFORM_WEAPON_ABILITY], TYPE_PROC_REF(/datum/action, Trigger), src, NONE)
+/mob/living/basic/boss/blood_drunk_miner/proc/transform_saw()
+ miner_saw.attack_self(src)
+ var/saw_open = HAS_TRAIT(miner_saw, TRAIT_TRANSFORM_ACTIVE)
+ rapid_melee_hits = saw_open ? 3 : 5
+ icon_state = "[base_icon_state][saw_open ? "_transformed":""]"
+ icon_living = "[base_icon_state][saw_open ? "_transformed":""]"
+
/mob/living/basic/boss/blood_drunk_miner/ex_act(severity, target)
var/datum/action/cooldown/mob_cooldown/dash_ability = ai_controller.blackboard[BB_BDM_DASH_ABILITY]
if(dash_ability.Trigger(target = target))
@@ -145,28 +153,58 @@ Difficulty: Medium
/// Namely, we just use the miner saw to rapidly hit the target multiple times
/mob/living/basic/boss/blood_drunk_miner/proc/attack_override(mob/living/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
- if(!istype(target, /mob/living))
+
+ if(!isliving(target))
return
var/mob/living/victim = target
- if(should_devour(target))
- devour(target)
+ if(should_devour(victim))
+ devour(victim)
return COMPONENT_HOSTILE_NO_ATTACK
+ do_chain_attack(victim, modifiers)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/mob/living/basic/boss/blood_drunk_miner/proc/do_chain_attack(mob/living/victim, modifiers, sequence_hit = 1)
+ if (!Adjacent(victim))
+ post_attack_effects(victim, modifiers)
+ return
+
changeNext_move(CLICK_CD_MELEE)
victim.visible_message(
span_danger("[src] slashes at [victim] with [p_their()] cleaving saw!"),
span_userdanger("You are slashed at by [src]'s cleaving saw!"),
)
- var/datum/callback/melee_callback = CALLBACK(miner_saw, TYPE_PROC_REF(/obj/item/melee/cleaving_saw/miner, melee_attack_chain), src, victim, modifiers)
- var/delay = 0.2 SECONDS
- for(var/i in 1 to rapid_melee_hits)
- addtimer(melee_callback, (i - 1) * delay)
+ var/delay = HAS_TRAIT(miner_saw, TRAIT_TRANSFORM_ACTIVE) ? 0.5 SECONDS : 0.3 SECONDS
+ apply_status_effect(/datum/status_effect/saw_slashes_slowdown, delay)
+ INVOKE_ASYNC(miner_saw, TYPE_PROC_REF(/obj/item/melee/cleaving_saw/miner, melee_attack_chain), src, victim, modifiers)
- post_attack_effects(victim, modifiers)
+ if (sequence_hit >= rapid_melee_hits)
+ post_attack_effects(victim, modifiers)
+ return
- return COMPONENT_HOSTILE_NO_ATTACK
+ addtimer(CALLBACK(src, PROC_REF(do_chain_attack), victim, modifiers, sequence_hit + 1), delay)
+
+/datum/status_effect/saw_slashes_slowdown
+ id = "saw_slashes_slowdown"
+ alert_type = null
+ status_type = STATUS_EFFECT_REFRESH
+
+/datum/status_effect/saw_slashes_slowdown/on_creation(mob/living/new_owner, new_duration)
+ duration = new_duration
+ return ..()
+
+/datum/status_effect/saw_slashes_slowdown/refresh(effect, new_duration)
+ duration = new_duration
+
+/datum/status_effect/saw_slashes_slowdown/on_apply()
+ . = ..()
+ owner.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/saw_slashes_slowdown)
+
+/datum/status_effect/saw_slashes_slowdown/on_remove()
+ . = ..()
+ owner.remove_movespeed_modifier(/datum/movespeed_modifier/status_effect/saw_slashes_slowdown)
/// Hook for potential additional behaviors after attacking
/mob/living/basic/boss/blood_drunk_miner/proc/post_attack_effects(mob/living/victim, list/modifiers)
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_actions.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_actions.dm
new file mode 100644
index 000000000000..468d01ce3a98
--- /dev/null
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_actions.dm
@@ -0,0 +1,82 @@
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner
+ cooldown_time = 1.5 SECONDS
+ charge_delay = 0.1 SECONDS
+ shake_duration = 0.2 SECONDS // A bit longer so he shakes during the dash too
+ charge_distance = 6
+ // Don't stun ourselves or the target
+ recoil_duration = -1
+ knockdown_duration = -1
+ destroy_objects = FALSE
+ charge_damage = 0
+ charge_speed = 0.3
+
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner/hit_target(atom/movable/source, atom/target, damage_dealt)
+ . = ..()
+ if(!isbasicmob(source) || !isliving(target))
+ return
+ var/mob/living/basic/basic_source = source
+ basic_source.melee_attack(target, ignore_cooldown = TRUE)
+
+/datum/action/cooldown/mob_cooldown/transform_weapon
+ name = "Transform Weapon"
+ button_icon = 'icons/obj/mining_zones/artefacts.dmi'
+ button_icon_state = "cleaving_saw"
+ desc = "Transform weapon into a different state."
+ cooldown_time = 5 SECONDS
+ shared_cooldown = MOB_SHARED_COOLDOWN_2
+ /// The max possible cooldown, cooldown is random between the default cooldown time and this
+ var/max_cooldown_time = 10 SECONDS
+
+/datum/action/cooldown/mob_cooldown/transform_weapon/Activate(atom/target_atom)
+ disable_cooldown_actions()
+ do_transform()
+ StartCooldown(rand(cooldown_time, max_cooldown_time), 0)
+ enable_cooldown_actions()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/transform_weapon/proc/do_transform()
+ if(!istype(owner, /mob/living/basic/boss/blood_drunk_miner))
+ return
+ var/mob/living/basic/boss/blood_drunk_miner/blood_drunk_miner = owner
+ blood_drunk_miner.transform_saw()
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator
+ name = "Fire Kinetic Accelerator"
+ desc = "Fires a kinetic accelerator projectile at the target."
+ button_icon = 'icons/obj/weapons/guns/energy.dmi'
+ button_icon_state = "kineticgun"
+ cooldown_time = 1.5 SECONDS
+ projectile_type = /obj/projectile/kinetic/miner
+ projectile_sound = 'sound/items/weapons/kinetic_accel.ogg'
+ shot_count = 3
+ shot_delay = 0.15 SECONDS
+ default_projectile_spread = 10
+ can_move = FALSE
+ /// Delay for the alert
+ var/alert_delay = 0.5 SECONDS
+ /// Delay before we start shooting during which we cannot move
+ var/prefire_delay = 0.2 SECONDS
+ /// Delay before the user can move or act again after firing
+ var/reload_delay = 0.1 SECONDS
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/Activate(atom/target_atom)
+ owner.visible_message(span_danger("[owner] fires the proto-kinetic accelerator!"))
+ owner.face_atom(target_atom)
+ owner.do_alert_animation(alert_delay + (shot_count - 1) * shot_delay)
+ disable_cooldown_actions()
+ if (alert_delay > prefire_delay) // As to delay movement blocking
+ SLEEP_CHECK_DEATH(alert_delay - prefire_delay, owner)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/attack_sequence(mob/living/firer, atom/target)
+ SLEEP_CHECK_DEATH(prefire_delay, firer)
+ . = ..()
+ sleep(reload_delay)
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/shoot_projectile(atom/origin, atom/target, set_angle, mob/firer, projectile_spread, speed_multiplier, override_projectile_type, override_homing)
+ . = ..()
+ new /obj/effect/temp_visual/dir_setting/firing_effect(get_turf(firer), firer.dir)
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/long_burst
+ shot_count = 5
+ shot_delay = 0.1 SECONDS
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_ai.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_ai.dm
index f41400aa52a0..64efdb0b8a96 100644
--- a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_ai.dm
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_ai.dm
@@ -12,7 +12,7 @@
BB_BDM_RANGED_ATTACK_COOLDOWN = 0,
)
- movement_delay = 0.3 SECONDS
+ movement_delay = 0.25 SECONDS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
@@ -63,4 +63,4 @@
return TRUE
/datum/ai_controller/blood_drunk_miner/doom
- movement_delay = 0.8 SECONDS
+ movement_delay = 0.5 SECONDS
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_objects.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_objects.dm
index 81b49ea593a7..df5028557705 100644
--- a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_objects.dm
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_objects.dm
@@ -1,15 +1,10 @@
/// A slightly nerfed saw as the normal one is much too murdery.
/obj/item/melee/cleaving_saw/miner
- force = 6
- open_force = 10
-
-/obj/item/melee/cleaving_saw/miner/attack(mob/living/target, mob/living/carbon/human/user)
- target.add_stun_absorption(source = "miner", duration = 1 SECONDS, priority = INFINITY)
- return ..()
+ force = 8
+ open_force = 12
/obj/projectile/kinetic/miner
- damage = 20
- speed = 1.1
+ damage = 18
icon_state = "ka_tracer"
range = 4
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_subtypes.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_subtypes.dm
index a9e6c4d50b11..414fafe2b49f 100644
--- a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_subtypes.dm
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_subtypes.dm
@@ -1,6 +1,6 @@
#define HUNTER_DASH_PROBABILITY 12
-/// heals slightly on melee hits
+/// Heals slightly on melee hits
/mob/living/basic/boss/blood_drunk_miner/guidance
/mob/living/basic/boss/blood_drunk_miner/guidance/attack_override(mob/living/source, atom/target, proximity, modifiers)
@@ -19,10 +19,11 @@
if(!isnull(dash_attack))
INVOKE_ASYNC(dash_attack, TYPE_PROC_REF(/datum/action, Trigger), src, NONE, victim)
+/// Slow but constantly dashes and has longer barrages
/mob/living/basic/boss/blood_drunk_miner/doom
name = "hostile-environment miner"
desc = "A miner destined to hop across dimensions for all eternity, hunting anomalous creatures."
- speed = 8
+ speed = 5
ranged_attack_cooldown_duration = 0.8 SECONDS
ai_controller = /datum/ai_controller/blood_drunk_miner/doom
@@ -32,4 +33,17 @@
if(!isnull(dash_ability))
dash_ability.cooldown_time = 0.8 SECONDS
+ var/datum/action/cooldown/dash_attack = ai_controller.blackboard[BB_BDM_DASH_ATTACK_ABILITY]
+ if(!isnull(dash_attack))
+ dash_attack.cooldown_time = 4 SECONDS
+
+/mob/living/basic/boss/blood_drunk_miner/doom/get_innate_actions()
+ var/list/innate_abilities = list(
+ /datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner = BB_BDM_DASH_ABILITY,
+ /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/long_burst = BB_BDM_KINETIC_ACCELERATOR_ABILITY,
+ /datum/action/cooldown/mob_cooldown/dash_attack = BB_BDM_DASH_ATTACK_ABILITY,
+ /datum/action/cooldown/mob_cooldown/transform_weapon = BB_BDM_TRANSFORM_WEAPON_ABILITY,
+ )
+ return innate_abilities
+
#undef HUNTER_DASH_PROBABILITY
diff --git a/code/modules/mob/living/basic/boss/thing/thing_ai.dm b/code/modules/mob/living/basic/boss/thing/thing_ai.dm
index be6033ec2b26..e8956cac2b13 100644
--- a/code/modules/mob/living/basic/boss/thing/thing_ai.dm
+++ b/code/modules/mob/living/basic/boss/thing/thing_ai.dm
@@ -5,14 +5,15 @@
BB_THETHING_ATTACKMODE = TRUE, //Whether we are using our melee abilities right now
BB_THETHING_NOAOE = TRUE, // Restricts us to only melee abilities
BB_THETHING_LASTAOE = null, // Last AOE ability key executed
- BB_AGGRO_RANGE = 6, //lets not execute hearers for a 16 tile radius
+ BB_AGGRO_RANGE = 16,
+ BB_AGGRO_GRAB_RANGE = 6,
)
ai_movement = /datum/ai_movement/basic_avoidance // dont need anything better because the arena is a square lol
idle_behavior = null
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
- /datum/ai_planning_subtree/simple_find_target/increased_range, //aggros at 6, sees 16 tiles
+ /datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/thing_boss_aoe,
/datum/ai_planning_subtree/thing_boss_melee,
)
diff --git a/code/modules/mob/living/basic/bots/bot_ai.dm b/code/modules/mob/living/basic/bots/bot_ai.dm
index 6f726c9586c9..a5f8202e66e7 100644
--- a/code/modules/mob/living/basic/bots/bot_ai.dm
+++ b/code/modules/mob/living/basic/bots/bot_ai.dm
@@ -63,6 +63,8 @@
source.clear_path_hud(remove_hud = FALSE)
/datum/ai_controller/basic_controller/bot/proc/add_to_blacklist(atom/target, duration)
+ if(QDELETED(target))
+ return
var/final_duration = duration || blackboard[BB_UNREACHABLE_LIST_COOLDOWN]
set_blackboard_key_assoc_lazylist(BB_TEMPORARY_IGNORE_LIST, target, TRUE)
addtimer(CALLBACK(src, PROC_REF(remove_from_blacklist), target), final_duration)
@@ -107,11 +109,15 @@
///set the target if we can reach them
/datum/ai_controller/basic_controller/bot/proc/set_if_can_reach(key, target, duration, distance = 10, bypass_add_to_blacklist = FALSE)
if(can_reach_target(target, distance))
+ EVLOG_MAPTEXT(src, EVLOG_CATEGORY_AI_TARGETING, "[pawn] has selected [target] as a target for blackboard key [key]!", get_turf(target), "Target: [target]")
+ EVLOG_LINES(src, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(pawn), get_turf(target))
set_blackboard_key(key, target)
return TRUE
if(bypass_add_to_blacklist)
return FALSE
var/final_duration = duration || blackboard[BB_UNREACHABLE_LIST_COOLDOWN]
+ EVLOG_MAPTEXT(src, EVLOG_CATEGORY_AI_TARGETING, "[pawn] has added [target] to its targetting blacklist!", get_turf(target), "Target: [target]")
+ EVLOG_LINES(src, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(pawn), get_turf(target))
add_to_blacklist(target, final_duration)
return FALSE
diff --git a/code/modules/mob/living/basic/bots/honkbots/honkbot.dm b/code/modules/mob/living/basic/bots/honkbots/honkbot.dm
index 9d7abf3c1f63..5daa8e5b25b7 100644
--- a/code/modules/mob/living/basic/bots/honkbots/honkbot.dm
+++ b/code/modules/mob/living/basic/bots/honkbots/honkbot.dm
@@ -26,8 +26,6 @@
baton_type = /obj/item/bikehorn/airhorn
cuff_type = /obj/item/restraints/handcuffs/cable/zipties/fake
-
-
/mob/living/basic/bot/secbot/honkbot/Initialize(mapload)
. = ..()
var/static/list/clown_friends = typecacheof(list(
@@ -121,3 +119,6 @@
honkbot_assembly.created_name = name
new /obj/item/assembly/prox_sensor(drop_location)
drop_part(baton_type, drop_location)
+
+/mob/living/basic/bot/secbot/honkbot/nopatrol
+ bot_mode_flags = parent_type::bot_mode_flags & ~BOT_MODE_AUTOPATROL
diff --git a/code/modules/mob/living/basic/clown/clown.dm b/code/modules/mob/living/basic/clown/clown.dm
index 7808c45f72fc..9c8ec1e8bb7d 100644
--- a/code/modules/mob/living/basic/clown/clown.dm
+++ b/code/modules/mob/living/basic/clown/clown.dm
@@ -57,7 +57,7 @@
if (!istype(attacker))
return
for (var/mob/living/basic/clown/harbringer in oview(src, 7))
- harbringer.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
+ harbringer.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)
/mob/living/basic/clown/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
if(!istype(target, /obj/item/food/grown/banana/bunch))
diff --git a/code/modules/mob/living/basic/drone/inventory.dm b/code/modules/mob/living/basic/drone/inventory.dm
index acacdb0e2659..8253fafb98aa 100644
--- a/code/modules/mob/living/basic/drone/inventory.dm
+++ b/code/modules/mob/living/basic/drone/inventory.dm
@@ -58,7 +58,7 @@
if(equipping.pulledby)
equipping.pulledby.stop_pulling()
- equipping.screen_loc = null // will get moved if inventory is visible
+ hud_used?.update_inventory_slot(slot)
equipping.forceMove(src) //This has to come before has_equipped is called.
SET_PLANE_EXPLICIT(equipping, ABOVE_HUD_PLANE, src)
diff --git a/code/modules/mob/living/basic/drone/visuals_icons.dm b/code/modules/mob/living/basic/drone/visuals_icons.dm
index 59952d8d2490..3714c27decab 100644
--- a/code/modules/mob/living/basic/drone/visuals_icons.dm
+++ b/code/modules/mob/living/basic/drone/visuals_icons.dm
@@ -22,18 +22,13 @@
update_inv_internal_storage()
/mob/living/basic/drone/proc/update_inv_internal_storage()
- if(internal_storage && client && hud_used?.hud_shown)
- internal_storage.screen_loc = ui_drone_storage
- client.screen += internal_storage
-
+ hud_used?.update_inventory_slot(ITEM_SLOT_DEX_STORAGE)
/mob/living/basic/drone/update_worn_head()
remove_overlay(DRONE_HEAD_LAYER)
+ hud_used?.update_inventory_slot(ITEM_SLOT_HEAD)
if(head)
- if(client && hud_used?.hud_shown)
- head.screen_loc = ui_drone_head
- client.screen += head
var/used_head_icon = 'icons/mob/clothing/head/utility.dmi'
if(istype(head, /obj/item/clothing/mask))
used_head_icon = 'icons/mob/clothing/mask.dmi'
diff --git a/code/modules/mob/living/basic/farm_animals/bee/_bee.dm b/code/modules/mob/living/basic/farm_animals/bee/_bee.dm
index 66cd498ccd60..c3a1c5d91395 100644
--- a/code/modules/mob/living/basic/farm_animals/bee/_bee.dm
+++ b/code/modules/mob/living/basic/farm_animals/bee/_bee.dm
@@ -199,7 +199,7 @@
/// Picks a random toxin and assigns it to the bee
/mob/living/basic/bee/proc/assign_random_toxin_reagent()
- assign_reagent(get_random_reagent_id(whitelist = subtypesof(/datum/reagent/toxin)))
+ assign_reagent(GLOB.chemical_reagents_list[get_random_reagent_id(whitelist = subtypesof(/datum/reagent/toxin))])
/mob/living/basic/bee/mutate()
. = ..()
diff --git a/code/modules/mob/living/basic/guardian/guardian_creator.dm b/code/modules/mob/living/basic/guardian/guardian_creator.dm
index ff83c69a75e8..b95b424af9e8 100644
--- a/code/modules/mob/living/basic/guardian/guardian_creator.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_creator.dm
@@ -25,6 +25,10 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
var/use_message = span_holoparasite("You shuffle the deck...")
/// Message sent when it's already used.
var/used_message = span_holoparasite("All the cards seem to be blank now.")
+ /// Examine description if the creator is unused
+ var/unused_description = span_holoparasite("You feel beckoned to draw one...")
+ /// Examine description if the creator is used.
+ var/used_description = span_holoparasite("They seem rather uninteresting.")
/// Failure message if no ghost picks the holopara.
var/failure_message = span_boldholoparasite("..And draw a card! It's... blank? Maybe you should try again later.")
/// Failure message if we don't allow lings.
@@ -113,6 +117,13 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
to_chat(user, failure_message)
used = FALSE
+/obj/item/guardian_creator/examine(mob/user)
+ . = ..()
+ if(used)
+ . += span_holoparasite(used_description)
+ else
+ . += span_holoparasite(unused_description)
+
/obj/item/guardian_creator/proc/on_reimbursed(datum/source)
SIGNAL_HANDLER
was_refunded = TRUE
@@ -185,6 +196,8 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
allow_changeling = FALSE
use_message = span_holoparasite("You start to power on the injector...")
used_message = span_holoparasite("The injector has already been used.")
+ unused_description = span_holoparasite("Its vial is filled with a violent storm of color.")
+ used_description = span_holoparasite("Nothing seems to be loaded in the injector.")
failure_message = span_boldholoparasite("...ERROR. BOOT SEQUENCE ABORTED. AI FAILED TO INTIALIZE. PLEASE CONTACT SUPPORT OR TRY AGAIN LATER.")
ling_failure = span_boldholoparasite("The holoparasites recoil in horror. They want nothing to do with a creature like you.")
success_message = span_holoparasite("%GUARDIAN is now online!")
@@ -198,6 +211,8 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
theme = GUARDIAN_THEME_CARP
use_message = span_holoparasite("You put the fishsticks in your mouth...")
used_message = span_holoparasite("Someone's already taken a bite out of these fishsticks! Ew.")
+ unused_description = span_holoparasite("They look hot and ready to eat!")
+ used_description = span_holoparasite("They look soggy and old...")
failure_message = span_boldholoparasite("You couldn't catch any carp spirits from the seas of Lake Carp. Maybe there are none, maybe you fucked up.")
ling_failure = span_boldholoparasite("Carp'sie seems to not have taken you as the chosen one. Maybe it's because of your horrifying origin.")
success_message = span_holoparasite("%GUARDIAN has been caught!")
@@ -212,6 +227,8 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
theme = GUARDIAN_THEME_MINER
use_message = span_holoparasite("You pierce your skin with the shard...")
used_message = span_holoparasite("This shard seems to have lost all its power...")
+ unused_description = span_holoparasite("It glows with an otherwordly power...")
+ used_description = span_holoparasite("It looks dull, with dried blood on the tip.")
failure_message = span_boldholoparasite("The shard hasn't reacted at all. Maybe try again later...")
ling_failure = span_boldholoparasite("The power of the shard seems to not react with your horrifying, mutated body.")
success_message = span_holoparasite("%GUARDIAN has appeared!")
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm b/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm
index fc4ec6788373..b3d37a28e9e5 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm
@@ -92,7 +92,7 @@
if(equipping.pulledby)
equipping.pulledby.stop_pulling()
- equipping.screen_loc = null // will get moved if inventory is visible
+ hud_used?.update_inventory_slot(slot)
equipping.forceMove(src)
SET_PLANE_EXPLICIT(equipping, ABOVE_HUD_PLANE, src)
@@ -106,10 +106,7 @@
return ITEM_SLOT_DEX_STORAGE
/mob/living/basic/guardian/dextrous/proc/update_inv_internal_storage()
- if(isnull(internal_storage) || isnull(client) || !hud_used?.hud_shown)
- return
- internal_storage.screen_loc = ui_back
- client.screen += internal_storage
+ hud_used?.update_inventory_slot(ITEM_SLOT_DEX_STORAGE)
/mob/living/basic/guardian/dextrous/regenerate_icons()
update_inv_internal_storage()
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm b/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm
index a70bd01f43bc..7db7107e1099 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm
@@ -33,20 +33,15 @@
. = ..()
if (QDELETED(src))
return
- RegisterSignal(summoner, COMSIG_LIVING_IGNITED, PROC_REF(on_summoner_ignited))
+ ADD_TRAIT(summoner, TRAIT_NOFIRE, REF(src))
RegisterSignal(summoner, COMSIG_LIVING_LIFE, PROC_REF(on_summoner_life))
/mob/living/basic/guardian/gaseous/cut_summoner(different_person)
if (!isnull(summoner))
- UnregisterSignal(summoner, list(COMSIG_LIVING_IGNITED, COMSIG_LIVING_LIFE))
+ REMOVE_TRAIT(summoner, TRAIT_NOFIRE, REF(src))
+ UnregisterSignal(summoner, COMSIG_LIVING_LIFE)
return ..()
-/// Prevent our summoner from being on fire
-/mob/living/basic/guardian/gaseous/proc/on_summoner_ignited(mob/living/source)
- SIGNAL_HANDLER
- source.extinguish_mob()
- source.set_fire_stacks(0, remove_wet_stacks = FALSE)
-
/// Maintain our summoner at a stable body temperature
/mob/living/basic/guardian/gaseous/proc/on_summoner_life(mob/living/source, seconds_per_tick)
SIGNAL_HANDLER
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/support.dm b/code/modules/mob/living/basic/guardian/guardian_types/support.dm
index 5ddc07e809b3..503dc93b7787 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/support.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/support.dm
@@ -112,6 +112,8 @@
/// Validate whether we can teleport this object
/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/proc/can_teleport(mob/living/source, atom/movable/target)
+ if(!istype(target)) // Turfs
+ return FALSE
if (isnull(beacon))
source.balloon_alert(source, "no beacon!")
return FALSE
@@ -126,7 +128,7 @@
if (target.anchored)
target.balloon_alert(source, "it won't budge!")
return FALSE
- if(beacon.z != target.z)
+ if((beacon.z != target.z) && !(target.z in SSmapping.get_connected_levels(beacon.z)))
target.balloon_alert(source, "too far from beacon!")
return FALSE
return TRUE
diff --git a/code/modules/mob/living/basic/icemoon/polar_bear/polar_bear.dm b/code/modules/mob/living/basic/icemoon/polar_bear/polar_bear.dm
new file mode 100644
index 000000000000..b1dc69d06bf0
--- /dev/null
+++ b/code/modules/mob/living/basic/icemoon/polar_bear/polar_bear.dm
@@ -0,0 +1,72 @@
+// The taxonomy of polar vs space bear left to imagination of the reader
+/mob/living/basic/mining/polarbear
+ name = "polar bear"
+ desc = "An aggressive animal that defends its territory with incredible power. These beasts don't run from their enemies."
+ icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
+ icon_state = "polarbear"
+ icon_living = "polarbear"
+ icon_dead = "polarbear_dead"
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST|MOB_MINING
+
+ friendly_verb_continuous = "growls at"
+ friendly_verb_simple = "growl at"
+ response_help_continuous = "pets"
+ response_help_simple = "pet"
+ response_disarm_continuous = "gently pushes aside"
+ response_disarm_simple = "gently push aside"
+ attack_verb_continuous = "claws"
+ attack_verb_simple = "claw"
+ attack_sound = 'sound/items/weapons/bladeslice.ogg'
+ attack_vis_effect = ATTACK_EFFECT_CLAW
+
+ habitable_atmos = null
+ speed = 2
+ maxHealth = 300
+ health = 300
+ obj_damage = 40
+ melee_damage_lower = 25
+ melee_damage_upper = 25
+ wound_bonus = -5
+ exposed_wound_bonus = 10
+ sharpness = SHARP_EDGED
+
+ move_force = MOVE_FORCE_VERY_STRONG
+ move_resist = MOVE_FORCE_VERY_STRONG
+ pull_force = MOVE_FORCE_VERY_STRONG
+ butcher_results = list(/obj/item/food/meat/slab/bear = 3, /obj/item/stack/sheet/bone = 2)
+ guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/goliath_hide/polar_bear_hide = 1)
+ crusher_loot = /obj/item/crusher_trophy/bear_paw
+
+ ai_controller = /datum/ai_controller/basic_controller/polar
+
+/mob/living/basic/mining/polarbear/Initialize(mapload)
+ . = ..()
+
+ add_traits(list(TRAIT_SPACEWALK, TRAIT_SWIMMER, TRAIT_FENCE_CLIMBER, TRAIT_SNOWSTORM_IMMUNE), INNATE_TRAIT)
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BEAR, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+ AddElement(/datum/element/change_force_on_death, move_force = MOVE_FORCE_DEFAULT, move_resist = MOVE_RESIST_DEFAULT, pull_force = PULL_FORCE_DEFAULT)
+ AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_CLAW)
+
+/mob/living/basic/mining/polarbear/lesser
+ name = "magic polar bear"
+ desc = "It seems sentient somehow."
+ faction = list(FACTION_NEUTRAL)
+
+/datum/ai_controller/basic_controller/polar
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ BB_AGGRO_RANGE = 2,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/enrage,
+ /datum/ai_planning_subtree/escape_captivity,
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/random_speech/bear,
+ )
diff --git a/code/modules/mob/living/basic/lavaland/bileworm/_bileworm.dm b/code/modules/mob/living/basic/lavaland/bileworm/_bileworm.dm
index 0ed4359bf238..19d1fd20c6ae 100644
--- a/code/modules/mob/living/basic/lavaland/bileworm/_bileworm.dm
+++ b/code/modules/mob/living/basic/lavaland/bileworm/_bileworm.dm
@@ -6,8 +6,8 @@
icon_living = "bileworm"
icon_dead = "bileworm_dead"
mob_biotypes = MOB_ORGANIC|MOB_BUG|MOB_MINING
- maxHealth = 100
- health = 100
+ maxHealth = 110
+ health = 110
verb_say = "spittles"
verb_ask = "spittles questioningly"
verb_exclaim = "splutters and gurgles"
@@ -30,10 +30,16 @@
ai_controller = /datum/ai_controller/basic_controller/bileworm
- ///which action this mob will be given, subtypes have different attacks
- var/attack_action_path = /datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm
- ///which, if at all, mob this evolves into at the 30 min mark
+ /// Which action this mob will be given, subtypes have different attacks
+ var/attack_action_path = /datum/action/cooldown/mob_cooldown/bileworm_spew
+ /// Which, if at all, mob this evolves into at the 30 min mark
var/evolve_path = /mob/living/basic/mining/bileworm/vileworm
+ /// Emissive icon_state
+ var/emissive_state = "bileworm_e"
+ /// Icon file used for the jumping animation
+ var/icon_jump = 'icons/mob/simple/lavaland/bileworm_jump.dmi'
+ /// Are we currently in the middle of a jump?
+ var/jumping = FALSE
/mob/living/basic/mining/bileworm/Initialize(mapload)
. = ..()
@@ -42,11 +48,7 @@
if(ispath(evolve_path))
AddComponent(/datum/component/evolutionary_leap, 30 MINUTES, evolve_path)
AddElement(/datum/element/content_barfer)
-
- //setup mob abilities
-
- //well, one of them has to start on infinite cooldown
- var/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/spew_bile = new(src)
+ var/datum/action/cooldown/mob_cooldown/spew_bile = new attack_action_path(src)
spew_bile.Grant(src)
spew_bile.StartCooldownSelf(INFINITY)
ai_controller?.set_blackboard_key(BB_BILEWORM_SPEW_BILE, spew_bile)
@@ -61,5 +63,10 @@
/mob/living/basic/mining/bileworm/update_overlays()
. = ..()
- if (stat != DEAD)
- . += emissive_appearance(icon, "[icon_living]_e", src)
+ if (stat != DEAD && !jumping)
+ . += emissive_appearance(icon, emissive_state, src)
+
+/mob/living/basic/mining/bileworm/on_attacked(datum/source, atom/attacker, attack_flags)
+ . = ..()
+ if (!(attack_flags & (ATTACKER_STAMINA_ATTACK | ATTACKER_SHOVING)))
+ ai_controller?.set_blackboard_key(BB_BILEWORM_SCARED, TRUE)
diff --git a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_actions.dm b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_actions.dm
index bd480cc6972e..d3a45a89a762 100644
--- a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_actions.dm
+++ b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_actions.dm
@@ -2,21 +2,70 @@
name = "Resurface"
desc = "Burrow underground, and then move to a new location near your target. Must spew bile to refresh."
shared_cooldown = MOB_SHARED_COOLDOWN_1 | MOB_SHARED_COOLDOWN_2
+ /// Damage tracker var for bileworms
+ var/jump_damaged = FALSE
+ /// How long does the jump take to perform?
+ var/jump_length = 1 SECONDS
+ /// How long do we get stunned for if we fail a jump?
+ var/jump_stun = 2.4 SECONDS
+
+/datum/action/cooldown/mob_cooldown/resurface/Grant(mob/granted_to)
+ . = ..()
+ owner?.AddElement(/datum/element/relay_attackers)
/datum/action/cooldown/mob_cooldown/resurface/Activate(atom/target_atom)
StartCooldownSelf(INFINITY)
+ StartCooldownOthers(INFINITY)
burrow(owner, target_atom)
//spew now off cooldown shortly
StartCooldownOthers(1.5 SECONDS)
-/datum/action/cooldown/mob_cooldown/resurface/proc/burrow(mob/living/burrower, atom/target)
+/// Amount of frames in the jump animation
+#define BILEWORM_JUMP_FRAMES 14
+
+/datum/action/cooldown/mob_cooldown/resurface/proc/burrow(mob/living/burrower, atom/target, force = FALSE)
var/turf/unburrow_turf = get_unburrow_turf(burrower, target)
- if(!unburrow_turf) // means all the turfs nearby are station turfs or something, not lavaland
+ if (!unburrow_turf) // means all the turfs nearby are station turfs or something, not lavaland
to_chat(burrower, span_warning("Couldn't burrow anywhere near the target!"))
if(burrower.ai_controller?.ai_status == AI_STATUS_ON)
//this is a valid reason to give up on a target
burrower.ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
return
+
+ if (istype(burrower, /mob/living/basic/mining/bileworm) && !force)
+ var/mob/living/basic/mining/bileworm/worm = burrower
+ new /obj/effect/temp_visual/mook_dust(get_turf(worm))
+ var/old_icon_state = worm.icon_state
+ worm.icon_state = null
+ // Get rid of the base emissive
+ worm.jumping = TRUE
+ worm.update_appearance(UPDATE_OVERLAYS)
+ var/atom/movable/visual = worm.flick_overlay_view(mutable_appearance(worm.icon_jump, "[worm.icon_living]_jump_1"), jump_length)
+ visual.vis_flags |= VIS_INHERIT_ID // So you can attack it
+ var/atom/movable/emissive_visual = worm.flick_overlay_view(emissive_appearance(worm.icon_jump, "[worm.emissive_state]_jump_1", worm), jump_length)
+ // We have to manually animate these to get around BYOND icon_state animations happening in world time and not per-object
+ for (var/i in 2 to BILEWORM_JUMP_FRAMES)
+ animate(visual, time = jump_length / BILEWORM_JUMP_FRAMES, icon_state = "[worm.icon_living]_jump_[i]", flags = ANIMATION_CONTINUE)
+ animate(emissive_visual, time = jump_length / BILEWORM_JUMP_FRAMES, icon_state = "[worm.emissive_state]_jump_[i]", flags = ANIMATION_CONTINUE)
+ jump_damaged = FALSE
+ RegisterSignal(worm, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
+ // Not in an if check direclty to reduce duplicate code
+ var/jump_result = do_after(worm, jump_length, worm, hidden = TRUE, extra_checks = CALLBACK(src, PROC_REF(damage_check)))
+ UnregisterSignal(worm, COMSIG_ATOM_WAS_ATTACKED)
+ if (worm.icon_state == null)
+ worm.icon_state = old_icon_state
+ worm.jumping = FALSE
+ worm.update_appearance(UPDATE_OVERLAYS)
+ if (!jump_result && jump_stun)
+ if (!QDELETED(visual))
+ QDEL_NULL(visual)
+ QDEL_NULL(emissive_visual)
+ worm.flick_overlay_view(mutable_appearance('icons/effects/effects.dmi', "dazed"), jump_stun)
+ worm.Stun(jump_stun)
+ addtimer(CALLBACK(src, PROC_REF(burrow_again), burrower, target), jump_stun)
+ return
+
+ burrower.ai_controller?.set_blackboard_key(BB_BILEWORM_SCARED, FALSE)
playsound(burrower, 'sound/effects/break_stone.ogg', 50, TRUE)
new /obj/effect/temp_visual/mook_dust(get_turf(burrower))
ADD_TRAIT(burrower, TRAIT_GODMODE, REF(src))
@@ -29,59 +78,189 @@
REMOVE_TRAIT(burrower, TRAIT_GODMODE, REF(src))
burrower.RemoveInvisibility(type)
+#undef BILEWORM_JUMP_FRAMES
+
+/datum/action/cooldown/mob_cooldown/resurface/proc/burrow_again(mob/living/burrower, atom/target)
+ if (!QDELETED(burrower) && !burrower.stat)
+ // Burrow immediatelly after being stunned out of the first jump to avoid chainstuns
+ burrow(burrower, target, force = TRUE)
+
+/datum/action/cooldown/mob_cooldown/resurface/proc/on_attacked(datum/source, atom/attacker, attack_flags)
+ SIGNAL_HANDLER
+ if (!(attack_flags & (ATTACKER_STAMINA_ATTACK | ATTACKER_SHOVING)) && jump_stun)
+ jump_damaged = TRUE
+
+/datum/action/cooldown/mob_cooldown/resurface/proc/damage_check()
+ return !jump_damaged
+
/datum/action/cooldown/mob_cooldown/resurface/proc/get_unburrow_turf(mob/living/burrower, atom/target)
- //we want the worm to try guaranteeing a hit on a living target if it thinks it can
- var/cardinal_only = FALSE
-
- if(isliving(target))
- var/mob/living/living_target = target
- if(living_target.stat >= UNCONSCIOUS)
- cardinal_only = TRUE
-
- var/list/potential_turfs = shuffle(oview(5, target))//get in view, shuffle
- var/list/fallback_turfs = list()
- for(var/turf/open/misc/chosen_one in potential_turfs)//first turf that counts as ground
- if(cardinal_only && !(get_dir(chosen_one, target) in GLOB.cardinals))
- fallback_turfs.Add(chosen_one)
- continue
+ var/list/potential_turfs = shuffle(oview(5, target)) // get in view, shuffle
+ for(var/turf/open/misc/chosen_one in potential_turfs) // first turf that counts as ground
return chosen_one
- //even if a worm can't execute someone in crit, it should not fail if it has SOMETHING to move to.
- if(fallback_turfs.len)
- return pick(fallback_turfs)
-/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm
+/datum/action/cooldown/mob_cooldown/bileworm_spew
name = "Spew Bile"
- desc = "Spews bile everywhere. Must resurface after use to refresh."
- projectile_type = /obj/projectile/bileworm_acid
- projectile_sound = 'sound/mobs/non-humanoids/bileworm/bileworm_spit.ogg'
+ desc = "Spew a barrage of bile globs."
shared_cooldown = MOB_SHARED_COOLDOWN_1 | MOB_SHARED_COOLDOWN_2
+ cooldown_time = 3 SECONDS
+ /// Sound played when firing a projectile
+ var/projectile_sound = 'sound/mobs/non-humanoids/bileworm/bileworm_spit.ogg'
+ /// How many additional projectiles to shoot around the target?
+ var/additional_shots = 4
+ /// Delay between each shot
+ var/shot_delay = 0.15 SECONDS
+ /// Effect to spawn
+ var/obj/effect/bileworm_acid/acid_type = /obj/effect/bileworm_acid
-/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/Activate(atom/target_atom)
+/datum/action/cooldown/mob_cooldown/bileworm_spew/Activate(atom/target_atom)
StartCooldownSelf(INFINITY)
attack_sequence(owner, target_atom)
- //resurface now off cooldown shortly
- StartCooldownOthers(2.5 SECONDS)
+ StartCooldownSelf()
+ // Resurface now off cooldown shortly
+ StartCooldownOthers(2 SECONDS) // Enough time for a mark + detonation combo, or 3 shots with a PKA
+
+/datum/action/cooldown/mob_cooldown/bileworm_spew/proc/attack_sequence(mob/living/firer, atom/target)
+ new acid_type(firer, target)
+ playsound(firer, projectile_sound, 70)
+ var/list/all_dirs = GLOB.alldirs.Copy()
+ var/turf/hit_turf = get_turf(target)
+ // One guaranteed to hit target's turf, the rest hit randomly around them
+ for (var/i in 1 to additional_shots)
+ if (!length(all_dirs))
+ return
+
+ SLEEP_CHECK_DEATH(shot_delay, firer)
+ if (hit_turf != target.loc)
+ all_dirs = list(NONE) + GLOB.alldirs // Refresh potential target turfs if they've moved
+
+ var/turf/target_turf = null
+ // NODE drones get a much more focused barrage sent at them
+ if (istype(target, /mob/living/basic/node_drone) && prob(30))
+ target_turf = get_turf(target)
+ else
+ var/target_dir = pick_n_take(all_dirs)
+ target_turf = target_dir ? get_step(target, target_dir) : get_turf(target)
+
+ if (target_turf == firer.loc) // Don't hit ourselves
+ var/target_dir = pick_n_take(all_dirs)
+ target_turf = target_dir ? get_step(target, target_dir) : get_turf(target)
-/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/attack_sequence(mob/living/firer, atom/target)
- fire_in_directions(firer, target, GLOB.cardinals)
- SLEEP_CHECK_DEATH(0.5 SECONDS, firer)
- fire_in_directions(firer, target, GLOB.diagonals)
+ new acid_type(firer, target_turf)
+ if (i % 2 == 0)
+ playsound(firer, projectile_sound, 70)
-/obj/projectile/bileworm_acid
+/obj/effect/bileworm_acid
name = "acidic bile"
- icon_state = "neurotoxin"
- hitsound = 'sound/items/weapons/sear.ogg'
- damage = 20
- speed = 0.5
- range = 20
- jitter = 3 SECONDS
- stutter = 3 SECONDS
- damage_type = BURN
- pass_flags = PASSTABLE
-
-/obj/projectile/bileworm_acid/Initialize(mapload)
+ icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ icon_state = "bile_glob"
+ layer = ABOVE_ALL_MOB_LAYER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ /// Damage dealt to all mobs in the impact turf
+ var/damage = 20
+ /// Tiles travelled per second at maximum range
+ var/speed = 5
+ /// Maximum range for our speed calculations
+ var/max_range = 10
+ /// Warning sign on the turf we're about to hit
+ var/obj/effect/bileworm_target/target_sign = null
+
+/obj/effect/bileworm_acid/Initialize(mapload, atom/new_target)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+ if (new_target)
+ fire_at(get_turf(new_target))
+
+/obj/effect/bileworm_acid/Destroy(force)
+ QDEL_NULL(target_sign)
+ return ..()
+
+/obj/effect/bileworm_acid/update_overlays()
. = ..()
- AddComponent(/datum/component/parriable_projectile)
+ . += emissive_appearance(icon, icon_state, src, alpha = 20, effect_type = EMISSIVE_BLOOM)
+
+/obj/effect/bileworm_acid/proc/fire_at(turf/target)
+ if (!istype(target))
+ target = get_turf(target)
+ var/turf/start = get_turf(src)
+ forceMove(target) // Immediately move to the target turf so we can guarantee they can see us
+ pixel_x = (start.x - target.x) * ICON_SIZE_X
+ pixel_y = (start.y - target.y) * ICON_SIZE_Y
+ // Because BYOND's get_dist is taxicab distance
+ var/travel_dist = sqrt((start.x - target.x) ** 2 + (start.y - target.y) ** 2)
+ // Slightly scale the speed with distance for fairer gameplay
+ if (travel_dist < max_range)
+ speed *= 0.5 + travel_dist / max_range * 0.5
+ var/travel_time = travel_dist / speed * 1 SECONDS
+ animate(src, pixel_x = 0, time = travel_time, flags = ANIMATION_PARALLEL, easing = SINE_EASING | EASE_OUT) // Not a perfect arc but looks better
+ animate(src, pixel_y = 160, time = travel_time / 2, flags = ANIMATION_PARALLEL, easing = QUAD_EASING | EASE_OUT)
+ animate(pixel_y = 0, time = travel_time / 2, easing = QUAD_EASING | EASE_IN)
+ animate(src, transform = matrix().Turn(start.x > target.x ? -91 : 91), time = travel_time / 2, flags = ANIMATION_PARALLEL, easing = SINE_EASING | EASE_IN)
+ animate(transform = matrix().Turn(start.x > target.x ? -180 : 180), time = travel_time / 2, easing = SINE_EASING | EASE_OUT)
+ target_sign = new(target)
+ addtimer(CALLBACK(src, PROC_REF(impact), target), travel_time)
+
+/obj/effect/bileworm_acid/proc/impact(turf/target)
+ var/obj/effect/abstract/particle_holder/impact_particles = new(target, /particles/bile)
+ QDEL_IN(impact_particles, /particles/bile::lifespan)
+
+ var/hit_something = FALSE
+ for (var/mob/living/victim in target)
+ if(HAS_TRAIT(target, TRAIT_UNHITTABLE_BY_PROJECTILES))
+ if(!HAS_TRAIT(target, TRAIT_BLOCKING_PROJECTILES) && isliving(target))
+ var/mob/living/living_target = target
+ living_target.block_projectile_effects()
+ continue
+
+ // Doesn't make much sense to use melee for mobs, but its a mining mob so eeeeeh
+ // Can't use acid either as its mostly for atom armor only
+ var/blocked = victim.run_armor_check(null, MELEE, armour_penetration = 40, silent = TRUE)
+ if (blocked >= 100)
+ continue
+
+ hit_something = TRUE
+ victim.apply_damage(damage, BURN, null, blocked, wound_bonus = CANT_WOUND)
+ to_chat(victim, span_userdanger("You're hit by [src]!"))
+
+ for (var/obj/thing in target)
+ if (!thing.uses_integrity || !thing.density)
+ continue
+ hit_something = TRUE
+ thing.take_damage(damage, BURN, ACID, sound_effect = FALSE)
+
+ if (hit_something)
+ playsound(target, 'sound/items/weapons/sear.ogg', 50, -1)
+
+ qdel(src)
+
+/obj/effect/bileworm_target
+ icon = 'icons/mob/telegraphing/telegraph.dmi'
+ icon_state = "projectile_circle"
+ layer = BELOW_MOB_LAYER
+ plane = GAME_PLANE
+
+/obj/effect/bileworm_target/Initialize(mapload)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/effect/bileworm_target/update_overlays()
+ . = ..()
+ . += emissive_appearance(icon, icon_state, src, alpha = 90, effect_type = EMISSIVE_BLOOM)
+
+/particles/bile
+ icon = 'icons/effects/particles/goop.dmi'
+ icon_state = list("goop_1" = 6, "goop_2" = 2, "goop_3" = 1)
+ width = 100
+ height = 100
+ count = 10
+ spawning = 10
+ color = "#00ea2b80" //to get 96 alpha
+ lifespan = 1.5 SECONDS
+ fade = 1 SECONDS
+ grow = -0.025
+ gravity = list(0, 0.15)
+ position = generator(GEN_SPHERE, 0, 8, NORMAL_RAND)
+ spin = generator(GEN_NUM, -15, 15, NORMAL_RAND)
+ velocity = generator(GEN_BOX, list(-2, 2), list(2, 4), NORMAL_RAND)
/datum/action/cooldown/mob_cooldown/devour
name = "Devour"
diff --git a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_ai.dm b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_ai.dm
index e18af7ed2ced..cd2450018cf3 100644
--- a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_ai.dm
@@ -1,10 +1,13 @@
/datum/ai_controller/basic_controller/bileworm
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/bileworm,
+ BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
+ BB_BILEWORM_FLEE_DISTANCE = 3,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
+ /datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/bileworm_attack,
/datum/ai_planning_subtree/bileworm_execute,
@@ -20,16 +23,12 @@
return
var/datum/action/cooldown/mob_cooldown/resurface = controller.blackboard[BB_BILEWORM_RESURFACE]
+ var/datum/action/cooldown/mob_cooldown/bile = controller.blackboard[BB_BILEWORM_SPEW_BILE]
- //because one ability is always INFINITY cooldown, this actually works to check which ability should be used
- //sometimes it will try to spew bile on infinity cooldown, but that's okay because as soon as resurface is ready it will attempt that
-
- if(resurface?.IsAvailable())
+ if(resurface?.IsAvailable() && (controller.blackboard[BB_BILEWORM_SCARED] || get_dist(controller.pawn, controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) <= controller.blackboard[BB_BILEWORM_FLEE_DISTANCE]))
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_plan_execute, BB_BILEWORM_RESURFACE, BB_BASIC_MOB_CURRENT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
- var/datum/action/cooldown/mob_cooldown/bile = controller.blackboard[BB_BILEWORM_SPEW_BILE]
-
if(bile?.IsAvailable())
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_plan_execute, BB_BILEWORM_SPEW_BILE, BB_BASIC_MOB_CURRENT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING //focus on the fight
diff --git a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_instrument.dm b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_instrument.dm
index 508728ffa739..74d20946737f 100644
--- a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_instrument.dm
+++ b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_instrument.dm
@@ -2,7 +2,7 @@
name = "bilehorn"
desc = "Bits of bileworm anatomy rearranged to produce wonderful music, not bile. Keeps the name though, because for an instrument, it is quite vile."
force = 5
- icon = 'icons/mob/simple/lavaland/bileworm.dmi'
+ icon = 'icons/obj/mining_zones/artefacts.dmi'
icon_state = "bilehorn"
allowed_instrument_ids = "bilehorn"
inhand_icon_state = null
diff --git a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_loot.dm b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_loot.dm
index 55f960e736a9..a8bedef666b4 100644
--- a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_loot.dm
+++ b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_loot.dm
@@ -5,7 +5,7 @@
name = "bileworm skin"
desc = "The slushy, squishy and slightly shiny skin of a postmortem bileworm."
singular_name = "bileworm skin piece"
- icon = 'icons/mob/simple/lavaland/bileworm.dmi'
+ icon = 'icons/obj/stack_objects.dmi'
icon_state = "sheet-bileworm"
inhand_icon_state = null
merge_type = /obj/item/stack/sheet/animalhide/bileworm
diff --git a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_vileworm.dm b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_vileworm.dm
index d03ff00f85a1..73918285b418 100644
--- a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_vileworm.dm
+++ b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_vileworm.dm
@@ -4,34 +4,20 @@
icon_state = "vileworm"
icon_living = "vileworm"
icon_dead = "vileworm_dead"
- maxHealth = 150
- health = 150
+ maxHealth = 175
+ health = 175
- attack_action_path = /datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/vileworm
+ attack_action_path = /datum/action/cooldown/mob_cooldown/bileworm_spew/corrupt
evolve_path = null
-/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/vileworm
+/datum/action/cooldown/mob_cooldown/bileworm_spew/corrupt
name = "Spew Corrupted Bile"
- desc = "Spews corrupted bile everywhere. Must resurface after use to refresh."
- projectile_type = /obj/projectile/bileworm_acid/vile
+ desc = "Spew a barrage of corrupted bile globs."
+ cooldown_time = 2.5 SECONDS
+ acid_type = /obj/effect/bileworm_acid/corrupt
+ additional_shots = 6
+ shot_delay = 0.1 SECONDS
-/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/vileworm/Activate(atom/target_atom)
- StartCooldownSelf(INFINITY)
- attack_sequence(owner, target_atom)
- //faster than unevolved
- StartCooldownOthers(1.5 SECONDS)
-
-/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/vileworm/attack_sequence(mob/living/firer, atom/target)
- fire_in_directions(firer, target, GLOB.cardinals)
- SLEEP_CHECK_DEATH(0.25 SECONDS, firer)
- fire_in_directions(firer, target, GLOB.diagonals)
- SLEEP_CHECK_DEATH(0.25 SECONDS, firer)
- fire_in_directions(firer, target, GLOB.cardinals)
- // surprise!
- if(prob(25))
- SLEEP_CHECK_DEATH(0.25 SECONDS, firer)
- fire_in_directions(firer, target, GLOB.diagonals)
-
-/obj/projectile/bileworm_acid/vile
- name = "corrupted bile"
- icon_state = "vileworm"
+/obj/effect/bileworm_acid/corrupt
+ icon_state = "corrupt_bile_glob"
+ damage = 27
diff --git a/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm b/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm
index e558939f8e30..41a1a0f228a3 100644
--- a/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm
+++ b/code/modules/mob/living/basic/lavaland/brimdemon/brimbeam.dm
@@ -15,9 +15,16 @@
var/beam_duration = 2 SECONDS
/// How long do we wind up before firing?
var/charge_duration = 1 SECONDS
+ /// Have we been hit and have to abort the blast?
+ var/abort_blast = FALSE
/// A list of all the beam parts.
var/list/beam_parts = list()
+/datum/action/cooldown/mob_cooldown/brimbeam/Grant(mob/granted_to)
+ . = ..()
+ if(owner)
+ owner.AddElement(/datum/element/relay_attackers)
+
/datum/action/cooldown/mob_cooldown/brimbeam/Destroy()
extinguish_laser()
return ..()
@@ -25,6 +32,7 @@
/datum/action/cooldown/mob_cooldown/brimbeam/Activate(atom/target)
StartCooldown(360 SECONDS)
+ abort_blast = FALSE
owner.face_atom(target)
owner.move_resist = MOVE_FORCE_VERY_STRONG
owner.balloon_alert_to_viewers("charging...")
@@ -32,17 +40,20 @@
var/mutable_appearance/direction_emissive = emissive_appearance('icons/mob/simple/lavaland/lavaland_monsters.dmi', "brimdemon_telegraph_dir", owner, alpha = 150, effect_type = EMISSIVE_NO_BLOOM)
owner.add_overlay(direction_overlay)
owner.add_overlay(direction_emissive)
+ RegisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_owner_attacked))
- var/fully_charged = do_after(owner, delay = charge_duration, target = owner)
+ var/fully_charged = do_after(owner, delay = charge_duration, target = owner, extra_checks = CALLBACK(src, PROC_REF(beam_charge_check)))
owner.cut_overlay(direction_overlay)
owner.cut_overlay(direction_emissive)
if (!fully_charged)
+ UnregisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED)
StartCooldown()
return TRUE
if (!fire_laser())
var/static/list/fail_emotes = list("coughs.", "wheezes.", "belches out a puff of black smoke.")
owner.manual_emote(pick(fail_emotes))
+ UnregisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED)
StartCooldown()
return TRUE
@@ -51,11 +62,20 @@
demon.icon_state = demon.firing_icon_state
demon.update_appearance(UPDATE_OVERLAYS)
- do_after(owner, delay = beam_duration, target = owner, hidden = TRUE)
+ do_after(owner, delay = beam_duration, target = owner, hidden = TRUE, extra_checks = CALLBACK(src, PROC_REF(beam_charge_check)))
+ UnregisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED)
extinguish_laser()
StartCooldown()
return TRUE
+/datum/action/cooldown/mob_cooldown/brimbeam/proc/on_owner_attacked(datum/source, atom/attacker, attack_flags, direction)
+ SIGNAL_HANDLER
+ if (!(attack_flags & ATTACK_RANGED) && !(direction & owner.dir))
+ abort_blast = TRUE
+
+/datum/action/cooldown/mob_cooldown/brimbeam/proc/beam_charge_check()
+ return !abort_blast
+
/// Create a laser in the direction we are facing
/datum/action/cooldown/mob_cooldown/brimbeam/proc/fire_laser()
owner.visible_message(span_danger("[owner] fires a brimbeam!"))
@@ -67,7 +87,7 @@
if(affected_turf.opacity)
break
var/blocked = FALSE
- for(var/obj/potential_block in affected_turf.contents)
+ for(var/obj/potential_block in affected_turf)
if(potential_block.opacity)
blocked = TRUE
break
@@ -77,8 +97,8 @@
new_brimbeam.dir = owner.dir
beam_parts += new_brimbeam
new_brimbeam.assign_creator(owner)
- for(var/mob/living/hit_mob in affected_turf.contents)
- hit_mob.apply_damage(damage = 25, damagetype = BURN)
+ for(var/mob/living/hit_mob in affected_turf)
+ hit_mob.apply_damage(25, BURN, blocked = hit_mob.run_armor_check(null, LASER, silent = TRUE), wound_bonus = CANT_WOUND)
to_chat(hit_mob, span_userdanger("You're blasted by [owner]'s brimbeam!"))
RegisterSignal(new_brimbeam, COMSIG_QDELETING, PROC_REF(extinguish_laser)) // In case idk a singularity eats it or something
if(!length(beam_parts))
@@ -126,16 +146,10 @@
return ..()
/obj/effect/brimbeam/process()
- var/atom/ignore = creator?.resolve()
+ var/ignore = creator?.resolve()
for(var/mob/living/hit_mob in get_turf(src))
- if(hit_mob == ignore)
- continue
- damage(hit_mob)
-
-/// Hurt the passed mob
-/obj/effect/brimbeam/proc/damage(mob/living/hit_mob)
- hit_mob.apply_damage(damage = 5, damagetype = BURN)
- to_chat(hit_mob, span_danger("You're damaged by [src]!"))
+ if(hit_mob != ignore)
+ hit_mob.apply_damage(7, BURN, blocked = hit_mob.run_armor_check(null, LASER, silent = TRUE), wound_bonus = CANT_WOUND)
/// Ignore damage dealt to this mob
/obj/effect/brimbeam/proc/assign_creator(mob/living/maker)
diff --git a/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm b/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm
index eb6f6f671ed5..b60d7ab4a906 100644
--- a/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm
@@ -4,6 +4,7 @@
/datum/ai_controller/basic_controller/brimdemon
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining/low_node_priority,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
@@ -11,6 +12,7 @@
idle_behavior = /datum/idle_behavior/idle_random_walk/no_target
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
+ /datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic,
/datum/ai_planning_subtree/move_to_cardinal/brimdemon,
diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
index 6aef2570060d..7b982ddf5be8 100644
--- a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
+++ b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
@@ -9,7 +9,7 @@
pixel_x = -12
base_pixel_x = -12
gender = MALE // Female ones are the bipedal elites
- speed = 30
+ speed = 12
basic_mob_flags = IMMUNE_TO_FISTS
maxHealth = 300
health = 300
@@ -197,7 +197,7 @@
This one is clearly ancient, and its tentacles constantly churn the earth around it."
maxHealth = 400
health = 400
- crusher_drop_chance = 30 // Wow a whole 5% more likely, how generous
+ crusher_drop_chance = 100
/// Don't re-check nearby turfs for this long
COOLDOWN_DECLARE(retarget_turfs_cooldown)
/// List of places we might spawn a tentacle, if we're alive
@@ -214,7 +214,7 @@
tentacle_target_turfs -= target_turf
continue
if (prob(10))
- new /obj/effect/goliath_tentacle(target_turf)
+ new /obj/effect/goliath_tentacle(target_turf, src)
/mob/living/basic/mining/goliath/ancient/immortal/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
@@ -224,7 +224,7 @@
/// Store nearby turfs in our list so we can pop them out later
/mob/living/basic/mining/goliath/ancient/immortal/proc/cache_nearby_turfs()
- COOLDOWN_START(src, retarget_turfs_cooldown, 10 SECONDS)
+ COOLDOWN_START(src, retarget_turfs_cooldown, 5 SECONDS)
LAZYCLEARLIST(tentacle_target_turfs)
for(var/turf/open/floor in orange(4, loc))
LAZYADD(tentacle_target_turfs, floor)
diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath_actions.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath_actions.dm
index 31eecc036290..d1336aa90948 100644
--- a/code/modules/mob/living/basic/lavaland/goliath/goliath_actions.dm
+++ b/code/modules/mob/living/basic/lavaland/goliath/goliath_actions.dm
@@ -20,12 +20,12 @@
return ..()
/datum/action/cooldown/mob_cooldown/goliath_tentacles/Activate(atom/target)
- new /obj/effect/goliath_tentacle(target)
- var/list/directions = GLOB.cardinals.Copy()
- for(var/i in 1 to 3)
- var/spawndir = pick_n_take(directions)
+ new /obj/effect/goliath_tentacle(target, owner)
+ for(var/spawndir in GLOB.cardinals)
var/turf/adjacent_target = get_step(target, spawndir)
- if(adjacent_target)
+ if(isgroundlessturf(adjacent_target) && !islava(adjacent_target))
+ continue
+ if(isopenturf(adjacent_target) || ismineralturf(adjacent_target))
new /obj/effect/goliath_tentacle(adjacent_target)
if (isliving(target))
@@ -51,7 +51,7 @@
for (var/dir in directions)
var/turf/adjacent_target = get_step(target, dir)
if(adjacent_target)
- new /obj/effect/goliath_tentacle(adjacent_target)
+ new /obj/effect/goliath_tentacle/drag(adjacent_target, owner)
owner.visible_message(span_warning("[owner] unleashes tentacles from the ground around it!"))
StartCooldown()
return TRUE
@@ -70,7 +70,7 @@
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/tentacle_grasp/Activate(atom/target)
- new /obj/effect/temp_visual/effect_trail/burrowed_tentacle(owner.loc, target)
+ new /obj/effect/temp_visual/effect_trail/burrowed_tentacle(owner.loc, target, owner)
if (isliving(target))
owner.visible_message(span_warning("[owner] reaches for [target] with its tentacles!"))
StartCooldown()
@@ -83,4 +83,20 @@
move_speed = 2
homing = FALSE
spawn_interval = 0.1 SECONDS
- spawned_effect = /obj/effect/goliath_tentacle
+ spawned_effect = /obj/effect/goliath_tentacle/drag
+ /// Mob who fired us
+ var/mob/living/owner = null
+
+/obj/effect/temp_visual/effect_trail/burrowed_tentacle/Initialize(mapload, atom/target, mob/living/owner)
+ . = ..()
+ src.owner = owner
+
+/obj/effect/temp_visual/effect_trail/burrowed_tentacle/Destroy()
+ . = ..()
+ owner = null
+
+/obj/effect/temp_visual/effect_trail/burrowed_tentacle/add_spawner()
+ AddComponent(/datum/component/spawner, spawn_types = list(spawned_effect), max_spawned = max_spawned, spawn_time = spawn_interval, spawn_callback = CALLBACK(src, PROC_REF(on_tentacle_spawned)))
+
+/obj/effect/temp_visual/effect_trail/burrowed_tentacle/proc/on_tentacle_spawned(obj/effect/goliath_tentacle/tentacle)
+ tentacle.set_owner(owner)
diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm
index 63842f05ef50..60dfd7a15d07 100644
--- a/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm
@@ -4,6 +4,7 @@
/datum/ai_controller/basic_controller/goliath
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
@@ -11,6 +12,7 @@
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
+ /datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/target_retaliate/check_faction,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/find_food,
@@ -34,7 +36,7 @@
return ..()
var/mob/living/target = controller.blackboard[target_key]
// Interrupt attack chain to use tentacles, unless the target is already tentacled
- if (ismecha(target) || (isliving(target) && !target.has_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled)))
+ if (ismecha(target) || (isliving(target) && !target.get_item_by_slot(ITEM_SLOT_LEGCUFFED)))
var/datum/action/cooldown/using_action = controller.blackboard[BB_GOLIATH_TENTACLES]
if (using_action?.IsAvailable())
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
@@ -46,7 +48,7 @@
/datum/ai_planning_subtree/targeted_mob_ability/goliath_tentacles/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/target = controller.blackboard[target_key]
- if (!(isliving(target) || ismecha(target)) || (isliving(target) && target.has_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled)))
+ if (!(isliving(target) || ismecha(target)) || (isliving(target) && target.get_item_by_slot(ITEM_SLOT_LEGCUFFED)))
return // Target can be an item or already grabbed, we don't want to tentacle those
var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0
if (time_on_target < MIN_TIME_TO_TENTACLE)
diff --git a/code/modules/mob/living/basic/lavaland/goliath/tentacle.dm b/code/modules/mob/living/basic/lavaland/goliath/tentacle.dm
index cc9546474ec2..b5af54e4da0f 100644
--- a/code/modules/mob/living/basic/lavaland/goliath/tentacle.dm
+++ b/code/modules/mob/living/basic/lavaland/goliath/tentacle.dm
@@ -13,9 +13,13 @@
/// Lower bound of damage to inflict
var/min_damage = 10
/// Upper bound of damage to inflict
- var/max_damage = 15
+ var/max_damage = 15
+ /// Type of legcuff we spawn
+ var/trap_type = /obj/item/restraints/legcuffs/goliath_tentacle
+ /// Mob who fired the tentacle
+ var/mob/living/owner = null
-/obj/effect/goliath_tentacle/Initialize(mapload)
+/obj/effect/goliath_tentacle/Initialize(mapload, mob/living/goliath)
. = ..()
if (ismineralturf(loc))
var/turf/closed/mineral/floor = loc
@@ -28,11 +32,24 @@
deltimer(action_timer)
action_timer = addtimer(CALLBACK(src, PROC_REF(animate_grab)), 0.7 SECONDS, TIMER_STOPPABLE)
update_appearance(UPDATE_OVERLAYS)
+ set_owner(goliath)
/obj/effect/goliath_tentacle/Destroy()
deltimer(action_timer)
return ..()
+/obj/effect/goliath_tentacle/proc/set_owner(mob/living/goliath)
+ if (owner)
+ UnregisterSignal(owner, COMSIG_QDELETING)
+ owner = goliath
+ if (owner)
+ RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(on_owner_del))
+
+/obj/effect/goliath_tentacle/proc/on_owner_del(datum/source)
+ SIGNAL_HANDLER
+ owner = null
+ retract()
+
/// Change to next icon state and set up grapple
/obj/effect/goliath_tentacle/proc/animate_grab()
icon_state = "goliath_tentacle_wiggle"
@@ -42,20 +59,31 @@
/// Grab everyone we share space with. If it's nobody, go home.
/obj/effect/goliath_tentacle/proc/grab()
+ var/trapped_mobs = FALSE
for (var/mob/living/victim in loc)
- if (victim.stat == DEAD || HAS_TRAIT(victim, TRAIT_TENTACLE_IMMUNE))
+ if (victim.stat == DEAD || owner && victim.faction_check_atom(owner))
+ continue
+ if (HAS_TRAIT(victim, TRAIT_TENTACLE_IMMUNE) || SEND_SIGNAL(victim, COMSIG_GOLIATH_TENTACLED_GRABBED) & COMPONENT_GOLIATH_CANCEL_TENTACLE_GRAB)
+ continue
+ var/obj/item/restraints/legcuffs/goliath_tentacle/trap = new trap_type(loc, victim, src)
+ if (QDELETED(trap))
continue
balloon_alert(victim, "grabbed")
visible_message(span_danger("[src] grabs hold of [victim]!"))
- victim.adjust_brute_loss(rand(min_damage, max_damage))
- if (victim.apply_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled, grapple_time, src))
- buckle_mob(victim, TRUE)
- SEND_SIGNAL(victim, COMSIG_GOLIATH_TENTACLED_GRABBED)
+ victim.apply_damage(rand(min_damage, max_damage), BRUTE, pick(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG), wound_bonus = CANT_WOUND)
+ trapped_mobs = TRUE
+
for (var/obj/vehicle/sealed/mecha/mech in loc)
mech.take_damage(rand(min_damage, max_damage), damage_type = BRUTE, damage_flag = MELEE, sound_effect = TRUE)
- if (!has_buckled_mobs())
+
+ // Already retracting
+ if (icon_state == "goliath_tentacle_retract")
+ return
+
+ if (!trapped_mobs)
retract()
return
+
deltimer(action_timer)
action_timer = addtimer(CALLBACK(src, PROC_REF(retract)), grapple_time, TIMER_STOPPABLE)
@@ -64,7 +92,6 @@
if (icon_state == "goliath_tentacle_retract")
return // Already retracting
SEND_SIGNAL(src, COMSIG_GOLIATH_TENTACLE_RETRACTING)
- unbuckle_all_mobs(force = TRUE)
icon_state = "goliath_tentacle_retract"
update_appearance(UPDATE_OVERLAYS)
deltimer(action_timer)
@@ -77,54 +104,136 @@
/obj/effect/goliath_tentacle/attack_hand(mob/living/user, list/modifiers)
. = ..()
- if (. || !has_buckled_mobs())
- return
- retract()
- return TRUE
+ if(!.)
+ retract()
+ return TRUE
-/// Goliath tentacle stun with special removal conditions
-/datum/status_effect/incapacitating/stun/goliath_tentacled
- id = "goliath_tentacled"
- duration = 10 SECONDS
- /// The tentacle that is tenderly holding us close
+/// An alternative variant which drags the mob in towards the goliath
+/obj/effect/goliath_tentacle/drag
+ trap_type = /obj/item/restraints/legcuffs/goliath_tentacle/drag
+
+/obj/item/restraints/legcuffs/goliath_tentacle
+ name = "tentacle mass"
+ desc = "A writhing tentacle constricting one of your limbs."
+ icon_state = "goliath_tentacle"
+ slowdown = 4
+ breakouttime = 6 SECONDS
+ resistance_flags = LAVA_PROOF | FIRE_PROOF
+ obj_flags = CONDUCTS_ELECTRICITY | CAN_BE_HIT
+ item_flags = DROPDEL
+ legcuff_state = "goliath_tentacle"
+ uses_integrity = TRUE
+ max_integrity = 75 // 3 PKC swings, 5 bayonet swings
+ /// Tentacle we're attached to
var/obj/effect/goliath_tentacle/tentacle
+ /// Leash component linking the victim to the turf
+ var/datum/component/leash/leash
+ /// Beam between the victim and the tentacle tile
+ var/datum/beam/beam_effect
-/datum/status_effect/incapacitating/stun/goliath_tentacled/on_creation(mob/living/new_owner, set_duration, obj/effect/goliath_tentacle/tentacle)
+/obj/item/restraints/legcuffs/goliath_tentacle/Initialize(mapload, mob/living/target, obj/effect/goliath_tentacle/tentacle)
. = ..()
- if (!.)
- return
+ ADD_TRAIT(src, TRAIT_IGNORE_DEMOLITION, INNATE_TRAIT)
src.tentacle = tentacle
+ if (!target?.equip_to_slot_if_possible(src, ITEM_SLOT_LEGCUFFED, disable_warning = TRUE, bypass_equip_delay_self = TRUE))
+ return INITIALIZE_HINT_QDEL
-/datum/status_effect/incapacitating/stun/goliath_tentacled/on_apply()
+/obj/item/restraints/legcuffs/goliath_tentacle/Destroy(force)
. = ..()
- RegisterSignal(owner, COMSIG_CARBON_PRE_MISC_HELP, PROC_REF(on_helped))
- RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE), COMSIG_BRIMDUST_EXPLOSION), PROC_REF(release))
- RegisterSignals(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING), PROC_REF(on_tentacle_left))
+ if (tentacle)
+ tentacle.retract()
+ tentacle = null
+ if (leash)
+ UnregisterSignal(leash, COMSIG_QDELETING)
+ QDEL_NULL(leash)
+ QDEL_NULL(beam_effect)
-/datum/status_effect/incapacitating/stun/goliath_tentacled/on_remove()
+/obj/item/restraints/legcuffs/goliath_tentacle/equipped(mob/living/user, slot, initial)
. = ..()
- UnregisterSignal(owner, list(COMSIG_CARBON_PRE_MISC_HELP, SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE), COMSIG_BRIMDUST_EXPLOSION))
- if (isnull(tentacle))
+ if (slot != ITEM_SLOT_LEGCUFFED || leash)
return
- UnregisterSignal(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING))
- tentacle.retract()
- tentacle = null
+ leash_target(user)
-/// Some kind soul has rescued us
-/datum/status_effect/incapacitating/stun/goliath_tentacled/proc/on_helped(mob/source, mob/helping)
+/obj/item/restraints/legcuffs/goliath_tentacle/proc/release(datum/source)
SIGNAL_HANDLER
- release()
- source.visible_message(span_notice("[helping] rips [source] from the tentacle's grasp!"))
- return COMPONENT_BLOCK_MISC_HELP
+ qdel(src)
-/// Something happened to make the tentacle let go
-/datum/status_effect/incapacitating/stun/goliath_tentacled/proc/release()
+/obj/item/restraints/legcuffs/goliath_tentacle/proc/on_tentacle_left(datum/source)
SIGNAL_HANDLER
- owner.remove_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled)
+ if (tentacle)
+ UnregisterSignal(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING))
+ tentacle = null
+ release()
-/// Something happened to our associated tentacle
-/datum/status_effect/incapacitating/stun/goliath_tentacled/proc/on_tentacle_left()
+/obj/item/restraints/legcuffs/goliath_tentacle/proc/leash_target(mob/living/user)
+ leash = user.AddComponent(/datum/component/leash, owner = get_turf(user), distance = 1, silent = TRUE)
+ beam_effect = user.Beam(get_turf(user), "goliath_tentacle", beam_type = /obj/effect/ebeam/goliath, emissive = FALSE)
+ RegisterSignal(beam_effect.visuals, COMSIG_CLICK, PROC_REF(on_beam_click))
+ RegisterSignals(user, list(SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE), COMSIG_BRIMDUST_EXPLOSION), PROC_REF(release))
+ RegisterSignals(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING), PROC_REF(on_tentacle_left))
+ RegisterSignal(leash, COMSIG_QDELETING, PROC_REF(release))
+
+/obj/item/restraints/legcuffs/goliath_tentacle/proc/on_beam_click(atom/source, atom/location, control, params, mob/user)
SIGNAL_HANDLER
- UnregisterSignal(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING)) // No endless loops for us please
+ INVOKE_ASYNC(src, PROC_REF(process_beam_click), source, location, params, user)
+
+/obj/item/restraints/legcuffs/goliath_tentacle/proc/process_beam_click(atom/source, atom/location, params, mob/user)
+ if(world.time <= user.next_move)
+ return
+
+ var/obj/item/held_thing = user.get_active_held_item()
+ if (!istype(held_thing))
+ return
+
+ var/turf/nearest_turf = null
+ for (var/turf/line_turf in get_line(get_turf(src), get_turf(beam_effect.target)))
+ if (line_turf.IsReachableBy(user))
+ nearest_turf = line_turf
+ break
+
+ if (isnull(nearest_turf))
+ return
+
+ if (!user.can_perform_action(nearest_turf))
+ nearest_turf.balloon_alert(user, "cannot reach!")
+ return
+
+ held_thing.melee_attack_chain(user, src, params2list(params))
+ user.changeNext_move(held_thing.attack_speed)
+ playsound(user, held_thing.hitsound || 'sound/effects/blob/attackblob.ogg', held_thing.get_clamped_volume(), TRUE, extrarange = held_thing.stealthy_audio ? SILENCED_SOUND_EXTRARANGE : -1, falloff_distance = 0)
+
+/obj/item/restraints/legcuffs/goliath_tentacle/play_attack_sound(damage_amount, damage_type, damage_flag)
+ return
+
+/obj/item/restraints/legcuffs/goliath_tentacle/drag
+
+/obj/item/restraints/legcuffs/goliath_tentacle/drag/Initialize(mapload, mob/living/target, obj/effect/goliath_tentacle/tentacle)
+ . = ..()
+ QDEL_IN(src, 15 SECONDS)
+ if (. != INITIALIZE_HINT_QDEL)
+ START_PROCESSING(SSprocessing, src)
+
+/obj/item/restraints/legcuffs/goliath_tentacle/drag/Destroy(force)
+ . = ..()
+ STOP_PROCESSING(SSprocessing, src)
+
+/obj/item/restraints/legcuffs/goliath_tentacle/drag/process(seconds_per_tick)
+ if (leash.distance <= 1)
+ return PROCESS_KILL
+ leash.set_distance(leash.distance - 1)
+
+/obj/item/restraints/legcuffs/goliath_tentacle/drag/leash_target(mob/living/user)
+ if (!tentacle?.owner)
+ return ..()
+ leash = user.AddComponent(/datum/component/leash, owner = tentacle.owner, distance = get_dist(user, tentacle.owner), silent = TRUE)
+ beam_effect = user.Beam(tentacle.owner, "goliath_tentacle", beam_type = /obj/effect/ebeam/goliath, emissive = FALSE)
+ RegisterSignal(beam_effect.visuals, COMSIG_CLICK, PROC_REF(on_beam_click))
+ RegisterSignals(user, list(SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE), COMSIG_BRIMDUST_EXPLOSION), PROC_REF(release))
+ RegisterSignals(tentacle.owner, COMSIG_QDELETING, PROC_REF(on_tentacle_left))
+ RegisterSignal(leash, COMSIG_QDELETING, PROC_REF(release))
+ tentacle.retract()
tentacle = null
- release()
+
+/obj/effect/ebeam/goliath
+ name = "goliath tentacle"
+ mouse_opacity = MOUSE_OPACITY_ICON
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion.dm b/code/modules/mob/living/basic/lavaland/legion/legion.dm
index 32968a349c72..6d8ac2e2a734 100644
--- a/code/modules/mob/living/basic/lavaland/legion/legion.dm
+++ b/code/modules/mob/living/basic/lavaland/legion/legion.dm
@@ -75,7 +75,7 @@
/mob/living/basic/mining/legion/update_overlays()
. = ..()
if (stat != DEAD && has_emissive) // Shouldn't really happen but just in case
- . += emissive_appearance(icon, "[icon_living]_e", src, effect_type = EMISSIVE_NO_BLOOM)
+ . += emissive_appearance(icon, "[icon_living]_e", src, effect_type = EMISSIVE_BLOOM)
/// Put a corpse in this guy
/mob/living/basic/mining/legion/proc/consume(mob/living/carbon/human/consumed)
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
index 96cde9e5ac5e..8d5082d4c088 100644
--- a/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
@@ -2,25 +2,29 @@
/datum/ai_controller/basic_controller/legion
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/legion,
+ BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
BB_AGGRO_RANGE = 5, // Unobservant
- BB_BASIC_MOB_FLEE_DISTANCE = 6,
+ BB_RANGED_SKIRMISH_MIN_DISTANCE = 4,
+ BB_RANGED_SKIRMISH_MAX_DISTANCE = 6,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
+ /datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/random_speech/legion,
/datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/maintain_distance,
/datum/ai_planning_subtree/targeted_mob_ability,
- /datum/ai_planning_subtree/flee_target/legion,
)
/// Chase and attack whatever we are targeting, if it's friendly we will heal them
/datum/ai_controller/basic_controller/legion_brood
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/legion,
+ BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining/low_node_priority,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
index 1fc6a7d02be3..dd2bbe18de2b 100644
--- a/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
@@ -103,8 +103,17 @@
else
add_ally(creator)
created_by = WEAKREF(creator)
- ai_controller?.set_blackboard_key(BB_LEGION_BROOD_CREATOR, creator)
RegisterSignal(creator, COMSIG_QDELETING, PROC_REF(creator_destroyed))
+ if (!ai_controller)
+ return
+
+ ai_controller.set_blackboard_key(BB_LEGION_BROOD_CREATOR, creator)
+ if (!creator.ai_controller)
+ return
+
+ // Inherit our creator's target and reinforcement requests
+ ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, creator.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ ai_controller.set_blackboard_key(BB_MINING_MOB_REINFORCEMENTS_REQUESTS, creator.ai_controller.blackboard[BB_MINING_MOB_REINFORCEMENTS_REQUESTS])
/// Reference handling
/mob/living/basic/mining/legion_brood/proc/creator_destroyed()
diff --git a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm
index c634f1238d18..bd9b2addff62 100644
--- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm
@@ -8,12 +8,13 @@
/datum/ai_controller/basic_controller/lobstrosity
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
BB_LOBSTROSITY_EXPLOIT_TRAITS = list(TRAIT_INCAPACITATED, TRAIT_FLOORED, TRAIT_IMMOBILIZED, TRAIT_KNOCKEDOUT),
BB_LOBSTROSITY_FINGER_LUST = 0,
BB_LOBSTROSITY_NAIVE_HUNTER = FALSE,
- BB_BASIC_MOB_FLEE_DISTANCE = 8,
+ BB_BASIC_MOB_FLEE_DISTANCE = 6,
BB_EAT_FOOD_COOLDOWN = 3 MINUTES,
BB_ONLY_FISH_WHILE_HUNGRY = TRUE,
BB_TARGET_PRIORITY_TRAIT = TRAIT_SCARY_FISHERMAN,
@@ -24,6 +25,7 @@
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
+ /datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/random_speech/insect,
/datum/ai_planning_subtree/hoard_fingers,
/datum/ai_planning_subtree/pet_planning,
diff --git a/code/modules/mob/living/basic/lavaland/mining.dm b/code/modules/mob/living/basic/lavaland/mining.dm
index 592477928005..b2ba41395599 100644
--- a/code/modules/mob/living/basic/lavaland/mining.dm
+++ b/code/modules/mob/living/basic/lavaland/mining.dm
@@ -40,7 +40,7 @@
drop_mod = crusher_drop_chance,\
drop_immediately = basic_mob_flags & DEL_ON_DEATH,\
)
- RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(check_ashwalker_peace_violation))
+ RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
// We add this to ensure that mobs will actually receive the above signal, as some will lack AI
// handling for retaliation and attack special cases
AddElement(/datum/element/relay_attackers)
@@ -55,9 +55,9 @@
throw_blocked_message = throw_blocked_message,\
)
-/mob/living/basic/mining/proc/check_ashwalker_peace_violation(datum/source, mob/living/carbon/human/possible_ashwalker)
+/mob/living/basic/mining/proc/on_attacked(datum/source, atom/attacker, attack_flags)
SIGNAL_HANDLER
- if(!isashwalker(possible_ashwalker) || !has_faction(FACTION_ASHWALKER))
+ if(!isashwalker(attacker) || !has_faction(FACTION_ASHWALKER))
return
remove_faction(FACTION_ASHWALKER)
diff --git a/code/modules/mob/living/basic/lavaland/mook/mook.dm b/code/modules/mob/living/basic/lavaland/mook/mook.dm
index 80d398f23d0c..966c1bfba185 100644
--- a/code/modules/mob/living/basic/lavaland/mook/mook.dm
+++ b/code/modules/mob/living/basic/lavaland/mook/mook.dm
@@ -187,7 +187,7 @@
if(istype(intruder, /mob/living/basic/mining/mook))
return
for(var/mob/living/basic/mining/mook/villager in oview(src, 9))
- villager.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, intruder)
+ villager.ai_controller?.set_blackboard_key_assoc(BB_BASIC_MOB_RETALIATE_LIST, intruder, world.time)
/mob/living/basic/mining/mook/worker
diff --git a/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm b/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm
index 9d8fb37179df..610682fd7a36 100644
--- a/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm
+++ b/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm
@@ -92,7 +92,7 @@ GLOBAL_LIST_EMPTY(raptor_population)
else
change_growth_stage(growth_stage, RAPTOR_ADULT)
- add_traits(list(TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE, TRAIT_MINING_AOE_IMMUNE), INNATE_TRAIT)
+ add_traits(list(TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE, TRAIT_MINING_AOE_IMMUNE, TRAIT_NO_SLIP_ICE, TRAIT_NO_SLIP_SLIDE), INNATE_TRAIT)
AddElement(\
/datum/element/crusher_loot,\
trophy_type = /obj/item/crusher_trophy/raptor_feather,\
diff --git a/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm b/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm
index b366a44b7b61..1be8dcb6ad9d 100644
--- a/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm
+++ b/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm
@@ -96,6 +96,10 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors())
// Doesn't care for your excuses for friendly fire
ai_controller = /datum/ai_controller/basic_controller/raptor/aggressive
+/datum/raptor_color/red/setup_raptor(mob/living/basic/raptor/raptor)
+ . = ..()
+ ADD_TRAIT(raptor, TRAIT_MINING_AGGRO, INNATE_TRAIT)
+
/datum/raptor_color/purple
color = "purple"
description = "A small, nimble breed, these raptors have been bred as travel companions rather than mounts, capable of storing the owner's possessions and helping them escape from danger unscathed."
@@ -440,7 +444,7 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors())
/datum/raptor_color/black/setup_raptor(mob/living/basic/raptor/raptor)
. = ..()
- raptor.add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_NOFIRE_SPREAD), INNATE_TRAIT)
+ raptor.add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_NOFIRE_SPREAD, TRAIT_MINING_AGGRO), INNATE_TRAIT)
/datum/raptor_color/black/setup_adult(mob/living/basic/raptor/raptor)
. = ..()
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm
new file mode 100644
index 000000000000..b31e8a982a73
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm
@@ -0,0 +1,155 @@
+#define HEARTBEAT_NORMAL (1.8 SECONDS)
+#define HEARTBEAT_FAST (1 SECONDS)
+#define HEARTBEAT_FRANTIC (0.4 SECONDS)
+
+GLOBAL_LIST_INIT(tendrils, list())
+
+/mob/living/basic/mining/tendril
+ name = "necropolis tendril"
+ desc = "A vile tendril of corruption, originating deep underground."
+ icon = 'icons/mob/simple/lavaland/tendril.dmi'
+ icon_state = "tendril"
+ icon_living = "tendril"
+ pixel_w = -8
+ base_pixel_w = -8
+ status_flags = NONE
+ mob_biotypes = MOB_ORGANIC | MOB_SKELETAL | MOB_MINING
+ basic_mob_flags = DEL_ON_DEATH | IMMUNE_TO_FISTS
+ mob_size = MOB_SIZE_HUGE
+ maxHealth = 1200
+ health = 1200
+
+ friendly_verb_continuous = "flails at"
+ friendly_verb_simple = "flail at"
+ speak_emote = list("resonates")
+ obj_damage = 100
+ melee_damage_lower = 20
+ melee_damage_upper = 20
+ sharpness = SHARP_POINTY
+ wound_bonus = CANT_WOUND
+ attack_sound = 'sound/items/weapons/pierce.ogg'
+ attack_verb_continuous = "pierces through"
+ attack_verb_simple = "pierce through"
+ throw_blocked_message = "does nothing to the thick shell of"
+ move_resist = INFINITY
+
+ light_range = 3
+ light_power = 2
+ light_color = LIGHT_COLOR_LAVA
+
+ ai_controller = /datum/ai_controller/basic_controller/tendril
+
+ /// Looping heartbeat sound
+ var/datum/looping_sound/heartbeat/soundloop
+ /// Melee attack ability to used in retaliation to melee strikes and whenever it manages to grab someone
+ var/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee/tendril_melee
+
+/mob/living/basic/mining/tendril/Initialize(mapload)
+ . = ..()
+ GLOB.tendrils += src
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/death_drops, /obj/structure/closet/crate/necropolis/tendril)
+ AddComponent(/datum/component/ai_target_timer)
+ AddComponent(/datum/component/gps, "Eerie Signal")
+ AddComponent(/datum/component/basic_mob_attack_telegraph, display_telegraph_overlay = FALSE, telegraph_duration = 0.4 SECONDS)
+ AddComponent(/datum/component/regenerator, regeneration_delay = 30 SECONDS, brute_per_second = 20)
+ add_traits(list(TRAIT_BACKSTAB_IMMUNE, TRAIT_IMMOBILIZED), INNATE_TRAIT)
+
+ var/static/list/abilities = list(
+ /datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash = BB_TENDRIL_LASH,
+ /datum/action/cooldown/mob_cooldown/tendril_chaser = BB_TENDRIL_CHASER,
+ /datum/action/cooldown/mob_cooldown/tendril_cross_spikes = BB_TENDRIL_SPIKES,
+ )
+ grant_actions_by_list(abilities)
+
+ tendril_melee = new(src)
+ tendril_melee.Grant(src)
+ AddComponent(/datum/component/revenge_ability, tendril_melee, targeting = GET_TARGETING_STRATEGY(ai_controller.blackboard[BB_TARGETING_STRATEGY]), max_range = 2, target_self = TRUE)
+
+ soundloop = new(src, start_immediately = FALSE)
+ soundloop.mid_length = HEARTBEAT_NORMAL
+ soundloop.pressure_affected = FALSE
+ soundloop.start()
+ update_appearance(UPDATE_OVERLAYS)
+
+ var/turf/our_turf = get_turf(src)
+ for (var/turf/rock in range(4, src))
+ var/dist = sqrt((rock.x - our_turf.x) ** 2 + (rock.y - our_turf.y) ** 2)
+ if (dist > 4.5)
+ continue
+
+ if (ismineralturf(rock))
+ rock.ScrapeAway(null, CHANGETURF_IGNORE_AIR)
+
+ if (istype(rock, /turf/open/misc/asteroid) && prob(100 / sqrt(max(1, dist))))
+ rock.ChangeTurf(/turf/open/indestructible/necropolis, null, CHANGETURF_IGNORE_AIR)
+
+/mob/living/basic/mining/tendril/Destroy()
+ GLOB.tendrils -= src
+ QDEL_NULL(soundloop)
+
+ if(!SSachievements.achievements_enabled || (flags_1 & ADMIN_SPAWNED_1))
+ return ..()
+
+ for(var/mob/living/killer in view(7, src))
+ if(killer.stat || !killer.client)
+ continue
+ killer.client.give_award(/datum/award/score/tendril_score, killer)
+ if (!length(GLOB.tendrils))
+ killer.client.give_award(/datum/award/achievement/boss/tendril_exterminator, killer)
+
+ return ..()
+
+/mob/living/basic/mining/tendril/update_overlays()
+ . = ..()
+ . += emissive_appearance(icon, "[icon_state]_e", src, effect_type = EMISSIVE_NO_BLOOM)
+ . += emissive_appearance(icon, "[icon_state]_e_bloom", src, effect_type = EMISSIVE_BLOOM)
+
+/mob/living/basic/mining/tendril/Life(seconds_per_tick)
+ . = ..()
+ update_heartbeat() // Just a single math op unless we need updating so its fine to put it here
+
+/mob/living/basic/mining/tendril/updatehealth()
+ . = ..()
+ update_heartbeat()
+
+/mob/living/basic/mining/tendril/proc/update_heartbeat()
+ if (!soundloop)
+ return
+
+ var/beat_rate = HEARTBEAT_NORMAL
+ if (ai_controller?.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ beat_rate = round(HEARTBEAT_FRANTIC + health / maxHealth * (HEARTBEAT_FAST - HEARTBEAT_FRANTIC), 0.05 SECONDS)
+
+ if (beat_rate != soundloop.mid_length)
+ soundloop.set_mid_length(beat_rate)
+
+/mob/living/basic/mining/tendril/do_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, no_effect, fov_effect = TRUE, item_animation_override = null)
+ if(!no_effect && (visual_effect_icon || used_item))
+ do_item_attack_animation(attacked_atom, visual_effect_icon, used_item, animation_type = item_animation_override)
+
+ // Stabby visuals
+ var/obj/effect/temp_visual/spike_stab/stab = new(get_turf(src))
+ var/target_dir = get_dir(src, attacked_atom)
+ stab.transform = matrix().Turn(dir2angle(target_dir) + rand(-7, 7))
+ if (target_dir & NORTH)
+ stab.pixel_z = 24
+ else if (target_dir & SOUTH)
+ stab.pixel_z = -24
+ if (target_dir & EAST)
+ stab.pixel_w = 24
+ else if (target_dir & WEST)
+ stab.pixel_w = -24
+
+/obj/effect/temp_visual/spike_stab
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "spike_small"
+ duration = 0.4 SECONDS
+
+/mob/living/basic/mining/tendril/proc/snatch_react()
+ if (tendril_melee.IsAvailable())
+ tendril_melee.Activate(warning = FALSE)
+
+#undef HEARTBEAT_NORMAL
+#undef HEARTBEAT_FAST
+#undef HEARTBEAT_FRANTIC
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
new file mode 100644
index 000000000000..1a2272fd85b7
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
@@ -0,0 +1,360 @@
+/// Fires out two cross patterns of damaging tentacles which reel in anything they hit, then causes a followup attack
+/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash
+ name = "Tentacle Lash"
+ desc = "Lash out with your tentacles in 8 directions, reeling in whatever you hit and unleashing a deadly followup attack afterwards."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "goliath_tentacle_wiggle"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = FALSE
+ cooldown_time = 8 SECONDS
+ melee_cooldown_time = 0
+ shared_cooldown = NONE
+ projectile_type = /obj/projectile/tentacle_lash
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash/Activate(atom/target)
+ disable_cooldown_actions()
+
+ for (var/swipe_dir in GLOB.cardinals)
+ var/turf/open/line_turf = get_step(owner, swipe_dir)
+ for (var/i in 1 to projectile_type::range)
+ if (!istype(line_turf) || line_turf.is_blocked_turf(exclude_mobs = TRUE))
+ break
+ var/obj/effect/temp_visual/telegraphing/line/telegraph = new(line_turf)
+ telegraph.dir = swipe_dir
+ line_turf = get_step(line_turf, swipe_dir)
+
+ SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
+
+ for (var/swipe_dir in GLOB.cardinals)
+ shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner)
+
+ SLEEP_CHECK_DEATH(1.6 SECONDS, owner)
+
+ for (var/swipe_dir in GLOB.diagonals)
+ var/turf/open/line_turf = get_step(owner, swipe_dir)
+ for (var/i in 1 to projectile_type::range)
+ if (!istype(line_turf) || line_turf.is_blocked_turf(exclude_mobs = TRUE))
+ break
+ var/obj/effect/temp_visual/telegraphing/line/telegraph = new(line_turf)
+ telegraph.dir = swipe_dir
+ line_turf = get_step(line_turf, swipe_dir)
+
+ SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
+
+ for (var/swipe_dir in GLOB.diagonals)
+ shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner)
+
+ SLEEP_CHECK_DEATH(1.2 SECONDS, owner)
+ StartCooldown()
+ SLEEP_CHECK_DEATH(0.6 SECONDS, owner)
+ enable_cooldown_actions()
+ return TRUE
+
+/obj/projectile/tentacle_lash
+ name = "tentacle spike"
+ icon_state = "tentacle_spike"
+ pass_flags = PASSTABLE
+ damage = 5 // +10 from the grab
+ armor_flag = MELEE
+ range = 7
+ hit_prone_targets = TRUE
+ hitsound = 'sound/effects/wounds/pierce1.ogg'
+ sharpness = SHARP_POINTY
+ /// Beam connecting us and the firer
+ var/datum/beam/tentacle_beam = null
+ /// Does this projectile persist and reel in targets?
+ var/reel_in = TRUE
+ /// How long does the projectile persist?
+ var/duration = 1.2 SECONDS
+ /// Damage dealt to targets who get snatched from entering the beam or being hit directly
+ var/snatch_damage = 10
+
+/obj/projectile/tentacle_lash/stab
+ damage = 15
+ range = 2
+ duration = 0.2 SECONDS // Just for visual flair
+ reel_in = FALSE
+
+/obj/projectile/tentacle_lash/fire(fire_angle, atom/direct_target)
+ . = ..()
+ if (!firer)
+ return
+ tentacle_beam = Beam(firer, "goliath_tentacle", beam_type = (reel_in ? /obj/effect/ebeam/reacting : /obj/effect/ebeam), emissive = FALSE)
+ if (reel_in)
+ RegisterSignal(tentacle_beam, COMSIG_BEAM_ENTERED, PROC_REF(on_beam_entered))
+
+/obj/projectile/tentacle_lash/Destroy()
+ QDEL_NULL(tentacle_beam)
+ return ..()
+
+// Don't range out, stop and persist until we're done
+/obj/projectile/tentacle_lash/on_range()
+ STOP_PROCESSING(SSprojectiles, src)
+ // Reset to tile center
+ pixel_x = 0
+ pixel_y = 0
+ QDEL_IN(src, duration)
+
+/obj/projectile/tentacle_lash/prehit_pierce(atom/target)
+ // Ye 'ole colossus cheese
+ if (astype(target, /mob/living)?.stat == DEAD)
+ return PROJECTILE_PIERCE_PHASE
+ return ..()
+
+/obj/projectile/tentacle_lash/on_hit(atom/target, blocked, pierce_hit)
+ . = ..()
+ if (!reel_in || !isliving(target) || blocked >= 100 || pierce_hit || . != BULLET_ACT_HIT)
+ return
+ snatch_target(target)
+
+/obj/projectile/tentacle_lash/proc/on_beam_entered(datum/beam/source, obj/effect/ebeam/hit, atom/movable/entered)
+ SIGNAL_HANDLER
+
+ if (!reel_in || entered == firer || !isliving(entered))
+ return
+
+ var/mob/living/victim = entered
+ if ((!firer || !firer.faction_check_atom(victim)) && victim.stat != DEAD)
+ INVOKE_ASYNC(src, PROC_REF(snatch_target), entered)
+
+/obj/projectile/tentacle_lash/proc/snatch_target(mob/living/victim)
+ if (HAS_TRAIT(victim, TRAIT_TENTACLE_IMMUNE) || SEND_SIGNAL(victim, COMSIG_TENDRIL_TENTACLED_GRABBED) & COMPONENT_TENDRIL_CANCEL_TENTACLE_GRAB)
+ return
+
+ to_chat(victim, span_userdanger("You're snatched by [firer]'s tentacles!"))
+ victim.apply_damage(snatch_damage, BRUTE, BODY_ZONE_CHEST, wound_bonus = CANT_WOUND)
+ if (QDELETED(victim))
+ qdel(src)
+ return
+ var/snatch_callback = null
+ if (istype(firer, /mob/living/basic/mining/tendril))
+ var/mob/living/basic/mining/tendril/tendril = firer
+ snatch_callback = CALLBACK(tendril, TYPE_PROC_REF(/mob/living/basic/mining/tendril, snatch_react))
+ victim.throw_at(firer, initial(range), 1, firer, FALSE, gentle = TRUE, callback = snatch_callback)
+ playsound(victim, hitsound, 50, -3, pressure_affected = FALSE)
+ qdel(src)
+
+/// An ability which makes spikes come out of the ground towards your target
+/datum/action/cooldown/mob_cooldown/tendril_chaser
+ name = "Impaling Spikes"
+ desc = "Send a spiked subterranean tendril chasing after your target."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "spikes_stabbing"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ cooldown_time = 8 SECONDS
+ click_to_activate = TRUE
+ shared_cooldown = NONE
+ /// Lazy list of references to spike trails
+ var/list/active_chasers
+ /// Health percentage threshold at which we send out wide charsers after the main target
+ var/wide_chaser_threshold = 0.7
+
+/datum/action/cooldown/mob_cooldown/tendril_chaser/Grant(mob/granted_to)
+ . = ..()
+ RegisterSignal(granted_to, COMSIG_MOB_ABILITY_STARTED, PROC_REF(on_ability_started))
+ RegisterSignal(granted_to, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(on_ability_finished))
+
+// Clean up after ourselves
+/datum/action/cooldown/mob_cooldown/tendril_chaser/Remove(mob/removed_from)
+ UnregisterSignal(removed_from, list(COMSIG_MOB_ABILITY_STARTED, COMSIG_MOB_ABILITY_FINISHED))
+ QDEL_LIST(active_chasers)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_ability_started(mob/living/owner, datum/action/cooldown/activated)
+ SIGNAL_HANDLER
+
+ // Delete all of our chasers when our owner triggers cross spikes as to not cause guaranteed damage
+ if (istype(activated, /datum/action/cooldown/mob_cooldown/tendril_cross_spikes))
+ QDEL_LIST(active_chasers)
+
+/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_ability_finished(mob/living/owner, datum/action/cooldown/activated)
+ SIGNAL_HANDLER
+
+ if (istype(activated, /datum/action/cooldown/mob_cooldown/tendril_cross_spikes))
+ ResetCooldown()
+
+/datum/action/cooldown/mob_cooldown/tendril_chaser/Activate(atom/target)
+ . = ..()
+ var/primary_type = /obj/effect/temp_visual/effect_trail/tendril_chaser
+ if (isliving(owner))
+ var/mob/living/as_living = owner
+ if (as_living.health / as_living.maxHealth <= wide_chaser_threshold)
+ primary_type = /obj/effect/temp_visual/effect_trail/tendril_chaser/wide_area
+
+ var/obj/effect/temp_visual/effect_trail/tendril_chaser/chaser = new primary_type(get_turf(owner), target)
+ LAZYADD(active_chasers, WEAKREF(chaser))
+ RegisterSignal(chaser, COMSIG_QDELETING, PROC_REF(on_chaser_destroyed))
+ playsound(owner, 'sound/effects/magic/demon_attack1.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
+
+/// Remove a spike trail from our list of active trails
+/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_chaser_destroyed(atom/chaser)
+ SIGNAL_HANDLER
+ LAZYREMOVE(active_chasers, WEAKREF(chaser))
+
+/obj/effect/temp_visual/effect_trail/tendril_chaser
+ duration = 10 SECONDS
+ move_speed = 4
+ spawned_effect = /obj/effect/temp_visual/emerging_ground_spike/tendril
+ /// Do we spawn spikes around ourselves as well or only on our own turf?
+ var/area_spawn = FALSE
+
+/obj/effect/temp_visual/effect_trail/tendril_chaser/wide_area
+ area_spawn = TRUE
+
+/obj/effect/temp_visual/effect_trail/tendril_chaser/add_spawner()
+ return
+
+/obj/effect/temp_visual/effect_trail/tendril_chaser/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ if (!area_spawn)
+ var/turf/spawn_turf = get_turf(src)
+ if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_turf) && isopenturf(spawn_turf))
+ new spawned_effect(spawn_turf)
+ return
+
+ for (var/spawn_dir in GLOB.cardinals)
+ if (spawn_dir & REVERSE_DIR(movement_dir))
+ continue
+ var/turf/spawn_loc = get_step(src, spawn_dir)
+ if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_loc) && isopenturf(spawn_loc))
+ new spawned_effect(spawn_loc)
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "spikes_stabbing"
+ duration = 0.7 SECONDS
+ position_variance = 3
+ impale_damage = 10
+ damage_blacklist_typecache = list(
+ /mob/living/basic/mining/tendril,
+ )
+ impale_wound_bonus = CANT_WOUND
+ // Have we hit someone yet?
+ var/hit_loser = FALSE
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/single
+ icon_state = "spike"
+ duration = 1 SECONDS
+ harm_delay = 0.25 SECONDS
+ position_variance = 5
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/impale()
+ . = ..()
+ hit_loser |= .
+ RegisterSignal(loc, COMSIG_ATOM_ENTERED, PROC_REF(on_entered))
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/proc/on_entered(atom/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+
+ if (!isliving(arrived))
+ return
+
+ if (harm_mob(arrived) && !hit_loser)
+ playsound(src, 'sound/items/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
+ hit_loser = TRUE
+
+/datum/action/cooldown/mob_cooldown/tendril_cross_spikes
+ name = "Cross Spikes"
+ desc = "Create a wave of spikes around yourself, impaling anyone caught in it."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "spike"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = FALSE
+ cooldown_time = 10 SECONDS
+ melee_cooldown_time = 0
+ shared_cooldown = NONE
+ /// Range in which we create spikes
+ var/spike_range = 7
+ /// Health threshold at which we reduce the amount of empty spots on the ground
+ var/health_threshold = 0.3
+
+/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/Activate(atom/target)
+ disable_cooldown_actions()
+ spawn_spikes()
+ SLEEP_CHECK_DEATH(0.4 SECONDS, owner)
+ spawn_spikes(inverse = TRUE)
+ StartCooldown()
+ enable_cooldown_actions()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/proc/spawn_spikes(inverse = FALSE)
+ var/list/turf/spike_turfs = list()
+ var/turf/owner_turf = get_turf(owner)
+
+ var/reduced_spawns = FALSE
+ if (isliving(owner))
+ var/mob/living/as_living = owner
+ if (as_living.health / as_living.maxHealth <= health_threshold)
+ reduced_spawns = TRUE
+
+ for (var/turf/open/target_turf in oview(spike_range, owner_turf))
+ if (sqrt((target_turf.x - owner_turf.x) ** 2 + (target_turf.y - owner_turf.y) ** 2) > 9.5) // big circle is a lie
+ continue
+
+ if (reduced_spawns)
+ if (abs(target_turf.x - owner_turf.x) % 2 == abs(target_turf.y - owner_turf.y + inverse) % 2)
+ new /obj/effect/temp_visual/telegraphing/circle/short(target_turf)
+ spike_turfs += target_turf
+ continue
+
+ var/row_diff = inverse ? abs(target_turf.x - owner_turf.x) : abs(target_turf.y - owner_turf.y)
+ var/column_diff = inverse ? abs(target_turf.y - owner_turf.y) : abs(target_turf.x - owner_turf.x)
+ var/row = floor((row_diff + 1) / 3)
+ if (row % 2 == 0)
+ if (row_diff - row * 3 == 0)
+ if (column_diff % 4 == 2)
+ continue
+ else
+ if (column_diff % 4 != 0)
+ continue
+ else
+ if (row_diff - row * 3 == 0)
+ if (column_diff % 4 == 0)
+ continue
+ else
+ if (column_diff % 4 != 2)
+ continue
+
+ new /obj/effect/temp_visual/telegraphing/circle/short(target_turf)
+ spike_turfs += target_turf
+
+ SLEEP_CHECK_DEATH(1 SECONDS, owner)
+
+ for (var/turf/open/to_spawn in spike_turfs)
+ new /obj/effect/temp_visual/emerging_ground_spike/tendril/single(to_spawn)
+
+ SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee
+ name = "Tentacle Stab"
+ desc = "Stab nearby hostiles with long tentacles."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "goliath_tentacle_wiggle"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = FALSE
+ cooldown_time = 4 SECONDS
+ melee_cooldown_time = 0
+ shared_cooldown = NONE
+ projectile_type = /obj/projectile/tentacle_lash/stab
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee/Activate(atom/target_atom, warning = TRUE)
+ if (warning)
+ for (var/stab_dir in GLOB.alldirs)
+ var/turf/open/stab_turf = get_step(owner, stab_dir)
+ if (!istype(stab_turf))
+ continue
+ var/obj/effect/temp_visual/telegraphing/line/short/telegraph = new(stab_turf)
+ telegraph.dir = stab_dir
+
+ SLEEP_CHECK_DEATH(0.5 SECONDS, owner)
+
+ for (var/stab_dir in GLOB.alldirs)
+ shoot_projectile(get_turf(owner), get_step(owner, stab_dir), firer = owner)
+
+ SLEEP_CHECK_DEATH(0.5 SECONDS, owner)
+ StartCooldown()
+ return TRUE
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm
new file mode 100644
index 000000000000..5da4fcedf285
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm
@@ -0,0 +1,40 @@
+/datum/ai_controller/basic_controller/tendril
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ BB_AGGRO_RANGE = 9, // Keeps an eye on you even if you flee
+ BB_AGGRO_GRAB_RANGE = 5, // Only aggros if you get real close and personal
+ )
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/call_reinforcements/mining,
+ /datum/ai_planning_subtree/target_retaliate/check_faction,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability/tendril_chaser,
+ /datum/ai_planning_subtree/use_mob_ability/tendril_spikes,
+ /datum/ai_planning_subtree/use_mob_ability/tendril_lash,
+ )
+
+/datum/ai_planning_subtree/targeted_mob_ability/tendril_chaser
+ ability_key = BB_TENDRIL_CHASER
+ operational_datums = list(/datum/component/ai_target_timer)
+ finish_planning = FALSE
+
+/datum/ai_planning_subtree/use_mob_ability/tendril_lash
+ ability_key = BB_TENDRIL_LASH
+
+/datum/ai_planning_subtree/use_mob_ability/tendril_lash/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ if (isnull(target) || get_dist(controller.pawn, target) > /obj/projectile/tentacle_lash::range)
+ return FALSE
+ return ..()
+
+/datum/ai_planning_subtree/use_mob_ability/tendril_spikes
+ ability_key = BB_TENDRIL_SPIKES
+
+/datum/ai_planning_subtree/use_mob_ability/tendril_spikes/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ var/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/ability = controller.blackboard[ability_key]
+ if (isnull(target) || !istype(ability) || get_dist(controller.pawn, target) > ability.spike_range)
+ return FALSE
+ return ..()
diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
index d31f76eaee6d..cd45eb9c6ecb 100644
--- a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
@@ -1,12 +1,16 @@
/datum/ai_controller/basic_controller/watcher
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
+ BB_RANGED_SKIRMISH_MIN_DISTANCE = 3,
+ BB_RANGED_SKIRMISH_MAX_DISTANCE = 5,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
+ /datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/target_retaliate/check_faction,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/maintain_distance,
diff --git a/code/modules/mob/living/basic/pets/orbie/orbie.dm b/code/modules/mob/living/basic/pets/orbie/orbie.dm
index fbbdfd89d22d..a75f1f5b0a2c 100644
--- a/code/modules/mob/living/basic/pets/orbie/orbie.dm
+++ b/code/modules/mob/living/basic/pets/orbie/orbie.dm
@@ -21,7 +21,7 @@
pass_flags = PASSMOB
move_force = 0
move_resist = 0
- pull_force = 0
+ pull_force = MOVE_FORCE_NONE
minimum_survivable_temperature = TCMB
maximum_survivable_temperature = INFINITY
death_message = "fades out of existence!"
diff --git a/code/modules/mob/living/basic/pets/parrot/poly.dm b/code/modules/mob/living/basic/pets/parrot/poly.dm
index 70840c73d27e..16d5c7dfead3 100644
--- a/code/modules/mob/living/basic/pets/parrot/poly.dm
+++ b/code/modules/mob/living/basic/pets/parrot/poly.dm
@@ -7,7 +7,7 @@
/// Poly has only just survived a round, and is doing a consecutive one.
#define POLY_CONSECUTIVE_ROUND "consecutive_round"
/// haunt filter we apply to who we possess
-#define POLY_POSSESS_FILTER
+#define POLY_POSSESS_FILTER "poly_possess_filter"
/// haunt filter color we apply to who we possess
#define POLY_POSSESS_GLOW "#522059"
diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp.dm b/code/modules/mob/living/basic/space_fauna/carp/carp.dm
index 6f1ff9e864ff..d114b6d42a1e 100644
--- a/code/modules/mob/living/basic/space_fauna/carp/carp.dm
+++ b/code/modules/mob/living/basic/space_fauna/carp/carp.dm
@@ -320,4 +320,4 @@
/// If someone slaps one of the school, scatter
/mob/living/basic/carp/passive/proc/on_attacked(mob/living/attacker)
for(var/mob/living/basic/carp/passive/schoolmate in oview(src, 9))
- schoolmate.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
+ schoolmate.ai_controller?.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)
diff --git a/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm b/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm
index c6116cd041a5..fa3015c011be 100644
--- a/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm
+++ b/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm
@@ -19,7 +19,7 @@
damage_coeff = list(BRUTE = 1, BURN = 1.25, TOX = 4, STAMINA = 1, OXY = 1)
basic_mob_flags = FLAMMABLE_MOB
status_flags = NONE
- speed = -0.1
+ speed = 0.2
maxHealth = 90
health = 90
melee_damage_lower = 15
@@ -61,7 +61,7 @@
)
AddComponent(\
/datum/component/regenerator,\
- regeneration_delay = 4 SECONDS,\
+ regeneration_delay = 3 SECONDS,\
brute_per_second = maxHealth / 6,\
outline_colour = COLOR_PINK,\
)
diff --git a/code/modules/mob/living/basic/space_fauna/garden_gnome.dm b/code/modules/mob/living/basic/space_fauna/garden_gnome.dm
index d2ab4e691b56..f6926b2ea51b 100644
--- a/code/modules/mob/living/basic/space_fauna/garden_gnome.dm
+++ b/code/modules/mob/living/basic/space_fauna/garden_gnome.dm
@@ -121,7 +121,7 @@
if (!istype(attacker))
return
for (var/mob/living/basic/garden_gnome/potential_gnome in oview(src, 7))
- potential_gnome.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
+ potential_gnome.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)
/datum/ai_controller/basic_controller/garden_gnome
blackboard = list(
diff --git a/code/modules/mob/living/basic/space_fauna/lightgeist.dm b/code/modules/mob/living/basic/space_fauna/lightgeist.dm
index 36ae135619c5..b86d50e2efba 100644
--- a/code/modules/mob/living/basic/space_fauna/lightgeist.dm
+++ b/code/modules/mob/living/basic/space_fauna/lightgeist.dm
@@ -41,18 +41,14 @@
minimum_survivable_temperature = 0
maximum_survivable_temperature = 1500
obj_damage = 0
+ pull_force = MOVE_FORCE_NONE
environment_smash = ENVIRONMENT_SMASH_NONE
ai_controller = /datum/ai_controller/basic_controller/lightgeist
/mob/living/basic/lightgeist/Initialize(mapload)
. = ..()
- ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
- ADD_TRAIT(src, TRAIT_MEDICAL_HUD, INNATE_TRAIT)
-
- remove_verb(src, /mob/living/verb/pulled)
- remove_verb(src, /mob/verb/me_verb)
-
+ add_traits(list(TRAIT_VENTCRAWLER_ALWAYS, TRAIT_MEDICAL_HUD, TRAIT_EMOTEMUTE), INNATE_TRAIT)
AddElement(/datum/element/simple_flying)
AddComponent(\
/datum/component/healing_touch,\
diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
index b50b089b0ffa..60288e9c5e74 100644
--- a/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
+++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
@@ -45,6 +45,8 @@
var/position_variance = 8
/// Damage to deal on impale
var/impale_damage = 15
+ /// Wound bonus on impale
+ var/impale_wound_bonus = 0
/// Typecache of types of mobs not to damage
var/list/damage_blacklist_typecache = list(
/mob/living/basic/meteor_heart,
@@ -67,14 +69,22 @@
/obj/effect/temp_visual/emerging_ground_spike/proc/impale()
if (!isturf(loc))
return
+
var/hit_someone = FALSE
for(var/mob/living/victim in loc)
- if (is_type_in_typecache(victim, damage_blacklist_typecache))
- continue
- hit_someone = TRUE
- var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
- victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, sharpness = SHARP_POINTY)
+ if (harm_mob(victim))
+ hit_someone = TRUE
+
if (hit_someone)
playsound(src, 'sound/items/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
else
playsound(src, 'sound/misc/splort.ogg', vol = 25, vary = TRUE, pressure_affected = FALSE)
+ return hit_someone
+
+/obj/effect/temp_visual/emerging_ground_spike/proc/harm_mob(mob/living/victim)
+ if (is_type_in_typecache(victim, damage_blacklist_typecache))
+ return FALSE
+
+ var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
+ victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, wound_bonus = impale_wound_bonus, sharpness = SHARP_POINTY)
+ return TRUE
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
index 3bd92b489505..af30342e1fe9 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
@@ -62,6 +62,8 @@
var/draining = FALSE
/// Have we already given this revenant abilities?
var/generated_objectives_and_spells = FALSE
+ /// ckey of the player who controlled this mob when it was killed
+ var/old_ckey = ""
/// Lazylist of drained mobs to ensure that we don't steal a soul from someone twice
var/list/drained_mobs = null
@@ -343,10 +345,7 @@
visible_message(span_danger("[src]'s body breaks apart into a fine pile of blue dust."))
- var/obj/item/ectoplasm/revenant/goop = new(get_turf(src)) // the ectoplasm will handle moving us out of dormancy
- goop.old_ckey = client.ckey
- goop.revenant = src
- forceMove(goop)
+ new /obj/item/ectoplasm/revenant(get_turf(src), src) // the ectoplasm will handle moving us out of dormancy
/mob/living/basic/revenant/proc/on_move(datum/source, atom/entering_loc)
SIGNAL_HANDLER
@@ -519,4 +518,30 @@
if(!HAS_TRAIT(src, TRAIT_REVENANT_REVEALED) && !istype(reflecting_in, /obj/structure/mirror/magic))
apply_wibbly_filters(reflection)
+/mob/living/basic/revenant/proc/get_new_user()
+ message_admins("A poll for the reforming revenant was created.")
+ var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to be [span_notice(name)] (reforming)?", check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, poll_time = 5 SECONDS, checked_target = src, alert_pic = src, role_name_text = "reforming revenant", chat_text_border_icon = src)
+ if(!chosen_one)
+ message_admins("No candidates were found for the new revenant.")
+ visible_message(span_revenwarning("A blue dust appears from thin air and settles down."))
+ new /obj/item/ectoplasm/revenant(get_turf(src)) // inert
+ qdel(src)
+ return
+
+ PossessByPlayer(chosen_one.key)
+ message_admins("[chosen_one.key] has been made into the reformed revenant via poll.")
+ qdel(chosen_one)
+
+/mob/living/basic/revenant/proc/reform(cause)
+ if(QDELETED(src))
+ return FALSE
+
+ death_reset()
+ if(isnull(client))
+ INVOKE_ASYNC(src, PROC_REF(get_new_user))
+ return TRUE
+
+ message_admins("[client.ckey] has been remade into a revenant.")
+ log_message("was remade as a revenant.", LOG_GAME)
+ return TRUE
#undef REVENANT_STUNNED_TRAIT
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
index 2030315410cc..86849f1ffd9f 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
@@ -1,7 +1,7 @@
#define REVENANT_DEFILE_MIN_DAMAGE 30
#define REVENANT_DEFILE_MAX_DAMAGE 50
-//Transmit: the revemant's only direct way to communicate. Sends a single message silently to a single mob
+//Transmit: the revenant's only direct way to communicate. Sends a single message silently to a single mob
/datum/action/cooldown/spell/list_target/telepathy/revenant
name = "Revenant Transmit"
background_icon_state = "bg_revenant"
@@ -12,6 +12,14 @@
antimagic_flags = MAGIC_RESISTANCE_HOLY|MAGIC_RESISTANCE_MIND
+/datum/action/cooldown/spell/list_target/telepathy/revenant/get_list_targets(atom/center, target_radius = 7)
+ if(!istype(center, /mob/living/basic/revenant))
+ return ..()
+ var/mob/living/basic/revenant/revenant = center
+ if(!revenant.dormant)
+ return ..()
+ return ..(get_turf(revenant), 2)
+
/datum/action/cooldown/spell/aoe/revenant
background_icon_state = "bg_revenant"
overlay_icon_state = "bg_revenant_border"
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm
index f691521b0765..b9007a7f7225 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm
@@ -5,31 +5,36 @@
icon = 'icons/effects/effects.dmi'
icon_state = "revenantEctoplasm"
w_class = WEIGHT_CLASS_SMALL
- /// Are we currently reforming?
- var/reforming = TRUE
- /// Are we inert (aka distorted such that we can't reform)?
+ // Can the revenant reform?
var/inert = FALSE
- /// The key of the revenant that we started the reform as
- var/old_ckey
- /// The revenant we're currently storing
- var/mob/living/basic/revenant/revenant
-/obj/item/ectoplasm/revenant/Initialize(mapload)
+/obj/item/ectoplasm/revenant/Initialize(mapload, revenant)
. = ..()
- addtimer(CALLBACK(src, PROC_REF(try_reform)), 1 MINUTES)
+ inert = !revenant
+ if(revenant)
+ AddComponent(/datum/component/revenant_prison, revenant = revenant)
+ addtimer(CALLBACK(src, PROC_REF(reform)), 1 MINUTES)
/obj/item/ectoplasm/revenant/Destroy()
- if(!QDELETED(revenant))
- qdel(revenant)
return ..()
+/obj/item/ectoplasm/revenant/proc/check_for_mirrors(turf/location, radius)
+ PRIVATE_PROC(TRUE)
+ for(var/obj/structure/mirror/mirror in view(radius, location))
+ if(mirror.cursable && !mirror.GetComponent(/datum/component/revenant_prison))
+ return mirror
+ return null
+
/obj/item/ectoplasm/revenant/attack_self(mob/user)
- if(!reforming || inert)
+ if(inert)
return ..()
user.visible_message(
span_notice("[user] scatters [src] in all directions."),
- span_notice("You scatter [src] across the area. The particles slowly fade away."),
+ span_notice("You scatter [src] across the area."),
)
+ var/obj/structure/mirror/nearby_mirror = check_for_mirrors(drop_location(), 5)
+ if(nearby_mirror)
+ transfer_to_mirror(nearby_mirror)
user.dropItemToGround(src)
qdel(src)
@@ -37,14 +42,25 @@
. = ..()
if(inert)
return
- visible_message(span_notice("[src] breaks into particles upon impact, which fade away to nothingness."))
+ var/obj/structure/mirror/nearby_mirror = check_for_mirrors(get_turf(hit_atom), 3)
+ if(!nearby_mirror)
+ visible_message(span_notice("[src] breaks into particles upon impact, which fade away to nothingness."))
+ else
+ transfer_to_mirror(nearby_mirror)
qdel(src)
+/obj/item/ectoplasm/revenant/proc/transfer_to_mirror(obj/structure/mirror/nearby_mirror)
+ PRIVATE_PROC(TRUE)
+ nearby_mirror.TakeComponent(GetComponent(/datum/component/revenant_prison))
+ nearby_mirror.visible_message(span_revenwarning("A dismal moan echoes as particles of [src] fall onto [nearby_mirror]!"))
+ log_game("A revenant was trapped inside [nearby_mirror]")
+ message_admins("A revenant was trapped inside [nearby_mirror] [ADMIN_JMP(nearby_mirror)]")
+
/obj/item/ectoplasm/revenant/examine(mob/user)
. = ..()
if(inert)
. += span_revennotice("It seems inert.")
- else if(reforming)
+ else
. += span_revenwarning("It is shifting and distorted. It would be wise to destroy this.")
/obj/item/ectoplasm/revenant/suicide_act(mob/living/user)
@@ -52,45 +68,13 @@
qdel(src)
return OXYLOSS
-/obj/item/ectoplasm/revenant/proc/try_reform()
- if(reforming)
- reforming = FALSE
- reform()
- else
- inert = TRUE
- visible_message(span_warning("[src] settles down and seems lifeless."))
-
/// Actually moves the revenant out of ourself
/obj/item/ectoplasm/revenant/proc/reform()
- if(QDELETED(src) || QDELETED(revenant) || inert)
+ if(QDELETED(src) || inert)
+ return
+ if(!GetComponent(/datum/component/revenant_prison))
return
-
message_admins("Revenant ectoplasm was left undestroyed for 1 minute and is reforming into a new revenant.")
- forceMove(drop_location()) //In case it's in a backpack or someone's hand
-
- var/user_name = old_ckey
- if(isnull(revenant.client))
- var/mob/potential_user = get_new_user()
- revenant.PossessByPlayer(potential_user.key)
- user_name = potential_user.ckey
- qdel(potential_user)
-
- message_admins("[user_name] has been [old_ckey == user_name ? "re":""]made into a revenant by reforming ectoplasm.")
- revenant.log_message("was [old_ckey == user_name ? "re":""]made as a revenant by reforming ectoplasm.", LOG_GAME)
+ SEND_SIGNAL(src, COMSIG_REVENANT_RELEASE, cause = "ectoplasm reforming")
visible_message(span_revenboldnotice("[src] suddenly rises into the air before fading away."))
-
- revenant.death_reset()
- revenant = null
qdel(src)
-
-/// Handles giving the revenant a new client to control it
-/obj/item/ectoplasm/revenant/proc/get_new_user()
- message_admins("The new revenant's old client either could not be found or is in a new, living mob - grabbing a random candidate instead...")
- var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to be [span_notice(revenant.name)] (reforming)?", check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, poll_time = 5 SECONDS, checked_target = revenant, alert_pic = revenant, role_name_text = "reforming revenant", chat_text_border_icon = revenant)
- if(isnull(chosen_one))
- message_admins("No candidates were found for the new revenant.")
- inert = TRUE
- visible_message(span_revenwarning("[src] settles down and seems lifeless."))
- qdel(revenant)
- return null
- return chosen_one
diff --git a/code/modules/mob/living/basic/trooper/nanotrasen.dm b/code/modules/mob/living/basic/trooper/nanotrasen.dm
index 80f0f348624d..8798467969b9 100644
--- a/code/modules/mob/living/basic/trooper/nanotrasen.dm
+++ b/code/modules/mob/living/basic/trooper/nanotrasen.dm
@@ -98,4 +98,4 @@
if (!istype(attacker))
return
for (var/mob/living/basic/trooper/nanotrasen/potential_trooper in oview(src, 7))
- potential_trooper.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
+ potential_trooper.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)
diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm
index 1455e49bc649..7bdb9779c11c 100644
--- a/code/modules/mob/living/blood.dm
+++ b/code/modules/mob/living/blood.dm
@@ -91,7 +91,6 @@
/// Updates effects that rely on blood volume or status, like blood HUDs.
/mob/living/proc/update_blood_effects()
- living_flags &= ~BLOOD_UPDATE_QUEUED
blood_hud_set_status()
/// Updates effects that rely on whether the mob can have blood.
@@ -161,16 +160,16 @@
if((sigreturn & HANDLE_BLOOD_HANDLED) || !CAN_HAVE_BLOOD(src))
return
+ var/heart_blood_multiplier = get_heart_blood_regeneration_multiplier()
//Blood regeneration if there is some space
- if(!(sigreturn & HANDLE_BLOOD_NO_NUTRITION_DRAIN) && get_blood_volume() < BLOOD_VOLUME_NORMAL && !HAS_TRAIT(src, TRAIT_NOHUNGER))
+ if(heart_blood_multiplier && !(sigreturn & HANDLE_BLOOD_NO_NUTRITION_DRAIN) && get_blood_volume() < BLOOD_VOLUME_NORMAL && !HAS_TRAIT(src, TRAIT_NOHUNGER))
var/nutrition_ratio = round(nutrition / NUTRITION_LEVEL_WELL_FED, 0.2)
if(satiety > 80)
nutrition_ratio *= 1.25
- var/blood_to_restore = BLOOD_REGEN_FACTOR * physiology.blood_regen_mod * nutrition_ratio * seconds_per_tick
+ var/blood_to_restore = BLOOD_REGEN_FACTOR * physiology.blood_regen_mod * heart_blood_multiplier * nutrition_ratio * seconds_per_tick
var/blood_restored = adjust_blood_volume(blood_to_restore, maximum = BLOOD_VOLUME_NORMAL)
-
if (blood_restored > 0)
adjust_nutrition(-nutrition_ratio * HUNGER_FACTOR * seconds_per_tick * (blood_restored / blood_to_restore))
@@ -192,7 +191,7 @@
var/modified_blood_volume = get_blood_volume(apply_modifiers = TRUE)
// Some effects are halved mid-combat.
- var/determined_mod = has_status_effect(/datum/status_effect/determined) ? 0.5 : 0
+ var/determined_mod = has_status_effect(/datum/status_effect/determined) ? 0.5 : 1
var/word = pick("dizzy","woozy","faint")
switch(modified_blood_volume)
@@ -476,17 +475,12 @@
blood_data["factions"] = faction
- // DARKPACK EDIT ADD - Store kindred clan and generation in blood data
- var/datum/splat/vampire/kindred/kindred_splat = get_kindred_splat(src)
- if(kindred_splat)
- blood_data["generation"] = kindred_splat.generation
- blood_data["clan"] = kindred_splat.clan
+ blood_data["splat"] = get_primary_splat()?.id// DARKPACK EDIT ADD - SPLATS
+ blood_data["donor"] = WEAKREF(src) // DARKPACK EDIT ADD - VITAE
+ // DARKPACK EDIT ADD START
+ blood_data["generation"] = get_generation()
+ blood_data["clan"] = get_clan()?.name
blood_data["real_name"] = real_name
-
- if(istype(src, /mob/living/carbon/human))
- var/mob/living/carbon/human/H = src
- if(H.dna && H.dna.species)
- blood_data["species"] = LOWER_TEXT(H.dna.species.name)
// DARKPACK EDIT ADD END
return blood_data
@@ -521,11 +515,13 @@
for(var/datum/quirk/quirk as anything in quirks)
blood_data["quirks"] += quirk.type
- // DARKPACK EDIT START - Vitae
- blood_data["donor"] = WEAKREF(src)
+ blood_data["splat"] = get_primary_splat()?.id// DARKPACK EDIT ADD - SPLATS
+ blood_data["donor"] = WEAKREF(src) // DARKPACK EDIT ADD - VITAE
+ // DARKPACK EDIT ADD START
blood_data["generation"] = get_generation()
blood_data["clan"] = get_clan()?.name
- // DARKPACK EDIT END
+ blood_data["real_name"] = real_name
+ // DARKPACK EDIT ADD END
return blood_data
diff --git a/code/modules/mob/living/blood_types.dm b/code/modules/mob/living/blood_types.dm
index 7335bd2204f1..6ec859b5d053 100644
--- a/code/modules/mob/living/blood_types.dm
+++ b/code/modules/mob/living/blood_types.dm
@@ -359,7 +359,7 @@
dna_string = "Plant DNA"
color = /datum/reagent/water::color
reagent_type = /datum/reagent/water
- restoration_chem = null
+ restoration_chem = /datum/reagent/plantnutriment/eznutriment
blood_flags = BLOOD_ADD_DNA | BLOOD_TRANSFER_VIRAL_DATA
// Prevents awkward grey wounds on the mob while keeping bleed overlays looking like water leaking from a balloon
diff --git a/code/modules/mob/living/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm
index 67dc73b3338c..e9247477c35a 100644
--- a/code/modules/mob/living/brain/brain_item.dm
+++ b/code/modules/mob/living/brain/brain_item.dm
@@ -241,8 +241,6 @@
. += span_notice("It is a bit on the smaller side...")
if(brain_size > 1)
. += span_notice("It is bigger than average...")
- if(GetComponent(/datum/component/ghostrole_on_revive))
- . += span_notice("Its soul might yet come back...")
/// Needed so subtypes can override examine text while still calling parent
/obj/item/organ/brain/proc/brain_damage_examine()
diff --git a/code/modules/mob/living/carbon/alien/adult/adult_update_icons.dm b/code/modules/mob/living/carbon/alien/adult/adult_update_icons.dm
index c45313137696..e85dc79f20c8 100644
--- a/code/modules/mob/living/carbon/alien/adult/adult_update_icons.dm
+++ b/code/modules/mob/living/carbon/alien/adult/adult_update_icons.dm
@@ -77,7 +77,6 @@
. = ..()
remove_overlay(HANDS_LAYER)
var/list/hands = list()
-
var/obj/item/l_hand = get_item_for_held_index(1)
if(l_hand)
var/itm_state = l_hand.inhand_icon_state
diff --git a/code/modules/mob/living/carbon/alien/adult/queen.dm b/code/modules/mob/living/carbon/alien/adult/queen.dm
index b388bba684d2..339ae0ac6ed8 100644
--- a/code/modules/mob/living/carbon/alien/adult/queen.dm
+++ b/code/modules/mob/living/carbon/alien/adult/queen.dm
@@ -83,7 +83,7 @@
/datum/action/cooldown/alien/promote,
)
grant_actions_by_list(innate_actions)
-
+ add_movespeed_mod_immunities(type, /datum/movespeed_modifier/tail_dragger)
return ..()
/mob/living/carbon/alien/adult/royal/queen/set_name()
diff --git a/code/modules/mob/living/carbon/alien/organs.dm b/code/modules/mob/living/carbon/alien/organs.dm
index ebbacdf2c02d..92b6b6c0bafc 100644
--- a/code/modules/mob/living/carbon/alien/organs.dm
+++ b/code/modules/mob/living/carbon/alien/organs.dm
@@ -85,7 +85,6 @@
/obj/item/organ/alien/plasmavessel/on_mob_insert(mob/living/carbon/organ_owner)
. = ..()
- RegisterSignal(organ_owner, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item))
RegisterSignal(organ_owner, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created))
if (organ_owner.hud_used)
on_hud_created()
@@ -93,13 +92,8 @@
/obj/item/organ/alien/plasmavessel/on_mob_remove(mob/living/carbon/organ_owner)
. = ..()
UnregisterSignal(organ_owner, COMSIG_MOB_GET_STATUS_TAB_ITEMS)
- UnregisterSignal(organ_owner, COMSIG_MOB_HUD_CREATED)
organ_owner.hud_used?.remove_screen_object(HUD_ALIEN_PLASMA_DISPLAY)
-/obj/item/organ/alien/plasmavessel/proc/get_status_tab_item(mob/living/carbon/source, list/items)
- SIGNAL_HANDLER
- items += "Plasma Stored: [stored_plasma]/[max_plasma]"
-
/obj/item/organ/alien/plasmavessel/proc/update_plasma_display()
owner.hud_used?.screen_objects[HUD_ALIEN_PLASMA_DISPLAY]?.maptext = MAPTEXT( \
"
[round(owner.getPlasma())]
" \
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 068db7caf721..bdf153a8bd79 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -213,14 +213,15 @@
* @param {number} breakouttime - The time it takes to break the cuffs. Use SECONDS/MINUTES defines
* @param {number} cuff_break - Speed multiplier, 0 is default, see _DEFINES\combat.dm
*/
-/mob/living/carbon/proc/cuff_resist(obj/item/cuffs, breakouttime = 1 MINUTES, cuff_break = 0)
+/mob/living/carbon/proc/cuff_resist(obj/item/cuffs, breakouttime = null, cuff_break = 0)
if((cuff_break != INSTANT_CUFFBREAK) && (SEND_SIGNAL(src, COMSIG_MOB_REMOVING_CUFFS, cuffs) & COMSIG_MOB_BLOCK_CUFF_REMOVAL))
return //The blocking object should sent a fluff-appropriate to_chat about cuff removal being blocked
if(cuffs.item_flags & BEING_REMOVED)
to_chat(src, span_warning("You're already attempting to remove [cuffs]!"))
return
cuffs.item_flags |= BEING_REMOVED
- breakouttime = cuffs.breakouttime
+ if (isnull(breakouttime))
+ breakouttime = cuffs.breakouttime
if(!cuff_break)
visible_message(span_warning("[src] attempts to remove [cuffs]!"))
to_chat(src, span_notice("You attempt to remove [cuffs]... (This will take around [DisplayTimeText(breakouttime)] and you need to stand still.)"))
@@ -745,7 +746,6 @@
clear_mood_event("handcuffed")
update_mob_action_buttons() //some of our action buttons might be unusable when we're handcuffed.
update_worn_handcuffs()
- update_hud_handcuffed()
/mob/living/carbon/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE)
if(excess_healing)
@@ -984,6 +984,7 @@
var/obj/item/bodypart/stump = new old_bodypart.stump_typepath()
stump.bodyshape = old_bodypart.bodyshape
stump.bodytype = old_bodypart.bodytype
+ stump.add_biostate(old_bodypart.biological_state & ~BIO_JOINTED)
if(!stump.try_attach_limb(src, special = TRUE))
// the only way this can happen is if the stump is rejected via signal
// not much we can do about that besides hope they know what they're doing
diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm
index 497f133b117e..6f1cf08d1ad5 100644
--- a/code/modules/mob/living/carbon/carbon_defense.dm
+++ b/code/modules/mob/living/carbon/carbon_defense.dm
@@ -661,6 +661,9 @@
if (HAS_TRAIT(src, TRAIT_GENELESS))
return FALSE
+ if(flags_1 & HOLOGRAM_1)
+ return FALSE
+
if (run_armor_check(attack_flag = BIO, silent = TRUE) >= 100)
to_chat(src, span_warning("Your armor shields you from [scramble_source]!"))
return FALSE
@@ -671,7 +674,7 @@
var/changed_something = FALSE
var/obj/item/organ/new_organ = pick(GLOB.bioscrambler_valid_organs)
var/obj/item/organ/replaced = get_organ_slot(initial(new_organ.slot))
- if (!replaced || !IS_ROBOTIC_ORGAN(replaced))
+ if (!replaced || ORGAN_CAN_BE_BIOSCRAMBLED(replaced))
changed_something = TRUE
new_organ = new new_organ()
new_organ.replace_into(src)
@@ -680,7 +683,7 @@
if (!HAS_TRAIT(src, TRAIT_NODISMEMBER))
var/obj/item/bodypart/new_part = pick(GLOB.bioscrambler_valid_parts)
var/obj/item/bodypart/picked_user_part = get_bodypart(initial(new_part.body_zone))
- if (picked_user_part && BODYTYPE_CAN_BE_BIOSCRAMBLED(picked_user_part.bodytype))
+ if (picked_user_part && BODYPART_CAN_BE_BIOSCRAMBLED(picked_user_part))
changed_something = TRUE
new_part = new new_part()
new_part.replace_limb(src)
@@ -698,7 +701,7 @@
/mob/living/carbon/proc/init_bioscrambler_lists()
var/list/body_parts = typesof(/obj/item/bodypart/chest) + typesof(/obj/item/bodypart/head) + subtypesof(/obj/item/bodypart/arm) + subtypesof(/obj/item/bodypart/leg)
for(var/obj/item/bodypart/part as anything in body_parts)
- if(!is_type_in_typecache(part, GLOB.bioscrambler_parts_blacklist) && BODYTYPE_CAN_BE_BIOSCRAMBLED(initial(part.bodytype)))
+ if(!is_type_in_typecache(part, GLOB.bioscrambler_parts_blacklist) && BODYPART_CAN_BE_BIOSCRAMBLED(part))
continue
body_parts -= part
GLOB.bioscrambler_valid_parts = body_parts
diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm
index 171e53c65afc..7280ef782a11 100644
--- a/code/modules/mob/living/carbon/carbon_update_icons.dm
+++ b/code/modules/mob/living/carbon/carbon_update_icons.dm
@@ -253,24 +253,11 @@
/mob/living/carbon/proc/get_held_overlays()
var/list/hands = list()
for(var/obj/item/I in held_items)
- if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
- I.screen_loc = ui_hand_position(get_held_index_of_item(I))
- client.screen += I
- if(length(observers))
- for(var/mob/dead/observe as anything in observers)
- if(observe.client && observe.client.eye == src)
- observe.client.screen += I
- else
- observers -= observe
- if(!observers.len)
- observers = null
- break
-
var/icon_file = I.lefthand_file
if(IS_RIGHT_INDEX(get_held_index_of_item(I)))
icon_file = I.righthand_file
- hands += I.build_worn_icon(default_layer = HANDS_LAYER, default_icon_file = icon_file, isinhands = TRUE)
+ hands += I.build_worn_icon(default_layer = HANDS_LAYER, default_icon_file = icon_file, isinhands = TRUE, bodyshape = bodyshape)
return hands
/mob/living/carbon/get_fire_overlay(stacks, on_fire)
@@ -334,76 +321,61 @@
/mob/living/carbon/update_worn_mask()
remove_overlay(FACEMASK_LAYER)
+ hud_used?.update_inventory_slot(ITEM_SLOT_MASK)
if(!get_bodypart(BODY_ZONE_HEAD)) //Decapitated
return
- if(client && hud_used?.inv_slots[TOBITSHIFT(ITEM_SLOT_MASK) + 1])
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_MASK) + 1]
- inv.update_appearance()
-
- if(wear_mask)
- if(!(obscured_slots & HIDEMASK))
- overlays_standing[FACEMASK_LAYER] = wear_mask.build_worn_icon(default_layer = FACEMASK_LAYER, default_icon_file = 'icons/mob/clothing/mask.dmi')
- update_hud_wear_mask(wear_mask)
+ if(wear_mask && !(obscured_slots & HIDEMASK))
+ overlays_standing[FACEMASK_LAYER] = wear_mask.build_worn_icon(default_layer = FACEMASK_LAYER, default_icon_file = 'icons/mob/clothing/mask.dmi')
+ overlays_standing[FACEMASK_LAYER] = wear_mask.build_worn_icon(default_layer = FACEMASK_LAYER, default_icon_file = 'icons/mob/clothing/mask.dmi', bodyshape = bodyshape)
apply_overlay(FACEMASK_LAYER)
/mob/living/carbon/update_worn_neck()
remove_overlay(NECK_LAYER)
+ hud_used?.update_inventory_slot(ITEM_SLOT_NECK)
- if(client && hud_used?.inv_slots[TOBITSHIFT(ITEM_SLOT_NECK) + 1])
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_NECK) + 1]
- inv.update_appearance()
-
- if(wear_neck)
- if(!(obscured_slots & HIDENECK))
- overlays_standing[NECK_LAYER] = wear_neck.build_worn_icon(default_layer = NECK_LAYER, default_icon_file = 'icons/mob/clothing/neck.dmi')
- update_hud_neck(wear_neck)
-
+ if(wear_neck && !(obscured_slots & HIDENECK))
+ overlays_standing[NECK_LAYER] = wear_neck.build_worn_icon(default_layer = NECK_LAYER, default_icon_file = 'icons/mob/clothing/neck.dmi', bodyshape = bodyshape)
apply_overlay(NECK_LAYER)
/mob/living/carbon/update_worn_back()
remove_overlay(BACK_LAYER)
-
- if(client && hud_used?.inv_slots[TOBITSHIFT(ITEM_SLOT_BACK) + 1])
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_BACK) + 1]
- inv.update_appearance()
+ hud_used?.update_inventory_slot(ITEM_SLOT_BACK)
if(back)
- overlays_standing[BACK_LAYER] = back.build_worn_icon(default_layer = BACK_LAYER, default_icon_file = 'icons/mob/clothing/back.dmi')
- update_hud_back(back)
+ overlays_standing[BACK_LAYER] = back.build_worn_icon(default_layer = BACK_LAYER, default_icon_file = 'icons/mob/clothing/back.dmi', bodyshape = bodyshape)
apply_overlay(BACK_LAYER)
/mob/living/carbon/update_worn_legcuffs()
remove_overlay(LEGCUFF_LAYER)
clear_alert("legcuffed")
- if(legcuffed)
- overlays_standing[LEGCUFF_LAYER] = mutable_appearance('icons/mob/simple/mob.dmi', "legcuff1", -LEGCUFF_LAYER)
- apply_overlay(LEGCUFF_LAYER)
- throw_alert("legcuffed", /atom/movable/screen/alert/restrained/legcuffed, new_master = src.legcuffed)
+ if(!legcuffed)
+ return
+ if(astype(legcuffed, /obj/item/restraints/legcuffs)?.legcuff_state)
+ var/obj/item/restraints/legcuffs/cuffs = legcuffed
+ overlays_standing[LEGCUFF_LAYER] = mutable_appearance('icons/mob/simple/mob.dmi', cuffs.legcuff_state, -LEGCUFF_LAYER)
+ apply_overlay(LEGCUFF_LAYER)
+ throw_alert("legcuffed", /atom/movable/screen/alert/restrained/legcuffed, new_master = src.legcuffed)
/mob/living/carbon/update_worn_head()
remove_overlay(HEAD_LAYER)
+ hud_used?.update_inventory_slot(ITEM_SLOT_HEAD)
if(!get_bodypart(BODY_ZONE_HEAD)) //Decapitated
return
- if(client && hud_used?.inv_slots[TOBITSHIFT(ITEM_SLOT_HEAD) + 1])
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_HEAD) + 1]
- inv.update_appearance()
-
- if(head)
- if(!(obscured_slots & HIDEHEADGEAR))
- overlays_standing[HEAD_LAYER] = head.build_worn_icon(default_layer = HEAD_LAYER, default_icon_file = 'icons/mob/clothing/head/default.dmi')
- update_hud_head(head)
+ if(head && !(obscured_slots & HIDEHEADGEAR))
+ overlays_standing[HEAD_LAYER] = head.build_worn_icon(default_layer = HEAD_LAYER, default_icon_file = 'icons/mob/clothing/head/default.dmi', bodyshape = bodyshape)
apply_overlay(HEAD_LAYER)
/mob/living/carbon/update_worn_handcuffs()
remove_overlay(HANDCUFF_LAYER)
+ hud_used?.update_inventory_slot(ITEM_SLOT_HANDS)
if(handcuffed)
var/mutable_appearance/handcuff_overlay = mutable_appearance('icons/mob/simple/mob.dmi', "handcuff1", -HANDCUFF_LAYER)
if(handcuffed.blocks_emissive != EMISSIVE_BLOCK_NONE)
@@ -415,30 +387,6 @@
//mob HUD updates for items in our inventory
-//update whether handcuffs appears on our hud.
-/mob/living/carbon/proc/update_hud_handcuffed()
- if(!hud_used)
- return
-
- for(var/atom/movable/screen/inventory/hand/hand in hud_used.hand_slots)
- hand.update_appearance()
-
-//update whether our head item appears on our hud.
-/mob/living/carbon/proc/update_hud_head(obj/item/I)
- return
-
-//update whether our mask item appears on our hud.
-/mob/living/carbon/proc/update_hud_wear_mask(obj/item/I)
- return
-
-//update whether our neck item appears on our hud.
-/mob/living/carbon/proc/update_hud_neck(obj/item/I)
- return
-
-//update whether our back item appears on our hud.
-/mob/living/carbon/proc/update_hud_back(obj/item/I)
- return
-
/// Overlays for the worn overlay so you can overlay while you overlay
/// eg: ammo counters, primed grenade flashing, etc.
/// "icon_file" is used automatically for inhands etc. to make sure it gets the right inhand file
@@ -545,7 +493,7 @@
for(var/datum/bodypart_overlay/overlay as anything in bodypart_overlays)
if(!overlay.can_draw_on_bodypart(src, owner, is_husked))
continue
- . += overlay.generate_icon_cache()
+ . += overlay.generate_icon_cache(src)
if(ishuman(owner))
var/mob/living/carbon/human/human_owner = owner
. += "[human_owner.mob_height]"
@@ -574,7 +522,7 @@
for(var/datum/bodypart_overlay/overlay as anything in bodypart_overlays)
if(!overlay.can_draw_on_bodypart(src, owner, TRUE))
continue
- . += overlay.generate_icon_cache()
+ . += overlay.generate_icon_cache(src)
if(ishuman(owner))
var/mob/living/carbon/human/human_owner = owner
. += "[human_owner.mob_height]"
diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm
index f35342173224..16984f115f5e 100644
--- a/code/modules/mob/living/carbon/examine.dm
+++ b/code/modules/mob/living/carbon/examine.dm
@@ -361,7 +361,7 @@
//This checks to see if the body is revivable
var/obj/item/organ/brain = get_organ_by_type(/obj/item/organ/brain)
if((brain && HAS_TRAIT(brain, TRAIT_GHOSTROLE_ON_REVIVE)) || HAS_TRAIT(src, TRAIT_GHOSTROLE_ON_REVIVE))
- return span_deadsay("[t_He] [t_is] limp and unresponsive; but [t_his] soul might yet come back...")
+ return span_deadsay("[t_He] [t_is] limp and unresponsive; there are no signs of life, but another soul may take [t_his] place...")
var/client_like = client || HAS_TRAIT(src, TRAIT_MIND_TEMPORARILY_GONE)
var/valid_ghost = ghost?.can_reenter_corpse && ghost?.client
var/valid_soul = brain || !HAS_TRAIT(src, TRAIT_FAKE_SOULLESS)
diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm
index d56e98ba1aeb..6b1a314b0ea7 100644
--- a/code/modules/mob/living/carbon/human/_species.dm
+++ b/code/modules/mob/living/carbon/human/_species.dm
@@ -552,11 +552,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
return new_features
-/datum/species/proc/spec_life(mob/living/carbon/human/H, seconds_per_tick)
- SHOULD_CALL_PARENT(TRUE)
- if(HAS_TRAIT(H, TRAIT_NOBREATH) && (H.health < H.crit_threshold) && !HAS_TRAIT(H, TRAIT_NOCRITDAMAGE))
- H.adjust_brute_loss(0.5 * seconds_per_tick)
-
/datum/species/proc/can_equip(obj/item/I, slot, disable_warning, mob/living/carbon/human/H, bypass_equip_delay_self = FALSE, ignore_equipped = FALSE, indirect_action = FALSE)
if(no_equip_flags & slot)
if(!I.species_exception || !is_type_in_list(src, I.species_exception))
@@ -603,7 +598,7 @@ GLOBAL_LIST_EMPTY(features_by_species)
if(ITEM_SLOT_FEET)
if(H.num_legs < 2)
return FALSE
- if((H.bodyshape & BODYSHAPE_DIGITIGRADE) && !(I.item_flags & IGNORE_DIGITIGRADE))
+ if((H.bodytype & BODYTYPE_DIGITIGRADE) && !(I.item_flags & IGNORE_DIGITIGRADE))
if(!(I.supports_variations_flags & DIGITIGRADE_VARIATIONS))
if(!disable_warning)
to_chat(H, span_warning("The footwear around here isn't compatible with your feet!"))
diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm
index 567df82d3fc8..d24f8f524692 100644
--- a/code/modules/mob/living/carbon/human/death.dm
+++ b/code/modules/mob/living/carbon/human/death.dm
@@ -40,6 +40,8 @@ GLOBAL_LIST_EMPTY(dead_players_during_shift)
Reagents: [reagents_readout()]", INVESTIGATE_DEATHS)
to_chat(src, span_warning("You have died. Barring complete bodyloss, you can in most cases be revived by other players. \
If you do not wish to be brought back, use the \"Do Not Resuscitate\" button at the bottom of your screen."))
+ if(SSlag_switch.measures[DISABLE_DEAD_KEYLOOP] && !client?.holder)
+ to_chat(src, span_warning("Ghost movement is currently disabled by admins. To leave your body use the Ghost verb."))
/mob/living/carbon/human/proc/reagents_readout()
var/readout = "[get_bloodtype()?.get_blood_name() || "Blood"]stream:"
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 93ff73faf36e..9faf2b2fcacf 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -714,7 +714,7 @@
if(heal_flags & HEAL_NEGATIVE_MUTATIONS)
for(var/datum/mutation/existing_mutation in dna.mutations)
if(existing_mutation.quality != POSITIVE && existing_mutation.remove_on_aheal)
- dna.remove_mutation(existing_mutation, list(MUTATION_SOURCE_ACTIVATED, MUTATION_SOURCE_MUTATOR, MUTATION_SOURCE_TIMED_INJECTOR))
+ dna.remove_mutation(existing_mutation, GLOB.standard_mutation_sources)
if(heal_flags & HEAL_TEMP)
set_coretemperature(get_body_temp_normal(apply_change = FALSE))
diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm
index 5eaf99419f1a..e15594a8eec6 100644
--- a/code/modules/mob/living/carbon/human/human_update_icons.dm
+++ b/code/modules/mob/living/carbon/human/human_update_icons.dm
@@ -75,15 +75,10 @@ There are several things that need to be remembered:
/mob/living/carbon/human/update_worn_undersuit()
remove_overlay(UNIFORM_LAYER)
-
- if(client && hud_used)
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_ICLOTHING) + 1]
- inv.update_icon()
+ hud_used?.update_inventory_slot(ITEM_SLOT_ICLOTHING)
if(istype(w_uniform, /obj/item/clothing/under))
var/obj/item/clothing/under/uniform = w_uniform
- update_hud_uniform(uniform)
-
if(HAS_TRAIT(uniform, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDEJUMPSUIT))
return
@@ -120,6 +115,7 @@ There are several things that need to be remembered:
female_uniform = woman ? uniform.female_sprite_flags : null,
override_state = target_overlay,
override_file = handled_by_bodyshape ? icon_file : null,
+ bodyshape = bodyshape,
)
var/obj/item/bodypart/chest/my_chest = get_bodypart(BODY_ZONE_CHEST)
@@ -127,27 +123,22 @@ There are several things that need to be remembered:
overlays_standing[UNIFORM_LAYER] = uniform_overlay
apply_overlay(UNIFORM_LAYER)
- check_body_shape(BODYSHAPE_DIGITIGRADE, ITEM_SLOT_ICLOTHING)
/mob/living/carbon/human/update_worn_id()
remove_overlay(ID_LAYER)
-
- if(client && hud_used)
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_ID) + 1]
- inv.update_icon()
+ hud_used?.update_inventory_slot(ITEM_SLOT_ID)
var/mutable_appearance/id_overlay = overlays_standing[ID_LAYER]
if(wear_id)
var/obj/item/worn_item = wear_id
- update_hud_id(worn_item)
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON))
return
var/icon_file = 'icons/mob/clothing/id.dmi'
- id_overlay = wear_id.build_worn_icon(default_layer = ID_LAYER, default_icon_file = icon_file)
+ id_overlay = wear_id.build_worn_icon(default_layer = ID_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
if(!id_overlay)
return
@@ -161,10 +152,7 @@ There are several things that need to be remembered:
/mob/living/carbon/human/update_worn_gloves()
remove_overlay(GLOVES_LAYER)
-
- if(client && hud_used && hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_GLOVES) + 1])
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_GLOVES) + 1]
- inv.update_icon()
+ hud_used?.update_inventory_slot(ITEM_SLOT_GLOVES)
//Bloody hands begin
if(isnull(gloves))
@@ -186,14 +174,12 @@ There are several things that need to be remembered:
// Bloody hands end
var/obj/item/worn_item = gloves
- update_hud_gloves(worn_item)
-
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDEGLOVES))
return
var/icon_file = 'icons/mob/clothing/hands.dmi'
- var/mutable_appearance/gloves_overlay = gloves.build_worn_icon(default_layer = GLOVES_LAYER, default_icon_file = icon_file)
+ var/mutable_appearance/gloves_overlay = gloves.build_worn_icon(default_layer = GLOVES_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
var/feature_y_offset = 0
//needs to be typed, hand_bodyparts can have nulls
@@ -222,25 +208,20 @@ There are several things that need to be remembered:
/mob/living/carbon/human/update_worn_glasses()
remove_overlay(GLASSES_LAYER)
+ hud_used?.update_inventory_slot(ITEM_SLOT_EYES)
var/obj/item/bodypart/head/my_head = get_bodypart(BODY_ZONE_HEAD)
if(isnull(my_head)) //decapitated
return
- if(client && hud_used)
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_EYES) + 1]
- inv.update_icon()
-
if(glasses)
var/obj/item/worn_item = glasses
- update_hud_glasses(worn_item)
-
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDEEYES))
return
var/icon_file = 'icons/mob/clothing/eyes.dmi'
- var/mutable_appearance/glasses_overlay = glasses.build_worn_icon(default_layer = GLASSES_LAYER, default_icon_file = icon_file)
+ var/mutable_appearance/glasses_overlay = glasses.build_worn_icon(default_layer = GLASSES_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
my_head.worn_glasses_offset?.apply_offset(glasses_overlay)
overlays_standing[GLASSES_LAYER] = glasses_overlay
apply_overlay(GLASSES_LAYER)
@@ -248,46 +229,37 @@ There are several things that need to be remembered:
/mob/living/carbon/human/update_worn_ears()
remove_overlay(EARS_LAYER)
+ hud_used?.update_inventory_slot(ITEM_SLOT_EARS)
var/obj/item/bodypart/head/my_head = get_bodypart(BODY_ZONE_HEAD)
if(isnull(my_head)) //decapitated
return
- if(client && hud_used)
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_EARS) + 1]
- inv.update_icon()
-
if(ears)
var/obj/item/worn_item = ears
- update_hud_ears(worn_item)
-
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDEEARS))
return
var/icon_file = 'icons/mob/clothing/ears.dmi'
- var/mutable_appearance/ears_overlay = ears.build_worn_icon(default_layer = EARS_LAYER, default_icon_file = icon_file)
+ var/mutable_appearance/ears_overlay = ears.build_worn_icon(default_layer = EARS_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
my_head.worn_ears_offset?.apply_offset(ears_overlay)
overlays_standing[EARS_LAYER] = ears_overlay
apply_overlay(EARS_LAYER)
/mob/living/carbon/human/update_worn_neck()
remove_overlay(NECK_LAYER)
-
- if(client && hud_used && hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_NECK) + 1])
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_NECK) + 1]
- inv.update_icon()
+ hud_used?.update_inventory_slot(ITEM_SLOT_NECK)
if(wear_neck)
var/obj/item/worn_item = wear_neck
- update_hud_neck(wear_neck)
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDENECK))
return
var/icon_file = 'icons/mob/clothing/neck.dmi'
- var/mutable_appearance/neck_overlay = worn_item.build_worn_icon(default_layer = NECK_LAYER, default_icon_file = icon_file)
+ var/mutable_appearance/neck_overlay = worn_item.build_worn_icon(default_layer = NECK_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
var/obj/item/bodypart/chest/my_chest = get_bodypart(BODY_ZONE_CHEST)
my_chest?.worn_neck_offset?.apply_offset(neck_overlay)
overlays_standing[NECK_LAYER] = neck_overlay
@@ -296,24 +268,20 @@ There are several things that need to be remembered:
/mob/living/carbon/human/update_worn_shoes()
remove_overlay(SHOES_LAYER)
+ hud_used?.update_inventory_slot(ITEM_SLOT_FEET)
if(num_legs < 2)
return
- if(client && hud_used)
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_FEET) + 1]
- inv.update_icon()
-
if(shoes)
var/obj/item/worn_item = shoes
- update_hud_shoes(worn_item)
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDESHOES))
return
var/icon_file = DEFAULT_SHOES_FILE
- var/mutable_appearance/shoes_overlay = shoes.build_worn_icon(default_layer = SHOES_LAYER, default_icon_file = icon_file)
+ var/mutable_appearance/shoes_overlay = shoes.build_worn_icon(default_layer = SHOES_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
if(!shoes_overlay)
return
@@ -330,19 +298,13 @@ There are several things that need to be remembered:
overlays_standing[SHOES_LAYER] = shoes_overlay
apply_overlay(SHOES_LAYER)
- check_body_shape(BODYSHAPE_DIGITIGRADE, ITEM_SLOT_FEET)
/mob/living/carbon/human/update_suit_storage()
remove_overlay(SUIT_STORE_LAYER)
-
- if(client && hud_used)
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_SUITSTORE) + 1]
- inv.update_icon()
+ hud_used?.update_inventory_slot(ITEM_SLOT_SUITSTORE)
if(s_store)
var/obj/item/worn_item = s_store
- update_hud_s_store(worn_item)
-
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDESUITSTORAGE))
return
@@ -354,44 +316,34 @@ There are several things that need to be remembered:
/mob/living/carbon/human/update_worn_head()
remove_overlay(HEAD_LAYER)
- if(client && hud_used && hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_HEAD) + 1])
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_HEAD) + 1]
- inv.update_icon()
+ hud_used?.update_inventory_slot(ITEM_SLOT_HEAD)
if(head)
var/obj/item/worn_item = head
- update_hud_head(worn_item)
-
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDEHEADGEAR))
return
var/icon_file = 'icons/mob/clothing/head/default.dmi'
- var/mutable_appearance/head_overlay = head.build_worn_icon(default_layer = HEAD_LAYER, default_icon_file = icon_file)
+ var/mutable_appearance/head_overlay = head.build_worn_icon(default_layer = HEAD_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
var/obj/item/bodypart/head/my_head = get_bodypart(BODY_ZONE_HEAD)
my_head?.worn_head_offset?.apply_offset(head_overlay)
overlays_standing[HEAD_LAYER] = head_overlay
apply_overlay(HEAD_LAYER)
- check_body_shape(BODYSHAPE_SNOUTED, ITEM_SLOT_HEAD)
/mob/living/carbon/human/update_worn_belt()
remove_overlay(BELT_LAYER)
-
- if(client && hud_used)
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_BELT) + 1]
- inv.update_icon()
+ hud_used?.update_inventory_slot(ITEM_SLOT_BELT)
if(belt)
var/obj/item/worn_item = belt
- update_hud_belt(worn_item)
-
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDEBELT))
return
var/icon_file = 'icons/mob/clothing/belt.dmi'
- var/mutable_appearance/belt_overlay = belt.build_worn_icon(default_layer = BELT_LAYER, default_icon_file = icon_file)
+ var/mutable_appearance/belt_overlay = belt.build_worn_icon(default_layer = BELT_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
var/obj/item/bodypart/chest/my_chest = get_bodypart(BODY_ZONE_CHEST)
my_chest?.worn_belt_offset?.apply_offset(belt_overlay)
overlays_standing[BELT_LAYER] = belt_overlay
@@ -400,94 +352,63 @@ There are several things that need to be remembered:
/mob/living/carbon/human/update_worn_oversuit()
remove_overlay(SUIT_LAYER)
-
- if(client && hud_used)
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_OCLOTHING) + 1]
- inv.update_icon()
+ hud_used?.update_inventory_slot(ITEM_SLOT_OCLOTHING)
if(wear_suit)
var/obj/item/worn_item = wear_suit
- update_hud_wear_suit(worn_item)
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON))
return
var/icon_file = DEFAULT_SUIT_FILE
- var/mutable_appearance/suit_overlay = wear_suit.build_worn_icon(default_layer = SUIT_LAYER, default_icon_file = icon_file)
+ var/mutable_appearance/suit_overlay = wear_suit.build_worn_icon(default_layer = SUIT_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
var/obj/item/bodypart/chest/my_chest = get_bodypart(BODY_ZONE_CHEST)
my_chest?.worn_suit_offset?.apply_offset(suit_overlay)
overlays_standing[SUIT_LAYER] = suit_overlay
apply_overlay(SUIT_LAYER)
- check_body_shape(BODYSHAPE_DIGITIGRADE, ITEM_SLOT_OCLOTHING)
/mob/living/carbon/human/update_pockets()
- if(client && hud_used)
- var/atom/movable/screen/inventory/inv
-
- inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_LPOCKET) + 1]
- inv.update_icon()
- inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_RPOCKET) + 1]
- inv.update_icon()
-
- if(l_store)
- l_store.screen_loc = ui_storage1
- if(hud_used.hud_shown)
- client.screen += l_store
- update_observer_view(l_store)
-
- if(r_store)
- r_store.screen_loc = ui_storage2
- if(hud_used.hud_shown)
- client.screen += r_store
- update_observer_view(r_store)
+ if (hud_used)
+ hud_used.update_inventory_slot(ITEM_SLOT_LPOCKET)
+ hud_used.update_inventory_slot(ITEM_SLOT_RPOCKET)
/mob/living/carbon/human/update_worn_mask()
remove_overlay(FACEMASK_LAYER)
+ hud_used?.update_inventory_slot(ITEM_SLOT_MASK)
var/obj/item/bodypart/head/my_head = get_bodypart(BODY_ZONE_HEAD)
if(isnull(my_head)) //Decapitated
return
- if(client && hud_used && hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_MASK) + 1])
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_MASK) + 1]
- inv.update_icon()
-
if(wear_mask)
var/obj/item/worn_item = wear_mask
- update_hud_wear_mask(worn_item)
-
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON) || (obscured_slots & HIDEMASK))
return
var/icon_file = 'icons/mob/clothing/mask.dmi'
- var/mutable_appearance/mask_overlay = wear_mask.build_worn_icon(default_layer = FACEMASK_LAYER, default_icon_file = icon_file)
+ var/mutable_appearance/mask_overlay = wear_mask.build_worn_icon(default_layer = FACEMASK_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
my_head.worn_mask_offset?.apply_offset(mask_overlay)
overlays_standing[FACEMASK_LAYER] = mask_overlay
apply_overlay(FACEMASK_LAYER)
- check_body_shape(BODYSHAPE_SNOUTED, ITEM_SLOT_MASK)
/mob/living/carbon/human/update_worn_back()
remove_overlay(BACK_LAYER)
-
- if(client && hud_used && hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_BACK) + 1])
- var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_BACK) + 1]
- inv.update_icon()
+ hud_used?.update_inventory_slot(ITEM_SLOT_BACK)
if(back)
var/obj/item/worn_item = back
var/mutable_appearance/back_overlay
- update_hud_back(worn_item)
if(HAS_TRAIT(worn_item, TRAIT_NO_WORN_ICON))
return
var/icon_file = 'icons/mob/clothing/back.dmi'
- back_overlay = back.build_worn_icon(default_layer = BACK_LAYER, default_icon_file = icon_file)
+ back_overlay = back.build_worn_icon(default_layer = BACK_LAYER, default_icon_file = icon_file, bodyshape = bodyshape)
if(!back_overlay)
return
@@ -497,30 +418,17 @@ There are several things that need to be remembered:
apply_overlay(BACK_LAYER)
/mob/living/carbon/human/get_held_overlays()
+ hud_used?.update_inventory_slot(ITEM_SLOT_HANDS)
var/list/hands = list()
for(var/obj/item/worn_item in held_items)
var/held_index = get_held_index_of_item(worn_item)
- if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
- worn_item.screen_loc = ui_hand_position(held_index)
- client.screen += worn_item
- if(observers?.len)
- for(var/M in observers)
- var/mob/dead/observe = M
- if(observe.client && observe.client.eye == src)
- observe.client.screen += worn_item
- else
- observers -= observe
- if(!observers.len)
- observers = null
- break
-
var/t_state = worn_item.inhand_icon_state
if(!t_state)
t_state = worn_item.icon_state
var/mutable_appearance/hand_overlay
var/icon_file = IS_RIGHT_INDEX(held_index) ? worn_item.righthand_file : worn_item.lefthand_file
- hand_overlay = worn_item.build_worn_icon(default_layer = HANDS_LAYER, default_icon_file = icon_file, isinhands = TRUE)
+ hand_overlay = worn_item.build_worn_icon(default_layer = HANDS_LAYER, default_icon_file = icon_file, isinhands = TRUE, bodyshape = bodyshape)
var/obj/item/bodypart/arm/held_in_hand = hand_bodyparts[held_index]
held_in_hand?.held_hand_offset?.apply_offset(hand_overlay)
@@ -642,90 +550,6 @@ There are several things that need to be remembered:
out += overlays_standing[i]
return out
-
-//human HUD updates for items in our inventory
-
-/mob/living/carbon/human/proc/update_hud_uniform(obj/item/worn_item)
- worn_item.screen_loc = ui_iclothing
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-/mob/living/carbon/human/proc/update_hud_id(obj/item/worn_item)
- worn_item.screen_loc = ui_id
- if((client && hud_used?.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item)
-
-/mob/living/carbon/human/proc/update_hud_gloves(obj/item/worn_item)
- worn_item.screen_loc = ui_gloves
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-/mob/living/carbon/human/proc/update_hud_glasses(obj/item/worn_item)
- worn_item.screen_loc = ui_glasses
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-/mob/living/carbon/human/proc/update_hud_ears(obj/item/worn_item)
- worn_item.screen_loc = ui_ears
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-/mob/living/carbon/human/proc/update_hud_shoes(obj/item/worn_item)
- worn_item.screen_loc = ui_shoes
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-/mob/living/carbon/human/proc/update_hud_s_store(obj/item/worn_item)
- worn_item.screen_loc = ui_sstore1
- if(client && hud_used?.hud_shown)
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-/mob/living/carbon/human/proc/update_hud_wear_suit(obj/item/worn_item)
- worn_item.screen_loc = ui_oclothing
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-/mob/living/carbon/human/proc/update_hud_belt(obj/item/worn_item)
- belt.screen_loc = ui_belt
- if(client && hud_used?.hud_shown)
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-/mob/living/carbon/human/update_hud_head(obj/item/worn_item)
- worn_item.screen_loc = ui_head
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-//update whether our mask item appears on our hud.
-/mob/living/carbon/human/update_hud_wear_mask(obj/item/worn_item)
- worn_item.screen_loc = ui_mask
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-//update whether our neck item appears on our hud.
-/mob/living/carbon/human/update_hud_neck(obj/item/worn_item)
- worn_item.screen_loc = ui_neck
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
- client.screen += worn_item
- update_observer_view(worn_item,TRUE)
-
-//update whether our back item appears on our hud.
-/mob/living/carbon/human/update_hud_back(obj/item/worn_item)
- worn_item.screen_loc = ui_back
- if(client && hud_used?.hud_shown)
- client.screen += worn_item
- update_observer_view(worn_item, inventory = TRUE)
-
/*
Does everything in relation to building the /mutable_appearance used in the mob's overlays list
covers:
@@ -758,6 +582,7 @@ generate/load female uniform sprites matching all previously decided variables
female_uniform = NO_FEMALE_UNIFORM,
override_state = null,
override_file = null,
+ bodyshape = NONE,
)
//Find a valid icon_state from variables+arguments
@@ -767,9 +592,6 @@ generate/load female uniform sprites matching all previously decided variables
//Find a valid layer from variables+arguments
var/layer2use = alternate_worn_layer || default_layer
- var/mob/living/carbon/wearer = loc
- var/is_digi = istype(wearer) && (wearer.bodyshape & BODYSHAPE_DIGITIGRADE) && !wearer.is_digitigrade_squished()
-
var/mutable_appearance/draw_target // MA of the item itself, not the final result
var/icon/building_icon // used to construct an icon across multiple procs before converting it to MA
if(female_uniform)
@@ -779,7 +601,7 @@ generate/load female uniform sprites matching all previously decided variables
type = female_uniform,
greyscale_colors = greyscale_colors,
)
- if(!isinhands && is_digi && (supports_variations_flags & CLOTHING_DIGITIGRADE_MASK))
+ if(!isinhands && (bodyshape & BODYSHAPE_DIGITIGRADE) && (supports_variations_flags & CLOTHING_DIGITIGRADE_MASK))
building_icon = wear_digi_version(
base_icon = building_icon || icon(file2use, t_state),
item = src,
@@ -844,21 +666,6 @@ generate/load female uniform sprites matching all previously decided variables
else //No offsets or Unwritten number of hands
return list("x" = 0, "y" = 0)//Handle held offsets
-/mob/living/carbon/human/proc/update_observer_view(obj/item/worn_item, inventory)
- if(observers?.len)
- for(var/M in observers)
- var/mob/dead/observe = M
- if(observe.client && observe.client.eye == src)
- if(observe.hud_used)
- if(inventory && !observe.hud_used.inventory_shown)
- continue
- observe.client.screen += worn_item
- else
- observers -= observe
- if(!observers.len)
- observers = null
- break
-
/mob/living/carbon/human/update_body(is_creating = FALSE)
remove_overlay(BODY_LAYER)
@@ -879,22 +686,21 @@ generate/load female uniform sprites matching all previously decided variables
return .
// Underwear, Undershirts & Socks
- var/active_bodyshapes = get_active_bodyshapes()
if(underwear)
var/datum/sprite_accessory/clothing/underwear/undie_accessory = SSaccessories.underwear_list[underwear]
- var/mutable_appearance/underwear_overlay = undie_accessory?.make_appearance(underwear_color, physique, active_bodyshapes)
+ var/mutable_appearance/underwear_overlay = undie_accessory?.make_appearance(underwear_color, physique, bodyshape)
if(underwear_overlay)
. += underwear_overlay
if(undershirt)
var/datum/sprite_accessory/clothing/undershirt/shirt_accessory = SSaccessories.undershirt_list[undershirt]
- var/mutable_appearance/shirt_overlay = shirt_accessory?.make_appearance(null, physique, active_bodyshapes)
+ var/mutable_appearance/shirt_overlay = shirt_accessory?.make_appearance(null, physique, bodyshape)
if(shirt_overlay)
. += shirt_overlay
if(socks && num_legs >= 2 && !(bodyshape & BODYSHAPE_DIGITIGRADE))
var/datum/sprite_accessory/clothing/socks/sock_accessory = SSaccessories.socks_list[socks]
- var/mutable_appearance/socks_overlay = sock_accessory?.make_appearance(null, physique, active_bodyshapes)
+ var/mutable_appearance/socks_overlay = sock_accessory?.make_appearance(null, physique, bodyshape)
if(socks_overlay)
. += socks_overlay
@@ -970,44 +776,6 @@ generate/load female uniform sprites matching all previously decided variables
update_eyes()
update_hair()
-/**
- * Used to perform regular updates to the limbs of humans with special bodyshapes
- *
- * * check_shapes: The bodyshapes to check for.
- * Any limbs or organs which share this shape, will be updated.
- * * ignore_slots: The slots to ignore when updating the limbs.
- * This is useful for things like digitigrade legs, where we can skip some slots that we're already updating.
- *
- * return an integer, the number of limbs updated
- */
-/mob/living/carbon/human/proc/check_body_shape(check_shapes = BODYSHAPE_DIGITIGRADE|BODYSHAPE_SNOUTED, ignore_slots = NONE)
- . = 0
- if(!(bodyshape & check_shapes))
- // optimization - none of our limbs or organs have the desired shape
- return .
-
- for(var/obj/item/bodypart/limb as anything in get_bodyparts())
- var/checked_bodyshape = limb.bodyshape
- // accounts for stuff like snouts
- for(var/obj/item/organ/organ in limb)
- checked_bodyshape |= organ.external_bodyshapes
-
- // any limb needs to be updated, so stop here and do it
- if(checked_bodyshape & check_shapes)
- . = update_body_parts()
- break
-
- if(!.)
- return
- // hardcoding this here until bodypart updating is more sane
- // we need to update clothing items that may have been affected by bodyshape updates
- if(check_shapes & BODYSHAPE_DIGITIGRADE)
- for(var/obj/item/thing as anything in get_equipped_items(INCLUDE_PROSTHETICS|INCLUDE_ABSTRACT))
- if(thing.slot_flags & ignore_slots)
- continue
- if(thing.supports_variations_flags & DIGITIGRADE_VARIATIONS)
- thing.update_slot_icon()
-
// Hooks into human apply overlay so that we can modify all overlays applied through standing overlays to our height system.
// Some of our overlays will be passed through a displacement filter to make our mob look taller or shorter.
// Some overlays can't be displaced as they're too close to the edge of the sprite or cross the middle point in a weird way.
@@ -1062,12 +830,14 @@ generate/load female uniform sprites matching all previously decided variables
var/static/icon/cut_legs_mask = icon('icons/effects/cut.dmi', "Cut2")
var/static/icon/lenghten_torso_mask = icon('icons/effects/cut.dmi', "Cut3")
var/static/icon/lenghten_legs_mask = icon('icons/effects/cut.dmi', "Cut4")
+ var/static/icon/lenghten_arms_mask = icon('icons/effects/cut.dmi', "Cut5")
appearance.remove_filter(list(
"Cut_Torso",
"Cut_Legs",
"Lenghten_Legs",
"Lenghten_Torso",
+ "Lenghten_Arms",
"Gnome_Cut_Torso",
"Gnome_Cut_Legs",
"Monkey_Torso",
@@ -1169,10 +939,15 @@ generate/load female uniform sprites matching all previously decided variables
"priority" = 1,
"params" = displacement_map_filter(lenghten_torso_mask, x = 0, y = 0, size = 1),
),
+ list(
+ "name" = "Lenghten_Arms",
+ "priority" = 1,
+ "params" = displacement_map_filter(lenghten_arms_mask, x = 0, y = 0, size = 1),
+ ),
list(
"name" = "Lenghten_Legs",
"priority" = 1,
- "params" = displacement_map_filter(lenghten_legs_mask, x = 0, y = 0, size = 2),
+ "params" = displacement_map_filter(lenghten_legs_mask, x = 0, y = 0, size = 1),
),
))
diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm
index e32b3b53619e..3c21ea44ed54 100644
--- a/code/modules/mob/living/carbon/human/life.dm
+++ b/code/modules/mob/living/carbon/human/life.dm
@@ -23,7 +23,6 @@
return
. = ..()
-
if(QDELETED(src))
return FALSE
@@ -45,12 +44,9 @@
handle_heart(seconds_per_tick)
// Handles liver failure effects, if we lack a liver
handle_liver(seconds_per_tick)
- // For special species interactions
- dna.species.spec_life(src, seconds_per_tick)
- // DARKPACK EDIT ADD START - SPLATS
- for(var/datum/splat/splat in splats)
- splat.splat_life(seconds_per_tick)
- // DARKPACK EDIT ADD END
+ // Crit damage but specifically for people who don't get suffocate while in crit so they can actually die eventually
+ if(HAS_TRAIT(src, TRAIT_NOBREATH) && (health < crit_threshold) && !HAS_TRAIT(src, TRAIT_NOCRITDAMAGE))
+ adjust_brute_loss(0.5 * seconds_per_tick)
return stat != DEAD
/mob/living/carbon/human/calculate_affecting_pressure(pressure)
diff --git a/code/modules/mob/living/carbon/human/species_types/flypeople.dm b/code/modules/mob/living/carbon/human/species_types/flypeople.dm
index eca9383698f9..93d054d5344a 100644
--- a/code/modules/mob/living/carbon/human/species_types/flypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/flypeople.dm
@@ -26,20 +26,6 @@
BODY_ZONE_CHEST = /obj/item/bodypart/chest/fly,
)
-/datum/species/fly/on_species_gain(mob/living/carbon/human/human_who_gained_species, datum/species/old_species, pref_load, regenerate_icons)
- . = ..()
- RegisterSignal(human_who_gained_species, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby))
-
-/datum/species/fly/on_species_loss(mob/living/carbon/human/C, datum/species/new_species, pref_load)
- . = ..()
- UnregisterSignal(C, COMSIG_ATOM_ATTACKBY)
-
-/datum/species/fly/proc/on_attackby(mob/living/source, obj/item/attacking_item, mob/living/attacker, list/modifiers, list/attack_modifiers)
- SIGNAL_HANDLER
-
- if(istype(attacking_item, /obj/item/melee/flyswatter))
- MODIFY_ATTACK_FORCE_MULTIPLIER(attack_modifiers, 30) // Yes, a 30x damage modifier
-
/datum/species/fly/get_physical_attributes()
return "These hideous creatures suffer from pesticide immensely, eat waste, and are incredibly vulnerable to bright lights. They do have wings though."
diff --git a/code/modules/mob/living/carbon/human/species_types/golems.dm b/code/modules/mob/living/carbon/human/species_types/golems.dm
index 493540162b51..23e3554d170f 100644
--- a/code/modules/mob/living/carbon/human/species_types/golems.dm
+++ b/code/modules/mob/living/carbon/human/species_types/golems.dm
@@ -66,6 +66,7 @@
RegisterSignal(human_who_gained_species, COMSIG_CARBON_DEFIB_HEART_CHECK, PROC_REF(defib_check))
RegisterSignal(human_who_gained_species, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(rebuild_check))
RegisterSignal(human_who_gained_species, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(human_who_gained_species, COMSIG_LIVING_LIFE, PROC_REF(on_life))
// nutrition = health, so give people a head start
human_who_gained_species.set_nutrition(NUTRITION_LEVEL_WELL_FED)
@@ -81,14 +82,16 @@
COMSIG_CARBON_DEFIB_HEART_CHECK,
COMSIG_ATOM_ITEM_INTERACTION,
COMSIG_ATOM_EXAMINE,
+ COMSIG_LIVING_LIFE,
))
human_who_lost_species.physiology.stamina_mod /= 0.6
human_who_lost_species.physiology.stun_mod /= 0.6
human_who_lost_species.physiology.knockdown_mod /= 1.2
-/datum/species/golem/spec_life(mob/living/carbon/human/source, seconds_per_tick)
- . = ..()
+/datum/species/golem/proc/on_life(mob/living/carbon/human/source, seconds_per_tick)
+ SIGNAL_HANDLER
+
if(source.nutrition <= 20)
// this is "hard crit" for golems
source.Unconscious(1.5 SECONDS * seconds_per_tick)
diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
index 67b88d64dbcb..fa2d34012ccf 100644
--- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
@@ -215,7 +215,10 @@
// so if someone mindswapped into them, they'd still be shared.
bodies = null
C.set_blood_volume(C.get_blood_volume(), maximum = BLOOD_VOLUME_NORMAL)
- UnregisterSignal(C, COMSIG_LIVING_DEATH)
+ UnregisterSignal(C, list(
+ COMSIG_LIVING_DEATH,
+ COMSIG_LIVING_LIFE,
+ ))
..()
/datum/species/jelly/slime/on_species_gain(mob/living/carbon/C, datum/species/old_species, pref_load, regenerate_icons)
@@ -232,6 +235,7 @@
bodies |= C
RegisterSignal(C, COMSIG_LIVING_DEATH, PROC_REF(on_death_move_body))
+ RegisterSignal(C, COMSIG_LIVING_LIFE, PROC_REF(on_life))
/datum/species/jelly/slime/proc/on_death_move_body(mob/living/carbon/human/source, gibbed)
SIGNAL_HANDLER
@@ -255,16 +259,16 @@
/datum/species/jelly/slime/copy_properties_from(datum/species/jelly/slime/old_species)
bodies = old_species.bodies
-/datum/species/jelly/slime/spec_life(mob/living/carbon/human/H, seconds_per_tick)
- . = ..()
- if(H.get_blood_volume() >= BLOOD_VOLUME_SLIME_SPLIT)
+/datum/species/jelly/slime/proc/on_life(mob/living/carbon/human/source, seconds_per_tick)
+ SIGNAL_HANDLER
+ if(source.get_blood_volume() >= BLOOD_VOLUME_SLIME_SPLIT)
if(SPT_PROB(2.5, seconds_per_tick))
- to_chat(H, span_notice("You feel very bloated!"))
+ to_chat(source, span_notice("You feel very bloated!"))
- else if(H.nutrition >= NUTRITION_LEVEL_WELL_FED)
- H.adjust_blood_volume(1.5 * seconds_per_tick)
- if(H.get_blood_volume() <= BLOOD_VOLUME_LOSE_NUTRITION)
- H.adjust_nutrition(-1.25 * seconds_per_tick)
+ else if(source.nutrition >= NUTRITION_LEVEL_WELL_FED)
+ source.adjust_blood_volume(1.5 * seconds_per_tick)
+ if(source.get_blood_volume() <= BLOOD_VOLUME_LOSE_NUTRITION)
+ source.adjust_nutrition(-1.25 * seconds_per_tick)
/datum/action/innate/split_body
name = "Split Body"
diff --git a/code/modules/mob/living/carbon/human/species_types/mothmen.dm b/code/modules/mob/living/carbon/human/species_types/mothmen.dm
index 4b410f52ee71..9660b2d2e4c2 100644
--- a/code/modules/mob/living/carbon/human/species_types/mothmen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/mothmen.dm
@@ -29,20 +29,6 @@
BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/moth,
)
-/datum/species/moth/on_species_gain(mob/living/carbon/human/human_who_gained_species, datum/species/old_species, pref_load, regenerate_icons)
- . = ..()
- RegisterSignal(human_who_gained_species, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby))
-
-/datum/species/moth/on_species_loss(mob/living/carbon/human/C, datum/species/new_species, pref_load)
- . = ..()
- UnregisterSignal(C, COMSIG_ATOM_ATTACKBY)
-
-/datum/species/moth/proc/on_attackby(mob/living/source, obj/item/attacking_item, mob/living/attacker, list/modifiers, list/attack_modifiers)
- SIGNAL_HANDLER
-
- if(istype(attacking_item, /obj/item/melee/flyswatter))
- MODIFY_ATTACK_FORCE_MULTIPLIER(attack_modifiers, 10) // Yes, a 10x damage modifier
-
/datum/species/moth/randomize_features()
var/list/features = ..()
features[FEATURE_MOTH_MARKINGS] = pick(SSaccessories.feature_list[FEATURE_MOTH_MARKINGS])
diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm
index fb891df0f26f..fa31d7a89cf1 100644
--- a/code/modules/mob/living/carbon/human/species_types/vampire.dm
+++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm
@@ -33,16 +33,21 @@
new_vampire.skin_tone = "albino"
RegisterSignal(new_vampire, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby))
RegisterSignal(new_vampire, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created))
+ RegisterSignal(new_vampire, COMSIG_LIVING_LIFE, PROC_REF(on_life))
if(new_vampire.hud_used)
on_hud_created(new_vampire)
/datum/species/human/vampire/on_species_loss(mob/living/carbon/human/old_vampire, datum/species/new_species, pref_load)
. = ..()
- UnregisterSignal(old_vampire, COMSIG_ATOM_ATTACKBY)
+ UnregisterSignal(old_vampire, list(
+ COMSIG_ATOM_ATTACKBY,
+ COMSIG_MOB_HUD_CREATED,
+ COMSIG_LIVING_LIFE,
+ ))
old_vampire.hud_used?.remove_screen_object(HUD_MOB_BLOOD_LEVEL)
-/datum/species/human/vampire/spec_life(mob/living/carbon/human/vampire, seconds_per_tick)
- . = ..()
+/datum/species/human/vampire/proc/on_life(mob/living/carbon/human/vampire, seconds_per_tick)
+ SIGNAL_HANDLER
if(istype(vampire.loc, /obj/structure/closet/crate/coffin))
var/need_mob_update = FALSE
need_mob_update += vampire.heal_overall_damage(brute = 2 * seconds_per_tick, burn = 2 * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
@@ -155,7 +160,6 @@
icon_state = "tongue_vampire"
actions_types = list(/datum/action/item_action/organ_action/vampire)
organ_traits = list(
- TRAIT_SPEAKS_CLEARLY,
TRAIT_DRINKS_BLOOD,
// future todo : tie nobreath and nohunger to a vampire organ set bonus
TRAIT_NOBREATH,
diff --git a/code/modules/mob/living/carbon/human/species_types/zombies.dm b/code/modules/mob/living/carbon/human/species_types/zombies.dm
index 3053374e7d70..3307bc4215ae 100644
--- a/code/modules/mob/living/carbon/human/species_types/zombies.dm
+++ b/code/modules/mob/living/carbon/human/species_types/zombies.dm
@@ -47,15 +47,6 @@
BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/zombie
)
- /// Spooky growls we sometimes play while alive
- var/static/list/spooks = list(
- 'sound/effects/hallucinations/growl1.ogg',
- 'sound/effects/hallucinations/growl2.ogg',
- 'sound/effects/hallucinations/growl3.ogg',
- 'sound/effects/hallucinations/veryfar_noise.ogg',
- 'sound/effects/hallucinations/wail.ogg',
- )
-
/// Zombies do not stabilize body temperature they are the walking dead and are cold blooded
/datum/species/zombie/body_temperature_core(mob/living/carbon/human/humi, seconds_per_tick)
return
@@ -178,11 +169,6 @@
/datum/species/zombie/infectious/spec_stun(mob/living/carbon/human/H,amount)
return min(2 SECONDS, amount)
-/datum/species/zombie/infectious/spec_life(mob/living/carbon/carbon_mob, seconds_per_tick)
- . = ..()
- if(!HAS_TRAIT(carbon_mob, TRAIT_CRITICAL_CONDITION) && SPT_PROB(2, seconds_per_tick))
- playsound(carbon_mob, pick(spooks), 50, TRUE, 10)
-
// Weaker subtype - less healing, weaker attacks, etc
/datum/species/zombie/infectious/mindless
name = "Mindless Infectious Zombie"
diff --git a/code/modules/mob/living/carbon/inventory.dm b/code/modules/mob/living/carbon/inventory.dm
index b8dfca193ba6..fcf292496f27 100644
--- a/code/modules/mob/living/carbon/inventory.dm
+++ b/code/modules/mob/living/carbon/inventory.dm
@@ -112,8 +112,7 @@
if(equipping.pulledby)
equipping.pulledby.stop_pulling()
- equipping.screen_loc = null
- client?.screen -= equipping
+ hud_used?.update_inventory_slot(slot)
for(var/mob/dead/observe as anything in observers)
observe.client?.screen -= equipping
@@ -174,6 +173,7 @@
if(item.hair_mask)
LAZYADD(hair_masks, item.hair_mask)
update_hair()
+ update_body() // this is solely for lizard frills
add_item_coverage(item)
/mob/living/carbon/has_unequipped(obj/item/item)
@@ -185,6 +185,7 @@
if(item.hair_mask)
LAZYREMOVE(hair_masks, item.hair_mask)
update_hair()
+ update_body() // this is solely for lizard frills
remove_item_coverage(item)
/mob/living/carbon/doUnEquip(obj/item/item_dropping, force, newloc, no_move, invdrop = TRUE, silent = FALSE)
@@ -258,8 +259,17 @@
* * removed_slots - slots that were removed from obscured_slots
*/
/mob/living/carbon/proc/item_coverage_changed(added_slots, removed_slots)
+ SEND_SIGNAL(src, COMSIG_CARBON_ITEM_COVERAGE_CHANGED, added_slots, removed_slots)
update_clothing(hidden_slots_to_inventory_slots(added_slots|removed_slots))
- if((added_slots|removed_slots) & (HIDEJUMPSUIT|HIDEEARS|HIDEEYES|HIDEHAIR|HIDEFACIALHAIR|HIDESNOUT|HIDEMUTWINGS|HIDEANTENNAE))
+ if((added_slots|removed_slots) & HIDESNOUT)
+ synchronize_bodyshapes()
+ if((added_slots|removed_slots) & (HIDEHAIR|HIDEFACIALHAIR))
+ update_hair()
+ if((added_slots|removed_slots) & HIDEEYES)
+ update_eyes()
+ // HIDEJUMPSUIT is for digitigrade legs, HIDEEARS is for lizard frills, HIDEHAIR is for felinid ears and lizard horns, the others should be obvious
+ // future todo; we should collect a list of all bodypart overlays and what conceals/reveals them dynamically, rather than hardcoding this
+ if((added_slots|removed_slots) & (HIDEJUMPSUIT|HIDEEARS|HIDEHAIR|HIDESNOUT|HIDEMUTWINGS|HIDEANTENNAE))
update_body()
/// Returns the helmet if an air tank compatible helmet is equipped.
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index 879544411f0e..c7d0a88c038d 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -13,6 +13,9 @@
if(HAS_TRAIT(src, TRAIT_STASIS))
. = ..()
+ if(QDELETED(src))
+ return
+
reagents?.handle_stasis_chems(src, seconds_per_tick)
else
//Reagent processing needs to come before breathing, to prevent edge cases.
@@ -31,9 +34,6 @@
GLOB.addictions[key].process_addiction(src, seconds_per_tick)
handle_brain_damage(seconds_per_tick)
- if(stat != DEAD)
- handle_bodyparts(seconds_per_tick)
-
if(stat != DEAD)
return TRUE
@@ -501,10 +501,6 @@
return COMPONENT_NO_EXPOSE_REAGENTS
-/mob/living/carbon/proc/handle_bodyparts(seconds_per_tick)
- for(var/obj/item/bodypart/limb as anything in get_bodyparts(include_stumps = TRUE))
- . |= limb.on_life(seconds_per_tick)
-
/mob/living/carbon/proc/handle_organs(seconds_per_tick)
if(stat == DEAD)
if(reagents && (reagents.has_reagent(/datum/reagent/toxin/formaldehyde, 1) || reagents.has_reagent(/datum/reagent/cryostylane))) // No organ decay if the body contains formaldehyde.
@@ -526,48 +522,20 @@
if(organ?.owner) // This exist mostly because reagent metabolization can cause organ reshuffling
organ.on_life(seconds_per_tick)
-/mob/living/carbon/handle_diseases(seconds_per_tick)
- for(var/datum/disease/disease as anything in diseases)
- if(QDELETED(disease)) //Got cured/deleted while the loop was still going.
- continue
- if(stat != DEAD || disease.process_dead)
- disease.stage_act(seconds_per_tick)
-
-/mob/living/carbon/handle_mutations(time_since_irradiated, seconds_per_tick)
- if(!LAZYLEN(dna?.temporary_mutations))
- return
+/**
+ * Returns a multiplier representing how effectively this mob can regenerate blood
+ *
+ * A return value of 0 means the mob cannot regenerate blood at all. (missing heart or the heart has stopped or is failing)
+ * Mobs that do not require a heart always return 1, as their blood regeneration is unaffected by heart status.
+ */
+/mob/living/carbon/proc/get_heart_blood_regeneration_multiplier()
+ if(!needs_heart())
+ return 1
+ var/obj/item/organ/heart/heart = get_organ_slot(ORGAN_SLOT_HEART)
+ if(isnull(heart))
+ return 0
- for(var/mut, mut_data in dna.temporary_mutations)
- if(mut_data < world.time)
- if(!LAZYLEN(dna.previous))
- continue
- if(mut == UI_CHANGED)
- if(dna.previous["UI"])
- dna.unique_identity = merge_text(dna.unique_identity,dna.previous["UI"])
- updateappearance(mutations_overlay_update=1)
- dna.previous.Remove("UI")
- LAZYREMOVE(dna.temporary_mutations, mut)
- continue
- if(mut == UF_CHANGED)
- if(dna.previous["UF"])
- dna.unique_features = merge_text(dna.unique_features,dna.previous["UF"])
- updateappearance(mutcolor_update=1, mutations_overlay_update=1)
- dna.previous.Remove("UF")
- LAZYREMOVE(dna.temporary_mutations, mut)
- continue
- if(mut == UE_CHANGED)
- if(dna.previous["name"])
- real_name = dna.previous["name"]
- name = real_name
- dna.previous.Remove("name")
- if(dna.previous["UE"])
- dna.unique_enzymes = dna.previous["UE"]
- dna.previous.Remove("UE")
- if(dna.previous["blood_type"])
- set_blood_type(dna.previous["blood_type"])
- dna.previous.Remove("blood_type")
- LAZYREMOVE(dna.temporary_mutations, mut)
- continue
+ return heart.get_blood_regeneration_multiplier()
/**
* Handles calling metabolization for dead people.
diff --git a/code/modules/mob/living/init_signals.dm b/code/modules/mob/living/init_signals.dm
index 36db0094f5de..3c9c0d7dd4f1 100644
--- a/code/modules/mob/living/init_signals.dm
+++ b/code/modules/mob/living/init_signals.dm
@@ -191,11 +191,13 @@
mobility_flags &= ~(MOBILITY_PULL)
if(pulling)
stop_pulling()
+ pull_force_change()
/// Called when [TRAIT_PULL_BLOCKED] is removed from the mob.
/mob/living/proc/on_pull_blocked_trait_loss(datum/source)
SIGNAL_HANDLER
mobility_flags |= MOBILITY_PULL
+ pull_force_change()
/// Called when [TRAIT_INCAPACITATED] is added to the mob.
diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm
index dc50f5ec56cd..178679bfc912 100644
--- a/code/modules/mob/living/life.dm
+++ b/code/modules/mob/living/life.dm
@@ -13,7 +13,7 @@
set waitfor = FALSE
SHOULD_NOT_SLEEP(TRUE)
- var/signal_result = SEND_SIGNAL(src, COMSIG_LIVING_LIFE, seconds_per_tick)
+ var/signal_result = SEND_SIGNAL(src, COMSIG_LIVING_PRE_LIFE, seconds_per_tick)
if(signal_result & COMPONENT_LIVING_CANCEL_LIFE_PROCESSING) // mmm less work
return
@@ -41,18 +41,15 @@
if(isnull(loc) || HAS_TRAIT(src, TRAIT_NO_TRANSFORM))
return
+ SEND_SIGNAL(src, COMSIG_LIVING_LIFE, seconds_per_tick)
+ if(QDELETED(src)) // signal handlers such as diseases could delete the mob
+ return
+
if(!HAS_TRAIT(src, TRAIT_STASIS))
if(stat != DEAD)
- //Mutations and radiation
- handle_mutations(seconds_per_tick)
//Breathing, if applicable
handle_breathing(seconds_per_tick)
- handle_diseases(seconds_per_tick) // DEAD check is in the proc itself; we want it to spread even if the mob is dead, but to handle its disease-y properties only if you're not.
-
- if (QDELETED(src)) // Diseases can qdel the mob via transformations
- return
-
// Handle temperature/pressure differences between body and environment
var/datum/gas_mixture/environment = loc.return_air()
if(environment)
@@ -64,21 +61,15 @@
update_nutrition()
living_flags &= ~QUEUE_NUTRITION_UPDATE
- if (living_flags & BLOOD_UPDATE_QUEUED)
+ if(living_flags & BLOOD_UPDATE_QUEUED)
update_blood_effects()
+ living_flags &= ~BLOOD_UPDATE_QUEUED
if(stat != DEAD)
return TRUE
/mob/living/proc/handle_breathing(seconds_per_tick)
SEND_SIGNAL(src, COMSIG_LIVING_HANDLE_BREATHING, seconds_per_tick)
- return
-
-/mob/living/proc/handle_mutations(seconds_per_tick)
- return
-
-/mob/living/proc/handle_diseases(seconds_per_tick)
- return
// Base mob environment handler for body temperature
/mob/living/proc/handle_environment(datum/gas_mixture/environment, seconds_per_tick)
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index c7b6ab02cbbd..94753937735f 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -13,6 +13,8 @@
var/datum/atom_hud/data/diagnostic/diag_hud = GLOB.huds[DATA_HUD_DIAGNOSTIC]
diag_hud.add_atom_to_hud(src)
add_ally(src)
+ if(!pull_force)
+ remove_verb(src, /mob/living/verb/pulled)
GLOB.mob_living_list += src
SSpoints_of_interest.make_point_of_interest(src)
update_fov()
@@ -512,14 +514,11 @@
//mob verbs are a lot faster than object verbs
//for more info on why this is not atom/pull, see examinate() in mob.dm
-/mob/living/verb/pulled(atom/movable/AM as mob|obj in oview(1))
+/mob/living/verb/pulled(atom/movable/thing_pulled as mob|obj in oview(1))
set name = "Pull"
- set category = "IC"
- if(istype(AM) && Adjacent(AM))
- start_pulling(AM)
- else if(!combat_mode) //Don;'t cancel pulls if misclicking in combat mode.
- stop_pulling()
+ if(istype(thing_pulled) && Adjacent(thing_pulled))
+ start_pulling(thing_pulled)
/mob/living/stop_pulling()
if(ismob(pulling))
@@ -528,11 +527,6 @@
update_pull_movespeed()
update_pull_hud_icon()
-/mob/living/verb/stop_pulling1()
- set name = "Stop Pulling"
- set category = "IC"
- stop_pulling()
-
//same as above
/mob/living/pointed(atom/A)
if(INCAPACITATED_IGNORING(src, INCAPABLE_RESTRAINTS))
@@ -618,7 +612,7 @@
/mob/living/proc/mob_sleep()
set name = "Sleep"
- set category = "IC"
+ set hidden = TRUE
if(IsSleeping())
to_chat(src, span_warning("You are already sleeping!"))
@@ -673,9 +667,6 @@
return account
/mob/living/proc/toggle_resting()
- set name = "Rest"
- set category = "IC"
-
set_resting(!resting, FALSE)
@@ -1157,10 +1148,7 @@
return FALSE
return TRUE
-/mob/living/verb/resist()
- set name = "Resist"
- set category = "IC"
-
+/mob/living/proc/resist()
DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(execute_resist)))
///proc extender of [/mob/living/verb/resist] meant to make the process queable if the server is overloaded when the verb is called
@@ -1169,7 +1157,9 @@
return
changeNext_move(CLICK_CD_RESIST)
- SEND_SIGNAL(src, COMSIG_LIVING_RESIST, src)
+ if(SEND_SIGNAL(src, COMSIG_LIVING_RESIST) & COMPONENT_BLOCK_RESIST)
+ return
+
//resisting grabs (as if it helps anyone...)
if(!HAS_TRAIT(src, TRAIT_RESTRAINED) && pulledby)
log_combat(src, pulledby, "resisted grab")
@@ -1622,6 +1612,7 @@
/mob/living/basic/bear/russian,
/mob/living/basic/blob_minion/blobbernaut,
/mob/living/basic/blob_minion/spore,
+ /mob/living/basic/blood_worm/hatchling/polymorph,
/mob/living/basic/butterfly,
/mob/living/basic/carp,
/mob/living/basic/carp/mega,
@@ -2136,6 +2127,10 @@ GLOBAL_LIST_EMPTY(fire_appearances)
return FALSE
update_transform(var_value/current_size)
. = TRUE
+ if(NAMEOF(src, pull_force))
+ set_pull_force(var_value)
+ . = TRUE
+
if(!isnull(.))
datum_flags |= DF_VAR_EDITED
@@ -2974,10 +2969,7 @@ GLOBAL_LIST_EMPTY(fire_appearances)
log_admin("[key_name(admin)] gave a guardian spirit controlled by [guardian_client] to [src].")
BLACKBOX_LOG_ADMIN_VERB("Give Guardian Spirit")
-/mob/living/verb/lookup()
- set name = "Look Up"
- set category = "IC"
-
+/mob/living/proc/lookup()
if(looking_vertically)
to_chat(src, "You set your head straight again.")
end_look()
@@ -2994,10 +2986,7 @@ GLOBAL_LIST_EMPTY(fire_appearances)
to_chat(src, "You tilt your head upwards.")
look_up()
-/mob/living/verb/lookdown()
- set name = "Look Down"
- set category = "IC"
-
+/mob/living/proc/lookdown()
if(looking_vertically)
to_chat(src, "You set your head straight again.")
end_look()
@@ -3085,3 +3074,15 @@ GLOBAL_LIST_EMPTY(fire_appearances)
if(HAS_TRAIT(src, TRAIT_ANALGESIA) && !force)
return
INVOKE_ASYNC(src, PROC_REF(emote), "scream")
+
+/mob/living/proc/set_pull_force(new_pull_force)
+ if(pull_force == new_pull_force)
+ return
+ pull_force = new_pull_force
+ pull_force_change()
+
+/mob/living/proc/pull_force_change()
+ if(!pull_force || HAS_TRAIT(src, TRAIT_PULL_BLOCKED))
+ remove_verb(src, /mob/living/verb/pulled)
+ else
+ add_verb(src, /mob/living/verb/pulled)
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 002a446039f6..80857085bef0 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -592,7 +592,7 @@
shock_damage *= siemens_coeff
if((flags & SHOCK_TESLA) && HAS_TRAIT(src, TRAIT_TESLA_SHOCKIMMUNE))
return FALSE
- if(HAS_TRAIT(src, TRAIT_SHOCKIMMUNE))
+ if(!(flags & SHOCK_IGNORE_IMMUNITY) && HAS_TRAIT(src, TRAIT_SHOCKIMMUNE))
return FALSE
if(shock_damage < 1)
return FALSE
diff --git a/code/modules/mob/living/navigation.dm b/code/modules/mob/living/navigation.dm
index dcaee5b09457..c63b4594b65f 100644
--- a/code/modules/mob/living/navigation.dm
+++ b/code/modules/mob/living/navigation.dm
@@ -10,7 +10,7 @@
/mob/living/verb/navigate()
set name = "Navigate"
- set category = "IC"
+ set hidden = TRUE
if(incapacitated)
return
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 86315686aac5..aa960b3444a4 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -97,6 +97,7 @@
RegisterSignal(ai_tracking_tool, COMSIG_TRACKABLE_GLIDE_CHANGED, PROC_REF(tracked_glidesize_changed))
add_traits(list(TRAIT_PULL_BLOCKED, TRAIT_AI_ACCESS, TRAIT_HANDS_BLOCKED, TRAIT_CAN_GET_AI_TRACKING_MESSAGE, TRAIT_LOUD_BINARY), INNATE_TRAIT)
+ AddElement(/datum/element/block_area_power_fail)
//Heads up to other binary chat listeners that a new AI is online and listening to Binary.
if(announce_init_to_others && !is_centcom_level(z)) //Skip new syndicate AIs and also new AIs on centcom Z
@@ -1083,16 +1084,10 @@
end_multicam()
/mob/living/silicon/ai/up()
- set name = "Move Upwards"
- set category = "IC"
-
if(eyeobj.zMove(UP, z_move_flags = ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move upwards."))
/mob/living/silicon/ai/down()
- set name = "Move Down"
- set category = "IC"
-
if(eyeobj.zMove(DOWN, z_move_flags = ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move down."))
diff --git a/code/modules/mob/living/silicon/ai/freelook/chunk.dm b/code/modules/mob/living/silicon/ai/freelook/chunk.dm
index 04281f0b7626..3068b3342c1e 100644
--- a/code/modules/mob/living/silicon/ai/freelook/chunk.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/chunk.dm
@@ -50,7 +50,9 @@
seenby -= eye
var/client/client = eye.GetViewerClient()
- if(client && eye.use_visibility && seenby.len == 0)
+ // Bandaid fix for AI multicamera. Static disappears on other cameras when focused camera is far away.
+ // seenby.len check fixes that, but prevents static removal for those, who exit camera console, when a chunk was observed by 2 or more eye mobs.
+ if(client && eye.use_visibility && (seenby.len == 0 || (isliving(client.mob) && !isAI(client.mob)))) // Bypass for non-AIs, to unsure static removal.
client.images -= active_static_images
/**
diff --git a/code/modules/mob/living/silicon/robot/inventory.dm b/code/modules/mob/living/silicon/robot/inventory.dm
index 30790c176b54..e5e3e6f867d2 100644
--- a/code/modules/mob/living/silicon/robot/inventory.dm
+++ b/code/modules/mob/living/silicon/robot/inventory.dm
@@ -73,23 +73,6 @@
deselect_module(module_num)
playsound_local(src, SFX_RUSTLE, 40, TRUE)
-/mob/living/silicon/robot/update_held_items()
- . = ..()
- if(isnull(client) || isnull(hud_used) || hud_used.hud_version == HUD_STYLE_NOHUD)
- return
-
- var/turf/our_turf = get_turf(src)
- for (var/held_index in 1 to length(held_items))
- var/obj/item/held = held_items[held_index]
- if (!held)
- continue
- SET_PLANE(held, ABOVE_HUD_PLANE, our_turf)
- var/atom/movable/screen/robot/module_slot/slot = hud_used.screen_objects[HUD_KEY_CYBORG_MODULE(held_index)]
- if (!slot) //??
- continue
- held.screen_loc = slot.screen_loc
- client.screen |= held
-
/mob/living/silicon/robot/put_in_hand_check(obj/item/item_equipping)
return (item_equipping in model.modules)
@@ -143,7 +126,7 @@
audible_message(span_warning("[src] sounds an alarm! \"SYSTEM ERROR: Module [module_num] OFFLINE.\""))
to_chat(src, span_userdanger("SYSTEM ERROR: Module [module_num] OFFLINE."))
- var/atom/movable/screen/robot/module_slot/module = hud_used?.screen_objects[HUD_KEY_CYBORG_MODULE(module_num)]
+ var/atom/movable/screen/robot/module_slot/module = hud_used?.screen_objects[HUD_KEY_HAND_SLOT(module_num)]
if(module)
module.icon_state = "[module.base_icon_state] +b"
return TRUE
@@ -188,7 +171,7 @@
disabled_modules &= ~BORG_MODULE_THREE_DISABLED
to_chat(src, span_notice("ERROR CLEARED: Module [module_num] back online."))
- var/atom/movable/screen/robot/module_slot/module = hud_used?.screen_objects[HUD_KEY_CYBORG_MODULE(module_num)]
+ var/atom/movable/screen/robot/module_slot/module = hud_used?.screen_objects[HUD_KEY_HAND_SLOT(module_num)]
if(module)
module.icon_state = module.base_icon_state
return TRUE
@@ -270,10 +253,11 @@
if(is_invalid_module_number(module_num) || !held_items[module_num]) //If the slot number is invalid, or there's nothing there, we have nothing to equip
return FALSE
- var/atom/movable/screen/robot/module_slot/module = hud_used?.screen_objects[HUD_KEY_CYBORG_MODULE(module_num)]
+ var/atom/movable/screen/robot/module_slot/module = hud_used?.screen_objects[HUD_KEY_HAND_SLOT(module_num)]
if(module && module_active != held_items[module_num])
module.icon_state = "[module.base_icon_state] +a"
module_active = held_items[module_num]
+ SEND_SIGNAL(module_active, COMSIG_SILICON_MODULE_ACTIVATION, TRUE)
return TRUE
/**
@@ -282,9 +266,11 @@
* * module_num - the slot number being de-selected
*/
/mob/living/silicon/robot/proc/deselect_module(module_num)
- var/atom/movable/screen/robot/module_slot/module = hud_used?.screen_objects[HUD_KEY_CYBORG_MODULE(module_num)]
+ var/atom/movable/screen/robot/module_slot/module = hud_used?.screen_objects[HUD_KEY_HAND_SLOT(module_num)]
if(module)
module.icon_state = module.base_icon_state
+ if(module_active)
+ SEND_SIGNAL(module_active, COMSIG_SILICON_MODULE_ACTIVATION, FALSE)
module_active = null
return TRUE
diff --git a/code/modules/mob/living/silicon/robot/robot_defense.dm b/code/modules/mob/living/silicon/robot/robot_defense.dm
index b3c899b25421..ab191bf71069 100644
--- a/code/modules/mob/living/silicon/robot/robot_defense.dm
+++ b/code/modules/mob/living/silicon/robot/robot_defense.dm
@@ -214,6 +214,18 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
return NONE
+//Checks blockchance of any items in any module slots
+/mob/living/silicon/robot/check_block(atom/hit_by, damage, attack_text = "the attack", attack_type = MELEE_ATTACK, armour_penetration = 0, damage_type = BRUTE)
+ . = ..()
+ if(. == SUCCESSFUL_BLOCK)
+ return SUCCESSFUL_BLOCK
+
+ var/block_chance_modifier = round(damage / -3)
+ for(var/obj/item/module in held_items)
+ var/final_block_chance = module.block_chance - (clamp((armour_penetration - module.armour_penetration) / 2, 0, 100)) + block_chance_modifier
+ if(module.hit_reaction(src, hit_by, attack_text, final_block_chance, damage, attack_type, damage_type))
+ return SUCCESSFUL_BLOCK
+
// This has to go at the very end of interaction so we don't block every interaction with ID-like items
/mob/living/silicon/robot/base_item_interaction(mob/living/user, obj/item/tool, list/modifiers)
. = ..()
@@ -522,6 +534,8 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
return TRUE
/mob/living/silicon/robot/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ if(check_block(hitting_projectile, hitting_projectile.damage, "\the [hitting_projectile]", PROJECTILE_ATTACK, hitting_projectile.armour_penetration, hitting_projectile.damage_type))
+ return ..(hitting_projectile, def_zone, piercing_hit, 100)
. = ..()
if(prob(25) || . != BULLET_ACT_HIT)
return
diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm
index 72032c2104a7..af9b1e6d49fa 100644
--- a/code/modules/mob/living/silicon/robot/robot_model.dm
+++ b/code/modules/mob/living/silicon/robot/robot_model.dm
@@ -389,7 +389,6 @@
/obj/item/construction/rcd/borg,
/obj/item/pipe_dispenser,
/obj/item/extinguisher,
- /obj/item/weldingtool/largetank/cyborg,
/obj/item/borg/cyborg_omnitool/engineering,
/obj/item/borg/cyborg_omnitool/engineering,
/obj/item/t_scanner,
@@ -904,7 +903,7 @@
name = "Syndicate Assault"
basic_modules = list(
/obj/item/assembly/flash/cyborg,
- /obj/item/melee/energy/sword/cyborg,
+ /obj/item/melee/energy/sword/saber/cyborg,
/obj/item/gun/energy/printer,
/obj/item/gun/ballistic/revolver/grenadelauncher/cyborg,
/obj/item/card/emag,
@@ -937,7 +936,7 @@
/obj/item/borg/cyborg_omnitool/medical,
/obj/item/borg/cyborg_omnitool/medical,
/obj/item/blood_filter,
- /obj/item/melee/energy/sword/cyborg/saw,
+ /obj/item/melee/energy/sword/saber/cyborg/saw,
/obj/item/emergency_bed/silicon,
/obj/item/crowbar/cyborg,
/obj/item/extinguisher/mini,
@@ -961,7 +960,6 @@
/obj/item/pipe_dispenser,
/obj/item/restraints/handcuffs/cable/zipties,
/obj/item/extinguisher,
- /obj/item/weldingtool/largetank/cyborg,
/obj/item/analyzer,
/obj/item/borg/cyborg_omnitool/engineering,
/obj/item/borg/cyborg_omnitool/engineering,
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
index df31650e2f92..11e61104571b 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
@@ -213,27 +213,34 @@
animate(src, alpha = 255, time = duration)
/obj/effect/temp_visual/lava_warning/proc/fall(reset_time)
- var/turf/T = get_turf(src)
- playsound(T,'sound/effects/magic/fleshtostone.ogg', 80, TRUE)
+ var/turf/our_turf = get_turf(src)
+ playsound(our_turf,'sound/effects/magic/fleshtostone.ogg', 80, TRUE)
sleep(duration)
- playsound(T,'sound/effects/magic/fireball.ogg', 200, TRUE)
+ playsound(our_turf,'sound/effects/magic/fireball.ogg', 200, TRUE)
+ var/can_transform_turf = !isclosedturf(our_turf) && !islava(our_turf)
- for(var/mob/living/L in T.contents - owner)
- if(istype(L, /mob/living/simple_animal/hostile/megafauna/dragon))
+ for(var/mob/living/victim in our_turf)
+ if(istype(victim, /mob/living/simple_animal/hostile/megafauna/dragon) || victim == owner)
continue
- L.adjust_fire_loss(10)
- to_chat(L, span_userdanger("You fall directly into the pool of lava!"))
+ victim.adjust_fire_loss(10)
+ if(can_transform_turf)
+ to_chat(victim, span_userdanger("You fall directly into the pool of lava!"))
+ else
+ to_chat(victim, span_userdanger("You are set ablaze by a fireball from above!"))
// deals damage to mechs
- for(var/obj/vehicle/sealed/mecha/M in T.contents)
- M.take_damage(45, BRUTE, MELEE, 1)
-
- // changes turf to lava temporarily
- if(!isclosedturf(T) && !islava(T))
- var/lava_turf = /turf/open/lava/smooth
- var/reset_turf = T.type
- T.TerraformTurf(lava_turf, flags = CHANGETURF_INHERIT_AIR)
- addtimer(CALLBACK(T, TYPE_PROC_REF(/turf, ChangeTurf), reset_turf, null, CHANGETURF_INHERIT_AIR), reset_time, TIMER_OVERRIDE|TIMER_UNIQUE)
+ for(var/obj/vehicle/sealed/mecha/mech in our_turf)
+ mech.take_damage(45, BRUTE, MELEE, 1)
+
+ // changes turf to lava temporarily if possible, create a fire visual otherwise
+ if(!can_transform_turf)
+ new /obj/effect/temp_visual/fire/light(our_turf)
+ return
+
+ var/lava_turf = /turf/open/lava/smooth
+ var/reset_turf = our_turf.type
+ our_turf.TerraformTurf(lava_turf, flags = CHANGETURF_INHERIT_AIR)
+ addtimer(CALLBACK(our_turf, TYPE_PROC_REF(/turf, ChangeTurf), reset_turf, null, CHANGETURF_INHERIT_AIR), reset_time, TIMER_OVERRIDE|TIMER_UNIQUE)
/obj/effect/temp_visual/drakewall
desc = "An ash drakes true flame."
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm
index d62fa709e77f..fd4c4d58a96d 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm
@@ -230,19 +230,19 @@
min_damage = 30
max_damage = 35
-/obj/effect/goliath_tentacle/broodmother/patch/Initialize(mapload, new_spawner)
+/obj/effect/goliath_tentacle/broodmother/patch/Initialize(mapload, mob/living/goliath)
. = ..()
INVOKE_ASYNC(src, PROC_REF(createpatch))
/obj/effect/goliath_tentacle/broodmother/patch/proc/createpatch()
var/tentacle_locs = spiral_range_turfs(1, get_turf(src))
for(var/T in tentacle_locs)
- new /obj/effect/goliath_tentacle/broodmother(T)
+ new /obj/effect/goliath_tentacle/broodmother(T, owner)
var/list/directions = GLOB.cardinals.Copy()
for(var/i in directions)
var/turf/T = get_step(get_turf(src), i)
T = get_step(T, i)
- new /obj/effect/goliath_tentacle/broodmother(T)
+ new /obj/effect/goliath_tentacle/broodmother(T, owner)
#undef CALL_CHILDREN
#undef RAGE
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm
deleted file mode 100644
index 8a73d1465966..000000000000
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm
+++ /dev/null
@@ -1,67 +0,0 @@
-/mob/living/simple_animal/hostile/asteroid/polarbear
- name = "polar bear"
- desc = "An aggressive animal that defends its territory with incredible power. These beasts don't run from their enemies."
- icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
- icon_state = "polarbear"
- icon_living = "polarbear"
- icon_dead = "polarbear_dead"
- mob_biotypes = MOB_ORGANIC|MOB_BEAST|MOB_MINING
- mouse_opacity = MOUSE_OPACITY_ICON
- friendly_verb_continuous = "growls at"
- friendly_verb_simple = "growl at"
- speak_emote = list("growls")
- speed = 3
- move_to_delay = 8
- maxHealth = 300
- health = 300
- obj_damage = 40
- melee_damage_lower = 25
- melee_damage_upper = 25
- attack_verb_continuous = "claws"
- attack_verb_simple = "claw"
- attack_sound = 'sound/items/weapons/bladeslice.ogg'
- attack_vis_effect = ATTACK_EFFECT_CLAW
- vision_range = 2 // don't aggro unless you basically antagonize it, though they will kill you worse than a goliath will
- aggro_vision_range = 9
- move_force = MOVE_FORCE_VERY_STRONG
- move_resist = MOVE_FORCE_VERY_STRONG
- pull_force = MOVE_FORCE_VERY_STRONG
- butcher_results = list(/obj/item/food/meat/slab/bear = 3, /obj/item/stack/sheet/bone = 2)
- guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/goliath_hide/polar_bear_hide = 1)
- loot = list()
- crusher_loot = /obj/item/crusher_trophy/bear_paw
- stat_attack = HARD_CRIT
- robust_searching = TRUE
- footstep_type = FOOTSTEP_MOB_CLAW
- /// Message for when the polar bear starts to attack faster
- var/aggressive_message_said = FALSE
-
-/mob/living/simple_animal/hostile/asteroid/polarbear/Initialize(mapload)
- . = ..()
- AddElement(\
- /datum/element/change_force_on_death,\
- move_force = MOVE_FORCE_DEFAULT,\
- move_resist = MOVE_RESIST_DEFAULT,\
- pull_force = PULL_FORCE_DEFAULT,\
- )
-
-/mob/living/simple_animal/hostile/asteroid/polarbear/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
- . = ..()
- if(health > maxHealth*0.5)
- rapid_melee = initial(rapid_melee)
- return
- if(!aggressive_message_said && target)
- visible_message(span_danger("\The [src] gets an enraged look at [target]!"))
- aggressive_message_said = TRUE
- rapid_melee = 2
-
-/mob/living/simple_animal/hostile/asteroid/polarbear/Life(seconds_per_tick = SSMOBS_DT)
- . = ..()
- if(!. || target)
- return
- aggressive_message_said = FALSE
-
-/mob/living/simple_animal/hostile/asteroid/polarbear/lesser
- name = "magic polar bear"
- desc = "It seems sentient somehow."
- faction = list(FACTION_NEUTRAL)
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index d2e65fa4d0b0..519047e50da8 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -475,17 +475,6 @@
. = ..()
update_held_items()
-/mob/living/simple_animal/update_held_items()
- . = ..()
- if(!client || !hud_used || hud_used.hud_version == HUD_STYLE_NOHUD)
- return
- var/turf/our_turf = get_turf(src)
- for(var/obj/item/I in held_items)
- var/index = get_held_index_of_item(I)
- SET_PLANE(I, ABOVE_HUD_PLANE, our_turf)
- I.screen_loc = ui_hand_position(index)
- client.screen |= I
-
//ANIMAL RIDING
/mob/living/simple_animal/user_buckle_mob(mob/living/M, mob/user, check_loc = TRUE)
diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm
index 1f20223787d7..b240aad32f63 100644
--- a/code/modules/mob/living/status_procs.dm
+++ b/code/modules/mob/living/status_procs.dm
@@ -489,6 +489,49 @@
return quirk
return null
+/**
+ * get_quirk_string() is used to get a printable string of all the quirk traits someone has for certain criteria
+ *
+ * Arguments:
+ * * Medical- If we want the long, fancy descriptions that show up in medical records, or if not, just the name
+ * * Category- Which types of quirks we want to print out. Defaults to everything
+ * * from_scan- If the source of this call is like a health analyzer or HUD, in which case QUIRK_HIDE_FROM_MEDICAL hides the quirk.
+ */
+/mob/living/proc/get_quirk_string(medical = FALSE, category = CAT_QUIRK_ALL, from_scan = FALSE)
+ var/list/dat = list()
+ for(var/datum/quirk/candidate as anything in quirks)
+ if(from_scan && (candidate.quirk_flags & QUIRK_HIDE_FROM_SCAN))
+ continue
+ switch(category)
+ if(CAT_QUIRK_MAJOR_DISABILITY)
+ if(candidate.value >= -4)
+ continue
+ if(CAT_QUIRK_MINOR_DISABILITY)
+ if(!ISINRANGE(candidate.value, -4, -1))
+ continue
+ if(CAT_QUIRK_NOTES)
+ if(candidate.value < 0)
+ continue
+ dat += medical ? candidate.medical_record_text : candidate.name
+
+ if(!length(dat))
+ return medical ? "No issues have been declared." : "None"
+ return medical ? dat.Join(" ") : dat.Join(", ")
+
+/mob/living/proc/cleanse_quirk_datums() //removes all trait datums
+ QDEL_LAZYLIST(quirks)
+
+/mob/living/proc/transfer_quirk_datums(mob/living/to_mob)
+ // We could be done before the client was moved or after the client was moved
+ var/datum/preferences/to_pass = client || to_mob.client
+
+ for(var/datum/quirk/quirk as anything in quirks)
+ if(quirk.quirk_flags & QUIRK_NO_TRANSFER)
+ continue
+ quirk.remove_from_current_holder(quirk_transfer = TRUE)
+ quirk.add_to_holder(to_mob, quirk_transfer = TRUE, client_source = to_pass)
+
+
/// Helper to easily add a personality by a typepath
/mob/living/proc/add_personality(personality_type)
var/datum/personality/personality = SSpersonalities.personalities_by_type[personality_type]
@@ -509,6 +552,14 @@
for(var/personality_type in personalities)
remove_personality(personality_type)
+/// Returns a string with the names of the personalities of this mob, and their description as tooltip
+/mob/living/proc/get_parsonality_string()
+ var/list/return_list = list()
+ for(var/personality_type in personalities)
+ var/datum/personality/personality = SSpersonalities.personalities_by_type[personality_type]
+ return_list += span_tooltip(personality.desc, personality.name)
+ return english_list(return_list)
+
/mob/living/proc/cure_husk(source)
REMOVE_TRAIT(src, TRAIT_HUSK, source)
if(HAS_TRAIT(src, TRAIT_HUSK))
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index 1f0069e203c4..6c32c7a0e790 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -104,6 +104,7 @@
stop_sound_channel(CHANNEL_AMBIENCE)
if(client)
+
client.view_size?.resetToDefault() // Resets the client.view in case it was changed.
for(var/datum/action/A as anything in persistent_client.player_actions)
@@ -121,14 +122,17 @@
)
auto_deadmin_on_login()
+ //Check if they should have a stat panel, after they deadmined.
+ client.set_stat_panel()
+
log_message("Client [key_name(src)] has taken ownership of mob [src]([src.type])", LOG_OWNERSHIP)
log_mob_tag("TAG: [tag] NEW OWNER: [key_name(src)]")
SEND_SIGNAL(src, COMSIG_MOB_CLIENT_LOGIN, client)
SEND_SIGNAL(client, COMSIG_CLIENT_MOB_LOGIN, src)
client.init_verbs()
- AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
- AddElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
+ AddElement(/datum/element/weather_listener, /datum/weather/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
+ AddElement(/datum/element/weather_listener, /datum/weather/particle/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
AddElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds)
AddElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index dfed8f5d0dbe..167deedd9569 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -799,11 +799,7 @@
*
* Calls attack self on the item and updates the inventory hud for hands
*/
-/mob/verb/mode()
- set name = "Activate Held Object"
- set category = "IC"
- set src = usr
-
+/mob/proc/mode()
DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(execute_mode)))
///proc version to finish /mob/verb/mode() execution. used in case the proc needs to be queued for the tick after its first called
@@ -964,14 +960,8 @@
var/previous_index = active_hand_index
active_hand_index = held_index
- if(hud_used)
- var/atom/movable/screen/inventory/hand/held_location
- held_location = hud_used.hand_slots[previous_index]
- if(!isnull(held_location))
- held_location.update_appearance()
- held_location = hud_used.hand_slots[held_index]
- if(!isnull(held_location))
- held_location.update_appearance()
+ hud_used?.update_inventory_slot(ITEM_SLOT_HANDS, previous_index)
+ hud_used?.update_inventory_slot(ITEM_SLOT_HANDS, held_index)
return TRUE
/mob/proc/activate_hand(selected_hand)
@@ -1483,13 +1473,6 @@
//Do not do parent's actions, as we *usually* do this differently.
fully_replace_character_name(real_name, new_name)
-///Show the language menu for this mob
-/mob/verb/open_language_menu_verb()
- set name = "Open Language Menu"
- set category = "IC"
-
- get_language_holder().open_language_menu(usr)
-
///Adjust the nutrition of a mob
/mob/proc/adjust_nutrition(change, forced = FALSE) //Honestly FUCK the oldcoders for putting nutrition on /mob someone else can move it up because holy hell I'd have to fix SO many typechecks
if(HAS_TRAIT(src, TRAIT_NOHUNGER) && !forced)
@@ -1609,21 +1592,6 @@
clear_important_client_contents()
canon_client = null
-///Shows a tgui window with memories
-/mob/verb/memory()
- set name = "Memories"
- set category = "IC"
- set desc = "View your character's memories."
- if(!mind)
- var/fail_message = "You have no mind!"
- if(isobserver(src))
- fail_message += " You have to be in the current round at some point to have one."
- to_chat(src, span_warning(fail_message))
- return
- if(!mind.memory_panel)
- mind.memory_panel = new(usr, mind)
- mind.memory_panel.ui_interact(usr)
-
///Shows a tgui window with memories
/mob/proc/open_memory_panel()
if(!mind)
@@ -1676,12 +1644,6 @@
data["memories"] = memories
return data
-/mob/verb/view_skills()
- set category = "IC"
- set name = "View Skills"
-
- mind?.print_levels(src)
-
/mob/key_down(key, client/client, full_key)
..()
SEND_SIGNAL(src, COMSIG_MOB_KEYDOWN, key, client, full_key)
diff --git a/code/modules/mob/mob_lists.dm b/code/modules/mob/mob_lists.dm
index 4e50a4d8595d..d020f9df0f2c 100644
--- a/code/modules/mob/mob_lists.dm
+++ b/code/modules/mob/mob_lists.dm
@@ -46,36 +46,44 @@
///Adds the cliented mob reference to the list of all player-mobs, besides to either the of dead or alive player-mob lists, as appropriate. Called on Login().
/mob/proc/add_to_player_list()
SHOULD_CALL_PARENT(TRUE)
+
+
GLOB.player_list |= src
- if(client.holder)
- GLOB.keyloop_list |= src
- else if(stat != DEAD || !SSlag_switch?.measures[DISABLE_DEAD_KEYLOOP])
- GLOB.keyloop_list |= src
if(stat == DEAD)
add_to_current_dead_players()
else
add_to_current_living_players()
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PLAYER_LOGIN, src)
///Removes the mob reference from the list of all player-mobs, besides from either the of dead or alive player-mob lists, as appropriate. Called on Logout().
/mob/proc/remove_from_player_list()
SHOULD_CALL_PARENT(TRUE)
+
GLOB.player_list -= src
GLOB.keyloop_list -= src
if(stat == DEAD)
remove_from_current_dead_players()
else
remove_from_current_living_players()
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PLAYER_LOGOUT, src)
///Adds the cliented mob reference to either the list of dead player-mobs or to the list of observers, depending on how they joined the game.
/mob/proc/add_to_current_dead_players()
GLOB.dead_player_list |= src
+ if(!SSlag_switch.measures[DISABLE_DEAD_KEYLOOP] || client.holder)
+ GLOB.keyloop_list |= src
+ else
+ GLOB.keyloop_list -= src
/mob/dead/observer/add_to_current_dead_players()
+ if(!SSlag_switch.measures[DISABLE_DEAD_KEYLOOP] || client?.holder) // observers can move
+ GLOB.keyloop_list |= src
if(started_as_observer)
GLOB.current_observers_list |= src
return
- return ..()
+ else
+ GLOB.dead_player_list |= src
/mob/dead/new_player/add_to_current_dead_players()
return
@@ -94,6 +102,7 @@
///Adds the cliented mob reference to the list of living player-mobs. If the mob is an antag, it adds it to the list of living antag player-mobs.
/mob/proc/add_to_current_living_players()
GLOB.alive_player_list |= src
+ GLOB.keyloop_list |= src
if(is_antag(NONE))
add_to_current_living_antags()
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index 9417859da992..ebb9920abf30 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -27,7 +27,7 @@
* in the parent proc with istype checks right?):
* * having incorporeal_move set (calls Process_Incorpmove() instead)
* * being grabbed
- * * being buckled (relaymove() is called to the buckled atom instead)
+ * * being buckled (relaymove() is called to the buckled atom instead)
* * having your loc be some other mob (relaymove() is called on that mob instead)
* * Not having MOBILITY_MOVE
* * Failing Process_Spacemove() call
@@ -559,10 +559,7 @@
SEND_SIGNAL(src, COMSIG_MOVE_INTENT_TOGGLED)
///Moves a mob upwards in z level
-/mob/verb/up()
- set name = "Move Upwards"
- set category = "IC"
-
+/mob/proc/up()
if(remote_control)
return remote_control.relaymove(src, UP)
@@ -586,10 +583,7 @@
to_chat(src, span_notice("You move upwards."))
///Moves a mob down a z level
-/mob/verb/down()
- set name = "Move Down"
- set category = "IC"
-
+/mob/proc/down()
if(remote_control)
return remote_control.relaymove(src, DOWN)
@@ -618,3 +612,8 @@
if(new_turf && (istype(new_turf, /turf/cordon/secret) || is_secret_level(new_turf.z)) && !client?.holder)
return
return ..()
+
+/mob/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ if(client?.sound_tokens.len)
+ SSsound_tokens.clients_needing_update[client] = TRUE
diff --git a/code/modules/mob/mob_update_icons.dm b/code/modules/mob/mob_update_icons.dm
index bbd617f97f7e..11da749e89a0 100644
--- a/code/modules/mob/mob_update_icons.dm
+++ b/code/modules/mob/mob_update_icons.dm
@@ -66,6 +66,7 @@
/mob/proc/update_held_items()
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_MOB_UPDATE_HELD_ITEMS)
+ hud_used?.update_inventory_slot(ITEM_SLOT_HANDS)
///Updates the mask overlay & HUD element.
/mob/proc/update_worn_mask()
diff --git a/code/modules/mob_spawn/mob_spawn.dm b/code/modules/mob_spawn/mob_spawn.dm
index 7e3940829f8d..2734fe4dca91 100644
--- a/code/modules/mob_spawn/mob_spawn.dm
+++ b/code/modules/mob_spawn/mob_spawn.dm
@@ -221,7 +221,7 @@
/obj/effect/mob_spawn/ghost_role/proc/can_ghost_take(mob/dead/observer/user)
if(is_banned_from(user.ckey, role_ban))
to_chat(user, span_warning("You are banned from this role!"))
- return FALL_STOP_INTERCEPTING
+ return FALSE
if(!(GLOB.ghost_role_flags & GHOSTROLE_SPAWNER) && !(flags_1 & ADMIN_SPAWNED_1))
to_chat(user, span_warning("An admin has temporarily disabled non-admin ghost roles!"))
return FALSE
diff --git a/code/modules/mod/modules/module_holding.dm b/code/modules/mod/modules/module_holding.dm
index fc235663f1f7..1d8c86423332 100644
--- a/code/modules/mod/modules/module_holding.dm
+++ b/code/modules/mod/modules/module_holding.dm
@@ -58,10 +58,12 @@
storage_type = /datum/storage/bag_of_holding
create_storage(storage_type = /datum/storage/bag_of_holding)
atom_storage.set_locked(STORAGE_FULLY_LOCKED)
+ item_flags |= BLUESPACE_INTERFERENCE
/obj/item/mod/module/storage/holding/proc/on_core_removed()
QDEL_NULL(atom_storage)
storage_type = null
+ item_flags &= ~BLUESPACE_INTERFERENCE
/obj/item/mod/module/storage/holding/proc/try_install(_source, obj/item/mod/control/suit, mob/user)
SIGNAL_HANDLER
@@ -92,6 +94,7 @@
/obj/item/mod/module/storage/holding/prebuilt
prebuilt = TRUE
+ item_flags = BLUESPACE_INTERFERENCE
/obj/item/mod/module/storage/holding/prebuilt/locked
core_removable = FALSE
diff --git a/code/modules/mod/modules/modules_antag.dm b/code/modules/mod/modules/modules_antag.dm
index 44443c90fa2e..f7d28ed13410 100644
--- a/code/modules/mod/modules/modules_antag.dm
+++ b/code/modules/mod/modules/modules_antag.dm
@@ -527,7 +527,7 @@
/obj/item/mod/module/stealth/wraith/proc/start_stealth()
if(!COOLDOWN_FINISHED(src, recloak_timer)) // Prevents being able to bypass the cooldown by disabling and re-enabling the module
- addtimer(CALLBACK(src, PROC_REF(start_stealth)), recloak_timer)
+ addtimer(CALLBACK(src, PROC_REF(start_stealth)), COOLDOWN_TIMELEFT(src, recloak_timer))
return
RegisterSignals(mod.wearer, list(COMSIG_LIVING_MOB_BUMP, COMSIG_ATOM_BUMPED, COMSIG_MOB_FIRED_GUN), PROC_REF(unstealth))
RegisterSignal(mod.wearer, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_unarmed_attack))
diff --git a/code/modules/mod/modules/modules_supply.dm b/code/modules/mod/modules/modules_supply.dm
index 51a89d29e90b..9fe1b8a137bb 100644
--- a/code/modules/mod/modules/modules_supply.dm
+++ b/code/modules/mod/modules/modules_supply.dm
@@ -208,10 +208,10 @@
// Even faster if it has ore!
var/has_ore = !isnull(rock.mineral_type)
if (has_ore)
- toolspeed /= 1.5
+ toolspeed /= 2
rock.attackby(src, bumper, null, null, exp_multiplier)
if (has_ore)
- toolspeed *= 1.5
+ toolspeed *= 2
/obj/item/mod/module/drill/proc/on_module_activated(datum/source, obj/item/mod/module/module)
SIGNAL_HANDLER
@@ -231,7 +231,7 @@
return
toolspeed = initial(toolspeed)
use_energy_cost *= 2
- exp_multiplier /= 2
+ exp_multiplier /= 0.2
ballin = FALSE
if (!active)
on_deactivation()
@@ -577,7 +577,7 @@
/obj/item/mod/module/sphere_transform
name = "MOD sphere transform module"
desc = "A module able to move the suit's parts around, turning it and the user into a sphere. \
- The sphere can move quickly, even through lava, and launch mining bombs to decimate terrain."
+ The sphere can move quickly, even through lava, and launch mining micromissile to decimate terrain and fauna alike."
icon_state = "sphere"
module_type = MODULE_ACTIVE
removable = FALSE
@@ -687,12 +687,19 @@
. = ..()
if(!.)
return
- var/obj/projectile/bullet/mining_bomb/bomb = new(mod.wearer.loc)
- bomb.aim_projectile(target, mod.wearer)
- bomb.firer = mod.wearer
- playsound(src, 'sound/items/weapons/gun/general/grenade_launch.ogg', 75, TRUE)
- INVOKE_ASYNC(bomb, TYPE_PROC_REF(/obj/projectile, fire))
drain_power(use_energy_cost)
+ INVOKE_ASYNC(src, PROC_REF(fire_missile), target)
+ for (var/i in 1 to 2)
+ addtimer(CALLBACK(src, PROC_REF(fire_missile), target), 0.2 SECONDS * i)
+
+/obj/item/mod/module/sphere_transform/proc/fire_missile(atom/target)
+ var/obj/projectile/bullet/mining_missile/missile = new(mod.wearer.loc)
+ missile.aim_projectile(target, mod.wearer)
+ missile.firer = mod.wearer
+ if (isliving(target))
+ missile.set_homing_target(target)
+ playsound(src, 'sound/items/weapons/gun/general/rocket_launch.ogg', 30, TRUE)
+ missile.fire()
/obj/item/mod/module/sphere_transform/on_active_process(seconds_per_tick)
if(!mod.wearer.has_gravity())
@@ -703,80 +710,60 @@
if(mod.wearer.stat)
deactivate()
-/obj/projectile/bullet/mining_bomb
- name = "mining bomb"
- desc = "A bomb. Why are you examining this?"
- icon_state = "mine_bomb"
+/obj/projectile/bullet/mining_missile
+ name = "mining micromissile"
+ desc = "A missile. Why are you examining this?"
+ icon_state = "mine_missile"
icon = 'icons/obj/clothing/modsuit/mod_modules.dmi'
- damage = 0
+ damage = 3 // 3 * 4 = 12, *3 = 36 damage between 3 missiles
range = 6
+ homing_turn_speed = 12
suppressed = SUPPRESSED_VERY
armor_flag = BOMB
light_system = OVERLAY_LIGHT
light_range = 1
light_power = 1
- light_color = COLOR_LIGHT_ORANGE
+ light_color = LIGHT_COLOR_BABY_BLUE
embed_type = null
can_hit_turfs = TRUE
-
-/obj/projectile/bullet/mining_bomb/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/projectile_drop, /obj/structure/mining_bomb)
- RegisterSignal(src, COMSIG_PROJECTILE_ON_SPAWN_DROP, PROC_REF(handle_drop))
-
-/obj/projectile/bullet/mining_bomb/proc/handle_drop(datum/source, obj/structure/mining_bomb/mining_bomb)
- SIGNAL_HANDLER
- addtimer(CALLBACK(mining_bomb, TYPE_PROC_REF(/obj/structure/mining_bomb, prime), firer), mining_bomb.prime_time)
-
-/obj/structure/mining_bomb
- name = "mining bomb"
- desc = "A bomb. Why are you examining this?"
- icon_state = "mine_bomb"
- icon = 'icons/obj/clothing/modsuit/mod_modules.dmi'
- anchored = TRUE
- resistance_flags = FIRE_PROOF|LAVA_PROOF
- light_system = OVERLAY_LIGHT
- light_range = 1
- light_power = 1
- light_color = COLOR_LIGHT_ORANGE
- /// Time to prime the explosion
- var/prime_time = 0.1 SECONDS
- /// Time to explode from the priming
- var/explosion_time = 0.9 SECONDS // Roughly this much until the blast part of the explosion animation
- /// Damage done on explosion.
- var/damage = 7
- /// Damage multiplier on hostile fauna.
+ /// Damage multiplier against lavaland fauna
var/fauna_boost = 4
-/obj/structure/mining_bomb/proc/prime(atom/movable/firer)
- var/mutable_appearance/explosion_image = mutable_appearance('icons/effects/96x96.dmi', "judicial_explosion", FLOAT_LAYER, src, ABOVE_GAME_PLANE)
- explosion_image.pixel_w = -32
- explosion_image.pixel_z = -32
- var/turf/our_loc = get_turf(src)
- our_loc.flick_overlay_view(explosion_image, 1.35 SECONDS)
- addtimer(CALLBACK(src, PROC_REF(boom), firer), explosion_time)
-
-/obj/structure/mining_bomb/proc/boom(atom/movable/firer)
- visible_message(span_danger("[src] explodes!"))
- playsound(src, 'sound/effects/magic/magic_missile.ogg', 200, vary = TRUE)
- var/turf/our_turf = get_turf(src)
- for(var/turf/closed/mineral/rock in RANGE_TURFS(1, src))
- if (rock == our_turf)
- rock.gets_drilled(firer, 0)
- else
- rock.drill_aoe(firer, 0)
- for(var/mob/living/victim in range(1, src))
- if(HAS_TRAIT(victim, TRAIT_MINING_AOE_IMMUNE))
- continue
- victim.apply_damage(damage * (ismining(victim) ? fauna_boost : 1), BRUTE, spread_damage = TRUE)
- to_chat(victim, span_userdanger("You are hit by a mining bomb explosion!"))
- if(!firer)
- continue
- if(ishostile(victim))
- var/mob/living/simple_animal/hostile/hostile_mob = victim
- hostile_mob.GiveTarget(firer)
- else if(isbasicmob(victim))
- victim.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, firer)
- for(var/obj/object in range(1, src))
- object.take_damage(damage, BRUTE, BOMB)
- qdel(src)
+/obj/projectile/bullet/mining_missile/on_hit(atom/target, blocked, pierce_hit)
+ playsound(get_turf(target), 'sound/items/weapons/sonic_jackhammer.ogg', 75, TRUE)
+ if (ismineralturf(target))
+ . = ..()
+ spawn_particles(target)
+ var/turf/closed/mineral/rock = target
+ rock.gets_drilled(firer)
+ return BULLET_ACT_HIT
+
+ if (!isliving(target))
+ . = ..()
+ spawn_particles(target)
+ return
+
+ if (isliving(target))
+ var/mob/living/victim = target
+ if (ismining(victim))
+ damage *= fauna_boost
+ . = ..()
+ spawn_particles(target)
+
+/obj/projectile/bullet/mining_missile/proc/spawn_particles(atom/target)
+ var/obj/effect/abstract/particle_holder/impact_particles = new(get_turf(target), /particles/micromissile_impact)
+ impact_particles.particles.position = generator(GEN_BOX, list(impact_x - 2, impact_y - 2), list(impact_x + 2, impact_y + 2), NORMAL_RAND)
+ impact_particles.particles.velocity = generator(GEN_BOX, list(movement_vector.pixel_x * 0.5 * speed * ICON_SIZE_X - 2, movement_vector.pixel_y * 0.5 * speed * ICON_SIZE_Y - 2, ), list(movement_vector.pixel_x * 0.5 * speed * ICON_SIZE_X + 2, movement_vector.pixel_y * 0.5 * speed * ICON_SIZE_Y + 2), NORMAL_RAND)
+ QDEL_IN(impact_particles, /particles/micromissile_impact::lifespan)
+
+/particles/micromissile_impact
+ icon = 'icons/effects/particles/generic.dmi'
+ icon_state = "cross"
+ width = 100
+ height = 100
+ count = 10
+ spawning = 10
+ color = LIGHT_COLOR_BABY_BLUE
+ lifespan = 1 SECONDS
+ fade = 1 SECONDS
+ spin = generator(GEN_NUM, -20, 20)
diff --git a/code/modules/modular_computers/computers/item/pda.dm b/code/modules/modular_computers/computers/item/pda.dm
index a2c83e5d6e7d..cfb6eb66554a 100644
--- a/code/modules/modular_computers/computers/item/pda.dm
+++ b/code/modules/modular_computers/computers/item/pda.dm
@@ -362,6 +362,7 @@
/obj/item/modular_computer/pda/silicon/Destroy()
silicon_owner = null
+ robotact = null
return ..()
///Silicons don't have the tools (or hands) to make circuits setups with their own PDAs.
diff --git a/code/modules/modular_computers/file_system/data.dm b/code/modules/modular_computers/file_system/data.dm
index 7721d50b7639..661318f4baf9 100644
--- a/code/modules/modular_computers/file_system/data.dm
+++ b/code/modules/modular_computers/file_system/data.dm
@@ -98,6 +98,10 @@
src.workspace = workspace
src.source_photo_or_painting = source_photo_or_painting
+/datum/computer_file/data/paint_project/Destroy(force)
+ source_photo_or_painting = null
+ return ..()
+
/datum/computer_file/data/paint_project/clone(rename)
var/datum/computer_file/data/paint_project/temp = ..()
temp.workspace = workspace.copy()
diff --git a/code/modules/modular_computers/file_system/image_file.dm b/code/modules/modular_computers/file_system/image_file.dm
index f029793701d2..8dcfd942f227 100644
--- a/code/modules/modular_computers/file_system/image_file.dm
+++ b/code/modules/modular_computers/file_system/image_file.dm
@@ -27,6 +27,10 @@
src.image_name = image_name
set_source(source_photo_or_painting)
+/datum/computer_file/image/Destroy(force)
+ source_photo_or_painting = null
+ return ..()
+
/datum/computer_file/image/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing)
. = ..()
assign_path()
diff --git a/code/modules/modular_computers/file_system/programs/budgetordering.dm b/code/modules/modular_computers/file_system/programs/budgetordering.dm
index d4f8e125fd7e..bbc4d29f6613 100644
--- a/code/modules/modular_computers/file_system/programs/budgetordering.dm
+++ b/code/modules/modular_computers/file_system/programs/budgetordering.dm
@@ -64,7 +64,7 @@
var/datum/bank_account/buyer = SSeconomy.get_dep_account(cargo_account)
var/obj/item/card/id/id_card = computer.stored_id?.GetID()
- if(id_card?.registered_account)
+ if(id_card?.registered_account?.account_job?.paycheck_department)
buyer = SSeconomy.get_dep_account(id_card?.registered_account.account_job.paycheck_department)
if((ACCESS_BUDGET in id_card.access))
requestonly = FALSE
@@ -285,7 +285,15 @@
var/turf/T = get_turf(computer)
var/datum/supply_order/SO = new(pack, name, rank, ckey, reason, account)
- SO.generateRequisition(T)
+ if(computer.stored_paper >= 1)
+ SO.generateRequisition(T)
+ computer.stored_paper -= 1
+ if(computer.stored_paper <= 4)
+ computer.say("Paper's storage has only [computer.stored_paper] papers. Refill please!")
+ if(computer.stored_paper <= 1)
+ computer.say("Only 1 paper has left, refill please!")
+ else
+ computer.say("Requisition cannot be printed, paper storage is empty. Please insert more paper!")
if((requestonly && !self_paid) || !(computer.stored_id?.GetID()))
SSshuttle.request_list += SO
else
diff --git a/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm b/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm
index f71d985197c9..5ea1bb1f51b9 100644
--- a/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm
+++ b/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm
@@ -126,8 +126,8 @@
if("PRG_savelog")
if(!channel)
return
- var/logname = stripped_input(params["log_name"])
- if(!logname)
+ var/logname = trim(params["log_name"], MAX_MESSAGE_LEN)
+ if(!length(logname) || !filter_filename_pda(logname))
return
var/datum/computer_file/data/text/logfile = new()
// Now we will generate HTML-compliant file that can actually be viewed/printed.
diff --git a/code/modules/modular_computers/file_system/programs/file_browser.dm b/code/modules/modular_computers/file_system/programs/file_browser.dm
index d25a9894307e..8c3dbfe213a5 100644
--- a/code/modules/modular_computers/file_system/programs/file_browser.dm
+++ b/code/modules/modular_computers/file_system/programs/file_browser.dm
@@ -119,8 +119,8 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
var/datum/computer_file/file = computer.find_file_by_name(params["name"])
if(!file)
return
- var/newname = reject_bad_name(params["new_name"], allow_numbers = TRUE, cap_after_symbols = FALSE, cap_at_start = FALSE)
- if(!newname || newname != params["new_name"])
+ var/newname = trim(params["new_name"], MAX_MESSAGE_LEN)
+ if(!length(newname) || !filter_filename_pda(newname))
playsound(computer, 'sound/machines/terminal/terminal_error.ogg', 25, FALSE)
return
file.filename = newname
@@ -131,8 +131,8 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
var/datum/computer_file/file = computer.find_file_by_name(params["name"], computer.inserted_disk)
if(!file)
return
- var/newname = reject_bad_name(params["new_name"], allow_numbers = TRUE, cap_after_symbols = FALSE, cap_at_start = FALSE)
- if(!newname || newname != params["new_name"])
+ var/newname = trim(params["new_name"], MAX_MESSAGE_LEN)
+ if(!length(newname) || !filter_filename_pda(newname))
playsound(computer, 'sound/machines/terminal/terminal_error.ogg', 25, FALSE)
return
file.filename = newname
diff --git a/code/modules/modular_computers/file_system/programs/nanopaint.dm b/code/modules/modular_computers/file_system/programs/nanopaint.dm
index 6e2d4bd49ab7..4d499db7c240 100644
--- a/code/modules/modular_computers/file_system/programs/nanopaint.dm
+++ b/code/modules/modular_computers/file_system/programs/nanopaint.dm
@@ -175,11 +175,14 @@ GLOBAL_LIST_INIT(nanopaint_supported_filetypes, zebra_typecacheof(list(\
return
dialog = null
var/uid = params["uid"]
- var/new_file_name = params["name"]
+ var/new_file_name = trim(params["name"], MAX_MESSAGE_LEN)
var/saving_to_disk = params["onDisk"]
var/datum/computer_file/new_file_type = text2path(params["typepath"])
var/extension = new_file_type::filetype
var/datum/computer_file/existing_file
+ if(!length(new_file_name))
+ dialog = list("type" = "error", "message" = "No name specified.")
+ return TRUE
if(saving_to_disk)
if(!computer.inserted_disk)
dialog = list("type" = "error", "message" = "[new_file_name] - The disk has been removed.")
@@ -208,6 +211,12 @@ GLOBAL_LIST_INIT(nanopaint_supported_filetypes, zebra_typecacheof(list(\
else
INVOKE_ASYNC(src, PROC_REF(write_to_file), user, existing_file)
return TRUE
+ if(is_ic_filtered_for_pdas(new_file_name) || is_soft_ic_filtered_for_pdas(new_file_name))
+ dialog = list("type" = "error", "message" = "The entered file name violates company policy.")
+ return TRUE
+ if(!filter_illegal_filename_chars(new_file_name))
+ dialog = list("type" = "error", "message" = "[new_file_name] - File names cannot include the following characters. \n \\ / : * ? \" < > |")
+ return TRUE
INVOKE_ASYNC(src, PROC_REF(save_file), user, new_file_name, new_file_type, saving_to_disk && computer.inserted_disk)
return TRUE
@@ -283,7 +292,7 @@ GLOBAL_LIST_INIT(nanopaint_supported_filetypes, zebra_typecacheof(list(\
/datum/computer_file/program/nanopaint/proc/save_file(mob/user, name, file_type, obj/item/disk/computer/target_disk)
var/datum/computer_file/file = new file_type()
- file.filename = reject_bad_name(name, allow_numbers = TRUE, cap_after_symbols = FALSE, cap_at_start = FALSE)
+ file.filename = name
var/file_stored
if(target_disk)
file_stored = target_disk.add_file(file)
diff --git a/code/modules/modular_computers/file_system/programs/secureye.dm b/code/modules/modular_computers/file_system/programs/secureye.dm
index c488e1d0ec68..640de44979c9 100644
--- a/code/modules/modular_computers/file_system/programs/secureye.dm
+++ b/code/modules/modular_computers/file_system/programs/secureye.dm
@@ -174,7 +174,7 @@
CRASH("[src] was able to track [target] through /datum/trackable, but was not on a visible turf to cameras.")
for(var/obj/machinery/camera/cameras as anything in target_camerachunk.cameras["[target.z]"])
// We need to find a particular camera that can see this turf
- if(length(cameras.can_see() & list(target_turf)))
+ if(!(length(cameras.can_see() & list(target_turf))))
continue
var/new_camera = WEAKREF(cameras)
if(camera_ref == new_camera)
diff --git a/code/modules/modular_computers/file_system/programs/techweb.dm b/code/modules/modular_computers/file_system/programs/techweb.dm
index c01c865fbf7e..3f9afa610799 100644
--- a/code/modules/modular_computers/file_system/programs/techweb.dm
+++ b/code/modules/modular_computers/file_system/programs/techweb.dm
@@ -80,7 +80,8 @@
"can_unlock" = stored_research.can_unlock_node(node),
"have_experiments_done" = stored_research.have_experiments_for_node(node),
"tier" = stored_research.tiers[node.id],
- "enqueued_by_user" = enqueued_by_user
+ "enqueued_by_user" = enqueued_by_user,
+ "discount_boosted" = node.discount_boosted
))
// Get experiments and serialize them
@@ -156,6 +157,8 @@
node_cache[compressed_id]["required_experiments"] = node.required_experiments
if (LAZYLEN(node.discount_experiments))
node_cache[compressed_id]["discount_experiments"] = node.discount_experiments
+ if (LAZYLEN(node.discount_boosts))
+ node_cache[compressed_id]["discount_boosts"] = node.discount_boosts
// Build design cache
var/design_cache = list()
diff --git a/code/modules/movespeed/modifiers/mobs.dm b/code/modules/movespeed/modifiers/mobs.dm
index 2dcef451fdf9..3eec563c671d 100644
--- a/code/modules/movespeed/modifiers/mobs.dm
+++ b/code/modules/movespeed/modifiers/mobs.dm
@@ -163,7 +163,7 @@
variable = TRUE
/datum/movespeed_modifier/goliath_mount
- multiplicative_slowdown = -27.5
+ multiplicative_slowdown = -9.5
/datum/movespeed_modifier/goldgrub_mount
multiplicative_slowdown = -4.5
diff --git a/code/modules/movespeed/modifiers/status_effects.dm b/code/modules/movespeed/modifiers/status_effects.dm
index df114aca4070..265362f7b8cf 100644
--- a/code/modules/movespeed/modifiers/status_effects.dm
+++ b/code/modules/movespeed/modifiers/status_effects.dm
@@ -41,6 +41,9 @@
/datum/movespeed_modifier/status_effect/tired_post_charge/lesser
multiplicative_slowdown = 2
+/datum/movespeed_modifier/status_effect/saw_slashes_slowdown
+ multiplicative_slowdown = 0.5
+
/// Get slower the more gold is in your system.
/datum/movespeed_modifier/status_effect/midas_blight
id = MOVESPEED_ID_MIDAS_BLIGHT
diff --git a/code/modules/pai/pai.dm b/code/modules/pai/pai.dm
index 2573d75f5e8c..37c9e943debf 100644
--- a/code/modules/pai/pai.dm
+++ b/code/modules/pai/pai.dm
@@ -25,7 +25,7 @@
move_resist = 0
name = "pAI"
pass_flags = PASSTABLE | PASSMOB
- pull_force = 0
+ pull_force = MOVE_FORCE_NONE
radio = /obj/item/radio/headset/silicon/pai
worn_slot_flags = ITEM_SLOT_HEAD
diff --git a/code/modules/paperwork/paper_biscuit.dm b/code/modules/paperwork/paper_biscuit.dm
index cd45586c9e64..75d14f551d2c 100644
--- a/code/modules/paperwork/paper_biscuit.dm
+++ b/code/modules/paperwork/paper_biscuit.dm
@@ -147,6 +147,8 @@
add_fingerprint(user)
if(!cracked)
return ..()
+ if(has_been_sealed)
+ return
if(tgui_alert(user, "Do you want to seal it? This can only be done once.", "Biscuit Sealing", list("Yes", "No")) != "Yes")
return
cracked = FALSE
diff --git a/code/modules/photography/camera/camera.dm b/code/modules/photography/camera/camera.dm
index cb9d1ca3089d..7689a4ec7b3c 100644
--- a/code/modules/photography/camera/camera.dm
+++ b/code/modules/photography/camera/camera.dm
@@ -189,6 +189,7 @@
/**
* Attempts to take an image of the target and all its surrounding tiles
+ * Returns TRUE if it successfully starts taking a picture.
* Arguments
*
* * atom/target - the target we are trying to take a photo of
@@ -200,14 +201,16 @@
if(!on)
if(user)
user.balloon_alert(user, "flash still charging!")
- return
+ return FALSE
if(blending)
if(user)
user.balloon_alert(user, "image still blending!")
- return
+ return FALSE
+ blending = TRUE
INVOKE_ASYNC(src, PROC_REF(capture_image), target, user)
+ return TRUE
/**
* Renders an image of the target and all its surrounding tiles
@@ -219,11 +222,14 @@
var/turf/target_turf = get_turf(target)
var/view_size = user?.client?.view || world.view
if(isnull(target_turf))
+ blending = FALSE
return
if(isAI(user))
if(!SScameras.is_visible_by_cameras(target_turf))
+ blending = FALSE
return
else if(!(target_turf in view(view_size, user)))
+ blending = FALSE
return
//These vars will be reused later on
@@ -234,11 +240,11 @@
var/viewer = get_turf(user?.client?.eye || user)
var/list/seen = get_hear_turfs(view_range, viewer)
if(!(target_turf in seen))
+ blending = FALSE
return
//taking the actual picture
on_flash(target, user)
- blending = TRUE
var/list/mobs_spotted = list()
var/list/dead_spotted = list()
var/list/turfs = list()
diff --git a/code/modules/plumbing/ducts.dm b/code/modules/plumbing/ducts.dm
index 961ee5341284..fe86c09e7127 100644
--- a/code/modules/plumbing/ducts.dm
+++ b/code/modules/plumbing/ducts.dm
@@ -114,7 +114,7 @@
neighbours[other] = direction
//connecting duct to us
LAZYADDASSOC(other.neighbours, src, opposite_dir)
- other.update_appearance(UPDATE_ICON_STATE)
+ other.update_appearance(UPDATE_ICON)
continue
@@ -138,7 +138,7 @@
//assign neighbour
neighbours[plumbing.parent] = direction
- update_appearance(UPDATE_ICON_STATE)
+ update_appearance(UPDATE_ICON)
///we disconnect ourself from our neighbours. we also destroy our ductnet and tell our neighbours to make a new one
/obj/machinery/duct/on_deconstruction()
@@ -152,7 +152,7 @@
//remove ourself from the duct
net.ducts -= src
if(!net.ducts.len)
- qdel(net) //destroy the pipeline. Suppliers aren't important if there are ducts
+ qdel(net) //destroy the pipeline. Suppliers aren't important if there aren't any ducts left
net = null
/obj/machinery/duct/Destroy()
@@ -168,7 +168,7 @@
var/obj/machinery/duct/pipe = neighbour
if(istype(pipe))
pipe.neighbours -= src
- pipe.update_appearance(UPDATE_ICON_STATE)
+ pipe.update_appearance(UPDATE_ICON)
//find every node that can be reached from our neighbour making sure to not revisit it again in circles
if(visited[neighbour])
diff --git a/code/modules/plumbing/plumbers/acclimator.dm b/code/modules/plumbing/plumbers/acclimator.dm
index 28c1ef7b8219..0e9e14433445 100644
--- a/code/modules/plumbing/plumbers/acclimator.dm
+++ b/code/modules/plumbing/plumbers/acclimator.dm
@@ -1,5 +1,5 @@
-///Same as a tier 1 chem heater
-#define HEATER_COFFICIENT 0.1
+///Same as a tier 3 chem heater
+#define HEATER_COFFICIENT 0.3
///Decimal point in rounding temperature
#define TEMP_ROUNDING 0.01
///Minimal allowed difference temperature range
@@ -11,7 +11,7 @@
icon_state = "acclimator"
base_icon_state = "acclimator"
buffer = 200
- active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2
+ active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2.5
///towards wich temperature do we build?
var/target_temperature = 300
diff --git a/code/modules/power/apc/apc_main.dm b/code/modules/power/apc/apc_main.dm
index 4af0bb0496b5..7335f9a9c442 100644
--- a/code/modules/power/apc/apc_main.dm
+++ b/code/modules/power/apc/apc_main.dm
@@ -100,7 +100,7 @@
var/long_term_power = 10
///Automatically name the APC after the area is in
var/auto_name = FALSE
- ///Time to allow the APC to regain some power and to turn the channels back online
+ ///Time to allow the APC to regain some power and to turn the channels back online in seconds
var/failure_timer = 0
///Forces an update on the power use to ensure that the apc has enough power
var/force_update = FALSE
@@ -282,8 +282,8 @@
/obj/machinery/power/apc/on_saboteur(datum/source, disrupt_duration)
. = ..()
- disrupt_duration *= 0.1 // so, turns out, failure timer is in seconds, not deciseconds; without this, disruptions last 10 times as long as they probably should
- energy_fail(disrupt_duration)
+ // failure timer is in seconds, not deciseconds, so we need to convert
+ energy_fail(disrupt_duration * 0.1)
return TRUE
/obj/machinery/power/apc/on_set_is_operational(old_value)
@@ -589,7 +589,7 @@
if(!area?.requires_power)
return
if(failure_timer)
- failure_timer--
+ failure_timer = max(0, failure_timer - seconds_per_tick)
force_update = TRUE
return
diff --git a/code/modules/power/apc/apc_malf.dm b/code/modules/power/apc/apc_malf.dm
index eb0526e32ca1..8cdcc3584590 100644
--- a/code/modules/power/apc/apc_malf.dm
+++ b/code/modules/power/apc/apc_malf.dm
@@ -56,8 +56,6 @@
disk_pinpointers.switch_mode_to(TRACK_MALF_AI) //Pinpointer will track the shunted AI
var/datum/action/innate/core_return/return_action = new
return_action.Grant(occupier)
- SEND_SIGNAL(src, COMSIG_SILICON_AI_OCCUPY_APC, occupier)
- SEND_SIGNAL(occupier, COMSIG_SILICON_AI_OCCUPY_APC, occupier)
occupier.cancel_camera()
/obj/machinery/power/apc/proc/malfvacate(forced)
@@ -71,6 +69,12 @@
occupier.gib(DROP_ALL_REMAINS)
occupier = null
return
+
+ if(!occupier.nuking) //Pinpointers go back to tracking the nuke disk, as long as the AI (somehow) isn't mid-nuking.
+ for(var/obj/item/pinpointer/nuke/disk_pinpointers in GLOB.pinpointer_list)
+ disk_pinpointers.switch_mode_to(TRACK_NUKE_DISK)
+ disk_pinpointers.alert = FALSE
+
if(occupier.linked_core)
occupier.shunted = FALSE
occupier.resolve_core_link()
@@ -78,10 +82,6 @@
else
stack_trace("An AI: [occupier] has vacated an APC with no linked core and without being gibbed.")
- if(!occupier.nuking) //Pinpointers go back to tracking the nuke disk, as long as the AI (somehow) isn't mid-nuking.
- for(var/obj/item/pinpointer/nuke/disk_pinpointers in GLOB.pinpointer_list)
- disk_pinpointers.switch_mode_to(TRACK_NUKE_DISK)
- disk_pinpointers.alert = FALSE
/obj/machinery/power/apc/transfer_ai(interaction, mob/user, mob/living/silicon/ai/AI, obj/item/aicard/card)
. = ..()
diff --git a/code/modules/power/apc/apc_power_proc.dm b/code/modules/power/apc/apc_power_proc.dm
index a561ba88a84c..cd1b412181a7 100644
--- a/code/modules/power/apc/apc_power_proc.dm
+++ b/code/modules/power/apc/apc_power_proc.dm
@@ -127,16 +127,15 @@
terminal.master = null
terminal = null
+/**
+ * Temporarily disables all power to the room for a set duration
+ *
+ * Some rooms are immune to this effect due to having important machines
+ *
+ * * duration - the duration of the power failure in seconds (not deciseconds)
+ */
/obj/machinery/power/apc/proc/energy_fail(duration)
- for(var/obj/machinery/failing_machine in area.contents)
- if(failing_machine.critical_machine)
- return
-
- for(var/mob/living/silicon/ai as anything in GLOB.ai_list)
- if(get_area(ai) == area)
- return
-
- failure_timer = max(failure_timer, round(duration))
+ failure_timer = max(failure_timer, round(duration, SSMACHINES_DT))
update()
queue_icon_update()
@@ -147,10 +146,9 @@
if(nightshift_lights == on)
return //no change
nightshift_lights = on
- for (var/list/zlevel_turfs as anything in area.get_zlevel_turf_lists())
- for(var/turf/area_turf as anything in zlevel_turfs)
- for(var/obj/machinery/light/night_light in area_turf)
- if(night_light.nightshift_allowed)
- night_light.nightshift_enabled = nightshift_lights
- night_light.update(FALSE)
- CHECK_TICK
+ for(var/turf/area_turf as anything in area.get_turfs_from_all_zlevels())
+ for(var/obj/machinery/light/night_light in area_turf)
+ if(night_light.nightshift_allowed)
+ night_light.nightshift_enabled = nightshift_lights
+ night_light.update(FALSE)
+ CHECK_TICK
diff --git a/code/modules/power/lighting/light.dm b/code/modules/power/lighting/light.dm
index 6a0654d98733..26e01f2dd801 100644
--- a/code/modules/power/lighting/light.dm
+++ b/code/modules/power/lighting/light.dm
@@ -81,6 +81,8 @@
var/power_consumption_rate = 20
///break if moved, if false also makes it ignore if the wall its on breaks
var/break_if_moved = TRUE
+ /// If TRUE we can break on init
+ var/allow_break_on_init = TRUE
// create a new lighting fixture
/obj/machinery/light/Initialize(mapload)
@@ -126,13 +128,14 @@
/obj/machinery/light/post_machine_initialize()
. = ..()
#ifndef MAP_TEST
- switch(fitting)
- if("tube")
- if(prob(1)) // DARKPACK EDIT CHANGE
- break_light_tube(TRUE)
- if("bulb")
- if(prob(2)) // DARKPACK EDIT CHANGE
- break_light_tube(TRUE)
+ if(allow_break_on_init)
+ switch(fitting)
+ if("tube")
+ if(prob(1)) // DARKPACK EDIT CHANGE
+ break_light_tube(TRUE)
+ if("bulb")
+ if(prob(2)) // DARKPACK EDIT CHANGE
+ break_light_tube(TRUE)
#endif
update(trigger = FALSE)
@@ -676,17 +679,19 @@
var/obj/item/light/light_tube = drop_light_tube()
return light_tube.attack_tk(user)
-// break the light and make sparks if was on
+// break the light and make sparks if was on, state is mutated BEFORE firing side-effects to prevent re-entrancy loops from synchronous signals.
/obj/machinery/light/proc/break_light_tube(skip_sound_and_sparks = FALSE)
if(status == LIGHT_EMPTY || status == LIGHT_BROKEN)
return
+ var/was_ok = (status == LIGHT_OK || status == LIGHT_BURNED)
+ status = LIGHT_BROKEN
+
if(!skip_sound_and_sparks)
- if(status == LIGHT_OK || status == LIGHT_BURNED)
+ if(was_ok)
playsound(loc, 'sound/effects/glass/glasshit.ogg', 75, TRUE)
if(on)
do_sparks(3, TRUE, src)
- status = LIGHT_BROKEN
update()
/obj/machinery/light/proc/fix()
diff --git a/code/modules/power/solar.dm b/code/modules/power/solar.dm
index b737ca8a4dfd..72e6b3985dd4 100644
--- a/code/modules/power/solar.dm
+++ b/code/modules/power/solar.dm
@@ -1,5 +1,4 @@
#define SOLAR_GEN_RATE 2500
-#define OCCLUSION_DISTANCE 20
#define PANEL_Z_OFFSET 13
#define PANEL_EDGE_Z_OFFSET (PANEL_Z_OFFSET - 2)
@@ -17,6 +16,7 @@
var/id
+ /// Tracks if the sun is obscured from the panel by the station (or something else)
var/obscured = FALSE
///`[0-1]` measure of obscuration -- multipllier against power generation
var/sunfrac = 0
@@ -204,30 +204,10 @@
if(azimuth_current != azimuth_target)
visually_turn(azimuth_target)
azimuth_current = azimuth_target
- occlusion_setup()
+ var/turf/sun_turf = get_turf(src)
+ obscured = isnull(sun_turf) ? TRUE : sun_turf.is_sunlight_blocked()
needs_to_update_solar_exposure = TRUE
-///trace towards sun to see if we're in shadow
-/obj/machinery/power/solar/proc/occlusion_setup()
- obscured = TRUE
-
- var/distance = OCCLUSION_DISTANCE
- var/target_x = round(sin(SSsun.azimuth), 0.01)
- var/target_y = round(cos(SSsun.azimuth), 0.01)
- var/x_hit = x
- var/y_hit = y
- var/turf/hit
-
- for(var/run in 1 to distance)
- x_hit += target_x
- y_hit += target_y
- hit = locate(round(x_hit, 1), round(y_hit, 1), z)
- if(IS_OPAQUE_TURF(hit))
- return
- if(hit.x == 1 || hit.x == world.maxx || hit.y == 1 || hit.y == world.maxy) //edge of the map
- break
- obscured = FALSE
-
///calculates the fraction of the sunlight that the panel receives
/obj/machinery/power/solar/proc/update_solar_exposure()
needs_to_update_solar_exposure = FALSE
@@ -245,12 +225,9 @@
sunfrac = .
/obj/machinery/power/solar/process()
- if(machine_stat & BROKEN)
- return
- // space vines block out sunlight
- var/obj/structure/spacevine/vine = locate(/obj/structure/spacevine) in loc
- if(istype(vine) && !(/datum/spacevine_mutation/transparency in vine.mutations))
- unset_control()
+ // If the turf is sun blocked directly we have no hope of being able to see the sun so don't bother processing
+ // On the other hand if the station is blocking the sun we might we might be able to see it later, so check for that (in update_turn)
+ if((machine_stat & BROKEN) || isnull(loc) || HAS_TRAIT(loc, TRAIT_TURF_SUN_BLOCKED))
return
if(control && (!powernet || control.powernet != powernet))
@@ -663,6 +640,5 @@
default_raw_text = "
Welcome
At greencorps we love the environment, and space. With this package you are able to help mother nature and produce energy without any usage of fossil fuel or plasma! Singularity energy is dangerous while solar energy is safe, which is why it's better. Now here is how you setup your own solar array.
You can make a solar panel by wrenching the solar assembly onto a cable node. Adding a glass panel, any non reinforced glass will do, will finish the construction of your solar panel. It is that easy!
Now after setting up 19 more of these solar panels you will want to create a solar tracker to keep track of our mother nature's gift, the sun. These are the same steps as before except you insert the tracker equipment circuit into the assembly before performing the final step of adding the glass. You now have a tracker! Now the last step is to add a computer to calculate the sun's movements and to send commands to the solar panels to change direction with the sun. Setting up the solar computer is the same as setting up any computer, so you should have no trouble in doing that. You do need to put a wire node under the computer, and the wire needs to be connected to the tracker.
Congratulations, you should have a working solar array. If you are having trouble, here are some tips. Make sure all solar equipment are on a cable node, even the computer. You can always deconstruct your creations if you make a mistake.
That's all to it, be safe, be green!
"
#undef SOLAR_GEN_RATE
-#undef OCCLUSION_DISTANCE
#undef PANEL_Z_OFFSET
#undef PANEL_EDGE_Z_OFFSET
diff --git a/code/modules/procedural_mapping/mapGenerators/lavaland.dm b/code/modules/procedural_mapping/mapGenerators/lavaland.dm
index 5251f5e8435a..b638e7908dd8 100644
--- a/code/modules/procedural_mapping/mapGenerators/lavaland.dm
+++ b/code/modules/procedural_mapping/mapGenerators/lavaland.dm
@@ -18,9 +18,7 @@
/datum/map_generator_module/splatter_layer/lavaland_tendrils
spawnableTurfs = list()
- spawnableAtoms = list(/obj/structure/spawner/lavaland = 5,
- /obj/structure/spawner/lavaland/legion = 5,
- /obj/structure/spawner/lavaland/goliath = 5)
+ spawnableAtoms = list(/mob/living/basic/mining/tendril = 15)
/datum/map_generator/lavaland/ground_only
modules = list(/datum/map_generator_module/bottom_layer/lavaland_default)
diff --git a/code/modules/projectiles/ammunition/ballistic/pistol.dm b/code/modules/projectiles/ammunition/ballistic/pistol.dm
index e5ddae7c2e91..0805b0060f9d 100644
--- a/code/modules/projectiles/ammunition/ballistic/pistol.dm
+++ b/code/modules/projectiles/ammunition/ballistic/pistol.dm
@@ -22,11 +22,6 @@
desc = "A 10mm incendiary bullet casing."
projectile_type = /obj/projectile/bullet/incendiary/c10mm
-/obj/item/ammo_casing/c10mm/reaper
- name = "10mm reaper bullet casing"
- desc = "A 10mm reaper bullet casing."
- projectile_type = /obj/projectile/bullet/c10mm/reaper
-
// 9mm (Makarov, Stechkin APS)
/obj/item/ammo_casing/c9mm
diff --git a/code/modules/projectiles/ammunition/ballistic/smg.dm b/code/modules/projectiles/ammunition/ballistic/smg.dm
index 7e84bc1a9182..e2be056126b5 100644
--- a/code/modules/projectiles/ammunition/ballistic/smg.dm
+++ b/code/modules/projectiles/ammunition/ballistic/smg.dm
@@ -41,3 +41,8 @@
name = ".45 incendiary bullet casing"
desc = "A .45 bullet casing."
projectile_type = /obj/projectile/bullet/incendiary/c45
+
+/obj/item/ammo_casing/c45/reaper
+ name = ".45 reaper bullet casing"
+ desc = "A .45 reaper bullet casing."
+ projectile_type = /obj/projectile/bullet/c45/reaper
diff --git a/code/modules/projectiles/boxes_magazines/external/pistol.dm b/code/modules/projectiles/boxes_magazines/external/pistol.dm
index 7fbece15863e..27c3d39f6b3c 100644
--- a/code/modules/projectiles/boxes_magazines/external/pistol.dm
+++ b/code/modules/projectiles/boxes_magazines/external/pistol.dm
@@ -92,25 +92,6 @@
MAGAZINE_TYPE_ARMORPIERCE
ammo_type = /obj/item/ammo_casing/c10mm/ap
-// Regal Condor (10mm) //
-
-/obj/item/ammo_box/magazine/r10mm
- name = "regal condor magazine (10mm Reaper)"
- desc = "A very expensive 10mm handgun magazine, suitable for the Regal Condor. Loaded with \"reaper\" rounds, which are dangerously effective against everything."
- icon_state = "r10mm-8"
- base_icon_state = "r10mm"
- ammo_type = /obj/item/ammo_casing/c10mm/reaper
- caliber = CALIBER_10MM
- max_ammo = 8
- multiple_sprites = AMMO_BOX_PER_BULLET
- multiple_sprite_use_base = TRUE
- custom_materials = list(
- /datum/material/iron = SHEET_MATERIAL_AMOUNT * 10,
- /datum/material/gold = SHEET_MATERIAL_AMOUNT * 10,
- /datum/material/silver = SHEET_MATERIAL_AMOUNT * 10,
- /datum/material/plasma = SHEET_MATERIAL_AMOUNT * 10,
- )
-
// M1911 (.45) //
/obj/item/ammo_box/magazine/m45
@@ -134,3 +115,22 @@
caliber = CALIBER_50AE
max_ammo = 7
multiple_sprites = AMMO_BOX_PER_BULLET
+
+// Regal Condor (.45) //
+
+/obj/item/ammo_box/magazine/r45
+ name = "regal condor magazine (.45 Reaper)"
+ desc = "A very expensive .45 handgun magazine, suitable for the Regal Condor. Loaded with \"reaper\" rounds, which are dangerously effective against everything."
+ icon_state = "r45-8"
+ base_icon_state = "r45"
+ ammo_type = /obj/item/ammo_casing/c45/reaper
+ caliber = CALIBER_10MM
+ max_ammo = 8
+ multiple_sprites = AMMO_BOX_PER_BULLET
+ multiple_sprite_use_base = TRUE
+ custom_materials = list(
+ /datum/material/iron = SHEET_MATERIAL_AMOUNT * 10,
+ /datum/material/gold = SHEET_MATERIAL_AMOUNT * 10,
+ /datum/material/silver = SHEET_MATERIAL_AMOUNT * 10,
+ /datum/material/plasma = SHEET_MATERIAL_AMOUNT * 10,
+ )
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index 843f0fcb741c..d0f61730727d 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -124,6 +124,7 @@
add_seclight_point()
add_bayonet_point()
+ add_deep_lore()
RegisterSignal(src, COMSIG_ITEM_IN_UNWRAPPED_TRAITOR_MAIL, PROC_REF(on_mail_unwrap))
/obj/item/gun/Destroy()
@@ -154,6 +155,10 @@
/obj/item/gun/proc/add_bayonet_point()
return
+/// For when you want to add the lore element to a gun, this is the proc to use.
+/obj/item/gun/proc/add_deep_lore()
+ return
+
/obj/item/gun/Exited(atom/movable/gone, direction)
. = ..()
if(gone == pin)
diff --git a/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm b/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
index d1d5a977de80..84b5a2167329 100644
--- a/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
+++ b/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
@@ -117,8 +117,8 @@
/obj/projectile/bullet/arrow/holy/Initialize(mapload)
. = ..()
- //50 damage to revenants
- AddElement(/datum/element/bane, mob_biotypes = MOB_SPIRIT, damage_multiplier = 0, added_damage = 30)
+ // 50 damage to revenants
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_SPIRIT, added_damage = 30)
/// plastic arrows
// completely dogshit quality and they break when they hit something.
@@ -190,5 +190,4 @@
/obj/projectile/bullet/arrow/ashen/Initialize(mapload)
. = ..()
- AddElement(/datum/element/bane, mob_biotypes = MOB_MINING, damage_multiplier = 0, added_damage = 40)
-
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_MINING, added_damage = 40)
diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm
index c28ed3c012dd..08ddb0155ed1 100644
--- a/code/modules/projectiles/guns/ballistic/pistol.dm
+++ b/code/modules/projectiles/guns/ballistic/pistol.dm
@@ -60,6 +60,17 @@
empty_indicator = TRUE
suppressor_x_offset = 12
+/obj/item/gun/ballistic/automatic/pistol/clandestine/add_deep_lore()
+ AddElement(/datum/element/examine_lore, \
+ lore = "Manufactured by Scarborough Arms, the Ansem is regarded as one of the best sidearms on the market. Minimal recoil combined with \
+ high stopping power makes it a favourite amongst soldiers of fortune across known space. \
+ \
+ The ease of concealment due to the weapon's sleek profile has given it a well-earned reputation as the 'smuggler's gun of course'. While \
+ space pirate bands tend to favour the HDS-1 created by Nanotrasen due to their low maintenance cost, the Ansem has seen plenty of use by \
+ mercernaries, hitmen and espionage agents. Dozens of seized Ansem pistols line evidence lockups across the Spinward. Many with extremely \
+ long and bloodied histories from years of service amongst underground criminal elements." \
+ )
+
/obj/item/gun/ballistic/automatic/pistol/clandestine/fisher
name = "\improper Ansem/SC pistol"
desc = "A modified variant of the Ansem, spiritual successor to the Makarov, featuring an integral suppressor and push-button trigger on the grip \
@@ -74,6 +85,8 @@
/obj/item/gun/ballistic/automatic/pistol/clandestine/fisher/Initialize(mapload)
. = ..()
underbarrel = new /obj/item/gun/energy/recharge/fisher(src)
+
+/obj/item/gun/ballistic/automatic/pistol/clandestine/fisher/add_deep_lore()
AddElement(/datum/element/examine_lore, \
lore = "The Ansem/SC is a Scarborough Arms overhaul suite for their own Ansem handgun, designed for special operators who operate operationally, \
especially against people who like using lightbulbs. \
@@ -125,6 +138,18 @@
lock_back_sound = 'sound/items/weapons/gun/pistol/slide_lock.ogg'
bolt_drop_sound = 'sound/items/weapons/gun/pistol/slide_drop.ogg'
+/obj/item/gun/ballistic/automatic/pistol/m1911/add_deep_lore()
+ AddElement(/datum/element/examine_lore, \
+ lore = "The Colt M1911, created by John Moses Browning centuries ago, is the primordial ancestor of modern automatic firearms. \
+ Somehow, to this day, there are still officers who refuse to utilize anything that isn't chambered in 'God's Caliber'. \
+ \
+ Few want to tell any of these people that .45 ACP has struggled to puncture modern ballistic plating for almost five centuries. \
+ \
+ You are VERY confident that this particular example was lathed this decade. Most M1911's seen in the wild today are recreations of the weapon \
+ made with modern alloyed metals and manufactoring techniques, mass produced for the consumer market. This does not stop fraudsters from \
+ claiming that their weapon is a 'family heirloom' that has saved them from various conflicts that they insist they participated in." \
+ )
+
/**
* Weak 1911 for syndicate chimps. It comes in a 4 TC kit.
* 15 damage every.. second? 7 shots to kill. Not fast.
@@ -136,6 +161,8 @@
projectile_wound_bonus = -12
pin = /obj/item/firing_pin/monkey
+/obj/item/gun/ballistic/automatic/pistol/m1911/chimpgun/add_deep_lore()
+ return
/obj/item/gun/ballistic/automatic/pistol/m1911/no_mag
spawnwithmagazine = FALSE
@@ -180,17 +207,30 @@
name = "\improper Regal Condor"
desc = "Unlike the Desert Eagle, this weapon seems to utilize some kind of advanced internal stabilization system to significantly \
reduce felt recoil and increase overall accuracy, at the cost of using a smaller caliber. \
- This does allow it to fire a very quick 2-round burst. Uses 10mm ammo."
+ This allows it to fire a very quick 2-round burst. Chambered in .45."
icon_state = "reagle"
inhand_icon_state = "deagleg"
burst_size = 2
burst_delay = 1
projectile_damage_multiplier = 1.25
- accepted_magazine_type = /obj/item/ammo_box/magazine/r10mm
+ accepted_magazine_type = /obj/item/ammo_box/magazine/r45
actions_types = list(/datum/action/item_action/toggle_firemode)
obj_flags = UNIQUE_RENAME // if you did the sidequest, you get the customization
custom_materials = list(/datum/material/gold = SHEET_MATERIAL_AMOUNT * 30, /datum/material/silver = SHEET_MATERIAL_AMOUNT * 25, /datum/material/iron = SHEET_MATERIAL_AMOUNT * 11.5, /datum/material/telecrystal = SHEET_MATERIAL_AMOUNT * 4)
+/obj/item/gun/ballistic/automatic/pistol/deagle/regal/add_deep_lore()
+ AddElement(/datum/element/examine_lore, \
+ lore = "One of the Culling Arms of the Tiger Cooperative, a collection of relic weapons said to have come to Martinson in a frenzied vision from \
+ God to enact holy retribution upon the enemies of the faith. Almost all examples of these guns are replicas of the original pattern. This one is no \
+ different. However, it carries some of the gravitus of the original firearm. And, some say, the holy might that it wielded against the enemies of the cult. \
+ \
+ The fact that you are looking at this relic weapon means one of two things. \
+ \
+ A) A Tiger Cooperative evangelist has died during or before they could commit a bloody massacre in the name of God. Or \
+ \
+ B) You are that evangelist, and will soon be adding another chapter to this gun's dark history." \
+ )
+
/obj/item/gun/ballistic/automatic/pistol/aps
name = "\improper Stechkin APS machine pistol"
desc = "An old Soviet machine pistol. It fires quickly, but kicks like a mule. Uses 9mm ammo. Has a threaded barrel for suppressors."
@@ -225,10 +265,9 @@
#define DOORHICKEY_GUN_MIN_DAMAGE 70
#define DOORHICKEY_GUN_MAX_DAMAGE 140
-/obj/item/gun/ballistic/automatic/pistol/doorhickey
+/obj/item/gun/ballistic/automatic/pistol/doohickey
name = "\improper Liberator"
- desc = "A poorly made 3D printed \"gun\", only capable of firing a single shot. Well-known throughout the Spinward Sector \
- after an incident where 3 assistants were killed by shrapnel from such a device exploding while attempting to shoot a mouse."
+ desc = "A poorly made 3D printed \"gun\", only capable of firing a single shot."
icon_state = "doorhickey"
custom_materials = list(/datum/material/plastic = SHEET_MATERIAL_AMOUNT * 2)
bolt_type = BOLT_TYPE_NO_BOLT
@@ -243,7 +282,7 @@
projectile_damage_multiplier = 0.5
spread = 10
-/obj/item/gun/ballistic/automatic/pistol/doorhickey/unload_ammo(mob/living/user, forced = FALSE)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/unload_ammo(mob/living/user, forced = FALSE)
if (forced)
return ..()
@@ -254,7 +293,7 @@
return
. = ..()
-/obj/item/gun/ballistic/automatic/pistol/doorhickey/load_gun(obj/item/ammo, mob/living/user)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/load_gun(obj/item/ammo, mob/living/user)
. = ..()
if (!.)
return
@@ -268,7 +307,7 @@
unload_ammo(user, forced = TRUE)
return FALSE
-/obj/item/gun/ballistic/automatic/pistol/doorhickey/process_fire(atom/target, mob/living/user, message, params, zone_override, bonus_spread)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/process_fire(atom/target, mob/living/user, message, params, zone_override, bonus_spread)
var/dmg_multiplier = 1
if (get_dist(target, user) <= 1)
@@ -287,7 +326,7 @@
. = ..()
projectile_damage_multiplier /= dmg_multiplier
-/obj/item/gun/ballistic/automatic/pistol/doorhickey/shoot_live_shot(mob/living/user, pointblank = FALSE, atom/pbtarget = null, message = TRUE)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/shoot_live_shot(mob/living/user, pointblank = FALSE, atom/pbtarget = null, message = TRUE)
. = ..()
if (!.)
return
@@ -326,6 +365,26 @@
new /obj/effect/decal/cleanable/plastic(get_turf(src))
take_damage(damage_to_take)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/add_deep_lore()
+ AddElement(/datum/element/examine_lore, \
+ lore = "The Liberator pattern, according to digital historians, was first posted to a fringe imageboard on the NTNet. The post included a now dead \
+ link to a defuncting hosting service through which board members were encouraged to download and 'admire' the design. The thread's author began their \
+ post by describing their extreme vitriolic hatred of so-called 'moon men' amongst the Nanotrasen 'elites'. And that, through 'the sauce', they had \
+ been shown a means to 'end the oppression of Luna's Tormentors'. They called it the 'Liberator' as a result. \
+ \
+ Response to this thread began as skeptism amongst posters regarding the 3D pattern. Many highlighted the dubious functionality, let alone safe operation \
+ of a gun created using cheap plastic and minimal machined parts firing a high pressure round through an unrifled barrel. But the thread suddenly began to \
+ gain traction immediately following a particular news article published by the Spinward Dove, a respected editorial in the Spinward Sector. The author of this \
+ article recounts that, during a particularly brisk day in New Moscow, they observed three off-duty Nanotrasen assistants attempting to use what the author \
+ described as a 'bizarre plastic doohickey' to shoot a mouse at point blank. The horrific aftermath, and images of the rattled but otherwise unscathed mouse, lead many \
+ to try and find from where these now crippled assistants had come across such an absurd device. Word spread of the post, and its author's continuously nonsensical \
+ rants regarding their hated foe in other areas of the imageboard. \
+ \
+ The author of the thread returned immediately following the news, claiming that those who frequented the imageboard had 'stolen' their 'dreams of freedom', and \
+ that clearly the moon men were attempting to discredit their invention through this publicity stunt. A slur-laiden rant from the thread author followed this declaration, \
+ leading to the the moderators of the imageboard to step in and close the thread for good." \
+ )
+
/obj/item/disk/design_disk/liberator
name = "illegal 3D printer design disk"
diff --git a/code/modules/projectiles/guns/energy.dm b/code/modules/projectiles/guns/energy.dm
index dccee957e4c9..77d61147b676 100644
--- a/code/modules/projectiles/guns/energy.dm
+++ b/code/modules/projectiles/guns/energy.dm
@@ -208,7 +208,6 @@
var/obj/item/ammo_casing/energy/shot = ammo_type[select] //Necessary to find cost of shot
if(robot.cell.use(shot.e_cost)) //Take power from the borg...
cell.give(shot.e_cost) //... to recharge the shot
- return ..()
if(!chambered)
var/obj/item/ammo_casing/energy/AC = ammo_type[select]
diff --git a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
index 64424d2a644d..0363850860f3 100644
--- a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
+++ b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
@@ -81,7 +81,8 @@
to_chat(user, span_notice("You pry all the modifications out."))
I.play_tool_sound(src, 100)
for(var/obj/item/borg/upgrade/modkit/modkit_upgrade as anything in modkits)
- modkit_upgrade.forceMove(drop_location()) //uninstallation handled in Exited(), or /mob/living/silicon/robot/remove_from_upgrades() for borgs
+ if (modkit_upgrade.removable)
+ modkit_upgrade.forceMove(drop_location()) //uninstallation handled in Exited(), or /mob/living/silicon/robot/remove_from_upgrades() for borgs
else
to_chat(user, span_notice("There are no modifications currently installed."))
@@ -99,7 +100,9 @@
var/list/display_names = list()
var/list/items = list()
for(var/modkits_length in 1 to length(modkits))
- var/obj/item/thing = modkits[modkits_length]
+ var/obj/item/borg/upgrade/modkit/thing = modkits[modkits_length]
+ if (!thing.removable)
+ continue
display_names["[thing.name] ([modkits_length])"] = REF(thing)
var/image/item_image = image(icon = thing.icon, icon_state = thing.icon_state)
if(length(thing.overlays))
@@ -157,6 +160,16 @@
for(var/obj/item/borg/upgrade/modkit/modkit_upgrade as anything in modkits)
modkit_upgrade.modify_projectile(kinetic_projectile)
+/obj/item/gun/energy/recharge/kinetic_accelerator/bdm
+ name = "infernal proto-kinetic accelerator"
+ icon_state = "kineticgun_evil"
+ inhand_icon_state = "kineticgun_evil"
+
+/obj/item/gun/energy/recharge/kinetic_accelerator/bdm/Initialize(mapload)
+ . = ..()
+ var/obj/item/borg/upgrade/modkit/cooldown/repeater/bdm/repeater_mod = new()
+ repeater_mod.install(src)
+
/obj/item/gun/energy/recharge/kinetic_accelerator/cyborg
icon_state = "kineticgun_b"
holds_charge = TRUE
@@ -300,6 +313,8 @@
var/modifier = 1 //For use in any mod kit that has numerical modifiers
var/minebot_upgrade = TRUE
var/minebot_exclusive = FALSE
+ /// Can it be removed?
+ var/removable = TRUE
/obj/item/borg/upgrade/modkit/examine(mob/user)
. = ..()
@@ -321,10 +336,12 @@
. = TRUE
if(minebot_upgrade)
if(minebot_exclusive && !istype(KA.loc, /mob/living/basic/mining_drone))
- to_chat(user, span_notice("The modkit you're trying to install is only rated for minebot use."))
+ if (user)
+ to_chat(user, span_notice("The modkit you're trying to install is only rated for minebot use."))
return FALSE
else if(istype(KA.loc, /mob/living/basic/mining_drone))
- to_chat(user, span_notice("The modkit you're trying to install is not rated for minebot use."))
+ if (user)
+ to_chat(user, span_notice("The modkit you're trying to install is not rated for minebot use."))
return FALSE
var/type_to_limit = denied_type
@@ -338,21 +355,24 @@
if(istype(modkit_upgrade, type_to_limit))
number_of_denied++
if(maximum_of_type && number_of_denied >= maximum_of_type || !maximum_of_type && number_of_denied) //if we denied a type, or we have a maximum to reach, break
- . = FALSE
- break
-
- if(KA.get_remaining_mod_capacity() >= cost)
- if(.)
- if(transfer_to_loc && !user.transferItemToLoc(src, KA))
- return
- to_chat(user, span_notice("You install the modkit."))
- playsound(loc, 'sound/items/tools/screwdriver.ogg', 100, TRUE)
- KA.modkits |= src
- else
- to_chat(user, span_notice("The modkit you're trying to install would conflict with an already installed modkit. Remove existing modkits first."))
- else
+ if (user)
+ to_chat(user, span_notice("The modkit you're trying to install would conflict with an already installed modkit. Remove existing modkits first."))
+ return FALSE
+
+ if(KA.get_remaining_mod_capacity() < cost)
to_chat(user, span_notice("You don't have room([KA.get_remaining_mod_capacity()]% remaining, [cost]% needed) to install this modkit. Use a crowbar or right click with an empty hand to remove existing modkits."))
- . = FALSE
+ return FALSE
+
+ if(transfer_to_loc)
+ if (user && !user.transferItemToLoc(src, KA))
+ return FALSE
+ else if (!user)
+ forceMove(KA)
+
+ if (user)
+ to_chat(user, span_notice("You install the modkit."))
+ playsound(loc, 'sound/items/tools/screwdriver.ogg', 100, TRUE)
+ KA.modkits |= src
/obj/item/borg/upgrade/modkit/deactivate(mob/living/silicon/robot/R, user = usr)
. = ..()
@@ -555,6 +575,12 @@
KA.cell.use(KA.cell.charge)
KA.attempt_reload(KA.recharge_time * 0.25) //If you hit, the cooldown drops to 0.75 seconds.
+/obj/item/borg/upgrade/modkit/cooldown/repeater/bdm
+ name = "infernal repeater"
+ removable = FALSE
+ cost = 30
+ modifier = -10
+
/obj/item/borg/upgrade/modkit/lifesteal
name = "lifesteal crystal"
desc = "Causes kinetic accelerator shots to slightly heal the firer on striking a living target."
diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm
index b48515ae9589..f9ce1b458a9f 100644
--- a/code/modules/projectiles/guns/energy/laser.dm
+++ b/code/modules/projectiles/guns/energy/laser.dm
@@ -11,8 +11,6 @@
/obj/item/gun/energy/laser/Initialize(mapload)
. = ..()
- add_deep_lore()
-
// Only regular lasguns can be slapcrafted
if(type != /obj/item/gun/energy/laser)
return
@@ -307,7 +305,7 @@
// Laser Gun
-/obj/item/gun/energy/laser/proc/add_deep_lore()
+/obj/item/gun/energy/laser/add_deep_lore()
AddElement(/datum/element/examine_lore, \
lore_hint = span_notice("You can [EXAMINE_HINT("look closer")] to learn a little more about [src]."), \
lore = "The NT Type 5 Heat Delivery System (sometimes referred to as the HDS-5 in promotional material) is what truly put Nanotrasen \
diff --git a/code/modules/projectiles/guns/magic/staff.dm b/code/modules/projectiles/guns/magic/staff.dm
index 308a6c87884a..3615616f4f39 100644
--- a/code/modules/projectiles/guns/magic/staff.dm
+++ b/code/modules/projectiles/guns/magic/staff.dm
@@ -213,7 +213,7 @@
/obj/projectile/magic/spellblade,
/obj/projectile/magic/swap,
/obj/projectile/magic/teleport,
- /obj/projectile/magic/tentacle,
+ /obj/projectile/magic/tentacle_staff,
/obj/projectile/magic/wipe,
/obj/projectile/temp/chill
)
diff --git a/code/modules/projectiles/guns/magic/wand.dm b/code/modules/projectiles/guns/magic/wand.dm
index e6280bde5f34..c2334b5c4c53 100644
--- a/code/modules/projectiles/guns/magic/wand.dm
+++ b/code/modules/projectiles/guns/magic/wand.dm
@@ -358,16 +358,12 @@
// Animating a nothing wand makes it into an animating wand (and also animates it)
/obj/item/gun/magic/wand/nothing/animate_atom_living(mob/living/owner)
- var/obj/item/gun/magic/wand/animate/animated_wand = new()
+ var/obj/item/gun/magic/wand/animate/animated_wand = new(loc)
animated_wand.charges = charges
animated_wand.name = name + "?"
- var/mob/living/basic/mimic/copy/ranged/living_wand = new(drop_location(), animated_wand, owner, TRUE) // It's already got eyes
- QDEL_NULL(living_wand.ai_controller)
- living_wand.ai_controller = new /datum/ai_controller/basic_controller/mimic_copy/gun/animator(living_wand)
-
qdel(src)
- return living_wand
+ return animated_wand.animate_atom_living(owner)
/// Also wand of doing fuck all
/obj/item/gun/magic/wand/nothing/fake_resurrection
diff --git a/code/modules/projectiles/guns/magic/wands/wand_animate.dm b/code/modules/projectiles/guns/magic/wands/wand_animate.dm
index 367962180ce4..04d4e8b0d616 100644
--- a/code/modules/projectiles/guns/magic/wands/wand_animate.dm
+++ b/code/modules/projectiles/guns/magic/wands/wand_animate.dm
@@ -50,7 +50,7 @@
return MANUAL_SUICIDE
/obj/item/gun/magic/wand/animate/animate_atom_living(mob/living/owner)
- var/mob/living/basic/mimic/copy/ranged/living_wand = new(drop_location(), src, owner, TRUE) // It's already got eyes
+ var/mob/living/basic/mimic/copy/ranged/living_wand = new(drop_location(), src, owner, FALSE, TRUE) // It's already got eyes
QDEL_NULL(living_wand.ai_controller)
living_wand.ai_controller = new /datum/ai_controller/basic_controller/mimic_copy/gun/animator(living_wand)
return living_wand
diff --git a/code/modules/projectiles/guns/magic/wands/wand_tentacle.dm b/code/modules/projectiles/guns/magic/wands/wand_tentacle.dm
index e01aa152f206..c4be4e60a8dd 100644
--- a/code/modules/projectiles/guns/magic/wands/wand_tentacle.dm
+++ b/code/modules/projectiles/guns/magic/wands/wand_tentacle.dm
@@ -7,7 +7,7 @@
name = "restraining rod"
desc = "This wriggling wand binds its victims in a place for a time, although it doesn't stop them from shooting back."
school = SCHOOL_CONJURATION
- ammo_type = /obj/item/ammo_casing/magic/tentacle
+ ammo_type = /obj/item/ammo_casing/magic/tentacle_staff
icon_state = "tentawand"
base_icon_state = "tentawand"
fire_sound = 'sound/effects/splat.ogg'
@@ -24,15 +24,15 @@
. = ..()
return user.has_status_effect(/datum/status_effect/incapacitating/immobilized/wizard_tentacle/suicide) ? MANUAL_SUICIDE : SHAME
-/obj/item/ammo_casing/magic/tentacle
- projectile_type = /obj/projectile/magic/tentacle
+/obj/item/ammo_casing/magic/tentacle_staff
+ projectile_type = /obj/projectile/magic/tentacle_staff
/// Grabs the target for a while in an unwanted hug
-/obj/projectile/magic/tentacle
+/obj/projectile/magic/tentacle_staff
name = "bolt of binding"
icon_state = "tentacle_end"
-/obj/projectile/magic/tentacle/on_hit(mob/living/target, blocked = 0, pierce_hit)
+/obj/projectile/magic/tentacle_staff/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
if (. == BULLET_ACT_BLOCK || !istype(target) || blocked >= 100)
return
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 3c695a10b738..d7aa59766b3c 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -1251,8 +1251,8 @@
free_hitscan_forceMove = TRUE
forceMove(source_loc)
starting = source_loc
- pixel_x = source.pixel_x
- pixel_y = source.pixel_y
+ pixel_x = source.pixel_x - source.base_pixel_x
+ pixel_y = source.pixel_y - source.base_pixel_y
original = target
// Trim off excess pixel_x/y by converting them into turf offset
diff --git a/code/modules/projectiles/projectile/beams.dm b/code/modules/projectiles/projectile/beams.dm
index a694f7a9163e..03ac61f8adb4 100644
--- a/code/modules/projectiles/projectile/beams.dm
+++ b/code/modules/projectiles/projectile/beams.dm
@@ -172,6 +172,7 @@
name = "disabler beam"
icon_state = "omnilaser"
damage = 30
+ speed = 1.6
damage_type = STAMINA
armor_flag = ENERGY
hitsound = 'sound/items/weapons/sear_disabler.ogg'
diff --git a/code/modules/projectiles/projectile/bullets/cannonball.dm b/code/modules/projectiles/projectile/bullets/cannonball.dm
index bfc781e6fac3..41277bc9559f 100644
--- a/code/modules/projectiles/projectile/bullets/cannonball.dm
+++ b/code/modules/projectiles/projectile/bullets/cannonball.dm
@@ -123,7 +123,7 @@
/obj/projectile/bullet/ballista_spear/dragonator/Initialize(mapload)
. = ..()
- AddElement(/datum/element/bane, mob_biotypes = MOB_MINING, damage_multiplier = 2)
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_MINING, damage_multiplier = 3)
/obj/projectile/bullet/ballista_spear/dragonator/attach_spear(obj/item/spear)
AddComponent(/datum/component/projectile_instance_drop, spear)
diff --git a/code/modules/projectiles/projectile/bullets/pistol.dm b/code/modules/projectiles/projectile/bullets/pistol.dm
index 9c799fb9c0a5..cb852ad446db 100644
--- a/code/modules/projectiles/projectile/bullets/pistol.dm
+++ b/code/modules/projectiles/projectile/bullets/pistol.dm
@@ -53,26 +53,6 @@
damage = 20
fire_stacks = 3
-/obj/projectile/bullet/c10mm/reaper
- name = "10mm reaper pellet"
- icon_state = null
- damage = 50
- armour_penetration = 40
- tracer_type = /obj/effect/projectile/tracer/sniper
- impact_type = /obj/effect/projectile/impact/sniper
- muzzle_type = /obj/effect/projectile/muzzle/sniper
- hitscan = TRUE
- impact_effect_type = null
- hitscan_light_intensity = 3
- hitscan_light_range = 0.75
- hitscan_light_color_override = LIGHT_COLOR_DIM_YELLOW
- muzzle_flash_intensity = 5
- muzzle_flash_range = 1
- muzzle_flash_color_override = LIGHT_COLOR_DIM_YELLOW
- impact_light_intensity = 5
- impact_light_range = 1
- impact_light_color_override = LIGHT_COLOR_DIM_YELLOW
-
// .160 Smart
/obj/projectile/bullet/c160smart
diff --git a/code/modules/projectiles/projectile/bullets/smg.dm b/code/modules/projectiles/projectile/bullets/smg.dm
index 38280b6f91e9..2c64bac2878c 100644
--- a/code/modules/projectiles/projectile/bullets/smg.dm
+++ b/code/modules/projectiles/projectile/bullets/smg.dm
@@ -20,6 +20,26 @@
damage = 15
fire_stacks = 2
+/obj/projectile/bullet/c45/reaper
+ name = ".45 reaper pellet"
+ icon_state = null
+ damage = 50
+ armour_penetration = 40
+ tracer_type = /obj/effect/projectile/tracer/sniper
+ impact_type = /obj/effect/projectile/impact/sniper
+ muzzle_type = /obj/effect/projectile/muzzle/sniper
+ hitscan = TRUE
+ impact_effect_type = null
+ hitscan_light_intensity = 3
+ hitscan_light_range = 0.75
+ hitscan_light_color_override = LIGHT_COLOR_DIM_YELLOW
+ muzzle_flash_intensity = 5
+ muzzle_flash_range = 1
+ muzzle_flash_color_override = LIGHT_COLOR_DIM_YELLOW
+ impact_light_intensity = 5
+ impact_light_range = 1
+ impact_light_color_override = LIGHT_COLOR_DIM_YELLOW
+
// 4.6x30mm (Autorifles)
/obj/projectile/bullet/c46x30mm
diff --git a/code/modules/reagents/chemistry/holder/mob_life.dm b/code/modules/reagents/chemistry/holder/mob_life.dm
index 693dc8bb8852..b90d825c6aa6 100644
--- a/code/modules/reagents/chemistry/holder/mob_life.dm
+++ b/code/modules/reagents/chemistry/holder/mob_life.dm
@@ -106,7 +106,7 @@
if(tick_return & COMSIG_MOB_STOP_REAGENT_TICK)
return FALSE
- if(liverless && !reagent.self_consuming) //need to be metabolized
+ if(liverless && !reagent.self_consuming) //need to be6 metabolized
return FALSE
var/need_mob_update = FALSE
diff --git a/code/modules/reagents/chemistry/holder/properties.dm b/code/modules/reagents/chemistry/holder/properties.dm
index f74b5893bb55..bfdff9f0ad06 100644
--- a/code/modules/reagents/chemistry/holder/properties.dm
+++ b/code/modules/reagents/chemistry/holder/properties.dm
@@ -22,7 +22,7 @@
if(cached_reagent.type != reagent)
continue
if(REAGENT_PARENT_TYPE) //to simulate typesof() which returns the type and then child types
- if(cached_reagent.type != reagent && type2parent(cached_reagent.type) != reagent)
+ if(cached_reagent.type != reagent && cached_reagent::parent_type != reagent)
continue
else
if(!istype(cached_reagent, reagent))
diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm
index fd115c1114a2..81e2a7ae9e62 100644
--- a/code/modules/reagents/chemistry/reagents.dm
+++ b/code/modules/reagents/chemistry/reagents.dm
@@ -232,12 +232,12 @@
/datum/reagent/proc/on_mob_metabolize(mob/living/affected_mob)
SHOULD_CALL_PARENT(TRUE)
if(metabolized_traits)
- affected_mob.add_traits(metabolized_traits, "metabolize:[type]")
+ affected_mob.add_traits(metabolized_traits, METABOLIZATION_TRAIT(type))
/// Called when this reagent stops being metabolized by a liver
/datum/reagent/proc/on_mob_end_metabolize(mob/living/affected_mob, metabolization_ratio)
SHOULD_CALL_PARENT(TRUE)
- REMOVE_TRAITS_IN(affected_mob, "metabolize:[type]")
+ REMOVE_TRAITS_IN(affected_mob, METABOLIZATION_TRAIT(type))
/**
* Called when a reagent is inside of a mob when they are dead if the reagent has the REAGENT_DEAD_PROCESS flag
diff --git a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
index 29291d4f1d28..4f755474024d 100644
--- a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
@@ -3260,7 +3260,7 @@
/datum/reagent/consumable/ethanol/aperitivo/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired) //This and some of the cocktails it gets mixed into stimulate the apetite, as an aperitivo should
. = ..()
- drinker.adjust_nutrition(-5 * REM * seconds_per_tick)
+ drinker.adjust_nutrition(-1 * REM * seconds_per_tick)
drinker.overeatduration = 0
/datum/reagent/consumable/ethanol/herbal_liqueur
@@ -3392,7 +3392,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS
-/datum/reagent/consumable/ethanol/negroni //Aperitif that increases hunger
+/datum/reagent/consumable/ethanol/negroni //Aperitif that supresses overeating
name = "Negroni"
description = "An iconic Italian aperitif, its simple intensity crowns it as perhaps the ultimate bitter cocktail. Supposedly it was named after an Italian count who wanted a stronger version of a spritz and asked his bartender to replace soda with gin."
boozepwr = 50
@@ -3405,8 +3405,7 @@
/datum/reagent/consumable/ethanol/negroni/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
. = ..()
- drinker.adjust_nutrition(-3 * REM * seconds_per_tick)
- drinker.overeatduration = 0
+ drinker.overeatduration -= 20
/datum/reagent/consumable/ethanol/nuclear_daiquiri
name = "Nuclear daiquiri"
@@ -3471,7 +3470,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
glass_price = DRINK_PRICE_HIGH
-/datum/reagent/consumable/ethanol/spritz //Aperitif that increases hunger
+/datum/reagent/consumable/ethanol/spritz //Aperitif that supresses overeating
name = "Spritz" // If someone wants to add an elderflower spritz or something else like that, just rename this to spritz al bitter or whatever
description = "This bittersweet and refreshing aperitif brings to mind the beautiful summer sunsets of venice."
boozepwr = 20
@@ -3484,8 +3483,7 @@
/datum/reagent/consumable/ethanol/spritz/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
. = ..()
- drinker.adjust_nutrition(-5 * REM * seconds_per_tick)
- drinker.overeatduration = 0
+ drinker.overeatduration -= 20
/datum/reagent/consumable/ethanol/vieux_carre
name = "Vieux Carré"
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index dabf218bc2b6..a6575cdab4b1 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -726,7 +726,7 @@
var/obj/effect/decal/cleanable/food/flour/flour_decal = exposed_turf.spawn_unique_cleanable(/obj/effect/decal/cleanable/food/flour)
if(flour_decal)
- flour_decal.reagents.add_reagent(/datum/reagent/consumable/flour, reac_volume)
+ flour_decal.init_reagents(/datum/reagent/consumable/flour, reac_volume)
/datum/reagent/consumable/cherryjelly
name = "Cherry Jelly"
@@ -1358,3 +1358,13 @@
taste_description = "metallic salt"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS
+
+/datum/reagent/consumable/gizmo_goop
+ name = "Gizmo Goop"
+ description = "A thick, grey goup that is supposedly 'nutritious'."
+ color = "#707070"
+ taste_description = "goop"
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ nutriment_factor = 0.5
+ randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS
+
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index b80f70594d35..89e74b81a32e 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -154,9 +154,11 @@
/datum/reagent/medicine/cryoxadone/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio)
. = ..()
metabolization_rate = REAGENTS_METABOLISM * (0.00001 * (affected_mob.bodytemperature ** 2) + 0.5)
- if(affected_mob.bodytemperature >= T0C || !HAS_TRAIT(affected_mob, TRAIT_KNOCKEDOUT))
+ if(affected_mob.bodytemperature >= T0C)
return
var/power = -0.00003 * (affected_mob.bodytemperature ** 2) + 3
+ if(HAS_TRAIT(affected_mob, TRAIT_KNOCKEDOUT)) //Significantly more effective when unconscious
+ power *= 2
var/need_mob_update
need_mob_update = affected_mob.adjust_oxy_loss(-1.5 * power * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
need_mob_update += affected_mob.adjust_brute_loss(-0.5 * power * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
@@ -1309,7 +1311,7 @@
if (ismonkey(human_mob))
if (!HAS_TRAIT(human_mob, TRAIT_BORN_MONKEY))
//This is the only time mutadone should remove monkeyism
- human_mob.dna.remove_mutation(/datum/mutation/race, list(MUTATION_SOURCE_ACTIVATED, MUTATION_SOURCE_MUTATOR))
+ human_mob.dna.remove_mutation(/datum/mutation/race, GLOB.standard_mutation_sources)
else if (HAS_TRAIT(human_mob, TRAIT_BORN_MONKEY))
human_mob.monkeyize()
@@ -1853,7 +1855,7 @@
/datum/reagent/medicine/granibitaluri/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio)
. = ..()
- var/healamount = max(0.5 - round(0.01 * (affected_mob.get_brute_loss() + affected_mob.get_fire_loss()), 0.1), 0) //base of 0.5 healing per cycle and loses 0.1 healing for every 10 combined brute/burn damage you have
+ var/healamount = max(1.5 - round(0.01 * (affected_mob.get_brute_loss() + affected_mob.get_fire_loss()), 0.2), 0.5) //base of 1.5 healing per cycle and loses 0.2 healing for every 10 combined brute/burn damage you have
var/need_mob_update
need_mob_update = affected_mob.adjust_brute_loss(-1 * healamount * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
need_mob_update += affected_mob.adjust_fire_loss(-1 * healamount * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 59edbd997d06..92f58ef213ee 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -1542,7 +1542,7 @@
color = "#535E66" // rgb: 83, 94, 102
taste_description = "sludge"
penetrates_skin = NONE
- randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS
+ randomized_spawns = REAGENT_SPAWN_MAINTENANCE_PILL
/datum/reagent/cyborg_mutation_nanomachines/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message = TRUE, touch_protection = 0)
. = ..()
@@ -1558,7 +1558,7 @@
color = "#535E66" // rgb: 83, 94, 102
taste_description = "sludge"
penetrates_skin = NONE
- randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS
+ randomized_spawns = REAGENT_SPAWN_MAINTENANCE_PILL
/datum/reagent/xenomicrobes/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message = TRUE, touch_protection = 0)
. = ..()
@@ -1872,12 +1872,15 @@
color = COLOR_BLACK // RBG: 0, 0, 0
taste_description = "plant food"
ph = 3
+ /// The chance of toxin damage for a mob (heals toxins for MOB_PLANT biotype)
var/tox_prob = 0
/datum/reagent/plantnutriment/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio)
. = ..()
+
if(SPT_PROB(tox_prob, seconds_per_tick))
- if(affected_mob.adjust_tox_loss(1 * metabolization_ratio, updating_health = FALSE, required_biotype = affected_biotype))
+ var/tox_modifier = (affected_mob.mob_biotypes & MOB_PLANT) ? -1 : 1
+ if(affected_mob.adjust_tox_loss(tox_modifier * metabolization_ratio, updating_health = FALSE, required_biotype = affected_biotype))
return UPDATE_MOB_HEALTH
/datum/reagent/plantnutriment/eznutriment
@@ -1904,7 +1907,6 @@
randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS
/datum/reagent/plantnutriment/left4zednutriment/on_hydroponics_apply(obj/machinery/hydroponics/mytray, mob/user)
-
mytray.adjust_plant_health(round(volume * 0.1))
mytray.myseed?.adjust_instability(round(volume * 0.2))
@@ -2840,10 +2842,27 @@
/datum/reagent/pax/peaceborg
name = "Synthpax"
- description = "A colorless liquid that suppresses violence in its subjects. Cheaper to synthesize than normal Pax, but wears off faster."
+ description = "A colorless liquid that suppresses violence in its subjects. Cheaper to synthesize than normal Pax, but wears off faster \
+ and cannot overpower any retaliatory responses triggered by physical trauma."
metabolization_rate = 1.5 * REAGENTS_METABOLISM
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS
+ metabolized_traits = null
+
+/datum/reagent/pax/peaceborg/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
+ if(!HAS_TRAIT(affected_mob, TRAIT_SYNTHPAX_IMMUNE))
+ ADD_TRAIT(affected_mob, TRAIT_PACIFISM, METABOLIZATION_TRAIT(type))
+ RegisterSignal(affected_mob, COMSIG_MOB_AFTER_APPLY_DAMAGE, PROC_REF(on_metabolizer_damaged))
+
+/datum/reagent/pax/peaceborg/on_mob_end_metabolize(mob/living/affected_mob, metabolization_ratio)
+ . = ..()
+ UnregisterSignal(affected_mob, COMSIG_MOB_AFTER_APPLY_DAMAGE)
+ REMOVE_TRAIT(affected_mob, TRAIT_PACIFISM, METABOLIZATION_TRAIT(type))
+
+/datum/reagent/pax/peaceborg/proc/on_metabolizer_damaged(mob/living/source, amount)
+ SIGNAL_HANDLER
+ source.adjust_timed_status_effect(amount * 1 SECONDS, /datum/status_effect/synthpax_immunity, max_duration = 5 SECONDS)
/datum/reagent/peaceborg/confuse
name = "Dizzying Solution"
diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
index ab6c99782870..9e73a0be3613 100644
--- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
@@ -251,6 +251,54 @@
SIGNAL_HANDLER
return COMSIG_CARBON_BLOCK_BREATH
+/datum/reagent/toxin/carnivorousblood
+ name = "Carnivorous Blood"
+ description = "An Interdyne-developed biological agent that consumes any blood similar to the blood it was trained on."
+ color ="#C80000"
+ toxpwr = 0
+ taste_description = "carbonated blood"
+ ph = 7.4
+ metabolization_rate = 1.5 * REAGENTS_METABOLISM
+ chemical_flags = REAGENT_DEAD_PROCESS
+ self_consuming = TRUE
+ // Will hold up to three DNA unique enzymes
+ data = list()
+
+/datum/reagent/toxin/carnivorousblood/expose_mob(mob/living/exposed_mob, methods, reac_volume, show_message, touch_protection)
+ . = ..()
+ if(methods & INHALE)
+ data = list() // Being vaporized is generally undesirable for living creatures
+
+/datum/reagent/toxin/carnivorousblood/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio)
+ . = ..()
+ if(!CAN_HAVE_BLOOD(affected_mob) || affected_mob.blood_volume == 0)
+ affected_mob.reagents.remove_reagent(/datum/reagent/toxin/carnivorousblood, volume)
+ return
+ var/multiplier = 1
+ if(affected_mob.stat == DEAD)
+ multiplier *= 2
+ if(affected_mob.dna.unique_enzymes in data)
+ multiplier *= 3
+ affected_mob.adjust_blood_volume(-2 * multiplier * seconds_per_tick)
+ if(SPT_PROB(10, seconds_per_tick))
+ to_chat(affected_mob, span_danger("Your blood is writhing in your veins!"))
+ if(SPT_PROB(25, seconds_per_tick))
+ affected_mob.emote("scream")
+ return UPDATE_MOB_HEALTH
+
+/datum/reagent/toxin/carnivorousblood/on_merge(list/mix_data, amount)
+ . = ..()
+ feed_dna_list(mix_data)
+
+/// Given a list of DNA keys, adds new keys up to the limit of three distinct sequences.
+/datum/reagent/toxin/carnivorousblood/proc/feed_dna_list(list/adding_list)
+ for(var/dna in adding_list)
+ if(data.len >= 3)
+ return
+ if(dna in data)
+ continue
+ data += dna
+
/datum/reagent/toxin/slimejelly
name = "Slime Jelly"
description = "A gooey semi-liquid produced from one of the deadliest lifeforms in existence. SO REAL."
@@ -1592,7 +1640,7 @@
. = ..()
var/merged_total = amount + volume
if(merged_total >= CRITICAL_CAPACITY)
- spew_waste(round(volume / WASTE_REACTION_THRESHOLD*2)) //Sure as HELL can't store it.
+ spew_waste(round(volume / WASTE_REACTION_THRESHOLD * 2)) //Sure as HELL can't store it.
var/atom/container = holder.my_atom
var/damage_mult = 1
if(ismachinery(container))
diff --git a/code/modules/reagents/chemistry/recipes.dm b/code/modules/reagents/chemistry/recipes.dm
index e48eb60025fd..ee607ca1dc80 100644
--- a/code/modules/reagents/chemistry/recipes.dm
+++ b/code/modules/reagents/chemistry/recipes.dm
@@ -61,7 +61,7 @@
///REACTION PROCS
/**
- * Checks if this reaction can occur. Only is ran if required_other is set to TRUE.
+ * Checks if this reaction can occur.
*/
/datum/chemical_reaction/proc/pre_reaction_other_checks(datum/reagents/holder)
return TRUE
diff --git a/code/modules/reagents/chemistry/recipes/cat2_medicines.dm b/code/modules/reagents/chemistry/recipes/cat2_medicines.dm
index 8600f1ecdb74..64b88bfc29aa 100644
--- a/code/modules/reagents/chemistry/recipes/cat2_medicines.dm
+++ b/code/modules/reagents/chemistry/recipes/cat2_medicines.dm
@@ -190,7 +190,7 @@
else
reaction.delta_t = delta_t/10 //slow without oxygen
-/datum/chemical_reaction/medicine/convermol/overheated(datum/reagents/holder, datum/equilibrium/equilibrium, impure = FALSE)
+/datum/chemical_reaction/medicine/convermol/overheated(datum/reagents/holder, datum/equilibrium/equilibrium, step_volume_added, impure = FALSE)
var/range = impure ? 4 : 3
if(holder.has_reagent(/datum/reagent/oxygen))
explode_shockwave(holder, equilibrium, range) //damage 5
@@ -199,7 +199,7 @@
/datum/chemical_reaction/medicine/convermol/overly_impure(datum/reagents/holder, datum/equilibrium/equilibrium, step_volume_added)
. = ..()
- overheated(holder, equilibrium, impure = TRUE)
+ overheated(holder, equilibrium, step_volume_added, impure = TRUE)
if(holder.has_reagent(/datum/reagent/oxygen))
clear_reactants(holder, step_volume_added*2)
else
@@ -226,7 +226,7 @@
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_HEALING | REACTION_TAG_OXY
//Sleepytime for chem
-/datum/chemical_reaction/medicine/tirimol/overheated(datum/reagents/holder, datum/equilibrium/equilibrium, impure = FALSE)
+/datum/chemical_reaction/medicine/tirimol/overheated(datum/reagents/holder, datum/equilibrium/equilibrium, step_volume_added, impure = FALSE)
var/bonus = impure ? 2 : 1
if(holder.has_reagent(/datum/reagent/oxygen))
explode_attack_chem(holder, equilibrium, /datum/reagent/inverse/healing/tirimol, 7.5*bonus, 2, ignore_eyes = TRUE) //since we're smoke/air based
@@ -237,7 +237,7 @@
/datum/chemical_reaction/medicine/tirimol/overly_impure(datum/reagents/holder, datum/equilibrium/equilibrium, step_volume_added)
. = ..()
- overheated(holder, equilibrium, TRUE)
+ overheated(holder, equilibrium, step_volume_added, impure = TRUE)
clear_reactants(holder, 2)
/*****TOX*****/
diff --git a/code/modules/reagents/chemistry/recipes/medicine.dm b/code/modules/reagents/chemistry/recipes/medicine.dm
index 9ebad8f29002..6ec8cf3a1f0a 100644
--- a/code/modules/reagents/chemistry/recipes/medicine.dm
+++ b/code/modules/reagents/chemistry/recipes/medicine.dm
@@ -198,7 +198,7 @@
thermic_constant = 25
mix_message = "The solution rapidly breaks apart, turning a mix of colors."
-/datum/chemical_reaction/medicine/albuterol_to_convermol/overheated(datum/reagents/holder, datum/equilibrium/equilibrium, impure = FALSE)
+/datum/chemical_reaction/medicine/albuterol_to_convermol/overheated(datum/reagents/holder, datum/equilibrium/equilibrium, step_volume_added, impure = FALSE)
var/bonus = impure ? 2 : 1
explode_smoke(holder, equilibrium, 7.5 * bonus, TRUE, TRUE)
diff --git a/code/modules/reagents/chemistry/recipes/others.dm b/code/modules/reagents/chemistry/recipes/others.dm
index 92004587f07e..00f2a53bd0de 100644
--- a/code/modules/reagents/chemistry/recipes/others.dm
+++ b/code/modules/reagents/chemistry/recipes/others.dm
@@ -344,11 +344,37 @@
/datum/chemical_reaction/foam
required_reagents = list(/datum/reagent/fluorosurfactant = 1, /datum/reagent/water = 1)
+ // Does the foam slip you?
+ var/slippery = TRUE
+ // How long the foam lasts for
+ var/lifetime = 8 SECONDS
mob_react = FALSE
reaction_flags = REACTION_INSTANT
+/datum/chemical_reaction/foam/hollow
+ required_reagents = list(/datum/reagent/fluorosurfactant = 1, /datum/reagent/water/hollowwater = 1)
+ lifetime = 24 SECONDS
+
+/datum/chemical_reaction/foam/salt
+ required_reagents = list(/datum/reagent/fluorosurfactant = 1, /datum/reagent/water/salt = 1)
+ lifetime = 1 SECONDS
+ slippery = FALSE
+
+/datum/chemical_reaction/foam/ice
+ required_reagents = list(/datum/reagent/fluorosurfactant = 1, /datum/reagent/consumable/ice = 1)
+ slippery = FALSE
+
+/datum/chemical_reaction/foam/soda
+ required_reagents = list(/datum/reagent/fluorosurfactant = 1, /datum/reagent/consumable/sodawater = 1)
+ lifetime = 1 SECONDS
+
+/datum/chemical_reaction/foam/holy
+ required_reagents = list(/datum/reagent/fluorosurfactant = 1, /datum/reagent/water/holywater = 1)
+ lifetime = 24 SECONDS
+ slippery = FALSE
+
/datum/chemical_reaction/foam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/fluid_spread/foam, 2 * created_volume, notification = span_danger("The solution spews out foam!"), log = TRUE)
+ holder.create_foam(/datum/effect_system/fluid_spread/foam, 2 * created_volume, notification = span_danger("The solution spews out foam!"), log = TRUE, lifetime = src.lifetime, slippery = src.slippery)
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
/datum/chemical_reaction/metalfoam
@@ -1104,3 +1130,22 @@
glitter.data["colors"] = list("[accumulated_color]" = 100)
glitter.color = accumulated_color
+
+/datum/chemical_reaction/pair_carnivorous_blood
+ results = list(/datum/reagent/toxin/carnivorousblood = 1)
+ required_reagents = list(/datum/reagent/toxin/carnivorousblood = 1, /datum/reagent/blood = 1)
+ mix_message = "the mixture jumps and sloshes around."
+ reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE | REACTION_TAG_OTHER
+
+/datum/chemical_reaction/pair_carnivorous_blood/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
+ var/datum/reagent/toxin/carnivorousblood/hungryblood = holder.has_reagent(/datum/reagent/toxin/carnivorousblood)
+ var/list/new_blood_dna = list()
+ for(var/datum/reagent/blood/bloodinstance in holder.reagent_list)
+ new_blood_dna += bloodinstance.data["blood_DNA"]
+ hungryblood.feed_dna_list(new_blood_dna)
+
+/datum/chemical_reaction/feed_carnivorous_blood
+ results = list(/datum/reagent/toxin/carnivorousblood = 1)
+ required_reagents = list(/datum/reagent/consumable/nutriment/protein = 1)
+ required_catalysts = list(/datum/reagent/toxin/carnivorousblood = 1)
+ reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
diff --git a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
index d627f119cd97..5e74d91f230a 100644
--- a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
+++ b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
@@ -214,7 +214,7 @@
/datum/chemical_reaction/thermite
results = list(/datum/reagent/thermite = 3)
required_reagents = list(/datum/reagent/aluminium = 1, /datum/reagent/iron = 1, /datum/reagent/oxygen = 1)
- reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE | REACTION_TAG_OTHER
+ reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE | REACTION_TAG_OTHER | REACTION_TAG_DAMAGING
/datum/chemical_reaction/emp_pulse
required_reagents = list(/datum/reagent/uranium = 1, /datum/reagent/iron = 1, /datum/reagent/aluminium = 1)
@@ -513,7 +513,7 @@
/datum/chemical_reaction/cryostylane/reaction_finish(datum/reagents/holder, datum/equilibrium/reaction, react_vol)
. = ..()
if(holder.chem_temp < CRYOSTYLANE_UNDERHEAT_TEMP)
- overheated(holder, null, react_vol) //replace null with fix win 2.3 is merged
+ overheated(holder, reaction, react_vol)
//Freezes the area around you!
/datum/chemical_reaction/cryostylane/overheated(datum/reagents/holder, datum/equilibrium/equilibrium, vol_added)
diff --git a/code/modules/reagents/chemistry/recipes/special.dm b/code/modules/reagents/chemistry/recipes/special.dm
index 7a26d8b7a1a7..eb1eee1a2682 100644
--- a/code/modules/reagents/chemistry/recipes/special.dm
+++ b/code/modules/reagents/chemistry/recipes/special.dm
@@ -41,98 +41,115 @@
/datum/chemical_reaction/randomized/New(recipe_data)
. = ..()
- //creation time, decides if we are random generating or not
- created = recipe_data ? text2num(recipe_data["timestamp"]) : world.realtime
- if(daysSince(created) > persistence_period)
- created = world.realtime
- recipe_data = null
-
- //all reagents
- if(recipe_data)
- required_reagents = unwrap_reagent_list(recipe_data["required_reagents"])
- if(!required_reagents)
- qdel(src)
- return
- required_catalysts = unwrap_reagent_list(recipe_data["required_catalysts"])
- if(!required_catalysts)
- qdel(src)
- return
- results = unwrap_reagent_list(recipe_data["results"])
- if(!results)
+ if(!recipe_data || !load_recipe(recipe_data))
+ log_game("Generating recipe for [src]")
+ if(!generate_recipe())
+ log_game("Couldn't generate recipe for [src]")
qdel(src)
- return
- else
- var/list/remaining_possible_reagents = GetPossibleReagents(RNGCHEM_INPUT)
- var/list/remaining_possible_catalysts = GetPossibleReagents(RNGCHEM_CATALYSTS)
- //We're going to assume we're not doing any weird partial reactions for now.
- for(var/reagent_type in results)
- remaining_possible_catalysts -= reagent_type
- remaining_possible_reagents -= reagent_type
-
- var/in_reagent_count = min(rand(min_input_reagents, max_input_reagents),remaining_possible_reagents.len)
- if(in_reagent_count <= 0)
- qdel(src)
- return
- required_reagents = list()
- for(var/i in 1 to in_reagent_count)
- var/r_id = pick_n_take(remaining_possible_reagents)
- required_reagents[r_id] = rand(min_input_reagent_amount,max_input_reagent_amount)
- remaining_possible_catalysts -= r_id //Can't have same reagents both as catalyst and reagent. Or can we ?
- required_catalysts = list()
- var/in_catalyst_count = min(rand(min_catalysts,max_catalysts),remaining_possible_catalysts.len)
- for(var/i in 1 to in_catalyst_count)
- required_catalysts[pick_n_take(remaining_possible_catalysts)] = rand(min_input_reagent_amount,max_input_reagent_amount)
+/datum/chemical_reaction/randomized/proc/load_recipe(recipe_data)
+ PRIVATE_PROC(TRUE)
+ // Timestamp
+ created = text2num(recipe_data["timestamp"])
+ if(daysSince(created) > persistence_period)
+ log_game("Recipe [src] expired.")
+ return FALSE
+ //ingredients and catalysts
+ required_reagents = unwrap_reagent_list(recipe_data["required_reagents"])
+ if(!required_reagents)
+ log_game("Couldn't load reagents for [src]")
+ return FALSE
+ required_catalysts = unwrap_reagent_list(recipe_data["required_catalysts"])
+ if(!required_catalysts)
+ log_game("Couldn't load catalysts for [src]")
+ return FALSE
+
+ //container
+ if(possible_containers)
+ var/container = recipe_data["required_container"] ? text2path(recipe_data["required_container"]) : null
+ if(!container)
+ log_game("Couldn't load container for [src]")
+ return FALSE
+ required_container = container
//temperature
- if(recipe_data)
- is_cold_recipe = recipe_data["is_cold_recipe"]
- required_temp = recipe_data["required_temp"]
- optimal_temp = recipe_data["optimal_temp"]
- overheat_temp = recipe_data["overheat_temp"]
- thermic_constant = recipe_data["thermic_constant"]
- else
- is_cold_recipe = pick(TRUE, FALSE)
- if(is_cold_recipe)
- required_temp = rand(min_temp + 50, max_temp)
- optimal_temp = rand(min_temp + 25, required_temp - 10)
- overheat_temp = rand(min_temp, optimal_temp - 10)
- if(overheat_temp >= 200) //Otherwise it can disappear when you're mixing and I don't want this to happen here
- overheat_temp = 200
- else
- required_temp = rand(min_temp, max_temp - 50)
- optimal_temp = rand(required_temp + 10, max_temp - 25)
- overheat_temp = rand(optimal_temp, max_temp + 50)
- if(overheat_temp <= 400)
- overheat_temp = 400
-
- //ph
- if(recipe_data)
- optimal_ph_min = recipe_data["optimal_ph_min"]
- optimal_ph_max = recipe_data["optimal_ph_max"]
- determin_ph_range = recipe_data["determin_ph_range"]
- H_ion_release = recipe_data["H_ion_release"]
- else
- optimal_ph_min = CHEMICAL_MIN_PH + rand(0, suboptimal_range_ph)
- optimal_ph_max = max((CHEMICAL_MAX_PH + rand(0, suboptimal_range_ph)), (CHEMICAL_MIN_PH + 1)) //Always ensure we've a window of 1
- determin_ph_range = suboptimal_range_ph
- H_ion_release = (rand(0, 25) / 100)// 0 - 0.25
+ is_cold_recipe = recipe_data["is_cold_recipe"]
+ required_temp = recipe_data["required_temp"]
+ optimal_temp = recipe_data["optimal_temp"]
+ overheat_temp = recipe_data["overheat_temp"]
+ thermic_constant = recipe_data["thermic_constant"]
+
+ //pH
+ optimal_ph_min = recipe_data["optimal_ph_min"]
+ optimal_ph_max = recipe_data["optimal_ph_max"]
+ determin_ph_range = recipe_data["determin_ph_range"]
+ H_ion_release = recipe_data["H_ion_release"]
//purity
- purity_min = recipe_data ? recipe_data["purity_min"] : (rand(0, 4) / 10)
-
- //container
- if(recipe_data)
- var/container = recipe_data["required_container"]
- if(container)
- container = text2path(container)
- if(!container)
- qdel(src)
- return
- required_container = container
- else if(length(possible_containers))
+ purity_min = recipe_data["purity_min"]
+
+ return TRUE
+
+/datum/chemical_reaction/randomized/proc/generate_recipe()
+ // Timestamp
+ created = world.realtime
+
+ // Reagents and catalysts
+ var/list/remaining_possible_reagents = GetPossibleReagents(RNGCHEM_INPUT)
+ var/list/remaining_possible_catalysts = GetPossibleReagents(RNGCHEM_CATALYSTS)
+ //We're going to assume we're not doing any weird partial reactions for now.
+ for(var/reagent_type in results)
+ remaining_possible_catalysts -= reagent_type
+ remaining_possible_reagents -= reagent_type
+
+ if(remaining_possible_reagents.len < min_input_reagents)
+ log_game("Couldn't find enough reagents for [src]")
+ return FALSE
+
+ required_reagents = list()
+ var/in_reagent_count = min(rand(min_input_reagents, max_input_reagents),remaining_possible_reagents.len)
+ for(var/i in 1 to in_reagent_count)
+ var/r_id = pick_n_take(remaining_possible_reagents)
+ required_reagents[r_id] = rand(min_input_reagent_amount,max_input_reagent_amount)
+ remaining_possible_catalysts -= r_id //Can't have same reagents both as catalyst and reagent. Or can we ?
+
+ if(remaining_possible_catalysts.len < min_catalysts)
+ log_game("Couldn't find enough catalysts for [src]")
+ return FALSE
+
+ required_catalysts = list()
+ var/in_catalyst_count = min(rand(min_catalysts,max_catalysts),remaining_possible_catalysts.len)
+ for(var/i in 1 to in_catalyst_count)
+ required_catalysts[pick_n_take(remaining_possible_catalysts)] = rand(min_input_reagent_amount,max_input_reagent_amount)
+
+ // Container
+ if(possible_containers)
required_container = pick(possible_containers)
+ // Temperature
+ is_cold_recipe = pick(TRUE, FALSE)
+ if(is_cold_recipe)
+ required_temp = rand(min_temp + 50, max_temp)
+ optimal_temp = rand(min_temp + 25, required_temp - 10)
+ overheat_temp = rand(min_temp, optimal_temp - 10)
+ if(overheat_temp >= 200) //Otherwise it can disappear when you're mixing and I don't want this to happen here
+ overheat_temp = 200
+ else
+ required_temp = rand(min_temp, max_temp - 50)
+ optimal_temp = rand(required_temp + 10, max_temp - 25)
+ overheat_temp = rand(optimal_temp, max_temp + 50)
+ if(overheat_temp <= 400)
+ overheat_temp = 400
+
+ //pH
+ optimal_ph_min = CHEMICAL_MIN_PH + rand(0, suboptimal_range_ph)
+ optimal_ph_max = max((CHEMICAL_MAX_PH - rand(0, suboptimal_range_ph)), (optimal_ph_min + 1)) //Always ensure we've a window of 1
+ determin_ph_range = suboptimal_range_ph
+ H_ion_release = (rand(0, 25) / 100)// 0 - 0.25
+
+ // Purity
+ purity_min = (rand(0, 4) / 10)
+ return TRUE
/**
* Returns the reagents to select for randomizing
diff --git a/code/modules/reagents/reagent_containers/cups/bottle.dm b/code/modules/reagents/reagent_containers/cups/bottle.dm
index e4ad220cb00f..9246bdb50f64 100644
--- a/code/modules/reagents/reagent_containers/cups/bottle.dm
+++ b/code/modules/reagents/reagent_containers/cups/bottle.dm
@@ -223,6 +223,11 @@
desc = "A small bottle. Contains Histamine."
list_reagents = list(/datum/reagent/toxin/histamine = 30)
+/obj/item/reagent_containers/cup/bottle/carnivorous_blood
+ name = "carnivorous blood bottle"
+ desc = "A small bottle. Contains carnivorous blood."
+ list_reagents = list(/datum/reagent/toxin/carnivorousblood = 30)
+
/obj/item/reagent_containers/cup/bottle/diphenhydramine
name = "antihistamine bottle"
desc = "A small bottle of diphenhydramine."
diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm
index b052473d55f8..efc98614861d 100644
--- a/code/modules/reagents/reagent_dispenser.dm
+++ b/code/modules/reagents/reagent_dispenser.dm
@@ -35,6 +35,8 @@
var/last_rigger = ""
/// is it climbable? some of our wall-mounted dispensers should not have this
var/climbable = FALSE
+ /// Flags passed to the reagents datum upon creation
+ var/reagent_flags = DRAINABLE | AMOUNT_VISIBLE
// This check is necessary for assemblies to automatically detect that we are compatible
/obj/structure/reagent_dispensers/IsSpecialAssembly()
@@ -152,7 +154,7 @@
UnregisterSignal(src, COMSIG_IGNITER_ACTIVATE)
/obj/structure/reagent_dispensers/Initialize(mapload)
- create_reagents(tank_volume, DRAINABLE | AMOUNT_VISIBLE)
+ create_reagents(tank_volume, reagent_flags)
if(reagent_id)
reagents.add_reagent(reagent_id, tank_volume)
. = ..()
@@ -358,6 +360,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/reagent_dispensers/wall/peppertank, 3
desc = "A machine that cools and dispenses liquids to drink. The 'hot' handle doesn't seem to do anything."
icon_state = "water_cooler"
anchored = TRUE
+ reagent_flags = DRAINABLE | TRANSPARENT
tank_volume = 200
can_be_tanked = FALSE
max_integrity = 150
diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm
index c0bfbe678edd..a16c894eed4c 100644
--- a/code/modules/research/anomaly/anomaly_core.dm
+++ b/code/modules/research/anomaly/anomaly_core.dm
@@ -206,7 +206,7 @@
if(!length(possible_targets))
return
var/turf/target = pick(possible_targets)
- new /obj/effect/temp_visual/telegraphing/thunderbolt(target)
+ new /obj/effect/temp_visual/telegraphing/circle(target)
addtimer(CALLBACK(src, PROC_REF(strike), target), 1 SECONDS)
/obj/item/assembly/signaler/anomaly/weather/proc/strike(turf/target)
diff --git a/code/modules/research/designs/autolathe/service_designs.dm b/code/modules/research/designs/autolathe/service_designs.dm
index 3d6720ec4696..784cc5688b87 100644
--- a/code/modules/research/designs/autolathe/service_designs.dm
+++ b/code/modules/research/designs/autolathe/service_designs.dm
@@ -10,6 +10,18 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+/datum/design/wet_floor_sign
+ name = "Wet Floor Sign"
+ id = "wet_floor_sign"
+ build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/plastic =SMALL_MATERIAL_AMOUNT)
+ build_path = /obj/item/clothing/suit/caution
+ category = list(
+ RND_CATEGORY_INITIAL,
+ RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_JANITORIAL,
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
/datum/design/watering_can
name = "Watering Can"
id = "watering_can"
diff --git a/code/modules/research/designs/electronics_designs.dm b/code/modules/research/designs/electronics_designs.dm
index b1da9f2c1c05..9e5e225865ee 100644
--- a/code/modules/research/designs/electronics_designs.dm
+++ b/code/modules/research/designs/electronics_designs.dm
@@ -65,3 +65,16 @@
RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_SCIENCE
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/manipulator_task_disk
+ name = "Manipulator Task Disk"
+ desc = "Produce additional disks for storing manipulator tasks."
+ id = "manipulator_task_disk"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/iron =SMALL_MATERIAL_AMOUNT * 3, /datum/material/glass =SMALL_MATERIAL_AMOUNT)
+ build_path = /obj/item/disk/manipulator
+ category = list(
+ RND_CATEGORY_INITIAL,
+ RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_SCIENCE
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm
index b46ce8075417..9ff9e638b2de 100644
--- a/code/modules/research/designs/medical_designs.dm
+++ b/code/modules/research/designs/medical_designs.dm
@@ -165,7 +165,7 @@
departmental_flags = DEPARTMENT_BITFLAG_MEDICAL | DEPARTMENT_BITFLAG_SCIENCE
/datum/design/stasis_bag
- name = "Stasis Bodybag"
+ name = "Stasis Body Bag"
desc = "A disposal bodybag designed to stabilize patients in the field in critical condition. \
The bag itself cannot maintain stasis for long, and will eventually fall apart."
id = "stasis_bodybag"
diff --git a/code/modules/research/designs/weapon_designs.dm b/code/modules/research/designs/weapon_designs.dm
index b09378ea49b8..767c5bfcfb3b 100644
--- a/code/modules/research/designs/weapon_designs.dm
+++ b/code/modules/research/designs/weapon_designs.dm
@@ -636,7 +636,7 @@
id = "liberator_gun"
build_type = AUTOLATHE
materials = list(/datum/material/plastic = SHEET_MATERIAL_AMOUNT * 2, /datum/material/iron = SMALL_MATERIAL_AMOUNT * 15)
- build_path = /obj/item/gun/ballistic/automatic/pistol/doorhickey
+ build_path = /obj/item/gun/ballistic/automatic/pistol/doohickey
category = list(RND_CATEGORY_IMPORTED)
/datum/design/stun_boomerang
diff --git a/code/modules/research/gizmo/gizmo_controller.dm b/code/modules/research/gizmo/gizmo_controller.dm
new file mode 100644
index 000000000000..90b5f7bef879
--- /dev/null
+++ b/code/modules/research/gizmo/gizmo_controller.dm
@@ -0,0 +1,42 @@
+/// The master datum that handles every bit of gizmo code
+/// Just so you will understand the full insanity of this:
+/// object > gizmo_controller > gizmo_puzzle + gizmo_interface > gizmode > gizpulse > callbacks
+/datum/gizmo_controller
+ /// Can hold different interacting modes (wires, voice) and connected interfaces
+ var/list/interfaces = list(GIZMO_INTERFACE_WIRES = /datum/gizmo_interface)
+ /// Instanted interfaces. really just here so I can check this shit in vv
+ var/list/instances = list()
+
+/// Generate interfaces for interacting with the gizmo
+/datum/gizmo_controller/proc/generate_interfaces(atom/movable/holder)
+ for(var/interface_define, interface_type in interfaces)
+
+ var/list/callbacks = list()
+ var/datum/gizmo_interface/interface_instance = new interface_type (src)
+ interface_instance.generate_interface(holder, callbacks)
+ instances += interface_instance
+
+ switch(interface_define)
+ if(GIZMO_INTERFACE_WIRES)
+ holder.set_wires(new /datum/wires/gizmo(holder, interface_instance.puzzle))
+ if(GIZMO_INTERFACE_VOICE)
+ holder.AddComponent(/datum/component/gizmo_voice, interface_instance.puzzle)
+
+/// Wired with an interface that moves
+/datum/gizmo_controller/beyblade
+ interfaces = list(GIZMO_INTERFACE_WIRES = /datum/gizmo_interface/beyblade)
+
+/// Wired with an interface that toggles the icon_state and/or glows
+/datum/gizmo_controller/toggle
+ interfaces = list(GIZMO_INTERFACE_WIRES = /datum/gizmo_interface/toggle)
+
+/// Voice controller with a voice puzzle and interface. Comes with a wire interface that gives you the hint to use the voice interface
+/datum/gizmo_controller/voice
+ interfaces = list(GIZMO_INTERFACE_WIRES = /datum/gizmo_interface/voice_unlock, GIZMO_INTERFACE_VOICE = /datum/gizmo_interface/voice)
+
+/// For held gizmo's
+/datum/gizmo_controller/item
+
+/// Always horrible agony!
+/datum/gizmo_controller/cursed
+ interfaces = list(GIZMO_INTERFACE_WIRES = /datum/gizmo_interface/cursed)
diff --git a/code/modules/research/gizmo/gizmo_interface.dm b/code/modules/research/gizmo/gizmo_interface.dm
new file mode 100644
index 000000000000..7e46a47fcc64
--- /dev/null
+++ b/code/modules/research/gizmo/gizmo_interface.dm
@@ -0,0 +1,69 @@
+/// Essentially a small wrapper that holds the puzzle datum and connects it with the operating modes of the machine
+/datum/gizmo_interface
+ /// The gizmo master ultra mega controller
+ var/datum/gizmo_controller/controller
+ /// The puzzle that connects the interface to the activation callbacks and checks if you pulsed the right sequences
+ var/datum/gizmo_puzzle/puzzle = /datum/gizmo_puzzle
+
+ /// List (or list define) for the gizmodes to pick
+ var/list/possible_active_modes = GIZMO_COMMON_MODES
+ /// Guaranteed active pulses
+ var/list/guaranteed_active_gizmodes = list()
+ /// The gizmode instances
+ var/list/active_gizmodes = list()
+
+ /// Min modes to select from possible_active_modes
+ var/min_modes = 1
+ /// Max modes to select from possible_active_modes
+ var/max_modes = 2
+
+/datum/gizmo_interface/New(datum/gizmo_controller/controller)
+ . = ..()
+
+ src.controller = controller
+
+/// Instantiate the active modes, tell them to instantiate and pass their callbacks to the puzzle maker
+/datum/gizmo_interface/proc/generate_interface(atom/movable/holder, datum/callback/pulse_callback)
+ var/list/trigger_callbacks = list()
+ var/list/modes_to_spawn = list() + guaranteed_active_gizmodes
+
+ for(var/i in 1 to rand(min_modes, max_modes))
+ var/path = pick_weight_take(possible_active_modes)
+ if(!path)
+ break
+ modes_to_spawn += path
+
+ for(var/path in modes_to_spawn)
+ var/datum/gizmodes/mode = new path ()
+ mode.generate_modes(trigger_callbacks, src)
+ active_gizmodes += mode
+
+ puzzle = new puzzle ()
+ puzzle.generate_code_sequences(trigger_callbacks)
+
+/// Moves around. Guaranteed to have a mode that controles the movement
+/datum/gizmo_interface/beyblade
+ guaranteed_active_gizmodes = list(/datum/gizmodes/mover)
+ min_modes = 0
+ max_modes = 1
+
+/// Guaranteed to have a lights mode
+/datum/gizmo_interface/toggle
+ guaranteed_active_gizmodes = list(/datum/gizmodes/lights)
+
+/// Guaranteed to have a mode that will give a voice components secret keywords. Assumes there's a voice interface added by the gizmo_controller
+/datum/gizmo_interface/voice_unlock
+ guaranteed_active_gizmodes = list(/datum/gizmodes/voice)
+ min_modes = 0
+ max_modes = 0
+
+/// For the actual voice operated puzzle. Is a bit more forgiving
+/datum/gizmo_interface/voice
+ puzzle = /datum/gizmo_puzzle/voice
+
+/// Always bad!
+/datum/gizmo_interface/cursed
+ possible_active_modes = list(
+ /datum/gizmodes/bad = 1,
+ )
+ max_modes = 1
diff --git a/code/modules/research/gizmo/gizmo_items.dm b/code/modules/research/gizmo/gizmo_items.dm
new file mode 100644
index 000000000000..bc6d237681a1
--- /dev/null
+++ b/code/modules/research/gizmo/gizmo_items.dm
@@ -0,0 +1,22 @@
+/// A handheld gizmo, with some different activation modes
+/obj/item/gizmo
+ name = "gizmo"
+ desc = "Fliggoes the giggoe when its oven in hot the device."
+ icon = 'icons/obj/science/gizmos.dmi'
+
+ resistance_flags = FIRE_PROOF | UNACIDABLE | ACID_PROOF
+
+ /// Possible icon states
+ var/list/icon_states = list("gizmo_item_1")
+ /// Reference to the gizmo master controller that handles all the other gizmo stuff
+ var/datum/gizmo_controller/controller = /datum/gizmo_controller/item
+
+/obj/item/gizmo/Initialize(mapload)
+ . = ..()
+
+ if(icon_states)
+ base_icon_state = pick(icon_states)
+ icon_state = base_icon_state
+
+ controller = new controller(src)
+ controller.generate_interfaces(src)
diff --git a/code/modules/research/gizmo/gizmo_machines.dm b/code/modules/research/gizmo/gizmo_machines.dm
new file mode 100644
index 000000000000..3d50d797d5d7
--- /dev/null
+++ b/code/modules/research/gizmo/gizmo_machines.dm
@@ -0,0 +1,136 @@
+/// Science object that behaves similairly to to strange objects/relics, but is activated by cracking wire sequences and other functions
+/obj/machinery/gizmo
+ name = "gizmo"
+ desc = "Does a function when you put the jigger at the other ends thing."
+ icon_state = "gizmo_1"
+ icon = 'icons/obj/science/gizmos.dmi'
+
+ resistance_flags = FIRE_PROOF | UNACIDABLE | ACID_PROOF
+
+ panel_open = TRUE
+ density = TRUE
+ anchored = FALSE
+
+ /// Possible icon states to pick from
+ var/list/icon_states = list("gizmo_0", "gizmo_1", "gizmo_2", "gizmo_3", "gizmo_4")
+ /// Reference to the gizmo. We dont actually need to track this for anything but ease of vv
+ var/datum/gizmo_controller/controller = /datum/gizmo_controller
+ /// Possible names to pick from to keep things confusing
+ var/list/possible_names = list(
+ "gizmo", "jigger", "doohickey", "particle inverter", "polarity superplexer", "flitcher poster", "natty gonk", "quantum quantum",
+ "entropy nilum", "tachyon streamer", "doing device", "task operator", "interface responder", "kinetic observer", "turbo encabulator",
+ "statistic responder", "possibility matrix", "toety aaier", "phase anchor",
+ )
+
+/obj/machinery/gizmo/Initialize(mapload)
+ . = ..()
+
+ name = pick(possible_names)
+
+ if(icon_states)
+ base_icon_state = pick(icon_states)
+ icon_state = base_icon_state
+
+ controller = new controller(src)
+ controller.generate_interfaces(src)
+
+ RegisterSignal(src, COMSIG_GIZMO_START_MOVING, PROC_REF(start_moving))
+ RegisterSignal(src, COMSIG_GIZMO_STOP_MOVING, PROC_REF(stop_moving))
+
+/obj/machinery/gizmo/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ add_fingerprint(user)
+
+ if(is_wire_tool(tool))
+ attempt_wire_interaction(user)
+ return ITEM_INTERACT_SUCCESS
+ return NONE
+
+/obj/machinery/gizmo/wrench_act(mob/living/user, obj/item/tool)
+ . = ITEM_INTERACT_BLOCKING
+ if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/gizmo/proc/start_moving(datum/source, datum/gizpulse/pulse)
+ SIGNAL_HANDLER
+
+ on_start_moving(pulse)
+
+/obj/machinery/gizmo/proc/stop_moving(datum/source, datum/gizpulse/pulse)
+ SIGNAL_HANDLER
+
+ on_stop_moving(pulse)
+
+/obj/machinery/gizmo/proc/on_start_moving(datum/gizpulse/pulse)
+ return
+
+/obj/machinery/gizmo/proc/on_stop_moving(datum/gizpulse/pulse)
+ return
+
+/// Gizmo that comes with a gizmo interface to start moving
+/obj/machinery/gizmo/beyblade
+ icon_states = list("beyblade")
+
+ controller = /datum/gizmo_controller/beyblade
+
+ /// Are we currently moving?
+ var/moving = FALSE
+
+/obj/machinery/gizmo/beyblade/update_icon(updates)
+ . = ..()
+
+ icon_state = base_icon_state + (moving ? "_spinning" : "")
+
+/obj/machinery/gizmo/beyblade/on_start_moving(datum/gizpulse/pulse)
+ density = TRUE
+
+ moving = TRUE
+ update_appearance(UPDATE_ICON)
+
+/obj/machinery/gizmo/beyblade/on_stop_moving(datum/gizpulse/pulse)
+ density = FALSE
+
+ moving = FALSE
+ update_appearance(UPDATE_ICON)
+
+/// A gizmo with some sort of "on" state. Really only for visuals
+/obj/machinery/gizmo/toggle
+ controller = /datum/gizmo_controller/toggle
+
+ icon_states = list("gizmo_active_0", "gizmo_active_1", "gizmo_active_2", "gizmo_active_3", "gizmo_active_4", "gizmo_active_5")
+
+ var/on_state = FALSE
+
+/obj/machinery/gizmo/toggle/Initialize(mapload)
+ . = ..()
+
+ RegisterSignal(src, COMSIG_GIZMO_ON_STATE, PROC_REF(on_state))
+ RegisterSignal(src, COMSIG_GIZMO_OFF_STATE, PROC_REF(off_state))
+
+/obj/machinery/gizmo/toggle/update_icon(updates)
+ . = ..()
+
+ icon_state = base_icon_state + (on_state ? "_on" : "")
+
+/obj/machinery/gizmo/toggle/tucker_tubes
+ icon_states = list("gizmo_active_0")
+ icon_state = "gizmo_active_0" //for the mapping icon
+ anchored = TRUE
+
+/obj/machinery/gizmo/toggle/proc/on_state(datum/source)
+ SIGNAL_HANDLER
+
+ on_state = TRUE
+ update_icon()
+
+/obj/machinery/gizmo/toggle/proc/off_state(datum/source)
+ SIGNAL_HANDLER
+
+ on_state = FALSE
+ update_icon()
+
+/// A gizmo with a voice activated interface
+/obj/machinery/gizmo/voice
+ controller = /datum/gizmo_controller/voice
+
+/obj/machinery/gizmo/cursed
+ controller = /datum/gizmo_controller/cursed
diff --git a/code/modules/research/gizmo/gizmo_spawner.dm b/code/modules/research/gizmo/gizmo_spawner.dm
new file mode 100644
index 000000000000..c7c33ff68a3d
--- /dev/null
+++ b/code/modules/research/gizmo/gizmo_spawner.dm
@@ -0,0 +1,9 @@
+/obj/effect/spawner/random/gizmo
+ name = "gizmo spawner"
+ icon_state = "gizmo"
+ loot = list(
+ /obj/machinery/gizmo = 6,
+ /obj/machinery/gizmo/toggle = 4,
+ /obj/machinery/gizmo/voice = 2,
+ /obj/machinery/gizmo/beyblade = 1,
+ )
diff --git a/code/modules/research/gizmo/gizmo_voice.dm b/code/modules/research/gizmo/gizmo_voice.dm
new file mode 100644
index 000000000000..0c69e44074a7
--- /dev/null
+++ b/code/modules/research/gizmo/gizmo_voice.dm
@@ -0,0 +1,62 @@
+/// The words (or tones really) the gizmo voice interface can listen for. Limit to 2 characters (or it breaks because im cringe)
+GLOBAL_LIST_INIT(gizmo_words, world.file2list("strings/gizmo_words.txt"))
+
+/// Listen to the tones and send the sequence to the puzzle datum
+/datum/component/gizmo_voice
+ var/datum/gizmo_puzzle/puzzle
+ /// they're not really words but you get it
+ var/static/list/active_words = list()
+
+/datum/component/gizmo_voice/Initialize(datum/gizmo_puzzle/puzzle)
+ if(!ismovable(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ var/atom/movable/movable = parent
+ movable.become_hearing_sensitive(type)
+ RegisterSignal(movable, COMSIG_MOVABLE_HEAR, PROC_REF(on_hear))
+
+ src.puzzle = puzzle
+
+ if(!active_words.len)
+ generate_active_words()
+
+/// Pick the activate keywords
+/datum/component/gizmo_voice/proc/generate_active_words()
+ var/list/possible_words = GLOB.gizmo_words.Copy()
+ for(var/i in 1 to puzzle.cryptic_pulse.len)
+ active_words += pick_n_take(possible_words)
+
+/// Listen to a message, and pick out the puzzle words
+/datum/component/gizmo_voice/proc/on_hear(datum/source, list/hearing_args)
+ SIGNAL_HANDLER
+
+ if(hearing_args[HEARING_SPEAKER] == parent)
+ return
+
+ var/haystack = hearing_args[HEARING_RAW_MESSAGE]
+ var/list/text_position_index = list()
+
+ for(var/needle in active_words)
+ var/position = 1
+ // we can have multiple of the same keyword in one sequence
+ for(var/i in 1 to puzzle.code_length)
+ position = findtext(haystack, needle, position)
+
+ if(!position)
+ break
+
+ /// needle + position because assocs need to be unique, we splice it away later when sending it
+ text_position_index[needle + "[position]"] = position
+ // So for the next loop we dont find the exact same word again
+ position++
+
+ if(!text_position_index.len)
+ return
+
+ text_position_index = sortTim(text_position_index, associative = TRUE)
+ for(var/thing, position in text_position_index)
+ // Only send feedback for the last speech packet
+ var/no_feedback = text_position_index.Find(thing) != text_position_index.len
+ // When solved, accept no further packets from this
+ if(puzzle.on_pulse(active_words.Find(copytext(thing, 1, 3)), hearing_args[HEARING_SPEAKER], parent, no_feedback = no_feedback) == GIZMO_PUZZLE_SOLVED)
+ return
diff --git a/code/modules/research/gizmo/gizmo_wires.dm b/code/modules/research/gizmo/gizmo_wires.dm
new file mode 100644
index 000000000000..01ce0c5eb3b6
--- /dev/null
+++ b/code/modules/research/gizmo/gizmo_wires.dm
@@ -0,0 +1,30 @@
+/// Wires that send pulses to a gizmo puzzle datum
+/datum/wires/gizmo
+ /// It's already randomized on the puzzle component
+ randomize = FALSE
+
+ /// Might as well keep it broad, it's all signals anyway
+ holder_type = /obj
+
+ /// The wires we need to pulse for cracking the code
+ var/list/cryptic_wires = list(
+ CRYPTIC_WIRE_1,
+ CRYPTIC_WIRE_2,
+ CRYPTIC_WIRE_3,
+ CRYPTIC_WIRE_4,
+ CRYPTIC_WIRE_5,
+ CRYPTIC_WIRE_6,
+ CRYPTIC_WIRE_7,
+ CRYPTIC_WIRE_8,
+ )
+
+ var/datum/gizmo_puzzle/puzzle
+
+/datum/wires/gizmo/New(atom/holder, datum/gizmo_puzzle/puzzle)
+ wires = cryptic_wires
+ src.puzzle = puzzle
+
+ ..()
+
+/datum/wires/gizmo/on_pulse(wire, mob/living/user)
+ puzzle.on_pulse(cryptic_wires.Find(wire), user, holder)
diff --git a/code/modules/research/gizmo/gizmodes/giz_filler.dm b/code/modules/research/gizmo/gizmodes/giz_filler.dm
new file mode 100644
index 000000000000..15adbc580239
--- /dev/null
+++ b/code/modules/research/gizmo/gizmodes/giz_filler.dm
@@ -0,0 +1,101 @@
+/// Shake around some, make some oil or just fly away
+/datum/gizmodes/sputter
+ possible_active_modes = list(
+ /datum/gizpulse/sputter = 1,
+ /datum/gizpulse/throw_self = 1,
+ )
+
+ min_modes = 2
+ cooldown_time = 5 SECONDS
+
+/// Shake around some and spill oil
+/datum/gizpulse/sputter
+ /// Range in which we can oilerize
+ var/oil_range = 3
+ /// Chance for a tile to get oiled
+ var/oil_chance = 8
+
+/datum/gizpulse/sputter/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ playsound(holder, 'sound/effects/splat.ogg', 30)
+ for(var/turf/open/tile in oview(oil_range, holder))
+ if(prob(oil_chance))
+ new /obj/effect/decal/cleanable/blood/oil(tile)
+
+ holder.Shake()
+
+/datum/gizpulse/throw_self/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ holder.throw_at(get_edge_target_turf(holder, pick(GLOB.alldirs)), 50, 1)
+
+/// Spawn some item
+/datum/gizmodes/dispenser
+ possible_active_modes = list(
+ /datum/gizpulse/dispense = 1,
+ /datum/gizpulse/dispense = 1,
+ /datum/gizpulse/dispense = 1,
+ /datum/gizpulse/dispense = 1,
+ /datum/gizpulse/dispense = 1,
+ /datum/gizpulse/dispense = 1,
+ )
+ min_modes = 4
+ max_modes = 6
+
+/datum/gizpulse/dispense
+ /// Weighted list of objects we could spawn
+ var/list/possible_objects = list(
+ /obj/item/crowbar = 1,
+ /obj/item/wrench = 1,
+ /obj/item/screwdriver = 1,
+ /obj/item/multitool = 1,
+ /obj/item/wirecutters = 1,
+ /obj/item/weldingtool = 1,
+ )
+ /// Typepath of object to spawn
+ var/object_to_spawn
+
+ /// List of softrefs of the objects we spawned. Exists only to prevent game-crashing object spam
+ var/list/spawned_objects_weakrefs = list()
+ /// Max objects that can exist at once
+ var/max_objects = 50
+ /// The position of the next object to spawn in spawned_objects_weakrefs
+ var/next_object_position = 0
+
+/datum/gizpulse/dispense/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ if(!object_to_spawn)
+ object_to_spawn = pick_weight(possible_objects)
+
+ // We don't technically track to make sure we dont make more than [max_objects] objects, we just delete whatever was made [max_objects] ago
+ if(spawned_objects_weakrefs.len > (next_object_position % max_objects))
+ qdel(spawned_objects_weakrefs["[(next_object_position % max_objects) + 1]"])
+
+ var/new_object = new object_to_spawn (get_turf(holder))
+ modify(new_object)
+ spawned_objects_weakrefs["[(next_object_position % max_objects) + 1]"] = WEAKREF(new_object)
+ next_object_position++
+
+/datum/gizpulse/dispense/proc/modify(atom/movable/new_object)
+ return
+
+/// Spawn fake goop food
+/datum/gizmodes/dispenser/food
+ possible_active_modes = list(
+ /datum/gizpulse/dispense/food = 1,
+ )
+
+ mode_pulses = list(
+ /datum/gizpulse/mode_controle/direct_activate,
+ )
+
+ min_modes = 1
+ max_modes = 1
+
+/// Food made with goop
+/datum/gizpulse/dispense/food
+ possible_objects = list(
+ /obj/item/food/donut/plain = 1,
+ /obj/item/food/burger = 1,
+ )
+
+/datum/gizpulse/dispense/food/modify(atom/movable/new_object)
+ new_object.reagents.clear_reagents()
+ // its goop all the way down
+ new_object.reagents.add_reagent(/datum/reagent/consumable/gizmo_goop, 5)
diff --git a/code/modules/research/gizmo/gizmodes/gizactives.dm b/code/modules/research/gizmo/gizmodes/gizactives.dm
new file mode 100644
index 000000000000..632ce7815f44
--- /dev/null
+++ b/code/modules/research/gizmo/gizmodes/gizactives.dm
@@ -0,0 +1,124 @@
+/// You can imagine a /datum/gizmodes as remote and a TV
+/// The mode_controles is the remote. Maybe there's 9 buttons for 1-9, and pressing the buttons just goes to that channel!
+/// Maybe there's 10 buttons, 1-9 for selecting a channel and an extra for going to the selected channel
+/// Or maybe there's two buttons, one that cycles to the next number and one that then goes to that channel]
+/// The giz_pulse is essentually the TV. Generally the functioning of the TV is themed, happy image :) or sad image :(
+/datum/gizmodes
+ /// Our paarent gizmo interface
+ var/datum/gizmo_interface/interface
+ /// The currently selected gizpulse
+ var/datum/gizpulse/current_active
+
+ /// Instantiated active operating modes/gizpulses
+ var/list/active_gizmodes = list()
+ /// Guaranted operating modes/gizpulses types. use GIZMO_PICK_ONE with an associated list for more bespoke guaranteed picking
+ var/list/guaranteed_active_gizmodes = list()
+
+ /// Random gizpulses we can have. PICKWEIGHTED SO GIVE IT A VALUE (/datum/gizpulse/milk_person = 1)
+ var/list/possible_active_modes = list()
+ /// Min modes from possible_active_modes
+ var/min_modes = 1
+ /// Max modes from possible_active_modes
+ var/max_modes = 2
+
+ /// Mode controles add signals that decide how gizpulses are activated
+ /// Such as cycle to the next gizpulse, directly activate a gizpulse, or select a specific gizpulse
+ /// Select mode, for example, adds a callback for every gizpulse + 1 for activating that
+ /// Cycle mode activate adds only one callback, which cycles to the next one and then activates it
+ var/list/mode_pulses = list(
+ /datum/gizpulse/mode_controle/select_mode,
+ /datum/gizpulse/mode_controle/cycle_mode,
+ /datum/gizpulse/mode_controle/direct_activate,
+ /datum/gizpulse/mode_controle/cycle_mode/activate,
+ )
+ /// The selected mode controle
+ var/datum/gizpulse/mode_controle/mode_pulse
+ /// Time between pulses
+ var/cooldown_time = 1 SECONDS
+ COOLDOWN_DECLARE(cooldown_timer)
+
+/// Pick the paths to generate and instantiate them
+/datum/gizmodes/proc/generate_modes(list/trigger_callbacks, datum/gizmo_interface/interface)
+ src.interface = interface
+
+ var/list/modes_to_spawn = list()
+
+ for(var/path in guaranteed_active_gizmodes)
+ if(path == GIZMO_PICK_ONE)
+ var/list/nested_list = guaranteed_active_gizmodes[path]
+ modes_to_spawn += pick_weight(nested_list)
+ else
+ modes_to_spawn += path
+
+ for(var/i in 1 to rand(min_modes, max_modes))
+ var/path = pick_weight_take(possible_active_modes)
+ if(!path)
+ break
+ modes_to_spawn += path
+
+ for(var/path in modes_to_spawn)
+ active_gizmodes += new path ()
+
+ current_active = pick(active_gizmodes)
+
+ var/mode_path = pick(mode_pulses)
+ mode_pulse = new mode_path()
+ mode_pulse.setup_mode_controle(src, active_gizmodes, trigger_callbacks)
+
+/// Activate this gizmode which in turn activates the active gizpulse (you following me here?)
+/datum/gizmodes/proc/activate(atom/movable/holder)
+ if(current_active.affect_timer)
+ if(!COOLDOWN_FINISHED(src, cooldown_timer))
+ return
+
+ COOLDOWN_START(src, cooldown_timer, cooldown_time)
+ current_active.activate(holder, src, interface)
+
+/// Holds some functionaly that is activated and selected by the /gizmodes
+/datum/gizpulse
+ /// If TRUE, put the gizmode into cooldown
+ var/affect_timer = TRUE
+
+/// Activate it do so stuff
+/datum/gizpulse/proc/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ return
+
+/// Changes the currently activate gizpulse and adds a way to activate gizpulses
+/datum/gizpulse/mode_controle
+ affect_timer = FALSE
+
+/// Select how activating the pulzes interacts with the gizpulses
+/datum/gizpulse/mode_controle/proc/setup_mode_controle(datum/gizmodes/master, list/active_gizmodes, list/trigger_callbacks)
+ return
+
+/// Adds a puzzle for every possible made to select it, and a single wire to activate the selected mode
+/datum/gizpulse/mode_controle/select_mode/setup_mode_controle(datum/gizmodes/master, list/active_gizmodes, list/trigger_callbacks)
+ for(var/active in active_gizmodes)
+ trigger_callbacks += VARSET_CALLBACK(master, current_active, active)
+ trigger_callbacks += CALLBACK(master, PROC_REF(activate))
+
+/// Adds a puzzle to cycle to the next gizpulse, and a puzzle to activate the currently active mode
+/datum/gizpulse/mode_controle/cycle_mode/setup_mode_controle(datum/gizmodes/master, list/active_gizmodes, list/trigger_callbacks)
+ trigger_callbacks += CALLBACK(src, PROC_REF(cycle_mode), master)
+ trigger_callbacks += CALLBACK(master, PROC_REF(activate))
+
+/datum/gizpulse/mode_controle/cycle_mode/proc/cycle_mode(datum/gizmodes/master, atom/movable/holder)
+ // Move to the next mode in the list (and loop back to 1 if needed)
+ master.current_active = master.active_gizmodes[((master.active_gizmodes.Find(master.current_active)) % (master.active_gizmodes.len)) + 1]
+
+/// Adds a puzzle for every gizpulse that just immediately activates that gizpulse
+/datum/gizpulse/mode_controle/direct_activate/setup_mode_controle(datum/gizmodes/master, list/active_gizmodes, list/trigger_callbacks)
+ for(var/active in active_gizmodes)
+ trigger_callbacks += CALLBACK(src, PROC_REF(switch_and_activate), master, active)
+
+/datum/gizpulse/mode_controle/direct_activate/proc/switch_and_activate(datum/gizmodes/master, datum/gizpulse/active, atom/movable/holder)
+ master.current_active = active
+ master.activate(holder)
+
+/// Adds a single wire, that cycles and then activates
+/datum/gizpulse/mode_controle/cycle_mode/activate/setup_mode_controle(datum/gizmodes/master, list/active_gizmodes, list/trigger_callbacks)
+ trigger_callbacks += CALLBACK(src, PROC_REF(cycle_mode), master)
+
+/datum/gizpulse/mode_controle/cycle_mode/activate/cycle_mode(datum/gizmodes/master, atom/movable/holder)
+ ..()
+ master.activate(holder)
diff --git a/code/modules/research/gizmo/gizmodes/gizbad.dm b/code/modules/research/gizmo/gizmodes/gizbad.dm
new file mode 100644
index 000000000000..07fb2b5c66ac
--- /dev/null
+++ b/code/modules/research/gizmo/gizmodes/gizbad.dm
@@ -0,0 +1,131 @@
+/// A bunch of bad effects that can maim or kill you (for science!!!)
+/datum/gizmodes/bad
+ possible_active_modes = list(
+ /datum/gizpulse/explode = 1,
+ /datum/gizpulse/explode/fire = 1,
+ /datum/gizpulse/dispense/robot_spider = 1,
+ /datum/gizpulse/thrower = 1,
+ /datum/gizpulse/thrower/grenade = 1,
+ /datum/gizpulse/radiation_pulse = 1,
+ /datum/gizpulse/bone_breaker = 1,
+ )
+
+ guaranteed_active_gizmodes = list(
+ /datum/gizpulse/ominous, //it may warn you, it may immediately explode. Who knows!
+ )
+
+ min_modes = 1
+ max_modes = 2
+
+ cooldown_time = 5 SECONDS
+
+/datum/gizpulse/explode
+ var/range_heavy = 0
+ var/range_medium = 1
+ var/range_light = 3
+ var/range_flame = 0
+
+/datum/gizpulse/explode/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ explosion(holder, range_heavy, range_medium, range_light, range_flame)
+
+/datum/gizpulse/explode/fire
+ range_flame = 5
+
+/datum/gizpulse/dispense/robot_spider
+ possible_objects = list(
+ /mob/living/basic/spider/robot = 1,
+ )
+
+/mob/living/basic/spider/robot
+ name = "robot spider"
+ desc = "Beep boop, the robot spider said."
+ icon_state = "robot"
+ mob_biotypes = MOB_ROBOTIC|MOB_BUG
+
+ speed = 5
+ maxHealth = 50
+ health = 50
+ obj_damage = 10
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+
+ ai_controller = /datum/ai_controller/basic_controller/giant_spider
+
+/mob/living/basic/spider/robot/death(gibbed)
+ . = ..()
+
+ explosion(src, 0, 0, 2)
+ if(prob(80))
+ qdel(src)
+
+/mob/living/basic/spider/robot/emp_act(severity)
+ . = ..()
+
+ death() //very sensitive spider robot antennae makes it die fast to emp
+
+/datum/gizpulse/thrower
+ /// Weighted list of items we can throw
+ var/list/throwables = list(
+ /obj/item/knife/kitchen = 1,
+ /obj/item/shard = 1,
+
+ )
+ /// Path of item to throw
+ var/throwing_path
+
+/datum/gizpulse/thrower/New()
+ . = ..()
+ throwing_path = pick_weight(throwables)
+
+/datum/gizpulse/thrower/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ var/obj/item/item = new throwing_path (get_turf(holder))
+
+ var/list/targets = list()
+ for(var/mob/living/victims in oview(5, holder))
+ targets += victims
+
+ if(!targets.len)
+ targets += get_edge_target_turf(holder, GLOB.alldirs)
+ item.throw_at(pick(targets), 20, 3)
+ modify(item)
+
+/// Do some extra modifications if need be
+/datum/gizpulse/thrower/proc/modify(obj/item/item)
+ return
+
+/datum/gizpulse/ominous/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ holder.audible_message(span_hear("You hear an ominous hum."))
+
+/datum/gizpulse/thrower/grenade
+ throwables = list(
+ /obj/item/grenade/iedcasing = 3,
+ /obj/item/grenade/chem_grenade/cleaner = 2,
+ /obj/item/grenade/smokebomb = 2,
+ /obj/item/grenade/syndieminibomb/concussion = 1,
+ /obj/item/grenade/frag = 1,
+ /obj/item/grenade/chem_grenade/teargas = 1,
+ /obj/item/grenade/chem_grenade/facid = 1,
+ /obj/item/grenade/chem_grenade/clf3 = 1,
+ )
+
+/datum/gizpulse/thrower/grenade/modify(obj/item/item)
+ var/obj/item/grenade/regret = item
+ regret.arm_grenade()
+
+/datum/gizpulse/bone_breaker/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ var/list/victims = list()
+ for(var/mob/living/loser in orange(1, holder))
+ victims += loser
+
+ if(!victims.len)
+ return
+
+ var/mob/living/victim = pick(victims)
+ holder.forceMove(get_turf(victim))
+ playsound(victim, 'sound/effects/wounds/crack2.ogg', 70, TRUE)
+
+ victim.apply_damage(60, BRUTE, wound_bonus = 100, sharpness = NONE)
+ victim.Stun(2 SECONDS)
+ victim.Knockdown(5 SECONDS)
+
+ victim.emote("scream")
diff --git a/code/modules/research/gizmo/gizmodes/gizcopier.dm b/code/modules/research/gizmo/gizmodes/gizcopier.dm
new file mode 100644
index 000000000000..50fe333f1050
--- /dev/null
+++ b/code/modules/research/gizmo/gizmodes/gizcopier.dm
@@ -0,0 +1,86 @@
+/// Scan and copies the nearest object. The copy copies no functionality, only visually
+/datum/gizmodes/copier
+ guaranteed_active_gizmodes = list(
+ /datum/gizpulse/scan,
+ /datum/gizpulse/copy,
+ /datum/gizpulse/erase,
+ )
+
+ /// Weakref of what is marked to copy
+ var/datum/weakref/marked
+ /// List of copies currently in circulation
+ var/list/copies = list()
+ /// The max amount of copies that can exist at a time
+ var/max_copies = 50
+
+/// Scan the nearest mob/object
+/datum/gizpulse/scan/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ if(!istype(master, /datum/gizmodes/copier))
+ return
+
+ var/datum/gizmodes/copier/copier = master
+
+ for(var/atom/movable/candidate in oview(1, holder))
+ if(candidate.anchored || HAS_TRAIT(candidate, TRAIT_UNDERFLOOR)) // skips most undertile and hidden objects
+ continue
+
+ copier.marked = WEAKREF(candidate)
+ playsound(src, 'sound/items/weapons/flash.ogg', 80) //give some feedback that *something* happened
+ return
+
+/// Make a copy of whatever you previously scanned
+/datum/gizpulse/copy
+ /// Reference to the gizmodes' copies list so we can track deletions
+ var/list/copies
+
+/datum/gizpulse/copy/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ if(!istype(master, /datum/gizmodes/copier))
+ return
+
+ var/datum/gizmodes/copier/copier = master
+ copies = copier.copies
+
+ var/atom/movable/object_to_copy = copier.marked?.resolve()
+
+ if(!object_to_copy)
+ return
+
+ // So you can pick up copied items but not copied structures and mobs
+ var/obj/item/copy = new /obj/item/gizmo_copy (get_turf(holder))
+ if(!isitem(object_to_copy))
+ copy.interaction_flags_item = NONE //so you cant pick it up anymore
+
+ copy.appearance = object_to_copy
+ copy.density = object_to_copy.density
+
+ copies.Add(copy)
+
+ if(copies.len > copier.max_copies)
+ var/obj/item/gizmo_copy/copy_to_delete = copies[1]
+ qdel(copy_to_delete) //it gets removed from the list on del
+
+ RegisterSignal(copy, COMSIG_QDELETING, PROC_REF(remove_from_list))
+
+/// Remove a copy from a list if they're deleted
+/datum/gizpulse/copy/proc/remove_from_list(datum/source)
+ SIGNAL_HANDLER
+
+ copies.Remove(source)
+
+/// Wipe all current copies
+/datum/gizpulse/erase/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ if(!istype(master, /datum/gizmodes/copier))
+ return
+
+ var/datum/gizmodes/copier/copier = master
+
+ for(var/copy in copier.copies)
+ qdel(copy)
+
+ copier.copies.Cut()
+
+/obj/item/gizmo_copy/emp_act(severity)
+ . = ..()
+
+ do_sparks(2, FALSE, "gizmo copy")
+ qdel(src)
diff --git a/code/modules/research/gizmo/gizmodes/gizlectric.dm b/code/modules/research/gizmo/gizmodes/gizlectric.dm
new file mode 100644
index 000000000000..9d8d90a52df8
--- /dev/null
+++ b/code/modules/research/gizmo/gizmodes/gizlectric.dm
@@ -0,0 +1,166 @@
+/// Suck power and shoot it out again
+/datum/gizmodes/electric
+ possible_active_modes = list(
+ /datum/gizpulse/electric/emp = 1,
+ /datum/gizpulse/electric/discharge = 1,
+ /datum/gizpulse/electric/charge = 1,
+ /datum/gizpulse/electric/revive = 1,
+ )
+
+ guaranteed_active_gizmodes = list(
+ GIZMO_PICK_ONE = list(
+ /datum/gizpulse/electric/draw = 1,
+ /datum/gizpulse/electric/passive_charge = 1,
+ )
+ )
+
+ mode_pulses = list(
+ /datum/gizpulse/mode_controle/select_mode,
+ /datum/gizpulse/mode_controle/cycle_mode,
+ /datum/gizpulse/mode_controle/direct_activate,
+ )
+
+ min_modes = 3
+ max_modes = 4
+
+ cooldown_time = 6 SECONDS
+
+ /// The internal power cell
+ var/obj/item/stock_parts/power_store/battery/gizmo/power
+
+/datum/gizmodes/electric/activate(atom/movable/holder)
+ if(!power)
+ power = new(holder)
+
+ return ..()
+
+/// Batter used in the gizmo device
+/obj/item/stock_parts/power_store/battery/gizmo
+ charge = STANDARD_BATTERY_CHARGE * 0.1 //you gotta work for your fun
+
+/// Get the total charge
+/datum/gizpulse/electric/proc/get_power(atom/movable/holder, datum/gizmodes/master)
+ if(istype(master, /datum/gizmodes/electric))
+ var/datum/gizmodes/electric/electromaster = master
+ return electromaster.power.charge()
+ return 0
+
+/// Use some charge
+/datum/gizpulse/electric/proc/use_power(amount, atom/movable/holder, datum/gizmodes/master)
+ if(istype(master, /datum/gizmodes/electric))
+ var/datum/gizmodes/electric/electromaster = master
+ return electromaster.power.use(amount)
+ return FALSE
+
+/// Do an EMP blast
+/datum/gizpulse/electric/emp
+ /// Min power to do an EMP
+ var/min_power = STANDARD_CELL_CHARGE
+
+/// Do an EMP blast using the cell of our gizmode
+/datum/gizpulse/electric/emp/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ var/charge = get_power(holder, master)
+ if(charge < min_power)
+ return
+ /// Max charge is 1MJ, standard cell charge is 10kJ. A full charge EMP is 1000 / 10 = 100. 100 / 15 is 6 tiles of light emp range
+ empulse(get_turf(holder), floor(charge / STANDARD_CELL_CHARGE / 40), floor(charge / STANDARD_CELL_CHARGE / 15), holder)
+ use_power(charge, holder, master)
+
+/// Shoot our current charge away as lightning
+/datum/gizpulse/electric/discharge/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ var/charge = get_power(holder, master)
+ if(charge)
+ return
+
+ tesla_zap(holder, power = charge, zap_flags = ZAP_GIZMO_FLAGS)
+ use_power(charge, holder, master)
+
+/// Look for an object with a cell, and CHARGE IT!!!
+/datum/gizpulse/electric/charge/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ if(!istype(master, /datum/gizmodes/electric))
+ return
+
+ var/datum/gizmodes/electric/electromaster = master
+
+ for(var/atom/movable/power_source in oview(4, holder))
+ if(!power_source.get_cell())
+ continue
+
+ var/obj/item/stock_parts/power_store/cell = power_source.get_cell()
+
+ var/charge = electromaster.power.charge() - cell.used_charge()
+ if(charge <= 0)
+ continue
+
+ cell.give(charge)
+ electromaster.power.use(charge)
+
+ holder.Beam(power_source, icon_state = "g_beam", time = 5)
+ playsound(power_source, 'sound/effects/magic/ethereal_exit.ogg', 40)
+ return
+
+/// Revive people in a radius like the revival surgery
+/datum/gizpulse/electric/revive
+ /// The charge cost for a defibrillation pulse
+ var/defib_cost = STANDARD_CELL_CHARGE * 0.5
+
+/datum/gizpulse/electric/revive/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ if(!istype(master, /datum/gizmodes/electric))
+ return
+
+ var/datum/gizmodes/electric/electromaster = master
+
+ if(electromaster.power.charge() < defib_cost)
+ return
+
+ electromaster.power.use(defib_cost)
+
+ for(var/mob/living/dead in orange(holder, 3))
+ if(dead.stat == DEAD)
+ if(dead.revive(excess_healing = 5))
+ if(iscarbon(dead)) //still want it to work for simple/basicmobs too
+ var/mob/living/carbon/dead_but_carbon = dead
+ dead_but_carbon.set_heartattack(FALSE)
+ dead.adjust_oxy_loss(-200)
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(playsound), dead, 'sound/machines/defib/defib_zap.ogg', 60), 3 SECONDS)
+
+ new /obj/effect/temp_visual/circle_wave(get_turf(holder), COLOR_YELLOW)
+ playsound(holder, 'sound/machines/defib/defib_charge.ogg', 80)
+
+/// Look for the nearest power-containing object and suck the power out
+/datum/gizpulse/electric/draw/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ if(!istype(master, /datum/gizmodes/electric))
+ return
+
+ var/datum/gizmodes/electric/electromaster = master
+
+ for(var/atom/movable/power_source in oview(4, holder))
+ if(!power_source.get_cell())
+ continue
+
+ var/obj/item/stock_parts/power_store/cell = power_source.get_cell()
+
+ var/charge = cell.charge() - electromaster.power.used_charge()
+ if(charge <= 0)
+ continue
+
+ cell.use(charge)
+ electromaster.power.give(charge)
+
+ holder.Beam(power_source, icon_state = "r_beam", time = 5)
+ playsound(power_source, 'sound/effects/magic/ethereal_exit.ogg', 40)
+ return
+
+/// Give a bit of charge, FOR FREE
+/datum/gizpulse/electric/passive_charge
+ /// How much we magically charge from nothing per pulse
+ var/recharge = STANDARD_BATTERY_CHARGE * 0.001
+
+/datum/gizpulse/electric/passive_charge/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ if(!istype(master, /datum/gizmodes/electric))
+ return
+
+ var/datum/gizmodes/electric/electromaster = master
+ electromaster.power.give(recharge)
+
+ playsound(holder, 'sound/effects/magic/charge.ogg', 50, TRUE)
diff --git a/code/modules/research/gizmo/gizmodes/gizmisc.dm b/code/modules/research/gizmo/gizmodes/gizmisc.dm
new file mode 100644
index 000000000000..ae3ad6ce9fa1
--- /dev/null
+++ b/code/modules/research/gizmo/gizmodes/gizmisc.dm
@@ -0,0 +1,51 @@
+/// Make the holder move by adding a movement element. Signal is for aestethic interactions mostly
+/datum/gizmodes/mover
+ guaranteed_active_gizmodes = list(/datum/gizpulse/start_moving = 1, /datum/gizpulse/stop_moving = 1)
+
+/datum/gizpulse/start_moving/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ holder.AddElement(/datum/element/moving_randomly)
+ SEND_SIGNAL(holder, COMSIG_GIZMO_START_MOVING, src)
+
+/datum/gizpulse/stop_moving/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ holder.RemoveElement(/datum/element/moving_randomly)
+ SEND_SIGNAL(holder, COMSIG_GIZMO_STOP_MOVING, src)
+
+/// Start glowing
+/datum/gizmodes/lights
+ guaranteed_active_gizmodes = list(/datum/gizpulse/lights_on, /datum/gizpulse/lights_off)
+ mode_pulses = list(
+ /datum/gizpulse/mode_controle/direct_activate,
+ /datum/gizpulse/mode_controle/cycle_mode/activate,
+ )
+
+/datum/gizpulse/lights_on/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ SEND_SIGNAL(holder, COMSIG_GIZMO_ON_STATE)
+
+ holder.set_light(
+ l_range = 3,
+ l_power = 2,
+ l_color = LIGHT_COLOR_INTENSE_RED,
+ l_on = TRUE,
+ )
+
+/datum/gizpulse/lights_off/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ SEND_SIGNAL(holder, COMSIG_GIZMO_OFF_STATE)
+ holder.set_light_on(FALSE)
+
+/// Gives a voice hint or changes the voices language for use with a voice interface (i mean you give this to a wire interface or other but it then gives you
+/// the info to use a voice interface)
+/datum/gizmodes/voice
+ guaranteed_active_gizmodes = list(/datum/gizpulse/voice_hint, /datum/gizpulse/language_change)
+ mode_pulses = list(
+ /datum/gizpulse/mode_controle/direct_activate,
+ )
+
+/// Spit out a hint for using the voice interface
+/datum/gizpulse/voice_hint/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ var/datum/component/gizmo_voice/voice = holder.GetComponent(/datum/component/gizmo_voice)
+
+ holder.say(voice.active_words.Join(" "))
+
+/// Pick a different language
+/datum/gizpulse/language_change/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ holder.grant_random_uncommon_language("gizmo")
diff --git a/code/modules/research/gizmo/gizmodes/gizmood.dm b/code/modules/research/gizmo/gizmodes/gizmood.dm
new file mode 100644
index 000000000000..ed9133f7e639
--- /dev/null
+++ b/code/modules/research/gizmo/gizmodes/gizmood.dm
@@ -0,0 +1,41 @@
+/// Send out mood pulses, good or bad
+/datum/gizmodes/mood_pulser
+ guaranteed_active_gizmodes = list(/datum/gizpulse/mood_pulser/positive, /datum/gizpulse/mood_pulser/negative)
+ possible_active_modes = list(
+ /datum/gizpulse/radiation_pulse = 1,
+ )
+ min_modes = 0
+ max_modes = 1
+
+/// Send out a mood pulse
+/datum/gizpulse/mood_pulser
+ /// Mood event to give out
+ var/datum/mood_event/mood
+ /// Color of the ring effect (god i love the ring effect)
+ var/ring_color
+ /// Range of the mood pulse
+ var/range = 14
+
+/datum/gizpulse/mood_pulser/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ mood_pulse(holder)
+
+/// Send a mood pulse to a range
+/datum/gizpulse/mood_pulser/proc/mood_pulse(atom/movable/holder)
+ new /obj/effect/temp_visual/circle_wave(get_turf(holder), ring_color)
+ for(var/mob/living/carbon/human/human in urange(range, holder))
+ human.add_mood_event("gizmo_mood_pulse", mood)
+
+/// Make a positive mood pulse
+/datum/gizpulse/mood_pulser/positive
+ mood = /datum/mood_event/gizmo_positive
+ ring_color = COLOR_GREEN
+
+/// Make a negative mood pulse
+/datum/gizpulse/mood_pulser/negative
+ mood = /datum/mood_event/gizmo_negative
+ ring_color = COLOR_RED
+
+/datum/gizpulse/radiation_pulse/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ new /obj/effect/temp_visual/circle_wave(get_turf(holder), COLOR_GREEN)
+
+ radiation_pulse(holder, max_range = 5, threshold = RAD_LIGHT_INSULATION, chance = 30)
diff --git a/code/modules/research/gizmo/gizmodes/gizmopper.dm b/code/modules/research/gizmo/gizmodes/gizmopper.dm
new file mode 100644
index 000000000000..560fc3b8cfaa
--- /dev/null
+++ b/code/modules/research/gizmo/gizmodes/gizmopper.dm
@@ -0,0 +1,92 @@
+/// Gizmo mode that regenerates, cycles and expells reagents in different functions
+/datum/gizmodes/mopper
+ possible_active_modes = list(
+ /datum/gizpulse/wet_tiles/fluid_circle/small = 1,
+ /datum/gizpulse/wet_tiles/fluid_circle/medium = 1,
+ /datum/gizpulse/wet_tiles/fluid_circle/large = 1,
+ /datum/gizpulse/fluid_smoke = 1,
+ /datum/gizpulse/swap_reagent = 1,
+ )
+
+ min_modes = 3
+ max_modes = 5
+
+ /// Reagents that can be selected
+ var/list/reagents = list(
+ /datum/reagent/water,
+ /datum/reagent/toxin/acid,
+ /datum/reagent/consumable/salt,
+ /datum/reagent/uranium/radium,
+ )
+ /// Reference to the reagent holder. Preferably access it from the holder instead, but some procs dont like that (process())
+ var/datum/reagents/reagent_holder
+ /// Reagent that is being generated right now
+ var/active_reagent = /datum/reagent/water
+ /// Max volume of the reagent holder we hand out
+ var/max_volume = 50
+ /// Amount of reagents we regenerate per second
+ var/regeneration_speed = 2
+ /// How many reagents we grab from get_random_reagent_id
+ var/random_reagents_to_add = 1
+ /// Flags to pass to the reagent holder
+ var/reagent_flags = AMOUNT_VISIBLE
+
+/datum/gizmodes/mopper/New()
+ . = ..()
+
+ for(var/i in 1 to random_reagents_to_add)
+ reagents += get_random_reagent_id()
+
+/datum/gizmodes/mopper/activate(atom/movable/holder)
+ if(!holder.reagents)
+ holder.create_reagents(max_volume, reagent_flags)
+ holder.reagents.add_reagent(active_reagent, max_volume)
+ reagent_holder = holder.reagents
+ START_PROCESSING(SSdcs, src)
+ return ..()
+
+/datum/gizmodes/mopper/process(seconds_per_tick)
+ reagent_holder.add_reagent(active_reagent, regeneration_speed * seconds_per_tick)
+
+/// Wet the surounding tiles
+/datum/gizpulse/wet_tiles/activate(atom/movable/holder, datum/gizmodes/mopper/master, datum/gizmo_interface/interface)
+ var/list/tiles = get_tiles(holder)
+ for(var/turf/open/tile in tiles)
+ tile.expose_reagents(holder.reagents.reagent_list, holder.reagents)
+
+ holder.reagents.expose(tile, TOUCH, 1, master.max_volume / tiles.len)
+ holder.reagents.remove_reagent(master.active_reagent, master.max_volume / tiles.len)
+
+/// Get the tiles to wet
+/datum/gizpulse/wet_tiles/proc/get_tiles(atom/movable/holder)
+ return
+
+/// Dump reagents in a circle
+/datum/gizpulse/wet_tiles/fluid_circle
+ /// Size, in a circle, around the holder for wetting
+ var/size = 0
+
+/datum/gizpulse/wet_tiles/fluid_circle/get_tiles(atom/movable/holder)
+ return RANGE_TURFS(size, holder)
+
+/// In a small circle
+/datum/gizpulse/wet_tiles/fluid_circle/small
+ size = 1
+
+/// In a medium circle
+/datum/gizpulse/wet_tiles/fluid_circle/medium
+ size = 2
+
+/// In a large circle
+/datum/gizpulse/wet_tiles/fluid_circle/large
+ size = 3
+
+/// Make a smoke cloud of our fluid
+/datum/gizpulse/fluid_smoke/activate(atom/movable/holder, datum/gizmo_interface/interface)
+ do_chem_smoke(3, holder, get_turf(holder), carry = holder.reagents)
+ holder.reagents.clear_reagents()
+
+/// Select different reagents
+/datum/gizpulse/swap_reagent/activate(atom/movable/holder, datum/gizmodes/mopper/master, datum/gizmo_interface/interface)
+ holder.reagents.clear_reagents()
+ master.active_reagent = pick(master.reagents - master.active_reagent) //maybe also add a cycle one instead of random
diff --git a/code/modules/research/gizmo/gizmodes/gizporter.dm b/code/modules/research/gizmo/gizmodes/gizporter.dm
new file mode 100644
index 000000000000..e28ca1580573
--- /dev/null
+++ b/code/modules/research/gizmo/gizmodes/gizporter.dm
@@ -0,0 +1,45 @@
+/// Teleports itself and/or others
+/datum/gizmodes/teleporter
+ possible_active_modes = list(
+ /datum/gizpulse/teleport/self = 1,
+ /datum/gizpulse/teleport/other = 1,
+ /datum/gizpulse/teleport/other/and_self = 1,
+ )
+
+ min_modes = 2
+ max_modes = 3
+
+/// Teleport... stuff...
+/datum/gizpulse/teleport
+ /// Min distance to teleport
+ var/offset_min = 5
+ /// Max distance to teleport
+ var/offset_max = 15
+
+/// Teleport... stuff...
+/datum/gizpulse/teleport/activate(atom/movable/holder, datum/gizmodes/master, datum/gizmo_interface/interface)
+ var/list/targets = get_teleport_targets(holder)
+ var/range = rand(offset_min, offset_max)
+ var/dir = pick(GLOB.alldirs)
+
+ for(var/atom/movable/target as anything in targets)
+ var/turf/new_turf = get_ranged_target_turf(target, dir, range)
+ do_teleport(target, new_turf, asoundin = 'sound/effects/cartoon_sfx/cartoon_pop.ogg', channel = TELEPORT_CHANNEL_BLUESPACE)
+
+/datum/gizpulse/teleport/proc/get_teleport_targets(atom/movable/holder)
+ return list()
+
+/// Teleport yourself
+/datum/gizpulse/teleport/self/get_teleport_targets(atom/movable/holder)
+ return list(holder)
+
+/// Teleport someone else
+/datum/gizpulse/teleport/other/get_teleport_targets(atom/movable/holder)
+ . = list()
+ for(var/mob/living/living in view(2, holder))
+ . += living
+
+/// Teleport yourself and someone else
+/datum/gizpulse/teleport/other/and_self/get_teleport_targets(atom/movable/holder)
+ return ..() + holder
+
diff --git a/code/modules/research/gizmo/gizpuzzle.dm b/code/modules/research/gizmo/gizpuzzle.dm
new file mode 100644
index 000000000000..18763db7a564
--- /dev/null
+++ b/code/modules/research/gizmo/gizpuzzle.dm
@@ -0,0 +1,95 @@
+/// Holds the puzzle sequences, receives the pulses, decides if theyre correct, and gives feedback and calls the right callbacks when it does
+/datum/gizmo_puzzle
+ /// The wires we need to pulse for cracking the code
+ var/list/cryptic_pulse = list(
+ GIZMO_PULSE_1,
+ GIZMO_PULSE_2,
+ GIZMO_PULSE_3,
+ GIZMO_PULSE_4,
+ GIZMO_PULSE_5,
+ GIZMO_PULSE_6,
+ GIZMO_PULSE_7,
+ GIZMO_PULSE_8,
+ )
+
+ /// How long a code sequence can be
+ var/code_length = 3
+ /// The codes that got generated, formatted as (1 = list(CRYPTIC_WIRE_5, CRYPTIC_WIRE_3, CRYPTIC_WIRE_7, 2 = list(...)))
+ var/list/code_sequences
+ /// The current sequence we're on. Will reset if it doesn't match anything
+ var/list/current_sequence = list()
+ /// List of callbacks that the solutions will call on succes
+ var/list/solution_callbacks
+
+ /// For if you want something to happen on merely being pulsed. If null, simply ping, bleep and creak or whatever as feedback
+ var/datum/callback/pulsed_callback
+
+ COOLDOWN_DECLARE(feedback_cooldown)
+ /// So the ping buzz feedback doesnt spam too much
+ var/feedback_cooldown_time = 0.2 SECONDS
+
+/datum/gizmo_puzzle/New(datum/callback/pulsed)
+ if(pulsed)
+ pulsed_callback = pulsed
+ else
+ pulsed_callback = CALLBACK(src, PROC_REF(default_on_pulsed))
+ return ..()
+
+/// Make up a sequence
+/datum/gizmo_puzzle/proc/generate_code_sequences(list/solution_callbacks)
+ src.solution_callbacks = solution_callbacks
+ code_sequences = list()
+
+ for(var/i in 1 to solution_callbacks.len)
+ code_sequences += list(list())
+ for(var/j in 1 to code_length)
+ code_sequences[i] += pick(cryptic_pulse)
+
+/// Whenever a puzzle attempt is made
+/datum/gizmo_puzzle/proc/on_pulse(pulse_number, mob/living/user, atom/movable/holder, no_feedback = FALSE)
+ current_sequence += cryptic_pulse[pulse_number]
+ . = GIZMO_PUZZLE_CORRECT
+
+ var/succeeded = FALSE
+
+ for(var/i in 1 to code_sequences.len)
+ var/list/a = code_sequences[i]
+ for(var/j in 1 to current_sequence.len)
+ if(current_sequence[j] != a[j])
+ break
+ if(current_sequence.len == j)
+ succeeded = TRUE
+ if(j == a.len)
+ var/datum/callback/callback = solution_callbacks[i]
+ callback.Invoke(holder)
+ current_sequence.Cut()
+ . = GIZMO_PUZZLE_SOLVED
+ break
+
+ if(!succeeded)
+ current_sequence.Cut()
+ . = GIZMO_PUZZLE_WRONG
+
+ pulsed_callback?.Invoke(holder, user, ., no_feedback)
+
+/// Just some feedback so people can start forcing sequences. No feedback if it's done automatically
+/datum/gizmo_puzzle/proc/default_on_pulsed(atom/movable/holder, mob/living/user, solved_type, no_feedback = FALSE)
+ if(!COOLDOWN_FINISHED(src, feedback_cooldown) || !isliving(user) || no_feedback)
+ return
+
+ COOLDOWN_START(src, feedback_cooldown, feedback_cooldown_time)
+
+ switch(solved_type)
+ if(GIZMO_PUZZLE_WRONG)
+ holder.balloon_alert(user, "buzz")
+ playsound(holder, 'sound/machines/buzz/buzz-sigh.ogg', 30, FALSE)
+ if(GIZMO_PUZZLE_CORRECT)
+ holder.balloon_alert(user, "ping")
+ playsound(holder, 'sound/machines/ping.ogg', 30, FALSE)
+ if(GIZMO_PUZZLE_SOLVED)
+ holder.balloon_alert(user, "creak")
+ playsound(holder, 'sound/machines/creak.ogg', 30, FALSE)
+
+/// Sequences can be a bit shorter since you have to constantly type and scream them
+/datum/gizmo_puzzle/voice
+ code_length = 2
diff --git a/code/modules/research/ordnance/_scipaper.dm b/code/modules/research/ordnance/_scipaper.dm
index e103416fd179..453bbe9fbea4 100644
--- a/code/modules/research/ordnance/_scipaper.dm
+++ b/code/modules/research/ordnance/_scipaper.dm
@@ -289,6 +289,14 @@
/// Associative list of which technology the partner might be able to boost and by how much.
var/list/boostable_nodes = list()
+/datum/scientific_partner/New()
+ . = ..()
+ // Convey boosts to their associated nodes so that they can then be passed
+ // to techweb UIs as static data.
+ for(var/node_id in boostable_nodes)
+ var/datum/techweb_node/node = SSresearch.techweb_node_by_id(node_id)
+ node.discount_boosts[TECHWEB_POINT_TYPE_GENERIC] = boostable_nodes[node_id]
+
/datum/scientific_partner/proc/purchase_boost(datum/techweb/purchasing_techweb, datum/techweb_node/node)
var/possible_boost = allowed_to_boost(purchasing_techweb, node.id)
if(!possible_boost)
@@ -301,9 +309,10 @@
return TRUE
/datum/scientific_partner/proc/allowed_to_boost(datum/techweb/purchasing_techweb, node_id)
+ var/datum/techweb_node/boosting_node = SSresearch.techweb_node_by_id(node_id)
if(purchasing_techweb.scientific_cooperation[type] < (boostable_nodes[node_id] * SCIENTIFIC_COOPERATION_PURCHASE_MULTIPLIER)) // Too expensive
return FALSE
- if((TECHWEB_POINT_TYPE_GENERIC in purchasing_techweb.boosted_nodes[node_id]) && (purchasing_techweb.boosted_nodes[node_id][TECHWEB_POINT_TYPE_GENERIC] >= boostable_nodes[node_id])) // Already bought or we have a bigger discount
+ if((boosting_node.discount_boosted) && (boosting_node.discount_boosts[TECHWEB_POINT_TYPE_GENERIC] >= boostable_nodes[node_id])) // Already bought or we have a bigger discount
return FALSE
if(node_id in purchasing_techweb.researched_nodes)
return SCIPAPER_ALREADY_BOUGHT
diff --git a/code/modules/research/part_replacer.dm b/code/modules/research/part_replacer.dm
index b0901cd9b01f..3b022850f654 100644
--- a/code/modules/research/part_replacer.dm
+++ b/code/modules/research/part_replacer.dm
@@ -123,6 +123,7 @@
new /obj/item/stock_parts/micro_laser(src)
new /obj/item/stock_parts/matter_bin(src)
new /obj/item/stock_parts/power_store/cell/high(src)
+ new /obj/item/stock_parts/power_store/battery/high(src)
/obj/item/storage/part_replacer/bluespace/tier2/PopulateContents()
for(var/i in 1 to 10)
@@ -132,6 +133,7 @@
new /obj/item/stock_parts/micro_laser/high(src)
new /obj/item/stock_parts/matter_bin/adv(src)
new /obj/item/stock_parts/power_store/cell/super(src)
+ new /obj/item/stock_parts/power_store/battery/super(src)
/obj/item/storage/part_replacer/bluespace/tier3/PopulateContents()
for(var/i in 1 to 10)
@@ -141,9 +143,21 @@
new /obj/item/stock_parts/micro_laser/ultra(src)
new /obj/item/stock_parts/matter_bin/super(src)
new /obj/item/stock_parts/power_store/cell/hyper(src)
+ new /obj/item/stock_parts/power_store/battery/hyper(src)
/obj/item/storage/part_replacer/bluespace/tier4/PopulateContents()
for(var/i in 1 to 10)
+ new /obj/item/stock_parts/capacitor/quadratic(src)
+ new /obj/item/stock_parts/scanning_module/triphasic(src)
+ new /obj/item/stock_parts/servo/femto(src)
+ new /obj/item/stock_parts/micro_laser/quadultra(src)
+ new /obj/item/stock_parts/matter_bin/bluespace(src)
+ new /obj/item/stock_parts/power_store/cell/bluespace(src)
+ new /obj/item/stock_parts/power_store/battery/bluespace(src)
+ new /obj/item/stack/cable_coil/thirty(src)
+
+/obj/item/storage/part_replacer/bluespace/AdminDebug/PopulateContents()
+ for(var/i in 1 to 40)
new /obj/item/stock_parts/capacitor/quadratic(src)
new /obj/item/stock_parts/scanning_module/triphasic(src)
new /obj/item/stock_parts/servo/femto(src)
diff --git a/code/modules/research/rdconsole.dm b/code/modules/research/rdconsole.dm
index 36c3a43a6034..60fa59075563 100644
--- a/code/modules/research/rdconsole.dm
+++ b/code/modules/research/rdconsole.dm
@@ -243,7 +243,8 @@ Nothing else in the console has ID requirements.
"can_unlock" = stored_research.can_unlock_node(n),
"have_experiments_done" = stored_research.have_experiments_for_node(n),
"tier" = stored_research.tiers[n.id],
- "enqueued_by_user" = enqueued_by_user
+ "enqueued_by_user" = enqueued_by_user,
+ "discount_boosted" = n.discount_boosted
))
// Get experiments and serialize them
@@ -305,6 +306,8 @@ Nothing else in the console has ID requirements.
node_cache[compressed_id]["required_experiments"] = node.required_experiments
if (LAZYLEN(node.discount_experiments))
node_cache[compressed_id]["discount_experiments"] = node.discount_experiments
+ if (LAZYLEN(node.discount_boosts))
+ node_cache[compressed_id]["discount_boosts"] = node.discount_boosts
// Build design cache
var/design_cache = list()
diff --git a/code/modules/research/server.dm b/code/modules/research/server.dm
index 628e0fcfe4af..12dbc25158bb 100644
--- a/code/modules/research/server.dm
+++ b/code/modules/research/server.dm
@@ -46,12 +46,14 @@
return ..()
/obj/machinery/rnd/server/update_icon_state()
+ . = ..()
if(machine_stat & NOPOWER)
icon_state = "[base_icon_state]-off"
+ else if(panel_open)
+ icon_state = "[base_icon_state]-on" + "_t"
else
// "working" will cover EMP'd, disabled, or just broken
icon_state = "[base_icon_state]-[working ? "on" : "halt"]"
- return ..()
/obj/machinery/rnd/server/power_change()
refresh_working()
diff --git a/code/modules/research/techweb/_techweb.dm b/code/modules/research/techweb/_techweb.dm
index ade382a02656..8d492aa4af94 100644
--- a/code/modules/research/techweb/_techweb.dm
+++ b/code/modules/research/techweb/_techweb.dm
@@ -22,8 +22,6 @@
var/list/researched_designs = list()
/// Custom inserted designs like from disks that should survive recalculation.
var/list/custom_designs = list()
- /// Already boosted nodes that can't be boosted again. node id = path of boost object.
- var/list/boosted_nodes = list()
/// Hidden nodes. id = TRUE. Used for unhiding nodes when requirements are met by removing the entry of the node.
var/list/hidden_nodes = list()
/// List of items already deconstructed for research points, preventing infinite research point generation.
@@ -433,9 +431,10 @@
/datum/techweb/proc/boost_techweb_node(datum/techweb_node/node, list/pointlist)
if(!istype(node))
return FALSE
- LAZYINITLIST(boosted_nodes[node.id])
- for(var/point_type in pointlist)
- boosted_nodes[node.id][point_type] = max(boosted_nodes[node.id][point_type], pointlist[point_type])
+ LAZYINITLIST(node.discount_boosts)
+ for(var/point_type in pointlist) // Essentially applies the greater boost(s) between the newer and any existing.
+ node.discount_boosts[point_type] = max(node.discount_boosts[point_type], pointlist[point_type])
+ node.discount_boosted = TRUE
unhide_node(node)
update_node_status(node)
return TRUE
diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm
index 104e475b7e58..0eccb61c4ec3 100644
--- a/code/modules/research/techweb/_techweb_node.dm
+++ b/code/modules/research/techweb/_techweb_node.dm
@@ -36,6 +36,13 @@
var/list/required_experiments = list()
/// If completed, these experiments give a specific point amount discount to the node.
var/list/discount_experiments = list()
+ /// Boost quantities from non-experiment sources (i.e., toxins papers).
+ /// Indexed by point type to boost amount (with only one boost per point type).
+ var/list/discount_boosts = list()
+ /// Boolean indicating whether or not this node is boosted by non-experiments.
+ /// This will need to be changed to a list of point types boosted if boosts
+ /// should ever need to vary by point type.
+ var/discount_boosted = FALSE
/// When this node is completed, allows these experiments to be performed.
var/list/experiments_to_unlock = list()
/// Whether or not this node should show on the wiki
@@ -92,11 +99,10 @@
if(host.completed_experiments[experiment_type]) //do we have this discount_experiment unlocked?
actual_costs[cost_type] -= discount_experiments[experiment_type]
- if(host.boosted_nodes[id]) // Boosts should be subservient to experiments.
- var/list/boostlist = host.boosted_nodes[id]
- for(var/booster in boostlist)
+ if(discount_boosts && discount_boosted) // Boosts should be subservient to experiments.
+ for(var/booster in discount_boosts)
if(actual_costs[booster])
- actual_costs[booster] = max(actual_costs[booster] - boostlist[booster], 0)
+ actual_costs[booster] = max(actual_costs[booster] - discount_boosts[booster], 0)
return actual_costs
diff --git a/code/modules/research/techweb/nodes/service_nodes.dm b/code/modules/research/techweb/nodes/service_nodes.dm
index c40e15b001cd..c0a07e72c68d 100644
--- a/code/modules/research/techweb/nodes/service_nodes.dm
+++ b/code/modules/research/techweb/nodes/service_nodes.dm
@@ -29,6 +29,7 @@
"razor",
"bucket",
"mop",
+ "wet_floor_sign",
"pushbroom",
"normtrash",
"wirebrush",
diff --git a/code/modules/research/xenobiology/crossbreeding/_status_effects.dm b/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
index 6ed0030a2dd9..a0315544d119 100644
--- a/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
+++ b/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
@@ -5,7 +5,8 @@
/datum/status_effect/rainbow_protection
id = "rainbow_protection"
- duration = 100
+ duration = 10 SECONDS
+ tick_interval = STATUS_EFFECT_NO_TICK
alert_type = /atom/movable/screen/alert/status_effect/rainbow_protection
show_duration = TRUE
@@ -13,14 +14,17 @@
owner.add_traits(list(TRAIT_GODMODE, TRAIT_PACIFISM), TRAIT_STATUS_EFFECT(id))
owner.visible_message(span_warning("[owner] shines with a brilliant rainbow light."),
span_notice("You feel protected by an unknown force!"))
- return ..()
-
-/datum/status_effect/rainbow_protection/tick(seconds_between_ticks)
- owner.add_atom_colour(RANDOM_COLOUR, TEMPORARY_COLOUR_PRIORITY)
- return ..()
+ // okay, now time for the rainbow animation.
+ owner.add_filter("rainbow_protection_[REF(src)]", 2, color_matrix_filter(list(0,0,0, 0,0.75,0, 0,0,1, 0,0.25,0), COLORSPACE_HSL))
+ var/color_filter = owner.get_filter("rainbow_protection_[REF(src)]")
+ animate(color_filter, list("color" = list(0,0,0, 0,0.75,0, 0,0,1, 0.33,0.25,0)), time = 1 SECONDS, loop = -1)
+ animate(list("color" = list(0,0,0, 0,0.75,0, 0,0,1, 0.66,0.25,0)), time = 1 SECONDS)
+ animate(list("color" = list(0,0,0, 0,0.75,0, 0,0,1, 1,0.25,0)), time = 1 SECONDS)
+ animate(list("color" = list(0,0,0, 0,0.75,0, 0,0,1, 0,0.25,0)), time = 0) // IMPORTANT!
+ return TRUE
/datum/status_effect/rainbow_protection/on_remove()
- owner.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY)
+ owner.remove_filter("rainbow_protection_[REF(src)]")
owner.remove_traits(list(TRAIT_GODMODE, TRAIT_PACIFISM), TRAIT_STATUS_EFFECT(id))
owner.visible_message(span_notice("[owner] stops glowing, the rainbow light fading away."),
span_warning("You no longer feel protected..."))
@@ -32,12 +36,12 @@
/datum/status_effect/slimeskin
id = "slimeskin"
- duration = 300
+ duration = 30 SECONDS
alert_type = /atom/movable/screen/alert/status_effect/slimeskin
show_duration = TRUE
/datum/status_effect/slimeskin/on_apply()
- owner.add_atom_colour("#3070CC", TEMPORARY_COLOUR_PRIORITY)
+ owner.add_atom_colour(color_transition_filter("#3070CC", SATURATION_OVERRIDE), TEMPORARY_COLOUR_PRIORITY)
if(ishuman(owner))
var/mob/living/carbon/human/H = owner
H.physiology.damage_resistance += 10
@@ -168,7 +172,7 @@
owner.ckey = originalmind.key
if(clone)
clone.unequip_everything()
- qdel(clone)
+ QDEL_NULL(clone)
/atom/movable/screen/alert/status_effect/clone_decay
name = "Clone Decay"
@@ -202,7 +206,7 @@
/datum/status_effect/bloodchill
id = "bloodchill"
- duration = 100
+ duration = 10 SECONDS
alert_type = /atom/movable/screen/alert/status_effect/bloodchill
/datum/status_effect/bloodchill/on_apply()
@@ -210,7 +214,7 @@
return ..()
/datum/status_effect/bloodchill/tick(seconds_between_ticks)
- if(prob(50))
+ if(SPT_PROB(50, seconds_between_ticks))
owner.adjust_fire_loss(2)
/datum/status_effect/bloodchill/on_remove()
@@ -218,7 +222,7 @@
/datum/status_effect/bonechill
id = "bonechill"
- duration = 80
+ duration = 8 SECONDS
alert_type = /atom/movable/screen/alert/status_effect/bonechill
/datum/status_effect/bonechill/on_apply()
@@ -226,13 +230,14 @@
return ..()
/datum/status_effect/bonechill/tick(seconds_between_ticks)
- if(prob(50))
- owner.adjust_fire_loss(1)
- owner.set_jitter_if_lower(6 SECONDS)
- owner.adjust_bodytemperature(-10)
- if(ishuman(owner))
- var/mob/living/carbon/human/humi = owner
- humi.adjust_coretemperature(-10)
+ if(!SPT_PROB(50, seconds_between_ticks))
+ return
+ owner.adjust_fire_loss(1)
+ owner.set_jitter_if_lower(6 SECONDS)
+ owner.adjust_bodytemperature(-10)
+ if(ishuman(owner))
+ var/mob/living/carbon/human/humi = owner
+ humi.adjust_coretemperature(-10)
/datum/status_effect/bonechill/on_remove()
owner.remove_movespeed_modifier(/datum/movespeed_modifier/status_effect/bonechill)
@@ -249,7 +254,7 @@
alert_type = null
/datum/status_effect/rebreathing/tick(seconds_between_ticks)
- owner.adjust_oxy_loss(-6, 0) //Just a bit more than normal breathing.
+ owner.adjust_oxy_loss(-6 * seconds_between_ticks) //Just a bit more than normal breathing.
///////////////////////////////////////////////////////
//////////////////CONSUMING EXTRACTS///////////////////
@@ -259,7 +264,8 @@
id = "firecookie"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 100
+ duration = 10 SECONDS
+ tick_interval = STATUS_EFFECT_NO_TICK
/datum/status_effect/firecookie/on_apply()
ADD_TRAIT(owner, TRAIT_RESISTCOLD, TRAIT_STATUS_EFFECT(id))
@@ -273,7 +279,7 @@
id = "watercookie"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 100
+ duration = 10 SECONDS
/datum/status_effect/watercookie/on_apply()
ADD_TRAIT(owner, TRAIT_NO_SLIP_WATER, TRAIT_STATUS_EFFECT(id))
@@ -290,7 +296,8 @@
id = "metalcookie"
status_type = STATUS_EFFECT_REFRESH
alert_type = null
- duration = 100
+ duration = 10 SECONDS
+ tick_interval = STATUS_EFFECT_NO_TICK
/datum/status_effect/metalcookie/on_apply()
if(ishuman(owner))
@@ -307,7 +314,8 @@
id = "sparkcookie"
status_type = STATUS_EFFECT_REFRESH
alert_type = null
- duration = 300
+ duration = 30 SECONDS
+ tick_interval = STATUS_EFFECT_NO_TICK
var/original_coeff
/datum/status_effect/sparkcookie/on_apply()
@@ -326,7 +334,8 @@
id = "toxincookie"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 600
+ duration = 1 MINUTES
+ tick_interval = STATUS_EFFECT_NO_TICK
/datum/status_effect/toxincookie/on_apply()
ADD_TRAIT(owner, TRAIT_TOXINLOVER, TRAIT_STATUS_EFFECT(id))
@@ -339,7 +348,8 @@
id = "timecookie"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 600
+ duration = 1 MINUTES
+ tick_interval = STATUS_EFFECT_NO_TICK
/datum/status_effect/timecookie/on_apply()
owner.add_actionspeed_modifier(/datum/actionspeed_modifier/timecookie)
@@ -353,7 +363,7 @@
id = "lovecookie"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 300
+ duration = 30 SECONDS
/datum/status_effect/lovecookie/tick(seconds_between_ticks)
if(owner.stat != CONSCIOUS)
@@ -374,7 +384,7 @@
id = "tarcookie"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 100
+ duration = 10 SECONDS
/datum/status_effect/tarcookie/tick(seconds_between_ticks)
for(var/mob/living/carbon/human/L in range(get_turf(owner),1))
@@ -385,7 +395,7 @@
id = "tarfoot"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 30
+ duration = 3 SECONDS
/datum/status_effect/tarfoot/on_apply()
owner.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/tarfoot)
@@ -398,12 +408,12 @@
id = "spookcookie"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 300
+ duration = 30 SECONDS
/datum/status_effect/spookcookie/on_apply()
- var/image/I = image(icon = 'icons/mob/human/human.dmi', icon_state = "skeleton", layer = ABOVE_MOB_LAYER, loc = owner)
- I.override = 1
- owner.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/everyone, "spookyscary", I)
+ var/image/skeleton_image = image(icon = 'icons/mob/human/human.dmi', icon_state = "skeleton", layer = ABOVE_MOB_LAYER, loc = owner)
+ skeleton_image.override = TRUE
+ owner.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/everyone, "spookyscary", skeleton_image)
return ..()
/datum/status_effect/spookcookie/on_remove()
@@ -413,7 +423,7 @@
id = "peacecookie"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 100
+ duration = 10 SECONDS
/datum/status_effect/peacecookie/tick(seconds_between_ticks)
for(var/mob/living/L in range(get_turf(owner),1))
@@ -423,7 +433,8 @@
id = "plur"
status_type = STATUS_EFFECT_REPLACE
alert_type = null
- duration = 30
+ duration = 3 SECONDS
+ tick_interval = STATUS_EFFECT_NO_TICK
/datum/status_effect/plur/on_apply()
ADD_TRAIT(owner, TRAIT_PACIFISM, TRAIT_STATUS_EFFECT(id))
@@ -436,7 +447,8 @@
id = "adamantinecookie"
status_type = STATUS_EFFECT_REFRESH
alert_type = null
- duration = 100
+ duration = 10 SECONDS
+ tick_interval = STATUS_EFFECT_NO_TICK
/datum/status_effect/adamantinecookie/on_apply()
if(ishuman(owner))
@@ -638,7 +650,7 @@
/datum/status_effect/stabilized/darkpurple/on_remove()
REMOVE_TRAIT(owner, TRAIT_RESISTHEATHANDS, TRAIT_STATUS_EFFECT(id))
- qdel(fire)
+ QDEL_NULL(fire)
/datum/status_effect/stabilized/darkpurple/get_examine_text()
return span_notice("[owner.p_Their()] fingertips burn brightly!")
@@ -698,7 +710,8 @@
/datum/status_effect/bluespacestabilization
id = "stabilizedbluespacecooldown"
- duration = 1200
+ duration = 2 MINUTES
+ tick_interval = STATUS_EFFECT_NO_TICK
alert_type = null
/datum/status_effect/stabilized/bluespace
@@ -796,7 +809,8 @@
colour = SLIME_TYPE_PYRITE
/datum/status_effect/stabilized/pyrite/tick(seconds_between_ticks)
- owner.add_atom_colour(RANDOM_COLOUR, TEMPORARY_COLOUR_PRIORITY)
+ var/new_color = rgb(rand(0, 360), 100, 50, space = COLORSPACE_HSL)
+ owner.add_atom_colour(color_transition_filter(new_color, SATURATION_OVERRIDE), TEMPORARY_COLOUR_PRIORITY)
return ..()
/datum/status_effect/stabilized/pyrite/on_remove()
diff --git a/code/modules/research/xenobiology/vatgrowing/petri_dish.dm b/code/modules/research/xenobiology/vatgrowing/petri_dish.dm
index 08e88b1e4818..c26906c00912 100644
--- a/code/modules/research/xenobiology/vatgrowing/petri_dish.dm
+++ b/code/modules/research/xenobiology/vatgrowing/petri_dish.dm
@@ -26,11 +26,8 @@
var/datum/micro_organism/MO = i
. += MO.get_details()
-/obj/item/petri_dish/pre_attack(atom/A, mob/living/user, list/modifiers, list/attack_modifiers)
+/obj/item/petri_dish/wash(clean_types)
. = ..()
- if(!sample || !istype(A, /obj/structure/sink))
- return FALSE
- to_chat(user, span_notice("You wash the sample out of [src]."))
sample = null
update_appearance()
diff --git a/code/modules/spells/spell_types/right_and_wrong.dm b/code/modules/spells/spell_types/right_and_wrong.dm
index 48422a26fdc9..7bf95a8d7054 100644
--- a/code/modules/spells/spell_types/right_and_wrong.dm
+++ b/code/modules/spells/spell_types/right_and_wrong.dm
@@ -27,7 +27,7 @@ GLOBAL_LIST_INIT(summoned_guns, list(
/obj/item/gun/ballistic/automatic/pistol/clandestine/fisher,
/obj/item/gun/ballistic/automatic/pistol/deagle,
/obj/item/gun/ballistic/automatic/pistol/deagle/regal,
- /obj/item/gun/ballistic/automatic/pistol/doorhickey,
+ /obj/item/gun/ballistic/automatic/pistol/doohickey,
/obj/item/gun/ballistic/automatic/pistol/m1911,
/obj/item/gun/ballistic/automatic/proto/unrestricted,
/obj/item/gun/ballistic/automatic/smartgun,
diff --git a/code/modules/spells/spell_types/shapeshift/polar_bear.dm b/code/modules/spells/spell_types/shapeshift/polar_bear.dm
index 93e1f6cea917..65a7342054fe 100644
--- a/code/modules/spells/spell_types/shapeshift/polar_bear.dm
+++ b/code/modules/spells/spell_types/shapeshift/polar_bear.dm
@@ -6,4 +6,4 @@
invocation_type = INVOCATION_EMOTE
spell_requirements = NONE
- possible_shapes = list(/mob/living/simple_animal/hostile/asteroid/polarbear/lesser)
+ possible_shapes = list(/mob/living/basic/mining/polarbear/lesser)
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 2836c854df9e..1a7a6033c50a 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -679,10 +679,6 @@
update_icon_dropped()
-//Return TRUE to get whatever mob this is in to update health.
-/obj/item/bodypart/proc/on_life(seconds_per_tick)
- SHOULD_CALL_PARENT(TRUE)
-
/**
* #receive_damage
*
@@ -1179,6 +1175,8 @@
/obj/item/bodypart/proc/update_limb(dropping_limb = FALSE, is_creating = FALSE)
SHOULD_CALL_PARENT(TRUE)
+ SEND_SIGNAL(src, COMSIG_BODYPART_UPDATED, dropping_limb, is_creating)
+
if(IS_ORGANIC_LIMB(src))
// Try to add a cached blood type data, we must do it in here because for some reason DNA gets initialized AFTER the mob's limbs are created.
// Should be fine as this gets called before all the important stuff happens
@@ -1816,6 +1814,22 @@
return
REMOVE_TRAIT(owner, old_trait, bodypart_trait_source)
+/// Add a bodyshape to the bodypart, then synchronize with the owner if necessary
+/obj/item/bodypart/proc/add_bodyshape(new_shape)
+ if(bodyshape & new_shape)
+ return
+
+ bodyshape |= new_shape
+ owner?.synchronize_bodyshapes()
+
+/// Remove a bodyshape from the bodypart, then synchronize with the owner if necessary
+/obj/item/bodypart/proc/remove_bodyshape(old_shape)
+ if(!(bodyshape & old_shape))
+ return
+
+ bodyshape &= ~old_shape
+ owner?.synchronize_bodyshapes()
+
/// Add one or multiple surgical states to the bodypart
/obj/item/bodypart/proc/add_surgical_state(new_states)
if(!new_states)
@@ -1870,3 +1884,34 @@
var/old_state = surgery_state
. = ..()
update_surgical_state(old_state, surgery_state ^ old_state)
+
+/// Adds biostate to the limb and ensures surgical states are updated accordingly
+/obj/item/bodypart/proc/add_biostate(new_biostate)
+ if(biological_state & new_biostate)
+ return
+
+ var/had_skin = LIMB_HAS_SKIN(src)
+ var/had_bones = LIMB_HAS_BONES(src)
+ var/had_vessels = LIMB_HAS_VESSELS(src)
+
+ biological_state |= new_biostate
+
+ if(!had_skin && LIMB_HAS_SKIN(src))
+ remove_surgical_state(SKINLESS_SURGERY_STATES)
+ if(!had_bones && LIMB_HAS_BONES(src))
+ remove_surgical_state(BONELESS_SURGERY_STATES)
+ if(!had_vessels && LIMB_HAS_VESSELS(src))
+ remove_surgical_state(VESSELLESS_SURGERY_STATES)
+
+/// Removes biostate from the limb and ensures surgical states are updated accordingly
+/obj/item/bodypart/proc/remove_biostate(old_biostate)
+ if(!(biological_state & old_biostate))
+ return
+
+ biological_state &= ~old_biostate
+ if(!LIMB_HAS_SKIN(src))
+ add_surgical_state(SKINLESS_SURGERY_STATES)
+ if(!LIMB_HAS_BONES(src))
+ add_surgical_state(BONELESS_SURGERY_STATES)
+ if(!LIMB_HAS_VESSELS(src))
+ add_surgical_state(VESSELLESS_SURGERY_STATES)
diff --git a/code/modules/surgery/bodyparts/dismemberment.dm b/code/modules/surgery/bodyparts/dismemberment.dm
index 2ae41bd5eaea..acc1468e506d 100644
--- a/code/modules/surgery/bodyparts/dismemberment.dm
+++ b/code/modules/surgery/bodyparts/dismemberment.dm
@@ -24,22 +24,17 @@
if (wounding_type)
LAZYSET(limb_owner.body_zone_dismembered_by, body_zone, wounding_type)
- if (can_bleed())
- limb_owner.bleed(rand(20, 40))
-
drop_limb(dismembered = TRUE)
-
limb_owner.update_equipment_speed_mods() // Update in case speed affecting item unequipped by dismemberment
- var/turf/owner_location = limb_owner.loc
- if(wounding_type != WOUND_BURN && istype(owner_location) && can_bleed())
- limb_owner.add_splatter_floor(owner_location)
if(QDELETED(src)) //Could have dropped into lava/explosion/chasm/whatever
return TRUE
if(dam_type == BURN)
burn()
return TRUE
- if (can_bleed())
+
+ // Assume we had our limb sliced/punched off by this point
+ if(can_bleed())
limb_owner.bleed(rand(20, 40))
var/direction = pick(GLOB.cardinals)
@@ -203,9 +198,7 @@
var/obj/item/lost_cuffs = arm_owner.handcuffed
arm_owner.set_handcuffed(null)
arm_owner.dropItemToGround(lost_cuffs, force = TRUE)
- if(arm_owner.hud_used)
- var/atom/movable/screen/inventory/hand/associated_hand = arm_owner.hud_used.hand_slots[held_index]
- associated_hand?.update_appearance()
+ arm_owner.hud_used?.update_inventory_slot(ITEM_SLOT_HANDS, held_index)
if(arm_owner.num_hands == 0)
arm_owner.dropItemToGround(arm_owner.gloves, force = TRUE)
arm_owner.update_worn_gloves() //to remove the bloody hands overlay
@@ -309,7 +302,7 @@
LAZYREMOVE(wounds, wound)
wound.apply_wound(src, TRUE, wound_source = wound.wound_source)
- if(new_limb_owner.mob_mood?.has_mood_of_category("dismembered_[body_zone]"))
+ if(new_limb_owner.mob_mood?.has_mood_of_category("dismembered_[body_zone]") && !(bodypart_flags & BODYPART_STUMP))
new_limb_owner.clear_mood_event("dismembered_[body_zone]")
new_limb_owner.add_mood_event("phantom_pain_[body_zone]", /datum/mood_event/reattachment, src)
diff --git a/code/modules/surgery/bodyparts/head_hair_and_lips.dm b/code/modules/surgery/bodyparts/head_hair_and_lips.dm
index 43689b1852b4..6979cbddda80 100644
--- a/code/modules/surgery/bodyparts/head_hair_and_lips.dm
+++ b/code/modules/surgery/bodyparts/head_hair_and_lips.dm
@@ -75,7 +75,7 @@
// Overlay
var/image/facial_hair_overlay = image(sprite_accessory.icon, sprite_accessory.icon_state, -HAIR_LAYER, dir = image_dir)
facial_hair_overlay.alpha = facial_hair_alpha
- set_overlay_hair_color(facial_hair_overlay)
+ set_overlay_hair_color(facial_hair_overlay, facial_hair_color)
// Emissive blocker
if(blocks_emissive != EMISSIVE_BLOCK_NONE)
var/mutable_appearance/em_block = emissive_blocker(facial_hair_overlay.icon, facial_hair_overlay.icon_state, location, alpha = facial_hair_alpha)
@@ -124,7 +124,7 @@
all_hair_overlays += image(hair_sprite_accessory.icon, icon_state = appendage_icon_state, layer = -OUTER_HAIR_LAYER, dir = image_dir)
for(var/image/hair_overlay as anything in all_hair_overlays)
- set_overlay_hair_color(hair_overlay)
+ set_overlay_hair_color(hair_overlay, hair_color)
hair_overlay.alpha = hair_alpha
hair_overlay.pixel_z = hair_sprite_accessory.y_offset
// Emissive blocker
@@ -147,14 +147,14 @@
return .
/// Helper for setting hair color of an overlay appropriately
-/obj/item/bodypart/head/proc/set_overlay_hair_color(image/hair_overlay)
+/obj/item/bodypart/head/proc/set_overlay_hair_color(image/hair_overlay, hair_color_to_use = src.hair_color)
PRIVATE_PROC(TRUE)
if(override_hair_color)
hair_overlay.color = override_hair_color
else if(fixed_hair_color)
hair_overlay.color = fixed_hair_color
else
- hair_overlay.color = hair_color
+ hair_overlay.color = hair_color_to_use
/// Returns a list of all eye related overlays, or an eyeless overlay if applicable
/obj/item/bodypart/head/proc/get_eye_overlays(dropped)
diff --git a/code/modules/surgery/bodyparts/helpers.dm b/code/modules/surgery/bodyparts/helpers.dm
index 73542be9b0c4..2e8650d903e7 100644
--- a/code/modules/surgery/bodyparts/helpers.dm
+++ b/code/modules/surgery/bodyparts/helpers.dm
@@ -301,17 +301,10 @@
all_limb_flags |= organ.external_bodyshapes
all_limb_flags |= limb.bodyshape
- bodyshape = all_limb_flags
+ if(obscured_slots & HIDESNOUT)
+ all_limb_flags &= ~BODYSHAPE_SNOUTED
-/// Get all bodyshapes but filter out bodyshapes that are currently being hidden
-/mob/living/carbon/proc/get_active_bodyshapes()
- var/active_shapes = bodyshape
- // future todo: both of these are state based, maybe we can just remove relevant bodyshapes directly. would remove the need for this proc
- if((active_shapes & BODYSHAPE_DIGITIGRADE) && is_digitigrade_squished())
- active_shapes &= ~BODYSHAPE_DIGITIGRADE
- if((active_shapes & BODYSHAPE_SNOUTED) && (obscured_slots & HIDESNOUT))
- active_shapes &= ~BODYSHAPE_SNOUTED
- return active_shapes
+ bodyshape = all_limb_flags
/proc/skintone2hex(skin_tone)
. = 0
diff --git a/code/modules/surgery/bodyparts/parts.dm b/code/modules/surgery/bodyparts/parts.dm
index 1eee423e1a93..6c52f99300d1 100644
--- a/code/modules/surgery/bodyparts/parts.dm
+++ b/code/modules/surgery/bodyparts/parts.dm
@@ -207,12 +207,7 @@
old_owner.on_lost_hand(src)
if(interaction_modifier != 0 || click_cd_modifier != 1)
old_owner.remove_status_effect(/datum/status_effect/arm_speed_penalty, held_index)
-
- if(!old_owner.hud_used)
- return
-
- var/atom/movable/screen/inventory/hand/hand = old_owner.hud_used.hand_slots[held_index]
- hand?.update_appearance()
+ old_owner.hud_used?.update_inventory_slot(ITEM_SLOT_HANDS, held_index)
/// We need to add hand hud items and appearance, so do that here
/obj/item/bodypart/arm/apply_ownership(mob/living/carbon/new_owner)
@@ -226,12 +221,7 @@
new_owner.on_added_hand(src, held_index)
if(interaction_modifier != 0 || click_cd_modifier != 1)
new_owner.apply_status_effect(/datum/status_effect/arm_speed_penalty, held_index, interaction_modifier, click_cd_modifier)
-
- if(!new_owner.hud_used)
- return
-
- var/atom/movable/screen/inventory/hand/hand = new_owner.hud_used.hand_slots[held_index]
- hand?.update_appearance()
+ new_owner.hud_used?.update_inventory_slot(ITEM_SLOT_HANDS, held_index)
/obj/item/bodypart/arm/set_disabled(new_disabled)
. = ..()
@@ -247,10 +237,7 @@
owner.dropItemToGround(owner.get_item_for_held_index(held_index))
else if(!bodypart_disabled)
owner.set_usable_hands(owner.usable_hands + 1)
-
- if(owner.hud_used)
- var/atom/movable/screen/inventory/hand/hand_screen_object = owner.hud_used.hand_slots[held_index]
- hand_screen_object?.update_appearance()
+ owner.hud_used?.update_inventory_slot(ITEM_SLOT_HANDS, held_index)
/obj/item/bodypart/arm/animate_atom_living(mob/living/owner)
var/mob/living/basic/slapper = ..()
diff --git a/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm
index 45889011818a..00858221c059 100644
--- a/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm
@@ -51,50 +51,26 @@
icon_greyscale = 'icons/mob/human/species/lizard/bodyparts.dmi'
limb_id = SPECIES_LIZARD
-/// Checks if this mob is wearing anything that does not have a valid sprite set for digitigrade legs
-/// (In other words, is the mob's digitigrade body squished by its clothing?)
-/mob/living/carbon/proc/is_digitigrade_squished()
- return FALSE
-
-/mob/living/carbon/human/is_digitigrade_squished()
- var/obj/item/clothing/shoes/worn_shoes = shoes
- var/obj/item/clothing/under/worn_suit = wear_suit
- var/obj/item/clothing/under/worn_uniform = w_uniform
-
- var/uniform_compatible = isnull(worn_uniform) \
- || (worn_uniform.supports_variations_flags & DIGITIGRADE_VARIATIONS) \
- || !(worn_uniform.body_parts_covered & LEGS) \
- || (obscured_slots & HIDEJUMPSUIT) // If suit hides our jumpsuit, it doesn't matter if it squishes
-
- var/suit_compatible = isnull(worn_suit) \
- || (worn_suit.supports_variations_flags & DIGITIGRADE_VARIATIONS) \
- || !(worn_suit.body_parts_covered & LEGS)
-
- var/shoes_compatible = isnull(worn_shoes) \
- || (worn_shoes.supports_variations_flags & DIGITIGRADE_VARIATIONS)
-
- return !uniform_compatible || !suit_compatible || !shoes_compatible
-
/obj/item/bodypart/leg/left/digitigrade
icon_greyscale = 'icons/mob/human/species/lizard/bodyparts.dmi'
limb_id = BODYPART_ID_DIGITIGRADE
species_id = SPECIES_LIZARD
- bodyshape = BODYSHAPE_HUMANOID | BODYSHAPE_DIGITIGRADE
+ bodyshape = BODYSHAPE_HUMANOID
footprint_sprite = FOOTPRINT_SPRITE_CLAWS
footstep_type = FOOTSTEP_MOB_CLAW
-/obj/item/bodypart/leg/left/digitigrade/update_limb(dropping_limb = FALSE, is_creating = FALSE)
+/obj/item/bodypart/leg/left/digitigrade/Initialize(mapload)
. = ..()
- limb_id = owner?.is_digitigrade_squished() ? SPECIES_LIZARD : BODYPART_ID_DIGITIGRADE
+ AddComponent(/datum/component/digitigrade_limb, SPECIES_LIZARD, initial(limb_id))
/obj/item/bodypart/leg/right/digitigrade
icon_greyscale = 'icons/mob/human/species/lizard/bodyparts.dmi'
limb_id = BODYPART_ID_DIGITIGRADE
species_id = SPECIES_LIZARD
- bodyshape = BODYSHAPE_HUMANOID | BODYSHAPE_DIGITIGRADE
+ bodyshape = BODYSHAPE_HUMANOID
footprint_sprite = FOOTPRINT_SPRITE_CLAWS
footstep_type = FOOTSTEP_MOB_CLAW
-/obj/item/bodypart/leg/right/digitigrade/update_limb(dropping_limb = FALSE, is_creating = FALSE)
+/obj/item/bodypart/leg/right/digitigrade/Initialize(mapload)
. = ..()
- limb_id = owner?.is_digitigrade_squished() ? SPECIES_LIZARD : BODYPART_ID_DIGITIGRADE
+ AddComponent(/datum/component/digitigrade_limb, SPECIES_LIZARD, initial(limb_id))
diff --git a/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
index 7c4cd5797a29..527416060132 100644
--- a/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
@@ -481,10 +481,6 @@
. = ..()
AddElement(/datum/element/blood_limb_overlay)
-/obj/item/bodypart/leg/left/skeleton/nonfunctional/update_limb(dropping_limb = FALSE, is_creating = FALSE)
- . = ..()
- limb_id = ((bodyshape & BODYSHAPE_DIGITIGRADE) && owner?.is_digitigrade_squished()) ? initial(limb_id) : "[initial(limb_id)]_[BODYPART_ID_DIGITIGRADE]"
-
/obj/item/bodypart/leg/right/skeleton/nonfunctional
limb_id = BODYPART_ID_BONE
disabling_threshold_percentage = 0
@@ -493,10 +489,6 @@
. = ..()
AddElement(/datum/element/blood_limb_overlay)
-/obj/item/bodypart/leg/right/skeleton/nonfunctional/update_limb(dropping_limb = FALSE, is_creating = FALSE)
- . = ..()
- limb_id = ((bodyshape & BODYSHAPE_DIGITIGRADE) && owner?.is_digitigrade_squished()) ? initial(limb_id) : "[initial(limb_id)]_[BODYPART_ID_DIGITIGRADE]"
-
///MUSHROOM
/obj/item/bodypart/head/mushroom
limb_id = SPECIES_MUSHROOM
diff --git a/code/modules/surgery/organs/external/_visual_organs.dm b/code/modules/surgery/organs/external/_visual_organs.dm
index 45df1a4a29dc..e3fcdc925000 100644
--- a/code/modules/surgery/organs/external/_visual_organs.dm
+++ b/code/modules/surgery/organs/external/_visual_organs.dm
@@ -144,7 +144,32 @@ Unlike normal organs, we're actually inside a persons limbs at all times
feature_key = FEATURE_FRILLS
/datum/bodypart_overlay/mutant/frills/can_draw_on_bodypart(obj/item/bodypart/bodypart_owner, mob/living/carbon/owner, is_husked = FALSE)
- return ..() && !(bodypart_owner.owner?.obscured_slots & HIDEEARS)
+ return ..() && !(bodypart_owner.owner?.obscured_slots & HIDEHAIR)
+
+/datum/bodypart_overlay/mutant/frills/generate_icon_cache(obj/item/bodypart/limb)
+ . = ..()
+ if(LAZYLEN(limb?.owner?.hair_masks))
+ . += jointext(limb.owner.hair_masks, ",")
+
+/datum/bodypart_overlay/mutant/frills/get_image(image_layer, obj/item/bodypart/limb)
+ if(!LAZYLEN(limb?.owner?.hair_masks))
+ return ..()
+
+ var/list/hair_masks_to_use = limb.owner.hair_masks
+ var/icon_state_to_use = build_icon_state(image_layer, limb)
+ var/frill_cache_key = "[sprite_datum.type]-[icon_state_to_use]-[jointext(hair_masks_to_use, ",")]"
+ var/static/list/cached_frill_icons
+ var/icon/cached_icon = LAZYACCESS(cached_frill_icons, frill_cache_key)
+ if(isnull(cached_icon))
+ cached_icon = icon(sprite_datum.icon, build_icon_state(image_layer, limb))
+ for(var/datum/hair_mask/mask as anything in hair_masks_to_use)
+ cached_icon.Blend(icon(mask::icon, mask::icon_state), ICON_ADD)
+ LAZYSET(cached_frill_icons, frill_cache_key, cached_icon)
+
+ var/mutable_appearance/uncached_appearance = mutable_appearance(cached_icon, layer = image_layer)
+ if(sprite_datum.center)
+ center_image(uncached_appearance, sprite_datum.dimension_x, sprite_datum.dimension_y)
+ return uncached_appearance
///Guess what part of the lizard this is?
/obj/item/organ/snout
diff --git a/code/modules/surgery/organs/external/wings/functional_wings.dm b/code/modules/surgery/organs/external/wings/functional_wings.dm
index ae84f98605be..f1be5fc90bab 100644
--- a/code/modules/surgery/organs/external/wings/functional_wings.dm
+++ b/code/modules/surgery/organs/external/wings/functional_wings.dm
@@ -183,7 +183,7 @@
feature_key = initial(feature_key)
set_appearance_from_name(sprite_datum.name)
-/datum/bodypart_overlay/mutant/wings/functional/generate_icon_cache()
+/datum/bodypart_overlay/mutant/wings/functional/generate_icon_cache(obj/item/bodypart/limb)
. = ..()
. += wings_open ? "open" : "closed"
diff --git a/code/modules/surgery/organs/internal/appendix/_appendix.dm b/code/modules/surgery/organs/internal/appendix/_appendix.dm
index db2692632ad2..0f759ebe110b 100644
--- a/code/modules/surgery/organs/internal/appendix/_appendix.dm
+++ b/code/modules/surgery/organs/internal/appendix/_appendix.dm
@@ -15,6 +15,7 @@
now_failing = span_warning("An explosion of pain erupts in your lower right abdomen!")
now_fixed = span_info("The pain in your abdomen has subsided.")
+ visual = FALSE
var/inflamation_stage = 0
diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm
index d3991d7aa999..f3833f0b4915 100644
--- a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm
+++ b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm
@@ -5,6 +5,7 @@
abstract_type = /obj/item/organ/cyberimp
organ_flags = ORGAN_ROBOTIC
failing_desc = "seems to be broken."
+ visual = FALSE
/// icon of the bodypart overlay we're going to be applying to our owner
var/aug_icon = 'icons/mob/human/species/misc/bodypart_overlay_augmentations.dmi'
/// icon_state of the bodypart overlay we're going to be applying to our owner
@@ -17,11 +18,12 @@
/obj/item/organ/cyberimp/Initialize(mapload)
. = ..()
if (aug_overlay)
+ visual = TRUE
bodypart_aug = new(src)
/obj/item/organ/cyberimp/Destroy()
- QDEL_NULL(bodypart_aug)
- return ..()
+ . = ..()
+ QDEL_NULL(bodypart_aug) // Do this after Remove() has done its thing, otherwise on_bodypart_remove() will not properly remove the overlay
/obj/item/organ/cyberimp/proc/get_overlay_state()
return aug_overlay
@@ -56,7 +58,7 @@
implant = null
return ..()
-/datum/bodypart_overlay/augment/generate_icon_cache()
+/datum/bodypart_overlay/augment/generate_icon_cache(obj/item/bodypart/limb)
. = ..()
. += implant.get_overlay_state()
diff --git a/code/modules/surgery/organs/internal/ears/_ears.dm b/code/modules/surgery/organs/internal/ears/_ears.dm
index a4d534b0d6df..fbcb9f79da66 100644
--- a/code/modules/surgery/organs/internal/ears/_ears.dm
+++ b/code/modules/surgery/organs/internal/ears/_ears.dm
@@ -13,6 +13,7 @@
now_failing = span_warning("You are unable to hear at all!")
now_fixed = span_info("Noise slowly begins filling your ears once more.")
low_threshold_cleared = span_info("The ringing in your ears has died down.")
+ visual = FALSE
/// temporary deafness, measured in seconds. While > 0, the person is unable to hear anything.
var/temporary_deafness = 0
@@ -141,7 +142,6 @@
/obj/item/organ/ears/invincible
damage_multiplier = 0
-
/obj/item/organ/ears/cat
name = "cat ears"
icon = 'icons/obj/clothing/head/costume.dmi'
@@ -199,6 +199,7 @@
sprite_accessory_override = /datum/sprite_accessory/ears/cat/cybernetic
organ_flags = ORGAN_ROBOTIC
failing_desc = "seems to be broken."
+ restyle_flags = NONE
/obj/item/organ/ears/cat/cybernetic/upgraded
name = "cybernetic cat ears"
diff --git a/code/modules/surgery/organs/internal/eyes/_eyes.dm b/code/modules/surgery/organs/internal/eyes/_eyes.dm
index a23b7ed06833..693f68477868 100644
--- a/code/modules/surgery/organs/internal/eyes/_eyes.dm
+++ b/code/modules/surgery/organs/internal/eyes/_eyes.dm
@@ -426,6 +426,8 @@
. = ..()
eye_color_left = initial(eye_color_left)
eye_color_right = initial(eye_color_right)
+ fix_scar(LEFT_EYE_SCAR)
+ fix_scar(RIGHT_EYE_SCAR)
/obj/item/organ/eyes/on_low_damage_received()
if(damage >= high_threshold)
diff --git a/code/modules/surgery/organs/internal/heart/_heart.dm b/code/modules/surgery/organs/internal/heart/_heart.dm
index 5120fbeb949c..c7b0bcce0f61 100644
--- a/code/modules/surgery/organs/internal/heart/_heart.dm
+++ b/code/modules/surgery/organs/internal/heart/_heart.dm
@@ -24,6 +24,7 @@
cell_line = CELL_LINE_ORGAN_HEART
cells_minimum = 1
cells_maximum = 2
+ visual = FALSE
// Heart attack code is in code/modules/mob/living/carbon/human/life.dm
@@ -35,7 +36,10 @@
var/beat = BEAT_NONE
/// whether the heart's been operated on to fix some of its damages
var/operated = FALSE
+ /// The message that is displayed when listening to a heart via a stethoscope
var/beat_noise = "a rhythmic thumping"
+ /// The rate at which blood is pumped by the heart is multiplied by this (value of 0 disables blood regeneration entirely)
+ var/blood_regeneration_multiplier = 1
/obj/item/organ/heart/update_icon_state()
. = ..()
@@ -92,6 +96,14 @@
. = ..()
Stop()
+/// Returns how effectively this heart regenerates the owner's blood based on organ health
+/obj/item/organ/heart/proc/get_blood_regeneration_multiplier()
+ if(!is_beating() || (organ_flags & (ORGAN_FAILING|ORGAN_EMP)))
+ return 0
+
+ var/health_percent = clamp((maxHealth - damage) / maxHealth, 0, 1)
+ return blood_regeneration_multiplier * health_percent
+
/// Checks if the heart is beating.
/// Can be overridden to add more conditions for more complex hearts.
/obj/item/organ/heart/proc/is_beating()
@@ -203,16 +215,12 @@
beat_noise = "a steady fsssh of hydraulics"
/// Whether or not we have a stabilization available. This prevents our owner from entering softcrit for an amount of time.
var/stabilization_available = FALSE
-
/// How long our stabilization lasts for.
var/stabilization_duration = 10 SECONDS
-
/// Whether our heart suppresses bleeders and restores blood automatically.
var/bleed_prevention = FALSE
-
/// The probability that our blood replication causes toxin damage.
var/toxification_probability = 20
-
/// Chance of permanent effects if emp-ed.
var/emp_vulnerability = 80
@@ -251,8 +259,6 @@
if(bleed_prevention && ishuman(owner) && owner.get_blood_volume() < BLOOD_VOLUME_NORMAL)
var/mob/living/carbon/human/wounded_owner = owner
- wounded_owner.adjust_blood_volume(2 * seconds_per_tick)
-
if(toxification_probability && prob(toxification_probability))
wounded_owner.adjust_tox_loss(1 * seconds_per_tick, updating_health = FALSE)
@@ -281,6 +287,7 @@
maxHealth = 1.5 * STANDARD_ORGAN_THRESHOLD
bleed_prevention = TRUE
emp_vulnerability = 40
+ blood_regeneration_multiplier = 9 // regenerates 2.25u of blood per tick (default is 0.25u)
/obj/item/organ/heart/cybernetic/tier3
name = "upgraded cybernetic heart"
@@ -342,9 +349,7 @@
desc = "It beats ever strong."
icon_state = "heart-evolved-on"
base_icon_state = "heart-evolved"
-
maxHealth = STANDARD_ORGAN_THRESHOLD * 1.2
-
/// Chance to heal per on_life
var/healing_probability = 10
/// Base healing we receive per tick at 0 damage and for standard versions
@@ -370,7 +375,6 @@
healing_probability = 5
base_healing = 0.5
-
// How much damage each magic block deals to us
var/damage_per_block = 50
diff --git a/code/modules/surgery/organs/internal/liver/_liver.dm b/code/modules/surgery/organs/internal/liver/_liver.dm
index d982259f6da8..2b2bb77780ba 100644
--- a/code/modules/surgery/organs/internal/liver/_liver.dm
+++ b/code/modules/surgery/organs/internal/liver/_liver.dm
@@ -21,6 +21,8 @@
cells_minimum = 1
cells_maximum = 2
+ visual = FALSE
+
/// Affects how much damage the liver takes from alcohol
var/alcohol_tolerance = ALCOHOL_RATE
/// The maximum volume of toxins the liver will ignore
diff --git a/code/modules/surgery/organs/internal/lungs/_lungs.dm b/code/modules/surgery/organs/internal/lungs/_lungs.dm
index 6ccd4a85a8d4..a93382c2b828 100644
--- a/code/modules/surgery/organs/internal/lungs/_lungs.dm
+++ b/code/modules/surgery/organs/internal/lungs/_lungs.dm
@@ -22,6 +22,8 @@
cells_minimum = 1
cells_maximum = 2
+ visual = FALSE
+
var/failed = FALSE
var/operated = FALSE //whether we can still have our damages fixed through surgery
diff --git a/code/modules/surgery/organs/internal/stomach/_stomach.dm b/code/modules/surgery/organs/internal/stomach/_stomach.dm
index 80c6772ed851..cc6e88854496 100644
--- a/code/modules/surgery/organs/internal/stomach/_stomach.dm
+++ b/code/modules/surgery/organs/internal/stomach/_stomach.dm
@@ -28,6 +28,8 @@
cells_minimum = 1
cells_maximum = 2
+ visual = FALSE
+
///The rate that disgust decays
var/disgust_metabolism = 1
diff --git a/code/modules/surgery/organs/internal/tongue/_tongue.dm b/code/modules/surgery/organs/internal/tongue/_tongue.dm
index 91fb99fa0a54..8334cbdc40db 100644
--- a/code/modules/surgery/organs/internal/tongue/_tongue.dm
+++ b/code/modules/surgery/organs/internal/tongue/_tongue.dm
@@ -8,7 +8,7 @@
attack_verb_continuous = list("licks", "slobbers", "slaps", "frenches", "tongues")
attack_verb_simple = list("lick", "slobber", "slap", "french", "tongue")
voice_filter = ""
- organ_traits = list(TRAIT_SPEAKS_CLEARLY)
+ visual = FALSE
/**
* A cached list of paths of all the languages this tongue is capable of speaking
*
@@ -31,6 +31,8 @@
///for temporary overrides of the above variable.
var/temp_say_mod = ""
+ /// Whether the owner of this tongue can speak clearly. Being set to FALSE means they mumble and slur things
+ var/speakable_with = TRUE
/// Whether the owner of this tongue can taste anything. Being set to FALSE will mean no taste feedback will be provided.
var/sense_of_taste = TRUE
/// Determines how "sensitive" this tongue is to tasting things, lower is more sensitive.
@@ -52,6 +54,8 @@
// - then we cache it via string list
// this results in tongues with identical possible languages sharing a cached list instance
languages_possible = string_list(get_possible_languages())
+ if(speakable_with)
+ add_organ_trait(TRAIT_SPEAKS_CLEARLY)
if(!sense_of_taste)
add_organ_trait(TRAIT_AGEUSIA)
@@ -165,7 +169,8 @@
add_organ_trait(TRAIT_AGEUSIA)
/obj/item/organ/tongue/on_failure_recovery()
- add_organ_trait(TRAIT_SPEAKS_CLEARLY)
+ if(speakable_with)
+ add_organ_trait(TRAIT_SPEAKS_CLEARLY)
if(sense_of_taste)
remove_organ_trait(TRAIT_AGEUSIA)
@@ -425,6 +430,14 @@
disliked_foodtypes = NONE
// List of english words that translate to zombie phrases
var/static/list/english_to_zombie = list()
+ /// Spooky growls we sometimes play while alive
+ var/static/list/spooks = list(
+ 'sound/effects/hallucinations/growl1.ogg',
+ 'sound/effects/hallucinations/growl2.ogg',
+ 'sound/effects/hallucinations/growl3.ogg',
+ 'sound/effects/hallucinations/veryfar_noise.ogg',
+ 'sound/effects/hallucinations/wail.ogg',
+ )
/obj/item/organ/tongue/zombie/proc/add_word_to_translations(english_word, zombie_word)
english_to_zombie[english_word] = zombie_word
@@ -480,6 +493,11 @@
message = capitalize(message)
speech_args[SPEECH_MESSAGE] = message
+/obj/item/organ/tongue/zombie/on_life(seconds_per_tick)
+ . = ..()
+ if(owner.stat == CONSCIOUS && SPT_PROB(2, seconds_per_tick))
+ playsound(owner, pick(spooks), 50, TRUE, 10)
+
/obj/item/organ/tongue/alien
name = "alien tongue"
desc = "According to leading xenobiologists the evolutionary benefit of having a second mouth in your mouth is \"that it looks badass\"."
@@ -558,7 +576,7 @@
attack_verb_simple = list("beep", "boop")
modifies_speech = TRUE
taste_sensitivity = 25 // not as good as an organic tongue
- organ_traits = list(TRAIT_SPEAKS_CLEARLY, TRAIT_SILICON_EMOTES_ALLOWED)
+ organ_traits = list(TRAIT_SILICON_EMOTES_ALLOWED)
voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
/obj/item/organ/tongue/robot/could_speak_language(datum/language/language_path)
@@ -620,7 +638,7 @@
say_mod = "meows"
liked_foodtypes = SEAFOOD | ORANGES | BUGS | GORE
disliked_foodtypes = GROSS | CLOTH | RAW
- organ_traits = list(TRAIT_SPEAKS_CLEARLY, TRAIT_WOUND_LICKER, TRAIT_FISH_EATER, TRAIT_CARPOTOXIN_IMMUNE)
+ organ_traits = list(TRAIT_WOUND_LICKER, TRAIT_FISH_EATER, TRAIT_CARPOTOXIN_IMMUNE)
languages_native = list(/datum/language/nekomimetic)
actions_types = list(/datum/action/item_action/organ_action/go_feral)
var/feral_mode = FALSE
diff --git a/code/modules/surgery/organs/internal/vocal_cords/_vocal_cords.dm b/code/modules/surgery/organs/internal/vocal_cords/_vocal_cords.dm
index eff8aaf5ac0f..86eb5afd29dc 100644
--- a/code/modules/surgery/organs/internal/vocal_cords/_vocal_cords.dm
+++ b/code/modules/surgery/organs/internal/vocal_cords/_vocal_cords.dm
@@ -6,6 +6,7 @@
gender = PLURAL
decay_factor = 0 //we don't want decaying vocal cords to somehow matter or appear on scanners since they don't do anything damaged
healing_factor = 0
+ visual = FALSE
var/list/spans = null
/obj/item/organ/vocal_cords/proc/can_speak_with() //if there is any limitation to speaking with these cords
diff --git a/code/modules/surgery/surgery_tools.dm b/code/modules/surgery/surgery_tools.dm
index 3ef6a2e2a2ec..eee5ac18c8a7 100644
--- a/code/modules/surgery/surgery_tools.dm
+++ b/code/modules/surgery/surgery_tools.dm
@@ -729,7 +729,7 @@
/obj/item/scalpel/cruel/Initialize(mapload)
. = ..()
- AddElement(/datum/element/bane, mob_biotypes = MOB_UNDEAD, damage_multiplier = 1) //Just in case one of the tennants get uppity
+ AddComponent(/datum/component/bane, affected_biotypes = MOB_UNDEAD, damage_multiplier = 2) //Just in case one of the tennants get uppity
/obj/item/surgicaldrill/cruel
name = "tearing drill"
diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm
index 2357e9030e92..f82457a8ba81 100644
--- a/code/modules/tgui/tgui_window.dm
+++ b/code/modules/tgui/tgui_window.dm
@@ -359,6 +359,8 @@
switch(type)
if("ping")
send_message("ping/reply", payload)
+ if("ping/set")
+ client?.avgping = payload["ping"]
if("visible")
visible = TRUE
SEND_SIGNAL(src, COMSIG_TGUI_WINDOW_VISIBLE, client)
diff --git a/code/modules/tgui_input/alert.dm b/code/modules/tgui_input/alert.dm
index b6df3fc4c3e0..b24ec394235c 100644
--- a/code/modules/tgui_input/alert.dm
+++ b/code/modules/tgui_input/alert.dm
@@ -26,7 +26,7 @@
// A gentle nudge - you should not be using TGUI alert for anything other than a simple message.
if(length(buttons) > 3)
log_tgui(user, "Error: TGUI Alert initiated with too many buttons. Use a list.", "TguiAlert")
- return tgui_input_list(user, message, title, buttons, timeout, autofocus)
+ return tgui_input_list(user, message, title, buttons, timeout=timeout)
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
if(length(buttons) == 2)
diff --git a/code/modules/tgui_input/say_modal/modal.dm b/code/modules/tgui_input/say_modal/modal.dm
index 331f00b1518b..848befe89780 100644
--- a/code/modules/tgui_input/say_modal/modal.dm
+++ b/code/modules/tgui_input/say_modal/modal.dm
@@ -42,7 +42,7 @@
/datum/tgui_say/New(client/client, id)
src.client = client
window = new(client, id)
- winset(client, "tgui_say", "size=1,1;is-visible=0;")
+ winset(client, SKIN_TGUISAY, "size=1,1;is-visible=0;")
window.subscribe(src, PROC_REF(on_message))
window.is_browser = TRUE
@@ -69,7 +69,7 @@
/datum/tgui_say/proc/load()
window_open = FALSE
- winset(client, "tgui_say", "pos=848,500;is-visible=0;")
+ winset(client, SKIN_TGUISAY, "pos=848,500;is-visible=0;")
window.send_message("props", list(
"lightMode" = client.prefs?.read_preference(/datum/preference/toggle/tgui_say_light_mode),
diff --git a/code/modules/tgui_input/say_modal/speech.dm b/code/modules/tgui_input/say_modal/speech.dm
index 50c37ed3f4cc..52515cd5388e 100644
--- a/code/modules/tgui_input/say_modal/speech.dm
+++ b/code/modules/tgui_input/say_modal/speech.dm
@@ -133,7 +133,7 @@
* boolean - success or failure
*/
/datum/tgui_say/proc/handle_entry(type, payload)
- if(!payload?["channel"] || !payload["entry"])
+ if(!payload?["channel"] || isnull(payload["entry"]))
CRASH("[usr] entered in a null payload to the chat window.")
if(length(payload["entry"]) > max_length)
CRASH("[usr] has entered more characters than allowed into a TGUI-Say")
diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm
index 0d715b2bf535..ae504fdb0c53 100644
--- a/code/modules/tgui_panel/external.dm
+++ b/code/modules/tgui_panel/external.dm
@@ -19,19 +19,19 @@
// Failed to fix, using tg_alert as fallback
action = tg_alert(src, "Did that work?", "", "Yes", "No, switch to old ui")
if (action == "No, switch to old ui")
- winset(src, "output_selector.legacy_output_selector", "left=output_legacy")
+ winset(src, OUTPUT_SELECTOR_LEGACY_OUTPUT_SELECTOR, "left=output_legacy")
log_tgui(src, "Failed to fix.", context = "verb/fix_tgui_panel")
/client/proc/nuke_chat()
// Catch all solution (kick the whole thing in the pants)
- winset(src, "output_selector.legacy_output_selector", "left=output_legacy")
+ winset(src, OUTPUT_SELECTOR_LEGACY_OUTPUT_SELECTOR, "left=output_legacy")
if(!tgui_panel || !istype(tgui_panel))
log_tgui(src, "tgui_panel datum is missing",
context = "verb/fix_tgui_panel")
tgui_panel = new(src)
tgui_panel.initialize(force = TRUE)
// Force show the panel to see if there are any errors
- winset(src, "output_selector.legacy_output_selector", "left=output_browser")
+ winset(src, OUTPUT_SELECTOR_LEGACY_OUTPUT_SELECTOR, "left=output_browser")
/client/verb/refresh_tgui()
set name = "Refresh TGUI"
diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm
index f3c691ce3e95..88973a9a7939 100644
--- a/code/modules/tooltip/tooltip.dm
+++ b/code/modules/tooltip/tooltip.dm
@@ -19,7 +19,7 @@ Notes:
/datum/tooltip
var/client/owner
- var/control = "mainwindow.tooltip"
+ var/control = SKIN_MAINWINDOW_TOOLTIP
var/showing = 0
var/queueHide = 0
var/init = 0
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index 5800b131c589..23153ef32f69 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -327,6 +327,7 @@
#include "strippable.dm"
#include "stuns.dm"
#include "style_hotswapping.dm"
+#include "subsystem_flags.dm"
#include "subsystem_init.dm"
#include "suit_sensor.dm"
#include "suit_storage_icons.dm"
diff --git a/code/modules/unit_tests/atmospherics_sanity.dm b/code/modules/unit_tests/atmospherics_sanity.dm
index 8b1d029ff57e..d7348a096d37 100644
--- a/code/modules/unit_tests/atmospherics_sanity.dm
+++ b/code/modules/unit_tests/atmospherics_sanity.dm
@@ -1,51 +1,58 @@
/**
- * This test checks that all expected areas are connected to a starting area
+ * This test checks that all areas are connected to their distribution loops
*/
/datum/unit_test/atmospherics_sanity
- // we iterate over all atmospherics devices on the starting networks
- priority = TEST_LONGER
+ priority = TEST_LONGER // we iterate over all atmospherics devices on the starting networks
- /// List of areas remaining to be checked
- var/list/area/remaining_areas
+ /// List of areas to start crawling from
+ var/list/area/starting_areas
/// List of areas already crawled, to prevent needless crawling
var/list/area/crawled_areas
- /// List of areas to start crawling from
- var/list/area/starting_areas
+ /// List of areas remaining to be checked
+ var/list/area/remaining_areas
+
+ /// List of areas that should absolutely not be encountered
+ var/list/area/forbidden_areas
/// We run this test in parallel, so we need to keep track of how many crawls are running
/// This is to prevent stack overflow mostly
var/crawls = 0
-/datum/unit_test/atmospherics_sanity/proc/get_areas()
+/datum/unit_test/atmospherics_sanity/proc/prepare_crawl()
starting_areas = list()
+ forbidden_areas = list()
+ crawled_areas = list()
+ remaining_areas = list()
+
for(var/obj/effect/landmark/atmospheric_sanity/start_area/start_marker in GLOB.landmarks_list)
var/area/starting_area = get_area(start_marker)
if(starting_area in starting_areas)
TEST_FAIL("Duplicate atmospherics sanity starting marker in '[starting_area]'([starting_area.type]) at ([start_marker.x], [start_marker.y], [start_marker.z])")
continue
if(starting_area.outdoors)
- TEST_FAIL("Atmospherics sanity starting marker in outdoors area '[starting_area]'([starting_area.type]) at ([start_marker.x], [start_marker.y], [start_marker.z])")
+ TEST_FAIL("Atmospherics sanity starting marker cannot be in outdoors area '[starting_area]'([starting_area.type]) at ([start_marker.x], [start_marker.y], [start_marker.z])")
continue
starting_areas |= get_area(start_marker)
- // If there are no starting areas, default to these
- var/static/list/area/default_starting_areas = list(
- /area/station/ai/satellite/chamber,
- /area/station/engineering/atmos,
- /area/station/medical/virology,
- /area/station/science/xenobiology,
- )
-
if(!length(starting_areas))
log_test("No starting areas found, defaulting...")
+
+ var/list/area/default_starting_areas = list(
+ // These areas have their own air supply
+ /area/station/ai/satellite/chamber,
+ /area/station/medical/virology,
+ /area/station/science/xenobiology,
+ // Otherwise, this should connect to the rest of the station
+ /area/station/engineering/atmos,
+ )
+
for(var/area/starting_area as anything in default_starting_areas)
var/area/station_area = GLOB.areas_by_type[starting_area]
if(!isnull(station_area))
starting_areas += station_area
- remaining_areas = list()
var/atom/mark_all_station_areas_marker = locate(/obj/effect/landmark/atmospheric_sanity/mark_all_station_areas_as_goal) in GLOB.landmarks_list
if(!isnull(mark_all_station_areas_marker))
@@ -61,58 +68,76 @@
if(goal_area.outdoors)
TEST_FAIL("Atmospherics sanity goal marker in outdoors area '[goal_area]'([goal_area.type]) at ([goal_marker.x], [goal_marker.y], [goal_marker.z])")
continue
- if(istype(goal_area, /area/space))
- TEST_FAIL("Atmospherics sanity goal marker in space at ([goal_marker.x], [goal_marker.y], [goal_marker.z])")
- continue
remaining_areas |= get_area(goal_marker)
if(!length(remaining_areas))
log_test("No goal areas found, defaulting...")
mark_station_areas_as_goals()
+ else
+ for(var/obj/effect/landmark/atmospheric_sanity/forbidden_area/forbidden_marker in GLOB.landmarks_list)
+ var/area/forbidden_area = get_area(forbidden_marker)
+ if(forbidden_area in remaining_areas)
+ var/obj/effect/landmark/atmospheric_sanity/goal_area/goal_marker = locate() in forbidden_area
+ TEST_FAIL("Area '[forbidden_area]'([forbidden_area.type]) \
+ has a goal marker at ([goal_marker.x], [goal_marker.y], [goal_marker.z]) \
+ and a forbidden marker at ([forbidden_marker.x], ([forbidden_marker.y], ([forbidden_marker.z])")
+ continue
+ if(forbidden_area in forbidden_areas)
+ TEST_FAIL("Area '[forbidden_area]'([forbidden_area.type]) is so forbidden it has a duplicate marker at at ([forbidden_marker.x], ([forbidden_marker.y], ([forbidden_marker.z])")
+ continue
+
+ forbidden_areas |= forbidden_area
for(var/obj/effect/landmark/atmospheric_sanity/ignore_area/ignore_marker in GLOB.landmarks_list)
remaining_areas -= get_area(ignore_marker)
/datum/unit_test/atmospherics_sanity/proc/mark_station_areas_as_goals()
- // We don't want to check these areas
- var/static/list/area/ignored_types = list(
+ // We don't care if we find these
+ var/list/area/ignored_types = list(
/area/station/asteroid,
- /area/station/engineering/supermatter,
/area/station/holodeck,
/area/station/maintenance,
/area/station/science/ordnance/bomb,
+ /area/station/solars,
+
+ // FIXME, burnchamber is usually mapped with a vent in the buffer airlock
+ // which causes us to leak into freezer. These two should be forbidden
/area/station/science/ordnance/burnchamber,
/area/station/science/ordnance/freezerchamber,
- /area/station/solars,
- /area/station/tcommsat/server,
)
for(var/area/ignored as anything in ignored_types)
ignored_types |= subtypesof(ignored)
- for(var/area/station/station_area_type as anything in subtypesof(/area/station) - ignored_types)
+ // We should never find these
+ var/list/area/forbidden_types = list(
+ /area/station/engineering/supermatter/engine,
+ /area/station/tcommsat/server,
+ )
+
+ for(var/area/forbidden as anything in forbidden_types)
+ forbidden_types |= subtypesof(forbidden)
+
+ for(var/area/station/station_area_type as anything in subtypesof(/area/station) - ignored_types - forbidden_types)
var/area/station_area = GLOB.areas_by_type[station_area_type]
if(!isnull(station_area))
remaining_areas += station_area
-/datum/unit_test/atmospherics_sanity/Run()
- get_areas()
- crawl_areas()
- UNTIL(crawls == 0)
- for(var/area/missed as anything in remaining_areas)
- if(missed.has_contained_turfs())
- var/turf/first_turf = missed.get_zlevel_turf_lists()[1][1]
- TEST_FAIL("Disconnected Area '[missed]'([missed.type]) at ([first_turf.x], [first_turf.y], [first_turf.z])")
- else
- TEST_NOTICE(src, "Disconnected Area '[missed]'([missed.type]) with no turfs?")
+ for(var/area/station/forbidden_area_type as anything in forbidden_types)
+ var/area/forbidden_area = GLOB.areas_by_type[forbidden_area_type]
+ if(!isnull(forbidden_area))
+ forbidden_areas += forbidden_area
-/// Iterates over starting_areas and ensures that all goal areas are connected to atleast one start
-/datum/unit_test/atmospherics_sanity/proc/crawl_areas()
- crawled_areas = list()
+/datum/unit_test/atmospherics_sanity/Run()
+ prepare_crawl()
for(var/area/start_area as anything in starting_areas)
ASYNC
crawl_area(start_area)
- starting_areas = null
+ UNTIL(crawls == 0)
+
+ for(var/area/missed as anything in remaining_areas)
+ var/turf/first_turf = missed.get_zlevel_turf_lists()[1][1]
+ TEST_FAIL("Goal area '[missed]'([missed.type]) is isolated from any distribution loops ([first_turf.x], [first_turf.y], [first_turf.z])")
/// Crawls through an area, iterating over all vents/scrubbers and their connected pipelines
/datum/unit_test/atmospherics_sanity/proc/crawl_area(area/the_area)
@@ -126,12 +151,16 @@
for(var/obj/machinery/atmospherics/components/component as anything in (the_area.air_vents + the_area.air_scrubbers))
for(var/datum/pipeline/parent as anything in component.parents)
if(isnull(parent))
- TEST_NOTICE(src, "Found a null parent for [component] in [the_area] at ([component.x], [component.y], [component.z])")
+ TEST_NOTICE(src, "[component] at ([component.x], [component.y], [component.z]) isn't attached to a pipenet, is this on purpose?")
continue
pipelines |= parent
- for(var/datum/pipeline/pipeline as anything in pipelines)
- crawl_pipeline(pipeline)
+ if((the_area in forbidden_areas) && length(pipelines)) // we don't care if this area is forbidden if it isn't actually connected to the air
+ var/turf/first_turf = the_area.get_zlevel_turf_lists()[1][1]
+ TEST_FAIL("Forbidden area '[the_area]'([the_area.type]) is connected to a distribution loop at ([first_turf.x], [first_turf.y], [first_turf.z])")
+ else
+ for(var/datum/pipeline/pipeline as anything in pipelines)
+ crawl_pipeline(pipeline)
crawls -= 1
diff --git a/code/modules/unit_tests/door_access.dm b/code/modules/unit_tests/door_access.dm
index 595e68297ad1..78e663bb310b 100644
--- a/code/modules/unit_tests/door_access.dm
+++ b/code/modules/unit_tests/door_access.dm
@@ -173,3 +173,55 @@
door.req_access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS)
subject.Bump(door)
TEST_ASSERT_EQUAL(door.density, TRUE, "Subject opened access-locked airlock while hands blocked!")
+
+/// Checks that doors with the id wire cut are inaccessible
+/datum/unit_test/door_require_id_wire_cut
+
+/datum/unit_test/door_require_id_wire_cut/Run()
+ var/obj/machinery/door/airlock/instant/door = EASY_ALLOCATE()
+ door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE
+ door.req_access = list(ACCESS_ENGINEERING)
+
+ var/mob/living/carbon/human/subject = EASY_ALLOCATE()
+ subject.equipOutfit(/datum/outfit/job/assistant/consistent)
+ var/obj/item/card/id/advanced/keycard = subject.wear_id
+ keycard.access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS)
+
+ subject.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Subject failed to open access-locked airlock before id wire cut!")
+ door.close()
+ door.wires.pulse(WIRE_OPEN, subject)
+ TEST_ASSERT_EQUAL(door.density, TRUE, "Pulsing the door's open wire allowed the subject to open the access-locked airlock without cutting the id wire!")
+ door.wires.cut(WIRE_IDSCAN, subject)
+ subject.last_bumped = 0
+ subject.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, TRUE, "Subject opened access-locked airlock with id wire cut!")
+ door.wires.pulse(WIRE_OPEN, subject)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Subject failed to open access-locked airlock with id wire cut and open wire pulsed!")
+ door.close()
+ door.wires.cut(WIRE_IDSCAN, subject) // mend
+ subject.last_bumped = 0
+ subject.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Subject failed to open access-locked airlock after mending id wire!")
+
+/// Same as above but testing throwing the item at the door
+/datum/unit_test/door_require_id_wire_cut/item_only
+
+/datum/unit_test/door_require_id_wire_cut/item_only/Run()
+ var/obj/machinery/door/airlock/instant/door = EASY_ALLOCATE()
+ door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE
+ door.req_access = list(ACCESS_ENGINEERING)
+
+ var/obj/item/card/id/advanced/keycard = EASY_ALLOCATE()
+ keycard.access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS)
+
+ keycard.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Throwing an ID at an access-locked airlock failed to open it before id wire cut!")
+ door.close()
+ door.wires.cut(WIRE_IDSCAN)
+ keycard.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, TRUE, "Throwing an ID at an access-locked airlock succeeded in opening it after id wire cut!")
+ door.close()
+ door.wires.cut(WIRE_IDSCAN) // mend
+ keycard.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Throwing an ID at an access-locked airlock failed to open it after mending the id wire!")
diff --git a/code/modules/unit_tests/plantgrowth_tests.dm b/code/modules/unit_tests/plantgrowth_tests.dm
index 595c71d05a49..a7213e37f388 100644
--- a/code/modules/unit_tests/plantgrowth_tests.dm
+++ b/code/modules/unit_tests/plantgrowth_tests.dm
@@ -8,6 +8,9 @@
for(var/seedpath in paths)
var/obj/item/seeds/seed = new seedpath
+ if (seed.seed_flags & NO_PLANTING)
+ continue
+
for(var/i in 1 to seed.growthstages)
if(icon_exists(seed.growing_icon, "[seed.icon_grow][i]"))
continue
diff --git a/code/modules/unit_tests/resist.dm b/code/modules/unit_tests/resist.dm
index 2fc64a02703e..4e746e09aae7 100644
--- a/code/modules/unit_tests/resist.dm
+++ b/code/modules/unit_tests/resist.dm
@@ -10,7 +10,7 @@
TEST_ASSERT_EQUAL(human.fire_stacks, 5, "Human does not have 5 fire stacks pre-resist")
// Stop, drop, and roll has a sleep call. This would delay the test, and is not necessary.
- call_async(human, /mob/living/verb/resist)
+ human.execute_resist()
//since resist() is a verb that possibly queues its actual execution for the next tick, we need to make the subsystem that handles the delayed execution process
//the callback. either that or sleep ourselves and see if it ran.
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard.png
index c0a3c30b8bb3..02d3799db695 100644
Binary files a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard.png and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard.png differ
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 1c6dc15cdae2..522511e88255 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -20,8 +20,6 @@
/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/gray_masses, // APOC EDIT ADD START - (Gray masses)
/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead/gray_masses, // APOC EDIT ADD END - (Gray masses)
/mob/living/simple_animal/hostile/asteroid/elite/pandora,
- /mob/living/simple_animal/hostile/asteroid/polarbear,
- /mob/living/simple_animal/hostile/asteroid/polarbear/lesser,
/mob/living/simple_animal/hostile/megafauna,
/mob/living/simple_animal/hostile/megafauna/bubblegum,
/mob/living/simple_animal/hostile/megafauna/bubblegum/hallucination,
diff --git a/code/modules/unit_tests/subsystem_flags.dm b/code/modules/unit_tests/subsystem_flags.dm
new file mode 100644
index 000000000000..60ce20d45609
--- /dev/null
+++ b/code/modules/unit_tests/subsystem_flags.dm
@@ -0,0 +1,12 @@
+// Ensures subystem flags are set in a coherent way
+/datum/unit_test/subsystem_flags
+
+/datum/unit_test/subsystem_flags/Run()
+ for(var/datum/controller/subsystem/sub_lad as anything in subtypesof(/datum/controller/subsystem))
+ if((sub_lad::ss_flags & (SS_TICKER | SS_KEEP_TIMING)) == (SS_TICKER | SS_KEEP_TIMING))
+ var/list/matching = get_matching_bitflags("ss_flags", sub_lad::ss_flags)
+ TEST_FAIL("[sub_lad] {[matching.Join(" | ")]} had both SS_TICKER and SEE_KEEP_TIMING set, this is redundant! You should likely remove SS_KEEP_TIMING.")
+ if((sub_lad::ss_flags & (SS_POST_FIRE_TIMING | SS_KEEP_TIMING)) == (SS_POST_FIRE_TIMING | SS_KEEP_TIMING))
+ var/list/matching = get_matching_bitflags("ss_flags", sub_lad::ss_flags)
+ TEST_FAIL("[sub_lad] {[matching.Join(" | ")]} had both SS_POST_FIRE_TIMING and SEE_KEEP_TIMING set, this is redundant! You should likely remove SS_KEEP_TIMING as SS_POST_FIRE_TIMING overrides it.")
+
diff --git a/code/modules/unit_tests/unequip_defib.dm b/code/modules/unit_tests/unequip_defib.dm
index 910cf599af2a..6af213dec122 100644
--- a/code/modules/unit_tests/unequip_defib.dm
+++ b/code/modules/unit_tests/unequip_defib.dm
@@ -12,7 +12,7 @@
usr = dummy // mouse drop still uses usr
- defib.MouseDrop(dummy.hud_used.hand_slots[1])
+ defib.MouseDrop(dummy.hud_used.screen_objects[HUD_KEY_HAND_SLOT(1)])
if(!dummy.is_holding(defib))
TEST_FAIL("The dummy failed to remove the defib from their back via mouse drop.")
diff --git a/code/modules/uplink/uplink_items/explosive.dm b/code/modules/uplink/uplink_items/explosive.dm
index 89d0d9d23ea7..c42dc2a0a399 100644
--- a/code/modules/uplink/uplink_items/explosive.dm
+++ b/code/modules/uplink/uplink_items/explosive.dm
@@ -121,6 +121,15 @@
purchasable_from = ~UPLINK_ALL_SYNDIE_OPS /// Ops get their own version.
limited_discount_stock = 4
+/datum/uplink_item/explosives/earthcracker
+ name = "E-2 Earthcracker"
+ desc = "The E-2 Earthcracker makes for a great partner to any conventional explosive. Set the device up on any station hull, arm, and activate.\
+ What remains is a conventionally dangerous weakpoint, that will crack open a random pattern of floors upon being hit with an explosive force.\
+ That pattern of cracks will in-turn also create additional cracks, ad-finimum if not repaired. It can also be used for mining out rock, though that's less advised."
+ item = /obj/item/earthcracker
+ cost = 2
+ limited_discount_stock = 2
+
/datum/uplink_item/explosives/syndicate_bomb/New()
. = ..()
desc = replacetext(desc, "%MIN_BOMB_TIMER", SYNDIEBOMB_MIN_TIMER_SECONDS)
diff --git a/code/modules/uplink/uplink_items/stealthy.dm b/code/modules/uplink/uplink_items/stealthy.dm
index 1d2e5016db29..e5e39bdcbbf4 100644
--- a/code/modules/uplink/uplink_items/stealthy.dm
+++ b/code/modules/uplink/uplink_items/stealthy.dm
@@ -44,6 +44,14 @@
cost = 6
surplus = 50
+/datum/uplink_item/stealthy_weapons/carnivorous_blood
+ name = "Carnivorous Blood"
+ desc = "A bottle of Carnivorous Blood, an Interdyne bioweapon that, once mixed with a potential victim's blood and then inserted into their bloodstream, \
+ will rapidly consume its victim's supply of usable blood. Feed it high-protein meat to let it cultivate."
+ item = /obj/item/storage/box/syndie_kit/carnivorous_blood
+ cost = 3
+ surplus = 50
+
/datum/uplink_item/stealthy_weapons/suppressor
name = "Suppressor"
desc = "This suppressor will silence the shots of the weapon it is attached to for increased stealth and superior ambushing capability. It is compatible with many small ballistic guns including the Makarov, Stechkin APS and C-20r, but not revolvers or energy guns."
diff --git a/code/modules/vehicles/cars/car.dm b/code/modules/vehicles/cars/car.dm
index 2b7307a0f888..79e5f6603d44 100644
--- a/code/modules/vehicles/cars/car.dm
+++ b/code/modules/vehicles/cars/car.dm
@@ -24,13 +24,13 @@
if(car_traits & CAN_KIDNAP)
initialize_controller_action_type(/datum/action/vehicle/sealed/dump_kidnapped_mobs, VEHICLE_CONTROL_DRIVE)
-/obj/vehicle/sealed/car/mouse_drop_receive(atom/dropping, mob/M, params)
- if(HAS_TRAIT(M, TRAIT_HANDS_BLOCKED) && !is_driver(M))
+/obj/vehicle/sealed/car/mouse_drop_receive(atom/dropping, mob/user, params)
+ if(!isliving(user) || (HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) && !is_driver(user)))
return
- if((car_traits & CAN_KIDNAP) && isliving(dropping) && M != dropping)
+ if((car_traits & CAN_KIDNAP) && isliving(dropping) && user != dropping)
var/mob/living/kidnapped = dropping
- kidnapped.visible_message(span_warning("[M] starts forcing [kidnapped] into [src]!"))
- mob_try_forced_enter(M, kidnapped)
+ kidnapped.visible_message(span_warning("[user] starts forcing [kidnapped] into [src]!"))
+ mob_try_forced_enter(user, kidnapped)
return ..()
/obj/vehicle/sealed/car/mob_try_exit(mob/future_pedestrian, mob/user, silent = FALSE)
diff --git a/code/modules/vehicles/mecha/equipment/tools/mining_tools.dm b/code/modules/vehicles/mecha/equipment/tools/mining_tools.dm
index a693fe33ab18..a4b600f496e5 100644
--- a/code/modules/vehicles/mecha/equipment/tools/mining_tools.dm
+++ b/code/modules/vehicles/mecha/equipment/tools/mining_tools.dm
@@ -120,7 +120,7 @@
return
target.visible_message(span_warning("[chassis] starts to drill [target]."), \
- span_userdanger("[chassis] starts to drill [target]..."), \
+ span_userdanger("[chassis] starts to drill you!"), \
span_hear("You hear drilling."))
log_message("Started drilling [target]", LOG_MECHA)
@@ -200,18 +200,20 @@
return
//drill makes a hole
- var/def_zone = target.get_random_valid_zone(BODY_ZONE_CHEST)
- var/obj/item/bodypart/target_part = target.get_bodypart(def_zone)
- var/blocked = target.run_armor_check(def_zone, MELEE)
- target.apply_damage(10, BRUTE, def_zone, blocked)
+ var/def_zone = target.get_random_valid_zone(user.zone_selected)
+ target.apply_damage(
+ 10,
+ BRUTE,
+ def_zone,
+ blocked = target.run_armor_check(def_zone, MELEE),
+ wound_bonus = 30,
+ exposed_wound_bonus = 50,
+ sharpness = SHARP_POINTY
+ )
//blood splatters
target.create_splatter(get_dir(chassis, target))
- //organs go everywhere
- if(target_part && blocked < 100 && prob(10 * drill_level))
- target_part.dismember(BRUTE)
-
/obj/item/mecha_parts/mecha_equipment/drill/diamonddrill
name = "diamond-tipped exosuit drill"
desc = "Equipment for engineering and combat exosuits. This is an upgraded version of the drill that'll pierce the heavens!"
diff --git a/code/modules/vehicles/sealed.dm b/code/modules/vehicles/sealed.dm
index c11164432042..6c5776e37d80 100644
--- a/code/modules/vehicles/sealed.dm
+++ b/code/modules/vehicles/sealed.dm
@@ -23,11 +23,11 @@
if(istype(E))
E.vehicle_entered_target = src
-/obj/vehicle/sealed/mouse_drop_receive(atom/dropping, mob/M, params)
- if(!istype(dropping) || !istype(M))
+/obj/vehicle/sealed/mouse_drop_receive(atom/dropping, mob/living/user, params)
+ if(!istype(dropping) || !istype(user))
return ..()
- if(M == dropping)
- mob_try_enter(M)
+ if(user == dropping)
+ mob_try_enter(user)
return ..()
/obj/vehicle/sealed/Exited(atom/movable/gone, direction)
diff --git a/code/modules/vending/autodrobe.dm b/code/modules/vending/autodrobe.dm
index ff465ce53052..a7ba45e2f05f 100644
--- a/code/modules/vending/autodrobe.dm
+++ b/code/modules/vending/autodrobe.dm
@@ -83,6 +83,9 @@ GLOBAL_LIST_INIT(autodrobe_entretainers_items, list(
/obj/item/clothing/under/rank/civilian/clown/jester = 3,
/obj/item/clothing/head/costume/jester = 3,
/obj/item/clothing/shoes/jester_shoes = 3,
+ /obj/item/clothing/under/rank/civilian/clown/jesteralt = 3,
+ /obj/item/clothing/head/costume/jesteralt = 3,
+ /obj/item/clothing/shoes/clown_shoes/jester = 1,
/obj/item/clothing/under/costume/villain = 3,
/obj/item/clothing/suit/costume/joker = 3,
/obj/item/clothing/under/costume/joker = 3,
diff --git a/code/modules/vending/vendor/interaction.dm b/code/modules/vending/vendor/interaction.dm
index 0e8c2cfc7b98..1cbd7a5ffa29 100644
--- a/code/modules/vending/vendor/interaction.dm
+++ b/code/modules/vending/vendor/interaction.dm
@@ -159,8 +159,9 @@
to_chat(user, span_notice("You loaded [restocked] items in [src][credits_contained > 0 ? ", and are rewarded [credits_contained] [MONEY_NAME]." : "."]"))
var/datum/bank_account/cargo_account = SSeconomy.get_dep_account(ACCOUNT_CAR)
cargo_account.adjust_money(round(credits_contained * 0.5), "Vending: Restock")
- var/obj/item/holochip/payday = new(src, credits_contained)
- try_put_in_hand(payday, user)
+ if(credits_contained >= 1)
+ var/obj/item/holochip/payday = new(src, credits_contained)
+ try_put_in_hand(payday, user)
credits_contained = 0
/obj/machinery/vending/exchange_parts(mob/user, obj/item/storage/part_replacer/replacer)
diff --git a/dependencies.sh b/dependencies.sh
index 2869f8c53d9d..bf5a66f6e7c7 100644
--- a/dependencies.sh
+++ b/dependencies.sh
@@ -8,7 +8,7 @@ export BYOND_MAJOR=516
export BYOND_MINOR=1659
#rust_g git tag
-export RUST_G_VERSION=4.2.0
+export RUST_G_VERSION=6.2.0
# node version
export NODE_VERSION_LTS=22.11.0
@@ -26,11 +26,11 @@ export PYTHON_VERSION=3.11.0
export DREAMLUAU_REPO="tgstation/dreamluau"
#dreamluau git tag
-export DREAMLUAU_VERSION=0.1.2
+export DREAMLUAU_VERSION=0.2.1
#hypnagogic repo
export CUTTER_REPO=spacestation13/hypnagogic
#hypnagogic git tag
-export CUTTER_VERSION=v5.0.0
+export CUTTER_VERSION=v5.0.1
diff --git a/dreamluau.dll b/dreamluau.dll
index cc2d56d2d1e4..6975fad39969 100644
Binary files a/dreamluau.dll and b/dreamluau.dll differ
diff --git a/html/changelogs/AutoChangeLog-pr-1017.yml b/html/changelogs/AutoChangeLog-pr-1017.yml
new file mode 100644
index 000000000000..6046101dc5dc
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1017.yml
@@ -0,0 +1,5 @@
+author: "FalloutFalcon"
+delete-after: True
+changes:
+ - balance: "Locks plants mutating behind a \"strange\" tray. (To be mapped inside pentex labs)"
+ - balance: "Removes the exotic seed crate from cargo"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1042.yml b/html/changelogs/AutoChangeLog-pr-1042.yml
deleted file mode 100644
index 85fc6ecc9674..000000000000
--- a/html/changelogs/AutoChangeLog-pr-1042.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "chazzyjazzy"
-delete-after: True
-changes:
- - rscadd: "adds the bomb trapped sarcophagus"
- - rscadd: "adds the sarcophagus & key random event"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1088.yml b/html/changelogs/AutoChangeLog-pr-1088.yml
deleted file mode 100644
index b56d5e6f1a14..000000000000
--- a/html/changelogs/AutoChangeLog-pr-1088.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "chazzyjazzy"
-delete-after: True
-changes:
- - code_imp: "removes hard deletes on area texts by cutting out the qdeletion method for making the text disappear, just setting the text to be invisible instead"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1091.yml b/html/changelogs/AutoChangeLog-pr-1091.yml
new file mode 100644
index 000000000000..8d5bb5581c3d
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1091.yml
@@ -0,0 +1,4 @@
+author: "chazzyjazzy"
+delete-after: True
+changes:
+ - rscadd: "adds artifacts to dumpsters and the fishing loot table"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1108.yml b/html/changelogs/AutoChangeLog-pr-1108.yml
new file mode 100644
index 000000000000..d4c1c6f5aa12
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1108.yml
@@ -0,0 +1,4 @@
+author: "FalloutFalcon, somniworld"
+delete-after: True
+changes:
+ - image: "Replaces the corax war form hud icon with a better icon"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1109.yml b/html/changelogs/AutoChangeLog-pr-1109.yml
new file mode 100644
index 000000000000..7b4f7e4aa8e9
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1109.yml
@@ -0,0 +1,4 @@
+author: "FalloutFalcon"
+delete-after: True
+changes:
+ - code_imp: "ritual book pulls vars from runes in a same method"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1114.yml b/html/changelogs/AutoChangeLog-pr-1114.yml
new file mode 100644
index 000000000000..39627fa506c0
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1114.yml
@@ -0,0 +1,4 @@
+author: "FalloutFalcon"
+delete-after: True
+changes:
+ - bugfix: "underplates layer above tables again"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1126.yml b/html/changelogs/AutoChangeLog-pr-1126.yml
new file mode 100644
index 000000000000..d04a0064670b
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1126.yml
@@ -0,0 +1,4 @@
+author: "FalloutFalcon"
+delete-after: True
+changes:
+ - bugfix: "Corax can have form specific flavor text like Garou"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1127.yml b/html/changelogs/AutoChangeLog-pr-1127.yml
new file mode 100644
index 000000000000..79b95bde816c
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1127.yml
@@ -0,0 +1,4 @@
+author: "FalloutFalcon"
+delete-after: True
+changes:
+ - bugfix: "Corax and Garou fur colors wont override one another"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1128.yml b/html/changelogs/AutoChangeLog-pr-1128.yml
new file mode 100644
index 000000000000..c7b30290a3ff
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1128.yml
@@ -0,0 +1,4 @@
+author: "FalloutFalcon"
+delete-after: True
+changes:
+ - bugfix: "Characters who have tzimisce selectected internally will not have a zulo preference if not the proper splat"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1129.yml b/html/changelogs/AutoChangeLog-pr-1129.yml
new file mode 100644
index 000000000000..8182659837a8
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1129.yml
@@ -0,0 +1,4 @@
+author: "FalloutFalcon"
+delete-after: True
+changes:
+ - bugfix: "balefire no longer runtimes"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1132.yml b/html/changelogs/AutoChangeLog-pr-1132.yml
new file mode 100644
index 000000000000..ad88a05353df
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1132.yml
@@ -0,0 +1,4 @@
+author: "Marshmellow105"
+delete-after: True
+changes:
+ - bugfix: "fixed stone floors using cave floor icons"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1137.yml b/html/changelogs/AutoChangeLog-pr-1137.yml
new file mode 100644
index 000000000000..f2bdab0f1e56
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1137.yml
@@ -0,0 +1,4 @@
+author: "Dusk-a"
+delete-after: True
+changes:
+ - balance: "Gives Corax the proper amount of gnosis they should have"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-1139.yml b/html/changelogs/AutoChangeLog-pr-1139.yml
new file mode 100644
index 000000000000..eda8cac2747a
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-1139.yml
@@ -0,0 +1,4 @@
+author: "Beautiful TG coders"
+delete-after: True
+changes:
+ - code_imp: "TG Pull. Thank you TG."
\ No newline at end of file
diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml
index 5bf2e6ee4776..edefa309d06c 100644
--- a/html/changelogs/archive/2026-06.yml
+++ b/html/changelogs/archive/2026-06.yml
@@ -23,3 +23,23 @@
chazzyjazzy:
- bugfix: fixes artifacts from being unbinded from the user when placed into a satchel
- bugfix: fixes the odious chalice so that it works
+2026-06-08:
+ FalloutFalcon:
+ - bugfix: Occult blood research should now work
+ - spellcheck: Tree stumps dont reference ss13
+ - rscadd: Descriptions of subsplats are now displayed when you select them
+ - code_imp: Removes old useless fire barrel subtype in favor of interactive one
+ - bugfix: Fixes corax "fur" color entry
+ TheCarnalest:
+ - bugfix: Garou no longer go upside down when transforming into Homid while resting
+ - bugfix: The Mage Blood disadvantage no longer removes Bloodheal
+ chazzyjazzy:
+ - rscadd: adds the bomb trapped sarcophagus
+ - rscadd: adds the sarcophagus & key random event
+ - code_imp: removes hard deletes on area texts by cutting out the qdeletion method
+ for making the text disappear, just setting the text to be invisible instead
+ - rscadd: sarcophagus event's announcement now only fires 20% of the time for added
+ stealth
+ - bugfix: sarcophagus event will now fire only once per round max
+ ellie:
+ - rscadd: adds dragon's breath shotgun ammo
diff --git a/html/statbrowser.js b/html/statbrowser.js
index d0f98fc60f7c..2292f199096b 100644
--- a/html/statbrowser.js
+++ b/html/statbrowser.js
@@ -10,33 +10,35 @@ if (!Array.prototype.includes) {
}
if (!String.prototype.trim) {
String.prototype.trim = function () {
- return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "");
+ return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
}
// Status panel implementation ------------------------------------------------
//status_tab_parts expects a list to be returned, to which we'll send a list within a list
//with just "loading" to not appear broken.
-var status_tab_parts = [["Loading..."]];
+var status_tab_parts = [['Loading...']];
var current_tab = null;
//mc_tab_parts expects a list to be returned, to which we'll send a list within a list
//with just "loading" to not appear broken.
-var mc_tab_parts = [["Loading..."]];
+var mc_tab_parts = [['Loading...']];
var href_token = null;
var verb_tabs = [];
-var verbs = [["", ""]]; // list with a list inside
+var verbs = [['', '']]; // list with a list inside
var tickets = [];
-var interviewManager = { status: "", interviews: [] };
+var interviewManager = { status: '', interviews: [] };
var sdql2 = [];
var permanent_tabs = []; // tabs that won't be cleared by wipes
var turfcontents = [];
-var turfname = "";
+var turfname = '';
var imageRetryDelay = 500;
var imageRetryLimit = 50;
-var menu = document.getElementById("menu");
-var statcontentdiv = document.getElementById("statcontent");
+var menu = document.getElementById('menu');
+var statcontentdiv = document.getElementById('statcontent');
var storedimages = [];
var split_admin_tabs = false;
+//The 'default' tab that everyone should have, that we swap to if the tab you're on is deleted or anything similar.
+var defaultTab = 'Status';
// Any BYOND commands that could result in the client's focus changing go through this
// to ensure that when we relinquish our focus, we don't do it after the result of
@@ -46,18 +48,18 @@ function run_after_focus(callback) {
}
function createStatusTab(name) {
- if (name.indexOf(".") != -1) {
- var splitName = name.split(".");
- if (split_admin_tabs && splitName[0] === "Admin") name = splitName[1];
+ if (name.indexOf('.') != -1) {
+ var splitName = name.split('.');
+ if (split_admin_tabs && splitName[0] === 'Admin') name = splitName[1];
else name = splitName[0];
}
- if (document.getElementById(name) || name.trim() == "") {
+ if (document.getElementById(name) || name.trim() == '') {
return;
}
if (!verb_tabs.includes(name) && !permanent_tabs.includes(name)) {
return;
}
- var button = document.createElement("DIV");
+ var button = document.createElement('DIV');
button.onclick = function () {
tab_change(name);
this.blur();
@@ -65,7 +67,7 @@ function createStatusTab(name) {
};
button.id = name;
button.textContent = name;
- button.className = "button";
+ button.className = 'button';
//ORDERING ALPHABETICALLY
button.style.order = { Status: 1, MC: 2 }[name] || name.charCodeAt(0);
//END ORDERING
@@ -87,7 +89,7 @@ function removeStatusTab(name) {
}
function sortVerbs() {
- verbs.sort(function (a, b) {
+ verbs.sort((a, b) => {
var selector = a[0] == b[0] ? 1 : 0;
if (a[selector].toUpperCase() < b[selector].toUpperCase()) {
return 1;
@@ -143,13 +145,13 @@ function check_verbs() {
function verbs_cat_check(cat) {
var tabCat = cat;
- if (cat.indexOf(".") != -1) {
- var splitName = cat.split(".");
- if (split_admin_tabs && splitName[0] === "Admin") tabCat = splitName[1];
+ if (cat.indexOf('.') != -1) {
+ var splitName = cat.split('.');
+ if (split_admin_tabs && splitName[0] === 'Admin') tabCat = splitName[1];
else tabCat = splitName[0];
}
var verbs_in_cat = 0;
- var verbcat = "";
+ var verbcat = '';
if (!verb_tabs.includes(tabCat)) {
removeStatusTab(tabCat);
return;
@@ -157,13 +159,12 @@ function verbs_cat_check(cat) {
for (var v = 0; v < verbs.length; v++) {
var part = verbs[v];
verbcat = part[0];
- if (verbcat.indexOf(".") != -1) {
- var splitName = verbcat.split(".");
- if (split_admin_tabs && splitName[0] === "Admin") verbcat = splitName[1];
+ if (verbcat.indexOf('.') != -1) {
+ var splitName = verbcat.split('.');
+ if (split_admin_tabs && splitName[0] === 'Admin') verbcat = splitName[1];
else verbcat = splitName[0];
}
- if (verbcat != tabCat || verbcat.trim() == "") {
- continue;
+ if (verbcat != tabCat || verbcat.trim() == '') {
} else {
verbs_in_cat = 1;
break; // we only need one
@@ -171,7 +172,7 @@ function verbs_cat_check(cat) {
}
if (verbs_in_cat != 1) {
removeStatusTab(tabCat);
- if (current_tab == tabCat) tab_change("Status");
+ if (current_tab == tabCat) tab_change(defaultTab);
}
}
@@ -182,14 +183,14 @@ function findVerbindex(name, verblist) {
}
}
function wipe_verbs() {
- verbs = [["", ""]];
+ verbs = [['', '']];
verb_tabs = [];
checkStatusTab(); // remove all empty verb tabs
}
function update_verbs() {
wipe_verbs();
- Byond.sendMessage("Update-Verbs");
+ Byond.sendMessage('Update-Verbs');
}
function SendTabsToByond() {
@@ -201,159 +202,158 @@ function SendTabsToByond() {
}
function SendTabToByond(tab) {
- Byond.sendMessage("Send-Tabs", { tab: tab });
+ Byond.sendMessage('Send-Tabs', { tab: tab });
}
//Byond can't have this tab anymore since we're removing it
function TakeTabFromByond(tab) {
- Byond.sendMessage("Remove-Tabs", { tab: tab });
+ Byond.sendMessage('Remove-Tabs', { tab: tab });
}
function tab_change(tab) {
if (tab == current_tab) return;
if (document.getElementById(current_tab))
- document.getElementById(current_tab).className = "button"; // disable active on last button
+ document.getElementById(current_tab).className = 'button'; // disable active on last button
current_tab = tab;
set_byond_tab(tab);
if (document.getElementById(tab))
- document.getElementById(tab).className = "button active"; // make current button active
+ document.getElementById(tab).className = 'button active'; // make current button active
var verb_tabs_thingy = verb_tabs.includes(tab);
- statcontentdiv.className = "statcontent";
- if (tab == "Status") {
+ statcontentdiv.className = 'statcontent';
+ if (tab == 'Status') {
draw_status();
- } else if (tab == "MC") {
+ } else if (tab == 'MC') {
draw_mc();
} else if (verb_tabs_thingy) {
draw_verbs(tab);
- } else if (tab == "Debug Stat Panel") {
+ } else if (tab == 'Debug Stat Panel') {
draw_debug();
- } else if (tab == "Tickets") {
+ } else if (tab == 'Tickets') {
draw_tickets();
draw_interviews();
- } else if (tab == "SDQL2") {
+ } else if (tab == 'SDQL2') {
draw_sdql2();
} else if (tab == turfname) {
draw_listedturf();
} else {
- statcontentdiv.textContext = "Loading...";
+ statcontentdiv.textContext = 'Loading...';
}
Byond.winset(Byond.windowId, {
- "is-visible": true,
+ 'is-visible': true,
});
}
function set_byond_tab(tab) {
- Byond.sendMessage("Set-Tab", { tab: tab });
+ Byond.sendMessage('Set-Tab', { tab: tab });
}
function draw_debug() {
- statcontentdiv.textContent = "";
- var wipeverbstabs = document.createElement("div");
- var link = document.createElement("a");
- link.onclick = function () {
+ statcontentdiv.textContent = '';
+ var wipeverbstabs = document.createElement('div');
+ var link = document.createElement('a');
+ link.onclick = () => {
wipe_verbs();
};
- link.textContent = "Wipe All Verbs";
+ link.textContent = 'Wipe All Verbs';
wipeverbstabs.appendChild(link);
- document.getElementById("statcontent").appendChild(wipeverbstabs);
- var wipeUpdateVerbsTabs = document.createElement("div");
- var updateLink = document.createElement("a");
- updateLink.onclick = function () {
+ document.getElementById('statcontent').appendChild(wipeverbstabs);
+ var wipeUpdateVerbsTabs = document.createElement('div');
+ var updateLink = document.createElement('a');
+ updateLink.onclick = () => {
update_verbs();
};
- updateLink.textContent = "Wipe and Update All Verbs";
+ updateLink.textContent = 'Wipe and Update All Verbs';
wipeUpdateVerbsTabs.appendChild(updateLink);
- document.getElementById("statcontent").appendChild(wipeUpdateVerbsTabs);
- var text = document.createElement("div");
- text.textContent = "Verb Tabs:";
- document.getElementById("statcontent").appendChild(text);
- var table1 = document.createElement("table");
+ document.getElementById('statcontent').appendChild(wipeUpdateVerbsTabs);
+ var text = document.createElement('div');
+ text.textContent = 'Verb Tabs:';
+ document.getElementById('statcontent').appendChild(text);
+ var table1 = document.createElement('table');
for (var i = 0; i < verb_tabs.length; i++) {
var part = verb_tabs[i];
// Hide subgroups except admin subgroups if they are split
- if (verb_tabs[i].lastIndexOf(".") != -1) {
- var splitName = verb_tabs[i].split(".");
- if (split_admin_tabs && splitName[0] === "Admin") part = splitName[1];
+ if (verb_tabs[i].lastIndexOf('.') != -1) {
+ var splitName = verb_tabs[i].split('.');
+ if (split_admin_tabs && splitName[0] === 'Admin') part = splitName[1];
else continue;
}
- var tr = document.createElement("tr");
- var td1 = document.createElement("td");
+ var tr = document.createElement('tr');
+ var td1 = document.createElement('td');
td1.textContent = part;
- var a = document.createElement("a");
- a.onclick = (function (part) {
- return function () {
- removeStatusTab(part);
- };
+ var a = document.createElement('a');
+ a.onclick = ((part) => () => {
+ removeStatusTab(part);
})(part);
- a.textContent = " Delete Tab " + part;
+ a.textContent = ' Delete Tab ' + part;
td1.appendChild(a);
tr.appendChild(td1);
table1.appendChild(tr);
}
- document.getElementById("statcontent").appendChild(table1);
- var header2 = document.createElement("div");
- header2.textContent = "Verbs:";
- document.getElementById("statcontent").appendChild(header2);
- var table2 = document.createElement("table");
+ document.getElementById('statcontent').appendChild(table1);
+ var header2 = document.createElement('div');
+ header2.textContent = 'Verbs:';
+ document.getElementById('statcontent').appendChild(header2);
+ var table2 = document.createElement('table');
for (var v = 0; v < verbs.length; v++) {
var part2 = verbs[v];
- var trr = document.createElement("tr");
- var tdd1 = document.createElement("td");
+ var trr = document.createElement('tr');
+ var tdd1 = document.createElement('td');
tdd1.textContent = part2[0];
- var tdd2 = document.createElement("td");
+ var tdd2 = document.createElement('td');
tdd2.textContent = part2[1];
trr.appendChild(tdd1);
trr.appendChild(tdd2);
table2.appendChild(trr);
}
- document.getElementById("statcontent").appendChild(table2);
- var text3 = document.createElement("div");
- text3.textContent = "Permanent Tabs:";
- document.getElementById("statcontent").appendChild(text3);
- var table3 = document.createElement("table");
+ document.getElementById('statcontent').appendChild(table2);
+ var text3 = document.createElement('div');
+ text3.textContent = 'Permanent Tabs:';
+ document.getElementById('statcontent').appendChild(text3);
+ var table3 = document.createElement('table');
for (var i = 0; i < permanent_tabs.length; i++) {
var part3 = permanent_tabs[i];
- var trrr = document.createElement("tr");
- var tddd1 = document.createElement("td");
+ var trrr = document.createElement('tr');
+ var tddd1 = document.createElement('td');
tddd1.textContent = part3;
trrr.appendChild(tddd1);
table3.appendChild(trrr);
}
- document.getElementById("statcontent").appendChild(table3);
+ document.getElementById('statcontent').appendChild(table3);
}
+
function draw_status() {
- if (!document.getElementById("Status")) {
- createStatusTab("Status");
- current_tab = "Status";
+ if (!document.getElementById('Status')) {
+ createStatusTab('Status');
+ current_tab = 'Status';
}
- statcontentdiv.textContent = "";
- var table = document.createElement("table");
+ statcontentdiv.textContent = '';
+ var table = document.createElement('table');
for (var i = 0; i < status_tab_parts.length; i++) {
var part = status_tab_parts[i];
if (!Array.isArray(part)) {
- var div = document.createElement("div");
- if (part.trim() == "") {
- table.appendChild(document.createElement("br"));
+ var div = document.createElement('div');
+ if (part.trim() == '') {
+ table.appendChild(document.createElement('br'));
} else {
div.textContent = part;
table.appendChild(div);
}
} else {
var div;
- if (part[0].trim() == "same_line") {
- var a = document.createElement("a");
- a.href = "byond://?" + part[2];
+ if (part[0].trim() == 'same_line') {
+ var a = document.createElement('a');
+ a.href = 'byond://?' + part[2];
a.textContent = part[1];
div.appendChild(a);
} else {
- div = document.createElement("div");
- if (part[0].trim() == "") {
- table.appendChild(document.createElement("br"));
+ div = document.createElement('div');
+ if (part[0].trim() == '') {
+ table.appendChild(document.createElement('br'));
} else {
div.textContent = part[0];
if (part[2]) {
- var a = document.createElement("a");
- a.href = "byond://?" + part[2];
+ var a = document.createElement('a');
+ a.href = 'byond://?' + part[2];
a.textContent = part[1];
div.appendChild(a);
}
@@ -362,29 +362,29 @@ function draw_status() {
}
}
}
- document.getElementById("statcontent").appendChild(table);
+ document.getElementById('statcontent').appendChild(table);
if (verb_tabs.length == 0 || !verbs) {
- Byond.command("Fix-Stat-Panel");
+ Byond.command('Fix-Stat-Panel');
}
}
function draw_mc() {
- statcontentdiv.textContent = "";
- statcontentdiv.className = "mcstatcontent";
- var table = document.createElement("table");
+ statcontentdiv.textContent = '';
+ statcontentdiv.className = 'mcstatcontent';
+ var table = document.createElement('table');
for (var i = 0; i < mc_tab_parts.length; i++) {
var part = mc_tab_parts[i];
- var tr = document.createElement("tr");
- var td0 = document.createElement("td");
- td0.className = "monospace";
+ var tr = document.createElement('tr');
+ var td0 = document.createElement('td');
+ td0.className = 'monospace';
td0.textContent = part[0];
- var td1 = document.createElement("td");
+ var td1 = document.createElement('td');
td1.textContent = part[1];
- var td2 = document.createElement("td");
+ var td2 = document.createElement('td');
if (part[3]) {
- var a = document.createElement("a");
+ var a = document.createElement('a');
a.href =
- "byond://?_src_=vars;admin_token=" + href_token + ";Vars=" + part[3];
+ 'byond://?_src_=vars;admin_token=' + href_token + ';Vars=' + part[3];
a.textContent = part[2];
td2.appendChild(a);
} else {
@@ -395,14 +395,14 @@ function draw_mc() {
tr.appendChild(td2);
table.appendChild(tr);
}
- document.getElementById("statcontent").appendChild(table);
+ document.getElementById('statcontent').appendChild(table);
}
function remove_tickets() {
if (tickets) {
tickets = [];
- removePermanentTab("Tickets");
- if (current_tab == "Tickets") tab_change("Status");
+ removePermanentTab('Tickets');
+ if (current_tab == 'Tickets') tab_change(defaultTab);
}
checkStatusTab();
}
@@ -410,8 +410,8 @@ function remove_tickets() {
function remove_sdql2() {
if (sdql2) {
sdql2 = [];
- removePermanentTab("SDQL2");
- if (current_tab == "SDQL2") tab_change("Status");
+ removePermanentTab('SDQL2');
+ if (current_tab == 'SDQL2') tab_change(defaultTab);
}
checkStatusTab();
}
@@ -427,55 +427,55 @@ function iconError(e) {
if (current_tab != turfname) {
return;
}
- setTimeout(function () {
+ setTimeout(() => {
var node = e.target;
- var current_attempts = Number(node.getAttribute("data-attempts")) || 0;
+ var current_attempts = Number(node.getAttribute('data-attempts')) || 0;
if (current_attempts > imageRetryLimit) {
return;
}
var src = node.src;
node.src = null;
- node.src = src + "#" + current_attempts;
- node.setAttribute("data-attempts", current_attempts + 1);
+ node.src = src + '#' + current_attempts;
+ node.setAttribute('data-attempts', current_attempts + 1);
draw_listedturf();
}, imageRetryDelay);
}
function draw_listedturf() {
- statcontentdiv.textContent = "";
- var table = document.createElement("table");
+ statcontentdiv.textContent = '';
+ var table = document.createElement('table');
for (var i = 0; i < turfcontents.length; i++) {
var part = turfcontents[i];
- var clickfunc = (function (part) {
+ var clickfunc = ((part) => {
// The outer function is used to close over a fresh "part" variable,
// rather than every onmousedown getting the "part" of the last entry.
- return function (e) {
+ return (e) => {
e.preventDefault();
- clickcatcher = "byond://?src=" + part[1];
+ clickcatcher = 'byond://?src=' + part[1];
switch (e.button) {
case 1:
- clickcatcher += ";statpanel_item_click=middle";
+ clickcatcher += ';statpanel_item_click=middle';
break;
case 2:
- clickcatcher += ";statpanel_item_click=right";
+ clickcatcher += ';statpanel_item_click=right';
break;
default:
- clickcatcher += ";statpanel_item_click=left";
+ clickcatcher += ';statpanel_item_click=left';
}
if (e.shiftKey) {
- clickcatcher += ";statpanel_item_shiftclick=1";
+ clickcatcher += ';statpanel_item_shiftclick=1';
}
if (e.ctrlKey) {
- clickcatcher += ";statpanel_item_ctrlclick=1";
+ clickcatcher += ';statpanel_item_ctrlclick=1';
}
if (e.altKey) {
- clickcatcher += ";statpanel_item_altclick=1";
+ clickcatcher += ';statpanel_item_altclick=1';
}
window.location.href = clickcatcher;
};
})(part);
if (storedimages[part[1]] == null && part[2]) {
- var img = document.createElement("img");
+ var img = document.createElement('img');
img.src = part[2];
img.id = part[1];
storedimages[part[1]] = part[2];
@@ -483,51 +483,51 @@ function draw_listedturf() {
img.onmousedown = clickfunc;
table.appendChild(img);
} else {
- var img = document.createElement("img");
+ var img = document.createElement('img');
img.onerror = iconError;
img.onmousedown = clickfunc;
img.src = storedimages[part[1]];
img.id = part[1];
table.appendChild(img);
}
- var b = document.createElement("div");
- var clickcatcher = "";
- b.className = "link";
+ var b = document.createElement('div');
+ var clickcatcher = '';
+ b.className = 'link';
b.onmousedown = clickfunc;
b.textContent = part[0];
table.appendChild(b);
- table.appendChild(document.createElement("br"));
+ table.appendChild(document.createElement('br'));
}
- document.getElementById("statcontent").appendChild(table);
+ document.getElementById('statcontent').appendChild(table);
}
function remove_listedturf() {
removePermanentTab(turfname);
checkStatusTab();
if (current_tab == turfname) {
- tab_change("Status");
+ tab_change(defaultTab);
}
}
function remove_mc() {
- removePermanentTab("MC");
- if (current_tab == "MC") {
- tab_change("Status");
+ removePermanentTab('MC');
+ if (current_tab == 'MC') {
+ tab_change(defaultTab);
}
}
function draw_sdql2() {
- statcontentdiv.textContent = "";
- var table = document.createElement("table");
+ statcontentdiv.textContent = '';
+ var table = document.createElement('table');
for (var i = 0; i < sdql2.length; i++) {
var part = sdql2[i];
- var tr = document.createElement("tr");
- var td1 = document.createElement("td");
+ var tr = document.createElement('tr');
+ var td1 = document.createElement('td');
td1.textContent = part[0];
- var td2 = document.createElement("td");
+ var td2 = document.createElement('td');
if (part[2]) {
- var a = document.createElement("a");
- a.href = "byond://?src=" + part[2] + ";statpanel_item_click=left";
+ var a = document.createElement('a');
+ a.href = 'byond://?src=' + part[2] + ';statpanel_item_click=left';
a.textContent = part[1];
td2.appendChild(a);
} else {
@@ -537,34 +537,34 @@ function draw_sdql2() {
tr.appendChild(td2);
table.appendChild(tr);
}
- document.getElementById("statcontent").appendChild(table);
+ document.getElementById('statcontent').appendChild(table);
}
function draw_tickets() {
- statcontentdiv.textContent = "";
- var table = document.createElement("table");
+ statcontentdiv.textContent = '';
+ var table = document.createElement('table');
if (!tickets) {
return;
}
for (var i = 0; i < tickets.length; i++) {
var part = tickets[i];
- var tr = document.createElement("tr");
- var td1 = document.createElement("td");
+ var tr = document.createElement('tr');
+ var td1 = document.createElement('td');
td1.textContent = part[0];
- var td2 = document.createElement("td");
+ var td2 = document.createElement('td');
if (part[2]) {
- var a = document.createElement("a");
+ var a = document.createElement('a');
a.href =
- "byond://?_src_=holder;admin_token=" +
+ 'byond://?_src_=holder;admin_token=' +
href_token +
- ";ahelp=" +
+ ';ahelp=' +
part[2] +
- ";ahelp_action=ticket;statpanel_item_click=left;action=ticket";
+ ';ahelp_action=ticket;statpanel_item_click=left;action=ticket';
a.textContent = part[1];
td2.appendChild(a);
} else if (part[3]) {
- var a = document.createElement("a");
- a.href = "byond://?src=" + part[3] + ";statpanel_item_click=left";
+ var a = document.createElement('a');
+ a.href = 'byond://?src=' + part[3] + ';statpanel_item_click=left';
a.textContent = part[1];
td2.appendChild(a);
} else {
@@ -574,33 +574,33 @@ function draw_tickets() {
tr.appendChild(td2);
table.appendChild(tr);
}
- document.getElementById("statcontent").appendChild(table);
+ document.getElementById('statcontent').appendChild(table);
}
function draw_interviews() {
- var body = document.createElement("div");
- var header = document.createElement("h3");
- header.textContent = "Interviews";
+ var body = document.createElement('div');
+ var header = document.createElement('h3');
+ header.textContent = 'Interviews';
body.appendChild(header);
- var manDiv = document.createElement("div");
- manDiv.className = "interview_panel_controls";
- var manLink = document.createElement("a");
- manLink.textContent = "Open Interview Manager Panel";
+ var manDiv = document.createElement('div');
+ manDiv.className = 'interview_panel_controls';
+ var manLink = document.createElement('a');
+ manLink.textContent = 'Open Interview Manager Panel';
manLink.href =
- "byond://?_src_=holder;admin_token=" +
+ 'byond://?_src_=holder;admin_token=' +
href_token +
- ";interview_man=1;statpanel_item_click=left";
+ ';interview_man=1;statpanel_item_click=left';
manDiv.appendChild(manLink);
body.appendChild(manDiv);
// List interview stats
- var statsDiv = document.createElement("table");
- statsDiv.className = "interview_panel_stats";
+ var statsDiv = document.createElement('table');
+ statsDiv.className = 'interview_panel_stats';
for (var key in interviewManager.status) {
- var d = document.createElement("div");
- var tr = document.createElement("tr");
- var stat_name = document.createElement("td");
- var stat_text = document.createElement("td");
+ var d = document.createElement('div');
+ var tr = document.createElement('tr');
+ var stat_name = document.createElement('td');
+ var stat_text = document.createElement('td');
stat_name.textContent = key;
stat_text.textContent = interviewManager.status[key];
tr.appendChild(stat_name);
@@ -608,94 +608,94 @@ function draw_interviews() {
statsDiv.appendChild(tr);
}
body.appendChild(statsDiv);
- document.getElementById("statcontent").appendChild(body);
+ document.getElementById('statcontent').appendChild(body);
// List interviews if any are open
- var table = document.createElement("table");
- table.className = "interview_panel_table";
+ var table = document.createElement('table');
+ table.className = 'interview_panel_table';
if (!interviewManager) {
return;
}
for (var i = 0; i < interviewManager.interviews.length; i++) {
var part = interviewManager.interviews[i];
- var tr = document.createElement("tr");
- var td = document.createElement("td");
- var a = document.createElement("a");
- a.textContent = part["status"];
+ var tr = document.createElement('tr');
+ var td = document.createElement('td');
+ var a = document.createElement('a');
+ a.textContent = part['status'];
a.href =
- "byond://?_src_=holder;admin_token=" +
+ 'byond://?_src_=holder;admin_token=' +
href_token +
- ";interview=" +
- part["ref"] +
- ";statpanel_item_click=left";
+ ';interview=' +
+ part['ref'] +
+ ';statpanel_item_click=left';
td.appendChild(a);
tr.appendChild(td);
table.appendChild(tr);
}
- document.getElementById("statcontent").appendChild(table);
+ document.getElementById('statcontent').appendChild(table);
}
function make_verb_onclick(command) {
- return function () {
- run_after_focus(function () {
+ return () => {
+ run_after_focus(() => {
Byond.command(command);
});
};
}
function draw_verbs(cat) {
- statcontentdiv.textContent = "";
- var table = document.createElement("div");
+ statcontentdiv.textContent = '';
+ var table = document.createElement('div');
var additions = {}; // additional sub-categories to be rendered
- table.className = "grid-container";
+ table.className = 'grid-container';
sortVerbs();
- if (split_admin_tabs && cat.lastIndexOf(".") != -1) {
- var splitName = cat.split(".");
- if (splitName[0] === "Admin") cat = splitName[1];
+ if (split_admin_tabs && cat.lastIndexOf('.') != -1) {
+ var splitName = cat.split('.');
+ if (splitName[0] === 'Admin') cat = splitName[1];
}
verbs.reverse(); // sort verbs backwards before we draw
for (var i = 0; i < verbs.length; ++i) {
var part = verbs[i];
var name = part[0];
- if (split_admin_tabs && name.lastIndexOf(".") != -1) {
- var splitName = name.split(".");
- if (splitName[0] === "Admin") name = splitName[1];
+ if (split_admin_tabs && name.lastIndexOf('.') != -1) {
+ var splitName = name.split('.');
+ if (splitName[0] === 'Admin') name = splitName[1];
}
var command = part[1];
if (
command &&
name.lastIndexOf(cat, 0) != -1 &&
- (name.length == cat.length || name.charAt(cat.length) == ".")
+ (name.length == cat.length || name.charAt(cat.length) == '.')
) {
- var subCat = name.lastIndexOf(".") != -1 ? name.split(".")[1] : null;
+ var subCat = name.lastIndexOf('.') != -1 ? name.split('.')[1] : null;
if (subCat && !additions[subCat]) {
- var newTable = document.createElement("div");
- newTable.className = "grid-container";
+ var newTable = document.createElement('div');
+ newTable.className = 'grid-container';
additions[subCat] = newTable;
}
- var a = document.createElement("a");
- a.href = "#";
- a.onclick = make_verb_onclick(command.replace(/\s/g, "-"));
- a.className = "grid-item";
- var t = document.createElement("span");
+ var a = document.createElement('a');
+ a.href = '#';
+ a.onclick = make_verb_onclick(command.replace(/\s/g, '-'));
+ a.className = 'grid-item';
+ var t = document.createElement('span');
t.textContent = command;
- t.className = "grid-item-text";
+ t.className = 'grid-item-text';
a.appendChild(t);
(subCat ? additions[subCat] : table).appendChild(a);
}
}
// Append base table to view
- var content = document.getElementById("statcontent");
+ var content = document.getElementById('statcontent');
content.appendChild(table);
// Append additional sub-categories if relevant
for (var cat in additions) {
- if (additions.hasOwnProperty(cat)) {
+ if (Object.hasOwn(additions, cat)) {
// do addition here
- var header = document.createElement("h3");
+ var header = document.createElement('h3');
header.textContent = cat;
content.appendChild(header);
content.appendChild(additions[cat]);
@@ -704,51 +704,51 @@ function draw_verbs(cat) {
}
function set_theme(which) {
- if (which == "light") {
- document.body.className = "";
- document.documentElement.className = "light";
- } else if (which == "dark") {
- document.body.className = "dark";
- document.documentElement.className = "dark";
+ if (which == 'light') {
+ document.body.className = '';
+ document.documentElement.className = 'light';
+ } else if (which == 'dark') {
+ document.body.className = 'dark';
+ document.documentElement.className = 'dark';
}
}
function set_font_size(size) {
- document.body.style.setProperty("font-size", size);
+ document.body.style.setProperty('font-size', size);
}
function set_tabs_style(style) {
- if (style == "default") {
- menu.classList.add("menu-wrap");
- menu.classList.remove("tabs-classic");
- } else if (style == "classic") {
- menu.classList.add("menu-wrap");
- menu.classList.add("tabs-classic");
- } else if (style == "scrollable") {
- menu.classList.remove("menu-wrap");
- menu.classList.remove("tabs-classic");
+ if (style == 'default') {
+ menu.classList.add('menu-wrap');
+ menu.classList.remove('tabs-classic');
+ } else if (style == 'classic') {
+ menu.classList.add('menu-wrap');
+ menu.classList.add('tabs-classic');
+ } else if (style == 'scrollable') {
+ menu.classList.remove('menu-wrap');
+ menu.classList.remove('tabs-classic');
}
}
function restoreFocus() {
- run_after_focus(function () {
- Byond.winset("map", {
+ run_after_focus(() => {
+ Byond.winset('map', {
focus: true,
});
});
}
function getCookie(cname) {
- var name = cname + "=";
- var ca = document.cookie.split(";");
+ var name = cname + '=';
+ var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
- while (c.charAt(0) == " ") c = c.substring(1);
+ while (c.charAt(0) == ' ') c = c.substring(1);
if (c.indexOf(name) === 0) {
return decoder(c.substring(name.length, c.length));
}
}
- return "";
+ return '';
}
function add_verb_list(payload) {
@@ -758,9 +758,9 @@ function add_verb_list(payload) {
var part = to_add[i];
if (!part[0]) continue;
var category = part[0];
- if (category.indexOf(".") != -1) {
- var splitName = category.split(".");
- if (split_admin_tabs && splitName[0] === "Admin") category = splitName[1];
+ if (category.indexOf('.') != -1) {
+ var splitName = category.split('.');
+ if (split_admin_tabs && splitName[0] === 'Admin') category = splitName[1];
else category = splitName[0];
}
if (findVerbindex(part[1], verbs)) continue;
@@ -777,19 +777,19 @@ function add_verb_list(payload) {
}
}
-document.addEventListener("mouseup", restoreFocus);
-document.addEventListener("keyup", restoreFocus);
+document.addEventListener('mouseup', restoreFocus);
+document.addEventListener('keyup', restoreFocus);
if (!current_tab) {
- addPermanentTab("Status");
- tab_change("Status");
+ addPermanentTab(defaultTab);
+ tab_change(defaultTab);
}
-window.onload = function () {
- Byond.sendMessage("Update-Verbs");
+window.onload = () => {
+ Byond.sendMessage('Update-Verbs');
};
-Byond.subscribeTo("remove_verb_list", function (v) {
+Byond.subscribeTo('remove_verb_list', (v) => {
var to_remove = v;
for (var i = 0; i < to_remove.length; i++) {
remove_verb(to_remove[i]);
@@ -801,13 +801,13 @@ Byond.subscribeTo("remove_verb_list", function (v) {
// passes a 2D list of (verbcategory, verbname) creates tabs and adds verbs to respective list
// example (IC, Say)
-Byond.subscribeTo("init_verbs", function (payload) {
+Byond.subscribeTo('init_verbs', (payload) => {
wipe_verbs(); // remove all verb categories so we can replace them
checkStatusTab(); // remove all status tabs
verb_tabs = payload.panel_tabs;
verb_tabs.sort(); // sort it
var do_update = false;
- var cat = "";
+ var cat = '';
for (var i = 0; i < verb_tabs.length; i++) {
cat = verb_tabs[i];
createStatusTab(cat); // create a category if the verb doesn't exist yet
@@ -825,8 +825,8 @@ Byond.subscribeTo("init_verbs", function (payload) {
SendTabsToByond();
});
-Byond.subscribeTo("update_stat", function (payload) {
- status_tab_parts = [payload.ping_str];
+Byond.subscribeTo('update_stat', (payload) => {
+ status_tab_parts = [];
var parsed = payload.global_data;
@@ -838,44 +838,44 @@ Byond.subscribeTo("update_stat", function (payload) {
for (var i = 0; i < parsed.length; i++)
if (parsed[i] != null) status_tab_parts.push(parsed[i]);
- if (current_tab == "Status") {
+ if (current_tab == 'Status') {
draw_status();
- } else if (current_tab == "Debug Stat Panel") {
+ } else if (current_tab == 'Debug Stat Panel') {
draw_debug();
}
});
-Byond.subscribeTo("update_mc", function (payload) {
+Byond.subscribeTo('update_mc', (payload) => {
mc_tab_parts = payload.mc_data;
- mc_tab_parts.splice(0, 0, ["", "Location:", payload.coord_entry]);
+ mc_tab_parts.splice(0, 0, ['', 'Location:', payload.coord_entry]);
- if (!verb_tabs.includes("MC")) {
- verb_tabs.push("MC");
+ if (!verb_tabs.includes('MC')) {
+ verb_tabs.push('MC');
}
- createStatusTab("MC");
+ createStatusTab('MC');
- if (current_tab == "MC") {
+ if (current_tab == 'MC') {
draw_mc();
}
});
-Byond.subscribeTo("create_debug", function () {
- if (!document.getElementById("Debug Stat Panel")) {
- addPermanentTab("Debug Stat Panel");
+Byond.subscribeTo('create_debug', () => {
+ if (!document.getElementById('Debug Stat Panel')) {
+ addPermanentTab('Debug Stat Panel');
} else {
- removePermanentTab("Debug Stat Panel");
+ removePermanentTab('Debug Stat Panel');
}
});
-Byond.subscribeTo("create_listedturf", function (TN) {
+Byond.subscribeTo('create_listedturf', (TN) => {
remove_listedturf(); // remove the last one if we had one
turfname = TN;
addPermanentTab(turfname);
tab_change(turfname);
});
-Byond.subscribeTo("remove_admin_tabs", function () {
+Byond.subscribeTo('remove_admin_tabs', () => {
href_token = null;
remove_mc();
remove_tickets();
@@ -883,66 +883,66 @@ Byond.subscribeTo("remove_admin_tabs", function () {
remove_interviews();
});
-Byond.subscribeTo("update_listedturf", function (TC) {
+Byond.subscribeTo('update_listedturf', (TC) => {
turfcontents = TC;
if (current_tab == turfname) {
draw_listedturf();
}
});
-Byond.subscribeTo("update_interviews", function (I) {
+Byond.subscribeTo('update_interviews', (I) => {
interviewManager = I;
- if (current_tab == "Tickets") {
+ if (current_tab == 'Tickets') {
draw_interviews();
}
});
-Byond.subscribeTo("update_split_admin_tabs", function (status) {
+Byond.subscribeTo('update_split_admin_tabs', (status) => {
status = status == true;
if (split_admin_tabs !== status) {
if (split_admin_tabs === true) {
- removeStatusTab("Events");
- removeStatusTab("Fun");
- removeStatusTab("Game");
+ removeStatusTab('Events');
+ removeStatusTab('Fun');
+ removeStatusTab('Game');
}
update_verbs();
}
split_admin_tabs = status;
});
-Byond.subscribeTo("add_admin_tabs", function (ht) {
+Byond.subscribeTo('add_admin_tabs', (ht) => {
href_token = ht;
- addPermanentTab("MC");
- addPermanentTab("Tickets");
+ addPermanentTab('MC');
+ addPermanentTab('Tickets');
});
-Byond.subscribeTo("update_sdql2", function (S) {
+Byond.subscribeTo('update_sdql2', (S) => {
sdql2 = S;
- if (sdql2.length > 0 && !verb_tabs.includes("SDQL2")) {
- verb_tabs.push("SDQL2");
- addPermanentTab("SDQL2");
+ if (sdql2.length > 0 && !verb_tabs.includes('SDQL2')) {
+ verb_tabs.push('SDQL2');
+ addPermanentTab('SDQL2');
}
- if (current_tab == "SDQL2") {
+ if (current_tab == 'SDQL2') {
draw_sdql2();
}
});
-Byond.subscribeTo("update_tickets", function (T) {
+Byond.subscribeTo('update_tickets', (T) => {
tickets = T;
- if (!verb_tabs.includes("Tickets")) {
- verb_tabs.push("Tickets");
- addPermanentTab("Tickets");
+ if (!verb_tabs.includes('Tickets')) {
+ verb_tabs.push('Tickets');
+ addPermanentTab('Tickets');
}
- if (current_tab == "Tickets") {
+ if (current_tab == 'Tickets') {
draw_tickets();
}
});
-Byond.subscribeTo("remove_listedturf", remove_listedturf);
+Byond.subscribeTo('remove_listedturf', remove_listedturf);
-Byond.subscribeTo("remove_sdql2", remove_sdql2);
+Byond.subscribeTo('remove_sdql2', remove_sdql2);
-Byond.subscribeTo("remove_mc", remove_mc);
+Byond.subscribeTo('remove_mc', remove_mc);
-Byond.subscribeTo("add_verb_list", add_verb_list);
+Byond.subscribeTo('add_verb_list', add_verb_list);
diff --git a/icons/effects/beam.dmi b/icons/effects/beam.dmi
index a4a9bbb8443d..c9723f51e2b2 100644
Binary files a/icons/effects/beam.dmi and b/icons/effects/beam.dmi differ
diff --git a/icons/effects/cut.dmi b/icons/effects/cut.dmi
index 0252704d2bbd..aa2865afb0b2 100644
Binary files a/icons/effects/cut.dmi and b/icons/effects/cut.dmi differ
diff --git a/icons/effects/hitsplats.dmi b/icons/effects/hitsplats.dmi
new file mode 100644
index 000000000000..b501854b6cca
Binary files /dev/null and b/icons/effects/hitsplats.dmi differ
diff --git a/icons/effects/interaction_points.dmi b/icons/effects/interaction_points.dmi
deleted file mode 100644
index 5523727247c7..000000000000
Binary files a/icons/effects/interaction_points.dmi and /dev/null differ
diff --git a/icons/effects/landmarks_static.dmi b/icons/effects/landmarks_static.dmi
index 53d3d939128e..c02b9e8c694b 100644
Binary files a/icons/effects/landmarks_static.dmi and b/icons/effects/landmarks_static.dmi differ
diff --git a/icons/effects/random_spawners.dmi b/icons/effects/random_spawners.dmi
index 4d816c6df800..014a14ca4b14 100644
Binary files a/icons/effects/random_spawners.dmi and b/icons/effects/random_spawners.dmi differ
diff --git a/icons/effects/tasks.dmi b/icons/effects/tasks.dmi
new file mode 100644
index 000000000000..58d398649b5a
Binary files /dev/null and b/icons/effects/tasks.dmi differ
diff --git a/icons/effects/vent_overlays.dmi b/icons/effects/vent_overlays.dmi
index 375117f4c79f..c1e676591e7a 100644
Binary files a/icons/effects/vent_overlays.dmi and b/icons/effects/vent_overlays.dmi differ
diff --git a/icons/effects/weather_overlay.dmi b/icons/effects/weather_overlay.dmi
new file mode 100644
index 000000000000..7dc15fbe40c7
Binary files /dev/null and b/icons/effects/weather_overlay.dmi differ
diff --git a/icons/hud/actions.dmi b/icons/hud/actions.dmi
index 071ba38f7264..6d51cc55534a 100644
Binary files a/icons/hud/actions.dmi and b/icons/hud/actions.dmi differ
diff --git a/icons/hud/screen_voidwalker.dmi b/icons/hud/screen_voidwalker.dmi
index 7eb522053be1..a4a5c5acd09e 100644
Binary files a/icons/hud/screen_voidwalker.dmi and b/icons/hud/screen_voidwalker.dmi differ
diff --git a/icons/map_icons/clothing/_clothing.dmi b/icons/map_icons/clothing/_clothing.dmi
index 6a9956844f4d..778966219458 100644
Binary files a/icons/map_icons/clothing/_clothing.dmi and b/icons/map_icons/clothing/_clothing.dmi differ
diff --git a/icons/map_icons/clothing/accessory.dmi b/icons/map_icons/clothing/accessory.dmi
index e41990215450..55df7680741f 100644
Binary files a/icons/map_icons/clothing/accessory.dmi and b/icons/map_icons/clothing/accessory.dmi differ
diff --git a/icons/map_icons/clothing/head/_head.dmi b/icons/map_icons/clothing/head/_head.dmi
index 67ffccf2c2ae..bf12dcac4524 100644
Binary files a/icons/map_icons/clothing/head/_head.dmi and b/icons/map_icons/clothing/head/_head.dmi differ
diff --git a/icons/map_icons/clothing/head/beret.dmi b/icons/map_icons/clothing/head/beret.dmi
index 160ce9249849..ccc8324c3895 100644
Binary files a/icons/map_icons/clothing/head/beret.dmi and b/icons/map_icons/clothing/head/beret.dmi differ
diff --git a/icons/map_icons/clothing/mask.dmi b/icons/map_icons/clothing/mask.dmi
index 2841cb9effc4..660a0c223705 100644
Binary files a/icons/map_icons/clothing/mask.dmi and b/icons/map_icons/clothing/mask.dmi differ
diff --git a/icons/map_icons/clothing/neck.dmi b/icons/map_icons/clothing/neck.dmi
index 3a5094318ef1..b95176c3deda 100644
Binary files a/icons/map_icons/clothing/neck.dmi and b/icons/map_icons/clothing/neck.dmi differ
diff --git a/icons/map_icons/clothing/shoes.dmi b/icons/map_icons/clothing/shoes.dmi
index d6c336245fb5..8ada17028560 100644
Binary files a/icons/map_icons/clothing/shoes.dmi and b/icons/map_icons/clothing/shoes.dmi differ
diff --git a/icons/map_icons/clothing/suit/_suit.dmi b/icons/map_icons/clothing/suit/_suit.dmi
index cdbb28e990c8..62a890ded65f 100644
Binary files a/icons/map_icons/clothing/suit/_suit.dmi and b/icons/map_icons/clothing/suit/_suit.dmi differ
diff --git a/icons/map_icons/clothing/suit/costume.dmi b/icons/map_icons/clothing/suit/costume.dmi
index b47fc0c68ab3..b4cc21296e79 100644
Binary files a/icons/map_icons/clothing/suit/costume.dmi and b/icons/map_icons/clothing/suit/costume.dmi differ
diff --git a/icons/map_icons/clothing/under/_under.dmi b/icons/map_icons/clothing/under/_under.dmi
index 5d9b9146e864..1ea52de10fa6 100644
Binary files a/icons/map_icons/clothing/under/_under.dmi and b/icons/map_icons/clothing/under/_under.dmi differ
diff --git a/icons/map_icons/clothing/under/color.dmi b/icons/map_icons/clothing/under/color.dmi
index 87189da75482..ec7ae58f833a 100644
Binary files a/icons/map_icons/clothing/under/color.dmi and b/icons/map_icons/clothing/under/color.dmi differ
diff --git a/icons/map_icons/clothing/under/costume.dmi b/icons/map_icons/clothing/under/costume.dmi
index 17e56c8eaa12..da6eaec2611b 100644
Binary files a/icons/map_icons/clothing/under/costume.dmi and b/icons/map_icons/clothing/under/costume.dmi differ
diff --git a/icons/map_icons/clothing/under/dress.dmi b/icons/map_icons/clothing/under/dress.dmi
index 14e11b72a7fc..15861b7f3581 100644
Binary files a/icons/map_icons/clothing/under/dress.dmi and b/icons/map_icons/clothing/under/dress.dmi differ
diff --git a/icons/map_icons/items/_item.dmi b/icons/map_icons/items/_item.dmi
index 16535bf205f1..6fad912023d0 100644
Binary files a/icons/map_icons/items/_item.dmi and b/icons/map_icons/items/_item.dmi differ
diff --git a/icons/map_icons/items/encryptionkey.dmi b/icons/map_icons/items/encryptionkey.dmi
index 360180773f39..4c6cd37c691e 100644
Binary files a/icons/map_icons/items/encryptionkey.dmi and b/icons/map_icons/items/encryptionkey.dmi differ
diff --git a/icons/map_icons/items/pda.dmi b/icons/map_icons/items/pda.dmi
index c28a9b7043fc..40ca680e0f6e 100644
Binary files a/icons/map_icons/items/pda.dmi and b/icons/map_icons/items/pda.dmi differ
diff --git a/icons/map_icons/mobs.dmi b/icons/map_icons/mobs.dmi
index 6a9956844f4d..778966219458 100644
Binary files a/icons/map_icons/mobs.dmi and b/icons/map_icons/mobs.dmi differ
diff --git a/icons/map_icons/objects.dmi b/icons/map_icons/objects.dmi
index dd3a926e2a7c..c6f765f81e8b 100644
Binary files a/icons/map_icons/objects.dmi and b/icons/map_icons/objects.dmi differ
diff --git a/icons/map_icons/turfs.dmi b/icons/map_icons/turfs.dmi
index 6a9956844f4d..778966219458 100644
Binary files a/icons/map_icons/turfs.dmi and b/icons/map_icons/turfs.dmi differ
diff --git a/icons/map_icons/unsorted.dmi b/icons/map_icons/unsorted.dmi
index 6a9956844f4d..778966219458 100644
Binary files a/icons/map_icons/unsorted.dmi and b/icons/map_icons/unsorted.dmi differ
diff --git a/icons/mob/actions/backgrounds.dmi b/icons/mob/actions/backgrounds.dmi
index ecbda704fe12..1256caf05b41 100644
Binary files a/icons/mob/actions/backgrounds.dmi and b/icons/mob/actions/backgrounds.dmi differ
diff --git a/icons/mob/clothing/head/costume.dmi b/icons/mob/clothing/head/costume.dmi
index ac776302e255..2a745f50f94d 100644
Binary files a/icons/mob/clothing/head/costume.dmi and b/icons/mob/clothing/head/costume.dmi differ
diff --git a/icons/mob/clothing/head/hats.dmi b/icons/mob/clothing/head/hats.dmi
index 712dbad93e6c..e29206ddc65d 100644
Binary files a/icons/mob/clothing/head/hats.dmi and b/icons/mob/clothing/head/hats.dmi differ
diff --git a/icons/mob/clothing/under/captain.dmi b/icons/mob/clothing/under/captain.dmi
index 49de8ab784ed..05f6b707c019 100644
Binary files a/icons/mob/clothing/under/captain.dmi and b/icons/mob/clothing/under/captain.dmi differ
diff --git a/icons/mob/clothing/under/civilian.dmi b/icons/mob/clothing/under/civilian.dmi
index 0aeebe27c32c..0fe66ae4f42e 100644
Binary files a/icons/mob/clothing/under/civilian.dmi and b/icons/mob/clothing/under/civilian.dmi differ
diff --git a/icons/mob/inhands/equipment/tools_lefthand.dmi b/icons/mob/inhands/equipment/tools_lefthand.dmi
index c8936fde8da7..adaedb6483f7 100644
Binary files a/icons/mob/inhands/equipment/tools_lefthand.dmi and b/icons/mob/inhands/equipment/tools_lefthand.dmi differ
diff --git a/icons/mob/inhands/equipment/tools_righthand.dmi b/icons/mob/inhands/equipment/tools_righthand.dmi
index 192413a739f6..454a225c82a7 100644
Binary files a/icons/mob/inhands/equipment/tools_righthand.dmi and b/icons/mob/inhands/equipment/tools_righthand.dmi differ
diff --git a/icons/mob/inhands/weapons/guns_lefthand.dmi b/icons/mob/inhands/weapons/guns_lefthand.dmi
index de6061573904..df7edf9077b7 100644
Binary files a/icons/mob/inhands/weapons/guns_lefthand.dmi and b/icons/mob/inhands/weapons/guns_lefthand.dmi differ
diff --git a/icons/mob/inhands/weapons/guns_righthand.dmi b/icons/mob/inhands/weapons/guns_righthand.dmi
index 4e97ea35e56a..62518d7646bf 100644
Binary files a/icons/mob/inhands/weapons/guns_righthand.dmi and b/icons/mob/inhands/weapons/guns_righthand.dmi differ
diff --git a/icons/mob/simple/arachnoid.dmi b/icons/mob/simple/arachnoid.dmi
index 7e15fde68524..35e5212b8173 100644
Binary files a/icons/mob/simple/arachnoid.dmi and b/icons/mob/simple/arachnoid.dmi differ
diff --git a/icons/mob/simple/broadMobs.dmi b/icons/mob/simple/broadMobs.dmi
index 00d01226f8ee..ae99a6ad7f05 100644
Binary files a/icons/mob/simple/broadMobs.dmi and b/icons/mob/simple/broadMobs.dmi differ
diff --git a/icons/mob/simple/lavaland/bileworm.dmi b/icons/mob/simple/lavaland/bileworm.dmi
index e3072364e934..e3206d648d55 100644
Binary files a/icons/mob/simple/lavaland/bileworm.dmi and b/icons/mob/simple/lavaland/bileworm.dmi differ
diff --git a/icons/mob/simple/lavaland/bileworm_jump.dmi b/icons/mob/simple/lavaland/bileworm_jump.dmi
new file mode 100644
index 000000000000..5f7491f1aa12
Binary files /dev/null and b/icons/mob/simple/lavaland/bileworm_jump.dmi differ
diff --git a/icons/mob/simple/lavaland/bileworm_old.dmi b/icons/mob/simple/lavaland/bileworm_old.dmi
new file mode 100644
index 000000000000..8337f6cc4f18
Binary files /dev/null and b/icons/mob/simple/lavaland/bileworm_old.dmi differ
diff --git a/icons/mob/simple/lavaland/lavaland_monsters.dmi b/icons/mob/simple/lavaland/lavaland_monsters.dmi
index 3f5eb9cbd3b5..f338fab310ae 100644
Binary files a/icons/mob/simple/lavaland/lavaland_monsters.dmi and b/icons/mob/simple/lavaland/lavaland_monsters.dmi differ
diff --git a/icons/mob/simple/lavaland/tendril.dmi b/icons/mob/simple/lavaland/tendril.dmi
new file mode 100644
index 000000000000..0bbd0ccbe218
Binary files /dev/null and b/icons/mob/simple/lavaland/tendril.dmi differ
diff --git a/icons/mob/simple/mob.dmi b/icons/mob/simple/mob.dmi
index 74b90ffd95b7..d34276cd3f3b 100644
Binary files a/icons/mob/simple/mob.dmi and b/icons/mob/simple/mob.dmi differ
diff --git a/icons/mob/telegraphing/telegraph.dmi b/icons/mob/telegraphing/telegraph.dmi
index fb8b7bc83fdb..1483234c763b 100644
Binary files a/icons/mob/telegraphing/telegraph.dmi and b/icons/mob/telegraphing/telegraph.dmi differ
diff --git a/icons/obj/clothing/head/costume.dmi b/icons/obj/clothing/head/costume.dmi
index fce68eb8f212..ef9ede6a6f1d 100644
Binary files a/icons/obj/clothing/head/costume.dmi and b/icons/obj/clothing/head/costume.dmi differ
diff --git a/icons/obj/clothing/head/hats.dmi b/icons/obj/clothing/head/hats.dmi
index b30c0c81ace0..6ba6906d2aa7 100644
Binary files a/icons/obj/clothing/head/hats.dmi and b/icons/obj/clothing/head/hats.dmi differ
diff --git a/icons/obj/clothing/modsuit/mod_modules.dmi b/icons/obj/clothing/modsuit/mod_modules.dmi
index 7ff5dcd95a99..4e4b07b70fff 100644
Binary files a/icons/obj/clothing/modsuit/mod_modules.dmi and b/icons/obj/clothing/modsuit/mod_modules.dmi differ
diff --git a/icons/obj/clothing/under/captain.dmi b/icons/obj/clothing/under/captain.dmi
index 335ce7332530..8192379d1011 100644
Binary files a/icons/obj/clothing/under/captain.dmi and b/icons/obj/clothing/under/captain.dmi differ
diff --git a/icons/obj/clothing/under/civilian.dmi b/icons/obj/clothing/under/civilian.dmi
index 572263d90851..0992bbfa88a9 100644
Binary files a/icons/obj/clothing/under/civilian.dmi and b/icons/obj/clothing/under/civilian.dmi differ
diff --git a/icons/obj/devices/tool.dmi b/icons/obj/devices/tool.dmi
index 9075c0d20630..5a4f58b0de33 100644
Binary files a/icons/obj/devices/tool.dmi and b/icons/obj/devices/tool.dmi differ
diff --git a/icons/obj/fluff/flora/rocks.dmi b/icons/obj/fluff/flora/rocks.dmi
index b972fef0e7eb..c68ddaef7d06 100644
Binary files a/icons/obj/fluff/flora/rocks.dmi and b/icons/obj/fluff/flora/rocks.dmi differ
diff --git a/icons/obj/food/food.dmi b/icons/obj/food/food.dmi
index 84c9248a3641..0ed1aaae4b97 100644
Binary files a/icons/obj/food/food.dmi and b/icons/obj/food/food.dmi differ
diff --git a/icons/obj/machines/camera.dmi b/icons/obj/machines/camera.dmi
index 0b0ba30483ca..48fbc621f27d 100644
Binary files a/icons/obj/machines/camera.dmi and b/icons/obj/machines/camera.dmi differ
diff --git a/icons/obj/machines/kitchen.dmi b/icons/obj/machines/kitchen.dmi
index 2cc92972f9e5..330841573610 100644
Binary files a/icons/obj/machines/kitchen.dmi and b/icons/obj/machines/kitchen.dmi differ
diff --git a/icons/obj/machines/mining_machines.dmi b/icons/obj/machines/mining_machines.dmi
index c1a4076e3a2f..a895f18da003 100644
Binary files a/icons/obj/machines/mining_machines.dmi and b/icons/obj/machines/mining_machines.dmi differ
diff --git a/icons/obj/machines/telepad.dmi b/icons/obj/machines/telepad.dmi
index b06436ebd631..c7f72d09d516 100644
Binary files a/icons/obj/machines/telepad.dmi and b/icons/obj/machines/telepad.dmi differ
diff --git a/icons/obj/medical/bodybag.dmi b/icons/obj/medical/bodybag.dmi
index 5ea5a2f6e389..66ec86e6074c 100644
Binary files a/icons/obj/medical/bodybag.dmi and b/icons/obj/medical/bodybag.dmi differ
diff --git a/icons/obj/mining_zones/artefacts.dmi b/icons/obj/mining_zones/artefacts.dmi
index 4c4dda6aa20f..1e9325be8d0e 100644
Binary files a/icons/obj/mining_zones/artefacts.dmi and b/icons/obj/mining_zones/artefacts.dmi differ
diff --git a/icons/obj/mining_zones/ash_flora.dmi b/icons/obj/mining_zones/ash_flora.dmi
index d796c200071c..5d64b1b931b9 100644
Binary files a/icons/obj/mining_zones/ash_flora.dmi and b/icons/obj/mining_zones/ash_flora.dmi differ
diff --git a/icons/obj/mining_zones/terrain.dmi b/icons/obj/mining_zones/terrain.dmi
index f46a677296b1..edbd2958a691 100644
Binary files a/icons/obj/mining_zones/terrain.dmi and b/icons/obj/mining_zones/terrain.dmi differ
diff --git a/icons/obj/ore.dmi b/icons/obj/ore.dmi
index 81a7cb03fdf2..8cb07a5312d0 100644
Binary files a/icons/obj/ore.dmi and b/icons/obj/ore.dmi differ
diff --git a/icons/obj/science/gizmos.dmi b/icons/obj/science/gizmos.dmi
new file mode 100644
index 000000000000..30792121a2c0
Binary files /dev/null and b/icons/obj/science/gizmos.dmi differ
diff --git a/icons/obj/service/hydroponics/equipment.dmi b/icons/obj/service/hydroponics/equipment.dmi
index 24a8d7bc5f93..4378a61cf8f3 100644
Binary files a/icons/obj/service/hydroponics/equipment.dmi and b/icons/obj/service/hydroponics/equipment.dmi differ
diff --git a/icons/obj/service/hydroponics/seeds.dmi b/icons/obj/service/hydroponics/seeds.dmi
index e180060041be..d08fbb8541dc 100644
Binary files a/icons/obj/service/hydroponics/seeds.dmi and b/icons/obj/service/hydroponics/seeds.dmi differ
diff --git a/icons/obj/stack_objects.dmi b/icons/obj/stack_objects.dmi
index 808453958034..8504bbc46906 100644
Binary files a/icons/obj/stack_objects.dmi and b/icons/obj/stack_objects.dmi differ
diff --git a/icons/obj/storage/crates.dmi b/icons/obj/storage/crates.dmi
index 3770375556b7..cf62706c5a18 100644
Binary files a/icons/obj/storage/crates.dmi and b/icons/obj/storage/crates.dmi differ
diff --git a/icons/obj/weapons/guns/ammo.dmi b/icons/obj/weapons/guns/ammo.dmi
index 168b55dfcb6b..06eb704f52dc 100644
Binary files a/icons/obj/weapons/guns/ammo.dmi and b/icons/obj/weapons/guns/ammo.dmi differ
diff --git a/icons/obj/weapons/guns/energy.dmi b/icons/obj/weapons/guns/energy.dmi
index 5d3834b6030b..46e2ceefdc46 100644
Binary files a/icons/obj/weapons/guns/energy.dmi and b/icons/obj/weapons/guns/energy.dmi differ
diff --git a/icons/obj/weapons/guns/projectiles.dmi b/icons/obj/weapons/guns/projectiles.dmi
index a78f5fa3e369..19bdf00bac92 100644
Binary files a/icons/obj/weapons/guns/projectiles.dmi and b/icons/obj/weapons/guns/projectiles.dmi differ
diff --git a/icons/obj/weapons/restraints.dmi b/icons/obj/weapons/restraints.dmi
index ddd3bf964369..aa24ef81c40d 100644
Binary files a/icons/obj/weapons/restraints.dmi and b/icons/obj/weapons/restraints.dmi differ
diff --git a/icons/turf/debug.dmi b/icons/turf/debug.dmi
index 4bc24f6c7878..6864dd09e6c8 100644
Binary files a/icons/turf/debug.dmi and b/icons/turf/debug.dmi differ
diff --git a/icons/turf/floors/basalt_outline.dmi b/icons/turf/floors/basalt_outline.dmi
new file mode 100644
index 000000000000..e2d774c00a3f
Binary files /dev/null and b/icons/turf/floors/basalt_outline.dmi differ
diff --git a/icons/turf/floors/basalt_outline.png b/icons/turf/floors/basalt_outline.png
new file mode 100644
index 000000000000..ce68171f9601
Binary files /dev/null and b/icons/turf/floors/basalt_outline.png differ
diff --git a/icons/turf/floors/basalt_outline.png.toml b/icons/turf/floors/basalt_outline.png.toml
new file mode 100644
index 000000000000..ebc026f21ca9
--- /dev/null
+++ b/icons/turf/floors/basalt_outline.png.toml
@@ -0,0 +1,2 @@
+output_name = "basalt_outline"
+template = "bitmask/diagonal_32x32.toml"
diff --git a/icons/turf/floors/shale.dmi b/icons/turf/floors/shale.dmi
new file mode 100644
index 000000000000..e7bdc5af6f65
Binary files /dev/null and b/icons/turf/floors/shale.dmi differ
diff --git a/icons/turf/floors/shale.png b/icons/turf/floors/shale.png
new file mode 100644
index 000000000000..3bc0d0247d52
Binary files /dev/null and b/icons/turf/floors/shale.png differ
diff --git a/icons/turf/floors/shale.png.toml b/icons/turf/floors/shale.png.toml
new file mode 100644
index 000000000000..2b6b33e54e25
--- /dev/null
+++ b/icons/turf/floors/shale.png.toml
@@ -0,0 +1,14 @@
+output_name = "shale"
+template = "bitmask/diagonal_32x32.toml"
+
+[icon_size]
+x = 48
+y = 48
+
+[output_icon_size]
+x = 48
+y = 48
+
+[cut_pos]
+x = 24
+y = 24
diff --git a/icons/turf/floors/shale_outline.dmi b/icons/turf/floors/shale_outline.dmi
new file mode 100644
index 000000000000..7c1313a093bb
Binary files /dev/null and b/icons/turf/floors/shale_outline.dmi differ
diff --git a/icons/turf/floors/shale_outline.png b/icons/turf/floors/shale_outline.png
new file mode 100644
index 000000000000..dd4104b06ee8
Binary files /dev/null and b/icons/turf/floors/shale_outline.png differ
diff --git a/icons/turf/floors/shale_outline.png.toml b/icons/turf/floors/shale_outline.png.toml
new file mode 100644
index 000000000000..3c104a643f64
--- /dev/null
+++ b/icons/turf/floors/shale_outline.png.toml
@@ -0,0 +1,2 @@
+output_name = "shale_outline"
+template = "bitmask/diagonal_32x32.toml"
diff --git a/icons/turf/floors/shale_variants.dmi b/icons/turf/floors/shale_variants.dmi
new file mode 100644
index 000000000000..3a6b69812274
Binary files /dev/null and b/icons/turf/floors/shale_variants.dmi differ
diff --git a/icons/turf/floors/siderite.dmi b/icons/turf/floors/siderite.dmi
new file mode 100644
index 000000000000..570ddb23c91d
Binary files /dev/null and b/icons/turf/floors/siderite.dmi differ
diff --git a/icons/turf/floors/siderite.png b/icons/turf/floors/siderite.png
new file mode 100644
index 000000000000..989acc29e067
Binary files /dev/null and b/icons/turf/floors/siderite.png differ
diff --git a/icons/turf/floors/siderite.png.toml b/icons/turf/floors/siderite.png.toml
new file mode 100644
index 000000000000..70f87a5c0ffb
--- /dev/null
+++ b/icons/turf/floors/siderite.png.toml
@@ -0,0 +1,14 @@
+output_name = "siderite"
+template = "bitmask/diagonal_32x32.toml"
+
+[icon_size]
+x = 48
+y = 48
+
+[output_icon_size]
+x = 48
+y = 48
+
+[cut_pos]
+x = 24
+y = 24
diff --git a/icons/turf/floors/siderite_outline.dmi b/icons/turf/floors/siderite_outline.dmi
new file mode 100644
index 000000000000..adbf092ee485
Binary files /dev/null and b/icons/turf/floors/siderite_outline.dmi differ
diff --git a/icons/turf/floors/siderite_outline.png b/icons/turf/floors/siderite_outline.png
new file mode 100644
index 000000000000..14da6135eda9
Binary files /dev/null and b/icons/turf/floors/siderite_outline.png differ
diff --git a/icons/turf/floors/siderite_outline.png.toml b/icons/turf/floors/siderite_outline.png.toml
new file mode 100644
index 000000000000..ec6db4a74167
--- /dev/null
+++ b/icons/turf/floors/siderite_outline.png.toml
@@ -0,0 +1,2 @@
+output_name = "siderite_outline"
+template = "bitmask/diagonal_32x32.toml"
diff --git a/icons/turf/floors/siderite_variants.dmi b/icons/turf/floors/siderite_variants.dmi
new file mode 100644
index 000000000000..2d4c03d12b03
Binary files /dev/null and b/icons/turf/floors/siderite_variants.dmi differ
diff --git a/icons/turf/mining.dmi b/icons/turf/mining.dmi
index 5a3a5af61f3d..79f41ca92ab2 100644
Binary files a/icons/turf/mining.dmi and b/icons/turf/mining.dmi differ
diff --git a/interface/interface.dm b/interface/interface.dm
index 3401262adf1f..08d9f5395516 100644
--- a/interface/interface.dm
+++ b/interface/interface.dm
@@ -146,13 +146,24 @@
if(prefs.lastchangelog != GLOB.changelog_hash)
prefs.lastchangelog = GLOB.changelog_hash
prefs.save_preferences()
- winset(src, "infobuttons.changelog", "font-style=;")
/client/verb/hotkeys_help()
set name = "Hotkeys Help"
- set category = "OOC"
+ set hidden = TRUE
if(!GLOB.hotkeys_tgui)
GLOB.hotkeys_tgui = new /datum/hotkeys_help()
GLOB.hotkeys_tgui.ui_interact(mob)
+
+/client/verb/emote_panel()
+ set name = "Emote Panel"
+ set hidden = TRUE
+
+ if(!isliving(mob))
+ to_chat(mob, span_notice("You can only use this while you're alive!"))
+ return
+
+ if(!GLOB.emote_panel)
+ GLOB.emote_panel = new /datum/emote_panel()
+ GLOB.emote_panel.ui_interact(mob)
diff --git a/interface/menu.dm b/interface/menu.dm
deleted file mode 100644
index ec9719e82365..000000000000
--- a/interface/menu.dm
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
-/datum/verbs/menu/Example/verb/Example()
- set name = "" //if this starts with @ the verb is not created and name becomes the command to invoke.
- set desc = "" //desc is the text given to this entry in the menu
- //You can not use src in these verbs. It will be the menu at compile time, but the client at runtime.
-*/
-
-GLOBAL_LIST_EMPTY(menulist)
-
-/datum/verbs/menu
- var/default //default checked type.
- //Set to true to append our children to our parent,
- //Rather then add us as a node (used for having more then one checkgroups in the same menu)
-
-/datum/verbs/menu/GetList()
- return GLOB.menulist
-
-/datum/verbs/menu/HandleVerb(list/entry, verbpath, client/C)
- return list2params(entry)
diff --git a/interface/skin.dmf b/interface/skin.dmf
index efb4047e95fe..915d7af8586f 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -8,38 +8,6 @@ macro "default"
name = "SHIFT+UP"
command = ".winset :map.right-click=true"
-
-menu "menu"
- elem
- name = "&File"
- command = ""
- saved-params = "is-checked"
- elem "reconnectbutton"
- name = "&Reconnect"
- command = ".reconnect"
- category = "&File"
- saved-params = "is-checked"
- elem
- name = "&Quit\tAlt-F4"
- command = ".quit"
- category = "&File"
- saved-params = "is-checked"
- elem "help-menu"
- name = "&Help"
- command = ""
- saved-params = "is-checked"
- elem
- name = "&Admin Help\tF1"
- command = "adminhelp"
- category = "&Help"
- saved-params = "is-checked"
- elem
- name = "&Hotkeys"
- command = "Hotkeys-Help"
- category = "&Help"
- saved-params = "is-checked"
-
-
window "mainwindow"
elem "mainwindow"
type = MAIN
@@ -52,7 +20,6 @@ window "mainwindow"
statusbar = false
icon = 'icons\\ui\\common\\vtm_32.png'
macro = "default"
- menu = "menu"
elem "split"
type = CHILD
pos = 0,0
@@ -138,6 +105,7 @@ window "info_and_buttons"
size = 640x477
anchor1 = 0,0
anchor2 = 100,100
+ background-color = #ffc41f
saved-params = "splitter"
left = "infobuttons"
right = "infowindow"
@@ -145,6 +113,82 @@ window "info_and_buttons"
splitter = 2
show-splitter = false
+window "infobuttons"
+ elem "infobuttons"
+ type = MAIN
+ pos = 291,0
+ size = 676x30
+ anchor1 = 0,0
+ anchor2 = 100,100
+ background-color = none
+ saved-params = "pos;size;is-minimized;is-maximized"
+ is-pane = true
+ outer-size = 692x88
+ outer-pos = 291,0
+ inner-size = 676x30
+ inner-pos = 8,31
+ screen-size = 1920x1040
+ elem "options"
+ type = BUTTON
+ pos = 0,5
+ size = 88x27
+ anchor1 = 0,0
+ anchor2 = 15,100
+ background-color = none
+ saved-params = "is-checked"
+ text = "Menu"
+ command = "open-escape-menu"
+ elem "hotkeys"
+ type = BUTTON
+ pos = 92,5
+ size = 88x27
+ anchor1 = 15,0
+ anchor2 = 29,100
+ background-color = none
+ saved-params = "is-checked"
+ text = "Hotkeys"
+ command = "Hotkeys-Help"
+ elem "emotes"
+ type = BUTTON
+ pos = 185,5
+ size = 88x27
+ anchor1 = 29,0
+ anchor2 = 43,100
+ background-color = none
+ saved-params = "is-checked"
+ text = "Emotes"
+ command = "Emote-Panel"
+ elem "fullscreen-toggle"
+ type = BUTTON
+ pos = 277,5
+ size = 88x27
+ anchor1 = 43,0
+ anchor2 = 57,100
+ background-color = none
+ saved-params = "is-checked"
+ text = "Fullscreen"
+ command = "fullscreen"
+ elem "reconnect"
+ type = BUTTON
+ pos = 369,5
+ size = 88x27
+ anchor1 = 57,0
+ anchor2 = 71,100
+ background-color = none
+ saved-params = "is-checked"
+ text = "Reconnect"
+ command = ".reconnect"
+ elem "chat"
+ type = BUTTON
+ pos = 560,5
+ size = 112x27
+ anchor1 = 85,0
+ anchor2 = 99,100
+ background-color = none
+ saved-params = "is-checked"
+ text = "Toggle Stat Panel"
+ command = "toggle-stat-panel"
+
window "infowindow"
elem "infowindow"
type = MAIN
@@ -162,7 +206,7 @@ window "infowindow"
size = 640x475
is-vert = false
splitter = 97.5
- background-color = #ffc41f
+ background-color = #ffc41f
show-splitter = false
saved-params = "splitter"
left = "info"
diff --git a/modular_darkpack/master_files/code/modules/client/preferences/_preference.dm b/modular_darkpack/master_files/code/modules/client/preferences/_preference.dm
index 27004194aab9..6a4c443e0919 100644
--- a/modular_darkpack/master_files/code/modules/client/preferences/_preference.dm
+++ b/modular_darkpack/master_files/code/modules/client/preferences/_preference.dm
@@ -4,3 +4,7 @@
/// If set to TRUE, only apply preference to the mob if it acctually shows up on there sheet
var/must_be_accessible = FALSE
+
+/datum/preference/proc/post_set_preference(mob/user, value)
+ SHOULD_CALL_PARENT(FALSE)
+ return
diff --git a/modular_darkpack/master_files/code/modules/fishing/sources/subtypes/turfs.dm b/modular_darkpack/master_files/code/modules/fishing/sources/subtypes/turfs.dm
index 715a6d65ced8..9f376edf3528 100644
--- a/modular_darkpack/master_files/code/modules/fishing/sources/subtypes/turfs.dm
+++ b/modular_darkpack/master_files/code/modules/fishing/sources/subtypes/turfs.dm
@@ -7,6 +7,7 @@
/obj/item/fish/darkpack/tuna = 20,
/obj/item/fish/darkpack/crab = 5,
/obj/item/fish/darkpack/shark = 5,
+ /obj/effect/spawner/random/occult/artifact = 1,
)
fish_counts = list(
///obj/structure/mystery_box/fishing = 1,
@@ -20,6 +21,7 @@
FISHING_DUD = 4,
/obj/effect/spawner/random/trash/garbage = 1,
/obj/item/fish/darkpack/catfish = 20,
+ /obj/effect/spawner/random/occult/artifact = 1,
)
fish_counts = list()
fish_count_regen = list()
@@ -29,4 +31,5 @@
FISHING_DUD = 15,
/obj/effect/spawner/random/trash/garbage = 5,
/obj/item/fish/darkpack/crab = 10,
+ /obj/effect/spawner/random/occult/artifact = 1,
)
diff --git a/modular_darkpack/master_files/code/modules/mob/living/carbon/human/life.dm b/modular_darkpack/master_files/code/modules/mob/living/carbon/human/life.dm
index b94d5c1538be..907e75b23935 100644
--- a/modular_darkpack/master_files/code/modules/mob/living/carbon/human/life.dm
+++ b/modular_darkpack/master_files/code/modules/mob/living/carbon/human/life.dm
@@ -1,5 +1,9 @@
-/mob/living/carbon/human/Life()
+/mob/living/carbon/human/Life(seconds_per_tick)
if(!get_kindred_splat(src))
if(prob(5))
adjust_agg_loss(-5, TRUE)
. = ..()
+
+ // SPLATS
+ for(var/datum/splat/splat in splats)
+ splat.splat_life(seconds_per_tick)
diff --git a/modular_darkpack/master_files/code/modules/rituals/code/ritual_tome.dm b/modular_darkpack/master_files/code/modules/rituals/code/ritual_tome.dm
index e6e037cf5728..1b7ef8449715 100644
--- a/modular_darkpack/master_files/code/modules/rituals/code/ritual_tome.dm
+++ b/modular_darkpack/master_files/code/modules/rituals/code/ritual_tome.dm
@@ -9,7 +9,7 @@
var/rune_type //ritual_rune/abyss, ritual_rune/thaumaturgy, etc
var/static/list/ritual_cache = list()
-/obj/item/ritual_tome/Initialize()
+/obj/item/ritual_tome/Initialize(mapload)
. = ..()
if(!rune_type)
return
@@ -24,10 +24,11 @@
/obj/item/ritual_tome/attack_self(mob/user)
. = ..()
- if(!isliving(user))
+ var/mob/living/reader = astype(user)
+ if(!reader)
return
- var/mob/living/reader = user
- if(!get_kindred_splat(user) && !get_ghoul_splat(user))
+
+ if(!get_splat_with_discipline(user))
if(reader.st_get_stat(STAT_OCCULT) < 3)
to_chat(reader, span_cult("A strange book that looks like it belongs in a dusty Library or a garage sale. You find yourself not caring, or understanding, too much about it."))
return
@@ -43,21 +44,17 @@
to_chat(user, span_cult("[level] [ritual_name] - [ritual_desc][requirements ? " Requirements: [requirements]." : ""]"))
-/obj/item/ritual_tome/proc/get_ritual_requirements(obj/rune)
- if(!islist(rune.vars["sacrifices"]))
- return ""
-
- var/list/sacrifices = rune.vars["sacrifices"]
- if(!length(sacrifices))
+/obj/item/ritual_tome/proc/get_ritual_requirements(obj/ritual_rune/rune)
+ if(!islist(rune.sacrifices) || !length(rune.sacrifices))
return ""
var/list/required_items = list()
- for(var/obj/item/item_type as anything in sacrifices)
+ for(var/obj/item/item_type as anything in rune.sacrifices)
required_items += item_type::name
return required_items.Join("\n")
-/obj/item/ritual_tome/proc/get_ritual_level(obj/rune)
- if(rune.vars["level"])
- return rune.vars["level"]
+/obj/item/ritual_tome/proc/get_ritual_level(obj/ritual_rune/rune)
+ if(rune.level)
+ return rune.level
return ""
diff --git a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_appendix.dm b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_appendix.dm
index 22ff5477e0c2..081532cc70ab 100644
--- a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_appendix.dm
+++ b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_appendix.dm
@@ -1,3 +1,3 @@
-/obj/item/organ/appendix/Initialize()
+/obj/item/organ/appendix/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling/organ, 50, "organ", TRUE, -1, 0)
diff --git a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_ears.dm b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_ears.dm
index 7fd5ddc068d6..a32a094b2057 100644
--- a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_ears.dm
+++ b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_ears.dm
@@ -1,3 +1,3 @@
-/obj/item/organ/ears/Initialize()
+/obj/item/organ/ears/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling/organ, 100, "organ", TRUE, -1, 0)
diff --git a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_eyes.dm b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_eyes.dm
index 36e12d677e57..e68b267bc471 100644
--- a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_eyes.dm
+++ b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_eyes.dm
@@ -1,3 +1,3 @@
-/obj/item/organ/eyes/Initialize()
+/obj/item/organ/eyes/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling/organ, 100, "organ", TRUE, -1, 0)
diff --git a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_heart.dm b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_heart.dm
index 3cea8e9ca607..7256fa2dec66 100644
--- a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_heart.dm
+++ b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_heart.dm
@@ -1,3 +1,3 @@
-/obj/item/organ/heart/Initialize()
+/obj/item/organ/heart/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling/organ, 400, "organ", TRUE, -1, 0)
diff --git a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_liver.dm b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_liver.dm
index 41df619e3f70..99cfb4cc4855 100644
--- a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_liver.dm
+++ b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_liver.dm
@@ -1,3 +1,3 @@
-/obj/item/organ/liver/Initialize()
+/obj/item/organ/liver/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling/organ, 200, "organ", TRUE, -1, 0)
diff --git a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_lungs.dm b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_lungs.dm
index c23aecd3c8c4..711eeedac246 100644
--- a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_lungs.dm
+++ b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_lungs.dm
@@ -1,3 +1,3 @@
-/obj/item/organ/lungs/Initialize()
+/obj/item/organ/lungs/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling/organ, 250, "organ", TRUE, -1, 0)
diff --git a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_stomach.dm b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_stomach.dm
index f81b1709c74e..b342b9209f36 100644
--- a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_stomach.dm
+++ b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_stomach.dm
@@ -1,3 +1,3 @@
-/obj/item/organ/stomach/Initialize()
+/obj/item/organ/stomach/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling/organ, 200, "organ", TRUE, -1, 0)
diff --git a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_tongue.dm b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_tongue.dm
index a40e9e382a5a..322b307ffeb2 100644
--- a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_tongue.dm
+++ b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_tongue.dm
@@ -1,3 +1,3 @@
-/obj/item/organ/tongue/Initialize()
+/obj/item/organ/tongue/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling/organ, 100, "organ", TRUE, -1, 0)
diff --git a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_vocal_chords.dm b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_vocal_chords.dm
index 5ae5fe4fbe6b..e1e344b36bf0 100644
--- a/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_vocal_chords.dm
+++ b/modular_darkpack/master_files/code/modules/surgery/bodyparts/organs/internal/_vocal_chords.dm
@@ -1,3 +1,3 @@
-/obj/item/organ/vocal_chords/Initialize()
+/obj/item/organ/vocal_chords/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling/organ, 200, "organ", TRUE, -1, 0)
diff --git a/modular_darkpack/master_files/icons/hud/screen_darkness.dmi b/modular_darkpack/master_files/icons/hud/screen_darkness.dmi
index 9848c17108fd..85e2a7e19e28 100644
Binary files a/modular_darkpack/master_files/icons/hud/screen_darkness.dmi and b/modular_darkpack/master_files/icons/hud/screen_darkness.dmi differ
diff --git a/modular_darkpack/master_files/icons/hud/screen_darkness_new.dmi b/modular_darkpack/master_files/icons/hud/screen_darkness_new.dmi
index 525f7077e656..8e4ca69c2c78 100644
Binary files a/modular_darkpack/master_files/icons/hud/screen_darkness_new.dmi and b/modular_darkpack/master_files/icons/hud/screen_darkness_new.dmi differ
diff --git a/modular_darkpack/master_files/icons/hud/screen_pentexknox.dmi b/modular_darkpack/master_files/icons/hud/screen_pentexknox.dmi
index ecec626fe3d2..184c10c36c49 100644
Binary files a/modular_darkpack/master_files/icons/hud/screen_pentexknox.dmi and b/modular_darkpack/master_files/icons/hud/screen_pentexknox.dmi differ
diff --git a/modular_darkpack/modules/billiards/code/billiard.dm b/modular_darkpack/modules/billiards/code/billiard.dm
index 9dc8c26d8cb0..cbbfd5656ad1 100644
--- a/modular_darkpack/modules/billiards/code/billiard.dm
+++ b/modular_darkpack/modules/billiards/code/billiard.dm
@@ -76,7 +76,7 @@
var/start_with_balls = TRUE
-/obj/structure/table/wood/billiard/Initialize()
+/obj/structure/table/wood/billiard/Initialize(mapload)
. = ..()
var/turf/my_turf = get_turf(src)
@@ -135,12 +135,12 @@
if(!choice)
return ITEM_INTERACT_BLOCKING
if(!length(get_balls_on_table(choice)))
- to_chat(user, span_warning("You cant aim for a [lowertext(choice)] because they are all sunk!"))
+ to_chat(user, span_warning("You cant aim for a [LOWER_TEXT(choice)] because they are all sunk!"))
return ITEM_INTERACT_BLOCKING
- user.visible_message(span_notice("[user] begins lining up a shot to hit a [lowertext(choice)]."), span_notice("You begin lining up a shot to hit a [lowertext(choice)]."))
+ user.visible_message(span_notice("[user] begins lining up a shot to hit a [LOWER_TEXT(choice)]."), span_notice("You begin lining up a shot to hit a [LOWER_TEXT(choice)]."))
if(!do_after(user, 1 TURNS, src))
return ITEM_INTERACT_BLOCKING
- user.visible_message(span_notice("[user] strikes a [lowertext(choice)]!"), span_notice("You strike your target!"))
+ user.visible_message(span_notice("[user] strikes a [LOWER_TEXT(choice)]!"), span_notice("You strike your target!"))
playsound(src, 'modular_darkpack/modules/billiards/sounds/poolball_strike.ogg', 75)
var/datum/storyteller_roll/pool_aiming/accuracy_roll = new()
diff --git a/modular_darkpack/modules/blood_drinking/code/overfeeding/check_can_drink_dry.dm b/modular_darkpack/modules/blood_drinking/code/overfeeding/check_can_drink_dry.dm
index 0eb70ac781c8..5c87fa8cb153 100644
--- a/modular_darkpack/modules/blood_drinking/code/overfeeding/check_can_drink_dry.dm
+++ b/modular_darkpack/modules/blood_drinking/code/overfeeding/check_can_drink_dry.dm
@@ -1,13 +1,13 @@
-/mob/living/carbon/human/proc/check_can_drink_dry(var/mob/living/mob)
- if(!get_kindred_splat(mob) || !get_kindred_splat(src))
+/mob/living/carbon/human/proc/check_can_drink_dry(mob/living/drunk_from_mob)
+ if(!get_kindred_splat(drunk_from_mob) || !get_kindred_splat(src))
return TRUE
- if(!mob.client)
- to_chat(src, span_warning("You need [mob]'s attention to do that..."))
+ if(!drunk_from_mob.client)
+ to_chat(src, span_warning("You need [drunk_from_mob]'s attention to do that..."))
return FALSE
- message_admins("[ADMIN_LOOKUPFLW(src)] is attempting to Diablerize [ADMIN_LOOKUPFLW(mob)]")
- log_attack("[key_name(src)] is attempting to Diablerize [key_name(mob)].")
+ message_admins("[ADMIN_LOOKUPFLW(src)] is attempting to Diablerize [ADMIN_LOOKUPFLW(drunk_from_mob)]")
+ log_attack("[key_name(src)] is attempting to Diablerize [key_name(drunk_from_mob)].")
- to_chat(src, span_userdanger(span_bold("YOU TRY TO COMMIT DIABLERIE ON [mob].")))
+ to_chat(src, span_userdanger(span_bold("YOU TRY TO COMMIT DIABLERIE ON [drunk_from_mob].")))
return TRUE
diff --git a/modular_darkpack/modules/blood_drinking/code/overfeeding/handle_drink_dry.dm b/modular_darkpack/modules/blood_drinking/code/overfeeding/handle_drink_dry.dm
index b4178f854369..aa548f2b8a51 100644
--- a/modular_darkpack/modules/blood_drinking/code/overfeeding/handle_drink_dry.dm
+++ b/modular_darkpack/modules/blood_drinking/code/overfeeding/handle_drink_dry.dm
@@ -1,8 +1,8 @@
-/mob/living/carbon/human/proc/handle_drink_dry(var/mob/living/mob)
- if(get_kindred_splat(mob) && get_kindred_splat(src))
- handle_diablerie(mob)
- else if(ishuman(mob))
- handle_overfeeding(mob)
+/mob/living/carbon/human/proc/handle_drink_dry(mob/living/drunk_from_mob)
+ if(get_kindred_splat(drunk_from_mob) && get_kindred_splat(src))
+ handle_diablerie(drunk_from_mob)
+ else if(ishuman(drunk_from_mob))
+ handle_overfeeding(drunk_from_mob)
else
- if(mob.stat != DEAD)
- mob.death()
+ if(drunk_from_mob.stat != DEAD)
+ drunk_from_mob.death()
diff --git a/modular_darkpack/modules/blood_drinking/code/overfeeding/handle_overfeeding.dm b/modular_darkpack/modules/blood_drinking/code/overfeeding/handle_overfeeding.dm
index d9a480453209..37e95f86b571 100644
--- a/modular_darkpack/modules/blood_drinking/code/overfeeding/handle_overfeeding.dm
+++ b/modular_darkpack/modules/blood_drinking/code/overfeeding/handle_overfeeding.dm
@@ -1,4 +1,4 @@
-/mob/living/carbon/human/proc/handle_overfeeding(var/mob/living/carbon/human/human_mob)
+/mob/living/carbon/human/proc/handle_overfeeding(mob/living/carbon/human/human_mob)
human_mob.blood_volume = 0
if(human_mob.stat != DEAD)
if(isnpc(human_mob))
diff --git a/modular_darkpack/modules/cargo/code/supply_packs/weapons.dm b/modular_darkpack/modules/cargo/code/supply_packs/weapons.dm
index 3bae8b0d9f24..02c2b05eb442 100644
--- a/modular_darkpack/modules/cargo/code/supply_packs/weapons.dm
+++ b/modular_darkpack/modules/cargo/code/supply_packs/weapons.dm
@@ -221,6 +221,13 @@
contains = list(/obj/item/ammo_box/darkpack/c12g/silver)
crate_name = "ammo crate"
+/datum/supply_pack/weapons/ammo12g/incendiary
+ name = "Ammo (12g, Dragon's Breath)"
+ desc = "Contains a box of 12g incendiary shells."
+ cost = 4000
+ contains = list(/obj/item/ammo_box/darkpack/c12g/buck/incendiary)
+ crate_name = "ammo crate"
+
/datum/supply_pack/weapons/ammo545
name = "Ammo (5.45)"
desc = "Contains a box of 5.45 ammunition."
diff --git a/modular_darkpack/modules/cars/code/gas.dm b/modular_darkpack/modules/cars/code/gas.dm
index fac793c55c63..f0f6f6ca5860 100644
--- a/modular_darkpack/modules/cars/code/gas.dm
+++ b/modular_darkpack/modules/cars/code/gas.dm
@@ -96,7 +96,7 @@
. = ..()
*/
-/obj/effect/decal/cleanable/gasoline/Initialize()
+/obj/effect/decal/cleanable/gasoline/Initialize(mapload)
. = ..()
var/turf/open/my_turf = get_turf(src)
if(istype(my_turf))
diff --git a/modular_darkpack/modules/decor/code/decor.dm b/modular_darkpack/modules/decor/code/decor.dm
index 9a8e432409dd..51bd0053083a 100644
--- a/modular_darkpack/modules/decor/code/decor.dm
+++ b/modular_darkpack/modules/decor/code/decor.dm
@@ -174,6 +174,9 @@
new /obj/effect/spawner/random/maintenance(src)
if(prob(external_trash_chance))
new /obj/effect/spawner/random/trash/grime(loc)
+ //artifacts
+ if(prob(CONFIG_GET(number/artifact_crate_probability)))
+ new /obj/effect/spawner/random/occult/artifact(src)
/obj/structure/closet/crate/dumpster/empty
internal_trash_chance = 0
@@ -451,7 +454,6 @@
name = "underplate"
icon = 'modular_darkpack/modules/decor/icons/restaurant.dmi'
icon_state = "underplate"
- layer = TABLE_LAYER
anchored = TRUE
/obj/underplate/stuff
@@ -519,17 +521,6 @@
sleep(6)
user.dir = 2
-/obj/structure/fire_barrel
- name = "barrel"
- desc = "Some kind of light and warm source..."
- icon = 'modular_darkpack/modules/decor/icons/fires.dmi'
- icon_state = "fire_barrel_on_fire"
- anchored = TRUE
- density = TRUE
- light_range = 3
- light_power = 2
- light_color = "#ffa800"
-
/obj/structure/fountain
name = "fountain"
desc = "Gothic water structure."
diff --git a/modular_darkpack/modules/decor/code/stick.dm b/modular_darkpack/modules/decor/code/stick.dm
index 7368c5e0e7c4..40092e50f17e 100644
--- a/modular_darkpack/modules/decor/code/stick.dm
+++ b/modular_darkpack/modules/decor/code/stick.dm
@@ -14,7 +14,7 @@
'modular_darkpack/modules/decor/sound/stick_snap5.ogg',
'modular_darkpack/modules/decor/sound/stick_snap6.ogg')
-/obj/effect/mine/stick/Initialize()
+/obj/effect/mine/stick/Initialize(mapload)
. = ..()
if(!stick_type)
stick_type = rand(1,variants)
diff --git a/modular_darkpack/modules/do_emotes/code/do_verbs.dm b/modular_darkpack/modules/do_emotes/code/do_verbs.dm
index e19e053937f8..96e87bab5b33 100644
--- a/modular_darkpack/modules/do_emotes/code/do_verbs.dm
+++ b/modular_darkpack/modules/do_emotes/code/do_verbs.dm
@@ -1,6 +1,6 @@
/mob/verb/do_verb()
set name = "Do"
- set category = "IC"
+ set hidden = TRUE
if(GLOB.say_disabled) // This is here to try to identify lag problems
to_chat(usr, span_danger("Speech is currently admin-disabled."))
return
@@ -44,9 +44,9 @@
if((ghost.client?.prefs.chat_toggles & CHAT_GHOSTSIGHT) && !(ghost in viewers))
to_chat(ghost, "[FOLLOW_LINK(ghost, user)] [span_emote(message_with_name)]")
- for(var/mob/reciever in viewers)
- name_stub = " ([GET_GUESTBOOK_NAME(reciever, user)])"
+ for(var/mob/receiver in viewers)
+ name_stub = " ([GET_GUESTBOOK_NAME(receiver, user)])"
message_with_name = message + name_stub
- reciever.show_message(span_emote(message_with_name), alt_msg = span_emote(message_with_name))
- if (reciever.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat))
- reciever.create_chat_message(user, null, message, null, EMOTE_MESSAGE)
+ receiver.show_message(span_emote(message_with_name), alt_msg = span_emote(message_with_name))
+ if (receiver.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat))
+ receiver.create_chat_message(user, null, message, null, EMOTE_MESSAGE)
diff --git a/modular_darkpack/modules/ert/code/items/first_team.dm b/modular_darkpack/modules/ert/code/items/first_team.dm
index 6db3421e5cce..0be78c8acdf5 100644
--- a/modular_darkpack/modules/ert/code/items/first_team.dm
+++ b/modular_darkpack/modules/ert/code/items/first_team.dm
@@ -79,7 +79,7 @@
resistance_flags = NONE
brand = "pentex"
-/obj/item/clothing/suit/vampire/darkpack_ert/Initialize()
+/obj/item/clothing/suit/vampire/darkpack_ert/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling, 200, "suit", FALSE)
@@ -108,7 +108,7 @@
ONFLOOR_ICON_HELPER('modular_darkpack/modules/ert/icons/onfloor.dmi')
brand = "pentex"
-/obj/item/clothing/under/vampire/darkpack_ert/Initialize()
+/obj/item/clothing/under/vampire/darkpack_ert/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling, 100, "undersuit", FALSE)
@@ -149,26 +149,30 @@
/obj/projectile/bullet/darkpack/vamp556mm/bale/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
- if(get_kindred_splat(target) || get_ghoul_splat(target))
- var/mob/living/carbon/human/H = target
- if(H.bloodpool == 0)
- to_chat(H, span_warning("Only ash remains in my veins!"))
- H.apply_damage(20, BURN)
+ bale_fire_damage(target, 4, bloodloss)
+
+/obj/projectile/bullet/proc/bale_fire_damage(mob/living/carbon/human/target, dice = 0, bloodloss = 1)
+ if(!istype(target))
+ return
+
+ var/datum/splat/werewolf/shifter/shot_pup_splat = get_shifter_splat(target)
+ if(shot_pup_splat)
+ target.apply_status_effect(STATUS_EFFECT_SILVER_BULLET_STACKS)
+ target.apply_damage(dice TTRPG_DAMAGE, AGGRAVATED)
+ playsound(target, 'modular_darkpack/modules/ert/sounds/balefire.ogg', rand(10,15), TRUE)
+ return
+
+ var/datum/splat/vampire/shot_vampire_splat = get_vampire_splat(target)
+ if(shot_vampire_splat)
+ if(target.bloodpool <= 0)
+ to_chat(target, span_warning("Only ash remains in my veins!"))
+ target.apply_damage(dice TTRPG_DAMAGE, BURN)
return
- H.adjust_blood_pool(-bloodloss)
- playsound(H, 'modular_darkpack/modules/ert/sounds/balefire.ogg', rand(10,15), TRUE)
- to_chat(H, span_warning("Green flames errupt from the bullets impact, boiling your blood!"))
-// DARKPACK TODO - GAROU
-/*
- if(iswerewolf(target) || get_garou_splat(target))
- var/mob/living/carbon/M = target
- if(M.auspice.gnosis)
- if(prob(50))
- adjust_gnosis(-1, M)
- M.apply_damage(20, CLONE)
- playsound(M, 'modular_tfn/modules/first_team/audio/balefire.ogg', rand(10,15), TRUE)
- M.apply_status_effect(STATUS_EFFECT_SILVER_SLOWDOWN)
-*/
+ target.adjust_blood_pool(-bloodloss)
+ playsound(target, 'modular_darkpack/modules/ert/sounds/balefire.ogg', rand(10,15), TRUE)
+ to_chat(target, span_warning("Green flames errupt from the bullets impact, boiling your blood!"))
+
+
/obj/item/ammo_casing/vampire/c12g/f12g
name = "Frag-12g shell casing"
desc = "A 12g explosive shell casing."
@@ -307,7 +311,7 @@
masquerade_violating = TRUE
// brand = "fullforce" // TODO: implement the rest of the non-top 21 pentex subsids
-/obj/item/gun/ballistic/automatic/darkpack/px66f/Initialize()
+/obj/item/gun/ballistic/automatic/darkpack/px66f/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling, 350, "aug", FALSE)
AddComponent(/datum/component/automatic_fire, 0.5 SECONDS)
diff --git a/modular_darkpack/modules/events/code/sarcophagus_event.dm b/modular_darkpack/modules/events/code/sarcophagus_event.dm
index bb9ef472a19b..680c58da9165 100644
--- a/modular_darkpack/modules/events/code/sarcophagus_event.dm
+++ b/modular_darkpack/modules/events/code/sarcophagus_event.dm
@@ -11,6 +11,8 @@
/datum/round_event_control/darkpack/sarcophagus/can_spawn_event(players_amt, allow_magic)
. = ..()
+ if(!.)
+ return FALSE
var/list/valid_landmarks = list()
for(var/obj/effect/landmark/event_spawn/sarcophagus/L in GLOB.generic_event_spawns)
var/player_nearby = FALSE
@@ -28,13 +30,14 @@
announce_when = 5
/datum/round_event/sarcophagus/announce(fake)
- priority_announce(
- "You receive a notification about a viral Endpost - a respected archaeologist notes that the location of a long-lost Assyrian sarcophagus alongside it's key, which was famously stolen, seems to be in your city according to newly published criminological records tracking the suspected thief.",
- "Viral News Story",
- 'modular_darkpack/modules/events/sounds/news_notification.ogg',
- ANNOUNCEMENT_TYPE_PRIORITY,
- color_override = "yellow",
- )
+ if(prob(20))
+ priority_announce(
+ "You receive a notification about a viral Endpost - a respected archaeologist notes that the location of a long-lost Assyrian sarcophagus alongside it's key, which was famously stolen, seems to be in your city according to newly published criminological records tracking the suspected thief.",
+ "Viral News Story",
+ 'modular_darkpack/modules/events/sounds/news_notification.ogg',
+ ANNOUNCEMENT_TYPE_PRIORITY,
+ color_override = "yellow",
+ )
/datum/round_event/sarcophagus/start()
var/list/landmarks = list()
diff --git a/modular_darkpack/modules/flavor_text/code/preferences.dm b/modular_darkpack/modules/flavor_text/code/preferences.dm
index af433a1c6259..fd5f578047e8 100644
--- a/modular_darkpack/modules/flavor_text/code/preferences.dm
+++ b/modular_darkpack/modules/flavor_text/code/preferences.dm
@@ -33,7 +33,7 @@
// extension will always be the last entry
var/extension = value_split[length(value_split)]
- if(!(lowertext(extension) in valid_extensions))
+ if(!(LOWER_TEXT(extension) in valid_extensions))
to_chat(usr, span_warning("The image must be one of the following extensions: '[english_list(valid_extensions)]'"))
return
diff --git a/modular_darkpack/modules/food/code/structure.dm b/modular_darkpack/modules/food/code/structure.dm
index c6db7cb4c5a5..04ceb0e9dbcf 100644
--- a/modular_darkpack/modules/food/code/structure.dm
+++ b/modular_darkpack/modules/food/code/structure.dm
@@ -7,7 +7,7 @@
anchored = TRUE
layer = ABOVE_MOB_LAYER
-/obj/structure/food_cart/Initialize()
+/obj/structure/food_cart/Initialize(mapload)
. = ..()
icon_state = "vat[rand(1, 3)]"
diff --git a/modular_darkpack/modules/forensics/code/serial_number_log.dm b/modular_darkpack/modules/forensics/code/serial_number_log.dm
index 346344ab7d21..b9e135a3338e 100644
--- a/modular_darkpack/modules/forensics/code/serial_number_log.dm
+++ b/modular_darkpack/modules/forensics/code/serial_number_log.dm
@@ -3,7 +3,7 @@
desc = "Checks all the guns in the placed area then prints their name and numbers on a generated piece of paper!"
// Needs to load AFTER everything else so it properly checks guns in the defined area.
-/obj/effect/mapping_helpers/serial_number_log/Initialize()
+/obj/effect/mapping_helpers/serial_number_log/Initialize(mapload)
..()
. = INITIALIZE_HINT_LATELOAD
diff --git a/modular_darkpack/modules/guestbook/code/human_helpers.dm b/modular_darkpack/modules/guestbook/code/human_helpers.dm
index caffb332351c..e9dca2231713 100644
--- a/modular_darkpack/modules/guestbook/code/human_helpers.dm
+++ b/modular_darkpack/modules/guestbook/code/human_helpers.dm
@@ -34,7 +34,7 @@
var/final_string = name
if(prefixed)
final_string = "\A [final_string]"
- return lowercase ? lowertext(final_string) : final_string
+ return lowercase ? LOWER_TEXT(final_string) : final_string
/mob/living/carbon/human/get_generic_name(prefixed = FALSE, lowercase = FALSE)
// var/visible_skin = GLOB.skin_tone_names[skin_tone] ? "[GLOB.skin_tone_names[skin_tone]] " : null // Removed until we think of a way to do this without calling people "ugly brown woman"
@@ -43,4 +43,4 @@
var/final_string = "[visible_adjective ? "[visible_adjective] " : null][visible_age ? "[visible_age] " : null][visible_gender]" // removed [visible_skin]
if(prefixed)
final_string = "\A [final_string]"
- return lowercase ? lowertext(final_string) : final_string
+ return lowercase ? LOWER_TEXT(final_string) : final_string
diff --git a/modular_darkpack/modules/jumping/code/crack_effect.dm b/modular_darkpack/modules/jumping/code/crack_effect.dm
index 1b2fb63cadb9..eb840388f630 100644
--- a/modular_darkpack/modules/jumping/code/crack_effect.dm
+++ b/modular_darkpack/modules/jumping/code/crack_effect.dm
@@ -8,6 +8,6 @@
plane = GAME_PLANE
layer = LOW_OBJ_LAYER
-/obj/effect/temp_visual/dir_setting/crack_effect/Initialize()
+/obj/effect/temp_visual/dir_setting/crack_effect/Initialize(mapload)
. = ..()
animate(src, alpha = 0, time = 50)
diff --git a/modular_darkpack/modules/masquerade/code/components/violation_observer.dm b/modular_darkpack/modules/masquerade/code/components/violation_observer.dm
index 50662339f8f6..8973784cd92b 100644
--- a/modular_darkpack/modules/masquerade/code/components/violation_observer.dm
+++ b/modular_darkpack/modules/masquerade/code/components/violation_observer.dm
@@ -61,7 +61,7 @@
/datum/component/violation_observer/proc/on_death(atom/source)
SIGNAL_HANDLER
- for(var/player_breacher as anything in breached_players)
+ for(var/player_breacher in breached_players)
SEND_SIGNAL(source, COMSIG_MASQUERADE_HUD_DELETE, player_breacher)
SSmasquerade.masquerade_reinforce(source, player_breacher)
source.observe_masquerade_reinforce(player_breacher)
diff --git a/modular_darkpack/modules/masquerade/code/fields/violation_check_aoe.dm b/modular_darkpack/modules/masquerade/code/fields/violation_check_aoe.dm
index 0f8575aaeebe..25d43b29071c 100644
--- a/modular_darkpack/modules/masquerade/code/fields/violation_check_aoe.dm
+++ b/modular_darkpack/modules/masquerade/code/fields/violation_check_aoe.dm
@@ -10,7 +10,7 @@
/datum/proximity_monitor/advanced/violation_check_aoe/Destroy()
violation_observer_callback = null
- for(var/mob as anything in tracking_mobs)
+ for(var/mob in tracking_mobs)
UnregisterSignal(mob, COMSIG_MASQUERADE_VIOLATION)
tracking_mobs = null
return ..()
@@ -54,7 +54,7 @@
/datum/proximity_monitor/advanced/violation_check_aoe/on_z_change()
if(QDELETED(src))
return
- for(var/mob as anything in tracking_mobs)
+ for(var/mob in tracking_mobs)
UnregisterSignal(mob, COMSIG_MASQUERADE_VIOLATION)
tracking_mobs -= mob
diff --git a/modular_darkpack/modules/masquerade/code/logging_machine.dm b/modular_darkpack/modules/masquerade/code/logging_machine.dm
index 3e0f2fafc00b..95968ff69210 100644
--- a/modular_darkpack/modules/masquerade/code/logging_machine.dm
+++ b/modular_darkpack/modules/masquerade/code/logging_machine.dm
@@ -10,7 +10,7 @@
var/datum/looping_sound/logging_machine/clearing_sound
COOLDOWN_DECLARE(printing_noise)
-/obj/machinery/logging_machine/Initialize()
+/obj/machinery/logging_machine/Initialize(mapload)
. = ..()
saved_logs = new()
clearing_sound = new(src, FALSE)
diff --git a/modular_darkpack/modules/masquerade/code/masquerade_contract.dm b/modular_darkpack/modules/masquerade/code/masquerade_contract.dm
index 6202168b5a31..113fab580174 100644
--- a/modular_darkpack/modules/masquerade/code/masquerade_contract.dm
+++ b/modular_darkpack/modules/masquerade/code/masquerade_contract.dm
@@ -19,7 +19,7 @@
return
var/turf/current_location = get_turf(user)
to_chat(user, "[span_bold("YOU")], [get_area_name(user)] X:[current_location.x] Y:[current_location.y] Z:[current_location.z]")
- for(var/mob/living/carbon/breacher as anything in GLOB.masquerade_breakers_list)
+ for(var/mob/living/carbon/breacher in GLOB.masquerade_breakers_list)
var/location_info
var/turf/turf = get_turf(breacher)
if(breacher.masquerade_score <= 2)
@@ -48,7 +48,7 @@
return
var/turf/current_location = get_turf(user)
to_chat(user, "[span_bold("YOU")], [get_area_name(user)] X:[current_location.x] Y:[current_location.y] Z:[current_location.z]")
- for(var/mob/living/breacher as anything in GLOB.veil_breakers_list)
+ for(var/mob/living/breacher in GLOB.veil_breakers_list)
var/location_info
var/turf/turf = get_turf(breacher)
if(breacher.masquerade_score <= 2)
diff --git a/modular_darkpack/modules/masquerade/code/subsystem/masquerade.dm b/modular_darkpack/modules/masquerade/code/subsystem/masquerade.dm
index d82bd0d70707..b9e5fc008835 100644
--- a/modular_darkpack/modules/masquerade/code/subsystem/masquerade.dm
+++ b/modular_darkpack/modules/masquerade/code/subsystem/masquerade.dm
@@ -47,7 +47,7 @@ SUBSYSTEM_DEF(masquerade)
*/
/datum/controller/subsystem/masquerade/proc/masquerade_reinforce(atom/source, mob/living/player_breacher, reason)
. = FALSE
- for(var/masquerade_breach as anything in masquerade_breachers)
+ for(var/masquerade_breach in masquerade_breachers)
var/breach_sources = masquerade_breach[2]
var/breach_reasons = masquerade_breach[3]
@@ -127,7 +127,7 @@ SUBSYSTEM_DEF(masquerade)
// This is for clearing the round's masquerade because a player matrix'd
/datum/controller/subsystem/masquerade/proc/matrix_masquerade_breacher(mob/living/player_breacher, update_preferences)
- for(var/masquerade_breach as anything in masquerade_breachers)
+ for(var/masquerade_breach in masquerade_breachers)
if((player_breacher in masquerade_breach))
masquerade_breachers -= list(masquerade_breach)
masquerade_level = min(MASQUERADE_MAX_LEVEL, masquerade_level + 1)
@@ -150,7 +150,7 @@ SUBSYSTEM_DEF(masquerade)
/datum/controller/subsystem/masquerade/proc/player_masquerade_reinforce(datum/source, mob/living/player_breacher)
SIGNAL_HANDLER
- for(var/masquerade_breach as anything in masquerade_breachers)
+ for(var/masquerade_breach in masquerade_breachers)
var/list/masquerade_breach_list = masquerade_breach
if(islist(masquerade_breach_list[2])) //If its the skull list, then its a long term masq breach. Clear it.
for(var/atom/list_object as anything in masquerade_breach_list[2])
@@ -166,13 +166,13 @@ SUBSYSTEM_DEF(masquerade)
if((masquerade_level != 0) || ending)
return
ending = TRUE
- for(var/player as anything in GLOB.player_list)
+ for(var/player in GLOB.player_list)
SEND_SOUND(player, 'modular_darkpack/modules/masquerade/sound/masquerade_failure.ogg') //Alerting them of their demise.
addtimer(CALLBACK(src, PROC_REF(end_round)), 65 SECONDS)
// Ending the actual round.
/datum/controller/subsystem/masquerade/proc/end_round()
- for(var/masquerade_breach as anything in masquerade_breachers)
+ for(var/masquerade_breach in masquerade_breachers)
var/list/masquerade_breach_list = masquerade_breach
if(islist(masquerade_breach_list[2])) //If its the skull list, then its a long term masq breach. Clear it.
for(var/atom/list_object as anything in masquerade_breach_list[2])
diff --git a/modular_darkpack/modules/masquerade/code/word_checker.dm b/modular_darkpack/modules/masquerade/code/word_checker.dm
index 6eea25ed7fa6..1df7499f3cf9 100644
--- a/modular_darkpack/modules/masquerade/code/word_checker.dm
+++ b/modular_darkpack/modules/masquerade/code/word_checker.dm
@@ -15,7 +15,7 @@
return FALSE
var/treated_message = translate_language(speaker, message_language, raw_message, spans, message_mods)
- if(lowertext(MASQUERADE_FILTER_CHECK(treated_message)))
+ if(LOWER_TEXT(MASQUERADE_FILTER_CHECK(treated_message)))
SEND_SIGNAL(src, COMSIG_SEEN_MASQUERADE_VIOLATION, speaker)
return TRUE
@@ -27,7 +27,7 @@
return
var/message = compose_message(hearing_args[HEARING_SPEAKER], hearing_args[HEARING_LANGUAGE], hearing_args[HEARING_RAW_MESSAGE], hearing_args[HEARING_RADIO_FREQ], hearing_args[HEARING_RADIO_FREQ_NAME], hearing_args[HEARING_RADIO_FREQ_COLOR], hearing_args[HEARING_SPANS], hearing_args[HEARING_MESSAGE_MODE], FALSE)
SSmasquerade.log_phone_message(message, source)
- if(MASQUERADE_FILTER_CHECK(lowertext(hearing_args[HEARING_RAW_MESSAGE])))
+ if(MASQUERADE_FILTER_CHECK(LOWER_TEXT(hearing_args[HEARING_RAW_MESSAGE])))
SEND_SIGNAL(src, COMSIG_SEEN_MASQUERADE_VIOLATION, hearing_args[HEARING_SPEAKER])
#undef MASQUERADE_FILTER_CHECK
diff --git a/modular_darkpack/modules/merits_flaws/code/negative_quirks/derangement.dm b/modular_darkpack/modules/merits_flaws/code/negative_quirks/derangement.dm
index b081b18fef0c..130acd27b90e 100644
--- a/modular_darkpack/modules/merits_flaws/code/negative_quirks/derangement.dm
+++ b/modular_darkpack/modules/merits_flaws/code/negative_quirks/derangement.dm
@@ -1,6 +1,50 @@
#define FLOOR_DISAPPEAR 3 SECONDS
+// associative list used by dementation and the derangement quirk
+GLOBAL_LIST_INIT(derangement_phrases,list(
+ "Evil crouches" = 'modular_darkpack/modules/powers/sounds/dementation/speech/crouch.ogg',
+ "Death" = 'modular_darkpack/modules/powers/sounds/dementation/speech/death.ogg',
+ "DIE!" = 'modular_darkpack/modules/powers/sounds/dementation/speech/die.ogg',
+ "I smell a rancid grave" = 'modular_darkpack/modules/powers/sounds/dementation/speech/grave.ogg',
+ "Rustling robes of the Reaper" = 'modular_darkpack/modules/powers/sounds/dementation/speech/reaper.ogg',
+ "All are blind whose eyes are closed" = 'modular_darkpack/modules/powers/sounds/dementation/speech/blind.ogg',
+ "The drove is a terrible mistress" = 'modular_darkpack/modules/powers/sounds/dementation/speech/mistress.ogg',
+ "Wishes and words sprout from the same seed" = 'modular_darkpack/modules/powers/sounds/dementation/speech/wishes_words.ogg',
+ "A dark light from your death" = 'modular_darkpack/modules/powers/sounds/dementation/speech/dark_light.ogg',
+ "Hemlock for the deceivers" = 'modular_darkpack/modules/powers/sounds/dementation/speech/hemlock.ogg',
+ "It has two mouths to lick from" = 'modular_darkpack/modules/powers/sounds/dementation/speech/two_mouths.ogg',
+ "Deep of the Atlantic, dark, dreaming, sleeping" = 'modular_darkpack/modules/powers/sounds/dementation/speech/atlantic.ogg',
+ "Can't see, can't see! Where have my eyes gone to?" = 'modular_darkpack/modules/powers/sounds/dementation/speech/eyes.ogg',
+ "Heloise said you. Cranberry sauce. Hotel foxtrot" = 'modular_darkpack/modules/powers/sounds/dementation/speech/heloise.ogg',
+ "Stop doing that. Mother shan't be too pleased. None too pleased" = 'modular_darkpack/modules/powers/sounds/dementation/speech/mother.ogg',
+ "Those lips bleed a putrid poison" = 'modular_darkpack/modules/powers/sounds/dementation/speech/putrid.ogg',
+ "Rat tails, cat tails, coat tails, all tales" = 'modular_darkpack/modules/powers/sounds/dementation/speech/tails.ogg',
+ "It's not fair! I wanted to" = 'modular_darkpack/modules/powers/sounds/dementation/speech/not_fair.ogg',
+ "Pennies for your eyes in its pockets" = 'modular_darkpack/modules/powers/sounds/dementation/speech/pennies.ogg',
+ "Why is it troubled?" = 'modular_darkpack/modules/powers/sounds/dementation/speech/troubled.ogg',
+ "Ask about the free arsenic" = 'modular_darkpack/modules/powers/sounds/dementation/speech/arsenic.ogg',
+ "Blood brings the vicious beast" = 'modular_darkpack/modules/powers/sounds/dementation/speech/beast.ogg',
+ "I see daggers hang on his breath" = 'modular_darkpack/modules/powers/sounds/dementation/speech/daggers.ogg',
+ "Bone round in melody and word layed in rain" = 'modular_darkpack/modules/powers/sounds/dementation/speech/bone.ogg',
+ "Cemetery runoff congealing at the door" = 'modular_darkpack/modules/powers/sounds/dementation/speech/cemetery.ogg',
+ "Maggots love you. Trust me" = 'modular_darkpack/modules/powers/sounds/dementation/speech/maggots.ogg',
+ "Mast lay shrouded and the moon is melting" = 'modular_darkpack/modules/powers/sounds/dementation/speech/moon.ogg',
+ "Try the corpse in the oven with peppers and fur" = 'modular_darkpack/modules/powers/sounds/dementation/speech/peppers.ogg',
+ "Souls draped in rotten tatters and Father dances in the dark" = 'modular_darkpack/modules/powers/sounds/dementation/speech/souls.ogg',
+ "Make the tallow from the fat of a hangman" = 'modular_darkpack/modules/powers/sounds/dementation/speech/tallow.ogg',
+ "Bent like a calf for the butcher" = 'modular_darkpack/modules/powers/sounds/dementation/speech/calf.ogg',
+ "You're in for it now" = 'modular_darkpack/modules/powers/sounds/dementation/speech/in_for_it.ogg',
+ "They're coming" = 'modular_darkpack/modules/powers/sounds/dementation/speech/theyre_coming.ogg',
+ "It casts a crooked shadow" = 'modular_darkpack/modules/powers/sounds/dementation/speech/shadow.ogg',
+ "Elkabo, elkabo, pixy queen where all is green" = 'modular_darkpack/modules/powers/sounds/dementation/speech/elkabo.ogg',
+ "It's a tangle of asps" = 'modular_darkpack/modules/powers/sounds/dementation/speech/asps.ogg',
+ "Sealed with the kiss of swine" = 'modular_darkpack/modules/powers/sounds/dementation/speech/swine.ogg',
+ "A trick with two tongues" = 'modular_darkpack/modules/powers/sounds/dementation/speech/tongues.ogg',
+ "The very thought falls to the flame" = 'modular_darkpack/modules/powers/sounds/dementation/speech/flame.ogg'
+))
+
+
/datum/quirk/darkpack/derangement
name = "Derangement"
desc = "Suffer from a permanent, incurable derangement that alters your perception."
diff --git a/modular_darkpack/modules/merits_flaws/code/negative_quirks/mage_blood.dm b/modular_darkpack/modules/merits_flaws/code/negative_quirks/mage_blood.dm
index 3a81f8eca09e..f5f501828995 100644
--- a/modular_darkpack/modules/merits_flaws/code/negative_quirks/mage_blood.dm
+++ b/modular_darkpack/modules/merits_flaws/code/negative_quirks/mage_blood.dm
@@ -11,6 +11,7 @@
if(!kindred_splat)
return
for(var/datum/action/discipline/action as anything in kindred_splat.powers)
- if(!istype(action.discipline, /datum/discipline/thaumaturgy))
+ // Unselectable Disciplines have special handling (e.g. Bloodheal) and are excluded
+ if(!istype(action.discipline, /datum/discipline/thaumaturgy) && action.discipline.selectable)
kindred_splat.remove_power(action.discipline.type)
diff --git a/modular_darkpack/modules/merits_flaws/code/negative_quirks/territorial.dm b/modular_darkpack/modules/merits_flaws/code/negative_quirks/territorial.dm
index ff78aea0e511..95e9cf311895 100644
--- a/modular_darkpack/modules/merits_flaws/code/negative_quirks/territorial.dm
+++ b/modular_darkpack/modules/merits_flaws/code/negative_quirks/territorial.dm
@@ -4,7 +4,7 @@ GLOBAL_LIST_INIT(territorial_type_choices, init_territorial_type_choices())
var/list/choices = list()
for(var/area/vtm/area_type as anything in subtypesof(/area/vtm))
var/area/vtm/typed = area_type
- var/area/vtm/parent = type2parent(area_type)
+ var/area/vtm/parent = area_type::parent_type
if(initial(typed.domain) && !initial(parent.domain))
choices[initial(typed.name)] = area_type
return choices
diff --git a/modular_darkpack/modules/npc/code/human/npc_human_subsystem.dm b/modular_darkpack/modules/npc/code/human/npc_human_subsystem.dm
index e1c48eeaf754..44b18ccbabbf 100644
--- a/modular_darkpack/modules/npc/code/human/npc_human_subsystem.dm
+++ b/modular_darkpack/modules/npc/code/human/npc_human_subsystem.dm
@@ -35,6 +35,11 @@ SUBSYSTEM_DEF(humannpcpool)
var/mob/living/carbon/human/npc/NPC = currentrun[currentrun.len]
--currentrun.len
+ if (QDELETED(NPC))
+ GLOB.npc_list -= NPC
+ stack_trace("Found a null in npc_list [NPC.type]!")
+ continue
+
if (MC_TICK_CHECK)
return
NPC.handle_automated_movement()
diff --git a/modular_darkpack/modules/npc/code/human/npc_movement.dm b/modular_darkpack/modules/npc/code/human/npc_movement.dm
index 7910ec308694..936c7ee304ed 100644
--- a/modular_darkpack/modules/npc/code/human/npc_movement.dm
+++ b/modular_darkpack/modules/npc/code/human/npc_movement.dm
@@ -110,6 +110,8 @@
return
if (!walktarget)
walktarget = ChoosePath()
+ if(walktarget)
+ EVLOG_PATH(src, EVLOG_CATEGORY_MOVELOOPS, "Set walktarget: [walktarget]", list(loc, get_turf(walktarget)))
if (loc == tupik_loc)
tupik_steps += 1
else
@@ -126,7 +128,10 @@
return
if (observed_by_player())
return
- forceMove(get_turf(walktarget))
+ var/turf/old_loc = loc
+ var/turf/new_loc = get_turf(walktarget)
+ forceMove(new_loc)
+ EVLOG_PATH(src, EVLOG_CATEGORY_MOVELOOPS, "Teleported using evil russian shitcode", list(old_loc, new_loc))
/mob/living/carbon/human/npc/proc/CreateWay(direction)
var/turf/location = get_turf(src)
diff --git a/modular_darkpack/modules/npc/code/human/npc_types/endron/endron_npcs.dm b/modular_darkpack/modules/npc/code/human/npc_types/endron/endron_npcs.dm
index 6ab28b61cd3e..21d1b155bae7 100644
--- a/modular_darkpack/modules/npc/code/human/npc_types/endron/endron_npcs.dm
+++ b/modular_darkpack/modules/npc/code/human/npc_types/endron/endron_npcs.dm
@@ -4,7 +4,7 @@
my_weapon_type = /obj/item/gun/ballistic/automatic/darkpack/mp5
my_backup_weapon_type = /obj/item/melee/baton/vamp
-/mob/living/carbon/human/npc/endronsecurity/Initialize()
+/mob/living/carbon/human/npc/endronsecurity/Initialize(mapload)
. = ..()
AssignSocialRole(/datum/socialrole/endronsecurity)
@@ -15,7 +15,7 @@
my_weapon_type = /obj/item/gun/ballistic/automatic/darkpack/mp5
my_backup_weapon_type = /obj/item/melee/baton/vamp
-/mob/living/carbon/human/npc/endronlabsecurity/Initialize()
+/mob/living/carbon/human/npc/endronlabsecurity/Initialize(mapload)
. = ..()
AssignSocialRole(/datum/socialrole/endronlabsecurity)
@@ -26,6 +26,6 @@
my_weapon_type = /obj/item/gun/ballistic/automatic/pistol/darkpack/deagle
my_backup_weapon_type = /obj/item/melee/baton/vamp
-/mob/living/carbon/human/npc/endronexecsecurity/Initialize()
+/mob/living/carbon/human/npc/endronexecsecurity/Initialize(mapload)
. = ..()
AssignSocialRole(/datum/socialrole/endronexecsecurity)
diff --git a/modular_darkpack/modules/npc/code/human/npc_types/police.dm b/modular_darkpack/modules/npc/code/human/npc_types/police.dm
index 1bb98623efd0..de9e1df8ce43 100644
--- a/modular_darkpack/modules/npc/code/human/npc_types/police.dm
+++ b/modular_darkpack/modules/npc/code/human/npc_types/police.dm
@@ -33,7 +33,7 @@
my_backup_weapon_type = /obj/item/melee/baton/vamp
/*
-/mob/living/carbon/human/npc/police/Initialize()
+/mob/living/carbon/human/npc/police/Initialize(mapload)
. = ..()
if(prob(66))
diff --git a/modular_darkpack/modules/paths/code/occult_research.dm b/modular_darkpack/modules/paths/code/occult_research.dm
index 71ee8c648ec4..f4604d05abce 100644
--- a/modular_darkpack/modules/paths/code/occult_research.dm
+++ b/modular_darkpack/modules/paths/code/occult_research.dm
@@ -41,8 +41,8 @@ SUBSYSTEM_DEF(occult_research)
/mob/living/carbon/human/proc/check_research_points()
set name = "Check Research Points"
- set category = "IC"
set desc = "Check your current research point balance."
+ set hidden = TRUE
if(!get_discipline(/datum/discipline/thaumaturgy))
to_chat(src, span_alert("You lack occult knowledge."))
@@ -56,14 +56,14 @@ SUBSYSTEM_DEF(occult_research)
return
var/blood_data = blood_sample.data
- var/blood_species = blood_data["species"]
+ var/blood_splat = blood_data["splat"]
var/blood_name = blood_data["real_name"]
- var/list/allowed_splats = list(SPLAT_KINDRED, /*PECIES_GAROU,*/ SPLAT_GHOUL/*, SPLAT_KUEI_JIN*/)
- if(!(blood_species in allowed_splats))
+ var/list/allowed_splats = list(SPLAT_KINDRED, SPLAT_GAROU, SPLAT_CORAX, SPLAT_GHOUL/*, SPLAT_KUEI_JIN*/)
+ if(!(blood_splat in allowed_splats))
return
- var/blood_identifier = "[blood_name]_[blood_species]"
+ var/blood_identifier = "[blood_name]_[blood_splat]"
// check if the bloods already been collected
if(LAZYFIND(collected_blood, blood_identifier))
@@ -73,30 +73,31 @@ SUBSYSTEM_DEF(occult_research)
LAZYADD(collected_blood, blood_identifier)
var/research_award = 0
- var/species_name = ""
+ var/splat_name = ""
var/research_message = ""
- switch(lowertext(blood_species))
+ if(blood_splat)
+ var/datum/splat/splat_type = GLOB.splat_list[blood_splat]
+ splat_name = splat_type::name
+
+ switch(blood_splat)
if(SPLAT_KINDRED)
var/generation = blood_data["generation"]
var/clan = blood_data["clan"]
research_award = (GHOUL_GENERATION - generation) * 5
- species_name = "Kindred"
- research_message = "You gain new insights into the [species_name] from clan [clan]! You gain [research_award] research points."
- /*
+ research_message = "You gain new insights into the [splat_name] from clan [clan]! You gain [research_award] research points."
if(SPLAT_GAROU)
research_award = 30
- species_name = "Garou"
research_message = "You gain [research_award] research points."
- */
+ if(SPLAT_CORAX)
+ research_award = 30
+ research_message = "You gain [research_award] research points."
if(SPLAT_GHOUL)
research_award = 5
- species_name = "Ghoul"
research_message = "You gain [research_award] research points."
/*
if(SPLAT_KUEI_JIN)
research_award = 15
- species_name = "Kuei-Jin"
research_message = "You gain [research_award] research points."
*/
diff --git a/modular_darkpack/modules/phones/code/_phone.dm b/modular_darkpack/modules/phones/code/_phone.dm
index 0236e401d8cc..0831f1aa4a94 100644
--- a/modular_darkpack/modules/phones/code/_phone.dm
+++ b/modular_darkpack/modules/phones/code/_phone.dm
@@ -348,7 +348,7 @@
to_chat(usr, span_danger("You must insert a SIM card to publish your number."))
return
name = trim(copytext_char(sanitize(name), 1, MAX_MESSAGE_LEN))
- for(var/contact as anything in SSphones.published_phone_numbers)
+ for(var/contact in SSphones.published_phone_numbers)
if(SSphones.published_phone_numbers[contact] == sim_card.phone_number)
to_chat(usr, span_danger("Error: This number is already published."))
return TRUE
@@ -360,7 +360,7 @@
return TRUE
if("unpublish_number")
- for(var/contact as anything in SSphones.published_phone_numbers)
+ for(var/contact in SSphones.published_phone_numbers)
if(SSphones.published_phone_numbers[contact] == sim_card.phone_number)
log_phone("[key_name(usr)] unpublished their number ([contact])/[sim_card.phone_number] from the phonebook.")
SSphones.published_phone_numbers.Remove(contact)
diff --git a/modular_darkpack/modules/phones/code/phone_book.dm b/modular_darkpack/modules/phones/code/phone_book.dm
index f5d6b2b7ec5e..27a4f269e7e7 100644
--- a/modular_darkpack/modules/phones/code/phone_book.dm
+++ b/modular_darkpack/modules/phones/code/phone_book.dm
@@ -8,5 +8,5 @@
/obj/item/phone_book/attack_self(mob/user)
. = ..()
- for(var/i as anything in SSphones.assigned_phone_numbers)
+ for(var/i in SSphones.assigned_phone_numbers)
to_chat(user, "[SSphones.assigned_phone_numbers[i]]")
diff --git a/modular_darkpack/modules/phones/code/phone_procs.dm b/modular_darkpack/modules/phones/code/phone_procs.dm
index 635c67e7df20..bec1500bbc71 100644
--- a/modular_darkpack/modules/phones/code/phone_procs.dm
+++ b/modular_darkpack/modules/phones/code/phone_procs.dm
@@ -29,7 +29,7 @@
output_user = contact.name
// If we dont have a contact name, refer to the published listings.
if(!output_user)
- for(var/contact as anything in SSphones.published_phone_numbers)
+ for(var/contact in SSphones.published_phone_numbers)
if(calling == SSphones.published_phone_numbers[contact])
output_user = contact
// Not in our contacts or published listings? Then resolve to showing the phone number.
diff --git a/modular_darkpack/modules/phones/code/sim_card.dm b/modular_darkpack/modules/phones/code/sim_card.dm
index 37475d1e2eb7..91fd25d60190 100644
--- a/modular_darkpack/modules/phones/code/sim_card.dm
+++ b/modular_darkpack/modules/phones/code/sim_card.dm
@@ -21,7 +21,7 @@
/obj/item/sim_card/Destroy(force)
. = ..()
SSphones.assigned_phone_numbers.Remove(src)
- for(var/contact as anything in SSphones.published_phone_numbers)
+ for(var/contact in SSphones.published_phone_numbers)
if(SSphones.published_phone_numbers[contact] == phone_number)
SSphones.published_phone_numbers.Remove(src)
phone_weakref = null
diff --git a/modular_darkpack/modules/powers/code/admin_discipline_editor.dm b/modular_darkpack/modules/powers/code/admin_discipline_editor.dm
index 3ff603e799c7..0e0d8c8c16cf 100644
--- a/modular_darkpack/modules/powers/code/admin_discipline_editor.dm
+++ b/modular_darkpack/modules/powers/code/admin_discipline_editor.dm
@@ -114,7 +114,7 @@
disc_data["name"] = discipline.name
disc_data["desc"] = discipline.desc
disc_data["max_level"] = discipline.max_selectable_level || length(discipline.all_powers)
- disc_data["rarity"] = (discipline_type in RARE_DISCIPLINE_TYPES) ? "rare" : "common"
+ disc_data["rarity"] = (discipline_type in GLOB.rare_discipline_types) ? "rare" : "common"
disc_data["icon"] = initial(discipline.icon)
disc_data["icon_state"] = discipline.icon_state
discipline_cache["[discipline_type]"] = disc_data
diff --git a/modular_darkpack/modules/powers/code/discipline/auspex/auspex.dm b/modular_darkpack/modules/powers/code/discipline/auspex/auspex.dm
index 61560dd4f30f..1d18ade5b26c 100644
--- a/modular_darkpack/modules/powers/code/discipline/auspex/auspex.dm
+++ b/modular_darkpack/modules/powers/code/discipline/auspex/auspex.dm
@@ -305,7 +305,7 @@
if(ROLL_SUCCESS)
disguised_voice = tgui_input_text(owner, "What will be the 'voice' of this implanted thought?", "Implanted Voice Selection")
if(ROLL_FAILURE, ROLL_BOTCH)
- to_chat(span_danger("You fail to disguise your voice - the subject hears your voice in their head!"))
+ to_chat(owner, span_danger("You fail to disguise your voice - the subject hears your voice in their head!"))
disguised_voice = owner.name
if("No")
disguised_voice = owner.name
diff --git a/modular_darkpack/modules/powers/code/discipline/auspex/avatar/avatar_verbs.dm b/modular_darkpack/modules/powers/code/discipline/auspex/avatar/avatar_verbs.dm
index 13938301f210..14e4b303c87e 100644
--- a/modular_darkpack/modules/powers/code/discipline/auspex/avatar/avatar_verbs.dm
+++ b/modular_darkpack/modules/powers/code/discipline/auspex/avatar/avatar_verbs.dm
@@ -25,16 +25,10 @@
return TRUE
/mob/living/basic/avatar/down()
- set name = "Move Down"
- set category = "IC"
-
if(zMove(DOWN, z_move_flags = ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move down."))
/mob/living/basic/avatar/up()
- set name = "Move Upwards"
- set category = "IC"
-
if(zMove(UP, z_move_flags = ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move upwards."))
diff --git a/modular_darkpack/modules/powers/code/discipline/dementation.dm b/modular_darkpack/modules/powers/code/discipline/dementation.dm
index 08b1d99d8b2a..d263dd6c50f7 100644
--- a/modular_darkpack/modules/powers/code/discipline/dementation.dm
+++ b/modular_darkpack/modules/powers/code/discipline/dementation.dm
@@ -17,7 +17,7 @@
activate_sound = 'modular_darkpack/modules/deprecated/sounds/insanity.ogg'
/datum/discipline_power/dementation/proc/remove_dementation_overlay(mob/living/carbon/human/target)
- target.remove_overlay(MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
/*
From V20:
@@ -71,11 +71,11 @@ Presence powers, etc
/datum/discipline_power/dementation/passion/activate(mob/living/carbon/human/target)
. = ..()
- target.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/dementation_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dementation.dmi', "dementation", -MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/dementation_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dementation.dmi', "dementation", -POWERS_LAYER)
dementation_overlay.pixel_z = 1
- target.overlays_standing[MUTATIONS_LAYER] = dementation_overlay
- target.apply_overlay(MUTATIONS_LAYER)
+ target.overlays_standing[POWERS_LAYER] = dementation_overlay
+ target.apply_overlay(POWERS_LAYER)
target.Stun(duration_length)
target.emote(pick("laugh","scream","cry")) // pick a random emotion for them to experience
var/attack_text = spooky_font_replace(dementation_phrase) // malk-ify what the attacker said
@@ -87,7 +87,7 @@ Presence powers, etc
/datum/discipline_power/dementation/passion/deactivate(mob/living/carbon/human/target)
. = ..()
- target.remove_overlay(MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
/*
@@ -154,11 +154,11 @@ pools for a turn or two after the manifestation.
/datum/discipline_power/dementation/the_haunting/activate(mob/living/carbon/human/target)
. = ..()
- target.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/dementation_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dementation.dmi', "dementation", -MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/dementation_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dementation.dmi', "dementation", -POWERS_LAYER)
dementation_overlay.pixel_z = 1
- target.overlays_standing[MUTATIONS_LAYER] = dementation_overlay
- target.apply_overlay(MUTATIONS_LAYER)
+ target.overlays_standing[POWERS_LAYER] = dementation_overlay
+ target.apply_overlay(POWERS_LAYER)
target.cause_hallucination( \
get_random_valid_hallucination_subtype(/datum/hallucination/delusion/preset), \
"the haunting", \
@@ -172,7 +172,7 @@ pools for a turn or two after the manifestation.
/datum/discipline_power/dementation/the_haunting/deactivate(mob/living/carbon/human/target)
. = ..()
- target.remove_overlay(MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
/*
From V20:
@@ -385,11 +385,11 @@ frenzy or Rötschreck response is automatic.
chosen.emote("scream")
GLOB.move_manager.move_away(moving = chosen, chasing = owner, max_dist = 10, timeout = (duration_length * 2), delay = chosen.cached_multiplicative_slowdown)
- chosen.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/dementation_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dementation.dmi', "dementation", -MUTATIONS_LAYER)
+ chosen.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/dementation_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dementation.dmi', "dementation", -POWERS_LAYER)
dementation_overlay.pixel_z = 1
- chosen.overlays_standing[MUTATIONS_LAYER] = dementation_overlay
- chosen.apply_overlay(MUTATIONS_LAYER)
+ chosen.overlays_standing[POWERS_LAYER] = dementation_overlay
+ chosen.apply_overlay(POWERS_LAYER)
addtimer(CALLBACK(src, PROC_REF(remove_dementation_overlay), chosen), duration_length)
/*
@@ -454,11 +454,11 @@ determines the duration.
/datum/discipline_power/dementation/total_insanity/activate(mob/living/carbon/human/target)
. = ..()
attack_target = target
- attack_target.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/dementation_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dementation.dmi', "dementation", -MUTATIONS_LAYER)
+ attack_target.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/dementation_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dementation.dmi', "dementation", -POWERS_LAYER)
dementation_overlay.pixel_z = 1
- attack_target.overlays_standing[MUTATIONS_LAYER] = dementation_overlay
- attack_target.apply_overlay(MUTATIONS_LAYER)
+ attack_target.overlays_standing[POWERS_LAYER] = dementation_overlay
+ attack_target.apply_overlay(POWERS_LAYER)
addtimer(CALLBACK(src, PROC_REF(self_attack), max(mypower)), 0) // attack_target will attack themselves n times equaling the caster's manipulation + intimidation subtracted by the attack_target's willpower
attack_target.cause_hallucination( \
diff --git a/modular_darkpack/modules/powers/code/discipline/dominate/dominate.dm b/modular_darkpack/modules/powers/code/discipline/dominate/dominate.dm
index ffa34066a7dc..ce9b53ba4194 100644
--- a/modular_darkpack/modules/powers/code/discipline/dominate/dominate.dm
+++ b/modular_darkpack/modules/powers/code/discipline/dominate/dominate.dm
@@ -57,11 +57,11 @@
. = ..()
var/mob/living/carbon/human/dominate_target = target
- dominate_target.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/dominate_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dominate.dmi', "dominate", -MUTATIONS_LAYER)
+ dominate_target.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/dominate_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/dominate.dmi', "dominate", -POWERS_LAYER)
dominate_overlay.pixel_z = 2
- dominate_target.overlays_standing[MUTATIONS_LAYER] = dominate_overlay
- dominate_target.apply_overlay(MUTATIONS_LAYER)
+ dominate_target.overlays_standing[POWERS_LAYER] = dominate_overlay
+ dominate_target.apply_overlay(POWERS_LAYER)
//dominate compels the target to have their gaze absolutely entrapped by the dominator
dominate_target.face_atom(owner)
@@ -177,7 +177,7 @@
REMOVE_TRAIT(target, TRAIT_IMMOBILIZED, TRAIT_GENERIC)
/mob/living/carbon/human/proc/post_dominate_checks(mob/living/carbon/human/dominate_target)
- dominate_target?.remove_overlay(MUTATIONS_LAYER)
+ dominate_target?.remove_overlay(POWERS_LAYER)
//COMMAND
/datum/discipline_power/dominate/command
diff --git a/modular_darkpack/modules/powers/code/discipline/melpominee.dm b/modular_darkpack/modules/powers/code/discipline/melpominee.dm
index 7fce7cbd8719..954d1b515628 100644
--- a/modular_darkpack/modules/powers/code/discipline/melpominee.dm
+++ b/modular_darkpack/modules/powers/code/discipline/melpominee.dm
@@ -329,7 +329,7 @@
effect(listener)
else
listener_list -= listener
- listener.remove_overlay(MUTATIONS_LAYER)
+ listener.remove_overlay(POWERS_LAYER)
cumulative_our_power[listener] = null
cumulative_list[listener] = null
@@ -345,10 +345,10 @@
/datum/discipline_power/melpominee/sirens_beckoning/proc/effect(mob/living/carbon/listener)
listener.Stun(1 TURNS)
- listener.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/song_overlay = mutable_appearance('modular_darkpack/modules/deprecated/icons/icons.dmi', "song", -MUTATIONS_LAYER)
- listener.overlays_standing[MUTATIONS_LAYER] = song_overlay
- listener.apply_overlay(MUTATIONS_LAYER)
+ listener.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/song_overlay = mutable_appearance('modular_darkpack/modules/deprecated/icons/icons.dmi', "song", -POWERS_LAYER)
+ listener.overlays_standing[POWERS_LAYER] = song_overlay
+ listener.apply_overlay(POWERS_LAYER)
if(cumulative_our_power[listener] >= 20)
listener.add_quirk(/datum/quirk/darkpack/derangement)
@@ -362,7 +362,7 @@
/datum/discipline_power/melpominee/sirens_beckoning/deactivate(mob/living/carbon/target)
. = ..()
for(var/mob/living/carbon/listener in listener_list)
- listener.remove_overlay(MUTATIONS_LAYER)
+ listener.remove_overlay(POWERS_LAYER)
owner.visible_message(span_purple("[owner]'s haunting melody ceases."), span_purple("You stop singing."))
channeling = FALSE
@@ -444,13 +444,13 @@
listener.apply_damage(15, AGGRAVATED, BODY_ZONE_HEAD)
- listener.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/song_overlay = mutable_appearance('modular_darkpack/modules/deprecated/icons/icons.dmi', "song", -MUTATIONS_LAYER)
- listener.overlays_standing[MUTATIONS_LAYER] = song_overlay
- listener.apply_overlay(MUTATIONS_LAYER)
+ listener.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/song_overlay = mutable_appearance('modular_darkpack/modules/deprecated/icons/icons.dmi', "song", -POWERS_LAYER)
+ listener.overlays_standing[POWERS_LAYER] = song_overlay
+ listener.apply_overlay(POWERS_LAYER)
addtimer(CALLBACK(src, PROC_REF(deactivate), listener), 1 TURNS)
/datum/discipline_power/melpominee/death_of_the_drum/deactivate(mob/living/carbon/human/target)
. = ..()
- target.remove_overlay(MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
diff --git a/modular_darkpack/modules/powers/code/discipline/presence/presence.dm b/modular_darkpack/modules/powers/code/discipline/presence/presence.dm
index f6792b09a0db..d76958da9431 100644
--- a/modular_darkpack/modules/powers/code/discipline/presence/presence.dm
+++ b/modular_darkpack/modules/powers/code/discipline/presence/presence.dm
@@ -42,11 +42,11 @@
return successes
/datum/discipline_power/presence/proc/apply_presence_overlay(mob/living/carbon/target)
- target.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/presence_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/presence.dmi', "presence", -MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/presence_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/presence.dmi', "presence", -POWERS_LAYER)
presence_overlay.pixel_z = 1
- target.overlays_standing[MUTATIONS_LAYER] = presence_overlay
- target.apply_overlay(MUTATIONS_LAYER)
+ target.overlays_standing[POWERS_LAYER] = presence_overlay
+ target.apply_overlay(POWERS_LAYER)
SEND_SOUND(target, sound('modular_darkpack/modules/powers/sounds/presence_activate.ogg'))
//used in awe - v20 book states that awe affects the targets of lowest willpower first if affecting multiple targets.
@@ -128,7 +128,7 @@
/datum/discipline_power/presence/awe/deactivate()
. = ..()
for(var/mob/living/carbon/target in affected_targets)
- target.remove_overlay(MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
affected_targets.Cut()
// DREAD GAZE
@@ -174,7 +174,7 @@
/datum/discipline_power/presence/dread_gaze/deactivate(mob/living/carbon/human/target)
. = ..()
- target.remove_overlay(MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
// ENTRANCEMENT
/datum/discipline_power/presence/entrancement
@@ -218,7 +218,7 @@
/datum/discipline_power/presence/entrancement/deactivate(mob/living/carbon/human/target)
. = ..()
- target.remove_overlay(MUTATIONS_LAYER)
+ target.remove_overlay(POWERS_LAYER)
// SUMMON
/datum/discipline_power/presence/summon
@@ -287,7 +287,7 @@
/datum/discipline_power/presence/summon/deactivate(mob/living/carbon/human/target)
. = ..()
- summon_target?.remove_overlay(MUTATIONS_LAYER)
+ summon_target?.remove_overlay(POWERS_LAYER)
// MAJESTY
/datum/discipline_power/presence/majesty
@@ -347,7 +347,7 @@
REMOVE_TRAIT(owner, TRAIT_PACIFISM, "Majesty")
for(var/mob/living/carbon/human/affected_target in affected_targets)
if(affected_target)
- affected_target.remove_overlay(MUTATIONS_LAYER)
+ affected_target.remove_overlay(POWERS_LAYER)
to_chat(affected_target, span_hypnophrase("The overwhelming presence of [owner] fades, and your will returns to normal."))
REMOVE_TRAIT(affected_target, TRAIT_PACIFISM, "Majesty")
affected_targets.Cut()
diff --git a/modular_darkpack/modules/powers/code/discipline/quietus/quietus.dm b/modular_darkpack/modules/powers/code/discipline/quietus/quietus.dm
index c2b2b00ba203..5edef289aecb 100644
--- a/modular_darkpack/modules/powers/code/discipline/quietus/quietus.dm
+++ b/modular_darkpack/modules/powers/code/discipline/quietus/quietus.dm
@@ -316,12 +316,12 @@
target.adjust_blood_points(-transfered)
if(ishuman(target))
var/mob/living/carbon/human/H = target
- H.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/quietus_overlay = mutable_appearance('modular_darkpack/modules/deprecated/icons/icons.dmi', "quietus", -MUTATIONS_LAYER)
- H.overlays_standing[MUTATIONS_LAYER] = quietus_overlay
- H.apply_overlay(MUTATIONS_LAYER)
+ H.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/quietus_overlay = mutable_appearance('modular_darkpack/modules/deprecated/icons/icons.dmi', "quietus", -POWERS_LAYER)
+ H.overlays_standing[POWERS_LAYER] = quietus_overlay
+ H.apply_overlay(POWERS_LAYER)
spawn(5*level_casting)
- H.remove_overlay(MUTATIONS_LAYER)
+ H.remove_overlay(POWERS_LAYER)
*/
/datum/discipline_power/quietus/taste_of_death
diff --git a/modular_darkpack/modules/powers/code/discipline/serpentis.dm b/modular_darkpack/modules/powers/code/discipline/serpentis.dm
index 937a2e6e2ecf..7f66d8fb4cf2 100644
--- a/modular_darkpack/modules/powers/code/discipline/serpentis.dm
+++ b/modular_darkpack/modules/powers/code/discipline/serpentis.dm
@@ -67,10 +67,10 @@
target.visible_message(span_hypnophrase("[owner] hypnotizes [target] with [owner.p_their()] eyes!"), span_warning("[owner] hypnotizes you! Their words seem to become more convincing and hypnotic..."))
if(ishuman(target))
var/mob/living/carbon/human/H = target
- H.remove_overlay(MUTATIONS_LAYER)
- var/mutable_appearance/serpentis_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/serpentis.dmi', "serpentis", -MUTATIONS_LAYER)
- H.overlays_standing[MUTATIONS_LAYER] = serpentis_overlay
- H.apply_overlay(MUTATIONS_LAYER)
+ H.remove_overlay(POWERS_LAYER)
+ var/mutable_appearance/serpentis_overlay = mutable_appearance('modular_darkpack/modules/powers/icons/serpentis.dmi', "serpentis", -POWERS_LAYER)
+ H.overlays_standing[POWERS_LAYER] = serpentis_overlay
+ H.apply_overlay(POWERS_LAYER)
immobilize_target(target)
/datum/discipline_power/serpentis/the_eyes_of_the_serpent/deactivate(mob/living/target)
@@ -78,7 +78,7 @@
release_target(target)
if (ishuman(target))
var/mob/living/carbon/human/human_target = target
- human_target.remove_overlay(MUTATIONS_LAYER)
+ human_target.remove_overlay(POWERS_LAYER)
//THE TONGUE OF THE ASP
/datum/discipline_power/serpentis/the_tongue_of_the_asp
diff --git a/modular_darkpack/modules/powers/code/discipline/thaumaturgy/paths/lure_of_flames.dm b/modular_darkpack/modules/powers/code/discipline/thaumaturgy/paths/lure_of_flames.dm
index f9a544d98c60..2c9656243665 100644
--- a/modular_darkpack/modules/powers/code/discipline/thaumaturgy/paths/lure_of_flames.dm
+++ b/modular_darkpack/modules/powers/code/discipline/thaumaturgy/paths/lure_of_flames.dm
@@ -279,7 +279,7 @@
alpha = 150
duration = 2 SECONDS // Matches the channel time
-/obj/effect/temp_visual/inferno_warning/Initialize()
+/obj/effect/temp_visual/inferno_warning/Initialize(mapload)
. = ..()
// pulsing animation
animate(src, alpha = 50, time = 10, loop = -1)
diff --git a/modular_darkpack/modules/powers/code/discipline/vicissitude/shapeshifting.dm b/modular_darkpack/modules/powers/code/discipline/vicissitude/shapeshifting.dm
index 83fbc3c2dce4..aa102bfa3861 100644
--- a/modular_darkpack/modules/powers/code/discipline/vicissitude/shapeshifting.dm
+++ b/modular_darkpack/modules/powers/code/discipline/vicissitude/shapeshifting.dm
@@ -184,7 +184,7 @@
/datum/action/cooldown/mob_cooldown/shapeshift/proc/change_race(mob/living/carbon/human/target)
var/list/skin_tones = list()
- for(var/skin_tone as anything in GLOB.skin_tone_names)
+ for(var/skin_tone in GLOB.skin_tone_names)
var/skin_tone_name = GLOB.skin_tone_names[skin_tone]
skin_tones[skin_tone_name] = skin_tone
diff --git a/modular_darkpack/modules/powers/code/discipline/vicissitude/surgeries/skin_colour_change.dm b/modular_darkpack/modules/powers/code/discipline/vicissitude/surgeries/skin_colour_change.dm
index ad065ef70350..140814dac4de 100644
--- a/modular_darkpack/modules/powers/code/discipline/vicissitude/surgeries/skin_colour_change.dm
+++ b/modular_darkpack/modules/powers/code/discipline/vicissitude/surgeries/skin_colour_change.dm
@@ -33,7 +33,7 @@
var/mob/living/carbon/human/patient = get_patient(operating_on)
var/list/skin_tones = list()
- for(var/skin_tone as anything in GLOB.skin_tone_names)
+ for(var/skin_tone in GLOB.skin_tone_names)
var/skin_tone_name = GLOB.skin_tone_names[skin_tone]
skin_tones[skin_tone_name] = skin_tone
diff --git a/modular_darkpack/modules/powers/code/discipline/vicissitude/zulo.dm b/modular_darkpack/modules/powers/code/discipline/vicissitude/zulo.dm
index 387672be0936..27f293fde116 100644
--- a/modular_darkpack/modules/powers/code/discipline/vicissitude/zulo.dm
+++ b/modular_darkpack/modules/powers/code/discipline/vicissitude/zulo.dm
@@ -48,10 +48,10 @@ GLOBAL_LIST_INIT(zulo_forms, list(
/mob/living/basic/zulo/Initialize(mapload)
. = ..()
- ADD_TRAIT(src, TRAIT_UNMASQUERADE, type)
+ ADD_TRAIT(src, TRAIT_UNMASQUERADE, INNATE_TRAIT)
/mob/living/basic/zulo/mind_initialize()
. = ..()
- var/preffered_form = client?.prefs.read_preference(/datum/preference/choiced/subsplat/zulo_form)
+ var/preffered_form = client?.prefs.read_preference(/datum/preference/choiced/zulo_form)
var/new_icon_state = GLOB.zulo_forms[preffered_form]
icon_state = new_icon_state ? new_icon_state : "fiend"
diff --git a/modular_darkpack/modules/powers/code/discipline/vicissitude/zulo_preferences.dm b/modular_darkpack/modules/powers/code/discipline/vicissitude/zulo_preferences.dm
index d78f3e2e200e..062f1e6d3e1f 100644
--- a/modular_darkpack/modules/powers/code/discipline/vicissitude/zulo_preferences.dm
+++ b/modular_darkpack/modules/powers/code/discipline/vicissitude/zulo_preferences.dm
@@ -1,12 +1,14 @@
-/datum/preference/choiced/subsplat/zulo_form
+/datum/preference/choiced/zulo_form
savefile_key = "zulo_form"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_FEATURES
priority = PREFERENCE_PRIORITY_REQUIRES_SUBSPLAT
main_feature_name = "Zulo Form"
+ relevant_inherent_trait = TRAIT_VTM_CLANS
+ must_have_relevant_trait = TRUE
should_generate_icons = TRUE
-/datum/preference/choiced/subsplat/zulo_form/has_relevant_feature(datum/preferences/preferences)
+/datum/preference/choiced/zulo_form/has_relevant_feature(datum/preferences/preferences)
. = ..()
if(!.) // Make sure we acctually can select clan in the first place
return FALSE
@@ -19,17 +21,17 @@
return TRUE
return FALSE
-/datum/preference/choiced/subsplat/zulo_form/init_possible_values()
+/datum/preference/choiced/zulo_form/init_possible_values()
var/list/values = list()
for(var/name in GLOB.zulo_forms)
values[name] = GLOB.zulo_forms[name]
return values
-/datum/preference/choiced/subsplat/zulo_form/icon_for(value)
+/datum/preference/choiced/zulo_form/icon_for(value)
var/icon_state = GLOB.zulo_forms[value]
var/datum/universal_icon/zulo_icon = uni_icon('modular_darkpack/modules/powers/icons/zulo_forms.dmi', icon_state)
zulo_icon.scale(32, 32)
return zulo_icon
-/datum/preference/choiced/subsplat/zulo_form/apply_to_human(mob/living/carbon/human/target, value)
+/datum/preference/choiced/zulo_form/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/modular_darkpack/modules/powers/code/discipline_pref_middleware.dm b/modular_darkpack/modules/powers/code/discipline_pref_middleware.dm
index c6359f727e9e..4017f4560f67 100644
--- a/modular_darkpack/modules/powers/code/discipline_pref_middleware.dm
+++ b/modular_darkpack/modules/powers/code/discipline_pref_middleware.dm
@@ -1,14 +1,16 @@
//discipline stuff
-var/global/list/RARE_DISCIPLINE_TYPES = list(
+GLOBAL_LIST_INIT(rare_discipline_types, list(
/datum/discipline/quietus,
/datum/discipline/temporis,
/datum/discipline/serpentis,
/datum/discipline/dementation,
/datum/discipline/obtenebration,
/datum/discipline/thaumaturgy,
- /datum/discipline/necromancy
- // daimonion, valeren, melpominee not yet implemented but will go here
-)
+ /datum/discipline/necromancy,
+ /datum/discipline/daimoinon,
+ /datum/discipline/valeren,
+ // melpominee not yet implemented but will go here
+))
// warns a player if they have no discipline dots assigned before joining
// returns TRUE if they want to proceed, FALSE if they want to go back and fix their disciplines
@@ -42,7 +44,7 @@ var/global/list/RARE_DISCIPLINE_TYPES = list(
total++
if(!(disc_path in clan_disciplines))
additional++
- if(text2path(disc_path) in RARE_DISCIPLINE_TYPES)
+ if(text2path(disc_path) in GLOB.rare_discipline_types)
additional_rare++
if(!is_trusted)
@@ -197,7 +199,7 @@ var/global/list/RARE_DISCIPLINE_TYPES = list(
disc_data["max_level"] = discipline.max_selectable_level || length(discipline.all_powers)
disc_data["icon"] = initial(discipline.icon)
disc_data["icon_state"] = discipline.icon_state
- disc_data["rarity"] = (discipline_type in RARE_DISCIPLINE_TYPES) ? "rare" : "common"
+ disc_data["rarity"] = (discipline_type in GLOB.rare_discipline_types) ? "rare" : "common"
data["[discipline_type]"] = disc_data
qdel(discipline)
diff --git a/modular_darkpack/modules/html/dementation/css/chaos.css b/modular_darkpack/modules/powers/html/dementation/css/chaos.css
similarity index 100%
rename from modular_darkpack/modules/html/dementation/css/chaos.css
rename to modular_darkpack/modules/powers/html/dementation/css/chaos.css
diff --git a/modular_darkpack/modules/radios/code/radio.dm b/modular_darkpack/modules/radios/code/radio.dm
index d9f99acfdb46..04cb6e427a0e 100644
--- a/modular_darkpack/modules/radios/code/radio.dm
+++ b/modular_darkpack/modules/radios/code/radio.dm
@@ -12,7 +12,7 @@
name = "military radio"
radio_network = NETWORK_MILITARY
-/obj/item/radio/headset/darkpack/military/Initialize()
+/obj/item/radio/headset/darkpack/military/Initialize(mapload)
. = ..()
set_frequency(FREQ_MILITARY)
radio_id = rand(1, 999) // Since we wont have a tranceiver for these, we're just auto-assigning a random ID. This isn't foolproof.
@@ -21,7 +21,7 @@
name = "military radio"
radio_network = NETWORK_ENDRON
-/obj/item/radio/headset/darkpack/pmc/Initialize()
+/obj/item/radio/headset/darkpack/pmc/Initialize(mapload)
. = ..()
set_frequency(FREQ_ENDRON)
AddElement(/datum/element/earhealing)
diff --git a/modular_darkpack/modules/radios/code/transceiver.dm b/modular_darkpack/modules/radios/code/transceiver.dm
index f07221c7bd74..9123e9a33860 100644
--- a/modular_darkpack/modules/radios/code/transceiver.dm
+++ b/modular_darkpack/modules/radios/code/transceiver.dm
@@ -142,7 +142,7 @@
if(!subspace_radio.can_receive(frequency, RADIO_NO_Z_LEVEL_RESTRICTION))
radios -= subspace_radio
- for(var/called_radio as anything in radios)
+ for(var/called_radio in radios)
playsound(get_turf(called_radio), 'modular_darkpack/modules/radios/sounds/panic.ogg', 50, TRUE)
/obj/machinery/radio_tranceiver/clinic
diff --git a/modular_darkpack/modules/retail/code/_retail.dm b/modular_darkpack/modules/retail/code/_retail.dm
index d5fc1c016cba..24010e5d9b7a 100644
--- a/modular_darkpack/modules/retail/code/_retail.dm
+++ b/modular_darkpack/modules/retail/code/_retail.dm
@@ -18,7 +18,7 @@
// Equivlenet to products list if you dont need to pass args. Will likely phase out the evil news in our type path definitions
var/list/product_types = list()
-/obj/structure/retail/Initialize()
+/obj/structure/retail/Initialize(mapload)
. = ..()
if(owner_needed == TRUE)
my_owner = locate(/mob/living/carbon/human/npc) in range(2, src)
diff --git a/modular_darkpack/modules/ritual_thaumaturgy/rituals/bloodwalk.dm b/modular_darkpack/modules/ritual_thaumaturgy/rituals/bloodwalk.dm
index efdb33afc94c..5b1391aa7bd7 100644
--- a/modular_darkpack/modules/ritual_thaumaturgy/rituals/bloodwalk.dm
+++ b/modular_darkpack/modules/ritual_thaumaturgy/rituals/bloodwalk.dm
@@ -44,7 +44,6 @@
else
if(generation >= 14)
message += "This is the vitae of a thinblood!\n"
- clan = lowertext(clan)
switch(clan)
if(VAMPIRE_CLAN_TOREADOR, VAMPIRE_CLAN_DAUGHTERS_OF_CACOPHONY)
message += "The blood is sweet and rich. The owner must, too, be beautiful.\n"
diff --git a/modular_darkpack/modules/ritual_thaumaturgy/rituals/chime_of_the_unseen_spirits.dm b/modular_darkpack/modules/ritual_thaumaturgy/rituals/chime_of_the_unseen_spirits.dm
index 121128d171d5..3d88cf29c4c3 100644
--- a/modular_darkpack/modules/ritual_thaumaturgy/rituals/chime_of_the_unseen_spirits.dm
+++ b/modular_darkpack/modules/ritual_thaumaturgy/rituals/chime_of_the_unseen_spirits.dm
@@ -76,7 +76,7 @@
return ITEM_INTERACT_SUCCESS
-/obj/item/spirit_chime/Initialize()
+/obj/item/spirit_chime/Initialize(mapload)
. = ..()
// Sets up a field with a range of 10
chime_field = new /datum/proximity_monitor/advanced/spirit_chime(src, detection_range)
diff --git a/modular_darkpack/modules/ritual_thaumaturgy/rituals/gargoyle_transformation.dm b/modular_darkpack/modules/ritual_thaumaturgy/rituals/gargoyle_transformation.dm
index 74033389dbd2..e83a0f694bcf 100644
--- a/modular_darkpack/modules/ritual_thaumaturgy/rituals/gargoyle_transformation.dm
+++ b/modular_darkpack/modules/ritual_thaumaturgy/rituals/gargoyle_transformation.dm
@@ -199,7 +199,7 @@
maxbloodpool = 15
ai_controller = null // Start with no AI, will be assigned if no player takes it
-/mob/living/basic/gargoyle/perfect/Initialize()
+/mob/living/basic/gargoyle/perfect/Initialize(mapload)
. = ..()
// Make the perfect gargoyle slightly larger
transform = transform.Scale(1.10, 1.10)
diff --git a/modular_darkpack/modules/sabbat/code/sabbat_blood_bath.dm b/modular_darkpack/modules/sabbat/code/sabbat_blood_bath.dm
index 81c0189f293e..43bc9951d04d 100644
--- a/modular_darkpack/modules/sabbat/code/sabbat_blood_bath.dm
+++ b/modular_darkpack/modules/sabbat/code/sabbat_blood_bath.dm
@@ -9,7 +9,7 @@
var/max_blood = 500
var/list/blood_donors = list()
-/obj/structure/bath/sabbatbath/Initialize()
+/obj/structure/bath/sabbatbath/Initialize(mapload)
. = ..()
create_reagents(max_blood, INJECTABLE)
update_icon()
diff --git a/modular_darkpack/modules/splats/code/subsplat/_subsplat.dm b/modular_darkpack/modules/splats/code/subsplat/_subsplat.dm
index dd68b8e11346..09c42a3cd9c1 100644
--- a/modular_darkpack/modules/splats/code/subsplat/_subsplat.dm
+++ b/modular_darkpack/modules/splats/code/subsplat/_subsplat.dm
@@ -16,6 +16,8 @@
var/name
/// Description of what the splat is and what it does
var/desc
+ /// If set, the roleplay level that is displayed in prefrences as a guide to players.
+ var/roleplay_level
/// ID for trait sources and whatnot
var/id
@@ -64,3 +66,10 @@
joining.put_in_r_hand(new subsplat_keys(joining))
UnregisterSignal(joining, COMSIG_MOB_LOGIN)
+
+/// Displays description and roleplay level of the subsplat.
+/datum/subsplat/proc/show_lore(mob/user)
+ if(desc)
+ to_chat(user, span_notice("[uppertext(name)] [desc]"))
+ if(roleplay_level)
+ to_chat(user, span_notice(" ROLEPLAY LEVEL: [roleplay_level] Roleplay levels, or, the difficulty to play and portray a character from that auspice, are as follows: Beginner Friendly, Intermediate, Advanced."))
diff --git a/modular_darkpack/modules/storyteller_dice/code/verbs.dm b/modular_darkpack/modules/storyteller_dice/code/verbs.dm
index bd0f3c9effba..47c134b01b3f 100644
--- a/modular_darkpack/modules/storyteller_dice/code/verbs.dm
+++ b/modular_darkpack/modules/storyteller_dice/code/verbs.dm
@@ -2,11 +2,11 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(roll_storyteller_dice, R_FUN, "Roll storyteller dic
M.roll_dice_custom()
BLACKBOX_LOG_ADMIN_VERB("Storyteller dice")
-/mob/living/verb/roll_dice_custom()
+/mob/living/verb/do_roll_dice_custom()
set name = "Roll custom dice"
- set category = "IC"
- set desc = "Roll dice!"
+ set hidden = TRUE
+/mob/living/proc/roll_dice_custom(atom/movable/roll_target)
var/list/allowed_stats = list()
for(var/stat_path, dots_in in storyteller_stats)
var/datum/st_stat/stat = stat_path
@@ -43,7 +43,25 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(roll_storyteller_dice, R_FUN, "Roll storyteller dic
custom_roll.difficulty = difficulty
custom_roll.successes_needed = successes_needed
custom_roll.roll_output_type = roll_type
- return custom_roll.st_roll(src, src, bonus_dice)
+ return custom_roll.st_roll(src, roll_target, bonus_dice)
+
/datum/storyteller_roll/custom_roll
bumper_text = "custom roll"
+
+#define UI_MOB_DICE_ROLL "EAST-4:22,SOUTH+1:24"
+
+/atom/movable/screen/dice_roll
+ name = "roll custom dice"
+ icon = 'icons/hud/screen_midnight.dmi'
+ icon_state = "dice"
+ screen_loc = UI_MOB_DICE_ROLL
+ mouse_over_pointer = MOUSE_HAND_POINTER
+
+/atom/movable/screen/dice_roll/Click()
+ . = ..()
+
+ var/mob/living/roller = astype(usr)
+ roller?.roll_dice_custom()
+
+#undef UI_MOB_DICE_ROLL
diff --git a/modular_darkpack/modules/storyteller_stats/code/global_procs.dm b/modular_darkpack/modules/storyteller_stats/code/global_procs.dm
index d86756153527..70e8baa900df 100644
--- a/modular_darkpack/modules/storyteller_stats/code/global_procs.dm
+++ b/modular_darkpack/modules/storyteller_stats/code/global_procs.dm
@@ -1,6 +1,6 @@
/proc/create_new_stat_prefs(list/preference_storyteller_stats)
var/list/stats_list = list()
- for(var/stat_path as anything in subtypesof(/datum/st_stat))
+ for(var/stat_path in subtypesof(/datum/st_stat))
var/datum/st_stat/stat = new stat_path()
stat.set_score(stat.starting_score)
stats_list[stat_path] = stat
diff --git a/modular_darkpack/modules/strings/global_strings.dm b/modular_darkpack/modules/strings/global_strings.dm
deleted file mode 100644
index 49daf9ac494a..000000000000
--- a/modular_darkpack/modules/strings/global_strings.dm
+++ /dev/null
@@ -1,43 +0,0 @@
-
-// associative list used by dementation and the derangement quirk
-GLOBAL_LIST_INIT(derangement_phrases,list(
- "Evil crouches" = 'modular_darkpack/modules/powers/sounds/dementation/speech/crouch.ogg',
- "Death" = 'modular_darkpack/modules/powers/sounds/dementation/speech/death.ogg',
- "DIE!" = 'modular_darkpack/modules/powers/sounds/dementation/speech/die.ogg',
- "I smell a rancid grave" = 'modular_darkpack/modules/powers/sounds/dementation/speech/grave.ogg',
- "Rustling robes of the Reaper" = 'modular_darkpack/modules/powers/sounds/dementation/speech/reaper.ogg',
- "All are blind whose eyes are closed" = 'modular_darkpack/modules/powers/sounds/dementation/speech/blind.ogg',
- "The drove is a terrible mistress" = 'modular_darkpack/modules/powers/sounds/dementation/speech/mistress.ogg',
- "Wishes and words sprout from the same seed" = 'modular_darkpack/modules/powers/sounds/dementation/speech/wishes_words.ogg',
- "A dark light from your death" = 'modular_darkpack/modules/powers/sounds/dementation/speech/dark_light.ogg',
- "Hemlock for the deceivers" = 'modular_darkpack/modules/powers/sounds/dementation/speech/hemlock.ogg',
- "It has two mouths to lick from" = 'modular_darkpack/modules/powers/sounds/dementation/speech/two_mouths.ogg',
- "Deep of the Atlantic, dark, dreaming, sleeping" = 'modular_darkpack/modules/powers/sounds/dementation/speech/atlantic.ogg',
- "Can't see, can't see! Where have my eyes gone to?" = 'modular_darkpack/modules/powers/sounds/dementation/speech/eyes.ogg',
- "Heloise said you. Cranberry sauce. Hotel foxtrot" = 'modular_darkpack/modules/powers/sounds/dementation/speech/heloise.ogg',
- "Stop doing that. Mother shan't be too pleased. None too pleased" = 'modular_darkpack/modules/powers/sounds/dementation/speech/mother.ogg',
- "Those lips bleed a putrid poison" = 'modular_darkpack/modules/powers/sounds/dementation/speech/putrid.ogg',
- "Rat tails, cat tails, coat tails, all tales" = 'modular_darkpack/modules/powers/sounds/dementation/speech/tails.ogg',
- "It's not fair! I wanted to" = 'modular_darkpack/modules/powers/sounds/dementation/speech/not_fair.ogg',
- "Pennies for your eyes in its pockets" = 'modular_darkpack/modules/powers/sounds/dementation/speech/pennies.ogg',
- "Why is it troubled?" = 'modular_darkpack/modules/powers/sounds/dementation/speech/troubled.ogg',
- "Ask about the free arsenic" = 'modular_darkpack/modules/powers/sounds/dementation/speech/arsenic.ogg',
- "Blood brings the vicious beast" = 'modular_darkpack/modules/powers/sounds/dementation/speech/beast.ogg',
- "I see daggers hang on his breath" = 'modular_darkpack/modules/powers/sounds/dementation/speech/daggers.ogg',
- "Bone round in melody and word layed in rain" = 'modular_darkpack/modules/powers/sounds/dementation/speech/bone.ogg',
- "Cemetery runoff congealing at the door" = 'modular_darkpack/modules/powers/sounds/dementation/speech/cemetery.ogg',
- "Maggots love you. Trust me" = 'modular_darkpack/modules/powers/sounds/dementation/speech/maggots.ogg',
- "Mast lay shrouded and the moon is melting" = 'modular_darkpack/modules/powers/sounds/dementation/speech/moon.ogg',
- "Try the corpse in the oven with peppers and fur" = 'modular_darkpack/modules/powers/sounds/dementation/speech/peppers.ogg',
- "Souls draped in rotten tatters and Father dances in the dark" = 'modular_darkpack/modules/powers/sounds/dementation/speech/souls.ogg',
- "Make the tallow from the fat of a hangman" = 'modular_darkpack/modules/powers/sounds/dementation/speech/tallow.ogg',
- "Bent like a calf for the butcher" = 'modular_darkpack/modules/powers/sounds/dementation/speech/calf.ogg',
- "You're in for it now" = 'modular_darkpack/modules/powers/sounds/dementation/speech/in_for_it.ogg',
- "They're coming" = 'modular_darkpack/modules/powers/sounds/dementation/speech/theyre_coming.ogg',
- "It casts a crooked shadow" = 'modular_darkpack/modules/powers/sounds/dementation/speech/shadow.ogg',
- "Elkabo, elkabo, pixy queen where all is green" = 'modular_darkpack/modules/powers/sounds/dementation/speech/elkabo.ogg',
- "It's a tangle of asps" = 'modular_darkpack/modules/powers/sounds/dementation/speech/asps.ogg',
- "Sealed with the kiss of swine" = 'modular_darkpack/modules/powers/sounds/dementation/speech/swine.ogg',
- "A trick with two tongues" = 'modular_darkpack/modules/powers/sounds/dementation/speech/tongues.ogg',
- "The very thought falls to the flame" = 'modular_darkpack/modules/powers/sounds/dementation/speech/flame.ogg'
-))
diff --git a/modular_darkpack/modules/vampire_the_masquerade/code/vampire_clan/clan_pref.dm b/modular_darkpack/modules/vampire_the_masquerade/code/vampire_clan/clan_pref.dm
index c2545c4ff6c3..cfb3c6c1821f 100644
--- a/modular_darkpack/modules/vampire_the_masquerade/code/vampire_clan/clan_pref.dm
+++ b/modular_darkpack/modules/vampire_the_masquerade/code/vampire_clan/clan_pref.dm
@@ -13,3 +13,7 @@
/datum/preference/choiced/subsplat/vampire_clan/apply_to_human(mob/living/carbon/human/target, value)
var/joining_round = !isdummy(target)
target.set_clan(value, joining_round)
+
+/datum/preference/choiced/subsplat/vampire_clan/post_set_preference(mob/user, value)
+ var/datum/subsplat/vampire_clan/clan = get_vampire_clan(value)
+ clan?.show_lore(user)
diff --git a/modular_darkpack/modules/vampire_the_masquerade/code/vampire_clan/clans/malkavian/malkavian.dm b/modular_darkpack/modules/vampire_the_masquerade/code/vampire_clan/clans/malkavian/malkavian.dm
index feeb19d325c7..7e45f100dad9 100644
--- a/modular_darkpack/modules/vampire_the_masquerade/code/vampire_clan/clans/malkavian/malkavian.dm
+++ b/modular_darkpack/modules/vampire_the_masquerade/code/vampire_clan/clans/malkavian/malkavian.dm
@@ -112,7 +112,7 @@
//before we inadvertently obfuscate the message to pass filters, filter it first.
//as funny as malkavians saying "amogus" would be, the filter also includes slurs... how unfortunate.
to_chat(clicker, span_warning("That message contained a word prohibited in IC chat! Consider reviewing the server rules.\n\"[mad_speak]\""))
- SSblackbox.record_feedback("tally", "ic_blocked_words", 1, lowertext(config.ic_filter_regex.match))
+ SSblackbox.record_feedback("tally", "ic_blocked_words", 1, LOWER_TEXT(config.ic_filter_regex.match))
return
if(!mad_speak)
return
diff --git a/modular_darkpack/modules/vitae/code/embracing.dm b/modular_darkpack/modules/vitae/code/embracing.dm
index 2eb95a1d87ab..951c93acf19a 100644
--- a/modular_darkpack/modules/vitae/code/embracing.dm
+++ b/modular_darkpack/modules/vitae/code/embracing.dm
@@ -1,13 +1,13 @@
/mob/living/carbon/human/proc/attempt_embrace_target(mob/living/carbon/human/childe, second_party_embrace)
- var/chat_message_reciever = src
+ var/chat_message_receiver = src
if(second_party_embrace)
- chat_message_reciever = second_party_embrace
+ chat_message_receiver = second_party_embrace
if(!childe.can_be_embraced || !childe.mind)
- to_chat(chat_message_reciever, span_notice("[childe.name] doesn't respond to the Vitae."))
+ to_chat(chat_message_receiver, span_notice("[childe.name] doesn't respond to the Vitae."))
return
// If they've been dead for more than 5 minutes, then nothing happens.
if(!((childe.timeofdeath + 5 MINUTES) > world.time))
- to_chat(chat_message_reciever, span_notice("[childe] is totally DEAD!"))
+ to_chat(chat_message_receiver, span_notice("[childe] is totally DEAD!"))
return FALSE
embrace_target(childe, second_party_embrace)
diff --git a/modular_darkpack/modules/walls/code/floors.dm b/modular_darkpack/modules/walls/code/floors.dm
index f6bf7d6f9ec8..e948f565ddd5 100644
--- a/modular_darkpack/modules/walls/code/floors.dm
+++ b/modular_darkpack/modules/walls/code/floors.dm
@@ -147,7 +147,7 @@
/turf/open/floor/plating/stone/Initialize(mapload)
. = ..()
- icon_state = "cave[rand(1, 7)]"
+ icon_state = "stone[rand(1, 7)]"
/turf/open/floor/plating/grate
name = "grate"
diff --git a/modular_darkpack/modules/weapons/code/ammo_boxes.dm b/modular_darkpack/modules/weapons/code/ammo_boxes.dm
index 33657551a5e0..0c62e532c17a 100644
--- a/modular_darkpack/modules/weapons/code/ammo_boxes.dm
+++ b/modular_darkpack/modules/weapons/code/ammo_boxes.dm
@@ -140,6 +140,11 @@
// icon_state = "s12box_buck"
// ammo_type = /obj/item/ammo_casing/vampire/c12g/buck/silver
+/obj/item/ammo_box/darkpack/c12g/buck/incendiary
+ name = "ammo box (12g, Dragon's Breath)"
+ icon_state = "12box_dragon"
+ ammo_type = /obj/item/ammo_casing/vampire/c12g/buck/incendiary
+
// Crossbow Bolt
/obj/item/ammo_box/darkpack/arrows
name = "ammo box (arrows)"
diff --git a/modular_darkpack/modules/weapons/code/ammo_casings.dm b/modular_darkpack/modules/weapons/code/ammo_casings.dm
index c587d7d06bd2..c9bb7597a31e 100644
--- a/modular_darkpack/modules/weapons/code/ammo_casings.dm
+++ b/modular_darkpack/modules/weapons/code/ammo_casings.dm
@@ -145,6 +145,15 @@
icon_state = "12i"
base_icon_state = "12i"
+/obj/item/ammo_casing/vampire/c12g/buck/incendiary
+ name = "12g dragon's breath shell casing"
+ desc = "An incendiary 12g shell casing."
+ projectile_type = /obj/projectile/bullet/darkpack/dragonsbreath
+ pellets = 8
+ variance = 25
+ icon_state = "12d"
+ base_icon_state = "12d"
+
// Crossbow Bolt
/obj/item/ammo_casing/caseless/bolt
name = "bolt"
diff --git a/modular_darkpack/modules/weapons/code/melee.dm b/modular_darkpack/modules/weapons/code/melee.dm
index 92eddd44ebd4..ddee072479df 100644
--- a/modular_darkpack/modules/weapons/code/melee.dm
+++ b/modular_darkpack/modules/weapons/code/melee.dm
@@ -427,6 +427,6 @@
masquerade_violating = FALSE
custom_price = 1200
-/obj/item/darkpack/spear/Initialize()
+/obj/item/darkpack/spear/Initialize(mapload)
. = ..()
AddComponent(/datum/component/selling, 400, "spear", FALSE)
diff --git a/modular_darkpack/modules/weapons/code/projectiles.dm b/modular_darkpack/modules/weapons/code/projectiles.dm
index 08fd44d46643..37c523064266 100644
--- a/modular_darkpack/modules/weapons/code/projectiles.dm
+++ b/modular_darkpack/modules/weapons/code/projectiles.dm
@@ -167,6 +167,24 @@
var/mob/living/carbon/M = target
M.Stun(4)
+/obj/projectile/bullet/darkpack/dragonsbreath
+ name = "12g shotgun incendiary pellet"
+ damage = 6
+ damage_type = BURN
+ range = 22 //range of where you can see + one screen after
+ armour_penetration = 0
+ exposed_wound_bonus = 0
+ wound_bonus = 0
+ var/fire_stacks = 1 // 1 stack per pellet but we have 9 pellets so it adds up
+
+/obj/projectile/bullet/darkpack/dragonsbreath/on_hit(atom/target, blocked = 0, pierce_hit)
+ . = ..()
+ do_sparks(2, TRUE, src)
+ if(iscarbon(target))
+ var/mob/living/carbon/M = target
+ M.adjust_fire_stacks(fire_stacks)
+ M.ignite_mob()
+
// Crossbow Bolt
/obj/projectile/bullet/crossbow_bolt
name = "bolt"
diff --git a/modular_darkpack/modules/weapons/icons/ammo.dmi b/modular_darkpack/modules/weapons/icons/ammo.dmi
index 0827ad63c68b..28132b5fe282 100644
Binary files a/modular_darkpack/modules/weapons/icons/ammo.dmi and b/modular_darkpack/modules/weapons/icons/ammo.dmi differ
diff --git a/modular_darkpack/modules/weapons/icons/ammo_onfloor.dmi b/modular_darkpack/modules/weapons/icons/ammo_onfloor.dmi
index a7c023e1559b..98f6856146e6 100644
Binary files a/modular_darkpack/modules/weapons/icons/ammo_onfloor.dmi and b/modular_darkpack/modules/weapons/icons/ammo_onfloor.dmi differ
diff --git a/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/auspice.dm b/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/auspice.dm
index 9afffe9d5f73..44fd86800485 100644
--- a/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/auspice.dm
+++ b/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/auspice.dm
@@ -22,6 +22,10 @@
var/joining_round = !isdummy(target)
target.set_auspice(value, joining_round)
+/datum/preference/choiced/subsplat/fera_auspice/post_set_preference(mob/user, value)
+ var/datum/subsplat/werewolf/auspice/auspice = get_fera_auspice(value)
+ auspice?.show_lore(user)
+
/datum/preference/choiced/subsplat/fera_auspice/is_accessible(datum/preferences/preferences)
. = ..()
var/datum/splat/splat_path = preferences.read_preference(/datum/preference/choiced/splats)
diff --git a/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/breed.dm b/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/breed.dm
index c325a73d7c36..91721e020a9d 100644
--- a/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/breed.dm
+++ b/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/breed.dm
@@ -25,6 +25,10 @@
var/joining_round = !isdummy(target)
target.set_breed_form(value, joining_round)
+/datum/preference/choiced/subsplat/fera_breed/post_set_preference(mob/user, value)
+ var/datum/subsplat/werewolf/breed_form/breed = get_fera_breed_form(value)
+ breed?.show_lore(user)
+
/datum/preference/choiced/subsplat/fera_breed/is_accessible(datum/preferences/preferences)
. = ..()
var/datum/splat/splat_path = preferences.read_preference(/datum/preference/choiced/splats)
diff --git a/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/fur.dm b/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/fur.dm
index 8b2793630fb6..7c735ce164ba 100644
--- a/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/fur.dm
+++ b/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/fur.dm
@@ -7,6 +7,7 @@
main_feature_name = "Fera Fur Color"
relevant_inherent_trait = TRAIT_FERA_FUR
must_have_relevant_trait = TRUE
+ must_be_accessible = TRUE
var/splat_id
/datum/preference/choiced/fera_fur_color/apply_to_human(mob/living/carbon/human/target, value)
diff --git a/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/tribe.dm b/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/tribe.dm
index 20ee26de9950..d27e9b97dd0a 100644
--- a/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/tribe.dm
+++ b/modular_darkpack/modules/werewolf_the_apocalypse/code/preferences/tribe.dm
@@ -22,6 +22,10 @@
var/joining_round = !isdummy(target)
target.set_fera_tribe(value, joining_round)
+/datum/preference/choiced/subsplat/fera_tribe/post_set_preference(mob/user, value)
+ var/datum/subsplat/werewolf/tribe/tribe = get_fera_tribe(value)
+ tribe?.show_lore(user)
+
/datum/preference/choiced/subsplat/fera_tribe/is_accessible(datum/preferences/preferences)
. = ..()
var/datum/splat/splat_path = preferences.read_preference(/datum/preference/choiced/splats)
diff --git a/modular_darkpack/modules/werewolf_the_apocalypse/code/species/transformation.dm b/modular_darkpack/modules/werewolf_the_apocalypse/code/species/transformation.dm
index a11ab096902d..44afa820bdac 100644
--- a/modular_darkpack/modules/werewolf_the_apocalypse/code/species/transformation.dm
+++ b/modular_darkpack/modules/werewolf_the_apocalypse/code/species/transformation.dm
@@ -17,7 +17,7 @@
if(istype(current_form, form_to_transform))
return
if(!force && !COOLDOWN_FINISHED(src, transform_cd))
- to_chat(owner, span_warning("Your shifting is on cooldown for one turn."))
+ to_chat(owner, span_warning("Your shifting is on cooldown for [DisplayTimeText(COOLDOWN_TIMELEFT(src, transform_cd))]."))
return
if(HAS_TRAIT(owner, TRAIT_METAMORPH))
@@ -86,6 +86,14 @@
/datum/splat/werewolf/shifter/proc/transform_finish(form_to_transform, time_taken = DOGGY_ANIMATION_TIME)
animate(owner, transform = null, color = "#FFFFFF", time = time_taken * 0.1)
+
+ // Hacky fix for angle getting messed up when transforming into a human while resting
+ if (owner.body_position == LYING_DOWN && ispath(form_to_transform, /datum/species/human/shifter/homid))
+ var/previous_angle = owner.set_lying_angle(0)
+ owner.set_species(form_to_transform)
+ owner.set_lying_angle(previous_angle)
+ return
+
owner.set_species(form_to_transform)
/datum/splat/werewolf/shifter/proc/is_breed_form()
diff --git a/modular_darkpack/modules/werewolf_the_apocalypse/code/splats/fera_splat.dm b/modular_darkpack/modules/werewolf_the_apocalypse/code/splats/fera_splat.dm
index dd460d52115b..3983a23cef03 100644
--- a/modular_darkpack/modules/werewolf_the_apocalypse/code/splats/fera_splat.dm
+++ b/modular_darkpack/modules/werewolf_the_apocalypse/code/splats/fera_splat.dm
@@ -226,6 +226,7 @@
name = "Corax"
id = SPLAT_CORAX
splat_traits = list(
+ TRAIT_FERA_FORMS,
TRAIT_FERA_FUR,
TRAIT_FERA_RENOWN,
TRAIT_FERA_FLIGHT,
diff --git a/modular_darkpack/modules/werewolf_the_apocalypse/code/subsplats/breeds/corax.dm b/modular_darkpack/modules/werewolf_the_apocalypse/code/subsplats/breeds/corax.dm
index 7c2e61e857d6..f05a6876dbf7 100644
--- a/modular_darkpack/modules/werewolf_the_apocalypse/code/subsplats/breeds/corax.dm
+++ b/modular_darkpack/modules/werewolf_the_apocalypse/code/subsplats/breeds/corax.dm
@@ -4,7 +4,7 @@
/datum/subsplat/werewolf/breed_form/corax/homid
name = BREED_CORAX_HOMID
- start_gnosis = 1
+ start_gnosis = 6
breed_species = /datum/species/human/shifter/homid
gifts_provided = list(
/datum/action/cooldown/power/gift/enemy_ways,
@@ -14,7 +14,7 @@
/datum/subsplat/werewolf/breed_form/corax/corvid
name = BREED_CORVID
- start_gnosis = 5
+ start_gnosis = 6
breed_species = /datum/species/human/shifter/feral
gifts_provided = list(
/datum/action/cooldown/power/gift/enemy_ways,
diff --git a/modular_darkpack/modules/werewolf_the_apocalypse/code/subsplats/breeds/garou.dm b/modular_darkpack/modules/werewolf_the_apocalypse/code/subsplats/breeds/garou.dm
index 329c30950fe5..5c97ce104781 100644
--- a/modular_darkpack/modules/werewolf_the_apocalypse/code/subsplats/breeds/garou.dm
+++ b/modular_darkpack/modules/werewolf_the_apocalypse/code/subsplats/breeds/garou.dm
@@ -15,7 +15,7 @@
breed_species = /datum/species/human/shifter/war
/datum/subsplat/werewolf/breed_form/garou/crinos/generation_pref_icon(datum/universal_icon/main_icon)
- var/datum/universal_icon/breed_lupus = uni_icon('modular_darkpack/modules/werewolf_the_apocalypse/icons/garou_forms/lupus.dmi', "black")
+ var/datum/universal_icon/breed_lupus = uni_icon('modular_darkpack/modules/werewolf_the_apocalypse/icons/garou_forms/crinos.dmi', "black")
breed_lupus.scale(32, 32)
main_icon.blend_icon(breed_lupus, ICON_OVERLAY)
@@ -26,6 +26,6 @@
breed_species = /datum/species/human/shifter/feral
/datum/subsplat/werewolf/breed_form/garou/lupus/generation_pref_icon(datum/universal_icon/main_icon)
- var/datum/universal_icon/breed_crinos = uni_icon('modular_darkpack/modules/werewolf_the_apocalypse/icons/garou_forms/crinos.dmi', "black")
+ var/datum/universal_icon/breed_crinos = uni_icon('modular_darkpack/modules/werewolf_the_apocalypse/icons/garou_forms/lupus.dmi', "black")
breed_crinos.scale(32, 32)
main_icon.blend_icon(breed_crinos, ICON_OVERLAY)
diff --git a/modular_darkpack/modules/werewolf_the_apocalypse/icons/hud_transforms_corax.dmi b/modular_darkpack/modules/werewolf_the_apocalypse/icons/hud_transforms_corax.dmi
index 6d04df9f25d3..a42f55380744 100644
Binary files a/modular_darkpack/modules/werewolf_the_apocalypse/icons/hud_transforms_corax.dmi and b/modular_darkpack/modules/werewolf_the_apocalypse/icons/hud_transforms_corax.dmi differ
diff --git a/modular_zapoc/modules/clothing/code/tracksuit.dm b/modular_zapoc/modules/clothing/code/tracksuit.dm
index 2f5085a20222..ceec286a0ca1 100644
--- a/modular_zapoc/modules/clothing/code/tracksuit.dm
+++ b/modular_zapoc/modules/clothing/code/tracksuit.dm
@@ -12,7 +12,7 @@
var/mutable_appearance/jacket_overlay
-/obj/item/clothing/under/trackpants/Initialize()
+/obj/item/clothing/under/trackpants/Initialize(mapload)
. = ..()
if(spawn_with_jacket)
var/obj/item/clothing/suit/trackjacket/TJ = new spawn_jacket
diff --git a/modular_zapoc/modules/decor/code/man_crystal.dm b/modular_zapoc/modules/decor/code/man_crystal.dm
index 1eefb0f1ff43..ee8aa9edf769 100644
--- a/modular_zapoc/modules/decor/code/man_crystal.dm
+++ b/modular_zapoc/modules/decor/code/man_crystal.dm
@@ -7,7 +7,7 @@
max_integrity = 400
color = "#c4eaff"
-/obj/structure/man_crystal/Initialize()
+/obj/structure/man_crystal/Initialize(mapload)
. = ..()
set_light(6, l_color = color)
diff --git a/modular_zapoc/modules/decor/code/torch.dm b/modular_zapoc/modules/decor/code/torch.dm
index 3cb13135962c..9786cc126bab 100644
--- a/modular_zapoc/modules/decor/code/torch.dm
+++ b/modular_zapoc/modules/decor/code/torch.dm
@@ -9,7 +9,7 @@
burn_icon = "torch_lit"
-// /obj/structure/bonfire/torch/Initialize()
+// /obj/structure/bonfire/torch/Initialize(mapload)
// . = ..()
// if(start_lit)
// StartBurning()
diff --git a/modular_zapoc/modules/mapping/code/mapping_helper.dm b/modular_zapoc/modules/mapping/code/mapping_helper.dm
index 5744ef0d5168..dac9e4d6811d 100644
--- a/modular_zapoc/modules/mapping/code/mapping_helper.dm
+++ b/modular_zapoc/modules/mapping/code/mapping_helper.dm
@@ -7,7 +7,7 @@
var/chance_to_del = 100
var/range = 0
-/obj/effect/mapping_helpers/deleter/Initialize()
+/obj/effect/mapping_helpers/deleter/Initialize(mapload)
. = ..()
if(!objs_to_del.len)
stack_trace("Deleter helper placed with no target type!")
diff --git a/rust_g.dll b/rust_g.dll
index 62c8ba24f015..3fafd146ec81 100644
Binary files a/rust_g.dll and b/rust_g.dll differ
diff --git a/sound/attributions.txt b/sound/attributions.txt
index ade4c8fe7e7b..3e315731646e 100644
--- a/sound/attributions.txt
+++ b/sound/attributions.txt
@@ -246,5 +246,7 @@ sound/effect/droplet.ogg -- "Drop - Water" By mattfinarelli -- https://freesound
sound/effects/swapper/swap_A.ogg and swap_B.ogg by ArcaneMusic, License: Creative Commons 0, man-made mouth noises, slight cleanup in audacity.
+sound/items/weapons/earthcracker_bang.mp3 by Jean Filho -- https://freesound.org/people/Jean_Filho/sounds/807381/ -- License: Creative Commons 0
+
sound/machines/data_transmission.ogg -- Elements from Tim Verberne -- https://freesound.org/people/Tim_Verberne/sounds/558942/ -- License: Creative Commons 0
and samples from Ping.ogg from throughout the codebase.
diff --git a/sound/items/weapons/earthcracker_bang.mp3 b/sound/items/weapons/earthcracker_bang.mp3
new file mode 100644
index 000000000000..7baaf2ac0ba4
Binary files /dev/null and b/sound/items/weapons/earthcracker_bang.mp3 differ
diff --git a/sound/machines/fire_alarm/fire_alarm1.ogg b/sound/machines/fire_alarm/fire_alarm1.ogg
index 4c709873c563..e33bdaa66be6 100644
Binary files a/sound/machines/fire_alarm/fire_alarm1.ogg and b/sound/machines/fire_alarm/fire_alarm1.ogg differ
diff --git a/sound/machines/fire_alarm/fire_alarm2.ogg b/sound/machines/fire_alarm/fire_alarm2.ogg
index ff34af491cd5..7900d6c9a069 100644
Binary files a/sound/machines/fire_alarm/fire_alarm2.ogg and b/sound/machines/fire_alarm/fire_alarm2.ogg differ
diff --git a/sound/machines/fire_alarm/fire_alarm3.ogg b/sound/machines/fire_alarm/fire_alarm3.ogg
index 09d970bdc370..7c09d2137aad 100644
Binary files a/sound/machines/fire_alarm/fire_alarm3.ogg and b/sound/machines/fire_alarm/fire_alarm3.ogg differ
diff --git a/sound/machines/fire_alarm/fire_alarm4.ogg b/sound/machines/fire_alarm/fire_alarm4.ogg
index 1b2491925c71..56569e7b9d4e 100644
Binary files a/sound/machines/fire_alarm/fire_alarm4.ogg and b/sound/machines/fire_alarm/fire_alarm4.ogg differ
diff --git a/strings/gizmo_words.txt b/strings/gizmo_words.txt
new file mode 100644
index 000000000000..e90a089ad234
--- /dev/null
+++ b/strings/gizmo_words.txt
@@ -0,0 +1,59 @@
+ba
+be
+bo
+bi
+ca
+ce
+co
+ci
+da
+de
+do
+di
+fa
+fe
+fo
+fi
+ga
+ge
+go
+gi
+ha
+he
+ho
+hi
+ja
+je
+jo
+ji
+ka
+ke
+ko
+ki
+la
+le
+lo
+li
+ma
+me
+mo
+mi
+na
+ne
+no
+pa
+pe
+po
+pi
+ra
+re
+ro
+ri
+sa
+se
+so
+si
+wa
+we
+wo
+wi
diff --git a/strings/tips.txt b/strings/tips.txt
index d832de900b3e..45942b207768 100644
--- a/strings/tips.txt
+++ b/strings/tips.txt
@@ -297,6 +297,7 @@ When hacking doors, cutting and mending a "test light wire" will restore power t
When in doubt about technical issues, clear your cache (byond launcher > cogwheel > preferences > game prefs), update your BYOND, and relog.
When placing floor tiles in space, you don't need to place down lattice if there is a piece of plating nearby.
Where the space map levels connect is randomized every round, but are otherwise kept consistent within rounds. Remember that they are not necessarily bidirectional!
+While the floor changer buttons allow you to move through vertical open space, you can also right-click it to look through open space and even glass floors without having to move!
Working out improves your fitness which increases your size and faster times to fireman carry. Remember that a quality diet and sleep are essential!
You can alt-click tank transfer valves to remove a tank from them.
You can automatically extract and retract arm implants by 'activating' the empty hand they're on. This includes integrated toolsets, cursed katanas, and vorpal scythes.
diff --git a/tgstation.dme b/tgstation.dme
index 21cd0de215c6..dadb532a4f08 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -98,6 +98,7 @@
#include "code\__DEFINES\dye_keys.dm"
#include "code\__DEFINES\economy.dm"
#include "code\__DEFINES\electrified_buckle.dm"
+#include "code\__DEFINES\event_logger.dm"
#include "code\__DEFINES\events.dm"
#include "code\__DEFINES\exosuit_fab.dm"
#include "code\__DEFINES\experisci.dm"
@@ -114,6 +115,7 @@
#include "code\__DEFINES\fov.dm"
#include "code\__DEFINES\generators.dm"
#include "code\__DEFINES\ghost.dm"
+#include "code\__DEFINES\gizmo.dm"
#include "code\__DEFINES\gradient.dm"
#include "code\__DEFINES\gravity.dm"
#include "code\__DEFINES\guardian_defines.dm"
@@ -227,6 +229,7 @@
#include "code\__DEFINES\si.dm"
#include "code\__DEFINES\sight.dm"
#include "code\__DEFINES\skills.dm"
+#include "code\__DEFINES\skin.dm"
#include "code\__DEFINES\song.dm"
#include "code\__DEFINES\sort_types.dm"
#include "code\__DEFINES\sound.dm"
@@ -827,6 +830,7 @@
#include "code\controllers\subsystem\shuttle.dm"
#include "code\controllers\subsystem\skills.dm"
#include "code\controllers\subsystem\sound_loops.dm"
+#include "code\controllers\subsystem\sound_tokens.dm"
#include "code\controllers\subsystem\sounds.dm"
#include "code\controllers\subsystem\spatial_gridmap.dm"
#include "code\controllers\subsystem\speech_controller.dm"
@@ -961,6 +965,7 @@
#include "code\datums\ruins.dm"
#include "code\datums\saymode.dm"
#include "code\datums\signals.dm"
+#include "code\datums\sound_token.dm"
#include "code\datums\spawners_menu.dm"
#include "code\datums\sprite_accessories.dm"
#include "code\datums\station_alert.dm"
@@ -968,7 +973,6 @@
#include "code\datums\stock_market_events.dm"
#include "code\datums\tgs_event_handler.dm"
#include "code\datums\verb_callbacks.dm"
-#include "code\datums\verbs.dm"
#include "code\datums\view.dm"
#include "code\datums\visual_data.dm"
#include "code\datums\voice_of_god_command.dm"
@@ -1028,7 +1032,6 @@
#include "code\datums\actions\mobs\sign_language.dm"
#include "code\datums\actions\mobs\sneak.dm"
#include "code\datums\actions\mobs\teleport.dm"
-#include "code\datums\actions\mobs\transform_weapon.dm"
#include "code\datums\actions\mobs\sequences\dash_attack.dm"
#include "code\datums\actions\mobs\sequences\projectile.dm"
#include "code\datums\ai\_ai_behavior.dm"
@@ -1072,6 +1075,7 @@
#include "code\datums\ai\basic_mobs\basic_subtrees\capricious_retaliate.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\climb_tree.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\drag_items.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\enrage.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\escape_captivity.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\express_happiness.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm"
@@ -1108,6 +1112,8 @@
#include "code\datums\ai\basic_mobs\pet_commands\pet_follow_friend.dm"
#include "code\datums\ai\basic_mobs\pet_commands\pet_use_targeted_ability.dm"
#include "code\datums\ai\basic_mobs\pet_commands\play_dead.dm"
+#include "code\datums\ai\basic_mobs\target_priority_strategies\_target_priority_strategy.dm"
+#include "code\datums\ai\basic_mobs\target_priority_strategies\mining_strategies.dm"
#include "code\datums\ai\basic_mobs\targeting_strategies\_targeting_strategy.dm"
#include "code\datums\ai\basic_mobs\targeting_strategies\basic_targeting_strategy.dm"
#include "code\datums\ai\basic_mobs\targeting_strategies\dont_target_friends.dm"
@@ -1202,6 +1208,7 @@
#include "code\datums\components\atom_mounted.dm"
#include "code\datums\components\aura_healing.dm"
#include "code\datums\components\bakeable.dm"
+#include "code\datums\components\bane.dm"
#include "code\datums\components\banned_from_space.dm"
#include "code\datums\components\basic_inhands.dm"
#include "code\datums\components\basic_mob_attack_telegraph.dm"
@@ -1257,6 +1264,7 @@
#include "code\datums\components\defaceable.dm"
#include "code\datums\components\dejavu.dm"
#include "code\datums\components\deployable.dm"
+#include "code\datums\components\digitigrade_linb.dm"
#include "code\datums\components\direct_explosive_trap.dm"
#include "code\datums\components\earprotection.dm"
#include "code\datums\components\echolocation.dm"
@@ -1300,6 +1308,7 @@
#include "code\datums\components\heirloom.dm"
#include "code\datums\components\hide_highest_offset.dm"
#include "code\datums\components\hide_weather_planes.dm"
+#include "code\datums\components\hitsplat.dm"
#include "code\datums\components\holderloving.dm"
#include "code\datums\components\holographic_nature.dm"
#include "code\datums\components\houlihan_teleport.dm"
@@ -1377,6 +1386,7 @@
#include "code\datums\components\religious_tool.dm"
#include "code\datums\components\rename.dm"
#include "code\datums\components\reskinnable_atom.dm"
+#include "code\datums\components\revenant_prison.dm"
#include "code\datums\components\revenge_ability.dm"
#include "code\datums\components\rot.dm"
#include "code\datums\components\scope.dm"
@@ -1610,7 +1620,6 @@
#include "code\datums\elements\attack_equip.dm"
#include "code\datums\elements\attack_zone_randomiser.dm"
#include "code\datums\elements\backblast.dm"
-#include "code\datums\elements\bane.dm"
#include "code\datums\elements\basic_allergenic_attack.dm"
#include "code\datums\elements\basic_eating.dm"
#include "code\datums\elements\basic_health_examine.dm"
@@ -1618,6 +1627,7 @@
#include "code\datums\elements\beauty.dm"
#include "code\datums\elements\bed_tucking.dm"
#include "code\datums\elements\befriend_petting.dm"
+#include "code\datums\elements\block_area_power_fail.dm"
#include "code\datums\elements\block_turf_fingerprints.dm"
#include "code\datums\elements\blocks_explosives.dm"
#include "code\datums\elements\blood_limb_overlay.dm"
@@ -1718,6 +1728,7 @@
#include "code\datums\elements\move_force_on_death.dm"
#include "code\datums\elements\movement_turf_changer.dm"
#include "code\datums\elements\movetype_handler.dm"
+#include "code\datums\elements\moving_randomly.dm"
#include "code\datums\elements\muffles_speech.dm"
#include "code\datums\elements\nav_computer_icon.dm"
#include "code\datums\elements\nerfed_pulling.dm"
@@ -1884,6 +1895,7 @@
#include "code\datums\mapgen\biomes\lavaland.dm"
#include "code\datums\mapgen\Cavegens\IcemoonCaves.dm"
#include "code\datums\mapgen\Cavegens\LavalandGenerator.dm"
+#include "code\datums\martial\_action.dm"
#include "code\datums\martial\_martial.dm"
#include "code\datums\martial\boxing.dm"
#include "code\datums\martial\cqc.dm"
@@ -2014,6 +2026,7 @@
#include "code\datums\quirks\negative_quirks\indebted.dm"
#include "code\datums\quirks\negative_quirks\insanity.dm"
#include "code\datums\quirks\negative_quirks\light_drinker.dm"
+#include "code\datums\quirks\negative_quirks\limper.dm"
#include "code\datums\quirks\negative_quirks\monophobia.dm"
#include "code\datums\quirks\negative_quirks\mute.dm"
#include "code\datums\quirks\negative_quirks\narcolepsy.dm"
@@ -2206,6 +2219,7 @@
#include "code\datums\votes\custom_vote.dm"
#include "code\datums\votes\map_vote.dm"
#include "code\datums\votes\restart_vote.dm"
+#include "code\datums\weather\particle_weather.dm"
#include "code\datums\weather\weather.dm"
#include "code\datums\weather\weather_types\ash_storm.dm"
#include "code\datums\weather\weather_types\floor_is_lava.dm"
@@ -2386,9 +2400,9 @@
#include "code\game\machinery\big_manipulator\_defines.dm"
#include "code\game\machinery\big_manipulator\big_manipulator.dm"
#include "code\game\machinery\big_manipulator\big_manipulator_interactions.dm"
-#include "code\game\machinery\big_manipulator\interaction_points.dm"
#include "code\game\machinery\big_manipulator\interaction_priorities.dm"
#include "code\game\machinery\big_manipulator\manipulator_arm.dm"
+#include "code\game\machinery\big_manipulator\manipulator_tasks.dm"
#include "code\game\machinery\big_manipulator\tasking.dm"
#include "code\game\machinery\camera\camera.dm"
#include "code\game\machinery\camera\camera_construction.dm"
@@ -2746,6 +2760,7 @@
#include "code\game\objects\items\devices\chameleonproj.dm"
#include "code\game\objects\items\devices\destabilizing_crystal.dm"
#include "code\game\objects\items\devices\desynchronizer.dm"
+#include "code\game\objects\items\devices\earthcracker.dm"
#include "code\game\objects\items\devices\electroadaptive_pseudocircuit.dm"
#include "code\game\objects\items\devices\flashlight.dm"
#include "code\game\objects\items\devices\forcefieldprojector.dm"
@@ -3145,7 +3160,6 @@
#include "code\game\objects\structures\icemoon\cave_entrance.dm"
#include "code\game\objects\structures\lavaland\geyser.dm"
#include "code\game\objects\structures\lavaland\gulag_vent.dm"
-#include "code\game\objects\structures\lavaland\necropolis_tendril.dm"
#include "code\game\objects\structures\lavaland\ore_vent.dm"
#include "code\game\objects\structures\plaques\_plaques.dm"
#include "code\game\objects\structures\plaques\static_plaques.dm"
@@ -3234,6 +3248,7 @@
#include "code\modules\admin\chat_commands.dm"
#include "code\modules\admin\check_antagonists.dm"
#include "code\modules\admin\create_mob.dm"
+#include "code\modules\admin\event_logger.dm"
#include "code\modules\admin\force_event.dm"
#include "code\modules\admin\fun_balloon.dm"
#include "code\modules\admin\greyscale_modify_menu.dm"
@@ -3276,6 +3291,7 @@
#include "code\modules\admin\smites\fireball.dm"
#include "code\modules\admin\smites\ghost_control.dm"
#include "code\modules\admin\smites\gib.dm"
+#include "code\modules\admin\smites\hitsplat.dm"
#include "code\modules\admin\smites\imaginary_friend_special.dm"
#include "code\modules\admin\smites\immerse.dm"
#include "code\modules\admin\smites\knot_shoes.dm"
@@ -3953,7 +3969,6 @@
#include "code\modules\bitrunning\antagonists\netguardian.dm"
#include "code\modules\bitrunning\components\avatar_connection.dm"
#include "code\modules\bitrunning\components\avatar_gear.dm"
-#include "code\modules\bitrunning\components\bitrunning_points.dm"
#include "code\modules\bitrunning\components\glitch.dm"
#include "code\modules\bitrunning\components\netpod_healing.dm"
#include "code\modules\bitrunning\components\npc_friendly.dm"
@@ -4240,7 +4255,7 @@
#include "code\modules\client\preferences\species_features\pod.dm"
#include "code\modules\client\preferences\species_features\vampire.dm"
#include "code\modules\client\verbs\ooc.dm"
-#include "code\modules\client\verbs\ping.dm"
+#include "code\modules\client\verbs\stat_panel.dm"
#include "code\modules\client\verbs\suicide.dm"
#include "code\modules\client\verbs\typing.dm"
#include "code\modules\client\verbs\who.dm"
@@ -5258,6 +5273,7 @@
#include "code\modules\mob\living\basic\blob_minions\blobbernaut.dm"
#include "code\modules\mob\living\basic\boss\boss.dm"
#include "code\modules\mob\living\basic\boss\blood_drunk_miner\_blood_drunk_miner.dm"
+#include "code\modules\mob\living\basic\boss\blood_drunk_miner\blood_drunk_actions.dm"
#include "code\modules\mob\living\basic\boss\blood_drunk_miner\blood_drunk_ai.dm"
#include "code\modules\mob\living\basic\boss\blood_drunk_miner\blood_drunk_objects.dm"
#include "code\modules\mob\living\basic\boss\blood_drunk_miner\blood_drunk_subtypes.dm"
@@ -5374,6 +5390,7 @@
#include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp.dm"
#include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp_abilities.dm"
#include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp_ai.dm"
+#include "code\modules\mob\living\basic\icemoon\polar_bear\polar_bear.dm"
#include "code\modules\mob\living\basic\icemoon\wolf\wolf.dm"
#include "code\modules\mob\living\basic\icemoon\wolf\wolf_ai.dm"
#include "code\modules\mob\living\basic\icemoon\wolf\wolf_extras.dm"
@@ -5439,6 +5456,9 @@
#include "code\modules\mob\living\basic\lavaland\raptor\raptor_egg.dm"
#include "code\modules\mob\living\basic\lavaland\raptor\raptor_food_trough.dm"
#include "code\modules\mob\living\basic\lavaland\raptor\raptor_inheritance.dm"
+#include "code\modules\mob\living\basic\lavaland\tendril\tendril.dm"
+#include "code\modules\mob\living\basic\lavaland\tendril\tendril_actions.dm"
+#include "code\modules\mob\living\basic\lavaland\tendril\tendril_ai.dm"
#include "code\modules\mob\living\basic\lavaland\watcher\watcher.dm"
#include "code\modules\mob\living\basic\lavaland\watcher\watcher_ai.dm"
#include "code\modules\mob\living\basic\lavaland\watcher\watcher_gaze.dm"
@@ -5784,7 +5804,6 @@
#include "code\modules\mob\living\simple_animal\hostile\megafauna\legion.dm"
#include "code\modules\mob\living\simple_animal\hostile\megafauna\wendigo.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\mining_mobs.dm"
-#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\polarbear.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\elite.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\goliath_broodmother.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\herald.dm"
@@ -6359,6 +6378,23 @@
#include "code\modules\research\experimentor\experimentor.dm"
#include "code\modules\research\experimentor\experiments.dm"
#include "code\modules\research\experimentor\wires.dm"
+#include "code\modules\research\gizmo\gizmo_controller.dm"
+#include "code\modules\research\gizmo\gizmo_interface.dm"
+#include "code\modules\research\gizmo\gizmo_items.dm"
+#include "code\modules\research\gizmo\gizmo_machines.dm"
+#include "code\modules\research\gizmo\gizmo_spawner.dm"
+#include "code\modules\research\gizmo\gizmo_voice.dm"
+#include "code\modules\research\gizmo\gizmo_wires.dm"
+#include "code\modules\research\gizmo\gizpuzzle.dm"
+#include "code\modules\research\gizmo\gizmodes\giz_filler.dm"
+#include "code\modules\research\gizmo\gizmodes\gizactives.dm"
+#include "code\modules\research\gizmo\gizmodes\gizbad.dm"
+#include "code\modules\research\gizmo\gizmodes\gizcopier.dm"
+#include "code\modules\research\gizmo\gizmodes\gizlectric.dm"
+#include "code\modules\research\gizmo\gizmodes\gizmisc.dm"
+#include "code\modules\research\gizmo\gizmodes\gizmood.dm"
+#include "code\modules\research\gizmo\gizmodes\gizmopper.dm"
+#include "code\modules\research\gizmo\gizmodes\gizporter.dm"
#include "code\modules\research\machinery\_production.dm"
#include "code\modules\research\machinery\circuit_imprinter.dm"
#include "code\modules\research\machinery\departmental_circuit_imprinter.dm"
@@ -7006,7 +7042,6 @@
#include "code\modules\zombie\items.dm"
#include "code\modules\zombie\organs.dm"
#include "interface\interface.dm"
-#include "interface\menu.dm"
#include "interface\stylesheet.dm"
#include "interface\skin.dmf"
#include "interface\fonts\fonts_datum.dm"
@@ -7814,7 +7849,6 @@
#include "modular_darkpack\modules\storyteller_stats\code\st_stats\base_type\_morality.dm"
#include "modular_darkpack\modules\storyteller_stats\code\st_stats\base_type\_pooled.dm"
#include "modular_darkpack\modules\storyteller_stats\code\st_stats\base_type\_virtue.dm"
-#include "modular_darkpack\modules\strings\global_strings.dm"
#include "modular_darkpack\modules\subtle\code\chat_helper.dm"
#include "modular_darkpack\modules\subtle\code\subtle.dm"
#include "modular_darkpack\modules\taser\code\taser.dm"
diff --git a/tgui/happydom.ts b/tgui/happydom.ts
index 18a7051a49ee..e3799b447408 100644
--- a/tgui/happydom.ts
+++ b/tgui/happydom.ts
@@ -2,4 +2,4 @@ import { GlobalRegistrator } from '@happy-dom/global-registrator';
GlobalRegistrator.register();
-import 'packages/tgui/__mocks__/setup.ts';
+import 'tgui/__mocks__/setup.ts';
diff --git a/tgui/packages/common/storage.ts b/tgui/packages/common/storage.ts
index 1955fa8ab8e1..9894fa1a95ec 100644
--- a/tgui/packages/common/storage.ts
+++ b/tgui/packages/common/storage.ts
@@ -33,6 +33,10 @@ const testHubStorage = testGeneric(
() => window.hubStorage && !!window.hubStorage.getItem,
);
+const STORAGE_CDN_TIMEOUT = 5000;
+const persistedStorageKeys = ['panel-settings', 'chat-state', 'chat-messages'];
+const legacyHubMigrationKeys = ['panel-settings'];
+
class HubStorageBackend implements StorageBackend {
public impl: StorageImplementation;
@@ -77,20 +81,35 @@ class IFrameIndexedDbBackend implements StorageBackend {
iframe.src = Byond.storageCdn;
const completePromise: Promise = new Promise((resolve) => {
- fetch(Byond.storageCdn, { method: "HEAD" }).then((response) => {
- if (response.status !== 200) {
- resolve(false);
+ const listener = (message: MessageEvent) => {
+ if (
+ message.source === iframe.contentWindow &&
+ message.data === 'ready'
+ ) {
+ resolveReady(true);
}
+ };
+ const resolveReady = (ready: boolean) => {
+ clearTimeout(timeout);
+ window.removeEventListener('message', listener);
+ resolve(ready);
+ };
+ const timeout = setTimeout(
+ () => resolveReady(false),
+ STORAGE_CDN_TIMEOUT,
+ );
+
+ fetch(Byond.storageCdn, { method: 'HEAD' })
+ .then((response) => {
+ if (response.status !== 200) {
+ resolveReady(false);
+ }
+ })
+ .catch(() => {
+ resolveReady(false);
+ });
- }).catch(() => {
- resolve(false);
- })
-
- window.addEventListener('message', (message) => {
- if (message.data === "ready") {
- resolve(true);
- }
- })
+ window.addEventListener('message', listener);
});
this.documentElement = document.body.appendChild(iframe);
@@ -105,11 +124,23 @@ class IFrameIndexedDbBackend implements StorageBackend {
async get(key: string): Promise {
const promise = new Promise((resolve) => {
- window.addEventListener('message', (message) => {
- if (message.data.key && message.data.key === key) {
+ const listener = (message: MessageEvent) => {
+ if (
+ message.source === this.iframeWindow &&
+ message.data.key &&
+ message.data.key === key
+ ) {
+ clearTimeout(timeout);
+ window.removeEventListener('message', listener);
resolve(message.data.value);
}
- });
+ };
+ const timeout = setTimeout(() => {
+ window.removeEventListener('message', listener);
+ resolve(undefined);
+ }, STORAGE_CDN_TIMEOUT);
+
+ window.addEventListener('message', listener);
});
this.iframeWindow.postMessage({ type: 'get', key: key }, '*');
@@ -143,65 +174,89 @@ class StorageProxy implements StorageBackend {
constructor() {
this.backendPromise = (async () => {
-
- // If we have not enabled byondstorage yet, we need to check
- // if we can use the IFrame, or if we need to enable byondstorage
- if (!testHubStorage()) {
-
- // If we have an IFrame URL we can use, and we haven't already enabled
- // byondstorage, we should use the IFrame backend
- if (Byond.storageCdn) {
- const iframe = new IFrameIndexedDbBackend();
-
- if ((await iframe.ready()) === true) {
- if (await iframe.get('byondstorage-migrated')) return iframe;
-
- Byond.winset(null, 'browser-options', '+byondstorage');
-
- await new Promise((resolve) => {
- document.addEventListener('byondstorageupdated', async () => {
- setTimeout(() => {
- const hub = new HubStorageBackend();
-
- // Migrate these existing settings from byondstorage to the IFrame
- for (const setting of ['panel-settings', 'chat-state', 'chat-messages']) {
- hub
- .get(setting)
- .then((settings) => iframe.set(setting, settings));
+ // Prefer the configured iframe storage when available. hubStorage may
+ // already be enabled by another window/server, but the iframe origin is
+ // the server-configured storage boundary.
+ if (Byond.storageCdn) {
+ const iframe = new IFrameIndexedDbBackend();
+
+ if ((await iframe.ready()) === true) {
+ if (await iframe.get('byondstorage-migrated')) return iframe;
+
+ const iframeHasPersistedStorage = (
+ await Promise.all(
+ persistedStorageKeys.map((setting) => iframe.get(setting)),
+ )
+ ).some((settings) => settings !== undefined);
+
+ if (!iframeHasPersistedStorage) {
+ const hubStorageWasEnabled = testHubStorage();
+ if (!hubStorageWasEnabled) {
+ Byond.winset(null, 'browser-options', '+byondstorage');
+
+ await new Promise((resolve) => {
+ document.addEventListener(
+ 'byondstorageupdated',
+ () => {
+ // This event is emitted *before* byondstorage is actually
+ // created, so we have to wait a little bit before using it.
+ setTimeout(resolve, 1);
+ },
+ { once: true },
+ );
+ });
+ }
+
+ const hub = new HubStorageBackend();
+
+ // Migrate safe legacy settings from byondstorage to the IFrame.
+ // Chat history may contain server-specific HTML/components from
+ // other codebases that shared the old byondstorage namespace.
+ await Promise.all(
+ legacyHubMigrationKeys.map(async (setting) => {
+ try {
+ const settings = await hub.get(setting);
+ if (settings !== undefined) {
+ await iframe.set(setting, settings);
}
+ } catch {
+ // Ignore unreadable legacy storage entries. A bad old cache
+ // key should not keep the client on byondstorage.
+ }
+ }),
+ );
+
+ if (!hubStorageWasEnabled) {
+ Byond.winset(null, 'browser-options', '-byondstorage');
+ }
+ }
- iframe.set('byondstorage-migrated', true);
- Byond.winset(null, 'browser-options', '-byondstorage');
-
- resolve();
- }, 1);
- });
- });
+ await iframe.set('byondstorage-migrated', true);
- return iframe;
- }
+ return iframe;
+ }
- iframe.destroy();
- };
+ iframe.destroy();
+ }
- // IFrame hasn't worked out for us, we'll need to enable byondstorage
- Byond.winset(null, 'browser-options', '+byondstorage');
+ if (testHubStorage()) {
+ return new HubStorageBackend();
+ }
- return new Promise((resolve) => {
- const listener = () => {
- document.removeEventListener('byondstorageupdated', listener);
+ // IFrame hasn't worked out for us, we'll need to enable byondstorage
+ Byond.winset(null, 'browser-options', '+byondstorage');
- // This event is emitted *before* byondstorage is actually created
- // so we have to wait a little bit before we can use it
- setTimeout(() => resolve(new HubStorageBackend()), 1);
- };
+ return new Promise((resolve) => {
+ const listener = () => {
+ document.removeEventListener('byondstorageupdated', listener);
- document.addEventListener('byondstorageupdated', listener);
- });
- }
+ // This event is emitted *before* byondstorage is actually created
+ // so we have to wait a little bit before we can use it
+ setTimeout(() => resolve(new HubStorageBackend()), 1);
+ };
- // byondstorage is already enabled, we can use it straight away
- return new HubStorageBackend();
+ document.addEventListener('byondstorageupdated', listener);
+ });
})();
}
diff --git a/tgui/packages/tgui-chat-dark/index.ts b/tgui/packages/tgui-chat-dark/index.ts
new file mode 100644
index 000000000000..dd105da97743
--- /dev/null
+++ b/tgui/packages/tgui-chat-dark/index.ts
@@ -0,0 +1 @@
+import '../tgui-panel/styles/tgchat/chat-dark.scss';
diff --git a/tgui/packages/tgui-panel/chat/renderer.tsx b/tgui/packages/tgui-panel/chat/renderer.tsx
index bee627b36ebb..8c28e1942dc5 100644
--- a/tgui/packages/tgui-panel/chat/renderer.tsx
+++ b/tgui/packages/tgui-panel/chat/renderer.tsx
@@ -422,10 +422,19 @@ class ChatRenderer {
outputProps[canon_name] = working_value;
}
const oldHtml = { __html: childNode.innerHTML };
+ const Element = TGUI_CHAT_COMPONENTS[targetName];
+ if (!Element) {
+ logger.error(
+ `Error: unknown chat component "${targetName}" in message`,
+ message,
+ );
+ childNode.removeAttribute('data-component');
+ continue;
+ }
+
while (childNode.firstChild) {
childNode.removeChild(childNode.firstChild);
}
- const Element = TGUI_CHAT_COMPONENTS[targetName];
const reactRoot = createRoot(childNode);
diff --git a/tgui/packages/tgui-panel/ping/helpers.ts b/tgui/packages/tgui-panel/ping/helpers.ts
index ff22c82abb4b..f30441bc2fd7 100644
--- a/tgui/packages/tgui-panel/ping/helpers.ts
+++ b/tgui/packages/tgui-panel/ping/helpers.ts
@@ -46,6 +46,7 @@ export function pingSuccess(roundtrip: number): void {
failCount: 0,
networkQuality,
});
+ Byond.sendMessage('ping/set', { ping: roundtrip });
store.set(lastPingedAtAtom, Date.now());
}
diff --git a/tgui/packages/tgui-panel/settings/constants.ts b/tgui/packages/tgui-panel/settings/constants.ts
index 0e076fc07f63..2e5056f785e7 100644
--- a/tgui/packages/tgui-panel/settings/constants.ts
+++ b/tgui/packages/tgui-panel/settings/constants.ts
@@ -12,12 +12,16 @@ export const COLORS = {
BG_SECOND: '#151515',
BUTTON: '#404040',
TEXT: '#A6A6A6',
+ TEXT_IMPORTANT: '#A6A6A6',
+ BG_IMPORTANT: '#492020',
},
LIGHT: {
BG_BASE: '#EEEEEE',
BG_SECOND: '#FFFFFF',
BUTTON: '#FFFFFF',
TEXT: '#000000',
+ TEXT_IMPORTANT: '#A6A6A6',
+ BG_IMPORTANT: '#910707',
},
} as const;
diff --git a/tgui/packages/tgui-panel/settings/scaling.ts b/tgui/packages/tgui-panel/settings/scaling.ts
index 77cc5f831bae..784a253876f4 100644
--- a/tgui/packages/tgui-panel/settings/scaling.ts
+++ b/tgui/packages/tgui-panel/settings/scaling.ts
@@ -1,12 +1,11 @@
// This is the elements from the skin.dmf that we need to adjust the fontsize of
const ELEMENTS_TO_ADJUST = [
- 'infobuttons.changelog',
- 'infobuttons.rules',
- 'infobuttons.wiki',
- 'infobuttons.forum',
- 'infobuttons.github',
- 'infobuttons.report-issue',
+ 'infobuttons.options',
+ 'infobuttons.hotkeys',
+ 'infobuttons.emotes',
'infobuttons.fullscreen-toggle',
+ 'infobuttons.reconnect',
+ 'infobuttons.chat',
'inputwindow.input',
'inputbuttons.saybutton',
'inputbuttons.mebutton',
diff --git a/tgui/packages/tgui-panel/settings/themes.ts b/tgui/packages/tgui-panel/settings/themes.ts
index 1bfd05800cba..036962e72b60 100644
--- a/tgui/packages/tgui-panel/settings/themes.ts
+++ b/tgui/packages/tgui-panel/settings/themes.ts
@@ -46,20 +46,18 @@ export function setClientTheme(name): void | Promise {
'mainwindow.background-color': themeColor.BG_BASE,
'split.background-color': themeColor.BG_BASE,
// Buttons
- 'changelog.background-color': themeColor.BUTTON,
- 'changelog.text-color': themeColor.TEXT,
- 'rules.background-color': themeColor.BUTTON,
- 'rules.text-color': themeColor.TEXT,
- 'wiki.background-color': themeColor.BUTTON,
- 'wiki.text-color': themeColor.TEXT,
- 'forum.background-color': themeColor.BUTTON,
- 'forum.text-color': themeColor.TEXT,
- 'github.background-color': themeColor.BUTTON,
- 'github.text-color': themeColor.TEXT,
- 'report-issue.background-color': themeColor.BUTTON,
- 'report-issue.text-color': themeColor.TEXT,
+ 'options.background-color': themeColor.BUTTON,
+ 'options.text-color': themeColor.TEXT,
+ 'hotkeys.background-color': themeColor.BUTTON,
+ 'hotkeys.text-color': themeColor.TEXT,
+ 'emotes.background-color': themeColor.BUTTON,
+ 'emotes.text-color': themeColor.TEXT,
'fullscreen-toggle.background-color': themeColor.BUTTON,
'fullscreen-toggle.text-color': themeColor.TEXT,
+ 'reconnect.background-color': themeColor.BG_IMPORTANT,
+ 'reconnect.text-color': themeColor.TEXT_IMPORTANT,
+ 'chat.background-color': themeColor.BUTTON,
+ 'chat.text-color': themeColor.TEXT,
// Status and verb tabs
'output.background-color': themeColor.BG_BASE,
'output.text-color': themeColor.TEXT,
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
index b6062a68b1ad..4acc617c4fd6 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
@@ -589,7 +589,7 @@ em {
}
.green {
- color: hsl(132.8, 93.4%, 29.6%);
+ color: hsl(132.9, 100%, 50.6%);
}
.grey {
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
index f738f9a22461..4098d9a0ffc1 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
@@ -607,7 +607,7 @@ h2.alert {
}
.green {
- color: hsl(132.9, 100%, 50.6%);
+ color: hsl(132.8, 93.4%, 29.6%);
}
.grey {
diff --git a/tgui/packages/tgui/interfaces/BigManipulator/index.tsx b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx
index a3ee67ce97a2..144e9d3e94c9 100644
--- a/tgui/packages/tgui/interfaces/BigManipulator/index.tsx
+++ b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx
@@ -3,8 +3,8 @@ import {
BlockQuote,
Box,
Button,
+ Dropdown,
Icon,
- Input,
Modal,
Section,
Slider,
@@ -15,18 +15,30 @@ import type { BooleanLike } from 'tgui-core/react';
import { useBackend } from '../../backend';
import { Window } from '../../layouts';
+import type { ManipulatorData, ManipulatorTask } from './types';
+
+const TASK_TYPE_LABELS: Record = {
+ pickup: 'Pick up...',
+ drop: 'Drop...',
+ throw: 'Throw...',
+ use: 'Use held...',
+ interact: 'Interact...',
+ wait: 'Wait...',
+};
-import type { InteractionPoint, ManipulatorData } from './types';
-
-const taskingSchedules = ['Round Robin', 'Strict Robin', 'Prefer First'];
+const TASK_TYPE_ICONS: Record = {
+ pickup: 'hand',
+ drop: 'box-open',
+ interact: 'bolt',
+ wait: 'hourglass-half',
+};
-const taskingScheduleIcons = {
- 'Round Robin': 'list-ol',
- 'Strict Robin': 'arrows-spin',
- 'Prefer First': 'arrow-down-1-9',
+const TASKING_STRATEGY_ICONS: Record = {
+ Sequential: 'list-ol',
+ 'Strict order': 'lock',
};
-const buttonNumberToIcon = {
+const buttonNumberToIcon: Record = {
1: '',
2: 'arrow-up',
3: '',
@@ -38,29 +50,23 @@ const buttonNumberToIcon = {
9: '',
};
-const MasterControls = () => {
+function MasterControls() {
const { act, data } = useBackend();
- const {
- delay_step,
- speed_multiplier,
- min_speed_multiplier,
- max_speed_multiplier,
- } = data;
+ const { speed_multiplier, min_speed_multiplier, max_speed_multiplier } = data;
+
return (
{
maxValue={max_speed_multiplier}
unit="x"
stepPixelSize={20}
- onDrag={(value) =>
- act('adjust_interaction_speed', {
- new_speed: value,
- })
+ onChange={(_e, value) =>
+ act('adjust_interaction_speed', { new_speed: value })
}
/>
@@ -80,9 +84,7 @@ const MasterControls = () => {
-
-
-
diff --git a/tgui/packages/tgui/interfaces/LaborClaimConsole.jsx b/tgui/packages/tgui/interfaces/LaborClaimConsole.jsx
index 0cd5f6823615..21e92eeffd19 100644
--- a/tgui/packages/tgui/interfaces/LaborClaimConsole.jsx
+++ b/tgui/packages/tgui/interfaces/LaborClaimConsole.jsx
@@ -5,20 +5,29 @@ import { Window } from '../layouts';
export const LaborClaimConsole = (props) => {
const { act, data } = useBackend();
- const { can_go_home, id_points, ores, status_info, unclaimed_points } = data;
+ const {
+ can_go_home,
+ id_points,
+ ores,
+ status_info,
+ unclaimed_points,
+ shuttle_exists,
+ } = data;
return (
{status_info}
-
- act('move_shuttle')}
- />
-
+ {!!shuttle_exists && (
+
+ act('move_shuttle')}
+ />
+
+ )}
{id_points} {
- The nearby stacking machine will unload crates and collect smelted
- materials, points will be calculated based on volume of delivered
- materials.
+ Collect boulders and ores from the mining area and bring them to the
+ unloading machine. Then, pull the lever to smelt them into sheets. Use
+ the production console to dispense the sheets, and then bring bring
+ them to the claim console to earn points.
+
+
+ Boulders cannot be smelted directly, and will require additional
+ manual processing - strike them with a pickaxe to break them down
+ further.
+
- Please note that only sheets printed with our manufacturer's seal
- of quality, such as those produced from the work camp furnace, will be
- accepted as proof of labour.
+ Sheets smelted from the labor camp's furnace will be stamped with an
+ official seal of quality. Only stamped sheets can be claimed for
+ points - so don't bother trying to strip down tables and chairs for
+ bonus metal.
diff --git a/tgui/packages/tgui/interfaces/LaunchpadConsole.tsx b/tgui/packages/tgui/interfaces/LaunchpadConsole.tsx
index 30f9a0008b51..4ec0781bcf3a 100644
--- a/tgui/packages/tgui/interfaces/LaunchpadConsole.tsx
+++ b/tgui/packages/tgui/interfaces/LaunchpadConsole.tsx
@@ -52,7 +52,7 @@ export function LaunchpadConsole(props) {
const { launchpads = [], selected_id } = data;
return (
-
+
{launchpads.length === 0 ? (
No Pads Connected
diff --git a/tgui/packages/tgui/interfaces/LaunchpadRemote.tsx b/tgui/packages/tgui/interfaces/LaunchpadRemote.tsx
index d56bfd1cc30f..77c896803bb0 100644
--- a/tgui/packages/tgui/interfaces/LaunchpadRemote.tsx
+++ b/tgui/packages/tgui/interfaces/LaunchpadRemote.tsx
@@ -17,8 +17,8 @@ export const LaunchpadRemote = (props) => {
return (
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/darkpack_fur_color.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/darkpack_fur_color.tsx
index fd37c54e8e3e..f6bb6b7f0fa4 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/darkpack_fur_color.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/darkpack_fur_color.tsx
@@ -2,6 +2,11 @@ import type { FeatureChoiced } from '../base';
import { FeatureDropdownInput } from '../dropdowns';
export const garou_fur_color: FeatureChoiced = {
- name: 'Fera Fur Color',
+ name: 'Garou Fur Color',
+ component: FeatureDropdownInput,
+};
+
+export const corax_fur_color: FeatureChoiced = {
+ name: 'Corax Feather Color',
component: FeatureDropdownInput,
};
diff --git a/tgui/packages/tgui/interfaces/Techweb/helpers.ts b/tgui/packages/tgui/interfaces/Techweb/helpers.ts
index 12e042963d70..8215572d64f5 100644
--- a/tgui/packages/tgui/interfaces/Techweb/helpers.ts
+++ b/tgui/packages/tgui/interfaces/Techweb/helpers.ts
@@ -51,6 +51,7 @@ function selectRemappedStaticData(data: TechWebData) {
unlock_ids: map(node.unlock_ids || [], remapId),
required_experiments: node.required_experiments || [],
discount_experiments: node.discount_experiments || [],
+ discount_boosts: node.discount_boosts || [],
};
}
diff --git a/tgui/packages/tgui/interfaces/Techweb/nodes/TechNode.tsx b/tgui/packages/tgui/interfaces/Techweb/nodes/TechNode.tsx
index 6e4d7aadb403..613fe82708d3 100644
--- a/tgui/packages/tgui/interfaces/Techweb/nodes/TechNode.tsx
+++ b/tgui/packages/tgui/interfaces/Techweb/nodes/TechNode.tsx
@@ -40,6 +40,7 @@ export function TechNode(props: Props) {
tier,
enqueued_by_user,
is_free,
+ discount_boosted,
} = node;
const {
name,
@@ -49,6 +50,7 @@ export function TechNode(props: Props) {
prereq_ids,
required_experiments,
discount_experiments,
+ discount_boosts,
} = node_cache[id];
const [techwebRoute, setTechwebRoute] = useTechWebRoute();
@@ -86,12 +88,20 @@ export function TechNode(props: Props) {
// Notice that this logic will have to be changed if we make the discounts
// pool-specific
- const nodeDiscount = Object.keys(discount_experiments)
+ const nodeDiscountExperiments = Object.keys(discount_experiments)
.filter((x) => experiments[x]?.completed)
.reduce((tot, curr) => {
return tot + discount_experiments[curr];
}, 0);
+ // Will need to be changed (along with some backend/DM code) if boosts should
+ // ever vary by point type. As is, this simply adds up all discount boosts.
+ const nodeDiscountBoosts = discount_boosted
+ ? Object.keys(discount_boosts).reduce((tot, curr) => {
+ return tot + discount_boosts[curr];
+ }, 0)
+ : 0;
+
return (
{costs.map((k) => {
- const reqPts = Math.max(0, k.value - nodeDiscount);
+ const reqPts = Math.max(
+ 0,
+ k.value - nodeDiscountExperiments - nodeDiscountBoosts,
+ );
const nodeProg = Math.min(reqPts, points[k.type]) || 0;
return (
diff --git a/tgui/packages/tgui/interfaces/Techweb/types.ts b/tgui/packages/tgui/interfaces/Techweb/types.ts
index 0c40c46f47d4..9ccfd0d39911 100644
--- a/tgui/packages/tgui/interfaces/Techweb/types.ts
+++ b/tgui/packages/tgui/interfaces/Techweb/types.ts
@@ -16,6 +16,7 @@ export type NodeCache = {
description: string;
design_ids: string[];
discount_experiments: Record;
+ discount_boosts: Record;
name: string;
prereq_ids: string[];
required_experiments?: string[];
@@ -32,6 +33,7 @@ export type TechwebNode = {
can_unlock: BooleanLike;
enqueued_by_user: BooleanLike;
have_experiments_done: BooleanLike;
+ discount_boosted: BooleanLike;
id: string;
is_free: BooleanLike;
tier: number;
diff --git a/tgui/packages/tgui/interfaces/VotePanel.tsx b/tgui/packages/tgui/interfaces/VotePanel.tsx
index fe3ef670dee5..c04fd69e0c25 100644
--- a/tgui/packages/tgui/interfaces/VotePanel.tsx
+++ b/tgui/packages/tgui/interfaces/VotePanel.tsx
@@ -1,13 +1,14 @@
import {
+ BlockQuote,
Box,
Button,
- Collapsible,
Dimmer,
Icon,
LabeledList,
NoticeBox,
Section,
Stack,
+ Tooltip,
} from 'tgui-core/components';
import type { BooleanLike } from 'tgui-core/react';
@@ -60,18 +61,15 @@ type Data = {
currentVote: ActiveVote;
possibleVotes: Vote[];
user: UserData;
- voting: string[];
LastVoteTime: number;
VoteCD: number;
+ deadVoteEnabled: BooleanLike;
};
export const VotePanel = (props) => {
const { act, data } = useBackend();
const { currentVote, user, LastVoteTime, VoteCD } = data;
- /**
- * Adds the voting type to title if there is an ongoing vote.
- */
let windowTitle = 'Vote';
if (currentVote) {
windowTitle +=
@@ -84,37 +82,49 @@ export const VotePanel = (props) => {
return (
-
-
-
- act('resetCooldown')}
- />
-
-
- act('toggleDeadVote')}
- />
-
-
- )
- }
- >
-
- {!!user.isLowerAdmin && currentVote && }
-
-
-
+
+
+
+
+ act('resetCooldown')}
+ >
+ Reset cooldown
+
+
+
+ act('toggleDeadVote')}
+ checked={!data.deadVoteEnabled}
+ color="primary"
+ >
+ Dead votes
+
+
+
+ )
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -137,239 +147,193 @@ const VoteOptionDimmer = (props) => {
);
};
-/**
- * The create vote options menu. Only upper admins can disable voting.
- * @returns A section visible to everyone with vote options.
- */
const VoteOptions = (props) => {
const { act, data } = useBackend();
const { possibleVotes, user, LastVoteTime, VoteCD } = data;
return (
-
-
- {LastVoteTime + VoteCD > 0 && }
-
- {possibleVotes.map((option) => (
-
-
- {!!user.isLowerAdmin && (
-
-
- act('toggleVote', {
- voteName: option.name,
- })
- }
- />
-
- )}
-
-
- act('callVote', {
- voteName: option.name,
- })
- }
- />
-
-
+ {LastVoteTime + VoteCD > 0 && }
+
+ {possibleVotes.map((option) => (
+
+
+ {!!user.isLowerAdmin && (
+
+
+ act('toggleVote', {
+ voteName: option.name,
+ })
+ }
+ >
+ Active
+
+
+ )}
+
+
+ act('callVote', {
+ voteName: option.name,
+ })
+ }
+ icon="play"
+ />
- ))}
-
-
-
-
- );
-};
-
-/**
- * View Voters by ckey. Admin only.
- * @returns A collapsible list of voters
- */
-const VotersList = (props) => {
- const { data } = useBackend();
-
- return (
-
-
-
- {data.voting.map((voter) => {
- return {voter};
- })}
-
-
+
+
+
+ {option.name} Vote
+
+
+
+
+
+ ))}
+
);
};
-/**
- * The choices panel which displays all options in the list.
- * @returns A section visible to all users.
- */
const ChoicesPanel = (props) => {
const { act, data } = useBackend();
const { currentVote, user } = data;
return (
-
-
- {currentVote && currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
- Select one option
- ) : null}
- {currentVote &&
- currentVote.choices.length !== 0 &&
- currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
-
- {currentVote.choices.map((choice) => (
-
- c.toUpperCase())}
- textAlign="right"
- buttons={
- {
- act('voteSingle', { voteOption: choice.name });
- }}
- >
- Vote
-
- }
- >
- {user.singleSelection &&
- choice.name === user.singleSelection && (
-
- )}
- {currentVote.displayStatistics
- ? `${choice.votes} Votes`
- : null}
-
-
-
- ))}
-
- ) : null}
- {currentVote && currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
- Select any number of options
- ) : null}
- {currentVote &&
- currentVote.choices.length !== 0 &&
- currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
-
- {currentVote.choices.map((choice) => (
-
- c.toUpperCase())}
- textAlign="right"
- buttons={
- {
- act('voteMulti', { voteOption: choice.name });
- }}
- >
- Vote
-
- }
- >
- {user.multiSelection &&
- user.multiSelection[user.ckey.concat(choice.name)] === 1 ? (
-
- ) : null}
- {choice.votes} Votes
-
-
-
- ))}
-
- ) : null}
- {currentVote ? null : No vote active!}
-
-
+ <>
+ {currentVote && currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
+ Select one option
+ ) : null}
+ {currentVote &&
+ currentVote.choices.length !== 0 &&
+ currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
+
+ {currentVote.choices.map((choice) => (
+
+ c.toUpperCase())}
+ textAlign="right"
+ buttons={
+ {
+ act('voteSingle', { voteOption: choice.name });
+ }}
+ >
+ Vote
+
+ }
+ >
+ {user.singleSelection &&
+ choice.name === user.singleSelection && (
+
+ )}
+ {currentVote.displayStatistics ? `${choice.votes} Votes` : null}
+
+
+
+ ))}
+
+ ) : null}
+ {currentVote && currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
+ Select any number of options
+ ) : null}
+ {currentVote &&
+ currentVote.choices.length !== 0 &&
+ currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
+
+ {currentVote.choices.map((choice) => (
+
+ c.toUpperCase())}
+ textAlign="right"
+ buttons={
+ {
+ act('voteMulti', { voteOption: choice.name });
+ }}
+ >
+ Vote
+
+ }
+ >
+ {user.multiSelection &&
+ user.multiSelection[user.ckey.concat(choice.name)] === 1 ? (
+
+ ) : null}
+ {choice.votes} Votes
+
+
+
+ ))}
+
+ ) : null}
+ {currentVote ? null : No vote active!}
+ >
);
};
-/**
- * Countdown timer at the bottom. Includes a cancel vote option for admins.
- * @returns A section visible to everyone.
- */
const TimePanel = (props) => {
const { act, data } = useBackend();
const { currentVote, user } = data;
return (
-
-
-
-
- Time Remaining:
- {currentVote?.timeRemaining || 0}s
-
- {!!user.isLowerAdmin && (
-
-
- act('endNow')}
- >
- End Now
-
-
-
- act('cancel')}
- >
- Cancel Vote
-
-
-
- )}
-
-
+
+
+
+ {currentVote
+ ? `Time remaining: ${currentVote.timeRemaining}s`
+ : 'No current vote'}
+
+ {!!user.isLowerAdmin && (
+
+
+ act('endNow')}
+ style={{ lineHeight: '1.8em' }}
+ >
+ End Now
+
+
+
+ act('cancel')}
+ style={{ lineHeight: '1.8em' }}
+ >
+ Cancel
+
+
+
+ )}
+
);
};
diff --git a/tgui/packages/tgui/styles/interfaces/EventLogger.scss b/tgui/packages/tgui/styles/interfaces/EventLogger.scss
new file mode 100644
index 000000000000..f8e33cbf22bd
--- /dev/null
+++ b/tgui/packages/tgui/styles/interfaces/EventLogger.scss
@@ -0,0 +1,16 @@
+// Custom HTML tooltip shown on timeline mark hover, rendered at fixed position
+.evlog-mark-tooltip {
+ position: fixed;
+ background: #1e1e2e;
+ border: 1px solid #555;
+ border-radius: 4px;
+ padding: 4px 8px;
+ white-space: pre-wrap;
+ font-size: 11px;
+ color: #ccc;
+ pointer-events: none;
+ z-index: 9999;
+ min-width: 120px;
+ max-width: 320px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
+}
diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss
index d61fc9fc2c5d..4f01038d289d 100644
--- a/tgui/packages/tgui/styles/main.scss
+++ b/tgui/packages/tgui/styles/main.scss
@@ -50,6 +50,7 @@
@include meta.load-css('./interfaces/ColorPicker.scss');
@include meta.load-css('./interfaces/AnomalyTower.scss');
@include meta.load-css('./interfaces/CivCargoHoldTerminal.scss');
+@include meta.load-css('./interfaces/EventLogger.scss');
// Layouts
@include meta.load-css('./layouts/Layout.scss');
diff --git a/tgui/rspack.config.ts b/tgui/rspack.config.ts
index 2639920b031a..6c5f65b9690f 100644
--- a/tgui/rspack.config.ts
+++ b/tgui/rspack.config.ts
@@ -29,6 +29,7 @@ export default defineConfig({
tgui: './packages/tgui',
'tgui-panel': './packages/tgui-panel',
'tgui-say': './packages/tgui-say',
+ 'tgui-chat-dark': './packages/tgui-chat-dark',
},
mode: 'production',
module: {
diff --git a/tgui/tsconfig.json b/tgui/tsconfig.json
index 5ff7f5926b41..bd6c8e1559a3 100644
--- a/tgui/tsconfig.json
+++ b/tgui/tsconfig.json
@@ -2,7 +2,6 @@
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
- "baseUrl": ".",
"checkJs": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
@@ -13,14 +12,14 @@
"moduleResolution": "Bundler",
"noEmit": true,
"paths": {
- "tgui": ["packages/tgui"],
- "tgui/*": ["packages/tgui/*"],
- "tgui-panel": ["packages/tgui-panel"],
- "tgui-panel/*": ["packages/tgui-panel/*"],
- "tgui-say": ["packages/tgui-say"],
- "tgui-say/*": ["packages/tgui-say/*"],
- "tgui-dev-server": ["packages/tgui-dev-server"],
- "tgui-dev-server/*": ["packages/tgui-dev-server/*"]
+ "tgui": ["./packages/tgui"],
+ "tgui/*": ["./packages/tgui/*"],
+ "tgui-panel": ["./packages/tgui-panel"],
+ "tgui-panel/*": ["./packages/tgui-panel/*"],
+ "tgui-say": ["./packages/tgui-say"],
+ "tgui-say/*": ["./packages/tgui-say/*"],
+ "tgui-dev-server": ["./packages/tgui-dev-server"],
+ "tgui-dev-server/*": ["./packages/tgui-dev-server/*"]
},
"resolveJsonModule": true,
"skipLibCheck": true,
diff --git a/tools/HubMigrator/HubMigrator.dm b/tools/HubMigrator/HubMigrator.dm
index fd746f8d6d96..ea13dc6ae5d6 100644
--- a/tools/HubMigrator/HubMigrator.dm
+++ b/tools/HubMigrator/HubMigrator.dm
@@ -162,3 +162,35 @@
outfile << keyline.Join()
i++
outfile << "END"
+
+#undef MEDAL_METEOR
+#undef MEDAL_PULSE
+#undef MEDAL_TIMEWASTE
+#undef MEDAL_RODSUPLEX
+#undef MEDAL_CLOWNCARKING
+#undef MEDAL_THANKSALOT
+#undef MEDAL_HELBITALJANKEN
+#undef MEDAL_MATERIALCRAFT
+#undef BOSS_MEDAL_ANY
+#undef BOSS_MEDAL_MINER
+#undef BOSS_MEDAL_BUBBLEGUM
+#undef BOSS_MEDAL_COLOSSUS
+#undef BOSS_MEDAL_DRAKE
+#undef BOSS_MEDAL_HIEROPHANT
+#undef BOSS_MEDAL_LEGION
+#undef BOSS_MEDAL_TENDRIL
+#undef BOSS_MEDAL_MINER_CRUSHER
+#undef BOSS_MEDAL_BUBBLEGUM_CRUSHER
+#undef BOSS_MEDAL_COLOSSUS_CRUSHER
+#undef BOSS_MEDAL_DRAKE_CRUSHER
+#undef BOSS_MEDAL_HIEROPHANT_CRUSHER
+#undef BOSS_MEDAL_LEGION_CRUSHER
+#undef BOSS_SCORE
+#undef MINER_SCORE
+#undef BUBBLEGUM_SCORE
+#undef COLOSSUS_SCORE
+#undef DRAKE_SCORE
+#undef HIEROPHANT_SCORE
+#undef LEGION_SCORE
+#undef SWARMER_BEACON_SCORE
+#undef TENDRIL_CLEAR_SCORE
diff --git a/tools/SS13SmoothingCutter/Cutter.dm b/tools/SS13SmoothingCutter/Cutter.dm
index 696f503c783f..a0a4d620c5d2 100644
--- a/tools/SS13SmoothingCutter/Cutter.dm
+++ b/tools/SS13SmoothingCutter/Cutter.dm
@@ -1,8 +1,6 @@
-
-#define A_BIG_NUMBER 9999999
-#define STATE_COUNT_NORMAL 4
-#define STATE_COUNT_DIAGONAL 7
-
+#define A_BIG_NUMBER 9999999
+#define STATE_COUNT_NORMAL 4
+#define STATE_COUNT_DIAGONAL 7
/mob/verb/ChooseDMI(dmi as file)
var/dmifile = file(dmi)
@@ -38,7 +36,7 @@
fdel(filename) //force refresh
for(var/state in states)
- var/statename = lowertext(state)
+ var/statename = LOWER_TEXT(state)
outputIcon = icon(filename) //open the icon again each iteration, to work around byond memory limits
switch(statename)
@@ -224,3 +222,7 @@
fcopy(outputIcon, filename) //Update output icon each iteration
world << "Finished [filename]!"
+
+#undef A_BIG_NUMBER
+#undef STATE_COUNT_NORMAL
+#undef STATE_COUNT_DIAGONAL
diff --git a/tools/ScrollAnimationAssembler/apply.dm b/tools/ScrollAnimationAssembler/apply.dm
index f9773c609aa0..198b2dfb4d88 100644
--- a/tools/ScrollAnimationAssembler/apply.dm
+++ b/tools/ScrollAnimationAssembler/apply.dm
@@ -1,5 +1,5 @@
/// Cut out a piece of the background at a specified frame, and paste it on top of the foreground
-/proc/Apply(var/icon/source, var/icon/mask, var/icon/target, frame)
+/proc/Apply(icon/source, icon/mask, icon/target, frame)
//Temporary copies of source, mask and target
var/icon/source_copy = new(source)
diff --git a/tools/UpdatePaths/96141_forbidden_areas.txt b/tools/UpdatePaths/96141_forbidden_areas.txt
new file mode 100644
index 000000000000..046a11f5c2dc
--- /dev/null
+++ b/tools/UpdatePaths/96141_forbidden_areas.txt
@@ -0,0 +1 @@
+/area/station/engineering/supermatter : /area/station/engineering/supermatter/engine{@OLD}
diff --git a/tools/UpdatePaths/Scripts/96056_pistols_and_stuff.txt b/tools/UpdatePaths/Scripts/96056_pistols_and_stuff.txt
new file mode 100644
index 000000000000..8e3f4b313f01
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/96056_pistols_and_stuff.txt
@@ -0,0 +1,4 @@
+/obj/item/gun/ballistic/automatic/pistol/doorhickey : /obj/item/gun/ballistic/automatic/pistol/doohickey{@OLD}
+/obj/item/ammo_box/magazine/r10mm : /obj/item/ammo_box/magazine/r45{@OLD}
+/obj/item/ammo_casing/c10mm/reaper : /obj/item/ammo_casing/c45/reaper{@OLD}
+/obj/projectile/bullet/c10mm/reaper : /obj/projectile/bullet/c45/reaper{@OLD}
diff --git a/tools/UpdatePaths/Scripts/96186_tendrils.txt b/tools/UpdatePaths/Scripts/96186_tendrils.txt
new file mode 100644
index 000000000000..eb2f45499c42
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/96186_tendrils.txt
@@ -0,0 +1 @@
+/obj/structure/spawner/lavaland : /mob/living/basic/mining/tendril
diff --git a/tools/UpdatePaths/Scripts/96295_polarbear.txt b/tools/UpdatePaths/Scripts/96295_polarbear.txt
new file mode 100644
index 000000000000..c3b0c19872f7
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/96295_polarbear.txt
@@ -0,0 +1 @@
+/mob/living/simple_animal/hostile/asteroid/polarbear : /mob/living/basic/mining/polarbear{@OLD}
diff --git a/tools/UpdatePaths/Scripts/DarkPack/1093_better_fire_barrel.txt b/tools/UpdatePaths/Scripts/DarkPack/1093_better_fire_barrel.txt
new file mode 100644
index 000000000000..a26f2536a7be
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/DarkPack/1093_better_fire_barrel.txt
@@ -0,0 +1 @@
+/obj/structure/fire_barrel : /obj/structure/bonfire/fire_barrel {@OLD}
diff --git a/tools/UpdatePaths/Scripts/DarkPack/931_bad_pliers.txt b/tools/UpdatePaths/Scripts/DarkPack/931_bad_pliers.txt
index f433740bba25..d3dbf885754f 100644
--- a/tools/UpdatePaths/Scripts/DarkPack/931_bad_pliers.txt
+++ b/tools/UpdatePaths/Scripts/DarkPack/931_bad_pliers.txt
@@ -1 +1 @@
-/obj/item/wirecutters/pliers/bad_pliers : /obj/item/wirecutters/pliers/bad {@OLD}
+/obj/item/wirecutters/pliers/bad_pliers : /obj/item/wirecutters/pliers/bad {@OLD}
diff --git a/tools/ci/check_grep.sh b/tools/ci/check_grep.sh
index e80af87d5f71..da88e80a98c9 100755
--- a/tools/ci/check_grep.sh
+++ b/tools/ci/check_grep.sh
@@ -19,14 +19,14 @@ if command -v rg >/dev/null 2>&1; then
if [ ! rg -P '' >/dev/null 2>&1 ] ; then
pcre2_support=0
fi
- code_files="code/**/**.dm"
+ code_files=( . -g '*.dm' -g '!DMCompiler_linux-x64/**' -g '!tools/ci/od_lints.dm' -g '!tools/CatchUnescapedBrackets/**' -g '!html/changelogs/**' )
map_files="_maps/**/**.dmm"
shuttle_map_files="_maps/shuttles/**.dmm"
code_x_515="code/**/!(__byond_version_compat).dm"
else
pcre2_support=0
grep=grep
- code_files="-r --include=code/**/**.dm"
+ code_files=( -r --include='*.dm' --exclude-dir='DMCompiler_linux-x64' --exclude='od_lints.dm' --exclude-dir='CatchUnescapedBrackets' --exclude-dir='changelogs' . )
map_files="-r --include=_maps/**/**.dmm"
shuttle_map_files="-r --include=_maps/shuttles/**.dmm"
code_x_515="-r --include=code/**/!(__byond_version_compat).dm"
@@ -51,8 +51,8 @@ section "map issues"
part "TGM"
if $grep -U '^".+" = \(.+\)' $map_files; then
echo
- echo -e "${RED}ERROR: Non-TGM formatted map detected. Please convert it using Map Merger!${NC}"
- st=1
+ echo -e "${RED}ERROR: Non-TGM formatted map detected. Please convert it using Map Merger!${NC}"
+ st=1
fi;
part "comments"
if $grep '//' $map_files | $grep -v '//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE' | $grep -v 'name|desc'; then
@@ -63,8 +63,8 @@ fi;
part "iconstate tags"
if $grep '^\ttag = "icon' $map_files; then
echo
- echo -e "${RED}ERROR: Tag vars from icon state generation detected in maps, please remove them.${NC}"
- st=1
+ echo -e "${RED}ERROR: Tag vars from icon state generation detected in maps, please remove them.${NC}"
+ st=1
fi;
part "invalid map procs"
if $grep '(new|newlist|icon|matrix|sound)\(.+\)' $map_files; then
@@ -81,42 +81,47 @@ fi;
part "common spelling mistakes"
if $grep -i 'nanotransen' $map_files; then
echo
- echo -e "${RED}ERROR: Misspelling(s) of Nanotrasen detected in maps, please remove the extra N(s).${NC}"
- st=1
+ echo -e "${RED}ERROR: Misspelling(s) of Nanotrasen detected in maps, please remove the extra N(s).${NC}"
+ st=1
fi;
if $grep 'NanoTrasen' $map_files; then
echo
- echo -e "${RED}ERROR: Misspelling(s) of Nanotrasen detected in maps, please uncapitalize the T(s).${NC}"
- st=1
+ echo -e "${RED}ERROR: Misspelling(s) of Nanotrasen detected in maps, please uncapitalize the T(s).${NC}"
+ st=1
fi;
if $grep -i 'centcomm' $map_files; then
echo
- echo -e "${RED}ERROR: Misspelling(s) of CentCom detected in maps, please remove the extra M(s).${NC}"
- st=1
+ echo -e "${RED}ERROR: Misspelling(s) of CentCom detected in maps, please remove the extra M(s).${NC}"
+ st=1
fi;
if $grep -i 'eciev' $map_files; then
echo
- echo -e "${RED}ERROR: Common I-before-E typo detected in maps.${NC}"
- st=1
+ echo -e "${RED}ERROR: Common I-before-E typo detected in maps.${NC}"
+ st=1
fi;
if $grep -i 'maintainance|maintainence|maintenence' $map_files; then
+ echo
+ echo -e "${RED}ERROR: Misspelling(s) of 'maintenance' detected in maps, please fix.${NC}";
+ st=1
+fi;
+if $grep -i 'securaty|securiy|secuirty' $map_files; then
echo
- echo -e "${RED}ERROR: Misspelling(s) of 'maintenance' detected in maps, please fix.${NC}";
+ echo -e "${RED}ERROR: Misspelling(s) of 'security' detected in maps, please fix.${NC}";
st=1
fi;
section "whitespace issues"
part "space indentation"
-if $grep '(^ {2})|(^ [^ * ])|(^ +)' $code_files; then
+if $grep '(^ {2})|(^ [^ * ])|(^ +)' "${code_files[@]}"; then
echo
- echo -e "${RED}ERROR: Space indentation detected, please use tab indentation.${NC}"
- st=1
+ echo -e "${RED}ERROR: Space indentation detected, please use tab indentation.${NC}"
+ st=1
fi;
part "mixed indentation"
-if $grep '^\t+ [^ *]' $code_files; then
+if $grep '^\t+ [^ *]' "${code_files[@]}"; then
echo
- echo -e "${RED}ERROR: Mixed indentation detected, please stick to tab indentation.${NC}"
- st=1
+ echo -e "${RED}ERROR: Mixed indentation detected, please stick to tab indentation.${NC}"
+ st=1
fi;
section "unit tests"
@@ -132,48 +137,48 @@ fi;
section "516 Href Styles"
part "byond href styles"
-if $grep "href[\s='\"\\\\]*\?" $code_files ; then
- echo
- echo -e "${RED}ERROR: BYOND requires internal href links to begin with \"byond://\".${NC}"
- st=1
+if $grep "href[\s='\"\\\\]*\?" "${code_files[@]}" ; then
+ echo
+ echo -e "${RED}ERROR: BYOND requires internal href links to begin with \"byond://\".${NC}"
+ st=1
fi;
section "common mistakes"
part "global vars"
-if $grep '^/*var/' $code_files; then
+if $grep '^/*var/' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Unmanaged global var use detected in code, please use the helpers.${NC}"
st=1
fi;
part "proc args with var/"
-if $grep '^/[\w/]\S+\(.*(var/|, ?var/.*).*\)' $code_files; then
+if $grep '^/[\w/]\S+\(.*(var/|, ?var/.*).*\)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Changed files contains a proc argument starting with 'var'.${NC}"
st=1
fi;
part "improperly pathed static lists"
-if $grep -i 'var/list/static/.*' $code_files; then
+if $grep -i 'var/list/static/.*' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Found incorrect static list definition 'var/list/static/', it should be 'var/static/list/' instead.${NC}"
st=1
fi;
part "can_perform_action argument check"
-if $grep 'can_perform_action\(\s*\)' $code_files; then
+if $grep 'can_perform_action\(\s*\)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Found a can_perform_action() proc with improper arguments.${NC}"
st=1
fi;
part "src as a trait source" # ideally we'd lint / test for ANY datum reference as a trait source, but 'src' is the most common.
-if $grep -i '(add_trait|remove_trait)\(.+,\s*.+,\s*src\)' $code_files; then
+if $grep -i '(add_trait|remove_trait)\(.+,\s*.+,\s*src\)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Using 'src' as a trait source. Source must be a string key - dont't use references to datums as a source, perhaps use 'REF(src)'.${NC}"
st=1
fi;
-if $grep -i '(add_traits|remove_traits)\(.+,\s*src\)' $code_files; then
+if $grep -i '(add_traits|remove_traits)\(.+,\s*src\)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Using 'src' as trait sources. Source must be a string key - dont't use references to datums as sources, perhaps use 'REF(src)'.${NC}"
st=1
@@ -183,137 +188,142 @@ part "ensure proper lowertext usage"
# lowertext() is a BYOND-level proc, so it can be used in any sort of code... including the TGS DMAPI which we don't manage in this repository.
# basically, we filter out any results with "tgs" in it to account for this edgecase without having to enforce this rule in that separate codebase.
# grepping the grep results is a bit of a sad solution to this but it's pretty much the only option in our existing linter framework
-if $grep -i 'lowertext\(.+\)' $code_files | $grep -v 'UNLINT\(.+\)' | $grep -v '\/modules\/tgs\/'; then
+if $grep -i 'lowertext\(.+\)' "${code_files[@]}" | $grep -v 'UNLINT\(.+\)' | $grep -v '\/modules\/tgs\/'; then
echo
echo -e "${RED}ERROR: Found a lowertext() proc call. Please use the LOWER_TEXT() macro instead. If you know what you are doing, wrap your text (ensure it is a string) in UNLINT().${NC}"
st=1
fi;
part "balloon_alert sanity"
-if $grep 'balloon_alert\(".*"\)' $code_files; then
+if $grep 'balloon_alert\(".*"\)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Found a balloon alert with improper arguments.${NC}"
st=1
fi;
-if $grep 'balloon_alert(.*span_)' $code_files; then
+if $grep 'balloon_alert(.*span_)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Balloon alerts should never contain spans.${NC}"
st=1
fi;
part "balloon_alert idiomatic usage"
-if $grep 'balloon_alert\(.*?, ?"[A-Z]' $code_files; then
+if $grep 'balloon_alert\(.*?, ?"[A-Z]' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Balloon alerts should not start with capital letters. This includes text like 'AI'. If this is a false positive, wrap the text in UNLINT().${NC}"
st=1
fi;
part "update_icon_updates_onmob element usage"
-if $grep 'AddElement\(/datum/element/update_icon_updates_onmob.+ITEM_SLOT_HANDS' $code_files; then
+if $grep 'AddElement\(/datum/element/update_icon_updates_onmob.+ITEM_SLOT_HANDS' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: update_icon_updates_onmob element automatically updates ITEM_SLOT_HANDS, this is redundant and should be removed.${NC}"
st=1
fi;
part "forceMove sanity"
-if $grep 'forceMove\(\s*(\w+\(\)|\w+)\s*,\s*(\w+\(\)|\w+)\s*\)' $code_files; then
+if $grep 'forceMove\(\s*(\w+\(\)|\w+)\s*,\s*(\w+\(\)|\w+)\s*\)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: forceMove() call with two arguments - this is not how forceMove() is invoked! It's x.forceMove(y), not forceMove(x, y).${NC}"
st=1
fi;
part "as anything on typeless loops"
-if $grep 'var/[^/]+ as anything' $code_files; then
- echo
- echo -e "${RED}ERROR: 'as anything' used in a typeless for loop. This doesn't do anything and should be removed.${NC}"
- st=1
+if $grep 'var/[^/]+ as anything' "${code_files[@]}"; then
+ echo
+ echo -e "${RED}ERROR: 'as anything' used in a typeless for loop. This doesn't do anything and should be removed.${NC}"
+ st=1
fi;
part "as anything on internal functions"
-if $grep 'var\/(turf|mob|obj|atom\/movable).+ as anything in o?(view|range|hearers)\(' $code_files; then
- echo
- echo -e "${RED}ERROR: 'as anything' typed for loop over an internal function. These functions have some internal optimization that relies on the loop not having 'as anything' in it.${NC}"
- st=1
+if $grep 'var\/(turf|mob|obj|atom\/movable).+ as anything in o?(view|range|hearers)\(' "${code_files[@]}"; then
+ echo
+ echo -e "${RED}ERROR: 'as anything' typed for loop over an internal function. These functions have some internal optimization that relies on the loop not having 'as anything' in it.${NC}"
+ st=1
fi;
part "common spelling mistakes"
-if $grep -i 'centcomm' $code_files; then
+if $grep -i 'centcomm' "${code_files[@]}"; then
echo
- echo -e "${RED}ERROR: Misspelling(s) of CentCom detected in code, please remove the extra M(s).${NC}"
- st=1
+ echo -e "${RED}ERROR: Misspelling(s) of CentCom detected in code, please remove the extra M(s).${NC}"
+ st=1
fi;
-if $grep -ni 'nanotransen' $code_files; then
+if $grep -ni 'nanotransen' "${code_files[@]}"; then
echo
- echo -e "${RED}ERROR: Misspelling(s) of Nanotrasen detected in code, please remove the extra N(s).${NC}"
- st=1
+ echo -e "${RED}ERROR: Misspelling(s) of Nanotrasen detected in code, please remove the extra N(s).${NC}"
+ st=1
fi;
-if $grep 'NanoTrasen' $code_files; then
+if $grep 'NanoTrasen' "${code_files[@]}"; then
echo
- echo -e "${RED}ERROR: Misspelling(s) of Nanotrasen detected in code, please uncapitalize the T(s).${NC}"
- st=1
+ echo -e "${RED}ERROR: Misspelling(s) of Nanotrasen detected in code, please uncapitalize the T(s).${NC}"
+ st=1
fi;
-if $grep -i 'eciev' $code_files; then
+if $grep -i 'eciev' "${code_files[@]}"; then
echo
- echo -e "${RED}ERROR: Common I-before-E typo detected in code.${NC}"
- st=1
+ echo -e "${RED}ERROR: Common I-before-E typo detected in code.${NC}"
+ st=1
+fi;
+if $grep -i 'maintainance|maintainence|maintenence' "${code_files[@]}"; then
+ echo
+ echo -e "${RED}ERROR: Misspelling(s) of 'maintenance' detected in code, please fix.${NC}";
+ st=1
fi;
-if $grep -i 'maintainance|maintainence|maintenence' $code_files; then
+if $grep -i 'securaty|securiy|secuirty' "${code_files[@]}"; then
echo
- echo -e "${RED}ERROR: Misspelling(s) of 'maintenance' detected in code, please fix.${NC}";
+ echo -e "${RED}ERROR: Misspelling(s) of 'security' detected in code, please fix.${NC}";
st=1
fi;
part "map json naming"
if ls _maps/*.json | $grep "[A-Z]"; then
echo
- echo -e "${RED}ERROR: Uppercase in a map .JSON file detected, these must be all lowercase.${NC}"
- st=1
+ echo -e "${RED}ERROR: Uppercase in a map .JSON file detected, these must be all lowercase.${NC}"
+ st=1
fi;
part "Ineffective easing flags in animate()"
-if $grep 'easing\w*=\w*(EASE_IN|EASE_OUT|\(EASE_IN\w*\|\w*EASE_OUT\))' $code_files; then
- echo
- echo -e "${RED}ERROR: 'animate' was called with an easing argument and the default, LINEAR_EASING curve. This doesn't do anything and should be adjusted.${NC}"
- st=1
+if $grep 'easing\w*=\w*(EASE_IN|EASE_OUT|\(EASE_IN\w*\|\w*EASE_OUT\))' "${code_files[@]}"; then
+ echo
+ echo -e "${RED}ERROR: 'animate' was called with an easing argument and the default, LINEAR_EASING curve. This doesn't do anything and should be adjusted.${NC}"
+ st=1
fi;
part "map json sanity"
for json in _maps/*.json
do
- map_path=$(jq -r '.map_path' $json)
- while read map_file; do
- filename="_maps/$map_path/$map_file"
- if [ ! -f $filename ]
- then
+ map_path=$(jq -r '.map_path' $json)
+ while read map_file; do
+ filename="_maps/$map_path/$map_file"
+ if [ ! -f $filename ]
+ then
echo
- echo -e "${RED}ERROR: Found an invalid file reference to $filename in _maps/$json ${NC}"
- st=1
- fi
- done < <(jq -r '[.map_file] | flatten | .[]' $json)
+ echo -e "${RED}ERROR: Found an invalid file reference to $filename in _maps/$json ${NC}"
+ st=1
+ fi
+ done < <(jq -r '[.map_file] | flatten | .[]' $json)
done
part "updatepaths validity"
missing_txt_lines=$(find tools/UpdatePaths/Scripts -type f ! -name "*.txt" | wc -l)
if [ $missing_txt_lines -gt 0 ]; then
- echo
- echo -e "${RED}ERROR: Found an UpdatePaths File that doesn't end in .txt! Please add the proper file extension!${NC}"
- st=1
+ echo
+ echo -e "${RED}ERROR: Found an UpdatePaths File that doesn't end in .txt! Please add the proper file extension!${NC}"
+ st=1
fi;
number_prefix_lines=$(find tools/UpdatePaths/Scripts -type f | wc -l)
valid_number_prefix_lines=$(find tools/UpdatePaths/Scripts -type f | $grep -P "\d+_(.+)" | wc -l)
if [ $valid_number_prefix_lines -ne $number_prefix_lines ]; then
- echo
- echo -e "${RED}ERROR: Detected an UpdatePaths File that doesn't start with the PR number! Please add the proper number prefix!${NC}"
- st=1
+ echo
+ echo -e "${RED}ERROR: Detected an UpdatePaths File that doesn't start with the PR number! Please add the proper number prefix!${NC}"
+ st=1
fi;
section "515 Proc Syntax"
part "proc ref syntax"
if $grep '\.proc/' $code_x_515 ; then
- echo
- echo -e "${RED}ERROR: Outdated proc reference use detected in code, please use proc reference helpers.${NC}"
- st=1
+ echo
+ echo -e "${RED}ERROR: Outdated proc reference use detected in code, please use proc reference helpers.${NC}"
+ st=1
fi;
if [ "$pcre2_support" -eq 1 ]; then
@@ -325,37 +335,37 @@ if [ "$pcre2_support" -eq 1 ]; then
st=1
fi;
part "to_chat sanity"
- if $grep -P 'to_chat\((?!.*,).*\)' $code_files; then
+ if $grep -P 'to_chat\((?!.*,).*\)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: to_chat() missing arguments.${NC}"
st=1
fi;
part "timer flag sanity"
- if $grep -P 'addtimer\((?=.*TIMER_OVERRIDE)(?!.*TIMER_UNIQUE).*\)' $code_files; then
+ if $grep -P 'addtimer\((?=.*TIMER_OVERRIDE)(?!.*TIMER_UNIQUE).*\)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: TIMER_OVERRIDE used without TIMER_UNIQUE.${NC}"
st=1
fi
part "trailing newlines"
- if $grep -PU '[^\n]$(?!\n)' $code_files; then
+ if $grep -PU '[^\n]$(?!\n)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: File(s) with no trailing newline detected, please add one.${NC}"
st=1
fi
part "datum stockpart sanity"
- if $grep -P 'for\b.*/obj/item/stock_parts/(?!power_store)(?![\w_]+ in )' $code_files; then
+ if $grep -P 'for\b.*/obj/item/stock_parts/(?!power_store)(?![\w_]+ in )' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Should be using datum/stock_part instead"
st=1
fi;
part "improper atom initialize args"
- if $grep -P '^/(obj|mob|turf|area|atom)/.+/Initialize\((?!mapload).*\)' $code_files; then
+ if $grep -P '^/(obj|mob|turf|area|atom)/.+/Initialize\((?!mapload).*\)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Initialize override without 'mapload' argument.${NC}"
st=1
fi;
part "pronoun helper spellcheck"
- if $grep -P '%PRONOUN_(?!they|They|their|Their|theirs|Theirs|them|Them|have|are|were|do|theyve|Theyve|theyre|Theyre|s|es)' $code_files; then
+ if $grep -P '%PRONOUN_(?!they|They|their|Their|theirs|Theirs|them|Them|have|are|were|do|theyve|Theyve|theyre|Theyre|s|es)' "${code_files[@]}"; then
echo
echo -e "${RED}ERROR: Invalid pronoun helper found.${NC}"
st=1
@@ -372,13 +382,13 @@ else
fi
if [ $st = 0 ]; then
- echo
- echo -e "${GREEN}No errors found using $grep!${NC}"
+ echo
+ echo -e "${GREEN}No errors found using $grep!${NC}"
fi;
if [ $st = 1 ]; then
- echo
- echo -e "${RED}Errors found, please fix them and try again.${NC}"
+ echo
+ echo -e "${RED}Errors found, please fix them and try again.${NC}"
fi;
exit $st
diff --git a/tools/define_sanity/check.py b/tools/define_sanity/check.py
index 02b8c6cea729..a7449c7cf83c 100644
--- a/tools/define_sanity/check.py
+++ b/tools/define_sanity/check.py
@@ -4,7 +4,7 @@
import re
import sys
-parent_directory = "**/*.dm" # DARKPACK EDIT CHANGE - ORIGINAL: parent_directory = "code/**/*.dm"
+parent_directory = "**/*.dm" # DARKPACK EDIT CHANGE - ORIGINAL: parent_directory = "*/**/*.dm"
output_file_name = "define_sanity_output.txt"
how_to_fix_message = "Please #undef the above defines or remake them as global defines in the code/__DEFINES directory."
@@ -37,7 +37,8 @@ def post_error(define_name, file, github_error_style):
# TGS files come from another repository so lets not worry about them.
"code/modules/tgs/**/*.dm",
"tools/**/*.dm", ## DARKPACK EDIT ADD
- "DMCompiler_linux-x64/**/*.dm" ## DARKPACK EDIT ADD
+ # Doesn't come with the repo, but is in CI.
+ "DMCompiler_linux-x64/*.dm",
]
define_regex = re.compile(r"(\s+)?#define\s?([A-Z0-9_]+)\(?(.+)\)?")
diff --git a/tools/pull_request_hooks/rerunFlakyTests.js b/tools/pull_request_hooks/rerunFlakyTests.js
index 29fb5eeb9bf2..8170a0541bb0 100644
--- a/tools/pull_request_hooks/rerunFlakyTests.js
+++ b/tools/pull_request_hooks/rerunFlakyTests.js
@@ -277,8 +277,20 @@ export async function reportFlakyTests({ github, context }) {
);
if (existingIssueId !== undefined) {
- // Maybe in the future, if it's helpful, update the existing issue with new links
console.log(`Existing issue found: #${existingIssueId}`);
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: existingIssueId,
+ body: createBody(
+ details,
+ `https://github.com/${context.repo.owner}/${
+ context.repo.repo
+ }/actions/runs/${context.payload.workflow_run.id}/attempts/${
+ context.payload.workflow_run.run_attempt - 1
+ }`,
+ ),
+ });
return;
}
diff --git a/tools/ticked_file_enforcement/schemas/tgstation_dme.json b/tools/ticked_file_enforcement/schemas/tgstation_dme.json
index 7fe1840d7534..7a7e378b7741 100644
--- a/tools/ticked_file_enforcement/schemas/tgstation_dme.json
+++ b/tools/ticked_file_enforcement/schemas/tgstation_dme.json
@@ -1,10 +1,16 @@
{
"file": "tgstation.dme",
- "scannable_directory": "code/",
+ "scannable_directory": "*/",
"subdirectories": true,
"excluded_files": [],
"forbidden_includes": [
"code/modules/tgs/**/*.dm",
- "code/modules/unit_tests/[!_]*.dm"
+ "code/modules/unit_tests/[!_]*.dm",
+ "DMCompiler_linux-x64/*.dm",
+ "tools/ci/od_lints.dm",
+ "tools/HubMigrator/HubMigrator.dm",
+ "tools/CatchUnescapedBrackets/*.dm",
+ "tools/SS13SmoothingCutter/*.dm",
+ "tools/ScrollAnimationAssembler/*.dm"
]
}