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 783a1bf796b..1ea6ef446de 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm @@ -76,6 +76,9 @@ #define HEARING_MESSAGE_MODE 9 #define HEARING_RANGE 10 +///called when space wind can't move a movable. (pressure_difference, pressure_direction) +#define COMSIG_MOVABLE_RESISTED_SPACEWIND "movable_resisted_wind" + ///called when the movable is added to a disposal holder object for disposal movement: (obj/structure/disposalholder/holder, obj/machinery/disposal/source) #define COMSIG_MOVABLE_DISPOSING "movable_disposing" // called when movable is expelled from a disposal pipe, bin or outlet on obj/pipe_eject: (direction) diff --git a/code/datums/components/crafting/atmospheric.dm b/code/datums/components/crafting/atmospheric.dm index 6bf75e1c884..0e59d00a714 100644 --- a/code/datums/components/crafting/atmospheric.dm +++ b/code/datums/components/crafting/atmospheric.dm @@ -39,6 +39,19 @@ blacklist = list(/obj/item/analyzer/ranged) category = CAT_ATMOSPHERIC +/datum/crafting_recipe/portable_wind_turbine + name = "Portable Wind Turbine" + result = /obj/item/portable_wind_turbine + tool_behaviors = list(TOOL_WELDER) + reqs = list( + /obj/item/knife/kitchen = 3, + /obj/item/stack/sheet/plastic = 5, + /obj/item/stack/rods = 8, + /obj/item/stock_parts/servo = 2, + /obj/item/stack/cable_coil = 5, + ) + category = CAT_ATMOSPHERIC + ///abstract path for pipe crafting recipes that set the pipe_type of their results and have other checks as well /datum/crafting_recipe/spec_pipe var/pipe_type diff --git a/code/datums/elements/drag_pickup.dm b/code/datums/elements/drag_pickup.dm index d86c615c638..8eb73bbb438 100644 --- a/code/datums/elements/drag_pickup.dm +++ b/code/datums/elements/drag_pickup.dm @@ -21,6 +21,10 @@ var/mob/living/picker = user if(!istype(picker) || !user.can_perform_action(source, FORBID_TELEKINESIS_REACH)) return + var/obj/pickup_object = source + if (pickup_object) + if (pickup_object.anchored) + return COMPONENT_CANCEL_MOUSEDROP_ONTO if(over == picker) INVOKE_ASYNC(picker, TYPE_PROC_REF(/mob/, put_in_hands), source) diff --git a/code/game/objects/items/wind_turbine.dm b/code/game/objects/items/wind_turbine.dm new file mode 100644 index 00000000000..74cd1213b69 --- /dev/null +++ b/code/game/objects/items/wind_turbine.dm @@ -0,0 +1,376 @@ +#define TURBINE_MAX_STORED_POWER (0.2 * STANDARD_CELL_CHARGE) +#define TURBINE_ANIMATION_TICKS_PER_TILE (1) +#define TURBINE_ANIMATION_TICKS (4) +#define TURBINE_CHARGE_PER_TILE (0.002 * STANDARD_CELL_CHARGE) +#define TURBINE_ANCHORED_POWER_PER_KPA (TURBINE_CHARGE_PER_TILE * 2) +#define TURBINE_MIN_SECONDS_BETWEEN_SOUNDS (0.33 SECONDS) + +/obj/item/portable_wind_turbine + name = "portable wind turbine" + icon = 'icons/obj/wind_turbine.dmi' + icon_state = "icon" + base_icon_state = "icon" + desc = "A portable wind turbine that can charge attached energy based weaponry, PDAs, and other devices. As a safety mechanism after an unprecedented amount of amputations, only charges when attached to your back / the floor." + worn_icon = 'icons/obj/wind_turbine.dmi' + worn_icon_state = "base_turbine" + inhand_icon_state = "wind_turbine" + lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi' + righthand_file = 'icons/mob/inhands/64x64_righthand.dmi' + inhand_x_dimension = 64 + inhand_y_dimension = 64 + slot_flags = ITEM_SLOT_SUITSTORE | ITEM_SLOT_BACK + force = 10 + throwforce = 6 + w_class = WEIGHT_CLASS_BULKY + ///What item is being charged currently? + var/obj/item/charging = null + ///Did we put power into "charging" last process()? + var/using_power = FALSE + ///Did we finish recharging the currently inserted item? + var/finished_recharging = FALSE + + ///Current rotor animation frame. (floating point value). + var/rotor_tick = 0 + ///Power that has been generated by moving about. + var/available_power = 0 + ///The last time (world.time) a sound was played at. + var/last_sound_time = 0 + ///Capacitor currently inserted. + var/obj/item/stock_parts/capacitor/cap = null + + var/static/list/allowed_devices = typecacheof(list( + /obj/item/gun/energy, + /obj/item/compact_remote, + /obj/item/controller, + /obj/item/organ/cyberimp/bci, + /obj/item/integrated_circuit, + /obj/item/melee/baton/security, + /obj/item/modular_computer, + /obj/item/stock_parts/power_store/cell, + )) + +/obj/item/portable_wind_turbine/Initialize(mapload) + . = ..() + AddElement(/datum/element/drag_pickup) + RegisterSignal(src, COMSIG_MOVABLE_SET_ANCHORED, PROC_REF(on_anchor)) + RegisterSignal(src, COMSIG_ATOM_PRE_DIR_CHANGE, PROC_REF(block_dir_changes_unanchored)) + update_appearance() + +/obj/item/portable_wind_turbine/loaded/Initialize(mapload) + . = ..() + cap = new /obj/item/stock_parts/capacitor(src) + +/obj/item/portable_wind_turbine/Destroy() + return ..() + +///Called when src is anchored +/obj/item/portable_wind_turbine/proc/on_anchor(atom/source, is_anchored) + SIGNAL_HANDLER + + if (is_anchored) + RegisterSignal(src, COMSIG_MOVABLE_RESISTED_SPACEWIND, PROC_REF(on_space_wind)) + UnregisterSignal(src, COMSIG_ATOM_PRE_DIR_CHANGE) + forceMove(get_turf(src)) + pixel_x = 0 + pixel_y = 0 + setDir(SOUTH) + else + UnregisterSignal(src, COMSIG_MOVABLE_RESISTED_SPACEWIND) + RegisterSignal(src, COMSIG_ATOM_PRE_DIR_CHANGE, PROC_REF(block_dir_changes_unanchored)) + +///Called when src changes direction when not equipped +/obj/item/portable_wind_turbine/proc/block_dir_changes_unanchored(atom/source, old_dir, new_dir) + SIGNAL_HANDLER + + return COMPONENT_ATOM_BLOCK_DIR_CHANGE + +///Called when this resists space wind +/obj/item/portable_wind_turbine/proc/on_space_wind(atom/source, pressure_difference, pressure_direction) + SIGNAL_HANDLER + + var/obj/item/portable_wind_turbine/turbine = source + if (!turbine) + return + if (isnull(cap)) + return + turbine.add_power(TURBINE_ANCHORED_POWER_PER_KPA * pressure_difference * cap.rating, ignore_cap = TRUE) + set_rotor_tick(rotor_tick + 1) + +/obj/item/portable_wind_turbine/update_appearance(updates) + . = ..() + update_back() + +/obj/item/portable_wind_turbine/equipped(mob/user, slot, initial) + . = ..() + update_appearance() + if(slot & slot_flags) + RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) + RegisterSignal(user, COMSIG_ATOM_POST_DIR_CHANGE, PROC_REF(on_dir_change)) + else + setDir(SOUTH) + UnregisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) + UnregisterSignal(user, COMSIG_ATOM_POST_DIR_CHANGE, PROC_REF(on_dir_change)) + +///Called when the thing HOLDING the turbine changes direction +/obj/item/portable_wind_turbine/proc/on_dir_change(datum/source, old_dir, new_dir) + SIGNAL_HANDLER + + update_appearance() + +///Updates the worn back icon for the current loc +/obj/item/portable_wind_turbine/proc/update_back() + if (ishuman(loc)) + var/mob/living/carbon/human/human = loc + human.update_worn_back() + +///Tries to play the woosh sound effect. May not play it if it's been too soon since the last call. +/obj/item/portable_wind_turbine/proc/try_playsound() + if ((world.time - last_sound_time) < TURBINE_MIN_SECONDS_BETWEEN_SOUNDS) + return + playsound(src, 'sound/machines/woosh.ogg', 20, FALSE) + last_sound_time = world.time + +///Sets the rotor animation tick to a new value. Returns TRUE if the rotor made a full rotation. +/obj/item/portable_wind_turbine/proc/set_rotor_tick(new_tick) + var/last_rotor_tick = floor(rotor_tick) + rotor_tick = new_tick + var/made_full_rotation = FALSE + if (rotor_tick >= TURBINE_ANIMATION_TICKS) + rotor_tick -= floor(rotor_tick / TURBINE_ANIMATION_TICKS) * TURBINE_ANIMATION_TICKS + try_playsound() + made_full_rotation = TRUE + var/rounded_rotor_tick = floor(rotor_tick) + if (rounded_rotor_tick != last_rotor_tick) + update_appearance() + return made_full_rotation + +///Adds a certain amount of power to the internal buffer +/obj/item/portable_wind_turbine/proc/add_power(power, ignore_cap = FALSE) + if (ignore_cap) + available_power += power + else + available_power = min(available_power + power, TURBINE_MAX_STORED_POWER * (isnull(cap?.rating) ? 1 : cap.rating)) + +///Called when the thing HOLDING the turbine moves +/obj/item/portable_wind_turbine/proc/on_move(atom/thing, atom/old_loc, dir) + SIGNAL_HANDLER + + var/mob/living/user = thing + if (!user) + return + if (isnull(cap)) + return + var/distance = get_dist(old_loc, user.loc) + var/turf/open/open_turf = get_turf(user) + if (!istype(open_turf)) + return + var/pressure_factor = open_turf.air.return_pressure() / 101.0 + if (pressure_factor <= 0) + return + var/made_full_rotation = set_rotor_tick(rotor_tick + distance * TURBINE_ANIMATION_TICKS_PER_TILE * pressure_factor) + add_power(distance * TURBINE_CHARGE_PER_TILE * pressure_factor * cap.rating) + if (!made_full_rotation) + return + if (!HAS_TRAIT(user, TRAIT_CLUMSY)) + return + var/obj/item/bodypart/head/head_to_bash = user.get_bodypart(BODY_ZONE_HEAD) + if (!head_to_bash) + return + user.Paralyze(5) + user.Knockdown(10) + user.visible_message(span_danger("[user] gets whacked in the head by the [src]'s spinning blades!"), span_userdanger("You get hit in the head by the [src] and fall over!")) + // imaginary friends call sleep in their emotes + // even though imaginary friends can't put on a wind turbine I still have to do this + INVOKE_ASYNC(user, TYPE_PROC_REF(/mob/living/, emote), "scream") + user.apply_damage(5, BRUTE, head_to_bash, attacking_item=src) + playsound(source = src, soundin = 'sound/items/weapons/smash.ogg', vol = src.get_clamped_volume(), vary = TRUE) + +/obj/item/portable_wind_turbine/wrench_act(mob/living/user, obj/item/tool) + . = NONE + switch(default_unfasten_wrench(user, tool, 4 SECONDS)) + if(SUCCESSFUL_UNFASTEN) + return ITEM_INTERACT_SUCCESS + if(FAILED_UNFASTEN) + return ITEM_INTERACT_BLOCKING + return . + +/obj/item/portable_wind_turbine/examine(mob/user) + . = ..() + + if(!in_range(user, src) && !issilicon(user) && !isobserver(user)) + . += span_warning("You're too far away to examine [src]'s contents! You can still watch it spin so wonderfully though...") + if (charging && istype(charging,/obj/item/melee/baton/security/)) + . += span_info("You can see the [charging] hanging precariously off the charging port...") + return + + if(cap) + . += span_info("Click it with a screwdriver to eject the [cap].") + . += span_info("Wrench it on a tile to anchor it and harness space wind.") + . += span_info("The wind turbine is currently storing [floor(available_power / 100) / 10]kJ.") + if(charging) + . += {"[span_notice("\The [src] contains:")] + [span_notice("- \A [charging].")]"} + +/obj/item/portable_wind_turbine/screwdriver_act(mob/living/user, obj/item/tool) + . = ..() + if(charging) + user.balloon_alert(user, "remove the [charging] first!") + return FALSE + if(cap) + tool.play_tool_sound(src, 50) + user.balloon_alert(user, "capacitor removed") + cap.forceMove(drop_location()) + available_power = 0 + cap = null + return TRUE + else + user.balloon_alert(user, "no capacitor!") + return FALSE + +/obj/item/portable_wind_turbine/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) + if(is_type_in_typecache(arrived, allowed_devices)) + charging = arrived + START_PROCESSING(SSmachines, src) + finished_recharging = FALSE + using_power = TRUE + update_appearance() + return ..() + +/obj/item/portable_wind_turbine/Exited(atom/movable/gone, direction) + if(gone == charging) + if(!QDELING(charging)) + charging.update_appearance() + charging = null + using_power = FALSE + update_appearance() + return ..() + +/obj/item/portable_wind_turbine/attackby(obj/item/attacking_item, mob/user, params) + if(istype(attacking_item, /obj/item/stock_parts/capacitor)) + if (cap) + balloon_alert(user, "already has a capacitor!") + return TRUE + user.transferItemToLoc(attacking_item, src) + cap = attacking_item + balloon_alert(user, "inserted the [attacking_item]") + return TRUE + if(!is_type_in_typecache(attacking_item, allowed_devices)) + return ..() + if(isnull(cap)) + balloon_alert(user, "no capacitor inserted!") + return TRUE + if(charging) + balloon_alert(user, "already charging something!") + return TRUE + if(istype(attacking_item, /obj/item/gun/energy)) + var/obj/item/gun/energy/energy_gun = attacking_item + if(!energy_gun.can_charge) + balloon_alert(user, "not rechargable!") + return TRUE + user.transferItemToLoc(attacking_item, src) + charging = attacking_item + return TRUE + +/obj/item/portable_wind_turbine/attack_hand(mob/user, list/modifiers) + if(loc == user || (istype(loc, /turf) && !isnull(charging))) + take_charging_out(user) + return TRUE + add_fingerprint(user) + return ..() + +/obj/item/portable_wind_turbine/handle_deconstruct(dissassembled) + charging?.forceMove(drop_location()) + cap?.forceMove(drop_location()) + return ..() + +///Takes charging item out if there is one +/obj/item/portable_wind_turbine/proc/take_charging_out(mob/user) + if(isnull(charging) || user.put_in_hands(charging)) + return + charging.forceMove(drop_location()) + update_appearance() + +/obj/item/portable_wind_turbine/attack_tk(mob/user) + if(isnull(charging)) + return + charging.forceMove(drop_location()) + return COMPONENT_CANCEL_ATTACK_CHAIN + +/obj/item/portable_wind_turbine/process(seconds_per_tick) + using_power = FALSE + if(isnull(charging)) + return PROCESS_KILL + if(isnull(cap)) + // this is easier than starting/stopping the process based on capacitor state + // this code only charges the attached item, so stopping it early should not be a problem + return + var/obj/item/stock_parts/power_store/cell/charging_cell = charging.get_cell() + if (!charging_cell && istype(charging, /obj/item/stock_parts/power_store/cell)) + charging_cell = charging + if (!charging_cell) + var/datum/component/shell/shell = charging.GetComponent(/datum/component/shell) + charging_cell = shell?.attached_circuit?.get_cell() + if(charging_cell) + var/wanted_power = min(charging_cell.maxcharge - charging_cell.charge, charging_cell.chargerate) + if(wanted_power > 0) + using_power = TRUE + var/power_to_give = min(available_power, wanted_power) * seconds_per_tick / 2 + if (power_to_give > 0) + charging_cell.give(power_to_give) + available_power -= power_to_give + update_appearance() + + if(!using_power && !finished_recharging) //Inserted thing is at max charge/ammo, notify those around us + finished_recharging = TRUE + playsound(src, 'sound/machines/ping.ogg', 30, TRUE) + say("[charging] has finished recharging!") + +/obj/item/portable_wind_turbine/emp_act(severity) + . = ..() + if (. & EMP_PROTECT_CONTENTS) + return + + if(istype(charging, /obj/item/gun/energy)) + var/obj/item/gun/energy/energy_gun = charging + energy_gun?.cell.emp_act(severity) + + else if(istype(charging, /obj/item/melee/baton/security)) + var/obj/item/melee/baton/security/batong = charging + batong?.cell.charge = 0 + +/obj/item/portable_wind_turbine/update_overlays() + . = ..() + var/mutable_appearance/rotor = mutable_appearance(worn_icon, "rotor_[floor(rotor_tick)]") + rotor.pixel_y -= 8 + . += rotor + if (isnull(charging)) + return + if (istype(charging, /obj/item/melee/baton/security/)) + . += mutable_appearance(icon, "baton") + +/obj/item/portable_wind_turbine/worn_overlays(mutable_appearance/standing, isinhands, icon_file) + . = ..() + if (isinhands) + return + var/mutable_appearance/rotor = mutable_appearance(worn_icon, "rotor_[floor(rotor_tick)]") + rotor.layer = BELOW_MOB_LAYER + if (ishuman(loc)) + var/mob/living/carbon/human/user = loc + if (user.dir & NORTH) + var/mutable_appearance/just_render_above_other_things = mutable_appearance(worn_icon, worn_icon_state) + just_render_above_other_things.layer = ABOVE_MOB_LAYER + rotor.layer = ABOVE_MOB_LAYER + . += just_render_above_other_things + . += rotor + if (isnull(charging)) + return + if (istype(charging, /obj/item/melee/baton/security/)) + var/mutable_appearance/baton_overlay = mutable_appearance(icon, "baton") + . += baton_overlay + +#undef TURBINE_MAX_STORED_POWER +#undef TURBINE_ANIMATION_TICKS_PER_TILE +#undef TURBINE_ANIMATION_TICKS +#undef TURBINE_CHARGE_PER_TILE +#undef TURBINE_ANCHORED_POWER_PER_KPA +#undef TURBINE_MIN_SECONDS_BETWEEN_SOUNDS diff --git a/code/modules/atmospherics/environmental/LINDA_turf_tile.dm b/code/modules/atmospherics/environmental/LINDA_turf_tile.dm index fbefb417c9a..827510313e2 100644 --- a/code/modules/atmospherics/environmental/LINDA_turf_tile.dm +++ b/code/modules/atmospherics/environmental/LINDA_turf_tile.dm @@ -385,8 +385,11 @@ var/atom/movable/moving_atom for(var/thing in src) moving_atom = thing - if (!moving_atom.anchored && !moving_atom.pulledby && moving_atom.last_high_pressure_movement_air_cycle < SSair.times_fired) - moving_atom.experience_pressure_difference(pressure_difference, pressure_direction) + if (moving_atom.last_high_pressure_movement_air_cycle < SSair.times_fired) + if (!moving_atom.anchored && !moving_atom.pulledby) + moving_atom.experience_pressure_difference(pressure_difference, pressure_direction) + else + SEND_SIGNAL(moving_atom, COMSIG_MOVABLE_RESISTED_SPACEWIND, pressure_difference, pressure_direction) /atom/movable ///How much delta pressure is needed for us to move diff --git a/code/modules/cargo/packs/imports.dm b/code/modules/cargo/packs/imports.dm index f11680dc0e8..41f39743935 100644 --- a/code/modules/cargo/packs/imports.dm +++ b/code/modules/cargo/packs/imports.dm @@ -65,6 +65,14 @@ contains = list(/obj/item/stack/sheet/mineral/bamboo/fifty) crate_name = "bamboo cuttings crate" +/datum/supply_pack/imports/wind_turbine + name = "Wind Turbine Crate" + desc = "Includes a portable wind turbine for charging small devices and appliances. Considered obsolete in the frontier, \ + but perfect for officers who think walking laps around the station is 'innovative engineering'." + cost = CARGO_CRATE_VALUE * 2 + contains = list(/obj/item/portable_wind_turbine/loaded) + crate_name = "wind turbine crate" + /datum/supply_pack/imports/bananium name = "A Single Sheet of Bananium" desc = "Don't let the clown know that he can order this. It costs a fortune even for this much." diff --git a/icons/mob/inhands/64x64_lefthand.dmi b/icons/mob/inhands/64x64_lefthand.dmi index a07b41d8425..1e880ce0bf7 100644 Binary files a/icons/mob/inhands/64x64_lefthand.dmi and b/icons/mob/inhands/64x64_lefthand.dmi differ diff --git a/icons/mob/inhands/64x64_righthand.dmi b/icons/mob/inhands/64x64_righthand.dmi index b1968bcab62..1f653793710 100644 Binary files a/icons/mob/inhands/64x64_righthand.dmi and b/icons/mob/inhands/64x64_righthand.dmi differ diff --git a/icons/obj/wind_turbine.dmi b/icons/obj/wind_turbine.dmi new file mode 100644 index 00000000000..ed3cec49443 Binary files /dev/null and b/icons/obj/wind_turbine.dmi differ diff --git a/sound/machines/woosh.ogg b/sound/machines/woosh.ogg new file mode 100644 index 00000000000..313d12523fd Binary files /dev/null and b/sound/machines/woosh.ogg differ diff --git a/tgstation.dme b/tgstation.dme index 4e99d488b8f..9f513a1e8bc 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2553,6 +2553,7 @@ #include "code\game\objects\items\wall_mounted.dm" #include "code\game\objects\items\weaponry.dm" #include "code\game\objects\items\wiki_manuals.dm" +#include "code\game\objects\items\wind_turbine.dm" #include "code\game\objects\items\wizard_clothes.dm" #include "code\game\objects\items\wizard_weapons.dm" #include "code\game\objects\items\AI_modules\_AI_modules.dm"