diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ef2ce67760b5..ff41d6065506c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,27 +18,7 @@ "git.branchProtection": ["master"], "gitlens.advanced.blame.customArguments": ["-w"], "tgstationTestExplorer.project.resultsType": "json", - "[javascript]": { - "editor.rulers": [80], - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[javascriptreact]": { - "editor.rulers": [80], - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[typescript]": { - "editor.rulers": [80], - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[typescriptreact]": { - "editor.rulers": [80], - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[scss]": { + "[javascript][typescript][typescriptreact][javascriptreact][html][scss][css][json][jsonc][markdown][yaml]": { "editor.rulers": [80], "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true diff --git a/_maps/shuttles/emergency_nebula.dmm b/_maps/shuttles/emergency_nebula.dmm index 39359e5c29e9d..a0dd9202b5628 100644 --- a/_maps/shuttles/emergency_nebula.dmm +++ b/_maps/shuttles/emergency_nebula.dmm @@ -864,6 +864,7 @@ name = "Emergency Recovery Airlock" }, /obj/effect/turf_decal/delivery, +/obj/effect/mapping_helpers/airlock/access/all/command/general, /turf/open/floor/iron/dark/textured_large, /area/shuttle/escape) "sI" = ( diff --git a/code/__DEFINES/bodyparts.dm b/code/__DEFINES/bodyparts.dm index 19af74fe43db2..f044bb12c84ea 100644 --- a/code/__DEFINES/bodyparts.dm +++ b/code/__DEFINES/bodyparts.dm @@ -47,3 +47,5 @@ // Color priorities for bodyparts #define LIMB_COLOR_HULK 10 #define LIMB_COLOR_CARP_INFUSION 20 +/// Base priority for atom colors, gets atom priorities added to it +#define LIMB_COLOR_ATOM_COLOR 30 diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 6e9af2cdb9929..00398630852e2 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -468,6 +468,9 @@ GLOBAL_LIST_INIT(heretic_path_to_color, list( // Lowest priority #define EYE_COLOR_ORGAN_PRIORITY 1 +/// Base priority for atom colors, gets atom priorities added to it +#define EYE_COLOR_ATOM_COLOR_PRIORITY 2 #define EYE_COLOR_SPECIES_PRIORITY 10 #define EYE_COLOR_WEED_PRIORITY 20 -#define EYE_COLOR_CULT_PRIORITY 30 +#define EYE_COLOR_LUMINESCENT_PRIORITY 30 +#define EYE_COLOR_CULT_PRIORITY 40 diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm index f00240722181a..ead6717bcbe07 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm @@ -29,6 +29,7 @@ #define COMSIG_MOVABLE_IMPACT "movable_impact" ///from base of mob/living/hitby(): (mob/living/target, hit_zone, blocked, datum/thrownthing/throwingdatum) #define COMSIG_MOVABLE_IMPACT_ZONE "item_impact_zone" + #define MOVABLE_IMPACT_ZONE_OVERRIDE (1<<0) ///from /atom/movable/proc/buckle_mob(): (mob/living/M, force, check_loc, buckle_mob_flags) #define COMSIG_MOVABLE_PREBUCKLE "prebuckle" // this is the last chance to interrupt and block a buckle before it finishes #define COMPONENT_BLOCK_BUCKLE (1<<0) diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm index ee157b9d51a34..5d9ce528c652f 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm @@ -73,10 +73,6 @@ ///from /mob/living/carbon/doUnEquip(obj/item/I, force, newloc, no_move, invdrop, silent) #define COMSIG_CARBON_UNEQUIP_SHOECOVER "carbon_unequip_shoecover" #define COMSIG_CARBON_EQUIP_SHOECOVER "carbon_equip_shoecover" -///defined twice, in carbon and human's topics, fired when interacting with a valid embedded_object to pull it out (mob/living/carbon/target, /obj/item, /obj/item/bodypart/L) -#define COMSIG_CARBON_EMBED_RIP "item_embed_start_rip" -///called when removing a given item from a mob, from mob/living/carbon/remove_embedded_object(mob/living/carbon/target, /obj/item) -#define COMSIG_CARBON_EMBED_REMOVAL "item_embed_remove_safe" ///Called when someone attempts to cuff a carbon #define COMSIG_CARBON_CUFF_ATTEMPTED "carbon_attempt_cuff" #define COMSIG_CARBON_CUFF_PREVENT (1<<0) diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index a83badb9ee067..96ef802cc87f5 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -198,8 +198,6 @@ #define COMSIG_TOOL_START_USE "tool_start_use" /// From /obj/item/multitool/remove_buffer(): (buffer) #define COMSIG_MULTITOOL_REMOVE_BUFFER "multitool_remove_buffer" -///from [/obj/item/proc/disableEmbedding]: -#define COMSIG_ITEM_DISABLE_EMBED "item_disable_embed" ///from [/obj/effect/mine/proc/triggermine]: #define COMSIG_MINE_TRIGGERED "minegoboom" ///from [/obj/structure/closet/supplypod/proc/preOpen]: @@ -404,10 +402,7 @@ #define COMSIG_PROJECTILE_RANGE_OUT "projectile_range_out" ///from the base of /obj/projectile/process(): () #define COMSIG_PROJECTILE_BEFORE_MOVE "projectile_before_move" -///from [/obj/item/proc/tryEmbed] sent when trying to force an embed (mainly for projectiles and eating glass) -#define COMSIG_EMBED_TRY_FORCE "item_try_embed" - #define COMPONENT_EMBED_SUCCESS (1<<1) -// FROM [/obj/item/proc/updateEmbedding] sent when an item's embedding properties are changed : () +// FROM [/obj/item/proc/set_embed] sent when an item's embedding properties are changed : () #define COMSIG_ITEM_EMBEDDING_UPDATE "item_embedding_update" ///sent to targets during the process_hit proc of projectiles @@ -418,9 +413,9 @@ ///sent to the projectile after an item is spawned by the projectile_drop element: (new_item) #define COMSIG_PROJECTILE_ON_SPAWN_DROP "projectile_on_spawn_drop" -///sent to the projectile when spawning the item (shrapnel) that may be embedded: (new_item) +///sent to the projectile when spawning the item (shrapnel) that may be embedded: (new_item, victim) #define COMSIG_PROJECTILE_ON_SPAWN_EMBEDDED "projectile_on_spawn_embedded" -///sent to the projectile when successfully embedding into something +///sent to the projectile when successfully embedding into something: (new_item, victim) #define COMSIG_PROJECTILE_ON_EMBEDDED "projectile_on_embedded" // /obj/vehicle/sealed/car/vim signals @@ -477,12 +472,12 @@ #define COMSIG_ITEM_ATTACK_SECONDARY "item_attack_secondary" ///from base of [obj/item/attack()]: (atom/target, mob/user, proximity_flag, click_parameters) #define COMSIG_ITEM_AFTERATTACK "item_afterattack" -///from base of obj/item/embedded(): (atom/target, obj/item/bodypart/part) +///from base of datum/embedding/proc/embed_into(): (mob/living/carbon/victim, obj/item/bodypart/limb) #define COMSIG_ITEM_EMBEDDED "item_embedded" -///from base of datum/component/embedded/safeRemove(): (mob/living/carbon/victim) +///from base of datum/embedding/proc/remove_embedding(): (mob/living/carbon/victim, obj/item/bodypart/limb) #define COMSIG_ITEM_UNEMBEDDED "item_unembedded" -/// from base of obj/item/failedEmbed() -#define COMSIG_ITEM_FAILED_EMBED "item_failed_embed" +///from base of datum/embedding/proc/failed_embed(): (mob/living/carbon/victim, hit_zone) +#define COMSIG_ITEM_FAILED_EMBED "item_unembedded" /// from base of datum/element/disarm_attack/secondary_attack(), used to prevent shoving: (victim, user, send_message) #define COMSIG_ITEM_CAN_DISARM_ATTACK "item_pre_disarm_attack" @@ -501,9 +496,6 @@ #define COMSIG_SPEED_POTION_APPLIED "speed_potion" #define SPEED_POTION_STOP (1<<0) -/// from /obj/structure/sign/poster/trap_succeeded() : (mob/user) -#define COMSIG_POSTER_TRAP_SUCCEED "poster_trap_succeed" - /// from /obj/item/detective_scanner/scan(): (mob/user, list/extra_data) #define COMSIG_DETECTIVE_SCANNED "det_scanned" diff --git a/code/__DEFINES/lighting.dm b/code/__DEFINES/lighting.dm index a59d1b2c14449..cfaaa9aad981d 100644 --- a/code/__DEFINES/lighting.dm +++ b/code/__DEFINES/lighting.dm @@ -46,6 +46,7 @@ // These are a percentage of how much darkness to cut off (in rgb) #define LIGHTING_CUTOFF_VISIBLE 0 #define LIGHTING_CUTOFF_REAL_LOW 4.5 +#define LIGHTING_CUTOFF_LOW 10 #define LIGHTING_CUTOFF_MEDIUM 15 #define LIGHTING_CUTOFF_HIGH 30 #define LIGHTING_CUTOFF_FULLBRIGHT 100 diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm index c2f0999a34f8c..93245670782a7 100644 --- a/code/__DEFINES/logging.dm +++ b/code/__DEFINES/logging.dm @@ -129,6 +129,7 @@ #define LOG_CATEGORY_DEBUG_ASSET "debug-asset" #define LOG_CATEGORY_DEBUG_JOB "debug-job" #define LOG_CATEGORY_DEBUG_LUA "debug-lua" +#define LOG_CATEGORY_DEBUG_TTS "debug-tts" #define LOG_CATEGORY_DEBUG_MAPPING "debug-mapping" #define LOG_CATEGORY_DEBUG_MOBTAG "debug-mobtag" #define LOG_CATEGORY_DEBUG_SQL "debug-sql" diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index d7b0c852e5d21..60f77fbb7806f 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -867,6 +867,8 @@ GLOBAL_LIST_INIT(layers_to_offset, list( #define NOT_INSIDE_TARGET (1<<10) /// Checks for base adjacency, but silences the error #define SILENT_ADJACENCY (1<<11) +/// Allows pAIs to perform an action +#define ALLOW_PAI (1<<12) /// The default mob sprite size (used for shrinking or enlarging the mob sprite to regular size) #define RESIZE_DEFAULT_SIZE 1 diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index c1957c3581a79..8bea99554ac4d 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -330,6 +330,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_MESON_VISION "meson_vision" /// Gives us Night vision #define TRAIT_TRUE_NIGHT_VISION "true_night_vision" +/// Gives us minor night vision +#define TRAIT_MINOR_NIGHT_VISION "minor_night_vision" /// Negates our gravity, letting us move normally on floors in 0-g #define TRAIT_NEGATES_GRAVITY "negates_gravity" /// We are ignoring gravity diff --git a/code/__DEFINES/traits/sources.dm b/code/__DEFINES/traits/sources.dm index c4e952ed77b7a..fd70a6c72abb8 100644 --- a/code/__DEFINES/traits/sources.dm +++ b/code/__DEFINES/traits/sources.dm @@ -315,3 +315,6 @@ /// Trait aquired from being painted a certain color #define ATOM_COLOR_TRAIT "atom_color" + +/// Permanent trait from an overdose effect +#define OVERDOSE_TRAIT "overdose" diff --git a/code/__HELPERS/logging/debug.dm b/code/__HELPERS/logging/debug.dm index ad5670d2d119e..8a47ea2dc9bc6 100644 --- a/code/__HELPERS/logging/debug.dm +++ b/code/__HELPERS/logging/debug.dm @@ -15,6 +15,10 @@ /proc/log_job_debug(text, list/data) logger.Log(LOG_CATEGORY_DEBUG_JOB, text, data) +/// Logging for TTS +/proc/log_tts(text, list/data) + logger.Log(LOG_CATEGORY_DEBUG_TTS, text, data) + /// Logging for lua scripting /proc/log_lua(text, list/data) logger.Log(LOG_CATEGORY_DEBUG_LUA, text, data) diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 429322447584d..ef40eb60326dc 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -344,6 +344,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_MIND_READER" = TRAIT_MIND_READER, "TRAIT_MINDSHIELD" = TRAIT_MINDSHIELD, "TRAIT_MIND_TEMPORARILY_GONE" = TRAIT_MIND_TEMPORARILY_GONE, + "TRAIT_MINOR_NIGHT_VISION" = TRAIT_MINOR_NIGHT_VISION, "TRAIT_FAKE_SOULLESS" = TRAIT_FAKE_SOULLESS, "TRAIT_MOB_BREEDER" = TRAIT_MOB_BREEDER, "TRAIT_MOB_CAN_DIG" = TRAIT_MOB_CAN_DIG, diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm index 1ef7fccfbf150..3bf03e2673487 100644 --- a/code/_onclick/hud/alert.dm +++ b/code/_onclick/hud/alert.dm @@ -266,8 +266,7 @@ return var/mob/living/carbon/carbon_owner = owner - - return carbon_owner.help_shake_act(carbon_owner) + return carbon_owner.check_self_for_injuries() /atom/movable/screen/alert/negative name = "Negative Gravity" diff --git a/code/controllers/subsystem/persistence/trophy_fishes.dm b/code/controllers/subsystem/persistence/trophy_fishes.dm index 62fe8dfdfa090..e7e0b635a2b8a 100644 --- a/code/controllers/subsystem/persistence/trophy_fishes.dm +++ b/code/controllers/subsystem/persistence/trophy_fishes.dm @@ -32,10 +32,9 @@ fish.set_custom_materials(mat_list) fish.persistence_load(data) fish.name = data[PERSISTENCE_FISH_NAME] - mount.catcher_name = data[PERSISTENCE_FISH_CATCHER] - mount.catch_date = data[PERSISTENCE_FISH_CATCH_DATE] fish.set_status(FISH_DEAD, silent = TRUE) mount.add_fish(fish, from_persistence = TRUE, catcher = data[PERSISTENCE_FISH_CATCHER]) + mount.catch_date = data[PERSISTENCE_FISH_CATCH_DATE] /datum/controller/subsystem/persistence/proc/save_trophy_fish(obj/structure/fish_mount/mount) var/obj/item/fish/fish = mount.mounted_fish diff --git a/code/controllers/subsystem/polling.dm b/code/controllers/subsystem/polling.dm index 6624c984cbb6a..a68ff090a8d91 100644 --- a/code/controllers/subsystem/polling.dm +++ b/code/controllers/subsystem/polling.dm @@ -155,7 +155,7 @@ SUBSYSTEM_DEF(polling) act_never = "[custom_link_style_start]\[Never For This Round\]" if(!duplicate_message_check(alert_poll)) //Only notify people once. They'll notice if there are multiple and we don't want to spam people. - SEND_SOUND(candidate_mob, 'sound/announcer/notice/notice2.ogg') + SEND_SOUND(candidate_mob, sound('sound/misc/prompt.ogg', volume = 70)) var/surrounding_icon if(chat_text_border_icon) var/image/surrounding_image diff --git a/code/controllers/subsystem/tts.dm b/code/controllers/subsystem/tts.dm index 8d893f12f38f2..dc206522bca6d 100644 --- a/code/controllers/subsystem/tts.dm +++ b/code/controllers/subsystem/tts.dm @@ -183,6 +183,12 @@ SUBSYSTEM_DEF(tts) var/identifier = current_request.identifier if(current_request.requests_errored()) current_request.timed_out = TRUE + var/datum/http_response/normal_response = current_request.request.into_response() + var/datum/http_response/blips_response = current_request.request_blips.into_response() + log_tts("TTS HTTP request errored | Normal: [normal_response.error] | Blips: [blips_response.error]", list( + "normal" = normal_response, + "blips" = blips_response + )) continue current_request.audio_length = text2num(response.headers["audio-length"]) * 10 if(!current_request.audio_length) diff --git a/code/datums/actions/items/toggles.dm b/code/datums/actions/items/toggles.dm index d43eb0dcd3b90..70b201777cf91 100644 --- a/code/datums/actions/items/toggles.dm +++ b/code/datums/actions/items/toggles.dm @@ -116,6 +116,17 @@ /datum/action/item_action/call_link name = "Call MODlink" +/datum/action/item_action/toggle_wearable_hud + name = "Toggle Wearable HUD" + desc = "Toggles your wearable HUD. You can still access examine information while it's off." + +/datum/action/item_action/toggle_wearable_hud/Trigger(trigger_flags) + . = ..() + if(!.) + return + var/obj/item/clothing/glasses/hud/hud_display = target + hud_display.toggle_hud_display(owner) + /datum/action/item_action/toggle_nv name = "Toggle Night Vision" var/stored_cutoffs diff --git a/code/datums/ai_laws/ai_laws.dm b/code/datums/ai_laws/ai_laws.dm index 88c5650008870..b33609eb98930 100644 --- a/code/datums/ai_laws/ai_laws.dm +++ b/code/datums/ai_laws/ai_laws.dm @@ -79,8 +79,15 @@ GLOBAL_VAR(round_default_lawset) /proc/pick_weighted_lawset() var/datum/ai_laws/lawtype var/list/law_weights = CONFIG_GET(keyed_list/law_weight) + var/list/specified_law_ids = CONFIG_GET(keyed_list/specified_laws) + if(HAS_TRAIT(SSstation, STATION_TRAIT_UNIQUE_AI)) - law_weights -= AI_LAWS_ASIMOV + switch(CONFIG_GET(number/default_laws)) + if(CONFIG_ASIMOV) + law_weights -= AI_LAWS_ASIMOV + if(CONFIG_CUSTOM) + law_weights -= specified_law_ids + while(!lawtype && law_weights.len) var/possible_id = pick_weight(law_weights) lawtype = lawid_to_type(possible_id) diff --git a/code/datums/components/armor_plate.dm b/code/datums/components/armor_plate.dm index 9e495ada52fae..992b7bd973858 100644 --- a/code/datums/components/armor_plate.dm +++ b/code/datums/components/armor_plate.dm @@ -35,7 +35,7 @@ src.armor_mod = armor_mod if(upgrade_prefix) src.upgrade_prefix = upgrade_prefix - var/obj/item/typecast = upgrade_item + var/obj/item/typecast = src.upgrade_item src.upgrade_name = initial(typecast.name) /datum/component/armor_plate/proc/examine(datum/source, mob/user, list/examine_list) diff --git a/code/datums/components/dart_insert.dm b/code/datums/components/dart_insert.dm index 459da9d217cbe..42bf777b519a8 100644 --- a/code/datums/components/dart_insert.dm +++ b/code/datums/components/dart_insert.dm @@ -133,7 +133,7 @@ new_overlays += mutable_appearance(projectile_overlay_icon, projectile_overlay_icon_state) /datum/component/dart_insert/proc/apply_var_modifiers(obj/projectile/projectile) - var_modifiers = istype(modifier_getter) ? modifier_getter.Invoke() : list() + var_modifiers = istype(modifier_getter) ? modifier_getter.Invoke(projectile) : list() projectile.damage += var_modifiers["damage"] projectile.speed += var_modifiers["speed"] projectile.armour_penetration += var_modifiers["armour_penetration"] diff --git a/code/datums/components/embedded.dm b/code/datums/components/embedded.dm deleted file mode 100644 index f6ee85c23729d..0000000000000 --- a/code/datums/components/embedded.dm +++ /dev/null @@ -1,377 +0,0 @@ -/* - This component is responsible for handling individual instances of embedded objects. The embeddable element is what allows an item to be embeddable and stores its embedding stats, - and when it impacts and meets the requirements to stick into something, it instantiates an embedded component. Once the item falls out, the component is destroyed, while the - element survives to embed another day. - - - Carbon embedding has all the classical embedding behavior, and tracks more events and signals. The main behaviors and hooks to look for are: - -- Every process tick, there is a chance to randomly proc pain, controlled by pain_chance. There may also be a chance for the object to fall out randomly, per fall_chance - -- Every time the mob moves, there is a chance to proc jostling pain, controlled by jostle_chance (and only 50% as likely if the mob is walking or crawling) - -- Various signals hooking into carbon topic() and the embed removal surgery in order to handle removals. - - - In addition, there are 2 cases of embedding: embedding, and sticking - - - Embedding involves harmful and dangerous embeds, whether they cause brute damage, stamina damage, or a mix. This is the default behavior for embeddings, for when something is "pointy" - - - Sticking occurs when an item should not cause any harm while embedding (imagine throwing a sticky ball of tape at someone, rather than a shuriken). An item is considered "sticky" - when it has 0 for both pain multiplier and jostle pain multiplier. It's a bit arbitrary, but fairly straightforward. - - Stickables differ from embeds in the following ways: - -- Text descriptors use phrasing like "X is stuck to Y" rather than "X is embedded in Y" - -- There is no slicing sound on impact - -- All damage checks and bloodloss are skipped - -*/ - -/datum/component/embedded - dupe_mode = COMPONENT_DUPE_ALLOWED - var/obj/item/bodypart/limb - var/obj/item/weapon - ///if both our pain multiplier and jostle pain multiplier are 0, we're harmless and can omit most of the damage related stuff - var/harmful - -/datum/component/embedded/Initialize(obj/item/weapon, - datum/thrownthing/throwingdatum, - obj/item/bodypart/part) - - if(!iscarbon(parent) || !isitem(weapon)) - return COMPONENT_INCOMPATIBLE - - src.weapon = weapon - - if(part) - limb = part - - if(!weapon.is_embed_harmless()) - harmful = TRUE - - weapon.embedded(parent, part) - START_PROCESSING(SSdcs, src) - var/mob/living/carbon/victim = parent - var/datum/embed_data/embed_data = weapon.get_embed() - limb._embed_object(weapon) // on the inside... on the inside... - weapon.forceMove(victim) - RegisterSignals(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(weaponDeleted)) - victim.visible_message(span_danger("[weapon] [harmful ? "embeds" : "sticks"] itself [harmful ? "in" : "to"] [victim]'s [limb.plaintext_zone]!"), span_userdanger("[weapon] [harmful ? "embeds" : "sticks"] itself [harmful ? "in" : "to"] your [limb.plaintext_zone]!")) - - var/damage = weapon.throwforce - if(harmful) - victim.throw_alert(ALERT_EMBEDDED_OBJECT, /atom/movable/screen/alert/embeddedobject) - playsound(victim,'sound/items/weapons/bladeslice.ogg', 40) - if (limb.can_bleed()) - weapon.add_mob_blood(victim)//it embedded itself in you, of course it's bloody! - damage += weapon.w_class * embed_data.impact_pain_mult - victim.add_mood_event("embedded", /datum/mood_event/embedded) - - if(damage > 0) - var/armor = victim.run_armor_check(limb.body_zone, MELEE, "Your armor has protected your [limb.plaintext_zone].", "Your armor has softened a hit to your [limb.plaintext_zone].", weapon.armour_penetration, weak_against_armour = weapon.weak_against_armour) - victim.apply_damage( - damage = (1 - embed_data.pain_stam_pct) * damage, - damagetype = BRUTE, - def_zone = limb, - blocked = armor, - wound_bonus = weapon.wound_bonus, - bare_wound_bonus = weapon.bare_wound_bonus, - sharpness = weapon.get_sharpness(), - attacking_item = weapon, - ) - victim.apply_damage( - damage = embed_data.pain_stam_pct * damage, - damagetype = STAMINA, - ) - -/datum/component/embedded/Destroy() - var/mob/living/carbon/victim = parent - if(victim && !victim.has_embedded_objects()) - victim.clear_alert(ALERT_EMBEDDED_OBJECT) - victim.clear_mood_event("embedded") - if(weapon) - UnregisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING)) - weapon = null - limb = null - return ..() - -/datum/component/embedded/RegisterWithParent() - RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(jostleCheck)) - RegisterSignal(parent, COMSIG_CARBON_EMBED_RIP, PROC_REF(ripOut)) - RegisterSignal(parent, COMSIG_CARBON_EMBED_REMOVAL, PROC_REF(safeRemove)) - RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(checkTweeze)) - RegisterSignal(parent, COMSIG_MAGIC_RECALL, PROC_REF(magic_pull)) - RegisterSignal(parent, COMSIG_ATOM_EX_ACT, PROC_REF(on_ex_act)) - -/datum/component/embedded/UnregisterFromParent() - UnregisterSignal(parent, list(COMSIG_MOVABLE_MOVED, COMSIG_CARBON_EMBED_RIP, COMSIG_CARBON_EMBED_REMOVAL, COMSIG_ATOM_ATTACKBY, COMSIG_MAGIC_RECALL, COMSIG_ATOM_EX_ACT)) - -/datum/component/embedded/process(seconds_per_tick) - var/mob/living/carbon/victim = parent - - if(!victim || !limb) // in case the victim and/or their limbs exploded (say, due to a sticky bomb) - weapon.forceMove(get_turf(weapon)) - qdel(src) - return - - if(victim.stat == DEAD) - return - - var/datum/embed_data/embed_data = weapon.get_embed() - var/damage = weapon.w_class * embed_data.pain_mult - var/pain_chance_current = SPT_PROB_RATE(embed_data.pain_chance / 100, seconds_per_tick) * 100 - if(embed_data.pain_stam_pct && HAS_TRAIT_FROM(victim, TRAIT_INCAPACITATED, STAMINA)) //if it's a less-lethal embed, give them a break if they're already stamcritted - pain_chance_current *= 0.2 - damage *= 0.5 - else if(victim.body_position == LYING_DOWN) - pain_chance_current *= 0.2 - - if(harmful && prob(pain_chance_current)) - victim.apply_damage( - damage = (1 - embed_data.pain_stam_pct) * damage, - damagetype = BRUTE, - def_zone = limb, - wound_bonus = CANT_WOUND, - sharpness = weapon.get_sharpness(), - attacking_item = weapon, - ) - victim.apply_damage( - damage = embed_data.pain_stam_pct * damage, - damagetype = STAMINA, - ) - to_chat(victim, span_userdanger("[weapon] embedded in your [limb.plaintext_zone] hurts!")) - - var/fall_chance_current = SPT_PROB_RATE(embed_data.fall_chance / 100, seconds_per_tick) * 100 - if(victim.body_position == LYING_DOWN) - fall_chance_current *= 0.2 - - if(prob(fall_chance_current)) - fallOut() - -/datum/component/embedded/proc/on_ex_act(atom/source, severity) - SIGNAL_HANDLER - // In the process of parent's ex_act - if (QDELETED(weapon)) - return - switch(severity) - if(EXPLODE_DEVASTATE) - SSexplosions.high_mov_atom += weapon - if(EXPLODE_HEAVY) - SSexplosions.med_mov_atom += weapon - if(EXPLODE_LIGHT) - SSexplosions.low_mov_atom += weapon - -//////////////////////////////////////// -////////////BEHAVIOR PROCS////////////// -//////////////////////////////////////// - - -/// Called every time a carbon with a harmful embed moves, rolling a chance for the item to cause pain. The chance is halved if the carbon is crawling or walking. -/datum/component/embedded/proc/jostleCheck() - SIGNAL_HANDLER - - var/mob/living/carbon/victim = parent - var/datum/embed_data/embed_data = weapon.get_embed() - var/chance = embed_data.jostle_chance - if(victim.move_intent == MOVE_INTENT_WALK || victim.body_position == LYING_DOWN) - chance *= 0.5 - - if(harmful && prob(chance)) - var/damage = weapon.w_class * embed_data.jostle_pain_mult - victim.apply_damage( - damage = (1 - embed_data.pain_stam_pct) * damage, - damagetype = BRUTE, - def_zone = limb, - wound_bonus = CANT_WOUND, - sharpness = weapon.get_sharpness(), - attacking_item = weapon, - ) - victim.apply_damage( - damage = embed_data.pain_stam_pct * damage, - damagetype = STAMINA, - ) - to_chat(victim, span_userdanger("[weapon] embedded in your [limb.plaintext_zone] jostles and stings!")) - embed_data.jostle_callback?.Invoke(victim, weapon, embed_data) - - -/// Called when then item randomly falls out of a carbon. This handles the damage and descriptors, then calls safe_remove() -/datum/component/embedded/proc/fallOut() - var/mob/living/carbon/victim = parent - var/datum/embed_data/embed_data = weapon.get_embed() - - if(harmful) - var/damage = weapon.w_class * embed_data.remove_pain_mult - victim.apply_damage( - damage = (1 - embed_data.pain_stam_pct) * damage, - damagetype = BRUTE, - def_zone = limb, - wound_bonus = CANT_WOUND, - sharpness = weapon.get_sharpness(), - attacking_item = weapon, - ) - victim.apply_damage( - damage = embed_data.pain_stam_pct * damage, - damagetype = STAMINA, - ) - victim.visible_message(span_danger("[weapon] falls [harmful ? "out" : "off"] of [victim.name]'s [limb.plaintext_zone]!"), span_userdanger("[weapon] falls [harmful ? "out" : "off"] of your [limb.plaintext_zone]!")) - safeRemove() - - -/// Called when a carbon with an object embedded/stuck to them inspects themselves and clicks the appropriate link to begin ripping the item out. This handles the ripping attempt, descriptors, and dealing damage, then calls safe_remove() -/datum/component/embedded/proc/ripOut(datum/source, obj/item/I, obj/item/bodypart/limb) - SIGNAL_HANDLER - - if(I != weapon || src.limb != limb) - return - var/mob/living/carbon/victim = parent - var/datum/embed_data/embed_data = weapon.get_embed() - var/time_taken = embed_data.rip_time * weapon.w_class - INVOKE_ASYNC(src, PROC_REF(complete_rip_out), victim, I, limb, time_taken) - -/// everything async that ripOut used to do -/datum/component/embedded/proc/complete_rip_out(mob/living/carbon/victim, obj/item/I, obj/item/bodypart/limb, time_taken) - victim.visible_message(span_warning("[victim] attempts to remove [weapon] from [victim.p_their()] [limb.plaintext_zone]."),span_notice("You attempt to remove [weapon] from your [limb.plaintext_zone]... (It will take [DisplayTimeText(time_taken)])")) - if(!do_after(victim, time_taken, target = victim)) - return - if(!weapon || !limb || weapon.loc != victim || !(weapon in limb.embedded_objects)) - qdel(src) - return - if(harmful) - damaging_removal(victim, I, limb) - - victim.visible_message(span_notice("[victim] successfully rips [weapon] [harmful ? "out" : "off"] of [victim.p_their()] [limb.plaintext_zone]!"), span_notice("You successfully remove [weapon] from your [limb.plaintext_zone].")) - safeRemove(victim) - -/// Proc that actually does the damage associated with ripping something out of yourself. Call this before safeRemove. -/datum/component/embedded/proc/damaging_removal(mob/living/carbon/victim, obj/item/removed, obj/item/bodypart/limb, ouch_multiplier = 1) - var/datum/embed_data/embed_data = weapon.get_embed() - var/damage = weapon.w_class * embed_data.remove_pain_mult * ouch_multiplier - victim.apply_damage( - damage = (1 - embed_data.pain_stam_pct) * damage, - damagetype = BRUTE, - def_zone = limb, - wound_bonus = max(0, weapon.wound_bonus), // It hurts to rip it out, get surgery you dingus. unlike the others, this CAN wound + increase slash bloodflow - sharpness = weapon.get_sharpness() || SHARP_EDGED, // always sharp, even if the object isn't - attacking_item = weapon, - ) - victim.apply_damage( - damage = embed_data.pain_stam_pct * damage, - damagetype = STAMINA, - ) - victim.emote("scream") - -/// This proc handles the final step and actual removal of an embedded/stuck item from a carbon, whether or not it was actually removed safely. -/// If you want the thing to go into someone's hands rather than the floor, pass them in to_hands -/datum/component/embedded/proc/safeRemove(mob/to_hands) - SIGNAL_HANDLER - - var/mob/living/carbon/victim = parent - limb._unembed_object(weapon) - UnregisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING)) // have to do it here otherwise we trigger weaponDeleted() - - SEND_SIGNAL(weapon, COMSIG_ITEM_UNEMBEDDED, victim) - if(!weapon.unembedded()) // if it hasn't deleted itself due to drop del - UnregisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING)) - if(to_hands) - INVOKE_ASYNC(to_hands, TYPE_PROC_REF(/mob, put_in_hands), weapon) - else - weapon.forceMove(get_turf(victim)) - - qdel(src) - -/// Something deleted or moved our weapon while it was embedded, how rude! -/datum/component/embedded/proc/weaponDeleted() - SIGNAL_HANDLER - - var/mob/living/carbon/victim = parent - limb._unembed_object(weapon) - - if(victim) - to_chat(victim, span_userdanger("\The [weapon] that was embedded in your [limb.plaintext_zone] disappears!")) - - qdel(src) - -/// The signal for listening to see if someone is using a hemostat on us to pluck out this object -/datum/component/embedded/proc/checkTweeze(mob/living/carbon/victim, obj/item/possible_tweezers, mob/user) - SIGNAL_HANDLER - - if(!istype(victim) || (possible_tweezers.tool_behaviour != TOOL_HEMOSTAT && possible_tweezers.tool_behaviour != TOOL_WIRECUTTER) || user.zone_selected != limb.body_zone) - return - - if(weapon != limb.embedded_objects[1]) // just pluck the first one, since we can't easily coordinate with other embedded components affecting this limb who is highest priority - return - - if(ishuman(victim)) // check to see if the limb is actually exposed - var/mob/living/carbon/human/victim_human = victim - if(!victim_human.try_inject(user, limb.body_zone, INJECT_CHECK_IGNORE_SPECIES | INJECT_TRY_SHOW_ERROR_MESSAGE)) - return TRUE - - INVOKE_ASYNC(src, PROC_REF(tweezePluck), possible_tweezers, user) - return COMPONENT_NO_AFTERATTACK - -/// The actual action for pulling out an embedded object with a hemostat -/datum/component/embedded/proc/tweezePluck(obj/item/possible_tweezers, mob/user) - var/mob/living/carbon/victim = parent - var/datum/embed_data/embed_data = weapon.get_embed() - var/self_pluck = (user == victim) - // quality of the tool we're using - var/tweezer_speed = possible_tweezers.toolspeed - // is this an actual piece of medical equipment - var/tweezer_safe = (possible_tweezers.tool_behaviour == TOOL_HEMOSTAT) - var/pluck_time = embed_data.rip_time * (weapon.w_class * 0.3) * (self_pluck ? 1.5 : 1) * tweezer_speed * (tweezer_safe ? 1 : 1.5) - - if(self_pluck) - user.visible_message(span_danger("[user] begins plucking [weapon] from [user.p_their()] [limb.plaintext_zone] with [possible_tweezers]..."), span_notice("You start plucking [weapon] from your [limb.plaintext_zone] with [possible_tweezers]... (It will take [DisplayTimeText(pluck_time)])"),\ - vision_distance=COMBAT_MESSAGE_RANGE, ignored_mobs=victim) - else - user.visible_message(span_danger("[user] begins plucking [weapon] from [victim]'s [limb.plaintext_zone] with [possible_tweezers]..."),span_notice("You start plucking [weapon] from [victim]'s [limb.plaintext_zone] with [possible_tweezers]... (It will take [DisplayTimeText(pluck_time)])"), \ - vision_distance=COMBAT_MESSAGE_RANGE, ignored_mobs=victim) - to_chat(victim, span_userdanger("[user] begins plucking [weapon] from your [limb.plaintext_zone] with [possible_tweezers]... (It will take [DisplayTimeText(pluck_time)])")) - - if(!do_after(user, pluck_time, victim)) - if(self_pluck) - to_chat(user, span_danger("You fail to pluck [weapon] from your [limb.plaintext_zone].")) - else - to_chat(user, span_danger("You fail to pluck [weapon] from [victim]'s [limb.plaintext_zone].")) - to_chat(victim, span_danger("[user] fails to pluck [weapon] from your [limb.plaintext_zone].")) - return - - to_chat(user, span_notice("You successfully pluck [weapon] from [victim]'s [limb.plaintext_zone][tweezer_safe ? "." : ", but hurt [victim.p_them()] in the process."]")) - to_chat(victim, span_notice("[user] plucks [weapon] from your [limb.plaintext_zone][tweezer_safe ? "." : ", but it's not perfect."]")) - if(!tweezer_safe) - // sure it still hurts but it sucks less - damaging_removal(victim, weapon, limb, (0.4 * possible_tweezers.w_class)) - safeRemove(user) - -/// Called when an object is ripped out of someone's body by magic or other abnormal means -/datum/component/embedded/proc/magic_pull(datum/source, mob/living/caster, obj/marked_item) - SIGNAL_HANDLER - - if(marked_item != weapon) - return - - var/mob/living/carbon/victim = parent - - if(!harmful) - victim.visible_message(span_danger("[marked_item] vanishes from [victim.name]'s [limb.plaintext_zone]!"), span_userdanger("[weapon] vanishes from [limb.plaintext_zone]!")) - return - - var/datum/embed_data/embed_data = weapon.get_embed() - var/damage = weapon.w_class * embed_data.remove_pain_mult - victim.apply_damage( - damage = (1 - embed_data.pain_stam_pct) * damage * 1.5, - damagetype = BRUTE, - def_zone = limb, - wound_bonus = max(0, weapon.wound_bonus), // Performs exit wounds and flings the user to the caster if nearby - sharpness = weapon.get_sharpness() || SHARP_EDGED, - attacking_item = weapon, - ) - victim.apply_damage( - damage = embed_data.pain_stam_pct * damage, - damagetype = STAMINA, - ) - victim.cause_wound_of_type_and_severity(WOUND_PIERCE, limb, WOUND_SEVERITY_MODERATE) - playsound(victim, 'sound/effects/wounds/blood2.ogg', 50, TRUE) - - var/dist = get_dist(caster, victim) //Check if the caster is close enough to yank them in - if(dist < 7) - victim.throw_at(caster, get_dist(victim, caster) - 1, 1, caster) - victim.Paralyze(1 SECONDS) - victim.visible_message(span_alert("[victim] is sent flying towards [caster] as the [marked_item] tears out of them!"), span_alert("You are launched at [caster] as the [marked_item] tears from your body and towards their hand!")) - victim.visible_message(span_danger("[marked_item] is violently torn from [victim.name]'s [limb.plaintext_zone]!"), span_userdanger("[weapon] is violently torn from your [limb.plaintext_zone]!")) diff --git a/code/datums/components/ghostrole_on_revive.dm b/code/datums/components/ghostrole_on_revive.dm index d9638b2d46a6f..fd5ed5ae1e28a 100644 --- a/code/datums/components/ghostrole_on_revive.dm +++ b/code/datums/components/ghostrole_on_revive.dm @@ -3,13 +3,13 @@ /// If revived and no ghosts, just die again? var/refuse_revival_if_failed /// Callback for when the mob is revived and has their body occupied by a ghost - var/datum/callback/on_succesful_revive + var/datum/callback/on_successful_revive -/datum/component/ghostrole_on_revive/Initialize(refuse_revival_if_failed, on_succesful_revive) +/datum/component/ghostrole_on_revive/Initialize(refuse_revival_if_failed, on_successful_revive) . = ..() src.refuse_revival_if_failed = refuse_revival_if_failed - src.on_succesful_revive = on_succesful_revive + src.on_successful_revive = on_successful_revive ADD_TRAIT(parent, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) //for adding an alternate examination @@ -102,7 +102,7 @@ aliver.visible_message(span_deadsay("[aliver.name]'s soul is struggling to return!")) else aliver.key = chosen_one.key - on_succesful_revive?.Invoke(aliver) + on_successful_revive?.Invoke(aliver) qdel(src) /datum/component/ghostrole_on_revive/Destroy(force) diff --git a/code/datums/components/speechmod.dm b/code/datums/components/speechmod.dm index fc01d8d2d846c..b20d1b3249300 100644 --- a/code/datums/components/speechmod.dm +++ b/code/datums/components/speechmod.dm @@ -1,6 +1,7 @@ /// Used to apply certain speech patterns /// Can be used on organs, wearables, mutations and mobs /datum/component/speechmod + dupe_mode = COMPONENT_DUPE_ALLOWED /// Assoc list for strings/regexes and their replacements. Should be lowercase, as case will be automatically changed var/list/replacements = list() /// String added to the end of the message diff --git a/code/datums/components/tackle.dm b/code/datums/components/tackle.dm index 1d66c465f5562..0abfacb9a24d2 100644 --- a/code/datums/components/tackle.dm +++ b/code/datums/components/tackle.dm @@ -601,7 +601,7 @@ if(windscreen_casualty.type in list(/obj/structure/window, /obj/structure/window/fulltile, /obj/structure/window/unanchored, /obj/structure/window/fulltile/unanchored)) // boring unreinforced windows for(var/i in 1 to speed) var/obj/item/shard/shard = new /obj/item/shard(get_turf(user)) - shard.set_embed(/datum/embed_data/glass_candy) + shard.set_embed(/datum/embedding/glass_candy) user.hitby(shard, skipcatch = TRUE, hitpush = FALSE) shard.set_embed(initial(shard.embed_type)) windscreen_casualty.atom_destruction() diff --git a/code/datums/elements/caseless.dm b/code/datums/elements/caseless.dm index 9b1c06012079c..a07c994fe5c4a 100644 --- a/code/datums/elements/caseless.dm +++ b/code/datums/elements/caseless.dm @@ -19,12 +19,11 @@ /datum/element/caseless/proc/on_ready_projectile(obj/item/ammo_casing/shell, atom/target, mob/living/user, quiet, zone_override, atom/fired_from) SIGNAL_HANDLER var/obj/projectile/proj = shell.loaded_projectile - if(isnull(proj)) + if(isnull(proj) || !reusable) return - if(reusable) - if(!ispath(proj.shrapnel_type)) - proj.shrapnel_type = shell.type - proj.AddElement(/datum/element/projectile_drop, shell.type) + if(!ispath(proj.shrapnel_type)) + proj.shrapnel_type = shell.type + proj.AddElement(/datum/element/projectile_drop, shell.type) /datum/element/caseless/proc/on_fired_casing(obj/item/ammo_casing/shell, atom/target, mob/living/user, fired_from, randomspread, spread, zone_override, params, distro, obj/projectile/proj) SIGNAL_HANDLER diff --git a/code/datums/elements/decals/blood.dm b/code/datums/elements/decals/blood.dm index 2028f02353c95..b61d0d4b2e985 100644 --- a/code/datums/elements/decals/blood.dm +++ b/code/datums/elements/decals/blood.dm @@ -26,7 +26,7 @@ var/icon/icon_for_size = icon(icon, icon_state) var/scale_factor_x = icon_for_size.Width()/ICON_SIZE_X var/scale_factor_y = icon_for_size.Height()/ICON_SIZE_Y - var/mutable_appearance/blood_splatter = mutable_appearance('icons/effects/blood.dmi', "itemblood", appearance_flags = RESET_COLOR) //MA of the blood that we apply + var/mutable_appearance/blood_splatter = mutable_appearance('icons/effects/blood.dmi', "itemblood", appearance_flags = KEEP_APART|RESET_COLOR) //MA of the blood that we apply blood_splatter.transform = blood_splatter.transform.Scale(scale_factor_x, scale_factor_y) blood_splatter.blend_mode = BLEND_INSET_OVERLAY blood_splatter.color = _color diff --git a/code/datums/elements/embed.dm b/code/datums/elements/embed.dm deleted file mode 100644 index 90787f8581798..0000000000000 --- a/code/datums/elements/embed.dm +++ /dev/null @@ -1,183 +0,0 @@ -/* - The presence of this element allows an item (or a projectile carrying an item) to embed itself in a carbon when it is thrown into a target (whether by hand, gun, or explosive wave) with either - at least 4 throwspeed (EMBED_THROWSPEED_THRESHOLD) or ignore_throwspeed_threshold set to TRUE. Items meant to be used as shrapnel for projectiles should have ignore_throwspeed_threshold set to true. - - Whether we're dealing with a direct /obj/item (throwing a knife at someone) or an /obj/projectile with a shrapnel_type, how we handle things plays out the same, with one extra step separating them. - Items simply make their COMSIG_MOVABLE_IMPACT_ZONE check, while projectiles check on COMSIG_PROJECTILE_SELF_ON_HIT. - Upon a projectile hitting a valid target, it spawns whatever type of payload it has defined, then has that try to embed itself in the target on its own. - - Otherwise non-embeddable or stickable items can be made embeddable/stickable through wizard events/sticky tape/admin memes. -*/ - -/datum/element/embed - -/datum/element/embed/Attach(datum/target) - . = ..() - - if(!isitem(target) && !isprojectile(target)) - return ELEMENT_INCOMPATIBLE - - RegisterSignal(target, COMSIG_ELEMENT_ATTACH, PROC_REF(sever_element)) - if(isprojectile(target)) - RegisterSignal(target, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(check_embed_projectile)) - return - - RegisterSignal(target, COMSIG_MOVABLE_IMPACT_ZONE, PROC_REF(check_embed)) - RegisterSignal(target, COMSIG_ATOM_EXAMINE_TAGS, PROC_REF(examined_tags)) - RegisterSignal(target, COMSIG_EMBED_TRY_FORCE, PROC_REF(try_force_embed)) - RegisterSignal(target, COMSIG_ITEM_DISABLE_EMBED, PROC_REF(detach_from_weapon)) - -/datum/element/embed/Detach(obj/target) - . = ..() - if(isprojectile(target)) - UnregisterSignal(target, list(COMSIG_PROJECTILE_SELF_ON_HIT, COMSIG_ELEMENT_ATTACH)) - return - - UnregisterSignal(target, list(COMSIG_MOVABLE_IMPACT_ZONE, COMSIG_ELEMENT_ATTACH, COMSIG_MOVABLE_IMPACT, COMSIG_ATOM_EXAMINE, COMSIG_EMBED_TRY_FORCE, COMSIG_ITEM_DISABLE_EMBED)) - -/// Checking to see if we're gonna embed into a human -/datum/element/embed/proc/check_embed(obj/item/weapon, mob/living/carbon/victim, hit_zone, blocked, datum/thrownthing/throwingdatum, forced=FALSE) - SIGNAL_HANDLER - - if(forced) - embed_object(weapon, victim, hit_zone, throwingdatum) - return TRUE - - if(blocked || !istype(victim) || HAS_TRAIT(victim, TRAIT_PIERCEIMMUNE)) - return FALSE - - if(HAS_TRAIT(victim, TRAIT_GODMODE)) - return FALSE - - var/flying_speed = throwingdatum?.speed || weapon.throw_speed - - if(flying_speed < EMBED_THROWSPEED_THRESHOLD && !weapon.get_embed().ignore_throwspeed_threshold) - return FALSE - - if(!roll_embed_chance(weapon, victim, hit_zone, throwingdatum)) - return FALSE - - embed_object(weapon, victim, hit_zone, throwingdatum) - return TRUE - -/// Actually sticks the object to a victim -/datum/element/embed/proc/embed_object(obj/item/weapon, mob/living/carbon/victim, hit_zone, datum/thrownthing/throwingdatum) - var/obj/item/bodypart/limb = victim.get_bodypart(hit_zone) || pick(victim.bodyparts) - victim.AddComponent(/datum/component/embedded,\ - weapon,\ - throwingdatum,\ - part = limb) - -///A different embed element has been attached, so we'll detach and let them handle things -/datum/element/embed/proc/sever_element(obj/weapon, datum/element/E) - SIGNAL_HANDLER - - if(istype(E, /datum/element/embed)) - Detach(weapon) - -///If we don't want to be embeddable anymore (deactivating an e-dagger for instance) -/datum/element/embed/proc/detach_from_weapon(obj/weapon) - SIGNAL_HANDLER - - Detach(weapon) - -///Someone inspected our embeddable item -/datum/element/embed/proc/examined_tags(obj/item/I, mob/user, list/examine_list) - SIGNAL_HANDLER - - if(I.is_embed_harmless()) - examine_list["sticky"] = "[I] feels sticky, and could probably get stuck to someone if thrown properly!" - else - examine_list["embeddable"] = "[I] has a fine point, and could probably embed in someone if thrown properly!" - -/** - * check_embed_projectile() is what we get when a projectile with a defined shrapnel_type impacts a target. - * - * If we hit a valid target, we create the shrapnel_type object and then forcefully try to embed it on its - * behalf. DO NOT EVER add an embed element to the payload and let it do the rest. - * That's awful, and it'll limit us to drop-deletable shrapnels in the worry of stuff like - * arrows and harpoons being embeddable even when not let loose by their weapons. - */ -/datum/element/embed/proc/check_embed_projectile(obj/projectile/source, atom/movable/firer, atom/hit, angle, hit_zone, blocked, pierce_hit) - SIGNAL_HANDLER - - if (pierce_hit) - return - - if(!source.can_embed_into(hit) || blocked) - Detach(source) - return // we don't care - - var/payload_type = source.shrapnel_type - var/obj/item/payload = new payload_type(get_turf(hit)) - payload.set_embed(source.get_embed()) - if(istype(payload, /obj/item/shrapnel/bullet)) - payload.name = source.name - SEND_SIGNAL(source, COMSIG_PROJECTILE_ON_SPAWN_EMBEDDED, payload) - var/mob/living/carbon/C = hit - var/obj/item/bodypart/limb = C.get_bodypart(hit_zone) - if(!limb) - limb = C.get_bodypart() - - if(!try_force_embed(payload, limb)) - payload.failedEmbed() - else - SEND_SIGNAL(source, COMSIG_PROJECTILE_ON_EMBEDDED, payload, hit) - Detach(source) - -/** - * try_force_embed() is called here when we fire COMSIG_EMBED_TRY_FORCE from [/obj/item/proc/tryEmbed]. Mostly, this means we're a piece of shrapnel from a projectile that just impacted something, and we're trying to embed in it. - * - * The reason for this extra mucking about is avoiding having to do an extra hitby(), and annoying the target by impacting them once with the projectile, then again with the shrapnel, and possibly - * AGAIN if we actually embed. This way, we save on at least one message. - * - * Arguments: - * * embedding_item- the item we're trying to insert into the target - * * target- what we're trying to shish-kabob, either a bodypart or a carbon - * * hit_zone- if our target is a carbon, try to hit them in this zone, if we don't have one, pick a random one. If our target is a bodypart, we already know where we're hitting. - * * forced- if we want this to succeed 100% - */ -/datum/element/embed/proc/try_force_embed(obj/item/embedding_item, atom/target, hit_zone, forced=FALSE) - SIGNAL_HANDLER - - var/obj/item/bodypart/limb - var/mob/living/carbon/victim - - if(iscarbon(target)) - victim = target - if(!hit_zone) - limb = pick(victim.bodyparts) - hit_zone = limb.body_zone - else if(isbodypart(target)) - limb = target - hit_zone = limb.body_zone - victim = limb.owner - - if(!forced && !roll_embed_chance(embedding_item, victim, hit_zone)) - return - - return check_embed(embedding_item, victim, hit_zone, forced=TRUE) // Don't repeat the embed roll, we already did it - -/// Calculates the actual chance to embed based on armour penetration and throwing speed, then returns true if we pass that probability check -/datum/element/embed/proc/roll_embed_chance(obj/item/embedding_item, mob/living/victim, hit_zone, datum/thrownthing/throwingdatum) - var/actual_chance = embedding_item.get_embed().embed_chance - - if(throwingdatum?.speed > embedding_item.throw_speed) - actual_chance += (throwingdatum.speed - embedding_item.throw_speed) * EMBED_CHANCE_SPEED_BONUS - - if(embedding_item.is_embed_harmless()) // all the armor in the world won't save you from a kick me sign - return prob(actual_chance) - - var/armor = max(victim.run_armor_check(hit_zone, BULLET, silent=TRUE), victim.run_armor_check(hit_zone, BOMB, silent=TRUE)) * 0.5 // we'll be nice and take the better of bullet and bomb armor, halved - if(!armor) // we only care about armor penetration if there's actually armor to penetrate - return prob(actual_chance) - - //Keep this above 1, as it is a multiplier for the pen_mod for determining actual embed chance. - var/penetrative_behaviour = embedding_item.weak_against_armour ? ARMOR_WEAKENED_MULTIPLIER : 1 - var/pen_mod = -(armor * penetrative_behaviour) // if our shrapnel is weak into armor, then we restore our armor to the full value. - actual_chance += pen_mod // doing the armor pen as a separate calc just in case this ever gets expanded on - if(actual_chance <= 0) - victim.visible_message(span_danger("[embedding_item] bounces off [victim]'s armor, unable to embed!"), span_notice("[embedding_item] bounces off your armor, unable to embed!"), vision_distance = COMBAT_MESSAGE_RANGE) - return FALSE - - return prob(actual_chance) diff --git a/code/datums/elements/food/love_food_buff.dm b/code/datums/elements/food/love_food_buff.dm new file mode 100644 index 0000000000000..5f2951be131ac --- /dev/null +++ b/code/datums/elements/food/love_food_buff.dm @@ -0,0 +1,51 @@ +/// Changes a food item's food buff to something else when it has "love" reagent within +/datum/element/love_food_buff + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + /// Buff typepath to add when our food has love within + var/love_buff_type + +/datum/element/love_food_buff/Attach(datum/target, love_buff_type) + . = ..() + if(!istype(target, /obj/item/food)) + return ELEMENT_INCOMPATIBLE + var/obj/item/food/food = target + if(isnull(food.reagents)) + return ELEMENT_INCOMPATIBLE + + src.love_buff_type = love_buff_type + RegisterSignals(food.reagents, list( + COMSIG_REAGENTS_ADD_REAGENT, + COMSIG_REAGENTS_CLEAR_REAGENTS, + COMSIG_REAGENTS_DEL_REAGENT, + COMSIG_REAGENTS_NEW_REAGENT, + COMSIG_REAGENTS_REM_REAGENT, + ), PROC_REF(on_reagents_changed)) + RegisterSignal(food, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + +/datum/element/love_food_buff/Detach(datum/source, ...) + var/obj/item/food/food = source + if(istype(food) && !isnull(food.reagents)) + UnregisterSignal(food.reagents, list( + COMSIG_REAGENTS_ADD_REAGENT, + COMSIG_REAGENTS_CLEAR_REAGENTS, + COMSIG_REAGENTS_DEL_REAGENT, + COMSIG_REAGENTS_NEW_REAGENT, + COMSIG_REAGENTS_REM_REAGENT, + )) + UnregisterSignal(food, COMSIG_ATOM_EXAMINE) + return ..() + +/datum/element/love_food_buff/proc/on_reagents_changed(datum/reagents/source, ...) + SIGNAL_HANDLER + + var/obj/item/food/food = source.my_atom + if(!istype(food)) + return + + food.crafted_food_buff = source.has_reagent(/datum/reagent/love) ? love_buff_type : initial(food.crafted_food_buff) + +/datum/element/love_food_buff/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + examine_list += span_notice("Delivering a chef's kiss to [source] will alter [source.p_their()] effects.") diff --git a/code/datums/embed_data.dm b/code/datums/embed_data.dm deleted file mode 100644 index 865b285d09bab..0000000000000 --- a/code/datums/embed_data.dm +++ /dev/null @@ -1,58 +0,0 @@ -/// Assosciative list of type -> embed data. -GLOBAL_LIST_INIT(embed_by_type, generate_embed_type_cache()) - -/proc/generate_embed_type_cache() - var/list/embed_cache = list() - for(var/datum/embed_data/embed_type as anything in subtypesof(/datum/embed_data)) - var/datum/embed_data/embed = new embed_type - embed_cache[embed_type] = embed - return embed_cache - -/proc/get_embed_by_type(embed_type) - var/datum/embed_data/embed = GLOB.embed_by_type[embed_type] - if(embed) - return embed - CRASH("Attempted to get an embed type that did not exist! '[embed_type]'") - -/datum/embed_data - /// Chance for an object to embed into somebody when thrown - var/embed_chance = 45 - /// Chance for embedded object to fall out (causing pain but removing the object) - var/fall_chance = 5 - /// Chance for embedded objects to cause pain (damage user) - var/pain_chance = 15 - /// Coefficient of multiplication for the damage the item does while embedded (this*item.w_class) - var/pain_mult = 2 - /// Coefficient of multiplication for the damage the item does when it first embeds (this*item.w_class) - var/impact_pain_mult = 4 - /// Coefficient of multiplication for the damage the item does when it falls out or is removed without a surgery (this*item.w_class) - var/remove_pain_mult = 6 - /// Time in ticks, total removal time = (this*item.w_class) - var/rip_time = 30 - /// If this should ignore throw speed threshold of 4 - var/ignore_throwspeed_threshold = FALSE - /// Chance for embedded objects to cause pain every time they move (jostle) - var/jostle_chance = 5 - /// Coefficient of multiplication for the damage the item does while - var/jostle_pain_mult = 1 - /// Call this proc on jostling, if it exists! - var/datum/callback/jostle_callback - /// This percentage of all pain will be dealt as stam damage rather than brute (0-1) - var/pain_stam_pct = 0 - -/datum/embed_data/proc/generate_with_values(embed_chance, fall_chance, pain_chance, pain_mult, impact_pain_mult, remove_pain_mult, rip_time, ignore_throwspeed_threshold, jostle_chance, jostle_pain_mult, pain_stam_pct, force_new = FALSE) - var/datum/embed_data/data = isnull(GLOB.embed_by_type[type]) && !force_new ? src : new() - - data.embed_chance = !isnull(embed_chance) ? embed_chance : src.embed_chance - data.fall_chance = !isnull(fall_chance) ? fall_chance : src.fall_chance - data.pain_chance = !isnull(pain_chance) ? pain_chance : src.pain_chance - data.pain_mult = !isnull(pain_mult) ? pain_mult : src.pain_mult - data.impact_pain_mult = !isnull(impact_pain_mult) ? impact_pain_mult : src.impact_pain_mult - data.remove_pain_mult = !isnull(remove_pain_mult) ? remove_pain_mult : src.remove_pain_mult - data.rip_time = !isnull(rip_time) ? rip_time : src.rip_time - data.ignore_throwspeed_threshold = !isnull(ignore_throwspeed_threshold) ? ignore_throwspeed_threshold : src.ignore_throwspeed_threshold - data.jostle_chance = !isnull(jostle_chance) ? jostle_chance : src.jostle_chance - data.jostle_pain_mult = !isnull(jostle_pain_mult) ? jostle_pain_mult : src.jostle_pain_mult - data.jostle_callback = !isnull(jostle_callback) ? jostle_callback : src.jostle_callback - data.pain_stam_pct = !isnull(pain_stam_pct) ? pain_stam_pct : src.pain_stam_pct - return data diff --git a/code/datums/embedding.dm b/code/datums/embedding.dm new file mode 100644 index 0000000000000..a61d7aa6903b1 --- /dev/null +++ b/code/datums/embedding.dm @@ -0,0 +1,599 @@ +/// How quicker is it for someone else to rip out an item? +#define RIPPING_OUT_HELP_TIME_MULTIPLIER 0.75 +/// How much safer is it for someone else to rip out an item? +#define RIPPING_OUT_HELP_DAMAGE_MULTIPLIER 0.75 + +/* + * The magical embedding datum which is a container for all embedding interactions an item (or a projectile) can have. + * Whenever an item with an embedding datum is thrown into a carbon with either EMBED_THROWSPEED_THRESHOLD throwspeed or ignore_throwspeed_threshold set to TRUE, it will + * embed into them, with latter option reserved for sticky items and shrapnel. + * Whenever a projectile embeds, the datum is copied onto the shrapnel + */ + +/datum/embedding + /// Chance for an object to embed into somebody when thrown + var/embed_chance = 45 + /// Chance for embedded object to fall out (causing pain but removing the object) + var/fall_chance = 5 + /// Chance for embedded objects to cause pain (damage user) + var/pain_chance = 15 + /// Coefficient of multiplication for the damage the item does while embedded (this*item.w_class) + var/pain_mult = 2 + /// Coefficient of multiplication for the damage the item does when it first embeds (this*item.w_class) + var/impact_pain_mult = 4 + /// Coefficient of multiplication for the damage the item does when it falls out or is removed without a surgery (this*item.w_class) + var/remove_pain_mult = 6 + /// Time in ticks, total removal time = (this*item.w_class) + var/rip_time = 3 SECONDS + /// If this should ignore throw speed threshold of 4 + var/ignore_throwspeed_threshold = FALSE + /// Chance for embedded objects to cause pain every time they move (jostle) + var/jostle_chance = 5 + /// Coefficient of multiplication for the damage the item does while + var/jostle_pain_mult = 1 + /// This percentage of all pain will be dealt as stam damage rather than brute (0-1) + var/pain_stam_pct = 0 + /// Traits which make target immune to us embedding into them, any trait from the list works + var/list/immune_traits = list(TRAIT_PIERCEIMMUNE) + + /// Thing that we're attached to + VAR_FINAL/obj/item/parent + /// Mob we've embedded into, if any + VAR_FINAL/mob/living/carbon/owner + /// Limb we've embedded into in whose contents we reside + VAR_FINAL/obj/item/bodypart/owner_limb + +/datum/embedding/New(obj/item/creator) + . = ..() + if (creator) + register_on(creator) + +/// Registers ourselves with an item +/datum/embedding/proc/register_on(obj/item/new_parent) + if(!isitem(new_parent)) + CRASH("Embedding datum attempted to register on a non-item object [new_parent] ([new_parent?.type])") + + parent = new_parent + RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(on_qdel)) + + RegisterSignal(parent, COMSIG_MOVABLE_IMPACT_ZONE, PROC_REF(try_embed)) + RegisterSignal(parent, COMSIG_ATOM_EXAMINE_TAGS, PROC_REF(examined_tags)) + +/datum/embedding/Destroy(force) + if (!parent) + return ..() + parent.set_embed(null) + UnregisterSignal(parent, list(COMSIG_QDELETING, COMSIG_MOVABLE_IMPACT_ZONE, COMSIG_ATOM_EXAMINE)) + owner = null + owner_limb = null + parent = null + return ..() + +/// Creates a copy and sets all of its *relevant* variables +/// Children should override this with new variables if they add any "generic" ones +/datum/embedding/proc/create_copy(atom/movable/new_owner) + var/datum/embedding/brother = new type(new_owner) + brother.embed_chance = embed_chance + brother.fall_chance = fall_chance + brother.pain_chance = pain_chance + brother.pain_mult = pain_mult + brother.impact_pain_mult = impact_pain_mult + brother.remove_pain_mult = remove_pain_mult + brother.rip_time = rip_time + brother.ignore_throwspeed_threshold = ignore_throwspeed_threshold + brother.jostle_chance = jostle_chance + brother.jostle_pain_mult = jostle_pain_mult + brother.pain_stam_pct = pain_stam_pct + brother.immune_traits = immune_traits.Copy() + return brother + +///Someone inspected our embeddable item +/datum/embedding/proc/examined_tags(obj/item/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + if(is_harmless()) + examine_list["sticky"] = "[parent] looks sticky, and could probably get stuck to someone if thrown properly!" + else + examine_list["embeddable"] = "[parent] has a fine point, and could probably embed in someone if thrown properly!" + +/// Is passed victim a valid target for us to embed into? +/datum/embedding/proc/can_embed(atom/movable/source, mob/living/carbon/victim, hit_zone, datum/thrownthing/throwingdatum) + if (!istype(victim)) + return FALSE + + if (HAS_TRAIT(victim, TRAIT_GODMODE)) + return + + if (immune_traits) + for (var/immunity_trait in immune_traits) + if (HAS_TRAIT(victim, immunity_trait)) + return FALSE + + if (isitem(source)) + var/flying_speed = throwingdatum?.speed || source.throw_speed + if(flying_speed < EMBED_THROWSPEED_THRESHOLD && !ignore_throwspeed_threshold) + return FALSE + + return TRUE + +/// Attempts to embed an object +/datum/embedding/proc/try_embed(obj/item/weapon, mob/living/carbon/victim, hit_zone, blocked, datum/thrownthing/throwingdatum) + SIGNAL_HANDLER + + if (blocked || !can_embed(parent, victim, hit_zone, throwingdatum)) + failed_embed(victim, hit_zone) + return + + if (!roll_embed_chance(victim, hit_zone, throwingdatum)) + failed_embed(victim, hit_zone, random = TRUE) + return + + var/obj/item/bodypart/limb = victim.get_bodypart(hit_zone) || victim.bodyparts[1] + embed_into(victim, limb) + return MOVABLE_IMPACT_ZONE_OVERRIDE + +/// Attempts to embed shrapnel from a projectile +/datum/embedding/proc/try_embed_projectile(obj/projectile/source, atom/hit, hit_zone, blocked, pierce_hit) + if (pierce_hit) + return + + if (blocked || !can_embed(source, hit)) + failed_embed(hit, hit_zone) + return + + var/mob/living/carbon/victim = hit + var/shrapnel_type = source.shrapnel_type + var/obj/item/payload = new shrapnel_type(get_turf(victim)) + setup_shrapnel(payload, source, victim) + + if (!roll_embed_chance(victim, hit_zone)) + failed_embed(victim, hit_zone, random = TRUE) + return + + var/obj/item/bodypart/limb = victim.get_bodypart(hit_zone) || victim.bodyparts[1] + embed_into(victim, limb) + SEND_SIGNAL(source, COMSIG_PROJECTILE_ON_EMBEDDED, payload, hit) + +/// Used for custom logic while setting up shrapnel payload +/datum/embedding/proc/setup_shrapnel(obj/item/payload, obj/projectile/source, mob/living/carbon/victim) + // Detach from parent, we don't want em to delete us + source.set_embed(null, dont_delete = TRUE) + // Hook signals up first, as payload sends a comsig upon embed update + register_on(payload) + payload.set_embed(src) + if(istype(payload, /obj/item/shrapnel/bullet)) + payload.name = source.name + SEND_SIGNAL(source, COMSIG_PROJECTILE_ON_SPAWN_EMBEDDED, payload, victim) + +/// Calculates the actual chance to embed based on armour penetration and throwing speed, then returns true if we pass that probability check +/datum/embedding/proc/roll_embed_chance(mob/living/carbon/victim, hit_zone, datum/thrownthing/throwingdatum) + var/chance = embed_chance + + // Something threw us really, really fast + if (throwingdatum?.speed > parent.throw_speed) + chance += (throwingdatum.speed - parent.throw_speed) * EMBED_CHANCE_SPEED_BONUS + + if (is_harmless()) + return prob(embed_chance) + + // We'll be nice and take the better of bullet and bomb armor, halved + var/armor = max(victim.run_armor_check(hit_zone, BULLET, armour_penetration = parent.armour_penetration, silent = TRUE), victim.run_armor_check(hit_zone, BOMB, armour_penetration = parent.armour_penetration, silent = TRUE)) * 0.5 + // We only care about armor penetration if there's actually armor to penetrate + if(!armor) + return prob(chance) + + if (parent.weak_against_armour) + armor *= ARMOR_WEAKENED_MULTIPLIER + + chance -= armor + if (chance < 0) + victim.visible_message(span_danger("[parent] bounces off [victim]'s armor, unable to embed!"), + span_notice("[parent] bounces off your armor, unable to embed!"), vision_distance = COMBAT_MESSAGE_RANGE) + return FALSE + + return prob(chance) + +/// We've tried to embed into something and failed +/// Random being TRUE means we've lost the roulette, FALSE means we've either been blocked or the target is invalid +/datum/embedding/proc/failed_embed(mob/living/carbon/victim, hit_zone, random = FALSE) + if (!istype(parent)) + return + SEND_SIGNAL(parent, COMSIG_ITEM_FAILED_EMBED, victim, hit_zone) + if((parent.item_flags & DROPDEL) && !QDELETED(parent)) + qdel(parent) + +/// Does this item deal any damage when embedding or jostling inside of someone? +/datum/embedding/proc/is_harmless() + return pain_mult == 0 && jostle_pain_mult == 0 + +//Handles actual embedding logic. +/datum/embedding/proc/embed_into(mob/living/carbon/victim, obj/item/bodypart/target_limb) + SHOULD_NOT_OVERRIDE(TRUE) + + set_owner(victim, target_limb) + + START_PROCESSING(SSprocessing, src) + owner_limb._embed_object(parent) + parent.forceMove(owner) + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(weapon_disappeared)) + RegisterSignal(parent, COMSIG_MAGIC_RECALL, PROC_REF(magic_pull)) + owner.visible_message(span_danger("[parent] [is_harmless() ? "sticks itself to" : "embeds itself in"] [owner]'s [owner_limb.plaintext_zone]!"), + span_userdanger("[parent] [is_harmless() ? "sticks itself to" : "embeds itself in"] your [owner_limb.plaintext_zone]!")) + + var/damage = parent.throwforce + if (!is_harmless()) + owner.throw_alert(ALERT_EMBEDDED_OBJECT, /atom/movable/screen/alert/embeddedobject) + playsound(owner,'sound/items/weapons/bladeslice.ogg', 40) + if (owner_limb.can_bleed()) + parent.add_mob_blood(owner) // it embedded itself in you, of course it's bloody! + damage += parent.w_class * impact_pain_mult + owner.add_mood_event("embedded", /datum/mood_event/embedded) + + SEND_SIGNAL(parent, COMSIG_ITEM_EMBEDDED, victim, target_limb) + on_successful_embed(victim, target_limb) + + if (damage <= 0) + return TRUE + + var/armor = owner.run_armor_check(owner_limb.body_zone, MELEE, "Your armor has protected your [owner_limb.plaintext_zone].", + "Your armor has softened a hit to your [owner_limb.plaintext_zone].", parent.armour_penetration, + weak_against_armour = parent.weak_against_armour, + ) + + owner.apply_damage( + damage = (1 - pain_stam_pct) * damage, + damagetype = BRUTE, + def_zone = owner_limb.body_zone, + blocked = armor, + wound_bonus = parent.wound_bonus, + bare_wound_bonus = parent.bare_wound_bonus, + sharpness = parent.get_sharpness(), + attacking_item = parent, + ) + + owner.apply_damage( + damage = pain_stam_pct * damage, + damagetype = STAMINA, + ) + return TRUE + +/// Proc which is called upon successfully embedding into someone/something, for children to override +/datum/embedding/proc/on_successful_embed(mob/living/carbon/victim, obj/item/bodypart/target_limb) + return + +/// Registers signals that our owner should have +/// Handles jostling, tweezing embedded items out and grenade chain reactions +/datum/embedding/proc/set_owner(mob/living/carbon/victim, obj/item/bodypart/target_limb) + owner = victim + owner_limb = target_limb + RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(owner_moved)) + RegisterSignal(owner, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby)) + RegisterSignal(owner, COMSIG_ATOM_EX_ACT, PROC_REF(on_ex_act)) + RegisterSignal(owner_limb, COMSIG_BODYPART_REMOVED, PROC_REF(on_removed)) + +/// Avoid calling this directly as this doesn't move the object from its owner's contents +/// Returns TRUE if the item got deleted due to DROPDEL flag +/datum/embedding/proc/stop_embedding() + if (owner_limb) + UnregisterSignal(owner_limb, COMSIG_BODYPART_REMOVED) + owner_limb._unembed_object(parent) + if (owner) + UnregisterSignal(owner, list(COMSIG_MOVABLE_MOVED, COMSIG_ATOM_ATTACKBY, COMSIG_ATOM_EX_ACT)) + if (!owner.has_embedded_objects()) + owner.clear_alert(ALERT_EMBEDDED_OBJECT) + owner.clear_mood_event("embedded") + UnregisterSignal(parent, list(COMSIG_MOVABLE_MOVED, COMSIG_MAGIC_RECALL)) + SEND_SIGNAL(parent, COMSIG_ITEM_UNEMBEDDED, owner, owner_limb) + owner = null + owner_limb = null + if((parent.item_flags & DROPDEL) && !QDELETED(parent)) + qdel(parent) + return TRUE + return FALSE + +/datum/embedding/proc/on_qdel(atom/movable/source) + SIGNAL_HANDLER + if (owner_limb) + weapon_disappeared() + qdel(src) + +/// Move self to owner's turf when our limb gets removed +/datum/embedding/proc/on_removed(datum/source, mob/living/carbon/old_owner) + SIGNAL_HANDLER + stop_embedding() + parent.forceMove(old_owner.drop_location()) + +/// Someone attempted to pull us out! Either the owner by inspecting themselves, or someone else by examining the owner and clicking the link. +/datum/embedding/proc/rip_out(mob/living/jack_the_ripper) + if (!jack_the_ripper.CanReach(owner)) + return + + if (!jack_the_ripper.can_perform_action(owner, FORBID_TELEKINESIS_REACH | NEED_HANDS | ALLOW_RESTING)) + return + + var/time_taken = rip_time * parent.w_class + var/damage_mult = 1 + if (jack_the_ripper != owner) + time_taken *= RIPPING_OUT_HELP_TIME_MULTIPLIER + damage_mult *= RIPPING_OUT_HELP_DAMAGE_MULTIPLIER + owner.visible_message(span_warning("[jack_the_ripper] attempts to remove [parent] from [owner]'s [owner_limb.plaintext_zone]!"), + span_userdanger("[jack_the_ripper] attempt to remove [parent] from your [owner_limb.plaintext_zone]!"), ignored_mobs = jack_the_ripper) + to_chat(jack_the_ripper, span_notice("You attempt to remove [parent] from [owner]'s [owner_limb.plaintext_zone]...")) + else + owner.visible_message(span_warning("[owner] attempts to remove [parent] from [owner.p_their()] [owner_limb.plaintext_zone]."), + span_notice("You attempt to remove [parent] from your [owner_limb.plaintext_zone]...")) + + if (!do_after(jack_the_ripper, time_taken, owner, extra_checks = CALLBACK(src, PROC_REF(still_in)))) + return + + if (parent.loc != owner || !(parent in owner_limb?.embedded_objects)) + return + + if (jack_the_ripper == owner) + owner.visible_message(span_notice("[owner] successfully rips [parent] [is_harmless() ? "off" : "out"] of [owner.p_their()] [owner_limb.plaintext_zone]!"), + span_notice("You successfully remove [parent] from your [owner_limb.plaintext_zone].")) + else + owner.visible_message(span_notice("[jack_the_ripper] successfully rips [parent] [is_harmless() ? "off" : "out"] of [owner]'s [owner_limb.plaintext_zone]!"), + span_userdanger("[jack_the_ripper] removes [parent] from your [owner_limb.plaintext_zone]!"), ignored_mobs = jack_the_ripper) + to_chat(jack_the_ripper, span_notice("You successfully remove [parent] from [owner]'s [owner_limb.plaintext_zone].")) + + if (!is_harmless()) + damaging_removal_effect(damage_mult) + remove_embedding(jack_the_ripper) + +/// Handles damage effects upon forceful removal +/datum/embedding/proc/damaging_removal_effect(ouchies_multiplier) + var/damage = parent.w_class * remove_pain_mult * ouchies_multiplier + owner.apply_damage( + damage = (1 - pain_stam_pct) * damage, + damagetype = BRUTE, + def_zone = owner_limb, + wound_bonus = max(0, parent.wound_bonus), // It hurts to rip it out, get surgery you dingus. unlike the others, this CAN wound + increase slash bloodflow + sharpness = parent.get_sharpness() || SHARP_EDGED, // always sharp, even if the object isn't + attacking_item = parent, + ) + + owner.apply_damage( + damage = pain_stam_pct * damage, + damagetype = STAMINA, + ) + + owner.emote("scream") + +/// The proper proc to call when you want to remove something. If a mob is passed, the item will be put in its hands - otherwise its just dumped onto the ground +/datum/embedding/proc/remove_embedding(mob/living/to_hands) + var/mob/living/carbon/stored_owner = owner + if (stop_embedding()) // Dropdel? + return + parent.forceMove(stored_owner.drop_location()) + if (!isnull(to_hands)) + to_hands.put_in_hands(parent) + +/// When owner moves around, attempt to jostle the item +/datum/embedding/proc/owner_moved(mob/living/carbon/source, atom/old_loc, dir, forced, list/old_locs) + SIGNAL_HANDLER + + var/chance = jostle_chance + if(!forced && (owner.move_intent == MOVE_INTENT_WALK || owner.body_position == LYING_DOWN) && !CHECK_MOVE_LOOP_FLAGS(source, MOVEMENT_LOOP_OUTSIDE_CONTROL)) + chance *= 0.5 + + if(is_harmless() || !prob(chance)) + return + + var/damage = parent.w_class * jostle_pain_mult + owner.apply_damage( + damage = (1 - pain_stam_pct) * damage, + damagetype = BRUTE, + def_zone = owner_limb, + wound_bonus = CANT_WOUND, + sharpness = parent.get_sharpness(), + attacking_item = parent, + ) + + owner.apply_damage( + damage = pain_stam_pct * damage, + damagetype = STAMINA, + ) + + to_chat(owner, span_userdanger("[parent] embedded in your [owner_limb.plaintext_zone] jostles and stings!")) + jostle_effects() + +/// Effects which should occur when the owner moves, sometimes +/datum/embedding/proc/jostle_effects() + return + +/// When someone attempts to pluck us with tweezers or wirecutters +/datum/embedding/proc/on_attackby(mob/living/carbon/victim, obj/item/tool, mob/user) + SIGNAL_HANDLER + + if (user.zone_selected != owner_limb.body_zone || (tool.tool_behaviour != TOOL_HEMOSTAT && tool.tool_behaviour != TOOL_WIRECUTTER)) + return + + if (parent != owner_limb.embedded_objects[1]) // Don't pluck everything at the same time + return + + // Ensure that we can actually + if (!owner.try_inject(user, owner_limb.body_zone, INJECT_CHECK_IGNORE_SPECIES | INJECT_TRY_SHOW_ERROR_MESSAGE)) + return COMPONENT_NO_AFTERATTACK + + INVOKE_ASYNC(src, PROC_REF(try_pluck), tool, user) + return COMPONENT_NO_AFTERATTACK + +/datum/embedding/process(seconds_per_tick) + if (!owner || !owner_limb || owner_limb.owner != owner) + stack_trace("Attempted to process embedding on [parent] ([parent.type]) without an owner, owner_limb or owner-less limb!") + parent.forceMove(get_turf(parent)) + return + + if (owner.stat == DEAD) + return + + var/fall_chance_current = SPT_PROB_RATE(fall_chance / 100, seconds_per_tick) * 100 + if(owner.body_position == LYING_DOWN) + fall_chance_current *= 0.2 + + if(prob(fall_chance_current)) + fall_out() + return + + var/damage = parent.w_class * pain_mult + var/pain_chance_current = SPT_PROB_RATE(pain_chance / 100, seconds_per_tick) * 100 + if(pain_stam_pct && HAS_TRAIT_FROM(owner, TRAIT_INCAPACITATED, STAMINA)) //if it's a less-lethal embed, give them a break if they're already stamcritted + pain_chance_current *= 0.2 + damage *= 0.5 + else if(owner.body_position == LYING_DOWN) + pain_chance_current *= 0.2 + + if (is_harmless() || !prob(pain_chance_current)) + return + + owner.apply_damage( + damage = (1 - pain_stam_pct) * damage, + damagetype = BRUTE, + def_zone = owner_limb, + wound_bonus = CANT_WOUND, + sharpness = parent.get_sharpness(), + attacking_item = parent, + ) + + owner.apply_damage( + damage = pain_stam_pct * damage, + damagetype = STAMINA, + ) + + to_chat(owner, span_userdanger("[parent] embedded in your [owner_limb.plaintext_zone] hurts!")) + +/// Attempt to pluck out the embedded item using tweezers of some kind +/datum/embedding/proc/try_pluck(obj/item/tool, mob/user) + var/pluck_time = rip_time * (parent.w_class * 0.3) * tool.toolspeed + var/self_pluck = (user == owner) + var/safe_pluck = tool.tool_behaviour != TOOL_HEMOSTAT + // Don't harm ourselves if we're just stuck + if (is_harmless()) + safe_pluck = TRUE + if (self_pluck) + pluck_time *= 1.5 + // Wirecutters are harder to use for this + if (safe_pluck) + pluck_time *= 1.5 + + if (self_pluck) + owner.visible_message(span_danger("[owner] begins plucking [parent] from [owner.p_their()] [owner_limb.plaintext_zone] with [tool]..."), + span_notice("You start plucking [parent] from your [owner_limb.plaintext_zone] with [tool]..."), visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE) + else + user.visible_message(span_danger("[user] begins plucking [parent] from [owner]'s [owner_limb.plaintext_zone] with [tool]..."), + span_notice("You start plucking [parent] from [owner]'s [owner_limb.plaintext_zone] with [tool]..."), ignored_mobs = owner) + to_chat(owner, span_userdanger("[user] begins plucking [parent] from your [owner_limb.plaintext_zone] with [tool]... ")) + + if (!do_after(user, pluck_time, owner, extra_checks = CALLBACK(src, PROC_REF(still_in)))) + if (self_pluck) + to_chat(user, span_danger("You fail to pluck [parent] from your [owner_limb.plaintext_zone].")) + else + to_chat(user, span_danger("You fail to pluck [parent] from [owner]'s [owner_limb.plaintext_zone].")) + to_chat(owner, span_danger("[user] fails to pluck [parent] from your [owner_limb.plaintext_zone].")) + return + + if (self_pluck) + to_chat(span_notice("You pluck [parent] from your [owner_limb.plaintext_zone][safe_pluck ? "." : span_danger(", but it hurts like hell")]")) + + if(!safe_pluck) + damaging_removal_effect(min(self_pluck ? 1 : RIPPING_OUT_HELP_DAMAGE_MULTIPLIER, 0.4 * tool.w_class)) + + remove_embedding(user) + +/// Called when then item randomly falls out of a carbon. This handles the damage and descriptors, then calls remove_embedding() +/datum/embedding/proc/fall_out() + if(is_harmless()) + owner.visible_message(span_danger("[parent] falls off of [owner.name]'s [owner_limb.plaintext_zone]!"), + span_userdanger("[parent] falls off of your [owner_limb.plaintext_zone]!")) + remove_embedding() + return + + var/damage = parent.w_class * remove_pain_mult + owner.apply_damage( + damage = (1 - pain_stam_pct) * damage, + damagetype = BRUTE, + def_zone = owner_limb, + wound_bonus = CANT_WOUND, + sharpness = parent.get_sharpness(), + attacking_item = parent, + ) + + owner.apply_damage( + damage = pain_stam_pct * damage, + damagetype = STAMINA, + ) + + owner.visible_message(span_danger("[parent] falls out of [owner.name]'s [owner_limb.plaintext_zone]!"), + span_userdanger("[parent] falls out of your [owner_limb.plaintext_zone]!")) + remove_embedding() + +/// Whenever the parent item is forcefully moved by some weird means +/datum/embedding/proc/weapon_disappeared(atom/old_loc, dir, forced) + SIGNAL_HANDLER + // If something moved it to their limb, its not really *disappearing*, is it? + if (owner && parent.loc != owner_limb) + to_chat(owner, span_userdanger("[parent] that was embedded in your [owner_limb.plaintext_zone] disappears!")) + stop_embedding() + +/// So the sticky grenades chain-detonate, because mobs are very careful with which of their contents they blow up +/datum/embedding/proc/on_ex_act(atom/source, severity) + SIGNAL_HANDLER + // In the process of owner's ex_act + if (QDELETED(parent)) + return + switch(severity) + if(EXPLODE_DEVASTATE) + SSexplosions.high_mov_atom += parent + if(EXPLODE_HEAVY) + SSexplosions.med_mov_atom += parent + if(EXPLODE_LIGHT) + SSexplosions.low_mov_atom += parent + +/// Called when an object is ripped out of someone's body by magic or other abnormal means +/datum/embedding/proc/magic_pull(obj/item/weapon, mob/living/caster) + SIGNAL_HANDLER + + if(is_harmless()) + owner.visible_message(span_danger("[parent] vanishes from [owner]'s [owner_limb.plaintext_zone]!"), span_userdanger("[parent] vanishes from [owner_limb.plaintext_zone]!")) + return + + var/damage = parent.w_class * remove_pain_mult + + owner.apply_damage( + damage = (1 - pain_stam_pct) * damage * 1.5, + damagetype = BRUTE, + def_zone = owner_limb, + wound_bonus = max(0, parent.wound_bonus), // Performs exit wounds and flings the user to the caster if nearby + sharpness = parent.get_sharpness() || SHARP_EDGED, + attacking_item = parent, + ) + + owner.apply_damage( + damage = pain_stam_pct * damage, + damagetype = STAMINA, + ) + + owner.cause_wound_of_type_and_severity(WOUND_PIERCE, owner_limb, WOUND_SEVERITY_MODERATE) + playsound(owner, 'sound/effects/wounds/blood2.ogg', 50, TRUE) + + var/dist = get_dist(caster, owner) //Check if the caster is close enough to yank them in + if(dist >= 7) + owner.visible_message(span_danger("[parent] is violently torn from [owner]'s [owner_limb.plaintext_zone]!"), span_userdanger("[parent] is violently torn from your [owner_limb.plaintext_zone]!")) + return + + owner.throw_at(caster, get_dist(owner, caster) - 1, 1, caster) + owner.Paralyze(1 SECONDS) + owner.visible_message(span_alert("[owner] is sent flying towards [caster] as the [parent] tears out of them!"), span_alert("You are launched at [caster] as the [parent] tears from your body and towards their hand!")) + +/datum/embedding/proc/still_in() + if (parent.loc != owner) + return FALSE + if (!(parent in owner_limb?.embedded_objects)) + return FALSE + if (owner_limb?.owner != owner) + return FALSE + return TRUE + +#undef RIPPING_OUT_HELP_TIME_MULTIPLIER +#undef RIPPING_OUT_HELP_DAMAGE_MULTIPLIER diff --git a/code/datums/http.dm b/code/datums/http.dm index 49b183fde6c64..8665b351ab757 100644 --- a/code/datums/http.dm +++ b/code/datums/http.dm @@ -80,3 +80,13 @@ var/errored = FALSE var/error + +/datum/http_response/serialize_list(list/options, list/semvers) + . = ..() + .["status_code"] = status_code + .["body"] = body + .["headers"] = headers + + .["errored"] = errored + .["error"] = error + return . diff --git a/code/datums/mutations/tongue_spike.dm b/code/datums/mutations/tongue_spike.dm index fbfeb66780750..99ce32c09e363 100644 --- a/code/datums/mutations/tongue_spike.dm +++ b/code/datums/mutations/tongue_spike.dm @@ -49,7 +49,7 @@ force = 2 throwforce = 25 throw_speed = 4 - embed_type = /datum/embed_data/tongue_spike + embed_type = /datum/embedding/tongue_spike w_class = WEIGHT_CLASS_SMALL sharpness = SHARP_POINTY custom_materials = list(/datum/material/biomass = SMALL_MATERIAL_AMOUNT * 5) @@ -58,34 +58,35 @@ /// if we missed our target var/missed = TRUE -/datum/embed_data/tongue_spike - impact_pain_mult = 0 - pain_mult = 15 - embed_chance = 100 - fall_chance = 0 - ignore_throwspeed_threshold = TRUE - /obj/item/hardened_spike/Initialize(mapload, mob/living/carbon/source) . = ..() src.fired_by_ref = WEAKREF(source) - addtimer(CALLBACK(src, PROC_REF(check_embedded)), 5 SECONDS) + addtimer(CALLBACK(src, PROC_REF(check_morph)), 5 SECONDS) -/obj/item/hardened_spike/proc/check_embedded() - if(missed) - unembedded() +/obj/item/hardened_spike/proc/check_morph() + // Failed to embed, morph back + if (!embed_data?.owner) + morph_back() -/obj/item/hardened_spike/embedded(atom/target) - . = ..() - if(isbodypart(target)) - missed = FALSE - -/obj/item/hardened_spike/unembedded() +/obj/item/hardened_spike/proc/morph_back() visible_message(span_warning("[src] cracks and twists, changing shape!")) for(var/obj/tongue as anything in contents) tongue.forceMove(get_turf(src)) - qdel(src) +/datum/embedding/tongue_spike + impact_pain_mult = 0 + pain_mult = 15 + embed_chance = 100 + fall_chance = 0 + ignore_throwspeed_threshold = TRUE + +/datum/embedding/tongue_spike/stop_embedding() + . = ..() + var/obj/item/hardened_spike/tongue_spike = parent + if (!QDELETED(tongue_spike)) // This can cause a qdel loop + tongue_spike.morph_back() + /datum/mutation/human/tongue_spike/chem name = "Chem Spike" desc = "Позволяет существу добровольно выстрелить своим языком из биомассы, позволяет передавать химические вещества на большое расстояние." @@ -112,39 +113,35 @@ desc = "Hardened biomass, shaped into... something." icon_state = "tonguespikechem" throwforce = 2 - embed_type = /datum/embed_data/tongue_spike/chem - /// Whether the tongue's already embedded in a target once before - var/embedded_once_alread = FALSE + embed_type = /datum/embedding/tongue_spike/chem -/datum/embed_data/tongue_spike/chem +/datum/embedding/tongue_spike/chem pain_mult = 0 pain_chance = 0 -/obj/item/hardened_spike/chem/embedded(mob/living/carbon/human/embedded_mob) - . = ..() - if(embedded_once_alread) - return - embedded_once_alread = TRUE - - var/mob/living/carbon/fired_by = fired_by_ref?.resolve() - if(!fired_by) +/datum/embedding/tongue_spike/chem/on_successful_embed(mob/living/carbon/victim, obj/item/bodypart/target_limb) + var/obj/item/hardened_spike/chem/tongue_spike = parent + var/mob/living/carbon/fired_by = tongue_spike.fired_by_ref?.resolve() + if(!istype(fired_by)) return - var/datum/action/send_chems/chem_action = new(src) - chem_action.transferred_ref = WEAKREF(embedded_mob) + var/datum/action/send_chems/chem_action = new(tongue_spike) + chem_action.transferred_ref = WEAKREF(victim) chem_action.Grant(fired_by) to_chat(fired_by, span_notice("Link established! Use the \"Transfer Chemicals\" ability \ to send your chemicals to the linked target!")) -/obj/item/hardened_spike/chem/unembedded() - var/mob/living/carbon/fired_by = fired_by_ref?.resolve() - if(fired_by) - to_chat(fired_by, span_warning("Link lost!")) - var/datum/action/send_chems/chem_action = locate() in fired_by.actions - QDEL_NULL(chem_action) +/datum/embedding/tongue_spike/chem/stop_embedding() + . = ..() + var/obj/item/hardened_spike/chem/tongue_spike = parent + var/mob/living/carbon/fired_by = tongue_spike.fired_by_ref?.resolve() + if(!istype(fired_by)) + return - return ..() + to_chat(fired_by, span_warning("Link lost!")) + var/datum/action/send_chems/chem_action = locate() in fired_by.actions + qdel(chem_action) /datum/action/send_chems name = "Transfer Chemicals" @@ -177,9 +174,11 @@ transferer.reagents.trans_to(transferred, transferer.reagents.total_volume, transferred_by = transferer) var/obj/item/hardened_spike/chem/chem_spike = target - var/obj/item/bodypart/spike_location = chem_spike.check_embedded() - //this is where it would deal damage, if it transfers chems it removes itself so no damage - chem_spike.forceMove(get_turf(spike_location)) - chem_spike.visible_message(span_notice("[chem_spike] falls out of [spike_location]!")) + // This is where it would deal damage, if it transfers chems it removes itself so no damage + var/mob/living/carbon/spike_owner = chem_spike.get_embed()?.owner + // Message first because it'll shift back into a tongue right after moving + if (istype(spike_owner)) + spike_owner.visible_message(span_notice("[chem_spike] falls out of [spike_owner]!")) + chem_spike.forceMove(get_turf(chem_spike)) return TRUE diff --git a/code/datums/quirks/negative_quirks/photophobia.dm b/code/datums/quirks/negative_quirks/photophobia.dm index e118ff7a968d2..4fd22ceaca9c1 100644 --- a/code/datums/quirks/negative_quirks/photophobia.dm +++ b/code/datums/quirks/negative_quirks/photophobia.dm @@ -31,7 +31,7 @@ if(istype(normal_eyes)) normal_eyes.flash_protect = initial(normal_eyes.flash_protect) -/datum/quirk/photophobia/proc/check_eyes(obj/item/organ/eyes/sensitive_eyes) +/datum/quirk/photophobia/proc/check_eyes(datum/source, obj/item/organ/eyes/sensitive_eyes) SIGNAL_HANDLER if(!istype(sensitive_eyes)) return @@ -42,7 +42,7 @@ return target_eyes.flash_protect = max(target_eyes.flash_protect - 1, FLASH_PROTECTION_HYPER_SENSITIVE) -/datum/quirk/photophobia/proc/restore_eyes(obj/item/organ/eyes/normal_eyes) +/datum/quirk/photophobia/proc/restore_eyes(datum/source, obj/item/organ/eyes/normal_eyes) SIGNAL_HANDLER if(!istype(normal_eyes)) return diff --git a/code/datums/station_traits/positive_traits.dm b/code/datums/station_traits/positive_traits.dm index d0e72e183dbb0..03e32b37d2f55 100644 --- a/code/datums/station_traits/positive_traits.dm +++ b/code/datums/station_traits/positive_traits.dm @@ -115,9 +115,8 @@ new /obj/machinery/light/floor(chosen) continue var/stick_type = pick(glowsticks) - var/obj/item/flashlight/glowstick/stick = new stick_type(chosen) + var/obj/item/flashlight/glowstick/stick = new stick_type(chosen, rand(10, 45)) ///we want a wider range, otherwise they'd all burn out in about 20 minutes. - stick.max_fuel = stick.fuel = rand(10 MINUTES, 45 MINUTES) stick.turn_on() /datum/station_trait/strong_supply_lines diff --git a/code/datums/status_effects/buffs/food/speech.dm b/code/datums/status_effects/buffs/food/speech.dm index 634fd739709b3..9878f0ebf35b0 100644 --- a/code/datums/status_effects/buffs/food/speech.dm +++ b/code/datums/status_effects/buffs/food/speech.dm @@ -3,9 +3,13 @@ /datum/status_effect/food/speech/italian alert_type = /atom/movable/screen/alert/status_effect/italian_speech + on_remove_on_mob_delete = TRUE + /// Ref to the component so we can clear it + var/datum/component/speechmod /datum/status_effect/food/speech/italian/on_apply() - AddComponent( \ + . = ..() + speechmod = AddComponent( \ /datum/component/speechmod, \ replacements = strings("italian_replacement.json", "italian"), \ end_string = list( @@ -16,7 +20,10 @@ ), \ end_string_chance = 3 \ ) - return ..() + +/datum/status_effect/food/speech/italian/on_remove() + . = ..() + QDEL_NULL(speechmod) /atom/movable/screen/alert/status_effect/italian_speech name = "Linguini Embrace" @@ -25,9 +32,13 @@ /datum/status_effect/food/speech/french alert_type = /atom/movable/screen/alert/status_effect/french_speech + on_remove_on_mob_delete = TRUE + /// Ref to the component so we can clear it + var/datum/component/speechmod /datum/status_effect/food/speech/french/on_apply() - AddComponent( \ + . = ..() + speechmod = owner.AddComponent( \ /datum/component/speechmod, \ replacements = strings("french_replacement.json", "french"), \ end_string = list( @@ -37,7 +48,10 @@ ), \ end_string_chance = 3, \ ) - return ..() + +/datum/status_effect/food/speech/french/on_remove() + . = ..() + QDEL_NULL(speechmod) /atom/movable/screen/alert/status_effect/french_speech name = "Café Chic" diff --git a/code/game/gamemodes/objective_items.dm b/code/game/gamemodes/objective_items.dm index 6381901624410..c2421532e62c2 100644 --- a/code/game/gamemodes/objective_items.dm +++ b/code/game/gamemodes/objective_items.dm @@ -37,6 +37,9 @@ /// A hint explaining how one may find the target item. var/steal_hint = "The clown might have one." + ///If the item takes special steps to destroy for an objective (e.g. blackbox) + var/destruction_method = null + /// For objectives with special checks (does that intellicard have an ai in it? etcetc) /datum/objective_item/proc/check_special_completion(obj/item/thing) return TRUE @@ -327,8 +330,8 @@ /datum/objective_item/steal/compactshotty name = "head of security's personal compact shotgun" targetitem = /obj/item/gun/ballistic/shotgun/automatic/combat/compact - excludefromjob = list(JOB_HEAD_OF_SECURITY) - item_owner = list(JOB_HEAD_OF_SECURITY) + excludefromjob = list(JOB_WARDEN) + item_owner = list(JOB_WARDEN) exists_on_map = TRUE difficulty = 4 steal_hint = "A miniaturized combat shotgun. May be found in Head of Security's locker or strapped to their back." @@ -558,6 +561,7 @@ exists_on_map = TRUE difficulty = 4 steal_hint = "The station's data Blackbox, found solely within Telecommunications." + destruction_method = "Too strong to be be destroyed via normal means - needs to be dusted via the supermatter, or burnt in the chapel's crematorium." /obj/item/blackbox/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/blackbox) diff --git a/code/game/machinery/dance_machine.dm b/code/game/machinery/dance_machine.dm index 43946538ac51b..4df9e6a107f49 100644 --- a/code/game/machinery/dance_machine.dm +++ b/code/game/machinery/dance_machine.dm @@ -26,6 +26,11 @@ QDEL_NULL(music_player) return ..() +/obj/machinery/jukebox/examine(mob/user) + . = ..() + if(music_player.active_song_sound) + . += "Now playing: [music_player.selection.song_name]" + /obj/machinery/jukebox/no_access req_access = null diff --git a/code/game/machinery/recharger.dm b/code/game/machinery/recharger.dm index 226e19bfe84f1..1a46340755268 100644 --- a/code/game/machinery/recharger.dm +++ b/code/game/machinery/recharger.dm @@ -58,7 +58,7 @@ return if(istype(charging, /obj/item/gun/ballistic/automatic/battle_rifle)) var/obj/item/gun/ballistic/automatic/battle_rifle/recalibrating_gun = charging - . += span_notice("- \The [charging]'s system degradation is at stage [recalibrating_gun.degradation_stage] of [recalibrating_gun.degradation_stage_max]%.") + . += span_notice("- \The [charging]'s system degradation is at stage [recalibrating_gun.degradation_stage] of [recalibrating_gun.degradation_stage_max].") . += span_notice("- \The [charging]'s degradation buffer is at [PERCENT(recalibrating_gun.shots_before_degradation/recalibrating_gun.max_shots_before_degradation)]%.") return . += span_notice("- \The [charging] is not reporting a power level.") diff --git a/code/game/objects/effects/posters/poster.dm b/code/game/objects/effects/posters/poster.dm index 135887aafc83f..ca4242daa767e 100644 --- a/code/game/objects/effects/posters/poster.dm +++ b/code/game/objects/effects/posters/poster.dm @@ -195,17 +195,16 @@ return FALSE return TRUE +// HO-HO-HOHOHO HU HU-HU HU-HU /obj/structure/sign/poster/proc/spring_trap(mob/user) var/obj/item/shard/payload = trap?.resolve() if (!payload) return to_chat(user, span_warning("There's something sharp behind this! What the hell?")) - if(!can_embed_trap(user) || !payload.tryEmbed(user.get_active_hand(), forced = TRUE)) + if(!can_embed_trap(user) || !payload.force_embed(user, user.get_active_hand())) visible_message(span_notice("A [payload.name] falls from behind the poster.") ) payload.forceMove(user.drop_location()) - else - SEND_SIGNAL(src, COMSIG_POSTER_TRAP_SUCCEED, user) /obj/structure/sign/poster/proc/can_embed_trap(mob/living/carbon/human/user) if (!istype(user) || HAS_TRAIT(user, TRAIT_PIERCEIMMUNE)) diff --git a/code/game/objects/effects/spawners/random/trash.dm b/code/game/objects/effects/spawners/random/trash.dm index 6f6f5badc8e7e..a6d9bfcc45afc 100644 --- a/code/game/objects/effects/spawners/random/trash.dm +++ b/code/game/objects/effects/spawners/random/trash.dm @@ -53,6 +53,12 @@ /obj/effect/spawner/random/entertainment/cigar = 1, /obj/item/stack/ore/gold = 1, ) +/obj/effect/spawner/random/trash/deluxe_garbage/Initialize(mapload) + if(mapload) + var/turf/location = get_turf(loc) + if(location.initial_gas_mix != OPENTURF_DEFAULT_ATMOS && location.initial_gas_mix != OPENTURF_DIRTY_ATMOS) + loot -= /mob/living/basic/mouse + return ..() /obj/effect/spawner/random/trash/cigbutt name = "cigarette butt spawner" diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index b8bb5d923a617..31abec9882d28 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -176,7 +176,7 @@ /// Does it embed and if yes, what kind of embed var/embed_type /// Stores embedding data - var/datum/embed_data/embed_data + VAR_PROTECTED/datum/embedding/embed_data ///for flags such as [GLASSESCOVERSEYES] var/flags_cover = 0 @@ -277,8 +277,6 @@ add_weapon_description() SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NEW_ITEM, src) - if(get_embed()) - AddElement(/datum/element/embed) setup_reskinning() @@ -855,15 +853,18 @@ /obj/item/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) . = ..() + if(!isliving(hit_atom)) //Living mobs handle hit sounds differently. if(throw_drop_sound) playsound(src, throw_drop_sound, YEET_SOUND_VOLUME, ignore_walls = FALSE, vary = sound_vary) return playsound(src, drop_sound, YEET_SOUND_VOLUME, ignore_walls = FALSE, vary = sound_vary) return - var/volume = get_volume_by_throwforce_and_or_w_class() + if(.) //it's been caught. return + + var/volume = get_volume_by_throwforce_and_or_w_class() if (throwforce > 0 || HAS_TRAIT(src, TRAIT_CUSTOM_TAP_SOUND)) if (mob_throw_hit_sound) playsound(hit_atom, mob_throw_hit_sound, volume, TRUE, -1) @@ -1308,15 +1309,6 @@ dropped(M, FALSE) return ..() -/obj/item/proc/embedded(atom/embedded_target, obj/item/bodypart/part) - SHOULD_CALL_PARENT(TRUE) - SEND_SIGNAL(src, COMSIG_ITEM_EMBEDDED, embedded_target, part) - -/obj/item/proc/unembedded() - if(item_flags & DROPDEL && !QDELETED(src)) - qdel(src) - return TRUE - /obj/item/proc/canStrip(mob/stripper, mob/owner) SHOULD_BE_PURE(TRUE) return !HAS_TRAIT(src, TRAIT_NODROP) && !(item_flags & ABSTRACT) @@ -1324,20 +1316,6 @@ /obj/item/proc/doStrip(mob/stripper, mob/owner) return owner.dropItemToGround(src) -///Does the current embedding var meet the criteria for being harmless? Namely, does it have a pain multiplier and jostle pain mult of 0? If so, return true. -/obj/item/proc/is_embed_harmless() - if (!get_embed()) - return FALSE - - return !isnull(embed_data.pain_mult) && !isnull(embed_data.jostle_pain_mult) && embed_data.pain_mult == 0 && embed_data.jostle_pain_mult == 0 - -///In case we want to do something special (like self delete) upon failing to embed in something. -/obj/item/proc/failedEmbed() - SHOULD_CALL_PARENT(TRUE) - SEND_SIGNAL(src, COMSIG_ITEM_FAILED_EMBED) - if(item_flags & DROPDEL && !QDELETED(src)) - qdel(src) - ///Called by the carbon throw_item() proc. Returns null if the item negates the throw, or a reference to the thing to suffer the throw else. /obj/item/proc/on_thrown(mob/living/carbon/user, atom/target) if((item_flags & ABSTRACT) || HAS_TRAIT(src, TRAIT_NODROP)) @@ -1348,34 +1326,6 @@ return return src -/** - * tryEmbed() is for when you want to try embedding something without dealing with the damage + hit messages of calling hitby() on the item while targeting the target. - * - * Really, this is used mostly with projectiles with shrapnel payloads, from [/datum/element/embed/proc/checkEmbedProjectile], and called on said shrapnel. Mostly acts as an intermediate between different embed elements. - * - * Returns TRUE if it embedded successfully, nothing otherwise - * - * Arguments: - * * target- Either a body part or a carbon. What are we hitting? - * * forced- Do we want this to go through 100%? - */ -/obj/item/proc/tryEmbed(atom/target, forced=FALSE) - if(!isbodypart(target) && !iscarbon(target)) - return NONE - - if(!forced && !get_embed()) - return NONE - - if(SEND_SIGNAL(src, COMSIG_EMBED_TRY_FORCE, target = target, forced = forced)) - return COMPONENT_EMBED_SUCCESS - - failedEmbed() - -///For when you want to disable an item's embedding capabilities (like transforming weapons and such), this proc will detach any active embed elements from it. -/obj/item/proc/disableEmbedding() - SEND_SIGNAL(src, COMSIG_ITEM_DISABLE_EMBED) - return - /// How many different types of mats will be counted in a bite? #define MAX_MATS_PER_BITE 2 @@ -1404,15 +1354,16 @@ victim.apply_damage(max(15, force), BRUTE, BODY_ZONE_HEAD, wound_bonus = 10, sharpness = TRUE) victim.losebreath += 2 - if(tryEmbed(victim.get_bodypart(BODY_ZONE_CHEST), forced = TRUE)) //and if it embeds successfully in their chest, cause a lot of pain + if(force_embed(victim, BODY_ZONE_CHEST)) //and if it embeds successfully in their chest, cause a lot of pain victim.apply_damage(max(25, force*1.5), BRUTE, BODY_ZONE_CHEST, wound_bonus = 7, sharpness = TRUE) victim.losebreath += 6 discover_after = FALSE if(QDELETED(src)) // in case trying to embed it caused its deletion (say, if it's DROPDEL) return source_item?.reagents?.add_reagent(/datum/reagent/blood, 2) + return discover_after - else if(custom_materials?.len) //if we've got materials, let's see what's in it + if(custom_materials?.len) //if we've got materials, let's see what's in it // How many mats have we found? You can only be affected by two material datums by default var/found_mats = 0 // How much of each material is in it? Used to determine if the glass should break @@ -1445,25 +1396,25 @@ victim.adjust_disgust(33) victim.visible_message(span_warning("[victim] looks like [victim.p_theyve()] just bitten into something hard."), \ span_warning("Eugh! Did I just bite into something?")) + return discover_after - else if(w_class == WEIGHT_CLASS_TINY) //small items like soap or toys that don't have mat datums - // victim's chest (for cavity implanting the item) - var/obj/item/bodypart/chest/victim_cavity = victim.get_bodypart(BODY_ZONE_CHEST) - if(victim_cavity.cavity_item) - victim.vomit(vomit_flags = (MOB_VOMIT_MESSAGE | MOB_VOMIT_HARM), lost_nutrition = 5, distance = 0) - forceMove(drop_location()) - to_chat(victim, span_warning("You vomit up a [name]! [source_item? "Was that in \the [source_item]?" : ""]")) - else - victim.transferItemToLoc(src, victim, TRUE) - victim.losebreath += 2 - victim_cavity.cavity_item = src - to_chat(victim, span_warning("You swallow hard. [source_item? "Something small was in \the [source_item]..." : ""]")) - discover_after = FALSE - - else + if(w_class > WEIGHT_CLASS_TINY) //small items like soap or toys that don't have mat datums to_chat(victim, span_warning("[source_item? "Something strange was in the \the [source_item]..." : "I just bit something strange..."] ")) + return discover_after + + // victim's chest (for cavity implanting the item) + var/obj/item/bodypart/chest/victim_cavity = victim.get_bodypart(BODY_ZONE_CHEST) + if(victim_cavity.cavity_item) + victim.vomit(vomit_flags = (MOB_VOMIT_MESSAGE | MOB_VOMIT_HARM), lost_nutrition = 5, distance = 0) + forceMove(drop_location()) + to_chat(victim, span_warning("You vomit up a [name]! [source_item? "Was that in \the [source_item]?" : ""]")) + return FALSE - return discover_after + victim.transferItemToLoc(src, victim, TRUE) + victim.losebreath += 2 + victim_cavity.cavity_item = src + to_chat(victim, span_warning("You swallow hard. [source_item? "Something small was in \the [source_item]..." : ""]")) + return FALSE #undef MAX_MATS_PER_BITE @@ -1925,21 +1876,6 @@ return TRUE return FALSE -/// Fetches embedding data -/obj/item/proc/get_embed() - RETURN_TYPE(/datum/embed_data) - return embed_type ? (embed_data ||= get_embed_by_type(embed_type)) : embed_data - -/obj/item/proc/set_embed(datum/embed_data/embed) - if(embed_data == embed) - return - if(isnull(get_embed())) // Add embed on objects that did not have it added - AddElement(/datum/element/embed) - if(!GLOB.embed_by_type[embed_data?.type]) - qdel(embed_data) - embed_data = ispath(embed) ? get_embed_by_type(embed) : embed - SEND_SIGNAL(src, COMSIG_ITEM_EMBEDDING_UPDATE) - /obj/item/apply_main_material_effects(datum/material/main_material, amount, multipier) . = ..() if(material_flags & MATERIAL_GREYSCALE) @@ -2048,3 +1984,40 @@ BARE WOUND: [bare_wound_bonus] "} + +/// Fetches, or lazyloads, our embedding datum +/obj/item/proc/get_embed() + RETURN_TYPE(/datum/embedding) + // Something may call this during qdeleting, which would cause a harddel + if (QDELETED(src)) + return null + if (embed_data) + return embed_data + if (embed_type) + embed_data = new embed_type(src) + return embed_data + +/// Sets our embedding datum to a different one. Can also take types +/obj/item/proc/set_embed(datum/embedding/new_embed) + if (new_embed == embed_data) + return + + // Needs to be QDELETED as embed data uses this to clean itself up from its parent (us) + if (!QDELETED(embed_data)) + qdel(embed_data) + + if (ispath(new_embed)) + new_embed = new new_embed(src) + + embed_data = new_embed + SEND_SIGNAL(src, COMSIG_ITEM_EMBEDDING_UPDATE) + +/// Embed ourselves into an object if we possess embedding data +/obj/item/proc/force_embed(mob/living/carbon/victim, obj/item/bodypart/target_limb) + if (!istype(victim)) + return FALSE + + if (!istype(target_limb)) + target_limb = victim.get_bodypart(target_limb) || victim.bodyparts[1] + + return get_embed()?.embed_into(victim, target_limb) diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm index 91de7f684a1ac..93bda22ef6a68 100644 --- a/code/game/objects/items/crayons.dm +++ b/code/game/objects/items/crayons.dm @@ -829,9 +829,7 @@ if(isbodypart(target)) var/obj/item/bodypart/limb = target if(IS_ROBOTIC_LIMB(limb)) - context[SCREENTIP_CONTEXT_CTRL_LMB] = "Restyle robotic limb" - else - context[SCREENTIP_CONTEXT_CTRL_LMB] = "Copy color" + context[SCREENTIP_CONTEXT_LMB] = "Restyle robotic limb" return CONTEXTUAL_SCREENTIP_SET @@ -871,7 +869,7 @@ . += "It's roughly [PERCENT(charges_left/charges)]% full." else . += "It is empty." - . += span_notice("Alt-click [src] to [ is_capped ? "take the cap off" : "put the cap on"]. Right-click a colored object to match its existing color.") + . += span_notice("Alt-click [src] to [ is_capped ? "take the cap off" : "put the cap on"].") /obj/item/toy/crayon/spraycan/can_use_on(atom/target, mob/user, list/modifiers) @@ -887,9 +885,6 @@ return ..() /obj/item/toy/crayon/spraycan/use_on(atom/target, mob/user, list/modifiers) - if (LAZYACCESS(modifiers, CTRL_CLICK)) - return ctrl_interact(target, user) - if(is_capped) balloon_alert(user, "take the cap off first!") return ITEM_INTERACT_BLOCKING @@ -897,6 +892,10 @@ if(check_empty(user)) return ITEM_INTERACT_BLOCKING + if (isbodypart(target)) + if (color_limb(target, user)) + return ITEM_INTERACT_SUCCESS + if(iscarbon(target)) if(pre_noise || post_noise) playsound(user.loc, 'sound/effects/spray.ogg', 25, TRUE, 5) @@ -994,30 +993,9 @@ user.visible_message(span_notice("[user] coats [target] with spray paint!"), span_notice("You coat [target] with spray paint.")) return ITEM_INTERACT_SUCCESS -/obj/item/toy/crayon/spraycan/proc/ctrl_interact(atom/interacting_with, mob/living/user) - if(is_capped) - if(!interacting_with.color) - // let's be generous and assume if they're trying to match something with no color, while capped, - // we shouldn't be blocking further interactions - return NONE - balloon_alert(user, "take the cap off first!") - return ITEM_INTERACT_BLOCKING - - if(check_empty(user)) - return ITEM_INTERACT_BLOCKING - - if(!isbodypart(interacting_with) || !actually_paints) - if(interacting_with.color) - paint_color = interacting_with.color - balloon_alert(user, "matched colour of target") - update_appearance() - return ITEM_INTERACT_BLOCKING - balloon_alert(user, "can't match those colours!") - return ITEM_INTERACT_BLOCKING - - var/obj/item/bodypart/limb = interacting_with +/obj/item/toy/crayon/spraycan/proc/color_limb(obj/item/bodypart/limb, mob/living/user) if(!IS_ROBOTIC_LIMB(limb)) - return ITEM_INTERACT_BLOCKING + return FALSE var/list/skins = list() var/static/list/style_list_icons = list( @@ -1036,7 +1014,7 @@ if(choice && (use_charges(user, 5, requires_full = FALSE))) playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) limb.change_appearance(style_list_icons[choice], greyscale = FALSE) - return ITEM_INTERACT_SUCCESS + return TRUE /obj/item/toy/crayon/spraycan/click_alt(mob/user) if(!has_cap) diff --git a/code/game/objects/items/devices/flashlight.dm b/code/game/objects/items/devices/flashlight.dm index 7a66a29da96d5..8e3518653a0d2 100644 --- a/code/game/objects/items/devices/flashlight.dm +++ b/code/game/objects/items/devices/flashlight.dm @@ -781,57 +781,69 @@ grind_results = list(/datum/reagent/phenol = 15, /datum/reagent/hydrogen = 10, /datum/reagent/oxygen = 5) //Meth-in-a-stick sound_on = 'sound/effects/wounds/crack2.ogg' // the cracking sound isn't just for wounds silly toggle_context = FALSE - /// How many seconds of fuel we have left - var/fuel = 0 /// How much max fuel we have var/max_fuel = 0 + /// How much oxygen gets added upon cracking the stick. Doesn't actually produce a reaction with the fluid but it does allow for bootleg chemical "grenades" + var/oxygen_added = 5 + /// How much temperature gets added for every unit of fuel burned down + var/temp_per_fuel = 3 + /// Type of reagent we add as fuel + var/fuel_type = /datum/reagent/luminescent_fluid /// The timer id powering our burning var/timer_id = TIMER_ID_NULL -/obj/item/flashlight/glowstick/Initialize(mapload) - fuel = rand(20 MINUTES, 25 MINUTES) - max_fuel = fuel +/obj/item/flashlight/glowstick/Initialize(mapload, fuel_override = null) + . = ..() + max_fuel = isnull(fuel_override) ? rand(20, 25) : fuel_override + create_reagents(max_fuel + oxygen_added, DRAWABLE | INJECTABLE) + reagents.add_reagent(fuel_type, max_fuel) set_light_color(color) - return ..() + AddComponent(/datum/component/edible,\ + food_flags = FOOD_NO_EXAMINE,\ + volume = reagents.total_volume,\ + bite_consumption = round(reagents.total_volume / (rand(20, 30) * 0.1)),\ + ) + +/obj/item/flashlight/glowstick/proc/get_fuel() + return reagents?.get_reagent_amount(fuel_type) /// Burns down the glowstick by the specified time /// Returns the amount of time we need to burn before a visual change will occur /obj/item/flashlight/glowstick/proc/burn_down(amount = 0) - fuel -= amount - var/fuel_target = 0 - if(fuel >= max_fuel) - fuel_target = max_fuel * 0.4 - else if(fuel >= max_fuel * 0.4) - fuel_target = max_fuel * 0.3 + if (!reagents.remove_all(amount)) + turn_off() + return 0 + + var/fuel = get_fuel() + if (fuel <= 0) + turn_off() + return 0 + + reagents.expose_temperature(amount * temp_per_fuel) + if(fuel >= max_fuel * 0.4) set_light_range(3) set_light_power(1.5) else if(fuel >= max_fuel * 0.3) - fuel_target = max_fuel * 0.2 set_light_range(2) set_light_power(1.25) else if(fuel >= max_fuel * 0.2) - fuel_target = max_fuel * 0.1 set_light_power(1) else if(fuel >= max_fuel * 0.1) - fuel_target = 0 set_light_range(1.5) set_light_power(0.5) - var/time_to_burn = round(fuel - fuel_target) - // Less then a ds? go home - if(time_to_burn <= 0) - turn_off() - - return time_to_burn + return round(reagents.total_volume * 0.1) /obj/item/flashlight/glowstick/proc/burn_loop(amount = 0) timer_id = TIMER_ID_NULL var/burn_next = burn_down(amount) if(burn_next <= 0) return - timer_id = addtimer(CALLBACK(src, PROC_REF(burn_loop), burn_next), burn_next, TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_OVERRIDE) + timer_id = addtimer(CALLBACK(src, PROC_REF(burn_loop), burn_next), burn_next MINUTES, TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_OVERRIDE) /obj/item/flashlight/glowstick/proc/turn_on() + reagents.add_reagent(/datum/reagent/oxygen, oxygen_added) + grind_results -= /datum/reagent/oxygen set_light_on(TRUE) // Just in case var/datum/action/toggle = locate(/datum/action/item_action/toggle_light) in actions // No sense having a toggle light action that we don't use eh? @@ -841,7 +853,7 @@ /obj/item/flashlight/glowstick/proc/turn_off() var/datum/action/toggle = locate(/datum/action/item_action/toggle_light) in actions - if(fuel && !toggle) + if(get_fuel() && !toggle) add_item_action(/datum/action/item_action/toggle_light) if(timer_id != TIMER_ID_NULL) var/expected_burn_time = burn_down(0) // This is dumb I'm sorry @@ -853,12 +865,12 @@ /obj/item/flashlight/glowstick/update_icon_state() . = ..() - icon_state = "[base_icon_state][(fuel <= 0) ? "-empty" : ""]" - inhand_icon_state = "[base_icon_state][((fuel > 0) && light_on) ? "-on" : ""]" + icon_state = "[base_icon_state][(get_fuel() <= 0) ? "-empty" : ""]" + inhand_icon_state = "[base_icon_state][((get_fuel() > 0) && light_on) ? "-on" : ""]" /obj/item/flashlight/glowstick/update_overlays() . = ..() - if(fuel <= 0 && !light_on) + if(get_fuel() <= 0 && !light_on) return var/mutable_appearance/glowstick_overlay = mutable_appearance(icon, "glowstick-glow") @@ -866,14 +878,14 @@ . += glowstick_overlay /obj/item/flashlight/glowstick/toggle_light(mob/user) - if(fuel <= 0) + if(get_fuel() <= 0) return FALSE if(light_on) return FALSE return ..() /obj/item/flashlight/glowstick/attack_self(mob/user) - if(fuel <= 0) + if(get_fuel() <= 0) balloon_alert(user, "glowstick is spent!") return if(light_on) @@ -886,7 +898,7 @@ turn_on() /obj/item/flashlight/glowstick/suicide_act(mob/living/carbon/human/user) - if(!fuel) + if(!get_fuel()) user.visible_message(span_suicide("[user] is trying to squirt [src]'s fluids into [user.p_their()] eyes... but it's empty!")) return SHAME var/obj/item/organ/eyes/eyes = user.get_organ_slot(ORGAN_SLOT_EYES) @@ -894,32 +906,38 @@ user.visible_message(span_suicide("[user] is trying to squirt [src]'s fluids into [user.p_their()] eyes... but [user.p_they()] don't have any!")) return SHAME user.visible_message(span_suicide("[user] is squirting [src]'s fluids into [user.p_their()] eyes! Кажется, [user.ru_p_they()] пытается совершить самоубийство!")) - burn_loop(fuel) + burn_loop(get_fuel()) return FIRELOSS /obj/item/flashlight/glowstick/red name = "red glowstick" color = COLOR_SOFT_RED + fuel_type = /datum/reagent/luminescent_fluid/red /obj/item/flashlight/glowstick/blue name = "blue glowstick" color = LIGHT_COLOR_BLUE + fuel_type = /datum/reagent/luminescent_fluid/blue /obj/item/flashlight/glowstick/cyan name = "cyan glowstick" color = LIGHT_COLOR_CYAN + fuel_type = /datum/reagent/luminescent_fluid/cyan /obj/item/flashlight/glowstick/orange name = "orange glowstick" color = LIGHT_COLOR_ORANGE + fuel_type = /datum/reagent/luminescent_fluid/orange /obj/item/flashlight/glowstick/yellow name = "yellow glowstick" color = LIGHT_COLOR_DIM_YELLOW + fuel_type = /datum/reagent/luminescent_fluid/yellow /obj/item/flashlight/glowstick/pink name = "pink glowstick" color = LIGHT_COLOR_PINK + fuel_type = /datum/reagent/luminescent_fluid/pink /obj/item/flashlight/spotlight //invisible lighting source name = "disco light" diff --git a/code/game/objects/items/food/egg.dm b/code/game/objects/items/food/egg.dm index bbb7d6784e2e0..d184c7598fdd1 100644 --- a/code/game/objects/items/food/egg.dm +++ b/code/game/objects/items/food/egg.dm @@ -277,7 +277,10 @@ GLOBAL_VAR_INIT(chicks_from_eggs, 0) foodtypes = MEAT | BREAKFAST | DAIRY venue_value = FOOD_PRICE_CHEAP crafting_complexity = FOOD_COMPLEXITY_2 - crafted_food_buff = /datum/status_effect/food/speech/french + +/obj/item/food/omelette/Initialize(mapload) + . = ..() + AddElement(/datum/element/love_food_buff, /datum/status_effect/food/speech/french) /obj/item/food/omelette/attackby(obj/item/item, mob/user, params) if(istype(item, /obj/item/kitchen/fork)) diff --git a/code/game/objects/items/food/spaghetti.dm b/code/game/objects/items/food/spaghetti.dm index bf1fca9332a08..feb299101fce0 100644 --- a/code/game/objects/items/food/spaghetti.dm +++ b/code/game/objects/items/food/spaghetti.dm @@ -262,4 +262,7 @@ tastes = list("spaghetti" = 1, "parmigiano reggiano" = 1, "guanciale" = 1) foodtypes = GRAIN | MEAT | DAIRY crafting_complexity = FOOD_COMPLEXITY_4 - crafted_food_buff = /datum/status_effect/food/speech/italian + +/obj/item/food/spaghetti/carbonara/Initialize(mapload) + . = ..() + AddElement(/datum/element/love_food_buff, /datum/status_effect/food/speech/italian) diff --git a/code/game/objects/items/forensicsspoofer.dm b/code/game/objects/items/forensicsspoofer.dm new file mode 100644 index 0000000000000..b2384d2dda237 --- /dev/null +++ b/code/game/objects/items/forensicsspoofer.dm @@ -0,0 +1,198 @@ +/obj/item/forensics_spoofer + name = /obj/item/detective_scanner::name + desc = "Used to adjacently scan objects and biomass for fibers and fingerprints. Can replicate the findings." + icon = /obj/item/detective_scanner::icon + icon_state = /obj/item/detective_scanner::icon_state + w_class = WEIGHT_CLASS_SMALL + inhand_icon_state = /obj/item/detective_scanner::inhand_icon_state + worn_icon_state = /obj/item/detective_scanner::worn_icon_state + lefthand_file = /obj/item/detective_scanner::lefthand_file + righthand_file = /obj/item/detective_scanner::righthand_file + obj_flags = CONDUCTS_ELECTRICITY + item_flags = NOBLUDGEON + slot_flags = ITEM_SLOT_BELT + /// stored fibers in memory + var/list/fibers = list() + /// stored fingerprints in memory + var/list/fingerprints = list() + /// chosen fiber to add to target + var/chosen_fiber + /// chosen fingerprint to add to target + var/chosen_fingerprint + /// max storage for fibers/fingerprints seperate for each + var/max_storage = 5 + /// do we scan for new material? if false will tamper + var/scan_mode = TRUE + /// do we make forensics scanner messages and sounds + var/silent_mode = FALSE + /// tamper cooldown time so people dont spam it on every single wall and thing ever + var/tamper_cooldown_time = 1 SECONDS + COOLDOWN_DECLARE(tamper_cooldown) + +/obj/item/forensics_spoofer/Initialize(mapload) + . = ..() + // most things have add_fingerprint in their item interaction because lol lmao + // tl;dr cut off the chain before anything fires so we dont add user fingerprints to target + RegisterSignal(src, COMSIG_ITEM_INTERACTING_WITH_ATOM, PROC_REF(do_interact)) + +/obj/item/forensics_spoofer/attack_self_secondary(mob/user, modifiers) + . = ..() + if(.) + return + scan_mode = !scan_mode + balloon_alert(user, "now [scan_mode ? "scanning" : "applying"]") + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +// ok due to shenanigans basically every item interact adds your fingerprints to it which isnt ideal so we have this +/obj/item/forensics_spoofer/proc/do_interact(datum/source, mob/living/user, atom/interacting_with, list/modifiers) + SIGNAL_HANDLER + if(scan_mode) + INVOKE_ASYNC(src, PROC_REF(scan), interacting_with, user) + else + tamper(interacting_with, user, do_fibers = !isnull(chosen_fiber)) + return ITEM_INTERACT_SUCCESS + +/obj/item/forensics_spoofer/proc/do_fake_scan(atom/target, mob/user) + if(silent_mode) + return + playsound(src, SFX_INDUSTRIAL_SCAN, 20, TRUE, -2, TRUE, FALSE) + user.visible_message( + span_notice("\The [user] points the [name] at \the [target] and performs a forensic scan.") + ) + +/obj/item/forensics_spoofer/proc/clear_values(list/the_list) + for(var/key in the_list) + the_list[key] = "" + +/obj/item/forensics_spoofer/proc/scan(atom/target, mob/living/user) + do_fake_scan(target, user) + if(isnull(target.forensics)) + target.balloon_alert(user, "nothing!") + return ITEM_INTERACT_FAILURE + var/list/new_fibers = LAZYCOPY(target.forensics.fibers) - fibers + var/list/new_prints = LAZYCOPY(target.forensics.fingerprints) - fingerprints + var/new_len = length(new_fibers) + length(new_prints) + balloon_alert(user, "[new_len ? new_len : "no"] new prints/fibers") + if(new_len) + var/list/message = list(span_bold("Scan results (Unstored Only):")) + for(var/text in new_fibers) + message += span_notice("Fiber: [text]") + if(length(fibers) > max_storage) + message += span_boldwarning("Fiber storage full.") + for(var/text in new_prints) + message += span_notice("Fingerprint: [text]") + if(length(fingerprints) > max_storage) + message += span_boldwarning("Fingerprint storage full.") + to_chat(user, boxed_message(jointext(message, "\n")), type = MESSAGE_TYPE_INFO) + if(length(fingerprints) < max_storage) + while(length(fingerprints) + length(new_prints) > max_storage) + var/to_remove = tgui_input_list(user, "Too many prints, cancel to discard all", "What to discard", new_fibers) + if(isnull(to_remove)) + return ITEM_INTERACT_FAILURE + new_prints -= to_remove + clear_values(new_prints) + fingerprints += new_prints + for(var/fingerprint in fingerprints) + fingerprints[fingerprint] = get_name_from_fingerprint(fingerprint) + if(length(fibers) < max_storage) + while(length(fibers) + length(new_fibers) > max_storage) + var/to_remove = tgui_input_list(user, "Too many prints, cancel to discard all", "What to discard", new_fibers) + if(isnull(to_remove)) + return ITEM_INTERACT_FAILURE + new_fibers -= to_remove + clear_values(new_fibers) + fibers += new_fibers + return ITEM_INTERACT_SUCCESS + +/obj/item/forensics_spoofer/proc/tamper(atom/target, mob/living/user, do_fibers = FALSE) + do_fake_scan(target, user) + if((!do_fibers && isnull(chosen_fingerprint)) || (do_fibers && isnull(chosen_fiber))) + balloon_alert(user, "no [do_fibers ? "fiber" : "fingerprint"] selected!") // we CAN automatically select it but if they dont have it selected then they likely didnt know of it in the first place so they learn it now + return ITEM_INTERACT_FAILURE + if(!COOLDOWN_FINISHED(src, tamper_cooldown)) + balloon_alert(user, "please wait!") + return ITEM_INTERACT_FAILURE + if(!isnull(target.forensics) && LAZYFIND(do_fibers ? target.forensics.fibers : target.forensics.fingerprints, do_fibers ? chosen_fiber : chosen_fingerprint)) + balloon_alert(user, "already present!") + return ITEM_INTERACT_FAILURE + + if(do_fibers) + target.add_fiber_list(list(chosen_fiber)) + user.log_message("has tampered with the fingerprints/fibers of [src]. Added [chosen_fiber]", LOG_ATTACK) + else + target.add_fingerprint_list(list(chosen_fingerprint)) + user.log_message("has tampered with the fingerprints/fibers of [src]. Added [chosen_fingerprint]", LOG_ATTACK) + + target.balloon_alert(user, "[do_fibers ? "fiber" : "fingerprint"] added") + target.add_hiddenprint(user) + COOLDOWN_START(src, tamper_cooldown, tamper_cooldown_time) + + return ITEM_INTERACT_SUCCESS + +/obj/item/forensics_spoofer/proc/get_name_from_fingerprint(fingerprint) + . = "Unknown" + for(var/datum/record/crew/player_record as anything in GLOB.manifest.general) + if(player_record.fingerprint != fingerprint) + continue + return player_record.name + +/obj/item/forensics_spoofer/ui_state(mob/user) + return GLOB.hands_state + +/obj/item/forensics_spoofer/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ForensicsSpoofer", name) + ui.open() + +/obj/item/forensics_spoofer/ui_static_data(mob/user) + . = list( + "max_storage" = max_storage, + ) + +/obj/item/forensics_spoofer/ui_data(mob/user) + return list( + "scanmode" = scan_mode, + "silent" = silent_mode, + "fibers" = fibers, + "fingerprints" = fingerprints, + "chosen_fiber" = chosen_fiber, + "chosen_fingerprint" = chosen_fingerprint, + ) + +/obj/item/forensics_spoofer/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + if(!isnull(params["chosen"])) //fiber/print actions + var/chosen = params["chosen"] + switch(action) + if("delete") + if(chosen in fibers) + if(chosen_fiber == chosen) + chosen_fiber = null + fibers -= chosen + else + if(chosen_fingerprint == chosen) + chosen_fingerprint = null + fingerprints -= chosen + return TRUE + if("choose") + var/is_fiber = !!(chosen in fibers) + chosen_fiber = is_fiber ? chosen : null + chosen_fingerprint = is_fiber ? null : chosen + return TRUE + if("make_note") + if(chosen in fibers) + fibers[chosen] = params["note"] + else + fingerprints[chosen] = params["note"] + return TRUE + else + switch(action) + if("scanmode") + scan_mode = !scan_mode + return TRUE + if("stealth") + silent_mode = !silent_mode + return TRUE diff --git a/code/game/objects/items/grenades/plastic.dm b/code/game/objects/items/grenades/plastic.dm index c9090912cc7e6..f50bcf4351187 100644 --- a/code/game/objects/items/grenades/plastic.dm +++ b/code/game/objects/items/grenades/plastic.dm @@ -143,8 +143,7 @@ var/obj/item/thrown_weapon = bomb_target thrown_weapon.throw_speed = max(1, (thrown_weapon.throw_speed - 3)) thrown_weapon.throw_range = max(1, (thrown_weapon.throw_range - 3)) - if(thrown_weapon.get_embed()) - thrown_weapon.set_embed(thrown_weapon.get_embed().generate_with_values(embed_chance = 0)) + thrown_weapon.get_embed()?.embed_chance = 0 else if(isliving(bomb_target)) plastic_overlay.layer = FLOAT_LAYER diff --git a/code/game/objects/items/knives.dm b/code/game/objects/items/knives.dm index 807fd7d96b810..aa80eafaf4bb7 100644 --- a/code/game/objects/items/knives.dm +++ b/code/game/objects/items/knives.dm @@ -145,14 +145,14 @@ icon_state = "buckknife" worn_icon_state = "buckknife" icon_angle = -45 - embed_type = /datum/embed_data/combat_knife + embed_type = /datum/embedding/combat_knife force = 20 throwforce = 20 attack_verb_continuous = list("slashes", "stabs", "slices", "tears", "lacerates", "rips", "cuts") attack_verb_simple = list("slash", "stab", "slice", "tear", "lacerate", "rip", "cut") slot_flags = ITEM_SLOT_MASK -/datum/embed_data/combat_knife +/datum/embedding/combat_knife pain_mult = 4 embed_chance = 65 fall_chance = 10 @@ -185,7 +185,7 @@ desc = "A hunting grade survival knife." icon_state = "survivalknife" worn_icon_state = "survivalknife" - embed_type = /datum/embed_data/combat_knife/weak + embed_type = /datum/embedding/combat_knife/weak force = 15 throwforce = 15 @@ -197,7 +197,7 @@ lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi' righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi' inhand_icon_state = "rootshiv" - embed_type = /datum/embed_data/combat_knife/weak + embed_type = /datum/embedding/combat_knife/weak force = 15 throwforce = 15 @@ -209,13 +209,13 @@ worn_icon_state = "bone_dagger" lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi' righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi' - embed_type = /datum/embed_data/combat_knife/weak + embed_type = /datum/embedding/combat_knife/weak obj_flags = parent_type::obj_flags & ~CONDUCTS_ELECTRICITY force = 15 throwforce = 15 custom_materials = null -/datum/embed_data/combat_knife/weak +/datum/embedding/combat_knife/weak embed_chance = 35 /obj/item/knife/combat/cyborg diff --git a/code/game/objects/items/melee/energy.dm b/code/game/objects/items/melee/energy.dm index 00f5bd38d42cf..9614069fb2b0c 100644 --- a/code/game/objects/items/melee/energy.dm +++ b/code/game/objects/items/melee/energy.dm @@ -170,7 +170,7 @@ return (BRUTELOSS|FIRELOSS) /// Energy swords. -/datum/embed_data/esword +/datum/embedding/esword embed_chance = 75 impact_pain_mult = 10 @@ -190,7 +190,7 @@ armour_penetration = 35 block_chance = 50 block_sound = 'sound/items/weapons/block_blade.ogg' - embed_type = /datum/embed_data/esword + embed_type = /datum/embedding/esword var/list/alt_continuous = list("stabs", "pierces", "impales") var/list/alt_simple = list("stab", "pierce", "impale") diff --git a/code/game/objects/items/robot/items/food.dm b/code/game/objects/items/robot/items/food.dm index 6eba8e8fa760b..90caf96705342 100644 --- a/code/game/objects/items/robot/items/food.dm +++ b/code/game/objects/items/robot/items/food.dm @@ -237,12 +237,12 @@ var/head_color /obj/projectile/bullet/lollipop/harmful - embed_type = /datum/embed_data/lollipop + embed_type = /datum/embedding/lollipop damage = 10 shrapnel_type = /obj/item/food/lollipop/cyborg embed_falloff_tile = 0 -/datum/embed_data/lollipop +/datum/embedding/lollipop embed_chance = 35 fall_chance = 2 jostle_chance = 0 diff --git a/code/game/objects/items/shrapnel.dm b/code/game/objects/items/shrapnel.dm index a4adc353db3f7..cb0cea91ed677 100644 --- a/code/game/objects/items/shrapnel.dm +++ b/code/game/objects/items/shrapnel.dm @@ -34,9 +34,9 @@ ignore_range_hit_prone_targets = TRUE sharpness = SHARP_EDGED wound_bonus = 30 - embed_type = /datum/embed_data/shrapnel + embed_type = /datum/embedding/shrapnel -/datum/embed_data/shrapnel +/datum/embedding/shrapnel embed_chance = 70 ignore_throwspeed_threshold = TRUE fall_chance = 1 @@ -75,9 +75,9 @@ ricochet_incidence_leeway = 0 embed_falloff_tile = -2 shrapnel_type = /obj/item/shrapnel/stingball - embed_type = /datum/embed_data/stingball + embed_type = /datum/embedding/stingball -/datum/embed_data/stingball +/datum/embedding/stingball embed_chance = 55 fall_chance = 2 jostle_chance = 7 @@ -107,11 +107,11 @@ ricochets_max = 2 ricochet_chance = 140 shrapnel_type = /obj/item/shrapnel/capmine - embed_type = /datum/embed_data/capmine + embed_type = /datum/embedding/capmine wound_falloff_tile = 0 embed_falloff_tile = 0 -/datum/embed_data/capmine +/datum/embedding/capmine embed_chance = 90 fall_chance = 3 jostle_chance = 7 diff --git a/code/game/objects/items/spear.dm b/code/game/objects/items/spear.dm index 0c7bc8d819f38..2cef649a1ed0c 100644 --- a/code/game/objects/items/spear.dm +++ b/code/game/objects/items/spear.dm @@ -13,7 +13,7 @@ throwforce = 20 throw_speed = 4 demolition_mod = 0.75 - embed_type = /datum/embed_data/spear + embed_type = /datum/embedding/spear armour_penetration = 10 custom_materials = list(/datum/material/iron = HALF_SHEET_MATERIAL_AMOUNT, /datum/material/glass= HALF_SHEET_MATERIAL_AMOUNT * 2) hitsound = 'sound/items/weapons/bladeslice.ogg' @@ -33,7 +33,7 @@ /// How much damage to do wielded var/force_wielded = 18 -/datum/embed_data/spear +/datum/embedding/spear impact_pain_mult = 2 remove_pain_mult = 4 jostle_chance = 2.5 diff --git a/code/game/objects/items/stacks/rods.dm b/code/game/objects/items/stacks/rods.dm index 0c066f80f0d29..0dc739c8a6eb8 100644 --- a/code/game/objects/items/stacks/rods.dm +++ b/code/game/objects/items/stacks/rods.dm @@ -33,7 +33,7 @@ GLOBAL_LIST_INIT(rod_recipes, list ( \ attack_verb_continuous = list("hits", "bludgeons", "whacks") attack_verb_simple = list("hit", "bludgeon", "whack") hitsound = 'sound/items/weapons/gun/general/grenade_launch.ogg' - embed_type = /datum/embed_data/rods + embed_type = /datum/embedding/rods novariants = TRUE matter_amount = 2 cost = HALF_SHEET_MATERIAL_AMOUNT @@ -43,7 +43,7 @@ GLOBAL_LIST_INIT(rod_recipes, list ( \ drop_sound = 'sound/items/handling/materials/metal_drop.ogg' sound_vary = TRUE -/datum/embed_data/rods +/datum/embedding/rods embed_chance = 50 /obj/item/stack/rods/suicide_act(mob/living/carbon/user) diff --git a/code/game/objects/items/stacks/sheets/glass.dm b/code/game/objects/items/stacks/sheets/glass.dm index d5c0006831039..03a94dc0d4119 100644 --- a/code/game/objects/items/stacks/sheets/glass.dm +++ b/code/game/objects/items/stacks/sheets/glass.dm @@ -306,12 +306,12 @@ GLOBAL_LIST_INIT(plastitaniumglass_recipes, list( var/shiv_type = /obj/item/knife/shiv var/craft_time = 3.5 SECONDS var/obj/item/stack/sheet/weld_material = /obj/item/stack/sheet/glass - embed_type = /datum/embed_data/shard + embed_type = /datum/embedding/shard -/datum/embed_data/shard +/datum/embedding/shard embed_chance = 65 -/datum/embed_data/glass_candy +/datum/embedding/glass_candy embed_chance = 100 ignore_throwspeed_threshold = TRUE impact_pain_mult = 1 diff --git a/code/game/objects/items/stacks/tape.dm b/code/game/objects/items/stacks/tape.dm index a1394bbad4fd9..24805aec64547 100644 --- a/code/game/objects/items/stacks/tape.dm +++ b/code/game/objects/items/stacks/tape.dm @@ -14,16 +14,17 @@ grind_results = list(/datum/reagent/cellulose = 5) splint_factor = 0.65 merge_type = /obj/item/stack/sticky_tape - var/conferred_embed = /datum/embed_data/sticky_tape + var/conferred_embed = /datum/embedding/sticky_tape ///The tape type you get when ripping off a piece of tape. var/obj/tape_gag = /obj/item/clothing/mask/muzzle/tape greyscale_config = /datum/greyscale_config/tape greyscale_colors = "#B2B2B2#BD6A62" -/datum/embed_data/sticky_tape +/datum/embedding/sticky_tape pain_mult = 0 jostle_pain_mult = 0 ignore_throwspeed_threshold = 0 + immune_traits = null /obj/item/stack/sticky_tape/attack_hand(mob/user, list/modifiers) if(user.get_inactive_held_item() == src) @@ -55,27 +56,29 @@ user.visible_message(span_notice("[user] begins wrapping [target] with [src]."), span_notice("You begin wrapping [target] with [src].")) playsound(user, 'sound/items/duct_tape/duct_tape_rip.ogg', 50, TRUE) - if(do_after(user, 3 SECONDS, target=target)) - playsound(user, 'sound/items/duct_tape/duct_tape_snap.ogg', 50, TRUE) - use(1) - if(istype(target, /obj/item/clothing/gloves/fingerless)) - var/obj/item/clothing/gloves/tackler/offbrand/O = new /obj/item/clothing/gloves/tackler/offbrand - to_chat(user, span_notice("You turn [target] into [O] with [src].")) - QDEL_NULL(target) - user.put_in_hands(O) - return ITEM_INTERACT_SUCCESS + if(!do_after(user, 3 SECONDS, target=target)) + return ITEM_INTERACT_BLOCKING + + playsound(user, 'sound/items/duct_tape/duct_tape_snap.ogg', 50, TRUE) + use(1) + if(istype(target, /obj/item/clothing/gloves/fingerless)) + var/obj/item/clothing/gloves/tackler/offbrand/O = new /obj/item/clothing/gloves/tackler/offbrand + to_chat(user, span_notice("You turn [target] into [O] with [src].")) + QDEL_NULL(target) + user.put_in_hands(O) + return ITEM_INTERACT_SUCCESS - if(target.get_embed() && target.get_embed().type == conferred_embed) - to_chat(user, span_warning("[target] is already coated in [src]!")) - return ITEM_INTERACT_BLOCKING + if(target.get_embed()?.type == conferred_embed) + to_chat(user, span_warning("[target] is already coated in [src]!")) + return ITEM_INTERACT_BLOCKING - target.set_embed(conferred_embed) - to_chat(user, span_notice("You finish wrapping [target] with [src].")) - target.name = "[prefix] [target.name]" + target.set_embed(conferred_embed) + to_chat(user, span_notice("You finish wrapping [target] with [src].")) + target.name = "[prefix] [target.name]" - if(isgrenade(target)) - var/obj/item/grenade/sticky_bomb = target - sticky_bomb.sticky = TRUE + if(isgrenade(target)) + var/obj/item/grenade/sticky_bomb = target + sticky_bomb.sticky = TRUE return ITEM_INTERACT_SUCCESS @@ -84,13 +87,13 @@ singular_name = "super sticky tape" desc = "Quite possibly the most mischevious substance in the galaxy. Use with extreme lack of caution." prefix = "super sticky" - conferred_embed = /datum/embed_data/sticky_tape/super + conferred_embed = /datum/embedding/sticky_tape/super splint_factor = 0.4 merge_type = /obj/item/stack/sticky_tape/super greyscale_colors = "#4D4D4D#75433F" tape_gag = /obj/item/clothing/mask/muzzle/tape/super -/datum/embed_data/sticky_tape/super +/datum/embedding/sticky_tape/super embed_chance = 100 fall_chance = 0.1 @@ -100,13 +103,13 @@ desc = "Used for sticking to things for sticking said things inside people." icon_state = "tape_spikes" prefix = "pointy" - conferred_embed = /datum/embed_data/pointy_tape + conferred_embed = /datum/embedding/pointy_tape merge_type = /obj/item/stack/sticky_tape/pointy greyscale_config = /datum/greyscale_config/tape/spikes greyscale_colors = "#E64539#808080#AD2F45" tape_gag = /obj/item/clothing/mask/muzzle/tape/pointy -/datum/embed_data/pointy_tape +/datum/embedding/pointy_tape ignore_throwspeed_threshold = TRUE /obj/item/stack/sticky_tape/pointy/super @@ -114,12 +117,12 @@ singular_name = "super pointy tape" desc = "You didn't know tape could look so sinister. Welcome to Space Station 13." prefix = "super pointy" - conferred_embed = /datum/embed_data/pointy_tape/super + conferred_embed = /datum/embedding/pointy_tape/super merge_type = /obj/item/stack/sticky_tape/pointy/super greyscale_colors = "#8C0A00#4F4F4F#300008" tape_gag = /obj/item/clothing/mask/muzzle/tape/pointy/super -/datum/embed_data/pointy_tape/super +/datum/embedding/pointy_tape/super embed_chance = 100 /obj/item/stack/sticky_tape/surgical @@ -127,14 +130,14 @@ singular_name = "surgical tape" desc = "Made for patching broken bones back together alongside bone gel, not for playing pranks." prefix = "surgical" - conferred_embed = /datum/embed_data/sticky_tape/surgical + conferred_embed = /datum/embedding/sticky_tape/surgical splint_factor = 0.5 custom_price = PAYCHECK_CREW merge_type = /obj/item/stack/sticky_tape/surgical greyscale_colors = "#70BAE7#BD6A62" tape_gag = /obj/item/clothing/mask/muzzle/tape/surgical -/datum/embed_data/sticky_tape/surgical +/datum/embedding/sticky_tape/surgical embed_chance = 30 /obj/item/stack/sticky_tape/surgical/get_surgery_tool_overlay(tray_extended) diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm index d945a3fd711db..1d0dd1fc2581d 100644 --- a/code/game/objects/items/storage/uplink_kits.dm +++ b/code/game/objects/items/storage/uplink_kits.dm @@ -348,6 +348,22 @@ new /obj/item/gun/ballistic/rifle/rebarxbow/syndie(src) new /obj/item/storage/bag/rebar_quiver/syndicate(src) +/obj/item/paper/syndicate_forensics_spoofer + name = "Forensics Spoofer Guide" + default_raw_text = {" + Forensics Spoofer Info:
+ The spoofer has two modes: SCAN which scans for fingerprints and fibers, and APPLY which applies the currently chosen fingerprint/fiber to your target.
+ The spoofer can only store 5 fingerprints and 5 fibers, and may not store or report fibers/prints already stored. Additionally, it taps into the stations network to associate scanned fingerprints with names.
+ The spoofer will make the same sounds and sights as a forensics scanner, when silent mode is off.
+ "} + +/obj/item/storage/box/syndie_kit/forensics_spoofer + name = "forensics spoofing kit" + +/obj/item/storage/box/syndie_kit/forensics_spoofer/PopulateContents() + new /obj/item/forensics_spoofer(src) + new /obj/item/paper/syndicate_forensics_spoofer(src) + /obj/item/storage/box/syndie_kit/origami_bundle name = "origami kit" desc = "A box full of a number of rather masterfully engineered paper planes and a manual on \"The Art of Origami\"." diff --git a/code/game/objects/items/tail_pin.dm b/code/game/objects/items/tail_pin.dm index dc2ffaefea0a9..08e7d9c29efac 100644 --- a/code/game/objects/items/tail_pin.dm +++ b/code/game/objects/items/tail_pin.dm @@ -14,9 +14,9 @@ sharpness = SHARP_POINTY max_integrity = 200 layer = CORGI_ASS_PIN_LAYER - embed_type = /datum/embed_data/corgi_pin + embed_type = /datum/embedding/corgi_pin -/datum/embed_data/corgi_pin +/datum/embedding/corgi_pin pain_chance = 0 jostle_pain_mult = 0 ignore_throwspeed_threshold = TRUE diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm index 26c649e6fac5b..0fb14d0693cdb 100644 --- a/code/game/objects/items/weaponry.dm +++ b/code/game/objects/items/weaponry.dm @@ -417,7 +417,7 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 force = 2 throwforce = 10 //10 + 2 (WEIGHT_CLASS_SMALL) * 4 (EMBEDDED_IMPACT_PAIN_MULTIPLIER) = 18 damage on hit due to guaranteed embedding throw_speed = 4 - embed_type = /datum/embed_data/throwing_star + embed_type = /datum/embedding/throwing_star armour_penetration = 40 w_class = WEIGHT_CLASS_SMALL @@ -425,7 +425,7 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 custom_materials = list(/datum/material/iron= SMALL_MATERIAL_AMOUNT * 5, /datum/material/glass= SMALL_MATERIAL_AMOUNT * 5) resistance_flags = FIRE_PROOF -/datum/embed_data/throwing_star +/datum/embedding/throwing_star pain_mult = 4 embed_chance = 100 fall_chance = 0 @@ -434,9 +434,9 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 name = "shock throwing star" desc = "An aerodynamic disc designed to cause excruciating pain when stuck inside fleeing targets, hopefully without causing fatal harm." throwforce = 5 - embed_type = /datum/embed_data/throwing_star/stamina + embed_type = /datum/embedding/throwing_star/stamina -/datum/embed_data/throwing_star/stamina +/datum/embedding/throwing_star/stamina pain_mult = 5 jostle_chance = 10 pain_stam_pct = 0.8 @@ -448,9 +448,9 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 sharpness = NONE force = 0 throwforce = 0 - embed_type = /datum/embed_data/throwing_star/toy + embed_type = /datum/embedding/throwing_star/toy -/datum/embed_data/throwing_star/toy +/datum/embedding/throwing_star/toy pain_mult = 0 jostle_pain_mult = 0 @@ -1234,7 +1234,7 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 throwforce = 25 throw_speed = 4 attack_speed = CLICK_CD_HYPER_RAPID - embed_type = /datum/embed_data/hfr_blade + embed_type = /datum/embedding/hfr_blade block_chance = 25 block_sound = 'sound/items/weapons/parry.ogg' sharpness = SHARP_EDGED @@ -1249,7 +1249,7 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 /// The previous target we attacked var/datum/weakref/previous_target -/datum/embed_data/hfr_blade +/datum/embedding/hfr_blade embed_chance = 100 /obj/item/highfrequencyblade/Initialize(mapload) diff --git a/code/game/objects/structures/ai_core.dm b/code/game/objects/structures/ai_core.dm index 4a5ee234c7742..cef73b0fbfd88 100644 --- a/code/game/objects/structures/ai_core.dm +++ b/code/game/objects/structures/ai_core.dm @@ -77,10 +77,13 @@ icon_state = "ai-empty" anchored = TRUE state = AI_READY_CORE + var/mob/living/silicon/ai/attached_ai -/obj/structure/ai_core/deactivated/Initialize(mapload, skip_mmi_creation = FALSE, posibrain = FALSE) +/obj/structure/ai_core/deactivated/Initialize(mapload, skip_mmi_creation = FALSE, posibrain = FALSE, linked_ai) . = ..() circuit = new(src) + if(linked_ai) + attached_ai = linked_ai if(skip_mmi_creation) return if(posibrain) @@ -90,6 +93,16 @@ core_mmi.brain = new(core_mmi) core_mmi.update_appearance() +/obj/structure/ai_core/deactivated/Destroy() + if(attached_ai) + attached_ai.linked_core = null + attached_ai = null + . = ..() + +/obj/structure/ai_core/deactivated/proc/disable_doomsday(datum/source) + SIGNAL_HANDLER + attached_ai.ShutOffDoomsdayDevice() + /obj/structure/ai_core/latejoin_inactive name = "networked AI core" desc = "This AI core is connected by bluespace transmitters to NTNet, allowing for an AI personality to be downloaded to it on the fly mid-shift." 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 e4488b0b7f436..3507dcca41fb3 100644 --- a/code/game/objects/structures/crates_lockers/closets/secure/security.dm +++ b/code/game/objects/structures/crates_lockers/closets/secure/security.dm @@ -77,7 +77,6 @@ // Traitor steal objectives new /obj/item/gun/energy/e_gun/hos(src) new /obj/item/pinpointer/nuke(src) - new /obj/item/gun/ballistic/shotgun/automatic/combat/compact(src) /obj/structure/closet/secure_closet/warden name = "warden's locker" @@ -96,6 +95,13 @@ new /obj/item/flashlight/seclite(src) new /obj/item/door_remote/head_of_security(src) + +/obj/structure/closet/secure_closet/warden/populate_contents_immediate() + . = ..() + + // Traitor steal objective + new /obj/item/gun/ballistic/shotgun/automatic/combat/compact(src) + /obj/structure/closet/secure_closet/security name = "security officer's locker" icon_state = "sec" diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm index 9adb4f75a2f94..22517603ef3fc 100644 --- a/code/game/objects/structures/tables_racks.dm +++ b/code/game/objects/structures/tables_racks.dm @@ -382,7 +382,6 @@ /obj/structure/table/rolling/Initialize(mapload) . = ..() AddElement(/datum/element/noisy_movement) - RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(on_our_moved)) /obj/structure/table/rolling/Destroy() for(var/item in attached_items) @@ -398,13 +397,14 @@ if(rable.loaded) to_chat(user, span_warning("You already have \a [rable.loaded] docked!")) return ITEM_INTERACT_FAILURE - if(locate(/mob/living) in get_turf(src)) + + if(locate(/mob/living) in loc.get_all_contents()) to_chat(user, span_warning("You can't collect \the [src] with that much on top!")) return ITEM_INTERACT_FAILURE rable.loaded = src forceMove(rable) - user.visible_message(span_notice("[user] collects \the [src]."), span_notice("you collect \the [src].")) + user.visible_message(span_notice("[user] collects \the [src]."), span_notice("You collect \the [src].")) return ITEM_INTERACT_SUCCESS /obj/structure/table/rolling/AfterPutItemOnTable(obj/item/thing, mob/living/user) @@ -423,8 +423,9 @@ clear_item_reference(thing) /// Handles movement of the table itself, as well as moving along any atoms we have on our surface. -/obj/structure/table/rolling/proc/on_our_moved(datum/source, atom/old_loc, dir, forced, list/old_locs, momentum_change) - SIGNAL_HANDLER +/obj/structure/table/rolling/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + if(isnull(loc)) // aw hell naw return diff --git a/code/game/turfs/baseturfs.dm b/code/game/turfs/baseturfs.dm index ad016b6344775..b65d1a6b71db4 100644 --- a/code/game/turfs/baseturfs.dm +++ b/code/game/turfs/baseturfs.dm @@ -112,17 +112,16 @@ /// Replaces all instances of needle_type in baseturfs with replacement_type /turf/proc/replace_baseturf(needle_type, replacement_type) if (islist(baseturfs)) - var/list/new_baseturfs + var/list/new_baseturfs = baseturfs.Copy() - while (TRUE) - var/found_index = baseturfs.Find(needle_type) + for(var/base_i in 1 to length(new_baseturfs)) + var/found_index = new_baseturfs.Find(needle_type) if (found_index == 0) break - new_baseturfs ||= baseturfs.Copy() new_baseturfs[found_index] = replacement_type - if (!isnull(new_baseturfs)) + if (length(new_baseturfs)) baseturfs = baseturfs_string_list(new_baseturfs, src) else if (baseturfs == needle_type) baseturfs = replacement_type diff --git a/code/modules/admin/verbs/admin.dm b/code/modules/admin/verbs/admin.dm index 629423e713faa..1688f34d2024f 100644 --- a/code/modules/admin/verbs/admin.dm +++ b/code/modules/admin/verbs/admin.dm @@ -144,7 +144,7 @@ ADMIN_VERB(cmd_admin_check_player_exp, R_ADMIN, "Player Playtime", "View player /////////////////////////////////////////////////////////////////////////////////////////////// -ADMIN_VERB(drop_everything, R_ADMIN, "Drop Everything", ADMIN_VERB_NO_DESCRIPTION, ADMIN_CATEGORY_HIDDEN, mob/dropee in GLOB.mob_list) +ADMIN_VERB(drop_everything, R_ADMIN, "Drop Everything", ADMIN_VERB_NO_DESCRIPTION, ADMIN_CATEGORY_HIDDEN, mob/living/dropee in GLOB.mob_list) var/confirm = tgui_alert(user, "Make [dropee] drop everything?", "Message", list("Yes", "No")) if(confirm != "Yes") return diff --git a/code/modules/antagonists/blob/blobstrains/_blobstrain.dm b/code/modules/antagonists/blob/blobstrains/_blobstrain.dm index 54d393780b25b..7215bdf3ae85f 100644 --- a/code/modules/antagonists/blob/blobstrains/_blobstrain.dm +++ b/code/modules/antagonists/blob/blobstrains/_blobstrain.dm @@ -22,8 +22,7 @@ GLOBAL_LIST_INIT(valid_blobstrains, subtypesof(/datum/blobstrain) - list(/datum/ var/message_living = null /// Stores world.time to figure out when to next give resources var/resource_delay = 0 - /// For blob-mobs and extinguishing-based effects - var/fire_based = FALSE + ///The blob overmind eye mob used to control the spread var/mob/eye/blob/overmind /// The amount of health regenned on core_process var/base_core_regen = BLOB_CORE_HP_REGEN diff --git a/code/modules/antagonists/blob/blobstrains/blazing_oil.dm b/code/modules/antagonists/blob/blobstrains/blazing_oil.dm index f01f2c2faadc6..c33d0b6e92961 100644 --- a/code/modules/antagonists/blob/blobstrains/blazing_oil.dm +++ b/code/modules/antagonists/blob/blobstrains/blazing_oil.dm @@ -12,7 +12,6 @@ message = "The blob splashes you with burning oil" message_living = ", and you feel your skin char and melt" reagent = /datum/reagent/blob/blazing_oil - fire_based = TRUE /datum/blobstrain/reagent/blazing_oil/extinguish_reaction(obj/structure/blob/B) B.take_damage(4.5, BURN, ENERGY) diff --git a/code/modules/antagonists/heretic/structures/carving_knife.dm b/code/modules/antagonists/heretic/structures/carving_knife.dm index 6c6789b95eb28..6e1bb8c05e995 100644 --- a/code/modules/antagonists/heretic/structures/carving_knife.dm +++ b/code/modules/antagonists/heretic/structures/carving_knife.dm @@ -16,7 +16,7 @@ attack_verb_continuous = list("attacks", "slashes", "slices", "tears", "lacerates", "rips", "dices", "rends") attack_verb_simple = list("attack", "slash", "slice", "tear", "lacerate", "rip", "dice", "rend") actions_types = list(/datum/action/item_action/rune_shatter) - embed_type = /datum/embed_data/rune_carver + embed_type = /datum/embedding/rune_carver /// Whether we're currently drawing a rune var/drawing = FALSE @@ -35,7 +35,7 @@ alt_simple = string_list(alt_simple) AddComponent(/datum/component/alternative_sharpness, SHARP_POINTY, alt_continuous, alt_simple) -/datum/embed_data/rune_carver +/datum/embedding/rune_carver ignore_throwspeed_threshold = TRUE embed_chance = 75 jostle_chance = 2 diff --git a/code/modules/antagonists/malf_ai/malf_ai_modules.dm b/code/modules/antagonists/malf_ai/malf_ai_modules.dm index 339956537849a..efde4125b8a23 100644 --- a/code/modules/antagonists/malf_ai/malf_ai_modules.dm +++ b/code/modules/antagonists/malf_ai/malf_ai_modules.dm @@ -904,12 +904,12 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf)) /datum/ai_module/malf/upgrade/mecha_domination name = "Unlock Mech Domination" description = "Позволяет вам взломать бортовой компьютер меха, загружая в него свои процессы, а также выкидывая любых пассажиров. \ - Не позволяйте меху покинуть станцию или быть уничтоженным. \ - Улучшение происходит сразу после покупки." + Улучшение происходит сразу после покупки. Не позволяйте меху покинуть станцию или быть уничтоженным. \ + Если ваше ядро уничтожено, вы потеряете соединение с устройством Судного дня и отчёт остановится." cost = 30 upgrade = TRUE - unlock_text = span_notice("Вирусный пакет скомпилирован. Выберите цель в любой момент. Вы должны оставаться на станции всегда. \ - Потеря сигнала приведёт к полной блокировке системы.") + unlock_text = span_notice("Вирусный пакет скомпилирован. Выберите цель в любой момент. Вы должны постоянно оставаться на станции. \ + Потеря сигнала приведёт к полной блокировке системы. Если ваше неактивное ядро уничтожено, вы потеряете соединение с устройством Судного дня и отчёт остановится.") unlock_sound = 'sound/vehicles/mecha/nominal.ogg' /datum/ai_module/malf/upgrade/mecha_domination/upgrade(mob/living/silicon/ai/AI) diff --git a/code/modules/antagonists/nukeop/equipment/pinpointer.dm b/code/modules/antagonists/nukeop/equipment/pinpointer.dm index 82113fb31be2b..ebdbdbe86980b 100644 --- a/code/modules/antagonists/nukeop/equipment/pinpointer.dm +++ b/code/modules/antagonists/nukeop/equipment/pinpointer.dm @@ -43,7 +43,10 @@ for(var/V in GLOB.ai_list) var/mob/living/silicon/ai/A = V if(A.nuking) - target = A + if(A.linked_core) + target = A.linked_core + else + target = A for(var/obj/machinery/power/apc/apc as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/power/apc)) if(apc.malfhack && apc.occupier) target = apc diff --git a/code/modules/antagonists/traitor/objectives/destroy_item.dm b/code/modules/antagonists/traitor/objectives/destroy_item.dm index e1339475b9e82..f91e1cfe62432 100644 --- a/code/modules/antagonists/traitor/objectives/destroy_item.dm +++ b/code/modules/antagonists/traitor/objectives/destroy_item.dm @@ -1,6 +1,6 @@ /datum/traitor_objective/destroy_item name = "Найдите и уничтожьте %ITEM%." - description = "Найдите %ITEM% и уничтожьте его любой ценой. Мы не можем позволить, чтобы экипаж станции владел %ITEM%, это совершенно не в наших интересах." + description = "Найдите %ITEM% и уничтожьте его любой ценой. Мы не можем позволить, чтобы экипаж станции владел %ITEM%, это совершенно не в наших интересах. %METHOD%" var/list/possible_items = list() /// The current target item that we are stealing. @@ -29,7 +29,6 @@ progression_minimum = 40 MINUTES progression_reward = 15 MINUTES telecrystal_reward = list(6, 9) - possible_items = list( /datum/objective_item/steal/blackbox, ) @@ -77,6 +76,10 @@ special_equipment = target_item.special_equipment var/obj/steal_target = target_item.targetitem // BANDASTATION EDIT ADDITION replace_in_name("%ITEM%", declent_ru_initial(steal_target::name, ACCUSATIVE, target_item.name)) + if(target_item.destruction_method != null) + replace_in_name("%METHOD%", target_item.destruction_method) + else + replace_in_name("%METHOD%", "This item can be destroyed normally, such as by using a recycler, found in disposals.") AddComponent(/datum/component/traitor_objective_mind_tracker, generating_for, \ signals = list(COMSIG_MOB_EQUIPPED_ITEM = PROC_REF(on_item_pickup))) return TRUE diff --git a/code/modules/cargo/packs/security.dm b/code/modules/cargo/packs/security.dm index b233ecd78dd38..612e9f5b3f92c 100644 --- a/code/modules/cargo/packs/security.dm +++ b/code/modules/cargo/packs/security.dm @@ -250,7 +250,7 @@ /datum/supply_pack/security/armory/battle_rifle name = "NT BR-38 Crate" - desc = "An experimental energy-based ballistc battle rifle. Only available to \ + desc = "An experimental energy-based ballistic battle rifle. Only available to \ Nanotrasen stations for security purposes. DO NOT RESELL TO OUTSIDE COMPANIES. \ Contains three NT BR-38 rifles and three magazines containing .38 Standard." cost = CARGO_CRATE_VALUE * 100 diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 062a402cc3f23..36ca38124a271 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -249,6 +249,9 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( GLOB.clients += src GLOB.directory[ckey] = src + if(byond_version >= 516) + winset(src, null, list("browser-options" = "find,refresh,byondstorage")) + // Instantiate stat panel stat_panel = new(src, "statbrowser") stat_panel.subscribe(src, PROC_REF(on_stat_panel_message)) diff --git a/code/modules/clothing/glasses/hud.dm b/code/modules/clothing/glasses/hud.dm index 54ad9d2658423..a154c571c650b 100644 --- a/code/modules/clothing/glasses/hud.dm +++ b/code/modules/clothing/glasses/hud.dm @@ -2,6 +2,9 @@ name = "HUD" desc = "A heads-up display that provides important info in (almost) real time." flags_1 = null //doesn't protect eyes because it's a monocle, duh + actions_types = list(/datum/action/item_action/toggle_wearable_hud) + /// Whether the HUD info is on or off + var/display_active = TRUE /obj/item/clothing/glasses/hud/emp_act(severity) . = ..() @@ -32,6 +35,23 @@ user.say("WHY IS THERE A BAR ON MY HEAD?!!") return OXYLOSS +/obj/item/clothing/glasses/hud/equipped(mob/living/user, slot) + . = ..() + display_active = TRUE + +/obj/item/clothing/glasses/hud/proc/toggle_hud_display(mob/living/carbon/eye_owner) + if(display_active) + display_active = FALSE + for(var/hud_trait as anything in clothing_traits) + REMOVE_CLOTHING_TRAIT(eye_owner, hud_trait) + balloon_alert(eye_owner, "hud disabled") + return + + display_active = TRUE + for(var/hud_trait as anything in clothing_traits) + ADD_CLOTHING_TRAIT(eye_owner, hud_trait) + balloon_alert(eye_owner, "hud enabled") + /obj/item/clothing/glasses/hud/health name = "health scanner HUD" desc = "A heads-up display that scans the humanoids in view and provides accurate data about their health status." diff --git a/code/modules/events/wizard/embeddies.dm b/code/modules/events/wizard/embeddies.dm index 8b4568942154a..49f4fbc5afe38 100644 --- a/code/modules/events/wizard/embeddies.dm +++ b/code/modules/events/wizard/embeddies.dm @@ -43,10 +43,10 @@ GLOBAL_DATUM(global_funny_embedding, /datum/global_funny_embedding) * Makes every item in the world embed when thrown, but also hooks into global signals for new items created to also bless them with embed-ability(??). */ /datum/global_funny_embedding - var/embed_type = /datum/embed_data/global_funny + var/embed_type = /datum/embedding/global_funny var/prefix = "error" -/datum/embed_data/global_funny +/datum/embedding/global_funny ignore_throwspeed_threshold = TRUE /datum/global_funny_embedding/New() @@ -91,9 +91,9 @@ GLOBAL_DATUM(global_funny_embedding, /datum/global_funny_embedding) ///everything will be... sticky? sure, why not /datum/global_funny_embedding/sticky - embed_type = /datum/embed_data/global_funny/sticky + embed_type = /datum/embedding/global_funny/sticky prefix = "sticky" -/datum/embed_data/global_funny/sticky +/datum/embedding/global_funny/sticky pain_mult = 0 jostle_pain_mult = 0 diff --git a/code/modules/experisci/experiment/handlers/experiment_handler.dm b/code/modules/experisci/experiment/handlers/experiment_handler.dm index 048443345bc1e..8ab57c0872e0b 100644 --- a/code/modules/experisci/experiment/handlers/experiment_handler.dm +++ b/code/modules/experisci/experiment/handlers/experiment_handler.dm @@ -148,7 +148,6 @@ for(var/scan_target in scanned_atoms) if(action_experiment(source, scan_target)) successful_scan = TRUE - break if(successful_scan) playsound(our_scanner, 'sound/machines/ping.ogg', 25) to_chat(our_scanner, span_notice("Сканирование успешно.")) diff --git a/code/modules/fishing/fish/types/rift.dm b/code/modules/fishing/fish/types/rift.dm index ce7046c1f0c3a..c06de438b4f5b 100644 --- a/code/modules/fishing/fish/types/rift.dm +++ b/code/modules/fishing/fish/types/rift.dm @@ -14,7 +14,7 @@ throwforce = 11 throw_range = 8 throw_speed = 4 - embed_type = /datum/embed_data/chrystarfish + embed_type = /datum/embedding/chrystarfish attack_verb_continuous = list("stabs", "jabs") attack_verb_simple = list("stab", "jab") hitsound = SFX_SHATTER @@ -44,7 +44,7 @@ electrogenesis_power = 9 MEGA JOULES // Basically a ninja star that's highly likely to embed and teleports you around if you don't stop to remove it. However it doesn't deal that much damage! -/datum/embed_data/chrystarfish +/datum/embedding/chrystarfish pain_mult = 1 embed_chance = 85 fall_chance = 3 @@ -55,16 +55,10 @@ ignore_throwspeed_threshold = TRUE // basically shaped like a shuriken jostle_chance = 15 jostle_pain_mult = 1 - // about to be set! - jostle_callback = null -/datum/embed_data/chrystarfish/New() - ..() - jostle_callback = CALLBACK(src, PROC_REF(teleport)) - -/datum/embed_data/chrystarfish/proc/teleport(mob/victim, atom/embed_parent, datum/embed_data/real_data) - do_teleport(victim, get_turf(victim), 3, asoundin = 'sound/effects/phasein.ogg', channel = TELEPORT_CHANNEL_BLUESPACE) - victim.visible_message(span_danger("[victim] teleports as [embed_parent] jostles inside [victim.p_them()]!")) +/datum/embedding/chrystarfish/jostle_effects() + do_teleport(owner, get_turf(owner), 3, asoundin = 'sound/effects/phasein.ogg', channel = TELEPORT_CHANNEL_BLUESPACE) + owner.visible_message(span_danger("[owner] teleports as [parent] jostles inside of [owner.p_them()]!")) /obj/item/fish/starfish/chrystarfish/set_status(new_status, silent) . = ..() diff --git a/code/modules/holiday/holidays.dm b/code/modules/holiday/holidays.dm index d5a9457141294..3138fff9032c1 100644 --- a/code/modules/holiday/holidays.dm +++ b/code/modules/holiday/holidays.dm @@ -705,7 +705,7 @@ /datum/holiday/xmas name = CHRISTMAS - begin_day = 23 + begin_day = 18 begin_month = DECEMBER end_day = 27 holiday_hat = /obj/item/clothing/head/costume/santa diff --git a/code/modules/holodeck/computer.dm b/code/modules/holodeck/computer.dm index ace4fc62aa6f0..2257f4c079651 100644 --- a/code/modules/holodeck/computer.dm +++ b/code/modules/holodeck/computer.dm @@ -365,6 +365,8 @@ GLOBAL_LIST_INIT(typecache_holodeck_linked_floorcheck_ok, typecacheof(list(/turf if(SPT_PROB(2.5, seconds_per_tick)) do_sparks(2, 1, holo_turf) return + if(spawning_simulation) + return // putting it here because updating power would be pointless we are only loading it . = ..() if(!. || program == offline_program)//we dont need to scan the holodeck if the holodeck is offline update_use_power(IDLE_POWER_USE) diff --git a/code/modules/holodeck/holodeck_map_templates.dm b/code/modules/holodeck/holodeck_map_templates.dm index e7354ceb70f4c..445574c7e03c9 100644 --- a/code/modules/holodeck/holodeck_map_templates.dm +++ b/code/modules/holodeck/holodeck_map_templates.dm @@ -1,134 +1,112 @@ /datum/map_template/holodeck + /// id var/template_id - var/description + /// Is this an emag program var/restricted = FALSE - var/datum/parsed_map/lastparsed should_place_on_top = FALSE returns_created_atoms = TRUE keep_cached_map = TRUE - var/obj/machinery/computer/holodeck/linked - /datum/map_template/holodeck/offline name = "Holodeck - Offline" template_id = "holodeck_offline" - description = "benis" mappath = "_maps/templates/holodeck_offline.dmm" /datum/map_template/holodeck/emptycourt name = "Holodeck - Empty Court" template_id = "holodeck_emptycourt" - description = "benis" mappath = "_maps/templates/holodeck_emptycourt.dmm" /datum/map_template/holodeck/dodgeball name = "Holodeck - Dodgeball Court" template_id = "holodeck_dodgeball" - description = "benis" mappath = "_maps/templates/holodeck_dodgeball.dmm" /datum/map_template/holodeck/basketball name = "Holodeck - Basketball Court" template_id = "holodeck_basketball" - description = "benis" mappath = "_maps/templates/holodeck_basketball.dmm" /datum/map_template/holodeck/thunderdome name = "Holodeck - Thunderdome Arena" template_id = "holodeck_thunderdome" - description = "benis" mappath = "_maps/templates/holodeck_thunderdome.dmm" /datum/map_template/holodeck/beach name = "Holodeck - Beach" template_id = "holodeck_beach" - description = "benis" mappath = "_maps/templates/holodeck_beach.dmm" /datum/map_template/holodeck/lounge name = "Holodeck - Lounge" template_id = "holodeck_lounge" - description = "benis" mappath = "_maps/templates/holodeck_lounge.dmm" /datum/map_template/holodeck/petpark name = "Holodeck - Pet Park" template_id = "holodeck_petpark" - description = "benis" mappath = "_maps/templates/holodeck_petpark.dmm" /datum/map_template/holodeck/firingrange name = "Holodeck - Firing Range" template_id = "holodeck_firingrange" - description = "benis" mappath = "_maps/templates/holodeck_firingrange.dmm" /datum/map_template/holodeck/anime_school name = "Holodeck - Anime School" template_id = "holodeck_animeschool" - description = "benis" mappath = "_maps/templates/holodeck_animeschool.dmm" /datum/map_template/holodeck/chapelcourt name = "Holodeck - Chapel Courtroom" template_id = "holodeck_chapelcourt" - description = "benis" mappath = "_maps/templates/holodeck_chapelcourt.dmm" /datum/map_template/holodeck/spacechess name = "Holodeck - Space Chess" template_id = "holodeck_spacechess" - description = "benis" mappath = "_maps/templates/holodeck_spacechess.dmm" /datum/map_template/holodeck/spacecheckers name = "Holodeck - Space Checkers" template_id = "holodeck_spacecheckers" - description = "benis" mappath = "_maps/templates/holodeck_spacecheckers.dmm" /datum/map_template/holodeck/kobayashi name = "Holodeck - Kobayashi Maru" template_id = "holodeck_kobayashi" - description = "benis" mappath = "_maps/templates/holodeck_kobayashi.dmm" /datum/map_template/holodeck/winterwonderland name = "Holodeck - Winter Wonderland" template_id = "holodeck_winterwonderland" - description = "benis" mappath = "_maps/templates/holodeck_winterwonderland.dmm" /datum/map_template/holodeck/photobooth name = "Holodeck - Photobooth" template_id = "holodeck_photobooth" - description = "benis" mappath = "_maps/templates/holodeck_photobooth.dmm" /datum/map_template/holodeck/skatepark name = "Holodeck - Skatepark" template_id = "holodeck_skatepark" - description = "benis" mappath = "_maps/templates/holodeck_skatepark.dmm" /datum/map_template/holodeck/microwave name = "Holodeck - Microwave Paradise" template_id = "holodeck_microwave" - description = "benis" mappath = "_maps/templates/holodeck_microwave.dmm" /datum/map_template/holodeck/baseball name = "Holodeck - Baseball Field" template_id = "holodeck_baseball" - description = "benis" mappath = "_maps/templates/holodeck_baseball.dmm" /datum/map_template/holodeck/card_battle name = "Holodeck - TGC Battle Arena" template_id = "holodeck_card_battle" - description = "An arena for playing Tactical Game Cards." mappath = "_maps/templates/holodeck_card_battle.dmm" //bad evil no good programs @@ -136,48 +114,41 @@ /datum/map_template/holodeck/medicalsim name = "Holodeck - Emergency Medical" template_id = "holodeck_medicalsim" - description = "benis" mappath = "_maps/templates/holodeck_medicalsim.dmm" restricted = TRUE /datum/map_template/holodeck/thunderdome1218 name = "Holodeck - 1218 AD" template_id = "holodeck_thunderdome1218" - description = "benis" mappath = "_maps/templates/holodeck_thunderdome1218.dmm" restricted = TRUE /datum/map_template/holodeck/burntest name = "Holodeck - Atmospheric Burn Test" template_id = "holodeck_burntest" - description = "benis" mappath = "_maps/templates/holodeck_burntest.dmm" restricted = TRUE /datum/map_template/holodeck/wildlifesim name = "Holodeck - Wildlife Simulation" template_id = "holodeck_wildlifesim" - description = "benis" mappath = "_maps/templates/holodeck_wildlifesim.dmm" restricted = TRUE /datum/map_template/holodeck/holdoutbunker name = "Holodeck - Holdout Bunker" template_id = "holodeck_holdoutbunker" - description = "benis" mappath = "_maps/templates/holodeck_holdoutbunker.dmm" restricted = TRUE /datum/map_template/holodeck/anthophillia name = "Holodeck - Anthophillia" template_id = "holodeck_anthophillia" - description = "benis" mappath = "_maps/templates/holodeck_anthophillia.dmm" restricted = TRUE /datum/map_template/holodeck/refuelingstation name = "Holodeck - Refueling Station" template_id = "holodeck_refuelingstation" - description = "benis" mappath = "_maps/templates/holodeck_refuelingstation.dmm" restricted = TRUE diff --git a/code/modules/hydroponics/hydroitemdefines.dm b/code/modules/hydroponics/hydroitemdefines.dm index 924cebcee8f84..41cc408d249a3 100644 --- a/code/modules/hydroponics/hydroitemdefines.dm +++ b/code/modules/hydroponics/hydroitemdefines.dm @@ -492,14 +492,14 @@ throwforce = 15 throw_speed = 4 throw_range = 7 - embed_type = /datum/embed_data/hatchet + embed_type = /datum/embedding/hatchet custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT*7.5) attack_verb_continuous = list("chops", "tears", "lacerates", "cuts") attack_verb_simple = list("chop", "tear", "lacerate", "cut") hitsound = 'sound/items/weapons/bladeslice.ogg' sharpness = SHARP_EDGED -/datum/embed_data/hatchet +/datum/embedding/hatchet pain_mult = 4 embed_chance = 35 fall_chance = 10 diff --git a/code/modules/hydroponics/plant_genes.dm b/code/modules/hydroponics/plant_genes.dm index 79dd725b6e354..85a560eb03422 100644 --- a/code/modules/hydroponics/plant_genes.dm +++ b/code/modules/hydroponics/plant_genes.dm @@ -864,14 +864,28 @@ var/obj/item/seeds/our_seed = our_plant.get_plant_seed() our_plant.throwforce = (our_seed.potency/20) - if (!our_plant.get_embed()) + var/datum/embedding/plant_embed = our_plant.get_embed() + if (!plant_embed) + if(our_seed.get_gene(/datum/plant_gene/trait/stinging)) + our_plant.set_embed(/datum/embedding/spiky_plant) + else + our_plant.set_embed(/datum/embedding/sticky_plant) return + plant_embed.ignore_throwspeed_threshold = TRUE if(our_seed.get_gene(/datum/plant_gene/trait/stinging)) - our_plant.set_embed(our_plant.get_embed().generate_with_values(ignore_throwspeed_threshold = TRUE)) return - our_plant.set_embed(our_plant.get_embed().generate_with_values(ignore_throwspeed_threshold = TRUE, pain_mult = 0, jostle_pain_mult = 0)) + plant_embed.pain_mult = 0 + plant_embed.jostle_pain_mult = 0 + +/datum/embedding/sticky_plant + pain_mult = 0 + jostle_pain_mult = 0 + ignore_throwspeed_threshold = TRUE + +/datum/embedding/spiky_plant + ignore_throwspeed_threshold = TRUE /** * This trait automatically heats up the plant's chemical contents when harvested. diff --git a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm index f23fdb7f76d50..64fe89085f60d 100644 --- a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm +++ b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm @@ -190,10 +190,10 @@ menu_description = "An odd sharp blade which provides a low chance of blocking incoming melee attacks and deals a random amount of damage, which can range from almost nothing to very high. Can be worn on the back." /obj/item/nullrod/claymore/multiverse/melee_attack_chain(mob/user, atom/target, params) - var/old_force = force - force += rand(-14, 15) + var/force_mod = rand(-14, 15) + force += force_mod . = ..() - force = old_force + force -= force_mod /obj/item/nullrod/claymore/saber name = "light energy sword" @@ -839,9 +839,11 @@ //We do this because our force could have been changed by things like whetstones and RPG stats. force += old_force - initial(force) + //Record change to our force in case something modifies it down the chain + var/force_diff = force - old_force . = ..() //Reapply our old force. - force = old_force + force -= force_diff /obj/item/nullrod/nullblade/afterattack(atom/target, mob/user, click_parameters) if(!isliving(target)) diff --git a/code/modules/jobs/job_types/coroner.dm b/code/modules/jobs/job_types/coroner.dm index 607b11ad76ae9..dfbdb94b9e34f 100644 --- a/code/modules/jobs/job_types/coroner.dm +++ b/code/modules/jobs/job_types/coroner.dm @@ -33,6 +33,7 @@ /obj/item/healthanalyzer = 10, /obj/item/shovel/serrated/dull = 5, /obj/effect/spawner/random/medical/organs = 5, + /obj/item/reagent_containers/cup/organ_jar/brain_in_a_jar = 5, /obj/effect/spawner/random/medical/memeorgans = 1, /obj/item/scythe = 1, ) diff --git a/code/modules/logging/categories/log_category_debug.dm b/code/modules/logging/categories/log_category_debug.dm index 8833a59b1a0e9..8d9318485b52b 100644 --- a/code/modules/logging/categories/log_category_debug.dm +++ b/code/modules/logging/categories/log_category_debug.dm @@ -9,6 +9,10 @@ category = LOG_CATEGORY_DEBUG_LUA master_category = /datum/log_category/debug +/datum/log_category/debug_tts + category = LOG_CATEGORY_DEBUG_TTS + master_category = /datum/log_category/debug + // This is not in the debug master category on purpose, do not add it /datum/log_category/debug_runtime category = LOG_CATEGORY_RUNTIME diff --git a/code/modules/lost_crew/lost_crew_manager.dm b/code/modules/lost_crew/lost_crew_manager.dm index a89302627318b..251b0ec98214e 100644 --- a/code/modules/lost_crew/lost_crew_manager.dm +++ b/code/modules/lost_crew/lost_crew_manager.dm @@ -72,11 +72,11 @@ GLOBAL_DATUM_INIT(lost_crew_manager, /datum/lost_crew_manager, new) hersens.AddComponent( /datum/component/ghostrole_on_revive, \ /* refuse_revival_if_failed = */ TRUE, \ - /*on_revival = */ CALLBACK(src, PROC_REF(on_succesful_revive), hersens, scenario.death_lore, on_revive_and_player_occupancy) \ + /*on_revival = */ CALLBACK(src, PROC_REF(on_successful_revive), hersens, scenario.death_lore, on_revive_and_player_occupancy) \ ) /// Set a timer for awarding succes and drop some awesome deathlore -/datum/lost_crew_manager/proc/on_succesful_revive(obj/item/organ/brain/brain, list/death_lore, list/datum/callback/on_revive_and_player_occupancy) +/datum/lost_crew_manager/proc/on_successful_revive(obj/item/organ/brain/brain, list/death_lore, list/datum/callback/on_revive_and_player_occupancy) var/mob/living/carbon/human/owner = brain.owner owner.mind.add_antag_datum(/datum/antagonist/recovered_crew) //for tracking mostly diff --git a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm index 871dbc6eccacc..329b6bd796fb8 100644 --- a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm +++ b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm @@ -167,7 +167,9 @@ if(QDELETED(target)) return var/datum/status_effect/crusher_mark/mark = target.has_status_effect(/datum/status_effect/crusher_mark) - var/boosted_mark = mark?.boosted + if(!mark) + return + var/boosted_mark = mark.boosted if(world.time < mark.mark_applied + mark.ready_delay) // Simple way to prevent right+left click at the same time to detonate the mark for free return if(!target.remove_status_effect(mark)) diff --git a/code/modules/mob/dead/observer/orbit.dm b/code/modules/mob/dead/observer/orbit.dm index 5e77627e00d55..a09475040f835 100644 --- a/code/modules/mob/dead/observer/orbit.dm +++ b/code/modules/mob/dead/observer/orbit.dm @@ -160,8 +160,7 @@ GLOBAL_DATUM_INIT(orbit_menu, /datum/orbit_menu, new) serialized["antag"] = antag.name serialized["antag_group"] = antag.antagpanel_category - serialized["job"] = antag.name - serialized["icon"] = antag.antag_hud_name + serialized["antag_icon"] = antag.antag_hud_name return serialized diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index ff26942461adf..17fcc86d78525 100644 --- a/code/modules/mob/inventory.dm +++ b/code/modules/mob/inventory.dm @@ -548,19 +548,6 @@ DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(execute_quick_equip))) -/// Safely drop everything, without deconstructing the mob -/mob/proc/drop_everything(del_on_drop, force, del_if_nodrop) - . = list() - for(var/obj/item/item in src) - if(!dropItemToGround(item, force)) - if(del_if_nodrop && !(item.item_flags & ABSTRACT)) - qdel(item) - if(del_on_drop) - qdel(item) - //Anything thats not deleted and isn't in the mob, so everything that is succesfully dropped to the ground, is returned - if(!QDELETED(item) && !(item in src)) - . += item - ///proc extender of [/mob/verb/quick_equip] used to make the verb queuable if the server is overloaded /mob/proc/execute_quick_equip() var/obj/item/I = get_active_held_item() @@ -609,3 +596,19 @@ for (var/obj/item/implant/storage/internal_bag in implants) belongings += internal_bag.contents return belongings + +/// Safely drop everything, without deconstructing the mob +/mob/living/proc/drop_everything(del_on_drop, force, del_if_nodrop) + . = list() //list of items that were successfully dropped + + var/list/all_gear = get_all_gear(recursive = FALSE) + for(var/obj/item/item in all_gear) + if(dropItemToGround(item, force)) + if(QDELETED(item)) //DROPDEL can cause this item to be deleted + continue + if(del_on_drop) + qdel(item) + continue + . += item + else if(del_if_nodrop && !(item.item_flags & ABSTRACT)) + qdel(item) diff --git a/code/modules/mob/living/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm index f9875bae327b8..7086298c19f3b 100644 --- a/code/modules/mob/living/brain/brain_item.dm +++ b/code/modules/mob/living/brain/brain_item.dm @@ -235,6 +235,8 @@ . += 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/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 7d9ad1af6b69f..8fad9fd43d28d 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -228,13 +228,13 @@ /mob/living/carbon/Topic(href, href_list) ..() if(href_list["embedded_object"]) - var/obj/item/bodypart/L = locate(href_list["embedded_limb"]) in bodyparts - if(!L) + var/obj/item/bodypart/limb = locate(href_list["embedded_limb"]) in bodyparts + if(!limb) return - var/obj/item/I = locate(href_list["embedded_object"]) in L.embedded_objects - if(!I || I.loc != src) //no item, no limb, or item is not in limb or in the person anymore + var/obj/item/weapon = locate(href_list["embedded_object"]) in limb.embedded_objects + if(!weapon || weapon.loc != src) //no item, no limb, or item is not in limb or in the person anymore return - SEND_SIGNAL(src, COMSIG_CARBON_EMBED_RIP, I, L) + weapon.get_embed().rip_out(usr) return if(href_list["show_paper_note"]) @@ -639,6 +639,9 @@ new_sight |= SEE_MOBS lighting_cutoff = max(lighting_cutoff, LIGHTING_CUTOFF_MEDIUM) + if (HAS_TRAIT(src, TRAIT_MINOR_NIGHT_VISION)) + lighting_cutoff = max(lighting_cutoff, LIGHTING_CUTOFF_LOW) + if(HAS_TRAIT(src, TRAIT_XRAY_VISION)) new_sight |= SEE_TURFS|SEE_MOBS|SEE_OBJS diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index b3f6e1d92aa59..d06146cc7046b 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -416,22 +416,22 @@ return var/embeds = FALSE - for(var/X in bodyparts) - var/obj/item/bodypart/LB = X - for(var/obj/item/I in LB.embedded_objects) + for(var/obj/item/bodypart/limb as anything in bodyparts) + for(var/obj/item/weapon as anything in limb.embedded_objects) if(!embeds) embeds = TRUE // this way, we only visibly try to examine ourselves if we have something embedded, otherwise we'll still hug ourselves :) visible_message(span_notice("[capitalize(declent_ru(NOMINATIVE))] осматривает себя."), \ - span_notice("Вы проверяете себя на наличие осколков.")) - if(I.is_embed_harmless()) - to_chat(src, "\t [capitalize(I.declent_ru(NOMINATIVE))] застревает у вас на [LB.declent_ru(PREPOSITIONAL)]!") + span_notice("Вы проверяете себя на наличие осколков."), visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE) + var/harmless = weapon.get_embed().is_harmless() + var/stuck_wordage = harmless ? "застревает у вас на" : "впивается у вас в" + var/embed_text = "\t [icon2html(weapon, src)] [capitalize(weapon.declent_ru(NOMINATIVE))] [stuck_wordage] [limb.declent_ru(PREPOSITIONAL)]!" + if (harmless) + to_chat(src, span_italics(span_notice(embed_text))) else - to_chat(src, "\t [capitalize(I.declent_ru(NOMINATIVE))] впивается у вас в [LB.declent_ru(PREPOSITIONAL)]!") - + to_chat(src, span_boldwarning(embed_text)) return embeds - /mob/living/carbon/flash_act(intensity = 1, override_blindness_check = 0, affect_silicon = 0, visual = 0, type = /atom/movable/screen/fullscreen/flash, length = 25) var/obj/item/organ/eyes/eyes = get_organ_slot(ORGAN_SLOT_EYES) if(!eyes) //can't flash what can't see! diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm index d4d6c8d449f9f..eb300e9cadeb7 100644 --- a/code/modules/mob/living/carbon/examine.dm +++ b/code/modules/mob/living/carbon/examine.dm @@ -1,5 +1,6 @@ /// Adds a newline to the examine list if the above entry is not empty and it is not the first element in the list #define ADD_NEWLINE_IF_NECESSARY(list) if(length(list) > 0 && list[length(list)]) { list += "" } +#define CARBON_EXAMINE_EMBEDDING_MAX_DIST 4 /mob/living/carbon/human/get_examine_name(mob/user, declent) // BANDASTATION EDIT - Declents if(!HAS_TRAIT(user, TRAIT_PROSOPAGNOSIA)) @@ -61,8 +62,16 @@ disabled += body_part missing -= body_part.body_zone for(var/obj/item/embedded as anything in body_part.embedded_objects) - var/stuck_wordage = embedded.is_embed_harmless() ? "застревает" : "впивается" - . += span_boldwarning("[icon2html(embedded, user)] [capitalize(embedded.declent_ru(ACCUSATIVE))] [stuck_wordage] в [t_his] [body_part.ru_plaintext_zone[ACCUSATIVE] || body_part.plaintext_zone]!") + var/harmless = embedded.get_embed().is_harmless() + var/stuck_wordage = harmless ? "застревает" : "впивается" + var/embed_line = "[embedded]" + if (get_dist(src, user) <= CARBON_EXAMINE_EMBEDDING_MAX_DIST) + embed_line = "[embedded]" + var/embed_text = "[icon2html(embedded, user)] [capitalize(embed_line.declent_ru(ACCUSATIVE))] [stuck_wordage] в [t_his] [body_part.ru_plaintext_zone[ACCUSATIVE] || body_part.plaintext_zone]!" + if (harmless) + . += span_italics(span_notice(embed_text)) + else + . += span_boldwarning(embed_text) for(var/datum/wound/iter_wound as anything in body_part.wounds) . += span_danger(iter_wound.get_examine_description(user)) @@ -583,4 +592,9 @@ if(undershirt.has_sensor == BROKEN_SENSORS) . += list(span_notice("[capitalize(undershirt.declent_ru(NOMINATIVE))] имеет коротящие медицинские датчики.")) + for(var/datum/scar/iter_scar as anything in all_scars) + if(iter_scar.is_visible(user)) + . += iter_scar.get_examine_description(user) + #undef ADD_NEWLINE_IF_NECESSARY +#undef CARBON_EXAMINE_EMBEDDING_MAX_DIST diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm index 73a1c1faac1bd..8408b0adb0ecc 100644 --- a/code/modules/mob/living/carbon/human/human_helpers.dm +++ b/code/modules/mob/living/carbon/human/human_helpers.dm @@ -167,8 +167,7 @@ for(var/i in missing_bodyparts) var/datum/scar/scaries = new scars += "[scaries.format_amputated(i)]" - for(var/i in all_scars) - var/datum/scar/iter_scar = i + for(var/datum/scar/iter_scar as anything in all_scars) if(!iter_scar.fake) scars += "[iter_scar.format()];" return scars diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index c476457efb9fb..282381fe52042 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1396,6 +1396,10 @@ to_chat(src, span_warning("У вас нет рук!")) return FALSE + if(!(action_bitflags & ALLOW_PAI) && ispAI(src)) + to_chat(src, span_warning("Your holochasis does not allow you to do this!")) + return FALSE + if(!(action_bitflags & BYPASS_ADJACENCY) && ((action_bitflags & NOT_INSIDE_TARGET) || !recursive_loc_check(src, target)) && !CanReach(target)) if(HAS_SILICON_ACCESS(src) && !ispAI(src)) if(!(action_bitflags & ALLOW_SILICON_REACH)) // silicons can ignore range checks (except pAIs) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 7541f64fff193..7c0bd76867371 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -228,7 +228,7 @@ blocked = TRUE var/zone = get_random_valid_zone(BODY_ZONE_CHEST, 65)//Hits a random part of the body, geared towards the chest - var/nosell_hit = SEND_SIGNAL(thrown_item, COMSIG_MOVABLE_IMPACT_ZONE, src, zone, blocked, throwingdatum) // TODO: find a better way to handle hitpush and skipcatch for humans + var/nosell_hit = (SEND_SIGNAL(thrown_item, COMSIG_MOVABLE_IMPACT_ZONE, src, zone, blocked, throwingdatum) & MOVABLE_IMPACT_ZONE_OVERRIDE) // TODO: find a better way to handle hitpush and skipcatch for humans if(nosell_hit) skipcatch = TRUE hitpush = FALSE diff --git a/code/modules/mob/living/simple_animal/hostile/ooze.dm b/code/modules/mob/living/simple_animal/hostile/ooze.dm index a47d9fe26768a..77ccf3ea483e7 100644 --- a/code/modules/mob/living/simple_animal/hostile/ooze.dm +++ b/code/modules/mob/living/simple_animal/hostile/ooze.dm @@ -389,50 +389,32 @@ name = "mending globule" icon_state = "glob_projectile" shrapnel_type = /obj/item/mending_globule - embed_type = /datum/embed_data/mending_globule + embed_type = /datum/embedding/mending_globule damage = 0 -///This item is what is embedded into the mob, and actually handles healing of mending globules +///This item is what is embedded into the mob /obj/item/mending_globule name = "mending globule" desc = "It somehow heals those who touch it." icon = 'icons/obj/science/vatgrowing.dmi' icon_state = "globule" - embed_type = /datum/embed_data/mending_globule - var/obj/item/bodypart/bodypart var/heals_left = 35 -/datum/embed_data/mending_globule +/datum/embedding/mending_globule embed_chance = 100 ignore_throwspeed_threshold = TRUE pain_mult = 0 jostle_pain_mult = 0 fall_chance = 0.5 -/obj/item/mending_globule/Destroy() +// This already processes, zero logic to add additional tracking to the item +/datum/embedding/mending_globule/process(seconds_per_tick) . = ..() - bodypart = null - -/obj/item/mending_globule/embedded(mob/living/carbon/human/embedded_mob, obj/item/bodypart/part) - . = ..() - if(!istype(part)) - return - bodypart = part - START_PROCESSING(SSobj, src) - -/obj/item/mending_globule/unembedded() - . = ..() - bodypart = null - STOP_PROCESSING(SSobj, src) - -///Handles the healing of the mending globule -/obj/item/mending_globule/process() - if(!bodypart) //this is fucked - return FALSE - bodypart.heal_damage(1,1) - heals_left-- - if(heals_left <= 0) - qdel(src) + var/obj/item/mending_globule/globule = parent + owner_limb.heal_damage(0.5 * seconds_per_tick, 0.5 * seconds_per_tick) + globule.heals_left-- + if(globule.heals_left <= 0) + qdel(globule) ///This action lets you put a mob inside of a cacoon that will inject it with some chemicals. /datum/action/cooldown/gel_cocoon diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 6e06ec613d068..71a98b6fb8b49 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1090,6 +1090,7 @@ * * BYPASS_ADJACENCY - The target does not have to be adjacent * * SILENT_ADJACENCY - Adjacency is required but errors are not printed * * NOT_INSIDE_TARGET - The target maybe adjacent but the mob should not be inside the target + * * ALLOW_PAI - Allows pAIs to perform an action * * silence_adjacency: Sometimes we want to use this proc to check interaction without allowing it to throw errors for base case adjacency * Alt click uses this, as otherwise you can detect what is interactable from a distance via the error message diff --git a/code/modules/mod/modules/modules_engineering.dm b/code/modules/mod/modules/modules_engineering.dm index ea12a61e5b4d4..7ffda55fbb09f 100644 --- a/code/modules/mod/modules/modules_engineering.dm +++ b/code/modules/mod/modules/modules_engineering.dm @@ -128,7 +128,7 @@ hitsound_wall = 'sound/items/weapons/batonextend.ogg' suppressed = SUPPRESSED_VERY hit_threshhold = ABOVE_NORMAL_TURF_LAYER - embed_type = /datum/embed_data/tether_projectile + embed_type = /datum/embedding/tether_projectile shrapnel_type = /obj/item/tether_anchor /// Reference to the beam following the projectile. var/line @@ -262,7 +262,7 @@ to_chat(target, span_userdanger("[user] attaches a tether to you!")) target.AddComponent(/datum/component/tether, src, 7, "tether", tether_trait_source = REF(src), no_target_trait = TRUE) -/datum/embed_data/tether_projectile +/datum/embedding/tether_projectile embed_chance = 65 //spiky fall_chance = 2 ignore_throwspeed_threshold = TRUE diff --git a/code/modules/paperwork/clipboard.dm b/code/modules/paperwork/clipboard.dm index 76dfc79a2edb4..b42fe45493fbc 100644 --- a/code/modules/paperwork/clipboard.dm +++ b/code/modules/paperwork/clipboard.dm @@ -92,14 +92,23 @@ /obj/item/clipboard/update_overlays() . = ..() - var/obj/item/paper/toppaper = toppaper_ref?.resolve() - if(toppaper) - . += toppaper.icon_state - . += toppaper.overlays + var/paper_to_add = get_paper_overlay() + if(paper_to_add) + . += paper_to_add if(pen) . += "clipboard_pen" . += "clipboard_over" +/obj/item/clipboard/proc/get_paper_overlay() + var/obj/item/paper/toppaper = toppaper_ref?.resolve() + if(isnull(toppaper)) + return + + var/mutable_appearance/paper_overlay = mutable_appearance(icon, toppaper.icon_state, offset_spokesman = src, appearance_flags = KEEP_APART) + paper_overlay = toppaper.color_atom_overlay(paper_overlay) + paper_overlay.overlays += toppaper.overlays + return paper_overlay + /obj/item/clipboard/attack_hand(mob/user, list/modifiers) if(LAZYACCESS(modifiers, RIGHT_CLICK)) var/obj/item/paper/toppaper = toppaper_ref?.resolve() diff --git a/code/modules/paperwork/folders.dm b/code/modules/paperwork/folders.dm index 73ceb9c219ccb..f77abe3b34074 100644 --- a/code/modules/paperwork/folders.dm +++ b/code/modules/paperwork/folders.dm @@ -17,6 +17,8 @@ )) /// Do we hide the contents on examine? var/contents_hidden = FALSE + /// icon_state of overlay for papers inside of this folder + var/paper_overlay_state = "folder_paper" /obj/item/folder/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] begins filing an imaginary death warrant! Кажется, [user.ru_p_they()] пытается совершить самоубийство!")) @@ -67,7 +69,14 @@ /obj/item/folder/update_overlays() . = ..() if(contents.len) - . += "folder_paper" + var/to_add = get_paper_overlay() + if (to_add) + . += to_add + +/obj/item/folder/proc/get_paper_overlay() + var/mutable_appearance/paper_overlay = mutable_appearance(icon, paper_overlay_state, offset_spokesman = src, appearance_flags = KEEP_APART) + paper_overlay = contents[1].color_atom_overlay(paper_overlay) + return paper_overlay /obj/item/folder/attackby(obj/item/weapon, mob/user, params) if(burn_paper_product_attackby_check(weapon, user)) diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm index d974141bc19c8..7a027eb8c759c 100644 --- a/code/modules/paperwork/paper.dm +++ b/code/modules/paperwork/paper.dm @@ -278,9 +278,9 @@ if(LAZYLEN(stamp_cache) > MAX_PAPER_STAMPS_OVERLAYS) return - var/mutable_appearance/stamp_overlay = mutable_appearance('icons/obj/service/bureaucracy.dmi', "paper_[stamp_icon_state]") - stamp_overlay.pixel_x = rand(-2, 2) - stamp_overlay.pixel_y = rand(-3, 2) + var/mutable_appearance/stamp_overlay = mutable_appearance('icons/obj/service/bureaucracy.dmi', "paper_[stamp_icon_state]", appearance_flags = KEEP_APART | RESET_COLOR) + stamp_overlay.pixel_w = rand(-2, 2) + stamp_overlay.pixel_z = rand(-3, 2) add_overlay(stamp_overlay) LAZYADD(stamp_cache, stamp_icon_state) @@ -306,6 +306,8 @@ /obj/item/paper/update_icon_state() if(LAZYLEN(raw_text_inputs) && show_written_words) icon_state = "[initial(icon_state)]_words" + else + icon_state = initial(icon_state) return ..() /obj/item/paper/verb/rename() diff --git a/code/modules/paperwork/paper_biscuit.dm b/code/modules/paperwork/paper_biscuit.dm index d98eb234c95fb..b0695fb19f288 100644 --- a/code/modules/paperwork/paper_biscuit.dm +++ b/code/modules/paperwork/paper_biscuit.dm @@ -8,6 +8,7 @@ drop_sound = 'sound/items/handling/disk_drop.ogg' pickup_sound = 'sound/items/handling/disk_pickup.ogg' contents_hidden = TRUE + paper_overlay_state = "paperbiscuit_paper" /// Is biscuit cracked open or not? var/cracked = FALSE /// The paper slip inside, if there is one @@ -38,12 +39,10 @@ playsound(get_turf(user), 'sound/effects/wounds/crackandbleed.ogg', 40, TRUE) //Don't eat plastic cards kids, they get really sharp if you chew on them. return BRUTELOSS -/obj/item/folder/biscuit/update_overlays() - . = ..() - if(contents.len) //This is to prevent the unsealed biscuit from having the folder_paper overlay when it gets sealed - . -= "folder_paper" - if(cracked) //Shows overlay only when it has contents and is cracked open - . += "paperbiscuit_paper" +/obj/item/folder/biscuit/get_paper_overlay() + if(!cracked) + return null + return ..() ///Checks if the biscuit has been already cracked. /obj/item/folder/biscuit/proc/crack_check(mob/user) diff --git a/code/modules/paperwork/pen.dm b/code/modules/paperwork/pen.dm index 890b072ae7167..375e70a878625 100644 --- a/code/modules/paperwork/pen.dm +++ b/code/modules/paperwork/pen.dm @@ -30,7 +30,7 @@ var/degrees = 0 var/font = PEN_FONT var/requires_gravity = TRUE // can you use this to write in zero-g - embed_type = /datum/embed_data/pen + embed_type = /datum/embedding/pen sharpness = SHARP_POINTY var/dart_insert_icon = 'icons/obj/weapons/guns/toy.dmi' var/dart_insert_casing_icon_state = "overlay_pen" @@ -38,7 +38,7 @@ /// If this pen can be clicked in order to retract it var/can_click = TRUE -/datum/embed_data/pen +/datum/embedding/pen embed_chance = 50 /obj/item/pen/Initialize(mapload) @@ -86,11 +86,11 @@ /obj/item/pen/proc/on_inserted_into_dart(datum/source, obj/projectile/dart, mob/user, embedded = FALSE) SIGNAL_HANDLER -/obj/item/pen/proc/get_dart_var_modifiers() +/obj/item/pen/proc/get_dart_var_modifiers(obj/projectile/projectile) return list( "damage" = max(5, throwforce), "speed" = max(0, throw_speed - 3), - "embedding" = get_embed(), + "embedding" = get_embed().create_copy(projectile), "armour_penetration" = armour_penetration, "wound_bonus" = wound_bonus, "bare_wound_bonus" = bare_wound_bonus, @@ -195,7 +195,7 @@ "Black and Silver" = "pen-fountain-b", "Command Blue" = "pen-fountain-cb" ) - embed_type = /datum/embed_data/pen/captain + embed_type = /datum/embedding/pen/captain dart_insert_casing_icon_state = "overlay_fountainpen_gold" dart_insert_projectile_icon_state = "overlay_fountainpen_gold_proj" var/list/overlay_reskin = list( @@ -206,7 +206,7 @@ "Command Blue" = "overlay_fountainpen_gold" ) -/datum/embed_data/pen/captain +/datum/embedding/pen/captain embed_chance = 50 /obj/item/pen/fountain/captain/Initialize(mapload) @@ -369,8 +369,8 @@ var/datum/component/transforming/transform_comp = GetComponent(/datum/component/transforming) .["damage"] = max(5, transform_comp.throwforce_on) .["speed"] = max(0, transform_comp.throw_speed_on - 3) - var/datum/embed_data/data = .["embedding"] - .["embedding"] = data.generate_with_values(embed_chance = 100) + var/datum/embedding/data = .["embedding"] + data.embed_chance = 100 /obj/item/pen/edagger/proc/on_containing_dart_fired(obj/projectile/source) SIGNAL_HANDLER @@ -426,7 +426,7 @@ inhand_icon_state = hidden_icon lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi' righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi' - set_embed(/datum/embed_data/edagger_active) + set_embed(/datum/embedding/edagger_active) else name = initial(name) desc = initial(desc) @@ -442,7 +442,7 @@ set_light_on(active) return COMPONENT_NO_DEFAULT_MESSAGE -/datum/embed_data/edagger_active +/datum/embedding/edagger_active embed_chance = 100 /obj/item/pen/edagger/proc/on_scan(datum/source, mob/user, list/extra_data) diff --git a/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm b/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm index 41152b170ad3b..4fc28dd789178 100644 --- a/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm +++ b/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm @@ -33,9 +33,9 @@ speed = 1 range = 25 shrapnel_type = null - embed_type = /datum/embed_data/arrow + embed_type = /datum/embedding/arrow -/datum/embed_data/arrow +/datum/embedding/arrow embed_chance = 90 fall_chance = 2 jostle_chance = 2 @@ -62,9 +62,9 @@ damage = 30 speed = 1.3 range = 20 - embed_type = /datum/embed_data/arrow/sticky + embed_type = /datum/embedding/arrow/sticky -/datum/embed_data/arrow/sticky +/datum/embedding/arrow/sticky embed_chance = 99 fall_chance = 0 jostle_chance = 1 @@ -89,7 +89,7 @@ desc = "Better to not get hit with this!" icon_state = "poison_arrow_projectile" damage = 40 - embed_type = /datum/embed_data/arrow + embed_type = /datum/embedding/arrow /obj/projectile/bullet/arrow/poison/on_hit(atom/target, blocked, pierce_hit) . = ..() diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index f907682305ef5..51f501572428c 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -248,7 +248,7 @@ /// If we have a shrapnel_type defined, these embedding stats will be passed to the spawned shrapnel type, which will roll for embedding on the target var/embed_type /// Saves embedding data - var/datum/embed_data/embed_data + VAR_PROTECTED/datum/embedding/embed_data /// If TRUE, hit mobs, even if they are lying on the floor and are not our target within MAX_RANGE_HIT_PRONE_TARGETS tiles var/hit_prone_targets = FALSE /// If TRUE, ignores the range of MAX_RANGE_HIT_PRONE_TARGETS tiles of hit_prone_targets @@ -273,8 +273,8 @@ /obj/projectile/Initialize(mapload) . = ..() maximum_range = range - if (get_embed()) - AddElement(/datum/element/embed) + if (embed_type) + set_embed(embed_type) add_traits(list(TRAIT_FREE_HYPERSPACE_MOVEMENT, TRAIT_FREE_HYPERSPACE_SOFTCORDON_MOVEMENT), INNATE_TRAIT) /obj/projectile/Destroy() @@ -283,6 +283,7 @@ STOP_PROCESSING(SSprojectiles, src) firer = null original = null + QDEL_NULL(embed_data) if (movement_vector) QDEL_NULL(movement_vector) if (beam_points) @@ -299,7 +300,7 @@ wound_bonus += wound_falloff_tile bare_wound_bonus = max(0, bare_wound_bonus + wound_falloff_tile) if(embed_falloff_tile && get_embed()) - set_embed(embed_data.generate_with_values(embed_data.embed_chance + embed_falloff_tile)) + embed_data.embed_chance += embed_falloff_tile if(damage_falloff_tile && damage >= 0) damage += damage_falloff_tile if(stamina_falloff_tile && stamina >= 0) @@ -387,6 +388,7 @@ new impact_effect_type(target_turf, impact_x, impact_y) var/mob/living/living_target = target + get_embed()?.try_embed_projectile(src, target, hit_limb_zone, blocked, pierce_hit) var/reagent_note if(reagents?.reagent_list) reagent_note = "REAGENTS: [pretty_string_from_reagent_list(reagents.reagent_list)]" @@ -1332,7 +1334,7 @@ ///Checks if the projectile can embed into someone /obj/projectile/proc/can_embed_into(atom/hit) - return get_embed() && shrapnel_type && iscarbon(hit) && !HAS_TRAIT(hit, TRAIT_PIERCEIMMUNE) + return shrapnel_type && get_embed()?.can_embed(src, hit) /// Reflects the projectile off of something /obj/projectile/proc/reflect(atom/hit_atom) @@ -1372,19 +1374,27 @@ bullet.fire() return bullet -/// Fetches embedding data -/obj/projectile/proc/get_embed() - RETURN_TYPE(/datum/embed_data) - return embed_type ? (embed_data ||= get_embed_by_type(embed_type)) : embed_data +#undef MOVES_HITSCAN +#undef MUZZLE_EFFECT_PIXEL_INCREMENT -/obj/projectile/proc/set_embed(datum/embed_data/embed) - if(embed_data == embed) +/// Fetches, or lazyloads, our embedding datum +/obj/projectile/proc/get_embed() + RETURN_TYPE(/datum/embedding) + if (embed_data) + return embed_data + if (embed_type) + embed_data = new embed_type(src) + return embed_data + +/// Sets our embedding datum to a different one. Can also take types +/obj/projectile/proc/set_embed(datum/embedding/new_embed, dont_delete = FALSE) + if (new_embed == embed_data) return - // GLOB.embed_by_type stores shared "default" embedding values of datums - // Dynamically generated embeds use the base class and thus are not present in there, and should be qdeleted upon being discarded - if(!isnull(embed_data) && !GLOB.embed_by_type[embed_data.type]) + + if (!isnull(embed_data) && !dont_delete) qdel(embed_data) - embed_data = ispath(embed) ? get_embed_by_type(armor) : embed -#undef MOVES_HITSCAN -#undef MUZZLE_EFFECT_PIXEL_INCREMENT + if (ispath(new_embed)) + new_embed = new new_embed() + + embed_data = new_embed diff --git a/code/modules/projectiles/projectile/bullets.dm b/code/modules/projectiles/projectile/bullets.dm index 1d1313d9e5551..2db698c670b3e 100644 --- a/code/modules/projectiles/projectile/bullets.dm +++ b/code/modules/projectiles/projectile/bullets.dm @@ -8,7 +8,7 @@ sharpness = SHARP_POINTY impact_effect_type = /obj/effect/temp_visual/impact_effect shrapnel_type = /obj/item/shrapnel/bullet - embed_type = /datum/embed_data/bullet + embed_type = /datum/embedding/bullet wound_bonus = 0 wound_falloff_tile = -5 embed_falloff_tile = -3 @@ -17,7 +17,7 @@ name = "divine retribution" damage = 10 -/datum/embed_data/bullet +/datum/embedding/bullet embed_chance=20 fall_chance=2 jostle_chance=0 diff --git a/code/modules/projectiles/projectile/bullets/junk.dm b/code/modules/projectiles/projectile/bullets/junk.dm index 1c6ea89962e14..12712cd9dc892 100644 --- a/code/modules/projectiles/projectile/bullets/junk.dm +++ b/code/modules/projectiles/projectile/bullets/junk.dm @@ -4,7 +4,7 @@ name = "junk bullet" icon_state = "trashball" damage = 30 - embed_type = /datum/embed_data/bullet_junk + embed_type = /datum/embedding/bullet_junk /// What biotype does our junk projectile especially harm? var/extra_damage_mob_biotypes = MOB_ROBOTIC /// How much do we multiply our total base damage? @@ -28,15 +28,15 @@ if(finalized_damage) living_target.apply_damage(finalized_damage, damagetype = extra_damage_type, def_zone = BODY_ZONE_CHEST, wound_bonus = wound_bonus) -/datum/embed_data/bullet_junk - embed_chance=15 - fall_chance=3 - jostle_chance=4 - ignore_throwspeed_threshold=TRUE - pain_stam_pct=0.4 - pain_mult=5 - jostle_pain_mult=6 - rip_time=10 +/datum/embedding/bullet_junk + embed_chance = 15 + fall_chance = 3 + jostle_chance = 4 + ignore_throwspeed_threshold = TRUE + pain_stam_pct = 0.4 + pain_mult = 5 + jostle_pain_mult = 6 + rip_time = 10 /obj/projectile/bullet/incendiary/fire/junk name = "burning oil" @@ -75,19 +75,19 @@ name = "junk ripper bullet" icon_state = "redtrac" damage = 10 - embed_type = /datum/embed_data/bullet_junk_ripper + embed_type = /datum/embedding/bullet_junk_ripper wound_bonus = 10 bare_wound_bonus = 30 -/datum/embed_data/bullet_junk_ripper - embed_chance=100 - fall_chance=3 - jostle_chance=4 - ignore_throwspeed_threshold=TRUE - pain_stam_pct=0.4 - pain_mult=5 - jostle_pain_mult=6 - rip_time=10 +/datum/embedding/bullet_junk_ripper + embed_chance = 100 + fall_chance = 3 + jostle_chance = 4 + ignore_throwspeed_threshold = TRUE + pain_stam_pct = 0.4 + pain_mult = 5 + jostle_pain_mult = 6 + rip_time = 10 /obj/projectile/bullet/junk/reaper name = "junk reaper bullet" diff --git a/code/modules/projectiles/projectile/bullets/pistol.dm b/code/modules/projectiles/projectile/bullets/pistol.dm index bc64363a2d33f..3d832da9a7d13 100644 --- a/code/modules/projectiles/projectile/bullets/pistol.dm +++ b/code/modules/projectiles/projectile/bullets/pistol.dm @@ -3,17 +3,17 @@ /obj/projectile/bullet/c9mm name = "9mm bullet" damage = 30 - embed_type = /datum/embed_data/bullet_c9mm + embed_type = /datum/embedding/bullet_c9mm -/datum/embed_data/bullet_c9mm - embed_chance=15 - fall_chance=3 - jostle_chance=4 - ignore_throwspeed_threshold=TRUE - pain_stam_pct=0.4 - pain_mult=5 - jostle_pain_mult=6 - rip_time=10 +/datum/embedding/bullet_c9mm + embed_chance = 15 + fall_chance = 3 + jostle_chance = 4 + ignore_throwspeed_threshold = TRUE + pain_stam_pct = 0.4 + pain_mult = 5 + jostle_pain_mult = 6 + rip_time = 10 /obj/projectile/bullet/c9mm/ap name = "9mm armor-piercing bullet" @@ -78,13 +78,13 @@ name = ".160 smart bullet" icon_state = "smartgun" damage = 10 - embed_type = /datum/embed_data/bullet_c160smart + embed_type = /datum/embedding/bullet_c160smart speed = 0.5 homing_turn_speed = 5 homing_inaccuracy_min = 4 homing_inaccuracy_max = 10 -/datum/embed_data/bullet_c160smart +/datum/embedding/bullet_c160smart embed_chance = 10 fall_chance = 5 jostle_chance = 3 diff --git a/code/modules/projectiles/projectile/bullets/revolver.dm b/code/modules/projectiles/projectile/bullets/revolver.dm index 273a0109c5699..df798142a1254 100644 --- a/code/modules/projectiles/projectile/bullets/revolver.dm +++ b/code/modules/projectiles/projectile/bullets/revolver.dm @@ -21,18 +21,18 @@ ricochet_auto_aim_range = 3 wound_bonus = -20 bare_wound_bonus = 10 - embed_type = /datum/embed_data/bullet_c38 + embed_type = /datum/embedding/bullet_c38 embed_falloff_tile = -4 -/datum/embed_data/bullet_c38 - embed_chance=25 - fall_chance=2 - jostle_chance=2 - ignore_throwspeed_threshold=TRUE - pain_stam_pct=0.4 - pain_mult=3 - jostle_pain_mult=5 - rip_time=1 SECONDS +/datum/embedding/bullet_c38 + embed_chance = 25 + fall_chance = 2 + jostle_chance = 2 + ignore_throwspeed_threshold = TRUE + pain_stam_pct = 0.4 + pain_mult = 3 + jostle_pain_mult = 5 + rip_time = 1 SECONDS /obj/projectile/bullet/c38/match name = ".38 Match bullet" @@ -75,19 +75,19 @@ sharpness = SHARP_EDGED wound_bonus = 20 bare_wound_bonus = 20 - embed_type = /datum/embed_data/bullet_c38_dumdum + embed_type = /datum/embedding/bullet_c38_dumdum wound_falloff_tile = -5 embed_falloff_tile = -15 -/datum/embed_data/bullet_c38_dumdum - embed_chance=75 - fall_chance=3 - jostle_chance=4 - ignore_throwspeed_threshold=TRUE - pain_stam_pct=0.4 - pain_mult=5 - jostle_pain_mult=6 - rip_time=1 SECONDS +/datum/embedding/bullet_c38_dumdum + embed_chance = 75 + fall_chance = 3 + jostle_chance = 4 + ignore_throwspeed_threshold = TRUE + pain_stam_pct = 0.4 + pain_mult = 5 + jostle_pain_mult = 6 + rip_time = 1 SECONDS /obj/projectile/bullet/c38/trac name = ".38 TRAC bullet" diff --git a/code/modules/projectiles/projectile/bullets/rifle.dm b/code/modules/projectiles/projectile/bullets/rifle.dm index 1302aea9315a4..67c06e021fea2 100644 --- a/code/modules/projectiles/projectile/bullets/rifle.dm +++ b/code/modules/projectiles/projectile/bullets/rifle.dm @@ -48,19 +48,19 @@ armour_penetration = 50 wound_bonus = -20 bare_wound_bonus = 80 - embed_type = /datum/embed_data/harpoon + embed_type = /datum/embedding/harpoon wound_falloff_tile = -5 shrapnel_type = null -/datum/embed_data/harpoon - embed_chance=100 - fall_chance=3 - jostle_chance=4 - ignore_throwspeed_threshold=TRUE - pain_stam_pct=0.4 - pain_mult=5 - jostle_pain_mult=6 - rip_time=10 +/datum/embedding/harpoon + embed_chance = 100 + fall_chance = 3 + jostle_chance = 4 + ignore_throwspeed_threshold = TRUE + pain_stam_pct = 0.4 + pain_mult = 5 + jostle_pain_mult = 6 + rip_time = 10 // Rebar (Rebar Crossbow) /obj/projectile/bullet/rebar @@ -72,12 +72,12 @@ armour_penetration = 10 wound_bonus = -20 bare_wound_bonus = 20 - embed_type = /datum/embed_data/rebar + embed_type = /datum/embedding/rebar embed_falloff_tile = -5 wound_falloff_tile = -2 shrapnel_type = /obj/item/ammo_casing/rebar -/datum/embed_data/rebar +/datum/embedding/rebar embed_chance = 60 fall_chance = 2 jostle_chance = 2 @@ -98,10 +98,10 @@ wound_bonus = 10 bare_wound_bonus = 20 embed_falloff_tile = -3 - embed_type = /datum/embed_data/rebar_syndie + embed_type = /datum/embedding/rebar_syndie shrapnel_type = /obj/item/ammo_casing/rebar/syndie -/datum/embed_data/rebar_syndie +/datum/embedding/rebar_syndie embed_chance = 80 fall_chance = 1 jostle_chance = 3 @@ -122,11 +122,11 @@ armour_penetration = 20 // not nearly as good, as its not as sharp. wound_bonus = 10 bare_wound_bonus = 40 - embed_type = /datum/embed_data/rebar_zaukerite + embed_type = /datum/embedding/rebar_zaukerite embed_falloff_tile = 0 // very spiky. shrapnel_type = /obj/item/ammo_casing/rebar/zaukerite -/datum/embed_data/rebar_zaukerite +/datum/embedding/rebar_zaukerite embed_chance = 100 fall_chance = 0 jostle_chance = 5 @@ -151,7 +151,7 @@ wound_bonus = -100 bare_wound_bonus = 0 shrapnel_type = /obj/item/ammo_casing/rebar/hydrogen - embed_type = /datum/embed_data/rebar_hydrogen + embed_type = /datum/embedding/rebar_hydrogen embed_falloff_tile = -3 accurate_range = 205 //15 tiles before falloff starts to kick in @@ -159,7 +159,7 @@ . = ..() def_zone = ran_zone(def_zone, clamp(205-(7*get_dist(get_turf(A), starting)), 5, 100)) -/datum/embed_data/rebar_hydrogen +/datum/embedding/rebar_hydrogen embed_chance = 0 /obj/projectile/bullet/rebar/hydrogen/on_hit(atom/target, blocked, pierce_hit) diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm index b718ee94cc1c9..28050b0256f70 100644 --- a/code/modules/reagents/chemistry/reagents/other_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm @@ -1632,6 +1632,7 @@ description = "A powder that is used for coloring things." color = COLOR_WHITE taste_description = "the back of class" + can_color_organs = TRUE var/colorname = "none" /datum/reagent/colorful_reagent/powder/New() @@ -1707,51 +1708,51 @@ name = "White Powder" colorname = "white" color = COLOR_WHITE - random_color_list = list(COLOR_WHITE) //doesn't actually change appearance at all + random_color_list = list(COLOR_WHITE) chemical_flags = REAGENT_CAN_BE_SYNTHESIZED /* used by crayons, can't color living things but still used for stuff like food recipes */ /datum/reagent/colorful_reagent/powder/red/crayon name = "Red Crayon Powder" - can_colour_mobs = FALSE + can_color_mobs = FALSE chemical_flags = REAGENT_CAN_BE_SYNTHESIZED /datum/reagent/colorful_reagent/powder/orange/crayon name = "Orange Crayon Powder" - can_colour_mobs = FALSE + can_color_mobs = FALSE chemical_flags = REAGENT_CAN_BE_SYNTHESIZED /datum/reagent/colorful_reagent/powder/yellow/crayon name = "Yellow Crayon Powder" - can_colour_mobs = FALSE + can_color_mobs = FALSE chemical_flags = REAGENT_CAN_BE_SYNTHESIZED /datum/reagent/colorful_reagent/powder/green/crayon name = "Green Crayon Powder" - can_colour_mobs = FALSE + can_color_mobs = FALSE chemical_flags = REAGENT_CAN_BE_SYNTHESIZED /datum/reagent/colorful_reagent/powder/blue/crayon name = "Blue Crayon Powder" - can_colour_mobs = FALSE + can_color_mobs = FALSE chemical_flags = REAGENT_CAN_BE_SYNTHESIZED /datum/reagent/colorful_reagent/powder/purple/crayon name = "Purple Crayon Powder" - can_colour_mobs = FALSE + can_color_mobs = FALSE chemical_flags = REAGENT_CAN_BE_SYNTHESIZED //datum/reagent/colorful_reagent/powder/invisible/crayon /datum/reagent/colorful_reagent/powder/black/crayon name = "Black Crayon Powder" - can_colour_mobs = FALSE + can_color_mobs = FALSE chemical_flags = REAGENT_CAN_BE_SYNTHESIZED /datum/reagent/colorful_reagent/powder/white/crayon name = "White Crayon Powder" - can_colour_mobs = FALSE + can_color_mobs = FALSE chemical_flags = REAGENT_CAN_BE_SYNTHESIZED //////////////////////////////////Hydroponics stuff/////////////////////////////// @@ -2163,8 +2164,13 @@ var/list/random_color_list = list("#00aedb","#a200ff","#f47835","#d41243","#d11141","#00b159","#00aedb","#f37735","#ffc425","#008744","#0057e7","#d62d20","#ffa700") color = COLOR_GRAY taste_description = "rainbows" - var/can_colour_mobs = TRUE chemical_flags = REAGENT_CAN_BE_SYNTHESIZED + /// Whenever this reagent can color mob limbs and organs upon exposure + var/can_color_mobs = TRUE + /// Whenever this reagent can color mob equipment when they're exposed to it externally + var/can_color_clothing = TRUE + /// Whenever this reagent can color mob organs when taken internally + var/can_color_organs = FALSE // False by default as this would cause chaotic flickering of victim's eyes var/datum/callback/color_callback /datum/reagent/colorful_reagent/New() @@ -2181,15 +2187,63 @@ color_callback = null color = pick(random_color_list) +/datum/reagent/colorful_reagent/expose_mob(mob/living/exposed_mob, methods, reac_volume, show_message, touch_protection) + . = ..() + var/picked_color = pick(random_color_list) + var/color_filter = color_transition_filter(picked_color, SATURATION_OVERRIDE) + if (can_color_clothing && (methods & TOUCH|VAPOR|INHALE)) + var/include_flags = INCLUDE_HELD|INCLUDE_ACCESSORIES + if (methods & VAPOR|INHALE) + include_flags |= INCLUDE_POCKETS + // Not as anyting because this can produce nulls with the flags we passed + for (var/obj/item/to_color in exposed_mob.get_equipped_items(include_flags)) + to_color.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY) + + if (ishuman(exposed_mob)) + var/mob/living/carbon/human/exposed_human = exposed_mob + exposed_human.set_facial_haircolor(picked_color, update = FALSE) + exposed_human.set_haircolor(picked_color) + + if (!can_color_mobs) + return + + if (!iscarbon(exposed_mob)) + exposed_mob.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY) + return + + if (!(methods & TOUCH|VAPOR|INHALE)) + return + + var/mob/living/carbon/exposed_carbon = exposed_mob + for (var/obj/item/bodypart/part as anything in exposed_carbon.bodyparts) + part.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY) + + for (var/obj/item/organ/organ as anything in exposed_carbon.organs) + organ.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY) + /datum/reagent/colorful_reagent/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) . = ..() - if(can_colour_mobs) - affected_mob.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY) + + if (!iscarbon(affected_mob)) + if (can_color_mobs) + affected_mob.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY) + return + + if(!can_color_organs) + return + + var/mob/living/carbon/carbon_mob = affected_mob + var/color_priority = WASHABLE_COLOUR_PRIORITY + if (current_cycle >= 30) // Seeps deep into your tissues + color_priority = FIXED_COLOUR_PRIORITY + + for (var/obj/item/organ/organ as anything in carbon_mob.organs) + organ.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), color_priority) /// Colors anything it touches a random color. /datum/reagent/colorful_reagent/expose_atom(atom/exposed_atom, reac_volume) . = ..() - if(!isliving(exposed_atom) || can_colour_mobs) + if(!isliving(exposed_atom)) exposed_atom.add_atom_colour(color_transition_filter(pick(random_color_list), SATURATION_OVERRIDE), WASHABLE_COLOUR_PRIORITY) /datum/reagent/hair_dye @@ -2210,7 +2264,7 @@ /datum/reagent/hair_dye/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message=TRUE, touch_protection=FALSE) . = ..() - if(!(methods & (TOUCH|VAPOR)) || !ishuman(exposed_mob)) + if(!(methods & (TOUCH|VAPOR|INHALE)) || !ishuman(exposed_mob)) return var/mob/living/carbon/human/exposed_human = exposed_mob @@ -3095,3 +3149,121 @@ /datum/reagent/gold/cursed name = "Cursed Gold" metabolization_rate = 0.2 * REAGENTS_METABOLISM + +/datum/reagent/luminescent_fluid + name = "Green Luminiscent Fluid" + description = "A colored fluid that produces light as a result of a chemical reaction with oxygen." // Reacts with oxygen in hydrogen peroxide IRL + taste_description = "buttery acid" // Best way I can describe glowstick fluid's taste + color = LIGHT_COLOR_GREEN + metabolization_rate = 0.3 * REAGENTS_METABOLISM + ph = 3 + chemical_flags = REAGENT_CAN_BE_SYNTHESIZED + overdose_threshold = 50 // GLOW GLOW GLOW + metabolized_traits = list(TRAIT_MINOR_NIGHT_VISION) + self_consuming = TRUE + /// Fake flashlight we're using to make owner's eyes glow + var/obj/item/flashlight/eyelight/glow/glowing + /// Previous overlay_ignore_lighting of owner's eyes + var/prev_ignore_lighting + /// Have we added a flashlight already and it got destroyed by something? + var/added_light = FALSE + +/datum/reagent/luminescent_fluid/on_mob_metabolize(mob/living/affected_mob) + . = ..() + if (volume > 20) // Even if you don't have eyes, your eyeholes still glow :) + glowing = new(affected_mob) + glowing.set_light_color(color) + glowing.set_light_on(TRUE) + added_light = TRUE + + if (!ishuman(affected_mob)) + return + + var/mob/living/carbon/human/affected_human = affected_mob + affected_human.add_eye_color(color, EYE_COLOR_LUMINESCENT_PRIORITY) + RegisterSignal(affected_human, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(on_organ_added)) + RegisterSignal(affected_human, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(on_organ_removed)) + var/obj/item/organ/eyes/eyes = affected_human.get_organ_slot(ORGAN_SLOT_EYES) + if (eyes && !IS_ROBOTIC_ORGAN(eyes)) + prev_ignore_lighting = eyes.overlay_ignore_lighting + eyes.overlay_ignore_lighting = TRUE + +/datum/reagent/luminescent_fluid/on_mob_end_metabolize(mob/living/affected_mob) + . = ..() + QDEL_NULL(glowing) + if (!ishuman(affected_mob)) + return + + var/mob/living/carbon/human/affected_human = affected_mob + affected_human.remove_eye_color(EYE_COLOR_LUMINESCENT_PRIORITY) + var/obj/item/organ/eyes/eyes = affected_human.get_organ_slot(ORGAN_SLOT_EYES) + if (eyes && !IS_ROBOTIC_ORGAN(eyes) && !overdosed) + eyes.overlay_ignore_lighting = prev_ignore_lighting + +/datum/reagent/luminescent_fluid/on_mob_life(mob/living/affected_mob, seconds_per_tick, times_fired) + . = ..() + + if (isnull(glowing) && !added_light && volume > 20) + glowing = new(affected_mob) + glowing.set_light_color(color) + glowing.set_light_on(TRUE) + added_light = TRUE + + if (SPT_PROB(8, seconds_per_tick)) + if(affected_mob.adjustToxLoss(1, updating_health = FALSE)) + return UPDATE_MOB_HEALTH + +/datum/reagent/luminescent_fluid/proc/on_organ_added(mob/living/source, obj/item/organ/eyes/new_eyes) + SIGNAL_HANDLER + + if (istype(new_eyes) && !IS_ROBOTIC_ORGAN(new_eyes)) + prev_ignore_lighting = new_eyes.overlay_ignore_lighting + new_eyes.overlay_ignore_lighting = TRUE + +/datum/reagent/luminescent_fluid/proc/on_organ_removed(mob/living/source, obj/item/organ/eyes/old_eyes) + SIGNAL_HANDLER + + if (istype(old_eyes) && !IS_ROBOTIC_ORGAN(old_eyes) && !overdosed) + old_eyes.overlay_ignore_lighting = prev_ignore_lighting + +/datum/reagent/luminescent_fluid/overdose_start(mob/living/affected_mob) + . = ..() + if (!ishuman(affected_mob)) + return + var/mob/living/carbon/human/affected_human = affected_mob + var/obj/item/organ/eyes/eyes = affected_human.get_organ_slot(ORGAN_SLOT_EYES) + if (eyes && !IS_ROBOTIC_ORGAN(eyes)) + eyes.eye_color_left = color + eyes.eye_color_right = color + +/datum/reagent/luminescent_fluid/red + name = "Red Luminiscent Fluid" + color = COLOR_SOFT_RED + // The glow *is* unnatural, so... + metabolized_traits = list(TRAIT_MINOR_NIGHT_VISION, TRAIT_UNNATURAL_RED_GLOWY_EYES) + +/datum/reagent/luminescent_fluid/overdose_start(mob/living/affected_mob) + . = ..() + if (!ishuman(affected_mob)) + return + ADD_TRAIT(affected_mob, TRAIT_UNNATURAL_RED_GLOWY_EYES, OVERDOSE_TRAIT) + +/datum/reagent/luminescent_fluid/blue + name = "Blue Luminiscent Fluid" + color = LIGHT_COLOR_BLUE + +/datum/reagent/luminescent_fluid/cyan + name = "Cyan Luminiscent Fluid" + color = LIGHT_COLOR_CYAN + +/datum/reagent/luminescent_fluid/yellow + name = "Yellow Luminiscent Fluid" + color = LIGHT_COLOR_DIM_YELLOW + +/datum/reagent/luminescent_fluid/orange + name = "Orange Luminiscent Fluid" + color = LIGHT_COLOR_ORANGE + +/datum/reagent/luminescent_fluid/pink + name = "Pink Luminiscent Fluid" + color = LIGHT_COLOR_PINK diff --git a/code/modules/reagents/reagent_containers/cups/organ_jar.dm b/code/modules/reagents/reagent_containers/cups/organ_jar.dm new file mode 100644 index 0000000000000..927e347714812 --- /dev/null +++ b/code/modules/reagents/reagent_containers/cups/organ_jar.dm @@ -0,0 +1,160 @@ +// The organ jar - a 150u bottle that can hold a single organ +/obj/item/reagent_containers/cup/organ_jar + name = "organ jar" + desc = "A jar large enough to put an organ inside it." + possible_transfer_amounts = list(10, 20, 30, 50, 150) + // It's pretty big + volume = 150 + icon_state = "organ_jar" + fill_icon_state = "organ_jar" + inhand_icon_state = "atoxinbottle" + worn_icon_state = "bottle" + fill_icon_thresholds = list(0, 1, 20, 40, 60, 80, 100) + w_class = WEIGHT_CLASS_SMALL // Organs are small by default, so the jar should be at least small as well + // The organ that is currently inside the jar + var/obj/item/organ/held_organ = null + // Whether the jar should preserve the organ inside (which would only happen if it's full of formaldehyde) + var/full_of_formaldehyde = FALSE + +/obj/item/reagent_containers/cup/organ_jar/examine(mob/user) + . = ..() + . += span_info("Any organ inside the jar will be preserved if it is filled with formaldehyde.") + if(held_organ && held_organ.GetComponent(/datum/component/ghostrole_on_revive)) + . += span_smallnoticeital("The brain is twitching...") // Guaranteed to be a brain if it has that component + +/obj/item/reagent_containers/cup/organ_jar/Initialize(mapload) + . = ..() + update_appearance() + +/obj/item/reagent_containers/cup/organ_jar/Destroy(force) + . = ..() + QDEL_NULL(held_organ) + +// Alt click lets you take the organ out, if it's present +/obj/item/reagent_containers/cup/organ_jar/click_alt(mob/user) + if(held_organ) + balloon_alert(user, "removed [held_organ]") + user.put_in_hands(held_organ) + held_organ.organ_flags &= ~ORGAN_FROZEN + held_organ = null + name = initial(name) + desc = initial(desc) + update_appearance() + return CLICK_ACTION_SUCCESS + return ..() + +// Clicking on the jar with an organ lets you put the organ inside, if there isn't one already +// Otherwise it should act like a normal bottle +/obj/item/reagent_containers/cup/organ_jar/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + . = ..() + if(!istype(tool, /obj/item/organ)) + return + if(held_organ) + balloon_alert(user, "the jar already contains [held_organ]") + return ITEM_INTERACT_BLOCKING + + if(!user.transferItemToLoc(tool, src)) + return ITEM_INTERACT_BLOCKING + balloon_alert(user, "inserted [tool]") + held_organ = tool + name = "[tool.name] in a jar" + desc = "A jar with the [tool.name] inside it." + check_organ_freeze() + update_appearance() + return ITEM_INTERACT_SUCCESS + +// Organ icon size goes from 32 to this +#define JAR_INNER_ICON_SIZE 24 + +/obj/item/reagent_containers/cup/organ_jar/update_overlays() + . = ..() + // Draw the organ icon inside the jar, if present + if(!isnull(held_organ)) + var/image/organ_img = image(held_organ, src) + var/list/icon_dimensions = get_icon_dimensions(held_organ.icon) + organ_img.transform = organ_img.transform.Scale( // Make it smaller so it fits + JAR_INNER_ICON_SIZE / icon_dimensions["width"], + JAR_INNER_ICON_SIZE / icon_dimensions["height"], + ) + organ_img.pixel_y -= 3 + organ_img.layer = FLOAT_LAYER + organ_img.plane = FLOAT_PLANE + organ_img.blend_mode = BLEND_INSET_OVERLAY + . += organ_img + +#undef JAR_INNER_ICON_SIZE + +/obj/item/reagent_containers/cup/organ_jar/on_reagent_change(datum/reagents/holder, ...) + . = ..() + full_of_formaldehyde = !!holder.has_reagent(/datum/reagent/toxin/formaldehyde, amount = holder.maximum_volume) + check_organ_freeze() + +// Proc that stops the held organ from rotting if the jar is full of formaldehyde +/obj/item/reagent_containers/cup/organ_jar/proc/check_organ_freeze() + if(isnull(held_organ)) + return + if(full_of_formaldehyde) + held_organ.organ_flags |= ORGAN_FROZEN + else + held_organ.organ_flags &= ~ORGAN_FROZEN + +// Defines for note flavor types +// One of these is picked whenever a brain in a jar is created +// A note with a "stuck in mail" flavor will appear upon examining more +#define NOTE_STUCK_IN_MAIL 0 +// A note with a "gift from a fellow morbid researcher" flavor will appear upon examining more +#define NOTE_MORBID_GIFT 1 +// A note with a "discarded brain from the recovered crew" flavor will appear upon examining more +#define NOTE_DISCARDED_LOST_CREW 2 + +/obj/item/reagent_containers/cup/organ_jar/brain_in_a_jar + // Which note to show when someone examins more + var/note_type = NOTE_STUCK_IN_MAIL + +/obj/item/reagent_containers/cup/organ_jar/brain_in_a_jar/examine(mob/user) + . = ..() + . += span_notice("You can see a note attached to the bottom..") + +/obj/item/reagent_containers/cup/organ_jar/brain_in_a_jar/examine_more(mob/user) + . = ..() + // Flavor for why the brain is scarred + switch(note_type) + if(NOTE_STUCK_IN_MAIL) + . += span_notice("According to the note, this jar must've been stuck in the mail for at least 50 years...") + if(NOTE_MORBID_GIFT) + . += span_notice("It reads...") + . += span_notice("Greetings, XXX. I stumbled upon a hermit in my travels, \ + whose quirks immediately piqued my interest. I'm sure his brain will be as useful to your research \ + as it has been to mine. Signed, YYY.") + if(NOTE_DISCARDED_LOST_CREW) + . += span_notice("It reads...") + . += span_notice("Hey, XXX. Management wanted me to discard this poor schmuck's brain, \ + claiming it's 'too damaged to viably recover', so I figured I might as well throw you a bone. \ + I know you like these sorts of things. Signed, ZZZ.") + + +/obj/item/reagent_containers/cup/organ_jar/brain_in_a_jar/Initialize(mapload) + . = ..() + note_type = rand(0, 2) // Attach a random note to it + var/obj/item/organ/brain/scarred_brain = new() // Make a new brain + // Make it revivable, scar it if revival is successful + scarred_brain.AddComponent( \ + /datum/component/ghostrole_on_revive,\ + refuse_revival_if_failed = TRUE, \ + on_successful_revive = CALLBACK(src, PROC_REF(handle_revival), scarred_brain) \ + ) + held_organ = scarred_brain // Put the brain inside the jar + reagents.add_reagent(/datum/reagent/toxin/formaldehyde, reagents.maximum_volume) // Fill the jar with formaldehyde + name = "brain in a jar" // Set a custom name&description + desc = "A brain in a jar. You can see it twitching..." + update_appearance() + +// All this does is add a random special brain trauma + add recovered crew antag datum for logging +/obj/item/reagent_containers/cup/organ_jar/brain_in_a_jar/proc/handle_revival(obj/item/organ/brain/brain_to_scar) + brain_to_scar.gain_trauma_type(BRAIN_TRAUMA_SPECIAL, TRAUMA_RESILIENCE_ABSOLUTE, natural_gain = TRUE) + var/mob/living/carbon/human/owner = brain_to_scar.owner + owner.mind.add_antag_datum(/datum/antagonist/recovered_crew) // for tracking mostly (c) + +#undef NOTE_STUCK_IN_MAIL +#undef NOTE_MORBID_GIFT +#undef NOTE_DISCARDED_LOST_CREW diff --git a/code/modules/recycling/conveyor.dm b/code/modules/recycling/conveyor.dm index 44d9631a60950..9e03dc98aeacc 100644 --- a/code/modules/recycling/conveyor.dm +++ b/code/modules/recycling/conveyor.dm @@ -248,8 +248,10 @@ GLOBAL_LIST_EMPTY(conveyors_by_id) start_conveying(movable) return TRUE -/obj/machinery/conveyor/proc/conveyable_enter(datum/source, atom/convayable) +/obj/machinery/conveyor/proc/conveyable_enter(datum/source, atom/movable/convayable) SIGNAL_HANDLER + if(convayable.loc != loc) // If we are not on the same turf (order of operations memes) go to hell + return if(operating == CONVEYOR_OFF) GLOB.move_manager.stop_looping(convayable, SSconveyors) return diff --git a/code/modules/spells/spell_types/self/summonitem.dm b/code/modules/spells/spell_types/self/summonitem.dm index 4165781c3668f..acea28fc19b55 100644 --- a/code/modules/spells/spell_types/self/summonitem.dm +++ b/code/modules/spells/spell_types/self/summonitem.dm @@ -128,17 +128,17 @@ // If its on someone, properly drop it if(ismob(item_to_retrieve.loc)) - var/mob/holding_mark = item_to_retrieve.loc - - // Items in silicons warp the whole silicon - if(issilicon(holding_mark)) - holding_mark.loc.visible_message(span_warning("[holding_mark] suddenly disappears!")) - holding_mark.forceMove(caster.loc) - holding_mark.loc.visible_message(span_warning("[holding_mark] suddenly appears!")) - item_to_retrieve = null + if(!issilicon(item_to_retrieve.loc)) break - holding_mark.dropItemToGround(item_to_retrieve) + // Items in silicons warp the whole silicon + var/mob/holding_mark = item_to_retrieve.loc + holding_mark.loc.visible_message(span_warning("[holding_mark] suddenly disappears!")) + holding_mark.forceMove(caster.loc) + holding_mark.loc.visible_message(span_warning("[holding_mark] suddenly appears!")) + SEND_SIGNAL(holding_mark, COMSIG_MAGIC_RECALL, caster, item_to_retrieve) + playsound(holding_mark, 'sound/effects/magic/summonitems_generic.ogg', 50, TRUE) + return else if(isobj(item_to_retrieve.loc)) var/obj/retrieved_item = item_to_retrieve.loc @@ -159,6 +159,13 @@ if(!item_to_retrieve) return + SEND_SIGNAL(item_to_retrieve, COMSIG_MAGIC_RECALL, caster, item_to_retrieve) + + if (ismob(item_to_retrieve.loc)) + var/mob/holder = item_to_retrieve.loc + if (!holder.dropItemToGround(item_to_retrieve, force = TRUE)) + return + item_to_retrieve.loc?.visible_message(span_warning("[item_to_retrieve] suddenly disappears!")) if(isitem(item_to_retrieve) && caster.put_in_hands(item_to_retrieve)) @@ -167,7 +174,6 @@ item_to_retrieve.forceMove(caster.drop_location()) item_to_retrieve.loc.visible_message(span_warning("[item_to_retrieve] suddenly appears!")) - SEND_SIGNAL(item_to_retrieve, COMSIG_MAGIC_RECALL, caster, item_to_retrieve) playsound(get_turf(item_to_retrieve), 'sound/effects/magic/summonitems_generic.ogg', 50, TRUE) /datum/action/cooldown/spell/summonitem/abductor diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm index 41762e956f5fc..dd35741407430 100644 --- a/code/modules/surgery/bodyparts/_bodyparts.dm +++ b/code/modules/surgery/bodyparts/_bodyparts.dm @@ -364,9 +364,14 @@ if(WOUND_SEVERITY_CRITICAL) check_list += "\t [span_boldwarning("Ваша [declent_ru(NOMINATIVE)] страдает от: [LOWER_TEXT(wound.name)]!!!")]" - for(var/obj/item/embedded_thing in embedded_objects) - var/stuck_word = embedded_thing.is_embed_harmless() ? "прилип" : "застрял" - check_list += "\t [capitalize(embedded_thing.declent_ru(NOMINATIVE))] [stuck_word] [stuck_word == "застрял" ? "в" : "к"] вашей [declent_ru(DATIVE)]!" + for(var/obj/item/embedded_thing as anything in embedded_objects) + var/harmless = embedded_thing.get_embed().is_harmless() + var/stuck_wordage = harmless ? "застрял" : "прилип" + var/embed_text = "\t [icon2html(embedded_thing, examiner)] [capitalize(embedded_thing.declent_ru(NOMINATIVE))] [stuck_wordage == "застрял" ? "в" : "к"] вашей [declent_ru(DATIVE)]!" + if (harmless) + check_list += span_italics(span_notice(embed_text)) + else + check_list += span_boldwarning(embed_text) /obj/item/bodypart/blob_act() receive_damage(max_damage, wound_bonus = CANT_WOUND) @@ -790,7 +795,7 @@ SIGNAL_ADDTRAIT(TRAIT_NOBLOOD), )) - UnregisterSignal(old_owner, COMSIG_ATOM_RESTYLE) + UnregisterSignal(old_owner, list(COMSIG_ATOM_RESTYLE, COMSIG_COMPONENT_CLEAN_ACT)) /// Apply ownership of a limb to someone, giving the appropriate traits, updates and signals /obj/item/bodypart/proc/apply_ownership(mob/living/carbon/new_owner) @@ -819,6 +824,7 @@ update_disabled() RegisterSignal(owner, COMSIG_ATOM_RESTYLE, PROC_REF(on_attempt_feature_restyle_mob)) + RegisterSignal(owner, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(on_owner_clean)) forceMove(owner) RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(on_forced_removal)) //this must be set after we moved, or we insta gib @@ -974,7 +980,12 @@ /obj/item/bodypart/proc/remove_color_override(color_priority) LAZYREMOVE(color_overrides, "[color_priority]") -//to update the bodypart's icon when not attached to a mob +/// Called when limb's current owner gets washed +/obj/item/bodypart/proc/on_owner_clean(mob/living/carbon/source, clean_types) + SIGNAL_HANDLER + wash(clean_types) + +/// To update the bodypart's icon when not attached to a mob /obj/item/bodypart/proc/update_icon_dropped() SHOULD_CALL_PARENT(TRUE) @@ -988,6 +999,24 @@ img.pixel_y += px_y add_overlay(standing) +/obj/item/bodypart/update_atom_colour() + . = ..() + for(var/i in 1 to COLOUR_PRIORITY_AMOUNT) + var/list/checked_color = atom_colours[i] + if (!checked_color) + remove_color_override(LIMB_COLOR_ATOM_COLOR + i) + continue + var/actual_color = checked_color[ATOM_COLOR_VALUE_INDEX] + if (checked_color[ATOM_COLOR_TYPE_INDEX] == ATOM_COLOR_TYPE_FILTER) + var/color_filter = checked_color[ATOM_COLOR_VALUE_INDEX] + actual_color = apply_matrix_to_color(COLOR_WHITE, color_filter["color"], color_filter["space"] || COLORSPACE_RGB) + add_color_override(actual_color, LIMB_COLOR_ATOM_COLOR + i) + update_limb() + if (owner) + owner.update_body_parts() + else + update_icon_dropped() + ///Generates an /image for the limb to be used as an overlay /obj/item/bodypart/proc/get_limb_icon(dropped) SHOULD_CALL_PARENT(TRUE) @@ -1194,8 +1223,8 @@ if(generic_bleedstacks > 0) cached_bleed_rate += 0.5 - for(var/obj/item/embeddies in embedded_objects) - if(!embeddies.is_embed_harmless()) + for(var/obj/item/embeddies as anything in embedded_objects) + if(!embeddies.get_embed().is_harmless()) cached_bleed_rate += 0.25 for(var/datum/wound/iter_wound as anything in wounds) diff --git a/code/modules/surgery/bodyparts/dismemberment.dm b/code/modules/surgery/bodyparts/dismemberment.dm index c333408c5d7e2..0a4be6850393f 100644 --- a/code/modules/surgery/bodyparts/dismemberment.dm +++ b/code/modules/surgery/bodyparts/dismemberment.dm @@ -106,8 +106,6 @@ qdel(surgery) break - for(var/obj/item/embedded in embedded_objects) - embedded.forceMove(src) // It'll self remove via signal reaction, just need to move it if(!phantom_owner.has_embedded_objects()) phantom_owner.clear_alert(ALERT_EMBEDDED_OBJECT) phantom_owner.clear_mood_event("embedded") diff --git a/code/modules/surgery/bodyparts/helpers.dm b/code/modules/surgery/bodyparts/helpers.dm index 2f9a42e0d1f9c..8d4d65de80f3c 100644 --- a/code/modules/surgery/bodyparts/helpers.dm +++ b/code/modules/surgery/bodyparts/helpers.dm @@ -129,18 +129,20 @@ ///Remove a specific embedded item from the carbon mob /mob/living/carbon/proc/remove_embedded_object(obj/item/embedded) - SEND_SIGNAL(src, COMSIG_CARBON_EMBED_REMOVAL, embedded) + if (embedded.get_embed()?.owner != src) + return + embedded.get_embed().remove_embedding() ///Remove all embedded objects from all limbs on the carbon mob /mob/living/carbon/proc/remove_all_embedded_objects() for(var/obj/item/bodypart/bodypart as anything in bodyparts) - for(var/obj/item/embedded in bodypart.embedded_objects) + for(var/obj/item/embedded as anything in bodypart.embedded_objects) remove_embedded_object(embedded) -/mob/living/carbon/proc/has_embedded_objects(include_harmless=FALSE) +/mob/living/carbon/proc/has_embedded_objects(include_harmless = FALSE) for(var/obj/item/bodypart/bodypart as anything in bodyparts) - for(var/obj/item/embedded in bodypart.embedded_objects) - if(!include_harmless && embedded.is_embed_harmless()) + for(var/obj/item/embedded as anything in bodypart.embedded_objects) + if(!include_harmless && embedded.get_embed().is_harmless()) continue return TRUE diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_eyes.dm b/code/modules/surgery/organs/internal/cyberimp/augments_eyes.dm index 204247e4de822..cdb881f02dded 100644 --- a/code/modules/surgery/organs/internal/cyberimp/augments_eyes.dm +++ b/code/modules/surgery/organs/internal/cyberimp/augments_eyes.dm @@ -20,11 +20,11 @@ /obj/item/organ/cyberimp/eyes/hud/proc/toggle_hud(mob/living/carbon/eye_owner) if(toggled_on) toggled_on = FALSE - eye_owner.add_traits(HUD_traits, ORGAN_TRAIT) + eye_owner.remove_traits(HUD_traits, ORGAN_TRAIT) balloon_alert(eye_owner, "hud disabled") return toggled_on = TRUE - eye_owner.remove_traits(HUD_traits, ORGAN_TRAIT) + eye_owner.add_traits(HUD_traits, ORGAN_TRAIT) balloon_alert(eye_owner, "hud enabled") /obj/item/organ/cyberimp/eyes/hud/on_mob_insert(mob/living/carbon/eye_owner, special = FALSE, movement_flags) diff --git a/code/modules/surgery/organs/internal/eyes/_eyes.dm b/code/modules/surgery/organs/internal/eyes/_eyes.dm index daf0b0b060cd3..7c65a6d68769a 100644 --- a/code/modules/surgery/organs/internal/eyes/_eyes.dm +++ b/code/modules/surgery/organs/internal/eyes/_eyes.dm @@ -56,6 +56,7 @@ apply_damaged_eye_effects() refresh(receiver, call_update = !special) RegisterSignal(receiver, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_bullet_act)) + RegisterSignal(receiver, COMSIG_COMPONENT_CLEAN_FACE_ACT, PROC_REF(on_face_wash)) if (scarring) apply_scarring_effects() @@ -69,10 +70,11 @@ return var/mob/living/carbon/human/affected_human = eye_owner - if(eye_color_left) + if(length(eye_color_left)) affected_human.add_eye_color_left(eye_color_left, EYE_COLOR_ORGAN_PRIORITY, update_body = FALSE) - if(eye_color_right) + if(length(eye_color_right)) affected_human.add_eye_color_right(eye_color_right, EYE_COLOR_ORGAN_PRIORITY, update_body = FALSE) + refresh_atom_color_overrides() if(HAS_TRAIT(affected_human, TRAIT_NIGHT_VISION) && !lighting_cutoff) lighting_cutoff = LIGHTING_CUTOFF_REAL_LOW @@ -88,6 +90,8 @@ if(ishuman(organ_owner)) var/mob/living/carbon/human/human_owner = organ_owner human_owner.remove_eye_color(EYE_COLOR_ORGAN_PRIORITY, update_body = FALSE) + for(var/i in 1 to COLOUR_PRIORITY_AMOUNT) + human_owner.remove_eye_color(EYE_COLOR_ATOM_COLOR_PRIORITY + i, update_body = FALSE) if(native_fov) organ_owner.remove_fov_trait(type) if(!special) @@ -105,7 +109,45 @@ organ_owner.update_tint() organ_owner.update_sight() - UnregisterSignal(organ_owner, COMSIG_ATOM_BULLET_ACT) + UnregisterSignal(organ_owner, list(COMSIG_ATOM_BULLET_ACT, COMSIG_COMPONENT_CLEAN_FACE_ACT)) + +/obj/item/organ/eyes/update_atom_colour() + . = ..() + if (ishuman(owner)) + refresh_atom_color_overrides() + owner.update_body() + +/// Adds eye color overrides to our owner from our atom color +/obj/item/organ/eyes/proc/refresh_atom_color_overrides() + if (!atom_colours) + return + + var/mob/living/carbon/human/human_owner = owner + for(var/i in 1 to COLOUR_PRIORITY_AMOUNT) + var/list/checked_color = atom_colours[i] + if (!checked_color) + human_owner.remove_eye_color(EYE_COLOR_ATOM_COLOR_PRIORITY + i, update_body = FALSE) + continue + + var/left_color = COLOR_WHITE + var/right_color = COLOR_WHITE + + if (length(eye_color_left)) + left_color = eye_color_left + if (length(eye_color_right)) + right_color = eye_color_right + + if (checked_color[ATOM_COLOR_TYPE_INDEX] == ATOM_COLOR_TYPE_FILTER) + var/color_filter = checked_color[ATOM_COLOR_VALUE_INDEX] + left_color = apply_matrix_to_color(left_color, color_filter["color"], color_filter["space"] || COLORSPACE_RGB) + right_color = apply_matrix_to_color(right_color, color_filter["color"], color_filter["space"] || COLORSPACE_RGB) + else + var/list/target_color = color_transition_filter(checked_color[ATOM_COLOR_VALUE_INDEX], SATURATION_OVERRIDE) + left_color = apply_matrix_to_color(left_color, target_color["color"], COLORSPACE_HSL) + right_color = apply_matrix_to_color(right_color, target_color["color"], COLORSPACE_HSL) + + human_owner.add_eye_color_left(left_color, EYE_COLOR_ATOM_COLOR_PRIORITY + i, update_body = FALSE) + human_owner.add_eye_color_right(right_color, EYE_COLOR_ATOM_COLOR_PRIORITY + i, update_body = FALSE) /obj/item/organ/eyes/proc/on_bullet_act(mob/living/carbon/source, obj/projectile/proj, def_zone, piercing_hit, blocked) SIGNAL_HANDLER @@ -134,6 +176,11 @@ eye_puncture.apply_wound(bodypart_owner, wound_source = "bullet impact", right_side = picked_side) apply_scar(picked_side) +/// When our owner washes their face. The idea that spessmen wash their eyeballs is highly disturbing but this is the easiest way to get rid of cursed crayon eye coloring +/obj/item/organ/eyes/proc/on_face_wash() + SIGNAL_HANDLER + wash(CLEAN_WASH) + #define OFFSET_X 1 #define OFFSET_Y 2 diff --git a/code/modules/unit_tests/embedding.dm b/code/modules/unit_tests/embedding.dm index 5e6a8a90647ef..f5d730ec27797 100644 --- a/code/modules/unit_tests/embedding.dm +++ b/code/modules/unit_tests/embedding.dm @@ -4,11 +4,10 @@ var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human/consistent) var/mob/living/carbon/human/firer = allocate(/mob/living/carbon/human/consistent) var/obj/projectile/bullet/c38/bullet = new(get_turf(firer)) - bullet.set_embed(bullet.get_embed().generate_with_values(embed_chance = 100)) + bullet.get_embed().embed_chance = 100 TEST_ASSERT_EQUAL(bullet.get_embed().embed_chance, 100, "embed_chance failed to modify") bullet.aim_projectile(victim, firer) bullet.fire(get_angle(firer, victim), victim) - var/list/components = victim.GetComponents(/datum/component/embedded) - TEST_ASSERT_EQUAL(components.len, 1, "Projectile with 100% embed chance didn't embed, or embedded multiple times") - var/datum/component/embedded/comp = components[1] - TEST_ASSERT_EQUAL(comp.weapon.get_embed().embed_chance, 100, "embed_chance modification did not transfer to shrapnel") + var/obj/item/shrapnel/shrapnel = locate() in victim + TEST_ASSERT(!isnull(shrapnel), "Projectile with 100% embed chance didn't embed") + TEST_ASSERT_EQUAL(shrapnel.get_embed().embed_chance, 100, "embed_chance modification did not transfer to shrapnel") diff --git a/code/modules/uplink/uplink_items/stealthy_tools.dm b/code/modules/uplink/uplink_items/stealthy_tools.dm index 000364f27be47..7268ef5efe359 100644 --- a/code/modules/uplink/uplink_items/stealthy_tools.dm +++ b/code/modules/uplink/uplink_items/stealthy_tools.dm @@ -104,6 +104,13 @@ cost = 1 surplus = 30 +/datum/uplink_item/stealthy_tools/forensics_spofer + name = "Forensics Spoofing Kit" + desc = "A box that contains the forensics spoofer (and instructions) which can scan and replicate fingerprints and fibers \ + and apply them to a target object. Helpful for framing crew. Recommend buying soap with your purchase." + item = /obj/item/storage/box/syndie_kit/forensics_spoofer + cost = 5 + /datum/uplink_item/stealthy_tools/telecomm_blackout name = "Disable Telecomms" desc = "When purchased, a virus will be uploaded to the telecommunication processing servers to temporarily disable themselves." diff --git a/code/modules/vehicles/mecha/mecha_ai_interaction.dm b/code/modules/vehicles/mecha/mecha_ai_interaction.dm index 4b4d92f06a268..f43a11903a950 100644 --- a/code/modules/vehicles/mecha/mecha_ai_interaction.dm +++ b/code/modules/vehicles/mecha/mecha_ai_interaction.dm @@ -66,7 +66,9 @@ return if(AI_MECH_HACK) //Called by AIs on the mech - AI.linked_core = new /obj/structure/ai_core/deactivated(AI.loc) + var/obj/structure/ai_core/deactivated/deactivated_core = new(AI.loc, FALSE, FALSE, AI) + AI.linked_core = deactivated_core + AI.linked_core.RegisterSignal(deactivated_core, COMSIG_ATOM_DESTRUCTION, TYPE_PROC_REF(/obj/structure/ai_core/deactivated, disable_doomsday)) //Protect that core! The structure goes bye-bye when we re-shunt back in so no need for cleanup. AI.linked_core.remote_ai = AI if(AI.can_dominate_mechs && LAZYLEN(occupants)) //Oh, I am sorry, were you using that? to_chat(AI, span_warning("Occupants detected! Forced ejection initiated!")) diff --git a/code/modules/vehicles/vehicle_key.dm b/code/modules/vehicles/vehicle_key.dm index cb485ed1335c5..46ad27f4bae50 100644 --- a/code/modules/vehicles/vehicle_key.dm +++ b/code/modules/vehicles/vehicle_key.dm @@ -34,11 +34,11 @@ attack_verb_continuous = list("stubs", "pokes") attack_verb_simple = list("stub", "poke") sharpness = SHARP_EDGED - embed_type = /datum/embed_data/janicart_key + embed_type = /datum/embedding/janicart_key wound_bonus = -1 bare_wound_bonus = 2 -/datum/embed_data/janicart_key +/datum/embedding/janicart_key pain_mult = 1 embed_chance = 30 fall_chance = 70 diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm index 27527eb764332..f723ebfcf5f82 100644 --- a/code/modules/vending/_vending.dm +++ b/code/modules/vending/_vending.dm @@ -1082,7 +1082,7 @@ GLOBAL_LIST_EMPTY(vending_machines_to_restock) var/mob/living/carbon/carbon_target = atom_target for(var/i in 1 to num_shards) var/obj/item/shard/shard = new /obj/item/shard(get_turf(carbon_target)) - shard.set_embed(/datum/embed_data/glass_candy) + shard.set_embed(/datum/embedding/glass_candy) carbon_target.hitby(shard, skipcatch = TRUE, hitpush = FALSE) shard.set_embed(initial(shard.embed_type)) return TRUE diff --git a/html/changelogs/AutoChangeLog-pr-88478.yml b/html/changelogs/AutoChangeLog-pr-88478.yml deleted file mode 100644 index 87ba8bf5daf38..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-88478.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "grungussuss" -delete-after: True -changes: - - rscadd: "computers and airlocks are now leanable" - - refactor: "changed how density/collision of some objects is changed, report any oddities!" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-88636.yml b/html/changelogs/AutoChangeLog-pr-88636.yml deleted file mode 100644 index 04193e1590d71..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-88636.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "mc-oofert" -delete-after: True -changes: - - qol: "you can adjust diagonal walls to be not diagonal walls with a wrench" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-88730.yml b/html/changelogs/AutoChangeLog-pr-88730.yml new file mode 100644 index 0000000000000..56d822141f7dd --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-88730.yml @@ -0,0 +1,4 @@ +author: "WebcomicArtist" +delete-after: True +changes: + - qol: "\"Destroy Blackbox\" objective now tells you how to destroy something invulnerable to conventional means of destruction." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-88764.yml b/html/changelogs/AutoChangeLog-pr-88764.yml deleted file mode 100644 index 0e7b7cd8beb19..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-88764.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "00-Steven" -delete-after: True -changes: - - bugfix: "Photocopying no longer removes the stamp overlays from the original paper, and actually copies them to the copy." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-88852.yml b/html/changelogs/AutoChangeLog-pr-88852.yml new file mode 100644 index 0000000000000..a89493f861175 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-88852.yml @@ -0,0 +1,6 @@ +author: "Hatterhat" +delete-after: True +changes: + - bugfix: "Proto-kinetic crushers no longer cause a runtime every time you hit a mob that doesn't have a mark." + - bugfix: "Equipment pieces with upgradable armor (e.g. explorer suits) now properly list what they use to upgrade." + - bugfix: "Mobs without targetable limbs no longer have extraneous spaces appear in their hit-by-projectile messages." \ No newline at end of file diff --git a/html/changelogs/archive/2024-12.yml b/html/changelogs/archive/2024-12.yml index 763a62a1c6230..1d9a421178bad 100644 --- a/html/changelogs/archive/2024-12.yml +++ b/html/changelogs/archive/2024-12.yml @@ -764,3 +764,24 @@ - admin: Add debugging sound earmuffs to admin equipment inside the debug box. Wear them to determine a sounds max range, distance, volume, and sound name. Highly recommended to walk otherwise you will get spammed with footstep sounds. +2024-12-30: + 00-Steven: + - bugfix: Photocopying no longer removes the stamp overlays from the original paper, + and actually copies them to the copy. + Rhials: + - balance: When dominating a mech as a Malfunctioning AI, the core you shunted from + will disable your Doomsday Counterdown when destroyed. Make sure to protect + that core! + grungussuss: + - rscadd: computers and airlocks are now leanable + - refactor: changed how density/collision of some objects is changed, report any + oddities! + mc-oofert: + - qol: you can adjust diagonal walls to be not diagonal walls with a wrench +2024-12-31: + Kylerace: + - bugfix: holodeck is slightly less likely to explode the server + SmArtKar: + - bugfix: Blood no longer gets colored with the item its attached to + mc-oofert: + - rscadd: forensics spoofing kit for traitors/whoever with an uplink diff --git a/html/changelogs/archive/2025-01.yml b/html/changelogs/archive/2025-01.yml new file mode 100644 index 0000000000000..c700a43fe288d --- /dev/null +++ b/html/changelogs/archive/2025-01.yml @@ -0,0 +1,86 @@ +2025-01-01: + 00-Steven: + - bugfix: Stamps no longer render below the paper sometimes. + - bugfix: Stamps no longer inherit the color of the paper they're on. + - bugfix: Clearing paper, like by splashing it with ethanol, actually resets its + icon state to the cleared version. + 00-Steven, SmArtKar: + - bugfix: Paper on clipboards uses its own colour rather than that of the clipboard. + Absolucy, Flleeppyy: + - sound: Added a new, unique sound for polling! + Absolucy, S34NW: + - bugfix: Chat settings properly save on BYOND 516 now. Settings still won't carry + over from 515 tho, 515 and 516 settings will be separate. + Arturlang: + - bugfix: The unique AI station trait will no longer be able to choose lawsets set + as default in the config. + LemonInTheDark: + - bugfix: Boulders will no longer randomly run free from smelting pipelines! We + have enslaved them once more. + Namelessfairy and SmArtKar: + - bugfix: The Extradimensional Blade no longer infinitely scales damage + - bugfix: The nullblade correctly does increased damage when sharpened + SmArtKar: + - rscadd: 'Changed how colorful reagent and crayon powder work: douse your victims + to color their clothing, bodyparts and even internal organs!' + - rscadd: You can wash your eyes when washing your face at a sink + - bugfix: You can color robotic limbs with left click (again) + - bugfix: Mice no longer can spawn in unsafe atmos from garbage spawners + mc-oofert: + - bugfix: holodeck no longer explodes if the server lags while its loading a new + sim +2025-01-02: + Ghommie: + - bugfix: Examining a trophy fish no longer shows the current day instead of when + it was actually caught and put on the mount. + Melbert: + - bugfix: Fixes Omelettes and Carbonara not making you speak French or Italian, + buuut... + - qol: Omelettes and Carbonara will now only make you speak French or Italian if + they were prepared with love (via chef's kiss) + SyncIt21: + - bugfix: dropping all the mobs' contents in some special admin cases won't gib + the player +2025-01-03: + AyIong: + - admin: Ticket actions buttons in chat got a little bigger + - bugfix: Highlighted PMs, vote results and examine will not have weird black title + Hatterhat: + - spellcheck: Removed an errant typo on the recharger's readout while recharging + the BR-38. + SmArtKar: + - spellcheck: Removed outdated examine line from spraycans + - refactor: Completely rewrote all item and projectile embedding code + - bugfix: Instant summons now actually drags people who had the summoned item embedded + into them + - bugfix: Items embedded into limbs no longer get shoved into limbs deep enough + for them to never be found again upon delimbing. + - bugfix: You can see scar descriptions upon detailed examination of targets + - bugfix: Fixed rolling table being able to carry an infinite amount of dwarves + - bugfix: Folders and paper biscuits no longer share their color with their contents + - spellcheck: Fixed a typo in battle rifle crate's description + - rscadd: You can chug glowstick fluid to get glowing eyes + - bugfix: Added command access requirement to the door between EVA and bridge on + Nebula's escape shuttle + TealSeer: + - qol: you can now examine the jukebox to see what song is currently playing + zoomachina: + - rscadd: brain in a jar, a rare coroner mail goodie + - qol: soul-less brains that can be revived (like those from recovered crew) now + have an examine tip +2025-01-04: + Bisar Metek: + - code_imp: Fixed up the return value of checkbox-input for tgui to behave consistently. + LT3: + - qol: Wearable HUDs can be toggled on and off + SmArtKar: + - bugfix: Fixed HUD implant message being reversed + SmArtKar, AnturK: + - admin: Fixes admin observe showing all antags as revs upon enabling Real Name + Display + high-speedspin-cycle: + - qol: The experimental deconstructive scanner can now scan multiple items. + necromanceranne: + - balance: The compact combat shotgun is now found in the warden's locker. + timothymtorres: + - bugfix: Fix pAIs interacting with machinery via alt/ctrl clicking. diff --git a/icons/obj/medical/chemical.dmi b/icons/obj/medical/chemical.dmi index 6cadd13c60e3a..c1fa2d403a4c6 100644 Binary files a/icons/obj/medical/chemical.dmi and b/icons/obj/medical/chemical.dmi differ diff --git a/icons/obj/medical/reagent_fillings.dmi b/icons/obj/medical/reagent_fillings.dmi index 0e4bd7f53ddf8..be55c79d2ad5e 100644 Binary files a/icons/obj/medical/reagent_fillings.dmi and b/icons/obj/medical/reagent_fillings.dmi differ diff --git a/sound/misc/license.txt b/sound/misc/license.txt index 2e596a4e128e3..945c48e279d1c 100644 --- a/sound/misc/license.txt +++ b/sound/misc/license.txt @@ -1,2 +1,4 @@ bloop.ogg by my man Tim Khan (https://freesound.org/people/tim.kahn/sounds/130377/) + +prompt.ogg by Flleeppyy (https://github.com/Flleeppyy), originally for https://github.com/Monkestation/Monkestation2.0/pull/2621 diff --git a/sound/misc/prompt.ogg b/sound/misc/prompt.ogg new file mode 100644 index 0000000000000..e32942e0d3cb7 Binary files /dev/null and b/sound/misc/prompt.ogg differ diff --git a/tgstation.dme b/tgstation.dme index 0fb0b25756864..0b536c6d6af41 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -802,7 +802,7 @@ #include "code\datums\drift_handler.dm" #include "code\datums\ductnet.dm" #include "code\datums\eigenstate.dm" -#include "code\datums\embed_data.dm" +#include "code\datums\embedding.dm" #include "code\datums\emotes.dm" #include "code\datums\ert.dm" #include "code\datums\hailer_phrase.dm" @@ -1123,7 +1123,6 @@ #include "code\datums\components\effect_remover.dm" #include "code\datums\components\egg_layer.dm" #include "code\datums\components\electrified_buckle.dm" -#include "code\datums\components\embedded.dm" #include "code\datums\components\energized.dm" #include "code\datums\components\engraved.dm" #include "code\datums\components\evolutionary_leap.dm" @@ -1496,7 +1495,6 @@ #include "code\datums\elements\easily_fragmented.dm" #include "code\datums\elements\effect_trail.dm" #include "code\datums\elements\elevation.dm" -#include "code\datums\elements\embed.dm" #include "code\datums\elements\empprotection.dm" #include "code\datums\elements\envenomable_casing.dm" #include "code\datums\elements\eyestab.dm" @@ -1618,6 +1616,7 @@ #include "code\datums\elements\food\foodlike_drink.dm" #include "code\datums\elements\food\fried_item.dm" #include "code\datums\elements\food\grilled_item.dm" +#include "code\datums\elements\food\love_food_buff.dm" #include "code\datums\elements\food\microwavable.dm" #include "code\datums\elements\food\processable.dm" #include "code\datums\elements\food\venue_price.dm" @@ -2406,6 +2405,7 @@ #include "code\game\objects\items\fireaxe.dm" #include "code\game\objects\items\flamethrower.dm" #include "code\game\objects\items\flatpacks.dm" +#include "code\game\objects\items\forensicsspoofer.dm" #include "code\game\objects\items\frog_statue.dm" #include "code\game\objects\items\gift.dm" #include "code\game\objects\items\gun_maintenance.dm" @@ -5824,6 +5824,7 @@ #include "code\modules\reagents\reagent_containers\cups\drinkingglass.dm" #include "code\modules\reagents\reagent_containers\cups\drinks.dm" #include "code\modules\reagents\reagent_containers\cups\glassbottle.dm" +#include "code\modules\reagents\reagent_containers\cups\organ_jar.dm" #include "code\modules\reagents\reagent_containers\cups\soda.dm" #include "code\modules\reagents\withdrawal\_addiction.dm" #include "code\modules\reagents\withdrawal\generic_addictions.dm" diff --git a/tgui/global.d.ts b/tgui/global.d.ts index d0bfdecf8909f..172bbc8aca767 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -41,6 +41,21 @@ type ByondType = { */ windowId: string; + /** + * True if javascript is running in BYOND. + */ + IS_BYOND: boolean; + + /** + * Version of Trident engine of Internet Explorer. Null if N/A. + */ + TRIDENT: number | null; + + /** + * Version of Blink engine of WebView2. Null if N/A. + */ + BLINK: number | null; + /** * If `true`, unhandled errors and common mistakes result in a blue screen * of death, which stops this window from handling incoming messages and @@ -85,14 +100,14 @@ type ByondType = { * * Returns a promise with a key-value object containing all properties. */ - winget(id: string | null): Promise; + winget(id: string | null): Promise>; /** * Retrieves all properties of the BYOND skin element. * * Returns a promise with a key-value object containing all properties. */ - winget(id: string | null, propName: '*'): Promise; + winget(id: string | null, propName: '*'): Promise>; /** * Retrieves an exactly one property of the BYOND skin element, @@ -108,7 +123,7 @@ type ByondType = { * * Returns a promise with a key-value object containing listed properties. */ - winget(id: string | null, propNames: string[]): Promise; + winget(id: string | null, propNames: string[]): Promise>; /** * Assigns properties to BYOND skin elements in bulk. @@ -175,4 +190,13 @@ interface Window { Byond: ByondType; __store__: Store; __augmentStack__: (store: Store) => StackAugmentor; + + // IE IndexedDB stuff. + msIndexedDB: IDBFactory; + msIDBTransaction: IDBTransaction; + + // 516 byondstorage API. + hubStorage: Storage; + domainStorage: Storage; + serverStorage: Storage; } diff --git a/tgui/package.json b/tgui/package.json index f89241c5a3f2e..24178dba6c707 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -19,7 +19,7 @@ "tgui:test-ci": "CI=true jest --color --collect-coverage", "tgui:tsc": "tsc", "tgui:prettier-fix": "prettier --write .", - "tgui:eslint-fix": "eslint --fix packages --ext .js,.cjs,.ts,.tsx" + "tgui:eslint-fix": "eslint --fix packages --ext .js,.cjs,.ts,.jsx,.tsx" }, "dependencies": { "@swc/core": "^1.9.1", diff --git a/tgui/packages/common/color.test.ts b/tgui/packages/common/color.test.ts deleted file mode 100644 index 93d90f05a23a7..0000000000000 --- a/tgui/packages/common/color.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Color } from './color'; - -describe('Color', () => { - it('should create a color with default values', () => { - const color = new Color(); - expect(color.r).toBe(0); - expect(color.g).toBe(0); - expect(color.b).toBe(0); - expect(color.a).toBe(1); - }); - - it('should create a color from hex', () => { - const color = Color.fromHex('#ff0000'); - expect(color.r).toBe(255); - expect(color.g).toBe(0); - expect(color.b).toBe(0); - }); - - it('should darken a color', () => { - const color = new Color(100, 100, 100).darken(50); - expect(color.r).toBe(50); - expect(color.g).toBe(50); - expect(color.b).toBe(50); - }); - - it('should lighten a color', () => { - const color = new Color(100, 100, 100).lighten(50); - expect(color.r).toBe(150); - expect(color.g).toBe(150); - expect(color.b).toBe(150); - }); - - it('should interpolate between two colors', () => { - const color1 = new Color(0, 0, 0); - const color2 = new Color(100, 100, 100); - const color = Color.lerp(color1, color2, 0.5); - expect(color.r).toBe(50); - expect(color.g).toBe(50); - expect(color.b).toBe(50); - }); - - it('should lookup a color in an array', () => { - const colors = [new Color(0, 0, 0), new Color(100, 100, 100)]; - const color = Color.lookup(0.5, colors); - expect(color.r).toBe(50); - expect(color.g).toBe(50); - expect(color.b).toBe(50); - }); -}); diff --git a/tgui/packages/common/color.ts b/tgui/packages/common/color.ts deleted file mode 100644 index 943b52a71fae9..0000000000000 --- a/tgui/packages/common/color.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -const EPSILON = 0.0001; - -export class Color { - r: number; - g: number; - b: number; - a: number; - - constructor(r = 0, g = 0, b = 0, a = 1) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } - - toString(): string { - // Alpha component needs to permit fractional values, so cannot use | - let alpha = this.a; - if (typeof alpha === 'string') { - alpha = parseFloat(this.a as any); - } - if (isNaN(alpha)) { - alpha = 1; - } - return `rgba(${this.r | 0}, ${this.g | 0}, ${this.b | 0}, ${alpha})`; - } - - /** Darkens a color by a given percent. Returns a color, which can have toString called to get it's rgba() css value. */ - darken(percent: number): Color { - percent /= 100; - return new Color( - this.r - this.r * percent, - this.g - this.g * percent, - this.b - this.b * percent, - this.a, - ); - } - - /** Brightens a color by a given percent. Returns a color, which can have toString called to get it's rgba() css value. */ - lighten(percent: number): Color { - // No point in rewriting code we already have. - return this.darken(-percent); - } - - /** - * Creates a color from the CSS hex color notation. - */ - static fromHex(hex: string): Color { - return new Color( - parseInt(hex.slice(1, 3), 16), - parseInt(hex.slice(3, 5), 16), - parseInt(hex.slice(5, 7), 16), - ); - } - - /** - * Linear interpolation of two colors. - */ - static lerp(c1: Color, c2: Color, n: number): Color { - return new Color( - (c2.r - c1.r) * n + c1.r, - (c2.g - c1.g) * n + c1.g, - (c2.b - c1.b) * n + c1.b, - (c2.a - c1.a) * n + c1.a, - ); - } - - /** - * Loops up the color in the provided list of colors - * with linear interpolation. - */ - static lookup(value: number, colors: Color[]): Color { - const len = colors.length; - if (len < 2) { - throw new Error('Needs at least two colors!'); - } - const scaled = value * (len - 1); - if (value < EPSILON) { - return colors[0]; - } - if (value >= 1 - EPSILON) { - return colors[len - 1]; - } - const ratio = scaled % 1; - const index = scaled | 0; - return this.lerp(colors[index], colors[index + 1], ratio); - } -} diff --git a/tgui/packages/common/events.test.ts b/tgui/packages/common/events.test.ts deleted file mode 100644 index b83ba467fd5fd..0000000000000 --- a/tgui/packages/common/events.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { EventEmitter } from './events'; - -describe('EventEmitter', () => { - it('should add and trigger an event listener', () => { - const emitter = new EventEmitter(); - const mockListener = jest.fn(); - emitter.on('test', mockListener); - emitter.emit('test', 'payload'); - expect(mockListener).toHaveBeenCalledWith('payload'); - }); - - it('should remove an event listener', () => { - const emitter = new EventEmitter(); - const mockListener = jest.fn(); - emitter.on('test', mockListener); - emitter.off('test', mockListener); - emitter.emit('test', 'payload'); - expect(mockListener).not.toHaveBeenCalled(); - }); - - it('should not fail when emitting an event with no listeners', () => { - const emitter = new EventEmitter(); - expect(() => emitter.emit('test', 'payload')).not.toThrow(); - }); - - it('should clear all event listeners', () => { - const emitter = new EventEmitter(); - const mockListener = jest.fn(); - emitter.on('test', mockListener); - emitter.clear(); - emitter.emit('test', 'payload'); - expect(mockListener).not.toHaveBeenCalled(); - }); -}); diff --git a/tgui/packages/common/events.ts b/tgui/packages/common/events.ts deleted file mode 100644 index 49223b29fb394..0000000000000 --- a/tgui/packages/common/events.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -type Fn = (...args: any[]) => void; - -export class EventEmitter { - private listeners: Record; - - constructor() { - this.listeners = {}; - } - - on(name: string, listener: Fn): void { - this.listeners[name] = this.listeners[name] || []; - this.listeners[name].push(listener); - } - - off(name: string, listener: Fn): void { - const listeners = this.listeners[name]; - if (!listeners) { - throw new Error(`There is no listeners for "${name}"`); - } - this.listeners[name] = listeners.filter((existingListener) => { - return existingListener !== listener; - }); - } - - emit(name: string, ...params: any[]): void { - const listeners = this.listeners[name]; - if (!listeners) { - return; - } - for (let i = 0, len = listeners.length; i < len; i += 1) { - const listener = listeners[i]; - listener(...params); - } - } - - clear(): void { - this.listeners = {}; - } -} diff --git a/tgui/packages/common/exhaustive.ts b/tgui/packages/common/exhaustive.ts deleted file mode 100644 index bc41757515b08..0000000000000 --- a/tgui/packages/common/exhaustive.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Throws an error such that a non-exhaustive check will error at compile time - * when using TypeScript, rather than at runtime. - * - * For example: - * enum Color { Red, Green, Blue } - * switch (color) { - * case Color.Red: - * return "red"; - * case Color.Green: - * return "green"; - * default: - * // This will error at compile time that we forgot blue. - * exhaustiveCheck(color); - * } - */ -export const exhaustiveCheck = (input: never) => { - throw new Error(`Unhandled case: ${input}`); -}; diff --git a/tgui/packages/common/fp.test.ts b/tgui/packages/common/fp.test.ts deleted file mode 100644 index 308a98d0f1227..0000000000000 --- a/tgui/packages/common/fp.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { flow } from './fp'; - -describe('flow', () => { - it('composes multiple functions into one', () => { - const add2 = (x) => x + 2; - const multiplyBy3 = (x) => x * 3; - const subtract5 = (x) => x - 5; - - const composedFunction = flow(add2, multiplyBy3, subtract5); - - expect(composedFunction(4)).toBe(13); // ((4 + 2) * 3) - 5 = 13 - }); - - it('handles arrays of functions', () => { - const add2 = (x) => x + 2; - const multiplyBy3 = (x) => x * 3; - const subtract5 = (x) => x - 5; - - const composedFunction = flow([add2, multiplyBy3], subtract5); - - expect(composedFunction(4)).toBe(13); // ((4 + 2) * 3) - 5 = 13 - }); -}); diff --git a/tgui/packages/common/fp.ts b/tgui/packages/common/fp.ts deleted file mode 100644 index 62883a693a24a..0000000000000 --- a/tgui/packages/common/fp.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -type Func = (...args: any[]) => any; - -/** - * Creates a function that returns the result of invoking the given - * functions, where each successive invocation is supplied the return - * value of the previous. - * - * @example - * ```tsx - * const add2 = (x) => x + 2; - * const multiplyBy3 = (x) => x * 3; - * const subtract5 = (x) => x - 5; - * - * const composedFunction = flow(add2, multiplyBy3, subtract5); // ((4 + 2) * 3) - 5 = 13 - * const composedFunction2 = flow([add2, multiplyBy3], subtract5); // ((4 + 2) * 3) - 5 = 13 - * - */ -export const flow = - (...funcs: Array) => - (input: any, ...rest: any[]): any => { - let output = input; - - for (let func of funcs) { - // Recurse into the array of functions - if (Array.isArray(func)) { - output = flow(...func)(output, ...rest); - } else if (func) { - output = func(output, ...rest); - } - } - return output; - }; diff --git a/tgui/packages/common/keycodes.ts b/tgui/packages/common/keycodes.ts deleted file mode 100644 index 8f18b154b56c9..0000000000000 --- a/tgui/packages/common/keycodes.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * All possible browser keycodes, in one file. - * - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -export const KEY_BACKSPACE = 8; -export const KEY_TAB = 9; -export const KEY_ENTER = 13; -export const KEY_SHIFT = 16; -export const KEY_CTRL = 17; -export const KEY_ALT = 18; -export const KEY_PAUSE = 19; -export const KEY_CAPSLOCK = 20; -export const KEY_ESCAPE = 27; -export const KEY_SPACE = 32; -export const KEY_PAGEUP = 33; -export const KEY_PAGEDOWN = 34; -export const KEY_END = 35; -export const KEY_HOME = 36; -export const KEY_LEFT = 37; -export const KEY_UP = 38; -export const KEY_RIGHT = 39; -export const KEY_DOWN = 40; -export const KEY_INSERT = 45; -export const KEY_DELETE = 46; -export const KEY_0 = 48; -export const KEY_1 = 49; -export const KEY_2 = 50; -export const KEY_3 = 51; -export const KEY_4 = 52; -export const KEY_5 = 53; -export const KEY_6 = 54; -export const KEY_7 = 55; -export const KEY_8 = 56; -export const KEY_9 = 57; -export const KEY_A = 65; -export const KEY_B = 66; -export const KEY_C = 67; -export const KEY_D = 68; -export const KEY_E = 69; -export const KEY_F = 70; -export const KEY_G = 71; -export const KEY_H = 72; -export const KEY_I = 73; -export const KEY_J = 74; -export const KEY_K = 75; -export const KEY_L = 76; -export const KEY_M = 77; -export const KEY_N = 78; -export const KEY_O = 79; -export const KEY_P = 80; -export const KEY_Q = 81; -export const KEY_R = 82; -export const KEY_S = 83; -export const KEY_T = 84; -export const KEY_U = 85; -export const KEY_V = 86; -export const KEY_W = 87; -export const KEY_X = 88; -export const KEY_Y = 89; -export const KEY_Z = 90; -export const KEY_F1 = 112; -export const KEY_F2 = 113; -export const KEY_F3 = 114; -export const KEY_F4 = 115; -export const KEY_F5 = 116; -export const KEY_F6 = 117; -export const KEY_F7 = 118; -export const KEY_F8 = 119; -export const KEY_F9 = 120; -export const KEY_F10 = 121; -export const KEY_F11 = 122; -export const KEY_F12 = 123; -export const KEY_SEMICOLON = 186; -export const KEY_EQUAL = 187; -export const KEY_COMMA = 188; -export const KEY_MINUS = 189; -export const KEY_PERIOD = 190; -export const KEY_SLASH = 191; -export const KEY_LEFT_BRACKET = 219; -export const KEY_BACKSLASH = 220; -export const KEY_RIGHT_BRACKET = 221; -export const KEY_QUOTE = 222; diff --git a/tgui/packages/common/keys.ts b/tgui/packages/common/keys.ts deleted file mode 100644 index 3e913151707ff..0000000000000 --- a/tgui/packages/common/keys.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * ### Key codes. - * event.keyCode is deprecated, use this reference instead. - * - * Handles modifier keys (Shift, Alt, Control) and arrow keys. - * - * For alphabetical keys, use the actual character (e.g. 'a') instead of the key code. - * Don't access Esc or Escape directly, use isEscape() instead - * - * Something isn't here that you want? Just add it: - * @url https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values - * @usage - * ```ts - * import { KEY } from 'tgui/common/keys'; - * - * if (event.key === KEY.Enter) { - * // do something - * } - * ``` - * - * - */ -export enum KEY { - Alt = 'Alt', - Backspace = 'Backspace', - Control = 'Control', - Delete = 'Delete', - Down = 'ArrowDown', - End = 'End', - Enter = 'Enter', - Esc = 'Esc', - Escape = 'Escape', - Home = 'Home', - Insert = 'Insert', - Left = 'ArrowLeft', - PageDown = 'PageDown', - PageUp = 'PageUp', - Right = 'ArrowRight', - Shift = 'Shift', - Space = ' ', - Tab = 'Tab', - Up = 'ArrowUp', -} - -/** - * ### isEscape - * - * Checks if the user has hit the 'ESC' key on their keyboard. - * There's a weirdness in BYOND where this could be either the string - * 'Escape' or 'Esc' depending on the browser. This function handles - * both cases. - * - * @param key - the key to check, typically from event.key - * @returns true if key is Escape or Esc, false otherwise - */ -export function isEscape(key: string): boolean { - return key === KEY.Esc || key === KEY.Escape; -} diff --git a/tgui/packages/common/math.ts b/tgui/packages/common/math.ts deleted file mode 100644 index 9dc1d65569362..0000000000000 --- a/tgui/packages/common/math.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -/** - * Limits a number to the range between 'min' and 'max'. - */ -export const clamp = (value, min, max) => { - return value < min ? min : value > max ? max : value; -}; - -/** - * Limits a number between 0 and 1. - */ -export const clamp01 = (value) => { - return value < 0 ? 0 : value > 1 ? 1 : value; -}; - -/** - * Scales a number to fit into the range between min and max. - */ -export const scale = (value, min, max) => { - return (value - min) / (max - min); -}; - -/** - * Robust number rounding. - * - * Adapted from Locutus, see: http://locutus.io/php/math/round/ - * - * @param {number} value - * @param {number} precision - * @return {number} - */ -export const round = (value, precision) => { - if (!value || isNaN(value)) { - return value; - } - // helper variables - let m, f, isHalf, sgn; - // making sure precision is integer - precision |= 0; - m = Math.pow(10, precision); - value *= m; - // sign of the number - sgn = +(value > 0) | -(value < 0); - // isHalf = value % 1 === 0.5 * sgn; - isHalf = Math.abs(value % 1) >= 0.4999999999854481; - f = Math.floor(value); - if (isHalf) { - // rounds .5 away from zero - value = f + (sgn > 0); - } - return (isHalf ? value : Math.round(value)) / m; -}; - -/** - * Returns a string representing a number in fixed point notation. - */ -export const toFixed = (value, fractionDigits = 0) => { - return Number(value).toFixed(Math.max(fractionDigits, 0)); -}; - -/** - * Checks whether a value is within the provided range. - * - * Range is an array of two numbers, for example: [0, 15]. - */ -export const inRange = (value, range) => { - return range && value >= range[0] && value <= range[1]; -}; - -/** - * Walks over the object with ranges, comparing value against every range, - * and returns the key of the first matching range. - * - * Range is an array of two numbers, for example: [0, 15]. - */ -export const keyOfMatchingRange = (value, ranges) => { - for (let rangeName of Object.keys(ranges)) { - const range = ranges[rangeName]; - if (inRange(value, range)) { - return rangeName; - } - } -}; - -/** - * Get number of digits following the decimal point in a number - */ -export const numberOfDecimalDigits = (value) => { - if (Math.floor(value) !== value) { - return value.toString().split('.')[1].length || 0; - } - return 0; -}; diff --git a/tgui/packages/common/random.ts b/tgui/packages/common/random.ts deleted file mode 100644 index fbf9030b1bafb..0000000000000 --- a/tgui/packages/common/random.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { clamp } from './math'; - -/** - * Returns random number between lowerBound exclusive and upperBound inclusive - */ -export const randomNumber = (lowerBound: number, upperBound: number) => { - return Math.random() * (upperBound - lowerBound) + lowerBound; -}; - -/** - * Returns random integer between lowerBound exclusive and upperBound inclusive - */ -export const randomInteger = (lowerBound: number, upperBound: number) => { - lowerBound = Math.ceil(lowerBound); - upperBound = Math.floor(upperBound); - return Math.floor(Math.random() * (upperBound - lowerBound) + lowerBound); -}; - -/** - * Returns random array element - */ -export const randomPick = (array: T[]) => { - return array[Math.floor(Math.random() * array.length)]; -}; - -/** - * Return 1 with probability P percent; otherwise 0 - */ -export const randomProb = (probability: number) => { - const normalized = clamp(probability, 0, 100) / 100; - return Math.random() <= normalized; -}; diff --git a/tgui/packages/common/react.spec.ts b/tgui/packages/common/react.spec.ts deleted file mode 100644 index 44102fdc97109..0000000000000 --- a/tgui/packages/common/react.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @file - * @copyright 2021 Aleksej Komarov - * @license MIT - */ - -import { classes } from './react'; - -describe('classes', () => { - test('empty', () => { - expect(classes([])).toBe(''); - }); - - test('result contains inputs', () => { - const output = classes(['foo', 'bar', false, true, 0, 1, 'baz']); - expect(output).toContain('foo'); - expect(output).toContain('bar'); - expect(output).toContain('baz'); - }); -}); diff --git a/tgui/packages/common/react.ts b/tgui/packages/common/react.ts deleted file mode 100644 index 5260ff6ae128b..0000000000000 --- a/tgui/packages/common/react.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -/** - * Helper for conditionally adding/removing classes in React - */ -export const classes = (classNames: (string | BooleanLike)[]) => { - let className = ''; - for (let i = 0; i < classNames.length; i++) { - const part = classNames[i]; - if (typeof part === 'string') { - className += part + ' '; - } - } - return className; -}; - -/** - * Normalizes children prop, so that it is always an array of VDom - * elements. - */ -export const normalizeChildren = (children: T | T[]) => { - if (Array.isArray(children)) { - return children.flat().filter((value) => value) as T[]; - } - if (typeof children === 'object') { - return [children]; - } - return []; -}; - -/** - * Shallowly checks if two objects are different. - * Credit: https://github.com/developit/preact-compat - */ -export const shallowDiffers = (a: object, b: object) => { - let i; - for (i in a) { - if (!(i in b)) { - return true; - } - } - for (i in b) { - if (a[i] !== b[i]) { - return true; - } - } - return false; -}; - -/** - * A common case in tgui, when you pass a value conditionally, these are - * the types that can fall through the condition. - */ -export type BooleanLike = number | boolean | null | undefined; - -/** - * A helper to determine whether the object is renderable by React. - */ -export const canRender = (value: unknown) => { - // prettier-ignore - return value !== undefined - && value !== null - && typeof value !== 'boolean'; -}; diff --git a/tgui/packages/common/storage.js b/tgui/packages/common/storage.ts similarity index 53% rename from tgui/packages/common/storage.js rename to tgui/packages/common/storage.ts index acf842f64083b..b2564acf36dc3 100644 --- a/tgui/packages/common/storage.js +++ b/tgui/packages/common/storage.ts @@ -7,9 +7,14 @@ */ export const IMPL_MEMORY = 0; -export const IMPL_LOCAL_STORAGE = 1; +export const IMPL_HUB_STORAGE = 1; export const IMPL_INDEXED_DB = 2; +type StorageImplementation = + | typeof IMPL_MEMORY + | typeof IMPL_HUB_STORAGE + | typeof IMPL_INDEXED_DB; + const INDEXED_DB_VERSION = 1; const INDEXED_DB_NAME = 'tgui'; const INDEXED_DB_STORE_NAME = 'storage-v1'; @@ -17,7 +22,15 @@ const INDEXED_DB_STORE_NAME = 'storage-v1'; const READ_ONLY = 'readonly'; const READ_WRITE = 'readwrite'; -const testGeneric = (testFn) => () => { +type StorageBackend = { + impl: StorageImplementation; + get(key: string): Promise; + set(key: string, value: any): Promise; + remove(key: string): Promise; + clear(): Promise; +}; + +const testGeneric = (testFn: () => boolean) => (): boolean => { try { return Boolean(testFn()); } catch { @@ -25,72 +38,77 @@ const testGeneric = (testFn) => () => { } }; -// Localstorage can sometimes throw an error, even if DOM storage is not -// disabled in IE11 settings. -// See: https://superuser.com/questions/1080011 -// prettier-ignore -const testLocalStorage = testGeneric(() => ( - window.localStorage && window.localStorage.getItem -)); +const testHubStorage = testGeneric( + () => window.hubStorage && !!window.hubStorage.getItem, +); +// TODO: Remove with 516 // prettier-ignore const testIndexedDb = testGeneric(() => ( (window.indexedDB || window.msIndexedDB) - && (window.IDBTransaction || window.msIDBTransaction) + && !!(window.IDBTransaction || window.msIDBTransaction) )); -class MemoryBackend { +class MemoryBackend implements StorageBackend { + private store: Record; + public impl: StorageImplementation; + constructor() { this.impl = IMPL_MEMORY; this.store = {}; } - get(key) { + async get(key: string): Promise { return this.store[key]; } - set(key, value) { + async set(key: string, value: any): Promise { this.store[key] = value; } - remove(key) { + async remove(key: string): Promise { this.store[key] = undefined; } - clear() { + async clear(): Promise { this.store = {}; } } -class LocalStorageBackend { +class HubStorageBackend implements StorageBackend { + public impl: StorageImplementation; + constructor() { - this.impl = IMPL_LOCAL_STORAGE; + this.impl = IMPL_HUB_STORAGE; } - get(key) { - const value = localStorage.getItem(key); + async get(key: string): Promise { + const value = await window.hubStorage.getItem(key); if (typeof value === 'string') { return JSON.parse(value); } + return undefined; } - set(key, value) { - localStorage.setItem(key, JSON.stringify(value)); + async set(key: string, value: any): Promise { + window.hubStorage.setItem(key, JSON.stringify(value)); } - remove(key) { - localStorage.removeItem(key); + async remove(key: string): Promise { + window.hubStorage.removeItem(key); } - clear() { - localStorage.clear(); + async clear(): Promise { + window.hubStorage.clear(); } } -class IndexedDbBackend { +class IndexedDbBackend implements StorageBackend { + public impl: StorageImplementation; + public dbPromise: Promise; + constructor() { this.impl = IMPL_INDEXED_DB; - /** @type {Promise} */ this.dbPromise = new Promise((resolve, reject) => { const indexedDB = window.indexedDB || window.msIndexedDB; const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION); @@ -98,7 +116,12 @@ class IndexedDbBackend { try { req.result.createObjectStore(INDEXED_DB_STORE_NAME); } catch (err) { - reject(new Error('Failed to upgrade IDB: ' + req.error)); + reject( + new Error( + 'Failed to upgrade IDB: ' + + (err instanceof Error ? err.message : String(err)), + ), + ); } }; req.onsuccess = () => resolve(req.result); @@ -108,14 +131,14 @@ class IndexedDbBackend { }); } - getStore(mode) { - // prettier-ignore - return this.dbPromise.then((db) => db + private async getStore(mode: IDBTransactionMode): Promise { + const db = await this.dbPromise; + return db .transaction(INDEXED_DB_STORE_NAME, mode) - .objectStore(INDEXED_DB_STORE_NAME)); + .objectStore(INDEXED_DB_STORE_NAME); } - async get(key) { + async get(key: string): Promise { const store = await this.getStore(READ_ONLY); return new Promise((resolve, reject) => { const req = store.get(key); @@ -124,26 +147,19 @@ class IndexedDbBackend { }); } - async set(key, value) { - // The reason we don't _save_ null is because IE 10 does - // not support saving the `null` type in IndexedDB. How - // ironic, given the bug below! - // See: https://github.com/mozilla/localForage/issues/161 - if (value === null) { - value = undefined; - } + async set(key: string, value: any): Promise { // NOTE: We deliberately make this operation transactionless const store = await this.getStore(READ_WRITE); store.put(value, key); } - async remove(key) { + async remove(key: string): Promise { // NOTE: We deliberately make this operation transactionless const store = await this.getStore(READ_WRITE); store.delete(key); } - async clear() { + async clear(): Promise { // NOTE: We deliberately make this operation transactionless const store = await this.getStore(READ_WRITE); store.clear(); @@ -154,9 +170,16 @@ class IndexedDbBackend { * Web Storage Proxy object, which selects the best backend available * depending on the environment. */ -class StorageProxy { +class StorageProxy implements StorageBackend { + private backendPromise: Promise; + public impl: StorageImplementation = IMPL_MEMORY; + constructor() { this.backendPromise = (async () => { + if (!Byond.TRIDENT && testHubStorage()) { + return new HubStorageBackend(); + } + // TODO: Remove with 516 if (testIndexedDb()) { try { const backend = new IndexedDbBackend(); @@ -164,29 +187,29 @@ class StorageProxy { return backend; } catch {} } - if (testLocalStorage()) { - return new LocalStorageBackend(); - } + console.warn( + 'No supported storage backend found. Using in-memory storage.', + ); return new MemoryBackend(); })(); } - async get(key) { + async get(key: string): Promise { const backend = await this.backendPromise; return backend.get(key); } - async set(key, value) { + async set(key: string, value: any): Promise { const backend = await this.backendPromise; return backend.set(key, value); } - async remove(key) { + async remove(key: string): Promise { const backend = await this.backendPromise; return backend.remove(key); } - async clear() { + async clear(): Promise { const backend = await this.backendPromise; return backend.clear(); } diff --git a/tgui/packages/common/string.test.ts b/tgui/packages/common/string.test.ts deleted file mode 100644 index 06b24da801361..0000000000000 --- a/tgui/packages/common/string.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createSearch, decodeHtmlEntities, toTitleCase } from './string'; - -describe('createSearch', () => { - it('matches search terms correctly', () => { - const search = createSearch('test', (obj: { value: string }) => obj.value); - - const obj1 = { value: 'This is a test string.' }; - const obj2 = { value: 'This is a different string.' }; - const obj3 = { value: 'This is a test string.' }; - - const objects = [obj1, obj2, obj3]; - - expect(objects.filter(search)).toEqual([obj1, obj3]); - }); -}); - -describe('toTitleCase', () => { - it('converts strings to title case correctly', () => { - expect(toTitleCase('hello world')).toBe('Hello World'); - expect(toTitleCase('HELLO WORLD')).toBe('Hello World'); - expect(toTitleCase('HeLLo wORLd')).toBe('Hello World'); - expect(toTitleCase('a tale of two cities')).toBe('A Tale of Two Cities'); - expect(toTitleCase('war and peace')).toBe('War and Peace'); - }); -}); - -describe('decodeHtmlEntities', () => { - it('decodes HTML entities and removes unnecessary HTML tags correctly', () => { - expect(decodeHtmlEntities('
')).toBe('\n'); - expect(decodeHtmlEntities('

Hello World

')).toBe('Hello World'); - expect(decodeHtmlEntities('&')).toBe('&'); - expect(decodeHtmlEntities('&')).toBe('&'); - expect(decodeHtmlEntities('&')).toBe('&'); - }); -}); diff --git a/tgui/packages/common/string.ts b/tgui/packages/common/string.ts deleted file mode 100644 index e23295e627110..0000000000000 --- a/tgui/packages/common/string.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -/** - * Creates a search terms matcher. Returns true if given string matches the search text. - * - * @example - * ```tsx - * type Thing = { id: string; name: string }; - * - * const objects = [ - * { id: '123', name: 'Test' }, - * { id: '456', name: 'Test' }, - * ]; - * - * const search = createSearch('123', (obj: Thing) => obj.id); - * - * objects.filter(search); // returns [{ id: '123', name: 'Test' }] - * ``` - */ -export function createSearch( - searchText: string, - stringifier = (obj: TObj) => JSON.stringify(obj), -): (obj: TObj) => boolean { - const preparedSearchText = searchText.toLowerCase().trim(); - - return (obj) => { - if (!preparedSearchText) { - return true; - } - const str = stringifier(obj); - if (!str) { - return false; - } - return str.toLowerCase().includes(preparedSearchText); - }; -} - -/** - * Capitalizes a word and lowercases the rest. - * - * @example - * ```tsx - * capitalize('heLLo') // Hello - * ``` - */ -export function capitalize(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); -} - -/** - * Similar to capitalize, this takes a string and replaces all first letters - * of any words. - * - * @example - * ```tsx - * capitalizeAll('heLLo woRLd') // 'HeLLo WoRLd' - * ``` - */ -export function capitalizeAll(str: string): string { - return str.replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase()); -} - -/** - * Capitalizes only the first letter of the str, leaving others untouched. - * - * @example - * ```tsx - * capitalizeFirst('heLLo woRLd') // 'HeLLo woRLd' - * ``` - */ -export function capitalizeFirst(str: string): string { - return str.replace(/^[\wа-яА-ЯёЁ]/, (letter) => letter.toUpperCase()); // BANDASTATION EDIT - Original: return str.replace(/^\w/, (letter) => letter.toUpperCase()); -} - -const WORDS_UPPER = ['Id', 'Tv'] as const; - -const WORDS_LOWER = [ - 'A', - 'An', - 'And', - 'As', - 'At', - 'But', - 'By', - 'For', - 'For', - 'From', - 'In', - 'Into', - 'Near', - 'Nor', - 'Of', - 'On', - 'Onto', - 'Or', - 'The', - 'To', - 'With', -] as const; - -/** - * Converts a string to title case. - * - * @example - * ```tsx - * toTitleCase('a tale of two cities') // 'A Tale of Two Cities' - * ``` - */ -export function toTitleCase(str: string): string { - if (!str) return str; - - let currentStr = str.replace(/([^\W_]+[^\s-]*) */g, (str) => { - return capitalize(str); - }); - - for (let word of WORDS_LOWER) { - const regex = new RegExp('\\s' + word + '\\s', 'g'); - currentStr = currentStr.replace(regex, (str) => str.toLowerCase()); - } - - for (let word of WORDS_UPPER) { - const regex = new RegExp('\\b' + word + '\\b', 'g'); - currentStr = currentStr.replace(regex, (str) => str.toLowerCase()); - } - - return currentStr; -} - -const TRANSLATE_REGEX = /&(nbsp|amp|quot|lt|gt|apos);/g; -const TRANSLATIONS = { - amp: '&', - apos: "'", - gt: '>', - lt: '<', - nbsp: ' ', - quot: '"', -} as const; - -/** - * Decodes HTML entities and removes unnecessary HTML tags. - * - * @example - * ```tsx - * decodeHtmlEntities('&') // returns '&' - * decodeHtmlEntities('<') // returns '<' - * ``` - */ -export function decodeHtmlEntities(str: string): string { - if (!str) return str; - - return ( - str - // Newline tags - .replace(/
/gi, '\n') - .replace(/<\/?[a-z0-9-_]+[^>]*>/gi, '') - // Basic entities - .replace(TRANSLATE_REGEX, (match, entity) => TRANSLATIONS[entity]) - // Decimal entities - .replace(/&#?([0-9]+);/gi, (match, numStr) => { - const num = parseInt(numStr, 10); - return String.fromCharCode(num); - }) - // Hex entities - .replace(/&#x?([0-9a-f]+);/gi, (match, numStr) => { - const num = parseInt(numStr, 16); - return String.fromCharCode(num); - }) - ); -} diff --git a/tgui/packages/common/timer.ts b/tgui/packages/common/timer.ts deleted file mode 100644 index 1fc3e11fd30e5..0000000000000 --- a/tgui/packages/common/timer.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -/** - * Returns a function, that, as long as it continues to be invoked, will - * not be triggered. The function will be called after it stops being - * called for N milliseconds. If `immediate` is passed, trigger the - * function on the leading edge, instead of the trailing. - */ -export const debounce = any>( - fn: F, - time: number, - immediate = false, -): ((...args: Parameters) => void) => { - let timeout: ReturnType | null; - return (...args: Parameters) => { - const later = () => { - timeout = null; - if (!immediate) { - fn(...args); - } - }; - const callNow = immediate && !timeout; - clearTimeout(timeout!); - timeout = setTimeout(later, time); - if (callNow) { - fn(...args); - } - }; -}; - -/** - * Returns a function, that, when invoked, will only be triggered at most once - * during a given window of time. - */ -export const throttle = any>( - fn: F, - time: number, -): ((...args: Parameters) => void) => { - let previouslyRun: number | null, - queuedToRun: ReturnType | null; - return function invokeFn(...args: Parameters) { - const now = Date.now(); - if (queuedToRun) { - clearTimeout(queuedToRun); - } - if (!previouslyRun || now - previouslyRun >= time) { - fn.apply(null, args); - previouslyRun = now; - } else { - queuedToRun = setTimeout( - () => invokeFn(...args), - time - (now - (previouslyRun ?? 0)), - ); - } - }; -}; - -/** - * Suspends an asynchronous function for N milliseconds. - * - * @param {number} time - */ -export const sleep = (time: number): Promise => - new Promise((resolve) => setTimeout(resolve, time)); diff --git a/tgui/packages/common/type-utils.ts b/tgui/packages/common/type-utils.ts deleted file mode 100644 index a73c0c1d59567..0000000000000 --- a/tgui/packages/common/type-utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Helps visualize highly complex ui data on the fly. - * @example - * ```tsx - * const { data } = useBackend(); - * logger.log(getShallowTypes(data)); - * ``` - */ -export function getShallowTypes( - data: Record, -): Record { - const output = {}; - - for (const key in data) { - if (Array.isArray(data[key])) { - const arr: any[] = data[key]; - - // Return the first array item if it exists - if (data[key].length > 0) { - output[key] = arr[0]; - continue; - } - - output[key] = 'emptyarray'; - } else if (typeof data[key] === 'object' && data[key] !== null) { - // Please inspect it further and make a new type for it - output[key] = 'object (inspect) || Record'; - } else if (typeof data[key] === 'number') { - const num = Number(data[key]); - - // 0 and 1 could be booleans from byond - if (num === 1 || num === 0) { - output[key] = `${num}, BooleanLike?`; - continue; - } - output[key] = data[key]; - } - } - - return output; -} diff --git a/tgui/packages/common/uuid.test.ts b/tgui/packages/common/uuid.test.ts deleted file mode 100644 index e3af57c472675..0000000000000 --- a/tgui/packages/common/uuid.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createUuid } from './uuid'; - -describe('createUuid', () => { - it('generates a UUID v4 string', () => { - const uuid = createUuid(); - expect(uuid).toHaveLength(36); - expect(uuid).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, - ); - }); -}); diff --git a/tgui/packages/common/uuid.ts b/tgui/packages/common/uuid.ts deleted file mode 100644 index 250809ab6a7d2..0000000000000 --- a/tgui/packages/common/uuid.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -/** - * Creates a UUID v4 string - * - * @example - * ```tsx - * createUuid(); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479' - * ``` - */ -export function createUuid(): string { - let d = new Date().getTime(); - - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (d + Math.random() * 16) % 16 | 0; - d = Math.floor(d / 16); - - return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); - }); -} diff --git a/tgui/packages/tgui-bench/entrypoint.tsx b/tgui/packages/tgui-bench/entrypoint.tsx index 48dcd3dcce113..399af3fd75a28 100644 --- a/tgui/packages/tgui-bench/entrypoint.tsx +++ b/tgui/packages/tgui-bench/entrypoint.tsx @@ -6,7 +6,7 @@ import 'tgui/styles/main.scss'; -import { setupGlobalEvents } from 'tgui/events'; +import { setupGlobalEvents } from 'tgui-core/events'; import Benchmark from './lib/benchmark'; diff --git a/tgui/packages/tgui-bench/package.json b/tgui/packages/tgui-bench/package.json index 532363ddda68f..51c71f76b2584 100644 --- a/tgui/packages/tgui-bench/package.json +++ b/tgui/packages/tgui-bench/package.json @@ -4,11 +4,14 @@ "version": "5.0.3", "dependencies": { "@fastify/static": "^8.0.2", + "@types/react-dom": "^18.3.1", "common": "workspace:*", "fastify": "^5.1.0", "lodash": "^4.17.21", "platform": "^1.3.6", "react": "^18.3.1", - "tgui": "workspace:*" + "react-dom": "^18.3.1", + "tgui": "workspace:*", + "tgui-core": "^1.5.4" } } diff --git a/tgui/packages/tgui-bench/tests/Button.test.tsx b/tgui/packages/tgui-bench/tests/Button.test.tsx deleted file mode 100644 index 0549e69b623ae..0000000000000 --- a/tgui/packages/tgui-bench/tests/Button.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Button } from 'tgui/components'; -import { createRenderer } from 'tgui/renderer'; - -const render = createRenderer(); - -export const SingleButton = () => { - const node = ; - render(node); -}; - -export const SingleButtonWithCallback = () => { - const node = ; - render(node); -}; - -export const ListOfButtons = () => { - const nodes: JSX.Element[] = []; - for (let i = 0; i < 100; i++) { - const node = ; - nodes.push(node); - } - render(
{nodes}
); -}; - -export const ListOfButtonsWithCallback = () => { - const nodes: JSX.Element[] = []; - for (let i = 0; i < 100; i++) { - const node = ( - - ); - nodes.push(node); - } - render(
{nodes}
); -}; - -export const ListOfButtonsWithIcons = () => { - const nodes: JSX.Element[] = []; - for (let i = 0; i < 100; i++) { - const node = ( - - ); - nodes.push(node); - } - render(
{nodes}
); -}; - -export const ListOfButtonsWithTooltips = () => { - const nodes: JSX.Element[] = []; - for (let i = 0; i < 100; i++) { - const node = ( - - ); - nodes.push(node); - } - render(
{nodes}
); -}; diff --git a/tgui/packages/tgui-bench/tests/Flex.test.tsx b/tgui/packages/tgui-bench/tests/Flex.test.tsx deleted file mode 100644 index 66c039a190833..0000000000000 --- a/tgui/packages/tgui-bench/tests/Flex.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Flex } from 'tgui/components'; -import { createRenderer } from 'tgui/renderer'; - -const render = createRenderer(); - -export const Default = () => { - const node = ( - - Text {Math.random()} - - Text {Math.random()} - - - ); - render(node); -}; diff --git a/tgui/packages/tgui-bench/tests/Stack.test.tsx b/tgui/packages/tgui-bench/tests/Stack.test.tsx deleted file mode 100644 index ce7f5599e721a..0000000000000 --- a/tgui/packages/tgui-bench/tests/Stack.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Stack } from 'tgui/components'; -import { createRenderer } from 'tgui/renderer'; - -const render = createRenderer(); - -export const Default = () => { - const node = ( - - Text {Math.random()} - - Text {Math.random()} - - - ); - render(node); -}; diff --git a/tgui/packages/tgui-bench/tests/Tooltip.test.tsx b/tgui/packages/tgui-bench/tests/Tooltip.test.tsx deleted file mode 100644 index 9dae16f5c0303..0000000000000 --- a/tgui/packages/tgui-bench/tests/Tooltip.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Box, Tooltip } from 'tgui/components'; -import { createRenderer } from 'tgui/renderer'; - -const render = createRenderer(); - -export const ListOfTooltips = () => { - const nodes: JSX.Element[] = []; - - for (let i = 0; i < 100; i++) { - nodes.push( - - - Tooltip #{i} - - , - ); - } - - render(
{nodes}
); -}; diff --git a/tgui/packages/tgui-panel/Notifications.tsx b/tgui/packages/tgui-panel/Notifications.tsx index 2b92995287fad..e46e6f98fa0c5 100644 --- a/tgui/packages/tgui-panel/Notifications.tsx +++ b/tgui/packages/tgui-panel/Notifications.tsx @@ -4,7 +4,7 @@ * @license MIT */ -import { Flex } from 'tgui/components'; +import { Flex } from 'tgui-core/components'; export const Notifications = (props) => { const { children } = props; diff --git a/tgui/packages/tgui-panel/Panel.tsx b/tgui/packages/tgui-panel/Panel.tsx index 2813b636574dc..d818f0351b878 100644 --- a/tgui/packages/tgui-panel/Panel.tsx +++ b/tgui/packages/tgui-panel/Panel.tsx @@ -4,8 +4,8 @@ * @license MIT */ -import { Button, Section, Stack } from 'tgui/components'; import { Pane } from 'tgui/layouts'; +import { Button, Section, Stack } from 'tgui-core/components'; import { NowPlayingWidget, useAudio } from './audio'; import { ChatPanel, ChatTabs } from './chat'; diff --git a/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx b/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx index f101d7da0381e..53f3d5d7048e7 100644 --- a/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx +++ b/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx @@ -4,9 +4,9 @@ * @license MIT */ -import { toFixed } from 'common/math'; import { useDispatch, useSelector } from 'tgui/backend'; -import { Button, Collapsible, Flex, Knob, Section } from 'tgui/components'; +import { Button, Collapsible, Flex, Knob, Section } from 'tgui-core/components'; +import { toFixed } from 'tgui-core/math'; import { useSettings } from '../settings'; import { selectAudio } from './selectors'; diff --git a/tgui/packages/tgui-panel/chat/ChatPageSettings.jsx b/tgui/packages/tgui-panel/chat/ChatPageSettings.jsx index ba577a1e5ef2f..a4402fc7bf294 100644 --- a/tgui/packages/tgui-panel/chat/ChatPageSettings.jsx +++ b/tgui/packages/tgui-panel/chat/ChatPageSettings.jsx @@ -12,7 +12,7 @@ import { Input, Section, Stack, -} from 'tgui/components'; +} from 'tgui-core/components'; import { moveChatPageLeft, @@ -30,7 +30,7 @@ export const ChatPageSettings = (props) => { return (
- {!!!page.isMain && ( + {!page.isMain && ( }` - */ - buttons: ReactNode; - /** - * Same as buttons, but. Have disabled pointer-events on content inside if non-fluid. - * Fluid version have humburger layout. - */ - buttonsAlt: ReactNode; - /** Content under image. Or on the right if fluid. */ - children: ReactNode; - /** Applies a CSS class to the element. */ - className: string; - /** Color of the button. See [Button](#button) but without `transparent`. */ - color: string; - /** Makes button disabled and dark red if true. Also disables onClick. */ - disabled: BooleanLike; - /** Optional. Adds a "stub" when loading DmIcon. */ - dmFallback: ReactNode; - /** Parameter `icon` of component `DmIcon`. */ - dmIcon: string | null; - /** Parameter `icon_state` of component `DmIcon`. */ - dmIconState: string | null; - /** Parameter `direction` of component `DmIcon`. */ - dmDirection: any; - /** - * Changes the layout of the button, making it fill the entire horizontally available space. - * Allows the use of `title` - */ - fluid: boolean; - /** Parameter responsible for the size of the image, component and standard "stubs". */ - imageSize: number; - /** Prop `src` of . Example: `imageSrc={resolveAsset(thing.image}` */ - imageSrc: string; - /** Called when button is clicked with LMB. */ - onClick: (e: any) => void; - /** Called when button is clicked with RMB. */ - onRightClick: (e: any) => void; - /** Makes button selected and green if true. */ - selected: BooleanLike; - /** Requires `fluid` for work. Bold text with divider betwen content. */ - title: string; - /** A fancy, boxy tooltip, which appears when hovering over the button */ - tooltip: ReactNode; - /** Position of the tooltip. See [`Popper`](#Popper) for valid options. */ - tooltipPosition: Placement; -}> & - BoxProps; - -export const ImageButton = (props: Props) => { - const { - asset, - base64, - buttons, - buttonsAlt, - children, - className, - color, - disabled, - dmFallback, - dmDirection, - dmIcon, - dmIconState, - fluid, - imageSize = 64, - imageSrc, - onClick, - onRightClick, - selected, - title, - tooltip, - tooltipPosition, - ...rest - } = props; - - const getFallback = (iconName: string, iconSpin: boolean) => { - return ( - - - - - - ); - }; - - let buttonContent = ( -
{ - if (!disabled && onClick) { - onClick(event); - } - }} - onContextMenu={(event) => { - event.preventDefault(); - if (!disabled && onRightClick) { - onRightClick(event); - } - }} - style={{ width: !fluid ? `calc(${imageSize}px + 0.5em + 2px)` : 'auto' }} - > -
- {base64 || asset || imageSrc ? ( - - ) : dmIcon && dmIconState ? ( - - ) : ( - getFallback('question', false) - )} -
- {fluid ? ( -
- {title && ( - - {title} - - )} - {children && ( - {children} - )} -
- ) : ( - children && ( - - {children} - - ) - )} -
- ); - - if (tooltip) { - buttonContent = ( - - {buttonContent} - - ); - } - - return ( -
- {buttonContent} - {buttons && ( -
- {buttons} -
- )} - {buttonsAlt && ( -
- {buttonsAlt} -
- )} -
- ); -}; diff --git a/tgui/packages/tgui/bandastation/ItemDisplay220.tsx b/tgui/packages/tgui/bandastation/ItemDisplay220.tsx index b3847d85117fe..a4b50d761ba1a 100644 --- a/tgui/packages/tgui/bandastation/ItemDisplay220.tsx +++ b/tgui/packages/tgui/bandastation/ItemDisplay220.tsx @@ -1,8 +1,7 @@ -import { Box, Button } from 'tgui-core/components'; +import { Box, Button, ImageButton } from 'tgui-core/components'; import { useBackend } from '../backend'; import { LoadoutItem } from '../interfaces/PreferencesMenu/loadout/base'; -import { ImageButton } from './ImageButton'; export const ItemDisplay220 = (props: { active: boolean; @@ -72,7 +71,8 @@ export const ItemDisplay220 = (props: { tooltipPosition={'top'} /> ))} - buttonsAlt={textInfo} + /** Temporately disabled. Until ImageButtons tweaks will be merged */ + // buttonsAlt={textInfo} onClick={() => act('select_item', { path: item.path, diff --git a/tgui/packages/tgui/components/AnimatedNumber.tsx b/tgui/packages/tgui/components/AnimatedNumber.tsx deleted file mode 100644 index 435ed992d3c44..0000000000000 --- a/tgui/packages/tgui/components/AnimatedNumber.tsx +++ /dev/null @@ -1,186 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { clamp, toFixed } from 'common/math'; -import { Component, createRef } from 'react'; - -const isSafeNumber = (value: number) => { - // prettier-ignore - return typeof value === 'number' - && Number.isFinite(value) - && !Number.isNaN(value); -}; - -export type AnimatedNumberProps = { - /** - * The target value to approach. - */ - value: number; - - /** - * If provided, the initial value displayed. By default, the same as `value`. - * If `initial` and `value` are different, the component immediately starts - * animating. - */ - initial?: number; - - /** - * If provided, a function that formats the inner string. By default, - * attempts to match the numeric precision of `value`. - */ - format?: (value: number) => string; -}; - -/** - * Animated numbers are animated at roughly 60 frames per second. - */ -const SIXTY_HZ = 1_000.0 / 60.0; - -/** - * The exponential moving average coefficient. Larger values result in a faster - * convergence. - */ -const Q = 0.8333; - -/** - * A small number. - */ -const EPSILON = 10e-4; - -/** - * An animated number label. Shows a number, formatted with an optionally - * provided function, and animates it towards its target value. - */ -export class AnimatedNumber extends Component { - /** - * The inner `` being updated sixty times per second. - */ - ref = createRef(); - - /** - * The interval being used to update the inner span. - */ - interval?: NodeJS.Timeout; - - /** - * The current value. This values approaches the target value. - */ - currentValue: number = 0; - - constructor(props: AnimatedNumberProps) { - super(props); - - const { initial, value } = props; - - if (initial !== undefined && isSafeNumber(initial)) { - this.currentValue = initial; - } else if (isSafeNumber(value)) { - this.currentValue = value; - } - } - - componentDidMount() { - if (this.currentValue !== this.props.value) { - this.startTicking(); - } - } - - componentWillUnmount() { - // Stop animating when the component is unmounted. - this.stopTicking(); - } - - shouldComponentUpdate(newProps: AnimatedNumberProps) { - if (newProps.value !== this.props.value) { - // The target value has been adjusted; start animating if we aren't - // already. - this.startTicking(); - } - - return false; - } - - /** - * Starts animating the inner span. If the inner span is already animating, - * this is a no-op. - */ - startTicking() { - if (this.interval !== undefined) { - // We're already ticking; do nothing. - return; - } - - this.interval = setInterval(() => this.tick(), SIXTY_HZ); - } - - /** - * Stops animating the inner span. - */ - stopTicking() { - if (this.interval === undefined) { - // We're not ticking; do nothing. - return; - } - - clearInterval(this.interval); - - this.interval = undefined; - } - - /** - * Steps forward one frame. - */ - tick() { - const { currentValue } = this; - const { value } = this.props; - - if (isSafeNumber(value)) { - // Converge towards the value. - this.currentValue = currentValue * Q + value * (1 - Q); - } else { - // If the value is unsafe, we're never going to converge, so stop ticking. - this.stopTicking(); - } - - if ( - Math.abs(value - this.currentValue) < Math.max(EPSILON, EPSILON * value) - ) { - // We're about as close as we're going to get--snap to the value and - // stop ticking. - this.currentValue = value; - this.stopTicking(); - } - - if (this.ref.current) { - this.ref.current.textContent = this.getText(); - } - } - - /** - * Gets the inner text of the span. - */ - getText() { - const { props, currentValue } = this; - const { format, value } = props; - - if (!isSafeNumber(value)) { - return String(value); - } - - if (format) { - return format(this.currentValue); - } - - const fraction = String(value).split('.')[1]; - const precision = fraction ? fraction.length : 0; - - return toFixed(currentValue, clamp(precision, 0, 8)); - } - - render() { - return {this.getText()}; - } -} diff --git a/tgui/packages/tgui/components/Autofocus.tsx b/tgui/packages/tgui/components/Autofocus.tsx deleted file mode 100644 index 403dbe2e96532..0000000000000 --- a/tgui/packages/tgui/components/Autofocus.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { PropsWithChildren, useEffect, useRef } from 'react'; - -/** Used to force the window to steal focus on load. Children optional */ -export function Autofocus(props: PropsWithChildren) { - const { children } = props; - const ref = useRef(null); - - useEffect(() => { - const timer = setTimeout(() => { - ref.current?.focus(); - }, 1); - - return () => { - clearTimeout(timer); - }; - }, []); - - return ( -
- {children} -
- ); -} diff --git a/tgui/packages/tgui/components/Blink.jsx b/tgui/packages/tgui/components/Blink.jsx deleted file mode 100644 index dc12f84346eff..0000000000000 --- a/tgui/packages/tgui/components/Blink.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Component } from 'react'; - -const DEFAULT_BLINKING_INTERVAL = 1000; -const DEFAULT_BLINKING_TIME = 1000; - -export class Blink extends Component { - constructor(props) { - super(props); - this.state = { - hidden: false, - }; - } - - createTimer() { - const { - interval = DEFAULT_BLINKING_INTERVAL, - time = DEFAULT_BLINKING_TIME, - } = this.props; - - clearInterval(this.interval); - clearTimeout(this.timer); - - this.setState({ - hidden: false, - }); - - this.interval = setInterval(() => { - this.setState({ - hidden: true, - }); - - this.timer = setTimeout(() => { - this.setState({ - hidden: false, - }); - }, time); - }, interval + time); - } - - componentDidMount() { - this.createTimer(); - } - - componentDidUpdate(prevProps) { - if ( - prevProps.interval !== this.props.interval || - prevProps.time !== this.props.time - ) { - this.createTimer(); - } - } - - componentWillUnmount() { - clearInterval(this.interval); - clearTimeout(this.timer); - } - - render() { - return ( - - {this.props.children} - - ); - } -} diff --git a/tgui/packages/tgui/components/BlockQuote.tsx b/tgui/packages/tgui/components/BlockQuote.tsx deleted file mode 100644 index 3c627d3fac244..0000000000000 --- a/tgui/packages/tgui/components/BlockQuote.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { classes } from 'common/react'; - -import { Box, BoxProps } from './Box'; - -export function BlockQuote(props: BoxProps) { - const { className, ...rest } = props; - - return ; -} diff --git a/tgui/packages/tgui/components/Box.tsx b/tgui/packages/tgui/components/Box.tsx deleted file mode 100644 index a39b98ed48f7d..0000000000000 --- a/tgui/packages/tgui/components/Box.tsx +++ /dev/null @@ -1,264 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { BooleanLike, classes } from 'common/react'; -import { - createElement, - DragEventHandler, - KeyboardEventHandler, - MouseEventHandler, - ReactNode, - UIEventHandler, -} from 'react'; - -import { CSS_COLORS } from '../constants'; -import { logger } from '../logging'; - -type BooleanProps = Partial>; -type StringProps = Partial< - Record ->; - -export type EventHandlers = Partial<{ - onClick: MouseEventHandler; - onContextMenu: MouseEventHandler; - onDoubleClick: MouseEventHandler; - onKeyDown: KeyboardEventHandler; - onKeyUp: KeyboardEventHandler; - onMouseDown: MouseEventHandler; - onMouseMove: MouseEventHandler; - onMouseOver: MouseEventHandler; - onMouseUp: MouseEventHandler; - onScroll: UIEventHandler; - onDrop: DragEventHandler; -}>; - -export type BoxProps = Partial<{ - as: string; - children: ReactNode; - className: string | BooleanLike; - style: Partial; -}> & - BooleanProps & - StringProps & - EventHandlers; - -// Don't you dare put this elsewhere -type DangerDoNotUse = { - dangerouslySetInnerHTML?: { - __html: any; - }; -}; - -/** - * Coverts our rem-like spacing unit into a CSS unit. - */ -export const unit = (value: unknown) => { - if (typeof value === 'string') { - // Transparently convert pixels into rem units - if (value.endsWith('px')) { - return parseFloat(value) / 12 + 'rem'; - } - return value; - } - if (typeof value === 'number') { - return value + 'rem'; - } -}; - -/** - * Same as `unit`, but half the size for integers numbers. - */ -export const halfUnit = (value: unknown) => { - if (typeof value === 'string') { - return unit(value); - } - if (typeof value === 'number') { - return unit(value * 0.5); - } -}; - -const isColorCode = (str: unknown) => !isColorClass(str); - -const isColorClass = (str: unknown): boolean => { - return typeof str === 'string' && CSS_COLORS.includes(str as any); -}; - -const mapRawPropTo = (attrName) => (style, value) => { - if (typeof value === 'number' || typeof value === 'string') { - style[attrName] = value; - } -}; - -const mapUnitPropTo = (attrName, unit) => (style, value) => { - if (typeof value === 'number' || typeof value === 'string') { - style[attrName] = unit(value); - } -}; - -const mapBooleanPropTo = (attrName, attrValue) => (style, value) => { - if (value) { - style[attrName] = attrValue; - } -}; - -const mapDirectionalUnitPropTo = (attrName, unit, dirs) => (style, value) => { - if (typeof value === 'number' || typeof value === 'string') { - for (let i = 0; i < dirs.length; i++) { - style[attrName + '-' + dirs[i]] = unit(value); - } - } -}; - -const mapColorPropTo = (attrName) => (style, value) => { - if (isColorCode(value)) { - style[attrName] = value; - } -}; - -// String / number props -const stringStyleMap = { - align: mapRawPropTo('textAlign'), - bottom: mapUnitPropTo('bottom', unit), - fontFamily: mapRawPropTo('fontFamily'), - fontSize: mapUnitPropTo('fontSize', unit), - fontWeight: mapRawPropTo('fontWeight'), - height: mapUnitPropTo('height', unit), - left: mapUnitPropTo('left', unit), - maxHeight: mapUnitPropTo('maxHeight', unit), - maxWidth: mapUnitPropTo('maxWidth', unit), - minHeight: mapUnitPropTo('minHeight', unit), - minWidth: mapUnitPropTo('minWidth', unit), - opacity: mapRawPropTo('opacity'), - overflow: mapRawPropTo('overflow'), - overflowX: mapRawPropTo('overflowX'), - overflowY: mapRawPropTo('overflowY'), - position: mapRawPropTo('position'), - right: mapUnitPropTo('right', unit), - textAlign: mapRawPropTo('textAlign'), - top: mapUnitPropTo('top', unit), - verticalAlign: mapRawPropTo('verticalAlign'), - width: mapUnitPropTo('width', unit), - - lineHeight: (style, value) => { - if (typeof value === 'number') { - style['lineHeight'] = value; - } else if (typeof value === 'string') { - style['lineHeight'] = unit(value); - } - }, - // Margin - m: mapDirectionalUnitPropTo('margin', halfUnit, [ - 'Top', - 'Bottom', - 'Left', - 'Right', - ]), - mb: mapUnitPropTo('marginBottom', halfUnit), - ml: mapUnitPropTo('marginLeft', halfUnit), - mr: mapUnitPropTo('marginRight', halfUnit), - mt: mapUnitPropTo('marginTop', halfUnit), - mx: mapDirectionalUnitPropTo('margin', halfUnit, ['Left', 'Right']), - my: mapDirectionalUnitPropTo('margin', halfUnit, ['Top', 'Bottom']), - // Padding - p: mapDirectionalUnitPropTo('padding', halfUnit, [ - 'Top', - 'Bottom', - 'Left', - 'Right', - ]), - pb: mapUnitPropTo('paddingBottom', halfUnit), - pl: mapUnitPropTo('paddingLeft', halfUnit), - pr: mapUnitPropTo('paddingRight', halfUnit), - pt: mapUnitPropTo('paddingTop', halfUnit), - px: mapDirectionalUnitPropTo('padding', halfUnit, ['Left', 'Right']), - py: mapDirectionalUnitPropTo('padding', halfUnit, ['Top', 'Bottom']), - // Color props - color: mapColorPropTo('color'), - textColor: mapColorPropTo('color'), - backgroundColor: mapColorPropTo('backgroundColor'), -} as const; - -// Boolean props -const booleanStyleMap = { - bold: mapBooleanPropTo('fontWeight', 'bold'), - fillPositionedParent: (style, value) => { - if (value) { - style['position'] = 'absolute'; - style['top'] = 0; - style['bottom'] = 0; - style['left'] = 0; - style['right'] = 0; - } - }, - inline: mapBooleanPropTo('display', 'inline-block'), - italic: mapBooleanPropTo('fontStyle', 'italic'), - nowrap: mapBooleanPropTo('whiteSpace', 'nowrap'), - preserveWhitespace: mapBooleanPropTo('whiteSpace', 'pre-wrap'), -} as const; - -export const computeBoxProps = (props) => { - const computedProps: Record = {}; - const computedStyles: Record = {}; - - // Compute props - for (let propName of Object.keys(props)) { - if (propName === 'style') { - continue; - } - - const propValue = props[propName]; - - const mapPropToStyle = - stringStyleMap[propName] || booleanStyleMap[propName]; - - if (mapPropToStyle) { - mapPropToStyle(computedStyles, propValue); - } else { - computedProps[propName] = propValue; - } - } - - // Merge computed styles and any directly provided styles - computedProps.style = { ...computedStyles, ...props.style }; - - return computedProps; -}; - -export const computeBoxClassName = (props: BoxProps) => { - const color = props.textColor || props.color; - const backgroundColor = props.backgroundColor; - return classes([ - isColorClass(color) && 'color-' + color, - isColorClass(backgroundColor) && 'color-bg-' + backgroundColor, - ]); -}; - -export const Box = (props: BoxProps & DangerDoNotUse) => { - const { as = 'div', className, children, ...rest } = props; - - // Compute class name and styles - const computedClassName = className - ? `${className} ${computeBoxClassName(rest)}` - : computeBoxClassName(rest); - const computedProps = computeBoxProps(rest); - - if (as === 'img') { - logger.error( - 'Box component cannot be used as an image. Use Image component instead.', - ); - } - - // Render the component - return createElement( - typeof as === 'string' ? as : 'div', - { - ...computedProps, - className: computedClassName, - }, - children, - ); -}; diff --git a/tgui/packages/tgui/components/Button.tsx b/tgui/packages/tgui/components/Button.tsx deleted file mode 100644 index acc96aff6b072..0000000000000 --- a/tgui/packages/tgui/components/Button.tsx +++ /dev/null @@ -1,407 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { Placement } from '@popperjs/core'; -import { isEscape, KEY } from 'common/keys'; -import { BooleanLike, classes } from 'common/react'; -import { - ChangeEvent, - createRef, - MouseEvent, - ReactNode, - useEffect, - useRef, - useState, -} from 'react'; - -import { Box, BoxProps, computeBoxClassName, computeBoxProps } from './Box'; -import { Icon } from './Icon'; -import { Tooltip } from './Tooltip'; - -/** - * Getting ellipses to work requires that you use: - * 1. A string rather than a node - * 2. A fixed width here or in a parent - * 3. Children prop rather than content - */ -type EllipsisUnion = - | { - ellipsis: true; - children: string; - /** @deprecated use children instead */ - content?: never; - } - | Partial<{ - ellipsis: undefined; - children: ReactNode; - /** @deprecated use children instead */ - content: ReactNode; - }>; - -type Props = Partial<{ - captureKeys: boolean; - circular: boolean; - compact: boolean; - disabled: BooleanLike; - fluid: boolean; - icon: string | false; - iconColor: string; - iconPosition: string; - iconRotation: number; - iconSpin: BooleanLike; - onClick: (e: any) => void; - selected: BooleanLike; - tooltip: ReactNode; - tooltipPosition: Placement; - verticalAlignContent: string; -}> & - EllipsisUnion & - BoxProps; - -/** Clickable button. Comes with variants. Read more in the documentation. */ -export const Button = (props: Props) => { - const { - captureKeys = true, - children, - circular, - className, - color, - compact, - content, - disabled, - ellipsis, - fluid, - icon, - iconColor, - iconPosition, - iconRotation, - iconSpin, - onClick, - selected, - tooltip, - tooltipPosition, - verticalAlignContent, - ...rest - } = props; - - const toDisplay: ReactNode = content || children; - - let buttonContent = ( -
{ - if (!disabled && onClick) { - onClick(event); - } - }} - onKeyDown={(event) => { - if (!captureKeys) { - return; - } - - // Simulate a click when pressing space or enter. - if (event.key === KEY.Space || event.key === KEY.Enter) { - event.preventDefault(); - if (!disabled && onClick) { - onClick(event); - } - return; - } - - // Refocus layout on pressing escape. - if (isEscape(event.key)) { - event.preventDefault(); - } - }} - {...computeBoxProps(rest)} - > -
- {icon && iconPosition !== 'right' && ( - - )} - {!ellipsis ? ( - toDisplay - ) : ( - - {toDisplay} - - )} - {icon && iconPosition === 'right' && ( - - )} -
-
- ); - - if (tooltip) { - buttonContent = ( - - {buttonContent} - - ); - } - - return buttonContent; -}; - -type CheckProps = Partial<{ - checked: BooleanLike; -}> & - Props; - -/** Visually toggles between checked and unchecked states. */ -export const ButtonCheckbox = (props: CheckProps) => { - const { checked, ...rest } = props; - - return ( - - ); -}; - -Button.Confirm = ButtonConfirm; - -type InputProps = Partial<{ - currentValue: string; - defaultValue: string; - fluid: boolean; - maxLength: number; - onCommit: (e: any, value: string) => void; - placeholder: string; -}> & - Props; - -/** Accepts and handles user input. */ -const ButtonInput = (props: InputProps) => { - const { - children, - color = 'default', - content, - currentValue, - defaultValue, - disabled, - fluid, - icon, - iconRotation, - iconSpin, - maxLength, - onCommit = () => null, - placeholder, - tooltip, - tooltipPosition, - ...rest - } = props; - const [inInput, setInInput] = useState(false); - const inputRef = createRef(); - - const toDisplay = content || children; - - const commitResult = (e) => { - const input = inputRef.current; - if (!input) return; - - const hasValue = input.value !== ''; - if (hasValue) { - onCommit(e, input.value); - } else { - if (defaultValue) { - onCommit(e, defaultValue); - } - } - }; - - useEffect(() => { - const input = inputRef.current; - if (!input) return; - - if (inInput) { - input.value = currentValue || ''; - try { - input.focus(); - input.select(); - } catch {} - } - }, [inInput, currentValue]); - - let buttonContent = ( - { - if (!disabled) { - setInInput(true); - } - }} - > - {icon && } -
{toDisplay}
- { - if (!inInput) { - return; - } - setInInput(false); - commitResult(event); - }} - onKeyDown={(event) => { - if (event.key === KEY.Enter) { - setInInput(false); - commitResult(event); - return; - } - if (isEscape(event.key)) { - setInInput(false); - } - }} - /> -
- ); - - if (tooltip) { - buttonContent = ( - - {buttonContent} - - ); - } - - return buttonContent; -}; - -Button.Input = ButtonInput; - -type FileProps = { - accept: string; - multiple?: boolean; - onSelectFiles: (files: FileList) => void; -} & Props; - -/** Accepts file input */ -function ButtonFile(props: FileProps) { - const { accept, multiple, onSelectFiles, ...rest } = props; - - const inputRef = useRef(null); - - async function handleChange(event: ChangeEvent) { - const files = event.target.files; - if (files?.length) { - onSelectFiles(files); - event.target.value = ''; - } - } - - return ( - <> - - - {buttons && ( -
{buttons}
- )} - - {open && {children}} -
- ); -} diff --git a/tgui/packages/tgui/components/ColorBox.tsx b/tgui/packages/tgui/components/ColorBox.tsx deleted file mode 100644 index b04cdd9241ae4..0000000000000 --- a/tgui/packages/tgui/components/ColorBox.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { classes } from 'common/react'; -import { ReactNode } from 'react'; - -import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; - -type Props = { - content?: ReactNode; -} & BoxProps; - -export function ColorBox(props: Props) { - const { content, children, className, ...rest } = props; - - rest.color = content ? null : 'default'; - rest.backgroundColor = props.color || 'default'; - - return ( -
- {content || '.'} -
- ); -} diff --git a/tgui/packages/tgui/components/Dialog.tsx b/tgui/packages/tgui/components/Dialog.tsx deleted file mode 100644 index 897ce236a4a17..0000000000000 --- a/tgui/packages/tgui/components/Dialog.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @file - * @copyright 2022 raffclar - * @license MIT - */ -import { Box } from './Box'; -import { Button } from './Button'; - -type DialogProps = { - title: any; - onClose: () => void; - children: any; - width?: string; - height?: string; -}; - -export const Dialog = (props: DialogProps) => { - const { title, onClose, children, width, height } = props; - return ( -
- -
-
{title}
- -
- {children} -
-
- ); -}; - -type DialogButtonProps = { - onClick: () => void; - children: any; -}; - -const DialogButton = (props: DialogButtonProps) => { - const { onClick, children } = props; - return ( - - ); -}; - -Dialog.Button = DialogButton; - -type UnsavedChangesDialogProps = { - documentName: string; - onSave: () => void; - onDiscard: () => void; - onClose: () => void; -}; - -export const UnsavedChangesDialog = (props: UnsavedChangesDialogProps) => { - const { documentName, onSave, onDiscard, onClose } = props; - return ( - -
- Do you want to save changes to {documentName}? -
-
- Save - Don't Save - Cancel -
-
- ); -}; diff --git a/tgui/packages/tgui/components/Dimmer.tsx b/tgui/packages/tgui/components/Dimmer.tsx deleted file mode 100644 index d1ddb64c4e0c4..0000000000000 --- a/tgui/packages/tgui/components/Dimmer.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { classes } from 'common/react'; - -import { Box, BoxProps } from './Box'; - -export function Dimmer(props: BoxProps) { - const { className, children, ...rest } = props; - - return ( - -
{children}
-
- ); -} diff --git a/tgui/packages/tgui/components/Divider.tsx b/tgui/packages/tgui/components/Divider.tsx deleted file mode 100644 index 1582aeadfd447..0000000000000 --- a/tgui/packages/tgui/components/Divider.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { classes } from 'common/react'; - -type Props = Partial<{ - hidden: boolean; - vertical: boolean; -}>; - -export function Divider(props: Props) { - const { hidden, vertical } = props; - - return ( -