Files
Bubberstation/code/game/objects/items/wind_turbine.dm
tonyhawq 64af7e953d Adds back mounted wind turbines (#92516)
## About The Pull Request
Adds portable wind turbines that can be put in your back slot or
anchored to the ground. They accept a cell-powered item and charge it
while you walk, or when space wind passes over them. Can be purchased
for 400 credits or crafted with 3 kitchen knives, plastic, and servos.
Requires a capacitor to charge things, and higher tiers charge faster,
faster walkspeed also charges faster.

<img width="592" height="644" alt="im222age"
src="https://github.com/user-attachments/assets/e9997536-5ee0-4417-a31c-cb58666d4d07"
/>


https://github.com/user-attachments/assets/1cf7fce5-d385-4e3e-be97-fb15e253c308

## Why It's Good For The Game
Sometimes you don't have a cell charger. And you need to charge
something. Now you can charge something by running laps around the
station.
During a blob, rechargers are brought to the front lines to charge
energy guns and such but what if the blob turns off the power? And what
are bar-rp'ers to do? Kill two birds with one stone by having them run
laps instead of sitting around doing nothing.
Also its funny.

## Changelog

🆑
add: Added a portable wind turbine which can charge things when you walk
around
add: Added a signal that procs when an object resists space wind (from
being anchored / pulled)
sound: added woosh.ogg as a low "wooshing" noise
image: added a wind turbine sprite
/🆑

---------

Co-authored-by: tattle <66640614+dragomagol@users.noreply.github.com>
2025-08-31 21:13:11 +00:00

377 lines
13 KiB
Plaintext

#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