diff --git a/code/__DEFINES/icon_smoothing.dm b/code/__DEFINES/icon_smoothing.dm index c456b550ae9..63e090d17d1 100644 --- a/code/__DEFINES/icon_smoothing.dm +++ b/code/__DEFINES/icon_smoothing.dm @@ -216,6 +216,8 @@ DEFINE_BITFIELD(smoothing_junction, list( #define SMOOTH_GROUP_SHIELD_GEN_WALL S_OBJ(79) // /obj/machinery/shieldwall #define SMOOTH_GROUP_VIS_BLOCK S_OBJ(80) // /obj/effect/abstract/finder/vis_block, marks turfs where vis_blocking is going on +#define SMOOTH_GROUP_GRAV_FIELD S_OBJ(69) + /// Performs the work to set smoothing_groups and canSmoothWith. /// An inlined function used in both turf/Initialize and atom/Initialize. #define SETUP_SMOOTHING(...) \ diff --git a/code/datums/proximity_monitor/fields/gravity.dm b/code/datums/proximity_monitor/fields/gravity.dm index b7e22840041..745072d69e1 100644 --- a/code/datums/proximity_monitor/fields/gravity.dm +++ b/code/datums/proximity_monitor/fields/gravity.dm @@ -63,3 +63,63 @@ /datum/proximity_monitor/advanced/gravity/warns_on_entrance/proc/clear_recent_warning(mob_ref_key) LAZYREMOVE(recently_warned, mob_ref_key) + +/obj/gravity_fluff_field + icon = 'icons/obj/smooth_structures/grav_field.dmi' + icon_state = "grav_field-0" + base_icon_state = "grav_field" + obj_flags = NONE + anchored = TRUE + move_resist = INFINITY + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + pass_flags_self = LETPASSCLICKS + smoothing_flags = SMOOTH_BITMASK + smoothing_groups = SMOOTH_GROUP_GRAV_FIELD + canSmoothWith = SMOOTH_GROUP_GRAV_FIELD + alpha = 200 + /// our emissive appearance + var/mutable_appearance/emissive + +/obj/gravity_fluff_field/Initialize(mapload, strength) + . = ..() + if(isnull(strength)) + return INITIALIZE_HINT_QDEL + QUEUE_SMOOTH(src) + QUEUE_SMOOTH_NEIGHBORS(src) + switch(strength) + if(2 to INFINITY) + particles = new /particles/grav_field_down/strong() + if(1 to 2) + particles = new /particles/grav_field_down() + if(0 to 1) + particles = new /particles/grav_field_float() + if(-INFINITY to -1) + particles = new /particles/grav_field_up() + color = particles.color + RegisterSignal(src, COMSIG_ATOM_SMOOTHED_ICON, PROC_REF(smoothed)) + +/obj/gravity_fluff_field/Destroy(force) + . = ..() + QDEL_NULL(particles) + emissive = null + +/obj/gravity_fluff_field/proc/smoothed(datum/source) + SIGNAL_HANDLER + cut_overlay(emissive) + // because it uses a different name + emissive = emissive_appearance('icons/obj/smooth_structures/grav_field_emissive.dmi', "grav_field_emissive-[splittext(icon_state, "-")[2]]", src) + add_overlay(emissive) + +// Subtype which adds a subtle overlay to all turfs +/datum/proximity_monitor/advanced/gravity/subtle_effect + +/datum/proximity_monitor/advanced/gravity/subtle_effect/setup_field_turf(turf/target) + . = ..() + if(!isopenturf(target)) + return + new /obj/gravity_fluff_field(target, gravity_value) + +/datum/proximity_monitor/advanced/gravity/subtle_effect/cleanup_field_turf(turf/target) + . = ..() + qdel(locate(/obj/gravity_fluff_field) in target) diff --git a/code/game/machinery/portagrav.dm b/code/game/machinery/portagrav.dm new file mode 100644 index 00000000000..c970fa5f8f1 --- /dev/null +++ b/code/game/machinery/portagrav.dm @@ -0,0 +1,268 @@ +/obj/machinery/power/portagrav + anchored = FALSE + density = TRUE + interaction_flags_machine = INTERACT_MACHINE_ALLOW_SILICON + icon = 'icons/obj/machines/gravity_generator.dmi' + icon_state = "portagrav" + base_icon_state = "portagrav" + name = "Portable Gravity Unit" + desc = "Generates gravity around itself. Powered by wire or cell. Must be anchored before use." + max_integrity = 250 + circuit = /obj/item/circuitboard/machine/portagrav + armor_type = /datum/armor/portable_gravity + interaction_flags_click = ALLOW_SILICON_REACH + //We don't use area power + use_power = NO_POWER_USE + ///The cell we spawn with + var/obj/item/stock_parts/power_store/cell/cell = /obj/item/stock_parts/power_store/cell/high + ///Is the machine on? + var/on = FALSE + /// do we use power from wire instead + var/wire_mode = FALSE + /// our gravity field + var/datum/proximity_monitor/advanced/gravity/subtle_effect/gravity_field + /// strength of our gravity + var/grav_strength = STANDARD_GRAVITY + /// gravity range + var/range = 4 + /// max gravity range + var/max_range = 6 + /// draw per range + var/draw_per_range = BASE_MACHINE_ACTIVE_CONSUMPTION + +/datum/armor/portable_gravity + fire = 100 + melee = 10 + bomb = 40 + +/obj/machinery/power/portagrav/Initialize(mapload) + . = ..() + if(ispath(cell)) + cell = new cell(src) + if(anchored && wire_mode) + connect_to_network() + + AddElement( \ + /datum/element/contextual_screentip_bare_hands, \ + rmb_text = "Toggle power", \ + ) + + var/static/list/tool_behaviors = list( + TOOL_WRENCH = list( + SCREENTIP_CONTEXT_LMB = "Anchor", + ), + ) + AddElement(/datum/element/contextual_screentip_tools, tool_behaviors) + +/obj/machinery/power/portagrav/Destroy() + . = ..() + cell = null + +/obj/machinery/power/portagrav/update_overlays() + . = ..() + if(anchored) + . += "portagrav_anchors" + if(on) + . += "portagrav_o" + . += "activated" + +/obj/machinery/power/portagrav/examine(mob/user) + . = ..() + . += "It is [on ? "on" : "off"]." + . += "The charge meter reads: [!isnull(cell) ? "[round(cell.percent(), 1)]%" : "NO CELL"]." + . += "It is[anchored ? "" : " not"] anchored." + if(in_range(user, src) || isobserver(user)) + . += span_notice("Right-click to toggle [on ? "off" : "on"].") + +/obj/machinery/power/portagrav/RefreshParts() + . = ..() + var/power_usage = initial(draw_per_range) + for(var/datum/stock_part/micro_laser/laser in component_parts) + power_usage -= BASE_MACHINE_ACTIVE_CONSUMPTION / 10 * (laser.tier - 1) + draw_per_range = power_usage + var/new_range = 4 + for(var/datum/stock_part/capacitor/capacitor in component_parts) + new_range += capacitor.tier + max_range = new_range + update_field() + +/obj/machinery/power/portagrav/screwdriver_act(mob/living/user, obj/item/tool) + . = NONE + if(default_deconstruction_screwdriver(user, "[base_icon_state]_o", base_icon_state, tool)) + return ITEM_INTERACT_SUCCESS + +/obj/machinery/power/portagrav/crowbar_act(mob/living/user, obj/item/tool) + . = NONE + if(default_deconstruction_crowbar(tool)) + return ITEM_INTERACT_SUCCESS + +/obj/machinery/power/portagrav/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + . = NONE + if(!istype(tool, /obj/item/stock_parts/power_store/cell)) + return + if(!panel_open) + balloon_alert(user, "must open panel!") + return ITEM_INTERACT_BLOCKING + if(cell) + balloon_alert(user, "already has a cell!") + return ITEM_INTERACT_BLOCKING + if(!user.transferItemToLoc(tool, src)) + return ITEM_INTERACT_FAILURE + cell = tool + return ITEM_INTERACT_SUCCESS + +/obj/machinery/power/portagrav/should_have_node() + return anchored + +/obj/machinery/power/portagrav/connect_to_network() + if(!anchored) + return FALSE + . = ..() + +/obj/machinery/power/portagrav/wrench_act(mob/living/user, obj/item/tool) + . = ..() + if(on) + balloon_alert(user, "turn off first!") + return + default_unfasten_wrench(user, tool) + if(anchored && wire_mode) + connect_to_network() + else + disconnect_from_network() + update_appearance() + return ITEM_INTERACT_SUCCESS + +/obj/machinery/power/portagrav/get_cell() + return cell + +/obj/machinery/power/portagrav/attack_hand(mob/living/carbon/user, list/modifiers) + . = ..() + if(!panel_open || isnull(cell) || !istype(user) || user.combat_mode) + return + if(user.put_in_hands(cell)) + cell = null + +/obj/machinery/power/portagrav/attack_hand_secondary(mob/user, list/modifiers) + if(!can_interact(user)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + toggle_on(user) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/machinery/power/portagrav/emag_act(mob/user, obj/item/card/emag/emag_card) + if(obj_flags & EMAGGED) + return FALSE + obj_flags |= EMAGGED + visible_message(span_warning("Sparks fly out of [src]!")) + if(user) + balloon_alert(user, "unsafe gravity unlocked") + user.log_message("emagged [src].", LOG_ATTACK) + playsound(src, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + return TRUE + +/obj/machinery/power/portagrav/proc/toggle_on(mob/user) + if(on) + turn_off(user) + else + turn_on(user) + +/obj/machinery/power/portagrav/proc/turn_on(mob/user) + if(!anchored) + if(!isnull(user)) + balloon_alert(user, "not anchored!") + return FALSE + if((!wire_mode && cell?.charge < draw_per_range * range) || (wire_mode && surplus() < draw_per_range * range)) + if(!isnull(user)) + balloon_alert(user, "not enough power!") + return FALSE + if(!isnull(user)) + balloon_alert(user, "turned on") + on = TRUE + START_PROCESSING(SSmachines, src) + gravity_field = new(src, range = src.range, gravity = grav_strength) + update_appearance() + +/obj/machinery/power/portagrav/proc/turn_off(mob/user) + on = FALSE + if(!isnull(user)) + balloon_alert(user, "turned off") + STOP_PROCESSING(SSmachines, src) + QDEL_NULL(gravity_field) + update_appearance() + +/obj/machinery/power/portagrav/process(seconds_per_tick) + if(!on || !anchored) + return PROCESS_KILL + if(wire_mode) + if(powernet && surplus() >= draw_per_range * range) + add_load(draw_per_range * range) + else + turn_off() + else + if(!cell?.use(draw_per_range * range)) + turn_off() + +/obj/machinery/power/portagrav/proc/update_field() + if(isnull(gravity_field)) + return + gravity_field.set_range(range) + gravity_field.gravity_value = grav_strength + gravity_field.recalculate_field(full_recalc = TRUE) + +/obj/machinery/power/portagrav/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Portagrav", name) + ui.open() + +/obj/machinery/power/portagrav/ui_data(mob/user) + . = list() + if(!isnull(cell)) + .["percentage"] = (cell.charge / cell.maxcharge) * 100 + .["gravity"] = grav_strength + .["range"] = range + .["maxrange"] = max_range + .["on"] = on + .["wiremode"] = wire_mode + .["draw"] = display_power(draw_per_range * range) + +/obj/machinery/power/portagrav/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + playsound(src, 'sound/machines/terminal_button07.ogg', 45, TRUE) + switch(action) + if("adjust_grav") + var/adjustment = text2num(params["adjustment"]) + if(isnull(adjustment)) + return + var/bonus = (obj_flags & EMAGGED) ? 2 : 0 + // REPLACE 0 with NEGATIVE_GRAVITY ONCE NEGATIVE GRAVITY IS SOMETHING ACTUALLY FUNCTIONAL + var/result = clamp(grav_strength + adjustment, 0, GRAVITY_DAMAGE_THRESHOLD - 1 + bonus) + if(result == grav_strength) + return + grav_strength = result + update_field() + return TRUE + if("toggle_power") + toggle_on(usr) + return TRUE + if("toggle_wire") + wire_mode = !wire_mode + if(wire_mode && anchored) + connect_to_network() + else + disconnect_from_network() + return TRUE + if("adjust_range") + var/adjustment = text2num(params["adjustment"]) + if(isnull(adjustment)) + return + var/result = clamp(range + adjustment, 0, max_range) + if(result == range) + return + range = result + update_field() + return TRUE + +/obj/machinery/power/portagrav/anchored + anchored = TRUE diff --git a/code/game/objects/effects/particles/gravity.dm b/code/game/objects/effects/particles/gravity.dm new file mode 100644 index 00000000000..0d74896e20a --- /dev/null +++ b/code/game/objects/effects/particles/gravity.dm @@ -0,0 +1,44 @@ +/particles/grav_field_down + icon = 'icons/effects/particles/generic.dmi' + icon_state = "cross" + width = 100 + height = 100 + count = 5 + spawning = 1 + lifespan = 0.6 SECONDS + fade = 0.5 SECONDS + fadein = 0.2 SECONDS + position = generator(GEN_CIRCLE, 0, 16, UNIFORM_RAND) + gravity = list(0, -0.75) + color = "#FF0000" + +/particles/grav_field_down/strong + gravity = list(0, -1.75) + +/particles/grav_field_up + icon = 'icons/effects/particles/generic.dmi' + icon_state = "cross" + width = 100 + height = 100 + count = 5 + spawning = 1 + lifespan = 0.6 SECONDS + fade = 0.5 SECONDS + fadein = 0.2 SECONDS + position = generator(GEN_CIRCLE, 0, 16, UNIFORM_RAND) + gravity = list(0, 0.75) + color = "#0077ff" + +/particles/grav_field_float + icon = 'icons/effects/particles/generic.dmi' + icon_state = "cross" + width = 100 + height = 100 + count = 5 + spawning = 1 + lifespan = 0.6 SECONDS + fade = 0.5 SECONDS + fadein = 0.2 SECONDS + position = generator(GEN_CIRCLE, 0, 16, UNIFORM_RAND) + velocity = generator(GEN_VECTOR, list(2,0), list(-2,0), UNIFORM_RAND) + color = "#FFFF00" diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm index 4e7e5f3dd2c..5e41b35d9bb 100644 --- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm @@ -1694,3 +1694,12 @@ req_components = list( /obj/item/pipe/trinary/flippable/filter = 1, ) + +/obj/item/circuitboard/machine/portagrav + name = "Portable Gravity Unit" + greyscale_colors = CIRCUIT_COLOR_ENGINEERING + build_path = /obj/machinery/power/portagrav + req_components = list( + /datum/stock_part/capacitor = 2, + /datum/stock_part/micro_laser = 2, + /obj/item/stack/sheet/glass = 1) diff --git a/code/modules/cargo/packs/engineering.dm b/code/modules/cargo/packs/engineering.dm index 771cba47df6..907ae1598c6 100644 --- a/code/modules/cargo/packs/engineering.dm +++ b/code/modules/cargo/packs/engineering.dm @@ -328,3 +328,12 @@ ) crate_name = "radioactive nebula shielding (IMPORTANT)" crate_type = /obj/structure/closet/crate/engineering + +/datum/supply_pack/engineering/portagrav + name = "Portable Gravity Unit Crate" + desc = "Contains a portable gravity unit, to make the clown float into the ceiling." + cost = CARGO_CRATE_VALUE * 4 + access_view = ACCESS_ENGINEERING + contains = list(/obj/machinery/power/portagrav = 1) + crate_name = "portable gravity unit crate" + crate_type = /obj/structure/closet/crate/engineering diff --git a/icons/obj/machines/gravity_generator.dmi b/icons/obj/machines/gravity_generator.dmi index 69395034dc8..18ad0ca0c20 100644 Binary files a/icons/obj/machines/gravity_generator.dmi and b/icons/obj/machines/gravity_generator.dmi differ diff --git a/icons/obj/smooth_structures/grav_field.dmi b/icons/obj/smooth_structures/grav_field.dmi new file mode 100644 index 00000000000..4f51707938e Binary files /dev/null and b/icons/obj/smooth_structures/grav_field.dmi differ diff --git a/icons/obj/smooth_structures/grav_field.png b/icons/obj/smooth_structures/grav_field.png new file mode 100644 index 00000000000..5177e577240 Binary files /dev/null and b/icons/obj/smooth_structures/grav_field.png differ diff --git a/icons/obj/smooth_structures/grav_field.png.toml b/icons/obj/smooth_structures/grav_field.png.toml new file mode 100644 index 00000000000..49f4000c213 --- /dev/null +++ b/icons/obj/smooth_structures/grav_field.png.toml @@ -0,0 +1,5 @@ +output_name = "grav_field" +template = "bitmask/diagonal_32x32.toml" + +[animation] +delays = [1, 1, 1] diff --git a/icons/obj/smooth_structures/grav_field_emissive.dmi b/icons/obj/smooth_structures/grav_field_emissive.dmi new file mode 100644 index 00000000000..a2db63f082a Binary files /dev/null and b/icons/obj/smooth_structures/grav_field_emissive.dmi differ diff --git a/icons/obj/smooth_structures/grav_field_emissive.png b/icons/obj/smooth_structures/grav_field_emissive.png new file mode 100644 index 00000000000..d3746ac5b63 Binary files /dev/null and b/icons/obj/smooth_structures/grav_field_emissive.png differ diff --git a/icons/obj/smooth_structures/grav_field_emissive.png.toml b/icons/obj/smooth_structures/grav_field_emissive.png.toml new file mode 100644 index 00000000000..ba6a7f73aaf --- /dev/null +++ b/icons/obj/smooth_structures/grav_field_emissive.png.toml @@ -0,0 +1,5 @@ +output_name = "grav_field_emissive" +template = "bitmask/diagonal_32x32.toml" + +[animation] +delays = [1, 1, 1] diff --git a/tgstation.dme b/tgstation.dme index ab54683d01a..8d65b76d6d3 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2073,6 +2073,7 @@ #include "code\game\machinery\nebula_shielding.dm" #include "code\game\machinery\PDApainter.dm" #include "code\game\machinery\photobooth.dm" +#include "code\game\machinery\portagrav.dm" #include "code\game\machinery\prisongate.dm" #include "code\game\machinery\prisonlabor.dm" #include "code\game\machinery\quantum_pad.dm" @@ -2275,6 +2276,7 @@ #include "code\game\objects\effects\landmarks\atmospherics_sanity_landmarks.dm" #include "code\game\objects\effects\particles\acid.dm" #include "code\game\objects\effects\particles\fire.dm" +#include "code\game\objects\effects\particles\gravity.dm" #include "code\game\objects\effects\particles\misc.dm" #include "code\game\objects\effects\particles\note_particles.dm" #include "code\game\objects\effects\particles\slime.dm" diff --git a/tgui/packages/tgui/interfaces/Portagrav.tsx b/tgui/packages/tgui/interfaces/Portagrav.tsx new file mode 100644 index 00000000000..d84c5b63842 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Portagrav.tsx @@ -0,0 +1,134 @@ +import { BooleanLike } from 'common/react'; + +import { useBackend } from '../backend'; +import { Box, Button, Icon, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +type Data = { + percentage: number; + on: BooleanLike; + range: number; + maxrange: number; + gravity: number; + wiremode: BooleanLike; + draw: number; +}; + +export const Portagrav = (props) => { + const { act, data } = useBackend(); + const { percentage, on, range, gravity, wiremode, maxrange, draw } = data; + + return ( + + + + {!wiremode && percentage !== undefined + ? percentage + '%' + : wiremode + ? 'WIRE POWERED' + : 'NO CELL'} + {' - ' + gravity + 'G'} + + + {on ? draw : 'OFF'} / Rng: {range}/{maxrange} + +
+ + + + + Power + + + + + + + + + + + + + Gravity Control + + + + + + + + + + + + + + Range Control + + + + + + + + + + + +
+
+
+ ); +};